Introduction

Overview

How Tagixo integrates with Primix and what the SDK provides out of the box.

Tagixo Primix SDK

ccast/tagixo-primix is the official adapter that connects the Tagixo visual builder to a Primix admin panel. It does not replace the core Tagixo package — it sits on top of it, providing Primix-aware page classes, bridge traits, form helpers, and a plugin registration point. All rendering logic, module registry, PropType resolution, and builder payload contract remain in ccast/tagixo core and are untouched by the adapter.

Architecture

The adapter introduces a thin integration layer between two otherwise independent systems.

┌──────────────────────────────────────────────────┐
│                 Primix Admin Panel                │
│  (LiVue reactivity, panel routes, notifications) │
└───────────────────────┬──────────────────────────┘
                        │ TagixoPrimixPlugin
                        ▼
┌──────────────────────────────────────────────────┐
│             ccast/tagixo-primix                  │
│  BuilderPage / PrimixVisualBuilderPage           │
│  InteractsWithVisualBuilderPrimix (bridge trait) │
│  PrimixFormColumns / PrimixFormFilters /         │
│  PrimixFormStyles                                │
└───────────────────────┬──────────────────────────┘
                        │ builder lifecycle hooks
                        ▼
┌──────────────────────────────────────────────────┐
│              ccast/tagixo (core)                 │
│  InteractsWithVisualBuilder (framework-agnostic) │
│  PageRenderer, ModuleRegistry, PropTypes,        │
│  builder HTTP routes, asset publication          │
└──────────────────────────────────────────────────┘

The split matters for portability. If you later migrate to Filament, you swap the bridge layer — your page class structure, loadStructure() / saveStructure() implementations, and your content model all remain unchanged.

LiVue vs. Livewire

Primix is built on LiVue, not Livewire. This distinction is important for SDK consumers because several Filament code patterns fail silently or behave differently under LiVue.

Aspect LiVue Livewire
Reactivity model Vue 3 composition API PHP component wire protocol
DOM morphing LiVue morphs on component updates Livewire morphs on response diff
dispatch() $dispatch() via Vue event bus Livewire dispatch() to PHP listener
<style> tag survival Stripped on LiVue component morph Survives Livewire updates
Date filter class Primix\Tables\Filters\DateFilter Filament\Tables\Filters\DateFilter
Wire prefix Not used wire:model, wire:click, etc.

Gotcha: if you copy filter or form component code from Filament documentation, import paths will be wrong. Primix classes live under the Primix\ namespace, not Filament\. The method signatures are often identical, but the namespaces differ.

Gotcha: <style> tags injected into LiVue component DOM are stripped on every morph cycle. The PrimixFormStyles helper works around this with a <script> that writes CSS into <head> — the only stable location across morphs.

What the SDK provides

Component Class / Artifact Purpose
Plugin Ccast\TagixoPrimix\TagixoPrimixPlugin Wires builder routes, registers Primix UI hooks
Standalone page Ccast\TagixoPrimix\Pages\BuilderPage Builder not tied to any resource record
Resource page Ccast\TagixoPrimix\Pages\PrimixVisualBuilderPage Builder bound to a model record
Bridge trait Ccast\TagixoPrimix\Concerns\InteractsWithVisualBuilderPrimix Adapts core lifecycle to LiVue dispatch
Form columns Ccast\TagixoPrimix\Forms\PrimixFormColumns Auto-generates Primix table columns from form schema
Form filters Ccast\TagixoPrimix\Forms\PrimixFormFilters Auto-generates Primix table filters from form schema
Form styles Ccast\TagixoPrimix\Forms\PrimixFormStyles Injects form CSS into <head>, survives LiVue morphs
CLI scaffold php artisan make:primix-builder-page Generates a starter page class

Plugin registration

Register TagixoPrimixPlugin in your panel provider. The plugin wires builder routes, registers LiVue dispatch hooks, and (optionally) activates the media gallery integration:

use Ccast\TagixoPrimix\TagixoPrimixPlugin;

public function panel(Panel $panel): Panel
{
    return $panel
        ->plugin(
            TagixoPrimixPlugin::make()
                ->withMediaGallery()   // optional: enable media library integration
        );
}

TagixoPrimixPlugin::make() with no options is valid if you do not use the media gallery. The plugin does not publish assets — that step is separate.

