Files
website/src/lib/components/shell/BatteryPopover.svelte
T

265 lines
5.2 KiB
Svelte

<script lang="ts">
import { focusTrap } from '#lib/ui/focusTrap';
import Icon from '@iconify/svelte';
import { slide } from 'svelte/transition';
import {
availabilityLabel,
batteryIcon,
formatDuration,
isBatteryLow,
moodLabel,
weekdayHoursLabel,
weekendHoursLabel,
type BatteryState
} from '#lib/desktop/battery';
let { battery, onclose }: { battery: BatteryState; onclose: () => void } = $props();
let scheduleOpen = $state(false);
const schedule = [
{ label: 'Weekdays', value: weekdayHoursLabel },
{ label: 'Weekends', value: weekendHoursLabel }
];
const isLow = $derived(isBatteryLow(battery));
const timeLeftLabel = $derived(
battery.charging
? `back in ${formatDuration(battery.minutesRemaining)}`
: `${formatDuration(battery.minutesRemaining)} left`
);
const details = $derived([
{ label: 'My time', value: battery.localTime },
{ label: battery.charging ? 'Back online' : 'Offline at', value: battery.charging ? battery.backOnlineAt : battery.offlineAt }
]);
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') onclose();
}
</script>
<svelte:window onkeydown={handleKeydown} />
<button class="backdrop" aria-label="Close battery details" onclick={onclose}></button>
<div class="popover" role="dialog" aria-modal="true" aria-label="Battery details" tabindex="-1" use:focusTrap>
<div class="popover-surface" class:charging={battery.charging} class:low={isLow}>
<header class="summary">
<span class="battery-icon">
<Icon icon={batteryIcon(battery)} width="30" />
</span>
<div class="summary-text">
<span class="title">Availability</span>
<span class="status">
{availabilityLabel(battery)}
<span class="time-left">· {timeLeftLabel}</span>
</span>
</div>
<span class="level">{battery.level}%</span>
</header>
<div class="level-track">
<div class="level-fill" style:width="{battery.level}%"></div>
</div>
<p class="mood">{moodLabel(battery)}</p>
<dl class="details">
{#each details as detail (detail.label)}
<div class="detail-row">
<dt>{detail.label}</dt>
<dd>{detail.value}</dd>
</div>
{/each}
</dl>
<section class="schedule">
<button class="schedule-toggle" aria-expanded={scheduleOpen} onclick={() => (scheduleOpen = !scheduleOpen)}>
<Icon icon="mdi:calendar-clock" width="16" />
<span>Schedule</span>
<span class="chevron" class:open={scheduleOpen}><Icon icon="mdi:chevron-down" width="16" /></span>
</button>
{#if scheduleOpen}
<dl class="schedule-list" transition:slide={{ duration: 180 }}>
{#each schedule as day (day.label)}
<div class="detail-row">
<dt>{day.label}</dt>
<dd>{day.value}</dd>
</div>
{/each}
</dl>
{/if}
</section>
</div>
</div>
<style>
.backdrop {
position: fixed;
inset: 0;
z-index: 40;
background: none;
border: none;
cursor: default;
}
.popover {
position: absolute;
top: calc(100% + 8px);
right: 0;
z-index: 50;
width: 340px;
padding: var(--border-size);
background: var(--border-active);
animation: window-pop-in 0.15s ease-out;
}
.popover-surface {
--state-color: var(--accent);
background: var(--background);
}
.popover-surface.charging {
--state-color: var(--green);
}
.popover-surface.low {
--state-color: var(--red);
}
.summary {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem 1rem 0.75rem;
}
.battery-icon {
display: grid;
place-items: center;
color: var(--state-color);
}
.summary-text {
display: flex;
flex: 1;
flex-direction: column;
gap: 0.15rem;
}
.title {
font-size: 1.05rem;
font-weight: 700;
}
.status {
color: var(--muted);
font-size: 0.75rem;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.level {
font-size: 1.9rem;
font-weight: 700;
}
.level-track {
height: 8px;
margin: 0 1rem;
background: var(--lighter-background);
}
.level-fill {
height: 100%;
background: var(--state-color);
transition: width 0.3s ease;
}
.mood {
margin: 0.6rem 1rem 0;
color: var(--state-color);
font-size: 0.8rem;
font-style: italic;
}
.details {
display: grid;
grid-template-columns: 1fr;
gap: 0.45rem;
margin: 0;
padding: 0.9rem 1rem 1rem;
font-size: 0.8rem;
}
.detail-row {
display: flex;
justify-content: space-between;
gap: 0.5rem;
white-space: nowrap;
}
.detail-row dt {
color: var(--muted);
}
.detail-row dd {
margin: 0;
}
.time-left {
color: var(--foreground);
text-transform: none;
letter-spacing: normal;
}
.schedule {
border-top: 1px solid color-mix(in srgb, var(--foreground) 12%, transparent);
}
.schedule-toggle {
display: flex;
align-items: center;
gap: 0.45rem;
width: 100%;
padding: 0.6rem 1rem;
background: none;
border: none;
color: var(--foreground);
font-size: 0.8rem;
}
.schedule-toggle:hover {
color: var(--accent);
}
.chevron {
display: grid;
margin-left: auto;
transition: transform 0.2s ease;
}
.chevron.open {
transform: rotate(180deg);
}
.schedule-list {
display: grid;
gap: 0.45rem;
margin: 0;
padding: 0 1rem 0.9rem;
font-size: 0.8rem;
}
@media (max-width: 640px) {
.popover {
position: fixed;
top: calc(var(--bar-height) + 6px);
right: 8px;
left: 8px;
width: auto;
}
}
</style>