New layout: Title bar & inline title (#8044)

This commit is contained in:
Elian Doran
2025-12-13 15:09:30 +02:00
committed by GitHub
17 changed files with 583 additions and 218 deletions

View File

@@ -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" }
]

View File

@@ -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 />)

View File

@@ -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[];

View File

@@ -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.",

View File

@@ -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",

View File

@@ -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>;
}
})}

View 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;
}
}

View 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

View 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;
}
}
}

View File

@@ -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} />&nbsp;</>}
<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>
);
}

View 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;
}
}
}

View File

@@ -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>
);

View File

@@ -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}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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} />&nbsp;</>}
<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>
);
}

View File

@@ -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,