Skip to content

Commit 8792b71

Browse files
authored
feat: implementation (#3)
1 parent 93de491 commit 8792b71

File tree

9 files changed

+519
-9
lines changed

9 files changed

+519
-9
lines changed

README.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,117 @@
11
# Rectpackr Layout
2+
3+
A web component that creates layouts by treating your HTML elements as rectangles and packing them using a best-fit 2D strip-packing algorithm.
4+
5+
## Why a Packing Algorithm for Web Layouts?
6+
7+
Web browsers naturally manage elements as rectangles. `rectpackr-layout` leverages this by applying a best-fit strip-packing algorithm — the same approach used in industrial optimization problems — to web layout creation.
8+
9+
## Intelligent Layouts Through Automated Measurement
10+
11+
The algorithm intelligently works with whatever dimensional information is available:
12+
13+
### How It Works:
14+
15+
- **Automatically measures** element dimensions through browser APIs
16+
- **Uses width as the primary constraint** for predictable flow
17+
- **Adapts to any height** — whether fixed, aspect-ratio based, or content-determined
18+
- **Handles mixed content seamlessly** without manual configuration
19+
20+
### You Can:
21+
22+
- Set explicit widths for pixel-perfect control
23+
- Use percentage-based or responsive widths
24+
- Let elements determine their own natural sizes
25+
- Mix and match approaches within the same layout
26+
27+
### What This Enables:
28+
29+
- **Truly flexible layouts** that work with your existing CSS approach
30+
- **Zero-configuration setups** for rapid prototyping
31+
- **Production-ready precision** when you need exact control
32+
- **Best of both worlds** — automation when you want it, control when you need it
33+
34+
## Installation
35+
36+
### Install the package via your preferred package manager:
37+
38+
#### npm
39+
40+
```bash
41+
npm install rectpackr-layout
42+
```
43+
44+
#### yarn
45+
46+
```bash
47+
yarn add rectpackr-layout
48+
```
49+
50+
#### pnpm
51+
52+
```bash
53+
pnpm install rectpackr-layout
54+
```
55+
56+
Then import it in your JavaScript:
57+
58+
```javascript
59+
// In your main.js or component file
60+
import 'rectpackr-layout';
61+
```
62+
63+
Or directly in your HTML:
64+
65+
```html
66+
<script type="module">
67+
import 'rectpackr-layout';
68+
</script>
69+
```
70+
71+
## API Reference
72+
73+
### Attributes
74+
75+
**`positioning`**
76+
77+
Defines the CSS method used to position items.
78+
79+
- `transform` (_Default_): Uses `transform: translate(x, y)`
80+
- `offset`: Uses CSS offset properties (`top`/`bottom` and `left`/`right`)
81+
82+
> **Performance Note:** The default `transform` value typically offers better performance through hardware acceleration. Use `offset` only when child elements already use `transform` for other purposes (animation etc.).
83+
84+
**`x-direction`**
85+
86+
Controls the horizontal packing direction.
87+
88+
- `ltr` (_Default_): Left-to-right packing
89+
- `rtl`: Right-to-left packing
90+
91+
**`y-direction`**
92+
93+
Controls the vertical packing direction.
94+
95+
- `ttb` (_Default_): Top-to-bottom packing
96+
- `btt`: Bottom-to-top packing
97+
98+
### A Note on Visual Order & Accessibility
99+
100+
The `x-direction` and `y-direction` attributes control visual placement, which may differ from DOM order.
101+
102+
- **DOM Order is Preserved:** The library never changes the underlying HTML structure, ensuring correct tab order and screen reader navigation
103+
- **Visual Order is Optimized:** The algorithm places items for spatial efficiency, which may not match linear DOM order
104+
105+
**Best Practice:** Ensure your HTML source reflects the logical reading order.
106+
107+
## Browser Support
108+
109+
Modern browsers with Web Components support.
110+
111+
## Issues and Support
112+
113+
If you encounter any issues or have questions, please [open an issue](https://github.com/styiannis/rectpackr-layout/issues).
114+
115+
## License
116+
117+
This project is licensed under the [MIT License](https://github.com/styiannis/rectpackr-layout?tab=MIT-1-ov-file#readme).

package.json

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,22 @@
11
{
22
"name": "rectpackr-layout",
3-
"version": "0.0.2",
4-
"description": "",
5-
"keywords": [],
3+
"version": "0.8.0",
4+
"description": "A web component that creates layouts by packing HTML elements as rectangles using a best-fit 2D strip-packing algorithm. Automatically measures dimensions and handles mixed content.",
5+
"keywords": [
6+
"best-fit",
7+
"custom-element",
8+
"custom-elements",
9+
"dom",
10+
"layout",
11+
"masonry",
12+
"masonry-layout",
13+
"responsive",
14+
"responsive-layout",
15+
"rectangle-packer",
16+
"strip-packing",
17+
"web-component",
18+
"web-components"
19+
],
620
"author": "Yiannis Stergiou <hello@styiannis.dev>",
721
"license": "MIT",
822
"repository": {
@@ -15,6 +29,8 @@
1529
"source": "src/index.ts",
1630
"main": "dist/cjs/index.js",
1731
"module": "dist/es/index.js",
32+
"unpkg": "./dist/umd/index.js",
33+
"jsdelivr": "./dist/umd/index.js",
1834
"types": "dist/@types/index.d.ts",
1935
"files": [
2036
"CHANGELOG.md",
@@ -68,7 +84,7 @@
6884
".": {
6985
"import": "./dist/es/index.js",
7086
"require": "./dist/cjs/index.js",
71-
"default": "./dist/es/index.js"
87+
"default": "./dist/umd/index.js"
7288
}
7389
}
7490
}

src/RectpackrLayout.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { IRectpackrConfig, IRectpackr, rectpackr } from './core';
2+
3+
/* ------------------------------------------------------------------------- */
4+
/* -------------------------- // Helper functions -------------------------- */
5+
/* ------------------------------------------------------------------------- */
6+
7+
const parseConfig = (config: {
8+
positioning: string | undefined;
9+
'x-direction': string | undefined;
10+
'y-direction': string | undefined;
11+
}): IRectpackrConfig => ({
12+
positioning:
13+
(config.positioning?.trim() ?? '') === 'offset' ? 'offset' : 'transform',
14+
'x-direction':
15+
(config['x-direction']?.trim() ?? '') === 'rtl' ? 'rtl' : 'ltr',
16+
'y-direction':
17+
(config['y-direction']?.trim() ?? '') === 'btt' ? 'btt' : 'ttb',
18+
});
19+
20+
const getStyleTextContent = (config: IRectpackrConfig) => {
21+
const insetValue = {
22+
ltr: { ttb: '0 auto auto 0', btt: 'auto auto 0 0' },
23+
rtl: { ttb: '0 0 auto auto', btt: 'auto 0 0 auto' },
24+
}[config['x-direction']][config['y-direction']];
25+
26+
return `
27+
slot{ box-sizing: border-box; position:relative; display:block; width:100% }
28+
::slotted(:not([slot])){ position:absolute; inset:${insetValue} }`;
29+
};
30+
31+
/* ------------------------------------------------------------------------- */
32+
/* -------------------------- Helper functions // -------------------------- */
33+
/* ------------------------------------------------------------------------- */
34+
35+
export class RectpackrLayout extends HTMLElement {
36+
#obj: IRectpackr | undefined = undefined;
37+
38+
static get observedAttributes() {
39+
return ['positioning', 'x-direction', 'y-direction'];
40+
}
41+
42+
#clear() {
43+
if (this.#obj) {
44+
rectpackr.clear(this.#obj);
45+
this.#obj = undefined;
46+
}
47+
}
48+
49+
#render() {
50+
const config = parseConfig({
51+
positioning: this.getAttribute('positioning') ?? undefined,
52+
'x-direction': this.getAttribute('x-direction') ?? undefined,
53+
'y-direction': this.getAttribute('y-direction') ?? undefined,
54+
});
55+
56+
if (this.shadowRoot) {
57+
const slot = this.shadowRoot.querySelector('slot')!;
58+
const style = this.shadowRoot.querySelector('style')!;
59+
style.textContent = getStyleTextContent(config);
60+
this.#obj = rectpackr.create(slot, this, config);
61+
} else {
62+
const shadowRoot = this.attachShadow({ mode: 'open' });
63+
const slot = document.createElement('slot');
64+
const style = document.createElement('style');
65+
style.textContent = getStyleTextContent(config);
66+
shadowRoot.appendChild(style);
67+
shadowRoot.appendChild(slot);
68+
this.#obj = rectpackr.create(slot, this, config);
69+
}
70+
}
71+
72+
attributeChangedCallback() {
73+
if (!this.shadowRoot) {
74+
return;
75+
}
76+
this.#clear();
77+
this.#render();
78+
}
79+
80+
connectedCallback() {
81+
this.#render();
82+
}
83+
84+
disconnectedCallback() {
85+
this.#clear();
86+
}
87+
}

src/core/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * as rectpackr from './rectpackr';
2+
export * from './types';

src/core/rectpackr.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { BestFitStripPack } from 'best-fit-strip-pack';
2+
import { IRectpackr } from './types';
3+
import {
4+
onChildResize,
5+
onChildrenContainerMutation,
6+
onContainerResize,
7+
resetStyle,
8+
startObserving,
9+
stopObserving,
10+
} from './util';
11+
12+
export function create<R extends IRectpackr>(
13+
container: R['container'],
14+
childrenContainer: R['childrenContainer'],
15+
config: R['config']
16+
) {
17+
const childrenContainerMutation = new MutationObserver(() =>
18+
onChildrenContainerMutation(instance)
19+
);
20+
21+
const childrenResize = new ResizeObserver((entries) =>
22+
onChildResize(instance, entries)
23+
);
24+
25+
const containerResize = new ResizeObserver(() => onContainerResize(instance));
26+
27+
const stripPack = new BestFitStripPack(
28+
Math.max(1, parseFloat(getComputedStyle(container).width))
29+
);
30+
31+
const instance = {
32+
config,
33+
container,
34+
children: [] as R['children'],
35+
childrenContainer,
36+
observers: { childrenContainerMutation, childrenResize, containerResize },
37+
stripPack,
38+
} as R;
39+
40+
startObserving(instance);
41+
42+
return instance;
43+
}
44+
45+
export function clear<R extends IRectpackr>(instance: R) {
46+
stopObserving(instance);
47+
resetStyle(instance);
48+
instance.stripPack.reset();
49+
}

src/core/types.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { BestFitStripPack } from 'best-fit-strip-pack';
2+
3+
export type IRectpackrChildElement = HTMLElement | SVGElement;
4+
5+
export interface IRectpackrConfig {
6+
positioning: 'offset' | 'transform';
7+
'x-direction': 'ltr' | 'rtl';
8+
'y-direction': 'ttb' | 'btt';
9+
}
10+
11+
export interface IRectpackr {
12+
config: IRectpackrConfig;
13+
container: HTMLElement;
14+
children: {
15+
element: IRectpackrChildElement;
16+
height: number;
17+
width: number;
18+
}[];
19+
childrenContainer: HTMLElement;
20+
observers: {
21+
childrenContainerMutation: MutationObserver;
22+
childrenResize: ResizeObserver;
23+
containerResize: ResizeObserver;
24+
};
25+
stripPack: BestFitStripPack;
26+
}

0 commit comments

Comments
 (0)