Initialize the NebulaOS monorepo layout with top-level documentation, ignore rules, placeholder service/package directories, compositor docs plus a baseline Sway config, and session startup/desktop entry files. Add the first desktop shell prototype under `shells/desktop/ui` using React + Vite, including themed surface components (desktop, top bar, launcher, preview), mock Nebula APIs and shell bridge state wiring, reusable UI controls/icons, and supporting styles/config needed for local development.
157 lines
4.1 KiB
React
157 lines
4.1 KiB
React
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
import { PowerIcon, RestartIcon } from '../icons.jsx';
|
|
import { nebula } from '../../lib/nebula.js';
|
|
import { IconButton } from '../ui/IconButton.jsx';
|
|
import { SearchField } from '../ui/SearchField.jsx';
|
|
import { AppItem } from './AppItem.jsx';
|
|
import './Launcher.css';
|
|
|
|
function matchesQuery(app, query) {
|
|
if (!query) {
|
|
return true;
|
|
}
|
|
|
|
const haystack = `${app.name} ${app.comment ?? ''}`.toLowerCase();
|
|
return haystack.includes(query);
|
|
}
|
|
|
|
export function Launcher({
|
|
open,
|
|
apps,
|
|
recents,
|
|
projects,
|
|
user,
|
|
onLaunch,
|
|
onOpenProject,
|
|
}) {
|
|
const [query, setQuery] = useState('');
|
|
const [wasOpen, setWasOpen] = useState(open);
|
|
const searchRef = useRef(null);
|
|
|
|
if (open !== wasOpen) {
|
|
setWasOpen(open);
|
|
if (!open) {
|
|
setQuery('');
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (!open) {
|
|
return undefined;
|
|
}
|
|
|
|
const frame = window.requestAnimationFrame(() => {
|
|
searchRef.current?.focus();
|
|
});
|
|
|
|
return () => window.cancelAnimationFrame(frame);
|
|
}, [open]);
|
|
|
|
const filteredApps = useMemo(() => {
|
|
const normalised = query.trim().toLowerCase();
|
|
return apps.filter((app) => matchesQuery(app, normalised));
|
|
}, [apps, query]);
|
|
|
|
const searching = query.trim().length > 0;
|
|
|
|
return (
|
|
<div
|
|
className={`launcher nebula-surface${open ? ' is-open' : ''}`}
|
|
role="dialog"
|
|
aria-label="Launcher"
|
|
aria-hidden={!open}
|
|
inert={!open ? true : undefined}
|
|
>
|
|
<SearchField
|
|
inputRef={searchRef}
|
|
value={query}
|
|
onChange={setQuery}
|
|
placeholder="Search applications"
|
|
/>
|
|
|
|
<div className="launcher-body">
|
|
{!searching && recents.length > 0 ? (
|
|
<section className="launcher-section">
|
|
<h2>Recent</h2>
|
|
<div className="launcher-recent">
|
|
{recents.map((app) => (
|
|
<AppItem
|
|
key={app.id}
|
|
item={app}
|
|
variant="recent"
|
|
onSelect={onLaunch}
|
|
/>
|
|
))}
|
|
</div>
|
|
</section>
|
|
) : null}
|
|
|
|
<section className="launcher-section">
|
|
<h2>{searching ? 'Results' : 'Applications'}</h2>
|
|
{filteredApps.length > 0 ? (
|
|
<div className="launcher-grid">
|
|
{filteredApps.map((app) => (
|
|
<AppItem
|
|
key={app.id}
|
|
item={app}
|
|
variant="tile"
|
|
onSelect={onLaunch}
|
|
/>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="launcher-empty">No matching applications</p>
|
|
)}
|
|
</section>
|
|
|
|
{!searching && projects.length > 0 ? (
|
|
<section className="launcher-section">
|
|
<h2>Recent projects</h2>
|
|
<div className="launcher-projects">
|
|
{projects.map((project) => (
|
|
<AppItem
|
|
key={project.id}
|
|
item={project}
|
|
variant="row"
|
|
onSelect={onOpenProject}
|
|
/>
|
|
))}
|
|
</div>
|
|
</section>
|
|
) : null}
|
|
</div>
|
|
|
|
<footer className="launcher-footer">
|
|
<div className="launcher-user">
|
|
<span className="launcher-avatar" aria-hidden="true">
|
|
{user?.initials ?? 'N'}
|
|
</span>
|
|
<span className="launcher-user-copy">
|
|
<span className="launcher-user-name">{user?.name ?? 'User'}</span>
|
|
<span className="launcher-user-meta">Local session</span>
|
|
</span>
|
|
</div>
|
|
<div className="launcher-power">
|
|
<IconButton
|
|
label="Restart"
|
|
onClick={() => {
|
|
nebula.power.restart();
|
|
}}
|
|
>
|
|
<RestartIcon />
|
|
</IconButton>
|
|
<IconButton
|
|
className="nebula-icon-button--danger"
|
|
label="Shut down"
|
|
onClick={() => {
|
|
nebula.power.shutdown();
|
|
}}
|
|
>
|
|
<PowerIcon />
|
|
</IconButton>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
);
|
|
}
|