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 (
}
onClick={handleClick}
aria-label="Insert new level row at the end"
/>
+ {isAnyChromaCapSet && (
+ }
+ onClick={resetAllChroma}
+ >
+ All caps
+
+ )}
);
});
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() {
-
);
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({
))}
-
+