diff --git a/.vscode/settings.json b/.vscode/settings.json index 2eaee6a3b..57d22dcb8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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" } ] diff --git a/apps/client/src/layouts/desktop_layout.tsx b/apps/client/src/layouts/desktop_layout.tsx index ac4590fec..2aa7c030a 100644 --- a/apps/client/src/layouts/desktop_layout.tsx +++ b/apps/client/src/layouts/desktop_layout.tsx @@ -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() - .child(); + .child() + .optChild(isNewLayout, ) + .optChild(!isNewLayout, ) + .child() + .child() + .child() + .child() + .optChild(isNewLayout, ); 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, ) - .child() - .child() - .child() - .child() - .child() - .optChild(isNewLayout, ) - ) - .optChild(!isFloatingTitlebar, titleRow) + .child(titleRow) .optChild(!isNewLayout, ) .optChild(isNewLayout, ) .child(new WatchedFileUpdateStatusWidget()) @@ -157,8 +153,8 @@ export default class DesktopLayout { .child( new ScrollingContainer() .filling() - .optChild(isFloatingTitlebar, titleRow) - .optChild(isNewLayout, ) + .optChild(isNewLayout, ) + .optChild(isNewLayout, ) .optChild(!isNewLayout, new ContentHeader() .child() .child() diff --git a/apps/client/src/services/experimental_features.ts b/apps/client/src/services/experimental_features.ts index 6e2f14e86..398467410 100644 --- a/apps/client/src/services/experimental_features.ts +++ b/apps/client/src/services/experimental_features.ts @@ -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[]; diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 17b1b90ae..986885a22 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -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 ", - "last_modified": "Last modified on " + "last_modified": "Last modified on ", + "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.", diff --git a/apps/client/src/widgets/dialogs/PopupEditor.tsx b/apps/client/src/widgets/dialogs/PopupEditor.tsx index 152afaa4a..887568d37 100644 --- a/apps/client/src/widgets/dialogs/PopupEditor.tsx +++ b/apps/client/src/widgets/dialogs/PopupEditor.tsx @@ -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() { - {isNewLayout && } + {isNewLayout && } } customTitleBarButtons={[{ iconClassName: "bx-expand-alt", 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 new file mode 100644 index 000000000..67bd4f980 --- /dev/null +++ b/apps/client/src/widgets/layout/InlineTitle.css @@ -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; + } +} diff --git a/apps/client/src/widgets/layout/InlineTitle.tsx b/apps/client/src/widgets/layout/InlineTitle.tsx new file mode 100644 index 000000000..7882ef202 --- /dev/null +++ b/apps/client/src/widgets/layout/InlineTitle.tsx @@ -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([ + "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(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 ( +
+
+ + +
+ + + +
+ ); +} + +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 && + ), + (!isHiddenNote && metadata?.dateModified && + ) + ].filter(item => !!item); + + return items.length > 0 && ( +
+ {joinElements(items, " • ")} +
+ ); +} + +function TextWithValue({ i18nKey, value, valueTooltip }: { + i18nKey: string; + value: string; + valueTooltip: string; +}) { + const listItemRef = useRef(null); + useStaticTooltip(listItemRef, { + selector: "span.value", + title: valueTooltip, + popperConfig: { placement: "bottom" } + }); + + return ( +
  • + {value} as React.ReactElement + }} + /> +
  • + ); +} +//#endregion + +//#region Note type switcher +const SWITCHER_PINNED_NOTE_TYPES = new Set([ "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" && +
    + {blob?.contentLength === 0 && ( + <> +
    {t("note_title.note_type_switcher_label", { type: currentNoteTypeData?.title.toLocaleLowerCase() })}
    + {pinnedNoteTypes.map(noteType => noteType.type !== currentNoteType && ( + switchNoteType(note.noteId, noteType)} + /> + ))} + {collectionTemplates.length > 0 && } + {builtinTemplates.length > 0 && } + {restNoteTypes.length > 0 && } + + )} +
    + ); +} + +function MoreNoteTypes({ noteId, restNoteTypes }: { noteId: string, restNoteTypes: NoteTypeMapping[] }) { + return ( + + {restNoteTypes.map(noteType => ( + switchNoteType(noteId, noteType)} + >{noteType.title} + ))} + + ); +} + +function CollectionNoteTypes({ noteId, collectionTemplates }: { noteId: string, collectionTemplates: FNote[] }) { + return ( + + {collectionTemplates.map(collectionTemplate => ( + setTemplate(noteId, collectionTemplate.noteId)} + >{collectionTemplate.title} + ))} + + ); +} + +function TemplateNoteTypes({ noteId, builtinTemplates }: { noteId: string, builtinTemplates: FNote[] }) { + const [ userTemplates, setUserTemplates ] = useState([]); + + async function refreshTemplates() { + const templateNoteIds = await server.get("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 ( + + {userTemplates.map(template => )} + {userTemplates.length > 0 && } + {builtinTemplates.map(template => )} + + ); +} + +function TemplateItem({ noteId, template }: { noteId: string, template: FNote }) { + return ( + setTemplate(noteId, template.noteId)} + >{template.title} + ); +} + +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 diff --git a/apps/client/src/widgets/layout/NoteBadges.css b/apps/client/src/widgets/layout/NoteBadges.css new file mode 100644 index 000000000..fa3bc504f --- /dev/null +++ b/apps/client/src/widgets/layout/NoteBadges.css @@ -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; + } + } +} + diff --git a/apps/client/src/widgets/BreadcrumbBadges.tsx b/apps/client/src/widgets/layout/NoteBadges.tsx similarity index 62% rename from apps/client/src/widgets/BreadcrumbBadges.tsx rename to apps/client/src/widgets/layout/NoteBadges.tsx index 805e9da7b..0959b773c 100644 --- a/apps/client/src/widgets/BreadcrumbBadges.tsx +++ b/apps/client/src/widgets/layout/NoteBadges.tsx @@ -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 ( -
    +
    @@ -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/layout/NoteTitleActions.css b/apps/client/src/widgets/layout/NoteTitleActions.css new file mode 100644 index 000000000..52aea7a90 --- /dev/null +++ b/apps/client/src/widgets/layout/NoteTitleActions.css @@ -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; + } + } +} diff --git a/apps/client/src/widgets/NoteTitleDetails.tsx b/apps/client/src/widgets/layout/NoteTitleActions.tsx similarity index 55% rename from apps/client/src/widgets/NoteTitleDetails.tsx rename to apps/client/src/widgets/layout/NoteTitleActions.tsx index bbd00364c..765eefca1 100644 --- a/apps/client/src/widgets/NoteTitleDetails.tsx +++ b/apps/client/src/widgets/layout/NoteTitleActions.tsx @@ -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 ( -
    +
    {note && !isHiddenNote && noteType === "book" && }
    ); diff --git a/apps/client/src/widgets/layout/StatusBar.tsx b/apps/client/src/widgets/layout/StatusBar.tsx index d18269210..175aeeb53 100644 --- a/apps/client/src/widgets/layout/StatusBar.tsx +++ b/apps/client/src/widgets/layout/StatusBar.tsx @@ -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" }} >
      @@ -356,14 +356,13 @@ function CodeNoteSwitcher({ note }: StatusBarContext) { mimeTypes.find(m => m.mime === currentNoteMime) ), [ mimeTypes, currentNoteMime ]); - return ( + return (note.type === "code" && <> :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; } } diff --git a/apps/client/src/widgets/BreadcrumbBadges.css b/apps/client/src/widgets/react/Badge.css similarity index 60% rename from apps/client/src/widgets/BreadcrumbBadges.css rename to apps/client/src/widgets/react/Badge.css index 55737ae9b..83f3f05ef 100644 --- a/apps/client/src/widgets/BreadcrumbBadges.css +++ b/apps/client/src/widgets/react/Badge.css @@ -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; } 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} + ); +} diff --git a/apps/client/src/widgets/ribbon/style.css b/apps/client/src/widgets/ribbon/style.css index b314d74d1..bb0da9c4d 100644 --- a/apps/client/src/widgets/ribbon/style.css +++ b/apps/client/src/widgets/ribbon/style.css @@ -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,