@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.
Files changed (38) hide show
  1. package/.storybook/main.ts +26 -6
  2. package/README.md +32 -6
  3. package/package.json +34 -12
  4. package/src/modal/Modal.tsx +9 -5
  5. package/src/modal/ModalContextProvider.tsx +35 -36
  6. package/src/modal/PreModal.tsx +45 -0
  7. package/src/panel/OpenPanelButton.tsx +18 -0
  8. package/src/panel/OpenPanelIcon.scss +73 -0
  9. package/src/panel/OpenPanelIcon.tsx +50 -0
  10. package/src/panel/Panel.scss +34 -0
  11. package/src/panel/Panel.tsx +150 -0
  12. package/src/panel/PanelContext.ts +17 -0
  13. package/src/panel/PanelInstanceContext.ts +41 -0
  14. package/src/panel/PanelInstanceContextProvider.tsx +47 -0
  15. package/src/panel/PanelsContext.tsx +36 -0
  16. package/src/panel/PanelsContextProvider.tsx +140 -0
  17. package/src/panel/PopoutWindow.tsx +183 -0
  18. package/src/panel/generateId.ts +23 -0
  19. package/src/panel/handleStyleSheetsChanges.ts +71 -0
  20. package/src/stories/Introduction.mdx +18 -0
  21. package/src/stories/Introduction.stories.tsx +8 -0
  22. package/src/stories/modal/Modal.mdx +9 -3
  23. package/src/stories/modal/Modal.stories.tsx +1 -1
  24. package/src/stories/modal/PreModal.mdx +26 -0
  25. package/src/stories/modal/PreModal.stories.tsx +27 -0
  26. package/src/stories/modal/PreModal.tsx +79 -0
  27. package/src/stories/modal/TestModal.scss +21 -0
  28. package/src/stories/panel/PanelButtons/ShowPanel.mdx +21 -0
  29. package/src/stories/panel/PanelButtons/ShowPanel.stories.tsx +27 -0
  30. package/src/stories/panel/PanelButtons/ShowPanel.tsx +86 -0
  31. package/src/stories/panel/ShowPanel/ShowPanel.mdx +20 -0
  32. package/src/stories/panel/ShowPanel/ShowPanel.stories.tsx +27 -0
  33. package/src/stories/panel/ShowPanel/ShowPanel.tsx +70 -0
  34. package/src/stories/panel/ShowPanelResizingAgGrid/ShowPanelResizingAgGrid.mdx +21 -0
  35. package/src/stories/panel/ShowPanelResizingAgGrid/ShowPanelResizingAgGrid.stories.tsx +27 -0
  36. package/src/stories/panel/ShowPanelResizingAgGrid/ShowPanelResizingStepAgGrid.tsx +164 -0
  37. package/src/stories/support.js +16 -0
  38. package/src/util/useInterval.ts +11 -19
@@ -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
- # linz/windows
1
+ # @linzjs/windows
2
2
 
3
- [![semantic-release](https://img.shields.io/badge/semantic--release-react-e10079?logo=semantic-release)](https://github.com/semantic-release/semantic-release)
3
+ [![semantic-release: angular](https://img.shields.io/badge/semantic--release-angular-e10079?logo=semantic-release)](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
- - Async React Modal dialogs
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-gsvwsgdsde.chromatic.com/) for usage.
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
- "version": "1.0.0",
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.24",
29
- "@storybook/addon-essentials": "^7.0.24",
30
- "@storybook/addon-interactions": "^7.0.24",
31
- "@storybook/addon-links": "^7.0.24",
32
- "@storybook/blocks": "^7.0.24",
33
- "@storybook/builder-webpack5": "^7.0.24",
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.24",
36
- "@storybook/react": "^7.0.24",
37
- "@storybook/react-vite": "^7.0.24",
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.24",
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",
@@ -1,13 +1,12 @@
1
1
  import { ModalInstanceContext } from "./ModalInstanceContext";
2
- import { defer } from "lodash-es";
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 = ({ selectFirstInput = true, children }: ModalProps): ReactElement => {
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
- selectFirstInput && defer(() => dialogRef.current?.querySelector("input")?.select());
25
- }, [selectFirstInput]);
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 = async (
71
- ownerRef: MutableRefObject<HTMLElement | null>,
72
- Component: ComponentType,
73
- args: any,
74
- ): Promise<any> => {
75
- let componentInstance: ReactElement | undefined;
76
- const promise = new Promise((resolve) => {
77
- try {
78
- // If there are any exceptions the modal won't show
79
- setModals([
80
- ...modals,
81
- {
82
- uuid: uuid(),
83
- ownerElement: ownerRef.current ?? document.body,
84
- componentInstance: <Component {...args} resolve={resolve} close={() => resolve(undefined)} />,
85
- resolve,
86
- },
87
- ]);
88
- } catch (e) {
89
- console.error(e);
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
- // Wait for modal to complete
95
- const result = await promise;
91
+ // Wait for modal to complete
92
+ const result = await promise;
96
93
 
97
- // Close modal
98
- setModals(modals.filter((e) => e.componentInstance !== componentInstance));
94
+ // Close modal
95
+ setModals(modals.filter((e) => e.componentInstance !== componentInstance));
99
96
 
100
- return result;
101
- };
97
+ return result;
98
+ },
99
+ [modals],
100
+ );
102
101
 
103
- const modalHasView = (modalInstance: ModalInstance): boolean =>
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
- if (newModals.length !== modals.length) {
110
- setModals(newModals);
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">&#160;</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
+ };