Overhaul of theme

This commit is contained in:
Dan Chadwick
2024-08-25 12:32:35 -04:00
parent f63bdddde4
commit 7304c4b0d7
58 changed files with 818 additions and 569 deletions

View File

@@ -0,0 +1,5 @@
name: Auto Alt
description: 'AI generated alt text on media items.'
type: module
package: Media
core_version_requirement: ^10 || ^11

View File

@@ -0,0 +1,9 @@
auto_alt:
css:
theme:
css/styles.css: { }
js:
js/autoAlt.js: { }
dependencies:
- core/once
- core/drupal

View File

@@ -0,0 +1,12 @@
<?php
function auto_alt_form_alter(&$form, &$form_state, $form_id) {
if ($form_id !== 'media_image_edit_form') {
return;
}
$form['auto_alt'] = [
'#type' => 'markup',
'#markup' => '<span id="autoalt">🪄</span>'
];
$form['#attached']['library'][] = 'auto_alt/auto_alt';
}

View File

@@ -0,0 +1,7 @@
auto_alt.content:
path: '/ai/alt-text-generator'
defaults:
_controller: '\Drupal\auto_alt\Controller\AltTextGenerator::generate'
_title: ''
requirements:
_permission: 'access content'

View File

@@ -0,0 +1,19 @@
.form-managed-file__meta-items {
position: relative;
}
#autoalt {
position: absolute;
top: 46px;
right: 3px;
padding: 8px;
background: 'whitesmoke';
transform: translateY(-50%);
border-radius: 0 5px 5px 0;
cursor: pointer;
transition: background .2s ease;
}
#autoalt:hover {
background: lightgray;
}

View File

@@ -0,0 +1,32 @@
(function (Drupal, once) {
Drupal.behaviors.autoAltBehavior = {
attach: function (context, settings) {
once('autoAltBehavior', '#edit-field-media-image-0-alt', context).forEach(function (element) {
// Move the wand to where it should be.
let wand = context.getElementById("autoalt");
let mediaMeta = context.querySelector('.form-managed-file__meta-items');
mediaMeta.append(wand);
wand.addEventListener('click', generateAltText);
});
async function generateAltText() {
let imageUrl = document.querySelector('.image-preview__img-wrapper img').src;
const url = "/ai/alt-text-generator?";
try {
const response = await fetch(url + new URLSearchParams({
image: imageUrl,
}).toString());
if (!response.ok) {
throw new Error(`Response status: ${response.status}`);
}
const json = await response.json();
let altInput = document.getElementById('edit-field-media-image-0-alt');
altInput.value = json;
} catch (error) {
console.error(error.message);
}
}
}
};
})(Drupal, once);

View File

@@ -0,0 +1,41 @@
<?php
namespace Drupal\auto_alt\Controller;
use \Drupal\Core\Controller\ControllerBase;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Generate Alt Text for an image.
*/
class AltTextGenerator extends ControllerBase {
/**
* Returns a renderable array for a test page.
*
* return []
*/
public function generate() {
// Get the url to the image from the request.
$image_url = \Drupal::request()->get('image');
if (!$image_url) {
throw new NotFoundHttpException();
}
$image_contents = file_get_contents($image_url);
$encoded_image = base64_encode($image_contents);
// Now send this to chat gpt / ai to generate alt text.
$response = [
"This is some alt text for an image.",
"A new string for alt text",
"Another string about alt text",
];
$text_to_return = rand(0,2);
$json_response = new JsonResponse();
$json_response->headers->set('Content-Type', 'application/json');
$json_response->setData($response[$text_to_return]);
return $json_response;
}
}

View File

