mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-05-20 01:03:59 +00:00
Reduce Office to document ownership
Keep _office focused on document artifacts, Markdown sessions, LibreOffice-compatible file actions, and document persistence. Route binary document editing through explicit Desktop requests instead of cold-opening the live Desktop surface from artifact results.
This commit is contained in:
parent
76a282ba8f
commit
0c08fa65f3
14 changed files with 610 additions and 2679 deletions
|
|
@ -1,7 +1,8 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from helpers.api import ApiHandler, Request
|
||||
from plugins._office.helpers import document_store, libreoffice, libreoffice_desktop, markdown_sessions
|
||||
from plugins._desktop.helpers import desktop_session
|
||||
from plugins._office.helpers import document_store, libreoffice, markdown_sessions
|
||||
|
||||
|
||||
class OfficeSession(ApiHandler):
|
||||
|
|
@ -14,6 +15,7 @@ class OfficeSession(ApiHandler):
|
|||
if action == "home":
|
||||
return {"ok": True, "path": document_store.default_open_path(context_id)}
|
||||
if action == "desktop":
|
||||
# Compatibility only. New Desktop callers use /plugins/_desktop/desktop_session.
|
||||
return self._desktop()
|
||||
if action == "close":
|
||||
closed = document_store.close_session(
|
||||
|
|
@ -58,30 +60,47 @@ class OfficeSession(ApiHandler):
|
|||
if action == "renamed":
|
||||
return self._renamed(input, context_id)
|
||||
if action == "desktop_save":
|
||||
# Compatibility only. New Desktop callers use /plugins/_desktop/desktop_session.
|
||||
return self._desktop_save(input)
|
||||
if action == "desktop_sync":
|
||||
# Compatibility only. New Desktop callers use /plugins/_desktop/desktop_session.
|
||||
return self._desktop_sync(input)
|
||||
if action == "desktop_state":
|
||||
# Compatibility only. New Desktop callers use /plugins/_desktop/desktop_session.
|
||||
return self._desktop_state(input)
|
||||
if action == "desktop_shutdown":
|
||||
# Compatibility only. New Desktop callers use /plugins/_desktop/desktop_session.
|
||||
return self._desktop_shutdown(input)
|
||||
return {"ok": False, "error": f"Unsupported office session action: {action}"}
|
||||
|
||||
async def _open_document(self, doc: dict, input: dict, request: Request) -> dict:
|
||||
mode = "edit" if str(input.get("mode") or "edit").lower() == "edit" else "view"
|
||||
store_session = document_store.create_session(
|
||||
doc["file_id"],
|
||||
user_id=str(input.get("user_id") or "agent-zero-user"),
|
||||
permission="write" if mode == "edit" else "read",
|
||||
origin=self._origin(request),
|
||||
)
|
||||
if str(doc.get("extension") or "").lower() in libreoffice_desktop.OFFICIAL_EXTENSIONS:
|
||||
desktop = libreoffice_desktop.get_manager().open(doc, refresh=input.get("refresh") is True)
|
||||
if str(doc.get("extension") or "").lower() in desktop_session.OFFICIAL_EXTENSIONS:
|
||||
if input.get("open_in_desktop") is not True:
|
||||
return {
|
||||
"ok": True,
|
||||
"requires_desktop": True,
|
||||
"file_id": doc["file_id"],
|
||||
"title": doc["basename"],
|
||||
"extension": doc["extension"],
|
||||
"path": doc["path"],
|
||||
"text": "",
|
||||
"document": _public_doc(doc),
|
||||
"version": document_store.item_version(doc),
|
||||
"mode": mode,
|
||||
}
|
||||
store_session = document_store.create_session(
|
||||
doc["file_id"],
|
||||
user_id=str(input.get("user_id") or "agent-zero-user"),
|
||||
permission="write" if mode == "edit" else "read",
|
||||
origin=self._origin(request),
|
||||
)
|
||||
desktop = desktop_session.get_manager().open(doc, refresh=input.get("refresh") is True)
|
||||
if not desktop.get("available"):
|
||||
document_store.close_session(session_id=store_session["session_id"])
|
||||
return {
|
||||
"ok": False,
|
||||
"error": desktop.get("error") or desktop.get("reason") or "Official LibreOffice desktop session is unavailable.",
|
||||
"error": desktop.get("error") or desktop.get("reason") or "Desktop session is unavailable.",
|
||||
"desktop": desktop,
|
||||
"libreoffice": libreoffice.collect_status(),
|
||||
}
|
||||
|
|
@ -100,6 +119,12 @@ class OfficeSession(ApiHandler):
|
|||
"store_session_id": store_session["session_id"],
|
||||
"mode": mode,
|
||||
}
|
||||
store_session = document_store.create_session(
|
||||
doc["file_id"],
|
||||
user_id=str(input.get("user_id") or "agent-zero-user"),
|
||||
permission="write" if mode == "edit" else "read",
|
||||
origin=self._origin(request),
|
||||
)
|
||||
try:
|
||||
editor = markdown_sessions.get_manager().open(doc, sid="")
|
||||
except ValueError as exc:
|
||||
|
|
@ -135,8 +160,8 @@ class OfficeSession(ApiHandler):
|
|||
except Exception as exc:
|
||||
return {"ok": False, "error": str(exc)}
|
||||
desktop = None
|
||||
if str(updated.get("extension") or "").lower() in libreoffice_desktop.OFFICIAL_EXTENSIONS:
|
||||
desktop = libreoffice_desktop.get_manager().retarget_document(file_id, updated)
|
||||
if str(updated.get("extension") or "").lower() in desktop_session.OFFICIAL_EXTENSIONS:
|
||||
desktop = desktop_session.get_manager().retarget_document(file_id, updated)
|
||||
return {
|
||||
"ok": True,
|
||||
"document": _public_doc(updated),
|
||||
|
|
@ -146,7 +171,7 @@ class OfficeSession(ApiHandler):
|
|||
}
|
||||
|
||||
def _desktop(self) -> dict:
|
||||
desktop = libreoffice_desktop.get_manager().ensure_system_desktop()
|
||||
desktop = desktop_session.get_manager().ensure_system_desktop()
|
||||
if not desktop.get("available"):
|
||||
return {
|
||||
"ok": False,
|
||||
|
|
@ -155,7 +180,7 @@ class OfficeSession(ApiHandler):
|
|||
"libreoffice": libreoffice.collect_status(),
|
||||
}
|
||||
document = {
|
||||
"file_id": libreoffice_desktop.SYSTEM_FILE_ID,
|
||||
"file_id": desktop_session.SYSTEM_FILE_ID,
|
||||
"path": desktop["path"],
|
||||
"basename": desktop["title"],
|
||||
"title": desktop["title"],
|
||||
|
|
@ -167,7 +192,7 @@ class OfficeSession(ApiHandler):
|
|||
"ok": True,
|
||||
"session_id": desktop["session_id"],
|
||||
"desktop_session_id": desktop["session_id"],
|
||||
"file_id": libreoffice_desktop.SYSTEM_FILE_ID,
|
||||
"file_id": desktop_session.SYSTEM_FILE_ID,
|
||||
"title": desktop["title"],
|
||||
"extension": "desktop",
|
||||
"path": desktop["path"],
|
||||
|
|
@ -183,24 +208,24 @@ class OfficeSession(ApiHandler):
|
|||
session_id = str(input.get("desktop_session_id") or input.get("session_id") or "").strip()
|
||||
if not session_id:
|
||||
return {"ok": False, "error": "desktop_session_id is required."}
|
||||
return libreoffice_desktop.get_manager().save(
|
||||
return desktop_session.get_manager().save(
|
||||
session_id,
|
||||
file_id=str(input.get("file_id") or ""),
|
||||
)
|
||||
|
||||
def _desktop_sync(self, input: dict) -> dict:
|
||||
return libreoffice_desktop.get_manager().sync(
|
||||
return desktop_session.get_manager().sync(
|
||||
session_id=str(input.get("desktop_session_id") or input.get("session_id") or ""),
|
||||
file_id=str(input.get("file_id") or ""),
|
||||
)
|
||||
|
||||
def _desktop_state(self, input: dict) -> dict:
|
||||
include_screenshot = bool(input.get("include_screenshot") is True)
|
||||
return libreoffice_desktop.get_manager().state(include_screenshot=include_screenshot)
|
||||
return desktop_session.get_manager().state(include_screenshot=include_screenshot)
|
||||
|
||||
def _desktop_shutdown(self, input: dict) -> dict:
|
||||
save_first = input.get("save_first") is not False
|
||||
return libreoffice_desktop.get_manager().shutdown_system_desktop(
|
||||
return desktop_session.get_manager().shutdown_system_desktop(
|
||||
save_first=save_first,
|
||||
source=str(input.get("source") or "api"),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ import {
|
|||
createActionButton,
|
||||
copyToClipboard,
|
||||
} from "/components/messages/action-buttons/simple-action-buttons.js";
|
||||
import { ensureModalOpen } from "/js/modals.js";
|
||||
import { open as openSurface } from "/js/surfaces.js";
|
||||
import { store as officeStore } from "/plugins/_office/webui/office-store.js";
|
||||
|
||||
function basename(path = "") {
|
||||
const value = String(path || "").split("?")[0].split("#")[0];
|
||||
|
|
@ -33,10 +36,8 @@ export function documentFromLog(args = {}, result = {}) {
|
|||
};
|
||||
}
|
||||
|
||||
export async function openOfficeCanvas(kvps = {}) {
|
||||
const canvas = globalThis.Alpine?.store?.("rightCanvas")
|
||||
|| (await import("/components/canvas/right-canvas-store.js")).store;
|
||||
await canvas?.open?.("office", {
|
||||
export async function openDocumentInDesktop(kvps = {}) {
|
||||
await openSurface("desktop", {
|
||||
path: kvps.path || "",
|
||||
file_id: kvps.file_id || "",
|
||||
refresh: true,
|
||||
|
|
@ -44,6 +45,33 @@ export async function openOfficeCanvas(kvps = {}) {
|
|||
});
|
||||
}
|
||||
|
||||
export async function openDocumentArtifact(kvps = {}) {
|
||||
if (usesDesktop(kvps)) {
|
||||
await openDocumentInDesktop(kvps);
|
||||
return;
|
||||
}
|
||||
await ensureModalOpen("/plugins/_office/webui/main.html");
|
||||
await officeStore.openSession?.({
|
||||
path: kvps.path || "",
|
||||
file_id: kvps.file_id || "",
|
||||
refresh: true,
|
||||
source: "message-action",
|
||||
});
|
||||
}
|
||||
|
||||
function usesDesktop(doc = {}) {
|
||||
const format = String(doc.format || doc.extension || "").toLowerCase();
|
||||
return ["odt", "ods", "odp", "docx", "xlsx", "pptx"].includes(format);
|
||||
}
|
||||
|
||||
function desktopActionLabel(doc = {}) {
|
||||
const format = String(doc.format || doc.extension || "").toLowerCase();
|
||||
if (["odt", "docx"].includes(format)) return "Edit in Writer";
|
||||
if (["ods", "xlsx"].includes(format)) return "Edit in Calc";
|
||||
if (["odp", "pptx"].includes(format)) return "Edit in Impress";
|
||||
return "Open Document";
|
||||
}
|
||||
|
||||
export function downloadDocument(doc = {}) {
|
||||
const path = String(doc.path || "");
|
||||
if (!path) return;
|
||||
|
|
@ -59,7 +87,8 @@ export function buildDocumentFileActionButtons(document = {}) {
|
|||
const hasTarget = Boolean(document?.path || document?.file_id);
|
||||
const buttons = [];
|
||||
if (hasTarget) {
|
||||
buttons.push(createActionButton("dock_to_right", "Open in canvas", () => openOfficeCanvas(document)));
|
||||
const icon = usesDesktop(document) ? "desktop_windows" : "article";
|
||||
buttons.push(createActionButton(icon, desktopActionLabel(document), () => openDocumentArtifact(document)));
|
||||
}
|
||||
if (document?.path) {
|
||||
buttons.push(
|
||||
|
|
|
|||
|
|
@ -1,14 +1,18 @@
|
|||
import { store as officeStore } from "/plugins/_office/webui/office-store.js";
|
||||
import { ensureModalOpen } from "/js/modals.js";
|
||||
import { open as openSurface } from "/js/surfaces.js";
|
||||
|
||||
const SYNC_WINDOW_MS = 10 * 60 * 1000;
|
||||
const DESKTOP_OFFICE_FORMATS = new Set(["odt", "ods", "odp", "docx", "xlsx", "pptx"]);
|
||||
const DESKTOP_DOCUMENT_EXTENSIONS = new Set(["odt", "ods", "odp", "docx", "xlsx", "pptx"]);
|
||||
const syncedDocumentResults = new Set();
|
||||
|
||||
export default async function syncDocumentResultsIntoOpenCanvas(context) {
|
||||
export default async function syncDocumentResultsIntoOpenOfficeModal(context) {
|
||||
if (!context?.results?.length || context.historyEmpty) return;
|
||||
|
||||
for (const { args } of context.results) {
|
||||
const payload = getDocumentPayload(args);
|
||||
if (getToolName(payload) !== "document_artifact") continue;
|
||||
if (!shouldSyncOpenOfficeCanvas(args, payload)) continue;
|
||||
if (!shouldSyncOpenOfficeModal(args, payload)) continue;
|
||||
|
||||
const document = payload.document && typeof payload.document === "object" ? payload.document : {};
|
||||
const path = payload.path || document.path || "";
|
||||
|
|
@ -25,14 +29,16 @@ export default async function syncDocumentResultsIntoOpenCanvas(context) {
|
|||
if (syncedDocumentResults.has(key)) continue;
|
||||
syncedDocumentResults.add(key);
|
||||
|
||||
if (!isOfficeCanvasOrModalOpen() && shouldColdOpenOfficeCanvas(payload, document)) {
|
||||
await openOfficeCanvasFromResult({ path, file_id: fileId });
|
||||
if (shouldOpenDocumentUiFromResult(payload, document)) {
|
||||
globalThis.setTimeout(() => {
|
||||
void openDocumentUiFromResult({ path, file_id: fileId }, payload, document);
|
||||
}, 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
globalThis.setTimeout(async () => {
|
||||
if (!isOfficeCanvasOrModalOpen()) return;
|
||||
const office = globalThis.Alpine?.store?.("office");
|
||||
if (!isOfficeModalOpen()) return;
|
||||
const office = officeStore;
|
||||
if (!office || isDirtySameDocument(office, { path, file_id: fileId })) return;
|
||||
await office.openSession?.({
|
||||
path,
|
||||
|
|
@ -63,8 +69,14 @@ function pickPayloadFields(args = {}) {
|
|||
"tool_name",
|
||||
"action",
|
||||
"canvas_surface",
|
||||
"extension",
|
||||
"file_id",
|
||||
"format",
|
||||
"open_canvas",
|
||||
"open_document",
|
||||
"open_desktop",
|
||||
"open_in_canvas",
|
||||
"open_in_desktop",
|
||||
"path",
|
||||
"version",
|
||||
"last_modified",
|
||||
|
|
@ -78,54 +90,62 @@ function getToolName(payload = {}) {
|
|||
return String(payload._tool_name || payload.tool_name || "").trim();
|
||||
}
|
||||
|
||||
function shouldSyncOpenOfficeCanvas(args = {}, payload = {}) {
|
||||
function shouldSyncOpenOfficeModal(args = {}, payload = {}) {
|
||||
if (!isFresh(args.timestamp, payload.last_modified || payload.document?.last_modified)) return false;
|
||||
const action = String(payload.action || "").trim().toLowerCase().replace("-", "_");
|
||||
return ["create", "open", "edit", "restore_version"].includes(action);
|
||||
}
|
||||
|
||||
function shouldColdOpenOfficeCanvas(payload = {}, document = {}) {
|
||||
const action = String(payload.action || "").trim().toLowerCase().replace("-", "_");
|
||||
if (!["create", "open"].includes(action)) return false;
|
||||
return DESKTOP_OFFICE_FORMATS.has(documentFormat(payload, document));
|
||||
function shouldOpenDocumentUiFromResult(payload = {}, document = {}) {
|
||||
if (!isExplicitDocumentUiRequest(payload)) return false;
|
||||
return Boolean(documentExtension(payload, document));
|
||||
}
|
||||
|
||||
async function openOfficeCanvasFromResult(document = {}) {
|
||||
const canvas = globalThis.Alpine?.store?.("rightCanvas")
|
||||
|| (await import("/components/canvas/right-canvas-store.js")).store;
|
||||
await canvas?.open?.("office", {
|
||||
path: document.path || "",
|
||||
file_id: document.file_id || "",
|
||||
function isExplicitDocumentUiRequest(payload = {}) {
|
||||
const action = String(payload.action || "").trim().toLowerCase().replace("-", "_");
|
||||
return action === "open"
|
||||
|| truthy(payload.open_in_canvas)
|
||||
|| truthy(payload.open_canvas)
|
||||
|| truthy(payload.open_document)
|
||||
|| truthy(payload.open_in_desktop)
|
||||
|| truthy(payload.open_desktop);
|
||||
}
|
||||
|
||||
async function openDocumentUiFromResult(target = {}, payload = {}, document = {}) {
|
||||
if (isDesktopDocument(payload, document)) {
|
||||
await openSurface("desktop", {
|
||||
path: target.path || "",
|
||||
file_id: target.file_id || "",
|
||||
refresh: true,
|
||||
source: "tool-result-open",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await ensureModalOpen("/plugins/_office/webui/main.html");
|
||||
await officeStore.openSession?.({
|
||||
path: target.path || "",
|
||||
file_id: target.file_id || "",
|
||||
refresh: true,
|
||||
source: "tool-result-sync",
|
||||
source: "tool-result-open",
|
||||
});
|
||||
}
|
||||
|
||||
function documentFormat(payload = {}, document = {}) {
|
||||
function isDesktopDocument(payload = {}, document = {}) {
|
||||
return DESKTOP_DOCUMENT_EXTENSIONS.has(documentExtension(payload, document));
|
||||
}
|
||||
|
||||
function documentExtension(payload = {}, document = {}) {
|
||||
return String(
|
||||
payload.format
|
||||
|| payload.extension
|
||||
|| document.extension
|
||||
|| extensionOf(payload.path || document.path || ""),
|
||||
).trim().toLowerCase().replace(/^\./, "");
|
||||
|| document.format
|
||||
|| "",
|
||||
).toLowerCase();
|
||||
}
|
||||
|
||||
function extensionOf(path = "") {
|
||||
const name = String(path || "").split("?")[0].split("#")[0].split("/").filter(Boolean).pop() || "";
|
||||
const index = name.lastIndexOf(".");
|
||||
return index >= 0 ? name.slice(index + 1) : "";
|
||||
}
|
||||
|
||||
function isOfficeCanvasAlreadyOpen() {
|
||||
const canvas = globalThis.Alpine?.store?.("rightCanvas");
|
||||
return Boolean(canvas?.isOpen && canvas?.activeSurfaceId === "office");
|
||||
}
|
||||
|
||||
function isOfficeCanvasOrModalOpen() {
|
||||
return Boolean(isOfficeCanvasAlreadyOpen() || isOfficeModalAlreadyOpen());
|
||||
}
|
||||
|
||||
function isOfficeModalAlreadyOpen() {
|
||||
function isOfficeModalOpen() {
|
||||
return Boolean(
|
||||
globalThis.isModalOpen?.("/plugins/_office/webui/main.html")
|
||||
|| globalThis.isModalOpen?.("plugins/_office/webui/main.html")
|
||||
|
|
@ -143,6 +163,13 @@ function isDirtySameDocument(office, document = {}) {
|
|||
);
|
||||
}
|
||||
|
||||
function truthy(value) {
|
||||
if (value === true) return true;
|
||||
if (value === false || value == null) return false;
|
||||
if (typeof value === "number") return value !== 0;
|
||||
return ["1", "true", "yes", "y", "on"].includes(String(value).trim().toLowerCase());
|
||||
}
|
||||
|
||||
function isFresh(...timestamps) {
|
||||
const now = Date.now();
|
||||
for (const value of timestamps) {
|
||||
|
|
|
|||
|
|
@ -141,9 +141,9 @@ def _refresh_open_editor_sessions(file_id: str) -> None:
|
|||
# Direct artifact edits should never fail just because no canvas is open.
|
||||
pass
|
||||
try:
|
||||
from plugins._office.helpers import libreoffice_desktop
|
||||
from plugins._desktop.helpers import desktop_session
|
||||
|
||||
libreoffice_desktop.get_manager().refresh_document(file_id)
|
||||
desktop_session.get_manager().refresh_document(file_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||
|
||||
from typing import Any
|
||||
|
||||
from plugins._office.helpers import desktop_state
|
||||
from plugins._desktop.helpers import desktop_state
|
||||
from plugins._office.helpers import document_store
|
||||
|
||||
|
||||
|
|
@ -13,7 +13,7 @@ def build_context(max_items: int = 6) -> str:
|
|||
return desktop_context
|
||||
|
||||
lines = [
|
||||
"These document artifacts have active canvas sessions. Content is omitted; load skill `office-artifacts` for edit workflow, then use document_artifact:read before content-sensitive edits.",
|
||||
"These document artifacts have active document sessions. Content is omitted; load skill `document-artifacts` for edit workflow, then use document_artifact:read before content-sensitive edits.",
|
||||
]
|
||||
for doc in documents:
|
||||
lines.append(format_document_line(doc))
|
||||
|
|
@ -46,5 +46,5 @@ def build_desktop_context() -> str:
|
|||
return (
|
||||
"[DESKTOP STATE]\n"
|
||||
f"- unavailable={exc}\n"
|
||||
"- next=Open the Desktop canvas manually, then run plugins/_office/skills/linux-desktop/scripts/desktopctl.sh observe --json."
|
||||
"- next=Open the Desktop surface manually, then run plugins/_desktop/skills/linux-desktop/scripts/desktopctl.sh observe --json."
|
||||
)
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ ODF_MIMETYPES = {
|
|||
"odp": "application/vnd.oasis.opendocument.presentation",
|
||||
}
|
||||
|
||||
STATE_DIR = Path(files.get_abs_path("usr", "plugins", PLUGIN_NAME, "documents"))
|
||||
STATE_DIR = Path(files.get_abs_path("usr", PLUGIN_NAME, "documents"))
|
||||
DB_PATH = STATE_DIR / "documents.sqlite3"
|
||||
BACKUP_DIR = STATE_DIR / "backups"
|
||||
WORKDIR = Path(files.get_abs_path("usr", "workdir"))
|
||||
|
|
|
|||
|
|
@ -36,9 +36,9 @@ def collect_status() -> dict[str, Any]:
|
|||
"message": "LibreOffice is available." if soffice else "LibreOffice is not installed in this runtime.",
|
||||
}
|
||||
try:
|
||||
from plugins._office.helpers import libreoffice_desktop
|
||||
from plugins._desktop.helpers import desktop_session
|
||||
|
||||
status["desktop"] = libreoffice_desktop.collect_desktop_status()
|
||||
status["desktop"] = desktop_session.collect_desktop_status()
|
||||
except Exception as exc:
|
||||
status["desktop"] = {"ok": False, "healthy": False, "error": str(exc)}
|
||||
return status
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
name: _office
|
||||
title: LibreOffice
|
||||
description: Markdown writing and ODF-first LibreOffice document artifacts in the right canvas.
|
||||
description: Markdown writing and ODF-first LibreOffice document artifacts.
|
||||
version: "0.1"
|
||||
settings_sections:
|
||||
- developer
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
### document_artifact
|
||||
create/open/read/edit reusable document artifacts in the Agent Zero canvas
|
||||
create/open/read/edit reusable document artifacts in Agent Zero
|
||||
formats: md odt ods odp docx xlsx pptx
|
||||
default format: md
|
||||
methods: create open read edit inspect export version_history restore_version status
|
||||
common args: method action kind title format content path file_id
|
||||
optional UI intent args: open_in_canvas open_in_desktop
|
||||
`method` is accepted as an alias for action when the tool_name has no suffix
|
||||
tool results save or update artifacts only; they do not open the canvas automatically
|
||||
created/updated artifacts are shown with explicit Download and Open in canvas message actions
|
||||
ODF is first-class for LibreOffice: use ODT for Writer, ODS for Spreadsheet/Calc, and ODP for Presentation/Impress unless the user explicitly requests Microsoft compatibility
|
||||
create/read/edit results save or update artifacts only; they do not open a surface automatically unless the user explicitly asks to open the document UI
|
||||
use action `open`, `open_in_canvas: true`, or `open_in_desktop: true` only when the user explicitly asks to open the document/editor/Desktop
|
||||
created/updated artifacts are shown with explicit Download, Open Document, or Desktop edit message actions
|
||||
ODF is first-class for LibreOffice: use ODT for Writer, ODS for Spreadsheet/Calc, and ODP for Presentation/Impress unless the user explicitly requests OOXML compatibility
|
||||
DOCX/XLSX/PPTX are compatibility formats, not defaults
|
||||
XLSX charts: use edit operation `create_chart` with `chart` object instead of code execution for embedded spreadsheet charts when an embedded chart is required
|
||||
chart types: line bar column pie area scatter stock ohlc candlestick
|
||||
ODS/XLSX create/edit tabular content: CSV, TSV, Markdown tables, or rows arrays become real spreadsheet cells
|
||||
for nontrivial document artifact work, load skill `office-artifacts` or the specific Markdown/Writer/Calc/Impress skill first
|
||||
for nontrivial document artifact work, load skill `document-artifacts` or the specific Markdown/Writer/Calc/Impress skill first
|
||||
|
|
|
|||
|
|
@ -28,10 +28,22 @@ class DocumentArtifact(Tool):
|
|||
chart: Any = None,
|
||||
slides: Any = None,
|
||||
max_chars: int | str = 12000,
|
||||
open_in_canvas: bool = False,
|
||||
open_in_desktop: bool = False,
|
||||
method: str = "",
|
||||
**kwargs: Any,
|
||||
) -> Response:
|
||||
action = str(action or method or self.method or "status").strip().lower().replace("-", "_")
|
||||
open_in_canvas = _truthy(
|
||||
open_in_canvas
|
||||
or kwargs.get("open_canvas")
|
||||
or kwargs.get("open_document")
|
||||
)
|
||||
open_in_desktop = _truthy(
|
||||
open_in_desktop
|
||||
or kwargs.get("open_desktop")
|
||||
or kwargs.get("desktop")
|
||||
)
|
||||
try:
|
||||
if action == "create":
|
||||
doc = document_store.create_document(
|
||||
|
|
@ -56,10 +68,22 @@ class DocumentArtifact(Tool):
|
|||
message=f"document_artifact create failed: {validation.get('error')}",
|
||||
break_loop=False,
|
||||
)
|
||||
return self._document_response("Created document artifact.", doc, action=action)
|
||||
return self._document_response(
|
||||
"Created document artifact.",
|
||||
doc,
|
||||
action=action,
|
||||
open_in_canvas=open_in_canvas,
|
||||
open_in_desktop=open_in_desktop,
|
||||
)
|
||||
if action == "open":
|
||||
doc = self._document_from_input(file_id=file_id, path=path)
|
||||
return self._document_response("Opened document artifact.", doc, action=action)
|
||||
return self._document_response(
|
||||
"Opened document artifact.",
|
||||
doc,
|
||||
action=action,
|
||||
open_in_canvas=open_in_canvas,
|
||||
open_in_desktop=open_in_desktop,
|
||||
)
|
||||
if action in {"read", "extract"}:
|
||||
doc = self._document_from_input(file_id=file_id, path=path)
|
||||
payload = {
|
||||
|
|
@ -68,7 +92,13 @@ class DocumentArtifact(Tool):
|
|||
"document": self._public_doc(doc),
|
||||
"content": artifact_editor.read_artifact(doc, max_chars=int(max_chars or 12000)),
|
||||
}
|
||||
return self._json_response(payload, doc=doc, action="read")
|
||||
return self._json_response(
|
||||
payload,
|
||||
doc=doc,
|
||||
action="read",
|
||||
open_in_canvas=open_in_canvas,
|
||||
open_in_desktop=open_in_desktop,
|
||||
)
|
||||
if action in {"edit", "update", "patch"}:
|
||||
doc = self._document_from_input(file_id=file_id, path=path)
|
||||
updated_doc, payload = artifact_editor.edit_artifact(
|
||||
|
|
@ -85,20 +115,44 @@ class DocumentArtifact(Tool):
|
|||
**kwargs,
|
||||
)
|
||||
payload["document"] = self._public_doc(updated_doc)
|
||||
return self._json_response(payload, doc=updated_doc, action="edit")
|
||||
return self._json_response(
|
||||
payload,
|
||||
doc=updated_doc,
|
||||
action="edit",
|
||||
open_in_canvas=open_in_canvas,
|
||||
open_in_desktop=open_in_desktop,
|
||||
)
|
||||
if action == "inspect":
|
||||
doc = self._document_from_input(file_id=file_id, path=path)
|
||||
return self._json_response({"ok": True, "action": action, "document": self._public_doc(doc)}, doc=doc, action=action)
|
||||
return self._json_response(
|
||||
{"ok": True, "action": action, "document": self._public_doc(doc)},
|
||||
doc=doc,
|
||||
action=action,
|
||||
open_in_canvas=open_in_canvas,
|
||||
open_in_desktop=open_in_desktop,
|
||||
)
|
||||
if action == "version_history":
|
||||
doc = self._document_from_input(file_id=file_id, path=path)
|
||||
versions = document_store.version_history(doc["file_id"])
|
||||
return self._json_response({"ok": True, "action": action, "versions": versions}, doc=doc, action=action)
|
||||
return self._json_response(
|
||||
{"ok": True, "action": action, "versions": versions},
|
||||
doc=doc,
|
||||
action=action,
|
||||
open_in_canvas=open_in_canvas,
|
||||
open_in_desktop=open_in_desktop,
|
||||
)
|
||||
if action == "restore_version":
|
||||
if version_id is None or str(version_id).strip() == "":
|
||||
return Response(message="version_id is required for restore_version.", break_loop=False)
|
||||
doc = self._document_from_input(file_id=file_id, path=path)
|
||||
restored = document_store.restore_version(doc["file_id"], int(version_id))
|
||||
return self._document_response("Restored document artifact version.", restored, action=action)
|
||||
return self._document_response(
|
||||
"Restored document artifact version.",
|
||||
restored,
|
||||
action=action,
|
||||
open_in_canvas=open_in_canvas,
|
||||
open_in_desktop=open_in_desktop,
|
||||
)
|
||||
if action == "export":
|
||||
doc = self._document_from_input(file_id=file_id, path=path)
|
||||
target_format = str(kwargs.get("target_format") or kwargs.get("export_format") or "").lower().lstrip(".")
|
||||
|
|
@ -111,13 +165,30 @@ class DocumentArtifact(Tool):
|
|||
"path": document_store.display_path(result["path"]),
|
||||
"document": self._public_doc(doc),
|
||||
}
|
||||
return self._json_response(payload, doc=doc, action=action)
|
||||
return self._json_response(
|
||||
payload,
|
||||
doc=doc,
|
||||
action=action,
|
||||
open_in_canvas=open_in_canvas,
|
||||
open_in_desktop=open_in_desktop,
|
||||
)
|
||||
return Response(
|
||||
message=f"document_artifact export failed: {result.get('error')}",
|
||||
break_loop=False,
|
||||
additional=self._additional(doc, action=action),
|
||||
additional=self._additional(
|
||||
doc,
|
||||
action=action,
|
||||
open_in_canvas=open_in_canvas,
|
||||
open_in_desktop=open_in_desktop,
|
||||
),
|
||||
)
|
||||
return self._document_response("Document artifact export path is ready.", doc, action=action)
|
||||
return self._document_response(
|
||||
"Document artifact export path is ready.",
|
||||
doc,
|
||||
action=action,
|
||||
open_in_canvas=open_in_canvas,
|
||||
open_in_desktop=open_in_desktop,
|
||||
)
|
||||
if action == "status":
|
||||
return self._json_response({"ok": True, "action": action, "status": libreoffice.collect_status()}, action=action)
|
||||
return Response(message=f"Unknown document_artifact action: {action}", break_loop=False)
|
||||
|
|
@ -143,28 +214,75 @@ class DocumentArtifact(Tool):
|
|||
def _context_id(self) -> str:
|
||||
return self.agent.context.id if self.agent and self.agent.context else ""
|
||||
|
||||
def _document_response(self, message: str, doc: dict[str, Any], action: str = "") -> Response:
|
||||
def _document_response(
|
||||
self,
|
||||
message: str,
|
||||
doc: dict[str, Any],
|
||||
action: str = "",
|
||||
*,
|
||||
open_in_canvas: bool = False,
|
||||
open_in_desktop: bool = False,
|
||||
) -> Response:
|
||||
payload = {"ok": True, "action": action, "message": message, "document": self._public_doc(doc)}
|
||||
return Response(
|
||||
message=json.dumps(payload, indent=2, ensure_ascii=False),
|
||||
break_loop=False,
|
||||
additional=self._additional(doc, action=action),
|
||||
additional=self._additional(
|
||||
doc,
|
||||
action=action,
|
||||
open_in_canvas=open_in_canvas,
|
||||
open_in_desktop=open_in_desktop,
|
||||
),
|
||||
)
|
||||
|
||||
def _json_response(self, payload: dict[str, Any], doc: dict[str, Any] | None = None, action: str = "") -> Response:
|
||||
def _json_response(
|
||||
self,
|
||||
payload: dict[str, Any],
|
||||
doc: dict[str, Any] | None = None,
|
||||
action: str = "",
|
||||
*,
|
||||
open_in_canvas: bool = False,
|
||||
open_in_desktop: bool = False,
|
||||
) -> Response:
|
||||
return Response(
|
||||
message=json.dumps(payload, indent=2, ensure_ascii=False, default=str),
|
||||
break_loop=False,
|
||||
additional=self._additional(doc, action=action) if doc else {"_tool_name": self.name, "canvas_surface": "office", "action": action},
|
||||
additional=self._additional(
|
||||
doc,
|
||||
action=action,
|
||||
open_in_canvas=open_in_canvas,
|
||||
open_in_desktop=open_in_desktop,
|
||||
) if doc else {
|
||||
"_tool_name": self.name,
|
||||
"canvas_surface": "office",
|
||||
"action": action,
|
||||
"open_in_canvas": bool(open_in_canvas),
|
||||
"open_in_desktop": bool(open_in_desktop),
|
||||
},
|
||||
)
|
||||
|
||||
def _additional(self, doc: dict[str, Any] | None, action: str = "") -> dict[str, Any]:
|
||||
def _additional(
|
||||
self,
|
||||
doc: dict[str, Any] | None,
|
||||
action: str = "",
|
||||
*,
|
||||
open_in_canvas: bool = False,
|
||||
open_in_desktop: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
if not doc:
|
||||
return {"_tool_name": self.name, "canvas_surface": "office", "action": action}
|
||||
return {
|
||||
"_tool_name": self.name,
|
||||
"canvas_surface": "office",
|
||||
"action": action,
|
||||
"open_in_canvas": bool(open_in_canvas),
|
||||
"open_in_desktop": bool(open_in_desktop),
|
||||
}
|
||||
return {
|
||||
"_tool_name": self.name,
|
||||
"canvas_surface": "office",
|
||||
"action": action,
|
||||
"open_in_canvas": bool(open_in_canvas),
|
||||
"open_in_desktop": bool(open_in_desktop),
|
||||
"file_id": doc["file_id"],
|
||||
"title": doc["basename"],
|
||||
"format": doc["extension"],
|
||||
|
|
@ -183,3 +301,13 @@ class DocumentArtifact(Tool):
|
|||
"last_modified": doc["last_modified"],
|
||||
"exists": Path(doc["path"]).exists(),
|
||||
}
|
||||
|
||||
|
||||
def _truthy(value: Any) -> bool:
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
if value is None:
|
||||
return False
|
||||
if isinstance(value, (int, float)):
|
||||
return value != 0
|
||||
return str(value).strip().lower() in {"1", "true", "yes", "y", "on"}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,6 @@
|
|||
<html
|
||||
class="office-modal modal-no-backdrop"
|
||||
data-canvas-surface="office"
|
||||
data-canvas-modal-path="/plugins/_office/webui/main.html"
|
||||
data-canvas-dock-title="Open Desktop in canvas"
|
||||
data-canvas-dock-icon="dock_to_right"
|
||||
>
|
||||
<html class="office-modal">
|
||||
<head>
|
||||
<title>Desktop</title>
|
||||
<title>Documents</title>
|
||||
<script type="module">
|
||||
import { store } from "/plugins/_office/webui/office-store.js";
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -5,123 +5,130 @@
|
|||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="office-panel" x-data x-create="$store.office.onMount($el, xAttrs($el) || {})" x-destroy="$store.office.cleanup()">
|
||||
<div x-data>
|
||||
<template x-if="$store.office">
|
||||
<div class="office-shell">
|
||||
<div class="office-document-header" x-show="$store.office.hasActiveFile()" style="display: none;">
|
||||
<div class="office-document-title" :title="$store.office.tabLabel($store.office.session)">
|
||||
<span class="material-symbols-outlined office-document-icon" aria-hidden="true" x-text="$store.office.tabIcon($store.office.session)"></span>
|
||||
<span class="office-document-name" x-text="$store.office.tabTitle($store.office.session)"></span>
|
||||
<span class="office-document-dirty" x-show="$store.office.dirty" aria-hidden="true">*</span>
|
||||
</div>
|
||||
<div class="office-panel" x-create="$store.office.onMount($el, xAttrs($el) || {})" x-destroy="$store.office.cleanup()">
|
||||
<div class="office-shell">
|
||||
<div class="office-document-header" x-show="$store.office.hasActiveFile()" style="display: none;">
|
||||
<div class="office-document-title" :title="$store.office.tabLabel($store.office.session)">
|
||||
<span class="material-symbols-outlined office-document-icon" aria-hidden="true" x-text="$store.office.tabIcon($store.office.session)"></span>
|
||||
<span class="office-document-name" x-text="$store.office.tabTitle($store.office.session)"></span>
|
||||
<span class="office-document-dirty" x-show="$store.office.dirty" aria-hidden="true">*</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="office-icon-button office-document-save-button"
|
||||
title="Save"
|
||||
aria-label="Save"
|
||||
:class="{ 'is-primary': $store.office.dirty }"
|
||||
:disabled="$store.office.saving"
|
||||
@click="$store.office.save()"
|
||||
>
|
||||
<span class="material-symbols-outlined" :class="{ spinning: $store.office.saving }" x-text="$store.office.saving ? 'progress_activity' : 'save'"></span>
|
||||
</button>
|
||||
|
||||
<div class="office-file-actions" x-data="{ open: false }" @click.outside="open = false" @keydown.escape.window="open = false">
|
||||
<button
|
||||
type="button"
|
||||
class="office-icon-button office-file-menu-button"
|
||||
title="File actions"
|
||||
aria-label="File actions"
|
||||
aria-haspopup="menu"
|
||||
:aria-expanded="open.toString()"
|
||||
class="office-icon-button office-document-save-button"
|
||||
title="Save"
|
||||
aria-label="Save"
|
||||
:class="{ 'is-primary': $store.office.dirty }"
|
||||
:disabled="$store.office.saving"
|
||||
@click.stop="open = !open"
|
||||
@click="$store.office.save()"
|
||||
>
|
||||
<span class="material-symbols-outlined">more_vert</span>
|
||||
<span class="material-symbols-outlined" :class="{ spinning: $store.office.saving }" x-text="$store.office.saving ? 'progress_activity' : 'save'"></span>
|
||||
</button>
|
||||
<div class="office-new-menu office-file-menu" role="menu" x-show="open" @click.stop>
|
||||
<button type="button" class="office-new-menu-item" role="menuitem" :disabled="$store.office.saving" @click="open = false; $store.office.renameActiveFile()">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">edit</span>
|
||||
<span>Rename</span>
|
||||
</button>
|
||||
<button type="button" class="office-new-menu-item" role="menuitem" :disabled="$store.office.loading" @click="open = false; $store.office.closeActiveFile()">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">close</span>
|
||||
<span>Close File</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="office-toolbar" x-show="$store.office.session && $store.office.isMarkdown()" style="display: none;">
|
||||
<div class="office-toolbar-row">
|
||||
<div class="office-tool-group office-editor-tools" x-show="$store.office.session && $store.office.isMarkdown()" style="display: none;">
|
||||
<button type="button" class="office-icon-button" title="Undo" aria-label="Undo" :disabled="!$store.office.canUndo()" @click="$store.office.undo()">
|
||||
<span class="material-symbols-outlined">undo</span>
|
||||
<div class="office-file-actions" x-data="{ open: false }" @click.outside="open = false" @keydown.escape.window="open = false">
|
||||
<button
|
||||
type="button"
|
||||
class="office-icon-button office-file-menu-button"
|
||||
title="File actions"
|
||||
aria-label="File actions"
|
||||
aria-haspopup="menu"
|
||||
:aria-expanded="open.toString()"
|
||||
:disabled="$store.office.saving"
|
||||
@click.stop="open = !open"
|
||||
>
|
||||
<span class="material-symbols-outlined">more_vert</span>
|
||||
</button>
|
||||
<button type="button" class="office-icon-button" title="Redo" aria-label="Redo" :disabled="!$store.office.canRedo()" @click="$store.office.redo()">
|
||||
<span class="material-symbols-outlined">redo</span>
|
||||
</button>
|
||||
<button type="button" class="office-icon-button" title="Bold" aria-label="Bold" @click="$store.office.format('bold')">
|
||||
<span class="material-symbols-outlined">format_bold</span>
|
||||
</button>
|
||||
<button type="button" class="office-icon-button" title="Italic" aria-label="Italic" @click="$store.office.format('italic')">
|
||||
<span class="material-symbols-outlined">format_italic</span>
|
||||
</button>
|
||||
<button type="button" class="office-icon-button" title="List" aria-label="List" @click="$store.office.format('list')">
|
||||
<span class="material-symbols-outlined">format_list_bulleted</span>
|
||||
</button>
|
||||
<button type="button" class="office-icon-button" title="Numbered list" aria-label="Numbered list" @click="$store.office.format('numbered')">
|
||||
<span class="material-symbols-outlined">format_list_numbered</span>
|
||||
</button>
|
||||
<button type="button" class="office-icon-button" title="Table" aria-label="Table" @click="$store.office.format('table')">
|
||||
<span class="material-symbols-outlined">table</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<span class="office-toolbar-spacer"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="office-state-line" x-show="$store.office.message || $store.office.error || $store.office.loading" style="display: none;">
|
||||
<span class="material-symbols-outlined" :class="{ spinning: $store.office.loading }" x-text="$store.office.loading ? 'progress_activity' : ($store.office.error ? 'error' : 'check_circle')"></span>
|
||||
<span x-text="$store.office.error || $store.office.message || 'Working'"></span>
|
||||
</div>
|
||||
|
||||
<div class="office-body" :class="{ 'is-source': $store.office.isMarkdown() }">
|
||||
<div class="office-editor-wrap" x-show="$store.office.session" style="display: none;">
|
||||
<div class="office-editor-scroll" :class="{ 'is-desktop': $store.office.hasOfficialOffice(), 'is-source': $store.office.isMarkdown() }" @click.self="$store.office.focusEditor()">
|
||||
<template x-if="$store.office.hasOfficialOffice()">
|
||||
<div
|
||||
class="office-desktop-wrap"
|
||||
data-office-desktop-host
|
||||
x-init="$nextTick(() => $store.office.mountDesktopFrameHost($el))"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<textarea
|
||||
class="office-source-editor"
|
||||
data-office-source
|
||||
aria-label="Markdown source"
|
||||
x-show="$store.office.isMarkdown()"
|
||||
x-model="$store.office.editorText"
|
||||
@input="$store.office.onSourceInput()"
|
||||
@blur="$store.office.flushInput()"
|
||||
spellcheck="true"
|
||||
style="display: none;"
|
||||
></textarea>
|
||||
|
||||
<div class="office-new-menu office-file-menu" role="menu" x-show="open" @click.stop>
|
||||
<button type="button" class="office-new-menu-item" role="menuitem" :disabled="$store.office.saving" @click="open = false; $store.office.renameActiveFile()">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">edit</span>
|
||||
<span>Rename</span>
|
||||
</button>
|
||||
<button type="button" class="office-new-menu-item" role="menuitem" :disabled="$store.office.loading" @click="open = false; $store.office.closeActiveFile()">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">close</span>
|
||||
<span>Close File</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="office-desktop-empty" x-show="$store.office.shouldShowDesktopEmptyState()" style="display: none;">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">power_settings_new</span>
|
||||
<span class="office-desktop-empty-title">Desktop is shut down</span>
|
||||
<button type="button" class="office-icon-button office-command-button" @click="$store.office.restartDesktopSession()">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">restart_alt</span>
|
||||
<span class="office-button-label">Restart Desktop</span>
|
||||
</button>
|
||||
<div class="office-toolbar" x-show="$store.office.session && $store.office.isMarkdown()" style="display: none;">
|
||||
<div class="office-toolbar-row">
|
||||
<div class="office-tool-group office-editor-tools">
|
||||
<button type="button" class="office-icon-button" title="Undo" aria-label="Undo" :disabled="!$store.office.canUndo()" @click="$store.office.undo()">
|
||||
<span class="material-symbols-outlined">undo</span>
|
||||
</button>
|
||||
<button type="button" class="office-icon-button" title="Redo" aria-label="Redo" :disabled="!$store.office.canRedo()" @click="$store.office.redo()">
|
||||
<span class="material-symbols-outlined">redo</span>
|
||||
</button>
|
||||
<button type="button" class="office-icon-button" title="Bold" aria-label="Bold" @click="$store.office.format('bold')">
|
||||
<span class="material-symbols-outlined">format_bold</span>
|
||||
</button>
|
||||
<button type="button" class="office-icon-button" title="Italic" aria-label="Italic" @click="$store.office.format('italic')">
|
||||
<span class="material-symbols-outlined">format_italic</span>
|
||||
</button>
|
||||
<button type="button" class="office-icon-button" title="List" aria-label="List" @click="$store.office.format('list')">
|
||||
<span class="material-symbols-outlined">format_list_bulleted</span>
|
||||
</button>
|
||||
<button type="button" class="office-icon-button" title="Numbered list" aria-label="Numbered list" @click="$store.office.format('numbered')">
|
||||
<span class="material-symbols-outlined">format_list_numbered</span>
|
||||
</button>
|
||||
<button type="button" class="office-icon-button" title="Table" aria-label="Table" @click="$store.office.format('table')">
|
||||
<span class="material-symbols-outlined">table</span>
|
||||
</button>
|
||||
</div>
|
||||
<span class="office-toolbar-spacer"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="office-state-line" x-show="$store.office.message || $store.office.error || $store.office.loading" style="display: none;">
|
||||
<span class="material-symbols-outlined" :class="{ spinning: $store.office.loading }" x-text="$store.office.loading ? 'progress_activity' : ($store.office.error ? 'error' : 'check_circle')"></span>
|
||||
<span x-text="$store.office.error || $store.office.message || 'Working'"></span>
|
||||
</div>
|
||||
|
||||
<div class="office-body" :class="{ 'is-source': $store.office.isMarkdown() }">
|
||||
<div class="office-editor-wrap" x-show="$store.office.session" style="display: none;">
|
||||
<div class="office-editor-scroll is-source" @click.self="$store.office.focusEditor()">
|
||||
<textarea
|
||||
class="office-source-editor"
|
||||
data-office-source
|
||||
aria-label="Markdown source"
|
||||
x-show="$store.office.isMarkdown()"
|
||||
x-model="$store.office.editorText"
|
||||
@input="$store.office.onSourceInput()"
|
||||
@blur="$store.office.flushInput()"
|
||||
spellcheck="true"
|
||||
style="display: none;"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="office-empty" x-show="!$store.office.session && !$store.office.loading" style="display: none;">
|
||||
<div class="office-empty-actions">
|
||||
<button type="button" class="office-icon-button office-command-button" @click="$store.office.runNewMenuAction('open')">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">folder_open</span>
|
||||
<span class="office-button-label">Open</span>
|
||||
</button>
|
||||
<button type="button" class="office-icon-button office-command-button" @click="$store.office.runNewMenuAction('markdown')">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">article</span>
|
||||
<span class="office-button-label">Markdown</span>
|
||||
</button>
|
||||
<button type="button" class="office-icon-button office-command-button" @click="$store.office.runNewMenuAction('writer')">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">description</span>
|
||||
<span class="office-button-label">Writer</span>
|
||||
</button>
|
||||
<button type="button" class="office-icon-button office-command-button" @click="$store.office.runNewMenuAction('spreadsheet')">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">table_chart</span>
|
||||
<span class="office-button-label">Calc</span>
|
||||
</button>
|
||||
<button type="button" class="office-icon-button office-command-button" @click="$store.office.runNewMenuAction('presentation')">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">co_present</span>
|
||||
<span class="office-button-label">Impress</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -148,24 +155,13 @@
|
|||
|
||||
.modal-inner.office-modal {
|
||||
box-sizing: border-box;
|
||||
width: min(1120px, calc(100vw - 32px));
|
||||
height: min(820px, calc(100vh - 32px));
|
||||
min-width: min(720px, calc(100vw - 16px));
|
||||
min-height: min(520px, calc(100vh - 16px));
|
||||
width: min(1040px, calc(100vw - 32px));
|
||||
height: min(760px, calc(100vh - 32px));
|
||||
min-width: min(640px, calc(100vw - 16px));
|
||||
min-height: min(460px, calc(100vh - 16px));
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
resize: none;
|
||||
overflow: hidden;
|
||||
will-change: width, height, left, top;
|
||||
}
|
||||
|
||||
.modal-inner.office-modal.is-resizing,
|
||||
.modal-inner.office-modal.is-dragging {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.modal-inner.office-modal.is-focus-mode {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.modal-inner.office-modal .modal-scroll {
|
||||
|
|
@ -178,592 +174,211 @@
|
|||
}
|
||||
|
||||
.modal-inner.office-modal .modal-header {
|
||||
grid-template-columns: minmax(0, 1fr) repeat(5, auto);
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
}
|
||||
|
||||
.office-modal-input-shield {
|
||||
position: absolute;
|
||||
inset: 42px 0 0 0;
|
||||
z-index: 4;
|
||||
display: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.office-modal-resizer {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
display: block;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.office-modal-resizer.is-right {
|
||||
top: 42px;
|
||||
right: -4px;
|
||||
bottom: 12px;
|
||||
width: 10px;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.office-modal-resizer.is-bottom {
|
||||
right: 12px;
|
||||
bottom: -4px;
|
||||
left: 0;
|
||||
height: 10px;
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.office-modal-resizer.is-corner {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
.office-modal-resizer.is-corner::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
bottom: 6px;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-right: 2px solid color-mix(in srgb, var(--color-text) 42%, transparent);
|
||||
border-bottom: 2px solid color-mix(in srgb, var(--color-text) 42%, transparent);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.modal-inner.office-modal.is-focus-mode .office-modal-resizer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal-inner.office-modal .modal-bd.office-modal-body,
|
||||
.modal-inner.office-modal .modal-bd.office-modal-body > x-component,
|
||||
.modal-inner.office-modal .modal-bd.office-modal-body > x-component > .office-panel {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.office-toolbar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
flex-wrap: nowrap;
|
||||
min-height: 0;
|
||||
padding: 6px 10px;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--color-border), transparent 20%);
|
||||
background: color-mix(in srgb, var(--color-background), var(--color-panel) 48%);
|
||||
}
|
||||
|
||||
.office-toolbar-row {
|
||||
.office-document-header,
|
||||
.office-toolbar,
|
||||
.office-state-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
flex-wrap: nowrap;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
gap: 8px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding: 8px 10px;
|
||||
min-width: 0;
|
||||
min-height: 32px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.office-tool-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
flex: 0 0 auto;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.office-editor-tools {
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.office-tool-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.office-toolbar-spacer {
|
||||
flex: 1 1 16px;
|
||||
min-width: 8px;
|
||||
}
|
||||
|
||||
.office-toolbar-divider {
|
||||
flex: 0 0 auto;
|
||||
width: 1px;
|
||||
height: 22px;
|
||||
margin-inline: 2px;
|
||||
background: color-mix(in srgb, var(--color-border), transparent 18%);
|
||||
}
|
||||
|
||||
.office-document-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 38px;
|
||||
padding: 5px 10px 5px 12px;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--color-border), transparent 22%);
|
||||
background: color-mix(in srgb, var(--color-panel), var(--color-background) 30%);
|
||||
}
|
||||
|
||||
.office-document-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.office-document-icon {
|
||||
flex: 0 0 auto;
|
||||
font-size: 19px;
|
||||
line-height: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.office-document-name {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 13px;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.office-document-dirty {
|
||||
flex: 0 0 auto;
|
||||
color: #2ca58d;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.office-icon-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
height: 32px;
|
||||
min-width: 32px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
background: var(--color-background);
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.office-icon-button:hover:not(:disabled) {
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
.office-icon-button:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.office-document-save-button.is-primary {
|
||||
border-color: var(--color-accent);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.office-file-actions {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.office-document-save-button,
|
||||
.office-file-menu-button {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
min-width: 30px;
|
||||
}
|
||||
|
||||
.office-header-actions {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.office-header-new-button {
|
||||
appearance: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
height: 34px;
|
||||
min-height: 34px;
|
||||
padding: 0 9px 0 8px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 7px;
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0;
|
||||
line-height: 1;
|
||||
opacity: 0.82;
|
||||
white-space: nowrap;
|
||||
transition: background-color 0.16s ease, border-color 0.16s ease, opacity 0.16s ease;
|
||||
}
|
||||
|
||||
.office-header-new-button:hover,
|
||||
.office-header-actions.is-open .office-header-new-button {
|
||||
opacity: 1;
|
||||
border-color: color-mix(in srgb, var(--color-primary) 28%, var(--color-border));
|
||||
background: color-mix(in srgb, var(--color-background-hover) 72%, transparent);
|
||||
}
|
||||
|
||||
.office-header-new-button .material-symbols-outlined {
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.office-header-new-button .office-new-chevron {
|
||||
margin-left: -2px;
|
||||
font-size: 16px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.office-new-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
min-width: 184px;
|
||||
padding: 5px;
|
||||
border: 1px solid color-mix(in srgb, var(--color-border), transparent 10%);
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--color-panel), var(--color-background) 10%);
|
||||
box-shadow: 0 14px 34px rgba(0, 0, 0, 0.34);
|
||||
}
|
||||
|
||||
.office-new-menu[hidden] {
|
||||
display: none;
|
||||
z-index: 15;
|
||||
display: grid;
|
||||
min-width: 172px;
|
||||
padding: 6px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
background: var(--color-background);
|
||||
box-shadow: 0 16px 36px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.office-new-menu-item {
|
||||
appearance: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
padding: 0 8px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
border: 0;
|
||||
border-radius: 5px;
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
letter-spacing: 0;
|
||||
line-height: 1;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.office-new-menu-item:hover {
|
||||
border-color: color-mix(in srgb, var(--color-primary) 22%, transparent);
|
||||
background: color-mix(in srgb, var(--color-background-hover) 76%, transparent);
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
.office-new-menu-item:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.46;
|
||||
}
|
||||
|
||||
.office-new-menu-item .material-symbols-outlined {
|
||||
flex: 0 0 auto;
|
||||
width: 18px;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.office-icon-button,
|
||||
.office-tab,
|
||||
.office-tab-close {
|
||||
border: 1px solid color-mix(in srgb, var(--color-border), transparent 12%);
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--color-panel), var(--color-background) 16%);
|
||||
color: inherit;
|
||||
transition: border-color 120ms ease, background 120ms ease, transform 120ms ease;
|
||||
}
|
||||
|
||||
.office-icon-button {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
min-width: 32px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.office-command-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
width: auto;
|
||||
max-width: 126px;
|
||||
padding: 0 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.office-command-button .office-button-label {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.office-icon-button.is-primary {
|
||||
border-color: color-mix(in srgb, #2c7be5, var(--color-border) 20%);
|
||||
background: color-mix(in srgb, #2c7be5, var(--color-panel) 82%);
|
||||
}
|
||||
|
||||
.office-icon-button.is-active {
|
||||
border-color: color-mix(in srgb, #2ca58d, var(--color-border) 24%);
|
||||
background: color-mix(in srgb, #2ca58d, var(--color-panel) 84%);
|
||||
}
|
||||
|
||||
.office-icon-button:hover:not(:disabled),
|
||||
.office-tab:hover,
|
||||
.office-tab-close:hover {
|
||||
border-color: color-mix(in srgb, #2c7be5, var(--color-border) 45%);
|
||||
background: color-mix(in srgb, var(--color-panel), #2c7be5 8%);
|
||||
}
|
||||
|
||||
.office-icon-button:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.42;
|
||||
}
|
||||
|
||||
.office-icon-button .material-symbols-outlined,
|
||||
.office-tab-icon {
|
||||
font-size: 19px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.office-tabs {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
min-height: 42px;
|
||||
padding: 7px 10px;
|
||||
overflow-x: auto;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--color-border), transparent 22%);
|
||||
background: color-mix(in srgb, var(--color-panel), var(--color-background) 28%);
|
||||
}
|
||||
|
||||
.office-tab-shell {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 28px;
|
||||
align-items: center;
|
||||
min-width: 150px;
|
||||
max-width: 240px;
|
||||
}
|
||||
|
||||
.office-tab-shell.is-system {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
min-width: 172px;
|
||||
}
|
||||
|
||||
.office-tab,
|
||||
.office-tab-close {
|
||||
height: 28px;
|
||||
min-height: 28px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.office-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
padding: 0 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.office-tab-shell.is-system .office-tab {
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.office-tab-close {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-left: 0;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.office-tab-close .material-symbols-outlined {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.office-tab-shell.is-active .office-tab,
|
||||
.office-tab-shell.is-active .office-tab-close {
|
||||
border-color: color-mix(in srgb, #2c7be5, var(--color-border) 36%);
|
||||
background: color-mix(in srgb, #2c7be5, var(--color-panel) 88%);
|
||||
}
|
||||
|
||||
.office-tab-shell.is-dirty .office-tab-title::after {
|
||||
content: " *";
|
||||
color: #2ca58d;
|
||||
}
|
||||
|
||||
.office-tab-title {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.office-state-line {
|
||||
.office-toolbar-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 34px;
|
||||
padding: 6px 12px;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--color-border), transparent 28%);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.office-tool-group,
|
||||
.office-empty-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.office-toolbar-spacer {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.office-state-line {
|
||||
font-size: 13px;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
|
||||
.office-body {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background: var(--color-background);
|
||||
}
|
||||
|
||||
.office-body.is-source {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.office-editor-wrap {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.office-editor-wrap,
|
||||
.office-editor-scroll {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 30px 24px;
|
||||
}
|
||||
|
||||
.office-editor-scroll.is-source {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
padding: var(--spacing-md);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.office-editor-scroll.is-desktop {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
background: #1f2329;
|
||||
}
|
||||
|
||||
.office-desktop-wrap {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
aspect-ratio: auto;
|
||||
background: #1f2329;
|
||||
}
|
||||
|
||||
.office-desktop-empty {
|
||||
display: grid;
|
||||
flex: 1 1 auto;
|
||||
place-items: center;
|
||||
align-content: center;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: 24px;
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.office-desktop-empty > .material-symbols-outlined {
|
||||
font-size: 32px;
|
||||
color: color-mix(in srgb, var(--color-text) 68%, transparent);
|
||||
}
|
||||
|
||||
.office-desktop-empty-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.office-desktop-frame {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
aspect-ratio: auto;
|
||||
border: 0;
|
||||
background: #20242a;
|
||||
}
|
||||
|
||||
.office-source-editor {
|
||||
box-sizing: border-box;
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
resize: none;
|
||||
border: 0;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.65;
|
||||
resize: none;
|
||||
padding: 16px;
|
||||
background: var(--color-background);
|
||||
color: var(--color-text);
|
||||
font: 14px/1.55 var(--font-mono, monospace);
|
||||
}
|
||||
|
||||
textarea:focus {
|
||||
background: transparent;
|
||||
filter: brightness(1) !important;
|
||||
.office-empty {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.office-panel .spinning {
|
||||
animation: office-spin 0.8s linear infinite;
|
||||
.office-command-button {
|
||||
min-width: 108px;
|
||||
justify-content: flex-start;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
@keyframes office-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
.office-header-actions {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@container (max-width: 680px) {
|
||||
.office-toolbar {
|
||||
padding-inline: 8px;
|
||||
.office-header-new-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
height: 32px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
background: var(--color-background);
|
||||
color: var(--color-text);
|
||||
padding: 0 9px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.office-header-actions .office-new-menu {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
@container (max-width: 560px) {
|
||||
.office-document-header,
|
||||
.office-toolbar,
|
||||
.office-state-line {
|
||||
padding: 7px;
|
||||
}
|
||||
|
||||
.office-toolbar-row {
|
||||
gap: 5px;
|
||||
.office-button-label,
|
||||
.office-header-new-button span:not(.material-symbols-outlined) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.office-command-button {
|
||||
width: 32px;
|
||||
max-width: 32px;
|
||||
padding-inline: 0;
|
||||
min-width: 32px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.office-command-button .office-button-label {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 17 KiB |
Loading…
Add table
Add a link
Reference in a new issue