├── examples └── demo │ ├── src │ ├── vite-env.d.ts │ ├── style.css │ └── main.tsx │ ├── .gitignore │ ├── postcss.config.js │ ├── vite.config.ts │ ├── tailwind.config.js │ ├── index.html │ ├── package.json │ ├── tsconfig.json │ └── favicon.svg ├── packages └── solid-mason │ ├── pridepack.json │ ├── tsconfig.json │ ├── LICENSE │ ├── package.json │ ├── .gitignore │ ├── README.md │ └── src │ └── index.ts ├── pnpm-workspace.yaml ├── images └── solid-mason.png ├── package.json ├── lerna.json ├── LICENSE ├── .gitignore ├── README.md └── biome.json /examples/demo/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/solid-mason/pridepack.json: -------------------------------------------------------------------------------- 1 | { 2 | "target": "es2018" 3 | } 4 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/**/*' 3 | - 'examples/**/*' -------------------------------------------------------------------------------- /examples/demo/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local -------------------------------------------------------------------------------- /images/solid-mason.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lxsmnsyc/solid-mason/HEAD/images/solid-mason.png -------------------------------------------------------------------------------- /examples/demo/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | } 6 | }; -------------------------------------------------------------------------------- /examples/demo/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import solidPlugin from 'vite-plugin-solid'; 3 | 4 | export default defineConfig({ 5 | plugins: [solidPlugin()], 6 | }); 7 | -------------------------------------------------------------------------------- /examples/demo/tailwind.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | mode: 'jit', 3 | content: [ 4 | './src/**/*.tsx', 5 | ], 6 | darkMode: 'class', // or 'media' or 'class' 7 | variants: {}, 8 | plugins: [ 9 | ], 10 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "workspaces": ["packages/*", "examples/*"], 5 | "devDependencies": { 6 | "@biomejs/biome": "^1.5.3", 7 | "lerna": "^8.0.2", 8 | "typescript": "^5.3.3" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmClient": "pnpm", 3 | "packages": [ 4 | "packages/*", 5 | "examples/*" 6 | ], 7 | "command": { 8 | "version": { 9 | "exact": true 10 | }, 11 | "publish": { 12 | "allowBranch": [ 13 | "main" 14 | ], 15 | "registry": "https://registry.npmjs.org/" 16 | } 17 | }, 18 | "version": "0.1.7" 19 | } 20 | -------------------------------------------------------------------------------- /examples/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/demo/src/style.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | @apply overflow-x-hidden; 7 | } 8 | 9 | .child { 10 | width: 100%; 11 | height: 100%; 12 | background-color: black; /* fallback color */ 13 | transition: all .5s; 14 | background-position: center; 15 | background-size: cover; 16 | } 17 | 18 | .parent:hover .child, 19 | .parent:focus .child { 20 | transform: scale(1.2); 21 | } 22 | -------------------------------------------------------------------------------- /examples/demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "0.1.7", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "serve": "vite preview" 9 | }, 10 | "devDependencies": { 11 | "autoprefixer": "^10.4.17", 12 | "postcss": "^8.4.33", 13 | "typescript": "^5.3.3", 14 | "vite": "^5.0.12", 15 | "vite-plugin-solid": "^2.9.1" 16 | }, 17 | "dependencies": { 18 | "solid-js": "^1.8.12", 19 | "solid-mason": "0.1.7", 20 | "tailwindcss": "^3.4.1" 21 | }, 22 | "private": true, 23 | "publishConfig": { 24 | "access": "restricted" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["node_modules"], 3 | "include": ["src", "types"], 4 | "compilerOptions": { 5 | "module": "ESNext", 6 | "lib": ["ESNext", "DOM"], 7 | "importHelpers": true, 8 | "declaration": true, 9 | "sourceMap": true, 10 | "rootDir": "./src", 11 | "strict": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "moduleResolution": "node", 17 | "jsx": "preserve", 18 | "jsxImportSource": "solid-js", 19 | "esModuleInterop": true, 20 | "target": "ES2017" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/solid-mason/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["node_modules"], 3 | "include": ["src", "types"], 4 | "compilerOptions": { 5 | "module": "ESNext", 6 | "lib": ["ESNext", "DOM"], 7 | "importHelpers": true, 8 | "declaration": true, 9 | "sourceMap": true, 10 | "rootDir": "./src", 11 | "strict": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "moduleResolution": "Bundler", 17 | "jsx": "preserve", 18 | "jsxImportSource": "solid-js", 19 | "esModuleInterop": true, 20 | "target": "ES2017", 21 | "useDefineForClassFields": false, 22 | "declarationMap": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Alexis Munsayac 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 | -------------------------------------------------------------------------------- /packages/solid-mason/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Alexis Munsayac 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # parcel-bundler cache (https://parceljs.org/) 61 | .cache 62 | 63 | # next.js build output 64 | .next 65 | 66 | # nuxt.js build output 67 | .nuxt 68 | 69 | # vuepress build output 70 | .vuepress/dist 71 | 72 | # Serverless directories 73 | .serverless 74 | 75 | # FuseBox cache 76 | .fusebox/ 77 | -------------------------------------------------------------------------------- /examples/demo/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/solid-mason/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solid-mason", 3 | "type": "module", 4 | "version": "0.1.7", 5 | "files": [ 6 | "dist", 7 | "src" 8 | ], 9 | "engines": { 10 | "node": ">=10" 11 | }, 12 | "license": "MIT", 13 | "keywords": [ 14 | "pridepack" 15 | ], 16 | "devDependencies": { 17 | "@types/node": "^20.11.5", 18 | "pridepack": "2.6.0", 19 | "solid-js": "^1.8.12", 20 | "tslib": "^2.6.2", 21 | "typescript": "^5.3.3" 22 | }, 23 | "peerDependencies": { 24 | "solid-js": "^1.6" 25 | }, 26 | "dependencies": { 27 | "solid-use": "^0.8.0" 28 | }, 29 | "scripts": { 30 | "prepublishOnly": "pridepack clean && pridepack build", 31 | "build": "pridepack build", 32 | "type-check": "pridepack check", 33 | "lint": "pridepack lint", 34 | "clean": "pridepack clean", 35 | "watch": "pridepack watch", 36 | "start": "pridepack start", 37 | "dev": "pridepack dev", 38 | "test": "vitest" 39 | }, 40 | "description": "Masonry layout for SolidJS", 41 | "repository": { 42 | "url": "https://github.com/lxsmnsyc/solid-mason.git", 43 | "type": "git" 44 | }, 45 | "homepage": "https://github.com/lxsmnsyc/solid-mason/tree/main/packages/solid-mason", 46 | "bugs": { 47 | "url": "https://github.com/lxsmnsyc/solid-mason/issues" 48 | }, 49 | "publishConfig": { 50 | "access": "public" 51 | }, 52 | "author": "Alexis Munsayac", 53 | "private": false, 54 | "types": "./dist/types/index.d.ts", 55 | "main": "./dist/cjs/production/index.cjs", 56 | "module": "./dist/esm/production/index.mjs", 57 | "exports": { 58 | ".": { 59 | "development": { 60 | "require": "./dist/cjs/development/index.cjs", 61 | "import": "./dist/esm/development/index.mjs" 62 | }, 63 | "require": "./dist/cjs/production/index.cjs", 64 | "import": "./dist/esm/production/index.mjs", 65 | "types": "./dist/types/index.d.ts" 66 | } 67 | }, 68 | "typesVersions": { 69 | "*": {} 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/solid-mason/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.production 74 | .env.development 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | 107 | .npmrc 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # solid-mason 2 | 3 | > Simple masonry layout in SolidJS 4 | 5 | [![NPM](https://img.shields.io/npm/v/solid-mason.svg)](https://www.npmjs.com/package/solid-mason) [![JavaScript Style Guide](https://badgen.net/badge/code%20style/airbnb/ff5a5f?icon=airbnb)](https://github.com/airbnb/javascript) [![Open in StackBlitz](https://img.shields.io/badge/Open%20in-StackBlitz-blue?style=flat-square&logo=stackblitz)](https://stackblitz.com//github/lxsmnsyc/solid-mason/tree/main/examples/demo) 6 | 7 |

