Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/bin/sh
#
# Runs before every commit. Kept under a couple of seconds, because a hook that
# is slow is a hook people disable.
#
# Two checks, both chosen because they fail *later* in a way that does not look
# like their cause:
#
# Formatting — CI runs `prettier --check` and fails the whole run on a stray
# space, several minutes after the push.
#
# Lock file — if package.json and package-lock.json disagree, `npm ci` refuses
# to install, so every CI job and every Vercel deploy dies at the install step
# before anything is tested. The error talks about the lock file, not about
# the dependency somebody added.
#
# Everything slower lives in .githooks/pre-push and in CI.
#
# To skip deliberately: git commit --no-verify

set -e

root=$(git rev-parse --show-toplevel)
cd "$root"

# --- Formatting, on the staged files only ------------------------------------
#
# Note this checks the file on disk, not the staged blob. If you stage a tidy
# version and then make a mess without staging it, this complains about the mess
# — which is the more useful of the two answers.

staged=$(git diff --cached --name-only --diff-filter=ACMR -- 'projects/***' |
grep -E '\.(ts|html|css|json)$' || true)

if [ -n "$staged" ]; then
# `xargs -d` is GNU-only; macOS ships BSD xargs, where it does not exist.
# NUL separation is the portable way to survive spaces in file names.
if ! printf '%s\n' "$staged" | tr '\n' '\0' | xargs -0 npx --no-install prettier --check; then
echo
echo " Fix it with: npm run format"
exit 1
fi
fi

# --- package.json and package-lock.json must agree ---------------------------

node scripts/verify-lockfile.mjs
41 changes: 41 additions & 0 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/bin/sh
#
# Runs before every push: the checks that must never be red on a branch someone
# else can see. Around half a minute.
#
# This is deliberately not the whole of CI. Server rendering, the prerendered
# pages, the documentation examples, native CSS masonry and the Angular version
# matrix all need a browser or several full builds, and a hook that takes four
# minutes gets bypassed. Those stay in CI, where waiting costs nobody anything.
#
# What is here is the set that catches an ordinary mistake:
#
# lock file — otherwise `npm ci` refuses and every job dies before testing
# tests — 185 of them, under three seconds
# build — the library has to compile at all
# zero deps — the claim on the README, checkable in ten seconds by a stranger
#
# To skip deliberately: git push --no-verify

set -e

root=$(git rev-parse --show-toplevel)
cd "$root"

echo "pre-push: lock file"
node scripts/verify-lockfile.mjs

echo
echo "pre-push: unit tests"
npm test

echo
echo "pre-push: library build"
npm run build:lib >/dev/null

echo
echo "pre-push: no runtime dependencies"
node scripts/verify-deps.mjs

echo
echo "pre-push: ok"
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ jobs:
with:
node-version: 24
cache: npm

# Before installing, not after: when package.json and package-lock.json
# disagree, `npm ci` fails with EUSAGE and every job dies before anything
# is tested. This says which dependency, and what to run.
- name: Lock file is in sync
run: node scripts/verify-lockfile.mjs

- run: npm ci

- name: Formatting
Expand Down
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ Tests, unlike the demo, run against the sources directly, so `npm test` needs no
| `npm run size:features` | Measure what each feature costs, by ablation. |
| `npm run size:app` | Build an app with and without the grid, and subtract. The number to quote. |
| `npm run verify:prerender` | Every route ships its text as HTML, and the sitemap matches. |
| `npm run og` | Redraw the social preview card. Run it when the wording or the numbers change. |
| `npm run verify:lockfile` | package.json and package-lock.json agree, so `npm ci` will not refuse to install. |
| `npm run ng` | The raw Angular CLI, for anything the scripts above do not cover. |

Three numbers worth knowing, all produced by scripts above rather than estimated: the library's own
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

Angular workspace for the **masonry-angular** library and its example app.

