@noya-app/noya-designsystem 0.1.66 → 0.1.68

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.
Files changed (41) hide show
  1. package/.turbo/turbo-build.log +11 -11
  2. package/CHANGELOG.md +14 -0
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.mts +81 -5
  5. package/dist/index.d.ts +81 -5
  6. package/dist/index.js +1205 -857
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +1047 -698
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +4 -2
  11. package/src/components/Anchor.tsx +30 -0
  12. package/src/components/BaseToolbar.tsx +11 -8
  13. package/src/components/Chip.tsx +16 -2
  14. package/src/components/Collection.tsx +10 -0
  15. package/src/components/Combobox.tsx +30 -21
  16. package/src/components/ComboboxMenu.tsx +49 -18
  17. package/src/components/Dialog.tsx +7 -1
  18. package/src/components/EditableText.tsx +7 -3
  19. package/src/components/Grid.tsx +2 -0
  20. package/src/components/GridView.tsx +52 -9
  21. package/src/components/List.tsx +54 -9
  22. package/src/components/ListView.tsx +44 -0
  23. package/src/components/ScrollArea2.tsx +56 -0
  24. package/src/components/SearchCompletionMenu.tsx +6 -2
  25. package/src/components/TreeView.tsx +4 -0
  26. package/src/components/UserPointer.tsx +34 -7
  27. package/src/components/connected-users-menu/ConnectedUsersMenuLayout.tsx +1 -3
  28. package/src/components/listView/ListViewEditableRowTitle.tsx +23 -1
  29. package/src/components/listView/ListViewRoot.tsx +45 -13
  30. package/src/components/sorting/Sortable.tsx +22 -10
  31. package/src/components/workspace/WorkspaceLayout.tsx +62 -4
  32. package/src/components/workspace/WorkspaceSideContext.tsx +18 -0
  33. package/src/components/workspace/renderPanelChildren.tsx +2 -2
  34. package/src/components/workspace/types.ts +8 -1
  35. package/src/contexts/DialogContext.tsx +22 -10
  36. package/src/index.css +143 -1
  37. package/src/index.tsx +4 -0
  38. package/src/theme/proseTheme.ts +2 -0
  39. package/src/utils/combobox.ts +17 -2
  40. package/src/utils/detailLineClamp.ts +33 -0
  41. package/tailwind.config.ts +3 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noya-app/noya-designsystem",
3
- "version": "0.1.66",
3
+ "version": "0.1.68",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.ts",
@@ -20,12 +20,13 @@
20
20
  "lint": "eslint . --max-warnings 0"
21
21
  },
22
22
  "dependencies": {
23
+ "@base-ui-components/react": "1.0.0-beta.4",
23
24
  "@dnd-kit/core": "6.3.1",
24
25
  "@dnd-kit/sortable": "10.0.0",
25
26
  "@noya-app/noya-colorpicker": "0.1.29",
26
27
  "@noya-app/noya-utils": "0.1.8",
27
28
  "@noya-app/noya-geometry": "0.1.15",
28
- "@noya-app/noya-icons": "0.1.13",
29
+ "@noya-app/noya-icons": "0.1.14",
29
30
  "@noya-app/noya-keymap": "0.1.4",
30
31
  "@noya-app/noya-tailwind-config": "0.1.7",
31
32
  "radix-ui": "1.4.2",
@@ -43,6 +44,7 @@
43
44
  },
