Add Lucidia shell prototype

This commit is contained in:
Alexa Amundson
2025-11-17 03:59:54 -06:00
parent 806cf095bc
commit 5e4583efef
2 changed files with 946 additions and 0 deletions

View File

@@ -123,6 +123,10 @@ php -S localhost:8000
Then visit: `http://localhost:8000`
### Lucidia Shell Prototype (v0.1)
For a minimal Lucidia-focused experience, open `lucidia-shell.html` in the same directory. It includes a retro desktop, window manager, Operator Core, and Lucidia Terminal with commands for listing/switching environments, listing/opening apps, and listing/running stub flows.
---
## 🌐 Deploy Anywhere

View File

@@ -0,0 +1,942 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>BlackRoad OS — Lucidia Shell v0.1</title>
<style>
/* ===============================
GLOBAL / DESKTOP STYLES
=============================== */
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #008080;
}
#desktop {
position: absolute;
inset: 0 0 32px 0;
background: linear-gradient(135deg, #008080, #006b6b);
color: #000;
}
/* ===============================
DESKTOP ICONS
=============================== */
.desktop-icon {
position: absolute;
width: 80px;
text-align: center;
cursor: default;
user-select: none;
color: #f0f0f0;
text-shadow: 0 1px 1px #000;
font-size: 11px;
}
.desktop-icon img {
display: block;
margin: 0 auto 4px;
width: 32px;
height: 32px;
image-rendering: pixelated;
}
.desktop-icon .label {
background: rgba(0, 0, 0, 0.35);
border-radius: 2px;
padding: 2px 4px;
}
.desktop-icon:hover .label {
background: rgba(0, 0, 0, 0.6);
}
/* ===============================
TASKBAR
=============================== */
#taskbar {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 32px;
background: linear-gradient(to top, #3c3c3c, #707070);
border-top: 2px solid #b0b0b0;
display: flex;
align-items: center;
padding: 0 4px;
gap: 4px;
box-sizing: border-box;
}
#start-button {
background: #c0c0c0;
border: 2px solid #ffffff;
border-right-color: #404040;
border-bottom-color: #404040;
padding: 2px 8px;
font-size: 13px;
display: flex;
align-items: center;
gap: 4px;
cursor: pointer;
}
#start-button:hover {
filter: brightness(1.1);
}
#taskbar-windows {
flex: 1;
display: flex;
align-items: center;
gap: 4px;
overflow: hidden;
}
.taskbar-item {
background: #c0c0c0;
border: 2px solid #ffffff;
border-right-color: #404040;
border-bottom-color: #404040;
padding: 2px 6px;
font-size: 12px;
cursor: pointer;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
max-width: 140px;
}
.taskbar-item.active {
background: #808080;
color: #fff;
}
#taskbar-clock {
min-width: 60px;
text-align: center;
font-size: 11px;
color: #f0f0f0;
text-shadow: 0 1px 1px #000;
}
/* ===============================
WINDOWS
=============================== */
.window {
position: absolute;
background: #c0c0c0;
border: 2px solid #ffffff;
border-right-color: #404040;
border-bottom-color: #404040;
box-shadow: 0 0 0 1px #000;
min-width: 260px;
min-height: 160px;
display: flex;
flex-direction: column;
}
.window-titlebar {
background: linear-gradient(to right, #000080, #1084d0);
color: #fff;
font-size: 12px;
padding: 2px 4px;
display: flex;
align-items: center;
justify-content: space-between;
cursor: grab;
user-select: none;
}
.window-titlebar .title {
font-weight: bold;
}
.window-controls {
display: flex;
gap: 2px;
}
.window-controls button {
width: 16px;
height: 16px;
padding: 0;
border: 1px solid #404040;
font-size: 10px;
background: #c0c0c0;
cursor: pointer;
}
.window-content {
flex: 1;
background: #efefef;
padding: 4px;
overflow: auto;
font-size: 12px;
box-sizing: border-box;
}
.window.focused {
box-shadow: 0 0 0 2px #ffd700;
}
/* ===============================
OPERATOR CORE
=============================== */
#operator-core {
position: absolute;
right: 8px;
bottom: 40px;
width: 200px;
background: #202020;
color: #e0e0e0;
border-radius: 6px;
box-shadow: 0 0 8px rgba(0,0,0,0.6);
font-size: 11px;
overflow: hidden;
z-index: 9999;
}
#operator-core-header {
background: #404040;
padding: 4px 6px;
font-weight: bold;
font-size: 10px;
letter-spacing: 1px;
}
#operator-core-body {
padding: 4px 6px 2px;
}
#operator-core-body .row {
display: flex;
justify-content: space-between;
margin-bottom: 2px;
}
#operator-core-controls {
display: flex;
gap: 4px;
padding: 4px 6px 6px;
}
#operator-core-controls button {
flex: 1;
font-size: 10px;
padding: 2px 4px;
border-radius: 3px;
border: none;
cursor: pointer;
background: #1565c0;
color: white;
}
#operator-core-controls button.secondary {
background: #555;
}
/* ===============================
TERMINAL APP
=============================== */
.terminal-output {
background: #000;
color: #00ff9c;
font-family: "SF Mono", Menlo, Monaco, Consolas, monospace;
font-size: 11px;
padding: 4px;
height: 150px;
overflow-y: auto;
border: 1px inset #808080;
}
.terminal-input-row {
display: flex;
margin-top: 4px;
}
.terminal-input-row span {
font-family: "SF Mono", Menlo, Monaco, Consolas, monospace;
font-size: 11px;
padding: 2px 4px;
background: #000;
color: #00ff9c;
border: 1px inset #808080;
border-right: none;
}
.terminal-input-row input {
flex: 1;
border: 1px inset #808080;
border-left: none;
background: #000;
color: #00ff9c;
font-family: "SF Mono", Menlo, Monaco, Consolas, monospace;
font-size: 11px;
padding: 2px;
}
.terminal-line.error {
color: #ff6b6b;
}
/* Safe mode overlay */
#safe-mode-overlay {
position: absolute;
inset: 0;
background: rgba(0,0,0,0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 9998;
}
#safe-mode-panel {
background: #202020;
color: #f0f0f0;
padding: 16px 20px;
border-radius: 8px;
text-align: center;
box-shadow: 0 0 16px rgba(0,0,0,0.8);
max-width: 320px;
}
#safe-mode-panel button {
margin-top: 8px;
padding: 4px 10px;
font-size: 11px;
border-radius: 4px;
border: none;
background: #1565c0;
color: white;
cursor: pointer;
}
</style>
</head>
<body>
<div id="desktop"></div>
<div id="taskbar">
<button id="start-button">🟢 Start</button>
<div id="taskbar-windows"></div>
<div id="taskbar-clock"></div>
</div>
<div id="operator-core">
<div id="operator-core-header">OPERATOR · LUCIDIA</div>
<div id="operator-core-body">
<div class="row"><span>ENV</span><span id="op-env">-</span></div>
<div class="row"><span>WINDOWS</span><span id="op-windows">0</span></div>
<div class="row"><span>HEALTH</span><span id="op-health">OK</span></div>
</div>
<div id="operator-core-controls">
<button id="btn-safe" class="secondary">Safe Mode</button>
<button id="btn-3d">3D View</button>
</div>
</div>
<script>
/***********************************************************
* SECTION: Lucidia v0.1 In-Memory Config
* PURPOSE: A tiny Lucidia-like structure the OS can consume.
* NOTE: This is JSON-based so we can upgrade to full .luc parsing later.
***********************************************************/
const LUCIDIA_CONFIG = {
envs: [
{
id: "env.lab",
name: "BlackRoad Lab",
type: "workspace",
layout: "desktop_2d",
apps: ["terminal", "explorer", "notes"],
onInit: ["open:terminal", "open:notes"]
},
{
id: "env.studio",
name: "Creator Studio",
type: "studio",
layout: "desktop_2d",
apps: ["terminal", "notes"],
onInit: ["open:terminal"]
}
],
apps: [
{
id: "terminal",
name: "Lucidia Terminal",
icon: "💻",
defaultSize: [700, 260]
},
{
id: "explorer",
name: "Explorer",
icon: "📂",
defaultSize: [480, 260]
},
{
id: "notes",
name: "Notes",
icon: "📝",
defaultSize: [420, 260]
}
],
flows: [
{
id: "flow.scaffold",
name: "Scaffold Project",
trigger: { type: "command", name: "scaffold" },
steps: [
{ action: "log", message: "Scaffolding project (stub)..." }
]
}
]
};
/***********************************************************
* SECTION: Lucidia Runtime
* PURPOSE: Minimal env/app/flow registry & helpers.
***********************************************************/
class LucRuntime {
constructor(config) {
this.envs = new Map();
this.apps = new Map();
this.flows = new Map();
config.envs.forEach(e => this.envs.set(e.id, e));
config.apps.forEach(a => this.apps.set(a.id, a));
config.flows.forEach(f => this.flows.set(f.id, f));
}
getEnv(id) { return this.envs.get(id); }
getApp(id) { return this.apps.get(id); }
getFlow(id) { return this.flows.get(id); }
listEnvs() { return Array.from(this.envs.values()); }
listApps() { return Array.from(this.apps.values()); }
listFlows() { return Array.from(this.flows.values()); }
}
const lucRuntime = new LucRuntime(LUCIDIA_CONFIG);
window.lucRuntime = lucRuntime; // for debugging
/***********************************************************
* SECTION: Desktop & Window Manager
***********************************************************/
class Desktop {
constructor(desktopEl, taskbarEl) {
this.desktopEl = desktopEl;
this.taskbarEl = taskbarEl;
this.currentEnv = null;
this.windows = new Map(); // id -> Window
this.zCounter = 10;
}
setEnv(env) {
this.currentEnv = env;
this.clearDesktop();
this.renderDesktopIcons(env);
this.runEnvOnInit(env);
updateOperatorEnv(env);
}
clearDesktop() {
this.desktopEl.innerHTML = "";
this.windows.clear();
refreshOperatorWindowCount(0);
updateTaskbar();
}
renderDesktopIcons(env) {
const apps = env.apps || [];
apps.forEach((appId, index) => {
const app = lucRuntime.getApp(appId);
if (!app) return;
const icon = document.createElement("div");
icon.className = "desktop-icon";
icon.style.left = (16 + (index % 6) * 96) + "px";
icon.style.top = (16 + Math.floor(index / 6) * 96) + "px";
icon.innerHTML = `
<div class="icon-symbol">${app.icon ?? "📦"}</div>
<div class="label">${app.name}</div>
`;
icon.addEventListener("dblclick", () => {
this.openApp(appId);
});
this.desktopEl.appendChild(icon);
});
}
runEnvOnInit(env) {
(env.onInit || []).forEach(action => {
const [kind, arg] = action.split(":");
if (kind === "open") this.openApp(arg);
});
}
openApp(appId) {
const appDef = lucRuntime.getApp(appId);
if (!appDef) return;
const winId = "win-" + Date.now() + "-" + Math.floor(Math.random() * 9999);
const win = new Win({
id: winId,
appId,
title: appDef.name,
size: appDef.defaultSize || [480, 260],
position: [80 + this.windows.size * 20, 80 + this.windows.size * 20]
}, this);
this.windows.set(winId, win);
win.render(this.desktopEl);
refreshOperatorWindowCount(this.windows.size);
updateTaskbar();
return win;
}
closeWindow(winId) {
const win = this.windows.get(winId);
if (!win) return;
win.destroy();
this.windows.delete(winId);
refreshOperatorWindowCount(this.windows.size);
updateTaskbar();
}
focusWindow(winId) {
const win = this.windows.get(winId);
if (!win) return;
this.windows.forEach(w => w.setFocused(false));
win.setFocused(true);
win.setZIndex(this.zCounter++);
updateTaskbarActive(winId);
}
minimizeWindow(winId) {
const win = this.windows.get(winId);
if (!win) return;
win.setMinimized(true);
updateTaskbar();
}
restoreWindow(winId) {
const win = this.windows.get(winId);
if (!win) return;
win.setMinimized(false);
this.focusWindow(winId);
}
}
class Win {
constructor(config, desktop) {
this.id = config.id;
this.appId = config.appId;
this.title = config.title;
this.size = config.size;
this.position = config.position;
this.desktop = desktop;
this.el = null;
this.minimized = false;
}
render(container) {
const [w, h] = this.size;
const [x, y] = this.position;
const el = document.createElement("div");
el.className = "window";
el.style.width = w + "px";
el.style.height = h + "px";
el.style.left = x + "px";
el.style.top = y + "px";
el.dataset.winId = this.id;
el.innerHTML = `
<div class="window-titlebar">
<span class="title">${this.title}</span>
<div class="window-controls">
<button data-action="minimize">_</button>
<button data-action="close">×</button>
</div>
</div>
<div class="window-content"></div>
`;
container.appendChild(el);
this.el = el;
this.setupDrag();
this.setupControls();
this.desktop.focusWindow(this.id);
this.mountAppContent();
}
setupDrag() {
const bar = this.el.querySelector(".window-titlebar");
let dragging = false;
let startX, startY, startLeft, startTop;
bar.addEventListener("mousedown", (e) => {
if (e.target.closest(".window-controls")) return;
dragging = true;
startX = e.clientX;
startY = e.clientY;
startLeft = this.el.offsetLeft;
startTop = this.el.offsetTop;
this.desktop.focusWindow(this.id);
});
document.addEventListener("mousemove", (e) => {
if (!dragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
this.el.style.left = startLeft + dx + "px";
this.el.style.top = startTop + dy + "px";
});
document.addEventListener("mouseup", () => dragging = false);
}
setupControls() {
const controls = this.el.querySelector(".window-controls");
controls.addEventListener("click", (e) => {
const action = e.target.dataset.action;
if (!action) return;
if (action === "close") {
this.desktop.closeWindow(this.id);
} else if (action === "minimize") {
this.desktop.minimizeWindow(this.id);
}
});
this.el.addEventListener("mousedown", () => {
this.desktop.focusWindow(this.id);
});
}
setFocused(focused) {
if (!this.el) return;
this.el.classList.toggle("focused", focused);
}
setZIndex(z) {
if (this.el) this.el.style.zIndex = z;
}
setMinimized(min) {
this.minimized = min;
if (!this.el) return;
this.el.style.display = min ? "none" : "flex";
}
destroy() {
if (this.el) this.el.remove();
}
mountAppContent() {
const container = this.el.querySelector(".window-content");
const appId = this.appId;
if (appId === "terminal") {
mountTerminal(container);
} else if (appId === "explorer") {
container.innerHTML = "<strong>Explorer</strong><br><br>Lucidia entities will show here (stub).";
} else if (appId === "notes") {
mountNotes(container);
} else {
container.innerHTML = `<em>${appId}</em> app stub.`;
}
}
}
const desktop = new Desktop(
document.getElementById("desktop"),
document.getElementById("taskbar-windows")
);
window.desktop = desktop; // for debugging
/***********************************************************
* SECTION: Operator Core bindings
***********************************************************/
function updateOperatorEnv(env) {
document.getElementById("op-env").textContent = env ? env.id : "-";
}
function refreshOperatorWindowCount(count) {
document.getElementById("op-windows").textContent = String(count);
}
let SAFE_MODE = false;
function enterSafeMode() {
SAFE_MODE = true;
const overlay = document.createElement("div");
overlay.id = "safe-mode-overlay";
overlay.innerHTML = `
<div id="safe-mode-panel">
<h2>SAFE MODE</h2>
<p>All non-essential windows minimized.</p>
<button id="btn-exit-safe">Exit Safe Mode</button>
</div>
`;
document.body.appendChild(overlay);
desktop.windows.forEach(win => {
if (win.appId !== "terminal") win.setMinimized(true);
});
document.getElementById("btn-exit-safe").onclick = exitSafeMode;
}
function exitSafeMode() {
SAFE_MODE = false;
document.getElementById("safe-mode-overlay")?.remove();
desktop.windows.forEach(win => win.setMinimized(false));
}
document.getElementById("btn-safe").addEventListener("click", () => {
SAFE_MODE ? exitSafeMode() : enterSafeMode();
});
document.getElementById("btn-3d").addEventListener("click", () => {
console.log("[3D VIEW] Envs:", lucRuntime.listEnvs());
console.log("[3D VIEW] Windows:", Array.from(desktop.windows.keys()));
alert("3D view is future work — check console for topology stub.");
});
/***********************************************************
* SECTION: Taskbar binding
***********************************************************/
function updateTaskbar() {
const bar = document.getElementById("taskbar-windows");
bar.innerHTML = "";
desktop.windows.forEach((win, id) => {
const item = document.createElement("div");
item.className = "taskbar-item" + (win.minimized ? "" : " active");
item.textContent = win.title;
item.dataset.winId = id;
item.addEventListener("click", () => {
if (win.minimized) {
desktop.restoreWindow(id);
} else {
desktop.minimizeWindow(id);
}
updateTaskbar();
});
bar.appendChild(item);
});
}
function updateTaskbarActive(activeId) {
document.querySelectorAll(".taskbar-item").forEach(el => {
el.classList.toggle("active", el.dataset.winId === activeId);
});
}
/***********************************************************
* SECTION: Terminal App Implementation
***********************************************************/
class LucidiaTerminal {
constructor(rootEl) {
this.rootEl = rootEl;
this.outputEl = rootEl.querySelector(".terminal-output");
this.inputEl = rootEl.querySelector(".terminal-input");
this.history = [];
this.historyIndex = 0;
this.currentEnv = lucRuntime.getEnv("env.lab");
this.bindEvents();
this.print("Lucidia Terminal v0.1");
this.print('Type "help" to see commands.');
}
bindEvents() {
this.inputEl.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
const value = this.inputEl.value.trim();
if (!value) return;
this.inputEl.value = "";
this.print("> " + value);
this.history.push(value);
this.historyIndex = this.history.length;
this.handleCommand(value);
} else if (e.key === "ArrowUp") {
if (this.historyIndex > 0) {
this.historyIndex--;
this.inputEl.value = this.history[this.historyIndex] || "";
}
} else if (e.key === "ArrowDown") {
if (this.historyIndex < this.history.length - 1) {
this.historyIndex++;
this.inputEl.value = this.history[this.historyIndex] || "";
} else {
this.historyIndex = this.history.length;
this.inputEl.value = "";
}
}
});
}
print(text, type = "normal") {
if (text == null) return;
const line = document.createElement("div");
line.textContent = text;
if (type === "error") line.classList.add("error");
this.outputEl.appendChild(line);
this.outputEl.scrollTop = this.outputEl.scrollHeight;
}
clear() {
this.outputEl.innerHTML = "";
}
async handleCommand(input) {
const [command, sub, ...rest] = input.split(/\s+/);
const args = rest;
try {
switch (command) {
case "help":
this.print(`
env list - list environments
env current - show current env
env switch <id> - switch environment
app list - list apps in current env
app open <id> - open app window
flow list - list flows
flow run <id> - run a flow (stub)
clear - clear terminal
`.trim());
break;
case "env":
if (sub === "list") {
lucRuntime.listEnvs().forEach(e => {
this.print(`${e.id} (${e.name})${e.id === this.currentEnv?.id ? " [current]" : ""}`);
});
} else if (sub === "current") {
if (!this.currentEnv) this.print("No env set");
else this.print(`${this.currentEnv.id} (${this.currentEnv.name})`);
} else if (sub === "switch") {
const id = args[0];
const env = lucRuntime.getEnv(id);
if (!env) this.print(`No such env: ${id}`, "error");
else {
this.currentEnv = env;
desktop.setEnv(env);
this.print(`Switched to ${id}`);
}
} else {
this.print('Usage: env [list|current|switch <id>]', "error");
}
break;
case "app":
if (sub === "list") {
if (!this.currentEnv) {
this.print("No env set", "error");
break;
}
(this.currentEnv.apps || []).forEach(aId => {
const a = lucRuntime.getApp(aId);
this.print(`${aId} (${a?.name || "Unknown"})`);
});
} else if (sub === "open") {
const id = args[0];
if (!id) { this.print("Usage: app open <id>", "error"); break; }
desktop.openApp(id);
} else {
this.print('Usage: app [list|open <id>]', "error");
}
break;
case "flow":
if (sub === "list") {
lucRuntime.listFlows().forEach(f => {
this.print(`${f.id} (${f.name})`);
});
} else if (sub === "run") {
const id = args[0];
const flow = lucRuntime.getFlow(id);
if (!flow) { this.print(`No flow: ${id}`, "error"); break; }
this.print(`Running ${id} (stub)...`);
flow.steps.forEach(step => {
if (step.action === "log") {
this.print(step.message);
}
});
this.print(`${id} finished.`);
} else {
this.print('Usage: flow [list|run <id>]', "error");
}
break;
case "clear":
this.clear();
break;
default:
this.print(`Unknown command: ${input}`, "error");
this.print('Type "help" for commands.');
}
} catch (err) {
console.error(err);
this.print("Error: " + err.message, "error");
}
}
}
let terminalInstance = null;
function mountTerminal(container) {
container.innerHTML = `
<div class="terminal-output"></div>
<div class="terminal-input-row">
<span>lucidia&gt;</span>
<input class="terminal-input" autocomplete="off" />
</div>
`;
terminalInstance = new LucidiaTerminal(container);
}
/***********************************************************
* SECTION: Notes App (simple localStorage scratchpad)
***********************************************************/
function mountNotes(container) {
container.innerHTML = `
<div><strong>Notes</strong></div>
<textarea id="notes-area" style="width:100%;height:180px;box-sizing:border-box;font-size:12px;"></textarea>
`;
const area = container.querySelector("#notes-area");
area.value = localStorage.getItem("blackroad.notes") || "";
area.addEventListener("input", () => {
localStorage.setItem("blackroad.notes", area.value);
});
}
/***********************************************************
* SECTION: Clock + Boot
***********************************************************/
function startClock() {
const clockEl = document.getElementById("taskbar-clock");
function tick() {
const d = new Date();
const hh = String(d.getHours()).padStart(2, "0");
const mm = String(d.getMinutes()).padStart(2, "0");
clockEl.textContent = `${hh}:${mm}`;
}
tick();
setInterval(tick, 1000 * 30);
}
document.getElementById("start-button").addEventListener("click", () => {
alert("Start menu coming soon.\nFor now: use desktop icons or the Terminal.");
});
// Boot into env.lab on load
window.addEventListener("load", () => {
const env = lucRuntime.getEnv("env.lab");
desktop.setEnv(env);
startClock();
});
</script>
</body>
</html>