diff --git a/README.md b/README.md
index 02e4a5c..87cbfbc 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,9 @@
# Rectpackr Layout
+[](https://www.npmjs.com/package/rectpackr-layout)
+[](https://coveralls.io/github/styiannis/rectpackr-layout?branch=main)
+[](https://codepen.io/collection/dGpeLa)
+
A web component that creates layouts by treating your HTML elements as rectangles and packing them using a best-fit 2D strip-packing algorithm.
## ⚙️ Why a Packing Algorithm for Web Layouts?
@@ -68,6 +72,39 @@ Or directly in your HTML:
```
+### Using a CDN (No Build Step Needed)
+
+Include it directly in your HTML via CDN:
+
+#### unpkg
+
+```html
+
+```
+
+#### jsDelivr
+
+```html
+
+```
+
+#### esm.sh
+
+```html
+
+```
+
+### Once installed, use the web component anywhere in your HTML:
+
+```html
+
+ Your content here
+
+```
+
## 📖 API Reference
### Attributes
@@ -151,6 +188,26 @@ The `x-direction` and `y-direction` attributes control visual placement, which m
```
+## 🎯 Live Demos
+
+### Consistent Width Gallery
+
+See predictable masonry-style layouts with equal-width elements
+
+[View on CodePen](https://codepen.io/styiannis/pen/ogbzBXg)
+
+### Mixed Dimension Gallery
+
+Explore optimal packing of variably-sized elements and aspect ratios
+
+[View on CodePen](https://codepen.io/styiannis/pen/XJXjayR)
+
+### Interactive Playground
+
+Experiment with real-time controls and dynamic content manipulation
+
+[View on CodePen](https://codepen.io/styiannis/pen/qEbqMBZ)
+
## ✅ Browser Support
Modern browsers with Web Components support.
diff --git a/package.json b/package.json
index c8f9006..e57f5f9 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "rectpackr-layout",
- "version": "0.9.0",
+ "version": "1.0.0",
"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.",
"keywords": [
"best-fit",
@@ -12,7 +12,7 @@
"masonry-layout",
"responsive",
"responsive-layout",
- "rectangle-packer",
+ "rectangle-packing",
"strip-packing",
"web-component",
"web-components"
@@ -63,22 +63,22 @@
"validate-exports": "node ./scripts/validate-exports.js"
},
"dependencies": {
- "best-fit-strip-pack": "^1.0.1"
+ "best-fit-strip-pack": "^1.0.2"
},
"devDependencies": {
- "@rollup/plugin-node-resolve": "^16.0.1",
+ "@rollup/plugin-node-resolve": "^16.0.2",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^12.1.4",
"@tsconfig/node22": "^22.0.2",
"@types/jest": "^30.0.0",
- "@types/node": "^24.5.2",
- "jest": "^30.1.3",
- "jest-environment-jsdom": "^30.1.2",
- "rollup": "^4.52.2",
+ "@types/node": "^24.7.0",
+ "jest": "^30.2.0",
+ "jest-environment-jsdom": "^30.2.0",
+ "rollup": "^4.52.4",
"rollup-plugin-dts": "^6.2.3",
"ts-jest": "^29.4.4",
"tslib": "^2.8.1",
- "typescript": "^5.9.2"
+ "typescript": "^5.9.3"
},
"exports": {
".": {
diff --git a/src/core/rectpackr.ts b/src/core/rectpackr.ts
index a3bad43..7f6afa2 100644
--- a/src/core/rectpackr.ts
+++ b/src/core/rectpackr.ts
@@ -33,9 +33,9 @@ export function create(
container,
children: [] as R['children'],
childrenContainer,
+ isPendingStartObservingChildren: false,
loadingImages: new Map(),
observers: { childrenContainerMutation, childrenResize, containerResize },
- pendingStartObservingChildren: false,
stripPack,
} as R;
@@ -47,5 +47,6 @@ export function create(
export function clear(instance: R) {
stopObserving(instance);
resetStyle(instance);
+ instance.children.length = 0;
instance.stripPack.reset();
}
diff --git a/src/core/types.ts b/src/core/types.ts
index bdcd821..52fc006 100644
--- a/src/core/types.ts
+++ b/src/core/types.ts
@@ -17,12 +17,12 @@ export interface IRectpackr {
width: number;
}[];
childrenContainer: HTMLElement;
+ isPendingStartObservingChildren: boolean;
loadingImages: Map void>;
observers: {
childrenContainerMutation: MutationObserver;
childrenResize: ResizeObserver;
containerResize: ResizeObserver;
};
- pendingStartObservingChildren: boolean;
stripPack: BestFitStripPack;
}
diff --git a/src/core/util.ts b/src/core/util.ts
index cb34a48..ddb4ba4 100644
--- a/src/core/util.ts
+++ b/src/core/util.ts
@@ -13,15 +13,15 @@ function render(instance: IRectpackr) {
}
function restartObservingChildren(instance: IRectpackr) {
- if (instance.pendingStartObservingChildren) {
+ if (instance.isPendingStartObservingChildren) {
return;
}
- instance.pendingStartObservingChildren = true;
+ instance.isPendingStartObservingChildren = true;
stopObservingChildren(instance);
requestAnimationFrame(() => {
- instance.pendingStartObservingChildren = false;
+ instance.isPendingStartObservingChildren = false;
startObservingChildren(instance);
});
}
@@ -53,15 +53,15 @@ function startObservingContainer(instance: IRectpackr) {
}
function startObservingImages(instance: IRectpackr) {
- function callback(this: HTMLImageElement) {
+ function onImgLoad(this: HTMLImageElement) {
instance.loadingImages.delete(this);
restartObservingChildren(instance);
}
for (const img of instance.childrenContainer.querySelectorAll('img')) {
if (!img.complete && !instance.loadingImages.get(img)) {
- instance.loadingImages.set(img, callback);
- img.addEventListener('load', callback, { once: true, passive: true });
+ instance.loadingImages.set(img, onImgLoad);
+ img.addEventListener('load', onImgLoad, { once: true, passive: true });
}
}
}
@@ -80,8 +80,8 @@ function stopObservingContainer(instance: IRectpackr) {
}
function stopObservingImages(instance: IRectpackr) {
- for (const [img, callback] of instance.loadingImages) {
- img.removeEventListener('load', callback);
+ for (const [img, onImgLoad] of instance.loadingImages) {
+ img.removeEventListener('load', onImgLoad);
instance.loadingImages.delete(img);
}
}
@@ -112,9 +112,7 @@ function updateStyle(
instance: IRectpackr,
children: { element: IRectpackrChildElement; point: [number, number] }[]
) {
- /*
- * Update children style.
- */
+ // Update children style
for (const { element, point } of children) {
const xVal =
point[0] *
@@ -143,9 +141,7 @@ function updateStyle(
}
}
- /*
- * Update container style.
- */
+ // Update container style
instance.container.style.height = `${instance.stripPack.packedHeight}px`;
}
@@ -154,6 +150,10 @@ function updateStyle(
/* ------------------------------------------------------------------------- */
export function onChildrenContainerMutation(instance: IRectpackr) {
+ if (instance.childrenContainer.children.length === 0) {
+ onChildResize(instance, []);
+ }
+
restartObservingChildren(instance);
restartObservingImages(instance);
}
@@ -196,9 +196,7 @@ export function onContainerResize(instance: IRectpackr) {
}
export function resetStyle(instance: IRectpackr) {
- /*
- * Reset children style.
- */
+ // Reset children style
for (const { element } of instance.children) {
if (instance.config.positioning === 'offset') {
element.style.inset = '';
@@ -207,9 +205,7 @@ export function resetStyle(instance: IRectpackr) {
}
}
- /*
- * Reset container style.
- */
+ // Reset container style
instance.container.style.height = '';
}
diff --git a/tests/rectpackr-layout-attributes.test.ts b/tests/rectpackr-layout-attributes.test.ts
index 1b1f4c3..112b268 100644
--- a/tests/rectpackr-layout-attributes.test.ts
+++ b/tests/rectpackr-layout-attributes.test.ts
@@ -6,12 +6,8 @@ import {
} from './util/types';
describe('Change attributes', () => {
- const shadowRootOffsetChildrenStyle = Object.freeze({
- ltr: '::slotted(:not([slot])){ position: absolute !important }',
- rtl: '::slotted(:not([slot])){ position: absolute !important }',
- ttb: '::slotted(:not([slot])){ position: absolute !important }',
- btt: '::slotted(:not([slot])){ position: absolute !important }',
- });
+ const shadowRootOffsetChildrenStyle =
+ '::slotted(:not([slot])){ position: absolute !important }';
const shadowRootTransformChildrenStyle = Object.freeze({
ltr: '::slotted(:not([slot])){ position: absolute !important; inset: 0 auto auto 0 !important }',
@@ -102,7 +98,7 @@ describe('Change attributes', () => {
changeAttributeAndValidate(
{ positioning: 'offset' },
- shadowRootOffsetChildrenStyle.rtl,
+ shadowRootOffsetChildrenStyle,
[
{ element: children[0]!, inset: '0 0 auto auto' },
{ element: children[1]!, inset: `0 ${1 * childWidth}px auto auto` },
@@ -113,7 +109,7 @@ describe('Change attributes', () => {
changeAttributeAndValidate(
{ 'x-direction': 'ltr' },
- shadowRootOffsetChildrenStyle.ltr,
+ shadowRootOffsetChildrenStyle,
[
{ element: children[0]!, inset: '0 auto auto 0' },
{ element: children[1]!, inset: `0 auto auto ${1 * childWidth}px` },
@@ -223,7 +219,7 @@ describe('Change attributes', () => {
changeAttributeAndValidate(
{ positioning: 'offset' },
- shadowRootOffsetChildrenStyle.btt,
+ shadowRootOffsetChildrenStyle,
[
{ element: children[0]!, inset: 'auto auto 0 0' },
{ element: children[1]!, inset: `auto auto ${1 * childHeight}px 0` },
@@ -234,7 +230,7 @@ describe('Change attributes', () => {
changeAttributeAndValidate(
{ 'y-direction': 'ttb' },
- shadowRootOffsetChildrenStyle.ttb,
+ shadowRootOffsetChildrenStyle,
[
{ element: children[0]!, inset: '0 auto auto 0' },
{ element: children[1]!, inset: `${1 * childHeight}px auto auto 0` },