**[Live demo](https://masonry-angular.vercel.app)** ·
**[npm](https://www.npmjs.com/package/masonry-angular)** ·
**[GitHub](https://github.com/MeAkib/masonry-angular)** ·
**[StackBlitz](https://stackblitz.com/github/MeAkib/masonry-angular/tree/main/examples/stackblitz)**
Expand Down Expand Up @@ -35,7 +36,7 @@ working on both.
| | |
| --- | --- |
| [Open in StackBlitz](https://stackblitz.com/github/MeAkib/masonry-angular/tree/main/examples/stackblitz) | A minimal grid, installing `masonry-angular` from npm. Boots in the browser, nothing to set up. |
| Demo app | The five examples in `projects/demo`, deployed to Vercel. <!-- add the URL after the first deploy --> |
| [Demo app](https://masonry-angular.vercel.app) | The five examples in `projects/demo`, prerendered and deployed to Vercel. |

### Deploying

Expand Down
1 change: 1 addition & 0 deletions llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ Under native layout the browser owns packing, so stamps and `masonryIgnore` have

## Examples

- [Live demo](https://masonry-angular.vercel.app): Five worked examples — gallery, spans and stamps, dashboard, dynamic items, 1,000 items — each showing its source.
- [Runnable starter](https://stackblitz.com/github/MeAkib/masonry-angular/tree/main/examples/stackblitz): A StackBlitz project that installs the published package from npm.
- [Starter source](https://raw.githubusercontent.com/MeAkib/masonry-angular/main/examples/stackblitz/src/main.ts): The same example as a single file — responsive columns, spans, add/remove/reorder.

Expand Down
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "0.0.1",
"private": true,
"scripts": {
"prepare": "git config core.hooksPath .githooks || true",
"ng": "ng",
"prebuild": "node scripts/sync-llms.mjs",
"start": "npm run build:lib && ng serve demo",
Expand All @@ -20,11 +21,13 @@
"verify:prerender": "node scripts/verify-prerender.mjs",
"size:features": "npm run build:lib && node scripts/feature-size.mjs",
"size:app": "npm run build:lib && node scripts/app-size.mjs",
"og": "node scripts/make-og.mjs",
"verify:ssr": "npm run build:lib && node scripts/verify-ssr.mjs",
"verify:compat": "node scripts/verify-compat.mjs",
"verify:docs": "npm run build:lib && node scripts/verify-docs.mjs",
"postbuild": "node scripts/make-sitemap.mjs && node scripts/inject-meta.mjs",
"verify:deps": "npm run build:lib && node scripts/verify-deps.mjs"
"verify:deps": "npm run build:lib && node scripts/verify-deps.mjs",
"verify:lockfile": "node scripts/verify-lockfile.mjs"
},
"packageManager": "npm@11.12.1",
"engines": {
Expand All @@ -46,6 +49,7 @@
"@angular/compiler-cli": "^22.0.0",
"@angular/platform-server": "^22.1.5",
"@angular/ssr": "~22.1.0",
"@fontsource/inter": "^5.2.8",
"jsdom": "^28.0.0",
"ng-packagr": "^22.1.0",
"prettier": "^3.8.1",
Expand Down
Binary file modified projects/demo/public/og.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions projects/masonry-angular/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ the DOM and re-lays out on its own. There is no `reloadItems()` to remember.
- **Fast.** One shared `ResizeObserver`, one frame per burst of changes, transform-based positioning.
The solver places 10,000 items in 0.16 ms.

**[See it running](https://masonry-angular.vercel.app)** — five live examples: a photo gallery, column spans and stamps,
a dashboard, adding and removing items, and 1,000 items at once. Each one shows its source.

**[Try it in StackBlitz](https://stackblitz.com/github/MeAkib/masonry-angular/tree/main/examples/stackblitz)** —
a running grid in your browser, no setup.

Expand Down
155 changes: 155 additions & 0 deletions scripts/make-og.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/**
* Draws the Open Graph card at projects/demo/public/og.png.
*
* The card exists because a link to the site with no preview looks like spam.
* This script exists because the previous card was made by hand, and had "8.6
* KB" painted into it — so when the published figure was corrected to 10.4 KB,
* the image kept saying the old number and nothing noticed. An image built from
* the same source as the text cannot drift from it that way.
*
* It renders HTML in Chromium at exactly 1200x630 — the size every platform
* crops to — and screenshots it. The output is committed, so the card is not
* rebuilt on every deploy; run this when the wording changes.
*
* npm run og
*
* Needs Chromium, the same way `npm run verify:native` does:
*
* npm i --no-save playwright && npx playwright install chromium
*
* The font is Inter, read from the `@fontsource/inter` devDependency and
* embedded in the page. Loading it from a CDN instead would make the card
* depend on the network and on whatever the font host serves that day; reading
* a file that npm pinned gives the same picture every time.
*/
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const OUT = join(ROOT, 'projects/demo/public/og.png');
const FONTS = join(ROOT, 'node_modules/@fontsource/inter/files');

/* ---- the words, which are the only thing that should ever need editing ---- */

const TITLE = 'masonry-angular';
const BLURB = 'Cascading grid layout for Angular. For photo walls, and for dashboards that stop leaving gaps.';
const PILLS = ['Zero dependencies', '10.4 KB', 'Angular 17–22', 'SSR-safe'];
const INSTALL = 'npm i masonry-angular';

/*
* The tiles on the right are the library's own argument: equal widths, unequal
* heights, each one starting where the shortest column ended. The hues walk
* around the wheel so no tile reads as meaning anything.
*/
const COLUMNS = [
[
{ h: 152, hue: 24 },
{ h: 236, hue: 68 },
{ h: 128, hue: 136 },
],
[
{ h: 194, hue: 158 },
{ h: 180, hue: 212 },
{ h: 210, hue: 262 },
],
[
{ h: 148, hue: 296 },
{ h: 216, hue: 348 },
{ h: 122, hue: 34 },
{ h: 90, hue: 76 },
],
];

/* -------------------------------------------------------------------------- */

let playwright;
try {
playwright = await import('playwright');
} catch {
console.error('make-og — playwright is not installed.\n');
console.error(' npm i --no-save playwright && npx playwright install chromium\n');
process.exit(1);
}

if (!existsSync(FONTS)) {
console.error(`make-og — no fonts at ${FONTS}. Run "npm install" first.`);
process.exit(1);
}

const face = (weight) => {
const file = join(FONTS, `inter-latin-${weight}-normal.woff2`);
if (!existsSync(file)) {
console.error(`make-og — missing ${file}. Has @fontsource/inter changed its layout?`);
process.exit(1);
}
return `@font-face{font-family:Inter;font-style:normal;font-weight:${weight};font-display:block;src:url(data:font/woff2;base64,${readFileSync(file).toString('base64')}) format('woff2')}`;
};

const tiles = COLUMNS.map(
(column) =>
`<div class="col">${column
.map(
({ h, hue }) =>
`<div class="tile" style="height:${h}px;background:linear-gradient(145deg,hsl(${hue} 68% 60%),hsl(${hue + 34} 66% 50%))"></div>`,
)
.join('')}</div>`,
).join('');

const html = `<!doctype html>
<html><head><meta charset="utf-8"><style>
${face(400)}
${face(700)}
*{margin:0;padding:0;box-sizing:border-box}
body{width:1200px;height:630px;overflow:hidden;background:#fafaf9;color:#1a1a17;
font-family:Inter,sans-serif;-webkit-font-smoothing:antialiased;display:flex}

.copy{flex:none;width:632px;padding:92px 0 0 70px}
h1{font-size:67px;font-weight:700;letter-spacing:-.035em;line-height:1}
p{margin-top:42px;max-width:520px;font-size:28px;line-height:1.36;color:#4d4d46}

.pills{display:flex;flex-wrap:wrap;gap:13px;max-width:520px;margin-top:46px}
.pill{padding:9px 19px;border:1px solid #dcdcd6;border-radius:999px;background:#fff;
font-size:19px;color:#3b3b35;white-space:nowrap}

.install{display:flex;align-items:center;margin-top:30px;width:565px;height:54px;padding:0 20px;
border:1px solid #dcdcd6;border-radius:11px;background:#fff;
font-family:"DejaVu Sans Mono",ui-monospace,monospace;font-size:19px;color:#3b3b35}

/* The right padding matters: without it the third column is sliced off by the
1200px edge, which reads as a rendering bug rather than a design. */
.grid{flex:1;display:flex;gap:17px;padding:32px 36px 0 0}
.col{flex:1;display:flex;flex-direction:column;gap:17px}
.col:nth-child(2){padding-top:0}
.tile{border-radius:13px}
</style></head>
<body>
<div class="copy">
<h1>${TITLE}</h1>
<p>${BLURB}</p>
<div class="pills">${PILLS.map((p) => `<span class="pill">${p}</span>`).join('')}</div>
<div class="install">${INSTALL}</div>
</div>
<div class="grid">${tiles}</div>
</body></html>
`;

const work = mkdtempSync(join(tmpdir(), 'og-'));
const page_html = join(work, 'og.html');
writeFileSync(page_html, html);

const browser = await playwright.chromium.launch(
process.env['CHROMIUM_PATH'] ? { executablePath: process.env['CHROMIUM_PATH'] } : {},
);
const page = await browser.newPage({ viewport: { width: 1200, height: 630 }, deviceScaleFactor: 1 });
await page.goto(`file://${page_html}`);
await page.evaluate(() => document.fonts.ready);
await page.screenshot({ path: OUT });
await browser.close();
rmSync(work, { recursive: true, force: true });

const bytes = readFileSync(OUT).length;
console.log(`make-og — ${OUT}`);
console.log(` 1200x630, ${(bytes / 1024).toFixed(0)} KB`);
console.log(` pills: ${PILLS.join(' · ')}`);
Loading
Loading