From bfaa7bd5b543d18b13306fa148550e9c52416f4a Mon Sep 17 00:00:00 2001 From: Greg Kaleka Date: Fri, 13 Mar 2026 08:41:59 -0400 Subject: [PATCH 01/24] Add sensible default password validators (#2148) --- config/settings.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/config/settings.py b/config/settings.py index e7098e4c4..9c5ca3df2 100755 --- a/config/settings.py +++ b/config/settings.py @@ -227,7 +227,17 @@ # Password validation # Only used in production -AUTH_PASSWORD_VALIDATORS = [] +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + "OPTIONS": {"min_length": 9}, + }, + {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, + {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, +] # Sessions From 9f3dc570bdf2e2ba483b4b793291141be44c78f3 Mon Sep 17 00:00:00 2001 From: Jeremy Childers <30885417+jlchilders11@users.noreply.github.com> Date: Tue, 17 Mar 2026 09:49:23 -0400 Subject: [PATCH 02/24] Story 2158: Banner Component (#2181) --- core/views.py | 5 +++ static/css/v3/animations.css | 23 +++++++++++ static/css/v3/banner.css | 39 +++++++++++++++++++ static/css/v3/components.css | 2 + static/css/v3/themes.css | 2 +- static/js/carousel.js | 2 - templates/includes/icon.html | 4 +- .../v3/examples/_v3_example_section.html | 12 ++++++ templates/v3/includes/_banner.html | 16 ++++++++ 9 files changed, 101 insertions(+), 4 deletions(-) create mode 100644 static/css/v3/animations.css create mode 100644 static/css/v3/banner.css create mode 100644 templates/v3/includes/_banner.html diff --git a/core/views.py b/core/views.py index 686c08b0b..54a183636 100644 --- a/core/views.py +++ b/core/views.py @@ -1290,6 +1290,11 @@ def get_context_data(self, **kwargs): ], } + context["banner_data"] = { + "icon_name": "alert", + "banner_message": "This is an older version of Boost and was released in 2017. The current version is 1.90.0.", + } + latest = Version.objects.most_recent() if latest: lv = ( diff --git a/static/css/v3/animations.css b/static/css/v3/animations.css new file mode 100644 index 000000000..b1333ff5c --- /dev/null +++ b/static/css/v3/animations.css @@ -0,0 +1,23 @@ +/* +This animaiton controls the fade out of banners and other items that are hidden after a delay. +*/ + +@keyframes fade-out { + from { + opacity: 1; + } + to { + opacity: 0; + display: none; + } +} + +.banner--fade { + /* + Set the --fade-delay variable on the element with the class to control + how quickly the element is hidden + */ + animation: fade-out 1s var(--fade-delay, 0ms) forwards; + animation-timing-function: ease; + transition-behavior: allow-discrete; +} diff --git a/static/css/v3/banner.css b/static/css/v3/banner.css new file mode 100644 index 000000000..38044d8a3 --- /dev/null +++ b/static/css/v3/banner.css @@ -0,0 +1,39 @@ +.banner { + width: 100%; + display: flex; + padding: var(--space-default, 8px) var(--space-large, 16px); + justify-content: center; + align-items: center; + gap: var(--space-large, 16px); + + border-radius: var(--border-radius-l, 8px); + background: var(--color-surface-brand-accent-default, #FFA000); +} + +.banner__message { + color: var(--color-text-on-accent, #050816); + + /* Sans/Desktop/Regular/XS/Tight */ + font-family: var(--font-sans, "Mona Sans VF"); + font-size: var(--font-size-xs, 12px); + font-weight: var(--font-weight-regular); + line-height: var(--line-height-tight); + /* 12px */ + letter-spacing: -0.12px; +} + +.banner__message a { + text-decoration-line: underline; + text-decoration-style: solid; + text-decoration-skip-ink: auto; + text-decoration-thickness: auto; + text-underline-offset: auto; +} + +.banner__icon { + width: 16px; + height: 16px; + flex-shrink: 0; + aspect-ratio: 1/1; + fill: var(--color-text-on-accent); +} diff --git a/static/css/v3/components.css b/static/css/v3/components.css index fe53a2a7f..6df6ce127 100644 --- a/static/css/v3/components.css +++ b/static/css/v3/components.css @@ -25,3 +25,5 @@ @import './learn-cards.css'; @import './terms-of-use.css'; @import './thread-archive-card.css'; +@import './banner.css'; +@import './animations.css'; diff --git a/static/css/v3/themes.css b/static/css/v3/themes.css index 07b24abad..ac55e6653 100644 --- a/static/css/v3/themes.css +++ b/static/css/v3/themes.css @@ -112,7 +112,7 @@ html.dark { /* Text */ --color-text-error: var(--color-error-mid); --color-text-link-accent: var(--color-secondary-mid-blue); - --color-text-on-accent: var(--color-primary-white); + --color-text-on-accent: var(--color-primary-black); --color-text-reversed: var(--color-primary-black); --color-text-tertiary: var(--color-primary-grey-600); } diff --git a/static/js/carousel.js b/static/js/carousel.js index 34b3f76eb..eefdf5d85 100644 --- a/static/js/carousel.js +++ b/static/js/carousel.js @@ -128,8 +128,6 @@ if (!root || !root.id) return; const track = root.querySelector('[data-carousel-track]'); const controls = document.getElementById(root.id + '-controls'); - console.log(track) - console.log(controls) if (!track || !controls) return; if (root.hasAttribute('data-carousel-infinite')) { diff --git a/templates/includes/icon.html b/templates/includes/icon.html index 5a5a4815b..a05030909 100644 --- a/templates/includes/icon.html +++ b/templates/includes/icon.html @@ -6,7 +6,7 @@ icon_class: optional, default "icon" (for size/color utilities) icon_size: optional, default 24 (viewBox is 24 for all) {% endcomment %} -

Form inputs

+ +
+

Alert Banner

+
+
+ {% include "v3/includes/_banner.html" with icon_name=banner_data.icon_name banner_message=banner_data.banner_message %} +
+ {% include "v3/includes/_banner.html" with icon_name=banner_data.icon_name banner_message=banner_data.banner_message fade_time=5000 %} +
+
+ +

Basic Card

{% with basic_card_data as card %} diff --git a/templates/v3/includes/_banner.html b/templates/v3/includes/_banner.html new file mode 100644 index 000000000..c99373864 --- /dev/null +++ b/templates/v3/includes/_banner.html @@ -0,0 +1,16 @@ +{% comment %} + Banner component for displaying alerts and other information on the screen. + Inputs: + Required: + banner_message, HTML content: Message to be displayed in the banner. + Optional: + icon_name, string: name of an icon to be displayed. See includes/icon.html for options + fate_time, number: time in ms after which the banner should fade. See animation.css for more info +{% endcomment %} + + From f63d121cd458c3e108fb79cd19cb03a0fb4652cb Mon Sep 17 00:00:00 2001 From: Greg Kaleka Date: Tue, 17 Mar 2026 11:03:34 -0400 Subject: [PATCH 03/24] V3: Account connections card component (#2150) --- core/views.py | 55 ++++++++++++++++ static/css/v3/account-connections.css | 66 +++++++++++++++++++ static/css/v3/components.css | 1 + templates/includes/icon.html | 21 +++++- .../v3/examples/_v3_example_section.html | 10 +++ .../includes/_account_connections_card.html | 36 ++++++++++ 6 files changed, 186 insertions(+), 3 deletions(-) create mode 100644 static/css/v3/account-connections.css create mode 100644 templates/v3/includes/_account_connections_card.html diff --git a/core/views.py b/core/views.py index 54a183636..47b96fb9d 100644 --- a/core/views.py +++ b/core/views.py @@ -1295,6 +1295,61 @@ def get_context_data(self, **kwargs): "banner_message": "This is an older version of Boost and was released in 2017. The current version is 1.90.0.", } + context["account_connections_mixed"] = [ + { + "platform": "github", + "label": "GitHub", + "connected": True, + "status_text": "Connected", + "action_label": "Manage", + "action_url": "#", + }, + { + "platform": "google", + "label": "Google", + "connected": False, + "status_text": "Not connected", + "action_label": "Connect", + "action_url": "#", + }, + ] + context["account_connections_all_connected"] = [ + { + "platform": "github", + "label": "GitHub", + "connected": True, + "status_text": "Connected", + "action_label": "Manage", + "action_url": "#", + }, + { + "platform": "google", + "label": "Google", + "connected": True, + "status_text": "Connected", + "action_label": "Manage", + "action_url": "#", + }, + ] + context["account_connections_none_connected"] = [ + { + "platform": "github", + "label": "GitHub", + "connected": False, + "status_text": "Not connected", + "action_label": "Connect", + "action_url": "#", + }, + { + "platform": "google", + "label": "Google", + "connected": False, + "status_text": "Not connected", + "action_label": "Connect", + "action_url": "#", + }, + ] + latest = Version.objects.most_recent() if latest: lv = ( diff --git a/static/css/v3/account-connections.css b/static/css/v3/account-connections.css new file mode 100644 index 000000000..472880c7f --- /dev/null +++ b/static/css/v3/account-connections.css @@ -0,0 +1,66 @@ +/* + Account Connections Card + Shows linked social/OAuth account status with management actions. + Extends the base .card component. + + account-connections__items — vertical list of connection rows + account-connections__item — single connection row (icon + name | status | button) + account-connections__platform — icon + platform name group + account-connections__platform-icon — brand icon (16x16) + account-connections__platform-name — platform display name + account-connections__status — connection status text (fills remaining space) +*/ + +.account-connections-card { + max-width: 696px; +} + +.account-connections__items { + display: flex; + flex-direction: column; + gap: var(--space-large, 16px); + padding: 0 var(--space-large, 16px); + width: 100%; + list-style: none; + margin: 0; +} + +.account-connections__item { + display: flex; + align-items: center; + gap: var(--space-xlarge, 24px); +} + +.account-connections__platform { + display: flex; + align-items: center; + gap: var(--space-default, 8px); + flex-shrink: 0; +} + +.account-connections__platform-icon { + width: 16px; + height: 16px; + flex-shrink: 0; + color: var(--color-text-primary, #050816); +} + +.account-connections__platform-name { + font-family: var(--font-sans, 'Mona Sans VF'), sans-serif; + font-size: var(--font-size-base, 16px); + font-weight: var(--font-weight-medium, 500); + line-height: var(--line-height-default, 1.2); + letter-spacing: var(--letter-spacing-tight, -0.01em); + color: var(--color-text-secondary, #585A64); + white-space: nowrap; +} + +.account-connections__status { + flex: 1 1 0; + font-family: var(--font-sans, 'Mona Sans VF'), sans-serif; + font-size: var(--font-size-base, 16px); + font-weight: var(--font-weight-regular, 400); + line-height: var(--line-height-default, 1.2); + letter-spacing: var(--letter-spacing-tight, -0.01em); + color: var(--color-text-secondary, #585A64); +} diff --git a/static/css/v3/components.css b/static/css/v3/components.css index 6df6ce127..d10cc1b0e 100644 --- a/static/css/v3/components.css +++ b/static/css/v3/components.css @@ -27,3 +27,4 @@ @import './thread-archive-card.css'; @import './banner.css'; @import './animations.css'; +@import './account-connections.css'; diff --git a/templates/includes/icon.html b/templates/includes/icon.html index a05030909..17eea4838 100644 --- a/templates/includes/icon.html +++ b/templates/includes/icon.html @@ -6,7 +6,23 @@ icon_class: optional, default "icon" (for size/color utilities) icon_size: optional, default 24 (viewBox is 24 for all) {% endcomment %} -

Learn Card

{% endwith %}
+ +
+

Account Connections Card

+
+ {% include "v3/includes/_account_connections_card.html" with connections=account_connections_mixed %} + {% include "v3/includes/_account_connections_card.html" with connections=account_connections_all_connected %} + {% include "v3/includes/_account_connections_card.html" with connections=account_connections_none_connected %} +
+
+

Testimonial Card

diff --git a/templates/v3/includes/_account_connections_card.html b/templates/v3/includes/_account_connections_card.html new file mode 100644 index 000000000..e538e73c2 --- /dev/null +++ b/templates/v3/includes/_account_connections_card.html @@ -0,0 +1,36 @@ +{% comment %} + Account Connections Card + Shows linked social/OAuth accounts with connection status and action buttons. + + Variables: + heading (str, optional) — card heading, default "Account connections" + connections (list of dict) — connection items, each with: + .platform (str) — icon key: "github" or "google" + .label (str) — display name, e.g. "GitHub" + .connected (bool) — whether the account is linked + .status_text (str) — status label, e.g. "Connected" or "Not connected" + .action_label (str) — button text, e.g. "Manage" or "Connect" + .action_url (str) — button destination URL + + Usage: + {% include "v3/includes/_account_connections_card.html" with heading="Account connections" connections=account_connections %} +{% endcomment %} + + From 706ca19fefe5cb8c343a7723cb3e4cf08cf849c3 Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Wed, 11 Mar 2026 14:06:30 -0700 Subject: [PATCH 04/24] Add Install card and Update similar form fields styling --- core/views.py | 33 +++++++ static/css/v3/code-block.css | 20 ++--- static/css/v3/components.css | 62 ++++++------- static/css/v3/forms.css | 78 +++++++++------- static/css/v3/install-card.css | 82 +++++++++++++++++ static/css/v3/tab.css | 46 ++++++++++ templates/includes/icon.html | 46 ++++++---- .../v3/examples/_v3_example_section.html | 39 ++++---- templates/v3/includes/_field_checkbox.html | 25 +++--- templates/v3/includes/_field_combo.html | 31 ++++--- templates/v3/includes/_field_dropdown.html | 51 ++++++----- templates/v3/includes/_field_multiselect.html | 37 ++++---- templates/v3/includes/_install_card.html | 90 +++++++++++++++++++ .../_install_card_tab_content_body.html | 51 +++++++++++ templates/v3/includes/_tab.html | 28 ++++++ templates/v3/includes/_tab_content.html | 21 +++++ 16 files changed, 558 insertions(+), 182 deletions(-) create mode 100644 static/css/v3/install-card.css create mode 100644 static/css/v3/tab.css create mode 100644 templates/v3/includes/_install_card.html create mode 100644 templates/v3/includes/_install_card_tab_content_body.html create mode 100644 templates/v3/includes/_tab.html create mode 100644 templates/v3/includes/_tab_content.html diff --git a/core/views.py b/core/views.py index 47b96fb9d..71d65bc85 100644 --- a/core/views.py +++ b/core/views.py @@ -1111,10 +1111,43 @@ def get_context_data(self, **kwargs): # Install bjam tool user config: https://www.bfgroup.xyz/b2/manual/release/index.html cp ./libs/beast/tools/user-config.jam $HOME""" + INSTALL_CARD_PKG_MANAGERS = [ + {"label": "Conan", "value": "conan", "command": "conan install boost"}, + {"label": "Vcpkg", "value": "vcpkg", "command": "vcpkg install boost"}, + ] + INSTALL_CARD_SYSTEM_INSTALL = [ + { + "label": "Ubuntu", + "value": "ubuntu", + "command": "sudo apt install libboost-all-dev", + }, + { + "label": "Fedora", + "value": "fedora", + "command": "sudo dnf install boost-devel", + }, + { + "label": "CentOS", + "value": "centos", + "command": "sudo yum install boost-devel", + }, + {"label": "Arch", "value": "arch", "command": "sudo pacman -S boost"}, + {"label": "Homebrew", "value": "homebrew", "command": "brew install boost"}, + ] + context = super().get_context_data(**kwargs) context["code_demo_beast"] = CODE_DEMO_BEAST context["code_demo_hello"] = CODE_DEMO_HELLO context["code_demo_install"] = CODE_DEMO_INSTALL + context["install_card_title"] = ( + "Install Boost and get started in your terminal." + ) + context["install_card_pkg_managers_json"] = json.dumps( + INSTALL_CARD_PKG_MANAGERS + ) + context["install_card_system_install_json"] = json.dumps( + INSTALL_CARD_SYSTEM_INSTALL + ) context["popular_terms"] = [ {"label": "Networking"}, {"label": "Math"}, diff --git a/static/css/v3/code-block.css b/static/css/v3/code-block.css index cdb7993d0..cf0637a12 100644 --- a/static/css/v3/code-block.css +++ b/static/css/v3/code-block.css @@ -1,6 +1,4 @@ - - - :root { +:root { --code-block-bg: var(--color-bg-secondary); --code-block-border: var(--color-border); --code-block-text: var(--color-syntax-text); @@ -16,7 +14,6 @@ .code-block--standalone { background: var(--code-block-bg-standalone); - border: 1px dashed var(--code-block-border-standalone); overflow: visible; } @@ -32,13 +29,13 @@ .code-block--white-bg { background: var(--color-bg-secondary); + border: 1px solid var(--code-block-border); } .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); @@ -68,13 +65,7 @@ overflow-wrap: break-word; } -.code-block__inner code{ - font-weight: 500; - color: var(--color-syntax-text) !important; - white-space: break-spaces !important; -} - -.code-block__inner code{ +.code-block__inner code { font-weight: 500; color: var(--color-syntax-text) !important; white-space: break-spaces !important; @@ -153,14 +144,15 @@ html.dark .code-block-card--grey { } .code-block-card__description { - padding: var(--space-card) var(--space-card) var(--space-default) var(--space-card); + padding: var(--space-card) var(--space-card) var(--space-default) + var(--space-card); margin: 0 0 var(--space-medium); font-size: var(--font-size-medium); line-height: var(--line-height-relaxed); color: var(--color-text-secondary); } -.code-block-card .code-block-card__description{ +.code-block-card .code-block-card__description { border-top: 1px solid var(--color-border); } diff --git a/static/css/v3/components.css b/static/css/v3/components.css index d10cc1b0e..5d8974b8b 100644 --- a/static/css/v3/components.css +++ b/static/css/v3/components.css @@ -1,30 +1,32 @@ -@import './fonts.css'; -@import './foundations.css'; -@import './button-tooltip.css'; -@import './post-cards.css'; -@import './buttons.css'; -@import './avatar.css'; -@import './carousel-buttons.css'; -@import './v3-examples-section.css'; -@import './header.css'; -@import './footer.css'; -@import './forms.css'; -@import './testimonial-card.css'; -@import './card.css'; -@import './event-cards.css'; -@import './content.css'; -@import './why-boost-cards.css'; -@import './stats.css'; -@import './category-tags.css'; -@import './code-block.css'; -@import './wysiwyg-editor.css'; -@import './search-card.css'; -@import './create-account-card.css'; -@import './privacy-policy.css'; -@import './library-intro-card.css'; -@import './learn-cards.css'; -@import './terms-of-use.css'; -@import './thread-archive-card.css'; -@import './banner.css'; -@import './animations.css'; -@import './account-connections.css'; +@import "./fonts.css"; +@import "./foundations.css"; +@import "./button-tooltip.css"; +@import "./post-cards.css"; +@import "./buttons.css"; +@import "./avatar.css"; +@import "./carousel-buttons.css"; +@import "./v3-examples-section.css"; +@import "./header.css"; +@import "./footer.css"; +@import "./forms.css"; +@import "./testimonial-card.css"; +@import "./card.css"; +@import "./event-cards.css"; +@import "./content.css"; +@import "./why-boost-cards.css"; +@import "./stats.css"; +@import "./category-tags.css"; +@import "./code-block.css"; +@import "./wysiwyg-editor.css"; +@import "./search-card.css"; +@import "./create-account-card.css"; +@import "./privacy-policy.css"; +@import "./library-intro-card.css"; +@import "./learn-cards.css"; +@import "./terms-of-use.css"; +@import "./thread-archive-card.css"; +@import "./tab.css"; +@import "./install-card.css"; +@import "./banner.css"; +@import "./animations.css"; +@import "./account-connections.css"; diff --git a/static/css/v3/forms.css b/static/css/v3/forms.css index 83a33f6b4..b2b6663d7 100644 --- a/static/css/v3/forms.css +++ b/static/css/v3/forms.css @@ -19,18 +19,20 @@ height: 40px; padding: 0 var(--space-large, 16px); background-color: var(--color-surface-weak, #fff); - border: 1px solid var(--color-stroke-weak, #0508161A); + 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; + transition: + background-color 0.15s ease, + border-color 0.15s ease; cursor: text; } .field__control:hover { - border-color: var(--color-stroke-mid, #0508162B); + border-color: var(--color-stroke-mid, #0508162b); } .field__control:hover .field__input::placeholder { - color: var(--color-text-link-accent, #00778B); + color: var(--color-text-link-accent, #00778b); } .field__control:focus-within { @@ -74,8 +76,8 @@ } .field__icon { - width: 24px; - height: 24px; + width: 16px; + height: 16px; flex-shrink: 0; color: var(--color-icon-secondary, #6b6d78); } @@ -100,7 +102,7 @@ } .field__submit:focus-visible { - outline: 2px solid var(--color-stroke-link-accent, #1F3044); + outline: 2px solid var(--color-stroke-link-accent, #1f3044); outline-offset: 2px; border-radius: var(--border-radius-xs, 2px); } @@ -137,16 +139,16 @@ .field--error .field__control { background-color: var(--color-surface-error-weak, #fdf2f2); - border-color: var(--color-stroke-error, #D53F3F33); + border-color: var(--color-stroke-error, #d53f3f33); } .field--error .field__control:hover { - border-color: var(--color-stroke-error, #D53F3F33); + 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); + border-color: var(--color-stroke-error, #d53f3f33); } .field--error .field__input { @@ -180,29 +182,30 @@ height: 40px; padding: 0 var(--space-large, 16px); background-color: var(--color-surface-weak, #fff); - border: 1px solid var(--color-stroke-weak, #0508161A); + 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; + transition: + background-color 0.15s ease, + border-color 0.15s ease; text-align: left; } .dropdown__trigger:hover { - border-color: var(--color-stroke-mid, #0508162B); + border-color: var(--color-stroke-mid, #0508162b); } .dropdown__trigger:hover .dropdown__trigger-placeholder { - color: var(--color-text-link-accent, #00778B); + 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; + border-color: var(--color-stroke-weak); } .dropdown__trigger-text { @@ -218,10 +221,10 @@ } .dropdown__chevron { - width: 24px; - height: 24px; + width: 16px; + height: 16px; flex-shrink: 0; - color: var(--color-icon-secondary, #6b6d78); + color: var(--color-icon-primary); transition: transform 0.15s ease; } @@ -231,10 +234,9 @@ .dropdown--open .dropdown__trigger { background-color: var(--color-surface-mid, #f7f7f8); - border-color: var(--color-stroke-strong, #05081640); + border-color: var(--color-stroke-weak); border-bottom-left-radius: 0; border-bottom-right-radius: 0; - border-bottom-color: var(--color-stroke-weak, #0508161A); } .dropdown__panel { @@ -243,7 +245,7 @@ left: 0; right: 0; background-color: var(--color-surface-weak, #fff); - border: 1px solid var(--color-stroke-strong, #05081640); + border: 1px solid var(--color-stroke-weak); border-top: none; border-top-left-radius: 0; border-top-right-radius: 0; @@ -257,7 +259,6 @@ .dropdown__list { max-height: 320px; overflow-y: auto; - padding: var(--space-s, 4px) 0; } .dropdown__list::-webkit-scrollbar { @@ -269,7 +270,7 @@ } .dropdown__list::-webkit-scrollbar-thumb { - background-color: var(--color-stroke-mid, #0508162B); + background-color: var(--color-stroke-mid, #0508162b); border-radius: 2px; } @@ -281,26 +282,33 @@ 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); + color: var(--color-text-secondary); cursor: pointer; transition: background-color 0.1s ease; } +.dropdown__item:not(:first-of-type) { + border-top: 1px solid var(--color-stroke-weak); +} + .dropdown__item:hover { background-color: var(--color-surface-mid, #f7f7f8); + color: var(--color-text-link-accent); } .dropdown__item--selected { + color: var(--color-text-primary); font-weight: var(--font-weight-medium, 500); + text-decoration: underline; } .field--error .dropdown__trigger { background-color: var(--color-surface-error-weak, #fdf2f2); - border-color: var(--color-stroke-error, #D53F3F33); + border-color: var(--color-stroke-error, #d53f3f33); } .field--error .dropdown__trigger:hover { - border-color: var(--color-stroke-error, #D53F3F33); + border-color: var(--color-stroke-error, #d53f3f33); } .combo__search-row { @@ -324,8 +332,8 @@ } .combo__search-icon { - width: 24px; - height: 24px; + width: 16px; + height: 16px; flex-shrink: 0; color: var(--color-icon-secondary, #6b6d78); } @@ -368,7 +376,7 @@ .multiselect__item { display: flex; align-items: center; - gap: var(--space-xlarge, 24px); + gap: var(--space-medium); height: 40px; padding: 0 var(--space-large, 16px); font-family: var(--font-sans); @@ -393,7 +401,9 @@ display: flex; align-items: center; justify-content: center; - transition: background-color 0.1s ease, border-color 0.1s ease; + transition: + background-color 0.1s ease, + border-color 0.1s ease; } .multiselect__check--active { @@ -444,7 +454,9 @@ display: flex; align-items: center; justify-content: center; - transition: background-color 0.1s ease, border-color 0.1s ease; + transition: + background-color 0.1s ease, + border-color 0.1s ease; } .checkbox__input:checked + .checkbox__box { @@ -465,7 +477,7 @@ } .checkbox__input:focus-visible + .checkbox__box { - outline: 2px solid var(--color-stroke-link-accent, #0077B8); + outline: 2px solid var(--color-stroke-link-accent, #0077b8); outline-offset: 2px; } diff --git a/static/css/v3/install-card.css b/static/css/v3/install-card.css new file mode 100644 index 000000000..7dea47c51 --- /dev/null +++ b/static/css/v3/install-card.css @@ -0,0 +1,82 @@ +/* + Install Card + Tabs + dropdown selector + command display for getting-started install instructions. + Builds on .card (card.css), .dropdown (forms.css), .code-block (code-block.css). + The tabs are styled in tab.css +*/ + +.install-card { + max-width: 458px; + gap: var(--space-xl); +} + +/* ── Heading ─────────────────────────────────────────────── */ +.install-card__title { + margin-bottom: 0; +} + +/* ── Body wrapper ─────────────────────────────────────────── */ +.install-card__body { + display: flex; + flex-direction: column; + gap: var(--space-medium); + width: 100%; + padding-bottom: var(--space-large); + box-sizing: border-box; + min-width: 0; +} + +/* ── Command display: code block with nested dropdown ─────── */ +.install-card__command { + display: flex; + flex-direction: row; + align-items: flex-start; + gap: var(--space-xl); + border-radius: var(--border-radius-l); + padding: var(--space-medium) var(--space-large); + /* Allow the dropdown panel to overflow the container */ + overflow: visible; + position: relative; +} + +/* Code text fills remaining space and wraps rather than going under the dropdown */ +.install-card__command .code-block__inner { + flex: 1 1 0; + min-width: 0; + white-space: pre-wrap; + word-break: keep-all; + margin-top: var(--space-medium); + margin-bottom: var(--space-medium); +} + +.install-card__command .code-block__inner code { + font-weight: var(--font-weight-regular); +} + +/* ── Dropdown (inside command block) ──────────────────────── */ +.install-card__dropdown { + flex-shrink: 0; + min-width: 140px; +} + +.install-card__dropdown .dropdown__trigger { + background-color: var(--color-surface-strong); + border: 1px solid var(--color-surface-strong); +} + +.install-card__dropdown.dropdown--open > .dropdown__trigger { + background-color: var(--color-surface-mid); + color: var(--color-text-secondary); +} + +.install-card__dropdown .dropdown__item:hover { + color: var(--color-text-link-accent); + background-color: var(--color-surface-mid); +} + +/* ── Responsive ───────────────────────────────────────────── */ +@media (max-width: 767px) { + .install-card__dropdown { + min-width: 120px; + } +} diff --git a/static/css/v3/tab.css b/static/css/v3/tab.css new file mode 100644 index 000000000..598229fed --- /dev/null +++ b/static/css/v3/tab.css @@ -0,0 +1,46 @@ +/* + Tab + Reusable tab trigger and tab list styles. + Used by _tab.html template component. +*/ + +/* ── Tab list ─────────────────────────────────────────────── */ + +.tab__list { + display: flex; + flex-direction: row; + gap: var(--space-medium); + overflow-x: auto; + -webkit-overflow-scrolling: touch; + /* Hide scrollbar while keeping scroll functionality */ + scrollbar-width: none; +} + +.tab__list::-webkit-scrollbar { + display: none; +} + +.tab__trigger { + margin: var(--space-xs); + background: none; + border: none; + border-bottom: 1px solid transparent; + cursor: pointer; + font-family: var(--font-sans); + font-size: var(--font-size-xs); + font-weight: var(--font-weight-regular); + color: var(--color-text-secondary); + line-height: var(--line-height-default); + transition: + color 0.1s ease, + border-color 0.1s ease; +} + +.tab__trigger:hover { + color: var(--color-text-primary); +} + +.tab__trigger--active { + border-bottom-color: var(--color-text-secondary); + color: var(--color-text-primary); +} diff --git a/templates/includes/icon.html b/templates/includes/icon.html index 17eea4838..1381daed2 100644 --- a/templates/includes/icon.html +++ b/templates/includes/icon.html @@ -5,6 +5,8 @@ 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) + x-show: optional, Alpine directive to conditionally show the icon + x-cloak: optional, Alpine directive to prevent flash of unstyled icon when using x-show {% endcomment %} {% if icon_name == "github" %} {% else %} -

Carousel buttons

{% include "v3/includes/_carousel_buttons.html" %}
-

Detail card carousel

+

Detail card carousel

{% include "v3/includes/_cards_carousel_v3.html" with carousel_id="post-cards-carousel-demo" heading="Libraries categories" cards=demo_cards_carousel_cards %}
-

Detail card carousel with autoplay

+

Detail card carousel with autoplay

{% include "v3/includes/_cards_carousel_v3.html" with carousel_id="post-cards-carousel-demo-autoplay" heading="Libraries categories" cards=demo_cards_carousel_cards autoplay=True autoplay_delay=5000 %}
-

Detail card carousel with infinite looping and autoplay

+

Detail card carousel with infinite looping and autoplay

{% include "v3/includes/_cards_carousel_v3.html" with carousel_id="post-cards-carousel-demo-infinite-autoplay" heading="Libraries categories" cards=demo_cards_carousel_cards infinite=True autoplay=True autoplay_delay=5000 %}
@@ -159,7 +159,7 @@

-

Detail card (single)

+

Detail card (single)

{% 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" %} @@ -173,14 +173,14 @@

Search Card

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

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 %} {% if example_library_choices %}
@@ -266,9 +266,8 @@

Alert Banner

-
-

Basic Card

+

Basic Card

{% with basic_card_data as card %}

Card with two buttons

@@ -288,8 +287,8 @@

Horizontal Card

{% endwith %}
-
-

Learn Card

+
+

Learn Card

{% with learn_card_data as data %} {% include "v3/includes/_learn_card.html" with title=data.title text=data.text links=data.links image_src=data.image_src url=data.url label=data.label %} @@ -386,7 +385,7 @@

-

Content event card – clickable cards (link wraps each card)

+

Content event card – clickable cards (link wraps each card)

@@ -427,6 +426,14 @@

Content detail card

+ +
+

Install Card

+
+ {% include "v3/includes/_install_card.html" with title=install_card_title %} +
+
+

Code blocks

diff --git a/templates/v3/includes/_field_checkbox.html b/templates/v3/includes/_field_checkbox.html index 90cc20780..fc617387d 100644 --- a/templates/v3/includes/_field_checkbox.html +++ b/templates/v3/includes/_field_checkbox.html @@ -11,21 +11,18 @@ Usage: {% include "v3/includes/_field_checkbox.html" with name="agree" label="I agree to the terms" %} {% endcomment %} -
diff --git a/templates/v3/includes/_field_dropdown.html b/templates/v3/includes/_field_dropdown.html index 4ede60fd0..2cfce4a96 100644 --- a/templates/v3/includes/_field_dropdown.html +++ b/templates/v3/includes/_field_dropdown.html @@ -14,6 +14,7 @@ {% include "v3/includes/_field_dropdown.html" with name="country" label="Country" options=country_options placeholder="Select a country" %} {% endcomment %}
- {% if label %} - - {% endif %} + {# djlint:on #} + {% if label %}{% endif %} - + {% if error %} - + {% elif help_text %} -

{{ help_text }}

+

{{ help_text }}

{% endif %}
diff --git a/templates/v3/includes/_field_multiselect.html b/templates/v3/includes/_field_multiselect.html index 6df31d218..5b8ecef20 100644 --- a/templates/v3/includes/_field_multiselect.html +++ b/templates/v3/includes/_field_multiselect.html @@ -13,9 +13,12 @@ {% include "v3/includes/_field_multiselect.html" with name="tags" label="Tags" options_json=tags_json placeholder="Select tags..." %} {% endcomment %} -{% if selected_json %}{% endif %} +{% if selected_json %} + +{% endif %}
- {% if label %} - - {% endif %} + {# djlint:on #} + {% if label %}{% endif %} diff --git a/templates/v3/includes/_install_card.html b/templates/v3/includes/_install_card.html new file mode 100644 index 000000000..d2a4a4625 --- /dev/null +++ b/templates/v3/includes/_install_card.html @@ -0,0 +1,90 @@ +{% comment %} + V3 Install Card + Interactive card with tabbed installation methods and a dynamic command display. + Uses Alpine.js for tab/dropdown state (script is placed in static/js/install-card.js). + CSS in static/css/v3/install-card.css. + + Variables: + title (optional) — card heading text; defaults to "Install Boost and get started in your terminal." + install_card_pkg_managers_json — JSON array of { label, value, command } for Package Managers tab + install_card_system_install_json — JSON array of { label, value, command } for System Install tab + extra_attrs (optional) — string of extra HTML attributes on the root element (e.g. for analytics) + + Usage: + {% include "v3/includes/_install_card.html" %} +{% endcomment %} + + + +
+ {# djlint:on #} + +
+

+ {{ title|default:"Install Boost and get started in your terminal." }} +

+
+ +
+ {# Tab list #} +
+ {% include "v3/includes/_tab.html" with tab_label="Package Managers" tab_value="package-managers" tab_id="install-tab-pkg" tab_controls="install-panel" tab_next="system-install" tab_prev="system-install" only %} + {% include "v3/includes/_tab.html" with tab_label="System Install" tab_value="system-install" tab_id="install-tab-sys" tab_controls="install-panel" tab_next="package-managers" tab_prev="package-managers" only %} +
+ {# Single tab content – content updates reactively with active tab #} + {% include "v3/includes/_tab_content.html" with content_id="install-panel" content_tab_a_id="install-tab-pkg" content_tab_b_id="install-tab-sys" content_tab_a_value="package-managers" tab_content_template="v3/includes/_install_card_tab_content_body.html" only %} +
+
diff --git a/templates/v3/includes/_install_card_tab_content_body.html b/templates/v3/includes/_install_card_tab_content_body.html new file mode 100644 index 000000000..be5d02d92 --- /dev/null +++ b/templates/v3/includes/_install_card_tab_content_body.html @@ -0,0 +1,51 @@ +{% comment %} + V3 Install Card — Tab Content Body + The tab content body for the install card: a code block with an embedded dropdown. + Relies on the parent Alpine.js x-data scope from _install_card.html. + CSS: install-card.css, code-block.css, forms.css (.dropdown) +{% endcomment %} +{# Code block with dropdown nested inside #} +
+ {# Code #} +
+ {# Inline Dropdown to share the parent x-data scope #} + +
diff --git a/templates/v3/includes/_tab.html b/templates/v3/includes/_tab.html new file mode 100644 index 000000000..36f8c3f92 --- /dev/null +++ b/templates/v3/includes/_tab.html @@ -0,0 +1,28 @@ +{% comment %} + V3 Tab Button + Reusable tab button for use inside a role="tablist" container. + Requires a parent Alpine.js x-data scope with switchTab(value) method and activeTab property. + CSS: tab.css (.tab__trigger) + + Variables: + tab_label — visible button text for the tab trigger + tab_value — Alpine.js tab identifier (e.g. 'package-managers') + tab_id — HTML id attribute (e.g. 'install-tab-pkg') + tab_controls — aria-controls target panel id + tab_next — tab value to switch to on right arrow key + tab_prev — tab value to switch to on left arrow key + + Usage: + {% include "v3/includes/_tab.html" with tab_label="Package Managers" tab_value="package-managers" tab_id="install-tab-pkg" tab_controls="install-panel" tab_next="system-install" tab_prev="system-install" only %} +{% endcomment %} + diff --git a/templates/v3/includes/_tab_content.html b/templates/v3/includes/_tab_content.html new file mode 100644 index 000000000..6217acb12 --- /dev/null +++ b/templates/v3/includes/_tab_content.html @@ -0,0 +1,21 @@ +{% comment %} + Tab Content + Reusable tab content wrapper. Pass a template path via tab_content_template + to render any component as the content body. + Requires a parent Alpine.js x-data scope with activeTab property. + CSS: install-card.css (.install-card__tab-content) + + Variables: + content_id — HTML id attribute (e.g. 'install-panel') + content_tab_a_id — id of the first tab (for aria-labelledby) + content_tab_b_id — id of the second tab (for aria-labelledby) + content_tab_a_value — Alpine.js value of the first tab (used in aria-labelledby ternary) + tab_content_template — template path to {% include %} as the content body + + Usage: + {% include "v3/includes/_tab_content.html" with content_id="install-panel" content_tab_a_id="install-tab-pkg" content_tab_b_id="install-tab-sys" content_tab_a_value="package-managers" tab_content_template="v3/includes/_install_card_tab_content_body.html" only %} +{% endcomment %} +
{% include tab_content_template %}
From 54acaa45177140f14e905bb4c40fc3dd58960ba6 Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Wed, 11 Mar 2026 14:08:53 -0700 Subject: [PATCH 05/24] Hide V3 demo components from all pages except Demo page --- templates/base.html | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/templates/base.html b/templates/base.html index 48377cc31..9f136c445 100644 --- a/templates/base.html +++ b/templates/base.html @@ -399,9 +399,11 @@ {% endblock %}
- {% flag "v3" %} - {% include "v3/examples/_v3_example_section.html" %} - {% endflag %} + {% if '/v3/demo/component' in request.path %} + {% flag "v3" %} + {% include "v3/examples/_v3_example_section.html" %} + {% endflag %} + {% endif %} {% if not hide_footer %} {% flag "v3" %} From d531aeead4909594192ddaa67bb71ee3a1181a18 Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Thu, 12 Mar 2026 10:29:05 -0700 Subject: [PATCH 06/24] Update URL path matching and remove outdated comment --- templates/base.html | 2 +- templates/v3/includes/_install_card.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/base.html b/templates/base.html index 9f136c445..528f31b24 100644 --- a/templates/base.html +++ b/templates/base.html @@ -399,7 +399,7 @@ {% endblock %}
- {% if '/v3/demo/component' in request.path %} + {% if request.resolver_match.url_name == 'v3-demo-components' %} {% flag "v3" %} {% include "v3/examples/_v3_example_section.html" %} {% endflag %} diff --git a/templates/v3/includes/_install_card.html b/templates/v3/includes/_install_card.html index d2a4a4625..44279bb02 100644 --- a/templates/v3/includes/_install_card.html +++ b/templates/v3/includes/_install_card.html @@ -1,7 +1,7 @@ {% comment %} V3 Install Card Interactive card with tabbed installation methods and a dynamic command display. - Uses Alpine.js for tab/dropdown state (script is placed in static/js/install-card.js). + Uses Alpine.js for tab/dropdown state CSS in static/css/v3/install-card.css. Variables: From 4a4e427432ed14abcf88bf9e1fb0322566d871ed Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Fri, 13 Mar 2026 14:56:47 -0700 Subject: [PATCH 07/24] Implement install card with minimal js --- core/views.py | 8 +- static/css/v3/install-card.css | 66 +++++++++- static/js/install-card.js | 107 +++++++++++++++ templates/base.html | 1 + templates/v3/includes/_install_card.html | 122 ++++++++---------- .../_install_card_tab_content_body.html | 80 ++++++------ templates/v3/includes/_tab.html | 28 ---- templates/v3/includes/_tab_content.html | 21 --- 8 files changed, 272 insertions(+), 161 deletions(-) create mode 100644 static/js/install-card.js delete mode 100644 templates/v3/includes/_tab.html delete mode 100644 templates/v3/includes/_tab_content.html diff --git a/core/views.py b/core/views.py index 71d65bc85..9bea576e8 100644 --- a/core/views.py +++ b/core/views.py @@ -1142,12 +1142,8 @@ def get_context_data(self, **kwargs): context["install_card_title"] = ( "Install Boost and get started in your terminal." ) - context["install_card_pkg_managers_json"] = json.dumps( - INSTALL_CARD_PKG_MANAGERS - ) - context["install_card_system_install_json"] = json.dumps( - INSTALL_CARD_SYSTEM_INSTALL - ) + context["install_card_pkg_managers"] = INSTALL_CARD_PKG_MANAGERS + context["install_card_system_install"] = INSTALL_CARD_SYSTEM_INSTALL context["popular_terms"] = [ {"label": "Networking"}, {"label": "Math"}, diff --git a/static/css/v3/install-card.css b/static/css/v3/install-card.css index 7dea47c51..0fb525033 100644 --- a/static/css/v3/install-card.css +++ b/static/css/v3/install-card.css @@ -1,6 +1,7 @@ /* Install Card Tabs + dropdown selector + command display for getting-started install instructions. + Uses CSS radio inputs for tab switching and option selection (no JS required). Builds on .card (card.css), .dropdown (forms.css), .code-block (code-block.css). The tabs are styled in tab.css */ @@ -10,6 +11,19 @@ gap: var(--space-xl); } +/* ── Visually hidden radio inputs ────────────────────────── */ +.install-card__radio { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + /* ── Heading ─────────────────────────────────────────────── */ .install-card__title { margin-bottom: 0; @@ -64,9 +78,24 @@ border: 1px solid var(--color-surface-strong); } -.install-card__dropdown.dropdown--open > .dropdown__trigger { +/* ── details/summary dropdown overrides ───────────────────── */ +.install-card__dropdown summary { + list-style: none; +} + +.install-card__dropdown summary::-webkit-details-marker { + display: none; +} + +.install-card__dropdown[open] > .dropdown__trigger { background-color: var(--color-surface-mid); color: var(--color-text-secondary); + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +.install-card__dropdown[open] .dropdown__chevron { + transform: rotate(180deg); } .install-card__dropdown .dropdown__item:hover { @@ -74,6 +103,41 @@ background-color: var(--color-surface-mid); } +/* ── Radio-driven show/hide (static rules) ──────────────── */ +/* Default: hide all panels, commands, and dropdown labels */ +.install-card__panel { + display: none; +} +.install-card__cmd { + display: none; +} +.install-card__dropdown .dropdown__label { + display: none; +} + +/* Tab panel switching */ +#install-tab-pkg:checked ~ .install-card__body .install-card__panel--pkg { + display: block; +} +#install-tab-sys:checked ~ .install-card__body .install-card__panel--sys { + display: block; +} + +/* Tab active styling */ +#install-tab-pkg:checked + ~ .install-card__body + .tab__list + [for="install-tab-pkg"], +#install-tab-sys:checked + ~ .install-card__body + .tab__list + [for="install-tab-sys"] { + border-bottom-color: var(--color-text-secondary); + color: var(--color-text-primary); +} + +/* Per-option visibility rules are generated by Django in _install_card.html */ + /* ── Responsive ───────────────────────────────────────────── */ @media (max-width: 767px) { .install-card__dropdown { diff --git a/static/js/install-card.js b/static/js/install-card.js new file mode 100644 index 000000000..846f376b1 --- /dev/null +++ b/static/js/install-card.js @@ -0,0 +1,107 @@ +/** + * Install Card — Keyboard navigation & UX enhancements + * Core interactions (tabs, dropdown, option selection) work without JS via + * CSS radio inputs and
/. This script adds: + * - Arrow-key navigation between tabs + * - Escape to close dropdown + * - Enter/Space to select dropdown items + * - Auto-close dropdown after option selection + * - ARIA attribute updates on state changes + */ +document.addEventListener("DOMContentLoaded", function () { + var card = document.querySelector("[data-install-card]"); + if (!card) return; + + var tabLabels = card.querySelectorAll('.tab__list [role="tab"]'); + + // --- Arrow-key tab navigation --- + tabLabels.forEach(function (label) { + label.addEventListener("keydown", function (e) { + var tabs = Array.from(tabLabels); + var idx = tabs.indexOf(label); + var target = null; + + if (e.key === "ArrowRight") target = tabs[(idx + 1) % tabs.length]; + if (e.key === "ArrowLeft") + target = tabs[(idx - 1 + tabs.length) % tabs.length]; + + if (target) { + e.preventDefault(); + // Check the associated radio to switch tab + var radio = document.getElementById(target.getAttribute("for")); + if (radio) radio.checked = true; + target.focus(); + updateTabAria(); + } + }); + }); + + // --- Tab ARIA updates on radio change --- + card.querySelectorAll('input[name="install-tab"]').forEach(function (radio) { + radio.addEventListener("change", updateTabAria); + }); + + function updateTabAria() { + tabLabels.forEach(function (label) { + var radio = document.getElementById(label.getAttribute("for")); + var isActive = radio && radio.checked; + label.setAttribute("aria-selected", isActive ? "true" : "false"); + label.setAttribute("tabindex", isActive ? "0" : "-1"); + }); + } + + // Set initial ARIA state + updateTabAria(); + + // --- Dropdown enhancements per panel --- + card.querySelectorAll(".install-card__panel").forEach(function (panel) { + var details = panel.querySelector("details.install-card__dropdown"); + if (!details) return; + + var summary = details.querySelector("summary"); + var items = panel.querySelectorAll(".dropdown__item"); + + // Auto-close
after selecting an option + items.forEach(function (item) { + item.addEventListener("click", function () { + details.removeAttribute("open"); + summary.focus(); + updateOptionAria(panel); + }); + + // Enter/Space to select + item.addEventListener("keydown", function (e) { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + item.click(); + } + }); + }); + + // Escape to close dropdown + details.addEventListener("keydown", function (e) { + if (e.key === "Escape" && details.hasAttribute("open")) { + e.preventDefault(); + details.removeAttribute("open"); + summary.focus(); + } + }); + + // Update ARIA on radio change + panel.querySelectorAll('input[type="radio"]').forEach(function (radio) { + radio.addEventListener("change", function () { + updateOptionAria(panel); + }); + }); + }); + + function updateOptionAria(panel) { + var items = panel.querySelectorAll(".dropdown__item"); + items.forEach(function (item) { + var radioId = item.getAttribute("for"); + var radio = document.getElementById(radioId); + var isSelected = radio && radio.checked; + item.setAttribute("aria-selected", isSelected ? "true" : "false"); + }); + } +}); diff --git a/templates/base.html b/templates/base.html index 528f31b24..3a08d0122 100644 --- a/templates/base.html +++ b/templates/base.html @@ -48,6 +48,7 @@ + {% endflag %} {% block extra_head %} diff --git a/templates/v3/includes/_install_card.html b/templates/v3/includes/_install_card.html index 44279bb02..3391595db 100644 --- a/templates/v3/includes/_install_card.html +++ b/templates/v3/includes/_install_card.html @@ -1,90 +1,78 @@ {% comment %} V3 Install Card Interactive card with tabbed installation methods and a dynamic command display. - Uses Alpine.js for tab/dropdown state + Uses CSS-only radio inputs for tab switching and option selection. + JS enhances keyboard navigation only. CSS in static/css/v3/install-card.css. Variables: title (optional) — card heading text; defaults to "Install Boost and get started in your terminal." - install_card_pkg_managers_json — JSON array of { label, value, command } for Package Managers tab - install_card_system_install_json — JSON array of { label, value, command } for System Install tab - extra_attrs (optional) — string of extra HTML attributes on the root element (e.g. for analytics) + install_card_pkg_managers — list of { label, value, command } for Package Managers tab + install_card_system_install — list of { label, value, command } for System Install tab Usage: {% include "v3/includes/_install_card.html" %} {% endcomment %} - - - +{# Per-option visibility rules (generated from data, complements static rules in install-card.css) #} +
- {# djlint:on #} - + data-install-card + aria-labelledby="install-card-heading"> + {# Tab radio inputs — must be direct children of section for sibling selectors #} + +

{{ title|default:"Install Boost and get started in your terminal." }}

-
{# Tab list #}
- {% include "v3/includes/_tab.html" with tab_label="Package Managers" tab_value="package-managers" tab_id="install-tab-pkg" tab_controls="install-panel" tab_next="system-install" tab_prev="system-install" only %} - {% include "v3/includes/_tab.html" with tab_label="System Install" tab_value="system-install" tab_id="install-tab-sys" tab_controls="install-panel" tab_next="package-managers" tab_prev="package-managers" only %} + + +
+ {# Panel: Package Managers #} +
+ {% include "v3/includes/_install_card_tab_content_body.html" with options=install_card_pkg_managers prefix="pkg" only %} +
+ {# Panel: System Install #} +
+ {% include "v3/includes/_install_card_tab_content_body.html" with options=install_card_system_install prefix="sys" only %}
- {# Single tab content – content updates reactively with active tab #} - {% include "v3/includes/_tab_content.html" with content_id="install-panel" content_tab_a_id="install-tab-pkg" content_tab_b_id="install-tab-sys" content_tab_a_value="package-managers" tab_content_template="v3/includes/_install_card_tab_content_body.html" only %}
diff --git a/templates/v3/includes/_install_card_tab_content_body.html b/templates/v3/includes/_install_card_tab_content_body.html index be5d02d92..a655869cd 100644 --- a/templates/v3/includes/_install_card_tab_content_body.html +++ b/templates/v3/includes/_install_card_tab_content_body.html @@ -1,51 +1,55 @@ {% comment %} V3 Install Card — Tab Content Body - The tab content body for the install card: a code block with an embedded dropdown. - Relies on the parent Alpine.js x-data scope from _install_card.html. + Pre-rendered code block with embedded dropdown for one tab. + Uses CSS radio inputs for option selection (no JS required). CSS: install-card.css, code-block.css, forms.css (.dropdown) + + Variables: + options — list of { label, value, command } dicts + prefix — radio name prefix (e.g. "pkg", "sys") {% endcomment %} -{# Code block with dropdown nested inside #} + +{# Option radio inputs — must be siblings of .code-block for CSS :checked selectors #} +{% for opt in options %} + +{% endfor %} + +{# Code block with commands + dropdown #}
- {# Code #} -
- {# Inline Dropdown to share the parent x-data scope #} - +
diff --git a/templates/v3/includes/_tab.html b/templates/v3/includes/_tab.html deleted file mode 100644 index 36f8c3f92..000000000 --- a/templates/v3/includes/_tab.html +++ /dev/null @@ -1,28 +0,0 @@ -{% comment %} - V3 Tab Button - Reusable tab button for use inside a role="tablist" container. - Requires a parent Alpine.js x-data scope with switchTab(value) method and activeTab property. - CSS: tab.css (.tab__trigger) - - Variables: - tab_label — visible button text for the tab trigger - tab_value — Alpine.js tab identifier (e.g. 'package-managers') - tab_id — HTML id attribute (e.g. 'install-tab-pkg') - tab_controls — aria-controls target panel id - tab_next — tab value to switch to on right arrow key - tab_prev — tab value to switch to on left arrow key - - Usage: - {% include "v3/includes/_tab.html" with tab_label="Package Managers" tab_value="package-managers" tab_id="install-tab-pkg" tab_controls="install-panel" tab_next="system-install" tab_prev="system-install" only %} -{% endcomment %} - diff --git a/templates/v3/includes/_tab_content.html b/templates/v3/includes/_tab_content.html deleted file mode 100644 index 6217acb12..000000000 --- a/templates/v3/includes/_tab_content.html +++ /dev/null @@ -1,21 +0,0 @@ -{% comment %} - Tab Content - Reusable tab content wrapper. Pass a template path via tab_content_template - to render any component as the content body. - Requires a parent Alpine.js x-data scope with activeTab property. - CSS: install-card.css (.install-card__tab-content) - - Variables: - content_id — HTML id attribute (e.g. 'install-panel') - content_tab_a_id — id of the first tab (for aria-labelledby) - content_tab_b_id — id of the second tab (for aria-labelledby) - content_tab_a_value — Alpine.js value of the first tab (used in aria-labelledby ternary) - tab_content_template — template path to {% include %} as the content body - - Usage: - {% include "v3/includes/_tab_content.html" with content_id="install-panel" content_tab_a_id="install-tab-pkg" content_tab_b_id="install-tab-sys" content_tab_a_value="package-managers" tab_content_template="v3/includes/_install_card_tab_content_body.html" only %} -{% endcomment %} -
{% include tab_content_template %}
From c765ab317f9df369a8311d240d61250db964926e Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Fri, 13 Mar 2026 17:00:14 -0700 Subject: [PATCH 08/24] Add default focus outline to tab trigger --- static/css/v3/install-card.css | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/static/css/v3/install-card.css b/static/css/v3/install-card.css index 0fb525033..46a26ccb5 100644 --- a/static/css/v3/install-card.css +++ b/static/css/v3/install-card.css @@ -24,6 +24,28 @@ border: 0; } +.install-card__radio:focus-visible + ~ .install-card__body + .tab__list + [for="install-tab-pkg"], +.install-card__radio:focus-visible + ~ .install-card__body + .tab__list + [for="install-tab-sys"] { + outline: none; +} + +#install-tab-pkg:focus-visible + ~ .install-card__body + .tab__list + [for="install-tab-pkg"], +#install-tab-sys:focus-visible + ~ .install-card__body + .tab__list + [for="install-tab-sys"] { + outline: medium auto -webkit-focus-ring-color; +} + /* ── Heading ─────────────────────────────────────────────── */ .install-card__title { margin-bottom: 0; From 1c82d826750b266fd0944365595dccc9cbb4ecb3 Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Mon, 16 Mar 2026 15:03:45 -0700 Subject: [PATCH 09/24] Add background color to secondary button, update dark text-on-accent token --- static/css/v3/buttons.css | 1 + static/css/v3/themes.css | 18 +++++++++--------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/static/css/v3/buttons.css b/static/css/v3/buttons.css index a23537dee..c8af0d353 100644 --- a/static/css/v3/buttons.css +++ b/static/css/v3/buttons.css @@ -63,6 +63,7 @@ } .btn-secondary { + background: var(--color-surface-weak); border-color: var(--color-stroke-strong); color: var(--color-text-primary); } diff --git a/static/css/v3/themes.css b/static/css/v3/themes.css index ac55e6653..79405b9e4 100644 --- a/static/css/v3/themes.css +++ b/static/css/v3/themes.css @@ -28,7 +28,7 @@ html.dark { /* 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; + --color-error-weak: #fdf2f217; /* Buttons */ --color-button-primary: var(--color-primary-grey-800); @@ -45,18 +45,18 @@ html.dark { --color-navigation-selected: var(--color-primary-grey-800); /* Stroke */ - --color-stroke-error: #D53F3F; + --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; + --color-stroke-mid: #f7f7f82b; + --color-stroke-strong: #f7f7f836; + --color-stroke-weak: #f7f7f81a; /* Surface */ - --color-surface-brand-accent-hovered: #FFA000D9; + --color-surface-brand-accent-hovered: #ffa000d9; --color-surface-error-strong: var(--color-error-mid); - --color-surface-error-weak: #FDF2F217; + --color-surface-error-weak: #fdf2f217; --color-surface-mid: var(--color-primary-grey-900); - --color-surface-modal: #0508164D; + --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); @@ -83,7 +83,7 @@ html.dark { /* Tag */ --color-tag-fill: var(--color-primary-grey-900); --color-tag-fill-hover: var(--color-primary-grey-850); - --color-tag-stroke: #FFFFFF1A; + --color-tag-stroke: #ffffff1a; /* Category tag dark: single source for neutral and colored tag surfaces (no raw primitives in category-tags.css) */ --color-tag-neutral-bg: var(--color-primary-grey-900); --color-tag-neutral-bg-hover: var(--color-primary-grey-800); From e4d89039d3a073d6bfd4c32b0fd2c857abf78994 Mon Sep 17 00:00:00 2001 From: "Teodoro B. Mendes" Date: Thu, 12 Mar 2026 13:53:49 -0300 Subject: [PATCH 10/24] feat: highlight missing/wrong variables --- config/settings.py | 5 +++++ core/template_utils.py | 14 ++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 core/template_utils.py diff --git a/config/settings.py b/config/settings.py index 9c5ca3df2..ca68efb29 100755 --- a/config/settings.py +++ b/config/settings.py @@ -9,6 +9,7 @@ from corsheaders.defaults import default_headers from django.core.exceptions import ImproperlyConfigured from pythonjsonlogger import jsonlogger +from core.template_utils import InvalidTemplateVariable env = environs.Env() @@ -170,6 +171,10 @@ str(BASE_DIR.joinpath("templates")), ], "OPTIONS": { + "debug": DEBUG, + "string_if_invalid": ( + InvalidTemplateVariable("INVALID_VARIABLE_%s") if DEBUG else "" + ), "context_processors": [ # Django Admin Env Notice "django_admin_env_notice.context_processors.from_settings", diff --git a/core/template_utils.py b/core/template_utils.py new file mode 100644 index 000000000..6348cc05a --- /dev/null +++ b/core/template_utils.py @@ -0,0 +1,14 @@ +class InvalidTemplateVariable(str): + def __mod__(self, other): + from django.conf import settings + + if settings.DEBUG: + # Revert from crashing to providing a visible marker + return f"INVALID_VARIABLE_{other}" + return "" + + def __repr__(self): + return f"InvalidTemplateVariable('{self}')" + + def __str__(self): + return f"Invalid template variable: {self}" From fb810e97bf66d91f1d84629dbe089d804fa65cc4 Mon Sep 17 00:00:00 2001 From: "Teodoro B. Mendes" Date: Fri, 13 Mar 2026 10:02:59 -0300 Subject: [PATCH 11/24] feat: add dialog component --- static/css/v3/components.css | 1 + static/css/v3/dialog.css | 161 ++++++++++++++++++ .../v3/examples/_v3_example_section.html | 15 ++ templates/v3/includes/_dialog.html | 52 ++++++ 4 files changed, 229 insertions(+) create mode 100644 static/css/v3/dialog.css create mode 100644 templates/v3/includes/_dialog.html diff --git a/static/css/v3/components.css b/static/css/v3/components.css index 5d8974b8b..3f7b15e71 100644 --- a/static/css/v3/components.css +++ b/static/css/v3/components.css @@ -30,3 +30,4 @@ @import "./banner.css"; @import "./animations.css"; @import "./account-connections.css"; +@import "./dialog.css"; diff --git a/static/css/v3/dialog.css b/static/css/v3/dialog.css new file mode 100644 index 000000000..dd7a33cef --- /dev/null +++ b/static/css/v3/dialog.css @@ -0,0 +1,161 @@ +/** + * Dialog Component + * + * A modal dialog triggered by user action, requiring an interstitial step. + * Dialog Container (.dialog-modal__container) is the inner content area and + * is NOT intended as a standalone module – it only exists inside Dialog Modal. + * + * Open/close is CSS-only via the :target pseudo-class. No JavaScript required. + * Trigger: + * Close: (close button and backdrop link inside the dialog) + * + * Structure: + * .dialog-modal – full-screen overlay (hidden by default) + * .dialog-modal__backdrop – invisible close link covering the overlay area + * .dialog-modal__container – centred content card + * .dialog-modal__header – title row + close link + * .dialog-modal__title – heading text + * .dialog-modal__close – dismiss link (×) + * .dialog-modal__description – optional body text + * .dialog-modal__buttons – primary / secondary action row + * + * Light/dark: all colour tokens are already theme-aware via semantics.css + themes.css. + */ + +/* ============================================ + DIALOG MODAL (full-screen overlay) + ============================================ */ +.dialog-modal { + display: none; + position: fixed; + inset: 0; + z-index: 10000; + background-color: var(--color-surface-modal, rgba(5, 8, 22, 0.7)); + align-items: center; + justify-content: center; +} + +.dialog-modal:target { + display: flex !important; +} + +/* ============================================ + BACKDROP CLOSE LINK + Fills the overlay; clicking outside the container closes the dialog. + ============================================ */ +.dialog-modal__backdrop { + position: absolute; + inset: 0; + cursor: default; + z-index: 0; +} + +/* ============================================ + DIALOG CONTAINER (inner content card) + ============================================ */ +.dialog-modal__container { + position: relative; + z-index: 1; + display: flex; + flex-direction: column; + gap: var(--space-large, 16px); + width: 695px; + max-width: calc(100vw - 2 * var(--space-large, 16px)); + background-color: var(--color-surface-page, #f7fdfe); + border-radius: var(--border-radius-xl, 12px); + overflow: hidden; +} + +/* ============================================ + HEADER (title + close link on the same row) + ============================================ */ +.dialog-modal__header { + display: flex !important; + flex-direction: row !important; + align-items: flex-start; + gap: var(--space-default, 8px); + padding: var(--space-large, 16px); + width: 100%; + box-sizing: border-box; + border-bottom: 1px solid var(--color-stroke-weak, rgba(5, 8, 22, 0.1)); +} + +.dialog-modal__title { + flex: 1 0 0 !important; + min-width: 0; + margin: 0 !important; + padding: 0 !important; + font-family: var(--font-display); + font-size: var(--font-size-large, 24px); + font-weight: var(--font-weight-medium, 500); + line-height: var(--line-height-tight, 1); + letter-spacing: var(--letter-spacing-display-regular, -0.02em); + color: var(--color-text-primary, #050816); + display: block !important; +} + +/* ============================================ + CLOSE LINK (×) + ============================================ */ +.dialog-modal__close { + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + color: var(--color-icon-primary, #050816); + text-decoration: none; +} + +.dialog-modal__close:hover { + color: var(--color-icon-secondary, #71737b); +} + +.dialog-modal__close svg { + display: block; + width: 24px; + height: 24px; +} + +/* ============================================ + DESCRIPTION (optional) + ============================================ */ +.dialog-modal__description { + padding: 0 var(--space-large, 16px); + width: 100%; + box-sizing: border-box; +} + +.dialog-modal__description p { + margin: 0; + font-family: var(--font-sans); + font-size: var(--font-size-base, 16px); + font-weight: var(--font-weight-regular, 400); + line-height: var(--line-height-default, 1.2); + letter-spacing: var(--letter-spacing-tight, -0.01em); + color: var(--color-text-secondary, #585a64); +} + +/* ============================================ + BUTTONS ROW + ============================================ */ +.dialog-modal__buttons { + display: flex; + flex-direction: row; + gap: var(--space-default, 8px); + padding: 0 var(--space-large, 16px) var(--space-large, 16px); + width: 100%; + box-sizing: border-box; +} + +.dialog-modal__buttons .btn { + flex: 1 0 0; + min-width: 128px; + width: auto; + text-decoration: none; +} + +.dialog-modal__buttons a.btn { + text-decoration: none; +} diff --git a/templates/v3/examples/_v3_example_section.html b/templates/v3/examples/_v3_example_section.html index 827528c36..46780ca37 100644 --- a/templates/v3/examples/_v3_example_section.html +++ b/templates/v3/examples/_v3_example_section.html @@ -440,4 +440,19 @@

Code blocks

{% include "v3/includes/_code_blocks_story.html" with code_standalone_1=code_demo_beast code_standalone_2=code_demo_beast code_card_1=code_demo_hello code_card_2=code_demo_beast code_card_3=code_demo_install %}
+ +
+

Dialog

+
+

Dialog Modal with description

+ Open dialog + +

Dialog Modal without description

+ Open dialog +
+
+ +{% comment %}Dialogs placed outside section to avoid position:fixed containment issues{% endcomment %} +{% include "v3/includes/_dialog.html" with dialog_id="demo-dialog-with-desc" title="Title of Dialog" description="Description that can go inside of Dialog" primary_label="Confirm" secondary_label="Cancel" %} +{% include "v3/includes/_dialog.html" with dialog_id="demo-dialog-no-desc" title="Title of Dialog" primary_label="Confirm" secondary_label="Cancel" %} diff --git a/templates/v3/includes/_dialog.html b/templates/v3/includes/_dialog.html new file mode 100644 index 000000000..79af83731 --- /dev/null +++ b/templates/v3/includes/_dialog.html @@ -0,0 +1,52 @@ +{% comment %} + Dialog Modal – full-screen overlay with an internal Dialog Container. + The Dialog Container is NOT available as a standalone include. + + Open/close is CSS-only via the :target pseudo-class (no JavaScript required). + Trigger: + + Variables: + dialog_id (required): Unique ID for this dialog instance. + title (required): Title displayed in the dialog header. + description (optional): Descriptive text below the title. + primary_label (required): Label for the primary action button. + secondary_label (required): Label for the secondary action button. + primary_style (optional): Button style for primary action. Default "secondary-grey". + secondary_style (optional): Button style for secondary action. Default "primary". + primary_url (optional): URL for primary button. Defaults to "" (renders as