Asset publication

The builder bundle (builder.js, builder.css, chunk files, fonts) lives in ccast/tagixo's dist/ directory and is published from there, not from the Primix adapter:

php artisan vendor:publish --tag=tagixo-assets --force

Assets land in public/vendor/tagixo/. The Primix SDK's layout Blade references them directly:

<script src="{{ asset('vendor/tagixo/builder.js') }}" type="module"></script>
<link rel="stylesheet" href="{{ asset('vendor/tagixo/builder.css') }}">

There is no @vite() call, no manifest lookup, and no npm/Vite toolchain required in the consumer app. Asset filenames are stable across patch releases — the Blade can reference them without manifest indirection.

Always pass --force when re-publishing after a Tagixo upgrade. Without it, vendor:publish skips files that already exist, leaving stale JavaScript in place.

Module script query busting trap: do not append ?v= cache-busting parameters to type="module" script tags. Browsers treat a different query string as a different module specifier, which forks the ES module graph and causes Pinia to instantiate twice (two stores, each unaware of the other). If CDN caching is an issue, disable asset caching at the proxy level rather than using query strings.

Trait architecture

The builder lifecycle is implemented across two traits that are composed together in the base page classes.

InteractsWithVisualBuilder (core)

Framework-agnostic. Ships in ccast/tagixo. Handles:

  • mount() — initializes builder state, resolves context and variant
  • loadStructure() contract — abstract hook you implement to read builder JSON from your model
  • saveStructure() contract — abstract hook you implement to persist builder JSON
  • getStructure() / getContext() — surface the builder payload to the Blade view
  • Save dispatch — calls saveStructure() on builder save events from JS

Do not modify this trait or override its methods in ways that skip the lifecycle steps. It is the contract between the builder JavaScript and your PHP page.

InteractsWithVisualBuilderPrimix (bridge)

Primix-specific. Ships in ccast/tagixo-primix. Handles:

  • Adapts the abstract save trigger to a LiVue $dispatch() event
  • Integrates save success/failure with Primix's notification system
  • Provides Primix-specific getPreviewUrl() default behavior
  • Wires the builder toolbar's "Saved" confirmation back to the Primix UI

You never apply these traits directly. The base page classes already use both:

// Inside BuilderPage (SDK source — do not copy this into your app)
use Ccast\Tagixo\Concerns\InteractsWithVisualBuilder;
use Ccast\TagixoPrimix\Concerns\InteractsWithVisualBuilderPrimix;

abstract class BuilderPage extends Page
{
    use InteractsWithVisualBuilder;
    use InteractsWithVisualBuilderPrimix;
}

Your class extends BuilderPage and implements the two abstract methods. That is all the wiring you need.

Standalone builder page

Use Ccast\TagixoPrimix\Pages\BuilderPage when the builder edits a single, fixed entity — a homepage, a global header, a contact page — that is not represented as a Primix resource with a list view.

You must implement three methods:

Method Signature Purpose
getContext() (): string Returns the builder context ('page', 'form', 'mail', 'pdf')
loadStructure() (): ?string Returns the current builder JSON, or null for a blank canvas
saveStructure() (string $structure): void Persists the JSON string to your model
use App\Models\SitePage;
use Ccast\TagixoPrimix\Pages\BuilderPage;

class HomepageBuilder extends BuilderPage
{
    protected static ?string $navigationIcon = 'heroicon-o-home';
    protected static ?string $navigationLabel = 'Homepage';

    public function getContext(): string
    {
        return 'page';
    }

    public function loadStructure(): ?string
    {
        $page = SitePage::query()->where('slug', 'home')->first();

        return $page?->builder_content
            ? json_encode($page->builder_content)
            : null;
    }

    public function saveStructure(string $structure): void
    {
        SitePage::query()
            ->where('slug', 'home')
            ->update(['builder_content' => json_decode($structure, true)]);
    }
}

loadStructure() returns a JSON string (or null). If your model stores the decoded array in a cast column, encode it on the way out and decode it on the way in. The core trait owns the wire between the string and the builder JavaScript.

Resource-bound builder page

Use Ccast\TagixoPrimix\Pages\PrimixVisualBuilderPage when the builder is a custom page inside a Primix resource — for example, an "Edit Content" action alongside the standard Edit form for a Post resource.

