diff --git a/package-lock.json b/package-lock.json index c1f8e66c2..38de8f92b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10785,6 +10785,16 @@ "node": ">=0.10.0" } }, + "node_modules/react-colorful": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz", + "integrity": "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -14358,7 +14368,8 @@ "@keybr/textinput": "*", "@keybr/textinput-ui": "*", "@keybr/themes": "*", - "@keybr/widget": "*" + "@keybr/widget": "*", + "react-colorful": "^5.6.1" }, "devDependencies": {} }, diff --git a/packages/keybr-theme-designer/lib/design/ColorImport.module.less b/packages/keybr-theme-designer/lib/design/ColorImport.module.less new file mode 100644 index 000000000..2c92fa939 --- /dev/null +++ b/packages/keybr-theme-designer/lib/design/ColorImport.module.less @@ -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; +} diff --git a/packages/keybr-theme-designer/lib/design/ColorInput.tsx b/packages/keybr-theme-designer/lib/design/ColorInput.tsx index 819523cb0..23f2f771e 100644 --- a/packages/keybr-theme-designer/lib/design/ColorInput.tsx +++ b/packages/keybr-theme-designer/lib/design/ColorInput.tsx @@ -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"); @@ -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(null); + const ref = useRef(null); + const [open, setOpen] = useState(false); + useOnClickOutside(ref, () => { + setOpen(false); + }); + const color = accessor.getColor(theme); return ( - { - setTheme(accessor.setColor(theme, Color.parse(ref.current!.value))); + { + setOpen(!open); + }} + /> + } + position="block-end-start" + offset={10} + > +
+ { + setTheme(accessor.setColor(theme, Color.parse(hex))); + }} + /> +
+ } + onClick={() => { + setTheme(accessor.setColor(theme, color.lighten(-1 / 255))); + }} + /> + lightness + } + onClick={() => { + setTheme(accessor.setColor(theme, color.lighten(+1 / 255))); + }} + /> +
+
+ } + onClick={() => { + setTheme(accessor.setColor(theme, color.saturate(-1 / 255))); + }} + /> + saturation + } + onClick={() => { + setTheme(accessor.setColor(theme, color.saturate(+1 / 255))); + }} + /> +
+
+
+ ); +} + +const Button = forwardRef(function Button( + { + anchor, + color, + disabled, + size, + onClick, + }: { + readonly color: Color; + readonly size?: SizeName; + readonly onClick: () => void; + } & AnchorProps, + ref: ForwardedRef, +) { + const element = useRef(null); + useImperativeHandle(ref, () => ({ + focus() { + element.current?.focus(); + }, + blur() { + element.current?.blur(); + }, + })); + useImperativeHandle(anchor, () => ({ + getBoundingBox(position) { + return getBoundingBox(element.current!, position); + }, + })); + return ( + ); -} +}); diff --git a/packages/keybr-theme-designer/package.json b/packages/keybr-theme-designer/package.json index 91d3eb77c..0aedad763 100644 --- a/packages/keybr-theme-designer/package.json +++ b/packages/keybr-theme-designer/package.json @@ -15,7 +15,8 @@ "@keybr/textinput": "*", "@keybr/textinput-ui": "*", "@keybr/themes": "*", - "@keybr/widget": "*" + "@keybr/widget": "*", + "react-colorful": "^5.6.1" }, "devDependencies": {}, "scripts": { diff --git a/packages/keybr-widget/lib/hooks/index.ts b/packages/keybr-widget/lib/hooks/index.ts index 087ce2529..feb573315 100644 --- a/packages/keybr-widget/lib/hooks/index.ts +++ b/packages/keybr-widget/lib/hooks/index.ts @@ -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"; diff --git a/packages/keybr-widget/lib/hooks/use-on-click-outside.ts b/packages/keybr-widget/lib/hooks/use-on-click-outside.ts new file mode 100644 index 000000000..523085107 --- /dev/null +++ b/packages/keybr-widget/lib/hooks/use-on-click-outside.ts @@ -0,0 +1,15 @@ +import { type RefObject } from "react"; +import { useDocumentEvent } from "./use-document-event.ts"; + +export const useOnClickOutside = ( + { current }: RefObject, + 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); +};