@noya-app/noya-designsystem 0.1.63 → 0.1.65

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 (143) hide show
  1. package/.turbo/turbo-build.log +13 -10
  2. package/CHANGELOG.md +25 -0
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.mts +648 -333
  5. package/dist/index.d.ts +648 -333
  6. package/dist/index.js +13813 -9157
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +13949 -9269
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +10 -22
  11. package/src/__tests__/validateDropIndicator.test.ts +26 -41
  12. package/src/__tests__/workspaceLayout.test.ts +281 -0
  13. package/src/components/ActionMenu.tsx +2 -0
  14. package/src/components/ActivityIndicator.tsx +13 -6
  15. package/src/components/ActivityLog.tsx +197 -0
  16. package/src/components/AnimatePresence.tsx +43 -26
  17. package/src/components/Avatar.tsx +45 -26
  18. package/src/components/Banner.tsx +57 -11
  19. package/src/components/BaseToolbar.tsx +67 -34
  20. package/src/components/Breadcrumbs.tsx +4 -2
  21. package/src/components/Button.tsx +28 -23
  22. package/src/components/Checkbox.tsx +6 -35
  23. package/src/components/Chip.tsx +47 -33
  24. package/src/components/Collection.tsx +20 -12
  25. package/src/components/ColorSwatch.tsx +42 -22
  26. package/src/components/ColorSwatchControl.tsx +7 -5
  27. package/src/components/Combobox.tsx +8 -6
  28. package/src/components/ComboboxMenu.tsx +6 -5
  29. package/src/components/CommandPalette.tsx +3 -1
  30. package/src/components/ContextMenu.tsx +3 -1
  31. package/src/components/Dialog.tsx +9 -13
  32. package/src/components/DimensionInput.tsx +2 -0
  33. package/src/components/Divider.tsx +18 -14
  34. package/src/components/DraggableMenuButton.tsx +11 -5
  35. package/src/components/Drawer.tsx +14 -6
  36. package/src/components/DropdownMenu.tsx +4 -2
  37. package/src/components/EditableText.tsx +4 -2
  38. package/src/components/Fade.tsx +13 -13
  39. package/src/components/FileUploadIndicator.tsx +108 -0
  40. package/src/components/FloatingWindow.tsx +4 -2
  41. package/src/components/Grid.tsx +58 -42
  42. package/src/components/GridView.tsx +65 -36
  43. package/src/components/IVirtualizedList.tsx +5 -0
  44. package/src/components/IconButton.tsx +2 -0
  45. package/src/components/Icons.tsx +24 -4
  46. package/src/components/InputField.tsx +21 -19
  47. package/src/components/InspectorContainer.tsx +2 -2
  48. package/src/components/InspectorPrimitives.tsx +15 -9
  49. package/src/components/Label.tsx +4 -3
  50. package/src/components/LabeledElementView.tsx +9 -5
  51. package/src/components/LabeledField.tsx +8 -6
  52. package/src/components/List.tsx +112 -46
  53. package/src/components/ListMenu.tsx +91 -0
  54. package/src/components/ListNavigator.tsx +106 -0
  55. package/src/components/ListView.tsx +22 -1147
  56. package/src/components/MediaThumbnail.tsx +84 -14
  57. package/src/components/Message.tsx +11 -12
  58. package/src/components/Navigator.tsx +461 -0
  59. package/src/components/NoyaLogo.tsx +1 -1
  60. package/src/components/Popover.tsx +12 -6
  61. package/src/components/Progress.tsx +31 -8
  62. package/src/components/ResizableContainer.tsx +187 -0
  63. package/src/components/RingProgress.tsx +128 -0
  64. package/src/components/ScrollArea.tsx +7 -5
  65. package/src/components/ScrollableSidebar.tsx +10 -0
  66. package/src/components/SearchCompletionMenu.tsx +5 -3
  67. package/src/components/Section.tsx +109 -57
  68. package/src/components/SegmentedControl.tsx +51 -25
  69. package/src/components/SelectMenu.tsx +16 -21
  70. package/src/components/SelectionToolbar.tsx +7 -4
  71. package/src/components/Slider.tsx +12 -10
  72. package/src/components/Spacer.tsx +2 -2
  73. package/src/components/StackNavigator.tsx +378 -0
  74. package/src/components/Stepper.tsx +88 -0
  75. package/src/components/Switch.tsx +7 -5
  76. package/src/components/Tabs.tsx +6 -4
  77. package/src/components/Text.tsx +29 -18
  78. package/src/components/TextArea.tsx +5 -3
  79. package/src/components/Toast.tsx +5 -5
  80. package/src/components/Toolbar.tsx +57 -10
  81. package/src/components/ToolbarDrawer.tsx +29 -0
  82. package/src/components/Tooltip.tsx +2 -2
  83. package/src/components/TreeView.tsx +13 -4
  84. package/src/components/UserPointer.tsx +6 -4
  85. package/src/components/ai-assistant/AIAssistantLayout.tsx +10 -8
  86. package/src/components/blocks/ImageBlockComponent.tsx +162 -0
  87. package/src/components/connected-users-menu/ConnectedUsersMenuLayout.tsx +6 -4
  88. package/src/components/file-explorer/FileExplorerLayout.tsx +22 -8
  89. package/src/components/internal/Checkmark.tsx +24 -0
  90. package/src/components/internal/Menu.tsx +24 -19
  91. package/src/components/internal/MenuViewport.tsx +15 -6
  92. package/src/components/internal/SelectItem.tsx +7 -26
  93. package/src/components/internal/TextInput.tsx +7 -1
  94. package/src/components/listView/ListViewContexts.tsx +68 -0
  95. package/src/components/listView/ListViewEditableRowTitle.tsx +55 -0
  96. package/src/components/listView/ListViewRoot.tsx +521 -0
  97. package/src/components/listView/ListViewRow.tsx +475 -0
  98. package/src/components/listView/ListViewRowTitle.tsx +21 -0
  99. package/src/components/listView/types.ts +11 -0
  100. package/src/components/pipeline/PipelineResultLayout.tsx +6 -6
  101. package/src/components/sorting/DragRegistration.tsx +10 -5
  102. package/src/components/sorting/SharedDragProvider.tsx +134 -27
  103. package/src/components/sorting/Sortable.tsx +90 -27
  104. package/src/components/sorting/createSharedDrag.tsx +7 -6
  105. package/src/components/sorting/sorting.ts +6 -9
  106. package/src/components/workspace/DrawerWorkspaceLayout.tsx +110 -29
  107. package/src/components/workspace/PanelWorkspaceLayout.tsx +148 -99
  108. package/src/components/workspace/VerticalTabMenu.tsx +46 -0
  109. package/src/components/workspace/WorkspaceLayout.tsx +76 -63
  110. package/src/components/workspace/constants.ts +4 -0
  111. package/src/components/workspace/panelStorage.ts +216 -0
  112. package/src/components/workspace/types.ts +11 -4
  113. package/src/contexts/DesignSystemConfiguration.tsx +12 -5
  114. package/src/contexts/DialogContext.tsx +19 -5
  115. package/src/contexts/FloatingWindowContext.tsx +2 -0
  116. package/src/contexts/GlobalInputBlurContext.tsx +2 -0
  117. package/src/contexts/ImageDataContext.tsx +2 -0
  118. package/src/contexts/LinkComponentContext.tsx +17 -0
  119. package/src/contexts/OpenPortalsContext.tsx +2 -0
  120. package/src/contexts/PortalScopeContext.tsx +2 -0
  121. package/src/hooks/mergeEventHandlers.ts +1 -1
  122. package/src/hooks/useHover.ts +30 -17
  123. package/src/hooks/useIndent.ts +5 -8
  124. package/src/hooks/useLabel.ts +2 -0
  125. package/src/hooks/useObjectURL.ts +5 -3
  126. package/src/hooks/usePlatform.ts +5 -3
  127. package/src/hooks/useTheme.ts +3 -1
  128. package/src/index.css +43 -10
  129. package/src/index.tsx +15 -7
  130. package/src/utils/classNames.ts +56 -9
  131. package/src/utils/combobox.ts +2 -0
  132. package/src/utils/editableBlockStyles.ts +2 -2
  133. package/src/utils/formatByteSize.ts +9 -0
  134. package/src/utils/inputs.ts +8 -6
  135. package/src/utils/sketchColor.ts +0 -34
  136. package/src/utils/withSeparatorElements.ts +1 -1
  137. package/tailwind.config.ts +1 -0
  138. package/src/components/FillInputField.tsx +0 -37
  139. package/src/components/FillPreviewBackground.tsx +0 -132
  140. package/src/components/GradientPicker.tsx +0 -88
  141. package/src/hooks/usePreservePanelSize.tsx +0 -183
  142. package/src/mediaQuery.ts +0 -13
  143. package/src/utils/getGradientBackground.tsx +0 -31
