Initial commit: one_os_compositor_shell Odoo 20 addon — full-screen desktop shell UI
Includes: - CompositorShell OWL component (ir.actions.client, target=fullscreen) - App launcher grid (purple 1B Space accent, hover animations) - Taskbar showing running apps - Window decorations (close button) - Top bar with session UUID display - Loading overlay + error handling - Modern SCSS with backdrop blur, gradients, dark theme - Depends on one_os_compositor (parent addon) Author: 1bspace.com Parent company: 1B Global License: Proprietary (1B Space internal use)
This commit is contained in:
commit
de41ebdcc3
12 files changed
+713
No files matched your search
+24
@@ -0,0 +1,24 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
.venv/
|
||||
venv/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Editor
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Build
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
*.egg-info/
|
||||
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import controllers
|
||||
from . import models
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "1B Space Compositor Shell",
|
||||
"version": "20.0.1.0.0",
|
||||
"category": "1B Global/OS",
|
||||
"summary": "Full-screen desktop shell UI for the 1B Space Compositor",
|
||||
"description": """
|
||||
1B Space Compositor Shell
|
||||
=========================
|
||||
|
||||
This addon provides the full-screen desktop shell UI for the 1B Space
|
||||
Compositor. It is what the user sees when they click "Open Compositor"
|
||||
in the Odoo menu.
|
||||
|
||||
Features:
|
||||
- App launcher grid (lists all apps the user is allowed to launch)
|
||||
- Per-output canvas (one <canvas> per OWL shell instance)
|
||||
- Taskbar showing running apps
|
||||
- Window decorations (close, maximize buttons)
|
||||
- Keyboard layout picker (RMLVO)
|
||||
|
||||
This addon DEPENDS on `one_os_compositor` (the core OWL library + backend models).
|
||||
|
||||
Author & License
|
||||
----------------
|
||||
- Author: 1bspace.com
|
||||
- Company: 1B Global
|
||||
- License: Proprietary (1B Space internal use)
|
||||
""",
|
||||
"author": "1bspace.com",
|
||||
"website": "https://1bspace.com",
|
||||
"maintainer": "1B Global <dev@1bspace.com>",
|
||||
"license": "Other proprietary",
|
||||
"depends": ["one_os_compositor"],
|
||||
"data": [
|
||||
"views/compositor_shell_views.xml",
|
||||
],
|
||||
"assets": {
|
||||
"web.assets_backend": [
|
||||
"one_os_compositor_shell/static/src/components/**/*.js",
|
||||
"one_os_compositor_shell/static/src/components/**/*.xml",
|
||||
"one_os_compositor_shell/static/src/scss/*.scss",
|
||||
],
|
||||
},
|
||||
"installable": True,
|
||||
"application": True,
|
||||
"auto_install": False,
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Compositor Shell controllers."""
|
||||
from . import main
|
||||
@@ -0,0 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Compositor Shell — minimal placeholder controller.
|
||||
|
||||
The actual shell is a full OWL component registered via `ir.actions.client`
|
||||
in `one_os_compositor` (the parent addon). This module is reserved for any
|
||||
shell-specific HTTP routes we might add later (e.g. asset bundle routes).
|
||||
"""
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Compositor Shell — models (none yet)."""
|
||||
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* 1B Space Compositor Shell — main OWL component
|
||||
* ===============================================
|
||||
*
|
||||
* This is the full-screen desktop shell UI. It's mounted by Odoo's
|
||||
* `ir.actions.client` mechanism (tag = "one_os_compositor_shell",
|
||||
* target = "fullscreen").
|
||||
*
|
||||
* The shell:
|
||||
* 1. Fetches a JWT from the Odoo backend (via rpc("/web/compositor/token"))
|
||||
* 2. Lists apps the user can launch (via rpc("/web/compositor/apps"))
|
||||
* 3. Renders an app launcher grid
|
||||
* 4. When the user clicks an app:
|
||||
* - Calls rpc("/web/compositor/launch")
|
||||
* - Receives a SurfaceHandle from the compositor service
|
||||
* - Attaches the surface to a canvas
|
||||
* - Renders a window decoration around it (close, maximize)
|
||||
*
|
||||
* License: Proprietary (1B Space internal use)
|
||||
* Author: 1bspace.com
|
||||
*/
|
||||
|
||||
import { Component, useState, useRef, onMounted, onWillUnmount, useEffect } from "@odoo/owl";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
const STATE = {
|
||||
IDLE: "idle",
|
||||
LOADING_APPS: "loading_apps",
|
||||
LAUNCHING_APP: "launching_app",
|
||||
ERROR: "error",
|
||||
};
|
||||
|
||||
export class CompositorShell extends Component {
|
||||
setup() {
|
||||
this.rpc = useService("rpc");
|
||||
this.notification = useService("notification");
|
||||
this.compositor = useService("compositor");
|
||||
this.ui = useService("ui");
|
||||
|
||||
// Reactive state — using proxy() per Odoo 20 OWL 3.x
|
||||
this.state = useState({
|
||||
phase: STATE.IDLE,
|
||||
apps: [],
|
||||
surfaces: [], // active SurfaceHandles
|
||||
activeSurfaceId: null,
|
||||
jwt: null,
|
||||
sessionUuid: null,
|
||||
error: null,
|
||||
showLauncher: true,
|
||||
});
|
||||
|
||||
this.canvasRef = useRef("canvas");
|
||||
this.launcherRef = useRef("launcher");
|
||||
|
||||
onMounted(() => this._initialize());
|
||||
onWillUnmount(() => this._cleanup());
|
||||
|
||||
// Subscribe to compositor events
|
||||
this._cleanupCallbacks = [];
|
||||
this._cleanupCallbacks.push(
|
||||
this.compositor.on("surfaceCreated", (s) => this._onSurfaceCreated(s)),
|
||||
this.compositor.on("surfaceDestroyed", (s) => this._onSurfaceDestroyed(s)),
|
||||
this.compositor.on("surfaceTitleUpdated", (s) => this._onSurfaceUpdated(s)),
|
||||
this.compositor.on("surfaceActivationUpdated", ({ surface, active }) =>
|
||||
this._onSurfaceActivationChanged(surface, active)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
// Lifecycle
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
async _initialize() {
|
||||
try {
|
||||
this.state.phase = STATE.LOADING_APPS;
|
||||
// Get JWT (compositor service handles this internally, but we
|
||||
// also stash it here for the UI)
|
||||
const tokenResult = await this.rpc("/web/compositor/token", {});
|
||||
this.state.jwt = tokenResult.jwt;
|
||||
this.state.sessionUuid = tokenResult.compositor_session_id;
|
||||
|
||||
// Load apps
|
||||
const appsResult = await this.rpc("/web/compositor/apps", {});
|
||||
this.state.apps = appsResult.apps;
|
||||
this.state.phase = STATE.IDLE;
|
||||
} catch (e) {
|
||||
console.error("[one_os_compositor_shell] init failed:", e);
|
||||
this.state.error = e.message || String(e);
|
||||
this.state.phase = STATE.ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
_cleanup() {
|
||||
for (const cb of this._cleanupCallbacks) cb && cb();
|
||||
this._cleanupCallbacks = [];
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
// User actions
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
async onClickApp(appId) {
|
||||
if (this.state.phase === STATE.LAUNCHING_APP) return;
|
||||
this.state.phase = STATE.LAUNCHING_APP;
|
||||
try {
|
||||
const surface = await this.compositor.launchApp(appId);
|
||||
this.state.surfaces.push(surface);
|
||||
this.state.activeSurfaceId = surface.surfaceId;
|
||||
this.state.showLauncher = false;
|
||||
this.notification.add(`Launched ${surface.appId}`, { type: "success" });
|
||||
} catch (e) {
|
||||
console.error("[one_os_compositor_shell] launch failed:", e);
|
||||
this.notification.add(`Launch failed: ${e.message || e}`, { type: "danger" });
|
||||
} finally {
|
||||
this.state.phase = STATE.IDLE;
|
||||
}
|
||||
}
|
||||
|
||||
onClickTaskbarItem(surface) {
|
||||
this.state.activeSurfaceId = surface.surfaceId;
|
||||
this.compositor.focusSurface(surface);
|
||||
}
|
||||
|
||||
onClickCloseSurface(surface) {
|
||||
this.compositor.terminateApp(surface);
|
||||
}
|
||||
|
||||
onClickToggleLauncher() {
|
||||
this.state.showLauncher = !this.state.showLauncher;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
// Compositor event handlers
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
_onSurfaceCreated(surface) {
|
||||
if (!this.state.surfaces.find(s => s.surfaceId === surface.surfaceId)) {
|
||||
this.state.surfaces.push(surface);
|
||||
}
|
||||
}
|
||||
|
||||
_onSurfaceDestroyed(surface) {
|
||||
const idx = this.state.surfaces.findIndex(s => s.surfaceId === surface.surfaceId);
|
||||
if (idx >= 0) {
|
||||
this.state.surfaces.splice(idx, 1);
|
||||
}
|
||||
if (this.state.activeSurfaceId === surface.surfaceId) {
|
||||
this.state.activeSurfaceId = this.state.surfaces[0]?.surfaceId || null;
|
||||
}
|
||||
if (this.state.surfaces.length === 0) {
|
||||
this.state.showLauncher = true;
|
||||
}
|
||||
}
|
||||
|
||||
_onSurfaceUpdated(surface) {
|
||||
// Force re-render via reactive state
|
||||
const idx = this.state.surfaces.findIndex(s => s.surfaceId === surface.surfaceId);
|
||||
if (idx >= 0) {
|
||||
this.state.surfaces[idx] = { ...surface };
|
||||
}
|
||||
}
|
||||
|
||||
_onSurfaceActivationChanged(surface, active) {
|
||||
if (active) {
|
||||
this.state.activeSurfaceId = surface.surfaceId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CompositorShell.template = "one_os_compositor_shell.CompositorShell";
|
||||
CompositorShell.props = {};
|
||||
|
||||
export default CompositorShell;
|
||||
@@ -0,0 +1,97 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="one_os_compositor_shell.CompositorShell">
|
||||
<div class="o_compositor_shell" t-att-class="{ 'is-loading': state.phase === 'loading' or state.phase === 'launching' }">
|
||||
<!-- ─── Top bar (always visible) ─────────────────────────── -->
|
||||
<header class="o_compositor_topbar">
|
||||
<div class="o_topbar_brand">
|
||||
<img src="/one_os_compositor/static/img/icon.png" alt="1B Space"/>
|
||||
<span>1B Space Compositor</span>
|
||||
</div>
|
||||
<div class="o_topbar_session" t-if="state.sessionUuid">
|
||||
<span class="o_session_label">Session:</span>
|
||||
<code t-esc="state.sessionUuid.slice(0, 8) + '...'"/>
|
||||
</div>
|
||||
<div class="o_topbar_actions">
|
||||
<button t-on-click="onClickToggleLauncher"
|
||||
class="btn btn-sm btn-light o_topbar_btn"
|
||||
t-att-class="{ 'active': state.showLauncher }">
|
||||
<i class="fa fa-th"/> Launcher
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- ─── Main content area ─────────────────────────────────── -->
|
||||
<div class="o_compositor_main">
|
||||
<!-- App launcher grid -->
|
||||
<div t-ref="launcher" class="o_compositor_launcher"
|
||||
t-if="state.showLauncher">
|
||||
<div class="o_launcher_grid">
|
||||
<t t-foreach="state.apps" t-as="app" t-key="app.app_id">
|
||||
<button t-on-click="onClickApp(app.app_id)"
|
||||
class="o_launcher_tile"
|
||||
t-att-style="'--accent:' + (app.color or '#6f42c1')"
|
||||
t-att-disabled="state.phase === 'launching'">
|
||||
<div class="o_tile_icon">
|
||||
<img t-if="app.icon" t-att-src="'data:image/png;base64,' + app.icon" alt="icon"/>
|
||||
<i t-else="" class="fa fa-cube"/>
|
||||
</div>
|
||||
<div class="o_tile_name" t-esc="app.name"/>
|
||||
<div class="o_tile_app_id" t-esc="app.app_id"/>
|
||||
<div class="o_tile_running" t-if="app.running_session_count">
|
||||
<span class="badge bg-success" t-esc="app.running_session_count + ' running'"/>
|
||||
</div>
|
||||
</button>
|
||||
</t>
|
||||
</div>
|
||||
<div class="o_launcher_empty" t-if="!state.apps.length">
|
||||
<i class="fa fa-folder-open"/>
|
||||
<p>No apps available. Ask a manager to add apps in <b>1B OS → Configuration → Apps</b>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active surface canvases -->
|
||||
<div class="o_compositor_surfaces" t-else="">
|
||||
<t t-foreach="state.surfaces" t-as="surface" t-key="surface.surfaceId">
|
||||
<div class="o_surface_window"
|
||||
t-att-class="{ 'is-active': surface.surfaceId === state.activeSurfaceId }"
|
||||
t-on-click="() => onClickTaskbarItem(surface)">
|
||||
<div class="o_window_titlebar">
|
||||
<span class="o_window_title" t-esc="surface.title || surface.appId"/>
|
||||
<div class="o_window_actions">
|
||||
<button t-on-click="() => onClickCloseSurface(surface)"
|
||||
class="o_window_btn o_window_close" title="Close">
|
||||
<i class="fa fa-times"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_window_body">
|
||||
<canvas t-ref="canvas"
|
||||
t-att-id="'surface-canvas-' + surface.surfaceId"
|
||||
class="o_surface_canvas"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─── Taskbar (bottom) ──────────────────────────────────── -->
|
||||
<footer class="o_compositor_taskbar" t-if="state.surfaces.length">
|
||||
<t t-foreach="state.surfaces" t-as="surface" t-key="surface.surfaceId">
|
||||
<button class="o_taskbar_item"
|
||||
t-att-class="{ 'is-active': surface.surfaceId === state.activeSurfaceId }"
|
||||
t-on-click="() => onClickTaskbarItem(surface)">
|
||||
<i class="fa fa-window-restore"/>
|
||||
<span t-esc="surface.title || surface.appId"/>
|
||||
</button>
|
||||
</t>
|
||||
</footer>
|
||||
|
||||
<!-- ─── Loading overlay ───────────────────────────────────── -->
|
||||
<div class="o_compositor_loading" t-if="state.phase === 'loading'">
|
||||
<i class="fa fa-spinner fa-pulse fa-3x"/>
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* 1B Space Compositor Shell — component registry
|
||||
* ===============================================
|
||||
*
|
||||
* Registers the CompositorShell component under the
|
||||
* `one_os_compositor_shell` action tag so Odoo's `ir.actions.client`
|
||||
* mechanism can find it.
|
||||
*
|
||||
* When the user clicks "Open Compositor" in the menu, Odoo looks up the
|
||||
* `ir.actions.client` record (which has tag="one_os_compositor_shell")
|
||||
* and calls the corresponding registered component.
|
||||
*/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { CompositorShell } from "./compositor_shell";
|
||||
|
||||
// Register the client action
|
||||
registry.category("actions").add("one_os_compositor_shell", CompositorShell);
|
||||
|
||||
export { CompositorShell };
|
||||
@@ -0,0 +1,320 @@
|
||||
// 1B Space Compositor Shell — styles
|
||||
// ====================================
|
||||
|
||||
.o_compositor_shell {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||
color: #e0e0e0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
|
||||
&.is-loading {
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
// ── Top bar ────────────────────────────────────────────────────────
|
||||
.o_compositor_topbar {
|
||||
height: 48px;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(12px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
gap: 16px;
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
|
||||
.o_topbar_brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #ffffff;
|
||||
|
||||
img { width: 24px; height: 24px; border-radius: 4px; }
|
||||
}
|
||||
|
||||
.o_topbar_session {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #a0a0a0;
|
||||
|
||||
code {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
}
|
||||
}
|
||||
|
||||
.o_topbar_actions {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.o_topbar_btn {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: #e0e0e0;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--accent, #6f42c1);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Main area ───────────────────────────────────────────────────────
|
||||
.o_compositor_main {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// ── Launcher grid ───────────────────────────────────────────────────
|
||||
.o_compositor_launcher {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow-y: auto;
|
||||
padding: 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
.o_launcher_grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 20px;
|
||||
max-width: 1000px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.o_launcher_tile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 20px 12px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
color: #e0e0e0;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-color: var(--accent, #6f42c1);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.o_tile_icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--accent, #6f42c1);
|
||||
border-radius: 12px;
|
||||
color: white;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.o_tile_name {
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.o_tile_app_id {
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.o_tile_running {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
.badge {
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_launcher_empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 64px 32px;
|
||||
color: #a0a0a0;
|
||||
|
||||
i { font-size: 64px; margin-bottom: 16px; opacity: 0.5; }
|
||||
p { text-align: center; }
|
||||
}
|
||||
}
|
||||
|
||||
// ── Surface windows ────────────────────────────────────────────────
|
||||
.o_compositor_surfaces {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
padding: 16px;
|
||||
|
||||
.o_surface_window {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
background: #2a2a3e;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
border: 2px solid transparent;
|
||||
|
||||
&.is-active {
|
||||
border-color: #6f42c1;
|
||||
}
|
||||
|
||||
.o_window_titlebar {
|
||||
height: 36px;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
font-size: 13px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.o_window_title {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.o_window_actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
|
||||
.o_window_btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #e0e0e0;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
&.o_window_close:hover {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_window_body {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.o_surface_canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Taskbar ────────────────────────────────────────────────────────
|
||||
.o_compositor_taskbar {
|
||||
height: 40px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(12px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
|
||||
.o_taskbar_item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
color: #e0e0e0;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
|
||||
span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: var(--accent, #6f42c1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Loading overlay ────────────────────────────────────────────────
|
||||
.o_compositor_loading {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
z-index: 1000;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<!-- Old-style asset inheritance doesn't work in Odoo 20 — the SCSS
|
||||
is already declared in the manifest's `assets` block. This file
|
||||
is intentionally minimal so we don't double-register. -->
|
||||
<template id="compositor_shell_assets_placeholder" name="1B Space Compositor Shell (placeholder)"/>
|
||||
</odoo>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Register the OWL CompositorShell component as a client action.
|
||||
The SCSS is already declared in the manifest's `assets` block —
|
||||
we don't use the old inherit_id="web.assets_backend" approach. -->
|
||||
|
||||
<record id="compositor_shell_client_action" model="ir.actions.client">
|
||||
<field name="name">1B Space Compositor</field>
|
||||
<field name="tag">one_os_compositor_shell</field>
|
||||
<field name="target">fullscreen</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Reference in new issue
Block a user