@botiverse/raft-ui 0.20.0 → 0.21.0

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/dist/wip.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { $n as useTheme, Fn as DialogContent, Gn as Button$1, Jn as defaultNativeButton, Mn as Dialog, Pn as DialogClose, Qn as useThemeScopeProps, Un as DialogTitle, Vt as useOverflowTitle, Wn as DialogTrigger, a as MessageListJumpPillDismiss, dt as MessageItemDelegatedAvatar, ft as MessageItemDelegatedBy, h as MessageQuotedPreviewAuthorDelegatedBy, i as MessageListJumpPill, mt as MessageItemDelegatedByName, o as MessageListJumpPillGroup, pt as MessageItemDelegatedByButton, r as MessageListJumpControls, t as useHoldThenMoveTouchSensor, tr as tv } from "./touch-drag-BA_Fxs2Z.mjs";
1
+ import { An as Tooltip, Bn as DialogClose, Fn as TextLink, Gt as readNow, Ht as measureContent, Pn as TooltipTrigger, Rn as Dialog, Ut as measureElement, Vn as DialogContent, Vt as useOverflowTitle, Wt as observeTruncation, Xn as DialogTrigger, Yn as DialogTitle, Zn as Button$1, a as MessageListJumpPillDismiss, ar as useTheme, dt as MessageItemDelegatedAvatar, er as defaultNativeButton, ft as MessageItemDelegatedBy, h as MessageQuotedPreviewAuthorDelegatedBy, i as MessageListJumpPill, ir as useThemeScopeProps, jn as TooltipContent, mt as MessageItemDelegatedByName, o as MessageListJumpPillGroup, pt as MessageItemDelegatedByButton, r as MessageListJumpControls, sr as tv, t as useHoldThenMoveTouchSensor } from "./touch-drag-B7uLTWXe.mjs";
2
2
  import { Check, ChevronDown, LoaderCircle, Plus, X } from "lucide-react";
