@wxn0brp/nya-dock 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 wxn0brP
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # Nyadock
2
+
3
+ A dock panel component library. Currently in early development stage.
4
+
5
+ > [!WARNING]
6
+ > This project is currently in early development and is not yet ready for production use.
7
+ > Expect breaking changes and incomplete features.
8
+
9
+ ## Features
10
+ - Dock interface component
11
+ - Built with TypeScript for type safety
12
+ - Styled with SCSS
13
+ - Part of the @wxn0brp ecosystem
14
+
15
+ ## Demo
16
+
17
+ [https://wxn0brp.github.io/Nyadock/demo](https://wxn0brp.github.io/Nyadock/demo)
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ yarn add github:wxn0brP/Nyadock#dist
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ Nyadock is controlled via a single `controller` instance that manages the entire layout.
28
+
29
+ ### 1. HTML Setup
30
+
31
+ First, you need a container element in your HTML to host the dock layout, and individual `div` elements that will become your panels.
32
+
33
+ ```html
34
+ <!-- The main container for the dock layout -->
35
+ <div id="app"></div>
36
+
37
+ <!-- Your panel content (can be hidden initially) -->
38
+ <div id="panel1-content" style="display: none;">
39
+ <h2>Panel 1</h2>
40
+ <p>Content for the first panel.</p>
41
+ </div>
42
+ <div id="panel2-content" style="display: none;">
43
+ <h2>Panel 2</h2>
44
+ <p>Content for the second panel.</p>
45
+ </div>
46
+ ```
47
+
48
+ ### 2. JavaScript/TypeScript Initialization
49
+
50
+ Import the `controller` and use it to configure and initialize the layout. Note that you also need to import the stylesheet.
51
+
52
+ ```typescript
53
+ import { controller } from "@wxn0brp/nya-dock/state";
54
+ import "@wxn0brp/nya-dock/style.css";
55
+
56
+ // 1. Get the master container element
57
+ const appContainer = document.querySelector("#app");
58
+
59
+ // 2. Get your panel content elements
60
+ const panel1 = document.querySelector("#panel1-content");
61
+ const panel2 = document.querySelector("#panel2-content");
62
+
63
+ // 3. Assign the master container
64
+ controller.master = appContainer;
65
+
66
+ // 4. Register your panels with unique IDs
67
+ controller.registerPanel("panel1", panel1);
68
+ controller.registerPanel("panel2", panel2);
69
+
70
+ // 5. Define the layout structure
71
+ // A horizontal split between "panel1" and "panel2"
72
+ const layout = ["panel1", "panel2"];
73
+
74
+ // A vertical split would be: ["panel1", "panel2", 1]
75
+ // Layouts can be nested.
76
+ controller.setDefaultState(layout);
77
+
78
+ // 6. Initialize the dock layout
79
+ controller.init();
80
+ ```
81
+
82
+ ### How the Layout Structure Works
83
+
84
+ The layout is defined by a `StructNode`, which is an array of panel IDs and nested arrays.
85
+
86
+ - **Horizontal Split (Row):** `["panelA", "panelB"]`
87
+ - **Vertical Split (Column):** `["panelA", "panelB", 1]` (the `1` indicates a vertical split)
88
+
89
+ You can nest these structures to create complex layouts:
90
+
91
+ ```javascript
92
+ // panelA is on the left.
93
+ // The right side is a vertical split between panelB and panelC.
94
+ const complexLayout = ["panelA", ["panelB", "panelC", 1]];
95
+ ```
96
+
97
+ ## License
98
+
99
+ MIT [LICENSE](./LICENSE)
100
+
101
+ ## Contributing
102
+
103
+ Contributions are welcome!
@@ -0,0 +1,2 @@
1
+ export declare const DRAG = 30;
2
+ export declare const RESIZE_MIN = 100;
package/dist/const.js ADDED
@@ -0,0 +1,2 @@
1
+ export const DRAG = 30;
2
+ export const RESIZE_MIN = 100;
@@ -0,0 +1,2 @@
1
+ import "@wxn0brp/flanker-ui/html";
2
+ import "./mouse/index.js";
package/dist/index.js ADDED
@@ -0,0 +1,103 @@
1
+ import "@wxn0brp/flanker-ui/html";
2
+ import logger from "./logger.js";
3
+ import "./mouse/index.js";
4
+ import { controller } from "./state.js";
5
+ logger.setLogLevel("DEBUG");
6
+ window.logger = logger;
7
+ window.controller = controller;
8
+ const app = qs("#app");
9
+ const panel1 = document.createElement("div");
10
+ panel1.innerHTML = `
11
+ <div class="panel-header">Panel 1</div>
12
+ <div class="panel-content">
13
+ <h3>Welcome to Panel 1</h3>
14
+ <p>This is the first panel in the layout.</p>
15
+ <ul>
16
+ <li>Item 1</li>
17
+ <li>Item 2</li>
18
+ <li>Item 3</li>
19
+ </ul>
20
+ <div class="button-group">
21
+ <button onclick="controller.reset()">Reset Config</button>
22
+ </div>
23
+ </div>
24
+ `;
25
+ const panel2 = document.createElement("div");
26
+ panel2.innerHTML = `
27
+ <div class="panel-header">Panel 2</div>
28
+ <div class="panel-content">
29
+ <h3>Content for Panel 2</h3>
30
+ <p>This is the second panel, split from the main container.</p>
31
+ <div class="button-group">
32
+ <button>Button 1</button>
33
+ <button>Button 2</button>
34
+ </div>
35
+ </div>
36
+ `;
37
+ const panel3 = document.createElement("div");
38
+ panel3.innerHTML = `
39
+ <div class="panel-header">Panel 3</div>
40
+ <div class="panel-content">
41
+ <h3>Additional Panel</h3>
42
+ <p>This is the third panel in the layout.</p>
43
+ <div class="info-box">
44
+ <p>This panel was created by splitting with Panel 2.</p>
45
+ </div>
46
+ </div>
47
+ `;
48
+ const panel4 = document.createElement("div");
49
+ panel4.innerHTML = `
50
+ <div class="panel-header">Panel 4</div>
51
+ <div class="panel-content">
52
+ <h3>Form Panel</h3>
53
+ <form>
54
+ <label for="name">Name:</label>
55
+ <input type="text" id="name" name="name"><br><br>
56
+ <label for="email">Email:</label>
57
+ <input type="email" id="email" name="email"><br><br>
58
+ <input type="submit" value="Submit">
59
+ </form>
60
+ </div>
61
+ `;
62
+ const panel5 = document.createElement("div");
63
+ panel5.innerHTML = `
64
+ <div class="panel-header">Panel 5</div>
65
+ <div class="panel-content">
66
+ <h3>Table Panel</h3>
67
+ <table border="1">
68
+ <tr>
69
+ <th>First name</th>
70
+ <th>Last name</th>
71
+ <th>Age</th>
72
+ </tr>
73
+ <tr>
74
+ <td>Jill</td>
75
+ <td>Smith</td>
76
+ <td>50</td>
77
+ </tr>
78
+ <tr>
79
+ <td>Eve</td>
80
+ <td>Jackson</td>
81
+ <td>94</td>
82
+ </tr>
83
+ </table>
84
+ </div>
85
+ `;
86
+ controller.master = app;
87
+ controller.setDefaultState([
88
+ "panel1",
89
+ [
90
+ "panel2",
91
+ [
92
+ ["panel3", "panel5", 1],
93
+ "panel4",
94
+ ],
95
+ 1
96
+ ]
97
+ ]);
98
+ controller.registerPanel("panel1", panel1);
99
+ controller.registerPanel("panel2", panel2);
100
+ controller.registerPanel("panel3", panel3);
101
+ controller.registerPanel("panel4", panel4);
102
+ controller.registerPanel("panel5", panel5);
103
+ controller.init();
@@ -0,0 +1,17 @@
1
+ declare const LOG_LEVELS: {
2
+ DEBUG: number;
3
+ INFO: number;
4
+ WARN: number;
5
+ ERROR: number;
6
+ };
7
+ declare function setLogLevel(level: keyof typeof LOG_LEVELS): void;
8
+ declare function exportLogs(): void;
9
+ declare const logger: {
10
+ debug: (...args: any[]) => void;
11
+ info: (...args: any[]) => void;
12
+ warn: (...args: any[]) => void;
13
+ error: (...args: any[]) => void;
14
+ setLogLevel: typeof setLogLevel;
15
+ exportLogs: typeof exportLogs;
16
+ };
17
+ export default logger;
package/dist/logger.js ADDED
@@ -0,0 +1,29 @@
1
+ const LOG_LEVELS = {
2
+ DEBUG: 0,
3
+ INFO: 1,
4
+ WARN: 2,
5
+ ERROR: 3,
6
+ };
7
+ let logs = [];
8
+ let currentLogLevel = LOG_LEVELS.ERROR;
9
+ function setLogLevel(level) {
10
+ currentLogLevel = LOG_LEVELS[level];
11
+ }
12
+ function exportLogs() {
13
+ console.log(logs.join("\n"));
14
+ }
15
+ function log(level, ...args) {
16
+ if (LOG_LEVELS[level] >= currentLogLevel) {
17
+ console.log(`[${level}]`, ...args);
18
+ logs.push(`[${level}] ${args.map(a => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}`);
19
+ }
20
+ }
21
+ const logger = {
22
+ debug: (...args) => log("DEBUG", ...args),
23
+ info: (...args) => log("INFO", ...args),
24
+ warn: (...args) => log("WARN", ...args),
25
+ error: (...args) => log("ERROR", ...args),
26
+ setLogLevel,
27
+ exportLogs,
28
+ };
29
+ export default logger;
@@ -0,0 +1,2 @@
1
+ import "./movePanel.js";
2
+ import "./resize.js";
@@ -0,0 +1,2 @@
1
+ import "./movePanel.js";
2
+ import "./resize.js";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,74 @@
1
+ import { DRAG } from "../const.js";
2
+ import { controller } from "../state.js";
3
+ import logger from "../logger.js";
4
+ import { detectDockZone, getRelativePosition } from "../utils/detect.js";
5
+ import { saveNyaState } from "../storage.js";
6
+ let draggingPanel = null;
7
+ document.addEventListener("mousedown", (e) => {
8
+ const target = e.target;
9
+ if (!target.classList.contains("panel")) {
10
+ logger.debug("Mousedown event ignored: target is not a panel");
11
+ return;
12
+ }
13
+ const data = getRelativePosition(e, target);
14
+ if (data.x > DRAG || data.y > DRAG) {
15
+ logger.debug("Mousedown event ignored: not in drag area");
16
+ return;
17
+ }
18
+ draggingPanel = target;
19
+ logger.info("Dragging panel started", draggingPanel);
20
+ document.body.style.cursor = "move";
21
+ });
22
+ document.addEventListener("mouseup", (e) => {
23
+ if (!draggingPanel) {
24
+ logger.debug("Mouseup event ignored: no panel is being dragged");
25
+ return;
26
+ }
27
+ document.body.style.cursor = "";
28
+ function end() {
29
+ logger.info("Dragging finished");
30
+ draggingPanel = null;
31
+ }
32
+ const allPanels = [...controller._panels.values()].filter(p => p !== draggingPanel);
33
+ logger.debug("All panels", allPanels);
34
+ const elemUnder = document.elementFromPoint(e.clientX, e.clientY);
35
+ if (!elemUnder) {
36
+ logger.debug("Mouseup event ignored: no element under cursor");
37
+ return end();
38
+ }
39
+ const targetPanel = elemUnder.closest(".panel");
40
+ if (!targetPanel) {
41
+ logger.debug("Mouseup event ignored: no target panel under cursor");
42
+ return end();
43
+ }
44
+ logger.debug("Target panel", targetPanel);
45
+ if (!allPanels.includes(targetPanel)) {
46
+ logger.debug("Mouseup event ignored: target panel is not a valid drop target");
47
+ return end();
48
+ }
49
+ const zone = detectDockZone(e, targetPanel);
50
+ logger.info(`Docking to ${zone}`);
51
+ if (zone === "center") {
52
+ logger.debug("Docking to center, no action taken");
53
+ return end();
54
+ }
55
+ let sourceId = draggingPanel.dataset.nya_id;
56
+ const targetId = targetPanel.dataset.nya_id;
57
+ if (!sourceId)
58
+ sourceId = draggingPanel.qs(".panel[data-nya_id]")?.dataset.nya_id;
59
+ if (!sourceId || !targetId) {
60
+ logger.error("Panel ID not found");
61
+ logger.error("Source ID:", sourceId);
62
+ logger.error("Target ID:", targetId);
63
+ return end();
64
+ }
65
+ if (sourceId === targetId) {
66
+ logger.debug("Mouseup event ignored: source and target are the same");
67
+ return end();
68
+ }
69
+ controller.movePanel(sourceId, targetId, zone);
70
+ controller._render();
71
+ controller._setDefaultSize();
72
+ saveNyaState();
73
+ end();
74
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,53 @@
1
+ import { clamp } from "@wxn0brp/flanker-ui/utils";
2
+ import { DRAG, RESIZE_MIN } from "../const.js";
3
+ import logger from "../logger.js";
4
+ import { controller } from "../state.js";
5
+ import { getRelativePosition } from "../utils/detect.js";
6
+ import { saveNyaState } from "../storage.js";
7
+ let draggingPanel = null;
8
+ let leftPanel = null;
9
+ let panelType = "width";
10
+ document.addEventListener("mousedown", (e) => {
11
+ const target = e.target;
12
+ if (!target.classList.contains("panel"))
13
+ return;
14
+ const _panelType = target.parentElement.classList.contains("column") ? "height" : "width";
15
+ const _leftPanel = target.parentElement.children[0];
16
+ const data = getRelativePosition(e, _leftPanel);
17
+ if (_panelType === "width") {
18
+ const delta = _leftPanel.offsetWidth - data.x;
19
+ if (delta > DRAG || delta < 0)
20
+ return;
21
+ }
22
+ else {
23
+ const delta = _leftPanel.offsetHeight - data.y;
24
+ if (delta > DRAG || delta < 0)
25
+ return;
26
+ }
27
+ draggingPanel = target;
28
+ leftPanel = _leftPanel;
29
+ panelType = _panelType;
30
+ logger.debug(`Resizing panel ${panelType}`);
31
+ document.body.style.cursor = panelType === "width" ? "col-resize" : "row-resize";
32
+ });
33
+ document.addEventListener("mousemove", (e) => {
34
+ if (!draggingPanel)
35
+ return;
36
+ const data = getRelativePosition(e, leftPanel);
37
+ let value = 0;
38
+ if (panelType === "width")
39
+ value = clamp(RESIZE_MIN, data.x, draggingPanel.parentElement.offsetWidth - RESIZE_MIN);
40
+ else
41
+ value = clamp(RESIZE_MIN, data.y, draggingPanel.parentElement.offsetHeight - RESIZE_MIN);
42
+ const id = draggingPanel.parentElement.dataset.nya_split;
43
+ const split = controller._splits.get(id);
44
+ split.style.setProperty("--size", `${value}px`);
45
+ logger.debug(`Resizing to ${value}`);
46
+ });
47
+ document.addEventListener("mouseup", (e) => {
48
+ if (draggingPanel)
49
+ logger.debug("Resizing finished");
50
+ draggingPanel = null;
51
+ document.body.style.cursor = "";
52
+ saveNyaState();
53
+ });
@@ -0,0 +1,2 @@
1
+ import { NyaController } from "./state.js";
2
+ export declare function render(controller: NyaController): void;
package/dist/render.js ADDED
@@ -0,0 +1,31 @@
1
+ export function render(controller) {
2
+ const tempElement = document.createElement("div");
3
+ for (const panel of controller._panels.values()) {
4
+ tempElement.appendChild(panel);
5
+ }
6
+ controller.master.innerHTML = "";
7
+ const masterSplit = renderSplit(controller, controller._split);
8
+ controller.master.appendChild(masterSplit);
9
+ }
10
+ function renderSplit(controller, split, prefix = "0") {
11
+ const div = document.createElement("div");
12
+ div.classList.add("split");
13
+ div.classList.add(split.type);
14
+ div.dataset.nya_split = prefix;
15
+ controller._splits.set(prefix, div);
16
+ for (let i = 0; i < split.nodes.length; i++) {
17
+ const node = split.nodes[i];
18
+ if (typeof node === "string") {
19
+ const panel = controller._panels.get(node);
20
+ div.appendChild(panel);
21
+ }
22
+ else {
23
+ const splitPanel = document.createElement("div");
24
+ splitPanel.classList.add("panel");
25
+ const splitContent = renderSplit(controller, node, `${prefix}.${i}`);
26
+ splitPanel.appendChild(splitContent);
27
+ div.appendChild(splitPanel);
28
+ }
29
+ }
30
+ return div;
31
+ }
@@ -0,0 +1,19 @@
1
+ import { Direction, NyaSplit, StructNode } from "./types.js";
2
+ export declare class NyaController {
3
+ _split: NyaSplit;
4
+ _panels: Map<string, HTMLDivElement>;
5
+ _splits: Map<string, HTMLDivElement>;
6
+ master: HTMLDivElement;
7
+ settings: {
8
+ key: string;
9
+ };
10
+ _struct: NyaSplit;
11
+ setDefaultState(state: NyaSplit | StructNode): void;
12
+ init(): void;
13
+ registerPanel(id: string, panel: HTMLDivElement): void;
14
+ _render(): void;
15
+ _setDefaultSize(): void;
16
+ movePanel(sourceId: string, targetId: string, zone: Direction): void;
17
+ reset(): void;
18
+ }
19
+ export declare const controller: NyaController;
package/dist/state.js ADDED
@@ -0,0 +1,45 @@
1
+ import { render } from "./render.js";
2
+ import { defaultState, loadState } from "./storage.js";
3
+ import { convertToSplits } from "./utils/convertSplit.js";
4
+ import { movePanel } from "./utils/movePanel.js";
5
+ export class NyaController {
6
+ _split;
7
+ _panels = new Map();
8
+ _splits = new Map();
9
+ master;
10
+ settings = {
11
+ key: "nya.dock",
12
+ };
13
+ _struct;
14
+ setDefaultState(state) {
15
+ this._struct = Array.isArray(state) ? convertToSplits(state) : state;
16
+ }
17
+ init() {
18
+ if (!this.master)
19
+ throw new Error("Master element not set");
20
+ if (!this._struct)
21
+ throw new Error("Structure not set");
22
+ loadState(this);
23
+ }
24
+ registerPanel(id, panel) {
25
+ panel.dataset.nya_id = id;
26
+ panel.classList.add("panel");
27
+ this._panels.set(id, panel);
28
+ }
29
+ _render() {
30
+ this._splits.clear();
31
+ return render(this);
32
+ }
33
+ _setDefaultSize() {
34
+ this._splits.forEach((split) => {
35
+ split.style.setProperty("--size", "50%");
36
+ });
37
+ }
38
+ movePanel(sourceId, targetId, zone) {
39
+ return movePanel(this, sourceId, targetId, zone);
40
+ }
41
+ reset() {
42
+ defaultState(this);
43
+ }
44
+ }
45
+ export const controller = new NyaController();
@@ -0,0 +1,5 @@
1
+ import { NyaController } from "./state.js";
2
+ export declare function saveState(controller: NyaController): void;
3
+ export declare function defaultState(controller: NyaController): void;
4
+ export declare function loadState(controller: NyaController): void;
5
+ export declare const saveNyaState: Function;
@@ -0,0 +1,32 @@
1
+ import { throttle } from "@wxn0brp/flanker-ui/utils";
2
+ import { controller } from "./state.js";
3
+ import logger from "./logger.js";
4
+ export function saveState(controller) {
5
+ const state = controller._split;
6
+ const sizes = {};
7
+ controller._splits.forEach((split, key) => {
8
+ sizes[key] = split.style.getPropertyValue("--size");
9
+ });
10
+ localStorage.setItem(controller.settings.key, JSON.stringify({ state, sizes }));
11
+ }
12
+ export function defaultState(controller) {
13
+ controller._split = Object.assign({}, controller._struct);
14
+ controller._render();
15
+ controller._setDefaultSize();
16
+ saveState(controller);
17
+ }
18
+ export function loadState(controller) {
19
+ const stateString = localStorage.getItem(controller.settings.key);
20
+ if (!stateString)
21
+ return defaultState(controller);
22
+ const { state, sizes } = JSON.parse(stateString);
23
+ controller._split = state;
24
+ controller._render();
25
+ controller._splits.forEach((split, key) => {
26
+ split.style.setProperty("--size", sizes[key]);
27
+ });
28
+ }
29
+ export const saveNyaState = throttle(() => {
30
+ logger.debug("Saving state");
31
+ saveState(controller);
32
+ }, 3000);
package/dist/style.css ADDED
@@ -0,0 +1,142 @@
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ :root {
8
+ --txt: #fff;
9
+ --back: #111;
10
+ }
11
+
12
+ body {
13
+ background: var(--back);
14
+ color: var(--txt);
15
+ font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
16
+ width: 100vw;
17
+ height: 100vh;
18
+ }
19
+
20
+ .master {
21
+ height: 100%;
22
+ width: 100%;
23
+ }
24
+
25
+ .split {
26
+ display: flex;
27
+ height: 100%;
28
+ width: 100%;
29
+ position: relative;
30
+ }
31
+ .split .panel {
32
+ position: relative;
33
+ }
34
+ .split .panel::after {
35
+ content: "";
36
+ position: absolute;
37
+ top: 0;
38
+ left: 0;
39
+ width: 30px;
40
+ height: 30px;
41
+ cursor: move;
42
+ z-index: 99;
43
+ }
44
+ .split.column {
45
+ flex-direction: column;
46
+ }
47
+ .split.column .panel {
48
+ width: 100%;
49
+ }
50
+ .split.column > :first-child {
51
+ border-bottom: 1px solid silver;
52
+ height: var(--size) !important;
53
+ }
54
+ .split.column > :first-child::before {
55
+ content: "";
56
+ position: absolute;
57
+ left: 0;
58
+ right: 0;
59
+ bottom: -15px;
60
+ height: 30px;
61
+ z-index: 99;
62
+ cursor: row-resize;
63
+ }
64
+ .split.row {
65
+ flex-direction: row;
66
+ }
67
+ .split.row .panel {
68
+ height: 100%;
69
+ }
70
+ .split.row > :first-child {
71
+ border-right: 1px solid silver;
72
+ width: var(--size) !important;
73
+ }
74
+ .split.row > :first-child::before {
75
+ content: "";
76
+ position: absolute;
77
+ top: 0;
78
+ bottom: 0;
79
+ right: -15px;
80
+ width: 30px;
81
+ z-index: 99;
82
+ cursor: col-resize;
83
+ }
84
+ .split > :nth-child(2) {
85
+ flex: 1;
86
+ }
87
+
88
+ .split .panel {
89
+ display: flex;
90
+ flex-direction: column;
91
+ }
92
+ .split .panel .panel-header {
93
+ background-color: #2c2c2c;
94
+ padding: 10px;
95
+ border-bottom: 1px solid #444;
96
+ font-weight: bold;
97
+ color: #ddd;
98
+ }
99
+ .split .panel .panel-content {
100
+ padding: 15px;
101
+ flex: 1;
102
+ overflow-y: auto;
103
+ }
104
+ .split .panel .panel-content h3 {
105
+ margin-top: 0;
106
+ color: #aaa;
107
+ border-bottom: 1px solid #444;
108
+ padding-bottom: 5px;
109
+ }
110
+ .split .panel .panel-content p {
111
+ margin: 10px 0;
112
+ line-height: 1.5;
113
+ }
114
+ .split .panel .panel-content ul {
115
+ padding-left: 20px;
116
+ }
117
+ .split .panel .panel-content ul li {
118
+ margin: 5px 0;
119
+ }
120
+ .split .panel .panel-content .button-group {
121
+ margin-top: 15px;
122
+ }
123
+ .split .panel .panel-content .button-group button {
124
+ background: #444;
125
+ color: white;
126
+ border: none;
127
+ padding: 8px 15px;
128
+ margin-right: 10px;
129
+ border-radius: 4px;
130
+ cursor: pointer;
131
+ }
132
+ .split .panel .panel-content .button-group button:hover {
133
+ background: #555;
134
+ }
135
+ .split .panel .panel-content .info-box {
136
+ background: #222;
137
+ padding: 10px;
138
+ border-radius: 4px;
139
+ border-left: 3px solid #666;
140
+ }
141
+
142
+ /*# sourceMappingURL=style.css.map */
@@ -0,0 +1 @@
1
+ {"version":3,"sourceRoot":"","sources":["../src/style.scss"],"names":[],"mappings":"AAAA;EACI;EACA;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACI;EACA;EACA;EACA;;AAEA;EACI;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAIR;EACI;;AAEA;EACI;;AAGJ;EACI;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAKZ;EACI;;AAEA;EACI;;AAGJ;EACI;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAKZ;EACI;;;AAIR;EACI;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;;AAEA;EACI;;AAIR;EACI;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAKZ;EACI;EACA;EACA;EACA","file":"style.css"}
@@ -0,0 +1,8 @@
1
+ export type Direction = "left" | "right" | "top" | "bottom";
2
+ export type NyaNode = string;
3
+ export type SplitType = "row" | "column";
4
+ export interface NyaSplit {
5
+ nodes: [NyaNode | NyaSplit, NyaNode | NyaSplit];
6
+ type: SplitType;
7
+ }
8
+ export type StructNode = [string | StructNode, string | StructNode, 1?];
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import { NyaSplit } from "../types.js";
2
+ export declare function convertToSplits(arr: any[]): NyaSplit;
@@ -0,0 +1,54 @@
1
+ export function convertToSplits(arr) {
2
+ if (arr.length === 0) {
3
+ throw new Error("Array structure must have at least one element");
4
+ }
5
+ let isColumnLayout = false;
6
+ let actualArray = arr;
7
+ if (arr.length === 3 && arr[2] === 1) {
8
+ isColumnLayout = true;
9
+ actualArray = arr.slice(0, 2);
10
+ }
11
+ if (actualArray.length === 1) {
12
+ const element = actualArray[0];
13
+ if (Array.isArray(element)) {
14
+ const nestedSplit = convertToSplits(element);
15
+ return {
16
+ type: isColumnLayout ? "column" : "row",
17
+ nodes: [nestedSplit, "empty"]
18
+ };
19
+ }
20
+ else {
21
+ return {
22
+ type: isColumnLayout ? "column" : "row",
23
+ nodes: [element, "empty"]
24
+ };
25
+ }
26
+ }
27
+ else if (actualArray.length === 2) {
28
+ const [first, second] = actualArray;
29
+ const firstElement = Array.isArray(first) ? convertToSplits(first) : first;
30
+ const secondElement = Array.isArray(second) ? convertToSplits(second) : second;
31
+ return {
32
+ type: isColumnLayout ? "column" : "row",
33
+ nodes: [firstElement, secondElement]
34
+ };
35
+ }
36
+ else {
37
+ const [first, ...rest] = actualArray;
38
+ let restStructure = [rest[0]];
39
+ for (let i = 1; i < rest.length; i++) {
40
+ if (isColumnLayout) {
41
+ restStructure = [restStructure, rest[i], 1];
42
+ }
43
+ else {
44
+ restStructure = [restStructure, rest[i]];
45
+ }
46
+ }
47
+ const firstElement = Array.isArray(first) ? convertToSplits(first) : first;
48
+ const restElement = Array.isArray(restStructure) ? convertToSplits(restStructure) : restStructure;
49
+ return {
50
+ type: isColumnLayout ? "column" : "row",
51
+ nodes: [firstElement, restElement]
52
+ };
53
+ }
54
+ }
@@ -0,0 +1,6 @@
1
+ import { Direction } from "../types.js";
2
+ export declare function getRelativePosition(e: MouseEvent, element: HTMLElement): {
3
+ x: number;
4
+ y: number;
5
+ };
6
+ export declare function detectDockZone(e: MouseEvent, panel: HTMLElement, threshold?: number): Direction | "center";
@@ -0,0 +1,21 @@
1
+ export function getRelativePosition(e, element) {
2
+ const rect = element.getBoundingClientRect();
3
+ return {
4
+ x: e.clientX - rect.left,
5
+ y: e.clientY - rect.top
6
+ };
7
+ }
8
+ export function detectDockZone(e, panel, threshold = 0.35) {
9
+ const rect = panel.getBoundingClientRect();
10
+ const relX = (e.clientX - rect.left) / rect.width;
11
+ const relY = (e.clientY - rect.top) / rect.height;
12
+ if (relX < threshold)
13
+ return "left";
14
+ if (relX > 1 - threshold)
15
+ return "right";
16
+ if (relY < threshold)
17
+ return "top";
18
+ if (relY > 1 - threshold)
19
+ return "bottom";
20
+ return "center";
21
+ }
@@ -0,0 +1,3 @@
1
+ import { NyaController } from "../state.js";
2
+ import { Direction } from "../types.js";
3
+ export declare function movePanel(controller: NyaController, sourceId: string, targetId: string, zone: Direction): void;
@@ -0,0 +1,57 @@
1
+ export function movePanel(controller, sourceId, targetId, zone) {
2
+ if (!controller._split || sourceId === targetId)
3
+ return;
4
+ if (controller._split.nodes.length === 0) {
5
+ controller._split = {
6
+ nodes: [sourceId, targetId],
7
+ type: (zone === "left" || zone === "right") ? "row" : "column",
8
+ };
9
+ return;
10
+ }
11
+ function recursiveRemove(node) {
12
+ if (typeof node === "string") {
13
+ return node === sourceId ? null : node;
14
+ }
15
+ const [nodeA, nodeB] = node.nodes;
16
+ const newA = recursiveRemove(nodeA);
17
+ const newB = recursiveRemove(nodeB);
18
+ if (newA && newB) {
19
+ if (newA !== nodeA || newB !== nodeB) {
20
+ return { ...node, nodes: [newA, newB] };
21
+ }
22
+ return node;
23
+ }
24
+ return newA || newB;
25
+ }
26
+ const treeAfterRemove = recursiveRemove(controller._split);
27
+ const newSplitNode = {
28
+ type: (zone === "left" || zone === "right") ? "row" : "column",
29
+ nodes: (zone === "left" || zone === "top") ? [sourceId, targetId] : [targetId, sourceId],
30
+ };
31
+ function recursiveDock(node) {
32
+ if (typeof node === "string") {
33
+ return node === targetId ? newSplitNode : node;
34
+ }
35
+ const [nodeA, nodeB] = node.nodes;
36
+ if (typeof nodeA === "string" && nodeA === targetId) {
37
+ return { ...node, nodes: [newSplitNode, nodeB] };
38
+ }
39
+ if (typeof nodeB === "string" && nodeB === targetId) {
40
+ return { ...node, nodes: [nodeA, newSplitNode] };
41
+ }
42
+ const newA = recursiveDock(nodeA);
43
+ const newB = recursiveDock(nodeB);
44
+ if (newA !== nodeA || newB !== nodeB) {
45
+ return { ...node, nodes: [newA, newB] };
46
+ }
47
+ return node;
48
+ }
49
+ let finalTree;
50
+ if (!treeAfterRemove) {
51
+ finalTree = newSplitNode;
52
+ }
53
+ else {
54
+ finalTree = recursiveDock(treeAfterRemove);
55
+ }
56
+ controller._split = finalTree;
57
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@wxn0brp/nya-dock",
3
+ "version": "0.0.1",
4
+ "main": "dist/index.js",
5
+ "types": "dist/index.d.ts",
6
+ "description": "",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/wxn0brP/Nyadock.git"
10
+ },
11
+ "homepage": "https://github.com/wxn0brP/Nyadock",
12
+ "author": "wxn0brP",
13
+ "license": "MIT",
14
+ "type": "module",
15
+ "scripts": {
16
+ "build": "tsc && tsc-alias && sass src:dist"
17
+ },
18
+ "devDependencies": {
19
+ "sass": "*",
20
+ "tsc-alias": "*",
21
+ "typescript": "*"
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.js",
30
+ "default": "./dist/index.js"
31
+ },
32
+ "./*": {
33
+ "types": "./dist/*.d.ts",
34
+ "import": "./dist/*.js",
35
+ "default": "./dist/*.js"
36
+ }
37
+ },
38
+ "dependencies": {
39
+ "@wxn0brp/flanker-ui": "^0.4.3"
40
+ }
41
+ }