diff --git a/packages/core/src/components/Grid/GridCellHueHeader.tsx b/packages/core/src/components/Grid/GridCellHueHeader.tsx index 6efc6595..97e1baab 100644 --- a/packages/core/src/components/Grid/GridCellHueHeader.tsx +++ b/packages/core/src/components/Grid/GridCellHueHeader.tsx @@ -18,7 +18,7 @@ import { updateHueName, } from "@core/stores/colors"; import { $bgColorModeLeft } from "@core/stores/settings"; -import { HueAngle, HueName, type HueId } from "@core/types"; +import type { HueId } from "@core/types"; import type { AnyProps } from "@core/utils/react/types"; import { DATA_ATTR_CELL_HUE_ID } from "./constants"; @@ -101,7 +101,7 @@ const NameInput = memo(function NameInput({ hueId }: HueComponentProps) { /> ) } - onChange={(e) => updateHueName(hueId, HueName(e.target.value))} + onChange={(e) => updateHueName(hueId, e.target.value)} /> ); }); @@ -120,14 +120,13 @@ const AngleInput = memo(function AngleInput({ hueId }: HueComponentProps) { kind="ghost" label={LABEL_HUE} placeholder={PLACEHOLDER_HUE} + inputMode="numeric" min={HUE_MIN_ANGLE} max={HUE_MAX_ANGLE} value={angle} title={HINT_DEGREE} error={error} - onChange={(e) => - updateHueAngle(hueId, HueAngle(e.target.value ? Number.parseFloat(e.target.value) : 0)) - } + onChange={(e) => updateHueAngle(hueId, e.target.value)} /> ); }); diff --git a/packages/core/src/components/Grid/GridCellLevelAdd.module.css b/packages/core/src/components/Grid/GridCellLevelAdd.module.css index 404af571..580724a8 100644 --- a/packages/core/src/components/Grid/GridCellLevelAdd.module.css +++ b/packages/core/src/components/Grid/GridCellLevelAdd.module.css @@ -1,9 +1,15 @@ .cell { composes: gridColumnHeader from "./Grid.module.css"; align-items: flex-start; + justify-content: flex-start; } -.button { +.addLevel { inline-size: 100%; - padding: calc(var(--spacing) * 4.5) calc(var(--spacing) * 2); + margin-block-start: calc(var(--spacing) * 9); + padding: calc(var(--spacing) * 4.75) calc(var(--spacing) * 2); +} + +.removeCaps { + margin-block-start: calc(var(--spacing) * 6.75); } diff --git a/packages/core/src/components/Grid/GridCellLevelAdd.tsx b/packages/core/src/components/Grid/GridCellLevelAdd.tsx index df1bbd59..944465e3 100644 --- a/packages/core/src/components/Grid/GridCellLevelAdd.tsx +++ b/packages/core/src/components/Grid/GridCellLevelAdd.tsx @@ -4,26 +4,40 @@ import { useSubscribe } from "@spred/react"; import { Button } from "@core/components/Button/Button"; import { LPlus } from "@core/components/Icon/LPlus"; -import { insertLevel } from "@core/stores/colors"; +import { $isAnyChromaCapSet, insertLevel, resetAllChroma } from "@core/stores/colors"; import { $bgColorModeRight } from "@core/stores/settings"; +import { XsArrowBackCross } from "../Icon/XsArrowBackCross"; + import { GridCell } from "./GridCell"; import styles from "./GridCellLevelAdd.module.css"; export const GridCellLevelAdd = memo(function GridCellLevelAdd() { const bgMode = useSubscribe($bgColorModeRight); + const isAnyChromaCapSet = useSubscribe($isAnyChromaCapSet); const handleClick = useCallback(() => insertLevel(), []); return ( + )} ); }); diff --git a/packages/core/src/components/Grid/GridCellLevelHeader.tsx b/packages/core/src/components/Grid/GridCellLevelHeader.tsx index af7cbec2..d03b4ce8 100644 --- a/packages/core/src/components/Grid/GridCellLevelHeader.tsx +++ b/packages/core/src/components/Grid/GridCellLevelHeader.tsx @@ -1,4 +1,4 @@ -import { memo, useCallback, useRef } from "react"; +import { type CSSProperties, memo, useCallback, useRef } from "react"; import { useSignal, useSubscribe } from "@spred/react"; import clsx from "clsx"; @@ -11,12 +11,18 @@ import { withNumericIncrementControls } from "@core/components/Input/enhancers/w import { withValidation } from "@core/components/Input/enhancers/withValidation"; import { Input } from "@core/components/Input/Input"; import { useAppEvent } from "@core/hooks/useFocusRefOnEvent"; -import { CONTRAST_MIN, getContrastMaxLevel, getContrastStep } from "@core/schemas/color"; +import { + CHROMA_MAX, + CHROMA_MIN, + CONTRAST_MIN, + getContrastMaxLevel, + getContrastStep, +} from "@core/schemas/color"; import { $levelIds, getLevel, insertLevel, - updateLevelChroma, + updateLevelchromaCap, updateLevelContrast, updateLevelName, } from "@core/stores/colors"; @@ -29,7 +35,7 @@ import { contrastModelStore, directionModeStore, } from "@core/stores/settings"; -import { LevelChroma, LevelContrast, LevelName, type LevelId } from "@core/types"; +import type { LevelId } from "@core/types"; import { formatOklch } from "@core/utils/colors/formatOklch"; import type { AnyProps } from "@core/utils/react/types"; @@ -41,10 +47,11 @@ import styles from "./GridCellLevelHeader.module.css"; type LevelComponentProps

