diff --git a/README.md b/README.md index 35827c1..1bae21f 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/05-UI-COMPONENT-MODEL.md b/docs/05-UI-COMPONENT-MODEL.md index 7d722e0..3477095 100644 --- a/docs/05-UI-COMPONENT-MODEL.md +++ b/docs/05-UI-COMPONENT-MODEL.md @@ -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 @@ -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. diff --git a/docs/16-IMPLEMENTATION-BACKLOG.md b/docs/16-IMPLEMENTATION-BACKLOG.md index 0f7d376..073d2e6 100644 --- a/docs/16-IMPLEMENTATION-BACKLOG.md +++ b/docs/16-IMPLEMENTATION-BACKLOG.md @@ -226,9 +226,10 @@ 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 @@ -236,6 +237,10 @@ Pozostają stany permission denied oraz degraded. - 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 diff --git a/src/UI/Catalog/BaseUiCatalog.php b/src/UI/Catalog/BaseUiCatalog.php index 59dd5ce..13f0e4e 100644 --- a/src/UI/Catalog/BaseUiCatalog.php +++ b/src/UI/Catalog/BaseUiCatalog.php @@ -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; @@ -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'))]], ); } diff --git a/src/UI/Component/DegradedState.php b/src/UI/Component/DegradedState.php new file mode 100644 index 0000000..356fd5f --- /dev/null +++ b/src/UI/Component/DegradedState.php @@ -0,0 +1,38 @@ + $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; + } +} diff --git a/src/UI/Component/PermissionDeniedState.php b/src/UI/Component/PermissionDeniedState.php new file mode 100644 index 0000000..9227a3d --- /dev/null +++ b/src/UI/Component/PermissionDeniedState.php @@ -0,0 +1,41 @@ +componentIdentity; + } + + public function children(): array + { + return []; + } +} diff --git a/src/UI/Contract/UiRenderer.php b/src/UI/Contract/UiRenderer.php new file mode 100644 index 0000000..3a93fa4 --- /dev/null +++ b/src/UI/Contract/UiRenderer.php @@ -0,0 +1,14 @@ +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)); + } +} diff --git a/src/UI/Theme/Base/BaseTheme.php b/src/UI/Theme/Base/BaseTheme.php index 34eb39a..2577fa5 100644 --- a/src/UI/Theme/Base/BaseTheme.php +++ b/src/UI/Theme/Base/BaseTheme.php @@ -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; @@ -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 { @@ -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; } diff --git a/src/UI/Theme/Base/Renderer/DegradedStateRenderer.php b/src/UI/Theme/Base/Renderer/DegradedStateRenderer.php new file mode 100644 index 0000000..db9eea2 --- /dev/null +++ b/src/UI/Theme/Base/Renderer/DegradedStateRenderer.php @@ -0,0 +1,32 @@ +children() === [] + ? '' + : '
' . Html::escape($component->description) . '
' + . $content . 'module.files.read', $html);
+ }
+
+ public function testPermissionDeniedStateRejectsUnsafePermissionIdentifier(): void
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ new PermissionDeniedState(requiredPermission: '