Skip to content

Commit 491d061

Browse files
committed
feat(panel-view)!: make card() a static CardEntry factory; add cards() for CardsBlock; update renderers and README.md.
1 parent 1b67c1f commit 491d061

8 files changed

Lines changed: 193 additions & 41 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## 0.3.1 Under development
8+
## 0.4.0 Under development
9+
10+
- feat(panel-view)!: make `card()` a static `CardEntry` factory; add `cards()` for `CardsBlock`; update renderers and `README.md`.
911

1012
## 0.3.0 September 17, 2026
1113

‎README.md‎

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,11 @@ Five types are published for panel authors: `CollectorInterface`, `Panel`, `Pane
173173
Everything a panel can display is a `PanelView` method, so an extension imports no value class and builds nothing by
174174
hand.
175175

176+
### Everyday vocabulary
177+
178+
Most panels need only these methods: metrics, headings, prose, a label and value overview, tables, groups, empty
179+
states, and raw payloads.
180+
176181
```php
177182
use PHPForge\Debug\{ColumnStyle, PanelView, Tone};
178183

@@ -181,19 +186,62 @@ PanelView::create()
181186
->toolbar('Props', 2)
182187
->heading('Props', section: true)
183188
->overview(['Component' => 'Site', 'State' => PanelView::badge('shared', Tone::INFO)])
184-
->paragraph('Rendered by ', PanelView::code('Inertia::render()'))
185-
->callout(Tone::WARNING, 'Runtime inspection is unavailable.')
189+
->paragraph('Rendered by ', PanelView::code('Inertia::render()'), ' in ', PanelView::strong('SiteController'))
190+
->callout(Tone::WARNING, 'Runtime inspection is unavailable. ', PanelView::link('Read more', '/docs/inertia'))
186191
->table(['Prop', 'Value'], [['auth', PanelView::value(['id' => 1])]], styles: [0 => ColumnStyle::IDENTIFIER])
187-
->group('Component', PanelView::create()->paragraph('Nested content'))
192+
->group('Component', PanelView::create()->paragraph(PanelView::text('Nested content')))
188193
->emptyState('No operations', 'The cache was observed, but nothing happened.')
189194
->disclosure('Raw payload', $json);
190195
```
191196

192-
Plain scalars and `null` become text. `PanelView::text()`, `::strong()`, `::code()`, `::preview()`, `::badge()`, and
193-
`::value()` produce validated inline values accepted wherever a scalar is accepted. Methods with explicit value
197+
Plain scalars and `null` become text. `PanelView::text()`, `::strong()`, `::code()`, `::badge()`, `::value()`, and
198+
`::link()` produce validated inline values accepted wherever a scalar is accepted. Methods with explicit value
194199
validation reject invalid input with an `InvalidArgumentException`; arguments with incompatible declared types raise
195200
PHP's native `TypeError`.
196201

202+
### Extended vocabulary
203+
204+
Denser layouts for inventories and dashboards. Each collection pairs a static factory that creates one entry with a
205+
method that appends the block laying those entries out, so a panel builds the entries first and appends them once.
206+
207+
```php
208+
use PHPForge\Debug\{PanelView, Tone};
209+
210+
PanelView::create()
211+
->stats(PanelView::stat('asset', 'bundles', '2'), PanelView::stat('link', 'links', '1', Tone::SUCCESS))
212+
->cards(
213+
PanelView::card(
214+
'app-asset',
215+
'asset',
216+
'AppAsset',
217+
'App\\Asset\\',
218+
[PanelView::badge('1 css', Tone::INFO)],
219+
PanelView::column('Files', PanelView::create()->files(PanelView::file('.css', 'site.css', Tone::INFO))),
220+
PanelView::column('Wiring', PanelView::create()->links('Depends on', PanelView::link('YiiAsset', '#yii'))),
221+
),
222+
);
223+
```
224+
225+
Every method below is exercised by a built-in panel of `php-forge/debug-core`, except the two reserved for portable
226+
query and trace panels:
227+
228+
| Method | Presents | Used by |
229+
| --- | --- | --- |
230+
| `section()` | A titled section wrapping its own content. | Config |
231+
| `stats()` / `stat()` | A strip of headline stat tiles. | Asset |
232+
| `readouts()` / `readout()` | A row of headline readout cards. | Config |
233+
| `facts()` / `fact()` | A compact strip of label and value pairs. | Asset, Config |
234+
| `pills()` / `pill()` | A strip of on/off status pills. | Config |
235+
| `files()` / `file()` | A list of typed file names. | Asset |
236+
| `manifest()` / `package()` | A vendor-grouped package roster. | Config |
237+
| `links()` | A labeled strip of `link()` targets. | Asset |
238+
| `cards()` / `card()` / `column()` | A grid of entity cards, each split into titled columns. | Asset |
239+
| `preview()` | Inline text the host clamps behind its expand control. | User |
240+
| `sql()` | Inline text the host highlights as an SQL statement. | No built-in emits it yet. |
241+
| `trace()` | Captured call-site frames the host renders as source links. | No built-in emits it yet. |
242+
243+
### Reading a description
244+
197245
The host reads the finished description through `summaryMetrics()`, `toolbarMetrics()`, and `blocks()`.
198246
Those accessors return `PHPForge\Debug\Presenter` value objects: `SummaryMetric`, `ToolbarMetric`, and the blocks,
199247
entries, and inline values behind them. A renderer narrows each value with `instanceof` over the sealed `Block` and

‎src/PanelView.php‎

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public function callout(Tone $tone, mixed ...$content): self
7777
}
7878

7979
/**
80-
* Appends a card describing one entity, optionally split into titled columns of its own content.
80+
* Creates one card describing an entity, optionally split into titled columns of its own content.
8181
*
8282
* @param string $id Anchor the host emits so other blocks can link to the card, or `''` to omit it.
8383
* @param string $icon Host icon key shown before the title, or `''` to omit it.
@@ -88,34 +88,44 @@ public function callout(Tone $tone, mixed ...$content): self
8888
*
8989
* @throws InvalidArgumentException if a meta value is not an accepted inline value.
9090
*
91-
* @return self New view with the card appended.
91+
* @return Presenter\CardEntry Card entry accepted by {@see self::cards()}.
9292
*/
93-
public function card(
93+
public static function card(
9494
string $id,
9595
string $icon,
9696
string $title,
9797
string $subtitle,
9898
array $meta,
9999
Presenter\ColumnEntry ...$columns
100-
): self {
100+
): Presenter\CardEntry {
101101
$inline = [];
102102

103103
foreach ($meta as $value) {
104104
$inline[] = self::inline($value);
105105
}
106106

107-
return $this->append(
108-
new Presenter\CardBlock(
109-
$id,
110-
$icon,
111-
$title,
112-
$subtitle,
113-
$inline,
114-
array_values($columns),
115-
),
107+
return new Presenter\CardEntry(
108+
$id,
109+
$icon,
110+
$title,
111+
$subtitle,
112+
$inline,
113+
array_values($columns),
116114
);
117115
}
118116

117+
/**
118+
* Appends a set of entity cards the host lays out together.
119+
*
120+
* @param Presenter\CardEntry ...$cards Entries produced by {@see self::card()}.
121+
*
122+
* @return self New view with the card set appended.
123+
*/
124+
public function cards(Presenter\CardEntry ...$cards): self
125+
{
126+
return $this->append(new Presenter\CardsBlock(array_values($cards)));
127+
}
128+
119129
/**
120130
* Creates inline text presented as source code.
121131
*

‎src/Presenter/Block.php‎

Lines changed: 1 addition & 1 deletion
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 CardBlock|DisclosureBlock|EmptyStateBlock|FactsBlock|FilesBlock|GroupBlock|HeadingBlock|LinksBlock
13+
* @phpstan-sealed CardsBlock|DisclosureBlock|EmptyStateBlock|FactsBlock|FilesBlock|GroupBlock|HeadingBlock|LinksBlock
1414
* |ManifestBlock|OverviewBlock|ParagraphBlock|PillsBlock|ReadoutsBlock|SectionBlock|StatsBlock|TableBlock
1515
*/
1616
interface Block {}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
namespace PHPForge\Debug\Presenter;
66

77
/**
8-
* Represents a card describing one entity, optionally split into titled columns of its own content.
8+
* Represents one card describing an entity, optionally split into titled columns of its own content.
99
*/
10-
final readonly class CardBlock implements Block
10+
final readonly class CardEntry
1111
{
1212
/**
1313
* @param string $id Anchor the host emits so other blocks can link to the card, or `''` to omit it.

‎src/Presenter/CardsBlock.php‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PHPForge\Debug\Presenter;
6+
7+
/**
8+
* Represents a set of entity cards the host lays out together.
9+
*/
10+
final readonly class CardsBlock implements Block
11+
{
12+
/**
13+
* @param list<CardEntry> $cards Entity cards in display order.
14+
*/
15+
public function __construct(public array $cards) {}
16+
}

‎src/Presenter/TraceInline.php‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public function __construct(array $frames)
4040
$fields = [];
4141

4242
foreach ($frame as $key => $value) {
43+
// @infection-ignore-all: numeric keys are stored as ints anyway; the cast only narrows the static type.
4344
$fields[(string) $key] = $value;
4445
}
4546

‎tests/CompositeBlockTest.php‎

Lines changed: 94 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
use PHPForge\Debug\Exception\PanelViewMessage;
99
use PHPForge\Debug\{PanelView, Tone};
1010
use PHPForge\Debug\Presenter\{
11-
CardBlock,
11+
CardEntry,
12+
CardsBlock,
1213
ColumnEntry,
1314
FactEntry,
1415
FactsBlock,
@@ -45,38 +46,64 @@ public function testCardKeepsItsMetaAndTitledColumnsInDeclarationOrder(): void
4546

4647
$files = PanelView::create()->files(PanelView::file('.css', 'site.css', Tone::INFO));
4748
$wiring = PanelView::create()->facts(PanelView::fact('SOURCE', '@app/assets'));
48-
$view = PanelView::create()
49-
->card(
49+
50+
self::assertEquals(
51+
new CardEntry(
52+
'app-asset',
53+
'asset',
54+
'AppAsset',
55+
'App\\Asset\\',
56+
[$badge, new TextInline('1 js', TextStyle::PLAIN)],
57+
[new ColumnEntry('FILES', $files), new ColumnEntry('WIRING', $wiring)],
58+
),
59+
PanelView::card(
5060
'app-asset',
5161
'asset',
5262
'AppAsset',
5363
'App\\Asset\\',
5464
[$badge, '1 js'],
5565
PanelView::column('FILES', $files),
5666
PanelView::column('WIRING', $wiring),
57-
);
67+
),
68+
'A card keeps its meta and columns in declaration order.',
69+
);
70+
71+
$columns = ['files' => PanelView::column('FILES', $files), 'wiring' => PanelView::column('WIRING', $wiring)];
5872

5973
self::assertEquals(
60-
[
61-
new CardBlock(
62-
'app-asset',
63-
'asset',
64-
'AppAsset',
65-
'App\\Asset\\',
66-
[$badge, new TextInline('1 js', TextStyle::PLAIN)],
67-
[new ColumnEntry('FILES', $files), new ColumnEntry('WIRING', $wiring)],
68-
),
69-
],
70-
$view->blocks(),
71-
'A card keeps its meta and columns in declaration order.',
74+
[new ColumnEntry('FILES', $files), new ColumnEntry('WIRING', $wiring)],
75+
PanelView::card('app-asset', 'asset', 'AppAsset', '', [], ...$columns)->columns,
76+
'String keys must not reach the column list.',
77+
);
78+
}
79+
80+
public function testCardsKeepTheirEntriesInDeclarationOrder(): void
81+
{
82+
$app = PanelView::card('app-asset', 'asset', 'AppAsset', '', []);
83+
$yii = PanelView::card('yii-asset', 'asset', 'YiiAsset', '', []);
84+
85+
$cards = ['app' => $app, 'yii' => $yii];
86+
87+
self::assertEquals(
88+
[new CardsBlock([$app, $yii]), new CardsBlock([])],
89+
PanelView::create()
90+
->cards($app, $yii)
91+
->cards()
92+
->blocks(),
93+
'A card set keeps its cards in declaration order.',
94+
);
95+
self::assertEquals(
96+
[new CardsBlock([$app, $yii])],
97+
PanelView::create()->cards(...$cards)->blocks(),
98+
'String keys must not reach the card list.',
7299
);
73100
}
74101

75102
public function testCardWithoutMetaOrColumnsCarriesEmptyLists(): void
76103
{
77104
self::assertEquals(
78-
[new CardBlock('', '', 'AppAsset', '', [], [])],
79-
PanelView::create()->card('', '', 'AppAsset', '', [])->blocks(),
105+
new CardEntry('', '', 'AppAsset', '', [], []),
106+
PanelView::card('', '', 'AppAsset', '', []),
80107
'An unadorned card keeps its optional parts empty.',
81108
);
82109
}
@@ -90,6 +117,14 @@ public function testFactsKeepTheirLabelAndValuePairs(): void
90117
->blocks(),
91118
'A fact strip keeps every pair in declaration order.',
92119
);
120+
121+
$facts = ['charset' => PanelView::fact('Charset', 'UTF-8')];
122+
123+
self::assertEquals(
124+
[new FactsBlock([new FactEntry('Charset', 'UTF-8')])],
125+
PanelView::create()->facts(...$facts)->blocks(),
126+
'String keys must not reach the fact list.',
127+
);
93128
}
94129

95130
public function testFilesKeepTheirTypeToneAndOrder(): void
@@ -118,6 +153,14 @@ public function testFilesKeepTheirTypeToneAndOrder(): void
118153
->blocks(),
119154
'A file list keeps every entry in declaration order.',
120155
);
156+
157+
$files = ['site' => PanelView::file('.css', 'site.css', Tone::INFO)];
158+
159+
self::assertEquals(
160+
[new FilesBlock([new FileEntry('.css', 'site.css', Tone::INFO)])],
161+
PanelView::create()->files(...$files)->blocks(),
162+
'String keys must not reach the file list.',
163+
);
121164
}
122165

123166
public function testLinksKeepTheirLabelAndOrder(): void
@@ -143,6 +186,14 @@ public function testLinksKeepTheirLabelAndOrder(): void
143186
->blocks(),
144187
'A link strip keeps every target in declaration order.',
145188
);
189+
190+
$links = ['yii' => PanelView::link('YiiAsset', '#yii-asset')];
191+
192+
self::assertEquals(
193+
[new LinksBlock('Depends on 1', [new LinkInline('YiiAsset', '#yii-asset', false)])],
194+
PanelView::create()->links('Depends on 1', ...$links)->blocks(),
195+
'String keys must not reach the link list.',
196+
);
146197
}
147198

148199
public function testManifestGroupsPackagesUnderOneVendorLabel(): void
@@ -163,6 +214,14 @@ public function testManifestGroupsPackagesUnderOneVendorLabel(): void
163214
$view->blocks(),
164215
'A manifest keeps its packages in declaration order under the vendor label.',
165216
);
217+
218+
$packages = ['aliases' => PanelView::package('aliases', 'v3.1.1')];
219+
220+
self::assertEquals(
221+
[new ManifestBlock('yiisoft/', [new PackageEntry('aliases', 'v3.1.1')])],
222+
PanelView::create()->manifest('yiisoft/', ...$packages)->blocks(),
223+
'String keys must not reach the package list.',
224+
);
166225
}
167226

168227
public function testPillsKeepTheirStateAndOrder(): void
@@ -177,6 +236,14 @@ public function testPillsKeepTheirStateAndOrder(): void
177236
$view->blocks(),
178237
'A pill strip keeps every subject with its own state.',
179238
);
239+
240+
$pills = ['apcu' => PanelView::pill('APCu', 'on', true)];
241+
242+
self::assertEquals(
243+
[new PillsBlock([new PillEntry('APCu', 'on', true)])],
244+
PanelView::create()->pills(...$pills)->blocks(),
245+
'String keys must not reach the pill list.',
246+
);
180247
}
181248

182249
public function testReadoutCaptionIsOptional(): void
@@ -193,6 +260,14 @@ public function testReadoutCaptionIsOptional(): void
193260
->blocks(),
194261
'A readout row carries its cards in declaration order.',
195262
);
263+
264+
$readouts = ['php' => PanelView::readout('PHP', '8.5.9', 'runtime')];
265+
266+
self::assertEquals(
267+
[new ReadoutsBlock([new ReadoutEntry('PHP', '8.5.9', 'runtime')])],
268+
PanelView::create()->readouts(...$readouts)->blocks(),
269+
'String keys must not reach the readout list.',
270+
);
196271
}
197272

198273
public function testSectionCountIsOptionalAndWrapsItsOwnBlocks(): void
@@ -248,6 +323,6 @@ public function testThrowInvalidArgumentExceptionForCardMetaNoInlineFactoryBuilt
248323
PanelViewMessage::INLINE_CONTENT_INVALID->getMessage('stdClass'),
249324
);
250325

251-
PanelView::create()->card('', '', 'AppAsset', '', [new stdClass()]);
326+
PanelView::card('', '', 'AppAsset', '', [new stdClass()]);
252327
}
253328
}

0 commit comments

Comments
 (0)