Repository: askides/react-plock
Branch: main
Commit: 15f82a92f943
Files: 41
Total size: 53.3 KB
Directory structure:
gitextract_nv2tjxfx/
├── .github/
│ └── workflows/
│ ├── release.yml
│ ├── stale.yml
│ └── testing.yml
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── assets/
│ └── data/
│ └── images.ts
├── examples/
│ ├── with-nextjs/
│ │ ├── .eslintrc.json
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── next.config.mjs
│ │ ├── package.json
│ │ ├── postcss.config.js
│ │ ├── src/
│ │ │ └── app/
│ │ │ ├── globals.css
│ │ │ ├── layout.tsx
│ │ │ ├── page.tsx
│ │ │ └── ui/
│ │ │ └── Button.tsx
│ │ ├── tailwind.config.ts
│ │ └── tsconfig.json
│ └── with-vite/
│ ├── .gitignore
│ ├── index.html
│ ├── package.json
│ ├── src/
│ │ ├── ReactPlock.tsx
│ │ ├── main.tsx
│ │ ├── styles/
│ │ │ └── index.css
│ │ └── ui/
│ │ └── Button.tsx
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ └── vite.config.ts
├── libs/
│ └── react-plock/
│ ├── package.json
│ ├── src/
│ │ ├── index.spec.tsx
│ │ ├── index.tsx
│ │ └── utils/
│ │ ├── useGridStyles.ts
│ │ └── useMediaValues.ts
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ ├── vite.config.ts
│ └── vitest.config.ts
├── package.json
└── pnpm-workspace.yaml
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/release.yml
================================================
name: Release
on:
workflow_dispatch:
inputs:
version:
description: 'Version to release (e.g., 1.0.0)'
required: true
type: string
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- uses: pnpm/action-setup@v3
with:
version: 8
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: pnpm i
- name: Run Tests
run: pnpm run --filter react-plock test
- name: Build
run: pnpm run --filter react-plock build
- name: Manual Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
VERSION: ${{ inputs.version }}
run: |
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
# Change directory to the package
cd libs/react-plock
# Update version in package.json
npm version $VERSION --no-git-tag-version
# Return to root
cd ../../
# Commit the version change
git add .
git commit -m "chore: release v${VERSION}"
# Push the version change commit
git push
# Publish to npm
pnpm --filter react-plock publish
- name: Generate Changelog
id: changelog
run: |
PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^)
REPO="${GITHUB_REPOSITORY}"
{
echo "CHANGELOG<<EOF"
echo "# Changelog"
echo ""
git log --pretty=format:"- %s (${REPO}@%h)" ${PREVIOUS_TAG}..HEAD
echo ""
echo "EOF"
} >> $GITHUB_ENV
- name: Create GitHub Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v${{ inputs.version }}
release_name: v${{ inputs.version }}
body: ${{ env.CHANGELOG }}
draft: false
prerelease: false
================================================
FILE: .github/workflows/stale.yml
================================================
name: Mark stale issues and pull requests
on:
schedule:
- cron: '29 18 * * *'
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v5
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'This issue has been automatically closed due to inactivity. If you still have further updates, please feel free to reopen or create a new issue.'
stale-pr-message: 'This pull request has been automatically closed due to inactivity. If you still intend to contribute, please feel free to reopen the PR or submit a new one.'
stale-issue-label: 'no-issue-activity'
stale-pr-label: 'no-pr-activity'
================================================
FILE: .github/workflows/testing.yml
================================================
name: Testing CI
on:
pull_request:
branches:
- main
- dev
jobs:
main:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: pnpm/action-setup@v3
with:
version: 8
- run: pnpm install
- run: pnpm run --filter react-plock test
================================================
FILE: .gitignore
================================================
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/**/dist
/tmp
/out-tsc
# dependencies
node_modules
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db
================================================
FILE: .prettierrc
================================================
{
"singleQuote": true
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2023 Renato Pozzi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================

React Plock is a tree-shakeable **ultra small** npm package (**less than 1kB gzipped**) that allows you to create amazing masonry layouts with an amazing developer experience. With React Plock, you can easily create responsive and customizable layouts that adapt to different screen sizes and devices.
### Features
- **Masonry Layout**: Create beautiful masonry layouts with ease.
- **Balanced Layout**: Create a more visually harmonious masonry grid by considering the height of each item.
- **Responsive**: Automatically adapts to different screen sizes and devices.
- **Customizable**: Customize the layout to match your needs.
- **TypeScript Ready**: Get the strength of type-safe languages.
- **Amazing DX**: Easy to use and well-documented.
### Examples
- Using Next.js 14 (Server Components) [See Working Demo](https://react-plock-with-nextjs.vercel.app/)
- Using ViteJS [See Working Demo](https://react-plock-with-vite.vercel.app/)
### Installation
```bash
npm install react-plock
```
### Usage
Using Plock with the new v3 APIs it's a piece of cake. Here's an example of how can you create a masonry grid. You can even see a demo of this example by clicking [here](https://react-plock-with-vite.vercel.app/).
```tsx
import { Masonry } from 'react-plock';
const ImagesMasonry = () => {
const items = [...imageUrls];
return (
<Masonry
items={items}
config={{
columns: [1, 2, 3],
gap: [24, 12, 6],
media: [640, 768, 1024],
}}
render={(item, idx) => (
<img key={idx} src={item} style={{ width: '100%', height: 'auto' }} />
)}
/>
);
};
```
### Balanced Layout
React Plock offers a balanced layout option that creates a more visually harmonious masonry grid by considering the height of each item. When enabled with `useBalancedLayout: true`, the layout algorithm distributes items across columns while attempting to minimize height differences between columns.
This is particularly useful for content with varying heights, such as images or cards, where a traditional masonry layout might create uneven columns.
Unlike the default layout which distributes items sequentially, the balanced layout dynamically measures and adjusts item placement to create a more aesthetically pleasing result.
```tsx
<Masonry
items={items}
config={{
columns: [2, 3, 4],
gap: [16, 16, 16],
media: [640, 768, 1024],
useBalancedLayout: true, // Enable balanced layout
}}
render={(item) => (
<img src={item.url} alt={item.alt} style={{ width: '100%' }} />
)}
/>
```
### API Reference
Here's the TypeScript definition for the Masonry Component, below you can find a more detailed explanation.
```ts
export type MasonryProps<T> = React.ComponentPropsWithoutRef<'div'> & {
items: T[];
render: (item: T, idx: number) => React.ReactNode;
config: {
columns: number | number[];
gap: number | number[];
media?: number[];
useBalancedLayout?: boolean;
};
as?: React.ElementType;
};
```
#### Items
This prop accepts a generic array of elements, each one will be passed to the **render** property.
#### Render
The masonry render prop. Here's where you define the styles of every tile of the grid, the function takes the current looping item and the relative index.
#### Config
A configuration object that is used to define the number of columns, media queries and gaps between items.
#### Other Props
As you can see, by using `React.ComponentPropsWithoutRef<"div">` you can simply pass every available property to the div, some examples are **id** and **className**. The only one property that will be overwritten will be the `style` because is used internally for the masonry generation.
### Important Note
Please, note that in case you are passing an array to the columns attribute of the config property, the number of elements **MUST** be equal to the number of media AND gap breakpoints provided!
```tsx
// Correct: This will be responsive with 3 breakpoints.
<Masonry
{...otherProps}
config={{
columns: [1, 2, 3],
gap: [12, 16, 20],
media: [640, 768, 1024],
}}
/>
// Correct: This will be responsive with 2 breakpoints.
<Masonry
{...otherProps}
config={{
columns: [1, 2],
gap: [2, 4],
media: [640, 1024],
}}
/>
// Correct: This will be fixed with 4 columns in every screen size.
<Masonry
{...otherProps}
config={{
columns: 4,
gap: 8
}}
/>
// NOT Correct: This will cause trouble in rendering.
<Masonry
{...otherProps}
config={{
columns: [4],
media: [640, 768],
}}
/>
```
================================================
FILE: assets/data/images.ts
================================================
export const images = [
"https://images.unsplash.com/photo-1606542758304-820b04394ac2?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NDQy&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1611419010196-a360856fc42f?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODUzMjg2&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1530919424169-4b95f917e937?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NDQ2&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1634703080363-98f94e5a1076?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NDUw&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1588007374946-c79543903e8a?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODUxNTk1&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1561411996-3794338f63cc?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODUzNzIy&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1560052775-e4f689f06f07?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NDYw&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1604818659463-34304eab8e70?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODUxNjgz&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1520563683082-7ef74b616a89?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NDY4&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1573537805874-4cedc5d389ce?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NDI0&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1534817043788-41286c872b7b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODUxNjcz&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1586461715699-1e192dcd04c6?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NDc5&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1580428180163-76ab1efe2aed?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NDgz&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1595271444083-08084c6857c7?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NDg2&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1604818659418-1c53672b00f6?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NDg4&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1602136773736-34d445b989cb?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODUxNDA0&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1555086156-e6c7353d283f?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODUyNzAw&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1530692228265-084b21566b12?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NTA0&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1547434836-398fc5a73e91?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NTA1&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1626759486966-c067e3f79982?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NTEw&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1611419010019-550124aef004?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NTE2&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1560671021-cb36f70ce82d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NTE3&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1574357265250-10c88f63ebfd?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODUyNTAx&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/flagged/photo-1579451442952-f0365f3f0aed?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NTIy&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1619796404374-aff912b43cd2?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NTIz&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1582472978953-12929ab18f3e?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NTIz&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1594886801338-b81548345f77?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NTI5&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1539606328118-80c679838702?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NTMw&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1573455494057-12684d151bf4?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NTMx&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1557515126-1bf9ada5cb93?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NTMy&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1536890992765-f42a1ee1e2a8?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NTQy&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1586754102101-36b67e4c5bcf?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODUyNzg5&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1547355253-ff0740f6e8c1?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NTU0&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1555679025-2b0c57d18992?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NTU4&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1608687087357-845abfade367?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NTYw&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/flagged/photo-1579451443170-44b3963c3341?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODUxMjgx&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1535237654113-09c1e5d6e622?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NTY4&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1503187685617-d78d295f163e?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NTcx&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1628753254988-da6979f94173?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NTc1&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1585731833344-88bf310c0db6?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NTc2&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1579169703977-e4575236583c?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NTc2&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1622099330140-69125ac0d6a9?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODUzNjQ3&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1576075796033-848c2a5f3696?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODUyNTE3&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1573501815578-6252ee088c47?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NTg2&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1553227957-454e04fa8472?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODUyNDAw&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1564836663277-c4aa761b9882?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NTk2&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1607419674405-256ed5bc8f69?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NTk3&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1535737005411-82def79a9038?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODUyODY2&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1582701973975-0fff9b4c06e6?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0MDQz&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1548387834-7bf05019e89b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NjA2&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1598933385397-43259d8c1554?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NjA3&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1606152538442-6764e7a61d3d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NjEy&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1529641484336-ef35148bab06?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODUyNzE1&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1585732436715-4c25f4ba85f5?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NjE2&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1625632019469-108132f8fc60?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NjE4&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1574357278720-2809ce8065db?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NjIw&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1582150800250-347e369a020c?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NjIx&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1595353447630-3b8758e6a386?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NjI2&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1563964040780-8605906e3eb6?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODUxMjg3&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1620215175664-cb9a6f5b6103?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODUyMDI2&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1564736676781-d0f57b29f67a?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NjM4&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1556015174-ac6f87f53456?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODUxMTMy&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1498736297812-3a08021f206f?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NjQ0&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1588007374916-76ab3ff82a78?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NjQ3&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1563679200937-f4266649d170?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NjU0&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1559828801-04565cd31e27?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODUxODIy&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1575290904798-0f89bf0297d5?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NjYw&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1556367713-029b57f4a20e?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NjYx&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1610802752018-795027c7eca9?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NjY0&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1581250190370-6368e32dbcb5?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NjY5&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1579024567508-3cbf27eec69b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0Njcy&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1534946366195-7bf1dfc2fbd1?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0Njcz&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1628174383885-642404980686?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0Njc4&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1612974904144-5c725b5c6e98?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0Njg2&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1638621029425-6c77f2219438?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0Njg2&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1574391884720-bbc3740c59d1?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODUzOTMx&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1608558259020-0ba7302dc3d1?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0Njkw&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1600382224527-3ab1474fbb2a?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0Njk0&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1588007375246-3ee823ef4851?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODUyMDc1&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1622407132338-48135fd10360?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0Njk5&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1498036882173-b41c28a8ba34?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NjMx&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1568838572861-3d9cc2724435?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NzA4&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1582150809510-8098a39b1ab9?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NzEw&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1573455494060-c5595004fb6c?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODUzNzQ5&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1515036551567-bf1198cccc35?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NzEy&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1502211261676-a07824b55355?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NzEz&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1607817895500-6edefa86bdb7?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NzE3&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1626848810124-aa1bf62c0230?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODUxNjUw&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1571182160015-2169f6e1aa5f?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODUzODQ5&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1536901766856-5d45744cd180?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NzI1&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1555852224-2a3e675fc47e?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NzI4&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1594162958229-f7d5c3bf33d7?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NzI4&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1578608738964-cd27acd5af2c?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODUyNTc5&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1626339277573-ff8da132c10b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NzQy&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1612781367540-433dff529376?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NzQ2&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1485163819542-13adeb5e0068?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODUyNzE5&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1598476902279-11952e5a21b8?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NzYz&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1615006801329-249dcef6aa0f?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NzY3&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1610802711091-89aaa0ce4bc5?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0NzY5&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1604604994333-f1b0e9471186?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODUyOTEz&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1594974027866-46b2413fb07d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0Nzc0&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1601601083968-da6bc248246f?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0MDMy&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1583430999549-6813db72eb6e?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODUyMTcz&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1575907199965-cf4c0ea4092d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0Nzgy&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1536098561742-ca998e48cbcc?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODUyNzQ2&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1578608724117-4e61ac0ff89f?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0Nzg1&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1476445704028-a36e0c798192?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODUzMTE4&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1571942662090-0d81d067ca19?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0Nzk1&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1558961910-90e0503c1064?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0Nzk2&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1626111416202-4afbfda51ce3?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0Nzk3&ixlib=rb-4.0.3&q=80&w=720",
"https://images.unsplash.com/photo-1574357273651-588a9228b9a6?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjc3ODU0ODAw&ixlib=rb-4.0.3&q=80&w=720",
];
================================================
FILE: examples/with-nextjs/.eslintrc.json
================================================
{
"extends": "next/core-web-vitals"
}
================================================
FILE: examples/with-nextjs/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
================================================
FILE: examples/with-nextjs/README.md
================================================
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
================================================
FILE: examples/with-nextjs/next.config.mjs
================================================
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "images.unsplash.com",
port: "",
pathname: "/**/**",
},
],
},
};
export default nextConfig;
================================================
FILE: examples/with-nextjs/package.json
================================================
{
"name": "with-nextjs",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"react": "19.2.0",
"react-dom": "19.2.0",
"next": "15.5.9",
"react-plock": "workspace:*"
},
"devDependencies": {
"typescript": "^5",
"@types/node": "^20",
"@types/react": "19.2.2",
"@types/react-dom": "19.2.1",
"autoprefixer": "^10.0.1",
"postcss": "^8",
"tailwindcss": "^3.3.0",
"eslint": "^8",
"eslint-config-next": "15.5.4"
},
"pnpm": {
"overrides": {
"@types/react": "19.2.2",
"@types/react-dom": "19.2.1"
}
}
}
================================================
FILE: examples/with-nextjs/postcss.config.js
================================================
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
================================================
FILE: examples/with-nextjs/src/app/globals.css
================================================
body {
margin: 0;
}
================================================
FILE: examples/with-nextjs/src/app/layout.tsx
================================================
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}
================================================
FILE: examples/with-nextjs/src/app/page.tsx
================================================
'use client';
import { useState } from 'react';
import { Masonry } from 'react-plock';
import { Button } from './ui/Button';
const items = [
{ height: 600, color: '#D32F2F', id: 1 }, // Extra Tall - Darker Red
{ height: 200, color: '#00796B', id: 2 }, // Short - Darker Teal
{ height: 400, color: '#1976D2', id: 3 }, // Tall - Darker Blue
{ height: 300, color: '#2E7D32', id: 4 }, // Medium - Darker Green
{ height: 500, color: '#F57F17', id: 5 }, // Very Tall - Darker Yellow
{ height: 250, color: '#C2185B', id: 6 }, // Medium-Short - Darker Pink
{ height: 450, color: '#4527A0', id: 7 }, // Tall - Darker Purple
{ height: 200, color: '#1565C0', id: 8 }, // Short - Darker Blue
{ height: 350, color: '#D84315', id: 9 }, // Medium-Tall - Darker Orange
{ height: 550, color: '#1B5E20', id: 10 }, // Very Tall - Darker Green
{ height: 150, color: '#B71C1C', id: 11 }, // Very Short - Darker Red
{ height: 400, color: '#311B92', id: 12 }, // Tall - Darker Purple
{ height: 300, color: '#00695C', id: 13 }, // Medium - Darker Turquoise
{ height: 250, color: '#E65100', id: 14 }, // Medium-Short - Darker Orange
{ height: 500, color: '#880E4F', id: 15 }, // Very Tall - Darker Pink
{ height: 200, color: '#0D47A1', id: 16 }, // Short - Darker Blue
{ height: 450, color: '#3E2723', id: 17 }, // Tall - Darker Brown
{ height: 350, color: '#4A148C', id: 18 }, // Medium-Tall - Darker Purple
{ height: 500, color: '#01579B', id: 19 }, // Medium - Darker Blue
{ height: 400, color: '#AD1457', id: 20 }, // Tall - Darker Pink
{ height: 300, color: '#E65100', id: 21 }, // Medium-Short - Darker Orange
];
export default function Home() {
const [isBalanced, setIsBalanced] = useState(false);
return (
<div style={{ padding: '16px', fontFamily: 'system-ui' }}>
<fieldset style={{ padding: 16 }}>
<legend>Settings</legend>
<Button onClick={() => setIsBalanced(!isBalanced)}>
{isBalanced ? 'Balanced Layout' : 'Default Layout'}
</Button>
</fieldset>
<div style={{ paddingTop: '16px' }} />
<Masonry
items={items}
config={{
columns: [1, 2, 3],
gap: [24, 12, 6],
media: [640, 1024, 1280],
useBalancedLayout: isBalanced,
}}
render={(item) => (
<div
style={{
height: item.height,
backgroundColor: item.color,
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#FFF',
fontSize: '22px',
fontWeight: 'bold',
}}
>
{`${item.height}px - #${item.id}`}
</div>
)}
/>
</div>
);
}
================================================
FILE: examples/with-nextjs/src/app/ui/Button.tsx
================================================
import { ButtonHTMLAttributes, forwardRef } from 'react';
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, style, ...props }, ref) => {
return <button className={className} ref={ref} {...props} />;
}
);
Button.displayName = 'Button';
export { Button };
================================================
FILE: examples/with-nextjs/tailwind.config.ts
================================================
import type { Config } from "tailwindcss";
const config: Config = {
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
backgroundImage: {
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
"gradient-conic":
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
},
},
},
plugins: [],
};
export default config;
================================================
FILE: examples/with-nextjs/tsconfig.json
================================================
{
"compilerOptions": {
"baseUrl": ".",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"],
"@assets/*": ["../../assets/*"]
},
"target": "ES2017"
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
================================================
FILE: examples/with-vite/.gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
================================================
FILE: examples/with-vite/index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
================================================
FILE: examples/with-vite/package.json
================================================
{
"name": "with-vite",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-plock": "workspace:*",
"vite-tsconfig-paths": "^4.3.1"
},
"devDependencies": {
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"@vitejs/plugin-react": "^3.1.0",
"typescript": "^5.3.3",
"vite": "^4.1.0"
}
}
================================================
FILE: examples/with-vite/src/ReactPlock.tsx
================================================
import { Masonry } from 'react-plock';
import { useState } from 'react';
import { Button } from './ui/Button';
const items = [
{ height: 600, color: '#D32F2F', id: 1 }, // Extra Tall - Darker Red
{ height: 200, color: '#00796B', id: 2 }, // Short - Darker Teal
{ height: 400, color: '#1976D2', id: 3 }, // Tall - Darker Blue
{ height: 300, color: '#2E7D32', id: 4 }, // Medium - Darker Green
{ height: 500, color: '#F57F17', id: 5 }, // Very Tall - Darker Yellow
{ height: 250, color: '#C2185B', id: 6 }, // Medium-Short - Darker Pink
{ height: 450, color: '#4527A0', id: 7 }, // Tall - Darker Purple
{ height: 200, color: '#1565C0', id: 8 }, // Short - Darker Blue
{ height: 350, color: '#D84315', id: 9 }, // Medium-Tall - Darker Orange
{ height: 550, color: '#1B5E20', id: 10 }, // Very Tall - Darker Green
{ height: 150, color: '#B71C1C', id: 11 }, // Very Short - Darker Red
{ height: 400, color: '#311B92', id: 12 }, // Tall - Darker Purple
{ height: 300, color: '#00695C', id: 13 }, // Medium - Darker Turquoise
{ height: 250, color: '#E65100', id: 14 }, // Medium-Short - Darker Orange
{ height: 500, color: '#880E4F', id: 15 }, // Very Tall - Darker Pink
{ height: 200, color: '#0D47A1', id: 16 }, // Short - Darker Blue
{ height: 450, color: '#3E2723', id: 17 }, // Tall - Darker Brown
{ height: 350, color: '#4A148C', id: 18 }, // Medium-Tall - Darker Purple
{ height: 500, color: '#01579B', id: 19 }, // Medium - Darker Blue
{ height: 400, color: '#AD1457', id: 20 }, // Tall - Darker Pink
{ height: 300, color: '#E65100', id: 21 }, // Medium-Short - Darker Orange
];
export function ReactPlock() {
const [isBalanced, setIsBalanced] = useState(false);
return (
<div style={{ padding: '16px', fontFamily: 'system-ui' }}>
<fieldset style={{ padding: 16 }}>
<legend>Settings</legend>
<Button onClick={() => setIsBalanced(!isBalanced)}>
{isBalanced ? 'Balanced Layout' : 'Default Layout'}
</Button>
</fieldset>
<div style={{ paddingTop: '16px' }} />
<Masonry
items={items}
config={{
columns: [1, 2, 3],
gap: [24, 12, 6],
media: [640, 1024, 1280],
useBalancedLayout: isBalanced,
}}
render={(item) => (
<div
style={{
height: item.height,
backgroundColor: item.color,
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#FFF',
fontSize: '22px',
fontWeight: 'bold',
}}
>
{`${item.height}px - #${item.id}`}
</div>
)}
/>
</div>
);
}
================================================
FILE: examples/with-vite/src/main.tsx
================================================
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ReactPlock } from './ReactPlock';
import './styles/index.css';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<ReactPlock />
</React.StrictMode>
);
================================================
FILE: examples/with-vite/src/styles/index.css
================================================
body {
margin: 0;
}
================================================
FILE: examples/with-vite/src/ui/Button.tsx
================================================
import { ButtonHTMLAttributes, forwardRef } from 'react';
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, style, ...props }, ref) => {
return <button className={className} ref={ref} {...props} />;
}
);
Button.displayName = 'Button';
export { Button };
================================================
FILE: examples/with-vite/tsconfig.json
================================================
{
"compilerOptions": {
"baseUrl": ".",
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"paths": {
"@assets/*": ["../../assets/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
================================================
FILE: examples/with-vite/tsconfig.node.json
================================================
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
================================================
FILE: examples/with-vite/vite.config.ts
================================================
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), tsconfigPaths()],
});
================================================
FILE: libs/react-plock/package.json
================================================
{
"name": "react-plock",
"version": "3.6.1",
"type": "module",
"description": "The 1kB Masonry Grid for React",
"author": "Renato Pozzi <askides@proton.me>",
"homepage": "https://github.com/askides/react-plock#readme",
"license": "ISC",
"main": "dist/index.es.js",
"module": "dist/index.es.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.es.js",
"require": "./dist/index.cjs.js"
}
},
"directories": {
"dist": "dist"
},
"files": [
"dist"
],
"repository": {
"type": "git",
"url": "git+https://github.com/askides/react-plock.git"
},
"scripts": {
"test": "vitest run",
"build": "vite build",
"lib:publish": "npm publish react-plock",
"lib:publish:dev": "npm publish react-plock --tag dev"
},
"bugs": {
"url": "https://github.com/askides/react-plock/issues"
},
"peerDependencies": {
"react": "^18.2.0 || ^19.0.0"
},
"devDependencies": {
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.2"
}
}
================================================
FILE: libs/react-plock/src/index.spec.tsx
================================================
import { describe, expect, it } from 'vitest';
import { createBalancedColumns, createChunks, createDataColumns } from '.';
describe('Plock', () => {
it('should create chunks', () => {
const data = [1, 2, 3, 4, 5, 6, 7, 8];
const result = createChunks(data, 4);
expect(result).toEqual([
[1, 2, 3, 4],
[5, 6, 7, 8],
]);
});
it('should create columns', () => {
const result = createDataColumns(
[
[1, 2, 3, 4],
[5, 6, 7, 8],
],
4
);
expect(result).toEqual([
[1, 5],
[2, 6],
[3, 7],
[4, 8],
]);
});
it('should create columns even with not equal values', () => {
const data = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const chunks = createChunks(data, 4);
const result = createDataColumns(chunks, 4);
expect(result).toEqual([
[1, 5, 9],
[2, 6],
[3, 7],
[4, 8],
]);
});
it('should create columns even with not equal values', () => {
const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
const chunks = createChunks(data, 4);
const result = createDataColumns(chunks, 4);
expect(result).toEqual([
[1, 5, 9, 13],
[2, 6, 10, 14],
[3, 7, 11, 15],
[4, 8, 12],
]);
});
it('should create columns even with not equal values another', () => {
const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];
const chunks = createChunks(data, 2);
const result = createDataColumns(chunks, 2);
expect(chunks).toEqual([
[1, 2],
[3, 4],
[5, 6],
[7, 8],
[9, 10],
[11, 12],
[13, 14],
]);
expect(result).toEqual([
[1, 3, 5, 7, 9, 11, 13],
[2, 4, 6, 8, 10, 12, 14],
]);
});
});
describe('Balanced Layout', () => {
it('should distribute items to columns based on height', () => {
const items = [1, 2, 3, 4];
const heightMap = new Map([
[1, 100], // tall
[2, 50], // short
[3, 120], // tallest
[4, 30], // shortest
]);
const result = createBalancedColumns(
items,
2,
(item) => heightMap.get(item) || 0
);
expect(result).toEqual([
[1, 4], // Column 1: 100 + 30 = 130
[2, 3], // Column 2: 50 + 120 = 170
]);
});
it('should handle empty items array', () => {
const result = createBalancedColumns([], 3, () => 0);
expect(result).toEqual([[], [], []]);
});
it('should handle single column', () => {
const items = [1, 2, 3];
const result = createBalancedColumns(items, 1, () => 100);
expect(result).toEqual([[1, 2, 3]]);
});
it('should maintain item order within columns', () => {
const items = [1, 2, 3, 4, 5, 6];
const heightMap = new Map([
[1, 50],
[2, 50],
[3, 50],
[4, 50],
[5, 50],
[6, 50],
]);
const result = createBalancedColumns(
items,
3,
(item) => heightMap.get(item) || 0
);
// With equal heights, items should be distributed evenly while maintaining order
expect(result).toEqual([
[1, 4],
[2, 5],
[3, 6],
]);
});
});
================================================
FILE: libs/react-plock/src/index.tsx
================================================
import React, { useState, useMemo } from 'react';
import { useGridStyles } from './utils/useGridStyles';
import { useMediaValues } from './utils/useMediaValues';
export function asList(data: number | number[]) {
return Array.isArray(data) ? data : [data];
}
export type MasonryProps<T> = React.ComponentPropsWithoutRef<'div'> & {
items: T[];
render: (item: T, idx: number) => React.ReactNode;
config: {
columns: number | number[];
gap: number | number[];
media?: number[];
useBalancedLayout?: boolean;
};
as?: React.ElementType;
};
export function Masonry<T>({
items = [],
render,
config,
as: Component = 'div',
...rest
}: MasonryProps<T>) {
const [heights, setHeights] = useState<Map<T, number>>(new Map());
const _columns = useMemo(() => asList(config.columns), [config.columns]);
const _gaps = useMemo(() => asList(config.gap), [config.gap]);
const { columns, gap } = useMediaValues(config.media, _columns, _gaps);
const styles = useGridStyles(columns, gap);
if (!columns) return null;
// Choose layout strategy based on config
const dataColumns = config.useBalancedLayout
? createBalancedColumns(items, columns, (item) => heights.get(item) ?? 0)
: createDataColumns(createChunks(items, columns), columns);
return (
<Component {...rest} style={styles}>
{dataColumns.map((column, columnIdx) => (
<MasonryRow gap={gap} key={columnIdx}>
{column.map((item, idx) => (
<div
key={idx}
ref={(node) => {
if (node && config.useBalancedLayout) {
const height = node.getBoundingClientRect().height;
if (heights.get(item) !== height) {
// Update heights state if changed, triggering re-render
setHeights((prev) => {
const next = new Map(prev);
next.set(item, height);
return next;
});
}
}
}}
>
{render(item, idx)}
</div>
))}
</MasonryRow>
))}
</Component>
);
}
export function MasonryRow({
children,
gap,
}: {
children: React.ReactNode;
gap: number;
}) {
return (
<div
style={{
display: 'grid',
rowGap: gap,
gridTemplateColumns: 'minmax(0, 1fr)',
}}
>
{children}
</div>
);
}
export function createChunks<T>(data: T[] = [], columns = 3) {
const result = [];
for (let idx = 0; idx < data.length; idx += columns) {
const slice = data.slice(idx, idx + columns);
result.push(slice);
}
return result;
}
export function createDataColumns<T>(data: T[][] = [], columns = 3) {
const result = Array.from<T[], T[]>({ length: columns }, () => []);
for (let idx = 0; idx < columns; idx++) {
for (let jdx = 0; jdx < data.length; jdx += 1) {
if (data[jdx][idx]) {
result[idx].push(data[jdx][idx]);
}
}
}
return result;
}
export function createBalancedColumns<T>(
items: T[],
columns: number,
getHeight: (item: T) => number
): T[][] {
const result = Array.from<T[], T[]>({ length: columns }, () => []);
const columnHeights = new Array(columns).fill(0);
// Maintain original order, but distribute to shortest column
for (const item of items) {
let shortestColumnIndex = 0;
let minHeight = columnHeights[0];
for (let i = 1; i < columns; i++) {
if (columnHeights[i] < minHeight) {
minHeight = columnHeights[i];
shortestColumnIndex = i;
}
}
result[shortestColumnIndex].push(item);
columnHeights[shortestColumnIndex] += getHeight(item);
}
return result;
}
================================================
FILE: libs/react-plock/src/utils/useGridStyles.ts
================================================
import { useMemo } from 'react';
export function useGridStyles(columns: number, gap: number) {
return useMemo(
() => ({
display: 'grid',
alignItems: 'start',
gridColumnGap: gap,
gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
}),
[columns, gap]
);
}
================================================
FILE: libs/react-plock/src/utils/useMediaValues.ts
================================================
import { useState, useEffect } from 'react';
export function useMediaValues(
medias: number[] | undefined,
columns: number[],
gap: number[]
) {
const [values, setValues] = useState({ columns: 0, gap: 1 });
useEffect(() => {
if (!medias) {
setValues({ columns: columns[0], gap: gap[0] });
return;
}
const mediaQueries = medias.map((media) =>
window.matchMedia(`(min-width: ${media}px)`)
);
const onSizeChange = () => {
let matches = 0;
mediaQueries.forEach((mediaQuery) => {
if (mediaQuery.matches) {
matches++;
}
});
// Update Values
const idx = Math.min(mediaQueries.length - 1, Math.max(0, matches));
setValues({ columns: columns[idx], gap: gap[idx] });
};
// Initial Call
onSizeChange();
// Apply Listeners
for (const mediaQuery of mediaQueries) {
mediaQuery.addEventListener('change', onSizeChange);
}
return () => {
for (const mediaQuery of mediaQueries) {
mediaQuery.removeEventListener('change', onSizeChange);
}
};
}, [medias, columns, gap]);
return values;
}
================================================
FILE: libs/react-plock/tsconfig.json
================================================
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true
},
"include": ["src", "index.ts"],
"references": [{ "path": "./tsconfig.node.json" }]
}
================================================
FILE: libs/react-plock/tsconfig.node.json
================================================
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
================================================
FILE: libs/react-plock/vite.config.ts
================================================
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
import dts from 'vite-plugin-dts';
export default defineConfig({
plugins: [react(), dts({ rollupTypes: true })],
build: {
lib: {
entry: resolve(__dirname, 'src/index.tsx'),
formats: ['es', 'cjs'],
fileName: (format) => `index.${format}.js`,
},
rollupOptions: {
external: [/^react($|\/.*)/, 'react-dom'],
output: {
globals: {
react: 'React',
'react-dom': 'ReactDOM',
},
},
},
},
});
================================================
FILE: libs/react-plock/vitest.config.ts
================================================
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'jsdom',
globals: true,
},
});
================================================
FILE: package.json
================================================
{
"name": "react-plock-mono",
"private": true,
"devDependencies": {
"@types/node": "^18.14.4",
"@vitejs/plugin-react": "^4.3.4",
"jsdom": "^25.0.1",
"typescript": "^5.3.3",
"vite": "^6.0.3",
"vite-plugin-dts": "^4.3.0",
"vitest": "^2.1.8"
},
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34"
}
================================================
FILE: pnpm-workspace.yaml
================================================
packages:
- libs/*
- examples/*
ignoredBuiltDependencies:
- sharp
gitextract_nv2tjxfx/ ├── .github/ │ └── workflows/ │ ├── release.yml │ ├── stale.yml │ └── testing.yml ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── assets/ │ └── data/ │ └── images.ts ├── examples/ │ ├── with-nextjs/ │ │ ├── .eslintrc.json │ │ ├── .gitignore │ │ ├── README.md │ │ ├── next.config.mjs │ │ ├── package.json │ │ ├── postcss.config.js │ │ ├── src/ │ │ │ └── app/ │ │ │ ├── globals.css │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── ui/ │ │ │ └── Button.tsx │ │ ├── tailwind.config.ts │ │ └── tsconfig.json │ └── with-vite/ │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── src/ │ │ ├── ReactPlock.tsx │ │ ├── main.tsx │ │ ├── styles/ │ │ │ └── index.css │ │ └── ui/ │ │ └── Button.tsx │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── libs/ │ └── react-plock/ │ ├── package.json │ ├── src/ │ │ ├── index.spec.tsx │ │ ├── index.tsx │ │ └── utils/ │ │ ├── useGridStyles.ts │ │ └── useMediaValues.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── vite.config.ts │ └── vitest.config.ts ├── package.json └── pnpm-workspace.yaml
SYMBOL INDEX (14 symbols across 8 files)
FILE: examples/with-nextjs/src/app/layout.tsx
function RootLayout (line 12) | function RootLayout({
FILE: examples/with-nextjs/src/app/page.tsx
function Home (line 31) | function Home() {
FILE: examples/with-nextjs/src/app/ui/Button.tsx
type ButtonProps (line 3) | interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {}
FILE: examples/with-vite/src/ReactPlock.tsx
function ReactPlock (line 29) | function ReactPlock() {
FILE: examples/with-vite/src/ui/Button.tsx
type ButtonProps (line 3) | interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {}
FILE: libs/react-plock/src/index.tsx
function asList (line 5) | function asList(data: number | number[]) {
type MasonryProps (line 9) | type MasonryProps<T> = React.ComponentPropsWithoutRef<'div'> & {
function Masonry (line 21) | function Masonry<T>({
function MasonryRow (line 73) | function MasonryRow({
function createChunks (line 93) | function createChunks<T>(data: T[] = [], columns = 3) {
function createDataColumns (line 104) | function createDataColumns<T>(data: T[][] = [], columns = 3) {
function createBalancedColumns (line 118) | function createBalancedColumns<T>(
FILE: libs/react-plock/src/utils/useGridStyles.ts
function useGridStyles (line 3) | function useGridStyles(columns: number, gap: number) {
FILE: libs/react-plock/src/utils/useMediaValues.ts
function useMediaValues (line 3) | function useMediaValues(
Condensed preview — 41 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (59K chars).
[
{
"path": ".github/workflows/release.yml",
"chars": 2363,
"preview": "name: Release\n\non:\n workflow_dispatch:\n inputs:\n version:\n description: 'Version to release (e.g., 1.0.0"
},
{
"path": ".github/workflows/stale.yml",
"chars": 741,
"preview": "name: Mark stale issues and pull requests\n\non:\n schedule:\n - cron: '29 18 * * *'\n\njobs:\n stale:\n\n runs-on: ubuntu-"
},
{
"path": ".github/workflows/testing.yml",
"chars": 349,
"preview": "name: Testing CI\n\non:\n pull_request:\n branches:\n - main\n - dev\n\njobs:\n main:\n runs-on: ubuntu-latest\n "
},
{
"path": ".gitignore",
"chars": 505,
"preview": "# See http://help.github.com/ignore-files/ for more about ignoring files.\n\n# compiled output\n/**/dist\n/tmp\n/out-tsc\n\n# d"
},
{
"path": ".prettierrc",
"chars": 26,
"preview": "{\n \"singleQuote\": true\n}\n"
},
{
"path": "LICENSE",
"chars": 1069,
"preview": "MIT License\n\nCopyright (c) 2023 Renato Pozzi\n\nPermission is hereby granted, free of charge, to any person obtaining a co"
},
{
"path": "README.md",
"chars": 4610,
"preview": "\n\nReact Plock is a tree-shakeable **ultra small** npm package (**less than 1kB gzipped*"
},
{
"path": "assets/data/images.ts",
"chars": 20200,
"preview": "export const images = [\n \"https://images.unsplash.com/photo-1606542758304-820b04394ac2?crop=entropy&cs=tinysrgb&fit=max"
},
{
"path": "examples/with-nextjs/.eslintrc.json",
"chars": 40,
"preview": "{\n \"extends\": \"next/core-web-vitals\"\n}\n"
},
{
"path": "examples/with-nextjs/.gitignore",
"chars": 391,
"preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
},
{
"path": "examples/with-nextjs/README.md",
"chars": 1383,
"preview": "This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js"
},
{
"path": "examples/with-nextjs/next.config.mjs",
"chars": 270,
"preview": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n images: {\n remotePatterns: [\n {\n protocol"
},
{
"path": "examples/with-nextjs/package.json",
"chars": 719,
"preview": "{\n \"name\": \"with-nextjs\",\n \"version\": \"1.0.0\",\n \"private\": true,\n \"scripts\": {\n \"dev\": \"next dev --turbopack\",\n "
},
{
"path": "examples/with-nextjs/postcss.config.js",
"chars": 83,
"preview": "module.exports = {\n plugins: {\n tailwindcss: {},\n autoprefixer: {},\n },\n};\n"
},
{
"path": "examples/with-nextjs/src/app/globals.css",
"chars": 22,
"preview": "body {\n margin: 0;\n}\n"
},
{
"path": "examples/with-nextjs/src/app/layout.tsx",
"chars": 473,
"preview": "import type { Metadata } from \"next\";\nimport { Inter } from \"next/font/google\";\nimport \"./globals.css\";\n\nconst inter = I"
},
{
"path": "examples/with-nextjs/src/app/page.tsx",
"chars": 2786,
"preview": "'use client';\n\nimport { useState } from 'react';\nimport { Masonry } from 'react-plock';\nimport { Button } from './ui/But"
},
{
"path": "examples/with-nextjs/src/app/ui/Button.tsx",
"chars": 369,
"preview": "import { ButtonHTMLAttributes, forwardRef } from 'react';\n\nexport interface ButtonProps extends ButtonHTMLAttributes<HTM"
},
{
"path": "examples/with-nextjs/tailwind.config.ts",
"chars": 510,
"preview": "import type { Config } from \"tailwindcss\";\n\nconst config: Config = {\n content: [\n \"./src/pages/**/*.{js,ts,jsx,tsx,m"
},
{
"path": "examples/with-nextjs/tsconfig.json",
"chars": 661,
"preview": "{\n \"compilerOptions\": {\n \"baseUrl\": \".\",\n \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n \"allowJs\": true,\n \"ski"
},
{
"path": "examples/with-vite/.gitignore",
"chars": 253,
"preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndis"
},
{
"path": "examples/with-vite/index.html",
"chars": 366,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <link rel=\"icon\" type=\"image/svg+xml\" href=\"/"
},
{
"path": "examples/with-vite/package.json",
"chars": 520,
"preview": "{\n \"name\": \"with-vite\",\n \"private\": true,\n \"version\": \"1.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n"
},
{
"path": "examples/with-vite/src/ReactPlock.tsx",
"chars": 2769,
"preview": "import { Masonry } from 'react-plock';\nimport { useState } from 'react';\nimport { Button } from './ui/Button';\n\nconst it"
},
{
"path": "examples/with-vite/src/main.tsx",
"chars": 283,
"preview": "import React from 'react';\nimport ReactDOM from 'react-dom/client';\nimport { ReactPlock } from './ReactPlock';\n\nimport '"
},
{
"path": "examples/with-vite/src/styles/index.css",
"chars": 22,
"preview": "body {\n margin: 0;\n}\n"
},
{
"path": "examples/with-vite/src/ui/Button.tsx",
"chars": 369,
"preview": "import { ButtonHTMLAttributes, forwardRef } from 'react';\n\nexport interface ButtonProps extends ButtonHTMLAttributes<HTM"
},
{
"path": "examples/with-vite/tsconfig.json",
"chars": 639,
"preview": "{\n \"compilerOptions\": {\n \"baseUrl\": \".\",\n \"target\": \"ESNext\",\n \"useDefineForClassFields\": true,\n \"lib\": [\"D"
},
{
"path": "examples/with-vite/tsconfig.node.json",
"chars": 184,
"preview": "{\n \"compilerOptions\": {\n \"composite\": true,\n \"module\": \"ESNext\",\n \"moduleResolution\": \"Node\",\n \"allowSynthe"
},
{
"path": "examples/with-vite/vite.config.ts",
"chars": 232,
"preview": "import react from \"@vitejs/plugin-react\";\nimport { defineConfig } from \"vite\";\nimport tsconfigPaths from \"vite-tsconfig-"
},
{
"path": "libs/react-plock/package.json",
"chars": 1078,
"preview": "{\n \"name\": \"react-plock\",\n \"version\": \"3.6.1\",\n \"type\": \"module\",\n \"description\": \"The 1kB Masonry Grid for React\",\n"
},
{
"path": "libs/react-plock/src/index.spec.tsx",
"chars": 3126,
"preview": "import { describe, expect, it } from 'vitest';\nimport { createBalancedColumns, createChunks, createDataColumns } from '."
},
{
"path": "libs/react-plock/src/index.tsx",
"chars": 3758,
"preview": "import React, { useState, useMemo } from 'react';\nimport { useGridStyles } from './utils/useGridStyles';\nimport { useMed"
},
{
"path": "libs/react-plock/src/utils/useGridStyles.ts",
"chars": 302,
"preview": "import { useMemo } from 'react';\n\nexport function useGridStyles(columns: number, gap: number) {\n return useMemo(\n ()"
},
{
"path": "libs/react-plock/src/utils/useMediaValues.ts",
"chars": 1150,
"preview": "import { useState, useEffect } from 'react';\n\nexport function useMediaValues(\n medias: number[] | undefined,\n columns:"
},
{
"path": "libs/react-plock/tsconfig.json",
"chars": 534,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ES2020\",\n \"useDefineForClassFields\": true,\n \"lib\": [\"ES2020\", \"DOM\", \"DOM."
},
{
"path": "libs/react-plock/tsconfig.node.json",
"chars": 213,
"preview": "{\n \"compilerOptions\": {\n \"composite\": true,\n \"skipLibCheck\": true,\n \"module\": \"ESNext\",\n \"moduleResolution\""
},
{
"path": "libs/react-plock/vite.config.ts",
"chars": 588,
"preview": "import { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\nimport { resolve } from 'path';\nimport dt"
},
{
"path": "libs/react-plock/vitest.config.ts",
"chars": 141,
"preview": "import { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n test: {\n environment: 'jsdom',\n glo"
},
{
"path": "package.json",
"chars": 451,
"preview": "{\n \"name\": \"react-plock-mono\",\n \"private\": true,\n \"devDependencies\": {\n \"@types/node\": \"^18.14.4\",\n \"@vitejs/pl"
},
{
"path": "pnpm-workspace.yaml",
"chars": 73,
"preview": "packages:\n - libs/*\n - examples/*\n\nignoredBuiltDependencies:\n - sharp\n"
}
]
About this extraction
This page contains the full source code of the askides/react-plock GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 41 files (53.3 KB), approximately 21.3k tokens, and a symbol index with 14 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.