tangled
alpha
login
or
join now
margin.at
/
margin
86
fork
atom
Write on the margins of the internet. Powered by the AT Protocol.
margin.at
extension
web
atproto
comments
86
fork
atom
overview
issues
4
pulls
1
pipelines
emoji picker for collections
scanash.com
1 month ago
5345691c
35352b49
+332
-65
6 changed files
expand all
collapse all
unified
split
web
bun.lock
package.json
src
components
modals
AddToCollectionModal.tsx
EditCollectionModal.tsx
styles
global.css
views
collections
Collections.tsx
+5
web/bun.lock
···
13
13
"autoprefixer": "^10.4.24",
14
14
"clsx": "^2.1.1",
15
15
"date-fns": "^4.1.0",
16
16
+
"emoji-picker-react": "^4.18.0",
16
17
"lucide-react": "^0.563.0",
17
18
"nanostores": "^1.1.0",
18
19
"postcss": "^8.5.6",
···
593
594
594
595
"electron-to-chromium": ["electron-to-chromium@1.5.286", "", {}, "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A=="],
595
596
597
597
+
"emoji-picker-react": ["emoji-picker-react@4.18.0", "", { "dependencies": { "flairup": "1.0.0" }, "peerDependencies": { "react": ">=16" } }, "sha512-vLTrLfApXAIciguGE57pXPWs9lPLBspbEpPMiUq03TIli2dHZBiB+aZ0R9/Wat0xmTfcd4AuEzQgSYxEZ8C88Q=="],
598
598
+
596
599
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
597
600
598
601
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
···
680
683
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
681
684
682
685
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
686
686
+
687
687
+
"flairup": ["flairup@1.0.0", "", {}, "sha512-IKlE+pNvL2R+kVL1kEhUYqRxVqeFnjiIvHWDMLFXNaqyUdFXQM2wte44EfMYJNHkW16X991t2Zg8apKkhv7OBA=="],
683
688
684
689
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
685
690
+1
web/package.json
···
19
19
"autoprefixer": "^10.4.24",
20
20
"clsx": "^2.1.1",
21
21
"date-fns": "^4.1.0",
22
22
+
"emoji-picker-react": "^4.18.0",
22
23
"lucide-react": "^0.563.0",
23
24
"nanostores": "^1.1.0",
24
25
"postcss": "^8.5.6",
+86
-22
web/src/components/modals/AddToCollectionModal.tsx
···
9
9
} from "lucide-react";
10
10
import CollectionIcon from "../common/CollectionIcon";
11
11
import { ICON_MAP } from "../common/iconMap";
12
12
+
import EmojiPicker, { Theme } from "emoji-picker-react";
12
13
import { useStore } from "@nanostores/react";
13
14
import { $user } from "../../store/auth";
15
15
+
import { $theme } from "../../store/theme";
14
16
import {
15
17
getCollections,
16
18
addCollectionItem,
···
31
33
annotationUri,
32
34
}: AddToCollectionModalProps) {
33
35
const user = useStore($user);
36
36
+
const theme = useStore($theme);
34
37
const [collections, setCollections] = useState<Collection[]>([]);
35
38
const [loading, setLoading] = useState(true);
36
39
const [addingTo, setAddingTo] = useState<string | null>(null);
···
41
44
const [newName, setNewName] = useState("");
42
45
const [newDescription, setNewDescription] = useState("");
43
46
const [newIcon, setNewIcon] = useState("");
47
47
+
const [activeTab, setActiveTab] = useState<"icon" | "emoji">("icon");
44
48
const [creating, setCreating] = useState(false);
45
49
46
50
useEffect(() => {
···
96
100
if (!newName.trim()) return;
97
101
try {
98
102
setCreating(true);
99
99
-
const iconValue = newIcon ? `icon:${newIcon}` : undefined;
103
103
+
const iconValue = newIcon
104
104
+
? ICON_MAP[newIcon]
105
105
+
? `icon:${newIcon}`
106
106
+
: newIcon
107
107
+
: undefined;
100
108
const newCollection = await createCollection(
101
109
newName.trim(),
102
110
newDescription.trim() || undefined,
···
184
192
<label className="block text-sm font-medium text-surface-700 dark:text-surface-300 mb-2">
185
193
Icon
186
194
</label>
187
187
-
<div className="grid grid-cols-8 gap-1.5 max-h-32 overflow-y-auto p-2 bg-surface-50 dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700">
188
188
-
{Object.keys(ICON_MAP).map((iconName) => {
189
189
-
const isSelected = newIcon === iconName;
190
190
-
return (
191
191
-
<button
192
192
-
key={iconName}
193
193
-
type="button"
194
194
-
onClick={() => setNewIcon(isSelected ? "" : iconName)}
195
195
-
className={`w-8 h-8 flex items-center justify-center rounded-lg transition-all ${
196
196
-
isSelected
197
197
-
? "bg-primary-600 text-white"
198
198
-
: "hover:bg-surface-200 dark:hover:bg-surface-700 text-surface-600 dark:text-surface-400"
199
199
-
}`}
200
200
-
title={iconName}
201
201
-
>
202
202
-
<CollectionIcon icon={`icon:${iconName}`} size={16} />
203
203
-
</button>
204
204
-
);
205
205
-
})}
195
195
+
196
196
+
<div className="flex gap-2 mb-3 bg-surface-100 dark:bg-surface-800 p-1 rounded-xl">
197
197
+
<button
198
198
+
type="button"
199
199
+
onClick={() => setActiveTab("icon")}
200
200
+
className={`flex-1 py-1.5 text-sm font-medium rounded-lg transition-colors ${
201
201
+
activeTab === "icon"
202
202
+
? "bg-white dark:bg-surface-700 text-surface-900 dark:text-white shadow-sm"
203
203
+
: "text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-200"
204
204
+
}`}
205
205
+
>
206
206
+
Icons
207
207
+
</button>
208
208
+
<button
209
209
+
type="button"
210
210
+
onClick={() => setActiveTab("emoji")}
211
211
+
className={`flex-1 py-1.5 text-sm font-medium rounded-lg transition-colors ${
212
212
+
activeTab === "emoji"
213
213
+
? "bg-white dark:bg-surface-700 text-surface-900 dark:text-white shadow-sm"
214
214
+
: "text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-200"
215
215
+
}`}
216
216
+
>
217
217
+
Emojis
218
218
+
</button>
206
219
</div>
220
220
+
221
221
+
{activeTab === "icon" ? (
222
222
+
<div className="grid grid-cols-8 gap-1.5 max-h-60 overflow-y-auto p-2 bg-surface-50 dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 custom-scrollbar">
223
223
+
{Object.keys(ICON_MAP).map((iconName) => {
224
224
+
const isSelected = newIcon === iconName;
225
225
+
return (
226
226
+
<button
227
227
+
key={iconName}
228
228
+
type="button"
229
229
+
onClick={() => setNewIcon(isSelected ? "" : iconName)}
230
230
+
className={`w-8 h-8 flex items-center justify-center rounded-lg transition-all ${
231
231
+
isSelected
232
232
+
? "bg-primary-600 text-white"
233
233
+
: "hover:bg-surface-200 dark:hover:bg-surface-700 text-surface-600 dark:text-surface-400"
234
234
+
}`}
235
235
+
title={iconName}
236
236
+
>
237
237
+
<CollectionIcon icon={`icon:${iconName}`} size={16} />
238
238
+
</button>
239
239
+
);
240
240
+
})}
241
241
+
</div>
242
242
+
) : (
243
243
+
<div className="w-full bg-surface-50 dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 overflow-hidden">
244
244
+
<EmojiPicker
245
245
+
className="custom-emoji-picker"
246
246
+
onEmojiClick={(emojiData) => setNewIcon(emojiData.emoji)}
247
247
+
autoFocusSearch={false}
248
248
+
width="100%"
249
249
+
height={300}
250
250
+
previewConfig={{ showPreview: false }}
251
251
+
skinTonesDisabled
252
252
+
lazyLoadEmojis
253
253
+
theme={
254
254
+
theme === "dark" ||
255
255
+
(theme === "system" &&
256
256
+
window.matchMedia("(prefers-color-scheme: dark)")
257
257
+
.matches)
258
258
+
? (Theme.DARK as Theme)
259
259
+
: (Theme.LIGHT as Theme)
260
260
+
}
261
261
+
/>
262
262
+
</div>
263
263
+
)}
264
264
+
207
265
{newIcon && (
208
208
-
<p className="mt-1 text-xs text-surface-500">
209
209
-
Selected: {newIcon}
266
266
+
<p className="mt-2 text-sm text-surface-600 dark:text-surface-300 flex items-center gap-2">
267
267
+
Selected:
268
268
+
<span className="inline-flex items-center justify-center w-8 h-8 bg-surface-100 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700">
269
269
+
<CollectionIcon
270
270
+
icon={ICON_MAP[newIcon] ? `icon:${newIcon}` : newIcon}
271
271
+
size={18}
272
272
+
/>
273
273
+
</span>
210
274
</p>
211
275
)}
212
276
</div>
+97
-23
web/src/components/modals/EditCollectionModal.tsx
···
2
2
import { X, Loader2 } from "lucide-react";
3
3
import CollectionIcon from "../common/CollectionIcon";
4
4
import { ICON_MAP } from "../common/iconMap";
5
5
+
import EmojiPicker, { Theme } from "emoji-picker-react";
5
6
import { updateCollection, type Collection } from "../../api/client";
7
7
+
import { useStore } from "@nanostores/react";
8
8
+
import { $theme } from "../../store/theme";
6
9
7
10
interface EditCollectionModalProps {
8
11
isOpen: boolean;
···
19
22
}: EditCollectionModalProps) {
20
23
const [name, setName] = useState(collection.name);
21
24
const [description, setDescription] = useState(collection.description || "");
22
22
-
const [icon, setIcon] = useState(collection.icon?.replace("icon:", "") || "");
25
25
+
const initialIsIcon = collection.icon?.startsWith("icon:") ?? false;
26
26
+
const initialIconValue = collection.icon?.replace("icon:", "") || "";
27
27
+
28
28
+
const [activeTab, setActiveTab] = useState<"icon" | "emoji">(
29
29
+
initialIsIcon || !collection.icon ? "icon" : "emoji",
30
30
+
);
31
31
+
const [icon, setIcon] = useState(initialIconValue);
23
32
const [loading, setLoading] = useState(false);
24
33
const [error, setError] = useState<string | null>(null);
34
34
+
const theme = useStore($theme);
25
35
26
36
useEffect(() => {
27
37
if (isOpen) {
28
38
setName(collection.name);
29
39
setDescription(collection.description || "");
40
40
+
41
41
+
const isIcon = collection.icon?.startsWith("icon:") ?? false;
42
42
+
setActiveTab(isIcon || !collection.icon ? "icon" : "emoji");
30
43
setIcon(collection.icon?.replace("icon:", "") || "");
44
44
+
31
45
setError(null);
32
46
document.body.style.overflow = "hidden";
33
47
}
···
43
57
try {
44
58
setLoading(true);
45
59
setError(null);
46
46
-
const iconValue = icon ? `icon:${icon}` : undefined;
60
60
+
const iconValue = icon
61
61
+
? ICON_MAP[icon]
62
62
+
? `icon:${icon}`
63
63
+
: icon
64
64
+
: undefined;
47
65
const updated = await updateCollection(
48
66
collection.uri,
49
67
name.trim(),
···
121
139
<label className="block text-sm font-medium text-surface-700 dark:text-surface-300 mb-2">
122
140
Icon
123
141
</label>
124
124
-
<div className="grid grid-cols-8 gap-1.5 max-h-32 overflow-y-auto p-2 bg-surface-50 dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700">
125
125
-
{Object.keys(ICON_MAP).map((iconName) => {
126
126
-
const isSelected = icon === iconName;
127
127
-
return (
128
128
-
<button
129
129
-
key={iconName}
130
130
-
type="button"
131
131
-
onClick={() => setIcon(isSelected ? "" : iconName)}
132
132
-
className={`w-8 h-8 flex items-center justify-center rounded-lg transition-all ${
133
133
-
isSelected
134
134
-
? "bg-primary-600 text-white"
135
135
-
: "hover:bg-surface-200 dark:hover:bg-surface-700 text-surface-600 dark:text-surface-400"
136
136
-
}`}
137
137
-
title={iconName}
138
138
-
>
139
139
-
<CollectionIcon icon={`icon:${iconName}`} size={16} />
140
140
-
</button>
141
141
-
);
142
142
-
})}
142
142
+
143
143
+
<div className="flex gap-2 mb-3 bg-surface-100 dark:bg-surface-800 p-1 rounded-xl">
144
144
+
<button
145
145
+
type="button"
146
146
+
onClick={() => setActiveTab("icon")}
147
147
+
className={`flex-1 py-1.5 text-sm font-medium rounded-lg transition-colors ${
148
148
+
activeTab === "icon"
149
149
+
? "bg-white dark:bg-surface-700 text-surface-900 dark:text-white shadow-sm"
150
150
+
: "text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-200"
151
151
+
}`}
152
152
+
>
153
153
+
Icons
154
154
+
</button>
155
155
+
<button
156
156
+
type="button"
157
157
+
onClick={() => setActiveTab("emoji")}
158
158
+
className={`flex-1 py-1.5 text-sm font-medium rounded-lg transition-colors ${
159
159
+
activeTab === "emoji"
160
160
+
? "bg-white dark:bg-surface-700 text-surface-900 dark:text-white shadow-sm"
161
161
+
: "text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-200"
162
162
+
}`}
163
163
+
>
164
164
+
Emojis
165
165
+
</button>
143
166
</div>
167
167
+
168
168
+
{activeTab === "icon" ? (
169
169
+
<div className="grid grid-cols-8 gap-1.5 max-h-60 overflow-y-auto p-2 bg-surface-50 dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 custom-scrollbar">
170
170
+
{Object.keys(ICON_MAP).map((iconName) => {
171
171
+
const isSelected = icon === iconName;
172
172
+
return (
173
173
+
<button
174
174
+
key={iconName}
175
175
+
type="button"
176
176
+
onClick={() => setIcon(isSelected ? "" : iconName)}
177
177
+
className={`w-8 h-8 flex items-center justify-center rounded-lg transition-all ${
178
178
+
isSelected
179
179
+
? "bg-primary-600 text-white"
180
180
+
: "hover:bg-surface-200 dark:hover:bg-surface-700 text-surface-600 dark:text-surface-400"
181
181
+
}`}
182
182
+
title={iconName}
183
183
+
>
184
184
+
<CollectionIcon icon={`icon:${iconName}`} size={16} />
185
185
+
</button>
186
186
+
);
187
187
+
})}
188
188
+
</div>
189
189
+
) : (
190
190
+
<div className="w-full bg-surface-50 dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 overflow-hidden">
191
191
+
<EmojiPicker
192
192
+
className="custom-emoji-picker"
193
193
+
onEmojiClick={(emojiData) => setIcon(emojiData.emoji)}
194
194
+
autoFocusSearch={false}
195
195
+
width="100%"
196
196
+
height={300}
197
197
+
previewConfig={{ showPreview: false }}
198
198
+
skinTonesDisabled
199
199
+
lazyLoadEmojis
200
200
+
theme={
201
201
+
theme === "dark" ||
202
202
+
(theme === "system" &&
203
203
+
window.matchMedia("(prefers-color-scheme: dark)")
204
204
+
.matches)
205
205
+
? (Theme.DARK as Theme)
206
206
+
: (Theme.LIGHT as Theme)
207
207
+
}
208
208
+
/>
209
209
+
</div>
210
210
+
)}
211
211
+
144
212
{icon && (
145
145
-
<p className="mt-1 text-xs text-surface-500">
146
146
-
Selected: {icon}
213
213
+
<p className="mt-2 text-sm text-surface-600 dark:text-surface-300 flex items-center gap-2">
214
214
+
Selected:
215
215
+
<span className="inline-flex items-center justify-center w-8 h-8 bg-surface-100 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700">
216
216
+
<CollectionIcon
217
217
+
icon={ICON_MAP[icon] ? `icon:${icon}` : icon}
218
218
+
size={18}
219
219
+
/>
220
220
+
</span>
147
221
</p>
148
222
)}
149
223
</div>
+63
web/src/styles/global.css
···
71
71
.custom-scrollbar::-webkit-scrollbar-thumb {
72
72
@apply bg-surface-300 dark:bg-surface-700 rounded-full hover:bg-surface-400 dark:hover:bg-surface-600 transition-colors;
73
73
}
74
74
+
75
75
+
.custom-emoji-picker {
76
76
+
--epr-bg-color: transparent !important;
77
77
+
--epr-category-label-bg-color: transparent !important;
78
78
+
--epr-text-color: theme("colors.surface.900") !important;
79
79
+
--epr-picker-border-color: theme("colors.surface.200") !important;
80
80
+
81
81
+
--epr-search-input-bg-color: theme("colors.surface.50") !important;
82
82
+
--epr-search-input-text-color: theme("colors.surface.900") !important;
83
83
+
84
84
+
--epr-hover-bg-color: theme("colors.surface.200") !important;
85
85
+
--epr-focus-bg-color: theme("colors.primary.100") !important;
86
86
+
--epr-highlight-color: theme("colors.primary.500") !important;
87
87
+
88
88
+
font-family: inherit !important;
89
89
+
border: none !important;
90
90
+
}
91
91
+
92
92
+
[data-theme="dark"] .custom-emoji-picker {
93
93
+
--epr-text-color: theme("colors.white") !important;
94
94
+
--epr-picker-border-color: theme("colors.surface.700") !important;
95
95
+
96
96
+
--epr-search-input-bg-color: theme("colors.surface.800") !important;
97
97
+
--epr-search-input-text-color: theme("colors.white") !important;
98
98
+
99
99
+
--epr-hover-bg-color: theme("colors.surface.700") !important;
100
100
+
--epr-focus-bg-color: theme("colors.primary.900") !important;
101
101
+
}
102
102
+
103
103
+
.custom-emoji-picker .epr-search-container input {
104
104
+
border-radius: 0.5rem !important;
105
105
+
border: 1px solid theme("colors.surface.200") !important;
106
106
+
background-color: theme("colors.surface.50") !important;
107
107
+
}
108
108
+
109
109
+
[data-theme="dark"] .custom-emoji-picker .epr-search-container input {
110
110
+
border: 1px solid theme("colors.surface.700") !important;
111
111
+
background-color: theme("colors.surface.800") !important;
112
112
+
}
113
113
+
114
114
+
.custom-emoji-picker .epr-header-overlay {
115
115
+
padding: 0 !important;
116
116
+
}
117
117
+
118
118
+
.custom-emoji-picker .epr-emoji-category-label {
119
119
+
position: static !important;
120
120
+
background-color: transparent !important;
121
121
+
height: 32px !important;
122
122
+
line-height: 32px !important;
123
123
+
}
124
124
+
125
125
+
.custom-emoji-picker .epr-body::-webkit-scrollbar {
126
126
+
width: 6px;
127
127
+
height: 6px;
128
128
+
}
129
129
+
130
130
+
.custom-emoji-picker .epr-body::-webkit-scrollbar-track {
131
131
+
@apply bg-transparent;
132
132
+
}
133
133
+
134
134
+
.custom-emoji-picker .epr-body::-webkit-scrollbar-thumb {
135
135
+
@apply bg-surface-300 dark:bg-surface-700 rounded-full hover:bg-surface-400 dark:hover:bg-surface-600 transition-colors;
136
136
+
}
74
137
}
75
138
76
139
@keyframes fadeIn {
+80
-20
web/src/views/collections/Collections.tsx
···
9
9
import { ICON_MAP } from "../../components/common/iconMap";
10
10
import { useStore } from "@nanostores/react";
11
11
import { $user } from "../../store/auth";
12
12
+
import EmojiPicker, { Theme } from "emoji-picker-react";
13
13
+
import { $theme } from "../../store/theme";
12
14
import type { Collection } from "../../types";
13
15
import { formatDistanceToNow } from "date-fns";
14
16
import { clsx } from "clsx";
···
16
18
17
19
export default function Collections() {
18
20
const user = useStore($user);
21
21
+
const theme = useStore($theme);
19
22
const [collections, setCollections] = useState<Collection[]>([]);
20
23
const [loading, setLoading] = useState(true);
21
24
const [showCreateModal, setShowCreateModal] = useState(false);
22
25
const [newItemName, setNewItemName] = useState("");
23
26
const [newItemDesc, setNewItemDesc] = useState("");
24
27
const [newItemIcon, setNewItemIcon] = useState("folder");
28
28
+
const [activeTab, setActiveTab] = useState<"icon" | "emoji">("icon");
25
29
const [creating, setCreating] = useState(false);
26
30
27
31
const fetchCollections = async () => {
···
45
49
if (!newItemName.trim()) return;
46
50
47
51
setCreating(true);
48
48
-
const res = await createCollection(newItemName, newItemDesc);
52
52
+
const finalIcon = ICON_MAP[newItemIcon]
53
53
+
? `icon:${newItemIcon}`
54
54
+
: newItemIcon;
55
55
+
56
56
+
const res = await createCollection(newItemName, newItemDesc, finalIcon);
49
57
if (res) {
50
58
setCollections([res, ...collections]);
51
59
setShowCreateModal(false);
52
60
setNewItemName("");
53
61
setNewItemDesc("");
54
62
setNewItemIcon("folder");
63
63
+
setActiveTab("icon");
55
64
fetchCollections();
56
65
}
57
66
setCreating(false);
···
187
196
<label className="block text-sm font-medium text-surface-700 dark:text-surface-300 mb-2">
188
197
Icon
189
198
</label>
190
190
-
<div className="grid grid-cols-7 gap-1.5 p-3 bg-surface-50 dark:bg-surface-800 border border-surface-200 dark:border-surface-700 rounded-xl max-h-28 overflow-y-auto">
191
191
-
{Object.keys(ICON_MAP).map((key) => {
192
192
-
const Icon = ICON_MAP[key];
193
193
-
return (
194
194
-
<button
195
195
-
key={key}
196
196
-
type="button"
197
197
-
onClick={() => setNewItemIcon(key)}
198
198
-
className={clsx(
199
199
-
"p-2 rounded-lg flex items-center justify-center transition-all",
200
200
-
newItemIcon === key
201
201
-
? "bg-primary-100 dark:bg-primary-900/50 text-primary-600 dark:text-primary-400 ring-2 ring-primary-500"
202
202
-
: "hover:bg-surface-100 dark:hover:bg-surface-700 text-surface-500 dark:text-surface-400",
203
203
-
)}
204
204
-
>
205
205
-
<Icon size={18} />
206
206
-
</button>
207
207
-
);
208
208
-
})}
199
199
+
200
200
+
<div className="flex gap-2 mb-3 bg-surface-100 dark:bg-surface-800 p-1 rounded-xl">
201
201
+
<button
202
202
+
type="button"
203
203
+
onClick={() => setActiveTab("icon")}
204
204
+
className={`flex-1 py-1.5 text-sm font-medium rounded-lg transition-colors ${
205
205
+
activeTab === "icon"
206
206
+
? "bg-white dark:bg-surface-700 text-surface-900 dark:text-white shadow-sm"
207
207
+
: "text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-200"
208
208
+
}`}
209
209
+
>
210
210
+
Icons
211
211
+
</button>
212
212
+
<button
213
213
+
type="button"
214
214
+
onClick={() => setActiveTab("emoji")}
215
215
+
className={`flex-1 py-1.5 text-sm font-medium rounded-lg transition-colors ${
216
216
+
activeTab === "emoji"
217
217
+
? "bg-white dark:bg-surface-700 text-surface-900 dark:text-white shadow-sm"
218
218
+
: "text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-200"
219
219
+
}`}
220
220
+
>
221
221
+
Emojis
222
222
+
</button>
209
223
</div>
224
224
+
225
225
+
{activeTab === "icon" ? (
226
226
+
<div className="grid grid-cols-7 gap-1.5 p-3 bg-surface-50 dark:bg-surface-800 border border-surface-200 dark:border-surface-700 rounded-xl max-h-48 overflow-y-auto custom-scrollbar">
227
227
+
{Object.keys(ICON_MAP).map((key) => {
228
228
+
const Icon = ICON_MAP[key];
229
229
+
return (
230
230
+
<button
231
231
+
key={key}
232
232
+
type="button"
233
233
+
onClick={() => setNewItemIcon(key)}
234
234
+
className={clsx(
235
235
+
"p-2 rounded-lg flex items-center justify-center transition-all",
236
236
+
newItemIcon === key
237
237
+
? "bg-primary-100 dark:bg-primary-900/50 text-primary-600 dark:text-primary-400 ring-2 ring-primary-500"
238
238
+
: "hover:bg-surface-100 dark:hover:bg-surface-700 text-surface-500 dark:text-surface-400",
239
239
+
)}
240
240
+
>
241
241
+
<Icon size={18} />
242
242
+
</button>
243
243
+
);
244
244
+
})}
245
245
+
</div>
246
246
+
) : (
247
247
+
<div className="w-full bg-surface-50 dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 overflow-hidden">
248
248
+
<EmojiPicker
249
249
+
className="custom-emoji-picker"
250
250
+
onEmojiClick={(emojiData) =>
251
251
+
setNewItemIcon(emojiData.emoji)
252
252
+
}
253
253
+
autoFocusSearch={false}
254
254
+
width="100%"
255
255
+
height={300}
256
256
+
previewConfig={{ showPreview: false }}
257
257
+
skinTonesDisabled
258
258
+
lazyLoadEmojis
259
259
+
theme={
260
260
+
theme === "dark" ||
261
261
+
(theme === "system" &&
262
262
+
window.matchMedia("(prefers-color-scheme: dark)")
263
263
+
.matches)
264
264
+
? (Theme.DARK as Theme)
265
265
+
: (Theme.LIGHT as Theme)
266
266
+
}
267
267
+
/>
268
268
+
</div>
269
269
+
)}
210
270
</div>
211
271
<div className="mb-6">
212
272
<label className="block text-sm font-medium text-surface-700 dark:text-surface-300 mb-2">