@@ -5,9 +5,9 @@ import React, {
5
5
  memo,
6
6
  ReactNode,
7
7
  } from "react";
8
+ import { cx } from "../utils/classNames";
8
9
  import { Spacer } from "./Spacer";
9
10
  import { textStyles } from "./Text";
10
- import { cx } from "../utils/classNames";
11
11
 
12
12
  export const Section = forwardRef<
13
13
  HTMLDivElement,
@@ -15,7 +15,7 @@ export const Section = forwardRef<
15
15
  >(({ className, ...props }, ref) => (
16
16
  <div
17
17
  ref={ref}
18
- className={cx(`flex-none flex flex-col `, className)}
18
+ className={cx(`n-flex-none n-flex n-flex-col`, className)}
19
19
  {...props}
20
20
  />
21
21
  ));
@@ -24,7 +24,7 @@ export const SectionHeader = forwardRef<
24
24
  HTMLDivElement,
25
25
  React.HTMLAttributes<HTMLDivElement>
26
26
  >(({ className, ...props }, ref) => (
27
- <div ref={ref} className={cx(`flex items-center `, className)} {...props} />
27
+ <div ref={ref} className={cx(`n-flex n-items-center`, className)} {...props} />
28
28
  ));
29
29
 
30
30
  type TitleTextStyle = "small" | "heading5" | "heading4" | "heading3";
