@noya-app/noya-designsystem 0.1.47 → 0.1.48

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 +10 -10
  2. package/CHANGELOG.md +10 -0
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.mts +205 -106
  5. package/dist/index.d.ts +205 -106
  6. package/dist/index.js +2108 -1589
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +2078 -1566
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +6 -7
  11. package/src/__tests__/validateDropIndicator.test.ts +263 -0
  12. package/src/components/ActionMenu.tsx +40 -0
  13. package/src/components/ActivityIndicator.tsx +7 -1
  14. package/src/components/Collection.tsx +12 -2
  15. package/src/components/Combobox.tsx +14 -1
  16. package/src/components/ComboboxMenu.tsx +14 -5
  17. package/src/components/Dialog.tsx +17 -12
  18. package/src/components/Drawer.tsx +98 -0
  19. package/src/components/Grid.tsx +57 -27
  20. package/src/components/GridView.tsx +39 -25
  21. package/src/components/InputField.tsx +87 -92
  22. package/src/components/Label.tsx +7 -7
  23. package/src/components/List.tsx +42 -16
  24. package/src/components/ListView.tsx +21 -17
  25. package/src/components/MediaThumbnail.tsx +117 -0
  26. package/src/components/SearchCompletionMenu.tsx +30 -16
  27. package/src/components/SelectMenu.tsx +19 -17
  28. package/src/components/Sortable.tsx +70 -44
  29. package/src/components/internal/Menu.tsx +38 -23
  30. package/src/components/internal/MenuViewport.tsx +7 -1
  31. package/src/components/internal/SelectItem.tsx +51 -27
  32. package/src/components/internal/__tests__/Menu.test.tsx +12 -0
  33. package/src/components/workspace/DrawerWorkspaceLayout.tsx +86 -0
  34. package/src/components/workspace/PanelWorkspaceLayout.tsx +102 -0
  35. package/src/components/{WorkspaceLayout.tsx → workspace/WorkspaceLayout.tsx} +109 -106
  36. package/src/components/workspace/types.ts +31 -0
  37. package/src/contexts/DialogContext.tsx +49 -34
  38. package/src/hooks/usePreservePanelSize.tsx +1 -5
  39. package/src/index.css +1 -0
  40. package/src/index.tsx +4 -1
  41. package/tailwind.config.ts +1 -0
@@ -1,10 +1,12 @@
1
- import React, { useImperativeHandle, useState } from "react";
1
+ import { useFileDropTarget } from "@noya-app/react-utils";
2
+ import React, { useImperativeHandle, useRef, useState } from "react";
2
3
  import {
3
4
  forwardRefGeneric,
4
5
  memoGeneric,
5
6
  } from "../../../noya-react-utils/src/utils/reactGenerics";
6
7
  import { cx } from "../utils/classNames";
7
8
  import { updateSelection } from "../utils/selection";
9
+ import { ActionMenu } from "./ActionMenu";
8
10
  import { CollectionProps, CollectionRef } from "./Collection";
9
11
  import { EditableText } from "./EditableText";
10
12
  import { GridView } from "./GridView";
@@ -35,15 +37,15 @@ export const getGridSize = (size: GridViewSize) => {
35
37
  },
36
38
  medium: {
37
39
  minColumnWidth: "220px",
38
- gap: 20,
40
+ gap: 30,
39
41
  },
40
42
  large: {
41
43
  minColumnWidth: "280px",
42
- gap: 24,
44
+ gap: 40,
43
45
  },
44
46
  xl: {
45
47
  minColumnWidth: "400px",
46
- gap: 32,
48
+ gap: 48,
47
49
  },
48
50
  };
49
51
 
@@ -83,13 +85,16 @@ export const Grid = memoGeneric(
83
85
  scrollable = false,
84
86
  // acceptsDrop,
85
87
  // onMoveItem,
86
- // onFilesDrop,
88
+ onFilesDrop,
87
89
  renamable,
90
+ onClickItem,
88
91
  onDoubleClickItem,
92
+ renderEmptyState,
89
93
  } = props;
90
94
 
