From 668e827c70f70697ece80eb12b0a3404d254dcc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Fri, 13 Mar 2026 10:38:42 +0100 Subject: [PATCH 1/7] Implement Callout Implement a callout box element that can be used to convey important information to the user. This element is designed to be used above certain form elements, or over the whole form or page. --- asset/css/callout.less | 60 +++++++++++++++++++++ src/Common/CalloutType.php | 31 +++++++++++ src/Widget/Callout.php | 103 +++++++++++++++++++++++++++++++++++++ 3 files changed, 194 insertions(+) create mode 100644 asset/css/callout.less create mode 100644 src/Common/CalloutType.php create mode 100644 src/Widget/Callout.php diff --git a/asset/css/callout.less b/asset/css/callout.less new file mode 100644 index 00000000..889d5ace --- /dev/null +++ b/asset/css/callout.less @@ -0,0 +1,60 @@ +// Layout +.callout { + display: flex; + justify-content: center; + column-gap: 1em; + + width: fit-content; + margin: 0 auto 1em auto; + + &.fullwidth { + width: 100%; + } + + i.icon::before { + margin-right: 0; + } + + p { + margin: 0; + } + + .callout-text { + display: flex; + flex-direction: column; + } + + &.form-callout { + margin-left: 14em; + width: auto; + } +} + +// Style +.callout { + padding: .5em 1em; + border: 1px solid var(--callout-color); + background-color: color-mix(in srgb, var(--callout-color) 10%, transparent); + border-radius: .25em; + + i.icon { + color: var(--callout-color); + font-size: 1.5em; + } + + &.info { + --callout-color: @color-pending; + } + + &.success { + --callout-color: @color-ok; + } + + &.warning { + --callout-color: @color-warning; + } + + &.error { + --callout-color: @color-critical; + } +} diff --git a/src/Common/CalloutType.php b/src/Common/CalloutType.php new file mode 100644 index 00000000..aedc05ee --- /dev/null +++ b/src/Common/CalloutType.php @@ -0,0 +1,31 @@ + new Icon('circle-info'), + self::Success => new Icon('circle-check'), + self::Warning => new Icon('warning'), + self::Error => new Icon('circle-xmark'), + }; + } +} diff --git a/src/Widget/Callout.php b/src/Widget/Callout.php new file mode 100644 index 00000000..293f86c6 --- /dev/null +++ b/src/Widget/Callout.php @@ -0,0 +1,103 @@ + 'callout']; + + /** @var string|null An optional title */ + protected ?string $title; + + /** @var string The content to display */ + protected string $content; + + /** @var CalloutType The type of callout, determines the color and icon */ + protected CalloutType $type; + + public function __construct(CalloutType $type, string $content, ?string $title = null) + { + $this->type = $type; + $this->content = $content; + $this->title = $title; + + $this->addAttributes(Attributes::create(['class' => $type->value])); + } + + public function assemble(): void + { + $this->addHtml($this->type->getIcon()); + + if ($this->title) { + $this->addHtml(HtmlElement::create( + 'div', + ['class' => 'callout-text'], + [ + HtmlElement::create('strong', null, new Text($this->title)), + HtmlElement::create('p', null, new Text($this->content)), + ], + )); + } else { + $this->addHtml(HtmlElement::create( + 'div', + ['class' => 'callout-text'], + HtmlElement::create('strong', null, new Text($this->content)), + )); + } + } + + /** + * Callouts are only as wide as their content. + * Setting it to fullwidth will force the callout to be as wide as its container. + * + * @param bool $fullwidth should the callout be fullwidth + * + * @return $this + */ + public function setFullwidth(bool $fullwidth = true): static + { + if ($fullwidth) { + $this->addAttributes(Attributes::create(['class' => 'fullwidth'])); + } else { + $this->removeAttribute('class', 'fullwidth'); + } + + return $this; + } + + /** + * Setting this to true will allow the callout to be used for a single form element. + * This is used to visually align the callout to the content of the form element. + * + * @param bool $isFormElement should the callout be used for a form element + * + * @return $this + */ + public function setFormElement(bool $isFormElement = true): static + { + if ($isFormElement) { + $this->addAttributes(Attributes::create(['class' => 'form-callout'])); + } else { + $this->removeAttribute('class', 'from-callout'); + } + + return $this; + } +} From 55a88e7548dbfe9a8647e287a235bc08d6845275 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Fri, 13 Mar 2026 10:55:55 +0100 Subject: [PATCH 2/7] Stylelint suggestions --- asset/css/callout.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asset/css/callout.less b/asset/css/callout.less index 889d5ace..1b90f64e 100644 --- a/asset/css/callout.less +++ b/asset/css/callout.less @@ -5,7 +5,7 @@ column-gap: 1em; width: fit-content; - margin: 0 auto 1em auto; + margin: 0 auto 1em; &.fullwidth { width: 100%; From 72667b44cc7b3b6b9a0fef46812dd8439e612fcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Fri, 13 Mar 2026 10:56:28 +0100 Subject: [PATCH 3/7] Left align fillwidth and form callouts --- asset/css/callout.less | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/asset/css/callout.less b/asset/css/callout.less index 1b90f64e..920465e2 100644 --- a/asset/css/callout.less +++ b/asset/css/callout.less @@ -9,6 +9,13 @@ &.fullwidth { width: 100%; + justify-content: start; + } + + &.form-callout { + margin-left: 14em; + width: auto; + justify-content: start; } i.icon::before { @@ -23,11 +30,6 @@ display: flex; flex-direction: column; } - - &.form-callout { - margin-left: 14em; - width: auto; - } } // Style From dc6501bbed733dfbfbe26990c1139c113f65ae97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Mon, 16 Mar 2026 09:34:08 +0100 Subject: [PATCH 4/7] Use constructor property promotion --- src/Widget/Callout.php | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/Widget/Callout.php b/src/Widget/Callout.php index 293f86c6..a2555cc6 100644 --- a/src/Widget/Callout.php +++ b/src/Widget/Callout.php @@ -23,21 +23,18 @@ class Callout extends BaseHtmlElement protected $defaultAttributes = ['class' => 'callout']; - /** @var string|null An optional title */ - protected ?string $title; - - /** @var string The content to display */ - protected string $content; - - /** @var CalloutType The type of callout, determines the color and icon */ - protected CalloutType $type; - - public function __construct(CalloutType $type, string $content, ?string $title = null) - { - $this->type = $type; - $this->content = $content; - $this->title = $title; - + /** + * Create a new callout + * + * @param CalloutType $type the type of the callout. The type determines the color and icon that is used. + * @param string $content the text content of the callout + * @param string|null $title an optional title, displayed above the content + */ + public function __construct( + protected CalloutType $type, + protected string $content, + protected ?string $title = null + ) { $this->addAttributes(Attributes::create(['class' => $type->value])); } From 86d2483198a635502d38b36abfa42dd5a3cfe00f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Tue, 17 Mar 2026 10:39:10 +0100 Subject: [PATCH 5/7] Allow ValidHtml as the callout body --- asset/css/callout.less | 4 ++++ src/Widget/Callout.php | 40 ++++++++++++++++++++++------------------ 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/asset/css/callout.less b/asset/css/callout.less index 920465e2..336d3e88 100644 --- a/asset/css/callout.less +++ b/asset/css/callout.less @@ -26,6 +26,10 @@ margin: 0; } + .callout-title { + margin-bottom: .5em; + } + .callout-text { display: flex; flex-direction: column; diff --git a/src/Widget/Callout.php b/src/Widget/Callout.php index a2555cc6..011ce1eb 100644 --- a/src/Widget/Callout.php +++ b/src/Widget/Callout.php @@ -6,6 +6,7 @@ use ipl\Html\BaseHtmlElement; use ipl\Html\HtmlElement; use ipl\Html\Text; +use ipl\Html\ValidHtml; use ipl\I18n\Translation; use ipl\Web\Common\CalloutType; @@ -23,18 +24,27 @@ class Callout extends BaseHtmlElement protected $defaultAttributes = ['class' => 'callout']; + /** + * @var ValidHtml The content of the callout + */ + protected ValidHtml $content; + /** * Create a new callout * * @param CalloutType $type the type of the callout. The type determines the color and icon that is used. - * @param string $content the text content of the callout + * @param ValidHtml|string $content the content of the callout * @param string|null $title an optional title, displayed above the content */ public function __construct( protected CalloutType $type, - protected string $content, + ValidHtml|string $content, protected ?string $title = null ) { + if (is_string($content)) { + $content = new Text($content); + } + $this->content = $content; $this->addAttributes(Attributes::create(['class' => $type->value])); } @@ -42,22 +52,16 @@ public function assemble(): void { $this->addHtml($this->type->getIcon()); - if ($this->title) { - $this->addHtml(HtmlElement::create( - 'div', - ['class' => 'callout-text'], - [ - HtmlElement::create('strong', null, new Text($this->title)), - HtmlElement::create('p', null, new Text($this->content)), - ], - )); - } else { - $this->addHtml(HtmlElement::create( - 'div', - ['class' => 'callout-text'], - HtmlElement::create('strong', null, new Text($this->content)), - )); - } + $this->addHtml(HtmlElement::create( + 'div', + ['class' => 'callout-text'], + [ + $this->title + ? HtmlElement::create('strong', ['class' => 'callout-title'], new Text($this->title)) + : null, + $this->content, + ], + )); } /** From e07f395a7d8b166d363ab9af01379c211a2d3248 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Tue, 24 Mar 2026 11:49:27 +0100 Subject: [PATCH 6/7] Prefix css classes --- asset/css/callout.less | 10 +++++----- src/Common/CalloutType.php | 20 ++++++++++---------- src/Widget/Callout.php | 4 ++-- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/asset/css/callout.less b/asset/css/callout.less index 336d3e88..fcb4fabf 100644 --- a/asset/css/callout.less +++ b/asset/css/callout.less @@ -7,7 +7,7 @@ width: fit-content; margin: 0 auto 1em; - &.fullwidth { + &.callout-fullwidth { width: 100%; justify-content: start; } @@ -48,19 +48,19 @@ font-size: 1.5em; } - &.info { + &.callout-type-info { --callout-color: @color-pending; } - &.success { + &.callout-type-success { --callout-color: @color-ok; } - &.warning { + &.callout-type-warning { --callout-color: @color-warning; } - &.error { + &.callout-type-error { --callout-color: @color-critical; } } diff --git a/src/Common/CalloutType.php b/src/Common/CalloutType.php index aedc05ee..b8d58cc0 100644 --- a/src/Common/CalloutType.php +++ b/src/Common/CalloutType.php @@ -9,10 +9,10 @@ */ enum CalloutType: string { - case Info = "info"; - case Success = "success"; - case Warning = "warning"; - case Error = "error"; + case Info = 'callout-type-info'; + case Success = 'callout-type-success'; + case Warning = 'callout-type-warning'; + case Error = 'callout-type-error'; /** * Get the icon element for use in the callout @@ -21,11 +21,11 @@ enum CalloutType: string */ public function getIcon(): Icon { - return match ($this) { - self::Info => new Icon('circle-info'), - self::Success => new Icon('circle-check'), - self::Warning => new Icon('warning'), - self::Error => new Icon('circle-xmark'), - }; + return new Icon(match ($this) { + self::Info => 'circle-info', + self::Success => 'circle-check', + self::Warning => 'warning', + self::Error => 'circle-xmark', + }); } } diff --git a/src/Widget/Callout.php b/src/Widget/Callout.php index 011ce1eb..78ecc7bd 100644 --- a/src/Widget/Callout.php +++ b/src/Widget/Callout.php @@ -75,9 +75,9 @@ public function assemble(): void public function setFullwidth(bool $fullwidth = true): static { if ($fullwidth) { - $this->addAttributes(Attributes::create(['class' => 'fullwidth'])); + $this->addAttributes(Attributes::create(['class' => 'callout-fullwidth'])); } else { - $this->removeAttribute('class', 'fullwidth'); + $this->removeAttribute('class', 'callout-fullwidth'); } return $this; From f67c9a8b0e941d68c28f88bb43cfd3b42272ebf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rie=C3=9F?= Date: Tue, 24 Mar 2026 13:05:57 +0100 Subject: [PATCH 7/7] Move convesion from string to Text into Callout::assemble --- src/Widget/Callout.php | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/Widget/Callout.php b/src/Widget/Callout.php index 78ecc7bd..b3bf9f3f 100644 --- a/src/Widget/Callout.php +++ b/src/Widget/Callout.php @@ -24,11 +24,6 @@ class Callout extends BaseHtmlElement protected $defaultAttributes = ['class' => 'callout']; - /** - * @var ValidHtml The content of the callout - */ - protected ValidHtml $content; - /** * Create a new callout * @@ -38,13 +33,9 @@ class Callout extends BaseHtmlElement */ public function __construct( protected CalloutType $type, - ValidHtml|string $content, + protected ValidHtml|string $content, protected ?string $title = null ) { - if (is_string($content)) { - $content = new Text($content); - } - $this->content = $content; $this->addAttributes(Attributes::create(['class' => $type->value])); } @@ -59,7 +50,7 @@ public function assemble(): void $this->title ? HtmlElement::create('strong', ['class' => 'callout-title'], new Text($this->title)) : null, - $this->content, + is_string($this->content) ? new Text($this->content) : $this->content, ], )); }