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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ Projekt pozostaje świadomie lekki i server-driven: PHP generuje HTML, MySQL/Mar

## Status

**Faza: implementacja Service Platform / `1.0.0-alpha.2` (w toku).**
**Faza: implementacja UI Core / `1.0.0-alpha.3` (w toku).**

Core Kernel posiada już bootstrap, bezpieczny error boundary, data-only package discovery, routing, capability registry i izolację błędu modułu. Service Platform dostarcza baseline cache, bezpiecznego filesystemu, PDO Storage, plan-first migracji schematu per pakiet, typowanego klienta HTTP oraz kontraktu Jobs z pamięciowym providerem testowym. Nadal nie budujemy modułów domenowych takich jak Minecraft, VPS, PunisherX czy StableManagerX — najpierw platforma musi przejść kolejne kryteria roadmapy.
Core Kernel posiada bootstrap, bezpieczny error boundary, data-only package discovery, routing, capability registry i izolację błędu modułu. Service Platform ma bazowe kontrakty i providery dla cache, filesystemu, PDO Storage, migracji plan-first, HTTP, Jobs, Realtime i Audit. UI Core posiada semantyczne `PageDefinition`, renderer registry z dziedziczeniem theme, formularze, stany danych, podstawowy `DataTable`/`Pagination` oraz transport-neutralne renderowanie pełnej strony, regionu i pojedynczego komponentu. Nadal nie budujemy modułów domenowych takich jak Minecraft, VPS, PunisherX czy StableManagerX — najpierw platforma musi przejść kolejne kryteria roadmapy.

## Zasady nadrzędne

Expand Down
19 changes: 15 additions & 4 deletions docs/05-UI-COMPONENT-MODEL.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,10 +242,13 @@ Każdy data-driven component powinien jawnie obsługiwać:

Nie należy dodawać ich ad hoc per module.

`LoadingState`, `EmptyState` i `ErrorState` są już częścią publicznego kontraktu.
Base Theme nadaje im odpowiednie role accessibility (`status`/`alert`), a
`ErrorState` może pokazać bezpieczny publiczny error ID bez ujawniania szczegółu
technicznego. Stany `degraded` i `permission denied` pozostają do dodania.
`LoadingState`, `EmptyState`, `ErrorState`, `PermissionDeniedState` i
`DegradedState` są częścią publicznego kontraktu. Base Theme nadaje im
odpowiednie role accessibility (`status`/`alert`), a `ErrorState` może pokazać
bezpieczny publiczny error ID bez ujawniania szczegółu technicznego.
`PermissionDeniedState` może opcjonalnie ujawnić wyłącznie bezpieczny identyfikator
wymaganego uprawnienia, natomiast `DegradedState` może zawierać nadal dostępne
komponenty zamiast sprowadzać częściową awarię do pustego ekranu.

## 9. Fragment rendering

Expand All @@ -259,6 +262,14 @@ UI Renderer powinien umieć wyrenderować:

Dzięki temu htmx/SSE korzystają z tego samego drzewa UI zamiast osobnych template'ów.

Bazowy kontrakt `UiRenderer` i implementacja `ThemeUiRenderer` obsługują już
pełną stronę, nazwany region oraz pojedynczy komponent. Renderowanie fragmentu
używa registry aktywnego theme wraz z całym łańcuchem dziedziczenia, więc np.
komponent odziedziczony z Base Theme może nadal zawierać zagnieżdżony override
aktywnego theme. Mechanika out-of-band update, wybór target/swap oraz error
replacement pozostają odpowiedzialnością przyszłego adaptera interakcji (htmx /
Realtime), a nie modułów domenowych.

## 10. Component identity

Komponenty dynamiczne potrzebują stabilnego logical ID, ale moduł nie powinien ręcznie składać losowych DOM ID. UI Core może generować DOM identity na podstawie component tree/request context.
Expand Down
11 changes: 8 additions & 3 deletions docs/16-IMPLEMENTATION-BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,16 +226,21 @@ identyfikatory wierszy i akcje row/bulk.
- permission denied,
- degraded.

