feat(plugins): add aside with plugin metadata next to plugin's readme

- enhance plugin card ui
- refactor components to be more consistent
- invert toggler label for better UX
- edit view components regex
This commit is contained in:
Yassine Doghri 2024-05-09 17:55:41 +00:00
parent e6bfdfc390
commit dfb7888aeb
193 changed files with 1632 additions and 1348 deletions

View File

@ -31,6 +31,6 @@ if (! function_exists('replace_breadcrumb_params')) {
function replace_breadcrumb_params(array $newParams): void
{
$breadcrumb = Services::breadcrumb();
$breadcrumb->replaceParams(esc($newParams));
$breadcrumb->replaceParams($newParams);
}
}

View File

@ -16,31 +16,6 @@ use CodeIgniter\View\Table;
// ------------------------------------------------------------------------
if (! function_exists('hint_tooltip')) {
/**
* Hint component
*
* Used to produce tooltip with a question mark icon for hint texts
*
* @param string $hintText The hint text
*/
function hint_tooltip(string $hintText = '', string $class = ''): string
{
$tooltip =
'<span data-tooltip="bottom" tabindex="0" title="' .
esc($hintText) .
'" class="inline-block align-middle opacity-75 focus:ring-accent';
if ($class !== '') {
$tooltip .= ' ' . $class;
}
return $tooltip . '">' . icon('question-fill') . '</span>';
}
}
// ------------------------------------------------------------------------
if (! function_exists('data_table')) {
/**
* Data table component
@ -113,12 +88,12 @@ if (! function_exists('publication_pill')) {
*/
function publication_pill(?Time $publicationDate, string $publicationStatus, string $customClass = ''): string
{
$class = match ($publicationStatus) {
'published' => 'text-pine-500 border-pine-500 bg-pine-50',
'scheduled' => 'text-red-600 border-red-600 bg-red-50',
'with_podcast' => 'text-blue-600 border-blue-600 bg-blue-50',
'not_published' => 'text-gray-600 border-gray-600 bg-gray-50',
default => 'text-gray-600 border-gray-600 bg-gray-50',
$variant = match ($publicationStatus) {
'published' => 'success',
'scheduled' => 'warning',
'with_podcast' => 'info',
'not_published' => 'default',
default => 'default',
};
$title = match ($publicationStatus) {
@ -130,16 +105,12 @@ if (! function_exists('publication_pill')) {
$label = lang('Episode.publication_status.' . $publicationStatus);
return '<span ' . ($title === '' ? '' : 'title="' . $title . '"') . ' class="flex items-center px-1 font-semibold border rounded w-max ' .
$class .
' ' .
$customClass .
'">' .
$label .
($publicationStatus === 'with_podcast' ? icon('error-warning-fill', [
// @icon('error-warning-fill')
return '<x-Pill ' . ($title === '' ? '' : 'title="' . $title . '"') . ' variant="' . $variant . '" class="' . $customClass .
'">' . $label . ($publicationStatus === 'with_podcast' ? icon('error-warning-fill', [
'class' => 'flex-shrink-0 ml-1 text-lg',
]) : '') .
'</span>';
'</x-Pill>';
}
}
@ -182,7 +153,7 @@ if (! function_exists('publication_button')) {
}
return <<<HTML
<Button variant="{$variant}" uri="{$route}" iconLeft="{$iconLeft}" >{$label}</Button>
<x-Button variant="{$variant}" uri="{$route}" iconLeft="{$iconLeft}" >{$label}</x-Button>
HTML;
}
}
@ -356,7 +327,7 @@ if (! function_exists('location_link')) {
'class' => 'mr-2 flex-shrink-0',
]) . '<span class="truncate">' . esc($location->name) . '</span>',
[
'class' => 'w-full overflow-hidden inline-flex items-baseline hover:underline focus:ring-accent' .
'class' => 'w-full overflow-hidden inline-flex items-baseline hover:underline' .
($class === '' ? '' : " {$class}"),
'target' => '_blank',
'rel' => 'noreferrer noopener',

View File

@ -20,30 +20,30 @@ 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 focus:ring-accent',
'class' => 'px-2 py-1 underline hover:no-underline',
]);
if ($podcastHandle !== null) {
$links .= anchor(route_to('podcast-links', $podcastHandle), lang('Podcast.links'), [
'class' => 'px-2 py-1 underline hover:no-underline focus:ring-accent',
'class' => 'px-2 py-1 underline hover:no-underline',
]);
}
$links .= anchor(route_to('credits'), lang('Person.credits'), [
'class' => 'px-2 py-1 underline hover:no-underline focus:ring-accent',
'class' => 'px-2 py-1 underline hover:no-underline',
]);
$links .= anchor(route_to('map'), lang('Page.map.title'), [
'class' => 'px-2 py-1 underline hover:no-underline focus:ring-accent',
'class' => 'px-2 py-1 underline hover:no-underline',
]);
foreach ($pages as $page) {
$links .= anchor($page->link, esc($page->title), [
'class' => 'px-2 py-1 underline hover:no-underline focus:ring-accent',
'class' => 'px-2 py-1 underline hover:no-underline',
]);
}
// if set in .env, add legal notice link at the end of page links
if (config('App')->legalNoticeURL !== null) {
$links .= anchor(config('App')->legalNoticeURL, lang('Common.legal_notice'), [
'class' => 'px-2 py-1 underline hover:no-underline focus:ring-accent',
'class' => 'px-2 py-1 underline hover:no-underline',
'target' => '_blank',
'rel' => 'noopener noreferrer',
]);

View File

@ -32,12 +32,18 @@ class Breadcrumb
$uri = '';
foreach (current_url(true)->getSegments() as $segment) {
$uri .= '/' . $segment;
$this->links[] = [
$link = [
'text' => is_numeric($segment)
? $segment
: lang('Breadcrumb.' . $segment),
'href' => base_url($uri),
];
if (is_numeric($segment)) {
$this->links[] = $link;
} else {
$this->links[$segment] = $link;
}
}
}
@ -46,20 +52,19 @@ class Breadcrumb
*
* Given a breadcrumb with numeric params, this function replaces them with the values provided in $newParams
*
* Example with `Home / podcasts / 1 / episodes / 1`
* Example with `Home / podcasts / 1 / episodes / 1 / foo`
*
* $newParams = [ 0 => 'foo', 1 => 'bar' ] replaceParams($newParams);
* $newParams = [ 0 => 'bar', 1 => 'baz', 'foo' => 'I Pity The Foo' ] replaceParams($newParams);
*
* The breadcrumb is now `Home / podcasts / foo / episodes / bar`
* The breadcrumb is now `Home / podcasts / foo / episodes / bar / I Pity The Foo`
*
* @param string[] $newParams
*/
public function replaceParams(array $newParams): void
{
foreach ($this->links as $key => $link) {
if (is_numeric($link['text'])) {
$this->links[$key]['text'] = $newParams[0];
array_shift($newParams);
foreach ($newParams as $key => $newValue) {
if (array_key_exists($key, $this->links)) {
$this->links[$key]['text'] = $newValue;
}
}
}

View File

@ -4,26 +4,30 @@ declare(strict_types=1);
namespace ViewComponents;
class Component implements ComponentInterface
abstract class Component implements ComponentInterface
{
protected string $slot = '';
/**
* @var list<string>
*/
protected array $props = [];
protected string $class = '';
/**
* @var array<string, string|'boolean'|'array'|'number'>
*/
protected array $casts = [];
protected ?string $slot = null;
/**
* @var array<string, string>
*/
protected array $attributes = [
'class' => '',
];
protected array $attributes = [];
/**
* @param array<string, string> $attributes
*/
public function __construct(array $attributes)
{
helper('viewcomponents');
// overwrite default attributes if set
$this->attributes = [...$this->attributes, ...$attributes];
@ -42,9 +46,39 @@ class Component implements ComponentInterface
if (is_callable([$this, $method])) {
$this->{$method}($value);
} else {
if (array_key_exists($name, $this->casts)) {
$value = match ($this->casts[$name]) {
'boolean' => $value === 'true',
'number' => (int) $value,
'array' => json_decode(htmlspecialchars_decode($value), true),
default => $value
};
}
$this->{$name} = $value;
}
// remove from attributes
if (in_array($name, $this->props, true)) {
unset($this->attributes[$name]);
}
}
unset($this->attributes['slot']);
}
public function mergeClass(string $class): void
{
if (! array_key_exists('class', $this->attributes)) {
$this->attributes['class'] = $class;
} else {
$this->attributes['class'] .= ' ' . $class;
}
}
public function getStringifiedAttributes(): string
{
return stringify_attributes($this->attributes);
}
public function render(): string

View File

@ -43,38 +43,38 @@ class ComponentRenderer
private function renderSelfClosingTags(string $output): string
{
// Pattern borrowed and adapted from Laravel's ComponentTagCompiler
// Should match any Component tags <Component />
// Should match any Component tags <x-Component />
$pattern = "/
<
\s*
(?<name>[A-Z][A-Za-z0-9\.]*?)
\s*
\\s*
x[-\\:](?<name>[\\w\\-\\:\\.]*)
\\s*
(?<attributes>
(?:
\s+
\\s+
(?:
(?:
\{\{\s*\\\$attributes(?:[^}]+?)?\s*\}\}
\\{\\{\\s*\\\$attributes(?:[^}]+?)?\\s*\\}\\}
)
|
(?:
[\w\-:.@]+
[\\w\\-:.@]+
(
=
(?:
\\\"[^\\\"]*\\\"
|
\'[^\']*\'
\\'[^\\']*\\'
|
[^\'\\\"=<>]+
[^\\'\\\"=<>]+
)
)?
)
)
)*
\s*
\\s*
)
\/>
\\/>
/x";
/*
@ -96,8 +96,9 @@ class ComponentRenderer
private function renderPairedTags(string $output): string
{
$pattern = '/<\s*(?<name>[A-Z][A-Za-z0-9\.]*?)(?<attributes>(\s*[\w\-]+\s*=\s*(\'[^\']*\'|\"[^\"]*\"))+\s*)>(?<slot>.*)<\/\s*\1\s*>/uUsm';
ini_set('pcre.backtrack_limit', '-1');
// ini_set('pcre.backtrack_limit', '-1');
$pattern = '/<\s*x[-\:](?<name>[\w\-\:\.]*?)(?<attributes>(\s*[\w\-]+\s*=\s*(\'[^\']*\'|\"[^\"]*\"))+\s*)>(?<slot>.*)<\/\s*x-\1\s*>/uiUsm';
/*
$matches[0] = full tags matched and all of its content
$matches[name] = pascal cased tag name
@ -167,8 +168,6 @@ class ComponentRenderer
(
\"[^\"]+\"
|
\'[^\']+\'
|
\\\'[^\\\']+\\\'
|
[^\s>]+

View File

@ -1,33 +0,0 @@
<?php
declare(strict_types=1);
if (! function_exists('flatten_attributes')) {
/**
* Stringify attributes for use in HTML tags.
*
* Helper function used to convert a string, array, or object of attributes to a string.
*
* @param mixed $attributes string, array, object
*/
function flatten_attributes(mixed $attributes, bool $js = false): string
{
$atts = '';
if ($attributes === null) {
return $atts;
}
if (is_string($attributes)) {
return ' ' . $attributes;
}
$attributes = (array) $attributes;
foreach ($attributes as $key => $val) {
$atts .= ($js) ? $key . '=' . esc($val, 'js') . ',' : ' ' . $key . '="' . $val . '"';
}
return rtrim($atts, ',');
}
}

View File

@ -15,10 +15,6 @@
&:hover {
@apply underline;
}
&:focus {
@apply ring-accent;
}
}
.breadcrumb-item.active {

View File

@ -9,10 +9,6 @@
font-size: 16px;
}
.choices:focus {
outline: none;
}
.choices:last-child {
margin-bottom: 0;
}
@ -327,10 +323,6 @@
cursor: pointer;
}
.choices__button:focus {
outline: none;
}
.choices__input {
@apply mb-1 align-middle bg-elevated;

View File

@ -5,6 +5,15 @@
}
}
.ring-accent {
@apply outline-none ring-2 ring-offset-2;
/* FIXME: why doesn't ring-accent-base work? */
--tw-ring-opacity: 1;
--tw-ring-color: hsl(var(--color-accent-base) / var(--tw-ring-opacity));
--tw-ring-offset-color: hsl(var(--color-background-base));
}
.rounded-conditional-b-xl {
border-bottom-right-radius: max(
0px,

View File

@ -34,7 +34,7 @@ Read more component (basic unstyled component)
/* Don't forget focus and hover styles for accessibility! */
.read-more__checkbox:focus ~ .read-more__label {
@apply ring;
@apply ring-accent;
}
.read-more__checkbox:hover ~ .read-more__label {

View File

@ -8,9 +8,15 @@ use ViewComponents\Component;
class Alert extends Component
{
protected ?string $glyph = null;
protected array $props = ['glyph', 'title'];
protected ?string $title = null;
protected string $glyph = '';
protected ?string $title = '';
protected array $attributes = [
'role' => 'alert',
];
/**
* @var 'default'|'success'|'danger'|'warning'
@ -19,7 +25,7 @@ class Alert extends Component
public function render(): string
{
$variants = [
$variantData = match ($this->variant) {
'success' => [
'class' => 'text-pine-900 bg-pine-100 border-pine-300',
'glyph' => 'check-fill', // @icon('check-fill')
@ -32,30 +38,21 @@ class Alert extends Component
'class' => 'text-yellow-900 bg-yellow-100 border-yellow-300',
'glyph' => 'alert-fill', // @icon('alert-fill')
],
'default' => [
default => [
'class' => 'text-blue-900 bg-blue-100 border-blue-300',
'glyph' => 'error-warning-fill', // @icon('error-warning-fill')
],
];
};
if (! array_key_exists($this->variant, $variants)) {
$this->variant = 'default';
}
$glyph = icon(($this->glyph ?? $variants[$this->variant]['glyph']), [
$glyph = icon(($this->glyph === '' ? $variantData['glyph'] : $this->glyph), [
'class' => 'flex-shrink-0 mr-2 text-lg',
]);
$title = $this->title === null ? '' : '<div class="font-semibold">' . $this->title . '</div>';
$class = 'inline-flex w-full p-2 text-sm border rounded ' . $variants[$this->variant]['class'] . ' ' . $this->class;
unset($this->attributes['slot']);
unset($this->attributes['variant']);
unset($this->attributes['class']);
unset($this->attributes['glyph']);
$attributes = stringify_attributes($this->attributes);
$title = $this->title === '' ? '' : '<div class="font-semibold">' . $this->title . '</div>';
$this->mergeClass('inline-flex w-full p-2 text-sm border rounded ');
$this->mergeClass($variantData['class']);
return <<<HTML
<div class="{$class}" role="alert" {$attributes}>{$glyph}<div>{$title}<p>{$this->slot}</p></div></div>
<div {$this->getStringifiedAttributes()}>{$glyph}<div>{$title}<p>{$this->slot}</p></div></div>
HTML;
}
}

View File

@ -8,10 +8,20 @@ use ViewComponents\Component;
class Button extends Component
{
protected array $props = ['uri', 'variant', 'size', 'iconLeft', 'iconRight', 'isSquared', 'isExternal'];
protected array $casts = [
'isSquared' => 'boolean',
'isExternal' => 'boolean',
];
protected string $uri = '';
protected string $variant = 'default';
/**
* @var 'small'|'base'|'large'
*/
protected string $size = 'base';
protected string $iconLeft = '';
@ -20,65 +30,54 @@ class Button extends Component
protected bool $isSquared = false;
public function setIsSquared(string $value): void
{
$this->isSquared = $value === 'true';
}
protected bool $isExternal = false;
public function render(): string
{
$baseClass =
'gap-x-2 flex-shrink-0 inline-flex items-center justify-center font-semibold rounded-full';
$this->mergeClass('gap-x-2 flex-shrink-0 inline-flex items-center justify-center font-semibold rounded-full');
$variantClass = [
'default' => 'shadow-sm text-black bg-gray-300 hover:bg-gray-400',
$variantClass = match ($this->variant) {
'primary' => 'shadow-sm text-accent-contrast bg-accent-base hover:bg-accent-hover',
'secondary' => 'shadow-sm ring-2 ring-accent ring-inset text-accent-base bg-white hover:border-accent-hover hover:text-accent-hover',
'success' => 'shadow-sm text-white bg-pine-500 hover:bg-pine-800',
'danger' => 'shadow-sm text-white bg-red-600 hover:bg-red-700',
'warning' => 'shadow-sm text-black bg-yellow-500 hover:bg-yellow-600',
'info' => 'shadow-sm text-white bg-blue-500 hover:bg-blue-600',
'success' => 'shadow-sm ring-2 ring-pine-700 ring-inset text-pine-700 hover:ring-pine-800 hover:text-pine-800',
'danger' => 'shadow-sm ring-2 ring-red-700 ring-inset text-red-700 hover:ring-red-800 hover:text-red-800',
'warning' => 'shadow-sm ring-2 ring-yellow-700 ring-inset text-yellow-700 hover:ring-yellow-800 hover:text-yellow-800',
'info' => 'shadow-sm ring-2 ring-blue-700 ring-inset text-blue-700 hover:ring-blue-800 hover:text-blue-800',
'disabled' => 'shadow-sm text-black bg-gray-300 cursor-not-allowed',
];
default => 'shadow-sm text-black bg-gray-100 hover:bg-gray-300',
};
$sizeClass = [
$sizeClass = match ($this->size) {
'small' => 'text-xs leading-6',
'base' => 'text-sm leading-5',
'large' => 'text-base leading-6',
];
default => 'text-sm leading-5',
};
$iconSize = [
$iconSizeClass = match ($this->size) {
'small' => 'text-sm',
'base' => 'text-lg',
'large' => 'text-2xl',
];
default => 'text-lg',
};
$basePaddings = [
$basePaddings = match ($this->size) {
'small' => 'px-3 py-1',
'base' => 'px-3 py-2',
'large' => 'px-4 py-2',
];
default => 'px-3 py-2',
};
$squaredPaddings = [
$squaredPaddings = match ($this->size) {
'small' => 'p-1',
'base' => 'p-2',
'large' => 'p-3',
];
default => 'p-2',
};
$buttonClass =
$baseClass .
' ' .
($this->isSquared
? $squaredPaddings[$this->size]
: $basePaddings[$this->size]) .
' ' .
$sizeClass[$this->size] .
' ' .
$variantClass[$this->variant];
$this->mergeClass($variantClass);
$this->mergeClass($sizeClass);
if (array_key_exists('class', $this->attributes)) {
$buttonClass .= ' ' . $this->attributes['class'];
unset($this->attributes['class']);
if ($this->isSquared) {
$this->mergeClass($squaredPaddings);
} else {
$this->mergeClass($basePaddings);
}
if ($this->iconLeft !== '' || $this->iconRight !== '') {
@ -87,41 +86,30 @@ class Button extends Component
if ($this->iconLeft !== '') {
$this->slot = icon($this->iconLeft, [
'class' => 'opacity-75 ' . $iconSize[$this->size],
'class' => 'opacity-75 ' . $iconSizeClass,
]) . $this->slot;
}
if ($this->iconRight !== '') {
$this->slot .= icon($this->iconRight, [
'class' => 'opacity-75 ' . $iconSize[$this->size],
'class' => 'opacity-75 ' . $iconSizeClass,
]);
}
unset($this->attributes['slot']);
unset($this->attributes['variant']);
unset($this->attributes['size']);
unset($this->attributes['iconLeft']);
unset($this->attributes['iconRight']);
unset($this->attributes['isSquared']);
unset($this->attributes['uri']);
unset($this->attributes['label']);
if ($this->uri !== '') {
$tagName = 'a';
$defaultButtonAttributes = [
'href' => $this->uri,
];
$this->attributes['href'] = $this->uri;
if ($this->isExternal) {
$this->attributes['target'] = '_blank';
$this->attributes['rel'] = 'noopener noreferrer';
}
} else {
$tagName = 'button';
$defaultButtonAttributes = [
'type' => 'button',
];
$this->attributes['type'] ??= 'button';
}
$attributes = stringify_attributes(array_merge($defaultButtonAttributes, $this->attributes));
return <<<HTML
<{$tagName} class="{$buttonClass}" {$attributes}>{$this->slot}</{$tagName}>
<{$tagName} {$this->getStringifiedAttributes()}>{$this->slot}</{$tagName}>
HTML;
}
}

View File

@ -8,13 +8,13 @@ use ViewComponents\Component;
class ChartsComponent extends Component
{
protected string $title = '';
protected string $title;
protected string $subtitle = '';
protected string $dataUrl = '';
protected string $dataUrl;
protected string $type = '';
protected string $type;
public function render(): string
{
@ -23,8 +23,10 @@ class ChartsComponent extends Component
$subtitleBlock = '<p class="px-6 -mt-4 text-sm text-skin-muted">' . $this->subtitle . '</p>';
}
$this->mergeClass('bg-elevated border-3 rounded-xl border-subtle');
return <<<HTML
<div class="bg-elevated border-3 rounded-xl border-subtle {$this->class}">
<div {$this->getStringifiedAttributes()}>
<h2 class="px-6 py-4 text-xl">{$this->title}</h2>
{$subtitleBlock}
<div class="w-full h-[500px]" data-chart-type="{$this->type}" data-chart-url="{$this->dataUrl}"></div>

View File

@ -8,7 +8,9 @@ use ViewComponents\Component;
class DashboardCard extends Component
{
protected ?string $href = null;
protected array $props = ['href', 'glyph', 'title', 'subtitle'];
protected string $href = '';
protected string $glyph;
@ -27,11 +29,11 @@ class DashboardCard extends Component
'class' => 'flex-shrink-0 bg-base rounded-full w-8 h-8 p-2 text-accent-base',
]);
if ($this->href !== null && $this->href !== '') {
if ($this->href !== '') {
$chevronRight = icon('arrow-right-s-fill');
$viewLang = lang('Common.view');
return <<<HTML
<a href="{$this->href}" class="flex items-center justify-between w-full gap-4 p-4 lg:max-w-sm lg:flex-col xl:flex-row bg-elevated focus:ring-accent rounded-xl border-3 border-subtle group">
<a href="{$this->href}" class="flex items-center justify-between w-full gap-4 p-4 lg:max-w-sm lg:flex-col xl:flex-row bg-elevated rounded-xl border-3 border-subtle group">
<div class="flex items-start">{$glyph}<div class="flex flex-col ml-2"><div class="flex items-center"><span class="text-xs font-semibold leading-loose tracking-wider uppercase">{$this->title}</span><div class="inline-flex items-center ml-4 transition -translate-x-full group-hover:translate-x-0 group-focus:translate-x-0"><span class="-ml-2 text-xs lowercase transition opacity-0 group-hover:opacity-100 group-focus:opacity-100">{$viewLang}</span>{$chevronRight}</div></div><p class="text-xs">{$this->subtitle}</p></div></div>
<div class="text-5xl font-bold">{$this->slot}</div>
</a>

View File

@ -9,17 +9,25 @@ use ViewComponents\Component;
class DropdownMenu extends Component
{
public string $id = '';
protected array $props = ['id', 'labelledby', 'placement', 'offsetX', 'offsetY', 'items'];
public string $labelledby;
protected array $casts = [
'offsetX' => 'number',
'offsetY' => 'number',
'items' => 'array',
];
public string $placement = 'bottom-end';
protected string $id;
public string $offsetX = '0';
protected string $labelledby;
public string $offsetY = '0';
protected string $placement = 'bottom-end';
public array $items = [];
protected int $offsetX = 0;
protected int $offsetY = 0;
protected array $items = [];
public function setItems(string $value): void
{
@ -37,7 +45,7 @@ class DropdownMenu extends Component
switch ($item['type']) {
case 'link':
$menuItems .= anchor($item['uri'], $item['title'], [
'class' => 'inline-flex gap-x-1 items-center px-4 py-1 hover:bg-highlight focus:ring-accent focus:ring-inset' . (array_key_exists('class', $item) ? ' ' . $item['class'] : ''),
'class' => 'inline-flex gap-x-1 items-center px-4 py-1 hover:bg-highlight' . (array_key_exists('class', $item) ? ' ' . $item['class'] : ''),
]);
break;
case 'html':
@ -51,14 +59,16 @@ class DropdownMenu extends Component
}
}
$this->mergeClass('absolute flex flex-col py-2 rounded-lg z-60 whitespace-nowrap text-skin-base border-contrast bg-elevated border-3');
$this->attributes['id'] = $this->id;
$this->attributes['aria-labelledby'] = $this->labelledby;
$this->attributes['data-dropdown'] = 'menu';
$this->attributes['data-dropdown-placement'] = $this->placement;
$this->attributes['data-dropdown-offset-x'] = $this->offsetX;
$this->attributes['data-dropdown-offset-y'] = $this->offsetY;
return <<<HTML
<nav id="{$this->id}"
class="absolute flex flex-col py-2 rounded-lg z-60 whitespace-nowrap text-skin-base border-contrast bg-elevated border-3"
aria-labelledby="{$this->labelledby}"
data-dropdown="menu"
data-dropdown-placement="{$this->placement}"
data-dropdown-offset-x="{$this->offsetX}"
data-dropdown-offset-y="{$this->offsetY}">{$menuItems}</nav>
<nav {$this->getStringifiedAttributes()}>{$menuItems}</nav>
HTML;
}
}

View File

@ -4,35 +4,41 @@ declare(strict_types=1);
namespace App\Views\Components\Forms;
use App\Views\Components\Hint;
class Checkbox extends FormComponent
{
protected ?string $hint = null;
protected array $props = ['hint', 'isChecked'];
protected array $casts = [
'isChecked' => 'boolean',
];
protected string $hint = '';
protected bool $isChecked = false;
public function setIsChecked(string $value): void
{
$this->isChecked = $value === 'true';
}
public function render(): string
{
$attributes = [
'id' => $this->value,
'name' => $this->name,
'class' => 'form-checkbox bg-elevated text-accent-base border-contrast border-3 focus:ring-accent w-6 h-6',
];
$checkboxInput = form_checkbox(
$attributes,
[
'id' => $this->value,
'name' => $this->name,
'class' => 'form-checkbox bg-elevated text-accent-base border-contrast border-3 w-6 h-6',
],
'yes',
old($this->name) ? old($this->name) === $this->value : $this->isChecked,
);
$hint = $this->hint === null ? '' : hint_tooltip($this->hint, 'ml-1');
$hint = $this->hint === '' ? '' : (new Hint([
'class' => 'ml-1',
'slot' => $this->hint,
]))->render();
$this->mergeClass('inline-flex items-center');
return <<<HTML
<label class="inline-flex items-center {$this->class}">{$checkboxInput}<span class="ml-2">{$this->slot}{$hint}</span></label>
<label {$this->getStringifiedAttributes()}>{$checkboxInput}<span class="ml-2">{$this->slot}{$hint}</span></label>
HTML;
}
}

View File

@ -6,15 +6,14 @@ namespace App\Views\Components\Forms;
class ColorRadioButton extends FormComponent
{
protected array $props = ['isChecked'];
protected array $casts = [
'isChecked' => 'boolean',
];
protected bool $isChecked = false;
protected string $style = '';
public function setIsChecked(string $value): void
{
$this->isChecked = $value === 'true';
}
public function render(): string
{
$data = [
@ -23,7 +22,7 @@ class ColorRadioButton extends FormComponent
'class' => 'color-radio-btn',
];
if ($this->required) {
if ($this->isRequired) {
$data['required'] = 'required';
}
@ -34,7 +33,7 @@ class ColorRadioButton extends FormComponent
);
return <<<HTML
<div class="{$this->class}" style="{$this->style}">
<div {$this->getStringifiedAttributes()}>
{$radioInput}
<label for="{$this->value}" title="{$this->slot}" data-tooltip="bottom"></label>
</div>

View File

@ -6,21 +6,28 @@ namespace App\Views\Components\Forms;
class DatetimePicker extends FormComponent
{
protected array $attributes = [
'data-picker' => 'datetime',
];
public function render(): string
{
$this->attributes['class'] = 'rounded-l-lg border-0 border-rounded-r-none flex-1 focus:ring-0';
$this->attributes['data-input'] = '';
$dateInput = form_input($this->attributes, old($this->name, $this->value));
$dateInput = form_input([
'class' => 'rounded-l-lg border-0 border-rounded-r-none flex-1 focus:ring-0',
'data-input' => '',
], old($this->name, $this->value));
$clearLabel = lang(
'Episode.publish_form.scheduled_publication_date_clear',
);
$closeIcon = icon('close-fill');
$this->mergeClass('flex border-3 rounded-lg border-contrast focus-within:ring-accent');
return <<<HTML
<div class="flex border-3 rounded-lg border-contrast focus-within:ring-accent {$this->class}" data-picker="datetime">
<div {$this->getStringifiedAttributes()}>
{$dateInput}
<button class="p-3 bg-elevated hover:bg-base rounded-r-md focus:ring-inset focus:ring-accent" type="button" aria-label="{$clearLabel}" title="{$clearLabel}" data-clear="">
<button class="p-3 bg-elevated hover:bg-base rounded-r-md focus:ring-inset" type="button" aria-label="{$clearLabel}" title="{$clearLabel}" data-clear="">
{$closeIcon}
</button>
</div>

View File

@ -4,49 +4,76 @@ declare(strict_types=1);
namespace App\Views\Components\Forms;
class Field extends FormComponent
use ViewComponents\Component;
class Field extends Component
{
protected array $props = [
'name',
'label',
'isRequired',
'isReadonly',
'as',
'helper',
'hint',
];
protected array $casts = [
'isRequired' => 'boolean',
'isReadonly' => 'boolean',
];
protected string $name;
protected string $label;
protected bool $isRequired = false;
protected bool $isReadonly = false;
protected string $as = 'Input';
protected string $label = '';
protected string $helper = '';
protected ?string $helper = null;
protected ?string $hint = null;
protected string $hint = '';
public function render(): string
{
$helperText = '';
if ($this->helper !== null) {
$helperId = $this->id . 'Help';
$helperText = '<Forms.Helper id="' . $helperId . '">' . $this->helper . '</Forms.Helper>';
if ($this->helper !== '') {
$helperId = $this->name . 'Help';
$helperText = (new Helper([
'id' => $helperId,
'slot' => $this->helper,
]))->render();
$this->attributes['aria-describedby'] = $helperId;
}
$labelAttributes = [
'for' => $this->id,
'isOptional' => $this->required ? 'false' : 'true',
'for' => $this->name,
'isOptional' => $this->isRequired ? 'false' : 'true',
'class' => '-mb-1',
'slot' => $this->label,
];
if ($this->hint) {
if ($this->hint !== '') {
$labelAttributes['hint'] = $this->hint;
}
$labelAttributes = stringify_attributes($labelAttributes);
$label = new Label($labelAttributes);
// remove field specific attributes to inject the rest to Form Component
$fieldComponentAttributes = $this->attributes;
unset($fieldComponentAttributes['as']);
unset($fieldComponentAttributes['label']);
unset($fieldComponentAttributes['class']);
unset($fieldComponentAttributes['helper']);
unset($fieldComponentAttributes['hint']);
$this->mergeClass('flex flex-col');
$fieldClass = $this->attributes['class'];
unset($this->attributes['class']);
$this->attributes['name'] = $this->name;
$this->attributes['isRequired'] = $this->isRequired ? 'true' : 'false';
$this->attributes['isReadonly'] = $this->isReadonly ? 'true' : 'false';
$element = __NAMESPACE__ . '\\' . $this->as;
$fieldElement = new $element($fieldComponentAttributes);
$fieldElement = new $element($this->attributes);
return <<<HTML
<div class="flex flex-col {$this->class}">
<Forms.Label {$labelAttributes}>{$this->label}</Forms.Label>
<div class="{$fieldClass}">
{$label->render()}
{$helperText}
<div class="w-full mt-1">
{$fieldElement->render()}

View File

@ -6,28 +6,55 @@ namespace App\Views\Components\Forms;
use ViewComponents\Component;
class FormComponent extends Component
abstract class FormComponent extends Component
{
protected ?string $id = null;
protected array $props = [
'id',
'name',
'value',
'isRequired',
'isReadonly',
];
protected string $name = '';
protected array $casts = [
'isRequired' => 'boolean',
'isReadonly' => 'boolean',
];
protected string $id;
protected string $name;
protected string $value = '';
protected bool $required = false;
protected bool $isRequired = false;
protected bool $readonly = false;
protected bool $isReadonly = false;
/**
* @param array<string, string> $attributes
*/
public function __construct(array $attributes)
{
$parentVars = get_class_vars(self::class);
$this->casts = [...$parentVars['casts'], ...$this->casts];
$this->props = [...$parentVars['props'], $this->props];
parent::__construct($attributes);
if ($this->id === null) {
if (! isset($this->id)) {
$this->id = $this->name;
$this->attributes['id'] = $this->id;
}
$this->attributes['id'] = $this->id;
$this->attributes['name'] = $this->name;
if ($this->isRequired) {
$this->attributes['required'] = 'required';
}
if ($this->isReadonly) {
$this->attributes['readonly'] = 'readonly';
}
}
@ -35,23 +62,4 @@ class FormComponent extends Component
{
$this->value = htmlspecialchars_decode($value, ENT_QUOTES);
}
public function setRequired(string $value): void
{
$this->required = $value === 'true';
unset($this->attributes['required']);
if ($this->required) {
$this->attributes['required'] = 'required';
}
}
public function setReadonly(string $value): void
{
$this->readonly = $value === 'true';
if ($this->readonly) {
$this->attributes['readonly'] = 'readonly';
} else {
unset($this->attributes['readonly']);
}
}
}

View File

@ -4,19 +4,18 @@ declare(strict_types=1);
namespace App\Views\Components\Forms;
class Helper extends FormComponent
use ViewComponents\Component;
class Helper extends Component
{
/**
* @var 'default'|'error'
*/
protected string $type = 'default';
// TODO: add type with error and show errors inline
public function render(): string
{
$class = 'text-skin-muted';
$this->mergeClass('text-skin-muted');
return <<<HTML
<small id="{$this->id}" class="{$class} {$this->class}">{$this->slot}</small>
<small {$this->getStringifiedAttributes()}>{$this->slot}</small>
HTML;
}
}

View File

@ -6,26 +6,29 @@ namespace App\Views\Components\Forms;
class Input extends FormComponent
{
protected array $props = ['type'];
protected string $type = 'text';
public function render(): string
{
$baseClass = 'w-full border-contrast rounded-lg focus:border-contrast border-3 focus:ring-accent focus-within:ring-accent ' . $this->class;
$this->attributes['class'] = $baseClass;
$this->mergeClass('w-full border-contrast rounded-lg focus:border-contrast border-3 focus-within:ring-accent');
if ($this->type === 'file') {
$this->attributes['class'] .= ' file:px-3 file:py-2 file:h-[40px] file:font-semibold file:text-skin-muted file:text-sm file:rounded-none file:border-none file:bg-highlight file:cursor-pointer';
$this->mergeClass('file:px-3 file:py-2 file:h-[40px] file:font-semibold file:text-skin-muted file:text-sm file:rounded-none file:border-none file:bg-highlight file:cursor-pointer');
} else {
$this->attributes['class'] .= ' px-3 py-2';
$this->mergeClass('px-3 py-2');
}
if ($this->readonly) {
$this->attributes['class'] .= ' bg-base';
if ($this->isReadonly) {
$this->mergeClass('bg-base');
} else {
$this->attributes['class'] .= ' bg-elevated';
$this->mergeClass('bg-elevated');
}
$this->attributes['type'] = $this->type;
$this->attributes['value'] = $this->value;
return form_input($this->attributes, old($this->name, $this->value));
}
}

View File

@ -4,39 +4,38 @@ declare(strict_types=1);
namespace App\Views\Components\Forms;
use App\Views\Components\Hint;
use ViewComponents\Component;
class Label extends Component
{
protected ?string $for = null;
protected array $props = ['for', 'hint', 'isOptional'];
protected ?string $hint = null;
protected array $casts = [
'isOptional' => 'boolean',
];
protected string $for;
protected string $hint = '';
protected bool $isOptional = false;
public function setIsOptional(string $value): void
{
$this->isOptional = $value === 'true';
}
public function render(): string
{
$labelClass = 'text-sm font-semibold ' . $this->attributes['class'];
unset($this->attributes['class']);
$this->mergeClass('text-sm font-semibold');
$optionalText = $this->isOptional ? '<small class="ml-1 font-normal lowercase">(' .
lang('Common.optional') .
')</small>' : '';
$hint = $this->hint === null ? '' : hint_tooltip($this->hint, 'ml-1');
unset($this->attributes['isOptional']);
unset($this->attributes['hint']);
unset($this->attributes['slot']);
$attributes = stringify_attributes($this->attributes);
$hint = $this->hint === '' ? '' : (new Hint([
'class' => 'ml-1',
'slot' => $this->hint,
]))->render();
return <<<HTML
<label class="{$labelClass}" {$attributes}>{$this->slot}{$optionalText}{$hint}</label>
<label {$this->getStringifiedAttributes()}>{$this->slot}{$optionalText}{$hint}</label>
HTML;
}
}

View File

@ -6,6 +6,8 @@ namespace App\Views\Components\Forms;
class MarkdownEditor extends FormComponent
{
protected array $props = ['disallowList'];
/**
* @var string[]
*/
@ -18,18 +20,20 @@ class MarkdownEditor extends FormComponent
public function render(): string
{
$editorClass = 'w-full flex flex-col bg-elevated border-3 border-contrast rounded-lg overflow-hidden focus-within:ring-accent ' . $this->class;
$this->mergeClass('w-full flex flex-col bg-elevated border-3 border-contrast rounded-lg overflow-hidden focus-within:ring-accent');
$wrapperClass = $this->attributes['class'];
$this->attributes['class'] = 'bg-elevated border-none focus:border-none focus:outline-none focus:ring-0 w-full h-full';
$this->attributes['rows'] = 6;
$textarea = form_textarea($this->attributes, old($this->name, $this->value));
$markdownIcon = icon(
'markdown-fill',
[
'class' => 'mr-1 text-lg opacity-40',
]
$textarea = form_textarea(
$this->attributes,
old($this->name, $this->value)
);
$markdownIcon = icon('markdown-fill', [
'class' => 'mr-1 text-lg opacity-40',
]);
$translations = [
'write' => lang('Common.forms.editor.write'),
'preview' => lang('Common.forms.editor.preview'),
@ -85,19 +89,19 @@ class MarkdownEditor extends FormComponent
$toolbarContent .= '<div class="inline-flex text-2xl gap-x-1">';
foreach ($buttonsGroup as $button) {
if (! in_array($button['name'], $this->disallowList, true)) {
$toolbarContent .= '<' . $button['tag'] . ' class="opacity-50 hover:opacity-100 focus:ring-accent focus:opacity-100">' . $button['icon'] . '</' . $button['tag'] . '>';
$toolbarContent .= '<' . $button['tag'] . ' class="opacity-50 hover:opacity-100 focus:opacity-100">' . $button['icon'] . '</' . $button['tag'] . '>';
}
}
$toolbarContent .= '</div>';
}
return <<<HTML
<div class="{$editorClass}">
<div class="{$wrapperClass}">
<header class="px-2">
<div class="sticky top-0 z-20 flex flex-wrap justify-between border-b border-gray-300 bg-elevated">
<markdown-write-preview for="{$this->id}" class="relative inline-flex h-8">
<button type="button" slot="write" class="px-2 font-semibold focus:ring-inset focus:ring-accent">{$translations['write']}</button>
<button type="button" slot="preview" class="px-2 font-semibold focus:ring-inset focus:ring-accent">{$translations['preview']}</button>
<button type="button" slot="write" class="px-2 font-semibold">{$translations['write']}</button>
<button type="button" slot="preview" class="px-2 font-semibold">{$translations['preview']}</button>
</markdown-write-preview>
<markdown-toolbar for="{$this->id}" class="flex gap-4 px-2 py-1">{$toolbarContent}</markdown-toolbar>
</div>

View File

@ -6,6 +6,13 @@ namespace App\Views\Components\Forms;
class MultiSelect extends FormComponent
{
protected array $props = ['options', 'selected'];
protected array $casts = [
'options' => 'array',
'selected' => 'array',
];
/**
* @var array<string, string>
*/
@ -16,18 +23,10 @@ class MultiSelect extends FormComponent
*/
protected array $selected = [];
public function setOptions(string $value): void
{
$this->options = json_decode(htmlspecialchars_decode($value), true);
}
public function setSelected(string $selected): void
{
$this->selected = json_decode(htmlspecialchars_decode($selected), true);
}
public function render(): string
{
$this->mergeClass('w-full bg-elevated border-3 border-contrast rounded-lg');
$defaultAttributes = [
'data-class' => $this->attributes['class'],
'multiple' => 'multiple',
@ -37,8 +36,7 @@ class MultiSelect extends FormComponent
'data-no-choices-text' => lang('Common.forms.multiSelect.noChoicesText'),
'data-max-item-text' => lang('Common.forms.multiSelect.maxItemText'),
];
$this->attributes['class'] .= ' w-full bg-elevated border-3 border-contrast rounded-lg';
$extra = array_merge($defaultAttributes, $this->attributes);
$extra = [...$defaultAttributes, ...$this->attributes];
return form_dropdown($this->name, $this->options, $this->selected, $extra);
}

View File

@ -6,12 +6,13 @@ namespace App\Views\Components\Forms;
class Radio extends FormComponent
{
protected bool $isChecked = false;
protected array $props = ['isChecked'];
public function setIsChecked(string $value): void
{
$this->isChecked = $value === 'true';
}
protected array $casts = [
'isChecked' => 'boolean',
];
protected bool $isChecked = false;
public function render(): string
{
@ -19,14 +20,16 @@ class Radio extends FormComponent
[
'id' => $this->value,
'name' => $this->name,
'class' => 'text-accent-base bg-elevated border-contrast border-3 focus:ring-accent w-6 h-6',
'class' => 'text-accent-base bg-elevated border-contrast border-3 w-6 h-6',
],
$this->value,
old($this->name) ? old($this->name) === $this->value : $this->isChecked,
);
$this->mergeClass('inline-flex items-center');
return <<<HTML
<label class="inline-flex items-center {$this->class}">{$radioInput}<span class="ml-2">{$this->slot}</span></label>
<label {$this->getStringifiedAttributes()}>{$radioInput}<span class="ml-2">{$this->slot}</span></label>
HTML;
}
}

View File

@ -4,16 +4,19 @@ declare(strict_types=1);
namespace App\Views\Components\Forms;
use App\Views\Components\Hint;
class RadioButton extends FormComponent
{
protected array $props = ['isChecked', 'hint'];
protected array $casts = [
'isChecked' => 'boolean',
];
protected bool $isChecked = false;
protected ?string $hint = null;
public function setIsChecked(string $value): void
{
$this->isChecked = $value === 'true';
}
protected string $hint = '';
public function render(): string
{
@ -23,7 +26,7 @@ class RadioButton extends FormComponent
'class' => 'form-radio-btn bg-elevated',
];
if ($this->required) {
if ($this->isRequired) {
$data['required'] = 'required';
}
@ -33,10 +36,13 @@ class RadioButton extends FormComponent
old($this->name) ? old($this->name) === $this->value : $this->isChecked,
);
$hint = $this->hint ? hint_tooltip($this->hint, 'ml-1 text-base') : '';
$hint = $this->hint === '' ? '' : (new Hint([
'class' => 'ml-1 text-base',
'slot' => $this->hint,
]))->render();
return <<<HTML
<div class="{$this->class}">
<div {$this->getStringifiedAttributes()}">
{$radioInput}
<label for="{$this->value}">{$this->slot}{$hint}</label>
</div>

View File

@ -8,17 +8,21 @@ use ViewComponents\Component;
class Section extends Component
{
protected string $title = '';
protected array $props = ['title', 'subtitle'];
protected ?string $subtitle = null;
protected string $title;
protected string $subtitle = '';
public function render(): string
{
$subtitle = $this->subtitle === null ? '' : '<p class="text-sm text-skin-muted">' . $this->subtitle . '</p>';
$subtitle = $this->subtitle === '' ? '' : '<p class="text-sm text-skin-muted">' . $this->subtitle . '</p>';
$this->mergeClass('w-full p-8 bg-elevated border-3 flex flex-col items-start border-subtle rounded-xl');
return <<<HTML
<fieldset class="w-full p-8 bg-elevated border-3 flex flex-col items-start border-subtle rounded-xl {$this->class}">
<Heading tagName="legend" class="float-left">{$this->title}</Heading>
<fieldset {$this->getStringifiedAttributes()}>
<x-Heading tagName="legend" class="float-left">{$this->title}</x-Heading>
{$subtitle}
<div class="flex flex-col w-0 min-w-full gap-4 py-4">{$this->slot}</div>
</fieldset>

View File

@ -6,6 +6,12 @@ namespace App\Views\Components\Forms;
class Select extends FormComponent
{
protected array $props = ['options', 'selected'];
protected array $casts = [
'options' => 'array',
];
/**
* @var array<string, string>
*/
@ -13,26 +19,17 @@ class Select extends FormComponent
protected string $selected = '';
public function setOptions(string $value): void
{
$this->options = json_decode(htmlspecialchars_decode($value), true);
}
public function render(): string
{
$this->mergeClass('w-full focus:border-contrast border-3 rounded-lg bg-elevated border-contrast');
$defaultAttributes = [
'class' => 'w-full focus:border-contrast focus:ring-accent border-3 rounded-lg bg-elevated border-contrast ' . $this->class,
'data-class' => $this->class,
'data-select-text' => lang('Common.forms.multiSelect.selectText'),
'data-loading-text' => lang('Common.forms.multiSelect.loadingText'),
'data-no-results-text' => lang('Common.forms.multiSelect.noResultsText'),
'data-no-choices-text' => lang('Common.forms.multiSelect.noChoicesText'),
'data-max-item-text' => lang('Common.forms.multiSelect.maxItemText'),
];
unset($this->attributes['name']);
unset($this->attributes['options']);
unset($this->attributes['selected']);
$extra = [...$this->attributes, ...$defaultAttributes];
$extra = [...$defaultAttributes, ...$this->attributes];
return form_dropdown($this->name, $this->options, old($this->name, $this->selected !== '' ? [$this->selected] : []), $extra);
}

View File

@ -15,9 +15,9 @@ class Textarea extends FormComponent
public function render(): string
{
unset($this->attributes['value']);
$this->mergeClass('bg-elevated w-full rounded-lg border-3 border-contrast focus:border-contrast focus-within:ring-accent');
$this->attributes['class'] = 'bg-elevated w-full focus:border-contrast focus:ring-accent rounded-lg border-3 border-contrast ' . $this->class;
$this->attributes['id'] = $this->id;
$textarea = form_textarea(
$this->attributes,

View File

@ -4,45 +4,48 @@ declare(strict_types=1);
namespace App\Views\Components\Forms;
use App\Views\Components\Hint;
class Toggler extends FormComponent
{
protected array $props = ['size', 'hint', 'isChecked'];
protected array $casts = [
'isChecked' => 'boolean',
];
/**
* @var 'base'|'small
*/
protected string $size = 'base';
protected string $label = '';
protected string $hint = '';
protected bool $checked = false;
public function setChecked(string $value): void
{
$this->checked = $value === 'true';
}
protected bool $isChecked = false;
public function render(): string
{
unset($this->attributes['checked']);
$wrapperClass = $this->class;
unset($this->attributes['class']);
$sizeClass = [
'base' => 'form-switch-slider',
$sizeClass = match ($this->size) {
'small' => 'form-switch-slider form-switch-slider--small',
];
default => 'form-switch-slider',
};
$this->attributes['class'] = 'form-switch';
$this->mergeClass('relative justify-between inline-flex items-center gap-x-2');
$checkbox = form_checkbox([
'class' => 'form-switch',
], 'yes', old($this->name) === 'yes' ? true : $this->isChecked);
$hint = $this->hint === '' ? '' : (new Hint([
'class' => 'ml-1',
'slot' => $this->hint,
]))->render();
$checkbox = form_checkbox($this->attributes, $this->value, old($this->name) === 'yes' ? true : $this->checked);
$hint = $this->hint === '' ? '' : hint_tooltip($this->hint, 'ml-1');
return <<<HTML
<label class="relative inline-flex items-center {$wrapperClass}">
<label {$this->getStringifiedAttributes()}>
<span class="">{$this->slot}{$hint}</span>
{$checkbox}
<span class="{$sizeClass[$this->size]}"></span>
<span class="ml-2">{$this->slot}{$hint}</span>
<span class="{$sizeClass}"></span>
</label>
HTML;
}

View File

@ -6,6 +6,8 @@ namespace App\Views\Components\Forms;
class XMLEditor extends FormComponent
{
protected array $props = ['content'];
/**
* @var array<string, string>
*/

View File

@ -8,6 +8,8 @@ use ViewComponents\Component;
class Heading extends Component
{
protected array $props = ['tagName', 'size'];
protected string $tagName = 'div';
/**
@ -17,16 +19,17 @@ class Heading extends Component
public function render(): string
{
$sizeClasses = [
$sizeClass = match ($this->size) {
'small' => 'tracking-wide text-base',
'base' => 'text-xl',
'large' => 'text-3xl',
];
default => 'text-xl',
};
$class = $this->class . ' relative z-10 font-bold text-heading-foreground font-display before:w-full before:absolute before:h-1/2 before:left-0 before:bottom-0 before:rounded-full before:bg-heading-background before:z-[-10] ' . $sizeClasses[$this->size];
$this->mergeClass('relative z-10 font-bold text-heading-foreground font-display before:w-full before:absolute before:h-1/2 before:left-0 before:bottom-0 before:rounded-full before:bg-heading-background before:z-[-10]');
$this->mergeClass($sizeClass);
return <<<HTML
<{$this->tagName} class="{$class}">{$this->slot}</{$this->tagName}>
<{$this->tagName} {$this->getStringifiedAttributes()}>{$this->slot}</{$this->tagName}>
HTML;
}
}

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Views\Components;
use ViewComponents\Component;
class Hint extends Component
{
protected array $attributes = [
'data-tooltip' => 'bottom',
'tabindex' => '0',
];
public function render(): string
{
$this->attributes['title'] = $this->slot;
$this->mergeClass('inline-block align-middle opacity-75');
$icon = icon('question-fill');
return <<<HTML
<span {$this->getStringifiedAttributes()}>{$icon}</span>
HTML;
}
}

View File

@ -6,7 +6,9 @@ namespace App\Views\Components;
class IconButton extends Button
{
public string $glyph = '';
public string $glyph;
protected array $props = ['glyph'];
public function __construct(array $attributes)
{
@ -16,18 +18,18 @@ class IconButton extends Button
'data-tooltip' => 'bottom',
];
$glyphSize = [
'small' => 'text-sm',
'base' => 'text-lg',
'large' => 'text-2xl',
];
$allAttributes = [...$attributes, ...$iconButtonAttributes];
parent::__construct($allAttributes);
$glyphSizeClass = match ($this->size) {
'small' => 'text-sm',
'large' => 'text-2xl',
default => 'text-lg',
};
$this->slot = icon($this->glyph, [
'class' => $glyphSize[$this->size],
'class' => $glyphSizeClass,
]);
}
}

View File

@ -15,29 +15,44 @@ class Pill extends Component
public string $variant = 'default';
public ?string $icon = null;
public string $icon = '';
public ?string $iconClass = '';
public string $iconClass = '';
protected ?string $hint = null;
protected array $props = ['size', 'variant', 'icon', 'iconClass', 'hint'];
protected string $hint = '';
public function render(): string
{
$variantClasses = [
'default' => 'text-gray-800 bg-gray-100 border-gray-300',
$variantClass = match ($this->variant) {
'primary' => 'text-accent-contrast bg-accent-base border-accent-base',
'success' => 'text-pine-900 bg-pine-100 border-pine-300',
'danger' => 'text-red-900 bg-red-100 border-red-300',
'warning' => 'text-yellow-900 bg-yellow-100 border-yellow-300',
];
default => 'text-gray-800 bg-gray-100 border-gray-300',
};
$icon = $this->icon ? icon($this->icon, [
$sizeClass = match ($this->size) {
'small' => 'text-xs tracking-wide',
default => 'text-sm',
};
$icon = $this->icon !== '' ? icon($this->icon, [
'class' => $this->iconClass,
]) : '';
$hint = $this->hint ? 'data-tooltip="bottom" title="' . $this->hint . '"' : '';
if ($this->hint !== '') {
$this->attributes['data-tooltip'] = 'bottom';
$this->attributes['title'] = $this->hint;
}
$this->mergeClass('inline-flex lowercase items-center gap-x-1 px-1 font-semibold border rounded');
$this->mergeClass($variantClass);
$this->mergeClass($sizeClass);
return <<<HTML
<span class="inline-flex items-center gap-x-1 px-1 font-semibold text-sm border rounded {$variantClasses[$this->variant]} {$this->class}" {$hint}>{$icon}{$this->slot}</span>
<span {$this->getStringifiedAttributes()}>{$icon}{$this->slot}</span>
HTML;
}
}

View File

@ -8,17 +8,23 @@ use ViewComponents\Component;
class ReadMore extends Component
{
public string $id;
protected array $props = ['id'];
protected string $id;
public function render(): string
{
$readMoreLabel = lang('Common.read_more');
$readLessLabel = lang('Common.read_less');
$this->mergeClass('read-more');
$this->attributes['style'] = '--line-clamp: 3';
return <<<HTML
<div class="read-more {$this->class}" style="--line-clamp: 3">
<div {$this->getStringifiedAttributes()}>
<input id="read-more-checkbox_{$this->id}" type="checkbox" class="read-more__checkbox" aria-hidden="true">
<div class="mb-2 read-more__text">{$this->slot}</div>
<label for="read-more-checkbox_{$this->id}" class="read-more__label" data-read-more="{$readMoreLabel}" data-read-less="{$readLessLabel}" aria-hidden="true"></label>
<div class="read-more__text">{$this->slot}</div>
<label for="read-more-checkbox_{$this->id}" class="mt-2 read-more__label" data-read-more="{$readMoreLabel}" data-read-less="{$readLessLabel}" aria-hidden="true"></label>
</div>
HTML;
}

View File

@ -12,11 +12,15 @@ class SeeMore extends Component
{
$seeMoreLabel = lang('Common.see_more');
$seeLessLabel = lang('Common.see_less');
$this->mergeClass('see-more');
$this->attributes['styles'] = '--content-height: 10rem';
return <<<HTML
<div class="see-more" style="--content-height: 10rem">
<div {$this->getStringifiedAttributes()}>
<input id="see-more-checkbox" type="checkbox" class="see-more__checkbox" aria-hidden="true">
<div class="mb-2 see-more__content {$this->class}"><div class="see-more_content-fade"></div>{$this->slot}</div>
<label for="see-more-checkbox" class="see-more__label" data-see-more="{$seeMoreLabel}" data-see-less="{$seeLessLabel}" aria-hidden="true"></label>
<div class="see-more__content"><div class="see-more_content-fade"></div>{$this->slot}</div>
<label for="see-more-checkbox" class="mt-2 see-more__label" data-see-more="{$seeMoreLabel}" data-see-less="{$seeLessLabel}" aria-hidden="true"></label>
</div>
HTML;
}

View File

@ -1,18 +1,18 @@
<?php declare(strict_types=1);
if (session()->has('message')): ?>
<Alert variant="success" class="mb-4"><?= esc(session('message')) ?></Alert>
<Alert variant="success" class="mb-4"><?= session('message') ?></Alert>
<?php endif; ?>
<?php if (session()->has('error')): ?>
<Alert variant="danger" class="mb-4"><?= esc(session('error')) ?></Alert>
<Alert variant="danger" class="mb-4"><?= session('error') ?></Alert>
<?php endif; ?>
<?php if (session()->has('errors')): ?>
<Alert variant="danger" class="mb-4">
<ul>
<?php foreach (session('errors') as $error): ?>
<li><?= esc($error) ?></li>
<li><?= $error ?></li>
<?php endforeach; ?>
</ul>
</Alert>

View File

@ -23,7 +23,7 @@
You do not have sufficient permissions to access that page.
<?php endif; ?>
</p>
<a href="<?= previous_url() ?>" class="inline-flex items-center justify-center px-3 py-1 text-sm font-semibold rounded-full shadow-xs text-accent-contrast focus:ring-accent md:px-4 md:py-2 md:text-base bg-accent-base hover:bg-accent-hover"><?= lang('Common.go_back') ?></a>
<a href="<?= previous_url() ?>" class="inline-flex items-center justify-center px-3 py-1 text-sm font-semibold rounded-full shadow-xs text-accent-contrast md:px-4 md:py-2 md:text-base bg-accent-base hover:bg-accent-hover"><?= lang('Common.go_back') ?></a>
</body>
</html>

View File

@ -23,7 +23,7 @@
<?= lang('Errors.sorryCannotFind') ?>
<?php endif; ?>
</p>
<a href="<?= previous_url() ?>" class="inline-flex items-center justify-center px-3 py-1 text-sm font-semibold rounded-full shadow-xs text-accent-contrast focus:ring-accent md:px-4 md:py-2 md:text-base bg-accent-base hover:bg-accent-hover"><?= lang('Common.go_back') ?></a>
<a href="<?= previous_url() ?>" class="inline-flex items-center justify-center px-3 py-1 text-sm font-semibold rounded-full shadow-xs text-accent-contrast md:px-4 md:py-2 md:text-base bg-accent-base hover:bg-accent-hover"><?= lang('Common.go_back') ?></a>
</body>
</html>

View File

@ -28,7 +28,7 @@
<h2 class="font-mono font-semibold"><?= esc($title), esc($exception->getCode() ? ' #' . $exception->getCode() : '') ?></h2>
<p class="font-mono"><?= nl2br(esc($exception->getMessage())) ?><br/><span class="pl-4">at <span class="select-all bg-elevated"><?= nl2br(esc($exception->getFile())) ?>:<?= esc($exception->getLine()) ?></span></span></p>
<p id="error-stack-trace" class="hidden"><?= nl2br(esc($exception)) ?></p>
<clipboard-copy for="error-stack-trace" class="items-center self-end px-3 py-1 mt-2 font-semibold leading-8 transition-all rounded-full shadow group text-accent-contrast hover:bg-accent-hover bg-accent-base focus:ring-accent">
<clipboard-copy for="error-stack-trace" class="items-center self-end px-3 py-1 mt-2 font-semibold leading-8 transition-all rounded-full shadow group text-accent-contrast hover:bg-accent-hover bg-accent-base">
<span class="inline-flex items-center copy-base"><?= icon('file-copy-fill', [
'class' => 'mr-2',
]) ?>Copy stack trace</span>
@ -41,11 +41,11 @@
<div class="flex flex-col justify-center w-full gap-6 py-12 border-t-2 md:flex-row border-subtle">
<div class="w-full max-w-md mx-auto md:mx-0">
<h2 class="text-xl font-semibold font-display">Found a bug?</h2>
<p>You can help get it fixed by <a href="https://castopod.org/new-issue_bug" target="_blank" rel="noopener noreferrer" class="underline decoration-3 hover:no-underline focus:ring-accent decoration-accent">creating an issue on the Castopod issue tracker</a>. Please check that the issue does not already exist beforehand.</p>
<p>You can help get it fixed by <a href="https://castopod.org/new-issue_bug" target="_blank" rel="noopener noreferrer" class="underline decoration-3 hover:no-underline decoration-accent">creating an issue on the Castopod issue tracker</a>. Please check that the issue does not already exist beforehand.</p>
</div>
<div class="w-full max-w-md mx-auto md:mx-0">
<h2 class="text-xl font-semibold font-display">Not sure what's happening?</h2>
<p>You can ask for help in the <a href="https://castopod.org/chat" target="_blank" rel="noopener noreferrer" class="underline decoration-2 hover:no-underline focus:ring-accent decoration-accent">Castopod community chat</a>!</p>
<p>You can ask for help in the <a href="https://castopod.org/chat" target="_blank" rel="noopener noreferrer" class="underline decoration-2 hover:no-underline decoration-accent">Castopod community chat</a>!</p>
</div>
</div>
<?php else: ?>

View File

@ -41,6 +41,9 @@ class PluginController extends BaseController
$plugins = service('plugins');
$vendorPlugins = $plugins->getVendorPlugins($vendor);
replace_breadcrumb_params([
$vendor => $vendor,
]);
return view('plugins/installed', [
'total' => count($vendorPlugins),
'plugins' => $vendorPlugins,
@ -59,6 +62,10 @@ class PluginController extends BaseController
throw PageNotFoundException::forPageNotFound();
}
replace_breadcrumb_params([
$vendor => $vendor,
$package => $package,
]);
return view('plugins/view', [
'plugin' => $plugin,
]);
@ -76,6 +83,10 @@ class PluginController extends BaseController
}
helper('form');
replace_breadcrumb_params([
$vendor => $vendor,
$package => $package,
]);
return view('plugins/settings_general', [
'plugin' => $plugin,
]);
@ -122,7 +133,9 @@ class PluginController extends BaseController
helper('form');
replace_breadcrumb_params([
0 => $podcast->handle,
0 => $podcast->handle,
$vendor => $vendor,
$package => $package,
]);
return view('plugins/settings_podcast', [
'podcast' => $podcast,
@ -171,8 +184,10 @@ class PluginController extends BaseController
helper('form');
replace_breadcrumb_params([
0 => $episode->podcast->handle,
1 => $episode->title,
0 => $episode->podcast->handle,
1 => $episode->title,
$vendor => $vendor,
$package => $package,
]);
return view('plugins/settings_episode', [
'podcast' => $episode->podcast,

View File

@ -17,6 +17,8 @@ use League\CommonMark\MarkdownConverter;
use Modules\Plugins\ExternalImageProcessor;
use Modules\Plugins\ExternalLinkProcessor;
use Modules\Plugins\Manifest\Manifest;
use Modules\Plugins\Manifest\Person;
use Modules\Plugins\Manifest\Repository;
use Modules\Plugins\Manifest\Settings;
use Modules\Plugins\Manifest\SettingsField;
use RuntimeException;
@ -35,7 +37,7 @@ abstract class BasePlugin implements PluginInterface
protected Manifest $manifest;
protected string $readmeHTML;
protected ?string $readmeHTML;
public function __construct(
protected string $vendor,
@ -121,6 +123,27 @@ abstract class BasePlugin implements PluginInterface
return $this->manifest->homepage;
}
final public function getRepository(): ?Repository
{
return $this->manifest->repository;
}
/**
* @return list<string>
*/
final public function getKeywords(): array
{
return $this->manifest->keywords;
}
/**
* @return Person[]
*/
final public function getAuthors(): array
{
return $this->manifest->authors;
}
final public function getIconSrc(): string
{
return $this->iconSrc;
@ -194,11 +217,16 @@ abstract class BasePlugin implements PluginInterface
return $description;
}
final public function getReadmeHTML(): string
final public function getReadmeHTML(): ?string
{
return $this->readmeHTML;
}
final public function getLicense(): string
{
return $this->manifest->license ?? 'UNLICENSED';
}
final protected function getOption(string $option): mixed
{
return get_plugin_option($this->key, $option);
@ -238,7 +266,7 @@ abstract class BasePlugin implements PluginInterface
$environment = new Environment([
'html_input' => 'escape',
'allow_unsafe_links' => false,
'host' => 'hello',
'host' => (new URI(base_url()))->getHost(),
]);
$environment->addExtension(new CommonMarkCoreExtension());
@ -247,11 +275,15 @@ abstract class BasePlugin implements PluginInterface
$environment->addEventListener(
DocumentParsedEvent::class,
[new ExternalLinkProcessor($environment), 'onDocumentParsed']
static function (DocumentParsedEvent $event): void {
(new ExternalLinkProcessor())->onDocumentParsed($event);
}
);
$environment->addEventListener(
DocumentParsedEvent::class,
[new ExternalImageProcessor($environment), 'onDocumentParsed']
static function (DocumentParsedEvent $event): void {
(new ExternalImageProcessor())->onDocumentParsed($event);
}
);
$converter = new MarkdownConverter($environment);

View File

@ -35,6 +35,8 @@ class Plugins
protected static int $installedCount = 0;
protected static int $activeCount = 0;
public function __construct()
{
helper('plugins');
@ -63,6 +65,21 @@ class Plugins
return array_slice(static::$plugins, (($page - 1) * $perPage), $perPage);
}
/**
* @return array<BasePlugin>
*/
public function getActivePlugins(): array
{
$activePlugins = [];
foreach (static::$plugins as $plugin) {
if ($plugin->isActive()) {
$activePlugins[] = $plugin;
}
}
return $activePlugins;
}
/**
* @return array<BasePlugin>
*/
@ -177,6 +194,11 @@ class Plugins
return static::$installedCount;
}
public function getActiveCount(): int
{
return static::$activeCount;
}
public function uninstall(BasePlugin $plugin): bool
{
// remove all settings data
@ -235,6 +257,10 @@ class Plugins
static::$plugins[] = $plugin;
static::$pluginsByVendor[$vendor][] = $plugin;
++static::$installedCount;
if ($plugin->isActive()) {
++static::$activeCount;
}
}
}

View File

@ -5,19 +5,11 @@ declare(strict_types=1);
namespace Modules\Plugins;
use CodeIgniter\HTTP\URI;
use League\CommonMark\Environment\EnvironmentInterface;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Extension\CommonMark\Node\Inline\Image;
class ExternalImageProcessor
{
private EnvironmentInterface $environment;
public function __construct(EnvironmentInterface $environment)
{
$this->environment = $environment;
}
public function onDocumentParsed(DocumentParsedEvent $event): void
{
$document = $event->getDocument();
@ -47,7 +39,6 @@ class ExternalImageProcessor
$host = parse_url($url, PHP_URL_HOST);
// TODO: load from environment's config
// return $host != $this->environment->getConfiguration()->get('host');
return $host !== (new URI(base_url()))->getHost();
}
}

View File

@ -5,19 +5,11 @@ declare(strict_types=1);
namespace Modules\Plugins;
use CodeIgniter\HTTP\URI;
use League\CommonMark\Environment\EnvironmentInterface;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
class ExternalLinkProcessor
{
private EnvironmentInterface $environment;
public function __construct(EnvironmentInterface $environment)
{
$this->environment = $environment;
}
public function onDocumentParsed(DocumentParsedEvent $event): void
{
$document = $event->getDocument();
@ -48,7 +40,6 @@ class ExternalLinkProcessor
$host = parse_url($url, PHP_URL_HOST);
// TODO: load from environment's config
// return $host != $this->environment->getConfiguration()->get('host');
return $host !== (new URI(base_url()))->getHost();
}
}

View File

@ -9,11 +9,19 @@ declare(strict_types=1);
*/
return [
'installed' => 'Installed plugins ({count})',
'installed' => 'Plugins installed',
'about' => 'About',
'website' => 'Website',
'settings' => '{pluginName} settings',
'repository' => 'Code repository',
'authors' => 'Authors',
'author_email' => 'Email {authorName}',
'author_homepage' => '{authorName} homepage',
'settings' => 'Settings',
'view' => 'View',
'activate' => 'Activate',
'deactivate' => 'Deactivate',
'active' => 'Active',
'inactive' => 'Inactive',
'uninstall' => 'Uninstall',
'keywords' => [
'podcasting20' => 'Podcasting 2.0',

View File

@ -10,14 +10,15 @@ use CodeIgniter\HTTP\URI;
* @property string $name
* @property string $version
* @property ?string $description
* @property ?Author $author
* @property Author[] $authors
* @property Person[] $authors
* @property Person[] $contributors
* @property ?URI $homepage
* @property ?string $license
* @property bool $private
* @property list<string> $keywords
* @property list<string> $hooks
* @property ?Settings $settings
* @property ?Repository $repository
*/
class Manifest extends ManifestObject
{
@ -25,27 +26,27 @@ class Manifest extends ManifestObject
* @var array<string,string>
*/
protected const VALIDATION_RULES = [
'name' => 'required|max_length[32]',
'name' => 'required|max_length[128]',
'version' => 'required|regex_match[/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/]',
'description' => 'permit_empty|max_length[128]',
'author' => 'permit_empty',
'description' => 'permit_empty|max_length[256]',
'authors' => 'permit_empty|is_list',
'homepage' => 'permit_empty|valid_url_strict',
'license' => 'permit_empty|string',
'private' => 'permit_empty|is_boolean',
'keywords.*' => 'permit_empty',
'hooks.*' => 'permit_empty|in_list[channelTag,itemTag,siteHead]',
'settings' => 'permit_empty',
'settings' => 'permit_empty|is_list',
'repository' => 'permit_empty|is_list',
];
/**
* @var array<string,array{string}|string>
*/
protected const CASTS = [
'author' => Author::class,
'authors' => [Author::class],
'homepage' => URI::class,
'settings' => Settings::class,
'authors' => [Person::class],
'homepage' => URI::class,
'settings' => Settings::class,
'repository' => Repository::class,
];
protected string $name;
@ -54,10 +55,8 @@ class Manifest extends ManifestObject
protected ?string $description = null;
protected ?Author $author = null;
/**
* @var Author[]
* @var Person[]
*/
protected array $authors = [];
@ -78,4 +77,6 @@ class Manifest extends ManifestObject
protected array $hooks = [];
protected ?Settings $settings = null;
protected ?Repository $repository = null;
}

View File

@ -32,7 +32,7 @@ abstract class ManifestObject
public function __get(string $name): mixed
{
if (isset($this->{$name})) {
if (property_exists($this, $name)) {
return $this->{$name};
}

View File

@ -12,7 +12,7 @@ use Exception;
* @property ?string $email
* @property ?URI $url
*/
class Author extends ManifestObject
class Person extends ManifestObject
{
protected const VALIDATION_RULES = [
'name' => 'required',

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Modules\Plugins\Manifest;
use CodeIgniter\HTTP\URI;
/**
* @property string $type
* @property ?URI $url
* @property ?string $directory
*/
class Repository extends ManifestObject
{
protected const VALIDATION_RULES = [
'type' => 'required|in_list[git]',
'url' => 'required|valid_url_strict',
'directory' => 'permit_empty',
];
/**
* @var array<string,array{string}|string>
*/
protected const CASTS = [
'url' => URI::class,
];
protected string $type;
protected URI $url;
protected ?string $directory = null;
}

View File

@ -29,9 +29,9 @@ class SettingsField extends ManifestObject
protected string $label;
protected ?string $hint = '';
protected string $hint = '';
protected ?string $helper = '';
protected string $helper = '';
protected bool $optional = false;
}

View File

@ -21,9 +21,6 @@
"description": "This helps people discover your plugin as it's listed in repositories",
"type": "string"
},
"author": {
"$ref": "#/$defs/person"
},
"authors": {
"type": "array",
"items": {

View File

@ -139,6 +139,9 @@ module.exports = {
textDecoration: "none",
},
},
input: {
margin: 0,
},
},
},
sm: {

View File

@ -36,15 +36,15 @@ $isEpisodeArea = isset($podcast) && isset($episode);
<div class="flex flex-col justify-end w-full -mt-4 sticky-header-inner bg-elevated">
<?= render_breadcrumb('text-xs items-center flex') ?>
<div class="flex justify-between py-1">
<div class="flex flex-wrap items-center truncate">
<div class="flex flex-wrap items-center truncate gap-x-2">
<?php if (($isEpisodeArea && $episode->is_premium) || ($isPodcastArea && $podcast->is_premium)): ?>
<div class="inline-flex items-center">
<?php // @icon('exchange-dollar-fill')?>
<IconButton uri="<?= route_to('subscription-list', $podcast->id) ?>" glyph="exchange-dollar-fill" variant="secondary" size="large" class="p-0 mr-2 border-0"><?= ($isEpisodeArea && $episode->is_premium) ? lang('PremiumPodcasts.episode_is_premium') : lang('PremiumPodcasts.podcast_is_premium') ?></IconButton>
<Heading tagName="h1" size="large" class="truncate"><?= $this->renderSection('pageTitle') ?></Heading>
<x-IconButton uri="<?= route_to('subscription-list', $podcast->id) ?>" glyph="exchange-dollar-fill" variant="secondary" size="large" class="p-0 mr-2 border-0"><?= ($isEpisodeArea && $episode->is_premium) ? lang('PremiumPodcasts.episode_is_premium') : lang('PremiumPodcasts.podcast_is_premium') ?></x-IconButton>
<x-Heading tagName="h1" size="large" class="truncate"><?= $this->renderSection('pageTitle') ?></x-Heading>
</div>
<?php else: ?>
<Heading tagName="h1" size="large" class="truncate"><?= $this->renderSection('pageTitle') ?></Heading>
<x-Heading tagName="h1" size="large" class="truncate"><?= $this->renderSection('pageTitle') ?></x-Heading>
<?php endif; ?>
<?= $this->renderSection('headerLeft') ?>
</div>

View File

@ -1,33 +1,33 @@
<?php declare(strict_types=1);
if (session()->has('message')): ?>
<Alert variant="success" class="mb-4"><?= esc(session('message')) ?></Alert>
<x-Alert variant="success" class="mb-4"><?= esc(session('message')) ?></x-Alert>
<?php endif; ?>
<?php if (session()->has('error')): ?>
<Alert variant="danger" class="mb-4"><?= esc(session('error')) ?></Alert>
<x-Alert variant="danger" class="mb-4"><?= esc(session('error')) ?></x-Alert>
<?php endif; ?>
<?php if (session()->has('errors')): ?>
<Alert variant="danger" class="mb-4">
<x-Alert variant="danger" class="mb-4">
<ul>
<?php foreach (session('errors') as $error): ?>
<li><?= esc($error) ?></li>
<?php endforeach; ?>
</ul>
</Alert>
</x-Alert>
<?php endif; ?>
<?php if (session()->has('warning')): ?>
<Alert variant="warning" class="mb-4"><?= esc(session('warning')) ?></Alert>
<x-Alert variant="warning" class="mb-4"><?= esc(session('warning')) ?></x-Alert>
<?php endif; ?>
<?php if (session()->has('warnings')): ?>
<Alert variant="warning" class="mb-4">
<x-Alert variant="warning" class="mb-4">
<ul>
<?php foreach (session('warnings') as $warning): ?>
<li><?= esc($warning) ?></li>
<?php endforeach; ?>
</ul>
</Alert>
</x-Alert>
<?php endif; ?>

View File

@ -15,7 +15,7 @@ $isEpisodeArea = isset($podcast) && isset($episode);
<?php endif; ?>
<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 focus:ring-accent" href="https://castopod.org/" target="_blank" rel="noreferrer noopener">Castopod' . icon('social:castopod', [
'castopod' => '<a class="inline-flex font-semibold hover:underline" href="https://castopod.org/" target="_blank" rel="noreferrer noopener">Castopod' . icon('social:castopod', [
'class' => 'ml-1 text-lg',
]) . '</a> ' .
CP_VERSION,

View File

@ -5,18 +5,18 @@ $userPodcasts = get_podcasts_user_can_interact_with(auth()->user()); ?>
<header class="sticky top-0 z-[60] flex items-center h-10 text-white border-b col-span-full bg-navigation border-navigation">
<button type="button"
data-sidebar-toggler="toggler"
class="h-full pr-1 text-xl md:hidden focus:ring-accent focus:ring-inset" aria-label="<?= lang('Navigation.toggle_sidebar') ?>"><?= icon('menu-2-fill') ?></button>
class="h-full pr-1 text-xl md:hidden" aria-label="<?= lang('Navigation.toggle_sidebar') ?>"><?= icon('menu-2-fill') ?></button>
<div class="inline-flex items-center h-full">
<a href="<?= route_to(
'admin',
) ?>" class="inline-flex items-center h-full px-2 border-r border-navigation focus:ring-inset focus:ring-accent">
) ?>" class="inline-flex items-center h-full px-2 border-r border-navigation">
<?= (isset($podcast) ? icon('arrow-left-line', [
'class' => 'mr-2',
]) : '') . svg('castopod-logo-base', 'h-6') ?>
</a>
<a href="<?= route_to(
'home',
) ?>" class="inline-flex items-center h-full px-2 text-sm font-semibold sm:px-6 hover:underline focus:ring-inset focus:ring-accent" title="<?= lang('Navigation.go_to_website') ?>">
) ?>" class="inline-flex items-center h-full px-2 text-sm font-semibold sm:px-6 hover:underline" title="<?= lang('Navigation.go_to_website') ?>">
<span class="hidden sm:block"><?= lang('Navigation.go_to_website') ?></span>
<?= icon('external-link-fill', [
'class' => 'sm:ml-1 text-xl sm:text-base sm:opacity-60',
@ -24,7 +24,7 @@ $userPodcasts = get_podcasts_user_can_interact_with(auth()->user()); ?>
</a>
</div>
<div class="inline-flex items-center h-full ml-auto">
<button type="button" class="relative h-full px-2 focus:ring-accent focus:ring-inset" id="notifications-dropdown" data-dropdown="button" data-dropdown-target="notifications-dropdown-menu" aria-haspopup="true" aria-expanded="false" title="<?= lang('Notifications.title') ?>" data-tooltip="bottom">
<button type="button" class="relative h-full px-2" id="notifications-dropdown" data-dropdown="button" data-dropdown-target="notifications-dropdown-menu" aria-haspopup="true" aria-expanded="false" title="<?= lang('Notifications.title') ?>" data-tooltip="bottom">
<?= icon('notification-2-fill', [
'class' => 'text-2xl opacity-80',
]) ?>
@ -74,11 +74,11 @@ if ($userPodcasts !== []) {
];
}
?>
<DropdownMenu id="notifications-dropdown-menu" labelledby="notifications-dropdown" items="<?= esc(json_encode($items)) ?>" placement="bottom-end"/>
<x-DropdownMenu id="notifications-dropdown-menu" labelledby="notifications-dropdown" items="<?= esc(json_encode($items)) ?>" placement="bottom-end"/>
<button
type="button"
class="inline-flex items-center h-full px-3 text-sm font-semibold focus:ring-inset focus:ring-accent gap-x-2"
class="inline-flex items-center h-full px-3 text-sm font-semibold gap-x-2"
id="my-account-dropdown"
data-dropdown="button"
data-dropdown-target="my-account-dropdown-menu"
@ -154,5 +154,5 @@ if ($userPodcasts !== []) {
], $menuItems);
}
?>
<DropdownMenu id="my-account-dropdown-menu" labelledby="my-account-dropdown" items="<?= esc(json_encode($menuItems)) ?>" />
<x-DropdownMenu id="my-account-dropdown-menu" labelledby="my-account-dropdown" items="<?= esc(json_encode($menuItems)) ?>" />
</header>

View File

@ -15,18 +15,18 @@
}
?>
<details <?= $isSectionActive ? 'open="open"' : '' ?> class="<?= $isSectionActive ? 'bg-navigation-active' : '' ?> [&[open]>summary::after]:rotate-90">
<summary class="inline-flex items-center w-full px-4 py-2 font-semibold focus:ring-accent focus:ring-inset after:w-5 after:h-5 after:transition-transform after:content-chevronRightIcon after:ml-2 after:opacity-60 after:text-white">
<summary class="inline-flex items-center w-full h-12 px-4 py-2 font-semibold after:w-5 after:h-5 after:transition-transform after:content-chevronRightIcon after:ml-2 after:opacity-60 after:text-white">
<div class="inline-flex items-center mr-auto">
<?= icon($data['icon'], [
'class' => 'opacity-60 text-2xl mr-4',
]) ?>
<?= lang($langKey . '.' . $section) ?>
<?php if (array_key_exists('count', $data)): ?>
<a href="<?= route_to($data['count-route'], $podcastId ?? null, $episodeId ?? null) ?>" class="px-2 ml-2 text-xs font-normal rounded-full focus:ring-accent <?= $isSectionActive ? 'bg-navigation' : 'bg-navigation-active' ?>"><?= $data['count'] ?></a>
<a href="<?= route_to($data['count-route'], $podcastId ?? null, $episodeId ?? null) ?>" class="px-2 ml-2 text-xs font-normal rounded-full <?= $isSectionActive ? 'bg-navigation' : 'bg-navigation-active' ?>"><?= $data['count'] ?></a>
<?php endif; ?>
</div>
<?php if(array_key_exists('add-cta', $data)): ?>
<a href="<?= route_to($data['add-cta'], $podcastId ?? null, $episodeId ?? null) ?>" class="p-2 rounded-full shadow bg-accent-base focus:ring-accent" title="<?= lang($langKey . '.' . $data['add-cta']) ?>" data-tooltip="bottom">
<a href="<?= route_to($data['add-cta'], $podcastId ?? null, $episodeId ?? null) ?>" class="p-2 rounded-full shadow bg-accent-base" title="<?= lang($langKey . '.' . $data['add-cta']) ?>" data-tooltip="bottom">
<?= icon('add-fill') ?>
</a>
<?php endif; ?>
@ -34,7 +34,8 @@
<ul class="flex flex-col pb-4">
<?php foreach ($data['items'] as $key => $item):
$isActive = $item === $activeItem;
$label = array_key_exists('items-labels', $data) ? $data['items-labels'][$key] : lang($langKey . '.' . $item);
$label = (array_key_exists('items-labels', $data) && array_key_exists($item, $data['items-labels'])) ? $data['items-labels'][$item] : lang($langKey . '.' . $item);
$href = str_starts_with($item, '/') ? $item : route_to($item, $podcastId ?? null, $episodeId ?? null);
$isAllowed = true;
@ -48,11 +49,11 @@
?>
<li class="inline-flex">
<?php if ($isAllowed): ?>
<a class="relative w-full py-3 pl-14 pr-2 text-sm hover:opacity-100 before:content-chevronRightIcon before:absolute before:-ml-5 before:opacity-0 before:w-5 before:h-5 hover:bg-navigation-active focus:ring-inset focus:ring-accent<?= $isActive
<a class="relative w-full py-3 pl-14 pr-2 text-sm hover:opacity-100 before:content-chevronRightIcon before:absolute before:-ml-5 before:opacity-0 before:w-5 before:h-5 hover:bg-navigation-active<?= $isActive
? ' before:opacity-100 font-semibold inline-flex items-center'
: ' hover:before:opacity-60 focus:before:opacity-60' ?>" href="<?= $href ?>"><?= $label ?></a>
<?php else: ?>
<span data-tooltip="right" title="<?= lang('Navigation.not-authorized') ?>" class="relative w-full py-3 pr-2 text-sm cursor-not-allowed before:inset-y-0 before:my-auto pl-14 hover:opacity-100 before:absolute before:content-prohibitedIcon before:-ml-5 before:opacity-60 before:w-4 before:h-4 hover:bg-navigation-active focus:ring-inset focus:ring-accent"><?= $label ?></span>
<span data-tooltip="right" title="<?= lang('Navigation.not-authorized') ?>" class="relative w-full py-3 pr-2 text-sm cursor-not-allowed before:inset-y-0 before:my-auto pl-14 hover:opacity-100 before:absolute before:content-prohibitedIcon before:-ml-5 before:opacity-60 before:w-4 before:h-4 hover:bg-navigation-active"><?= $label ?></span>
<?php endif; ?>
</li>
<?php endforeach; ?>

View File

@ -22,12 +22,15 @@ $navigation = [
'count-route' => 'podcast-list',
],
'plugins' => [
'icon' => 'puzzle-fill', // @icon('puzzle-fill')
'items' => ['plugins-installed'],
'icon' => 'puzzle-fill', // @icon('puzzle-fill')
'items' => ['plugins-installed'],
'items-labels' => [
'plugins-installed' => lang('Navigation.plugins-installed') . ' (' . service('plugins')->getInstalledCount() . ')',
],
'items-permissions' => [
'plugins-installed' => 'plugins.manage',
],
'count' => service('plugins')->getInstalledCount(),
'count' => service('plugins')->getActiveCount(),
'count-route' => 'plugins-installed',
],
'persons' => [
@ -82,12 +85,20 @@ $navigation = [
],
];
foreach (plugins()->getActivePlugins() as $plugin) {
$route = route_to('plugins-view', $plugin->getKey());
$navigation['plugins']['items'][] = $route;
$navigation['plugins']['items-labels'][$route] = $plugin->getName();
$navigation['plugins']['items-permissions'][$route] = 'plugins.manage';
}
if (auth()->user()->can('podcasts.view')) {
$navigation['podcasts']['count'] = (new PodcastModel())->countAllResults();
} else {
$navigation['podcasts']['count'] = count(get_user_podcasts(auth()->user()));
} ?>
<?= view('_partials/_nav_menu', [
'navigation' => $navigation,
'langKey' => 'Navigation',

View File

@ -14,24 +14,24 @@
<form method="POST" action="<?= route_to('contributor-add', $podcast->id) ?>" class="flex flex-col max-w-sm gap-y-4">
<?= csrf_field() ?>
<Forms.Field
<x-Forms.Field
as="Select"
name="user"
label="<?= esc(lang('Contributor.form.user')) ?>"
options="<?= esc(json_encode($contributorOptions)) ?>"
placeholder="<?= lang('Contributor.form.user_placeholder') ?>"
required="true" />
isRequired="true" />
<Forms.Field
<x-Forms.Field
as="Select"
name="role"
label="<?= esc(lang('Contributor.form.role')) ?>"
options="<?= esc(json_encode($roleOptions)) ?>"
placeholder="<?= lang('Contributor.form.role_placeholder') ?>"
selected="<?= setting('AuthGroups.defaultPodcastGroup') ?>"
required="true" />
isRequired="true" />
<Button type="submit" class="self-end" variant="primary"><?= lang('Contributor.form.submit_add') ?></Button>
<x-Button type="submit" class="self-end" variant="primary"><?= lang('Contributor.form.submit_add') ?></x-Button>
</form>

View File

@ -17,19 +17,19 @@
<form action="<?= route_to('contributor-delete', $podcast->id, $contributor->id) ?>" method="POST" class="flex flex-col w-full max-w-xl mx-auto">
<?= csrf_field() ?>
<Alert variant="danger" class="font-semibold"><?= lang('Contributor.delete_form.disclaimer', [
<x-Alert variant="danger" class="font-semibold"><?= lang('Contributor.delete_form.disclaimer', [
'contributor' => $contributor->username,
'podcastTitle' => $podcast->title,
]) ?></Alert>
]) ?></x-Alert>
<Forms.Checkbox class="mt-2" name="understand" required="true" isChecked="false"><?= lang('Contributor.delete_form.understand', [
<x-Forms.Checkbox class="mt-2" name="understand" isRequired="true" isChecked="false"><?= lang('Contributor.delete_form.understand', [
'contributor' => $contributor->username,
'podcastTitle' => $podcast->title,
]) ?></Forms.Checkbox>
]) ?></x-Forms.Checkbox>
<div class="self-end mt-4">
<Button uri="<?= route_to('contributor-view', $podcast->id, $contributor->id) ?>"><?= lang('Common.cancel') ?></Button>
<Button type="submit" variant="danger"><?= lang('Contributor.delete_form.submit') ?></Button>
<x-Button uri="<?= route_to('contributor-view', $podcast->id, $contributor->id) ?>"><?= lang('Common.cancel') ?></x-Button>
<x-Button type="submit" variant="danger"><?= lang('Contributor.delete_form.submit') ?></x-Button>
</div>
</form>

View File

@ -14,16 +14,16 @@
<form method="POST" action="<?= route_to('contributor-edit', $podcast->id, $contributor->id) ?>" class="flex flex-col max-w-sm gap-y-4">
<?= csrf_field() ?>
<Forms.Field
<x-Forms.Field
as="Select"
name="role"
label="<?= esc(lang('Contributor.form.role')) ?>"
options="<?= esc(json_encode($roleOptions)) ?>"
selected="<?= $contributorGroup ?>"
placeholder="<?= lang('Contributor.form.role_placeholder') ?>"
required="true" />
isRequired="true" />
<Button variant="primary" type="submit" class="self-end"><?= lang('Contributor.form.submit_edit') ?></Button>
<x-Button variant="primary" type="submit" class="self-end"><?= lang('Contributor.form.submit_edit') ?></x-Button>
</form>

View File

@ -10,7 +10,7 @@
<?= $this->section('headerRight') ?>
<?php // @icon('add-fill')?>
<Button uri="<?= route_to('contributor-add', $podcast->id) ?>" variant="primary" iconLeft="add-fill"><?= lang('Contributor.add') ?></Button>
<x-Button uri="<?= route_to('contributor-add', $podcast->id) ?>" variant="primary" iconLeft="add-fill"><?= lang('Contributor.add') ?></x-Button>
<?= $this->endSection() ?>
@ -30,7 +30,7 @@
$role = get_group_info(get_podcast_group($contributor, $podcast->id), $podcast->id)['title'];
if ($podcast->created_by === $contributor->id) {
$role = '<div class="inline-flex items-center"><span class="mr-2 focus:ring-accent" tabindex="0" data-tooltip="bottom" title="' . lang('Auth.podcast_groups.owner.title') . '">' . icon('shield-user-fill') . '</span>' . $role . '</div>';
$role = '<div class="inline-flex items-center"><span class="mr-2" tabindex="0" data-tooltip="bottom" title="' . lang('Auth.podcast_groups.owner.title') . '">' . icon('shield-user-fill') . '</span>' . $role . '</div>';
}
return $role;
@ -41,8 +41,8 @@
'cell' => function ($contributor, $podcast) {
// @icon('pencil-fill')
// @icon('delete-bin-fill')
return '<Button uri="' . route_to('contributor-edit', $podcast->id, $contributor->id) . '" variant="secondary" iconLeft="pencil-fill" size="small">' . lang('Contributor.edit') . '</Button>' .
'<Button uri="' . route_to('contributor-remove', $podcast->id, $contributor->id) . '" variant="danger" iconLeft="delete-bin-fill" size="small">' . lang('Contributor.remove') . '</Button>';
return '<x-Button uri="' . route_to('contributor-edit', $podcast->id, $contributor->id) . '" variant="secondary" iconLeft="pencil-fill" size="small">' . lang('Contributor.edit') . '</x-Button>' .
'<x-Button uri="' . route_to('contributor-remove', $podcast->id, $contributor->id) . '" variant="danger" iconLeft="delete-bin-fill" size="small">' . lang('Contributor.remove') . '</x-Button>';
},
],
],

View File

@ -13,27 +13,27 @@
<div class="flex flex-col items-stretch gap-4 lg:flex-row">
<?php // @icon('mic-fill')?>
<DashboardCard href="<?= $onlyPodcastId === null ? route_to('podcast-list') : route_to('podcast-view', $onlyPodcastId) ?>" glyph="mic-fill" title="<?= lang('Dashboard.podcasts.title') ?>" subtitle="<?= $podcastsData['last_published_at'] ? esc(lang('Dashboard.podcasts.last_published', [
<x-DashboardCard href="<?= $onlyPodcastId === null ? route_to('podcast-list') : route_to('podcast-view', $onlyPodcastId) ?>" glyph="mic-fill" title="<?= lang('Dashboard.podcasts.title') ?>" subtitle="<?= $podcastsData['last_published_at'] ? esc(lang('Dashboard.podcasts.last_published', [
'lastPublicationDate' => local_date($podcastsData['last_published_at']),
], null, false)) : lang('Dashboard.podcasts.not_found') ?>"><?= $podcastsData['number_of_podcasts'] ?></DashboardCard>
], null, false)) : lang('Dashboard.podcasts.not_found') ?>"><?= $podcastsData['number_of_podcasts'] ?></x-DashboardCard>
<?php // @icon('play-fill')?>
<DashboardCard href="<?= $onlyPodcastId === null ? '' : route_to('episode-list', $onlyPodcastId) ?>" glyph="play-fill" title="<?= lang('Dashboard.episodes.title') ?>" subtitle="<?= $episodesData['last_published_at'] ? esc(lang('Dashboard.episodes.last_published', [
<x-DashboardCard href="<?= $onlyPodcastId === null ? '' : route_to('episode-list', $onlyPodcastId) ?>" glyph="play-fill" title="<?= lang('Dashboard.episodes.title') ?>" subtitle="<?= $episodesData['last_published_at'] ? esc(lang('Dashboard.episodes.last_published', [
'lastPublicationDate' => local_date($episodesData['last_published_at']),
], null, false)) : lang('Dashboard.episodes.not_found') ?>"><?= $episodesData['number_of_episodes'] ?></DashboardCard>
], null, false)) : lang('Dashboard.episodes.not_found') ?>"><?= $episodesData['number_of_episodes'] ?></x-DashboardCard>
<?php // @icon('database-2-fill')?>
<DashboardCard glyph="database-2-fill" title="<?= lang('Dashboard.storage.title') ?>" subtitle="<?= lang('Dashboard.storage.subtitle', [
<x-DashboardCard glyph="database-2-fill" title="<?= lang('Dashboard.storage.title') ?>" subtitle="<?= lang('Dashboard.storage.subtitle', [
'totalUploaded' => $storageData['total_uploaded'],
'totalStorage' => $storageData['limit'],
]) ?>"><?= $storageData['percentage'] ?>%</DashboardCard>
]) ?>"><?= $storageData['percentage'] ?>%</x-DashboardCard>
</div>
<div class="grid grid-cols-1 gap-4 mt-4 lg:grid-cols-2">
<Charts.XY class="col-span-1" title="<?= lang('Charts.total_storage_by_month') ?>" dataUrl="<?= route_to(
<x-Charts.XY class="col-span-1" title="<?= lang('Charts.total_storage_by_month') ?>" dataUrl="<?= route_to(
'analytics-data-instance',
'Podcast',
'TotalStorageByMonth',
) ?>" />
<Charts.XY class="col-span-1" title="<?= lang('Charts.total_bandwidth_by_month') ?>" subtitle="<?= $bandwidthLimit !== null ? lang('Charts.total_bandwidth_by_month_limit', [
<x-Charts.XY class="col-span-1" title="<?= lang('Charts.total_bandwidth_by_month') ?>" subtitle="<?= $bandwidthLimit !== null ? lang('Charts.total_bandwidth_by_month_limit', [
'totalBandwidth' => $bandwidthLimit,
]) : '' ?>" dataUrl="<?= route_to(
'analytics-data-instance',

View File

@ -19,7 +19,7 @@
<span class="font-semibold leading-tight line-clamp-2"><?= esc($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-accent 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-2-fill') ?></button>
<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: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-2-fill') ?></button>
<?php $items = [
[
'type' => 'link',
@ -75,5 +75,5 @@ if ($episode->published_at === null) {
HTML),
];
} ?>
<DropdownMenu id="more-dropdown-<?= $episode->id ?>-menu" labelledby="more-dropdown-<?= $episode->id ?>" offsetY="-32" items="<?= esc(json_encode($items)) ?>" />
<x-DropdownMenu id="more-dropdown-<?= $episode->id ?>-menu" labelledby="more-dropdown-<?= $episode->id ?>" offsetY="-32" items="<?= esc(json_encode($items)) ?>" />
</article>

View File

@ -35,13 +35,13 @@ $episodeNavigation = [
foreach (plugins()->getPluginsWithEpisodeSettings() as $plugin) {
$route = route_to('plugins-episode-settings', $podcast->id, $episode->id, $plugin->getKey());
$episodeNavigation['plugins']['items'][] = $route;
$episodeNavigation['plugins']['items-labels'][] = $plugin->getName();
$episodeNavigation['plugins']['items-labels'][$route] = $plugin->getName();
$episodeNavigation['plugins']['items-permissions'][$route] = 'episodes.edit';
}
?>
<a href="<?= route_to('podcast-view', $podcast->id) ?>" class="flex items-center px-4 py-2 focus:ring-inset focus:ring-accent">
<a href="<?= route_to('podcast-view', $podcast->id) ?>" class="flex items-center px-4 py-2">
<?= icon('arrow-left-line', [
'class' => 'mr-2',
]) ?>
@ -71,7 +71,7 @@ foreach (plugins()->getPluginsWithEpisodeSettings() as $plugin) {
'episode',
esc($podcast->handle),
esc($episode->slug),
) ?>" class="inline-flex items-center text-xs hover:underline focus:ring-accent"><?= lang(
) ?>" class="inline-flex items-center text-xs hover:underline"><?= lang(
'EpisodeNavigation.go_to_page',
) ?>
<?= icon('external-link-fill', [

View File

@ -15,20 +15,20 @@
<?= csrf_field() ?>
<Forms.Section title="<?= lang('Episode.form.info_section_title') ?>" >
<x-Forms.Section title="<?= lang('Episode.form.info_section_title') ?>" >
<Forms.Field
<x-Forms.Field
name="audio_file"
label="<?= esc(lang('Episode.form.audio_file')) ?>"
hint="<?= esc(lang('Episode.form.audio_file_hint')) ?>"
helper="<?= esc(lang('Common.size_limit', [formatBytes(file_upload_max_size(), true)])) ?>"
type="file"
accept=".mp3,.m4a"
required="true"
isRequired="true"
data-max-size="<?= file_upload_max_size() ?>"
data-max-size-error="<?= lang('Episode.form.file_size_error', [formatBytes(file_upload_max_size(), true)]) ?>" />
<Forms.Field
<x-Forms.Field
name="cover"
label="<?= esc(lang('Episode.form.cover')) ?>"
hint="<?= esc(lang('Episode.form.cover_hint')) ?>"
@ -36,30 +36,30 @@
type="file"
accept=".jpg,.jpeg,.png" />
<Forms.Field
<x-Forms.Field
name="title"
label="<?= esc(lang('Episode.form.title')) ?>"
hint="<?= esc(lang('Episode.form.title_hint')) ?>"
required="true"
isRequired="true"
data-slugify="title" />
<div>
<Forms.Label for="slug"><?= lang('Episode.form.permalink') ?></Forms.Label>
<x-Forms.Label for="slug"><?= lang('Episode.form.permalink') ?></x-Forms.Label>
<permalink-edit class="inline-flex items-center w-full text-xs" edit-label="<?= lang('Common.edit') ?>" copy-label="<?= lang('Common.copy') ?>" copied-label="<?= lang('Common.copied') ?>" permalink-base="<?= url_to('podcast-episodes', $podcast->handle) ?>">
<span slot="domain"><?= '…/' . esc($podcast->at_handle) . '/' ?></span>
<Forms.Input name="slug" required="true" data-slugify="slug" slot="slug-input" class="flex-1 text-xs" />
<x-Forms.Input name="slug" isRequired="true" data-slugify="slug" slot="slug-input" class="flex-1 text-xs" />
</permalink-edit>
</div>
<div class="flex flex-col gap-x-2 gap-y-4 md:flex-row">
<Forms.Field
<x-Forms.Field
class="flex-1 w-full"
name="season_number"
label="<?= esc(lang('Episode.form.season_number')) ?>"
type="number"
value="<?= $currentSeasonNumber ?>"
/>
<Forms.Field
<x-Forms.Field
class="flex-1 w-full"
name="episode_number"
label="<?= esc(lang('Episode.form.episode_number')) ?>"
@ -71,59 +71,56 @@
<fieldset class="flex gap-1">
<legend><?= lang('Episode.form.type.label') ?></legend>
<Forms.RadioButton
<x-Forms.RadioButton
value="full"
name="type"
hint="<?= esc(lang('Episode.form.type.full_hint')) ?>"
isChecked="true" ><?= lang('Episode.form.type.full') ?></Forms.RadioButton>
<Forms.RadioButton
isChecked="true" ><?= lang('Episode.form.type.full') ?></x-Forms.RadioButton>
<x-Forms.RadioButton
value="trailer"
name="type"
hint="<?= esc(lang('Episode.form.type.trailer_hint')) ?>"
isChecked="false" ><?= lang('Episode.form.type.trailer') ?></Forms.RadioButton>
<Forms.RadioButton
isChecked="false" ><?= lang('Episode.form.type.trailer') ?></x-Forms.RadioButton>
<x-Forms.RadioButton
value="bonus"
name="type"
hint="<?= esc(lang('Episode.form.type.bonus_hint')) ?>"
isChecked="false" ><?= lang('Episode.form.type.bonus') ?></Forms.RadioButton>
isChecked="false" ><?= lang('Episode.form.type.bonus') ?></x-Forms.RadioButton>
</fieldset>
<fieldset class="flex gap-1">
<legend>
<?= lang('Episode.form.parental_advisory.label') .
hint_tooltip(lang('Episode.form.parental_advisory.hint'), 'ml-1') ?>
</legend>
<Forms.RadioButton
<legend><?= lang('Episode.form.parental_advisory.label') ?><x-Hint class="ml-1"><?= lang('Episode.form.parental_advisory.hint') ?></x-Hint></legend>
<x-Forms.RadioButton
value="undefined"
name="parental_advisory"
isChecked="true" ><?= lang('Episode.form.parental_advisory.undefined') ?></Forms.RadioButton>
<Forms.RadioButton
isChecked="true" ><?= lang('Episode.form.parental_advisory.undefined') ?></x-Forms.RadioButton>
<x-Forms.RadioButton
value="clean"
name="parental_advisory"
isChecked="false" ><?= lang('Episode.form.parental_advisory.clean') ?></Forms.RadioButton>
<Forms.RadioButton
isChecked="false" ><?= lang('Episode.form.parental_advisory.clean') ?></x-Forms.RadioButton>
<x-Forms.RadioButton
value="explicit"
name="parental_advisory"
isChecked="false" ><?= lang('Episode.form.parental_advisory.explicit') ?></Forms.RadioButton>
isChecked="false" ><?= lang('Episode.form.parental_advisory.explicit') ?></x-Forms.RadioButton>
</fieldset>
</Forms.Section>
</x-Forms.Section>
<Forms.Section
<x-Forms.Section
title="<?= lang('Episode.form.show_notes_section_title') ?>"
subtitle="<?= lang('Episode.form.show_notes_section_subtitle') ?>">
<Forms.Field
<x-Forms.Field
as="MarkdownEditor"
name="description"
label="<?= esc(lang('Episode.form.description')) ?>"
required="true"
isRequired="true"
disallowList="header,quote" />
<Forms.Field
<x-Forms.Field
as="MarkdownEditor"
name="description_footer"
label="<?= esc(lang('Episode.form.description_footer')) ?>"
@ -131,32 +128,31 @@
value="<?= esc($podcast->episode_description_footer_markdown) ?? '' ?>"
disallowList="header,quote" />
</Forms.Section>
</x-Forms.Section>
<Forms.Section title="<?= lang('Episode.form.premium_title') ?>">
<Forms.Toggler class="mt-2" name="premium" value="yes" checked="<?= $podcast->is_premium_by_default ? 'true' : 'false' ?>">
<?= lang('Episode.form.premium') ?></Forms.Toggler>
</Forms.Section>
<x-Forms.Section title="<?= lang('Episode.form.premium_title') ?>">
<x-Forms.Toggler class="mt-2" name="premium" isChecked="<?= $podcast->is_premium_by_default ? 'true' : 'false' ?>">
<?= lang('Episode.form.premium') ?></x-Forms.Toggler>
</x-Forms.Section>
<Forms.Section
<x-Forms.Section
title="<?= lang('Episode.form.location_section_title') ?>"
subtitle="<?= lang('Episode.form.location_section_subtitle') ?>"
>
<Forms.Field
<x-Forms.Field
name="location_name"
label="<?= esc(lang('Episode.form.location_name')) ?>"
hint="<?= esc(lang('Episode.form.location_name_hint')) ?>" />
</Forms.Section>
</x-Forms.Section>
<Forms.Section
<x-Forms.Section
title="<?= lang('Episode.form.additional_files_section_title') ?>">
<fieldset class="flex flex-col">
<legend><?= lang('Episode.form.transcript') .
'<small class="ml-1 lowercase">(' .
lang('Common.optional') .
')</small>' .
hint_tooltip(lang('Episode.form.transcript_hint'), 'ml-1') ?></legend>
')</small>' ?><x-Hint class="ml-1"><?= lang('Episode.form.transcript_hint') ?></x-Hint></legend>
<div class="form-input-tabs">
<input type="radio" name="transcript-choice" id="transcript-file-upload-choice" aria-controls="transcript-file-upload-choice" value="upload-file" <?= old('transcript-choice') !== 'remote-file' ? 'checked' : '' ?> />
<label for="transcript-file-upload-choice"><?= lang('Common.forms.upload_file') ?></label>
@ -166,12 +162,12 @@
<div class="py-2 tab-panels">
<section id="transcript-file-upload" class="flex items-center tab-panel">
<Forms.Label class="sr-only" for="transcript_file" isOptional="true"><?= lang('Episode.form.transcript_file') ?></Forms.Label>
<Forms.Input class="w-full" name="transcript_file" type="file" accept=".srt,.vtt" />
<x-Forms.Label class="sr-only" for="transcript_file" isOptional="true"><?= lang('Episode.form.transcript_file') ?></x-Forms.Label>
<x-Forms.Input class="w-full" name="transcript_file" type="file" accept=".srt,.vtt" />
</section>
<section id="transcript-file-remote-url" class="tab-panel">
<Forms.Label class="sr-only" for="transcript_remote_url" isOptional="true"><?= lang('Episode.form.transcript_remote_url') ?></Forms.Label>
<Forms.Input class="w-full" placeholder="https://…" name="transcript_remote_url" />
<x-Forms.Label class="sr-only" for="transcript_remote_url" isOptional="true"><?= lang('Episode.form.transcript_remote_url') ?></x-Forms.Label>
<x-Forms.Input class="w-full" placeholder="https://…" name="transcript_remote_url" />
</section>
</div>
</div>
@ -182,8 +178,7 @@
<legend><?= lang('Episode.form.chapters') .
'<small class="ml-1 lowercase">(' .
lang('Common.optional') .
')</small>' .
hint_tooltip(lang('Episode.form.chapters_hint'), 'ml-1') ?></legend>
')</small>' ?><x-Hint class="ml-1"><?= lang('Episode.form.chapters_hint') ?></x-Hint></legend>
<div class="form-input-tabs">
<input type="radio" name="chapters-choice" id="chapters-file-upload-choice" aria-controls="chapters-file-upload-choice" value="upload-file" <?= old('chapters-choice') !== 'remote-file' ? 'checked' : '' ?> />
<label for="chapters-file-upload-choice"><?= lang('Common.forms.upload_file') ?></label>
@ -193,35 +188,35 @@
<div class="py-2 tab-panels">
<section id="chapters-file-upload" class="flex items-center tab-panel">
<Forms.Label class="sr-only" for="chapters_file" isOptional="true"><?= lang('Episode.form.chapters_file') ?></Forms.Label>
<Forms.Input class="w-full" name="chapters_file" type="file" accept=".json" />
<x-Forms.Label class="sr-only" for="chapters_file" isOptional="true"><?= lang('Episode.form.chapters_file') ?></x-Forms.Label>
<x-Forms.Input class="w-full" name="chapters_file" type="file" accept=".json" />
</section>
<section id="chapters-file-remote-url" class="tab-panel">
<Forms.Label class="sr-only" for="chapters_remote_url" isOptional="true"><?= lang('Episode.form.chapters_remote_url') ?></Forms.Label>
<Forms.Input class="w-full" placeholder="https://…" name="chapters_remote_url" />
<x-Forms.Label class="sr-only" for="chapters_remote_url" isOptional="true"><?= lang('Episode.form.chapters_remote_url') ?></x-Forms.Label>
<x-Forms.Input class="w-full" placeholder="https://…" name="chapters_remote_url" />
</section>
</div>
</div>
</fieldset>
</Forms.Section>
</x-Forms.Section>
<Forms.Section
<x-Forms.Section
title="<?= lang('Episode.form.advanced_section_title') ?>"
subtitle="<?= lang('Episode.form.advanced_section_subtitle') ?>"
>
<Forms.Field
<x-Forms.Field
as="XMLEditor"
name="custom_rss"
label="<?= esc(lang('Episode.form.custom_rss')) ?>"
hint="<?= esc(lang('Episode.form.custom_rss_hint')) ?>"
/>
<Forms.Toggler name="block" value="yes" checked="false" hint="<?= esc(lang('Episode.form.block_hint')) ?>"><?= lang('Episode.form.block') ?></Forms.Toggler>
<x-Forms.Toggler name="block" isChecked="false" hint="<?= esc(lang('Episode.form.block_hint')) ?>"><?= lang('Episode.form.block') ?></x-Forms.Toggler>
</Forms.Section>
</x-Forms.Section>
<Button class="self-end" variant="primary" type="submit"><?= lang('Episode.form.submit_create') ?></Button>
<x-Button class="self-end" variant="primary" type="submit"><?= lang('Episode.form.submit_create') ?></x-Button>
</form>

View File

@ -13,13 +13,13 @@
<form action="<?= route_to('episode-delete', $podcast->id, $episode->id) ?>" method="POST" class="flex flex-col w-full max-w-xl mx-auto">
<?= csrf_field() ?>
<Alert variant="danger" class="font-semibold"><?= lang('Episode.delete_form.disclaimer') ?></Alert>
<x-Alert variant="danger" class="font-semibold"><?= lang('Episode.delete_form.disclaimer') ?></x-Alert>
<Forms.Checkbox class="mt-2" name="understand" required="true" isChecked="false"><?= lang('Episode.delete_form.understand') ?></Forms.Checkbox>
<x-Forms.Checkbox class="mt-2" name="understand" isRequired="true" isChecked="false"><?= lang('Episode.delete_form.understand') ?></x-Forms.Checkbox>
<div class="self-end mt-4">
<Button uri="<?= route_to('episode-view', $podcast->id, $episode->id) ?>"><?= lang('Common.cancel') ?></Button>
<Button type="submit" variant="danger"><?= lang('Episode.delete_form.submit') ?></Button>
<x-Button uri="<?= route_to('episode-view', $podcast->id, $episode->id) ?>"><?= lang('Common.cancel') ?></x-Button>
<x-Button type="submit" variant="danger"><?= lang('Episode.delete_form.submit') ?></x-Button>
</div>
</form>

View File

@ -9,7 +9,7 @@
<?= $this->endSection() ?>
<?= $this->section('headerRight') ?>
<Button variant="primary" type="submit" form="episode-edit-form"><?= lang('Episode.form.submit_edit') ?></Button>
<x-Button variant="primary" type="submit" form="episode-edit-form"><?= lang('Episode.form.submit_edit') ?></x-Button>
<?= $this->endSection() ?>
@ -19,9 +19,9 @@
<?= csrf_field() ?>
<Forms.Section title="<?= lang('Episode.form.info_section_title') ?>" >
<x-Forms.Section title="<?= lang('Episode.form.info_section_title') ?>" >
<Forms.Field
<x-Forms.Field
name="audio_file"
label="<?= esc(lang('Episode.form.audio_file')) ?>"
hint="<?= esc(lang('Episode.form.audio_file_hint')) ?>"
@ -31,7 +31,7 @@
data-max-size="<?= file_upload_max_size() ?>"
data-max-size-error="<?= lang('Episode.form.file_size_error', [formatBytes(file_upload_max_size(), true)]) ?>" />
<Forms.Field
<x-Forms.Field
name="cover"
label="<?= esc(lang('Episode.form.cover')) ?>"
hint="<?= esc(lang('Episode.form.cover_hint')) ?>"
@ -39,31 +39,31 @@
type="file"
accept=".jpg,.jpeg,.png" />
<Forms.Field
<x-Forms.Field
name="title"
label="<?= esc(lang('Episode.form.title')) ?>"
hint="<?= esc(lang('Episode.form.title_hint')) ?>"
value="<?= esc($episode->title) ?>"
required="true"
isRequired="true"
data-slugify="title" />
<div>
<Forms.Label for="slug"><?= lang('Episode.form.permalink') ?></Forms.Label>
<x-Forms.Label for="slug"><?= lang('Episode.form.permalink') ?></x-Forms.Label>
<permalink-edit class="inline-flex items-center w-full text-xs" edit-label="<?= lang('Common.edit') ?>" copy-label="<?= lang('Common.copy') ?>" copied-label="<?= lang('Common.copied') ?>" permalink-base="<?= url_to('podcast-episodes', esc($podcast->handle)) ?>">
<span slot="domain"><?= '…/' . esc($podcast->handle) . '/' ?></span>
<Forms.Input name="slug" value="<?= esc($episode->slug) ?>" required="true" data-slugify="slug" slot="slug-input" class="flex-1 text-xs" />
<x-Forms.Input name="slug" value="<?= esc($episode->slug) ?>" isRequired="true" data-slugify="slug" slot="slug-input" class="flex-1 text-xs" />
</permalink-edit>
</div>
<div class="flex flex-col gap-x-2 gap-y-4 md:flex-row">
<Forms.Field
<x-Forms.Field
class="flex-1 w-full"
name="season_number"
label="<?= esc(lang('Episode.form.season_number')) ?>"
type="number"
value="<?= $episode->season_number ?>"
/>
<Forms.Field
<x-Forms.Field
class="flex-1 w-full"
name="episode_number"
label="<?= esc(lang('Episode.form.episode_number')) ?>"
@ -75,57 +75,54 @@
<fieldset class="flex gap-1">
<legend><?= lang('Episode.form.type.label') ?></legend>
<Forms.RadioButton
<x-Forms.RadioButton
value="full"
name="type"
hint="<?= esc(lang('Episode.form.type.full_hint')) ?>"
isChecked="<?= $episode->type === 'full' ? 'true' : 'false' ?>" ><?= lang('Episode.form.type.full') ?></Forms.RadioButton>
<Forms.RadioButton
isChecked="<?= $episode->type === 'full' ? 'true' : 'false' ?>" ><?= lang('Episode.form.type.full') ?></x-Forms.RadioButton>
<x-Forms.RadioButton
value="trailer"
name="type"
hint="<?= esc(lang('Episode.form.type.trailer_hint')) ?>"
isChecked="<?= $episode->type === 'trailer' ? 'true' : 'false' ?>" ><?= lang('Episode.form.type.trailer') ?></Forms.RadioButton>
<Forms.RadioButton
isChecked="<?= $episode->type === 'trailer' ? 'true' : 'false' ?>" ><?= lang('Episode.form.type.trailer') ?></x-Forms.RadioButton>
<x-Forms.RadioButton
value="bonus"
name="type"
hint="<?= esc(lang('Episode.form.type.bonus_hint')) ?>"
isChecked="<?= $episode->type === 'bonus' ? 'true' : 'false' ?>" ><?= lang('Episode.form.type.bonus') ?></Forms.RadioButton>
isChecked="<?= $episode->type === 'bonus' ? 'true' : 'false' ?>" ><?= lang('Episode.form.type.bonus') ?></x-Forms.RadioButton>
</fieldset>
<fieldset class="flex gap-1">
<legend>
<?= lang('Episode.form.parental_advisory.label') .
hint_tooltip(lang('Episode.form.parental_advisory.hint'), 'ml-1') ?>
</legend>
<Forms.RadioButton
<legend><?= lang('Episode.form.parental_advisory.label') ?><x-Hint class="ml-1"><?= lang('Episode.form.parental_advisory.hint') ?></x-Hint></legend>
<x-Forms.RadioButton
value="undefined"
name="parental_advisory"
isChecked="<?= $episode->parental_advisory === null ? 'true' : 'false' ?>" ><?= lang('Episode.form.parental_advisory.undefined') ?></Forms.RadioButton>
<Forms.RadioButton
isChecked="<?= $episode->parental_advisory === null ? 'true' : 'false' ?>" ><?= lang('Episode.form.parental_advisory.undefined') ?></x-Forms.RadioButton>
<x-Forms.RadioButton
value="clean"
name="parental_advisory"
isChecked="<?= $episode->parental_advisory === 'clean' ? 'true' : 'false' ?>" ><?= lang('Episode.form.parental_advisory.clean') ?></Forms.RadioButton>
<Forms.RadioButton
isChecked="<?= $episode->parental_advisory === 'clean' ? 'true' : 'false' ?>" ><?= lang('Episode.form.parental_advisory.clean') ?></x-Forms.RadioButton>
<x-Forms.RadioButton
value="explicit"
name="parental_advisory"
isChecked="<?= $episode->parental_advisory === 'explicit' ? 'true' : 'false' ?>" ><?= lang('Episode.form.parental_advisory.explicit') ?></Forms.RadioButton>
isChecked="<?= $episode->parental_advisory === 'explicit' ? 'true' : 'false' ?>" ><?= lang('Episode.form.parental_advisory.explicit') ?></x-Forms.RadioButton>
</fieldset>
</Forms.Section>
</x-Forms.Section>
<Forms.Section
<x-Forms.Section
title="<?= lang('Episode.form.show_notes_section_title') ?>"
subtitle="<?= lang('Episode.form.show_notes_section_subtitle') ?>">
<Forms.Field
<x-Forms.Field
as="MarkdownEditor"
name="description"
label="<?= esc(lang('Episode.form.description')) ?>"
value="<?= esc($episode->description_markdown) ?>"
required="true"
isRequired="true"
disallowList="header,quote" />
<Forms.Field
<x-Forms.Field
as="MarkdownEditor"
name="description_footer"
label="<?= esc(lang('Episode.form.description_footer')) ?>"
@ -133,33 +130,32 @@
value="<?= esc($podcast->episode_description_footer_markdown) ?? '' ?>"
disallowList="header,quote" />
</Forms.Section>
</x-Forms.Section>
<Forms.Section title="<?= lang('Episode.form.premium_title') ?>" >
<Forms.Toggler class="mt-2" name="premium" value="yes" checked="<?= $episode->is_premium ? 'true' : 'false' ?>">
<?= lang('Episode.form.premium') ?></Forms.Toggler>
</Forms.Section>
<x-Forms.Section title="<?= lang('Episode.form.premium_title') ?>" >
<x-Forms.Toggler class="mt-2" name="premium" isChecked="<?= $episode->is_premium ? 'true' : 'false' ?>">
<?= lang('Episode.form.premium') ?></x-Forms.Toggler>
</x-Forms.Section>
<Forms.Section
<x-Forms.Section
title="<?= lang('Episode.form.location_section_title') ?>"
subtitle="<?= lang('Episode.form.location_section_subtitle') ?>"
>
<Forms.Field
<x-Forms.Field
name="location_name"
label="<?= esc(lang('Episode.form.location_name')) ?>"
hint="<?= esc(lang('Episode.form.location_name_hint')) ?>"
value="<?= esc($episode->location_name) ?>" />
</Forms.Section>
</x-Forms.Section>
<Forms.Section
<x-Forms.Section
title="<?= lang('Episode.form.additional_files_section_title') ?>">
<fieldset class="flex flex-col">
<legend><?= lang('Episode.form.transcript') .
'<small class="ml-1 lowercase">(' .
lang('Common.optional') .
')</small>' .
hint_tooltip(lang('Episode.form.transcript_hint'), 'ml-1') ?></legend>
')</small>' ?><x-Hint class="ml-1"><?= lang('Episode.form.transcript_hint') ?></x-Hint></legend>
<div class="form-input-tabs">
<input type="radio" name="transcript-choice" id="transcript-file-upload-choice" aria-controls="transcript-file-upload-choice" value="upload-file" <?= $episode->transcript_remote_url ? '' : 'checked' ?> />
<label for="transcript-file-upload-choice"><?= lang('Common.forms.upload_file') ?></label>
@ -191,7 +187,7 @@
'class' => 'mx-auto',
]),
[
'class' => 'p-1 text-sm bg-red-100 rounded-full text-red-700 hover:text-red-900 focus:ring-accent',
'class' => 'p-1 text-sm bg-red-100 rounded-full text-red-700 hover:text-red-900',
'data-tooltip' => 'bottom',
'title' => lang(
'Episode.form.transcript_file_delete',
@ -200,12 +196,12 @@
) ?>
</div>
<?php endif; ?>
<Forms.Label class="sr-only" for="transcript_file" isOptional="true"><?= lang('Episode.form.transcript_file') ?></Forms.Label>
<Forms.Input class="w-full" name="transcript_file" type="file" accept=".srt,.vtt" />
<x-Forms.Label class="sr-only" for="transcript_file" isOptional="true"><?= lang('Episode.form.transcript_file') ?></x-Forms.Label>
<x-Forms.Input class="w-full" name="transcript_file" type="file" accept=".srt,.vtt" />
</section>
<section id="transcript-file-remote-url" class="tab-panel">
<Forms.Label class="sr-only" for="transcript_remote_url" isOptional="true"><?= lang('Episode.form.transcript_remote_url') ?></Forms.Label>
<Forms.Input class="w-full" placeholder="https://…" name="transcript_remote_url" value="<?= esc($episode->transcript_remote_url) ?>" />
<x-Forms.Label class="sr-only" for="transcript_remote_url" isOptional="true"><?= lang('Episode.form.transcript_remote_url') ?></x-Forms.Label>
<x-Forms.Input class="w-full" placeholder="https://…" name="transcript_remote_url" value="<?= esc($episode->transcript_remote_url) ?>" />
</section>
</div>
</div>
@ -216,8 +212,7 @@
<legend><?= lang('Episode.form.chapters') .
'<small class="ml-1 lowercase">(' .
lang('Common.optional') .
')</small>' .
hint_tooltip(lang('Episode.form.chapters_hint'), 'ml-1') ?></legend>
')</small>' ?><x-Hint class="ml-1"><?= lang('Episode.form.chapters_hint') ?></x-Hint></legend>
<div class="form-input-tabs">
<input type="radio" name="chapters-choice" id="chapters-file-upload-choice" aria-controls="chapters-file-upload-choice" value="upload-file" <?= $episode->chapters_remote_url ? '' : 'checked' ?> />
<label for="chapters-file-upload-choice"><?= lang('Common.forms.upload_file') ?></label>
@ -249,7 +244,7 @@
'class' => 'mx-auto',
]),
[
'class' => 'text-sm p-1 bg-red-100 rounded-full text-red-700 hover:text-red-900 focus:ring-accent',
'class' => 'text-sm p-1 bg-red-100 rounded-full text-red-700 hover:text-red-900',
'data-tooltip' => 'bottom',
'title' => lang(
'Episode.form.chapters_file_delete',
@ -258,23 +253,23 @@
) ?>
</div>
<?php endif; ?>
<Forms.Label class="sr-only" for="chapters_file" isOptional="true"><?= lang('Episode.form.chapters_file') ?></Forms.Label>
<Forms.Input class="w-full" name="chapters_file" type="file" accept=".json" />
<x-Forms.Label class="sr-only" for="chapters_file" isOptional="true"><?= lang('Episode.form.chapters_file') ?></x-Forms.Label>
<x-Forms.Input class="w-full" name="chapters_file" type="file" accept=".json" />
</section>
<section id="chapters-file-remote-url" class="tab-panel">
<Forms.Label class="sr-only" for="chapters_remote_url" isOptional="true"><?= lang('Episode.form.chapters_remote_url') ?></Forms.Label>
<Forms.Input class="w-full" placeholder="https://…" name="chapters_remote_url" value="<?= esc($episode->chapters_remote_url) ?>" />
<x-Forms.Label class="sr-only" for="chapters_remote_url" isOptional="true"><?= lang('Episode.form.chapters_remote_url') ?></x-Forms.Label>
<x-Forms.Input class="w-full" placeholder="https://…" name="chapters_remote_url" value="<?= esc($episode->chapters_remote_url) ?>" />
</section>
</div>
</div>
</fieldset>
</Forms.Section>
</x-Forms.Section>
<Forms.Section
<x-Forms.Section
title="<?= lang('Episode.form.advanced_section_title') ?>"
subtitle="<?= lang('Episode.form.advanced_section_subtitle') ?>"
>
<Forms.Field
<x-Forms.Field
as="XMLEditor"
name="custom_rss"
label="<?= esc(lang('Episode.form.custom_rss')) ?>"
@ -282,18 +277,18 @@
content="<?= esc($episode->custom_rss_string) ?>"
/>
<Forms.Toggler id="block" name="block" value="yes" checked="<?= $episode->is_blocked ? 'true' : 'false' ?>" hint="<?= esc(lang('Episode.form.block_hint')) ?>"><?= lang('Episode.form.block') ?></Forms.Toggler>
<x-Forms.Toggler id="block" name="block" isChecked="<?= $episode->is_blocked ? 'true' : 'false' ?>" hint="<?= esc(lang('Episode.form.block_hint')) ?>"><?= lang('Episode.form.block') ?></x-Forms.Toggler>
</Forms.Section>
</x-Forms.Section>
</form>
<?php if ($episode->published_at === null): ?>
<?php // @icon('delete-bin-fill')?>
<Button class="mt-8" variant="danger" uri="<?= route_to('episode-delete', $podcast->id, $episode->id) ?>" iconLeft="delete-bin-fill"><?= lang('Episode.delete') ?></Button>
<x-Button class="mt-8" variant="danger" uri="<?= route_to('episode-delete', $podcast->id, $episode->id) ?>" iconLeft="delete-bin-fill"><?= lang('Episode.delete') ?></x-Button>
<?php else: ?>
<?php // @icon('forbid-fill')?>
<Button class="mt-8" variant="disabled" iconLeft="forbid-fill" data-tooltip="right" title="<?= lang('Episode.messages.unpublishBeforeDeleteTip') ?>"><?= lang('Episode.delete') ?></Button>
<x-Button class="mt-8" variant="disabled" iconLeft="forbid-fill" data-tooltip="right" title="<?= lang('Episode.messages.unpublishBeforeDeleteTip') ?>"><?= lang('Episode.delete') ?></x-Button>
<?php endif ?>

View File

@ -33,15 +33,15 @@ $embedHeight = config('Embed')->height;
<iframe name="embed" id="embed" class="w-full max-w-xl mt-6 h-28" frameborder="0" scrolling="no" style="width: 100%; overflow: hidden;" src="<?= $episode->embed_url ?>"></iframe>
<div class="flex items-center mt-8 gap-x-2">
<Forms.Textarea readonly="true" class="w-full max-w-xl" name="iframe" rows="2" value="<?= esc("<iframe width=\"100%\" height=\"{$embedHeight}\" frameborder=\"0\" scrolling=\"no\" style=\"width: 100%; height: {$embedHeight}px; overflow: hidden;\" src=\"{$episode->embed_url}\"></iframe>") ?>" />
<x-Forms.Textarea isReadonly="true" class="w-full max-w-xl" name="iframe" rows="2" value="<?= esc("<iframe width=\"100%\" height=\"{$embedHeight}\" frameborder=\"0\" scrolling=\"no\" style=\"width: 100%; height: {$embedHeight}px; overflow: hidden;\" src=\"{$episode->embed_url}\"></iframe>") ?>" />
<?php // @icon('file-copy-fill')?>
<IconButton glyph="file-copy-fill" data-type="clipboard-copy" data-clipboard-target="iframe"><?= lang('Episode.embed.clipboard_iframe') ?></IconButton>
<x-IconButton glyph="file-copy-fill" data-type="clipboard-copy" data-clipboard-target="iframe"><?= lang('Episode.embed.clipboard_iframe') ?></x-IconButton>
</div>
<div class="flex items-center mt-4 gap-x-2">
<Forms.Input readonly="true" class="w-full max-w-xl" name="url" value="<?= esc($episode->embed_url) ?>" />
<x-Forms.Input isReadonly="true" class="w-full max-w-xl" name="url" value="<?= esc($episode->embed_url) ?>" />
<?php // @icon('file-copy-fill')?>
<IconButton glyph="file-copy-fill" data-type="clipboard-copy" data-clipboard-target="url"><?= lang('Episode.embed.clipboard_url') ?></IconButton>
<x-IconButton glyph="file-copy-fill" data-type="clipboard-copy" data-clipboard-target="url"><?= lang('Episode.embed.clipboard_url') ?></x-IconButton>
</div>
<?= $this->endSection() ?>

View File

@ -10,7 +10,7 @@
<?= $this->section('headerRight') ?>
<?php // @icon('add-fill')?>
<Button uri="<?= route_to('episode-create', $podcast->id) ?>" variant="primary" iconLeft="add-fill"><?= lang('Episode.create') ?></Button>
<x-Button uri="<?= route_to('episode-create', $podcast->id) ?>" variant="primary" iconLeft="add-fill"><?= lang('Episode.create') ?></x-Button>
<?= $this->endSection() ?>
@ -28,16 +28,16 @@
</p>
<form class="relative flex">
<div class="relative">
<Forms.Input name="q" placeholder="<?= lang('Episode.list.search.placeholder') ?>" value="<?= esc($query) ?>" class="<?= $query ? 'pr-8' : '' ?>" />
<x-Forms.Input name="q" placeholder="<?= lang('Episode.list.search.placeholder') ?>" value="<?= esc($query) ?>" class="<?= $query ? 'pr-8' : '' ?>" />
<?php if ($query): ?>
<a href="<?= route_to('episode-list', $podcast->id) ?>" class="absolute inset-y-0 right-0 inline-flex items-center justify-center px-2 opacity-75 focus:ring-accent hover:opacity-100 focus:opacity-100" title="<?= lang('Episode.list.search.clear') ?>" data-tooltip="bottom"><?= icon('close-fill', [
<a href="<?= route_to('episode-list', $podcast->id) ?>" class="absolute inset-y-0 right-0 inline-flex items-center justify-center px-2 opacity-75 hover:opacity-100 focus:opacity-100" title="<?= lang('Episode.list.search.clear') ?>" data-tooltip="bottom"><?= icon('close-fill', [
'class' => 'text-lg',
]) ?></a>
<?php endif; ?>
</div>
<Button type="submit" variant="secondary" class="px-3 ml-2 rounded-lg shadow-md" title="<?= lang('Episode.list.search.submit') ?>" data-tooltip="bottom" isSquared="true"><?= icon('search-fill', [
<x-Button type="submit" variant="secondary" class="px-3 ml-2 rounded-lg shadow-md" title="<?= lang('Episode.list.search.submit') ?>" data-tooltip="bottom" isSquared="true"><?= icon('search-fill', [
'class' => 'text-xl',
]) ?></Button>
]) ?></x-Button>
</form>
</div>
@ -161,10 +161,10 @@ data_table(
HTML),
];
}
return '<button id="more-dropdown-' . $episode->id . '" type="button" class="inline-flex items-center p-1 rounded-full focus:ring-accent" 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" data-dropdown="button" data-dropdown-target="more-dropdown-' . $episode->id . '-menu" aria-haspopup="true" aria-expanded="false">' .
icon('more-2-fill') .
'</button>' .
'<DropdownMenu id="more-dropdown-' . $episode->id . '-menu" labelledby="more-dropdown-' . $episode->id . '" offsetY="-24" items="' . esc(json_encode($items)) . '" />';
'<x-DropdownMenu id="more-dropdown-' . $episode->id . '-menu" labelledby="more-dropdown-' . $episode->id . '" offsetY="-24" items="' . esc(json_encode($items)) . '" />';
},
],
],

View File

@ -10,7 +10,7 @@
<?= $this->section('headerRight') ?>
<?php // @icon('add-fill')?>
<Button variant="primary" uri="<?= route_to('person-create') ?>" iconLeft="add-fill"><?= lang('Person.create') ?></Button>
<x-Button variant="primary" uri="<?= route_to('person-create') ?>" iconLeft="add-fill"><?= lang('Person.create') ?></x-Button>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
@ -18,12 +18,12 @@
<form action="<?= route_to('episode-persons-manage', $podcast->id, $episode->id) ?>" method="POST" class="max-w-xl">
<?= csrf_field() ?>
<Forms.Section
<x-Forms.Section
title="<?= lang('Person.episode_form.add_section_title') ?>"
subtitle="<?= lang('Person.episode_form.add_section_subtitle') ?>"
>
<Forms.Field
<x-Forms.Field
as="MultiSelect"
id="persons"
name="persons[]"
@ -31,10 +31,10 @@
hint="<?= esc(lang('Person.episode_form.persons_hint')) ?>"
options="<?= esc(json_encode($personOptions)) ?>"
selected="<?= esc(json_encode(old('persons', []))) ?>"
required="true"
isRequired="true"
/>
<Forms.Field
<x-Forms.Field
as="MultiSelect"
id="roles"
name="roles[]"
@ -44,9 +44,9 @@
selected="<?= esc(json_encode(old('roles', []))) ?>"
/>
<Button variant="primary" type="submit" class="self-end"><?= lang('Person.episode_form.submit_add') ?></Button>
<x-Button variant="primary" type="submit" class="self-end"><?= lang('Person.episode_form.submit_add') ?></x-Button>
</Forms.Section>
</x-Forms.Section>
</form>
@ -87,7 +87,7 @@
'header' => lang('Common.actions'),
'cell' => function ($person): string {
// @icon('delete-bin-fill')
return '<Button uri="' . route_to('episode-person-remove', $person->podcast_id, $person->episode_id, $person->id) . '" variant="danger" size="small" iconLeft="delete-bin-fill">' . lang('Person.episode_form.remove') . '</Button>';
return '<x-Button uri="' . route_to('episode-person-remove', $person->podcast_id, $person->episode_id, $person->id) . '" variant="danger" size="small" iconLeft="delete-bin-fill">' . lang('Person.episode_form.remove') . '</x-Button>';
},
],
],

View File

@ -16,7 +16,7 @@
'class' => 'mr-2 text-lg',
]) . lang('Episode.publish_form.back_to_episode_dashboard'),
[
'class' => 'inline-flex items-center font-semibold mr-4 text-sm focus:ring-accent',
'class' => 'inline-flex items-center font-semibold mr-4 text-sm',
],
) ?>
@ -39,7 +39,7 @@
</div>
</div>
<div class="px-4 mb-2">
<Forms.Textarea name="message" placeholder="<?= lang('Episode.publish_form.message_placeholder') ?>" autofocus="" rows="2" />
<x-Forms.Textarea name="message" placeholder="<?= lang('Episode.publish_form.message_placeholder') ?>" autofocus="" rows="2" />
</div>
<div class="flex border-y">
<img src="<?= $episode->cover
@ -82,14 +82,14 @@
<legend class="text-lg font-semibold"><?= lang(
'Episode.publish_form.publication_date',
) ?></legend>
<Forms.Radio value="now" name="publication_method" isChecked="<?= old('publication_method') ? old('publish') === 'now' : true ?>"><?= lang('Episode.publish_form.publication_method.now') ?></Forms.Radio>
<x-Forms.Radio value="now" name="publication_method" isChecked="<?= old('publication_method') ? old('publish') === 'now' : true ?>"><?= lang('Episode.publish_form.publication_method.now') ?></x-Forms.Radio>
<div class="inline-flex flex-wrap items-center radio-toggler">
<input
class="w-6 h-6 border-contrast text-accent-base border-3 focus:ring-accent"
class="w-6 h-6 border-contrast text-accent-base border-3"
type="radio" id="schedule" name="publication_method" value="schedule" <?= old('publication_method') && old('publication_method') === 'schedule' ? 'checked' : '' ?> />
<Label for="schedule" class="pl-2 leading-8"><?= lang('Episode.publish_form.publication_method.schedule') ?></label>
<x-Label for="schedule" class="pl-2 leading-8"><?= lang('Episode.publish_form.publication_method.schedule') ?></label>
<div class="w-full mt-2 radio-toggler-element">
<Forms.Field
<x-Forms.Field
as="DatetimePicker"
name="scheduled_publication_date"
label="<?= esc(lang('Episode.publish_form.scheduled_publication_date')) ?>"
@ -101,11 +101,11 @@
</fieldset>
<?php endif ?>
<Alert id="publish-warning" variant="warning" class="hidden mt-2" title="<?= lang('Episode.publish_form.message_warning') ?>"><?= lang('Episode.publish_form.message_warning_hint') ?></Alert>
<x-Alert id="publish-warning" variant="warning" class="hidden mt-2" title="<?= lang('Episode.publish_form.message_warning') ?>"><?= lang('Episode.publish_form.message_warning_hint') ?></x-Alert>
<div class="flex items-center justify-between w-full mt-4">
<Button uri="<?= route_to('episode-publish-cancel', $podcast->id, $episode->id) ?>" variant="danger"><?= lang('Episode.publish_form.cancel_publication') ?></Button>
<Button variant="primary" type="submit" data-btn-text-warning="<?= lang('Episode.publish_form.message_warning_submit') ?>" data-btn-text="<?= lang('Episode.publish_form.submit') ?>"><?= lang('Episode.publish_form.submit') ?></Button>
<x-Button uri="<?= route_to('episode-publish-cancel', $podcast->id, $episode->id) ?>" variant="danger"><?= lang('Episode.publish_form.cancel_publication') ?></x-Button>
<x-Button variant="primary" type="submit" data-btn-text-warning="<?= lang('Episode.publish_form.message_warning_submit') ?>" data-btn-text="<?= lang('Episode.publish_form.submit') ?>"><?= lang('Episode.publish_form.submit') ?></x-Button>
</div>
</form>

