petersburg 0.0.1 → 0.0.3
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 +23 -1
- package/dist/{index.mjs → index.cjs} +118 -30
- package/dist/index.cjs.map +1 -0
- package/dist/{index.d.mts → index.d.cts} +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +90 -56
- package/dist/index.js.map +1 -1
- package/package.json +17 -6
- package/dist/index.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@ Named after the Hermitage Museum in St. Petersburg, where curators arrange paint
|
|
|
8
8
|
|
|
9
9
|
- **MaxRects bin-packing algorithm** — efficient 2D rectangle packing
|
|
10
10
|
- **Multiple sort strategies** — optimize for packing efficiency or preserve input order
|
|
11
|
+
- **Auto-height measurement** — items can omit height; Petersburg measures rendered content
|
|
11
12
|
- **Responsive** — auto-measures container and recalculates on resize
|
|
12
13
|
- **Accessible** — DOM order matches visual flow for proper tab navigation
|
|
13
14
|
- **Lightweight** — no dependencies beyond React
|
|
@@ -56,7 +57,7 @@ function Gallery() {
|
|
|
56
57
|
interface LayoutItem {
|
|
57
58
|
id: string;
|
|
58
59
|
width: number;
|
|
59
|
-
height
|
|
60
|
+
height?: number; // Optional — if omitted, auto-measured from content
|
|
60
61
|
content: ReactNode;
|
|
61
62
|
}
|
|
62
63
|
|
|
@@ -82,6 +83,27 @@ A hybrid approach for galleries where order matters at the top but efficiency ma
|
|
|
82
83
|
|
|
83
84
|
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
|
|
|
86
|
+
## Auto-Height Mode
|
|
87
|
+
|
|
88
|
+
For items with dynamic content (text cards, variable-length descriptions), omit the `height` property:
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
const items = [
|
|
92
|
+
{ id: '1', width: 200, content: <Card title="Short" /> },
|
|
93
|
+
{ id: '2', width: 200, content: <Card title="Much Longer Title Here" /> },
|
|
94
|
+
{ id: '3', width: 150, content: <Card title="Medium" description="With extra text" /> },
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
<HermitageLayout items={items} gap={8} />
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Petersburg will:
|
|
101
|
+
1. Render items invisibly to measure their natural height
|
|
102
|
+
2. Pack using the measured dimensions
|
|
103
|
+
3. Display the final layout
|
|
104
|
+
|
|
105
|
+
This adds a brief measurement phase but avoids content overflow or fixed-height constraints.
|
|
106
|
+
|
|
85
107
|
## Responsive Layouts
|
|
86
108
|
|
|
87
109
|
Omit `containerWidth` to enable responsive mode:
|
|
@@ -1,5 +1,31 @@
|
|
|
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
|
+
|
|
1
27
|
// src/HermitageLayout.tsx
|
|
2
|
-
|
|
28
|
+
var import_react = require("react");
|
|
3
29
|
|
|
4
30
|
// src/maxrects.ts
|
|
5
31
|
function calcMaxHeight(items, gap) {
|
|
@@ -250,7 +276,7 @@ function rectContains(outer, inner) {
|
|
|
250
276
|
}
|
|
251
277
|
|
|
252
278
|
// src/HermitageLayout.tsx
|
|
253
|
-
|
|
279
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
254
280
|
function HermitageLayout({
|
|
255
281
|
items,
|
|
256
282
|
containerWidth: fixedWidth,
|
|
@@ -258,10 +284,18 @@ function HermitageLayout({
|
|
|
258
284
|
sortStrategy = "none",
|
|
259
285
|
className
|
|
260
286
|
}) {
|
|
261
|
-
const containerRef = useRef(null);
|
|
262
|
-
const
|
|
287
|
+
const containerRef = (0, import_react.useRef)(null);
|
|
288
|
+
const measureRef = (0, import_react.useRef)(null);
|
|
289
|
+
const [measuredWidth, setMeasuredWidth] = (0, import_react.useState)(0);
|
|
290
|
+
const [measuredHeights, setMeasuredHeights] = (0, import_react.useState)(
|
|
291
|
+
/* @__PURE__ */ new Map()
|
|
292
|
+
);
|
|
263
293
|
const containerWidth = fixedWidth ?? measuredWidth;
|
|
264
|
-
|
|
294
|
+
const itemsNeedingMeasure = (0, import_react.useMemo)(() => {
|
|
295
|
+
return items.filter((item) => item.height === void 0);
|
|
296
|
+
}, [items]);
|
|
297
|
+
const needsMeasurement = itemsNeedingMeasure.length > 0;
|
|
298
|
+
(0, import_react.useEffect)(() => {
|
|
265
299
|
if (fixedWidth !== void 0) {
|
|
266
300
|
return;
|
|
267
301
|
}
|
|
@@ -277,20 +311,48 @@ function HermitageLayout({
|
|
|
277
311
|
setMeasuredWidth(element.getBoundingClientRect().width);
|
|
278
312
|
return () => observer.disconnect();
|
|
279
313
|
}, [fixedWidth]);
|
|
280
|
-
|
|
281
|
-
if (containerWidth === 0) {
|
|
314
|
+
(0, import_react.useEffect)(() => {
|
|
315
|
+
if (!needsMeasurement || containerWidth === 0) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
requestAnimationFrame(() => {
|
|
319
|
+
const measureContainer = measureRef.current;
|
|
320
|
+
if (!measureContainer) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const newHeights = /* @__PURE__ */ new Map();
|
|
324
|
+
for (const item of itemsNeedingMeasure) {
|
|
325
|
+
const element = measureContainer.querySelector(
|
|
326
|
+
`[data-measure-id="${item.id}"]`
|
|
327
|
+
);
|
|
328
|
+
if (element) {
|
|
329
|
+
newHeights.set(item.id, element.getBoundingClientRect().height);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
setMeasuredHeights(newHeights);
|
|
333
|
+
});
|
|
334
|
+
}, [itemsNeedingMeasure, needsMeasurement, containerWidth]);
|
|
335
|
+
const resolvedItems = (0, import_react.useMemo)(() => {
|
|
336
|
+
return items.map((item) => ({
|
|
337
|
+
...item,
|
|
338
|
+
height: item.height ?? measuredHeights.get(item.id) ?? 0
|
|
339
|
+
}));
|
|
340
|
+
}, [items, measuredHeights]);
|
|
341
|
+
const allHeightsResolved = resolvedItems.every((item) => item.height > 0);
|
|
342
|
+
const { placements, totalHeight } = (0, import_react.useMemo)(() => {
|
|
343
|
+
if (containerWidth === 0 || !allHeightsResolved) {
|
|
282
344
|
return { placements: [], totalHeight: 0 };
|
|
283
345
|
}
|
|
284
|
-
return pack(
|
|
285
|
-
}, [
|
|
286
|
-
const placementMap = useMemo(() => {
|
|
346
|
+
return pack(resolvedItems, containerWidth, gap, sortStrategy);
|
|
347
|
+
}, [resolvedItems, containerWidth, gap, sortStrategy, allHeightsResolved]);
|
|
348
|
+
const placementMap = (0, import_react.useMemo)(() => {
|
|
287
349
|
const map = /* @__PURE__ */ new Map();
|
|
288
350
|
for (const p of placements) {
|
|
289
|
-
map.set(p.id, { x: p.x, y: p.y });
|
|
351
|
+
map.set(p.id, { x: p.x, y: p.y, height: p.height });
|
|
290
352
|
}
|
|
291
353
|
return map;
|
|
292
354
|
}, [placements]);
|
|
293
|
-
const sortedItems = useMemo(() => {
|
|
355
|
+
const sortedItems = (0, import_react.useMemo)(() => {
|
|
294
356
|
return [...items].sort((a, b) => {
|
|
295
357
|
const posA = placementMap.get(a.id);
|
|
296
358
|
const posB = placementMap.get(b.id);
|
|
@@ -299,7 +361,8 @@ function HermitageLayout({
|
|
|
299
361
|
return posA.x - posB.x;
|
|
300
362
|
});
|
|
301
363
|
}, [items, placementMap]);
|
|
302
|
-
|
|
364
|
+
const showLayout = allHeightsResolved && containerWidth > 0;
|
|
365
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
303
366
|
"div",
|
|
304
367
|
{
|
|
305
368
|
ref: containerRef,
|
|
@@ -307,30 +370,55 @@ function HermitageLayout({
|
|
|
307
370
|
style: {
|
|
308
371
|
position: "relative",
|
|
309
372
|
width: fixedWidth ?? "100%",
|
|
310
|
-
height: totalHeight || void 0
|
|
373
|
+
height: totalHeight || void 0,
|
|
374
|
+
overflow: "hidden"
|
|
311
375
|
},
|
|
312
|
-
children:
|
|
313
|
-
|
|
314
|
-
if (!position) return null;
|
|
315
|
-
return /* @__PURE__ */ jsx(
|
|
376
|
+
children: [
|
|
377
|
+
needsMeasurement && containerWidth > 0 && !allHeightsResolved && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
316
378
|
"div",
|
|
317
379
|
{
|
|
380
|
+
ref: measureRef,
|
|
381
|
+
"aria-hidden": "true",
|
|
318
382
|
style: {
|
|
319
383
|
position: "absolute",
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
width: item.width,
|
|
323
|
-
height: item.height
|
|
384
|
+
visibility: "hidden",
|
|
385
|
+
pointerEvents: "none"
|
|
324
386
|
},
|
|
325
|
-
children: item.
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
387
|
+
children: itemsNeedingMeasure.map((item) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
388
|
+
"div",
|
|
389
|
+
{
|
|
390
|
+
"data-measure-id": item.id,
|
|
391
|
+
style: { width: item.width },
|
|
392
|
+
children: item.content
|
|
393
|
+
},
|
|
394
|
+
item.id
|
|
395
|
+
))
|
|
396
|
+
}
|
|
397
|
+
),
|
|
398
|
+
showLayout && sortedItems.map((item) => {
|
|
399
|
+
const position = placementMap.get(item.id);
|
|
400
|
+
if (!position) return null;
|
|
401
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
402
|
+
"div",
|
|
403
|
+
{
|
|
404
|
+
style: {
|
|
405
|
+
position: "absolute",
|
|
406
|
+
left: position.x,
|
|
407
|
+
top: position.y,
|
|
408
|
+
width: item.width,
|
|
409
|
+
height: position.height
|
|
410
|
+
},
|
|
411
|
+
children: item.content
|
|
412
|
+
},
|
|
413
|
+
item.id
|
|
414
|
+
);
|
|
415
|
+
})
|
|
416
|
+
]
|
|
330
417
|
}
|
|
331
418
|
);
|
|
332
419
|
}
|
|
333
|
-
export
|
|
420
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
421
|
+
0 && (module.exports = {
|
|
334
422
|
HermitageLayout
|
|
335
|
-
};
|
|
336
|
-
//# sourceMappingURL=index.
|
|
423
|
+
});
|
|
424
|
+
//# sourceMappingURL=index.cjs.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 measureRef = useRef<HTMLDivElement>(null);\n const [measuredWidth, setMeasuredWidth] = useState<number>(0);\n const [measuredHeights, setMeasuredHeights] = useState<Map<string, number>>(\n new Map()\n );\n \n // Use fixed width if provided, otherwise use measured width\n const containerWidth = fixedWidth ?? measuredWidth;\n\n // Check which items need height measurement\n const itemsNeedingMeasure = useMemo(() => {\n return items.filter((item) => item.height === undefined);\n }, [items]);\n\n const needsMeasurement = itemsNeedingMeasure.length > 0;\n\n // Measure container width using ResizeObserver\n useEffect(() => {\n if (fixedWidth !== undefined) {\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 setMeasuredWidth(element.getBoundingClientRect().width);\n\n return () => observer.disconnect();\n }, [fixedWidth]);\n\n // Measure item heights when needed\n useEffect(() => {\n if (!needsMeasurement || containerWidth === 0) {\n return;\n }\n\n // Wait for next frame to ensure measurement elements are rendered\n requestAnimationFrame(() => {\n const measureContainer = measureRef.current;\n if (!measureContainer) {\n return;\n }\n\n const newHeights = new Map<string, number>();\n\n for (const item of itemsNeedingMeasure) {\n const element = measureContainer.querySelector(\n `[data-measure-id=\"${item.id}\"]`\n );\n if (element) {\n newHeights.set(item.id, element.getBoundingClientRect().height);\n }\n }\n\n setMeasuredHeights(newHeights);\n });\n }, [itemsNeedingMeasure, needsMeasurement, containerWidth]);\n\n // Build items with resolved heights\n const resolvedItems = useMemo(() => {\n return items.map((item) => ({\n ...item,\n height: item.height ?? measuredHeights.get(item.id) ?? 0,\n }));\n }, [items, measuredHeights]);\n\n // Check if we have all heights resolved\n const allHeightsResolved = resolvedItems.every((item) => item.height > 0);\n\n const { placements, totalHeight } = useMemo(() => {\n if (containerWidth === 0 || !allHeightsResolved) {\n return { placements: [], totalHeight: 0 };\n }\n return pack(resolvedItems, containerWidth, gap, sortStrategy);\n }, [resolvedItems, containerWidth, gap, sortStrategy, allHeightsResolved]);\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; height: number }>();\n for (const p of placements) {\n map.set(p.id, { x: p.x, y: p.y, height: p.height });\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 // Show layout only when all measurements are complete\n const showLayout = allHeightsResolved && containerWidth > 0;\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 overflow: \"hidden\",\n }}\n >\n {/* Hidden measurement container for items without explicit height */}\n {needsMeasurement && containerWidth > 0 && !allHeightsResolved && (\n <div\n ref={measureRef}\n aria-hidden=\"true\"\n style={{\n position: \"absolute\",\n visibility: \"hidden\",\n pointerEvents: \"none\",\n }}\n >\n {itemsNeedingMeasure.map((item) => (\n <div\n key={item.id}\n data-measure-id={item.id}\n style={{ width: item.width }}\n >\n {item.content}\n </div>\n ))}\n </div>\n )}\n\n {/* Main layout */}\n {showLayout &&\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: position.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":[]}
|
|
@@ -4,7 +4,8 @@ import { ReactNode } from 'react';
|
|
|
4
4
|
interface LayoutItem {
|
|
5
5
|
id: string;
|
|
6
6
|
width: number;
|
|
7
|
-
height
|
|
7
|
+
/** If omitted, height is auto-measured from rendered content */
|
|
8
|
+
height?: number;
|
|
8
9
|
content: ReactNode;
|
|
9
10
|
}
|
|
10
11
|
type SortStrategy = "none" | "height-desc" | "ordered";
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,8 @@ import { ReactNode } from 'react';
|
|
|
4
4
|
interface LayoutItem {
|
|
5
5
|
id: string;
|
|
6
6
|
width: number;
|
|
7
|
-
height
|
|
7
|
+
/** If omitted, height is auto-measured from rendered content */
|
|
8
|
+
height?: number;
|
|
8
9
|
content: ReactNode;
|
|
9
10
|
}
|
|
10
11
|
type SortStrategy = "none" | "height-desc" | "ordered";
|
package/dist/index.js
CHANGED
|
@@ -1,31 +1,5 @@
|
|
|
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
1
|
// src/HermitageLayout.tsx
|
|
28
|
-
|
|
2
|
+
import { useMemo, useRef, useState, useEffect } from "react";
|
|
29
3
|
|
|
30
4
|
// src/maxrects.ts
|
|
31
5
|
function calcMaxHeight(items, gap) {
|
|
@@ -276,7 +250,7 @@ function rectContains(outer, inner) {
|
|
|
276
250
|
}
|
|
277
251
|
|
|
278
252
|
// src/HermitageLayout.tsx
|
|
279
|
-
|
|
253
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
280
254
|
function HermitageLayout({
|
|
281
255
|
items,
|
|
282
256
|
containerWidth: fixedWidth,
|
|
@@ -284,10 +258,18 @@ function HermitageLayout({
|
|
|
284
258
|
sortStrategy = "none",
|
|
285
259
|
className
|
|
286
260
|
}) {
|
|
287
|
-
const containerRef =
|
|
288
|
-
const
|
|
261
|
+
const containerRef = useRef(null);
|
|
262
|
+
const measureRef = useRef(null);
|
|
263
|
+
const [measuredWidth, setMeasuredWidth] = useState(0);
|
|
264
|
+
const [measuredHeights, setMeasuredHeights] = useState(
|
|
265
|
+
/* @__PURE__ */ new Map()
|
|
266
|
+
);
|
|
289
267
|
const containerWidth = fixedWidth ?? measuredWidth;
|
|
290
|
-
|
|
268
|
+
const itemsNeedingMeasure = useMemo(() => {
|
|
269
|
+
return items.filter((item) => item.height === void 0);
|
|
270
|
+
}, [items]);
|
|
271
|
+
const needsMeasurement = itemsNeedingMeasure.length > 0;
|
|
272
|
+
useEffect(() => {
|
|
291
273
|
if (fixedWidth !== void 0) {
|
|
292
274
|
return;
|
|
293
275
|
}
|
|
@@ -303,20 +285,48 @@ function HermitageLayout({
|
|
|
303
285
|
setMeasuredWidth(element.getBoundingClientRect().width);
|
|
304
286
|
return () => observer.disconnect();
|
|
305
287
|
}, [fixedWidth]);
|
|
306
|
-
|
|
307
|
-
if (containerWidth === 0) {
|
|
288
|
+
useEffect(() => {
|
|
289
|
+
if (!needsMeasurement || containerWidth === 0) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
requestAnimationFrame(() => {
|
|
293
|
+
const measureContainer = measureRef.current;
|
|
294
|
+
if (!measureContainer) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const newHeights = /* @__PURE__ */ new Map();
|
|
298
|
+
for (const item of itemsNeedingMeasure) {
|
|
299
|
+
const element = measureContainer.querySelector(
|
|
300
|
+
`[data-measure-id="${item.id}"]`
|
|
301
|
+
);
|
|
302
|
+
if (element) {
|
|
303
|
+
newHeights.set(item.id, element.getBoundingClientRect().height);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
setMeasuredHeights(newHeights);
|
|
307
|
+
});
|
|
308
|
+
}, [itemsNeedingMeasure, needsMeasurement, containerWidth]);
|
|
309
|
+
const resolvedItems = useMemo(() => {
|
|
310
|
+
return items.map((item) => ({
|
|
311
|
+
...item,
|
|
312
|
+
height: item.height ?? measuredHeights.get(item.id) ?? 0
|
|
313
|
+
}));
|
|
314
|
+
}, [items, measuredHeights]);
|
|
315
|
+
const allHeightsResolved = resolvedItems.every((item) => item.height > 0);
|
|
316
|
+
const { placements, totalHeight } = useMemo(() => {
|
|
317
|
+
if (containerWidth === 0 || !allHeightsResolved) {
|
|
308
318
|
return { placements: [], totalHeight: 0 };
|
|
309
319
|
}
|
|
310
|
-
return pack(
|
|
311
|
-
}, [
|
|
312
|
-
const placementMap =
|
|
320
|
+
return pack(resolvedItems, containerWidth, gap, sortStrategy);
|
|
321
|
+
}, [resolvedItems, containerWidth, gap, sortStrategy, allHeightsResolved]);
|
|
322
|
+
const placementMap = useMemo(() => {
|
|
313
323
|
const map = /* @__PURE__ */ new Map();
|
|
314
324
|
for (const p of placements) {
|
|
315
|
-
map.set(p.id, { x: p.x, y: p.y });
|
|
325
|
+
map.set(p.id, { x: p.x, y: p.y, height: p.height });
|
|
316
326
|
}
|
|
317
327
|
return map;
|
|
318
328
|
}, [placements]);
|
|
319
|
-
const sortedItems =
|
|
329
|
+
const sortedItems = useMemo(() => {
|
|
320
330
|
return [...items].sort((a, b) => {
|
|
321
331
|
const posA = placementMap.get(a.id);
|
|
322
332
|
const posB = placementMap.get(b.id);
|
|
@@ -325,7 +335,8 @@ function HermitageLayout({
|
|
|
325
335
|
return posA.x - posB.x;
|
|
326
336
|
});
|
|
327
337
|
}, [items, placementMap]);
|
|
328
|
-
|
|
338
|
+
const showLayout = allHeightsResolved && containerWidth > 0;
|
|
339
|
+
return /* @__PURE__ */ jsxs(
|
|
329
340
|
"div",
|
|
330
341
|
{
|
|
331
342
|
ref: containerRef,
|
|
@@ -333,31 +344,54 @@ function HermitageLayout({
|
|
|
333
344
|
style: {
|
|
334
345
|
position: "relative",
|
|
335
346
|
width: fixedWidth ?? "100%",
|
|
336
|
-
height: totalHeight || void 0
|
|
347
|
+
height: totalHeight || void 0,
|
|
348
|
+
overflow: "hidden"
|
|
337
349
|
},
|
|
338
|
-
children:
|
|
339
|
-
|
|
340
|
-
if (!position) return null;
|
|
341
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
350
|
+
children: [
|
|
351
|
+
needsMeasurement && containerWidth > 0 && !allHeightsResolved && /* @__PURE__ */ jsx(
|
|
342
352
|
"div",
|
|
343
353
|
{
|
|
354
|
+
ref: measureRef,
|
|
355
|
+
"aria-hidden": "true",
|
|
344
356
|
style: {
|
|
345
357
|
position: "absolute",
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
width: item.width,
|
|
349
|
-
height: item.height
|
|
358
|
+
visibility: "hidden",
|
|
359
|
+
pointerEvents: "none"
|
|
350
360
|
},
|
|
351
|
-
children: item
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
361
|
+
children: itemsNeedingMeasure.map((item) => /* @__PURE__ */ jsx(
|
|
362
|
+
"div",
|
|
363
|
+
{
|
|
364
|
+
"data-measure-id": item.id,
|
|
365
|
+
style: { width: item.width },
|
|
366
|
+
children: item.content
|
|
367
|
+
},
|
|
368
|
+
item.id
|
|
369
|
+
))
|
|
370
|
+
}
|
|
371
|
+
),
|
|
372
|
+
showLayout && sortedItems.map((item) => {
|
|
373
|
+
const position = placementMap.get(item.id);
|
|
374
|
+
if (!position) return null;
|
|
375
|
+
return /* @__PURE__ */ jsx(
|
|
376
|
+
"div",
|
|
377
|
+
{
|
|
378
|
+
style: {
|
|
379
|
+
position: "absolute",
|
|
380
|
+
left: position.x,
|
|
381
|
+
top: position.y,
|
|
382
|
+
width: item.width,
|
|
383
|
+
height: position.height
|
|
384
|
+
},
|
|
385
|
+
children: item.content
|
|
386
|
+
},
|
|
387
|
+
item.id
|
|
388
|
+
);
|
|
389
|
+
})
|
|
390
|
+
]
|
|
356
391
|
}
|
|
357
392
|
);
|
|
358
393
|
}
|
|
359
|
-
|
|
360
|
-
0 && (module.exports = {
|
|
394
|
+
export {
|
|
361
395
|
HermitageLayout
|
|
362
|
-
}
|
|
396
|
+
};
|
|
363
397
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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":[]}
|
|
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 measureRef = useRef<HTMLDivElement>(null);\n const [measuredWidth, setMeasuredWidth] = useState<number>(0);\n const [measuredHeights, setMeasuredHeights] = useState<Map<string, number>>(\n new Map()\n );\n \n // Use fixed width if provided, otherwise use measured width\n const containerWidth = fixedWidth ?? measuredWidth;\n\n // Check which items need height measurement\n const itemsNeedingMeasure = useMemo(() => {\n return items.filter((item) => item.height === undefined);\n }, [items]);\n\n const needsMeasurement = itemsNeedingMeasure.length > 0;\n\n // Measure container width using ResizeObserver\n useEffect(() => {\n if (fixedWidth !== undefined) {\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 setMeasuredWidth(element.getBoundingClientRect().width);\n\n return () => observer.disconnect();\n }, [fixedWidth]);\n\n // Measure item heights when needed\n useEffect(() => {\n if (!needsMeasurement || containerWidth === 0) {\n return;\n }\n\n // Wait for next frame to ensure measurement elements are rendered\n requestAnimationFrame(() => {\n const measureContainer = measureRef.current;\n if (!measureContainer) {\n return;\n }\n\n const newHeights = new Map<string, number>();\n\n for (const item of itemsNeedingMeasure) {\n const element = measureContainer.querySelector(\n `[data-measure-id=\"${item.id}\"]`\n );\n if (element) {\n newHeights.set(item.id, element.getBoundingClientRect().height);\n }\n }\n\n setMeasuredHeights(newHeights);\n });\n }, [itemsNeedingMeasure, needsMeasurement, containerWidth]);\n\n // Build items with resolved heights\n const resolvedItems = useMemo(() => {\n return items.map((item) => ({\n ...item,\n height: item.height ?? measuredHeights.get(item.id) ?? 0,\n }));\n }, [items, measuredHeights]);\n\n // Check if we have all heights resolved\n const allHeightsResolved = resolvedItems.every((item) => item.height > 0);\n\n const { placements, totalHeight } = useMemo(() => {\n if (containerWidth === 0 || !allHeightsResolved) {\n return { placements: [], totalHeight: 0 };\n }\n return pack(resolvedItems, containerWidth, gap, sortStrategy);\n }, [resolvedItems, containerWidth, gap, sortStrategy, allHeightsResolved]);\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; height: number }>();\n for (const p of placements) {\n map.set(p.id, { x: p.x, y: p.y, height: p.height });\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 // Show layout only when all measurements are complete\n const showLayout = allHeightsResolved && containerWidth > 0;\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 overflow: \"hidden\",\n }}\n >\n {/* Hidden measurement container for items without explicit height */}\n {needsMeasurement && containerWidth > 0 && !allHeightsResolved && (\n <div\n ref={measureRef}\n aria-hidden=\"true\"\n style={{\n position: \"absolute\",\n visibility: \"hidden\",\n pointerEvents: \"none\",\n }}\n >\n {itemsNeedingMeasure.map((item) => (\n <div\n key={item.id}\n data-measure-id={item.id}\n style={{ width: item.width }}\n >\n {item.content}\n </div>\n ))}\n </div>\n )}\n\n {/* Main layout */}\n {showLayout &&\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: position.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
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "petersburg",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"description": "React layout library implementing Hermitage-style 2D bin-packing for picture arrangement",
|
|
5
|
-
"main": "./dist/index.
|
|
6
|
-
"module": "./dist/index.
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
7
8
|
"types": "./dist/index.d.ts",
|
|
8
9
|
"exports": {
|
|
9
10
|
".": {
|
|
10
11
|
"types": "./dist/index.d.ts",
|
|
11
|
-
"import": "./dist/index.
|
|
12
|
-
"require": "./dist/index.
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
13
14
|
}
|
|
14
15
|
},
|
|
15
16
|
"files": [
|
|
@@ -18,6 +19,8 @@
|
|
|
18
19
|
"scripts": {
|
|
19
20
|
"build": "tsup",
|
|
20
21
|
"dev": "tsup --watch",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"test:watch": "vitest",
|
|
21
24
|
"lint": "eslint src",
|
|
22
25
|
"typecheck": "tsc --noEmit",
|
|
23
26
|
"prepublishOnly": "npm run build"
|
|
@@ -42,11 +45,19 @@
|
|
|
42
45
|
"react-dom": ">=17.0.0"
|
|
43
46
|
},
|
|
44
47
|
"devDependencies": {
|
|
48
|
+
"@eslint/js": "^9.39.2",
|
|
49
|
+
"@testing-library/dom": "^10.4.1",
|
|
50
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
51
|
+
"@testing-library/react": "^16.3.1",
|
|
45
52
|
"@types/react": "^18.2.0",
|
|
46
53
|
"@types/react-dom": "^18.2.0",
|
|
54
|
+
"eslint": "^9.39.2",
|
|
55
|
+
"jsdom": "^27.4.0",
|
|
47
56
|
"react": "^18.2.0",
|
|
48
57
|
"react-dom": "^18.2.0",
|
|
49
58
|
"tsup": "^8.0.0",
|
|
50
|
-
"typescript": "^5.3.0"
|
|
59
|
+
"typescript": "^5.3.0",
|
|
60
|
+
"typescript-eslint": "^8.52.0",
|
|
61
|
+
"vitest": "^4.0.16"
|
|
51
62
|
}
|
|
52
63
|
}
|
package/dist/index.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
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":[]}
|