711f218622
Comprehensive pre-production sweep across the Aurora redesign — drives svelte-check to 0 errors / 0 warnings (was 61) without changing visual intent. Highlights: - Mobile: hero title shrinks at 480px, signal-list stacks timestamp under sentence below 640px, sidebar icon buttons bumped to 40x40 - Light theme: muted-foreground darkened to #3a3560 to clear WCAG AA on glass surfaces and the modal close button - Perf: topbar backdrop-filter 28→14px, mobile-more sheet 28→12px to cut concurrent blur layers on mid-tier mobile - a11y: prefers-reduced-motion mute for aurora drift / pulses / shimmer / stagger; aria-label on every icon-only button; aria-describedby on Hint; combobox/listbox/aria-activedescendant on SearchPalette; modal dialog tabindex; 47 label-without-control warnings across 14 form pages cleaned up via for=/id= or label→div - Dashboard derived state split into topology- vs status-bound layers so polling no longer re-runs the full provider/wires computation - Mobile bottom nav derived from baseNavEntries by key lookup so adding a top-level nav entry keeps the two trees in sync - Bug: template-configs page now respects the global provider filter for both the count meter and the type pill (was reading the unfiltered cache) - Misc: portal EventChart tooltip and switch its swatches to Aurora tokens; CollapsibleSlot warning state uses warning-fg/-bg tokens instead of #d97706; Hint z-index 99999→9999; element refs across Modal/EntitySelect/MultiEntitySelect/SearchPalette/IconGridSelect/ Hint/targets converted to \$state for reactivity; 4 dead .topbar-cta selectors removed
192 lines
8.8 KiB
Svelte
192 lines
8.8 KiB
Svelte
<script lang="ts">
|
|
import { slide } from 'svelte/transition';
|
|
import { t } from '$lib/i18n';
|
|
import Card from '$lib/components/Card.svelte';
|
|
import IconPicker from '$lib/components/IconPicker.svelte';
|
|
import Hint from '$lib/components/Hint.svelte';
|
|
import IconGridSelect from '$lib/components/IconGridSelect.svelte';
|
|
import EntitySelect from '$lib/components/EntitySelect.svelte';
|
|
import MultiEntitySelect from '$lib/components/MultiEntitySelect.svelte';
|
|
import type { EntityItem } from '$lib/components/EntitySelect.svelte';
|
|
import type { GridItem } from '$lib/components/IconGridSelect.svelte';
|
|
import Button from '$lib/components/Button.svelte';
|
|
|
|
interface Props {
|
|
form: {
|
|
name: string;
|
|
icon: string;
|
|
bot_id: number;
|
|
bot_token: string;
|
|
max_media_to_send: number;
|
|
max_media_per_group: number;
|
|
media_delay: number;
|
|
max_asset_size: number;
|
|
disable_url_preview: boolean;
|
|
send_large_photos_as_documents: boolean;
|
|
ai_captions: boolean;
|
|
chat_action: string;
|
|
username: string;
|
|
server_url: string;
|
|
auth_token: string;
|
|
matrix_bot_id: number;
|
|
email_bot_id: number;
|
|
child_target_ids: number[];
|
|
};
|
|
formType: string;
|
|
activeType: string | null;
|
|
typeGridItems: GridItem[];
|
|
telegramBotItems: EntityItem[];
|
|
emailBotItems: EntityItem[];
|
|
matrixBotItems: EntityItem[];
|
|
chatActionItems: GridItem[];
|
|
broadcastChildItems?: { value: number; label: string; icon: string; desc: string }[];
|
|
telegramBotCount: number;
|
|
emailBotCount: number;
|
|
matrixBotCount: number;
|
|
editing: number | null;
|
|
submitting: boolean;
|
|
error: string;
|
|
showTelegramSettings: boolean;
|
|
onsave: (e: SubmitEvent) => void;
|
|
ontoggleTelegramSettings: () => void;
|
|
}
|
|
|
|
let {
|
|
form = $bindable(),
|
|
formType = $bindable(),
|
|
activeType,
|
|
typeGridItems,
|
|
telegramBotItems,
|
|
emailBotItems,
|
|
matrixBotItems,
|
|
chatActionItems,
|
|
broadcastChildItems = [],
|
|
telegramBotCount,
|
|
emailBotCount,
|
|
matrixBotCount,
|
|
editing,
|
|
submitting,
|
|
error,
|
|
showTelegramSettings = $bindable(),
|
|
onsave,
|
|
ontoggleTelegramSettings,
|
|
}: Props = $props();
|
|
</script>
|
|
|
|
<div in:slide={{ duration: 200 }}>
|
|
<Card class="mb-6">
|
|
{#if error}<div class="bg-[var(--color-error-bg)] text-[var(--color-error-fg)] text-sm rounded-md p-3 mb-4">{error}</div>{/if}
|
|
<form onsubmit={onsave} class="space-y-4">
|
|
{#if !activeType}
|
|
<div>
|
|
<div class="block text-sm font-medium mb-1">{t('targets.type')}</div>
|
|
<IconGridSelect items={typeGridItems} bind:value={formType} columns={4} />
|
|
</div>
|
|
{/if}
|
|
<div>
|
|
<label for="tgt-name" class="block text-sm font-medium mb-1">{t('targets.name')}</label>
|
|
<div class="flex gap-2">
|
|
<IconPicker value={form.icon} onselect={(v: string) => form.icon = v} />
|
|
<input id="tgt-name" bind:value={form.name} required placeholder={t('targets.namePlaceholder')} class="flex-1 px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
|
|
</div>
|
|
</div>
|
|
{#if formType === 'telegram'}
|
|
<div>
|
|
<div class="block text-sm font-medium mb-1">{t('telegramBot.selectBot')}</div>
|
|
<EntitySelect items={telegramBotItems} bind:value={form.bot_id} placeholder={t('telegramBot.selectBot')} />
|
|
{#if telegramBotCount === 0}
|
|
<p class="text-xs text-[var(--color-muted-foreground)] mt-1">{t('telegramBot.noBots')} <a href="/bots?tab=telegram" class="underline">→</a></p>
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="border border-[var(--color-border)] rounded-md p-3">
|
|
<button type="button" onclick={ontoggleTelegramSettings}
|
|
class="text-sm font-medium cursor-pointer w-full text-left flex items-center justify-between">
|
|
{t('targets.telegramSettings')}
|
|
<span class="text-xs transition-transform duration-200" class:rotate-180={showTelegramSettings}>▼</span>
|
|
</button>
|
|
{#if showTelegramSettings}
|
|
<div in:slide={{ duration: 150 }} class="grid grid-cols-2 gap-3 mt-3">
|
|
<div>
|
|
<label for="tgt-maxmedia" class="block text-xs mb-1">{t('targets.maxMedia')}<Hint text={t('hints.maxMedia')} /></label>
|
|
<input id="tgt-maxmedia" type="number" bind:value={form.max_media_to_send} min="0" max="50" class="w-full px-2 py-1 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
|
|
</div>
|
|
<div>
|
|
<label for="tgt-groupsize" class="block text-xs mb-1">{t('targets.maxGroupSize')}<Hint text={t('hints.groupSize')} /></label>
|
|
<input id="tgt-groupsize" type="number" bind:value={form.max_media_per_group} min="2" max="10" class="w-full px-2 py-1 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
|
|
</div>
|
|
<div>
|
|
<label for="tgt-delay" class="block text-xs mb-1">{t('targets.chunkDelay')}<Hint text={t('hints.chunkDelay')} /></label>
|
|
<input id="tgt-delay" type="number" bind:value={form.media_delay} min="0" max="60000" step="100" class="w-full px-2 py-1 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
|
|
</div>
|
|
<div>
|
|
<label for="tgt-maxsize" class="block text-xs mb-1">{t('targets.maxAssetSize')}<Hint text={t('hints.maxAssetSize')} /></label>
|
|
<input id="tgt-maxsize" type="number" bind:value={form.max_asset_size} min="1" max="50" class="w-full px-2 py-1 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
|
|
</div>
|
|
<div class="col-span-2">
|
|
<div class="block text-xs mb-1">{t('targets.chatAction')}</div>
|
|
<IconGridSelect items={chatActionItems} bind:value={form.chat_action} columns={4} compact />
|
|
</div>
|
|
<label class="flex items-center gap-2 text-sm col-span-2"><input type="checkbox" bind:checked={form.disable_url_preview} /> {t('targets.disableUrlPreview')}</label>
|
|
<label class="flex items-center gap-2 text-sm col-span-2"><input type="checkbox" bind:checked={form.send_large_photos_as_documents} /> {t('targets.sendLargeAsDocuments')}</label>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{:else if formType === 'discord' || formType === 'slack'}
|
|
<div>
|
|
<label for="tgt-user" class="block text-sm font-medium mb-1">{t('targets.overrideUsername')}</label>
|
|
<input id="tgt-user" bind:value={form.username} placeholder="Notify Bridge"
|
|
class="w-full px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
|
|
</div>
|
|
{:else if formType === 'ntfy'}
|
|
<div>
|
|
<label for="tgt-ntfy-server" class="block text-sm font-medium mb-1">{t('targets.ntfyServer')}</label>
|
|
<input id="tgt-ntfy-server" bind:value={form.server_url} required placeholder="https://ntfy.sh"
|
|
class="w-full px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
|
|
</div>
|
|
<div>
|
|
<label for="tgt-ntfy-token" class="block text-sm font-medium mb-1">{t('targets.ntfyToken')}</label>
|
|
<input id="tgt-ntfy-token" bind:value={form.auth_token} placeholder={t('targets.ntfyTokenPlaceholder')}
|
|
class="w-full px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
|
|
</div>
|
|
{:else if formType === 'email'}
|
|
<div>
|
|
<div class="block text-sm font-medium mb-1">{t('targets.selectEmailBot')}</div>
|
|
<EntitySelect items={emailBotItems} bind:value={form.email_bot_id} placeholder={t('targets.selectEmailBot')} />
|
|
{#if emailBotCount === 0}
|
|
<p class="text-xs text-[var(--color-muted-foreground)] mt-1">{t('emailBot.noBots')} <a href="/bots?tab=email" class="underline">→</a></p>
|
|
{/if}
|
|
</div>
|
|
{:else if formType === 'matrix'}
|
|
<div>
|
|
<div class="block text-sm font-medium mb-1">{t('targets.selectMatrixBot')}</div>
|
|
<EntitySelect items={matrixBotItems} bind:value={form.matrix_bot_id} placeholder={t('targets.selectMatrixBot')} />
|
|
{#if matrixBotCount === 0}
|
|
<p class="text-xs text-[var(--color-muted-foreground)] mt-1">{t('matrixBot.noBots')} <a href="/bots?tab=matrix" class="underline">→</a></p>
|
|
{/if}
|
|
</div>
|
|
{:else if formType === 'broadcast'}
|
|
{@const childIds = (form.child_target_ids || []).map(String)}
|
|
<div>
|
|
<div class="block text-sm font-medium mb-1">{t('targets.selectChildTargets')}</div>
|
|
<MultiEntitySelect
|
|
items={broadcastChildItems?.map(i => ({ value: String(i.value), label: i.label, icon: i.icon, desc: i.desc })) ?? []}
|
|
values={childIds}
|
|
onchange={(vals) => form.child_target_ids = vals.map(Number)}
|
|
placeholder={t('targets.selectChildTargets')}
|
|
/>
|
|
{#if broadcastChildItems?.length === 0}
|
|
<p class="text-xs text-[var(--color-muted-foreground)] mt-1">{t('targets.noChildTargetsAvailable')}</p>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
|
|
{#if formType === 'telegram'}
|
|
<label class="flex items-center gap-2 text-sm"><input type="checkbox" bind:checked={form.ai_captions} /> {t('targets.aiCaptions')}<Hint text={t('hints.aiCaptions')} /></label>
|
|
{/if}
|
|
|
|
<Button type="submit" disabled={submitting}>{submitting ? t('common.loading') : (editing ? t('common.save') : t('targets.create'))}</Button>
|
|
</form>
|
|
</Card>
|
|
</div>
|