@noya-app/noya-designsystem 0.1.70 → 0.1.72

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noya-app/noya-designsystem",
3
- "version": "0.1.70",
3
+ "version": "0.1.72",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.ts",
@@ -26,9 +26,9 @@
26
26
  "@noya-app/noya-colorpicker": "0.1.30",
27
27
  "@noya-app/noya-utils": "0.1.9",
28
28
  "@noya-app/noya-geometry": "0.1.16",
29
- "@noya-app/noya-icons": "0.1.15",
29
+ "@noya-app/noya-icons": "0.1.16",
30
30
  "@noya-app/noya-keymap": "0.1.4",
31
- "@noya-app/noya-tailwind-config": "0.1.8",
31
+ "@noya-app/noya-tailwind-config": "0.1.9",
32
32
  "radix-ui": "1.4.2",
33
33
  "@radix-ui/react-compose-refs": "^1.0.0",
34
34
  "@radix-ui/primitive": "^1.0.1",
@@ -38,17 +38,13 @@
38
38
  "immer": "9.0.5",
39
39
  "kiwi.js": "^1.1.3",
40
40
  "react-resizable-panels": "3.0.3",
41
- "react-window": "^1.8.8",
42
- "vscode-fuzzy-scorer": "^0.0.4",
43
- "react-virtualized": "9.22.6"
41
+ "vscode-fuzzy-scorer": "^0.0.4"
44
42
  },
