mirror of
https://github.com/outline/outline.git
synced 2026-08-03 13:27:25 +03:00
Icon picker glow-up (#13177)
* Icon panel glow-up * Move colorPalette to shared/constants
This commit is contained in:
@@ -9,7 +9,7 @@ import { randomElement } from "@shared/random";
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import type { Option } from "~/components/InputSelect";
|
||||
import { IconLibrary } from "@shared/utils/IconLibrary";
|
||||
import { colorPalette } from "@shared/utils/collections";
|
||||
import { colorPalette } from "@shared/constants";
|
||||
import { CollectionValidation } from "@shared/validations";
|
||||
import type Collection from "~/models/Collection";
|
||||
import Button from "~/components/Button";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import styled, { css } from "styled-components";
|
||||
import NudeButton from "./NudeButton";
|
||||
import { hover, s } from "@shared/styles";
|
||||
|
||||
@@ -29,6 +29,9 @@ export const ColorButton = React.forwardRef(
|
||||
)
|
||||
);
|
||||
|
||||
/** Highlight for the hovered or selected swatch, falls back for the gradient. */
|
||||
const ring = css`2px solid var(--color, ${s("textTertiary")})`;
|
||||
|
||||
const Selected = styled.span`
|
||||
width: 10px;
|
||||
height: 5px;
|
||||
@@ -52,9 +55,11 @@ const ColorButtonInternal = styled(NudeButton)<{
|
||||
linear-gradient(135deg, #ff5858 0%, #fbcc34 50%, #00c6ff 100%)
|
||||
);
|
||||
|
||||
outline-offset: 1px;
|
||||
outline: ${({ $active }) => ($active ? ring : "none")};
|
||||
|
||||
&: ${hover} {
|
||||
outline: 2px solid ${s("menuBackground")} !important;
|
||||
box-shadow: 0px 0px 3px 3px var(--color);
|
||||
outline: ${ring} !important;
|
||||
}
|
||||
|
||||
& ${Selected} {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { toError } from "@shared/utils/error";
|
||||
import Icon from "@shared/components/Icon";
|
||||
import { colorPalette } from "@shared/utils/collections";
|
||||
import { colorPalette } from "@shared/constants";
|
||||
import type { Option } from "~/components/InputSelect";
|
||||
import { InputSelect } from "~/components/InputSelect";
|
||||
import useStores from "~/hooks/useStores";
|
||||
|
||||
@@ -8,8 +8,9 @@ import { getEmojis, getEmojisWithCategory, search } from "@shared/utils/emoji";
|
||||
import Flex from "~/components/Flex";
|
||||
import { EmojiCreateDialog } from "~/components/EmojiDialog/EmojiCreateDialog";
|
||||
import { DisplayCategory } from "../utils";
|
||||
import type { DataNode, EmojiNode } from "./GridTemplate";
|
||||
import type { DataNode, EmojiNode, IconNode } from "./GridTemplate";
|
||||
import GridTemplate from "./GridTemplate";
|
||||
import { IconPreview, PREVIEW_HEIGHT } from "./IconPreview";
|
||||
import SkinTonePicker from "./SkinTonePicker";
|
||||
import { StyledInputSearch, UserInputContainer } from "./Components";
|
||||
import { useIconState } from "../useIconState";
|
||||
@@ -136,6 +137,20 @@ const EmojiPanel = ({
|
||||
});
|
||||
}, [emojis, getFrequentCustomIcons]);
|
||||
|
||||
const [activeEmoji, setActiveEmoji] = React.useState<EmojiNode>();
|
||||
const [hasMoreBelow, setHasMoreBelow] = React.useState(false);
|
||||
|
||||
const handleEmojiActive = React.useCallback((icon: IconNode) => {
|
||||
if (icon.type !== IconType.SVG) {
|
||||
setActiveEmoji(icon);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Drop the preview when the grid contents change out from under it.
|
||||
React.useEffect(() => {
|
||||
setActiveEmoji(undefined);
|
||||
}, [query, skinTone]);
|
||||
|
||||
const isSearch = query !== "";
|
||||
const templateData: DataNode[] = isSearch
|
||||
? getSearchResults({
|
||||
@@ -180,9 +195,11 @@ const EmojiPanel = ({
|
||||
<GridTemplate
|
||||
ref={scrollableRef}
|
||||
width={panelWidth}
|
||||
height={height - 48}
|
||||
height={height - 48 - PREVIEW_HEIGHT}
|
||||
data={templateData}
|
||||
onIconSelect={handleEmojiSelection}
|
||||
onIconActive={handleEmojiActive}
|
||||
onOverflowChange={setHasMoreBelow}
|
||||
empty={
|
||||
<IconButton
|
||||
onClick={handleUploadClick}
|
||||
@@ -192,10 +209,20 @@ const EmojiPanel = ({
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
<IconPreview
|
||||
icon={activeEmoji ?? firstIcon(templateData)}
|
||||
showFade={hasMoreBelow}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
/** The first icon shown in the grid, previewed until the user hovers another. */
|
||||
const firstIcon = (data: DataNode[]): EmojiNode | undefined => {
|
||||
const icon = data.find((node) => node.icons.length)?.icons[0];
|
||||
return icon && icon.type !== IconType.SVG ? icon : undefined;
|
||||
};
|
||||
|
||||
const getSearchResults = ({
|
||||
query,
|
||||
skinTone,
|
||||
@@ -218,6 +245,7 @@ const getSearchResults = ({
|
||||
type: IconType.Emoji as const,
|
||||
id: emoji.id,
|
||||
value: emoji.value,
|
||||
name: emoji.name,
|
||||
})),
|
||||
];
|
||||
|
||||
@@ -251,6 +279,7 @@ const getAllEmojis = ({
|
||||
type: IconType.Emoji as const,
|
||||
id: emoji.id,
|
||||
value: emoji.value,
|
||||
name: emoji.name,
|
||||
})),
|
||||
...freqCustomEmojis,
|
||||
];
|
||||
@@ -269,6 +298,7 @@ const getAllEmojis = ({
|
||||
type: IconType.Emoji,
|
||||
id: emoji.id,
|
||||
value: emoji.value,
|
||||
name: emoji.name,
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -9,23 +9,45 @@ type Props = {
|
||||
data: React.ReactNode[][];
|
||||
columns: number;
|
||||
itemWidth: number;
|
||||
onOverflowChange?: (hasMoreBelow: boolean) => void;
|
||||
};
|
||||
|
||||
const Grid = (
|
||||
{ width, height, data, columns, itemWidth }: Props,
|
||||
{ width, height, data, columns, itemWidth, onOverflowChange }: Props,
|
||||
ref: React.Ref<HTMLDivElement>
|
||||
) => (
|
||||
<Container
|
||||
outerRef={ref}
|
||||
width={width}
|
||||
height={height}
|
||||
itemCount={data.length}
|
||||
itemSize={itemWidth}
|
||||
itemData={{ data, columns }}
|
||||
>
|
||||
{Row}
|
||||
</Container>
|
||||
);
|
||||
) => {
|
||||
const offsetRef = React.useRef(0);
|
||||
const maxOffset = Math.max(0, data.length * itemWidth - height);
|
||||
|
||||
const notify = React.useCallback(() => {
|
||||
onOverflowChange?.(offsetRef.current < maxOffset - 1);
|
||||
}, [onOverflowChange, maxOffset]);
|
||||
|
||||
// The amount of scrollable content changes as rows are added or removed.
|
||||
React.useEffect(notify, [notify]);
|
||||
|
||||
const handleScroll = React.useCallback(
|
||||
({ scrollOffset }: { scrollOffset: number }) => {
|
||||
offsetRef.current = scrollOffset;
|
||||
notify();
|
||||
},
|
||||
[notify]
|
||||
);
|
||||
|
||||
return (
|
||||
<Container
|
||||
outerRef={ref}
|
||||
width={width}
|
||||
height={height}
|
||||
itemCount={data.length}
|
||||
itemSize={itemWidth}
|
||||
itemData={{ data, columns }}
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
{Row}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
type RowProps = {
|
||||
data: React.ReactNode[][];
|
||||
|
||||
@@ -21,7 +21,7 @@ const BUTTON_SIZE_MOBILE = 40;
|
||||
const ICON_SIZE_DESKTOP = 24;
|
||||
const ICON_SIZE_MOBILE = 32;
|
||||
|
||||
type OutlineNode = {
|
||||
export type OutlineNode = {
|
||||
type: IconType.SVG;
|
||||
name: string;
|
||||
color: string;
|
||||
@@ -36,9 +36,11 @@ export type EmojiNode = {
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export type IconNode = OutlineNode | EmojiNode;
|
||||
|
||||
export type DataNode = {
|
||||
category: keyof typeof TRANSLATED_CATEGORIES;
|
||||
icons: (OutlineNode | EmojiNode)[];
|
||||
icons: IconNode[];
|
||||
};
|
||||
|
||||
type Props = {
|
||||
@@ -52,10 +54,22 @@ type Props = {
|
||||
empty?: React.ReactNode;
|
||||
/** Callback when an icon is selected */
|
||||
onIconSelect: ({ id, value }: { id: string; value: string }) => void;
|
||||
/** Callback when an icon is hovered or focused */
|
||||
onIconActive?: (icon: IconNode) => void;
|
||||
/** Callback when whether the grid has content below the fold changes */
|
||||
onOverflowChange?: (hasMoreBelow: boolean) => void;
|
||||
};
|
||||
|
||||
const GridTemplate = (
|
||||
{ width, height, data, empty, onIconSelect }: Props,
|
||||
{
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
empty,
|
||||
onIconSelect,
|
||||
onIconActive,
|
||||
onOverflowChange,
|
||||
}: Props,
|
||||
ref: React.Ref<HTMLDivElement>
|
||||
) => {
|
||||
const isMobile = useMobile();
|
||||
@@ -85,11 +99,15 @@ const GridTemplate = (
|
||||
}
|
||||
|
||||
const items = node.icons.map((item) => {
|
||||
const handleActive = () => onIconActive?.(item);
|
||||
|
||||
if (item.type === IconType.SVG) {
|
||||
return (
|
||||
<IconButton
|
||||
key={item.name}
|
||||
onClick={() => onIconSelect({ id: item.name, value: item.name })}
|
||||
onMouseEnter={handleActive}
|
||||
onFocus={handleActive}
|
||||
style={{ "--delay": `${item.delay}ms` } as React.CSSProperties}
|
||||
>
|
||||
<Icon
|
||||
@@ -107,6 +125,8 @@ const GridTemplate = (
|
||||
<IconButton
|
||||
key={item.id}
|
||||
onClick={() => onIconSelect({ id: item.id, value: item.value })}
|
||||
onMouseEnter={handleActive}
|
||||
onFocus={handleActive}
|
||||
>
|
||||
<Emoji
|
||||
width={iconSize}
|
||||
@@ -136,6 +156,7 @@ const GridTemplate = (
|
||||
data={gridItems}
|
||||
columns={itemsPerRow}
|
||||
itemWidth={buttonSize}
|
||||
onOverflowChange={onOverflowChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import { s } from "@shared/styles";
|
||||
import { colorPalette } from "@shared/utils/collections";
|
||||
import Flex from "~/components/Flex";
|
||||
import { colorPalette } from "@shared/constants";
|
||||
import { SwatchButton } from "~/components/SwatchButton";
|
||||
import { ColorButton } from "~/components/ColorButton";
|
||||
|
||||
const SWATCH_SIZE = 20;
|
||||
|
||||
type Props = {
|
||||
width: number;
|
||||
activeColor: string;
|
||||
@@ -27,12 +27,12 @@ const IconColorPicker = ({ activeColor, onSelect }: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Container justify="space-between" align="center" auto>
|
||||
<Container>
|
||||
<PresetColors activeColor={selectedColor} onClick={handleSelect} />
|
||||
<Divider />
|
||||
<SwatchButton
|
||||
color={color}
|
||||
active={!isBuiltInColor}
|
||||
size={SWATCH_SIZE}
|
||||
onChange={handleSelect}
|
||||
pickerInModal
|
||||
/>
|
||||
@@ -40,12 +40,6 @@ const IconColorPicker = ({ activeColor, onSelect }: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const Divider = styled.div`
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background-color: ${s("inputBorder")};
|
||||
`;
|
||||
|
||||
const PresetColors = ({
|
||||
activeColor,
|
||||
onClick,
|
||||
@@ -59,16 +53,22 @@ const PresetColors = ({
|
||||
key={color}
|
||||
color={color}
|
||||
active={color === activeColor}
|
||||
size={SWATCH_SIZE}
|
||||
onClick={() => onClick(color)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
const Container = styled(Flex)`
|
||||
// One column per preset color plus the custom swatch, matching the horizontal
|
||||
// rhythm of the icon grid below.
|
||||
const Container = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(${colorPalette.length + 1}, 1fr);
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
height: 48px;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid ${s("inputBorder")};
|
||||
`;
|
||||
|
||||
export default IconColorPicker;
|
||||
|
||||
@@ -7,8 +7,9 @@ import Flex from "~/components/Flex";
|
||||
import InputSearch from "~/components/InputSearch";
|
||||
import { DisplayCategory } from "../utils";
|
||||
import IconColorPicker from "./IconColorPicker";
|
||||
import type { DataNode } from "./GridTemplate";
|
||||
import type { DataNode, IconNode } from "./GridTemplate";
|
||||
import GridTemplate from "./GridTemplate";
|
||||
import { IconPreview, PREVIEW_HEIGHT } from "./IconPreview";
|
||||
import { useIconState } from "../useIconState";
|
||||
|
||||
const IconNames = Object.keys(IconLibrary.mapping);
|
||||
@@ -18,7 +19,7 @@ const TotalIcons = IconNames.length;
|
||||
* This is needed as a constant for react-window.
|
||||
* Calculated from the heights of TabPanel, ColorPicker and InputSearch.
|
||||
*/
|
||||
const GRID_HEIGHT = 314;
|
||||
const GRID_HEIGHT = 314 - PREVIEW_HEIGHT;
|
||||
|
||||
type Props = {
|
||||
panelWidth: number;
|
||||
@@ -75,6 +76,24 @@ const IconPanel = ({
|
||||
[onIconChange, incrementIconCount]
|
||||
);
|
||||
|
||||
const [activeIcon, setActiveIcon] = React.useState<string>();
|
||||
const [hasMoreBelow, setHasMoreBelow] = React.useState(false);
|
||||
|
||||
const handleIconActive = React.useCallback((icon: IconNode) => {
|
||||
if (icon.type === IconType.SVG) {
|
||||
setActiveIcon(icon.name);
|
||||
}
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
setActiveIcon(undefined);
|
||||
}, [query]);
|
||||
|
||||
// Preview the first icon shown in the grid until the user hovers another.
|
||||
const previewIcon =
|
||||
activeIcon ??
|
||||
(isSearch ? filteredIcons[0] : (freqIcons[0] ?? filteredIcons[0]));
|
||||
|
||||
const baseIcons: DataNode = {
|
||||
category,
|
||||
icons: filteredIcons.map((name, index) => ({
|
||||
@@ -133,6 +152,22 @@ const IconPanel = ({
|
||||
height={GRID_HEIGHT}
|
||||
data={templateData}
|
||||
onIconSelect={handleIconSelection}
|
||||
onIconActive={handleIconActive}
|
||||
onOverflowChange={setHasMoreBelow}
|
||||
/>
|
||||
<IconPreview
|
||||
showFade={hasMoreBelow}
|
||||
icon={
|
||||
previewIcon
|
||||
? {
|
||||
type: IconType.SVG,
|
||||
name: previewIcon,
|
||||
color,
|
||||
initial,
|
||||
delay: 0,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { startCase } from "es-toolkit/compat";
|
||||
import styled from "styled-components";
|
||||
import { s } from "@shared/styles";
|
||||
import { IconType } from "@shared/types";
|
||||
import { IconLibrary } from "@shared/utils/IconLibrary";
|
||||
import { CustomEmoji } from "@shared/components/CustomEmoji";
|
||||
import { Emoji } from "~/components/Emoji";
|
||||
import Flex from "~/components/Flex";
|
||||
import Text from "~/components/Text";
|
||||
import type { IconNode } from "./GridTemplate";
|
||||
|
||||
/** Height of the preview bar, used to size the grid above it. */
|
||||
export const PREVIEW_HEIGHT = 56;
|
||||
|
||||
const GLYPH_SIZE = 28;
|
||||
|
||||
type Props = {
|
||||
/** The icon or emoji to preview, nothing is rendered when empty */
|
||||
icon?: IconNode;
|
||||
/** Whether the grid above has content below the fold, fades the last row */
|
||||
showFade?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Displays a large preview of the hovered icon or emoji alongside its name and
|
||||
* shortcode.
|
||||
*/
|
||||
export const IconPreview = ({ icon, showFade }: Props) => (
|
||||
<Container align="center" gap={12} $showFade={showFade}>
|
||||
{icon && (
|
||||
<>
|
||||
<Glyph justify="center" align="center">
|
||||
{icon.type === IconType.SVG ? (
|
||||
<Icon
|
||||
as={IconLibrary.getComponent(icon.name)}
|
||||
color={icon.color}
|
||||
size={GLYPH_SIZE}
|
||||
>
|
||||
{icon.initial}
|
||||
</Icon>
|
||||
) : (
|
||||
<Emoji size={GLYPH_SIZE}>
|
||||
{icon.type === IconType.Custom ? (
|
||||
<CustomEmoji value={icon.value} title={icon.name} />
|
||||
) : (
|
||||
icon.value
|
||||
)}
|
||||
</Emoji>
|
||||
)}
|
||||
</Glyph>
|
||||
<Labels column>
|
||||
<Name weight="bold" ellipsis>
|
||||
{displayName(icon)}
|
||||
</Name>
|
||||
<Text type="tertiary" size="xsmall" monospace ellipsis>
|
||||
:{shortcode(icon)}:
|
||||
</Text>
|
||||
</Labels>
|
||||
</>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
|
||||
const displayName = (icon: IconNode) =>
|
||||
icon.type === IconType.SVG
|
||||
? startCase(icon.name)
|
||||
: startCase(icon.name ?? "");
|
||||
|
||||
const shortcode = (icon: IconNode) => {
|
||||
if (icon.type === IconType.SVG) {
|
||||
return icon.name;
|
||||
}
|
||||
return icon.type === IconType.Custom ? (icon.name ?? icon.value) : icon.id;
|
||||
};
|
||||
|
||||
const Container = styled(Flex)<{ $showFade?: boolean }>`
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
height: ${PREVIEW_HEIGHT}px;
|
||||
padding: 0 16px;
|
||||
background: ${s("menuBackground")};
|
||||
|
||||
// Offsets the bottom padding of the containing popover.
|
||||
margin-bottom: -6px;
|
||||
|
||||
// Fades out the last row of the grid while there's more to scroll.
|
||||
&:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 100%;
|
||||
height: 24px;
|
||||
pointer-events: none;
|
||||
opacity: ${({ $showFade }) => ($showFade ? 1 : 0)};
|
||||
transition: opacity 150ms ease-in-out;
|
||||
background: linear-gradient(to bottom, transparent, ${s("menuBackground")});
|
||||
}
|
||||
`;
|
||||
|
||||
const Name = styled(Text)`
|
||||
font-size: 15px;
|
||||
`;
|
||||
|
||||
const Glyph = styled(Flex)`
|
||||
width: ${GLYPH_SIZE}px;
|
||||
height: ${GLYPH_SIZE}px;
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
const Labels = styled(Flex)`
|
||||
min-width: 0;
|
||||
`;
|
||||
|
||||
const Icon = styled.svg`
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
@@ -2,7 +2,7 @@ import { observer } from "mobx-react";
|
||||
import { CollectionIcon, PrivateCollectionIcon } from "outline-icons";
|
||||
import { getLuminance } from "polished";
|
||||
import Icon from "@shared/components/Icon";
|
||||
import { colorPalette } from "@shared/utils/collections";
|
||||
import { colorPalette } from "@shared/constants";
|
||||
import type Collection from "~/models/Collection";
|
||||
import useStores from "~/hooks/useStores";
|
||||
|
||||
|
||||
@@ -20,6 +20,12 @@ const EmojiPanel = createLazyComponent(
|
||||
() => import("~/components/IconPicker/components/EmojiPanel")
|
||||
);
|
||||
|
||||
/** Fits nine 32px emoji per row, with 12px of padding on either side. */
|
||||
const PANEL_WIDTH = 332;
|
||||
|
||||
/** Leaves room for two more 32px rows than the panel's minimum. */
|
||||
const PANEL_HEIGHT = 364;
|
||||
|
||||
type Props = {
|
||||
/** Callback when an emoji is selected by the user. */
|
||||
onSelect: (emoji: string) => Promise<void>;
|
||||
@@ -37,7 +43,7 @@ const ReactionPicker: React.FC<Props> = ({ onSelect, className, size }) => {
|
||||
|
||||
const [query, setQuery] = React.useState("");
|
||||
|
||||
const popoverWidth = isMobile ? windowWidth : 300;
|
||||
const popoverWidth = isMobile ? windowWidth : PANEL_WIDTH;
|
||||
// In mobile, popover is absolutely positioned to leave 8px on both sides.
|
||||
const panelWidth = isMobile ? windowWidth - 16 : popoverWidth;
|
||||
|
||||
@@ -79,7 +85,7 @@ const ReactionPicker: React.FC<Props> = ({ onSelect, className, size }) => {
|
||||
<React.Suspense fallback={<Placeholder />}>
|
||||
<EventBoundary>
|
||||
<EmojiPanel.Component
|
||||
height={300}
|
||||
height={PANEL_HEIGHT}
|
||||
panelWidth={panelWidth}
|
||||
query={query}
|
||||
onEmojiChange={handleEmojiSelect}
|
||||
@@ -96,7 +102,10 @@ const ReactionPicker: React.FC<Props> = ({ onSelect, className, size }) => {
|
||||
|
||||
const Placeholder = React.memo(
|
||||
() => (
|
||||
<VStack spacing={6} style={{ height: "300px", padding: "6px 12px" }}>
|
||||
<VStack
|
||||
spacing={6}
|
||||
style={{ height: `${PANEL_HEIGHT}px`, padding: "6px 12px" }}
|
||||
>
|
||||
<HStack>
|
||||
<PlaceholderText height={32} minWidth={90} />
|
||||
<PlaceholderText height={32} width={32} />
|
||||
|
||||
@@ -9,7 +9,7 @@ import Heading from "~/components/Heading";
|
||||
import ContentEditable from "~/components/ContentEditable";
|
||||
import CollectionIcon from "~/components/Icons/CollectionIcon";
|
||||
import type Collection from "~/models/Collection";
|
||||
import { colorPalette } from "@shared/utils/collections";
|
||||
import { colorPalette } from "@shared/constants";
|
||||
import usePolicy from "~/hooks/usePolicy";
|
||||
import { observer } from "mobx-react";
|
||||
import lazyWithRetry from "~/utils/lazyWithRetry";
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useRouteMatch } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import Text from "@shared/components/Text";
|
||||
import { richExtensions, withComments } from "@shared/editor/nodes";
|
||||
import { colorPalette } from "@shared/utils/collections";
|
||||
import { colorPalette } from "@shared/constants";
|
||||
import Comment from "~/models/Comment";
|
||||
import type Document from "~/models/Document";
|
||||
import type Template from "~/models/Template";
|
||||
|
||||
@@ -10,7 +10,7 @@ import { s, depths, hover } from "@shared/styles";
|
||||
import { cloneDeep } from "es-toolkit/compat";
|
||||
import type { ProsemirrorData } from "@shared/types";
|
||||
import { ProsemirrorHelper } from "@shared/utils/ProsemirrorHelper";
|
||||
import { colorPalette } from "@shared/utils/collections";
|
||||
import { colorPalette } from "@shared/constants";
|
||||
import Editor from "~/components/Editor";
|
||||
import NudeButton from "~/components/NudeButton";
|
||||
import Text from "~/components/Text";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { colorPalette } from "@shared/utils/collections";
|
||||
import { colorPalette } from "@shared/constants";
|
||||
import type Document from "~/models/Document";
|
||||
import type Revision from "~/models/Revision";
|
||||
import type { Props as EditorProps } from "~/components/Editor";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FetchError } from "node-fetch";
|
||||
import { Op, QueryTypes } from "sequelize";
|
||||
import { toError } from "@shared/utils/error";
|
||||
import { colorPalette } from "@shared/utils/collections";
|
||||
import { colorPalette } from "@shared/constants";
|
||||
import WebhookDisabledEmail from "@server/emails/templates/WebhookDisabledEmail";
|
||||
import env from "@server/env";
|
||||
import Logger from "@server/logging/Logger";
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
ImportTaskState,
|
||||
MentionType,
|
||||
} from "@shared/types";
|
||||
import { colorPalette } from "@shared/utils/collections";
|
||||
import { colorPalette } from "@shared/constants";
|
||||
import { UrlHelper } from "@shared/utils/UrlHelper";
|
||||
import { CollectionValidation } from "@shared/validations";
|
||||
import { createContext } from "@server/context";
|
||||
|
||||
@@ -5,7 +5,7 @@ import breakpoint from "styled-components-breakpoint";
|
||||
import useStores from "../hooks/useStores";
|
||||
import { IconType } from "../types";
|
||||
import { IconLibrary } from "../utils/IconLibrary";
|
||||
import { colorPalette } from "../utils/collections";
|
||||
import { colorPalette } from "../constants";
|
||||
import { determineIconType } from "../utils/icon";
|
||||
import EmojiIcon from "./EmojiIcon";
|
||||
import Flex from "./Flex";
|
||||
|
||||
@@ -10,6 +10,21 @@ import {
|
||||
|
||||
export const MAX_AVATAR_DISPLAY = 6;
|
||||
|
||||
/** Preset colors offered when choosing an icon color. */
|
||||
export const colorPalette = [
|
||||
"#4E5C6E",
|
||||
"#0366D6",
|
||||
"#2BC2FF",
|
||||
"#9E5CF7",
|
||||
"#FF825C",
|
||||
"#FF5C80",
|
||||
"#FFBE0B",
|
||||
"#42DED1",
|
||||
"#00D084",
|
||||
"#FF4DFA",
|
||||
"#2F362F",
|
||||
];
|
||||
|
||||
export const Pagination = {
|
||||
defaultLimit: 25,
|
||||
defaultOffset: 0,
|
||||
|
||||
@@ -40,16 +40,3 @@ export const sortNavigationNodes = (
|
||||
return { ...node, children: sortedChildren };
|
||||
});
|
||||
};
|
||||
|
||||
export const colorPalette = [
|
||||
"#4E5C6E",
|
||||
"#0366D6",
|
||||
"#9E5CF7",
|
||||
"#FF825C",
|
||||
"#FF5C80",
|
||||
"#FFBE0B",
|
||||
"#42DED1",
|
||||
"#00D084",
|
||||
"#FF4DFA",
|
||||
"#2F362F",
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user