Baseline gotowy dla `LoadingState`, `EmptyState` i `ErrorState`, łącznie z
rendererami Base Theme, accessibility roles i reprezentacją w UI Catalog.
Pozostają stany permission denied oraz degraded.
Baseline gotowy dla `LoadingState`, `EmptyState`, `ErrorState`,
`PermissionDeniedState` i `DegradedState`, łącznie z rendererami Base Theme,
accessibility roles i reprezentacją w UI Catalog. `DegradedState` zachowuje
możliwość renderowania nadal dostępnej części drzewa UI.

### E6. Fragment rendering contract

- full page,
- named region,
- single component.

Baseline gotowy: `UiRenderer` + `ThemeUiRenderer` renderują pełną stronę,
pojedynczy komponent i nazwany region przez registry aktywnego theme. Szczegóły
transportu, out-of-band updates i target/swap pozostają w Epic G.

## Epic F — Theme Engine

### F1. Base Theme
Expand Down
6 changes: 6 additions & 0 deletions src/UI/Catalog/BaseUiCatalog.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
use SyntaxDevTeam\MiniPortal\UI\Component\ErrorState;
use SyntaxDevTeam\MiniPortal\UI\Component\LoadingState;
use SyntaxDevTeam\MiniPortal\UI\Component\Pagination;
use SyntaxDevTeam\MiniPortal\UI\Component\PermissionDeniedState;
use SyntaxDevTeam\MiniPortal\UI\Component\DegradedState;
use SyntaxDevTeam\MiniPortal\UI\Model\AlertSeverity;
use SyntaxDevTeam\MiniPortal\UI\Model\ComponentIdentity;
use SyntaxDevTeam\MiniPortal\UI\Model\PageRegion;
Expand Down Expand Up @@ -59,6 +61,10 @@ public function page(): PageDefinition
['name' => 'Worker', 'status' => 'idle', 'jobs' => 0],
], 'Usługi platformy'),
new Pagination(2, 3, '/catalog?page=1', '/catalog?page=3'),
new PermissionDeniedState('Brak dostępu', 'Nie masz uprawnienia do tej sekcji.', 'admin.system.read'),
new DegradedState('Dane częściowe', 'Jedno ze źródeł jest chwilowo niedostępne.', [
new Text('Pozostałe dane pozostają dostępne.', TextTone::Muted),
]),
], new ComponentIdentity('catalog.content'))]],
);
}
Expand Down
38 changes: 38 additions & 0 deletions src/UI/Component/DegradedState.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace SyntaxDevTeam\MiniPortal\UI\Component;

use SyntaxDevTeam\MiniPortal\UI\Contract\Component;
use SyntaxDevTeam\MiniPortal\UI\Model\ComponentIdentity;

final readonly class DegradedState implements Component
{
/** @param list<Component> $availableContent */
public function __construct(
public string $title,
public string $description,
private array $availableContent = [],
private ?ComponentIdentity $componentIdentity = null,
) {
if (trim($title) === '' || trim($description) === '') {
throw new \InvalidArgumentException('Degraded state requires a title and description.');
}
}

public static function componentType(): string
{
return 'degraded_state';
}

public function identity(): ?ComponentIdentity
{
return $this->componentIdentity;
}

public function children(): array
{
return $this->availableContent;
}
}
41 changes: 41 additions & 0 deletions src/UI/Component/PermissionDeniedState.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace SyntaxDevTeam\MiniPortal\UI\Component;

use SyntaxDevTeam\MiniPortal\UI\Contract\Component;
use SyntaxDevTeam\MiniPortal\UI\Model\ComponentIdentity;

final readonly class PermissionDeniedState implements Component
{
public function __construct(
public string $title = 'Brak dostępu',
public ?string $description = null,
public ?string $requiredPermission = null,
private ?ComponentIdentity $componentIdentity = null,
) {
if (trim($title) === '' || ($description !== null && trim($description) === '')) {
throw new \InvalidArgumentException('Permission denied state requires a title and a non-empty optional description.');
}
if ($requiredPermission !== null
&& preg_match('/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/D', $requiredPermission) !== 1) {
throw new \InvalidArgumentException('Required permission must be a safe permission identifier.');
}
}

public static function componentType(): string
{
return 'permission_denied_state';
}

public function identity(): ?ComponentIdentity
{
return $this->componentIdentity;
}

public function children(): array
{
return [];
}
}
14 changes: 14 additions & 0 deletions src/UI/Contract/UiRenderer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace SyntaxDevTeam\MiniPortal\UI\Contract;

