@noya-app/noya-designsystem 0.1.48 → 0.1.50

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 (55) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +19 -0
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.mts +347 -173
  5. package/dist/index.d.ts +347 -173
  6. package/dist/index.js +3602 -2811
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +4761 -3993
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +6 -5
  11. package/src/__tests__/validateDropIndicator.test.ts +4 -4
  12. package/src/components/ActionMenu.tsx +15 -4
  13. package/src/components/Avatar.tsx +2 -2
  14. package/src/components/Banner.tsx +29 -0
  15. package/src/components/BaseToolbar.tsx +2 -2
  16. package/src/components/Button.tsx +2 -2
  17. package/src/components/Checkbox.tsx +2 -2
  18. package/src/components/Chip.tsx +15 -14
  19. package/src/components/Collection.tsx +25 -2
  20. package/src/components/Combobox.tsx +22 -23
  21. package/src/components/ComboboxMenu.tsx +1 -1
  22. package/src/components/Dialog.tsx +2 -2
  23. package/src/components/DimensionInput.tsx +14 -12
  24. package/src/components/EditableText.tsx +3 -1
  25. package/src/components/FillInputField.tsx +2 -2
  26. package/src/components/Grid.tsx +131 -113
  27. package/src/components/GridView.tsx +36 -18
  28. package/src/components/InputField.tsx +116 -171
  29. package/src/components/Label.tsx +14 -73
  30. package/src/components/LabeledField.tsx +13 -2
  31. package/src/components/List.tsx +106 -47
  32. package/src/components/ListView.tsx +52 -31
  33. package/src/components/MediaThumbnail.tsx +14 -6
  34. package/src/components/Message.tsx +8 -8
  35. package/src/components/NoyaLogo.tsx +41 -0
  36. package/src/components/Popover.tsx +9 -6
  37. package/src/components/Section.tsx +172 -0
  38. package/src/components/SegmentedControl.tsx +1 -1
  39. package/src/components/SelectMenu.tsx +9 -4
  40. package/src/components/Slider.tsx +16 -7
  41. package/src/components/Sortable.tsx +186 -47
  42. package/src/components/UserPointer.tsx +1 -1
  43. package/src/components/ai-assistant/AIAssistantLayout.tsx +102 -0
  44. package/src/components/connected-users-menu/ConnectedUsersMenuLayout.tsx +71 -0
  45. package/src/components/file-explorer/FileExplorerLayout.tsx +71 -0
  46. package/src/components/internal/Menu.tsx +20 -10
  47. package/src/components/internal/SelectItem.tsx +3 -2
  48. package/src/components/pipeline/PipelineResultLayout.tsx +32 -0
  49. package/src/index.css +118 -112
  50. package/src/index.tsx +9 -0
  51. package/src/theme/index.ts +2 -0
  52. package/src/utils/classNames.ts +76 -3
  53. package/src/utils/inputs.ts +21 -0
  54. package/src/utils/moveTreeItem.ts +99 -0
  55. package/tailwind.config.ts +82 -75
@@ -19,17 +19,18 @@ import {
19
19
  } from "@dnd-kit/sortable";
20
20
  import { memoGeneric } from "@noya-app/react-utils";
21
21
  import * as React from "react";
22
+ import { memo } from "react";
22
23
  import { createPortal } from "react-dom";
23
24
 
24
25
  export type RelativeDropPosition = "above" | "below" | "inside";
25
26
 
26
27
  export type DropValidator = (
27
28
  sourceIndex: number,
28
- destinationIndex: number,
29
+ targetIndex: number,
29
30
  position: RelativeDropPosition
30
31
  ) => boolean;
31
32
 
