Files
windmill/frontend/src/lib/components/InstanceBanner.svelte
T
Ruben FiszelandClaude Opus 5.5 1fd729ac1a feat: add an instance-wide accent color setting with sidebar tint (#11335)
* feat: add an instance-wide accent color setting with sidebar tint

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: paint the cached accent before the license resolves

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: serve the banner and accent color from one cached endpoint

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: invalidate the instance ui cache and bound it with a ttl

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: keep the instance ui ttl under the client poll period

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* refactor: load the banner and accent color once per page load

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* refactor: read the banner and accent color without a server cache

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: show a cleared accent color as off

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 11:23:30 +02:00

54 lines
1.9 KiB
Svelte

<script lang="ts">
import { Button } from '$lib/components/common'
import { classes as alertClasses, icons as alertIcons } from '$lib/components/common/alert/model'
import { ExternalLink, X } from 'lucide-svelte'
import { useLocalStorageValue } from '$lib/svelte5Utils.svelte'
import { isInstanceBannerVisible, resolveInstanceBanner } from './instanceBanner'
import { instanceUi } from '$lib/instanceUi'
// Fetched by `InstanceUiSync`, which also carries the accent color.
let banner = $derived(resolveInstanceBanner($instanceUi?.instance_banner))
// Per-viewer, per-announcement. Holds the fingerprint of the dismissed banner, so
// a new announcement shows up again for everyone who dismissed the previous one.
const dismissed = useLocalStorageValue<string>('instance_banner_dismissed', '', 'string')
let shown = $derived(isInstanceBannerVisible(banner, dismissed.val))
let palette = $derived(alertClasses[banner?.severity ?? 'info'])
let Icon = $derived(alertIcons[banner?.severity ?? 'info'])
</script>
{#if shown && banner}
<!-- Sits in the content column above the page, in flow rather than overlaid, so it
pushes the app down instead of covering the top of whatever page is open. -->
<div
class="shrink-0 px-4 py-1.5 flex items-center justify-center gap-x-3 gap-y-1 flex-wrap {palette.bgClass}"
role="status"
>
<Icon size={16} class="shrink-0 {palette.iconClass}" />
<span class="text-xs font-medium {palette.titleClass}">{banner.message}</span>
{#if banner.link}
<Button
variant="subtle"
unifiedSize="xs"
href={banner.link}
target="_blank"
endIcon={{ icon: ExternalLink }}
>
{banner.linkLabel}
</Button>
{/if}
{#if banner.dismissible}
<Button
variant="subtle"
unifiedSize="xs"
iconOnly
startIcon={{ icon: X }}
title="Dismiss"
aria-label="Dismiss announcement"
onclick={() => (dismissed.val = banner?.fingerprint ?? '')}
/>
{/if}
</div>
{/if}