265 lines
5.2 KiB
Svelte
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>
|