mirror of
https://github.com/zadam/trilium.git
synced 2025-12-16 13:19:54 +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/edit-docs/demo/**": true
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"eslint.rules.customizations": [
|
||||
{ "rule": "*", "severity": "warn" }
|
||||
]
|
||||
|
||||
@@ -49,9 +49,10 @@ import { isExperimentalFeatureEnabled } from "../services/experimental_features.
|
||||
import NoteActions from "../widgets/ribbon/NoteActions.jsx";
|
||||
import FormattingToolbar from "../widgets/ribbon/FormattingToolbar.jsx";
|
||||
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
|
||||
import BreadcrumbBadges from "../widgets/BreadcrumbBadges.jsx";
|
||||
import NoteTitleDetails from "../widgets/NoteTitleDetails.jsx";
|
||||
import NoteBadges from "../widgets/layout/NoteBadges.jsx";
|
||||
import NoteTitleActions from "../widgets/layout/NoteTitleActions.jsx";
|
||||
import StatusBar from "../widgets/layout/StatusBar.jsx";
|
||||
import InlineTitle from "../widgets/layout/InlineTitle.jsx";
|
||||
|
||||
export default class DesktopLayout {
|
||||
|
||||
@@ -78,12 +79,19 @@ export default class DesktopLayout {
|
||||
const fullWidthTabBar = launcherPaneIsHorizontal || (isElectron && !hasNativeTitleBar && isMac);
|
||||
const customTitleBarButtons = !hasNativeTitleBar && !isMac && !isWindows;
|
||||
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
||||
const isFloatingTitlebar = isExperimentalFeatureEnabled("floating-titlebar");
|
||||
|
||||
const titleRow = new FlexContainer("row")
|
||||
.class("title-row")
|
||||
.cssBlock(".title-row > * { margin: 5px; }")
|
||||
.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)
|
||||
.setParent(appContext)
|
||||
@@ -137,19 +145,7 @@ export default class DesktopLayout {
|
||||
.child(
|
||||
new SplitNoteContainer(() =>
|
||||
new NoteWrapperWidget()
|
||||
.child(
|
||||
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)
|
||||
.child(titleRow)
|
||||
.optChild(!isNewLayout, <Ribbon><NoteActions /></Ribbon>)
|
||||
.optChild(isNewLayout, <Ribbon />)
|
||||
.child(new WatchedFileUpdateStatusWidget())
|
||||
@@ -157,8 +153,8 @@ export default class DesktopLayout {
|
||||
.child(
|
||||
new ScrollingContainer()
|
||||
.filling()
|
||||
.optChild(isFloatingTitlebar, titleRow)
|
||||
.optChild(isNewLayout, <NoteTitleDetails />)
|
||||
.optChild(isNewLayout, <InlineTitle />)
|
||||
.optChild(isNewLayout, <NoteTitleActions />)
|
||||
.optChild(!isNewLayout, new ContentHeader()
|
||||
.child(<ReadOnlyNoteInfoBar />)
|
||||
.child(<SharedInfo />)
|
||||
|
||||
@@ -12,11 +12,6 @@ export const experimentalFeatures = [
|
||||
id: "new-layout",
|
||||
name: t("experimental_features.new_layout_name"),
|
||||
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[];
|
||||
|
||||
|
||||
@@ -1102,9 +1102,7 @@
|
||||
"title": "Experimental Options",
|
||||
"disclaimer": "These options are experimental and may cause instability. Use with caution.",
|
||||
"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.",
|
||||
"floating_titlebar": "Floating Titlebar",
|
||||
"floating_titlebar_description": "The title bar is part of the content and is scrolled along with the note content."
|
||||
"new_layout_description": "Try out the new layout for a more modern look and improved usability. Subject to heavy change in the upcoming releases."
|
||||
},
|
||||
"fonts": {
|
||||
"theme_defined": "Theme defined",
|
||||
@@ -1755,7 +1753,11 @@
|
||||
"note_title": {
|
||||
"placeholder": "type note's title here...",
|
||||
"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": {
|
||||
"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 FormattingToolbar from "../ribbon/FormattingToolbar";
|
||||
import MobileEditorToolbar from "../type_widgets/text/mobile_editor_toolbar";
|
||||
import BreadcrumbBadges from "../BreadcrumbBadges";
|
||||
import NoteBadges from "../layout/NoteBadges";
|
||||
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
|
||||
|
||||
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
||||
@@ -69,7 +69,7 @@ export default function PopupEditor() {
|
||||
<Modal
|
||||
title={<>
|
||||
<TitleRow />
|
||||
{isNewLayout && <BreadcrumbBadges />}
|
||||
{isNewLayout && <NoteBadges />}
|
||||
</>}
|
||||
customTitleBarButtons={[{
|
||||
iconClassName: "bx-expand-alt",
|
||||
|
||||
@@ -8,7 +8,7 @@ import note_types from "../../services/note_types";
|
||||
import { MenuCommandItem, MenuItem } from "../../menus/context_menu";
|
||||
import { TreeCommandNames } from "../../menus/tree_context_menu";
|
||||
import { Suggestion } from "../../services/note_autocomplete";
|
||||
import Badge from "../react/Badge";
|
||||
import SimpleBadge from "../react/Badge";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
|
||||
export interface ChooseNoteTypeResponse {
|
||||
@@ -108,7 +108,7 @@ export default function NoteTypeChooserDialogComponent() {
|
||||
value={[ item.type, item.templateNoteId ].join(",") }
|
||||
icon={item.uiIcon}>
|
||||
{item.title}
|
||||
{item.badges && item.badges.map((badge) => <Badge {...badge} />)}
|
||||
{item.badges && item.badges.map((badge) => <SimpleBadge {...badge} />)}
|
||||
</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 { ComponentChildren, MouseEventHandler } from "preact";
|
||||
import { useRef } from "preact/hooks";
|
||||
|
||||
import { t } from "../services/i18n";
|
||||
import Dropdown, { DropdownProps } from "./react/Dropdown";
|
||||
import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean, useStaticTooltip } from "./react/hooks";
|
||||
import Icon from "./react/Icon";
|
||||
import { useShareInfo } from "./shared_info";
|
||||
import { t } from "../../services/i18n";
|
||||
import Dropdown, { DropdownProps } from "../react/Dropdown";
|
||||
import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean, useStaticTooltip } from "../react/hooks";
|
||||
import Icon from "../react/Icon";
|
||||
import { useShareInfo } from "../shared_info";
|
||||
import { Badge } from "../react/Badge";
|
||||
|
||||
export default function BreadcrumbBadges() {
|
||||
export default function NoteBadges() {
|
||||
return (
|
||||
<div className="breadcrumb-badges">
|
||||
<div className="note-badges">
|
||||
<ReadOnlyBadge />
|
||||
<ShareBadge />
|
||||
<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 { useNoteContext, useNoteProperty } from "./react/hooks";
|
||||
import CollectionProperties from "../note_bars/CollectionProperties";
|
||||
import { useNoteContext, useNoteProperty } from "../react/hooks";
|
||||
import "./NoteTitleActions.css";
|
||||
|
||||
export default function NoteTitleDetails() {
|
||||
export default function NoteTitleActions() {
|
||||
const { note } = useNoteContext();
|
||||
const isHiddenNote = note && note.noteId !== "_search" && note.noteId.startsWith("_");
|
||||
const noteType = useNoteProperty(note, "type");
|
||||
|
||||
return (
|
||||
<div className="title-details">
|
||||
<div className="title-actions">
|
||||
{note && !isHiddenNote && noteType === "book" && <CollectionProperties note={note} />}
|
||||
</div>
|
||||
);
|
||||
@@ -12,6 +12,7 @@ import FNote from "../../entities/fnote";
|
||||
import attributes from "../../services/attributes";
|
||||
import { t } from "../../services/i18n";
|
||||
import { ViewScope } from "../../services/link";
|
||||
import server from "../../services/server";
|
||||
import { openInAppHelpFromUrl } from "../../services/utils";
|
||||
import { formatDateTime } from "../../utils/formatters";
|
||||
import { BacklinksList, useBacklinkCount } from "../FloatingButtonsDefinitions";
|
||||
@@ -28,7 +29,6 @@ import { NotePathsWidget, useSortedNotePaths } from "../ribbon/NotePathsTab";
|
||||
import { useAttachments } from "../type_widgets/Attachment";
|
||||
import { useProcessedLocales } from "../type_widgets/options/components/LocaleSelector";
|
||||
import Breadcrumb from "./Breadcrumb";
|
||||
import server from "../../services/server";
|
||||
|
||||
interface StatusBarContext {
|
||||
note: FNote;
|
||||
@@ -84,7 +84,6 @@ function StatusBarDropdown({ children, icon, text, buttonClassName, titleOptions
|
||||
...titleOptions
|
||||
}}
|
||||
dropdownOptions={{
|
||||
autoClose: "outside",
|
||||
popperConfig: {
|
||||
strategy: "fixed",
|
||||
placement: "top"
|
||||
@@ -204,6 +203,7 @@ export function NoteInfoBadge({ note }: { note: FNote | null | undefined }) {
|
||||
icon="bx bx-info-circle"
|
||||
title={t("status_bar.note_info_title")}
|
||||
dropdownContainerClassName="dropdown-note-info"
|
||||
dropdownOptions={{ autoClose: "outside" }}
|
||||
>
|
||||
<ul>
|
||||
<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, currentNoteMime ]);
|
||||
|
||||
return (
|
||||
return (note.type === "code" &&
|
||||
<>
|
||||
<StatusBarDropdown
|
||||
icon="bx bx-code-curly"
|
||||
text={correspondingMimeType?.title}
|
||||
title={t("status_bar.code_note_switcher")}
|
||||
dropdownContainerClassName="dropdown-code-note-switcher"
|
||||
dropdownOptions={{ autoClose: true }}
|
||||
>
|
||||
<NoteTypeCodeNoteList
|
||||
currentMimeType={currentNoteMime}
|
||||
|
||||
@@ -30,88 +30,26 @@ body.desktop .note-title-widget input.note-title {
|
||||
}
|
||||
|
||||
body.experimental-feature-new-layout {
|
||||
.title-details {
|
||||
max-width: var(--max-content-width);
|
||||
padding: 0;
|
||||
padding-inline-start: 24px;
|
||||
}
|
||||
.title-row {
|
||||
container-type: size;
|
||||
|
||||
.title-details {
|
||||
padding-inline-end: 16px;
|
||||
@container (max-width: 700px) {
|
||||
.note-icon-widget .note-icon {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
input.form-control {
|
||||
padding: 2px 8px;
|
||||
margin-left: 1em;
|
||||
.note-title-widget {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.note-title {
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.note-title-widget:focus-within + .note-badges,
|
||||
.ext-badge .text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
gap: 0.25em;
|
||||
margin: 0;
|
||||
list-style-type: none;
|
||||
|
||||
span.value {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.scrolling-container:has(> :is(.note-detail.full-height, .note-list-widget.full-height)),
|
||||
.note-split.type-book {
|
||||
.title-details {
|
||||
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 {
|
||||
contain: none;
|
||||
}
|
||||
|
||||
.breadcrumb-badges {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
min-width: 0;
|
||||
flex-shrink: 1;
|
||||
overflow: hidden;
|
||||
--badge-radius: 12px;
|
||||
}
|
||||
|
||||
.breadcrumb-badge {
|
||||
.ext-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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 {
|
||||
color: inherit !important;
|
||||
text-decoration: none;
|
||||
@@ -49,18 +30,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb-dropdown-badge {
|
||||
.dropdown-badge {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
border-radius: var(--badge-radius);
|
||||
|
||||
&.dropdown-backlinks-badge .dropdown-menu {
|
||||
min-width: 500px;
|
||||
}
|
||||
|
||||
.breadcrumb-badge {
|
||||
.ext-badge {
|
||||
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;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export default function Badge({ title, className }: BadgeProps) {
|
||||
return <span class={`badge ${className ?? ""}`}>{title}</span>
|
||||
interface BadgeProps {
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
border-top: 1px solid var(--main-border-color);
|
||||
border: 0;
|
||||
|
||||
.ribbon-tab-spacer,
|
||||
.ribbon-tab-title,
|
||||
|
||||
Reference in New Issue
Block a user