Skip to content

Commit 2cf595d

Browse files
committed
feat(panel-view): add typed readouts, pills, manifests, facts, and sections with factory methods.
1 parent 046584c commit 2cf595d

3 files changed

Lines changed: 290 additions & 35 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.1.3 Under development
99

10+
- feat(panel-view): add typed readouts, pills, manifests, facts, and sections with factory methods.
11+
1012
## 0.1.2 September 13, 2026
1113

1214
- fix(security): reject link targets a browser would normalize, and move exception messages to the `Exception\Message` enum.

‎src/PanelView.php‎

Lines changed: 172 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88
use JsonSerializable;
99
use PHPForge\Debug\Exception\PanelViewMessage;
1010

11+
use function array_key_exists;
1112
use function count;
1213
use function in_array;
1314
use function is_array;
15+
use function is_bool;
1416
use function is_float;
1517
use function is_int;
1618
use function is_string;
@@ -29,6 +31,8 @@
2931
* The host reads the result through {@see self::summaryMetrics()}, {@see self::toolbarMetrics()}, {@see self::blocks()},
3032
* and {@see self::isActive()}. Nothing outside this class can build or alter a shape.
3133
*
34+
* Inline values.
35+
*
3236
* @phpstan-type BadgeInline array{kind: 'badge', label: string, tone: Tone}
3337
* @phpstan-type LinkInline array{kind: 'link', label: string, href: string, external: bool}
3438
* @phpstan-type TextInline array{kind: 'text', value: string, style: 'code'|'plain'|'preview'|'sql'|'strong'}
@@ -37,10 +41,27 @@
3741
* @phpstan-type Inline BadgeInline|LinkInline|TextInline|TraceInline|ValueInline
3842
* @phpstan-type Pair array{label: string, value: Inline}
3943
* @phpstan-type TextPair array{label: string, value: TextInline}
44+
*
45+
* Entries of a composite block.
46+
*
47+
* @phpstan-type FactEntry array{kind: 'fact', label: string, value: string}
48+
* @phpstan-type PackageEntry array{kind: 'package', name: string, version: string}
49+
* @phpstan-type PillEntry array{kind: 'pill', label: string, state: string, enabled: bool}
50+
* @phpstan-type ReadoutEntry array{kind: 'readout', label: string, value: string, caption: string}
51+
*
52+
* Content blocks.
53+
*
54+
* @phpstan-type DisclosureBlock array{kind: 'disclosure', title: string, content: string}
4055
* @phpstan-type EmptyStateBlock array{kind: 'emptyState', title: string, paragraphs: list<ParagraphBlock>}
56+
* @phpstan-type FactsBlock array{kind: 'facts', facts: list<FactEntry>}
4157
* @phpstan-type GroupBlock array{kind: 'group', label: string, content: PanelView}
58+
* @phpstan-type HeadingBlock array{kind: 'heading', title: string, section: bool}
59+
* @phpstan-type ManifestBlock array{kind: 'manifest', label: string, packages: list<PackageEntry>}
4260
* @phpstan-type OverviewBlock array{kind: 'overview', fields: list<Pair>, compact: bool}
4361
* @phpstan-type ParagraphBlock array{kind: 'paragraph', content: list<Inline>, tone: Tone|null}
62+
* @phpstan-type PillsBlock array{kind: 'pills', pills: list<PillEntry>}
63+
* @phpstan-type ReadoutsBlock array{kind: 'readouts', readouts: list<ReadoutEntry>}
64+
* @phpstan-type SectionBlock array{kind: 'section', mark: string, title: string, count: int|null, content: PanelView}
4465
* @phpstan-type TableBlock array{
4566
* kind: 'table',
4667
* headers: list<string>,
@@ -49,11 +70,8 @@
4970
* collapsible: bool,
5071
* filterable: bool
5172
* }
52-
* @phpstan-type Block array{
53-
* kind: 'disclosure',
54-
* title: string,
55-
* content: string
56-
* }|array{kind: 'heading', title: string, section: bool}|EmptyStateBlock|GroupBlock|OverviewBlock|ParagraphBlock|TableBlock
73+
* @phpstan-type Block DisclosureBlock|EmptyStateBlock|FactsBlock|GroupBlock|HeadingBlock|ManifestBlock|OverviewBlock
74+
* |ParagraphBlock|PillsBlock|ReadoutsBlock|SectionBlock|TableBlock
5775
*/
5876
final readonly class PanelView implements JsonSerializable
5977
{
@@ -79,12 +97,7 @@ private function __construct(
7997
*/
8098
public function active(bool $active): self
8199
{
82-
return new self(
83-
$this->summary,
84-
$this->blocks,
85-
$this->toolbar,
86-
$active,
87-
);
100+
return new self($this->summary, $this->blocks, $this->toolbar, $active);
88101
}
89102

90103
/**
@@ -152,12 +165,7 @@ public static function code(string $value): array
152165
*/
153166
public static function create(): self
154167
{
155-
return new self(
156-
[],
157-
[],
158-
[],
159-
true,
160-
);
168+
return new self([], [], [], true);
161169
}
162170

163171
/**
@@ -195,6 +203,35 @@ public function emptyState(string $title, mixed ...$paragraphs): self
195203
return $this->append(['kind' => 'emptyState', 'title' => $title, 'paragraphs' => $content]);
196204
}
197205

206+
/**
207+
* Creates one compact label and value pair of a fact strip.
208+
*
209+
* @param string $label Name of the fact; the host escapes it.
210+
* @param string $value Recorded value; the host escapes it.
211+
*
212+
* @return FactEntry Fact entry accepted by {@see self::facts()}.
213+
*/
214+
public static function fact(string $label, string $value): array
215+
{
216+
return [
217+
'kind' => 'fact',
218+
'label' => $label,
219+
'value' => $value,
220+
];
221+
}
222+
223+
/**
224+
* Appends a compact strip of label and value pairs.
225+
*
226+
* @param FactEntry ...$facts Entries produced by {@see self::fact()}.
227+
*
228+
* @return self New view with the fact strip appended.
229+
*/
230+
public function facts(array ...$facts): self
231+
{
232+
return $this->append(['kind' => 'facts', 'facts' => array_values($facts)]);
233+
}
234+
198235
/**
199236
* Groups only the child's content; metrics and activity belong to the root view.
200237
*
@@ -270,6 +307,19 @@ public static function link(string $label, string $href, bool $external = false)
270307
];
271308
}
272309

310+
/**
311+
* Appends a vendor-grouped package manifest.
312+
*
313+
* @param string $label Vendor prefix the packages share, shown as the group heading.
314+
* @param PackageEntry ...$packages Entries produced by {@see self::package()}.
315+
*
316+
* @return self New view with the manifest appended.
317+
*/
318+
public function manifest(string $label, array ...$packages): self
319+
{
320+
return $this->append(['kind' => 'manifest', 'label' => $label, 'packages' => array_values($packages)]);
321+
}
322+
273323
/**
274324
* Appends labeled overview fields, using array keys as labels.
275325
*
@@ -293,6 +343,23 @@ public function overview(array $values, bool $compact = false): self
293343
return $this->append(['kind' => 'overview', 'fields' => $fields, 'compact' => $compact]);
294344
}
295345

346+
/**
347+
* Creates one package entry of a manifest.
348+
*
349+
* @param string $name Package name; the host escapes it.
350+
* @param string $version Resolved version; the host escapes it.
351+
*
352+
* @return PackageEntry Package entry accepted by {@see self::manifest()}.
353+
*/
354+
public static function package(string $name, string $version): array
355+
{
356+
return [
357+
'kind' => 'package',
358+
'name' => $name,
359+
'version' => $version,
360+
];
361+
}
362+
296363
/**
297364
* Appends an ordinary paragraph, converting plain values to text.
298365
*
@@ -307,6 +374,37 @@ public function paragraph(mixed ...$content): self
307374
return $this->append(self::paragraphBlock($content, null));
308375
}
309376

377+
/**
378+
* Creates one status pill.
379+
*
380+
* @param string $label Subject of the pill, such as an extension name; the host escapes it.
381+
* @param string $state Short state text shown after the label, such as `on` or a version.
382+
* @param bool $enabled Whether the subject is active, selecting the on or off presentation.
383+
*
384+
* @return PillEntry Pill entry accepted by {@see self::pills()}.
385+
*/
386+
public static function pill(string $label, string $state, bool $enabled): array
387+
{
388+
return [
389+
'kind' => 'pill',
390+
'label' => $label,
391+
'state' => $state,
392+
'enabled' => $enabled,
393+
];
394+
}
395+
396+
/**
397+
* Appends a strip of status pills.
398+
*
399+
* @param PillEntry ...$pills Entries produced by {@see self::pill()}.
400+
*
401+
* @return self New view with the pill strip appended.
402+
*/
403+
public function pills(array ...$pills): self
404+
{
405+
return $this->append(['kind' => 'pills', 'pills' => array_values($pills)]);
406+
}
407+
310408
/**
311409
* Creates inline text the host may clamp behind its standard expand control.
312410
*
@@ -323,6 +421,60 @@ public static function preview(string $value): array
323421
];
324422
}
325423

424+
/**
425+
* Creates one headline readout card.
426+
*
427+
* @param string $label Metric name shown above the value; the host escapes it.
428+
* @param string $value Headline value; the host escapes it.
429+
* @param string $caption Qualifier shown under the value, or `''` to omit it.
430+
*
431+
* @return ReadoutEntry Readout entry accepted by {@see self::readouts()}.
432+
*/
433+
public static function readout(string $label, string $value, string $caption = ''): array
434+
{
435+
return [
436+
'kind' => 'readout',
437+
'label' => $label,
438+
'value' => $value,
439+
'caption' => $caption,
440+
];
441+
}
442+
443+
/**
444+
* Appends a row of headline readout cards.
445+
*
446+
* @param ReadoutEntry ...$readouts Entries produced by {@see self::readout()}.
447+
*
448+
* @return self New view with the readout row appended.
449+
*/
450+
public function readouts(array ...$readouts): self
451+
{
452+
return $this->append(['kind' => 'readouts', 'readouts' => array_values($readouts)]);
453+
}
454+
455+
/**
456+
* Appends a titled section wrapping its own content.
457+
*
458+
* @param string $mark Short glyph shown before the title, such as `::` or `//`.
459+
* @param string $title Section title announced as its accessible name.
460+
* @param self $content Blocks the section wraps.
461+
* @param int|null $count Optional tally shown at the end of the title, or `null` to omit it.
462+
*
463+
* @return self New view with the section appended.
464+
*/
465+
public function section(string $mark, string $title, self $content, int|null $count = null): self
466+
{
467+
return $this->append(
468+
[
469+
'kind' => 'section',
470+
'mark' => $mark,
471+
'title' => $title,
472+
'count' => $count,
473+
'content' => $content,
474+
],
475+
);
476+
}
477+
326478
/**
327479
* Creates inline text the host highlights as an SQL statement.
328480
*
@@ -371,12 +523,7 @@ public function summary(string $label, string|int|float $value, bool $emphasized
371523
'value' => $emphasized ? self::strong((string) $value) : self::text((string) $value),
372524
];
373525

374-
return new self(
375-
[...$this->summary, $metric],
376-
$this->blocks,
377-
$this->toolbar,
378-
$this->active,
379-
);
526+
return new self([...$this->summary, $metric], $this->blocks, $this->toolbar, $this->active);
380527
}
381528

382529
/**
@@ -455,12 +602,7 @@ public function toolbar(string $label, string|int|float $value): self
455602
'value' => self::text((string) $value),
456603
];
457604

458-
return new self(
459-
$this->summary,
460-
$this->blocks,
461-
[...$this->toolbar, $metric],
462-
$this->active,
463-
);
605+
return new self($this->summary, $this->blocks, [...$this->toolbar, $metric], $this->active);
464606
}
465607

466608
/**
@@ -536,12 +678,7 @@ public static function value(mixed $value, bool $typeOnly = false): array
536678
*/
537679
private function append(array $block): self
538680
{
539-
return new self(
540-
$this->summary,
541-
[...$this->blocks, $block],
542-
$this->toolbar,
543-
$this->active,
544-
);
681+
return new self($this->summary, [...$this->blocks, $block], $this->toolbar, $this->active);
545682
}
546683

547684
/**

0 commit comments

Comments
 (0)