@botiverse/raft-ui 0.10.5 → 0.10.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## [0.10.6](https://github.com/botiverse/raft-design/compare/v0.10.5...v0.10.6) (2026-10-21)
2
+
3
+ ### Added
4
+
5
+ - Comments beside a document, as in Google Docs, through `@botiverse/raft-ui/wip`: `CommentMargin`, `CommentCard` (with `CommentCardTrigger`, `CommentCardMeta`, `CommentCardQuote`), `CommentEntry` and `CommentMarker`. Cards sit level with their passages and keep the document's order; on a narrow viewer a `CommentMarker` beside each passage opens the discussion in a bottom sheet instead. `useCommentMarginFits(viewer, { minWidth })` picks between the two, `setCommentHighlights` with `COMMENT_HIGHLIGHT` / `COMMENT_HIGHLIGHT_ACTIVE` highlights the passages, and `resolveCommentMarginLayout` is the layout for callers that place cards themselves. New pattern: Comments. ([#614](https://github.com/botiverse/raft-design/pull/614))
6
+ - `AvatarGroup` reads `--avatar-group-surface` (default `layer-canvas`) for the ring between overlapping avatars, so a stack on another surface can match it. ([#614](https://github.com/botiverse/raft-design/pull/614))
7
+
1
8
  ## [0.10.5](https://github.com/botiverse/raft-design/compare/v0.10.4...v0.10.5) (2026-10-20)
2
9
 
3
10
  ### Added
package/dist/index.mjs CHANGED
@@ -5701,8 +5701,8 @@ const avatar = tv({
5701
5701
  group: [
5702
5702
  "group/avatar-group flex items-center",
5703
5703
  "stack-x-[-5px]",
5704
- "*:data-[slot=avatar]:shadow-[0_0_0_1.5px_var(--sticker-edge),0_0_0_3px_var(--layer-canvas)]",
5705
- "*:data-[slot=avatar-group-count]:shadow-[0_0_0_1.5px_var(--sticker-edge),0_0_0_3px_var(--layer-canvas)]"
5704
+ "*:data-[slot=avatar]:shadow-[0_0_0_1.5px_var(--sticker-edge),0_0_0_3px_var(--avatar-group-surface,var(--layer-canvas))]",
5705
+ "*:data-[slot=avatar-group-count]:shadow-[0_0_0_1.5px_var(--sticker-edge),0_0_0_3px_var(--avatar-group-surface,var(--layer-canvas))]"
5706
5706
  ]
5707
5707
  },
5708
5708
  variants: {
package/dist/styles.css CHANGED
@@ -442,6 +442,26 @@ body {
442
442
  position: relative;
443
443
  }
444
444
 
445
+ /* The passages comments are on, painted with the CSS Custom Highlight API so
446
+ the document's DOM is never changed (setCommentHighlights registers the
447
+ ranges; design/patterns/comments.md). Every commented passage gets the soft
448
+ yellow of a highlight and a 2px yellow underline; the passage of the open
449
+ discussion gets the full yellow with ink type, like a search hit, in both
450
+ modes. `--brand-400` is the `brand` role (what `bg-brand` paints):
451
+ `--color-brand` is an inline theme value and emits no variable. */
452
+ ::highlight(raft-comment) {
453
+ background-color: var(--brand-soft);
454
+ text-decoration-line: underline;
455
+ text-decoration-color: var(--brand-400);
456
+ text-decoration-thickness: 2px;
457
+ }
458
+
459
+ ::highlight(raft-comment-active) {
460
+ background-color: var(--brand-400);
461
+ color: var(--sticker-ink);
462
+ text-decoration-line: none;
463
+ }
464
+
445
465
  @utility image-transparency-bg {
446
466
  --checker-cell: light-dark(oklch(0.1813 0.00497 17.62 / 0.12), oklch(1 0 0 / 0.1));
447
467
  background-color: light-dark(oklch(1 0 0), oklch(0.23 0 0));
package/dist/wip.d.mts CHANGED
@@ -1,5 +1,7 @@
1
- import { O as ButtonProps, T as DialogTrigger, i as DialogClose, s as DialogContentProps, t as Dialog, w as DialogTitleProps } from "./index-CDHLWJtB.mjs";
1
+ import { M as StyledProps, O as ButtonProps, T as DialogTrigger, i as DialogClose, j as DataAttributes, s as DialogContentProps, t as Dialog, w as DialogTitleProps } from "./index-CDHLWJtB.mjs";
2
2
  import { CSSProperties, ComponentPropsWithRef, ReactNode } from "react";
3
+ import { Button } from "@base-ui/react/button";
4
+ import { useRender } from "@base-ui/react/use-render";
3
5
  import { Combobox } from "@base-ui/react/combobox";
4
6
  //#region src/components/combobox-select/combobox-select.types.d.ts
5
7
  type ComboboxSelectValue = string | number;
@@ -110,4 +112,170 @@ export declare function AnnouncementDialogFooter({ className, ...props }: Compon
110
112
  export declare function AnnouncementDialogAction({ className, ...props }: Omit<ButtonProps, "variant">): import("react").JSX.Element;
111
113
  export declare function AnnouncementDialogSecondaryAction({ className, ...props }: Omit<ButtonProps, "variant">): import("react").JSX.Element;
112
114
  //#endregion
113
- export { Dialog as AnnouncementDialog, DialogClose as AnnouncementDialogClose, DialogTrigger as AnnouncementDialogTrigger, type ComboboxSelectGroup, type ComboboxSelectMessages, type ComboboxSelectOption, type ComboboxSelectProps, type ComboboxSelectSelection, type ComboboxSelectValue };
115
+ //#region src/components/comment/comment-card.d.ts
116
+ type CommentCardProps = useRender.ComponentProps<"div"> & {
117
+ /** The discussion people are reading or replying to: an ink border, lifted. */
118
+ active?: boolean;
119
+ };
120
+ /**
121
+ * The card of one discussion: its comments, a line such as "2 replies", and
122
+ * the reply box when it is open. A `div`; which parts it holds (collapsed or
123
+ * open) is the caller's choice. To open it with a click, add
124
+ * `CommentCardTrigger`.
125
+ */
126
+ export declare function CommentCard({ className, render, active, ...props }: CommentCardProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
127
+ type CommentCardTriggerProps = useRender.ComponentProps<"button">;
128
+ /**
129
+ * A button laid over the whole card, for a collapsed card that opens when it
130
+ * is clicked or chosen with the keyboard. It has no text of its own: give it
131
+ * an `aria-label` ("Comment from Martin").
132
+ */
133
+ export declare function CommentCardTrigger({ className, render, ...props }: CommentCardTriggerProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
134
+ type CommentCardMetaProps = useRender.ComponentProps<"div">;
135
+ /**
136
+ * A short line about the discussion, in the caller's words: how many replies
137
+ * it has, that it is about the whole document, or that the passage it was on
138
+ * can no longer be found.
139
+ */
140
+ export declare function CommentCardMeta({ className, render, ...props }: CommentCardMetaProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
141
+ type CommentCardQuoteProps = useRender.ComponentProps<"span">;
142
+ /**
143
+ * The passage the discussion is about, in the caller's words (usually the
144
+ * quoted text). Hidden on screen, where the card's place beside the
145
+ * highlighted passage says it; give it an `id` and point the
146
+ * `aria-describedby` of `CommentCardTrigger` or `CommentMarker` at it, so a
147
+ * screen reader says which passage a card is about.
148
+ */
149
+ export declare function CommentCardQuote({ className, render, ...props }: CommentCardQuoteProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
150
+ //#endregion
151
+ //#region src/components/comment/comment-entry.d.ts
152
+ type CommentEntryProps = useRender.ComponentProps<"div">;
153
+ /** One comment in a discussion: its header, then its text. */
154
+ export declare function CommentEntry({ className, render, ...props }: CommentEntryProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
155
+ type CommentEntryHeaderProps = useRender.ComponentProps<"div">;
156
+ /** One line: the author's `Avatar` (`size="xs"`), `CommentEntryAuthor`, then `CommentEntryTime`. */
157
+ export declare function CommentEntryHeader({ className, render, ...props }: CommentEntryHeaderProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
158
+ type CommentEntryAuthorProps = useRender.ComponentProps<"span">;
159
+ /** Who wrote the comment, in bold. It truncates; the time after it doesn't. */
160
+ export declare function CommentEntryAuthor({ className, render, ...props }: CommentEntryAuthorProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
161
+ type CommentEntryTimeProps = useRender.ComponentProps<"time">;
162
+ /**
163
+ * When the comment was written, right after the author. A `<time>`: pass the
164
+ * moment in `dateTime` (ISO 8601).
165
+ */
166
+ export declare function CommentEntryTime({ className, render, ...props }: CommentEntryTimeProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
167
+ type CommentEntryBodyProps = useRender.ComponentProps<"div"> & {
168
+ /** Cuts the text to 2 lines, for a collapsed card. */
169
+ clamp?: boolean;
170
+ };
171
+ /** What the comment says. */
172
+ export declare function CommentEntryBody({ className, render, clamp, ...props }: CommentEntryBodyProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
173
+ //#endregion
174
+ //#region src/components/comment/comment-highlight.d.ts
175
+ /** The highlight of every passage that has comments (`::highlight(raft-comment)` in styles.css). */
176
+ export declare const COMMENT_HIGHLIGHT = "raft-comment";
177
+ /** The highlight of the passage of the open discussion (`::highlight(raft-comment-active)`). */
178
+ export declare const COMMENT_HIGHLIGHT_ACTIVE = "raft-comment-active";
179
+ /**
180
+ * Highlights the passages comments are on with the CSS Custom Highlight API,
181
+ * so the document's DOM is never changed: no `<mark>` is wrapped around its
182
+ * text. Register the ranges of every anchored comment under
183
+ * `COMMENT_HIGHLIGHT`, and the open discussion's under
184
+ * `COMMENT_HIGHLIGHT_ACTIVE`, which paints over it. Passing no ranges clears
185
+ * the name. Returns a function that clears it.
186
+ *
187
+ * Where the browser has no `CSS.highlights`, it does nothing: the passages are
188
+ * not highlighted, and the cards still say where they belong.
189
+ */
190
+ export declare function setCommentHighlights(name: string, ranges: Iterable<Range>): () => void;
191
+ //#endregion
192
+ //#region src/components/comment/comment-margin.d.ts
193
+ type CommentMarginProps = useRender.ComponentProps<"div"> & {
194
+ /** Space between two cards that would otherwise touch, in px. Default 8. */
195
+ gap?: number;
196
+ };
197
+ /**
198
+ * The column beside a document that holds its comment cards, each level with
199
+ * the passage it is about. It measures its `CommentMarginItem`s with one
200
+ * `ResizeObserver`, resolves their collisions with
201
+ * `resolveCommentMarginLayout`, and takes the height the lowest card needs.
202
+ * Place it beside the document so that its top is the top the items' `top`
203
+ * is measured from.
204
+ */
205
+ export declare function CommentMargin({ className, render, style, gap, ...props }: CommentMarginProps): import("react").JSX.Element;
206
+ type CommentMarginItemProps = useRender.ComponentProps<"div"> & {
207
+ /** Where the card's anchor is: its y, in px, from the top of the margin. */
208
+ top: number;
209
+ /** Pins this card to its anchor and moves the others around it. */
210
+ active?: boolean;
211
+ };
212
+ /**
213
+ * One card's place in a `CommentMargin`. It sits at `top`, or lower when the
214
+ * card above it is in the way; the active one stays at `top` and the others
215
+ * make room. Items with the same `top` keep the order they were first
216
+ * rendered in. The resolved top is `data-top`, applied as a `translateY`.
217
+ */
218
+ export declare function CommentMarginItem({ className, render, style, top, active, ...props }: CommentMarginItemProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
219
+ //#endregion
220
+ //#region src/components/comment/comment-margin-layout.d.ts
221
+ /** One card in the margin: where its anchor is and how tall the card is, in px. */
222
+ type CommentMarginLayoutItem = {
223
+ id: string;
224
+ /** The y of the card's anchor, measured from the top of the margin. */
225
+ top: number;
226
+ /** The card's rendered height. */
227
+ height: number;
228
+ };
229
+ type CommentMarginLayoutOptions = {
230
+ /** Space between two cards that would otherwise touch, in px. Default 8. */
231
+ gap?: number;
232
+ /** The card pinned to its anchor; the others make room around it. */
233
+ activeId?: string | null;
234
+ };
235
+ type CommentMarginLayout = {
236
+ /** The resolved top of each card, by id. */
237
+ tops: Record<string, number>;
238
+ /** The bottom of the lowest card: the height the margin needs. */
239
+ height: number;
240
+ };
241
+ /**
242
+ * Places the cards of a comment margin the way Google Docs does. Cards keep
243
+ * document order (by their anchor's top; equal tops keep the order given) and
244
+ * never overlap: each sits at its anchor, or `gap` below the card before it.
245
+ * The active card is pinned to its anchor instead, the cards above it are
246
+ * pushed up to make room and the cards below it pushed down. Nothing goes
247
+ * above 0: when the cards above the active one don't fit between 0 and its
248
+ * anchor, they stack from 0 and the active card follows them.
249
+ */
250
+ export declare function resolveCommentMarginLayout(items: readonly CommentMarginLayoutItem[], { gap, activeId }?: CommentMarginLayoutOptions): CommentMarginLayout;
251
+ //#endregion
252
+ //#region src/components/comment/comment-marker.d.ts
253
+ type CommentMarkerProps = StyledProps<Button.Props> & DataAttributes & {
254
+ /** The marker of the discussion that is open. */
255
+ active?: boolean;
256
+ };
257
+ /**
258
+ * Where there is no room for a margin: a small button beside a passage that
259
+ * has comments, with the authors' avatars (an `AvatarGroup` of up to 3
260
+ * `size="2xs"` avatars) and the count. It opens the discussion, in a bottom
261
+ * sheet. Give it an `aria-label` that starts with the count ("2 comments").
262
+ */
263
+ export declare function CommentMarker({ className, render, nativeButton, active, ...props }: CommentMarkerProps): import("react").JSX.Element;
264
+ //#endregion
265
+ //#region src/components/comment/use-comment-margin-fits.d.ts
266
+ type UseCommentMarginFitsOptions = {
267
+ /** The narrowest the viewer may be for the margin to show, in px. Default 784. */
268
+ minWidth?: number;
269
+ };
270
+ /**
271
+ * Whether a viewer has room for a comment margin beside its document: true
272
+ * only on a desktop (from 768px with a mouse or a trackpad, the query the
273
+ * desktop metrics use) and while `element`, the viewer that holds the
274
+ * document and the margin, is at least `minWidth` wide. Otherwise show
275
+ * `CommentMarker`s and open discussions in a bottom sheet. Cards and markers
276
+ * are different trees, so this is measured in JavaScript (a
277
+ * `ResizeObserver` on the element), not decided by a container query.
278
+ */
279
+ export declare function useCommentMarginFits(element: Element | null, { minWidth }?: UseCommentMarginFitsOptions): boolean;
280
+ //#endregion
281
+ export { Dialog as AnnouncementDialog, DialogClose as AnnouncementDialogClose, DialogTrigger as AnnouncementDialogTrigger, type ComboboxSelectGroup, type ComboboxSelectMessages, type ComboboxSelectOption, type ComboboxSelectProps, type ComboboxSelectSelection, type ComboboxSelectValue, type CommentCardMetaProps, type CommentCardProps, type CommentCardQuoteProps, type CommentCardTriggerProps, type CommentEntryAuthorProps, type CommentEntryBodyProps, type CommentEntryHeaderProps, type CommentEntryProps, type CommentEntryTimeProps, type CommentMarginItemProps, type CommentMarginLayout, type CommentMarginLayoutItem, type CommentMarginLayoutOptions, type CommentMarginProps, type CommentMarkerProps, type UseCommentMarginFitsOptions };
package/dist/wip.mjs CHANGED
@@ -1,7 +1,10 @@
1
- import { D as DialogTitle, I as useThemeScopeProps, L as useTheme, O as DialogTrigger, a as DrawerContent, c as DrawerHeader, d as DrawerTitle, f as DrawerTrigger, g as Dialog, i as DrawerClose, k as Button, n as DrawerActions, r as DrawerBody, s as DrawerFooter, t as Drawer, v as DialogClose, y as DialogContent, z as tv } from "./drawer-yZD16Ybl.mjs";
1
+ import { D as DialogTitle, I as useThemeScopeProps, L as useTheme, M as defaultNativeButton, O as DialogTrigger, a as DrawerContent, c as DrawerHeader, d as DrawerTitle, f as DrawerTrigger, g as Dialog, i as DrawerClose, k as Button$1, n as DrawerActions, r as DrawerBody, s as DrawerFooter, t as Drawer, v as DialogClose, y as DialogContent, z as tv } from "./drawer-yZD16Ybl.mjs";
2
2
  import { Check, ChevronDown, LoaderCircle, Plus, Search, X } from "lucide-react";
3
- import { useCallback, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
3
+ import { createContext, use, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
4
4
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
5
+ import { Button } from "@base-ui/react/button";
6
+ import { mergeProps } from "@base-ui/react/merge-props";
7
+ import { useRender } from "@base-ui/react/use-render";
5
8
  import { Combobox } from "@base-ui/react/combobox";
6
9
  import { Select } from "@base-ui/react/select";
7
10
  //#region src/lib/phone-media.ts
@@ -741,7 +744,7 @@ function ComboboxSelect({ options, multiple, value, defaultValue, onValueChange,
741
744
  })]
742
745
  }),
743
746
  multiple ? /* @__PURE__ */ jsx(DrawerFooter, { children: /* @__PURE__ */ jsx(DrawerActions, { children: /* @__PURE__ */ jsx(DrawerClose, {
744
- render: /* @__PURE__ */ jsx(Button, { size: "lg" }),
747
+ render: /* @__PURE__ */ jsx(Button$1, { size: "lg" }),
745
748
  children: strings.done
746
749
  }) }) }) : null
747
750
  ]
@@ -900,7 +903,7 @@ function AnnouncementDialogFooter({ className, ...props }) {
900
903
  }
901
904
  function AnnouncementDialogAction({ className, ...props }) {
902
905
  const { action } = announcement();
903
- return /* @__PURE__ */ jsx(Button, {
906
+ return /* @__PURE__ */ jsx(Button$1, {
904
907
  variant: "accent",
905
908
  "data-announcement-action": true,
906
909
  className: action({ className }),
@@ -909,11 +912,502 @@ function AnnouncementDialogAction({ className, ...props }) {
909
912
  }
910
913
  function AnnouncementDialogSecondaryAction({ className, ...props }) {
911
914
  const { secondary } = announcement();
912
- return /* @__PURE__ */ jsx(Button, {
915
+ return /* @__PURE__ */ jsx(Button$1, {
913
916
  variant: "ghost",
914
917
  className: secondary({ className }),
915
918
  ...props
916
919
  });
917
920
  }
918
921
  //#endregion
919
- export { Dialog as AnnouncementDialog, AnnouncementDialogAction, AnnouncementDialogBody, DialogClose as AnnouncementDialogClose, AnnouncementDialogContent, AnnouncementDialogFooter, AnnouncementDialogHeader, AnnouncementDialogLabel, AnnouncementDialogPicture, AnnouncementDialogSecondaryAction, AnnouncementDialogText, AnnouncementDialogTitle, DialogTrigger as AnnouncementDialogTrigger, ComboboxSelect };
922
+ //#region src/components/comment/comment.recipe.ts
923
+ const comment = tv({
924
+ slots: {
925
+ margin: ["relative min-w-0"],
926
+ marginItem: [
927
+ "absolute inset-x-0 top-0",
928
+ "data-animate:transition-transform data-animate:duration-150 data-animate:ease-out",
929
+ "motion-reduce:transition-none",
930
+ "data-active:z-10"
931
+ ],
932
+ card: [
933
+ "relative flex min-w-0 flex-col gap-2",
934
+ "rounded-container border border-line-hairline p-3",
935
+ "bg-layer-card text-foreground shadow-none outline-none",
936
+ "data-active:border-[1.5px] data-active:border-line-strong data-active:p-[11.5px] data-active:shadow-float"
937
+ ],
938
+ cardTrigger: [
939
+ "absolute -inset-px z-10 cursor-pointer",
940
+ "rounded-container border-0 bg-transparent p-0",
941
+ "hover:bg-fill-pressed active:bg-fill-pressed",
942
+ "outline-none focus-visible:shadow-focus"
943
+ ],
944
+ cardQuote: ["sr-only"],
945
+ cardMeta: [
946
+ "flex min-w-0 items-center gap-1",
947
+ "font-sans text-caption text-foreground-hint",
948
+ "[&_svg]:size-3.5 [&_svg]:shrink-0"
949
+ ],
950
+ entry: ["flex min-w-0 flex-col gap-1", "[[data-slot=comment-entry]+&]:mt-1"],
951
+ entryHeader: ["flex min-w-0 items-center gap-1.5", "*:data-[slot=avatar]:me-0.5"],
952
+ entryAuthor: ["min-w-0 truncate", "text-subhead font-bold text-foreground"],
953
+ entryTime: ["shrink-0 whitespace-nowrap", "font-mono text-mono-meta tabular-nums text-foreground-hint"],
954
+ entryBody: [
955
+ "wrap-break-word select-text",
956
+ "text-callout text-foreground",
957
+ "selection:bg-brand-soft"
958
+ ],
959
+ marker: [
960
+ "relative inline-flex h-6 shrink-0 items-center gap-1 align-top whitespace-nowrap",
961
+ "rounded-chip border border-line-hairline py-0 ps-[3px] pe-1.5",
962
+ "font-mono text-mono-meta font-bold tabular-nums text-foreground",
963
+ "bg-layer-card [--avatar-group-surface:var(--layer-card)]",
964
+ "cursor-pointer hover:press-overlay active:press-overlay",
965
+ "data-active:border-[1.5px] data-active:border-line-strong data-active:bg-brand-soft data-active:ps-[2.5px] data-active:pe-[5.5px]",
966
+ "data-popup-open:border-[1.5px] data-popup-open:border-line-strong data-popup-open:bg-brand-soft data-popup-open:ps-[2.5px] data-popup-open:pe-[5.5px]",
967
+ "data-active:[--avatar-group-surface:var(--brand-soft)] data-popup-open:[--avatar-group-surface:var(--brand-soft)]",
968
+ "outline-none focus-visible:shadow-focus",
969
+ "after:absolute after:-inset-x-1 after:-inset-y-2.5"
970
+ ]
971
+ },
972
+ variants: { clamp: { true: { entryBody: ["line-clamp-2"] } } }
973
+ });
974
+ //#endregion
975
+ //#region src/components/comment/comment-card.tsx
976
+ /**
977
+ * The card of one discussion: its comments, a line such as "2 replies", and
978
+ * the reply box when it is open. A `div`; which parts it holds (collapsed or
979
+ * open) is the caller's choice. To open it with a click, add
980
+ * `CommentCardTrigger`.
981
+ */
982
+ function CommentCard({ className, render, active = false, ...props }) {
983
+ const { card } = comment();
984
+ const defaultProps = {
985
+ "data-slot": "comment-card",
986
+ "data-active": active ? "" : void 0,
987
+ "aria-current": active ? "true" : void 0,
988
+ className: card({ className })
989
+ };
990
+ return useRender({
991
+ defaultTagName: "div",
992
+ render,
993
+ props: mergeProps(defaultProps, props)
994
+ });
995
+ }
996
+ /**
997
+ * A button laid over the whole card, for a collapsed card that opens when it
998
+ * is clicked or chosen with the keyboard. It has no text of its own: give it
999
+ * an `aria-label` ("Comment from Martin").
1000
+ */
1001
+ function CommentCardTrigger({ className, render, ...props }) {
1002
+ const { cardTrigger } = comment();
1003
+ const defaultProps = {
1004
+ "data-slot": "comment-card-trigger",
1005
+ type: "button",
1006
+ className: cardTrigger({ className })
1007
+ };
1008
+ return useRender({
1009
+ defaultTagName: "button",
1010
+ render,
1011
+ props: mergeProps(defaultProps, props)
1012
+ });
1013
+ }
1014
+ /**
1015
+ * A short line about the discussion, in the caller's words: how many replies
1016
+ * it has, that it is about the whole document, or that the passage it was on
1017
+ * can no longer be found.
1018
+ */
1019
+ function CommentCardMeta({ className, render, ...props }) {
1020
+ const { cardMeta } = comment();
1021
+ const defaultProps = {
1022
+ "data-slot": "comment-card-meta",
1023
+ className: cardMeta({ className })
1024
+ };
1025
+ return useRender({
1026
+ defaultTagName: "div",
1027
+ render,
1028
+ props: mergeProps(defaultProps, props)
1029
+ });
1030
+ }
1031
+ /**
1032
+ * The passage the discussion is about, in the caller's words (usually the
1033
+ * quoted text). Hidden on screen, where the card's place beside the
1034
+ * highlighted passage says it; give it an `id` and point the
1035
+ * `aria-describedby` of `CommentCardTrigger` or `CommentMarker` at it, so a
1036
+ * screen reader says which passage a card is about.
1037
+ */
1038
+ function CommentCardQuote({ className, render, ...props }) {
1039
+ const { cardQuote } = comment();
1040
+ const defaultProps = {
1041
+ "data-slot": "comment-card-quote",
1042
+ className: cardQuote({ className })
1043
+ };
1044
+ return useRender({
1045
+ defaultTagName: "span",
1046
+ render,
1047
+ props: mergeProps(defaultProps, props)
1048
+ });
1049
+ }
1050
+ //#endregion
1051
+ //#region src/components/comment/comment-entry.tsx
1052
+ /** One comment in a discussion: its header, then its text. */
1053
+ function CommentEntry({ className, render, ...props }) {
1054
+ const { entry } = comment();
1055
+ const defaultProps = {
1056
+ "data-slot": "comment-entry",
1057
+ className: entry({ className })
1058
+ };
1059
+ return useRender({
1060
+ defaultTagName: "div",
1061
+ render,
1062
+ props: mergeProps(defaultProps, props)
1063
+ });
1064
+ }
1065
+ /** One line: the author's `Avatar` (`size="xs"`), `CommentEntryAuthor`, then `CommentEntryTime`. */
1066
+ function CommentEntryHeader({ className, render, ...props }) {
1067
+ const { entryHeader } = comment();
1068
+ const defaultProps = {
1069
+ "data-slot": "comment-entry-header",
1070
+ className: entryHeader({ className })
1071
+ };
1072
+ return useRender({
1073
+ defaultTagName: "div",
1074
+ render,
1075
+ props: mergeProps(defaultProps, props)
1076
+ });
1077
+ }
1078
+ /** Who wrote the comment, in bold. It truncates; the time after it doesn't. */
1079
+ function CommentEntryAuthor({ className, render, ...props }) {
1080
+ const { entryAuthor } = comment();
1081
+ const defaultProps = {
1082
+ "data-slot": "comment-entry-author",
1083
+ className: entryAuthor({ className })
1084
+ };
1085
+ return useRender({
1086
+ defaultTagName: "span",
1087
+ render,
1088
+ props: mergeProps(defaultProps, props)
1089
+ });
1090
+ }
1091
+ /**
1092
+ * When the comment was written, right after the author. A `<time>`: pass the
1093
+ * moment in `dateTime` (ISO 8601).
1094
+ */
1095
+ function CommentEntryTime({ className, render, ...props }) {
1096
+ const { entryTime } = comment();
1097
+ const defaultProps = {
1098
+ "data-slot": "comment-entry-time",
1099
+ className: entryTime({ className })
1100
+ };
1101
+ return useRender({
1102
+ defaultTagName: "time",
1103
+ render,
1104
+ props: mergeProps(defaultProps, props)
1105
+ });
1106
+ }
1107
+ /** What the comment says. */
1108
+ function CommentEntryBody({ className, render, clamp = false, ...props }) {
1109
+ const { entryBody } = comment({ clamp });
1110
+ const defaultProps = {
1111
+ "data-slot": "comment-entry-body",
1112
+ "data-clamp": clamp ? "" : void 0,
1113
+ className: entryBody({ className })
1114
+ };
1115
+ return useRender({
1116
+ defaultTagName: "div",
1117
+ render,
1118
+ props: mergeProps(defaultProps, props)
1119
+ });
1120
+ }
1121
+ //#endregion
1122
+ //#region src/components/comment/comment-highlight.ts
1123
+ /** The highlight of every passage that has comments (`::highlight(raft-comment)` in styles.css). */
1124
+ const COMMENT_HIGHLIGHT = "raft-comment";
1125
+ /** The highlight of the passage of the open discussion (`::highlight(raft-comment-active)`). */
1126
+ const COMMENT_HIGHLIGHT_ACTIVE = "raft-comment-active";
1127
+ /**
1128
+ * Highlights the passages comments are on with the CSS Custom Highlight API,
1129
+ * so the document's DOM is never changed: no `<mark>` is wrapped around its
1130
+ * text. Register the ranges of every anchored comment under
1131
+ * `COMMENT_HIGHLIGHT`, and the open discussion's under
1132
+ * `COMMENT_HIGHLIGHT_ACTIVE`, which paints over it. Passing no ranges clears
1133
+ * the name. Returns a function that clears it.
1134
+ *
1135
+ * Where the browser has no `CSS.highlights`, it does nothing: the passages are
1136
+ * not highlighted, and the cards still say where they belong.
1137
+ */
1138
+ function setCommentHighlights(name, ranges) {
1139
+ const registry = typeof CSS === "undefined" ? void 0 : CSS.highlights;
1140
+ if (!registry || typeof Highlight === "undefined") return () => {};
1141
+ const list = [...ranges];
1142
+ if (list.length === 0) {
1143
+ registry.delete(name);
1144
+ return () => {};
1145
+ }
1146
+ const highlight = new Highlight(...list);
1147
+ if (name === "raft-comment-active") highlight.priority = 1;
1148
+ registry.set(name, highlight);
1149
+ return () => {
1150
+ if (registry.get(name) === highlight) registry.delete(name);
1151
+ };
1152
+ }
1153
+ //#endregion
1154
+ //#region src/components/comment/comment-margin-layout.ts
1155
+ /**
1156
+ * Places the cards of a comment margin the way Google Docs does. Cards keep
1157
+ * document order (by their anchor's top; equal tops keep the order given) and
1158
+ * never overlap: each sits at its anchor, or `gap` below the card before it.
1159
+ * The active card is pinned to its anchor instead, the cards above it are
1160
+ * pushed up to make room and the cards below it pushed down. Nothing goes
1161
+ * above 0: when the cards above the active one don't fit between 0 and its
1162
+ * anchor, they stack from 0 and the active card follows them.
1163
+ */
1164
+ function resolveCommentMarginLayout(items, { gap = 8, activeId = null } = {}) {
1165
+ const order = items.map((item, index) => ({
1166
+ item,
1167
+ index
1168
+ })).sort((a, b) => a.item.top - b.item.top || a.index - b.index).map(({ item }) => item);
1169
+ const tops = order.map((item) => item.top);
1170
+ const stackDown = (from, floor) => {
1171
+ let next = floor;
1172
+ for (let i = from; i < order.length; i += 1) {
1173
+ tops[i] = Math.max(order[i].top, next);
1174
+ next = tops[i] + order[i].height + gap;
1175
+ }
1176
+ };
1177
+ const pinned = activeId == null ? -1 : order.findIndex((item) => item.id === activeId);
1178
+ if (pinned < 0) stackDown(0, 0);
1179
+ else {
1180
+ tops[pinned] = Math.max(order[pinned].top, 0);
1181
+ for (let i = pinned - 1; i >= 0; i -= 1) tops[i] = Math.min(order[i].top, tops[i + 1] - gap - order[i].height);
1182
+ if (pinned > 0 && tops[0] < 0) stackDown(0, 0);
1183
+ else stackDown(pinned + 1, tops[pinned] + order[pinned].height + gap);
1184
+ }
1185
+ const result = {};
1186
+ let height = 0;
1187
+ order.forEach((item, i) => {
1188
+ result[item.id] = tops[i];
1189
+ height = Math.max(height, tops[i] + item.height);
1190
+ });
1191
+ return {
1192
+ tops: result,
1193
+ height
1194
+ };
1195
+ }
1196
+ //#endregion
1197
+ //#region src/components/comment/comment-margin.tsx
1198
+ const CommentMarginContext = createContext(null);
1199
+ function useCommentMargin() {
1200
+ const context = use(CommentMarginContext);
1201
+ if (!context) throw new Error("CommentMarginItem must be used within CommentMargin.");
1202
+ return context;
1203
+ }
1204
+ const heightOf = (element) => element.getBoundingClientRect().height;
1205
+ /**
1206
+ * The column beside a document that holds its comment cards, each level with
1207
+ * the passage it is about. It measures its `CommentMarginItem`s with one
1208
+ * `ResizeObserver`, resolves their collisions with
1209
+ * `resolveCommentMarginLayout`, and takes the height the lowest card needs.
1210
+ * Place it beside the document so that its top is the top the items' `top`
1211
+ * is measured from.
1212
+ */
1213
+ function CommentMargin({ className, render, style, gap = 8, ...props }) {
1214
+ const { margin } = comment();
1215
+ const [entries, setEntries] = useState(() => /* @__PURE__ */ new Map());
1216
+ const [animate, setAnimate] = useState(false);
1217
+ const observer = useRef(null);
1218
+ const observed = useRef(/* @__PURE__ */ new Map());
1219
+ const patch = useCallback((id, next) => {
1220
+ setEntries((current) => {
1221
+ const previous = current.get(id);
1222
+ if (next === null) {
1223
+ if (!previous) return current;
1224
+ const without = new Map(current);
1225
+ without.delete(id);
1226
+ return without;
1227
+ }
1228
+ if (!previous && next.top === void 0) return current;
1229
+ const merged = {
1230
+ top: 0,
1231
+ height: 0,
1232
+ active: false,
1233
+ ...previous,
1234
+ ...next
1235
+ };
1236
+ if (previous && previous.top === merged.top && previous.height === merged.height && previous.active === merged.active) return current;
1237
+ const updated = new Map(current);
1238
+ updated.set(id, merged);
1239
+ return updated;
1240
+ });
1241
+ }, []);
1242
+ const place = useCallback((id, placement) => patch(id, placement), [patch]);
1243
+ const observe = useCallback((id, element) => {
1244
+ observer.current ??= new ResizeObserver((records) => {
1245
+ for (const record of records) {
1246
+ const target = observed.current.get(record.target);
1247
+ if (target) patch(target, { height: heightOf(record.target) });
1248
+ }
1249
+ });
1250
+ observed.current.set(element, id);
1251
+ patch(id, { height: heightOf(element) });
1252
+ observer.current.observe(element);
1253
+ return () => {
1254
+ observer.current?.unobserve(element);
1255
+ observed.current.delete(element);
1256
+ };
1257
+ }, [patch]);
1258
+ useEffect(() => () => observer.current?.disconnect(), []);
1259
+ const layout = useMemo(() => {
1260
+ const items = [...entries].map(([id, { top, height }]) => ({
1261
+ id,
1262
+ top,
1263
+ height
1264
+ }));
1265
+ const activeId = [...entries].find(([, entry]) => entry.active)?.[0];
1266
+ return resolveCommentMarginLayout(items, {
1267
+ gap,
1268
+ activeId
1269
+ });
1270
+ }, [entries, gap]);
1271
+ const measured = entries.size > 0;
1272
+ useEffect(() => {
1273
+ if (!measured) return;
1274
+ const frame = requestAnimationFrame(() => setAnimate(true));
1275
+ return () => cancelAnimationFrame(frame);
1276
+ }, [measured]);
1277
+ const context = useMemo(() => ({
1278
+ tops: layout.tops,
1279
+ animate,
1280
+ place,
1281
+ observe
1282
+ }), [
1283
+ layout.tops,
1284
+ animate,
1285
+ place,
1286
+ observe
1287
+ ]);
1288
+ const defaultProps = {
1289
+ "data-slot": "comment-margin",
1290
+ className: margin({ className }),
1291
+ style: {
1292
+ minHeight: layout.height,
1293
+ ...style
1294
+ }
1295
+ };
1296
+ const element = useRender({
1297
+ defaultTagName: "div",
1298
+ render,
1299
+ props: mergeProps(defaultProps, props)
1300
+ });
1301
+ return /* @__PURE__ */ jsx(CommentMarginContext, {
1302
+ value: context,
1303
+ children: element
1304
+ });
1305
+ }
1306
+ /**
1307
+ * One card's place in a `CommentMargin`. It sits at `top`, or lower when the
1308
+ * card above it is in the way; the active one stays at `top` and the others
1309
+ * make room. Items with the same `top` keep the order they were first
1310
+ * rendered in. The resolved top is `data-top`, applied as a `translateY`.
1311
+ */
1312
+ function CommentMarginItem({ className, render, style, top, active = false, ...props }) {
1313
+ const { marginItem } = comment();
1314
+ const { tops, animate, place, observe } = useCommentMargin();
1315
+ const id = useId();
1316
+ const [element, setElement] = useState(null);
1317
+ useLayoutEffect(() => {
1318
+ place(id, {
1319
+ top,
1320
+ active
1321
+ });
1322
+ }, [
1323
+ place,
1324
+ id,
1325
+ top,
1326
+ active
1327
+ ]);
1328
+ useLayoutEffect(() => element ? observe(id, element) : void 0, [
1329
+ observe,
1330
+ id,
1331
+ element
1332
+ ]);
1333
+ useLayoutEffect(() => () => place(id, null), [place, id]);
1334
+ const resolved = tops[id] ?? Math.max(top, 0);
1335
+ const defaultProps = {
1336
+ "data-slot": "comment-margin-item",
1337
+ "data-top": String(resolved),
1338
+ "data-active": active ? "" : void 0,
1339
+ "data-animate": animate ? "" : void 0,
1340
+ className: marginItem({ className }),
1341
+ style: {
1342
+ ...style,
1343
+ transform: `translateY(${resolved}px)`
1344
+ }
1345
+ };
1346
+ return useRender({
1347
+ defaultTagName: "div",
1348
+ render,
1349
+ ref: setElement,
1350
+ props: mergeProps(defaultProps, props)
1351
+ });
1352
+ }
1353
+ //#endregion
1354
+ //#region src/components/comment/comment-marker.tsx
1355
+ /**
1356
+ * Where there is no room for a margin: a small button beside a passage that
1357
+ * has comments, with the authors' avatars (an `AvatarGroup` of up to 3
1358
+ * `size="2xs"` avatars) and the count. It opens the discussion, in a bottom
1359
+ * sheet. Give it an `aria-label` that starts with the count ("2 comments").
1360
+ */
1361
+ function CommentMarker({ className, render, nativeButton, active = false, ...props }) {
1362
+ const { marker } = comment();
1363
+ return /* @__PURE__ */ jsx(Button, {
1364
+ ...props,
1365
+ render,
1366
+ nativeButton: nativeButton ?? defaultNativeButton(render),
1367
+ "data-slot": "comment-marker",
1368
+ "data-active": active ? "" : void 0,
1369
+ className: marker({ className })
1370
+ });
1371
+ }
1372
+ //#endregion
1373
+ //#region src/lib/desktop-media.ts
1374
+ /**
1375
+ * Where the desktop layout and its metrics begin: from 768px, with a mouse or
1376
+ * a trackpad (design/foundations/spacing.md#hit-targets). The same query as
1377
+ * the desktop blocks in styles.css and foundation.css, for code that has to
1378
+ * decide in JavaScript what CSS decides with `md:pointer-fine:`.
1379
+ */
1380
+ const DESKTOP_MEDIA_QUERY = "(width >= 48rem) and (pointer: fine)";
1381
+ //#endregion
1382
+ //#region src/components/comment/use-comment-margin-fits.ts
1383
+ function subscribeDesktop(onChange) {
1384
+ if (typeof window === "undefined") return () => void 0;
1385
+ const media = window.matchMedia(DESKTOP_MEDIA_QUERY);
1386
+ media.addEventListener("change", onChange);
1387
+ return () => media.removeEventListener("change", onChange);
1388
+ }
1389
+ const getDesktop = () => typeof window !== "undefined" && window.matchMedia("(width >= 48rem) and (pointer: fine)").matches;
1390
+ /**
1391
+ * Whether a viewer has room for a comment margin beside its document: true
1392
+ * only on a desktop (from 768px with a mouse or a trackpad, the query the
1393
+ * desktop metrics use) and while `element`, the viewer that holds the
1394
+ * document and the margin, is at least `minWidth` wide. Otherwise show
1395
+ * `CommentMarker`s and open discussions in a bottom sheet. Cards and markers
1396
+ * are different trees, so this is measured in JavaScript (a
1397
+ * `ResizeObserver` on the element), not decided by a container query.
1398
+ */
1399
+ function useCommentMarginFits(element, { minWidth = 784 } = {}) {
1400
+ const desktop = useSyncExternalStore(subscribeDesktop, getDesktop, () => false);
1401
+ const [width, setWidth] = useState(0);
1402
+ useLayoutEffect(() => {
1403
+ if (!element) return;
1404
+ const measure = () => setWidth(element.getBoundingClientRect().width);
1405
+ measure();
1406
+ const observer = new ResizeObserver(measure);
1407
+ observer.observe(element);
1408
+ return () => observer.disconnect();
1409
+ }, [element]);
1410
+ return desktop && element !== null && width >= minWidth;
1411
+ }
1412
+ //#endregion
1413
+ export { Dialog as AnnouncementDialog, AnnouncementDialogAction, AnnouncementDialogBody, DialogClose as AnnouncementDialogClose, AnnouncementDialogContent, AnnouncementDialogFooter, AnnouncementDialogHeader, AnnouncementDialogLabel, AnnouncementDialogPicture, AnnouncementDialogSecondaryAction, AnnouncementDialogText, AnnouncementDialogTitle, DialogTrigger as AnnouncementDialogTrigger, COMMENT_HIGHLIGHT, COMMENT_HIGHLIGHT_ACTIVE, ComboboxSelect, CommentCard, CommentCardMeta, CommentCardQuote, CommentCardTrigger, CommentEntry, CommentEntryAuthor, CommentEntryBody, CommentEntryHeader, CommentEntryTime, CommentMargin, CommentMarginItem, CommentMarker, resolveCommentMarginLayout, setCommentHighlights, useCommentMarginFits };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botiverse/raft-ui",
3
- "version": "0.10.5",
3
+ "version": "0.10.6",
4
4
  "description": "React components and CSS foundations for the Raft Design System.",
5
5
  "keywords": [
6
6
  "agent-skills",