8 | Example 13 |

14 | 15 | ## Install 16 | 17 | ```bash 18 | npm install --save solid-mason 19 | ``` 20 | 21 | ```bash 22 | yarn add solid-mason 23 | ``` 24 | 25 | ```bash 26 | pnpm add solid-mason 27 | ``` 28 | 29 | ## Usage 30 | 31 | ### Basic example 32 | 33 | ```js 34 | import { Mason } from 'solid-mason'; 35 | 36 | 41 | {(item, index) => } 42 | 43 | ``` 44 | 45 | ### Breakpoints example 46 | 47 | ```js 48 | import { Mason, createMasonryBreakpoints } from 'solid-mason'; 49 | 50 | const breakpoints = createMasonryBreakpoints(() => [ 51 | { query: '(min-width: 1536px)', columns: 6 }, 52 | { query: '(min-width: 1280px) and (max-width: 1536px)', columns: 5 }, 53 | { query: '(min-width: 1024px) and (max-width: 1280px)', columns: 4 }, 54 | { query: '(min-width: 768px) and (max-width: 1024px)', columns: 3 }, 55 | { query: '(max-width: 768px)', columns: 2 }, 56 | ]); 57 | 58 | 63 | {(item, index) => } 64 | 65 | ``` 66 | 67 | ## Notes 68 | 69 | - Masonry's layout order is based on the shortest column at the time a new element is being inserted. 70 | - Each children must have a definite height on initial paint. Elements, like images, that changes height dynamically won't be re-adjused automatically by the mansory container. 71 | 72 | ## Sponsors 73 | 74 | ![Sponsors](https://github.com/lxsmnsyc/sponsors/blob/main/sponsors.svg?raw=true) 75 | 76 | ## License 77 | 78 | MIT © [lxsmnsyc](https://github.com/lxsmnsyc) 79 | -------------------------------------------------------------------------------- /packages/solid-mason/README.md: -------------------------------------------------------------------------------- 1 | # solid-mason 2 | 3 | > Simple masonry layout in SolidJS 4 | 5 | [![NPM](https://img.shields.io/npm/v/solid-mason.svg)](https://www.npmjs.com/package/solid-mason) [![JavaScript Style Guide](https://badgen.net/badge/code%20style/airbnb/ff5a5f?icon=airbnb)](https://github.com/airbnb/javascript) [![Open in StackBlitz](https://img.shields.io/badge/Open%20in-StackBlitz-blue?style=flat-square&logo=stackblitz)](https://stackblitz.com//github/lxsmnsyc/solid-mason/tree/main/examples/demo) 6 | 7 |

