├── .github └── workflows │ └── build.yml ├── .gitignore ├── .npmrc.example ├── .nvmrc ├── README.md ├── assets ├── images │ ├── close.svg │ └── no-thumbnail.png ├── js │ ├── app.js │ └── modules │ │ ├── back-top.js │ │ ├── bootstrap.js │ │ ├── fontawesome.js │ │ ├── jquery-ui.js │ │ ├── main-menu.js │ │ └── mobile-menu.js └── scss │ ├── app.scss │ ├── bootstrap.scss │ ├── ckeditor.scss │ ├── content │ ├── elements.scss │ ├── fields.scss │ ├── spacing.scss │ └── wysiwyg.scss │ ├── drupal │ ├── breadcrumbs.scss │ ├── dropbutton.scss │ ├── drupal.scss │ ├── field-group.scss │ ├── forms.scss │ ├── layout-builder.scss │ ├── layout-paragraphs.scss │ ├── media-library-errors.scss │ ├── media-library.scss │ ├── messages.scss │ ├── table-draggable.scss │ ├── ui-autocomplete.scss │ └── ui-dialog.scss │ ├── layout │ ├── footer.scss │ ├── header.scss │ └── layout--twocol.scss │ ├── navigation │ ├── back-top.scss │ ├── mobile-button.scss │ ├── mobile-menu.scss │ └── primary-menu.scss │ └── variables.scss ├── config └── install │ ├── block.block.dvb_account_menu.yml │ ├── block.block.dvb_account_menu_mobile.yml │ ├── block.block.dvb_breadcrumbs.yml │ ├── block.block.dvb_content.yml │ ├── block.block.dvb_help.yml │ ├── block.block.dvb_main_menu.yml │ ├── block.block.dvb_main_menu_mobile.yml │ ├── block.block.dvb_messages.yml │ ├── block.block.dvb_page_title.yml │ ├── block.block.dvb_powered.yml │ ├── block.block.dvb_primary_admin_actions.yml │ ├── block.block.dvb_primary_local_tasks.yml │ ├── block.block.dvb_secondary_local_tasks.yml │ ├── block.block.dvb_site_branding.yml │ └── dvb.settings.yml ├── dist ├── .vite │ └── manifest.json └── assets │ ├── app-C4TaqamP.js │ ├── app.CK3aqZXw.css │ ├── back-top-BbUvcPYO.js │ ├── back-top.DrNfoJkY.css │ ├── bootstrap-DAuYFD14.js │ ├── bootstrap.DSIkgM62.css │ ├── ckeditor.css │ ├── close.C_jwcm6v.svg │ ├── fontawesome-DZjOfxZp.js │ ├── jquery-ui-BWCvzDkq.js │ ├── main-menu-BVBh2ugi.js │ ├── mobile-menu-DMP3NAtO.js │ ├── mobile-menu.C_HpnVi1.css │ └── no-thumbnail.DCFMKjjA.png ├── dvb.breakpoints.yml ├── dvb.info.yml ├── dvb.libraries.yml ├── dvb.theme ├── includes ├── blocks.theme ├── fields.theme ├── forms.theme ├── nodes.theme ├── settings.theme └── views.theme ├── logo.svg ├── package-lock.json ├── package.json ├── screenshot.png ├── src ├── DvbPreRender.php └── Vite │ ├── ViteAsset.php │ ├── ViteLibrary.php │ ├── ViteManifest.php │ ├── ViteMode.php │ └── ViteServer.php ├── templates ├── block │ ├── block--local-actions-block.html.twig │ ├── block--local-tasks-block.html.twig │ ├── block--system-branding-block.html.twig │ ├── block--system-breadcrumb-block.html.twig │ ├── block--system-menu-block.html.twig │ └── block.html.twig ├── content-edit │ └── filter-guidelines.html.twig ├── content │ ├── comment.html.twig │ ├── node.html.twig │ └── page-title.html.twig ├── dataset │ ├── item-list.html.twig │ └── table.html.twig ├── field-group │ ├── field-group-accordion-item.html.twig │ ├── field-group-accordion.html.twig │ ├── field-group-html-element--blockquote.html.twig │ └── horizontal-tabs.html.twig ├── field │ ├── field--comment.html.twig │ ├── field--field-description.html.twig │ ├── field--field-title.html.twig │ ├── field--media--field-media-oembed-video.html.twig │ ├── field--media--field-media-video-file.html.twig │ ├── field--paragraph--field-citation--quote.html.twig │ ├── field--paragraph--field-link--quote.html.twig │ ├── field--text-long.html.twig │ ├── field--text-with-summary.html.twig │ ├── field--text.html.twig │ ├── field.html.twig │ └── image.html.twig ├── form │ ├── container--actions.html.twig │ ├── container--media-library-widget-selection.html.twig │ ├── container--text-format-filter-guidelines.html.twig │ ├── container--text-format-filter-help.html.twig │ ├── container--text-format-filter-wrapper.html.twig │ ├── container.html.twig │ ├── datetime-form.html.twig │ ├── datetime-wrapper.html.twig │ ├── details.html.twig │ ├── field-multiple-value-form.html.twig │ ├── fieldset.html.twig │ ├── form-element-label--type--checkbox.html.twig │ ├── form-element-label--type--item.html.twig │ ├── form-element-label--type--radio.html.twig │ ├── form-element-label.html.twig │ ├── form-element.html.twig │ ├── form.html.twig │ ├── input--checkbox.html.twig │ ├── input--color.html.twig │ ├── input--radio.html.twig │ ├── input--submit.html.twig │ ├── input.html.twig │ ├── select.html.twig │ └── textarea.html.twig ├── layout │ ├── maintenance-page.html.twig │ ├── page.html.twig │ └── region.html.twig ├── media-library │ ├── feed-icon.html.twig │ └── media--media-library.html.twig ├── misc │ ├── progress-bar.html.twig │ └── status-messages.html.twig ├── navigation │ ├── breadcrumb.html.twig │ ├── links--comment.html.twig │ ├── links--dropbutton.html.twig │ ├── links--media-library-menu.html.twig │ ├── links--node.html.twig │ ├── menu--region--mobile-menu.html.twig │ ├── menu--region--primary-menu.html.twig │ ├── menu--region--secondary-menu.html.twig │ ├── menu-local-task.html.twig │ ├── menu-local-tasks.html.twig │ └── pager.html.twig ├── regions │ ├── region--alerts.html.twig │ ├── region--breadcrumb.html.twig │ ├── region--content.html.twig │ ├── region--help.html.twig │ ├── region--mobile-menu.html.twig │ ├── region--primary-menu.html.twig │ └── region.html.twig ├── user │ └── user.html.twig ├── views │ ├── views-exposed-form.html.twig │ ├── views-mini-pager.html.twig │ ├── views-view-table.html.twig │ ├── views-view-unformatted--media-library.html.twig │ ├── views-view-unformatted.html.twig │ └── views-view.html.twig └── webform │ ├── webform-submission-data.html.twig │ └── webform.html.twig └── vite.config.js /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | release: 5 | types: [created] 6 | push: 7 | branches: [main] 8 | pull_request: 9 | branches: [main] 10 | 11 | env: 12 | PHP_VERSION: 8.2 13 | NODE_VERSION: 20.x 14 | TERM: xterm 15 | 16 | jobs: 17 | phpcs: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout Repository 21 | uses: 'actions/checkout@v4' 22 | with: 23 | fetch-depth: 0 24 | 25 | - name: Set up PHP 26 | uses: 'shivammathur/setup-php@v2' 27 | with: 28 | php-version: '${{ env.PHP_VERSION }}' 29 | tools: 'phpcs, composer' 30 | 31 | - name: Lint 32 | run: | 33 | composer global config --no-plugins allow-plugins.dealerdirect/phpcodesniffer-composer-installer true 34 | composer global require --ignore-platform-reqs drupal/coder 35 | phpcs --standard=Drupal,DrupalPractice -n --extensions="php,module,inc,install,test,profile,theme,yml" . 36 | 37 | build: 38 | runs-on: ubuntu-latest 39 | 40 | needs: [phpcs] 41 | 42 | steps: 43 | - uses: actions/checkout@v4 44 | with: 45 | fetch-depth: 0 46 | 47 | - name: Cleaning up dist 48 | run: | 49 | rm -rf dist 50 | ls -la 51 | 52 | - name: Install Node 53 | uses: actions/setup-node@v4 54 | with: 55 | node-version: ${{ env.NODE_VERSION }} 56 | 57 | - run: npm ci 58 | 59 | - run: npm run build --if-present --verbose 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | # Node installs 11 | node_modules 12 | *.local 13 | 14 | # Editor directories and files 15 | .vscode/* 16 | !.vscode/extensions.json 17 | .idea 18 | .DS_Store 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | 25 | # Credentials 26 | .npmrc 27 | -------------------------------------------------------------------------------- /.npmrc.example: -------------------------------------------------------------------------------- 1 | @fortawesome:registry=https://npm.fontawesome.com/ 2 | //npm.fontawesome.com/:_authToken=AAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAA 3 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Drupal Vite Bootstrap theme 2 | 3 | ![NPM build](https://github.com/almunnings/drupal-vite-bootstrap/actions/workflows/build.yml/badge.svg?branch=main) 4 | 5 | It's intended you copy this repo, and alter this template as much as you want. 6 | 7 | > If you plan to use Vite HMR via a Lando domain, you need to trust your [Lando SSL certificates](https://docs.lando.dev/core/v3/security.html#trusting-the-ca). 8 | 9 | ## Install and use 10 | 11 | ```bash 12 | npm ci 13 | ``` 14 | 15 | ### Local development (live reloading) 16 | 17 | Enable developer mode, visit `/admin/appearance/settings/dvb` and check the enable option. 18 | 19 | ```bash 20 | npm run dev 21 | ``` 22 | 23 | ### Build production (dist) 24 | 25 | ```bash 26 | npm run build 27 | ``` 28 | 29 | ## Vite entry points 30 | 31 | - Assets are added to Vite via `dvb.libraries.yml` 32 | - The `vite: true` key is used enable Vite remapping 33 | - Any js/css path is converted into an entry point 34 | 35 | Vite assets within the yml should map to their location within the `assets/` dir. 36 | 37 | ```yml 38 | # Example dvb.libraries.yml 39 | 40 | app: 41 | vite: true 42 | version: 1.2.3 43 | dependencies: 44 | - core/drupal 45 | - core/drupalSettings 46 | css: 47 | theme: 48 | assets/scss/bootstrap.scss: { minified: true, preprocess: false } 49 | assets/scss/app.scss: { minified: true, preprocess: false } 50 | js: 51 | assets/js/app.js: { minified: true, preprocess: false, attributes: { type: 'module' } } 52 | ``` 53 | 54 | ## Lando config 55 | 56 | > If you plan to use a HMR theme via the Lando proxy, you need to trust your [Lando SSL certificates](https://docs.lando.dev/core/v3/security.html#trusting-the-ca). 57 | 58 | ```yml 59 | # Example .lando.yml 60 | 61 | services: 62 | node: 63 | type: node:20 64 | ssl: true 65 | sslExpose: false 66 | port: 3000 67 | scanner: false 68 | 69 | proxy: 70 | node: 71 | - node-drupal-boilerplate.lndo.site:3000 72 | 73 | tooling: 74 | npm: 75 | service: node 76 | ``` 77 | 78 | You can now run `lando npm run dev` within the theme directory to start the Vite server. 79 | 80 | ## DDEV config 81 | 82 | ```yml 83 | # Example .ddev/config.yaml 84 | 85 | nodejs_version: '20' 86 | 87 | web_extra_exposed_ports: 88 | - name: nodejs 89 | container_port: 3000 90 | http_port: 3330 91 | https_port: 3333 92 | ``` 93 | 94 | You can now run `ddev npm run dev` within the theme directory to start the Vite server. 95 | 96 | ## Get a Vite asset path manually 97 | 98 | You could create a utility to resolve the Vite asset path. 99 | 100 | ```php 101 | \Drupal::classResolver(ViteAsset::class)->find('assets/scss/whatever.scss', $absolute = TRUE); 102 | ``` 103 | -------------------------------------------------------------------------------- /assets/images/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/images/no-thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almunnings/drupal-vite-bootstrap/70c5190377e66c666a14139f41b0a53963491e7d/assets/images/no-thumbnail.png -------------------------------------------------------------------------------- /assets/js/app.js: -------------------------------------------------------------------------------- 1 | // Global app JS 2 | // Extracted CSS will be render blocking in the . 3 | 4 | import('./modules/fontawesome').then(({ default: Module }) => { 5 | new Module(); 6 | }); 7 | 8 | import('./modules/main-menu').then(({ default: Module }) => { 9 | new Module(); 10 | }); 11 | 12 | import('./modules/bootstrap').then(({ default: Module }) => { 13 | new Module(); 14 | }); 15 | 16 | import('./modules/mobile-menu').then(({ default: Module }) => { 17 | new Module(); 18 | }); 19 | 20 | import('./modules/back-top').then(({ default: Module }) => { 21 | new Module(); 22 | }); 23 | 24 | import('./modules/jquery-ui').then(({ default: Module }) => { 25 | new Module(); 26 | }); 27 | -------------------------------------------------------------------------------- /assets/js/modules/back-top.js: -------------------------------------------------------------------------------- 1 | /** 2 | * DVB back-top.js 3 | * 4 | * Adds a floating "back to top" element. 5 | */ 6 | 7 | import '../../scss/navigation/back-top.scss'; 8 | 9 | export default class Module { 10 | constructor() { 11 | this.footer = document.querySelector('.site-footer'); 12 | 13 | if (this.footer) { 14 | this.inject(); 15 | this.toggle(); 16 | window.addEventListener('scroll', this.toggle, { passive: true }); 17 | } 18 | } 19 | 20 | inject() { 21 | const html = ` 22 |
23 | 24 | 25 | 26 |
27 | `; 28 | 29 | this.footer.insertAdjacentHTML('beforebegin', html); 30 | 31 | if (Drupal.behaviors.fa) { 32 | Drupal.behaviors.fa.attach(this.footer.parentNode); 33 | } 34 | } 35 | 36 | toggle() { 37 | const y = window.scrollY; 38 | const button = document.querySelector('.back-to-top'); 39 | const offset = document.querySelector('.site-footer').offsetTop - window.innerHeight + 32; 40 | 41 | button.classList.toggle('show', y >= 200 || (y >= offset && offset > 0)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /assets/js/modules/bootstrap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * DVB bootstrap.js 3 | * 4 | * Adds bootstrap js and functionality. 5 | */ 6 | 7 | import * as bootstrap from 'bootstrap'; 8 | 9 | export default class Module { 10 | constructor() { 11 | Drupal.behaviors.iconBootstrap = { 12 | attach: (context) => { 13 | const popovers = context.querySelectorAll('[data-bs-toggle="popover"]'); 14 | const alerts = context.querySelectorAll('.alert'); 15 | const offcanvas = context.querySelectorAll('.offcanvas'); 16 | 17 | [...popovers].forEach((element) => new bootstrap.Popover(element)); 18 | [...alerts].forEach((element) => new bootstrap.Alert(element)); 19 | [...offcanvas].forEach((element) => element.removeAttribute('hidden')); 20 | }, 21 | }; 22 | 23 | Drupal.behaviors.iconBootstrap.attach(document); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /assets/js/modules/fontawesome.js: -------------------------------------------------------------------------------- 1 | /** 2 | * DVB fontawesome.js 3 | * 4 | * https://fontawesome.com/docs/apis/javascript/import-icons#package-names 5 | */ 6 | 7 | import { library, dom } from '@fortawesome/fontawesome-svg-core'; 8 | 9 | // Font Awesome Icons 10 | import { 11 | faChevronDown, 12 | faChevronUp, 13 | faChevronRight, 14 | faChevronLeft, 15 | faCircleNotch, 16 | faBell, 17 | faSquareRss, 18 | } from '@fortawesome/free-solid-svg-icons'; 19 | 20 | export default class Module { 21 | constructor() { 22 | // Add icons 23 | library.add(faChevronDown, faChevronUp, faChevronRight, faChevronLeft, faBell, faCircleNotch, faSquareRss); 24 | 25 | // Attach to a drupal behaviour to update on content changes. 26 | Drupal.behaviors.fa = { 27 | attach: (context) => { 28 | if (context.querySelector('i')) { 29 | dom.i2svg({ node: context }); 30 | } 31 | }, 32 | }; 33 | 34 | // Initialize this behaviour. 35 | Drupal.behaviors.fa.attach(document); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /assets/js/modules/jquery-ui.js: -------------------------------------------------------------------------------- 1 | /** 2 | * DVB jquery-ui.js 3 | * 4 | * Fix jquery UI issue with bootstrap. 5 | */ 6 | 7 | import jQuery from 'jquery'; 8 | 9 | export default class Module { 10 | constructor() { 11 | const $ = jQuery; 12 | 13 | if (!$.ui?.dialog) { 14 | return; 15 | } 16 | 17 | $.widget('ui.dialog', $.ui.dialog, { 18 | open: function () { 19 | const close = ` 20 | 21 | 22 | Close 23 | `; 24 | 25 | const classes = ['ui-button', 'ui-corner-all', 'ui-widget', 'ui-button-icon-only', 'ui-dialog-titlebar-close']; 26 | 27 | [...this.uiDialogTitlebarClose].forEach((button) => { 28 | button.innerHTML = close; 29 | button.classList.add(...classes); 30 | }); 31 | 32 | return this._super(); 33 | }, 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /assets/js/modules/main-menu.js: -------------------------------------------------------------------------------- 1 | /** 2 | * DVB main-menu.js 3 | */ 4 | 5 | import AccessibleSubmenu from 'accessible-submenu'; 6 | 7 | export default class Module { 8 | constructor() { 9 | this.menuElement = document.querySelector('.region-primary-menu .nav'); 10 | 11 | if (this.menuElement) { 12 | const items = this.menuElement.children; 13 | 14 | [...items].forEach((li) => { 15 | new AccessibleSubmenu(li, {}); 16 | 17 | this.bindTouch(li.querySelector(':scope > a')); 18 | }); 19 | 20 | this.bindTouchWindow(); 21 | } 22 | } 23 | 24 | /** 25 | * Close all other dropdowns on window touches outside of a double-tap element. 26 | */ 27 | bindTouchWindow() { 28 | document.addEventListener('touchstart', (event) => { 29 | const linkTouched = this.elementTouchedLink(event.target, 0); 30 | const dropdownTouched = this.elementTouchedDropdown(event.target, 0); 31 | const tapped = this.menuElement.querySelectorAll('[data-double-tap="true"]'); 32 | 33 | if (dropdownTouched) { 34 | return; 35 | } 36 | 37 | [...tapped].forEach((a) => { 38 | if (a !== linkTouched) { 39 | a.dataset.doubleTap = false; 40 | } 41 | }); 42 | }); 43 | } 44 | 45 | /** 46 | * Bind the touch events. 47 | * 48 | * @param {Element} link Link element on top level. 49 | */ 50 | bindTouch(link) { 51 | if (!link.parentNode.querySelectorAll('.js-submenu').length) { 52 | return; 53 | } 54 | 55 | link.addEventListener('touchend', (event) => { 56 | if (!this.doubleTapped(event)) { 57 | event.preventDefault(); 58 | } 59 | }); 60 | } 61 | 62 | /** 63 | * Check if its been double tapped and open accordingly. 64 | * 65 | * @param {Event} event Click event 66 | * 67 | * @return {boolean} Event target is double tapped. 68 | */ 69 | doubleTapped(event) { 70 | const target = event.currentTarget; 71 | 72 | if (target.dataset.doubleTap) { 73 | return true; 74 | } 75 | 76 | target.dataset.doubleTap = true; 77 | 78 | return false; 79 | } 80 | 81 | /** 82 | * Find the element target parents, so we don't close it on the touchstart. 83 | * 84 | * @param {Element} element A HTML element to check. 85 | * @param {integer} depth Recursive depth 86 | * 87 | * @return {mixed} Element or false. 88 | */ 89 | elementTouchedLink(element, depth) { 90 | if (depth < 5) { 91 | if (element?.dataset?.doubleTap) { 92 | return element; 93 | } 94 | 95 | if (element.parentNode) { 96 | return this.elementTouchedLink(element.parentNode, depth + 1); 97 | } 98 | } 99 | 100 | return false; 101 | } 102 | 103 | /** 104 | * Check if we tapped inside a dropdown. 105 | * 106 | * @param {Element} element A HTML element to check. 107 | * @param {integer} depth Recursive depth 108 | * 109 | * @return {mixed} Element or false. 110 | */ 111 | elementTouchedDropdown(element, depth) { 112 | if (depth < 10) { 113 | if (element.classList && element.classList.contains('js-submenu')) { 114 | return element; 115 | } 116 | 117 | if (element.parentNode) { 118 | return this.elementTouchedDropdown(element.parentNode, depth + 1); 119 | } 120 | } 121 | 122 | return false; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /assets/js/modules/mobile-menu.js: -------------------------------------------------------------------------------- 1 | /** 2 | * DVB mobile-menu.js 3 | */ 4 | 5 | import '../../scss/navigation/mobile-button.scss'; 6 | import '../../scss/navigation/mobile-menu.scss'; 7 | 8 | export default class Module { 9 | constructor() { 10 | // Mobile menu button that will toggle open and close. 11 | this.button = document.getElementById('mobile-nav-button'); 12 | this.bind(this.button); 13 | } 14 | 15 | // Open the menu and toggle the button class. 16 | bind(button) { 17 | const activeClass = 'is-active'; 18 | const menu = document.querySelector(button.dataset.bsTarget); 19 | 20 | menu.addEventListener('show.bs.offcanvas', () => { 21 | button.classList.add(activeClass); 22 | }); 23 | 24 | menu.addEventListener('hidden.bs.offcanvas', () => { 25 | button.classList.remove(activeClass); 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /assets/scss/app.scss: -------------------------------------------------------------------------------- 1 | // DVB app.scss 2 | 3 | // Drupal specific fixes that can't be templated. 4 | @import 'drupal/drupal'; 5 | 6 | // Regions 7 | @import 'layout/header'; 8 | @import 'layout/footer'; 9 | @import 'layout/layout--twocol'; 10 | 11 | // Navigation 12 | @import 'navigation/primary-menu'; 13 | 14 | // Text 15 | @import 'content/elements'; 16 | @import 'content/spacing'; 17 | @import 'content/fields'; 18 | 19 | // WYSIWYG 20 | @import 'ckeditor'; 21 | -------------------------------------------------------------------------------- /assets/scss/bootstrap.scss: -------------------------------------------------------------------------------- 1 | // scss-docs-start import-stack 2 | // Layout & components 3 | @import 'bootstrap/scss/root'; 4 | @import 'bootstrap/scss/reboot'; 5 | @import 'bootstrap/scss/type'; 6 | @import 'bootstrap/scss/images'; 7 | @import 'bootstrap/scss/containers'; 8 | @import 'bootstrap/scss/grid'; 9 | @import 'bootstrap/scss/tables'; 10 | @import 'bootstrap/scss/forms'; 11 | @import 'bootstrap/scss/buttons'; 12 | @import 'bootstrap/scss/transitions'; 13 | @import 'bootstrap/scss/dropdown'; 14 | @import 'bootstrap/scss/button-group'; 15 | @import 'bootstrap/scss/nav'; 16 | @import 'bootstrap/scss/navbar'; 17 | @import 'bootstrap/scss/card'; 18 | @import 'bootstrap/scss/accordion'; 19 | @import 'bootstrap/scss/breadcrumb'; 20 | @import 'bootstrap/scss/pagination'; 21 | @import 'bootstrap/scss/badge'; 22 | @import 'bootstrap/scss/alert'; 23 | @import 'bootstrap/scss/progress'; 24 | @import 'bootstrap/scss/list-group'; 25 | @import 'bootstrap/scss/close'; 26 | @import 'bootstrap/scss/toasts'; 27 | @import 'bootstrap/scss/modal'; 28 | @import 'bootstrap/scss/tooltip'; 29 | @import 'bootstrap/scss/popover'; 30 | // @import 'bootstrap/scss/carousel'; 31 | // @import 'bootstrap/scss/spinners'; 32 | @import 'bootstrap/scss/offcanvas'; 33 | @import 'bootstrap/scss/placeholders'; 34 | 35 | // Helpers 36 | @import 'bootstrap/scss/helpers'; 37 | 38 | // Utilities 39 | @import 'bootstrap/scss/utilities/api'; 40 | // scss-docs-end import-stack 41 | 42 | // Oddball extensions. 43 | .button { 44 | @extend .btn; 45 | } 46 | 47 | .messages { 48 | @extend .alert; 49 | } 50 | -------------------------------------------------------------------------------- /assets/scss/ckeditor.scss: -------------------------------------------------------------------------------- 1 | // An entrypoint file to inject styles into ckeditor 5. 2 | 3 | // Wrap in ck-content to avoid style bleed in the admin and frontend UI. 4 | // @see templates/field/field--text.html.twig 5 | 6 | .ck-content { 7 | /* stylelint-disable no-invalid-position-at-import-rule */ 8 | @import 'content/wysiwyg'; 9 | } 10 | -------------------------------------------------------------------------------- /assets/scss/content/elements.scss: -------------------------------------------------------------------------------- 1 | // Generic HTML elements that could be in WYSIWYG or elsewhere. 2 | 3 | // Block quotes in WYSIWYG don't have a class .blockquote 4 | // make a generic blockquote. 5 | blockquote, 6 | .blockquote { 7 | @include font-size($blockquote-font-size); 8 | 9 | padding-left: var(--bs-gutter-x); 10 | border-left: 4px solid var(--bs-border-color); 11 | font-style: italic; 12 | 13 | .field + .field { 14 | margin-top: map-get($spacers, 2); 15 | } 16 | } 17 | 18 | picture, 19 | .picture { 20 | display: inline-block; 21 | } 22 | 23 | audio, 24 | video { 25 | width: 100%; 26 | } 27 | 28 | .paragraph--type--media { 29 | @include border-radius(); 30 | 31 | padding: map-get($spacers, 3); 32 | border: var(--bs-border-width) solid var(--bs-border-color); 33 | 34 | &:has(.ratio) { 35 | max-width: map-get($container-max-widths, 'lg'); 36 | margin: 0 auto; 37 | } 38 | 39 | img, 40 | video, 41 | iframe { 42 | @include border-radius(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /assets/scss/content/fields.scss: -------------------------------------------------------------------------------- 1 | .field__label, 2 | .field .field-label-inline, 3 | .field .field-label-above { 4 | font-weight: bold; 5 | } 6 | 7 | .field--label-inline { 8 | > .field__label, 9 | > .field__items, 10 | > .field__items > .field__item, 11 | > .field__item { 12 | display: inline; 13 | } 14 | } 15 | 16 | .field--type-image > a { 17 | display: block; 18 | } 19 | 20 | // Tables in forms use a h4 for the label. Why. 21 | .field-label :is(h1, h2, h3, h4).label { 22 | display: inline-block; 23 | margin: 0; 24 | color: inherit; 25 | font-size: inherit; 26 | font-weight: inherit; 27 | font-style: inherit; 28 | } 29 | 30 | .node__meta .field--name-user-picture { 31 | max-width: 3rem; 32 | } 33 | 34 | .comment__meta .field--name-user-picture { 35 | max-width: 2rem; 36 | } 37 | -------------------------------------------------------------------------------- /assets/scss/content/spacing.scss: -------------------------------------------------------------------------------- 1 | // Basic spacing flow for fields in content. 2 | 3 | // Remove if you go manual spacing with d-grid. 4 | 5 | /* prettier-ignore */ 6 | :is( 7 | form, 8 | .lpb-formatter, 9 | .field, 10 | .layout, 11 | .paragraph, 12 | .block-layout-builder, 13 | .field__item 14 | ) + :is( 15 | form, 16 | .lpb-formatter, 17 | .field, 18 | .layout, 19 | .paragraph, 20 | .block-layout-builder, 21 | .field__item 22 | ) { 23 | margin-top: map-get($spacers, 3); 24 | } 25 | 26 | .field + .field--name-field-description { 27 | margin-top: map-get($spacers, 2); 28 | } 29 | 30 | // Match the doc flow. 31 | ul, 32 | ol, 33 | blockquote, 34 | .table-responsive { 35 | margin-bottom: map-get($spacers, 3); 36 | } 37 | 38 | // Squares up some elements in grids. 39 | // Without this, you need to pay close attention to margins. 40 | :is(p, ul, ol, table, blockquote, .table-responsive, img):last-child { 41 | margin-bottom: 0; 42 | } 43 | 44 | // ul ul 45 | .list-group .list-group { 46 | margin-top: var(--bs-list-group-item-padding-y); 47 | } 48 | -------------------------------------------------------------------------------- /assets/scss/content/wysiwyg.scss: -------------------------------------------------------------------------------- 1 | // This file is intended to house WYSIWYG content from ckeditor. 2 | // Styles defined here are available within ckeditor and will apply to .text-formatted. 3 | // 4 | // ckeditor will not have bootstrap grids etc unless you explicitly add bootstrap to ckeditor.scss 5 | 6 | img { 7 | @include img-fluid(); 8 | 9 | margin-bottom: $paragraph-margin-bottom; 10 | } 11 | 12 | .align-left { 13 | clear: left; 14 | margin-right: var(--bs-gutter-x); 15 | } 16 | 17 | .align-right { 18 | clear: right; 19 | margin-left: var(--bs-gutter-x); 20 | } 21 | -------------------------------------------------------------------------------- /assets/scss/drupal/breadcrumbs.scss: -------------------------------------------------------------------------------- 1 | .gin-breadcrumb { 2 | ol { 3 | display: flex; 4 | margin: 0; 5 | padding: 0; 6 | list-style: none; 7 | } 8 | li { 9 | margin: 0; 10 | padding: 0; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /assets/scss/drupal/dropbutton.scss: -------------------------------------------------------------------------------- 1 | .dropbutton { 2 | .btn { 3 | width: 100%; 4 | } 5 | 6 | .dropbutton-toggle { 7 | button { 8 | color: var(--bs-white); 9 | background-color: var(--bs-link-color); 10 | &:hover { 11 | background-color: var(--bs-link-hover-color); 12 | } 13 | } 14 | } 15 | 16 | .secondary-action { 17 | border-top: var(--bs-border-width) var(--bs-border-style) var(--bs-link-color); 18 | } 19 | } 20 | 21 | .dropbutton-widget { 22 | border: var(--bs-border-width) var(--bs-border-style) var(--bs-link-color); 23 | border-radius: var(--bs-border-radius); 24 | background-color: var(--bs-white); 25 | } 26 | -------------------------------------------------------------------------------- /assets/scss/drupal/drupal.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * Contains minimal fixes for Drupal and Bootstrap. 4 | */ 5 | 6 | @import 'field-group'; 7 | @import 'forms'; 8 | @import 'dropbutton'; 9 | @import 'breadcrumbs'; 10 | @import 'layout-builder'; 11 | @import 'layout-paragraphs'; 12 | @import 'media-library-errors'; 13 | @import 'media-library'; 14 | @import 'messages'; 15 | @import 'table-draggable'; 16 | @import 'ui-dialog'; 17 | @import 'ui-autocomplete'; 18 | 19 | .indented { 20 | // Comments and forums. 21 | margin-left: var(--bs-gutter-x); 22 | } 23 | 24 | :root { 25 | // Ajax loader replacement animation colour 26 | --sk-color: var(--bs-gray-500); 27 | } 28 | -------------------------------------------------------------------------------- /assets/scss/drupal/field-group.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * Contains minimal layout styling for the field-group elements. 4 | */ 5 | 6 | // Details with chevron fontawesome toggle on right. 7 | details { 8 | > summary { 9 | display: grid; 10 | grid-template-areas: 11 | 'title icon' 12 | 'summary icon'; 13 | grid-template-rows: max-content auto; 14 | grid-template-columns: auto min-content; 15 | grid-auto-flow: row; 16 | gap: 0 0; 17 | 18 | &::marker { 19 | display: none; 20 | } 21 | 22 | .title { 23 | grid-area: title; 24 | color: var(--bs-link-color); 25 | } 26 | 27 | .icon { 28 | grid-area: icon; 29 | .svg-inline--fa { 30 | @include transition; 31 | } 32 | } 33 | 34 | .summary { 35 | @include font-size($font-size-sm); 36 | 37 | grid-area: summary; 38 | font-weight: var(--bs-body-font-weight); 39 | } 40 | 41 | &:hover .title { 42 | text-decoration: underline; 43 | color: var(--bs-link-hover-color); 44 | } 45 | } 46 | 47 | &[open] > summary { 48 | .title { 49 | color: inherit; 50 | } 51 | .svg-inline--fa { 52 | transform: rotate(-180deg); 53 | } 54 | } 55 | 56 | &:not([open]) > summary { 57 | border-bottom: none; 58 | } 59 | } 60 | 61 | // Mobile detail flow 62 | div[data-vertical-tabs-panes]:not(.vertical-tabs__panes), 63 | div[data-horizontal-tabs-panes]:not(.horizontal-tabs-panes) { 64 | > details + details { 65 | margin-top: map-get($spacers, 3); 66 | } 67 | } 68 | 69 | // Vertical tabs 70 | .vertical-tabs { 71 | --vertical-tabs-width: clamp(10rem, 20vw, 18rem); 72 | 73 | margin: 0 0 0 var(--vertical-tabs-width); 74 | border-color: var(--bs-border-color-translucent); 75 | border-top-right-radius: var(--bs-border-radius); 76 | border-bottom-right-radius: var(--bs-border-radius); 77 | background: var(--bs-white); 78 | } 79 | 80 | .vertical-tabs__menu { 81 | width: var(--vertical-tabs-width); 82 | margin-left: calc(-1 * var(--vertical-tabs-width)); 83 | border-color: var(--bs-border-color-translucent); 84 | border-top-left-radius: var(--bs-border-radius); 85 | } 86 | 87 | .vertical-tabs__menu-item { 88 | border-color: var(--bs-border-color-translucent); 89 | background: rgba(var(--bs-black-rgb), 0.03); 90 | 91 | a { 92 | padding: $card-spacer-y $card-spacer-x; 93 | } 94 | &.first { 95 | border-top-left-radius: var(--bs-border-radius); 96 | } 97 | &.last { 98 | border-bottom-left-radius: var(--bs-border-radius); 99 | } 100 | &.is-selected { 101 | background: var(--bs-white); 102 | } 103 | &.is-selected .vertical-tabs__menu-item-title { 104 | color: var(--bs-dark); 105 | } 106 | } 107 | 108 | .vertical-tabs__menu-item-title { 109 | display: flex; 110 | overflow: hidden; 111 | text-overflow: ellipsis; 112 | font-weight: var(--bs-body-font-weight); 113 | } 114 | 115 | .vertical-tabs__menu-item-summary { 116 | @include font-size($font-size-base * 0.75); 117 | 118 | color: rgba(var(--bs-body-color-rgb), 0.75); 119 | } 120 | 121 | .vertical-tabs__panes > * { 122 | border: none; 123 | border-radius: 0; 124 | background: transparent; 125 | } 126 | 127 | // horizontal-tabs 128 | .horizontal-tabs { 129 | margin: 0; 130 | border: none; 131 | } 132 | 133 | .horizontal-tab-button a { 134 | @include font-size(var(--bs-nav-link-font-size)); 135 | @include transition($nav-link-transition); 136 | @include border-top-radius(var(--bs-nav-tabs-border-radius)); 137 | 138 | display: block; 139 | margin-bottom: calc(var(--bs-nav-tabs-border-width) * -1); 140 | padding: var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x); 141 | text-decoration: if($link-decoration == none, null, none); 142 | color: var(--bs-nav-link-color); 143 | border: var(--bs-nav-tabs-border-width) solid transparent; 144 | background: none; 145 | font-weight: var(--bs-nav-link-font-weight); 146 | 147 | strong { 148 | font-weight: inherit; 149 | } 150 | 151 | &:hover, 152 | &:focus { 153 | text-decoration: if($link-hover-decoration == underline, none, null); 154 | color: var(--bs-nav-link-hover-color); 155 | border-color: var(--bs-nav-tabs-link-hover-border-color); 156 | isolation: isolate; 157 | } 158 | } 159 | 160 | .horizontal-tab-button.selected a { 161 | color: var(--bs-nav-tabs-link-active-color); 162 | border-color: var(--bs-nav-tabs-link-active-border-color); 163 | background-color: var(--bs-nav-tabs-link-active-bg); 164 | } 165 | 166 | .horizontal-tabs-panes .horizontal-tabs-pane { 167 | padding: 0; 168 | } 169 | 170 | .horizontal-tabs-pane > .details-wrapper { 171 | border: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color); 172 | border-top: none; 173 | border-bottom-right-radius: var(--bs-border-radius); 174 | border-bottom-left-radius: var(--bs-border-radius); 175 | } 176 | 177 | ul[data-horizontal-tabs-list]:empty { 178 | border: none; 179 | } 180 | -------------------------------------------------------------------------------- /assets/scss/drupal/forms.scss: -------------------------------------------------------------------------------- 1 | // Drupal clobber. 2 | input.form-text { 3 | margin-top: 0; 4 | } 5 | 6 | // Some elements contain multiple form-items in the form-item. 7 | // Eg password on registration page. 8 | :is(.form-item, .form-wrapper):not(.form--inline) > :is(.form-wrapper, .form-item) + :is(.form-wrapper, .form-item) { 9 | margin-top: map-get($spacers, 3); 10 | } 11 | 12 | // Add an asterisk. 13 | .form-required::after { 14 | content: '\00a0*'; 15 | color: var(--bs-danger); 16 | } 17 | 18 | .required.error + .ck-editor { 19 | --ck-color-base-border: var(--bs-danger); 20 | } 21 | 22 | // Unstyled button. 23 | .button:not(.btn) { 24 | @include button-outline-variant($secondary); 25 | 26 | &.button--primary { 27 | @include button-outline-variant($primary); 28 | } 29 | 30 | &.button--danger { 31 | @include button-outline-variant($danger); 32 | } 33 | } 34 | 35 | // Small buttons from Drupal that snuck through somehow. 36 | .small .button:not(.btn), 37 | .button--small, 38 | .button--extrasmall { 39 | @include button-size($btn-padding-y-sm, $btn-padding-x-sm, $btn-font-size-sm, $btn-border-radius-sm); 40 | } 41 | 42 | // Button to style as plain text. 43 | .filter-wrapper a, 44 | button.webform-details-toggle-state, 45 | button.link:not(.btn) { 46 | @include font-size($font-size-sm); 47 | 48 | margin: 0; 49 | padding: 0; 50 | color: var(--bs-secondary); 51 | border: none; 52 | background: transparent; 53 | font-family: inherit; 54 | font-weight: normal; 55 | line-height: inherit; 56 | 57 | &:hover, 58 | &:focus, 59 | &:active { 60 | color: var(--bs-link-hover-color); 61 | } 62 | } 63 | 64 | .form-check-input, 65 | .form-check-label { 66 | cursor: pointer; 67 | } 68 | 69 | // Webform 70 | .webform-wizard-pages-links { 71 | display: none !important; 72 | } 73 | .webform-details-toggle-state-wrapper { 74 | margin-top: 0; 75 | } 76 | .webform-scale-option .form-check { 77 | padding-left: 0; 78 | } 79 | .webform-scale-option .form-check-label { 80 | line-height: 1.6; 81 | } 82 | .rateit { 83 | display: flex; 84 | } 85 | .rateit-range { 86 | vertical-align: middle; 87 | } 88 | .webform-flexbox { 89 | display: flex; 90 | flex-direction: column; 91 | margin: 0; 92 | gap: var(--bs-gutter-x); 93 | @include media-breakpoint-up(md) { 94 | flex-direction: row; 95 | } 96 | } 97 | .webform-signature-pad { 98 | border-radius: var(--bs-border-radius); 99 | .button { 100 | top: map-get($spacers, 3); 101 | right: map-get($spacers, 3); 102 | } 103 | } 104 | .webform-flexbox + .webform-flexbox { 105 | margin-top: map-get($spacers, 3); 106 | } 107 | .webform-flex--container { 108 | margin: 0; 109 | } 110 | .webform-flex--container > .form-wrapper { 111 | padding: 0 !important; 112 | border: none !important; 113 | } 114 | 115 | // Select 2 116 | .select2-container .select2-selection { 117 | height: 38px; 118 | border-color: var(--bs-border-color); 119 | &.select2-selection--single .select2-selection__rendered { 120 | line-height: 38px; 121 | } 122 | } 123 | 124 | // Sign up form injected by Drupal JS. 125 | .password-suggestions, 126 | .password-confirm-message, 127 | .password-strength { 128 | @include font-size($font-size-sm); 129 | 130 | margin-top: map-get($spacers, 1); 131 | color: var(--bs-secondary); 132 | 133 | ul { 134 | margin: 0; 135 | } 136 | .ok { 137 | color: var(--bs-success); 138 | } 139 | .error { 140 | color: var(--bs-danger); 141 | } 142 | } 143 | 144 | // Use bootstrap colors. 145 | .password-strength { 146 | .is-weak { 147 | background: var(--bs-danger); 148 | } 149 | .is-fair { 150 | background: var(--bs-warning); 151 | } 152 | .is-good { 153 | background: var(--bs-info); 154 | } 155 | .is-strong { 156 | background: var(--bs-success); 157 | } 158 | 159 | .password-strength__indicator, 160 | .password-strength__meter { 161 | border-radius: $progress-border-radius; 162 | } 163 | 164 | .password-strength__title { 165 | margin-top: map-get($spacers, 1); 166 | } 167 | } 168 | 169 | .description.small { 170 | > .list-group { 171 | --bs-list-group-color: inherit; 172 | 173 | margin-top: map-get($spacers, 3); 174 | } 175 | } 176 | 177 | .node-preview-form-select.gin-layout-container { 178 | display: flex !important; 179 | .form-label { 180 | margin: 0; 181 | white-space: nowrap; 182 | font-weight: normal; 183 | } 184 | .form-item-view-mode { 185 | display: none; 186 | 187 | .form-select { 188 | @include font-size($font-size-sm); 189 | } 190 | 191 | @include media-breakpoint-up(sm) { 192 | display: flex; 193 | align-items: center; 194 | margin-left: auto; 195 | } 196 | } 197 | } 198 | 199 | .entity-content-form-footer { 200 | margin: map-get($spacers, 3) 0; 201 | padding: map-get($spacers, 3); 202 | border: var(--bs-border-width) solid var(--bs-border-color-translucent); 203 | border-radius: var(--bs-border-radius); 204 | } 205 | 206 | // Disable media contextual links within form. 207 | form .lpb-layout .field--type-entity-reference .contextual { 208 | display: none !important; 209 | } 210 | 211 | // Paragraphs weird. 212 | .js .field--widget-paragraphs th .paragraphs-actions { 213 | margin-right: calc(var(--bs-gutter-x) * -0.25); 214 | } 215 | 216 | // Poorly made composites 217 | .input-group .field-prefix:empty { 218 | display: none; 219 | } 220 | -------------------------------------------------------------------------------- /assets/scss/drupal/layout-builder.scss: -------------------------------------------------------------------------------- 1 | form + .layout-builder { 2 | margin-top: map-get($spacers, 3); 3 | } 4 | 5 | #drupal-off-canvas { 6 | --bs-body-color-rgb: 255, 255, 255; 7 | --bs-body-bg-rgb: var(--bs-dark-rgb); 8 | --bs-body-color: var(--bs-white); 9 | 10 | > form { 11 | display: block !important; 12 | padding-top: 10px; 13 | } 14 | 15 | .form-label { 16 | display: block; 17 | } 18 | 19 | .form-item { 20 | min-height: revert; 21 | padding-left: revert; 22 | } 23 | 24 | #formatter-settings-wrapper { 25 | margin-bottom: $spacer; 26 | } 27 | 28 | .form-actions { 29 | margin-top: 15px; 30 | > * { 31 | margin: 0; 32 | } 33 | } 34 | 35 | .list-group-item { 36 | clear: both; 37 | } 38 | 39 | .layout-selection li a { 40 | display: flex; 41 | align-items: center; 42 | 43 | .layout-icon { 44 | flex-shrink: 0; 45 | align-self: start; 46 | margin-right: 15px; 47 | } 48 | } 49 | 50 | .inline-block-create-button { 51 | background-color: var(--bs-link-color); 52 | &:hover, 53 | &:focus { 54 | background-color: var(--bs-link-hover-color); 55 | } 56 | } 57 | 58 | .table-responsive { 59 | overflow: initial; 60 | } 61 | } 62 | 63 | .layout-icon__region { 64 | fill: var(--bs-secondary); 65 | } 66 | -------------------------------------------------------------------------------- /assets/scss/drupal/layout-paragraphs.scss: -------------------------------------------------------------------------------- 1 | // Give the paragraphs some breathing room. 2 | .field--widget-layout-paragraphs { 3 | margin-top: map-get($spacers, 4); 4 | margin-bottom: map-get($spacers, 4); 5 | } 6 | 7 | .lpb-enable { 8 | z-index: 100; 9 | } 10 | 11 | .lpb-layout { 12 | padding: map-get($spacers, 4); 13 | } 14 | 15 | .lpb-formatter:hover, 16 | .lpb-formatter:focus-within { 17 | @include border-radius($btn-border-radius); 18 | 19 | outline-color: var(--bs-primary); 20 | } 21 | 22 | form.layout-paragraphs-builder-form .js-lpb-component-list { 23 | padding: 0; 24 | } 25 | 26 | a.lpb-enable-button, 27 | .lpb-controls.is-layout { 28 | background: var(--bs-primary); 29 | } 30 | 31 | .lpb-tooltiptext { 32 | @include font-size($tooltip-font-size); 33 | @include border-radius($tooltip-border-radius); 34 | 35 | margin: $tooltip-font-size; 36 | padding: $tooltip-arrow-height; 37 | word-wrap: break-word; 38 | color: $tooltip-color; 39 | background-color: $tooltip-bg; 40 | } 41 | 42 | .lp-builder:not(.is-navigating) .js-lpb-component:hover, 43 | .lp-builder:not(.is-navigating) .js-lpb-component:focus-within { 44 | outline-color: var(--bs-primary); 45 | } 46 | 47 | a.lpb-enable-button, 48 | .lpb-btn { 49 | @include transition($btn-transition); 50 | @include font-size($btn-font-size); 51 | @include border-radius($btn-border-radius); 52 | 53 | display: inline-flex; 54 | padding: $btn-padding-y $btn-padding-x !important; 55 | cursor: pointer; 56 | user-select: none; 57 | text-align: center; 58 | vertical-align: middle; 59 | white-space: $btn-white-space; 60 | text-decoration: none; 61 | color: var(--bs-white); 62 | background: var(--bs-primary); 63 | box-shadow: $btn-box-shadow !important; 64 | font-weight: $btn-font-weight; 65 | line-height: $btn-line-height; 66 | 67 | &:hover { 68 | color: var(--bs-white); 69 | background: var(--bs-teal); 70 | } 71 | } 72 | 73 | .lpb-enable__empty-message__wrapper { 74 | @include border-radius($btn-border-radius); 75 | 76 | padding: map-get($spacers, 4); 77 | border: var(--bs-border-width) solid var(--bs-border-color-translucent); 78 | } 79 | 80 | .lpb-enable__empty-message { 81 | .lpb-enable-button { 82 | display: flex; 83 | width: fit-content; 84 | margin-top: map-get($spacers, 4); 85 | } 86 | } 87 | 88 | a.lpb-enable-button::before { 89 | width: 1.2em; 90 | margin-right: map-get($spacers, 2); 91 | } 92 | 93 | .lpb-btn.before { 94 | top: #{-1 * map-get($spacers, 4)}; 95 | } 96 | 97 | .lpb-btn.after { 98 | bottom: #{-1 * map-get($spacers, 4)}; 99 | } 100 | 101 | .lpb-btn--add.before { 102 | top: #{-1 * map-get($spacers, 3)}; 103 | } 104 | 105 | .lpb-btn--add.after { 106 | bottom: #{-1 * map-get($spacers, 3)}; 107 | } 108 | 109 | .lpb-component-list { 110 | .lpb-component-list-search-input { 111 | @include font-size($input-font-size); 112 | @include border-radius($input-border-radius, 0); 113 | @include box-shadow($input-box-shadow); 114 | @include transition($input-transition); 115 | 116 | display: block; 117 | width: 100%; 118 | padding: $input-padding-y $input-padding-x; 119 | color: $input-color; 120 | border: $input-border-width solid $input-border-color; 121 | background-color: $input-bg; 122 | background-clip: padding-box; 123 | font-family: $input-font-family; 124 | font-weight: $input-font-weight; 125 | line-height: $input-line-height; 126 | appearance: none; // Fix appearance for date inputs in Safari 127 | &:focus { 128 | color: $input-focus-color; 129 | border-color: $input-focus-border-color; 130 | outline: 0; 131 | background-color: $input-focus-bg; 132 | 133 | @if $enable-shadows { 134 | @include box-shadow($input-box-shadow, $input-focus-box-shadow); 135 | } @else { 136 | // Avoid using mixin so we can pass custom focus shadow properly 137 | box-shadow: $input-focus-box-shadow; 138 | } 139 | } 140 | } 141 | .lpb-component-list__group { 142 | margin-top: map-get($spacers, 2); 143 | } 144 | 145 | .lpb-component-list__item { 146 | border-top: var(--bs-border-width) solid var(--bs-border-color-translucent); 147 | 148 | a { 149 | @include transition($btn-transition); 150 | @include font-size($font-size-sm); 151 | @include border-radius(); 152 | 153 | position: relative; 154 | margin: 0; 155 | padding: map-get($spacers, 2); 156 | text-decoration: none; 157 | 158 | &:hover { 159 | color: var(--bs-white); 160 | background: var(--bs-primary); 161 | 162 | img { 163 | filter: invert(1); 164 | } 165 | } 166 | } 167 | } 168 | } 169 | 170 | #layout-paragraphs-element { 171 | .form-wrapper + .form-wrapper { 172 | margin-top: map-get($spacers, 3); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /assets/scss/drupal/media-library-errors.scss: -------------------------------------------------------------------------------- 1 | // The ckeditor caption filter's styling overrides ours. 2 | .media-embed-error, 3 | .caption > .media-embed-error { 4 | max-width: 200px; 5 | padding: 100px 20px 20px; 6 | text-align: center; 7 | background-color: #ebebeb; 8 | background-image: url('/assets/images/no-thumbnail.png'); 9 | background-repeat: no-repeat; 10 | background-position: center top; 11 | background-size: 100px 100px; 12 | } 13 | -------------------------------------------------------------------------------- /assets/scss/drupal/media-library.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * Contains minimal layout styling for the media library. 4 | */ 5 | 6 | .view-id-media_library { 7 | .view-header { 8 | float: right; 9 | margin: 0 !important; 10 | padding: 0 !important; 11 | } 12 | .view-content { 13 | clear: right; 14 | } 15 | } 16 | 17 | .view-id-media_library.view-display-id-widget_table { 18 | .views-field-media-library-select-form { 19 | width: 20px; 20 | } 21 | .views-field-thumbnail__target-id { 22 | width: 64px; 23 | img { 24 | max-width: 64px; 25 | height: auto; 26 | } 27 | } 28 | .views-field-uid { 29 | width: 20%; 30 | } 31 | .views-field-changed { 32 | width: 20%; 33 | } 34 | } 35 | 36 | .view-id-media_library.view-display-id-widget { 37 | .views-field-media-library-select-form { 38 | position: absolute; 39 | z-index: 2; 40 | top: 10px; 41 | left: calc(var(--bs-gutter-x) / 2 + 10px); 42 | } 43 | } 44 | 45 | .js-media-library-item { 46 | position: relative; 47 | max-width: 100%; 48 | cursor: pointer; 49 | 50 | .btn { 51 | @include font-size($font-size-sm); 52 | 53 | position: absolute; 54 | z-index: 10; 55 | top: calc(var(--bs-gutter-x) / 2); 56 | right: var(--bs-gutter-x); 57 | } 58 | 59 | .ajax-progress { 60 | position: absolute; 61 | top: calc(var(--bs-gutter-x) / 2); 62 | left: var(--bs-gutter-x); 63 | .message { 64 | display: none; 65 | } 66 | } 67 | 68 | .form-type-number { 69 | @include font-size($font-size-sm); 70 | 71 | margin-top: map-get($spacers, 2); 72 | } 73 | 74 | img { 75 | width: 100%; 76 | height: 100%; 77 | pointer-events: none; 78 | object-fit: cover; 79 | object-position: center; 80 | } 81 | 82 | .field--name-thumbnail { 83 | display: flex; 84 | user-select: none; 85 | 86 | img[src*='/generic/'] { 87 | max-width: 90px; 88 | object-fit: contain; 89 | } 90 | 91 | @include media-breakpoint-up(md) { 92 | align-items: center; 93 | justify-content: center; 94 | } 95 | } 96 | 97 | .media-library-item--disabled { 98 | pointer-events: none; 99 | opacity: 0.5; 100 | } 101 | 102 | &.is-hover .media-library-item__preview-wrapper { 103 | border-color: var(--bs-secondary) !important; 104 | } 105 | 106 | &.checked .media-library-item__preview-wrapper { 107 | border-color: var(--bs-primary) !important; 108 | } 109 | 110 | .media-library-item__preview { 111 | height: clamp(100px, 10vw, 200px); 112 | padding-bottom: 0; 113 | > div { 114 | height: 100%; 115 | } 116 | } 117 | 118 | .media-library-item__attributes { 119 | position: static; 120 | max-width: 100%; 121 | margin: 0; 122 | padding: 0; 123 | } 124 | } 125 | 126 | .js-media-library-add-form { 127 | margin-bottom: map-get($spacers, 4); 128 | } 129 | -------------------------------------------------------------------------------- /assets/scss/drupal/messages.scss: -------------------------------------------------------------------------------- 1 | // Inline errors smaller. 2 | .region-content div[data-drupal-messages] { 3 | @include font-size($font-size-sm); 4 | } 5 | 6 | // Drupal clobber. 7 | em.placeholder { 8 | display: initial; 9 | min-height: unset; 10 | cursor: unset; 11 | vertical-align: initial; 12 | opacity: initial; 13 | background-color: initial; 14 | font-style: inherit; 15 | } 16 | 17 | // Warning injected by drupal JS. 18 | abbr.warning { 19 | text-decoration: none; 20 | } 21 | 22 | // Messages coming from an admin action and injected dynamically into the interface don't get themed. 23 | .messages { 24 | $messages: ( 25 | 'status': 'success', 26 | 'warning': 'danger', 27 | ); 28 | 29 | @each $key, $state in $messages { 30 | .messages--#{$key}, 31 | &.messages--#{$key} { 32 | --#{$prefix}alert-color: var(--#{$prefix}#{$state}-text-emphasis); 33 | --#{$prefix}alert-bg: var(--#{$prefix}#{$state}-bg-subtle); 34 | --#{$prefix}alert-border-color: var(--#{$prefix}#{$state}-border-subtle); 35 | --#{$prefix}alert-link-color: var(--#{$prefix}#{$state}-text-emphasis); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /assets/scss/drupal/table-draggable.scss: -------------------------------------------------------------------------------- 1 | // Tide up sidebar clipping. 2 | .draggable { 3 | @include transition(background 0.2s ease-in-out); 4 | 5 | box-sizing: content-box; 6 | 7 | .handle, 8 | .tabledrag-handle { 9 | box-sizing: content-box; 10 | } 11 | 12 | &.drag { 13 | background: rgba(var(--bs-info-rgb), 0.125); 14 | } 15 | } 16 | 17 | // Multiple table input alert injected by drupal JS. 18 | .tabledrag-changed-warning { 19 | --bs-text-opacity: 0.75; 20 | 21 | @include font-size($font-size-sm); 22 | 23 | color: rgba(var(--bs-body-color-rgb), 0.75) !important; 24 | } 25 | 26 | .js .field--widget-paragraphs .field-multiple-drag { 27 | width: 40px; 28 | } 29 | -------------------------------------------------------------------------------- /assets/scss/drupal/ui-autocomplete.scss: -------------------------------------------------------------------------------- 1 | // Pad in a bit. Looks better. 2 | .js input.form-autocomplete { 3 | background-position-x: calc(100% - #{$input-padding-x}); 4 | &.is-invalid { 5 | background-image: escape-svg($form-feedback-icon-invalid); 6 | } 7 | } 8 | 9 | .ui-widget.ui-widget-content.ui-autocomplete { 10 | @include border-radius(); 11 | @include box-shadow; 12 | 13 | border: $input-border-width solid $input-border-color; 14 | 15 | .ui-menu-item { 16 | display: block; 17 | 18 | &:first-child .ui-menu-item-wrapper { 19 | border-top-left-radius: $border-radius; 20 | border-top-right-radius: $border-radius; 21 | } 22 | 23 | &:last-child .ui-menu-item-wrapper { 24 | border-bottom-right-radius: $border-radius; 25 | border-bottom-left-radius: $border-radius; 26 | } 27 | } 28 | 29 | .ui-menu-item + .ui-menu-item { 30 | border-top: $input-border-width solid $input-border-color; 31 | } 32 | 33 | .ui-menu-item-wrapper { 34 | display: block; 35 | margin: 0 !important; 36 | padding: map-get($spacers, 1) map-get($spacers, 3); 37 | text-decoration: none; 38 | border: none !important; 39 | 40 | &:hover, 41 | &:active, 42 | &:focus, 43 | &.ui-state-active { 44 | color: var(--bs-white); 45 | background-color: var(--bs-primary); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /assets/scss/drupal/ui-dialog.scss: -------------------------------------------------------------------------------- 1 | .ui-dialog:not(.ui-dialog-off-canvas) { 2 | @include box-shadow; 3 | 4 | max-width: map-get($container-max-widths, 'xl'); 5 | padding: 0; 6 | border: 0 none !important; 7 | border-radius: var(--bs-border-radius); 8 | background: var(--bs-body-bg); 9 | 10 | // Dialog without title 11 | .ui-dialog-content:first-of-type { 12 | border-top-left-radius: var(--bs-border-radius); 13 | border-top-right-radius: var(--bs-border-radius); 14 | } 15 | 16 | &:focus { 17 | outline: 2px dotted transparent; 18 | box-shadow: $input-focus-box-shadow; 19 | } 20 | 21 | @media (forced-colors: active) { 22 | border: 1px solid buttonBorder !important; 23 | } 24 | } 25 | 26 | @include media-breakpoint-down(sm) { 27 | .ui-dialog:not(.ui-dialog-off-canvas) { 28 | min-width: 92%; 29 | max-width: 92%; 30 | } 31 | } 32 | 33 | .ui-dialog { 34 | position: absolute; 35 | z-index: 1260; 36 | top: 0; 37 | left: 0; 38 | outline: none; 39 | font-family: var(--bs-body-font-family); 40 | 41 | .ui-dialog-titlebar { 42 | position: relative; 43 | padding: map-get($spacers, 3); 44 | border: 0 none; 45 | border-radius: var(--bs-border-radius) var(--bs-border-radius) 0 0; 46 | background: var(--bs-dark); 47 | line-height: var(--bs-body-line-height); 48 | 49 | @media (forced-colors: active) { 50 | border-bottom: var(--bs-border-width) var(--bs-border-style) buttonBorder; 51 | } 52 | } 53 | 54 | .ui-dialog-title { 55 | box-sizing: border-box; 56 | width: 100%; 57 | padding-right: map-get($spacers, 5); 58 | color: var(--bs-light); 59 | font-weight: normal; 60 | } 61 | 62 | .ui-dialog-content { 63 | position: static; 64 | padding: map-get($spacers, 3); 65 | color: var(--bs-body-color); 66 | } 67 | 68 | .ui-widget-content.ui-dialog-content { 69 | background: var(--bs-body-bg); 70 | } 71 | 72 | .ui-widget-content.ui-dialog-buttonpane { 73 | padding: map-get($spacers, 3); 74 | border-top: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color); 75 | border-bottom-right-radius: var(--bs-border-radius); 76 | border-bottom-left-radius: var(--bs-border-radius); 77 | background: var(--bs-tertiary-bg); 78 | 79 | .button { 80 | margin-top: 0; 81 | margin-right: map-get($spacers, 3); 82 | margin-bottom: 0; 83 | 84 | &:last-of-type { 85 | margin-right: 0; 86 | } 87 | } 88 | } 89 | 90 | .ui-dialog-buttonset { 91 | margin-top: 0; 92 | } 93 | 94 | .ui-dialog-titlebar-close { 95 | position: absolute; 96 | top: 50%; 97 | right: map-get($spacers, 3); 98 | left: auto; 99 | margin: 0; 100 | transform: translateY(-50%); 101 | opacity: 0.8; 102 | border: none !important; 103 | background: none !important; 104 | 105 | .ui-icon-closethick { 106 | top: 0; 107 | left: 0; 108 | width: 100%; 109 | height: 100%; 110 | margin: 0; 111 | transform: none; 112 | background: var(--bs-light) !important; 113 | mask-image: url(../images/close.svg#close-view); 114 | mask-size: #{map-get($spacers, 4)} #{map-get($spacers, 4)}; 115 | mask-repeat: no-repeat; 116 | mask-position: center center; 117 | @media (forced-colors: active) { 118 | background: linktext; 119 | } 120 | } 121 | 122 | .ui-button-text { 123 | position: absolute; 124 | top: -9999px; 125 | left: -9999px; 126 | } 127 | 128 | &:hover { 129 | opacity: 1; 130 | } 131 | } 132 | } 133 | 134 | .ui-widget-overlay { 135 | z-index: 1259; 136 | opacity: $modal-backdrop-opacity !important; 137 | background: $modal-backdrop-bg !important; 138 | } 139 | 140 | .views-ui-dialog { 141 | .views-offset-bottom { 142 | border-top: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color); 143 | 144 | @media (forced-colors: active) { 145 | border-top: none; 146 | } 147 | } 148 | } 149 | 150 | .ui-dialog-buttons.lpb-dialog .ui-dialog-content > form > .form-actions { 151 | display: none !important; 152 | } 153 | 154 | // Token 155 | table.treetable { 156 | tr.branch { 157 | background-color: transparent; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /assets/scss/layout/footer.scss: -------------------------------------------------------------------------------- 1 | footer.site-footer { 2 | // Generic links. 3 | --bs-link-color-rgb: var(--bs-white-rgb); 4 | --bs-link-hover-color-rgb: var(--bs-teal-rgb); 5 | 6 | a { 7 | @include transition; 8 | 9 | text-decoration: none; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /assets/scss/layout/header.scss: -------------------------------------------------------------------------------- 1 | // Very top of page, eg beta bar. 2 | .region-alerts { 3 | --bs-link-color-rgb: var(--bs-dark-rgb); 4 | --bs-link-hover-color-rgb: var(--bs-dark-rgb); 5 | } 6 | 7 | .site-header { 8 | // Generic links. 9 | --bs-link-color-rgb: var(--bs-white-rgb); 10 | --bs-link-hover-color-rgb: var(--bs-teal-rgb); 11 | 12 | // Nav links. 13 | --bs-link-color: var(--bs-white); 14 | --bs-link-hover-color: var(--bs-teal); 15 | } 16 | 17 | // Fix devel z-indexes. 18 | .region-messages { 19 | position: relative; 20 | z-index: 1; 21 | } 22 | 23 | // Square up as its 100% width. 24 | .region-messages .alert, 25 | .region-messages .messages { 26 | --bs-alert-border-radius: 0; 27 | --bs-alert-margin-bottom: 0; 28 | } 29 | 30 | // This also appears on the maintenance page. 31 | .site-branding { 32 | @include transition; 33 | 34 | text-decoration: none; 35 | 36 | .site-logo { 37 | max-width: clamp(2rem, 7vw, 25vw); 38 | 39 | img { 40 | max-height: clamp(2rem, 7vw, 4rem); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /assets/scss/layout/layout--twocol.scss: -------------------------------------------------------------------------------- 1 | // Override builtin layouts. 2 | 3 | .layout--twocol { 4 | display: grid; 5 | row-gap: map-get($spacers, 3); 6 | column-gap: map-get($spacers, 4); 7 | 8 | @include media-breakpoint-up(md) { 9 | grid-template-areas: 10 | 'top top' 11 | 'left right' 12 | 'bottom bottom'; 13 | grid-template-columns: 1fr 1fr; 14 | 15 | .layout__region--top { 16 | grid-area: top; 17 | } 18 | .layout__region--first { 19 | grid-area: left; 20 | } 21 | .layout__region--second { 22 | grid-area: right; 23 | } 24 | .layout__region--bottom { 25 | grid-area: bottom; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /assets/scss/navigation/back-top.scss: -------------------------------------------------------------------------------- 1 | // This will conflict with marketing chat popups. Good. 2 | .back-to-top { 3 | position: sticky; 4 | z-index: 20; 5 | bottom: $spacer * 2; 6 | transition: opacity 0.2s ease-in-out; 7 | 8 | a { 9 | position: absolute; 10 | top: 0; 11 | right: $spacer; 12 | } 13 | 14 | .btn { 15 | // Square up the button. 16 | --bs-btn-padding-y: var(--bs-btn-padding-x); 17 | --bs-btn-line-height: 1; 18 | 19 | transform: translateY(-50%); 20 | } 21 | 22 | &:not(.show) { 23 | pointer-events: none; 24 | opacity: 0; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /assets/scss/navigation/mobile-button.scss: -------------------------------------------------------------------------------- 1 | // @see https://jonsuh.com/hamburgers/ 2 | 3 | $hamburger-padding-x: 0.5rem; 4 | $hamburger-padding-y: 0.5rem; 5 | $hamburger-layer-border-radius: 0; 6 | $hamburger-layer-width: 26px; 7 | $hamburger-layer-height: 2px; 8 | $hamburger-layer-spacing: 7px; 9 | $hamburger-types: (squeeze); 10 | $hamburger-layer-color: currentColor; 11 | 12 | @import 'hamburgers/_sass/hamburgers/hamburgers'; 13 | 14 | .hamburger { 15 | border-radius: $btn-border-radius; 16 | line-height: 1; 17 | } 18 | -------------------------------------------------------------------------------- /assets/scss/navigation/mobile-menu.scss: -------------------------------------------------------------------------------- 1 | .region-mobile-menu { 2 | --bs-link-color-rgb: var(--bs-light-rgb); 3 | --bs-link-hover-color-rgb: var(--bs-teal-rgb); 4 | --bs-border-color: #{$gray-700}; 5 | 6 | .menu { 7 | li { 8 | display: grid; 9 | grid-template-areas: 10 | 'link button' 11 | 'expand expand'; 12 | grid-template-rows: max-content auto; 13 | grid-template-columns: auto min-content; 14 | grid-auto-flow: row; 15 | gap: 0 0; 16 | 17 | > a { 18 | grid-area: link; 19 | } 20 | .btn { 21 | grid-area: button; 22 | } 23 | .collapse { 24 | grid-area: expand; 25 | } 26 | } 27 | 28 | a { 29 | @include transition; 30 | 31 | text-decoration: none; 32 | } 33 | 34 | a.is-active, 35 | .menu-item--active-trail > a { 36 | --bs-link-color: var(--bs-link-hover-color); 37 | } 38 | 39 | .fa-chevron-up { 40 | @include transition; 41 | } 42 | .btn.collapsed .fa-chevron-up { 43 | transform: rotate(-180deg); 44 | } 45 | } 46 | 47 | .block + .block { 48 | margin-top: map-get($spacers, 3); 49 | padding-top: map-get($spacers, 3); 50 | border-top: 1px solid var(--bs-border-color); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /assets/scss/navigation/primary-menu.scss: -------------------------------------------------------------------------------- 1 | // Menu styling. 2 | .region-primary-menu .nav { 3 | a { 4 | text-decoration: none; 5 | } 6 | 7 | // Level 1 8 | .nav-link { 9 | --bs-nav-pills-border-radius: 0; 10 | 11 | &:hover, 12 | &:focus { 13 | color: white; 14 | background: var(--bs-gray-800); 15 | } 16 | } 17 | 18 | // Level 2 19 | .nav-link ~ div { 20 | --bs-link-color-rgb: var(--bs-light-rgb); 21 | --bs-link-hover-color-rgb: var(--bs-teal-rgb); 22 | --bs-border-color: var(--bs-gray-700); 23 | 24 | z-index: 100; 25 | 26 | // Split into columns. 27 | > ul { 28 | columns: 2; 29 | column-gap: var(--bs-gutter-x); 30 | 31 | @include media-breakpoint-up(lg) { 32 | columns: 3; 33 | } 34 | } 35 | 36 | // Fix border wrapping in columns. 37 | li { 38 | break-inside: avoid-column; 39 | } 40 | ul ul { 41 | border-bottom: 1px solid transparent; 42 | } 43 | 44 | // Active trail. 45 | .menu-item--active-trail > a { 46 | --bs-link-color: var(--bs-danger); 47 | } 48 | 49 | // Active page. 50 | a.is-active { 51 | --bs-link-color: var(--bs-info); 52 | } 53 | } 54 | } 55 | 56 | // Expand/collapse. 57 | .region-primary-menu .nav-item { 58 | .nav-link ~ div { 59 | @include transition; 60 | 61 | position: absolute; 62 | z-index: 15; 63 | top: 100%; 64 | right: 0; 65 | left: 0; 66 | visibility: hidden; 67 | opacity: 0; 68 | } 69 | 70 | .js-submenu-expand[aria-expanded='true'] ~ div, 71 | &:hover .nav-link ~ div { 72 | visibility: visible; 73 | opacity: 1; 74 | } 75 | } 76 | 77 | // Accessible button. 78 | .js-submenu-expand { 79 | position: absolute; 80 | z-index: 101; 81 | top: 0; 82 | height: stretch; 83 | transform: translateX(-50%); 84 | } 85 | -------------------------------------------------------------------------------- /assets/scss/variables.scss: -------------------------------------------------------------------------------- 1 | // _variables.scss 2 | 3 | // Important: This file is added to every entrypoint scss. 4 | // Do not add any CSS in this file. Only variables. 5 | 6 | @import 'bootstrap/scss/functions'; 7 | 8 | // Variables come next 9 | @import 'bootstrap/scss/variables'; 10 | @import 'bootstrap/scss/variables-dark'; 11 | 12 | // Optional variable overrides here 13 | $enable-smooth-scroll: false; 14 | 15 | $viva-magenta: #bb2649; 16 | 17 | $custom-theme-colors: ( 18 | 'teal': $teal, 19 | 'viva-magenta': $viva-magenta, 20 | ); 21 | 22 | $font-size-lg: 1.15rem; 23 | 24 | $breadcrumb-font-size: $font-size-sm; 25 | $breadcrumb-margin-bottom: 0; 26 | 27 | $blockquote-font-size: $font-size-lg; 28 | 29 | $form-label-font-weight: bold; 30 | 31 | $legend-margin-bottom: $form-label-margin-bottom; 32 | $legend-font-size: $form-label-font-size; 33 | $legend-font-weight: $form-label-font-weight; 34 | 35 | // Optional Sass map overrides here 36 | $theme-colors: map-merge($theme-colors, $custom-theme-colors); 37 | 38 | $modal-backdrop-bg: var(--bs-gray-800); 39 | $modal-backdrop-opacity: 0.8; 40 | 41 | $tooltip-font-size: 0.75rem; 42 | 43 | // Rest of bootstrap imports 44 | @import 'bootstrap/scss/maps'; 45 | @import 'bootstrap/scss/mixins'; 46 | @import 'bootstrap/scss/utilities'; 47 | -------------------------------------------------------------------------------- /config/install/block.block.dvb_account_menu.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | config: 5 | - system.menu.account 6 | module: 7 | - system 8 | theme: 9 | - dvb 10 | id: dvb_account_menu 11 | theme: dvb 12 | region: secondary_menu 13 | weight: -7 14 | provider: null 15 | plugin: 'system_menu_block:account' 16 | settings: 17 | id: 'system_menu_block:account' 18 | label: 'User account menu' 19 | label_display: '0' 20 | provider: system 21 | level: 1 22 | depth: 1 23 | expand_all_items: false 24 | visibility: {} 25 | -------------------------------------------------------------------------------- /config/install/block.block.dvb_account_menu_mobile.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | config: 5 | - system.menu.account 6 | module: 7 | - system 8 | theme: 9 | - dvb 10 | id: dvb_account_menu_mobile 11 | theme: dvb 12 | region: mobile_menu 13 | weight: -6 14 | provider: null 15 | plugin: 'system_menu_block:account' 16 | settings: 17 | id: 'system_menu_block:account' 18 | label: 'User account menu' 19 | label_display: '0' 20 | provider: system 21 | level: 1 22 | depth: 0 23 | expand_all_items: false 24 | visibility: {} 25 | -------------------------------------------------------------------------------- /config/install/block.block.dvb_breadcrumbs.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | module: 5 | - system 6 | theme: 7 | - dvb 8 | id: dvb_breadcrumbs 9 | theme: dvb 10 | region: breadcrumb 11 | weight: -6 12 | provider: null 13 | plugin: system_breadcrumb_block 14 | settings: 15 | id: system_breadcrumb_block 16 | label: Breadcrumbs 17 | label_display: '0' 18 | provider: system 19 | visibility: {} 20 | -------------------------------------------------------------------------------- /config/install/block.block.dvb_content.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | module: 5 | - system 6 | theme: 7 | - dvb 8 | id: dvb_content 9 | theme: dvb 10 | region: content 11 | weight: -2 12 | provider: null 13 | plugin: system_main_block 14 | settings: 15 | id: system_main_block 16 | label: 'Main page content' 17 | label_display: '0' 18 | provider: system 19 | visibility: {} 20 | -------------------------------------------------------------------------------- /config/install/block.block.dvb_help.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | module: 5 | - help 6 | theme: 7 | - dvb 8 | id: dvb_help 9 | theme: dvb 10 | region: help 11 | weight: -6 12 | provider: null 13 | plugin: help_block 14 | settings: 15 | id: help_block 16 | label: Help 17 | label_display: '0' 18 | provider: help 19 | visibility: {} 20 | -------------------------------------------------------------------------------- /config/install/block.block.dvb_main_menu.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | config: 5 | - system.menu.main 6 | module: 7 | - system 8 | theme: 9 | - dvb 10 | id: dvb_main_menu 11 | theme: dvb 12 | region: primary_menu 13 | weight: 0 14 | provider: null 15 | plugin: 'system_menu_block:main' 16 | settings: 17 | id: 'system_menu_block:main' 18 | label: 'Main navigation' 19 | label_display: '0' 20 | provider: system 21 | level: 1 22 | depth: 3 23 | expand_all_items: true 24 | visibility: {} 25 | -------------------------------------------------------------------------------- /config/install/block.block.dvb_main_menu_mobile.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | config: 5 | - system.menu.main 6 | module: 7 | - system 8 | theme: 9 | - dvb 10 | id: dvb_main_menu_mobile 11 | theme: dvb 12 | region: mobile_menu 13 | weight: -7 14 | provider: null 15 | plugin: 'system_menu_block:main' 16 | settings: 17 | id: 'system_menu_block:main' 18 | label: 'Main navigation' 19 | label_display: '0' 20 | provider: system 21 | level: 1 22 | depth: 2 23 | expand_all_items: true 24 | visibility: {} 25 | -------------------------------------------------------------------------------- /config/install/block.block.dvb_messages.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | module: 5 | - system 6 | theme: 7 | - dvb 8 | id: dvb_messages 9 | theme: dvb 10 | region: messages 11 | weight: -7 12 | provider: null 13 | plugin: system_messages_block 14 | settings: 15 | id: system_messages_block 16 | label: 'Status messages' 17 | label_display: '0' 18 | provider: system 19 | visibility: {} 20 | -------------------------------------------------------------------------------- /config/install/block.block.dvb_page_title.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | theme: 5 | - dvb 6 | id: dvb_page_title 7 | theme: dvb 8 | region: content 9 | weight: -3 10 | provider: null 11 | plugin: page_title_block 12 | settings: 13 | id: page_title_block 14 | label: 'Page title' 15 | label_display: '0' 16 | provider: core 17 | visibility: {} 18 | -------------------------------------------------------------------------------- /config/install/block.block.dvb_powered.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | module: 5 | - system 6 | theme: 7 | - dvb 8 | id: dvb_powered 9 | theme: dvb 10 | region: footer 11 | weight: -6 12 | provider: null 13 | plugin: system_powered_by_block 14 | settings: 15 | id: system_powered_by_block 16 | label: 'Powered by Drupal' 17 | label_display: '0' 18 | provider: system 19 | visibility: {} 20 | -------------------------------------------------------------------------------- /config/install/block.block.dvb_primary_admin_actions.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | theme: 5 | - dvb 6 | id: dvb_primary_admin_actions 7 | theme: dvb 8 | region: content 9 | weight: -6 10 | provider: null 11 | plugin: local_actions_block 12 | settings: 13 | id: local_actions_block 14 | label: 'Primary admin actions' 15 | label_display: '0' 16 | provider: core 17 | visibility: {} 18 | -------------------------------------------------------------------------------- /config/install/block.block.dvb_primary_local_tasks.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | theme: 5 | - dvb 6 | id: dvb_primary_local_tasks 7 | theme: dvb 8 | region: content 9 | weight: -5 10 | provider: null 11 | plugin: local_tasks_block 12 | settings: 13 | id: local_tasks_block 14 | label: 'Primary tabs' 15 | label_display: '0' 16 | provider: core 17 | primary: true 18 | secondary: false 19 | visibility: {} 20 | -------------------------------------------------------------------------------- /config/install/block.block.dvb_secondary_local_tasks.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | theme: 5 | - dvb 6 | id: dvb_secondary_local_tasks 7 | theme: dvb 8 | region: content 9 | weight: -4 10 | provider: null 11 | plugin: local_tasks_block 12 | settings: 13 | id: local_tasks_block 14 | label: 'Secondary tabs' 15 | label_display: '0' 16 | provider: core 17 | primary: false 18 | secondary: true 19 | visibility: {} 20 | -------------------------------------------------------------------------------- /config/install/block.block.dvb_site_branding.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | module: 5 | - system 6 | theme: 7 | - dvb 8 | id: dvb_site_branding 9 | theme: dvb 10 | region: header 11 | weight: -7 12 | provider: null 13 | plugin: system_branding_block 14 | settings: 15 | id: system_branding_block 16 | label: 'Site branding' 17 | label_display: '0' 18 | provider: system 19 | use_site_logo: true 20 | use_site_name: true 21 | use_site_slogan: true 22 | visibility: {} 23 | -------------------------------------------------------------------------------- /config/install/dvb.settings.yml: -------------------------------------------------------------------------------- 1 | features: 2 | node_user_picture: 0 3 | comment_user_picture: true 4 | comment_user_verification: true 5 | favicon: 1 6 | logo: 7 | use_default: 1 8 | favicon: 9 | use_default: 1 10 | vite: 11 | port: '3000' 12 | dist_path: dist 13 | manifest_path: dist/.vite/manifest.json 14 | -------------------------------------------------------------------------------- /dist/.vite/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "assets/images/close.svg": { 3 | "file": "assets/close.C_jwcm6v.svg", 4 | "src": "assets/images/close.svg" 5 | }, 6 | "assets/images/no-thumbnail.png": { 7 | "file": "assets/no-thumbnail.DCFMKjjA.png", 8 | "src": "assets/images/no-thumbnail.png" 9 | }, 10 | "assets/js/app.js": { 11 | "file": "assets/app-C4TaqamP.js", 12 | "name": "app", 13 | "src": "assets/js/app.js", 14 | "isEntry": true, 15 | "dynamicImports": [ 16 | "assets/js/modules/fontawesome.js", 17 | "assets/js/modules/main-menu.js", 18 | "assets/js/modules/bootstrap.js", 19 | "assets/js/modules/mobile-menu.js", 20 | "assets/js/modules/back-top.js", 21 | "assets/js/modules/jquery-ui.js" 22 | ] 23 | }, 24 | "assets/js/modules/back-top.js": { 25 | "file": "assets/back-top-BbUvcPYO.js", 26 | "name": "back-top", 27 | "src": "assets/js/modules/back-top.js", 28 | "isDynamicEntry": true, 29 | "css": [ 30 | "assets/back-top.DrNfoJkY.css" 31 | ] 32 | }, 33 | "assets/js/modules/bootstrap.js": { 34 | "file": "assets/bootstrap-DAuYFD14.js", 35 | "name": "bootstrap", 36 | "src": "assets/js/modules/bootstrap.js", 37 | "isDynamicEntry": true 38 | }, 39 | "assets/js/modules/fontawesome.js": { 40 | "file": "assets/fontawesome-DZjOfxZp.js", 41 | "name": "fontawesome", 42 | "src": "assets/js/modules/fontawesome.js", 43 | "isDynamicEntry": true 44 | }, 45 | "assets/js/modules/jquery-ui.js": { 46 | "file": "assets/jquery-ui-BWCvzDkq.js", 47 | "name": "jquery-ui", 48 | "src": "assets/js/modules/jquery-ui.js", 49 | "isDynamicEntry": true 50 | }, 51 | "assets/js/modules/main-menu.js": { 52 | "file": "assets/main-menu-BVBh2ugi.js", 53 | "name": "main-menu", 54 | "src": "assets/js/modules/main-menu.js", 55 | "isDynamicEntry": true 56 | }, 57 | "assets/js/modules/mobile-menu.js": { 58 | "file": "assets/mobile-menu-DMP3NAtO.js", 59 | "name": "mobile-menu", 60 | "src": "assets/js/modules/mobile-menu.js", 61 | "isDynamicEntry": true, 62 | "css": [ 63 | "assets/mobile-menu.C_HpnVi1.css" 64 | ] 65 | }, 66 | "assets/scss/app.scss": { 67 | "file": "assets/app.CK3aqZXw.css", 68 | "src": "assets/scss/app.scss", 69 | "isEntry": true 70 | }, 71 | "assets/scss/bootstrap.scss": { 72 | "file": "assets/bootstrap.DSIkgM62.css", 73 | "src": "assets/scss/bootstrap.scss", 74 | "isEntry": true 75 | }, 76 | "assets/scss/ckeditor.scss": { 77 | "file": "assets/ckeditor.css", 78 | "src": "assets/scss/ckeditor.scss", 79 | "isEntry": true 80 | } 81 | } -------------------------------------------------------------------------------- /dist/assets/app-C4TaqamP.js: -------------------------------------------------------------------------------- 1 | function __vite__mapDeps(indexes) { 2 | if (!__vite__mapDeps.viteFileDeps) { 3 | __vite__mapDeps.viteFileDeps = ["assets/mobile-menu-DMP3NAtO.js","assets/mobile-menu.C_HpnVi1.css","assets/back-top-BbUvcPYO.js","assets/back-top.DrNfoJkY.css"] 4 | } 5 | return indexes.map((i) => __vite__mapDeps.viteFileDeps[i]) 6 | } 7 | const E="modulepreload",v=function(e){return"/themes/contrib/dvb/dist/"+e},d={},o=function(f,u,h){let a=Promise.resolve();if(u&&u.length>0){const i=document.getElementsByTagName("link"),t=document.querySelector("meta[property=csp-nonce]"),_=(t==null?void 0:t.nonce)||(t==null?void 0:t.getAttribute("nonce"));a=Promise.all(u.map(n=>{if(n=v(n),n in d)return;d[n]=!0;const s=n.endsWith(".css"),m=s?'[rel="stylesheet"]':"";if(!!h)for(let l=i.length-1;l>=0;l--){const c=i[l];if(c.href===n&&(!s||c.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${n}"]${m}`))return;const r=document.createElement("link");if(r.rel=s?"stylesheet":E,s||(r.as="script",r.crossOrigin=""),r.href=n,_&&r.setAttribute("nonce",_),document.head.appendChild(r),s)return new Promise((l,c)=>{r.addEventListener("load",l),r.addEventListener("error",()=>c(new Error(`Unable to preload CSS for ${n}`)))})}))}return a.then(()=>f()).catch(i=>{const t=new Event("vite:preloadError",{cancelable:!0});if(t.payload=i,window.dispatchEvent(t),!t.defaultPrevented)throw i})};o(()=>import("./fontawesome-DZjOfxZp.js"),[]).then(({default:e})=>{new e});o(()=>import("./main-menu-BVBh2ugi.js"),[]).then(({default:e})=>{new e});o(()=>import("./bootstrap-DAuYFD14.js"),[]).then(({default:e})=>{new e});o(()=>import("./mobile-menu-DMP3NAtO.js"),__vite__mapDeps([0,1])).then(({default:e})=>{new e});o(()=>import("./back-top-BbUvcPYO.js"),__vite__mapDeps([2,3])).then(({default:e})=>{new e});o(()=>import("./jquery-ui-BWCvzDkq.js"),[]).then(({default:e})=>{new e}); 8 | -------------------------------------------------------------------------------- /dist/assets/back-top-BbUvcPYO.js: -------------------------------------------------------------------------------- 1 | class r{constructor(){this.footer=document.querySelector(".site-footer"),this.footer&&(this.inject(),this.toggle(),window.addEventListener("scroll",this.toggle,{passive:!0}))}inject(){this.footer.insertAdjacentHTML("beforebegin",` 2 |
3 | 4 | 5 | 6 |
7 | `),Drupal.behaviors.fa&&Drupal.behaviors.fa.attach(this.footer.parentNode)}toggle(){const t=window.scrollY,o=document.querySelector(".back-to-top"),e=document.querySelector(".site-footer").offsetTop-window.innerHeight+32;o.classList.toggle("show",t>=200||t>=e&&e>0)}}export{r as default}; 8 | -------------------------------------------------------------------------------- /dist/assets/back-top.DrNfoJkY.css: -------------------------------------------------------------------------------- 1 | .back-to-top{position:sticky;z-index:20;bottom:2rem;transition:opacity .2s ease-in-out}.back-to-top a{position:absolute;top:0;right:1rem}.back-to-top .btn{--bs-btn-padding-y: var(--bs-btn-padding-x);--bs-btn-line-height: 1;transform:translateY(-50%)}.back-to-top:not(.show){pointer-events:none;opacity:0} 2 | -------------------------------------------------------------------------------- /dist/assets/ckeditor.css: -------------------------------------------------------------------------------- 1 | .ck-content img{max-width:100%;height:auto;margin-bottom:1rem}.ck-content .align-left{clear:left;margin-right:var(--bs-gutter-x)}.ck-content .align-right{clear:right;margin-left:var(--bs-gutter-x)} 2 | -------------------------------------------------------------------------------- /dist/assets/close.C_jwcm6v.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /dist/assets/jquery-ui-BWCvzDkq.js: -------------------------------------------------------------------------------- 1 | const u=window.jQuery;class a{constructor(){var o;const i=u;(o=i.ui)!=null&&o.dialog&&i.widget("ui.dialog",i.ui.dialog,{open:function(){const n=` 2 | 3 | 4 | Close 5 | `,t=["ui-button","ui-corner-all","ui-widget","ui-button-icon-only","ui-dialog-titlebar-close"];return[...this.uiDialogTitlebarClose].forEach(s=>{s.innerHTML=n,s.classList.add(...t)}),this._super()}})}}export{a as default}; 6 | -------------------------------------------------------------------------------- /dist/assets/mobile-menu-DMP3NAtO.js: -------------------------------------------------------------------------------- 1 | class n{constructor(){this.button=document.getElementById("mobile-nav-button"),this.bind(this.button)}bind(s){const t="is-active",e=document.querySelector(s.dataset.bsTarget);e.addEventListener("show.bs.offcanvas",()=>{s.classList.add(t)}),e.addEventListener("hidden.bs.offcanvas",()=>{s.classList.remove(t)})}}export{n as default}; 2 | -------------------------------------------------------------------------------- /dist/assets/mobile-menu.C_HpnVi1.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Hamburgers 3 | * @description Tasty CSS-animated hamburgers 4 | * @author Jonathan Suh @jonsuh 5 | * @site https://jonsuh.com/hamburgers 6 | * @link https://github.com/jonsuh/hamburgers 7 | */.hamburger{padding:.5rem;display:inline-block;cursor:pointer;transition-property:opacity,filter;transition-duration:.15s;transition-timing-function:linear;font:inherit;color:inherit;text-transform:none;background-color:transparent;border:0;margin:0;overflow:visible}.hamburger:hover,.hamburger.is-active:hover{opacity:.7}.hamburger.is-active .hamburger-inner,.hamburger.is-active .hamburger-inner:before,.hamburger.is-active .hamburger-inner:after{background-color:currentColor}.hamburger-box{width:26px;height:20px;display:inline-block;position:relative}.hamburger-inner{display:block;top:50%;margin-top:-1px}.hamburger-inner,.hamburger-inner:before,.hamburger-inner:after{width:26px;height:2px;background-color:currentColor;border-radius:0;position:absolute;transition-property:transform;transition-duration:.15s;transition-timing-function:ease}.hamburger-inner:before,.hamburger-inner:after{content:"";display:block}.hamburger-inner:before{top:-9px}.hamburger-inner:after{bottom:-9px}.hamburger--squeeze .hamburger-inner{transition-duration:75ms;transition-timing-function:cubic-bezier(.55,.055,.675,.19)}.hamburger--squeeze .hamburger-inner:before{transition:top 75ms .12s ease,opacity 75ms ease}.hamburger--squeeze .hamburger-inner:after{transition:bottom 75ms .12s ease,transform 75ms cubic-bezier(.55,.055,.675,.19)}.hamburger--squeeze.is-active .hamburger-inner{transform:rotate(45deg);transition-delay:.12s;transition-timing-function:cubic-bezier(.215,.61,.355,1)}.hamburger--squeeze.is-active .hamburger-inner:before{top:0;opacity:0;transition:top 75ms ease,opacity 75ms .12s ease}.hamburger--squeeze.is-active .hamburger-inner:after{bottom:0;transform:rotate(-90deg);transition:bottom 75ms ease,transform 75ms .12s cubic-bezier(.215,.61,.355,1)}.hamburger{border-radius:var(--bs-border-radius);line-height:1}.region-mobile-menu{--bs-link-color-rgb: var(--bs-light-rgb);--bs-link-hover-color-rgb: var(--bs-teal-rgb);--bs-border-color: #495057}.region-mobile-menu .menu li{display:grid;grid-template-areas:"link button" "expand expand";grid-template-rows:max-content auto;grid-template-columns:auto min-content;grid-auto-flow:row;gap:0 0}.region-mobile-menu .menu li>a{grid-area:link}.region-mobile-menu .menu li .btn{grid-area:button}.region-mobile-menu .menu li .collapse{grid-area:expand}.region-mobile-menu .menu a{transition:all .2s ease-in-out;text-decoration:none}@media (prefers-reduced-motion: reduce){.region-mobile-menu .menu a{transition:none}}.region-mobile-menu .menu a.is-active,.region-mobile-menu .menu .menu-item--active-trail>a{--bs-link-color: var(--bs-link-hover-color)}.region-mobile-menu .menu .fa-chevron-up{transition:all .2s ease-in-out}@media (prefers-reduced-motion: reduce){.region-mobile-menu .menu .fa-chevron-up{transition:none}}.region-mobile-menu .menu .btn.collapsed .fa-chevron-up{transform:rotate(-180deg)}.region-mobile-menu .block+.block{margin-top:1rem;padding-top:1rem;border-top:1px solid var(--bs-border-color)} 8 | -------------------------------------------------------------------------------- /dist/assets/no-thumbnail.DCFMKjjA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almunnings/drupal-vite-bootstrap/70c5190377e66c666a14139f41b0a53963491e7d/dist/assets/no-thumbnail.DCFMKjjA.png -------------------------------------------------------------------------------- /dvb.breakpoints.yml: -------------------------------------------------------------------------------- 1 | dvb.sm: 2 | label: sm 3 | mediaQuery: 'all and (max-width: 767px)' 4 | weight: 1 5 | multipliers: 6 | - 1x 7 | dvb.md: 8 | label: md 9 | mediaQuery: 'all and (min-width: 768px)' 10 | weight: 2 11 | multipliers: 12 | - 1x 13 | dvb.lg: 14 | label: lg 15 | mediaQuery: 'all and (min-width: 992px)' 16 | weight: 3 17 | multipliers: 18 | - 1x 19 | dvb.xl: 20 | label: xl 21 | mediaQuery: 'all and (min-width: 1200px)' 22 | weight: 4 23 | multipliers: 24 | - 1x 25 | dvb.xxl: 26 | label: xxl 27 | mediaQuery: 'all and (min-width: 1400px)' 28 | weight: 5 29 | multipliers: 30 | - 1x 31 | -------------------------------------------------------------------------------- /dvb.info.yml: -------------------------------------------------------------------------------- 1 | name: Drupal Vite Bootstrap 2 | description: | 3 | A custom theme specifically created for this website. 4 | 5 | type: theme 6 | 'base theme': stable9 7 | starterkit: true 8 | core_version_requirement: ^9.5 || ^10 9 | php: 8.1 10 | 11 | ckeditor5-stylesheets: 12 | - dist/assets/ckeditor.css 13 | 14 | libraries: 15 | - dvb/hmr 16 | - dvb/app 17 | 18 | libraries-override: 19 | gin/ajax: false 20 | gin/dialog: false 21 | gin/media_library: false 22 | layout_discovery/onecol: false 23 | layout_discovery/twocol: false 24 | claro/claro.drupal.dialog: false 25 | webform/webform.composite: false 26 | 27 | regions: 28 | alerts: Alerts 29 | header: Header 30 | primary_menu: 'Primary menu' 31 | secondary_menu: 'Secondary menu' 32 | messages: Messages 33 | breadcrumb: Breadcrumb 34 | highlighted: Highlighted 35 | help: Help 36 | content: Content 37 | sidebar_first: 'Sidebar first' 38 | sidebar_second: 'Sidebar second' 39 | footer: 'Footer' 40 | mobile_menu: 'Mobile menu' 41 | 42 | dependencies: 43 | - core:help 44 | - core:menu_ui 45 | - core:menu_link_content 46 | -------------------------------------------------------------------------------- /dvb.libraries.yml: -------------------------------------------------------------------------------- 1 | # DVB theme library. 2 | 3 | app: 4 | # Modified by Vite, use the original source location. 5 | vite: true 6 | version: 3.0.0 7 | dependencies: 8 | - core/jquery 9 | - core/drupal 10 | - core/drupalSettings 11 | - core/once 12 | css: 13 | theme: 14 | assets/scss/bootstrap.scss: { minified: true, preprocess: false } 15 | assets/scss/app.scss: { minified: true, preprocess: false } 16 | js: 17 | assets/js/app.js: { minified: true, preprocess: false, attributes: { type: 'module' } } 18 | -------------------------------------------------------------------------------- /dvb.theme: -------------------------------------------------------------------------------- 1 | getDefault()) { 29 | $libraries = \Drupal::classResolver(ViteLibrary::class)->alter($libraries); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /includes/blocks.theme: -------------------------------------------------------------------------------- 1 | getRegion(); 21 | } 22 | } 23 | } 24 | 25 | /** 26 | * Implements hook_theme_suggestions_HOOK_alter(). 27 | */ 28 | function dvb_theme_suggestions_block_alter(array &$suggestions, array $variables): void { 29 | if (isset($variables['element']['#id']) && $block = Block::load($variables['elements']['#id'])) { 30 | foreach ($suggestions as $suggestion) { 31 | $suggestions[] = $suggestion . '__region__' . $block->getRegion(); 32 | } 33 | } 34 | } 35 | 36 | /** 37 | * Implements hook_theme_suggestions_HOOK_alter(). 38 | * 39 | * - Add block region theme suggestions for menu. 40 | */ 41 | function dvb_theme_suggestions_menu_alter(array &$suggestions, array $variables): void { 42 | if (isset($variables['attributes']['data-region'])) { 43 | $suggestions[] = 'menu__region__' . $variables['attributes']['data-region']; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /includes/fields.theme: -------------------------------------------------------------------------------- 1 | hasField('field_title')) { 53 | $variables['title'] = $entity->field_title->value; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /includes/forms.theme: -------------------------------------------------------------------------------- 1 | 'details', 23 | '#title' => t('Vite'), 24 | '#open' => TRUE, 25 | '#tree' => TRUE, 26 | ]; 27 | 28 | $form['vite']['developer_mode'] = [ 29 | '#type' => 'checkbox', 30 | '#title' => t('Developer mode'), 31 | '#default_value' => \Drupal::state()->get('vite.developer_mode', FALSE), 32 | '#description' => t('Enable Vite HMR developer mode. Do not use in production.'), 33 | ]; 34 | 35 | $form['vite']['port'] = [ 36 | '#type' => 'textfield', 37 | '#title' => t('Vite port'), 38 | '#default_value' => theme_get_setting('vite.port'), 39 | '#description' => t('The port to listen for Vite.'), 40 | '#required' => TRUE, 41 | ]; 42 | 43 | $form['vite']['dist_path'] = [ 44 | '#type' => 'textfield', 45 | '#title' => t('Vite dist path'), 46 | '#default_value' => theme_get_setting('vite.dist_path'), 47 | '#description' => t('The path to the Vite dist directory.'), 48 | '#required' => TRUE, 49 | ]; 50 | 51 | $form['vite']['manifest_path'] = [ 52 | '#type' => 'textfield', 53 | '#title' => t('Vite manifest Path'), 54 | '#default_value' => theme_get_setting('vite.manifest_path'), 55 | '#description' => t('The path to the Vite manifest.'), 56 | '#required' => TRUE, 57 | ]; 58 | 59 | $form['#submit'][] = 'dvb_form_system_theme_settings_submit'; 60 | } 61 | 62 | /** 63 | * Form submit callback for dvb_form_system_theme_settings_alter(). 64 | */ 65 | function dvb_form_system_theme_settings_submit(array $form, FormStateInterface $form_state): void { 66 | \Drupal::state()->set( 67 | 'vite.developer_mode', 68 | (bool) $form_state->getValue(['vite', 'developer_mode']) 69 | ); 70 | 71 | $form_state->unsetValue(['vite', 'developer_mode']); 72 | 73 | drupal_flush_all_caches(); 74 | } 75 | -------------------------------------------------------------------------------- /includes/views.theme: -------------------------------------------------------------------------------- 1 | id(); 18 | $suggestions[] = 'views_view__' . $view->id() . '__' . $view->current_display; 19 | } 20 | 21 | /** 22 | * Implements hook_theme_suggestions_HOOK_alter(). 23 | */ 24 | function dvb_theme_suggestions_views_view_grouping_alter(array &$suggestions, array $variables): void { 25 | /** @var \Drupal\views\Entity\View $view */ 26 | $view = $variables['view']; 27 | 28 | $suggestions[] = 'views_view_grouping__' . $view->id(); 29 | $suggestions[] = 'views_view_grouping__' . $view->id() . '__' . $view->current_display; 30 | } 31 | 32 | /** 33 | * Implements hook_theme_suggestions_HOOK_alter(). 34 | */ 35 | function dvb_theme_suggestions_views_view_list_alter(array &$suggestions, array $variables): void { 36 | /** @var \Drupal\views\Entity\View $view */ 37 | $view = $variables['view']; 38 | 39 | $suggestions[] = 'views_view_list__' . $view->id(); 40 | $suggestions[] = 'views_view_list__' . $view->id() . '__' . $view->current_display; 41 | } 42 | 43 | /** 44 | * Implements hook_theme_suggestions_HOOK_alter(). 45 | */ 46 | function dvb_theme_suggestions_views_view_unformatted_alter(array &$suggestions, array $variables): void { 47 | /** @var \Drupal\views\Entity\View $view */ 48 | $view = $variables['view']; 49 | 50 | $suggestions[] = 'views_view_unformatted__' . $view->id(); 51 | $suggestions[] = 'views_view_unformatted__' . $view->id() . '__' . $view->current_display; 52 | } 53 | 54 | /** 55 | * Implements hook_theme_suggestions_HOOK_alter(). 56 | */ 57 | function dvb_theme_suggestions_views_view_field_alter(array &$suggestions, array $variables): void { 58 | /** @var \Drupal\views\Entity\View $view */ 59 | $view = $variables['view']; 60 | 61 | /** @var \Drupal\views\Plugin\views\field\EntityField $field */ 62 | $field = $variables['field']; 63 | 64 | $suggestions[] = 'views_view_field__' . $view->id(); 65 | $suggestions[] = 'views_view_field__' . $view->id() . '__field__' . $field->field; 66 | $suggestions[] = 'views_view_field__' . $view->id() . '__' . $view->current_display; 67 | $suggestions[] = 'views_view_field__' . $view->id() . '__' . $view->current_display . '__field__' . $field->field; 68 | } 69 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@almunnings/dvb", 3 | "private": true, 4 | "type": "module", 5 | "version": "0.0.5", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build" 9 | }, 10 | "dependencies": { 11 | "@fortawesome/fontawesome-svg-core": "^6.5.1", 12 | "@fortawesome/free-regular-svg-icons": "^6.5.1", 13 | "@fortawesome/free-solid-svg-icons": "^6.5.1", 14 | "accessible-submenu": "^1.0.2", 15 | "bootstrap": "^5.3.2", 16 | "hamburgers": "^1.2.1" 17 | }, 18 | "devDependencies": { 19 | "@almunnings/eslint-config": "^0.0.6", 20 | "autoprefixer": "^10.4.17", 21 | "sass": "^1.70.0", 22 | "vite": "^5.0.12", 23 | "vite-plugin-eslint": "^1.8.1", 24 | "vite-plugin-externals": "^0.6.2", 25 | "vite-plugin-live-reload": "^3.0.3" 26 | }, 27 | "eslintConfig": { 28 | "extends": [ 29 | "@almunnings/eslint-config/drupal" 30 | ], 31 | "ignorePatterns": [ 32 | "vite.config.js" 33 | ], 34 | "root": true 35 | }, 36 | "stylelint": { 37 | "extends": [ 38 | "@almunnings/eslint-config/style" 39 | ] 40 | }, 41 | "prettier": "@almunnings/eslint-config/prettier" 42 | } 43 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almunnings/drupal-vite-bootstrap/70c5190377e66c666a14139f41b0a53963491e7d/screenshot.png -------------------------------------------------------------------------------- /src/DvbPreRender.php: -------------------------------------------------------------------------------- 1 | get('class_resolver'); 40 | 41 | return new static( 42 | $container->get('theme_handler'), 43 | $resolver->getInstanceFromDefinition(ViteManifest::class), 44 | $resolver->getInstanceFromDefinition(ViteMode::class), 45 | $resolver->getInstanceFromDefinition(ViteServer::class), 46 | ); 47 | } 48 | 49 | /** 50 | * Get dev or manifested file path. 51 | * 52 | * @param string $file 53 | * The file to find. 54 | * @param bool $absolute 55 | * Whether to return the absolute path. 56 | * 57 | * @return string|null 58 | * The path to the file. 59 | */ 60 | public function find(string $file, bool $absolute = FALSE): ?string { 61 | $file = ltrim($file, '/'); 62 | 63 | return $this->mode->developer() 64 | ? $this->server->find($file) 65 | : $this->manifest->find($file, $absolute); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/Vite/ViteLibrary.php: -------------------------------------------------------------------------------- 1 | get('class_resolver'); 36 | 37 | return new static( 38 | $resolver->getInstanceFromDefinition(ViteAsset::class), 39 | $resolver->getInstanceFromDefinition(ViteManifest::class), 40 | $resolver->getInstanceFromDefinition(ViteMode::class), 41 | ); 42 | } 43 | 44 | /** 45 | * Get the Vite altered libraries. 46 | * 47 | * (Optionally) Create the HMR library for development mode. 48 | * 49 | * @return array 50 | * The altered libraries. 51 | */ 52 | public function alter(array $libraries): array { 53 | if ($this->mode->developer()) { 54 | $libraries['hmr'] = [ 55 | 'header' => TRUE, 56 | 'js' => [ 57 | $this->asset->find('@vite/client') => [ 58 | 'type' => 'external', 59 | 'attributes' => [ 60 | 'type' => 'module', 61 | ], 62 | ], 63 | ], 64 | ]; 65 | } 66 | 67 | return array_map( 68 | fn ($library) => $this->map($library), 69 | $libraries 70 | ); 71 | } 72 | 73 | /** 74 | * Replace non-dev asset paths. 75 | * 76 | * @param array $library 77 | * The library to alter. 78 | * 79 | * @return array 80 | * The altered library. 81 | */ 82 | private function map(array $library): array { 83 | if (empty($library['vite'])) { 84 | return $library; 85 | } 86 | 87 | foreach ($library['js'] ?? [] as $file => $options) { 88 | unset($library['js'][$file]); 89 | $library['js'][$this->asset->find($file)] = $options; 90 | 91 | // Find any child css assets that need to load. 92 | // In development mode, Vite client does this. 93 | if ($this->mode->production()) { 94 | $manifest = $this->manifest->decode(); 95 | 96 | foreach ($manifest[$file]['css'] ?? [] as $child) { 97 | $library['css']['theme'][$child] = [ 98 | 'minified' => TRUE, 99 | 'weight' => 10, 100 | 'preprocess' => FALSE, 101 | ]; 102 | } 103 | } 104 | } 105 | 106 | foreach ($library['css'] ?? [] as $category => $css) { 107 | foreach ($css as $file => $options) { 108 | unset($library['css'][$category][$file]); 109 | $library['css'][$category][$this->asset->find($file)] = $options; 110 | } 111 | } 112 | 113 | return $library; 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /src/Vite/ViteManifest.php: -------------------------------------------------------------------------------- 1 | get('theme_handler') 33 | ); 34 | } 35 | 36 | /** 37 | * Decode the manifest contents. 38 | * 39 | * @return array 40 | * The decoded manifest contents. 41 | */ 42 | public function decode(): array { 43 | $manifest = &drupal_static(__CLASS__ . '::' . __METHOD__); 44 | 45 | if (is_null($manifest)) { 46 | $theme = $this->themeHandler->getTheme($this->themeHandler->getDefault()); 47 | $file = $theme->getPath() . '/' . theme_get_setting('vite.manifest_path'); 48 | 49 | $manifest = file_exists($file) 50 | ? Json::decode(file_get_contents($file)) 51 | : []; 52 | } 53 | 54 | return $manifest; 55 | } 56 | 57 | /** 58 | * Get the manifested file path. 59 | * 60 | * @param string $file 61 | * The file to find. 62 | * @param bool $absolute 63 | * Whether to return the absolute path. 64 | * 65 | * @return string|null 66 | * The path to the file. 67 | */ 68 | public function find(string $file, bool $absolute = FALSE): ?string { 69 | $manifest = $this->decode(); 70 | $path = theme_get_setting('vite.dist_path') . '/' . ($manifest[$file]['file'] ?? $file); 71 | 72 | if ($absolute && $path) { 73 | $default_theme = $this->themeHandler->getDefault(); 74 | $theme_path = $this->themeHandler->getTheme($default_theme)->getPath(); 75 | $path = base_path() . $theme_path . '/' . ltrim($path, '/'); 76 | } 77 | 78 | return $path; 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/Vite/ViteMode.php: -------------------------------------------------------------------------------- 1 | get('state') 32 | ); 33 | } 34 | 35 | /** 36 | * Get the state of production mode. 37 | * 38 | * @return bool 39 | * The state of production mode. 40 | */ 41 | public function production(): bool { 42 | return !$this->developer(); 43 | } 44 | 45 | /** 46 | * Get the state of developer mode. 47 | * 48 | * @return bool 49 | * The state of developer mode. 50 | */ 51 | public function developer(): bool { 52 | return (bool) $this->state->get('vite.developer_mode', FALSE); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/Vite/ViteServer.php: -------------------------------------------------------------------------------- 1 | get('http_client') 38 | ); 39 | } 40 | 41 | /** 42 | * Get the URL for the HMR Vite client. 43 | * 44 | * @return string|null 45 | * The URL to the Vite client. 46 | */ 47 | public function find(?string $file = NULL): ?string { 48 | $base_url = $this->getViteBaseUrl(); 49 | 50 | return $file 51 | ? $base_url . '/' . $file 52 | : $base_url; 53 | } 54 | 55 | /** 56 | * Get the Vite development host. 57 | * 58 | * @return string|null 59 | * The hostname of the first accessible internal domain. 60 | */ 61 | protected function getViteBaseUrl(): ?string { 62 | $base_url = &drupal_static(__CLASS__ . '::' . __METHOD__, FALSE); 63 | 64 | if ($base_url === FALSE) { 65 | $base_url = NULL; 66 | 67 | $hosts = array_merge( 68 | $this->lando(), 69 | $this->ddev(), 70 | $this->docker(), 71 | ['localhost' => 'http://localhost:' . theme_get_setting('vite.port')], 72 | ); 73 | 74 | foreach ($hosts as $internal => $external) { 75 | if ($this->connect($internal)) { 76 | $base_url = $external; 77 | break; 78 | } 79 | } 80 | } 81 | 82 | if (!$base_url) { 83 | throw new \Exception('Vite server not running. Run `npm run dev` and clear caches, or disable development mode in the theme settings.'); 84 | } 85 | 86 | return $base_url; 87 | } 88 | 89 | /** 90 | * Get any node services host names from Lando. 91 | * 92 | * @return array 93 | * The host names for node services in Lando. 94 | */ 95 | private function lando(): array { 96 | if (!getenv('LANDO_INFO')) { 97 | return []; 98 | } 99 | 100 | $lando_info = Json::decode(getenv('LANDO_INFO')); 101 | 102 | $lando_services = array_filter( 103 | $lando_info, 104 | fn($service) => $service['type'] === 'node' && !empty($service['urls']) 105 | ); 106 | 107 | $lando_urls = []; 108 | foreach ($lando_services as $service) { 109 | $urls = array_filter($service['urls'], fn($url) => str_starts_with($url, 'https')); 110 | $url = reset($urls) ?: reset($service['urls']); 111 | 112 | $internal = reset($service['hostnames']); 113 | $lando_urls[$internal] = rtrim($url, '/'); 114 | } 115 | 116 | return $lando_urls; 117 | } 118 | 119 | /** 120 | * Get any node services host names from ddev. 121 | * 122 | * @return array 123 | * The host names for node services in ddev. 124 | */ 125 | private function ddev(): array { 126 | if (!getenv('DDEV_HOSTNAME') || !getenv('HTTPS_EXPOSE') || !getenv('HOSTNAME')) { 127 | return []; 128 | } 129 | 130 | // Get the bindings from the DDEV string. 131 | $bindings = array_map( 132 | fn($mapping) => explode(':', $mapping), 133 | explode(',', getenv('HTTPS_EXPOSE')) 134 | ); 135 | 136 | // Find the binding that maps port 3000. 137 | $binding = array_filter( 138 | $bindings, 139 | fn($binding) => $binding[1] === (string) theme_get_setting('vite.port') 140 | ); 141 | 142 | $binding = reset($binding); 143 | 144 | return $binding 145 | ? [getenv('HOSTNAME') => 'https://' . getenv('DDEV_HOSTNAME') . ':' . $binding[0]] 146 | : []; 147 | } 148 | 149 | /** 150 | * Get any docker host names to try. 151 | * 152 | * @return array 153 | * The host names for node services in ddev. 154 | */ 155 | private function docker(): array { 156 | return file_exists('/.dockerenv') 157 | ? ['host.docker.internal' => 'http://localhost:' . theme_get_setting('vite.port')] 158 | : []; 159 | } 160 | 161 | /** 162 | * Try connect to a running Vite service through the internal domain. 163 | * 164 | * @param string $hostname 165 | * The internal domain to check. 166 | * 167 | * @return bool 168 | * TRUE if the domain is responding. 169 | */ 170 | private function connect(string $hostname): bool { 171 | try { 172 | $options = [ 173 | 'timeout' => 0.1, 174 | 'verify' => FALSE, 175 | 'http_errors' => FALSE, 176 | ]; 177 | 178 | return (bool) $this->httpClient->head( 179 | 'http://' . $hostname . ':' . theme_get_setting('vite.port'), 180 | $options 181 | ); 182 | } 183 | catch (ConnectException) { 184 | return FALSE; 185 | } 186 | } 187 | 188 | } 189 | -------------------------------------------------------------------------------- /templates/block/block--local-actions-block.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "block.html.twig" %} 2 | {# 3 | /** 4 | * @file 5 | * Theme override for local actions (primary admin actions.) 6 | */ 7 | #} 8 | {% block content %} 9 | {% if content %} 10 | 11 | {% endif %} 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /templates/block/block--local-tasks-block.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "block.html.twig" %} 2 | {# 3 | /** 4 | * @file 5 | * Theme override for tabs. 6 | */ 7 | #} 8 | {% block content %} 9 | {% if content %} 10 | 13 | {% endif %} 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /templates/block/block--system-branding-block.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "block.html.twig" %} 2 | {# 3 | /** 4 | * @file 5 | * Theme override for a branding block. 6 | * 7 | * Each branding element variable (logo, name, slogan) is only available if 8 | * enabled in the block configuration. 9 | * 10 | * Available variables: 11 | * - site_logo: Logo for site as defined in Appearance or theme settings. 12 | * - site_name: Name for site as defined in Site information settings. 13 | * - site_slogan: Slogan for site as defined in Site information settings. 14 | */ 15 | #} 16 | 17 | {# Fix for embedding on maintenance page #} 18 | {% set site_logo = site_logo ?? logo %} 19 | 20 | {% block content %} 21 | 22 | {% if site_logo or site_name or site_slogan %} 23 | 24 | {% if site_logo %} 25 | 28 | {% endif %} 29 | 30 | {% if site_name or site_slogan %} 31 |
32 | {% if site_name %} 33 |
{{ site_name }}
34 | {% endif %} 35 | {% if site_slogan %} 36 |
{{ site_slogan }}
37 | {% endif %} 38 |
39 | {% endif %} 40 |
41 | {% endif %} 42 | 43 | {% endblock %} 44 | -------------------------------------------------------------------------------- /templates/block/block--system-breadcrumb-block.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'block.html.twig' %} 2 | 3 | {% do attributes.addClass('border-top') %} 4 | 5 | {% block content %} 6 |
7 | {{ content }} 8 |
9 | {% endblock %} -------------------------------------------------------------------------------- /templates/block/block--system-menu-block.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override for a menu block. 5 | * 6 | * Available variables: 7 | * - plugin_id: The ID of the block implementation. 8 | * - label: The configured label of the block if visible. 9 | * - configuration: A list of the block's configuration values. 10 | * - label: The configured label for the block. 11 | * - label_display: The display settings for the label. 12 | * - provider: The module or other provider that provided this block plugin. 13 | * - Block plugin specific settings will also be stored here. 14 | * - content: The content of this block. 15 | * - attributes: HTML attributes for the containing element. 16 | * - id: A valid HTML ID and guaranteed unique. 17 | * - title_attributes: HTML attributes for the title element. 18 | * - content_attributes: HTML attributes for the content element. 19 | * - title_prefix: Additional output populated by modules, intended to be 20 | * displayed in front of the main title tag that appears in the template. 21 | * - title_suffix: Additional output populated by modules, intended to be 22 | * displayed after the main title tag that appears in the template. 23 | * 24 | * Headings should be used on navigation menus that consistently appear on 25 | * multiple pages. When this menu block's label is configured to not be 26 | * displayed, it is automatically made invisible using the 'visually-hidden' CSS 27 | * class, which still keeps it visible for screen-readers and assistive 28 | * technology. Headings allow screen-reader and keyboard only users to navigate 29 | * to or skip the links. 30 | * See http://juicystudio.com/article/screen-readers-display-none.php and 31 | * http://www.w3.org/TR/WCAG-TECHS/H42.html for more information. 32 | */ 33 | #} 34 | {% 35 | set classes = [ 36 | 'block', 37 | 'block-menu', 38 | 'navigation', 39 | 'menu--' ~ derivative_plugin_id|clean_class, 40 | ] 41 | %} 42 | {% set heading_id = attributes.id ~ '-menu'|clean_id %} 43 | 56 | -------------------------------------------------------------------------------- /templates/block/block.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override to display a block. 5 | * 6 | * Available variables: 7 | * - plugin_id: The ID of the block implementation. 8 | * - label: The configured label of the block if visible. 9 | * - configuration: A list of the block's configuration values. 10 | * - label: The configured label for the block. 11 | * - label_display: The display settings for the label. 12 | * - provider: The module or other provider that provided this block plugin. 13 | * - Block plugin specific settings will also be stored here. 14 | * - content: The content of this block. 15 | * - attributes: array of HTML attributes populated by modules, intended to 16 | * be added to the main container tag of this template. 17 | * - id: A valid HTML ID and guaranteed unique. 18 | * - title_attributes: Same as attributes, except applied to the main title 19 | * tag that appears in the template. 20 | * - title_prefix: Additional output populated by modules, intended to be 21 | * displayed in front of the main title tag that appears in the template. 22 | * - title_suffix: Additional output populated by modules, intended to be 23 | * displayed after the main title tag that appears in the template. 24 | * 25 | * @see template_preprocess_block() 26 | */ 27 | #} 28 | {% 29 | set classes = [ 30 | 'block', 31 | 'block-' ~ configuration.provider|clean_class, 32 | 'block-' ~ plugin_id|clean_class, 33 | ] 34 | %} 35 | 36 | 37 | {{ title_prefix }} 38 | 39 | {% block label %} 40 | {% if label %} 41 | {{ label }} 42 | {% endif %} 43 | {% endblock %} 44 | 45 | {{ title_suffix }} 46 | 47 | {% block content %} 48 | {{ content }} 49 | {% endblock %} 50 | 51 | -------------------------------------------------------------------------------- /templates/content-edit/filter-guidelines.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override for guidelines for a text format. 5 | * 6 | * Available variables: 7 | * - format: Contains information about the current text format, including the 8 | * following: 9 | * - name: The name of the text format, potentially unsafe and needs to be 10 | * escaped. 11 | * - format: The machine name of the text format, e.g. 'basic_html'. 12 | * - attributes: HTML attributes for the containing element. 13 | * - tips: Descriptions and a CSS ID in the form of 'module-name/filter-id' 14 | * (only used when 'long' is TRUE) for each filter in one or more text 15 | * formats. 16 | * 17 | * @see template_preprocess_filter_tips() 18 | */ 19 | #} 20 | {% set tips = tips|render %} 21 | 22 | {% if tips|striptags|trim %} 23 | 24 |

