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
32 changes: 32 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Deploy landing page

on:
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: true

jobs:
deploy:
name: Deploy GitHub Pages site
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/checkout@v4
- uses: actions/configure-pages@v5
- uses: actions/upload-pages-artifact@v3
with:
path: site
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ The `/htmx` demo shows an optional HTMX enhancement around a normal Flask form.

The demo also includes realistic application screens at `/blocks`, `/login`, `/signup`, and `/admin`. The reusable block macros live under `jinjalume/blocks/` and compose the core components into auth flows and a responsive admin console. See [docs/blocks.md](docs/blocks.md) for signatures and copy-paste examples.

The public landing page source lives in [`site/`](site/). The Pages workflow publishes it from the `main` branch; the repository's Pages setting should use **GitHub Actions** as its build source.

## Development

```bash
Expand Down
20 changes: 14 additions & 6 deletions playwright.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,18 @@ module.exports = defineConfig({
use: { ...devices["Desktop Chrome"], viewport: { width: 390, height: 844 } },
},
],
webServer: {
command: `npm run build:css && ${python} -m flask --app demo.app run --no-debugger --no-reload --port 5000`,
url: "http://127.0.0.1:5000",
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
webServer: [
{
command: `npm run build:css && ${python} -m flask --app demo.app run --no-debugger --no-reload --port 5000`,
url: "http://127.0.0.1:5000",
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
{
command: `${python} -m http.server 8000 --directory site`,
url: "http://127.0.0.1:8000",
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
],
});
161 changes: 161 additions & 0 deletions site/index.html

Large diffs are not rendered by default.

50 changes: 50 additions & 0 deletions site/site.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
:root { color-scheme: light; }

html { scroll-behavior: smooth; }

body { text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; }

::selection { background: #7759ee; color: #fff; }

.noise { background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.8'/%3E%3C/svg%3E"); }

.logo-mark { display: inline-flex; height: 2.5rem; width: 2.5rem; align-items: center; justify-content: center; border-radius: .75rem; background: #7759ee; color: #fff; font-size: 1.35rem; font-weight: 700; letter-spacing: -.1em; line-height: 1; transition: transform 300ms cubic-bezier(.22, 1, .36, 1), background 300ms ease; }
.group:hover .logo-mark { transform: rotate(-8deg) scale(1.05); background: #111116; }

[data-reveal] { opacity: 0; transform: translateY(26px); transition: opacity 800ms cubic-bezier(.22, 1, .36, 1), transform 800ms cubic-bezier(.22, 1, .36, 1); }
[data-reveal].is-visible { opacity: 1; transform: translateY(0); }
[data-reveal][data-delay="1"] { transition-delay: 100ms; }
[data-reveal][data-delay="2"] { transition-delay: 200ms; }

.component-card { position: relative; overflow: hidden; border: 1px solid rgba(17, 17, 22, .13); border-radius: 1rem; background: #fff; box-shadow: 0 3px 5px rgba(17, 17, 22, .12), 0 12px 25px rgba(17, 17, 22, .07); transition: box-shadow 240ms ease, transform 240ms cubic-bezier(.22, 1, .36, 1); }
.component-card::before { position: absolute; inset: 0 0 auto; height: 4px; background: #d7ff63; content: ""; }
.component-card:nth-child(3n + 2)::before { background: #7759ee; }
.component-card:nth-child(3n)::before { background: #111116; }
.component-card:hover { box-shadow: 0 5px 8px rgba(17, 17, 22, .14), 0 18px 32px rgba(17, 17, 22, .1); transform: translateY(-3px); }
.component-card-head, .component-card-foot { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 1rem 1.25rem; font-size: .72rem; }
.component-card-head { border-bottom: 1px solid rgba(17, 17, 22, .1); color: rgba(17, 17, 22, .55); }
.component-card-head code { color: #7759ee; }
.component-card-foot { min-height: 3.5rem; border-top: 1px solid rgba(17, 17, 22, .1); color: rgba(17, 17, 22, .55); }
.component-card-foot a { white-space: nowrap; }

.hero-login-grid { background-image: radial-gradient(circle at 100% 0, rgba(119, 89, 238, .14), transparent 38%), radial-gradient(circle at 0 100%, rgba(215, 255, 99, .35), transparent 35%); }
.ui-preview { min-height: 23rem; }
.mini-field { height: 2rem; display: flex; align-items: center; border: 1px solid rgba(17, 17, 22, .12); border-radius: .5rem; background: #fff; color: rgba(17, 17, 22, .42); font-size: .62rem; padding: 0 .65rem; }
.mini-dashboard { display: flex; min-height: 22rem; overflow: hidden; border: 1px solid rgba(17, 17, 22, .1); border-radius: .75rem; background: #f7f4ed; }
.mini-sidebar { display: flex; width: 4.25rem; flex-shrink: 0; flex-direction: column; gap: .65rem; border-right: 1px solid rgba(17, 17, 22, .1); background: #fff; padding: .75rem .45rem; }
.mini-logo, .mini-avatar { display: inline-flex; height: 1.5rem; width: 1.5rem; align-items: center; justify-content: center; border-radius: .5rem; background: #111116; color: #fff; font-size: .65rem; font-weight: 700; }
.mini-nav { overflow: hidden; border-radius: .35rem; color: rgba(17, 17, 22, .55); font-size: .5rem; padding: .4rem .25rem; text-overflow: ellipsis; white-space: nowrap; }
.mini-nav.active { background: #111116; color: #fff; }
.mini-avatar { border-radius: 999px; background: #d9d0ff; color: #7759ee; }
.mini-stat { display: flex; min-height: 3.25rem; flex-direction: column; justify-content: space-between; border-radius: .65rem; background: #fff; padding: .55rem; }
.mini-stat span { color: rgba(17, 17, 22, .5); font-size: .5rem; }
.mini-stat b { font-size: .9rem; letter-spacing: -.05em; }
.mini-row { display: flex; justify-content: space-between; gap: .5rem; border-bottom: 1px solid rgba(17, 17, 22, .08); padding: .45rem 0; color: rgba(17, 17, 22, .62); font-size: .52rem; }
.mini-row:last-child { border-bottom: 0; }
.mini-row b { color: #111116; }

@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
[data-reveal] { opacity: 1; transform: none; transition: none; }
.component-card, .logo-mark { transition: none; }
}
52 changes: 52 additions & 0 deletions site/site.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
const menuButton = document.querySelector("[data-menu-toggle]");
const mobileMenu = document.querySelector("[data-mobile-menu]");

if (menuButton && mobileMenu) {
menuButton.addEventListener("click", () => {
const isOpen = menuButton.getAttribute("aria-expanded") === "true";
menuButton.setAttribute("aria-expanded", String(!isOpen));
menuButton.setAttribute("aria-label", isOpen ? "Open navigation" : "Close navigation");
mobileMenu.classList.toggle("hidden", isOpen);
});

mobileMenu.querySelectorAll("a").forEach((link) => {
link.addEventListener("click", () => {
menuButton.setAttribute("aria-expanded", "false");
menuButton.setAttribute("aria-label", "Open navigation");
mobileMenu.classList.add("hidden");
});
});
}

document.querySelectorAll("[data-copy]").forEach((copyButton) => {
copyButton.addEventListener("click", async () => {
const source = document.getElementById(copyButton.dataset.copy);
if (!source) return;

try {
await navigator.clipboard.writeText(source.innerText);
const originalLabel = copyButton.textContent;
copyButton.textContent = "Copied ✓";
window.setTimeout(() => { copyButton.textContent = originalLabel; }, 1600);
} catch {
copyButton.textContent = "Select to copy";
}
});
});

const revealItems = document.querySelectorAll("[data-reveal]");
if ("IntersectionObserver" in window) {
const observer = new IntersectionObserver((entries, currentObserver) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
entry.target.classList.add("is-visible");
currentObserver.unobserve(entry.target);
});
}, { threshold: 0.12 });
revealItems.forEach((item) => observer.observe(item));
} else {
revealItems.forEach((item) => item.classList.add("is-visible"));
}

const year = document.getElementById("year");
if (year) year.textContent = new Date().getFullYear();
2 changes: 1 addition & 1 deletion tests/visual/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Visual regression tests

The component gallery and the login, signup, and admin dashboard blocks are checked with Playwright at desktop (`1280x800`) and mobile (`390x844`) Chromium viewports. The gallery also has a dark-theme snapshot. Reference PNGs live beside each spec in its snapshot directory.
The static landing page, component gallery, and the login, signup, and admin dashboard blocks are checked with Playwright at desktop (`1280x800`) and mobile (`390x844`) Chromium viewports. The gallery also has a dark-theme snapshot. Reference PNGs live beside each visual spec in its snapshot directory.

Install the Python development environment and browser once:

Expand Down
30 changes: 30 additions & 0 deletions tests/visual/landing.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const { test, expect } = require("@playwright/test");

test.use({ baseURL: "http://127.0.0.1:8000" });

test.describe("landing page", () => {
test("shows the UI block catalog", async ({ page }) => {
await page.goto("/");

await expect(page).toHaveTitle(/UI for Python apps/);
await expect(page.locator("h1")).toContainText("Jinja.");
await expect(page.locator("#blocks")).toContainText("Composition blocks");
await expect(page.locator("#login-block")).toContainText("Welcome back");
await expect(page.locator("#signup-block")).toContainText("Create your account");
await expect(page.locator("#admin-block")).toContainText("Recent orders");
});

test("mobile navigation exposes the block links", async ({ page }) => {
test.skip(test.info().project.name !== "mobile", "Mobile navigation is covered by the mobile project.");
await page.goto("/");

const menuButton = page.locator("[data-menu-toggle]");
const mobileMenu = page.locator("[data-mobile-menu]");

await expect(mobileMenu).toBeHidden();
await menuButton.click();
await expect(menuButton).toHaveAttribute("aria-expanded", "true");
await expect(mobileMenu).toBeVisible();
await expect(mobileMenu).toContainText("UI blocks");
});
});
Loading