@noya-app/noya-designsystem 0.1.49 → 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 (39) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +11 -0
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.mts +137 -12
  5. package/dist/index.d.ts +137 -12
  6. package/dist/index.js +1627 -1129
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +1582 -1102
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +5 -5
  11. package/src/components/Avatar.tsx +2 -2
  12. package/src/components/Banner.tsx +29 -0
  13. package/src/components/Button.tsx +2 -2
  14. package/src/components/Chip.tsx +8 -8
  15. package/src/components/Collection.tsx +6 -1
  16. package/src/components/Dialog.tsx +2 -2
  17. package/src/components/FillInputField.tsx +1 -1
  18. package/src/components/Grid.tsx +1 -3
  19. package/src/components/Label.tsx +11 -6
  20. package/src/components/LabeledField.tsx +6 -1
  21. package/src/components/List.tsx +4 -4
  22. package/src/components/ListView.tsx +10 -5
  23. package/src/components/MediaThumbnail.tsx +11 -3
  24. package/src/components/Popover.tsx +9 -6
  25. package/src/components/Section.tsx +172 -0
  26. package/src/components/SelectMenu.tsx +1 -1
  27. package/src/components/Slider.tsx +1 -1
  28. package/src/components/Sortable.tsx +124 -22
  29. package/src/components/UserPointer.tsx +1 -1
  30. package/src/components/ai-assistant/AIAssistantLayout.tsx +102 -0
  31. package/src/components/connected-users-menu/ConnectedUsersMenuLayout.tsx +71 -0
  32. package/src/components/file-explorer/FileExplorerLayout.tsx +71 -0
  33. package/src/components/internal/Menu.tsx +12 -7
  34. package/src/components/pipeline/PipelineResultLayout.tsx +32 -0
  35. package/src/index.css +118 -118
  36. package/src/index.tsx +7 -0
  37. package/src/utils/classNames.ts +27 -2
  38. package/src/utils/moveTreeItem.ts +99 -0
  39. package/tailwind.config.ts +79 -79
@@ -19,6 +19,7 @@ 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";
@@ -96,42 +97,67 @@ export function validateDropIndicator({
96
97
 
97
98
  const acceptsDropInside = acceptsDrop(activeIndex, overIndex, "inside");
98
99
 
99
- // If cursor is in the middle third of the element (between 1/3 and 2/3)
100
- // and dropping inside is allowed, show "inside" indicator
100
+ // Drop into the middle third if possible
101
101
  if (
102
- offsetStart >= elementStart + elementSize / 3 &&
103
- offsetStart <= elementStart + (elementSize * 2) / 3 &&
102
+ isContainedInMiddleThird(elementStart, elementSize, offsetStart) &&
104
103
  acceptsDropInside
105
- )
104
+ ) {
106
105
  return "inside";
106
+ }
107
+
108
+ const containingHalf = getContainingHalf(
109
+ elementStart,
110
+ elementSize,
111
+ offsetStart
112
+ );
107
113
 
108
- // Are we over the top or bottom half of the element?
109
- const indicator =
110
- offsetStart < elementStart + elementSize / 2 ? "above" : "below";
111
- // Drop above or below if possible, falling back to inside
112
- return acceptsDrop(activeIndex, overIndex, indicator)
113
- ? indicator
114
- : 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
115
121
  ? "inside"
116
122
  : undefined;
117
123
  }
118
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
+
119
145
  /* ----------------------------------------------------------------------------
120
146
  * Item
121
147
  * ------------------------------------------------------------------------- */
122
148
 
123
149
  type UseSortableReturnType = ReturnType<typeof useSortable>;
124
150
 
151
+ type ItemChildrenProps<T> = {
152
+ ref: React.Ref<T>;
153
+ relativeDropPosition?: RelativeDropPosition;
154
+ style?: React.CSSProperties;
155
+ } & UseSortableReturnType["attributes"];
156
+
125
157
  interface ItemProps<T> {
126
158
  id: UniqueIdentifier;
127
159
  disabled?: boolean;
128
- children: (
129
- props: {
130
- ref: React.Ref<T>;
131
- relativeDropPosition?: RelativeDropPosition;
132
- style?: React.CSSProperties;
133
- } & UseSortableReturnType["attributes"]
134
- ) => JSX.Element;
160
+ children: (props: ItemChildrenProps<T>) => JSX.Element;
135
161
  }
136
162
 
137
163
  function SortableItem<T extends HTMLElement>({
@@ -360,7 +386,83 @@ function SortableRoot({
360
386
  );
361
387
  }
362
388
 
363
- export namespace Sortable {
364
- export const Item = memoGeneric(SortableItem);
365
- 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;
366
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";
@@ -270,12 +270,17 @@ export const SectionHeader = memo(function SectionHeader({
270
270
  return (
271
271
  <span
272
272
  id={id}
273
- className={cx(
274
- variant === "label"
275
- ? `text-label ${textStyles.label} !font-bold text-text-disabled`
276
- : "font-sans text-heading5 font-normal",
277
- "bg-listview-raised-background flex items-center py-1.5 px-3",
278
- 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
+ }
279
284
  )}
280
285
  >
281
286
  {indented && (
@@ -0,0 +1,32 @@
1
+ import { Link1Icon } from "@noya-app/noya-icons";
2
+ import React from "react";
3
+ import { DividerVertical } from "../Divider";
4
+ import { Small } from "../Text";
5
+
6
+ export const PipelineResultLink = ({ url }: { url: string }) => {
7
+ return (
8
+ <div className="flex flex-col gap-0.5">
9
+ {/* <Label position="start">{name}</Label> */}
10
+ <a
11
+ href={url}
12
+ target="_blank"
13
+ rel="noopener noreferrer"
14
+ className="flex text-primary hover:opacity-80 border border-divider rounded-md"
15
+ >
16
+ <div className="flex gap-1 items-center p-2">
17
+ <Link1Icon className="w-[15px] h-[15px] flex-shrink-0" />
18
+ </div>
19
+ <DividerVertical />
20
+ <Small className="truncate p-2 flex-1">{url}</Small>
21
+ </a>
22
+ </div>
23
+ );
24
+ };
25
+
26
+ export const PipelineResultLayout = ({
27
+ children,
28
+ }: {
29
+ children: React.ReactNode;
30
+ }) => {
31
+ return <div className="flex flex-col gap-2">{children}</div>;
32
+ };