use SyntaxDevTeam\MiniPortal\UI\PageDefinition;

interface UiRenderer extends PageRenderer
{
public function renderComponent(Component $component): string;

public function renderRegion(PageDefinition $page, string $region): string;
}
35 changes: 35 additions & 0 deletions src/UI/Rendering/ThemeUiRenderer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace SyntaxDevTeam\MiniPortal\UI\Rendering;

use SyntaxDevTeam\MiniPortal\UI\Contract\Component;
use SyntaxDevTeam\MiniPortal\UI\Contract\Theme;
use SyntaxDevTeam\MiniPortal\UI\Contract\UiRenderer;
use SyntaxDevTeam\MiniPortal\UI\PageDefinition;

final readonly class ThemeUiRenderer implements UiRenderer
{
private RendererRegistry $renderers;

public function __construct(private Theme $theme)
{
$this->renderers = $theme->renderers();
}

public function render(PageDefinition $page, string $language = 'pl'): string
{
return $this->theme->render($page, $language);
}

public function renderComponent(Component $component): string
{
return $this->renderers->render($component);
}

public function renderRegion(PageDefinition $page, string $region): string
{
return $this->renderers->renderMany($page->region($region));
}
}
6 changes: 6 additions & 0 deletions src/UI/Theme/Base/BaseTheme.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
use SyntaxDevTeam\MiniPortal\UI\Component\ErrorState;
use SyntaxDevTeam\MiniPortal\UI\Component\LoadingState;
use SyntaxDevTeam\MiniPortal\UI\Component\Pagination;
use SyntaxDevTeam\MiniPortal\UI\Component\PermissionDeniedState;
use SyntaxDevTeam\MiniPortal\UI\Component\DegradedState;
use SyntaxDevTeam\MiniPortal\UI\Rendering\RendererRegistry;
use SyntaxDevTeam\MiniPortal\UI\PageDefinition;
use SyntaxDevTeam\MiniPortal\UI\Theme\Base\Renderer\AlertRenderer;
Expand All @@ -35,6 +37,8 @@
use SyntaxDevTeam\MiniPortal\UI\Theme\Base\Renderer\ErrorStateRenderer;
use SyntaxDevTeam\MiniPortal\UI\Theme\Base\Renderer\LoadingStateRenderer;
use SyntaxDevTeam\MiniPortal\UI\Theme\Base\Renderer\PaginationRenderer;
use SyntaxDevTeam\MiniPortal\UI\Theme\Base\Renderer\PermissionDeniedStateRenderer;
use SyntaxDevTeam\MiniPortal\UI\Theme\Base\Renderer\DegradedStateRenderer;

final class BaseTheme implements Theme
{
Expand Down Expand Up @@ -70,6 +74,8 @@ public function renderers(): RendererRegistry
$registry->register(LoadingState::class, new LoadingStateRenderer());
$registry->register(DataTable::class, new DataTableRenderer());
$registry->register(Pagination::class, new PaginationRenderer());
$registry->register(PermissionDeniedState::class, new PermissionDeniedStateRenderer());
$registry->register(DegradedState::class, new DegradedStateRenderer());
return $registry;
}

Expand Down
32 changes: 32 additions & 0 deletions src/UI/Theme/Base/Renderer/DegradedStateRenderer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace SyntaxDevTeam\MiniPortal\UI\Theme\Base\Renderer;

use SyntaxDevTeam\MiniPortal\UI\Component\DegradedState;
use SyntaxDevTeam\MiniPortal\UI\Contract\Component;
use SyntaxDevTeam\MiniPortal\UI\Rendering\ComponentRenderer;
use SyntaxDevTeam\MiniPortal\UI\Rendering\ComponentRendererIdentity;
use SyntaxDevTeam\MiniPortal\UI\Rendering\Html;
use SyntaxDevTeam\MiniPortal\UI\Rendering\RendererRegistry;

