@noya-app/noya-designsystem 0.1.90 → 0.1.92
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/.turbo/turbo-build.log +8 -8
- package/CHANGELOG.md +12 -0
- package/dist/index.d.mts +17 -2
- package/dist/index.d.ts +17 -2
- package/dist/index.js +114 -40
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +146 -72
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/OverlayToolbar.tsx +87 -1
- package/src/components/Toast.tsx +2 -1
- package/src/components/Toolbar.tsx +1 -1
- package/src/components/__tests__/OverlayToolbar.test.tsx +63 -0
- package/src/components/__tests__/Toolbar.test.tsx +3 -0
- package/src/components/workspace/WorkspaceLayout.tsx +19 -2
- package/src/components/workspace/__tests__/WorkspaceLayout.test.tsx +41 -0
package/package.json
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import React, { useMemo } from "react";
|
|
3
|
+
import React, { useCallback, useLayoutEffect, useMemo, useRef } from "react";
|
|
4
4
|
import { cx } from "../utils/classNames";
|
|
5
5
|
import { toolbarTransparentStyle } from "./BaseToolbar";
|
|
6
6
|
|
|
7
7
|
export type OverlayLayout = "bottomCenterTopRight" | "topLeftTopRight";
|
|
8
8
|
|
|
9
|
+
export type OverlayToolbarRect = {
|
|
10
|
+
id: string;
|
|
11
|
+
rect: { x: number; y: number; width: number; height: number };
|
|
12
|
+
};
|
|
13
|
+
|
|
9
14
|
export interface OverlayToolbarProps {
|
|
10
15
|
left?: React.ReactNode;
|
|
11
16
|
right?: React.ReactNode;
|
|
12
17
|
overlayLayout?: OverlayLayout;
|
|
13
18
|
className?: string;
|
|
14
19
|
style?: React.CSSProperties;
|
|
20
|
+
onRectsChange?: (rects: OverlayToolbarRect[]) => void;
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
const panelClassName =
|
|
@@ -23,11 +29,16 @@ export function OverlayToolbar({
|
|
|
23
29
|
overlayLayout = "bottomCenterTopRight",
|
|
24
30
|
className,
|
|
25
31
|
style,
|
|
32
|
+
onRectsChange,
|
|
26
33
|
}: OverlayToolbarProps) {
|
|
27
34
|
const resolvedLayout =
|
|
28
35
|
overlayLayout === "topLeftTopRight"
|
|
29
36
|
? "topLeftTopRight"
|
|
30
37
|
: "bottomCenterTopRight";
|
|
38
|
+
const leftPanelRef = useRef<HTMLDivElement>(null);
|
|
39
|
+
const rightPanelRef = useRef<HTMLDivElement>(null);
|
|
40
|
+
const hasLeft = Boolean(left);
|
|
41
|
+
const hasRight = Boolean(right);
|
|
31
42
|
|
|
32
43
|
// Merge transparent backgrounds with custom style
|
|
33
44
|
const panelStyle = useMemo(
|
|
@@ -38,11 +49,85 @@ export function OverlayToolbar({
|
|
|
38
49
|
[style]
|
|
39
50
|
);
|
|
40
51
|
|
|
52
|
+
const emitRects = useCallback(() => {
|
|
53
|
+
if (!onRectsChange) return;
|
|
54
|
+
|
|
55
|
+
const panels = [
|
|
56
|
+
{
|
|
57
|
+
id:
|
|
58
|
+
resolvedLayout === "topLeftTopRight"
|
|
59
|
+
? "noya-toolbar-top-left"
|
|
60
|
+
: "noya-toolbar-bottom-center",
|
|
61
|
+
element: leftPanelRef.current,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "noya-toolbar-top-right",
|
|
65
|
+
element: rightPanelRef.current,
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
onRectsChange(
|
|
70
|
+
panels.flatMap(({ id, element }) => {
|
|
71
|
+
if (!element) return [];
|
|
72
|
+
|
|
73
|
+
const rect = element.getBoundingClientRect();
|
|
74
|
+
const containerRect = element
|
|
75
|
+
.closest<HTMLElement>("[data-workspace-content]")
|
|
76
|
+
?.getBoundingClientRect();
|
|
77
|
+
return [
|
|
78
|
+
{
|
|
79
|
+
id,
|
|
80
|
+
rect: {
|
|
81
|
+
x: rect.x - (containerRect?.x ?? 0),
|
|
82
|
+
y: rect.y - (containerRect?.y ?? 0),
|
|
83
|
+
width: rect.width,
|
|
84
|
+
height: rect.height,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
})
|
|
89
|
+
);
|
|
90
|
+
}, [onRectsChange, resolvedLayout]);
|
|
91
|
+
|
|
92
|
+
useLayoutEffect(() => {
|
|
93
|
+
if (!onRectsChange) return;
|
|
94
|
+
|
|
95
|
+
let animationFrame: number | undefined;
|
|
96
|
+
const scheduleEmit = () => {
|
|
97
|
+
if (animationFrame !== undefined) {
|
|
98
|
+
cancelAnimationFrame(animationFrame);
|
|
99
|
+
}
|
|
100
|
+
animationFrame = requestAnimationFrame(emitRects);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
emitRects();
|
|
104
|
+
|
|
105
|
+
const resizeObserver =
|
|
106
|
+
typeof ResizeObserver === "undefined"
|
|
107
|
+
? undefined
|
|
108
|
+
: new ResizeObserver(scheduleEmit);
|
|
109
|
+
const panels = [leftPanelRef.current, rightPanelRef.current];
|
|
110
|
+
panels.forEach((panel) => panel && resizeObserver?.observe(panel));
|
|
111
|
+
window.addEventListener("resize", scheduleEmit);
|
|
112
|
+
window.addEventListener("scroll", scheduleEmit, true);
|
|
113
|
+
|
|
114
|
+
return () => {
|
|
115
|
+
if (animationFrame !== undefined) {
|
|
116
|
+
cancelAnimationFrame(animationFrame);
|
|
117
|
+
}
|
|
118
|
+
resizeObserver?.disconnect();
|
|
119
|
+
window.removeEventListener("resize", scheduleEmit);
|
|
120
|
+
window.removeEventListener("scroll", scheduleEmit, true);
|
|
121
|
+
onRectsChange([]);
|
|
122
|
+
};
|
|
123
|
+
}, [emitRects, hasLeft, hasRight, onRectsChange]);
|
|
124
|
+
|
|
41
125
|
const renderLeftPanel = (overlayId: string) => {
|
|
42
126
|
if (!left) return null;
|
|
43
127
|
|
|
44
128
|
return (
|
|
45
129
|
<div
|
|
130
|
+
ref={leftPanelRef}
|
|
46
131
|
className={panelClassName}
|
|
47
132
|
data-embedded-overlay-id={overlayId}
|
|
48
133
|
style={panelStyle}
|
|
@@ -84,6 +169,7 @@ export function OverlayToolbar({
|
|
|
84
169
|
)}
|
|
85
170
|
>
|
|
86
171
|
<div
|
|
172
|
+
ref={rightPanelRef}
|
|
87
173
|
className={panelClassName}
|
|
88
174
|
data-embedded-overlay-id="noya-toolbar-top-right"
|
|
89
175
|
style={panelStyle}
|
package/src/components/Toast.tsx
CHANGED
|
@@ -10,6 +10,7 @@ interface ToastData {
|
|
|
10
10
|
title?: string;
|
|
11
11
|
content: ReactNode;
|
|
12
12
|
action?: ReactNode;
|
|
13
|
+
timeout?: number;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
// Context for toast functionality
|
|
@@ -78,7 +79,7 @@ function ToastProviderInner({ children }: { children: React.ReactNode }) {
|
|
|
78
79
|
|
|
79
80
|
const showToast = useCallback(
|
|
80
81
|
(data: ToastData) => {
|
|
81
|
-
toastManager.add({ data });
|
|
82
|
+
toastManager.add({ data, timeout: data.timeout });
|
|
82
83
|
},
|
|
83
84
|
[toastManager]
|
|
84
85
|
);
|
|
@@ -24,7 +24,7 @@ export const Toolbar = forwardRef<HTMLDivElement, ToolbarProps>(
|
|
|
24
24
|
ref={ref}
|
|
25
25
|
role={role}
|
|
26
26
|
className={cx(
|
|
27
|
-
"n-pointer-events-auto n-flex n-items-center n-rounded n-bg-popover-background n-shadow-popover",
|
|
27
|
+
"n-pointer-events-auto n-flex n-max-w-full n-flex-wrap n-items-center n-justify-center n-rounded n-bg-popover-background n-shadow-popover",
|
|
28
28
|
size === "small" ? "n-gap-1 n-p-1" : "n-gap-toolbar-separator n-p-2",
|
|
29
29
|
className
|
|
30
30
|
)}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { cleanup, render } from "@testing-library/react";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { OverlayToolbar, OverlayToolbarRect } from "../OverlayToolbar";
|
|
5
|
+
|
|
6
|
+
afterEach(cleanup);
|
|
7
|
+
|
|
8
|
+
describe("OverlayToolbar", () => {
|
|
9
|
+
test("reports the bounds of its local panels", () => {
|
|
10
|
+
const originalGetBoundingClientRect =
|
|
11
|
+
HTMLElement.prototype.getBoundingClientRect;
|
|
12
|
+
const updates: OverlayToolbarRect[][] = [];
|
|
13
|
+
|
|
14
|
+
HTMLElement.prototype.getBoundingClientRect = function () {
|
|
15
|
+
const id = this.getAttribute("data-embedded-overlay-id");
|
|
16
|
+
const rect = this.hasAttribute("data-workspace-content")
|
|
17
|
+
? { x: 100, y: 200, width: 800, height: 600 }
|
|
18
|
+
: id === "noya-toolbar-top-left"
|
|
19
|
+
? { x: 116, y: 216, width: 120, height: 40 }
|
|
20
|
+
: id === "noya-toolbar-top-right"
|
|
21
|
+
? { x: 700, y: 224, width: 160, height: 48 }
|
|
22
|
+
: undefined;
|
|
23
|
+
|
|
24
|
+
if (!rect) return originalGetBoundingClientRect.call(this);
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
...rect,
|
|
28
|
+
top: rect.y,
|
|
29
|
+
right: rect.x + rect.width,
|
|
30
|
+
bottom: rect.y + rect.height,
|
|
31
|
+
left: rect.x,
|
|
32
|
+
toJSON: () => rect,
|
|
33
|
+
} as DOMRect;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
render(
|
|
38
|
+
<div data-workspace-content>
|
|
39
|
+
<OverlayToolbar
|
|
40
|
+
left={<div>Left</div>}
|
|
41
|
+
right={<div>Right</div>}
|
|
42
|
+
overlayLayout="topLeftTopRight"
|
|
43
|
+
onRectsChange={(rects) => updates.push(rects)}
|
|
44
|
+
/>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
expect(updates.at(-1)).toEqual([
|
|
49
|
+
{
|
|
50
|
+
id: "noya-toolbar-top-left",
|
|
51
|
+
rect: { x: 16, y: 16, width: 120, height: 40 },
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "noya-toolbar-top-right",
|
|
55
|
+
rect: { x: 600, y: 24, width: 160, height: 48 },
|
|
56
|
+
},
|
|
57
|
+
]);
|
|
58
|
+
} finally {
|
|
59
|
+
HTMLElement.prototype.getBoundingClientRect =
|
|
60
|
+
originalGetBoundingClientRect;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -18,6 +18,9 @@ describe("Toolbar", () => {
|
|
|
18
18
|
expect(toolbar.classList.contains("n-shadow-popover")).toBe(true);
|
|
19
19
|
expect(toolbar.classList.contains("n-gap-toolbar-separator")).toBe(true);
|
|
20
20
|
expect(toolbar.classList.contains("n-p-2")).toBe(true);
|
|
21
|
+
expect(toolbar.classList.contains("n-max-w-full")).toBe(true);
|
|
22
|
+
expect(toolbar.classList.contains("n-flex-wrap")).toBe(true);
|
|
23
|
+
expect(toolbar.classList.contains("n-justify-center")).toBe(true);
|
|
21
24
|
expect(
|
|
22
25
|
toolbar.style.getPropertyValue(cssVarNames.colors.buttonBackground)
|
|
23
26
|
).toBe("transparent");
|
|
@@ -16,7 +16,11 @@ import React, {
|
|
|
16
16
|
} from "react";
|
|
17
17
|
import { cx } from "../../utils/classNames";
|
|
18
18
|
import { MenuItem } from "../internal/Menu";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
OverlayLayout,
|
|
21
|
+
OverlayToolbar,
|
|
22
|
+
OverlayToolbarRect,
|
|
23
|
+
} from "../OverlayToolbar";
|
|
20
24
|
import { DrawerWorkspaceLayout } from "./DrawerWorkspaceLayout";
|
|
21
25
|
import { PanelWorkspaceLayout } from "./PanelWorkspaceLayout";
|
|
22
26
|
import { renderPanelChildren } from "./renderPanelChildren";
|
|
@@ -76,6 +80,10 @@ export interface WorkspaceLayoutProps<
|
|
|
76
80
|
* @default "bottomCenterTopRight"
|
|
77
81
|
*/
|
|
78
82
|
overlayLayout?: OverlayLayout;
|
|
83
|
+
/** Empty space to reserve above workspace content. @default 0 */
|
|
84
|
+
contentTopInset?: number;
|
|
85
|
+
/** Called when the local overlay toolbar panels move or resize. */
|
|
86
|
+
onOverlayToolbarRectsChange?: (rects: OverlayToolbarRect[]) => void;
|
|
79
87
|
sideType?: SideType;
|
|
80
88
|
sideTypeBreakpoint?: number;
|
|
81
89
|
detectSize?: DetectSizeType;
|
|
@@ -165,6 +173,8 @@ export const WorkspaceLayout = forwardRefGeneric(function WorkspaceLayout<
|
|
|
165
173
|
overlayToolbarLeft,
|
|
166
174
|
overlayToolbarRight,
|
|
167
175
|
overlayLayout,
|
|
176
|
+
contentTopInset = 0,
|
|
177
|
+
onOverlayToolbarRectsChange,
|
|
168
178
|
children,
|
|
169
179
|
sideType = "auto",
|
|
170
180
|
sideTypeBreakpoint = 800,
|
|
@@ -450,7 +460,13 @@ export const WorkspaceLayout = forwardRefGeneric(function WorkspaceLayout<
|
|
|
450
460
|
data-theme={theme}
|
|
451
461
|
>
|
|
452
462
|
{!isOverlay && toolbar}
|
|
453
|
-
<div
|
|
463
|
+
<div
|
|
464
|
+
className="n-flex n-flex-row n-flex-1 n-min-h-0 n-relative"
|
|
465
|
+
data-workspace-content
|
|
466
|
+
style={
|
|
467
|
+
contentTopInset > 0 ? { paddingTop: contentTopInset } : undefined
|
|
468
|
+
}
|
|
469
|
+
>
|
|
454
470
|
{readyToRender && (
|
|
455
471
|
<>
|
|
456
472
|
<LayoutComponent
|
|
@@ -478,6 +494,7 @@ export const WorkspaceLayout = forwardRefGeneric(function WorkspaceLayout<
|
|
|
478
494
|
left={overlayToolbarLeft}
|
|
479
495
|
right={overlayToolbarRight}
|
|
480
496
|
overlayLayout={overlayLayout}
|
|
497
|
+
onRectsChange={onOverlayToolbarRectsChange}
|
|
481
498
|
/>
|
|
482
499
|
)}
|
|
483
500
|
</>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { cleanup, render } from "@testing-library/react";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { WorkspaceLayout } from "../WorkspaceLayout";
|
|
5
|
+
|
|
6
|
+
afterEach(cleanup);
|
|
7
|
+
|
|
8
|
+
describe("WorkspaceLayout", () => {
|
|
9
|
+
test("applies the top inset to content without moving the toolbar", () => {
|
|
10
|
+
const { container, getByText } = render(
|
|
11
|
+
<WorkspaceLayout
|
|
12
|
+
contentTopInset={72}
|
|
13
|
+
detectSize="window"
|
|
14
|
+
toolbar={<div>Toolbar</div>}
|
|
15
|
+
>
|
|
16
|
+
<div>Content</div>
|
|
17
|
+
</WorkspaceLayout>
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const content = container.querySelector<HTMLElement>(
|
|
21
|
+
"[data-workspace-content]"
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
expect(content?.style.paddingTop).toBe("72px");
|
|
25
|
+
expect(content?.contains(getByText("Toolbar"))).toBe(false);
|
|
26
|
+
expect(content?.contains(getByText("Content"))).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("does not pad content when the inset is zero", () => {
|
|
30
|
+
const { container } = render(
|
|
31
|
+
<WorkspaceLayout contentTopInset={0} detectSize="window">
|
|
32
|
+
<div>Content</div>
|
|
33
|
+
</WorkspaceLayout>
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const content = container.querySelector<HTMLElement>(
|
|
37
|
+
"[data-workspace-content]"
|
|
38
|
+
);
|
|
39
|
+
expect(content?.style.paddingTop).toBe("");
|
|
40
|
+
});
|
|
41
|
+
});
|