@bunnix/components 0.9.2 → 0.9.4
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/@types/index.d.ts +203 -5
- package/README.md +73 -103
- package/package.json +1 -1
- package/src/components/ComboBox.mjs +8 -2
- package/src/components/DatePicker.mjs +234 -57
- package/src/components/Dialog.mjs +1 -1
- package/src/components/InputField.mjs +55 -6
- package/src/components/ProgressBar.mjs +81 -0
- package/src/components/TimePicker.mjs +255 -65
- package/src/index.mjs +4 -0
- package/src/styles/controls.css +72 -0
- package/src/utils/maskUtils.mjs +569 -0
|
@@ -1,28 +1,75 @@
|
|
|
1
|
-
import Bunnix, { useRef, useState, useMemo } from "@bunnix/core";
|
|
1
|
+
import Bunnix, { useRef, useState, useMemo, useEffect } from "@bunnix/core";
|
|
2
2
|
import { clampSize, toSizeToken } from "../utils/sizeUtils.mjs";
|
|
3
3
|
import Icon from "./Icon.mjs";
|
|
4
|
-
const { div, button, span, hr
|
|
4
|
+
const { div, label, input: inputEl, button, span, hr } = Bunnix;
|
|
5
5
|
|
|
6
6
|
const formatSegment = (val) => val.toString().padStart(2, '0');
|
|
7
7
|
|
|
8
|
+
const applyTimeMask = (value) => {
|
|
9
|
+
// Remove all non-digits
|
|
10
|
+
const digits = value.replace(/\D/g, "");
|
|
11
|
+
|
|
12
|
+
// Apply mask HH:MM
|
|
13
|
+
let masked = "";
|
|
14
|
+
for (let i = 0; i < digits.length && i < 4; i++) {
|
|
15
|
+
if (i === 2) {
|
|
16
|
+
masked += ":";
|
|
17
|
+
}
|
|
18
|
+
masked += digits[i];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return masked;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const parseTime = (str) => {
|
|
25
|
+
if (!str) return null;
|
|
26
|
+
const parts = str.split(":");
|
|
27
|
+
if (parts.length !== 2) return null;
|
|
28
|
+
const hours = parseInt(parts[0], 10);
|
|
29
|
+
const minutes = parseInt(parts[1], 10);
|
|
30
|
+
if (isNaN(hours) || isNaN(minutes)) return null;
|
|
31
|
+
if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return null;
|
|
32
|
+
return { hours, minutes };
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const formatTime = (hours, minutes) => {
|
|
36
|
+
if (hours === null || minutes === null) return "";
|
|
37
|
+
return `${formatSegment(hours)}:${formatSegment(minutes)}`;
|
|
38
|
+
};
|
|
39
|
+
|
|
8
40
|
export default function TimePicker({
|
|
9
41
|
id,
|
|
10
|
-
placeholder,
|
|
42
|
+
placeholder = "HH:MM",
|
|
11
43
|
variant = "regular",
|
|
12
44
|
size = "regular",
|
|
45
|
+
label: labelText,
|
|
46
|
+
disabled = false,
|
|
47
|
+
value,
|
|
48
|
+
onInput,
|
|
49
|
+
onChange,
|
|
50
|
+
onFocus,
|
|
51
|
+
onBlur,
|
|
52
|
+
input,
|
|
53
|
+
change,
|
|
54
|
+
focus,
|
|
55
|
+
blur,
|
|
13
56
|
class: className = ""
|
|
14
57
|
} = {}) {
|
|
15
58
|
const popoverRef = useRef(null);
|
|
59
|
+
const inputRef = useRef(null);
|
|
16
60
|
const hourInputRef = useRef(null);
|
|
17
61
|
const minuteInputRef = useRef(null);
|
|
18
62
|
const pickerId = id || `timepicker-${Math.random().toString(36).slice(2, 8)}`;
|
|
19
63
|
const anchorName = `--${pickerId}`;
|
|
20
64
|
|
|
21
|
-
//
|
|
65
|
+
// Initialize from value prop if provided
|
|
22
66
|
const now = new Date();
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
67
|
+
const initialHours = value?.hours ?? now.getHours();
|
|
68
|
+
const initialMinutes = value?.minutes ?? now.getMinutes();
|
|
69
|
+
|
|
70
|
+
const hour = useState(formatSegment(initialHours));
|
|
71
|
+
const minute = useState(formatSegment(initialMinutes));
|
|
72
|
+
const inputValue = useState(value ? formatTime(initialHours, initialMinutes) : "");
|
|
26
73
|
|
|
27
74
|
const openPopover = () => {
|
|
28
75
|
const popover = popoverRef.current;
|
|
@@ -38,11 +85,82 @@ export default function TimePicker({
|
|
|
38
85
|
}
|
|
39
86
|
};
|
|
40
87
|
|
|
88
|
+
// Handle click outside and escape key for manual popover
|
|
89
|
+
useEffect((popoverElement) => {
|
|
90
|
+
if (!popoverElement) return;
|
|
91
|
+
|
|
92
|
+
const handleClickOutside = (e) => {
|
|
93
|
+
if (!popoverElement.matches(":popover-open")) return;
|
|
94
|
+
|
|
95
|
+
const input = inputRef.current;
|
|
96
|
+
const isClickInside = popoverElement.contains(e.target);
|
|
97
|
+
const isClickOnInput = input && input.contains(e.target);
|
|
98
|
+
|
|
99
|
+
if (!isClickInside && !isClickOnInput) {
|
|
100
|
+
closePopover();
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const handleEscape = (e) => {
|
|
105
|
+
if (e.key === "Escape" && popoverElement.matches(":popover-open")) {
|
|
106
|
+
closePopover();
|
|
107
|
+
inputRef.current?.focus();
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
document.addEventListener("click", handleClickOutside, true);
|
|
112
|
+
document.addEventListener("keydown", handleEscape);
|
|
113
|
+
|
|
114
|
+
return () => {
|
|
115
|
+
document.removeEventListener("click", handleClickOutside, true);
|
|
116
|
+
document.removeEventListener("keydown", handleEscape);
|
|
117
|
+
};
|
|
118
|
+
}, popoverRef);
|
|
119
|
+
|
|
120
|
+
const updateTime = (hours, minutes) => {
|
|
121
|
+
hour.set(formatSegment(hours));
|
|
122
|
+
minute.set(formatSegment(minutes));
|
|
123
|
+
inputValue.set(formatTime(hours, minutes));
|
|
124
|
+
|
|
125
|
+
const handleChange = onChange ?? change;
|
|
126
|
+
if (handleChange) {
|
|
127
|
+
handleChange({ target: { value: formatTime(hours, minutes) }, time: { hours, minutes } });
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
41
131
|
const handleNow = () => {
|
|
42
132
|
const d = new Date();
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
133
|
+
updateTime(d.getHours(), d.getMinutes());
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const handleClear = () => {
|
|
137
|
+
hour.set("00");
|
|
138
|
+
minute.set("00");
|
|
139
|
+
inputValue.set("");
|
|
140
|
+
closePopover();
|
|
141
|
+
|
|
142
|
+
const handleChange = onChange ?? change;
|
|
143
|
+
if (handleChange) {
|
|
144
|
+
handleChange({ target: { value: "" }, time: null });
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const handleOK = () => {
|
|
149
|
+
// Update the main input with current segment values when OK is clicked
|
|
150
|
+
const h = hour.get().padStart(2, '0');
|
|
151
|
+
const m = minute.get().padStart(2, '0');
|
|
152
|
+
const time = formatTime(parseInt(h, 10), parseInt(m, 10));
|
|
153
|
+
inputValue.set(time);
|
|
154
|
+
|
|
155
|
+
const handleChange = onChange ?? change;
|
|
156
|
+
if (handleChange) {
|
|
157
|
+
handleChange({
|
|
158
|
+
target: { value: time },
|
|
159
|
+
time: { hours: parseInt(h, 10), minutes: parseInt(m, 10) }
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
closePopover();
|
|
46
164
|
};
|
|
47
165
|
|
|
48
166
|
const handleHourInput = (e) => {
|
|
@@ -52,7 +170,12 @@ export default function TimePicker({
|
|
|
52
170
|
if (val > 23) raw = "23";
|
|
53
171
|
}
|
|
54
172
|
hour.set(raw);
|
|
55
|
-
|
|
173
|
+
|
|
174
|
+
// Update main input value
|
|
175
|
+
const currentMinute = minute.get();
|
|
176
|
+
if (raw && currentMinute) {
|
|
177
|
+
inputValue.set(formatTime(parseInt(raw, 10), parseInt(currentMinute, 10)));
|
|
178
|
+
}
|
|
56
179
|
|
|
57
180
|
// Auto-focus minutes if we have 2 digits or a digit that can't be leading (3-9)
|
|
58
181
|
if (raw.length === 2 || (raw.length === 1 && parseInt(raw, 10) > 2)) {
|
|
@@ -68,91 +191,158 @@ export default function TimePicker({
|
|
|
68
191
|
if (val > 59) raw = "59";
|
|
69
192
|
}
|
|
70
193
|
minute.set(raw);
|
|
71
|
-
|
|
194
|
+
|
|
195
|
+
// Update main input value
|
|
196
|
+
const currentHour = hour.get();
|
|
197
|
+
if (currentHour && raw) {
|
|
198
|
+
inputValue.set(formatTime(parseInt(currentHour, 10), parseInt(raw, 10)));
|
|
199
|
+
}
|
|
72
200
|
};
|
|
73
201
|
|
|
74
|
-
const
|
|
202
|
+
const handleSegmentBlur = (type) => {
|
|
75
203
|
const state = type === 'hour' ? hour : minute;
|
|
76
204
|
let val = state.get();
|
|
77
205
|
if (val === "") val = "00";
|
|
78
|
-
|
|
206
|
+
const padded = val.padStart(2, '0');
|
|
207
|
+
state.set(padded);
|
|
208
|
+
|
|
209
|
+
// Update main input with padded values
|
|
210
|
+
const h = type === 'hour' ? padded : hour.get();
|
|
211
|
+
const m = type === 'minute' ? padded : minute.get();
|
|
212
|
+
if (h && m) {
|
|
213
|
+
const time = formatTime(parseInt(h, 10), parseInt(m, 10));
|
|
214
|
+
inputValue.set(time);
|
|
215
|
+
|
|
216
|
+
const handleChange = onChange ?? change;
|
|
217
|
+
if (handleChange) {
|
|
218
|
+
handleChange({
|
|
219
|
+
target: { value: time },
|
|
220
|
+
time: { hours: parseInt(h, 10), minutes: parseInt(m, 10) }
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const handleMainInputChange = (e) => {
|
|
227
|
+
const rawValue = e.target.value;
|
|
228
|
+
const maskedValue = applyTimeMask(rawValue);
|
|
229
|
+
inputValue.set(maskedValue);
|
|
230
|
+
|
|
231
|
+
// Try to parse the time if complete
|
|
232
|
+
if (maskedValue.length === 5) {
|
|
233
|
+
const parsedTime = parseTime(maskedValue);
|
|
234
|
+
if (parsedTime) {
|
|
235
|
+
hour.set(formatSegment(parsedTime.hours));
|
|
236
|
+
minute.set(formatSegment(parsedTime.minutes));
|
|
237
|
+
|
|
238
|
+
const handleChange = onChange ?? change;
|
|
239
|
+
if (handleChange) {
|
|
240
|
+
handleChange({ target: { value: maskedValue }, time: parsedTime });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const handleInput = onInput ?? input;
|
|
246
|
+
if (handleInput) {
|
|
247
|
+
handleInput(e);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const handleMainInputFocus = (e) => {
|
|
252
|
+
openPopover();
|
|
253
|
+
|
|
254
|
+
const handleFocus = onFocus ?? focus;
|
|
255
|
+
if (handleFocus) {
|
|
256
|
+
handleFocus(e);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const handleMainInputBlur = (e) => {
|
|
261
|
+
// Don't close popover on blur - let it handle its own dismissal
|
|
262
|
+
|
|
263
|
+
const handleBlur = onBlur ?? blur;
|
|
264
|
+
if (handleBlur) {
|
|
265
|
+
handleBlur(e);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const handleClockIconClick = (e) => {
|
|
270
|
+
e.preventDefault();
|
|
271
|
+
e.stopPropagation();
|
|
272
|
+
const input = inputRef.current;
|
|
273
|
+
if (input) {
|
|
274
|
+
input.focus();
|
|
275
|
+
}
|
|
79
276
|
};
|
|
80
277
|
|
|
81
|
-
//
|
|
82
|
-
const
|
|
83
|
-
if (!mod && placeholder) return placeholder;
|
|
84
|
-
// Ensure we show padded values in the trigger even if input is mid-edit
|
|
85
|
-
const hh = h === "" ? "00" : h.padStart(2, '0');
|
|
86
|
-
const mm = m === "" ? "00" : m.padStart(2, '0');
|
|
87
|
-
return `${hh}:${hh === h ? '' : ''}${mm}`; // Trigger re-render correctly
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
// Refined display label using state directly for consistency
|
|
91
|
-
const finalLabel = useMemo([hour, minute, isModified], (h, m, mod) => {
|
|
92
|
-
if (!mod && placeholder) return placeholder;
|
|
93
|
-
const hh = h.padStart(2, '0');
|
|
94
|
-
const mm = m.padStart(2, '0');
|
|
95
|
-
return `${hh}:${mm}`;
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
const hasValue = isModified.map(m => !!m);
|
|
99
|
-
|
|
100
|
-
// TimePicker does not support small size (clamps to regular)
|
|
101
|
-
const normalizeSize = (value) => clampSize(value, ["xsmall", "regular", "large", "xlarge"], "regular");
|
|
278
|
+
// TimePicker supports regular, large, xlarge (no xsmall, small)
|
|
279
|
+
const normalizeSize = (value) => clampSize(value, ["regular", "large", "xlarge"], "regular");
|
|
102
280
|
const normalizedSize = normalizeSize(size);
|
|
103
281
|
const sizeToken = toSizeToken(normalizedSize);
|
|
282
|
+
const sizeClass = sizeToken === "xl" ? "input-xl" : sizeToken === "lg" ? "input-lg" : "";
|
|
104
283
|
const variantClass = variant === "rounded" ? "rounded-full" : "";
|
|
105
|
-
const
|
|
106
|
-
? "
|
|
107
|
-
:
|
|
108
|
-
? "
|
|
109
|
-
:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
:
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
284
|
+
const iconSizeValue = normalizedSize === "large"
|
|
285
|
+
? "large"
|
|
286
|
+
: normalizedSize === "xlarge"
|
|
287
|
+
? "xlarge"
|
|
288
|
+
: undefined;
|
|
289
|
+
|
|
290
|
+
return div({ class: `column-container no-margin shrink-0 gap-0 ${className}`.trim() }, [
|
|
291
|
+
labelText && label({ class: "label select-none" }, labelText),
|
|
292
|
+
div({ class: "timepicker-input-wrapper w-full relative" }, [
|
|
293
|
+
inputEl({
|
|
294
|
+
ref: inputRef,
|
|
295
|
+
id: pickerId,
|
|
296
|
+
type: "text",
|
|
297
|
+
value: inputValue,
|
|
298
|
+
placeholder,
|
|
299
|
+
disabled,
|
|
300
|
+
autocomplete: "off",
|
|
301
|
+
class: `input ${sizeClass} ${variantClass} pr-xl`.trim(),
|
|
302
|
+
style: `anchor-name: ${anchorName}`,
|
|
303
|
+
input: handleMainInputChange,
|
|
304
|
+
focus: handleMainInputFocus,
|
|
305
|
+
blur: handleMainInputBlur,
|
|
306
|
+
maxlength: "5"
|
|
307
|
+
}),
|
|
308
|
+
button({
|
|
309
|
+
class: "timepicker-icon-button",
|
|
310
|
+
type: "button",
|
|
311
|
+
disabled,
|
|
312
|
+
click: handleClockIconClick,
|
|
313
|
+
tabindex: "-1"
|
|
314
|
+
}, [
|
|
315
|
+
Icon({ name: "clock", fill: "quaternary", size: iconSizeValue })
|
|
316
|
+
])
|
|
127
317
|
]),
|
|
128
318
|
|
|
129
319
|
div({
|
|
130
320
|
ref: popoverRef,
|
|
131
|
-
popover: "
|
|
321
|
+
popover: "manual",
|
|
132
322
|
class: "timepicker-popover popover-base",
|
|
133
323
|
style: `--anchor-id: ${anchorName}`
|
|
134
324
|
}, [
|
|
135
325
|
div({ class: "card column-container shadow gap-0 p-0 bg-base timepicker-card" }, [
|
|
136
326
|
div({ class: "timepicker-display" }, [
|
|
137
|
-
|
|
327
|
+
inputEl({
|
|
138
328
|
ref: hourInputRef,
|
|
139
329
|
type: "text",
|
|
140
330
|
class: "time-segment",
|
|
141
331
|
value: hour,
|
|
142
332
|
placeholder: "00",
|
|
143
333
|
input: handleHourInput,
|
|
144
|
-
blur: () =>
|
|
334
|
+
blur: () => handleSegmentBlur('hour'),
|
|
145
335
|
focus: (e) => e.target.select()
|
|
146
336
|
}),
|
|
147
337
|
span({ class: "time-separator" }, ":"),
|
|
148
|
-
|
|
338
|
+
inputEl({
|
|
149
339
|
ref: minuteInputRef,
|
|
150
340
|
type: "text",
|
|
151
341
|
class: "time-segment",
|
|
152
342
|
value: minute,
|
|
153
343
|
placeholder: "00",
|
|
154
344
|
input: handleMinuteInput,
|
|
155
|
-
blur: () =>
|
|
345
|
+
blur: () => handleSegmentBlur('minute'),
|
|
156
346
|
focus: (e) => e.target.select()
|
|
157
347
|
})
|
|
158
348
|
]),
|
|
@@ -160,9 +350,9 @@ export default function TimePicker({
|
|
|
160
350
|
hr({ class: "no-margin" }),
|
|
161
351
|
|
|
162
352
|
div({ class: "row-container justify-center items-center gap-md p-base shrink-0" }, [
|
|
163
|
-
button({ class: "btn btn-flat", click:
|
|
164
|
-
button({ class: "btn btn-flat", click: handleNow }, "Now"),
|
|
165
|
-
button({ class: "btn", click:
|
|
353
|
+
button({ type: "button", class: "btn btn-flat", click: handleClear }, "Clear"),
|
|
354
|
+
button({ type: "button", class: "btn btn-flat", click: handleNow }, "Now"),
|
|
355
|
+
button({ type: "button", class: "btn", click: handleOK }, "OK")
|
|
166
356
|
])
|
|
167
357
|
])
|
|
168
358
|
])
|
package/src/index.mjs
CHANGED
|
@@ -17,6 +17,7 @@ export { default as NavigationBar } from "./components/NavigationBar.mjs";
|
|
|
17
17
|
export { default as PageHeader } from "./components/PageHeader.mjs";
|
|
18
18
|
export { default as PageSection } from "./components/PageSection.mjs";
|
|
19
19
|
export { default as PopoverMenu } from "./components/PopoverMenu.mjs";
|
|
20
|
+
export { default as ProgressBar } from "./components/ProgressBar.mjs";
|
|
20
21
|
export { default as RadioCheckbox } from "./components/RadioCheckbox.mjs";
|
|
21
22
|
export { default as SearchBox } from "./components/SearchBox.mjs";
|
|
22
23
|
export { default as Sidebar } from "./components/Sidebar.mjs";
|
|
@@ -29,3 +30,6 @@ export { default as VStack } from "./components/VStack.mjs";
|
|
|
29
30
|
|
|
30
31
|
export { dialogState, showDialog, hideDialog } from "./components/Dialog.mjs";
|
|
31
32
|
export { toastState, showToast, hideToast } from "./components/ToastNotification.mjs";
|
|
33
|
+
|
|
34
|
+
// Mask utilities
|
|
35
|
+
export { applyMask, validateMask, getMaskMaxLength } from "./utils/maskUtils.mjs";
|
package/src/styles/controls.css
CHANGED
|
@@ -201,6 +201,41 @@ textarea::placeholder {
|
|
|
201
201
|
color: white;
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
/* Progress Bar */
|
|
205
|
+
.progress-bar {
|
|
206
|
+
width: 100%;
|
|
207
|
+
background-color: var(--alternate-background-color);
|
|
208
|
+
border-radius: var(--min-control-radius);
|
|
209
|
+
overflow: hidden;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.progress-bar-fill {
|
|
213
|
+
height: 100%;
|
|
214
|
+
width: 0%;
|
|
215
|
+
background-color: currentColor;
|
|
216
|
+
transition: width 0.2s ease;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.progress-bar-xs {
|
|
220
|
+
height: 0.25rem;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.progress-bar-sm {
|
|
224
|
+
height: 0.375rem;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.progress-bar-md {
|
|
228
|
+
height: 0.5rem;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.progress-bar-lg {
|
|
232
|
+
height: 0.75rem;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.progress-bar-xl {
|
|
236
|
+
height: 1rem;
|
|
237
|
+
}
|
|
238
|
+
|
|
204
239
|
.badge-solid.badge-dimmed {
|
|
205
240
|
background-color: var(--highlight-background-color);
|
|
206
241
|
color: var(--color-secondary);
|
|
@@ -301,6 +336,43 @@ input::-webkit-calendar-picker-indicator {
|
|
|
301
336
|
-webkit-appearance: none;
|
|
302
337
|
}
|
|
303
338
|
|
|
339
|
+
/* DatePicker & TimePicker Input Wrapper */
|
|
340
|
+
.datepicker-input-wrapper,
|
|
341
|
+
.timepicker-input-wrapper {
|
|
342
|
+
position: relative;
|
|
343
|
+
display: flex;
|
|
344
|
+
align-items: center;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.datepicker-icon-button,
|
|
348
|
+
.timepicker-icon-button {
|
|
349
|
+
position: absolute;
|
|
350
|
+
right: var(--base-padding);
|
|
351
|
+
top: 50%;
|
|
352
|
+
transform: translateY(-50%);
|
|
353
|
+
background: none;
|
|
354
|
+
border: none;
|
|
355
|
+
padding: 0;
|
|
356
|
+
cursor: pointer;
|
|
357
|
+
display: flex;
|
|
358
|
+
align-items: center;
|
|
359
|
+
justify-content: center;
|
|
360
|
+
color: var(--color-quaternary);
|
|
361
|
+
transition: color 0.2s;
|
|
362
|
+
z-index: 1;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.datepicker-icon-button:hover:not(:disabled),
|
|
366
|
+
.timepicker-icon-button:hover:not(:disabled) {
|
|
367
|
+
color: var(--color-tertiary);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.datepicker-icon-button:disabled,
|
|
371
|
+
.timepicker-icon-button:disabled {
|
|
372
|
+
cursor: not-allowed;
|
|
373
|
+
opacity: 0.5;
|
|
374
|
+
}
|
|
375
|
+
|
|
304
376
|
textarea {
|
|
305
377
|
min-height: 100px;
|
|
306
378
|
resize: vertical;
|