Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions packages/core/src/components/BetaMenu/BetaMenu.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.button {
/* No positioning needed - will be positioned by parent FloatingActions */
}

.iconContainer {
display: flex;
gap: calc(var(--spacing) * 0.5);
align-items: center;
}

.flaskIcon {
width: 16px;
height: 16px;
}

.caretIcon {
width: 12px;
height: 12px;
opacity: 0.7;
}
63 changes: 63 additions & 0 deletions packages/core/src/components/BetaMenu/BetaMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useSubscribe } from "@spred/react";

import { Button } from "@core/components/Button/Button";
import { MFlask } from "@core/components/Icon/MFlask";
import { MTriangleDown } from "@core/components/Icon/MTriangleDown";
import { Menu } from "@core/components/Menu/Menu";
import { MenuItemButton } from "@core/components/Menu/MenuItemButton";
import { MenuItemGroup } from "@core/components/Menu/MenuItemGroup";
import { MenuItemSeparator } from "@core/components/Menu/MenuItemSeparator";
import { levelStepsPresets } from "@core/levelStepsPresets";
import { LEVEL_STEPS_PRESETS, type LevelStepsPreset } from "@core/schemas/brand";
import { resetToInitialState } from "@core/stores/config";
import {
applyLevelStepsPreset,
distributeContrastEvenly,
levelStepsPresetStore,
} from "@core/stores/settings";

import styles from "./BetaMenu.module.css";

export function BetaMenu() {
const currentPreset = useSubscribe(levelStepsPresetStore.$lastValidValue);

return (
<Menu
renderTrigger={(triggerProps) => (
<Button
size="m"
kind="floating"
{...triggerProps}
aria-label="Beta features"
className={styles.button}
>
<span className={styles.iconContainer}>
<MFlask className={styles.flaskIcon} />
<MTriangleDown className={styles.caretIcon} />
</span>
</Button>
)}
>
<MenuItemGroup id="level-steps" label="Palette Range Steps">
{LEVEL_STEPS_PRESETS.map((preset) => (
<MenuItemButton
key={preset}
value={preset}
data-current={currentPreset === preset ? "" : undefined}
onClick={() => applyLevelStepsPreset(preset as LevelStepsPreset)}
>
{levelStepsPresets[preset].label}
</MenuItemButton>
))}
</MenuItemGroup>
<MenuItemSeparator />
<MenuItemButton value="distribute-contrast" onClick={() => distributeContrastEvenly()}>
Distribute Contrast Evenly
</MenuItemButton>
<MenuItemSeparator />
<MenuItemButton value="reset-initial" onClick={resetToInitialState}>
Reset to Initial State
</MenuItemButton>
</Menu>
);
}
18 changes: 17 additions & 1 deletion packages/core/src/components/Grid/GridCellHueHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { memo, useCallback, useRef } from "react";
import { type ClipboardEvent, memo, useCallback, useRef } from "react";

import { useSubscribe } from "@spred/react";

Expand All @@ -19,6 +19,7 @@ import {
} from "@core/stores/colors";
import { $bgColorModeLeft } from "@core/stores/settings";
import type { HueId } from "@core/types";
import { hexToHueAngle } from "@core/utils/colors/hexToHueAngle";
import type { AnyProps } from "@core/utils/react/types";

import { DATA_ATTR_CELL_HUE_ID } from "./constants";
Expand Down Expand Up @@ -70,6 +71,20 @@ const NameInput = memo(function NameInput({ hueId }: HueComponentProps) {
}
});

const handlePaste = useCallback(
(e: ClipboardEvent<HTMLInputElement>) => {
const pastedText = e.clipboardData.getData("text");
const hueAngle = hexToHueAngle(pastedText);

if (hueAngle !== null) {
e.preventDefault();
updateHueAngle(hueId, String(Math.round(hueAngle)));
resetHueName(hueId);
}
},
[hueId],
);

return (
<HueNameInput
ref={inputRef}
Expand All @@ -88,6 +103,7 @@ const NameInput = memo(function NameInput({ hueId }: HueComponentProps) {
resetHueName(hueId);
}
}}
onPaste={handlePaste}
slotEnd={
showResetNameButton && (
<Button
Expand Down
60 changes: 60 additions & 0 deletions packages/core/src/components/Grid/GridCellLevelHeader.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,63 @@
opacity: 1;
}
}

.contrastSection {
position: relative;
}

.lockButton {
position: absolute;
z-index: 1;
inset-block-end: 0;
inset-inline-end: 50%;
translate: 50% 50%;

opacity: 0;

transition: opacity 0.2s;

.cell:hover &,
.cell:has(:focus-visible) & {
opacity: 0.3;
}

.cell:hover &:hover,
.cell:has(:focus-visible) &:hover {
opacity: 0.8;
}

&.locked {
color: var(--color-primary);
opacity: 1;
}

&.locked:hover {
opacity: 1;
}
}

.chromaSection {
margin-block-start: calc(var(--spacing) * 1.5);

&:has(.copyChromaButton:hover) .inputSecondary {
--input-color: var(--color-primary);
}
}

.copyChromaButton {
inline-size: 100%;
margin-block-start: calc(var(--spacing) * 0.5);
opacity: 0;
transition: opacity 0.2s;

.cell:hover &,
.cell:has(:focus-visible) & {
opacity: 0.6;
}

.cell:hover &:hover,
.cell:has(:focus-visible) &:hover {
opacity: 1;
}
}
145 changes: 91 additions & 54 deletions packages/core/src/components/Grid/GridCellLevelHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useSignal, useSubscribe } from "@spred/react";
import clsx from "clsx";