45
43
  "devDependencies": {
46
44
  "@tailwindcss/container-queries": "^0.1.1",
47
45
  "@tailwindcss/typography": "^0.5.19",
48
46
  "@types/react": "*",
49
47
  "@types/react-dom": "*",
50
- "@types/react-virtualized": "^9.21.30",
51
- "@types/react-window": "^1.8.5",
52
48
  "tailwindcss": "^3.4.17",
53
49
  "postcss": "^8.5.3",
54
50
  "autoprefixer": "^10.4.21",
@@ -85,7 +85,7 @@ export function BaseToolbar({
85
85
  {children && (
86
86
  <div
87
87
  className={
88
- "n-flex n-items-center n-justify-center n-text-text-muted n-pointer-events-none @2xl/toolbar:n-absolute n-inset-0"
88
+ "n-flex n-items-center n-justify-center n-text-text-muted n-pointer-events-none @xl/toolbar:!n-absolute n-inset-0"
89
89
  }
90
90
  >
91
91
  <div className="n-flex n-items-center n-justify-center n-pointer-events-auto">
@@ -48,6 +48,7 @@ export const BreadcrumbSlash = () => {
48
48
  xmlns="http://www.w3.org/2000/svg"
49
49
  opacity={0.5}
50
50
  style={slashStyle}
51
+ className="n-flex-none"
51
52
  >
52
53
  <path
53
54
  d="M1 16L6 1"
@@ -1,6 +1,5 @@
1
1
  "use client";
2
2
 
3
- import { Size } from "@noya-app/noya-geometry";
4
3
  import { forwardRefGeneric } from "@noya-app/react-utils";
5
4
  import React, { CSSProperties } from "react";
6
5
  import { Grid } from "./Grid";
@@ -34,7 +33,7 @@ export type CollectionRenderActionProps<T> = {
34
33
  export interface CollectionProps<T, M extends string = string> {
35
34
  className?: string;
36
35
  items: T[];
37
- virtualized?: Size;
36
+ virtualized?: number;
38
37
  getId: (item: T) => string;
39
38
  getName: (item: T) => string;
40
39
  getHref?: (item: T) => string;
@@ -47,6 +47,7 @@ export type BaseComboboxProps<T extends string> = {
47
47
  readOnly?: boolean;
48
48
  tabBehavior?: "autocomplete" | "select";
49
49
  openMenuBehavior?: "showAllItems" | "showFilteredItems";
50
+ iconPosition?: "left" | "right";
50
51
  };
51
52
 
52
53
  export type StringModeOnlyProps = {
@@ -103,6 +104,7 @@ export const Combobox = memoGeneric(
103
104
  openMenuBehavior = "showFilteredItems",
104
105
  onFocus,
105
106
  onDeleteWhenEmpty,
107
+ iconPosition = "right",
106
108
  ...rest
107
109
  }: ComboboxProps<T>,
108
110
  forwardedRef: ForwardedRef<ComboboxRef<T>>
@@ -500,6 +502,7 @@ export const Combobox = memoGeneric(
500
502
  onSelectItem={handleUpdateSelection}
501
503
  onHoverIndex={handleIndexChange}
502
504
  listSize={adjustedListSize}
505
+ iconPosition={iconPosition}
503
506
  style={{
504
507
  flex: `0 0 ${computedHeight}px`,
505
508
  }}
@@ -62,7 +62,7 @@ export const ComboboxMenu = memoGeneric(
62
62
  return item.value;
63
63
  }}
64
64
  data={items}
65
- virtualized={listSize}
65
+ virtualized={listSize.height}
66
66
  {...(itemHeight ? { itemHeight } : {})}
67
67
  {...(getRowHeightForItem
68
68
  ? { getItemHeightForItem: getRowHeightForItem as any }
@@ -350,6 +350,10 @@ const InputFieldTypeahead = (props: {
350
350
  * NumberInput
351
351
  * ------------------------------------------------------------------------- */
352
352
 
353
+ type NumberChangeOptions = {
354
+ isEmpty: boolean;
355
+ };
356
+
353
357
  type InputFieldNumberInputProps = Omit<
354
358
  TextInputProps,
355
359
  "value" | "onChange" | "onKeyDown" | "onSubmit" | "readOnly"
@@ -361,9 +365,10 @@ type InputFieldNumberInputProps = Omit<
361
365
  onBlur?: FocusEventHandler;
362
366
  readOnly?: boolean;
363
367
  className?: string;
368
+ triggersOnEmpty?: boolean;
364
369
  } & (
365
370
  | {
366
- onChange: (value: number) => void;
371
+ onChange: (value: number, options: NumberChangeOptions) => void;
367
372
  }
368
373
  | {
369
374
  onSubmit: (value: number) => void;
@@ -375,7 +380,15 @@ function parseNumber(value: string) {
375
380
  }
376
381
 
377
382
  function InputFieldNumberInput(props: InputFieldNumberInputProps) {
378
- const { value, placeholder, onNudge, onBlur, readOnly, ...rest } = props;
383
+ const {
384
+ value,
385
+ placeholder,
386
+ onNudge,
387
+ onBlur,
388
+ readOnly,
389
+ triggersOnEmpty,
390
+ ...rest
391
+ } = props;
379
392
  const onSubmit = "onSubmit" in props ? props.onSubmit : undefined;
380
393
  const onChange = "onChange" in props ? props.onChange : undefined;
381
394
 
@@ -414,6 +427,9 @@ function InputFieldNumberInput(props: InputFieldNumberInputProps) {
414
427
  (value: string) => {
415
428
  if (value === "" || value === "-" || value === ".") {
416
429
  setInternalValue(value);
430
+ if (triggersOnEmpty) {
431
+ onChange?.(0, { isEmpty: true });
432
+ }
417
433
  return;
418
434
  }
419
435
 
@@ -427,10 +443,10 @@ function InputFieldNumberInput(props: InputFieldNumberInputProps) {
427
443
  const newValue = parseNumber(value);
428
444
 
429
445
  if (!isNaN(newValue)) {
430
- onChange?.(newValue);
446
+ onChange?.(newValue, { isEmpty: false });
431
447
  }
432
448
  },
433
- [onChange]
449
+ [onChange, triggersOnEmpty]
434
450
  );
435
451
 
436
452
  const handleBlur = useCallback(
@@ -44,7 +44,7 @@ export function ListMenu<T extends string>({
44
44
  .flatMap((item) =>
45
45
  isSelectableMenuItem(item) && item.checked ? [item.value] : []
46
46
  )
47
- .concat(selectedIdsProp as T[]);
47
+ .concat(selectedIdsProp ?? []);
48
48
 
49
49
  let chunks = chunkBy(
50
50
  items,
@@ -23,7 +23,8 @@ export const Message = memo(
23
23
  ref: React.ForwardedRef<HTMLDivElement>
24
24
  ) {
25
25
  const randomId = uuid();
26
- const userMessageBackgroundColor = colorFromString(user?.id ?? randomId);
26
+ const resolvedUserId = user?.userId ?? user?.connectionId ?? randomId;
27
+ const userMessageBackgroundColor = colorFromString(resolvedUserId);
27
28
  const styles = useMemo(
28
29
  () =>
29
30
  role === "user"
@@ -50,7 +51,7 @@ export const Message = memo(
50
51
  </Avatar>
51
52
  ) : (
52
53
  <Avatar
53
- userId={!user ? randomId : user?.id}
54
+ userId={resolvedUserId}
54
55
  name={user?.name ?? "Anonymous"}
55
56
  image={user?.image}
56
57
  size={avatarSize}
@@ -17,21 +17,41 @@ const SCROLLBAR_THUMB_VERTICAL =
17
17
  const SCROLLBAR_THUMB_HORIZONTAL =
18
18
  "scroll-component n-h-full n-rounded n-bg-scrollbar";
19
19
 
20
+ type ScrollAreaViewportProps = React.HTMLAttributes<HTMLDivElement> & {
21
+ ref?: React.Ref<HTMLDivElement>;
22
+ };
23
+
20
24
  export function ScrollArea2({
21
25
  children,
22
26
  className,
23
27
  viewportClassName,
24
28
  orientation = "vertical",
29
+ style,
30
+ viewportProps,
25
31
  }: {
26
32
  children: React.ReactNode;
27
33
  className?: string;
28
34
  viewportClassName?: string;
29
35
  orientation?: "vertical" | "horizontal" | "both";
36
+ style?: React.CSSProperties;
37
+ viewportProps?: ScrollAreaViewportProps;
30
38
  }) {
39
+ const {
40
+ className: viewportPropsClassName,
41
+ ref: viewportRef,
42
+ ...viewportRest
43
+ } = viewportProps ?? {};
44
+
31
45
  return (
32
- <ScrollArea.Root className={cx("n-relative", className)}>
46
+ <ScrollArea.Root className={cx("n-relative", className)} style={style}>
33
47
  <ScrollArea.Viewport
34
- className={cx("n-h-full n-overscroll-contain", viewportClassName)}
48
+ ref={viewportRef}
49
+ className={cx(
50
+ "n-h-full n-overscroll-contain",
51
+ viewportClassName,
52
+ viewportPropsClassName
53
+ )}
54
+ {...viewportRest}
35
55
  >
36
56
  {children}
37
57
  </ScrollArea.Viewport>
@@ -0,0 +1,128 @@
1
+ "use client";
2
+
3
+ import type {
4
+ OptionModeProps,
5
+ SelectableMenuItem,
6
+ } from "@noya-app/noya-designsystem";
7
+ import { Avatar, Combobox } from "@noya-app/noya-designsystem";
8
+ import React, { useCallback, useMemo, useState } from "react";
9
+
10
+ type ComboboxOptionProps = Omit<
11
+ OptionModeProps<string>,
12
+ "mode" | "items" | "value" | "onSelectItem" | "onChange" | "onHoverItem"
13
+ >;
14
+
15
+ export type UserPickerUser = {
16
+ id: string;
17
+ name?: string | null;
18
+ email?: string | null;
19
+ image?: string | null;
20
+ };
21
+
22
+ export type UserPickerProps<TUser extends UserPickerUser = UserPickerUser> =
23
+ ComboboxOptionProps & {
24
+ users: readonly TUser[];
25
+ value?: string;
26
+ defaultValue?: string;
27
+ onChangeUserId?: (userId: string | undefined) => void;
28
+ onSelectUser?: (user: TUser | undefined) => void;
29
+ };
30
+
31
+ export function UserPicker<TUser extends UserPickerUser>({
32
+ users,
33
+ value,
34
+ defaultValue,
35
+ onChangeUserId,
36
+ onSelectUser,
37
+ placeholder = "Search users...",
38
+ ...rest
39
+ }: UserPickerProps<TUser>) {
40
+ const sortedUsers = useMemo(() => {
41
+ return [...users].sort((a, b) =>
42
+ getUserDisplayName(a).localeCompare(getUserDisplayName(b), undefined, {
43
+ sensitivity: "base",
44
+ })
45
+ );
46
+ }, [users]);
47
+
48
+ const usersById = useMemo(() => {
49
+ return new Map(sortedUsers.map((user) => [user.id, user] as const));
50
+ }, [sortedUsers]);
51
+
52
+ const items = useMemo(() => {
53
+ return sortedUsers.map(createUserMenuItem);
54
+ }, [sortedUsers]);
55
+
56
+ const isControlled = value !== undefined;
57
+ const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
58
+ const selectedUserId = isControlled ? value : uncontrolledValue;
59
+
60
+ const selectedItem = useMemo(() => {
61
+ if (!selectedUserId) return undefined;
62
+ return items.find((item) => item.value === selectedUserId);
63
+ }, [items, selectedUserId]);
64
+
65
+ const updateSelectedUserId = useCallback(
66
+ (userId?: string) => {
67
+ if (!isControlled) {
68
+ setUncontrolledValue(userId);
69
+ }
70
+
71
+ onChangeUserId?.(userId);
72
+ },
73
+ [isControlled, onChangeUserId]
74
+ );
75
+
76
+ const handleSelectItem = useCallback(
77
+ (item: SelectableMenuItem<string>) => {
78
+ updateSelectedUserId(item.value);
79
+ onSelectUser?.(usersById.get(item.value));
80
+ },
81
+ [updateSelectedUserId, onSelectUser, usersById]
82
+ );
83
+
84
+ return (
85
+ <Combobox
86
+ {...rest}
87
+ placeholder={placeholder}
88
+ mode="option"
89
+ items={items}
90
+ value={selectedItem}
91
+ onSelectItem={handleSelectItem}
92
+ iconPosition="left"
93
+ />
94
+ );
95
+ }
96
+
97
+ export function getUserDisplayName(user: UserPickerUser) {
98
+ const name = user.name?.trim();
99
+ const email = user.email?.trim();
100
+
101
+ return name || email || "Anonymous user";
102
+ }
103
+
104
+ function createUserMenuItem(user: UserPickerUser): SelectableMenuItem<string> {
105
+ const title = getUserDisplayName(user);
106
+ const overflow = 3;
107
+
108
+ return {
109
+ value: user.id,
110
+ title,
111
+ icon: (
112
+ <div className="n-relative n-w-[15px] n-h-[15px]">
113
+ <Avatar
114
+ size={15 + overflow * 2}
115
+ name={title}
116
+ userId={user.id}
117
+ image={user.image ?? undefined}
118
+ className="n-pointer-events-none"
119
+ variant="bare"
120
+ style={{
121
+ position: "absolute",
122
+ inset: -overflow,
123
+ }}
124
+ />
125
+ </div>
126
+ ),
127
+ };
128
+ }