Skip to content

Commit

Permalink
feat: use a custom color picker widget
Browse files Browse the repository at this point in the history
  • Loading branch information
aradzie committed Oct 15, 2024
1 parent 6777530 commit 6bc61b1
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 13 deletions.
13 changes: 12 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 29 additions & 0 deletions packages/keybr-theme-designer/lib/design/ColorImport.module.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
@import "@keybr/widget/lib/index.less";

.root {
display: inline-block;
vertical-align: middle;
inline-size: 4rem;
min-block-size: @field-height;
margin: 0;
padding: 0;
border: var(--separator-border);
cursor: pointer;
}

.popup {
color: var(--Popup__color);
background-color: var(--Popup__background-color);
box-shadow: var(--Popup--small__box-shadow);
}

.adjust {
display: flex;
align-items: center;
justify-content: center;
}

.label {
flex: 1;
text-align: center;
}
139 changes: 128 additions & 11 deletions packages/keybr-theme-designer/lib/design/ColorInput.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,28 @@
import { type CustomTheme, type PropName } from "@keybr/themes";
import { Color } from "@keybr/widget";
import { useRef } from "react";
import {
type AnchorProps,
Color,
type Focusable,
getBoundingBox,
Icon,
IconButton,
Popover,
sizeClassName,
type SizeName,
useOnClickOutside,
} from "@keybr/widget";
import { mdiMenuLeft, mdiMenuRight } from "@mdi/js";
import { clsx } from "clsx";
import {
type ForwardedRef,
forwardRef,
useImperativeHandle,
useRef,
useState,
} from "react";
import { HexColorPicker } from "react-colorful";
import { useCustomTheme } from "../context/context.ts";
import * as styles from "./ColorImport.module.less";

export const black = Color.parse("#000000");
export const gray = Color.parse("#999999");
Expand All @@ -19,17 +40,113 @@ export const makeAccessor = (prop: PropName): Accessor => {
} as Accessor;
};

export function ColorInput({ accessor }: { readonly accessor: Accessor }) {
export function ColorInput({
accessor,
size,
}: {
readonly accessor: Accessor;
readonly size?: SizeName;
}) {
const { theme, setTheme } = useCustomTheme();
const ref = useRef<HTMLInputElement>(null);
const ref = useRef<HTMLDivElement>(null);
const [open, setOpen] = useState(false);
useOnClickOutside(ref, () => {
setOpen(false);
});
const color = accessor.getColor(theme);
return (
<input
ref={ref}
type="color"
value={accessor.getColor(theme).toRgb().formatHex()}
onChange={() => {
setTheme(accessor.setColor(theme, Color.parse(ref.current!.value)));
<Popover
open={open}
anchor={
<Button
color={color}
size={size}
onClick={() => {
setOpen(!open);
}}
/>
}
position="block-end-start"
offset={10}
>
<div ref={ref} className={styles.popup}>
<HexColorPicker
color={color.toRgb().formatHex()}
onChange={(hex) => {
setTheme(accessor.setColor(theme, Color.parse(hex)));
}}
/>
<div className={styles.adjust}>
<IconButton
icon={<Icon shape={mdiMenuLeft} />}
onClick={() => {
setTheme(accessor.setColor(theme, color.lighten(-1 / 255)));
}}
/>
<span className={styles.label}>lightness</span>
<IconButton
icon={<Icon shape={mdiMenuRight} />}
onClick={() => {
setTheme(accessor.setColor(theme, color.lighten(+1 / 255)));
}}
/>
</div>
<div className={styles.adjust}>
<IconButton
icon={<Icon shape={mdiMenuLeft} />}
onClick={() => {
setTheme(accessor.setColor(theme, color.saturate(-1 / 255)));
}}
/>
<span className={styles.label}>saturation</span>
<IconButton
icon={<Icon shape={mdiMenuRight} />}
onClick={() => {
setTheme(accessor.setColor(theme, color.saturate(+1 / 255)));
}}
/>
</div>
</div>
</Popover>
);
}

const Button = forwardRef(function Button(
{
anchor,
color,
disabled,
size,
onClick,
}: {
readonly color: Color;
readonly size?: SizeName;
readonly onClick: () => void;
} & AnchorProps,
ref: ForwardedRef<Focusable>,
) {
const element = useRef<HTMLSpanElement>(null);
useImperativeHandle(ref, () => ({
focus() {
element.current?.focus();
},
blur() {
element.current?.blur();
},
}));
useImperativeHandle(anchor, () => ({
getBoundingBox(position) {
return getBoundingBox(element.current!, position);
},
}));
return (
<span
ref={element}
className={clsx(styles.root, sizeClassName(size))}
style={{
backgroundColor: color.toRgb().formatHex(),
}}
onClick={onClick}
/>
);
}
});
3 changes: 2 additions & 1 deletion packages/keybr-theme-designer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"@keybr/textinput": "*",
"@keybr/textinput-ui": "*",
"@keybr/themes": "*",
"@keybr/widget": "*"
"@keybr/widget": "*",
"react-colorful": "^5.6.1"
},
"devDependencies": {},
"scripts": {
Expand Down
1 change: 1 addition & 0 deletions packages/keybr-widget/lib/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from "./use-fullscreen.ts";
export * from "./use-hotkeys.ts";
export * from "./use-interval.ts";
export * from "./use-mouse-hover.ts";
export * from "./use-on-click-outside.ts";
export * from "./use-screen-scroll.ts";
export * from "./use-screen-size.ts";
export * from "./use-timeout.ts";
Expand Down
15 changes: 15 additions & 0 deletions packages/keybr-widget/lib/hooks/use-on-click-outside.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { type RefObject } from "react";
import { useDocumentEvent } from "./use-document-event.ts";

export const useOnClickOutside = (
{ current }: RefObject<HTMLElement>,
onClickOutside: (event: MouseEvent | TouchEvent) => void,
) => {
const listener = (event: MouseEvent | TouchEvent) => {
if (current != null && !current.contains(event.target as Node)) {
onClickOutside(event);
}
};
useDocumentEvent("mousedown", listener);
useDocumentEvent("touchstart", listener);
};

0 comments on commit 6bc61b1

Please sign in to comment.