@noya-app/noya-designsystem 0.1.64 → 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 (115) hide show
  1. package/.turbo/turbo-build.log +13 -10
  2. package/CHANGELOG.md +12 -0
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.mts +501 -228
  5. package/dist/index.d.ts +501 -228
  6. package/dist/index.js +6738 -2571
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +6697 -2499
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +6 -6
  11. package/src/components/ActivityIndicator.tsx +9 -4
  12. package/src/components/ActivityLog.tsx +197 -0
  13. package/src/components/AnimatePresence.tsx +37 -22
  14. package/src/components/Avatar.tsx +42 -25
  15. package/src/components/Banner.tsx +57 -11
  16. package/src/components/BaseToolbar.tsx +29 -7
  17. package/src/components/Breadcrumbs.tsx +2 -2
  18. package/src/components/Button.tsx +23 -24
  19. package/src/components/Checkbox.tsx +6 -35
  20. package/src/components/Chip.tsx +45 -33
  21. package/src/components/Collection.tsx +16 -11
  22. package/src/components/ColorSwatch.tsx +3 -3
  23. package/src/components/ColorSwatchControl.tsx +2 -2
  24. package/src/components/Combobox.tsx +6 -6
  25. package/src/components/ComboboxMenu.tsx +6 -5
  26. package/src/components/CommandPalette.tsx +1 -1
  27. package/src/components/Dialog.tsx +6 -12
  28. package/src/components/Divider.tsx +8 -8
  29. package/src/components/DraggableMenuButton.tsx +9 -5
  30. package/src/components/Drawer.tsx +5 -5
  31. package/src/components/DropdownMenu.tsx +1 -1
  32. package/src/components/EditableText.tsx +2 -2
  33. package/src/components/Fade.tsx +13 -13
  34. package/src/components/FileUploadIndicator.tsx +11 -11
  35. package/src/components/FloatingWindow.tsx +2 -2
  36. package/src/components/Grid.tsx +18 -11
  37. package/src/components/GridView.tsx +32 -25
  38. package/src/components/IVirtualizedList.tsx +5 -0
  39. package/src/components/Icons.tsx +24 -4
  40. package/src/components/InputField.tsx +19 -19
  41. package/src/components/InspectorContainer.tsx +2 -2
  42. package/src/components/InspectorPrimitives.tsx +15 -9
  43. package/src/components/Label.tsx +4 -3
  44. package/src/components/LabeledElementView.tsx +3 -3
  45. package/src/components/LabeledField.tsx +6 -6
  46. package/src/components/List.tsx +103 -45
  47. package/src/components/ListMenu.tsx +91 -0
  48. package/src/components/ListNavigator.tsx +106 -0
  49. package/src/components/ListView.tsx +20 -1158
  50. package/src/components/MediaThumbnail.tsx +56 -15
  51. package/src/components/Message.tsx +5 -5
  52. package/src/components/Navigator.tsx +461 -0
  53. package/src/components/NoyaLogo.tsx +1 -1
  54. package/src/components/Popover.tsx +9 -5
  55. package/src/components/Progress.tsx +19 -9
  56. package/src/components/ResizableContainer.tsx +187 -0
  57. package/src/components/RingProgress.tsx +128 -0
  58. package/src/components/ScrollArea.tsx +4 -4
  59. package/src/components/ScrollableSidebar.tsx +10 -0
  60. package/src/components/SearchCompletionMenu.tsx +3 -3
  61. package/src/components/Section.tsx +107 -57
  62. package/src/components/SegmentedControl.tsx +29 -29
  63. package/src/components/SelectMenu.tsx +13 -20
  64. package/src/components/SelectionToolbar.tsx +4 -3
  65. package/src/components/Slider.tsx +9 -9
  66. package/src/components/Spacer.tsx +2 -2
  67. package/src/components/StackNavigator.tsx +378 -0
  68. package/src/components/Stepper.tsx +88 -0
  69. package/src/components/Switch.tsx +4 -4
  70. package/src/components/Tabs.tsx +3 -3
  71. package/src/components/Text.tsx +23 -13
  72. package/src/components/TextArea.tsx +3 -3
  73. package/src/components/Toast.tsx +4 -4
  74. package/src/components/Toolbar.tsx +50 -9
  75. package/src/components/ToolbarDrawer.tsx +2 -2
  76. package/src/components/Tooltip.tsx +1 -1
  77. package/src/components/TreeView.tsx +11 -4
  78. package/src/components/UserPointer.tsx +4 -4
  79. package/src/components/ai-assistant/AIAssistantLayout.tsx +8 -8
  80. package/src/components/blocks/ImageBlockComponent.tsx +13 -7
  81. package/src/components/connected-users-menu/ConnectedUsersMenuLayout.tsx +4 -4
  82. package/src/components/file-explorer/FileExplorerLayout.tsx +20 -8
  83. package/src/components/internal/Checkmark.tsx +1 -1
  84. package/src/components/internal/Menu.tsx +22 -18
  85. package/src/components/internal/MenuViewport.tsx +13 -6
  86. package/src/components/internal/SelectItem.tsx +3 -3
  87. package/src/components/internal/TextInput.tsx +5 -1
  88. package/src/components/listView/ListViewContexts.tsx +68 -0
  89. package/src/components/listView/ListViewEditableRowTitle.tsx +55 -0
  90. package/src/components/listView/ListViewRoot.tsx +521 -0
  91. package/src/components/listView/ListViewRow.tsx +475 -0
  92. package/src/components/listView/ListViewRowTitle.tsx +21 -0
  93. package/src/components/listView/types.ts +11 -0
  94. package/src/components/pipeline/PipelineResultLayout.tsx +6 -6
  95. package/src/components/sorting/DragRegistration.tsx +2 -1
  96. package/src/components/sorting/SharedDragProvider.tsx +91 -9
  97. package/src/components/sorting/Sortable.tsx +71 -17
  98. package/src/components/workspace/DrawerWorkspaceLayout.tsx +5 -5
  99. package/src/components/workspace/PanelWorkspaceLayout.tsx +65 -49
  100. package/src/components/workspace/VerticalTabMenu.tsx +25 -15
  101. package/src/components/workspace/WorkspaceLayout.tsx +30 -21
  102. package/src/contexts/DialogContext.tsx +17 -5
  103. package/src/hooks/useIndent.ts +3 -8
  104. package/src/index.css +24 -10
  105. package/src/index.tsx +10 -5
  106. package/src/utils/classNames.ts +52 -6
  107. package/src/utils/editableBlockStyles.ts +2 -2
  108. package/src/utils/formatByteSize.ts +1 -0
  109. package/src/utils/inputs.ts +8 -6
  110. package/src/utils/sketchColor.ts +0 -34
  111. package/tailwind.config.ts +1 -0
  112. package/src/components/FillInputField.tsx +0 -37
  113. package/src/components/FillPreviewBackground.tsx +0 -134
  114. package/src/components/GradientPicker.tsx +0 -90
  115. package/src/utils/getGradientBackground.tsx +0 -31
