tipowerup/ti-theme-toolkit
Shared PHP and frontend infrastructure for TastyIgniter themes: extend one service provider to inherit color palettes, form widgets, Livewire auth/contact/newsletter components, dark mode, and a curated field schema.
Requirements first — this targets TastyIgniter 4.0+, PHP 8.2+, Livewire 3, Node 18+, Vite 5, Alpine 3, and (since 0.3.0) Tailwind v4. If you're on Tailwind v3, stop and read the 0.3.0 changelog before touching anything; the old preset layer is gone.
Install both halves — the PHP side and the frontend tooling:
composer require tipowerup/ti-theme-toolkit
npm install --save-dev @tipowerup/ti-theme-toolkit
Pin an exact version. The README's own example still shows "0.1.0" while releases are at 0.6.x. This is pre-1.0 and minors have shipped breaking changes (Tailwind v4 rewrite, tokens.css → theme.css, tailwind-preset.js deleted). Use "tipowerup/ti-theme-toolkit": "0.6.2", not ^0.6.
Where to look first: the docs/ directory, not the README. The README Quickstart predates 0.3.0 in several places. Start with docs/getting-started.md, then docs/service-provider.md and docs/color-system.md.
First use case — scaffold a theme:
src/ServiceProvider.php extending AbstractThemeServiceProvider. Override themeCode(), viewsPath(), langPath(), livewirePath(), bladeComponentsPath(). You can skip phpNamespace() — since 0.3.0 it's derived via reflection of the concrete child provider. Only override it if your classes sit outside the SP's namespace.fields.php with BaseSchema::merge() — the 7-tab structure (general, banners, colors, dark mode, social, advanced, GDPR) is already provided; merge only your deltas.toolkitPreset({ input: [...] }).@tipowerup/ti-theme-toolkit/js/dark-mode in your entry JS.npm install && npm run build, then php artisan igniter:theme-vendor-publish --force.If you get a working theme booting with colors and dark mode in an afternoon, you've hit the package's sweet spot.
Let the provider do the wiring. AbstractThemeServiceProvider handles views, translations, Livewire, Blade components, routing, view composers, error views, and asset publishing. Resist adding your own boot() registration for these — you'll double-register or fight the ordering.
Color pipeline as a single source of truth. The flow is: admin saves colors → ThemePayloadResolver::resolve() reads themes.data → ColorHelper::derivePrimaryPalette() derives shades → buildBrandStyle() emits --color-* custom properties on the <html style> attribute → Blade uses bg-primary, text-primary-400, dark:bg-primary-900. The Tailwind preset maps primary to rgb(var(--color-primary) / <alpha-value>) so opacity modifiers keep working. Never hardcode hex in Blade — always go through the CSS variables, or admin color changes silently do nothing.
Compose fields.php additively. The pattern in the README is right:
return [
'form' => BaseSchema::merge(
['tabs' => BaseSchema::tabs()],
['tabs' => ['colors' => ['fields' => [
'color[primary]' => ['default' => '#0f172a'],
]]]],
)['tabs'],
];
Add fields by key; don't re-declare whole tabs or you'll lose the defaults BaseSchema gives you.
Override shared Livewire components by mirroring the path. The auto-loader scans the toolkit's src/Livewire/ first, then yours, under the active theme's view namespace — and registration is last-writer-wins. Drop a class at the same relative path (Livewire/Login.php) and yours replaces Login, Register, ResetPassword, Socialite, Contact, or NewsletterSubscribeForm. Keep the toolkit's componentMeta() contract intact, or use the placeholder form ('{ns}::login', '{lang}::default.component_login_title') and let resolveComponentMeta() substitute them.
Dark mode via the Alpine store. Import the JS, then in Alpine components use the store (persisted to localStorage, toggles html.dark). If you need to react outside Alpine, listen for the darkmode:changed window event. TypeScript consumers get full intellisense from the shipped .d.ts files — no build step, and it works under strict: true.
Ship error views in resources/views/errors/. Since 0.5.0 they're auto-wired onto config('view.paths'), and a composer injects themeBrandStyle / themeNeutralStyle into errors.layout (the wildcard composer can't reach these because the exception handler runs outside any TI controller). Just create 404.blade.php / 500.blade.php and it works.
Trust activation-time publishing. 0.4.0 added a main.theme.activated listener that runs igniter:theme-vendor-publish --theme=<code> --force in-process. Themes installed after igniter:install no longer ship with missing favicons.
Write tests with Pest 4 + testbench. The toolkit's own suite patterns are worth copying — especially for Livewire component registration and view-namespace binding (container key tipowerup.theme.viewNamespace is bound at SP boot and is what toolkit Livewire components fall back to when controller() is null).
The README is stale — the changelog is authoritative. Specifically: tailwind-preset.js is removed, resources/src/css/tokens.css is superseded by theme.css, and phpNamespace() is now optional. If you copy the README verbatim you'll get a Tailwind v3-era setup that doesn't fit the current release. Read changelog/0.3.0.md carefully.
Multiple toolkit themes + error views = conflict. Before 0.6.2, registerStorefrontErrorViews() unconditionally unshifted the theme's resources/views onto the global view.paths, so whichever SP booted last won unnamespaced errors.404/errors.500. It's now gated on ThemeManager::getActiveThemeCode() === themeCode(). If you're on <0.6.2 with two themes installed, upgrade.
Livewire override failures are silent. Last-writer-wins by relative path means a typo'd class name or mismatched componentMeta() gives you the toolkit's component and no error. If an override "isn't taking," dd() the registration and confirm the namespace scan order.
Auto-publish is skipped under runningUnitTests(). If your testbench suite asserts on published assets, publish explicitly. Also note the listener wraps the artisan call in try/catch and only logs a warning on failure — a broken publish won't fail activation, so check the log if assets go missing.
Neutral colors can't ride the <html> style attribute. Since 0.3.0 they're emitted as a light-mode-scoped <style> block via themeNeutralStyle. If neutral tokens look wrong in dark mode, that's the seam to inspect — the brand style and neutral style are delivered through different channels.
Tailwind v4 alpha modifiers need the rgb() wrapper. The preset maps primary to rgb(var(--color-primary) / <alpha-value>). A bare var(--color-primary) in your own utilities will break bg-primary/50.
The static-page menu resolver is a temporary upstream workaround. registerStaticPageResolverPatch() corrects pages.menuitem.resolveItem for tastyigniter/ti-ext-pages#21. If your theme has its own pages menu fix, they'll both register — check for duplicate listeners when the upstream PR lands.
Error views are storefront-only. Gated on ! Igniter::runningInAdmin(), so admin-area exceptions keep the admin UI. Don't expect your errors/500.blade.php to render during an admin 500 — that's intentional and it will confuse you during debugging.
Extension points worth knowing: the tipowerup.theme.viewNamespace container binding for test harnesses, the *Form.php classes (Forms\LoginForm, Forms\RegisterForm) if you need to adjust validation, and BannerManager as a form widget if you're building custom banner UIs rather than using the stock slides tab.
Debugging tip: when a color doesn't apply, check the rendered <html style="..."> attribute first — if the variable's there, the problem is Tailwind content scanning or a malformed rgb() wrapper; if it's absent, the problem is ThemePayloadResolver reading themes.data (usually a stale customizer save).
How can I help you explore Laravel packages today?