Skip to content
Open
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
37 changes: 28 additions & 9 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ This is the **Next.js website** for Mellea — the landing page and developer bl
npm install
npm run dev # http://localhost:4000
npm run lint # ESLint
npm run lint:md # Markdown lint (content files)
npm run typecheck # tsc --noEmit
npm run test:unit # Vitest (no browser required)
npm run test:e2e # Playwright (auto-starts dev server)
Expand Down Expand Up @@ -61,16 +62,34 @@ Plain descriptive messages: `fix: nav link selector in E2E tests`, `feat: add ta

No Angular-style mandatory types required, but keep messages short and imperative.

## 6. Self-Review (before notifying user)
## 6. Pre-commit Checklist (mandatory — do not skip)

1. `npm run lint` clean?
2. `npm run typecheck` clean?
3. `npm run test:unit` passes?
4. `npm run test:e2e` passes?
5. `npm run build` succeeds?
6. No new `any` types introduced without justification?
7. No hardcoded URLs that should be in `src/config/site.ts`?
8. Added or edited Markdown with external links? CI will run lychee — broken links block deploy.
Run the appropriate checks **before every commit**. CI will reject failures; fixing them after the fact wastes pipeline time.

### Code changes (any `.ts`, `.tsx`, `.css`, `.mjs`, or config file)

```bash
npm run lint # must be clean
npm run typecheck # must be clean
npm run test:unit # must pass
npm run test:e2e # must pass
```

If you rename or remove a CSS class, check `tests/e2e/` for selectors that reference it and update them in the same commit.

### Content-only changes (`.md` files in `content/blogs/` only, no code touched)

```bash
npm run lint:md # must be clean
```

No build or E2E run required for content-only changes.

### Additional checks (code changes)

- No new `any` types without a comment explaining why
- No hardcoded URLs — use `src/config/site.ts`
- External links in Markdown? CI runs lychee — broken links block deploy

## 7. Architecture

Expand Down
2 changes: 1 addition & 1 deletion next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/types/routes.d.ts";

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
1 change: 1 addition & 0 deletions next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
allowedDevOrigins: ['192.168.100.102'],
output: 'export',
basePath: process.env.NEXT_PUBLIC_BASE_PATH || '',
trailingSlash: true,
Expand Down
2 changes: 1 addition & 1 deletion src/app/blogs/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export default async function BlogPostPage({ params }: { params: Promise<{ slug:
<h1 className="blog-post-title">{blog.title}</h1>

{blog.tags.length > 0 && (
<div className="blog-post-tags">
<div className="blog-post-tags" aria-label="Tags">
{blog.tags.map((tag) => (
<span key={tag} className="tag">{tag}</span>
))}
Expand Down
37 changes: 21 additions & 16 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,27 @@ a {
color: var(--text-primary);
}

/* ── Mobile nav overlay (portaled to <body>, separate from desktop .header-nav) ── */
.mobile-nav-overlay {
display: none;
}

.mobile-nav-overlay--open {
display: flex;
position: fixed;
top: 48px;
left: 0;
right: 0;
flex-direction: column;
align-items: stretch;
padding: 1rem 0;
background: var(--bg-primary);
border-top: 1px solid var(--border);
border-bottom: 1px solid var(--border);
overflow-y: auto;
z-index: 9999;
}

@media (max-width: 768px) {
.mobile-menu-toggle {
display: flex;
Expand All @@ -214,22 +235,6 @@ a {

.header-nav {
display: none;
position: fixed;
top: 48px;
left: 0;
right: 0;
bottom: 0;
background: var(--bg-primary);
flex-direction: column;
align-items: stretch;
padding: 1rem 0;
z-index: 99;
border-top: 1px solid var(--border);
overflow-y: auto;
}

.header-nav--open {
display: flex;
}

.nav-link {
Expand Down
28 changes: 14 additions & 14 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default function HomePage() {
return (
<>
{/* ── Hero ── */}
<section className="hero">
<section className="hero" aria-label="Hero">
<div className="container">
<div className="hero-inner">
<div className="hero-text">
Expand Down Expand Up @@ -96,7 +96,7 @@ export default function HomePage() {
</div>

<div className="feature-grid">
<div className="feature-card">
<article className="feature-card">
{/* Python logo */}
<svg className="feature-card-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.914 0C5.82 0 6.2 2.656 6.2 2.656l.007 2.752h5.814v.826H3.9S0 5.789 0 11.969c0 6.18 3.403 5.96 3.403 5.96h2.031v-2.867s-.109-3.402 3.35-3.402h5.766s3.24.052 3.24-3.131V3.19S18.304 0 11.914 0zm-3.2 1.84a1.046 1.046 0 1 1 0 2.092 1.046 1.046 0 0 1 0-2.092z" fill="currentColor"/>
Expand All @@ -105,8 +105,8 @@ export default function HomePage() {
<h3 className="feature-card-title">Python not Prose</h3>
<p className="feature-card-body">The <code>@generative</code> decorator turns typed function signatures into LLM specifications. Docstrings are prompts, type hints are schemas — no templates, no parsers.</p>
<Link href="https://docs.mellea.ai/concepts/generative-functions" target="_blank" className="feature-card-link">Learn more →</Link>
</div>
<div className="feature-card">
</article>
<article className="feature-card">
{/* Lock / constrained */}
<svg className="feature-card-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3" y="11" width="18" height="12" rx="2" stroke="currentColor" strokeWidth="1.75"/>
Expand All @@ -116,8 +116,8 @@ export default function HomePage() {
<h3 className="feature-card-title">Constrained Decoding</h3>
<p className="feature-card-body">Grammar-constrained generation for Ollama, vLLM, and HuggingFace. Unlike Instructor and PydanticAI, valid output is enforced at the token level — not retried into existence.</p>
<Link href="https://docs.mellea.ai/how-to/enforce-structured-output" target="_blank" className="feature-card-link">Learn more →</Link>
</div>
<div className="feature-card">
</article>
<article className="feature-card">
{/* Clipboard checklist */}
<svg className="feature-card-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round"/>
Expand All @@ -128,8 +128,8 @@ export default function HomePage() {
<h3 className="feature-card-title">Requirements Driven</h3>
<p className="feature-card-body">Declare rules — tone, length, content, custom logic — and Mellea validates every output before it leaves. Automatic retries mean bad output never reaches your users.</p>
<Link href="https://docs.mellea.ai/concepts/requirements-system" target="_blank" className="feature-card-link">Learn more →</Link>
</div>
<div className="feature-card">
</article>
<article className="feature-card">
{/* Shield */}
<svg className="feature-card-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2L4 6v6c0 5.25 3.5 10.15 8 11.35C16.5 22.15 20 17.25 20 12V6l-8-4z" stroke="currentColor" strokeWidth="1.75" strokeLinejoin="round"/>
Expand All @@ -138,8 +138,8 @@ export default function HomePage() {
<h3 className="feature-card-title">Predictable and Resilient</h3>
<p className="feature-card-body">Need higher confidence? Switch from single-shot to majority voting or best-of-n with one parameter. No code rewrites, no new infrastructure.</p>
<Link href="https://docs.mellea.ai/advanced/inference-time-scaling" target="_blank" className="feature-card-link">Learn more →</Link>
</div>
<div className="feature-card">
</article>
<article className="feature-card">
{/* Plug / connector */}
<svg className="feature-card-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 22v-3" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round"/>
Expand All @@ -150,8 +150,8 @@ export default function HomePage() {
<h3 className="feature-card-title">MCP Compatible</h3>
<p className="feature-card-body">Expose any Mellea program as an MCP tool. The calling agent gets validated output — requirements checked, retries run — not raw LLM responses.</p>
<Link href="https://docs.mellea.ai/integrations/mcp" target="_blank" className="feature-card-link">Learn more →</Link>
</div>
<div className="feature-card">
</article>
<article className="feature-card">
{/* Shield with eye — safety */}
<svg className="feature-card-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2L4 6v6c0 5.25 3.5 10.15 8 11.35C16.5 22.15 20 17.25 20 12V6l-8-4z" stroke="currentColor" strokeWidth="1.75" strokeLinejoin="round"/>
Expand All @@ -161,7 +161,7 @@ export default function HomePage() {
<h3 className="feature-card-title">Safety &amp; Guardrails</h3>
<p className="feature-card-body">Built-in Granite Guardian integration detects harmful outputs, hallucinations, and jailbreak attempts before they reach your users — no external service required.</p>
<Link href="https://docs.mellea.ai/how-to/safety-guardrails" target="_blank" className="feature-card-link">Learn more →</Link>
</div>
</article>
</div>
</div>
</section>
Expand Down Expand Up @@ -201,7 +201,7 @@ export default function HomePage() {
</section>

{/* ── Vision / closing CTA ── */}
<section className="section vision-section">
<section className="section vision-section" aria-label="Vision">
<div className="container">
<div className="vision-inner">
<p className="vision-text">
Expand Down
2 changes: 1 addition & 1 deletion src/components/GitHubStats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export default function GitHubStats() {
</div>
<div className="gh-stats-footer">
{state.status === 'success' && state.data.contributorAvatars.length > 0 && (
<div className="gh-avatars">
<div className="gh-avatars" data-testid="contributor-avatars">
{state.data.contributorAvatars.map((c) => (
<a
key={c.login}
Expand Down
91 changes: 41 additions & 50 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,40 @@
'use client';

import { useState, useEffect } from 'react';
import { useState, useSyncExternalStore } from 'react';
import { createPortal } from 'react-dom';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { siteConfig } from '@/config/site';

const emptySubscribe = () => () => {};

export default function Header() {
const pathname = usePathname();
const [menuOpen, setMenuOpen] = useState(false);
// useSyncExternalStore returns false on server, true on client — no setState-in-effect needed.
const mounted = useSyncExternalStore(emptySubscribe, () => true, () => false);

const closeMenu = () => setMenuOpen(false);

// Prevent body scroll when menu is open
useEffect(() => {
document.body.style.overflow = menuOpen ? 'hidden' : '';
return () => { document.body.style.overflow = ''; };
}, [menuOpen]);
const navLinks = (
<>
<Link href={siteConfig.docsUrl} target="_blank" rel="noopener noreferrer" className="nav-link" onClick={closeMenu}>
Docs
</Link>
<Link href="/blogs" className={`nav-link ${pathname.startsWith('/blogs') ? 'active' : ''}`} onClick={closeMenu}>
Blog
</Link>
<Link href={siteConfig.discussionsUrl} target="_blank" rel="noopener noreferrer" className="nav-link" onClick={closeMenu}>
Community
</Link>
<Link href={siteConfig.githubUrl} target="_blank" rel="noopener noreferrer" className="nav-link" onClick={closeMenu}>
GitHub
</Link>
<Link href={siteConfig.docsUrl} target="_blank" rel="noopener noreferrer" className="nav-cta" onClick={closeMenu}>
Get Started →
</Link>
</>
);

return (
<header className="header">
Expand Down Expand Up @@ -43,52 +62,24 @@ export default function Header() {
)}
</button>

<nav className={`header-nav${menuOpen ? ' header-nav--open' : ''}`}>
<Link
href={siteConfig.docsUrl}
target="_blank"
rel="noopener noreferrer"
className="nav-link"
onClick={closeMenu}
>
Docs
</Link>
<Link
href="/blogs"
className={`nav-link ${pathname.startsWith('/blogs') ? 'active' : ''}`}
onClick={closeMenu}
>
Blog
</Link>
<Link
href={siteConfig.discussionsUrl}
target="_blank"
rel="noopener noreferrer"
className="nav-link"
onClick={closeMenu}
>
Community
</Link>
<Link
href={siteConfig.githubUrl}
target="_blank"
rel="noopener noreferrer"
className="nav-link"
onClick={closeMenu}
>
GitHub
</Link>
<Link
href={siteConfig.docsUrl}
target="_blank"
rel="noopener noreferrer"
className="nav-cta"
onClick={closeMenu}
>
Get Started →
</Link>
{/* Desktop nav — inline in header */}
<nav className="header-nav">
{navLinks}
</nav>
</div>

{/* Mobile nav overlay — portaled to <body> so position:fixed is viewport-relative,
not relative to the sticky header ancestor (iOS Safari limitation). */}
{mounted && createPortal(
<nav
className={`mobile-nav-overlay${menuOpen ? ' mobile-nav-overlay--open' : ''}`}
aria-label="Mobile navigation"
aria-hidden={!menuOpen}
>
{navLinks}
</nav>,
document.body
)}
</header>
);
}
7 changes: 1 addition & 6 deletions tests/e2e/accessibility.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ test('blog index has exactly one h1', async ({ page }) => {
test('blog post has exactly one h1', async ({ page }) => {
// Navigate to first available post
await page.goto('/blogs/');
const href = await page.locator('a.blog-card').first().getAttribute('href');
const href = await page.getByRole('main').locator('a[href^="/blogs/"]:not([href="/blogs/"])').first().getAttribute('href');
await page.goto(href!);
await expect(page.locator('h1')).toHaveCount(1);
});
Expand Down Expand Up @@ -74,8 +74,3 @@ test('code showcase uses proper ARIA roles', async ({ page }) => {
await expect(page.locator('[role="tab"][aria-selected="true"]')).toHaveCount(1);
});

test('mobile menu toggle has aria-expanded', async ({ page }) => {
await page.goto('/');
const toggle = page.locator('.mobile-menu-toggle');
await expect(toggle).toHaveAttribute('aria-expanded', 'false');
});
Loading
Loading