final class DegradedStateRenderer implements ComponentRenderer
{
public function render(Component $component, RendererRegistry $registry): string
{
if (!$component instanceof DegradedState) {
throw new \LogicException('DegradedStateRenderer received an unsupported component.');
}

$content = $component->children() === []
? ''
: '<div class="mp-degraded-state__available">' . $registry->renderMany($component->children()) . '</div>';

return '<section class="mp-degraded-state" role="status" aria-live="polite"'
. Html::identityAttribute(new ComponentRendererIdentity($component)) . '>'
. '<h2 class="mp-degraded-state__title">' . Html::escape($component->title) . '</h2>'
. '<p class="mp-degraded-state__description">' . Html::escape($component->description) . '</p>'
. $content . '</section>';
}
}
35 changes: 35 additions & 0 deletions src/UI/Theme/Base/Renderer/PermissionDeniedStateRenderer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace SyntaxDevTeam\MiniPortal\UI\Theme\Base\Renderer;

use SyntaxDevTeam\MiniPortal\UI\Component\PermissionDeniedState;
use SyntaxDevTeam\MiniPortal\UI\Contract\Component;
use SyntaxDevTeam\MiniPortal\UI\Rendering\ComponentRenderer;
use SyntaxDevTeam\MiniPortal\UI\Rendering\ComponentRendererIdentity;
use SyntaxDevTeam\MiniPortal\UI\Rendering\Html;
use SyntaxDevTeam\MiniPortal\UI\Rendering\RendererRegistry;

final class PermissionDeniedStateRenderer implements ComponentRenderer
{
public function render(Component $component, RendererRegistry $_registry): string
{
if (!$component instanceof PermissionDeniedState) {
throw new \LogicException('PermissionDeniedStateRenderer received an unsupported component.');
}

$description = $component->description === null
? ''
: '<p class="mp-permission-denied-state__description">' . Html::escape($component->description) . '</p>';
$permission = $component->requiredPermission === null
? ''
: '<p class="mp-permission-denied-state__permission">Wymagane uprawnienie: <code>'
. Html::escape($component->requiredPermission) . '</code></p>';

return '<section class="mp-permission-denied-state" role="alert"'
. Html::identityAttribute(new ComponentRendererIdentity($component)) . '>'
. '<h2 class="mp-permission-denied-state__title">' . Html::escape($component->title) . '</h2>'
. $description . $permission . '</section>';
}
}
6 changes: 6 additions & 0 deletions tests/UI/BaseThemeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
use SyntaxDevTeam\MiniPortal\UI\Component\ErrorState;
use SyntaxDevTeam\MiniPortal\UI\Component\LoadingState;
use SyntaxDevTeam\MiniPortal\UI\Component\Pagination;
use SyntaxDevTeam\MiniPortal\UI\Component\PermissionDeniedState;
use SyntaxDevTeam\MiniPortal\UI\Component\DegradedState;
use SyntaxDevTeam\MiniPortal\UI\Model\AlertSeverity;
use SyntaxDevTeam\MiniPortal\UI\Model\ComponentIdentity;
use SyntaxDevTeam\MiniPortal\UI\Model\PageRegion;
Expand Down Expand Up @@ -53,6 +55,8 @@ public function testEveryCurrentPublicComponentHasBaseRenderer(): void
LoadingState::class,
DataTable::class,
Pagination::class,
PermissionDeniedState::class,
DegradedState::class,
], $registered);
}

Expand Down Expand Up @@ -92,6 +96,8 @@ public function testCatalogExercisesEveryRegisteredComponent(): void
self::assertStringContainsString('mp-error-state', $html);
self::assertStringContainsString('mp-data-table', $html);
self::assertStringContainsString('mp-pagination', $html);
self::assertStringContainsString('mp-permission-denied-state', $html);
self::assertStringContainsString('mp-degraded-state', $html);
}

public function testUnknownComponentFailsExplicitly(): void
Expand Down
Loading
Loading