mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-19 14:14:17 +00:00
feat: implement multiple aux timers
This commit is contained in:
committed by
Carlos Valente
parent
d1c58712ae
commit
bf8ed8d017
@@ -42,7 +42,15 @@
|
||||
}
|
||||
|
||||
.separator {
|
||||
width: 1px;
|
||||
height: 0.75em;
|
||||
background-color: $border-color-ondark;
|
||||
|
||||
&.horizontal {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
&.vertical {
|
||||
width: 1px;
|
||||
height: 0.75em;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,10 @@ export function Label({ children, className, ...elementProps }: LabelHTMLAttribu
|
||||
);
|
||||
}
|
||||
|
||||
export function Separator({ className, ...elementProps }: HTMLAttributes<HTMLDivElement>) {
|
||||
return <div className={cx([style.separator, className])} {...elementProps} />;
|
||||
interface SeparatorProps extends HTMLAttributes<HTMLDivElement> {
|
||||
orientation?: 'horizontal' | 'vertical';
|
||||
}
|
||||
|
||||
export function Separator({ className, orientation = 'vertical', ...elementProps }: SeparatorProps) {
|
||||
return <div className={cx([style.separator, style[orientation], className])} {...elementProps} />;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { InputHTMLAttributes } from 'react';
|
||||
import { forwardRef, InputHTMLAttributes } from 'react';
|
||||
|
||||
import { cx } from '../../../utils/styleUtils';
|
||||
|
||||
@@ -9,6 +9,18 @@ interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
height?: 'medium' | 'large';
|
||||
}
|
||||
|
||||
export default function Input({ className, variant = 'subtle', height = 'medium', ...inputProps }: InputProps) {
|
||||
return <input type='text' className={cx([style.input, style[variant], style[height], className])} {...inputProps} />;
|
||||
}
|
||||
const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
||||
{ className, variant = 'subtle', height = 'medium', ...inputProps },
|
||||
ref,
|
||||
) {
|
||||
return (
|
||||
<input
|
||||
ref={ref}
|
||||
type='text'
|
||||
className={cx([style.input, style[variant], style[height], className])}
|
||||
{...inputProps}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default Input;
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
.timeInput {
|
||||
letter-spacing: 1px;
|
||||
width: 6.5em;
|
||||
width: 100%;
|
||||
max-width: 7.5em;
|
||||
letter-spacing: 1px;
|
||||
font-size: 1rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { FocusEvent, KeyboardEvent, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Input } from '@chakra-ui/react';
|
||||
import { millisToString, parseUserTime } from 'ontime-utils';
|
||||
|
||||
import { useEmitLog } from '../../../stores/logger';
|
||||
import { cx } from '../../../utils/styleUtils';
|
||||
import Input from '../input/Input';
|
||||
|
||||
import style from './TimeInput.module.scss';
|
||||
|
||||
interface TimeInputProps<T extends string> {
|
||||
id?: T;
|
||||
name: T;
|
||||
submitHandler: (field: T, value: string) => void;
|
||||
time?: number;
|
||||
placeholder: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
align?: 'left' | 'center';
|
||||
className?: string;
|
||||
@@ -27,6 +30,9 @@ export default function TimeInput<T extends string>(props: TimeInputProps<T>) {
|
||||
*/
|
||||
const resetValue = useCallback(() => {
|
||||
try {
|
||||
if (typeof time !== 'number' || isNaN(time)) {
|
||||
throw new Error(`Invalid time value: ${time}`);
|
||||
}
|
||||
setValue(millisToString(time));
|
||||
} catch (error) {
|
||||
setValue(millisToString(0));
|
||||
@@ -121,24 +127,20 @@ export default function TimeInput<T extends string>(props: TimeInputProps<T>) {
|
||||
<Input
|
||||
id={id}
|
||||
disabled={disabled}
|
||||
size='sm'
|
||||
ref={inputRef}
|
||||
data-testid={`time-input-${name}`}
|
||||
className={className}
|
||||
fontSize='1rem'
|
||||
type='text'
|
||||
className={cx([style.timeInput, className])}
|
||||
placeholder={placeholder}
|
||||
variant='ontime-filled'
|
||||
onFocus={handleFocus}
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
onBlur={onBlurHandler}
|
||||
onKeyDown={onKeyDownHandler}
|
||||
value={value}
|
||||
maxLength={8}
|
||||
maxWidth='7.5em'
|
||||
letterSpacing='1px'
|
||||
autoComplete='off'
|
||||
textAlign={align}
|
||||
style={{
|
||||
textAlign: align,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
.select {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
height: 2rem;
|
||||
|
||||
background-color: $gray-1200;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
color: $gray-200;
|
||||
border-radius: $component-border-radius-md;
|
||||
border: 1px solid transparent;
|
||||
|
||||
padding-inline: 0.5rem;
|
||||
font-size: calc(1rem - 2px);
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: $gray-1100;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: $gray-1000;
|
||||
}
|
||||
|
||||
&[data-popup-open] {
|
||||
background-color: $gray-1000;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.selectIcon {
|
||||
color: $gray-200;
|
||||
}
|
||||
|
||||
.popup {
|
||||
font-size: calc(1rem - 2px);
|
||||
background-color: $gray-1200;
|
||||
box-sizing: border-box;
|
||||
padding: 2px;
|
||||
border-radius: $component-border-radius-md;
|
||||
color: $ui-white;
|
||||
overflow-y: auto;
|
||||
max-height: 20rem;
|
||||
border: 1px solid $gray-1000;
|
||||
|
||||
&[data-side='start'] {
|
||||
transition: 50%;
|
||||
transform: 100%;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
box-sizing: border-box;
|
||||
outline: 0;
|
||||
line-height: 1rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
|
||||
min-width: var(--anchor-width);
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
align-items: center;
|
||||
grid-template-columns: 0.75rem 1fr;
|
||||
scroll-margin-block: 1rem;
|
||||
|
||||
&[data-highlighted] {
|
||||
z-index: 0;
|
||||
position: relative;
|
||||
background-color: $blue-700;
|
||||
}
|
||||
}
|
||||
|
||||
.itemIndicator {
|
||||
grid-column-start: 1;
|
||||
}
|
||||
|
||||
.itemIndicatorIcon {
|
||||
display: block;
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
}
|
||||
|
||||
.itemLabel {
|
||||
grid-column-start: 2;
|
||||
}
|
||||
|
||||
.scrollArrow {
|
||||
width: 100%;
|
||||
background: canvas;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
cursor: default;
|
||||
border-radius: 0.375rem;
|
||||
height: 1rem;
|
||||
font-size: 0.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&[data-direction='up'] {
|
||||
&::before {
|
||||
top: -100%;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-direction='down'] {
|
||||
bottom: 0;
|
||||
|
||||
&::before {
|
||||
bottom: -100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { IoCheckmark } from 'react-icons/io5';
|
||||
import { LuChevronsUpDown } from 'react-icons/lu';
|
||||
import { Select as BaseSelect } from '@base-ui-components/react/select';
|
||||
|
||||
import styles from './Select.module.scss';
|
||||
|
||||
interface SelectProps<T extends string | null = string> {
|
||||
defaultValue?: T;
|
||||
options: {
|
||||
value: NonNullable<T>;
|
||||
label: string;
|
||||
}[];
|
||||
placeholder?: string;
|
||||
value?: T;
|
||||
onChange?: (value: NonNullable<T>) => void;
|
||||
}
|
||||
|
||||
export default function Select<T extends string | null = string>({
|
||||
defaultValue,
|
||||
options,
|
||||
placeholder,
|
||||
value,
|
||||
onChange,
|
||||
}: SelectProps<T>) {
|
||||
return (
|
||||
<BaseSelect.Root defaultValue={defaultValue} onValueChange={onChange} value={value}>
|
||||
<BaseSelect.Trigger className={styles.select}>
|
||||
<BaseSelect.Value placeholder={placeholder} />
|
||||
<BaseSelect.Icon className={styles.selectIcon}>
|
||||
<LuChevronsUpDown />
|
||||
</BaseSelect.Icon>
|
||||
</BaseSelect.Trigger>
|
||||
<BaseSelect.Portal>
|
||||
<BaseSelect.Positioner side='bottom' align='start'>
|
||||
<BaseSelect.ScrollUpArrow className={styles.scrollArrow} />
|
||||
<BaseSelect.Popup className={styles.popup}>
|
||||
{options.map((option) => {
|
||||
return (
|
||||
<BaseSelect.Item key={option.value} className={styles.item} value={option.value}>
|
||||
<BaseSelect.ItemIndicator className={styles.itemIndicator}>
|
||||
<IoCheckmark className={styles.itemIndicatorIcon} />
|
||||
</BaseSelect.ItemIndicator>
|
||||
<BaseSelect.ItemText className={styles.itemLabel}>{option.label}</BaseSelect.ItemText>
|
||||
</BaseSelect.Item>
|
||||
);
|
||||
})}
|
||||
</BaseSelect.Popup>
|
||||
<BaseSelect.ScrollDownArrow className={styles.scrollArrow} />
|
||||
</BaseSelect.Positioner>
|
||||
</BaseSelect.Portal>
|
||||
</BaseSelect.Root>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user