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:
2026-08-26 15:31:54 +12:00
parent bf5686cebf
commit dd9b43bd27
61 changed files with 3471 additions and 0 deletions
@@ -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>
);
}