@@ -20,6 +20,16 @@
display: flex;
flex-wrap: wrap;
flex-direction: row;
color: var(--site-platinum, #fff);
font-size: 1.5rem;
}
.ping-pong__info-text p {
margin-bottom: 0;
}
.ping-pong__info-text {
margin-bottom: 2rem;
}
.ping-pong__info-inner {
@@ -35,7 +45,10 @@
}
.ping-pong__info h2 {
font-size: 3rem;
color: var(--site-white, #fff);
margin: 0;
margin-bottom: 1rem;
}
.ping-pong.right {
@@ -55,7 +68,7 @@
}
.field--name-field-ping-pong-cta a {
background: rgba(255, 255, 255, 0.66);
background: var(--site-secondary, #fff);
padding: 10px 20px;
border-radius: 10px;
transition: background .5s ease;

View File

@@ -31,7 +31,7 @@
<div class="ping-pong__info-text">
{% block body %}{% endblock %}
</div>
<div class="ping-pong__info-cta">
<div class="ping-pong__info-cta btn-primary">
{% block cta %}{% endblock %}
</div>
</div>

View File

@@ -1,2 +1,2 @@
{{ attach_library('ufc/recent_fights_react') }}
<div id="recent-fights"></div>
<div id="recent-fights" class="container"></div>

View File

@@ -1,35 +1,233 @@
@import url("https://fonts.googleapis.com/css2?family=Ubuntu:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap");
header {
:root {
--site-primary: #009FB7;
--site-secondary: #FED766;
--site-white: #F4F4F8;
--site-platinum: #E6E6EA;
--site-danger: #FE4A49;
}
/* OLD GET RID OF THESE */
h1 {
font-size: 3rem;
margin: 20px 0;
}
h1.center {
text-align: center;
width: 100%;
}
#block-dchadwick-primary-local-tasks {
width: 100%;
}
#block-dchadwick-primary-local-tasks > ul {
margin: 0;
padding: 20px;
display: flex;
justify-content: center;
list-style-type: none;
gap: 5px;
background: var(--site-platinum);
}
#block-dchadwick-primary-local-tasks > ul li a {
padding: 10px 20px;
border-radius: 10px;
background: var(--site-primary);
color: var(--site-white);
transition: all 0.2s ease;
}
#block-dchadwick-primary-local-tasks > ul li a:hover {
background: var(--site-secondary);
color: var(--site-primary);
}
.btn-primary {
background: var(--site-primary) !important;
color: var(--site-white);
cursor: pointer;
transition: background 0.2s ease;
padding: 10px 20px;
}
.btn-primary:hover {
background: var(--site-secondary);
color: var(--site-primary);
}
header#header {
display: flex;
width: 100%;
justify-content: center;
padding: 0.5rem;
border-top: 5px solid transparent;
border-bottom: 5px solid #9ccfd8;
border-bottom: 5px solid var(--site-secondary);
background: var(--site-primary);
}
header > div {
header#header > div {
display: flex;
justify-content: space-between;
width: 1152px;
align-items: center;
}
header .navbar-nav {
header#header #block-dchadwick-site-branding a {
white-space: nowrap;
color: var(--site-secondary);
text-transform: uppercase;
font-size: 1.5rem;
font-weight: 700;
transition: color 0.2s ease;
}
header#header #block-dchadwick-site-branding a:hover {
color: var(--site-white);
}
header#header #block-dchadwick-main-menu > ul {
display: flex;
flex-direction: row !important;
}
header .navbar-nav li:not(:last-child) {
border-right: 2px solid #f4e8d9;
header#header #block-dchadwick-main-menu > ul li:not(:last-child) {
border-right: 2px solid var(--site-secondary);
}
header .navbar-nav a.nav-link {
header#header #block-dchadwick-main-menu > ul a {
color: var(--site-white);
padding: 0 1rem;
text-decoration: none;
transition: color 0.2s ease-in-out;
}
header .navbar-nav a.nav-link:hover {
color: #f6c177;
header#header #block-dchadwick-main-menu > ul a:hover {
color: var(--site-secondary);
}
header#header #block-dchadwick-main-menu > ul a.is-active {
color: var(--site-secondary);
}
header#header #block-dchadwick-main-menu > ul a.is-active:hover {
color: var(--site-white);
}
#main {
z-index: 501;
#footer {
display: flex;
background: var(--site-primary);
color: var(--site-secondary);
justify-content: center;
align-items: center;
min-height: 50px;
margin-top: 50px;
box-shadow: 0 50vh 0 50vh var(--site-primary);
}
#footer h2 {
color: white !important;
}
#footer p {
margin: 0;
}
.table {
width: auto;
margin: auto;
border-collapse: collapse;
font-size: 0.9em;
min-width: 800px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
}
.table.cols-5 {
width: 1152px;
}
.table th, .table td {
padding: 12px 15px;
}
.table thead {
border-radius: 5px 5px 0 0;
}
.table thead tr > th {
background: var(--site-primary);
color: var(--site-white);
}
.hero-slide {
height: 600px;
overflow: hidden;
position: relative;
}
.hero-slide video {
position: absolute;
top: -50px;
left: 0;
}
.hero-caption-wrapper {
position: relative;
height: 100%;
}
.hero-caption {
width: 50%;
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 2rem;
background: white;
padding: 2rem 8rem 2rem 2rem;
}
@media screen and (max-width: 767px) {
.hero-caption {
width: 100%;
}
}
.hero-caption__text {
font-size: 2rem;
}
#recent-fights > div {
grid-gap: 5px;
justify-content: space-evenly;
}
#block-dchadwick-recentfightsblock h2, .block-recent-fights-block h2 {
text-transform: uppercase;
font-size: 2.4rem;
text-align: center;
width: 100%;
margin: 50px 0;
}
@media screen and (min-width: 1024px) {
#block-dchadwick-recentfightsblock .card, .block-recent-fights-block .card {
flex: 1 0 21%;
}
}
#block-dchadwick-recentfightsblock .fightcard-img, .block-recent-fights-block .fightcard-img {
height: 60px;
border-radius: 50%;
}
#block-dchadwick-recentfightsblock .card-header, .block-recent-fights-block .card-header {
display: flex;
justify-content: space-evenly;
align-items: center;
flex-flow: wrap;
}
#block-dchadwick-recentfightsblock .card-body, .block-recent-fights-block .card-body {
text-align: center;
}
#block-dchadwick-recentfightsblock .card-body h4, .block-recent-fights-block .card-body h4 {
font-size: 1.2rem;
}
@media screen and (max-width: 767px) {
#block-dchadwick-recentfightsblock .card-body h4, .block-recent-fights-block .card-body h4 {
font-size: 0.8rem;
}
}
#block-dchadwick-recentfightsblock .card-footer, .block-recent-fights-block .card-footer {
display: flex;
}
#block-dchadwick-recentfightsblock .card-footer button, .block-recent-fights-block .card-footer button {
background: #fffaf3;
border: 1px solid #6e6a86;
flex-grow: 1;
}
#block-dchadwick-recentfightsblock .card-footer button:hover, .block-recent-fights-block .card-footer button:hover {
background: #191724;
}
#block-dchadwick-recentfightsblock .card-footer button:hover a, .block-recent-fights-block .card-footer button:hover a {
color: #f6c177;
}
#block-dchadwick-recentfightsblock .card-footer button a, .block-recent-fights-block .card-footer button a {
color: #6e6a86;
}
#fighter__personal-info .fieldset-wrapper, #fighter__stats .fieldset-wrapper {
@@ -203,97 +401,6 @@ table.cols-5 td.incorrect {
color: white;
}
.hero-slide {
margin: 0 -0.5rem;
max-height: 500px;
overflow: hidden;
position: relative;
}
.hero-slide video {
position: relative;
bottom: 100px;
}
.hero-caption {
width: auto;
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 2rem;
background: white;
padding: 2rem 8rem 2rem 2rem;
}
#footer {
text-align: center;
background: #c4a7e7;
color: white;
padding: 0;
width: 100%;
}
#footer h2 {
color: white !important;
}
#footer p {
margin: 0;
}
#recent-fights > div {
grid-gap: 5px;
justify-content: space-evenly;
}
#block-dchadwick-recentfightsblock h2, .block-recent-fights-block h2 {
text-transform: uppercase;
font-size: 2.4rem;
text-align: center;
width: 100%;
margin: 50px 0;
}
@media screen and (min-width: 1024px) {
#block-dchadwick-recentfightsblock .card, .block-recent-fights-block .card {
flex: 1 0 21%;
}
}
#block-dchadwick-recentfightsblock .fightcard-img, .block-recent-fights-block .fightcard-img {
height: 60px;
border-radius: 50%;
}
#block-dchadwick-recentfightsblock .card-header, .block-recent-fights-block .card-header {
display: flex;
justify-content: space-evenly;
align-items: center;
flex-flow: wrap;
}
#block-dchadwick-recentfightsblock .card-body, .block-recent-fights-block .card-body {
text-align: center;
}
#block-dchadwick-recentfightsblock .card-body h4, .block-recent-fights-block .card-body h4 {
font-size: 1.2rem;
}
@media screen and (max-width: 767px) {
#block-dchadwick-recentfightsblock .card-body h4, .block-recent-fights-block .card-body h4 {
font-size: 0.8rem;
}
}
#block-dchadwick-recentfightsblock .card-footer, .block-recent-fights-block .card-footer {
display: flex;
}
#block-dchadwick-recentfightsblock .card-footer button, .block-recent-fights-block .card-footer button {
background: #fffaf3;
border: 1px solid #6e6a86;
flex-grow: 1;
}
#block-dchadwick-recentfightsblock .card-footer button:hover, .block-recent-fights-block .card-footer button:hover {
background: #191724;
}
#block-dchadwick-recentfightsblock .card-footer button:hover a, .block-recent-fights-block .card-footer button:hover a {
color: #f6c177;
}
#block-dchadwick-recentfightsblock .card-footer button a, .block-recent-fights-block .card-footer button a {
color: #6e6a86;
}
html {
font-size: 100%;
box-sizing: border-box;
@@ -304,15 +411,16 @@ html {
}
body {
background: #faf4ed;
color: #6e6a86;
background: var(--site-white);
color: var(--site-primary);
font-family: "Ubuntu", sans-serif;
margin: auto;
}
a {
color: #6e6a86;
color: var(--site-primary);
text-decoration: none;
cursor: pointer;
}
img {
@@ -320,4 +428,9 @@ img {
height: auto;
max-width: 100%;
max-height: 100%;
}
.container {
width: 1152px;
margin: auto;
}

View File

@@ -6,7 +6,7 @@ core_version_requirement: ^9 || ^10
libraries:
- dchadwick/global-styling
base theme: bootstrap5
base theme: stable9
regions:
navigation: Main navigation
hero: Hero

View File

@@ -0,0 +1,12 @@
.btn-primary {
background: var(--site-primary) !important;
color: var(--site-white);
cursor: pointer;
transition: background .2s ease;
padding: 10px 20px;
&:hover {
background: var(--site-secondary);
color: var(--site-primary);
}
}

View File

@@ -1,9 +1,12 @@
#footer {
text-align: center;
background: $iris;
color: white;
padding: 0;
width: 100%;
display: flex;
background: var(--site-primary);
color: var(--site-secondary);
justify-content: center;
align-items: center;
min-height: 50px;
margin-top: 50px;
box-shadow: 0 50vh 0 50vh var(--site-primary);
h2 {
color: white !important;

View File

@@ -1,31 +1,56 @@
header {
header#header {
display: flex;
width: 100%;
justify-content: center;
padding: .5rem;
border-top: 5px solid transparent;
border-bottom: 5px solid $foam;
border-bottom: 5px solid var(--site-secondary);
background: var(--site-primary);
> div {
display: flex;
justify-content: space-between;
width: $container-full;
align-items: center;
}
.navbar-nav {
#block-dchadwick-site-branding {
a {
white-space: nowrap;
color: var(--site-secondary);
text-transform: uppercase;
font-size: 1.5rem;
font-weight: 700;
transition: color .2s ease;
&:hover {
color: var(--site-white);
}
}
}
#block-dchadwick-main-menu > ul {
display: flex;
flex-direction: row !important;
li:not(:last-child) {
border-right: 2px solid darken($base_light, 5%);
border-right: 2px solid var(--site-secondary);
}
a.nav-link {
a {
color: var(--site-white);
padding: 0 1rem;
text-decoration: none;
transition: color .2s ease-in-out;
&:hover {
color: $orangeish;
color: var(--site-secondary);
}
&.is-active {
color: var(--site-secondary);
&:hover {
color: var(--site-white)
}
}
}
}

View File

@@ -0,0 +1,9 @@
h1 {
font-size: 3rem;
margin: 20px 0;
&.center {
text-align: center;
width: 100%;
}
}

View File

@@ -1,21 +1,38 @@
.hero-slide {
margin: 0 -0.5rem;
max-height: 500px;
height: 600px;
overflow: hidden;
position: relative;
.hero-bground {
// position: relative;
}
video {
position: relative;
bottom: 100px;
position: absolute;
top: -50px;
left: 0;
}
}
.hero-caption-wrapper {
position: relative;
height: 100%;
}
.hero-caption {
width: auto;
width: 50%;
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 2rem;
background: white;
padding: 2rem 8rem 2rem 2rem;
@media screen and (max-width: 767px) {
width: 100%;
}
&__text {
font-size: 2rem;
}
}

View File

@@ -0,0 +1,26 @@
#block-dchadwick-primary-local-tasks {
width: 100%;
> ul {
margin: 0;
padding: 20px;
display: flex;
justify-content: center;
list-style-type: none;
gap: 5px;
background: var(--site-platinum);
li a {
padding: 10px 20px;
border-radius: 10px;
background: var(--site-primary);
color: var(--site-white);
transition: all .2s ease;
&:hover {
background: var(--site-secondary);
color: var(--site-primary);
}
}
}
}

View File

@@ -4,5 +4,5 @@ main {
}
#main {
z-index: 501;
// z-index: 501;
}

View File

@@ -0,0 +1,30 @@
.table {
width: auto;
margin: auto;
border-collapse: collapse;
font-size: 0.9em;
min-width: 800px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
// overflow-x: scroll;
&.cols-5 {
width: $container-full;
}
tr {
// width: 100% !important;
}
th, td {
padding: 12px 15px;
// width: 100%;
}
thead {
border-radius: 5px 5px 0 0;
tr > th {
background: var(--site-primary);
color: var(--site-white);
}
}
}

View File

@@ -1,4 +1,13 @@
// Colors.
:root {
--site-primary: #009FB7;
--site-secondary: #FED766;
--site-white: #F4F4F8;
--site-platinum: #E6E6EA;
--site-danger: #FE4A49;
}
/* OLD GET RID OF THESE */
$base_light: #faf4ed;
$base_bl: #191724;
$overlay_bl: #1f1d2e;
@@ -22,19 +31,24 @@ $container-plus-padding: 1216px;
@import url("https://fonts.googleapis.com/css2?family=Ubuntu:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap");
// Partials.
@import "partials/headings";
@import "partials/local-tasks";
@import "partials/buttons";
@import "partials/header";
@import "partials/footer";
@import "partials/main";
@import "partials/tables";
@import "partials/hero";
@import "partials/recent-fights";
@import "partials/fighter";
@import "partials/fighters-view";
@import "partials/fight";
@import "partials/event";
@import "partials/hero";
@import "partials/footer";
@import "partials/recent-fights";
html {
font-size: 100%;
box-sizing: border-box;
// overflow: hidden;
}
.region-content {
@@ -42,15 +56,16 @@ html {
}
body {
background: $base_light;
color: $muted;
background: var(--site-white);
color: var(--site-primary);
font-family: "Ubuntu", sans-serif;
margin: auto;
}
a {
color: $muted;
color: var(--site-primary);
text-decoration: none;
cursor: pointer;
}
img {
@@ -60,3 +75,8 @@ img {
max-height: 100%;
}
.container {
width: $container-full;
margin: auto;
}

View File

@@ -54,18 +54,16 @@
{% endif %}
</div>
<div class="hero-caption">
<div class="hero-caption__text">
{{ content.field_slide_text }}
<div class="hero-caption-wrapper container">
<div class="hero-caption">
<div class="hero-caption__text">
{{ content.field_slide_text }}
</div>
<div class="hero-caption__button">
{{ content.field_slide_link }}
</div>
</div>
<div class="hero-caption__button">
{{ content.field_slide_link }}
</div>
</div>
{% endblock %}
</div>

View File

@@ -61,7 +61,7 @@
{% set linkText = item.content['#title'] %}
{% set url = item.content['#url'] ?? '' %}
{% include '@dchadwick/templates/components/button.html.twig' with {
'type': 'dark',
'type': 'primary',
'text': linkText,
'uri': url
} %}

View File

@@ -0,0 +1,26 @@
{#
/**
* @file
* Theme override to display a region.
*
* Available variables:
* - content: The content for this region, typically blocks.
* - attributes: HTML attributes for the region <div>.
* - region: The name of the region variable as defined in the theme's
* .info.yml file.
*
* @see template_preprocess_region()
*/
#}
{%
set classes = [
'region',
'region-' ~ region|clean_class,
'container'
]
%}
{% if content %}
<div{{ attributes.addClass(classes) }}>
{{ content }}
</div>
{% endif %}

View File

@@ -14,7 +14,7 @@
<article{{ attributes }} id="fight" data-fight="{{ node.id }}">
<div{{ content_attributes }}>
<table class="align-center mx-auto w-auto table-bordered table table-striped" id="compare-fighters">
<table class="table" id="compare-fighters">
<thead>
<th></th>
<th><a href="/node/{{ fighter_1.id }}">{{ node.field_fighter_one.entity.getTitle() }}<br/><img src="{{ fighter1img }}"/></a></th>

View File

@@ -77,7 +77,6 @@
view_mode ? 'node--view-mode-' ~ view_mode|clean_class,
]
%}
{{ attach_library('bootstrap5/node') }}
<div{{ attributes.addClass(classes) }}>
{% include '@dchadwick/components/card.html.twig' with {

View File

@@ -1,4 +1,3 @@
{# attach_library('ufc/ufc_react') #}
<header aria-label="Site header" class="header" id="header" role="banner">
{{ page.navigation }}
</header>

View File

@@ -0,0 +1,119 @@
{#
/**
* @file
* Theme override for displaying a view as a table.
*
* Available variables:
* - attributes: Remaining HTML attributes for the element.
* - class: HTML classes that can be used to style contextually through CSS.
* - title : The title of this group of rows.
* - header: The table header columns.
* - attributes: Remaining HTML attributes for the element.
* - content: HTML classes to apply to each header cell, indexed by
* the header's key.
* - default_classes: A flag indicating whether default classes should be
* used.
* - caption_needed: Is the caption tag needed.
* - caption: The caption for this table.
* - accessibility_description: Extended description for the table details.
* - accessibility_summary: Summary for the table details.
* - rows: Table row items. Rows are keyed by row number.
* - attributes: HTML classes to apply to each row.
* - columns: Row column items. Columns are keyed by column number.
* - attributes: HTML classes to apply to each column.
* - content: The column content.
* - default_classes: A flag indicating whether default classes should be
* used.
* - responsive: A flag indicating whether table is responsive.
* - sticky: A flag indicating whether table header is sticky.
* - summary_element: A render array with table summary information (if any).
*
* @see template_preprocess_views_view_table()
*/
#}
{%
set classes = [
'cols-' ~ header|length,
responsive ? 'responsive-enabled',
sticky ? 'sticky-enabled sticky-header',
'table'
]
%}
<table{{ attributes.addClass(classes) }}>
{% if caption_needed %}
<caption>
{% if caption %}
{{ caption }}
{% else %}
{{ title }}
{% endif %}
{% if (summary_element is not empty) %}
{{ summary_element }}
{% endif %}
</caption>
{% endif %}
{% if header %}
<thead>
<tr>
{% for key, column in header %}
{% if column.default_classes %}
{%
set column_classes = [
'views-field',
'views-field-' ~ fields[key],
]
%}
{% endif %}
<th{{ column.attributes.addClass(column_classes).setAttribute('scope', 'col') }}>
{%- if column.wrapper_element -%}
<{{ column.wrapper_element }}>
{%- if column.url -%}
<a href="{{ column.url }}" title="{{ column.title }}" rel="nofollow">{{ column.content }}{{ column.sort_indicator }}</a>
{%- else -%}
{{ column.content }}{{ column.sort_indicator }}
{%- endif -%}
</{{ column.wrapper_element }}>
{%- else -%}
{%- if column.url -%}
<a href="{{ column.url }}" title="{{ column.title }}" rel="nofollow">{{ column.content }}{{ column.sort_indicator }}</a>
{%- else -%}
{{- column.content }}{{ column.sort_indicator }}
{%- endif -%}
{%- endif -%}
</th>
{% endfor %}
</tr>
</thead>
{% endif %}
<tbody>
{% for row in rows %}
<tr{{ row.attributes }}>
{% for key, column in row.columns %}
{% if column.default_classes %}
{%
set column_classes = [
'views-field'
]
%}
{% for field in column.fields %}
{% set column_classes = column_classes|merge(['views-field-' ~ field]) %}
{% endfor %}
{% endif %}
<td{{ column.attributes.addClass(column_classes) }}>
{%- if column.wrapper_element -%}
<{{ column.wrapper_element }}>
{% for content in column.content %}
{{ content.separator }}{{ content.field_output }}
{% endfor %}
</{{ column.wrapper_element }}>
{%- else -%}
{% for content in column.content %}
{{- content.separator }}{{ content.field_output -}}
{% endfor %}
{%- endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>