= { levelId: LevelId } & P; const LABEL_NAME = "Level name"; +const LABEL_CHROMA_CAP_SET = "Set cap"; +const LABEL_CHROMA_CAP_DEFINED = "Cap"; const LABEL_CHROMA = "Level chroma"; const PLACEHOLDER_LEVEL = "Level"; const PLACEHOLDER_CONTRAST = "CR"; -const PLACEHOLDER_CHROMA = "Chroma"; const HINT_LEVEL = "Color level name"; const HINT_FG_TO_BG_CONTRAST = "Contrast of text color to the background"; @@ -101,7 +108,7 @@ const NameInput = memo(function NameInput({ levelId }: LevelComponentProps) { value={name} title={HINT_LEVEL} error={error} - onChange={(e) => updateLevelName(levelId, LevelName(e.target.value))} + onChange={(e) => updateLevelName(levelId, e.target.value)} /> ); }); @@ -125,7 +132,6 @@ const ContrastInput = memo(function ContrastInput({ return ( { - let newValue = Number.parseFloat(e.target.value); - if (Number.isNaN(newValue)) { - newValue = 0; - } - updateLevelContrast(levelId, LevelContrast(newValue)); - }} + onChange={(e) => updateLevelContrast(levelId, e.target.value)} /> ); }); -const LevelChromaInput = withValidation(withNumericIncrementControls(withAutosize(Input))); +const CHROMA_INPUT_PRECISION = 3; +const LevelChromaInput = withValidation(withNumericIncrementControls(Input)); const ChromaInput = memo(function ChromaInput({ levelId }: LevelComponentProps) { const level = getLevel(levelId); - const $chroma = useSignal((get) => { + const chroma = useSubscribe(level.chroma.$raw); + const chromaCap = useSubscribe(level.chromaCap.$raw); + const error = useSubscribe(level.chromaCap.$validationError); + const $chromaPlaceholder = useSignal((get) => { return get(chromaModeStore.$lastValidValue) === "even" - ? get(level.$tintColor).referencedC.toFixed(2) + ? get(level.chroma.$lastValidValue).toFixed(CHROMA_INPUT_PRECISION) : "max"; }); - const chroma = useSubscribe($chroma); - const error = useSubscribe(level.chroma.$validationError); + const $chromaLabel = useSignal((get) => { + const chromaCap = get(level.chromaCap.$raw); + + return chromaCap ? LABEL_CHROMA_CAP_DEFINED : LABEL_CHROMA_CAP_SET; + }); + const chromaLabel = useSubscribe($chromaLabel); + const chromaPlaceholder = useSubscribe($chromaPlaceholder); return ( { - let newValue = Number.parseFloat(e.target.value); - if (Number.isNaN(newValue)) { - newValue = 0; - } - updateLevelChroma(levelId, LevelChroma(newValue)); - }} + onChange={(e) => updateLevelchromaCap(levelId, e.target.value)} /> ); }); diff --git a/packages/core/src/components/Grid/GridLeftTopCell.module.css b/packages/core/src/components/Grid/GridLeftTopCell.module.css index 284b9a87..9dbc49ff 100644 --- a/packages/core/src/components/Grid/GridLeftTopCell.module.css +++ b/packages/core/src/components/Grid/GridLeftTopCell.module.css @@ -1,6 +1,6 @@ .cell { composes: gridTopLeftCell from "./Grid.module.css"; - gap: calc(var(--spacing) * 4.5); + gap: calc(var(--spacing) * 3.75); align-items: flex-start; justify-content: flex-start; } @@ -18,7 +18,6 @@ .bottom { translate: 0 -1px; - margin-top: auto; } .contrastModelButton { diff --git a/packages/core/src/components/Grid/GridLeftTopCell.tsx b/packages/core/src/components/Grid/GridLeftTopCell.tsx index d12fac2f..fb20f5f2 100644 --- a/packages/core/src/components/Grid/GridLeftTopCell.tsx +++ b/packages/core/src/components/Grid/GridLeftTopCell.tsx @@ -66,13 +66,6 @@ export const GridLeftTopCell = memo(function GridLeftTopCell() {

- updateChromaMode(parseChromaMode(e.target.value))} + title={LABEL_CHROMA_MODE} + />
); diff --git a/packages/core/src/components/Icon/XsArrowBackCross.tsx b/packages/core/src/components/Icon/XsArrowBackCross.tsx new file mode 100644 index 00000000..b6975f10 --- /dev/null +++ b/packages/core/src/components/Icon/XsArrowBackCross.tsx @@ -0,0 +1,14 @@ +import type { SVGProps } from "react"; + +export type XsArrowBackProps = SVGProps; + +export function XsArrowBackCross(props: XsArrowBackProps) { + return ( + + + + ); +} diff --git a/packages/core/src/components/Input/Input.module.css b/packages/core/src/components/Input/Input.module.css index 0ca6ec43..69463b55 100644 --- a/packages/core/src/components/Input/Input.module.css +++ b/packages/core/src/components/Input/Input.module.css @@ -1,7 +1,7 @@ .container { display: inline-flex; flex-direction: column; - gap: calc(var(--spacing) / 2); + gap: var(--spacing); inline-size: 100%; max-inline-size: 100%; @@ -60,20 +60,28 @@ .label { cursor: text; + font-family: var(--font-sans); font-size: var(--text-ui-s); - font-weight: 500; + font-weight: var(--input-label-fw, 500); line-height: var(--text-ui-s--line-height); + color: var(--input-label-color, var(--input-color)); text-align: center; + + .kind_ghost & { + visibility: hidden; + } + + .kind_ghost:focus-within &[data-visible], + .kind_ghost:hover &[data-visible], + .kind_ghost &[data-visible="always"] { + visibility: visible; + } } .inputContainer { display: flex; gap: var(--spacing); align-items: center; - - :where(.size_xl) & { - inline-size: 100%; - } } .slot { @@ -84,13 +92,19 @@ .input { inline-size: 100%; max-inline-size: var(--input-max-inline-size, 100%); + font-size: max(var(--min-font-size), var(--text-m)); + font-weight: 500; line-height: max(var(--min-font-size), 1.25rem); :where(.size_xl) & { inline-size: 100%; font-size: var(--text-ui-l); line-height: var(--text-ui-l--line-height); + } + + &.fullWidth { + inline-size: 100%; text-align: center; } diff --git a/packages/core/src/components/Input/Input.tsx b/packages/core/src/components/Input/Input.tsx index e4fd3939..b6af313e 100644 --- a/packages/core/src/components/Input/Input.tsx +++ b/packages/core/src/components/Input/Input.tsx @@ -11,6 +11,7 @@ export type InputSize = "m" | "xl"; export type InputProps = Omit, "size"> & { size: InputSize; label?: string; + showLabel?: "always" | "hover"; labelRef?: RefObject; kind?: InputKind; fitContent?: boolean; @@ -29,6 +30,7 @@ export function Input({ kind = "bordered", size = "m", label, + showLabel, labelRef, title, value, @@ -49,17 +51,21 @@ export function Input({ htmlFor={id} > {label && ( - + {label} )}
{slotStart &&
{slotStart}
} {slotEnd &&
{slotEnd}
} diff --git a/packages/core/src/components/Input/enhancers/enhancers.module.css b/packages/core/src/components/Input/enhancers/enhancers.module.css index dce53af4..76df708c 100644 --- a/packages/core/src/components/Input/enhancers/enhancers.module.css +++ b/packages/core/src/components/Input/enhancers/enhancers.module.css @@ -1,6 +1,11 @@ -.autosize { +.autosize.autosize { inline-size: fit-content; max-inline-size: 100%; font-family: var(--font-mono); font-variant-numeric: tabular-nums; } + +.numericIncrement.numericIncrement { + font-family: var(--font-mono); + font-variant-numeric: tabular-nums slashed-zero; +} diff --git a/packages/core/src/components/Input/enhancers/withNumericIncrementControls.tsx b/packages/core/src/components/Input/enhancers/withNumericIncrementControls.tsx index d25c60fb..6df52f7d 100644 --- a/packages/core/src/components/Input/enhancers/withNumericIncrementControls.tsx +++ b/packages/core/src/components/Input/enhancers/withNumericIncrementControls.tsx @@ -1,24 +1,58 @@ import { type ChangeEvent, type ComponentType, + type FocusEvent, useCallback, useEffect, useMemo, useRef, } from "react"; +import clsx from "clsx"; + import { isNumber } from "@core/utils/number/isNumber"; import { mergeRefs } from "@core/utils/react/mergeRefs"; import type { InputProps } from "../Input"; +import styles from "./enhancers.module.css"; + type WithNumericIncrementControlsProps = { + /** + * The base value for arrows & wheel value manipulations. Will be used only when value isn't defined. + */ + baseValue?: number | string; + /** + * How many decimal places to display. + */ + precision?: number; step?: number; }; -function formatWithFractional(value: string, fractionalLength: number): string { +function formatWithPrecision(value: string | number, precision: number): string { const number = Number(value); - return Number.isNaN(number) || fractionalLength === 0 ? value : number.toFixed(fractionalLength); + return (Number.isNaN(number) ? 0 : number).toFixed(precision); +} + +const NUMERIC_REGEX = /^\d*$/; +const DECIMAL_REGEX = /^\d*(?:[.,])?\d*$/; + +function replaceDecimalDelimiter(value: string): string { + return value.replace(",", "."); +} + +function isInputValid(inputMode: string | undefined, value: string) { + switch (inputMode) { + case "numeric": { + return NUMERIC_REGEX.test(value); + } + case "decimal": { + return DECIMAL_REGEX.test(value); + } + default: { + return true; + } + } } export function withNumericIncrementControls

( @@ -27,13 +61,13 @@ export function withNumericIncrementControls

( const NumberKeyboardInput = ({ step = 1, value, + baseValue, + precision = -Math.log10(step), ...props - }: P & WithNumericIncrementControlsProps) => { - const { onChange } = props; + }: WithNumericIncrementControlsProps & P) => { + const { onChange, onBlur } = props; const inputRef = useRef(null); const labelRef = useRef(null); - const fractionalLength = -Math.log10(step); - const formattedValue = formatWithFractional(String(value), fractionalLength); const refCallback = useMemo(() => mergeRefs(inputRef, props.ref), []); const labelRefCallback = useMemo(() => mergeRefs(labelRef, props.labelRef), []); @@ -53,7 +87,9 @@ export function withNumericIncrementControls

( return value; } - const currentValue = Number.parseFloat(input.value); + const currentValue = Number.parseFloat( + input.value || (baseValue ? String(baseValue) : ""), + ); if (Number.isNaN(currentValue)) { return; @@ -66,7 +102,7 @@ export function withNumericIncrementControls

( return; } - input.value = Number(newValue.toFixed(fractionalLength)).toString(); + input.value = formatWithPrecision(newValue, precision); if (onChange) { const nativeEvent = new Event("change", { bubbles: true }); @@ -79,7 +115,7 @@ export function withNumericIncrementControls

( } as unknown as ChangeEvent); } }, - [onChange, fractionalLength], + [onChange, precision], ); useEffect(() => { @@ -132,17 +168,50 @@ export function withNumericIncrementControls

( return () => label.removeEventListener("wheel", handleWheel); }, [step, updateValue]); + const handleOnChange = useCallback( + (e: ChangeEvent) => { + const input = inputRef.current; + + if (!input) return; + + if (isInputValid(input.inputMode, input.value)) { + e.target.value = replaceDecimalDelimiter(e.target.value); + onChange?.(e); + } + }, + [onChange], + ); + + const handleBlur = useCallback( + (e: FocusEvent) => { + const input = inputRef.current; + + if (!input?.value) return; + + const formattedValue = formatWithPrecision(input.value, precision); + if (input.value !== formattedValue) { + input.value = formattedValue; + } + + onBlur?.(e); + }, + [onBlur], + ); + return ( ); }; diff --git a/packages/core/src/components/Input/enhancers/withValidation.tsx b/packages/core/src/components/Input/enhancers/withValidation.tsx index 38bd0f76..1e96dcbe 100644 --- a/packages/core/src/components/Input/enhancers/withValidation.tsx +++ b/packages/core/src/components/Input/enhancers/withValidation.tsx @@ -30,5 +30,5 @@ export function withValidation

(WrappedComponent: Component ); }; - return ValidationInput as React.ComponentType

; + return ValidationInput as ComponentType

; } diff --git a/packages/core/src/components/Select/Select.module.css b/packages/core/src/components/Select/Select.module.css index fd1a2573..bae6c6ef 100644 --- a/packages/core/src/components/Select/Select.module.css +++ b/packages/core/src/components/Select/Select.module.css @@ -36,6 +36,10 @@ background-color: var(--color-universal-elevation); } +.control { + display: inline-flex; +} + .content { display: flex; align-items: center; diff --git a/packages/core/src/components/Select/Select.tsx b/packages/core/src/components/Select/Select.tsx index 8027e636..840e019f 100644 --- a/packages/core/src/components/Select/Select.tsx +++ b/packages/core/src/components/Select/Select.tsx @@ -55,7 +55,7 @@ export function Select({ ))} -

+