diff --git a/config/urls.py b/config/urls.py index 36a46efeb..8e315a088 100755 --- a/config/urls.py +++ b/config/urls.py @@ -1,6 +1,7 @@ import logging from django.conf import settings +from django.contrib.admin.views.decorators import staff_member_required from django.conf.urls.static import static from django.contrib import admin from django.urls import include, path, re_path, register_converter, reverse_lazy @@ -21,22 +22,23 @@ from config.settings import DEBUG_TOOLBAR from core.views import ( BSLView, + BoostDevelopmentView, CalendarView, ClearCacheView, DocLibsTemplateView, ImageView, MarkdownTemplateView, + V3ComponentDemoView, + ModernizedDocsView, RedirectToDocsView, RedirectToHTMLDocsView, RedirectToHTMLToolsView, RedirectToLibrariesView, + RedirectToLibraryDetailView, RedirectToReleaseView, RedirectToToolsView, StaticContentTemplateView, UserGuideTemplateView, - BoostDevelopmentView, - ModernizedDocsView, - RedirectToLibraryDetailView, ) from marketing.views import PlausibleRedirectView, WhitePaperView from libraries.api import LibrarySearchView @@ -241,6 +243,11 @@ TemplateView.as_view(template_name="style_guide.html"), name="style-guide", ), + path( + "v3/demo/components/", + staff_member_required(V3ComponentDemoView.as_view()), + name="v3-demo-components", + ), path("libraries/", LibraryListDispatcher.as_view(), name="libraries"), path( "libraries///", diff --git a/core/views.py b/core/views.py index bc247ac4a..b076de929 100644 --- a/core/views.py +++ b/core/views.py @@ -1,3 +1,4 @@ +import json import os import re @@ -1034,3 +1035,51 @@ def get(self, request: HttpRequest, campaign_identifier: str, main_path: str = " redirect_path = f"{redirect_path}?{qs}" return HttpResponseRedirect(redirect_path) + + +class V3ComponentDemoView(TemplateView): + """Demo page for V3 design system components.""" + + template_name = "base.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["popular_terms"] = [ + {"label": "Networking"}, + {"label": "Math"}, + {"label": "Data processing"}, + {"label": "Concurrency"}, + {"label": "File systems"}, + {"label": "Testing"}, + ] + context["demo_libs_json"] = json.dumps( + [ + {"value": "asio", "label": "Asio"}, + {"value": "beast", "label": "Beast"}, + {"value": "filesystem", "label": "Filesystem"}, + {"value": "json", "label": "JSON"}, + {"value": "spirit", "label": "Spirit"}, + ] + ) + context["demo_cats_json"] = json.dumps( + [ + {"value": "algorithms", "label": "Algorithms"}, + {"value": "containers", "label": "Containers"}, + {"value": "io", "label": "I/O"}, + {"value": "math", "label": "Math & Numerics"}, + {"value": "networking", "label": "Networking"}, + ] + ) + from libraries.models import LibraryVersion + from libraries.utils import build_library_intro_context + + latest = Version.objects.most_recent() + if latest: + lv = ( + LibraryVersion.objects.filter(version=latest, library__slug="beast") + .select_related("library") + .first() + ) + if lv: + context["library_intro"] = build_library_intro_context(lv) + return context diff --git a/docs/django-waffle-v3-flag.md b/docs/django-waffle-v3-flag.md new file mode 100644 index 000000000..934d922b2 --- /dev/null +++ b/docs/django-waffle-v3-flag.md @@ -0,0 +1,133 @@ +# Django Waffle — Feature flags + +This document describes how **feature flags** work in the Boost website project (django-waffle), how to assign and manage them, and how the **v3** flag is set up. + +--- + +## How feature flags work in this project + +- **Library:** [django-waffle](https://github.com/jazzband/django-waffle) (v5.0.0). Flags are stored in the database and evaluated per request (and optionally per user, group, or session). +- **Middleware:** `waffle.middleware.WaffleMiddleware` runs on each request so that flag state is available in templates and views. +- **Settings** (`config/settings.py`): + - **`WAFFLE_CREATE_MISSING_FLAGS = True`** — The first time a flag name is used in code (e.g. `{% flag "v3" %}` or `flag_is_active(request, "v3")`), it is auto-created in the database with default **off**. + - **`WAFFLE_FLAG_DEFAULT = False`** — Newly created flags have “Everyone” = No; they only become active when explicitly enabled for users, groups, percentage, etc. +- **Evaluation:** For each request, waffle checks the flag’s rules (everyone, superusers, staff, authenticated, groups, users, percentage, testing cookie). If any rule matches, the flag is **active** for that request; otherwise it is **inactive**. Anonymous users only get a flag if “Everyone” is Yes or a percentage/testing rule applies; group-based activation applies only to authenticated users in the chosen groups. + +So in short: **flags are off by default**, and you turn them on by assigning **groups**, **users**, or other options in the Django admin. + +--- + +## How to assign feature flags + +Feature flags are managed in the **Django Admin** under **Waffle**. + +### 1. Create or open a flag + +- Go to **Django Admin** → **Waffle** → **Flags** (`/admin/waffle/flag/`). +- Either: + - **Create** a new flag (Name = e.g. `v3`), or + - **Open** an existing flag (e.g. `v3`). + +With `WAFFLE_CREATE_MISSING_FLAGS = True`, the flag may already exist because it was used in code; you only need to edit it. + +### 2. Choose who gets the flag + +On the flag’s edit page you can enable the flag for: + +- **Everyone** — Yes/No/Unknown. “Yes” turns the flag on for all requests (use with care). +- **Superusers** — If checked, the flag is always on for superusers. +- **Staff** — If checked, the flag is on for staff users. +- **Authenticated** — If checked, the flag is on for any logged-in user. +- **Groups (Chosen groups)** — The flag is on only for users who belong to at least one of the selected groups. This is the usual way to target testers (e.g. `v3_testers`). +- **Users (Chosen users)** — The flag is on only for the selected users. +- **Percentage** — Roll out to a percentage of users (0.0–99.9). +- **Testing** — When enabled, the flag can be toggled via a query/cookie for testing (see waffle docs). + +For a **group-based** flag (e.g. v3): + +1. In **Chosen groups**, move the desired group (e.g. **v3_testers**) from “Available groups” to “Chosen groups”. +2. Leave **Everyone** as “No” (or “Unknown”) so only the chosen group sees the flag. +3. Click **Save**. + +### 3. Put users into the group + +Group-based flags only apply to **authenticated** users who are **members** of one of the chosen groups. + +- Go to **Users** (or **Auth** → **Users** / **Users** → **Users**, depending on your project). +- Open the **user** that should see the flag. +- In **Groups** (or “User groups”), add the group (e.g. **v3_testers**). +- Save. + +After that, when that user is logged in, the flag is active for their requests. Log out and back in (or use a fresh session) if you don’t see the change. + +--- + +## The “v3” flag and banner + +The **v3** flag is used to show a banner to users who are part of the v3 rollout. + +### What was added in the repo + +- **django-waffle** in `requirements.txt` (v5.0.0). +- **Config** in `config/settings.py`: `waffle` in `INSTALLED_APPS`, `waffle.middleware.WaffleMiddleware` in `MIDDLEWARE`, `WAFFLE_CREATE_MISSING_FLAGS = True`, `WAFFLE_FLAG_DEFAULT = False`. +- **Banner** in `templates/base.html`: `{% load waffle_tags %}` and a block `{% flag "v3" %} ... {% endflag %}` that shows a grey “v3 flag enabled” bar at the top of the page when the flag is active for the requesting user. +- **Data migration** `users/migrations/0021_add_v3_testers_group.py` creates the Django auth group **v3_testers**, which can be assigned to the v3 flag. + +### How to enable the v3 banner for yourself + +1. **Apply migrations** (so the `v3_testers` group exists): + ```bash + just migrate + # or: docker compose run --rm web python manage.py migrate + ``` +2. **Admin** → **Waffle** → **Flags** → open (or create) the **v3** flag. +3. In **Chosen groups**, add **v3_testers** and save. +4. **Admin** → **Users** → open **your user** → add **v3_testers** to Groups → save. +5. Log in on the site and reload; the **“v3 flag enabled”** banner should appear at the top. + +### If the “v3 flag enabled” banner does not appear + +- You must be **logged in**; group-based flags do not apply to anonymous users. +- Your **user** must be in the **v3_testers** group (Users → your user → Groups). +- **Log out and log back in** (or use a new incognito session) so the session reflects the group. +- Confirm the **v3** flag is saved with **v3_testers** in Chosen groups. + +--- + +## Where it is in the codebase + +| What | Where | +|------|--------| +| Conditional banner | `templates/base.html`: `{% load waffle_tags %}` and `{% flag "v3" %} ... {% endflag %}` | +| Waffle config | `config/settings.py`: `INSTALLED_APPS`, `MIDDLEWARE`, `WAFFLE_*` | +| v3_testers group | `users/migrations/0021_add_v3_testers_group.py` | +| Package | `requirements.txt`: `django-waffle==5.0.0` | + +--- + +## Using flags in Python (views) + +```python +from waffle import flag_is_active + +def my_view(request): + if flag_is_active(request, "v3"): + # Flag is active for this request (e.g. user in v3_testers) + ... + else: + ... +``` + +--- + +## Using flags in templates + +```django +{% load waffle_tags %} + +{% flag "v3" %} +
Content only shown when the v3 flag is active for this user.
+{% endflag %} +``` + +You can use the same pattern for any flag name (e.g. `{% flag "my_feature" %}`) and assign it via groups or users in the admin as described above. diff --git a/libraries/mixins.py b/libraries/mixins.py index 19d2e6c96..f92aa34e0 100644 --- a/libraries/mixins.py +++ b/libraries/mixins.py @@ -1,10 +1,8 @@ import re import structlog -from types import SimpleNamespace from django.db.models import Count, Exists, OuterRef -from django.db.models.functions import Lower from django.shortcuts import get_object_or_404 from django.urls import reverse @@ -17,11 +15,11 @@ from libraries.models import ( Commit, CommitAuthor, - CommitAuthorEmail, Library, LibraryVersion, ) from libraries.path_matcher.utils import determine_latest_url +from libraries.utils import patch_commit_authors from versions.models import Version logger = structlog.get_logger() @@ -232,23 +230,7 @@ def get_related(self, library_version, relation="maintainers", exclude_ids=None) if exclude_ids: qs = qs.exclude(id__in=exclude_ids) qs = list(qs) - commit_authors = { - author_email.email: author_email - for author_email in CommitAuthorEmail.objects.annotate( - email_lower=Lower("email") - ) - .filter(email_lower__in=[x.email.lower() for x in qs]) - .select_related("author") - } - for user in qs: - if author_email := commit_authors.get(user.email.lower(), None): - user.commitauthor = author_email.author - else: - user.commitauthor = SimpleNamespace( - github_profile_url="", - avatar_url="", - display_name=f"{user.display_name}", - ) + patch_commit_authors(qs) return qs def get_author_tag(self, library_version): diff --git a/libraries/utils.py b/libraries/utils.py index b4e227ea7..5e85cfbe1 100644 --- a/libraries/utils.py +++ b/libraries/utils.py @@ -2,6 +2,7 @@ import string import re from itertools import islice +from types import SimpleNamespace import boto3 import structlog @@ -13,6 +14,9 @@ from dateutil.parser import ParserError, parse from django.conf import settings +from django.db.models import Count +from django.db.models.functions import Lower +from django.urls import reverse from django.utils.text import slugify from libraries.constants import ( @@ -366,6 +370,126 @@ def parse_line(line: str): ) +def patch_commit_authors(users): + """Patch CommitAuthor data onto a list of User objects. + + For each user, looks up their email in CommitAuthorEmail and attaches + the matching CommitAuthor (with avatar_url, github_profile_url, + display_name) as user.commitauthor. Falls back to a SimpleNamespace + stub when no match is found. + """ + from libraries.models import CommitAuthorEmail + + commit_authors = { + author_email.email: author_email + for author_email in CommitAuthorEmail.objects.annotate( + email_lower=Lower("email") + ) + .filter(email_lower__in=[u.email.lower() for u in users]) + .select_related("author") + } + for user in users: + if author_email := commit_authors.get(user.email.lower(), None): + user.commitauthor = author_email.author + else: + user.commitauthor = SimpleNamespace( + github_profile_url="", + avatar_url="", + display_name=f"{user.display_name}", + ) + return users + + +def build_library_intro_context(library_version, *, max_authors=3): + """Build template context for the library intro card. + + Returns a dict with keys: library_name, description, authors, cta_url. + + Matches the detail page's ordering: authors first, then maintainers + (excluding duplicates), then top git contributors to fill remaining slots. + """ + from libraries.models import CommitAuthor + + library = library_version.library + + # Authors first, then maintainers — same order as ContributorMixin + authors = list(library_version.authors.all()) + author_ids = {a.id for a in authors} + maintainers = list(library_version.maintainers.exclude(id__in=author_ids)) + + combined = (authors + maintainers)[:max_authors] + roles = {} + for user in combined: + roles[user.id] = "Author" if user.id in author_ids else "Maintainer" + + # Fill remaining slots with top git contributors + remaining = max_authors - len(combined) + if remaining > 0: + exclude_commit_author_ids = [] + patch_commit_authors(combined) + for user in combined: + ca_id = getattr(user.commitauthor, "id", None) + if ca_id: + exclude_commit_author_ids.append(ca_id) + + top_contributors = ( + CommitAuthor.objects.filter(commit__library_version=library_version) + .exclude(id__in=exclude_commit_author_ids) + .annotate(count=Count("commit")) + .order_by("-count")[:remaining] + ) + else: + top_contributors = [] + patch_commit_authors(combined) + + def get_avatar(user): + url = user.get_thumbnail_url() + if url: + return url + return getattr(user.commitauthor, "avatar_url", "") or "" + + medals = ["🥇", "🥈", "🥉"] + + author_dicts = [] + for user in combined: + author_dicts.append( + { + "name": user.display_name or user.get_full_name(), + "role": roles[user.id], + "avatar_url": get_avatar(user), + "badge": ( + medals[len(author_dicts)] if len(author_dicts) < len(medals) else "" + ), + "bio": "", + } + ) + for ca in top_contributors: + author_dicts.append( + { + "name": ca.display_name, + "role": "Contributor", + "avatar_url": ca.avatar_url or "", + "badge": ( + medals[len(author_dicts)] if len(author_dicts) < len(medals) else "" + ), + "bio": "", + } + ) + + return { + "library_name": library.display_name, + "description": library_version.description or library.description or "", + "authors": author_dicts, + "cta_url": reverse( + "library-detail", + kwargs={ + "version_slug": LATEST_RELEASE_URL_PATH_STR, + "library_slug": library.slug, + }, + ), + } + + def update_base_tag(html: str, base_uri: str): """ Replace the base tag href with the new base_uri diff --git a/static/css/v3/avatar.css b/static/css/v3/avatar.css new file mode 100644 index 000000000..d26beacd2 --- /dev/null +++ b/static/css/v3/avatar.css @@ -0,0 +1,133 @@ +:root { + --avatar-size-sm: 32px; + --avatar-size-md: 40px; + --avatar-size-lg: 44px; + --avatar-size-xl: 48px; +} + +/* Mobile avatar size overrides */ +@media (max-width: 767px) { + :root { + --avatar-size-sm: 28px; + --avatar-size-md: 36px; + --avatar-size-lg: 40px; + --avatar-size-xl: 40px; + } +} + +/* ========== Avatar base ========== */ +.avatar { + width: var(--avatar-size-md); + height: var(--avatar-size-md); + flex-shrink: 0; + border-radius: var(--border-radius-m); + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; +} + +/* Avatar size modifiers */ +.avatar.avatar--sm { + width: var(--avatar-size-sm); + height: var(--avatar-size-sm); +} + +.avatar.avatar--md { + width: var(--avatar-size-md); + height: var(--avatar-size-md); +} + +.avatar.avatar--lg { + width: var(--avatar-size-lg); + height: var(--avatar-size-lg); +} + +.avatar.avatar--xl { + width: var(--avatar-size-xl); + height: var(--avatar-size-xl); +} + +/* Image inside avatar container: fill and cover so it's contained and cropped */ +.avatar img.avatar__img { + width: 100%; + height: 100%; + display: block; + object-fit: cover; + object-position: center; +} + +.avatar__initials { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + font-family: var(--font-display); + font-size: var(--font-size-base); + font-weight: var(--font-weight-medium); + color: var(--color-text-on-accent); + line-height: 1; +} + +/* Scale initials font by avatar size */ +.avatar.avatar--sm .avatar__initials { + font-size: var(--font-size-small); +} + +.avatar.avatar--md .avatar__initials { + font-size: var(--font-size-small); +} + +.avatar.avatar--lg .avatar__initials { + font-size: var(--font-size-small); +} + +.avatar.avatar--xl .avatar__initials { + font-size: var(--font-size-base); +} + +/* Initials variants (no image): same element has .avatar.avatar--* .avatar__initials */ +.avatar.avatar--yellow { + background: var(--color-accent-weak-yellow); +} + +.avatar.avatar--green { + background: var(--color-accent-weak-green); +} + +.avatar.avatar--teal { + background: var(--color-accent-weak-teal); +} + +/* Placeholder (e.g. Header when no image and no name) */ +.avatar-placeholder { + width: var(--avatar-size-md); + height: var(--avatar-size-md); + flex-shrink: 0; + border-radius: var(--border-radius-m); + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + background: var(--color-surface-mid); + color: var(--color-text-secondary); + font-size: var(--font-size-xs); + font-weight: 600; + line-height: 1; +} + +.avatar-placeholder.avatar--sm { + width: var(--avatar-size-sm); + height: var(--avatar-size-sm); +} + +.avatar-placeholder.avatar--lg { + width: var(--avatar-size-lg); + height: var(--avatar-size-lg); +} + +.avatar-placeholder.avatar--xl { + width: var(--avatar-size-xl); + height: var(--avatar-size-xl); +} diff --git a/static/css/v3/border-radius.css b/static/css/v3/border-radius.css new file mode 100644 index 000000000..8a9de939e --- /dev/null +++ b/static/css/v3/border-radius.css @@ -0,0 +1,30 @@ +/** + * Border Radius Tokens + * + * Border radius tokens define consistent corner rounding values. + * + * Naming Convention: --border-radius-{size} + * Example: --border-radius-s, --border-radius-xl + */ + +:root { + /* Border Radius Scale (Desktop) */ + --border-radius-xs: 2px; + --border-radius-s: 4px; + --border-radius-m: 6px; + --border-radius-l: 8px; + --border-radius-xl: 12px; + --border-radius-xxl: 16px; +} + +/* Mobile Border Radius Overrides */ +@media (max-width: 767px) { + :root { + --border-radius-xs: 1px; + --border-radius-s: 3px; + --border-radius-m: 4px; + --border-radius-l: 6px; + --border-radius-xl: 10px; + --border-radius-xxl: 12px; + } +} diff --git a/static/css/v3/button-tooltip.css b/static/css/v3/button-tooltip.css new file mode 100644 index 000000000..04ddb6e00 --- /dev/null +++ b/static/css/v3/button-tooltip.css @@ -0,0 +1,136 @@ +/** + * Tooltip button – icon or icon+text trigger with hover tooltip. + * Use with v3/includes/_button_tooltip_v3.html + */ + +.btn-tooltip { + width: 32px; + height: 32px; + padding: 0; + border-radius: var(--border-radius-l, 8px); + background-color: var(--color-button-primary, #efeff1); + border: 1px solid var(--color-border, #d5d6d8); + color: var(--color-icon-primary, #050816); +} +.btn-tooltip:hover { + background-color: var(--color-primary-grey-300, #d5d6d8); +} + +html.dark .btn-tooltip { + background-color: var(--color-button-primary); + border-color: var(--color-border); + color: var(--color-icon-primary); +} +html.dark .btn-tooltip:hover { + background-color: var(--color-primary-grey-700); +} + +.btn-tooltip .btn-icon { + width: 16px; + height: 16px; +} +.btn-tooltip--with-text { + width: auto; + min-width: 32px; + height: 32px; + padding: 0 var(--space-default, 8px); + gap: var(--space-s, 4px); +} + +.tooltip-wrapper { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; +} + + +:where(.tooltip-label) { + position: absolute; + padding: var(--space-s, 4px) var(--space-default, 8px); + background: var(--color-primary-grey-900, #2f313d); + color: var(--color-text-reversed, #fff); + font-size: var(--font-size-xs, 12px); + font-weight: var(--font-weight-regular, 400); + line-height: 1.25; + border-radius: var(--border-radius-s, 4px); + white-space: nowrap; + z-index: 10; + opacity: 0; + visibility: hidden; + transition: opacity 0.15s ease, visibility 0.15s ease, transform 0.15s ease; + transition-delay: 0s; + pointer-events: none; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); +} +.tooltip-wrapper:hover .tooltip-label, +.tooltip-wrapper:focus-within .tooltip-label { + opacity: 1; + visibility: visible; + transition-delay: 0.1s; +} + +.tooltip-label::after { + content: ""; + position: absolute; + border: 5px solid transparent; +} + +.tooltip-label.top { + bottom: 100%; + left: 50%; + transform: translate(-50%, -8px); +} +.tooltip-label.top::after { + top: 100%; + left: 50%; + margin-left: -5px; + border-top-color: var(--color-primary-grey-900, #2f313d); +} + +.tooltip-label.bottom { + top: 100%; + left: 50%; + transform: translate(-50%, 8px); +} +.tooltip-label.bottom::after { + bottom: 100%; + left: 50%; + margin-left: -5px; + border-bottom-color: var(--color-primary-grey-900, #2f313d); +} + +.tooltip-label.left { + right: 100%; + top: 50%; + transform: translate(-8px, -50%); +} +.tooltip-label.left::after { + left: 100%; + top: 50%; + margin-top: -5px; + border-left-color: var(--color-primary-grey-900, #2f313d); +} + +.tooltip-label.right { + left: 100%; + top: 50%; + transform: translate(8px, -50%); +} +.tooltip-label.right::after { + right: 100%; + top: 50%; + margin-top: -5px; + border-right-color: var(--color-primary-grey-900, #2f313d); +} + +html.dark .tooltip-wrapper .tooltip-label { + background: #ffffff !important; + color: #050816 !important; + border: 1px solid rgba(255, 255, 255, 0.25) !important; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35); +} +html.dark .tooltip-wrapper .tooltip-label.top::after { border-top-color: #ffffff !important; } +html.dark .tooltip-wrapper .tooltip-label.bottom::after { border-bottom-color: #ffffff !important; } +html.dark .tooltip-wrapper .tooltip-label.left::after { border-left-color: #ffffff !important; } +html.dark .tooltip-wrapper .tooltip-label.right::after { border-right-color: #ffffff !important; } diff --git a/static/css/v3/buttons.css b/static/css/v3/buttons.css new file mode 100644 index 000000000..3c80c804a --- /dev/null +++ b/static/css/v3/buttons.css @@ -0,0 +1,240 @@ +:root { + --color-button-accent: var(--color-primary-orange-mustard); + --color-button-primary: var(--color-primary-grey-200); + --color-button-secondary: var(--color-primary-white); + + --button-padding-x: var(--space-default); + --button-padding-y: var(--space-s); + --button-gap: var(--space-default); + --button-radius: var(--border-radius-m); + --button-font-size: var(--font-size-small); + --button-font-weight: 600; + --button-line-height: var(--line-height-default); + --button-font-family: var(--font-sans); +} + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--space-default, 8px); + font-family: var(--font-sans); + font-size: var(--font-size-small, 14px); + font-weight: var(--font-weight-medium, 500); + line-height: var(--line-height-relaxed, 1.24); + letter-spacing: -0.01em; + border-radius: var(--border-radius-l, 8px); + border: 1px solid transparent; + min-height: 36px; + width: 128px; + padding: 8px 16px; + cursor: pointer; + transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease; +} + +.btn-row { + display: flex; + flex-wrap: wrap; + gap: 40px; + align-items: stretch; +} + +.btn-row>.btn-error { + flex: 0 0 128px; + width: 128px; + min-width: 128px; +} + +.btn-primary { + background-color: var(--color-button-secondary, #fff); + border-color: var(--color-stroke-strong, #05081640); + color: var(--color-text-primary, #050816); +} + +.btn-primary:hover { + background-color: var(--color-secondary-dark-blue, #00778B); + border-color: var(--color-secondary-dark-blue, #00778B); + color: var(--color-text-reversed, #fff); +} + +.btn-secondary { + border-color: var(--color-stroke-strong, #05081640); + color: var(--color-text-primary, #050816); +} + +.btn-secondary:hover { + background-color: var(--color-button-secondary, #fff); + border-color: var(--color-secondary-dark-blue, #0077B8); + color: var(--color-secondary-dark-blue, #0077B8); +} + +.btn-primary-outline { + background-color: var(--color-button-secondary, #fff); + border-color: var(--color-stroke-link-accent, #0077B8); + color: var(--color-text-link-accent, #0077B8); +} + +.btn-primary-outline:hover { + background-color: var(--color-secondary-light-blue, #f7fdfe); + border-color: var(--color-text-link-accent, #00778B); + color: var(--color-text-link-accent, #00778B); +} + +.btn-secondary-grey { + background-color: var(--color-button-primary, #efeff1); + border-color: var(--color-border, #d5d6d8); + color: var(--color-text-primary, #050816); +} + +.btn-secondary-grey:hover { + background-color: var(--color-primary-grey-300, #d5d6d8); + border-color: var(--color-primary-grey-300, #d5d6d8); + color: var(--color-text-primary, #050816); +} + +.btn-green { + background-color: var(--color-surface-strong-accent-green-default, #CACA62); + border-color: transparent; + color: var(--color-text-primary, #050816); +} + +.btn-green:hover { + background-color: var(--color-surface-strong-accent-green-hover, #CACA62BF); + border-color: transparent; + color: var(--color-text-primary, #050816); +} + +.btn-yellow { + background-color: var(--color-surface-strong-accent-yellow-default, #F5D039); + border-color: transparent; + color: var(--color-text-primary, #050816); +} + +.btn-yellow:hover { + background-color: var(--color-surface-strong-accent-yellow-hover, #F5D039BF); + border-color: transparent; + color: var(--color-text-primary, #050816); +} + +.btn-teal { + background-color: var(--color-surface-strong-accent-teal-default, #64DACE); + border-color: transparent; + color: var(--color-text-primary, #050816); +} + +.btn-teal:hover { + background-color: var(--color-surface-strong-accent-teal-hover, #64DACEBF); + border-color: transparent; + color: var(--color-text-primary, #050816); +} + +.btn-primary.btn-error, +.btn-error { + background-color: var(--color-surface-error-weak, #fdf2f2); + border-color: var(--color-stroke-error, #d53f3f33); + color: var(--color-text-error, #d32f2f); +} + +.btn-error:hover { + background-color: var(--color-surface-error-weak, #fdf2f2); + border-color: var(--color-error-strong, #d32f2f); + color: var(--color-text-error, #d32f2f); +} + +.btn-hero { + min-height: 48px; + padding: var(--space-medium, 12px) var(--space-large, 24px); + font-size: var(--font-size-base, 16px); + gap: var(--space-default, 8px); + width: auto; +} + +.btn.btn-hero.btn-primary { + background-color: var(--color-surface-brand-accent-default); + border-color: var(--color-surface-brand-accent-default); + color: var(--color-text-reversed); +} +.btn.btn-hero.btn-primary:hover { + background-color: var(--color-surface-brand-accent-hovered); + border-color: var(--color-surface-brand-accent-hovered); + color: var(--color-text-reversed); +} + +.btn-hero .btn-icon { + width: 20px; + height: 20px; + flex-shrink: 0; +} + +.btn-card-link { + display: inline-flex; + align-items: center; + gap: var(--space-s, 4px); + font-family: var(--font-sans); + font-size: var(--font-size-small, 14px); + font-weight: var(--font-weight-medium, 500); + color: var(--color-text-link-accent, #00778B); + background: none; + border: none; + padding: 0; + cursor: pointer; +} + +.btn-card-link:hover { + text-decoration: underline; +} + +.btn-card-link .btn-icon { + width: 16px; + height: 16px; +} + +.btn-list-link { + display: inline-flex; + align-items: center; + gap: var(--space-default, 8px); + font-family: var(--font-sans); + font-size: var(--font-size-base, 16px); + font-weight: var(--font-weight-regular, 400); + color: var(--color-text-primary, #050816); + background: none; + border: none; + padding: var(--space-s, 4px) 0; + cursor: pointer; + text-align: left; +} + +.btn-list-link:hover { + color: var(--color-text-link-accent, #00778B); +} + +.btn-list-link .btn-icon { + width: 20px; + height: 20px; + flex-shrink: 0; + color: var(--color-icon-link-accent, #00778B); +} + +.btn-icon-library { + display: inline-flex; + align-items: center; + gap: var(--space-default, 8px); + padding: var(--space-default, 8px) var(--space-medium, 12px); + font-family: var(--font-sans); + font-size: var(--font-size-small, 14px); + font-weight: var(--font-weight-regular, 400); + color: var(--color-text-primary, #050816); + background-color: var(--color-button-primary, #efeff1); + border: 1px solid var(--color-border, #d5d6d8); + border-radius: var(--border-radius-l, 8px); + cursor: pointer; +} + +.btn-icon-library:hover { + background-color: var(--color-primary-grey-300, #d5d6d8); +} + +.btn-icon-library .btn-icon { + width: 24px; + height: 24px; +} diff --git a/static/css/v3/cards.css b/static/css/v3/cards.css new file mode 100644 index 000000000..2f663e3af --- /dev/null +++ b/static/css/v3/cards.css @@ -0,0 +1,11 @@ + +:root { + --color-card-bg: var(--color-bg-secondary); + --color-card-border: var(--color-border); + --color-card-shadow: var(--color-stroke-weak); + --card-padding: var(--space-card); + --card-radius: var(--border-radius-l); + --card-radius-lg: var(--border-radius-xxl); + --card-gap: var(--space-default); + --card-shadow-value: 0 1px 3px var(--color-card-shadow); +} diff --git a/static/css/v3/carousel-buttons.css b/static/css/v3/carousel-buttons.css new file mode 100644 index 000000000..a6fc016e2 --- /dev/null +++ b/static/css/v3/carousel-buttons.css @@ -0,0 +1,44 @@ +/** + * Carousel buttons – single block with prev/next controls. + * Wrapper uses Surface-Strong background; inner buttons are borderless, icon turns blue on hover. + */ + +.carousel-buttons { + display: inline-flex; + padding: 4px; + align-items: center; + gap: 4px; + box-sizing: border-box; + width: 44px; + height: 24px; + border-radius: 8px; + background: var(--color-button-primary, #EFEFF1); +} + +.carousel-buttons .btn-carousel { + width: 16px; + height: 16px; + min-width: 16px; + min-height: 16px; + padding: 0; + background: transparent; + border: none; +} + +.carousel-buttons .btn-carousel .btn-icon { + width: 12px; + height: 12px; +} + +.carousel-buttons .btn-carousel .btn-icon svg { + width: 12px; + height: 12px; +} + +.carousel-buttons .btn-carousel:hover { + color: var(--color-secondary-dark-blue, #0077B8); +} + +.carousel-buttons .btn-carousel[data-hover] { + color: var(--color-secondary-dark-blue, #0077B8); +} diff --git a/static/css/v3/category-tags.css b/static/css/v3/category-tags.css new file mode 100644 index 000000000..f4094bb7b --- /dev/null +++ b/static/css/v3/category-tags.css @@ -0,0 +1,147 @@ +.category-tag { + display: inline-flex; + align-items: center; + justify-content: center; + font-family: var(--font-sans); + font-size: var(--font-size-xs); + line-height: var(--line-height-default); + font-weight: var(--font-weight-regular); + border: 1px solid; + border-radius: var(--border-radius-s); + text-decoration: none; + cursor: pointer; + transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease; +} + +.category-tag--default { + padding: var(--space-default) var(--space-default); +} + +.category-tag--tight { + padding: var(--space-s) var(--space-s); +} + +.category-tag--neutral { + background: var(--color-primary-grey-100); + border-color: var(--color-tag-stroke); + color: var(--color-text-secondary); +} + +.category-tag--neutral:hover, +.category-tag--neutral.category-tag--hover-state { + background: var(--color-primary-grey-200); + border-color: var(--color-tag-stroke); + color: var(--color-text-secondary); +} + +.category-tag--green { + background: var(--color-surface-strong-accent-green-default); + border-color: var(--color-accent-strong-green); + color: var(--color-text-on-accent); +} + +.category-tag--green:hover, +.category-tag--green.category-tag--hover-state { + background: var(--color-surface-strong-accent-green-hover); + border-color: var(--color-accent-strong-green); + color: var(--color-text-on-accent); +} + +.category-tag--yellow { + background: var(--color-surface-strong-accent-yellow-default); + border-color: var(--color-accent-strong-yellow); + color: var(--color-text-on-accent); +} + +.category-tag--yellow:hover, +.category-tag--yellow.category-tag--hover-state { + background: var(--color-surface-strong-accent-yellow-hover); + border-color: var(--color-accent-strong-yellow); + color: var(--color-text-on-accent); +} + +.category-tag--teal { + background: var(--color-surface-strong-accent-teal-default); + border-color: var(--color-accent-strong-teal); + color: var(--color-text-on-accent); +} + +.category-tag--teal:hover, +.category-tag--teal.category-tag--hover-state { + background: var(--color-surface-strong-accent-teal-hover); + border-color: var(--color-accent-strong-teal); + color: var(--color-text-on-accent); +} + +.version-tag { + display: inline-flex; + align-items: center; + padding: var(--space-default) var(--space-default); + font-family: var(--font-sans); + font-size: var(--font-size-xs); + color: var(--color-text-secondary); + border: 1px solid var(--color-tag-stroke); + border-radius: var(--border-radius-xl); + box-sizing: border-box; +} + +.version-tag--default { + background: var(--color-primary-grey-100); +} + +.version-tag--hover { + background: var(--color-primary-grey-200); +} + + +.category-cards-demo { + font-family: var(--font-sans); + color: var(--color-text-primary); +} + +.category-cards-demo__heading { + margin: 0 0 var(--space-large); + font-size: var(--font-size-medium); + font-weight: var(--font-weight-medium); +} + +.category-cards-demo__list { + display: flex; + flex-wrap: wrap; + gap: var(--space-default); + align-items: center; +} + +.category-cards-demo__block { + margin-bottom: var(--space-large); +} + +.category-cards-demo__block:last-child { + margin-bottom: 0; +} + +.category-cards-demo__label { + display: block; + margin-bottom: var(--space-default); + font-size: var(--font-size-xs); + color: var(--color-text-secondary); +} + +.category-cards-demo__row { + display: flex; + flex-wrap: wrap; + gap: 16px; + align-items: center; +} + +.category-cards-demo__group { + display: flex; + gap: 16px; + align-items: center; +} + +.category-cards-demo__state-label { + width: 56px; + font-size: var(--font-size-xs); + color: var(--color-text-secondary); +} diff --git a/static/css/v3/code-block.css b/static/css/v3/code-block.css new file mode 100644 index 000000000..163cd14e2 --- /dev/null +++ b/static/css/v3/code-block.css @@ -0,0 +1,318 @@ + + + :root { + --code-block-bg: var(--color-bg-secondary); + --code-block-border: var(--color-border); + --code-block-text: var(--color-syntax-text); + --code-block-bg-standalone: var(--color-primary-grey-100); + --code-block-border-standalone: var(--color-border); + --code-block-copy-icon-size: var(--space-card); + --code-block-copy-icon-color: var(--color-icon-secondary); + --code-block-copy-icon-color-success: var(--color-syntax-green); + --code-block-card-max-width: 458px; + --code-block-card-radius: var(--card-radius-lg); + --code-block-card-btn-bg: var(--color-primary-grey-200); +} + +.code-block--standalone { + background: var(--code-block-bg-standalone); + border: 1px dashed var(--code-block-border-standalone); + overflow: visible; +} + +.code-block--standalone .code-block__inner { + white-space: pre-wrap; + overflow-wrap: break-word; + overflow: hidden; +} + +.code-block--grey-bg { + background: var(--color-primary-grey-100); +} + +.code-block--white-bg { + background: var(--color-bg-secondary); +} + +.code-block { + position: relative; + margin: 0; + padding: var(--space-card); + border: 1px solid var(--code-block-border); + border-radius: var(--border-radius-l); + overflow: auto; + font-family: var(--font-code); + font-size: var(--font-size-small); + line-height: var(--line-height-code); + color: var(--code-block-text); + box-sizing: border-box; +} + +.code-block-card { + padding: var(--space-card) 0; + width: 100%; + box-sizing: border-box; + font-family: var(--font-sans); + color: var(--color-text-primary); + border-radius: var(--code-block-card-radius); + box-shadow: var(--card-shadow-value); +} + +.code-block-card .code-block { + margin: 0 var(--space-medium); + overflow: visible; +} + +.code-block-card .code-block__inner { + white-space: pre-wrap; + overflow-wrap: break-word; +} + +.code-block-card .code-block-card__btn { + margin: var(--space-medium) var(--space-medium) 0 var(--space-medium); + max-width: calc(100% - var(--space-medium) * 2); +} + +.code-block-card--neutral { + background: var(--color-bg-secondary); + border: 1px solid var(--color-border); +} + +.code-block-card--teal { + background: var(--color-accent-weak-teal); + border: 1px solid var(--color-accent-strong-teal); +} + +.code-block-card__heading { + margin: 0; + padding: 0 var(--space-medium) var(--space-card) var(--space-medium); + font-size: var(--font-size-large); + font-weight: 600; + font-family: var(--font-display); + color: var(--color-text-primary); +} + +.code-block-card__description { + padding: var(--space-card) var(--space-medium) var(--space-default) var(--space-medium); + margin: 0 0 var(--space-medium); + font-size: var(--font-size-small); + line-height: var(--line-height-relaxed); + color: var(--color-text-secondary); +} + +.code-block-card .code-block-card__description{ + border-top: 1px solid var(--color-border); +} + +.code-block-card__btn { + margin-top: var(--space-medium); + padding: var(--space-default) var(--space-default); + width: 100%; + background: var(--code-block-card-btn-bg); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-m); + font-size: var(--font-size-small); + font-weight: 600; + font-family: var(--font-sans); + color: var(--color-text-primary); + cursor: pointer; + box-sizing: border-box; + text-align: center; + display: block; +} + +.code-block__inner { + display: block; + margin: 0; + padding: 0; + white-space: pre; + min-height: 1em; + overflow: hidden; +} + +.code-block__copy { + position: absolute; + top: var(--space-default); + right: var(--space-default); + width: var(--code-block-copy-icon-size); + height: var(--code-block-copy-icon-size); + padding: 0; + border: none; + border-radius: var(--border-radius-s); + background: transparent; + color: var(--code-block-copy-icon-color); + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + transition: color 0.15s ease; +} + +.code-block__copy:hover { + color: var(--color-icon-primary); +} + +.code-block__copy:focus { + outline: 2px solid var(--color-stroke-link-accent); + outline-offset: 2px; +} + +.code-block__copy[data-copied="true"] { + color: var(--code-block-copy-icon-color-success); +} + +.code-block__copy-icon { + width: var(--code-block-copy-icon-size); + height: var(--code-block-copy-icon-size); + display: block; +} + +.code-block-story-page { + padding: var(--space-xl); + background: var(--color-bg-primary); + color: var(--color-text-primary); + font-family: var(--font-sans); + min-height: 100vh; + width: 100%; + max-width: 1312px; + margin: 0 auto; + box-sizing: border-box; +} + +.code-block-story-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--space-xl); + align-items: start; +} + +.code-block-story-header { + background: var(--color-bg-secondary); + border: 1px solid var(--color-border); + border-radius: 16px; + padding: var(--space-xl); + margin-bottom: var(--space-xl); + display: flex; + flex-direction: column; + gap: var(--space-large); +} + +.code-block-story-header__logo { + width: 64px; + height: 64px; + position: relative; +} + +.code-block-story-header__logo img { + width: 100%; + height: 100%; + object-fit: contain; +} + +.code-block-story-header__title { + margin: 0; + font-size: var(--font-size-3xl); + font-weight: 500; + line-height: 1; + letter-spacing: -0.02em; + color: var(--color-text-primary); + font-family: var(--font-display); +} + +.code-block-story-header__description { + margin: 0; + font-size: var(--font-size-medium); + line-height: 1.4; + letter-spacing: -0.01em; + color: var(--color-text-secondary); + font-family: var(--font-sans); +} + +.code-block-story-column { + display: flex; + flex-direction: column; + gap: var(--space-large); + min-width: 0; +} + +.code-block-story-section-title { + margin: 0 0 var(--space-default); + font-size: var(--font-size-large); + font-weight: 600; + color: var(--color-text-primary); +} + +.code-block-story-content { + width: 100%; + border-top: 1px solid var(--color-border); + padding-top: var(--space-large); +} + +@media (max-width: 767px) { + .code-block-story-page { + padding: var(--space-card); + } + + .code-block-story-grid { + grid-template-columns: 1fr; + gap: var(--space-large); + } + + .code-block-story-header { + padding: var(--space-card); + margin-bottom: var(--space-large); + gap: var(--space-default); + } + + .code-block-story-header__logo { + width: 48px; + height: 48px; + } + + .code-block-story-header__title { + font-size: var(--font-size-2xl); + } + + .code-block-story-header__description { + font-size: var(--font-size-small); + } + + .code-block-card { + max-width: 100%; + } + + .code-block-story-section-title { + font-size: var(--font-size-large); + } + + .code-block-story-content { + padding-top: var(--space-default); + } +} + +.code-block__copy-icon { + width: var(--code-block-copy-icon-size); + height: var(--code-block-copy-icon-size); + display: inline-flex; + align-items: center; + justify-content: center; + pointer-events: none; +} + +.code-block__copy-icon svg { + width: 100%; + height: 100%; + display: block; +} + +.code-block__copy-icon--check { + display: none; +} + +.code-block__copy[data-copied="true"] .code-block__copy-icon--copy { + display: none; +} + +.code-block__copy[data-copied="true"] .code-block__copy-icon--check { + display: inline-flex; +} diff --git a/static/css/v3/components.css b/static/css/v3/components.css new file mode 100644 index 000000000..44abbeadc --- /dev/null +++ b/static/css/v3/components.css @@ -0,0 +1,18 @@ +@import './fonts.css'; +@import './foundations.css'; +@import './buttons.css'; +@import './button-tooltip.css'; +@import './post-cards.css'; +@import './avatar.css'; +@import './carousel-buttons.css'; +@import './v3-examples-section.css'; +@import './header.css'; +@import './footer.css'; +@import './forms.css'; +@import './event-cards.css'; +@import './content.css'; +@import './why-boost-cards.css'; +@import './category-tags.css'; +@import './code-block.css'; +@import './search-card.css'; +@import './library-intro-card.css'; diff --git a/static/css/v3/content.css b/static/css/v3/content.css new file mode 100644 index 000000000..5cba3a969 --- /dev/null +++ b/static/css/v3/content.css @@ -0,0 +1,246 @@ + +.event-content { + font-family: var(--font-sans); + color: var(--color-text-primary); + margin: 0; + padding: 0; + border-radius: var(--card-radius); + box-sizing: border-box; +} + +.event-content--card-contained { + background: var(--color-primary-grey-100); + padding: var(--card-padding); + border: 1px solid var(--color-border); +} + +.event-content__title { + margin: 0 0 var(--space-xs); + font-size: var(--font-size-base); + font-weight: 700; + line-height: var(--line-height-default); + font-family: var(--font-sans); + color: var(--color-text-primary); +} + +.event-content__description { + margin: 0 0 var(--space-default); + font-size: var(--font-size-small); + line-height: var(--line-height-relaxed); + color: var(--color-text-secondary); + padding: 0; +} + +.event-content__date { + font-size: var(--font-size-small); + color: var(--color-text-tertiary); +} + +.content-detail-icon { + font-family: var(--font-sans); + color: var(--color-text-primary); + margin: 0; + padding: var(--space-card); + border-radius: var(--border-radius-xxl); + background: var(--color-bg-secondary); + border: 1px solid var(--color-border); + display: flex; + flex-direction: column; + gap: var(--space-default); + position: relative; + box-sizing: border-box; + align-self: start; +} + +.content-detail-icon__icon { + position: absolute; + top: var(--space-card); + right: var(--space-card); + color: var(--color-icon-secondary); + display: flex; + align-items: center; + justify-content: center; + width: var(--space-card); + height: var(--space-card); +} + +.content-detail-icon__icon svg { + width: 100%; + height: 100%; + display: block; +} + +.content-detail-icon__title { + margin: 0; + font-size: var(--font-size-base); + font-weight: var(--font-weight-medium); + line-height: var(--line-height-default); +} + +.content-detail-icon--has-icon .content-detail-icon__title { + padding-right: var(--space-icon-offset); +} + +.content-detail-icon__title-link { + color: inherit; + text-decoration: none; +} + +.content-detail-icon__description { + margin: 0; + font-size: var(--font-size-small); + line-height: var(--line-height-relaxed); + color: var(--color-text-secondary); + padding: 0; +} + +.content-detail-icon--contained { + background: transparent; + border: none; + padding: 0; + width: 100%; + align-self: stretch; + min-width: 0; +} + +.content-detail-icon--contained .content-detail-icon__icon { + top: 0; + right: 0; +} + +.content-story-page { + padding: var(--space-xl); + background: var(--color-bg-primary); + color: var(--color-text-primary); + font-family: var(--font-sans); + min-height: 100vh; + width: 100%; + max-width: 1312px; + margin: 0 auto; + box-sizing: border-box; +} + +.content-story-header { + background: var(--color-bg-secondary); + border: 1px solid var(--color-border); + border-radius: 16px; + padding: var(--space-xl); + margin-bottom: var(--space-xl); + display: flex; + flex-direction: column; + gap: var(--space-large); +} + +.content-story-header__logo { + width: 64px; + height: 64px; + position: relative; +} + +.content-story-header__logo img { + width: 100%; + height: 100%; + object-fit: contain; +} + +.content-story-header__title { + margin: 0; + font-size: var(--font-size-3xl); + font-weight: 500; + line-height: 1; + letter-spacing: -0.02em; + color: var(--color-text-primary); + font-family: var(--font-display); +} + +.content-story-header__description { + margin: 0; + font-size: var(--font-size-medium); + line-height: 1.4; + letter-spacing: -0.01em; + color: var(--color-text-secondary); + font-family: var(--font-sans); +} + +.content-story-variant-title { + margin: 0 0 var(--space-default); + font-size: var(--font-size-large); + font-weight: 600; + color: var(--color-text-primary); +} + +.content-story-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: var(--space-xl); + align-items: start; + margin-bottom: var(--space-default); +} + +.content-story-block { + outline: 2px dashed var(--color-border); + outline-offset: var(--space-default); + border-radius: var(--border-radius-l); + padding: var(--space-default); +} + +.content-story-variant-label { + margin: 0; + font-size: var(--font-size-small); + color: var(--color-text-secondary); +} + +.content-story-docs { + border-top: 1px solid var(--color-border); + padding-top: var(--space-large); +} + +.content-story-section { + margin-bottom: var(--space-xl); +} + +.content-story-section__title { + margin: 0 0 var(--space-xs); + font-size: var(--font-size-large); + font-weight: 600; + color: var(--color-text-primary); +} + +.content-story-section__description { + margin: 0 0 var(--space-default); + font-size: var(--font-size-small); + line-height: 1.5; + color: var(--color-text-secondary); +} + +.content-story-code { + margin: 0 0 var(--space-default); + padding: var(--space-card); + background: var(--color-bg-primary); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-m); + overflow: auto; + font-family: var(--font-code); + font-size: var(--font-size-small); + line-height: var(--line-height-code); + color: var(--color-text-primary); +} + +.content-story-code code { + white-space: pre; +} + +.content-story-preview { + margin-top: var(--space-default); + padding: var(--space-card); + background: var(--color-bg-secondary); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-l); + max-width: 458px; +} + +@media (max-width: 767px) { + .content-story-grid { + grid-template-columns: 1fr; + } +} diff --git a/static/css/v3/event-cards.css b/static/css/v3/event-cards.css new file mode 100644 index 000000000..020d82a3a --- /dev/null +++ b/static/css/v3/event-cards.css @@ -0,0 +1,186 @@ + +.event-cards { + font-family: var(--font-sans); + color: var(--color-text-primary); + border-radius: var(--card-radius-lg); + padding: var(--space-large) 0; + width: 100%; + max-width: 458px; + min-width: 0; + box-sizing: border-box; + flex-shrink: 0; +} + +.event-cards--white { + background: var(--color-bg-secondary); + border: 1px solid var(--color-border); +} + +.event-cards--grey { + background: var(--color-primary-grey-100); + border: 1px solid var(--color-border); +} + +.event-cards--yellow { + background: var(--color-accent-weak-yellow); + border: 1px solid var(--color-accent-strong-yellow); +} + +.event-cards--beige { + background: var(--color-accent-weak-green); + border: 1px solid var(--color-accent-strong-green); +} + +.event-cards--teal { + background: var(--color-accent-weak-teal); + border: 1px solid var(--color-accent-strong-teal); +} + +.event-cards__heading { + margin: 0 var(--space-card) var(--space-medium); + padding-bottom: var(--space-default); + font-size: var(--font-size-large); + font-weight: 500; + font-family: var(--font-display); +} + +.event-cards__list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: var(--space-large); +} + +.event-cards--white .event-cards__list { + gap: 0; + border-top: 1px solid var(--color-border); +} + +.event-cards__item { + list-style: none; +} + +.event-cards--white .event-cards__item { + border-bottom: 1px solid var(--color-border); + padding-top: var(--space-medium); +} + +.event-cards--white .event-card { + background: transparent; + box-shadow: none; + margin: 0; + padding: var(--space-medium) var(--space-card); +} + +.event-card { + background: var(--color-card-bg); + border-radius: var(--card-radius); + padding: var(--card-padding); + margin: 0 var(--space-card); +} + +.event-card__title { + margin: 0 0 var(--space-xs); + font-size: var(--font-size-base); + font-weight: 700; + line-height: var(--line-height-default); + font-family: var(--font-sans); +} + +.event-card__description { + margin: 0 0 var(--space-default); + font-size: var(--font-size-small); + line-height: var(--line-height-relaxed); + color: var(--color-text-secondary); +} + +.event-card__date { + font-size: var(--font-size-small); + color: var(--color-text-tertiary); +} + +.event-cards__buttons { + display: flex; + gap: var(--button-gap); + margin: 0 var(--space-card); + padding-top: var(--space-xl); +} + +.event-cards__btn { + flex: 1 1 0; + min-width: 0; + padding: var(--space-default) var(--space-default); /* 8px 8px */ + border-radius: var(--button-radius); + font-size: var(--button-font-size); + font-weight: var(--button-font-weight); + line-height: var(--button-line-height); + font-family: var(--button-font-family); + text-decoration: none; + text-align: center; + box-sizing: border-box; + color: var(--color-text-primary); +} + +.event-cards--white .event-cards__btn--primary, +.event-cards--grey .event-cards__btn--primary { + background: var(--color-button-primary); + border: none; +} + +.event-cards--white .event-cards__btn--secondary { + background: var(--color-button-secondary); + border: 1px solid var(--color-border); +} + +.event-cards--grey .event-cards__btn--secondary { + background: var(--color-primary-grey-100); + border: 1px solid var(--color-border); +} + +.event-cards--yellow .event-cards__btn--primary { + background: var(--color-surface-strong-accent-yellow-default); + border: none; +} + +.event-cards--yellow .event-cards__btn--secondary { + background: var(--color-accent-weak-yellow); + border: 1px solid var(--color-border); +} + +.event-cards--beige .event-cards__btn--primary { + background: var(--color-surface-strong-accent-green-default); + border: none; +} + +.event-cards--beige .event-cards__btn--secondary { + background: var(--color-accent-weak-green); + border: 1px solid var(--color-border); +} + +.event-cards--teal .event-cards__btn--primary { + background: var(--color-surface-strong-accent-teal-default); + border: none; +} + +.event-cards--teal .event-cards__btn--secondary { + background: var(--color-accent-weak-teal); + border: 1px solid var(--color-border); +} + +/* Gallery: event cards en fila (wrap) para no apilarse */ +.event-cards-gallery { + display: flex; + flex-wrap: wrap; + gap: var(--space-xl); + align-items: flex-start; + justify-content: center; +} + +.event-cards-gallery__item { + max-width: 458px; + width: 100%; + min-width: 0; + flex-shrink: 0; +} diff --git a/static/css/v3/fonts.css b/static/css/v3/fonts.css new file mode 100644 index 000000000..be85f4690 --- /dev/null +++ b/static/css/v3/fonts.css @@ -0,0 +1,115 @@ +/** + * V3 typography fonts. + * Reuses v2 fonts from /static/font/ where same (Monaspace); v3-only (Mona Sans, Space Mono) in /static/font/v3/ + */ + +/* Mona Sans – v3 only */ +@font-face { + font-family: 'Mona Sans VF'; + font-style: normal; + font-weight: 200 900; + font-stretch: 75% 125%; + font-display: swap; + src: url('/static/font/v3/mona-sans/mona-sans-vf.ttf') format('truetype'); +} + +@font-face { + font-family: 'Mona Sans VF'; + font-style: italic; + font-weight: 200 900; + font-stretch: 75% 125%; + font-display: swap; + src: url('/static/font/v3/mona-sans/mona-sans-vf-italic.ttf') format('truetype'); +} + +@font-face { + font-family: 'Mona Sans Display SemiCondensed'; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url('/static/font/v3/mona-sans-display/MonaSans_SemiCondensed-Regular.ttf') format('truetype'); +} + +@font-face { + font-family: 'Mona Sans Display SemiCondensed'; + font-style: normal; + font-weight: 500; + font-stretch: 100%; + font-display: swap; + src: url('/static/font/v3/mona-sans-display/MonaSans_SemiCondensed-Medium.ttf') format('truetype'); +} + +/* Monaspace Neon – reuse v2 assets in /static/font/ */ +@font-face { + font-family: 'Monaspace Neon'; + font-style: normal; + font-weight: 100 900; + font-stretch: 100%; + font-display: swap; + src: url('/static/font/MonaspaceNeon-Var.woff2') format('woff2'); +} + +@font-face { + font-family: 'Monaspace Neon'; + font-style: italic; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url('/static/font/v3/monaspace-neon/monaspace-neon-latin-400-italic.ttf') format('truetype'); +} + +/* Monaspace Xenon – reuse v2 assets in /static/font/ */ +@font-face { + font-family: 'Monaspace Xenon'; + font-style: normal; + font-weight: 100 900; + font-stretch: 100%; + font-display: swap; + src: url('/static/font/MonaspaceXenon-Var.woff2') format('woff2'); +} + +@font-face { + font-family: 'Monaspace Xenon'; + font-style: italic; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url('/static/font/MonaspaceXenon-Italic.woff2') format('woff2'); +} + +@font-face { + font-family: 'Space Mono'; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url('/static/font/v3/space-mono/SpaceMono-Regular.ttf') format('truetype'); +} + +@font-face { + font-family: 'Space Mono'; + font-style: italic; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url('/static/font/v3/space-mono/SpaceMono-Italic.ttf') format('truetype'); +} + +@font-face { + font-family: 'Space Mono'; + font-style: normal; + font-weight: 700; + font-stretch: 100%; + font-display: swap; + src: url('/static/font/v3/space-mono/SpaceMono-Bold.ttf') format('truetype'); +} + +@font-face { + font-family: 'Space Mono'; + font-style: italic; + font-weight: 700; + font-stretch: 100%; + font-display: swap; + src: url('/static/font/v3/space-mono/SpaceMono-BoldItalic.ttf') format('truetype'); +} diff --git a/static/css/v3/footer.css b/static/css/v3/footer.css new file mode 100644 index 000000000..6f6698487 --- /dev/null +++ b/static/css/v3/footer.css @@ -0,0 +1,173 @@ +.footer { + --footer-text: var(--Text-Primary, var(--color-text-primary, #050816)); + padding: var(--space-large) var(--space-large); + color: var(--footer-text); + font-family: var(--Typefaces-Sans, var(--font-sans, "Mona Sans VF")); + font-size: var(--Sizes-XS, 12px); + font-style: normal; + font-weight: 400; + line-height: 120%; + letter-spacing: -0.12px; + border-top: 1px solid var(--color-border); +} + +.footer *, +.footer a, +.footer p { + color: var(--footer-text); + font-family: var(--Typefaces-Sans, var(--font-sans, "Mona Sans VF")); + font-size: var(--Sizes-XS, 12px); + font-style: normal; + font-weight: 400; + line-height: 120%; + letter-spacing: -0.12px; +} + +.footer__inner { + max-width: 80rem; + margin: 0 auto; + display: flex; + flex-direction: column; + align-items: stretch; + gap: var(--space-xlarge); +} + +/* ========================================================================== + Footer – nav: flex row, links left, social icons right (24x24) + ========================================================================== */ + +.footer__nav { + display: flex; + flex-wrap: nowrap; + align-items: center; + gap: var(--space-medium); + width: 100%; +} + +.footer__nav-spacer { + flex: 1 1 0; + min-width: var(--space-medium); +} + +.footer__links { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--space-default) var(--space-medium); +} + +.footer__link { + color: var(--footer-text); + text-decoration: none; + transition: color 0.15s ease, background-color 0.15s ease; +} + +.footer__link:hover { + color: var(--color-text-link-accent); + background: var(--color-navigation-hover); +} + +.footer__link--inline { + color: var(--color-text-link-accent); + text-decoration: underline; + padding: 0; + border-radius: 0; +} + +.footer__link--inline:hover { + color: var(--color-text-link-accent); + background: transparent; + text-decoration-thickness: 2px; +} + +/* ========================================================================== + Footer – social: icons 24x24 + ========================================================================== */ + +.footer__social { + display: flex; + align-items: center; + gap: 16px; +} + +.footer__social a { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + padding: 0; + color: var(--footer-text); + border-radius: var(--border-radius-m, 6px); + transition: color 0.15s ease, background-color 0.15s ease; +} + +.footer__social a:hover { + color: var(--color-text-link-accent); + background: var(--color-navigation-hover); +} + +/* Restore Font Awesome icon font (overridden by .footer *) */ +.footer__social i, +.footer__social .fa-brands { + font-family: "Font Awesome 6 Brands", "Font Awesome 6 Free"; + font-size: 24px; + width: 24px; + height: 24px; + display: inline-flex; + align-items: center; + justify-content: center; + font-style: normal; + font-weight: 400; +} + +/* ========================================================================== + Footer – legal text (same typography) + ========================================================================== */ + +.footer__legal { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: var(--space-default); +} + +.footer__text { + margin: 0; + color: var(--footer-text); +} + +.footer__text--xsmall { + font-style: italic; +} + +/* ========================================================================== + Mobile: up to 500px – stack, space-between, icons centered + ========================================================================== */ + +@media (max-width: 500px) { + .footer__nav { + flex-direction: column; + justify-content: space-between; + align-items: flex-start; + gap: var(--space-xl, 32px); + } + + .footer__nav-spacer { + display: none; + } + + .footer__links { + justify-content: space-around; + width: 100%; + } + + .footer__social { + align-self: center; + } + + .footer__legal { + align-items: flex-start; + text-align: left; + } +} diff --git a/static/css/v3/forms.css b/static/css/v3/forms.css new file mode 100644 index 000000000..4cf43a146 --- /dev/null +++ b/static/css/v3/forms.css @@ -0,0 +1,474 @@ +.field { + display: flex; + flex-direction: column; + gap: var(--space-default, 8px); +} + +.field__label { + font-family: var(--font-sans); + font-size: var(--font-size-xs, 12px); + font-weight: var(--font-weight-medium, 500); + line-height: var(--line-height-relaxed, 1.24); + color: var(--color-text-primary, #050816); +} + +.field__control { + display: flex; + align-items: center; + gap: var(--space-default, 8px); + height: 40px; + padding: 0 var(--space-large, 16px); + background-color: var(--color-surface-weak, #fff); + border: 1px solid var(--color-stroke-weak, #0508161A); + border-radius: var(--border-radius-xl, 12px); + transition: background-color 0.15s ease, border-color 0.15s ease; + cursor: text; +} + +.field__control:hover { + border-color: var(--color-stroke-mid, #0508162B); +} + +.field__control:hover .field__input::placeholder { + color: var(--color-text-link-accent, #00778B); +} + +.field__control:focus-within { + background-color: var(--color-surface-mid, #f7f7f8); + border-color: var(--color-stroke-strong, #05081640); +} + +.field__input { + flex: 1; + min-width: 0; + border: none; + background: transparent; + outline: none; + box-shadow: none; + -webkit-appearance: none; + font-family: var(--font-sans); + font-size: var(--font-size-small, 14px); + font-weight: var(--font-weight-regular, 400); + line-height: var(--line-height-relaxed, 1.24); + color: var(--color-text-primary, #050816); + padding: 0; +} + +.field__input:focus { + outline: none; + box-shadow: none; + border: none; +} + +.field__input::placeholder { + color: var(--color-text-secondary, #6b6d78); +} + +.field__icon { + width: 24px; + height: 24px; + flex-shrink: 0; + color: var(--color-icon-secondary, #6b6d78); +} + +.field__submit { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: 24px; + height: 24px; + padding: 0; + border: none; + background: transparent; + color: var(--color-icon-primary, #050816); + cursor: pointer; +} + +.field__submit:disabled { + opacity: 0.3; + cursor: default; +} + +.field__submit:focus-visible { + outline: 2px solid var(--color-stroke-link-accent, #1F3044); + outline-offset: 2px; + border-radius: var(--border-radius-xs, 2px); +} + +.field__submit-icon { + width: 24px; + height: 24px; +} + +.field__help { + margin: 0; + padding: 0; + font-family: var(--font-sans); + font-size: var(--font-size-xs, 12px); + font-weight: var(--font-weight-regular, 400); + line-height: var(--line-height-relaxed, 1.24); + color: var(--color-text-tertiary, #6b6d78); +} + +.field__error { + margin: 0; + padding: 0; + font-family: var(--font-sans); + font-size: var(--font-size-xs, 12px); + font-weight: var(--font-weight-regular, 400); + line-height: var(--line-height-relaxed, 1.24); + color: var(--color-text-error, #d32f2f); +} + +/* Error state modifier */ +.field--error .field__label { + color: var(--color-text-error, #d32f2f); +} + +.field--error .field__control { + background-color: var(--color-surface-error-weak, #fdf2f2); + border-color: var(--color-stroke-error, #D53F3F33); +} + +.field--error .field__control:hover { + border-color: var(--color-stroke-error, #D53F3F33); +} + +.field--error .field__control:focus-within { + background-color: var(--color-surface-error-weak, #fdf2f2); + border-color: var(--color-stroke-error, #D53F3F33); +} + +.field--error .field__input { + color: var(--color-text-error, #d32f2f); +} + +.field--error .field__input::placeholder { + color: var(--color-text-error, #d32f2f); +} + +.field__control--disabled, +.field__control--disabled:hover, +.field__control--disabled:focus-within { + opacity: 0.5; + cursor: not-allowed; +} + +.field__control--disabled .field__input { + cursor: not-allowed; +} + +.dropdown { + position: relative; +} + +.dropdown__trigger { + display: flex; + align-items: center; + gap: var(--space-default, 8px); + width: 100%; + height: 40px; + padding: 0 var(--space-large, 16px); + background-color: var(--color-surface-weak, #fff); + border: 1px solid var(--color-stroke-weak, #0508161A); + border-radius: var(--border-radius-xl, 12px); + font-family: var(--font-sans); + font-size: var(--font-size-small, 14px); + font-weight: var(--font-weight-regular, 400); + color: var(--color-text-primary, #050816); + cursor: pointer; + transition: background-color 0.15s ease, border-color 0.15s ease; + text-align: left; +} + +.dropdown__trigger:hover { + border-color: var(--color-stroke-mid, #0508162B); +} + +.dropdown__trigger:hover .dropdown__trigger-placeholder { + color: var(--color-text-link-accent, #00778B); +} + +.dropdown__trigger:focus { + background-color: var(--color-surface-mid, #f7f7f8); + border-color: var(--color-stroke-strong, #05081640); + outline: none; +} + +.dropdown__trigger-text { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.dropdown__trigger-placeholder { + color: var(--color-text-secondary, #6b6d78); +} + +.dropdown__chevron { + width: 24px; + height: 24px; + flex-shrink: 0; + color: var(--color-icon-secondary, #6b6d78); + transition: transform 0.15s ease; +} + +.dropdown--open .dropdown__chevron { + transform: rotate(180deg); +} + +.dropdown--open .dropdown__trigger { + background-color: var(--color-surface-mid, #f7f7f8); + border-color: var(--color-stroke-strong, #05081640); + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + border-bottom-color: var(--color-stroke-weak, #0508161A); +} + +.dropdown__panel { + position: absolute; + top: 100%; + left: 0; + right: 0; + background-color: var(--color-surface-weak, #fff); + border: 1px solid var(--color-stroke-strong, #05081640); + border-top: none; + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom-left-radius: var(--border-radius-xl, 12px); + border-bottom-right-radius: var(--border-radius-xl, 12px); + box-shadow: 0 1px 4px rgba(5, 8, 22, 0.04); + z-index: 50; + overflow: hidden; +} + +.dropdown__list { + max-height: 320px; + overflow-y: auto; + padding: var(--space-s, 4px) 0; +} + +.dropdown__list::-webkit-scrollbar { + width: 4px; +} + +.dropdown__list::-webkit-scrollbar-track { + background: transparent; +} + +.dropdown__list::-webkit-scrollbar-thumb { + background-color: var(--color-stroke-mid, #0508162B); + border-radius: 2px; +} + +.dropdown__item { + display: flex; + align-items: center; + height: 40px; + padding: 0 var(--space-large, 16px); + font-family: var(--font-sans); + font-size: var(--font-size-small, 14px); + font-weight: var(--font-weight-regular, 400); + color: var(--color-text-primary, #050816); + cursor: pointer; + transition: background-color 0.1s ease; +} + +.dropdown__item:hover { + background-color: var(--color-surface-mid, #f7f7f8); +} + +.dropdown__item--selected { + font-weight: var(--font-weight-medium, 500); +} + +.field--error .dropdown__trigger { + background-color: var(--color-surface-error-weak, #fdf2f2); + border-color: var(--color-stroke-error, #D53F3F33); +} + +.field--error .dropdown__trigger:hover { + border-color: var(--color-stroke-error, #D53F3F33); +} + +.combo__search-row { + display: flex; + align-items: center; + gap: var(--space-default, 8px); + flex: 1; + min-width: 0; +} + +.combo__chevron-btn { + display: flex; + align-items: center; + justify-content: center; + background: none; + border: none; + padding: 0; + margin: 0; + cursor: pointer; + flex-shrink: 0; +} + +.combo__search-icon { + width: 24px; + height: 24px; + flex-shrink: 0; + color: var(--color-icon-secondary, #6b6d78); +} + +.combo__search-input { + flex: 1; + min-width: 0; + border: none; + background: transparent; + outline: none; + box-shadow: none; + -webkit-appearance: none; + font-family: var(--font-sans); + font-size: var(--font-size-small, 14px); + font-weight: var(--font-weight-regular, 400); + color: var(--color-text-primary, #050816); + padding: 0; +} + +.combo__search-input:focus { + outline: none; + box-shadow: none; + border: none; +} + +.combo__search-input::placeholder { + color: var(--color-text-secondary, #6b6d78); +} + +.dropdown__empty { + display: flex; + align-items: center; + height: 40px; + padding: 0 var(--space-large, 16px); + font-family: var(--font-sans); + font-size: var(--font-size-small, 14px); + color: var(--color-text-tertiary, #6b6d78); +} + +.multiselect__item { + display: flex; + align-items: center; + gap: var(--space-xlarge, 24px); + height: 40px; + padding: 0 var(--space-large, 16px); + font-family: var(--font-sans); + font-size: var(--font-size-small, 14px); + font-weight: var(--font-weight-regular, 400); + color: var(--color-text-primary, #050816); + cursor: pointer; + transition: background-color 0.1s ease; +} + +.multiselect__item:hover { + background-color: var(--color-surface-mid, #f7f7f8); +} + +.multiselect__check { + width: 24px; + height: 24px; + flex-shrink: 0; + border: 1px solid var(--color-stroke-strong, #05081640); + border-radius: var(--border-radius-xs, 2px); + background-color: var(--color-surface-weak, #fff); + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.1s ease, border-color 0.1s ease; +} + +.multiselect__check--active { + background-color: var(--color-surface-strong, #efeff1); + border-color: var(--color-text-primary, #050816); +} + +.multiselect__check-icon { + width: 16px; + height: 16px; + color: var(--color-text-primary, #050816); +} + +.multiselect__trigger-text { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.checkbox { + display: inline-flex; + align-items: center; + gap: var(--space-medium, 12px); + cursor: pointer; +} + +.checkbox__input { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.checkbox__box { + width: 24px; + height: 24px; + flex-shrink: 0; + border: 1px solid var(--color-stroke-strong, #05081640); + border-radius: var(--border-radius-xs, 2px); + background-color: var(--color-surface-weak, #fff); + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.1s ease, border-color 0.1s ease; +} + +.checkbox__input:checked + .checkbox__box { + background-color: var(--color-surface-strong, #efeff1); + border-color: var(--color-text-primary, #050816); +} + +.checkbox__check { + width: 16px; + height: 16px; + color: var(--color-text-primary, #050816); + opacity: 0; + transition: opacity 0.1s ease; +} + +.checkbox__input:checked + .checkbox__box .checkbox__check { + opacity: 1; +} + +.checkbox__input:focus-visible + .checkbox__box { + outline: 2px solid var(--color-stroke-link-accent, #0077B8); + outline-offset: 2px; +} + +.checkbox__label { + font-family: var(--font-sans); + font-size: var(--font-size-small, 14px); + font-weight: var(--font-weight-regular, 400); + line-height: var(--line-height-relaxed, 1.24); + color: var(--color-text-primary, #050816); +} + +.checkbox--disabled { + opacity: 0.5; + cursor: not-allowed; +} diff --git a/static/css/v3/foundations.css b/static/css/v3/foundations.css new file mode 100644 index 000000000..dcb5fac9b --- /dev/null +++ b/static/css/v3/foundations.css @@ -0,0 +1,124 @@ +@import './index.css'; + +html.v3 body { + font-family: var(--font-sans); + font-size: var(--font-size-base); + line-height: var(--line-height-default); + letter-spacing: var(--letter-spacing-tight); +} + +html.v3 code { + font-family: var(--font-code); +} + +/* Typography utility classes */ +.font-sans { font-family: var(--font-sans); } +.font-display { font-family: var(--font-display); } +.font-code { font-family: var(--font-code); } +.font-comments { font-family: var(--font-comments); } + +.text-xs { font-size: var(--font-size-xs); } +.text-small { font-size: var(--font-size-small); } +.text-base { font-size: var(--font-size-base); } +.text-medium { font-size: var(--font-size-medium); } +.text-large { font-size: var(--font-size-large); } +.text-xl { font-size: var(--font-size-xl); } +.text-2xl { font-size: var(--font-size-2xl); } +.text-3xl { font-size: var(--font-size-3xl); } +.text-4xl { font-size: var(--font-size-4xl); } + +.font-regular { font-weight: var(--font-weight-regular); } +.font-medium { font-weight: var(--font-weight-medium); } + +.leading-tight { line-height: var(--line-height-tight); } +.leading-default { line-height: var(--line-height-default); } +.leading-relaxed { line-height: var(--line-height-relaxed); } +.leading-loose { line-height: var(--line-height-loose); } +.leading-code { line-height: var(--line-height-code); } + +.tracking-tight { letter-spacing: var(--letter-spacing-tight); } + +/* Border Radius utility classes */ +.rounded-xs { border-radius: var(--border-radius-xs); } +.rounded-s { border-radius: var(--border-radius-s); } +.rounded-m { border-radius: var(--border-radius-m); } +.rounded-l { border-radius: var(--border-radius-l); } +.rounded-xl { border-radius: var(--border-radius-xl); } +.rounded-xxl { border-radius: var(--border-radius-xxl); } + +/* Spacing utility classes - Padding */ +.p-xs { padding: var(--space-xs); } +.p-s { padding: var(--space-s); } +.p-default { padding: var(--space-default); } +.p-medium { padding: var(--space-medium); } +.p-large { padding: var(--space-large); } +.p-xl { padding: var(--space-xl); } +.p-xxl { padding: var(--space-xxl); } +.p-avatar { padding: var(--space-avatar); } + +/* Spacing utility classes - Margin */ +.m-xs { margin: var(--space-xs); } +.m-s { margin: var(--space-s); } +.m-default { margin: var(--space-default); } +.m-medium { margin: var(--space-medium); } +.m-large { margin: var(--space-large); } +.m-xl { margin: var(--space-xl); } +.m-xxl { margin: var(--space-xxl); } +.m-avatar { margin: var(--space-avatar); } + +/* Spacing utility classes - Gap */ +.gap-xs { gap: var(--space-xs); } +.gap-s { gap: var(--space-s); } +.gap-default { gap: var(--space-default); } +.gap-medium { gap: var(--space-medium); } +.gap-large { gap: var(--space-large); } +.gap-xl { gap: var(--space-xl); } +.gap-xxl { gap: var(--space-xxl); } +.gap-avatar { gap: var(--space-avatar); } + +/* Spacing utility classes - Margin Bottom */ +.mb-xs { margin-bottom: var(--space-xs); } +.mb-s { margin-bottom: var(--space-s); } +.mb-default { margin-bottom: var(--space-default); } +.mb-medium { margin-bottom: var(--space-medium); } +.mb-large { margin-bottom: var(--space-large); } +.mb-xl { margin-bottom: var(--space-xl); } +.mb-xxl { margin-bottom: var(--space-xxl); } + +/* Spacing utility classes - Padding X and Y */ +.px-medium { padding-left: var(--space-medium); padding-right: var(--space-medium); } +.py-medium { padding-top: var(--space-medium); padding-bottom: var(--space-medium); } +.px-large { padding-left: var(--space-large); padding-right: var(--space-large); } +.py-large { padding-top: var(--space-large); padding-bottom: var(--space-large); } + +/* Color utility classes - Background */ +.bg-primary { background-color: var(--color-bg-primary); } +.bg-secondary { background-color: var(--color-bg-secondary); } +.bg-surface-mid { background-color: var(--color-surface-mid); } +.bg-surface-strong { background-color: var(--color-surface-strong); } +.bg-surface-weak { background-color: var(--color-surface-weak); } +.bg-surface-brand-accent { background-color: var(--color-surface-brand-accent-default); } +.bg-button-accent { background-color: var(--color-button-accent); } +.bg-button-primary { background-color: var(--color-button-primary); } +.bg-button-secondary { background-color: var(--color-button-secondary); } + +/* Color utility classes - Icon (Icon/Brand Accent etc.) */ +.icon-brand-accent { color: var(--color-icon-brand-accent); } + +/* Color utility classes - Text */ +.text-primary { color: var(--color-text-primary); } +.text-secondary { color: var(--color-text-secondary); } +.text-tertiary { color: var(--color-text-tertiary); } +.text-error { color: var(--color-text-error); } +.text-link-accent { color: var(--color-text-link-accent); } +.text-on-accent { color: var(--color-text-on-accent); } +.text-reversed { color: var(--color-text-reversed); } + +/* Color utility classes - Border/Stroke */ +.border-brand-accent { border-color: var(--color-stroke-brand-accent); } +.border-strong { border-color: var(--color-stroke-strong); } +.border-mid { border-color: var(--color-stroke-mid); } +.border-weak { border-color: var(--color-stroke-weak); } +.border-default { border-color: var(--color-border); } +.border-error { border-color: var(--color-stroke-error); } +.border-link-accent { border-color: var(--color-stroke-link-accent); } diff --git a/static/css/v3/header.css b/static/css/v3/header.css new file mode 100644 index 000000000..6b0c9a598 --- /dev/null +++ b/static/css/v3/header.css @@ -0,0 +1,562 @@ +/* ========================================================================== + Header – base & layout + ========================================================================== */ + +.header { + max-width: 1440px; + --header-radius: var(--border-radius-l, 8px); + + --header-height: 48px; + --header-control-border: 1px solid var(--color-border); + --header-control-radius: var(--header-radius); + --header-control-bg: var(--color-surface-weak); + padding: var(--space-large, 16px); + font-family: var(--Typefaces-Sans, "Mona Sans VF"); + font-size: var(--Sizes-Small, 14px); + font-style: normal; + font-weight: 400; + color: var(--color-text-primary, var(--text-color)); +} + +.header__inner { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-s, 4px); + height: var(--header-height); + max-width: 80rem; + margin: 0 auto; +} + +.header__logo { + display: flex; + flex-shrink: 0; + align-items: center; +} + +.header__logo img { + width: 31px; + height: 31px; + display: block; +} + +/* ========================================================================== + Components (link, input, button, etc.) + ========================================================================== */ + +.header__link { + color: var(--color-text-primary, var(--text-color)); + text-decoration: none; + font-weight: 400; + font-size: var(--Sizes-Small, 14px); + line-height: var(--line-height-relaxed, 1.24); + letter-spacing: var(--letter-spacing-tight, -0.01em); + padding: var(--space-s, 4px) var(--space-default, 8px); + border-radius: var(--header-radius); + white-space: nowrap; + transition: color 0.15s ease, background-color 0.15s ease; +} + +.header__link:hover { + color: var(--color-text-primary, var(--text-color)); + border-radius: var(--border-radius-m, 6px); + background: var(--Navigation-Hover, var(--color-navigation-hover, #D5D6D8)); +} + +.header__link--posts { + position: relative; + padding-right: var(--space-medium, 12px); +} + +.header__dot { + position: absolute; + top: 50%; + right: var(--space-s, 4px); + transform: translateY(-50%); + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--color-surface-brand-accent-default, var(--primary-color)); +} + +.header .btn { + width: auto; + min-height: var(--header-height); + padding: var(--space-s, 4px) var(--space-medium, 12px); + font-size: var(--Sizes-Small, 14px); + font-weight: 400; + text-decoration: none; +} + +.header .btn:hover { + border-radius: var(--border-radius-m, 6px); + background: var(--Navigation-Hover, var(--color-navigation-hover, #D5D6D8)); + border-color: var(--Navigation-Hover, var(--color-navigation-hover, #D5D6D8)); + color: var(--color-text-primary, var(--text-color)); +} + +.header__drawer-btn { + display: block; + width: 100%; + margin-top: var(--space-default, 8px); + text-align: center; + padding: var(--space-default, 8px) var(--space-medium, 12px); +} + +/* Search: icon, input trigger, kbd */ +.header__search-icon { + color: var(--color-text-secondary, var(--color-text-primary)); + opacity: 0.8; + font-size: var(--Sizes-Small, 14px); +} + +.header__search-input-trigger { + flex: 1; + min-width: 0; + border: none; + background: transparent; + color: var(--color-text-primary, var(--text-color)); + font-size: var(--Sizes-Small, 14px); + font-weight: 400; + text-align: left; + cursor: pointer; + font-family: var(--Typefaces-Sans, "Mona Sans VF"); +} + +.header__search-kbd { + display: inline-flex; + align-items: center; + gap: 2px; + height: 24px; + padding: var(--space-default, 8px); + font-family: var(--Typefaces-Sans, "Mona Sans VF"); + font-size: var(--Sizes-XS, 12px); + font-style: normal; + font-weight: 400; + line-height: var(--line-height-default, 1.2); + color: var(--color-text-tertiary, var(--color-text-primary)); + border-radius: var(--border-radius-m, 6px); + background: var(--color-surface-mid, #F7F7F8); +} + +.header__search-btn { + display: inline-flex; + align-items: center; + gap: var(--space-s, 4px); + min-height: var(--header-height); + padding: var(--space-s, 4px) var(--space-medium, 12px); + border: var(--header-control-border); + border-radius: var(--header-control-radius); + background: var(--header-control-bg); + color: var(--color-text-primary, var(--text-color)); + font-size: var(--Sizes-Small, 14px); + font-weight: 400; + cursor: pointer; + font-family: var(--Typefaces-Sans, "Mona Sans VF"); +} + +.header__search-btn:hover { + border-color: var(--color-stroke-link-accent); + color: var(--color-text-link-accent); +} + +.header__search-btn-label { + display: none; +} + +.header__latest-trigger { + display: inline-flex; + align-items: center; + gap: var(--space-s, 4px); + min-height: var(--header-height); + padding: var(--space-s, 4px) var(--space-default, 8px); + color: var(--color-text-primary, var(--text-color)); + text-decoration: none; + font-size: var(--Sizes-Small, 14px); + font-weight: 400; + border: var(--header-control-border); + border-radius: var(--header-control-radius); + background: var(--header-control-bg); +} + +.header__latest-trigger:hover { + border-radius: var(--border-radius-m, 6px); + background: var(--Navigation-Hover, var(--color-navigation-hover, #D5D6D8)); + border-color: var(--Navigation-Hover, var(--color-navigation-hover, #D5D6D8)); + color: var(--color-text-primary, var(--text-color)); +} + +.header__icon-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: var(--header-height); + min-height: var(--header-height); + padding: 0; + border: var(--header-control-border); + border-radius: var(--header-control-radius); + background: var(--header-control-bg); + color: var(--color-icon-primary, var(--color-text-primary)); + cursor: pointer; +} + +/* Theme icon: 16×16, black */ +.header__icon--theme { + width: 16px; + height: 16px; + color: var(--color-text-primary); +} + +/* v3 theme button uses pixel icons only; hide any Font Awesome or other ::before/::after */ +.header .header__icon-btn::before, +.header .header__icon-btn::after { + display: none !important; +} + +/* Theme switcher: light mode → moon (switch to dark), dark mode → sun (switch to light). html class set by theme_handling.js */ +.header__icon--theme-sun, +.header__icon--theme-moon { + display: inline-flex; + align-items: center; +} +/* Default / light: show moon */ +.header__icon--theme-sun { + display: none; +} +.header__icon--theme-moon { + display: inline-flex; +} +html.dark .header__icon--theme-sun { + display: inline-flex; +} +html.dark .header__icon--theme-moon { + display: none; +} + +.header__icon-btn:hover { + border-radius: var(--border-radius-m, 6px); + background: var(--Navigation-Hover, var(--color-navigation-hover, #D5D6D8)); + border-color: var(--Navigation-Hover, var(--color-navigation-hover, #D5D6D8)); + color: var(--color-icon-primary, var(--color-text-primary)); +} + +.header__user { + display: inline-flex; + align-items: center; + min-height: var(--header-height); +} + +.header__user-menu { + position: relative; +} + +/* User avatar trigger: click opens dropdown. Default / hover / selected use design tokens. */ +.header__user-trigger { + display: inline-flex; + align-items: center; + justify-content: center; + width: var(--header-height); + height: var(--header-height); + min-height: var(--header-height); + padding: 0; + border-radius: var(--border-radius-xl, 12px); + border: 1px solid var(--color-stroke-weak, rgba(5, 8, 22, 0.10)); + background: var(--color-surface-weak, #FFF); + cursor: pointer; + transition: border-color 0.15s ease, background-color 0.15s ease; + overflow: hidden; +} +.header__user-trigger:hover { + border-color: var(--color-stroke-strong, rgba(5, 8, 22, 0.25)); + background: var(--color-surface-mid, #F7F7F8); +} +.header__user-trigger--open, +.header__user-trigger[aria-expanded="true"] { + border-color: var(--color-stroke-mid, rgba(5, 8, 22, 0.17)); + background: var(--color-surface-mid, #F7F7F8); +} +.header__user-trigger .header__user-avatar-inner { + width: 100%; + height: 100%; + display: block; + border-radius: inherit; + pointer-events: none; +} +.header__user-trigger .header__user-avatar-inner > div, +.header__user-trigger .header__user-avatar-inner a, +.header__user-trigger .header__user-avatar-inner img { + width: 100%; + height: 100%; + display: block; + border-radius: inherit; + object-fit: cover; +} +.header__user-trigger .header__user-avatar-inner > div { + margin: 0; + gap: 0; +} +.header__user-trigger .header__user-avatar-inner a { + text-decoration: none; +} +.header__user-trigger .header__user-avatar-inner [class*="w-"] { + width: 100% !important; + height: 100% !important; + min-width: 0; + min-height: 0; +} + +/* Dark mode: subtler avatar border and inner so it doesn’t look harsh or double-framed */ +html.dark .header__user-trigger { + border-color: var(--color-border); +} +html.dark .header__user-trigger:hover { + border-color: var(--color-stroke-mid); +} +html.dark .header__user-trigger--open, +html.dark .header__user-trigger[aria-expanded="true"] { + border-color: var(--color-stroke-mid); +} +html.dark .header__user-trigger .header__user-avatar-inner > div { + background: transparent !important; +} +html.dark .header__user-trigger .header__user-avatar-inner .fa-user { + color: var(--color-icon-primary) !important; +} + +.header__user-dropdown { + position: absolute; + top: calc(100% + var(--space-s, 4px)); + right: 0; + min-width: 10rem; + padding: var(--space-s, 4px); + border-radius: var(--border-radius-m, 6px); + border: 1px solid var(--color-border); + background: var(--color-bg-secondary, var(--color-surface-weak)); + z-index: 1002; +} +.header__user-dropdown-link { + display: block; + padding: var(--space-default, 8px) var(--space-medium, 12px); + color: var(--color-text-primary); + text-decoration: none; + font-size: var(--Sizes-Small, 14px); + font-weight: 400; + border-radius: var(--border-radius-s, 4px); + white-space: nowrap; +} +.header__user-dropdown-link:hover { + background: var(--color-navigation-hover); + color: var(--color-text-primary); +} + +/* ========================================================================== + Containers (nav, right, search bar, mobile actions) + ========================================================================== */ + +.header__right { + display: flex; + align-items: center; + gap: var(--space-s, 4px); + height: var(--header-height); + flex: 1; + min-width: 0; +} + +.header__utilities { + display: flex; + align-items: center; + gap: var(--space-default, 8px); +} + +.header__nav--desktop { + display: none; + flex-shrink: 0; + align-items: center; + gap: var(--space-s, 4px) var(--space-medium, 12px); + flex-wrap: wrap; + height: var(--header-height); + padding: var(--space-s, 4px) var(--space-default, 8px); + border: var(--header-control-border); + border-radius: var(--header-control-radius); + background: var(--header-control-bg); +} + +.header__search-bar--full { + display: none; + align-items: center; + gap: var(--space-default, 8px); + flex: 1; + min-width: 0; + height: var(--header-height); + padding: 0 var(--space-medium, 12px); + border: var(--header-control-border); + border-radius: var(--header-control-radius); + background: var(--header-control-bg); +} + +.header__search-compact { + display: none; +} + +.header__mobile-actions { + display: none; + align-items: center; + gap: var(--space-s, 4px); + width: 100%; +} + +.header__mobile-logo { + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + min-height: var(--header-height); + padding: 0 var(--space-default, 8px); + border: var(--header-control-border); + border-radius: var(--header-control-radius); + background: var(--header-control-bg); + text-decoration: none; +} + +.header__mobile-logo img { + width: 31px; + height: 31px; + display: block; +} + +.header__menu-btn, +.header__search-btn--mobile { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--space-s, 4px); + flex: 1; + min-width: 0; + min-height: var(--header-height); + padding: var(--space-s, 4px) var(--space-medium, 12px); + border: var(--header-control-border); + border-radius: var(--header-control-radius); + background: var(--header-control-bg); + color: var(--color-text-primary, var(--text-color)); + font-size: var(--Sizes-Small, 14px); + font-weight: 400; + cursor: pointer; + font-family: var(--Typefaces-Sans, "Mona Sans VF"); +} + +.header__menu-btn:hover, +.header__search-btn--mobile:hover { + border-color: var(--color-stroke-link-accent); + color: var(--color-text-link-accent); +} + +/* ========================================================================== + Desktop (min-width: 992px) + ========================================================================== */ + +@media (min-width: 992px) { + .header__nav--desktop { + display: flex; + } + .header__search-bar--full { + display: flex; + } + .header__search-compact { + display: none; + } + .header__search-btn-label { + display: inline; + } +} + +/* ========================================================================== + Mobile (max-width: 991px) + ========================================================================== */ + +@media (max-width: 991px) { + + .header__nav--desktop, + .header__right { + display: none; + } + .header__search-bar--full { + display: none; + } + .header__mobile-actions { + display: flex; + } +} + +/* ========================================================================== + Drawer (mobile overlay) + ========================================================================== */ + +.header__drawer { + position: fixed; + top: 0; + right: 0; + width: min(20rem, 85vw); + height: 100vh; + background: var(--color-bg-secondary, var(--card-color)); + border-left: 1px solid var(--color-border); + z-index: 1001; + padding: var(--header-height) var(--space-large, 1rem) var(--space-large, 1rem); + overflow-y: auto; +} + +.header__drawer-nav { + display: flex; + flex-direction: column; + gap: var(--space-s, 4px); + margin-bottom: var(--space-large, 24px); +} + +.header__drawer-link { + display: block; + padding: var(--space-default, 8px) var(--space-medium, 12px); + color: var(--color-text-primary, var(--text-color)); + text-decoration: none; + font-weight: 400; + font-size: var(--Sizes-Small, 14px); + border-radius: var(--header-control-radius); +} + +.header__drawer-link:hover { + color: var(--color-text-link-accent); + background: var(--color-navigation-selected); +} + +.header__drawer-utilities { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--space-medium, 12px); + padding-top: var(--space-large, 16px); + border-top: 1px solid var(--color-border); +} + +.header__drawer-utilities a { + color: var(--color-text-primary, var(--text-color)); + text-decoration: none; + font-weight: 400; + font-size: var(--Sizes-Small, 14px); +} + +.header__drawer-utilities a:hover { + color: var(--color-text-link-accent); +} + +.header__backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.4); + z-index: 1000; +} + +/* ========================================================================== + Utility + ========================================================================== */ + +[x-cloak] { + display: none !important; +} diff --git a/static/css/v3/index.css b/static/css/v3/index.css new file mode 100644 index 000000000..57d02b4c8 --- /dev/null +++ b/static/css/v3/index.css @@ -0,0 +1,7 @@ +@import './primitives.css'; +@import './typography.css'; +@import './spacing.css'; +@import './border-radius.css'; +@import './semantics.css'; +@import './cards.css'; +@import './themes.css'; diff --git a/static/css/v3/library-intro-card.css b/static/css/v3/library-intro-card.css new file mode 100644 index 000000000..c61d62b74 --- /dev/null +++ b/static/css/v3/library-intro-card.css @@ -0,0 +1,141 @@ +/* Library Intro Card Component */ + +.library-intro-card { + font-family: var(--font-sans, 'Mona Sans VF'), sans-serif; + display: flex; + flex-direction: column; + max-width: 458px; + width: 100%; + box-sizing: border-box; + border-radius: var(--border-radius-xl, 12px); + border: 1px solid var(--color-stroke-weak, rgba(5, 8, 22, 0.1)); + background: var(--color-surface-weak, white); + overflow: hidden; +} + +/* Reset Tailwind/global base styles leaking into the card */ +.library-intro-card p { + margin: 0; + padding: 0; +} + +/* Header */ + +.library-intro-card__header { + display: flex; + flex-direction: column; + gap: var(--space-default, 8px); + padding: var(--space-large, 16px); +} + +.library-intro-card__title { + margin: 0; + display: flex; + align-items: center; + gap: 4px; + font-family: var(--font-display, 'Mona Sans Display SemiCondensed'), sans-serif; + font-size: var(--font-size-large, 24px); + font-weight: var(--font-weight-medium, 500); + line-height: var(--line-height-tight, 1); + letter-spacing: -0.24px; + color: var(--color-text-primary, #050816); +} + +.library-intro-card__title-pill { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 4px; + background: var(--color-text-primary, #050816); + color: var(--color-text-reversed, white); + border-radius: var(--border-radius-m, 6px); +} + +.library-intro-card__description { + margin: 0; + font-size: var(--font-size-base, 16px); + font-weight: var(--font-weight-regular, 400); + line-height: var(--line-height-default, 1.2); + letter-spacing: -0.16px; + color: var(--color-text-secondary, #585a64); +} + +/* Divider */ + +.library-intro-card__divider { + height: 0; + border: none; + border-top: 1px solid var(--color-stroke-weak, rgba(5, 8, 22, 0.1)); + margin: 0; +} + +/* Author row */ + +.library-intro-card__author { + display: flex; + align-items: center; + gap: var(--space-medium, 12px); + padding: var(--space-large, 16px); +} + +.library-intro-card__author-content { + display: flex; + flex-direction: column; + gap: var(--space-s, 4px); + min-width: 0; + flex: 1; +} + +.library-intro-card__author-meta { + display: flex; + align-items: center; + gap: var(--space-default, 8px); +} + +.library-intro-card__author-name { + margin: 0; + font-size: var(--font-size-xs, 12px); + font-weight: var(--font-weight-regular, 400); + line-height: var(--line-height-default, 1.2); + letter-spacing: -0.12px; + color: var(--color-text-primary, #050816); +} + +.library-intro-card__author-role { + margin: 0; + font-size: var(--font-size-xs, 12px); + font-weight: var(--font-weight-regular, 400); + line-height: var(--line-height-default, 1.2); + letter-spacing: -0.12px; + color: var(--color-text-secondary, #585a64); +} + +.library-intro-card__author-badge { + flex-shrink: 0; + font-size: 16px; + line-height: 1; +} + +.library-intro-card__author-badge img { + width: 16px; + height: 16px; +} + +.library-intro-card__author-bio { + margin: 0; + font-size: var(--font-size-xs, 12px); + font-weight: var(--font-weight-regular, 400); + line-height: var(--line-height-default, 1.2); + letter-spacing: -0.12px; + color: var(--color-text-secondary, #585a64); +} + +/* CTA */ + +.library-intro-card__cta { + padding: var(--space-large, 16px); +} + +.library-intro-card__cta .btn { + width: 100%; +} diff --git a/static/css/v3/post-cards.css b/static/css/v3/post-cards.css new file mode 100644 index 000000000..fab7120bc --- /dev/null +++ b/static/css/v3/post-cards.css @@ -0,0 +1,253 @@ +.post-cards a { + text-decoration: none; +} + +.post-cards { + font-family: var(--font-sans); + color: var(--color-text-primary); + display: flex; + flex-direction: column; + gap: var(--space-card); + padding: var(--space-card) 0; + max-width: 458px; + width: 100%; + min-width: 0; + box-sizing: border-box; + flex-shrink: 0; + border-radius: var(--border-radius-xl); + border: 1px solid var(--color-stroke-weak); + background: var(--color-surface-weak); +} + +.post-cards--default, +.post-cards--neutral { + background: var(--color-surface-weak); + border-color: var(--color-stroke-weak); +} + +.post-cards--teal { + background: var(--color-surface-weak-accent-teal); + border-color: var(--color-accent-strong-teal); +} + +.post-cards--yellow { + background: var(--color-surface-weak-accent-yellow); + border-color: var(--color-accent-strong-yellow); +} + +.post-cards__heading { + margin: 0; + padding: 0 var(--space-card); + font-family: var(--font-display); + font-size: var(--font-size-large); + font-weight: var(--font-weight-medium); + line-height: 1; + letter-spacing: -0.24px; + color: var(--color-text-primary); +} + +.post-cards__heading-link { + color: inherit; + text-decoration: none; +} + +.post-cards__heading-link:hover { + text-decoration: none; +} + +.post-cards__list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; +} + +.post-cards--horizontal { + max-width: 100%; + min-width: 960px; /* ~3 × post min-width (300px) + borders and padding */ +} + +.post-cards--horizontal .post-cards__list { + flex-direction: row; + flex-wrap: nowrap; + overflow-x: auto; + overflow-y: hidden; + -webkit-overflow-scrolling: touch; +} + +.post-cards__item { + list-style: none; + border-top: 1px solid var(--color-stroke-weak); + min-width: 300px; +} + +.post-cards__item:last-child { + border-bottom: 1px solid var(--color-stroke-weak); +} + +.post-cards--horizontal .post-cards__item { + border-top: none; + border-bottom: none; + border-right: 1px solid var(--color-stroke-weak); + flex-shrink: 0; + max-width: 458px; +} + +.post-cards--horizontal .post-cards__item:last-child { + border-right: none; +} + +.post-card { + display: flex; + flex-direction: column; + gap: var(--space-medium); + padding: var(--space-card); + border-radius: var(--border-radius-xl); + border: 1px solid var(--color-stroke-weak); + background: var(--color-surface-weak); +} + +.post-cards .post-card { + border: none; + border-radius: 0; + background: transparent; +} + +.post-card__title-block { + display: flex; + flex-direction: column; + gap: var(--space-default); +} + +.post-card__title { + margin: 0; + font-family: var(--font-sans); + font-size: var(--font-size-base); + font-weight: var(--font-weight-medium); + line-height: var(--line-height-default); + letter-spacing: -0.16px; + color: var(--color-text-primary); + text-decoration: none; +} + +.post-card__title:hover { + text-decoration: none; +} + +.post-card__meta { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px; + font-family: var(--font-sans); + font-size: var(--font-size-xs); + font-weight: var(--font-weight-regular); + line-height: var(--line-height-default); + letter-spacing: -0.12px; + color: var(--color-text-secondary); +} + +.post-card__meta-sep { + color: var(--color-text-secondary); + user-select: none; +} + +.post-card__author { + display: flex; + align-items: center; + gap: var(--space-medium); +} + +.post-card__avatar { + width: var(--space-avatar); + height: var(--space-avatar); + flex-shrink: 0; + border-radius: var(--border-radius-m); + object-fit: cover; + overflow: hidden; +} + +.post-card__author-info { + display: flex; + flex-direction: column; + gap: var(--space-s); + justify-content: center; +} + +.post-card__author-name { + margin: 0; + padding: 0; + font-family: var(--font-sans); + font-size: var(--font-size-xs); + font-weight: var(--font-weight-regular); + line-height: var(--line-height-default); + letter-spacing: -0.12px; + color: var(--color-text-primary); +} + +.post-card__author-role-row { + display: flex; + align-items: center; + gap: var(--space-default); +} + +.post-card__author-role { + margin: 0; + padding: 0; + font-family: var(--font-sans); + font-size: var(--font-size-xs); + font-weight: var(--font-weight-regular); + line-height: var(--line-height-default); + letter-spacing: -0.12px; + color: var(--color-text-secondary); +} + +.post-card__badge { + width: 16px; + height: 16px; + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.post-cards__cta { + display: flex; + gap: var(--space-default); + padding: 0 var(--space-card); +} + +.post-cards__cta .btn { + flex: 1 1 0; + min-width: 128px; + text-decoration: none; +} + +.post-cards__cta .btn:hover { + text-decoration: none; +} + +.post-cards__cta .btn-secondary-grey { + background-color: var(--color-button-primary); + border-color: var(--color-border); + color: var(--color-text-primary); +} + +.post-cards__cta .btn-secondary { + background-color: transparent; + border: 1px solid var(--color-stroke-strong); + color: var(--color-text-primary); +} + +.post-cards__cta .btn-secondary-grey:hover { + background-color: var(--color-primary-grey-300); + border-color: var(--color-primary-grey-300); + color: var(--color-text-primary); +} + +.post-cards__cta .btn-secondary:hover { + background-color: var(--color-button-secondary); + border-color: var(--color-secondary-dark-blue); + color: var(--color-secondary-dark-blue); +} diff --git a/static/css/v3/primitives.css b/static/css/v3/primitives.css new file mode 100644 index 000000000..8923a867b --- /dev/null +++ b/static/css/v3/primitives.css @@ -0,0 +1,62 @@ +/** + * Primitive Tokens + * + * Primitive tokens are the foundational design values that are theme-agnostic. + * These are raw color values, spacing values, typography scales, etc. that don't + * change between themes. + * + * Naming Convention: --{category}-{property}-{variant} + * Example: --color-primary-grey-100, --space-default + */ + +:root { + /* ============================================ + COLOR PRIMITIVES + ============================================ */ + + /* Accent Colors */ + --color-accent-strong-green: #CACA62; + --color-accent-strong-teal: #64DACE; + --color-accent-strong-yellow: #F5D039; + --color-accent-weak-green: #E4E4C0; + --color-accent-weak-teal: #C9F2EE; + --color-accent-weak-yellow: #FBEBA9; + + /* Error Colors */ + --color-error-mid: #FF3B30; + --color-error-strong: #D32F2F; + --color-error-weak: #FDF2F2; + + /* Primary Colors */ + --color-primary-black: #050816; + --color-primary-grey-100: #F7F7F8; + --color-primary-grey-200: #EFEFF1; + --color-primary-grey-300: #D5D6D8; + --color-primary-grey-400: #CACBCE; + --color-primary-grey-500: #ACADB1; + --color-primary-grey-600: #82848A; + --color-primary-grey-700: #71737B; + --color-primary-grey-800: #585A64; + --color-primary-grey-850: #444651; + --color-primary-grey-900: #2F313D; + --color-primary-grey-950: #1A1C29; + --color-primary-orange-mustard: #FFA000; + --color-primary-white: #FFFFFF; + + /* Secondary Colors */ + --color-secondary-dark-blue: #00778B; + --color-secondary-light-blue: #F7FDFE; + --color-secondary-mid-blue: #6DCDF7; + + /* Syntax Colors */ + --color-syntax-strong-blue: #1345E8; + --color-syntax-strong-green: #289D30; + --color-syntax-strong-grey: #9E9E9E; + --color-syntax-strong-pink: #D31FA7; + --color-syntax-strong-yellow: #A3A38C; + --color-syntax-weak-blue: #38DDFF; + --color-syntax-weak-green: #72FE92; + --color-syntax-weak-grey: #A3A3A3; + --color-syntax-weak-pink: #F358C0; + --color-syntax-weak-yellow: #FFF173; +} diff --git a/static/css/v3/search-card.css b/static/css/v3/search-card.css new file mode 100644 index 000000000..11ce21d8e --- /dev/null +++ b/static/css/v3/search-card.css @@ -0,0 +1,81 @@ +/* Search Card Component */ + +.search-card { + font-family: var(--font-sans, 'Mona Sans VF'), sans-serif; + display: flex; + flex-direction: column; + gap: var(--space-large, 16px); + padding: var(--space-large, 16px); + max-width: 458px; + width: 100%; + box-sizing: border-box; + border-radius: var(--border-radius-xl, 12px); + border: 1px solid var(--color-stroke-weak, rgba(5, 8, 22, 0.1)); + background: var(--color-surface-weak-accent-green, #e4e4c0); +} + +.search-card__heading { + margin: 0; + font-family: var(--font-display, 'Mona Sans Display SemiCondensed'), sans-serif; + font-size: var(--font-size-large, 24px); + font-weight: var(--font-weight-medium, 500); + line-height: var(--line-height-tight, 1); + letter-spacing: -0.24px; + color: var(--color-text-primary, #050816); +} + +/* Popular terms section */ + +.search-card__popular { + display: flex; + flex-direction: column; + gap: var(--space-medium, 12px); +} + +.search-card__popular-label { + margin: 0; + padding: 0; + font-family: var(--font-sans, 'Mona Sans VF'), sans-serif; + font-size: var(--font-size-xs, 12px); + font-weight: var(--font-weight-regular, 400); + line-height: var(--line-height-default, 1.2); + letter-spacing: -0.12px; + color: var(--color-text-secondary, #585a64); +} + +.search-card__tags { + display: flex; + flex-wrap: wrap; + align-content: center; + align-items: center; + gap: var(--space-s, 4px); +} + +.search-card__tag { + display: inline-flex; + align-items: center; + justify-content: center; + padding: var(--space-default, 8px); + background: var(--color-surface-strong-accent-green-default, #caca62); + border: none; + border-radius: var(--border-radius-s, 4px); + font-family: var(--font-sans, 'Mona Sans VF'), sans-serif; + font-size: var(--font-size-xs, 12px); + font-weight: var(--font-weight-regular, 400); + line-height: var(--line-height-tight, 1); + letter-spacing: -0.12px; + color: var(--color-text-primary, #050816); + text-decoration: none; + cursor: pointer; + transition: background 0.15s ease; +} + +.search-card__tag:hover { + background: var(--color-surface-strong-accent-green-hover, #caca62bf); + text-decoration: none; +} + +.search-card__tag:focus-visible { + outline: 2px solid var(--color-stroke-link-accent, #1F3044); + outline-offset: 2px; +} diff --git a/static/css/v3/semantics.css b/static/css/v3/semantics.css new file mode 100644 index 000000000..449a00097 --- /dev/null +++ b/static/css/v3/semantics.css @@ -0,0 +1,112 @@ +/** + * Semantic Tokens + * + * Semantic tokens provide meaning-based naming that maps to primitive tokens. + * These tokens represent how colors, spacing, and other values are used in context. + * Semantic tokens can be overridden per theme. + * + * Naming Convention: --color-{context}-{property}-{variant} + * Example: --color-button-primary, --color-text-error + * + * Note: Semantic tokens reference primitive tokens where appropriate, but can + * also have theme-specific values defined in themes.css + */ + +:root { + /* ============================================ + BASE SEMANTIC COLORS (Light Theme Defaults) + ============================================ */ + + /* Background */ + --color-bg-primary: var(--color-secondary-light-blue); + --color-bg-secondary: var(--color-primary-white); + --color-border: var(--color-primary-grey-300); + + /* Text */ + --color-text-primary: var(--color-primary-black); + --color-text-secondary: var(--color-primary-grey-800); + + /* ============================================ + BUTTON SEMANTIC COLORS + ============================================ */ + --color-button-accent: var(--color-primary-orange-mustard); + --color-button-primary: var(--color-primary-grey-200); + --color-button-secondary: var(--color-primary-white); + + /* ============================================ + ICON SEMANTIC COLORS + ============================================ */ + --color-icon-brand-accent: var(--color-primary-orange-mustard); + --color-icon-error: var(--color-error-strong); + --color-icon-link-accent: var(--color-secondary-dark-blue); + --color-icon-on-accent: var(--color-primary-black); + --color-icon-primary: var(--color-primary-black); + --color-icon-reverse: var(--color-primary-white); + --color-icon-secondary: var(--color-primary-grey-700); + + /* ============================================ + NAVIGATION SEMANTIC COLORS + ============================================ */ + --color-navigation-hover: var(--color-primary-grey-300); + --color-navigation-selected: var(--color-primary-grey-200); + + /* ============================================ + STROKE SEMANTIC COLORS + ============================================ */ + --color-stroke-brand-accent: var(--color-primary-orange-mustard); + --color-stroke-error: #D53F3F33; + --color-stroke-link-accent: var(--color-secondary-dark-blue); + --color-stroke-mid: #0508162B; + --color-stroke-strong: #05081640; + --color-stroke-weak: #0508161A; + + /* ============================================ + SURFACE SEMANTIC COLORS + ============================================ */ + --color-surface-brand-accent-default: var(--color-primary-orange-mustard); + --color-surface-brand-accent-hovered: #FFA000BF; + --color-surface-error-strong: var(--color-error-strong); + --color-surface-error-weak: var(--color-error-weak); + --color-surface-mid: var(--color-primary-grey-100); + --color-surface-modal: #050816B3; + --color-surface-page: var(--color-secondary-light-blue); + --color-surface-strong: var(--color-primary-grey-200); + --color-surface-weak: var(--color-primary-white); + + /* Accent Surfaces */ + --color-surface-strong-accent-green-default: var(--color-accent-strong-green); + --color-surface-strong-accent-green-hover: #CACA62BF; + --color-surface-strong-accent-teal-default: var(--color-accent-strong-teal); + --color-surface-strong-accent-teal-hover: #64DACEBF; + --color-surface-strong-accent-yellow-default: var(--color-accent-strong-yellow); + --color-surface-strong-accent-yellow-hover: #F5D039BF; + --color-surface-weak-accent-green: var(--color-accent-weak-green); + --color-surface-weak-accent-teal: var(--color-accent-weak-teal); + --color-surface-weak-accent-yellow: var(--color-accent-weak-yellow); + + /* ============================================ + SYNTAX SEMANTIC COLORS + ============================================ */ + --color-syntax-blue: var(--color-syntax-strong-blue); + --color-syntax-comments: var(--color-syntax-strong-grey); + --color-syntax-green: var(--color-syntax-strong-green); + --color-syntax-pink: var(--color-syntax-strong-pink); + --color-syntax-text: var(--color-primary-black); + --color-syntax-yellow: var(--color-syntax-strong-yellow); + + /* ============================================ + TAG SEMANTIC COLORS + ============================================ */ + --color-tag-fill: var(--color-primary-grey-100); + --color-tag-stroke: #0508161A; + + /* ============================================ + TEXT SEMANTIC COLORS + ============================================ */ + --color-text-error: var(--color-error-strong); + --color-text-link-accent: var(--color-secondary-dark-blue); + --color-text-on-accent: var(--color-primary-black); + --color-text-reversed: var(--color-primary-white); + --color-text-reversed-on-accent: var(--color-primary-white); + --color-text-tertiary: var(--color-primary-grey-700); +} diff --git a/static/css/v3/spacing.css b/static/css/v3/spacing.css new file mode 100644 index 000000000..8e7040c3f --- /dev/null +++ b/static/css/v3/spacing.css @@ -0,0 +1,37 @@ +/** + * Spacing Tokens + * + * Spacing tokens define consistent spacing values across the design system. + * All values are on a 4px grid. + * + * Naming Convention: --space-{size} + * Example: --space-default, --space-large + */ + +:root { + /* Spacing Scale (Desktop) */ + --space-xs: 2px; + --space-s: 4px; + --space-default: 8px; + --space-medium: 12px; + --space-large: 16px; + --space-card: 16px; + --space-xlarge: 24px; + --space-xl: 32px; + --space-xxl: 60px; + --space-avatar: 48px; +} + +/* Mobile Spacing Overrides */ +@media (max-width: 767px) { + :root { + --space-default: 6px; + --space-medium: 8px; + --space-large: 16px; + --space-xl: 20px; + --space-xxl: 48px; + --space-avatar: 40px; + --space-card: 12px; + /* xs, s unchanged on mobile */ + } +} diff --git a/static/css/v3/themes.css b/static/css/v3/themes.css new file mode 100644 index 000000000..37dcf0210 --- /dev/null +++ b/static/css/v3/themes.css @@ -0,0 +1,92 @@ +/** + * Theme Mappings + * + * Theme-specific overrides for semantic tokens. These override the default + * semantic token values defined in semantics.css. + * + * Dark theme: html.dark (set by theme_handling.js). + * Naming follows semantics.css. + */ + +/* ============================================ + LIGHT THEME (Default) + ============================================ + Light theme values are defined as defaults in semantics.css. + */ + +/* ============================================ + DARK THEME (Boost) + ============================================ */ +html.dark { + /* Base Colors */ + --color-bg-primary: var(--color-primary-black); + --color-bg-secondary: var(--color-primary-grey-950); + --color-text-primary: var(--color-primary-white); + --color-text-secondary: var(--color-primary-grey-400); + --color-border: var(--color-primary-grey-800); + + /* Error Primitives (Dark theme override) + Note: This is a special case where a primitive token needs theme-specific opacity. + In dark theme, error-weak uses reduced opacity for better contrast. */ + --color-error-weak: #FDF2F217; + + /* Buttons */ + --color-button-primary: var(--color-primary-grey-800); + --color-button-secondary: var(--color-primary-grey-900); + + /* Icons */ + --color-icon-link-accent: var(--color-secondary-mid-blue); + --color-icon-primary: var(--color-primary-white); + --color-icon-reverse: var(--color-primary-black); + --color-icon-secondary: var(--color-primary-grey-400); + + /* Navigation */ + --color-navigation-hover: var(--color-primary-grey-900); + --color-navigation-selected: var(--color-primary-grey-800); + + /* Stroke */ + --color-stroke-error: #D53F3F; + --color-stroke-link-accent: var(--color-secondary-mid-blue); + --color-stroke-mid: #F7F7F82B; + --color-stroke-strong: #F7F7F836; + --color-stroke-weak: #F7F7F81A; + + /* Surface */ + --color-surface-brand-accent-hovered: #FFA000D9; + --color-surface-error-strong: var(--color-error-mid); + --color-surface-error-weak: #FDF2F217; + --color-surface-mid: var(--color-primary-grey-900); + --color-surface-modal: #0508164D; + --color-surface-page: var(--color-primary-black); + --color-surface-strong: var(--color-primary-grey-800); + --color-surface-weak: var(--color-primary-grey-950); + + /* Accent Surfaces (Dark theme overrides) */ + --color-surface-strong-accent-green-default: var(--color-primary-grey-800); + --color-surface-strong-accent-green-hover: var(--color-primary-grey-850); + --color-surface-strong-accent-teal-default: var(--color-primary-grey-800); + --color-surface-strong-accent-teal-hover: var(--color-primary-grey-850); + --color-surface-strong-accent-yellow-default: var(--color-primary-grey-800); + --color-surface-strong-accent-yellow-hover: var(--color-primary-grey-850); + --color-surface-weak-accent-green: var(--color-primary-grey-850); + --color-surface-weak-accent-teal: var(--color-primary-grey-850); + --color-surface-weak-accent-yellow: var(--color-primary-grey-850); + + /* Syntax */ + --color-syntax-blue: var(--color-syntax-weak-blue); + --color-syntax-comments: var(--color-syntax-weak-grey); + --color-syntax-green: var(--color-syntax-weak-green); + --color-syntax-pink: var(--color-syntax-weak-pink); + --color-syntax-text: var(--color-primary-white); + --color-syntax-yellow: var(--color-syntax-weak-yellow); + + /* Tag */ + --color-tag-fill: var(--color-primary-grey-900); + --color-tag-stroke: #FFFFFF1A; + + /* Text */ + --color-text-error: var(--color-error-mid); + --color-text-link-accent: var(--color-secondary-mid-blue); + --color-text-reversed: var(--color-primary-black); + --color-text-tertiary: var(--color-primary-grey-600); +} diff --git a/static/css/v3/typography.css b/static/css/v3/typography.css new file mode 100644 index 000000000..e87b11af7 --- /dev/null +++ b/static/css/v3/typography.css @@ -0,0 +1,59 @@ +/** + * Typography Tokens + * + * Typography tokens define font families, sizes, weights, line heights, and letter spacing. + * These are primitive values that don't change between themes. + * + * Naming Convention: --font-{property}-{variant} + * Example: --font-size-base, --font-weight-medium + */ + +:root { + /* Typeface */ + --font-sans: 'Mona Sans VF', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --font-display: 'Mona Sans Display SemiCondensed', 'Mona Sans VF', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --font-code: 'Monaspace Neon', 'Space Mono', 'Monaco', 'Courier New', monospace; + --font-comments: 'Monaspace Xenon', 'Space Mono', 'Monaco', 'Courier New', monospace; + + /* Weight */ + --font-weight-regular: 400; + --font-weight-medium: 500; + + /* Size (Desktop) */ + --font-size-xs: 12px; + --font-size-small: 14px; + --font-size-base: 16px; + --font-size-medium: 18px; + --font-size-large: 24px; + --font-size-xl: 32px; + --font-size-2xl: 40px; + --font-size-3xl: 64px; + --font-size-4xl: 72px; + + /* Line Height */ + --line-height-tight: 1; + --line-height-default: 1.2; + --line-height-relaxed: 1.24; + --line-height-loose: 1.35; + --line-height-loose-alt: 1.33; + --line-height-code: 1.3; + + /* Letter Spacing */ + --letter-spacing-tight: -0.01em; + --letter-spacing-display-regular: -0.02em; +} + +/* Mobile Typography Overrides */ +@media (max-width: 767px) { + :root { + --font-size-xs: 10px; + --font-size-small: 12px; + --font-size-base: 14px; + --font-size-medium: 16px; + --font-size-large: 20px; + --font-size-xl: 24px; + --font-size-2xl: 28px; + --font-size-3xl: 32px; + --font-size-4xl: 40px; + } +} diff --git a/static/css/v3/v3-examples-section.css b/static/css/v3/v3-examples-section.css new file mode 100644 index 000000000..5c5239889 --- /dev/null +++ b/static/css/v3/v3-examples-section.css @@ -0,0 +1,170 @@ +.v3-examples-section { + max-width: 80rem; + margin-left: auto; + margin-right: auto; + padding: var(--space-xl, 32px) var(--space-default, 8px); + color: var(--color-text-primary); +} + +.v3-examples-section__heading { + font-size: var(--font-size-xl, 20px); + font-weight: var(--font-weight-medium, 500); + margin-bottom: var(--space-default, 8px); +} + +.v3-examples-section__block { + margin-bottom: var(--space-xl, 32px); +} + +.v3-examples-section__block:last-child { + margin-bottom: 0; +} + +.v3-examples-section__block h3 { + font-size: var(--font-size-base, 16px); + font-weight: var(--font-weight-medium, 500); + margin-bottom: var(--space-default, 8px); +} + +.v3-examples-section__example-box { + box-sizing: border-box; + width: 100%; + padding: var(--space-xl, 32px); + background: var(--color-surface-mid); + border: 1px solid var(--color-border); + border-radius: 8px; +} + +.v3-examples-section__tools { + display: flex; + flex-wrap: wrap; + gap: var(--space-default, 8px); + align-items: center; +} + +/* Avatar block – table presentation */ + +.v3-examples-section__avatar-item { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-small, 6px); +} + +.v3-examples-section__avatar-label { + font-size: var(--font-size-xs, 12px); + color: var(--color-text-secondary); + line-height: 1; + text-align: center; +} + +.v3-examples-section__avatar-table { + width: 100%; + border-collapse: collapse; +} + +.v3-examples-section__avatar-table th, +.v3-examples-section__avatar-table td { + padding: var(--space-l, 16px); + text-align: center; + vertical-align: middle; + border: 1px solid var(--color-border); +} + +.v3-examples-section__avatar-table th { + font-size: var(--font-size-small, 14px); + font-weight: var(--font-weight-medium, 500); + color: var(--color-text-secondary); + background: var(--color-bg-secondary); + padding: var(--space-xl, 32px) var(--space-xl, 32px); +} + +/* Extra gap between Variants row and Sizes row */ +.v3-examples-section__avatar-table tbody tr:first-child th, +.v3-examples-section__avatar-table tbody tr:first-child td { + padding-bottom: var(--space-xl, 32px); +} + +.v3-examples-section__avatar-table tbody tr:nth-child(2) th, +.v3-examples-section__avatar-table tbody tr:nth-child(2) td { + padding-top: var(--space-xl, 32px); +} + +.v3-examples-section__avatar-table td { + background: var(--color-bg-secondary); +} + +.v3-examples-section__forms { + display: flex; + flex-direction: column; + gap: var(--space-l, 16px); + max-width: 24rem; +} + +/* Button groups inside examples – spacing only, no extra box */ +.v3-buttons-group { + margin-bottom: var(--space-xl, 24px); +} + +.v3-buttons-group:last-child { + margin-bottom: 0; +} + +.v3-buttons-group__title { + font-size: var(--font-size-base, 16px); + font-weight: var(--font-weight-medium, 500); + margin: 0 0 var(--space-default, 8px); +} + +/* Demo hover overrides for this section only */ +.v3-examples-section [data-hover].btn-primary { + background-color: var(--color-secondary-dark-blue, #00778B); + border-color: var(--color-secondary-dark-blue, #00778B); + color: var(--color-text-reversed, #fff); +} + +.v3-examples-section [data-hover].btn-primary-outline { + background-color: var(--color-secondary-light-blue, #f7fdfe); + border-color: var(--color-text-link-accent, #00778B); + color: var(--color-text-link-accent, #00778B); +} + +.v3-examples-section [data-hover].btn-secondary { + background-color: var(--color-button-secondary, #fff); + border-color: var(--color-secondary-dark-blue, #0077B8); + color: var(--color-secondary-dark-blue, #0077B8); +} + +.v3-examples-section [data-hover].btn-secondary-grey { + background-color: var(--color-primary-grey-300, #d5d6d8); + border-color: var(--color-primary-grey-300, #d5d6d8); + color: var(--color-text-primary, #050816); +} + +.v3-examples-section [data-hover].btn-green { + background-color: var(--color-surface-strong-accent-green-hover, #CACA62BF); + border-color: transparent; + color: var(--color-text-primary, #050816); +} + +.v3-examples-section [data-hover].btn-yellow { + background-color: var(--color-surface-strong-accent-yellow-hover, #F5D039BF); + border-color: transparent; + color: var(--color-text-primary, #050816); +} + +.v3-examples-section [data-hover].btn-teal { + background-color: var(--color-surface-strong-accent-teal-hover, #64DACEBF); + border-color: transparent; + color: var(--color-text-primary, #050816); +} + +.v3-examples-section [data-hover].btn-carousel { + color: var(--color-secondary-dark-blue, #0077B8); +} + +.v3-examples-section [data-hover].btn.btn-hero.btn-primary { + background-color: var(--color-surface-brand-accent-hovered); + border-color: var(--color-surface-brand-accent-hovered); + color: var(--color-text-reversed); +} diff --git a/static/css/v3/why-boost-cards.css b/static/css/v3/why-boost-cards.css new file mode 100644 index 000000000..721301a3c --- /dev/null +++ b/static/css/v3/why-boost-cards.css @@ -0,0 +1,72 @@ +.why-boost-cards { + font-family: var(--font-sans); + color: var(--color-text-primary); +} + +.why-boost-cards__heading { + margin: 0 0 var(--space-large); + font-size: var(--font-size-medium); + font-weight: var(--font-weight-medium); + font-family: var(--font-display); +} + +.why-boost-cards__grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: var(--space-large); + max-width: 100%; +} + +.why-boost-cards__card { + background: var(--color-bg-secondary); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-xxl); + padding: var(--space-card); + display: flex; + flex-direction: column; + gap: var(--space-default); + position: relative; + width: fit-content; + align-self: start; +} + +.why-boost-cards__card-icon { + position: absolute; + top: var(--space-card); + right: var(--space-card); + color: var(--color-icon-secondary); + display: flex; + align-items: center; + justify-content: center; + width: var(--space-card); + height: var(--space-card); +} + +.why-boost-cards__card-title { + margin: 0; + font-size: var(--font-size-base); + font-weight: var(--font-weight-medium); + line-height: var(--line-height-default); +} + +.why-boost-cards__card--has-icon .why-boost-cards__card-title { + padding-right: var(--space-icon-offset); +} + +.why-boost-cards__card-title-link { + color: inherit; + text-decoration: none; +} + +.why-boost-cards__card-description { + margin: 0; + font-size: var(--font-size-small); + line-height: var(--line-height-relaxed); + color: var(--color-text-secondary); +} + +@media (max-width: 767px) { + .why-boost-cards__grid { + grid-template-columns: 1fr; + } +} diff --git a/static/font/v3/mona-sans-display/MonaSans_SemiCondensed-Medium.ttf b/static/font/v3/mona-sans-display/MonaSans_SemiCondensed-Medium.ttf new file mode 100644 index 000000000..95babb5b4 Binary files /dev/null and b/static/font/v3/mona-sans-display/MonaSans_SemiCondensed-Medium.ttf differ diff --git a/static/font/v3/mona-sans-display/MonaSans_SemiCondensed-Regular.ttf b/static/font/v3/mona-sans-display/MonaSans_SemiCondensed-Regular.ttf new file mode 100644 index 000000000..bd275a8b2 Binary files /dev/null and b/static/font/v3/mona-sans-display/MonaSans_SemiCondensed-Regular.ttf differ diff --git a/static/font/v3/mona-sans/mona-sans-vf-italic.ttf b/static/font/v3/mona-sans/mona-sans-vf-italic.ttf new file mode 100644 index 000000000..00bc96dea Binary files /dev/null and b/static/font/v3/mona-sans/mona-sans-vf-italic.ttf differ diff --git a/static/font/v3/mona-sans/mona-sans-vf.ttf b/static/font/v3/mona-sans/mona-sans-vf.ttf new file mode 100644 index 000000000..a49f373ec Binary files /dev/null and b/static/font/v3/mona-sans/mona-sans-vf.ttf differ diff --git a/static/font/v3/monaspace-neon/monaspace-neon-latin-400-italic.ttf b/static/font/v3/monaspace-neon/monaspace-neon-latin-400-italic.ttf new file mode 100644 index 000000000..3faf6ce5c Binary files /dev/null and b/static/font/v3/monaspace-neon/monaspace-neon-latin-400-italic.ttf differ diff --git a/static/font/v3/monaspace-neon/monaspace-neon-latin-400-normal.ttf b/static/font/v3/monaspace-neon/monaspace-neon-latin-400-normal.ttf new file mode 100644 index 000000000..cc3cc95a7 Binary files /dev/null and b/static/font/v3/monaspace-neon/monaspace-neon-latin-400-normal.ttf differ diff --git a/static/font/v3/space-mono/SpaceMono-Bold.ttf b/static/font/v3/space-mono/SpaceMono-Bold.ttf new file mode 100644 index 000000000..7c38f236f Binary files /dev/null and b/static/font/v3/space-mono/SpaceMono-Bold.ttf differ diff --git a/static/font/v3/space-mono/SpaceMono-BoldItalic.ttf b/static/font/v3/space-mono/SpaceMono-BoldItalic.ttf new file mode 100644 index 000000000..b2f1e0d6d Binary files /dev/null and b/static/font/v3/space-mono/SpaceMono-BoldItalic.ttf differ diff --git a/static/font/v3/space-mono/SpaceMono-Italic.ttf b/static/font/v3/space-mono/SpaceMono-Italic.ttf new file mode 100644 index 000000000..3845e1717 Binary files /dev/null and b/static/font/v3/space-mono/SpaceMono-Italic.ttf differ diff --git a/static/font/v3/space-mono/SpaceMono-Regular.ttf b/static/font/v3/space-mono/SpaceMono-Regular.ttf new file mode 100644 index 000000000..8bf82cece Binary files /dev/null and b/static/font/v3/space-mono/SpaceMono-Regular.ttf differ diff --git a/static/js/code-block-copy.js b/static/js/code-block-copy.js new file mode 100644 index 000000000..fd02e8878 --- /dev/null +++ b/static/js/code-block-copy.js @@ -0,0 +1,58 @@ +/** + * Code block copy: al hacer clic en .code-block__copy, copia el texto del código + * al portapapeles, pone data-copied="true" en el botón y muestra el icono check. + * Tras COPY_FEEDBACK_MS vuelve al estado inicial. + */ +(function () { + var COPY_FEEDBACK_MS = 2000; + + function init() { + document.querySelectorAll(".code-block__copy").forEach(function (btn) { + if (btn.dataset.codeBlockCopyInit) return; + btn.dataset.codeBlockCopyInit = "true"; + btn.addEventListener("click", handleCopyClick); + }); + } + + function handleCopyClick(ev) { + var btn = ev.currentTarget; + var block = btn.closest(".code-block"); + if (!block) return; + var codeEl = block.querySelector(".code-block__inner code"); + if (!codeEl) return; + var text = codeEl.textContent || codeEl.innerText || ""; + + if (typeof navigator !== "undefined" && navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text).then( + function () { + setCopiedState(btn, true); + setTimeout(function () { + setCopiedState(btn, false); + }, COPY_FEEDBACK_MS); + }, + function () { + setCopiedState(btn, true); + setTimeout(function () { + setCopiedState(btn, false); + }, COPY_FEEDBACK_MS); + } + ); + } else { + setCopiedState(btn, true); + setTimeout(function () { + setCopiedState(btn, false); + }, COPY_FEEDBACK_MS); + } + } + + function setCopiedState(btn, copied) { + btn.setAttribute("data-copied", copied ? "true" : "false"); + btn.setAttribute("aria-label", copied ? "Copied" : "Copy code to clipboard"); + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + } else { + init(); + } +})(); diff --git a/templates/base.html b/templates/base.html index a55dbfea9..7a39435ad 100644 --- a/templates/base.html +++ b/templates/base.html @@ -3,7 +3,7 @@ {% load waffle_tags %} {% get_current_language as LANGUAGE_CODE %} - + {% if not disable_plausible %} @@ -44,11 +44,17 @@ + {% flag "v3" %} + + {% endflag %} {% block extra_head %} {% comment %} {% endcomment %} + {% flag "v3" %} + + {% endflag %} {% endblock %} {% block css %}{% endblock css %} @@ -353,7 +359,11 @@ {% endflag %} {% block main_content_wrapper %}
{% endblock %} {% block content_header %} - {% include "includes/_header.html" %} + {% flag "v3" %} + {% include "v3/includes/_header_v3.html" %} + {% else %} + {% include "includes/_header.html" %} + {% endflag %} {% endblock %}
@@ -385,8 +395,16 @@ {% endblock %}
+ {% flag "v3" %} + {% include "v3/examples/_v3_example_section.html" %} + {% endflag %} + {% if not hide_footer %} - {% include "includes/_footer.html" %} + {% flag "v3" %} + {% include "v3/includes/_footer_v3.html" %} + {% else %} + {% include "includes/_footer.html" %} + {% endflag %} {% endif %}
diff --git a/templates/homepage.html b/templates/homepage.html index 9dd8930b4..a8b04e771 100644 --- a/templates/homepage.html +++ b/templates/homepage.html @@ -2,6 +2,7 @@ {% load static %} {% load text_helpers avatar_tags %} +{% load waffle_tags %} {% block content %} {# homepage hero #} @@ -204,6 +205,25 @@

+

+ Recent News +

+ +
+ View all news +
+ + {% else %}
@@ -248,8 +268,8 @@

diff --git a/templates/includes/_social_icon_links.html b/templates/includes/_social_icon_links.html index c4e77915f..5801db5e8 100644 --- a/templates/includes/_social_icon_links.html +++ b/templates/includes/_social_icon_links.html @@ -1,6 +1,6 @@ - - - - - - + + + + + + diff --git a/templates/includes/icon.html b/templates/includes/icon.html new file mode 100644 index 000000000..5a5a4815b --- /dev/null +++ b/templates/includes/icon.html @@ -0,0 +1,45 @@ +{% comment %} + This is TBD!! + Icon component – Pixel Art Icons (pixelarticons, same as @iconify-icons/pixelarticons). + Usage: {% include "includes/icon.html" with icon_name="search" icon_class="header__icon icon-brand-accent" %} + icon_name: required, e.g. "search", "chevron-down" + icon_class: optional, default "icon" (for size/color utilities) + icon_size: optional, default 24 (viewBox is 24 for all) +{% endcomment %} + diff --git a/templates/v3/base.html b/templates/v3/base.html new file mode 100644 index 000000000..49da6ae78 --- /dev/null +++ b/templates/v3/base.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block title %}V3{% endblock %} + +{% block content %} +
+

V3 placeholder

+

Content for the v3 experience (when the v3 flag is enabled).

+
+{% endblock %} diff --git a/templates/v3/examples/_v3_example_buttons_block.html b/templates/v3/examples/_v3_example_buttons_block.html new file mode 100644 index 000000000..6edd82b05 --- /dev/null +++ b/templates/v3/examples/_v3_example_buttons_block.html @@ -0,0 +1,42 @@ +
+

Default

+
+ + + + + +
+
+ + + + + +
+
+ +
+
+ +
+

Hero

+
+ + + + +
+
diff --git a/templates/v3/examples/_v3_example_section.html b/templates/v3/examples/_v3_example_section.html new file mode 100644 index 000000000..b01c01af9 --- /dev/null +++ b/templates/v3/examples/_v3_example_section.html @@ -0,0 +1,269 @@ +
+ {% comment %}V3 examples section for testing. To be removed – everything under v3/examples/ is disposable.{% endcomment %} +

V3 Examples for testing

+ +
+

Buttons

+
+ {% include "v3/examples/_v3_example_buttons_block.html" %} +
+
+ +
+

Tooltip button

+
+
+ {% include "v3/includes/_button_tooltip_v3.html" with label="Top" position="top" button_text="Help" %} + {% include "v3/includes/_button_tooltip_v3.html" with label="Right" position="right" button_text="Help" %} + {% include "v3/includes/_button_tooltip_v3.html" with label="Bottom" position="bottom" button_text="Help" %} + {% include "v3/includes/_button_tooltip_v3.html" with label="Left" position="left" button_text="Help" %} + {% include "v3/includes/_button_tooltip_v3.html" with label="More information here" position="bottom" button_text="Info" %} + {% include "v3/includes/_button_tooltip_v3.html" with label="Top" position="top" %} + {% include "v3/includes/_button_tooltip_v3.html" with label="Right" position="right" %} + {% include "v3/includes/_button_tooltip_v3.html" with label="Bottom" position="bottom" %} + {% include "v3/includes/_button_tooltip_v3.html" with label="Left" position="left" %} + {% include "v3/includes/_button_tooltip_v3.html" with label="Icon only tooltip" position="bottom" %} +
+
+
+ +
+

Avatar

+
+ + + + + + + + + + + + + + + + + + + + +
Variants +
+ {% include "v3/includes/_avatar_v3.html" with src="https://thispersondoesnotexist.com/" size="md" %} + Example photo +
+
+
+ {% with user_avatar_url=request.user.get_thumbnail_url %} + {% if user_avatar_url %} + {% include "v3/includes/_avatar_v3.html" with src=user_avatar_url size="md" %} + {% elif request.user.is_authenticated %} + {% include "v3/includes/_avatar_v3.html" with name=request.user.display_name size="md" %} + {% else %} + {% include "v3/includes/_avatar_v3.html" with size="md" %} + {% endif %} + {% endwith %} + You +
+
+
+ {% 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 +
+
Sizes +
+ {% 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 +
+
+
+
+ + + +
+

Post card (standalone)

+
+ {% 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 %} +
+
+ +
+

Post cards

+
+
+

+ Posts from the Boost community +

+
    +
  • + {% 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 %} +
  • +
  • + {% 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="Peter Dimov" author_role="Maintainer" author_show_badge=True author_avatar_url="https://ui-avatars.com/api/?name=Peter+Dimov&size=48" %} + {% include "v3/includes/_post_card_v3.html" %} + {% endwith %} +
  • +
  • + {% with post_title="Boost.Bind and modern C++: a quick overview" post_url="#" post_date="15/02/2025" post_category="Releases" post_tag="bind" author_name="Alex Morgan" author_role="Contributor" author_show_badge=False author_avatar_url="https://thispersondoesnotexist.com/" %} + {% include "v3/includes/_post_card_v3.html" %} + {% endwith %} +
  • +
+
+ View all posts +
+
+
+
+ +
+

Search Card

+
+ {% include "v3/includes/_search_card.html" with heading="What are you trying to find?" action_url="#" popular_terms=popular_terms %} +
+
+ + {% if library_intro %} +
+

Library Intro Card

+
+ {% with intro=library_intro %} + {% include "v3/includes/_library_intro_card.html" with library_name=intro.library_name description=intro.description authors=intro.authors cta_url=intro.cta_url %} + {% endwith %} +
+
+ {% endif %} + +
+

Form inputs

+
+
+ {% include "v3/includes/_field_text.html" with name="ex_basic" label="Text field" placeholder="Enter text..." %} + {% include "v3/includes/_field_text.html" with name="ex_search" label="With icon" placeholder="Search..." icon_left="search" %} + {% include "v3/includes/_field_text.html" with name="ex_error" label="Error state" placeholder="Enter value" error="This field is required." %} + {% include "v3/includes/_field_checkbox.html" with name="ex_agree" label="I agree to the terms and conditions" %} + {% include "v3/includes/_field_combo.html" with name="ex_library" label="Combo (searchable)" placeholder="Search libraries..." options_json=demo_libs_json %} + {% include "v3/includes/_field_multiselect.html" with name="ex_categories" label="Multi-select" placeholder="Select categories..." options_json=demo_cats_json %} +
+
+
+ +
+

Why Boost

+
+
+

Why Boost?

+
+ {% 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="bullseye-arrow" %} + {% 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="bullseye-arrow" %} + {% 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="bullseye-arrow" %} + {% 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="bullseye-arrow" %} +
+
+
+
+ +
+

Category tags

+
+ {% include "v3/includes/_category_cards.html" with show_version_tags=True %} +
+
+ +
+

Event cards

+
+ {% include "v3/includes/_event_cards.html" %} +
+
+ +
+

Content event card

+
+
+

Content event card

+

Default and contained variants.

+
+
+

Default

+ {% include "v3/includes/_content_event_card_item.html" with title="Boost 1.83.0 closed for major changes" description="Release closed for major code changes. Still open for serious problem fixes and docs changes without release manager review." date="23/11/23" datetime="2023-11-23" %} +
+
+

Contained

+ {% include "v3/includes/_content_event_card_item.html" with title="Boost 1.83.0 closed for major changes" description="Release closed for major code changes. Still open for serious problem fixes and docs changes without release manager review." date="23/11/23" datetime="2023-11-23" contained=True %} +
+
+
+
+
+ +
+

Content detail card

+
+
+

Content detail card

+

With icon and optional title link.

+
+
+

With link

+ {% 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" %} +
+
+

Another card

+ {% 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" %} +
+
+
+
+
+ +
+

Code blocks

+
+ {% include "v3/includes/_code_blocks_story.html" %} +
+
+
diff --git a/templates/v3/includes/_avatar_v3.html b/templates/v3/includes/_avatar_v3.html new file mode 100644 index 000000000..4c39274c4 --- /dev/null +++ b/templates/v3/includes/_avatar_v3.html @@ -0,0 +1,27 @@ +{% comment %} + Variables: src (optional; image URL), name (optional), variant (optional; yellow|green|teal, default yellow), size (optional; sm|md|lg|xl, default md). + Usage: {% include "v3/includes/_avatar_v3.html" with src=author.avatar_url name=author.name variant="yellow" size="md" %} + Image: {% include "v3/includes/_avatar_v3.html" with src="/static/images/avatar-demo.png" size="md" %} + Initials: {% include "v3/includes/_avatar_v3.html" with name="Jane Doe" variant="green" %} + Placeholder: {% include "v3/includes/_avatar_v3.html" %} +{% endcomment %} +{% load avatar_tags %} +{% with size=size|default:"md" variant=variant|default:"yellow" %} + {% if src %} + + + + {% elif name %} + {% with inits=name|avatar_initials %} + {% if inits == "?" %} + ? + {% else %} + + {{ inits }} + + {% endif %} + {% endwith %} + {% else %} + ? + {% endif %} +{% endwith %} diff --git a/templates/v3/includes/_button_hero_primary.html b/templates/v3/includes/_button_hero_primary.html new file mode 100644 index 000000000..b6646dc62 --- /dev/null +++ b/templates/v3/includes/_button_hero_primary.html @@ -0,0 +1,11 @@ +{% comment %} + V3 foundation button: hero primary (larger, for hero section). Optional leading icon. + Variables: label (required), url (optional), icon_html (optional; HTML for icon, use with |safe), type (optional). + Icon must be wrapped in .... + Usage: {% include "v3/includes/_button_hero_primary.html" with label="Get started" icon_html='' %} +{% endcomment %} +{% if url %} +{% if icon_html %}{{ icon_html|safe }}{% endif %}{{ label }} +{% else %} + +{% endif %} diff --git a/templates/v3/includes/_button_hero_secondary.html b/templates/v3/includes/_button_hero_secondary.html new file mode 100644 index 000000000..e0cc6b347 --- /dev/null +++ b/templates/v3/includes/_button_hero_secondary.html @@ -0,0 +1,10 @@ +{% comment %} + V3 foundation button: hero secondary. + Variables: label (required), url (optional), icon_html (optional), type (optional). + Usage: {% include "v3/includes/_button_hero_secondary.html" with label="Learn more" %} +{% endcomment %} +{% if url %} +{% if icon_html %}{{ icon_html|safe }}{% endif %}{{ label }} +{% else %} + +{% endif %} diff --git a/templates/v3/includes/_button_tooltip_v3.html b/templates/v3/includes/_button_tooltip_v3.html new file mode 100644 index 000000000..6236e86c0 --- /dev/null +++ b/templates/v3/includes/_button_tooltip_v3.html @@ -0,0 +1,19 @@ +{% comment %} + Variables: label (required; tooltip content), position (optional; top|right|bottom|left), button_text (optional; text on trigger), url, aria_label, icon_html, tooltip_id (optional; for multiple tooltips with same label, pass a unique id). + Usage: {% include "v3/includes/_button_tooltip_v3.html" with label="Help" position="bottom" %} + With text: {% include "v3/includes/_button_tooltip_v3.html" with label="More info" button_text="Help" %} +{% endcomment %} +{% with tid=tooltip_id|default:label|slugify %} +
+ {{ label }} + {% if url %} + + {% if icon_html %}{{ icon_html|safe }}{% else %}{% include "includes/icon.html" with icon_name="info-box" icon_size=16 %}{% endif %}{% if button_text %} {{ button_text }}{% endif %} + + {% else %} + + {% endif %} +
+{% endwith %} diff --git a/templates/v3/includes/_carousel_buttons.html b/templates/v3/includes/_carousel_buttons.html new file mode 100644 index 000000000..e677fc379 --- /dev/null +++ b/templates/v3/includes/_carousel_buttons.html @@ -0,0 +1,13 @@ +{% comment %} + Variables: carousel_id (optional, for JS), prev_label, next_label (defaults: Previous, Next). + Usage: {% include "v3/includes/_carousel_buttons.html" %} + For JS: use data-carousel-prev / data-carousel-next or wrap in a carousel container with carousel_id. +{% endcomment %} + diff --git a/templates/v3/includes/_category_cards.html b/templates/v3/includes/_category_cards.html new file mode 100644 index 000000000..db6f465f0 --- /dev/null +++ b/templates/v3/includes/_category_cards.html @@ -0,0 +1,88 @@ +{% comment %} + V3 Category cards – shows all category tag and version tag variants. + Uses static/css/v3/category-tags.css. Tags and variants only (no header/logo). + + Variables (all optional, for dynamic use from view): + category_tags (optional) — list of dicts with tag_label, url, variant, size. + If omitted, shows the variant gallery (Default + Tight, neutral/green/yellow/teal). + section_heading (optional) — section heading, default "Category tags" + show_version_tags (optional) — if truthy, shows version tags block (Default + Hover). + + Usage from view (dynamic tag list): + {% include "v3/includes/_category_cards.html" with category_tags=my_tags section_heading="Categories" %} + Usage in style guide (all variants): + {% include "v3/includes/_category_cards.html" %} +{% endcomment %} +
+

{{ section_heading|default:"Category tags" }}

+ + {% if category_tags %} + {% comment %}Dynamic mode: single list of tags with classes per item.{% endcomment %} +
+ {% for tag in category_tags %} + {% include "v3/includes/_category_tag.html" with tag_label=tag.tag_label url=tag.url variant=tag.variant size=tag.size %} + {% endfor %} +
+ {% else %} + {% comment %}Variant gallery: Default and Tight, each with neutral / green / yellow / teal and normal + hover state.{% endcomment %} +
+ Default +
+
+ {% include "v3/includes/_category_tag.html" with tag_label="Math" url="#" variant="neutral" size="default" %} + {% include "v3/includes/_category_tag.html" with tag_label="Math" url="#" variant="neutral" size="default" show_hover=True %} +
+
+ {% include "v3/includes/_category_tag.html" with tag_label="Math" url="#" variant="green" size="default" %} + {% include "v3/includes/_category_tag.html" with tag_label="Math" url="#" variant="green" size="default" show_hover=True %} +
+
+ {% include "v3/includes/_category_tag.html" with tag_label="Math" url="#" variant="yellow" size="default" %} + {% include "v3/includes/_category_tag.html" with tag_label="Math" url="#" variant="yellow" size="default" show_hover=True %} +
+
+ {% include "v3/includes/_category_tag.html" with tag_label="Math" url="#" variant="teal" size="default" %} + {% include "v3/includes/_category_tag.html" with tag_label="Math" url="#" variant="teal" size="default" show_hover=True %} +
+
+
+ +
+ Tight +
+
+ {% include "v3/includes/_category_tag.html" with tag_label="Math" url="#" variant="neutral" size="tight" %} + {% include "v3/includes/_category_tag.html" with tag_label="Math" url="#" variant="neutral" size="tight" show_hover=True %} +
+
+ {% include "v3/includes/_category_tag.html" with tag_label="Math" url="#" variant="green" size="tight" %} + {% include "v3/includes/_category_tag.html" with tag_label="Math" url="#" variant="green" size="tight" show_hover=True %} +
+
+ {% include "v3/includes/_category_tag.html" with tag_label="Math" url="#" variant="yellow" size="tight" %} + {% include "v3/includes/_category_tag.html" with tag_label="Math" url="#" variant="yellow" size="tight" show_hover=True %} +
+
+ {% include "v3/includes/_category_tag.html" with tag_label="Math" url="#" variant="teal" size="tight" %} + {% include "v3/includes/_category_tag.html" with tag_label="Math" url="#" variant="teal" size="tight" show_hover=True %} +
+
+
+ + {% if show_version_tags %} +
+ Version tags (not clickable) +
+
+ Default + C++ 03 +
+
+ Hover + C++ 03 +
+
+
+ {% endif %} + {% endif %} +
diff --git a/templates/v3/includes/_category_tag.html b/templates/v3/includes/_category_tag.html new file mode 100644 index 000000000..3b44682b9 --- /dev/null +++ b/templates/v3/includes/_category_tag.html @@ -0,0 +1,21 @@ +{% comment %} + V3 Category tag – single tag (link or span). + Uses styles from static/css/v3/category-tags.css. + + Variables: + tag_label (required) — text inside the tag (e.g. "Math", "C++ 03") + url (optional) — if set, renders ; if omitted, renders (e.g. version tags) + variant (required) — "neutral" | "green" | "yellow" | "teal" + size (optional) — "default" | "tight", default "default" + show_hover (optional) — if truthy, adds category-tag--hover-state for hover preview in docs + + Usage (clickable category tag): + {% include "v3/includes/_category_tag.html" with tag_label="Math" url="#" variant="neutral" size="default" %} + Usage (version tag, non-clickable): + {% include "v3/includes/_category_tag.html" with tag_label="C++ 03" variant="neutral" size="default" %} +{% endcomment %} +{% if url %} +{{ tag_label }} +{% else %} +{{ tag_label }} +{% endif %} diff --git a/templates/v3/includes/_code_block.html b/templates/v3/includes/_code_block.html new file mode 100644 index 000000000..b1086c84c --- /dev/null +++ b/templates/v3/includes/_code_block.html @@ -0,0 +1,22 @@ +{% comment %} + V3 Code block – code block with copy button. + Uses static/css/v3/code-block.css. + JS in static/js/code-block-copy.js copies text to clipboard and toggles icon to check. + + Variables: + code (optional) — plain text string to show in the code box (auto-escaped). Use for dynamic content from view. + code_html (optional) — pre-rendered HTML inside (use |safe when you need token spans). Use code or code_html. + variant (optional) — "standalone" | "white-bg" | "grey-bg". Default "standalone". + + Usage with plain string from view: + {% include "v3/includes/_code_block.html" with code=code_string variant="grey-bg" %} + Usage with pre-rendered HTML: + {% include "v3/includes/_code_block.html" with code_html=my_code_html variant="standalone" %} +{% endcomment %} +
+ +
{% if code_html %}{{ code_html|safe }}{% else %}{{ code }}{% endif %}
+
diff --git a/templates/v3/includes/_code_block_card.html b/templates/v3/includes/_code_block_card.html new file mode 100644 index 000000000..e05109a80 --- /dev/null +++ b/templates/v3/includes/_code_block_card.html @@ -0,0 +1,22 @@ +{% comment %} + V3 Code block card – card with heading, optional description, code block and optional button. + Variables: + card_variant (optional) — "neutral" | "teal", default "neutral" + heading (required) — card heading + description (optional) — paragraph below heading + code (optional) — plain text string for the code block (use code or code_html) + code_html (optional) — pre-rendered HTML for + block_variant (optional) — "standalone" | "white-bg" | "grey-bg", default "grey-bg" + button_text (optional) — if set, a button is shown + button_url (optional) — button href, default "#" +{% endcomment %} +
+

{{ heading }}

+ {% if description %} +

{{ description }}

+ {% endif %} + {% include "v3/includes/_code_block.html" with code=code code_html=code_html variant=block_variant|default:"grey-bg" %} + {% if button_text %} + {{ button_text }} + {% endif %} +
diff --git a/templates/v3/includes/_code_blocks_story.html b/templates/v3/includes/_code_blocks_story.html new file mode 100644 index 000000000..5b08ba099 --- /dev/null +++ b/templates/v3/includes/_code_blocks_story.html @@ -0,0 +1,22 @@ +{% comment %} + V3 Code blocks story – two-column grid with code block variants and cards. + Uses static/css/v3/code-block.css. Requires static/js/code-block-copy.js for copy button. + Variables (all optional; pass from view for dynamic code): + code_standalone_1, code_standalone_2 — strings for the two standalone blocks (left column) + code_card_1, code_card_2, code_card_3 — strings for the three cards (right column) + When omitted, the demo uses the inline code strings below. + Usage from view: {% include "v3/includes/_code_blocks_story.html" with code_standalone_1=my_code code_card_1=hello_code %} +{% endcomment %} +{% with code_demo_beast='int main()
{
net::io_context ioc;
tcp::resolver resolver(ioc);
beast::tcp_stream stream(ioc);

stream.connect(resolver.resolve("example.com", "80"));

http::request<http::empty_body> req{http::verb::get, "/", 11};
req.set(http::field::host, "example.com");

http::write(stream, req);

beast::flat_buffer buffer;
http::response<http::string_body> res;
http::read(stream, buffer, res);

std::cout << res << std::endl;
}' code_demo_hello='#include <iostream>
int main()
{
std::cout << "Hello, Boost.";
}' code_demo_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' %} +
+
+ {% include "v3/includes/_code_block.html" with code_html=code_standalone_1|default:code_demo_beast variant="standalone" %} + {% include "v3/includes/_code_block.html" with code_html=code_standalone_2|default:code_demo_beast variant="white-bg" %} +
+
+ {% include "v3/includes/_code_block_card.html" with heading="Get started with our libraries" code_html=code_card_1|default:code_demo_hello block_variant="grey-bg" button_text="Explore examples" %} + {% include "v3/includes/_code_block_card.html" with 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_html=code_card_2|default:code_demo_beast block_variant="grey-bg" %} + {% include "v3/includes/_code_block_card.html" with card_variant="teal" heading="Install" description="Get started with header-only libraries." code_html=code_card_3|default:code_demo_install block_variant="grey-bg" %} +
+
+{% endwith %} diff --git a/templates/v3/includes/_content_detail_card_item.html b/templates/v3/includes/_content_detail_card_item.html new file mode 100644 index 000000000..eb702b326 --- /dev/null +++ b/templates/v3/includes/_content_detail_card_item.html @@ -0,0 +1,22 @@ +{% comment %} + V3 Content detail icon – single card (content-detail-icon). + Variables: + title (required) — card heading + description (required) — card body text + icon_name (optional) — icon name for includes/icon.html (e.g. "bullseye-arrow"). + If omitted, card is rendered without icon and modifier --has-icon is not applied. + title_url (optional) — if set, title is wrapped in a link + Usage: + {% include "v3/includes/_content_detail_card_item.html" with title="Get help" description="Tap into quick answers..." icon_name="bullseye-arrow" %} +{% endcomment %} +
+ {% if icon_name %} + + {% endif %} +

+ {% if title_url %}{% endif %}{{ title }}{% if title_url %}{% endif %} +

+

{{ description }}

+
diff --git a/templates/v3/includes/_content_event_card_item.html b/templates/v3/includes/_content_event_card_item.html new file mode 100644 index 000000000..61f1ef373 --- /dev/null +++ b/templates/v3/includes/_content_event_card_item.html @@ -0,0 +1,21 @@ +{% comment %} + V3 Event content card – single event item (title, description, date). + Uses event-content from static/css/v3/content.css inside event-card from event-cards.css. + + Variables: + title (required) — event title + description (required) — event description + date (required) — human-readable date (e.g. "29/10/25") + datetime (optional) — value for