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
44 changes: 44 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
## Description

<!-- What does this PR do and why? -->

## Screenshots

<!-- If your change affects the UI, include before/after screenshots or screenshots of new UI. -->
<!-- Drag and drop images here, or paste from clipboard. -->

| Before | After |
| ------ | ----- |
| | |

- [ ] N/A — no visual changes

## AI Models Used

<!-- If you used AI tools during development, list them below. This is about transparency, not gatekeeping — AI tools are welcome! -->

| Model | How it was used |
| ----- | --------------- |
| | |

<!-- Example:
| Claude Opus 4.6 | Generated initial component structure and tests |
| GitHub Copilot | Inline code completions |
-->

- [ ] No AI tools were used for this PR

## Testing

<!-- How did you test your changes? -->

- [ ] Ran `bun lint`
- [ ] Ran `bun test integration_test/`
- [ ] Manually tested in the browser

## Checklist

- [ ] My code follows the existing style of this project
- [ ] I've included screenshots (or marked N/A)
- [ ] I've disclosed AI model usage (or marked N/A)
- [ ] I've tested my changes
110 changes: 110 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Contributing to OpenBrand

Thanks for your interest in contributing to OpenBrand! Whether you're fixing a bug, adding a feature, or improving docs — we appreciate it.

## Getting Started

1. **Fork and clone** the repository:

```bash
git clone https://github.com/<your-username>/openbrand.git
cd openbrand
```

