mirror of
https://github.com/zadam/trilium.git
synced 2025-12-17 05:39:55 +01:00
New layout: Title bar & inline title (#8044)
This commit is contained in:
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -37,6 +37,9 @@
|
|||||||
"apps/server/src/assets/doc_notes/**": true,
|
"apps/server/src/assets/doc_notes/**": true,
|
||||||
"apps/edit-docs/demo/**": true
|
"apps/edit-docs/demo/**": true
|
||||||
},
|
},
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit"
|
||||||
|
},
|
||||||
"eslint.rules.customizations": [
|
"eslint.rules.customizations": [
|
||||||
{ "rule": "*", "severity": "warn" }
|
{ "rule": "*", "severity": "warn" }
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -49,9 +49,10 @@ import { isExperimentalFeatureEnabled } from "../services/experimental_features.
|
|||||||
import NoteActions from "../widgets/ribbon/NoteActions.jsx";
|
import NoteActions from "../widgets/ribbon/NoteActions.jsx";
|
||||||
import FormattingToolbar from "../widgets/ribbon/FormattingToolbar.jsx";
|
import FormattingToolbar from "../widgets/ribbon/FormattingToolbar.jsx";
|
||||||
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
|
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
|
||||||
import BreadcrumbBadges from "../widgets/BreadcrumbBadges.jsx";
|
import NoteBadges from "../widgets/layout/NoteBadges.jsx";
|
||||||
import NoteTitleDetails from "../widgets/NoteTitleDetails.jsx";
|
import NoteTitleActions from "../widgets/layout/NoteTitleActions.jsx";
|
||||||
import StatusBar from "../widgets/layout/StatusBar.jsx";
|
import StatusBar from "../widgets/layout/StatusBar.jsx";
|
||||||
|
import InlineTitle from "../widgets/layout/InlineTitle.jsx";
|
||||||
|
|
||||||
export default class DesktopLayout {
|
export default class DesktopLayout {
|
||||||
|
|
||||||
@@ -78,12 +79,19 @@ export default class DesktopLayout {
|
|||||||
const fullWidthTabBar = launcherPaneIsHorizontal || (isElectron && !hasNativeTitleBar && isMac);
|
const fullWidthTabBar = launcherPaneIsHorizontal || (isElectron && !hasNativeTitleBar && isMac);
|
||||||
const customTitleBarButtons = !hasNativeTitleBar && !isMac && !isWindows;
|
const customTitleBarButtons = !hasNativeTitleBar && !isMac && !isWindows;
|
||||||
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
||||||
const isFloatingTitlebar = isExperimentalFeatureEnabled("floating-titlebar");
|
|
||||||
|
|
||||||
const titleRow = new FlexContainer("row")
|
const titleRow = new FlexContainer("row")
|
||||||
.class("title-row")
|
.class("title-row")
|
||||||
|
.cssBlock(".title-row > * { margin: 5px; }")
|
||||||
.child(<NoteIconWidget />)
|
.child(<NoteIconWidget />)
|
||||||
.child(<NoteTitleWidget />);
|
.child(<NoteTitleWidget />)
|
||||||
|
.optChild(isNewLayout, <NoteBadges />)
|
||||||
|
.optChild(!isNewLayout, <SpacerWidget baseSize={0} growthFactor={1} />)
|
||||||
|
.child(<MovePaneButton direction="left" />)
|
||||||
|
.child(<MovePaneButton direction="right" />)
|
||||||
|
.child(<ClosePaneButton />)
|
||||||
|
.child(<CreatePaneButton />)
|
||||||
|
.optChild(isNewLayout, <NoteActions />);
|
||||||
|
|
||||||
const rootContainer = new RootContainer(true)
|
const rootContainer = new RootContainer(true)
|
||||||
.setParent(appContext)
|
.setParent(appContext)
|
||||||
@@ -137,19 +145,7 @@ export default class DesktopLayout {
|
|||||||
.child(
|
.child(
|
||||||
new SplitNoteContainer(() =>
|
new SplitNoteContainer(() =>
|
||||||
new NoteWrapperWidget()
|
new NoteWrapperWidget()
|
||||||
.child(
|
.child(titleRow)
|
||||||
new FlexContainer("row")
|
|
||||||
.class("breadcrumb-row")
|
|
||||||
.cssBlock(".breadcrumb-row > * { margin: 5px; }")
|
|
||||||
.optChild(isNewLayout, <BreadcrumbBadges />)
|
|
||||||
.child(<SpacerWidget baseSize={0} growthFactor={1} />)
|
|
||||||
.child(<MovePaneButton direction="left" />)
|
|
||||||
.child(<MovePaneButton direction="right" />)
|
|
||||||
.child(<ClosePaneButton />)
|
|
||||||
.child(<CreatePaneButton />)
|
|
||||||
.optChild(isNewLayout, <NoteActions />)
|
|
||||||
)
|
|
||||||
.optChild(!isFloatingTitlebar, titleRow)
|
|
||||||
.optChild(!isNewLayout, <Ribbon><NoteActions /></Ribbon>)
|
.optChild(!isNewLayout, <Ribbon><NoteActions /></Ribbon>)
|
||||||
.optChild(isNewLayout, <Ribbon />)
|
.optChild(isNewLayout, <Ribbon />)
|
||||||
.child(new WatchedFileUpdateStatusWidget())
|
.child(new WatchedFileUpdateStatusWidget())
|
||||||
@@ -157,8 +153,8 @@ export default class DesktopLayout {
|
|||||||
.child(
|
.child(
|
||||||
new ScrollingContainer()
|
new ScrollingContainer()
|
||||||
.filling()
|
.filling()
|
||||||
.optChild(isFloatingTitlebar, titleRow)
|
.optChild(isNewLayout, <InlineTitle />)
|
||||||
.optChild(isNewLayout, <NoteTitleDetails />)
|
.optChild(isNewLayout, <NoteTitleActions />)
|
||||||
.optChild(!isNewLayout, new ContentHeader()
|
.optChild(!isNewLayout, new ContentHeader()
|
||||||
.child(<ReadOnlyNoteInfoBar />)
|
.child(<ReadOnlyNoteInfoBar />)
|
||||||
.child(<SharedInfo />)
|
.child(<SharedInfo />)
|
||||||
|
|||||||
@@ -12,11 +12,6 @@ export const experimentalFeatures = [
|
|||||||
id: "new-layout",
|
id: "new-layout",
|
||||||
name: t("experimental_features.new_layout_name"),
|
name: t("experimental_features.new_layout_name"),
|
||||||
description: t("experimental_features.new_layout_description"),
|
description: t("experimental_features.new_layout_description"),
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "floating-titlebar",
|
|
||||||
name: t("experimental_features.floating_titlebar"),
|
|
||||||
description: t("experimental_features.floating_titlebar_description"),
|
|
||||||
}
|
}
|
||||||
] as const satisfies ExperimentalFeature[];
|
] as const satisfies ExperimentalFeature[];
|
||||||
|
|
||||||
|
|||||||
@@ -1102,9 +1102,7 @@
|
|||||||
"title": "Experimental Options",
|
"title": "Experimental Options",
|
||||||
"disclaimer": "These options are experimental and may cause instability. Use with caution.",
|
"disclaimer": "These options are experimental and may cause instability. Use with caution.",
|
||||||
"new_layout_name": "New Layout",
|
"new_layout_name": "New Layout",
|
||||||
"new_layout_description": "Try out the new layout for a more modern look and improved usability. Subject to heavy change in the upcoming releases.",
|
"new_layout_description": "Try out the new layout for a more modern look and improved usability. Subject to heavy change in the upcoming releases."
|
||||||
"floating_titlebar": "Floating Titlebar",
|
|
||||||
"floating_titlebar_description": "The title bar is part of the content and is scrolled along with the note content."
|
|
||||||
},
|
},
|
||||||
"fonts": {
|
"fonts": {
|
||||||
"theme_defined": "Theme defined",
|
"theme_defined": "Theme defined",
|
||||||
@@ -1755,7 +1753,11 @@
|
|||||||
"note_title": {
|
"note_title": {
|
||||||
"placeholder": "type note's title here...",
|
"placeholder": "type note's title here...",
|
||||||
"created_on": "Created on <Value />",
|
"created_on": "Created on <Value />",
|
||||||
"last_modified": "Last modified on <Value />"
|
"last_modified": "Last modified on <Value />",
|
||||||
|
"note_type_switcher_label": "Switch from {{type}} to:",
|
||||||
|
"note_type_switcher_others": "More note types",
|
||||||
|
"note_type_switcher_templates": "Templates",
|
||||||
|
"note_type_switcher_collection": "Collections"
|
||||||
},
|
},
|
||||||
"search_result": {
|
"search_result": {
|
||||||
"no_notes_found": "No notes have been found for given search parameters.",
|
"no_notes_found": "No notes have been found for given search parameters.",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import ReadOnlyNoteInfoBar from "../ReadOnlyNoteInfoBar";
|
|||||||
import StandaloneRibbonAdapter from "../ribbon/components/StandaloneRibbonAdapter";
|
import StandaloneRibbonAdapter from "../ribbon/components/StandaloneRibbonAdapter";
|
||||||
import FormattingToolbar from "../ribbon/FormattingToolbar";
|
import FormattingToolbar from "../ribbon/FormattingToolbar";
|
||||||
import MobileEditorToolbar from "../type_widgets/text/mobile_editor_toolbar";
|
import MobileEditorToolbar from "../type_widgets/text/mobile_editor_toolbar";
|
||||||
import BreadcrumbBadges from "../BreadcrumbBadges";
|
import NoteBadges from "../layout/NoteBadges";
|
||||||
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
|
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
|
||||||
|
|
||||||
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
||||||
@@ -69,7 +69,7 @@ export default function PopupEditor() {
|
|||||||
<Modal
|
<Modal
|
||||||
title={<>
|
title={<>
|
||||||
<TitleRow />
|
<TitleRow />
|
||||||
{isNewLayout && <BreadcrumbBadges />}
|
{isNewLayout && <NoteBadges />}
|
||||||
</>}
|
</>}
|
||||||
customTitleBarButtons={[{
|
customTitleBarButtons={[{
|
||||||
iconClassName: "bx-expand-alt",
|
iconClassName: "bx-expand-alt",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import note_types from "../../services/note_types";
|
|||||||
import { MenuCommandItem, MenuItem } from "../../menus/context_menu";
|
import { MenuCommandItem, MenuItem } from "../../menus/context_menu";
|
||||||
import { TreeCommandNames } from "../../menus/tree_context_menu";
|
import { TreeCommandNames } from "../../menus/tree_context_menu";
|
||||||
import { Suggestion } from "../../services/note_autocomplete";
|
import { Suggestion } from "../../services/note_autocomplete";
|
||||||
import Badge from "../react/Badge";
|
import SimpleBadge from "../react/Badge";
|
||||||
import { useTriliumEvent } from "../react/hooks";
|
import { useTriliumEvent } from "../react/hooks";
|
||||||
|
|
||||||
export interface ChooseNoteTypeResponse {
|
export interface ChooseNoteTypeResponse {
|
||||||
@@ -108,7 +108,7 @@ export default function NoteTypeChooserDialogComponent() {
|
|||||||
value={[ item.type, item.templateNoteId ].join(",") }
|
value={[ item.type, item.templateNoteId ].join(",") }
|
||||||
icon={item.uiIcon}>
|
icon={item.uiIcon}>
|
||||||
{item.title}
|
{item.title}
|
||||||
{item.badges && item.badges.map((badge) => <Badge {...badge} />)}
|
{item.badges && item.badges.map((badge) => <SimpleBadge {...badge} />)}
|
||||||
</FormListItem>;
|
</FormListItem>;
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
|
|||||||
85
apps/client/src/widgets/layout/InlineTitle.css
Normal file
85
apps/client/src/widgets/layout/InlineTitle.css
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
:root {
|
||||||
|
--title-transition: opacity 200ms ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.component.inline-title {
|
||||||
|
contain: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-title {
|
||||||
|
padding-bottom: 2em;
|
||||||
|
padding-inline-start: 24px;
|
||||||
|
|
||||||
|
& > .inline-title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
transition: var(--title-transition);
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-icon-widget {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-row {
|
||||||
|
&.note-icon-widget,
|
||||||
|
&.note-title-widget {
|
||||||
|
transition: var(--title-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hide-title .note-icon-widget,
|
||||||
|
&.hide-title .note-title-widget {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-split.type-code:not(.mime-text-x-sqlite) .inline-title {
|
||||||
|
background-color: var(--main-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.prefers-centered-content .inline-title {
|
||||||
|
margin-inline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-details {
|
||||||
|
margin-top: 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25em;
|
||||||
|
margin: 0;
|
||||||
|
list-style-type: none;
|
||||||
|
color: var(--muted-text-color);
|
||||||
|
|
||||||
|
span.value {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-type-switcher {
|
||||||
|
padding: 1em 0;
|
||||||
|
display: flex;
|
||||||
|
overflow-x: auto;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 5px;
|
||||||
|
min-height: 60px;
|
||||||
|
--badge-radius: 12px;
|
||||||
|
|
||||||
|
>* {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ext-badge {
|
||||||
|
--color: var(--input-background-color);
|
||||||
|
color: var(--main-text-color);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
303
apps/client/src/widgets/layout/InlineTitle.tsx
Normal file
303
apps/client/src/widgets/layout/InlineTitle.tsx
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
import "./InlineTitle.css";
|
||||||
|
|
||||||
|
import { NoteType } from "@triliumnext/commons";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { ComponentChild } from "preact";
|
||||||
|
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||||
|
import { Trans } from "react-i18next";
|
||||||
|
|
||||||
|
import FNote from "../../entities/fnote";
|
||||||
|
import attributes from "../../services/attributes";
|
||||||
|
import froca from "../../services/froca";
|
||||||
|
import { t } from "../../services/i18n";
|
||||||
|
import { ViewScope } from "../../services/link";
|
||||||
|
import { NOTE_TYPES, NoteTypeMapping } from "../../services/note_types";
|
||||||
|
import server from "../../services/server";
|
||||||
|
import { formatDateTime } from "../../utils/formatters";
|
||||||
|
import NoteIcon from "../note_icon";
|
||||||
|
import NoteTitleWidget from "../note_title";
|
||||||
|
import { Badge, BadgeWithDropdown } from "../react/Badge";
|
||||||
|
import { FormDropdownDivider, FormListItem } from "../react/FormList";
|
||||||
|
import { useNoteBlob, useNoteContext, useNoteProperty, useStaticTooltip, useTriliumEvent } from "../react/hooks";
|
||||||
|
import { joinElements } from "../react/react_utils";
|
||||||
|
import { useNoteMetadata } from "../ribbon/NoteInfoTab";
|
||||||
|
import { onWheelHorizontalScroll } from "../widget_utils";
|
||||||
|
|
||||||
|
const supportedNoteTypes = new Set<NoteType>([
|
||||||
|
"text", "code"
|
||||||
|
]);
|
||||||
|
|
||||||
|
export default function InlineTitle() {
|
||||||
|
const { note, parentComponent, viewScope } = useNoteContext();
|
||||||
|
const type = useNoteProperty(note, "type");
|
||||||
|
const [ shown, setShown ] = useState(shouldShow(note?.noteId, type, viewScope));
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [ titleHidden, setTitleHidden ] = useState(false);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
setShown(shouldShow(note?.noteId, type, viewScope));
|
||||||
|
}, [ note, type, viewScope ]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!shown) return;
|
||||||
|
|
||||||
|
const titleRow = parentComponent.$widget[0].closest(".note-split")?.querySelector(":scope > .title-row");
|
||||||
|
if (!titleRow) return;
|
||||||
|
|
||||||
|
titleRow.classList.toggle("hide-title", true);
|
||||||
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
titleRow.classList.toggle("hide-title", entries[0].isIntersecting);
|
||||||
|
setTitleHidden(!entries[0].isIntersecting);
|
||||||
|
}, {
|
||||||
|
threshold: 0.85
|
||||||
|
});
|
||||||
|
if (containerRef.current) {
|
||||||
|
observer.observe(containerRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
titleRow.classList.remove("hide-title");
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}, [ shown, parentComponent ]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={clsx("inline-title", !shown && "hidden")}
|
||||||
|
>
|
||||||
|
<div class={clsx("inline-title-row", titleHidden && "hidden")}>
|
||||||
|
<NoteIcon />
|
||||||
|
<NoteTitleWidget />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<NoteTitleDetails />
|
||||||
|
<NoteTypeSwitcher />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldShow(noteId: string | undefined, type: NoteType | undefined, viewScope: ViewScope | undefined) {
|
||||||
|
if (viewScope?.viewMode !== "default") return false;
|
||||||
|
if (noteId?.startsWith("_options")) return true;
|
||||||
|
return type && supportedNoteTypes.has(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
//#region Title details
|
||||||
|
export function NoteTitleDetails() {
|
||||||
|
const { note } = useNoteContext();
|
||||||
|
const { metadata } = useNoteMetadata(note);
|
||||||
|
const isHiddenNote = note?.noteId.startsWith("_");
|
||||||
|
|
||||||
|
const items: ComponentChild[] = [
|
||||||
|
(!isHiddenNote && metadata?.dateCreated &&
|
||||||
|
<TextWithValue
|
||||||
|
i18nKey="note_title.created_on"
|
||||||
|
value={formatDateTime(metadata.dateCreated, "medium", "none")}
|
||||||
|
valueTooltip={formatDateTime(metadata.dateCreated, "full", "long")}
|
||||||
|
/>),
|
||||||
|
(!isHiddenNote && metadata?.dateModified &&
|
||||||
|
<TextWithValue
|
||||||
|
i18nKey="note_title.last_modified"
|
||||||
|
value={formatDateTime(metadata.dateModified, "medium", "none")}
|
||||||
|
valueTooltip={formatDateTime(metadata.dateModified, "full", "long")}
|
||||||
|
/>)
|
||||||
|
].filter(item => !!item);
|
||||||
|
|
||||||
|
return items.length > 0 && (
|
||||||
|
<div className="title-details">
|
||||||
|
{joinElements(items, " • ")}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TextWithValue({ i18nKey, value, valueTooltip }: {
|
||||||
|
i18nKey: string;
|
||||||
|
value: string;
|
||||||
|
valueTooltip: string;
|
||||||
|
}) {
|
||||||
|
const listItemRef = useRef<HTMLLIElement>(null);
|
||||||
|
useStaticTooltip(listItemRef, {
|
||||||
|
selector: "span.value",
|
||||||
|
title: valueTooltip,
|
||||||
|
popperConfig: { placement: "bottom" }
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li ref={listItemRef}>
|
||||||
|
<Trans
|
||||||
|
i18nKey={i18nKey}
|
||||||
|
components={{
|
||||||
|
Value: <span className="value">{value}</span> as React.ReactElement
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region Note type switcher
|
||||||
|
const SWITCHER_PINNED_NOTE_TYPES = new Set<NoteType>([ "text", "code", "book", "canvas" ]);
|
||||||
|
|
||||||
|
function NoteTypeSwitcher() {
|
||||||
|
const { note } = useNoteContext();
|
||||||
|
const blob = useNoteBlob(note);
|
||||||
|
const currentNoteType = useNoteProperty(note, "type");
|
||||||
|
const { pinnedNoteTypes, restNoteTypes } = useMemo(() => {
|
||||||
|
const pinnedNoteTypes: NoteTypeMapping[] = [];
|
||||||
|
const restNoteTypes: NoteTypeMapping[] = [];
|
||||||
|
for (const noteType of NOTE_TYPES) {
|
||||||
|
if (noteType.reserved || noteType.static || noteType.type === "book") continue;
|
||||||
|
if (SWITCHER_PINNED_NOTE_TYPES.has(noteType.type)) {
|
||||||
|
pinnedNoteTypes.push(noteType);
|
||||||
|
} else {
|
||||||
|
restNoteTypes.push(noteType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { pinnedNoteTypes, restNoteTypes };
|
||||||
|
}, []);
|
||||||
|
const currentNoteTypeData = useMemo(() => NOTE_TYPES.find(t => t.type === currentNoteType), [ currentNoteType ]);
|
||||||
|
const { builtinTemplates, collectionTemplates } = useBuiltinTemplates();
|
||||||
|
|
||||||
|
return (note?.type === "text" &&
|
||||||
|
<div
|
||||||
|
className="note-type-switcher"
|
||||||
|
onWheel={onWheelHorizontalScroll}
|
||||||
|
>
|
||||||
|
{blob?.contentLength === 0 && (
|
||||||
|
<>
|
||||||
|
<div className="intro">{t("note_title.note_type_switcher_label", { type: currentNoteTypeData?.title.toLocaleLowerCase() })}</div>
|
||||||
|
{pinnedNoteTypes.map(noteType => noteType.type !== currentNoteType && (
|
||||||
|
<Badge
|
||||||
|
key={noteType.type}
|
||||||
|
text={noteType.title}
|
||||||
|
icon={`bx ${noteType.icon}`}
|
||||||
|
onClick={() => switchNoteType(note.noteId, noteType)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{collectionTemplates.length > 0 && <CollectionNoteTypes noteId={note.noteId} collectionTemplates={collectionTemplates} />}
|
||||||
|
{builtinTemplates.length > 0 && <TemplateNoteTypes noteId={note.noteId} builtinTemplates={builtinTemplates} />}
|
||||||
|
{restNoteTypes.length > 0 && <MoreNoteTypes noteId={note.noteId} restNoteTypes={restNoteTypes} />}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MoreNoteTypes({ noteId, restNoteTypes }: { noteId: string, restNoteTypes: NoteTypeMapping[] }) {
|
||||||
|
return (
|
||||||
|
<BadgeWithDropdown
|
||||||
|
text={t("note_title.note_type_switcher_others")}
|
||||||
|
icon="bx bx-dots-vertical-rounded"
|
||||||
|
>
|
||||||
|
{restNoteTypes.map(noteType => (
|
||||||
|
<FormListItem
|
||||||
|
key={noteType.type}
|
||||||
|
icon={`bx ${noteType.icon}`}
|
||||||
|
onClick={() => switchNoteType(noteId, noteType)}
|
||||||
|
>{noteType.title}</FormListItem>
|
||||||
|
))}
|
||||||
|
</BadgeWithDropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CollectionNoteTypes({ noteId, collectionTemplates }: { noteId: string, collectionTemplates: FNote[] }) {
|
||||||
|
return (
|
||||||
|
<BadgeWithDropdown
|
||||||
|
text={t("note_title.note_type_switcher_collection")}
|
||||||
|
icon="bx bx-book"
|
||||||
|
>
|
||||||
|
{collectionTemplates.map(collectionTemplate => (
|
||||||
|
<FormListItem
|
||||||
|
key={collectionTemplate.noteId}
|
||||||
|
icon={collectionTemplate.getIcon()}
|
||||||
|
onClick={() => setTemplate(noteId, collectionTemplate.noteId)}
|
||||||
|
>{collectionTemplate.title}</FormListItem>
|
||||||
|
))}
|
||||||
|
</BadgeWithDropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TemplateNoteTypes({ noteId, builtinTemplates }: { noteId: string, builtinTemplates: FNote[] }) {
|
||||||
|
const [ userTemplates, setUserTemplates ] = useState<FNote[]>([]);
|
||||||
|
|
||||||
|
async function refreshTemplates() {
|
||||||
|
const templateNoteIds = await server.get<string[]>("search-templates");
|
||||||
|
const templateNotes = await froca.getNotes(templateNoteIds);
|
||||||
|
setUserTemplates(templateNotes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// First load.
|
||||||
|
useEffect(() => {
|
||||||
|
refreshTemplates();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// React to external changes.
|
||||||
|
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||||
|
if (loadResults.getAttributeRows().some(attr => attr.type === "label" && attr.name === "template")) {
|
||||||
|
refreshTemplates();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BadgeWithDropdown
|
||||||
|
text={t("note_title.note_type_switcher_templates")}
|
||||||
|
icon="bx bx-copy-alt"
|
||||||
|
>
|
||||||
|
{userTemplates.map(template => <TemplateItem key={template.noteId} noteId={noteId} template={template} />)}
|
||||||
|
{userTemplates.length > 0 && <FormDropdownDivider />}
|
||||||
|
{builtinTemplates.map(template => <TemplateItem key={template.noteId} noteId={noteId} template={template} />)}
|
||||||
|
</BadgeWithDropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TemplateItem({ noteId, template }: { noteId: string, template: FNote }) {
|
||||||
|
return (
|
||||||
|
<FormListItem
|
||||||
|
icon={template.getIcon()}
|
||||||
|
onClick={() => setTemplate(noteId, template.noteId)}
|
||||||
|
>{template.title}</FormListItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchNoteType(noteId: string, { type, mime }: NoteTypeMapping) {
|
||||||
|
return server.put(`notes/${noteId}/type`, { type, mime });
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTemplate(noteId: string, templateId: string) {
|
||||||
|
return attributes.setRelation(noteId, "template", templateId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useBuiltinTemplates() {
|
||||||
|
const [ templates, setTemplates ] = useState<{
|
||||||
|
builtinTemplates: FNote[];
|
||||||
|
collectionTemplates: FNote[];
|
||||||
|
}>({
|
||||||
|
builtinTemplates: [],
|
||||||
|
collectionTemplates: []
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadBuiltinTemplates() {
|
||||||
|
const templatesRoot = await froca.getNote("_templates");
|
||||||
|
if (!templatesRoot) return;
|
||||||
|
const childNotes = await templatesRoot.getChildNotes();
|
||||||
|
const builtinTemplates: FNote[] = [];
|
||||||
|
const collectionTemplates: FNote[] = [];
|
||||||
|
for (const childNote of childNotes) {
|
||||||
|
if (!childNote.hasLabel("template")) continue;
|
||||||
|
if (childNote.hasLabel("collection")) {
|
||||||
|
collectionTemplates.push(childNote);
|
||||||
|
} else {
|
||||||
|
builtinTemplates.push(childNote);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setTemplates({ builtinTemplates, collectionTemplates });
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadBuiltinTemplates();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return templates;
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
27
apps/client/src/widgets/layout/NoteBadges.css
Normal file
27
apps/client/src/widgets/layout/NoteBadges.css
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
.component.note-badges {
|
||||||
|
contain: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-badges {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
min-width: 0;
|
||||||
|
flex-shrink: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
--badge-radius: 12px;
|
||||||
|
|
||||||
|
.ext-badge {
|
||||||
|
&.temporarily-editable-badge { --color: #4fa52b; }
|
||||||
|
&.read-only-badge { --color: #e33f3b; }
|
||||||
|
&.share-badge { --color: #3b82f6; }
|
||||||
|
&.clipped-note-badge { --color: #57a2a5; }
|
||||||
|
&.execute-badge { --color: #f59e0b; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-badge {
|
||||||
|
&.dropdown-backlinks-badge .dropdown-menu {
|
||||||
|
min-width: 500px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
import "./BreadcrumbBadges.css";
|
import "./NoteBadges.css";
|
||||||
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { ComponentChildren, MouseEventHandler } from "preact";
|
import { ComponentChildren, MouseEventHandler } from "preact";
|
||||||
import { useRef } from "preact/hooks";
|
import { useRef } from "preact/hooks";
|
||||||
|
|
||||||
import { t } from "../services/i18n";
|
import { t } from "../../services/i18n";
|
||||||
import Dropdown, { DropdownProps } from "./react/Dropdown";
|
import Dropdown, { DropdownProps } from "../react/Dropdown";
|
||||||
import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean, useStaticTooltip } from "./react/hooks";
|
import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean, useStaticTooltip } from "../react/hooks";
|
||||||
import Icon from "./react/Icon";
|
import Icon from "../react/Icon";
|
||||||
import { useShareInfo } from "./shared_info";
|
import { useShareInfo } from "../shared_info";
|
||||||
|
import { Badge } from "../react/Badge";
|
||||||
|
|
||||||
export default function BreadcrumbBadges() {
|
export default function NoteBadges() {
|
||||||
return (
|
return (
|
||||||
<div className="breadcrumb-badges">
|
<div className="note-badges">
|
||||||
<ReadOnlyBadge />
|
<ReadOnlyBadge />
|
||||||
<ShareBadge />
|
<ShareBadge />
|
||||||
<ClippedNoteBadge />
|
<ClippedNoteBadge />
|
||||||
@@ -97,63 +98,3 @@ function ExecuteBadge() {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BadgeProps {
|
|
||||||
text?: string;
|
|
||||||
icon?: string;
|
|
||||||
className: string;
|
|
||||||
tooltip?: string;
|
|
||||||
onClick?: MouseEventHandler<HTMLDivElement>;
|
|
||||||
href?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Badge({ icon, className, text, tooltip, onClick, href }: BadgeProps) {
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
useStaticTooltip(containerRef, {
|
|
||||||
placement: "bottom",
|
|
||||||
fallbackPlacements: [ "bottom" ],
|
|
||||||
animation: false,
|
|
||||||
html: true,
|
|
||||||
title: tooltip
|
|
||||||
});
|
|
||||||
|
|
||||||
const content = <>
|
|
||||||
{icon && <><Icon icon={icon} /> </>}
|
|
||||||
<span class="text">{text}</span>
|
|
||||||
</>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={containerRef}
|
|
||||||
className={clsx("breadcrumb-badge", className, { "clickable": !!onClick })}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
{href ? <a href={href}>{content}</a> : <span>{content}</span>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function BadgeWithDropdown({ children, tooltip, className, dropdownOptions, ...props }: BadgeProps & {
|
|
||||||
children: ComponentChildren,
|
|
||||||
dropdownOptions?: Partial<DropdownProps>
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Dropdown
|
|
||||||
className={`breadcrumb-dropdown-badge dropdown-${className}`}
|
|
||||||
text={<Badge className={className} {...props} />}
|
|
||||||
noDropdownListStyle
|
|
||||||
noSelectButtonStyle
|
|
||||||
hideToggleArrow
|
|
||||||
title={tooltip}
|
|
||||||
titlePosition="bottom"
|
|
||||||
{...dropdownOptions}
|
|
||||||
dropdownOptions={{
|
|
||||||
...dropdownOptions?.dropdownOptions,
|
|
||||||
popperConfig: {
|
|
||||||
...dropdownOptions?.dropdownOptions?.popperConfig,
|
|
||||||
placement: "bottom", strategy: "fixed"
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>{children}</Dropdown>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
28
apps/client/src/widgets/layout/NoteTitleActions.css
Normal file
28
apps/client/src/widgets/layout/NoteTitleActions.css
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
body.experimental-feature-new-layout {
|
||||||
|
.component.title-actions {
|
||||||
|
contain: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-actions {
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25em;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
max-width: unset;
|
||||||
|
padding-inline-start: 15px;
|
||||||
|
padding-bottom: 0.2em;
|
||||||
|
font-size: 0.8em;
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
input.form-control {
|
||||||
|
padding: 2px 8px;
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
import CollectionProperties from "./note_bars/CollectionProperties";
|
import CollectionProperties from "../note_bars/CollectionProperties";
|
||||||
import { useNoteContext, useNoteProperty } from "./react/hooks";
|
import { useNoteContext, useNoteProperty } from "../react/hooks";
|
||||||
|
import "./NoteTitleActions.css";
|
||||||
|
|
||||||
export default function NoteTitleDetails() {
|
export default function NoteTitleActions() {
|
||||||
const { note } = useNoteContext();
|
const { note } = useNoteContext();
|
||||||
const isHiddenNote = note && note.noteId !== "_search" && note.noteId.startsWith("_");
|
const isHiddenNote = note && note.noteId !== "_search" && note.noteId.startsWith("_");
|
||||||
const noteType = useNoteProperty(note, "type");
|
const noteType = useNoteProperty(note, "type");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="title-details">
|
<div className="title-actions">
|
||||||
{note && !isHiddenNote && noteType === "book" && <CollectionProperties note={note} />}
|
{note && !isHiddenNote && noteType === "book" && <CollectionProperties note={note} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -12,6 +12,7 @@ import FNote from "../../entities/fnote";
|
|||||||
import attributes from "../../services/attributes";
|
import attributes from "../../services/attributes";
|
||||||
import { t } from "../../services/i18n";
|
import { t } from "../../services/i18n";
|
||||||
import { ViewScope } from "../../services/link";
|
import { ViewScope } from "../../services/link";
|
||||||
|
import server from "../../services/server";
|
||||||
import { openInAppHelpFromUrl } from "../../services/utils";
|
import { openInAppHelpFromUrl } from "../../services/utils";
|
||||||
import { formatDateTime } from "../../utils/formatters";
|
import { formatDateTime } from "../../utils/formatters";
|
||||||
import { BacklinksList, useBacklinkCount } from "../FloatingButtonsDefinitions";
|
import { BacklinksList, useBacklinkCount } from "../FloatingButtonsDefinitions";
|
||||||
@@ -28,7 +29,6 @@ import { NotePathsWidget, useSortedNotePaths } from "../ribbon/NotePathsTab";
|
|||||||
import { useAttachments } from "../type_widgets/Attachment";
|
import { useAttachments } from "../type_widgets/Attachment";
|
||||||
import { useProcessedLocales } from "../type_widgets/options/components/LocaleSelector";
|
import { useProcessedLocales } from "../type_widgets/options/components/LocaleSelector";
|
||||||
import Breadcrumb from "./Breadcrumb";
|
import Breadcrumb from "./Breadcrumb";
|
||||||
import server from "../../services/server";
|
|
||||||
|
|
||||||
interface StatusBarContext {
|
interface StatusBarContext {
|
||||||
note: FNote;
|
note: FNote;
|
||||||
@@ -84,7 +84,6 @@ function StatusBarDropdown({ children, icon, text, buttonClassName, titleOptions
|
|||||||
...titleOptions
|
...titleOptions
|
||||||
}}
|
}}
|
||||||
dropdownOptions={{
|
dropdownOptions={{
|
||||||
autoClose: "outside",
|
|
||||||
popperConfig: {
|
popperConfig: {
|
||||||
strategy: "fixed",
|
strategy: "fixed",
|
||||||
placement: "top"
|
placement: "top"
|
||||||
@@ -204,6 +203,7 @@ export function NoteInfoBadge({ note }: { note: FNote | null | undefined }) {
|
|||||||
icon="bx bx-info-circle"
|
icon="bx bx-info-circle"
|
||||||
title={t("status_bar.note_info_title")}
|
title={t("status_bar.note_info_title")}
|
||||||
dropdownContainerClassName="dropdown-note-info"
|
dropdownContainerClassName="dropdown-note-info"
|
||||||
|
dropdownOptions={{ autoClose: "outside" }}
|
||||||
>
|
>
|
||||||
<ul>
|
<ul>
|
||||||
<NoteInfoValue text={t("note_info_widget.created")} value={formatDateTime(metadata?.dateCreated)} />
|
<NoteInfoValue text={t("note_info_widget.created")} value={formatDateTime(metadata?.dateCreated)} />
|
||||||
@@ -356,14 +356,13 @@ function CodeNoteSwitcher({ note }: StatusBarContext) {
|
|||||||
mimeTypes.find(m => m.mime === currentNoteMime)
|
mimeTypes.find(m => m.mime === currentNoteMime)
|
||||||
), [ mimeTypes, currentNoteMime ]);
|
), [ mimeTypes, currentNoteMime ]);
|
||||||
|
|
||||||
return (
|
return (note.type === "code" &&
|
||||||
<>
|
<>
|
||||||
<StatusBarDropdown
|
<StatusBarDropdown
|
||||||
icon="bx bx-code-curly"
|
icon="bx bx-code-curly"
|
||||||
text={correspondingMimeType?.title}
|
text={correspondingMimeType?.title}
|
||||||
title={t("status_bar.code_note_switcher")}
|
title={t("status_bar.code_note_switcher")}
|
||||||
dropdownContainerClassName="dropdown-code-note-switcher"
|
dropdownContainerClassName="dropdown-code-note-switcher"
|
||||||
dropdownOptions={{ autoClose: true }}
|
|
||||||
>
|
>
|
||||||
<NoteTypeCodeNoteList
|
<NoteTypeCodeNoteList
|
||||||
currentMimeType={currentNoteMime}
|
currentMimeType={currentNoteMime}
|
||||||
|
|||||||
@@ -30,88 +30,26 @@ body.desktop .note-title-widget input.note-title {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body.experimental-feature-new-layout {
|
body.experimental-feature-new-layout {
|
||||||
.title-details {
|
.title-row {
|
||||||
max-width: var(--max-content-width);
|
container-type: size;
|
||||||
padding: 0;
|
|
||||||
padding-inline-start: 24px;
|
@container (max-width: 700px) {
|
||||||
|
.note-icon-widget .note-icon {
|
||||||
|
font-size: 1.3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-details {
|
.note-title-widget {
|
||||||
padding-inline-end: 16px;
|
|
||||||
|
|
||||||
.dropdown-menu {
|
|
||||||
input.form-control {
|
|
||||||
padding: 2px 8px;
|
|
||||||
margin-left: 1em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.spacer {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-split.type-code:not(.mime-text-x-sqlite) .title-details {
|
|
||||||
background-color: var(--main-background-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-details {
|
|
||||||
margin-top: 0;
|
|
||||||
contain: none;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.25em;
|
align-items: center;
|
||||||
margin: 0;
|
.note-title {
|
||||||
list-style-type: none;
|
font-size: 1em;
|
||||||
|
|
||||||
span.value {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrolling-container:has(> :is(.note-detail.full-height, .note-list-widget.full-height)),
|
.note-title-widget:focus-within + .note-badges,
|
||||||
.note-split.type-book {
|
.ext-badge .text {
|
||||||
.title-details {
|
display: none;
|
||||||
width: 100%;
|
|
||||||
max-width: unset;
|
|
||||||
padding-inline-start: 15px;
|
|
||||||
padding-bottom: 0.2em;
|
|
||||||
font-size: 0.8em;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.prefers-centered-content .title-details {
|
|
||||||
margin-inline: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body.experimental-feature-floating-titlebar {
|
|
||||||
.title-row {
|
|
||||||
max-width: var(--max-content-width);
|
|
||||||
padding: 0;
|
|
||||||
padding-inline-start: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-icon-widget {
|
|
||||||
padding: 0;
|
|
||||||
width: 41px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-split.type-code:not(.mime-text-x-sqlite) .title-row {
|
|
||||||
background-color: var(--main-background-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrolling-container:has(> :is(.note-detail.full-height, .note-list-widget.full-height)),
|
|
||||||
.note-split.type-book {
|
|
||||||
.title-row {
|
|
||||||
width: 100%;
|
|
||||||
max-width: unset;
|
|
||||||
padding-inline-start: 15px;
|
|
||||||
padding-bottom: 0.2em;
|
|
||||||
font-size: 0.8em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.prefers-centered-content .title-row {
|
|
||||||
margin-inline: auto;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,4 @@
|
|||||||
.component.breadcrumb-badges {
|
.ext-badge {
|
||||||
contain: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumb-badges {
|
|
||||||
display: flex;
|
|
||||||
gap: 5px;
|
|
||||||
min-width: 0;
|
|
||||||
flex-shrink: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
--badge-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumb-badge {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
@@ -30,12 +17,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.temporarily-editable-badge { --color: #4fa52b; }
|
|
||||||
&.read-only-badge { --color: #e33f3b; }
|
|
||||||
&.share-badge { --color: #3b82f6; }
|
|
||||||
&.clipped-note-badge { --color: #57a2a5; }
|
|
||||||
&.execute-badge { --color: #f59e0b; }
|
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: inherit !important;
|
color: inherit !important;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -49,18 +30,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumb-dropdown-badge {
|
.dropdown-badge {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
border-radius: var(--badge-radius);
|
border-radius: var(--badge-radius);
|
||||||
|
|
||||||
&.dropdown-backlinks-badge .dropdown-menu {
|
.ext-badge {
|
||||||
min-width: 500px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumb-badge {
|
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,8 +1,78 @@
|
|||||||
interface BadgeProps {
|
import "./Badge.css";
|
||||||
|
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { ComponentChildren, MouseEventHandler } from "preact";
|
||||||
|
import { useRef } from "preact/hooks";
|
||||||
|
|
||||||
|
import Dropdown, { DropdownProps } from "./Dropdown";
|
||||||
|
import { useStaticTooltip } from "./hooks";
|
||||||
|
import Icon from "./Icon";
|
||||||
|
|
||||||
|
interface SimpleBadgeProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
title: string;
|
title: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Badge({ title, className }: BadgeProps) {
|
interface BadgeProps {
|
||||||
return <span class={`badge ${className ?? ""}`}>{title}</span>
|
text?: string;
|
||||||
|
icon?: string;
|
||||||
|
className?: string;
|
||||||
|
tooltip?: string;
|
||||||
|
onClick?: MouseEventHandler<HTMLDivElement>;
|
||||||
|
href?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SimpleBadge({ title, className }: SimpleBadgeProps) {
|
||||||
|
return <span class={`badge ${className ?? ""}`}>{title}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Badge({ icon, className, text, tooltip, onClick, href }: BadgeProps) {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
useStaticTooltip(containerRef, {
|
||||||
|
placement: "bottom",
|
||||||
|
fallbackPlacements: [ "bottom" ],
|
||||||
|
animation: false,
|
||||||
|
html: true,
|
||||||
|
title: tooltip
|
||||||
|
});
|
||||||
|
|
||||||
|
const content = <>
|
||||||
|
{icon && <><Icon icon={icon} /> </>}
|
||||||
|
<span class="text">{text}</span>
|
||||||
|
</>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={clsx("ext-badge", className, { "clickable": !!onClick })}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{href ? <a href={href}>{content}</a> : <span>{content}</span>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BadgeWithDropdown({ children, tooltip, className, dropdownOptions, ...props }: BadgeProps & {
|
||||||
|
children: ComponentChildren,
|
||||||
|
dropdownOptions?: Partial<DropdownProps>
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
className={`dropdown-badge dropdown-${className}`}
|
||||||
|
text={<Badge className={className} {...props} />}
|
||||||
|
noDropdownListStyle
|
||||||
|
noSelectButtonStyle
|
||||||
|
hideToggleArrow
|
||||||
|
title={tooltip}
|
||||||
|
titlePosition="bottom"
|
||||||
|
{...dropdownOptions}
|
||||||
|
dropdownOptions={{
|
||||||
|
...dropdownOptions?.dropdownOptions,
|
||||||
|
popperConfig: {
|
||||||
|
...dropdownOptions?.dropdownOptions?.popperConfig,
|
||||||
|
placement: "bottom", strategy: "fixed"
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>{children}</Dropdown>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -429,7 +429,7 @@ body.experimental-feature-new-layout {
|
|||||||
.ribbon-container {
|
.ribbon-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
border-top: 1px solid var(--main-border-color);
|
border: 0;
|
||||||
|
|
||||||
.ribbon-tab-spacer,
|
.ribbon-tab-spacer,
|
||||||
.ribbon-tab-title,
|
.ribbon-tab-title,
|
||||||
|
|||||||
Reference in New Issue
Block a user