@noya-app/noya-designsystem 0.1.62 → 0.1.64

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 (118) hide show
  1. package/.eslintrc.js +1 -5
  2. package/.turbo/turbo-build.log +10 -10
  3. package/.turbo/turbo-lint.log +0 -1
  4. package/CHANGELOG.md +23 -0
  5. package/dist/index.css +1 -1
  6. package/dist/index.d.mts +299 -179
  7. package/dist/index.d.ts +299 -179
  8. package/dist/index.js +3772 -2999
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.mjs +3485 -2720
  11. package/dist/index.mjs.map +1 -1
  12. package/package.json +10 -22
  13. package/src/__tests__/validateDropIndicator.test.ts +26 -41
  14. package/src/__tests__/workspaceLayout.test.ts +281 -0
  15. package/src/components/ActionMenu.tsx +2 -0
  16. package/src/components/ActivityIndicator.tsx +5 -3
  17. package/src/components/AnimatePresence.tsx +6 -4
  18. package/src/components/Avatar.tsx +4 -2
  19. package/src/components/BaseToolbar.tsx +45 -34
  20. package/src/components/Breadcrumbs.tsx +2 -0
  21. package/src/components/Button.tsx +35 -21
  22. package/src/components/Chip.tsx +2 -0
  23. package/src/components/Collection.tsx +4 -1
  24. package/src/components/ColorSwatch.tsx +42 -22
  25. package/src/components/ColorSwatchControl.tsx +5 -3
  26. package/src/components/Combobox.tsx +2 -0
  27. package/src/components/CommandPalette.tsx +2 -0
  28. package/src/components/ContextMenu.tsx +3 -1
  29. package/src/components/Dialog.tsx +3 -1
  30. package/src/components/DimensionInput.tsx +3 -1
  31. package/src/components/Divider.tsx +10 -6
  32. package/src/components/DraggableMenuButton.tsx +22 -4
  33. package/src/components/Drawer.tsx +11 -3
  34. package/src/components/DropdownMenu.tsx +34 -6
  35. package/src/components/EditableText.tsx +2 -0
  36. package/src/components/FileUploadIndicator.tsx +108 -0
  37. package/src/components/FillPreviewBackground.tsx +2 -0
  38. package/src/components/FloatingWindow.tsx +2 -0
  39. package/src/components/GradientPicker.tsx +2 -0
  40. package/src/components/Grid.tsx +47 -38
  41. package/src/components/GridView.tsx +33 -11
  42. package/src/components/IconButton.tsx +4 -2
  43. package/src/components/Icons.tsx +1 -1
  44. package/src/components/InputField.tsx +7 -5
  45. package/src/components/Label.tsx +1 -3
  46. package/src/components/LabeledElementView.tsx +6 -2
  47. package/src/components/LabeledField.tsx +38 -34
  48. package/src/components/List.tsx +10 -2
  49. package/src/components/ListView.tsx +27 -14
  50. package/src/components/MediaThumbnail.tsx +31 -2
  51. package/src/components/Message.tsx +8 -9
  52. package/src/components/Popover.tsx +15 -4
  53. package/src/components/Progress.tsx +20 -7
  54. package/src/components/ScrollArea.tsx +3 -1
  55. package/src/components/SearchCompletionMenu.tsx +2 -0
  56. package/src/components/Section.tsx +2 -0
  57. package/src/components/SegmentedControl.tsx +43 -17
  58. package/src/components/SelectMenu.tsx +6 -4
  59. package/src/components/SelectionToolbar.tsx +31 -8
  60. package/src/components/Slider.tsx +4 -2
  61. package/src/components/Spacer.tsx +0 -1
  62. package/src/components/Switch.tsx +3 -1
  63. package/src/components/Tabs.tsx +48 -0
  64. package/src/components/Text.tsx +6 -5
  65. package/src/components/TextArea.tsx +2 -0
  66. package/src/components/Toast.tsx +1 -1
  67. package/src/components/Toolbar.tsx +113 -12
  68. package/src/components/ToolbarDrawer.tsx +29 -0
  69. package/src/components/Tooltip.tsx +9 -9
  70. package/src/components/TreeView.tsx +2 -0
  71. package/src/components/UserPointer.tsx +2 -0
  72. package/src/components/ai-assistant/AIAssistantLayout.tsx +2 -0
  73. package/src/components/blocks/ImageBlockComponent.tsx +156 -0
  74. package/src/components/connected-users-menu/ConnectedUsersMenuLayout.tsx +2 -0
  75. package/src/components/file-explorer/FileExplorerLayout.tsx +14 -6
  76. package/src/components/internal/Checkmark.tsx +24 -0
  77. package/src/components/internal/Menu.tsx +21 -3
  78. package/src/components/internal/MenuViewport.tsx +8 -5
  79. package/src/components/internal/SelectItem.tsx +4 -23
  80. package/src/components/internal/TextInput.tsx +2 -0
  81. package/src/components/sorting/DragRegistration.tsx +8 -4
  82. package/src/components/sorting/SharedDragProvider.tsx +43 -18
  83. package/src/components/sorting/Sortable.tsx +19 -10
  84. package/src/components/sorting/createSharedDrag.tsx +7 -6
  85. package/src/components/sorting/sorting.ts +6 -9
  86. package/src/components/workspace/DrawerWorkspaceLayout.tsx +113 -28
  87. package/src/components/workspace/PanelWorkspaceLayout.tsx +149 -16
  88. package/src/components/workspace/VerticalTabMenu.tsx +36 -0
  89. package/src/components/workspace/WorkspaceLayout.tsx +132 -47
  90. package/src/components/workspace/constants.ts +4 -0
  91. package/src/components/workspace/panelStorage.ts +216 -0
  92. package/src/components/workspace/renderPanelChildren.tsx +12 -0
  93. package/src/components/workspace/types.ts +30 -7
  94. package/src/contexts/DesignSystemConfiguration.tsx +12 -5
  95. package/src/contexts/DialogContext.tsx +2 -0
  96. package/src/contexts/FloatingWindowContext.tsx +2 -0
  97. package/src/contexts/GlobalInputBlurContext.tsx +2 -0
  98. package/src/contexts/ImageDataContext.tsx +2 -0
  99. package/src/contexts/LinkComponentContext.tsx +17 -0
  100. package/src/contexts/OpenPortalsContext.tsx +2 -0
  101. package/src/contexts/PortalScopeContext.tsx +2 -0
  102. package/src/hooks/mergeEventHandlers.ts +1 -1
  103. package/src/hooks/useHover.ts +30 -17
  104. package/src/hooks/useIndent.ts +2 -0
  105. package/src/hooks/useLabel.ts +12 -10
  106. package/src/hooks/useObjectURL.ts +5 -3
  107. package/src/hooks/usePlatform.ts +5 -3
  108. package/src/hooks/useTheme.ts +3 -1
  109. package/src/index.css +25 -4
  110. package/src/index.tsx +7 -2
  111. package/src/utils/classNames.ts +4 -3
  112. package/src/utils/combobox.ts +2 -0
  113. package/src/utils/editableBlockStyles.ts +10 -0
  114. package/src/utils/formatByteSize.ts +8 -0
  115. package/src/utils/withSeparatorElements.ts +1 -1
  116. package/src/hooks/usePreservePanelSize.tsx +0 -183
  117. package/src/hooks/useWindowSize.tsx +0 -26
  118. package/src/mediaQuery.ts +0 -13
