Portfolio/app/components/StatusBar.vue
2026-09-08 19:54:06 +02:00

139 lines
3.2 KiB
Vue

<script setup lang="ts">
const { site } = useSiteData()
const route = useRoute()
const { theme, mounted, toggle } = useTheme()
const path = computed(() => {
const p = route.path.replace(/\/+$/, '')
return p === '' ? '~' : `~${p}`
})
const cmd = computed(() => {
const p = route.path
if (p === '/') return 'cat about.md'
if (p === '/projects') return 'ls -la ./projects'
if (p === '/blog') return 'ls ./posts'
if (p.startsWith('/blog/')) return `bat posts/${p.slice(6)}.md`
return `find . -path "${p}"`
})
const clock = ref('--:--:--')
let timer: ReturnType<typeof setInterval> | undefined
function tick() {
clock.value = new Intl.DateTimeFormat('en-GB', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
timeZone: site.value.timezone,
}).format(new Date())
}
onMounted(() => {
tick()
timer = setInterval(tick, 1000)
})
onBeforeUnmount(() => clearInterval(timer))
</script>
<template>
<div class="status">
<div class="status-left">
<span class="prompt">{{ site.handle }}@dev</span>
<span class="sep">:</span>
<span class="path">{{ path }}</span>
<span class="dollar">$</span>
<span class="cmd"><slot name="cmd">{{ cmd }}</slot></span>
<span class="cursor">_</span>
</div>
<div class="status-right small">
<NuxtLink v-for="item in site.nav" :key="item.href" :to="item.href">
{{ item.label }}
</NuxtLink>
<span class="sep">|</span>
<button class="theme-toggle" type="button" :aria-label="`switch to ${theme === 'dark' ? 'light' : 'dark'} theme`" @click="toggle">
<span class="theme-icon">{{ mounted && theme === 'dark' ? '◐' : '◑' }}</span>
<span>{{ mounted ? theme : 'theme' }}</span>
</button>
<span class="sep">|</span>
<span class="muted nowrap">{{ clock }}</span>
</div>
</div>
</template>
<style scoped>
.status {
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid var(--ink);
border-bottom: 1px solid var(--ink);
padding: 0.5rem 0.75rem;
font-size: 0.85rem;
background: var(--paper);
flex-wrap: wrap;
gap: 0.5rem;
}
.status-left,
.status-right {
display: inline-flex;
align-items: center;
gap: 0.4rem;
flex-wrap: wrap;
}
.status-right {
gap: 0.6rem;
}
.prompt { color: var(--accent); }
.sep { color: var(--ink-mute); }
.path { color: var(--ink); }
.dollar { color: var(--ink-mute); }
.cmd { color: var(--ink); }
.cursor {
display: inline-block;
color: var(--ink);
animation: blink 1s steps(1, end) infinite;
}
@keyframes blink {
50% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.cursor { animation: none; }
}
.theme-toggle {
background: transparent;
border: 0;
padding: 0;
margin: 0;
color: var(--ink);
font: inherit;
font-size: 0.85rem;
cursor: pointer;
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: 3px;
display: inline-flex;
align-items: baseline;
gap: 0.25rem;
}
.theme-toggle:hover {
background: var(--accent);
color: var(--paper);
text-decoration: none;
}
.theme-icon { color: var(--accent); }
.theme-toggle:hover .theme-icon { color: var(--paper); }
</style>