feat(layout/inline-title): basic note type switcher

This commit is contained in:
Elian Doran
2025-12-13 12:25:01 +02:00
parent 4473f80d73
commit 50a847777e
7 changed files with 178 additions and 122 deletions

View File

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

View File

@@ -40,3 +40,19 @@ body.prefers-centered-content .inline-title {
font-weight: 500; font-weight: 500;
} }
} }
.note-type-switcher {
padding: 1em 0;
display: flex;
overflow-x: auto;
min-width: 0;
gap: 5px;
--badge-radius: 12px;
.ext-badge {
--color: var(--input-background-color);
color: var(--main-text-color);
flex-shrink: 0;
font-size: 0.9rem;
}
}

View File

@@ -3,7 +3,7 @@ import "./InlineTitle.css";
import { NoteType } from "@triliumnext/commons"; import { NoteType } from "@triliumnext/commons";
import clsx from "clsx"; import clsx from "clsx";
import { ComponentChild } from "preact"; import { ComponentChild } from "preact";
import { useEffect, useRef, useState } from "preact/hooks"; import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import { Trans } from "react-i18next"; import { Trans } from "react-i18next";
import FNote from "../../entities/fnote"; import FNote from "../../entities/fnote";
@@ -14,6 +14,9 @@ import NoteTitleWidget from "../note_title";
import { useNoteContext, useStaticTooltip } from "../react/hooks"; import { useNoteContext, useStaticTooltip } from "../react/hooks";
import { joinElements } from "../react/react_utils"; import { joinElements } from "../react/react_utils";
import { useNoteMetadata } from "../ribbon/NoteInfoTab"; import { useNoteMetadata } from "../ribbon/NoteInfoTab";
import { NOTE_TYPES } from "../../services/note_types";
import { Badge } from "../react/Badge";
import server from "../../services/server";
const supportedNoteTypes = new Set<NoteType>([ const supportedNoteTypes = new Set<NoteType>([
"text", "code" "text", "code"
@@ -60,6 +63,7 @@ export default function InlineTitle() {
</div> </div>
<NoteTitleDetails /> <NoteTitleDetails />
<NoteTypeSwitcher />
</div> </div>
); );
} }
@@ -71,6 +75,7 @@ function shouldShow(note: FNote | null | undefined, viewScope: ViewScope | undef
return supportedNoteTypes.has(note.type); return supportedNoteTypes.has(note.type);
} }
//#region Title details
export function NoteTitleDetails() { export function NoteTitleDetails() {
const { note } = useNoteContext(); const { note } = useNoteContext();
const { metadata } = useNoteMetadata(note); const { metadata } = useNoteMetadata(note);
@@ -121,3 +126,23 @@ function TextWithValue({ i18nKey, value, valueTooltip }: {
</li> </li>
); );
} }
//#endregion
//#region Note type switcher
function NoteTypeSwitcher() {
const { note } = useNoteContext();
const noteTypes = useMemo(() => NOTE_TYPES.filter((nt) => !nt.reserved && !nt.static), []);
return (note &&
<div className="note-type-switcher">
{noteTypes.map(noteType => (
<Badge
key={noteType.type}
text={noteType.title}
onClick={() => server.put(`notes/${note.noteId}/type`, { type: noteType.type, mime: noteType.mime })}
/>
))}
</div>
);
}
//#endregion

View File

@@ -9,64 +9,19 @@
flex-shrink: 1; flex-shrink: 1;
overflow: hidden; overflow: hidden;
--badge-radius: 12px; --badge-radius: 12px;
}
.breadcrumb-badge { .ext-badge {
display: flex; &.temporarily-editable-badge { --color: #4fa52b; }
align-items: center; &.read-only-badge { --color: #e33f3b; }
padding: 2px 6px; &.share-badge { --color: #3b82f6; }
border-radius: var(--badge-radius); &.clipped-note-badge { --color: #57a2a5; }
font-size: 0.75em; &.execute-badge { --color: #f59e0b; }
background-color: var(--color, transparent); }
color: white;
min-width: 0;
flex-shrink: 1;
&.clickable { .dropdown-badge {
cursor: pointer; &.dropdown-backlinks-badge .dropdown-menu {
min-width: 500px;
&:hover {
background-color: color-mix(in srgb, var(--color, --badge-background-color) 80%, black);
} }
} }
&.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;
}
> * {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
} }
.breadcrumb-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 {
border-radius: 0;
}
.btn {
border: 0;
margin: 0;
padding: 0;
}
}

View File

@@ -9,6 +9,7 @@ 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 NoteBadges() { export default function NoteBadges() {
return ( return (
@@ -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,49 @@
.ext-badge {
display: flex;
align-items: center;
padding: 2px 6px;
border-radius: var(--badge-radius);
font-size: 0.75em;
background-color: var(--color, transparent);
color: white;
min-width: 0;
flex-shrink: 1;
&.clickable {
cursor: pointer;
&:hover {
background-color: color-mix(in srgb, var(--color, --badge-background-color) 80%, black);
}
}
a {
color: inherit !important;
text-decoration: none;
}
> * {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.dropdown-badge {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
border-radius: var(--badge-radius);
.ext-badge {
border-radius: 0;
}
.btn {
border: 0;
margin: 0;
padding: 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; 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} />&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>
);
}