{{ format.label }}

25 | {{ tips }} 26 | 27 | {% endif %} 28 | -------------------------------------------------------------------------------- /templates/content/page-title.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override for page titles. 5 | * 6 | * Available variables: 7 | * - title_attributes: HTML attributes for the page title element. 8 | * - title_prefix: Additional output populated by modules, intended to be 9 | * displayed in front of the main title tag that appears in the template. 10 | * - title: The page title, for use in the actual content. 11 | * - title_suffix: Additional output populated by modules, intended to be 12 | * displayed after the main title tag that appears in the template. 13 | */ 14 | #} 15 | {{ title_prefix }} 16 | {% if title %} 17 | {{ title }} 18 | {% endif %} 19 | {{ title_suffix }} 20 | -------------------------------------------------------------------------------- /templates/dataset/item-list.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override for an item list. 5 | * 6 | * Available variables: 7 | * - items: A list of items. Each item contains: 8 | * - attributes: HTML attributes to be applied to each list item. 9 | * - value: The content of the list element. 10 | * - title: The title of the list. 11 | * - list_type: The tag for list element ("ul" or "ol"). 12 | * - wrapper_attributes: HTML attributes to be applied to the list wrapper. 13 | * - attributes: HTML attributes to be applied to the list. 14 | * - empty: A message to display when there are no items. Allowed value is a 15 | * string or render array. 16 | * - context: A list of contextual data associated with the list. May contain: 17 | * - list_style: The custom list style. 18 | * 19 | * @see template_preprocess_item_list() 20 | */ 21 | #} 22 | {% if context.list_style %} 23 | {%- set attributes = attributes.addClass('item-list__' ~ context.list_style) %} 24 | {% endif %} 25 | {% if items or empty %} 26 | {%- if title is not empty -%} 27 |

