diff --git a/demo/DemoApp.tsx b/demo/DemoApp.tsx index f129f6c..1168332 100644 --- a/demo/DemoApp.tsx +++ b/demo/DemoApp.tsx @@ -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 (

KernelUI

@@ -73,6 +77,16 @@ export function DemoApp() Test password + + Date test + + + + Time test + + +

Currently selected datetime: {datetime ? datetime.toISOString() : "none"}

+ Checkbox demo Radio box test Radio box test @@ -183,33 +197,33 @@ export function DemoApp()

Popovers

- + - + - I am focused}> + I am focused}> - + - You can add complex (clickable) content in me. )}> - + - + - +
- ()}> + ()}> {(show, hide) => ( )} - +

Tooltips

diff --git a/package.json b/package.json index 005ec65..5d82e6e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/Components/Dates/Calendar.tsx b/src/Components/Dates/Calendar.tsx new file mode 100644 index 0000000..d94ac58 --- /dev/null +++ b/src/Components/Dates/Calendar.tsx @@ -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): React.ReactElement +{ + locale = useMemo(() => (locale ?? "fr"), [locale]); + + const currentMonthHeader = useMemo(() => ( + + { // 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 ( + + {(new Intl.DateTimeFormat(locale, {weekday: "short"})).format(dayOfWeek)} + + ); + }) + } + + ), [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( + + ); + + // 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( + + {currentWeek} + + ); + 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 ( + + + {currentMonthHeader} + + + {currentMonthTable} + +
+ ); +} + +/** + * Calendar day component. + */ +function Day({date, onClick, faded, selected}: { + date: Date; + onClick?: (date: Date) => void; + faded: boolean; + selected: boolean; +}): React.ReactElement +{ + return ( + + onClick?.(date)}> + {date.getDate()} + + + ); +} diff --git a/src/Components/Dates/Datepicker.tsx b/src/Components/Dates/Datepicker.tsx new file mode 100644 index 0000000..62add1e --- /dev/null +++ b/src/Components/Dates/Datepicker.tsx @@ -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): 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 ( +
+
+ {(new Intl.DateTimeFormat(locale, {month: "long", year: "numeric"})).format(date)} +
+ + + + + + + + + +
+ ); +} diff --git a/src/Components/Floating/Float.tsx b/src/Components/Floating/Float.tsx new file mode 100644 index 0000000..bb975c8 --- /dev/null +++ b/src/Components/Floating/Float.tsx @@ -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 = (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)|Managed<(React.ReactElement & React.ClassAttributes)>; + content?: React.ReactNode|Managed; + 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 && +
+ + {floatingContent} + +
+ } + + ); +} diff --git a/src/Components/Popovers/Tooltip.tsx b/src/Components/Floating/Tooltip.tsx similarity index 55% rename from src/Components/Popovers/Tooltip.tsx rename to src/Components/Floating/Tooltip.tsx index 07a8b31..9a95fc2 100644 --- a/src/Components/Popovers/Tooltip.tsx +++ b/src/Components/Floating/Tooltip.tsx @@ -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 ( - + {children} - + ); } diff --git a/src/Components/Forms/DatepickerInput.tsx b/src/Components/Forms/DatepickerInput.tsx new file mode 100644 index 0000000..d4bbbee --- /dev/null +++ b/src/Components/Forms/DatepickerInput.tsx @@ -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, { + /** + * 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(); + + /** + * 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 ( + + ); +} diff --git a/src/Components/Forms/TimepickerInput.tsx b/src/Components/Forms/TimepickerInput.tsx new file mode 100644 index 0000000..91fa33f --- /dev/null +++ b/src/Components/Forms/TimepickerInput.tsx @@ -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, { + 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 ( + + ); +} diff --git a/src/Components/Popovers/Popover.tsx b/src/Components/Popovers/Popover.tsx deleted file mode 100644 index 52a88de..0000000 --- a/src/Components/Popovers/Popover.tsx +++ /dev/null @@ -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 = (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; - content?: React.ReactNode|Managed; - className?: string; - mode?: PopoverMode; - popperOptions?: Partial; -}): 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} - - - - ); -} diff --git a/src/Utils.tsx b/src/Utils.tsx new file mode 100644 index 0000000..ecb3adc --- /dev/null +++ b/src/Utils.tsx @@ -0,0 +1,12 @@ + +export type Modify = Omit & 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(); +} diff --git a/src/styles/_components.less b/src/styles/_components.less index 6bef8a6..c0ae390 100644 --- a/src/styles/_components.less +++ b/src/styles/_components.less @@ -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"; diff --git a/src/styles/components/_card.less b/src/styles/components/_card.less index 6b26038..d349abb 100644 --- a/src/styles/components/_card.less +++ b/src/styles/components/_card.less @@ -30,7 +30,7 @@ margin-bottom: 0; } - &.popover + &.floating { width: 20em; } diff --git a/src/styles/components/_dates.less b/src/styles/components/_dates.less new file mode 100644 index 0000000..e60406d --- /dev/null +++ b/src/styles/components/_dates.less @@ -0,0 +1,3 @@ + +@import "dates/_calendar"; +@import "dates/_datepicker"; diff --git a/src/styles/components/_floating.less b/src/styles/components/_floating.less new file mode 100644 index 0000000..fd2d7cd --- /dev/null +++ b/src/styles/components/_floating.less @@ -0,0 +1,2 @@ +@import "floating/_floating"; +@import "floating/_tooltip"; diff --git a/src/styles/components/_form.less b/src/styles/components/_form.less index 8c9a422..6ac2505 100644 --- a/src/styles/components/_form.less +++ b/src/styles/components/_form.less @@ -1,5 +1,6 @@ @import "forms/_box"; +@import "forms/_datepicker-input"; @import "forms/_input"; @import "forms/_label"; @import "forms/_password-input"; diff --git a/src/styles/components/_popover.less b/src/styles/components/_popover.less deleted file mode 100644 index 12dc107..0000000 --- a/src/styles/components/_popover.less +++ /dev/null @@ -1,2 +0,0 @@ -@import "popovers/_popover"; -@import "popovers/_tooltip"; diff --git a/src/styles/components/dates/_calendar.less b/src/styles/components/dates/_calendar.less new file mode 100644 index 0000000..b605be7 --- /dev/null +++ b/src/styles/components/dates/_calendar.less @@ -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; + } + } +} diff --git a/src/styles/components/dates/_datepicker.less b/src/styles/components/dates/_datepicker.less new file mode 100644 index 0000000..d2f440a --- /dev/null +++ b/src/styles/components/dates/_datepicker.less @@ -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; + } + } +} diff --git a/src/styles/components/floating/_floating.less b/src/styles/components/floating/_floating.less new file mode 100644 index 0000000..c6d003d --- /dev/null +++ b/src/styles/components/floating/_floating.less @@ -0,0 +1,10 @@ +:not(.card).floating +{ + display: block; + z-index: 1; + + > .card.floating + { + margin: 0.4em; + } +} diff --git a/src/styles/components/popovers/_tooltip.less b/src/styles/components/floating/_tooltip.less similarity index 89% rename from src/styles/components/popovers/_tooltip.less rename to src/styles/components/floating/_tooltip.less index 68382a3..d8897ca 100644 --- a/src/styles/components/popovers/_tooltip.less +++ b/src/styles/components/floating/_tooltip.less @@ -1,4 +1,4 @@ -.card.popover.tooltip +.card.floating.tooltip { width: unset; max-width: 20em; diff --git a/src/styles/components/forms/_datepicker-input.less b/src/styles/components/forms/_datepicker-input.less new file mode 100644 index 0000000..02a5315 --- /dev/null +++ b/src/styles/components/forms/_datepicker-input.less @@ -0,0 +1,9 @@ +label.datepicker-input +{ + .card.datepicker + { + padding: 0.2em; + width: fit-content; + max-width: unset; + } +} diff --git a/src/styles/components/forms/_label.less b/src/styles/components/forms/_label.less index e80e0c8..3552463 100644 --- a/src/styles/components/forms/_label.less +++ b/src/styles/components/forms/_label.less @@ -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); + } + } } diff --git a/src/styles/components/forms/_timepicker-input.less b/src/styles/components/forms/_timepicker-input.less new file mode 100644 index 0000000..e69de29 diff --git a/src/styles/components/popovers/_popover.less b/src/styles/components/popovers/_popover.less deleted file mode 100644 index ff7354b..0000000 --- a/src/styles/components/popovers/_popover.less +++ /dev/null @@ -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; - } -} diff --git a/yarn.lock b/yarn.lock index f939a66..edf1fe6 100644 --- a/yarn.lock +++ b/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"