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, notFilament\. The method signatures are often identical, but the namespaces differ.
Gotcha:
<style>tags injected into LiVue component DOM are stripped on every morph cycle. ThePrimixFormStyleshelper 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
--forcewhen re-publishing after a Tagixo upgrade. Without it,vendor:publishskips files that already exist, leaving stale JavaScript in place.
Module script query busting trap: do not append
?v=cache-busting parameters totype="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 variantloadStructure()contract — abstract hook you implement to read builder JSON from your modelsaveStructure()contract — abstract hook you implement to persist builder JSONgetStructure()/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
PageRenderercalls inside Primix resources to re-render content - Storing only rendered HTML in your model and discarding the raw builder JSON
- Overriding
InteractsWithVisualBuildermethods in ways that skip lifecycle steps - Injecting builder-specific CSS into Primix component views (use
PrimixFormStylesfor form CSS, not inline styles in Blade) - Adding
?v=cache-busting query strings totype="module"script tags
See also
- Installation — Composer setup, panel provider registration, asset publication step-by-step
- Plugin registration —
TagixoPrimixPluginoptions 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 tables —
PrimixFormColumns,PrimixFormFilters,PrimixFormStylesreference - Media gallery —
withMediaGallery()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