Compare commits
9 Commits
cb9a01109e
...
ddfc2fff15
Author | SHA1 | Date | |
---|---|---|---|
|
ddfc2fff15 | ||
|
ba8991d1b5 | ||
|
25a3cf0fce | ||
|
56ba55a4ca | ||
|
036b9fead6 | ||
|
1db89e57cc | ||
|
367117d8aa | ||
|
e32eed405f | ||
|
6643eae433 |
|
@ -1,15 +1,16 @@
|
||||||
import { Icon, IconProps } from '@iconify/react/dist/iconify.js';
|
import { Icon, IconProps } from '@iconify/react/dist/iconify.js';
|
||||||
import cx from 'clsx';
|
import cx from 'clsx';
|
||||||
import { MouseEvent, MouseEventHandler, useCallback } from 'react';
|
import { MouseEvent, MouseEventHandler, RefObject, useCallback } from 'react';
|
||||||
import styles from './ActionIcon.module.css';
|
import styles from './ActionIcon.module.css';
|
||||||
|
|
||||||
type ActionIconProps = {
|
type ActionIconProps = {
|
||||||
icon: IconProps['icon'];
|
icon: IconProps['icon'];
|
||||||
onClick?: MouseEventHandler<HTMLButtonElement>;
|
onClick?: MouseEventHandler<HTMLButtonElement>;
|
||||||
extendClassName?: HTMLButtonElement['className'];
|
extendClassName?: HTMLButtonElement['className'];
|
||||||
|
ref: RefObject<HTMLButtonElement>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ActionIcon({ icon, onClick, extendClassName }: ActionIconProps) {
|
export function ActionIcon({ icon, onClick, extendClassName, ref }: ActionIconProps) {
|
||||||
const handleClick = useCallback(
|
const handleClick = useCallback(
|
||||||
(event: MouseEvent<HTMLButtonElement>) => {
|
(event: MouseEvent<HTMLButtonElement>) => {
|
||||||
onClick?.(event);
|
onClick?.(event);
|
||||||
|
@ -18,7 +19,11 @@ export function ActionIcon({ icon, onClick, extendClassName }: ActionIconProps)
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button type="button" onClick={handleClick} className={cx(styles.action_icon, extendClassName)}>
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleClick}
|
||||||
|
className={cx(styles.action_icon, extendClassName)}
|
||||||
|
ref={ref}>
|
||||||
<Icon icon={icon} className={styles.icon} />
|
<Icon icon={icon} className={styles.icon} />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
42
src/components/ContextMenu.module.css
Normal file
42
src/components/ContextMenu.module.css
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
@layer components {
|
||||||
|
.context_menu_locationer {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
.action_icon {
|
||||||
|
background-color: transparent;
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-neutral-hover);
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
background-color: var(--color-neutral-active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.menu_body {
|
||||||
|
position: absolute;
|
||||||
|
width: max-content;
|
||||||
|
background-color: var(--color-wumeizi);
|
||||||
|
color: var(--color-yudubai);
|
||||||
|
border: 1px solid var(--color-xuanqing);
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
padding-block: var(--spacing-xs);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
z-index: 300;
|
||||||
|
.menu_item {
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--spacing-xs) var(--spacing-s);
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-primary-hover);
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
background-color: var(--color-primary-active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hr {
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
390
src/components/ContextMenu.tsx
Normal file
390
src/components/ContextMenu.tsx
Normal file
|
@ -0,0 +1,390 @@
|
||||||
|
import { SwatchEntry } from 'color-module';
|
||||||
|
import { useAtomValue, useSetAtom } from 'jotai';
|
||||||
|
import { capitalize, size } from 'lodash-es';
|
||||||
|
import {
|
||||||
|
FC,
|
||||||
|
MouseEvent,
|
||||||
|
RefObject,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { MaterialDesign2SchemeSource } from '../material-2-scheme';
|
||||||
|
import {
|
||||||
|
MaterialDesign3DynamicSchemeSource,
|
||||||
|
MaterialDesign3SchemeSource,
|
||||||
|
} from '../material-3-scheme';
|
||||||
|
import { QSchemeSource } from '../q-scheme';
|
||||||
|
import { currentPickedColor } from '../stores/colors';
|
||||||
|
import { activeSchemeAtom, useActiveScheme, useUpdateScheme } from '../stores/schemes';
|
||||||
|
import { SwatchSchemeSource } from '../swatch_scheme';
|
||||||
|
import { ActionIcon } from './ActionIcon';
|
||||||
|
import styles from './ContextMenu.module.css';
|
||||||
|
import { NotificationType, useNotification } from './Notifications';
|
||||||
|
|
||||||
|
interface ContextMenuItemProps {
|
||||||
|
color: string;
|
||||||
|
afterClick?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SetPickerMenu: FC<ContextMenuItemProps> = ({ color, afterClick }) => {
|
||||||
|
const setCurrentPicker = useSetAtom(currentPickedColor);
|
||||||
|
const handleClickAction = useCallback(() => {
|
||||||
|
setCurrentPicker(color);
|
||||||
|
afterClick?.();
|
||||||
|
}, [afterClick, color]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.menu_item} onClick={handleClickAction}>
|
||||||
|
Set to default Picker
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const QSchemeMenu: FC<ContextMenuItemProps> = ({ color, afterClick }) => {
|
||||||
|
const { showToast } = useNotification();
|
||||||
|
const activeSchemeId = useAtomValue(activeSchemeAtom);
|
||||||
|
const updateScheme = useUpdateScheme(activeSchemeId);
|
||||||
|
const updateSchemeContent = useCallback(
|
||||||
|
(content: keyof QSchemeSource) => {
|
||||||
|
updateScheme((prev) => {
|
||||||
|
prev.schemeStorage.source[content] = color;
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
showToast(
|
||||||
|
NotificationType.SUCCESS,
|
||||||
|
`${capitalize(content)} color in active scheme updated.`,
|
||||||
|
'tabler:settings-up',
|
||||||
|
3000,
|
||||||
|
);
|
||||||
|
afterClick?.();
|
||||||
|
},
|
||||||
|
[color, activeSchemeId, updateScheme],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<hr />
|
||||||
|
<div className={styles.menu_item} onClick={() => updateSchemeContent('primary')}>
|
||||||
|
Set as Primary color
|
||||||
|
</div>
|
||||||
|
<div className={styles.menu_item} onClick={() => updateSchemeContent('secondary')}>
|
||||||
|
Set as Secondary color
|
||||||
|
</div>
|
||||||
|
<div className={styles.menu_item} onClick={() => updateSchemeContent('tertiary')}>
|
||||||
|
Set as Tertiary color
|
||||||
|
</div>
|
||||||
|
<div className={styles.menu_item} onClick={() => updateSchemeContent('accent')}>
|
||||||
|
Set as Accent color
|
||||||
|
</div>
|
||||||
|
<div className={styles.menu_item} onClick={() => updateSchemeContent('danger')}>
|
||||||
|
Set as Danger color
|
||||||
|
</div>
|
||||||
|
<div className={styles.menu_item} onClick={() => updateSchemeContent('success')}>
|
||||||
|
Set as Success color
|
||||||
|
</div>
|
||||||
|
<div className={styles.menu_item} onClick={() => updateSchemeContent('warning')}>
|
||||||
|
Set as Warn color
|
||||||
|
</div>
|
||||||
|
<div className={styles.menu_item} onClick={() => updateSchemeContent('info')}>
|
||||||
|
Set as Info color
|
||||||
|
</div>
|
||||||
|
<div className={styles.menu_item} onClick={() => updateSchemeContent('foreground')}>
|
||||||
|
Set as Foreground color
|
||||||
|
</div>
|
||||||
|
<div className={styles.menu_item} onClick={() => updateSchemeContent('background')}>
|
||||||
|
Set as Background color
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SwatchSchemeMenu: FC<ContextMenuItemProps> = ({ color, afterClick }) => {
|
||||||
|
const { showToast } = useNotification();
|
||||||
|
const activeSchemeId = useAtomValue(activeSchemeAtom);
|
||||||
|
const updateScheme = useUpdateScheme(activeSchemeId);
|
||||||
|
const addColorEntry = useCallback(() => {
|
||||||
|
updateScheme((prev) => {
|
||||||
|
const source = prev.schemeStorage.source as SwatchSchemeSource;
|
||||||
|
|
||||||
|
const colorAmount = source.colors.length;
|
||||||
|
const newEntry = new SwatchEntry(`Custom Color ${colorAmount + 1}`, color);
|
||||||
|
source.colors.push(newEntry.toJsValue());
|
||||||
|
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
|
||||||
|
showToast(NotificationType.SUCCESS, 'New color entry added.', 'tabler:settings-up', 3000);
|
||||||
|
|
||||||
|
afterClick?.();
|
||||||
|
}, [color, activeSchemeId, updateScheme]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<hr />
|
||||||
|
<div className={styles.menu_item} onClick={addColorEntry}>
|
||||||
|
Add to swatch color
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Material2SchemeMenu: FC<ContextMenuItemProps> = ({ color, afterClick }) => {
|
||||||
|
const { showToast } = useNotification();
|
||||||
|
const activeSchemeId = useAtomValue(activeSchemeAtom);
|
||||||
|
const updateScheme = useUpdateScheme(activeSchemeId);
|
||||||
|
const updateSchemeColor = useCallback(
|
||||||
|
(content: keyof MaterialDesign2SchemeSource) => {
|
||||||
|
updateScheme((prev) => {
|
||||||
|
prev.schemeStorage.source[content] = color;
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
showToast(
|
||||||
|
NotificationType.SUCCESS,
|
||||||
|
`${capitalize(content)} color in active scheme updated.`,
|
||||||
|
'tabler:settings-up',
|
||||||
|
3000,
|
||||||
|
);
|
||||||
|
afterClick?.();
|
||||||
|
},
|
||||||
|
[color, activeSchemeId, updateScheme],
|
||||||
|
);
|
||||||
|
const addToCustomColor = useCallback(() => {
|
||||||
|
updateScheme((prev) => {
|
||||||
|
const source = prev.schemeStorage.source as MaterialDesign2SchemeSource;
|
||||||
|
const colorAmount = size(source.custom_colors);
|
||||||
|
source.custom_colors[`Custom Color ${colorAmount + 1}`] = color;
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
showToast(NotificationType.SUCCESS, `New color entry added.`, 'tabler:settings-up', 3000);
|
||||||
|
afterClick?.();
|
||||||
|
}, [color, activeSchemeId, updateScheme]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<hr />
|
||||||
|
<div className={styles.menu_item} onClick={() => updateSchemeColor('primary')}>
|
||||||
|
Set as Primary color
|
||||||
|
</div>
|
||||||
|
<div className={styles.menu_item} onClick={() => updateSchemeColor('secondary')}>
|
||||||
|
Set as Secondary color
|
||||||
|
</div>
|
||||||
|
<div className={styles.menu_item} onClick={() => updateSchemeColor('error')}>
|
||||||
|
Set as Error color
|
||||||
|
</div>
|
||||||
|
<div className={styles.menu_item} onClick={() => addToCustomColor()}>
|
||||||
|
Add to Custom colors
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Material3SchemeMenu: FC<ContextMenuItemProps> = ({ color, afterClick }) => {
|
||||||
|
const { showToast } = useNotification();
|
||||||
|
const activeSchemeId = useAtomValue(activeSchemeAtom);
|
||||||
|
const updateScheme = useUpdateScheme(activeSchemeId);
|
||||||
|
const updateSchemeContent = useCallback(
|
||||||
|
(content: keyof MaterialDesign3SchemeSource) => {
|
||||||
|
updateScheme((prev) => {
|
||||||
|
prev.schemeStorage.source[content] = color;
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
|
||||||
|
showToast(
|
||||||
|
NotificationType.SUCCESS,
|
||||||
|
`${capitalize(content)} color in active scheme updated.`,
|
||||||
|
'tabler:settings-up',
|
||||||
|
3000,
|
||||||
|
);
|
||||||
|
|
||||||
|
afterClick?.();
|
||||||
|
},
|
||||||
|
[color, activeSchemeId, updateScheme],
|
||||||
|
);
|
||||||
|
const addCustomColor = useCallback(() => {
|
||||||
|
updateScheme((prev) => {
|
||||||
|
const source = prev.schemeStorage.source as
|
||||||
|
| MaterialDesign3DynamicSchemeSource
|
||||||
|
| MaterialDesign3SchemeSource;
|
||||||
|
const colorAmount = size(source.custom_colors);
|
||||||
|
source.custom_colors[`Custom Color ${colorAmount + 1}`] = color;
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
|
||||||
|
showToast(NotificationType.SUCCESS, `New color entry added.`, 'tabler:settings-up', 3000);
|
||||||
|
|
||||||
|
afterClick?.();
|
||||||
|
}, [color, activeSchemeId, updateScheme]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<hr />
|
||||||
|
<div className={styles.menu_item} onClick={() => updateSchemeContent('source')}>
|
||||||
|
Set as Source color
|
||||||
|
</div>
|
||||||
|
<div className={styles.menu_item} onClick={() => updateSchemeContent('error')}>
|
||||||
|
Set as Error color
|
||||||
|
</div>
|
||||||
|
<div className={styles.menu_item} onClick={addCustomColor}>
|
||||||
|
Add to Custom colors
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ContextMenuBodyProps {
|
||||||
|
color: string;
|
||||||
|
afterClick?: () => void;
|
||||||
|
x?: number;
|
||||||
|
y?: number;
|
||||||
|
ref?: RefObject<HTMLDivElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ContextMenuBody: FC<ContextMenuBodyProps> = ({ color, afterClick, x, y, ref }) => {
|
||||||
|
const activeScheme = useActiveScheme();
|
||||||
|
|
||||||
|
const schemeMenu = useMemo(() => {
|
||||||
|
const sharedProps: ContextMenuItemProps = {
|
||||||
|
color,
|
||||||
|
afterClick,
|
||||||
|
};
|
||||||
|
switch (activeScheme?.type) {
|
||||||
|
case 'q_scheme':
|
||||||
|
return <QSchemeMenu {...sharedProps} />;
|
||||||
|
case 'swatch_scheme':
|
||||||
|
return <SwatchSchemeMenu {...sharedProps} />;
|
||||||
|
case 'material_2':
|
||||||
|
return <Material2SchemeMenu {...sharedProps} />;
|
||||||
|
case 'material_3':
|
||||||
|
case 'material_3_dynamic':
|
||||||
|
return <Material3SchemeMenu {...sharedProps} />;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, [activeScheme, color, afterClick]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.menu_body} ref={ref} style={{ top: y, left: x }}>
|
||||||
|
<SetPickerMenu color={color} afterClick={afterClick} />
|
||||||
|
{schemeMenu}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ContextMenuProps {
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ContextMenu: FC<ContextMenuProps> = ({ color }) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [initialPosition, setInitialPosition] = useState({ x: 0, y: 0 });
|
||||||
|
const [renderPosition, setRenderPosition] = useState({ x: 0, y: 0 });
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const handleOpenMenu = useCallback(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
setIsOpen(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (triggerRef.current && containerRef.current) {
|
||||||
|
const triggerRect = triggerRef.current.getBoundingClientRect();
|
||||||
|
const containerRect = containerRef.current.getBoundingClientRect();
|
||||||
|
|
||||||
|
const x = triggerRect.left - containerRect.left;
|
||||||
|
const y = triggerRect.bottom - containerRect.top;
|
||||||
|
|
||||||
|
setInitialPosition({ x, y });
|
||||||
|
setRenderPosition({ x, y });
|
||||||
|
setIsOpen(true);
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
const handleCloseMenu = useCallback(() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
}, []);
|
||||||
|
const handleLeaveClose = useCallback(
|
||||||
|
(evt: MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (!isOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const relatedTarget = evt.relatedTarget as Node | null;
|
||||||
|
|
||||||
|
if (menuRef.current && menuRef.current.contains(relatedTarget)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (triggerRef.current && triggerRef.current.contains(relatedTarget)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCloseMenu();
|
||||||
|
},
|
||||||
|
[handleCloseMenu, isOpen],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && menuRef.current && containerRef.current && triggerRef.current) {
|
||||||
|
const menuElemenet = menuRef.current;
|
||||||
|
const triggerRect = triggerRef.current.getBoundingClientRect();
|
||||||
|
const containerRect = containerRef.current.getBoundingClientRect();
|
||||||
|
|
||||||
|
const menuHeight = menuElemenet.offsetHeight;
|
||||||
|
const menuWidth = menuElemenet.offsetWidth;
|
||||||
|
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
|
||||||
|
const viewportX = containerRect.left + initialPosition.x;
|
||||||
|
const viewportY = containerRect.top + initialPosition.y;
|
||||||
|
|
||||||
|
let adjustedX = initialPosition.x;
|
||||||
|
let adjustedY = initialPosition.y;
|
||||||
|
|
||||||
|
if (viewportX + menuWidth > viewportWidth) {
|
||||||
|
adjustedX = initialPosition.x - menuWidth + triggerRect.width;
|
||||||
|
if (containerRect.left + adjustedX < 0) {
|
||||||
|
adjustedX = -containerRect.left + 5; // 留5px边距
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewportY + menuHeight > viewportHeight) {
|
||||||
|
adjustedY = initialPosition.y - menuHeight - triggerRect.height;
|
||||||
|
if (containerRect.top + adjustedY < 0) {
|
||||||
|
adjustedY = -containerRect.top + 5; // 留5px边距
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (adjustedX !== renderPosition.x || adjustedY !== renderPosition.y) {
|
||||||
|
setRenderPosition({ x: adjustedX, y: adjustedY });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isOpen, initialPosition, renderPosition.x, renderPosition.y]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.context_menu_locationer}
|
||||||
|
ref={containerRef}
|
||||||
|
onMouseLeave={handleLeaveClose}>
|
||||||
|
<ActionIcon
|
||||||
|
icon="tabler:dots-vertical"
|
||||||
|
extendClassName={styles.action_icon}
|
||||||
|
onClick={handleOpenMenu}
|
||||||
|
ref={triggerRef}
|
||||||
|
/>
|
||||||
|
{isOpen && (
|
||||||
|
<ContextMenuBody
|
||||||
|
color={color}
|
||||||
|
afterClick={handleCloseMenu}
|
||||||
|
x={renderPosition.x}
|
||||||
|
y={renderPosition.y}
|
||||||
|
ref={menuRef}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContextMenu;
|
|
@ -11,12 +11,19 @@
|
||||||
.color_block {
|
.color_block {
|
||||||
flex: 1 0;
|
flex: 1 0;
|
||||||
}
|
}
|
||||||
.color_value {
|
.operate_row {
|
||||||
padding: var(--spacing-xxs) var(--spacing-xs);
|
padding: var(--spacing-xxs) var(--spacing-xs);
|
||||||
font-size: var(--font-size-xs);
|
font-size: var(--font-size-xs);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
.color_value {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useColorFunction } from '../ColorFunctionContext';
|
import { useColorFunction } from '../ColorFunctionContext';
|
||||||
import { useCopyColor } from '../hooks/useCopyColor';
|
import { useCopyColor } from '../hooks/useCopyColor';
|
||||||
|
import ContextMenu from './ContextMenu';
|
||||||
import styles from './FlexColorStand.module.css';
|
import styles from './FlexColorStand.module.css';
|
||||||
|
|
||||||
type FlexColorStandProps = {
|
type FlexColorStandProps = {
|
||||||
|
@ -51,9 +52,12 @@ export function FlexColorStand({ color, valueMode = 'hex' }: FlexColorStandProps
|
||||||
return (
|
return (
|
||||||
<div className={styles.color_stand}>
|
<div className={styles.color_stand}>
|
||||||
<div className={styles.color_block} style={{ backgroundColor: bgColor }} />
|
<div className={styles.color_block} style={{ backgroundColor: bgColor }} />
|
||||||
|
<div className={styles.operate_row}>
|
||||||
<div className={styles.color_value} onClick={() => copyToClipboard(colorValue)}>
|
<div className={styles.color_value} onClick={() => copyToClipboard(colorValue)}>
|
||||||
{bgColor}
|
{bgColor}
|
||||||
</div>
|
</div>
|
||||||
|
<ContextMenu color={color} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
line-height: var(--font-size-xxs);
|
line-height: var(--font-size-xxs);
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
.color_block {
|
.color_block {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -19,6 +18,7 @@
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: var(--spacing-xs) var(--spacing-s);
|
padding: var(--spacing-xs) var(--spacing-s);
|
||||||
|
gap: var(--spacing-s);
|
||||||
}
|
}
|
||||||
.title {
|
.title {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -38,5 +38,6 @@
|
||||||
}
|
}
|
||||||
.color_value {
|
.color_value {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { capitalize, isEmpty } from 'lodash-es';
|
import { capitalize, isEmpty } from 'lodash-es';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { useColorFunction } from '../../ColorFunctionContext';
|
import { useColorFunction } from '../../ColorFunctionContext';
|
||||||
|
import ContextMenu from '../../components/ContextMenu';
|
||||||
import { useCopyColor } from '../../hooks/useCopyColor';
|
import { useCopyColor } from '../../hooks/useCopyColor';
|
||||||
import { ColorDescription } from '../../models';
|
import { ColorDescription } from '../../models';
|
||||||
import styles from './ColorCard.module.css';
|
import styles from './ColorCard.module.css';
|
||||||
|
@ -57,7 +58,7 @@ export function ColorCard({ color, copyMode }: ColorCardProps) {
|
||||||
}, [copytToClipboard, color, copyMode, colorHex]);
|
}, [copytToClipboard, color, copyMode, colorHex]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.card} onClick={handleCopy}>
|
<div className={styles.card}>
|
||||||
<div
|
<div
|
||||||
className={styles.color_block}
|
className={styles.color_block}
|
||||||
style={{ backgroundColor: `rgb(${color.rgb[0]}, ${color.rgb[1]}, ${color.rgb[2]})` }}
|
style={{ backgroundColor: `rgb(${color.rgb[0]}, ${color.rgb[1]}, ${color.rgb[2]})` }}
|
||||||
|
@ -69,7 +70,10 @@ export function ColorCard({ color, copyMode }: ColorCardProps) {
|
||||||
<span className={styles.en_name}>{color.pinyin.map(capitalize).join(' ')}</span>
|
<span className={styles.en_name}>{color.pinyin.map(capitalize).join(' ')}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.color_value}>#{colorHex}</div>
|
<div className={styles.color_value} onClick={handleCopy}>
|
||||||
|
#{colorHex}
|
||||||
|
</div>
|
||||||
|
<ContextMenu color={colorHex} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
@layer pages {
|
@layer pages {
|
||||||
.preview {
|
.preview {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
padding: 0 var(--spacing-m);
|
padding: 0 var(--spacing-m);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -9,6 +10,7 @@
|
||||||
font-size: var(--font-size-m);
|
font-size: var(--font-size-m);
|
||||||
}
|
}
|
||||||
.color_blocks {
|
.color_blocks {
|
||||||
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
@ -16,6 +18,7 @@
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
gap: var(--spacing-s);
|
gap: var(--spacing-s);
|
||||||
.color_block {
|
.color_block {
|
||||||
|
max-height: 23em;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
@ -42,6 +45,16 @@
|
||||||
padding-inline: var(--spacing-s);
|
padding-inline: var(--spacing-s);
|
||||||
font-size: var(--font-size-s);
|
font-size: var(--font-size-s);
|
||||||
}
|
}
|
||||||
|
.color_code_row {
|
||||||
|
height: 1.5em;
|
||||||
|
padding-inline: var(--spacing-s);
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
overflow: visible;
|
||||||
.color_code {
|
.color_code {
|
||||||
height: 1.5em;
|
height: 1.5em;
|
||||||
padding-inline: var(--spacing-s);
|
padding-inline: var(--spacing-s);
|
||||||
|
@ -55,4 +68,5 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import cx from 'clsx';
|
import cx from 'clsx';
|
||||||
import { constant, flatten, isEqual, take, times } from 'lodash-es';
|
import { constant, flatten, isEqual, take, times } from 'lodash-es';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
import ContextMenu from '../../components/ContextMenu';
|
||||||
import { useCopyColor } from '../../hooks/useCopyColor';
|
import { useCopyColor } from '../../hooks/useCopyColor';
|
||||||
import { HarmonyColor } from '../../models';
|
import { HarmonyColor } from '../../models';
|
||||||
import styles from './HarmonyPreview.module.css';
|
import styles from './HarmonyPreview.module.css';
|
||||||
|
@ -33,9 +34,12 @@ export function HarmonyPreview({ colors = [] }: HarmonyPreviewProps) {
|
||||||
style={{ flexGrow: ratio }}>
|
style={{ flexGrow: ratio }}>
|
||||||
<div className={styles.color_ratio}>{ratio > 0 && `Ratio: ${ratio}`}</div>
|
<div className={styles.color_ratio}>{ratio > 0 && `Ratio: ${ratio}`}</div>
|
||||||
<div className={styles.color_square} style={{ backgroundColor: `#${color}` }}></div>
|
<div className={styles.color_square} style={{ backgroundColor: `#${color}` }}></div>
|
||||||
|
<div className={styles.color_code_row}>
|
||||||
<div className={styles.color_code}>
|
<div className={styles.color_code}>
|
||||||
{ratio > 0 && <span onClick={() => copyColor(color)}>#{color}</span>}
|
{ratio > 0 && <span onClick={() => copyColor(color)}>#{color}</span>}
|
||||||
</div>
|
</div>
|
||||||
|
<ContextMenu color={color} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -31,7 +31,7 @@ function SchemeItem({ item }: SchemeItemProps) {
|
||||||
const isActived = useMemo(() => isEqual(activedScheme, item.id), [activedScheme, item.id]);
|
const isActived = useMemo(() => isEqual(activedScheme, item.id), [activedScheme, item.id]);
|
||||||
const isSelected = useMemo(() => isEqual(navParams['id'], item.id), [navParams, item.id]);
|
const isSelected = useMemo(() => isEqual(navParams['id'], item.id), [navParams, item.id]);
|
||||||
const handleActiveScheme = useCallback(() => {
|
const handleActiveScheme = useCallback(() => {
|
||||||
setActiveScheme((prev) => (prev ? null : item.id));
|
setActiveScheme((prev) => (prev === item.id ? null : item.id));
|
||||||
}, [item]);
|
}, [item]);
|
||||||
const handleRemoveScheme = useCallback(() => {
|
const handleRemoveScheme = useCallback(() => {
|
||||||
removeScheme();
|
removeScheme();
|
||||||
|
|
|
@ -70,7 +70,7 @@ export function useScheme(id?: string | null): SchemeContent<SchemeStorage> | nu
|
||||||
|
|
||||||
export function useActiveScheme(): SchemeContent<SchemeStorage> | null {
|
export function useActiveScheme(): SchemeContent<SchemeStorage> | null {
|
||||||
const activeSchemeId = useAtomValue(activeSchemeAtom);
|
const activeSchemeId = useAtomValue(activeSchemeAtom);
|
||||||
const activeScheme = useScheme(activeSchemeId ?? 'UNEXISTS');
|
const activeScheme = useScheme(activeSchemeId ?? null);
|
||||||
return activeScheme;
|
return activeScheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user