44
45
  "devDependencies": {
45
46
  "@tailwindcss/container-queries": "^0.1.1",
47
+ "@tailwindcss/typography": "^0.5.19",
46
48
  "@types/react": "*",
47
49
  "@types/react-dom": "*",
48
50
  "@types/react-virtualized": "^9.21.30",
@@ -0,0 +1,30 @@
1
+ import React, { ComponentProps, ForwardedRef, forwardRef } from "react";
2
+ import { cx } from "../utils/classNames";
3
+
4
+ interface AnchorProps extends ComponentProps<"a"> {
5
+ active?: boolean;
6
+ as?: React.ElementType;
7
+ }
8
+
9
+ export const Anchor = forwardRef(function Anchor(
10
+ { className, active, children, as, ...props }: AnchorProps,
11
+ ref: ForwardedRef<HTMLAnchorElement>
12
+ ) {
13
+ const Component = as ?? "a";
14
+
15
+ return (
16
+ <Component
17
+ ref={ref}
18
+ className={cx(
19
+ "n-font-sans n-text-heading5 n-text-breadcrumb-text n-font-normal hover:n-text-breadcrumb-text-hover n-leading-[19px] n-rounded n-no-underline n-inline-flex n-items-center n-justify-center n-gap-2 hover:n-opacity-80 active:n-opacity-90 n-select-none",
20
+ active
21
+ ? "n-text-text !n-underline n-decoration-text-decorative-light"
22
+ : "n-text-text-muted",
23
+ className
24
+ )}
25
+ {...props}
26
+ >
27
+ {children}
28
+ </Component>
29
+ );
30
+ });
@@ -82,18 +82,21 @@ export function BaseToolbar({
82
82
  <Spacer.Horizontal size={10} />
83
83
  </>
84
84
  )}
85
- <div
86
- className={
87
- "n-flex n-items-center n-justify-center n-text-text-muted n-pointer-events-none @2xl/toolbar:n-absolute n-inset-0"
88
- }
89
- >
90
- <div className="n-flex n-items-center n-justify-center n-pointer-events-auto">
91
- {children}
85
+ {children && (
86
+ <div
87
+ className={
88
+ "n-flex n-items-center n-justify-center n-text-text-muted n-pointer-events-none @2xl/toolbar:n-absolute n-inset-0"
89
+ }
90
+ >
91
+ <div className="n-flex n-items-center n-justify-center n-pointer-events-auto">
92
+ {children}
93
+ </div>
92
94
  </div>
93
- </div>
95
+ )}
94
96
  <Spacer.Horizontal />
95
97
  <Spacer.Horizontal size={10} />
96
98
  <div className="n-flex n-gap-toolbar-separator">{right}</div>
99
+ <Spacer.Horizontal size={10} />
97
100
  </BaseToolbarContainer>
98
101
  );
99
102
  }
@@ -29,6 +29,15 @@ const SHADOW_STYLES = {
29
29
  default: "n-shadow-[0_0_0_1px_var(--n-chip-default-shadow)_inset]",
30
30
  } as const;
31
31
 
