@noya-app/noya-designsystem 0.1.71 → 0.1.73

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.71",
3
+ "version": "0.1.73",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.ts",
@@ -28,7 +28,7 @@
28
28
  "@noya-app/noya-geometry": "0.1.16",
29
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;
@@ -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 }
@@ -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?.authId ?? 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,439 @@
1
+ "use client";
2
+
3
+ import React, {
4
+ CSSProperties,
5
+ ForwardedRef,
6
+ ReactNode,
7
+ UIEventHandler,
8
+ useCallback,
9
+ useImperativeHandle,
10
+ useLayoutEffect,
11
+ useMemo,
12
+ useRef,
13
+ useState,
14
+ } from "react";
15
+ import { flushSync } from "react-dom";
16
+ import { IVirtualizedList } from "./IVirtualizedList";
17
+ import { ScrollArea2 } from "./ScrollArea2";
18
+
19
+ export type ItemMeasurement = {
20
+ index: number;
21
+ size: number;
22
+ start: number;
23
+ end: number;
24
+ };
25
+
26
+ export type VirtualizedRenderItem = (index: number) => ReactNode;
27
+
28
+ type VirtualizedPadding =
29
+ | number
30
+ | {
31
+ top?: number;
32
+ bottom?: number;
33
+ };
34
+
35
+ type PaddingValues = {
36
+ top: number;
37
+ bottom: number;
38
+ };
39
+
40
+ type MeasurementResult = {
41
+ measurements: ItemMeasurement[];
42
+ totalHeight: number;
43
+ };
44
+
45
+ type VisibleRange = {
46
+ start: number;
47
+ end: number;
48
+ };
49
+
50
+ export function calculateScrollOffsetForIndex({
51
+ measurements,
52
+ height,
53
+ totalHeight,
54
+ targetIndex,
55
+ currentScrollTop,
56
+ }: {
57
+ measurements: ItemMeasurement[];
58
+ height: number;
59
+ totalHeight: number;
60
+ targetIndex: number;
61
+ currentScrollTop: number;
62
+ }): number | undefined {
63
+ if (!measurements.length) return;
64
+
65
+ const index = Math.min(Math.max(targetIndex, 0), measurements.length - 1);
66
+ const measurement = measurements[index];
67
+
68
+ if (!measurement) return;
69
+
70
+ const viewportTop = currentScrollTop;
71
+ const viewportBottom = viewportTop + height;
72
+ const itemTop = measurement.start;
73
+ const itemBottom = measurement.end;
74
+
75
+ let nextScrollTop = viewportTop;
76
+
77
+ if (itemTop < viewportTop) {
78
+ nextScrollTop = itemTop;
79
+ } else if (itemBottom > viewportBottom) {
80
+ const canAlignTop = height >= measurement.size;
81
+
82
+ nextScrollTop = canAlignTop ? itemTop : itemBottom - height;
83
+ } else {
84
+ return;
85
+ }
86
+
87
+ const maxScrollTop = Math.max(0, totalHeight - height);
88
+ nextScrollTop = Math.max(0, Math.min(nextScrollTop, maxScrollTop));
89
+
90
+ if (nextScrollTop === viewportTop) return;
91
+
92
+ return nextScrollTop;
93
+ }
94
+
95
+ export function resolvePaddingValues(
96
+ padding?: VirtualizedPadding
97
+ ): PaddingValues {
98
+ if (typeof padding === "number") {
99
+ return { top: padding, bottom: padding };
100
+ }
101
+
102
+ return {
103
+ top: padding?.top ?? 0,
104
+ bottom: padding?.bottom ?? 0,
105
+ };
106
+ }
107
+
108
+ export function clampGapValue(gap: number | undefined): number {
109
+ return Math.max(0, gap ?? 0);
110
+ }
111
+
112
+ export function buildMeasurements({
113
+ itemCount,
114
+ getItemHeight,
115
+ paddingTop,
116
+ paddingBottom,
117
+ gap,
118
+ }: {
119
+ itemCount: number;
120
+ getItemHeight: (index: number) => number;
121
+ paddingTop: number;
122
+ paddingBottom: number;
123
+ gap: number;
124
+ }): MeasurementResult {
125
+ const measurements: ItemMeasurement[] = [];
126
+ let offset = paddingTop;
127
+
128
+ for (let index = 0; index < itemCount; index += 1) {
129
+ const size = Math.max(0, getItemHeight(index));
130
+ const start = offset;
131
+ const end = start + size;
132
+ measurements.push({ index, size, start, end });
133
+ offset = end + gap;
134
+ }
135
+
136
+ const totalHeight = measurements.length
137
+ ? measurements[measurements.length - 1].end + paddingBottom
138
+ : paddingTop + paddingBottom;
139
+
140
+ return { measurements, totalHeight };
141
+ }
142
+
143
+ export function findFirstVisibleIndex(
144
+ measurements: ItemMeasurement[],
145
+ offset: number
146
+ ): number {
147
+ if (!measurements.length) return 0;
148
+
149
+ let low = 0;
150
+ let high = measurements.length - 1;
151
+ let match = measurements.length - 1;
152
+
153
+ while (low <= high) {
154
+ const middle = Math.floor((low + high) / 2);
155
+ const measurement = measurements[middle];
156
+
157
+ if (measurement.end > offset) {
158
+ match = middle;
159
+ high = middle - 1;
160
+ } else {
161
+ low = middle + 1;
162
+ }
163
+ }
164
+
165
+ return match;
166
+ }
167
+
168
+ export function calculateVisibleRange({
169
+ measurements,
170
+ scrollTop,
171
+ height,
172
+ overscan,
173
+ }: {
174
+ measurements: ItemMeasurement[];
175
+ scrollTop: number;
176
+ height: number;
177
+ overscan: number;
178
+ }): VisibleRange {
179
+ if (!measurements.length) {
180
+ return { start: 0, end: -1 };
181
+ }
182
+
183
+ const viewBottom = scrollTop + height;
184
+ const firstVisible = findFirstVisibleIndex(measurements, scrollTop);
185
+ let lastVisibleExclusive = firstVisible;
186
+
187
+ while (
188
+ lastVisibleExclusive < measurements.length &&
189
+ measurements[lastVisibleExclusive].start < viewBottom
190
+ ) {
191
+ lastVisibleExclusive += 1;
192
+ }
193
+
194
+ const lastVisible = Math.max(
195
+ firstVisible,
196
+ Math.min(measurements.length - 1, lastVisibleExclusive - 1)
197
+ );
198
+ const overscanCount = Math.max(0, Math.floor(overscan));
199
+
200
+ return {
201
+ start: Math.max(0, firstVisible - overscanCount),
202
+ end: Math.min(measurements.length - 1, lastVisible + overscanCount),
203
+ };
204
+ }
205
+
206
+ export type VirtualizedProps = {
207
+ height: number;
208
+ itemCount: number;
209
+ getItemHeight: (index: number) => number;
210
+ renderItem: VirtualizedRenderItem;
211
+ overscan?: number;
212
+ className?: string;
213
+ style?: CSSProperties;
214
+ innerClassName?: string;
215
+ innerStyle?: CSSProperties;
216
+ onScroll?: (scrollTop: number) => void;
217
+ itemKey?: (index: number) => string | number;
218
+ initialScrollOffset?: number;
219
+ /**
220
+ * When true (default) we flush scroll updates with `flushSync`
221
+ * to avoid visual gaps during fast scrolling.
222
+ */
223
+ syncScrollUpdates?: boolean;
224
+ /**
225
+ * Optional padding applied above and below the content. Numbers apply to both.
226
+ */
227
+ padding?: VirtualizedPadding;
228
+ /**
229
+ * Optional constant gap inserted between items.
230
+ */
231
+ gap?: number;
232
+ /**
233
+ * Render content inside ScrollArea2 instead of a plain div.
234
+ */
235
+ useScrollArea?: boolean;
236
+ /**
237
+ * Optional class applied to the ScrollArea viewport when `useScrollArea` is true.
238
+ */
239
+ scrollAreaViewportClassName?: string;
240
+ };
241
+
242
+ const DEFAULT_OVERSCAN = 2;
243
+
244
+ const VirtualizedInner = React.forwardRef(function Virtualized(
245
+ {
246
+ height,
247
+ itemCount,
248
+ getItemHeight,
249
+ renderItem,
250
+ overscan = DEFAULT_OVERSCAN,
251
+ className,
252
+ style,
253
+ innerClassName,
254
+ innerStyle,
255
+ onScroll,
256
+ itemKey,
257
+ initialScrollOffset = 0,
258
+ syncScrollUpdates = true,
259
+ padding,
260
+ gap = 0,
261
+ useScrollArea = false,
262
+ scrollAreaViewportClassName,
263
+ }: VirtualizedProps,
264
+ forwardedRef: ForwardedRef<IVirtualizedList>
265
+ ) {
266
+ const containerRef = useRef<HTMLDivElement | null>(null);
267
+ const setScrollElement = useCallback((element: HTMLDivElement | null) => {
268
+ containerRef.current = element;
269
+ }, []);
270
+ const [scrollTop, setScrollTop] = useState(initialScrollOffset);
271
+ const pendingInitialOffset = useRef<number | null>(initialScrollOffset);
272
+
273
+ useLayoutEffect(() => {
274
+ if (pendingInitialOffset.current == null) return;
275
+ if (containerRef.current) {
276
+ containerRef.current.scrollTop = pendingInitialOffset.current;
277
+ setScrollTop(pendingInitialOffset.current);
278
+ }
279
+ pendingInitialOffset.current = null;
280
+ }, []);
281
+
282
+ const paddingValues = useMemo(() => resolvePaddingValues(padding), [padding]);
283
+
284
+ const gapValue = useMemo(() => clampGapValue(gap), [gap]);
285
+
286
+ const { measurements, totalHeight } = useMemo(
287
+ () =>
288
+ buildMeasurements({
289
+ itemCount,
290
+ getItemHeight,
291
+ paddingTop: paddingValues.top,
292
+ paddingBottom: paddingValues.bottom,
293
+ gap: gapValue,
294
+ }),
295
+ [
296
+ gapValue,
297
+ getItemHeight,
298
+ itemCount,
299
+ paddingValues.bottom,
300
+ paddingValues.top,
301
+ ]
302
+ );
303
+
304
+ const visibleRange = useMemo(
305
+ () =>
306
+ calculateVisibleRange({
307
+ measurements,
308
+ scrollTop,
309
+ height,
310
+ overscan,
311
+ }),
312
+ [height, measurements, overscan, scrollTop]
313
+ );
314
+
315
+ const handleScroll = useCallback<UIEventHandler<HTMLDivElement>>(
316
+ (event) => {
317
+ const nextOffset = event.currentTarget.scrollTop;
318
+
319
+ if (nextOffset === scrollTop) return;
320
+
321
+ if (syncScrollUpdates) {
322
+ flushSync(() => {
323
+ setScrollTop(nextOffset);
324
+ });
325
+ } else {
326
+ setScrollTop(nextOffset);
327
+ }
328
+ onScroll?.(nextOffset);
329
+ },
330
+ [onScroll, scrollTop, syncScrollUpdates]
331
+ );
332
+
333
+ useImperativeHandle(
334
+ forwardedRef,
335
+ () => ({
336
+ scrollToIndex(index: number) {
337
+ const container = containerRef.current;
338
+ if (!container) return;
339
+
340
+ const nextScrollTop = calculateScrollOffsetForIndex({
341
+ measurements,
342
+ height,
343
+ totalHeight,
344
+ targetIndex: index,
345
+ currentScrollTop: container.scrollTop,
346
+ });
347
+
348
+ if (nextScrollTop == null) return;
349
+
350
+ container.scrollTop = nextScrollTop;
351
+ setScrollTop(nextScrollTop);
352
+ },
353
+ }),
354
+ [height, measurements, totalHeight]
355
+ );
356
+
357
+ const containerStyle = useMemo<CSSProperties>(
358
+ () => ({
359
+ height,
360
+ overflowY: "auto",
361
+ overflowX: "hidden",
362
+ ...style,
363
+ }),
364
+ [height, style]
365
+ );
366
+
367
+ const scrollAreaStyle = useMemo<CSSProperties>(
368
+ () => ({
369
+ height,
370
+ ...style,
371
+ }),
372
+ [height, style]
373
+ );
374
+
375
+ const totalContent = (
376
+ <div
377
+ className={innerClassName}
378
+ style={{
379
+ position: "relative",
380
+ width: "100%",
381
+ height: totalHeight,
382
+ ...innerStyle,
383
+ }}
384
+ >
385
+ {visibleRange.end >= visibleRange.start &&
386
+ Array.from(
387
+ { length: visibleRange.end - visibleRange.start + 1 },
388
+ (_, offset) => {
389
+ const index = visibleRange.start + offset;
390
+ const measurement = measurements[index];
391
+ const key = itemKey ? itemKey(index) : index;
392
+
393
+ return (
394
+ <div
395
+ key={key}
396
+ style={{
397
+ position: "absolute",
398
+ top: measurement.start,
399
+ height: measurement.size,
400
+ width: "100%",
401
+ }}
402
+ >
403
+ {renderItem(index)}
404
+ </div>
405
+ );
406
+ }
407
+ )}
408
+ </div>
409
+ );
410
+
411
+ if (useScrollArea) {
412
+ return (
413
+ <ScrollArea2
414
+ className={className}
415
+ style={scrollAreaStyle}
416
+ viewportClassName={scrollAreaViewportClassName}
417
+ viewportProps={{
418
+ ref: setScrollElement,
419
+ onScroll: handleScroll,
420
+ }}
421
+ >
422
+ {totalContent}
423
+ </ScrollArea2>
424
+ );
425
+ }
426
+
427
+ return (
428
+ <div
429
+ ref={setScrollElement}
430
+ className={className}
431
+ style={containerStyle}
432
+ onScroll={handleScroll}
433
+ >
434
+ {totalContent}
435
+ </div>
436
+ );
437
+ });
438
+
439
+ export const Virtualized = React.memo(VirtualizedInner);