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 {Checkbox} from "../src/Components/Forms/Checkbox";
|
||||
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 {PasswordInput} from "../src/Components/Forms/PasswordInput";
|
||||
import {RequiredField} from "../src/Components/Forms/RequiredField";
|
||||
import {Popover} from "../src/Components/Popovers/Popover";
|
||||
import {Tooltip} from "../src/Components/Popovers/Tooltip";
|
||||
import {Float} from "../src/Components/Floating/Float";
|
||||
import {Tooltip} from "../src/Components/Floating/Tooltip";
|
||||
import {DatepickerInput} from "../src/Components/Forms/DatepickerInput";
|
||||
import {TimepickerInput} from "../src/Components/Forms/TimepickerInput";
|
||||
|
||||
export function DemoApp()
|
||||
{
|
||||
const [datetime, setDatetime] = useState(null);
|
||||
|
||||
return (
|
||||
<main className={"app"}>
|
||||
<h1>KernelUI</h1>
|
||||
|
@ -73,6 +77,16 @@ export function DemoApp()
|
|||
Test password
|
||||
</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>
|
||||
<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>
|
||||
|
||||
<Popover mode={"hover"} content={"Do you see me?"}>
|
||||
<Float mode={"hover"} content={"Do you see me?"}>
|
||||
<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>
|
||||
</Popover>
|
||||
</Float>
|
||||
|
||||
<Popover mode={"click"} content={(
|
||||
<Float mode={"click"} content={(
|
||||
<div>
|
||||
You can add complex (clickable) content in me.
|
||||
<button type={"button"}>OK</button>
|
||||
</div>
|
||||
)}>
|
||||
<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>
|
||||
</Popover>
|
||||
</Float>
|
||||
|
||||
<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) => (
|
||||
<button type={"button"} onClick={show}>Customized behavior</button>
|
||||
)}
|
||||
</Popover>
|
||||
</Float>
|
||||
</div>
|
||||
|
||||
<h2>Tooltips</h2>
|
||||
|
|
|
@ -11,13 +11,13 @@
|
|||
"types": "lib/index.d.ts",
|
||||
"main": "lib/index.js",
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.26.17",
|
||||
"@fontsource-variable/jetbrains-mono": "^5.0.21",
|
||||
"@fontsource-variable/manrope": "^5.0.20",
|
||||
"@fontsource-variable/source-serif-4": "^5.0.19",
|
||||
"@phosphor-icons/react": "^2.1.5",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"react": "^18.3.1",
|
||||
"react-popper": "^2.3.0"
|
||||
"react-merge-refs": "^2.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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 {Popover} from "./Popover";
|
||||
import {Float} from "./Float";
|
||||
|
||||
export function Tooltip({children, content}: {
|
||||
children: React.ReactElement;
|
||||
|
@ -7,8 +7,8 @@ export function Tooltip({children, content}: {
|
|||
}): React.ReactElement
|
||||
{
|
||||
return (
|
||||
<Popover mode={"hover"} content={content} className={"tooltip"} popperOptions={{ placement: "top" }}>
|
||||
<Float mode={"hover"} content={content} className={"tooltip"} floatingOptions={{ placement: "top" }}>
|
||||
{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/_card";
|
||||
@import "components/_dates";
|
||||
@import "components/_form";
|
||||
@import "components/_headings";
|
||||
@import "components/_link";
|
||||
@import "components/_list";
|
||||
@import "components/_popover";
|
||||
@import "components/_floating";
|
||||
@import "components/_steps";
|
||||
@import "components/_table";
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&.popover
|
||||
&.floating
|
||||
{
|
||||
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/_datepicker-input";
|
||||
@import "forms/_input";
|
||||
@import "forms/_label";
|
||||
@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;
|
||||
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; }
|
||||
}
|
||||
|
||||
&.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
|
||||
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":
|
||||
version: 5.0.21
|
||||
resolution: "@fontsource-variable/jetbrains-mono@npm:5.0.21"
|
||||
|
@ -599,13 +651,6 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 5.1.0
|
||||
resolution: "@rollup/pluginutils@npm:5.1.0"
|
||||
|
@ -1769,18 +1814,18 @@ __metadata:
|
|||
version: 0.0.0-use.local
|
||||
resolution: "kernel-ui-core@workspace:."
|
||||
dependencies:
|
||||
"@floating-ui/react": "npm:^0.26.17"
|
||||
"@fontsource-variable/jetbrains-mono": "npm:^5.0.21"
|
||||
"@fontsource-variable/manrope": "npm:^5.0.20"
|
||||
"@fontsource-variable/source-serif-4": "npm:^5.0.19"
|
||||
"@phosphor-icons/react": "npm:^2.1.5"
|
||||
"@popperjs/core": "npm:^2.11.8"
|
||||
"@types/react": "npm:^18.3.3"
|
||||
"@types/react-dom": "npm:^18.3.0"
|
||||
"@vitejs/plugin-react": "npm:^4.3.0"
|
||||
less: "npm:^4.2.0"
|
||||
react: "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"
|
||||
vite: "npm:^5.2.11"
|
||||
vite-plugin-dts: "npm:^3.9.1"
|
||||
|
@ -1850,7 +1895,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0":
|
||||
"loose-envify@npm:^1.1.0":
|
||||
version: 1.4.0
|
||||
resolution: "loose-envify@npm:1.4.0"
|
||||
dependencies:
|
||||
|
@ -2254,24 +2299,10 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-fast-compare@npm:^3.0.1":
|
||||
version: 3.2.2
|
||||
resolution: "react-fast-compare@npm:3.2.2"
|
||||
checksum: 10c0/0bbd2f3eb41ab2ff7380daaa55105db698d965c396df73e6874831dbafec8c4b5b08ba36ff09df01526caa3c61595247e3269558c284e37646241cba2b90a367
|
||||
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
|
||||
"react-merge-refs@npm:^2.1.1":
|
||||
version: 2.1.1
|
||||
resolution: "react-merge-refs@npm:2.1.1"
|
||||
checksum: 10c0/b68cbc4ea51fd96f77247733388738630769d928f32958e63262ce43dff10c6b20f18a0483c6a640a1ffba48f32be26059d4ee9711881cacc63297b01311c8f6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -2635,6 +2666,13 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 6.2.1
|
||||
resolution: "tar@npm:6.2.1"
|
||||
|
@ -2844,15 +2882,6 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 2.0.2
|
||||
resolution: "which@npm:2.0.2"
|
||||
|
|
Loading…
Reference in a new issue