32
+ const HOVER_SHADOW_STYLES = {
33
+ primary: "hover:n-shadow-[0_0_0_1px_var(--n-chip-primary-shadow-hover)_inset]",
34
+ secondary:
35
+ "hover:n-shadow-[0_0_0_1px_var(--n-chip-secondary-shadow-hover)_inset]",
36
+ info: "hover:n-shadow-[0_0_0_1px_var(--n-chip-default-shadow-hover)_inset]",
37
+ error: "hover:n-shadow-[0_0_0_1px_var(--n-chip-error-shadow-hover)_inset]",
38
+ default: "hover:n-shadow-[0_0_0_1px_var(--n-chip-default-shadow-hover)_inset]",
39
+ } as const;
40
+
32
41
  const COLOR_STYLES = {
33
42
  primary: {
34
43
  text: "n-text-primary",
@@ -133,9 +142,14 @@ export const Chip = memo(
133
142
  const colors = COLOR_STYLES[colorScheme ?? "default"];
134
143
  const variantClasses =
135
144
  variant === "solid"
136
- ? `${colors.text} ${colors.bg}`
145
+ ? cx(colors.text, colors.bg)
137
146
  : variant === "outlined"
138
- ? `${colors.text} n-bg-transparent ${SHADOW_STYLES[colorScheme ?? "default"]} hover:${colors.subtle} hover:n-shadow-none`
147
+ ? cx(
148
+ colors.text,
149
+ "n-bg-transparent",
150
+ SHADOW_STYLES[colorScheme ?? "default"],
151
+ HOVER_SHADOW_STYLES[colorScheme ?? "default"]
152
+ )
139
153
  : "";
140
154
 
141
155
  const chipClasses = cx(
@@ -40,6 +40,11 @@ export interface CollectionProps<T, M extends string = string> {
40
40
  getHref?: (item: T) => string;
41
41
  getExpanded?: (item: T) => boolean | undefined;
42
42
  getPlaceholder?: (item: T) => string;
43
+ /**
44
+ * When starting a rename, select only the portion before
45
+ * the last '.' (keeping the extension unselected). Defaults to false.
46
+ */
47
+ renameSelectsBeforeDot?: boolean;
43
48
  /**
44
49
  * Whether directories should be expandable.
45
50
  * @default true
@@ -64,6 +69,11 @@ export interface CollectionProps<T, M extends string = string> {
64
69
  renderDetail?: (item: T, selected: boolean) => React.ReactNode;
65
70
  /** Optional right-side content renderer for each item */
66
71
  renderRight?: (item: T, selected: boolean) => React.ReactNode;
72
+ /**
73
+ * Number of lines to display for detail text when rendered below grid items.
74
+ * @default 1
75
+ */
76
+ detailLineClamp?: number;
67
77
  /** Callback when selection changes. If not provided, selection will be disabled. */
68
78
  onSelectionChange?: (
69
79
  selectedItemIds: string[],
@@ -17,12 +17,13 @@ import { cx } from "../utils/classNames";
17
17
  import { ComboboxState, useComboboxState } from "../utils/combobox";
18
18
  import { ActivityIndicator } from "./ActivityIndicator";
19
19
  import { ComboboxMenu } from "./ComboboxMenu";
20
+ import { renderIcon } from "./Icons";
20
21
  import { InputField, InputFieldSize } from "./InputField";
21
22
  import {
22
23
  MenuItem,
24
+ MenuItemIcon,
23
25
  ScoredMenuItem,
24
26
  SelectableMenuItem,
25
- isMenuItemSectionHeader,
26
27
  isNonSelectableMenuItem,
27
28
  isSelectableMenuItem,
28
29
  } from "./internal/Menu";
@@ -34,6 +35,7 @@ export type BaseComboboxProps<T extends string> = {
34
35
  loading?: boolean;
35
36
  placeholder?: string;
36
37
  items: MenuItem<T>[];
38
+ icon?: MenuItemIcon;
37
39
  onFocus?: (event: React.FocusEvent<HTMLInputElement>) => void;
38
40
  onDeleteWhenEmpty?: () => void;
39
41
  size?: InputFieldSize;
@@ -90,6 +92,7 @@ export const Combobox = memoGeneric(
90
92
  placeholder,
91
93
  items,
92
94
  size,
95
+ icon,
93
96
  style,
94
97
  className,
95
98
  label: labelProp,
@@ -431,15 +434,8 @@ export const Combobox = memoGeneric(
431
434
  handleBlur();
432
435
  }, [shouldBlur, handleBlur]);
433
436
 
434
- const { sectionHeaderCount, menuItemsCount } = useMemo(
435
- () => ({
436
- sectionHeaderCount: filteredItems.filter((item) =>
437
- isMenuItemSectionHeader(item)
438
- ).length,
439
- menuItemsCount: filteredItems.filter((item) =>
440
- isSelectableMenuItem(item)
441
- ).length,
442
- }),
437
+ const { sectionHeaderCount, menuItemsCount, tallMenuItemsCount } = useMemo(
438
+ () => ListView.getMenuMetrics(filteredItems),
443
439
  [filteredItems]
444
440
  );
445
441
 
@@ -452,6 +448,7 @@ export const Combobox = memoGeneric(
452
448
  id={id}
453
449
  size={size}
454
450
  sideOffset={6}
451
+ start={icon ? renderIcon(icon) : undefined}
455
452
  end={
456
453
  <>
457
454
  {loading && (
@@ -475,15 +472,18 @@ export const Combobox = memoGeneric(
475
472
  </>
476
473
  }
477
474
  renderPopoverContent={({ width }) => {
478
- const height = Math.min(
479
- ListView.calculateHeight({
480
- menuItemsCount,
481
- sectionHeaderCount,
482
- }),
483
- ListView.rowHeight * 10.5
484
- );
475
+ const hasTooltips = tallMenuItemsCount > 0;
476
+
477
+ const baseRowHeight = ListView.rowHeight; // 31
478
+
479
+ const computedHeight = ListView.calculateHeightWithTallRows({
480
+ menuItemsCount,
481
+ sectionHeaderCount,
482
+ tallMenuItemsCount,
483
+ maxBaseRows: 10.5,
484
+ });
485
485
 
486
- const listSize = { width, height };
486
+ const adjustedListSize = { width, height: computedHeight };
487
487
 
488
488
  return (
489
489
  <div
@@ -493,17 +493,26 @@ export const Combobox = memoGeneric(
493
493
  )}
494
494
  >
495
495
  {filteredItems.length > 0 && !loading ? (
496
- <ComboboxMenu
496
+ <ComboboxMenu<T>
497
497
  ref={listRef}
498
498
  items={filteredItems}
499
499
  selectedIndex={selectedIndex}
500
500
  onSelectItem={handleUpdateSelection}
501
501
  onHoverIndex={handleIndexChange}
502
- listSize={listSize}
502
+ listSize={adjustedListSize}
503
503
  style={{
504
- flex: `0 0 ${height}px`,
504
+ flex: `0 0 ${computedHeight}px`,
505
505
  }}
506
506
  indented={hasCheckedItems}
507
+ // Use per-item height if any tooltips exist
508
+ {...(hasTooltips
509
+ ? {
510
+ getRowHeightForItem: (item) =>
511
+ isSelectableMenuItem(item) && item.tooltip
512
+ ? ListView.tallRowHeight
513
+ : baseRowHeight,
514
+ }
515
+ : { itemHeight: baseRowHeight })}
507
516
  />
508
517
  ) : (
509
518
  <div className="n-flex n-flex-col n-h-[50px] n-p-5 n-items-center n-justify-center">
@@ -1,15 +1,16 @@
1
1
  import { Size } from "@noya-app/noya-geometry";
2
2
  import { CheckIcon } from "@noya-app/noya-icons";
3
3
  import { forwardRefGeneric, memoGeneric } from "@noya-app/react-utils";
4
- import React, { ForwardedRef } from "react";
4
+ import React, { ForwardedRef, memo } from "react";
5
5
  import { cx } from "../utils/classNames";
6
6
 
7
7
  import { fuzzyTokenize } from "../utils/fuzzyScorer";
8
- import { renderIcon } from "./Icons";
8
+ import { IconProp, renderIcon } from "./Icons";
9
9
  import {
10
10
  CHECKBOX_INDENT_WIDTH,
11
11
  CHECKBOX_RIGHT_INSET,
12
12
  CHECKBOX_WIDTH,
13
+ isSelectableMenuItem,
13
14
  KeyboardShortcut,
14
15
  ScoredMenuItem,
15
16
  styles,
@@ -17,6 +18,7 @@ import {
17
18
  import { IVirtualizedList } from "./IVirtualizedList";
18
19
  import { ListView } from "./ListView";
19
20
  import { Spacer } from "./Spacer";
21
+ import { Small } from "./Text";
20
22
 
21
23
  interface ComboboxMenuProps<T extends string> {
22
24
  items: ScoredMenuItem<T>[];
@@ -28,6 +30,10 @@ interface ComboboxMenuProps<T extends string> {
28
30
  indented?: boolean;
29
31
  /** @default right */
30
32
  iconPosition?: "left" | "right";
33
+ /** Optional fixed row height for items (e.g., to accommodate descriptions) */
34
+ itemHeight?: number;
35
+ /** Optional per-item height resolver */
36
+ getRowHeightForItem?: (item: ScoredMenuItem<T>, index: number) => number;
31
37
  }
32
38
 
33
39
  export const ComboboxMenu = memoGeneric(
@@ -41,6 +47,8 @@ export const ComboboxMenu = memoGeneric(
41
47
  style,
42
48
  indented,
43
49
  iconPosition = "right",
50
+ itemHeight,
51
+ getRowHeightForItem,
44
52
  }: ComboboxMenuProps<T>,
45
53
  forwardedRef: ForwardedRef<IVirtualizedList>
46
54
  ) {
@@ -55,6 +63,10 @@ export const ComboboxMenu = memoGeneric(
55
63
  }}
56
64
  data={items}
57
65
  virtualized={listSize}
66
+ {...(itemHeight ? { itemHeight } : {})}
67
+ {...(getRowHeightForItem
68
+ ? { getItemHeightForItem: getRowHeightForItem as any }
69
+ : {})}
58
70
  pressEventName="onPointerDown"
59
71
  sectionHeaderVariant="label"
60
72
  style={style}
@@ -81,6 +93,10 @@ export const ComboboxMenu = memoGeneric(
81
93
  itemScore: item,
82
94
  });
83
95
 
96
+ const tooltip: React.ReactNode | undefined =
97
+ // Only selectable items can have tooltips in this menu
98
+ isSelectableMenuItem(item) ? item.tooltip : undefined;
99
+
84
100
  return (
85
101
  <ListView.Row
86
102
  key={item.value}
@@ -93,7 +109,7 @@ export const ComboboxMenu = memoGeneric(
93
109
  }
94
110
  }}
95
111
  >
96
- <div className="n-flex n-flex-1 n-min-w-0 n-items-center">
112
+ <div className="n-flex n-flex-1 n-min-w-0 n-items-start">
97
113
  {indented && <Spacer.Horizontal size={CHECKBOX_INDENT_WIDTH} />}
98
114
  {item.checked ? (
99
115
  <div {...styles.itemIndicator}>
@@ -106,30 +122,41 @@ export const ComboboxMenu = memoGeneric(
106
122
  ) : null}
107
123
  {iconPosition === "left" && item.icon && (
108
124
  <>
109
- {renderIcon(item.icon)}
125
+ <MenuIcon icon={item.icon} />
110
126
  <Spacer.Horizontal size={8} />
111
127
  </>
112
128
  )}
113
- {tokens.map((token, j) => (
114
- <span
115
- key={`${item.value}-token-${j}`}
116
- className={cx(
117
- token.type === "match" ? "n-font-bold" : "n-font-normal",
118
- "n-whitespace-pre n-truncate"
119
- )}
120
- >
121
- {token.text}
122
- </span>
123
- ))}
129
+ <div className="n-flex n-flex-col n-min-w-0 n-flex-1">
130
+ <div className="n-min-w-0 n-truncate">
131
+ {tokens.map((token, j) => (
132
+ <span
133
+ key={`${item.value}-token-${j}`}
134
+ className={cx(
135
+ token.type === "match"
136
+ ? "n-font-bold"
137
+ : "n-font-normal",
138
+ "n-whitespace-pre n-truncate"
139
+ )}
140
+ >
141
+ {token.text}
142
+ </span>
143
+ ))}
144
+ </div>
145
+ {tooltip && (
146
+ <Small color="textSubtle" className="n-truncate">
147
+ {tooltip}
148
+ </Small>
149
+ )}
150
+ </div>
124
151
  </div>
125
152
  {(item.shortcut || (item.icon && iconPosition === "right")) && (
126
- <>
153
+ <div className="n-flex n-items-center n-min-w-0 n-ml-2">
127
154
  {item.shortcut ? (
128
155
  <KeyboardShortcut shortcut={item.shortcut} />
129
156
  ) : (
130
- item.icon && renderIcon(item.icon)
157
+ item.icon && <MenuIcon icon={item.icon} />
131
158
  )}
132
- </>
159
+ </div>
133
160
  )}
134
161
  </ListView.Row>
135
162
  );
@@ -138,3 +165,7 @@ export const ComboboxMenu = memoGeneric(
138
165
  );
139
166
  })
140
167
  );
168
+
169
+ const MenuIcon = memo(function MenuIcon({ icon }: { icon: IconProp }) {
170
+ return <div className="n-relative n-top-[2px]">{renderIcon(icon)}</div>;
171
+ });
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Dialog as DialogPrimitive } from "radix-ui";
3
+ import { Dialog as DialogPrimitive, VisuallyHidden } from "radix-ui";
4
4
  import React, {
5
5
  ComponentProps,
6
6
  ForwardedRef,
@@ -121,6 +121,7 @@ export const Dialog = forwardRef(function Dialog(
121
121
  onOpenAutoFocus={onOpenAutoFocus}
122
122
  style={style}
123
123
  className={className}
124
+ aria-describedby={typeof description === "string" ? description : ""}
124
125
  {...(closeOnInteractOutside === false && {
125
126
  onPointerDownOutside: (event) => event.preventDefault(),
126
127
  onInteractOutside: (event) => event.preventDefault(),
@@ -145,6 +146,11 @@ export const Dialog = forwardRef(function Dialog(
145
146
  <Spacer.Vertical size={description ? 10 : 20} />
146
147
  </>
147
148
  )}
149
+ {!title && (
150
+ <VisuallyHidden.Root>
151
+ <StyledTitle>Dialog</StyledTitle>
152
+ </VisuallyHidden.Root>
153
+ )}
148
154
  {description && (
149
155
  <>
150
156
  <StyledDescription>{description}</StyledDescription>
@@ -138,20 +138,24 @@ export const EditableText = memo(
138
138
 
139
139
  const preview = children({
140
140
  children: displayValue,
141
- onClick: () => setFocused(true),
141
+ onClick: () => {
142
+ if (readOnly) return;
143
+ setFocused(true);
144
+ },
142
145
  ref: previewRef,
143
146
  tabIndex,
144
147
  onKeyDown: (e: React.KeyboardEvent) => {
148
+ if (readOnly) return;
145
149
  if (e.key === "Enter" || e.key === " ") {
146
150
  setFocused(true);
147
151
  }
148
152
  },
149
153
  style: textStyle,
150
- className: textClassName,
154
+ className: cx(focused && "n-invisible", textClassName),
151
155
  });
152
156
 
153
157
  const input = (
154
- <div className="n-flex n-absolute n-inset-0 -mx-1.5 -n-my-1">
158
+ <div className="n-flex n-absolute n-inset-0 -n-mx-1.5 -n-my-1">
155
159
  <InputField.Root onFocusChange={setFocused}>
156
160
  <InputField.Input
157
161
  ref={inputRef}
@@ -85,6 +85,7 @@ export const Grid = memoGeneric(
85
85
  renderAction,
86
86
  renderDetail,
87
87
  renderRight,
88
+ detailLineClamp,
88
89
  onSelectionChange,
89
90
  selectedIds = emptyArray,
90
91
  // itemRoleDescription = "clickable item",
@@ -162,6 +163,7 @@ export const Grid = memoGeneric(
162
163
  scrollable={items.length > 0 && scrollable}
163
164
  contentClassName={cx(items.length === 0 && "n-flex n-flex-1")}
164
165
  gap={gap}
166
+ descriptionLineClamp={detailLineClamp}
165
167
  className={cx(
166
168
  isDropTargetActive && "n-bg-selected-list-item-background",
167
169
  className
@@ -16,6 +16,10 @@ import React, {
16
16
  import { useLinkComponent } from "../contexts/LinkComponentContext";
17
17
  import { useHover } from "../hooks/useHover";
18
18
  import { cx, mergeConflictingClassNames } from "../utils/classNames";
19
+ import {
20
+ DETAIL_LINE_CLAMP_CLASSES,
21
+ normalizeDetailLineClamp,
22
+ } from "../utils/detailLineClamp";
19
23
  import withSeparatorElements from "../utils/withSeparatorElements";
20
24
  import { ActivityIndicator } from "./ActivityIndicator";
21
25
  import { ContextMenu } from "./ContextMenu";
@@ -47,14 +51,21 @@ const ItemTitle = ({
47
51
  const ItemDescription = ({
48
52
  showBackground,
49
53
  children,
54
+ lineClamp,
50
55
  }: {
51
56
  showBackground?: boolean;
52
57
  children?: React.ReactNode;
58
+ lineClamp?: number;
53
59
  }) => {
60
+ const normalizedLineClamp = normalizeDetailLineClamp(lineClamp) ?? 1;
61
+
62
+ const clampClassName = DETAIL_LINE_CLAMP_CLASSES[normalizedLineClamp];
63
+
54
64
  return (
55
65
  <span
56
66
  className={cx(
57
- "n-font-sans n-text-sm n-text-text-muted n-select-none n-truncate",
67
+ "n-font-sans n-text-sm n-text-text-muted n-select-none",
68
+ clampClassName,
58
69
  showBackground &&
59
70
  "n-bg-sidebar-background n-n-border n-border-divider-subtle n-rounded-[2px] n-backdrop-blur-[4px] n-p-[2px_4px]"
60
71
  )}
@@ -139,8 +150,13 @@ const GridViewItem = forwardRefGeneric(function GridViewItem<
139
150
  onHoverChange,
140
151
  });
141
152
 
142
- const { textPosition, bordered, disabled, alwaysShowSubtitles } =
143
- useContext(GridViewContext);
153
+ const {
154
+ textPosition,
155
+ bordered,
156
+ disabled,
157
+ alwaysShowSubtitles,
158
+ descriptionLineClamp,
159
+ } = useContext(GridViewContext);
144
160
 
145
161
  const handleClick = useCallback(
146
162
  (event: React.MouseEvent) => {
@@ -215,16 +231,25 @@ const GridViewItem = forwardRefGeneric(function GridViewItem<
215
231
  {(alwaysShowSubtitles || subtitle) && (
216
232
  <>
217
233
  <Spacer.Vertical size={2} />
218
- <ItemDescription>{subtitle || " "}</ItemDescription>
234
+ <ItemDescription lineClamp={descriptionLineClamp}>
235
+ {subtitle || " "}
236
+ </ItemDescription>
219
237
  </>
220
238
  )}
221
239
  </>
222
240
  )}
223
241
  {textPosition === "overlay" && hovered && (title || subtitle) && (
224
- <div className="n-absolute n-inset-0 n-flex n-flex-col n-justify-end n-items-start n-p-1 n-pointer-events-none n-truncate n-gap-0.5">
242
+ <div
243
+ className={cx(
244
+ "n-absolute n-inset-0 n-flex n-flex-col n-justify-end n-items-start n-p-1 n-pointer-events-none n-gap-0.5",
245
+ descriptionLineClamp === 1 && "n-truncate"
246
+ )}
247
+ >
225
248
  {title && <ItemTitle showBackground>{title}</ItemTitle>}
226
249
  {subtitle && (
227
- <ItemDescription showBackground>{subtitle}</ItemDescription>
250
+ <ItemDescription showBackground lineClamp={descriptionLineClamp}>
251
+ {subtitle}
252
+ </ItemDescription>
228
253
  )}
229
254
  </div>
230
255
  )}
@@ -259,7 +284,9 @@ const GridViewItem = forwardRefGeneric(function GridViewItem<
259
284
  content={
260
285
  <div className="n-flex n-flex-col gap-0.5">
261
286
  <ItemTitle>{title}</ItemTitle>
262
- <ItemDescription>{subtitle}</ItemDescription>
287
+ <ItemDescription lineClamp={descriptionLineClamp}>
288
+ {subtitle}
289
+ </ItemDescription>
263
290
  </div>
264
291
  }
265
292
  >
@@ -278,6 +305,7 @@ type GridViewContextValue = {
278
305
  disabled: boolean;
279
306
  gap: number;
280
307
  alwaysShowSubtitles: boolean;
308
+ descriptionLineClamp: number;
281
309
  };
282
310
 
283
311
  const GridViewContext = createContext<GridViewContextValue>({
@@ -287,6 +315,7 @@ const GridViewContext = createContext<GridViewContextValue>({
287
315
  disabled: false,
288
316
  gap: 0,
289
317
  alwaysShowSubtitles: false,
318
+ descriptionLineClamp: 1,
290
319
  });
291
320
 
292
321
  interface GridViewRootProps extends Partial<GridViewContextValue> {
@@ -302,6 +331,7 @@ interface GridViewRootProps extends Partial<GridViewContextValue> {
302
331
  renderEmptyState?: () => ReactNode;
303
332
  contentClassName?: string;
304
333
  alwaysShowSubtitles?: boolean;
334
+ descriptionLineClamp?: number;
305
335
  }
306
336
 
307
337
  const GridViewRoot = forwardRef(function GridViewRoot(
@@ -317,6 +347,7 @@ const GridViewRoot = forwardRef(function GridViewRoot(
317
347
  className,
318
348
  contentClassName,
319
349
  alwaysShowSubtitles = false,
350
+ descriptionLineClamp = 1,
320
351
  ...props
321
352
  }: GridViewRootProps,
322
353
  forwardedRef: ForwardedRef<HTMLDivElement>
@@ -331,6 +362,9 @@ const GridViewRoot = forwardRef(function GridViewRoot(
331
362
  [onClick]
332
363
  );
333
364
 
365
+ const normalizedDescriptionLineClamp =
366
+ normalizeDetailLineClamp(descriptionLineClamp) ?? 1;
367
+
334
368
  const contextValue = useMemo(
335
369
  (): GridViewContextValue => ({
336
370
  minColumnWidth,
@@ -339,13 +373,22 @@ const GridViewRoot = forwardRef(function GridViewRoot(
339
373
  disabled,
340
374
  gap,
341
375
  alwaysShowSubtitles,
376
+ descriptionLineClamp: normalizedDescriptionLineClamp,
342
377
  }),
343
- [bordered, disabled, gap, minColumnWidth, textPosition, alwaysShowSubtitles]
378
+ [
379
+ bordered,
380
+ disabled,
381
+ gap,
382
+ minColumnWidth,
383
+ textPosition,
384
+ alwaysShowSubtitles,
385
+ normalizedDescriptionLineClamp,
386
+ ]
344
387
  );
345
388
 
346
389
  const mergedClassName = useMemo(() => {
347
390
  const baseClassName = cx(
348
- "n-flex n-flex-col n--mx-3",
391
+ "n-flex n-flex-col -n-mx-3",
349
392
  scrollable ? "n-basis-0 n-min-h-0" : "n-flex-none",
350
393
  className
351
394
  );