Files
plainleaf/client/components/top_bar.tsx
T
2026-03-10 16:07:25 +01:00

194 lines
6.1 KiB
TypeScript

import type { ComponentChildren, FunctionalComponent } from "preact";
import type { Notification } from "@silverbulletmd/silverbullet/type/client";
import { MiniEditor } from "./mini_editor.tsx";
export type ActionButton = {
icon: FunctionalComponent<any>;
description: string;
class?: string;
callback: () => void;
href?: string;
mobile?: boolean;
};
export function TopBar({
pageName,
unsavedChanges,
isOnline,
isLoading,
notifications,
onRename,
onDismissNotification,
actionButtons,
darkMode,
vimMode,
progressPercentage,
progressType,
lhs,
onClick,
rhs,
pageNamePrefix,
cssClass,
mobileMenuStyle,
}: {
pageName?: string;
unsavedChanges: boolean;
isOnline: boolean;
isLoading: boolean;
notifications: Notification[];
darkMode?: boolean;
vimMode: boolean;
progressPercentage?: number;
progressType?: string;
onRename: (newName?: string) => Promise<void>;
onDismissNotification: (id: number) => void;
onClick: () => void;
actionButtons: ActionButton[];
lhs?: ComponentChildren;
rhs?: ComponentChildren;
pageNamePrefix?: string;
cssClass?: string;
mobileMenuStyle?: string;
}) {
return (
<div
id="sb-top"
className={isOnline ? undefined : "sb-sync-error"}
onClick={onClick}
>
{lhs}
<div className="main">
<div className="inner">
<div className="wrapper">
<div className="sb-page-prefix">{pageNamePrefix}</div>
<span
id="sb-current-page"
className={
(isLoading
? "sb-loading"
: unsavedChanges
? "sb-unsaved"
: "sb-saved") +
(cssClass ? ` sb-decorated-object ${cssClass}` : "")
}
>
<MiniEditor
text={pageName ?? ""}
vimMode={vimMode}
darkMode={darkMode}
onBlur={(newName) => {
if (newName !== pageName) {
return onRename(newName);
} else {
return onRename();
}
}}
onEnter={(newName) => {
void onRename(newName);
}}
editable={
!client.ui.viewState.uiOptions.forcedROMode &&
!client.bootConfig.readOnly
}
/>
</span>
{notifications.length > 0 && (
<div className="sb-notifications">
{notifications.map((notification) => (
<div
key={notification.id}
className={`sb-notification-${notification.type}`}
>
<span className="sb-notification-message">
{notification.message}
</span>
{notification.actions && notification.actions.length > 0 && (
<span className="sb-notification-actions">
{notification.actions.map((action, i) => (
<button
key={i}
className="sb-button"
onClick={(e) => {
e.stopPropagation();
action.run();
}}
>
{action.name}
</button>
))}
</span>
)}
{notification.persistent && (
<button
className="sb-notification-dismiss"
onClick={(e) => {
e.stopPropagation();
onDismissNotification(notification.id);
}}
>
&times;
</button>
)}
</div>
))}
</div>
)}
<div className="sb-sync-progress">
{progressPercentage !== undefined && (
<div
className="progress-wrapper"
title={`${progressType} progress: ${progressPercentage}%`}
>
<div
className="progress-bar"
style={`background: radial-gradient(closest-side, var(--top-background-color) 79%, transparent 80% 100%), conic-gradient(var(--progress-${progressType}-color) ${progressPercentage}%, var(--progress-background-color) 0);`}
>
{progressPercentage}
</div>
</div>
)}
</div>
<div
className={`sb-actions ${mobileMenuStyle ? mobileMenuStyle : ""}`}
>
{actionButtons.map((actionButton) => {
const button = (
<button
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
actionButton.callback();
}}
onBlur={() => {
// Close the hamburger menu in mobile mode if the action button loses focus after callback
if (mobileMenuStyle === "hamburger") {
document
.querySelector("#sb-top .sb-actions.hamburger")
?.classList.remove("open");
}
}}
title={actionButton.description}
className={actionButton.class}
>
<actionButton.icon size={18} />
</button>
);
return actionButton.href ? (
<a href={actionButton.href} key={actionButton.href}>
{button}
</a>
) : (
button
);
})}
</div>
</div>
</div>
</div>
{rhs}
</div>
);
}