Skip to content

Commit 9edd01f

Browse files
committed
feat(panel-view): add hero() for HeroBlock, the header naming the subject a panel describes with its status badge and metric row; update README.md.
1 parent c358993 commit 9edd01f

6 files changed

Lines changed: 104 additions & 5 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## 0.4.1 Under development
99

10+
- feat(panel-view): add `hero()` for `HeroBlock`, the header naming the subject a panel describes with its status badge and metric row; update `README.md`.
11+
1012
## 0.4.0 September 25, 2026
1113

1214
- feat(panel-view)!: make `card()` a static `CardEntry` factory; add `cards()` for `CardsBlock`; update renderers and `README.md`.

‎README.md‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,12 +225,29 @@ PanelView::create()
225225
);
226226
```
227227

228+
A panel about one subject opens with `hero()`: the mark and title, a status badge whose tone also colors the header
229+
accent, and one row of `fact()` metrics.
230+
231+
```php
232+
use PHPForge\Debug\{PanelView, Tone};
233+
234+
PanelView::create()->hero(
235+
'AD',
236+
'admin',
237+
'admin@example.com',
238+
PanelView::badge('Active', Tone::SUCCESS),
239+
PanelView::fact('User ID', '1'),
240+
PanelView::fact('Roles', 'admin'),
241+
);
242+
```
243+
228244
Every method below is exercised by a built-in panel of `php-forge/debug-core`, except the two reserved for portable
229245
query and trace panels:
230246

231247
| Method | Presents | Used by |
232248
| --------------------------------- | ----------------------------------------------------------- | ------------------------- |
233-
| `section()` | A titled section wrapping its own content. | Config |
249+
| `hero()` | The header naming the subject, its status, and metrics. | User |
250+
| `section()` | A titled section wrapping its own content. | Config, User |
234251
| `stats()` / `stat()` | A strip of headline stat tiles. | Asset |
235252
| `readouts()` / `readout()` | A row of headline readout cards. | Config |
236253
| `facts()` / `fact()` | A compact strip of label and value pairs. | Asset, Config |

‎src/PanelView.php‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,30 @@ public function heading(string $title, bool $section = false): self
273273
return $this->append(new Presenter\HeadingBlock($title, $section));
274274
}
275275

276+
/**
277+
* Appends the header identifying the subject the panel describes.
278+
*
279+
* Use it once, first, when the panel is about one subject, such as the signed-in user; the status badge tone also
280+
* colors the header accent.
281+
*
282+
* @param string $mark Short text drawn as an avatar before the title, such as initials, or `''` to omit it.
283+
* @param string $title Subject name, also announced as the accessible name of the header; the host escapes it.
284+
* @param string $subtitle Qualifier shown under the title, or `''` to omit it; the host escapes it.
285+
* @param Presenter\BadgeInline|null $status Badge produced by {@see self::badge()}, or `null` for a neutral header.
286+
* @param Presenter\FactEntry ...$metrics Entries produced by {@see self::fact()}, shown as one metric row.
287+
*
288+
* @return self New view with the header appended.
289+
*/
290+
public function hero(
291+
string $mark,
292+
string $title,
293+
string $subtitle,
294+
Presenter\BadgeInline|null $status,
295+
Presenter\FactEntry ...$metrics,
296+
): self {
297+
return $this->append(new Presenter\HeroBlock($mark, $title, $subtitle, $status, array_values($metrics)));
298+
}
299+
276300
/**
277301
* Creates an inline navigation link the host renders as an anchor.
278302
*

‎src/Presenter/Block.php‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* The union is sealed, so a host narrows a block with `instanceof` and static analysis proves that no case is left
1111
* unhandled.
1212
*
13-
* @phpstan-sealed CardsBlock|DisclosureBlock|EmptyStateBlock|FactsBlock|FilesBlock|GroupBlock|HeadingBlock|LinksBlock
14-
* |ManifestBlock|OverviewBlock|ParagraphBlock|PillsBlock|ReadoutsBlock|SectionBlock|StatsBlock|TableBlock
13+
* @phpstan-sealed CardsBlock|DisclosureBlock|EmptyStateBlock|FactsBlock|FilesBlock|GroupBlock|HeadingBlock|HeroBlock
14+
* |LinksBlock|ManifestBlock|OverviewBlock|ParagraphBlock|PillsBlock|ReadoutsBlock|SectionBlock|StatsBlock|TableBlock
1515
*/
1616
interface Block {}

‎src/Presenter/HeroBlock.php‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PHPForge\Debug\Presenter;
6+
7+
/**
8+
* Represents the header identifying the subject a panel describes, such as the signed-in user.
9+
*
10+
* The host draws it as the lead card of the panel: the mark and title with their subtitle, the status badge whose tone
11+
* also colors the card accent, and one row of metrics under them.
12+
*/
13+
final readonly class HeroBlock implements Block
14+
{
15+
/**
16+
* @param string $mark Short text drawn as an avatar before the title, such as initials, or `''` to omit it.
17+
* @param string $title Subject name, also announced as the accessible name of the header; the host escapes it.
18+
* @param string $subtitle Qualifier shown under the title, or `''` to omit it; the host escapes it.
19+
* @param BadgeInline|null $status Status shown opposite the title, or `null` for a neutral header without one.
20+
* @param list<FactEntry> $metrics Label and value pairs of the metric row in display order.
21+
*/
22+
public function __construct(
23+
public string $mark,
24+
public string $title,
25+
public string $subtitle,
26+
public BadgeInline|null $status,
27+
public array $metrics,
28+
) {}
29+
}

‎tests/CompositeBlockTest.php‎

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
FactsBlock,
1616
FileEntry,
1717
FilesBlock,
18+
HeroBlock,
1819
LinkInline,
1920
LinksBlock,
2021
ManifestBlock,
@@ -34,8 +35,8 @@
3435
use stdClass;
3536

3637
/**
37-
* Unit tests for the {@see PanelView} composite blocks describing cards, stats, files, links, facts, readouts, pills,
38-
* manifests, and sections.
38+
* Unit tests for the {@see PanelView} composite blocks describing heroes, cards, stats, files, links, facts, readouts,
39+
* pills, manifests, and sections.
3940
*/
4041
#[Group('panel-view')]
4142
final class CompositeBlockTest extends TestCase
@@ -163,6 +164,32 @@ public function testFilesKeepTheirTypeToneAndOrder(): void
163164
);
164165
}
165166

167+
public function testHeroKeepsItsIdentityStatusAndMetricOrder(): void
168+
{
169+
$status = PanelView::badge('Active', Tone::SUCCESS);
170+
171+
$metrics = ['id' => PanelView::fact('User ID', '1'), 'roles' => PanelView::fact('Roles', 'admin')];
172+
173+
self::assertEquals(
174+
[
175+
new HeroBlock(
176+
'AD',
177+
'admin',
178+
'admin@example.com',
179+
$status,
180+
[new FactEntry('User ID', '1'), new FactEntry('Roles', 'admin')],
181+
),
182+
],
183+
PanelView::create()->hero('AD', 'admin', 'admin@example.com', $status, ...$metrics)->blocks(),
184+
'Metrics must keep declaration order without their string keys.',
185+
);
186+
self::assertEquals(
187+
[new HeroBlock('', 'admin', '', null, [])],
188+
PanelView::create()->hero('', 'admin', '', null)->blocks(),
189+
'A neutral header keeps its optional parts empty.',
190+
);
191+
}
192+
166193
public function testLinksKeepTheirLabelAndOrder(): void
167194
{
168195
self::assertEquals(

0 commit comments

Comments
 (0)