@@ -43,14 +43,14 @@ export const InspectorContainer = memo(
43
43
  {header}
44
44
  {children ? (
45
45
  <ScrollArea>
46
- <div className="flex flex-col relative">
46
+ <div className="n-flex n-flex-col n-relative">
47
47
  {showDividers
48
48
  ? withSeparatorElements(children, <Divider />)
49
49
  : children}
50
50
  </div>
51
51
  </ScrollArea>
52
52
  ) : fallback ? (
53
- <div className="flex flex-col relative h-full">{fallback}</div>
53
+ <div className="n-flex n-flex-col n-relative n-h-full">{fallback}</div>
54
54
  ) : null}
55
55
  </div>
56
56
  );
@@ -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-none 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
  {
@@ -152,9 +152,9 @@ export const LabeledElementView = React.memo(function LabeledElementView({
152
152
  }, [refs, labelElements]);
153
153
 
154
154
  return (
155
- <div className="flex flex-1 flex-col relative" ref={containerRef}>
156
- <div className="flex flex-1 items-center">{children}</div>
157
- <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">
158
158
  {labelElements}
159
159
  </div>
160
160
  </div>
@@ -15,9 +15,9 @@ import { Label } from "./Label";
15
15
 
16
16
  export const labeledFieldStyles = (labelType?: LabelType) =>
17
17
  cx(
18
- "flex flex-row flex-1 flex-wrap items-center gap-x-1.5",
19
- labelType === "end" && "flex-row-reverse justify-end",
20
- 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"
21
21
  );
22
22
 
23
23
  const labelTypeInset: LabelTypeContextValue = {
@@ -96,7 +96,7 @@ export const LabeledField = memo(function LabeledField({
96
96
  {children}
97
97
  </LabelTypeContext.Provider>
98
98
  ) : (
99
- <div className={cx("flex relative flex-1", className)} style={style}>
99
+ <div className={cx("n-flex n-relative n-flex-1", className)} style={style}>
100
100
  {Boolean(indentLevel) && <div style={indentStyle} />}
101
101
  <div className={labeledFieldClasses}>
102
102
  {labelType !== "none" && (
@@ -106,7 +106,7 @@ export const LabeledField = memo(function LabeledField({
106
106
  // and when start/end position with textarea it aligns label to the top of the field
107
107
  className={
108
108
  labelType === "start" || labelType === "end"
109
- ? "self-start"
109
+ ? "n-self-start"
110
110
  : undefined
111
111
  }
112
112
  style={labelStyle}
@@ -116,7 +116,7 @@ export const LabeledField = memo(function LabeledField({
116
116
  </Label>
117
117
  )}
118
118
  {labelType === "start" ? (
119
- <div className="flex-auto flex" style={fieldStyle}>
119
+ <div className="n-flex-auto n-flex" style={fieldStyle}>
120
120
  {children}
121
121
  </div>
122
122
  ) : (
@@ -5,7 +5,13 @@ import {
5
5
  memoGeneric,
6
6
  useFileDropTarget,
7
7
  } from "@noya-app/react-utils";
8
- import React, { memo, useImperativeHandle, useRef, useState } from "react";
8
+ import React, {
9
+ memo,
10
+ useImperativeHandle,
11
+ useMemo,
12
+ useRef,
13
+ useState,
14
+ } from "react";
9
15
  import { useLinkComponent } from "../contexts/LinkComponentContext";
10
16
  import { cssVars } from "../theme";
11
17
  import { cx } from "../utils/classNames";
@@ -16,7 +22,8 @@ import {
16
22
  CollectionProps,
17
23
  CollectionRef,
18
24
  } from "./Collection";
19
- import { ListView, ListViewItemInfo } from "./ListView";
25
+ import { ListView } from "./ListView";
26
+ import { ListViewItemInfo } from "./listView/ListViewRoot";
20
27
  import { TreeView } from "./TreeView";
21
28
 
22
29
  const cssVarOverrides = {
@@ -47,7 +54,10 @@ export const List = memoGeneric(
47
54
  renderThumbnail,
48
55
  renderAction,
49
56
  renderDetail,
57
+ renderRight,
50
58
  onSelectionChange,
59
+ itemStyle: itemStyleProp,
60
+ itemClassName: itemClassNameProp,
51
61
  itemRoleDescription = "clickable item",
52
62
  detailPosition = "end",
53
63
  size = "medium",
@@ -67,9 +77,11 @@ export const List = memoGeneric(
67
77
  getDropTargetParentIndex,
68
78
  getPlaceholder,
69
79
  onClickItem,
80
+ onClick,
70
81
  dragIndicatorStyle,
71
82
  sortableId,
72
83
  sharedDragProps,
84
+ virtualized,
73
85
  } = props;
74
86
 
75
87
  const [internalHoveredId, setHoveredId] = useState<string>();
@@ -77,6 +89,16 @@ export const List = memoGeneric(
77
89
  const [dropdownOpenId, setDropdownOpenId] = useState<string>();
78
90
  const hoveredId = internalHoveredId ?? testHoveredId;
79
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
+ );
80
102
 
81
103
  const handleSelect = (itemId: string, event?: ListView.ClickInfo) => {
82
104
  if (!onSelectionChange) return;
@@ -124,11 +146,12 @@ export const List = memoGeneric(
124
146
  variant="bare"
125
147
  className={cx(
126
148
  className,
127
- isDropTargetActive && "bg-selected-list-item-background",
128
- scrollable && "basis-0 min-h-0"
149
+ isDropTargetActive && "n-bg-selected-list-item-background",
150
+ scrollable && "n-basis-0 n-min-h-0"
129
151
  )}
130
152
  containerRef={fileDropTargetRef}
131
153
  scrollable={scrollable}
154
+ virtualized={virtualized}
132
155
  data={items}
133
156
  keyExtractor={getId}
134
157
  role="listbox"
@@ -142,6 +165,8 @@ export const List = memoGeneric(
142
165
  getDropTargetParentIndex={getDropTargetParentIndex}
143
166
  sharedDragProps={sharedDragProps}
144
167
  sortableId={sortableId}
168
+ onPress={onClick}
169
+ itemHeight={itemHeightMap[size]}
145
170
  {...dropTargetProps}
146
171
  renderItem={(
147
172
  item: T,
@@ -198,9 +223,9 @@ export const List = memoGeneric(
198
223
  const end =
199
224
  action ||
200
225
  (detailPosition === "end" && renderDetail?.(item, isSelected));
226
+ const right = renderRight?.(item, isSelected);
201
227
  const below =
202
228
  detailPosition === "below" && renderDetail?.(item, isSelected);
203
-
204
229
  return (
205
230
  <ViewComponent.Row
206
231
  as={typeof href === "string" ? LinkComponent : undefined}
@@ -212,12 +237,12 @@ export const List = memoGeneric(
212
237
  aria-roledescription={itemRoleDescription}
213
238
  aria-label={getName(item)}
214
239
  aria-selected={isSelected}
215
- className={cx("cursor-pointer rounded px-1", pyMap[size])}
240
+ className={itemClassName}
216
241
  testRelativeDropPosition={
217
242
  testShowDropIndicatorId === id ? "inside" : undefined
218
243
  }
219
- chevronClassName={cx(expandable && "relative left-2")}
220
- style={cssVarOverrides as React.CSSProperties}
244
+ chevronClassName={cx(expandable && "n-relative n-left-2")}
245
+ style={itemStyle}
221
246
  hovered={isHovered}
222
247
  selected={isSelected}
223
248
  expanded={expanded}
@@ -228,6 +253,7 @@ export const List = memoGeneric(
228
253
  onPress={(e: ListView.ClickInfo) => {
229
254
  handleSelect(id, e);
230
255
  onClickItem?.(id);
256
+ e.stopPropagation();
231
257
  }}
232
258
  onDoubleClick={() => {
233
259
  onDoubleClickItem?.(id);
@@ -255,24 +281,24 @@ export const List = memoGeneric(
255
281
  dragIndicatorStyle={dragIndicatorStyle}
256
282
  >
257
283
  <div
258
- 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])}
259
285
  >
260
286
  {thumbnail && (
261
287
  <div
262
288
  className={cx(
263
- "overflow-hidden flex-none flex items-center justify-center",
289
+ "n-overflow-hidden n-flex-none n-flex n-items-center n-justify-center",
264
290
  thumbnailSize === "auto" && thumbnailSizeMap[size],
265
- isSelected && "text-selected-list-item-text",
266
- size === "small" ? "rounded-sm" : "rounded"
291
+ isSelected && "n-text-selected-list-item-text",
292
+ roundedMap[size]
267
293
  )}
268
294
  tabIndex={-1}
269
295
  >
270
296
  {thumbnail}
271
297
  </div>
272
298
  )}
273
- <div className="flex flex-col flex-1 w-0">
274
- <div className="flex items-center min-h-input-height flex-1 gap-2">
275
- <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}>
276
302
  {isRenaming ? (
277
303
  <ViewComponent.EditableRowTitle
278
304
  value={getName(item)}
@@ -291,13 +317,19 @@ export const List = memoGeneric(
291
317
  <ViewComponent.RowTitle
292
318
  className={cx(
293
319
  fontStyleMap[size],
294
- isSelected && "text-selected-list-item-text"
320
+ isSelected && "n-text-selected-list-item-text"
295
321
  )}
296
322
  >
297
323
  {getName(item)}
298
324
  </ViewComponent.RowTitle>
299
325
  )}
300
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
+ )}
301
333
  {end && detailPosition === "end" && (
302
334
  <DetailContainer selected={isSelected}>
303
335
  {end}
@@ -310,6 +342,12 @@ export const List = memoGeneric(
310
342
  </DetailContainer>
311
343
  )}
312
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
+ )}
313
351
  {end && detailPosition === "below" && (
314
352
  <DetailContainer selected={isSelected}>{end}</DetailContainer>
315
353
  )}
@@ -331,7 +369,7 @@ const DetailContainer = memo(function DetailContainer({
331
369
  }) {
332
370
  return (
333
371
  <div
334
- 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")}
335
373
  tabIndex={-1}
336
374
  >
337
375
  {children}
@@ -340,41 +378,61 @@ const DetailContainer = memo(function DetailContainer({
340
378
  });
341
379
 
342
380
  const pyMap: Record<CollectionItemSize, string> = {
343
- xxs: "py-0.5",
344
- xs: "py-0.5",
345
- small: "py-0.5",
346
- medium: "py-1",
347
- mediumLarge: "py-1.5",
348
- large: "py-2",
349
- 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",
350
388
  };
351
389
 
352
390
  const thumbnailSizeMap: Record<CollectionItemSize, string> = {
353
- xxs: "w-5 h-5",
354
- xs: "w-5 h-5",
355
- small: "w-5 h-5",
356
- medium: "w-8 h-8",
357
- mediumLarge: "w-12 h-12",
358
- large: "w-16 h-16",
359
- 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",
360
408
  };
361
409
 
362
410
  const rowGapMap: Record<CollectionItemSize, string> = {
363
- xxs: "gap-2",
364
- xs: "gap-2",
365
- small: "gap-2",
366
- medium: "gap-2",
367
- mediumLarge: "gap-3",
368
- large: "gap-3",
369
- 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",
370
418
  };
371
419
 
372
420
  const fontStyleMap: Record<CollectionItemSize, string> = {
373
- xxs: "font-medium text-list-small",
374
- xs: "font-medium text-list-small",
375
- small: "font-medium text-list-small",
376
- medium: "font-medium",
377
- mediumLarge: "font-medium",
378
- large: "font-medium",
379
- 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,
380
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
+ }
@@ -0,0 +1,106 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { isSubMenuItem, SubMenuItem } from "./internal/Menu";
5
+ import { ListMenu, ListMenuProps } from "./ListMenu";
6
+ import { createNavigator, NavigatorProps } from "./Navigator";
7
+
8
+ export type ListNavigatorProps<T extends string> = Pick<
9
+ ListMenuProps<T>,
10
+ "items" | "renderRight" | "selectedIds"
11
+ > &
12
+ Pick<NavigatorProps<any, any>, "className" | "style"> & {
13
+ title?: React.ReactNode;
14
+ onSelect: (item: T, { popToRoot }: { popToRoot: () => void }) => void;
15
+ renderHeader?: ({
16
+ title,
17
+ onBack,
18
+ }: {
19
+ title?: React.ReactNode;
20
+ onBack?: () => void;
21
+ }) => React.ReactNode;
22
+ };
23
+
24
+ const ListNavigatorInternal = createNavigator<{
25
+ screen: { id: string | undefined };
26
+ }>();
27
+
28
+ export function ListNavigator<T extends string>({
29
+ title,
30
+ items,
31
+ onSelect,
32
+ className,
33
+ style,
34
+ renderRight,
35
+ renderHeader,
36
+ selectedIds,
37
+ }: ListNavigatorProps<T>) {
38
+ return (
39
+ <ListNavigatorInternal
40
+ initialRoute={{ name: "screen", params: { id: undefined } }}
41
+ renderHeader={
42
+ renderHeader &&
43
+ (({ route, onBack }) => {
44
+ const parentItem = items.find(
45
+ (item): item is SubMenuItem<T> =>
46
+ isSubMenuItem(item) && item.id === route.params.id
47
+ );
48
+
49
+ return renderHeader({
50
+ title: parentItem?.title ?? title,
51
+ onBack: parentItem ? onBack : undefined,
52
+ });
53
+ })
54
+ }
55
+ // Always show header if there is a renderHeader or title.
56
+ // Otherwise, show header if there is more than one item in the stack.
57
+ showHeader={
58
+ Boolean(renderHeader) ||
59
+ Boolean(title) ||
60
+ (({ stack }) => stack.length > 1)
61
+ }
62
+ cardClassName="n-py-1"
63
+ className={className}
64
+ style={style}
65
+ >
66
+ <ListNavigatorInternal.Screen
67
+ name="screen"
68
+ title={({ route }) => {
69
+ const nestedItem = items.find(
70
+ (item): item is SubMenuItem<T> =>
71
+ isSubMenuItem(item) && item.id === route.params.id
72
+ );
73
+
74
+ return nestedItem?.title ?? title ?? null;
75
+ }}
76
+ >
77
+ {({ route, navigation }) => {
78
+ const nestedItem = items.find(
79
+ (item): item is SubMenuItem<T> =>
80
+ isSubMenuItem(item) && item.id === route.params.id
81
+ );
82
+
83
+ return (
84
+ <ListMenu
85
+ items={nestedItem?.items ?? items}
86
+ selectedIds={selectedIds}
87
+ onSelect={(itemId) => {
88
+ const nestedItem = items.find(
89
+ (item): item is SubMenuItem<T> =>
90
+ isSubMenuItem(item) && item.id === itemId
91
+ );
92
+
93
+ if (nestedItem) {
94
+ navigation.push("screen", { id: nestedItem.id });
95
+ } else {
96
+ onSelect(itemId as T, { popToRoot: () => navigation.pop() });
97
+ }
98
+ }}
99
+ renderRight={renderRight}
100
+ />
101
+ );
102
+ }}
103
+ </ListNavigatorInternal.Screen>
104
+ </ListNavigatorInternal>
105
+ );
106
+ }