@noya-app/noya-designsystem 0.1.89 → 0.1.91
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 +7 -7
- package/CHANGELOG.md +16 -0
- package/dist/index.css +1 -1
- package/dist/index.d.mts +18 -3
- package/dist/index.d.ts +18 -3
- package/dist/index.js +119 -41
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +151 -73
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/components/BaseToolbar.tsx +3 -1
- package/src/components/Dialog.tsx +4 -1
- package/src/components/OverlayToolbar.tsx +87 -1
- package/src/components/__tests__/BaseToolbar.test.tsx +22 -0
- package/src/components/__tests__/DialogNavigator.test.tsx +13 -0
- package/src/components/__tests__/OverlayToolbar.test.tsx +63 -0
- package/src/components/workspace/DrawerWorkspaceLayout.tsx +1 -1
- package/src/components/workspace/WorkspaceLayout.tsx +19 -2
- package/src/components/workspace/__tests__/DrawerWorkspaceLayout.test.tsx +6 -1
- package/src/components/workspace/__tests__/WorkspaceLayout.test.tsx +41 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noya-app/noya-designsystem",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.91",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"module": "./dist/index.mjs",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
"@dnd-kit/core": "6.3.1",
|
|
30
30
|
"@dnd-kit/sortable": "10.0.0",
|
|
31
31
|
"@floating-ui/react": "^0.27.0",
|
|
32
|
-
"@noya-app/noya-colorpicker": "0.1.
|
|
33
|
-
"@noya-app/noya-utils": "0.1.
|
|
34
|
-
"@noya-app/noya-geometry": "0.1.
|
|
32
|
+
"@noya-app/noya-colorpicker": "0.1.36",
|
|
33
|
+
"@noya-app/noya-utils": "0.1.10",
|
|
34
|
+
"@noya-app/noya-geometry": "0.1.19",
|
|
35
35
|
"@noya-app/noya-icons": "0.1.20",
|
|
36
36
|
"@noya-app/noya-keymap": "0.1.4",
|
|
37
37
|
"@noya-app/noya-tailwind-config": "0.1.10",
|
|
@@ -9,6 +9,7 @@ export interface BaseToolbarProps {
|
|
|
9
9
|
left?: React.ReactNode;
|
|
10
10
|
right?: React.ReactNode;
|
|
11
11
|
logo?: React.ReactNode;
|
|
12
|
+
showLogoDivider?: boolean;
|
|
12
13
|
showDivider?: boolean;
|
|
13
14
|
className?: string;
|
|
14
15
|
innerClassName?: string;
|
|
@@ -63,6 +64,7 @@ export function BaseToolbar({
|
|
|
63
64
|
left,
|
|
64
65
|
right,
|
|
65
66
|
logo,
|
|
67
|
+
showLogoDivider = true,
|
|
66
68
|
showDivider = true,
|
|
67
69
|
className,
|
|
68
70
|
innerClassName,
|
|
@@ -97,7 +99,7 @@ export function BaseToolbar({
|
|
|
97
99
|
{logo && (
|
|
98
100
|
<>
|
|
99
101
|
{logo}
|
|
100
|
-
<DividerVertical />
|
|
102
|
+
{showLogoDivider && <DividerVertical />}
|
|
101
103
|
</>
|
|
102
104
|
)}
|
|
103
105
|
<Spacer.Horizontal size={10} />
|
|
@@ -19,7 +19,10 @@ const StyledBackdrop = forwardRef<
|
|
|
19
19
|
>(({ className, ...props }, ref) => (
|
|
20
20
|
<DialogPrimitive.Backdrop
|
|
21
21
|
ref={ref}
|
|
22
|
-
className={cx(
|
|
22
|
+
className={cx(
|
|
23
|
+
`n-fixed n-inset-0 n-z-[999] n-bg-[rgba(0,0,0,0.5)]`,
|
|
24
|
+
className
|
|
25
|
+
)}
|
|
23
26
|
{...props}
|
|
24
27
|
/>
|
|
25
28
|
));
|
|
@@ -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}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { cleanup, render } from "@testing-library/react";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { BaseToolbar } from "../BaseToolbar";
|
|
5
|
+
|
|
6
|
+
afterEach(cleanup);
|
|
7
|
+
|
|
8
|
+
describe("BaseToolbar", () => {
|
|
9
|
+
test("shows a vertical divider after the logo by default", () => {
|
|
10
|
+
const { container } = render(<BaseToolbar logo={<div>Logo</div>} />);
|
|
11
|
+
|
|
12
|
+
expect(container.querySelector(".n-w-px")).toBeTruthy();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("can hide the vertical divider after the logo", () => {
|
|
16
|
+
const { container } = render(
|
|
17
|
+
<BaseToolbar logo={<div>Back</div>} showLogoDivider={false} />
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
expect(container.querySelector(".n-w-px")).toBeNull();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -64,6 +64,19 @@ describe("DialogNavigator", () => {
|
|
|
64
64
|
expect(view.getByRole("button", { name: "Close" })).toBeTruthy();
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
+
test("stacks the backdrop above elevated page controls", () => {
|
|
68
|
+
render(<TestDialogNavigator onOpenChange={() => {}} />);
|
|
69
|
+
|
|
70
|
+
const backdrop = Array.from(document.querySelectorAll("div")).find(
|
|
71
|
+
(element) =>
|
|
72
|
+
element.classList.contains("n-bg-[rgba(0,0,0,0.5)]") &&
|
|
73
|
+
element.classList.contains("n-inset-0")
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
expect(backdrop).toBeTruthy();
|
|
77
|
+
expect(backdrop?.classList.contains("n-z-[999]")).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
67
80
|
test("combines navigation and dialog controls in the header", async () => {
|
|
68
81
|
const onOpenChange = mock(() => {});
|
|
69
82
|
const view = render(<TestDialogNavigator onOpenChange={onOpenChange} />);
|
|
@@ -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
|
+
});
|
|
@@ -182,7 +182,7 @@ export const DrawerWorkspaceLayout = memoGeneric(function DrawerWorkspaceLayout<
|
|
|
182
182
|
)}
|
|
183
183
|
|
|
184
184
|
{compactTabItem && (
|
|
185
|
-
<div className="n-absolute n-top-0 n-left-0 n-h-[calc(var(--n-toolbar-height)+1px)] n-w-[calc(var(--n-toolbar-height)+1px)] n-bg-sidebar-background n-border-
|
|
185
|
+
<div className="n-absolute n-top-0 n-left-0 n-h-[calc(var(--n-toolbar-height)+1px)] n-w-[calc(var(--n-toolbar-height)+1px)] n-bg-sidebar-background n-border-b n-border-divider-strong n-z-10 n-flex n-items-center n-justify-center">
|
|
186
186
|
<Button
|
|
187
187
|
aria-label={
|
|
188
188
|
typeof compactTabItem.title === "string"
|
|
@@ -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
|
</>
|
|
@@ -27,7 +27,12 @@ describe("DrawerWorkspaceLayout", () => {
|
|
|
27
27
|
|
|
28
28
|
expect(container.textContent).toBe("Center content");
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
const navigationButton = getByRole("button", { name: "Navigation" });
|
|
31
|
+
const compactTrigger = navigationButton.parentElement;
|
|
32
|
+
expect(compactTrigger?.classList.contains("n-border-b")).toBe(true);
|
|
33
|
+
expect(compactTrigger?.classList.contains("n-border-r")).toBe(false);
|
|
34
|
+
|
|
35
|
+
fireEvent.click(navigationButton);
|
|
31
36
|
|
|
32
37
|
expect(getByText("Sidebar content")).toBeTruthy();
|
|
33
38
|
});
|
|
@@ -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
|
+
});
|