PrimixVisualBuilderPage extends Primix's ResourcePage and gives you access to $this->record after mount() resolves the record. Declare the resource class and implement the same three methods:

use App\Primix\Resources\PostResource;
use Ccast\TagixoPrimix\Pages\PrimixVisualBuilderPage;

class EditPostContent extends PrimixVisualBuilderPage
{
    protected static string $resource = PostResource::class;

    public function getContext(): string
    {
        return 'page';
    }

    public function loadStructure(): ?string
    {
        return $this->record->builder_content
            ? json_encode($this->record->builder_content)
            : null;
    }

    public function saveStructure(string $structure): void
    {
        $this->record->update([
            'builder_content' => json_decode($structure, true),
        ]);
    }

    public function getPreviewUrl(): ?string
    {
        return route('posts.show', $this->record);
    }
}

Register the page inside your resource:

public static function getPages(): array
{
    return [
        'index'          => Pages\ListPosts::route('/'),
        'create'         => Pages\CreatePost::route('/create'),
        'edit'           => Pages\EditPost::route('/{record}/edit'),
        'edit-content'   => EditPostContent::route('/{record}/content'),
    ];
}

Authorization is inherited from the resource — the SDK does not bypass Primix's policy checks.

CLI scaffold

Generate a starter page class with:

php artisan make:primix-builder-page PostContent --context=page

The generated class extends the correct base, stubs the three required methods, and includes a $resource declaration if you pass --resource=PostResource. Fill in the persistence logic before using the page in production.

Core-first principle

The Primix SDK is purely a host-layer concern. Everything that determines what the builder renders, which modules are available, how PropTypes resolve to CSS, and what the saved JSON looks like is unchanged from the standalone integration:

  • Module registration: Tagixo::registerModule()
  • PropType definitions: same classes, same schema
  • PageRenderer::renderWithLayout(): same PHP render path
  • Builder payload JSON shape: identical contract

Switching from a standalone builder page to a Primix resource-bound page requires changing your page class's parent and moving persistence into loadStructure() / saveStructure(). Nothing else changes. Your templates, module registrations, and front-end render pipeline are unaffected.

App-target forms

The form builder has two module palettes:

  • Universal — renders to standard HTML via core Blade partials. Works in any host.
  • App — adds interactive layout wrappers (Tabs, Wizard, Group) that require the host panel's runtime renderer. Only meaningful when the panel renders forms natively.

The Primix SDK activates the app palette automatically in its service provider. You do not need to call this yourself:

// Already done inside TagixoPrimixPlugin — do not duplicate
Tagixo::enableAppForms();

If you build a custom SDK (not Primix, not Filament), activate it explicitly in your service provider's boot():

use Ccast\Tagixo\Facades\Tagixo;

public function boot(): void
{
    Tagixo::enableAppForms();

    Tagixo::registerAppFormPreviewer(function (int|string $formId): ?string {
        return route('my-sdk.form-preview', ['id' => $formId]);
    });
}

Without a registered previewer, the builder's Preview button falls back to the universal HTML preview even for app-target forms.

What to avoid

The following patterns create maintenance problems and make future SDK migrations harder:

  • Putting persistence logic directly in the panel provider rather than the page class
  • Duplicating PageRenderer calls inside Primix resources to re-render content
  • Storing only rendered HTML in your model and discarding the raw builder JSON
  • Overriding InteractsWithVisualBuilder methods in ways that skip lifecycle steps
  • Injecting builder-specific CSS into Primix component views (use PrimixFormStyles for form CSS, not inline styles in Blade)
  • Adding ?v= cache-busting query strings to type="module" script tags

See also

  • Installation — Composer setup, panel provider registration, asset publication step-by-step
  • Plugin registrationTagixoPrimixPlugin options and advanced configuration
  • Standalone builder page — Full reference for BuilderPage
  • Resource builder page — Full reference for PrimixVisualBuilderPage, authorization, preview URLs
  • Form context — Form builder in Primix: excludedCanvasPropTypes(), app-target forms, form preview
  • Form tablesPrimixFormColumns, PrimixFormFilters, PrimixFormStyles reference
  • Media gallerywithMediaGallery() setup and media picker integration
  • Advanced configuration — Layout frame pattern, context variants, builder toolbar customization
  • Troubleshooting — Asset caching, LiVue morph issues, style injection, common errors