View File

@ -24,16 +24,16 @@
<?= csrf_field() ?>
<input type="hidden" name="client_timezone" value="UTC" />
<Forms.Field
<x-Forms.Field
as="DatetimePicker"
name="new_publication_date"
label="<?= esc(lang('Episode.publish_date_edit_form.new_publication_date')) ?>"
hint="<?= esc(lang('Episode.publish_date_edit_form.new_publication_date_hint')) ?>"
value="<?= $episode->published_at ?>"
required="true"
isRequired="true"
/>
<Button variant="primary" type="submit" class="mt-4"><?= lang('Episode.publish_date_edit_form.submit') ?></Button>
<x-Button variant="primary" type="submit" class="mt-4"><?= lang('Episode.publish_date_edit_form.submit') ?></x-Button>
</form>

View File

@ -41,7 +41,7 @@
</div>
</div>
<div class="px-4 mb-2">
<Forms.Textarea name="message" placeholder="<?= lang('Episode.publish_form.message_placeholder') ?>" autofocus="" value="<?= esc($post->message) ?>" rows="2" />
<x-Forms.Textarea name="message" placeholder="<?= lang('Episode.publish_form.message_placeholder') ?>" autofocus="" value="<?= esc($post->message) ?>" rows="2" />
</div>
<div class="flex border-y">
<img src="<?= $episode->cover
@ -86,14 +86,14 @@
<legend class="text-lg font-semibold"><?= lang(
'Episode.publish_form.publication_date',
) ?></legend>
<Forms.Radio value="now" name="publication_method" isChecked="<?= old('publication_method') && old('publish') === 'now' ?>"><?= lang('Episode.publish_form.publication_method.now') ?></Forms.Radio>
<x-Forms.Radio value="now" name="publication_method" isChecked="<?= old('publication_method') && old('publish') === 'now' ?>"><?= lang('Episode.publish_form.publication_method.now') ?></x-Forms.Radio>
<div class="inline-flex flex-wrap items-center radio-toggler">
<input
class="w-6 h-6 border-contrast text-accent-base border-3 focus:ring-accent"
class="w-6 h-6 border-contrast text-accent-base border-3"
type="radio" id="schedule" name="publication_method" value="schedule" <?= old('publication_method') ? old('publication_method') === 'schedule' : 'checked' ?> />
<Label for="schedule" class="pl-2 leading-8"><?= lang('Episode.publish_form.publication_method.schedule') ?></label>
<x-Label for="schedule" class="pl-2 leading-8"><?= lang('Episode.publish_form.publication_method.schedule') ?></label>
<div class="w-full mt-2 radio-toggler-element">
<Forms.Field
<x-Forms.Field
as="DatetimePicker"
name="scheduled_publication_date"
label="<?= esc(lang('Episode.publish_form.scheduled_publication_date')) ?>"
@ -105,11 +105,11 @@
</fieldset>
<?php endif ?>
<Alert id="publish-warning" variant="warning" class="hidden mt-2" title="<?= lang('Episode.publish_form.message_warning') ?>"><?= lang('Episode.publish_form.message_warning_hint') ?></Alert>
<x-Alert id="publish-warning" variant="warning" class="hidden mt-2" title="<?= lang('Episode.publish_form.message_warning') ?>"><?= lang('Episode.publish_form.message_warning_hint') ?></x-Alert>
<div class="flex items-center justify-between w-full mt-4">
<Button uri="<?= route_to('episode-publish-cancel', $podcast->id, $episode->id) ?>" variant="danger"><?= lang('Episode.publish_form.cancel_publication') ?></Button>
<Button variant="primary" type="submit" data-btn-text-warning="<?= lang('Episode.publish_form.message_warning_submit') ?>" data-btn-text="<?= lang('Episode.publish_form.submit_edit') ?>"><?= lang('Episode.publish_form.submit_edit') ?></Button>
<x-Button uri="<?= route_to('episode-publish-cancel', $podcast->id, $episode->id) ?>" variant="danger"><?= lang('Episode.publish_form.cancel_publication') ?></x-Button>
<x-Button variant="primary" type="submit" data-btn-text-warning="<?= lang('Episode.publish_form.message_warning_submit') ?>" data-btn-text="<?= lang('Episode.publish_form.submit_edit') ?>"><?= lang('Episode.publish_form.submit_edit') ?></x-Button>
</div>
</form>

