@linzjs/windows 1.0.0 → 1.1.1
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/.storybook/main.ts +26 -6
- package/README.md +32 -6
- package/package.json +34 -12
- package/src/modal/Modal.tsx +9 -5
- package/src/modal/ModalContextProvider.tsx +35 -36
- package/src/modal/PreModal.tsx +45 -0
- package/src/panel/OpenPanelButton.tsx +18 -0
- package/src/panel/OpenPanelIcon.scss +73 -0
- package/src/panel/OpenPanelIcon.tsx +50 -0
- package/src/panel/Panel.scss +34 -0
- package/src/panel/Panel.tsx +150 -0
- package/src/panel/PanelContext.ts +17 -0
- package/src/panel/PanelInstanceContext.ts +41 -0
- package/src/panel/PanelInstanceContextProvider.tsx +47 -0
- package/src/panel/PanelsContext.tsx +36 -0
- package/src/panel/PanelsContextProvider.tsx +140 -0
- package/src/panel/PopoutWindow.tsx +183 -0
- package/src/panel/generateId.ts +23 -0
- package/src/panel/handleStyleSheetsChanges.ts +71 -0
- package/src/stories/Introduction.mdx +18 -0
- package/src/stories/Introduction.stories.tsx +8 -0
- package/src/stories/modal/Modal.mdx +9 -3
- package/src/stories/modal/Modal.stories.tsx +1 -1
- package/src/stories/modal/PreModal.mdx +26 -0
- package/src/stories/modal/PreModal.stories.tsx +27 -0
- package/src/stories/modal/PreModal.tsx +79 -0
- package/src/stories/modal/TestModal.scss +21 -0
- package/src/stories/panel/PanelButtons/ShowPanel.mdx +21 -0
- package/src/stories/panel/PanelButtons/ShowPanel.stories.tsx +27 -0
- package/src/stories/panel/PanelButtons/ShowPanel.tsx +86 -0
- package/src/stories/panel/ShowPanel/ShowPanel.mdx +20 -0
- package/src/stories/panel/ShowPanel/ShowPanel.stories.tsx +27 -0
- package/src/stories/panel/ShowPanel/ShowPanel.tsx +70 -0
- package/src/stories/panel/ShowPanelResizingAgGrid/ShowPanelResizingAgGrid.mdx +21 -0
- package/src/stories/panel/ShowPanelResizingAgGrid/ShowPanelResizingAgGrid.stories.tsx +27 -0
- package/src/stories/panel/ShowPanelResizingAgGrid/ShowPanelResizingStepAgGrid.tsx +164 -0
- package/src/stories/support.js +16 -0
- package/src/util/useInterval.ts +11 -19
package/.storybook/main.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
import { NodeGlobalsPolyfillPlugin } from "@esbuild-plugins/node-globals-polyfill";
|
|
2
|
+
import { NodeModulesPolyfillPlugin } from "@esbuild-plugins/node-modules-polyfill";
|
|
1
3
|
import type { StorybookConfig } from "@storybook/react-vite";
|
|
4
|
+
import { mergeConfig } from "vite";
|
|
5
|
+
|
|
2
6
|
const config: StorybookConfig = {
|
|
3
|
-
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
|
|
4
|
-
addons: [
|
|
5
|
-
"@storybook/addon-links",
|
|
6
|
-
"@storybook/addon-essentials",
|
|
7
|
-
"@storybook/addon-interactions",
|
|
8
|
-
],
|
|
7
|
+
stories: ["../src/**/Introduction.mdx", "../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
|
|
8
|
+
addons: ["@storybook/addon-links", "@storybook/addon-essentials", "@storybook/addon-interactions"],
|
|
9
9
|
framework: {
|
|
10
10
|
name: "@storybook/react-vite",
|
|
11
11
|
options: {},
|
|
@@ -13,5 +13,25 @@ const config: StorybookConfig = {
|
|
|
13
13
|
docs: {
|
|
14
14
|
autodocs: "tag",
|
|
15
15
|
},
|
|
16
|
+
viteFinal: async (config) => {
|
|
17
|
+
return mergeConfig(config, {
|
|
18
|
+
optimizeDeps: {
|
|
19
|
+
esbuildOptions: {
|
|
20
|
+
// Node.js global to browser globalThis
|
|
21
|
+
define: {
|
|
22
|
+
global: "globalThis",
|
|
23
|
+
},
|
|
24
|
+
// Enable esbuild polyfill plugins
|
|
25
|
+
plugins: [
|
|
26
|
+
NodeGlobalsPolyfillPlugin({
|
|
27
|
+
buffer: true,
|
|
28
|
+
process: true,
|
|
29
|
+
}),
|
|
30
|
+
NodeModulesPolyfillPlugin(),
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
},
|
|
16
36
|
};
|
|
17
37
|
export default config;
|
package/README.md
CHANGED
|
@@ -1,10 +1,36 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @linzjs/windows
|
|
2
2
|
|
|
3
|
-
[](https://github.com/semantic-release/semantic-release)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
> Reusable promise based windowing component for LINZ / Toitū te whenua.
|
|
7
|
+
|
|
8
|
+
Rect state based modals/windows are painful because they require:
|
|
9
|
+
- shared states for open/closed.
|
|
10
|
+
- callbacks/states for return values.
|
|
11
|
+
- inline modal/window includes, which prevent you from closing the invoking component before the modal/window has
|
|
12
|
+
completed.
|
|
13
|
+
|
|
14
|
+
This module gives you promise based modals/windows which don't require all the state
|
|
15
|
+
based boiler-plate / inline-components.
|
|
4
16
|
|
|
5
|
-
> Reusable windowing component for LINZ / Toitū te whenua.
|
|
6
|
-
>
|
|
7
17
|
## Features
|
|
8
|
-
|
|
18
|
+
- Async HTML dialog based Modals.
|
|
19
|
+
- Draggable and resizeable, pop-in/out Windows.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
```
|
|
23
|
+
npm install @linzjs/windows
|
|
24
|
+
```
|
|
25
|
+
or with Yarn
|
|
26
|
+
```
|
|
27
|
+
yarn add @linzjs/windows
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Demo
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm run storybook
|
|
34
|
+
```
|
|
9
35
|
|
|
10
|
-
See [Chromatic](https://64a2356b80885af35510b627
|
|
36
|
+
See [Chromatic storybook](https://master--64a2356b80885af35510b627.chromatic.com/) for documentation.
|
package/package.json
CHANGED
|
@@ -2,8 +2,19 @@
|
|
|
2
2
|
"name": "@linzjs/windows",
|
|
3
3
|
"repository": "github:linz/windows.git",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"
|
|
5
|
+
"keywords": [
|
|
6
|
+
"react",
|
|
7
|
+
"ts",
|
|
8
|
+
"typescript",
|
|
9
|
+
"modal",
|
|
10
|
+
"react-component",
|
|
11
|
+
"window",
|
|
12
|
+
"panel",
|
|
13
|
+
"popout"
|
|
14
|
+
],
|
|
15
|
+
"version": "1.1.1",
|
|
6
16
|
"peerDependencies": {
|
|
17
|
+
"@linzjs/lui": "^17",
|
|
7
18
|
"lodash-es": ">=4",
|
|
8
19
|
"react": ">=17",
|
|
9
20
|
"react-dom": ">=17"
|
|
@@ -16,25 +27,33 @@
|
|
|
16
27
|
"node": ">=16"
|
|
17
28
|
},
|
|
18
29
|
"dependencies": {
|
|
30
|
+
"@emotion/cache": "^11.11.0",
|
|
31
|
+
"@emotion/react": "^11.11.1",
|
|
32
|
+
"@emotion/styled": "^11.11.0",
|
|
33
|
+
"@linzjs/lui": "^17",
|
|
19
34
|
"lodash-es": ">=4",
|
|
20
35
|
"react": ">=17",
|
|
21
36
|
"react-dom": ">=17",
|
|
37
|
+
"react-rnd": "^10.4.1",
|
|
22
38
|
"uuid": "^9.0.0"
|
|
23
39
|
},
|
|
24
40
|
"devDependencies": {
|
|
41
|
+
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
|
|
42
|
+
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
|
|
43
|
+
"@linzjs/step-ag-grid": "^14.9.3",
|
|
25
44
|
"@rollup/plugin-commonjs": "^25.0.2",
|
|
26
45
|
"@rollup/plugin-json": "^6.0.0",
|
|
27
46
|
"@rollup/plugin-node-resolve": "^15.1.0",
|
|
28
|
-
"@storybook/addon-docs": "^7.0.
|
|
29
|
-
"@storybook/addon-essentials": "^7.0.
|
|
30
|
-
"@storybook/addon-interactions": "^7.0.
|
|
31
|
-
"@storybook/addon-links": "^7.0.
|
|
32
|
-
"@storybook/blocks": "^7.0.
|
|
33
|
-
"@storybook/builder-webpack5": "^7.0.
|
|
47
|
+
"@storybook/addon-docs": "^7.0.26",
|
|
48
|
+
"@storybook/addon-essentials": "^7.0.26",
|
|
49
|
+
"@storybook/addon-interactions": "^7.0.26",
|
|
50
|
+
"@storybook/addon-links": "^7.0.26",
|
|
51
|
+
"@storybook/blocks": "^7.0.26",
|
|
52
|
+
"@storybook/builder-webpack5": "^7.0.26",
|
|
34
53
|
"@storybook/jest": "^0.1.0",
|
|
35
|
-
"@storybook/preset-create-react-app": "^7.0.
|
|
36
|
-
"@storybook/react": "^7.0.
|
|
37
|
-
"@storybook/react-vite": "^7.0.
|
|
54
|
+
"@storybook/preset-create-react-app": "^7.0.26",
|
|
55
|
+
"@storybook/react": "^7.0.26",
|
|
56
|
+
"@storybook/react-vite": "^7.0.26",
|
|
38
57
|
"@storybook/test-runner": "^0.11.0",
|
|
39
58
|
"@storybook/testing-library": "^0.2.0",
|
|
40
59
|
"@testing-library/jest-dom": "^5.16.5",
|
|
@@ -47,6 +66,8 @@
|
|
|
47
66
|
"@types/react": "^18.2.14",
|
|
48
67
|
"@types/react-dom": "^18.2.6",
|
|
49
68
|
"@types/uuid": "^9.0.2",
|
|
69
|
+
"ag-grid-community": "^27.3.0",
|
|
70
|
+
"ag-grid-react": "^27.3.0",
|
|
50
71
|
"eslint": "^8.44.0",
|
|
51
72
|
"eslint-config-prettier": "^8.8.0",
|
|
52
73
|
"eslint-config-react-app": "^7.0.1",
|
|
@@ -73,7 +94,7 @@
|
|
|
73
94
|
"sass": "^1.63.6",
|
|
74
95
|
"sass-loader": "^13.3.2",
|
|
75
96
|
"semantic-release": "^19.0.5",
|
|
76
|
-
"storybook": "^7.0.
|
|
97
|
+
"storybook": "^7.0.26",
|
|
77
98
|
"style-loader": "^3.3.3",
|
|
78
99
|
"stylelint": "^14.16.1",
|
|
79
100
|
"stylelint-config-prettier": "^9.0.5",
|
|
@@ -85,13 +106,14 @@
|
|
|
85
106
|
"vite": "^4.3.9"
|
|
86
107
|
},
|
|
87
108
|
"scripts": {
|
|
88
|
-
"build": "run-s clean stylelint lint bundle",
|
|
109
|
+
"build": "run-s clean stylelint lint lint-circular-deps bundle",
|
|
89
110
|
"yalc": "run-s clean css bundle && yalc publish",
|
|
90
111
|
"clean": "rimraf dist && mkdirp ./dist",
|
|
91
112
|
"bundle": "rollup -c",
|
|
92
113
|
"test": "jest",
|
|
93
114
|
"stylelint": "stylelint src/**/*.scss src/**/*.css --fix",
|
|
94
115
|
"lint": "eslint ./src --ext .js,.ts,.tsx --fix --cache --ignore-path .gitignore",
|
|
116
|
+
"lint-circular-deps": "npx madge --circular --extensions ts,tsx ./",
|
|
95
117
|
"storybook": "storybook dev -p 6006",
|
|
96
118
|
"build-storybook": "storybook build",
|
|
97
119
|
"deploy-storybook": "npx --yes -p @storybook/storybook-deployer storybook-to-ghpages",
|
package/src/modal/Modal.tsx
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { ModalInstanceContext } from "./ModalInstanceContext";
|
|
2
|
-
import {
|
|
2
|
+
import { delay } from "lodash-es";
|
|
3
3
|
import { ReactElement, useContext, useEffect, useRef } from "react";
|
|
4
4
|
|
|
5
5
|
export interface ModalProps {
|
|
6
|
-
selectFirstInput?: boolean;
|
|
7
6
|
children: ReactElement | ReactElement[];
|
|
8
7
|
}
|
|
9
8
|
|
|
10
|
-
export const Modal = ({
|
|
9
|
+
export const Modal = ({ children }: ModalProps): ReactElement => {
|
|
11
10
|
const dialogRef = useRef<HTMLDialogElement>(null);
|
|
12
11
|
|
|
13
12
|
const { close } = useContext(ModalInstanceContext);
|
|
@@ -21,8 +20,13 @@ export const Modal = ({ selectFirstInput = true, children }: ModalProps): ReactE
|
|
|
21
20
|
}, []);
|
|
22
21
|
|
|
23
22
|
useEffect(() => {
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
// Dialogs auto select the first focusable element, this is in case you don't want that
|
|
24
|
+
delay(() => {
|
|
25
|
+
const input = dialogRef.current?.querySelectorAll("[data-autofocus]") as any;
|
|
26
|
+
input[0]?.focus?.();
|
|
27
|
+
input?.select?.();
|
|
28
|
+
}, 100);
|
|
29
|
+
}, []);
|
|
26
30
|
|
|
27
31
|
return (
|
|
28
32
|
<dialog ref={dialogRef} onClick={(e) => e.target === e.currentTarget && close()} style={{ padding: 0 }}>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useInterval } from "../util/useInterval";
|
|
2
2
|
import { ComponentType, ModalContext } from "./ModalContext";
|
|
3
3
|
import { ModalInstanceContext } from "./ModalInstanceContext";
|
|
4
|
-
import { Fragment, MutableRefObject, ReactElement, useState } from "react";
|
|
4
|
+
import { Fragment, MutableRefObject, ReactElement, useCallback, useState } from "react";
|
|
5
5
|
import * as ReactDOM from "react-dom";
|
|
6
6
|
import { v4 as uuid } from "uuid";
|
|
7
7
|
|
|
@@ -67,49 +67,48 @@ export const ModalContextProvider = ({ children }: { children: ReactElement }):
|
|
|
67
67
|
* @param Component React component.
|
|
68
68
|
* @param args Arguments for react component.
|
|
69
69
|
*/
|
|
70
|
-
const showModal =
|
|
71
|
-
ownerRef: MutableRefObject<HTMLElement | null>,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
});
|
|
70
|
+
const showModal = useCallback(
|
|
71
|
+
async (ownerRef: MutableRefObject<HTMLElement | null>, Component: ComponentType, args: any): Promise<any> => {
|
|
72
|
+
let componentInstance: ReactElement | undefined;
|
|
73
|
+
const promise = new Promise((resolve) => {
|
|
74
|
+
try {
|
|
75
|
+
// If there are any exceptions the modal won't show
|
|
76
|
+
setModals([
|
|
77
|
+
...modals,
|
|
78
|
+
{
|
|
79
|
+
uuid: uuid(),
|
|
80
|
+
ownerElement: ownerRef.current ?? document.body,
|
|
81
|
+
componentInstance: <Component {...args} resolve={resolve} close={() => resolve(undefined)} />,
|
|
82
|
+
resolve,
|
|
83
|
+
},
|
|
84
|
+
]);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
console.error(e);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
93
90
|
|
|
94
|
-
|
|
95
|
-
|
|
91
|
+
// Wait for modal to complete
|
|
92
|
+
const result = await promise;
|
|
96
93
|
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
// Close modal
|
|
95
|
+
setModals(modals.filter((e) => e.componentInstance !== componentInstance));
|
|
99
96
|
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
return result;
|
|
98
|
+
},
|
|
99
|
+
[modals],
|
|
100
|
+
);
|
|
102
101
|
|
|
103
|
-
const modalHasView = (
|
|
104
|
-
!!modalInstance.ownerElement?.ownerDocument?.defaultView
|
|
102
|
+
const modalHasView = useCallback(
|
|
103
|
+
(modalInstance: ModalInstance): boolean => !!modalInstance.ownerElement?.ownerDocument?.defaultView,
|
|
104
|
+
[],
|
|
105
|
+
);
|
|
105
106
|
|
|
106
107
|
// Tidy up modals that have closed because of an external window closing
|
|
107
108
|
useInterval(() => {
|
|
108
109
|
const newModals = modals.filter(modalHasView);
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
}, 1000);
|
|
110
|
+
newModals.length !== modals.length && setModals(newModals);
|
|
111
|
+
}, 500);
|
|
113
112
|
|
|
114
113
|
return (
|
|
115
114
|
<ModalContext.Provider
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Modal } from "./Modal";
|
|
2
|
+
import { ModalCallback } from "./ModalContext";
|
|
3
|
+
import { ReactElement } from "react";
|
|
4
|
+
|
|
5
|
+
import { LuiAlertModalButtons, LuiButton, LuiIcon } from "@linzjs/lui";
|
|
6
|
+
import { IconName } from "@linzjs/lui/dist/components/LuiIcon/LuiIcon";
|
|
7
|
+
|
|
8
|
+
export type WarningLevel = "success" | "info" | "warning" | "error";
|
|
9
|
+
|
|
10
|
+
export interface PreModalProps extends ModalCallback<boolean> {
|
|
11
|
+
level?: WarningLevel;
|
|
12
|
+
children: ReactElement;
|
|
13
|
+
}
|
|
14
|
+
export const getIconForLevel = (level: "success" | "info" | "warning" | "error"): IconName => {
|
|
15
|
+
switch (level) {
|
|
16
|
+
case "success":
|
|
17
|
+
return "ic_check_circle";
|
|
18
|
+
case "info":
|
|
19
|
+
return "ic_info";
|
|
20
|
+
case "warning":
|
|
21
|
+
return "ic_warning";
|
|
22
|
+
case "error":
|
|
23
|
+
return "ic_error";
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const PreModal = ({ level = "warning", children, resolve }: PreModalProps) => {
|
|
28
|
+
const icon = getIconForLevel(level);
|
|
29
|
+
return (
|
|
30
|
+
<Modal>
|
|
31
|
+
<div className={`lui-modal lui-box-shadow lui-modal-${level}`} style={{ minWidth: 400 }}>
|
|
32
|
+
<LuiIcon name={icon} alt={"warning"} size={"lg"} className={"lui-msg-status-icon"} />
|
|
33
|
+
{children}
|
|
34
|
+
<LuiAlertModalButtons>
|
|
35
|
+
{level === "warning" && (
|
|
36
|
+
<LuiButton level="secondary" onClick={() => resolve(false)} buttonProps={{ "data-autofocus": true }}>
|
|
37
|
+
Cancel
|
|
38
|
+
</LuiButton>
|
|
39
|
+
)}
|
|
40
|
+
<LuiButton onClick={() => resolve(true)}>Continue</LuiButton>
|
|
41
|
+
</LuiAlertModalButtons>
|
|
42
|
+
</div>
|
|
43
|
+
</Modal>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Simple button to open panel and show panel state
|
|
2
|
+
import { PanelsContext } from "./PanelsContext";
|
|
3
|
+
import { ReactElement, useContext } from "react";
|
|
4
|
+
|
|
5
|
+
interface OpenPanelButtonProps {
|
|
6
|
+
buttonText: string;
|
|
7
|
+
component: () => ReactElement;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const OpenPanelButton = ({ buttonText, component }: OpenPanelButtonProps) => {
|
|
11
|
+
const { openPanel, openPanels } = useContext(PanelsContext);
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<button onClick={() => openPanel(buttonText, component)}>
|
|
15
|
+
Show {buttonText} {openPanels.has(buttonText) ? "(Open)" : ""}
|
|
16
|
+
</button>
|
|
17
|
+
);
|
|
18
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
@use "node_modules/@linzjs/lui/dist/scss/Core" as lui;
|
|
2
|
+
@use "node_modules/@linzjs/lui/dist/scss/Foundation/Variables/ColorVars.scss" as colours;
|
|
3
|
+
|
|
4
|
+
.lui-button.lui-button-toolbar {
|
|
5
|
+
border-color: transparent;
|
|
6
|
+
padding: 4px;
|
|
7
|
+
line-height: 12px;
|
|
8
|
+
margin: 2px;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.OpenPanelIcon-selected {
|
|
12
|
+
cursor: pointer;
|
|
13
|
+
color: lui.$white !important;
|
|
14
|
+
background-color: lui.$blue-75 !important;
|
|
15
|
+
box-shadow: inset 0 2px 4px rgb(41 92 130);
|
|
16
|
+
|
|
17
|
+
fill: lui.$white !important;
|
|
18
|
+
|
|
19
|
+
svg * {
|
|
20
|
+
fill: lui.$white !important;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
svg * {
|
|
24
|
+
color: lui.$white !important;
|
|
25
|
+
fill: lui.$white !important;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.OpenPanelIcon-disabled {
|
|
30
|
+
background-color: lui.$white !important;
|
|
31
|
+
|
|
32
|
+
fill: lui.$grey-20 !important;
|
|
33
|
+
|
|
34
|
+
svg * {
|
|
35
|
+
fill: lui.$grey-20 !important;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
%OpenPanelIcon-Group {
|
|
40
|
+
background-color: white;
|
|
41
|
+
border-radius: 4px;
|
|
42
|
+
padding: 4px;
|
|
43
|
+
align-items: center;
|
|
44
|
+
box-shadow: 0 0 10px rgb(0 0 0 / 20%);
|
|
45
|
+
display: inline-flex;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.OpenPanelIcon-verticalGroup {
|
|
49
|
+
@extend %OpenPanelIcon-Group;
|
|
50
|
+
flex-direction: column;
|
|
51
|
+
|
|
52
|
+
.OpenPanelIcon-separator {
|
|
53
|
+
margin: 6px 0;
|
|
54
|
+
height: 2px;
|
|
55
|
+
width: 100%;
|
|
56
|
+
background-color: colours.$grey-10;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.OpenPanelIcon-horizontalGroup {
|
|
61
|
+
@extend %OpenPanelIcon-Group;
|
|
62
|
+
flex-direction: row;
|
|
63
|
+
|
|
64
|
+
.OpenPanelIcon-separator {
|
|
65
|
+
height: 100%;
|
|
66
|
+
margin-left: 6px;
|
|
67
|
+
margin-right: 6px;
|
|
68
|
+
margin-top: -3px;
|
|
69
|
+
padding-bottom: 8px;
|
|
70
|
+
width: 2px;
|
|
71
|
+
background-color: colours.$grey-10;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import "./OpenPanelIcon.scss";
|
|
2
|
+
|
|
3
|
+
import { PanelsContext } from "./PanelsContext";
|
|
4
|
+
import clsx from "clsx";
|
|
5
|
+
import { ReactElement, ReactNode, useContext, useRef } from "react";
|
|
6
|
+
import { v4 as uuid } from "uuid";
|
|
7
|
+
|
|
8
|
+
import { LuiIcon } from "@linzjs/lui";
|
|
9
|
+
import { IconName } from "@linzjs/lui/dist/components/LuiIcon/LuiIcon";
|
|
10
|
+
|
|
11
|
+
export const ButtonIconHorizontalGroup = ({ children }: { children: ReactNode }) => (
|
|
12
|
+
<div className={"OpenPanelIcon-horizontalGroup"}>{children}</div>
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
export const ButtonIconVerticalGroup = ({ children }: { children: ReactNode }) => (
|
|
16
|
+
<div className={"OpenPanelIcon-verticalGroup"}>{children}</div>
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export const ButtonIconSeparator = () => <div className="OpenPanelIcon-separator"> </div>;
|
|
20
|
+
|
|
21
|
+
interface OpenPanelIconProps {
|
|
22
|
+
uniqueId?: string;
|
|
23
|
+
iconTitle: string;
|
|
24
|
+
icon: IconName;
|
|
25
|
+
component: () => ReactElement;
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const OpenPanelIcon = ({ iconTitle, uniqueId, icon, component, className, disabled }: OpenPanelIconProps) => {
|
|
31
|
+
const { openPanel, openPanels } = useContext(PanelsContext);
|
|
32
|
+
const id = useRef(uniqueId ?? uuid());
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<button
|
|
36
|
+
type="button"
|
|
37
|
+
className={clsx(
|
|
38
|
+
className,
|
|
39
|
+
"lui-button lui-button-secondary lui-button-toolbar panel-button",
|
|
40
|
+
openPanels.has(iconTitle) && "OpenPanelIcon-selected",
|
|
41
|
+
disabled && "OpenPanelIcon-disabled",
|
|
42
|
+
)}
|
|
43
|
+
title={iconTitle}
|
|
44
|
+
onClick={() => openPanel(id.current, component)}
|
|
45
|
+
disabled={disabled}
|
|
46
|
+
>
|
|
47
|
+
<LuiIcon name={icon} alt={iconTitle} size={"md"} />
|
|
48
|
+
</button>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
.WindowPanel {
|
|
2
|
+
box-shadow: 0 1px 6px 0 #00000026, 0 6px 10px 0 #00000040;
|
|
3
|
+
background-color: #fff;
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
border-radius: 9px;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.WindowPanel-header {
|
|
10
|
+
height: 48px;
|
|
11
|
+
line-height: 48px;
|
|
12
|
+
color: #2a292c;
|
|
13
|
+
padding: 0 8px;
|
|
14
|
+
display: flex;
|
|
15
|
+
overflow: hidden;
|
|
16
|
+
justify-content: space-between;
|
|
17
|
+
border-bottom: 2px #eaeaea solid;
|
|
18
|
+
font-size: 1em;
|
|
19
|
+
font-weight: 600;
|
|
20
|
+
flex-direction: row;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.WindowPanel-header-title {
|
|
24
|
+
white-space: nowrap;
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
text-overflow: ellipsis;
|
|
27
|
+
flex: 1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.WindowPanel-content {
|
|
31
|
+
flex: 1;
|
|
32
|
+
overflow: auto;
|
|
33
|
+
display: flex;
|
|
34
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import "./Panel.scss";
|
|
2
|
+
import "@linzjs/lui/dist/scss/base.scss";
|
|
3
|
+
|
|
4
|
+
import { PanelContext } from "./PanelContext";
|
|
5
|
+
import { PanelInstanceContext, PanelSize } from "./PanelInstanceContext";
|
|
6
|
+
import { PanelPosition, PanelsContext } from "./PanelsContext";
|
|
7
|
+
import { PopoutWindow } from "./PopoutWindow";
|
|
8
|
+
import { ReactElement, ReactNode, useContext, useEffect, useState } from "react";
|
|
9
|
+
import { Rnd } from "react-rnd";
|
|
10
|
+
|
|
11
|
+
import { LuiButton, LuiIcon } from "@linzjs/lui";
|
|
12
|
+
import { LuiIconName } from "@linzjs/lui/dist/assets/svg-content";
|
|
13
|
+
|
|
14
|
+
export interface PanelProps {
|
|
15
|
+
title: string;
|
|
16
|
+
position?: PanelPosition;
|
|
17
|
+
size?: PanelSize;
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const Panel = ({ title, position, size = { width: 320, height: 200 }, children }: PanelProps): ReactElement => {
|
|
22
|
+
const { nextStackPosition } = useContext(PanelsContext);
|
|
23
|
+
const { panelPoppedOut, bounds, zIndex, bringPanelToFront, uniqueId, setTitle } = useContext(PanelInstanceContext);
|
|
24
|
+
|
|
25
|
+
const [panelPosition, setPanelPosition] = useState(() => position ?? nextStackPosition());
|
|
26
|
+
const [panelSize, setPanelSize] = useState(size ?? { width: 320, height: 200 });
|
|
27
|
+
|
|
28
|
+
const resizePanel = (newPanelSize: Partial<PanelSize>) => {
|
|
29
|
+
if (panelPoppedOut) return;
|
|
30
|
+
const newSize = { ...panelSize, ...newPanelSize };
|
|
31
|
+
if (newSize.width !== panelSize.width || newSize.height !== panelSize.height) {
|
|
32
|
+
setPanelSize(newSize);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
setTitle(title);
|
|
38
|
+
}, [setTitle, title]);
|
|
39
|
+
|
|
40
|
+
//panelClose
|
|
41
|
+
return (
|
|
42
|
+
<PanelContext.Provider value={{ resizePanel }}>
|
|
43
|
+
{panelPoppedOut ? (
|
|
44
|
+
<PopoutWindow name={uniqueId} title={title} size={panelSize}>
|
|
45
|
+
<div
|
|
46
|
+
style={{
|
|
47
|
+
display: "flex",
|
|
48
|
+
flexDirection: "column",
|
|
49
|
+
width: "100%",
|
|
50
|
+
height: "100%",
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
{children}
|
|
54
|
+
</div>
|
|
55
|
+
</PopoutWindow>
|
|
56
|
+
) : (
|
|
57
|
+
<Rnd
|
|
58
|
+
className={"WindowPanel"}
|
|
59
|
+
dragHandleClassName={"draggable-handle"}
|
|
60
|
+
minWidth={100}
|
|
61
|
+
minHeight={100}
|
|
62
|
+
position={panelPosition}
|
|
63
|
+
size={panelSize}
|
|
64
|
+
style={{ zIndex }}
|
|
65
|
+
bounds={bounds ?? document.body}
|
|
66
|
+
onDragStop={(_evt, data) => {
|
|
67
|
+
setPanelPosition({ x: data.x, y: data.y });
|
|
68
|
+
}}
|
|
69
|
+
onResizeStop={(_evt, _dir, ref, _delta, position) => {
|
|
70
|
+
setPanelPosition(position);
|
|
71
|
+
setPanelSize({
|
|
72
|
+
width: parseInt(ref.style.width),
|
|
73
|
+
height: parseInt(ref.style.height),
|
|
74
|
+
});
|
|
75
|
+
}}
|
|
76
|
+
onMouseDown={bringPanelToFront}
|
|
77
|
+
>
|
|
78
|
+
<div
|
|
79
|
+
style={{
|
|
80
|
+
display: "flex",
|
|
81
|
+
flexDirection: "column",
|
|
82
|
+
width: "100%",
|
|
83
|
+
height: "100%",
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
{children}
|
|
87
|
+
</div>
|
|
88
|
+
</Rnd>
|
|
89
|
+
)}
|
|
90
|
+
</PanelContext.Provider>
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export interface PanelHeaderButtonProps {
|
|
95
|
+
"aria-label": string;
|
|
96
|
+
icon: LuiIconName;
|
|
97
|
+
onClick: () => void;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const PanelHeaderButton = ({ "aria-label": ariaLabel, icon, onClick }: PanelHeaderButtonProps) => (
|
|
101
|
+
<LuiButton
|
|
102
|
+
level={"plain-text"}
|
|
103
|
+
className={"lui-button-icon-only"}
|
|
104
|
+
size={"sm"}
|
|
105
|
+
onClick={onClick}
|
|
106
|
+
buttonProps={{
|
|
107
|
+
onTouchStart: onClick,
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
<LuiIcon name={icon} alt={ariaLabel} />
|
|
111
|
+
</LuiButton>
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
export interface PanelHeaderProps {
|
|
115
|
+
extraButtons?: ReactNode;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const PanelHeader = ({ extraButtons }: PanelHeaderProps) => {
|
|
119
|
+
const { panelClose, panelTogglePopout, panelPoppedOut, title } = useContext(PanelInstanceContext);
|
|
120
|
+
const [cursor, setCursor] = useState<"grab" | "grabbing">("grab");
|
|
121
|
+
|
|
122
|
+
const headerMouseDown = () => {
|
|
123
|
+
!panelPoppedOut && setCursor("grabbing");
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<div
|
|
128
|
+
className={"WindowPanel-header draggable-handle"}
|
|
129
|
+
onMouseDown={headerMouseDown}
|
|
130
|
+
onMouseUp={() => !panelPoppedOut && setCursor("grab")}
|
|
131
|
+
style={{ cursor }}
|
|
132
|
+
>
|
|
133
|
+
<div className={"WindowPanel-header-title"}>{title}</div>
|
|
134
|
+
<div className={"LuiFloatingWindow-buttons"}>{extraButtons}</div>
|
|
135
|
+
<div className={"LuiFloatingWindow-extra-buttons-divider"}>|</div>
|
|
136
|
+
<div className={"LuiFloatingWindow-buttons"}>
|
|
137
|
+
<PanelHeaderButton
|
|
138
|
+
aria-label={panelPoppedOut ? "Pop-in" : "Pop-out"}
|
|
139
|
+
onClick={panelTogglePopout}
|
|
140
|
+
icon={panelPoppedOut ? "ic_pop_back" : "ic_launch_new window_open"}
|
|
141
|
+
/>
|
|
142
|
+
<PanelHeaderButton aria-label={"Close"} onClick={panelClose} icon={"ic_clear"} />
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export const PanelContent = ({ children }: { children: ReactNode }) => {
|
|
149
|
+
return <div className={"WindowPanel-content"}>{children}</div>;
|
|
150
|
+
};
|