Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- refactor: centralize exception messages and panel titles in typed enums while preserving diagnostics, labels, and configurable names.
- refactor: use shared `PanelIcon` enum values for built-in panel SVG keys.
- fix: improve UI contrast, focus, deep links, history alignment, shared asset sizing, and local rebuild documentation.
- feat!: split panel texts into Event, Log, Profile, and Inertia enums; add the event detail cell, Format::typeOf(), and PageSize::selectorFor().
26 changes: 15 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,21 +189,25 @@ and pagination. Event/source shortcuts show whole-capture counts and retain the

`EventInspectorRenderer::renderControls()` renders group shortcuts and capture guidance. Adapters reuse one
`EventSequence` for the complete capture and call `renderTimeCell()` and `renderEventCell()` for each visible row.
Append `renderDetailRow()` immediately after each event row, passing the table's column count. The native disclosure
reveals context and source trace across the table width, side by side on larger screens and stacked on narrow screens.
Diagnostics do not repeat the timestamp, event name, class, source, or static flag already available in the table.
There is no standalone execution-flow renderer or secondary event table.

`PanelMessage` centralizes static presentation text, starting with Events. Shared labels have unprefixed case names;
event-specific guidance and capture-state descriptions use `EVENT_`. Pass cases directly to `content()` without
`->value`; `ui-awesome/html-mixin ^0.8.1` normalizes the enum value before HTML encoding. Captured values, filter keys,
and dynamic text remain outside the catalog. The rendered wording and snapshot format are unchanged.
Append `renderDetailRow()` immediately after each event row, passing the table's column count. Adapters that build
their own rows, for example through a grid widget's after-row callback, call `renderDetailCell()` to obtain the
disclosure content without the row wrapper. The native disclosure reveals context and source trace across the table
width, side by side on larger screens and stacked on narrow screens. Diagnostics do not repeat the timestamp, event
name, class, source, or static flag already available in the table. There is no standalone execution-flow renderer or
secondary event table.

`PanelMessage` holds only the labels shared by every panel (`CONTEXT`, `GROUP_FILTERS`, `SOURCE_TRACE`). Each panel
owns its texts in an enum next to its code (`Panel\Event\EventMessage`, `Panel\Log\LogMessage`,
`Panel\Profile\ProfileMessage`, `Panel\Inertia\InertiaMessage`) with unprefixed case names. Pass cases directly to
`content()` without `->value`; `ui-awesome/html-mixin ^0.8.1` normalizes the enum value before HTML encoding. Captured
values, filter keys, and dynamic text remain outside the catalogs. The rendered wording and snapshot format are
unchanged.

```php
use PHPForge\Debug\Panel\PanelMessage;
use PHPForge\Debug\Panel\Event\EventMessage;
use UIAwesome\Html\Flow\P;

echo P::tag()->content(PanelMessage::EVENT_CAPTURE_GUIDANCE)->render();
echo P::tag()->content(EventMessage::CAPTURE_GUIDANCE)->render();
```

`EventRow::withInspection()` creates an enriched copy without changing the captured row. `EventInspection` supplies
Expand Down
10 changes: 10 additions & 0 deletions src/Data/PageSize.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,16 @@ public static function resolve(string|null $raw, int $default = self::DEFAULT):
return min($size, self::MAX);
}

/**
* Renders the page-size selector for the `per-page` value found in the query parameters.
*
* @param array<array-key, mixed> $queryParams Query parameters already normalized by the panel.
*/
public static function selectorFor(array $queryParams): string
{
return self::selectorHtml(self::current(QueryInput::scalar($queryParams, 'per-page')));
}

/**
* Renders the inline page-size selector shown in the grid summary header.
*
Expand Down
30 changes: 29 additions & 1 deletion src/Helper/Format.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,19 @@

namespace PHPForge\Debug\Helper;

use function count;
use function gettype;
use function is_array;
use function is_bool;
use function is_float;
use function is_int;
use function is_string;
use function rtrim;
use function sprintf;
use function strlen;

/**
* Formats numeric values for display in debug-panel views and toolbar chips.
* Formats values and type labels for display in debug-panel views and toolbar chips.
*/
final class Format
{
Expand Down Expand Up @@ -42,4 +50,24 @@ public static function cssPercent(float $value): string

return "{$rendered}%";
}

