From 50a847777e38b456cd60bc16e9a6695474bd7801 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 12:25:01 +0200 Subject: [PATCH] feat(layout/inline-title): basic note type switcher --- .../src/widgets/dialogs/note_type_chooser.tsx | 4 +- .../client/src/widgets/layout/InlineTitle.css | 16 ++++ .../client/src/widgets/layout/InlineTitle.tsx | 27 ++++++- apps/client/src/widgets/layout/NoteBadges.css | 65 +++------------- apps/client/src/widgets/layout/NoteBadges.tsx | 61 +-------------- apps/client/src/widgets/react/Badge.css | 49 ++++++++++++ apps/client/src/widgets/react/Badge.tsx | 78 ++++++++++++++++++- 7 files changed, 178 insertions(+), 122 deletions(-) create mode 100644 apps/client/src/widgets/react/Badge.css diff --git a/apps/client/src/widgets/dialogs/note_type_chooser.tsx b/apps/client/src/widgets/dialogs/note_type_chooser.tsx index 7db061c1a..efb44f48c 100644 --- a/apps/client/src/widgets/dialogs/note_type_chooser.tsx +++ b/apps/client/src/widgets/dialogs/note_type_chooser.tsx @@ -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) => )} + {item.badges && item.badges.map((badge) => )} ; } })} diff --git a/apps/client/src/widgets/layout/InlineTitle.css b/apps/client/src/widgets/layout/InlineTitle.css index b18f1066b..237398449 100644 --- a/apps/client/src/widgets/layout/InlineTitle.css +++ b/apps/client/src/widgets/layout/InlineTitle.css @@ -40,3 +40,19 @@ body.prefers-centered-content .inline-title { 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; + } +} diff --git a/apps/client/src/widgets/layout/InlineTitle.tsx b/apps/client/src/widgets/layout/InlineTitle.tsx index e9eb66a39..06959a6e3 100644 --- a/apps/client/src/widgets/layout/InlineTitle.tsx +++ b/apps/client/src/widgets/layout/InlineTitle.tsx @@ -3,7 +3,7 @@ import "./InlineTitle.css"; import { NoteType } from "@triliumnext/commons"; import clsx from "clsx"; 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 FNote from "../../entities/fnote"; @@ -14,6 +14,9 @@ import NoteTitleWidget from "../note_title"; import { useNoteContext, useStaticTooltip } from "../react/hooks"; import { joinElements } from "../react/react_utils"; 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([ "text", "code" @@ -60,6 +63,7 @@ export default function InlineTitle() { + ); } @@ -71,6 +75,7 @@ function shouldShow(note: FNote | null | undefined, viewScope: ViewScope | undef return supportedNoteTypes.has(note.type); } +//#region Title details export function NoteTitleDetails() { const { note } = useNoteContext(); const { metadata } = useNoteMetadata(note); @@ -121,3 +126,23 @@ function TextWithValue({ i18nKey, value, valueTooltip }: { ); } +//#endregion + +//#region Note type switcher +function NoteTypeSwitcher() { + const { note } = useNoteContext(); + const noteTypes = useMemo(() => NOTE_TYPES.filter((nt) => !nt.reserved && !nt.static), []); + + return (note && +
+ {noteTypes.map(noteType => ( + server.put(`notes/${note.noteId}/type`, { type: noteType.type, mime: noteType.mime })} + /> + ))} +
+ ); +} +//#endregion diff --git a/apps/client/src/widgets/layout/NoteBadges.css b/apps/client/src/widgets/layout/NoteBadges.css index fca26305b..fa3bc504f 100644 --- a/apps/client/src/widgets/layout/NoteBadges.css +++ b/apps/client/src/widgets/layout/NoteBadges.css @@ -9,64 +9,19 @@ flex-shrink: 1; overflow: hidden; --badge-radius: 12px; -} -.breadcrumb-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; + .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; } + } - &.clickable { - cursor: pointer; - - &:hover { - background-color: color-mix(in srgb, var(--color, --badge-background-color) 80%, black); + .dropdown-badge { + &.dropdown-backlinks-badge .dropdown-menu { + min-width: 500px; } } - - &.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; - } -} diff --git a/apps/client/src/widgets/layout/NoteBadges.tsx b/apps/client/src/widgets/layout/NoteBadges.tsx index 51faf7183..0959b773c 100644 --- a/apps/client/src/widgets/layout/NoteBadges.tsx +++ b/apps/client/src/widgets/layout/NoteBadges.tsx @@ -9,6 +9,7 @@ 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 NoteBadges() { return ( @@ -97,63 +98,3 @@ function ExecuteBadge() { /> ); } - -interface BadgeProps { - text?: string; - icon?: string; - className: string; - tooltip?: string; - onClick?: MouseEventHandler; - href?: string; -} - -function Badge({ icon, className, text, tooltip, onClick, href }: BadgeProps) { - const containerRef = useRef(null); - useStaticTooltip(containerRef, { - placement: "bottom", - fallbackPlacements: [ "bottom" ], - animation: false, - html: true, - title: tooltip - }); - - const content = <> - {icon && <> } - {text} - ; - - return ( -
- {href ? {content} : {content}} -
- ); -} - -function BadgeWithDropdown({ children, tooltip, className, dropdownOptions, ...props }: BadgeProps & { - children: ComponentChildren, - dropdownOptions?: Partial -}) { - return ( - } - noDropdownListStyle - noSelectButtonStyle - hideToggleArrow - title={tooltip} - titlePosition="bottom" - {...dropdownOptions} - dropdownOptions={{ - ...dropdownOptions?.dropdownOptions, - popperConfig: { - ...dropdownOptions?.dropdownOptions?.popperConfig, - placement: "bottom", strategy: "fixed" - } - }} - >{children} - ); -} diff --git a/apps/client/src/widgets/react/Badge.css b/apps/client/src/widgets/react/Badge.css new file mode 100644 index 000000000..83f3f05ef --- /dev/null +++ b/apps/client/src/widgets/react/Badge.css @@ -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; + } +} diff --git a/apps/client/src/widgets/react/Badge.tsx b/apps/client/src/widgets/react/Badge.tsx index 49d4b879d..e7844368c 100644 --- a/apps/client/src/widgets/react/Badge.tsx +++ b/apps/client/src/widgets/react/Badge.tsx @@ -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 {title} -} \ No newline at end of file +interface BadgeProps { + text?: string; + icon?: string; + className?: string; + tooltip?: string; + onClick?: MouseEventHandler; + href?: string; +} + +export default function SimpleBadge({ title, className }: SimpleBadgeProps) { + return {title}; +} + +export function Badge({ icon, className, text, tooltip, onClick, href }: BadgeProps) { + const containerRef = useRef(null); + useStaticTooltip(containerRef, { + placement: "bottom", + fallbackPlacements: [ "bottom" ], + animation: false, + html: true, + title: tooltip + }); + + const content = <> + {icon && <> } + {text} + ; + + return ( +
+ {href ? {content} : {content}} +
+ ); +} + +export function BadgeWithDropdown({ children, tooltip, className, dropdownOptions, ...props }: BadgeProps & { + children: ComponentChildren, + dropdownOptions?: Partial +}) { + return ( + } + noDropdownListStyle + noSelectButtonStyle + hideToggleArrow + title={tooltip} + titlePosition="bottom" + {...dropdownOptions} + dropdownOptions={{ + ...dropdownOptions?.dropdownOptions, + popperConfig: { + ...dropdownOptions?.dropdownOptions?.popperConfig, + placement: "bottom", strategy: "fixed" + } + }} + >{children} + ); +}