import { Button } from "@core/components/Button/Button";
import { MLock } from "@core/components/Icon/MLock";
import { MPlus } from "@core/components/Icon/MPlus";
import { withAutosize } from "@core/components/Input/enhancers/withAutosize";
import { withNumericIncrementControls } from "@core/components/Input/enhancers/withNumericIncrementControls";
Expand All @@ -19,8 +20,12 @@ import {
} from "@core/schemas/color";
import {
$levelIds,
copyChromaCapToAllLevels,
getLevel,
insertLevel,
startChromaCopyPreview,
stopChromaCopyPreview,
toggleLevelLocked,
updateLevelchromaCap,
updateLevelContrast,
updateLevelName,
Expand Down Expand Up @@ -117,39 +122,56 @@ const ContrastInput = memo(function ContrastInput({ levelId }: LevelComponentPro
const contrast = useSubscribe(level.contrast.$raw);
const error = useSubscribe(level.contrast.$validationError);
const contrastModel = useSubscribe(contrastModelStore.$lastValidValue);
const locked = useSubscribe(level.$locked);

const tintColor = useSubscribe(level.$tintColor);
const directionMode = useSubscribe(directionModeStore.$lastValidValue);

return (
<LevelContrastInput
id={`level-contrast-${levelId}`}
size="xl"
kind="bordered"
customization={
directionMode === "fgToBg"
? {
"--input-color": tintColor.css,
"--input-border-color": formatOklch(tintColor, 0.2),
}
: {
"--input-color": bgColorVariable,
"--input-border-color": tintColor.css,
"--input-bg-color": tintColor.css,
}
}
inputMode={contrastModel === "apca" ? "numeric" : "decimal"}
min={CONTRAST_MIN}
max={getContrastMaxLevel(contrastModel)}
step={getContrastStep(contrastModel)}
label={contrastModel.toUpperCase()}
showLabel="always"
placeholder={PLACEHOLDER_CONTRAST}
value={contrast}
title={directionMode === "fgToBg" ? HINT_FG_TO_BG_CONTRAST : HINT_BG_TO_FG_CONTRAST}
error={error}
onChange={(e) => updateLevelContrast(levelId, e.target.value)}
/>
<div className={styles.contrastSection}>
<LevelContrastInput
id={`level-contrast-${levelId}`}
size="xl"
kind="bordered"
customization={
directionMode === "fgToBg"
? {
"--input-color": tintColor.css,
"--input-border-color": formatOklch(tintColor, 0.2),
}
: {
"--input-color": bgColorVariable,
"--input-border-color": tintColor.css,
"--input-bg-color": tintColor.css,
}
}
inputMode={contrastModel === "apca" ? "numeric" : "decimal"}
min={CONTRAST_MIN}
max={getContrastMaxLevel(contrastModel)}
step={getContrastStep(contrastModel)}
label={contrastModel.toUpperCase()}
showLabel="always"
placeholder={PLACEHOLDER_CONTRAST}
value={contrast}
title={directionMode === "fgToBg" ? HINT_FG_TO_BG_CONTRAST : HINT_BG_TO_FG_CONTRAST}
error={error}
onChange={(e) => updateLevelContrast(levelId, e.target.value)}
/>
<Button
size="m"
kind="ghost"
rounded
className={clsx(styles.lockButton, locked && styles.locked)}
icon={<MLock />}
onClick={() => toggleLevelLocked(levelId)}
title={
locked
? "Unlock contrast (allow automatic changes)"
: "Lock contrast (prevent automatic changes)"
}
aria-label={locked ? "Unlock contrast" : "Lock contrast"}
/>
</div>
);
});

Expand All @@ -174,32 +196,47 @@ const ChromaInput = memo(function ChromaInput({ levelId }: LevelComponentProps)
const chromaPlaceholder = useSubscribe($chromaPlaceholder);

return (
<LevelChromaInput
id={`level-chroma-${levelId}`}
className={styles.inputSecondary}
size="m"
kind="ghost"
inputMode="decimal"
style={
{
"--input-label-color": "var(--color-secondary)",
"--input-label-fw": 400,
} as CSSProperties
}
min={CHROMA_MIN}
max={CHROMA_MAX}
precision={CHROMA_INPUT_PRECISION}
baseValue={chroma}
step={0.001}
label={chromaLabel}
aria-label={LABEL_CHROMA}
showLabel={chromaCap ? "always" : "hover"}
placeholder={chromaPlaceholder}
value={chromaCap ?? ""}
title={HINT_CHROMA}
error={error}
onChange={(e) => updateLevelchromaCap(levelId, e.target.value || null)}
/>
<div className={styles.chromaSection}>
<LevelChromaInput
id={`level-chroma-${levelId}`}
className={styles.inputSecondary}
size="m"
kind="ghost"
inputMode="decimal"
style={
{
"--input-label-color": "var(--color-secondary)",
"--input-label-fw": 400,
} as CSSProperties
}
min={CHROMA_MIN}
max={CHROMA_MAX}
precision={CHROMA_INPUT_PRECISION}
baseValue={chroma}
step={0.001}
label={chromaLabel}
aria-label={LABEL_CHROMA}
showLabel={chromaCap ? "always" : "hover"}
placeholder={chromaPlaceholder}
value={chromaCap ?? ""}
title={HINT_CHROMA}
error={error}
onChange={(e) => updateLevelchromaCap(levelId, e.target.value || null)}
/>
<Button
size="xs"
kind="ghost"
className={styles.copyChromaButton}
onClick={() => copyChromaCapToAllLevels(levelId)}
onMouseEnter={() => startChromaCopyPreview(levelId)}
onMouseLeave={stopChromaCopyPreview}
title={
chromaCap ? "Copy this cap to all other levels" : "Copy this chroma to all other levels"
}
>
Apply
</Button>
</div>
);
});

Expand Down
Loading