{{ title }}

28 | {%- endif -%} 29 | 30 | {%- if items -%} 31 | <{{ list_type }}{{ attributes.addClass('list-group') }}> 32 | {%- for item in items -%} 33 | {{ item.value }} 34 | {%- endfor -%} 35 | 36 | {%- else -%} 37 | {{- empty -}} 38 | {%- endif -%} 39 | {%- endif %} 40 | -------------------------------------------------------------------------------- /templates/dataset/table.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override to display a table. 5 | * 6 | * Available variables: 7 | * - attributes: HTML attributes to apply to the tag. 8 | * - caption: A localized string for the tag. 11 | * Note: Drupal currently supports only one table header row, see 12 | * https://www.drupal.org/node/893530 and 13 | * http://api.drupal.org/api/drupal/includes!theme.inc/function/theme_table/7#comment-5109. 14 | * - header: Table header cells. Each cell contains the following properties: 15 | * - tag: The HTML tag name to use; either 'th' or 'td'. 16 | * - attributes: HTML attributes to apply to the tag. 17 | * - content: A localized string for the title of the column. 18 | * - field: Field name (required for column sorting). 19 | * - sort: Default sort order for this column ("asc" or "desc"). 20 | * - sticky: A flag indicating whether to use a "sticky" table header. 21 | * - rows: Table rows. Each row contains the following properties: 22 | * - attributes: HTML attributes to apply to the tag. 23 | * - data: Table cells. 24 | * - no_striping: A flag indicating that the row should receive no 25 | * 'even / odd' styling. Defaults to FALSE. 26 | * - cells: Table cells of the row. Each cell contains the following keys: 27 | * - tag: The HTML tag name to use; either 'th' or 'td'. 28 | * - attributes: Any HTML attributes, such as "colspan", to apply to the 29 | * table cell. 30 | * - content: The string to display in the table cell. 31 | * - active_table_sort: A boolean indicating whether the cell is the active 32 | table sort. 33 | * - footer: Table footer rows, in the same format as the rows variable. 34 | * - empty: The message to display in an extra row if table does not have 35 | * any rows. 36 | * - no_striping: A boolean indicating that the row should receive no striping. 37 | * - header_columns: The number of columns in the header. 38 | * 39 | * @see template_preprocess_table() 40 | */ 41 | #} 42 |
43 | 44 | {% if caption %} 45 |
46 | {% endif %} 47 | 48 | {% for colgroup in colgroups %} 49 | {% if colgroup.cols %} 50 | 51 | {% for col in colgroup.cols %} 52 | 53 | {% endfor %} 54 | 55 | {% else %} 56 | 57 | {% endif %} 58 | {% endfor %} 59 | 60 | {% if header %} 61 | 62 | 63 | {% for cell in header %} 64 | {% 65 | set cell_classes = [ 66 | cell.active_table_sort ? 'active', 67 | cell.active_table_sort ? 'is-active', 68 | ] 69 | %} 70 | <{{ cell.tag }}{{ cell.attributes.addClass(cell_classes) }}> 71 | {{- cell.content -}} 72 | 73 | {% endfor %} 74 | 75 | 76 | {% endif %} 77 | 78 | {% if rows %} 79 | 80 | {% for row in rows %} 81 | {% 82 | set row_classes = [ 83 | not no_striping ? cycle(['odd', 'even'], loop.index0), 84 | ] 85 | %} 86 | 87 | {% for cell in row.cells %} 88 | <{{ cell.tag }}{{ cell.attributes }}> 89 | {{- cell.content -}} 90 | 91 | {% endfor %} 92 | 93 | {% endfor %} 94 | 95 | {% elseif empty %} 96 | 97 | 98 | 99 | 100 | 101 | {% endif %} 102 | {% if footer %} 103 | 104 | {% for row in footer %} 105 | 106 | {% for cell in row.cells %} 107 | <{{ cell.tag }}{{ cell.attributes }}> 108 | {{- cell.content -}} 109 | 110 | {% endfor %} 111 | 112 | {% endfor %} 113 | 114 | {% endif %} 115 |
tag. 9 | * - colgroups: Column groups. Each group contains the following properties: 10 | * - attributes: HTML attributes to apply to the
{{ caption }}
{{ empty }}
116 | -------------------------------------------------------------------------------- /templates/field-group/field-group-accordion-item.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Default theme implementation for a fieldgroup accordion item. 5 | * 6 | * Available variables: 7 | * - title: Title of the group. 8 | * - children: The children of the group. 9 | * - label_attributes: A list of HTML attributes for the label. 10 | * - attributes: A list of HTML attributes for the group wrapper. 11 | * 12 | * @see template_preprocess_field_group_accordion() 13 | * 14 | * @ingroup themeable 15 | */ 16 | #} 17 | 18 | {% set id = element['#group_name'] %} 19 | 20 | 21 | 22 | 32 | 33 | 34 | {% 35 | set body_attributes = create_attribute({ 36 | 'id': id ~ '__body', 37 | 'aria-labelledby': id, 38 | 'class': [ 39 | 'accordion-collapse', 40 | 'collapse', 41 | open ? 'show', 42 | ] 43 | }) 44 | %} 45 | 46 | 47 |
48 | {% if description %}
{% endif %} 49 | {{children}} 50 |
51 | 52 | -------------------------------------------------------------------------------- /templates/field-group/field-group-accordion.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Default theme implementation for a fieldgroup accordion item. 5 | * 6 | * Available variables: 7 | * - children: The children of the group. 8 | * - attributes: A list of HTML attributes for the group wrapper. 9 | * 10 | * @see template_preprocess_field_group_accordion() 11 | * 12 | * @ingroup themeable 13 | */ 14 | #} 15 | 16 |
{{ children }}
17 | -------------------------------------------------------------------------------- /templates/field-group/field-group-html-element--blockquote.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Default theme implementation for a fieldgroup html element. 5 | * 6 | * Available variables: 7 | * - title: Title of the group. 8 | * - title_element: Element to wrap the title. 9 | * - children: The children of the group. 10 | * - wrapper_element: The html element to use 11 | * - attributes: A list of HTML attributes for the group wrapper. 12 | * 13 | * @see template_preprocess_field_group_html_element() 14 | * 15 | * @ingroup themeable 16 | */ 17 | #} 18 | 19 | <{{ wrapper_element }} {{ attributes.addClass('blockquote') }}>{{children}} 20 | -------------------------------------------------------------------------------- /templates/field-group/horizontal-tabs.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Default theme implementation for horizontal tabs. 5 | * 6 | * Available variables 7 | * - attributes: A list of HTML attributes for the wrapper element. 8 | * - children: The rendered children. 9 | * 10 | * @see template_preprocess_horizontal_tabs() 11 | * 12 | * @ingroup themeable 13 | */ 14 | #} 15 |
16 | 17 |
{{ children }}
18 |
19 | 20 | -------------------------------------------------------------------------------- /templates/field/field--comment.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "field.html.twig" %} 2 | 3 | {% 4 | set classes = [ 5 | 'border', 6 | 'rounded', 7 | 'p-4', 8 | 'mt-4', 9 | ] 10 | %} 11 | 12 | {% do attributes.addClass(classes) %} 13 | 14 | {% block content %} 15 | 16 | {% if comments and not label_hidden %} 17 | {{ title_prefix }} 18 | {{ label }} 19 | {{ title_suffix }} 20 | {% endif %} 21 | 22 | {% if comments %} 23 | {{ comments }} 24 | {% endif %} 25 | 26 | {% if comment_form %} 27 |