@@ -38,7 +38,11 @@ export const Title = forwardRef<
38
38
  <div
39
39
  ref={ref}
40
40
  className={cx(
41
- `flex select-none ${$textStyle ? `${textStyles[$textStyle]} text-text` : "font-sans text-label uppercase text-text-muted font-bold"} `,
41
+ `n-flex n-select-none ${
42
+ $textStyle
43
+ ? `${textStyles[$textStyle]} n-text-text`
44
+ : "n-font-sans n-text-label n-text-text-muted n-font-semibold"
45
+ } `,
42
46
  className
43
47
  )}
44
48
  {...props}
@@ -51,7 +55,7 @@ export const Row = forwardRef<
51
55
  >(({ className, ...props }, ref) => (
52
56
  <div
53
57
  ref={ref}
54
- className={cx(`flex-1 flex flex-row items-center `, className)}
58
+ className={cx(`n-flex-1 n-flex n-flex-row n-items-center`, className)}
55
59
  {...props}
56
60
  />
57
61
  ));
@@ -62,7 +66,7 @@ export const Column = forwardRef<
62
66
  >(({ className, ...props }, ref) => (
63
67
  <div
64
68
  ref={ref}
65
- className={cx(`flex-1 flex flex-col gap-1 `, className)}
69
+ className={cx(`n-flex-1 n-flex n-flex-col n-gap-1`, className)}
66
70
  {...props}
67
71
  />
68
72
  ));
@@ -74,7 +78,7 @@ export const Checkbox = forwardRef<
74
78
  <input
75
79
  ref={ref}
76
80
  type="checkbox"
77
- className={cx(`m-0 focus:z-interactable `, className)}
81
+ className={cx(`n-m-0 focus:n-z-interactable`, className)}
78
82
  {...props}
79
83
  />
80
84
  ));
@@ -85,7 +89,7 @@ export const Text = forwardRef<
85
89
  >(({ className, ...props }, ref) => (
86
90
  <span
87
91
  ref={ref}
88
- className={cx(`font-sans text-heading5 font-normal `, className)}
92
+ className={cx(`n-font-sans n-text-heading5 n-font-normal`, className)}
89
93
  {...props}
90
94
  />
91
95
  ));
