fix: UI polish — overflow, placeholders, dashboard provider card
- Fix bot card header overflow by replacing "Sync with Telegram" text button with icon button, add flex-wrap - Rename sync button label to "Sync Commands" - Remove decorative dashes from selector placeholders (— X — → X) - Show selected provider name/icon in dashboard stat card when global provider filter is active - Add selector placeholder convention to frontend-architecture.md
This commit is contained in:
@@ -42,3 +42,9 @@ Shared entities use a `$state`-based cache layer in `frontend/src/lib/stores/`:
|
||||
2. Add `export const fooCache = createEntityCache<Foo>('/foo');` to `caches.svelte.ts`
|
||||
3. Add `fooCache.clear()` to `clearAllCaches()`
|
||||
4. In page components: replace `let foo = $state<Foo[]>([])` with `let foo = $derived(fooCache.items)` and replace `api('/foo')` with `fooCache.fetch()`
|
||||
|
||||
## UI Conventions
|
||||
|
||||
### Selector Placeholders
|
||||
|
||||
**IMPORTANT**: Selector/dropdown placeholder text must be plain and simple — no decorative dashes or special characters. Use `Select provider...` or `Tracking Configs`, NOT `— Select provider —` or `-- Tracking Configs --`. The i18n keys already follow this convention; never wrap them with `'— ' + t('key') + ' —'` in Svelte templates.
|
||||
|
||||
@@ -16,8 +16,9 @@ Detailed context is split into focused documents under `.claude/docs/`. Read the
|
||||
2. **Overlays** MUST use `position: fixed` with inline styles and `z-index: 9999` — see [frontend-architecture.md](.claude/docs/frontend-architecture.md).
|
||||
3. **Template variables** must be updated in 6 files simultaneously — see [template-system.md](.claude/docs/template-system.md).
|
||||
4. **Entity cache** — shared entities use `$state`-based caches in `frontend/src/lib/stores/caches.svelte.ts`. Always use cache for cross-page data; invalidate after mutations — see [frontend-architecture.md](.claude/docs/frontend-architecture.md).
|
||||
5. **Telegram API** — ALL Telegram Bot API calls (sendMessage, sendPhoto, sendMediaGroup, etc.) MUST go through `TelegramClient` in `packages/core/src/notify_bridge_core/notifications/telegram/client.py`. NEVER duplicate sending logic in command handlers, API routes, or services. If `TelegramClient` lacks a method you need, add it there.
|
||||
6. **Service provider defaults** — when implementing a new service provider, ALWAYS create default notification and command templates and configs. This requires changes across all of these locations:
|
||||
5. **Selector placeholders** — use plain text without decorative dashes. `Select provider...` not `— Select provider —` — see [frontend-architecture.md](.claude/docs/frontend-architecture.md).
|
||||
6. **Telegram API** — ALL Telegram Bot API calls (sendMessage, sendPhoto, sendMediaGroup, etc.) MUST go through `TelegramClient` in `packages/core/src/notify_bridge_core/notifications/telegram/client.py`. NEVER duplicate sending logic in command handlers, API routes, or services. If `TelegramClient` lacks a method you need, add it there.
|
||||
7. **Service provider defaults** — when implementing a new service provider, ALWAYS create default notification and command templates and configs. This requires changes across all of these locations:
|
||||
- Jinja2 notification templates for each locale in `packages/core/src/notify_bridge_core/templates/defaults/{en,ru}/`
|
||||
- Jinja2 command templates for each locale in `packages/core/src/notify_bridge_core/templates/command_defaults/{en,ru}/{provider}/`
|
||||
- Notification slot mapping in `packages/core/src/notify_bridge_core/templates/defaults/loader.py` (`PROVIDER_SLOT_FILE_MAP`)
|
||||
|
||||
@@ -330,7 +330,7 @@
|
||||
"rateFind": "Find cooldown",
|
||||
"rateDefault": "Default cooldown",
|
||||
"noCommandsForProvider": "This provider type does not support bot commands.",
|
||||
"syncCommands": "Sync with Telegram",
|
||||
"syncCommands": "Sync Commands",
|
||||
"discoverChats": "Discover chats from Telegram",
|
||||
"clickToCopy": "Click to copy chat ID",
|
||||
"chatsDiscovered": "Chats discovered",
|
||||
@@ -873,7 +873,7 @@
|
||||
"favoritesOnly": "Favorites only",
|
||||
"targetAlbum": "Target Album",
|
||||
"selectAlbum": "Album",
|
||||
"selectAlbumPlaceholder": "— Select album —",
|
||||
"selectAlbumPlaceholder": "Select album",
|
||||
"albumId": "Album ID",
|
||||
"createAlbumIfMissing": "Create album if it doesn't exist",
|
||||
"newAlbumName": "New album name",
|
||||
|
||||
@@ -330,7 +330,7 @@
|
||||
"rateFind": "Кулдаун поиска файлов",
|
||||
"rateDefault": "Кулдаун по умолчанию",
|
||||
"noCommandsForProvider": "Этот тип провайдера не поддерживает команды бота.",
|
||||
"syncCommands": "Синхронизировать с Telegram",
|
||||
"syncCommands": "Синхр. команды",
|
||||
"discoverChats": "Обнаружить чаты из Telegram",
|
||||
"clickToCopy": "Нажмите, чтобы скопировать ID чата",
|
||||
"chatsDiscovered": "Чаты обнаружены",
|
||||
@@ -873,7 +873,7 @@
|
||||
"favoritesOnly": "Только избранное",
|
||||
"targetAlbum": "Целевой альбом",
|
||||
"selectAlbum": "Альбом",
|
||||
"selectAlbumPlaceholder": "— Выберите альбом —",
|
||||
"selectAlbumPlaceholder": "Выберите альбом",
|
||||
"albumId": "ID альбома",
|
||||
"createAlbumIfMissing": "Создать альбом, если не существует",
|
||||
"newAlbumName": "Название нового альбома",
|
||||
|
||||
@@ -177,8 +177,12 @@
|
||||
|
||||
let displayCommandTrackers = $state(0);
|
||||
|
||||
const filteredProvider = $derived(globalProviderFilter.id ? providers.find(p => p.id === globalProviderFilter.id) : null);
|
||||
|
||||
const statCards = $derived(status ? [
|
||||
{ icon: 'mdiServer', label: 'dashboard.providers', value: displayProviders, color: '#0d9488' },
|
||||
filteredProvider
|
||||
? { icon: providerDefaultIcon(filteredProvider), label: filteredProvider.name, value: filteredProvider.type, color: '#0d9488', isProvider: true }
|
||||
: { icon: 'mdiServer', label: 'dashboard.providers', value: displayProviders, color: '#0d9488' },
|
||||
{ icon: 'mdiRadar', label: 'dashboard.activeTrackers', value: displayActive, suffix: ` / ${displayTotal}`, color: '#6366f1' },
|
||||
{ icon: 'mdiTarget', label: 'dashboard.targets', value: displayTargets, color: '#f59e0b' },
|
||||
...(status.command_trackers !== undefined ? [{ icon: 'mdiConsoleLine', label: 'nav.commandTrackers', value: displayCommandTrackers, color: '#8b5cf6' }] : []),
|
||||
@@ -234,10 +238,14 @@
|
||||
<MdiIcon name={card.icon} size={22} />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm" style="color: var(--color-muted-foreground);">{t(card.label)}</p>
|
||||
<p class="stat-value font-mono" style="animation-delay: {i * 80 + 200}ms;">
|
||||
{card.value}{#if card.suffix}<span class="stat-suffix">{card.suffix}</span>{/if}
|
||||
</p>
|
||||
<p class="text-sm" style="color: var(--color-muted-foreground);">{card.isProvider ? card.label : t(card.label)}</p>
|
||||
{#if card.isProvider}
|
||||
<p class="text-lg font-medium" style="color: {card.color};">{card.value}</p>
|
||||
{:else}
|
||||
<p class="stat-value font-mono" style="animation-delay: {i * 80 + 200}ms;">
|
||||
{card.value}{#if card.suffix}<span class="stat-suffix">{card.suffix}</span>{/if}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -318,9 +318,9 @@
|
||||
<div class="space-y-3 stagger-children">
|
||||
{#each bots as bot}
|
||||
<Card hover entityId={bot.id}>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center justify-between gap-2 flex-wrap">
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<span style="color: var(--color-primary);"><MdiIcon name={bot.icon || 'mdiRobot'} size={20} /></span>
|
||||
<p class="font-medium">{bot.name}</p>
|
||||
{#if bot.bot_username}
|
||||
@@ -335,21 +335,17 @@
|
||||
</div>
|
||||
<p class="text-xs text-[var(--color-muted-foreground)] font-mono">{bot.token_preview}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="flex items-center gap-1 flex-shrink-0 flex-wrap">
|
||||
<IconButton icon="mdiPencil" title={t('common.edit')} onclick={() => editBot(bot)} />
|
||||
<button onclick={() => toggleSection(bot.id, 'chats')}
|
||||
class="text-xs text-[var(--color-muted-foreground)] hover:underline px-2 py-1">
|
||||
class="text-xs text-[var(--color-muted-foreground)] hover:underline px-2 py-1 whitespace-nowrap">
|
||||
{t('telegramBot.chats')} {expandedSection[bot.id] === 'chats' ? '▲' : '▼'}
|
||||
</button>
|
||||
<button onclick={() => { toggleSection(bot.id, 'listeners'); if (expandedSection[bot.id] === 'listeners') loadListenerStatus(bot.id); }}
|
||||
class="text-xs text-[var(--color-muted-foreground)] hover:underline px-2 py-1">
|
||||
class="text-xs text-[var(--color-muted-foreground)] hover:underline px-2 py-1 whitespace-nowrap">
|
||||
{t('commandTracker.listeners')} {expandedSection[bot.id] === 'listeners' ? '▲' : '▼'}
|
||||
</button>
|
||||
<button onclick={() => syncCommands(bot.id)} disabled={modeChanging[bot.id]}
|
||||
class="text-xs text-[var(--color-primary)] hover:underline px-2 py-1 flex items-center gap-1">
|
||||
<MdiIcon name="mdiSync" size={14} />
|
||||
{t('telegramBot.syncCommands')}
|
||||
</button>
|
||||
<IconButton icon="mdiSync" title={t('telegramBot.syncCommands')} onclick={() => syncCommands(bot.id)} disabled={modeChanging[bot.id]} />
|
||||
<IconButton icon="mdiDelete" title={t('common.delete')} onclick={() => remove(bot.id)} variant="danger" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -85,12 +85,12 @@
|
||||
<div class="flex items-center gap-2 flex-wrap justify-end">
|
||||
<div class="min-w-[140px]">
|
||||
<EntitySelect items={trackingConfigItems} value={tt.tracking_config_id}
|
||||
placeholder={'— ' + t('trackingConfig.title') + ' —'} size="sm" allowNone noneLabel={'— ' + t('trackingConfig.title') + ' —'}
|
||||
placeholder={t('trackingConfig.title')} size="sm" allowNone noneLabel={t('trackingConfig.title')}
|
||||
onselect={(v) => onupdateLink(tt, 'tracking_config_id', Number(v) || null)} />
|
||||
</div>
|
||||
<div class="min-w-[140px]">
|
||||
<EntitySelect items={templateConfigItems} value={tt.template_config_id}
|
||||
placeholder={'— ' + t('templateConfig.title') + ' —'} size="sm" allowNone noneLabel={'— ' + t('templateConfig.title') + ' —'}
|
||||
placeholder={t('templateConfig.title')} size="sm" allowNone noneLabel={t('templateConfig.title')}
|
||||
onselect={(v) => onupdateLink(tt, 'template_config_id', Number(v) || null)} />
|
||||
</div>
|
||||
<div class="relative">
|
||||
@@ -113,17 +113,17 @@
|
||||
<div class="flex items-center gap-2 mt-2">
|
||||
<div class="flex-1 min-w-[140px]">
|
||||
<EntitySelect items={targetItems} value={newLinkTargetId || null}
|
||||
placeholder={'— ' + t('notificationTracker.addTarget') + ' —'} size="sm"
|
||||
placeholder={t('notificationTracker.addTarget')} size="sm"
|
||||
onselect={(v) => onchangeNewTarget(Number(v) || 0)} />
|
||||
</div>
|
||||
<div class="min-w-[140px]">
|
||||
<EntitySelect items={trackingConfigItems} value={newLinkTrackingConfigId || null}
|
||||
placeholder={'— ' + t('trackingConfig.title') + ' —'} size="sm" allowNone noneLabel={'— ' + t('trackingConfig.title') + ' —'}
|
||||
placeholder={t('trackingConfig.title')} size="sm" allowNone noneLabel={t('trackingConfig.title')}
|
||||
onselect={(v) => onchangeNewTrackingConfig(Number(v) || 0)} />
|
||||
</div>
|
||||
<div class="min-w-[140px]">
|
||||
<EntitySelect items={templateConfigItems} value={newLinkTemplateConfigId || null}
|
||||
placeholder={'— ' + t('templateConfig.title') + ' —'} size="sm" allowNone noneLabel={'— ' + t('templateConfig.title') + ' —'}
|
||||
placeholder={t('templateConfig.title')} size="sm" allowNone noneLabel={t('templateConfig.title')}
|
||||
onselect={(v) => onchangeNewTemplateConfig(Number(v) || 0)} />
|
||||
</div>
|
||||
<button onclick={onaddLink}
|
||||
|
||||
Reference in New Issue
Block a user