{{ 'Add new comment'|t }}

28 | {{ comment_form }} 29 | {% endif %} 30 | 31 | 32 | {% endblock %} -------------------------------------------------------------------------------- /templates/field/field--field-description.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "field.html.twig" %} 2 | {% set attributes = attributes.addClass('small', 'text-secondary') %} -------------------------------------------------------------------------------- /templates/field/field--field-title.html.twig: -------------------------------------------------------------------------------- 1 | {% 2 | set classes = [ 3 | 'field--name-' ~ field_name|clean_class, 4 | 'field--type-' ~ field_type|clean_class, 5 | 'field--label-' ~ label_display, 6 | 'h5', 7 | ] 8 | %} 9 | 10 | {% for item in items %} 11 | 12 | {{ item.content }} 13 | 14 | {% endfor %} 15 | -------------------------------------------------------------------------------- /templates/field/field--media--field-media-oembed-video.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'field--media--field-media-video-file.html.twig' %} 2 | -------------------------------------------------------------------------------- /templates/field/field--media--field-media-video-file.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'field.html.twig' %} 2 | 3 | {% 4 | set item_classes = [ 5 | 'ratio', 6 | 'ratio-16x9' 7 | ] 8 | %} 9 | 10 | {% if multiple %} 11 | {% for item in items %} 12 | {% do item.attributes.addClass(item_classes) %} 13 | {% endfor %} 14 | {% else %} 15 | {% do attributes.addClass(item_classes) %} 16 | {% endif %} 17 | -------------------------------------------------------------------------------- /templates/field/field--paragraph--field-citation--quote.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "field--field-description.html.twig" %} 2 | -------------------------------------------------------------------------------- /templates/field/field--paragraph--field-link--quote.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "field--field-description.html.twig" %} 2 | -------------------------------------------------------------------------------- /templates/field/field--text-long.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "field--text.html.twig" %} 2 | -------------------------------------------------------------------------------- /templates/field/field--text-with-summary.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "field--text.html.twig" %} 2 | -------------------------------------------------------------------------------- /templates/field/field--text.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "field.html.twig" %} 2 | {# 3 | /** 4 | * @file 5 | * Default theme implementation for a text field. 6 | * 7 | * A 'text-formatted' and 'ck-content' class is added to assist with default 8 | * styling of base elements such as paragraphs and lists that may not have 9 | * classes assigned to them. This allows user entered content to have default 10 | * styling without interfering with the styles of other UI components such as 11 | * system generated lists or other dynamic content. 12 | * 13 | * @see https://www.drupal.org/node/2539860 14 | * 15 | * @ingroup themeable 16 | */ 17 | #} 18 | {% set attributes = attributes.addClass('text-formatted', 'ck-content') %} 19 | -------------------------------------------------------------------------------- /templates/field/field.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override for a field. 5 | * 6 | * To override output, copy the "field.html.twig" from the templates directory 7 | * to your theme's directory and customize it, just like customizing other 8 | * Drupal templates such as page.html.twig or node.html.twig. 9 | * 10 | * Instead of overriding the theming for all fields, you can also just override 11 | * theming for a subset of fields using 12 | * @link themeable Theme hook suggestions. @endlink For example, 13 | * here are some theme hook suggestions that can be used for a field_foo field 14 | * on an article node type: 15 | * - field--node--field-foo--article.html.twig 16 | * - field--node--field-foo.html.twig 17 | * - field--node--article.html.twig 18 | * - field--field-foo.html.twig 19 | * - field--text-with-summary.html.twig 20 | * - field.html.twig 21 | * 22 | * Available variables: 23 | * - attributes: HTML attributes for the containing element. 24 | * - label_hidden: Whether to show the field label or not. 25 | * - title_attributes: HTML attributes for the title. 26 | * - label: The label for the field. 27 | * - multiple: TRUE if a field can contain multiple items. 28 | * - items: List of all the field items. Each item contains: 29 | * - attributes: List of HTML attributes for each item. 30 | * - content: The field item's content. 31 | * - entity_type: The entity type to which the field belongs. 32 | * - field_name: The name of the field. 33 | * - field_type: The type of the field. 34 | * - label_display: The display settings for the label. 35 | * 36 | * 37 | * @see template_preprocess_field() 38 | */ 39 | #} 40 | {% 41 | set classes = [ 42 | 'field', 43 | 'field--name-' ~ field_name|clean_class, 44 | 'field--type-' ~ field_type|clean_class, 45 | 'field--label-' ~ label_display, 46 | multiple ? 'field--multiple', 47 | label_display == 'inline' ? 'clearfix' 48 | ] 49 | %} 50 | {% 51 | set title_classes = [ 52 | 'field__label', 53 | label_display == 'visually_hidden' ? 'visually-hidden', 54 | ] 55 | %} 56 | 57 | {% block content %} 58 | {% if label_hidden %} 59 | {% if multiple %} 60 | 61 | {% for item in items %} 62 | {{ item.content }} 63 | {% endfor %} 64 | 65 | {% else %} 66 | {% for item in items %} 67 | {{ item.content }} 68 | {% endfor %} 69 | {% endif %} 70 | {% else %} 71 | 72 | {{ label }} 73 | {% if multiple %} 74 |
75 | {% endif %} 76 | {% for item in items %} 77 | {{ item.content }}
78 | {% endfor %} 79 | {% if multiple %} 80 | 81 | {% endif %} 82 | 83 | {% endif %} 84 | {% endblock %} -------------------------------------------------------------------------------- /templates/field/image.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override of an image. 5 | * 6 | * Available variables: 7 | * - attributes: HTML attributes for the img tag. 8 | * - style_name: (optional) The name of the image style applied. 9 | * 10 | * @see template_preprocess_image() 11 | */ 12 | #} 13 | 14 | -------------------------------------------------------------------------------- /templates/form/container--actions.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'container.html.twig' %} 2 | 3 | {% 4 | set classes = [ 5 | has_parent ? 'd-flex', 6 | has_parent ? 'gap-3', 7 | has_parent ? 'align-items-center', 8 | ] 9 | %} 10 | 11 | {% do attributes.addClass(classes) %} 12 | -------------------------------------------------------------------------------- /templates/form/container--media-library-widget-selection.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'container.html.twig' %} 2 | 3 | {% 4 | set classes = [ 5 | 'row', 6 | 'row-cols-2', 7 | 'row-cols-lg-3', 8 | 'row-cols-xl-4', 9 | 'gy-4', 10 | ] 11 | %} 12 | 13 | {% do attributes.addClass(classes) %} 14 | -------------------------------------------------------------------------------- /templates/form/container--text-format-filter-guidelines.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'container.html.twig' %} 2 | 3 | {% 4 | set classes = [ 5 | 'filter-guidelines', 6 | ] 7 | %} 8 | 9 | {% do attributes.addClass(classes) %} 10 | -------------------------------------------------------------------------------- /templates/form/container--text-format-filter-help.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'container.html.twig' %} 2 | 3 | {% 4 | set classes = [ 5 | 'filter-help', 6 | 'float-sm-end', 7 | 'small', 8 | 'ms-sm-2', 9 | 'mb-2', 10 | ] 11 | %} 12 | 13 | {% do attributes.addClass(classes) %} 14 | -------------------------------------------------------------------------------- /templates/form/container--text-format-filter-wrapper.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'container.html.twig' %} 2 | 3 | {% 4 | set classes = [ 5 | 'filter-wrapper', 6 | ] 7 | %} 8 | 9 | {% do attributes.addClass(classes) %} 10 | -------------------------------------------------------------------------------- /templates/form/container.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override of a container used to wrap child elements. 5 | * 6 | * Used for grouped form items. Can also be used as a theme wrapper for any 7 | * renderable element, to surround it with a
and HTML attributes. 8 | * See the @link forms_api_reference.html Form API reference @endlink for more 9 | * information on the #theme_wrappers render array property. 10 | * 11 | * Available variables: 12 | * - attributes: HTML attributes for the containing element. 13 | * - children: The rendered child elements of the container. 14 | * - has_parent: A flag to indicate that the container has one or more parent 15 | containers. 16 | * 17 | * @see template_preprocess_container() 18 | */ 19 | #} 20 | 21 | {% 22 | set classes = [ 23 | has_parent ? 'js-form-wrapper', 24 | has_parent ? 'form-wrapper', 25 | ] 26 | %} 27 | 28 | {# Webform, take the wheel. #} 29 | {% if has_parent and attributes.hasClass('webform') and not attributes.hasClass('webform-flexbox') %} 30 | {% set inline = attributes.hasClass('container-inline') %} 31 | {% 32 | set classes = classes|merge([ 33 | inline ? 'd-flex', 34 | inline ? 'align-items-center', 35 | not inline ? 'd-grid', 36 | 'gap-3', 37 | ]) 38 | %} 39 | {% endif %} 40 | 41 | {# Developer note: With twig debug on, this will always have something in it. #} 42 | {% if children %} 43 | {{ children }}
44 | {% endif %} -------------------------------------------------------------------------------- /templates/form/datetime-form.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override of a datetime form element. 5 | * 6 | * Available variables: 7 | * - attributes: HTML attributes for the datetime form element. 8 | * - content: The datelist form element to be output. 9 | * 10 | * @see template_preprocess_datetime_form() 11 | */ 12 | #} 13 | 14 | {{ content }} 15 | 16 | -------------------------------------------------------------------------------- /templates/form/datetime-wrapper.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override of a datetime form wrapper. 5 | * 6 | * Available variables: 7 | * - content: The form element to be output, usually a datelist, or datetime. 8 | * - title: The title of the form element. 9 | * - title_attributes: HTML attributes for the title wrapper. 10 | * - description: Description text for the form element. 11 | * - required: An indicator for whether the associated form element is required. 12 | * 13 | * @see template_preprocess_datetime_wrapper() 14 | */ 15 | #} 16 | {% 17 | set title_classes = [ 18 | 'form-label', 19 | required ? 'js-form-required', 20 | required ? 'form-required', 21 | ] 22 | %} 23 |
24 | {% if title %} 25 | {{ title }} 26 | {% endif %} 27 | 28 | {{ content }} 29 | 30 | {% if errors %} 31 |
{{ errors }}
32 | {% endif %} 33 | 34 | {% if description %} 35 |
{{ description }}
36 | {% endif %} 37 |
-------------------------------------------------------------------------------- /templates/form/details.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override for a details element. 5 | * 6 | * Available variables 7 | * - attributes: A list of HTML attributes for the details element. 8 | * - errors: (optional) Any errors for this details element, may not be set. 9 | * - title: (optional) The title of the element, may not be set. 10 | * - summary_attributes: A list of HTML attributes for the summary element. 11 | * - description: (optional) The description of the element, may not be set. 12 | * - children: (optional) The children of the element, may not be set. 13 | * - value: (optional) The value of the element, may not be set. 14 | * 15 | * @see template_preprocess_details() 16 | */ 17 | #} 18 | 19 | {%- if title -%} 20 | {% 21 | set summary_classes = [ 22 | required ? 'js-form-required', 23 | required ? 'form-required', 24 | 'form-label', 25 | 'card-header', 26 | ] 27 | %} 28 | 29 | {{ title }} 30 | 31 | 32 | {%- endif -%} 33 |
34 |
35 | {%- if description -%} 36 |
{{ description }}
37 | {%- endif -%} 38 | 39 | {%- if children -%} 40 | {{ children }} 41 | {%- endif -%} 42 | 43 | {%- if value -%} 44 | {{ value }} 45 | {%- endif -%} 46 |
47 | 48 | {% if errors %} 49 |
{{ errors }}
50 | {% endif %} 51 |
52 | 53 | -------------------------------------------------------------------------------- /templates/form/field-multiple-value-form.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override for an individual form element. 5 | * 6 | * Available variables for all fields: 7 | * - multiple: Whether there are multiple instances of the field. 8 | * 9 | * Available variables for single cardinality fields: 10 | * - elements: Form elements to be rendered. 11 | * 12 | * Available variables when there are multiple fields. 13 | * - table: Table of field items. 14 | * - description: The description element containing the following properties: 15 | * - content: The description content of the form element. 16 | * - attributes: HTML attributes to apply to the description container. 17 | * - button: "Add another item" button. 18 | * 19 | * @see template_preprocess_field_multiple_value_form() 20 | */ 21 | #} 22 | {% if multiple %} 23 | {% 24 | set classes = [ 25 | 'js-form-item', 26 | 'form-item', 27 | 'border', 28 | 'rounded', 29 | 'p-3', 30 | ] 31 | %} 32 | 33 | {{ table }} 34 | {% if description.content %} 35 | {{ description.content }} 36 | {% endif %} 37 | {% if button %} 38 |
{{ button }}
39 | {% endif %} 40 | 41 | {% else %} 42 | {% for element in elements %} 43 | {{ element }} 44 | {% endfor %} 45 | {% endif %} 46 | -------------------------------------------------------------------------------- /templates/form/fieldset.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override for a fieldset element and its children. 5 | * 6 | * Available variables: 7 | * - attributes: HTML attributes for the fieldset element. 8 | * - errors: (optional) Any errors for this fieldset element, may not be set. 9 | * - required: Boolean indicating whether the fieldset element is required. 10 | * - legend: The legend element containing the following properties: 11 | * - title: Title of the fieldset, intended for use as the text of the legend. 12 | * - attributes: HTML attributes to apply to the legend. 13 | * - description: The description element containing the following properties: 14 | * - content: The description content of the fieldset. 15 | * - attributes: HTML attributes to apply to the description container. 16 | * - children: The rendered child elements of the fieldset. 17 | * - prefix: The content to add before the fieldset children. 18 | * - suffix: The content to add after the fieldset children. 19 | * 20 | * @see template_preprocess_fieldset() 21 | */ 22 | #} 23 | {% 24 | set classes = [ 25 | 'js-form-item', 26 | 'form-item', 27 | 'js-form-wrapper', 28 | 'form-wrapper', 29 | 'form-group', 30 | 'border', 31 | 'rounded', 32 | 'p-3', 33 | ] 34 | %} 35 | 36 | {% 37 | set legend_span_classes = [ 38 | 'fieldset-legend', 39 | required ? 'js-form-required', 40 | required ? 'form-required', 41 | ] 42 | %} 43 | 44 | {% if legend_span.attributes.hasClass('visually-hidden') %} 45 | {% do legend.attributes.addClass('visually-hidden') %} 46 | {% endif %} 47 | 48 | {# Always wrap fieldset legends in a for CSS positioning. #} 49 | 50 | {{ legend.title }} 51 | 52 | 53 |
54 |
55 | {% if prefix %} 56 |
{{ prefix }}
57 | {% endif %} 58 | 59 | {{ children }} 60 | 61 | {% if suffix %} 62 |
{{ suffix }}
63 | {% endif %} 64 |
65 | 66 | {% if errors %} 67 |
{{ errors }}
68 | {% endif %} 69 | 70 | {% if description.content %} 71 | 72 | {{ description.content }} 73 |
74 | {% endif %} 75 | 76 | 77 | -------------------------------------------------------------------------------- /templates/form/form-element-label--type--checkbox.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override for a form element label. 5 | * 6 | * Available variables: 7 | * - title: The label's text. 8 | * - title_display: Elements title_display setting. 9 | * - required: An indicator for whether the associated form element is required. 10 | * - attributes: A list of HTML attributes for the label. 11 | * 12 | * @see template_preprocess_form_element_label() 13 | */ 14 | #} 15 | {% 16 | set classes = [ 17 | title_display == 'after' ? 'option', 18 | title_display == 'invisible' ? 'visually-hidden', 19 | required ? 'js-form-required', 20 | required ? 'form-required', 21 | 'form-check-label', 22 | ] 23 | %} 24 | 25 | {% if title is not empty or required -%} 26 | {{ title }} 27 | {%- endif %} 28 | -------------------------------------------------------------------------------- /templates/form/form-element-label--type--item.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'form-element-label.html.twig' %} 2 | 3 | {% do attributes.addClass('m-0') %} 4 | -------------------------------------------------------------------------------- /templates/form/form-element-label--type--radio.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override for a form element label. 5 | * 6 | * Available variables: 7 | * - title: The label's text. 8 | * - title_display: Elements title_display setting. 9 | * - required: An indicator for whether the associated form element is required. 10 | * - attributes: A list of HTML attributes for the label. 11 | * 12 | * @see template_preprocess_form_element_label() 13 | */ 14 | #} 15 | {% 16 | set classes = [ 17 | title_display == 'after' ? 'option', 18 | title_display == 'invisible' ? 'visually-hidden', 19 | required ? 'js-form-required', 20 | required ? 'form-required', 21 | 'form-check-label', 22 | ] 23 | %} 24 | 25 | {% if title is not empty or required -%} 26 | {{ title }} 27 | {%- endif %} 28 | -------------------------------------------------------------------------------- /templates/form/form-element-label.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override for a form element label. 5 | * 6 | * Available variables: 7 | * - title: The label's text. 8 | * - title_display: Elements title_display setting. 9 | * - required: An indicator for whether the associated form element is required. 10 | * - attributes: A list of HTML attributes for the label. 11 | * 12 | * @see template_preprocess_form_element_label() 13 | */ 14 | #} 15 | {% 16 | set classes = [ 17 | title_display == 'after' ? 'option', 18 | title_display == 'invisible' ? 'visually-hidden', 19 | required ? 'js-form-required', 20 | required ? 'form-required', 21 | 'form-label', 22 | ] 23 | %} 24 | 25 | {% if title is not empty or required -%} 26 | {{ title }} 27 | {%- endif %} 28 | -------------------------------------------------------------------------------- /templates/form/form-element.html.twig: -------------------------------------------------------------------------------- 1 | {% 2 | set classes = [ 3 | 'js-form-item', 4 | 'form-item', 5 | 'js-form-type-' ~ type|clean_class, 6 | 'form-type-' ~ type|clean_class, 7 | 'js-form-item-' ~ name|clean_class, 8 | 'form-item-' ~ name|clean_class, 9 | type in ['checkbox', 'radio'] ? 'form-check', 10 | type == 'checkbox' ? 'form-switch', 11 | title_display not in ['after', 'before'] ? 'form-no-label', 12 | disabled == 'disabled' ? 'form-disabled', 13 | errors ? 'form-item--error', 14 | ] 15 | %} 16 | 17 | {% 18 | set description_classes = [ 19 | 'description', 20 | 'small', 21 | 'text-muted', 22 | description_display == 'invisible' ? 'visually-hidden', 23 | ] 24 | %} 25 | 26 | {% if label_display in ['before', 'invisible'] %} 27 | {{ label }} 28 | {% endif %} 29 | 30 | {% if description_display == 'before' and description.content %} 31 | 32 | {{ description.content }} 33 | 34 | {% endif %} 35 | 36 | {% set prefix = prefix|render|trim %} 37 | {% set suffix = suffix|render|trim %} 38 | 39 | {% if (prefix is not empty) or (suffix is not empty) %} 40 |
41 | {% endif %} 42 | 43 | {% if prefix is not empty %} 44 |
{{- prefix -}}
45 | {% endif %} 46 | 47 | {{ children }} 48 | 49 | {% if suffix is not empty %} 50 |
{{- suffix -}}
51 | {% endif %} 52 | 53 | {% if (prefix is not empty) or (suffix is not empty) %} 54 |
55 | {% endif %} 56 | 57 | {% if errors %} 58 |
{{ errors }}
59 | {% endif %} 60 | 61 | {% if label_display == 'after' %} 62 | {{ label }} 63 | {% endif %} 64 | 65 | {% if description_display in ['after', 'invisible'] and description.content %} 66 | 67 | {{ description.content }} 68 | 69 | {% endif %} 70 | 71 | -------------------------------------------------------------------------------- /templates/form/form.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override for a 'form' element. 5 | * 6 | * Available variables 7 | * - attributes: A list of HTML attributes for the wrapper element. 8 | * - children: The child elements of the form. 9 | * 10 | * @see template_preprocess_form() 11 | */ 12 | #} 13 | {% 14 | set classes = [ 15 | 'd-grid', 16 | 'gap-3', 17 | ] 18 | %} 19 | 20 | {{ children }} 21 | 22 | -------------------------------------------------------------------------------- /templates/form/input--checkbox.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override for an 'input' #type form element. 5 | * 6 | * Available variables: 7 | * - attributes: A list of HTML attributes for the input element. 8 | * - children: Optional additional rendered elements. 9 | * 10 | * @see template_preprocess_input() 11 | */ 12 | #} 13 | 14 | {% do attributes.setAttribute('role', 'switch') %} 15 | 16 | {% if attributes.hasClass('error') %} 17 | {% do attributes.addClass('is-invalid') %} 18 | {% endif %} 19 | 20 | {{ children }} 21 | -------------------------------------------------------------------------------- /templates/form/input--color.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override for an 'input' #type form element. 5 | * 6 | * Available variables: 7 | * - attributes: A list of HTML attributes for the input element. 8 | * - children: Optional additional rendered elements. 9 | * 10 | * @see template_preprocess_input() 11 | */ 12 | #} 13 | 14 | {% if attributes.hasClass('error') %} 15 | {% do attributes.addClass('is-invalid') %} 16 | {% endif %} 17 | 18 |
19 | {{ children }} 20 |
21 | -------------------------------------------------------------------------------- /templates/form/input--radio.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override for an 'input' #type form element. 5 | * 6 | * Available variables: 7 | * - attributes: A list of HTML attributes for the input element. 8 | * - children: Optional additional rendered elements. 9 | * 10 | * @see template_preprocess_input() 11 | */ 12 | #} 13 | 14 | {% if attributes.hasClass('error') %} 15 | {% do attributes.addClass('is-invalid') %} 16 | {% endif %} 17 | 18 | {{ children }} 19 | -------------------------------------------------------------------------------- /templates/form/input--submit.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override for an 'input' #type form element. 5 | * 6 | * Available variables: 7 | * - attributes: A list of HTML attributes for the input element. 8 | * - children: Optional additional rendered elements. 9 | * 10 | * @see template_preprocess_input() 11 | */ 12 | #} 13 | 14 | {# Only upgrade non-btn defined buttons. #} 15 | {% if not attributes.hasClass('btn') and not attributes.hasClass('no-btn') %} 16 | {% do attributes.addClass('btn') %} 17 | 18 | {% if safe_value_label matches '{(reset)}i' %} 19 | {% do attributes.addClass('btn-outline-danger') %} 20 | {% elseif safe_value_label matches '{(delete|remove)}i' %} 21 | {% do attributes.addClass('btn-danger') %} 22 | {% elseif safe_value_label matches '{(back|close|undo|cancel|previous|discard|revert)}i' %} 23 | {% do attributes.addClass('btn-outline-secondary') %} 24 | {% elseif safe_value_label matches '{(preview)}i' %} 25 | {% do attributes.addClass('btn-outline-primary') %} 26 | {% elseif safe_value_label matches '{^(add|edit)}i' %} 27 | {% do attributes.addClass('btn-outline-secondary', 'btn-sm') %} 28 | {% else %} 29 | {% do attributes.addClass('btn-primary') %} 30 | {% endif %} 31 | {% endif %} 32 | 33 | {# Remove redundant class #} 34 | {% if attributes.hasClass('button--small') and attributes.hasClass('btn') %} 35 | {% do attributes.addClass('btn-sm').removeClass('button--small') %} 36 | {% endif %} 37 | 38 | {# Extra small creates clutter. Resize to sm. #} 39 | {% if attributes.hasClass('button--extrasmall') and attributes.hasClass('btn') %} 40 | {% do attributes.addClass('btn-sm').removeClass('button--extrasmall') %} 41 | {% endif %} 42 | 43 | {% if safe_value_label matches '/{{ safe_value_label | raw }}{{ children }} 46 | {% else %} 47 | {# Leave as input submit, less issues overall/ #} 48 | {{ children }} 49 | {% endif %} -------------------------------------------------------------------------------- /templates/form/input.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override for an 'input' #type form element. 5 | * 6 | * Available variables: 7 | * - attributes: A list of HTML attributes for the input element. 8 | * - children: Optional additional rendered elements. 9 | * 10 | * @see template_preprocess_input() 11 | */ 12 | #} 13 | 14 | {% if attributes.hasClass('error') %} 15 | {% do attributes.addClass('is-invalid') %} 16 | {% endif %} 17 | 18 | {{ children }} 19 | -------------------------------------------------------------------------------- /templates/form/select.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override for a select element. 5 | * 6 | * Available variables: 7 | * - attributes: HTML attributes for the 31 | {% endapply %} 32 | -------------------------------------------------------------------------------- /templates/form/textarea.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override for a 'textarea' #type form element. 5 | * 6 | * Available variables 7 | * - wrapper_attributes: A list of HTML attributes for the wrapper element. 8 | * - attributes: A list of HTML attributes for the textarea element. 9 | * - resizable: An indicator for whether the textarea is resizable. 10 | * - required: An indicator for whether the textarea is required. 11 | * - value: The textarea content. 12 | * 13 | * @see template_preprocess_textarea() 14 | */ 15 | #} 16 | {% 17 | set classes = [ 18 | 'form-textarea', 19 | 'form-control', 20 | resizable ? 'resize-' ~ resizable, 21 | required ? 'required', 22 | ] 23 | %} 24 | 25 | {{ value }} 26 | 27 | -------------------------------------------------------------------------------- /templates/layout/maintenance-page.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override to display a single Drupal page while offline. 5 | * 6 | * All available variables are mirrored in page.html.twig. 7 | * Some may be blank but they are provided for consistency. 8 | * 9 | * @see template_preprocess_maintenance_page() 10 | */ 11 | #} 12 | 17 | 18 |
19 |
20 |
21 | {% if title %} 22 |

{{ title }}

23 | {% endif %} 24 | 25 | {{ page.highlighted }} 26 | 27 | {{ page.content }} 28 |
29 | 30 | {% if page.sidebar_first %} 31 | 34 | {% endif %} 35 | 36 | {% if page.sidebar_second %} 37 | 40 | {% endif %} 41 |
42 |
43 | 44 | {% if page.footer %} 45 |
46 |
47 | {{ page.footer }} 48 |
49 |
50 | {% endif %} 51 | -------------------------------------------------------------------------------- /templates/layout/page.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override to display a single page. 5 | * 6 | * The doctype, html, head and body tags are not in this template. Instead they 7 | * can be found in the html.html.twig template in this directory. 8 | * 9 | * Available variables: 10 | * 11 | * General utility variables: 12 | * - base_path: The base URL path of the Drupal installation. Will usually be 13 | * "/" unless you have installed Drupal in a sub-directory. 14 | * - is_front: A flag indicating if the current page is the front page. 15 | * - logged_in: A flag indicating if the user is registered and signed in. 16 | * - is_admin: A flag indicating if the user has permission to access 17 | * administration pages. 18 | * 19 | * Site identity: 20 | * - front_page: The URL of the front page. Use this instead of base_path when 21 | * linking to the front page. This includes the language domain or prefix. 22 | * 23 | * Page content (in order of occurrence in the default page.html.twig): 24 | * - messages: Status and error messages. Should be displayed prominently. 25 | * - node: Fully loaded node, if there is an automatically-loaded node 26 | * associated with the page and the node ID is the second argument in the 27 | * page's path (e.g. node/12345 and node/12345/revisions, but not 28 | * comment/reply/12345). 29 | * 30 | * Regions: 31 | * - page.header: Items for the header region. 32 | * - page.primary_menu: Items for the primary menu region. 33 | * - page.secondary_menu: Items for the secondary menu region. 34 | * - page.highlighted: Items for the highlighted content region. 35 | * - page.messages: Dynamic status messages. 36 | * - page.help: Dynamic help text, mostly for admin pages. 37 | * - page.content: The main content of the current page. 38 | * - page.sidebar_first: Items for the first sidebar. 39 | * - page.sidebar_second: Items for the second sidebar. 40 | * - page.footer: Items for the footer region. 41 | * - page.breadcrumb: Items for the breadcrumb region. 42 | * 43 | * @see template_preprocess_page() 44 | * @see html.html.twig 45 | */ 46 | #} 47 | 48 | 49 | 50 |
51 | {% if page.alerts %} 52 | {{ page.alerts }} 53 | {% endif %} 54 | 55 | 78 | 79 | 80 | {% if page.primary_menu %} 81 | {{ page.primary_menu }} 82 | {% endif %} 83 | 84 | 85 | {# page.messages is always true. Put borders and padding within blocks. #} 86 | {{ page.messages }} 87 | 88 | 89 | {# page.breadcrumb is always true. Put borders and padding within blocks. #} 90 | {{ page.breadcrumb }} 91 | 92 | 93 | {% if page.highlighted %} 94 | {{ page.highlighted }} 95 | {% endif %} 96 | 97 | 98 | {% if page.primary_menu %} 99 | {{ page.help }} 100 | {% endif %} 101 | 102 | 103 |
104 | 105 | {# link is in html.html.twig #} 106 | 107 | 108 | {# Sidebar layout #} 109 | {% block main %} 110 |
111 |
112 | {{ page.content }} 113 |
114 | 115 | {% if page.sidebar_first %} 116 | 119 | {% endif %} 120 | 121 | {% if page.sidebar_second %} 122 | 125 | {% endif %} 126 |
127 | {% endblock %} 128 |
129 | 130 | 131 | {% if page.footer %} 132 | {% block footer %} 133 |
134 |
135 | {{ page.footer }} 136 |
137 |
138 | {% endblock %} 139 | {% endif %} 140 | 141 |
{# /#page #} 142 | 143 | {% if page.mobile_menu %} 144 | {{ page.mobile_menu }} 145 | {% endif %} 146 | -------------------------------------------------------------------------------- /templates/layout/region.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override to display a region. 5 | * 6 | * Available variables: 7 | * - content: The content for this region, typically blocks. 8 | * - attributes: HTML attributes for the region
. 9 | * - region: The name of the region variable as defined in the theme's 10 | * .info.yml file. 11 | * 12 | * @see template_preprocess_region() 13 | */ 14 | #} 15 | {% 16 | set classes = [ 17 | 'region', 18 | 'region-' ~ region|clean_class, 19 | ] 20 | %} 21 | 22 | {% if content %} 23 | 24 | {{ content }} 25 |
26 | {% endif %} 27 | -------------------------------------------------------------------------------- /templates/media-library/feed-icon.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override for a feed icon. 5 | * 6 | * Available variables: 7 | * - url: An internal system path or a fully qualified external URL of the feed. 8 | * - title: Title of the feed for describing the feed on the subscribe link. 9 | * - attributes: Remaining HTML attributes for the feed link. 10 | * - title: A descriptive title of the feed link. 11 | * - class: HTML classes to be applied to the feed link. 12 | */ 13 | #} 14 | {% do attributes.setAttribute('href', url) %} 15 | {% do attributes.setAttribute('title', title ? 'Subscribe to @title feed'|t({'@title': title}) : 'Subscribe to feed') %} 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /templates/media-library/media--media-library.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override of a media item in the media library. 5 | * 6 | * This is used for media that the user can select from the grid of media 7 | * items. It is not used for items that have already been selected in the 8 | * corresponding field widget, or for items that have been previously selected 9 | * before adding new media to the library. 10 | * 11 | * Available variables: 12 | * - media: The entity with limited access to object properties and methods. 13 | * Only method names starting with "get", "has", or "is" and a few common 14 | * methods such as "id", "label", and "bundle" are available. For example: 15 | * - entity.getEntityTypeId() will return the entity type ID. 16 | * - entity.hasField('field_example') returns TRUE if the entity includes 17 | * field_example. (This does not indicate the presence of a value in this 18 | * field.) 19 | * Calling other methods, such as entity.delete(), will result in an exception. 20 | * See \Drupal\Core\Entity\EntityInterface for a full list of methods. 21 | * - name: Name of the media. 22 | * - content: Media content. 23 | * - title_prefix: Additional output populated by modules, intended to be 24 | * displayed in front of the main title tag that appears in the template. 25 | * - title_suffix: Additional output populated by modules, intended to be 26 | * displayed after the main title tag that appears in the template. 27 | * - view_mode: View mode; for example, "teaser" or "full". 28 | * - attributes: HTML attributes for the containing element. 29 | * - title_attributes: Same as attributes, except applied to the main title 30 | * tag that appears in the template. 31 | * - url: Direct URL of the media. 32 | * - preview_attributes: HTML attributes for the preview wrapper. 33 | * - metadata_attributes: HTML attributes for the expandable metadata area. 34 | * - status: Whether or not the Media is published. 35 | * 36 | * @see template_preprocess_media() 37 | * 38 | * @ingroup themeable 39 | */ 40 | #} 41 | {% 42 | set classes = [ 43 | 'media-library-item__preview-wrapper', 44 | 'border', 45 | 'rounded', 46 | 'bg-light', 47 | 'd-flex', 48 | 'flex-column', 49 | 'overflow-hidden', 50 | ] 51 | %} 52 | 53 | {% 54 | set preview_classes = [ 55 | 'media-library-item__preview', 56 | 'js-media-library-item-preview', 57 | 'w-100', 58 | ] 59 | %} 60 | 61 | 62 | {% if content %} 63 | 64 | {{ content|without('name') }} 65 | 66 | {% if not status %} 67 |
{{ "unpublished" | t }}
68 | {% endif %} 69 | 70 |
71 | {{ name }} 72 |
73 | 74 | {% endif %} 75 | 76 | -------------------------------------------------------------------------------- /templates/misc/progress-bar.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override for a progress bar. 5 | * 6 | * Note that the core Batch API uses this only for non-JavaScript batch jobs. 7 | * 8 | * Available variables: 9 | * - label: The label of the working task. 10 | * - percent: The percentage of the progress. 11 | * - message: A string containing information to be displayed. 12 | */ 13 | #} 14 | 15 | {% if label %} 16 |

{{ label }}

17 | {% endif %} 18 | 19 |
20 |
21 | {{ percent }}% 22 |
23 |
24 | 25 |
26 | {{ message }} 27 |
-------------------------------------------------------------------------------- /templates/misc/status-messages.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Default theme implementation for status messages. 5 | * 6 | * Displays status, error, and warning messages, grouped by type. 7 | * 8 | * An invisible heading identifies the messages for assistive technology. 9 | * Sighted users see a colored box. See http://www.w3.org/TR/WCAG-TECHS/H69.html 10 | * for info. 11 | * 12 | * Add an ARIA label to the contentinfo area so that assistive technology 13 | * user agents will better describe this landmark. 14 | * 15 | * Available variables: 16 | * - message_list: List of messages to be displayed, grouped by type. 17 | * - status_headings: List of all status types. 18 | * - display: (optional) May have a value of 'status' or 'error' when only 19 | * displaying messages of that specific type. 20 | * - attributes: HTML attributes for the element, including: 21 | * - class: HTML classes. 22 | * 23 | * @see template_preprocess_status_messages() 24 | * 25 | * @ingroup themeable 26 | */ 27 | #} 28 |
29 | {% for type, messages in message_list %} 30 | {% 31 | set classes = [ 32 | 'alert', 33 | 'alert-dismissible', 34 | 'fade', 35 | 'show', 36 | 'col-12', 37 | type == 'status' ? 'alert-success', 38 | type == 'warning' ? 'alert-warning', 39 | type == 'error' ? 'alert-danger', 40 | type == 'info' ? 'alert-info', 41 | ] 42 | %} 43 | {% 44 | set role = { 45 | 'status': 'status', 46 | 'error': 'alert', 47 | 'warning': 'alert', 48 | 'info': 'status', 49 | } 50 | %} 51 |
52 | 53 | {% if is_message_with_title or is_message_with_icon %} 54 | {% if is_message_with_title %} 55 |

56 | {{ status_headings[type] }} 57 |

58 | {% endif %} 59 | {% endif %} 60 | {% for message in messages %} 61 | {% if loop.last %} 62 | {{ message }} 63 | {% else %} 64 | {{ message }} 65 |
66 | {% endif %} 67 | {% endfor %} 68 |
69 | {% endfor %} 70 |
71 | -------------------------------------------------------------------------------- /templates/navigation/breadcrumb.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override for a breadcrumb trail. 5 | * 6 | * Available variables: 7 | * - breadcrumb: Breadcrumb trail items. 8 | */ 9 | #} 10 | {% if breadcrumb %} 11 | 32 | {% endif %} 33 | -------------------------------------------------------------------------------- /templates/navigation/links--comment.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'links--node.html.twig' %} -------------------------------------------------------------------------------- /templates/navigation/links--dropbutton.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override for a set of links. 5 | * 6 | * Available variables: 7 | * - attributes: Attributes for the UL containing the list of links. 8 | * - links: Links to be output. 9 | * Each link will have the following elements: 10 | * - link: (optional) A render array that returns a link. See 11 | * template_preprocess_links() for details how it is generated. 12 | * - text: The link text. 13 | * - attributes: HTML attributes for the list item element. 14 | * - text_attributes: (optional) HTML attributes for the span element if no 15 | * 'url' was supplied. 16 | * - heading: (optional) A heading to precede the links. 17 | * - text: The heading text. 18 | * - level: The heading level (e.g. 'h2', 'h3'). 19 | * - attributes: (optional) A keyed list of attributes for the heading. 20 | * If the heading is a string, it will be used as the text of the heading and 21 | * the level will default to 'h2'. 22 | * 23 | * Headings should be used on navigation menus and any list of links that 24 | * consistently appears on multiple pages. To make the heading invisible use 25 | * the 'visually-hidden' CSS class. Do not use 'display:none', which 26 | * removes it from screen readers and assistive technology. Headings allow 27 | * screen reader and keyboard only users to navigate to or skip the links. 28 | * See http://juicystudio.com/article/screen-readers-display-none.php and 29 | * http://www.w3.org/TR/WCAG-TECHS/H42.html for more information. 30 | * 31 | * @see template_preprocess_links() 32 | */ 33 | #} 34 | {% set btn_classes = ['btn', 'btn-sm', 'px-3', 'm-0', 'btn-outline-primary', 'border-0', 'rounded-0', 'text-start', 'text-nowrap'] %} 35 | 36 | {% if links -%} 37 | {%- if heading -%} 38 | {%- if heading.level -%} 39 | <{{ heading.level }}{{ heading.attributes }}>{{ heading.text }} 40 | {%- else -%} 41 | {{ heading.text }} 42 | {%- endif -%} 43 | {%- endif -%} 44 | 45 | {%- for item in links -%} 46 | 47 | {%- if item.link -%} 48 | {%- 49 | set link = item.link|merge({ 50 | '#options': link['#options']|default({})|merge({ 51 | 'attributes': link['#options'].attributes|default({})|merge({ 52 | 'class': link['#options']|default({}).attributes|default({}).class|default([])|merge(btn_classes) 53 | }) 54 | }) 55 | }) 56 | -%} 57 | 58 | {{ link }} 59 | {%- elseif item.text_attributes -%} 60 | {{ item.text }}
61 | {%- else -%} 62 | {%- 63 | set text = item.text|merge({ 64 | '#attributes': item.text['#attributes']|default({})|merge({ 65 | 'class': item.text['#attributes'].class|default([])|merge(btn_classes) 66 | }) 67 | }) 68 | -%} 69 | 70 | {{ text }} 71 | {%- endif -%} 72 | 73 | {%- endfor -%} 74 | 75 | {%- endif %} 76 | -------------------------------------------------------------------------------- /templates/navigation/links--media-library-menu.html.twig: -------------------------------------------------------------------------------- 1 | {% do attributes.addClass('nav', 'nav-tabs', 'mb-4') %} 2 | 3 | {% for item in links %} 4 | {% do item.attributes.addClass('nav-item') %} 5 | {% if 'hidden' in item.link['#options'].attributes.class %} 6 | {% do item.attributes.addClass('visually-hidden') %} 7 | {% endif %} 8 | {% endfor %} 9 | 10 | 11 | {# Build as tabs #} 12 | {% if links -%} 13 | {%- if heading -%} 14 | {%- if heading.level -%} 15 | <{{ heading.level }}{{ heading.attributes }}>{{ heading.text }} 16 | {%- else -%} 17 | {{ heading.text }} 18 | {%- endif -%} 19 | {%- endif -%} 20 | 21 | {%- for item in links -%} 22 | 23 | {% set link_attributes = create_attribute() %} 24 | {% do link_attributes.addClass('nav-link') %} 25 | 26 | {% if item.text_attributes.hasClass('active') %} 27 | {% do link_attributes.setAttribute('aria-current', 'page') %} 28 | {% do link_attributes.addClass('active') %} 29 | {% endif %} 30 | 31 | 32 | {%- if item.link -%} 33 | {{ link(item.link['#title'], item.link['#url'], link_attributes) }} 34 | {%- elseif item.text_attributes -%} 35 | {{ item.text }} 36 | {%- else -%} 37 | {{ item.text }} 38 | {%- endif -%} 39 | 40 | {%- endfor -%} 41 | 42 | {%- endif %} 43 | -------------------------------------------------------------------------------- /templates/navigation/links--node.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'links.html.twig' %} 2 | 3 | {% do attributes.addClass('list-inline', 'small', 'field') %} 4 | 5 | {% for item in links %} 6 | {% do item.attributes.addClass('list-inline-item') %} 7 | {% if 'hidden' in item.link['#options'].attributes.class %} 8 | {% do item.attributes.addClass('visually-hidden') %} 9 | {% endif %} 10 | {% endfor %} 11 | -------------------------------------------------------------------------------- /templates/navigation/menu--region--mobile-menu.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override to display a menu. 5 | * 6 | * Available variables: 7 | * - menu_name: The machine name of the menu. 8 | * - items: A nested list of menu items. Each menu item contains: 9 | * - attributes: HTML attributes for the menu item. 10 | * - below: The menu item child items. 11 | * - title: The menu link title. 12 | * - url: The menu link url, instance of \Drupal\Core\Url 13 | * - localized_options: Menu link localized options. 14 | * - is_expanded: TRUE if the link has visible children within the current 15 | * menu tree. 16 | * - is_collapsed: TRUE if the link has children within the current menu tree 17 | * that are not currently visible. 18 | * - in_active_trail: TRUE if the link is in the active trail. 19 | */ 20 | #} 21 | {% import _self as menus %} 22 | 23 | {# 24 | We call a macro which calls itself to render the full tree. 25 | @see https://twig.symfony.com/doc/1.x/tags/macro.html 26 | #} 27 | {{ menus.menu_links(items, attributes, 0) }} 28 | 29 | {% macro menu_links(items, attributes, menu_level) %} 30 | {% import _self as menus %} 31 | {% if items %} 32 | {% if menu_level == 0 %} 33 | 34 | {% else %} 35 |
    36 | {% endif %} 37 | {% for item in items %} 38 | {% 39 | set classes = [ 40 | item.is_expanded ? 'menu-item--expanded', 41 | item.is_collapsed ? 'menu-item--collapsed', 42 | item.in_active_trail ? 'menu-item--active-trail', 43 | ] 44 | %} 45 | 46 | {% set collapse_id = 'mobile-menu-' ~ item.original_link.pluginId | clean_id %} 47 | 48 | 49 | 50 | {% 51 | set link_classes = [ 52 | 'text-break', 53 | 'py-2', 54 | menu_level > 0 ? 'small' 55 | ] 56 | %} 57 | 58 | {{ link(item.title, item.url, {'class': link_classes }) }} 59 | 60 | {% if item.below %} 61 | {% 62 | set button_classes = [ 63 | 'btn', 64 | 'btn-dark', 65 | 'border-0', 66 | 'py-0', 67 | 'ms-2', 68 | not item.in_active_trail ? 'collapsed', 69 | ] 70 | %} 71 | 81 | {% endif %} 82 | 83 | 84 | 85 | {% if item.below %} 86 |
    87 | {{ menus.menu_links(item.below, attributes, menu_level + 1) }} 88 |
    89 | {% endif %} 90 | 91 | 92 | {% endfor %} 93 |
94 | {% endif %} 95 | {% endmacro %} 96 | -------------------------------------------------------------------------------- /templates/navigation/menu--region--primary-menu.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override to display a menu. 5 | * 6 | * Available variables: 7 | * - menu_name: The machine name of the menu. 8 | * - items: A nested list of menu items. Each menu item contains: 9 | * - attributes: HTML attributes for the menu item. 10 | * - below: The menu item child items. 11 | * - title: The menu link title. 12 | * - url: The menu link url, instance of \Drupal\Core\Url 13 | * - localized_options: Menu link localized options. 14 | * - is_expanded: TRUE if the link has visible children within the current 15 | * menu tree. 16 | * - is_collapsed: TRUE if the link has children within the current menu tree 17 | * that are not currently visible. 18 | * - in_active_trail: TRUE if the link is in the active trail. 19 | */ 20 | #} 21 | {% import _self as menus %} 22 | 23 | {% if items %} 24 | 25 | {{ menus.level_one(items) }} 26 | 27 | {% endif %} 28 | 29 | 30 | {% macro level_one(items) %} 31 | {% import _self as menus %} 32 | 79 | {% endmacro %} 80 | 81 | 82 | {% macro level_two(items) %} 83 | {% import _self as menus %} 84 |
    85 | {% for item in items %} 86 | {% 87 | set classes = [ 88 | item.is_expanded ? 'menu-item--expanded', 89 | item.is_collapsed ? 'menu-item--collapsed', 90 | item.in_active_trail ? 'menu-item--active-trail', 91 | ] 92 | %} 93 | 94 | {% 95 | set link_classes = [ 96 | 'd-flex', 97 | 'w-100', 98 | 'border-bottom', 99 | 'py-2', 100 | ] 101 | %} 102 | 103 | {% set link_text -%} 104 | {{ item.title }} 105 | 106 | {% endset %} 107 | 108 | 109 | {{ link(link_text, item.url, {'class': link_classes }) }} 110 | 111 | {%- if item.below -%} 112 | {{ menus.level_three(item.below) }} 113 | {%- endif -%} 114 | 115 | {% endfor %} 116 |
117 | {% endmacro %} 118 | 119 | 120 | {% macro level_three(items) %} 121 |
    122 | {% for item in items %} 123 | {% 124 | set classes = [ 125 | 'my-2', 126 | item.is_expanded ? 'menu-item--expanded', 127 | item.is_collapsed ? 'menu-item--collapsed', 128 | item.in_active_trail ? 'menu-item--active-trail', 129 | ] 130 | %} 131 | 132 | 133 | {{ link(item.title, item.url) }} 134 | 135 | {% endfor %} 136 |
137 | {% endmacro %} 138 | -------------------------------------------------------------------------------- /templates/navigation/menu--region--secondary-menu.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override to display a menu. 5 | * 6 | * Available variables: 7 | * - menu_name: The machine name of the menu. 8 | * - items: A nested list of menu items. Each menu item contains: 9 | * - attributes: HTML attributes for the menu item. 10 | * - below: The menu item child items. 11 | * - title: The menu link title. 12 | * - url: The menu link url, instance of \Drupal\Core\Url 13 | * - localized_options: Menu link localized options. 14 | * - is_expanded: TRUE if the link has visible children within the current 15 | * menu tree. 16 | * - is_collapsed: TRUE if the link has children within the current menu tree 17 | * that are not currently visible. 18 | * - in_active_trail: TRUE if the link is in the active trail. 19 | */ 20 | #} 21 | {% if items %} 22 | 23 | {% for item in items %} 24 | {% 25 | set classes = [ 26 | 'nav-item', 27 | item.is_expanded ? 'menu-item--expanded', 28 | item.is_collapsed ? 'menu-item--collapsed', 29 | item.in_active_trail ? 'menu-item--active-trail', 30 | ] 31 | %} 32 | {% 33 | set link_classes = [ 34 | 'nav-link', 35 | item.in_active_trail ? 'active', 36 | ] 37 | %} 38 | 39 | {{ link(item.title, item.url, {'class': link_classes }) }} 40 | 41 | {% endfor %} 42 | 43 | {% endif %} 44 | 45 | -------------------------------------------------------------------------------- /templates/navigation/menu-local-task.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override for a local task link. 5 | * 6 | * Available variables: 7 | * - attributes: HTML attributes for the wrapper element. 8 | * - is_active: Whether the task item is an active tab. 9 | * - link: A rendered link element. 10 | * 11 | * Note: This template renders the content for each task item in 12 | * menu-local-tasks.html.twig. 13 | * 14 | * @see template_preprocess_menu_local_task() 15 | */ 16 | #} 17 | 18 | {% set link_attributes = create_attribute() %} 19 | 20 | {% 21 | do link_attributes.addClass([ 22 | 'nav-link', 23 | is_active ? 'active', 24 | link['#url'].getOption('attributes').class ?: [] | join(' '), 25 | ]) 26 | %} 27 | 28 | {% if is_active %} 29 | {% do link_attributes.setAttribute('aria-current', 'page') %} 30 | {% endif %} 31 | 32 | 33 | {{ link(link['#title'], link['#url'], link_attributes) }} 34 | 35 | -------------------------------------------------------------------------------- /templates/navigation/menu-local-tasks.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override to display primary and secondary local tasks. 5 | * 6 | * Available variables: 7 | * - primary: HTML list items representing primary tasks. 8 | * - secondary: HTML list items representing secondary tasks. 9 | * 10 | * Each item in these variables (primary and secondary) can be individually 11 | * themed in menu-local-task.html.twig. 12 | */ 13 | #} 14 | {% if primary %} 15 |

{{ 'Primary tabs'|t }}

16 | 17 | {% endif %} 18 | {% if secondary %} 19 |

{{ 'Secondary tabs'|t }}

20 | 21 | {% endif %} 22 | -------------------------------------------------------------------------------- /templates/regions/region--alerts.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'region.html.twig' %} 2 | 3 | {% 4 | set classes = [ 5 | 'bg-warning', 6 | ] 7 | %} 8 | 9 | {% do attributes.addClass(classes) %} 10 | {% do attributes.setAttribute('role', 'alert') %} 11 | 12 | 13 | {% block content %} 14 |
15 |
16 |
17 |
18 | {{ content }} 19 |
20 |
21 |
22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /templates/regions/region--breadcrumb.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'region.html.twig' %} 2 | 3 | {% 4 | set classes = [ 5 | 'd-none', 6 | 'd-sm-block', 7 | 'bg-light', 8 | ] 9 | %} 10 | 11 | {% do attributes.addClass(classes) %} -------------------------------------------------------------------------------- /templates/regions/region--content.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'region.html.twig' %} 2 | 3 | {% 4 | set classes = [ 5 | 'd-grid', 6 | 'gap-3', 7 | ] 8 | %} 9 | 10 | {% do attributes.addClass(classes) %} -------------------------------------------------------------------------------- /templates/regions/region--help.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'region.html.twig' %} 2 | 3 | {% 4 | set classes = [ 5 | 'container', 6 | ] 7 | %} 8 | 9 | {% do attributes.addClass(classes) %} 10 | {% do attributes.setAttribute('role', 'alert') %} 11 | 12 | 13 | {% block content %} 14 |
15 |
16 | {{ content }} 17 |
18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /templates/regions/region--mobile-menu.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'region.html.twig' %} 2 | 3 | {% 4 | set classes = [ 5 | 'offcanvas', 6 | 'offcanvas-end', 7 | 'bg-dark', 8 | 'text-light', 9 | ] 10 | %} 11 | 12 | {% do attributes.addClass(classes) %} 13 | {% do attributes.setAttribute('tabindex', '-1') %} 14 | {% do attributes.setAttribute('id', 'mobile-menu') %} 15 | {% do attributes.setAttribute('aria-labelledby', 'mobile-menu-label') %} 16 | {% do attributes.setAttribute('hidden', 'hidden') %} 17 | 18 | {% block content %} 19 |
20 |
{{ 'Menu'|t }}
21 | 22 |
23 |
24 | {{ content }} 25 |
26 | {% endblock %} -------------------------------------------------------------------------------- /templates/regions/region--primary-menu.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'region.html.twig' %} 2 | 3 | {% 4 | set classes = [ 5 | 'd-none', 6 | 'd-md-block', 7 | 'bg-light', 8 | 'position-relative', 9 | ] 10 | %} 11 | 12 | {% do attributes.addClass(classes) %} -------------------------------------------------------------------------------- /templates/regions/region.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override to display a region. 5 | * 6 | * Available variables: 7 | * - content: The content for this region, typically blocks. 8 | * - attributes: HTML attributes for the region
. 9 | * - region: The name of the region variable as defined in the theme's 10 | * .info.yml file. 11 | * 12 | * @see template_preprocess_region() 13 | */ 14 | #} 15 | {% 16 | set classes = [ 17 | 'region', 18 | 'region-' ~ region|clean_class, 19 | ] 20 | %} 21 | 22 | {% if content %} 23 | 24 | {% block content %} 25 | {{ content }} 26 | {% endblock %} 27 |
28 | {% endif %} -------------------------------------------------------------------------------- /templates/user/user.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override to present all user data. 5 | * 6 | * This template is used when viewing a registered user's page, 7 | * e.g., example.com/user/123. 123 being the user's ID. 8 | * 9 | * Available variables: 10 | * - content: A list of content items. Use 'content' to print all content, or 11 | * print a subset such as 'content.field_example'. Fields attached to a user 12 | * such as 'user_picture' are available as 'content.user_picture'. 13 | * - attributes: HTML attributes for the container element. 14 | * - user: A Drupal User entity. 15 | * 16 | * @see template_preprocess_user() 17 | */ 18 | #} 19 | 20 | {% if content %} 21 | {{- content -}} 22 | {% endif %} 23 | 24 | -------------------------------------------------------------------------------- /templates/views/views-exposed-form.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override for a views exposed form. 5 | * 6 | * Available variables: 7 | * - form: A render element representing the form. 8 | * 9 | * @see template_preprocess_views_exposed_form() 10 | */ 11 | #} 12 | {% if q is not empty %} 13 | {# 14 | This ensures that, if clean URLs are off, the 'q' is added first, 15 | as a hidden form element, so that it shows up first in the POST URL. 16 | #} 17 | {{ q }} 18 | {% endif %} 19 |
20 | {{ form }} 21 |
22 | -------------------------------------------------------------------------------- /templates/views/views-mini-pager.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override for a views mini-pager. 5 | * 6 | * Available variables: 7 | * - heading_id: Pagination heading ID. 8 | * - items: List of pager items. 9 | * 10 | * @see template_preprocess_views_mini_pager() 11 | */ 12 | #} 13 | {% if items.previous or items.next %} 14 | 44 | {% endif %} 45 | -------------------------------------------------------------------------------- /templates/views/views-view-table.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override for displaying a view as a table. 5 | * 6 | * Available variables: 7 | * - attributes: Remaining HTML attributes for the element. 8 | * - class: HTML classes that can be used to style contextually through CSS. 9 | * - title : The title of this group of rows. 10 | * - header: The table header columns. 11 | * - attributes: Remaining HTML attributes for the element. 12 | * - content: HTML classes to apply to each header cell, indexed by 13 | * the header's key. 14 | * - default_classes: A flag indicating whether default classes should be 15 | * used. 16 | * - caption_needed: Is the caption tag needed. 17 | * - caption: The caption for this table. 18 | * - accessibility_description: Extended description for the table details. 19 | * - accessibility_summary: Summary for the table details. 20 | * - rows: Table row items. Rows are keyed by row number. 21 | * - attributes: HTML classes to apply to each row. 22 | * - columns: Row column items. Columns are keyed by column number. 23 | * - attributes: HTML classes to apply to each column. 24 | * - content: The column content. 25 | * - default_classes: A flag indicating whether default classes should be 26 | * used. 27 | * - responsive: A flag indicating whether table is responsive. 28 | * - sticky: A flag indicating whether table header is sticky. 29 | * - summary_element: A render array with table summary information (if any). 30 | * 31 | * @see template_preprocess_views_view_table() 32 | */ 33 | #} 34 | {% 35 | set classes = [ 36 | 'table', 37 | 'views-table', 38 | 'views-view-table', 39 | 'cols-' ~ header|length, 40 | responsive ? 'responsive-enabled', 41 | sticky ? 'sticky-enabled', 42 | ] 43 | %} 44 |
45 | 46 | {% if caption_needed %} 47 | 48 | {% if caption %} 49 | {{ caption }} 50 | {% else %} 51 | {{ title }} 52 | {% endif %} 53 | {% if (summary_element is not empty) %} 54 | {{ summary_element }} 55 | {% endif %} 56 | 57 | {% endif %} 58 | {% if header %} 59 | 60 | 61 | {% for key, column in header %} 62 | {% if column.default_classes %} 63 | {% 64 | set column_classes = [ 65 | 'views-field', 66 | 'views-field-' ~ fields[key], 67 | ] 68 | %} 69 | {% endif %} 70 | 71 | {%- if column.wrapper_element -%} 72 | <{{ column.wrapper_element }}> 73 | {%- if column.url -%} 74 | {{ column.content }}{{ column.sort_indicator }} 75 | {%- else -%} 76 | {{ column.content }}{{ column.sort_indicator }} 77 | {%- endif -%} 78 | 79 | {%- else -%} 80 | {%- if column.url -%} 81 | {{ column.content }}{{ column.sort_indicator }} 82 | {%- else -%} 83 | {{- column.content }}{{ column.sort_indicator }} 84 | {%- endif -%} 85 | {%- endif -%} 86 | 87 | {% endfor %} 88 | 89 | 90 | {% endif %} 91 | 92 | {% for row in rows %} 93 | 94 | {% for key, column in row.columns %} 95 | {% if column.default_classes %} 96 | {% 97 | set column_classes = [ 98 | 'views-field' 99 | ] 100 | %} 101 | {% for field in column.fields %} 102 | {% set column_classes = column_classes|merge(['views-field-' ~ field]) %} 103 | {% endfor %} 104 | {% endif %} 105 | 106 | {%- if column.wrapper_element -%} 107 | <{{ column.wrapper_element }}> 108 | {% for content in column.content %} 109 | {{ content.separator }}{{ content.field_output }} 110 | {% endfor %} 111 | 112 | {%- else -%} 113 | {% for content in column.content %} 114 | {{- content.separator }}{{ content.field_output -}} 115 | {% endfor %} 116 | {%- endif %} 117 | 118 | {% endfor %} 119 | 120 | {% endfor %} 121 | 122 | 123 |
-------------------------------------------------------------------------------- /templates/views/views-view-unformatted--media-library.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override to display a view of unformatted rows. 5 | * 6 | * Available variables: 7 | * - title: The title of this group of rows. May be empty. 8 | * - rows: A list of the view's row items. 9 | * - attributes: The row's HTML attributes. 10 | * - content: The row's content. 11 | * - view: The view object. 12 | * - default_row_class: A flag indicating whether default classes should be 13 | * used on rows. 14 | * 15 | * @see template_preprocess_views_view_unformatted() 16 | */ 17 | #} 18 | {% if title %} 19 |

