Skip to content

Commit 5d3aab9

Browse files
authored
style: dynamic color (#1387)
* style: dark mode card adjust * style: dynamic color
1 parent f5aad0f commit 5d3aab9

3 files changed

Lines changed: 115 additions & 4 deletions

File tree

apps/nextjs-app/src/features/app/blocks/space/BaseCard.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { useRouter } from 'next/router';
1111
import { useState, type FC, useRef } from 'react';
1212
import { Emoji } from '../../components/emoji/Emoji';
1313
import { EmojiPicker } from '../../components/emoji/EmojiPicker';
14+
import { ColorBg } from './ColorBg';
1415
import { BaseActionTrigger } from './component/BaseActionTrigger';
1516
import { StarButton } from './space-side-bar/StarButton';
1617

@@ -91,11 +92,11 @@ export const BaseCard: FC<IBaseCard> = (props) => {
9192
className={cn('relative group cursor-pointer hover:shadow-md overflow-x-hidden', className)}
9293
onClick={intoBase}
9394
>
94-
<div className="absolute inset-0 z-0 bg-gradient-to-r from-primary/10 via-primary/5 to-transparent opacity-0 transition-opacity duration-200 group-hover:opacity-100"></div>
95+
<ColorBg emoji={base.icon || undefined} />
9596
<CardContent className="relative flex size-full items-center gap-3 px-4 py-0">
9697
<div onClick={(e) => hasUpdatePermission && clickStopPropagation(e)}>
9798
<EmojiPicker disabled={!hasUpdatePermission || renaming} onChange={iconChange}>
98-
<div className="size-12 rounded-lg bg-white bg-gradient-to-br from-background to-muted p-3 outline outline-1 outline-gray-200 transition-all group-hover:outline-gray-300 hover:shadow-lg">
99+
<div className="size-12 rounded-lg bg-white bg-gradient-to-br from-background to-muted p-3 outline outline-1 outline-card-foreground/10 transition-all group-hover:outline-card-foreground/15 hover:shadow-lg">
99100
{base.icon ? <Emoji emoji={base.icon} size={24} /> : <Database className="size-6" />}
100101
</div>
101102
</EmojiPicker>
@@ -127,7 +128,7 @@ export const BaseCard: FC<IBaseCard> = (props) => {
127128
</div>
128129
<div className="absolute right-0 top-1 flex gap-2 px-1 md:opacity-0 md:group-hover:opacity-100">
129130
<StarButton
130-
className="size-6 rounded-full bg-gray-100/50 p-1 shadow backdrop-blur-sm transition-colors hover:bg-gray-200"
131+
className="size-6 rounded-full bg-gray-100/50 p-1 shadow backdrop-blur-sm transition-colors hover:bg-gray-200/80"
131132
id={base.id}
132133
type={PinType.Base}
133134
/>
@@ -143,7 +144,7 @@ export const BaseCard: FC<IBaseCard> = (props) => {
143144
<Button
144145
variant="ghost"
145146
size={'xs'}
146-
className="size-6 rounded-full bg-gray-100/50 p-1 shadow backdrop-blur-sm transition-colors hover:bg-gray-200"
147+
className="size-6 rounded-full bg-gray-100/50 p-1 shadow backdrop-blur-sm transition-colors hover:bg-gray-200/80"
147148
>
148149
<MoreHorizontal className="size-4" />
149150
</Button>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { useEffect, useState } from 'react';
2+
import { getEmojiColor } from '@/lib/emoji-color';
3+
4+
const colorCache: Record<string, string> = {};
5+
6+
export function ColorBg({ emoji }: { emoji?: string }) {
7+
const [color, setColor] = useState<string>('');
8+
9+
useEffect(() => {
10+
if (!emoji) return;
11+
12+
if (colorCache[emoji]) {
13+
setColor(colorCache[emoji]);
14+
return;
15+
}
16+
17+
try {
18+
const emojiColor = getEmojiColor(emoji);
19+
colorCache[emoji] = emojiColor;
20+
setColor(emojiColor);
21+
} catch (error) {
22+
console.error('Error calculating emoji color:', error);
23+
setColor('#0000001A');
24+
}
25+
}, [emoji]);
26+
27+
if (!emoji) {
28+
return (
29+
<div className="absolute inset-0 z-0 bg-gradient-to-r from-primary/10 via-primary/5 to-transparent opacity-0 transition-opacity duration-200 group-hover:opacity-100"></div>
30+
);
31+
}
32+
33+
return (
34+
<div
35+
className="absolute inset-0 z-0 opacity-0 transition-opacity duration-200 group-hover:opacity-100"
36+
style={{
37+
background: color
38+
? `linear-gradient(to right, ${color}1A, ${color}0A, transparent)`
39+
: undefined,
40+
}}
41+
></div>
42+
);
43+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
function getEmojiAverageColor(
2+
emoji: string,
3+
size: number = 64
4+
): { r: number; g: number; b: number } {
5+
const canvas = document.createElement('canvas');
6+
const context = canvas.getContext('2d');
7+
8+
if (!context) {
9+
throw new Error('No canvas context');
10+
}
11+
12+
canvas.width = size;
13+
canvas.height = size;
14+
15+
context.clearRect(0, 0, size, size);
16+
17+
context.font = `${Math.floor(size * 0.8)}px Arial`;
18+
context.textAlign = 'center';
19+
context.textBaseline = 'middle';
20+
21+
context.fillText(emoji, size / 2, size / 2);
22+
23+
const imageData = context.getImageData(0, 0, size, size);
24+
const pixels = imageData.data;
25+
26+
let totalR = 0;
27+
let totalG = 0;
28+
let totalB = 0;
29+
let count = 0;
30+
31+
for (let i = 0; i < pixels.length; i += 4) {
32+
const r = pixels[i];
33+
const g = pixels[i + 1];
34+
const b = pixels[i + 2];
35+
const a = pixels[i + 3];
36+
37+
if (a > 0) {
38+
totalR += r;
39+
totalG += g;
40+
totalB += b;
41+
count++;
42+
}
43+
}
44+
45+
if (count === 0) {
46+
return { r: 0, g: 0, b: 0 };
47+
}
48+
49+
const avgR = Math.round(totalR / count);
50+
const avgG = Math.round(totalG / count);
51+
const avgB = Math.round(totalB / count);
52+
53+
return { r: avgR, g: avgG, b: avgB };
54+
}
55+
56+
function rgbToHex(r: number, g: number, b: number): string {
57+
return `#${[r, g, b].map((x) => x.toString(16).padStart(2, '0')).join('')}`;
58+
}
59+
60+
export function getEmojiColor(emoji: string, size: number = 64): string {
61+
try {
62+
const { r, g, b } = getEmojiAverageColor(emoji, size);
63+
return rgbToHex(r, g, b);
64+
} catch (error) {
65+
return '#000000';
66+
}
67+
}

0 commit comments

Comments
 (0)