88use JsonSerializable ;
99use PHPForge \Debug \Exception \PanelViewMessage ;
1010
11+ use function array_key_exists ;
1112use function count ;
1213use function in_array ;
1314use function is_array ;
15+ use function is_bool ;
1416use function is_float ;
1517use function is_int ;
1618use function is_string ;
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'}
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>,
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 */
5876final 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