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 diff --git a/core/views.py b/core/views.py index 686c08b0b..9bea576e8 100644 --- a/core/views.py +++ b/core/views.py @@ -1111,10 +1111,39 @@ 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"] = INSTALL_CARD_PKG_MANAGERS + context["install_card_system_install"] = INSTALL_CARD_SYSTEM_INSTALL context["popular_terms"] = [ {"label": "Networking"}, {"label": "Math"}, @@ -1290,6 +1319,66 @@ 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.", + } + + 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/flake.nix b/flake.nix index 64c5cb891..fb8e0b33a 100644 --- a/flake.nix +++ b/flake.nix @@ -67,6 +67,7 @@ python313.pkgs.black python313.pkgs.isort python313.pkgs.pip-tools + git ]; # Host system installation workflow goes into the bootstrap justfile target. # Project specific installation and execution workflow should go here. 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/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/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/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 fe53a2a7f..3f7b15e71 100644 --- a/static/css/v3/components.css +++ b/static/css/v3/components.css @@ -1,27 +1,33 @@ -@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 "./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"; +@import "./dialog.css"; diff --git a/static/css/v3/dialog.css b/static/css/v3/dialog.css new file mode 100644 index 000000000..26623967c --- /dev/null +++ b/static/css/v3/dialog.css @@ -0,0 +1,171 @@ +/** + * 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); + 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)); + background-color: var(--color-surface-page); + border-radius: var(--border-radius-xl); + overflow: hidden; +} + +/* Mobile: full-width dialog with minimal margins */ +@media (max-width: 767px) { + .dialog-modal__container { + width: 100%; + max-width: calc(100vw - 2 * var(--space-default)); + margin: 0 var(--space-default); + border-radius: var(--border-radius-l); + } +} + +/* ============================================ + 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); + padding: var(--space-large); + width: 100%; + box-sizing: border-box; + border-bottom: 1px solid var(--color-stroke-weak); +} + +.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); + font-weight: var(--font-weight-medium); + line-height: var(--line-height-tight); + letter-spacing: var(--letter-spacing-display-regular); + color: var(--color-text-primary); + 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); + text-decoration: none; +} + +.dialog-modal__close:hover { + color: var(--color-icon-secondary); +} + +.dialog-modal__close svg { + display: block; + width: 24px; + height: 24px; +} + +/* ============================================ + DESCRIPTION (optional) + ============================================ */ +.dialog-modal__description { + padding: 0 var(--space-large); + width: 100%; + box-sizing: border-box; +} + +.dialog-modal__description p { + margin: 0; + font-family: var(--font-sans); + font-size: var(--font-size-base); + font-weight: var(--font-weight-regular); + line-height: var(--line-height-default); + letter-spacing: var(--letter-spacing-tight); + color: var(--color-text-secondary); +} + +/* ============================================ + BUTTONS ROW + ============================================ */ +.dialog-modal__buttons { + display: flex; + flex-direction: row; + gap: var(--space-default); + padding: 0 var(--space-large) var(--space-large); + 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/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..46a26ccb5 --- /dev/null +++ b/static/css/v3/install-card.css @@ -0,0 +1,168 @@ +/* + 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 +*/ + +.install-card { + max-width: 458px; + 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; +} + +.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; +} + +/* ── 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); +} + +/* ── 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 { + color: var(--color-text-link-accent); + 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 { + min-width: 120px; + } +} diff --git a/static/css/v3/semantics.css b/static/css/v3/semantics.css index 6aa86f988..0f49b3c6a 100644 --- a/static/css/v3/semantics.css +++ b/static/css/v3/semantics.css @@ -17,11 +17,6 @@ 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); @@ -110,4 +105,17 @@ --color-text-reversed: var(--color-primary-white); --color-text-reversed-on-accent: var(--color-primary-white); --color-text-tertiary: var(--color-primary-grey-700); + + + + /* ============================================ + DEPRECATED + + Please, do not use these colors. They were originally assigned as "background" colors, but this + semantics doesn't exist in the Figma Foundations designs. + Use "surface" colors instead. + ============================================ */ + --color-bg-primary: var(--color-secondary-light-blue); + --color-bg-secondary: var(--color-primary-white); + --color-border: var(--color-primary-grey-300); } 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/static/css/v3/themes.css b/static/css/v3/themes.css index 07b24abad..7abd100f8 100644 --- a/static/css/v3/themes.css +++ b/static/css/v3/themes.css @@ -18,17 +18,10 @@ 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; + --color-error-weak: #fdf2f217; /* Buttons */ --color-button-primary: var(--color-primary-grey-800); @@ -45,18 +38,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 +76,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); @@ -112,7 +105,20 @@ 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); + --color-text-primary: var(--color-primary-white); + --color-text-secondary: var(--color-primary-grey-400); + + /* ============================================ + DEPRECATED + + Please, do not use these colors. They were originally assigned as "background" colors, but this + semantics doesn't exist in the Figma Foundations designs. + Use "surface" colors instead. + ============================================ */ + --color-bg-primary: var(--color-primary-black); + --color-bg-secondary: var(--color-primary-grey-950); + --color-border: var(--color-primary-grey-800); } 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/static/js/dialog.js b/static/js/dialog.js new file mode 100644 index 000000000..877ad1ab0 --- /dev/null +++ b/static/js/dialog.js @@ -0,0 +1,84 @@ +/** + * Dialog – ESC key support and focus trap for CSS-only dialogs. + * + * The dialog uses CSS :target for open/close (no JS required for basic functionality). + * This script adds progressive enhancement: + * - ESC key closes any open dialog + * - Focus trap: keeps keyboard focus within the dialog + */ +(function () { + 'use strict'; + + function getFocusableElements(container) { + const focusableSelectors = [ + 'a[href]', + 'button:not([disabled])', + 'textarea:not([disabled])', + 'input:not([disabled])', + 'select:not([disabled])', + '[tabindex]:not([tabindex="-1"])' + ].join(', '); + + return Array.from(container.querySelectorAll(focusableSelectors)); + } + + function trapFocus(e, dialog) { + if (e.key !== 'Tab') return; + + const focusableElements = getFocusableElements(dialog); + if (focusableElements.length === 0) return; + + const firstElement = focusableElements[0]; + const lastElement = focusableElements[focusableElements.length - 1]; + const activeElement = document.activeElement; + + const currentIndex = focusableElements.indexOf(activeElement); + + const isForbiddenElement = currentIndex === -1 || !dialog.contains(activeElement); + + if (e.shiftKey && (activeElement === firstElement || isForbiddenElement)) { + e.preventDefault(); + lastElement.focus(); + } else if (!e.shiftKey && (activeElement === lastElement || isForbiddenElement)) { + e.preventDefault(); + firstElement.focus(); + } + } + + function setInitialFocus(dialog) { + const focusableElements = getFocusableElements(dialog); + if (focusableElements.length > 0) { + focusableElements[0].focus(); + } + } + + // Dialogs are opened via hash change in the URL. This listens for those specific events. + window.addEventListener('hashchange', function () { + const openDialogWrapper = document.querySelector('.dialog-modal:target'); + if (openDialogWrapper) { + const dialogContainer = openDialogWrapper.querySelector('[role="dialog"]'); + if (dialogContainer) { + setInitialFocus(dialogContainer); + } + } + }); + + document.addEventListener('keydown', function (e) { + const openDialogWrapper = document.querySelector('.dialog-modal:target'); + + if (!openDialogWrapper) return; + + const dialogContainer = openDialogWrapper.querySelector('[role="dialog"]'); + if (!dialogContainer) return; + + // ESC key closes the dialog + if (e.key === 'Escape' || e.key === 'Esc') { + e.preventDefault(); + window.location.hash = '_'; + return; + } + + // Tab key traps focus within the dialog + trapFocus(e, dialogContainer); + }); +})(); 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 48377cc31..c3e4e1d9e 100644 --- a/templates/base.html +++ b/templates/base.html @@ -48,6 +48,7 @@ + {% endflag %} {% block extra_head %} @@ -399,9 +400,11 @@ {% endblock %} - {% flag "v3" %} - {% include "v3/examples/_v3_example_section.html" %} - {% endflag %} + {% if request.resolver_match.url_name == 'v3-demo-components' %} + {% flag "v3" %} + {% include "v3/examples/_v3_example_section.html" %} + {% endflag %} + {% endif %} {% if not hide_footer %} {% flag "v3" %} @@ -414,6 +417,7 @@ {% flag "v3" %} + {% endflag %} {% block footer_js %}{% endblock %} diff --git a/templates/includes/icon.html b/templates/includes/icon.html index 5a5a4815b..1381daed2 100644 --- a/templates/includes/icon.html +++ b/templates/includes/icon.html @@ -5,41 +5,68 @@ 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 %} -

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 %}
@@ -255,8 +255,19 @@