@@ -123,7 +127,9 @@ export const RowLabel = forwardRef(function RowLabel(
123
127
  <label
124
128
  ref={ref}
125
129
  className={cx(
126
- `font-sans text-label uppercase flex items-center leading-[19px] font-bold text-text-subtle z-label ${$textStyle && textStyles[$textStyle]} `,
130
+ `n-font-sans n-text-label n-uppercase n-flex n-items-center n-leading-[19px] n-font-bold n-text-text-subtle n-z-label ${
131
+ $textStyle && textStyles[$textStyle]
132
+ } `,
127
133
  className
128
134
  )}
129
135
  {...props}
@@ -3,9 +3,10 @@ import { mergeConflictingClassNames } from "../utils/classNames";
3
3
 
4
4
  export interface LabelProps extends LabelHTMLAttributes<HTMLLabelElement> {}
5
5
 
6
- const labelStyles = "flex items-center select-none z-label relative";
6
+ const labelStyles =
7
+ "n-flex n-items-center n-select-none n-z-label n-relative";
7
8
  const labelTextStyles =
8
- "font-sans text-label uppercase leading-3 font-bold text-text-muted";
9
+ "n-font-sans n-text-label n-leading-none n-font-semibold n-text-text-muted";
9
10
 
10
11
  export const Label = memo(
11
12
  forwardRef<HTMLLabelElement, LabelProps>(function Label(
@@ -19,7 +20,7 @@ export const Label = memo(
19
20
  [
20
21
  labelStyles,
21
22
  labelTextStyles,
22
- "text-left min-h-input-height",
23
+ "n-text-left n-min-h-input-height",
23
24
  className,
24
25
  ],
25
26
  {
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import * as kiwi from "kiwi.js";
2
4
  import * as React from "react";
3
5
 
@@ -13,11 +15,13 @@ export const LabeledElementView = React.memo(function LabeledElementView({
13
15
  const elementIds: string[] = React.Children.toArray(children)
14
16
  .flatMap((child) =>
15
17
  React.isValidElement(child) && child.type === React.Fragment
16
- ? (child.props.children as React.ReactNode[])
18
+ ? ((child as any).props.children as React.ReactNode[])
17
19
  : [child]
18
20
  )
19
21
  .map((child) =>
20
- React.isValidElement(child) && "id" in child.props ? child.props.id : null
22
+ React.isValidElement(child) && "id" in (child as any).props
23
+ ? (child as any).props.id
24
+ : null
21
25
  )
22
26
  .filter((id) => !!id);
23
27
  const serializedIds = elementIds.join(",");
@@ -148,9 +152,9 @@ export const LabeledElementView = React.memo(function LabeledElementView({
148
152
  }, [refs, labelElements]);
149
153
 
150
154
  return (
151
- <div className="flex flex-1 flex-col relative" ref={containerRef}>
152
- <div className="flex flex-1 items-center">{children}</div>
153
- <div className="relative overflow-hidden select-none">
155
+ <div className="n-flex n-flex-1 n-flex-col n-relative" ref={containerRef}>
156
+ <div className="n-flex n-flex-1 n-items-center">{children}</div>
157
+ <div className="n-relative n-overflow-hidden n-select-none">
154
158
  {labelElements}
155
159
  </div>
156
160
  </div>
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import React, { memo, useMemo } from "react";
2
4
  import { useIndent } from "../hooks/useIndent";
3
5
  import {
@@ -13,9 +15,9 @@ import { Label } from "./Label";
13
15
 
14
16
  export const labeledFieldStyles = (labelType?: LabelType) =>
15
17
  cx(
16
- "flex flex-row flex-1 flex-wrap items-center gap-x-1.5",
17
- labelType === "end" && "flex-row-reverse justify-end",
18
- labelType === "above" && "flex-col items-stretch justify-start"
18
+ "n-flex n-flex-row n-flex-1 n-flex-wrap n-items-center n-gap-x-1.5",
19
+ labelType === "end" && "n-flex-row-reverse n-justify-end",
20
+ labelType === "above" && "n-flex-col n-items-stretch n-justify-start"
19
21
  );
20
22
 
21
23
  const labelTypeInset: LabelTypeContextValue = {
@@ -94,7 +96,7 @@ export const LabeledField = memo(function LabeledField({
94
96
  {children}
95
97
  </LabelTypeContext.Provider>
96
98
  ) : (
97
- <div className={cx("flex relative flex-1", className)} style={style}>
99
+ <div className={cx("n-flex n-relative n-flex-1", className)} style={style}>
98
100
  {Boolean(indentLevel) && <div style={indentStyle} />}
99
101
  <div className={labeledFieldClasses}>
100
102
  {labelType !== "none" && (
@@ -104,7 +106,7 @@ export const LabeledField = memo(function LabeledField({
104
106
  // and when start/end position with textarea it aligns label to the top of the field
105
107
  className={
106
108
  labelType === "start" || labelType === "end"
107
- ? "self-start"
109
+ ? "n-self-start"
108
110
  : undefined
109
111
  }
110
112
  style={labelStyle}
@@ -114,7 +116,7 @@ export const LabeledField = memo(function LabeledField({
114
116
  </Label>
115
117
  )}
116
118
  {labelType === "start" ? (
117
- <div className="flex-auto flex" style={fieldStyle}>
119
+ <div className="n-flex-auto n-flex" style={fieldStyle}>
118
120
  {children}
119
121
  </div>
120
122
  ) : (
@@ -1,9 +1,18 @@
1
+ "use client";
2
+
1
3
  import {
2
4
  forwardRefGeneric,
3
5
  memoGeneric,
4
6
  useFileDropTarget,
5
7
  } from "@noya-app/react-utils";
6
- import React, { memo, useImperativeHandle, useRef, useState } from "react";
8
+ import React, {
9
+ memo,
10
+ useImperativeHandle,
11
+ useMemo,
12
+ useRef,
13
+ useState,
14
+ } from "react";
15
+ import { useLinkComponent } from "../contexts/LinkComponentContext";
7
16
  import { cssVars } from "../theme";
8
17
  import { cx } from "../utils/classNames";
9
18
  import { updateSelection } from "../utils/selection";
@@ -13,7 +22,8 @@ import {
13
22
  CollectionProps,
14
23
  CollectionRef,
15
24
  } from "./Collection";
16
- import { ListView, ListViewItemInfo } from "./ListView";
25
+ import { ListView } from "./ListView";
26
+ import { ListViewItemInfo } from "./listView/ListViewRoot";
17
27
  import { TreeView } from "./TreeView";
18
28
 
19
29
  const cssVarOverrides = {
@@ -32,6 +42,7 @@ export const List = memoGeneric(
32
42
  items,
33
43
  getId,
34
44
  getName,
45
+ getHref,
35
46
  expandable,
36
47
  getExpanded,
37
48
  getRenamable,
@@ -43,7 +54,10 @@ export const List = memoGeneric(
43
54
  renderThumbnail,
44
55
  renderAction,
45
56
  renderDetail,
57
+ renderRight,
46
58
  onSelectionChange,
59
+ itemStyle: itemStyleProp,
60
+ itemClassName: itemClassNameProp,
47
61
  itemRoleDescription = "clickable item",
48
62
  detailPosition = "end",
49
63
  size = "medium",
@@ -63,9 +77,11 @@ export const List = memoGeneric(
63
77
  getDropTargetParentIndex,
64
78
  getPlaceholder,
65
79
  onClickItem,
80
+ onClick,
66
81
  dragIndicatorStyle,
67
82
  sortableId,
68
83
  sharedDragProps,
84
+ virtualized,
69
85
  } = props;
70
86
 
71
87
  const [internalHoveredId, setHoveredId] = useState<string>();
@@ -73,6 +89,16 @@ export const List = memoGeneric(
73
89
  const [dropdownOpenId, setDropdownOpenId] = useState<string>();
74
90
  const hoveredId = internalHoveredId ?? testHoveredId;
75
91
  const renamingId = testRenamingId ?? internalRenamingId;
92
+ const itemClassName = cx(
93
+ "n-cursor-pointer n-px-1",
94
+ pyMap[size],
95
+ itemClassNameProp
96
+ );
97
+
98
+ const itemStyle = useMemo(
99
+ () => ({ ...cssVarOverrides, ...itemStyleProp }),
100
+ [itemStyleProp]
101
+ );
76
102
 
77
103
  const handleSelect = (itemId: string, event?: ListView.ClickInfo) => {
78
104
  if (!onSelectionChange) return;
@@ -101,8 +127,9 @@ export const List = memoGeneric(
101
127
  };
102
128
 
103
129
  const ViewComponent = expandable ? TreeView : ListView;
130
+ const LinkComponent = useLinkComponent();
104
131
 
105
- const fileDropTargetRef = useRef<HTMLDivElement>(null);
132
+ const fileDropTargetRef = useRef<HTMLDivElement | null>(null);
106
133
  const { isDropTargetActive, dropTargetProps } = useFileDropTarget(
107
134
  fileDropTargetRef,
108
135
  onFilesDrop
@@ -119,11 +146,12 @@ export const List = memoGeneric(
119
146
  variant="bare"
120
147
  className={cx(
121
148
  className,
122
- isDropTargetActive && "bg-selected-list-item-background",
123
- scrollable && "basis-0 min-h-0"
149
+ isDropTargetActive && "n-bg-selected-list-item-background",
150
+ scrollable && "n-basis-0 n-min-h-0"
124
151
  )}
125
152
  containerRef={fileDropTargetRef}
126
153
  scrollable={scrollable}
154
+ virtualized={virtualized}
127
155
  data={items}
128
156
  keyExtractor={getId}
129
157
  role="listbox"
@@ -137,6 +165,8 @@ export const List = memoGeneric(
137
165
  getDropTargetParentIndex={getDropTargetParentIndex}
138
166
  sharedDragProps={sharedDragProps}
139
167
  sortableId={sortableId}
168
+ onPress={onClick}
169
+ itemHeight={itemHeightMap[size]}
140
170
  {...dropTargetProps}
141
171
  renderItem={(
142
172
  item: T,
@@ -144,6 +174,7 @@ export const List = memoGeneric(
144
174
  { isDragOverlay }: ListViewItemInfo
145
175
  ) => {
146
176
  const id = getId(item);
177
+ const href = getHref?.(item);
147
178
  const isDropdownOpen = dropdownOpenId === id;
148
179
  const isHovered =
149
180
  (hoveredId === id || isDropdownOpen) && !isDragOverlay;
@@ -192,11 +223,13 @@ export const List = memoGeneric(
192
223
  const end =
193
224
  action ||
194
225
  (detailPosition === "end" && renderDetail?.(item, isSelected));
226
+ const right = renderRight?.(item, isSelected);
195
227
  const below =
196
228
  detailPosition === "below" && renderDetail?.(item, isSelected);
197
-
198
229
  return (
199
230
  <ViewComponent.Row
231
+ as={typeof href === "string" ? LinkComponent : undefined}
232
+ href={href}
200
233
  id={id}
201
234
  key={id}
202
235
  role="option"
@@ -204,12 +237,12 @@ export const List = memoGeneric(
204
237
  aria-roledescription={itemRoleDescription}
205
238
  aria-label={getName(item)}
206
239
  aria-selected={isSelected}
207
- className={cx("cursor-pointer rounded px-1", pyMap[size])}
240
+ className={itemClassName}
208
241
  testRelativeDropPosition={
209
242
  testShowDropIndicatorId === id ? "inside" : undefined
210
243
  }
211
- chevronClassName={cx(expandable && "relative left-2")}
212
- style={cssVarOverrides as React.CSSProperties}
244
+ chevronClassName={cx(expandable && "n-relative n-left-2")}
245
+ style={itemStyle}
213
246
  hovered={isHovered}
214
247
  selected={isSelected}
215
248
  expanded={expanded}
@@ -220,6 +253,7 @@ export const List = memoGeneric(
220
253
  onPress={(e: ListView.ClickInfo) => {
221
254
  handleSelect(id, e);
222
255
  onClickItem?.(id);
256
+ e.stopPropagation();
223
257
  }}
224
258
  onDoubleClick={() => {
225
259
  onDoubleClickItem?.(id);
@@ -247,24 +281,24 @@ export const List = memoGeneric(
247
281
  dragIndicatorStyle={dragIndicatorStyle}
248
282
  >
249
283
  <div
250
- className={cx("flex flex-1 items-center px-2", rowGapMap[size])}
284
+ className={cx("n-flex n-flex-1 n-items-center n-px-2", rowGapMap[size])}
251
285
  >
252
286
  {thumbnail && (
253
287
  <div
254
288
  className={cx(
255
- "overflow-hidden flex-none flex items-center justify-center",
289
+ "n-overflow-hidden n-flex-none n-flex n-items-center n-justify-center",
256
290
  thumbnailSize === "auto" && thumbnailSizeMap[size],
257
- isSelected && "text-selected-list-item-text",
258
- size === "small" ? "rounded-sm" : "rounded"
291
+ isSelected && "n-text-selected-list-item-text",
292
+ roundedMap[size]
259
293
  )}
260
294
  tabIndex={-1}
261
295
  >
262
296
  {thumbnail}
263
297
  </div>
264
298
  )}
265
- <div className="flex flex-col flex-1 w-0">
266
- <div className="flex items-center min-h-input-height flex-1 gap-2">
267
- <div className="flex-1 w-0 flex items-center" tabIndex={-1}>
299
+ <div className="n-flex n-flex-col n-flex-1 n-w-0">
300
+ <div className="n-flex n-items-center n-min-h-input-height n-flex-1 n-gap-2">
301
+ <div className="n-flex-1 n-w-0 n-flex n-items-center" tabIndex={-1}>
268
302
  {isRenaming ? (
269
303
  <ViewComponent.EditableRowTitle
270
304
  value={getName(item)}
@@ -283,13 +317,19 @@ export const List = memoGeneric(
283
317
  <ViewComponent.RowTitle
284
318
  className={cx(
285
319
  fontStyleMap[size],
286
- isSelected && "text-selected-list-item-text"
320
+ isSelected && "n-text-selected-list-item-text"
287
321
  )}
288
322
  >
289
323
  {getName(item)}
290
324
  </ViewComponent.RowTitle>
291
325
  )}
292
326
  </div>
327
+ {/* Right content should appear before end/detail when detailPosition is 'end' */}
328
+ {right && detailPosition === "end" && (
329
+ <DetailContainer selected={isSelected}>
330
+ {right}
331
+ </DetailContainer>
332
+ )}
293
333
  {end && detailPosition === "end" && (
294
334
  <DetailContainer selected={isSelected}>
295
335
  {end}
@@ -302,6 +342,12 @@ export const List = memoGeneric(
302
342
  </DetailContainer>
303
343
  )}
304
344
  </div>
345
+ {/* When detail is below, right should still render on the right next to the action */}
346
+ {right && detailPosition === "below" && (
347
+ <DetailContainer selected={isSelected}>
348
+ {right}
349
+ </DetailContainer>
350
+ )}
305
351
  {end && detailPosition === "below" && (
306
352
  <DetailContainer selected={isSelected}>{end}</DetailContainer>
307
353
  )}
@@ -323,7 +369,7 @@ const DetailContainer = memo(function DetailContainer({
323
369
  }) {
324
370
  return (
325
371
  <div
326
- className={cx("flex items-center gap-2", selected && "text-primary")}
372
+ className={cx("n-flex n-items-center n-gap-2", selected && "n-text-primary")}
327
373
  tabIndex={-1}
328
374
  >
329
375
  {children}
@@ -332,41 +378,61 @@ const DetailContainer = memo(function DetailContainer({
332
378
  });
333
379
 
334
380
  const pyMap: Record<CollectionItemSize, string> = {
335
- xxs: "py-0.5",
336
- xs: "py-0.5",
337
- small: "py-0.5",
338
- medium: "py-1",
339
- mediumLarge: "py-1.5",
340
- large: "py-2",
341
- xl: "py-3",
381
+ xxs: "n-py-0.5",
382
+ xs: "n-py-0.5",
383
+ small: "n-py-1",
384
+ medium: "n-py-1",
385
+ mediumLarge: "n-py-1.5",
386
+ large: "n-py-2",
387
+ xl: "n-py-3",
342
388
  };
343
389
 
344
390
  const thumbnailSizeMap: Record<CollectionItemSize, string> = {
345
- xxs: "w-5 h-5",
346
- xs: "w-5 h-5",
347
- small: "w-5 h-5",
348
- medium: "w-8 h-8",
349
- mediumLarge: "w-12 h-12",
350
- large: "w-16 h-16",
351
- xl: "w-24 h-24",
391
+ xxs: "n-w-4 n-h-4",
392
+ xs: "n-w-5 n-h-5",
393
+ small: "n-w-6 n-h-6",
394
+ medium: "n-w-8 n-h-8",
395
+ mediumLarge: "n-w-12 n-h-12",
396
+ large: "n-w-16 n-h-16",
397
+ xl: "n-w-24 n-h-24",
398
+ };
399
+
400
+ const roundedMap: Record<CollectionItemSize, string> = {
401
+ xxs: "n-rounded-sm",
402
+ xs: "n-rounded-sm",
403
+ small: "n-rounded",
404
+ medium: "n-rounded",
405
+ mediumLarge: "n-rounded",
406
+ large: "n-rounded",
407
+ xl: "n-rounded",
352
408
  };
353
409
 
354
410
  const rowGapMap: Record<CollectionItemSize, string> = {
355
- xxs: "gap-2",
356
- xs: "gap-2",
357
- small: "gap-2",
358
- medium: "gap-3",
359
- mediumLarge: "gap-3",
360
- large: "gap-3",
361
- xl: "gap-4",
411
+ xxs: "n-gap-2",
412
+ xs: "n-gap-2",
413
+ small: "n-gap-2",
414
+ medium: "n-gap-2",
415
+ mediumLarge: "n-gap-3",
416
+ large: "n-gap-3",
417
+ xl: "n-gap-4",
362
418
  };
363
419
 
364
420
  const fontStyleMap: Record<CollectionItemSize, string> = {
365
- xxs: "font-medium text-list-small",
366
- xs: "font-medium text-list-small",
367
- small: "font-medium text-list-small",
368
- medium: "font-medium",
369
- mediumLarge: "font-medium",
370
- large: "font-medium",
371
- xl: "font-medium text-heading4 text-text",
421
+ xxs: "n-font-medium n-text-list-small",
422
+ xs: "n-font-medium n-text-list-small",
423
+ small: "n-font-medium n-text-list-small",
424
+ medium: "n-font-medium",
425
+ mediumLarge: "n-font-medium",
426
+ large: "n-font-medium",
427
+ xl: "n-font-medium n-text-heading4 n-text-text",
428
+ };
429
+
430
+ const itemHeightMap: Record<CollectionItemSize, number> = {
431
+ xxs: 31,
432
+ xs: 31,
433
+ small: 31,
434
+ medium: 40,
435
+ mediumLarge: 60,
436
+ large: 80,
437
+ xl: 120,
372
438
  };
@@ -0,0 +1,91 @@
1
+ "use client";
2
+
3
+ import { ArrowRightIcon } from "@noya-app/noya-icons";
4
+ import { chunkBy } from "@noya-app/noya-utils";
5
+ import React from "react";
6
+ import withSeparatorElements from "../utils/withSeparatorElements";
7
+ import { CollectionProps } from "./Collection";
8
+ import { Divider } from "./Divider";
9
+ import { renderIcon } from "./Icons";
10
+ import {
11
+ isSelectableMenuItem,
12
+ isSubMenuItem,
13
+ MenuItem,
14
+ SelectableMenuItem,
15
+ SubMenuItem,
16
+ } from "./internal/Menu";
17
+ import { List } from "./List";
18
+
19
+ export type ListMenuProps<T extends string> = {
20
+ items: MenuItem<T>[];
21
+ selectedIds?: T[];
22
+ onSelect: (item: T) => void;
23
+ renderRight?: CollectionProps<MenuItem<T>, T>["renderRight"];
24
+ className?: string;
25
+ };
26
+
27
+ function getItemId<T extends string>(
28
+ item: SubMenuItem<T> | SelectableMenuItem<T>
29
+ ): string {
30
+ if (item.type === "submenu") {
31
+ return item.id;
32
+ }
33
+ return item.value;
34
+ }
35
+
36
+ export function ListMenu<T extends string>({
37
+ items,
38
+ selectedIds: selectedIdsProp,
39
+ onSelect,
40
+ renderRight,
41
+ className,
42
+ }: ListMenuProps<T>) {
43
+ const selectedIds = items
44
+ .flatMap((item) =>
45
+ isSelectableMenuItem(item) && item.checked ? [item.value] : []
46
+ )
47
+ .concat(selectedIdsProp as T[]);
48
+
49
+ let chunks = chunkBy(
50
+ items,
51
+ (a, b) => a.type !== "separator" && b.type !== "separator"
52
+ );
53
+
54
+ chunks = chunks.map((chunk) =>
55
+ chunk.filter((item) => item.type !== "separator")
56
+ );
57
+
58
+ chunks = chunks.filter((chunk) => chunk.length > 0);
59
+
60
+ const lists = chunks.map((chunk) => {
61
+ const regularItems = chunk.filter(
62
+ (item) => isSubMenuItem(item) || isSelectableMenuItem(item)
63
+ );
64
+
65
+ return (
66
+ <List
67
+ key={regularItems.map(getItemId).join("-")}
68
+ items={regularItems}
69
+ size="small"
70
+ itemClassName="n-rounded-sm"
71
+ className={className}
72
+ getId={getItemId}
73
+ getName={(item) => item.title.toString()}
74
+ selectedIds={selectedIds}
75
+ renderRight={renderRight}
76
+ renderThumbnail={({ item }) => (
77
+ <div className="n-flex n-flex-1 n-items-center n-justify-center n-bg-divider-subtle n-w-full n-h-full n-rounded-sm">
78
+ {renderIcon(item.icon)}
79
+ </div>
80
+ )}
81
+ renderDetail={(item) => isSubMenuItem(item) && <ArrowRightIcon />}
82
+ onClickItem={(itemId) => onSelect(itemId as T)}
83
+ />
84
+ );
85
+ });
86
+
87
+ return withSeparatorElements(
88
+ lists,
89
+ <Divider overflow={12} className="n-my-1" />
90
+ );
91
+ }