diff --git a/config/settings.py b/config/settings.py index 359433b95..217bacaf8 100755 --- a/config/settings.py +++ b/config/settings.py @@ -89,6 +89,7 @@ "mptt", "haystack", "widget_tweaks", + "django_lookbook", ] # Wagtail Apps @@ -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" diff --git a/config/urls.py b/config/urls.py index 498f31a0e..4846a60e6 100644 --- a/config/urls.py +++ b/config/urls.py @@ -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")), diff --git a/docs/component_workflow.md b/docs/component_workflow.md new file mode 100644 index 000000000..b31653324 --- /dev/null +++ b/docs/component_workflow.md @@ -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` diff --git a/previews/__init__.py b/previews/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/previews/account_connections_preview.py b/previews/account_connections_preview.py new file mode 100644 index 000000000..ce18b0512 --- /dev/null +++ b/previews/account_connections_preview.py @@ -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": "#", + }, + ], + }, + ) diff --git a/previews/avatars_preview.py b/previews/avatars_preview.py new file mode 100644 index 000000000..404c3ebe1 --- /dev/null +++ b/previews/avatars_preview.py @@ -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 %} +
+
+ {% include "v3/includes/_avatar_v3.html" with src="https://thispersondoesnotexist.com/" size="md" %} + Example photo +
+
+ {% include "v3/includes/_avatar_v3.html" with name="Jane Doe" variant="yellow" %} + Yellow +
+
+ {% include "v3/includes/_avatar_v3.html" with name="Jane Doe" variant="green" %} + Green +
+
+ {% include "v3/includes/_avatar_v3.html" with name="Jane Doe" variant="teal" %} + Teal +
+
+ {% include "v3/includes/_avatar_v3.html" %} + Placeholder +
+
+ """ + ) + 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 %} +
+
+ {% include "v3/includes/_avatar_v3.html" with name="JD" variant="yellow" size="sm" %} + sm +
+
+ {% include "v3/includes/_avatar_v3.html" with name="JD" variant="yellow" size="md" %} + md +
+
+ {% include "v3/includes/_avatar_v3.html" with name="JD" variant="yellow" size="lg" %} + lg +
+
+ {% include "v3/includes/_avatar_v3.html" with name="JD" variant="yellow" size="xl" %} + xl +
+
+ """ + ) + return template.render(Context({})) diff --git a/previews/banner_preview.py b/previews/banner_preview.py new file mode 100644 index 000000000..15170c020 --- /dev/null +++ b/previews/banner_preview.py @@ -0,0 +1,38 @@ +from django_lookbook.preview import LookbookPreview +from django.template.loader import render_to_string + + +class BannerPreview(LookbookPreview): + + def default(self, **kwargs): + """ + Alert banner with icon and message. + + Template: `v3/includes/_banner.html` + + | Variable | Required | Description | + |---|---|---| + | `banner_message` | Yes | HTML content for the banner message | + | `icon_name` | No | Icon name (see `includes/icon.html`) | + | `fade_time` | No | Time in ms after which the banner fades out | + """ + return render_to_string( + "v3/includes/_banner.html", + { + "icon_name": "alert", + "banner_message": "This is an older version of Boost and was released in 2017. The current version is 1.90.0.", + }, + ) + + def with_fade(self, **kwargs): + """ + Alert banner with auto-fade after 5 seconds. + """ + return render_to_string( + "v3/includes/_banner.html", + { + "icon_name": "alert", + "banner_message": "This is an older version of Boost and was released in 2017. The current version is 1.90.0.", + "fade_time": 5000, + }, + ) diff --git a/previews/buttons_preview.py b/previews/buttons_preview.py new file mode 100644 index 000000000..3c9b09234 --- /dev/null +++ b/previews/buttons_preview.py @@ -0,0 +1,100 @@ +from django_lookbook.preview import LookbookPreview +from django.template import Context, Template + + +class ButtonsPreview(LookbookPreview): + + def default_buttons(self, **kwargs): + """ + All button style variants in their default state. + + Available styles: `primary`, `secondary`, `green`, `yellow`, `teal`, `error`. + + Template: `v3/includes/_button.html` + + | Variable | Required | Description | + |---|---|---| + | `label` | Yes | Button text | + | `url` | No | If set, renders as `` instead of ` + + + + + + + """ + ) + return template.render(Context({})) + + def hovered_buttons(self, **kwargs): + """ + All button style variants with the `data-hover` attribute applied to + force the hover appearance without user interaction. + + This is useful for visual regression testing of hover states. + """ + template = Template( + """ +
+ + + + + + +
+ """ + ) + return template.render(Context({})) + + def hero_buttons(self, **kwargs): + """ + Hero buttons — larger buttons intended for hero / landing sections. + Shows default and hovered states for both primary and secondary. + + Template: `v3/includes/_button_hero.html` + + | Variable | Required | Description | + |---|---|---| + | `label` | Yes | Button text | + | `url` | No | If set, renders as `
` | + | `icon_name` | No | Icon name. Default: "arrow-right" | + | `style` | No | "primary" or "secondary". Default: "primary" | + | `type` | No | Button type attribute | + | `extra_classes` | No | Additional CSS classes | + """ + template = Template( + """ + {% load static %} +
+ + + + +
+ """ + ) + return template.render(Context({})) diff --git a/previews/cards_preview.py b/previews/cards_preview.py new file mode 100644 index 000000000..de926df9c --- /dev/null +++ b/previews/cards_preview.py @@ -0,0 +1,395 @@ +from django_lookbook.preview import LookbookPreview +from django.template import Context, Template +from django.template.loader import render_to_string + + +class BasicCardPreview(LookbookPreview): + + def with_two_buttons(self, **kwargs): + """ + Basic card with both primary and secondary action buttons. + + Template: `v3/includes/_basic_card.html` + + | Variable | Required | Description | + |---|---|---| + | `title` | Yes | Bold heading at the top | + | `text` | Yes | Body text | + | `primary_button_url` | No | Primary CTA destination | + | `primary_button_label` | No | Primary CTA text | + | `secondary_button_url` | No | Secondary CTA destination | + | `secondary_button_label` | No | Secondary CTA text | + """ + return render_to_string( + "v3/includes/_basic_card.html", + { + "title": "Found a Bug?", + "text": "We rely on developers like you to keep Boost solid. Here's how to report issues that help the whole comm", + "primary_button_url": "www.example.com", + "primary_button_label": "Primary Button", + "secondary_button_url": "www.example.com", + "secondary_button_label": "Secondary Button", + }, + ) + + def with_one_button(self, **kwargs): + """ + Basic card with only a primary button. + """ + return render_to_string( + "v3/includes/_basic_card.html", + { + "title": "Found a Bug?", + "text": "We rely on developers like you to keep Boost solid. Here's how to report issues that help the whole comm", + "primary_button_url": "www.example.com", + "primary_button_label": "Primary Button", + }, + ) + + +class VerticalCardPreview(LookbookPreview): + + def default(self, **kwargs): + """ + Vertical layout card with an image, text, and a button. + + Template: `v3/includes/_vertical_card.html` + + | Variable | Required | Description | + |---|---|---| + | `title` | Yes | Bold heading | + | `text` | No | Body text | + | `image_url` | No | Image URL | + | `primary_button_url` | No | Primary CTA destination | + | `primary_button_label` | No | Primary CTA text | + | `primary_style` | No | Button style override | + | `secondary_button_url` | No | Secondary CTA destination | + | `secondary_button_label` | No | Secondary CTA text | + """ + return render_to_string( + "v3/includes/_vertical_card.html", + { + "title": "Found a Bug?", + "text": "We rely on developers like you to keep Boost solid. Here's how to report issues that help the whole comm", + "primary_button_url": "www.example.com", + "primary_button_label": "Primary Button", + "primary_style": "secondary-grey", + "image_url": "/static/img/v3/demo_page/Calendar.png", + }, + ) + + +class SearchCardPreview(LookbookPreview): + + def default(self, **kwargs): + """ + Search prompt card with a text input and popular term tags. + + Template: `v3/includes/_search_card.html` + + | Variable | Required | Description | + |---|---|---| + | `heading` | Yes | Card heading text | + | `placeholder` | No | Input placeholder. Default: "Search libraries, docs, examples" | + | `action_url` | Yes | Form action URL | + | `input_name` | No | Input name attribute. Default: "q" | + | `popular_terms` | No | List of objects with `.label` | + | `popular_label` | No | Label above tags. Default: "Popular terms:" | + """ + return render_to_string( + "v3/includes/_search_card.html", + { + "heading": "What are you trying to find?", + "action_url": "#", + "popular_terms": [ + {"label": "Networking"}, + {"label": "Math"}, + {"label": "Data processing"}, + {"label": "Concurrency"}, + {"label": "File systems"}, + {"label": "Testing"}, + ], + }, + ) + + +class CreateAccountCardPreview(LookbookPreview): + + def default(self, **kwargs): + """ + Create Account Card with rich text body and preview image. + + Template: `v3/includes/_create_account_card.html` + + | Variable | Required | Description | + |---|---|---| + | `heading` | No | Card title text | + | `body_html` | No | Rich text HTML content | + | `preview_image_url` | No | Preview image URL | + | `preview_image_alt` | No | Preview image alt text | + | `cta_url` | No | CTA target URL | + | `cta_label` | No | CTA text | + """ + return render_to_string( + "v3/includes/_create_account_card.html", + { + "heading": "Contribute to earn badges, track your progress and grow your impact", + "preview_image_url": "/static/img/checker.png", + "cta_url": "#", + "cta_label": "Start contributing", + }, + ) + + +class LearnCardPreview(LookbookPreview): + + def default(self, **kwargs): + """ + Learn card — introduces a topic with links and a large image. + + Template: `v3/includes/_learn_card.html` + + | Variable | Required | Description | + |---|---|---| + | `title` | Yes | Card title | + | `text` | Yes | Short description | + | `links` | Yes | List of dicts with `label` and `url` | + | `label` | Yes | Hero button text | + | `url` | Yes | Hero button URL | + | `image_src` | Yes | Image URL | + """ + return render_to_string( + "v3/includes/_learn_card.html", + { + "title": "I want to learn:", + "text": "How to install Boost, use its libraries, build projects, and get help when you need it.", + "links": [ + { + "label": "Explore common use cases", + "url": "https://www.example.com", + }, + {"label": "Build with CMake", "url": "https://www.example.com"}, + {"label": "Visit the FAQ", "url": "https://www.example.com"}, + ], + "url": "https://www.example.com", + "label": "Learn more about Boost", + "image_src": "/static/img/v3/examples/Learn Card Image.png", + }, + ) + + +class TestimonialCardPreview(LookbookPreview): + + def default(self, **kwargs): + """ + Testimonial card — a carousel of user quotes with author info. + + Template: `v3/includes/_testimonial_card.html` + + | Variable | Required | Description | + |---|---|---| + | `heading` | Yes | Card title | + | `testimonials` | Yes | List of testimonial objects, each with `quote` and `author` (name, avatar_url, role, role_badge) | + """ + return render_to_string( + "v3/includes/_testimonial_card.html", + { + "heading": "What Engineers are saying", + "testimonials": [ + { + "quote": "I use Boost daily. I absolutely love it. It's wonderful. I could not do my job w/o it. Much of it is in the new C++11 standard too.", + "author": { + "name": "Name Surname", + "avatar_url": "/static/img/v3/demo_page/Avatar.png", + "role": "Contributor", + "role_badge": "/static/img/v3/demo_page/Badge.svg", + }, + }, + { + "quote": "I use Boost daily. I absolutely love it. It's wonderful. I could not do my job w/o it. Much of it is in the new C++11 standard too.", + "author": { + "name": "Name Surname", + "avatar_url": "/static/img/v3/demo_page/Avatar.png", + "role": "Contributor", + "role_badge": "/static/img/v3/demo_page/Badge.svg", + }, + }, + { + "quote": "I use Boost d1aily. I absolutely love it. It's wonderful. I could not do my job w/o it. Much of it is in the new C++11 standard too.", + "author": { + "name": "Name Surname", + "avatar_url": "/static/img/v3/demo_page/Avatar.png", + "role": "Contributor", + "role_badge": "/static/img/v3/demo_page/Badge.svg", + }, + }, + { + "quote": "I use Boost daily. I absolutely love it. It's wonderful. I could not do my job w/o it. Much of it is in the new C++11 standard too.", + "author": { + "name": "Name Surname", + "avatar_url": "/static/img/v3/demo_page/Avatar.png", + "role": "Contributor", + "role_badge": "/static/img/v3/demo_page/Badge.svg", + }, + }, + ], + }, + ) + + +class PostCardPreview(LookbookPreview): + + def single_post_card(self, **kwargs): + """ + A single post card with title, meta info, and author. + + Template: `v3/includes/_post_card_v3.html` + + | Variable | Required | Description | + |---|---|---| + | `post_title` | No | Post title | + | `post_url` | No | Link URL | + | `post_date` | No | Date string | + | `post_category` | No | Category label | + | `post_tag` | No | Tag (shown as #tag) | + | `author_name` | No | Author display name | + | `author_role` | No | Author role | + | `author_avatar_url` | No | Author avatar image URL | + | `author_show_badge` | No | If truthy, shows a badge icon | + """ + template = Template( + '{% with post_title="A talk by Richard Thomson at the Utah C++ Programmers Group" ' + 'post_url="#" post_date="03/03/2025" post_category="Issues" post_tag="beast" ' + 'author_name="Richard Thomson" author_role="Contributor" author_show_badge=True ' + 'author_avatar_url="https://ui-avatars.com/api/?name=Richard+Thomson&size=48" %}' + '{% include "v3/includes/_post_card_v3.html" %}' + "{% endwith %}" + ) + return template.render(Context({})) + + def post_cards_list(self, **kwargs): + """ + A list of post cards inside the standard post-cards section wrapper. + """ + template = Template( + """ +
+

+ Posts from the Boost community +

+ +
+ {% include "v3/includes/_button.html" with label="View all posts" url="#" %} +
+
+ """ + ) + return template.render(Context({})) + + +class MailingListCardPreview(LookbookPreview): + + def default(self, **kwargs): + """ + Mailing list sign-up card with email input and subscribe button. + + Template: `v3/includes/_mailing_list_card.html` + + Self-contained component with no required variables. + """ + return render_to_string("v3/includes/_mailing_list_card.html") + + +class ThreadArchiveCardPreview(LookbookPreview): + + def default(self, **kwargs): + """ + Thread archive card linking to the Boost mailing list archive. + + Template: `v3/includes/_thread_archive_card.html` + + Self-contained component with no required variables. + """ + return render_to_string("v3/includes/_thread_archive_card.html") + + +class LibraryIntroCardPreview(LookbookPreview): + + def default(self, **kwargs): + """ + Library introduction card with authors and CTA. + + Template: `v3/includes/_library_intro_card.html` + + | Variable | Required | Description | + |---|---|---| + | `library_name` | Yes | Library display name | + | `description` | Yes | Short library description | + | `authors` | No | List of author objects (name, role, avatar_url, badge, badge_url, bio) | + | `cta_label` | No | Button text | + | `cta_url` | No | Button link URL | + """ + try: + from libraries.utils import build_library_intro_context + from libraries.models import LibraryVersion + from versions.models import Version + + latest = Version.objects.most_recent() + if latest: + lv = ( + LibraryVersion.objects.filter(version=latest, library__slug="beast") + .select_related("library") + .first() + ) + if lv: + ctx = build_library_intro_context(lv) + return render_to_string("v3/includes/_library_intro_card.html", ctx) + except Exception: + pass + return render_to_string( + "v3/includes/_library_intro_card.html", + { + "library_name": "Boost.Beast", + "description": "Portable HTTP, WebSocket, and network operations using only C++11 and Boost.Asio", + "authors": [ + { + "name": "Vinnie Falco", + "role": "Author", + "avatar_url": "https://ui-avatars.com/api/?name=Vinnie+Falco&size=48", + "badge": "\U0001f947", + "bio": "", + }, + { + "name": "Mohammad Nejati", + "role": "Maintainer", + "avatar_url": "https://ui-avatars.com/api/?name=Mohammad+Nejati&size=48", + "badge": "\U0001f948", + "bio": "", + }, + { + "name": "dvtate", + "role": "Contributor", + "avatar_url": "https://ui-avatars.com/api/?name=dvtate&size=48", + "badge": "\U0001f949", + "bio": "", + }, + ], + "cta_url": "#", + }, + ) diff --git a/previews/carousel_preview.py b/previews/carousel_preview.py new file mode 100644 index 000000000..2603f3704 --- /dev/null +++ b/previews/carousel_preview.py @@ -0,0 +1,152 @@ +from django_lookbook.preview import LookbookPreview +from django.template.loader import render_to_string + + +SAMPLE_CARDS = [ + { + "title": "Get help", + "description": "Tap into quick answers, networking, and chat with 24,000+ members.", + "icon_name": "info-box", + "cta_label": "Start here", + "cta_href": "#", + }, + { + "title": "Documentation", + "description": "Browse library docs, examples, and release notes in one place.", + "icon_name": "link", + "cta_label": "View docs", + "cta_href": "#", + }, + { + "title": "Community", + "description": "Mailing lists, GitHub, and community guidelines for contributors.", + "icon_name": "human", + "cta_label": "Join", + "cta_href": "#", + }, + { + "title": "Releases", + "description": "Latest releases, download links, and release notes.", + "icon_name": "info-box", + "cta_label": "Download", + "cta_href": "#", + }, + { + "title": "Libraries", + "description": "Explore the full catalog of Boost C++ libraries with docs and metadata.", + "icon_name": "link", + "cta_label": "Browse libraries", + "cta_href": "#", + }, + { + "title": "News", + "description": "Blog posts, announcements, and community news from the Boost project.", + "icon_name": "device-tv", + "cta_label": "Read news", + "cta_href": "#", + }, + { + "title": "Getting started", + "description": "Step-by-step guides to build and use Boost in your projects.", + "icon_name": "bullseye-arrow", + "cta_label": "Get started", + "cta_href": "#", + }, + { + "title": "Resources", + "description": "Learning resources, books, and other materials for Boost users.", + "icon_name": "get-help", + "cta_label": "View resources", + "cta_href": "#", + }, + { + "title": "Calendar", + "description": "Community events, meetings, and review schedule.", + "icon_name": "info-box", + "cta_label": "View calendar", + "cta_href": "#", + }, + { + "title": "Donate", + "description": "Support the Boost Software Foundation and open-source C++.", + "icon_name": "human", + "cta_label": "Donate", + "cta_href": "#", + }, +] + + +class CarouselButtonsPreview(LookbookPreview): + + def default(self, **kwargs): + """ + Standalone carousel navigation buttons (previous / next). + + Template: `v3/includes/_carousel_buttons.html` + + | Variable | Required | Description | + |---|---|---| + | `carousel_id` | No | ID prefix for JS hooks | + | `prev_label` | No | Aria-label for previous button. Default: "Previous" | + | `next_label` | No | Aria-label for next button. Default: "Next" | + """ + return render_to_string("v3/includes/_carousel_buttons.html") + + +class CardsCarouselPreview(LookbookPreview): + + def default(self, **kwargs): + """ + Detail card carousel — horizontal scrolling cards with heading and navigation. + + Template: `v3/includes/_cards_carousel_v3.html` + + | Variable | Required | Description | + |---|---|---| + | `carousel_id` | Yes | Unique id for the carousel | + | `heading` | Yes | Section heading | + | `cards` | No | List of dicts with: title, description, icon_name, cta_label, cta_href | + | `track_aria_label` | No | Aria-label for the track. Default: "Detail cards carousel" | + | `infinite` | No | If True, carousel loops. Default: False | + | `autoplay` | No | If True, auto-scrolls. Default: False | + | `autoplay_delay` | No | Autoplay interval in ms. Default: 4000 | + """ + return render_to_string( + "v3/includes/_cards_carousel_v3.html", + { + "carousel_id": "lookbook-carousel-default", + "heading": "Libraries categories", + "cards": SAMPLE_CARDS, + }, + ) + + def with_autoplay(self, **kwargs): + """ + Cards carousel with autoplay enabled (5-second interval). + """ + return render_to_string( + "v3/includes/_cards_carousel_v3.html", + { + "carousel_id": "lookbook-carousel-autoplay", + "heading": "Libraries categories", + "cards": SAMPLE_CARDS, + "autoplay": True, + "autoplay_delay": 5000, + }, + ) + + def with_infinite_and_autoplay(self, **kwargs): + """ + Cards carousel with infinite looping and autoplay enabled. + """ + return render_to_string( + "v3/includes/_cards_carousel_v3.html", + { + "carousel_id": "lookbook-carousel-infinite", + "heading": "Libraries categories", + "cards": SAMPLE_CARDS, + "infinite": True, + "autoplay": True, + "autoplay_delay": 5000, + }, + ) diff --git a/previews/code_blocks_preview.py b/previews/code_blocks_preview.py new file mode 100644 index 000000000..0b94fa9d3 --- /dev/null +++ b/previews/code_blocks_preview.py @@ -0,0 +1,179 @@ +from django_lookbook.preview import LookbookPreview +from django.template.loader import render_to_string + + +CODE_CPP = """int main() +{ + net::io_context ioc; + tcp::resolver resolver(ioc); + beast::tcp_stream stream(ioc); + + stream.connect(resolver.resolve("example.com", "80")); + + http::request req{http::verb::get, "/", 11}; + req.set(http::field::host, "example.com"); + + http::write(stream, req); + + beast::flat_buffer buffer; + http::response res; + http::read(stream, buffer, res); + + std::cout << res << std::endl; +}""" + +CODE_HELLO = """#include +int main() +{ + std::cout << "Hello, Boost."; +}""" + +CODE_INSTALL = """brew install openssl + +export OPENSSL_ROOT=$(brew --prefix openssl) + +# Install bjam tool user config: https://www.bfgroup.xyz/b2/manual/release/index.html +cp ./libs/beast/tools/user-config.jam $HOME""" + + +class CodeBlockPreview(LookbookPreview): + + def standalone(self, **kwargs): + """ + Standalone code block with copy button and syntax highlighting. + + Template: `v3/includes/_code_block.html` + + | Variable | Required | Description | + |---|---|---| + | `code` | No | Plain text string (auto-escaped, highlight.js processes it) | + | `code_html` | No | Pre-rendered HTML with spans. Use `code` or `code_html` | + | `variant` | No | "standalone", "white-bg", or "grey-bg". Default: "standalone" | + | `language` | No | Language for highlight.js (e.g. "cpp", "bash"). Default: "cpp" | + | `cpp_highlight` | No | If truthy with code_html, adds cpp-highlight class | + """ + return render_to_string( + "v3/includes/_code_block.html", + { + "code": CODE_CPP, + "variant": "standalone", + "language": "cpp", + }, + ) + + def white_background(self, **kwargs): + """ + Code block with white background variant. + """ + return render_to_string( + "v3/includes/_code_block.html", + { + "code": CODE_CPP, + "variant": "white-bg", + "language": "cpp", + }, + ) + + def grey_background(self, **kwargs): + """ + Code block with grey background variant. + """ + return render_to_string( + "v3/includes/_code_block.html", + { + "code": CODE_CPP, + "variant": "grey-bg", + "language": "cpp", + }, + ) + + +class CodeBlockCardPreview(LookbookPreview): + + def with_button(self, **kwargs): + """ + Code block card with heading and a CTA button below the code. + + Template: `v3/includes/_code_block_card.html` + + | Variable | Required | Description | + |---|---|---| + | `card_variant` | No | "neutral" or "teal". Default: "neutral" | + | `heading` | Yes | Card heading | + | `description` | No | Paragraph below heading | + | `code` | No | Plain text code string | + | `code_html` | No | Pre-rendered HTML code | + | `language` | No | Language for highlight.js | + | `block_variant` | No | Code block variant. Default: "grey-bg" | + | `button_text` | No | If set, shows a button | + | `button_url` | No | Button href. Default: "#" | + | `button_aria_label` | No | Accessible name for the button | + """ + return render_to_string( + "v3/includes/_code_block_card.html", + { + "heading": "Get started with our libraries", + "code": CODE_HELLO, + "block_variant": "grey-bg", + "button_text": "Explore examples", + "language": "cpp", + "button_aria_label": "Explore examples", + }, + ) + + def neutral_with_description(self, **kwargs): + """ + Neutral code block card with a description paragraph. + """ + return render_to_string( + "v3/includes/_code_block_card.html", + { + "card_variant": "neutral", + "heading": "About Beast", + "description": "Beast lets you use HTTP and WebSocket to write clients and servers that connect to networks using Boost.Asio.", + "code": CODE_CPP, + "block_variant": "grey-bg", + "language": "cpp", + }, + ) + + def teal_variant(self, **kwargs): + """ + Teal-themed code block card with bash content. + """ + return render_to_string( + "v3/includes/_code_block_card.html", + { + "card_variant": "teal", + "heading": "Install", + "description": "Get started with header-only libraries.", + "code": CODE_INSTALL, + "block_variant": "grey-bg", + "language": "bash", + }, + ) + + def full_story_layout(self, **kwargs): + """ + Full two-column code blocks layout with standalone blocks and cards. + + Template: `v3/includes/_code_blocks_story.html` + + | Variable | Required | Description | + |---|---|---| + | `code_standalone_1` | Yes | Code for first standalone block | + | `code_standalone_2` | Yes | Code for second standalone block | + | `code_card_1` | Yes | Code for first card | + | `code_card_2` | Yes | Code for second card | + | `code_card_3` | Yes | Code for third card | + """ + return render_to_string( + "v3/includes/_code_blocks_story.html", + { + "code_standalone_1": CODE_CPP, + "code_standalone_2": CODE_CPP, + "code_card_1": CODE_HELLO, + "code_card_2": CODE_CPP, + "code_card_3": CODE_INSTALL, + }, + ) diff --git a/previews/content_preview.py b/previews/content_preview.py new file mode 100644 index 000000000..f06fefaed --- /dev/null +++ b/previews/content_preview.py @@ -0,0 +1,220 @@ +from django_lookbook.preview import LookbookPreview +from django.template import Context, Template +from django.template.loader import render_to_string + + +class ContentDetailCardPreview(LookbookPreview): + + def with_icon_and_link(self, **kwargs): + """ + Content detail card with an icon and a linked title. + + Template: `v3/includes/_content_detail_card_item.html` + + | Variable | Required | Description | + |---|---|---| + | `title` | Yes | Card heading | + | `description` | Yes | Card body text | + | `icon_name` | No | Icon name (e.g. "bullseye-arrow"). If omitted, no icon is rendered | + | `title_url` | No | If set, title becomes a link | + | `cta_label` | No | CTA link text (used with `cta_href`) | + | `cta_href` | No | CTA link URL | + """ + template = Template( + '
' + "
" + '{% include "v3/includes/_content_detail_card_item.html" with title="Get help" description="Tap into quick answers, networking, and chat with 24,000+ members." icon_name="get-help" title_url="/help" %}' + "
" + "
" + '{% include "v3/includes/_content_detail_card_item.html" with title="Another card" description="With a different icon. Icon is optional and dynamic." icon_name="device-tv" %}' + "
" + "
" + ) + return template.render(Context({})) + + def with_cta(self, **kwargs): + """ + Content detail card with icon and a CTA link below the description. + """ + return render_to_string( + "v3/includes/_content_detail_card_item.html", + { + "title": "Get help", + "description": "Tap into quick answers, networking, and chat with 24,000+ members.", + "icon_name": "info-box", + "cta_label": "Start here", + "cta_href": "#", + }, + ) + + def without_icon(self, **kwargs): + """ + Content detail card without an icon — renders without the `--has-icon` modifier. + """ + return render_to_string( + "v3/includes/_content_detail_card_item.html", + { + "title": "Plain card", + "description": "This variant has no icon. Useful for simpler layouts.", + }, + ) + + +class ContentEventCardPreview(LookbookPreview): + + def as_list_items(self, **kwargs): + """ + Content event cards rendered as a list (no card wrapper). + + Template: `v3/includes/_content_event_card_item.html` + + | Variable | Required | Description | + |---|---|---| + | `title` | Yes | Event title | + | `description` | Yes | Event description | + | `date` | Yes | Human-readable date | + | `datetime` | No | Value for `