@ermis-network/ermis-chat-react 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/index.cjs +752 -333
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +382 -0
- package/dist/index.css.map +1 -1
- package/dist/index.d.mts +88 -1
- package/dist/index.d.ts +88 -1
- package/dist/index.mjs +691 -274
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/ChannelInfo/AddMemberModal.tsx +48 -174
- package/src/components/ChannelList.tsx +5 -0
- package/src/components/CreateChannelModal.tsx +274 -0
- package/src/components/UserPicker.tsx +377 -0
- package/src/index.ts +11 -0
- package/src/styles/_create-channel-modal.css +183 -0
- package/src/styles/_user-picker.css +268 -0
- package/src/styles/index.css +3 -0
- package/src/types.ts +100 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import React, { useState, useEffect, useMemo, useCallback, useRef, useTransition } from 'react';
|
|
2
|
+
import { useChatClient } from '../hooks/useChatClient';
|
|
3
|
+
import { Avatar } from './Avatar';
|
|
4
|
+
import { VList, type VListHandle } from 'virtua';
|
|
5
|
+
import type {
|
|
6
|
+
UserPickerProps,
|
|
7
|
+
UserPickerItemProps,
|
|
8
|
+
UserPickerSelectedBoxProps,
|
|
9
|
+
UserPickerUser,
|
|
10
|
+
} from '../types';
|
|
11
|
+
|
|
12
|
+
/* ---------- Constants ---------- */
|
|
13
|
+
const DEFAULT_PAGE_SIZE = 30;
|
|
14
|
+
const SEARCH_DEBOUNCE_MS = 500;
|
|
15
|
+
|
|
16
|
+
/* ---------- Static styles ---------- */
|
|
17
|
+
const LIST_STYLE: React.CSSProperties = { height: '100%' };
|
|
18
|
+
|
|
19
|
+
/* ==========================================================
|
|
20
|
+
Default Sub-Components
|
|
21
|
+
========================================================== */
|
|
22
|
+
|
|
23
|
+
/** Check icon for selected state */
|
|
24
|
+
const CheckIcon: React.FC = () => (
|
|
25
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
|
|
26
|
+
<polyline points="20 6 9 17 4 12" />
|
|
27
|
+
</svg>
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
/** Default user row */
|
|
31
|
+
const DefaultUserItem: React.FC<UserPickerItemProps> = React.memo(({
|
|
32
|
+
user, selected, disabled, mode, onToggle, AvatarComponent,
|
|
33
|
+
}) => {
|
|
34
|
+
const handleClick = useCallback(() => {
|
|
35
|
+
if (!disabled) onToggle(user);
|
|
36
|
+
}, [disabled, onToggle, user]);
|
|
37
|
+
|
|
38
|
+
const inputClass = [
|
|
39
|
+
'ermis-user-picker__input',
|
|
40
|
+
mode === 'radio' ? 'ermis-user-picker__input--radio' : 'ermis-user-picker__input--checkbox',
|
|
41
|
+
selected ? 'ermis-user-picker__input--checked' : '',
|
|
42
|
+
].join(' ');
|
|
43
|
+
|
|
44
|
+
const itemClass = [
|
|
45
|
+
'ermis-user-picker__item',
|
|
46
|
+
selected ? 'ermis-user-picker__item--selected' : '',
|
|
47
|
+
disabled ? 'ermis-user-picker__item--disabled' : '',
|
|
48
|
+
].join(' ');
|
|
49
|
+
|
|
50
|
+
const detail = user.email || user.phone || '';
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className={itemClass} onClick={handleClick} role="option" aria-selected={selected}>
|
|
54
|
+
<div className={inputClass}>
|
|
55
|
+
{selected && <CheckIcon />}
|
|
56
|
+
</div>
|
|
57
|
+
<AvatarComponent image={user.avatar} name={user.name || user.id} size={36} />
|
|
58
|
+
<div className="ermis-user-picker__info">
|
|
59
|
+
<span className="ermis-user-picker__name">{user.name || user.id}</span>
|
|
60
|
+
{detail && <span className="ermis-user-picker__detail">{detail}</span>}
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
DefaultUserItem.displayName = 'DefaultUserItem';
|
|
66
|
+
|
|
67
|
+
/** Default search input */
|
|
68
|
+
const DefaultSearchInput: React.FC<{ value: string; onChange: (e: React.ChangeEvent<HTMLInputElement>) => void; placeholder: string }> = ({ value, onChange, placeholder }) => (
|
|
69
|
+
<div className="ermis-user-picker__search">
|
|
70
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
71
|
+
<circle cx="11" cy="11" r="8" />
|
|
72
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
73
|
+
</svg>
|
|
74
|
+
<input
|
|
75
|
+
type="text"
|
|
76
|
+
placeholder={placeholder}
|
|
77
|
+
value={value}
|
|
78
|
+
onChange={onChange}
|
|
79
|
+
autoFocus
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
/** Default selected users chip box */
|
|
85
|
+
const DefaultSelectedBox: React.FC<UserPickerSelectedBoxProps> = React.memo(({
|
|
86
|
+
users, onRemove, AvatarComponent, emptyLabel,
|
|
87
|
+
}) => (
|
|
88
|
+
<div className="ermis-user-picker__selected-box">
|
|
89
|
+
{users.length === 0 && emptyLabel && (
|
|
90
|
+
<span className="ermis-user-picker__selected-empty">{emptyLabel}</span>
|
|
91
|
+
)}
|
|
92
|
+
{users.map(u => (
|
|
93
|
+
<div key={u.id} className="ermis-user-picker__chip">
|
|
94
|
+
<AvatarComponent image={u.avatar} name={u.name || u.id} size={20} />
|
|
95
|
+
<span className="ermis-user-picker__chip-name">{u.name || u.id}</span>
|
|
96
|
+
<button
|
|
97
|
+
className="ermis-user-picker__chip-remove"
|
|
98
|
+
onClick={() => onRemove(u.id)}
|
|
99
|
+
aria-label={`Remove ${u.name || u.id}`}
|
|
100
|
+
>
|
|
101
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
|
|
102
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
103
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
104
|
+
</svg>
|
|
105
|
+
</button>
|
|
106
|
+
</div>
|
|
107
|
+
))}
|
|
108
|
+
</div>
|
|
109
|
+
));
|
|
110
|
+
DefaultSelectedBox.displayName = 'DefaultSelectedBox';
|
|
111
|
+
|
|
112
|
+
/* ==========================================================
|
|
113
|
+
UserPicker Component
|
|
114
|
+
========================================================== */
|
|
115
|
+
|
|
116
|
+
export const UserPicker: React.FC<UserPickerProps> = ({
|
|
117
|
+
mode,
|
|
118
|
+
onSelectionChange,
|
|
119
|
+
excludeUserIds,
|
|
120
|
+
initialSelectedUsers,
|
|
121
|
+
pageSize = DEFAULT_PAGE_SIZE,
|
|
122
|
+
AvatarComponent = Avatar,
|
|
123
|
+
UserItemComponent,
|
|
124
|
+
SelectedBoxComponent,
|
|
125
|
+
SearchInputComponent,
|
|
126
|
+
searchPlaceholder = 'Search by name, email or phone...',
|
|
127
|
+
loadingText = 'Loading users...',
|
|
128
|
+
emptyText = 'No users found.',
|
|
129
|
+
loadingMoreText = 'Loading more...',
|
|
130
|
+
selectedEmptyLabel,
|
|
131
|
+
}) => {
|
|
132
|
+
const { client } = useChatClient();
|
|
133
|
+
const currentUserId = client?.userID;
|
|
134
|
+
|
|
135
|
+
/* ---------- State ---------- */
|
|
136
|
+
const [allUsers, setAllUsers] = useState<UserPickerUser[]>([]);
|
|
137
|
+
const [page, setPage] = useState(1);
|
|
138
|
+
const [hasMore, setHasMore] = useState(true);
|
|
139
|
+
const [loading, setLoading] = useState(true);
|
|
140
|
+
const [loadingMore, setLoadingMore] = useState(false);
|
|
141
|
+
|
|
142
|
+
const [remoteUsers, setRemoteUsers] = useState<UserPickerUser[]>([]);
|
|
143
|
+
const [isSearching, setIsSearching] = useState(false);
|
|
144
|
+
|
|
145
|
+
const [searchInput, setSearchInput] = useState('');
|
|
146
|
+
const [search, setSearch] = useState('');
|
|
147
|
+
const [isPendingFilter, startTransition] = useTransition();
|
|
148
|
+
|
|
149
|
+
const [selectedMap, setSelectedMap] = useState<Map<string, UserPickerUser>>(() => {
|
|
150
|
+
const map = new Map<string, UserPickerUser>();
|
|
151
|
+
initialSelectedUsers?.forEach(u => map.set(u.id, u));
|
|
152
|
+
return map;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const vlistRef = useRef<VListHandle>(null);
|
|
156
|
+
|
|
157
|
+
/* ---------- Resolved sub-components ---------- */
|
|
158
|
+
const UserRow = UserItemComponent || DefaultUserItem;
|
|
159
|
+
const SearchInput = SearchInputComponent || DefaultSearchInput;
|
|
160
|
+
const SelectedBox = SelectedBoxComponent || DefaultSelectedBox;
|
|
161
|
+
|
|
162
|
+
/* ---------- Excluded IDs set ---------- */
|
|
163
|
+
const excludeSet = useMemo(() => new Set(excludeUserIds || []), [excludeUserIds]);
|
|
164
|
+
|
|
165
|
+
/* ---------- Search handler ---------- */
|
|
166
|
+
const handleSearchChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
167
|
+
const val = e.target.value;
|
|
168
|
+
setSearchInput(val);
|
|
169
|
+
startTransition(() => {
|
|
170
|
+
setSearch(val);
|
|
171
|
+
});
|
|
172
|
+
}, [startTransition]);
|
|
173
|
+
|
|
174
|
+
/* ---------- 1. Fetch initial page ---------- */
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
let active = true;
|
|
177
|
+
const fetchUsers = async () => {
|
|
178
|
+
if (!client) return;
|
|
179
|
+
try {
|
|
180
|
+
setLoading(true);
|
|
181
|
+
const response = await client.queryUsers(String(pageSize), 1);
|
|
182
|
+
if (active && response.data) {
|
|
183
|
+
setAllUsers(response.data);
|
|
184
|
+
setHasMore(response.data.length >= pageSize);
|
|
185
|
+
setPage(1);
|
|
186
|
+
}
|
|
187
|
+
} catch (err) {
|
|
188
|
+
console.error('[UserPicker] Error fetching users:', err);
|
|
189
|
+
} finally {
|
|
190
|
+
if (active) setLoading(false);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
fetchUsers();
|
|
194
|
+
return () => { active = false; };
|
|
195
|
+
}, [client, pageSize]);
|
|
196
|
+
|
|
197
|
+
/* ---------- 2. Load more (infinite scroll) ---------- */
|
|
198
|
+
const loadMore = useCallback(async () => {
|
|
199
|
+
if (!client || loadingMore || !hasMore || search.trim()) return;
|
|
200
|
+
const nextPage = page + 1;
|
|
201
|
+
setLoadingMore(true);
|
|
202
|
+
try {
|
|
203
|
+
const response = await client.queryUsers(String(pageSize), nextPage);
|
|
204
|
+
if (response.data) {
|
|
205
|
+
setAllUsers(prev => {
|
|
206
|
+
const existingIds = new Set(prev.map(u => u.id));
|
|
207
|
+
const newUsers = response.data.filter((u: UserPickerUser) => !existingIds.has(u.id));
|
|
208
|
+
return [...prev, ...newUsers];
|
|
209
|
+
});
|
|
210
|
+
setHasMore(response.data.length >= pageSize);
|
|
211
|
+
setPage(nextPage);
|
|
212
|
+
}
|
|
213
|
+
} catch (err) {
|
|
214
|
+
console.error('[UserPicker] Error loading more users:', err);
|
|
215
|
+
} finally {
|
|
216
|
+
setLoadingMore(false);
|
|
217
|
+
}
|
|
218
|
+
}, [client, loadingMore, hasMore, page, pageSize, search]);
|
|
219
|
+
|
|
220
|
+
/* ---------- 3. Local filter ---------- */
|
|
221
|
+
const localFilteredUsers = useMemo(() => {
|
|
222
|
+
const term = search.toLowerCase().trim();
|
|
223
|
+
if (!term) return allUsers;
|
|
224
|
+
return allUsers.filter(u => {
|
|
225
|
+
const name = (u.name || '').toLowerCase();
|
|
226
|
+
const email = (u.email || '').toLowerCase();
|
|
227
|
+
const phone = (u.phone || '').toLowerCase();
|
|
228
|
+
return name.includes(term) || email.includes(term) || phone.includes(term);
|
|
229
|
+
});
|
|
230
|
+
}, [search, allUsers]);
|
|
231
|
+
|
|
232
|
+
/* ---------- 4. Remote search fallback ---------- */
|
|
233
|
+
useEffect(() => {
|
|
234
|
+
if (!search.trim() || localFilteredUsers.length > 0) {
|
|
235
|
+
setRemoteUsers([]);
|
|
236
|
+
setIsSearching(false);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
let cancelled = false;
|
|
241
|
+
const timer = setTimeout(async () => {
|
|
242
|
+
setIsSearching(true);
|
|
243
|
+
try {
|
|
244
|
+
const response = await client.searchUsers(1, 25, search.trim());
|
|
245
|
+
if (!cancelled && response.data) {
|
|
246
|
+
setRemoteUsers(response.data);
|
|
247
|
+
}
|
|
248
|
+
} catch (err) {
|
|
249
|
+
console.error('[UserPicker] Error searching remote users:', err);
|
|
250
|
+
} finally {
|
|
251
|
+
if (!cancelled) setIsSearching(false);
|
|
252
|
+
}
|
|
253
|
+
}, SEARCH_DEBOUNCE_MS);
|
|
254
|
+
|
|
255
|
+
return () => {
|
|
256
|
+
cancelled = true;
|
|
257
|
+
clearTimeout(timer);
|
|
258
|
+
};
|
|
259
|
+
}, [search, localFilteredUsers.length, client, excludeSet]);
|
|
260
|
+
|
|
261
|
+
/* ---------- 5. Derived display list ---------- */
|
|
262
|
+
const usersToDisplay = (search.trim() && localFilteredUsers.length === 0)
|
|
263
|
+
? remoteUsers
|
|
264
|
+
: localFilteredUsers;
|
|
265
|
+
const isListLoading = loading || isSearching || isPendingFilter;
|
|
266
|
+
|
|
267
|
+
/* ---------- 6. Selection handlers ---------- */
|
|
268
|
+
const handleToggle = useCallback((user: UserPickerUser) => {
|
|
269
|
+
// Don't allow toggling disabled users (current user or excluded)
|
|
270
|
+
if (user.id === currentUserId || excludeSet.has(user.id)) return;
|
|
271
|
+
|
|
272
|
+
setSelectedMap(prev => {
|
|
273
|
+
const next = new Map(prev);
|
|
274
|
+
if (mode === 'radio') {
|
|
275
|
+
// Radio: clear all, set this one (or deselect if same)
|
|
276
|
+
if (next.has(user.id)) {
|
|
277
|
+
next.clear();
|
|
278
|
+
} else {
|
|
279
|
+
next.clear();
|
|
280
|
+
next.set(user.id, user);
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
// Checkbox: toggle
|
|
284
|
+
if (next.has(user.id)) {
|
|
285
|
+
next.delete(user.id);
|
|
286
|
+
} else {
|
|
287
|
+
next.set(user.id, user);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return next;
|
|
291
|
+
});
|
|
292
|
+
}, [mode, currentUserId, excludeSet]);
|
|
293
|
+
|
|
294
|
+
// Notify parent of selection changes
|
|
295
|
+
useEffect(() => {
|
|
296
|
+
onSelectionChange?.(Array.from(selectedMap.values()));
|
|
297
|
+
}, [selectedMap, onSelectionChange]);
|
|
298
|
+
|
|
299
|
+
const handleRemoveSelected = useCallback((userId: string) => {
|
|
300
|
+
setSelectedMap(prev => {
|
|
301
|
+
const next = new Map(prev);
|
|
302
|
+
next.delete(userId);
|
|
303
|
+
return next;
|
|
304
|
+
});
|
|
305
|
+
}, []);
|
|
306
|
+
|
|
307
|
+
/* ---------- 7. Scroll handler for infinite scroll ---------- */
|
|
308
|
+
const handleScroll = useCallback((offset: number) => {
|
|
309
|
+
// VList provides scroll offset. We detect near-bottom using the ref
|
|
310
|
+
const el = vlistRef.current;
|
|
311
|
+
if (!el) return;
|
|
312
|
+
// scrollSize = total scroll height, viewportSize = visible height
|
|
313
|
+
const scrollSize = (el as any).scrollSize ?? 0;
|
|
314
|
+
const viewportSize = (el as any).viewportSize ?? 0;
|
|
315
|
+
if (scrollSize > 0 && offset + viewportSize >= scrollSize - 50) {
|
|
316
|
+
loadMore();
|
|
317
|
+
}
|
|
318
|
+
}, [loadMore]);
|
|
319
|
+
|
|
320
|
+
/* ---------- Render ---------- */
|
|
321
|
+
const selectedArr = useMemo(() => Array.from(selectedMap.values()), [selectedMap]);
|
|
322
|
+
|
|
323
|
+
return (
|
|
324
|
+
<div className="ermis-user-picker" role="listbox" aria-multiselectable={mode === 'checkbox'}>
|
|
325
|
+
{/* Selected Users Box (checkbox mode only) */}
|
|
326
|
+
{mode === 'checkbox' && (
|
|
327
|
+
<SelectedBox
|
|
328
|
+
users={selectedArr}
|
|
329
|
+
onRemove={handleRemoveSelected}
|
|
330
|
+
AvatarComponent={AvatarComponent}
|
|
331
|
+
emptyLabel={selectedEmptyLabel}
|
|
332
|
+
/>
|
|
333
|
+
)}
|
|
334
|
+
|
|
335
|
+
{/* Search Input */}
|
|
336
|
+
<SearchInput
|
|
337
|
+
value={searchInput}
|
|
338
|
+
onChange={handleSearchChange}
|
|
339
|
+
placeholder={searchPlaceholder}
|
|
340
|
+
/>
|
|
341
|
+
|
|
342
|
+
{/* User List */}
|
|
343
|
+
<div className="ermis-user-picker__list">
|
|
344
|
+
{isListLoading ? (
|
|
345
|
+
<div className="ermis-user-picker__loading">
|
|
346
|
+
<span className="ermis-user-picker__spinner" />
|
|
347
|
+
{loadingText}
|
|
348
|
+
</div>
|
|
349
|
+
) : usersToDisplay.length === 0 ? (
|
|
350
|
+
<div className="ermis-user-picker__empty">{emptyText}</div>
|
|
351
|
+
) : (
|
|
352
|
+
<VList ref={vlistRef} style={LIST_STYLE} onScroll={handleScroll}>
|
|
353
|
+
{usersToDisplay.map(user => (
|
|
354
|
+
<UserRow
|
|
355
|
+
key={user.id}
|
|
356
|
+
user={user}
|
|
357
|
+
selected={selectedMap.has(user.id)}
|
|
358
|
+
disabled={user.id === currentUserId || excludeSet.has(user.id)}
|
|
359
|
+
mode={mode}
|
|
360
|
+
onToggle={handleToggle}
|
|
361
|
+
AvatarComponent={AvatarComponent}
|
|
362
|
+
/>
|
|
363
|
+
))}
|
|
364
|
+
{loadingMore && (
|
|
365
|
+
<div className="ermis-user-picker__load-more">
|
|
366
|
+
<span className="ermis-user-picker__spinner" />
|
|
367
|
+
{loadingMoreText}
|
|
368
|
+
</div>
|
|
369
|
+
)}
|
|
370
|
+
</VList>
|
|
371
|
+
)}
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
);
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
UserPicker.displayName = 'UserPicker';
|
package/src/index.ts
CHANGED
|
@@ -127,3 +127,14 @@ export type {
|
|
|
127
127
|
AddMemberUserItemProps,
|
|
128
128
|
AddMemberButtonProps,
|
|
129
129
|
} from './types';
|
|
130
|
+
|
|
131
|
+
export { UserPicker } from './components/UserPicker';
|
|
132
|
+
export type {
|
|
133
|
+
UserPickerProps,
|
|
134
|
+
UserPickerUser,
|
|
135
|
+
UserPickerItemProps,
|
|
136
|
+
UserPickerSelectedBoxProps,
|
|
137
|
+
} from './types';
|
|
138
|
+
|
|
139
|
+
export { CreateChannelModal } from './components/CreateChannelModal';
|
|
140
|
+
export type { CreateChannelModalProps } from './types';
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/* ============================================================
|
|
2
|
+
Create Channel Modal
|
|
3
|
+
BEM: .ermis-create-channel__*
|
|
4
|
+
============================================================ */
|
|
5
|
+
|
|
6
|
+
.ermis-create-channel__body {
|
|
7
|
+
display: flex;
|
|
8
|
+
flex-direction: column;
|
|
9
|
+
gap: var(--ermis-spacing-md, 0.75rem);
|
|
10
|
+
padding: var(--ermis-spacing-md, 0.75rem);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.ermis-create-channel__tabs {
|
|
14
|
+
display: flex;
|
|
15
|
+
gap: 8px;
|
|
16
|
+
background-color: var(--ermis-bg-secondary, #111118);
|
|
17
|
+
padding: 4px;
|
|
18
|
+
border-radius: var(--ermis-radius-md, 0.5rem);
|
|
19
|
+
margin-bottom: var(--ermis-spacing-sm, 0.5rem);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.ermis-create-channel__tab {
|
|
23
|
+
flex: 1;
|
|
24
|
+
padding: 8px 12px;
|
|
25
|
+
border-radius: var(--ermis-radius-sm, 0.375rem);
|
|
26
|
+
text-align: center;
|
|
27
|
+
font-size: var(--ermis-font-size-sm, 0.875rem);
|
|
28
|
+
font-weight: 500;
|
|
29
|
+
color: var(--ermis-text-muted, #6b7280);
|
|
30
|
+
background: transparent;
|
|
31
|
+
border: none;
|
|
32
|
+
cursor: pointer;
|
|
33
|
+
transition: all var(--ermis-transition, 150ms ease);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.ermis-create-channel__tab:hover {
|
|
37
|
+
color: var(--ermis-text-primary, #e5e7eb);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.ermis-create-channel__tab--active {
|
|
41
|
+
background-color: var(--ermis-bg-active, rgba(99, 102, 241, 0.12));
|
|
42
|
+
color: var(--ermis-text-primary, #e5e7eb);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* Form Fields */
|
|
46
|
+
.ermis-create-channel__field {
|
|
47
|
+
display: flex;
|
|
48
|
+
flex-direction: column;
|
|
49
|
+
gap: var(--ermis-spacing-xs, 0.25rem);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.ermis-create-channel__label {
|
|
53
|
+
font-size: var(--ermis-font-size-sm, 0.875rem);
|
|
54
|
+
font-weight: 500;
|
|
55
|
+
color: var(--ermis-text-primary, #e5e7eb);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.ermis-create-channel__input,
|
|
59
|
+
.ermis-create-channel__textarea {
|
|
60
|
+
width: 100%;
|
|
61
|
+
padding: 10px 12px;
|
|
62
|
+
border-radius: var(--ermis-radius-md, 0.5rem);
|
|
63
|
+
border: 1px solid var(--ermis-border, rgba(255, 255, 255, 0.08));
|
|
64
|
+
background-color: var(--ermis-bg-secondary, #111118);
|
|
65
|
+
color: var(--ermis-text-primary, #e5e7eb);
|
|
66
|
+
font-size: var(--ermis-font-size-sm, 0.875rem);
|
|
67
|
+
font-family: inherit;
|
|
68
|
+
outline: none;
|
|
69
|
+
transition: border-color var(--ermis-transition, 150ms ease);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.ermis-create-channel__input:focus,
|
|
73
|
+
.ermis-create-channel__textarea:focus {
|
|
74
|
+
border-color: var(--ermis-accent, #6366f1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.ermis-create-channel__textarea {
|
|
78
|
+
resize: vertical;
|
|
79
|
+
min-height: 80px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* Public toggle */
|
|
83
|
+
.ermis-create-channel__field--toggle {
|
|
84
|
+
flex-direction: row;
|
|
85
|
+
align-items: center;
|
|
86
|
+
justify-content: space-between;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.ermis-create-channel__toggle {
|
|
90
|
+
position: relative;
|
|
91
|
+
width: 44px;
|
|
92
|
+
height: 24px;
|
|
93
|
+
background-color: var(--ermis-bg-secondary, #111118);
|
|
94
|
+
border: 1px solid var(--ermis-border, rgba(255, 255, 255, 0.08));
|
|
95
|
+
border-radius: 12px;
|
|
96
|
+
cursor: pointer;
|
|
97
|
+
transition: background-color var(--ermis-transition, 150ms ease);
|
|
98
|
+
padding: 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.ermis-create-channel__toggle--on {
|
|
102
|
+
background-color: var(--ermis-accent, #6366f1);
|
|
103
|
+
border-color: var(--ermis-accent, #6366f1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.ermis-create-channel__toggle-thumb {
|
|
107
|
+
position: absolute;
|
|
108
|
+
top: 1px;
|
|
109
|
+
left: 2px;
|
|
110
|
+
width: 20px;
|
|
111
|
+
height: 20px;
|
|
112
|
+
background-color: #ffffff;
|
|
113
|
+
border-radius: 50%;
|
|
114
|
+
transition: transform var(--ermis-transition, 150ms ease);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.ermis-create-channel__toggle--on .ermis-create-channel__toggle-thumb {
|
|
118
|
+
transform: translateX(20px);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* User Picker Wrap */
|
|
122
|
+
.ermis-create-channel__users {
|
|
123
|
+
margin-top: var(--ermis-spacing-sm, 0.5rem);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.ermis-create-channel__users-title {
|
|
127
|
+
font-size: var(--ermis-font-size-sm, 0.875rem);
|
|
128
|
+
font-weight: 500;
|
|
129
|
+
color: var(--ermis-text-primary, #e5e7eb);
|
|
130
|
+
margin-bottom: var(--ermis-spacing-xs, 0.25rem);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* Error */
|
|
134
|
+
.ermis-create-channel__error {
|
|
135
|
+
display: flex;
|
|
136
|
+
align-items: center;
|
|
137
|
+
gap: var(--ermis-spacing-xs, 0.25rem);
|
|
138
|
+
padding: var(--ermis-spacing-sm, 0.5rem);
|
|
139
|
+
color: var(--ermis-error, #ef4444);
|
|
140
|
+
background-color: rgba(239, 68, 68, 0.1);
|
|
141
|
+
border-radius: var(--ermis-radius-sm, 0.375rem);
|
|
142
|
+
font-size: var(--ermis-font-size-sm, 0.875rem);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* Footer Buttons */
|
|
146
|
+
.ermis-create-channel__footer {
|
|
147
|
+
display: flex;
|
|
148
|
+
justify-content: flex-end;
|
|
149
|
+
gap: var(--ermis-spacing-sm, 0.5rem);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.ermis-create-channel__btn {
|
|
153
|
+
padding: 8px 16px;
|
|
154
|
+
border-radius: var(--ermis-radius-md, 0.5rem);
|
|
155
|
+
font-size: var(--ermis-font-size-sm, 0.875rem);
|
|
156
|
+
font-weight: 500;
|
|
157
|
+
cursor: pointer;
|
|
158
|
+
transition: all var(--ermis-transition, 150ms ease);
|
|
159
|
+
border: none;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.ermis-create-channel__btn:disabled {
|
|
163
|
+
opacity: 0.5;
|
|
164
|
+
cursor: not-allowed;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.ermis-create-channel__btn--cancel {
|
|
168
|
+
background-color: transparent;
|
|
169
|
+
color: var(--ermis-text-primary, #e5e7eb);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.ermis-create-channel__btn--cancel:hover:not(:disabled) {
|
|
173
|
+
background-color: var(--ermis-bg-hover, rgba(255, 255, 255, 0.04));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.ermis-create-channel__btn--create {
|
|
177
|
+
background-color: var(--ermis-accent, #6366f1);
|
|
178
|
+
color: #fff;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.ermis-create-channel__btn--create:hover:not(:disabled) {
|
|
182
|
+
opacity: 0.9;
|
|
183
|
+
}
|