Form inputs

+
-

Basic Card

+

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

Card with two buttons

@@ -276,14 +287,24 @@

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

@@ -364,7 +385,7 @@

-

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

+

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

@@ -405,10 +426,29 @@

Content detail card

+ +
+

Install Card

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

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 +
+
+ +{% 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_url="#_" secondary_url="#_" primary_label="Button" secondary_label="Button" %} 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 %} + + 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 %} + + diff --git a/templates/v3/includes/_dialog.html b/templates/v3/includes/_dialog.html new file mode 100644 index 000000000..d839b35c0 --- /dev/null +++ b/templates/v3/includes/_dialog.html @@ -0,0 +1,53 @@ +{% 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
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..3391595db --- /dev/null +++ b/templates/v3/includes/_install_card.html @@ -0,0 +1,78 @@ +{% comment %} + V3 Install Card + Interactive card with tabbed installation methods and a dynamic command display. + 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 — 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) #} + +
+ {# Tab radio inputs — must be direct children of section for sibling selectors #} + + +
+

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

+
+
+ {# Tab list #} +
+ + +
+ {# 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 %} +
+
+
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..a655869cd --- /dev/null +++ b/templates/v3/includes/_install_card_tab_content_body.html @@ -0,0 +1,55 @@ +{% comment %} + V3 Install Card — Tab Content Body + 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 %} + +{# Option radio inputs — must be siblings of .code-block for CSS :checked selectors #} +{% for opt in options %} + +{% endfor %} + +{# Code block with commands + dropdown #} +
+ {# Pre-rendered commands — CSS shows only the one matching the checked radio #} + {% for opt in options %} +
{{ opt.command }}
+ {% endfor %} + + {# Dropdown #} + +