View File

@ -10,7 +10,7 @@
<?= $this->section('headerRight') ?>
<?php // @icon('add-fill')?>
<Button uri="<?= route_to('soundbites-create', $podcast->id, $episode->id) ?>" variant="primary" iconLeft="add-fill"><?= lang('Soundbite.create') ?></Button>
<x-Button uri="<?= route_to('soundbites-create', $podcast->id, $episode->id) ?>" variant="primary" iconLeft="add-fill"><?= lang('Soundbite.create') ?></x-Button>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
@ -26,10 +26,10 @@
[
'header' => lang('Common.actions'),
'cell' => function ($soundbite): string {
return '<button id="more-dropdown-' . $soundbite->id . '" type="button" class="inline-flex items-center p-1 rounded-full focus:ring-accent" data-dropdown="button" data-dropdown-target="more-dropdown-' . $soundbite->id . '-menu" aria-haspopup="true" aria-expanded="false">' .
return '<button id="more-dropdown-' . $soundbite->id . '" type="button" class="inline-flex items-center p-1 rounded-full" data-dropdown="button" data-dropdown-target="more-dropdown-' . $soundbite->id . '-menu" aria-haspopup="true" aria-expanded="false">' .
icon('more-2-fill') .
'</button>' .
'<DropdownMenu id="more-dropdown-' . $soundbite->id . '-menu" labelledby="more-dropdown-' . $soundbite->id . '" offsetY="-24" items="' . esc(json_encode([
'<x-DropdownMenu id="more-dropdown-' . $soundbite->id . '-menu" labelledby="more-dropdown-' . $soundbite->id . '" offsetY="-24" items="' . esc(json_encode([
[
'type' => 'link',
'title' => lang('Soundbite.delete'),

View File

@ -14,10 +14,10 @@
<form id="soundbites-form" action="<?= route_to('episode-soundbites-edit', $podcast->id, $episode->id) ?>" method="POST" class="flex flex-col">
<?= csrf_field() ?>
<Forms.Field
<x-Forms.Field
name="title"
label="<?= esc(lang('Soundbite.form.soundbite_title')) ?>"
required="true"
isRequired="true"
class="max-w-sm"
/>
<audio-clipper start-time="<?= old('start_time', 0) ?>" audio-duration="<?= $episode->audio->duration ?>" duration="<?= old('duration', $episode->audio->duration >= 60 ? 60 : $episode->audio->duration) ?>" min-duration="10" volume=".5" height="50" trim-start-label="<?= lang('VideoClip.form.trim_start') ?>" trim-end-label="<?= lang('VideoClip.form.trim_end') ?>" class="mt-8">
@ -29,7 +29,7 @@
</audio-clipper>
<?php // @icon('arrow-right-fill')?>
<Button variant="primary" type="submit" class="self-end mt-4" iconRight="arrow-right-fill"><?= lang('Soundbite.form.submit') ?></Button>
<x-Button variant="primary" type="submit" class="self-end mt-4" iconRight="arrow-right-fill"><?= lang('Soundbite.form.submit') ?></x-Button>
</form>

View File

@ -13,13 +13,13 @@
<form action="<?= route_to('episode-unpublish', $podcast->id, $episode->id) ?>" method="POST" class="flex flex-col max-w-lg mx-auto">
<?= csrf_field() ?>
<Alert variant="danger" class="font-semibold"><?= lang('Episode.unpublish_form.disclaimer') ?></Alert>
<x-Alert variant="danger" class="font-semibold"><?= lang('Episode.unpublish_form.disclaimer') ?></x-Alert>
<Forms.Checkbox class="mt-2" name="understand" required="true" isChecked="false"><?= lang('Episode.unpublish_form.understand') ?></Forms.Checkbox>
<x-Forms.Checkbox class="mt-2" name="understand" isRequired="true" isChecked="false"><?= lang('Episode.unpublish_form.understand') ?></x-Forms.Checkbox>
<div class="self-end mt-4">
<Button uri="<?= route_to('episode-view', $podcast->id, $episode->id) ?>"><?= lang('Common.cancel') ?></Button>
<Button type="submit" variant="danger"><?= lang('Episode.unpublish_form.submit') ?></Button>
<x-Button uri="<?= route_to('episode-view', $podcast->id, $episode->id) ?>"><?= lang('Common.cancel') ?></x-Button>
<x-Button type="submit" variant="danger"><?= lang('Episode.unpublish_form.submit') ?></x-Button>
</div>
</form>

View File

@ -16,7 +16,7 @@ use CodeIgniter\I18n\Time;
<?= $this->section('headerRight') ?>
<?php // @icon('add-fill')?>
<Button uri="<?= route_to('video-clips-create', $podcast->id, $episode->id) ?>" variant="primary" iconLeft="add-fill"><?= lang('VideoClip.create') ?></Button>
<x-Button uri="<?= route_to('video-clips-create', $podcast->id, $episode->id) ?>" variant="primary" iconLeft="add-fill"><?= lang('VideoClip.create') ?></x-Button>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
@ -52,7 +52,7 @@ use CodeIgniter\I18n\Time;
'passed' => '',
];
return '<Pill variant="' . $pillVariantMap[$videoClip->status] . '" icon="' . $pillIconMap[$videoClip->status] . '" iconClass="' . $pillIconClassMap[$videoClip->status] . '" hint="' . lang('VideoClip.list.status.' . $videoClip->status . '_hint') . '">' . lang('VideoClip.list.status.' . $videoClip->status) . '</Pill>';
return '<x-Pill variant="' . $pillVariantMap[$videoClip->status] . '" icon="' . $pillIconMap[$videoClip->status] . '" iconClass="' . $pillIconClassMap[$videoClip->status] . '" hint="' . lang('VideoClip.list.status.' . $videoClip->status . '_hint') . '">' . lang('VideoClip.list.status.' . $videoClip->status) . '</x-Pill>';
},
],
[
@ -63,7 +63,7 @@ use CodeIgniter\I18n\Time;
'portrait' => 'aspect-[9/16]',
'squared' => 'aspect-square',
];
return '<a href="' . route_to('video-clip', $videoClip->podcast_id, $videoClip->episode_id, $videoClip->id) . '" class="inline-flex items-center w-full group gap-x-2 focus:ring-accent"><div class="relative"><span class="absolute block w-3 h-3 rounded-full ring-2 ring-white -bottom-1 -left-1" data-tooltip="bottom" title="' . lang('Settings.theme.' . $videoClip->theme['name']) . '" style="background-color:hsl(' . $videoClip->theme['preview'] . ')"></span><div class="flex items-center justify-center h-6 overflow-hidden bg-black rounded-sm aspect-video" data-tooltip="bottom" title="' . lang('VideoClip.format.' . $videoClip->format) . '"><span class="flex items-center justify-center h-full text-white bg-gray-400 ' . $formatClass[$videoClip->format] . '">' . icon('play-fill') . '</span></div></div><div class="flex flex-col"><div class="text-sm">#' . $videoClip->id . ' <span class="font-semibold group-hover:underline">' . esc($videoClip->title) . '</span><span class="ml-1 text-sm">by ' . esc($videoClip->user->username) . '</span></div><span class="text-xs">' . format_duration((int) $videoClip->duration) . '</span></div></a>';
return '<a href="' . route_to('video-clip', $videoClip->podcast_id, $videoClip->episode_id, $videoClip->id) . '" class="inline-flex items-center w-full group gap-x-2"><div class="relative"><span class="absolute block w-3 h-3 rounded-full ring-2 ring-white -bottom-1 -left-1" data-tooltip="bottom" title="' . lang('Settings.theme.' . $videoClip->theme['name']) . '" style="background-color:hsl(' . $videoClip->theme['preview'] . ')"></span><div class="flex items-center justify-center h-6 overflow-hidden bg-black rounded-sm aspect-video" data-tooltip="bottom" title="' . lang('VideoClip.format.' . $videoClip->format) . '"><span class="flex items-center justify-center h-full text-white bg-gray-400 ' . $formatClass[$videoClip->format] . '">' . icon('play-fill') . '</span></div></div><div class="flex flex-col"><div class="text-sm">#' . $videoClip->id . ' <span class="font-semibold group-hover:underline">' . esc($videoClip->title) . '</span><span class="ml-1 text-sm">by ' . esc($videoClip->user->username) . '</span></div><span class="text-xs">' . format_duration((int) $videoClip->duration) . '</span></div></a>';
},
],
[
@ -98,14 +98,14 @@ use CodeIgniter\I18n\Time;
helper('misc');
$filename = 'clip-' . slugify($videoClip->title) . "-{$videoClip->start_time}-{$videoClip->end_time}";
// @icon('import-fill')
$downloadButton = '<IconButton glyph="import-fill" uri="' . $videoClip->media->file_url . '" download="' . $filename . '">' . lang('VideoClip.download_clip') . '</IconButton>';
$downloadButton = '<x-IconButton glyph="import-fill" uri="' . $videoClip->media->file_url . '" download="' . $filename . '">' . lang('VideoClip.download_clip') . '</x-IconButton>';
}
return '<div class="inline-flex items-center gap-x-2">' . $downloadButton .
'<button id="more-dropdown-' . $videoClip->id . '" type="button" class="inline-flex items-center p-1 rounded-full focus:ring-accent" data-dropdown="button" data-dropdown-target="more-dropdown-' . $videoClip->id . '-menu" aria-haspopup="true" aria-expanded="false">' .
'<button id="more-dropdown-' . $videoClip->id . '" type="button" class="inline-flex items-center p-1 rounded-full" data-dropdown="button" data-dropdown-target="more-dropdown-' . $videoClip->id . '-menu" aria-haspopup="true" aria-expanded="false">' .
icon('more-2-fill') .
'</button>' .
'<DropdownMenu id="more-dropdown-' . $videoClip->id . '-menu" labelledby="more-dropdown-' . $videoClip->id . '" offsetY="-24" items="' . esc(json_encode([
'<x-DropdownMenu id="more-dropdown-' . $videoClip->id . '-menu" labelledby="more-dropdown-' . $videoClip->id . '" offsetY="-24" items="' . esc(json_encode([
[
'type' => 'link',
'title' => lang('VideoClip.go_to_page'),

View File

@ -31,48 +31,48 @@
</div>
<div class="flex flex-col items-end w-full max-w-xl xl:max-w-sm 2xl:max-w-xl gap-y-4">
<Forms.Section title="<?= lang('VideoClip.form.params_section_title') ?>" >
<Forms.Field
<x-Forms.Section title="<?= lang('VideoClip.form.params_section_title') ?>" >
<x-Forms.Field
name="title"
label="<?= esc(lang('VideoClip.form.clip_title')) ?>"
required="true"
isRequired="true"
/>
<fieldset class="flex flex-wrap gap-x-1 gap-y-2">
<legend><?= lang('VideoClip.form.format.label') ?></legend>
<Forms.RadioButton
<x-Forms.RadioButton
value="landscape"
name="format"
isChecked="true"
required="true"
hint="<?= esc(lang('VideoClip.form.format.landscape_hint')) ?>"><?= lang('VideoClip.format.landscape') ?></Forms.RadioButton>
<Forms.RadioButton
isRequired="true"
hint="<?= esc(lang('VideoClip.form.format.landscape_hint')) ?>"><?= lang('VideoClip.format.landscape') ?></x-Forms.RadioButton>
<x-Forms.RadioButton
value="portrait"
name="format"
required="true"
hint="<?= esc(lang('VideoClip.form.format.portrait_hint')) ?>"><?= lang('VideoClip.format.portrait') ?></Forms.RadioButton>
<Forms.RadioButton
isRequired="true"
hint="<?= esc(lang('VideoClip.form.format.portrait_hint')) ?>"><?= lang('VideoClip.format.portrait') ?></x-Forms.RadioButton>
<x-Forms.RadioButton
value="squared"
name="format"
required="true"
hint="<?= esc(lang('VideoClip.form.format.squared_hint')) ?>"><?= lang('VideoClip.format.squared') ?></Forms.RadioButton>
isRequired="true"
hint="<?= esc(lang('VideoClip.form.format.squared_hint')) ?>"><?= lang('VideoClip.format.squared') ?></x-Forms.RadioButton>
</fieldset>
<fieldset>
<legend><?= lang('VideoClip.form.theme') ?></legend>
<div class="grid gap-x-4 gap-y-2 grid-cols-colorButtons">
<?php foreach (config('MediaClipper')->themes as $themeName => $colors): ?>
<Forms.ColorRadioButton
<x-Forms.ColorRadioButton
class="mx-auto"
value="<?= esc($themeName) ?>"
name="theme"
required="true"
isRequired="true"
isChecked="<?= $themeName === 'pine' ? 'true' : 'false' ?>"
style="--color-accent-base: <?= $colors['preview']?>; --color-background-preview: <?= $colors['preview-background'] ?>"><?= lang('Settings.theme.' . $themeName) ?></Forms.ColorRadioButton>
style="--color-accent-base: <?= $colors['preview']?>; --color-background-preview: <?= $colors['preview-background'] ?>"><?= lang('Settings.theme.' . $themeName) ?></x-Forms.ColorRadioButton>
<?php endforeach; ?>
</div>
</fieldset>
</Forms.Section>
</x-Forms.Section>
<?php // @icon('arrow-right-fill')?>
<Button variant="primary" type="submit" iconRight="arrow-right-fill" class="self-end"><?= lang('VideoClip.form.submit') ?></Button>
<x-Button variant="primary" type="submit" iconRight="arrow-right-fill" class="self-end"><?= lang('VideoClip.form.submit') ?></x-Button>
</div>
</form>

View File

@ -12,9 +12,9 @@
<div class="flex flex-col gap-6">
<div class="flex flex-col items-start">
<Heading class="flex items-center gap-x-2"><?= icon('alert-fill', [
<x-Heading class="flex items-center gap-x-2"><?= icon('alert-fill', [
'class' => 'flex-shrink-0 text-xl text-orange-600',
]) ?><?= lang('VideoClip.requirements.title') ?></Heading>
]) ?><?= lang('VideoClip.requirements.title') ?></x-Heading>
<p class="max-w-sm font-semibold text-gray-500"><?= lang('VideoClip.requirements.missing') ?></p>
<div class="flex flex-col mt-4">
<?php foreach ($checks as $requirement => $value): ?>

View File

@ -12,19 +12,19 @@
<?= publication_pill(
$episode->published_at,
$episode->publication_status,
'text-sm ml-2 align-middle',
'text-sm align-middle',
) ?>
<?= $this->endSection() ?>
<?= $this->section('headerRight') ?>
<?php if ($episode->publication_status === 'published'): ?>
<?php // @icon('history-fill')?>
<IconButton
<x-IconButton
uri="<?= route_to('episode-publish_date_edit', $podcast->id, $episode->id) ?>"
glyph="history-fill"
variant="secondary"
glyphClass="text-xl"
><?= lang('Episode.publish_date_edit') ?></IconButton>
><?= lang('Episode.publish_date_edit') ?></x-IconButton>
<?php endif; ?>
<?= publication_button(
$podcast->id,
@ -41,7 +41,7 @@
</div>
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
<Charts.XY title="<?= lang('Charts.episode_by_day') ?>" dataUrl="<?= route_to(
<x-Charts.XY title="<?= lang('Charts.episode_by_day') ?>" dataUrl="<?= route_to(
'analytics-filtered-data',
$podcast->id,
'PodcastByEpisode',
@ -49,7 +49,7 @@
$episode->id,
) ?>"/>
<Charts.XY title="<?= lang('Charts.episode_by_month') ?>" dataUrl="<?= route_to(
<x-Charts.XY title="<?= lang('Charts.episode_by_month') ?>" dataUrl="<?= route_to(
'analytics-filtered-data',
$podcast->id,
'PodcastByEpisode',

View File

@ -14,12 +14,12 @@
<form action="<?= route_to('fediverse-attempt-block-actor') ?>" method="POST" class="flex flex-col max-w-md">
<?= csrf_field() ?>
<Forms.Field
<x-Forms.Field
name="handle"
label="<?= esc(lang('Fediverse.block_lists_form.handle')) ?>"
hint="<?= esc(lang('Fediverse.block_lists_form.handle_hint')) ?>"
required="true" />
<Button variant="primary" type="submit" class="self-end"><?= lang('Fediverse.block_lists_form.submit') ?></Button>
isRequired="true" />
<x-Button variant="primary" type="submit" class="self-end"><?= lang('Fediverse.block_lists_form.submit') ?></x-Button>
</form>
<?= data_table(
@ -40,7 +40,7 @@
$blockedActor->id .
'" />' .
csrf_field() .
'<Button uri="' . route_to('fediverse-unblock-actor', esc($blockedActor->username)) . '" variant="info" size="small" type="submit">' . lang('Fediverse.list.unblock') . '</Button>' .
'<x-Button uri="' . route_to('fediverse-unblock-actor', esc($blockedActor->username)) . '" variant="info" size="small" type="submit">' . lang('Fediverse.list.unblock') . '</x-Button>' .
'</form>';
},
],

View File

@ -14,11 +14,11 @@
<form action="<?= route_to('fediverse-attempt-block-domain') ?>" method="POST" class="flex flex-col max-w-md">
<?= csrf_field() ?>
<Forms.Field
<x-Forms.Field
name="domain"
label="<?= esc(lang('Fediverse.block_lists_form.domain')) ?>"
required="true" />
<Button variant="primary" type="submit" class="self-end"><?= lang('Fediverse.block_lists_form.submit') ?></Button>
isRequired="true" />
<x-Button variant="primary" type="submit" class="self-end"><?= lang('Fediverse.block_lists_form.submit') ?></x-Button>
</form>
<?= data_table(
@ -39,7 +39,7 @@
esc($blockedDomain->name) .
'" />' .
csrf_field() .
'<Button uri="' . route_to('fediverse-unblock-domain', esc($blockedDomain->name)) . '" variant="info" size="small" type="submit">' . lang('Fediverse.list.unblock') . '</Button>' .
'<x-Button uri="' . route_to('fediverse-unblock-domain', esc($blockedDomain->name)) . '" variant="info" size="small" type="submit">' . lang('Fediverse.list.unblock') . '</x-Button>' .
'</form>';
},
],

View File

@ -38,9 +38,9 @@ use Modules\PodcastImport\Entities\TaskStatus;
'passed' => '',
];
$errorHint = $importTask->status === TaskStatus::Failed ? hint_tooltip(esc($importTask->error), 'ml-1') : '';
$errorHint = $importTask->status === TaskStatus::Failed ? '<x-Hint class="ml-1">' . esc($importTask->error) . '</x-Hint>' : '';
return '<div class="flex items-center"><Pill variant="' . $pillVariantMap[$importTask->status->value] . '" icon="' . $pillIconMap[$importTask->status->value] . '" iconClass="' . $pillIconClassMap[$importTask->status->value] . '" hint="' . lang('PodcastImport.queue.status.' . $importTask->status->value . '_hint') . '">' . lang('PodcastImport.queue.status.' . $importTask->status->value) . '</Pill>' . $errorHint . '</div>';
return '<div class="flex items-center"><x-Pill variant="' . $pillVariantMap[$importTask->status->value] . '" icon="' . $pillIconMap[$importTask->status->value] . '" iconClass="' . $pillIconClassMap[$importTask->status->value] . '" hint="' . lang('PodcastImport.queue.status.' . $importTask->status->value . '_hint') . '">' . lang('PodcastImport.queue.status.' . $importTask->status->value) . '</x-Pill>' . $errorHint . '</div>';
},
],
[
@ -86,10 +86,10 @@ use Modules\PodcastImport\Entities\TaskStatus;
'cell' => function (PodcastImportTask $importTask) {
if ($importTask->episodes_count) {
$progressPercentage = (int) ($importTask->getProgress() * 100) . '%';
$moreInfoHelper = hint_tooltip(lang('PodcastImport.queue.imported_episodes_hint', [
$moreInfoHelper = '<x-Hint class="ml-1">' . lang('PodcastImport.queue.imported_episodes_hint', [
'newlyImportedCount' => $importTask->episodes_newly_imported,
'alreadyImportedCount' => $importTask->episodes_already_imported,
]), 'ml-1');
]) . '</x-Hint>';
return <<<HTML
<div class="flex flex-col">
<span>{$progressPercentage}</span>
@ -134,10 +134,10 @@ use Modules\PodcastImport\Entities\TaskStatus;
}
return '<div class="inline-flex items-center gap-x-2">' .
'<button id="more-dropdown-' . $importTask->id . '" type="button" class="inline-flex items-center p-1 rounded-full focus:ring-accent" data-dropdown="button" data-dropdown-target="more-dropdown-' . $importTask->id . '-menu" aria-haspopup="true" aria-expanded="false">' .
'<button id="more-dropdown-' . $importTask->id . '" type="button" class="inline-flex items-center p-1 rounded-full" data-dropdown="button" data-dropdown-target="more-dropdown-' . $importTask->id . '-menu" aria-haspopup="true" aria-expanded="false">' .
icon('more-2-fill') .
'</button>' .
'<DropdownMenu id="more-dropdown-' . $importTask->id . '-menu" labelledby="more-dropdown-' . $importTask->id . '" offsetY="-24" items="' . esc(json_encode($menuItems)) . '" />' .
'<x-DropdownMenu id="more-dropdown-' . $importTask->id . '-menu" labelledby="more-dropdown-' . $importTask->id . '" offsetY="-24" items="' . esc(json_encode($menuItems)) . '" />' .
'</div>';
},
],

View File

@ -13,51 +13,51 @@
<form action="<?= route_to('import') ?>" method="POST" enctype='multipart/form-data' class="flex flex-col w-full max-w-xl gap-y-8">
<?= csrf_field() ?>
<Forms.Section
<x-Forms.Section
title="<?= lang('PodcastImport.old_podcast_section_title') ?>">
<?php // @icon('scales-3-fill')?>
<Alert glyph="scales-3-fill" variant="info" title="<?= lang('PodcastImport.old_podcast_legal_disclaimer_title') ?>"><?= lang('PodcastImport.old_podcast_legal_disclaimer') ?></Alert>
<Forms.Field
<x-Alert glyph="scales-3-fill" variant="info" title="<?= lang('PodcastImport.old_podcast_legal_disclaimer_title') ?>"><?= lang('PodcastImport.old_podcast_legal_disclaimer') ?></x-Alert>
<x-Forms.Field
name="imported_feed_url"
label="<?= esc(lang('PodcastImport.imported_feed_url')) ?>"
hint="<?= esc(lang('PodcastImport.imported_feed_url_hint')) ?>"
placeholder="https://…"
type="url"
required="true" />
</Forms.Section>
isRequired="true" />
</x-Forms.Section>
<Forms.Section
<x-Forms.Section
title="<?= lang('PodcastImport.new_podcast_section_title') ?>" >
<div class="flex flex-col">
<Forms.Label for="handle" hint="<?= esc(lang('Podcast.form.handle_hint')) ?>"><?= lang('Podcast.form.handle') ?></Forms.Label>
<x-Forms.Label for="handle" hint="<?= esc(lang('Podcast.form.handle_hint')) ?>"><?= lang('Podcast.form.handle') ?></x-Forms.Label>
<div class="relative">
<?= icon('at-line', [
'class' => 'absolute inset-0 h-full text-xl opacity-40 left-3',
]) ?>
<Forms.Input name="handle" class="w-full pl-8" required="true" />
<x-Forms.Input name="handle" class="w-full pl-8" isRequired="true" />
</div>
</div>
<Forms.Field
<x-Forms.Field
as="Select"
name="language"
label="<?= esc(lang('Podcast.form.language')) ?>"
selected="<?= $browserLang ?>"
required="true"
isRequired="true"
options="<?= esc(json_encode($languageOptions)) ?>" />
<Forms.Field
<x-Forms.Field
as="Select"
name="category"
label="<?= esc(lang('Podcast.form.category')) ?>"
required="true"
isRequired="true"
options="<?= esc(json_encode($categoryOptions)) ?>" />
</Forms.Section>
</x-Forms.Section>
<Button variant="primary" type="submit" class="self-end"><?= lang('PodcastImport.submit') ?></Button>
<x-Button variant="primary" type="submit" class="self-end"><?= lang('PodcastImport.submit') ?></x-Button>
</form>

View File

@ -10,7 +10,7 @@
<?= $this->section('headerRight') ?>
<?php // @icon('loop-left-fill')?>
<Button uri="<?= route_to('podcast-imports-sync', $podcast->id) ?>" variant="primary" iconLeft="loop-left-fill"><?= lang('PodcastImport.syncForm.title') ?></Button>
<x-Button uri="<?= route_to('podcast-imports-sync', $podcast->id) ?>" variant="primary" iconLeft="loop-left-fill"><?= lang('PodcastImport.syncForm.title') ?></x-Button>
<?= $this->endSection() ?>
<?= $this->section('content') ?>

View File

@ -11,14 +11,14 @@
<?= $this->section('content') ?>
<form action="<?= route_to('podcast-imports-sync', $podcast->id) ?>" method="POST" class="flex flex-col max-w-sm gap-y-4" enctype="multipart/form-data">
<?= csrf_field() ?>
<Forms.Field
<x-Forms.Field
name="feed_url"
label="<?= esc(lang('PodcastImport.syncForm.feed_url')) ?>"
hint="<?= esc(lang('PodcastImport.syncForm.feed_url_hint')) ?>"
required="true"
isRequired="true"
value="<?= $podcast->imported_feed_url ?? '' ?>"
/>
<Button variant="primary" class="self-end" type="submit"><?= lang('PodcastImport.syncForm.submit') ?></Button>
<x-Button variant="primary" class="self-end" type="submit"><?= lang('PodcastImport.syncForm.submit') ?></x-Button>
</form>
<?= $this->endSection() ?>

View File

@ -13,7 +13,7 @@
<?= $this->section('headerRight') ?>
<?php // @icon('add-fill')?>
<Button uri="<?= route_to('podcast-imports-add') ?>" variant="primary" iconLeft="add-fill"><?= lang('Podcast.import') ?></Button>
<x-Button uri="<?= route_to('podcast-imports-add') ?>" variant="primary" iconLeft="add-fill"><?= lang('Podcast.import') ?></x-Button>
<?= $this->endSection() ?>

View File

@ -13,18 +13,18 @@
<form action="<?= route_to('change-password') ?>" method="POST" class="flex flex-col max-w-sm gap-y-4">
<?= csrf_field() ?>
<Forms.Field
<x-Forms.Field
name="password"
label="<?= esc(lang('User.form.password')) ?>"
required="true"
isRequired="true"
type="password" />
<Forms.Field
<x-Forms.Field
name="new_password"
label="<?= esc(lang('User.form.new_password')) ?>"
required="true"
isRequired="true"
type="password"
autocomplete="new-password" />
<Button variant="primary" class="self-end" type="submit"><?= lang('User.form.submit_password_change') ?></Button>
<x-Button variant="primary" class="self-end" type="submit"><?= lang('User.form.submit_password_change') ?></x-Button>
</form>
<?= $this->endSection() ?>

View File

@ -14,29 +14,29 @@
<form action="<?= route_to('page-create') ?>" method="POST" class="flex flex-col max-w-3xl gap-y-4">
<?= csrf_field() ?>
<Forms.Field
<x-Forms.Field
name="title"
label="<?= esc(lang('Page.form.title')) ?>"
required="true"
isRequired="true"
data-slugify="title"
class="max-w-sm" />
<div class="flex flex-col max-w-sm">
<Forms.Label for="slug"><?= lang('Page.form.permalink') ?></Forms.Label>
<x-Forms.Label for="slug"><?= lang('Page.form.permalink') ?></x-Forms.Label>
<permalink-edit class="inline-flex items-center w-full text-xs" edit-label="<?= lang('Common.edit') ?>" copy-label="<?= lang('Common.copy') ?>" copied-label="<?= lang('Common.copied') ?>" permalink-base="<?= base_url('pages') ?>">
<span slot="domain" class="flex-shrink-0">/pages/</span>
<Forms.Input name="slug" required="true" data-slugify="slug" slot="slug-input" class="flex-1 text-xs" />
<x-Forms.Input name="slug" isRequired="true" data-slugify="slug" slot="slug-input" class="flex-1 text-xs" />
</permalink-edit>
</div>
<Forms.Field
<x-Forms.Field
as="MarkdownEditor"
name="content"
label="<?= esc(lang('Page.form.content')) ?>"
required="true"
isRequired="true"
rows="20" />
<Button variant="primary" type="submit" class="self-end"><?= lang('Page.form.submit_create') ?></Button>
<x-Button variant="primary" type="submit" class="self-end"><?= lang('Page.form.submit_create') ?></x-Button>
</form>

View File

@ -14,32 +14,32 @@
<form action="<?= route_to('page-edit', $page->id) ?>" method="POST" class="flex flex-col max-w-3xl gap-y-4">
<?= csrf_field() ?>
<Forms.Field
<x-Forms.Field
name="title"
label="<?= esc(lang('Page.form.title')) ?>"
required="true"
isRequired="true"
data-slugify="title"
value="<?= esc($page->title) ?>"
slot="slug-input"
class="max-w-sm" />
<div class="flex flex-col max-w-sm">
<Forms.Label for="slug"><?= lang('Page.form.permalink') ?></Forms.Label>
<x-Forms.Label for="slug"><?= lang('Page.form.permalink') ?></x-Forms.Label>
<permalink-edit class="inline-flex items-center text-xs" edit-label="<?= lang('Common.edit') ?>" copy-label="<?= lang('Common.copy') ?>" copied-label="<?= lang('Common.copied') ?>" permalink-base="<?= base_url('pages') ?>">
<span slot="domain" class="flex-shrink-0">/pages/<span>
<Forms.Input name="slug" value="<?= esc($page->slug) ?>" required="true" data-slugify="slug" slot="slug-input" class="flex-1 text-xs" value="<?= esc($page->slug) ?>"/>
<x-Forms.Input name="slug" value="<?= esc($page->slug) ?>" isRequired="true" data-slugify="slug" slot="slug-input" class="flex-1 text-xs" value="<?= esc($page->slug) ?>"/>
</permalink-edit>
</div>
<Forms.Field
<x-Forms.Field
as="MarkdownEditor"
name="content"
label="<?= esc(lang('Page.form.content')) ?>"
value="<?= esc($page->content_markdown) ?>"
required="true"
isRequired="true"
rows="20" />
<Button variant="primary" type="submit" class="self-end"><?= lang('Page.form.submit_edit') ?></Button>
<x-Button variant="primary" type="submit" class="self-end"><?= lang('Page.form.submit_edit') ?></x-Button>
</form>

View File

@ -10,7 +10,7 @@
<?= $this->section('headerRight') ?>
<?php // @icon('add-fill')?>
<Button uri="<?= route_to('page-create') ?>" variant="primary" iconLeft="add-fill"><?= lang('Page.create') ?></Button>
<x-Button uri="<?= route_to('page-create') ?>" variant="primary" iconLeft="add-fill"><?= lang('Page.create') ?></x-Button>
<?= $this->endSection() ?>
@ -31,9 +31,9 @@
[
'header' => lang('Common.actions'),
'cell' => function ($page) {
return '<Button uri="' . route_to('page', esc($page->slug)) . '" variant="secondary" size="small">' . lang('Page.go_to_page') . '</Button>' .
'<Button uri="' . route_to('page-edit', $page->id) . '" variant="info" size="small">' . lang('Page.edit') . '</Button>' .
'<Button uri="' . route_to('page-delete', $page->id) . '" variant="danger" size="small">' . lang('Page.delete') . '</Button>';
return '<x-Button uri="' . route_to('page', esc($page->slug)) . '" variant="secondary" size="small">' . lang('Page.go_to_page') . '</x-Button>' .
'<x-Button uri="' . route_to('page-edit', $page->id) . '" variant="info" size="small">' . lang('Page.edit') . '</x-Button>' .
'<x-Button uri="' . route_to('page-delete', $page->id) . '" variant="danger" size="small">' . lang('Page.delete') . '</x-Button>';
},
],
],

View File

@ -10,7 +10,7 @@
<?= $this->section('headerRight') ?>
<?php // @icon('add-fill')?>
<Button variant="primary" uri="<?= route_to('page-edit', $page->id) ?>" iconLeft="add-fill"><?= lang('Page.edit') ?></Button>
<x-Button variant="primary" uri="<?= route_to('page-edit', $page->id) ?>" iconLeft="add-fill"><?= lang('Page.edit') ?></x-Button>
<?= $this->endSection() ?>
<?= $this->section('content') ?>

View File

@ -8,8 +8,8 @@
<h2 class="px-4 py-2 font-semibold leading-tight"><?= esc($person->full_name) ?></h2>
</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-accent focus:opacity-100 focus:-translate-y-0 group-hover:translate-y-0 bg-black/50 group-hover:opacity-100" id="more-dropdown-<?= $person->id ?>" data-dropdown="button" data-dropdown-target="more-dropdown-<?= $person->id ?>-menu" aria-haspopup="true" aria-expanded="false" title="<?= lang('Common.more') ?>"><?= icon('more-2-fill') ?></button>
<DropdownMenu id="more-dropdown-<?= $person->id ?>-menu" labelledby="more-dropdown-<?= $person->id ?>" offsetY="-32" items="<?= esc(json_encode([
<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:opacity-100 focus:-translate-y-0 group-hover:translate-y-0 bg-black/50 group-hover:opacity-100" id="more-dropdown-<?= $person->id ?>" data-dropdown="button" data-dropdown-target="more-dropdown-<?= $person->id ?>-menu" aria-haspopup="true" aria-expanded="false" title="<?= lang('Common.more') ?>"><?= icon('more-2-fill') ?></button>
<x-DropdownMenu id="more-dropdown-<?= $person->id ?>-menu" labelledby="more-dropdown-<?= $person->id ?>" offsetY="-32" items="<?= esc(json_encode([
[
'type' => 'link',
'title' => lang('Person.view'),

View File

@ -14,32 +14,32 @@
<form action="<?= route_to('person-create') ?>" method="POST" class="flex flex-col max-w-sm gap-y-4" enctype="multipart/form-data">
<?= csrf_field() ?>
<Forms.Field
<x-Forms.Field
name="avatar"
label="<?= esc(lang('Person.form.avatar')) ?>"
helper="<?= esc(lang('Person.form.avatar_size_hint')) ?>"
type="file"
accept=".jpg,.jpeg,.png" />
<Forms.Field
<x-Forms.Field
name="full_name"
label="<?= esc(lang('Person.form.full_name')) ?>"
hint="<?= esc(lang('Person.form.full_name_hint')) ?>"
required="true"
isRequired="true"
data-slugify="title" />
<Forms.Field
<x-Forms.Field
name="unique_name"
label="<?= esc(lang('Person.form.unique_name')) ?>"
hint="<?= esc(lang('Person.form.unique_name_hint')) ?>"
required="true"
isRequired="true"
data-slugify="slug" />
<Forms.Field
<x-Forms.Field
name="information_url"
label="<?= esc(lang('Person.form.information_url')) ?>"
hint="<?= esc(lang('Person.form.information_url_hint')) ?>" />
<Button variant="primary" class="self-end" type="submit"><?= lang('Person.form.submit_create') ?></Button>
<x-Button variant="primary" class="self-end" type="submit"><?= lang('Person.form.submit_create') ?></x-Button>
</form>

View File

@ -14,36 +14,36 @@
<form action="<?= route_to('person-edit', $person->id) ?>" method="POST" class="flex flex-col max-w-sm gap-y-4" enctype="multipart/form-data">
<?= csrf_field() ?>
<Forms.Field
<x-Forms.Field
name="avatar"
label="<?= esc(lang('Person.form.avatar')) ?>"
helper="<?= esc(lang('Person.form.avatar_size_hint')) ?>"
type="file"
accept=".jpg,.jpeg,.png" />
<Forms.Field
<x-Forms.Field
name="full_name"
value="<?= esc($person->full_name) ?>"
label="<?= esc(lang('Person.form.full_name')) ?>"
hint="<?= esc(lang('Person.form.full_name_hint')) ?>"
required="true"
isRequired="true"
data-slugify="title" />
<Forms.Field
<x-Forms.Field
name="unique_name"
value="<?= esc($person->unique_name) ?>"
label="<?= esc(lang('Person.form.unique_name')) ?>"
hint="<?= esc(lang('Person.form.unique_name_hint')) ?>"
required="true"
isRequired="true"
data-slugify="slug" />
<Forms.Field
<x-Forms.Field
name="information_url"
label="<?= esc(lang('Person.form.information_url')) ?>"
hint="<?= esc(lang('Person.form.information_url_hint')) ?>"
value="<?= esc($person->information_url) ?>" />
<Button variant="primary" class="self-end" type="submit"><?= lang('Person.form.submit_edit') ?></Button>
<x-Button variant="primary" class="self-end" type="submit"><?= lang('Person.form.submit_edit') ?></x-Button>
</form>

Some files were not shown because too many files have changed in this diff Show More