petersburg 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) 2026 xfaSts9cwY6VqLNTMAtR
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,155 @@
1
+ # Petersburg
2
+
3
+ A React layout library implementing Hermitage-style 2D bin-packing for picture arrangement.
4
+
5
+ Named after the Hermitage Museum in St. Petersburg, where curators arrange paintings tetris-style to maximize limited wall space.
6
+
7
+ ## Features
8
+
9
+ - **MaxRects bin-packing algorithm** — efficient 2D rectangle packing
10
+ - **Multiple sort strategies** — optimize for packing efficiency or preserve input order
11
+ - **Responsive** — auto-measures container and recalculates on resize
12
+ - **Accessible** — DOM order matches visual flow for proper tab navigation
13
+ - **Lightweight** — no dependencies beyond React
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install petersburg
19
+ ```
20
+
21
+ ## Basic Usage
22
+
23
+ ```tsx
24
+ import { HermitageLayout } from 'petersburg';
25
+
26
+ const items = [
27
+ { id: '1', width: 200, height: 150, content: <img src="..." /> },
28
+ { id: '2', width: 100, height: 100, content: <img src="..." /> },
29
+ { id: '3', width: 150, height: 200, content: <img src="..." /> },
30
+ ];
31
+
32
+ function Gallery() {
33
+ return (
34
+ <HermitageLayout
35
+ items={items}
36
+ gap={8}
37
+ sortStrategy="ordered"
38
+ />
39
+ );
40
+ }
41
+ ```
42
+
43
+ ## Props
44
+
45
+ | Prop | Type | Default | Description |
46
+ |------|------|---------|-------------|
47
+ | `items` | `LayoutItem[]` | required | Array of items to layout |
48
+ | `containerWidth` | `number` | auto | Fixed width in pixels. If omitted, measures parent container. |
49
+ | `gap` | `number` | `8` | Gap between items in pixels |
50
+ | `sortStrategy` | `SortStrategy` | `"none"` | How to order items during packing |
51
+ | `className` | `string` | — | CSS class for the container |
52
+
53
+ ## Types
54
+
55
+ ```tsx
56
+ interface LayoutItem {
57
+ id: string;
58
+ width: number;
59
+ height: number;
60
+ content: ReactNode;
61
+ }
62
+
63
+ type SortStrategy =
64
+ | "none" // Keep input order
65
+ | "height-desc" // Sort by height descending (best packing)
66
+ | "ordered"; // Row 1 strict order, row 2+ flexible
67
+ ```
68
+
69
+ ## Sort Strategies
70
+
71
+ ### `none`
72
+ Items are placed in input order using the MaxRects algorithm. Good for when your input is already sorted (e.g., by date) and you want to preserve that order as much as possible.
73
+
74
+ ### `height-desc`
75
+ Items are sorted by height (tallest first) before packing. This typically produces the most compact layout with minimal wasted space.
76
+
77
+ ### `ordered`
78
+ A hybrid approach for galleries where order matters at the top but efficiency matters overall:
79
+ - **Row 1**: Items placed strictly left-to-right in input order
80
+ - **Row 2**: Next batch of items, can be reordered within the row to fill gaps
81
+ - **Row 3+**: Full algorithmic freedom for optimal packing
82
+
83
+ This is ideal for "newest items at top" layouts where the first row should show items 1, 2, 3... in order, but lower rows can be optimized.
84
+
85
+ ## Responsive Layouts
86
+
87
+ Omit `containerWidth` to enable responsive mode:
88
+
89
+ ```tsx
90
+ <div style={{ width: '100%' }}>
91
+ <HermitageLayout items={items} gap={8} />
92
+ </div>
93
+ ```
94
+
95
+ The component will measure its parent container and recalculate the layout when the container resizes.
96
+
97
+ ## Fixed Width
98
+
99
+ For fixed-width layouts, provide `containerWidth`:
100
+
101
+ ```tsx
102
+ <HermitageLayout
103
+ items={items}
104
+ containerWidth={800}
105
+ gap={8}
106
+ />
107
+ ```
108
+
109
+ ## Styling Items
110
+
111
+ Each item is rendered in an absolutely-positioned wrapper. Style your content to fill it:
112
+
113
+ ```tsx
114
+ const items = [
115
+ {
116
+ id: '1',
117
+ width: 200,
118
+ height: 150,
119
+ content: (
120
+ <img
121
+ src="..."
122
+ style={{ width: '100%', height: '100%', objectFit: 'cover' }}
123
+ />
124
+ ),
125
+ },
126
+ ];
127
+ ```
128
+
129
+ ## Animations
130
+
131
+ Petersburg intentionally doesn't include animations to stay lightweight. Add your own with CSS transitions:
132
+
133
+ ```css
134
+ .my-gallery img {
135
+ transition: transform 0.3s ease, opacity 0.3s ease;
136
+ }
137
+ ```
138
+
139
+ Or use your preferred animation library on the item content.
140
+
141
+ ## How It Works
142
+
143
+ Petersburg uses the **MaxRects** bin-packing algorithm:
144
+
145
+ 1. Start with the full container as free space
146
+ 2. For each item, find the best position (topmost, then leftmost)
147
+ 3. Place the item and split the remaining free space into new rectangles
148
+ 4. Prune redundant free rectangles
149
+ 5. Repeat until all items are placed
150
+
151
+ This produces efficient layouts where items fill gaps left by differently-sized neighbors.
152
+
153
+ ## License
154
+
155
+ MIT
@@ -0,0 +1,22 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+
4
+ interface LayoutItem {
5
+ id: string;
6
+ width: number;
7
+ height: number;
8
+ content: ReactNode;
9
+ }
10
+ type SortStrategy = "none" | "height-desc" | "ordered";
11
+ interface HermitageLayoutProps {
12
+ items: LayoutItem[];
13
+ /** Fixed width. If omitted, component auto-measures its container. */
14
+ containerWidth?: number;
15
+ gap?: number;
16
+ sortStrategy?: SortStrategy;
17
+ className?: string;
18
+ }
19
+
20
+ declare function HermitageLayout({ items, containerWidth: fixedWidth, gap, sortStrategy, className, }: HermitageLayoutProps): react_jsx_runtime.JSX.Element;
21
+
22
+ export { HermitageLayout, type HermitageLayoutProps, type LayoutItem, type SortStrategy };
@@ -0,0 +1,22 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+
4
+ interface LayoutItem {
5
+ id: string;
6
+ width: number;
7
+ height: number;
8
+ content: ReactNode;
9
+ }
10
+ type SortStrategy = "none" | "height-desc" | "ordered";
11
+ interface HermitageLayoutProps {
12
+ items: LayoutItem[];
13
+ /** Fixed width. If omitted, component auto-measures its container. */
14
+ containerWidth?: number;
15
+ gap?: number;
16
+ sortStrategy?: SortStrategy;
17
+ className?: string;
18
+ }
19
+
20
+ declare function HermitageLayout({ items, containerWidth: fixedWidth, gap, sortStrategy, className, }: HermitageLayoutProps): react_jsx_runtime.JSX.Element;
21
+
22
+ export { HermitageLayout, type HermitageLayoutProps, type LayoutItem, type SortStrategy };
package/dist/index.js ADDED
@@ -0,0 +1,363 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ HermitageLayout: () => HermitageLayout
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/HermitageLayout.tsx
28
+ var import_react = require("react");
29
+
30
+ // src/maxrects.ts
31
+ function calcMaxHeight(items, gap) {
32
+ const totalHeight = items.reduce((sum, item) => sum + item.height + gap, 0);
33
+ return Math.max(totalHeight, 1e3);
34
+ }
35
+ function sortItems(items, strategy) {
36
+ if (strategy === "none" || strategy === "ordered") {
37
+ return items;
38
+ }
39
+ const sorted = [...items];
40
+ switch (strategy) {
41
+ case "height-desc":
42
+ sorted.sort((a, b) => b.height - a.height);
43
+ break;
44
+ }
45
+ return sorted;
46
+ }
47
+ function pack(items, containerWidth, gap = 0, sortStrategy = "none") {
48
+ if (sortStrategy === "ordered") {
49
+ return packOrdered(items, containerWidth, gap);
50
+ }
51
+ const sortedItems = sortItems(items, sortStrategy);
52
+ const maxHeight = calcMaxHeight(sortedItems, gap);
53
+ const freeRects = [
54
+ { x: 0, y: 0, width: containerWidth, height: maxHeight }
55
+ ];
56
+ const placements = [];
57
+ let maxY = 0;
58
+ for (const item of sortedItems) {
59
+ const paddedWidth = item.width + gap;
60
+ const paddedHeight = item.height + gap;
61
+ const position = findBestPosition(paddedWidth, paddedHeight, freeRects);
62
+ if (position) {
63
+ const placement = {
64
+ id: item.id,
65
+ x: position.x,
66
+ y: position.y,
67
+ width: item.width,
68
+ height: item.height
69
+ };
70
+ placements.push(placement);
71
+ maxY = Math.max(maxY, position.y + item.height);
72
+ const placedRect = {
73
+ x: position.x,
74
+ y: position.y,
75
+ width: paddedWidth,
76
+ height: paddedHeight
77
+ };
78
+ splitFreeRects(freeRects, placedRect);
79
+ pruneFreeRects(freeRects);
80
+ }
81
+ }
82
+ return {
83
+ placements,
84
+ totalHeight: maxY
85
+ };
86
+ }
87
+ function packOrdered(items, containerWidth, gap) {
88
+ const maxHeight = calcMaxHeight(items, gap);
89
+ const freeRects = [
90
+ { x: 0, y: 0, width: containerWidth, height: maxHeight }
91
+ ];
92
+ const placements = [];
93
+ let maxY = 0;
94
+ let itemIndex = 0;
95
+ let row1NextX = 0;
96
+ const row1Count = countRow1Items(items, containerWidth, gap);
97
+ for (let i = 0; i < row1Count; i++) {
98
+ const item = items[itemIndex];
99
+ const paddedWidth = item.width + gap;
100
+ const paddedHeight = item.height + gap;
101
+ const placement = {
102
+ id: item.id,
103
+ x: row1NextX,
104
+ y: 0,
105
+ width: item.width,
106
+ height: item.height
107
+ };
108
+ placements.push(placement);
109
+ maxY = Math.max(maxY, item.height);
110
+ const placedRect = {
111
+ x: row1NextX,
112
+ y: 0,
113
+ width: paddedWidth,
114
+ height: paddedHeight
115
+ };
116
+ splitFreeRects(freeRects, placedRect);
117
+ pruneFreeRects(freeRects);
118
+ row1NextX += paddedWidth;
119
+ itemIndex++;
120
+ }
121
+ const remainingItems = items.slice(itemIndex);
122
+ if (remainingItems.length === 0) {
123
+ return { placements, totalHeight: maxY };
124
+ }
125
+ const row2CandidateCount = Math.min(row1Count, remainingItems.length);
126
+ const row2Candidates = remainingItems.slice(0, row2CandidateCount);
127
+ const row3Items = remainingItems.slice(row2CandidateCount);
128
+ const row2Sorted = [...row2Candidates].sort((a, b) => b.height - a.height);
129
+ for (const item of row2Sorted) {
130
+ const paddedWidth = item.width + gap;
131
+ const paddedHeight = item.height + gap;
132
+ const position = findBestPosition(paddedWidth, paddedHeight, freeRects);
133
+ if (position) {
134
+ const placement = {
135
+ id: item.id,
136
+ x: position.x,
137
+ y: position.y,
138
+ width: item.width,
139
+ height: item.height
140
+ };
141
+ placements.push(placement);
142
+ maxY = Math.max(maxY, position.y + item.height);
143
+ const placedRect = {
144
+ x: position.x,
145
+ y: position.y,
146
+ width: paddedWidth,
147
+ height: paddedHeight
148
+ };
149
+ splitFreeRects(freeRects, placedRect);
150
+ pruneFreeRects(freeRects);
151
+ }
152
+ }
153
+ const row3Sorted = [...row3Items].sort((a, b) => b.height - a.height);
154
+ for (const item of row3Sorted) {
155
+ const paddedWidth = item.width + gap;
156
+ const paddedHeight = item.height + gap;
157
+ const position = findBestPosition(paddedWidth, paddedHeight, freeRects);
158
+ if (position) {
159
+ const placement = {
160
+ id: item.id,
161
+ x: position.x,
162
+ y: position.y,
163
+ width: item.width,
164
+ height: item.height
165
+ };
166
+ placements.push(placement);
167
+ maxY = Math.max(maxY, position.y + item.height);
168
+ const placedRect = {
169
+ x: position.x,
170
+ y: position.y,
171
+ width: paddedWidth,
172
+ height: paddedHeight
173
+ };
174
+ splitFreeRects(freeRects, placedRect);
175
+ pruneFreeRects(freeRects);
176
+ }
177
+ }
178
+ return {
179
+ placements,
180
+ totalHeight: maxY
181
+ };
182
+ }
183
+ function countRow1Items(items, containerWidth, gap) {
184
+ let x = 0;
185
+ let count = 0;
186
+ for (const item of items) {
187
+ const paddedWidth = item.width + gap;
188
+ if (x + paddedWidth > containerWidth + gap) {
189
+ break;
190
+ }
191
+ x += paddedWidth;
192
+ count++;
193
+ }
194
+ return count;
195
+ }
196
+ function findBestPosition(width, height, freeRects) {
197
+ let bestY = Infinity;
198
+ let bestX = Infinity;
199
+ let bestPosition = null;
200
+ for (const rect of freeRects) {
201
+ if (width <= rect.width && height <= rect.height) {
202
+ const x = rect.x;
203
+ const y = rect.y;
204
+ if (y < bestY || y === bestY && x < bestX) {
205
+ bestY = y;
206
+ bestX = x;
207
+ bestPosition = { x, y };
208
+ }
209
+ }
210
+ }
211
+ return bestPosition;
212
+ }
213
+ function splitFreeRects(freeRects, placedRect) {
214
+ const newRects = [];
215
+ for (let i = freeRects.length - 1; i >= 0; i--) {
216
+ const freeRect = freeRects[i];
217
+ if (!rectsOverlap(freeRect, placedRect)) {
218
+ continue;
219
+ }
220
+ freeRects.splice(i, 1);
221
+ if (placedRect.x > freeRect.x) {
222
+ newRects.push({
223
+ x: freeRect.x,
224
+ y: freeRect.y,
225
+ width: placedRect.x - freeRect.x,
226
+ height: freeRect.height
227
+ });
228
+ }
229
+ const placedRight = placedRect.x + placedRect.width;
230
+ const freeRight = freeRect.x + freeRect.width;
231
+ if (placedRight < freeRight) {
232
+ newRects.push({
233
+ x: placedRight,
234
+ y: freeRect.y,
235
+ width: freeRight - placedRight,
236
+ height: freeRect.height
237
+ });
238
+ }
239
+ if (placedRect.y > freeRect.y) {
240
+ newRects.push({
241
+ x: freeRect.x,
242
+ y: freeRect.y,
243
+ width: freeRect.width,
244
+ height: placedRect.y - freeRect.y
245
+ });
246
+ }
247
+ const placedBottom = placedRect.y + placedRect.height;
248
+ const freeBottom = freeRect.y + freeRect.height;
249
+ if (placedBottom < freeBottom) {
250
+ newRects.push({
251
+ x: freeRect.x,
252
+ y: placedBottom,
253
+ width: freeRect.width,
254
+ height: freeBottom - placedBottom
255
+ });
256
+ }
257
+ }
258
+ freeRects.push(...newRects);
259
+ }
260
+ function pruneFreeRects(freeRects) {
261
+ for (let i = freeRects.length - 1; i >= 0; i--) {
262
+ for (let j = 0; j < freeRects.length; j++) {
263
+ if (i === j) continue;
264
+ if (rectContains(freeRects[j], freeRects[i])) {
265
+ freeRects.splice(i, 1);
266
+ break;
267
+ }
268
+ }
269
+ }
270
+ }
271
+ function rectsOverlap(a, b) {
272
+ return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
273
+ }
274
+ function rectContains(outer, inner) {
275
+ return inner.x >= outer.x && inner.y >= outer.y && inner.x + inner.width <= outer.x + outer.width && inner.y + inner.height <= outer.y + outer.height;
276
+ }
277
+
278
+ // src/HermitageLayout.tsx
279
+ var import_jsx_runtime = require("react/jsx-runtime");
280
+ function HermitageLayout({
281
+ items,
282
+ containerWidth: fixedWidth,
283
+ gap = 8,
284
+ sortStrategy = "none",
285
+ className
286
+ }) {
287
+ const containerRef = (0, import_react.useRef)(null);
288
+ const [measuredWidth, setMeasuredWidth] = (0, import_react.useState)(0);
289
+ const containerWidth = fixedWidth ?? measuredWidth;
290
+ (0, import_react.useEffect)(() => {
291
+ if (fixedWidth !== void 0) {
292
+ return;
293
+ }
294
+ const element = containerRef.current;
295
+ if (!element) return;
296
+ const observer = new ResizeObserver((entries) => {
297
+ for (const entry of entries) {
298
+ const width = entry.contentRect.width;
299
+ setMeasuredWidth(width);
300
+ }
301
+ });
302
+ observer.observe(element);
303
+ setMeasuredWidth(element.getBoundingClientRect().width);
304
+ return () => observer.disconnect();
305
+ }, [fixedWidth]);
306
+ const { placements, totalHeight } = (0, import_react.useMemo)(() => {
307
+ if (containerWidth === 0) {
308
+ return { placements: [], totalHeight: 0 };
309
+ }
310
+ return pack(items, containerWidth, gap, sortStrategy);
311
+ }, [items, containerWidth, gap, sortStrategy]);
312
+ const placementMap = (0, import_react.useMemo)(() => {
313
+ const map = /* @__PURE__ */ new Map();
314
+ for (const p of placements) {
315
+ map.set(p.id, { x: p.x, y: p.y });
316
+ }
317
+ return map;
318
+ }, [placements]);
319
+ const sortedItems = (0, import_react.useMemo)(() => {
320
+ return [...items].sort((a, b) => {
321
+ const posA = placementMap.get(a.id);
322
+ const posB = placementMap.get(b.id);
323
+ if (!posA || !posB) return 0;
324
+ if (posA.y !== posB.y) return posA.y - posB.y;
325
+ return posA.x - posB.x;
326
+ });
327
+ }, [items, placementMap]);
328
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
329
+ "div",
330
+ {
331
+ ref: containerRef,
332
+ className,
333
+ style: {
334
+ position: "relative",
335
+ width: fixedWidth ?? "100%",
336
+ height: totalHeight || void 0
337
+ },
338
+ children: sortedItems.map((item) => {
339
+ const position = placementMap.get(item.id);
340
+ if (!position) return null;
341
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
342
+ "div",
343
+ {
344
+ style: {
345
+ position: "absolute",
346
+ left: position.x,
347
+ top: position.y,
348
+ width: item.width,
349
+ height: item.height
350
+ },
351
+ children: item.content
352
+ },
353
+ item.id
354
+ );
355
+ })
356
+ }
357
+ );
358
+ }
359
+ // Annotate the CommonJS export names for ESM import in node:
360
+ 0 && (module.exports = {
361
+ HermitageLayout
362
+ });
363
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/HermitageLayout.tsx","../src/maxrects.ts"],"sourcesContent":["export { HermitageLayout } from \"./HermitageLayout\";\nexport type { HermitageLayoutProps, LayoutItem, SortStrategy } from \"./types\";\n","import { useMemo, useRef, useState, useEffect } from \"react\";\nimport { HermitageLayoutProps } from \"./types\";\nimport { pack } from \"./maxrects\";\n\nexport function HermitageLayout({\n items,\n containerWidth: fixedWidth,\n gap = 8,\n sortStrategy = \"none\",\n className,\n}: HermitageLayoutProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [measuredWidth, setMeasuredWidth] = useState<number>(0);\n\n // Use fixed width if provided, otherwise use measured width\n const containerWidth = fixedWidth ?? measuredWidth;\n\n // Measure container width using ResizeObserver\n useEffect(() => {\n if (fixedWidth !== undefined) {\n // Fixed width provided, no need to measure\n return;\n }\n\n const element = containerRef.current;\n if (!element) return;\n\n const observer = new ResizeObserver((entries) => {\n for (const entry of entries) {\n const width = entry.contentRect.width;\n setMeasuredWidth(width);\n }\n });\n\n observer.observe(element);\n\n // Initial measurement\n setMeasuredWidth(element.getBoundingClientRect().width);\n\n return () => observer.disconnect();\n }, [fixedWidth]);\n\n const { placements, totalHeight } = useMemo(() => {\n if (containerWidth === 0) {\n // Not yet measured, return empty layout\n return { placements: [], totalHeight: 0 };\n }\n return pack(items, containerWidth, gap, sortStrategy);\n }, [items, containerWidth, gap, sortStrategy]);\n\n // Create a map for quick lookup of placements by id\n const placementMap = useMemo(() => {\n const map = new Map<string, { x: number; y: number }>();\n for (const p of placements) {\n map.set(p.id, { x: p.x, y: p.y });\n }\n return map;\n }, [placements]);\n\n // Sort items by visual position (y, then x) for proper tab order\n const sortedItems = useMemo(() => {\n return [...items].sort((a, b) => {\n const posA = placementMap.get(a.id);\n const posB = placementMap.get(b.id);\n if (!posA || !posB) return 0;\n if (posA.y !== posB.y) return posA.y - posB.y;\n return posA.x - posB.x;\n });\n }, [items, placementMap]);\n\n return (\n <div\n ref={containerRef}\n className={className}\n style={{\n position: \"relative\",\n width: fixedWidth ?? \"100%\",\n height: totalHeight || undefined,\n }}\n >\n {sortedItems.map((item) => {\n const position = placementMap.get(item.id);\n if (!position) return null;\n\n return (\n <div\n key={item.id}\n style={{\n position: \"absolute\",\n left: position.x,\n top: position.y,\n width: item.width,\n height: item.height,\n }}\n >\n {item.content}\n </div>\n );\n })}\n </div>\n );\n}\n","import { Rect, PlacedItem, PackResult, SortStrategy } from \"./types\";\n\ninterface PackInput {\n id: string;\n width: number;\n height: number;\n}\n\n/**\n * Calculate a safe maximum height for the packing area.\n * Uses sum of all item heights as worst-case (vertical stack).\n */\nfunction calcMaxHeight(items: PackInput[], gap: number): number {\n const totalHeight = items.reduce((sum, item) => sum + item.height + gap, 0);\n return Math.max(totalHeight, 1000); // At least 1000px\n}\n\nfunction sortItems(items: PackInput[], strategy: SortStrategy): PackInput[] {\n if (strategy === \"none\" || strategy === \"ordered\") {\n // \"ordered\" uses a different placement approach, not pre-sorting\n return items;\n }\n\n const sorted = [...items];\n\n switch (strategy) {\n case \"height-desc\":\n sorted.sort((a, b) => b.height - a.height);\n break;\n }\n\n return sorted;\n}\n\n/**\n * MaxRects bin-packing algorithm.\n * Places rectangles in a container, minimizing wasted space.\n */\nexport function pack(\n items: PackInput[],\n containerWidth: number,\n gap: number = 0,\n sortStrategy: SortStrategy = \"none\"\n): PackResult {\n if (sortStrategy === \"ordered\") {\n return packOrdered(items, containerWidth, gap);\n }\n\n const sortedItems = sortItems(items, sortStrategy);\n\n // Start with one large free rectangle\n const maxHeight = calcMaxHeight(sortedItems, gap);\n const freeRects: Rect[] = [\n { x: 0, y: 0, width: containerWidth, height: maxHeight },\n ];\n\n const placements: PlacedItem[] = [];\n let maxY = 0;\n\n for (const item of sortedItems) {\n // Account for gap in item dimensions during placement\n const paddedWidth = item.width + gap;\n const paddedHeight = item.height + gap;\n\n const position = findBestPosition(paddedWidth, paddedHeight, freeRects);\n\n if (position) {\n const placement: PlacedItem = {\n id: item.id,\n x: position.x,\n y: position.y,\n width: item.width,\n height: item.height,\n };\n placements.push(placement);\n\n // Track the maximum Y extent\n maxY = Math.max(maxY, position.y + item.height);\n\n // Split free rects around the placed item (using padded dimensions)\n const placedRect: Rect = {\n x: position.x,\n y: position.y,\n width: paddedWidth,\n height: paddedHeight,\n };\n splitFreeRects(freeRects, placedRect);\n pruneFreeRects(freeRects);\n }\n }\n\n return {\n placements,\n totalHeight: maxY,\n };\n}\n\n/**\n * Ordered packing strategy:\n * - Row 1 (y=0): Strict input order, left-to-right, no gap filling\n * - Row 2: Next batch of items (in input order), can be reordered for better fit\n * - Row 3+: Full algorithmic freedom (height-desc sorting)\n */\nfunction packOrdered(\n items: PackInput[],\n containerWidth: number,\n gap: number\n): PackResult {\n // Start with full container as free space\n const maxHeight = calcMaxHeight(items, gap);\n const freeRects: Rect[] = [\n { x: 0, y: 0, width: containerWidth, height: maxHeight },\n ];\n\n const placements: PlacedItem[] = [];\n let maxY = 0;\n let itemIndex = 0;\n\n // --- Row 1: Strict left-to-right order at y=0 ---\n let row1NextX = 0;\n const row1Count = countRow1Items(items, containerWidth, gap);\n\n for (let i = 0; i < row1Count; i++) {\n const item = items[itemIndex];\n const paddedWidth = item.width + gap;\n const paddedHeight = item.height + gap;\n\n const placement: PlacedItem = {\n id: item.id,\n x: row1NextX,\n y: 0,\n width: item.width,\n height: item.height,\n };\n placements.push(placement);\n\n maxY = Math.max(maxY, item.height);\n\n // Update freeRects to account for this placement\n const placedRect: Rect = {\n x: row1NextX,\n y: 0,\n width: paddedWidth,\n height: paddedHeight,\n };\n splitFreeRects(freeRects, placedRect);\n pruneFreeRects(freeRects);\n\n row1NextX += paddedWidth;\n itemIndex++;\n }\n\n // Remaining items after row 1\n const remainingItems = items.slice(itemIndex);\n\n if (remainingItems.length === 0) {\n return { placements, totalHeight: maxY };\n }\n\n // --- Row 2: Next batch of items, reorderable for better fit ---\n // Take roughly the same count as row 1 (or remaining, whichever is smaller)\n const row2CandidateCount = Math.min(row1Count, remainingItems.length);\n const row2Candidates = remainingItems.slice(0, row2CandidateCount);\n const row3Items = remainingItems.slice(row2CandidateCount);\n\n // Sort row 2 candidates by height-desc for better gap filling\n const row2Sorted = [...row2Candidates].sort((a, b) => b.height - a.height);\n\n for (const item of row2Sorted) {\n const paddedWidth = item.width + gap;\n const paddedHeight = item.height + gap;\n\n const position = findBestPosition(paddedWidth, paddedHeight, freeRects);\n\n if (position) {\n const placement: PlacedItem = {\n id: item.id,\n x: position.x,\n y: position.y,\n width: item.width,\n height: item.height,\n };\n placements.push(placement);\n\n maxY = Math.max(maxY, position.y + item.height);\n\n const placedRect: Rect = {\n x: position.x,\n y: position.y,\n width: paddedWidth,\n height: paddedHeight,\n };\n splitFreeRects(freeRects, placedRect);\n pruneFreeRects(freeRects);\n }\n }\n\n // --- Row 3+: Full freedom, height-desc sorting ---\n const row3Sorted = [...row3Items].sort((a, b) => b.height - a.height);\n\n for (const item of row3Sorted) {\n const paddedWidth = item.width + gap;\n const paddedHeight = item.height + gap;\n\n const position = findBestPosition(paddedWidth, paddedHeight, freeRects);\n\n if (position) {\n const placement: PlacedItem = {\n id: item.id,\n x: position.x,\n y: position.y,\n width: item.width,\n height: item.height,\n };\n placements.push(placement);\n\n maxY = Math.max(maxY, position.y + item.height);\n\n const placedRect: Rect = {\n x: position.x,\n y: position.y,\n width: paddedWidth,\n height: paddedHeight,\n };\n splitFreeRects(freeRects, placedRect);\n pruneFreeRects(freeRects);\n }\n }\n\n return {\n placements,\n totalHeight: maxY,\n };\n}\n\n/**\n * Count how many items fit in row 1 (strict left-to-right at y=0).\n */\nfunction countRow1Items(\n items: PackInput[],\n containerWidth: number,\n gap: number\n): number {\n let x = 0;\n let count = 0;\n\n for (const item of items) {\n const paddedWidth = item.width + gap;\n if (x + paddedWidth > containerWidth + gap) {\n break;\n }\n x += paddedWidth;\n count++;\n }\n\n return count;\n}\n\n/**\n * Find a position for an item, but only accept positions at a specific y.\n */\nfunction findPositionAtY(\n width: number,\n height: number,\n freeRects: Rect[],\n targetY: number\n): { x: number; y: number } | null {\n let bestX = Infinity;\n let bestPosition: { x: number; y: number } | null = null;\n\n for (const rect of freeRects) {\n // Only consider rects that start at targetY\n if (rect.y !== targetY) continue;\n\n if (width <= rect.width && height <= rect.height) {\n const x = rect.x;\n if (x < bestX) {\n bestX = x;\n bestPosition = { x, y: targetY };\n }\n }\n }\n\n return bestPosition;\n}\n\n/**\n * Get the maximum height among a list of items.\n */\nfunction getMaxHeight(items: PackInput[]): number {\n return items.reduce((max, item) => Math.max(max, item.height), 0);\n}\n\n/**\n * Find the best position for an item using \"Best Y then Best X\" heuristic.\n * Prefers positions higher up (smaller Y), then leftward (smaller X).\n */\nfunction findBestPosition(\n width: number,\n height: number,\n freeRects: Rect[]\n): { x: number; y: number } | null {\n let bestY = Infinity;\n let bestX = Infinity;\n let bestPosition: { x: number; y: number } | null = null;\n\n for (const rect of freeRects) {\n // Check if item fits in this free rect\n if (width <= rect.width && height <= rect.height) {\n // Position at top-left corner of free rect\n const x = rect.x;\n const y = rect.y;\n\n // Prefer smaller Y, then smaller X\n if (y < bestY || (y === bestY && x < bestX)) {\n bestY = y;\n bestX = x;\n bestPosition = { x, y };\n }\n }\n }\n\n return bestPosition;\n}\n\n/**\n * Split free rectangles that overlap with the placed rectangle.\n * Generates up to 4 new rectangles around the placed item.\n */\nfunction splitFreeRects(freeRects: Rect[], placedRect: Rect): void {\n const newRects: Rect[] = [];\n\n for (let i = freeRects.length - 1; i >= 0; i--) {\n const freeRect = freeRects[i];\n\n // Check if this free rect overlaps with placed rect\n if (!rectsOverlap(freeRect, placedRect)) {\n continue;\n }\n\n // Remove the overlapping free rect\n freeRects.splice(i, 1);\n\n // Generate new free rects from the non-overlapping portions\n\n // Left portion\n if (placedRect.x > freeRect.x) {\n newRects.push({\n x: freeRect.x,\n y: freeRect.y,\n width: placedRect.x - freeRect.x,\n height: freeRect.height,\n });\n }\n\n // Right portion\n const placedRight = placedRect.x + placedRect.width;\n const freeRight = freeRect.x + freeRect.width;\n if (placedRight < freeRight) {\n newRects.push({\n x: placedRight,\n y: freeRect.y,\n width: freeRight - placedRight,\n height: freeRect.height,\n });\n }\n\n // Top portion\n if (placedRect.y > freeRect.y) {\n newRects.push({\n x: freeRect.x,\n y: freeRect.y,\n width: freeRect.width,\n height: placedRect.y - freeRect.y,\n });\n }\n\n // Bottom portion\n const placedBottom = placedRect.y + placedRect.height;\n const freeBottom = freeRect.y + freeRect.height;\n if (placedBottom < freeBottom) {\n newRects.push({\n x: freeRect.x,\n y: placedBottom,\n width: freeRect.width,\n height: freeBottom - placedBottom,\n });\n }\n }\n\n // Add all new rects\n freeRects.push(...newRects);\n}\n\n/**\n * Remove free rectangles that are fully contained within other free rectangles.\n */\nfunction pruneFreeRects(freeRects: Rect[]): void {\n for (let i = freeRects.length - 1; i >= 0; i--) {\n for (let j = 0; j < freeRects.length; j++) {\n if (i === j) continue;\n\n if (rectContains(freeRects[j], freeRects[i])) {\n freeRects.splice(i, 1);\n break;\n }\n }\n }\n}\n\n/**\n * Check if two rectangles overlap.\n */\nfunction rectsOverlap(a: Rect, b: Rect): boolean {\n return (\n a.x < b.x + b.width &&\n a.x + a.width > b.x &&\n a.y < b.y + b.height &&\n a.y + a.height > b.y\n );\n}\n\n/**\n * Check if rectangle `outer` fully contains rectangle `inner`.\n */\nfunction rectContains(outer: Rect, inner: Rect): boolean {\n return (\n inner.x >= outer.x &&\n inner.y >= outer.y &&\n inner.x + inner.width <= outer.x + outer.width &&\n inner.y + inner.height <= outer.y + outer.height\n );\n}\n"],"mappings":"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","names":[]}
package/dist/index.mjs ADDED
@@ -0,0 +1,336 @@
1
+ // src/HermitageLayout.tsx
2
+ import { useMemo, useRef, useState, useEffect } from "react";
3
+
4
+ // src/maxrects.ts
5
+ function calcMaxHeight(items, gap) {
6
+ const totalHeight = items.reduce((sum, item) => sum + item.height + gap, 0);
7
+ return Math.max(totalHeight, 1e3);
8
+ }
9
+ function sortItems(items, strategy) {
10
+ if (strategy === "none" || strategy === "ordered") {
11
+ return items;
12
+ }
13
+ const sorted = [...items];
14
+ switch (strategy) {
15
+ case "height-desc":
16
+ sorted.sort((a, b) => b.height - a.height);
17
+ break;
18
+ }
19
+ return sorted;
20
+ }
21
+ function pack(items, containerWidth, gap = 0, sortStrategy = "none") {
22
+ if (sortStrategy === "ordered") {
23
+ return packOrdered(items, containerWidth, gap);
24
+ }
25
+ const sortedItems = sortItems(items, sortStrategy);
26
+ const maxHeight = calcMaxHeight(sortedItems, gap);
27
+ const freeRects = [
28
+ { x: 0, y: 0, width: containerWidth, height: maxHeight }
29
+ ];
30
+ const placements = [];
31
+ let maxY = 0;
32
+ for (const item of sortedItems) {
33
+ const paddedWidth = item.width + gap;
34
+ const paddedHeight = item.height + gap;
35
+ const position = findBestPosition(paddedWidth, paddedHeight, freeRects);
36
+ if (position) {
37
+ const placement = {
38
+ id: item.id,
39
+ x: position.x,
40
+ y: position.y,
41
+ width: item.width,
42
+ height: item.height
43
+ };
44
+ placements.push(placement);
45
+ maxY = Math.max(maxY, position.y + item.height);
46
+ const placedRect = {
47
+ x: position.x,
48
+ y: position.y,
49
+ width: paddedWidth,
50
+ height: paddedHeight
51
+ };
52
+ splitFreeRects(freeRects, placedRect);
53
+ pruneFreeRects(freeRects);
54
+ }
55
+ }
56
+ return {
57
+ placements,
58
+ totalHeight: maxY
59
+ };
60
+ }
61
+ function packOrdered(items, containerWidth, gap) {
62
+ const maxHeight = calcMaxHeight(items, gap);
63
+ const freeRects = [
64
+ { x: 0, y: 0, width: containerWidth, height: maxHeight }
65
+ ];
66
+ const placements = [];
67
+ let maxY = 0;
68
+ let itemIndex = 0;
69
+ let row1NextX = 0;
70
+ const row1Count = countRow1Items(items, containerWidth, gap);
71
+ for (let i = 0; i < row1Count; i++) {
72
+ const item = items[itemIndex];
73
+ const paddedWidth = item.width + gap;
74
+ const paddedHeight = item.height + gap;
75
+ const placement = {
76
+ id: item.id,
77
+ x: row1NextX,
78
+ y: 0,
79
+ width: item.width,
80
+ height: item.height
81
+ };
82
+ placements.push(placement);
83
+ maxY = Math.max(maxY, item.height);
84
+ const placedRect = {
85
+ x: row1NextX,
86
+ y: 0,
87
+ width: paddedWidth,
88
+ height: paddedHeight
89
+ };
90
+ splitFreeRects(freeRects, placedRect);
91
+ pruneFreeRects(freeRects);
92
+ row1NextX += paddedWidth;
93
+ itemIndex++;
94
+ }
95
+ const remainingItems = items.slice(itemIndex);
96
+ if (remainingItems.length === 0) {
97
+ return { placements, totalHeight: maxY };
98
+ }
99
+ const row2CandidateCount = Math.min(row1Count, remainingItems.length);
100
+ const row2Candidates = remainingItems.slice(0, row2CandidateCount);
101
+ const row3Items = remainingItems.slice(row2CandidateCount);
102
+ const row2Sorted = [...row2Candidates].sort((a, b) => b.height - a.height);
103
+ for (const item of row2Sorted) {
104
+ const paddedWidth = item.width + gap;
105
+ const paddedHeight = item.height + gap;
106
+ const position = findBestPosition(paddedWidth, paddedHeight, freeRects);
107
+ if (position) {
108
+ const placement = {
109
+ id: item.id,
110
+ x: position.x,
111
+ y: position.y,
112
+ width: item.width,
113
+ height: item.height
114
+ };
115
+ placements.push(placement);
116
+ maxY = Math.max(maxY, position.y + item.height);
117
+ const placedRect = {
118
+ x: position.x,
119
+ y: position.y,
120
+ width: paddedWidth,
121
+ height: paddedHeight
122
+ };
123
+ splitFreeRects(freeRects, placedRect);
124
+ pruneFreeRects(freeRects);
125
+ }
126
+ }
127
+ const row3Sorted = [...row3Items].sort((a, b) => b.height - a.height);
128
+ for (const item of row3Sorted) {
129
+ const paddedWidth = item.width + gap;
130
+ const paddedHeight = item.height + gap;
131
+ const position = findBestPosition(paddedWidth, paddedHeight, freeRects);
132
+ if (position) {
133
+ const placement = {
134
+ id: item.id,
135
+ x: position.x,
136
+ y: position.y,
137
+ width: item.width,
138
+ height: item.height
139
+ };
140
+ placements.push(placement);
141
+ maxY = Math.max(maxY, position.y + item.height);
142
+ const placedRect = {
143
+ x: position.x,
144
+ y: position.y,
145
+ width: paddedWidth,
146
+ height: paddedHeight
147
+ };
148
+ splitFreeRects(freeRects, placedRect);
149
+ pruneFreeRects(freeRects);
150
+ }
151
+ }
152
+ return {
153
+ placements,
154
+ totalHeight: maxY
155
+ };
156
+ }
157
+ function countRow1Items(items, containerWidth, gap) {
158
+ let x = 0;
159
+ let count = 0;
160
+ for (const item of items) {
161
+ const paddedWidth = item.width + gap;
162
+ if (x + paddedWidth > containerWidth + gap) {
163
+ break;
164
+ }
165
+ x += paddedWidth;
166
+ count++;
167
+ }
168
+ return count;
169
+ }
170
+ function findBestPosition(width, height, freeRects) {
171
+ let bestY = Infinity;
172
+ let bestX = Infinity;
173
+ let bestPosition = null;
174
+ for (const rect of freeRects) {
175
+ if (width <= rect.width && height <= rect.height) {
176
+ const x = rect.x;
177
+ const y = rect.y;
178
+ if (y < bestY || y === bestY && x < bestX) {
179
+ bestY = y;
180
+ bestX = x;
181
+ bestPosition = { x, y };
182
+ }
183
+ }
184
+ }
185
+ return bestPosition;
186
+ }
187
+ function splitFreeRects(freeRects, placedRect) {
188
+ const newRects = [];
189
+ for (let i = freeRects.length - 1; i >= 0; i--) {
190
+ const freeRect = freeRects[i];
191
+ if (!rectsOverlap(freeRect, placedRect)) {
192
+ continue;
193
+ }
194
+ freeRects.splice(i, 1);
195
+ if (placedRect.x > freeRect.x) {
196
+ newRects.push({
197
+ x: freeRect.x,
198
+ y: freeRect.y,
199
+ width: placedRect.x - freeRect.x,
200
+ height: freeRect.height
201
+ });
202
+ }
203
+ const placedRight = placedRect.x + placedRect.width;
204
+ const freeRight = freeRect.x + freeRect.width;
205
+ if (placedRight < freeRight) {
206
+ newRects.push({
207
+ x: placedRight,
208
+ y: freeRect.y,
209
+ width: freeRight - placedRight,
210
+ height: freeRect.height
211
+ });
212
+ }
213
+ if (placedRect.y > freeRect.y) {
214
+ newRects.push({
215
+ x: freeRect.x,
216
+ y: freeRect.y,
217
+ width: freeRect.width,
218
+ height: placedRect.y - freeRect.y
219
+ });
220
+ }
221
+ const placedBottom = placedRect.y + placedRect.height;
222
+ const freeBottom = freeRect.y + freeRect.height;
223
+ if (placedBottom < freeBottom) {
224
+ newRects.push({
225
+ x: freeRect.x,
226
+ y: placedBottom,
227
+ width: freeRect.width,
228
+ height: freeBottom - placedBottom
229
+ });
230
+ }
231
+ }
232
+ freeRects.push(...newRects);
233
+ }
234
+ function pruneFreeRects(freeRects) {
235
+ for (let i = freeRects.length - 1; i >= 0; i--) {
236
+ for (let j = 0; j < freeRects.length; j++) {
237
+ if (i === j) continue;
238
+ if (rectContains(freeRects[j], freeRects[i])) {
239
+ freeRects.splice(i, 1);
240
+ break;
241
+ }
242
+ }
243
+ }
244
+ }
245
+ function rectsOverlap(a, b) {
246
+ return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
247
+ }
248
+ function rectContains(outer, inner) {
249
+ return inner.x >= outer.x && inner.y >= outer.y && inner.x + inner.width <= outer.x + outer.width && inner.y + inner.height <= outer.y + outer.height;
250
+ }
251
+
252
+ // src/HermitageLayout.tsx
253
+ import { jsx } from "react/jsx-runtime";
254
+ function HermitageLayout({
255
+ items,
256
+ containerWidth: fixedWidth,
257
+ gap = 8,
258
+ sortStrategy = "none",
259
+ className
260
+ }) {
261
+ const containerRef = useRef(null);
262
+ const [measuredWidth, setMeasuredWidth] = useState(0);
263
+ const containerWidth = fixedWidth ?? measuredWidth;
264
+ useEffect(() => {
265
+ if (fixedWidth !== void 0) {
266
+ return;
267
+ }
268
+ const element = containerRef.current;
269
+ if (!element) return;
270
+ const observer = new ResizeObserver((entries) => {
271
+ for (const entry of entries) {
272
+ const width = entry.contentRect.width;
273
+ setMeasuredWidth(width);
274
+ }
275
+ });
276
+ observer.observe(element);
277
+ setMeasuredWidth(element.getBoundingClientRect().width);
278
+ return () => observer.disconnect();
279
+ }, [fixedWidth]);
280
+ const { placements, totalHeight } = useMemo(() => {
281
+ if (containerWidth === 0) {
282
+ return { placements: [], totalHeight: 0 };
283
+ }
284
+ return pack(items, containerWidth, gap, sortStrategy);
285
+ }, [items, containerWidth, gap, sortStrategy]);
286
+ const placementMap = useMemo(() => {
287
+ const map = /* @__PURE__ */ new Map();
288
+ for (const p of placements) {
289
+ map.set(p.id, { x: p.x, y: p.y });
290
+ }
291
+ return map;
292
+ }, [placements]);
293
+ const sortedItems = useMemo(() => {
294
+ return [...items].sort((a, b) => {
295
+ const posA = placementMap.get(a.id);
296
+ const posB = placementMap.get(b.id);
297
+ if (!posA || !posB) return 0;
298
+ if (posA.y !== posB.y) return posA.y - posB.y;
299
+ return posA.x - posB.x;
300
+ });
301
+ }, [items, placementMap]);
302
+ return /* @__PURE__ */ jsx(
303
+ "div",
304
+ {
305
+ ref: containerRef,
306
+ className,
307
+ style: {
308
+ position: "relative",
309
+ width: fixedWidth ?? "100%",
310
+ height: totalHeight || void 0
311
+ },
312
+ children: sortedItems.map((item) => {
313
+ const position = placementMap.get(item.id);
314
+ if (!position) return null;
315
+ return /* @__PURE__ */ jsx(
316
+ "div",
317
+ {
318
+ style: {
319
+ position: "absolute",
320
+ left: position.x,
321
+ top: position.y,
322
+ width: item.width,
323
+ height: item.height
324
+ },
325
+ children: item.content
326
+ },
327
+ item.id
328
+ );
329
+ })
330
+ }
331
+ );
332
+ }
333
+ export {
334
+ HermitageLayout
335
+ };
336
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/HermitageLayout.tsx","../src/maxrects.ts"],"sourcesContent":["import { useMemo, useRef, useState, useEffect } from \"react\";\nimport { HermitageLayoutProps } from \"./types\";\nimport { pack } from \"./maxrects\";\n\nexport function HermitageLayout({\n items,\n containerWidth: fixedWidth,\n gap = 8,\n sortStrategy = \"none\",\n className,\n}: HermitageLayoutProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [measuredWidth, setMeasuredWidth] = useState<number>(0);\n\n // Use fixed width if provided, otherwise use measured width\n const containerWidth = fixedWidth ?? measuredWidth;\n\n // Measure container width using ResizeObserver\n useEffect(() => {\n if (fixedWidth !== undefined) {\n // Fixed width provided, no need to measure\n return;\n }\n\n const element = containerRef.current;\n if (!element) return;\n\n const observer = new ResizeObserver((entries) => {\n for (const entry of entries) {\n const width = entry.contentRect.width;\n setMeasuredWidth(width);\n }\n });\n\n observer.observe(element);\n\n // Initial measurement\n setMeasuredWidth(element.getBoundingClientRect().width);\n\n return () => observer.disconnect();\n }, [fixedWidth]);\n\n const { placements, totalHeight } = useMemo(() => {\n if (containerWidth === 0) {\n // Not yet measured, return empty layout\n return { placements: [], totalHeight: 0 };\n }\n return pack(items, containerWidth, gap, sortStrategy);\n }, [items, containerWidth, gap, sortStrategy]);\n\n // Create a map for quick lookup of placements by id\n const placementMap = useMemo(() => {\n const map = new Map<string, { x: number; y: number }>();\n for (const p of placements) {\n map.set(p.id, { x: p.x, y: p.y });\n }\n return map;\n }, [placements]);\n\n // Sort items by visual position (y, then x) for proper tab order\n const sortedItems = useMemo(() => {\n return [...items].sort((a, b) => {\n const posA = placementMap.get(a.id);\n const posB = placementMap.get(b.id);\n if (!posA || !posB) return 0;\n if (posA.y !== posB.y) return posA.y - posB.y;\n return posA.x - posB.x;\n });\n }, [items, placementMap]);\n\n return (\n <div\n ref={containerRef}\n className={className}\n style={{\n position: \"relative\",\n width: fixedWidth ?? \"100%\",\n height: totalHeight || undefined,\n }}\n >\n {sortedItems.map((item) => {\n const position = placementMap.get(item.id);\n if (!position) return null;\n\n return (\n <div\n key={item.id}\n style={{\n position: \"absolute\",\n left: position.x,\n top: position.y,\n width: item.width,\n height: item.height,\n }}\n >\n {item.content}\n </div>\n );\n })}\n </div>\n );\n}\n","import { Rect, PlacedItem, PackResult, SortStrategy } from \"./types\";\n\ninterface PackInput {\n id: string;\n width: number;\n height: number;\n}\n\n/**\n * Calculate a safe maximum height for the packing area.\n * Uses sum of all item heights as worst-case (vertical stack).\n */\nfunction calcMaxHeight(items: PackInput[], gap: number): number {\n const totalHeight = items.reduce((sum, item) => sum + item.height + gap, 0);\n return Math.max(totalHeight, 1000); // At least 1000px\n}\n\nfunction sortItems(items: PackInput[], strategy: SortStrategy): PackInput[] {\n if (strategy === \"none\" || strategy === \"ordered\") {\n // \"ordered\" uses a different placement approach, not pre-sorting\n return items;\n }\n\n const sorted = [...items];\n\n switch (strategy) {\n case \"height-desc\":\n sorted.sort((a, b) => b.height - a.height);\n break;\n }\n\n return sorted;\n}\n\n/**\n * MaxRects bin-packing algorithm.\n * Places rectangles in a container, minimizing wasted space.\n */\nexport function pack(\n items: PackInput[],\n containerWidth: number,\n gap: number = 0,\n sortStrategy: SortStrategy = \"none\"\n): PackResult {\n if (sortStrategy === \"ordered\") {\n return packOrdered(items, containerWidth, gap);\n }\n\n const sortedItems = sortItems(items, sortStrategy);\n\n // Start with one large free rectangle\n const maxHeight = calcMaxHeight(sortedItems, gap);\n const freeRects: Rect[] = [\n { x: 0, y: 0, width: containerWidth, height: maxHeight },\n ];\n\n const placements: PlacedItem[] = [];\n let maxY = 0;\n\n for (const item of sortedItems) {\n // Account for gap in item dimensions during placement\n const paddedWidth = item.width + gap;\n const paddedHeight = item.height + gap;\n\n const position = findBestPosition(paddedWidth, paddedHeight, freeRects);\n\n if (position) {\n const placement: PlacedItem = {\n id: item.id,\n x: position.x,\n y: position.y,\n width: item.width,\n height: item.height,\n };\n placements.push(placement);\n\n // Track the maximum Y extent\n maxY = Math.max(maxY, position.y + item.height);\n\n // Split free rects around the placed item (using padded dimensions)\n const placedRect: Rect = {\n x: position.x,\n y: position.y,\n width: paddedWidth,\n height: paddedHeight,\n };\n splitFreeRects(freeRects, placedRect);\n pruneFreeRects(freeRects);\n }\n }\n\n return {\n placements,\n totalHeight: maxY,\n };\n}\n\n/**\n * Ordered packing strategy:\n * - Row 1 (y=0): Strict input order, left-to-right, no gap filling\n * - Row 2: Next batch of items (in input order), can be reordered for better fit\n * - Row 3+: Full algorithmic freedom (height-desc sorting)\n */\nfunction packOrdered(\n items: PackInput[],\n containerWidth: number,\n gap: number\n): PackResult {\n // Start with full container as free space\n const maxHeight = calcMaxHeight(items, gap);\n const freeRects: Rect[] = [\n { x: 0, y: 0, width: containerWidth, height: maxHeight },\n ];\n\n const placements: PlacedItem[] = [];\n let maxY = 0;\n let itemIndex = 0;\n\n // --- Row 1: Strict left-to-right order at y=0 ---\n let row1NextX = 0;\n const row1Count = countRow1Items(items, containerWidth, gap);\n\n for (let i = 0; i < row1Count; i++) {\n const item = items[itemIndex];\n const paddedWidth = item.width + gap;\n const paddedHeight = item.height + gap;\n\n const placement: PlacedItem = {\n id: item.id,\n x: row1NextX,\n y: 0,\n width: item.width,\n height: item.height,\n };\n placements.push(placement);\n\n maxY = Math.max(maxY, item.height);\n\n // Update freeRects to account for this placement\n const placedRect: Rect = {\n x: row1NextX,\n y: 0,\n width: paddedWidth,\n height: paddedHeight,\n };\n splitFreeRects(freeRects, placedRect);\n pruneFreeRects(freeRects);\n\n row1NextX += paddedWidth;\n itemIndex++;\n }\n\n // Remaining items after row 1\n const remainingItems = items.slice(itemIndex);\n\n if (remainingItems.length === 0) {\n return { placements, totalHeight: maxY };\n }\n\n // --- Row 2: Next batch of items, reorderable for better fit ---\n // Take roughly the same count as row 1 (or remaining, whichever is smaller)\n const row2CandidateCount = Math.min(row1Count, remainingItems.length);\n const row2Candidates = remainingItems.slice(0, row2CandidateCount);\n const row3Items = remainingItems.slice(row2CandidateCount);\n\n // Sort row 2 candidates by height-desc for better gap filling\n const row2Sorted = [...row2Candidates].sort((a, b) => b.height - a.height);\n\n for (const item of row2Sorted) {\n const paddedWidth = item.width + gap;\n const paddedHeight = item.height + gap;\n\n const position = findBestPosition(paddedWidth, paddedHeight, freeRects);\n\n if (position) {\n const placement: PlacedItem = {\n id: item.id,\n x: position.x,\n y: position.y,\n width: item.width,\n height: item.height,\n };\n placements.push(placement);\n\n maxY = Math.max(maxY, position.y + item.height);\n\n const placedRect: Rect = {\n x: position.x,\n y: position.y,\n width: paddedWidth,\n height: paddedHeight,\n };\n splitFreeRects(freeRects, placedRect);\n pruneFreeRects(freeRects);\n }\n }\n\n // --- Row 3+: Full freedom, height-desc sorting ---\n const row3Sorted = [...row3Items].sort((a, b) => b.height - a.height);\n\n for (const item of row3Sorted) {\n const paddedWidth = item.width + gap;\n const paddedHeight = item.height + gap;\n\n const position = findBestPosition(paddedWidth, paddedHeight, freeRects);\n\n if (position) {\n const placement: PlacedItem = {\n id: item.id,\n x: position.x,\n y: position.y,\n width: item.width,\n height: item.height,\n };\n placements.push(placement);\n\n maxY = Math.max(maxY, position.y + item.height);\n\n const placedRect: Rect = {\n x: position.x,\n y: position.y,\n width: paddedWidth,\n height: paddedHeight,\n };\n splitFreeRects(freeRects, placedRect);\n pruneFreeRects(freeRects);\n }\n }\n\n return {\n placements,\n totalHeight: maxY,\n };\n}\n\n/**\n * Count how many items fit in row 1 (strict left-to-right at y=0).\n */\nfunction countRow1Items(\n items: PackInput[],\n containerWidth: number,\n gap: number\n): number {\n let x = 0;\n let count = 0;\n\n for (const item of items) {\n const paddedWidth = item.width + gap;\n if (x + paddedWidth > containerWidth + gap) {\n break;\n }\n x += paddedWidth;\n count++;\n }\n\n return count;\n}\n\n/**\n * Find a position for an item, but only accept positions at a specific y.\n */\nfunction findPositionAtY(\n width: number,\n height: number,\n freeRects: Rect[],\n targetY: number\n): { x: number; y: number } | null {\n let bestX = Infinity;\n let bestPosition: { x: number; y: number } | null = null;\n\n for (const rect of freeRects) {\n // Only consider rects that start at targetY\n if (rect.y !== targetY) continue;\n\n if (width <= rect.width && height <= rect.height) {\n const x = rect.x;\n if (x < bestX) {\n bestX = x;\n bestPosition = { x, y: targetY };\n }\n }\n }\n\n return bestPosition;\n}\n\n/**\n * Get the maximum height among a list of items.\n */\nfunction getMaxHeight(items: PackInput[]): number {\n return items.reduce((max, item) => Math.max(max, item.height), 0);\n}\n\n/**\n * Find the best position for an item using \"Best Y then Best X\" heuristic.\n * Prefers positions higher up (smaller Y), then leftward (smaller X).\n */\nfunction findBestPosition(\n width: number,\n height: number,\n freeRects: Rect[]\n): { x: number; y: number } | null {\n let bestY = Infinity;\n let bestX = Infinity;\n let bestPosition: { x: number; y: number } | null = null;\n\n for (const rect of freeRects) {\n // Check if item fits in this free rect\n if (width <= rect.width && height <= rect.height) {\n // Position at top-left corner of free rect\n const x = rect.x;\n const y = rect.y;\n\n // Prefer smaller Y, then smaller X\n if (y < bestY || (y === bestY && x < bestX)) {\n bestY = y;\n bestX = x;\n bestPosition = { x, y };\n }\n }\n }\n\n return bestPosition;\n}\n\n/**\n * Split free rectangles that overlap with the placed rectangle.\n * Generates up to 4 new rectangles around the placed item.\n */\nfunction splitFreeRects(freeRects: Rect[], placedRect: Rect): void {\n const newRects: Rect[] = [];\n\n for (let i = freeRects.length - 1; i >= 0; i--) {\n const freeRect = freeRects[i];\n\n // Check if this free rect overlaps with placed rect\n if (!rectsOverlap(freeRect, placedRect)) {\n continue;\n }\n\n // Remove the overlapping free rect\n freeRects.splice(i, 1);\n\n // Generate new free rects from the non-overlapping portions\n\n // Left portion\n if (placedRect.x > freeRect.x) {\n newRects.push({\n x: freeRect.x,\n y: freeRect.y,\n width: placedRect.x - freeRect.x,\n height: freeRect.height,\n });\n }\n\n // Right portion\n const placedRight = placedRect.x + placedRect.width;\n const freeRight = freeRect.x + freeRect.width;\n if (placedRight < freeRight) {\n newRects.push({\n x: placedRight,\n y: freeRect.y,\n width: freeRight - placedRight,\n height: freeRect.height,\n });\n }\n\n // Top portion\n if (placedRect.y > freeRect.y) {\n newRects.push({\n x: freeRect.x,\n y: freeRect.y,\n width: freeRect.width,\n height: placedRect.y - freeRect.y,\n });\n }\n\n // Bottom portion\n const placedBottom = placedRect.y + placedRect.height;\n const freeBottom = freeRect.y + freeRect.height;\n if (placedBottom < freeBottom) {\n newRects.push({\n x: freeRect.x,\n y: placedBottom,\n width: freeRect.width,\n height: freeBottom - placedBottom,\n });\n }\n }\n\n // Add all new rects\n freeRects.push(...newRects);\n}\n\n/**\n * Remove free rectangles that are fully contained within other free rectangles.\n */\nfunction pruneFreeRects(freeRects: Rect[]): void {\n for (let i = freeRects.length - 1; i >= 0; i--) {\n for (let j = 0; j < freeRects.length; j++) {\n if (i === j) continue;\n\n if (rectContains(freeRects[j], freeRects[i])) {\n freeRects.splice(i, 1);\n break;\n }\n }\n }\n}\n\n/**\n * Check if two rectangles overlap.\n */\nfunction rectsOverlap(a: Rect, b: Rect): boolean {\n return (\n a.x < b.x + b.width &&\n a.x + a.width > b.x &&\n a.y < b.y + b.height &&\n a.y + a.height > b.y\n );\n}\n\n/**\n * Check if rectangle `outer` fully contains rectangle `inner`.\n */\nfunction rectContains(outer: Rect, inner: Rect): boolean {\n return (\n inner.x >= outer.x &&\n inner.y >= outer.y &&\n inner.x + inner.width <= outer.x + outer.width &&\n inner.y + inner.height <= outer.y + outer.height\n );\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "petersburg",
3
+ "version": "0.0.1",
4
+ "description": "React layout library implementing Hermitage-style 2D bin-packing for picture arrangement",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup",
20
+ "dev": "tsup --watch",
21
+ "lint": "eslint src",
22
+ "typecheck": "tsc --noEmit",
23
+ "prepublishOnly": "npm run build"
24
+ },
25
+ "keywords": [
26
+ "react",
27
+ "layout",
28
+ "bin-packing",
29
+ "masonry",
30
+ "gallery",
31
+ "grid",
32
+ "hermitage"
33
+ ],
34
+ "author": "xk7200",
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/xfaSts9cwY6VqLNTMAtR/Petersburg.git"
39
+ },
40
+ "peerDependencies": {
41
+ "react": ">=17.0.0",
42
+ "react-dom": ">=17.0.0"
43
+ },
44
+ "devDependencies": {
45
+ "@types/react": "^18.2.0",
46
+ "@types/react-dom": "^18.2.0",
47
+ "react": "^18.2.0",
48
+ "react-dom": "^18.2.0",
49
+ "tsup": "^8.0.0",
50
+ "typescript": "^5.3.0"
51
+ }
52
+ }