├── 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 | [](https://www.npmjs.com/package/solid-mason) [](https://github.com/airbnb/javascript) [](https://stackblitz.com//github/lxsmnsyc/solid-mason/tree/main/examples/demo)
6 |
7 |
8 |
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 | 
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 | [](https://www.npmjs.com/package/solid-mason) [](https://github.com/airbnb/javascript) [](https://stackblitz.com//github/lxsmnsyc/solid-mason/tree/main/examples/demo)
6 |
7 |
8 |
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 | 
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 |
--------------------------------------------------------------------------------