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 @@ + diff --git a/resources/css/app.css b/resources/css/app.css index 10b3715..07d6267 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -9,40 +9,57 @@ --font-sans: 'Space Grotesk', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; --font-mono: 'IBM Plex Mono', ui-monospace, monospace; + --color-nord0: #2e3440; + --color-nord1: #3b4252; + --color-nord2: #434c5e; + --color-nord3: #4c566a; + --color-nord4: #d8dee9; + --color-nord5: #e5e9f0; + --color-nord6: #eceff4; + --color-nord7: #8fbcbb; + --color-nord8: #88c0d0; + --color-nord9: #81a1c1; + --color-nord10: #5e81ac; + --color-nord11: #bf616a; + --color-nord12: #d08770; + --color-nord13: #ebcb8b; + --color-nord14: #a3be8c; + --color-nord15: #b48ead; + --color-katra-accent: var(--color-nord15); } :root { color-scheme: dark; - --shell-base: #2e3440; - --shell-panel: #3b4252; - --shell-surface: #434c5e; - --shell-elevated: #4c566a; + --shell-base: var(--color-nord0); + --shell-panel: var(--color-nord1); + --shell-surface: var(--color-nord2); + --shell-elevated: var(--color-nord3); --shell-context: #353d4d; - --shell-input: #2e3440; + --shell-input: var(--color-nord0); --shell-border: rgb(76 86 106 / 72%); --shell-overlay: rgb(46 52 64 / 62%); --shell-search-overlay: rgb(46 52 64 / 76%); - --shell-text: #eceff4; - --shell-text-muted: #d8dee9; + --shell-text: var(--color-nord6); + --shell-text-muted: var(--color-nord4); --shell-text-soft: rgb(216 222 233 / 72%); --shell-text-faint: rgb(216 222 233 / 52%); --shell-text-subtle: rgb(216 222 233 / 62%); - --shell-accent: #b48ead; + --shell-accent: var(--color-nord15); --shell-accent-hover: #c2a0ba; - --shell-accent-text: #2e3440; + --shell-accent-text: var(--color-nord0); --shell-accent-soft: rgb(180 142 173 / 18%); - --shell-bot: #81a1c1; - --shell-bot-text: #2e3440; + --shell-bot: var(--color-nord9); + --shell-bot-text: var(--color-nord0); --shell-bot-soft: rgb(129 161 193 / 18%); - --shell-room: #88c0d0; - --shell-room-text: #2e3440; + --shell-room: var(--color-nord8); + --shell-room-text: var(--color-nord0); --shell-room-soft: rgb(136 192 208 / 16%); - --shell-info: #88c0d0; - --shell-info-strong: #81a1c1; + --shell-info: var(--color-nord8); + --shell-info-strong: var(--color-nord9); --shell-info-soft: rgb(129 161 193 / 16%); --shell-danger-soft: rgb(191 97 106 / 14%); --shell-danger-hover: rgb(191 97 106 / 22%); - --shell-danger-text: #ebcb8b; + --shell-danger-text: var(--color-nord13); --shell-shadow: 0 28px 64px rgb(15 23 42 / 35%); } diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php new file mode 100644 index 0000000..cc8cb9e --- /dev/null +++ b/resources/views/layouts/app.blade.php @@ -0,0 +1,23 @@ + + + + + + + {{ $title ?? config('app.name') }} + + + + + @if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot'))) + @vite(['resources/css/app.css', 'resources/js/app.js']) + @endif + + @livewireStyles + + + {{ $slot }} + + @livewireScripts + + diff --git a/resources/views/livewire/foundation-preview.blade.php b/resources/views/livewire/foundation-preview.blade.php new file mode 100644 index 0000000..bc04b06 --- /dev/null +++ b/resources/views/livewire/foundation-preview.blade.php @@ -0,0 +1,86 @@ +
+
+
+

Frontend foundation

+

Katra UI 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. +

+
+ +
+
+
+
+ Katra +

Laravel + NativePHP + SurrealDB

+

A branded shell foundation that can grow in every runtime.

+

+ 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

+

Runtime surfaces

+

+ Cycle through the intended Katra runtime targets to confirm the Livewire layer is active. +

+
+ + +
+ +
+ @foreach ($surfaces as $index => $surface) +
+

{{ $surface['label'] }}

+

{{ $surface['title'] }}

+
+ @endforeach +
+ +
+

Current focus

+

{{ $activeSurface['title'] }}

+

{{ $activeSurface['detail'] }}

+
+
+
+
+
diff --git a/routes/web.php b/routes/web.php index cf74ff5..fec1331 100644 --- a/routes/web.php +++ b/routes/web.php @@ -2,8 +2,11 @@ use App\Http\Controllers\HomeController; use App\Http\Controllers\InstanceConnectionController; +use App\Livewire\FoundationPreview; use Illuminate\Support\Facades\Route; +Route::get('/foundation-preview', FoundationPreview::class)->name('foundation.preview'); + Route::get('/connect-server', [InstanceConnectionController::class, 'showServerConnect'])->name('server.connect'); Route::post('/connect-server', [InstanceConnectionController::class, 'prepareServerLogin'])->name('server.connect.prepare'); Route::post('/connect-server/authenticate', [InstanceConnectionController::class, 'authenticateGuestServer'])->name('server.connect.authenticate'); diff --git a/tests/Feature/FrontendFoundationTest.php b/tests/Feature/FrontendFoundationTest.php new file mode 100644 index 0000000..f5c5df7 --- /dev/null +++ b/tests/Feature/FrontendFoundationTest.php @@ -0,0 +1,35 @@ +get(route('foundation.preview')) + ->assertSuccessful() + ->assertSee('Katra UI Foundation') + ->assertSee('Livewire 4') + ->assertSee('Tailwind CSS v4') + ->assertSee('/foundation-preview') + ->assertSee('Katra'); +}); + +test('the livewire foundation preview cycles through runtime surfaces', function () { + Livewire::test(FoundationPreview::class) + ->assertSee('Desktop-first shell') + ->call('cycleSurface') + ->assertSee('Server deployment') + ->call('cycleSurface') + ->assertSee('Container runtime') + ->call('cycleSurface') + ->assertSee('Desktop-first shell'); +}); + +test('the livewire foundation preview clamps invalid surface indexes', function () { + Livewire::test(FoundationPreview::class) + ->set('surfaceIndex', 99) + ->assertSet('surfaceIndex', 2) + ->assertSee('Container runtime') + ->set('surfaceIndex', -4) + ->assertSet('surfaceIndex', 0) + ->assertSee('Desktop-first shell'); +});