@linzjs/windows 1.0.0 → 1.1.0

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 +129 -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,133 @@
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
+ ```
35
+
36
+ See [Chromatic storybook](https://64a2356b80885af35510b627-gsvwsgdsde.chromatic.com/) for the best documentation.
37
+
38
+ ## Examples
39
+
40
+ ### Modal
41
+ #### Example: Modal Context Provider
42
+ Don't forget to add a ModalContextProvider at the root of your project.
43
+ ```
44
+ export const App = () => (
45
+ <ModalContextProvider>
46
+ <div>...the rest of your app...</div>
47
+ </ModalContextProvider>
48
+ );
49
+ ```
50
+
51
+ #### Example: Modal Component
52
+ ```
53
+ // Extend your props with ModalCallback<RESULT_TYPE> to add a return type, and enable close/resolve
54
+ export interface TestModalProps extends ModalCallback<number> {
55
+ text: string; // A user property
56
+ }
57
+
58
+ // Close and resolve will be passed to your props magically!
59
+ export const TestModal = ({ text, close, resolve }: TestModalProps) => (
60
+ <Modal>
61
+ <div>
62
+ <div>This is the modal text: '{text}'</div>
63
+ <div>
64
+ <button onClick={close}>Close</button>
65
+ <button onClick={() => resolve(1)}>Return 1</button>
66
+ </div>
67
+ </div>
68
+ </Modal>
69
+ );
70
+ ```
71
+
72
+ #### Example: Modal Invocation
73
+ ```
74
+ export const TestModalUsage = () => {
75
+ // showModal to show modal, modalOwnerRef is only required if you have popout windows
76
+ const { showModal, modalOwnerRef } = useShowModal();
77
+
78
+ const showModalHandler = async () => {
79
+ // Show modal and await result
80
+ const result = await showModal(TestModal, { text: "Text text" });
81
+
82
+ // If result is undefined the modal was closed
83
+ if (!result) return alert("Modal closed");
84
+
85
+ // Otherwise we have a result
86
+ alert(`Modal result is: ${result}`);
87
+ };
88
+
89
+ // Remember to add the modalOwnerRef!
90
+ return (
91
+ <div ref={modalOwnerRef}>
92
+ <button onClick={showModalHandler}>Show modal</button>
93
+ </div>
94
+ );
95
+ };
96
+ ```
97
+
98
+ ### Windows
99
+ **Note:** *Windows are called Panels so as not to conflict with browser window types*
100
+
101
+ ##### Example: Panel Context Provider
102
+ Don't forget to add a PanelContextProvider at the root of your project.
103
+ ```
104
+ export const App = () => (
105
+ <PanelsContextProvider baseZIndex={500}>
106
+ ...the rest of your app...
107
+ </PanelsContextProvider>
108
+ );
109
+ ```
110
+
111
+ ##### Example: Panel Component
112
+ ```
113
+ export const TestPanel = () => {
114
+ return (
115
+ <Panel name={"Panel demo"} size={{ width: 640, height: 400 }}>
116
+ <PanelHeader title={"Panel demo"} />
117
+ <PanelContent>
118
+ ...PanelContent...
119
+ </PanelContent>
120
+ </Panel>
121
+ );
122
+ };
123
+ ```
124
+
125
+ ##### Example: Panel Invocation
126
+ ```
127
+ export const TestPanelUsage = () => {
128
+ const { openPanel } = useContext(PanelsContext);
129
+
130
+ return <button onClick={() => openPanel(TestPanel)}>Show panel</button>;
131
+ };
132
+ ```
9
133
 
10
- See [Chromatic](https://64a2356b80885af35510b627-gsvwsgdsde.chromatic.com/) for usage.
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.0",
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
+ }