Make QOffice apps standalone Office-style surfaces

This commit is contained in:
Курнат Андрей
2026-06-01 23:08:56 +03:00
parent 3418725017
commit 91e8a6e9fd
7 changed files with 900 additions and 744 deletions
+25 -98
View File
@@ -1,23 +1,16 @@
import {
BarChart3,
FileText,
HelpCircle,
Mail,
MonitorCog,
PanelRight,
Presentation,
ChevronDown,
Redo2,
Save,
Search,
Settings,
Share2,
Table2
Undo2
} from "lucide-react";
import type { ReactNode } from "react";
import type { AppId } from "../types";
interface AppShellProps {
activeApp: AppId;
onAppChange: (appId: AppId) => void;
status: {
savedLabel: string;
updatedAt?: string;
@@ -25,85 +18,37 @@ interface AppShellProps {
children: ReactNode;
}
const apps: Array<{ id: AppId; label: string; icon: typeof FileText; accent: string }> = [
{ id: "word", label: "QWord", icon: FileText, accent: "word" },
{ id: "sheets", label: "QExcell", icon: Table2, accent: "sheets" },
{ id: "slides", label: "QPowerPoint", icon: Presentation, accent: "slides" },
{ id: "mail", label: "QOutlook", icon: Mail, accent: "mail" }
];
const ribbonGroups = [
{
title: "Буфер",
commands: [
{ label: "Вставить", icon: FileText },
{ label: "Копировать", icon: Save }
]
},
{
title: "Вставка",
commands: [
{ label: "Таблица", icon: Table2 },
{ label: "Диаграмма", icon: BarChart3 },
{ label: "Панель", icon: PanelRight }
]
},
{
title: "Совместная работа",
commands: [
{ label: "Поделиться", icon: Share2 },
{ label: "Параметры", icon: MonitorCog }
]
}
];
export function AppShell({ activeApp, onAppChange, status, children }: AppShellProps) {
const active = apps.find((app) => app.id === activeApp) ?? apps[0];
const appTitles: Record<AppId, string> = {
word: "Документ1 - QWord",
sheets: "Книга1 - QExcell",
slides: "Презентация1 - QPowerPoint",
mail: "Почта - QOutlook"
};
export function AppShell({ activeApp, status, children }: AppShellProps) {
return (
<div className="qoffice-shell">
<aside className="app-rail" aria-label="Приложения QOffice">
<div className="brand">QOffice</div>
<nav className="app-nav">
{apps.map((app) => {
const Icon = app.icon;
return (
<button
key={app.id}
className={`app-nav-item accent-${app.accent} ${activeApp === app.id ? "is-active" : ""}`}
type="button"
onClick={() => onAppChange(app.id)}
aria-pressed={activeApp === app.id}
title={`Открыть ${app.label}`}
>
<span className="app-icon">
<Icon size={22} strokeWidth={2.2} />
</span>
<span>{app.label}</span>
</button>
);
})}
</nav>
<div className="rail-footer">
<button className="icon-button" type="button" title="Параметры" aria-label="Параметры">
<Settings size={18} />
</button>
<button className="icon-button" type="button" title="Справка" aria-label="Справка">
<HelpCircle size={18} />
</button>
</div>
</aside>
<main className="workspace">
<header className="titlebar">
<div>
<strong>{active.label}</strong>
<span>{status.savedLabel}</span>
<div className="quick-access" aria-label="Панель быстрого доступа">
<button type="button" title="Сохранить" aria-label="Сохранить">
<Save size={16} />
</button>
<button type="button" title="Отменить" aria-label="Отменить">
<Undo2 size={16} />
</button>
<button type="button" title="Повторить" aria-label="Повторить">
<Redo2 size={16} />
</button>
<button type="button" title="Настроить панель" aria-label="Настроить панель">
<ChevronDown size={14} />
</button>
</div>
<strong className="window-title">{appTitles[activeApp]}</strong>
<div className="titlebar-actions">
<label className="command-search">
<Search size={16} />
<input placeholder="Найти команду" aria-label="Найти команду" />
<input placeholder="Поиск" aria-label="Поиск" />
</label>
<button className="primary-command" type="button">
<Share2 size={16} />
@@ -120,24 +65,6 @@ export function AppShell({ activeApp, onAppChange, status, children }: AppShellP
</button>
))}
</div>
<div className="ribbon-groups">
{ribbonGroups.map((group) => (
<div className="ribbon-group" key={group.title}>
<div className="ribbon-command-row">
{group.commands.map((command) => {
const Icon = command.icon;
return (
<button type="button" key={command.label} className="tool-button" title={command.label}>
<Icon size={17} />
<span>{command.label}</span>
</button>
);
})}
</div>
<span className="ribbon-group-title">{group.title}</span>
</div>
))}
</div>
</section>
<div className="surface-host">{children}</div>