Skip to content
Draft
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
11 changes: 11 additions & 0 deletions config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
"mptt",
"haystack",
"widget_tweaks",
"django_lookbook",
]

# Wagtail Apps
Expand Down Expand Up @@ -692,3 +693,13 @@
"extensions_settings_mode": "extend", # optional. Possible values: "extend" or "override". Defaults to "extend".
"tab_length": 4, # optional. Sets the length of tabs used by python-markdown to render the output. This is the number of spaces used to replace with a tab character. Defaults to 4.
}

LOOKBOOK = {
# we will put previews in this directory later
"preview_base": ["previews"],
# show_previews is True by default
"show_previews": DEBUG,
}

# to make iframe work
X_FRAME_OPTIONS = "SAMEORIGIN"
1 change: 1 addition & 0 deletions config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@

urlpatterns = (
[
path("lookbook/", include("django_lookbook.urls")),
path("", HomepageView.as_view(), name="home"),
path("admin/", admin.site.urls),
path("oauth2/", include("oauth2_provider.urls", namespace="oauth2_provider")),
Expand Down
170 changes: 170 additions & 0 deletions docs/component_workflow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# Component Workflow

This document describes the process for adding, previewing, and maintaining UI components in the Boost website v3 design system.

## Overview

We use [django-lookbook](https://django-lookbook.readthedocs.io/) as our component storybook. It provides:

- **Live previews** — each component is rendered in an isolated iframe at `/lookbook/`
- **Auto-discovery** — preview files in the `previews/` directory are detected automatically
- **Documentation** — docstrings on preview methods are rendered as Markdown in the "Notes" tab
- **Source inspection** — the generated HTML source is visible alongside the preview

## Directory structure

```
previews/
├── __init__.py
├── buttons_preview.py # Buttons & hero buttons
├── avatars_preview.py # Avatar component
├── tooltips_preview.py # Tooltip buttons
├── cards_preview.py # Basic, Vertical, Search, Create Account,
│ # Learn, Testimonial, Post, Mailing List,
│ # Thread Archive, Library Intro cards
├── forms_preview.py # Text field, Checkbox, Combo, Multi-select
├── carousel_preview.py # Carousel buttons & Cards carousel
├── content_preview.py # Content detail card, Content event card,
│ # Why Boost cards, Event cards, Category tags
├── code_blocks_preview.py # Code block, Code block card, full story layout
├── stats_preview.py # Stats in numbers (bar charts)
└── template_preview.py # (empty, kept for reference)

templates/
├── django_lookbook/
│ └── preview.html # Wrapper template for all lookbook previews
│ # (loads v3 CSS, Alpine.js, highlight.js, etc.)
└── v3/
└── includes/ # The actual component templates
├── _button.html
├── _avatar_v3.html
└── ...
```

## Adding a new component

### 1. Create the component template

Add a new partial template in `templates/v3/includes/`:

```
templates/v3/includes/_my_component.html
```

Follow the existing conventions:
- Prefix with `_` (Django partial convention).
- Add a `{% comment %}` block at the top documenting all variables, their types, whether they're required, and a usage example.
- Use BEM-style class names scoped to the component (e.g. `my-component__title`).

### 2. Add the component CSS

Create a CSS file in `static/css/v3/`:

```
static/css/v3/my-component.css
```

Then import it in `static/css/v3/components.css`:

```css
@import './my-component.css';
```

### 3. Write a lookbook preview

Create a new preview file (or add to an existing one if the component belongs to a logical group):

```python
# previews/my_component_preview.py

from django_lookbook.preview import LookbookPreview
from django.template.loader import render_to_string


class MyComponentPreview(LookbookPreview):

def default(self, **kwargs):
"""
Brief description of what this preview shows.

Template: `v3/includes/_my_component.html`

| Variable | Required | Description |
|---|---|---|
| `title` | Yes | Component heading |
| `description` | No | Body text |
"""
return render_to_string("v3/includes/_my_component.html", {
"title": "Example title",
"description": "Example description text.",
})

def another_variant(self, **kwargs):
"""
Description of this variant (e.g. different theme, size, or state).
"""
return render_to_string("v3/includes/_my_component.html", {
"title": "Variant title",
"description": "Shows a different configuration.",
})
```

**Conventions for preview files:**

- **One `LookbookPreview` subclass per component** (or per tightly related group).
- **Each public method = one preview** in the lookbook sidebar.
- **Method names** should be descriptive: `default`, `with_icon`, `error_state`, `few_items`, etc.
- **Docstrings** are rendered as Markdown in the lookbook Notes tab. Include:
- A brief description of what the preview demonstrates.
- The template path.
- A table of the component's variables (for the primary/default preview).
- **Use `render_to_string`** when passing context variables. Use `Template(...).render(Context({...}))` when you need inline template logic (e.g. `{% include %}` with `with` parameters).
- **Don't invent documentation** — only document variables that actually exist in the template's `{% comment %}` block.

### 4. Verify in the lookbook

Start the dev server and visit:

```
http://localhost:8000/lookbook/
```

Your new preview should appear automatically in the sidebar. Check:

- The preview renders correctly in the iframe.
- The HTML source tab shows the expected markup.
- The Notes tab shows your docstring documentation.

### 5. For components with hover / active states

Two approaches:

- **Interactive** (preferred): Let the user hover naturally in the lookbook preview iframe.
- **Forced state**: Add a separate preview method (e.g. `hovered_buttons`) that applies a `data-hover` attribute or equivalent CSS class to force the visual state. This is useful for visual regression testing and design review.

Both approaches can coexist — the default preview for interactive testing, and a forced-state preview for static screenshots.

### 6. Preview template (`django_lookbook/preview.html`)

All previews are rendered inside `templates/django_lookbook/preview.html`. This template loads:

- V3 component CSS (`static/css/v3/components.css`)
- Alpine.js (for interactive components like dropdowns, tooltips)
- highlight.js (for code blocks)
- carousel.js (for carousel behaviour)
- Google Fonts (Noto Sans)

If your component requires additional JS or CSS, add it to this template.

## Components not in lookbook

- **WYSIWYG editor** (`v3/includes/_wysiwyg_editor.html`) — This component requires TipTap JS initialization via a dynamic module import (`/static/js/v3/wysiwyg-editor.js`) and custom inline scripts for demo content. It is better tested in-context (e.g. in the Django admin or a dedicated test page) rather than in an isolated lookbook preview.

## Checklist for new components

- [ ] Template created in `templates/v3/includes/` with `{% comment %}` documentation
- [ ] CSS file created in `static/css/v3/` and imported in `components.css`
- [ ] Lookbook preview written in `previews/` with docstring documentation
- [ ] Preview renders correctly at `/lookbook/`
- [ ] All component variants have dedicated preview methods
- [ ] Any required JS is loaded in `templates/django_lookbook/preview.html`
Empty file added previews/__init__.py
Empty file.
96 changes: 96 additions & 0 deletions previews/account_connections_preview.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
from django_lookbook.preview import LookbookPreview
from django.template.loader import render_to_string


class AccountConnectionsCardPreview(LookbookPreview):

def mixed(self, **kwargs):
"""
Account connections card with one connected and one not connected.

Template: `v3/includes/_account_connections_card.html`

| Variable | Required | Description |
|---|---|---|
| `heading` | No | Card heading. Default: "Account connections" |
| `connections` | Yes | List of connection dicts with: platform, label, connected, status_text, action_label, action_url |
"""
return render_to_string(
"v3/includes/_account_connections_card.html",
{
"connections": [
{
"platform": "github",
"label": "GitHub",
"connected": True,
"status_text": "Connected",
"action_label": "Manage",
"action_url": "#",
},
{
"platform": "google",
"label": "Google",
"connected": False,
"status_text": "Not connected",
"action_label": "Connect",
"action_url": "#",
},
],
},
)

def all_connected(self, **kwargs):
"""
Account connections card with all accounts connected.
"""
return render_to_string(
"v3/includes/_account_connections_card.html",
{
"connections": [
{
"platform": "github",
"label": "GitHub",
"connected": True,
"status_text": "Connected",
"action_label": "Manage",
"action_url": "#",
},
{
"platform": "google",
"label": "Google",
"connected": True,
"status_text": "Connected",
"action_label": "Manage",
"action_url": "#",
},
],
},
)

def none_connected(self, **kwargs):
"""
Account connections card with no accounts connected.
"""
return render_to_string(
"v3/includes/_account_connections_card.html",
{
"connections": [
{
"platform": "github",
"label": "GitHub",
"connected": False,
"status_text": "Not connected",
"action_label": "Connect",
"action_url": "#",
},
{
"platform": "google",
"label": "Google",
"connected": False,
"status_text": "Not connected",
"action_label": "Connect",
"action_url": "#",
},
],
},
)
78 changes: 78 additions & 0 deletions previews/avatars_preview.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from django_lookbook.preview import LookbookPreview
from django.template import Context, Template


class AvatarsPreview(LookbookPreview):

def variants(self, **kwargs):
"""
Avatar component — all visual variants.

Template: `v3/includes/_avatar_v3.html`

| Variable | Required | Description |
|---|---|---|
| `src` | No | Image URL. If set, renders an image avatar |
| `name` | No | Person's name. If set (and no `src`), renders initials |
| `variant` | No | Color variant for initials: `yellow`, `green`, `teal`. Default: `yellow` |
| `size` | No | Size: `sm`, `md`, `lg`, `xl`. Default: `md` |

Priority: `src` > `name` (initials) > placeholder (`?`).
"""
template = Template(
"""
{% load avatar_tags %}
<div style="display: flex; gap: 24px; align-items: center; flex-wrap: wrap;">
<div style="display: flex; flex-direction: column; align-items: center; gap: 8px;">
{% include "v3/includes/_avatar_v3.html" with src="https://thispersondoesnotexist.com/" size="md" %}
<span style="font-size: 12px;">Example photo</span>
</div>
<div style="display: flex; flex-direction: column; align-items: center; gap: 8px;">
{% include "v3/includes/_avatar_v3.html" with name="Jane Doe" variant="yellow" %}
<span style="font-size: 12px;">Yellow</span>
</div>
<div style="display: flex; flex-direction: column; align-items: center; gap: 8px;">
{% include "v3/includes/_avatar_v3.html" with name="Jane Doe" variant="green" %}
<span style="font-size: 12px;">Green</span>
</div>
<div style="display: flex; flex-direction: column; align-items: center; gap: 8px;">
{% include "v3/includes/_avatar_v3.html" with name="Jane Doe" variant="teal" %}
<span style="font-size: 12px;">Teal</span>
</div>
<div style="display: flex; flex-direction: column; align-items: center; gap: 8px;">
{% include "v3/includes/_avatar_v3.html" %}
<span style="font-size: 12px;">Placeholder</span>
</div>
</div>
"""
)
return template.render(Context({}))

def sizes(self, **kwargs):
"""
Avatar component at each available size: `sm` (32px), `md` (40px), `lg` (44px), `xl` (48px).
"""
template = Template(
"""
{% load avatar_tags %}
<div style="display: flex; gap: 24px; align-items: end; flex-wrap: wrap;">
<div style="display: flex; flex-direction: column; align-items: center; gap: 8px;">
{% include "v3/includes/_avatar_v3.html" with name="JD" variant="yellow" size="sm" %}
<span style="font-size: 12px;">sm</span>
</div>
<div style="display: flex; flex-direction: column; align-items: center; gap: 8px;">
{% include "v3/includes/_avatar_v3.html" with name="JD" variant="yellow" size="md" %}
<span style="font-size: 12px;">md</span>
</div>
<div style="display: flex; flex-direction: column; align-items: center; gap: 8px;">
{% include "v3/includes/_avatar_v3.html" with name="JD" variant="yellow" size="lg" %}
<span style="font-size: 12px;">lg</span>
</div>
<div style="display: flex; flex-direction: column; align-items: center; gap: 8px;">
{% include "v3/includes/_avatar_v3.html" with name="JD" variant="yellow" size="xl" %}
<span style="font-size: 12px;">xl</span>
</div>
</div>
"""
)
return template.render(Context({}))
Loading
Loading