{{ title }}

20 | {% endif %} 21 | 22 |
23 | {% for row in rows %} 24 | {% 25 | set row_classes = [ 26 | default_row_class ? 'views-row', 27 | 'media-library-item', 28 | 'col', 29 | ] 30 | %} 31 | 32 | {{- row.content -}} 33 |
34 | {% endfor %} 35 | 36 | -------------------------------------------------------------------------------- /templates/views/views-view-unformatted.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override to display a view of unformatted rows. 5 | * 6 | * Available variables: 7 | * - title: The title of this group of rows. May be empty. 8 | * - rows: A list of the view's row items. 9 | * - attributes: The row's HTML attributes. 10 | * - content: The row's content. 11 | * - view: The view object. 12 | * - default_row_class: A flag indicating whether default classes should be 13 | * used on rows. 14 | * 15 | * @see template_preprocess_views_view_unformatted() 16 | */ 17 | #} 18 | {% if title %} 19 |

{{ title }}

20 | {% endif %} 21 | 22 | {% for row in rows %} 23 | {% 24 | set row_classes = [ 25 | default_row_class ? 'views-row', 26 | ] 27 | %} 28 | 29 | {{- row.content -}} 30 | 31 | {% endfor %} 32 | -------------------------------------------------------------------------------- /templates/views/views-view.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme override for a main view template. 5 | * 6 | * Available variables: 7 | * - attributes: Remaining HTML attributes for the element. 8 | * - css_name: A css-safe version of the view name. 9 | * - css_class: The user-specified classes names, if any. 10 | * - header: The optional header. 11 | * - footer: The optional footer. 12 | * - rows: The results of the view query, if any. 13 | * - empty: The content to display if there are no rows. 14 | * - pager: The optional pager next/prev links to display. 15 | * - exposed: Exposed widget form/info to display. 16 | * - feed_icons: Optional feed icons to display. 17 | * - more: An optional link to the next page of results. 18 | * - title: Title of the view, only used when displaying in the admin preview. 19 | * - title_prefix: Additional output populated by modules, intended to be 20 | * displayed in front of the view title. 21 | * - title_suffix: Additional output populated by modules, intended to be 22 | * displayed after the view title. 23 | * - attachment_before: An optional attachment view to be displayed before the 24 | * view content. 25 | * - attachment_after: An optional attachment view to be displayed after the 26 | * view content. 27 | * - dom_id: Unique id for every view being printed to give unique class for 28 | * Javascript. 29 | * 30 | * @see template_preprocess_views_view() 31 | */ 32 | #} 33 | {% 34 | set classes = [ 35 | 'view', 36 | 'view-' ~ id|clean_class, 37 | 'view-id-' ~ id, 38 | 'view-display-id-' ~ display_id, 39 | dom_id ? 'js-view-dom-id-' ~ dom_id, 40 | ] 41 | %} 42 | 43 | 44 | {% block title %} 45 | {{ title_prefix }} 46 | {% if title %} 47 | {{ title }} 48 | {% endif %} 49 | {{ title_suffix }} 50 | {% endblock %} 51 | 52 | {% block header %} 53 | {% if header %} 54 |
55 | {{ header }} 56 |
57 | {% endif %} 58 | {% endblock %} 59 | 60 | {% block exposed %} 61 | {% if exposed %} 62 |
63 | {{ exposed }} 64 |
65 | {% endif %} 66 | {% endblock %} 67 | 68 | {% block attachment_before %} 69 | {% if attachment_before %} 70 |
71 | {{ attachment_before }} 72 |
73 | {% endif %} 74 | {% endblock %} 75 | 76 | {% block rows %} 77 | {% if rows %} 78 |
79 | {{ rows }} 80 |
81 | {% elseif empty %} 82 |
83 | {{ empty }} 84 |
85 | {% endif %} 86 | {% endblock %} 87 | 88 | {% block pager %} 89 | {% if pager %} 90 | {{ pager }} 91 | {% endif %} 92 | {% endblock %} 93 | 94 | {% block attachment_after %} 95 | {% if attachment_after %} 96 |
97 | {{ attachment_after }} 98 |
99 | {% endif %} 100 | {% endblock %} 101 | 102 | {% block more %} 103 | {% if more %} 104 | {{ more }} 105 | {% endif %} 106 | {% endblock %} 107 | 108 | 109 | {% block footer %} 110 | {% if footer %} 111 | 114 | {% endif %} 115 | {% endblock %} 116 | 117 | 118 | {% block feed_icons %} 119 | {% if feed_icons %} 120 |
121 | {{ feed_icons }} 122 |
123 | {% endif %} 124 | {% endblock %} 125 | 126 | -------------------------------------------------------------------------------- /templates/webform/webform-submission-data.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Default theme implementation for webform submission data. 5 | * 6 | * Available variables: 7 | * - webform_submission: The webform submission. 8 | * - webform: The webform. 9 | * 10 | * @see template_preprocess_webform_submission_data() 11 | * 12 | * @ingroup themeable 13 | */ 14 | #} 15 | {% 16 | set classes = [ 17 | 'webform-submission-data', 18 | 'webform-submission-data--webform-' ~ webform.id()|clean_class, 19 | view_mode ? 'webform-submission-data--view-mode-' ~ view_mode|clean_class, 20 | 'd-grid', 21 | 'gap-3', 22 | ] 23 | %} 24 | 25 | {{ elements }} 26 | 27 | -------------------------------------------------------------------------------- /templates/webform/webform.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Theme implementation for a 'webform' element. 5 | * 6 | * This is an copy of the webform.html.twig theme_wrapper which includes the 7 | * 'title_prefix' and 'title_suffix' variables needed for 8 | * contextual links to appear. 9 | * 10 | * Available variables 11 | * - attributes: A list of HTML attributes for the wrapper element. 12 | * - children: The child elements of the webform. 13 | * - title_prefix: Additional output populated by modules, intended to be 14 | * displayed in front of the main title tag that appears in the template. 15 | * - title_suffix: Additional output populated by modules, intended to be 16 | * displayed after the main title tag that appears in the template. 17 | * 18 | * @see template_preprocess_webform() 19 | * @see _webform_form_after_build() 20 | * 21 | * @ingroup themeable 22 | */ 23 | #} 24 | {% 25 | set classes = [ 26 | 'd-grid', 27 | 'gap-3', 28 | ] 29 | %} 30 | 31 | {{ title_prefix }} 32 | {{ children }} 33 | {{ title_suffix }} 34 | 35 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import YAML from 'yaml' 3 | import path from 'path' 4 | import { defineConfig, loadEnv } from 'vite' 5 | import autoprefixer from 'autoprefixer' 6 | import eslint from 'vite-plugin-eslint' 7 | import liveReload from 'vite-plugin-live-reload' 8 | import { viteExternalsPlugin } from 'vite-plugin-externals' 9 | 10 | // Resolve dirs. 11 | const pwd = path.resolve(__dirname, '.') 12 | const drupalPath = path.resolve(__dirname, '../../../../') 13 | const themePath = pwd.match(/\/themes\/[^\/]+\/[^\/]+/i) 14 | const basePath = `${themePath ? themePath[0] : ''}/dist/` 15 | 16 | // Extract YML as object. 17 | const yaml = (filename) => { 18 | const path = fs.readdirSync(pwd).find(fn => fn.endsWith(filename)); 19 | return YAML.parse(fs.readFileSync(path, 'utf8')) 20 | } 21 | 22 | // Find any library with vite: true 23 | const librariesYaml = yaml('.libraries.yml') 24 | const librariesInput = Object.keys(librariesYaml) 25 | .filter(v => librariesYaml[v].vite) 26 | .map(v => [ 27 | ...Object.keys(librariesYaml[v].css?.theme), 28 | ...Object.keys(librariesYaml[v].js), 29 | ]) 30 | .flat() 31 | .filter(v => v.match(/^assets/)) 32 | 33 | // Find any CSS for ckeditor to statically map. 34 | const infoYaml = yaml('.info.yml') 35 | const ckeditorInput = (infoYaml['ckeditor5-stylesheets'] || []) 36 | .filter(v => v.match(/\.css$/)) 37 | .map(v => v.replace('.css', '.scss')) 38 | .map(v => v.replace('dist/assets', 'assets/scss')) 39 | 40 | // Statically rename the output file. 41 | const outputMap = {} 42 | ckeditorInput.forEach(v => { 43 | outputMap[path.basename(v).replace('.scss', '.css')] = 'assets/[name].[ext]' 44 | }) 45 | 46 | export default ({ mode }) => { 47 | const env = loadEnv(mode, drupalPath, '') 48 | 49 | const config = { 50 | plugins: [ 51 | eslint(), 52 | liveReload(__dirname + '/**/*.(php|theme|twig|module)'), 53 | viteExternalsPlugin({ 54 | jquery: 'jQuery', 55 | Drupal: 'Drupal', 56 | once: 'once', 57 | drupalSettings: 'drupalSettings', 58 | }), 59 | ], 60 | 61 | base: mode === 'development' ? '/' : basePath, 62 | 63 | build: { 64 | outDir: 'dist', 65 | manifest: true, 66 | rollupOptions: { 67 | input: [...librariesInput, ...ckeditorInput], 68 | output: { 69 | assetFileNames: (assetInfo) => { 70 | return outputMap[assetInfo.name] || 'assets/[name].[hash].[ext]' 71 | }, 72 | } 73 | }, 74 | }, 75 | 76 | css: { 77 | devSourcemap: true, 78 | preprocessorOptions: { 79 | scss: { 80 | additionalData: `@import '/assets/scss/variables.scss';`, 81 | }, 82 | }, 83 | postcss: { 84 | plugins: [ 85 | autoprefixer(), 86 | ], 87 | }, 88 | }, 89 | 90 | server: { 91 | host: true, 92 | port: 3000, 93 | }, 94 | } 95 | 96 | let proxyTarget = null 97 | 98 | if (env.LANDO_APP_NAME) { 99 | proxyTarget = `https://${env.LANDO_APP_NAME}.${env.LANDO_DOMAIN}` 100 | } else if (env.DDEV_PRIMARY_URL) { 101 | proxyTarget = env.DDEV_PRIMARY_URL 102 | } 103 | 104 | if (proxyTarget && mode === 'development') { 105 | config.server.proxy = { 106 | '^/(system|api|jsonapi|graphql|oauth)/.*': { 107 | target: proxyTarget, 108 | changeOrigin: true, 109 | }, 110 | } 111 | } 112 | 113 | return defineConfig(config) 114 | } 115 | --------------------------------------------------------------------------------