From 3f32d1a244e5b9356ffa52191745179f1927f9df Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Thu, 20 Feb 2025 09:54:17 +0100 Subject: [PATCH] Added ariakit comments implementation --- packages/ariakit/src/ariakitStyles.css | 183 ++++++++++++++++++ packages/ariakit/src/badge/Badge.tsx | 75 +++++++ packages/ariakit/src/comments/Card.tsx | 39 ++++ packages/ariakit/src/comments/Comment.tsx | 84 ++++++++ packages/ariakit/src/comments/Editor.tsx | 55 ++++++ packages/ariakit/src/components.ts | 15 +- packages/ariakit/src/popover/Popover.tsx | 6 +- packages/ariakit/src/style.css | 105 +++++++++- packages/ariakit/src/toolbar/Toolbar.tsx | 9 +- .../ariakit/src/toolbar/ToolbarButton.tsx | 3 +- 10 files changed, 566 insertions(+), 8 deletions(-) create mode 100644 packages/ariakit/src/badge/Badge.tsx create mode 100644 packages/ariakit/src/comments/Card.tsx create mode 100644 packages/ariakit/src/comments/Comment.tsx create mode 100644 packages/ariakit/src/comments/Editor.tsx diff --git a/packages/ariakit/src/ariakitStyles.css b/packages/ariakit/src/ariakitStyles.css index c0e42e964f..82e5016755 100644 --- a/packages/ariakit/src/ariakitStyles.css +++ b/packages/ariakit/src/ariakitStyles.css @@ -1003,4 +1003,187 @@ responsible for the majority of the styling. */ .bn-ak-link:where(.dark, .dark *) { color: hsl(204 100% 64%); +} + +/* https://ariakit.org/components/hovercard */ +/*.bn-ak-button {*/ +/* --border: rgb(0 0 0/13%);*/ +/* --highlight: rgb(255 255 255/20%);*/ +/* --shadow: rgb(0 0 0/10%);*/ +/* display: flex;*/ +/* height: 2.5rem;*/ +/* user-select: none;*/ +/* align-items: center;*/ +/* justify-content: center;*/ +/* gap: 0.25rem;*/ +/* white-space: nowrap;*/ +/* border-style: none;*/ +/* background-color: white;*/ +/* padding-left: 1rem;*/ +/* padding-right: 1rem;*/ +/* font-size: 1rem;*/ +/* line-height: 1.5rem;*/ +/* color: black;*/ +/* text-decoration-line: none;*/ +/* outline-width: 2px;*/ +/* outline-offset: 2px;*/ +/* outline-color: hsl(204 100% 40%);*/ +/* box-shadow:*/ +/* inset 0 0 0 1px var(--border),*/ +/* inset 0 2px 0 var(--highlight),*/ +/* inset 0 -1px 0 var(--shadow),*/ +/* 0 1px 1px var(--shadow);*/ +/* position: absolute;*/ +/* top: 1rem;*/ +/* right: 1rem;*/ +/* border-radius: 9999px;*/ +/* font-weight: 600;*/ +/*}*/ + +/*.bn-ak-button:where(.dark, .dark *) {*/ +/* --border: rgb(255 255 255/10%);*/ +/* --highlight: rgb(255 255 255/5%);*/ +/* --shadow: rgb(0 0 0/25%);*/ +/* background-color: rgb(255 255 255 / 0.05);*/ +/* color: white;*/ +/* box-shadow:*/ +/* inset 0 0 0 1px var(--border),*/ +/* inset 0 -1px 0 1px var(--shadow),*/ +/* inset 0 1px 0 var(--highlight);*/ +/*}*/ + +/*.bn-ak-button:not(:active):hover {*/ +/* --border: rgb(0 0 0/33%);*/ +/*}*/ + +/*.bn-ak-button:where(.dark, .dark *):not(:active):hover {*/ +/* --border: rgb(255 255 255/25%);*/ +/*}*/ + +.bn-ak-primary { + --border: rgba(0, 0, 0, 0.15); + --highlight: rgba(255, 255, 255, 0.25); + --shadow: rgba(0, 0, 0, 0.15); + background-color: hsl(204 100% 40%); + color: white; +} + +.bn-ak-primary:hover { + --border: rgba(0, 0, 0, 0.35); + background-color: hsl(204 100% 35%); +} + +.bn-ak-primary:where(.dark, .dark *) { + --border: rgba(255, 255, 255, 0.25); + --highlight: rgba(255, 255, 255, 0.1); + --shadow: rgba(0, 0, 0, 0.25); + background-color: hsl(204 100% 35%); +} + +.bn-ak-primary:hover:where(.dark, .dark *) { + --border: rgba(255, 255, 255, 0.45); + background-color: hsl(204 100% 40%); +} + +.bn-ak-button[aria-disabled="true"] { + opacity: 0.5; +} + +.bn-ak-button[data-focus-visible] { + outline-style: solid; +} + +.bn-ak-button:active, +.bn-ak-button[data-active] { + padding-top: 0.125rem; + box-shadow: + inset 0 0 0 1px var(--border), + inset 0 2px 0 var(--border); +} + +@media (min-width: 640px) { + .bn-ak-button { + gap: 0.5rem; + } +} + +.bn-ak-button:active:where(.dark, .dark *), +.bn-ak-button[data-active]:where(.dark, .dark *) { + box-shadow: + inset 0 0 0 1px var(--border), + inset 0 1px 1px 1px var(--shadow); +} + +.bn-ak-flat { + box-shadow: none; +} + +.bn-ak-flat:where(.dark, .dark *) { + box-shadow: none; +} + +.bn-ak-flat:active:where(.dark, .dark *), +.bn-ak-flat[data-active]:where(.dark, .dark *) { + box-shadow: none; +} + +.bn-ak-anchor { + font-weight: 500; + color: hsl(204 100% 35%); + text-decoration-line: underline; + text-underline-offset: 0.25em; + text-decoration-skip-ink: none; +} + +.bn-ak-anchor:hover { + text-decoration-thickness: 3px; +} + +.bn-ak-anchor:where(.dark, .dark *) { + color: hsl(204 100% 64%); +} + +.bn-ak-hovercard { + position: relative; + z-index: 50; + display: flex; + width: 300px; + flex-direction: column; + gap: 0.5rem; + border-radius: 0.5rem; + border-width: 1px; + border-style: solid; + border-color: hsl(204 20% 88%); + background-color: white; + padding: 1rem; + color: black; + box-shadow: + 0 10px 15px -3px rgb(0 0 0 / 0.1), + 0 4px 6px -4px rgb(0 0 0 / 0.1); +} + +.bn-ak-hovercard:focus-visible, +.bn-ak-hovercard[data-focus-visible] { + outline: 2px solid hsl(204 100% 40%); + outline-offset: 2px; +} + +.bn-ak-hovercard:where(.dark, .dark *) { + border-color: hsl(204 4% 24%); + background-color: hsl(204 4% 16%); + color: white; + box-shadow: + 0 10px 15px -3px rgb(0 0 0 / 0.25), + 0 4px 6px -4px rgb(0 0 0 / 0.1); +} + +.bn-ak-avatar { + width: 4rem; + border-radius: 9999px; +} + +.bn-ak-username { + font-size: 1.125rem; + line-height: 1.75rem; + font-weight: 600; } \ No newline at end of file diff --git a/packages/ariakit/src/badge/Badge.tsx b/packages/ariakit/src/badge/Badge.tsx new file mode 100644 index 0000000000..0d89b8a592 --- /dev/null +++ b/packages/ariakit/src/badge/Badge.tsx @@ -0,0 +1,75 @@ +import { + Button as AriakitButton, + Group as AriakitGroup, + Tooltip as AriakitTooltip, + TooltipAnchor as AriakitTooltipAnchor, + TooltipProvider as AriakitTooltipProvider, +} from "@ariakit/react"; + +import { assertEmpty, mergeCSSClasses } from "@blocknote/core"; +import { ComponentProps } from "@blocknote/react"; +import { forwardRef } from "react"; + +export const Badge = forwardRef< + HTMLButtonElement, + ComponentProps["Generic"]["Badge"]["Root"] +>((props, ref) => { + const { + className, + text, + icon, + isSelected, + mainTooltip, + secondaryTooltip, + onClick, + ...rest + } = props; + + assertEmpty(rest, false); + + const badge = ( + onClick?.(event)} + ref={ref}> + {icon} + {text} + + ); + + if (!mainTooltip) { + return badge; + } + + return ( + + + + {mainTooltip} + {secondaryTooltip && {secondaryTooltip}} + + + ); +}); + +export const BadgeGroup = forwardRef< + HTMLDivElement, + ComponentProps["Generic"]["Badge"]["Group"] +>((props, ref) => { + const { className, children, ...rest } = props; + + assertEmpty(rest); + + return ( + + {children} + + ); +}); diff --git a/packages/ariakit/src/comments/Card.tsx b/packages/ariakit/src/comments/Card.tsx new file mode 100644 index 0000000000..0a31a72806 --- /dev/null +++ b/packages/ariakit/src/comments/Card.tsx @@ -0,0 +1,39 @@ +import { Group as AriakitGroup } from "@ariakit/react"; + +import { assertEmpty, mergeCSSClasses } from "@blocknote/core"; +import { ComponentProps } from "@blocknote/react"; +import { forwardRef } from "react"; + +export const Card = forwardRef< + HTMLDivElement, + ComponentProps["Comments"]["Card"] +>((props, ref) => { + const { className, children, ...rest } = props; + + assertEmpty(rest, false); + + return ( + + {children} + + ); +}); + +export const CardSection = forwardRef< + HTMLDivElement, + ComponentProps["Comments"]["CardSection"] +>((props, ref) => { + const { className, children, ...rest } = props; + + assertEmpty(rest, false); + + return ( + + {children} + + ); +}); diff --git a/packages/ariakit/src/comments/Comment.tsx b/packages/ariakit/src/comments/Comment.tsx new file mode 100644 index 0000000000..ad8413da48 --- /dev/null +++ b/packages/ariakit/src/comments/Comment.tsx @@ -0,0 +1,84 @@ +import { Group as AriakitGroup } from "@ariakit/react"; + +import { assertEmpty } from "@blocknote/core"; +import { ComponentProps } from "@blocknote/react"; +import { forwardRef, useState } from "react"; + +const AuthorInfo = forwardRef< + HTMLDivElement, + Pick +>((props, _ref) => { + const { authorInfo, timeString, ...rest } = props; + + assertEmpty(rest, false); + + if (authorInfo === "loading") { + return ( + +
+
+ + ); + } + + return ( + + {authorInfo.username} +
+ {authorInfo.username} + {timeString} +
+
+ ); +}); + +export const Comment = forwardRef< + HTMLDivElement, + ComponentProps["Comments"]["Comment"] +>((props, ref) => { + const { + className, + showActions, + authorInfo, + timeString, + actions, + children, + ...rest + } = props; + + assertEmpty(rest, false); + + const [hovered, setHovered] = useState(false); + + const doShowActions = + actions && + (showActions === true || + showActions === undefined || + (showActions === "hover" && hovered)); + + return ( + setHovered(true)} + onMouseLeave={() => setHovered(false)}> + {doShowActions ? ( + + {actions} + + ) : null} + + {children} + + ); +}); diff --git a/packages/ariakit/src/comments/Editor.tsx b/packages/ariakit/src/comments/Editor.tsx new file mode 100644 index 0000000000..857b417109 --- /dev/null +++ b/packages/ariakit/src/comments/Editor.tsx @@ -0,0 +1,55 @@ +import { assertEmpty } from "@blocknote/core"; +import { + ComponentProps, + FormattingToolbar, + FormattingToolbarController, + getFormattingToolbarItems, +} from "@blocknote/react"; +import { forwardRef, useEffect } from "react"; +import { BlockNoteView } from "../BlockNoteView.js"; + +export const Editor = forwardRef< + HTMLDivElement, + ComponentProps["Comments"]["Editor"] +>((props, ref) => { + const { className, onFocus, onBlur, editor, editable, ...rest } = props; + + assertEmpty(rest, false); + + // When we click the edit button on a comment, we also want to focus the + // comment editor + useEffect(() => { + if (editable) { + editor.focus(); + } + }, [editable, editor]); + + return ( + + + + ); +}); + +const CustomFormattingToolbar = () => { + const items = getFormattingToolbarItems([]).filter( + (el) => el.key !== "nestBlockButton" && el.key !== "unnestBlockButton" + ); + return ( + {items} + ); +}; diff --git a/packages/ariakit/src/components.ts b/packages/ariakit/src/components.ts index 372a235699..9819fcfb21 100644 --- a/packages/ariakit/src/components.ts +++ b/packages/ariakit/src/components.ts @@ -32,6 +32,10 @@ import { TableHandle } from "./tableHandle/TableHandle.js"; import { Toolbar } from "./toolbar/Toolbar.js"; import { ToolbarButton } from "./toolbar/ToolbarButton.js"; import { ToolbarSelect } from "./toolbar/ToolbarSelect.js"; +import { Card, CardSection } from "./comments/Card.js"; +import { Comment } from "./comments/Comment.js"; +import { Editor } from "./comments/Editor.js"; +import { Badge, BadgeGroup } from "./badge/Badge.js"; export const components: Components = { FormattingToolbar: { @@ -71,8 +75,17 @@ export const components: Components = { Root: TableHandle, ExtendButton: ExtendButton, }, - // @ts-ignore TODO + Comments: { + Comment: Comment, + Editor: Editor, + Card: Card, + CardSection: CardSection, + }, Generic: { + Badge: { + Root: Badge, + Group: BadgeGroup, + }, Toolbar: { Root: Toolbar, Button: ToolbarButton, diff --git a/packages/ariakit/src/popover/Popover.tsx b/packages/ariakit/src/popover/Popover.tsx index 0a92d59e4e..7118cbbbf7 100644 --- a/packages/ariakit/src/popover/Popover.tsx +++ b/packages/ariakit/src/popover/Popover.tsx @@ -29,7 +29,11 @@ export const PopoverContent = forwardRef< return ( {children} diff --git a/packages/ariakit/src/style.css b/packages/ariakit/src/style.css index eb05443137..af81137370 100644 --- a/packages/ariakit/src/style.css +++ b/packages/ariakit/src/style.css @@ -11,10 +11,11 @@ gap: 0.5rem; } -.bn-toolbar.bn-ak-toolbar { - overflow-x: auto; - max-width: 100vw; +.bn-ak-toolbar { + height: fit-content; + overflow: visible; } + .bn-toolbar .bn-ak-button { width: unset; } @@ -213,3 +214,101 @@ display: flex; flex-direction: column; } + +.bn-ariakit .bn-thread-comments { + display: flex; + flex-direction: column; + gap: 16px; +} + +.bn-ak-avatar { + height: 24px; + width: 24px; +} + +.bn-ak-username { + align-items: center; + display: flex; + font-size: 14px; + gap: 8px; + line-height: 1rem; +} + +.bn-ak-username > span { + font-size: 10px; +} + +.bn-ak-author-info { + align-items: center; + display: flex; + gap: 16px +} + +.bn-ariakit .bn-comment-editor .bn-editor { + background-color: transparent; +} + +.bn-ak-badge { + align-items: center; + border-radius: 12px; + display: flex; + gap: 4px; + height: fit-content; + padding: 2px 10px; + position: relative; + width: fit-content; +} + +.bn-ak-badge span { + align-items: center; + display: flex; + height: fit-content; +} + +.bn-ak-badge-group { + align-items: center; + display: flex; + gap: 4px; + flex-wrap: wrap; + width: 100%; +} + +.bn-ariakit .bn-thread-comment { + position: relative; + display: flex; + flex-direction: column; + gap: 4px; +} + +.bn-ariakit .bn-action-toolbar .bn-ak-button { + height: 1.5rem; + padding: 8px; + width: fit-content; +} + +.bn-ak-hovercard { + z-index: 0; +} + +.bn-ak-panel-popover { + background-color: transparent; + border: none; + box-shadow: none; + padding: 0; +} + +.bn-ariakit .bn-comment-actions-wrapper { + align-items: start; + display: flex; + justify-content: flex-end; +} + +.bn-ak-skeleton { + background-color: rgb(255 255 255/25%); +} + +.bn-ak-username.bn-ak-skeleton { + border-radius: 8px; + height: 16px; + width: 100px; +} \ No newline at end of file diff --git a/packages/ariakit/src/toolbar/Toolbar.tsx b/packages/ariakit/src/toolbar/Toolbar.tsx index b5def0f74b..6ce24ed560 100644 --- a/packages/ariakit/src/toolbar/Toolbar.tsx +++ b/packages/ariakit/src/toolbar/Toolbar.tsx @@ -9,7 +9,14 @@ type ToolbarProps = ComponentProps["FormattingToolbar"]["Root"] & export const Toolbar = forwardRef( (props, ref) => { - const { className, children, onMouseEnter, onMouseLeave, ...rest } = props; + const { + className, + children, + onMouseEnter, + onMouseLeave, + variant, + ...rest + } = props; // TODO // @ts-ignore diff --git a/packages/ariakit/src/toolbar/ToolbarButton.tsx b/packages/ariakit/src/toolbar/ToolbarButton.tsx index c62021f3cf..b277963db9 100644 --- a/packages/ariakit/src/toolbar/ToolbarButton.tsx +++ b/packages/ariakit/src/toolbar/ToolbarButton.tsx @@ -40,7 +40,6 @@ export const ToolbarButton = forwardRef( return ( ( } /> - + {mainTooltip} {secondaryTooltip && {secondaryTooltip}}