32
- export const normalizeListDestinationIndex = (
33
+ export const normalizeListTargetIndex = (
33
34
  index: number,
34
35
  position: "above" | "below"
35
36
  ): number => {
@@ -38,12 +39,12 @@ export const normalizeListDestinationIndex = (
38
39
 
39
40
  export const defaultAcceptsDrop: DropValidator = (
40
41
  sourceIndex,
41
- destinationIndex,
42
+ targetIndex,
42
43
  position
43
44
  ) => {
44
45
  if (position === "inside") return false;
45
46
 
46
- const normalized = normalizeListDestinationIndex(destinationIndex, position);
47
+ const normalized = normalizeListTargetIndex(targetIndex, position);
47
48
 
48
49
  if (sourceIndex === normalized || sourceIndex + 1 === normalized) {
49
50
  return false;
@@ -52,18 +53,22 @@ export const defaultAcceptsDrop: DropValidator = (
52
53
  return true;
53
54
  };
54
55
 
55
- const SortableItemContext = React.createContext<{
56
+ export type SortableItemContextProps = {
56
57
  keys: UniqueIdentifier[];
57
58
  acceptsDrop: DropValidator;
58
59
  axis: "x" | "y";
59
60
  position: { x: number; y: number };
60
61
  setActivatorEvent: (event: PointerEvent) => void;
61
- }>({
62
+ getDropTargetParentIndex?: (index: number) => number | undefined;
63
+ };
64
+
65
+ const SortableItemContext = React.createContext<SortableItemContextProps>({
62
66
  keys: [],
63
67
  acceptsDrop: defaultAcceptsDrop,
64
68
  axis: "y",
65
69
  position: { x: 0, y: 0 },
66
70
  setActivatorEvent: () => {},
71
+ getDropTargetParentIndex: undefined,
67
72
  });
68
73
 
69
74
  type ValidateDropIndicatorParams = {
@@ -92,42 +97,67 @@ export function validateDropIndicator({
92
97
 
93
98
  const acceptsDropInside = acceptsDrop(activeIndex, overIndex, "inside");
94
99
 
95
- // If cursor is in the middle third of the element (between 1/3 and 2/3)
96
- // and dropping inside is allowed, show "inside" indicator
100
+ // Drop into the middle third if possible
97
101
  if (
98
- offsetStart >= elementStart + elementSize / 3 &&
99
- offsetStart <= elementStart + (elementSize * 2) / 3 &&
102
+ isContainedInMiddleThird(elementStart, elementSize, offsetStart) &&
100
103
  acceptsDropInside
101
- )
104
+ ) {
102
105
  return "inside";
106
+ }
107
+
108
+ const containingHalf = getContainingHalf(
109
+ elementStart,
110
+ elementSize,
111
+ offsetStart
112
+ );
103
113
 
104
- // Are we over the top or bottom half of the element?
105
- const indicator =
106
- offsetStart < elementStart + elementSize / 2 ? "above" : "below";
107
- // Drop above or below if possible, falling back to inside
108
- return acceptsDrop(activeIndex, overIndex, indicator)
109
- ? indicator
110
- : acceptsDropInside
114
+ // Drop above or below if possible
115
+ const acceptedHalf = acceptsDrop(activeIndex, overIndex, containingHalf);
116
+
117
+ return acceptedHalf
118
+ ? containingHalf
119
+ : // Lastly, check if inside but not middle third
120
+ acceptsDropInside
111
121
  ? "inside"
112
122
  : undefined;
113
123
  }
114
124
 
125
+ function isContainedInMiddleThird(
126
+ elementStart: number,
127
+ elementSize: number,
128
+ offsetStart: number
129
+ ): boolean {
130
+ return (
131
+ offsetStart >= elementStart + elementSize / 3 &&
132
+ offsetStart <= elementStart + (elementSize * 2) / 3
133
+ );
134
+ }
135
+
136
+ function getContainingHalf(
137
+ elementStart: number,
138
+ elementSize: number,
139
+ offsetStart: number
140
+ ): "above" | "below" {
141
+ if (offsetStart < elementStart + elementSize / 2) return "above";
142
+ return "below";
143
+ }
144
+
115
145
  /* ----------------------------------------------------------------------------
116
146
  * Item
117
147
  * ------------------------------------------------------------------------- */
118
148
 
119
149
  type UseSortableReturnType = ReturnType<typeof useSortable>;
120
150
 
151
+ type ItemChildrenProps<T> = {
152
+ ref: React.Ref<T>;
153
+ relativeDropPosition?: RelativeDropPosition;
154
+ style?: React.CSSProperties;
155
+ } & UseSortableReturnType["attributes"];
156
+
121
157
  interface ItemProps<T> {
122
158
  id: UniqueIdentifier;
123
159
  disabled?: boolean;
124
- children: (
125
- props: {
126
- ref: React.Ref<T>;
127
- relativeDropPosition?: RelativeDropPosition;
128
- style?: React.CSSProperties;
129
- } & UseSortableReturnType["attributes"]
130
- ) => JSX.Element;
160
+ children: (props: ItemChildrenProps<T>) => JSX.Element;
131
161
  }
132
162
 
133
163
  function SortableItem<T extends HTMLElement>({
@@ -135,8 +165,14 @@ function SortableItem<T extends HTMLElement>({
135
165
  disabled,
136
166
  children,
137
167
  }: ItemProps<T>) {
138
- const { keys, position, acceptsDrop, setActivatorEvent, axis } =
139
- React.useContext(SortableItemContext);
168
+ const {
169
+ keys,
170
+ position,
171
+ acceptsDrop,
172
+ setActivatorEvent,
173
+ axis,
174
+ getDropTargetParentIndex,
175
+ } = React.useContext(SortableItemContext);
140
176
  const sortable = useSortable({ id, disabled });
141
177
  const { activatorEvent } = useDndContext();
142
178
 
@@ -147,6 +183,7 @@ function SortableItem<T extends HTMLElement>({
147
183
  setNodeRef,
148
184
  isDragging,
149
185
  index,
186
+ activeIndex,
150
187
  overIndex,
151
188
  over,
152
189
  } = sortable;
@@ -161,23 +198,40 @@ function SortableItem<T extends HTMLElement>({
161
198
  const offsetTop = eventY + position.y;
162
199
 
163
200
  const ref = React.useCallback((node: T) => setNodeRef(node), [setNodeRef]);
201
+ const parentIndex =
202
+ overIndex === -1 ? undefined : getDropTargetParentIndex?.(overIndex);
203
+ const isValidParent =
204
+ parentIndex === undefined || parentIndex === -1
205
+ ? false
206
+ : acceptsDrop(activeIndex, parentIndex, "inside");
207
+ const overIdAcceptsDrop =
208
+ overIndex === -1
209
+ ? undefined
210
+ : acceptsDrop(activeIndex, overIndex, "inside");
211
+
212
+ const relativeDropPosition =
213
+ index >= 0 && index === overIndex && !isDragging && active && over
214
+ ? validateDropIndicator({
215
+ acceptsDrop,
216
+ keys,
217
+ activeId: active.id,
218
+ overId: over.id,
219
+ offsetStart: axis === "x" ? offsetLeft : offsetTop,
220
+ elementStart: axis === "x" ? over.rect.left : over.rect.top,
221
+ elementSize: axis === "x" ? over.rect.width : over.rect.height,
222
+ })
223
+ : undefined;
224
+
225
+ const showDragInsideIndicatorOnParent =
226
+ !overIdAcceptsDrop && isValidParent && parentIndex === index;
164
227
 
165
228
  return children({
166
229
  ref,
167
230
  ...attributes,
168
231
  ...listeners,
169
232
  relativeDropPosition:
170
- index >= 0 && index === overIndex && !isDragging && active && over
171
- ? validateDropIndicator({
172
- acceptsDrop,
173
- keys,
174
- activeId: active.id,
175
- overId: over.id,
176
- offsetStart: axis === "x" ? offsetLeft : offsetTop,
177
- elementStart: axis === "x" ? over.rect.left : over.rect.top,
178
- elementSize: axis === "x" ? over.rect.width : over.rect.height,
179
- })
180
- : undefined,
233
+ relativeDropPosition ??
234
+ (showDragInsideIndicatorOnParent ? "inside" : undefined),
181
235
  });
182
236
  }
183
237
 
@@ -191,11 +245,12 @@ interface RootProps {
191
245
  renderOverlay?: (index: number) => React.ReactNode;
192
246
  onMoveItem?: (
193
247
  sourceIndex: number,
194
- destinationIndex: number,
248
+ targetIndex: number,
195
249
  position: RelativeDropPosition
196
250
  ) => void;
197
- acceptsDrop?: DropValidator;
198
- axis?: "x" | "y";
251
+ acceptsDrop?: SortableItemContextProps["acceptsDrop"];
252
+ axis?: SortableItemContextProps["axis"];
253
+ getDropTargetParentIndex?: SortableItemContextProps["getDropTargetParentIndex"];
199
254
  }
200
255
 
201
256
  function SortableRoot({
@@ -205,6 +260,7 @@ function SortableRoot({
205
260
  renderOverlay,
206
261
  acceptsDrop = defaultAcceptsDrop,
207
262
  axis = "y",
263
+ getDropTargetParentIndex,
208
264
  }: RootProps) {
209
265
  const sensors = useSensors(
210
266
  useSensor(PointerSensor, {
@@ -242,7 +298,6 @@ function SortableRoot({
242
298
  const { active, over } = event;
243
299
 
244
300
  setActiveIndex(undefined);
245
-
246
301
  if (over && active.id !== over.id) {
247
302
  const oldIndex = keys.findIndex((id) => id === active.id);
248
303
  const newIndex = keys.findIndex((id) => id === over.id);
@@ -281,14 +336,22 @@ function SortableRoot({
281
336
  return (
282
337
  <SortableItemContext.Provider
283
338
  value={React.useMemo(
284
- () => ({
339
+ (): SortableItemContextProps => ({
285
340
  keys,
286
341
  acceptsDrop,
287
342
  position,
288
343
  setActivatorEvent,
289
344
  axis,
345
+ getDropTargetParentIndex,
290
346
  }),
291
- [acceptsDrop, axis, keys, position, setActivatorEvent]
347
+ [
348
+ acceptsDrop,
349
+ axis,
350
+ getDropTargetParentIndex,
351
+ keys,
352
+ position,
353
+ setActivatorEvent,
354
+ ]
292
355
  )}
293
356
  >
294
357
  <DndContext
@@ -323,7 +386,83 @@ function SortableRoot({
323
386
  );
324
387
  }
325
388
 
326
- export namespace Sortable {
327
- export const Item = memoGeneric(SortableItem);
328
- export const Root = memoGeneric(SortableRoot);
389
+ // Create a map of all subcomponents
390
+ const SortableSubcomponents = {
391
+ Root: memo(SortableRoot),
392
+ Item: memoGeneric(SortableItem),
393
+ } as const;
394
+
395
+ interface SortableProps<T, TElement extends HTMLElement>
396
+ extends Omit<RootProps, "children" | "keys" | "renderOverlay"> {
397
+ items: T[];
398
+ keyExtractor: (item: T) => UniqueIdentifier;
399
+ shouldRenderOverlay?: boolean;
400
+ renderItem: (
401
+ item: T,
402
+ props: ItemChildrenProps<TElement>,
403
+ { isOverlay }: { isOverlay: boolean }
404
+ ) => JSX.Element;
329
405
  }
406
+
407
+ const SortableComponent = memoGeneric(function Sortable<
408
+ TItem,
409
+ TElement extends HTMLElement,
410
+ >(props: SortableProps<TItem, TElement>) {
411
+ const {
412
+ renderItem,
413
+ items,
414
+ keyExtractor,
415
+ shouldRenderOverlay = true,
416
+ ...rest
417
+ } = props;
418
+
419
+ const keys = React.useMemo(
420
+ () => items.map(keyExtractor),
421
+ [items, keyExtractor]
422
+ );
423
+
424
+ const renderOverlay = React.useCallback(
425
+ (index: number) => {
426
+ return renderItem(
427
+ items[index],
428
+ {
429
+ ref: () => {},
430
+ style: {},
431
+ tabIndex: -1,
432
+ "aria-disabled": true,
433
+ "aria-pressed": false,
434
+ "aria-roledescription": "Item being dragged",
435
+ "aria-describedby": "Item being dragged",
436
+ role: "button",
437
+ },
438
+ { isOverlay: true }
439
+ );
440
+ },
441
+ [renderItem, items]
442
+ );
443
+
444
+ return (
445
+ <SortableSubcomponents.Root
446
+ {...rest}
447
+ keys={keys}
448
+ renderOverlay={shouldRenderOverlay ? renderOverlay : undefined}
449
+ >
450
+ {keys.map((key, index) => (
451
+ <SortableSubcomponents.Item key={key} id={key}>
452
+ {(props) => {
453
+ return renderItem(
454
+ items[index],
455
+ props as ItemChildrenProps<TElement>,
456
+ { isOverlay: false }
457
+ );
458
+ }}
459
+ </SortableSubcomponents.Item>
460
+ ))}
461
+ </SortableSubcomponents.Root>
462
+ );
463
+ });
464
+
465
+ // Export both the namespace and the component
466
+ const Sortable = Object.assign(SortableComponent, SortableSubcomponents);
467
+
468
+ export { Sortable };
@@ -94,7 +94,7 @@ const UserPointerIcon = memo(function PointerIcon({
94
94
  <polygon
95
95
  points={points}
96
96
  fill={color}
97
- stroke="var(--background)"
97
+ stroke="var(--n-background)"
98
98
  strokeWidth="0.5"
99
99
  />
100
100
  </svg>
@@ -0,0 +1,102 @@
1
+ import { IconButton } from "../IconButton";
2
+
3
+ import { cssVars } from "../../theme";
4
+
5
+ import { Avatar } from "../Avatar";
6
+ import { Logo } from "../NoyaLogo";
7
+
8
+ import React, { forwardRef } from "react";
9
+ import { INPUT_HEIGHT } from "../../theme";
10
+ import { cx } from "../../utils/classNames";
11
+ import { TextAreaRow } from "../TextArea";
12
+
13
+ export const AIAssistantLoadingIndicator = () => {
14
+ return (
15
+ <div className="flex gap-2">
16
+ <Avatar
17
+ name="Assistant"
18
+ size={INPUT_HEIGHT}
19
+ backgroundColor={cssVars.colors.primaryPastel}
20
+ >
21
+ <Logo style={{ width: 15 }} fill={cssVars.colors.primary} />
22
+ </Avatar>
23
+ <div className="animate-pulse">▋</div>
24
+ </div>
25
+ );
26
+ };
27
+
28
+ type AIAssistantInputProps = {
29
+ value: string;
30
+ onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
31
+ onSubmitClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
32
+ onKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
33
+ disabled: boolean;
34
+ };
35
+
36
+ export const AIAssistantInput = forwardRef<
37
+ HTMLDivElement,
38
+ AIAssistantInputProps
39
+ >(({ value, onChange, onSubmitClick, onKeyDown, disabled }, ref) => (
40
+ <TextAreaRow
41
+ autoResize
42
+ autoFocus
43
+ value={value}
44
+ onChange={onChange}
45
+ ref={ref}
46
+ textAreaClassName="py-2 pl-3 !pr-9 w-full mt-3"
47
+ placeholder="Tell me what you'd like to do..."
48
+ onKeyDown={onKeyDown}
49
+ disabled={disabled}
50
+ end={
51
+ <IconButton
52
+ iconName="ArrowUpIcon"
53
+ onClick={onSubmitClick}
54
+ disabled={!value.trim() || disabled}
55
+ className="h-8 w-8 z-20 rounded-full"
56
+ size={20}
57
+ color={cssVars.colors.text}
58
+ style={{
59
+ backgroundColor: cssVars.colors.chipDefaultBg,
60
+ }}
61
+ />
62
+ }
63
+ />
64
+ ));
65
+
66
+ type AIAssistantLayoutProps = {
67
+ renderMessages: () => React.ReactNode;
68
+ renderInput: () => React.ReactNode;
69
+ isLoading: boolean;
70
+ renderLoadingIndicator?: () => React.ReactNode;
71
+ className?: string;
72
+ style?: React.CSSProperties;
73
+ };
74
+
75
+ export const AIAssistantLayout = forwardRef<
76
+ HTMLDivElement,
77
+ AIAssistantLayoutProps
78
+ >(
79
+ (
80
+ {
81
+ renderMessages,
82
+ renderInput,
83
+ isLoading,
84
+ renderLoadingIndicator,
85
+ className,
86
+ style,
87
+ }: AIAssistantLayoutProps,
88
+ ref
89
+ ) => {
90
+ return (
91
+ <div className={cx("flex flex-col flex-1", className)} style={style}>
92
+ <div className="flex-1 min-h-0 overflow-auto" ref={ref}>
93
+ <div className="flex flex-col gap-2 pb-4 pt-1">
94
+ {renderMessages()}
95
+ {isLoading ? renderLoadingIndicator?.() : null}
96
+ </div>
97
+ </div>
98
+ <div className="border-t border-divider pt-1 px-1">{renderInput()}</div>
99
+ </div>
100
+ );
101
+ }
102
+ );
@@ -0,0 +1,71 @@
1
+ import { cssVars } from "../../theme";
2
+
3
+ import { ChatBubbleWithDotsIcon } from "@noya-app/noya-icons";
4
+ import React from "react";
5
+ import { INPUT_HEIGHT } from "../../theme";
6
+ import { colorFromString } from "../../utils/colorFromString";
7
+ import { Avatar, AvatarProps, AvatarStack } from "../Avatar";
8
+ import Button from "../Button";
9
+ import { Spacer } from "../Spacer";
10
+ import { Small } from "../Text";
11
+ import { Tooltip } from "../Tooltip";
12
+
13
+ export const UserAvatar = ({ userId, name, image }: AvatarProps) => (
14
+ <Tooltip
15
+ key={userId}
16
+ content={
17
+ <div className="flex flex-col">
18
+ <Small>{name}</Small>
19
+ </div>
20
+ }
21
+ >
22
+ <Avatar size={INPUT_HEIGHT} userId={userId} name={name} image={image} />
23
+ </Tooltip>
24
+ );
25
+
26
+ type ConnectedUsersMenuLayoutProps = {
27
+ renderUsers: () => React.ReactNode;
28
+ currentUserId?: string;
29
+ launchAIAssistant?: () => void;
30
+ isAssistantOpen?: boolean;
31
+ };
32
+
33
+ export const ConnectedUsersMenuLayout = ({
34
+ renderUsers,
35
+ currentUserId,
36
+ launchAIAssistant,
37
+ isAssistantOpen,
38
+ }: ConnectedUsersMenuLayoutProps) => {
39
+ return (
40
+ <div className="flex gap-1.5">
41
+ <AvatarStack size={INPUT_HEIGHT}>{renderUsers()}</AvatarStack>
42
+
43
+ {/* AI Assistant Avatar */}
44
+ {launchAIAssistant && (
45
+ <Tooltip
46
+ content={
47
+ <div className="flex flex-col">
48
+ <Small>AI Assistant</Small>
49
+ </div>
50
+ }
51
+ >
52
+ <Button
53
+ variant={isAssistantOpen ? "secondary" : undefined}
54
+ onClick={launchAIAssistant}
55
+ style={{
56
+ backgroundColor: isAssistantOpen
57
+ ? colorFromString(currentUserId ?? "")
58
+ : undefined,
59
+ }}
60
+ >
61
+ AI
62
+ <Spacer.Horizontal size={8} />
63
+ <ChatBubbleWithDotsIcon
64
+ color={isAssistantOpen ? undefined : cssVars.colors.icon}
65
+ />
66
+ </Button>
67
+ </Tooltip>
68
+ )}
69
+ </div>
70
+ );
71
+ };
@@ -0,0 +1,71 @@
1
+ import {
2
+ Banner,
3
+ BannerProps,
4
+ IconButton,
5
+ Section,
6
+ SectionProps,
7
+ } from "@noya-app/noya-designsystem";
8
+ import { forwardRefGeneric } from "@noya-app/react-utils";
9
+ import React from "react";
10
+ import { cx } from "../../utils/classNames";
11
+ import { Collection, CollectionProps, CollectionRef } from "../Collection";
12
+
13
+ export const FileExplorerLayout = ({
14
+ children,
15
+ className,
16
+ ...props
17
+ }: SectionProps) => (
18
+ <Section className={cx(className, "px-3 flex-1")} {...props}>
19
+ {children}
20
+ </Section>
21
+ );
22
+
23
+ export const FileExplorerUploadButton = ({
24
+ showUploadButton,
25
+ onUpload,
26
+ children,
27
+ }: {
28
+ showUploadButton?: boolean;
29
+ onUpload?: () => void;
30
+ children?: React.ReactNode;
31
+ }) => (
32
+ <div className="flex items-center gap-2">
33
+ {showUploadButton && (
34
+ <IconButton
35
+ iconName="UploadIcon"
36
+ onClick={onUpload}
37
+ aria-label="Upload media"
38
+ tooltip="Upload media"
39
+ />
40
+ )}
41
+ {children}
42
+ </div>
43
+ );
44
+
45
+ export const FileExplorerCollection = forwardRefGeneric(
46
+ function FileExplorerCollection<T, M extends string = string>(
47
+ { ...props }: CollectionProps<T, M>,
48
+ ref: React.ForwardedRef<CollectionRef>
49
+ ) {
50
+ return <Collection<T, M> ref={ref} className="-mx-3 flex-1" {...props} />;
51
+ }
52
+ );
53
+
54
+ export const FileExplorerDetail = ({
55
+ selected,
56
+ children,
57
+ }: {
58
+ selected: boolean;
59
+ children: React.ReactNode;
60
+ }) => (
61
+ <span
62
+ className={cx("text-sm", selected ? "text-primary" : "text-text-muted")}
63
+ >
64
+ {children}
65
+ </span>
66
+ );
67
+
68
+ export const FileExplorerEmptyState = ({
69
+ label = "No files",
70
+ ...props
71
+ }: BannerProps) => <Banner label={label} className="mx-3" {...props} />;
@@ -1,7 +1,7 @@
1
1
  import { getShortcutDisplayParts } from "@noya-app/noya-keymap";
2
2
  import React, { memo, ReactNode } from "react";
3
3
  import { useDesignSystemConfiguration } from "../../contexts/DesignSystemConfiguration";
4
- import { cx } from "../../utils/classNames";
4
+ import { cx, mergeConflictingClassNames } from "../../utils/classNames";
5
5
  import { IScoredItem } from "../../utils/fuzzyScorer";
6
6
  import withSeparatorElements from "../../utils/withSeparatorElements";
7
7
  import { IconName } from "../Icons";
@@ -157,12 +157,12 @@ export const getMenuItemKey = <T extends string>(
157
157
  }
158
158
  };
159
159
 
160
- export const CHECKBOX_WIDTH = 16;
160
+ export const CHECKBOX_WIDTH = 15;
161
161
  export const CHECKBOX_RIGHT_INSET = 8;
162
162
  export const CHECKBOX_INDENT_WIDTH = 6;
163
163
 
164
164
  export const styles = {
165
- separatorStyle: "h-px bg-divider mx-4 my-1",
165
+ separatorStyle: "h-px bg-divider mx-3 my-1",
166
166
  selectedItemStyle:
167
167
  "focus:outline-none focus:text-white focus:bg-primary focus:kbd:text-white",
168
168
  testSelectedItemStyle: "outline-none text-white bg-primary kbd:text-white",
@@ -175,7 +175,12 @@ export const styles = {
175
175
  font-sans text-button font-medium
176
176
  ${disabled ? "text-text-disabled" : ""}
177
177
  `,
178
- itemIndicatorStyle: "flex items-center relative -left-2.5 -mr-2",
178
+ itemIndicator: {
179
+ className: "flex items-center relative -left-2.5",
180
+ style: {
181
+ marginRight: -CHECKBOX_RIGHT_INSET,
182
+ },
183
+ },
179
184
  contentStyle:
180
185
  "rounded bg-popover-background text-text shadow-[0_2px_4px_rgba(0,0,0,0.2),_0_0_12px_rgba(0,0,0,0.1)] z-menu py-1",
181
186
  };
@@ -265,12 +270,17 @@ export const SectionHeader = memo(function SectionHeader({
265
270
  return (
266
271
  <span
267
272
  id={id}
268
- className={cx(
269
- variant === "label"
270
- ? `text-label ${textStyles.label} !font-bold text-text-disabled`
271
- : "font-sans text-heading5 font-normal",
272
- "bg-listview-raised-background flex items-center py-1.5 px-3",
273
- isFirst && "-mt-1"
273
+ className={mergeConflictingClassNames(
274
+ [
275
+ variant === "label"
276
+ ? `text-label ${textStyles.label} font-bold text-text-disabled`
277
+ : "font-sans text-heading5 font-normal",
278
+ "bg-listview-raised-background flex items-center py-1.5 px-3",
279
+ isFirst && "-mt-1",
280
+ ],
281
+ {
282
+ categories: ["font"],
283
+ }
274
284
  )}
275
285
  >
276
286
  {indented && (