/**
* Returns the display label of a value's type, with the element count for arrays and the byte length for strings.
*
* @param mixed $value JSON-safe value to describe.
*
* @return string Type label such as `array(3)`, `string(16)`, `int`, `float`, `bool`, or `null`.
*/
public static function typeOf(mixed $value): string
{
return match (true) {
is_array($value) => 'array(' . count($value) . ')',
is_string($value) => 'string(' . strlen($value) . ')',
is_int($value) => 'int',
is_float($value) => 'float',
is_bool($value) => 'bool',
$value === null => 'null',
default => gettype($value),
};
}
}
4 changes: 2 additions & 2 deletions src/Panel/Dump/DumpRow.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
/**
* Typed dump row narrowed once from the Yii logger tuple and persisted in that form.
*
* @phpstan-import-type LogMessage from \PHPForge\Debug\Panel\Log\LogSnapshot
* @phpstan-import-type LogTuple from \PHPForge\Debug\Panel\Log\LogSnapshot
*/
final readonly class DumpRow implements PanelRow
{
Expand Down Expand Up @@ -61,7 +61,7 @@ public static function fromArray(mixed $data, string $path): self
/**
* Converts one canonical logger tuple into a typed row.
*
* @param LogMessage $message Logger tuple `[message, level, category, timestamp, traces]`.
* @param LogTuple $message Logger tuple `[message, level, category, timestamp, traces]`.
*/
public static function fromLoggerTuple(array $message): self
{
Expand Down
4 changes: 2 additions & 2 deletions src/Panel/Dump/DumpSnapshot.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
/**
* Canonical Dump panel snapshot holding the captured rows in their typed form.
*
* @phpstan-import-type LogMessage from \PHPForge\Debug\Panel\Log\LogSnapshot
* @phpstan-import-type LogTuple from \PHPForge\Debug\Panel\Log\LogSnapshot
*/
final readonly class DumpSnapshot implements PanelSnapshot
{
Expand All @@ -23,7 +23,7 @@ public function __construct(private array $entries) {}
/**
* Converts canonical logger tuples into typed rows.
*
* @param list<LogMessage> $messages Logger tuples in capture order.
* @param list<LogTuple> $messages Logger tuples in capture order.
*/
public static function capture(array $messages): self
{
Expand Down
135 changes: 72 additions & 63 deletions src/Panel/Event/EventInspectorRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,32 +44,32 @@ public static function renderControls(
->html(
P::tag()
->class('yii-debug-muted')
->content(PanelMessage::EVENT_INSPECTION_GUIDANCE),
->content(EventMessage::INSPECTION_GUIDANCE),
Details::tag()
->class('yii-debug-event-coverage')
->html(
Summary::tag()->content(PanelMessage::GROUP_FILTERS),
self::groups($allRows, $filterUrl, $eventAttribute, PanelMessage::EVENT_GROUP_BY_EVENT),
self::groups($allRows, $filterUrl, 'senderClass', PanelMessage::EVENT_GROUP_BY_SOURCE),
self::groups($allRows, $filterUrl, $eventAttribute, EventMessage::GROUP_BY_EVENT),
self::groups($allRows, $filterUrl, 'senderClass', EventMessage::GROUP_BY_SOURCE),
),
Details::tag()
->class('yii-debug-event-coverage yii-debug-muted')
->html(
Summary::tag()->content(PanelMessage::EVENT_CAPTURE_COVERAGE),
Summary::tag()->content(EventMessage::CAPTURE_COVERAGE),
P::tag()->content($coverage),
P::tag()->content(PanelMessage::EVENT_CAPTURE_GUIDANCE),
P::tag()->content(PanelMessage::EVENT_TIMING_GUIDANCE),
P::tag()->content(EventMessage::CAPTURE_GUIDANCE),
P::tag()->content(EventMessage::TIMING_GUIDANCE),
),
)
->render();
}

/**
* Renders a full-width diagnostic row controlled by the preceding event disclosure.
* Renders the diagnostic disclosure content of one event without the table row wrapper.
*
* @param int<1, 1000> $columns Number of visible columns in the adapter table.
* {@see renderDetailRow()} wraps this content in a full-width table row.
*/
public static function renderDetailRow(EventRow $row, EventSequence $sequence, int $columns): string
public static function renderDetailCell(EventRow $row, EventSequence $sequence): string
{
$inspection = $row->inspection();
$index = $sequence->index($row);
Expand All @@ -84,67 +84,76 @@ public static function renderDetailRow(EventRow $row, EventSequence $sequence, i
}

$contextStatus = match ($inspection?->getContextStatus()) {
'captured' => PanelMessage::EVENT_CONTEXT_CAPTURED,
'unsupported' => PanelMessage::EVENT_CONTEXT_UNSUPPORTED,
'failed' => PanelMessage::EVENT_CONTEXT_FAILED,
default => PanelMessage::EVENT_CONTEXT_NOT_CAPTURED,
'captured' => EventMessage::CONTEXT_CAPTURED,
'unsupported' => EventMessage::CONTEXT_UNSUPPORTED,
'failed' => EventMessage::CONTEXT_FAILED,
default => EventMessage::CONTEXT_NOT_CAPTURED,
};
$traceStatus = match ($inspection?->getTraceStatus()) {
'captured' => PanelMessage::EVENT_TRACE_CAPTURED,
'failed' => PanelMessage::EVENT_TRACE_FAILED,
default => PanelMessage::EVENT_TRACE_NOT_CAPTURED,
'captured' => EventMessage::TRACE_CAPTURED,
'failed' => EventMessage::TRACE_FAILED,
default => EventMessage::TRACE_NOT_CAPTURED,
};

return Div::tag()
->id("event-{$index}-detail")
->class('yii-debug-event-detail')
->role('region')
->addAriaAttribute('label', "Diagnostics for event #{$index}")
->html(
Div::tag()->class('yii-debug-event-context')
->html(
Strong::tag()->content(PanelMessage::CONTEXT),
P::tag()->content($contextStatus),
...$context === []
? []
: [
Dl::tag()
->class('yii-debug-event-metadata')
->html(...$context),
],
),
Div::tag()
->class('yii-debug-event-trace')
->html(
Strong::tag()->content(PanelMessage::SOURCE_TRACE),
P::tag()->content($traceStatus),
...$trace === [] ? [] : [Pre::tag()->content(implode("\n", $trace))],
),
Div::tag()
->class('yii-debug-event-detail-footer')
->html(
A::tag()
->class('yii-debug-event-permalink')
->href("#event-{$index}")
->content("Link to event #{$index}"),
...$phase === '' ? [] : [
Span::tag()
->class('yii-debug-muted')
->content(
$inspection?->getPairId() === null
? EventMessage::UNMATCHED_ENTRY
: "Lifecycle correlation: scope #{$inspection->getPairId()}",
),
],
),
)
->render();
}

/**
* Renders a full-width diagnostic row controlled by the preceding event disclosure.
*
* @param int<1, 1000> $columns Number of visible columns in the adapter table.
*/
public static function renderDetailRow(EventRow $row, EventSequence $sequence, int $columns): string
{
return Tr::tag()
->class('yii-debug-event-detail-row')
->html(
Td::tag()
->colspan($columns)
->html(
Div::tag()
->id("event-{$index}-detail")
->class('yii-debug-event-detail')
->role('region')
->addAriaAttribute('label', "Diagnostics for event #{$index}")
->html(
Div::tag()->class('yii-debug-event-context')
->html(
Strong::tag()->content(PanelMessage::CONTEXT),
P::tag()->content($contextStatus),
...$context === []
? []
: [
Dl::tag()
->class('yii-debug-event-metadata')
->html(...$context),
],
),
Div::tag()
->class('yii-debug-event-trace')
->html(
Strong::tag()->content(PanelMessage::SOURCE_TRACE),
P::tag()->content($traceStatus),
...$trace === [] ? [] : [Pre::tag()->content(implode("\n", $trace))],
),
Div::tag()
->class('yii-debug-event-detail-footer')
->html(
A::tag()
->class('yii-debug-event-permalink')
->href("#event-{$index}")
->content("Link to event #{$index}"),
...$phase === '' ? [] : [
Span::tag()
->class('yii-debug-muted')
->content(
$inspection?->getPairId() === null
? PanelMessage::EVENT_UNMATCHED_ENTRY
: "Lifecycle correlation: scope #{$inspection->getPairId()}",
),
],
),
),
),
->html(self::renderDetailCell($row, $sequence)),
)
->render();
}
Expand Down Expand Up @@ -196,7 +205,7 @@ public static function renderTimeCell(EventRow $row, EventSequence $sequence): s
$gap = $sequence->gap($row);

$timing = $interval === null
? ($gap === null ? PanelMessage::EVENT_FIRST_OBSERVATION : sprintf('%+.3f ms gap', $gap))
? ($gap === null ? EventMessage::FIRST_OBSERVATION : sprintf('%+.3f ms gap', $gap))
: sprintf('%.3f ms inclusive interval', $interval);

return Div::tag()
Expand All @@ -217,7 +226,7 @@ public static function renderTimeCell(EventRow $row, EventSequence $sequence): s
* @param list<EventRow> $rows
* @param (Closure(string, string): string)|null $filterUrl
*/
private static function groups(array $rows, Closure|null $filterUrl, string $attribute, PanelMessage $label): Div
private static function groups(array $rows, Closure|null $filterUrl, string $attribute, EventMessage $label): Div
{
$groups = [];

Expand Down
Loading
Loading