@@ -0,0 +1,48 @@
1
+ import { useControlledOrUncontrolled } from "@noya-app/react-utils";
2
+ import * as React from "react";
3
+ import { DetailedHTMLProps, HTMLAttributes } from "react";
4
+ import { cx } from "../utils/classNames";
5
+ import { Divider } from "./Divider";
6
+ import { isSelectableMenuItem, MenuItem } from "./internal/Menu";
7
+ import { SegmentedControl } from "./SegmentedControl";
8
+
9
+ export type TabsProps<T extends string = string> = {
10
+ value?: T;
11
+ onValueChange?: (value: T) => void;
12
+ items: MenuItem<T>[];
13
+ renderContent: ({ value }: { value: T }) => React.ReactNode;
14
+ variant?: "default" | "tabs" | "buttons";
15
+ } & DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
16
+
17
+ export function Tabs<T extends string = string>({
18
+ value,
19
+ onValueChange,
20
+ items,
21
+ renderContent,
22
+ variant = "tabs",
23
+ ...props
24
+ }: TabsProps<T>) {
25
+ const selectableItems = items.filter(isSelectableMenuItem);
26
+
27
+ const [activeTab, setActiveTab] = useControlledOrUncontrolled({
28
+ defaultValue: selectableItems.at(0)?.value ?? ("" as T),
29
+ value,
30
+ onChange: onValueChange,
31
+ });
32
+
33
+ return (
34
+ <div {...props} className={cx("flex flex-col", props.className)}>
35
+ <div className="flex flex-row px-3">
36
+ <SegmentedControl
37
+ className="flex-1"
38
+ value={activeTab}
39
+ onValueChange={setActiveTab}
40
+ variant={variant}
41
+ items={selectableItems}
42
+ />
43
+ </div>
44
+ <Divider />
45
+ {renderContent({ value: activeTab })}
46
+ </div>
47
+ );
48
+ }
@@ -1,4 +1,6 @@
1
- import React, { ForwardedRef, forwardRef, ReactHTML, ReactNode } from "react";
1
+ "use client";
2
+
3
+ import React, { ForwardedRef, forwardRef, ReactNode } from "react";
2
4
  import { ThemeColor } from "../theme";
