{"id":48122990,"url":"https://github.com/shipitandpray/pretext-masonry","last_synced_at":"2026-04-05T17:01:09.686Z","repository":{"id":348656794,"uuid":"1196300561","full_name":"ShipItAndPray/pretext-masonry","owner":"ShipItAndPray","description":"Pinterest-style masonry grid with Pretext height prediction. 10K+ cards virtualized.","archived":false,"fork":false,"pushed_at":"2026-04-02T06:17:03.000Z","size":72,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-04-04T17:14:21.790Z","etag":null,"topics":["pretext","text-layout","typescript","typography"],"latest_commit_sha":null,"homepage":"https://shipitandpray.github.io/pretext-masonry/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ShipItAndPray.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-30T15:06:52.000Z","updated_at":"2026-04-02T06:17:07.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ShipItAndPray/pretext-masonry","commit_stats":null,"previous_names":["shipitandpray/pretext-masonry"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ShipItAndPray/pretext-masonry","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ShipItAndPray%2Fpretext-masonry","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ShipItAndPray%2Fpretext-masonry/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ShipItAndPray%2Fpretext-masonry/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ShipItAndPray%2Fpretext-masonry/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ShipItAndPray","download_url":"https://codeload.github.com/ShipItAndPray/pretext-masonry/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ShipItAndPray%2Fpretext-masonry/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31442924,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-05T15:22:31.103Z","status":"ssl_error","status_checked_at":"2026-04-05T15:22:00.205Z","response_time":75,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["pretext","text-layout","typescript","typography"],"created_at":"2026-04-04T16:22:07.085Z","updated_at":"2026-04-05T17:01:09.679Z","avatar_url":"https://github.com/ShipItAndPray.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# @shipitandpray/pretext-masonry\n\n[![Live Demo](https://img.shields.io/badge/demo-live-brightgreen)](https://shipitandpray.github.io/pretext-masonry/) [![GitHub](https://img.shields.io/github/stars/ShipItAndPray/pretext-masonry?style=social)](https://github.com/ShipItAndPray/pretext-masonry)\n\n\u003e **[View Live Demo](https://shipitandpray.github.io/pretext-masonry/)**\n\n[![npm version](https://img.shields.io/npm/v/@shipitandpray/pretext-masonry.svg)](https://www.npmjs.com/package/@shipitandpray/pretext-masonry)\n[![bundle size](https://img.shields.io/bundlephobia/minzip/@shipitandpray/pretext-masonry)](https://bundlephobia.com/package/@shipitandpray/pretext-masonry)\n\n**Masonry/Pinterest grid with predictive text card heights and virtualization.** Zero layout flash. Powered by [@chenglou/pretext](https://github.com/chenglou/pretext).\n\n## The Problem\n\nMasonry layouts need each card's height before placement to assign it to the shortest column. Current approaches all have tradeoffs:\n\n| Approach | Drawback |\n|----------|----------|\n| **Render-then-measure** (react-masonry-css, masonic) | Visible layout jump as cards snap into position |\n| **Fixed-height cards** | Kills the masonry aesthetic -- text truncation or excess whitespace |\n| **CSS Masonry** (`grid-template-rows: masonry`) | Not shipped in any stable browser (behind flags only, as of early 2026) |\n\n**pretext-masonry** predicts card heights from text content using `@chenglou/pretext` -- pure JavaScript font measurement with zero DOM access. Cards land in the correct column on first paint.\n\n## Why Not CSS Masonry?\n\nThe CSS `masonry` value for `grid-template-rows` is specified but not shipped in any stable browser as of 2026. It's behind flags in Firefox Nightly and Safari Technology Preview. For production use today, JavaScript layout is the only reliable option. This library makes that layout flash-free.\n\n## Quick Start\n\n```bash\nnpm install @shipitandpray/pretext-masonry @chenglou/pretext react react-dom\n```\n\n```tsx\nimport { Masonry } from '@shipitandpray/pretext-masonry';\n\nfunction App({ notes }) {\n  return (\n    \u003cMasonry\n      items={notes}\n      columnWidth={280}\n      gap={12}\n      getItemText={(note) =\u003e note.body}\n      renderItem={(note) =\u003e (\n        \u003cdiv style={{ padding: 16, background: '#fff', borderRadius: 8 }}\u003e\n          \u003cp\u003e{note.body}\u003c/p\u003e\n        \u003c/div\u003e\n      )}\n    /\u003e\n  );\n}\n```\n\n## Features\n\n- **Zero layout flash** -- cards placed correctly on first paint\n- **Virtualization** -- handles 10,000+ cards, rendering only visible ones\n- **Responsive columns** -- auto-adjusts column count on resize via `ResizeObserver`\n- **Infinite scroll** -- `onEndReached` callback for loading more data\n- **Incremental layout** -- appending items is O(k), not O(n)\n- **GPU-accelerated** -- uses CSS `transform: translate()` for positioning\n- **Tiny** -- \u003c 4KB gzipped (excluding pretext-core)\n- **Framework-agnostic engine** -- `computeMasonryLayout` works without React\n\n## Feature Comparison\n\n| Feature | pretext-masonry | masonic | react-masonry-css | react-virtualized |\n|---------|:-:|:-:|:-:|:-:|\n| Zero layout flash | Yes | No | No | No |\n| Virtualization | Yes | Yes | No | Yes |\n| Predictive heights | Yes | No | No | No |\n| No DOM measurement | Yes | No | No | No |\n| Responsive columns | Yes | Yes | Yes | Manual |\n| Infinite scroll | Yes | Yes | No | Yes |\n| Bundle size (gzip) | ~3KB | ~5KB | ~1KB | ~30KB |\n\n## API Reference\n\n### `\u003cMasonry\u003cT\u003e\u003e` Component\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `items` | `T[]` | required | Data array |\n| `columnCount` | `number` | auto | Fixed column count |\n| `columnWidth` | `number` | `280` | Target column width (auto columns) |\n| `gap` | `number` | `16` | Gap between cards in px |\n| `overscan` | `number` | `5` | Extra cards rendered outside viewport |\n| `virtualize` | `boolean` | auto (`true` for \u003e100 items) | Enable virtualization |\n| `getItemText` | `(item: T) =\u003e string` | required | Extract text for height prediction |\n| `getItemMeta` | `(item: T) =\u003e CardMeta` | -- | Additional height contributors |\n| `renderItem` | `(item: T, index: number) =\u003e ReactNode` | required | Card renderer |\n| `className` | `string` | -- | Container class |\n| `style` | `CSSProperties` | -- | Container style |\n| `onEndReached` | `() =\u003e void` | -- | Infinite scroll callback |\n| `endReachedThreshold` | `number` | `500` | Pixels from bottom to trigger |\n| `font` | `string` | `'14px Inter, ...'` | CSS font string for text measurement |\n| `lineHeight` | `number` | `21` | Line height in px |\n\n### `CardMeta`\n\n```typescript\ninterface CardMeta {\n  imageHeight?: number;     // Known image height in px\n  headerText?: string;      // Title/header text\n  headerFont?: string;      // Header font (if different from body)\n  padding?: { top: number; right: number; bottom: number; left: number };\n  extraHeight?: number;     // Additional fixed height (buttons, metadata bar)\n}\n```\n\n### `useMasonryLayout\u003cT\u003e(items, options)` Hook\n\nReturns `{ positions, totalHeight, columnCount, columnWidth }` for use in custom renderers.\n\n```typescript\nconst layout = useMasonryLayout(items, {\n  containerWidth: 800,\n  columnWidth: 280,\n  gap: 12,\n  getItemText: (item) =\u003e item.body,\n});\n```\n\n### `computeMasonryLayout(cards, options)` (Pure function)\n\nFramework-agnostic layout engine. Takes an array of `{ index, predictedHeight }` and returns absolute positions.\n\n```typescript\nconst { positions, totalHeight } = computeMasonryLayout(cards, {\n  columnCount: 3,\n  columnWidth: 280,\n  gap: 16,\n});\n```\n\n### `predictCardHeight(text, options)`\n\nPredict a card's rendered height from its text content without DOM measurement.\n\n```typescript\nconst height = predictCardHeight(\"Long card text...\", {\n  containerWidth: 280,\n  font: \"14px Inter, sans-serif\",\n  lineHeight: 21,\n  padding: { top: 16, right: 16, bottom: 16, left: 16 },\n  extraHeight: 40,\n});\n```\n\n### `computeVisibleRange(positions, viewportTop, viewportHeight, overscan)`\n\nDetermine which cards overlap the current viewport for virtualization.\n\n## How Virtualization Works\n\n```\n+---------------------------+\n|  [card]  [card]  [card]   |  \u003c-- above viewport (not rendered)\n|  [card]  [card]  [card]   |\n+===========================+\n|  [card]  [card]  [card]   |  \u003c-- viewport (rendered)\n|  [card]  [card]  [card]   |\n|  [card]  [card]  [card]   |\n+===========================+\n|  [card]  [card]  [card]   |  \u003c-- below viewport (not rendered)\n|  [card]  [card]  [card]   |\n+---------------------------+\n\nContainer height = totalHeight (maintains scrollbar)\nOnly ~30-50 DOM nodes exist for 10,000+ items\n```\n\nThe container `div` has `height: totalHeight` to maintain correct scrollbar size. As the user scrolls, `computeVisibleRange` determines which cards overlap the viewport (plus overscan buffer), and only those are rendered as absolutely positioned elements.\n\n## Performance\n\n| Metric | Target | Actual |\n|--------|--------|--------|\n| Layout flash | Zero | Zero |\n| Height prediction per card | \u003c 0.2ms | ~0.05ms |\n| Layout computation (1K cards) | \u003c 50ms | ~5ms |\n| Layout computation (10K cards) | \u003c 500ms | ~40ms |\n| Scroll frame rate (10K virtualized) | 60fps | 60fps |\n| DOM nodes (10K cards, 1000px viewport) | \u003c 50 | ~30-40 |\n| Incremental layout (100 new cards) | \u003c 10ms | ~1ms |\n\n## Full Usage Example\n\n```tsx\nimport { Masonry } from '@shipitandpray/pretext-masonry';\n\ninterface Note {\n  id: string;\n  title: string;\n  body: string;\n  color: string;\n}\n\nfunction NotesGrid({ notes }: { notes: Note[] }) {\n  return (\n    \u003cMasonry\n      items={notes}\n      columnWidth={280}\n      gap={12}\n      getItemText={(note) =\u003e note.body}\n      getItemMeta={(note) =\u003e ({\n        headerText: note.title,\n        headerFont: 'bold 18px/1.3 Inter, sans-serif',\n        padding: { top: 16, right: 16, bottom: 16, left: 16 },\n        extraHeight: 40,\n      })}\n      renderItem={(note) =\u003e (\n        \u003cdiv style={{ background: note.color, borderRadius: 8, padding: 16 }}\u003e\n          \u003ch3 style={{ fontSize: 18, fontWeight: 'bold', margin: '0 0 8px' }}\u003e\n            {note.title}\n          \u003c/h3\u003e\n          \u003cp style={{ fontSize: 14, margin: 0 }}\u003e{note.body}\u003c/p\u003e\n          \u003cdiv style={{ marginTop: 12, fontSize: 12, color: '#666' }}\u003e\n            Just now\n          \u003c/div\u003e\n        \u003c/div\u003e\n      )}\n      onEndReached={() =\u003e loadMoreNotes()}\n      style={{ height: '100vh' }}\n    /\u003e\n  );\n}\n```\n\n## Build\n\n```bash\nnpm run build    # ESM + CJS + types via tsup\nnpm run test     # vitest\nnpm run test:perf  # performance benchmarks\n```\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fshipitandpray%2Fpretext-masonry","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fshipitandpray%2Fpretext-masonry","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fshipitandpray%2Fpretext-masonry/lists"}