8 | Example 13 |

14 | 15 | ## Install 16 | 17 | ```bash 18 | npm install --save solid-mason 19 | ``` 20 | 21 | ```bash 22 | yarn add solid-mason 23 | ``` 24 | 25 | ```bash 26 | pnpm add solid-mason 27 | ``` 28 | 29 | ## Usage 30 | 31 | ### Basic example 32 | 33 | ```js 34 | import { Mason } from 'solid-mason'; 35 | 36 | 41 | {(item, index) => } 42 | 43 | ``` 44 | 45 | ### Breakpoints example 46 | 47 | ```js 48 | import { Mason, createMasonryBreakpoints } from 'solid-mason'; 49 | 50 | const breakpoints = createMasonryBreakpoints(() => [ 51 | { query: '(min-width: 1536px)', columns: 6 }, 52 | { query: '(min-width: 1280px) and (max-width: 1536px)', columns: 5 }, 53 | { query: '(min-width: 1024px) and (max-width: 1280px)', columns: 4 }, 54 | { query: '(min-width: 768px) and (max-width: 1024px)', columns: 3 }, 55 | { query: '(max-width: 768px)', columns: 2 }, 56 | ]); 57 | 58 | 63 | {(item, index) => } 64 | 65 | ``` 66 | 67 | ## Notes 68 | 69 | - Masonry's layout order is based on the shortest column at the time a new element is being inserted. 70 | - Each children must have a definite height on initial paint. Elements, like images, that changes height dynamically won't be re-adjused automatically by the mansory container. 71 | 72 | ## Sponsors 73 | 74 | ![Sponsors](https://github.com/lxsmnsyc/sponsors/blob/main/sponsors.svg?raw=true) 75 | 76 | ## License 77 | 78 | MIT © [lxsmnsyc](https://github.com/lxsmnsyc) 79 | -------------------------------------------------------------------------------- /examples/demo/src/main.tsx: -------------------------------------------------------------------------------- 1 | import type { JSX } from 'solid-js'; 2 | import { createSignal, onCleanup, onMount } from 'solid-js'; 3 | import { render } from 'solid-js/web'; 4 | import { Mason, createMasonryBreakpoints } from 'solid-mason'; 5 | import './style.css'; 6 | 7 | const HORIZONTAL_ASPECT_RATIO = [ 8 | { width: 4, height: 4 }, // Square 9 | { width: 4, height: 3 }, // Standard Fullscreen 10 | { width: 16, height: 10 }, // Standard LCD 11 | { width: 16, height: 9 }, // HD 12 | // { width: 37, height: 20 }, // Widescreen 13 | { width: 6, height: 3 }, // Univisium 14 | { width: 21, height: 9 }, // Anamorphic 2.35:1 15 | // { width: 64, height: 27 }, // Anamorphic 2.39:1 or 2.37:1 16 | { width: 19, height: 16 }, // Movietone 17 | { width: 5, height: 4 }, // 17' LCD CRT 18 | // { width: 48, height: 35 }, // 16mm and 35mm 19 | { width: 11, height: 8 }, // 35mm full sound 20 | // { width: 143, height: 100 }, // IMAX 21 | { width: 6, height: 4 }, // 35mm photo 22 | { width: 14, height: 9 }, // commercials 23 | { width: 5, height: 3 }, // Paramount 24 | { width: 7, height: 4 }, // early 35mm 25 | { width: 11, height: 5 }, // 70mm 26 | { width: 12, height: 5 }, // Bluray 27 | { width: 8, height: 3 }, // Super 16 28 | { width: 18, height: 5 }, // IMAX 29 | { width: 12, height: 3 }, // Polyvision 30 | ]; 31 | 32 | const VERTICAL_ASPECT_RATIO = HORIZONTAL_ASPECT_RATIO.map(item => ({ 33 | width: item.height, 34 | height: item.width, 35 | })); 36 | 37 | const ASPECT_RATIO = [...HORIZONTAL_ASPECT_RATIO, ...VERTICAL_ASPECT_RATIO].map( 38 | item => ({ 39 | width: item.width * 50, 40 | height: item.height * 50, 41 | }), 42 | ); 43 | 44 | interface Item { 45 | id: number; 46 | width: number; 47 | height: number; 48 | } 49 | 50 | function createNewImage(id: number): Item { 51 | const randomAspectRatio = 52 | ASPECT_RATIO[Math.floor(Math.random() * ASPECT_RATIO.length)]; 53 | 54 | return { 55 | ...randomAspectRatio, 56 | id, 57 | }; 58 | } 59 | 60 | function Root(): JSX.Element { 61 | const [items, setItems] = createSignal([]); 62 | 63 | function addItems() { 64 | setItems(current => { 65 | const newData = [...current]; 66 | 67 | for (let i = 0; i < 20; i += 1) { 68 | newData.push(createNewImage(newData.length + 1)); 69 | } 70 | 71 | return newData; 72 | }); 73 | } 74 | 75 | function onScroll() { 76 | if (window.innerHeight + window.scrollY >= document.body.offsetHeight) { 77 | addItems(); 78 | } 79 | } 80 | 81 | onMount(() => { 82 | addItems(); 83 | 84 | document.addEventListener('scroll', onScroll, { 85 | passive: true, 86 | }); 87 | 88 | onCleanup(() => { 89 | document.removeEventListener('scroll', onScroll); 90 | }); 91 | }); 92 | 93 | const breakpoints = createMasonryBreakpoints(() => [ 94 | { query: '(min-width: 1536px)', columns: 6 }, 95 | { query: '(min-width: 1280px) and (max-width: 1536px)', columns: 5 }, 96 | { query: '(min-width: 1024px) and (max-width: 1280px)', columns: 4 }, 97 | { query: '(min-width: 768px) and (max-width: 1024px)', columns: 3 }, 98 | { query: '(max-width: 768px)', columns: 2 }, 99 | ]); 100 | 101 | return ( 102 |
103 | 104 | {item => ( 105 |
106 |
110 |
116 | {`Image no. ${item.id}`} 117 |
118 |
119 |
120 | )} 121 |
122 |
123 | ); 124 | } 125 | 126 | const app = document.getElementById('app'); 127 | 128 | if (app) { 129 | render(() => , app); 130 | } 131 | -------------------------------------------------------------------------------- /packages/solid-mason/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { JSX } from 'solid-js'; 2 | import { 3 | For, 4 | createEffect, 5 | createMemo, 6 | createSignal, 7 | mergeProps, 8 | on, 9 | onCleanup, 10 | } from 'solid-js'; 11 | import type { DynamicProps } from 'solid-js/web'; 12 | import { Dynamic } from 'solid-js/web'; 13 | import { omitProps } from 'solid-use/props'; 14 | 15 | type OmitAndMerge = T & Omit; 16 | 17 | type MasonProps< 18 | Data, 19 | T extends keyof JSX.HTMLElementTags = 'div', 20 | > = OmitAndMerge< 21 | { 22 | as?: T; 23 | columns: number; 24 | items?: Data[] | null | undefined; 25 | children: (item: Data, index: () => number) => JSX.Element; 26 | style?: JSX.CSSProperties | string; 27 | }, 28 | JSX.HTMLElementTags[T] 29 | >; 30 | 31 | function getShortestColumn(columns: number[]): number { 32 | let min = 0; 33 | let record = Number.MAX_SAFE_INTEGER; 34 | 35 | for (let i = 0, len = columns.length; i < len; i += 1) { 36 | if (columns[i] < record) { 37 | record = columns[i]; 38 | min = i; 39 | } 40 | } 41 | 42 | return min; 43 | } 44 | 45 | function getLongestColumn(columns: number[]): number { 46 | let min = 0; 47 | let record = 0; 48 | 49 | for (let i = 0, len = columns.length; i < len; i += 1) { 50 | if (columns[i] > record) { 51 | record = columns[i]; 52 | min = i; 53 | } 54 | } 55 | 56 | return min; 57 | } 58 | 59 | const MASON_STYLE: JSX.CSSProperties = { 60 | position: 'relative', 61 | width: '100%', 62 | 'max-width': '100%', 63 | }; 64 | 65 | const MASON_STYLE_STRING = ';position:relative;width:100%;max-width:100%;'; 66 | 67 | const MASON_KEY = 'data-solid-mason'; 68 | 69 | function getContentWidth(el: HTMLElement | SVGAElement): number { 70 | const styles = getComputedStyle(el); 71 | 72 | return ( 73 | el.clientWidth - 74 | Number.parseFloat(styles.paddingLeft) - 75 | Number.parseFloat(styles.paddingRight) 76 | ); 77 | } 78 | 79 | interface MasonState { 80 | width: number; 81 | columns: number[]; 82 | elements: HTMLElement[]; 83 | heights: number[]; 84 | } 85 | 86 | function createMason(el: HTMLElement, state: MasonState): void { 87 | // Get computed style 88 | const columnCount = state.columns.length; 89 | const containerWidth = getContentWidth(el); 90 | let isAllDirty = containerWidth !== state.width; 91 | const widthPerColumn = containerWidth / columnCount; 92 | 93 | const newColumns: number[] = new Array(state.columns.length).fill(0); 94 | if (isAllDirty) { 95 | state.columns = [...newColumns]; 96 | } 97 | 98 | let node = el.firstElementChild; 99 | let nodeIndex = 0; 100 | 101 | while (node) { 102 | if (node instanceof HTMLElement) { 103 | const targetColumn = getShortestColumn(newColumns); 104 | if (isAllDirty || state.elements[nodeIndex] !== node) { 105 | // Set the width of the node 106 | node.style.width = `${widthPerColumn}px`; 107 | // Set the position 108 | node.style.position = 'absolute'; 109 | // Set the top/left 110 | const currentColumnHeight = newColumns[targetColumn]; 111 | node.style.top = `${currentColumnHeight}px`; 112 | node.style.left = `${targetColumn * widthPerColumn}px`; 113 | // Increase column height 114 | node.getBoundingClientRect(); 115 | const nodeHeight = node.offsetHeight; 116 | state.elements[nodeIndex] = node; 117 | state.heights[nodeIndex] = nodeHeight; 118 | isAllDirty = true; 119 | newColumns[targetColumn] = currentColumnHeight + nodeHeight; 120 | } else { 121 | newColumns[targetColumn] += state.heights[nodeIndex]; 122 | } 123 | nodeIndex += 1; 124 | } 125 | node = node.nextElementSibling; 126 | } 127 | const targetColumn = getLongestColumn(newColumns); 128 | const currentColumnHeight = newColumns[targetColumn]; 129 | el.style.height = `${currentColumnHeight}px`; 130 | state.width = containerWidth; 131 | state.columns = newColumns; 132 | } 133 | 134 | function createRAFDebounce(callback: () => void): () => void { 135 | let timeout: number; 136 | 137 | onCleanup(() => { 138 | cancelAnimationFrame(timeout); 139 | }); 140 | 141 | return () => { 142 | if (timeout) { 143 | cancelAnimationFrame(timeout); 144 | } 145 | 146 | timeout = requestAnimationFrame(() => { 147 | callback(); 148 | }); 149 | }; 150 | } 151 | const MEDIA = new Map(); 152 | 153 | function getMediaMatcher(query: string): MediaQueryList { 154 | const media = MEDIA.get(query); 155 | if (media) { 156 | return media; 157 | } 158 | const newMedia = window.matchMedia(query); 159 | MEDIA.set(query, newMedia); 160 | return newMedia; 161 | } 162 | 163 | export interface MasonryBreakpoint { 164 | query: string; 165 | columns: number; 166 | } 167 | 168 | export function createMasonryBreakpoints( 169 | breakpoints: () => MasonryBreakpoint[], 170 | defaultColumns = 1, 171 | ): () => number { 172 | const [columns, setColumns] = createSignal(defaultColumns); 173 | 174 | createEffect(() => { 175 | const br = breakpoints(); 176 | for (let i = 0, len = br.length; i < len; i += 1) { 177 | const item = br[i]; 178 | createMemo(() => { 179 | const media = getMediaMatcher(item.query); 180 | const callback = (): void => { 181 | if (media.matches) { 182 | setColumns(item.columns); 183 | } 184 | }; 185 | callback(); 186 | media.addEventListener('change', callback, false); 187 | onCleanup(() => { 188 | media.removeEventListener('change', callback, false); 189 | }); 190 | }); 191 | } 192 | }); 193 | 194 | return columns; 195 | } 196 | 197 | export function Mason( 198 | props: MasonProps, 199 | ): JSX.Element { 200 | const [ref, setRef] = createSignal(); 201 | 202 | createEffect(() => { 203 | const el = ref(); 204 | if (el) { 205 | // Set style 206 | el.style.position = 'relative'; 207 | el.style.width = '100%'; 208 | el.style.maxWidth = '100%'; 209 | 210 | const state: MasonState = { 211 | width: 0, 212 | columns: new Array(props.columns).fill(0), 213 | elements: [], 214 | heights: [], 215 | }; 216 | 217 | const recalculate = createRAFDebounce(() => { 218 | createMason(el, state); 219 | }); 220 | 221 | createMemo( 222 | on( 223 | () => props.items, 224 | () => { 225 | recalculate(); 226 | }, 227 | ), 228 | ); 229 | 230 | // Track window resize 231 | window.addEventListener('resize', recalculate, { passive: true }); 232 | onCleanup(() => { 233 | window.removeEventListener('resize', recalculate); 234 | }); 235 | 236 | // Track child mutations 237 | const observer = new MutationObserver(mutations => { 238 | if (mutations.length) { 239 | recalculate(); 240 | } 241 | }); 242 | 243 | observer.observe(el, { 244 | childList: true, 245 | }); 246 | 247 | onCleanup(() => { 248 | observer.disconnect(); 249 | }); 250 | } 251 | }); 252 | 253 | return Dynamic( 254 | mergeProps( 255 | { 256 | get component() { 257 | return props.as ?? 'div'; 258 | }, 259 | ref: setRef, 260 | get children() { 261 | return For({ 262 | get each() { 263 | return props.items; 264 | }, 265 | children(item, index) { 266 | return Dynamic({ 267 | component: 'div', 268 | get children() { 269 | return props.children(item, index); 270 | }, 271 | style: { 272 | position: 'absolute', 273 | }, 274 | }); 275 | }, 276 | }); 277 | }, 278 | get [MASON_KEY]() { 279 | return props.columns; 280 | }, 281 | get style() { 282 | const current = props.style; 283 | if (typeof current === 'string') { 284 | return `${current}${MASON_STYLE_STRING}`; 285 | } 286 | if (current) { 287 | return { ...current, MASON_STYLE }; 288 | } 289 | return MASON_STYLE_STRING; 290 | }, 291 | }, 292 | omitProps(props, ['as', 'children', 'columns', 'items', 'style']), 293 | ) as unknown as DynamicProps, 294 | ); 295 | } 296 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@biomejs/biome/configuration_schema.json", 3 | "files": { 4 | "ignore": ["node_modules/**/*"] 5 | }, 6 | "vcs": { 7 | "useIgnoreFile": true 8 | }, 9 | "linter": { 10 | "enabled": true, 11 | "ignore": ["node_modules/**/*"], 12 | "rules": { 13 | "a11y": { 14 | "noAccessKey": "error", 15 | "noAriaHiddenOnFocusable": "off", 16 | "noAriaUnsupportedElements": "error", 17 | "noAutofocus": "error", 18 | "noBlankTarget": "error", 19 | "noDistractingElements": "error", 20 | "noHeaderScope": "error", 21 | "noInteractiveElementToNoninteractiveRole": "error", 22 | "noNoninteractiveElementToInteractiveRole": "error", 23 | "noNoninteractiveTabindex": "error", 24 | "noPositiveTabindex": "error", 25 | "noRedundantAlt": "error", 26 | "noRedundantRoles": "error", 27 | "noSvgWithoutTitle": "error", 28 | "useAltText": "error", 29 | "useAnchorContent": "error", 30 | "useAriaActivedescendantWithTabindex": "error", 31 | "useAriaPropsForRole": "error", 32 | "useButtonType": "error", 33 | "useHeadingContent": "error", 34 | "useHtmlLang": "error", 35 | "useIframeTitle": "warn", 36 | "useKeyWithClickEvents": "warn", 37 | "useKeyWithMouseEvents": "warn", 38 | "useMediaCaption": "error", 39 | "useValidAnchor": "error", 40 | "useValidAriaProps": "error", 41 | "useValidAriaRole": "error", 42 | "useValidAriaValues": "error", 43 | "useValidLang": "error" 44 | }, 45 | "complexity": { 46 | "noBannedTypes": "error", 47 | "noExcessiveCognitiveComplexity": "error", 48 | "noExtraBooleanCast": "error", 49 | "noForEach": "error", 50 | "noMultipleSpacesInRegularExpressionLiterals": "warn", 51 | "noStaticOnlyClass": "error", 52 | "noThisInStatic": "error", 53 | "noUselessCatch": "error", 54 | "noUselessConstructor": "error", 55 | "noUselessEmptyExport": "error", 56 | "noUselessFragments": "error", 57 | "noUselessLabel": "error", 58 | "noUselessRename": "error", 59 | "noUselessSwitchCase": "error", 60 | "noUselessThisAlias": "error", 61 | "noUselessTypeConstraint": "error", 62 | "noVoid": "off", 63 | "noWith": "error", 64 | "useArrowFunction": "error", 65 | "useFlatMap": "error", 66 | "useLiteralKeys": "error", 67 | "useOptionalChain": "warn", 68 | "useRegexLiterals": "error", 69 | "useSimpleNumberKeys": "error", 70 | "useSimplifiedLogicExpression": "error" 71 | }, 72 | "correctness": { 73 | "noChildrenProp": "error", 74 | "noConstantCondition": "error", 75 | "noConstAssign": "error", 76 | "noConstructorReturn": "error", 77 | "noEmptyCharacterClassInRegex": "error", 78 | "noEmptyPattern": "error", 79 | "noGlobalObjectCalls": "error", 80 | "noInnerDeclarations": "error", 81 | "noInvalidConstructorSuper": "error", 82 | "noInvalidNewBuiltin": "error", 83 | "noNewSymbol": "error", 84 | "noNonoctalDecimalEscape": "error", 85 | "noPrecisionLoss": "error", 86 | "noRenderReturnValue": "error", 87 | "noSelfAssign": "error", 88 | "noSetterReturn": "error", 89 | "noStringCaseMismatch": "error", 90 | "noSwitchDeclarations": "error", 91 | "noUndeclaredVariables": "error", 92 | "noUnnecessaryContinue": "error", 93 | "noUnreachable": "error", 94 | "noUnreachableSuper": "error", 95 | "noUnsafeFinally": "error", 96 | "noUnsafeOptionalChaining": "error", 97 | "noUnusedLabels": "error", 98 | "noUnusedVariables": "error", 99 | "noVoidElementsWithChildren": "error", 100 | "noVoidTypeReturn": "error", 101 | "useExhaustiveDependencies": "error", 102 | "useHookAtTopLevel": "error", 103 | "useIsNan": "error", 104 | "useValidForDirection": "error", 105 | "useYield": "error" 106 | }, 107 | "performance": { 108 | "noAccumulatingSpread": "error", 109 | "noDelete": "off" 110 | }, 111 | "security": { 112 | "noDangerouslySetInnerHtml": "error", 113 | "noDangerouslySetInnerHtmlWithChildren": "error" 114 | }, 115 | "style": { 116 | "noArguments": "error", 117 | "noCommaOperator": "off", 118 | "noDefaultExport": "off", 119 | "noImplicitBoolean": "error", 120 | "noInferrableTypes": "error", 121 | "noNamespace": "error", 122 | "noNegationElse": "error", 123 | "noNonNullAssertion": "off", 124 | "noParameterAssign": "off", 125 | "noParameterProperties": "off", 126 | "noRestrictedGlobals": "error", 127 | "noShoutyConstants": "error", 128 | "noUnusedTemplateLiteral": "error", 129 | "noUselessElse": "error", 130 | "noVar": "error", 131 | "useAsConstAssertion": "error", 132 | "useBlockStatements": "error", 133 | "useCollapsedElseIf": "error", 134 | "useConst": "error", 135 | "useDefaultParameterLast": "error", 136 | "useEnumInitializers": "error", 137 | "useExponentiationOperator": "error", 138 | "useFragmentSyntax": "error", 139 | "useLiteralEnumMembers": "error", 140 | "useNamingConvention": "off", 141 | "useNumericLiterals": "error", 142 | "useSelfClosingElements": "error", 143 | "useShorthandArrayType": "error", 144 | "useShorthandAssign": "error", 145 | "useSingleCaseStatement": "error", 146 | "useSingleVarDeclarator": "error", 147 | "useTemplate": "off", 148 | "useWhile": "error" 149 | }, 150 | "suspicious": { 151 | "noApproximativeNumericConstant": "error", 152 | "noArrayIndexKey": "error", 153 | "noAssignInExpressions": "error", 154 | "noAsyncPromiseExecutor": "error", 155 | "noCatchAssign": "error", 156 | "noClassAssign": "error", 157 | "noCommentText": "error", 158 | "noCompareNegZero": "error", 159 | "noConfusingLabels": "error", 160 | "noConfusingVoidType": "error", 161 | "noConsoleLog": "warn", 162 | "noConstEnum": "off", 163 | "noControlCharactersInRegex": "error", 164 | "noDebugger": "off", 165 | "noDoubleEquals": "error", 166 | "noDuplicateCase": "error", 167 | "noDuplicateClassMembers": "error", 168 | "noDuplicateJsxProps": "error", 169 | "noDuplicateObjectKeys": "error", 170 | "noDuplicateParameters": "error", 171 | "noEmptyInterface": "error", 172 | "noExplicitAny": "warn", 173 | "noExtraNonNullAssertion": "error", 174 | "noFallthroughSwitchClause": "error", 175 | "noFunctionAssign": "error", 176 | "noGlobalIsFinite": "error", 177 | "noGlobalIsNan": "error", 178 | "noImplicitAnyLet": "off", 179 | "noImportAssign": "error", 180 | "noLabelVar": "error", 181 | "noMisleadingInstantiator": "error", 182 | "noMisrefactoredShorthandAssign": "off", 183 | "noPrototypeBuiltins": "error", 184 | "noRedeclare": "error", 185 | "noRedundantUseStrict": "error", 186 | "noSelfCompare": "off", 187 | "noShadowRestrictedNames": "error", 188 | "noSparseArray": "off", 189 | "noUnsafeDeclarationMerging": "error", 190 | "noUnsafeNegation": "error", 191 | "useDefaultSwitchClauseLast": "error", 192 | "useGetterReturn": "error", 193 | "useIsArray": "error", 194 | "useNamespaceKeyword": "error", 195 | "useValidTypeof": "error" 196 | }, 197 | "nursery": { 198 | "noDuplicateJsonKeys": "off", 199 | "noEmptyBlockStatements": "error", 200 | "noEmptyTypeParameters": "error", 201 | "noGlobalEval": "off", 202 | "noGlobalAssign": "error", 203 | "noInvalidUseBeforeDeclaration": "error", 204 | "noMisleadingCharacterClass": "error", 205 | "noNodejsModules": "off", 206 | "noThenProperty": "warn", 207 | "noUnusedImports": "error", 208 | "noUnusedPrivateClassMembers": "error", 209 | "noUselessLoneBlockStatements": "error", 210 | "noUselessTernary": "error", 211 | "useAwait": "error", 212 | "useConsistentArrayType": "error", 213 | "useExportType": "error", 214 | "useFilenamingConvention": "off", 215 | "useForOf": "warn", 216 | "useGroupedTypeImport": "error", 217 | "useImportRestrictions": "off", 218 | "useImportType": "error", 219 | "useNodejsImportProtocol": "warn", 220 | "useNumberNamespace": "error", 221 | "useShorthandFunctionType": "warn" 222 | } 223 | } 224 | }, 225 | "formatter": { 226 | "enabled": true, 227 | "ignore": ["node_modules/**/*"], 228 | "formatWithErrors": false, 229 | "indentWidth": 2, 230 | "indentStyle": "space", 231 | "lineEnding": "lf", 232 | "lineWidth": 80 233 | }, 234 | "organizeImports": { 235 | "enabled": true, 236 | "ignore": ["node_modules/**/*"] 237 | }, 238 | "javascript": { 239 | "formatter": { 240 | "enabled": true, 241 | "arrowParentheses": "asNeeded", 242 | "bracketSameLine": false, 243 | "bracketSpacing": true, 244 | "indentWidth": 2, 245 | "indentStyle": "space", 246 | "jsxQuoteStyle": "double", 247 | "lineEnding": "lf", 248 | "lineWidth": 80, 249 | "quoteProperties": "asNeeded", 250 | "quoteStyle": "single", 251 | "semicolons": "always", 252 | "trailingComma": "all" 253 | }, 254 | "globals": [], 255 | "parser": { 256 | "unsafeParameterDecoratorsEnabled": true 257 | } 258 | }, 259 | "json": { 260 | "formatter": { 261 | "enabled": true, 262 | "indentWidth": 2, 263 | "indentStyle": "space", 264 | "lineEnding": "lf", 265 | "lineWidth": 80 266 | }, 267 | "parser": { 268 | "allowComments": false, 269 | "allowTrailingCommas": false 270 | } 271 | } 272 | } 273 | --------------------------------------------------------------------------------