3
5
  import { camelToKebabCase, KebabToCamelCase } from "../theme/themeUtils";
4
6
  import { cx } from "../utils/classNames";
@@ -17,7 +19,7 @@ type Variant =
17
19
  | "code"
18
20
  | "label";
19
21
 
20
- const elements: Record<Variant, keyof ReactHTML> = {
22
+ const elements: Record<Variant, React.HTMLElementType> = {
21
23
  title: "h1",
22
24
  subtitle: "h1",
23
25
  heading1: "h1",
@@ -48,7 +50,7 @@ export const textStyles: Record<Variant, string> = {
48
50
  } as const;
49
51
 
50
52
  export type TextProps = {
51
- as?: keyof ReactHTML;
53
+ as?: React.HTMLElementType;
52
54
  href?: string;
53
55
  className?: string;
54
56
  style?: React.CSSProperties;
@@ -78,11 +80,10 @@ export const Text = forwardRef(function Text(
78
80
  }: TextProps,
79
81
  forwardedRef: ForwardedRef<HTMLElement>
80
82
  ) {
81
- const Component = as ?? elements[variant] ?? "span";
83
+ const Component = (as ?? elements[variant] ?? "span") as "span";
82
84
 
83
85
  return (
84
86
  <Component
85
- // @ts-expect-error expected to be many semantic html elements
86
87
  ref={forwardedRef}
87
88
  className={cx(
88
89
  `${textStyles[variant]} text-${camelToKebabCase(String(color)) ?? "text"} `,
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import { assignRef } from "@noya-app/react-utils";
2
4
  import React, {
3
5
  ComponentProps,
@@ -1,4 +1,4 @@
1
- import * as ToastPrimitive from "@radix-ui/react-toast";
1
+ import { Toast as ToastPrimitive } from "radix-ui";
2
2
  import React, { ReactNode } from "react";
3
3
  import { IconButton } from "./IconButton";
4
4
  import { textStyles } from "./Text";
@@ -1,5 +1,7 @@
1
+ "use client";
2
+
1
3
  import { useKeyboardShortcuts } from "@noya-app/noya-keymap";
2
- import React, { ComponentProps } from "react";
4
+ import React, { ComponentProps, ReactNode } from "react";
3
5
 
4
6
  import { memoGeneric } from "@noya-app/react-utils";
5
7
  import { BaseToolbar } from "./BaseToolbar";
@@ -9,17 +11,18 @@ import { DropdownMenu } from "./DropdownMenu";
9
11
  import {
10
12
  getKeyboardShortcutsForMenuItems,
11
13
  isSelectableMenuItem,
14
+ KeyboardShortcut,
12
15
  MenuItem,
16
+ PopoverMenuItem,
13
17
  SelectableMenuItem,
14
18
  SubMenuItem,
15
19
  } from "./internal/Menu";
20
+ import { Popover } from "./Popover";
21
+ import { Spacer } from "./Spacer";
22
+ import { ToolbarDrawer } from "./ToolbarDrawer";
16
23
  import { Tooltip } from "./Tooltip";
17
24
 
18
- const iconButtonStyle = {
19
- height: 27,
20
- width: 27,
21
- minWidth: 27,
22
- };
25
+ const SIDE_OFFSET = 6;
23
26
 
24
27
  export const ToolbarMenuDropdown = memoGeneric(function ToolbarMenuDropdown<
25
28
  T extends string,
@@ -55,24 +58,102 @@ export const ToolbarMenuDropdown = memoGeneric(function ToolbarMenuDropdown<
55
58
  );
56
59
  });
57
60
 
61
+ export const ToolbarShortcut = ({
62
+ shortcut,
63
+ label,
64
+ }: {
65
+ shortcut: string;
66
+ label?: ReactNode;
67
+ }) => {
68
+ return (
69
+ <div className="flex items-center">
70
+ {label}
71
+ <Spacer.Horizontal size={6} />
72
+ <KeyboardShortcut shortcut={shortcut} />
73
+ </div>
74
+ );
75
+ };
76
+
77
+ export const ToolbarMenuPopover = memoGeneric(function ToolbarMenuPopover({
78
+ item,
79
+ portalContainer,
80
+ }: {
81
+ item: PopoverMenuItem;
82
+ portalContainer?: HTMLElement | null;
83
+ }) {
84
+ const {
85
+ disabled,
86
+ icon,
87
+ title,
88
+ tooltip,
89
+ content: itemContent,
90
+ checked,
91
+ } = item;
92
+ const [open, setOpen] = React.useState(false);
93
+
94
+ const content = (
95
+ <span>
96
+ <Popover
97
+ open={open}
98
+ onOpenChange={setOpen}
99
+ side="top"
100
+ showArrow={false}
101
+ sideOffset={SIDE_OFFSET}
102
+ portalContainer={portalContainer}
103
+ trigger={
104
+ <Button
105
+ disabled={disabled}
106
+ active={checked || open}
107
+ icon={icon}
108
+ iconRight="DropdownChevronIcon"
109
+ >
110
+ {title}
111
+ </Button>
112
+ }
113
+ >
114
+ {typeof itemContent === "function"
115
+ ? itemContent({ close: () => setOpen(false) })
116
+ : itemContent}
117
+ </Popover>
118
+ </span>
119
+ );
120
+
121
+ return tooltip && !open ? (
122
+ <Tooltip
123
+ sideOffset={SIDE_OFFSET}
124
+ side="top"
125
+ content={tooltip}
126
+ portalContainer={portalContainer}
127
+ >
128
+ {content}
129
+ </Tooltip>
130
+ ) : (
131
+ content
132
+ );
133
+ });
134
+
58
135
  export const ToolbarMenuButton = memoGeneric(function ToolbarMenuButton<
59
136
  T extends string,
60
137
  >({
61
138
  item,
62
139
  onSelectMenuItem,
63
140
  as,
141
+ activeValue,
142
+ tooltipSide,
64
143
  }: {
65
144
  item: SelectableMenuItem<T>;
66
145
  onSelectMenuItem?: (value: T) => void;
67
146
  as?: ComponentProps<typeof Button>["as"];
147
+ activeValue?: T;
148
+ tooltipSide?: ComponentProps<typeof Tooltip>["side"];
68
149
  }) {
150
+ const isActive = item.checked || activeValue === item.value;
69
151
  const content = (
70
152
  <Button
71
153
  as={as}
72
154
  disabled={item.disabled}
73
- active={item.checked}
155
+ active={isActive}
74
156
  icon={item.icon}
75
- style={item.icon && !item.title ? iconButtonStyle : undefined}
76
157
  {...(as === "a" && { href: item.value })}
77
158
  onClick={() => {
78
159
  if (onSelectMenuItem && item.value) {
@@ -84,8 +165,10 @@ export const ToolbarMenuButton = memoGeneric(function ToolbarMenuButton<
84
165
  </Button>
85
166
  );
86
167
 
87
- return item.tooltip ? (
88
- <Tooltip sideOffset={10} content={item.tooltip}>
168
+ return item.drawer && isActive ? (
169
+ <ToolbarDrawer trigger={content}>{item.drawer}</ToolbarDrawer>
170
+ ) : item.tooltip ? (
171
+ <Tooltip sideOffset={SIDE_OFFSET} content={item.tooltip} side={tooltipSide}>
89
172
  {content}
90
173
  </Tooltip>
91
174
  ) : (
@@ -99,23 +182,32 @@ export const ToolbarMenuItem = memoGeneric(function ToolbarMenuItem<
99
182
  item,
100
183
  onSelectMenuItem,
101
184
  buttonAs,
185
+ activeValue,
186
+ tooltipSide,
187
+ portalContainer,
102
188
  }: {
103
189
  item: MenuItem<T>;
104
190
  onSelectMenuItem?: (value: T) => void;
105
191
  buttonAs?: ComponentProps<typeof Button>["as"];
192
+ activeValue?: T;
193
+ tooltipSide?: ComponentProps<typeof Tooltip>["side"];
194
+ portalContainer?: HTMLElement | null;
106
195
  }) {
107
196
  if (item.type === "sectionHeader") return null;
108
-
109
197
  if (item.type === "separator") {
110
198
  return <DividerVertical overflow={4} />;
111
199
  }
112
-
200
+ if (item.type === "popover") {
201
+ return <ToolbarMenuPopover item={item} portalContainer={portalContainer} />;
202
+ }
113
203
  if (isSelectableMenuItem(item)) {
114
204
  return (
115
205
  <ToolbarMenuButton
116
206
  item={item}
117
207
  onSelectMenuItem={onSelectMenuItem}
118
208
  as={buttonAs}
209
+ activeValue={activeValue}
210
+ tooltipSide={tooltipSide}
119
211
  />
120
212
  );
121
213
  }
@@ -129,10 +221,16 @@ export const ToolbarMenu = memoGeneric(function ToolbarMenu<T extends string>({
129
221
  items,
130
222
  onSelectMenuItem,
131
223
  buttonAs,
224
+ activeValue,
225
+ tooltipSide,
226
+ portalContainer,
132
227
  }: {
133
228
  items: MenuItem<T>[];
134
229
  onSelectMenuItem?: (value: T) => void;
135
230
  buttonAs?: ComponentProps<typeof Button>["as"];
231
+ activeValue?: T;
232
+ tooltipSide?: ComponentProps<typeof Tooltip>["side"];
233
+ portalContainer?: HTMLElement | null;
136
234
  }) {
137
235
  return (
138
236
  <>
@@ -143,6 +241,9 @@ export const ToolbarMenu = memoGeneric(function ToolbarMenu<T extends string>({
143
241
  item={item}
144
242
  onSelectMenuItem={onSelectMenuItem}
145
243
  buttonAs={buttonAs}
244
+ activeValue={activeValue}
245
+ tooltipSide={tooltipSide}
246
+ portalContainer={portalContainer}
146
247
  />
147
248
  );
148
249
  })}
@@ -0,0 +1,29 @@
1
+ import React, { ForwardedRef, forwardRef, memo, ReactNode } from "react";
2
+
3
+ type ToolbarDrawerProps = {
4
+ children: ReactNode;
5
+ trigger: ReactNode;
6
+ sideOffset?: number;
7
+ };
8
+
9
+ export const ToolbarDrawer = memo(
10
+ forwardRef(function ToolbarDrawer(
11
+ { children, trigger, sideOffset = 8 }: ToolbarDrawerProps,
12
+ ref: ForwardedRef<HTMLDivElement>
13
+ ) {
14
+ return (
15
+ <div className="relative" ref={ref}>
16
+ <div
17
+ className="absolute bottom-full left-1/2 -translate-x-1/2 rounded-t-lg border border-toolbar-drawer-border border-b-transparent bg-toolbar-drawer-background -z-10"
18
+ style={{
19
+ marginBottom: `${sideOffset}px`,
20
+ boxShadow: "0px -2px 12px 0px var(--n-toolbar-drawer-shadow)",
21
+ }}
22
+ >
23
+ {children}
24
+ </div>
25
+ {trigger}
26
+ </div>
27
+ );
28
+ })
29
+ );
@@ -1,25 +1,26 @@
1
- import * as TooltipPrimitive from "@radix-ui/react-tooltip";
1
+ import { Tooltip as TooltipPrimitive } from "radix-ui";
2
2
  import * as React from "react";
3
+ import { ComponentProps } from "react";
3
4
  import {
4
5
  portalScopeProps,
5
6
  usePortalScopeId,
6
7
  } from "../contexts/PortalScopeContext";
7
8
  import { textStyles } from "./Text";
8
9
 
9
- // const Arrow = styled(TooltipPrimitive.Arrow)(({ theme }) => ({
10
- // fill: theme.colors.popover.background,
11
- // }));
12
-
13
10
  interface Props {
14
11
  children: React.ReactNode;
15
12
  content: React.ReactNode;
16
13
  sideOffset?: number;
14
+ side?: ComponentProps<typeof TooltipPrimitive.Content>["side"];
15
+ portalContainer?: HTMLElement | null;
17
16
  }
18
17
 
19
18
  export const Tooltip = React.memo(function Tooltip({
20
19
  children,
21
20
  content,
22
21
  sideOffset = 2,
22
+ side = "bottom",
23
+ portalContainer,
23
24
  }: Props) {
24
25
  const portalScopeId = usePortalScopeId();
25
26
 
@@ -27,17 +28,16 @@ export const Tooltip = React.memo(function Tooltip({
27
28
  <TooltipPrimitive.Provider>
28
29
  <TooltipPrimitive.Root>
29
30
  <TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>
30
- <TooltipPrimitive.Portal>
31
+ <TooltipPrimitive.Portal container={portalContainer}>
31
32
  <TooltipPrimitive.Content
32
33
  {...portalScopeProps(portalScopeId)}
33
- side="bottom"
34
+ side={side}
34
35
  align="center"
35
36
  sideOffset={sideOffset}
36
37
  collisionPadding={8}
37
- className={`${textStyles.small} text-text rounded-[3px] py-small px-medium bg-popover-background shadow-popover border border-solid border-divider-strong z-menu`}
38
+ className={`${textStyles.small} text-text rounded-sm p-2 bg-popover-background shadow-tooltip z-menu`}
38
39
  >
39
40
  {content}
40
- {/* <Arrow /> */}
41
41
  </TooltipPrimitive.Content>
42
42
  </TooltipPrimitive.Portal>
43
43
  </TooltipPrimitive.Root>
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import { forwardRefGeneric, memoGeneric } from "@noya-app/react-utils";
2
4
  import React, { ForwardedRef, ReactNode, useCallback, useContext } from "react";
3
5
  import { IconButton } from "./IconButton";
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import React, { memo, useMemo } from "react";
2
4
  import { cx } from "../utils/classNames";
3
5
  import { colorFromString } from "../utils/colorFromString";
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import { IconButton } from "../IconButton";
2
4
 
3
5
  import { cssVars } from "../../theme";
@@ -0,0 +1,156 @@
1
+ import { ActivityIndicator, cx } from "@noya-app/noya-designsystem";
2
+ import { ImageIcon } from "@noya-app/noya-icons";
3
+ import React, { ForwardedRef, forwardRef } from "react";
4
+
5
+ type ImageState = "loading" | "error" | "loaded";
6
+
7
+ const placeholderClassName =
8
+ "cursor-pointer flex items-center justify-center p-4 border border-dashed border-indigo-100 rounded-md bg-indigo-50";
9
+
10
+ export function decodeImageSrc(src?: string): string | undefined {
11
+ if (!src) return undefined;
12
+
13
+ try {
14
+ return decodeURIComponent(src);
15
+ } catch (error) {
16
+ return undefined;
17
+ }
18
+ }
19
+
20
+ const ImageLoadingComponent = forwardRef(function ImageLoadingComponent(
21
+ props: React.HTMLAttributes<HTMLSpanElement>,
22
+ forwardedRef: ForwardedRef<HTMLSpanElement>
23
+ ) {
24
+ return (
25
+ <span
26
+ {...props}
27
+ ref={forwardedRef}
28
+ contentEditable={false}
29
+ className={cx(placeholderClassName, props.className)}
30
+ >
31
+ <ActivityIndicator />
32
+ </span>
33
+ );
34
+ });
35
+
36
+ const ImagePlaceholderComponent = forwardRef(function ImagePlaceholderComponent(
37
+ props: React.HTMLAttributes<HTMLSpanElement>,
38
+ forwardedRef: ForwardedRef<HTMLSpanElement>
39
+ ) {
40
+ return (
41
+ <span
42
+ {...props}
43
+ ref={forwardedRef}
44
+ contentEditable={false}
45
+ className={cx(placeholderClassName, props.className)}
46
+ >
47
+ <ImageIcon className="w-[30px] h-[30px] text-indigo-900 pointer-events-none" />
48
+ </span>
49
+ );
50
+ });
51
+
52
+ const ImageErrorComponent = forwardRef(function ImageErrorComponent(
53
+ { src, ...props }: React.HTMLAttributes<HTMLSpanElement> & { src?: string },
54
+ forwardedRef: ForwardedRef<HTMLSpanElement>
55
+ ) {
56
+ return (
57
+ <span
58
+ {...props}
59
+ ref={forwardedRef}
60
+ contentEditable={false}
61
+ className={cx(placeholderClassName, props.className)}
62
+ >
63
+ <span className="text-cm-error font-mono text-sm">
64
+ Invalid image URL: {src ?? "No URL provided"}
65
+ </span>
66
+ </span>
67
+ );
68
+ });
69
+
70
+ const ImageDisplayComponent = forwardRef(function ImageDisplayComponent(
71
+ {
72
+ src,
73
+ alt,
74
+ title,
75
+ ...props
76
+ }: React.DetailedHTMLProps<
77
+ React.ImgHTMLAttributes<HTMLImageElement>,
78
+ HTMLImageElement
79
+ >,
80
+ forwardedRef: ForwardedRef<HTMLImageElement>
81
+ ) {
82
+ return (
83
+ <img
84
+ {...props}
85
+ ref={forwardedRef}
86
+ src={src}
87
+ alt={alt ?? ""}
88
+ title={title ?? undefined}
89
+ className={cx(
90
+ "cursor-pointer max-w-full max-h-[720px] object-contain mx-auto my-8",
91
+ props.className
92
+ )}
93
+ />
94
+ );
95
+ });
96
+
97
+ const ImageBlockComponent_ = forwardRef(function ImageBlockComponent(
98
+ {
99
+ src,
100
+ alt,
101
+ title,
102
+ imageState,
103
+ ...props
104
+ }: React.DetailedHTMLProps<
105
+ React.ImgHTMLAttributes<HTMLImageElement>,
106
+ HTMLImageElement
107
+ > & { imageState?: ImageState },
108
+ forwardedRef: ForwardedRef<HTMLImageElement>
109
+ ) {
110
+ if (imageState === "loading") {
111
+ return (
112
+ <ImageLoadingComponent
113
+ {...props}
114
+ ref={forwardedRef as ForwardedRef<HTMLSpanElement>}
115
+ />
116
+ );
117
+ }
118
+
119
+ if (!src) {
120
+ return (
121
+ <ImagePlaceholderComponent
122
+ {...props}
123
+ ref={forwardedRef as ForwardedRef<HTMLSpanElement>}
124
+ />
125
+ );
126
+ }
127
+
128
+ const decodedSrc = decodeImageSrc(src.toString());
129
+
130
+ if (!decodedSrc || imageState === "error") {
131
+ return (
132
+ <ImageErrorComponent
133
+ {...props}
134
+ src={src.toString()}
135
+ ref={forwardedRef as ForwardedRef<HTMLSpanElement>}
136
+ />
137
+ );
138
+ }
139
+
140
+ return (
141
+ <ImageDisplayComponent
142
+ {...props}
143
+ ref={forwardedRef}
144
+ src={src}
145
+ alt={alt}
146
+ title={title}
147
+ />
148
+ );
149
+ });
150
+
151
+ export const ImageBlockComponent = Object.assign(ImageBlockComponent_, {
152
+ Loading: ImageLoadingComponent,
153
+ Placeholder: ImagePlaceholderComponent,
154
+ Error: ImageErrorComponent,
155
+ Display: ImageDisplayComponent,
156
+ });
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import React from "react";
2
4
  import { INPUT_HEIGHT } from "../../theme";
3
5
  import { Avatar, AvatarProps, AvatarStack } from "../Avatar";
@@ -1,7 +1,10 @@
1
+ "use client";
2
+
1
3
  import {
4
+ ActivityIndicator,
2
5
  Banner,
3
6
  BannerProps,
4
- IconButton,
7
+ Button,
5
8
  Section,
6
9
  SectionProps,
7
10
  } from "@noya-app/noya-designsystem";
@@ -28,20 +31,25 @@ export const FileExplorerLayout = ({
28
31
  export const FileExplorerUploadButton = ({
29
32
  showUploadButton,
30
33
  onUpload,
34
+ isUploading,
31
35
  children,
32
36
  }: {
33
37
  showUploadButton?: boolean;
34
38
  onUpload?: () => void;
39
+ isUploading?: boolean;
35
40
  children?: React.ReactNode;
36
41
  }) => (
37
42
  <div className="flex items-center gap-2">
38
43
  {showUploadButton && (
39
- <IconButton
40
- iconName="UploadIcon"
44
+ <Button
45
+ icon={isUploading ? <ActivityIndicator size={15} /> : "UploadIcon"}
41
46
  onClick={onUpload}
42
- aria-label="Upload media"
43
- tooltip="Upload media"
44
- />
47
+ aria-label="Upload file"
48
+ tooltip="Upload file"
49
+ disabled={isUploading}
50
+ >
51
+ Upload
52
+ </Button>
45
53
  )}
46
54
  {children}
47
55
  </div>
@@ -0,0 +1,24 @@
1
+ import React from "react";
2
+
3
+ export function Checkmark(props: React.SVGProps<SVGSVGElement>) {
4
+ return (
5
+ <svg
6
+ viewBox="0 0 15 15"
7
+ fill="none"
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ aria-hidden="true"
10
+ width={15}
11
+ height={15}
12
+ {...props}
13
+ >
14
+ <path
15
+ className="scale-90 origin-center"
16
+ d="M3 8L6 11L12 5"
17
+ strokeWidth="1.1"
18
+ strokeLinecap="round"
19
+ strokeLinejoin="round"
20
+ stroke="currentColor"
21
+ />
22
+ </svg>
23
+ );
24
+ }