3
- import { createContext, use, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
3
+ import { createContext, use, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
4
4
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
5
5
  import { Button } from "@base-ui/react/button";
6
6
  import { mergeProps } from "@base-ui/react/merge-props";
@@ -1123,4 +1123,348 @@ function useCommentMarginFits(element, { minWidth = 784 } = {}) {
1123
1123
  return desktop && element !== null && width >= minWidth;
1124
1124
  }
1125
1125
  //#endregion
1126
- 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, MessageItemDelegatedAvatar, MessageItemDelegatedBy, MessageItemDelegatedByButton, MessageItemDelegatedByName, MessageListJumpControls, MessageListJumpPill, MessageListJumpPillDismiss, MessageListJumpPillGroup, MessageQuotedPreviewAuthorDelegatedBy, resolveCommentMarginLayout, setCommentHighlights, useCommentMarginFits, useHoldThenMoveTouchSensor };
1126
+ //#region src/components/ellipsis/ellipsis.tsx
1127
+ const ellipsis = tv({
1128
+ slots: {
1129
+ root: ["group/ellipsis min-w-0 max-w-full"],
1130
+ text: ["min-w-0"],
1131
+ content: [],
1132
+ spacer: ["float-end h-[calc(100%-1lh)] w-0", "data-hidden:hidden group-data-measuring/ellipsis:hidden"],
1133
+ after: ["data-hidden:hidden group-data-measuring/ellipsis:hidden"]
1134
+ },
1135
+ variants: {
1136
+ multiline: {
1137
+ true: {
1138
+ root: ["flex"],
1139
+ text: ["flex-1", "wrap-break-word"],
1140
+ after: ["float-end clear-end ps-1"]
1141
+ },
1142
+ false: {
1143
+ root: ["inline-flex items-baseline gap-x-1 align-bottom"],
1144
+ after: ["shrink-0"]
1145
+ }
1146
+ },
1147
+ expanded: {
1148
+ true: {},
1149
+ false: {}
1150
+ },
1151
+ clickable: {
1152
+ true: { text: ["cursor-pointer"] },
1153
+ false: {}
1154
+ }
1155
+ },
1156
+ compoundVariants: [
1157
+ {
1158
+ multiline: false,
1159
+ expanded: false,
1160
+ class: { text: ["truncate", "print:overflow-visible print:whitespace-normal"] }
1161
+ },
1162
+ {
1163
+ multiline: false,
1164
+ expanded: true,
1165
+ class: { text: ["wrap-anywhere whitespace-normal"] }
1166
+ },
1167
+ {
1168
+ multiline: true,
1169
+ expanded: false,
1170
+ class: { text: ["line-clamp-(--ellipsis-lines)", "print:line-clamp-none"] }
1171
+ },
1172
+ {
1173
+ multiline: true,
1174
+ expanded: true,
1175
+ class: { text: ["line-clamp-none"] }
1176
+ }
1177
+ ]
1178
+ });
1179
+ const EllipsisContext = createContext(null);
1180
+ const INTERACTIVE = "a[href], button, input, select, textarea, summary, [tabindex], [contenteditable]:not([contenteditable='false']), [role='button'], [role='link']";
1181
+ const TOUCH_MEDIA_QUERY = "(hover: none), (pointer: coarse)";
1182
+ function isTouchScreen() {
1183
+ return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia(TOUCH_MEDIA_QUERY).matches;
1184
+ }
1185
+ function contains(ref, target) {
1186
+ return target instanceof Node && ref.current?.contains(target) === true;
1187
+ }
1188
+ function hasSelection(within) {
1189
+ const selection = within.ownerDocument.defaultView?.getSelection();
1190
+ return !!selection && !selection.isCollapsed && selection.anchorNode != null && within.contains(selection.anchorNode);
1191
+ }
1192
+ function useExpanded(expandedProp, defaultExpanded, onExpandedChange) {
1193
+ const [expandedState, setExpandedState] = useState(defaultExpanded);
1194
+ const expanded = expandedProp ?? expandedState;
1195
+ return [expanded, useCallback(() => {
1196
+ const next = !expanded;
1197
+ if (expandedProp === void 0) setExpandedState(next);
1198
+ onExpandedChange?.(next);
1199
+ }, [
1200
+ expanded,
1201
+ expandedProp,
1202
+ onExpandedChange
1203
+ ])];
1204
+ }
1205
+ /**
1206
+ * Registers the text with the shared measurer (lib/truncation.ts) and keeps
1207
+ * its answer: whether the text, alone, is cut, and whether it has a control
1208
+ * inside. While it is expanded the last collapsed answer is kept, so the
1209
+ * text and its control stay what they were until it collapses and is
1210
+ * measured again. `layout` names what remounts or reclamps the text.
1211
+ */
1212
+ function useTruncation(expanded, layout, onNotTruncated) {
1213
+ const rootRef = useRef(null);
1214
+ const textRef = useRef(null);
1215
+ const contentRef = useRef(null);
1216
+ const afterRef = useRef(null);
1217
+ const setTextRef = useCallback((node) => {
1218
+ textRef.current = node;
1219
+ }, []);
1220
+ const [truncation, setTruncation] = useState(void 0);
1221
+ const [interactive, setInteractive] = useState(false);
1222
+ const expandedRef = useRef(expanded);
1223
+ const targetRef = useRef(null);
1224
+ const commit = useCallback((next) => {
1225
+ setInteractive(contentRef.current?.querySelector(INTERACTIVE) != null);
1226
+ if (expandedRef.current) return;
1227
+ setTruncation(next);
1228
+ if (next !== true) onNotTruncated();
1229
+ }, [onNotTruncated]);
1230
+ useLayoutEffect(() => {
1231
+ expandedRef.current = expanded;
1232
+ const root = rootRef.current;
1233
+ const text = textRef.current;
1234
+ const content = contentRef.current;
1235
+ if (!root || !text || !content) return void 0;
1236
+ const target = {
1237
+ observe: () => [text, afterRef.current],
1238
+ watch: () => content,
1239
+ isolate: () => {
1240
+ root.setAttribute("data-measuring", "");
1241
+ return () => root.removeAttribute("data-measuring");
1242
+ },
1243
+ read: () => measureContent(text, content),
1244
+ commit
1245
+ };
1246
+ targetRef.current = target;
1247
+ return observeTruncation(target);
1248
+ }, [
1249
+ commit,
1250
+ expanded,
1251
+ layout
1252
+ ]);
1253
+ return {
1254
+ rootRef,
1255
+ textRef,
1256
+ setTextRef,
1257
+ contentRef,
1258
+ afterRef,
1259
+ truncation,
1260
+ interactive,
1261
+ measureNow: useCallback(() => {
1262
+ const target = targetRef.current;
1263
+ if (!target) return void 0;
1264
+ const next = readNow(target);
1265
+ commit(next);
1266
+ return next;
1267
+ }, [commit])
1268
+ };
1269
+ }
1270
+ /** The tooltip's `onOpenChange`: it opens only on a text measured as cut, now. */
1271
+ function useOpenWhenTruncated(setOpen, expanded, afterRef, measureNow) {
1272
+ return useCallback((next, details) => {
1273
+ if (!next) {
1274
+ setOpen(false);
1275
+ return;
1276
+ }
1277
+ if (isTouchScreen() || expanded) return;
1278
+ if (contains(afterRef, details.event?.target)) return;
1279
+ if (measureNow() === true) setOpen(true);
1280
+ }, [
1281
+ afterRef,
1282
+ expanded,
1283
+ measureNow,
1284
+ setOpen
1285
+ ]);
1286
+ }
1287
+ /**
1288
+ * Makes the text the button that expands and collapses it, while
1289
+ * `clickable`. It stays one while expanded, `aria-expanded="true"`, so focus
1290
+ * stays on it and it collapses again the same way.
1291
+ */
1292
+ function useExpandOnClick(clickable, expanded, toggle, textRef) {
1293
+ const onClick = useCallback(() => {
1294
+ if (!clickable) return;
1295
+ if (textRef.current && hasSelection(textRef.current)) return;
1296
+ toggle();
1297
+ }, [
1298
+ clickable,
1299
+ textRef,
1300
+ toggle
1301
+ ]);
1302
+ const onKeyDown = useCallback((event) => {
1303
+ if (!clickable || event.target !== event.currentTarget) return;
1304
+ if (event.key === "Enter" || event.key === " ") {
1305
+ event.preventDefault();
1306
+ toggle();
1307
+ }
1308
+ }, [clickable, toggle]);
1309
+ if (!clickable) return {};
1310
+ return {
1311
+ role: "button",
1312
+ tabIndex: 0,
1313
+ "aria-expanded": expanded,
1314
+ onClick,
1315
+ onKeyDown
1316
+ };
1317
+ }
1318
+ function useRootStyle(style, lines) {
1319
+ return useMemo(() => lines > 1 ? {
1320
+ ...style,
1321
+ ["--ellipsis-lines"]: String(lines)
1322
+ } : style, [style, lines]);
1323
+ }
1324
+ function Ellipsis({ children, lines = 1, tooltip = true, after, expanded: expandedProp, defaultExpanded = false, onExpandedChange, expandOnClick = false, className, style, ...props }) {
1325
+ const textId = useId();
1326
+ const [open, setOpen] = useState(false);
1327
+ const closeTooltip = useCallback(() => setOpen(false), []);
1328
+ const [expanded, toggle] = useExpanded(expandedProp, defaultExpanded, onExpandedChange);
1329
+ const multiline = lines > 1;
1330
+ const hasTooltip = tooltip !== false && tooltip != null;
1331
+ const hasAfter = after != null;
1332
+ const measured = useTruncation(expanded, `${lines} ${hasAfter} ${hasTooltip}`, closeTooltip);
1333
+ const { rootRef, textRef, setTextRef, contentRef, afterRef, truncation } = measured;
1334
+ const handleOpenChange = useOpenWhenTruncated(setOpen, expanded, afterRef, measured.measureNow);
1335
+ const truncated = truncation === true && !expanded;
1336
+ const clickable = expandOnClick && !hasAfter && !measured.interactive && (truncation === true || expanded);
1337
+ const afterNode = typeof after === "function" ? after({
1338
+ truncated,
1339
+ expanded,
1340
+ toggle
1341
+ }) : after;
1342
+ const afterHidden = !truncated && !expanded;
1343
+ const slots = ellipsis({
1344
+ multiline,
1345
+ expanded,
1346
+ clickable
1347
+ });
1348
+ const context = useMemo(() => ({
1349
+ expanded,
1350
+ toggle,
1351
+ textId
1352
+ }), [
1353
+ expanded,
1354
+ toggle,
1355
+ textId
1356
+ ]);
1357
+ const rootStyle = useRootStyle(style, lines);
1358
+ const clickProps = useExpandOnClick(clickable, expanded, toggle, textRef);
1359
+ const afterElement = hasAfter ? /* @__PURE__ */ jsx("span", {
1360
+ ref: afterRef,
1361
+ "data-slot": "ellipsis-after",
1362
+ "data-hidden": afterHidden ? "" : void 0,
1363
+ inert: afterHidden,
1364
+ className: slots.after(),
1365
+ onPointerEnter: closeTooltip,
1366
+ children: afterNode
1367
+ }) : null;
1368
+ const floatingAfter = multiline && hasAfter;
1369
+ const textChildren = /* @__PURE__ */ jsxs(Fragment$1, { children: [
1370
+ floatingAfter && /* @__PURE__ */ jsx("span", {
1371
+ "aria-hidden": true,
1372
+ "data-slot": "ellipsis-spacer",
1373
+ "data-hidden": afterHidden ? "" : void 0,
1374
+ className: slots.spacer()
1375
+ }),
1376
+ floatingAfter && afterElement,
1377
+ /* @__PURE__ */ jsx("span", {
1378
+ ref: contentRef,
1379
+ "data-slot": "ellipsis-content",
1380
+ className: slots.content(),
1381
+ children
1382
+ })
1383
+ ] });
1384
+ const textProps = {
1385
+ id: textId,
1386
+ "data-slot": "ellipsis-text",
1387
+ "data-truncated": truncated ? "" : void 0,
1388
+ className: slots.text(),
1389
+ ...clickProps
1390
+ };
1391
+ const text = hasTooltip ? /* @__PURE__ */ jsxs(Tooltip, {
1392
+ open: open && truncated,
1393
+ onOpenChange: handleOpenChange,
1394
+ children: [/* @__PURE__ */ jsx(TooltipTrigger, {
1395
+ ref: setTextRef,
1396
+ render: /* @__PURE__ */ jsx("span", {}),
1397
+ ...textProps,
1398
+ className: slots.text({ class: "select-auto" }),
1399
+ children: textChildren
1400
+ }), /* @__PURE__ */ jsx(TooltipContent, { children: tooltip === true ? children : tooltip })]
1401
+ }) : /* @__PURE__ */ jsx("span", {
1402
+ ref: setTextRef,
1403
+ ...textProps,
1404
+ children: textChildren
1405
+ });
1406
+ return /* @__PURE__ */ jsx(EllipsisContext.Provider, {
1407
+ value: context,
1408
+ children: /* @__PURE__ */ jsxs("span", {
1409
+ ref: rootRef,
1410
+ "data-slot": "ellipsis",
1411
+ className: slots.root({ className }),
1412
+ style: rootStyle,
1413
+ ...props,
1414
+ children: [text, !multiline ? afterElement : null]
1415
+ })
1416
+ });
1417
+ }
1418
+ /**
1419
+ * The control that expands an Ellipsis and collapses it again, for its
1420
+ * `after`. A real button, with `aria-expanded` and `aria-controls` pointing
1421
+ * at the text. The label is the caller's: "Show More" / "Show Less".
1422
+ */
1423
+ function EllipsisToggle({ onClick, ...props }) {
1424
+ const context = useContext(EllipsisContext);
1425
+ if (!context) throw new Error("EllipsisToggle must be inside an Ellipsis `after`.");
1426
+ const { expanded, toggle, textId } = context;
1427
+ const handleClick = useCallback((event) => {
1428
+ onClick?.(event);
1429
+ if (!event.defaultPrevented) toggle();
1430
+ }, [onClick, toggle]);
1431
+ return /* @__PURE__ */ jsx(TextLink, {
1432
+ "aria-expanded": expanded,
1433
+ "aria-controls": textId,
1434
+ onClick: handleClick,
1435
+ ...props
1436
+ });
1437
+ }
1438
+ //#endregion
1439
+ //#region src/lib/use-truncated.ts
1440
+ /**
1441
+ * Whether the text in the element `ref` points at is cut off by its CSS
1442
+ * (`truncate`, `line-clamp`, a fixed size with `overflow: hidden`): `true`,
1443
+ * `false`, or `undefined` until it is measured or while it has no box.
1444
+ *
1445
+ * It measures fractional sizes with a 0.5px tolerance, past either inline
1446
+ * edge and below the bottom edge, so it needs no line count. It measures again
1447
+ * when the element changes size, when its text changes and when a web font
1448
+ * finishes loading, in one batched frame for every measured element on the
1449
+ * page (lib/truncation.ts), before that frame paints.
1450
+ *
1451
+ * The element is read when the component mounts: keep `ref` on one element
1452
+ * for the component's life, or give the component a `key` that changes with
1453
+ * the element.
1454
+ */
1455
+ function useTruncated(ref) {
1456
+ const [truncated, setTruncated] = useState(void 0);
1457
+ useLayoutEffect(() => {
1458
+ const element = ref.current;
1459
+ if (!element) return void 0;
1460
+ return observeTruncation({
1461
+ observe: () => [element],
1462
+ watch: () => element,
1463
+ read: () => measureElement(element),
1464
+ commit: setTruncated
1465
+ });
1466
+ }, [ref]);
1467
+ return truncated;
1468
+ }
1469
+ //#endregion
1470
+ 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, Ellipsis, EllipsisToggle, MessageItemDelegatedAvatar, MessageItemDelegatedBy, MessageItemDelegatedByButton, MessageItemDelegatedByName, MessageListJumpControls, MessageListJumpPill, MessageListJumpPillDismiss, MessageListJumpPillGroup, MessageQuotedPreviewAuthorDelegatedBy, resolveCommentMarginLayout, setCommentHighlights, useCommentMarginFits, useHoldThenMoveTouchSensor, useTruncated };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botiverse/raft-ui",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "description": "React components and CSS foundations for the Raft Design System.",
5
5
  "keywords": [
6
6
  "agent-skills",
@@ -62,6 +62,7 @@
62
62
  "tailwind-variants": "^3.3.1"
63
63
  },
64
64
  "devDependencies": {
65
+ "@babel/parser": "^7.29.9",
65
66
  "@tailwindcss/vite": "^4.3.3",
66
67
  "@testing-library/dom": "^10.4.2",
67
68
  "@testing-library/react": "^16.3.3",
@@ -82,9 +83,15 @@
82
83
  "vitest": "5.0.1"
83
84
  },
84
85
  "peerDependencies": {
86
+ "@babel/parser": "^7.29.9",
85
87
  "react": "^19",
86
88
  "react-dom": "^19"
87
89
  },
90
+ "peerDependenciesMeta": {
91
+ "@babel/parser": {
92
+ "optional": true
93
+ }
94
+ },
88
95
  "scripts": {
89
96
  "bench:virtual-list": "node bench/virtual-list/run.mjs",
90
97
  "build:tokens": "node scripts/build-tokens.mjs",