91
95
  const [hoveredId, setHoveredId] = useState<string>();
92
96
  const [internalRenamingId, setRenamingId] = useState<string>();
97
+ const [dropdownOpenId, setDropdownOpenId] = useState<string>();
93
98
 
94
99
  useImperativeHandle(ref, () => ({
95
100
  editName: (id: string) => {
@@ -104,29 +109,35 @@ export const Grid = memoGeneric(
104
109
  if (!onSelectionChange) return;
105
110
 
106
111
  setRenamingId(undefined);
112
+
107
113
  const newSelectedIds = updateSelection(
108
114
  items.map(getId),
109
115
  selectedIds,
110
116
  itemId,
111
117
  event
112
118
  );
119
+
113
120
  onSelectionChange(newSelectedIds, event);
114
121
  };
115
122
 
116
- const handleMenuAction = (action: M) => {
123
+ const handleMenuAction = (action: M, actionItem: T) => {
117
124
  if (!onSelectMenuItem) return;
118
125
 
119
126
  const selectedItems = items.filter((item) =>
120
127
  selectedIds.includes(getId(item))
121
128
  );
122
- onSelectMenuItem(action, selectedItems);
129
+
130
+ onSelectMenuItem(
131
+ action,
132
+ selectedItems.length === 0 ? [actionItem] : selectedItems
133
+ );
123
134
  };
124
135
 
125
- // const fileDropTargetRef = useRef<HTMLDivElement>(null);
126
- // const { isDropTargetActive, dropTargetProps } = useFileDropTarget(
127
- // fileDropTargetRef,
128
- // onFilesDrop
129
- // );
136
+ const fileDropTargetRef = useRef<HTMLDivElement>(null);
137
+ const { isDropTargetActive, dropTargetProps } = useFileDropTarget(
138
+ fileDropTargetRef,
139
+ onFilesDrop
140
+ );
130
141
 
131
142
  const { minColumnWidth, gap } = getGridSize(size);
132
143
 
@@ -134,23 +145,45 @@ export const Grid = memoGeneric(
134
145
  <GridView.Root
135
146
  minColumnWidth={minColumnWidth}
136
147
  scrollable={scrollable}
137
- className={className}
138
148
  gap={gap}
149
+ className={cx(isDropTargetActive && "bg-primary-pastel", className)}
150
+ {...dropTargetProps}
151
+ ref={fileDropTargetRef}
139
152
  >
140
- <GridView.Section>
153
+ <GridView.Section renderEmptyState={renderEmptyState}>
141
154
  {items.map((item) => {
142
155
  const id = getId(item);
143
- const isHovered = currentHoveredId === id;
156
+ const isDropdownOpen = dropdownOpenId === id;
157
+ const isHovered =
158
+ testHoveredId === id || currentHoveredId === id || isDropdownOpen;
144
159
  const itemIsRenamable = getRenamable?.(item) ?? renamable;
145
160
  const isSelected = selectedIds.includes(id);
146
161
  const isRenaming = itemIsRenamable && onRename && renamingId === id;
147
162
  const isTestingRenaming = testRenamingId === id;
148
-
149
- const thumbnail = renderThumbnail?.(item, isSelected);
163
+ const thumbnail = renderThumbnail?.({
164
+ item,
165
+ selected: isSelected,
166
+ });
150
167
  const action =
151
168
  (isHovered || isSelected) &&
152
169
  !isRenaming &&
153
- renderAction?.(item, isSelected);
170
+ (typeof renderAction === "function"
171
+ ? renderAction(item, isSelected)
172
+ : renderAction === "menu" &&
173
+ menuItems && (
174
+ <ActionMenu
175
+ menuItems={menuItems}
176
+ onSelect={(action) => handleMenuAction(action, item)}
177
+ selected={isSelected}
178
+ onOpenChange={(open) => {
179
+ setDropdownOpenId(open ? id : undefined);
180
+
181
+ if (open && !isSelected) {
182
+ handleSelect(id);
183
+ }
184
+ }}
185
+ />
186
+ ));
154
187
 
155
188
  return (
156
189
  <GridView.Item<M>
@@ -196,6 +229,9 @@ export const Grid = memoGeneric(
196
229
  }
197
230
  loading={false}
198
231
  selected={isSelected}
232
+ onClick={() => {
233
+ onClickItem?.(id);
234
+ }}
199
235
  onSelect={(event) => {
200
236
  handleSelect(id, event);
201
237
  }}
@@ -208,24 +244,18 @@ export const Grid = memoGeneric(
208
244
  onDoubleClickItem?.(id);
209
245
  }}
210
246
  menuItems={menuItems}
211
- onSelectMenuItem={handleMenuAction}
247
+ onSelectMenuItem={(action) => handleMenuAction(action, item)}
212
248
  action={action}
213
- testHoveredId={testHoveredId}
214
249
  onMenuOpenChange={(open: boolean) => {
215
250
  if (open && !isSelected) {
216
- handleSelect(id, {
217
- shiftKey: false,
218
- altKey: false,
219
- metaKey: false,
220
- ctrlKey: false,
221
- });
251
+ handleSelect(id);
222
252
  }
223
253
  }}
224
254
  >
225
255
  {thumbnail && (
226
256
  <div
227
257
  className={cx(
228
- "rounded overflow-hidden flex-none flex items-center w-full justify-center aspect-square",
258
+ "rounded overflow-hidden flex-none flex w-full",
229
259
  isSelected && "text-primary"
230
260
  )}
231
261
  tabIndex={-1}
@@ -4,6 +4,7 @@ import React, {
4
4
  createContext,
5
5
  CSSProperties,
6
6
  ForwardedRef,
7
+ forwardRef,
7
8
  memo,
8
9
  ReactNode,
9
10
  useCallback,
@@ -93,7 +94,6 @@ interface ItemProps<MenuItemType extends string = string> {
93
94
  onMenuOpenChange?: (open: boolean) => void;
94
95
  style?: CSSProperties;
95
96
  action?: ReactNode;
96
- testHoveredId?: string;
97
97
  onKeyDown?: (event: React.KeyboardEvent) => void;
98
98
  hovered?: boolean;
99
99
  }
@@ -118,7 +118,6 @@ const GridViewItem = forwardRefGeneric(function GridViewItem<
118
118
  onMenuOpenChange,
119
119
  style,
120
120
  action,
121
- testHoveredId,
122
121
  onKeyDown,
123
122
  hovered = false,
124
123
  }: ItemProps<MenuItemType>,
@@ -128,8 +127,6 @@ const GridViewItem = forwardRefGeneric(function GridViewItem<
128
127
  onHoverChange,
129
128
  });
130
129
 
131
- const isHovered = testHoveredId ? testHoveredId === id : hovered;
132
-
133
130
  const { textPosition, bordered, disabled } = useContext(GridViewContext);
134
131
 
135
132
  const handleClick = useCallback(
@@ -162,26 +159,27 @@ const GridViewItem = forwardRefGeneric(function GridViewItem<
162
159
  <div
163
160
  className={cx(
164
161
  "flex flex-col relative focus:shadow-[0px_0px_0px_1px_var(--sidebar-background),_0px_0px_0px_3px_var(--primary)] focus:outline-none rounded p-2 -m-2",
162
+ "cursor-pointer",
163
+ "active:opacity-70",
165
164
  selected && "bg-primary-pastel",
166
- isHovered && "ring-1 ring-primary ring-inset"
165
+ disabled && "opacity-50",
166
+ hovered && "ring-1 ring-primary ring-inset opacity-85"
167
167
  )}
168
168
  id={id}
169
169
  ref={forwardedRef}
170
170
  {...hoverProps}
171
171
  tabIndex={disabled ? undefined : 0}
172
172
  onKeyDown={handleKeyDown}
173
+ onClick={handleClick}
174
+ onDoubleClick={onDoubleClick}
175
+ onContextMenu={onContextMenu}
173
176
  >
174
177
  <div
175
178
  className={cx(
176
179
  "flex items-center justify-center rounded",
177
- "cursor-pointer",
178
180
  bordered ? "border border-divider" : "border border-transparent",
179
- disabled ? "opacity-50" : "hover:opacity-85 active:opacity-70",
180
181
  selected ? "bg-primary-pastel" : "bg-input-background text-text"
181
182
  )}
182
- onClick={handleClick}
183
- onDoubleClick={onDoubleClick}
184
- onContextMenu={onContextMenu}
185
183
  style={style}
186
184
  >
187
185
  {children}
@@ -193,7 +191,7 @@ const GridViewItem = forwardRefGeneric(function GridViewItem<
193
191
  <ItemDescription>{subtitle || " "}</ItemDescription>
194
192
  </>
195
193
  )}
196
- {textPosition === "overlay" && isHovered && (title || subtitle) && (
194
+ {textPosition === "overlay" && hovered && (title || subtitle) && (
197
195
  <div className="absolute inset-0 flex flex-col justify-end items-start p-1 pointer-events-none truncate gap-0.5">
198
196
  {title && <ItemTitle showBackground>{title}</ItemTitle>}
199
197
  {subtitle && (
@@ -265,19 +263,27 @@ interface GridViewRootProps extends Partial<GridViewContextValue> {
265
263
  onClick?: () => void;
266
264
  scrollable?: boolean;
267
265
  className?: string;
266
+ onDragOver?: React.DragEventHandler<HTMLDivElement>;
267
+ onDragEnter?: React.DragEventHandler<HTMLDivElement>;
268
+ onDragLeave?: React.DragEventHandler<HTMLDivElement>;
269
+ onDrop?: React.DragEventHandler<HTMLDivElement>;
268
270
  }
269
271
 
270
- function GridViewRoot({
271
- minColumnWidth = "220px",
272
- gap = 20,
273
- children,
274
- scrollable,
275
- onClick,
276
- textPosition = "below",
277
- bordered = false,
278
- disabled = false,
279
- className,
280
- }: GridViewRootProps) {
272
+ const GridViewRoot = forwardRef(function GridViewRoot(
273
+ {
274
+ minColumnWidth = "220px",
275
+ gap = 20,
276
+ children,
277
+ scrollable,
278
+ onClick,
279
+ textPosition = "below",
280
+ bordered = false,
281
+ disabled = false,
282
+ className,
283
+ ...props
284
+ }: GridViewRootProps,
285
+ forwardedRef: ForwardedRef<HTMLDivElement>
286
+ ) {
281
287
  const handleClick = useCallback(
282
288
  (event: React.MouseEvent) => {
283
289
  event.stopPropagation();
@@ -305,9 +311,11 @@ function GridViewRoot({
305
311
  onClick={handleClick}
306
312
  className={cx(
307
313
  "flex flex-col -mx-3 -mb-1",
308
- scrollable ? "flex-1" : "flex-[0_0_auto]",
314
+ scrollable ? "basis-0 min-h-0" : "flex-none",
309
315
  className
310
316
  )}
317
+ {...props}
318
+ ref={forwardedRef}
311
319
  >
312
320
  {scrollable ? (
313
321
  <ScrollArea>
@@ -319,14 +327,16 @@ function GridViewRoot({
319
327
  </div>
320
328
  </GridViewContext.Provider>
321
329
  );
322
- }
330
+ });
323
331
 
324
332
  function GridViewSection({
325
333
  children,
326
334
  className,
335
+ renderEmptyState,
327
336
  }: {
328
337
  children?: ReactNode;
329
338
  className?: string;
339
+ renderEmptyState?: () => ReactNode;
330
340
  }) {
331
341
  const { minColumnWidth, gap } = useContext(GridViewContext);
332
342
 
@@ -335,7 +345,11 @@ function GridViewSection({
335
345
  gap: `${gap}px`,
336
346
  };
337
347
 
338
- return (
348
+ const isEmpty = React.Children.count(children) === 0;
349
+
350
+ return isEmpty && renderEmptyState ? (
351
+ renderEmptyState()
352
+ ) : (
339
353
  <div className={cx("grid text-text", className)} style={styles}>
340
354
  {children}
341
355
  </div>