Improve floating elements, add form inputs for date and time with a full datepicker.
+ Add datepicker form input. + Add datepicker component. + Add calendar component. + Add timepicker form input. * Change Popover to Float, with a better library and general behavior, and some fixes. * Floating elements visual improvements.
This commit is contained in:
parent
d2d4c9cab4
commit
0faaa6b10c
25 changed files with 822 additions and 183 deletions
|
@ -1,4 +1,4 @@
|
||||||
import React from "react";
|
import React, {useState} from "react";
|
||||||
import "../index";
|
import "../index";
|
||||||
import {Checkbox} from "../src/Components/Forms/Checkbox";
|
import {Checkbox} from "../src/Components/Forms/Checkbox";
|
||||||
import { Radio } from "../src/Components/Forms/Radio";
|
import { Radio } from "../src/Components/Forms/Radio";
|
||||||
|
@ -6,11 +6,15 @@ import {FloppyDisk, TrashSimple, XCircle} from "@phosphor-icons/react";
|
||||||
import {Card} from "../src/Components/Card";
|
import {Card} from "../src/Components/Card";
|
||||||
import {PasswordInput} from "../src/Components/Forms/PasswordInput";
|
import {PasswordInput} from "../src/Components/Forms/PasswordInput";
|
||||||
import {RequiredField} from "../src/Components/Forms/RequiredField";
|
import {RequiredField} from "../src/Components/Forms/RequiredField";
|
||||||
import {Popover} from "../src/Components/Popovers/Popover";
|
import {Float} from "../src/Components/Floating/Float";
|
||||||
import {Tooltip} from "../src/Components/Popovers/Tooltip";
|
import {Tooltip} from "../src/Components/Floating/Tooltip";
|
||||||
|
import {DatepickerInput} from "../src/Components/Forms/DatepickerInput";
|
||||||
|
import {TimepickerInput} from "../src/Components/Forms/TimepickerInput";
|
||||||
|
|
||||||
export function DemoApp()
|
export function DemoApp()
|
||||||
{
|
{
|
||||||
|
const [datetime, setDatetime] = useState(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className={"app"}>
|
<main className={"app"}>
|
||||||
<h1>KernelUI</h1>
|
<h1>KernelUI</h1>
|
||||||
|
@ -73,6 +77,16 @@ export function DemoApp()
|
||||||
Test password
|
Test password
|
||||||
</PasswordInput>
|
</PasswordInput>
|
||||||
|
|
||||||
|
<DatepickerInput value={datetime} onChange={setDatetime}>
|
||||||
|
Date test
|
||||||
|
</DatepickerInput>
|
||||||
|
|
||||||
|
<TimepickerInput value={datetime} onChange={setDatetime}>
|
||||||
|
Time test
|
||||||
|
</TimepickerInput>
|
||||||
|
|
||||||
|
<p>Currently selected datetime: <strong>{datetime ? datetime.toISOString() : "none"}</strong></p>
|
||||||
|
|
||||||
<Checkbox>Checkbox demo</Checkbox>
|
<Checkbox>Checkbox demo</Checkbox>
|
||||||
<Radio name={"radio-test"}>Radio box test</Radio>
|
<Radio name={"radio-test"}>Radio box test</Radio>
|
||||||
<Radio name={"radio-test"}>Radio box test</Radio>
|
<Radio name={"radio-test"}>Radio box test</Radio>
|
||||||
|
@ -183,33 +197,33 @@ export function DemoApp()
|
||||||
|
|
||||||
<h2>Popovers</h2>
|
<h2>Popovers</h2>
|
||||||
|
|
||||||
<Popover mode={"hover"} content={"Do you see me?"}>
|
<Float mode={"hover"} content={"Do you see me?"}>
|
||||||
<button type={"button"}>Hover me!</button>
|
<button type={"button"}>Hover me!</button>
|
||||||
</Popover>
|
</Float>
|
||||||
|
|
||||||
<Popover mode={"focus"} content={<>I am <strong>focused</strong></>}>
|
<Float mode={"focus"} content={<>I am <strong>focused</strong></>}>
|
||||||
<button>Focus me!</button>
|
<button>Focus me!</button>
|
||||||
</Popover>
|
</Float>
|
||||||
|
|
||||||
<Popover mode={"click"} content={(
|
<Float mode={"click"} content={(
|
||||||
<div>
|
<div>
|
||||||
You can add complex (clickable) content in me.
|
You can add complex (clickable) content in me.
|
||||||
<button type={"button"}>OK</button>
|
<button type={"button"}>OK</button>
|
||||||
</div>
|
</div>
|
||||||
)}>
|
)}>
|
||||||
<button>Click me!</button>
|
<button>Click me!</button>
|
||||||
</Popover>
|
</Float>
|
||||||
|
|
||||||
<Popover content={"I am always shown."} popperOptions={{ placement: "top" }}>
|
<Float mode={"always"} content={"I am always shown."} floatingOptions={{ placement: "top" }}>
|
||||||
<button>Why always me?</button>
|
<button>Why always me?</button>
|
||||||
</Popover>
|
</Float>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Popover mode={"managed"} content={(show, hide) => (<button onClick={hide}>I can hide the popover!</button>)}>
|
<Float mode={"managed"} content={(show, hide) => (<button onClick={hide}>I can hide the popover!</button>)}>
|
||||||
{(show, hide) => (
|
{(show, hide) => (
|
||||||
<button type={"button"} onClick={show}>Customized behavior</button>
|
<button type={"button"} onClick={show}>Customized behavior</button>
|
||||||
)}
|
)}
|
||||||
</Popover>
|
</Float>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>Tooltips</h2>
|
<h2>Tooltips</h2>
|
||||||
|
|
|
@ -11,13 +11,13 @@
|
||||||
"types": "lib/index.d.ts",
|
"types": "lib/index.d.ts",
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@floating-ui/react": "^0.26.17",
|
||||||
"@fontsource-variable/jetbrains-mono": "^5.0.21",
|
"@fontsource-variable/jetbrains-mono": "^5.0.21",
|
||||||
"@fontsource-variable/manrope": "^5.0.20",
|
"@fontsource-variable/manrope": "^5.0.20",
|
||||||
"@fontsource-variable/source-serif-4": "^5.0.19",
|
"@fontsource-variable/source-serif-4": "^5.0.19",
|
||||||
"@phosphor-icons/react": "^2.1.5",
|
"@phosphor-icons/react": "^2.1.5",
|
||||||
"@popperjs/core": "^2.11.8",
|
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-popper": "^2.3.0"
|
"react-merge-refs": "^2.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
|
|
107
src/Components/Dates/Calendar.tsx
Normal file
107
src/Components/Dates/Calendar.tsx
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
import React, {useMemo} from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calendar component.
|
||||||
|
*/
|
||||||
|
export function Calendar({date, onDateSelected, locale, className, ...tableProps}: {
|
||||||
|
date: Date;
|
||||||
|
onDateSelected: (date: Date) => void;
|
||||||
|
locale?: string;
|
||||||
|
} & React.TableHTMLAttributes<HTMLTableElement>): React.ReactElement
|
||||||
|
{
|
||||||
|
locale = useMemo(() => (locale ?? "fr"), [locale]);
|
||||||
|
|
||||||
|
const currentMonthHeader = useMemo(() => (
|
||||||
|
<tr>
|
||||||
|
{ // For each day of the week, showing its name.
|
||||||
|
[1, 2, 3, 4, 5, 6, 7].map(day => {
|
||||||
|
// Getting a date with the right day of the week.
|
||||||
|
const dayOfWeek = new Date(1970, 0, 4 + day);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<th key={day}>
|
||||||
|
{(new Intl.DateTimeFormat(locale, {weekday: "short"})).format(dayOfWeek)}
|
||||||
|
</th>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</tr>
|
||||||
|
), [date]);
|
||||||
|
|
||||||
|
const currentMonthTable = useMemo(() => {
|
||||||
|
// Initialize weeks.
|
||||||
|
const weeksRows = [];
|
||||||
|
let currentWeek = [];
|
||||||
|
|
||||||
|
// Get start date of the calendar.
|
||||||
|
let currentDate = new Date(date);
|
||||||
|
currentDate.setDate(1); // First day of the month.
|
||||||
|
currentDate.setDate(currentDate.getDate() - (currentDate.getDay() - 1 + 7) % 7); // Searching the start of the first week of the current month.
|
||||||
|
|
||||||
|
// Get last day of the calendar.
|
||||||
|
const lastDate = new Date(date);
|
||||||
|
lastDate.setMonth(lastDate.getMonth() + 1, 0); // Get the last day of the month.
|
||||||
|
lastDate.setDate(lastDate.getDate() + (7 - lastDate.getDay())); // Searching the end of the last week of the current month.
|
||||||
|
|
||||||
|
|
||||||
|
while (currentDate.getTime() <= lastDate.getTime() || weeksRows.length < 6)
|
||||||
|
{ // While the current date is before or is the last day of the current view,
|
||||||
|
// adding the current day to the current week.
|
||||||
|
currentWeek.push(
|
||||||
|
<Day key={`${currentDate.getFullYear()}-${currentDate.getMonth()}-${currentDate.getDate()}`}
|
||||||
|
date={new Date(currentDate)}
|
||||||
|
faded={date.getMonth() != currentDate.getMonth()}
|
||||||
|
selected={date.getTime() == currentDate.getTime()}
|
||||||
|
onClick={onDateSelected} />
|
||||||
|
);
|
||||||
|
|
||||||
|
// We're on sunday, adding the current week and creating a new one.
|
||||||
|
if (currentDate.getDay() == 0)
|
||||||
|
{ // The current week is ended, adding it and creating a new one.
|
||||||
|
weeksRows.push(
|
||||||
|
<tr key={`${currentDate.getFullYear()}-${currentDate.getMonth()}-${currentDate.getDate()}`}>
|
||||||
|
{currentWeek}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
currentWeek = []; // Reset the current week to a new one.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the next day.
|
||||||
|
currentDate.setDate(currentDate.getDate() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return generated weeks rows.
|
||||||
|
return weeksRows;
|
||||||
|
}, [date, onDateSelected]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<table className={`calendar${className ? ` ${className}` : ""}`} {...tableProps}>
|
||||||
|
<thead>
|
||||||
|
{currentMonthHeader}
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{currentMonthTable}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calendar day component.
|
||||||
|
*/
|
||||||
|
function Day({date, onClick, faded, selected}: {
|
||||||
|
date: Date;
|
||||||
|
onClick?: (date: Date) => void;
|
||||||
|
faded: boolean;
|
||||||
|
selected: boolean;
|
||||||
|
}): React.ReactElement
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<td key={`${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`}>
|
||||||
|
<a className={`day${faded ? " faded" : ""}${selected ? " selected" : ""}`}
|
||||||
|
onClick={() => onClick?.(date)}>
|
||||||
|
{date.getDate()}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
64
src/Components/Dates/Datepicker.tsx
Normal file
64
src/Components/Dates/Datepicker.tsx
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import React, {useCallback, useMemo} from "react";
|
||||||
|
import {CaretLeft, CaretRight} from "@phosphor-icons/react";
|
||||||
|
import {Tooltip} from "../Floating/Tooltip";
|
||||||
|
import {Calendar} from "./Calendar";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Datepicker component.
|
||||||
|
*/
|
||||||
|
export function Datepicker({date, onDateSelected, locale, className, ...divProps}: {
|
||||||
|
date: Date;
|
||||||
|
onDateSelected: (date: Date) => void;
|
||||||
|
locale?: string;
|
||||||
|
} & React.HTMLAttributes<HTMLDivElement>): React.ReactElement
|
||||||
|
{
|
||||||
|
locale = useMemo(() => (locale ?? "fr"), [locale]);
|
||||||
|
|
||||||
|
// Get previous month name.
|
||||||
|
const previousMonthName = useMemo(() => {
|
||||||
|
// Copy the current date and get back one month earlier.
|
||||||
|
const previousMonthDate = new Date(date);
|
||||||
|
previousMonthDate.setMonth(previousMonthDate.getMonth() - 1);
|
||||||
|
return (new Intl.DateTimeFormat(locale, { month: "long", year: "numeric" })).format(previousMonthDate);
|
||||||
|
}, [date]);
|
||||||
|
// Get next month name.
|
||||||
|
const nextMonthName = useMemo(() => {
|
||||||
|
// Copy the current date and go to one month later.
|
||||||
|
const nextMonthDate = new Date(date);
|
||||||
|
nextMonthDate.setMonth(nextMonthDate.getMonth() + 1);
|
||||||
|
return (new Intl.DateTimeFormat(locale, { month: "long", year: "numeric" })).format(nextMonthDate);
|
||||||
|
}, [date]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`datepicker${className ? ` ${className}` : ""}`} {...divProps}>
|
||||||
|
<div className={"year-month"}>
|
||||||
|
{(new Intl.DateTimeFormat(locale, {month: "long", year: "numeric"})).format(date)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Calendar date={date} onDateSelected={onDateSelected} />
|
||||||
|
|
||||||
|
<Tooltip content={previousMonthName}>
|
||||||
|
<button type={"button"} className={"previous-month"} onClick={
|
||||||
|
useCallback((event) => {
|
||||||
|
const newDate = new Date(date);
|
||||||
|
newDate.setMonth(newDate.getMonth() - 1);
|
||||||
|
onDateSelected(newDate);
|
||||||
|
}, [date, onDateSelected])
|
||||||
|
}>
|
||||||
|
<CaretLeft weight={"bold"}/>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content={nextMonthName}>
|
||||||
|
<button type={"button"} className={"next-month"} onClick={
|
||||||
|
useCallback((event) => {
|
||||||
|
const newDate = new Date(date);
|
||||||
|
newDate.setMonth(newDate.getMonth() + 1);
|
||||||
|
onDateSelected(newDate);
|
||||||
|
}, [date, onDateSelected])
|
||||||
|
}>
|
||||||
|
<CaretRight weight={"bold"}/>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
122
src/Components/Floating/Float.tsx
Normal file
122
src/Components/Floating/Float.tsx
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
import React, {useCallback, useMemo, useState} from "react";
|
||||||
|
import {Card} from "../Card";
|
||||||
|
import {
|
||||||
|
shift,
|
||||||
|
useClick,
|
||||||
|
useFloating,
|
||||||
|
useFocus,
|
||||||
|
useHover,
|
||||||
|
useInteractions, useTransitionStyles
|
||||||
|
} from "@floating-ui/react";
|
||||||
|
import {UseFloatingOptions} from "@floating-ui/react/dist/floating-ui.react";
|
||||||
|
import {mergeRefs} from "react-merge-refs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fully managed floating content function.
|
||||||
|
*/
|
||||||
|
export type Managed<T = React.ReactElement|React.ReactNode> = (show: () => void, hide: () => void) => T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allowed floating modes.
|
||||||
|
*/
|
||||||
|
export type FloatingMode = "always"|"click"|"hover"|"focus"|"managed";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A component to show something floating next to an element.
|
||||||
|
*/
|
||||||
|
export function Float({children, content, className, mode, floatingOptions}: {
|
||||||
|
children: (React.ReactElement & React.ClassAttributes<HTMLElement>)|Managed<(React.ReactElement & React.ClassAttributes<HTMLElement>)>;
|
||||||
|
content?: React.ReactNode|Managed<React.ReactNode>;
|
||||||
|
className?: string;
|
||||||
|
mode?: FloatingMode;
|
||||||
|
floatingOptions?: UseFloatingOptions;
|
||||||
|
}): React.ReactElement
|
||||||
|
{
|
||||||
|
// By default, use "always" mode.
|
||||||
|
if (!mode) mode = "always";
|
||||||
|
|
||||||
|
// Followed show status.
|
||||||
|
const [shown, setShown] = useState(false);
|
||||||
|
|
||||||
|
// Create show / hide functions.
|
||||||
|
const show = useCallback(() => setShown(true), [setShown]);
|
||||||
|
const hide = useCallback(() => setShown(false), [setShown]);
|
||||||
|
|
||||||
|
// If show mode is "always", always show the floating part after render.
|
||||||
|
if (mode == "always")
|
||||||
|
{
|
||||||
|
setTimeout(() => {
|
||||||
|
setShown(true);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Floating initialization.
|
||||||
|
const { refs, floatingStyles, context } = useFloating(
|
||||||
|
useMemo(() => (Object.assign({
|
||||||
|
open: shown,
|
||||||
|
onOpenChange: setShown,
|
||||||
|
middleware: [shift()],
|
||||||
|
} as UseFloatingOptions, floatingOptions)), [floatingOptions, shown, setShown])
|
||||||
|
);
|
||||||
|
|
||||||
|
// Interactions initialization.
|
||||||
|
const hover = useHover(context, useMemo(() => ({ enabled: mode == "hover" }), [mode]));
|
||||||
|
const focus = useFocus(context, useMemo(() => ({ enabled: mode == "focus", visibleOnly: false }), [mode]));
|
||||||
|
const click = useClick(context, useMemo(() => ({ enabled: mode == "click" }), [mode]));
|
||||||
|
const {getReferenceProps, getFloatingProps} = useInteractions([hover, focus, click]);
|
||||||
|
|
||||||
|
// Transition configuration.
|
||||||
|
const {isMounted, styles: transitionStyles} = useTransitionStyles(context, {
|
||||||
|
duration: 200,
|
||||||
|
common: ({side}) => ({
|
||||||
|
transformOrigin: {
|
||||||
|
top: "bottom",
|
||||||
|
bottom: "top",
|
||||||
|
left: "right",
|
||||||
|
right: "left",
|
||||||
|
}[side],
|
||||||
|
}),
|
||||||
|
initial: {
|
||||||
|
transform: "scale(0.66)",
|
||||||
|
opacity: "0",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Change the child element to use the reference and the interactions properties.
|
||||||
|
const referencedChild = useMemo(() => {
|
||||||
|
// Render the children if a managed floating function is passed.
|
||||||
|
const child = typeof children == "function" ? children(show, hide) : children;
|
||||||
|
|
||||||
|
return React.cloneElement(child,
|
||||||
|
Object.assign(
|
||||||
|
{
|
||||||
|
// Pass references.
|
||||||
|
ref: mergeRefs([refs.setReference, child?.ref]),
|
||||||
|
},
|
||||||
|
// Get interaction properties.
|
||||||
|
getReferenceProps(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}, [children, show, hide, refs.setReference, getReferenceProps]);
|
||||||
|
|
||||||
|
// Update floating content.
|
||||||
|
const floatingContent = useMemo(() => (
|
||||||
|
// Render the children if a managed floating function is passed.
|
||||||
|
typeof content == "function" ? content(show, hide) : content
|
||||||
|
), [shown, show, hide, content]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{referencedChild}
|
||||||
|
|
||||||
|
{ // Showing floating element if the state says to do so.
|
||||||
|
isMounted &&
|
||||||
|
<div ref={refs.setFloating} style={floatingStyles} {...getFloatingProps()} className={"floating"}>
|
||||||
|
<Card style={transitionStyles} className={`floating${className ? ` ${className}` : ""}`}>
|
||||||
|
{floatingContent}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {Popover} from "./Popover";
|
import {Float} from "./Float";
|
||||||
|
|
||||||
export function Tooltip({children, content}: {
|
export function Tooltip({children, content}: {
|
||||||
children: React.ReactElement;
|
children: React.ReactElement;
|
||||||
|
@ -7,8 +7,8 @@ export function Tooltip({children, content}: {
|
||||||
}): React.ReactElement
|
}): React.ReactElement
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<Popover mode={"hover"} content={content} className={"tooltip"} popperOptions={{ placement: "top" }}>
|
<Float mode={"hover"} content={content} className={"tooltip"} floatingOptions={{ placement: "top" }}>
|
||||||
{children}
|
{children}
|
||||||
</Popover>
|
</Float>
|
||||||
);
|
);
|
||||||
}
|
}
|
155
src/Components/Forms/DatepickerInput.tsx
Normal file
155
src/Components/Forms/DatepickerInput.tsx
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
import React, {useCallback, useEffect, useMemo, useRef, useState} from "react";
|
||||||
|
import {formatDate, Modify} from "../../Utils";
|
||||||
|
import {Float} from "../Floating/Float";
|
||||||
|
import {Datepicker} from "../Dates/Datepicker";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A form input for a date with a datepicker.
|
||||||
|
*/
|
||||||
|
export function DatepickerInput(
|
||||||
|
{
|
||||||
|
children, className,
|
||||||
|
value, onChange,
|
||||||
|
// Properties to pass down.
|
||||||
|
onKeyUp, onBlur,
|
||||||
|
// Already set properties.
|
||||||
|
type, placeholder,
|
||||||
|
...props}: React.PropsWithChildren<Modify<React.InputHTMLAttributes<HTMLInputElement>, {
|
||||||
|
/**
|
||||||
|
* The current date value.
|
||||||
|
*/
|
||||||
|
value?: Date|null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when picked date is changed.
|
||||||
|
* @param newDate The new date.
|
||||||
|
*/
|
||||||
|
onChange: (newDate: Date) => void;
|
||||||
|
|
||||||
|
// Already set properties.
|
||||||
|
type?: never;
|
||||||
|
placeholder?: never;
|
||||||
|
}>>): React.ReactElement
|
||||||
|
{
|
||||||
|
// Date text state.
|
||||||
|
const [dateText, setDateText] = useState("");
|
||||||
|
|
||||||
|
// Update date text when date value has changed.
|
||||||
|
useEffect(() => {
|
||||||
|
if (value && value instanceof Date && !isNaN(value.getTime()))
|
||||||
|
setDateText(formatDate(value));
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
// Check if date is valid.
|
||||||
|
const invalidDate = useMemo(() => !(value && value instanceof Date && !isNaN(value.getTime())) && dateText.length > 0, [value, dateText]);
|
||||||
|
const dateValue = useMemo(() => invalidDate ? new Date() : (value ?? new Date()), [invalidDate, value]);
|
||||||
|
|
||||||
|
const inputRef = useRef<HTMLInputElement>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit a new date from its raw text.
|
||||||
|
*/
|
||||||
|
const submitDate = useCallback((customDateText?: string) => {
|
||||||
|
// Get the current date text status.
|
||||||
|
const dateTextMatch = (customDateText ?? dateText).match(/^([0-9]?[0-9])\/([0-9]?[0-9])\/([0-9]?[0-9]?[0-9]?[0-9])$/);
|
||||||
|
|
||||||
|
if (dateTextMatch)
|
||||||
|
{
|
||||||
|
// Parse day.
|
||||||
|
let day = dateTextMatch[1];
|
||||||
|
if (day.length < 2) day = "0" + day;
|
||||||
|
if (parseInt(day) <= 0) day = "01";
|
||||||
|
|
||||||
|
// Parse month.
|
||||||
|
let month = dateTextMatch[2];
|
||||||
|
if (month.length < 2) month = "0" + month;
|
||||||
|
if (parseInt(month) <= 0) month = "01";
|
||||||
|
|
||||||
|
// Parse year.
|
||||||
|
let year = parseInt(dateTextMatch[3]);
|
||||||
|
if (year < 100)
|
||||||
|
{
|
||||||
|
year += 1900;
|
||||||
|
if ((new Date()).getFullYear() - year > 96) year += 100;
|
||||||
|
}
|
||||||
|
if (year < 1000) year += 1000;
|
||||||
|
|
||||||
|
// Try to build the structurally valid date.
|
||||||
|
const date = new Date(year + "-" + month + "-" + day);
|
||||||
|
|
||||||
|
if (!isNaN(date.getTime()))
|
||||||
|
{ // Date is valid, checking that it uses the right month (to fix the behavior when we go change 31/03 to 31/02, we want 28/02 or 29/02 and not 01/03).
|
||||||
|
if ((date.getMonth() + 1) > parseInt(month))
|
||||||
|
{ // Current date month is not valid, we're getting back to it.
|
||||||
|
date.setDate(date.getDate() - 1);
|
||||||
|
}
|
||||||
|
if ((date.getMonth() + 1) < parseInt(month))
|
||||||
|
{ // Current date month is not valid, we're getting back to it.
|
||||||
|
date.setDate(date.getDate() + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to keep original hours.
|
||||||
|
date.setHours(value?.getHours() ?? 0, value?.getMinutes() ?? 0, value?.getSeconds() ?? 0, value?.getMilliseconds() ?? 0);
|
||||||
|
|
||||||
|
// Set the structurally valid date.
|
||||||
|
onChange?.(date);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
// No structurally valid date, removing it.
|
||||||
|
onChange?.(null);
|
||||||
|
}, [dateText, onChange]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label className={`datepicker-input${invalidDate ? " error" : ""}${className ? ` ${className}` : ""}`}
|
||||||
|
// Keeping focus on the input when something else in the label takes it.
|
||||||
|
onFocus={useCallback(() => inputRef.current?.focus(), [inputRef])}>
|
||||||
|
{children}
|
||||||
|
|
||||||
|
<Float mode={"focus"} className={"datepicker"} content={
|
||||||
|
<Datepicker date={dateValue} onDateSelected={onChange} />
|
||||||
|
}>
|
||||||
|
<input type={"text"} ref={inputRef} placeholder={"DD/MM/AAAA"} value={dateText}
|
||||||
|
onChange={useCallback((event) => {
|
||||||
|
let dateText = event.currentTarget.value;
|
||||||
|
|
||||||
|
// Add first '/' if missing.
|
||||||
|
if (dateText.match(/^[0-9]{2}[^\/]*$/))
|
||||||
|
dateText = dateText.substring(0, 2) + "/" + dateText.substring(2);
|
||||||
|
|
||||||
|
// Add second '/' if missing.
|
||||||
|
if (dateText.match(/^[0-9]{2}\/[0-9]{2}[^\/]*$/))
|
||||||
|
dateText = dateText.substring(0, 5) + "/" + dateText.substring(5);
|
||||||
|
|
||||||
|
// Set the new date text.
|
||||||
|
setDateText(dateText);
|
||||||
|
|
||||||
|
// If a full date has been entered, reading it.
|
||||||
|
const fullDateMatch = dateText.match(/^([0-9]{2})\/([0-9]{2})\/([0-9]{4})$/);
|
||||||
|
if (fullDateMatch)
|
||||||
|
{ // We have a structurally valid date, submitting it.
|
||||||
|
submitDate(dateText);
|
||||||
|
}
|
||||||
|
}, [submitDate])}
|
||||||
|
onKeyUp={useCallback((event) => {
|
||||||
|
if (event.key == "Enter")
|
||||||
|
// Submit date when enter is pressed.
|
||||||
|
submitDate();
|
||||||
|
return onKeyUp?.(event);
|
||||||
|
}, [submitDate, onKeyUp])}
|
||||||
|
onBlur={useCallback((event) => {
|
||||||
|
// Submit date when leaving form input.
|
||||||
|
submitDate();
|
||||||
|
return onBlur?.(event);
|
||||||
|
}, [submitDate, onBlur])}
|
||||||
|
{...props} />
|
||||||
|
</Float>
|
||||||
|
|
||||||
|
{ // The date is invalid, showing a subtext to say so.
|
||||||
|
invalidDate && (
|
||||||
|
<span className={"error subtext"}>Invalid date.</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
117
src/Components/Forms/TimepickerInput.tsx
Normal file
117
src/Components/Forms/TimepickerInput.tsx
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
import React, {useCallback, useEffect, useMemo, useState} from "react";
|
||||||
|
import {formatTime, Modify} from "../../Utils";
|
||||||
|
|
||||||
|
export function TimepickerInput(
|
||||||
|
{
|
||||||
|
children, className,
|
||||||
|
value, onChange,
|
||||||
|
// Properties to pass down.
|
||||||
|
onKeyUp, onBlur,
|
||||||
|
// Already set properties.
|
||||||
|
type, placeholder,
|
||||||
|
...props
|
||||||
|
}: React.PropsWithChildren<Modify<React.InputHTMLAttributes<HTMLInputElement>, {
|
||||||
|
value?: Date|null;
|
||||||
|
|
||||||
|
onChange: (newDateTime: Date) => void;
|
||||||
|
|
||||||
|
// Already set properties.
|
||||||
|
type?: never;
|
||||||
|
placeholder?: never;
|
||||||
|
}>>): React.ReactElement
|
||||||
|
{
|
||||||
|
// Time text state.
|
||||||
|
const [timeText, setTimeText] = useState("");
|
||||||
|
|
||||||
|
// Update time text when datetime value has changed.
|
||||||
|
useEffect(() => {
|
||||||
|
if (value && value instanceof Date && !isNaN(value.getTime()))
|
||||||
|
setTimeText(formatTime(value));
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
// Check if time is valid.
|
||||||
|
const invalidTime = useMemo(() => !(value && value instanceof Date && !isNaN(value.getTime())) && timeText.length > 0, [value, timeText]);
|
||||||
|
const timeValue = useMemo(() => invalidTime ? new Date() : (value ?? new Date()), [invalidTime, value]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit a new time from its raw text.
|
||||||
|
*/
|
||||||
|
const submitTime = useCallback((customTimeText?: string) => {
|
||||||
|
// Get the current date text status.
|
||||||
|
const timeTextMatch = (customTimeText ?? timeText).match(/^([0-2]?[0-9]):([0-5]?[0-9])$/);
|
||||||
|
|
||||||
|
if (timeTextMatch)
|
||||||
|
{
|
||||||
|
// Parse hours.
|
||||||
|
let rawHours = timeTextMatch[1];
|
||||||
|
if (rawHours.length < 2) rawHours = "0" + rawHours;
|
||||||
|
let hours = parseInt(rawHours);
|
||||||
|
if (isNaN(hours)) hours = 0;
|
||||||
|
|
||||||
|
// Parse minutes.
|
||||||
|
let rawMinutes = timeTextMatch[2];
|
||||||
|
if (rawMinutes.length < 2) rawMinutes = "0" + rawMinutes;
|
||||||
|
let minutes = parseInt(rawMinutes);
|
||||||
|
if (isNaN(minutes)) minutes = 0;
|
||||||
|
|
||||||
|
//TODO
|
||||||
|
// Parse seconds?
|
||||||
|
|
||||||
|
// Try to build the structurally valid date with this time.
|
||||||
|
//TODO
|
||||||
|
const datetime = new Date(timeValue);
|
||||||
|
datetime.setHours(hours, minutes);
|
||||||
|
// Put the date back, if it changed to another day.
|
||||||
|
datetime.setFullYear(timeValue.getFullYear(), timeValue.getMonth(), timeValue.getDate());
|
||||||
|
|
||||||
|
// Set the structurally valid date.
|
||||||
|
onChange?.(datetime);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
// No structurally valid date, removing it.
|
||||||
|
onChange?.(null);
|
||||||
|
}, [timeText, timeValue, onChange]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label className={`timepicker${className ? ` ${className}` : ""}`}>
|
||||||
|
{children}
|
||||||
|
|
||||||
|
<input type={"text"} placeholder={"HH:MM"} value={timeText}
|
||||||
|
onChange={useCallback((event) => {
|
||||||
|
let timeText = event.currentTarget.value;
|
||||||
|
|
||||||
|
// Add first ':' if missing.
|
||||||
|
if (timeText.match(/^[0-9]{2}[^:]*$/))
|
||||||
|
timeText = timeText.substring(0, 2) + ":" + timeText.substring(2);
|
||||||
|
|
||||||
|
// Set the new time text.
|
||||||
|
setTimeText(timeText);
|
||||||
|
|
||||||
|
// If a full time has been entered, reading it.
|
||||||
|
const fullTimeMatch = timeText.match(/^([0-2][0-9]):([0-5][0-9])$/);
|
||||||
|
if (fullTimeMatch)
|
||||||
|
{ // We have a structurally valid time, submitting it.
|
||||||
|
submitTime(timeText);
|
||||||
|
}
|
||||||
|
}, [submitTime])}
|
||||||
|
onKeyUp={useCallback((event) => {
|
||||||
|
if (event.key == "Enter")
|
||||||
|
// Submit time when enter is pressed.
|
||||||
|
submitTime();
|
||||||
|
return onKeyUp?.(event);
|
||||||
|
}, [submitTime, onKeyUp])}
|
||||||
|
onBlur={useCallback((event) => {
|
||||||
|
// Submit time when leaving form input.
|
||||||
|
submitTime();
|
||||||
|
return onBlur?.(event);
|
||||||
|
}, [submitTime, onBlur])}
|
||||||
|
{...props} />
|
||||||
|
|
||||||
|
{ // The date is invalid, showing a subtext to say so.
|
||||||
|
invalidTime && (
|
||||||
|
<span className={"error subtext"}>Invalid time.</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,96 +0,0 @@
|
||||||
import React, {useMemo, useRef, useState} from "react";
|
|
||||||
import {usePopper} from "react-popper";
|
|
||||||
import * as PopperJS from "@popperjs/core";
|
|
||||||
import {Card} from "../Card";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fully managed popover content function.
|
|
||||||
*/
|
|
||||||
export type Managed<T = React.ReactElement|React.ReactNode> = (show: () => void, hide: () => void) => T;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Allowed popover modes.
|
|
||||||
*/
|
|
||||||
export type PopoverMode = "always"|"click"|"hover"|"focus"|"managed";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A component to show something next to an element.
|
|
||||||
*/
|
|
||||||
export function Popover({children, content, className, mode, popperOptions}: {
|
|
||||||
children: React.ReactElement|Managed<React.ReactElement>;
|
|
||||||
content?: React.ReactNode|Managed<React.ReactNode>;
|
|
||||||
className?: string;
|
|
||||||
mode?: PopoverMode;
|
|
||||||
popperOptions?: Partial<PopperJS.Options>;
|
|
||||||
}): React.ReactElement
|
|
||||||
{
|
|
||||||
// By default, use "always" mode.
|
|
||||||
if (!mode) mode = "always";
|
|
||||||
|
|
||||||
// Followed show status.
|
|
||||||
const [shown, setShown] = useState(false);
|
|
||||||
|
|
||||||
// Create show / hide functions.
|
|
||||||
const show = useMemo(() => (!shown ? () => setShown(true) : () => {}), [shown]);
|
|
||||||
const hide = useMemo(() => (shown ? () => setShown(false) : () => {}), [shown]);
|
|
||||||
|
|
||||||
// If show mode is "always", always show the popover after render.
|
|
||||||
setTimeout(() => {
|
|
||||||
if (mode == "always") setShown(true);
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
// HTML elements references.
|
|
||||||
const referenceElement = useRef();
|
|
||||||
const popperElement = useRef();
|
|
||||||
|
|
||||||
// Change the child element to use the reference.
|
|
||||||
const referencedChild = useMemo(() => (
|
|
||||||
React.cloneElement(
|
|
||||||
// Render the children if a managed popover function is passed.
|
|
||||||
typeof children == "function" ? children(show, hide) : children,
|
|
||||||
Object.assign(
|
|
||||||
{
|
|
||||||
ref: referenceElement,
|
|
||||||
},
|
|
||||||
// Pass click event if click mode is enabled.
|
|
||||||
mode == "click" ? {
|
|
||||||
onClick: (event: React.MouseEvent): void => {
|
|
||||||
setShown(!shown);
|
|
||||||
},
|
|
||||||
} : {},
|
|
||||||
// Pass focus events if focus mode is enabled.
|
|
||||||
mode == "focus" ? {
|
|
||||||
onFocus: show,
|
|
||||||
onBlur: hide,
|
|
||||||
} : {},
|
|
||||||
// Pass the hover event if hover mode is enabled.
|
|
||||||
mode == "hover" ? {
|
|
||||||
onMouseEnter: show,
|
|
||||||
onMouseOut: hide,
|
|
||||||
} : {},
|
|
||||||
))
|
|
||||||
), [mode, shown, show, hide, children]);
|
|
||||||
|
|
||||||
// Update popover content.
|
|
||||||
const popoverContent = useMemo(() => (
|
|
||||||
// Render the children if a managed popover function is passed.
|
|
||||||
typeof content == "function" ? content(show, hide) : content
|
|
||||||
), [shown, show, hide, content]);
|
|
||||||
|
|
||||||
// Initialize popper.
|
|
||||||
const { styles, attributes } = usePopper(
|
|
||||||
referenceElement?.current, popperElement?.current, popperOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{referencedChild}
|
|
||||||
|
|
||||||
<div ref={popperElement} style={styles.popper} {...attributes.popper} className={"popover"} hidden={!shown}>
|
|
||||||
<Card className={`popover${className ? ` ${className}` : ""}`}>
|
|
||||||
{popoverContent}
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
12
src/Utils.tsx
Normal file
12
src/Utils.tsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
|
||||||
|
export type Modify<T, R> = Omit<T, keyof R> & R;
|
||||||
|
|
||||||
|
export function formatDate(date: Date): string
|
||||||
|
{
|
||||||
|
return ((date.getDate() < 10 ? "0" : "") + date.getDate()) + "/" + (((date.getMonth() + 1) < 10 ? "0" : "") + (date.getMonth() + 1)) + "/" + date.getFullYear();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatTime(date: Date): string
|
||||||
|
{
|
||||||
|
return (date.getHours() < 10 ? "0" : "") + date.getHours() + ":" + (date.getMinutes() < 10 ? "0" : "") + date.getMinutes();
|
||||||
|
}
|
|
@ -1,10 +1,11 @@
|
||||||
|
|
||||||
@import "components/_button";
|
@import "components/_button";
|
||||||
@import "components/_card";
|
@import "components/_card";
|
||||||
|
@import "components/_dates";
|
||||||
@import "components/_form";
|
@import "components/_form";
|
||||||
@import "components/_headings";
|
@import "components/_headings";
|
||||||
@import "components/_link";
|
@import "components/_link";
|
||||||
@import "components/_list";
|
@import "components/_list";
|
||||||
@import "components/_popover";
|
@import "components/_floating";
|
||||||
@import "components/_steps";
|
@import "components/_steps";
|
||||||
@import "components/_table";
|
@import "components/_table";
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.popover
|
&.floating
|
||||||
{
|
{
|
||||||
width: 20em;
|
width: 20em;
|
||||||
}
|
}
|
||||||
|
|
3
src/styles/components/_dates.less
Normal file
3
src/styles/components/_dates.less
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
|
||||||
|
@import "dates/_calendar";
|
||||||
|
@import "dates/_datepicker";
|
2
src/styles/components/_floating.less
Normal file
2
src/styles/components/_floating.less
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
@import "floating/_floating";
|
||||||
|
@import "floating/_tooltip";
|
|
@ -1,5 +1,6 @@
|
||||||
|
|
||||||
@import "forms/_box";
|
@import "forms/_box";
|
||||||
|
@import "forms/_datepicker-input";
|
||||||
@import "forms/_input";
|
@import "forms/_input";
|
||||||
@import "forms/_label";
|
@import "forms/_label";
|
||||||
@import "forms/_password-input";
|
@import "forms/_password-input";
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
@import "popovers/_popover";
|
|
||||||
@import "popovers/_tooltip";
|
|
57
src/styles/components/dates/_calendar.less
Normal file
57
src/styles/components/dates/_calendar.less
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
table.calendar
|
||||||
|
{
|
||||||
|
thead th
|
||||||
|
{
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody > tr
|
||||||
|
{
|
||||||
|
border-bottom: none;
|
||||||
|
|
||||||
|
&:nth-child(even) { background: transparent; }
|
||||||
|
&:hover { background: transparent; }
|
||||||
|
|
||||||
|
> td
|
||||||
|
{
|
||||||
|
padding: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a.day
|
||||||
|
{
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
width: 2.2em;
|
||||||
|
height: 2.2em;
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
background: transparent;
|
||||||
|
color: var(--foreground);
|
||||||
|
|
||||||
|
font-weight: 400;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
{
|
||||||
|
background: var(--background-darker);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected
|
||||||
|
{
|
||||||
|
background: var(--primary);
|
||||||
|
color: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.faded
|
||||||
|
{
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
44
src/styles/components/dates/_datepicker.less
Normal file
44
src/styles/components/dates/_datepicker.less
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
.datepicker
|
||||||
|
{
|
||||||
|
position: relative;
|
||||||
|
padding: 0 1em;
|
||||||
|
background: var(--background-lighter);
|
||||||
|
font-size: 0.9em;
|
||||||
|
|
||||||
|
.year-month
|
||||||
|
{
|
||||||
|
font-size: 1.1em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
button
|
||||||
|
{
|
||||||
|
&.previous-month, &.next-month
|
||||||
|
{
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
margin: auto;
|
||||||
|
padding: 0;
|
||||||
|
width: 2em;
|
||||||
|
height: 2em;
|
||||||
|
|
||||||
|
svg
|
||||||
|
{ margin: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
&.previous-month
|
||||||
|
{
|
||||||
|
left: -1.1em;
|
||||||
|
}
|
||||||
|
&.next-month
|
||||||
|
{
|
||||||
|
right: -1.1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
src/styles/components/floating/_floating.less
Normal file
10
src/styles/components/floating/_floating.less
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
:not(.card).floating
|
||||||
|
{
|
||||||
|
display: block;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
> .card.floating
|
||||||
|
{
|
||||||
|
margin: 0.4em;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
.card.popover.tooltip
|
.card.floating.tooltip
|
||||||
{
|
{
|
||||||
width: unset;
|
width: unset;
|
||||||
max-width: 20em;
|
max-width: 20em;
|
9
src/styles/components/forms/_datepicker-input.less
Normal file
9
src/styles/components/forms/_datepicker-input.less
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
label.datepicker-input
|
||||||
|
{
|
||||||
|
.card.datepicker
|
||||||
|
{
|
||||||
|
padding: 0.2em;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: unset;
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,4 +11,21 @@ label
|
||||||
{
|
{
|
||||||
&::after { content: "*"; color: var(--red); font-weight: 600; }
|
&::after { content: "*"; color: var(--red); font-weight: 600; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.error
|
||||||
|
{
|
||||||
|
input, textarea, select { border-color: var(--red); }
|
||||||
|
}
|
||||||
|
|
||||||
|
span.subtext
|
||||||
|
{
|
||||||
|
display: block;
|
||||||
|
font-size: 0.9em;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&.error
|
||||||
|
{
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
0
src/styles/components/forms/_timepicker-input.less
Normal file
0
src/styles/components/forms/_timepicker-input.less
Normal file
|
@ -1,27 +0,0 @@
|
||||||
:not(.card).popover
|
|
||||||
{
|
|
||||||
transition: opacity 0.2s ease;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
z-index: 1;
|
|
||||||
|
|
||||||
opacity: 1;
|
|
||||||
|
|
||||||
&[data-popper-placement="top"], &[data-popper-placement="bottom"]
|
|
||||||
{ justify-content: center; }
|
|
||||||
&[data-popper-placement="right"]
|
|
||||||
{ justify-content: left; }
|
|
||||||
&[data-popper-placement="left"]
|
|
||||||
{ justify-content: right; }
|
|
||||||
|
|
||||||
> .card.popover
|
|
||||||
{
|
|
||||||
margin: 0.4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[hidden]
|
|
||||||
{
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
}
|
|
103
yarn.lock
103
yarn.lock
|
@ -430,6 +430,58 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@floating-ui/core@npm:^1.0.0":
|
||||||
|
version: 1.6.2
|
||||||
|
resolution: "@floating-ui/core@npm:1.6.2"
|
||||||
|
dependencies:
|
||||||
|
"@floating-ui/utils": "npm:^0.2.0"
|
||||||
|
checksum: 10c0/db2621dc682e7f043d6f118d087ae6a6bfdacf40b26ede561760dd53548c16e2e7c59031e013e37283801fa307b55e6de65bf3b316b96a054e4a6a7cb937c59e
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@floating-ui/dom@npm:^1.0.0":
|
||||||
|
version: 1.6.5
|
||||||
|
resolution: "@floating-ui/dom@npm:1.6.5"
|
||||||
|
dependencies:
|
||||||
|
"@floating-ui/core": "npm:^1.0.0"
|
||||||
|
"@floating-ui/utils": "npm:^0.2.0"
|
||||||
|
checksum: 10c0/ebdc14806f786e60df8e7cc2c30bf9cd4d75fe734f06d755588bbdef2f60d0a0f21dffb14abdc58dea96e5577e2e366feca6d66ba962018efd1bc91a3ece4526
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@floating-ui/react-dom@npm:^2.1.0":
|
||||||
|
version: 2.1.0
|
||||||
|
resolution: "@floating-ui/react-dom@npm:2.1.0"
|
||||||
|
dependencies:
|
||||||
|
"@floating-ui/dom": "npm:^1.0.0"
|
||||||
|
peerDependencies:
|
||||||
|
react: ">=16.8.0"
|
||||||
|
react-dom: ">=16.8.0"
|
||||||
|
checksum: 10c0/9ee44dfeb27f585fb1e0114cbe37c72ff5d34149900f4f3013f6b0abf8c3365eab13286c360f97fbe0c44bb91a745e7a4c18b82d111990b45a7a7796dc55e461
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@floating-ui/react@npm:^0.26.17":
|
||||||
|
version: 0.26.17
|
||||||
|
resolution: "@floating-ui/react@npm:0.26.17"
|
||||||
|
dependencies:
|
||||||
|
"@floating-ui/react-dom": "npm:^2.1.0"
|
||||||
|
"@floating-ui/utils": "npm:^0.2.0"
|
||||||
|
tabbable: "npm:^6.0.0"
|
||||||
|
peerDependencies:
|
||||||
|
react: ">=16.8.0"
|
||||||
|
react-dom: ">=16.8.0"
|
||||||
|
checksum: 10c0/3d3a995aff5e905acd1fc4a3a7c92beba801b1d241f5ee1a05667c98dc1fcb9a0cc6ec82d4330fd84a0fd9b9fafa50f76dee886819659c49371733caa8d48b12
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@floating-ui/utils@npm:^0.2.0":
|
||||||
|
version: 0.2.2
|
||||||
|
resolution: "@floating-ui/utils@npm:0.2.2"
|
||||||
|
checksum: 10c0/b2becdcafdf395af1641348da0031ff1eaad2bc60c22e14bd3abad4acfe2c8401e03097173d89a2f646a99b75819a78ef21ebb2572cab0042a56dd654b0065cd
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@fontsource-variable/jetbrains-mono@npm:^5.0.21":
|
"@fontsource-variable/jetbrains-mono@npm:^5.0.21":
|
||||||
version: 5.0.21
|
version: 5.0.21
|
||||||
resolution: "@fontsource-variable/jetbrains-mono@npm:5.0.21"
|
resolution: "@fontsource-variable/jetbrains-mono@npm:5.0.21"
|
||||||
|
@ -599,13 +651,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@popperjs/core@npm:^2.11.8":
|
|
||||||
version: 2.11.8
|
|
||||||
resolution: "@popperjs/core@npm:2.11.8"
|
|
||||||
checksum: 10c0/4681e682abc006d25eb380d0cf3efc7557043f53b6aea7a5057d0d1e7df849a00e281cd8ea79c902a35a414d7919621fc2ba293ecec05f413598e0b23d5a1e63
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@rollup/pluginutils@npm:^5.1.0":
|
"@rollup/pluginutils@npm:^5.1.0":
|
||||||
version: 5.1.0
|
version: 5.1.0
|
||||||
resolution: "@rollup/pluginutils@npm:5.1.0"
|
resolution: "@rollup/pluginutils@npm:5.1.0"
|
||||||
|
@ -1769,18 +1814,18 @@ __metadata:
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "kernel-ui-core@workspace:."
|
resolution: "kernel-ui-core@workspace:."
|
||||||
dependencies:
|
dependencies:
|
||||||
|
"@floating-ui/react": "npm:^0.26.17"
|
||||||
"@fontsource-variable/jetbrains-mono": "npm:^5.0.21"
|
"@fontsource-variable/jetbrains-mono": "npm:^5.0.21"
|
||||||
"@fontsource-variable/manrope": "npm:^5.0.20"
|
"@fontsource-variable/manrope": "npm:^5.0.20"
|
||||||
"@fontsource-variable/source-serif-4": "npm:^5.0.19"
|
"@fontsource-variable/source-serif-4": "npm:^5.0.19"
|
||||||
"@phosphor-icons/react": "npm:^2.1.5"
|
"@phosphor-icons/react": "npm:^2.1.5"
|
||||||
"@popperjs/core": "npm:^2.11.8"
|
|
||||||
"@types/react": "npm:^18.3.3"
|
"@types/react": "npm:^18.3.3"
|
||||||
"@types/react-dom": "npm:^18.3.0"
|
"@types/react-dom": "npm:^18.3.0"
|
||||||
"@vitejs/plugin-react": "npm:^4.3.0"
|
"@vitejs/plugin-react": "npm:^4.3.0"
|
||||||
less: "npm:^4.2.0"
|
less: "npm:^4.2.0"
|
||||||
react: "npm:^18.3.1"
|
react: "npm:^18.3.1"
|
||||||
react-dom: "npm:^18.3.1"
|
react-dom: "npm:^18.3.1"
|
||||||
react-popper: "npm:^2.3.0"
|
react-merge-refs: "npm:^2.1.1"
|
||||||
typescript: "npm:^5.4.5"
|
typescript: "npm:^5.4.5"
|
||||||
vite: "npm:^5.2.11"
|
vite: "npm:^5.2.11"
|
||||||
vite-plugin-dts: "npm:^3.9.1"
|
vite-plugin-dts: "npm:^3.9.1"
|
||||||
|
@ -1850,7 +1895,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0":
|
"loose-envify@npm:^1.1.0":
|
||||||
version: 1.4.0
|
version: 1.4.0
|
||||||
resolution: "loose-envify@npm:1.4.0"
|
resolution: "loose-envify@npm:1.4.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -2254,24 +2299,10 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"react-fast-compare@npm:^3.0.1":
|
"react-merge-refs@npm:^2.1.1":
|
||||||
version: 3.2.2
|
version: 2.1.1
|
||||||
resolution: "react-fast-compare@npm:3.2.2"
|
resolution: "react-merge-refs@npm:2.1.1"
|
||||||
checksum: 10c0/0bbd2f3eb41ab2ff7380daaa55105db698d965c396df73e6874831dbafec8c4b5b08ba36ff09df01526caa3c61595247e3269558c284e37646241cba2b90a367
|
checksum: 10c0/b68cbc4ea51fd96f77247733388738630769d928f32958e63262ce43dff10c6b20f18a0483c6a640a1ffba48f32be26059d4ee9711881cacc63297b01311c8f6
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"react-popper@npm:^2.3.0":
|
|
||||||
version: 2.3.0
|
|
||||||
resolution: "react-popper@npm:2.3.0"
|
|
||||||
dependencies:
|
|
||||||
react-fast-compare: "npm:^3.0.1"
|
|
||||||
warning: "npm:^4.0.2"
|
|
||||||
peerDependencies:
|
|
||||||
"@popperjs/core": ^2.0.0
|
|
||||||
react: ^16.8.0 || ^17 || ^18
|
|
||||||
react-dom: ^16.8.0 || ^17 || ^18
|
|
||||||
checksum: 10c0/23f93540537ca4c035425bb8d5e51b11131fbc921d7ac1d041d0ae557feac8c877f3a012d36b94df8787803f52ed81e6df9257ac9e58719875f7805518d6db3f
|
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
@ -2635,6 +2666,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"tabbable@npm:^6.0.0":
|
||||||
|
version: 6.2.0
|
||||||
|
resolution: "tabbable@npm:6.2.0"
|
||||||
|
checksum: 10c0/ced8b38f05f2de62cd46836d77c2646c42b8c9713f5bd265daf0e78ff5ac73d3ba48a7ca45f348bafeef29b23da7187c72250742d37627883ef89cbd7fa76898
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"tar@npm:^6.1.11, tar@npm:^6.1.2":
|
"tar@npm:^6.1.11, tar@npm:^6.1.2":
|
||||||
version: 6.2.1
|
version: 6.2.1
|
||||||
resolution: "tar@npm:6.2.1"
|
resolution: "tar@npm:6.2.1"
|
||||||
|
@ -2844,15 +2882,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"warning@npm:^4.0.2":
|
|
||||||
version: 4.0.3
|
|
||||||
resolution: "warning@npm:4.0.3"
|
|
||||||
dependencies:
|
|
||||||
loose-envify: "npm:^1.0.0"
|
|
||||||
checksum: 10c0/aebab445129f3e104c271f1637fa38e55eb25f968593e3825bd2f7a12bd58dc3738bb70dc8ec85826621d80b4acfed5a29ebc9da17397c6125864d72301b937e
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"which@npm:^2.0.1":
|
"which@npm:^2.0.1":
|
||||||
version: 2.0.2
|
version: 2.0.2
|
||||||
resolution: "which@npm:2.0.2"
|
resolution: "which@npm:2.0.2"
|
||||||
|
|
Loading…
Reference in a new issue