@studiocms/ui 0.4.11 → 0.4.13
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/components/Badge/Badge.astro +16 -2
- package/dist/components/SearchSelect/SearchSelect.astro +137 -64
- package/dist/components/SearchSelect/searchselect.css +64 -1
- package/dist/components/SearchSelect/searchselect.d.ts +15 -2
- package/dist/components/SearchSelect/searchselect.js +347 -146
- package/dist/components/Select/Select.astro +133 -69
- package/dist/components/Select/select.css +76 -3
- package/dist/components/Select/select.d.ts +17 -0
- package/dist/components/Select/select.js +336 -123
- package/dist/components/Tabs/tabs.css +1 -1
- package/package.json +1 -1
|
@@ -14,6 +14,10 @@ interface Props extends Omit<HTMLAttributes<'span'>, 'color'> {
|
|
|
14
14
|
* The icon to display in the badge.
|
|
15
15
|
*/
|
|
16
16
|
icon?: HeroIconName;
|
|
17
|
+
/**
|
|
18
|
+
* The icon position. Defaults to `left`.
|
|
19
|
+
*/
|
|
20
|
+
iconPosition?: 'left' | 'right';
|
|
17
21
|
/**
|
|
18
22
|
* The size of the badge. Defaults to `md`.
|
|
19
23
|
*/
|
|
@@ -38,12 +42,22 @@ const {
|
|
|
38
42
|
size = 'md',
|
|
39
43
|
variant = 'default',
|
|
40
44
|
rounding = 'full',
|
|
45
|
+
iconPosition = 'left',
|
|
41
46
|
label,
|
|
47
|
+
class: className,
|
|
42
48
|
...props
|
|
43
49
|
} = Astro.props;
|
|
50
|
+
|
|
51
|
+
let iconSize = 16;
|
|
52
|
+
if (size === 'sm') {
|
|
53
|
+
iconSize = 8;
|
|
54
|
+
} else if (size === 'lg') {
|
|
55
|
+
iconSize = 24;
|
|
56
|
+
}
|
|
44
57
|
---
|
|
45
58
|
|
|
46
|
-
<span class="sui-badge" class:list={[color, size, variant, rounding]} {...props}>
|
|
47
|
-
|
|
59
|
+
<span class="sui-badge" class:list={[color, size, variant, rounding, className]} {...props}>
|
|
60
|
+
{icon && iconPosition === 'left' && <Icon name={icon} width={iconSize} height={iconSize} />}
|
|
48
61
|
{label}
|
|
62
|
+
{icon && iconPosition === 'right' && <Icon name={icon} width={iconSize} height={iconSize} />}
|
|
49
63
|
</span>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { generateID } from '../../utils/generateID.js';
|
|
3
|
+
import Badge from '../Badge/Badge.astro';
|
|
3
4
|
import Icon from '../Icon/Icon.astro';
|
|
4
5
|
import Input from '../Input/Input.astro';
|
|
5
6
|
import './searchselect.css';
|
|
@@ -25,45 +26,71 @@ interface Option {
|
|
|
25
26
|
/**
|
|
26
27
|
* The props for the search select component.
|
|
27
28
|
*/
|
|
28
|
-
|
|
29
|
+
type NonMultipleProps = {
|
|
29
30
|
/**
|
|
30
|
-
*
|
|
31
|
+
* Whether the select accepts multiple options. Defaults to `false`.
|
|
31
32
|
*/
|
|
32
|
-
|
|
33
|
+
multiple: false;
|
|
34
|
+
/**
|
|
35
|
+
* The default value of the select.
|
|
36
|
+
*/
|
|
37
|
+
defaultValue: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type MultipleProps = {
|
|
41
|
+
/**
|
|
42
|
+
* Whether the select accepts multiple options. Defaults to `false`.
|
|
43
|
+
*/
|
|
44
|
+
multiple: true;
|
|
33
45
|
/**
|
|
34
|
-
* The default value of the
|
|
46
|
+
* The default value of the select.
|
|
35
47
|
*/
|
|
36
|
-
defaultValue
|
|
48
|
+
defaultValue: string[];
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The props for the select component.
|
|
53
|
+
*/
|
|
54
|
+
type BaseProps = {
|
|
37
55
|
/**
|
|
38
|
-
*
|
|
56
|
+
* The label of the select.
|
|
57
|
+
*/
|
|
58
|
+
label?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Additional classes to apply to the select.
|
|
39
61
|
*/
|
|
40
62
|
class?: string;
|
|
41
63
|
/**
|
|
42
|
-
* The name of the
|
|
64
|
+
* The name of the select. Required because of the helper.
|
|
43
65
|
*/
|
|
44
66
|
name?: string;
|
|
45
67
|
/**
|
|
46
|
-
* Whether the
|
|
68
|
+
* Whether the select is required. Defaults to `false`.
|
|
47
69
|
*/
|
|
48
70
|
isRequired?: boolean;
|
|
49
71
|
/**
|
|
50
|
-
* The options to display in the
|
|
72
|
+
* The options to display in the select.
|
|
51
73
|
*/
|
|
52
74
|
options: Option[];
|
|
53
75
|
/**
|
|
54
|
-
* Whether the
|
|
76
|
+
* Whether the select is disabled. Defaults to `false`.
|
|
55
77
|
*/
|
|
56
78
|
disabled?: boolean;
|
|
57
79
|
/**
|
|
58
|
-
* Whether the
|
|
80
|
+
* Whether the select is full width. Defaults to `false`.
|
|
59
81
|
*/
|
|
60
82
|
fullWidth?: boolean;
|
|
61
83
|
/**
|
|
62
|
-
* The placeholder of the
|
|
84
|
+
* The placeholder of the select. Defaults to `Select`.
|
|
63
85
|
*/
|
|
64
86
|
placeholder?: string;
|
|
65
|
-
|
|
87
|
+
/**
|
|
88
|
+
* The maximum number of options that can be selected. Defaults to `undefined`.
|
|
89
|
+
*/
|
|
90
|
+
max?: number;
|
|
91
|
+
};
|
|
66
92
|
|
|
93
|
+
type Props = BaseProps & (MultipleProps | NonMultipleProps);
|
|
67
94
|
const {
|
|
68
95
|
label,
|
|
69
96
|
defaultValue,
|
|
@@ -73,62 +100,108 @@ const {
|
|
|
73
100
|
options = [],
|
|
74
101
|
disabled,
|
|
75
102
|
fullWidth,
|
|
76
|
-
placeholder,
|
|
103
|
+
placeholder = 'Select',
|
|
104
|
+
multiple = false,
|
|
105
|
+
max = undefined,
|
|
77
106
|
} = Astro.props;
|
|
107
|
+
|
|
108
|
+
let selected: Option | (Option | undefined)[] | undefined;
|
|
109
|
+
|
|
110
|
+
if (multiple && Array.isArray(defaultValue)) {
|
|
111
|
+
selected = defaultValue.map((x) => options.find((y) => y.value === x));
|
|
112
|
+
} else {
|
|
113
|
+
selected = options.find((x) => x.value === defaultValue);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const defaultLabel = selected
|
|
117
|
+
? Array.isArray(selected)
|
|
118
|
+
? placeholder
|
|
119
|
+
: selected.label
|
|
120
|
+
: placeholder;
|
|
78
121
|
---
|
|
79
122
|
|
|
80
123
|
<div
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
124
|
+
id={`${name}-container`}
|
|
125
|
+
class="sui-search-select-label"
|
|
126
|
+
class:list={[disabled && "disabled", className, fullWidth && "full"]}
|
|
127
|
+
data-options={JSON.stringify(options)}
|
|
128
|
+
data-multiple={multiple ? "true" : undefined}
|
|
129
|
+
data-multiple-max={multiple && max !== undefined ? max : undefined}
|
|
130
|
+
data-id={name}
|
|
86
131
|
>
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
+
<div class="sui-search-select-dropdown-container">
|
|
133
|
+
<div class="sui-search-input-wrapper" id={`${name}-search-input-wrapper`}>
|
|
134
|
+
<Input
|
|
135
|
+
placeholder={defaultLabel}
|
|
136
|
+
role='combobox'
|
|
137
|
+
aria-controls={`${name}-dropdown`}
|
|
138
|
+
aria-expanded="false"
|
|
139
|
+
label={label || ''}
|
|
140
|
+
isRequired={isRequired || false}
|
|
141
|
+
/>
|
|
142
|
+
<Icon name="chevron-up-down" class="sui-search-select-indicator" width={24} height={24} />
|
|
143
|
+
</div>
|
|
144
|
+
<ul class="sui-search-select-dropdown" role="listbox" id={`${name}-dropdown`}>
|
|
145
|
+
{
|
|
146
|
+
options.map((x, i) => {
|
|
147
|
+
const isSelected = Array.isArray(selected)
|
|
148
|
+
? selected.map((y) => y && y.value).includes(x.value)
|
|
149
|
+
: selected?.value === x.value;
|
|
150
|
+
return (
|
|
151
|
+
<li
|
|
152
|
+
class="sui-search-select-option"
|
|
153
|
+
role="option"
|
|
154
|
+
value={x.value}
|
|
155
|
+
class:list={[
|
|
156
|
+
isSelected && `selected`,
|
|
157
|
+
x.disabled && "disabled"
|
|
158
|
+
]}
|
|
159
|
+
id={isSelected ? `${name}-selected` : ""}
|
|
160
|
+
data-option-index={i}
|
|
161
|
+
data-value={x.value}
|
|
162
|
+
>
|
|
163
|
+
{x.label}
|
|
164
|
+
</li>
|
|
165
|
+
)
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
</ul>
|
|
169
|
+
</div>
|
|
170
|
+
<select class="sui-hidden-search-select" id={name} name={name} required={isRequired} multiple={multiple ? "" : undefined} hidden tabindex="-1">
|
|
171
|
+
<option value={""}> Select </option>
|
|
172
|
+
{
|
|
173
|
+
options.map((x) => {
|
|
174
|
+
const isSelected = Array.isArray(selected)
|
|
175
|
+
? selected.map((y) => y && y.value).includes(x.value)
|
|
176
|
+
: selected?.value === x.value;
|
|
177
|
+
(
|
|
178
|
+
<option
|
|
179
|
+
value={x.value}
|
|
180
|
+
selected={isSelected}
|
|
181
|
+
disabled={x.disabled}
|
|
182
|
+
>
|
|
183
|
+
{x.label}
|
|
184
|
+
</option>
|
|
185
|
+
)
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
</select>
|
|
189
|
+
{multiple && max !== undefined && (
|
|
190
|
+
<span class="sui-search-select-max-span">
|
|
191
|
+
<span class="sui-search-select-select-count">0</span> / {max} selected
|
|
192
|
+
</span>
|
|
193
|
+
)}
|
|
194
|
+
{
|
|
195
|
+
multiple && Array.isArray(selected ?? []) && (
|
|
196
|
+
<div class="sui-search-select-badge-container">
|
|
197
|
+
{
|
|
198
|
+
((selected ?? []) as Option[]).map((s) => s &&
|
|
199
|
+
<Badge class="sui-search-select-badge" data-value={s.value} size="sm" label={s.label} iconPosition="right" icon="x-mark" />
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
</div>
|
|
203
|
+
)
|
|
204
|
+
}
|
|
132
205
|
</div>
|
|
133
206
|
<script>
|
|
134
207
|
import "studiocms:ui/scripts/searchselect"
|
|
@@ -21,6 +21,14 @@
|
|
|
21
21
|
color: hsl(var(--danger-base));
|
|
22
22
|
font-weight: 700;
|
|
23
23
|
}
|
|
24
|
+
.sui-search-select-dropdown-container {
|
|
25
|
+
width: 100%;
|
|
26
|
+
display: flex;
|
|
27
|
+
flex-direction: column;
|
|
28
|
+
gap: 0.25rem;
|
|
29
|
+
position: relative;
|
|
30
|
+
height: fit-content;
|
|
31
|
+
}
|
|
24
32
|
.sui-search-select-dropdown {
|
|
25
33
|
position: absolute;
|
|
26
34
|
width: 100%;
|
|
@@ -40,7 +48,7 @@
|
|
|
40
48
|
}
|
|
41
49
|
.sui-search-select-dropdown.above {
|
|
42
50
|
top: auto;
|
|
43
|
-
bottom: calc(100% -
|
|
51
|
+
bottom: calc(100% - 1.5rem);
|
|
44
52
|
}
|
|
45
53
|
.sui-search-select-option,
|
|
46
54
|
.empty-search-results {
|
|
@@ -49,6 +57,10 @@
|
|
|
49
57
|
font-size: 0.975em;
|
|
50
58
|
transition: all 0.15s ease;
|
|
51
59
|
}
|
|
60
|
+
.empty-search-results:hover {
|
|
61
|
+
background-color: hsl(var(--background-step-2));
|
|
62
|
+
cursor: default;
|
|
63
|
+
}
|
|
52
64
|
.sui-search-select-option.disabled {
|
|
53
65
|
pointer-events: none;
|
|
54
66
|
color: hsl(var(--text-muted));
|
|
@@ -57,11 +69,19 @@
|
|
|
57
69
|
.sui-search-select-option.focused {
|
|
58
70
|
background-color: hsl(var(--background-step-3));
|
|
59
71
|
}
|
|
72
|
+
.sui-search-select-label[data-multiple=true] .sui-search-select-option.selected:hover,
|
|
73
|
+
.sui-search-select-label[data-multiple=true] .sui-search-select-option.selected:focus,
|
|
74
|
+
.sui-search-select-label[data-multiple=true] .sui-search-select-option.selected.focused {
|
|
75
|
+
background-color: hsl(var(--primary-hover));
|
|
76
|
+
}
|
|
60
77
|
.sui-search-select-option.selected {
|
|
61
78
|
background-color: hsl(var(--primary-base));
|
|
62
79
|
color: hsl(var(--text-inverted));
|
|
63
80
|
cursor: default;
|
|
64
81
|
}
|
|
82
|
+
.sui-search-select-label[data-multiple=true] .sui-search-select-option.selected {
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
}
|
|
65
85
|
.sui-hidden-select {
|
|
66
86
|
height: 0;
|
|
67
87
|
width: 0;
|
|
@@ -85,6 +105,7 @@
|
|
|
85
105
|
position: absolute;
|
|
86
106
|
bottom: .675rem;
|
|
87
107
|
right: .675rem;
|
|
108
|
+
pointer-events: none;
|
|
88
109
|
}
|
|
89
110
|
.sui-search-input-wrapper:has(input:focus) + .sui-search-select-dropdown {
|
|
90
111
|
display: flex;
|
|
@@ -93,3 +114,45 @@
|
|
|
93
114
|
.sui-search-select-dropdown:has(> li:active) {
|
|
94
115
|
display: flex;
|
|
95
116
|
}
|
|
117
|
+
.sui-search-select-badge-container {
|
|
118
|
+
width: 100%;
|
|
119
|
+
display: flex;
|
|
120
|
+
flex-wrap: wrap;
|
|
121
|
+
flex-direction: row;
|
|
122
|
+
gap: 0.5rem;
|
|
123
|
+
margin-top: 0.5rem;
|
|
124
|
+
}
|
|
125
|
+
.sui-search-select-label:has(.sui-search-select-max-span) .sui-search-select-badge-container {
|
|
126
|
+
margin-top: 0rem;
|
|
127
|
+
}
|
|
128
|
+
.sui-search-select-badge-container .sui-search-select-badge.sui-badge {
|
|
129
|
+
white-space: nowrap;
|
|
130
|
+
cursor: default;
|
|
131
|
+
position: relative;
|
|
132
|
+
padding-right: 2rem;
|
|
133
|
+
border-block: 1.5px solid rgba(0, 0, 0, 0);
|
|
134
|
+
}
|
|
135
|
+
.sui-search-select-badge-container .sui-search-select-badge.sui-badge svg {
|
|
136
|
+
position: absolute;
|
|
137
|
+
right: 0.25rem;
|
|
138
|
+
padding: 4px;
|
|
139
|
+
height: 20px;
|
|
140
|
+
width: 20px;
|
|
141
|
+
aspect-ratio: 1 / 1;
|
|
142
|
+
border-radius: 999px;
|
|
143
|
+
cursor: pointer;
|
|
144
|
+
outline: 1px solid hsla(var(--border), 0);
|
|
145
|
+
transition: background-color 0.15s ease, outline 0.15s ease;
|
|
146
|
+
}
|
|
147
|
+
.sui-search-select-badge-container .sui-search-select-badge.sui-badge svg:hover {
|
|
148
|
+
background-color: hsla(100, 100%, 95%, 0.2);
|
|
149
|
+
}
|
|
150
|
+
.sui-search-select-badge-container .sui-search-select-badge.sui-badge svg:active,
|
|
151
|
+
.sui-search-select-badge-container .sui-search-select-badge.sui-badge svg:focus {
|
|
152
|
+
background-color: hsla(100, 100%, 95%, 0.2);
|
|
153
|
+
outline: 1px solid hsl(var(--border));
|
|
154
|
+
}
|
|
155
|
+
.sui-search-select-max-span {
|
|
156
|
+
font-size: 0.875em;
|
|
157
|
+
color: hsl(var(--text-muted));
|
|
158
|
+
}
|
|
@@ -1,6 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
type SearchSelectOption = {
|
|
2
2
|
label: string;
|
|
3
3
|
value: string;
|
|
4
4
|
disabled?: boolean;
|
|
5
|
-
}
|
|
5
|
+
};
|
|
6
|
+
type SearchSelectContainer = HTMLDivElement & {
|
|
7
|
+
input: HTMLInputElement | null;
|
|
8
|
+
dropdown: Element | null;
|
|
9
|
+
select: HTMLSelectElement | null;
|
|
10
|
+
};
|
|
11
|
+
type SearchSelectState = {
|
|
12
|
+
optionsMap: Record<string, SelectOption[]>;
|
|
13
|
+
isMultipleMap: Record<string, boolean>;
|
|
14
|
+
selectedOptionsMap: Record<string, string[]>;
|
|
15
|
+
placeholderMap: Record<string, string>;
|
|
16
|
+
focusIndex: number;
|
|
17
|
+
isSelectingOption: boolean;
|
|
18
|
+
};
|
|
6
19
|
declare function loadSearchSelects(): void;
|