diff --git a/CHANGELOG.md b/CHANGELOG.md index e271d397f8..db39825612 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ ## vNEXT (not yet released) +## v3.15.1 + +### `@liveblocks/react-ui` + +- Add `gap` prop to `AvatarStack` to control the `--lb-avatar-stack-gap` CSS + variable. +- Add `padding` prop to `CommentPin` to control the `--lb-comment-pin-padding` + CSS variable. +- Fix `size` props on `AvatarStack` and `CommentPin` not working as expected + when passing numbers. +- Fix `autoFocus` prop on `FloatingComposer`. +- Improve avatars’ ordering and `max` logic in `AvatarStack`. +- Support `children` prop on `CommentPin`. + ## v3.15.0 ### `@liveblocks/react-ui` diff --git a/docs/pages/api-reference/liveblocks-react-ui.mdx b/docs/pages/api-reference/liveblocks-react-ui.mdx index 49a9bfc62d..175ed5e0e2 100644 --- a/docs/pages/api-reference/liveblocks-react-ui.mdx +++ b/docs/pages/api-reference/liveblocks-react-ui.mdx @@ -1883,74 +1883,6 @@ function Component() { -#### CommentPin - -Displays a comment pin that can be used as a trigger for `FloatingComposer` and -`FloatingThread`, or anywhere else in your UI. - -```tsx - - - -``` - -
- CommentPin -
- -Set the `userId` prop to display an avatar inside the pin, for example to -represent the thread’s author. - -```tsx - -``` - -Use the `corner` prop to choose which corner the pin points to, it will move -itself to always point to wherever it is positioned. - -```tsx - -``` - -You can either use the `size` prop or override `--lb-comment-pin-size` with CSS -to change the pin’s size. - -```tsx - - - -``` - -##### Props [#CommentPin-props] - - - - The corner that points to the comment position. - - - The user ID to optionally display an avatar for. - - - The size of the pin. - - - ##### Comment.Avatar [#Comment.Avatar] Displays a comment’s avatar. Use this within the `avatar` prop to follow default @@ -2037,6 +1969,87 @@ Displays a dropdown item in the comment’s dropdown menu. Use this within the +#### CommentPin + +Displays a comment pin that can be used as a trigger for `FloatingComposer` and +`FloatingThread`, or anywhere else in your UI. + +```tsx + + + +``` + +
+ CommentPin +
+ +Set the `userId` prop to display an avatar inside the pin, for example to +represent the thread’s author. + +```tsx + +``` + +Use the `corner` prop to choose which corner the pin points to, it will move +itself to always point to wherever it is positioned. + +```tsx + +``` + +You can either use the `size` prop or override `--lb-comment-pin-size` with CSS +to change the pin’s size. + +```tsx + + + +``` + +Pass `children` to display custom content inside the pin. When children are +provided, the `userId` prop is ignored. + +```tsx + + + +``` + +##### Props [#CommentPin-props] + + + + The corner that points to the comment position. + + + The user ID to optionally display an avatar for. Ignored if `children` is + provided. + + + The size of the pin. + + + The content shown in the pin. If provided, the `userId` prop is ignored. + + + ### Primitives Primitives are unstyled, headless components that can be used to create fully @@ -3794,8 +3807,9 @@ properties. Optional additional user IDs to include in the stack. - - The maximum number of visible avatars. + + The maximum number of items in the stack (at least 2). Set to `null` to show + all avatars. The size of the avatars. diff --git a/package-lock.json b/package-lock.json index 1a89ae3d23..03affc1e31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37178,10 +37178,10 @@ }, "packages/liveblocks-client": { "name": "@liveblocks/client", - "version": "3.15.0", + "version": "3.15.1", "license": "Apache-2.0", "dependencies": { - "@liveblocks/core": "3.15.0" + "@liveblocks/core": "3.15.1" }, "devDependencies": { "@liveblocks/eslint-config": "*", @@ -37190,7 +37190,7 @@ }, "packages/liveblocks-core": { "name": "@liveblocks/core", - "version": "3.15.0", + "version": "3.15.1", "license": "Apache-2.0", "devDependencies": { "@liveblocks/eslint-config": "*", @@ -37208,11 +37208,11 @@ }, "packages/liveblocks-emails": { "name": "@liveblocks/emails", - "version": "3.15.0", + "version": "3.15.1", "license": "Apache-2.0", "dependencies": { - "@liveblocks/core": "3.15.0", - "@liveblocks/node": "3.15.0" + "@liveblocks/core": "3.15.1", + "@liveblocks/node": "3.15.1" }, "devDependencies": { "@liveblocks/eslint-config": "*", @@ -37231,10 +37231,10 @@ }, "packages/liveblocks-node": { "name": "@liveblocks/node", - "version": "3.15.0", + "version": "3.15.1", "license": "Apache-2.0", "dependencies": { - "@liveblocks/core": "3.15.0", + "@liveblocks/core": "3.15.1", "@stablelib/base64": "^1.0.1", "fast-sha256": "^1.3.0", "node-fetch": "^2.6.1" @@ -37249,11 +37249,11 @@ }, "packages/liveblocks-node-lexical": { "name": "@liveblocks/node-lexical", - "version": "3.15.0", + "version": "3.15.1", "license": "Apache-2.0", "dependencies": { - "@liveblocks/core": "3.15.0", - "@liveblocks/node": "3.15.0", + "@liveblocks/core": "3.15.1", + "@liveblocks/node": "3.15.1", "yjs": "^13.6.18" }, "devDependencies": { @@ -37270,11 +37270,11 @@ }, "packages/liveblocks-node-prosemirror": { "name": "@liveblocks/node-prosemirror", - "version": "3.15.0", + "version": "3.15.1", "license": "Apache-2.0", "dependencies": { - "@liveblocks/core": "3.15.0", - "@liveblocks/node": "3.15.0", + "@liveblocks/core": "3.15.1", + "@liveblocks/node": "3.15.1", "yjs": "^13.6.20" }, "devDependencies": { @@ -37294,11 +37294,11 @@ }, "packages/liveblocks-react": { "name": "@liveblocks/react", - "version": "3.15.0", + "version": "3.15.1", "license": "Apache-2.0", "dependencies": { - "@liveblocks/client": "3.15.0", - "@liveblocks/core": "3.15.0" + "@liveblocks/client": "3.15.1", + "@liveblocks/core": "3.15.1" }, "devDependencies": { "@liveblocks/eslint-config": "*", @@ -37328,15 +37328,15 @@ }, "packages/liveblocks-react-blocknote": { "name": "@liveblocks/react-blocknote", - "version": "3.15.0", + "version": "3.15.1", "license": "Apache-2.0", "dependencies": { - "@liveblocks/client": "3.15.0", - "@liveblocks/core": "3.15.0", - "@liveblocks/react": "3.15.0", - "@liveblocks/react-tiptap": "3.15.0", - "@liveblocks/react-ui": "3.15.0", - "@liveblocks/yjs": "3.15.0", + "@liveblocks/client": "3.15.1", + "@liveblocks/core": "3.15.1", + "@liveblocks/react": "3.15.1", + "@liveblocks/react-tiptap": "3.15.1", + "@liveblocks/react-ui": "3.15.1", + "@liveblocks/yjs": "3.15.1", "@tiptap/core": "^3.19.0", "vitest-tsconfig-paths": "^3.4.1" }, @@ -37373,15 +37373,15 @@ }, "packages/liveblocks-react-lexical": { "name": "@liveblocks/react-lexical", - "version": "3.15.0", + "version": "3.15.1", "license": "Apache-2.0", "dependencies": { "@floating-ui/react-dom": "^2.1.0", - "@liveblocks/client": "3.15.0", - "@liveblocks/core": "3.15.0", - "@liveblocks/react": "3.15.0", - "@liveblocks/react-ui": "3.15.0", - "@liveblocks/yjs": "3.15.0", + "@liveblocks/client": "3.15.1", + "@liveblocks/core": "3.15.1", + "@liveblocks/react": "3.15.1", + "@liveblocks/react-ui": "3.15.1", + "@liveblocks/yjs": "3.15.1", "radix-ui": "^1.4.0", "yjs": "^13.6.18" }, @@ -39015,15 +39015,15 @@ }, "packages/liveblocks-react-tiptap": { "name": "@liveblocks/react-tiptap", - "version": "3.15.0", + "version": "3.15.1", "license": "Apache-2.0", "dependencies": { "@floating-ui/react-dom": "^2.1.0", - "@liveblocks/client": "3.15.0", - "@liveblocks/core": "3.15.0", - "@liveblocks/react": "3.15.0", - "@liveblocks/react-ui": "3.15.0", - "@liveblocks/yjs": "3.15.0", + "@liveblocks/client": "3.15.1", + "@liveblocks/core": "3.15.1", + "@liveblocks/react": "3.15.1", + "@liveblocks/react-ui": "3.15.1", + "@liveblocks/yjs": "3.15.1", "@tiptap/core": "^3.19.0", "@tiptap/react": "^3.19.0", "@tiptap/suggestion": "^3.19.0", @@ -40671,13 +40671,13 @@ }, "packages/liveblocks-react-ui": { "name": "@liveblocks/react-ui", - "version": "3.15.0", + "version": "3.15.1", "license": "Apache-2.0", "dependencies": { "@floating-ui/react-dom": "^2.1.0", - "@liveblocks/client": "3.15.0", - "@liveblocks/core": "3.15.0", - "@liveblocks/react": "3.15.0", + "@liveblocks/client": "3.15.1", + "@liveblocks/core": "3.15.1", + "@liveblocks/react": "3.15.1", "frimousse": "^0.2.0", "marked": "^15.0.11", "radix-ui": "^1.4.0", @@ -41051,11 +41051,11 @@ }, "packages/liveblocks-redux": { "name": "@liveblocks/redux", - "version": "3.15.0", + "version": "3.15.1", "license": "Apache-2.0", "dependencies": { - "@liveblocks/client": "3.15.0", - "@liveblocks/core": "3.15.0" + "@liveblocks/client": "3.15.1", + "@liveblocks/core": "3.15.1" }, "devDependencies": { "@liveblocks/eslint-config": "*", @@ -41211,11 +41211,11 @@ }, "packages/liveblocks-yjs": { "name": "@liveblocks/yjs", - "version": "3.15.0", + "version": "3.15.1", "license": "Apache-2.0", "dependencies": { - "@liveblocks/client": "3.15.0", - "@liveblocks/core": "3.15.0", + "@liveblocks/client": "3.15.1", + "@liveblocks/core": "3.15.1", "@noble/hashes": "^1.8.0", "js-base64": "^3.7.7", "y-indexeddb": "^9.0.12" @@ -41280,11 +41280,11 @@ }, "packages/liveblocks-zustand": { "name": "@liveblocks/zustand", - "version": "3.15.0", + "version": "3.15.1", "license": "Apache-2.0", "dependencies": { - "@liveblocks/client": "3.15.0", - "@liveblocks/core": "3.15.0" + "@liveblocks/client": "3.15.1", + "@liveblocks/core": "3.15.1" }, "devDependencies": { "@liveblocks/eslint-config": "*", diff --git a/packages/liveblocks-client/package.json b/packages/liveblocks-client/package.json index 8f8d2b3e94..981e7ca17f 100644 --- a/packages/liveblocks-client/package.json +++ b/packages/liveblocks-client/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/client", - "version": "3.15.0", + "version": "3.15.1", "description": "A client that lets you interact with Liveblocks servers. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.", "license": "Apache-2.0", "author": "Liveblocks Inc.", @@ -36,7 +36,7 @@ "test:watch": "NODE_OPTIONS=\"--no-deprecation\" vitest" }, "dependencies": { - "@liveblocks/core": "3.15.0" + "@liveblocks/core": "3.15.1" }, "devDependencies": { "@liveblocks/eslint-config": "*", diff --git a/packages/liveblocks-core/package.json b/packages/liveblocks-core/package.json index dd361f1c4f..e33128baa5 100644 --- a/packages/liveblocks-core/package.json +++ b/packages/liveblocks-core/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/core", - "version": "3.15.0", + "version": "3.15.1", "description": "Private internals for Liveblocks. DO NOT import directly from this package!", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/liveblocks-emails/package.json b/packages/liveblocks-emails/package.json index 5ac86af68d..81e1d63f97 100644 --- a/packages/liveblocks-emails/package.json +++ b/packages/liveblocks-emails/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/emails", - "version": "3.15.0", + "version": "3.15.1", "description": "A set of functions and utilities to make sending emails based on Liveblocks notification events easy. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.", "license": "Apache-2.0", "author": "Liveblocks Inc.", @@ -37,8 +37,8 @@ "test:watch": "NODE_OPTIONS=\"--no-deprecation\" vitest" }, "dependencies": { - "@liveblocks/core": "3.15.0", - "@liveblocks/node": "3.15.0" + "@liveblocks/core": "3.15.1", + "@liveblocks/node": "3.15.1" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc" diff --git a/packages/liveblocks-node-lexical/package.json b/packages/liveblocks-node-lexical/package.json index f9219d45a5..610a5a12b0 100644 --- a/packages/liveblocks-node-lexical/package.json +++ b/packages/liveblocks-node-lexical/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/node-lexical", - "version": "3.15.0", + "version": "3.15.1", "description": "A server-side utility that lets you modify lexical documents hosted in Liveblocks.", "license": "Apache-2.0", "author": "Liveblocks Inc.", @@ -36,8 +36,8 @@ "test:watch": "NODE_OPTIONS=\"--no-deprecation\" vitest" }, "dependencies": { - "@liveblocks/core": "3.15.0", - "@liveblocks/node": "3.15.0", + "@liveblocks/core": "3.15.1", + "@liveblocks/node": "3.15.1", "yjs": "^13.6.18" }, "peerDependencies": { diff --git a/packages/liveblocks-node-prosemirror/package.json b/packages/liveblocks-node-prosemirror/package.json index bcd4daa70b..6784924719 100644 --- a/packages/liveblocks-node-prosemirror/package.json +++ b/packages/liveblocks-node-prosemirror/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/node-prosemirror", - "version": "3.15.0", + "version": "3.15.1", "description": "A server-side utility that lets you modify prosemirror and tiptap documents hosted in Liveblocks.", "license": "Apache-2.0", "author": "Liveblocks Inc.", @@ -36,8 +36,8 @@ "test:watch": "NODE_OPTIONS=\"--no-deprecation\" vitest" }, "dependencies": { - "@liveblocks/core": "3.15.0", - "@liveblocks/node": "3.15.0", + "@liveblocks/core": "3.15.1", + "@liveblocks/node": "3.15.1", "yjs": "^13.6.20" }, "peerDependencies": { diff --git a/packages/liveblocks-node/package.json b/packages/liveblocks-node/package.json index 097354ffcb..7b11aced40 100644 --- a/packages/liveblocks-node/package.json +++ b/packages/liveblocks-node/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/node", - "version": "3.15.0", + "version": "3.15.1", "description": "A server-side utility that lets you set up a Liveblocks authentication endpoint. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.", "license": "Apache-2.0", "author": "Liveblocks Inc.", @@ -36,7 +36,7 @@ "test:watch": "NODE_OPTIONS=\"--no-deprecation\" vitest" }, "dependencies": { - "@liveblocks/core": "3.15.0", + "@liveblocks/core": "3.15.1", "@stablelib/base64": "^1.0.1", "fast-sha256": "^1.3.0", "node-fetch": "^2.6.1" diff --git a/packages/liveblocks-react-blocknote/package.json b/packages/liveblocks-react-blocknote/package.json index 292f32364e..82a705526e 100644 --- a/packages/liveblocks-react-blocknote/package.json +++ b/packages/liveblocks-react-blocknote/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/react-blocknote", - "version": "3.15.0", + "version": "3.15.1", "description": "An integration of BlockNote + React to enable collaboration, comments, live cursors, and more with Liveblocks.", "license": "Apache-2.0", "author": "Liveblocks Inc.", @@ -44,12 +44,12 @@ "test:watch": "NODE_OPTIONS=\"--no-deprecation\" vitest" }, "dependencies": { - "@liveblocks/client": "3.15.0", - "@liveblocks/core": "3.15.0", - "@liveblocks/react": "3.15.0", - "@liveblocks/react-tiptap": "3.15.0", - "@liveblocks/react-ui": "3.15.0", - "@liveblocks/yjs": "3.15.0", + "@liveblocks/client": "3.15.1", + "@liveblocks/core": "3.15.1", + "@liveblocks/react": "3.15.1", + "@liveblocks/react-tiptap": "3.15.1", + "@liveblocks/react-ui": "3.15.1", + "@liveblocks/yjs": "3.15.1", "@tiptap/core": "^3.19.0", "vitest-tsconfig-paths": "^3.4.1" }, diff --git a/packages/liveblocks-react-lexical/package.json b/packages/liveblocks-react-lexical/package.json index 788948b41a..dba25e9a1d 100644 --- a/packages/liveblocks-react-lexical/package.json +++ b/packages/liveblocks-react-lexical/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/react-lexical", - "version": "3.15.0", + "version": "3.15.1", "description": "An integration of Lexical + React to enable collaboration, comments, live cursors, and more with Liveblocks.", "license": "Apache-2.0", "author": "Liveblocks Inc.", @@ -45,11 +45,11 @@ }, "dependencies": { "@floating-ui/react-dom": "^2.1.0", - "@liveblocks/client": "3.15.0", - "@liveblocks/core": "3.15.0", - "@liveblocks/react": "3.15.0", - "@liveblocks/react-ui": "3.15.0", - "@liveblocks/yjs": "3.15.0", + "@liveblocks/client": "3.15.1", + "@liveblocks/core": "3.15.1", + "@liveblocks/react": "3.15.1", + "@liveblocks/react-ui": "3.15.1", + "@liveblocks/yjs": "3.15.1", "radix-ui": "^1.4.0", "yjs": "^13.6.18" }, diff --git a/packages/liveblocks-react-tiptap/package.json b/packages/liveblocks-react-tiptap/package.json index 9a40ac8f63..b8f966cb93 100644 --- a/packages/liveblocks-react-tiptap/package.json +++ b/packages/liveblocks-react-tiptap/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/react-tiptap", - "version": "3.15.0", + "version": "3.15.1", "description": "An integration of TipTap + React to enable collaboration, comments, live cursors, and more with Liveblocks.", "license": "Apache-2.0", "author": "Liveblocks Inc.", @@ -45,11 +45,11 @@ }, "dependencies": { "@floating-ui/react-dom": "^2.1.0", - "@liveblocks/client": "3.15.0", - "@liveblocks/core": "3.15.0", - "@liveblocks/react": "3.15.0", - "@liveblocks/react-ui": "3.15.0", - "@liveblocks/yjs": "3.15.0", + "@liveblocks/client": "3.15.1", + "@liveblocks/core": "3.15.1", + "@liveblocks/react": "3.15.1", + "@liveblocks/react-ui": "3.15.1", + "@liveblocks/yjs": "3.15.1", "@tiptap/core": "^3.19.0", "@tiptap/react": "^3.19.0", "@tiptap/suggestion": "^3.19.0", diff --git a/packages/liveblocks-react-ui/package.json b/packages/liveblocks-react-ui/package.json index b8b0b77bfc..2f9f6035a5 100644 --- a/packages/liveblocks-react-ui/package.json +++ b/packages/liveblocks-react-ui/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/react-ui", - "version": "3.15.0", + "version": "3.15.1", "description": "A set of React pre-built components for the Liveblocks products. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.", "license": "Apache-2.0", "author": "Liveblocks Inc.", @@ -78,9 +78,9 @@ }, "dependencies": { "@floating-ui/react-dom": "^2.1.0", - "@liveblocks/client": "3.15.0", - "@liveblocks/core": "3.15.0", - "@liveblocks/react": "3.15.0", + "@liveblocks/client": "3.15.1", + "@liveblocks/core": "3.15.1", + "@liveblocks/react": "3.15.1", "frimousse": "^0.2.0", "marked": "^15.0.11", "radix-ui": "^1.4.0", diff --git a/packages/liveblocks-react-ui/src/components/AvatarStack.tsx b/packages/liveblocks-react-ui/src/components/AvatarStack.tsx index 7546cf0530..836e0baeca 100644 --- a/packages/liveblocks-react-ui/src/components/AvatarStack.tsx +++ b/packages/liveblocks-react-ui/src/components/AvatarStack.tsx @@ -11,6 +11,7 @@ import { import type { GlobalOverrides } from "../overrides"; import { useOverrides } from "../overrides"; import { cn } from "../utils/cn"; +import { px } from "../utils/px"; import { Avatar } from "./internal/Avatar"; import { Tooltip, TooltipProvider } from "./internal/Tooltip"; import { User } from "./internal/User"; @@ -22,16 +23,21 @@ export interface AvatarStackProps extends ComponentPropsWithoutRef<"div"> { userIds?: string[]; /** - * The maximum number of visible avatars. - * Defaults to 3, set to `undefined` to show all avatars. + * The maximum number of items in the stack (at least 2). + * Defaults to 3, set to `null` to show all avatars. */ - max?: number; + max?: number | null; /** * The size of the avatars. */ size?: string | number; + /** + * The gap around the avatars. + */ + gap?: string | number; + /** * Override the component's strings. */ @@ -47,6 +53,7 @@ export const AvatarStack = forwardRef( userIds: additionalUserIds = [], max = 3, size, + gap, overrides, className, style, @@ -55,19 +62,25 @@ export const AvatarStack = forwardRef( forwardedRef ) => { const $ = useOverrides(overrides); - const otherIds = useOthers((others) => others.map((user) => user.id)); + const otherIds = useOthers((others) => + [...others] + .sort((a, b) => b.connectionId - a.connectionId) + .map((user) => user.id) + ); const selfId = useSelf((self) => self.id); const userIds = useMemo(() => { - const uniqueUserIds = new Set([ - selfId, - ...otherIds, - ...additionalUserIds, - ]); + const uniqueUserIds = new Set( + [selfId, ...otherIds, ...additionalUserIds].filter( + (userId): userId is string => userId !== null && userId !== undefined + ) + ); return [...uniqueUserIds]; }, [selfId, otherIds, additionalUserIds]); - const maxAvatars = Math.max(1, Math.floor(max)); - const visibleUserIds = userIds.slice(0, maxAvatars); + const maxItems = max === null ? Infinity : Math.max(2, Math.floor(max)); + const shouldShowMore = userIds.length > maxItems; + const visibleAvatarsCount = shouldShowMore ? maxItems - 1 : maxItems; + const visibleUserIds = userIds.slice(0, visibleAvatarsCount); const hiddenUserIds = userIds.slice(visibleUserIds.length); const remainingUsersCount = hiddenUserIds.length; const visibleItemsCount = @@ -84,8 +97,9 @@ export const AvatarStack = forwardRef( dir={$.dir} style={ { - "--lb-avatar-stack-count": visibleItemsCount - 1, - "--lb-avatar-stack-size": size, + "--lb-avatar-stack-count": visibleItemsCount, + "--lb-avatar-stack-size": px(size), + "--lb-avatar-stack-gap": px(gap), ...style, } as CSSProperties } diff --git a/packages/liveblocks-react-ui/src/components/CommentPin.tsx b/packages/liveblocks-react-ui/src/components/CommentPin.tsx index 10fa26f76b..d53efddbc8 100644 --- a/packages/liveblocks-react-ui/src/components/CommentPin.tsx +++ b/packages/liveblocks-react-ui/src/components/CommentPin.tsx @@ -1,9 +1,10 @@ "use client"; -import type { ComponentPropsWithoutRef, CSSProperties } from "react"; +import type { ComponentPropsWithoutRef, CSSProperties, ReactNode } from "react"; import { forwardRef } from "react"; import { cn } from "../utils/cn"; +import { px } from "../utils/px"; import { Avatar } from "./internal/Avatar"; export interface CommentPinProps extends ComponentPropsWithoutRef<"button"> { @@ -22,6 +23,17 @@ export interface CommentPinProps extends ComponentPropsWithoutRef<"button"> { * The size of the pin. */ size?: string | number; + + /** + * The padding within the pin. + */ + padding?: string | number; + + /** + * The content shown in the pin. + * If provided, the `userId` prop is ignored. + */ + children?: ReactNode; } /** @@ -34,9 +46,11 @@ export const CommentPin = forwardRef( corner = "bottom-left", userId, size, + padding, type = "button", className, style, + children, ...props }, forwardedRef @@ -45,14 +59,21 @@ export const CommentPin = forwardRef( ); } diff --git a/packages/liveblocks-react-ui/src/components/Cursors.tsx b/packages/liveblocks-react-ui/src/components/Cursors.tsx index 9971a1ac1f..9693056057 100644 --- a/packages/liveblocks-react-ui/src/components/Cursors.tsx +++ b/packages/liveblocks-react-ui/src/components/Cursors.tsx @@ -13,17 +13,11 @@ import type { MutableRefObject, PointerEvent, } from "react"; -import { - forwardRef, - useCallback, - useEffect, - useImperativeHandle, - useRef, - useState, -} from "react"; +import { forwardRef, useCallback, useEffect, useRef, useState } from "react"; import { type Animatable, makeAnimationLoop } from "../utils/animation-loop"; import { cn } from "../utils/cn"; +import { useRefs } from "../utils/use-refs"; import { useWindowFocus } from "../utils/use-window-focus"; import { Cursor } from "./Cursor"; @@ -241,7 +235,8 @@ export const Cursors = forwardRef( { className, children, presenceKey = DEFAULT_PRESENCE_KEY, ...props }, forwardedRef ) => { - const ref = useRef(null); + const containerRef = useRef(null); + const mergedRefs = useRefs(forwardedRef, containerRef); const updateMyPresence = useUpdateMyPresence(); const othersConnectionIds = useOthersConnectionIds(); const sizeRef = useRef(null); @@ -249,9 +244,9 @@ export const Cursors = forwardRef( const isWindowFocused = useWindowFocus(); useEffect(() => { - const element = ref.current; + const container = containerRef.current; - if (!element) { + if (!container) { return; } @@ -262,7 +257,7 @@ export const Cursors = forwardRef( const observer = new ResizeObserver((entries) => { for (const entry of entries) { - if (entry.target === element) { + if (entry.target === container) { setSize({ width: entry.contentRect.width, height: entry.contentRect.height, @@ -272,11 +267,11 @@ export const Cursors = forwardRef( }); setSize({ - width: element.clientWidth, - height: element.clientHeight, + width: container.clientWidth, + height: container.clientHeight, }); - observer.observe(element); + observer.observe(container); return () => { observer.disconnect(); @@ -285,13 +280,13 @@ export const Cursors = forwardRef( const handlePointerMove = useCallback( (event: PointerEvent) => { - const element = ref.current; + const container = containerRef.current; - if (!element) { + if (!container) { return; } - const bounds = element.getBoundingClientRect(); + const bounds = container.getBoundingClientRect(); if (bounds.width === 0 || bounds.height === 0) { return; @@ -321,19 +316,13 @@ export const Cursors = forwardRef( } }, [isWindowFocused, updateMyPresence, presenceKey]); - useImperativeHandle( - forwardedRef, - () => ref.current, - [] - ); - return (
{othersConnectionIds.map((connectionId) => ( diff --git a/packages/liveblocks-react-ui/src/components/FloatingComposer.tsx b/packages/liveblocks-react-ui/src/components/FloatingComposer.tsx index 5d798fbfe3..d5942bae33 100644 --- a/packages/liveblocks-react-ui/src/components/FloatingComposer.tsx +++ b/packages/liveblocks-react-ui/src/components/FloatingComposer.tsx @@ -7,6 +7,7 @@ import { forwardRef, type ReactNode, type RefAttributes, + useRef, } from "react"; import { useLiveblocksUiConfig } from "../config"; @@ -17,6 +18,7 @@ import { import { useOverrides } from "../overrides"; import { cn } from "../utils/cn"; import { useControllableState } from "../utils/use-controllable-state"; +import { useRefs } from "../utils/use-refs"; import type { ComposerProps } from "./Composer"; import { Composer } from "./Composer"; @@ -57,12 +59,15 @@ export const FloatingComposer = forwardRef( sideOffset = FLOATING_ELEMENT_SIDE_OFFSET, align = "start", alignOffset, + autoFocus = true, overrides, className, ...props }: FloatingComposerProps, forwardedRef: ForwardedRef ) => { + const composerRef = useRef(null); + const mergedRefs = useRefs(forwardedRef, composerRef); const $ = useOverrides(overrides); const { portalContainer } = useLiveblocksUiConfig(); const [isOpen, setIsOpen] = useControllableState( @@ -83,6 +88,7 @@ export const FloatingComposer = forwardRef( dir={$.dir} side={side} sideOffset={sideOffset} + updatePositionStrategy="always" align={align} alignOffset={alignOffset} collisionPadding={FLOATING_ELEMENT_COLLISION_PADDING} @@ -104,13 +110,20 @@ export const FloatingComposer = forwardRef( event.preventDefault(); } }} + onOpenAutoFocus={(event) => { + if (!autoFocus) { + event.preventDefault(); + composerRef.current?.focus(); + } + }} asChild > )} /> diff --git a/packages/liveblocks-react-ui/src/components/FloatingThread.tsx b/packages/liveblocks-react-ui/src/components/FloatingThread.tsx index cd2ac763a4..c092b1f38e 100644 --- a/packages/liveblocks-react-ui/src/components/FloatingThread.tsx +++ b/packages/liveblocks-react-ui/src/components/FloatingThread.tsx @@ -81,6 +81,7 @@ export const FloatingThread = forwardRef( dir={$.dir} side={side} sideOffset={sideOffset} + updatePositionStrategy="always" align={align} alignOffset={alignOffset} collisionPadding={FLOATING_ELEMENT_COLLISION_PADDING} diff --git a/packages/liveblocks-react-ui/src/components/internal/Dropdown.tsx b/packages/liveblocks-react-ui/src/components/internal/Dropdown.tsx index 17e560ce64..01e48853ec 100644 --- a/packages/liveblocks-react-ui/src/components/internal/Dropdown.tsx +++ b/packages/liveblocks-react-ui/src/components/internal/Dropdown.tsx @@ -55,6 +55,7 @@ export function Dropdown({ )} sideOffset={FLOATING_ELEMENT_SIDE_OFFSET} collisionPadding={FLOATING_ELEMENT_COLLISION_PADDING} + updatePositionStrategy="always" {...props} > {content} diff --git a/packages/liveblocks-react-ui/src/components/internal/EmojiPicker.tsx b/packages/liveblocks-react-ui/src/components/internal/EmojiPicker.tsx index 3b60cc2a1a..531950e211 100644 --- a/packages/liveblocks-react-ui/src/components/internal/EmojiPicker.tsx +++ b/packages/liveblocks-react-ui/src/components/internal/EmojiPicker.tsx @@ -104,6 +104,7 @@ export const EmojiPicker = forwardRef( ( )} side="top" align="center" + updatePositionStrategy="always" sideOffset={FLOATING_ELEMENT_SIDE_OFFSET} collisionPadding={FLOATING_ELEMENT_COLLISION_PADDING} {...props} diff --git a/packages/liveblocks-react-ui/src/icon.ts b/packages/liveblocks-react-ui/src/icon.ts index 72640b7a60..055b984ec7 100644 --- a/packages/liveblocks-react-ui/src/icon.ts +++ b/packages/liveblocks-react-ui/src/icon.ts @@ -34,6 +34,7 @@ export { ListOrderedIcon as ListOrdered, ListUnorderedIcon as ListUnordered, MentionIcon as Mention, + PlusIcon as Plus, QuestionMarkIcon as QuestionMark, RedoIcon as Redo, RetryIcon as Retry, diff --git a/packages/liveblocks-react-ui/src/icons/Plus.tsx b/packages/liveblocks-react-ui/src/icons/Plus.tsx new file mode 100644 index 0000000000..51d0ea5d0b --- /dev/null +++ b/packages/liveblocks-react-ui/src/icons/Plus.tsx @@ -0,0 +1,11 @@ +import type { ComponentProps } from "react"; + +import { Icon } from "../components/internal/Icon"; + +export function PlusIcon(props: ComponentProps<"svg">) { + return ( + + + + ); +} diff --git a/packages/liveblocks-react-ui/src/icons/index.ts b/packages/liveblocks-react-ui/src/icons/index.ts index 5b1a23cb37..8c7a7b096f 100644 --- a/packages/liveblocks-react-ui/src/icons/index.ts +++ b/packages/liveblocks-react-ui/src/icons/index.ts @@ -34,6 +34,7 @@ export { ListOrderedIcon } from "./ListOrdered"; export { ListUnorderedIcon } from "./ListUnordered"; export { MentionIcon } from "./Mention"; export { MinusCircleIcon } from "./MinusCircle"; +export { PlusIcon } from "./Plus"; export { QuestionMarkIcon } from "./QuestionMark"; export { RedoIcon } from "./Redo"; export { RestoreIcon } from "./Restore"; diff --git a/packages/liveblocks-react-ui/src/styles/index.css b/packages/liveblocks-react-ui/src/styles/index.css index a01037d4db..eb2fedb667 100644 --- a/packages/liveblocks-react-ui/src/styles/index.css +++ b/packages/liveblocks-react-ui/src/styles/index.css @@ -1056,6 +1056,8 @@ font-weight: 500; font-size: 35cqi; white-space: nowrap; + pointer-events: none; + user-select: none; /** * Progressive enhancement: Only show the fallback when container queries are supported @@ -1171,6 +1173,7 @@ position: relative; background: var(--lb-dynamic-background); color: var(--lb-foreground); + outline: none; transition-property: background; } @@ -1737,6 +1740,10 @@ border-end-start-radius: 2px; transform: translate(0, -100%); } + + :where(.lb-icon) { + color: var(--lb-foreground-moderate); + } } .lb-comment-pin-avatar { @@ -1750,8 +1757,8 @@ .lb-avatar-stack { --lb-avatar-stack-size: 24px; - --lb-avatar-stack-overlap: calc(0.25 * var(--lb-avatar-stack-size)); --lb-avatar-stack-gap: 2px; + --lb-avatar-stack-overlap: calc(0.25 * var(--lb-avatar-stack-size)); display: flex; flex-direction: row; @@ -1761,7 +1768,7 @@ .lb-avatar-stack-avatar { position: relative; z-index: calc( - var(--lb-avatar-stack-count) - var(--lb-avatar-stack-avatar-index) + var(--lb-avatar-stack-count) - 1 - var(--lb-avatar-stack-avatar-index) ); inline-size: var(--lb-avatar-stack-size); block-size: var(--lb-avatar-stack-size); diff --git a/packages/liveblocks-react-ui/src/utils/px.ts b/packages/liveblocks-react-ui/src/utils/px.ts new file mode 100644 index 0000000000..31b020500e --- /dev/null +++ b/packages/liveblocks-react-ui/src/utils/px.ts @@ -0,0 +1,11 @@ +export function px(value: number | string | undefined): string | undefined { + if (value === undefined) { + return undefined; + } + + if (typeof value === "number") { + return `${value}px`; + } + + return value; +} diff --git a/packages/liveblocks-react/package.json b/packages/liveblocks-react/package.json index 6df9037796..aebe9cfe39 100644 --- a/packages/liveblocks-react/package.json +++ b/packages/liveblocks-react/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/react", - "version": "3.15.0", + "version": "3.15.1", "description": "A set of React hooks and providers to use Liveblocks declaratively. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.", "license": "Apache-2.0", "author": "Liveblocks Inc.", @@ -63,8 +63,8 @@ "showdeps": "depcruise src --include-only '^src' --exclude='__tests__' --output-type dot | dot -T svg > /tmp/dependency-graph.svg && open /tmp/dependency-graph.svg" }, "dependencies": { - "@liveblocks/client": "3.15.0", - "@liveblocks/core": "3.15.0" + "@liveblocks/client": "3.15.1", + "@liveblocks/core": "3.15.1" }, "peerDependencies": { "@types/react": "*", diff --git a/packages/liveblocks-redux/package.json b/packages/liveblocks-redux/package.json index a4fc9c82d8..b9dafb5b93 100644 --- a/packages/liveblocks-redux/package.json +++ b/packages/liveblocks-redux/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/redux", - "version": "3.15.0", + "version": "3.15.1", "description": "A store enhancer to integrate Liveblocks into Redux stores. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.", "license": "Apache-2.0", "author": "Liveblocks Inc.", @@ -35,8 +35,8 @@ "test:watch": "vitest" }, "dependencies": { - "@liveblocks/client": "3.15.0", - "@liveblocks/core": "3.15.0" + "@liveblocks/client": "3.15.1", + "@liveblocks/core": "3.15.1" }, "peerDependencies": { "redux": "^4 || ^5" diff --git a/packages/liveblocks-server/package.json b/packages/liveblocks-server/package.json index 9a10b72edd..a1142eb948 100644 --- a/packages/liveblocks-server/package.json +++ b/packages/liveblocks-server/package.json @@ -58,7 +58,7 @@ "dependencies": { "@liveblocks/core": "3.14.0", "async-mutex": "^0.4.0", - "decoders": "^2.9.0-pre.4", + "decoders": "^2.9.0", "itertools": "^2.3.2", "js-base64": "^3.7.5", "nanoid": "^3", diff --git a/packages/liveblocks-yjs/package.json b/packages/liveblocks-yjs/package.json index a6186c8b16..3aba148061 100644 --- a/packages/liveblocks-yjs/package.json +++ b/packages/liveblocks-yjs/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/yjs", - "version": "3.15.0", + "version": "3.15.1", "description": "Integrate your existing or new Yjs documents with Liveblocks.", "license": "Apache-2.0", "author": "Liveblocks Inc.", @@ -35,8 +35,8 @@ "test:watch": "vitest" }, "dependencies": { - "@liveblocks/client": "3.15.0", - "@liveblocks/core": "3.15.0", + "@liveblocks/client": "3.15.1", + "@liveblocks/core": "3.15.1", "@noble/hashes": "^1.8.0", "js-base64": "^3.7.7", "y-indexeddb": "^9.0.12" diff --git a/packages/liveblocks-zustand/package.json b/packages/liveblocks-zustand/package.json index 348bb6e37b..8f0b9c63a3 100644 --- a/packages/liveblocks-zustand/package.json +++ b/packages/liveblocks-zustand/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/zustand", - "version": "3.15.0", + "version": "3.15.1", "description": "A middleware for Zustand to automatically synchronize your stores with Liveblocks. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.", "license": "Apache-2.0", "author": "Liveblocks Inc.", @@ -36,8 +36,8 @@ "test:watch": "vitest" }, "dependencies": { - "@liveblocks/client": "3.15.0", - "@liveblocks/core": "3.15.0" + "@liveblocks/client": "3.15.1", + "@liveblocks/core": "3.15.1" }, "peerDependencies": { "zustand": "^5.0.1" diff --git a/tools/liveblocks-cli/package.json b/tools/liveblocks-cli/package.json index 7b30ba5ac7..8455f3b7f2 100644 --- a/tools/liveblocks-cli/package.json +++ b/tools/liveblocks-cli/package.json @@ -42,7 +42,7 @@ "@liveblocks/core": "3.14.0", "@liveblocks/server": "1.0.15", "@liveblocks/zenrouter": "^1.0.17", - "decoders": "^2.9.0-pre.4", + "decoders": "^2.9.0", "js-base64": "^3.7.5", "yjs": "^13.6.10" }