diff --git a/apps/client/src/widgets/react/Icon.tsx b/apps/client/src/widgets/react/Icon.tsx index e047a1762..6d17dc3b9 100644 --- a/apps/client/src/widgets/react/Icon.tsx +++ b/apps/client/src/widgets/react/Icon.tsx @@ -1,8 +1,16 @@ -interface IconProps { +import clsx from "clsx"; +import { HTMLAttributes } from "preact"; + +interface IconProps extends Pick, "className" | "onClick"> { icon?: string; className?: string; } -export default function Icon({ icon, className }: IconProps) { - return -} \ No newline at end of file +export default function Icon({ icon, className, ...restProps }: IconProps) { + return ( + + ); +} diff --git a/apps/client/src/widgets/sidebar/TableOfContents.css b/apps/client/src/widgets/sidebar/TableOfContents.css new file mode 100644 index 000000000..79c6ee548 --- /dev/null +++ b/apps/client/src/widgets/sidebar/TableOfContents.css @@ -0,0 +1,82 @@ +.toc ol { + position: relative; + overflow: hidden; + padding-inline-start: 0px; + transition: max-height 0.3s ease; +} + +.toc li.collapsed + ol { + display:none; +} + +.toc li + ol:before { + content: ""; + position: absolute; + height: 100%; + border-inline-start: 1px solid var(--main-border-color); + z-index: 10; +} + +.toc li { + display: flex; + position: relative; + list-style: none; + align-items: center; + padding-inline-start: 7px; + cursor: pointer; + text-align: justify; + word-wrap: break-word; + hyphens: auto; +} + +.toc > ol { + --toc-depth-level: 1; +} +.toc > ol > ol { + --toc-depth-level: 2; +} +.toc > ol > ol > ol { + --toc-depth-level: 3; +} +.toc > ol > ol > ol > ol { + --toc-depth-level: 4; +} +.toc > ol > ol > ol > ol > ol { + --toc-depth-level: 5; +} + +.toc > ol ol::before { + inset-inline-start: calc((var(--toc-depth-level) - 2) * 20px + 14px); +} + +.toc li { + padding-inline-start: calc((var(--toc-depth-level) - 1) * 20px + 4px); +} + +.toc li .collapse-button { + display: flex; + position: relative; + width: 21px; + height: 21px; + flex-shrink: 0; + align-items: center; + justify-content: center; + transition: transform 0.3s ease; +} + +.toc li.collapsed .collapse-button { + transform: rotate(-90deg); +} + +.toc li .item-content { + margin-inline-start: 25px; + flex: 1; +} + +.toc li .collapse-button + .item-content { + margin-inline-start: 4px; +} + +.toc li:hover { + font-weight: bold; +} diff --git a/apps/client/src/widgets/sidebar/TableOfContents.tsx b/apps/client/src/widgets/sidebar/TableOfContents.tsx index b4391b678..8ed728c26 100644 --- a/apps/client/src/widgets/sidebar/TableOfContents.tsx +++ b/apps/client/src/widgets/sidebar/TableOfContents.tsx @@ -1,8 +1,12 @@ +import "./TableOfContents.css"; + import { CKTextEditor, ModelElement } from "@triliumnext/ckeditor5"; +import clsx from "clsx"; import { useEffect, useState } from "preact/hooks"; import { t } from "../../services/i18n"; import { useActiveNoteContext, useIsNoteReadOnly, useNoteProperty, useTextEditor } from "../react/hooks"; +import Icon from "../react/Icon"; import RightPanelWidget from "./RightPanelWidget"; interface RawHeading { @@ -66,19 +70,35 @@ function AbstractTableOfContents({ headings }: { headings: RawHeading[]; }) { const nestedHeadings = buildHeadingTree(headings); - return nestedHeadings.map(heading => ); + return ( + +
    + {nestedHeadings.map(heading => )} +
+
+ ); } function TableOfContentsHeading({ heading }: { heading: HeadingsWithNesting }) { + const [ collapsed, setCollapsed ] = useState(false); return ( -
  • - {heading.text} + <> +
  • + {heading.children.length > 0 && ( + setCollapsed(!collapsed)} + /> + )} + {heading.text} +
  • {heading.children && ( -
      +
        {heading.children.map(heading => )} -
    + )} - + ); }