Rewrite website as a Hyprland desktop
This commit is contained in:
1 parent
7c52779cf6
commit
3065ad6092
197 files changed
+5609
-17779
No files matched your search
@@ -0,0 +1,175 @@
|
||||
<script lang="ts">
|
||||
import Icon from '@iconify/svelte';
|
||||
import { desktop, type DesktopAction } from '#lib/content';
|
||||
import { allSitePages } from '#lib/site/pages';
|
||||
import { runDesktopAction } from '#lib/desktop/actions';
|
||||
import { windowManager } from '#lib/desktop/windows.svelte';
|
||||
|
||||
interface LauncherEntry {
|
||||
label: string;
|
||||
detail: string;
|
||||
icon: string;
|
||||
open: DesktopAction;
|
||||
}
|
||||
|
||||
const pageEntries: LauncherEntry[] = allSitePages.map((page) => ({
|
||||
label: page.title,
|
||||
detail: page.path,
|
||||
icon: page.icon,
|
||||
open: { page: page.path }
|
||||
}));
|
||||
|
||||
const iconEntries: LauncherEntry[] = desktop.icons
|
||||
.filter((item) => !('page' in item.open))
|
||||
.map((item) => ({
|
||||
label: item.label,
|
||||
detail: 'terminal' in item.open ? `kitty ${item.open.terminal}`.trim() : 'url' in item.open ? item.open.url : '',
|
||||
icon: item.icon,
|
||||
open: item.open
|
||||
}));
|
||||
|
||||
const entries = [...pageEntries, ...iconEntries];
|
||||
|
||||
let query = $state('');
|
||||
let selectedIndex = $state(0);
|
||||
let searchElement = $state<HTMLInputElement>();
|
||||
|
||||
const filteredEntries = $derived(
|
||||
entries.filter((entry) => `${entry.label} ${entry.detail}`.toLowerCase().includes(query.toLowerCase()))
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
query;
|
||||
selectedIndex = 0;
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
searchElement?.focus();
|
||||
});
|
||||
|
||||
function close() {
|
||||
windowManager.launcherOpen = false;
|
||||
}
|
||||
|
||||
function launch(entry: LauncherEntry | undefined) {
|
||||
if (entry) runDesktopAction(entry.open);
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') close();
|
||||
else if (event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
selectedIndex = Math.min(filteredEntries.length - 1, selectedIndex + 1);
|
||||
} else if (event.key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
selectedIndex = Math.max(0, selectedIndex - 1);
|
||||
} else if (event.key === 'Enter') launch(filteredEntries[selectedIndex]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<button class="backdrop" aria-label="Close launcher" onclick={close}></button>
|
||||
<div class="launcher" role="dialog" aria-label="Launcher">
|
||||
<div class="launcher-surface">
|
||||
<label class="search">
|
||||
<Icon icon="mdi:magnify" width="20" />
|
||||
<input bind:this={searchElement} bind:value={query} onkeydown={handleKeydown} placeholder="Search apps and pages" />
|
||||
</label>
|
||||
<ul class="results">
|
||||
{#each filteredEntries as entry, index (entry.label + entry.detail)}
|
||||
<li>
|
||||
<button class="result" class:selected={index === selectedIndex} onclick={() => launch(entry)} onmouseenter={() => (selectedIndex = index)}>
|
||||
<Icon icon={entry.icon} width="20" />
|
||||
<span class="result-label">{entry.label}</span>
|
||||
<span class="result-detail">{entry.detail}</span>
|
||||
</button>
|
||||
</li>
|
||||
{:else}
|
||||
<li class="empty">No matches</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 60;
|
||||
background: color-mix(in srgb, var(--dark-background) 40%, transparent);
|
||||
border: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.launcher {
|
||||
position: fixed;
|
||||
top: 18vh;
|
||||
left: 50%;
|
||||
z-index: 70;
|
||||
width: min(640px, calc(100vw - 24px));
|
||||
padding: var(--border-size);
|
||||
background: var(--border-active);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.launcher-surface {
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
.search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.6rem 0.8rem;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--foreground) 15%, transparent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.search input {
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
color: var(--foreground);
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.results {
|
||||
max-height: 50vh;
|
||||
margin: 0;
|
||||
padding: 0.3rem;
|
||||
overflow-y: auto;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.result {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
width: 100%;
|
||||
padding: 0.4rem 0.5rem;
|
||||
background: none;
|
||||
border: none;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.result.selected {
|
||||
background: color-mix(in srgb, var(--foreground) 15%, transparent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.result-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.result-detail,
|
||||
.empty {
|
||||
color: var(--muted);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user