diff --git a/README.md b/README.md index f3f308f..738be1f 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,15 @@ composer native:dev That path installs dependencies, prepares the Laravel app, bootstraps NativePHP, and starts the local desktop development loop. +### Frontend Foundation + +Katra's base UI foundation now includes Tailwind CSS v4 and Livewire 4. + +- Use `npm install` if you only need the frontend toolchain without running the full `composer setup` bootstrap. +- Use `npm run dev` while shaping Blade, Tailwind, and Livewire UI work in the browser. +- Use `npm run build` to verify the production asset bundle. +- Visit `/foundation-preview` to confirm the branded Livewire + Tailwind foundation is rendering successfully. + ### Authentication Katra now uses Laravel Fortify for the first authentication foundation. diff --git a/app/Livewire/FoundationPreview.php b/app/Livewire/FoundationPreview.php new file mode 100644 index 0000000..8b9763c --- /dev/null +++ b/app/Livewire/FoundationPreview.php @@ -0,0 +1,72 @@ + + */ + protected array $surfaces = [ + [ + 'label' => 'Desktop', + 'title' => 'Desktop-first shell', + 'detail' => 'NativePHP remains the first-class local shell for Katra while Laravel stays at the core.', + ], + [ + 'label' => 'Server', + 'title' => 'Server deployment', + 'detail' => 'The same Laravel foundation is intended to run as a traditional shared or dedicated server deployment.', + ], + [ + 'label' => 'Container', + 'title' => 'Container runtime', + 'detail' => 'Docker and Kubernetes targets stay in view so the product model is not trapped in a desktop-only shape.', + ], + ]; + + public function cycleSurface(): void + { + $this->surfaceIndex = ($this->surfaceIndex + 1) % count($this->surfaces); + } + + public function updatingSurfaceIndex(mixed $value): void + { + $this->surfaceIndex = $this->normalizeSurfaceIndex((int) $value); + } + + public function hydrate(): void + { + $this->surfaceIndex = $this->normalizeSurfaceIndex($this->surfaceIndex); + } + + public function render(): View + { + $this->surfaceIndex = $this->normalizeSurfaceIndex($this->surfaceIndex); + + return view('livewire.foundation-preview', [ + 'surfaces' => $this->surfaces, + 'activeSurface' => $this->surfaces[$this->surfaceIndex], + ]); + } + + protected function normalizeSurfaceIndex(int $index): int + { + $maxIndex = count($this->surfaces) - 1; + + if ($maxIndex < 0 || $index < 0) { + return 0; + } + + if ($index > $maxIndex) { + return $maxIndex; + } + + return $index; + } +} diff --git a/composer.json b/composer.json index c5e0879..76de5ee 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,7 @@ "laravel/mcp": "^0.6.3", "laravel/pennant": "^1.0", "laravel/tinker": "^3.0", + "livewire/livewire": "^4.2", "nativephp/desktop": "dev-l13-compatibility", "postare/blade-mdi": "^1.0" }, diff --git a/composer.lock b/composer.lock index 397e9d7..7555a2e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c27a0d705703a70b225ef6e4da8c55e8", + "content-hash": "87eb112b1e6534e65f94ce2bcfaf6cf7", "packages": [ { "name": "bacon/bacon-qr-code", @@ -2486,6 +2486,82 @@ ], "time": "2026-03-08T20:05:35+00:00" }, + { + "name": "livewire/livewire", + "version": "v4.2.2", + "source": { + "type": "git", + "url": "https://github.com/livewire/livewire.git", + "reference": "71c28888f99b4bfdaad89a07a104adbc3f98c04b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/livewire/livewire/zipball/71c28888f99b4bfdaad89a07a104adbc3f98c04b", + "reference": "71c28888f99b4bfdaad89a07a104adbc3f98c04b", + "shasum": "" + }, + "require": { + "illuminate/database": "^10.0|^11.0|^12.0|^13.0", + "illuminate/routing": "^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "illuminate/validation": "^10.0|^11.0|^12.0|^13.0", + "laravel/prompts": "^0.1.24|^0.2|^0.3", + "league/mime-type-detection": "^1.9", + "php": "^8.1", + "symfony/console": "^6.0|^7.0|^8.0", + "symfony/http-kernel": "^6.2|^7.0|^8.0" + }, + "require-dev": { + "calebporzio/sushi": "^2.1", + "laravel/framework": "^10.15.0|^11.0|^12.0|^13.0", + "mockery/mockery": "^1.3.1", + "orchestra/testbench": "^8.21.0|^9.0|^10.0|^11.0", + "orchestra/testbench-dusk": "^8.24|^9.1|^10.0|^11.0", + "phpunit/phpunit": "^10.4|^11.5|^12.5", + "psy/psysh": "^0.11.22|^0.12" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Livewire": "Livewire\\Livewire" + }, + "providers": [ + "Livewire\\LivewireServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Livewire\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Caleb Porzio", + "email": "calebporzio@gmail.com" + } + ], + "description": "A front-end framework for Laravel.", + "support": { + "issues": "https://github.com/livewire/livewire/issues", + "source": "https://github.com/livewire/livewire/tree/v4.2.2" + }, + "funding": [ + { + "url": "https://github.com/livewire", + "type": "github" + } + ], + "time": "2026-03-25T23:19:23+00:00" + }, { "name": "monolog/monolog", "version": "3.10.0", diff --git a/config/livewire.php b/config/livewire.php new file mode 100644 index 0000000..c4f2e90 --- /dev/null +++ b/config/livewire.php @@ -0,0 +1,282 @@ + [ + resource_path('views/components'), + resource_path('views/livewire'), + ], + + /* + |--------------------------------------------------------------------------- + | Component Namespaces + |--------------------------------------------------------------------------- + | + | This value sets default namespaces that will be used to resolve view-based + | components like single-file and multi-file components. These folders'll + | also be referenced when creating new components via the make command. + | + */ + + 'component_namespaces' => [ + 'layouts' => resource_path('views/layouts'), + 'pages' => resource_path('views/pages'), + ], + + /* + |--------------------------------------------------------------------------- + | Page Layout + |--------------------------------------------------------------------------- + | The view that will be used as the layout when rendering a single component as + | an entire page via `Route::livewire('/post/create', 'pages::create-post')`. + | In this case, the content of pages::create-post will render into $slot. + | + */ + + 'component_layout' => 'layouts::app', + + /* + |--------------------------------------------------------------------------- + | Lazy Loading Placeholder + |--------------------------------------------------------------------------- + | Livewire allows you to lazy load components that would otherwise slow down + | the initial page load. Every component can have a custom placeholder or + | you can define the default placeholder view for all components below. + | + */ + + 'component_placeholder' => null, // Example: 'placeholders::skeleton' + + /* + |--------------------------------------------------------------------------- + | Make Command + |--------------------------------------------------------------------------- + | This value determines the default configuration for the artisan make command + | You can configure the component type (sfc, mfc, class) and whether to use + | the high-voltage (⚡) emoji as a prefix in the sfc|mfc component names. + | + */ + + 'make_command' => [ + 'type' => 'class', // Options: 'sfc', 'mfc', 'class' + 'emoji' => false, // Options: true, false + 'with' => [ + 'js' => false, + 'css' => false, + 'test' => false, + ], + ], + + /* + |--------------------------------------------------------------------------- + | Class Namespace + |--------------------------------------------------------------------------- + | + | This value sets the root class namespace for Livewire component classes in + | your application. This value will change where component auto-discovery + | finds components. It's also referenced by the file creation commands. + | + */ + + 'class_namespace' => 'App\\Livewire', + + /* + |--------------------------------------------------------------------------- + | Class Path + |--------------------------------------------------------------------------- + | + | This value is used to specify the path where Livewire component class files + | are created when running creation commands like `artisan make:livewire`. + | This path is customizable to match your projects directory structure. + | + */ + + 'class_path' => app_path('Livewire'), + + /* + |--------------------------------------------------------------------------- + | View Path + |--------------------------------------------------------------------------- + | + | This value is used to specify where Livewire component Blade templates are + | stored when running file creation commands like `artisan make:livewire`. + | It is also used if you choose to omit a component's render() method. + | + */ + + 'view_path' => resource_path('views/livewire'), + + /* + |--------------------------------------------------------------------------- + | Temporary File Uploads + |--------------------------------------------------------------------------- + | + | Livewire handles file uploads by storing uploads in a temporary directory + | before the file is stored permanently. All file uploads are directed to + | a global endpoint for temporary storage. You may configure this below: + | + */ + + 'temporary_file_upload' => [ + 'disk' => env('LIVEWIRE_TEMPORARY_FILE_UPLOAD_DISK'), // Example: 'local', 's3' | Default: 'default' + 'rules' => null, // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB) + 'directory' => null, // Example: 'tmp' | Default: 'livewire-tmp' + 'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1' + 'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs... + 'png', 'gif', 'bmp', 'svg', 'wav', 'mp4', + 'mov', 'avi', 'wmv', 'mp3', 'm4a', + 'jpg', 'jpeg', 'mpga', 'webp', 'wma', + ], + 'max_upload_time' => 5, // Max duration (in minutes) before an upload is invalidated... + 'cleanup' => true, // Should cleanup temporary uploads older than 24 hrs... + ], + + /* + |--------------------------------------------------------------------------- + | Render On Redirect + |--------------------------------------------------------------------------- + | + | This value determines if Livewire will run a component's `render()` method + | after a redirect has been triggered using something like `redirect(...)` + | Setting this to true will render the view once more before redirecting + | + */ + + 'render_on_redirect' => false, + + /* + |--------------------------------------------------------------------------- + | Eloquent Model Binding + |--------------------------------------------------------------------------- + | + | Previous versions of Livewire supported binding directly to eloquent model + | properties using wire:model by default. However, this behavior has been + | deemed too "magical" and has therefore been put under a feature flag. + | + */ + + 'legacy_model_binding' => false, + + /* + |--------------------------------------------------------------------------- + | Auto-inject Frontend Assets + |--------------------------------------------------------------------------- + | + | By default, Livewire automatically injects its JavaScript and CSS into the + |
and of pages containing Livewire components. By disabling + | this behavior, you need to use @livewireStyles and @livewireScripts. + | + */ + + 'inject_assets' => false, + + /* + |--------------------------------------------------------------------------- + | Navigate (SPA mode) + |--------------------------------------------------------------------------- + | + | By adding `wire:navigate` to links in your Livewire application, Livewire + | will prevent the default link handling and instead request those pages + | via AJAX, creating an SPA-like effect. Configure this behavior here. + | + */ + + 'navigate' => [ + 'show_progress_bar' => true, + 'progress_bar_color' => '#2299dd', + ], + + /* + |--------------------------------------------------------------------------- + | HTML Morph Markers + |--------------------------------------------------------------------------- + | + | Livewire intelligently "morphs" existing HTML into the newly rendered HTML + | after each update. To make this process more reliable, Livewire injects + | "markers" into the rendered Blade surrounding @if, @class & @foreach. + | + */ + + 'inject_morph_markers' => true, + + /* + |--------------------------------------------------------------------------- + | Smart Wire Keys + |--------------------------------------------------------------------------- + | + | Livewire uses loops and keys used within loops to generate smart keys that + | are applied to nested components that don't have them. This makes using + | nested components more reliable by ensuring that they all have keys. + | + */ + + 'smart_wire_keys' => true, + + /* + |--------------------------------------------------------------------------- + | Pagination Theme + |--------------------------------------------------------------------------- + | + | When enabling Livewire's pagination feature by using the `WithPagination` + | trait, Livewire will use Tailwind templates to render pagination views + | on the page. If you want Bootstrap CSS, you can specify: "bootstrap" + | + */ + + 'pagination_theme' => 'tailwind', + + /* + |--------------------------------------------------------------------------- + | Release Token + |--------------------------------------------------------------------------- + | + | This token is stored client-side and sent along with each request to check + | a users session to see if a new release has invalidated it. If there is + | a mismatch it will throw an error and prompt for a browser refresh. + | + */ + + 'release_token' => 'a', + + /* + |--------------------------------------------------------------------------- + | CSP Safe + |--------------------------------------------------------------------------- + | + | This config is used to determine if Livewire will use the CSP-safe version + | of Alpine in its bundle. This is useful for applications that are using + | strict Content Security Policy (CSP) to protect against XSS attacks. + | + */ + + 'csp_safe' => false, + + /* + |--------------------------------------------------------------------------- + | Payload Guards + |--------------------------------------------------------------------------- + | + | These settings protect against malicious or oversized payloads that could + | cause denial of service. The default values should feel reasonable for + | most web applications. Each can be set to null to disable the limit. + | + */ + + 'payload' => [ + 'max_size' => 1024 * 1024, // 1MB - maximum request payload size in bytes + 'max_nesting_depth' => 10, // Maximum depth of dot-notation property paths + 'max_calls' => 50, // Maximum method calls per request + 'max_components' => 20, // Maximum components per batch request + ], +]; diff --git a/phpunit.xml b/phpunit.xml index e7f0a48..23af689 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -19,6 +19,7 @@Frontend foundation
++ Livewire 4 and Tailwind CSS v4 are now part of Katra's frontend foundation, sitting on top of the + existing brand assets and Nord color system. +
+Laravel + NativePHP + SurrealDB
++ The current UI layer stays intentionally small: one Livewire-driven preview surface, the existing + Katra logo and wordmark, and the Nord palette tokens with nord15 + carrying the primary accent role. +
+Stack
+Livewire 4
+Interactive Laravel components without leaving the product core.
+Styling
+Tailwind CSS v4
+CSS-first theme tokens backed by the Nord palette.
+Preview
+/foundation-preview
+A minimal landing surface to smoke-test the stack.
+Livewire preview
++ Cycle through the intended Katra runtime targets to confirm the Livewire layer is active. +
+{{ $surface['label'] }}
+{{ $surface['title'] }}
+Current focus
+{{ $activeSurface['detail'] }}
+