rectflow 0.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.
- package/README.md +187 -0
- package/dist/AreaRenderer.d.ts +11 -0
- package/dist/Grid.d.ts +16 -0
- package/dist/LayoutEngine.d.ts +9 -0
- package/dist/Rectflow.d.ts +13 -0
- package/dist/index.d.ts +1 -0
- package/dist/rectflow.cjs.js +1 -0
- package/dist/rectflow.es.js +105 -0
- package/dist/rectflow.umd.js +1 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Rectflow
|
|
2
|
+
|
|
3
|
+
A lightweight **JavaScript layout engine** inspired by CSS Grid, designed for **programmatic layouts**, canvas-heavy apps, charting tools, editors, and environments where CSS Grid is not flexible enough.
|
|
4
|
+
|
|
5
|
+
Rectflow lets you define rows, columns, gaps, and named areas — then calculates and applies absolute positions automatically.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## ✨ Features
|
|
10
|
+
|
|
11
|
+
- CSS-Grid–like API (rows, columns, areas)
|
|
12
|
+
- Works **without CSS Grid** (pure JS layout engine)
|
|
13
|
+
- Supports:
|
|
14
|
+
|
|
15
|
+
- `fr`, `px`, `auto` tracks
|
|
16
|
+
- Gaps
|
|
17
|
+
- Named areas
|
|
18
|
+
|
|
19
|
+
- Automatic DOM creation for missing areas
|
|
20
|
+
- Resize-aware (re-layout on container resize)
|
|
21
|
+
- CDN-ready bundle + npm package
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 📦 Installation
|
|
26
|
+
|
|
27
|
+
### Using npm
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install rectflow
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { Rectflow } from 'rectflow'
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Using CDN
|
|
38
|
+
|
|
39
|
+
```html
|
|
40
|
+
<script src="https://unpkg.com/rectflow/dist/rectflow.umd.js"></script>
|
|
41
|
+
<script>
|
|
42
|
+
const rectflow = new Rectflow(...)
|
|
43
|
+
</script>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 🚀 Basic Usage
|
|
49
|
+
|
|
50
|
+
### HTML
|
|
51
|
+
|
|
52
|
+
```html
|
|
53
|
+
<div class="main"></div>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### JavaScript
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
const mainElem = document.querySelector('.main')
|
|
60
|
+
|
|
61
|
+
const rectflow = new Rectflow({
|
|
62
|
+
container: mainElem,
|
|
63
|
+
layout: {
|
|
64
|
+
rows: '50px auto 50px',
|
|
65
|
+
columns: '50px auto 100px',
|
|
66
|
+
gap: 5,
|
|
67
|
+
areas: [
|
|
68
|
+
['tool tool tool'],
|
|
69
|
+
['drawing chart widget'],
|
|
70
|
+
['drawing base widget']
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
rectflow.layout()
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 🧩 Areas Syntax
|
|
81
|
+
|
|
82
|
+
Rectflow supports **two area formats**:
|
|
83
|
+
|
|
84
|
+
### Shortcut (recommended)
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
areas: [
|
|
88
|
+
['tool tool'],
|
|
89
|
+
['drawing chart']
|
|
90
|
+
]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Expanded form
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
areas: [
|
|
97
|
+
['tool', 'tool'],
|
|
98
|
+
['drawing', 'chart'],
|
|
99
|
+
]
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Both are equivalent.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 🧠 Automatic Area Creation
|
|
107
|
+
|
|
108
|
+
If an area is defined in `areas` but **not registered**, Rectflow will:
|
|
109
|
+
|
|
110
|
+
- Automatically create a `<div>`
|
|
111
|
+
- Assign it the area name
|
|
112
|
+
- Insert it into the container
|
|
113
|
+
- Apply a random background color (for debugging)
|
|
114
|
+
|
|
115
|
+
This makes rapid prototyping easy.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 🔁 Resize Handling
|
|
120
|
+
|
|
121
|
+
Rectflow listens to container resize events and automatically recalculates layout:
|
|
122
|
+
|
|
123
|
+
- Uses `ResizeObserver`
|
|
124
|
+
- Re-applies positions when size changes
|
|
125
|
+
|
|
126
|
+
No manual resize handling required.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## ⚙️ API Reference
|
|
131
|
+
|
|
132
|
+
### `new Rectflow(config)`
|
|
133
|
+
|
|
134
|
+
#### config
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
{
|
|
138
|
+
container: HTMLElement
|
|
139
|
+
layout: {
|
|
140
|
+
rows: string
|
|
141
|
+
columns: string
|
|
142
|
+
gap?: number
|
|
143
|
+
areas: string[][]
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
### `registerArea(name, element)`
|
|
151
|
+
|
|
152
|
+
Registers an existing DOM element for an area.
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
rectflow.registerArea('chart', chartElement)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
### `layout()`
|
|
161
|
+
|
|
162
|
+
Calculates layout and applies styles.
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
rectflow.layout()
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## 📄 License
|
|
171
|
+
|
|
172
|
+
MIT
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## ❤️ Inspiration
|
|
177
|
+
|
|
178
|
+
Inspired by:
|
|
179
|
+
|
|
180
|
+
- CSS Grid
|
|
181
|
+
- Game UI layout systems
|
|
182
|
+
- Charting & trading platforms
|
|
183
|
+
- Dashboards
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
If you’re building editors, dashboards, charting tools, or canvas-heavy apps — Rectflow is built for you.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RectflowConfig } from './Rectflow';
|
|
2
|
+
export declare class AreaRenderer {
|
|
3
|
+
private readonly config;
|
|
4
|
+
private areas;
|
|
5
|
+
private engine;
|
|
6
|
+
constructor(config: RectflowConfig);
|
|
7
|
+
registerArea(name: string, elem: HTMLElement): void;
|
|
8
|
+
layout(): void;
|
|
9
|
+
private ensureArea;
|
|
10
|
+
clearArea(): void;
|
|
11
|
+
}
|
package/dist/Grid.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type AreaName = string;
|
|
2
|
+
export type TrackSize = number | `${number}px` | `${number}fr` | 'auto';
|
|
3
|
+
export type GridAreas = string[][];
|
|
4
|
+
export interface GridConfig {
|
|
5
|
+
rows: string;
|
|
6
|
+
columns: string;
|
|
7
|
+
gap?: number;
|
|
8
|
+
areas: GridAreas;
|
|
9
|
+
}
|
|
10
|
+
export interface Rect {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
}
|
|
16
|
+
export type ComputedLayout = Record<AreaName, Rect>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Rect, ComputedLayout, GridConfig } from './Grid';
|
|
2
|
+
export declare class LayoutEngine {
|
|
3
|
+
private config;
|
|
4
|
+
constructor(config: GridConfig);
|
|
5
|
+
compute(container: Rect): ComputedLayout;
|
|
6
|
+
private normalizeAreas;
|
|
7
|
+
private parseTracks;
|
|
8
|
+
private accumulate;
|
|
9
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { GridConfig } from './Grid';
|
|
2
|
+
export type RectflowConfig = {
|
|
3
|
+
container: HTMLElement;
|
|
4
|
+
layout: GridConfig;
|
|
5
|
+
};
|
|
6
|
+
export declare class Rectflow {
|
|
7
|
+
private areaRenderer;
|
|
8
|
+
private observer;
|
|
9
|
+
constructor(config: RectflowConfig);
|
|
10
|
+
registerArea(area: string, elem: HTMLElement): void;
|
|
11
|
+
layout(): void;
|
|
12
|
+
destroy(): void;
|
|
13
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Rectflow';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});class g{constructor(e){this.config=e}compute(e){const t=this.parseTracks(this.config.rows,e.height),r=this.parseTracks(this.config.columns,e.width),i=this.config.gap??0,n=this.accumulate(t,i),a=this.accumulate(r,i),l=this.normalizeAreas(this.config.areas),h={};for(let s=0;s<l.length;s++)for(let o=0;o<l[s].length;o++){const u=l[s][o];if(u!==".")if(!h[u])h[u]={x:a[o],y:n[s],width:r[o],height:t[s]};else{const c=h[u];a[o]+r[o]>c.x+c.width&&(c.width=a[o]+r[o]-c.x),n[s]+t[s]>c.y+c.height&&(c.height=n[s]+t[s]-c.y)}}return h}normalizeAreas(e){return e.map(t=>t.length===1?t[0].trim().split(/\s+/):t)}parseTracks(e,t){const r=e.split(/\s+/),i=this.config.gap??0;let n=0,a=0;for(const s of r)s.endsWith("px")?n+=parseFloat(s):s.endsWith("fr")?a+=parseFloat(s):s==="auto"&&(a+=1);const l=t-n-(r.length-1)*i,h=a>0?l/a:0;return r.map(s=>s.endsWith("px")?parseFloat(s):s.endsWith("fr")?parseFloat(s)*h:s==="auto"?h:0)}accumulate(e,t){const r=[];let i=0;for(const n of e)r.push(i),i+=n+t;return r}}class d{constructor(e){this.config=e,this.engine=new g(e.layout)}areas=new Map;engine;registerArea(e,t){const r=this.areas.get(e);r?.auto&&r.elem.remove(),t.style.position="absolute",this.areas.set(e,{elem:t,auto:!1})}layout(){const e={x:0,y:0,width:this.config.container.clientWidth,height:this.config.container.clientHeight},t=this.engine.compute(e);for(const r in t){const i=this.ensureArea(r),n=t[r];Object.assign(i.style,{left:`${n.x}px`,top:`${n.y}px`,width:`${n.width}px`,height:`${n.height}px`})}}ensureArea(e){function t(){return`hsl(${Math.floor(Math.random()*360)}, 70%, 70%)`}const r=this.areas.get(e);if(r)return r.elem;const i=document.createElement("div");return i.dataset.rectflowArea=e,i.style.background=t(),i.style.position="absolute",this.config.container.appendChild(i),this.areas.set(e,{elem:i,auto:!0}),i}clearArea(){this.areas.clear()}}class p{areaRenderer;observer;constructor(e){e.container.style.position="relative",this.areaRenderer=new d(e),this.observer=new ResizeObserver(()=>this.layout()),this.observer.observe(e.container)}registerArea(e,t){this.areaRenderer.registerArea(e,t)}layout(){this.areaRenderer.layout()}destroy(){this.observer.disconnect(),this.areaRenderer.clearArea()}}exports.Rectflow=p;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
class g {
|
|
2
|
+
constructor(e) {
|
|
3
|
+
this.config = e;
|
|
4
|
+
}
|
|
5
|
+
compute(e) {
|
|
6
|
+
const t = this.parseTracks(this.config.rows, e.height), r = this.parseTracks(this.config.columns, e.width), i = this.config.gap ?? 0, n = this.accumulate(t, i), a = this.accumulate(r, i), l = this.normalizeAreas(this.config.areas), h = {};
|
|
7
|
+
for (let s = 0; s < l.length; s++)
|
|
8
|
+
for (let o = 0; o < l[s].length; o++) {
|
|
9
|
+
const u = l[s][o];
|
|
10
|
+
if (u !== ".")
|
|
11
|
+
if (!h[u])
|
|
12
|
+
h[u] = {
|
|
13
|
+
x: a[o],
|
|
14
|
+
y: n[s],
|
|
15
|
+
width: r[o],
|
|
16
|
+
height: t[s]
|
|
17
|
+
};
|
|
18
|
+
else {
|
|
19
|
+
const c = h[u];
|
|
20
|
+
a[o] + r[o] > c.x + c.width && (c.width = a[o] + r[o] - c.x), n[s] + t[s] > c.y + c.height && (c.height = n[s] + t[s] - c.y);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return h;
|
|
24
|
+
}
|
|
25
|
+
normalizeAreas(e) {
|
|
26
|
+
return e.map((t) => t.length === 1 ? t[0].trim().split(/\s+/) : t);
|
|
27
|
+
}
|
|
28
|
+
parseTracks(e, t) {
|
|
29
|
+
const r = e.split(/\s+/), i = this.config.gap ?? 0;
|
|
30
|
+
let n = 0, a = 0;
|
|
31
|
+
for (const s of r)
|
|
32
|
+
s.endsWith("px") ? n += parseFloat(s) : s.endsWith("fr") ? a += parseFloat(s) : s === "auto" && (a += 1);
|
|
33
|
+
const l = t - n - (r.length - 1) * i, h = a > 0 ? l / a : 0;
|
|
34
|
+
return r.map((s) => s.endsWith("px") ? parseFloat(s) : s.endsWith("fr") ? parseFloat(s) * h : s === "auto" ? h : 0);
|
|
35
|
+
}
|
|
36
|
+
accumulate(e, t) {
|
|
37
|
+
const r = [];
|
|
38
|
+
let i = 0;
|
|
39
|
+
for (const n of e)
|
|
40
|
+
r.push(i), i += n + t;
|
|
41
|
+
return r;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
class p {
|
|
45
|
+
constructor(e) {
|
|
46
|
+
this.config = e, this.engine = new g(e.layout);
|
|
47
|
+
}
|
|
48
|
+
areas = /* @__PURE__ */ new Map();
|
|
49
|
+
engine;
|
|
50
|
+
registerArea(e, t) {
|
|
51
|
+
const r = this.areas.get(e);
|
|
52
|
+
r?.auto && r.elem.remove(), t.style.position = "absolute", this.areas.set(e, {
|
|
53
|
+
elem: t,
|
|
54
|
+
auto: !1
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
layout() {
|
|
58
|
+
const e = {
|
|
59
|
+
x: 0,
|
|
60
|
+
y: 0,
|
|
61
|
+
width: this.config.container.clientWidth,
|
|
62
|
+
height: this.config.container.clientHeight
|
|
63
|
+
}, t = this.engine.compute(e);
|
|
64
|
+
for (const r in t) {
|
|
65
|
+
const i = this.ensureArea(r), n = t[r];
|
|
66
|
+
Object.assign(i.style, {
|
|
67
|
+
left: `${n.x}px`,
|
|
68
|
+
top: `${n.y}px`,
|
|
69
|
+
width: `${n.width}px`,
|
|
70
|
+
height: `${n.height}px`
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
ensureArea(e) {
|
|
75
|
+
function t() {
|
|
76
|
+
return `hsl(${Math.floor(Math.random() * 360)}, 70%, 70%)`;
|
|
77
|
+
}
|
|
78
|
+
const r = this.areas.get(e);
|
|
79
|
+
if (r) return r.elem;
|
|
80
|
+
const i = document.createElement("div");
|
|
81
|
+
return i.dataset.rectflowArea = e, i.style.background = t(), i.style.position = "absolute", this.config.container.appendChild(i), this.areas.set(e, { elem: i, auto: !0 }), i;
|
|
82
|
+
}
|
|
83
|
+
clearArea() {
|
|
84
|
+
this.areas.clear();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
class d {
|
|
88
|
+
areaRenderer;
|
|
89
|
+
observer;
|
|
90
|
+
constructor(e) {
|
|
91
|
+
e.container.style.position = "relative", this.areaRenderer = new p(e), this.observer = new ResizeObserver(() => this.layout()), this.observer.observe(e.container);
|
|
92
|
+
}
|
|
93
|
+
registerArea(e, t) {
|
|
94
|
+
this.areaRenderer.registerArea(e, t);
|
|
95
|
+
}
|
|
96
|
+
layout() {
|
|
97
|
+
this.areaRenderer.layout();
|
|
98
|
+
}
|
|
99
|
+
destroy() {
|
|
100
|
+
this.observer.disconnect(), this.areaRenderer.clearArea();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export {
|
|
104
|
+
d as Rectflow
|
|
105
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(l,u){typeof exports=="object"&&typeof module<"u"?u(exports):typeof define=="function"&&define.amd?define(["exports"],u):(l=typeof globalThis<"u"?globalThis:l||self,u(l.Rectflow={}))})(this,(function(l){"use strict";class u{constructor(e){this.config=e}compute(e){const t=this.parseTracks(this.config.rows,e.height),r=this.parseTracks(this.config.columns,e.width),i=this.config.gap??0,n=this.accumulate(t,i),a=this.accumulate(r,i),f=this.normalizeAreas(this.config.areas),h={};for(let s=0;s<f.length;s++)for(let o=0;o<f[s].length;o++){const d=f[s][o];if(d!==".")if(!h[d])h[d]={x:a[o],y:n[s],width:r[o],height:t[s]};else{const c=h[d];a[o]+r[o]>c.x+c.width&&(c.width=a[o]+r[o]-c.x),n[s]+t[s]>c.y+c.height&&(c.height=n[s]+t[s]-c.y)}}return h}normalizeAreas(e){return e.map(t=>t.length===1?t[0].trim().split(/\s+/):t)}parseTracks(e,t){const r=e.split(/\s+/),i=this.config.gap??0;let n=0,a=0;for(const s of r)s.endsWith("px")?n+=parseFloat(s):s.endsWith("fr")?a+=parseFloat(s):s==="auto"&&(a+=1);const f=t-n-(r.length-1)*i,h=a>0?f/a:0;return r.map(s=>s.endsWith("px")?parseFloat(s):s.endsWith("fr")?parseFloat(s)*h:s==="auto"?h:0)}accumulate(e,t){const r=[];let i=0;for(const n of e)r.push(i),i+=n+t;return r}}class p{constructor(e){this.config=e,this.engine=new u(e.layout)}areas=new Map;engine;registerArea(e,t){const r=this.areas.get(e);r?.auto&&r.elem.remove(),t.style.position="absolute",this.areas.set(e,{elem:t,auto:!1})}layout(){const e={x:0,y:0,width:this.config.container.clientWidth,height:this.config.container.clientHeight},t=this.engine.compute(e);for(const r in t){const i=this.ensureArea(r),n=t[r];Object.assign(i.style,{left:`${n.x}px`,top:`${n.y}px`,width:`${n.width}px`,height:`${n.height}px`})}}ensureArea(e){function t(){return`hsl(${Math.floor(Math.random()*360)}, 70%, 70%)`}const r=this.areas.get(e);if(r)return r.elem;const i=document.createElement("div");return i.dataset.rectflowArea=e,i.style.background=t(),i.style.position="absolute",this.config.container.appendChild(i),this.areas.set(e,{elem:i,auto:!0}),i}clearArea(){this.areas.clear()}}class m{areaRenderer;observer;constructor(e){e.container.style.position="relative",this.areaRenderer=new p(e),this.observer=new ResizeObserver(()=>this.layout()),this.observer.observe(e.container)}registerArea(e,t){this.areaRenderer.registerArea(e,t)}layout(){this.areaRenderer.layout()}destroy(){this.observer.disconnect(),this.areaRenderer.clearArea()}}l.Rectflow=m,Object.defineProperty(l,Symbol.toStringTag,{value:"Module"})}));
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rectflow",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Absolute-position layout engine with grid-like DSL",
|
|
6
|
+
"main": "./dist/rectflow.cjs.js",
|
|
7
|
+
"module": "./dist/rectflow.es.js",
|
|
8
|
+
"unpkg": "./dist/rectflow.umd.js",
|
|
9
|
+
"jsdelivr": "./dist/rectflow.umd.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"dev": "vite",
|
|
16
|
+
"build": "tsc && vite build",
|
|
17
|
+
"preview": "vite preview"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"layout",
|
|
21
|
+
"grid",
|
|
22
|
+
"dashboard",
|
|
23
|
+
"absolute-layout",
|
|
24
|
+
"ui-engine",
|
|
25
|
+
"dock",
|
|
26
|
+
"panel"
|
|
27
|
+
],
|
|
28
|
+
"author": "Danish Ahmed Khan",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^25.0.3",
|
|
32
|
+
"typescript": "~5.9.3",
|
|
33
|
+
"vite": "^7.2.4"
|
|
34
|
+
}
|
|
35
|
+
}
|