2. **Install dependencies** (we use [Bun](https://bun.sh)):

```bash
bun install
```

3. **Set up environment variables** — copy `.env.example` or create `.env.local`:

```
NEXT_PUBLIC_SUPABASE_URL=<your-supabase-url>
NEXT_PUBLIC_SUPABASE_ANON_KEY=<your-supabase-anon-key>
SUPABASE_SERVICE_ROLE_KEY=<your-service-role-key>
```

4. **Start the dev server**:

```bash
bun dev
```

## Project Structure

| Directory | What's in there |
| -------------- | -------------------------------------------- |
| `src/` | Core extraction library (published to npm) |
| `app/` | Next.js app routes and API endpoints |
| `components/` | React UI components |
| `mcp/` | MCP server for AI assistant integration |
| `integration_test/` | Integration tests |
| `lib/` | Shared utilities (auth, Supabase clients) |

## Development Workflow

1. **Create a branch** from `main`:

```bash
git checkout -b feat/my-feature
```

2. **Make your changes** — follow the existing code style (TypeScript, Tailwind CSS).

3. **Lint and test**:

```bash
bun lint
bun test integration_test/
```

4. **Open a pull request** against `main`.

## Pull Request Requirements

Every PR must include the following. Our PR template will remind you, but here's what we expect:

### UI Screenshots

If your change affects the UI in any way, include screenshots:

- **Before & after** screenshots for changes to existing UI
- **Screenshots of new UI** for new features or components
- If your change is purely backend/library code with no visual impact, note that in the PR

Visual changes without screenshots will not be merged. This helps reviewers understand the impact of your work at a glance.

### AI Model Disclosure

We believe in transparency about how code is created. If you used AI tools during development, please disclose:

- **Which model(s)** you used (e.g., Claude Opus 4.6, GPT-4, Gemini, Copilot)
- **How you used them** (e.g., code generation, debugging, code review, writing tests)

This isn't about gatekeeping — AI tools are welcome and encouraged! We track this to understand how our codebase evolves and to give proper attribution.

If you didn't use any AI tools, just check the "No AI tools were used" box in the PR template.

## Code Style

- **TypeScript** everywhere — avoid `any` types
- **ESLint** rules are enforced — run `bun lint` before pushing
- Follow existing patterns in the codebase rather than introducing new ones
- Keep changes focused — one feature or fix per PR

## Testing

- Integration tests live in `integration_test/`
- If you're modifying the core extraction logic in `src/`, make sure existing tests still pass
- Adding tests for new functionality is appreciated

## Where to Contribute

- Check [open issues](https://github.com/ethanjyx/openbrand/issues) for things to work on
- Issues labeled `good first issue` are great starting points
- Have an idea? Open an issue first to discuss it before writing code

## Questions?

Open an issue or start a discussion — we're happy to help you get started.
2 changes: 2 additions & 0 deletions app/api/extract/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export async function GET(request: NextRequest) {
logos: extracted.data.logos || [],
colors: extracted.data.colors || [],
backdrops: extracted.data.backdrop_images || [],
fonts: extracted.data.fonts || [],
};

console.log(JSON.stringify({
Expand All @@ -128,6 +129,7 @@ export async function GET(request: NextRequest) {
logoCount: result.logos.length,
colorCount: result.colors.length,
backdropCount: result.backdrops.length,
fontCount: result.fonts.length,
}));

// Insert into brand_cache, then log
Expand Down
2 changes: 2 additions & 0 deletions components/brand-results.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { BrandExtractionResult } from "@/src/types";
import { ColorPalette } from "./color-palette";
import { LogoDisplay } from "./logo-display";
import { BackdropGallery } from "./backdrop-gallery";
import { FontDisplay } from "./font-display";
import { JsonView } from "./json-view";

export function BrandResults({ data }: { data: BrandExtractionResult }) {
Expand Down Expand Up @@ -48,6 +49,7 @@ export function BrandResults({ data }: { data: BrandExtractionResult }) {
<>
<LogoDisplay logos={data.logos} />
<ColorPalette colors={data.colors} />
<FontDisplay fonts={data.fonts} />
<BackdropGallery backdrops={data.backdrops} />
</>
) : (
Expand Down
45 changes: 45 additions & 0 deletions components/font-display.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"use client";

import type { FontAsset } from "@/src/types";
import { useState } from "react";

export function FontDisplay({ fonts }: { fonts: FontAsset[] }) {
const [copied, setCopied] = useState<string | null>(null);

if (fonts.length === 0) return null;

const copy = (text: string) => {
navigator.clipboard.writeText(text);
setCopied(text);
setTimeout(() => setCopied(null), 1500);
};

return (
<div>
<h3 className="text-sm font-medium text-neutral-500 uppercase tracking-wider mb-3">
Fonts
</h3>
<div className="flex flex-wrap gap-3">
{fonts.map((font, i) => (
<button
key={i}
onClick={() => copy(font.family)}
className="group text-left p-4 rounded-xl border border-neutral-200 bg-white hover:border-neutral-400 transition-colors cursor-pointer"
>
<span className="block text-base font-semibold text-neutral-900">
{copied === font.family ? "Copied!" : font.family}
</span>
<span className="block mt-1 text-xs text-neutral-400">
{font.role} · {font.source}
</span>
{font.weights.length > 0 && (
<span className="block mt-0.5 text-[10px] text-neutral-400">
{font.weights.join(", ")}
</span>
)}
</button>
))}
</div>
</div>
);
}
33 changes: 33 additions & 0 deletions integration_test/asset-shapes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { extractBrandAssets } from "../src";

const VALID_LOGO_TYPES = ["img", "svg", "favicon", "apple-touch-icon", "icon", "logo"];
const VALID_COLOR_USAGES = ["primary", "secondary", "accent", "background", "text"];
const VALID_FONT_ROLES = ["heading", "body"];
const VALID_FONT_SOURCES = ["google", "system", "custom"];

describe("asset shape validation", () => {
// Extract once and share across tests
Expand Down Expand Up @@ -71,4 +73,35 @@ describe("asset shape validation", () => {
}
}
});

test("FontAsset shape — family is string, role and source are known values", () => {
// fonts array must exist (may be empty for some sites)
expect(Array.isArray(data.fonts)).toBe(true);

for (const font of data.fonts) {
expect(font.family).toBeString();
expect(font.family.length).toBeGreaterThan(0);

expect(VALID_FONT_ROLES).toContain(font.role);
expect(VALID_FONT_SOURCES).toContain(font.source);

expect(Array.isArray(font.weights)).toBe(true);
expect(font.weights.length).toBeGreaterThanOrEqual(1);
for (const w of font.weights) {
expect(typeof w).toBe("number");
expect(w).toBeGreaterThanOrEqual(100);
expect(w).toBeLessThanOrEqual(900);
}

expect(Array.isArray(font.fallbacks)).toBe(true);
for (const f of font.fallbacks) {
expect(typeof f).toBe("string");
}

if (font.source === "google") {
expect(font.googleFontsUrl).toBeDefined();
expect(font.googleFontsUrl).toContain("fonts.googleapis.com");
}
}
});
});
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export type {
LogoAsset,
ColorAsset,
BackdropAsset,
FontAsset,
} from "./types";
Loading