diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7636117 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# Node +node_modules/ +dist/ +.vite/ +dist-ssr/ +*.local + +# CMake +build/ +cmake-build-*/ + +# Compiled objects +*.o +*.so +*.a + +# IDE +.idea/ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +logs/ + +# Environment +.env +.env.* +!.env.example diff --git a/README.md b/README.md new file mode 100644 index 0000000..b9d7ac1 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# NebulaOS + +A Linux desktop environment built around a custom compositor, shell, and service stack. + +## Structure + +| Path | Purpose | +|------|---------| +| `core/nebula-core` | Core runtime and platform APIs | +| `compositor/` | Compositor configuration (Sway) | +| `shells/` | Desktop and bigscreen shell implementations | +| `packages/` | Shared UI libraries, icons, API, and design tokens | +| `services/` | System services (audio, network, bluetooth, power, accounts) | +| `session/` | Session manager and desktop entry | +| `packaging/` | Distribution packaging (Ubuntu) | +| `branding/` | Boot assets, wallpapers, and sounds | + +## Getting started + +Documentation and build instructions will be added as components land. diff --git a/branding/boot/.gitkeep b/branding/boot/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/branding/sounds/.gitkeep b/branding/sounds/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/branding/wallpapers/.gitkeep b/branding/wallpapers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/compositor/README.md b/compositor/README.md new file mode 100644 index 0000000..153f5b6 --- /dev/null +++ b/compositor/README.md @@ -0,0 +1,3 @@ +# Compositor + +NebulaOS uses Sway as its Wayland compositor. Configuration lives in `sway/nebula-sway.conf`. diff --git a/compositor/sway/nebula-sway.conf b/compositor/sway/nebula-sway.conf new file mode 100644 index 0000000..3f5afae --- /dev/null +++ b/compositor/sway/nebula-sway.conf @@ -0,0 +1,17 @@ +# NebulaOS Sway configuration + +include /etc/sway/config.d/* + +set $mod Mod4 + +input type:keyboard { + xkb_layout us +} + +output * bg #1a1a2e solid_color + +bindsym $mod+Return exec $term +bindsym $mod+Shift+q kill +bindsym $mod+d exec $menu + +exec_always nebula-session diff --git a/core/nebula-core/.gitkeep b/core/nebula-core/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages/nebula-api/.gitkeep b/packages/nebula-api/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages/nebula-icons/.gitkeep b/packages/nebula-icons/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages/nebula-tokens/.gitkeep b/packages/nebula-tokens/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages/nebula-ui/.gitkeep b/packages/nebula-ui/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packaging/ubuntu/.gitkeep b/packaging/ubuntu/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/services/accounts/.gitkeep b/services/accounts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/services/audio/.gitkeep b/services/audio/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/services/bluetooth/.gitkeep b/services/bluetooth/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/services/network/.gitkeep b/services/network/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/services/power/.gitkeep b/services/power/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/session/nebula-session b/session/nebula-session new file mode 100644 index 0000000..c573233 --- /dev/null +++ b/session/nebula-session @@ -0,0 +1,10 @@ +#!/bin/sh +# NebulaOS session startup + +set -e + +export XDG_CURRENT_DESKTOP=NebulaOS +export XDG_SESSION_DESKTOP=nebula + +# Shell and services are wired here as they land +# exec nebula-shell-desktop diff --git a/session/nebula.desktop b/session/nebula.desktop new file mode 100644 index 0000000..38d43dc --- /dev/null +++ b/session/nebula.desktop @@ -0,0 +1,6 @@ +[Desktop Entry] +Name=NebulaOS +Comment=NebulaOS desktop session +Exec=nebula-session +Type=Application +DesktopNames=NebulaOS diff --git a/shells/bigscreen/native/.gitkeep b/shells/bigscreen/native/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/shells/bigscreen/ui/.gitkeep b/shells/bigscreen/ui/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/shells/desktop/native/README.md b/shells/desktop/native/README.md new file mode 100644 index 0000000..3a006c4 --- /dev/null +++ b/shells/desktop/native/README.md @@ -0,0 +1,91 @@ +# Nebula Desktop native host + +This directory will contain the Linux native shell host for Nebula Desktop. + +The host is a C++ process that creates **several** GTK4 windows, each backed by WebKitGTK 6.0 and placed with **gtk4-layer-shell**. It does not composite application windows. Those remain ordinary Wayland / XWayland clients of the compositor. + +Planned stack: + +- C++ +- GTK4 +- gtk4-layer-shell +- WebKitGTK 6.0 +- Wayland + +## Surfaces + +The React UI in `shells/desktop/ui` can render each shell layer independently via a query parameter. The native host will create one WebKitGTK view per surface and append that parameter to the load URL. + +| Surface | Query | Layer Shell layer | Placement | +|-----------|-----------------------|-------------------|-----------| +| Desktop | `?surface=desktop` | `BACKGROUND` | Anchored to all monitor edges | +| Top Bar | `?surface=topbar` | `TOP` | Anchored top/left/right, fixed shell-defined height, exclusive zone enabled | +| Launcher | `?surface=launcher` | `OVERLAY` | Hidden/unmapped when closed | + +The host must not hardcode a Vite port. It loads a base URL and appends the surface parameter. + +## Stacking + +```text +OVERLAY + Nebula Launcher + notifications + OSDs + +TOP + Nebula Top Bar ← exclusive zone; apps do not go under it + +NORMAL APPLICATION WINDOWS + Firefox, Steam, Blender, games, terminals, … + +BACKGROUND + Nebula Desktop + wallpaper +``` + +React draws **shell chrome only**. Applications and games render through the compositor, not through WebKit. + +```text +Application / game + → Wayland / XWayland + → compositor + → GPU +``` + +## Loading modes + +### Development + +The host will load a configurable Vite origin, conceptually: + +```text +NEBULA_SHELL_DEV_URL=http://127.0.0.1:5174 +``` + +and request: + +```text +{NEBULA_SHELL_DEV_URL}/?surface=desktop +{NEBULA_SHELL_DEV_URL}/?surface=topbar +{NEBULA_SHELL_DEV_URL}/?surface=launcher +``` + +The port is environment configuration, not architecture. A combined browser preview (`/?surface=preview`, also the default) is for Windows UI work only; the native host should load the three surfaces separately. + +### Production + +The host will load the compiled assets from `npm run build` in `shells/desktop/ui/dist`, still using the same `?surface=` parameters. It will not depend on a Vite server. + +## Shell bridge + +Each WebKit view talks to the host through a JavaScript bridge (`shellBridge` in the React UI). The development build uses an in-page mock. The native host will inject the real implementation so the top bar can toggle the launcher, and so launching an app can unmap the overlay and update the top-bar context. + +`nebula.*` remains the API boundary for system services (apps, audio, network, power). Those stay mocked until Linux services land. + +## Current development compositor + +Sway is the temporary compositor. See `compositor/sway/nebula-sway.conf`. + +## Build notes + +This host is Linux-only. Do not expect it to compile on Windows. Implementation is the next Linux-side task. diff --git a/shells/desktop/ui/.gitignore b/shells/desktop/ui/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/shells/desktop/ui/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/shells/desktop/ui/.oxlintrc.json b/shells/desktop/ui/.oxlintrc.json new file mode 100644 index 0000000..1255078 --- /dev/null +++ b/shells/desktop/ui/.oxlintrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "oxc"], + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} diff --git a/shells/desktop/ui/README.md b/shells/desktop/ui/README.md new file mode 100644 index 0000000..176a033 --- /dev/null +++ b/shells/desktop/ui/README.md @@ -0,0 +1,30 @@ +# Nebula Desktop UI + +React UI for Nebula Desktop shell chrome. It does not host application or game windows. Those render as Wayland / XWayland surfaces. The native host in `shells/desktop/native` will load this UI in WebKitGTK. + +## Surfaces + +The same codebase can render each Wayland layer on its own: + +| URL | Renders | +|-----|---------| +| `/?surface=preview` (default) | Combined browser mockup | +| `/?surface=desktop` | Background layer only | +| `/?surface=topbar` | Top bar only | +| `/?surface=launcher` | Launcher only | + +The native host chooses the origin. Surface selection is a relative query parameter, not a hardcoded port. + +## Develop + +```powershell +npm install +npm run dev +``` + +```powershell +npm run lint +npm run build +``` + +Oxlint is the linter for this project. diff --git a/shells/desktop/ui/index.html b/shells/desktop/ui/index.html new file mode 100644 index 0000000..df30f2e --- /dev/null +++ b/shells/desktop/ui/index.html @@ -0,0 +1,14 @@ + + + + + + + + Nebula Desktop + + +
+ + + diff --git a/shells/desktop/ui/package-lock.json b/shells/desktop/ui/package-lock.json new file mode 100644 index 0000000..97bcd69 --- /dev/null +++ b/shells/desktop/ui/package-lock.json @@ -0,0 +1,1316 @@ +{ + "name": "ui", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ui", + "version": "0.0.0", + "dependencies": { + "react": "^19.2.8", + "react-dom": "^19.2.8" + }, + "devDependencies": { + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", + "@vitejs/plugin-react": "^6.1.0", + "oxlint": "^1.79.0", + "vite": "^8.2.2" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.146.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.146.0.tgz", + "integrity": "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.80.0.tgz", + "integrity": "sha512-RM3Plj+biQpxa5d1GOOX6ciDlcUROmm4OZ/pLTpitkQt2mJv4jhtY4cbgaetOm5UKWZe05/TGQ6o1Vl8EOHkrA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.80.0.tgz", + "integrity": "sha512-YlO5JEf0Yr2bUUlu8O8daVcUxtcGGbcSmyV7E7nSbJbfAdxTE0PFPwgnIlw7wXJaTYjb+qs5hI5q3jxUkI7cAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.80.0.tgz", + "integrity": "sha512-BULDOyO3AhsmdWfQeIUCykDt3dd7XZBGLhp1eIh56skRv01O+cNjNPwXMIbeW1x4+pxcln5if72wcRgViVo7PA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.80.0.tgz", + "integrity": "sha512-YJ4JzLw7N5TDSQFlA0hAQGHvnDZgyypm1yunObVWcWiF9KM7eGCJKYKLgTC2Fi/57OdnBhbj4OkzPGdFQJ6HyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.80.0.tgz", + "integrity": "sha512-AYUIk5QnL0s8oWAYsREZwkRYy1SupJTXALo93J1TgzHywxQtdM99FecRMQ87MXEdPQ0j1TmEpeeq3fGNkpvMqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.80.0.tgz", + "integrity": "sha512-9hBZVANupQ89W9dXyE0n8doCyaW5pDyGn3y6XlIMPZ+rIKuyqkr3SNUXmVJIhuvUq0NBU3RBiSXXE69l4XI6KA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.80.0.tgz", + "integrity": "sha512-SvS2uKqzY+pbfuvAHzH4338R6Zwo805GAwrIMVvK1KxoOWCIjZUdfzTCvilD7z6JK91v011+zYMryabhDo2AsQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.80.0.tgz", + "integrity": "sha512-tCLadyqRVL3pQTRPNg7cjXKvcvS4fbyXeQHhKk5BTJ1oftQln5/yIIWbu/Xom/DX41zv2P9QGt6+D/TtQVtY3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.80.0.tgz", + "integrity": "sha512-XfpCNRlOPcLlJl4Bn/FUhjqlR6BVavEykERBf/MV7YA9VZDa5g5znVqYhyviMafcxS9Pe/i/kPvHNO0U6svEHQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.80.0.tgz", + "integrity": "sha512-3I4yMwcFG9NeO8ioY6JBBuKsIm5GL/x7MATt1S4tVWaxPu5HcJ+XnLUbcVBTxG8q2Wu56HSj+NmXQiVYb1lp6A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.80.0.tgz", + "integrity": "sha512-E1wAKymkpe1/E8helzBKdm81OBOF+ezxRyXRMEuik3ZpWDER5CPOKZwF66RsdwW98uwZv8UTFremUQtC1CzdJA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.80.0.tgz", + "integrity": "sha512-+gLRGD4sIo3+VA++iham5UxD9tKSoJ/VOrROCEXIcknrYtQg6iIQgvjN0cpiRF7N6UYC7pJbvHJlDnMge5LRpQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.80.0.tgz", + "integrity": "sha512-aR0PrzHj9leW3NmzBAAP4EzdoBNoJcs9sjnIQPIwyRnBGYrRbXUIpEB5Q39AqK3PLY5JK5uEhDQDiUa1QSAstw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.80.0.tgz", + "integrity": "sha512-vSVh5cSo3Xxs6ghBCcFJlpbkbENzDog1qXtoXLa/HC3aCrR4XO76GZbXmQoCPHnu99nQpdCeC3H9tdNICfDh7A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.80.0.tgz", + "integrity": "sha512-FfzBXpNQ8u7/ZI/p8bl73MeZ508Ax3hxWp3SiJpEFiC+BB9XcXy5FAZHTLKDPSzrUpxQZSZJAVdDmuJp/+HDBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.80.0.tgz", + "integrity": "sha512-zMzbkumtmprCgRwoYNzcB3iC39fXdJIMLMU33KdCjEGLlJGOEt1+LwQ4LF8ndLzAEKVz4BR0y3V6Xrkk3Nm3yA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.80.0.tgz", + "integrity": "sha512-ib6iRcrXsk4t1fm3iKcwksyWh1ZkZXC/2mEzakl0ai2+6HZunf1WWMZ/xP9EJAvw9g9K4UVTC3NF/+G2qLrbTQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.80.0.tgz", + "integrity": "sha512-xhRWBMpLxZvgKAH6+DJZmpP+W8Y8UdQOSU1JfxSWNXsaBaRGW77j+1hCuNHlzj7OH4SPN8fYd1q0o2qrDtoVyw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.80.0.tgz", + "integrity": "sha512-yAnO7lwBYQnz2pcfBPIGQQZWIX5zd5R/1aAKIF3oE+TVj7IhoHcROjOkz3sRDngzqhfPKfFaXqug5j5rE5dn6Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.5.tgz", + "integrity": "sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.5.tgz", + "integrity": "sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.5.tgz", + "integrity": "sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.5.tgz", + "integrity": "sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.5.tgz", + "integrity": "sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.5.tgz", + "integrity": "sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.5.tgz", + "integrity": "sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.5.tgz", + "integrity": "sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.5.tgz", + "integrity": "sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.5.tgz", + "integrity": "sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.5.tgz", + "integrity": "sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.5.tgz", + "integrity": "sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.5.tgz", + "integrity": "sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.5.tgz", + "integrity": "sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.5.tgz", + "integrity": "sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.0.tgz", + "integrity": "sha512-qd2BzUBehkov86WFhg0JkEFEYyCLG9uPCe6qWTY/kRlss9OvJrOF2UbIWT7p+8IzZHkEu0DNGHc4HSv+JdDLsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "oxc-transform-react": "^0.145.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "oxc-transform-react": { + "optional": true + } + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/oxlint": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.80.0.tgz", + "integrity": "sha512-5nTiSps4qdbCWLbxzuO00alHkEO2exR9YMN/ig6QXWrLsYSG0KaObOAM+l6oU2LcKPWoSAGYbkZIGEu1ViiWKA==", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.80.0", + "@oxlint/binding-android-arm64": "1.80.0", + "@oxlint/binding-darwin-arm64": "1.80.0", + "@oxlint/binding-darwin-x64": "1.80.0", + "@oxlint/binding-freebsd-x64": "1.80.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.80.0", + "@oxlint/binding-linux-arm-musleabihf": "1.80.0", + "@oxlint/binding-linux-arm64-gnu": "1.80.0", + "@oxlint/binding-linux-arm64-musl": "1.80.0", + "@oxlint/binding-linux-ppc64-gnu": "1.80.0", + "@oxlint/binding-linux-riscv64-gnu": "1.80.0", + "@oxlint/binding-linux-riscv64-musl": "1.80.0", + "@oxlint/binding-linux-s390x-gnu": "1.80.0", + "@oxlint/binding-linux-x64-gnu": "1.80.0", + "@oxlint/binding-linux-x64-musl": "1.80.0", + "@oxlint/binding-openharmony-arm64": "1.80.0", + "@oxlint/binding-win32-arm64-msvc": "1.80.0", + "@oxlint/binding-win32-ia32-msvc": "1.80.0", + "@oxlint/binding-win32-x64-msvc": "1.80.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=7.0.2001", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/rolldown": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.5.tgz", + "integrity": "sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.146.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.5", + "@rolldown/binding-android-arm64": "1.2.5", + "@rolldown/binding-darwin-arm64": "1.2.5", + "@rolldown/binding-darwin-x64": "1.2.5", + "@rolldown/binding-freebsd-x64": "1.2.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.5", + "@rolldown/binding-linux-arm64-gnu": "1.2.5", + "@rolldown/binding-linux-arm64-musl": "1.2.5", + "@rolldown/binding-linux-ppc64-gnu": "1.2.5", + "@rolldown/binding-linux-s390x-gnu": "1.2.5", + "@rolldown/binding-linux-x64-gnu": "1.2.5", + "@rolldown/binding-linux-x64-musl": "1.2.5", + "@rolldown/binding-openharmony-arm64": "1.2.5", + "@rolldown/binding-win32-arm64-msvc": "1.2.5", + "@rolldown/binding-win32-x64-msvc": "1.2.5" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/vite": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0 || ^0.5.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/shells/desktop/ui/package.json b/shells/desktop/ui/package.json new file mode 100644 index 0000000..2690b15 --- /dev/null +++ b/shells/desktop/ui/package.json @@ -0,0 +1,23 @@ +{ + "name": "nebula-desktop-ui", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "oxlint", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.8", + "react-dom": "^19.2.8" + }, + "devDependencies": { + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", + "@vitejs/plugin-react": "^6.1.0", + "oxlint": "^1.79.0", + "vite": "^8.2.2" + } +} diff --git a/shells/desktop/ui/public/favicon.svg b/shells/desktop/ui/public/favicon.svg new file mode 100644 index 0000000..4d1807c --- /dev/null +++ b/shells/desktop/ui/public/favicon.svg @@ -0,0 +1,13 @@ + + + + + diff --git a/shells/desktop/ui/src/App.jsx b/shells/desktop/ui/src/App.jsx new file mode 100644 index 0000000..dc118d4 --- /dev/null +++ b/shells/desktop/ui/src/App.jsx @@ -0,0 +1,18 @@ +import { getSurfaceName, SURFACES } from './lib/surface.js'; +import { DesktopSurface } from './surfaces/DesktopSurface.jsx'; +import { LauncherSurface } from './surfaces/LauncherSurface.jsx'; +import { PreviewSurface } from './surfaces/PreviewSurface.jsx'; +import { TopBarSurface } from './surfaces/TopBarSurface.jsx'; + +const SURFACE_VIEWS = { + [SURFACES.DESKTOP]: DesktopSurface, + [SURFACES.TOPBAR]: TopBarSurface, + [SURFACES.LAUNCHER]: LauncherSurface, + [SURFACES.PREVIEW]: PreviewSurface, +}; + +export default function App() { + const surface = getSurfaceName(); + const View = SURFACE_VIEWS[surface] ?? PreviewSurface; + return ; +} diff --git a/shells/desktop/ui/src/components/desktop/Desktop.css b/shells/desktop/ui/src/components/desktop/Desktop.css new file mode 100644 index 0000000..6d79ff8 --- /dev/null +++ b/shells/desktop/ui/src/components/desktop/Desktop.css @@ -0,0 +1,43 @@ +.desktop { + position: relative; + width: 100%; + height: 100%; + overflow: hidden; +} + +/* CSS stand-in for the future wallpaper service. Swap this layer later. */ +.desktop-backdrop { + position: absolute; + inset: 0; + background-color: var(--nebula-bg); + background-image: + radial-gradient( + ellipse 90% 70% at 12% -8%, + rgba(110, 140, 180, 0.16), + transparent 52% + ), + radial-gradient( + ellipse 55% 45% at 88% 108%, + rgba(42, 68, 98, 0.28), + transparent 58% + ), + radial-gradient( + ellipse 40% 32% at 62% 38%, + rgba(72, 92, 120, 0.08), + transparent 70% + ); +} + +.desktop-backdrop::after { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient( + 180deg, + rgba(8, 11, 17, 0.22) 0%, + transparent 16%, + transparent 78%, + rgba(8, 11, 17, 0.38) 100% + ); + pointer-events: none; +} diff --git a/shells/desktop/ui/src/components/desktop/Desktop.jsx b/shells/desktop/ui/src/components/desktop/Desktop.jsx new file mode 100644 index 0000000..8b28dec --- /dev/null +++ b/shells/desktop/ui/src/components/desktop/Desktop.jsx @@ -0,0 +1,13 @@ +import './Desktop.css'; + +/** + * Desktop / wallpaper layer only. + * Top bar and launcher are separate shell surfaces. + */ +export function Desktop() { + return ( +
+ + ); +} diff --git a/shells/desktop/ui/src/components/icons.jsx b/shells/desktop/ui/src/components/icons.jsx new file mode 100644 index 0000000..1600be3 --- /dev/null +++ b/shells/desktop/ui/src/components/icons.jsx @@ -0,0 +1,309 @@ +function Svg({ children, size = 16, ...props }) { + return ( + + ); +} + +export function NebulaMark({ size = 18 }) { + return ( + + + + + ); +} + +export function SearchIcon({ size = 16 }) { + return ( + + + + + ); +} + +export function WifiIcon({ strength = 3, size = 16 }) { + const bars = [ + 'M4.8 15.2c4-4 10.4-4 14.4 0', + 'M7.4 17.4c2.6-2.5 6.6-2.5 9.2 0', + ]; + + return ( + + {bars.map((d, index) => ( + index ? 1 : 0.28} + /> + ))} + + + ); +} + +export function SpeakerIcon({ size = 16 }) { + return ( + + + + + + ); +} + +export function BatteryIcon({ percent = 84, size = 16 }) { + const fillWidth = Math.max(1.5, (14 * percent) / 100); + + return ( + + + + + + ); +} + +export function PowerIcon({ size = 16 }) { + return ( + + + + + ); +} + +export function RestartIcon({ size = 16 }) { + return ( + + + + + ); +} + +export function FolderGlyph({ size = 18 }) { + return ( + + + + ); +} + +export function GlobeGlyph({ size = 18 }) { + return ( + + + + + + ); +} + +export function TerminalGlyph({ size = 18 }) { + return ( + + + + + ); +} + +export function SettingsGlyph({ size = 18 }) { + return ( + + + + + ); +} + +export function GameGlyph({ size = 18 }) { + return ( + + + + + + + ); +} + +export function PhotoGlyph({ size = 18 }) { + return ( + + + + + + ); +} + +export function MusicGlyph({ size = 18 }) { + return ( + + + + + + ); +} + +export function DocumentGlyph({ size = 18 }) { + return ( + + + + + + ); +} + +const APP_GLYPHS = { + folder: FolderGlyph, + globe: GlobeGlyph, + terminal: TerminalGlyph, + settings: SettingsGlyph, + game: GameGlyph, + photo: PhotoGlyph, + music: MusicGlyph, + document: DocumentGlyph, +}; + +export function AppGlyph({ name, size = 18 }) { + const Glyph = APP_GLYPHS[name] ?? FolderGlyph; + return ; +} diff --git a/shells/desktop/ui/src/components/launcher/AppItem.jsx b/shells/desktop/ui/src/components/launcher/AppItem.jsx new file mode 100644 index 0000000..a57edb2 --- /dev/null +++ b/shells/desktop/ui/src/components/launcher/AppItem.jsx @@ -0,0 +1,26 @@ +import { AppGlyph } from '../icons.jsx'; + +export function AppItem({ item, variant = 'tile', onSelect }) { + const isRow = variant === 'row'; + + return ( + + ); +} diff --git a/shells/desktop/ui/src/components/launcher/Launcher.css b/shells/desktop/ui/src/components/launcher/Launcher.css new file mode 100644 index 0000000..a638b6c --- /dev/null +++ b/shells/desktop/ui/src/components/launcher/Launcher.css @@ -0,0 +1,173 @@ +.launcher { + display: flex; + flex-direction: column; + width: min(420px, calc(100vw - 20px)); + max-height: min(640px, calc(100vh - 24px)); + padding: 14px; +} + +.launcher-body { + display: flex; + flex-direction: column; + gap: 18px; + min-height: 0; + margin: 14px 0; + overflow: auto; +} + +.launcher-section h2 { + margin: 0 0 8px; + color: var(--nebula-text-secondary); + font-size: 11px; + font-weight: 600; + letter-spacing: 0.04em; +} + +.launcher-recent, +.launcher-grid { + display: grid; + gap: 6px; +} + +.launcher-recent { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.launcher-grid { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.launcher-projects { + display: flex; + flex-direction: column; + gap: 2px; +} + +.launcher-empty { + margin: 8px 0 0; + color: var(--nebula-text-tertiary); +} + +.app-item { + appearance: none; + border: 0; + background: transparent; + color: inherit; + text-align: left; + border-radius: var(--nebula-radius-medium); + transition: background var(--nebula-transition-fast); +} + +.app-item:hover, +.app-item:focus-visible { + background: rgba(255, 255, 255, 0.05); +} + +.app-item--tile, +.app-item--recent { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + padding: 10px 6px 8px; +} + +.app-item--row { + display: flex; + flex-direction: row; + align-items: center; + gap: 10px; + width: 100%; + padding: 8px; +} + +.app-item-icon { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border-radius: 11px; + background: rgba(255, 255, 255, 0.05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06); +} + +.app-item--recent .app-item-icon, +.app-item--row .app-item-icon { + width: 32px; + height: 32px; + border-radius: 9px; +} + +.app-item-copy { + display: flex; + flex-direction: column; + min-width: 0; +} + +.app-item-name { + overflow: hidden; + max-width: 100%; + font-size: 12px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.app-item--tile .app-item-name, +.app-item--recent .app-item-name { + text-align: center; +} + +.app-item-subtitle { + color: var(--nebula-text-tertiary); + font-size: 11px; +} + +.launcher-footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding-top: 10px; + border-top: 1px solid var(--nebula-border); +} + +.launcher-user { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} + +.launcher-avatar { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 50%; + background: var(--nebula-accent-soft); + color: var(--nebula-accent-strong); + font-size: 12px; + font-weight: 600; +} + +.launcher-user-copy { + display: flex; + flex-direction: column; + min-width: 0; +} + +.launcher-user-name { + font-weight: 600; +} + +.launcher-user-meta { + color: var(--nebula-text-tertiary); + font-size: 11px; +} + +.launcher-power { + display: flex; + gap: 2px; +} diff --git a/shells/desktop/ui/src/components/launcher/Launcher.jsx b/shells/desktop/ui/src/components/launcher/Launcher.jsx new file mode 100644 index 0000000..cb9d699 --- /dev/null +++ b/shells/desktop/ui/src/components/launcher/Launcher.jsx @@ -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 ( +
+ + +
+ {!searching && recents.length > 0 ? ( +
+

Recent

+
+ {recents.map((app) => ( + + ))} +
+
+ ) : null} + +
+

{searching ? 'Results' : 'Applications'}

+ {filteredApps.length > 0 ? ( +
+ {filteredApps.map((app) => ( + + ))} +
+ ) : ( +

No matching applications

+ )} +
+ + {!searching && projects.length > 0 ? ( +
+

Recent projects

+
+ {projects.map((project) => ( + + ))} +
+
+ ) : null} +
+ +
+
+ + + {user?.name ?? 'User'} + Local session + +
+
+ { + nebula.power.restart(); + }} + > + + + { + nebula.power.shutdown(); + }} + > + + +
+
+
+ ); +} diff --git a/shells/desktop/ui/src/components/system/Clock.jsx b/shells/desktop/ui/src/components/system/Clock.jsx new file mode 100644 index 0000000..5e59c20 --- /dev/null +++ b/shells/desktop/ui/src/components/system/Clock.jsx @@ -0,0 +1,24 @@ +import { useEffect, useState } from 'react'; +import { nebula } from '../../lib/nebula.js'; + +export function Clock() { + const [now, setNow] = useState(() => nebula.time.now()); + + useEffect(() => { + const timer = window.setInterval(() => { + setNow(nebula.time.now()); + }, 1000); + + return () => window.clearInterval(timer); + }, []); + + return ( + + ); +} diff --git a/shells/desktop/ui/src/components/system/SystemArea.css b/shells/desktop/ui/src/components/system/SystemArea.css new file mode 100644 index 0000000..d374f67 --- /dev/null +++ b/shells/desktop/ui/src/components/system/SystemArea.css @@ -0,0 +1,19 @@ +.system-area { + display: flex; + align-items: center; + gap: 2px; +} + +.clock { + display: inline-flex; + align-items: center; + min-width: 7.4em; + height: 28px; + margin-left: 4px; + padding: 0 8px; + border-radius: var(--nebula-radius-small); + color: var(--nebula-text); + font-variant-numeric: tabular-nums; + letter-spacing: 0.01em; + white-space: nowrap; +} diff --git a/shells/desktop/ui/src/components/system/SystemArea.jsx b/shells/desktop/ui/src/components/system/SystemArea.jsx new file mode 100644 index 0000000..c8ab07f --- /dev/null +++ b/shells/desktop/ui/src/components/system/SystemArea.jsx @@ -0,0 +1,59 @@ +import { useEffect, useState } from 'react'; +import { BatteryIcon, SpeakerIcon, WifiIcon } from '../icons.jsx'; +import { nebula } from '../../lib/nebula.js'; +import { IconButton } from '../ui/IconButton.jsx'; +import { Clock } from './Clock.jsx'; +import './SystemArea.css'; + +export function SystemArea() { + const [network, setNetwork] = useState(null); + const [volume, setVolume] = useState(null); + const [power, setPower] = useState(null); + + useEffect(() => { + let cancelled = false; + + Promise.all([ + nebula.network.getState(), + nebula.audio.getVolume(), + nebula.power.getState(), + ]).then(([networkState, volumeState, powerState]) => { + if (cancelled) { + return; + } + + setNetwork(networkState); + setVolume(volumeState); + setPower(powerState); + }); + + return () => { + cancelled = true; + }; + }, []); + + const networkLabel = network?.connected + ? `Network connected, ${network.name}` + : 'Network disconnected'; + const volumeLabel = + volume == null ? 'Audio' : `Volume ${Math.round(volume)}%`; + const powerLabel = + power == null + ? 'Power' + : `Battery ${Math.round(power.percent)}%${power.charging ? ', charging' : ''}`; + + return ( +
+ + + + + + + + + + +
+ ); +} diff --git a/shells/desktop/ui/src/components/topbar/TopBar.css b/shells/desktop/ui/src/components/topbar/TopBar.css new file mode 100644 index 0000000..9a2c285 --- /dev/null +++ b/shells/desktop/ui/src/components/topbar/TopBar.css @@ -0,0 +1,60 @@ +.topbar { + position: relative; + z-index: 20; + display: flex; + align-items: center; + justify-content: space-between; + height: var(--nebula-topbar-height); + padding: 0 10px 0 6px; + background: rgba(10, 13, 18, 0.55); + border-bottom: 1px solid var(--nebula-border); + backdrop-filter: blur(20px) saturate(1.15); + -webkit-backdrop-filter: blur(20px) saturate(1.15); + user-select: none; +} + +.topbar-left { + display: flex; + align-items: center; + min-width: 0; + gap: 8px; +} + +.nebula-menu-button { + appearance: none; + display: inline-flex; + align-items: center; + gap: 8px; + height: 28px; + padding: 0 10px 0 8px; + border: 0; + border-radius: var(--nebula-radius-small); + background: transparent; + color: var(--nebula-text); + font-weight: 600; + letter-spacing: 0.01em; + transition: background var(--nebula-transition-fast); +} + +.nebula-menu-button:hover, +.nebula-menu-button.is-open { + background: var(--nebula-accent-soft); +} + +.nebula-menu-button.is-open { + color: var(--nebula-accent-strong); +} + +.topbar-divider { + width: 1px; + height: 14px; + background: var(--nebula-border-strong); +} + +.topbar-context { + min-width: 0; + overflow: hidden; + color: var(--nebula-text-secondary); + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/shells/desktop/ui/src/components/topbar/TopBar.jsx b/shells/desktop/ui/src/components/topbar/TopBar.jsx new file mode 100644 index 0000000..32978f7 --- /dev/null +++ b/shells/desktop/ui/src/components/topbar/TopBar.jsx @@ -0,0 +1,26 @@ +import { NebulaMark } from '../icons.jsx'; +import { SystemArea } from '../system/SystemArea.jsx'; +import './TopBar.css'; + +export function TopBar({ launcherOpen, onToggleLauncher, contextName }) { + return ( +
+
+ +
+ +
+ ); +} diff --git a/shells/desktop/ui/src/components/ui/IconButton.jsx b/shells/desktop/ui/src/components/ui/IconButton.jsx new file mode 100644 index 0000000..d5f9251 --- /dev/null +++ b/shells/desktop/ui/src/components/ui/IconButton.jsx @@ -0,0 +1,29 @@ +import './controls.css'; + +export function IconButton({ + label, + active = false, + className = '', + children, + ...props +}) { + const classes = [ + 'nebula-icon-button', + active ? 'is-active' : '', + className, + ] + .filter(Boolean) + .join(' '); + + return ( + + ); +} diff --git a/shells/desktop/ui/src/components/ui/SearchField.jsx b/shells/desktop/ui/src/components/ui/SearchField.jsx new file mode 100644 index 0000000..06f50c6 --- /dev/null +++ b/shells/desktop/ui/src/components/ui/SearchField.jsx @@ -0,0 +1,25 @@ +import { SearchIcon } from '../icons.jsx'; +import './controls.css'; + +export function SearchField({ + value, + onChange, + placeholder = 'Search', + inputRef, +}) { + return ( + + ); +} diff --git a/shells/desktop/ui/src/components/ui/controls.css b/shells/desktop/ui/src/components/ui/controls.css new file mode 100644 index 0000000..57b4982 --- /dev/null +++ b/shells/desktop/ui/src/components/ui/controls.css @@ -0,0 +1,65 @@ +.nebula-icon-button { + appearance: none; + border: 0; + background: transparent; + color: var(--nebula-text-secondary); + width: 28px; + height: 28px; + padding: 0; + border-radius: var(--nebula-radius-small); + display: inline-flex; + align-items: center; + justify-content: center; + transition: + background var(--nebula-transition-fast), + color var(--nebula-transition-fast); +} + +.nebula-icon-button:hover { + background: rgba(255, 255, 255, 0.06); + color: var(--nebula-text); +} + +.nebula-icon-button.is-active { + background: var(--nebula-accent-soft); + color: var(--nebula-accent-strong); +} + +.nebula-icon-button--danger:hover { + color: var(--nebula-danger); +} + +.nebula-search { + display: flex; + align-items: center; + gap: 10px; + height: 36px; + padding: 0 12px; + border-radius: var(--nebula-radius-medium); + background: rgba(8, 11, 17, 0.45); + border: 1px solid var(--nebula-border); + color: var(--nebula-text-secondary); +} + +.nebula-search:focus-within { + border-color: var(--nebula-border-strong); + color: var(--nebula-text); +} + +.nebula-search input { + flex: 1; + min-width: 0; + border: 0; + background: transparent; + color: var(--nebula-text); + padding: 0; +} + +.nebula-search input::placeholder { + color: var(--nebula-text-tertiary); +} + +.nebula-search input::-webkit-search-decoration, +.nebula-search input::-webkit-search-cancel-button { + appearance: none; +} diff --git a/shells/desktop/ui/src/data/mockApps.js b/shells/desktop/ui/src/data/mockApps.js new file mode 100644 index 0000000..f0716d6 --- /dev/null +++ b/shells/desktop/ui/src/data/mockApps.js @@ -0,0 +1,102 @@ +/** + * Placeholder application catalogue for the desktop shell prototype. + * + * Real application discovery will later replace this module via + * `nebula.apps.list()` from native Nebula Core. + */ + +export const mockApps = [ + { + id: 'files', + name: 'Files', + comment: 'Browse files and folders', + category: 'system', + recent: true, + glyph: 'folder', + tint: '#c4a574', + }, + { + id: 'firefox', + name: 'Firefox', + comment: 'Web browser', + category: 'internet', + recent: true, + glyph: 'globe', + tint: '#d08a5a', + }, + { + id: 'terminal', + name: 'Terminal', + comment: 'Command line', + category: 'system', + recent: true, + glyph: 'terminal', + tint: '#7eaa8e', + }, + { + id: 'settings', + name: 'Settings', + comment: 'System preferences', + category: 'system', + recent: false, + glyph: 'settings', + tint: '#8e99ab', + }, + { + id: 'steam', + name: 'Steam', + comment: 'Games', + category: 'games', + recent: true, + glyph: 'game', + tint: '#6f8fb8', + }, + { + id: 'photos', + name: 'Photos', + comment: 'Pictures and albums', + category: 'media', + recent: false, + glyph: 'photo', + tint: '#8aa8c4', + }, + { + id: 'music', + name: 'Music', + comment: 'Listen and organise', + category: 'media', + recent: false, + glyph: 'music', + tint: '#b48a9a', + }, + { + id: 'text-editor', + name: 'Text Editor', + comment: 'Edit documents', + category: 'productivity', + recent: false, + glyph: 'document', + tint: '#9aa7bc', + }, +]; + +export const mockProjects = [ + { + id: 'nebulaos', + name: 'NebulaOS', + subtitle: 'Repository', + kind: 'folder', + }, + { + id: 'website', + name: 'Website', + subtitle: 'Project', + kind: 'folder', + }, + { + id: 'session-notes', + name: 'Session notes', + subtitle: 'Document', + kind: 'document', + }, +]; diff --git a/shells/desktop/ui/src/lib/nebula.js b/shells/desktop/ui/src/lib/nebula.js new file mode 100644 index 0000000..784b4bf --- /dev/null +++ b/shells/desktop/ui/src/lib/nebula.js @@ -0,0 +1,121 @@ +/** + * Nebula JavaScript API + * + * Boundary between the React shell UI and the future native Nebula Core: + * + * React UI → nebula.* → native host / Nebula Core + * + * Every method here is a placeholder. Implementations must not call Node + * APIs, spawn processes, or talk to D-Bus, PipeWire, NetworkManager, or + * Wayland. The WebKitGTK host will later inject a real bridge. + * + * The shell is chrome only. Applications and games render as compositor + * surfaces, not inside this React tree. + */ + +import { mockApps, mockProjects } from '../data/mockApps.js'; +import { formatClock, formatFullDateTime } from './time.js'; + +const state = { + volume: 72, + network: { + connected: true, + type: 'wifi', + name: 'Nebula-Net', + strength: 3, + }, + power: { + onBattery: true, + charging: false, + percent: 84, + }, + session: { + user: { + name: 'User', + initials: 'N', + }, + }, +}; + +export const nebula = { + apps: { + async list() { + return mockApps.map((app) => ({ ...app })); + }, + + async recents() { + return mockApps.filter((app) => app.recent).map((app) => ({ ...app })); + }, + + async launch(id) { + console.info(`[nebula] launch ${id}`); + return { ok: true, id }; + }, + }, + + projects: { + async recents() { + return mockProjects.map((project) => ({ ...project })); + }, + + async open(id) { + console.info(`[nebula] open project ${id}`); + return { ok: true, id }; + }, + }, + + audio: { + async getVolume() { + return state.volume; + }, + + async setVolume(value) { + const next = Math.min(100, Math.max(0, Number(value))); + state.volume = next; + console.info(`[nebula] volume ${next}`); + return next; + }, + }, + + network: { + async getState() { + return { ...state.network }; + }, + }, + + power: { + async getState() { + return { ...state.power }; + }, + + async shutdown() { + console.info('[nebula] shutdown requested'); + }, + + async restart() { + console.info('[nebula] restart requested'); + }, + }, + + session: { + async getUser() { + return { ...state.session.user }; + }, + }, + + time: { + now() { + return new Date(); + }, + + formatClock(date = new Date()) { + return formatClock(date); + }, + + formatFull(date = new Date()) { + return formatFullDateTime(date); + }, + }, +}; + +export default nebula; diff --git a/shells/desktop/ui/src/lib/shellBridge.js b/shells/desktop/ui/src/lib/shellBridge.js new file mode 100644 index 0000000..04d7d12 --- /dev/null +++ b/shells/desktop/ui/src/lib/shellBridge.js @@ -0,0 +1,98 @@ +/** + * Shell coordination bridge. + * + * Top bar, launcher, and desktop will later live in separate WebKitGTK views. + * They must not share React component state. The native host will replace this + * module with an injected C++ bridge. + * + * This development mock keeps in-page state and broadcasts it with window + * CustomEvents. No networking, WebSockets, or Node APIs. + */ + +const SHELL_EVENT = 'nebula:shell-state'; + +let state = { + launcherOpen: false, + contextName: 'Desktop', +}; + +function snapshot() { + return { + launcherOpen: state.launcherOpen, + contextName: state.contextName, + }; +} + +function publish() { + const next = snapshot(); + window.dispatchEvent(new CustomEvent(SHELL_EVENT, { detail: next })); +} + +export const shellBridge = { + getState() { + return snapshot(); + }, + + getLauncherOpen() { + return state.launcherOpen; + }, + + getContextName() { + return state.contextName; + }, + + openLauncher() { + if (state.launcherOpen) { + return; + } + + state = { ...state, launcherOpen: true }; + publish(); + }, + + closeLauncher() { + if (!state.launcherOpen) { + return; + } + + state = { ...state, launcherOpen: false }; + publish(); + }, + + toggleLauncher() { + state = { ...state, launcherOpen: !state.launcherOpen }; + publish(); + }, + + setContext(name) { + if (state.contextName === name) { + return; + } + + state = { ...state, contextName: name }; + publish(); + }, + + subscribe(callback) { + function onShellState(event) { + callback(event.detail); + } + + window.addEventListener(SHELL_EVENT, onShellState); + return () => window.removeEventListener(SHELL_EVENT, onShellState); + }, + + onLauncherChanged(callback) { + return shellBridge.subscribe((next) => { + callback(next.launcherOpen); + }); + }, + + onContextChanged(callback) { + return shellBridge.subscribe((next) => { + callback(next.contextName); + }); + }, +}; + +export default shellBridge; diff --git a/shells/desktop/ui/src/lib/surface.js b/shells/desktop/ui/src/lib/surface.js new file mode 100644 index 0000000..7d0d38b --- /dev/null +++ b/shells/desktop/ui/src/lib/surface.js @@ -0,0 +1,46 @@ +/** + * Shell surface routing. + * + * The native host will load this UI in separate WebKitGTK views, each with a + * surface query parameter. Never hardcode a host or port — the loader decides + * the URL; this module only reads the current location. + */ + +export const SURFACES = { + DESKTOP: 'desktop', + TOPBAR: 'topbar', + LAUNCHER: 'launcher', + PREVIEW: 'preview', +}; + +const KNOWN_SURFACES = new Set(Object.values(SURFACES)); + +const SURFACE_TITLES = { + [SURFACES.DESKTOP]: 'Nebula Desktop', + [SURFACES.TOPBAR]: 'Nebula Top Bar', + [SURFACES.LAUNCHER]: 'Nebula Launcher', + [SURFACES.PREVIEW]: 'Nebula Desktop', +}; + +export function getSurfaceName(location = window.location) { + const url = new URL(location.href); + const value = url.searchParams.get('surface'); + + if (!value) { + return SURFACES.PREVIEW; + } + + if (KNOWN_SURFACES.has(value)) { + return value; + } + + return SURFACES.PREVIEW; +} + +export function applySurfaceToDocument( + surface = getSurfaceName(), + doc = document, +) { + doc.documentElement.dataset.surface = surface; + doc.title = SURFACE_TITLES[surface] ?? SURFACE_TITLES[SURFACES.PREVIEW]; +} diff --git a/shells/desktop/ui/src/lib/time.js b/shells/desktop/ui/src/lib/time.js new file mode 100644 index 0000000..0aa80ad --- /dev/null +++ b/shells/desktop/ui/src/lib/time.js @@ -0,0 +1,34 @@ +/** + * Locale-aware clock formatting. + * + * Isolated from the Clock component so a future Nebula system API can + * replace Intl without touching the shell UI. + */ + +export function getUserLocale() { + if (typeof navigator !== 'undefined' && navigator.language) { + return navigator.language; + } + + return 'en-US'; +} + +export function formatClock(date, locale = getUserLocale()) { + return new Intl.DateTimeFormat(locale, { + weekday: 'short', + hour: 'numeric', + minute: '2-digit', + }).format(date); +} + +export function formatFullDateTime(date, locale = getUserLocale()) { + return new Intl.DateTimeFormat(locale, { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + second: '2-digit', + }).format(date); +} diff --git a/shells/desktop/ui/src/lib/useShellState.js b/shells/desktop/ui/src/lib/useShellState.js new file mode 100644 index 0000000..0234010 --- /dev/null +++ b/shells/desktop/ui/src/lib/useShellState.js @@ -0,0 +1,10 @@ +import { useEffect, useState } from 'react'; +import { shellBridge } from './shellBridge.js'; + +export function useShellState() { + const [state, setState] = useState(() => shellBridge.getState()); + + useEffect(() => shellBridge.subscribe(setState), []); + + return state; +} diff --git a/shells/desktop/ui/src/main.jsx b/shells/desktop/ui/src/main.jsx new file mode 100644 index 0000000..f3fc7ae --- /dev/null +++ b/shells/desktop/ui/src/main.jsx @@ -0,0 +1,13 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App.jsx'; +import { applySurfaceToDocument } from './lib/surface.js'; +import './styles/globals.css'; + +applySurfaceToDocument(); + +createRoot(document.getElementById('root')).render( + + + , +); diff --git a/shells/desktop/ui/src/styles/globals.css b/shells/desktop/ui/src/styles/globals.css new file mode 100644 index 0000000..a33bfde --- /dev/null +++ b/shells/desktop/ui/src/styles/globals.css @@ -0,0 +1,79 @@ +@import './tokens.css'; + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body, +#root { + width: 100%; + height: 100%; + margin: 0; +} + +html { + color-scheme: dark; + background: transparent; +} + +html[data-surface='desktop'], +html[data-surface='preview'] { + background: var(--nebula-bg); +} + +body { + font-family: var(--nebula-font); + font-size: 13px; + line-height: 1.35; + color: var(--nebula-text); + background: transparent; + overflow: hidden; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +button, +input { + font: inherit; + color: inherit; +} + +button { + cursor: pointer; +} + +img, +svg { + display: block; +} + +:focus { + outline: none; +} + +:focus-visible { + outline: 2px solid var(--nebula-focus); + outline-offset: 2px; +} + +.nebula-surface { + background: var(--nebula-surface); + border: 1px solid var(--nebula-border); + border-radius: var(--nebula-radius-large); + box-shadow: var(--nebula-shadow); + backdrop-filter: blur(var(--nebula-blur)); + -webkit-backdrop-filter: blur(var(--nebula-blur)); +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} diff --git a/shells/desktop/ui/src/styles/tokens.css b/shells/desktop/ui/src/styles/tokens.css new file mode 100644 index 0000000..65b2eeb --- /dev/null +++ b/shells/desktop/ui/src/styles/tokens.css @@ -0,0 +1,33 @@ +:root { + --nebula-bg: #0b0f16; + --nebula-bg-deep: #080b11; + --nebula-surface: rgba(20, 26, 36, 0.78); + --nebula-surface-raised: rgba(32, 40, 54, 0.9); + --nebula-border: rgba(232, 236, 242, 0.08); + --nebula-border-strong: rgba(232, 236, 242, 0.14); + --nebula-text: #e8ecf2; + --nebula-text-secondary: #8e99ab; + --nebula-text-tertiary: #6b7587; + --nebula-accent: #7ea0c4; + --nebula-accent-strong: #9bb6d4; + --nebula-accent-soft: rgba(126, 160, 196, 0.16); + --nebula-danger: #c98888; + --nebula-focus: rgba(126, 160, 196, 0.55); + + --nebula-radius-small: 6px; + --nebula-radius-medium: 10px; + --nebula-radius-large: 14px; + + --nebula-spacing-small: 8px; + --nebula-spacing-medium: 16px; + --nebula-spacing-large: 24px; + + --nebula-transition-fast: 120ms ease; + --nebula-transition-normal: 200ms ease; + + --nebula-topbar-height: 40px; + --nebula-blur: 24px; + --nebula-shadow: 0 18px 50px rgba(0, 0, 0, 0.42); + --nebula-font: "Segoe UI Variable Text", "Segoe UI", "Inter", "SF Pro Text", + "Ubuntu", system-ui, sans-serif; +} diff --git a/shells/desktop/ui/src/surfaces/DesktopSurface.jsx b/shells/desktop/ui/src/surfaces/DesktopSurface.jsx new file mode 100644 index 0000000..536a0f4 --- /dev/null +++ b/shells/desktop/ui/src/surfaces/DesktopSurface.jsx @@ -0,0 +1,14 @@ +import { Desktop } from '../components/desktop/Desktop.jsx'; +import './surfaces.css'; + +/** + * BACKGROUND layer surface. + * Native host: GTK_LAYER_SHELL_LAYER_BACKGROUND, anchored to all edges. + */ +export function DesktopSurface() { + return ( +
+ +
+ ); +} diff --git a/shells/desktop/ui/src/surfaces/LauncherSurface.jsx b/shells/desktop/ui/src/surfaces/LauncherSurface.jsx new file mode 100644 index 0000000..1fff484 --- /dev/null +++ b/shells/desktop/ui/src/surfaces/LauncherSurface.jsx @@ -0,0 +1,87 @@ +import { useEffect, useState } from 'react'; +import { Launcher } from '../components/launcher/Launcher.jsx'; +import { nebula } from '../lib/nebula.js'; +import { shellBridge } from '../lib/shellBridge.js'; +import { useShellState } from '../lib/useShellState.js'; +import './surfaces.css'; + +/** + * OVERLAY layer surface. + * Native host maps/unmaps this view. Standalone browser loads always show + * the launcher so the surface can be inspected without the preview shell. + */ +export function LauncherSurface({ embedded = false }) { + const shell = useShellState(); + const [apps, setApps] = useState([]); + const [recents, setRecents] = useState([]); + const [projects, setProjects] = useState([]); + const [user, setUser] = useState(null); + + const open = embedded ? shell.launcherOpen : true; + + useEffect(() => { + let cancelled = false; + + Promise.all([ + nebula.apps.list(), + nebula.apps.recents(), + nebula.projects.recents(), + nebula.session.getUser(), + ]).then(([appList, recentList, projectList, sessionUser]) => { + if (cancelled) { + return; + } + + setApps(appList); + setRecents(recentList); + setProjects(projectList); + setUser(sessionUser); + }); + + return () => { + cancelled = true; + }; + }, []); + + useEffect(() => { + if (embedded) { + return undefined; + } + + function onKeyDown(event) { + if (event.key === 'Escape') { + event.preventDefault(); + shellBridge.closeLauncher(); + } + } + + window.addEventListener('keydown', onKeyDown); + return () => window.removeEventListener('keydown', onKeyDown); + }, [embedded]); + + async function handleLaunch(app) { + await nebula.apps.launch(app.id); + shellBridge.setContext(app.name); + shellBridge.closeLauncher(); + } + + async function handleOpenProject(project) { + await nebula.projects.open(project.id); + shellBridge.setContext(project.name); + shellBridge.closeLauncher(); + } + + return ( +
+ +
+ ); +} diff --git a/shells/desktop/ui/src/surfaces/PreviewSurface.css b/shells/desktop/ui/src/surfaces/PreviewSurface.css new file mode 100644 index 0000000..09fffb6 --- /dev/null +++ b/shells/desktop/ui/src/surfaces/PreviewSurface.css @@ -0,0 +1,52 @@ +.preview-shell { + position: relative; + overflow: hidden; +} + +.preview-shell .desktop-surface { + position: absolute; + inset: 0; +} + +.preview-shell .topbar-surface { + position: relative; + z-index: 20; + height: var(--nebula-topbar-height); +} + +.preview-shell .launcher-surface { + position: absolute; + inset: 0; + z-index: 30; + padding: 0; + pointer-events: none; +} + +.preview-shell .launcher-surface .launcher { + position: absolute; + top: calc(var(--nebula-topbar-height) + 10px); + left: 10px; + max-height: min(640px, calc(100vh - var(--nebula-topbar-height) - 24px)); + opacity: 0; + pointer-events: none; + transform: translateY(-6px); + transition: + opacity var(--nebula-transition-normal), + transform var(--nebula-transition-normal); +} + +.preview-shell .launcher-surface .launcher.is-open { + opacity: 1; + pointer-events: auto; + transform: translateY(0); +} + +.launcher-dismiss { + position: absolute; + inset: var(--nebula-topbar-height) 0 0; + z-index: 25; + border: 0; + padding: 0; + background: transparent; + cursor: default; +} diff --git a/shells/desktop/ui/src/surfaces/PreviewSurface.jsx b/shells/desktop/ui/src/surfaces/PreviewSurface.jsx new file mode 100644 index 0000000..45ceebb --- /dev/null +++ b/shells/desktop/ui/src/surfaces/PreviewSurface.jsx @@ -0,0 +1,46 @@ +import { useEffect } from 'react'; +import { shellBridge } from '../lib/shellBridge.js'; +import { useShellState } from '../lib/useShellState.js'; +import { DesktopSurface } from './DesktopSurface.jsx'; +import { LauncherSurface } from './LauncherSurface.jsx'; +import { TopBarSurface } from './TopBarSurface.jsx'; +import './PreviewSurface.css'; +import './surfaces.css'; + +/** + * Browser-only composition of the three shell surfaces. + * Production loads each surface in its own WebKitGTK view. + */ +export function PreviewSurface() { + const { launcherOpen } = useShellState(); + + useEffect(() => { + function onKeyDown(event) { + if (event.key === 'Escape') { + event.preventDefault(); + shellBridge.closeLauncher(); + } + } + + window.addEventListener('keydown', onKeyDown); + return () => window.removeEventListener('keydown', onKeyDown); + }, []); + + return ( +
+ + + {launcherOpen ? ( +
+ ); +} diff --git a/shells/desktop/ui/src/surfaces/TopBarSurface.jsx b/shells/desktop/ui/src/surfaces/TopBarSurface.jsx new file mode 100644 index 0000000..9919a1a --- /dev/null +++ b/shells/desktop/ui/src/surfaces/TopBarSurface.jsx @@ -0,0 +1,37 @@ +import { useEffect } from 'react'; +import { TopBar } from '../components/topbar/TopBar.jsx'; +import { shellBridge } from '../lib/shellBridge.js'; +import { useShellState } from '../lib/useShellState.js'; +import './surfaces.css'; + +/** + * TOP layer surface. + * Native host: GTK_LAYER_SHELL_LAYER_TOP, exclusive zone, fixed height. + */ +export function TopBarSurface() { + const { launcherOpen, contextName } = useShellState(); + + useEffect(() => { + function onKeyDown(event) { + if (event.key === 'Escape') { + event.preventDefault(); + shellBridge.closeLauncher(); + } + } + + window.addEventListener('keydown', onKeyDown); + return () => window.removeEventListener('keydown', onKeyDown); + }, []); + + return ( +
+ { + shellBridge.toggleLauncher(); + }} + /> +
+ ); +} diff --git a/shells/desktop/ui/src/surfaces/surfaces.css b/shells/desktop/ui/src/surfaces/surfaces.css new file mode 100644 index 0000000..c604dae --- /dev/null +++ b/shells/desktop/ui/src/surfaces/surfaces.css @@ -0,0 +1,41 @@ +.desktop-surface, +.topbar-surface, +.launcher-surface, +.preview-shell { + width: 100%; +} + +.desktop-surface, +.launcher-surface, +.preview-shell { + height: 100%; +} + +.desktop-surface, +.topbar-surface, +.launcher-surface { + background: transparent; +} + +.topbar-surface { + width: 100%; +} + +.topbar-surface .topbar { + width: 100%; +} + +.launcher-surface { + padding: 10px; +} + +.launcher-surface .launcher { + position: relative; + top: auto; + left: auto; + z-index: auto; + max-height: min(640px, 100%); + opacity: 1; + pointer-events: auto; + transform: none; +} diff --git a/shells/desktop/ui/vite.config.js b/shells/desktop/ui/vite.config.js new file mode 100644 index 0000000..9982072 --- /dev/null +++ b/shells/desktop/ui/vite.config.js @@ -0,0 +1,7 @@ +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +})