@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
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import React, {
2
4
  CSSProperties,
3
5
  ForwardedRef,
@@ -12,29 +14,27 @@ import { Spacer } from "./Spacer";
12
14
  import { Tooltip } from "./Tooltip";
13
15
  import type { MenuItemIcon } from "./internal/Menu";
14
16
 
15
- type ButtonVariant =
16
- | "normal"
17
- | "primary"
18
- | "secondary"
19
- | "secondaryBright"
20
- | "thin"
21
- | "floating"
22
- | "none";
17
+ type ButtonVariant = "normal" | "thin" | "floating" | "none" | "ghost";
23
18
 
24
19
  type ButtonSize = "small" | "normal" | "large" | "floating";
25
20
 
26
- const variantStyles: Record<ButtonVariant, string> = {
27
- normal:
28
- "bg-button-background text-button-text hover:bg-button-background-hover active:bg-button-background-active min-w-[31px]",
29
- primary:
30
- "bg-primary text-white hover:bg-primary-light active:bg-primary min-w-[31px]",
21
+ type ButtonColorScheme = "primary" | "secondary";
22
+
23
+ const colorSchemeStyles: Record<ButtonColorScheme | "default", string> = {
24
+ default:
25
+ "bg-button-background text-button-text hover:bg-button-background-hover active:bg-button-background-active",
26
+ primary: "bg-primary text-white hover:bg-primary-light active:bg-primary",
31
27
  secondary:
32
- "bg-secondary text-white hover:bg-secondary-light active:bg-secondary min-w-[31px]",
33
- secondaryBright: "bg-[#0ab557] text-white hover:opacity-80 active:opacity-90",
28
+ "bg-secondary text-white hover:bg-secondary-light active:bg-secondary",
29
+ };
30
+
31
+ const variantStyles: Record<ButtonVariant, string> = {
32
+ normal: "",
34
33
  floating:
35
34
  "bg-floating-button text-text shadow-[0_1px_2px_rgba(0,0,0,0.1)] hover:opacity-80 active:opacity-90",
36
35
  thin: "bg-transparent text-text hover:bg-input-background-light",
37
36
  none: "bg-transparent text-text hover:bg-input-background-light",
37
+ ghost: "bg-transparent text-text hover:bg-input-background-light",
38
38
  };
39
39
 
40
40
  const sizeStyles: Record<ButtonSize | "thin" | "none", string> = {
@@ -46,16 +46,21 @@ const sizeStyles: Record<ButtonSize | "thin" | "none", string> = {
46
46
  none: "py-0 px-0",
47
47
  };
48
48
 
49
- export interface ButtonRootProps {
49
+ export interface ButtonProps
50
+ extends React.DetailedHTMLProps<
51
+ React.ButtonHTMLAttributes<HTMLButtonElement>,
52
+ HTMLButtonElement
53
+ > {
50
54
  id?: string;
51
55
  className?: string;
52
56
  style?: CSSProperties;
53
57
  tabIndex?: number;
54
58
  icon?: MenuItemIcon;
55
59
  iconRight?: MenuItemIcon;
56
- children: ReactNode;
60
+ children?: ReactNode;
57
61
  active?: boolean;
58
62
  disabled?: boolean;
63
+ colorScheme?: ButtonColorScheme;
59
64
  variant?: ButtonVariant;
60
65
  size?: ButtonSize;
61
66
  tooltip?: ReactNode;
@@ -81,6 +86,7 @@ export const Button = forwardRef(function Button(
81
86
  tooltip,
82
87
  active = false,
83
88
  disabled = false,
89
+ colorScheme: colorSchemeProp,
84
90
  variant = "normal",
85
91
  size = "normal",
86
92
  contentStyle,
@@ -88,15 +94,18 @@ export const Button = forwardRef(function Button(
88
94
  children,
89
95
  as: Component = "button",
90
96
  ...rest
91
- }: ButtonRootProps,
97
+ }: ButtonProps,
92
98
  forwardedRef: ForwardedRef<HTMLButtonElement>
93
99
  ) {
100
+ const colorScheme = colorSchemeProp ?? "default";
101
+
94
102
  const baseClassName = cx(
95
- "no-underline leading-[1] relative border-0 outline-none select-none text-left rounded flex items-center justify-center [&>*]:pointer-events-none [-webkit-app-region:no-drag] focus:ring-2 focus:ring-primary focus:shadow-[0_0_0_1px_var(--n-popover-background)_inset] transition-all",
103
+ "no-underline leading-[1] relative outline-none select-none text-left rounded flex items-center justify-center [&>*]:pointer-events-none [-webkit-app-region:no-drag] focus:ring-2 focus:ring-primary focus:shadow-[0_0_0_1px_var(--n-popover-background)_inset] transition-all",
104
+ variant === "ghost" ? "border" : "border-0",
96
105
  disabled ? "opacity-25" : "",
97
106
  !className?.includes("flex") ? "flex-none" : "",
98
107
  active
99
- ? "bg-button-background-active text-button-text-active hover:bg-button-background-active-hover min-w-[31px]"
108
+ ? "bg-button-background-active text-button-text-active hover:bg-button-background-active-hover"
100
109
  : variantStyles[variant],
101
110
  sizeStyles[
102
111
  variant === "thin"
@@ -106,7 +115,12 @@ export const Button = forwardRef(function Button(
106
115
  : variant === "none"
107
116
  ? "none"
108
117
  : size
109
- ]
118
+ ],
119
+ variant === "normal" && (children ? "min-w-[31px]" : "min-w-[27px]"),
120
+ variant === "ghost"
121
+ ? "border-solid border-input-background hover:bg-input-background-light"
122
+ : colorSchemeStyles[colorScheme],
123
+ !children && (icon || iconRight) && !(icon && iconRight) && "w-[27px]"
110
124
  );
111
125
 
112
126
  const buttonClassName = useMemo(() => {
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import { Cross1Icon, PlusIcon } from "@noya-app/noya-icons";
2
4
  import React, { forwardRef, memo } from "react";
3
5
  import { useHover } from "../hooks/useHover";
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import { forwardRefGeneric } from "@noya-app/react-utils";
2
4
  import React from "react";
3
5
  import { Grid } from "../components/Grid";
@@ -35,6 +37,7 @@ export interface CollectionProps<T, M extends string = string> {
35
37
  items: T[];
36
38
  getId: (item: T) => string;
37
39
  getName: (item: T) => string;
40
+ getHref?: (item: T) => string;
38
41
  getExpanded?: (item: T) => boolean | undefined;
39
42
  getPlaceholder?: (item: T) => string;
40
43
  /**
@@ -90,7 +93,7 @@ export interface CollectionProps<T, M extends string = string> {
90
93
  viewType?: CollectionViewType;
91
94
  onClickItem?: (itemId: string) => void;
92
95
  onDoubleClickItem?: (itemId: string) => void;
93
- renderEmptyState?: () => React.ReactElement;
96
+ renderEmptyState?: () => React.ReactElement<any>;
94
97
  /** @default false */
95
98
  sortable?: boolean;
96
99
  dragIndicatorStyle?: ListViewRowProps<M>["dragIndicatorStyle"];
@@ -1,9 +1,16 @@
1
+ import { getContrastColorForBackground } from "@noya-app/noya-colorpicker";
2
+ import { CheckIcon } from "@noya-app/noya-icons";
1
3
  import React, { forwardRef } from "react";
2
- import { cssVars } from "../theme";
3
4
 
4
- export type ColorSwatchSize = "xsmall" | "small" | "medium" | "large";
5
+ export type ColorSwatchSize =
6
+ | "xxsmall"
7
+ | "xsmall"
8
+ | "small"
9
+ | "medium"
10
+ | "large";
5
11
 
6
- export const colorSwatchSizeMap = {
12
+ export const colorSwatchSizeMap: Record<ColorSwatchSize, number> = {
13
+ xxsmall: 15,
7
14
  xsmall: 21,
8
15
  small: 27,
9
16
  medium: 36,
@@ -14,32 +21,45 @@ type ColorSwatchProps = {
14
21
  color: string;
15
22
  size?: ColorSwatchSize;
16
23
  checked?: boolean;
24
+ as?: React.ElementType;
17
25
  } & React.ButtonHTMLAttributes<HTMLButtonElement>;
18
26
 
19
27
  export const ColorSwatch = forwardRef(function ColorSwatch(
20
- { color, size = "small", checked, style, ...props }: ColorSwatchProps,
28
+ {
29
+ color,
30
+ size = "small",
31
+ checked,
32
+ style,
33
+ as: Component = "button",
34
+ ...props
35
+ }: ColorSwatchProps,
21
36
  ref: React.ForwardedRef<HTMLButtonElement>
22
37
  ) {
23
38
  const sizePx = colorSwatchSizeMap[size];
39
+ const checkedColor = getContrastColorForBackground(color);
24
40
 
25
41
  return (
26
- <button
27
- {...props}
28
- ref={ref}
29
- style={{
30
- background: color,
31
- width: sizePx,
32
- height: sizePx,
33
- borderRadius: "4px",
34
- outline: checked
35
- ? `2px solid ${cssVars.colors.primary}`
36
- : "1px solid rgba(0,0,0,0.1)",
37
- outlineOffset: -1,
38
- boxShadow: checked
39
- ? `inset 0 0 0 2px ${cssVars.colors.background}`
40
- : undefined,
41
- ...style,
42
- }}
43
- />
42
+ <div className="relative" style={{ width: sizePx, height: sizePx }}>
43
+ <Component
44
+ {...props}
45
+ ref={ref}
46
+ style={{
47
+ display: "inline-block",
48
+ background: color,
49
+ width: sizePx,
50
+ height: sizePx,
51
+ borderRadius: "4px",
52
+ outline: "1px solid rgba(0,0,0,0.1)",
53
+ outlineOffset: -1,
54
+ ...style,
55
+ }}
56
+ />
57
+ {checked && (
58
+ <CheckIcon
59
+ color={checkedColor}
60
+ className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
61
+ />
62
+ )}
63
+ </div>
44
64
  );
45
65
  });
@@ -1,4 +1,6 @@
1
- import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
1
+ "use client";
2
+
3
+ import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui";
2
4
  import React, { useMemo } from "react";
3
5
  import { cx } from "../utils/classNames";
4
6
  import {
@@ -52,7 +54,7 @@ export function ColorSwatchControl(props: ColorSwatchControlProps) {
52
54
  {options.map((option) => {
53
55
  const item = (
54
56
  <ColorSwatch
55
- key={option as string}
57
+ key={`${option as string}-color-swatch`}
56
58
  color={option as string}
57
59
  size={size}
58
60
  onFocus={onFocus}
@@ -67,7 +69,7 @@ export function ColorSwatchControl(props: ColorSwatchControlProps) {
67
69
  <ToggleGroupPrimitive.Item
68
70
  asChild
69
71
  value={option as string}
70
- key={option as string}
72
+ key={`${option as string}-toggle-group-item`}
71
73
  disabled={readOnly}
72
74
  >
73
75
  {item}
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import { DropdownChevronIcon } from "@noya-app/noya-icons";
2
4
  import { forwardRefGeneric, memoGeneric } from "@noya-app/react-utils";
3
5
  import React, {
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import { Dialog } from "@noya-app/noya-designsystem";
2
4
  import { AutoSizer } from "@noya-app/react-utils";
3
5
  import React, { memo } from "react";
@@ -1,6 +1,8 @@
1
+ "use client";
2
+
1
3
  import { useKeyboardShortcuts } from "@noya-app/noya-keymap";
2
4
  import { memoGeneric } from "@noya-app/react-utils";
3
- import * as RadixContextMenu from "@radix-ui/react-context-menu";
5
+ import { ContextMenu as RadixContextMenu } from "radix-ui";
4
6
  import React, { ReactNode, useCallback, useMemo } from "react";
5
7
  import {
6
8
  portalScopeProps,
@@ -1,4 +1,6 @@
1
- import * as DialogPrimitive from "@radix-ui/react-dialog";
1
+ "use client";
2
+
3
+ import { Dialog as DialogPrimitive } from "radix-ui";
2
4
  import React, {
3
5
  ComponentProps,
4
6
  ForwardedRef,
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import { round } from "@noya-app/noya-utils";
2
4
  import * as React from "react";
3
5
  import { InputField } from "./InputField";
@@ -51,7 +53,7 @@ export const DimensionInput = React.memo(function DimensionInput({
51
53
  );
52
54
 
53
55
  return (
54
- <LabeledField label={label} labelPosition="inset">
56
+ <LabeledField label={label} labelType="inset">
55
57
  <InputField.Root id={id} width={size}>
56
58
  <InputField.NumberInput
57
59
  value={value === undefined ? value : round(value, 2)}
@@ -12,7 +12,7 @@ interface DividerProps {
12
12
 
13
13
  export const Divider = memo(function Divider({
14
14
  variant = "normal",
15
- overflow = 0,
15
+ overflow,
16
16
  className,
17
17
  style,
18
18
  ...props
@@ -29,8 +29,10 @@ export const Divider = memo(function Divider({
29
29
  className
30
30
  )}
31
31
  style={{
32
- marginLeft: `-${overflow}px`,
33
- marginRight: `-${overflow}px`,
32
+ ...(typeof overflow === "number" && {
33
+ marginLeft: `-${overflow}px`,
34
+ marginRight: `-${overflow}px`,
35
+ }),
34
36
  ...style,
35
37
  }}
36
38
  {...props}
@@ -40,7 +42,7 @@ export const Divider = memo(function Divider({
40
42
 
41
43
  export const DividerVertical = memo(function DividerVertical({
42
44
  variant = "normal",
43
- overflow = 0,
45
+ overflow,
44
46
  className,
45
47
  style,
46
48
  ...props
@@ -57,8 +59,10 @@ export const DividerVertical = memo(function DividerVertical({
57
59
  className
58
60
  )}
59
61
  style={{
60
- marginTop: `-${overflow}px`,
61
- marginBottom: `-${overflow}px`,
62
+ ...(typeof overflow === "number" && {
63
+ marginTop: `-${overflow}px`,
64
+ marginBottom: `-${overflow}px`,
65
+ }),
62
66
  ...style,
63
67
  }}
64
68
  {...props}
@@ -1,5 +1,10 @@
1
+ "use client";
2
+
1
3
  import { DragHandleDots2Icon } from "@noya-app/noya-icons";
2
- import { memoGeneric } from "@noya-app/react-utils";
4
+ import {
5
+ memoGeneric,
6
+ useControlledOrUncontrolled,
7
+ } from "@noya-app/react-utils";
3
8
  import React, { useCallback, useState } from "react";
4
9
  import { cssVars } from "../theme";
5
10
  import { cx } from "../utils/classNames";
@@ -12,21 +17,33 @@ import { MenuItem } from "./internal/Menu";
12
17
  export const DraggableMenuButton = memoGeneric(function DraggableMenuButton<
13
18
  T extends string,
14
19
  >({
20
+ open: openProp,
21
+ onOpenChange: onOpenChangeProp,
15
22
  items,
16
23
  onSelect,
17
24
  onClick,
18
25
  isVisible = true,
19
26
  className,
20
27
  style,
28
+ ...rest
21
29
  }: {
30
+ open?: boolean;
31
+ onOpenChange?: (open: boolean) => void;
22
32
  items?: MenuItem<T>[];
23
33
  onSelect?: (value: T) => void;
24
34
  onClick?: () => void;
25
35
  isVisible?: boolean;
26
36
  className?: string;
27
37
  style?: React.CSSProperties;
28
- }) {
29
- const [open, setOpen] = useState(false);
38
+ } & Omit<
39
+ React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>,
40
+ "onSelect"
41
+ >) {
42
+ const [open, setOpen] = useControlledOrUncontrolled({
43
+ value: openProp,
44
+ onChange: onOpenChangeProp,
45
+ defaultValue: false,
46
+ });
30
47
  const [downPosition, setDownPosition] = useState<{
31
48
  x: number;
32
49
  y: number;
@@ -64,7 +81,7 @@ export const DraggableMenuButton = memoGeneric(function DraggableMenuButton<
64
81
 
65
82
  setDownPosition(null);
66
83
  },
67
- [downPosition, open]
84
+ [downPosition, open, setOpen]
68
85
  );
69
86
 
70
87
  const handleOpenChange = useCallback(
@@ -76,6 +93,7 @@ export const DraggableMenuButton = memoGeneric(function DraggableMenuButton<
76
93
 
77
94
  return (
78
95
  <div
96
+ {...rest}
79
97
  className={cx(
80
98
  "cursor-pointer rounded flex items-center justify-center py-[2px] hover:bg-input-background-light active:bg-active-background ",
81
99
  className
@@ -1,5 +1,7 @@
1
+ "use client";
2
+
1
3
  import { useControlledOrUncontrolled } from "@noya-app/react-utils";
2
- import * as Dialog from "@radix-ui/react-dialog";
4
+ import { Dialog } from "radix-ui";
3
5
  import * as React from "react";
4
6
  import { useImperativeHandle } from "react";
5
7
  import { cx } from "../utils/classNames";
@@ -14,6 +16,8 @@ export type DrawerProps = {
14
16
  className?: string;
15
17
  id?: string;
16
18
  style?: React.CSSProperties;
19
+ overlayClassName?: string;
20
+ overlayStyle?: React.CSSProperties;
17
21
  positioning?: "fixed" | "absolute";
18
22
  side?: "left" | "right";
19
23
  portalled?: boolean;
@@ -35,6 +39,8 @@ export const Drawer = React.memo(
35
39
  side = "left",
36
40
  portalled = true,
37
41
  portalContainer,
42
+ overlayClassName,
43
+ overlayStyle,
38
44
  }: DrawerProps,
39
45
  forwardedRef: React.ForwardedRef<SidebarRef>
40
46
  ) {
@@ -61,14 +67,16 @@ export const Drawer = React.memo(
61
67
  <Dialog.Overlay
62
68
  className={cx(
63
69
  "inset-0 bg-black/50",
64
- positioning === "absolute" ? "absolute" : "fixed"
70
+ positioning === "absolute" ? "absolute" : "fixed",
71
+ overlayClassName
65
72
  )}
73
+ style={overlayStyle}
66
74
  />
67
75
  <Dialog.Content
68
76
  id={id}
69
77
  style={style}
70
78
  className={cx(
71
- `top-0 bottom-0 z-50 overflow-y-auto bg-sidebar-background rounded-none max-h-none max-w-none sm:max-w-sm w-full border-divider-strong`,
79
+ `top-0 bottom-0 z-50 overflow-y-auto bg-sidebar-background rounded-none max-h-none max-w-none sm:!max-w-sm w-full border-divider-strong`,
72
80
  positioning === "absolute" ? "absolute" : "fixed",
73
81
  side === "left" ? "left-0 border-r" : "right-0 border-l",
74
82
  className
@@ -1,9 +1,16 @@
1
+ "use client";
2
+
1
3
  import { useKeyboardShortcuts } from "@noya-app/noya-keymap";
2
- import { forwardRefGeneric, memoGeneric } from "@noya-app/react-utils";
3
- import * as RadixDropdownMenu from "@radix-ui/react-dropdown-menu";
4
+ import {
5
+ forwardRefGeneric,
6
+ memoGeneric,
7
+ useControlledOrUncontrolled,
8
+ } from "@noya-app/react-utils";
9
+ import { DropdownMenu as RadixDropdownMenu } from "radix-ui";
4
10
  import React, {
5
11
  ComponentProps,
6
12
  ForwardedRef,
13
+ ReactNode,
7
14
  SyntheticEvent,
8
15
  useMemo,
9
16
  } from "react";
@@ -11,6 +18,7 @@ import {
11
18
  portalScopeProps,
12
19
  usePortalScopeId,
13
20
  } from "../contexts/PortalScopeContext";
21
+ import Button, { ButtonProps } from "./Button";
14
22
  import { MenuProps } from "./ContextMenu";
15
23
  import { getKeyboardShortcutsForMenuItems, styles } from "./internal/Menu";
16
24
  import { MenuViewport } from "./internal/MenuViewport";
@@ -19,7 +27,13 @@ import { MenuViewport } from "./internal/MenuViewport";
19
27
  * Root
20
28
  * ------------------------------------------------------------------------- */
21
29
 
22
- export type DropdownRootProps<T extends string> = MenuProps<T> & {
30
+ export type DropdownRootProps<T extends string> = Omit<
31
+ MenuProps<T>,
32
+ "children"
33
+ > & {
34
+ children?: ReactNode | ((props: { open: boolean }) => ReactNode);
35
+ title?: ReactNode;
36
+ icon?: ButtonProps["icon"];
23
37
  open?: boolean;
24
38
  onCloseAutoFocus?: React.EventHandler<SyntheticEvent<unknown>>;
25
39
  emptyState?: React.ReactNode;
@@ -50,7 +64,7 @@ const DropdownMenuRoot = forwardRefGeneric(function DropdownMenuRoot<
50
64
  onSelect,
51
65
  shouldBindKeyboardShortcuts,
52
66
  onOpenChange,
53
- open,
67
+ open: openProp,
54
68
  onCloseAutoFocus,
55
69
  side,
56
70
  sideOffset = 4,
@@ -59,8 +73,16 @@ const DropdownMenuRoot = forwardRefGeneric(function DropdownMenuRoot<
59
73
  collisionPadding = 8,
60
74
  emptyState,
61
75
  contentStyle,
76
+ title,
77
+ icon,
62
78
  } = props;
63
79
 
80
+ const [open, setOpen] = useControlledOrUncontrolled({
81
+ value: openProp,
82
+ onChange: onOpenChange,
83
+ defaultValue: false,
84
+ });
85
+
64
86
  const portalScopeId = usePortalScopeId();
65
87
 
66
88
  const keymap = useMemo(
@@ -83,12 +105,18 @@ const DropdownMenuRoot = forwardRefGeneric(function DropdownMenuRoot<
83
105
  );
84
106
 
85
107
  return (
86
- <RadixDropdownMenu.Root onOpenChange={onOpenChange} open={open}>
108
+ <RadixDropdownMenu.Root onOpenChange={setOpen} open={open}>
87
109
  <RadixDropdownMenu.Trigger
88
110
  ref={forwardedRef as ForwardedRef<any>}
89
111
  asChild
90
112
  >
91
- {children}
113
+ {typeof children === "function"
114
+ ? children({ open })
115
+ : children || (
116
+ <Button icon={icon} active={open} iconRight="DropdownChevronIcon">
117
+ {title}
118
+ </Button>
119
+ )}
92
120
  </RadixDropdownMenu.Trigger>
93
121
  <RadixDropdownMenu.Portal>
94
122
  <RadixDropdownMenu.Content
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import { cx, InputField } from "@noya-app/noya-designsystem";
2
4
  import React, {
3
5
  forwardRef,
@@ -0,0 +1,108 @@
1
+ import React from "react";
2
+ import { formatByteSize } from "../utils/formatByteSize";
3
+ import { ActivityIndicator } from "./ActivityIndicator";
4
+ import { Divider } from "./Divider";
5
+ import { MediaThumbnail } from "./MediaThumbnail";
6
+ import { Progress } from "./Progress";
7
+ import { Heading5, Small } from "./Text";
8
+ import { Checkmark } from "./internal/Checkmark";
9
+
10
+ type FileUploadIndicatorProps = {
11
+ name?: string;
12
+ percent?: number;
13
+ total?: number;
14
+ contentType?: string;
15
+ };
16
+
17
+ export function FileUploadIndicator({
18
+ name,
19
+ percent,
20
+ total,
21
+ contentType,
22
+ }: FileUploadIndicatorProps) {
23
+ const done = percent === 1;
24
+ const value = typeof percent === "number" ? Math.round(percent) : 0;
25
+
26
+ return (
27
+ <div className="flex items-center gap-2">
28
+ <div className="w-12 h-12 bg-background rounded-md overflow-hidden">
29
+ <MediaThumbnail
30
+ size="medium"
31
+ fileName={name}
32
+ contentType={contentType}
33
+ />
34
+ </div>
35
+ <div className="flex flex-col flex-1 justify-center gap-1 min-w-0">
36
+ {name && (
37
+ <Small color="text" className="truncate leading-none">
38
+ {name}
39
+ </Small>
40
+ )}
41
+ {typeof total === "number" && typeof percent === "number" && (
42
+ <Small
43
+ color="textSubtle"
44
+ className="leading-none"
45
+ style={{ fontSize: "11px" }}
46
+ >
47
+ {formatByteSize(Math.round((percent / 100) * total))} /{" "}
48
+ {formatByteSize(total)}
49
+ </Small>
50
+ )}
51
+ {typeof value === "number" && (
52
+ <Progress
53
+ value={value}
54
+ style={{ marginTop: "3px", height: "3px" }}
55
+ className="rounded-sm"
56
+ colorScheme="secondary"
57
+ />
58
+ )}
59
+ </div>
60
+ <div className="flex items-center justify-center gap-2">
61
+ {typeof value === "number" && (
62
+ <Small
63
+ color="textSubtle"
64
+ style={{ lineHeight: "1", fontSize: "13px" }}
65
+ className="tabular-nums text-right w-10"
66
+ >
67
+ {value}%
68
+ </Small>
69
+ )}
70
+ {done ? (
71
+ <div className="w-[15px] h-[15px] bg-secondary rounded-full flex items-center justify-center text-white">
72
+ <Checkmark />
73
+ </div>
74
+ ) : (
75
+ <ActivityIndicator size={15} />
76
+ )}
77
+ </div>
78
+ </div>
79
+ );
80
+ }
81
+
82
+ export function FileUploadList({
83
+ uploads,
84
+ }: {
85
+ uploads: (FileUploadIndicatorProps & { id: string })[];
86
+ }) {
87
+ return (
88
+ <>
89
+ <div className="flex items-center gap-2 py-2 px-4">
90
+ <Heading5>
91
+ Uploading {uploads.length} {uploads.length === 1 ? "asset" : "assets"}
92
+ </Heading5>
93
+ </div>
94
+ <Divider />
95
+ <div className="flex flex-col gap-3 max-h-[500px] overflow-y-auto overflow-x-hidden py-4 px-4">
96
+ {uploads.map((upload) => (
97
+ <FileUploadIndicator
98
+ key={upload.id}
99
+ name={upload?.name}
100
+ percent={upload?.percent}
101
+ total={upload?.total}
102
+ contentType={upload?.contentType}
103
+ />
104
+ ))}
105
+ </div>
106
+ </>
107
+ );
108
+ }
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import { Sketch } from "@noya-app/noya-file-format";
2
4
  // import { useSketchImage } from 'noya-renderer';
3
5
  import * as React from "react";
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import {
2
4
  cssVars,
3
5
  Divider,