@behivetech/atoms.select 0.0.0-stage → 0.4.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/README.md +45 -2
- package/dist/index.cjs +257 -0
- package/dist/index.css +164 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +243 -0
- package/docs.json +78 -0
- package/index.ts +2 -0
- package/package.json +59 -3
- package/select.example.tsx +62 -0
- package/select.module.scss +232 -0
- package/select.tsx +127 -0
package/README.md
CHANGED
|
@@ -1,3 +1,46 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Select
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A bare select control (built on Radix's `Select`) — the Radix trigger button is this component's own root element. It has no built-in label/hint/error; wrap it in `FormField` for that chrome.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```jsx
|
|
8
|
+
import { Select } from '@behivetech/atoms.select';
|
|
9
|
+
import { FormField } from '@behivetech/forms.form-field';
|
|
10
|
+
|
|
11
|
+
const [value, setValue] = useState('');
|
|
12
|
+
const roleOptions = [
|
|
13
|
+
{ label: 'Editor', value: 'editor' },
|
|
14
|
+
{ label: 'Admin', value: 'admin' },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
<Select
|
|
18
|
+
placeholder="Choose a color"
|
|
19
|
+
options={[
|
|
20
|
+
{ label: 'Red', value: 'red' },
|
|
21
|
+
{ label: 'Green', value: 'green' },
|
|
22
|
+
{ label: 'Blue', value: 'blue' },
|
|
23
|
+
]}
|
|
24
|
+
value={value}
|
|
25
|
+
onValueChange={setValue}
|
|
26
|
+
/>
|
|
27
|
+
|
|
28
|
+
<FormField label="Role" htmlFor="role">
|
|
29
|
+
<Select id="role" options={roleOptions} value={value} onValueChange={setValue} />
|
|
30
|
+
</FormField>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Pass `aria-invalid="true"` to visually mark the field as invalid (pair with `FormField`'s `error` prop for the message).
|
|
34
|
+
|
|
35
|
+
## Props
|
|
36
|
+
|
|
37
|
+
| Prop | Type | Default | Description |
|
|
38
|
+
| --------------- | ------------------------- | ------- | ------------------------------------------------- |
|
|
39
|
+
| `options` | `SelectOption[]` | — | Array of `{ label, value, disabled? }` (required) |
|
|
40
|
+
| `value` | `string` | — | Controlled selected value |
|
|
41
|
+
| `defaultValue` | `string` | — | Uncontrolled initial value |
|
|
42
|
+
| `onValueChange` | `(value: string) => void` | — | Called when the selection changes |
|
|
43
|
+
| `placeholder` | `string` | — | Placeholder shown when nothing is selected |
|
|
44
|
+
| `disabled` | `boolean` | `false` | Disables the field |
|
|
45
|
+
| `required` | `boolean` | `false` | Marks field as required |
|
|
46
|
+
| `className` | `string` | — | Additional class names |
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
9
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
10
|
+
};
|
|
11
|
+
var __export = (target, all) => {
|
|
12
|
+
for (var name in all)
|
|
13
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
14
|
+
};
|
|
15
|
+
var __copyProps = (to, from, except, desc) => {
|
|
16
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
17
|
+
for (let key of __getOwnPropNames(from))
|
|
18
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
19
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
20
|
+
}
|
|
21
|
+
return to;
|
|
22
|
+
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
24
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
25
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
26
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
27
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
28
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
29
|
+
mod
|
|
30
|
+
));
|
|
31
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
|
+
|
|
33
|
+
// ../../../../node_modules/.pnpm/classnames@2.5.1/node_modules/classnames/index.js
|
|
34
|
+
var require_classnames = __commonJS({
|
|
35
|
+
"../../../../node_modules/.pnpm/classnames@2.5.1/node_modules/classnames/index.js"(exports2, module2) {
|
|
36
|
+
"use strict";
|
|
37
|
+
(function() {
|
|
38
|
+
"use strict";
|
|
39
|
+
var hasOwn = {}.hasOwnProperty;
|
|
40
|
+
function classNames() {
|
|
41
|
+
var classes = "";
|
|
42
|
+
for (var i = 0; i < arguments.length; i++) {
|
|
43
|
+
var arg = arguments[i];
|
|
44
|
+
if (arg) {
|
|
45
|
+
classes = appendClass(classes, parseValue(arg));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return classes;
|
|
49
|
+
}
|
|
50
|
+
function parseValue(arg) {
|
|
51
|
+
if (typeof arg === "string" || typeof arg === "number") {
|
|
52
|
+
return arg;
|
|
53
|
+
}
|
|
54
|
+
if (typeof arg !== "object") {
|
|
55
|
+
return "";
|
|
56
|
+
}
|
|
57
|
+
if (Array.isArray(arg)) {
|
|
58
|
+
return classNames.apply(null, arg);
|
|
59
|
+
}
|
|
60
|
+
if (arg.toString !== Object.prototype.toString && !arg.toString.toString().includes("[native code]")) {
|
|
61
|
+
return arg.toString();
|
|
62
|
+
}
|
|
63
|
+
var classes = "";
|
|
64
|
+
for (var key in arg) {
|
|
65
|
+
if (hasOwn.call(arg, key) && arg[key]) {
|
|
66
|
+
classes = appendClass(classes, key);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return classes;
|
|
70
|
+
}
|
|
71
|
+
function appendClass(value, newClass) {
|
|
72
|
+
if (!newClass) {
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
if (value) {
|
|
76
|
+
return value + " " + newClass;
|
|
77
|
+
}
|
|
78
|
+
return value + newClass;
|
|
79
|
+
}
|
|
80
|
+
if (typeof module2 !== "undefined" && module2.exports) {
|
|
81
|
+
classNames.default = classNames;
|
|
82
|
+
module2.exports = classNames;
|
|
83
|
+
} else if (typeof define === "function" && typeof define.amd === "object" && define.amd) {
|
|
84
|
+
define("classnames", [], function() {
|
|
85
|
+
return classNames;
|
|
86
|
+
});
|
|
87
|
+
} else {
|
|
88
|
+
window.classNames = classNames;
|
|
89
|
+
}
|
|
90
|
+
})();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// index.ts
|
|
95
|
+
var index_exports = {};
|
|
96
|
+
__export(index_exports, {
|
|
97
|
+
Select: () => Select
|
|
98
|
+
});
|
|
99
|
+
module.exports = __toCommonJS(index_exports);
|
|
100
|
+
|
|
101
|
+
// select.tsx
|
|
102
|
+
var RadixSelect = __toESM(require("@radix-ui/react-select"), 1);
|
|
103
|
+
var import_react = require("react");
|
|
104
|
+
|
|
105
|
+
// ../../../../packages/registry/class-names/dist/index.js
|
|
106
|
+
var import_classnames = __toESM(require_classnames(), 1);
|
|
107
|
+
var getClassName = ({
|
|
108
|
+
className,
|
|
109
|
+
modifiers = {},
|
|
110
|
+
rootClass,
|
|
111
|
+
styles = {}
|
|
112
|
+
}) => {
|
|
113
|
+
function getModifiers() {
|
|
114
|
+
const updatedModifiers = {};
|
|
115
|
+
const modifierKeys = Object.keys(modifiers);
|
|
116
|
+
if (modifierKeys.length) {
|
|
117
|
+
modifierKeys.forEach((modKey) => {
|
|
118
|
+
const classKey = `${rootClass}--${modKey}`;
|
|
119
|
+
const newClassName = styles[classKey] || classKey;
|
|
120
|
+
updatedModifiers[newClassName] = modifiers[modKey] ?? false;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return updatedModifiers;
|
|
124
|
+
}
|
|
125
|
+
function getChildClass(childClassName) {
|
|
126
|
+
const classKey = `${rootClass}__${childClassName}`;
|
|
127
|
+
return styles[classKey] || classKey;
|
|
128
|
+
}
|
|
129
|
+
function getRootStyles() {
|
|
130
|
+
return (0, import_classnames.default)(
|
|
131
|
+
className,
|
|
132
|
+
{
|
|
133
|
+
...getModifiers()
|
|
134
|
+
},
|
|
135
|
+
styles[rootClass] || rootClass
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return [getRootStyles(), getChildClass];
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// select.module.scss
|
|
142
|
+
var select_module_default = {
|
|
143
|
+
"BHT__Select": "BHT__Select",
|
|
144
|
+
"BHT__Select__chevron": "BHT__Select__chevron",
|
|
145
|
+
"BHT__Select__content": "BHT__Select__content",
|
|
146
|
+
"select-in": "select-in",
|
|
147
|
+
"select-out": "select-out",
|
|
148
|
+
"BHT__Select__viewport": "BHT__Select__viewport",
|
|
149
|
+
"BHT__Select__item": "BHT__Select__item",
|
|
150
|
+
"BHT__Select__item-indicator": "BHT__Select__item-indicator"
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// select.tsx
|
|
154
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
155
|
+
var Select = ({
|
|
156
|
+
options,
|
|
157
|
+
value,
|
|
158
|
+
defaultValue,
|
|
159
|
+
onValueChange,
|
|
160
|
+
placeholder,
|
|
161
|
+
disabled,
|
|
162
|
+
required,
|
|
163
|
+
className,
|
|
164
|
+
id,
|
|
165
|
+
name,
|
|
166
|
+
...rest
|
|
167
|
+
}) => {
|
|
168
|
+
const generatedId = (0, import_react.useId)();
|
|
169
|
+
const selectId = id ?? generatedId;
|
|
170
|
+
const [rootClass, getChildClass] = getClassName({
|
|
171
|
+
className,
|
|
172
|
+
rootClass: "BHT__Select",
|
|
173
|
+
styles: select_module_default
|
|
174
|
+
});
|
|
175
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
176
|
+
RadixSelect.Root,
|
|
177
|
+
{
|
|
178
|
+
value,
|
|
179
|
+
defaultValue,
|
|
180
|
+
onValueChange,
|
|
181
|
+
disabled,
|
|
182
|
+
required,
|
|
183
|
+
name,
|
|
184
|
+
children: [
|
|
185
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(RadixSelect.Trigger, { ...rest, id: selectId, className: rootClass, children: [
|
|
186
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(RadixSelect.Value, { placeholder }),
|
|
187
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(RadixSelect.Icon, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
188
|
+
"svg",
|
|
189
|
+
{
|
|
190
|
+
className: getChildClass("chevron"),
|
|
191
|
+
viewBox: "0 0 12 8",
|
|
192
|
+
fill: "none",
|
|
193
|
+
stroke: "currentColor",
|
|
194
|
+
strokeWidth: "2",
|
|
195
|
+
strokeLinecap: "round",
|
|
196
|
+
strokeLinejoin: "round",
|
|
197
|
+
"aria-hidden": "true",
|
|
198
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M1 1.5L6 6.5L11 1.5" })
|
|
199
|
+
}
|
|
200
|
+
) })
|
|
201
|
+
] }),
|
|
202
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(RadixSelect.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
203
|
+
RadixSelect.Content,
|
|
204
|
+
{
|
|
205
|
+
className: getChildClass("content"),
|
|
206
|
+
position: "popper",
|
|
207
|
+
sideOffset: 4,
|
|
208
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RadixSelect.Viewport, { className: getChildClass("viewport"), children: options.map((option) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
209
|
+
RadixSelect.Item,
|
|
210
|
+
{
|
|
211
|
+
value: option.value,
|
|
212
|
+
disabled: option.disabled,
|
|
213
|
+
className: getChildClass("item"),
|
|
214
|
+
children: [
|
|
215
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(RadixSelect.ItemText, { children: option.label }),
|
|
216
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
217
|
+
RadixSelect.ItemIndicator,
|
|
218
|
+
{
|
|
219
|
+
className: getChildClass("item-indicator"),
|
|
220
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
221
|
+
"svg",
|
|
222
|
+
{
|
|
223
|
+
viewBox: "0 0 12 10",
|
|
224
|
+
fill: "none",
|
|
225
|
+
stroke: "currentColor",
|
|
226
|
+
strokeWidth: "2.5",
|
|
227
|
+
strokeLinecap: "round",
|
|
228
|
+
strokeLinejoin: "round",
|
|
229
|
+
"aria-hidden": "true",
|
|
230
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M1 5L4.5 8.5L11 1" })
|
|
231
|
+
}
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
)
|
|
235
|
+
]
|
|
236
|
+
},
|
|
237
|
+
option.value
|
|
238
|
+
)) })
|
|
239
|
+
}
|
|
240
|
+
) })
|
|
241
|
+
]
|
|
242
|
+
}
|
|
243
|
+
);
|
|
244
|
+
};
|
|
245
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
246
|
+
0 && (module.exports = {
|
|
247
|
+
Select
|
|
248
|
+
});
|
|
249
|
+
/*! Bundled license information:
|
|
250
|
+
|
|
251
|
+
classnames/index.js:
|
|
252
|
+
(*!
|
|
253
|
+
Copyright (c) 2018 Jed Watson.
|
|
254
|
+
Licensed under the MIT License (MIT), see
|
|
255
|
+
http://jedwatson.github.io/classnames
|
|
256
|
+
*)
|
|
257
|
+
*/
|
package/dist/index.css
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/* esbuild-sass-plugin:css-chunkselect.module.scss */
|
|
2
|
+
.BHT__Select {
|
|
3
|
+
position: relative;
|
|
4
|
+
display: flex;
|
|
5
|
+
align-items: center;
|
|
6
|
+
justify-content: space-between;
|
|
7
|
+
gap: var(--spacing-2, 0.5rem);
|
|
8
|
+
width: 100%;
|
|
9
|
+
min-height: 2.5rem;
|
|
10
|
+
padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);
|
|
11
|
+
font-family: var(--md-sys-typescale-body-large-font, var(--font-body));
|
|
12
|
+
font-size: var(--md-sys-typescale-body-large-size, 1rem);
|
|
13
|
+
font-weight: var(--md-sys-typescale-body-large-weight, 400);
|
|
14
|
+
line-height: var(--md-sys-typescale-body-large-line-height, 1.5rem);
|
|
15
|
+
letter-spacing: var(--md-sys-typescale-body-large-tracking, 0.0313em);
|
|
16
|
+
color: var(--md-sys-color-on-surface, var(--color-on-surface));
|
|
17
|
+
background: transparent;
|
|
18
|
+
border: 1px solid var(--md-sys-color-outline, var(--color-border-strong));
|
|
19
|
+
border-radius: var(--md-sys-shape-corner-extra-small, 4px);
|
|
20
|
+
cursor: pointer;
|
|
21
|
+
text-align: left;
|
|
22
|
+
transition: border-color var(--md-sys-motion-duration-short-3, 150ms) var(--md-sys-motion-easing-standard, ease), box-shadow var(--md-sys-motion-duration-short-3, 150ms) var(--md-sys-motion-easing-standard, ease);
|
|
23
|
+
}
|
|
24
|
+
.BHT__Select::before {
|
|
25
|
+
content: "";
|
|
26
|
+
position: absolute;
|
|
27
|
+
left: 0;
|
|
28
|
+
right: 0;
|
|
29
|
+
top: 50%;
|
|
30
|
+
translate: 0 -50%;
|
|
31
|
+
height: 100%;
|
|
32
|
+
min-height: 48px;
|
|
33
|
+
}
|
|
34
|
+
.BHT__Select[data-placeholder] {
|
|
35
|
+
color: var(--md-sys-color-on-surface-variant, var(--color-text-muted));
|
|
36
|
+
}
|
|
37
|
+
.BHT__Select:hover:not([data-disabled], [data-state=open], :focus-visible) {
|
|
38
|
+
border-color: var(--md-sys-color-on-surface, var(--color-on-surface));
|
|
39
|
+
}
|
|
40
|
+
.BHT__Select[data-state=open],
|
|
41
|
+
.BHT__Select:focus-visible {
|
|
42
|
+
outline: none;
|
|
43
|
+
border-color: var(--md-sys-color-primary, var(--color-border-focus));
|
|
44
|
+
box-shadow: inset 0 0 0 1px var(--md-sys-color-primary, var(--color-border-focus));
|
|
45
|
+
}
|
|
46
|
+
.BHT__Select[data-state=open] .BHT__Select__chevron {
|
|
47
|
+
transform: rotate(180deg);
|
|
48
|
+
}
|
|
49
|
+
.BHT__Select[data-disabled] {
|
|
50
|
+
opacity: 0.45;
|
|
51
|
+
cursor: not-allowed;
|
|
52
|
+
}
|
|
53
|
+
.BHT__Select[aria-invalid=true] {
|
|
54
|
+
border-color: var(--md-sys-color-error, var(--color-error));
|
|
55
|
+
}
|
|
56
|
+
.BHT__Select[aria-invalid=true]:hover:not([data-disabled], [data-state=open], :focus-visible) {
|
|
57
|
+
border-color: var(--md-sys-color-on-error-container, var(--color-error));
|
|
58
|
+
}
|
|
59
|
+
.BHT__Select[aria-invalid=true][data-state=open],
|
|
60
|
+
.BHT__Select[aria-invalid=true]:focus-visible {
|
|
61
|
+
border-color: var(--md-sys-color-error, var(--color-error));
|
|
62
|
+
box-shadow: inset 0 0 0 1px var(--md-sys-color-error, var(--color-error));
|
|
63
|
+
}
|
|
64
|
+
.BHT__Select__chevron {
|
|
65
|
+
width: 0.75rem;
|
|
66
|
+
height: 0.75rem;
|
|
67
|
+
flex-shrink: 0;
|
|
68
|
+
color: var(--md-sys-color-on-surface-variant, var(--color-text-muted));
|
|
69
|
+
transition: transform var(--md-sys-motion-duration-short-3, 150ms) var(--md-sys-motion-easing-standard, ease);
|
|
70
|
+
}
|
|
71
|
+
.BHT__Select__content {
|
|
72
|
+
background: var(--md-sys-color-surface-container, var(--color-surface-raised));
|
|
73
|
+
border-radius: var(--md-sys-shape-corner-extra-small, 4px);
|
|
74
|
+
box-shadow: var(--md-sys-elevation-2, var(--shadow-md));
|
|
75
|
+
overflow: hidden;
|
|
76
|
+
z-index: var(--z-dropdown, 450);
|
|
77
|
+
min-width: var(--radix-select-trigger-width);
|
|
78
|
+
max-height: var(--radix-select-content-available-height);
|
|
79
|
+
}
|
|
80
|
+
.BHT__Select__content[data-state=open] {
|
|
81
|
+
animation: select-in var(--md-sys-motion-duration-short-3, 150ms) var(--md-sys-motion-easing-standard-decelerate, ease-out);
|
|
82
|
+
}
|
|
83
|
+
.BHT__Select__content[data-state=closed] {
|
|
84
|
+
animation: select-out var(--md-sys-motion-duration-short-2, 100ms) var(--md-sys-motion-easing-standard-accelerate, ease-in);
|
|
85
|
+
}
|
|
86
|
+
.BHT__Select__viewport {
|
|
87
|
+
padding: var(--spacing-1, 0.25rem);
|
|
88
|
+
}
|
|
89
|
+
.BHT__Select__item {
|
|
90
|
+
position: relative;
|
|
91
|
+
isolation: isolate;
|
|
92
|
+
display: flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
justify-content: space-between;
|
|
95
|
+
gap: var(--spacing-2, 0.5rem);
|
|
96
|
+
padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);
|
|
97
|
+
border-radius: var(--md-sys-shape-corner-extra-small, 4px);
|
|
98
|
+
font-family: var(--md-sys-typescale-label-large-font, var(--font-body));
|
|
99
|
+
font-size: var(--md-sys-typescale-label-large-size, 0.875rem);
|
|
100
|
+
font-weight: var(--md-sys-typescale-label-large-weight, 500);
|
|
101
|
+
line-height: var(--md-sys-typescale-label-large-line-height, 1.25rem);
|
|
102
|
+
letter-spacing: var(--md-sys-typescale-label-large-tracking, 0.0071em);
|
|
103
|
+
color: var(--md-sys-color-on-surface, var(--color-on-surface));
|
|
104
|
+
cursor: pointer;
|
|
105
|
+
user-select: none;
|
|
106
|
+
outline: none;
|
|
107
|
+
}
|
|
108
|
+
.BHT__Select__item::after {
|
|
109
|
+
content: "";
|
|
110
|
+
position: absolute;
|
|
111
|
+
inset: 0;
|
|
112
|
+
border-radius: inherit;
|
|
113
|
+
background: currentColor;
|
|
114
|
+
opacity: 0;
|
|
115
|
+
pointer-events: none;
|
|
116
|
+
transition: opacity var(--md-sys-motion-duration-short-2, 100ms) var(--md-sys-motion-easing-standard, ease);
|
|
117
|
+
}
|
|
118
|
+
.BHT__Select__item[data-highlighted]::after {
|
|
119
|
+
opacity: var(--md-sys-state-hover-opacity, 0.08);
|
|
120
|
+
}
|
|
121
|
+
.BHT__Select__item:active::after {
|
|
122
|
+
opacity: var(--md-sys-state-pressed-opacity, 0.1);
|
|
123
|
+
}
|
|
124
|
+
.BHT__Select__item[data-state=checked] {
|
|
125
|
+
background: var(--md-sys-color-secondary-container, var(--color-surface-light));
|
|
126
|
+
color: var(--md-sys-color-on-secondary-container, var(--color-on-surface));
|
|
127
|
+
}
|
|
128
|
+
.BHT__Select__item[data-disabled] {
|
|
129
|
+
opacity: 0.4;
|
|
130
|
+
cursor: not-allowed;
|
|
131
|
+
pointer-events: none;
|
|
132
|
+
}
|
|
133
|
+
.BHT__Select__item-indicator {
|
|
134
|
+
width: 0.75rem;
|
|
135
|
+
height: 0.75rem;
|
|
136
|
+
flex-shrink: 0;
|
|
137
|
+
display: flex;
|
|
138
|
+
align-items: center;
|
|
139
|
+
justify-content: center;
|
|
140
|
+
}
|
|
141
|
+
.BHT__Select__item-indicator svg {
|
|
142
|
+
width: 100%;
|
|
143
|
+
height: 100%;
|
|
144
|
+
}
|
|
145
|
+
@keyframes select-in {
|
|
146
|
+
from {
|
|
147
|
+
opacity: 0;
|
|
148
|
+
transform: translateY(-0.25rem);
|
|
149
|
+
}
|
|
150
|
+
to {
|
|
151
|
+
opacity: 1;
|
|
152
|
+
transform: translateY(0);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
@keyframes select-out {
|
|
156
|
+
from {
|
|
157
|
+
opacity: 1;
|
|
158
|
+
transform: translateY(0);
|
|
159
|
+
}
|
|
160
|
+
to {
|
|
161
|
+
opacity: 0;
|
|
162
|
+
transform: translateY(-0.25rem);
|
|
163
|
+
}
|
|
164
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import * as RadixSelect from '@radix-ui/react-select';
|
|
3
|
+
|
|
4
|
+
interface SelectOption {
|
|
5
|
+
/** Text shown for this option */
|
|
6
|
+
label: string;
|
|
7
|
+
/** Value submitted/reported when this option is chosen */
|
|
8
|
+
value: string;
|
|
9
|
+
/** Prevents this option from being selected */
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
}
|
|
12
|
+
interface SelectProps extends Omit<RadixSelect.SelectTriggerProps, "className"> {
|
|
13
|
+
/** Options rendered in the dropdown */
|
|
14
|
+
options: SelectOption[];
|
|
15
|
+
/** Controlled selected value */
|
|
16
|
+
value?: string;
|
|
17
|
+
/** Initial selected value for uncontrolled usage */
|
|
18
|
+
defaultValue?: string;
|
|
19
|
+
/** Called with the new value when the selection changes */
|
|
20
|
+
onValueChange?: (value: string) => void;
|
|
21
|
+
/** Text shown when no value is selected */
|
|
22
|
+
placeholder?: string;
|
|
23
|
+
/** Marks the field as required for form validation */
|
|
24
|
+
required?: boolean;
|
|
25
|
+
/** Additional class names to merge with the component root element */
|
|
26
|
+
className?: string;
|
|
27
|
+
/** Form field name submitted with the value */
|
|
28
|
+
name?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Bare select control — the Radix select trigger button is this component's
|
|
32
|
+
* own root element, no built-in label/hint/error. Wrap it in `FormField` for
|
|
33
|
+
* that chrome: `<FormField label="..." htmlFor="x"><Select id="x" .../></FormField>`.
|
|
34
|
+
*/
|
|
35
|
+
declare const Select: ({ options, value, defaultValue, onValueChange, placeholder, disabled, required, className, id, name, ...rest }: SelectProps) => react.JSX.Element;
|
|
36
|
+
|
|
37
|
+
export { Select, type SelectOption, type SelectProps };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
8
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
19
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
20
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
21
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
22
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
23
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
24
|
+
mod
|
|
25
|
+
));
|
|
26
|
+
|
|
27
|
+
// ../../../../node_modules/.pnpm/classnames@2.5.1/node_modules/classnames/index.js
|
|
28
|
+
var require_classnames = __commonJS({
|
|
29
|
+
"../../../../node_modules/.pnpm/classnames@2.5.1/node_modules/classnames/index.js"(exports, module) {
|
|
30
|
+
"use strict";
|
|
31
|
+
(function() {
|
|
32
|
+
"use strict";
|
|
33
|
+
var hasOwn = {}.hasOwnProperty;
|
|
34
|
+
function classNames() {
|
|
35
|
+
var classes = "";
|
|
36
|
+
for (var i = 0; i < arguments.length; i++) {
|
|
37
|
+
var arg = arguments[i];
|
|
38
|
+
if (arg) {
|
|
39
|
+
classes = appendClass(classes, parseValue(arg));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return classes;
|
|
43
|
+
}
|
|
44
|
+
function parseValue(arg) {
|
|
45
|
+
if (typeof arg === "string" || typeof arg === "number") {
|
|
46
|
+
return arg;
|
|
47
|
+
}
|
|
48
|
+
if (typeof arg !== "object") {
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
if (Array.isArray(arg)) {
|
|
52
|
+
return classNames.apply(null, arg);
|
|
53
|
+
}
|
|
54
|
+
if (arg.toString !== Object.prototype.toString && !arg.toString.toString().includes("[native code]")) {
|
|
55
|
+
return arg.toString();
|
|
56
|
+
}
|
|
57
|
+
var classes = "";
|
|
58
|
+
for (var key in arg) {
|
|
59
|
+
if (hasOwn.call(arg, key) && arg[key]) {
|
|
60
|
+
classes = appendClass(classes, key);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return classes;
|
|
64
|
+
}
|
|
65
|
+
function appendClass(value, newClass) {
|
|
66
|
+
if (!newClass) {
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
if (value) {
|
|
70
|
+
return value + " " + newClass;
|
|
71
|
+
}
|
|
72
|
+
return value + newClass;
|
|
73
|
+
}
|
|
74
|
+
if (typeof module !== "undefined" && module.exports) {
|
|
75
|
+
classNames.default = classNames;
|
|
76
|
+
module.exports = classNames;
|
|
77
|
+
} else if (typeof define === "function" && typeof define.amd === "object" && define.amd) {
|
|
78
|
+
define("classnames", [], function() {
|
|
79
|
+
return classNames;
|
|
80
|
+
});
|
|
81
|
+
} else {
|
|
82
|
+
window.classNames = classNames;
|
|
83
|
+
}
|
|
84
|
+
})();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// select.tsx
|
|
89
|
+
import * as RadixSelect from "@radix-ui/react-select";
|
|
90
|
+
import { useId } from "react";
|
|
91
|
+
|
|
92
|
+
// ../../../../packages/registry/class-names/dist/index.js
|
|
93
|
+
var import_classnames = __toESM(require_classnames(), 1);
|
|
94
|
+
var getClassName = ({
|
|
95
|
+
className,
|
|
96
|
+
modifiers = {},
|
|
97
|
+
rootClass,
|
|
98
|
+
styles = {}
|
|
99
|
+
}) => {
|
|
100
|
+
function getModifiers() {
|
|
101
|
+
const updatedModifiers = {};
|
|
102
|
+
const modifierKeys = Object.keys(modifiers);
|
|
103
|
+
if (modifierKeys.length) {
|
|
104
|
+
modifierKeys.forEach((modKey) => {
|
|
105
|
+
const classKey = `${rootClass}--${modKey}`;
|
|
106
|
+
const newClassName = styles[classKey] || classKey;
|
|
107
|
+
updatedModifiers[newClassName] = modifiers[modKey] ?? false;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return updatedModifiers;
|
|
111
|
+
}
|
|
112
|
+
function getChildClass(childClassName) {
|
|
113
|
+
const classKey = `${rootClass}__${childClassName}`;
|
|
114
|
+
return styles[classKey] || classKey;
|
|
115
|
+
}
|
|
116
|
+
function getRootStyles() {
|
|
117
|
+
return (0, import_classnames.default)(
|
|
118
|
+
className,
|
|
119
|
+
{
|
|
120
|
+
...getModifiers()
|
|
121
|
+
},
|
|
122
|
+
styles[rootClass] || rootClass
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return [getRootStyles(), getChildClass];
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// select.module.scss
|
|
129
|
+
var select_module_default = {
|
|
130
|
+
"BHT__Select": "BHT__Select",
|
|
131
|
+
"BHT__Select__chevron": "BHT__Select__chevron",
|
|
132
|
+
"BHT__Select__content": "BHT__Select__content",
|
|
133
|
+
"select-in": "select-in",
|
|
134
|
+
"select-out": "select-out",
|
|
135
|
+
"BHT__Select__viewport": "BHT__Select__viewport",
|
|
136
|
+
"BHT__Select__item": "BHT__Select__item",
|
|
137
|
+
"BHT__Select__item-indicator": "BHT__Select__item-indicator"
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// select.tsx
|
|
141
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
142
|
+
var Select = ({
|
|
143
|
+
options,
|
|
144
|
+
value,
|
|
145
|
+
defaultValue,
|
|
146
|
+
onValueChange,
|
|
147
|
+
placeholder,
|
|
148
|
+
disabled,
|
|
149
|
+
required,
|
|
150
|
+
className,
|
|
151
|
+
id,
|
|
152
|
+
name,
|
|
153
|
+
...rest
|
|
154
|
+
}) => {
|
|
155
|
+
const generatedId = useId();
|
|
156
|
+
const selectId = id ?? generatedId;
|
|
157
|
+
const [rootClass, getChildClass] = getClassName({
|
|
158
|
+
className,
|
|
159
|
+
rootClass: "BHT__Select",
|
|
160
|
+
styles: select_module_default
|
|
161
|
+
});
|
|
162
|
+
return /* @__PURE__ */ jsxs(
|
|
163
|
+
RadixSelect.Root,
|
|
164
|
+
{
|
|
165
|
+
value,
|
|
166
|
+
defaultValue,
|
|
167
|
+
onValueChange,
|
|
168
|
+
disabled,
|
|
169
|
+
required,
|
|
170
|
+
name,
|
|
171
|
+
children: [
|
|
172
|
+
/* @__PURE__ */ jsxs(RadixSelect.Trigger, { ...rest, id: selectId, className: rootClass, children: [
|
|
173
|
+
/* @__PURE__ */ jsx(RadixSelect.Value, { placeholder }),
|
|
174
|
+
/* @__PURE__ */ jsx(RadixSelect.Icon, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
175
|
+
"svg",
|
|
176
|
+
{
|
|
177
|
+
className: getChildClass("chevron"),
|
|
178
|
+
viewBox: "0 0 12 8",
|
|
179
|
+
fill: "none",
|
|
180
|
+
stroke: "currentColor",
|
|
181
|
+
strokeWidth: "2",
|
|
182
|
+
strokeLinecap: "round",
|
|
183
|
+
strokeLinejoin: "round",
|
|
184
|
+
"aria-hidden": "true",
|
|
185
|
+
children: /* @__PURE__ */ jsx("path", { d: "M1 1.5L6 6.5L11 1.5" })
|
|
186
|
+
}
|
|
187
|
+
) })
|
|
188
|
+
] }),
|
|
189
|
+
/* @__PURE__ */ jsx(RadixSelect.Portal, { children: /* @__PURE__ */ jsx(
|
|
190
|
+
RadixSelect.Content,
|
|
191
|
+
{
|
|
192
|
+
className: getChildClass("content"),
|
|
193
|
+
position: "popper",
|
|
194
|
+
sideOffset: 4,
|
|
195
|
+
children: /* @__PURE__ */ jsx(RadixSelect.Viewport, { className: getChildClass("viewport"), children: options.map((option) => /* @__PURE__ */ jsxs(
|
|
196
|
+
RadixSelect.Item,
|
|
197
|
+
{
|
|
198
|
+
value: option.value,
|
|
199
|
+
disabled: option.disabled,
|
|
200
|
+
className: getChildClass("item"),
|
|
201
|
+
children: [
|
|
202
|
+
/* @__PURE__ */ jsx(RadixSelect.ItemText, { children: option.label }),
|
|
203
|
+
/* @__PURE__ */ jsx(
|
|
204
|
+
RadixSelect.ItemIndicator,
|
|
205
|
+
{
|
|
206
|
+
className: getChildClass("item-indicator"),
|
|
207
|
+
children: /* @__PURE__ */ jsx(
|
|
208
|
+
"svg",
|
|
209
|
+
{
|
|
210
|
+
viewBox: "0 0 12 10",
|
|
211
|
+
fill: "none",
|
|
212
|
+
stroke: "currentColor",
|
|
213
|
+
strokeWidth: "2.5",
|
|
214
|
+
strokeLinecap: "round",
|
|
215
|
+
strokeLinejoin: "round",
|
|
216
|
+
"aria-hidden": "true",
|
|
217
|
+
children: /* @__PURE__ */ jsx("path", { d: "M1 5L4.5 8.5L11 1" })
|
|
218
|
+
}
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
]
|
|
223
|
+
},
|
|
224
|
+
option.value
|
|
225
|
+
)) })
|
|
226
|
+
}
|
|
227
|
+
) })
|
|
228
|
+
]
|
|
229
|
+
}
|
|
230
|
+
);
|
|
231
|
+
};
|
|
232
|
+
export {
|
|
233
|
+
Select
|
|
234
|
+
};
|
|
235
|
+
/*! Bundled license information:
|
|
236
|
+
|
|
237
|
+
classnames/index.js:
|
|
238
|
+
(*!
|
|
239
|
+
Copyright (c) 2018 Jed Watson.
|
|
240
|
+
Licensed under the MIT License (MIT), see
|
|
241
|
+
http://jedwatson.github.io/classnames
|
|
242
|
+
*)
|
|
243
|
+
*/
|
package/docs.json
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://github.com/behivetech/bht-component-registry/docs.schema.json",
|
|
3
|
+
"version": 2,
|
|
4
|
+
"generatedAt": "2026-10-20T05:49:58.892Z",
|
|
5
|
+
"docs": [
|
|
6
|
+
{
|
|
7
|
+
"displayName": "Select",
|
|
8
|
+
"description": "Bare select control — the Radix select trigger button is this component's\nown root element, no built-in label/hint/error. Wrap it in `FormField` for\nthat chrome: `<FormField label=\"...\" htmlFor=\"x\"><Select id=\"x\" .../></FormField>`.",
|
|
9
|
+
"extendsNative": true,
|
|
10
|
+
"props": [
|
|
11
|
+
{
|
|
12
|
+
"name": "options",
|
|
13
|
+
"type": "SelectOption[]",
|
|
14
|
+
"required": true,
|
|
15
|
+
"defaultValue": null,
|
|
16
|
+
"description": "Options rendered in the dropdown"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"name": "asChild",
|
|
20
|
+
"type": "boolean",
|
|
21
|
+
"required": false,
|
|
22
|
+
"defaultValue": null,
|
|
23
|
+
"description": ""
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"name": "className",
|
|
27
|
+
"type": "string",
|
|
28
|
+
"required": false,
|
|
29
|
+
"defaultValue": null,
|
|
30
|
+
"description": "Additional class names to merge with the component root element"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "defaultValue",
|
|
34
|
+
"type": "string",
|
|
35
|
+
"required": false,
|
|
36
|
+
"defaultValue": null,
|
|
37
|
+
"description": "Initial selected value for uncontrolled usage"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"name": "name",
|
|
41
|
+
"type": "string",
|
|
42
|
+
"required": false,
|
|
43
|
+
"defaultValue": null,
|
|
44
|
+
"description": "Form field name submitted with the value"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"name": "onValueChange",
|
|
48
|
+
"type": "((value: string) => void)",
|
|
49
|
+
"required": false,
|
|
50
|
+
"defaultValue": null,
|
|
51
|
+
"description": "Called with the new value when the selection changes"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"name": "placeholder",
|
|
55
|
+
"type": "string",
|
|
56
|
+
"required": false,
|
|
57
|
+
"defaultValue": null,
|
|
58
|
+
"description": "Text shown when no value is selected"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"name": "required",
|
|
62
|
+
"type": "boolean",
|
|
63
|
+
"required": false,
|
|
64
|
+
"defaultValue": null,
|
|
65
|
+
"description": "Marks the field as required for form validation"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"name": "value",
|
|
69
|
+
"type": "string",
|
|
70
|
+
"required": false,
|
|
71
|
+
"defaultValue": null,
|
|
72
|
+
"description": "Controlled selected value"
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
],
|
|
77
|
+
"functions": []
|
|
78
|
+
}
|
package/index.ts
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,62 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@behivetech/atoms.select",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.4.2",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": false,
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"require": "./dist/index.cjs"
|
|
12
|
+
},
|
|
13
|
+
"./styles.css": "./dist/index.css"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md",
|
|
18
|
+
"index.ts",
|
|
19
|
+
"select.tsx",
|
|
20
|
+
"select.module.scss",
|
|
21
|
+
"*.example.tsx",
|
|
22
|
+
"docs.json"
|
|
23
|
+
],
|
|
24
|
+
"registry": {
|
|
25
|
+
"status": "stable",
|
|
26
|
+
"tags": []
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@radix-ui/react-select": "^2.1.0",
|
|
30
|
+
"react": "^19.2.7",
|
|
31
|
+
"react-dom": "^19.2.7"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@radix-ui/react-select": "^2.3.7",
|
|
35
|
+
"@types/react": "19.2.17",
|
|
36
|
+
"@types/react-dom": "19.2.3",
|
|
37
|
+
"eslint": "^9.39.5",
|
|
38
|
+
"tsup": "^8.5.1",
|
|
39
|
+
"typescript": "^6.0.3",
|
|
40
|
+
"vitest": "^4.1.10",
|
|
41
|
+
"@behivetech/forms.form-field": "0.3.2",
|
|
42
|
+
"@bht-component-registry/class-names": "0.0.6",
|
|
43
|
+
"@bht-component-registry/env": "0.1.0"
|
|
44
|
+
},
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "https://github.com/behivetech/bht-galaxy.git",
|
|
48
|
+
"directory": "registry/behivetech/atoms/select"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"registry": "https://registry.npmjs.org",
|
|
52
|
+
"access": "public",
|
|
53
|
+
"provenance": true
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "tsup && bht-registry-docgen",
|
|
57
|
+
"dev": "tsup --watch",
|
|
58
|
+
"lint": "eslint . --max-warnings 0",
|
|
59
|
+
"check-types": "tsc --noEmit",
|
|
60
|
+
"test": "vitest run"
|
|
61
|
+
}
|
|
6
62
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { FormField } from "@behivetech/forms.form-field";
|
|
3
|
+
import { Select } from "./select.js";
|
|
4
|
+
|
|
5
|
+
const colorOptions = [
|
|
6
|
+
{ label: "Red", value: "red" },
|
|
7
|
+
{ label: "Green", value: "green" },
|
|
8
|
+
{ label: "Blue", value: "blue" },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const roleOptions = [
|
|
12
|
+
{ label: "Admin", value: "admin" },
|
|
13
|
+
{ label: "Editor", value: "editor" },
|
|
14
|
+
{ label: "Viewer", value: "viewer" },
|
|
15
|
+
{ label: "Guest (disabled)", value: "guest", disabled: true },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export const BasicSelect = () => {
|
|
19
|
+
const [value, setValue] = useState("");
|
|
20
|
+
return (
|
|
21
|
+
<Select
|
|
22
|
+
placeholder="Choose a color"
|
|
23
|
+
options={colorOptions}
|
|
24
|
+
value={value}
|
|
25
|
+
onValueChange={setValue}
|
|
26
|
+
/>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const SelectWithFormField = () => {
|
|
31
|
+
const [value, setValue] = useState("");
|
|
32
|
+
return (
|
|
33
|
+
<FormField
|
|
34
|
+
label="Role"
|
|
35
|
+
htmlFor="role"
|
|
36
|
+
hint="Determines what the user can access."
|
|
37
|
+
>
|
|
38
|
+
<Select
|
|
39
|
+
id="role"
|
|
40
|
+
placeholder="Select a role"
|
|
41
|
+
options={roleOptions}
|
|
42
|
+
value={value}
|
|
43
|
+
onValueChange={setValue}
|
|
44
|
+
/>
|
|
45
|
+
</FormField>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const SelectWithError = () => (
|
|
50
|
+
<FormField label="Status" htmlFor="status" error="Please select a status.">
|
|
51
|
+
<Select
|
|
52
|
+
id="status"
|
|
53
|
+
aria-invalid="true"
|
|
54
|
+
placeholder="Select status"
|
|
55
|
+
options={colorOptions}
|
|
56
|
+
/>
|
|
57
|
+
</FormField>
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
export const DisabledSelect = () => (
|
|
61
|
+
<Select disabled options={colorOptions} value="red" />
|
|
62
|
+
);
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// Material Design 3 outlined select: the trigger is an outlined text field and
|
|
2
|
+
// the popup is a menu.
|
|
3
|
+
//
|
|
4
|
+
// The trigger's 2px focus/open edge is a 1px inset shadow on top of the 1px
|
|
5
|
+
// border rather than a wider border, so the value never shifts.
|
|
6
|
+
|
|
7
|
+
.BHT__Select {
|
|
8
|
+
// Radix Select.Trigger renders as <button>
|
|
9
|
+
position: relative;
|
|
10
|
+
display: flex;
|
|
11
|
+
align-items: center;
|
|
12
|
+
justify-content: space-between;
|
|
13
|
+
gap: var(--spacing-2, 0.5rem);
|
|
14
|
+
width: 100%;
|
|
15
|
+
min-height: 2.5rem;
|
|
16
|
+
padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);
|
|
17
|
+
font-family: var(--md-sys-typescale-body-large-font, var(--font-body));
|
|
18
|
+
font-size: var(--md-sys-typescale-body-large-size, 1rem);
|
|
19
|
+
font-weight: var(--md-sys-typescale-body-large-weight, 400);
|
|
20
|
+
line-height: var(--md-sys-typescale-body-large-line-height, 1.5rem);
|
|
21
|
+
letter-spacing: var(--md-sys-typescale-body-large-tracking, 0.0313em);
|
|
22
|
+
color: var(--md-sys-color-on-surface, var(--color-on-surface));
|
|
23
|
+
// Outlined fields have no container fill; they take the surface they sit on.
|
|
24
|
+
background: transparent;
|
|
25
|
+
border: 1px solid var(--md-sys-color-outline, var(--color-border-strong));
|
|
26
|
+
border-radius: var(--md-sys-shape-corner-extra-small, 4px);
|
|
27
|
+
cursor: pointer;
|
|
28
|
+
text-align: left;
|
|
29
|
+
transition:
|
|
30
|
+
border-color var(--md-sys-motion-duration-short-3, 150ms)
|
|
31
|
+
var(--md-sys-motion-easing-standard, ease),
|
|
32
|
+
box-shadow var(--md-sys-motion-duration-short-3, 150ms)
|
|
33
|
+
var(--md-sys-motion-easing-standard, ease);
|
|
34
|
+
|
|
35
|
+
// Touch target: extends the hit area to 48px without changing the drawn
|
|
36
|
+
// 40px height. px on purpose — a finger doesn't scale with font size.
|
|
37
|
+
&::before {
|
|
38
|
+
content: "";
|
|
39
|
+
position: absolute;
|
|
40
|
+
left: 0;
|
|
41
|
+
right: 0;
|
|
42
|
+
top: 50%;
|
|
43
|
+
translate: 0 -50%;
|
|
44
|
+
height: 100%;
|
|
45
|
+
min-height: 48px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
&[data-placeholder] {
|
|
49
|
+
color: var(--md-sys-color-on-surface-variant, var(--color-text-muted));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
&:hover:not([data-disabled], [data-state="open"], :focus-visible) {
|
|
53
|
+
border-color: var(--md-sys-color-on-surface, var(--color-on-surface));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
&[data-state="open"],
|
|
57
|
+
&:focus-visible {
|
|
58
|
+
outline: none;
|
|
59
|
+
border-color: var(--md-sys-color-primary, var(--color-border-focus));
|
|
60
|
+
box-shadow: inset 0 0 0 1px
|
|
61
|
+
var(--md-sys-color-primary, var(--color-border-focus));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
&[data-state="open"] {
|
|
65
|
+
.BHT__Select {
|
|
66
|
+
&__chevron {
|
|
67
|
+
transform: rotate(180deg);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
&[data-disabled] {
|
|
73
|
+
opacity: 0.45;
|
|
74
|
+
cursor: not-allowed;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
&[aria-invalid="true"] {
|
|
78
|
+
border-color: var(--md-sys-color-error, var(--color-error));
|
|
79
|
+
|
|
80
|
+
&:hover:not([data-disabled], [data-state="open"], :focus-visible) {
|
|
81
|
+
border-color: var(--md-sys-color-on-error-container, var(--color-error));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
&[data-state="open"],
|
|
85
|
+
&:focus-visible {
|
|
86
|
+
border-color: var(--md-sys-color-error, var(--color-error));
|
|
87
|
+
box-shadow: inset 0 0 0 1px var(--md-sys-color-error, var(--color-error));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
&__chevron {
|
|
92
|
+
width: 0.75rem;
|
|
93
|
+
height: 0.75rem;
|
|
94
|
+
flex-shrink: 0;
|
|
95
|
+
color: var(--md-sys-color-on-surface-variant, var(--color-text-muted));
|
|
96
|
+
transition: transform var(--md-sys-motion-duration-short-3, 150ms)
|
|
97
|
+
var(--md-sys-motion-easing-standard, ease);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// --- Menu ------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
&__content {
|
|
103
|
+
// Renders in a portal to document.body, so it is a *sibling* of any Modal
|
|
104
|
+
// or Drawer it was opened from rather than a child — the fallback must stay
|
|
105
|
+
// above --z-modal's (400) or an unstyled consumer gets a popup hidden
|
|
106
|
+
// behind the modal that spawned it.
|
|
107
|
+
//
|
|
108
|
+
// MD3 menus separate from the page by tone and elevation, not a border.
|
|
109
|
+
background: var(
|
|
110
|
+
--md-sys-color-surface-container,
|
|
111
|
+
var(--color-surface-raised)
|
|
112
|
+
);
|
|
113
|
+
border-radius: var(--md-sys-shape-corner-extra-small, 4px);
|
|
114
|
+
box-shadow: var(--md-sys-elevation-2, var(--shadow-md));
|
|
115
|
+
overflow: hidden;
|
|
116
|
+
z-index: var(--z-dropdown, 450);
|
|
117
|
+
min-width: var(--radix-select-trigger-width);
|
|
118
|
+
max-height: var(--radix-select-content-available-height);
|
|
119
|
+
|
|
120
|
+
&[data-state="open"] {
|
|
121
|
+
animation: select-in var(--md-sys-motion-duration-short-3, 150ms)
|
|
122
|
+
var(--md-sys-motion-easing-standard-decelerate, ease-out);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
&[data-state="closed"] {
|
|
126
|
+
animation: select-out var(--md-sys-motion-duration-short-2, 100ms)
|
|
127
|
+
var(--md-sys-motion-easing-standard-accelerate, ease-in);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
&__viewport {
|
|
132
|
+
padding: var(--spacing-1, 0.25rem);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
&__item {
|
|
136
|
+
position: relative;
|
|
137
|
+
isolation: isolate;
|
|
138
|
+
display: flex;
|
|
139
|
+
align-items: center;
|
|
140
|
+
justify-content: space-between;
|
|
141
|
+
gap: var(--spacing-2, 0.5rem);
|
|
142
|
+
padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);
|
|
143
|
+
border-radius: var(--md-sys-shape-corner-extra-small, 4px);
|
|
144
|
+
font-family: var(--md-sys-typescale-label-large-font, var(--font-body));
|
|
145
|
+
font-size: var(--md-sys-typescale-label-large-size, 0.875rem);
|
|
146
|
+
font-weight: var(--md-sys-typescale-label-large-weight, 500);
|
|
147
|
+
line-height: var(--md-sys-typescale-label-large-line-height, 1.25rem);
|
|
148
|
+
letter-spacing: var(--md-sys-typescale-label-large-tracking, 0.0071em);
|
|
149
|
+
color: var(--md-sys-color-on-surface, var(--color-on-surface));
|
|
150
|
+
cursor: pointer;
|
|
151
|
+
user-select: none;
|
|
152
|
+
outline: none;
|
|
153
|
+
|
|
154
|
+
// State layer. Radix sets data-highlighted for both pointer hover and
|
|
155
|
+
// keyboard focus, so it stands in for :hover here.
|
|
156
|
+
&::after {
|
|
157
|
+
content: "";
|
|
158
|
+
position: absolute;
|
|
159
|
+
inset: 0;
|
|
160
|
+
border-radius: inherit;
|
|
161
|
+
background: currentColor;
|
|
162
|
+
opacity: 0;
|
|
163
|
+
pointer-events: none;
|
|
164
|
+
transition: opacity var(--md-sys-motion-duration-short-2, 100ms)
|
|
165
|
+
var(--md-sys-motion-easing-standard, ease);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
&[data-highlighted]::after {
|
|
169
|
+
opacity: var(--md-sys-state-hover-opacity, 0.08);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
&:active::after {
|
|
173
|
+
opacity: var(--md-sys-state-pressed-opacity, 0.1);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// MD3 marks the selected menu item with a tonal container.
|
|
177
|
+
&[data-state="checked"] {
|
|
178
|
+
background: var(
|
|
179
|
+
--md-sys-color-secondary-container,
|
|
180
|
+
var(--color-surface-light)
|
|
181
|
+
);
|
|
182
|
+
color: var(
|
|
183
|
+
--md-sys-color-on-secondary-container,
|
|
184
|
+
var(--color-on-surface)
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
&[data-disabled] {
|
|
189
|
+
opacity: 0.4;
|
|
190
|
+
cursor: not-allowed;
|
|
191
|
+
pointer-events: none;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
&__item-indicator {
|
|
196
|
+
width: 0.75rem;
|
|
197
|
+
height: 0.75rem;
|
|
198
|
+
flex-shrink: 0;
|
|
199
|
+
display: flex;
|
|
200
|
+
align-items: center;
|
|
201
|
+
justify-content: center;
|
|
202
|
+
|
|
203
|
+
svg {
|
|
204
|
+
width: 100%;
|
|
205
|
+
height: 100%;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
@keyframes select-in {
|
|
211
|
+
from {
|
|
212
|
+
opacity: 0;
|
|
213
|
+
transform: translateY(-0.25rem);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
to {
|
|
217
|
+
opacity: 1;
|
|
218
|
+
transform: translateY(0);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
@keyframes select-out {
|
|
223
|
+
from {
|
|
224
|
+
opacity: 1;
|
|
225
|
+
transform: translateY(0);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
to {
|
|
229
|
+
opacity: 0;
|
|
230
|
+
transform: translateY(-0.25rem);
|
|
231
|
+
}
|
|
232
|
+
}
|
package/select.tsx
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import * as RadixSelect from "@radix-ui/react-select";
|
|
2
|
+
import { useId } from "react";
|
|
3
|
+
import { getClassName } from "@bht-component-registry/class-names";
|
|
4
|
+
import styles from "./select.module.scss";
|
|
5
|
+
|
|
6
|
+
export interface SelectOption {
|
|
7
|
+
/** Text shown for this option */
|
|
8
|
+
label: string;
|
|
9
|
+
/** Value submitted/reported when this option is chosen */
|
|
10
|
+
value: string;
|
|
11
|
+
/** Prevents this option from being selected */
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SelectProps extends Omit<
|
|
16
|
+
RadixSelect.SelectTriggerProps,
|
|
17
|
+
"className"
|
|
18
|
+
> {
|
|
19
|
+
/** Options rendered in the dropdown */
|
|
20
|
+
options: SelectOption[];
|
|
21
|
+
/** Controlled selected value */
|
|
22
|
+
value?: string;
|
|
23
|
+
/** Initial selected value for uncontrolled usage */
|
|
24
|
+
defaultValue?: string;
|
|
25
|
+
/** Called with the new value when the selection changes */
|
|
26
|
+
onValueChange?: (value: string) => void;
|
|
27
|
+
/** Text shown when no value is selected */
|
|
28
|
+
placeholder?: string;
|
|
29
|
+
/** Marks the field as required for form validation */
|
|
30
|
+
required?: boolean;
|
|
31
|
+
/** Additional class names to merge with the component root element */
|
|
32
|
+
className?: string;
|
|
33
|
+
/** Form field name submitted with the value */
|
|
34
|
+
name?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Bare select control — the Radix select trigger button is this component's
|
|
39
|
+
* own root element, no built-in label/hint/error. Wrap it in `FormField` for
|
|
40
|
+
* that chrome: `<FormField label="..." htmlFor="x"><Select id="x" .../></FormField>`.
|
|
41
|
+
*/
|
|
42
|
+
export const Select = ({
|
|
43
|
+
options,
|
|
44
|
+
value,
|
|
45
|
+
defaultValue,
|
|
46
|
+
onValueChange,
|
|
47
|
+
placeholder,
|
|
48
|
+
disabled,
|
|
49
|
+
required,
|
|
50
|
+
className,
|
|
51
|
+
id,
|
|
52
|
+
name,
|
|
53
|
+
...rest
|
|
54
|
+
}: SelectProps) => {
|
|
55
|
+
const generatedId = useId();
|
|
56
|
+
const selectId = id ?? generatedId;
|
|
57
|
+
|
|
58
|
+
const [rootClass, getChildClass] = getClassName({
|
|
59
|
+
className,
|
|
60
|
+
rootClass: "BHT__Select",
|
|
61
|
+
styles,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<RadixSelect.Root
|
|
66
|
+
value={value}
|
|
67
|
+
defaultValue={defaultValue}
|
|
68
|
+
onValueChange={onValueChange}
|
|
69
|
+
disabled={disabled}
|
|
70
|
+
required={required}
|
|
71
|
+
name={name}
|
|
72
|
+
>
|
|
73
|
+
<RadixSelect.Trigger {...rest} id={selectId} className={rootClass}>
|
|
74
|
+
<RadixSelect.Value placeholder={placeholder} />
|
|
75
|
+
<RadixSelect.Icon asChild>
|
|
76
|
+
<svg
|
|
77
|
+
className={getChildClass("chevron")}
|
|
78
|
+
viewBox="0 0 12 8"
|
|
79
|
+
fill="none"
|
|
80
|
+
stroke="currentColor"
|
|
81
|
+
strokeWidth="2"
|
|
82
|
+
strokeLinecap="round"
|
|
83
|
+
strokeLinejoin="round"
|
|
84
|
+
aria-hidden="true"
|
|
85
|
+
>
|
|
86
|
+
<path d="M1 1.5L6 6.5L11 1.5" />
|
|
87
|
+
</svg>
|
|
88
|
+
</RadixSelect.Icon>
|
|
89
|
+
</RadixSelect.Trigger>
|
|
90
|
+
<RadixSelect.Portal>
|
|
91
|
+
<RadixSelect.Content
|
|
92
|
+
className={getChildClass("content")}
|
|
93
|
+
position="popper"
|
|
94
|
+
sideOffset={4}
|
|
95
|
+
>
|
|
96
|
+
<RadixSelect.Viewport className={getChildClass("viewport")}>
|
|
97
|
+
{options.map((option) => (
|
|
98
|
+
<RadixSelect.Item
|
|
99
|
+
key={option.value}
|
|
100
|
+
value={option.value}
|
|
101
|
+
disabled={option.disabled}
|
|
102
|
+
className={getChildClass("item")}
|
|
103
|
+
>
|
|
104
|
+
<RadixSelect.ItemText>{option.label}</RadixSelect.ItemText>
|
|
105
|
+
<RadixSelect.ItemIndicator
|
|
106
|
+
className={getChildClass("item-indicator")}
|
|
107
|
+
>
|
|
108
|
+
<svg
|
|
109
|
+
viewBox="0 0 12 10"
|
|
110
|
+
fill="none"
|
|
111
|
+
stroke="currentColor"
|
|
112
|
+
strokeWidth="2.5"
|
|
113
|
+
strokeLinecap="round"
|
|
114
|
+
strokeLinejoin="round"
|
|
115
|
+
aria-hidden="true"
|
|
116
|
+
>
|
|
117
|
+
<path d="M1 5L4.5 8.5L11 1" />
|
|
118
|
+
</svg>
|
|
119
|
+
</RadixSelect.ItemIndicator>
|
|
120
|
+
</RadixSelect.Item>
|
|
121
|
+
))}
|
|
122
|
+
</RadixSelect.Viewport>
|
|
123
|
+
</RadixSelect.Content>
|
|
124
|
+
</RadixSelect.Portal>
|
|
125
|
+
</RadixSelect.Root>
|
|
126
|
+
);
|
|
127
|
+
};
|