├── .build.mjs ├── .eslintrc.cjs ├── .gitignore ├── .npmignore ├── .vscode └── extensions.json ├── LICENSE ├── README.md ├── package.json ├── packages └── vue-playground │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── public │ └── vite.svg │ ├── src │ ├── App.vue │ ├── main.ts │ ├── style.css │ └── vite-env.d.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── playground ├── .eslintrc.cjs ├── .gitignore ├── README.md ├── index.html ├── package.json ├── public │ ├── catsjuice.jpg │ └── imgs │ │ ├── thumb.1.jpg │ │ ├── thumb.10.jpg │ │ ├── thumb.2.jpg │ │ ├── thumb.3.jpg │ │ ├── thumb.4.jpg │ │ ├── thumb.5.jpg │ │ ├── thumb.6.jpg │ │ ├── thumb.7.jpg │ │ ├── thumb.8.jpg │ │ └── thumb.9.jpg ├── src │ ├── App.tsx │ ├── assets │ │ └── react.svg │ ├── components │ │ ├── app-header │ │ │ ├── index.tsx │ │ │ └── theme-toggle │ │ │ │ └── theme-toggle.tsx │ │ ├── card │ │ │ ├── card.css │ │ │ └── index.tsx │ │ └── resize-frame │ │ │ ├── index.tsx │ │ │ ├── resize.css │ │ │ └── ruler.tsx │ ├── hooks │ │ └── use-theme.ts │ ├── index.css │ ├── main.tsx │ ├── utils │ │ └── item.ts │ └── vite-env.d.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── uno.config.ts └── vite.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── rollup.config.js ├── src ├── core │ ├── calculator.ts │ ├── index.ts │ └── types.ts ├── react │ ├── context.ts │ ├── footer.tsx │ ├── index.ts │ ├── item.tsx │ ├── root.tsx │ └── type.ts └── vue │ ├── index.ts │ ├── item.ts │ ├── root.ts │ └── type.ts └── tsconfig.json /.build.mjs: -------------------------------------------------------------------------------- 1 | import { dirname, resolve } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import chalk from "chalk"; 4 | import fs from "fs"; 5 | import { readFile, writeFile } from "fs/promises"; 6 | import { execa } from "execa"; 7 | import { execSync } from "child_process"; 8 | import prompts from "prompts"; 9 | import brotliSize from "brotli-size"; 10 | import prettyBytes from "pretty-bytes"; 11 | 12 | const info = (m) => console.log("► " + chalk.blue(m)); 13 | const error = (m) => console.log("⚠️ " + chalk.red(m)); 14 | const success = (m) => console.log("✅ " + chalk.green(m)); 15 | const details = (m) => console.log(chalk.pink(m)); 16 | 17 | const __filename = fileURLToPath(import.meta.url); 18 | const __dirname = dirname(__filename); 19 | const rootDir = resolve(__dirname); 20 | const isPublishing = process.argv[2] === "--publish"; 21 | 22 | async function clean() { 23 | if (!fs.existsSync(`${rootDir}/dist`)) return; 24 | await execa("shx", ["rm", "-rf", `${rootDir}/dist`]); 25 | } 26 | 27 | async function baseBuild() { 28 | info("Building primary package"); 29 | await execa("npx", ["rollup", "-c", "rollup.config.js"]); 30 | } 31 | 32 | async function vueBuild() { 33 | info("Building Vue package"); 34 | await execa("npx", [ 35 | "rollup", 36 | "-c", 37 | "rollup.config.js", 38 | "--environment", 39 | "FRAMEWORK:vue", 40 | ]); 41 | 42 | let raw = await readFile(resolve(rootDir, "dist/vue/index.mjs"), "utf8"); 43 | raw = raw.replace("from '../index'", "from '../index.mjs'"); 44 | await writeFile(resolve(rootDir, "dist/vue/index.mjs"), raw); 45 | } 46 | 47 | async function reactBuild() { 48 | info("Building React package"); 49 | await execa("npx", [ 50 | "rollup", 51 | "-c", 52 | "rollup.config.js", 53 | "--environment", 54 | "FRAMEWORK:react", 55 | ]); 56 | let raw = await readFile(resolve(rootDir, "dist/react/index.mjs"), "utf8"); 57 | raw = raw.replace("from '../index'", "from '../index.mjs'"); 58 | await writeFile(resolve(rootDir, "dist/react/index.mjs"), raw); 59 | } 60 | 61 | async function declarationsBuild() { 62 | info("Building declarations"); 63 | await execa("npx", [ 64 | "rollup", 65 | "-c", 66 | "rollup.config.js", 67 | "--environment", 68 | "DECLARATIONS:true", 69 | ]); 70 | } 71 | 72 | async function bundleDeclarations() { 73 | info("Organizing declarations"); 74 | await execa("shx", [ 75 | "mv", 76 | `${rootDir}/dist/src/core/*`, 77 | `${rootDir}/dist/core/`, 78 | ]); 79 | await execa("shx", [ 80 | "mv", 81 | `${rootDir}/dist/src/react/*`, 82 | `${rootDir}/dist/react/`, 83 | ]); 84 | await execa("shx", [ 85 | "mv", 86 | `${rootDir}/dist/src/vue/*`, 87 | `${rootDir}/dist/vue/`, 88 | ]); 89 | await execa("shx", ["rm", "-rf", `${rootDir}/dist/src`]); 90 | await execa("shx", ["rm", `${rootDir}/dist/index.js`]); 91 | } 92 | 93 | async function addPackageJSON() { 94 | info("Generating package.json"); 95 | const raw = await readFile(resolve(rootDir, "package.json"), "utf8"); 96 | const packageJSON = JSON.parse(raw); 97 | delete packageJSON.private; 98 | delete packageJSON.devDependencies; 99 | delete packageJSON.scripts; 100 | await writeFile( 101 | resolve(rootDir, "dist/package.json"), 102 | JSON.stringify(packageJSON, null, 2) 103 | ); 104 | } 105 | 106 | async function addAssets() { 107 | info("Writing readme and license."); 108 | await execa("shx", [ 109 | "cp", 110 | `${rootDir}/README.md`, 111 | `${rootDir}/dist/README.md`, 112 | ]); 113 | await execa("shx", ["cp", `${rootDir}/LICENSE`, `${rootDir}/dist/LICENSE`]); 114 | } 115 | 116 | async function bumpVersion() { 117 | info("Bumping version"); 118 | const raw = await readFile(resolve(rootDir, "package.json"), "utf8"); 119 | const packageJSON = JSON.parse(raw); 120 | 121 | const version = packageJSON.version; 122 | const [major, minor, patch] = version.split("."); 123 | const versionRes = await prompts([ 124 | { 125 | type: "select", 126 | name: "value", 127 | message: `Select version bump type`, 128 | choices: [ 129 | { title: "Patch", value: "patch" }, 130 | { title: "Minor", value: "minor" }, 131 | { title: "Major", value: "major" }, 132 | ], 133 | initial: 0, 134 | }, 135 | ]); 136 | const newVersion = [major, minor, patch]; 137 | const index = ["major", "minor", "patch"].indexOf(versionRes.value); 138 | newVersion[index] = parseInt(newVersion[index]) + 1; 139 | for (let i = index + 1; i < newVersion.length; i++) { 140 | newVersion[i] = 0; 141 | } 142 | packageJSON.version = newVersion.join("."); 143 | 144 | const confirmRes = await prompts([ 145 | { 146 | type: "confirm", 147 | name: "value", 148 | message: `Bump version to ${packageJSON.version}?`, 149 | initial: true, 150 | }, 151 | ]); 152 | if (!confirmRes.value) { 153 | process.exit(); 154 | } 155 | await writeFile( 156 | resolve(rootDir, "package.json"), 157 | JSON.stringify(packageJSON, null, 2) 158 | ); 159 | 160 | execSync(`git add package.json`); 161 | execSync(`git commit -m "Bump version to ${packageJSON.version}"`); 162 | execSync(`git tag ${packageJSON.version}`); 163 | execSync(`git push origin --tags`); 164 | } 165 | 166 | async function prepareForPublishing() { 167 | info("Preparing for publication"); 168 | const isClean = !execSync(`git status --untracked-files=no --porcelain`, { 169 | encoding: "utf-8", 170 | }); 171 | if (!isClean) { 172 | error("Commit your changes before publishing."); 173 | process.exit(); 174 | } 175 | 176 | // bump version 177 | await bumpVersion(); 178 | 179 | const raw = await readFile(resolve(rootDir, "package.json"), "utf8"); 180 | const packageJSON = JSON.parse(raw); 181 | 182 | const response = await prompts([ 183 | { 184 | type: "confirm", 185 | name: "value", 186 | message: `Confirm you want to publish version ${chalk.red( 187 | packageJSON.version 188 | )}?`, 189 | initial: true, 190 | }, 191 | ]); 192 | if (!response.value) { 193 | error("Please adjust the version and try again"); 194 | process.exit(); 195 | } 196 | } 197 | 198 | async function publish() { 199 | const response = await prompts([ 200 | { 201 | type: "confirm", 202 | name: "value", 203 | message: `Project is build. Ready to publish?`, 204 | initial: false, 205 | }, 206 | ]); 207 | if (response.value) { 208 | execSync("npm publish ./dist --registry https://registry.npmjs.org"); 209 | } 210 | } 211 | 212 | async function outputSize(pkg) { 213 | const raw = await readFile(resolve(rootDir, `dist/${pkg}/index.mjs`), "utf8"); 214 | info(`Brotli size - ${pkg}: ` + prettyBytes(brotliSize.sync(raw))); 215 | } 216 | 217 | await clean(); 218 | await baseBuild(); 219 | await vueBuild(); 220 | await reactBuild(); 221 | await declarationsBuild(); 222 | await bundleDeclarations(); 223 | if (!isPublishing) await addPackageJSON(); 224 | await addAssets(); 225 | await outputSize("core"); 226 | await outputSize("react"); 227 | success("Build completed"); 228 | if (isPublishing) { 229 | await prepareForPublishing(); 230 | await addPackageJSON(); 231 | await publish(); 232 | } 233 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true, node: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist-playground 12 | dist 13 | dist-ssr 14 | *.local 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .eslintrc.cjs 2 | .gitignore 3 | index.html 4 | pnpm-lock.yaml 5 | public 6 | src 7 | tsconfig.json 8 | tsconfig.node.json 9 | uno.config.ts 10 | vite.config.playground.ts 11 | vite.config.ts 12 | dist-playground -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar"] 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Cats Juice 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Playground 2 | 3 | [masonry.oooo.so](https://masonry.oooo.so) 4 | 5 | # Install 6 | 7 | ``` 8 | npm install masonry-flow 9 | ``` 10 | 11 | # Usage 12 | 13 | ## React 14 | 15 | ```tsx 16 | import MasonryFlow from "masonry-flow/react"; 17 | 18 | 19 | 20 | 1 21 | 22 | 23 | 2 24 | 25 | 26 | 3 27 | 28 | ; 29 | ``` 30 | 31 | ## Vue 32 | 33 | ```vue 34 | 37 | 38 | 51 | ``` 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "masonry-flow", 3 | "version": "0.2.2", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "pnpm --filter playground dev", 7 | "dev:react": "pnpm --filter playground dev", 8 | "dev:vue": "pnpm --filter vue-playground dev", 9 | "build": "node ./.build.mjs", 10 | "build:playground": "cd ./playground && tsc && vite build", 11 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 12 | "preview": "vite preview", 13 | "pub": "npm run build && npm publish" 14 | }, 15 | "author": "CatsJuice", 16 | "license": "MIT", 17 | "repository": { 18 | "url": "https://github.com/CatsJuice/masonry-flow" 19 | }, 20 | "homepage": "https://masonry.oooo.so", 21 | "dependencies": {}, 22 | "types": "dist/index.d.ts", 23 | "exports": { 24 | "./core": { 25 | "import": "./core/index.mjs", 26 | "types": "./core/index.d.ts", 27 | "default": "./core/index.mjs" 28 | }, 29 | "./react": { 30 | "import": "./react/index.mjs", 31 | "types": "./react/index.d.ts", 32 | "default": "./react/index.mjs" 33 | }, 34 | "./vue": { 35 | "import": "./vue/index.mjs", 36 | "types": "./vue/index.d.ts", 37 | "default": "./vue/index.mjs" 38 | } 39 | }, 40 | "keywords": [ 41 | "masonry", 42 | "absolute grid", 43 | "masonry layout" 44 | ], 45 | "devDependencies": { 46 | "react": "^18.2.0", 47 | "react-dom": "^18.2.0", 48 | "@rollup/plugin-terser": "^0.4.4", 49 | "@rollup/plugin-typescript": "^11.1.6", 50 | "@tweakpane/core": "^2.0.4", 51 | "@types/node": "^20.12.4", 52 | "@types/react": "^18.2.66", 53 | "@types/react-dom": "^18.2.22", 54 | "@typescript-eslint/eslint-plugin": "^7.2.0", 55 | "@typescript-eslint/parser": "^7.2.0", 56 | "@vitejs/plugin-react": "^4.2.1", 57 | "brotli-size": "^4.0.0", 58 | "chalk": "^5.3.0", 59 | "eslint": "^8.57.0", 60 | "eslint-plugin-react-hooks": "^4.6.0", 61 | "eslint-plugin-react-refresh": "^0.4.6", 62 | "execa": "^9.3.0", 63 | "pretty-bytes": "^6.1.1", 64 | "prompts": "^2.4.2", 65 | "shx": "^0.3.4", 66 | "tslib": "^2.6.3", 67 | "tweakpane": "^4.0.4", 68 | "typescript": "^5.2.2", 69 | "vite": "^5.2.0", 70 | "vite-plugin-dts": "4.0.0-beta.1", 71 | "vue": "^3.4.31" 72 | } 73 | } -------------------------------------------------------------------------------- /packages/vue-playground/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /packages/vue-playground/README.md: -------------------------------------------------------------------------------- 1 | # Vue 3 + TypeScript + Vite 2 | 3 | This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/vue-playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-playground", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vue-tsc -b && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "vue": "^3.4.31" 13 | }, 14 | "devDependencies": { 15 | "@vitejs/plugin-vue": "^5.0.5", 16 | "typescript": "^5.2.2", 17 | "vite": "^5.3.4", 18 | "vue-tsc": "^2.0.24" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/vue-playground/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/vue-playground/src/App.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 61 | 62 | 95 | -------------------------------------------------------------------------------- /packages/vue-playground/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import "./style.css"; 3 | import App from "./App.vue"; 4 | 5 | createApp(App).mount("#app"); 6 | -------------------------------------------------------------------------------- /packages/vue-playground/src/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | * { 17 | box-sizing: border-box; 18 | margin: 0; 19 | padding: 0; 20 | } 21 | 22 | a { 23 | font-weight: 500; 24 | color: #646cff; 25 | text-decoration: inherit; 26 | } 27 | a:hover { 28 | color: #535bf2; 29 | } 30 | 31 | body { 32 | margin: 0; 33 | min-width: 320px; 34 | min-height: 100vh; 35 | } 36 | 37 | h1 { 38 | font-size: 3.2em; 39 | line-height: 1.1; 40 | } 41 | 42 | button { 43 | border-radius: 8px; 44 | border: 1px solid transparent; 45 | padding: 0.6em 1.2em; 46 | font-size: 1em; 47 | font-weight: 500; 48 | font-family: inherit; 49 | background-color: #1a1a1a; 50 | cursor: pointer; 51 | transition: border-color 0.25s; 52 | } 53 | button:hover { 54 | border-color: #646cff; 55 | } 56 | button:focus, 57 | button:focus-visible { 58 | outline: 4px auto -webkit-focus-ring-color; 59 | } 60 | 61 | .card { 62 | padding: 2em; 63 | } 64 | 65 | #app { 66 | width: 100%; 67 | margin: 0 auto; 68 | padding: 2rem; 69 | text-align: center; 70 | } 71 | 72 | @media (prefers-color-scheme: light) { 73 | :root { 74 | color: #213547; 75 | background-color: #ffffff; 76 | } 77 | a:hover { 78 | color: #747bff; 79 | } 80 | button { 81 | background-color: #f9f9f9; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /packages/vue-playground/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/vue-playground/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 5 | "target": "ES2020", 6 | "useDefineForClassFields": true, 7 | "module": "ESNext", 8 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 9 | "skipLibCheck": true, 10 | 11 | /* Bundler mode */ 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "moduleDetection": "force", 17 | "noEmit": true, 18 | "jsx": "preserve", 19 | 20 | /* Linting */ 21 | "strict": true, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "noFallthroughCasesInSwitch": true 25 | }, 26 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] 27 | } 28 | -------------------------------------------------------------------------------- /packages/vue-playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.app.json" 6 | }, 7 | { 8 | "path": "./tsconfig.node.json" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/vue-playground/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 5 | "skipLibCheck": true, 6 | "module": "ESNext", 7 | "moduleResolution": "bundler", 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "noEmit": true 11 | }, 12 | "include": ["vite.config.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/vue-playground/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [vue()], 7 | }) 8 | -------------------------------------------------------------------------------- /playground/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | "eslint:recommended", 6 | "plugin:@typescript-eslint/recommended", 7 | "plugin:react-hooks/recommended", 8 | ], 9 | ignorePatterns: ["dist", ".eslintrc.cjs"], 10 | parser: "@typescript-eslint/parser", 11 | plugins: ["react-refresh"], 12 | rules: { 13 | "react-refresh/only-export-components": [ 14 | "warn", 15 | { allowConstantExport: true }, 16 | ], 17 | // enable @ts-ignore comments 18 | "@typescript-eslint/ban-ts-comment": "off", 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /playground/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /playground/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default { 18 | // other rules... 19 | parserOptions: { 20 | ecmaVersion: 'latest', 21 | sourceType: 'module', 22 | project: ['./tsconfig.json', './tsconfig.node.json', './tsconfig.app.json'], 23 | tsconfigRootDir: __dirname, 24 | }, 25 | } 26 | ``` 27 | 28 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` 29 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list 31 | -------------------------------------------------------------------------------- /playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Masonry Flow Playground 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "masonry-flow": "workspace:*", 14 | "react": "^18.3.1", 15 | "react-dom": "^18.3.1", 16 | "unocss": "^0.61.5" 17 | }, 18 | "devDependencies": { 19 | "@iconify-json/mdi": "^1.1.67", 20 | "@types/react": "^18.3.3", 21 | "@types/react-dom": "^18.3.0", 22 | "@typescript-eslint/eslint-plugin": "^7.15.0", 23 | "@typescript-eslint/parser": "^7.15.0", 24 | "@vitejs/plugin-react": "^4.3.1", 25 | "eslint": "^8.57.0", 26 | "eslint-plugin-react-hooks": "^4.6.2", 27 | "eslint-plugin-react-refresh": "^0.4.7", 28 | "typescript": "^5.2.2", 29 | "vite": "^5.3.4" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /playground/public/catsjuice.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatsJuice/masonry-flow/17fb4fd63201a4263bbd88f14f33c3ce19f459cd/playground/public/catsjuice.jpg -------------------------------------------------------------------------------- /playground/public/imgs/thumb.1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatsJuice/masonry-flow/17fb4fd63201a4263bbd88f14f33c3ce19f459cd/playground/public/imgs/thumb.1.jpg -------------------------------------------------------------------------------- /playground/public/imgs/thumb.10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatsJuice/masonry-flow/17fb4fd63201a4263bbd88f14f33c3ce19f459cd/playground/public/imgs/thumb.10.jpg -------------------------------------------------------------------------------- /playground/public/imgs/thumb.2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatsJuice/masonry-flow/17fb4fd63201a4263bbd88f14f33c3ce19f459cd/playground/public/imgs/thumb.2.jpg -------------------------------------------------------------------------------- /playground/public/imgs/thumb.3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatsJuice/masonry-flow/17fb4fd63201a4263bbd88f14f33c3ce19f459cd/playground/public/imgs/thumb.3.jpg -------------------------------------------------------------------------------- /playground/public/imgs/thumb.4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatsJuice/masonry-flow/17fb4fd63201a4263bbd88f14f33c3ce19f459cd/playground/public/imgs/thumb.4.jpg -------------------------------------------------------------------------------- /playground/public/imgs/thumb.5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatsJuice/masonry-flow/17fb4fd63201a4263bbd88f14f33c3ce19f459cd/playground/public/imgs/thumb.5.jpg -------------------------------------------------------------------------------- /playground/public/imgs/thumb.6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatsJuice/masonry-flow/17fb4fd63201a4263bbd88f14f33c3ce19f459cd/playground/public/imgs/thumb.6.jpg -------------------------------------------------------------------------------- /playground/public/imgs/thumb.7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatsJuice/masonry-flow/17fb4fd63201a4263bbd88f14f33c3ce19f459cd/playground/public/imgs/thumb.7.jpg -------------------------------------------------------------------------------- /playground/public/imgs/thumb.8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatsJuice/masonry-flow/17fb4fd63201a4263bbd88f14f33c3ce19f459cd/playground/public/imgs/thumb.8.jpg -------------------------------------------------------------------------------- /playground/public/imgs/thumb.9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatsJuice/masonry-flow/17fb4fd63201a4263bbd88f14f33c3ce19f459cd/playground/public/imgs/thumb.9.jpg -------------------------------------------------------------------------------- /playground/src/App.tsx: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import MasonryFlow from "../../src/react"; 3 | import { useCallback, useEffect, useRef, useState } from "react"; 4 | import { Pane } from "tweakpane"; 5 | import { getHeight, type Item, randomItem } from "./utils/item"; 6 | import { Card } from "./components/card"; 7 | import { AppHeader } from "./components/app-header"; 8 | import { ResizeFrame } from "./components/resize-frame"; 9 | import { Theme, ThemeContext } from "./hooks/use-theme"; 10 | 11 | const PARAMS = { 12 | fixedHeight: false, 13 | gapX: 8, 14 | gapY: 8, 15 | minWidth: 260, 16 | maxWidth: 370, 17 | transitionDuration: 400, 18 | locationMode: "translate" as const, 19 | strategy: "as-many-as-possible" as const, 20 | }; 21 | 22 | function App() { 23 | const debuggerContainerRef = useRef(null); 24 | const insertPointRef = useRef(null); 25 | const [list, setList] = useState([]); 26 | const [fixedHeight, setFixedHeight] = useState(PARAMS.fixedHeight); 27 | const [gapX, setGapX] = useState(PARAMS.gapX); 28 | const [gapY, setGapY] = useState(PARAMS.gapY); 29 | const [minWidth, setMinWidth] = useState(PARAMS.minWidth); 30 | const [maxWidth, setMaxWidth] = useState(PARAMS.maxWidth); 31 | const [locationMode, setLocationMode] = useState(PARAMS.locationMode); 32 | const [transitionDuration, setTransitionDuration] = useState( 33 | PARAMS.transitionDuration 34 | ); 35 | const [strategy, setStrategy] = useState(PARAMS.strategy); 36 | 37 | const addItem = useCallback((item: Item) => { 38 | setList((prev) => [...prev, item]); 39 | }, []); 40 | 41 | const insertItem = useCallback((item: Item, index: number) => { 42 | setList((prev) => { 43 | const next = [...prev]; 44 | next.splice(index, 0, item); 45 | return next; 46 | }); 47 | }, []); 48 | 49 | const removeItem = useCallback((id?: number) => { 50 | if (id !== undefined) 51 | setList((prev) => prev.filter((item) => item.id !== id)); 52 | else setList((prev) => prev.slice(0, -1)); 53 | }, []); 54 | 55 | const moveItem = useCallback((id: number, offset: number) => { 56 | setList((prev) => { 57 | const index = prev.findIndex((item) => item.id === id); 58 | if (index === -1) return prev; 59 | const next = [...prev]; 60 | const item = next.splice(index, 1)[0]; 61 | next.splice(index + offset, 0, item); 62 | return next; 63 | }); 64 | }, []); 65 | 66 | const batchAdd = useCallback( 67 | (count = 10, delay = 100, offset = 5) => { 68 | addItem(randomItem(fixedHeight)); 69 | if (count > 1) { 70 | setTimeout(() => batchAdd(count - 1, delay), delay + offset); 71 | } 72 | }, 73 | [addItem, fixedHeight] 74 | ); 75 | const batchRemove = useCallback( 76 | (count = 10, delay = 50, offset = -2) => { 77 | removeItem(); 78 | if (count > 1) { 79 | setTimeout(() => batchRemove(count - 1, delay), delay + offset, offset); 80 | } 81 | }, 82 | [removeItem] 83 | ); 84 | 85 | const initializedRef = useRef(false); 86 | useEffect(() => { 87 | if (!initializedRef.current) batchAdd(10); 88 | initializedRef.current = true; 89 | // eslint-disable-next-line react-hooks/exhaustive-deps 90 | }, []); 91 | 92 | const prevHeights = useRef(new Map()); 93 | 94 | useEffect(() => { 95 | const pane = new Pane({ 96 | container: debuggerContainerRef.current!, 97 | }); 98 | 99 | const itemsControl = pane.addFolder({ title: "Items" }); 100 | 101 | // height control 102 | itemsControl 103 | .addBinding(PARAMS, "fixedHeight", { label: "Fix height" }) 104 | .on("change", ({ value }) => { 105 | setFixedHeight(value); 106 | setList((prev) => { 107 | if (value) { 108 | prevHeights.current.clear(); 109 | prev.forEach((item) => 110 | prevHeights.current.set(item.id, item.height) 111 | ); 112 | } 113 | return prev.map((item) => ({ 114 | ...item, 115 | height: value 116 | ? getHeight(true) 117 | : prevHeights.current.get(item.id) ?? getHeight(value), 118 | })); 119 | }); 120 | }); 121 | 122 | // clear 123 | itemsControl 124 | .addButton({ title: "Clear all", label: "Reset" }) 125 | .on("click", () => setList([])); 126 | 127 | // add 128 | itemsControl 129 | .addButton({ label: "Add single", title: "+1" }) 130 | .on("click", () => addItem(randomItem(fixedHeight))); 131 | itemsControl 132 | .addButton({ label: "Add 10", title: "+10" }) 133 | .on("click", () => batchAdd(10, 100)); 134 | // remove 135 | itemsControl 136 | .addButton({ label: "Remove single", title: "-1" }) 137 | .on("click", () => removeItem()); 138 | itemsControl 139 | .addButton({ label: "Remove 10", title: "-10" }) 140 | .on("click", () => batchRemove(10)); 141 | itemsControl 142 | .addBinding(PARAMS, "locationMode", { 143 | options: { "Left-Top": "left-top", Translate: "translate" }, 144 | label: "Location Mode", 145 | }) 146 | .on("change", ({ value }) => setLocationMode(value)); 147 | itemsControl 148 | .addBinding(PARAMS, "strategy", { 149 | options: { 150 | "As many as possible": "as-many-as-possible", 151 | "As less as possible": "as-less-as-possible", 152 | }, 153 | label: "Strategy", 154 | }) 155 | .on("change", ({ value }) => setStrategy(value)); 156 | 157 | const styleControl = pane.addFolder({ title: "Style" }); 158 | styleControl 159 | .addBinding(PARAMS, "gapX", { min: 0, max: 50, step: 1 }) 160 | .on("change", ({ value }) => setGapX(value)); 161 | styleControl 162 | .addBinding(PARAMS, "gapY", { min: 0, max: 50, step: 1 }) 163 | .on("change", ({ value }) => setGapY(value)); 164 | styleControl 165 | .addBinding(PARAMS, "minWidth", { min: 50, max: 300, step: 10 }) 166 | .on("change", ({ value }) => setMinWidth(value)); 167 | styleControl 168 | .addBinding(PARAMS, "maxWidth", { min: 300, max: 500, step: 10 }) 169 | .on("change", ({ value }) => setMaxWidth(value)); 170 | styleControl 171 | .addBinding(PARAMS, "transitionDuration", { 172 | min: 0, 173 | max: 1000, 174 | step: 10, 175 | label: "Duration", 176 | }) 177 | .on("change", ({ value }) => setTransitionDuration(value)); 178 | 179 | const virtualScrollControl = pane.addFolder({ title: "Virtual Scroll" }); 180 | virtualScrollControl.addButton({ title: "Not implemented", label: "TODO" }); 181 | 182 | return () => pane.dispose(); 183 | }, [addItem, batchAdd, batchRemove, fixedHeight, removeItem]); 184 | 185 | const [theme, setTheme] = useState("light"); 186 | 187 | return ( 188 | 189 |
190 | 194 | 195 | 205 | {list.map((item, index) => { 206 | return ( 207 | 208 | 212 | insertItem(randomItem(fixedHeight), index) 213 | } 214 | onInsertAfter={() => 215 | insertItem(randomItem(fixedHeight), index + 1) 216 | } 217 | onRemove={() => removeItem(item.id)} 218 | onMoveBefore={() => moveItem(item.id, -1)} 219 | onMoveAfter={() => moveItem(item.id, 1)} 220 | /> 221 | 222 | ); 223 | })} 224 |
225 | 226 | 227 |
228 | 229 | ); 230 | } 231 | 232 | export default App; 233 | -------------------------------------------------------------------------------- /playground/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /playground/src/components/app-header/index.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes, useEffect, useState } from "react"; 2 | import { ThemeToggle } from "./theme-toggle/theme-toggle"; 3 | import { useTheme } from "../../hooks/use-theme"; 4 | 5 | interface AppHeaderProps extends HTMLAttributes { 6 | debuggerContainerRef: React.RefObject; 7 | } 8 | 9 | export const AppHeader = ({ 10 | debuggerContainerRef, 11 | className, 12 | }: AppHeaderProps) => { 13 | const [showDebugger, setShowDebugger] = useState(false); 14 | const [theme] = useTheme(); 15 | 16 | const avatarUrl = `https://oooo.so/avatar#dark=${theme === "dark"}`; 17 | 18 | useEffect(() => { 19 | if (showDebugger) { 20 | const onClick = (e: MouseEvent) => { 21 | const container = debuggerContainerRef.current; 22 | const target = e.target as HTMLElement; 23 | if (!target.closest(".debugger-root") && !container?.contains(target)) { 24 | setShowDebugger(false); 25 | } 26 | }; 27 | window.addEventListener("click", onClick); 28 | return () => { 29 | window.removeEventListener("click", onClick); 30 | }; 31 | } 32 | }, [debuggerContainerRef, showDebugger]); 33 | 34 | return ( 35 |
38 |
39 |