Commit a505a1de authored by Yassine Doghri's avatar Yassine Doghri
Browse files

feat: restyle episode and person cards + add focus style to interactive elements for a11y

fix components in follow and remote action pages by calling new instances directly
parent 025b2f42
......@@ -238,7 +238,7 @@ if (! function_exists('location_link')) {
icon('map-pin', 'mr-2') . $location->name,
[
'class' =>
'inline-flex items-baseline hover:underline' .
'inline-flex items-baseline hover:underline focus:ring-castopod' .
($class === '' ? '' : " {$class}"),
'target' => '_blank',
'rel' => 'noreferrer noopener',
......
......@@ -20,17 +20,17 @@ if (! function_exists('render_page_links')) {
{
$pages = (new PageModel())->findAll();
$links = anchor(route_to('home'), lang('Common.home'), [
'class' => 'px-2 py-1 underline hover:no-underline',
'class' => 'px-2 py-1 underline hover:no-underline focus:ring-castopod',
]);
$links .= anchor(route_to('credits'), lang('Person.credits'), [
'class' => 'px-2 py-1 underline hover:no-underline',
'class' => 'px-2 py-1 underline hover:no-underline focus:ring-castopod',
]);
$links .= anchor(route_to('map'), lang('Page.map'), [
'class' => 'px-2 py-1 underline hover:no-underline',
'class' => 'px-2 py-1 underline hover:no-underline focus:ring-castopod',
]);
foreach ($pages as $page) {
$links .= anchor($page->link, $page->title, [
'class' => 'px-2 py-1 underline hover:no-underline',
'class' => 'px-2 py-1 underline hover:no-underline focus:ring-castopod',
]);
}
......
......@@ -18,6 +18,7 @@ return [
'no_episode_hint' =>
'Navigate the podcast episodes with the navigation bar above.',
'follow' => 'Follow',
'followTitle' => 'Follow {actorDisplayName} on the fediverse!',
'followers' => '{numberOfFollowers, plural,
one {<span class="font-semibold">#</span> follower}
other {<span class="font-semibold">#</span> followers}
......
......@@ -18,6 +18,7 @@ return [
'no_episode_hint' =>
'Naviguez au sein des épisodes du podcast episodes grâce à la barre de navigation ci-dessus.',
'follow' => 'Suivre',
'followTitle' => 'Suivez {actorDisplayName} sur le fédiverse !',
'followers' => '{numberOfFollowers, plural,
one {<span class="font-semibold">#</span> abonné·e}
other {<span class="font-semibold">#</span> abonné·e·s}
......
......@@ -25,9 +25,9 @@ const Dropdown = (): void => {
const offsetY = menu.dataset.dropdownOffsetY
? parseInt(menu.dataset.dropdownOffsetY)
: 0;
console.log(offsetX, offsetY);
popperInstance = createPopper(button, menu, {
placement: menu.dataset.dropdownPlacement as Placement,
// strategy: "fixed",
modifiers: [
{
name: "offset",
......
......@@ -17,4 +17,19 @@
.rounded-conditional-2xl {
border-radius: max(0px, min(1rem, calc((100vw - 1rem - 100%) * 9999)));
}
.backdrop-gradient {
background-image: linear-gradient(
180deg,
hsla(0, 0%, 35.29%, 0) 0%,
hsla(0, 0%, 34.53%, 0.034375) 16.36%,
hsla(0, 0%, 32.42%, 0.125) 33.34%,
hsla(0, 0%, 29.18%, 0.253125) 50.1%,
hsla(0, 0%, 24.96%, 0.4) 65.75%,
hsla(0, 0%, 19.85%, 0.546875) 79.43%,
hsla(0, 0%, 13.95%, 0.675) 90.28%,
hsla(0, 0%, 7.32%, 0.765625) 97.43%,
hsla(0, 0%, 0%, 0.8) 100%
);
}
}
......@@ -76,11 +76,17 @@ class Button extends Component
}
if ($this->iconLeft !== '') {
$this->slot = '<Icon glyph="' . $this->iconLeft . '" class="mr-2 opacity-75" />' . $this->slot;
$this->slot = (new Icon([
'glyph' => $this->iconLeft,
'class' => 'mr-2 opacity-75',
]))->render() . $this->slot;
}
if ($this->iconRight !== '') {
$this->slot .= '<Icon glyph="' . $this->iconRight . '" class="ml-2 opacity-75" />';
$this->slot .= (new Icon([
'glyph' => $this->iconRight,
'class' => 'ml-2 opacity-75',
]))->render();
}
unset($this->attributes['slot']);
......
......@@ -11,6 +11,12 @@ class DropdownMenu extends Component
{
public string $id = '';
public string $placement = 'bottom-end';
public string $offsetX = '0';
public string $offsetY = '0';
public array $items = [];
public function setItems(string $value): void
......@@ -48,7 +54,9 @@ class DropdownMenu extends Component
class="absolute z-50 flex flex-col py-2 text-black whitespace-no-wrap bg-white border-black rounded-lg border-3"
aria-labelledby="{$this->labelledby}"
data-dropdown="menu"
data-dropdown-placement="bottom-end">{$menuItems}</nav>
data-dropdown-placement="{$this->placement}"
data-dropdown-offset-x="{$this->offsetX}"
data-dropdown-offset-y="{$this->offsetY}">{$menuItems}</nav>
HTML;
}
}
......@@ -17,12 +17,16 @@ class Checkbox extends FormComponent
public function render(): string
{
$attributes = [
'id' => $this->value,
'name' => $this->name,
'class' => 'form-checkbox text-pine-500 border-black border-3 focus:ring-castopod w-6 h-6',
];
if ($this->required) {
$attributes['required'] = 'required';
}
$checkboxInput = form_checkbox(
[
'id' => $this->value,
'name' => $this->name,
'class' => 'form-checkbox text-pine-500 border-black border-3 focus:ring-castopod w-6 h-6',
],
$attributes,
'yes',
old($this->name) ? old($this->name) === $this->value : $this->isChecked,
);
......@@ -30,10 +34,7 @@ class Checkbox extends FormComponent
$hint = $this->hint === null ? '' : hint_tooltip($this->hint, 'ml-1');
return <<<HTML
<label class="leading-8 {$this->class}">
{$checkboxInput}
<span class="ml-2">{$this->slot}{$hint}</label>
</label>
<label class="inline-flex items-center {$this->class}">{$checkboxInput}<span class="ml-2">{$this->slot}{$hint}</span></label>
HTML;
}
}
......@@ -40,12 +40,15 @@ class Field extends FormComponent
unset($fieldComponentAttributes['helperText']);
unset($fieldComponentAttributes['hintText']);
$fieldComponentAttributes = flatten_attributes($fieldComponentAttributes);
$fieldComponentAttributes['class'] = 'mb-1';
$element = __NAMESPACE__ . '\\' . $this->as;
$fieldElement = new $element($fieldComponentAttributes);
return <<<HTML
<div class="flex flex-col {$this->class}">
<Forms.Label {$labelAttributes}>{$this->label}</Forms.Label>
<Forms.{$this->as} {$fieldComponentAttributes} class="mb-1"/>
{$fieldElement->render()}
{$helperText}
</div>
HTML;
......
......@@ -26,7 +26,7 @@ class Radio extends FormComponent
);
return <<<HTML
<label class="leading-8">{$radioInput}<span class="ml-2">{$this->slot}</span></label>
<label class="inline-flex items-center {$this->class}">{$radioInput}<span class="ml-2">{$this->slot}</span></label>
HTML;
}
}
......@@ -23,7 +23,7 @@ class Heading extends Component
'large' => 'text-3xl',
];
$class = $this->class . ' relative z-10 font-bold text-pine-800 font-display before:w-full before:absolute before:h-1/2 before:left-0 before:bottom-0 before:rounded-full before:bg-pine-100 before:-z-10 ' . $sizeClasses[$this->size];
$class = $this->class . ' relative z-10 font-bold text-pine-800 font-display before:w-full before:absolute before:h-1/2 before:left-0 before:bottom-0 before:rounded-full before:bg-pine-100 before:z-[-10] ' . $sizeClasses[$this->size];
return <<<HTML
<{$this->tagName} class="{$class}">{$this->slot}</{$this->tagName}>
......
......@@ -51,20 +51,15 @@ module.exports = {
admin: "300px calc(100% - 300px)",
podcast: "1fr minmax(auto, 960px) 1fr",
podcastMain: "1fr minmax(200px, 300px)",
podcasts: "repeat(auto-fill, minmax(14rem, 1fr))",
cards: "repeat(auto-fill, minmax(14rem, 1fr))",
latestEpisodes: "repeat(5, 1fr)",
},
gridTemplateRows: {
admin: "40px 1fr",
},
zIndex: {
"-10": "-10",
},
borderWidth: {
3: "3px",
},
ringWidth: {
3: "3px",
},
},
},
variants: {},
......
......@@ -26,11 +26,11 @@
<div class="flex flex-col justify-end w-full -mt-4 sticky-header-inner">
<?= render_breadcrumb('text-gray-800 text-xs items-center flex') ?>
<div class="flex justify-between py-1">
<div class="flex flex-wrap items-center">
<Heading tagName="h1" size="large"><?= $this->renderSection('pageTitle') ?></Heading>
<div class="flex flex-wrap items-center overflow-x-hidden">
<Heading tagName="h1" size="large" class="truncate"><?= $this->renderSection('pageTitle') ?></Heading>
<?= $this->renderSection('headerLeft') ?>
</div>
<div class="flex gap-1"><?= $this->renderSection('headerRight') ?></div>
<div class="flex flex-shrink-0 gap-1"><?= $this->renderSection('headerRight') ?></div>
</div>
</div>
</header>
......
......@@ -9,7 +9,7 @@
<footer class="px-2 py-2 mx-auto text-xs text-right">
<?= lang('Common.powered_by', [
'castopod' =>
'<a class="inline-flex font-semibold hover:underline" href="https://castopod.org/" target="_blank" rel="noreferrer noopener">Castopod' . icon('social/castopod', 'ml-1 text-lg') . '</a> ' .
'<a class="inline-flex font-semibold hover:underline focus:ring-castopod" href="https://castopod.org/" target="_blank" rel="noreferrer noopener">Castopod' . icon('social/castopod', 'ml-1 text-lg') . '</a> ' .
CP_VERSION,
]) ?>
</footer>
......
......@@ -22,7 +22,7 @@
aria-expanded="false"><div class="relative mr-1">
<?= icon('account-circle', 'text-3xl opacity-60') ?>
<?= user()
->podcasts === [] ? '' : '<img src="' . interact_as_actor()->avatar_image_url . '" class="absolute bottom-0 w-4 h-4 rounded-full -right-1" />' ?>
->podcasts === [] ? '' : '<img src="' . interact_as_actor()->avatar_image_url . '" class="absolute bottom-0 w-4 h-4 border rounded-full -right-1 border-pine-800" />' ?>
</div>
<?= user()->username ?>
<?= icon('caret-down', 'ml-auto text-2xl') ?></button>
......
<article class="relative flex flex-col flex-1 flex-shrink-0 w-full transition group overflow-hidden bg-white border-2 snap-center hover:shadow-lg focus-within:shadow-lg focus-within:ring-castopod border-pine-100 rounded-xl min-w-[12rem] max-w-[17rem]">
<a href="<?= route_to('episode-view', $episode->podcast->id, $episode->id) ?>" class="flex flex-col justify-end w-full h-full text-white group">
<div class="absolute bottom-0 left-0 z-10 w-full h-full backdrop-gradient"></div>
<div class="w-full h-full overflow-hidden">
<img src="<?= $episode->image->medium_url ?>" alt="<?= $episode->title ?>" class="object-cover w-full h-full transition duration-200 ease-in-out transform group-focus:scale-105 group-hover:scale-105" />
</div>
<?= publication_pill($episode->published_at, $episode->publication_status, 'absolute top-0 left-0 ml-2 mt-2 text-sm'); ?>
<div class="absolute z-20 flex flex-col items-start px-4 py-2">
<?= episode_numbering($episode->number, $episode->season_number, 'text-xs font-semibold !no-underline border px-1 border-gray-500 mr-1', true) ?>
<span class="font-semibold leading-tight line-clamp-2"><?= $episode->title ?></span>
</div>
</a>
<button class="absolute top-0 right-0 z-10 p-2 mt-2 mr-2 text-white transition -translate-y-12 rounded-full opacity-0 focus:ring-castopod focus:opacity-100 focus:-translate-y-0 group-hover:translate-y-0 bg-black/50 group-hover:opacity-100" id="more-dropdown-<?= $episode->id ?>" data-dropdown="button" data-dropdown-target="more-dropdown-<?= $episode->id ?>-menu" aria-haspopup="true" aria-expanded="false" title="<?= lang('Common.more') ?>"><?= icon('more') ?></button>
<DropdownMenu id="more-dropdown-<?= $episode->id ?>-menu" labelledby="more-dropdown-<?= $episode->id ?>" offsetY="-32" items="<?= esc(json_encode([
[
'type' => 'link',
'title' => lang('Episode.go_to_page'),
'uri' => route_to('episode', $episode->podcast->handle, $episode->slug),
],
[
'type' => 'link',
'title' => lang('Episode.edit'),
'uri' => route_to('episode-edit', $episode->podcast->id, $episode->id),
],
[
'type' => 'link',
'title' => lang('Episode.embed.title'),
'uri' => route_to('embed-add', $episode->podcast->id, $episode->id),
],
[
'type' => 'link',
'title' => lang('Person.persons'),
'uri' => route_to('episode-persons-manage', $episode->podcast->id, $episode->id),
],
[
'type' => 'link',
'title' => lang('Episode.soundbites'),
'uri' => route_to('soundbites-edit', $episode->podcast->id, $episode->id),
],
[
'type' => 'separator',
],
[
'type' => 'link',
'title' => lang('Episode.delete'),
'uri' => route_to('episode-delete', $episode->podcast->id, $episode->id),
'class' => 'font-semibold text-red-600',
],
])) ?>" />
</article>
\ No newline at end of file
......@@ -11,7 +11,7 @@
<?= $this->section('content') ?>
<Alert variant="danger" glyph="alert"><?= lang('Episode.form.warning') ?></Alert>
<Alert variant="danger" glyph="alert" class="max-w-xl"><?= lang('Episode.form.warning') ?></Alert>
<form action="<?= route_to('episode-create', $podcast->id) ?>" method="POST" enctype="multipart/form-data" class="flex flex-col mt-6 gap-y-8">
<?= csrf_field() ?>
......
......@@ -15,7 +15,7 @@
<?= $this->section('content') ?>
<Alert variant="danger" glyph="alert"><?= lang('Episode.form.warning') ?></Alert>
<Alert variant="danger" glyph="alert" class="max-w-xl"><?= lang('Episode.form.warning') ?></Alert>
<form id="episode-edit-form" action="<?= route_to('episode-edit', $podcast->id, $episode->id) ?>" method="POST" enctype="multipart/form-data" class="flex flex-col mt-6 gap-y-8">
<?= csrf_field() ?>
......
......@@ -73,10 +73,15 @@
[
'header' => lang('Episode.list.actions'),
'cell' => function ($episode, $podcast) {
return '<button id="more-dropdown-' . $episode->id . '" type="button" class="inline-flex items-center p-1 focus:ring-castopod" data-dropdown="button" data-dropdown-target="more-dropdown-' . $episode->id . '-menu" aria-haspopup="true" aria-expanded="false">' .
return '<button id="more-dropdown-' . $episode->id . '" type="button" class="inline-flex items-center p-1 rounded-full focus:ring-castopod" data-dropdown="button" data-dropdown-target="more-dropdown-' . $episode->id . '-menu" aria-haspopup="true" aria-expanded="false">' .
icon('more') .
'</button>' .
'<DropdownMenu id="more-dropdown-' . $episode->id . '-menu" labelledby="more-dropdown-' . $episode->id . '" items="' . esc(json_encode([
'<DropdownMenu id="more-dropdown-' . $episode->id . '-menu" labelledby="more-dropdown-' . $episode->id . '" offsetY="-24" items="' . esc(json_encode([
[
'type' => 'link',
'title' => lang('Episode.go_to_page'),
'uri' => route_to('episode', $podcast->handle, $episode->slug),
],
[
'type' => 'link',
'title' => lang('Episode.edit'),
......@@ -97,11 +102,6 @@
'title' => lang('Episode.soundbites'),
'uri' => route_to('soundbites-edit', $podcast->id, $episode->id),
],
[
'type' => 'link',
'title' => lang('Episode.go_to_page'),
'uri' => route_to('episode', $podcast->handle, $episode->slug),
],
[
'type' => 'separator',
],
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment