From ed7341267b5b3c9701a5340ae9182c61e9a913c3 Mon Sep 17 00:00:00 2001 From: CyberVitexus Date: Tue, 9 Jun 2026 09:11:27 +0200 Subject: [PATCH 1/4] Port FormGroup to Bootstrap 5 markup and flexible signature Replace the stale Bootstrap 4 carry-over with a proper Bootstrap 5 widget: .mb-3 wrapper, .form-label label and .form-text helper text. Restore the flexible ($label, $content, $placeholder, $helptext, $addTagClass) signature used across callers. The previous strict 3-arg, Input-typed constructor threw on label-only/no-arg calls, non-Input content (ATag, SelectTag) and the 4-arg helptext form, and referenced a non-existent Ease\TWB5\DivTag. Co-Authored-By: Claude Opus 4.8 --- src/Ease/TWB5/FormGroup.php | 79 +++++++++++++++++++++++++++++++------ 1 file changed, 67 insertions(+), 12 deletions(-) diff --git a/src/Ease/TWB5/FormGroup.php b/src/Ease/TWB5/FormGroup.php index 5d32c15..e382e3d 100644 --- a/src/Ease/TWB5/FormGroup.php +++ b/src/Ease/TWB5/FormGroup.php @@ -16,22 +16,77 @@ namespace Ease\TWB5; /** - * Description of FormGroup. + * Bootstrap 5 form group. * - * @author vitex + * Wraps a label, a form control and optional helper text using Bootstrap 5 + * markup (.mb-3 wrapper, .form-label label, .form-text helper). Replaces the + * Bootstrap 4 .form-group based widget. + * + * @author Vítězslav Dvořák */ class FormGroup extends \Ease\Html\DivTag { - public function __construct($label, \Ease\Html\Input $input, $desc = '') - { - $id = $input->setTagID(); - parent::__construct(new \Ease\Html\LabelTag($id, $label)); - $input->addTagClass('form-control'); - $input->setTagProperties(['aria-describedby' => 'desc'.$id]); - $this->addItem($input); - - if ($desc) { - $this->addItem(new DivTag($desc, ['id' => 'desc'.$id, 'class' => 'form-text'])); + /** + * Bootstrap 5 form group. + * + * @param mixed $label field label (string, Tag or array) + * @param mixed $content form control widget (or any renderable content) + * @param string $placeholder placeholder text put into the control + * @param mixed $helptext helper text rendered below the control + * @param string $addTagClass CSS class applied to the control (Bootstrap: form-control) + */ + public function __construct( + $label = null, + $content = null, + $placeholder = null, + $helptext = null, + $addTagClass = 'form-control', + ) { + parent::__construct(null, ['class' => 'mb-3']); + + // Resolve an id used to bind the label to the control. + $id = null; + + if (\is_object($content) && method_exists($content, 'getTagID')) { + $id = $content->getTagID(); + } + + if (empty($id) && \is_string($label) && $label !== '') { + $id = \Ease\Functions::lettersOnly($label); + } + + if (empty($id)) { + $id = 'formgroup_'.\Ease\Functions::randomString(); + } + + if ($label !== null && $label !== '') { + $this->addItem(new \Ease\Html\LabelTag((string) $id, $label, ['class' => 'form-label'])); + } + + if (\is_object($content)) { + if ($addTagClass && method_exists($content, 'addTagClass')) { + $content->addTagClass($addTagClass); + } + + if ($placeholder && method_exists($content, 'setTagProperties')) { + $content->setTagProperties(['placeholder' => $placeholder]); + } + + if (method_exists($content, 'setTagID')) { + $content->setTagID((string) $id); + } + + if ($helptext && method_exists($content, 'setTagProperties')) { + $content->setTagProperties(['aria-describedby' => 'desc'.$id]); + } + + $this->addItem($content); + } elseif ($content !== null) { + $this->addItem($content); + } + + if ($helptext) { + $this->addItem(new \Ease\Html\DivTag($helptext, ['id' => 'desc'.$id, 'class' => 'form-text'])); } } } From b77c410e22fbd3e1b357574c0fa41bbf44b372d5 Mon Sep 17 00:00:00 2001 From: CyberVitexus Date: Tue, 9 Jun 2026 20:22:47 +0200 Subject: [PATCH 2/4] Make OffCanvas toggle-able and add Navbar offcanvas mode OffCanvas no longer renders permanently visible (drop the hardcoded `show` class), accepts a placement argument (start|end|top|bottom), exposes public $header/$body, and gains a triggerButton() helper that mirrors Modal. Navbar gains an opt-in $offcanvas mode that turns the toggler into an offcanvas trigger and wraps the menu in a drawer (responsive offcanvas-in-navbar); the default collapse behaviour is unchanged. Co-Authored-By: Claude Opus 4.8 --- src/Ease/TWB5/Navbar.php | 54 ++++++++++++++++++++++++++-- src/Ease/TWB5/OffCanvas.php | 70 +++++++++++++++++++++++++++++-------- 2 files changed, 107 insertions(+), 17 deletions(-) diff --git a/src/Ease/TWB5/Navbar.php b/src/Ease/TWB5/Navbar.php index 1896ba1..cc34ba5 100644 --- a/src/Ease/TWB5/Navbar.php +++ b/src/Ease/TWB5/Navbar.php @@ -42,8 +42,20 @@ class Navbar extends NavTag * Brand link destination. */ public string $mainpage = '#'; + + /** + * Render the collapsible menu as an Offcanvas drawer instead of a + * Bootstrap collapse. Enable in a subclass before finalize(). + */ + public bool $offcanvas = false; + + /** + * Offcanvas drawer placement when $offcanvas is enabled. + */ + public string $offcanvasPlacement = 'end'; private string $navBarName = 'nav'; private \Ease\Html\DivTag $containerFluid; + private ButtonTag $toggler; /** * App Menu. @@ -70,7 +82,8 @@ public function __construct($brand = null, $name = 'navbar', $properties = []) $this->leftContent = new UlTag(null, ['class' => 'navbar-nav ms-auto flex-nowrap navbar-expand mb-2 mb-lg-0', 'style' => '--bs-scroll-height: 100px;']); $this->rightContent = new UlTag(null, ['class' => 'navbar-nav ml-auto']); // TODO - $this->containerFluid = $this->addItem(new \Ease\Html\DivTag([new ATag($this->mainpage, $brand, ['class' => 'navbar-brand']), $this->navBarToggler()], ['class' => 'container-fluid'])); + $this->toggler = $this->navBarToggler(); + $this->containerFluid = $this->addItem(new \Ease\Html\DivTag([new ATag($this->mainpage, $brand, ['class' => 'navbar-brand']), $this->toggler], ['class' => 'container-fluid'])); } /** @@ -136,12 +149,49 @@ public function navBarCollapse() return new \Ease\Html\DivTag($this->leftContent, ['class' => 'collapse navbar-collapse', 'id' => $this->navBarName]); } + /** + * Wrap the menu in an Offcanvas drawer (responsive offcanvas-in-navbar). + * + * @see https://getbootstrap.com/docs/5.3/components/navbar/#offcanvas + * + * @return \Ease\Html\DivTag Offcanvas drawer + */ + public function navBarOffcanvas() + { + $ocId = 'offcanvas'.$this->navBarName; + + $header = new \Ease\Html\DivTag([ + new \Ease\Html\H5Tag(_('Menu'), ['class' => 'offcanvas-title', 'id' => $ocId.'Label']), + new ButtonTag('', ['class' => 'btn-close', 'data-bs-dismiss' => 'offcanvas', 'aria-label' => _('Close')]), + ], ['class' => 'offcanvas-header']); + + $body = new \Ease\Html\DivTag($this->leftContent, ['class' => 'offcanvas-body']); + + return new \Ease\Html\DivTag([$header, $body], [ + 'class' => 'offcanvas offcanvas-'.$this->offcanvasPlacement, + 'tabindex' => '-1', + 'id' => $ocId, + 'aria-labelledby' => $ocId.'Label', + ]); + } + /** * Finalize NavBar. */ public function finalize(): void { - $this->containerFluid->addItem($this->navbarCollapse()); + if ($this->offcanvas) { + $ocId = 'offcanvas'.$this->navBarName; + $this->toggler->setTagProperties([ + 'data-bs-toggle' => 'offcanvas', + 'data-bs-target' => '#'.$ocId, + 'aria-controls' => $ocId, + ]); + $this->containerFluid->addItem($this->navBarOffcanvas()); + } else { + $this->containerFluid->addItem($this->navbarCollapse()); + } + parent::finalize(); } diff --git a/src/Ease/TWB5/OffCanvas.php b/src/Ease/TWB5/OffCanvas.php index ee13cbf..dcdecb5 100644 --- a/src/Ease/TWB5/OffCanvas.php +++ b/src/Ease/TWB5/OffCanvas.php @@ -19,25 +19,65 @@ use Ease\Html\DivTag; use Ease\Html\H5Tag; +/** + * Bootstrap Offcanvas slide-in panel. + * + * @see https://getbootstrap.com/docs/5.3/components/offcanvas/ + * + * @author Vítězslav Dvořák + */ class OffCanvas extends DivTag { - public function __construct($id, $title, $bodyContent) - { - $header = new DivTag( - [ - new H5Tag($title, ['class' => 'offcanvas-title', 'id' => $id.'Label']), - new ButtonTag('', ['class' => 'btn-close', 'data-bs-dismiss' => 'offcanvas', 'aria-label' => 'Close']), - ], - ['class' => 'offcanvas-header'], - ); - - $body = new DivTag($bodyContent, ['class' => 'offcanvas-body']); - - parent::__construct([$header, $body], [ - 'class' => 'offcanvas offcanvas-start show', + public DivTag $header; + public DivTag $body; + + /** + * Bootstrap Offcanvas. + * + * @param string $id Unique offcanvas ID + * @param mixed $title Header title + * @param mixed $bodyContent Offcanvas body content + * @param string $placement start|end|top|bottom + * @param array $properties Additional offcanvas div properties + */ + public function __construct( + string $id, + $title = null, + $bodyContent = null, + string $placement = 'start', + array $properties = [] + ) { + parent::__construct(null, array_merge([ 'tabindex' => '-1', - 'id' => $id, 'aria-labelledby' => $id.'Label', + ], $properties)); + $this->addTagClass('offcanvas offcanvas-'.$placement); + $this->setTagID($id); + + $this->header = new DivTag([ + new H5Tag($title, ['class' => 'offcanvas-title', 'id' => $id.'Label']), + new ButtonTag('', ['class' => 'btn-close', 'data-bs-dismiss' => 'offcanvas', 'aria-label' => 'Close']), + ], ['class' => 'offcanvas-header']); + + $this->body = new DivTag($bodyContent, ['class' => 'offcanvas-body']); + + $this->addItem($this->header); + $this->addItem($this->body); + } + + /** + * Returns a button that toggles this offcanvas. + * + * @param mixed $label Button label + * @param string $type primary|secondary|success|danger|warning|info|light|dark + */ + public function triggerButton($label, string $type = 'primary'): ButtonTag + { + return new ButtonTag($label, [ + 'class' => 'btn btn-'.$type, + 'data-bs-toggle' => 'offcanvas', + 'data-bs-target' => '#'.$this->getTagID(), + 'aria-controls' => $this->getTagID(), ]); } } From 1bbcad52ed72349a8366ac4091aa8d029506a8e2 Mon Sep 17 00:00:00 2001 From: CyberVitexus Date: Tue, 9 Jun 2026 20:33:39 +0200 Subject: [PATCH 3/4] Sync composer.lock after merging main (composer-normalize ^2.51) Co-Authored-By: Claude Opus 4.8 --- composer.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.lock b/composer.lock index c2569df..aacb6d9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6db6b3cf2c87765466c72588c5b8b729", + "content-hash": "f31de0d7946d45b36413fb7c854d8cbd", "packages": [ { "name": "pear/console_getopt", From 4d4678af3570c9e85ce2413ac3ecca84287b6a32 Mon Sep 17 00:00:00 2001 From: CyberVitexus Date: Tue, 9 Jun 2026 20:35:17 +0200 Subject: [PATCH 4/4] Resolve composer.lock to a compatible package set composer update --lock only refreshes the hash; actually re-resolve so the locked set is installable (composer-normalize ^2.51 pulls justinrainbow/json-schema 6.9.0). Co-Authored-By: Claude Opus 4.8 --- composer.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/composer.lock b/composer.lock index aacb6d9..a645d55 100644 --- a/composer.lock +++ b/composer.lock @@ -1579,16 +1579,16 @@ }, { "name": "justinrainbow/json-schema", - "version": "6.8.2", + "version": "6.9.0", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "2c89ebb95ca9cedc9347f780333f7b25792dcb76" + "reference": "bd1bda2ebfc8bff418565941771ea8f03c557886" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/2c89ebb95ca9cedc9347f780333f7b25792dcb76", - "reference": "2c89ebb95ca9cedc9347f780333f7b25792dcb76", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/bd1bda2ebfc8bff418565941771ea8f03c557886", + "reference": "bd1bda2ebfc8bff418565941771ea8f03c557886", "shasum": "" }, "require": { @@ -1598,7 +1598,7 @@ }, "require-dev": { "friendsofphp/php-cs-fixer": "3.3.0", - "json-schema/json-schema-test-suite": "dev-main", + "json-schema/json-schema-test-suite": "^23.2", "marc-mabe/php-enum-phpstan": "^2.0", "phpspec/prophecy": "^1.19", "phpstan/phpstan": "^1.12", @@ -1648,9 +1648,9 @@ ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/6.8.2" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.9.0" }, - "time": "2026-05-05T05:39:01+00:00" + "time": "2026-06-05T14:05:24+00:00" }, { "name": "kubawerlos/php-cs-fixer-custom-fixers",