Bootstrap NebulaOS repo and desktop shell UI
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.
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user