react-simple-dock 0.1.3 → 0.2.2

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/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # React-Simple-Dock
2
2
 
3
+ [![PyPI - pret-simple-dock](https://img.shields.io/pypi/v/pret-simple-dock?style=flat-square&color=blue)](https://pypi.org/project/pret-simple-dock/)
4
+ [![npm - react-simple-dock](https://img.shields.io/npm/v/react-simple-dock?style=flat-square&color=blue)](https://www.npmjs.com/package/react-simple-dock)
5
+ [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/percevalw/react-simple-dock/playwright.yml?style=flat-square)](https://github.com/percevalw/react-simple-dock/actions/workflows/playwright.yml)
6
+
3
7
  A set of React components to create a dockable interface, allowing to arrange and resize tabs.
4
8
 
5
9
  ## Installation of the javascript package
package/index.css CHANGED
@@ -1,6 +1,7 @@
1
1
  /* Light Theme, feel free to override */
2
2
  :root {
3
3
  --sd-grid-gap: 3px;
4
+ --sd-panel-border: solid;
4
5
  }
5
6
 
6
7
  :root[data-theme="light"] {
@@ -110,6 +111,8 @@ body[data-jp-theme-light] {
110
111
  .leaf > .panel-content {
111
112
  position: relative;
112
113
  flex: 1;
114
+ border: var(--sd-panel-border, solid) var(--sd-tab-border-color, #bdbdbd);
115
+ border-width: 0 1px 1px 1px;
113
116
  }
114
117
 
115
118
  .leaf > .panel-content > div {
@@ -119,7 +122,7 @@ body[data-jp-theme-light] {
119
122
  left: 0;
120
123
  width: 100%;
121
124
  height: 100%;
122
- overflow: scroll;
125
+ overflow: auto;
123
126
  }
124
127
 
125
128
  /* TAB HEADER */
package/index.d.ts CHANGED
@@ -1,14 +1,32 @@
1
1
  import React from "react";
2
2
  import "./index.css";
3
- import { LayoutConfig, PanelProps } from "./types";
3
+ import { PanelProps, DefaultLayoutConfig } from "./types";
4
+ /**
5
+ * A Panel component.
6
+ *
7
+ * This component represents a Panel within the layout.
8
+ *
9
+ * @param props.name The unique identifier of the panel.
10
+ * @param props.header The content to render in the panel header.
11
+ * @param props.children The content to render within the panel.
12
+ */
4
13
  export declare const Panel: (props: PanelProps) => any;
5
- export declare function Layout(props: {
14
+ /**
15
+ * Main layout component that organizes panels and handles drag and drop.
16
+ *
17
+ * The Layout component takes child panel components, constructs an initial layout configuration,
18
+ * and renders the panel structure using NestedPanel. It also wraps the layout in a DndProvider
19
+ * if drag and drop support is enabled.
20
+ *
21
+ * @param children The children `Panel` components to render within the layout.
22
+ * @param defaultConfig The default layout configuration to use.
23
+ * @param wrapDnd A boolean flag to enable or disable drag and drop support (default: true).
24
+ * @param collapseTabsOnMobile If true, auto-detect mobile devices and collapse the whole layout into a single tabbed panel.
25
+ * @returns A React element representing the complete panel layout.
26
+ */
27
+ export declare function Layout({ children, defaultConfig, wrapDnd, collapseTabsOnMobile, }: {
6
28
  children: React.ReactElement<PanelProps>[] | React.ReactElement<PanelProps>;
7
- defaultConfig?: LayoutConfig;
29
+ defaultConfig?: DefaultLayoutConfig;
8
30
  wrapDnd?: boolean;
31
+ collapseTabsOnMobile?: boolean | string[];
9
32
  }): import("react/jsx-runtime").JSX.Element;
10
- export declare namespace Layout {
11
- var defaultProps: {
12
- wrapDnd: boolean;
13
- };
14
- }
package/index.js CHANGED
@@ -1,17 +1,52 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
3
+ import { DndProvider, useDrag, useDrop, useDragDropManager } from "react-dnd";
3
4
  import "./index.css";
4
- import { DndProvider, useDrag, useDragDropManager, useDrop } from "react-dnd";
5
5
  import { filterPanels, movePanel } from "./utils";
6
6
  import { HTML5Backend } from "react-dnd-html5-backend";
7
- export const Panel = (props) => {
8
- return null;
9
- };
10
7
  function useForceUpdate() {
11
8
  const [value, setValue] = useState(0); // integer state
12
9
  return () => setValue((value) => value + 1); // update state to force render
13
10
  }
14
- const getPanelElementHeader = (node) => node.children[0];
11
+ const isMobileDevice = () => {
12
+ if (typeof window === "undefined" || typeof navigator === "undefined")
13
+ return false;
14
+ return /Mobi|Android|iPhone|iPad|iPod|Windows Phone|IEMobile|Opera Mini/i.test(navigator.userAgent || "");
15
+ };
16
+ const getPanelElementMaxHeaderHeight = (config, panelElements) => {
17
+ // If we have a leaf, then the header height is the max of each tabs header height
18
+ if (config.kind === "leaf") {
19
+ return Math.max(...config.tabs.map((tab) => panelElements.get(config).children[0].offsetHeight));
20
+ }
21
+ // If we have a row, then the header height is the max of each child's max header height
22
+ else if (config.kind === "row") {
23
+ return Math.max(...config.children.map((c) => getPanelElementMaxHeaderHeight(c, panelElements)));
24
+ }
25
+ // If we have a column, then the header height is the sum of each child's max header height
26
+ else {
27
+ return config.children.reduce((a, b) => a + getPanelElementMaxHeaderHeight(b, panelElements), 0);
28
+ }
29
+ };
30
+ function normalizeConfig(config, siblingsCount, depth = 0, default_kind = depth % 2 === 0 ? "row" : "column") {
31
+ if (typeof config === "string") {
32
+ config = { tabs: [config] };
33
+ }
34
+ if (Array.isArray(config)) {
35
+ config = { children: config };
36
+ }
37
+ // Leaf panel if tabs provided
38
+ if (config.tabs) {
39
+ const leaf = config;
40
+ const tabIndex = typeof leaf.tabIndex === "number" ? leaf.tabIndex : 0;
41
+ const size = typeof leaf.size === "number" ? leaf.size : 100 / siblingsCount;
42
+ return { kind: "leaf", tabs: leaf.tabs, tabIndex, size, nesting: leaf.nesting };
43
+ }
44
+ const container = config;
45
+ const kind = container.kind || default_kind;
46
+ const size = typeof container.size === "number" ? container.size : 100 / siblingsCount;
47
+ const children = container.children.map((child) => normalizeConfig(child, container.children.length, depth + 1, kind === "row" ? "column" : "row"));
48
+ return { kind, children, size, nesting: container.nesting };
49
+ }
15
50
  const TabHandle = ({ name, index, visible, onClick, children, }) => {
16
51
  const getItem = () => ({
17
52
  name,
@@ -119,15 +154,16 @@ const NestedPanel = React.memo(({ leaves, config, index, onResize, saveSizes, is
119
154
  let size = savedSizes.current[idx] * ratio;
120
155
  let nextSize = savedSizes.current[idx + 1] + (savedSizes.current[idx] - size);
121
156
  const total = savedSizes.current.reduce((a, b) => a + b, 0);
122
- const headerHeight = getPanelElementHeader(target.parentElement).offsetHeight;
123
157
  if (config.kind === "column") {
158
+ const headerHeightBefore = getPanelElementMaxHeaderHeight(config.children[idx], panelElements);
159
+ const headerHeightAfter = getPanelElementMaxHeaderHeight(config.children[idx + 1], panelElements);
124
160
  const parentHeight = panelContentRef.current.offsetHeight;
125
- if ((size * parentHeight) / total < headerHeight) {
126
- size = (headerHeight / parentHeight) * total;
161
+ if ((size * parentHeight) / total < headerHeightBefore) {
162
+ size = (headerHeightBefore / parentHeight) * total;
127
163
  nextSize = savedSizes.current[idx + 1] + (savedSizes.current[idx] - size);
128
164
  }
129
- else if ((nextSize * parentHeight) / total < headerHeight) {
130
- nextSize = (headerHeight / parentHeight) * total;
165
+ else if ((nextSize * parentHeight) / total < headerHeightAfter) {
166
+ nextSize = (headerHeightAfter / parentHeight) * total;
131
167
  size = savedSizes.current[idx] + (savedSizes.current[idx + 1] - nextSize);
132
168
  }
133
169
  }
@@ -148,9 +184,10 @@ const NestedPanel = React.memo(({ leaves, config, index, onResize, saveSizes, is
148
184
  };
149
185
  const panelContentRef = useRef(null);
150
186
  const panelRef = useRef(null);
151
- return (_jsxs("div", { className: `${config.kind} panel`, style: style, ref: panelRef, children: [config.kind === "leaf" ? (_jsx(TabHeader, { config: config, leaves: leaves, onClick: handleHeaderClick })) : null, _jsx("div", { className: "panel-content", ref: panelContentRef, style: makeStyle(), children: config.kind === "leaf" ? (config.tabIndex < config.tabs.length ? (_jsx("div", { children: leaves[config.tabs[config.tabIndex]].element })) : (_jsx("div", { style: { width: "100%", height: "100%" } }))) : (config.children.map((c, i) => (_jsx(NestedPanel, { config: c, leaves: leaves, saveSizes: handleSaveSizes, index: i, onResize: handleResize, isLast: i === config.children.length - 1, direction: config.kind, panelElements: panelElements }, i)))) }), !isLast && direction === "column" && (_jsx("div", { className: "resize-border bottom", onMouseDown: (e) => handleMouseDown(e, "bottom") })), !isLast && direction === "row" && (_jsx("div", { className: "resize-border right", onMouseDown: (e) => handleMouseDown(e, "right") }))] }));
187
+ return (_jsxs("div", { className: `${config.kind} panel`, style: style, ref: panelRef, children: [config.kind === "leaf" ? (_jsx(TabHeader, { config: config, leaves: leaves, onClick: handleHeaderClick })) : null, _jsx("div", { className: "panel-content", ref: panelContentRef, style: makeStyle(), children: config.kind === "leaf" ? (config.tabIndex < config.tabs.length ? (_jsx("div", { children: leaves[config.tabs[config.tabIndex]].element }, config.tabs[config.tabIndex])) : (_jsx("div", { style: { width: "100%", height: "100%" } }))) : (config.children.map((c, i) => (_jsx(NestedPanel, { config: c, leaves: leaves, saveSizes: handleSaveSizes, index: i, onResize: handleResize, isLast: i === config.children.length - 1, direction: config.kind, panelElements: panelElements }, i)))) }), !isLast && direction === "column" && (_jsx("div", { className: "resize-border bottom", onMouseDown: (e) => handleMouseDown(e, "bottom") })), !isLast && direction === "row" && (_jsx("div", { className: "resize-border right", onMouseDown: (e) => handleMouseDown(e, "right") }))] }));
152
188
  });
153
189
  const Overlay = ({ panelElements, onDrop, rootConfig, }) => {
190
+ const [, set] = useState();
154
191
  const closestRef = useRef(null);
155
192
  const lastItem = useRef(null);
156
193
  let lastPlaceholder = useRef(null);
@@ -208,14 +245,19 @@ const Overlay = ({ panelElements, onDrop, rootConfig, }) => {
208
245
  zones.push({ rect, config, index, element });
209
246
  };
210
247
  if (config.kind === "leaf") {
211
- if (!config.tabs.includes(name)) {
248
+ if (!(config.tabs.length == 1 && config.tabs[0] == name)) {
212
249
  pushZone("TOP", left, top, width, height / 2);
213
250
  pushZone("BOTTOM", left, top + height / 2, width, height / 2);
214
251
  pushZone("LEFT", left, top, width / 2, height);
215
252
  pushZone("RIGHT", left + width / 2, top, width / 2, height);
216
253
  }
217
- pushZone("CENTER", left, top, width, height);
218
- pushZone("TAB", left, top, width, getPanelElementHeader(element).offsetHeight);
254
+ // Only allow center zone if it's for the panel to stay at the same spot.
255
+ // Indeed, it was confusing since it appears like it's going to create
256
+ // a new panel, when in reality it just creates a new tab in the target panel.
257
+ else {
258
+ pushZone("CENTER", left, top, width, height);
259
+ }
260
+ pushZone("TAB", left, top, width, element.children[0].offsetHeight);
219
261
  }
220
262
  else {
221
263
  const firstTabs = config.children?.[0]?.tabs || [null];
@@ -340,9 +382,34 @@ const Overlay = ({ panelElements, onDrop, rootConfig, }) => {
340
382
  const overlayRef = useRef(null);
341
383
  return _jsx("div", { className: "tab-handle-overlay", ref: overlayRef });
342
384
  };
343
- export function Layout(props) {
344
- const children = React.Children.toArray(props.children);
345
- const namedChildren = Object.fromEntries(children.map((c, i) => [
385
+ /**
386
+ * A Panel component.
387
+ *
388
+ * This component represents a Panel within the layout.
389
+ *
390
+ * @param props.name The unique identifier of the panel.
391
+ * @param props.header The content to render in the panel header.
392
+ * @param props.children The content to render within the panel.
393
+ */
394
+ export const Panel = (props) => {
395
+ return null;
396
+ };
397
+ /**
398
+ * Main layout component that organizes panels and handles drag and drop.
399
+ *
400
+ * The Layout component takes child panel components, constructs an initial layout configuration,
401
+ * and renders the panel structure using NestedPanel. It also wraps the layout in a DndProvider
402
+ * if drag and drop support is enabled.
403
+ *
404
+ * @param children The children `Panel` components to render within the layout.
405
+ * @param defaultConfig The default layout configuration to use.
406
+ * @param wrapDnd A boolean flag to enable or disable drag and drop support (default: true).
407
+ * @param collapseTabsOnMobile If true, auto-detect mobile devices and collapse the whole layout into a single tabbed panel.
408
+ * @returns A React element representing the complete panel layout.
409
+ */
410
+ export function Layout({ children, defaultConfig, wrapDnd = true, collapseTabsOnMobile = true, }) {
411
+ const children_array = React.Children.toArray(children);
412
+ const namedChildren = Object.fromEntries(children_array.map((c, i) => [
346
413
  c.props.name || (c.key !== null ? c.key.toString().slice(2) : `unnamed-${i}`),
347
414
  {
348
415
  element: c.props.children,
@@ -350,15 +417,38 @@ export function Layout(props) {
350
417
  },
351
418
  ]));
352
419
  const panelElements = useRef(new Map());
353
- const [rootConfig, setRootConfig] = useState(props.defaultConfig || {
354
- kind: "row",
355
- size: 1,
356
- children: children.map((c, i) => ({
357
- kind: "leaf",
358
- tabs: [c.props.name || (c.key !== null ? c.key.toString().slice(2) : `unnamed-${i}`)],
359
- tabIndex: 0,
360
- size: 100 / children.length,
361
- })),
420
+ const [rootConfig, setRootConfig] = useState(() => {
421
+ const base = defaultConfig
422
+ ? normalizeConfig(defaultConfig, children_array.length)
423
+ : {
424
+ kind: "row",
425
+ size: 1,
426
+ children: children_array.map((c, i) => ({
427
+ kind: "leaf",
428
+ tabs: [c.props.name || (c.key !== null ? c.key.toString().slice(2) : `unnamed-${i}`)],
429
+ tabIndex: 0,
430
+ size: 100 / children_array.length,
431
+ })),
432
+ };
433
+ if (collapseTabsOnMobile && isMobileDevice()) {
434
+ // If collapseTabsOnMobile is a list, use the names in the list as the first tabs
435
+ // then complete with the rest of the named children
436
+ const actualTabs = Object.keys(namedChildren);
437
+ const tabs = [
438
+ ...(Array.isArray(collapseTabsOnMobile)
439
+ ? collapseTabsOnMobile.filter((name) => actualTabs.includes(name))
440
+ : []),
441
+ ...actualTabs.filter((name) => !Array.isArray(collapseTabsOnMobile) ||
442
+ !collapseTabsOnMobile.includes(name)),
443
+ ];
444
+ return {
445
+ kind: "leaf",
446
+ tabs: tabs,
447
+ tabIndex: 0,
448
+ size: 100,
449
+ };
450
+ }
451
+ return base;
362
452
  });
363
453
  let config = rootConfig;
364
454
  if (rootConfig.kind !== "leaf" || rootConfig.tabs.length > 0) {
@@ -373,11 +463,8 @@ export function Layout(props) {
373
463
  setRootConfig(newConfig);
374
464
  };
375
465
  const container = (_jsxs("div", { className: "container", children: [_jsx(NestedPanel, { leaves: namedChildren, config: config, panelElements: panelElements.current }), _jsx(Overlay, { panelElements: panelElements, onDrop: handleDrop, rootConfig: config })] }));
376
- if (props.wrapDnd) {
466
+ if (wrapDnd) {
377
467
  return _jsx(DndProvider, { backend: HTML5Backend, children: container });
378
468
  }
379
469
  return container;
380
470
  }
381
- Layout.defaultProps = {
382
- wrapDnd: true,
383
- };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-simple-dock",
3
- "version": "0.1.3",
3
+ "version": "0.2.2",
4
4
  "main": "index.js",
5
5
  "description": "Simple dock component for React",
6
6
  "repository": "https://github.com/percevalw/react-simple-dock",
package/types.d.ts CHANGED
@@ -28,6 +28,23 @@ export type ContainerLayoutConfig = {
28
28
  * A union type that represents either a leaf panel (with tabs) or a container panel (row/column with children).
29
29
  */
30
30
  export type LayoutConfig = LeafLayoutConfig | ContainerLayoutConfig;
31
+ /**
32
+ * User-provided config can omit kind, size, and tabIndex for inference
33
+ */
34
+ export type DefaultLeafConfig = {
35
+ kind?: "leaf";
36
+ tabs: string[];
37
+ tabIndex?: number;
38
+ size?: number;
39
+ nesting?: number;
40
+ };
41
+ export type DefaultContainerConfig = {
42
+ kind?: "row" | "column";
43
+ children: DefaultLayoutConfig[];
44
+ size?: number;
45
+ nesting?: number;
46
+ };
47
+ export type DefaultLayoutConfig = string | DefaultLeafConfig | DefaultContainerConfig | DefaultLayoutConfig[];
31
48
  /**
32
49
  * Properties for a panel component.
33
50
  *