diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php
index bc46559..5eeeb6b 100644
--- a/app/Models/Plugin.php
+++ b/app/Models/Plugin.php
@@ -60,8 +60,14 @@ class Plugin extends Model
});
static::updating(function ($model): void {
- // Reset image cache when markup changes
- if ($model->isDirty('render_markup')) {
+ // Reset image cache when any markup changes
+ if ($model->isDirty([
+ 'render_markup',
+ 'render_markup_half_horizontal',
+ 'render_markup_half_vertical',
+ 'render_markup_quadrant',
+ 'render_markup_shared',
+ ])) {
$model->current_image = null;
}
});
@@ -421,7 +427,9 @@ class Plugin extends Model
throw new InvalidArgumentException('Render method is only applicable for recipe plugins.');
}
- if ($this->render_markup) {
+ $markup = $this->getMarkupForSize($size);
+
+ if ($markup) {
$renderedContent = '';
if ($this->markup_language === 'liquid') {
@@ -471,7 +479,7 @@ class Plugin extends Model
// Check if external renderer should be used
if ($this->preferred_renderer === 'trmnl-liquid' && config('services.trmnl.liquid_enabled')) {
// Use external Ruby renderer - pass raw template without preprocessing
- $renderedContent = $this->renderWithExternalLiquidRenderer($this->render_markup, $context);
+ $renderedContent = $this->renderWithExternalLiquidRenderer($markup, $context);
} else {
// Use PHP keepsuit/liquid renderer
// Create a custom environment with inline templates support
@@ -493,14 +501,14 @@ class Plugin extends Model
$environment->tagRegistry->register(TemplateTag::class);
// Apply Liquid replacements (including 'with' syntax conversion)
- $processedMarkup = $this->applyLiquidReplacements($this->render_markup);
+ $processedMarkup = $this->applyLiquidReplacements($markup);
$template = $environment->parseString($processedMarkup);
$liquidContext = $environment->newRenderContext(data: $context);
$renderedContent = $template->render($liquidContext);
}
} else {
- $renderedContent = Blade::render($this->render_markup, [
+ $renderedContent = Blade::render($markup, [
'size' => $size,
'data' => $this->data_payload,
'config' => $this->configuration ?? [],
@@ -581,6 +589,30 @@ class Plugin extends Model
return $this->configuration[$key] ?? $default;
}
+ /**
+ * Get the appropriate markup for a given size, including shared prepending logic
+ *
+ * @param string $size The layout size (full, half_horizontal, half_vertical, quadrant)
+ * @return string|null The markup code for the given size, with shared prepended if available
+ */
+ public function getMarkupForSize(string $size): ?string
+ {
+ $markup = match ($size) {
+ 'full' => $this->render_markup,
+ 'half_horizontal' => $this->render_markup_half_horizontal ?? $this->render_markup,
+ 'half_vertical' => $this->render_markup_half_vertical ?? $this->render_markup,
+ 'quadrant' => $this->render_markup_quadrant ?? $this->render_markup,
+ default => $this->render_markup,
+ };
+
+ // Prepend shared markup if it exists
+ if ($markup && $this->render_markup_shared) {
+ $markup = $this->render_markup_shared."\n".$markup;
+ }
+
+ return $markup;
+ }
+
public function getPreviewMashupLayoutForSize(string $size): string
{
return match ($size) {
diff --git a/app/Services/PluginExportService.php b/app/Services/PluginExportService.php
index 241764d..be98461 100644
--- a/app/Services/PluginExportService.php
+++ b/app/Services/PluginExportService.php
@@ -51,17 +51,35 @@ class PluginExportService
$settings = $this->generateSettingsYaml($plugin);
$settingsYaml = Yaml::dump($settings, 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
File::put($tempDir.'/settings.yml', $settingsYaml);
- // Generate full template content
- $fullTemplate = $this->generateFullTemplate($plugin);
+
$extension = $plugin->markup_language === 'liquid' ? 'liquid' : 'blade.php';
- File::put($tempDir.'/full.'.$extension, $fullTemplate);
- // Generate shared.liquid if needed (for liquid templates)
- if ($plugin->markup_language === 'liquid') {
- $sharedTemplate = $this->generateSharedTemplate();
- /** @phpstan-ignore-next-line */
- if ($sharedTemplate) {
- File::put($tempDir.'/shared.liquid', $sharedTemplate);
- }
+
+ // Export full template if it exists
+ if ($plugin->render_markup) {
+ $fullTemplate = $this->generateLayoutTemplate($plugin->render_markup);
+ File::put($tempDir.'/full.'.$extension, $fullTemplate);
+ }
+
+ // Export layout-specific templates if they exist
+ if ($plugin->render_markup_half_horizontal) {
+ $halfHorizontalTemplate = $this->generateLayoutTemplate($plugin->render_markup_half_horizontal);
+ File::put($tempDir.'/half_horizontal.'.$extension, $halfHorizontalTemplate);
+ }
+
+ if ($plugin->render_markup_half_vertical) {
+ $halfVerticalTemplate = $this->generateLayoutTemplate($plugin->render_markup_half_vertical);
+ File::put($tempDir.'/half_vertical.'.$extension, $halfVerticalTemplate);
+ }
+
+ if ($plugin->render_markup_quadrant) {
+ $quadrantTemplate = $this->generateLayoutTemplate($plugin->render_markup_quadrant);
+ File::put($tempDir.'/quadrant.'.$extension, $quadrantTemplate);
+ }
+
+ // Export shared template if it exists
+ if ($plugin->render_markup_shared) {
+ $sharedTemplate = $this->generateLayoutTemplate($plugin->render_markup_shared);
+ File::put($tempDir.'/shared.'.$extension, $sharedTemplate);
}
// Create ZIP file
$zipPath = $tempDir.'/plugin_'.$plugin->trmnlp_id.'.zip';
@@ -124,29 +142,21 @@ class PluginExportService
}
/**
- * Generate the full template content
+ * Generate template content from markup, removing wrapper divs if present
*/
- private function generateFullTemplate(Plugin $plugin): string
+ private function generateLayoutTemplate(?string $markup): string
{
- $markup = $plugin->render_markup;
+ if (! $markup) {
+ return '';
+ }
- // Remove the wrapper div if it exists (it will be added during import)
+ // Remove the wrapper div if it exists (it will be added during import for liquid)
$markup = preg_replace('/^
\s*/', '', $markup);
$markup = preg_replace('/\s*<\/div>\s*$/', '', $markup);
return mb_trim($markup);
}
- /**
- * Generate the shared template content (for liquid templates)
- */
- private function generateSharedTemplate(): null
- {
- // For now, we don't have a way to store shared templates separately
- // TODO - add support for shared templates
- return null;
- }
-
/**
* Add a directory and its contents to a ZIP file
*/
diff --git a/app/Services/PluginImportService.php b/app/Services/PluginImportService.php
index 51a9aee..f3e7a5c 100644
--- a/app/Services/PluginImportService.php
+++ b/app/Services/PluginImportService.php
@@ -93,37 +93,59 @@ class PluginImportService
$settings = Yaml::parse($settingsYaml);
$this->validateYAML($settings);
- // Determine which template file to use and read its content
- $templatePath = null;
+ // Determine markup language from the first available file
$markupLanguage = 'blade';
+ $firstTemplatePath = $filePaths['fullLiquidPath']
+ ?? ($filePaths['halfHorizontalLiquidPath'] ?? null)
+ ?? ($filePaths['halfVerticalLiquidPath'] ?? null)
+ ?? ($filePaths['quadrantLiquidPath'] ?? null)
+ ?? ($filePaths['sharedLiquidPath'] ?? null)
+ ?? ($filePaths['sharedBladePath'] ?? null);
- if ($filePaths['fullLiquidPath']) {
- $templatePath = $filePaths['fullLiquidPath'];
- $fullLiquid = File::get($templatePath);
+ if ($firstTemplatePath && pathinfo((string) $firstTemplatePath, PATHINFO_EXTENSION) === 'liquid') {
+ $markupLanguage = 'liquid';
+ }
- // Prepend shared.liquid or shared.blade.php content if available
- if ($filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) {
- $sharedLiquid = File::get($filePaths['sharedLiquidPath']);
- $fullLiquid = $sharedLiquid."\n".$fullLiquid;
- } elseif ($filePaths['sharedBladePath'] && File::exists($filePaths['sharedBladePath'])) {
- $sharedBlade = File::get($filePaths['sharedBladePath']);
- $fullLiquid = $sharedBlade."\n".$fullLiquid;
- }
-
- // Check if the file ends with .liquid to set markup language
- if (pathinfo((string) $templatePath, PATHINFO_EXTENSION) === 'liquid') {
- $markupLanguage = 'liquid';
+ // Read full markup (don't prepend shared - it will be prepended at render time)
+ $fullLiquid = null;
+ if (isset($filePaths['fullLiquidPath']) && $filePaths['fullLiquidPath']) {
+ $fullLiquid = File::get($filePaths['fullLiquidPath']);
+ if ($markupLanguage === 'liquid') {
$fullLiquid = '
'."\n".$fullLiquid."\n".'
';
}
- } elseif ($filePaths['sharedLiquidPath']) {
- $templatePath = $filePaths['sharedLiquidPath'];
- $fullLiquid = File::get($templatePath);
- $markupLanguage = 'liquid';
- $fullLiquid = '
'."\n".$fullLiquid."\n".'
';
- } elseif ($filePaths['sharedBladePath']) {
- $templatePath = $filePaths['sharedBladePath'];
- $fullLiquid = File::get($templatePath);
- $markupLanguage = 'blade';
+ }
+
+ // Read shared markup separately
+ $sharedMarkup = null;
+ if (isset($filePaths['sharedLiquidPath']) && $filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) {
+ $sharedMarkup = File::get($filePaths['sharedLiquidPath']);
+ } elseif (isset($filePaths['sharedBladePath']) && $filePaths['sharedBladePath'] && File::exists($filePaths['sharedBladePath'])) {
+ $sharedMarkup = File::get($filePaths['sharedBladePath']);
+ }
+
+ // Read layout-specific markups
+ $halfHorizontalMarkup = null;
+ if (isset($filePaths['halfHorizontalLiquidPath']) && $filePaths['halfHorizontalLiquidPath'] && File::exists($filePaths['halfHorizontalLiquidPath'])) {
+ $halfHorizontalMarkup = File::get($filePaths['halfHorizontalLiquidPath']);
+ if ($markupLanguage === 'liquid') {
+ $halfHorizontalMarkup = '
'."\n".$halfHorizontalMarkup."\n".'
';
+ }
+ }
+
+ $halfVerticalMarkup = null;
+ if (isset($filePaths['halfVerticalLiquidPath']) && $filePaths['halfVerticalLiquidPath'] && File::exists($filePaths['halfVerticalLiquidPath'])) {
+ $halfVerticalMarkup = File::get($filePaths['halfVerticalLiquidPath']);
+ if ($markupLanguage === 'liquid') {
+ $halfVerticalMarkup = '
'."\n".$halfVerticalMarkup."\n".'
';
+ }
+ }
+
+ $quadrantMarkup = null;
+ if (isset($filePaths['quadrantLiquidPath']) && $filePaths['quadrantLiquidPath'] && File::exists($filePaths['quadrantLiquidPath'])) {
+ $quadrantMarkup = File::get($filePaths['quadrantLiquidPath']);
+ if ($markupLanguage === 'liquid') {
+ $quadrantMarkup = '
'."\n".$quadrantMarkup."\n".'
';
+ }
}
// Ensure custom_fields is properly formatted
@@ -160,6 +182,10 @@ class PluginImportService
'polling_body' => $settings['polling_body'] ?? null,
'markup_language' => $markupLanguage,
'render_markup' => $fullLiquid ?? null,
+ 'render_markup_half_horizontal' => $halfHorizontalMarkup,
+ 'render_markup_half_vertical' => $halfVerticalMarkup,
+ 'render_markup_quadrant' => $quadrantMarkup,
+ 'render_markup_shared' => $sharedMarkup,
'configuration_template' => $configurationTemplate,
'data_payload' => json_decode($settings['static_data'] ?? '{}', true),
]);
@@ -246,37 +272,59 @@ class PluginImportService
$settings = Yaml::parse($settingsYaml);
$this->validateYAML($settings);
- // Determine which template file to use and read its content
- $templatePath = null;
+ // Determine markup language from the first available file
$markupLanguage = 'blade';
+ $firstTemplatePath = $filePaths['fullLiquidPath']
+ ?? ($filePaths['halfHorizontalLiquidPath'] ?? null)
+ ?? ($filePaths['halfVerticalLiquidPath'] ?? null)
+ ?? ($filePaths['quadrantLiquidPath'] ?? null)
+ ?? ($filePaths['sharedLiquidPath'] ?? null)
+ ?? ($filePaths['sharedBladePath'] ?? null);
- if ($filePaths['fullLiquidPath']) {
- $templatePath = $filePaths['fullLiquidPath'];
- $fullLiquid = File::get($templatePath);
+ if ($firstTemplatePath && pathinfo((string) $firstTemplatePath, PATHINFO_EXTENSION) === 'liquid') {
+ $markupLanguage = 'liquid';
+ }
- // Prepend shared.liquid or shared.blade.php content if available
- if ($filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) {
- $sharedLiquid = File::get($filePaths['sharedLiquidPath']);
- $fullLiquid = $sharedLiquid."\n".$fullLiquid;
- } elseif ($filePaths['sharedBladePath'] && File::exists($filePaths['sharedBladePath'])) {
- $sharedBlade = File::get($filePaths['sharedBladePath']);
- $fullLiquid = $sharedBlade."\n".$fullLiquid;
- }
-
- // Check if the file ends with .liquid to set markup language
- if (pathinfo((string) $templatePath, PATHINFO_EXTENSION) === 'liquid') {
- $markupLanguage = 'liquid';
+ // Read full markup (don't prepend shared - it will be prepended at render time)
+ $fullLiquid = null;
+ if (isset($filePaths['fullLiquidPath']) && $filePaths['fullLiquidPath']) {
+ $fullLiquid = File::get($filePaths['fullLiquidPath']);
+ if ($markupLanguage === 'liquid') {
$fullLiquid = '
'."\n".$fullLiquid."\n".'
';
}
- } elseif ($filePaths['sharedLiquidPath']) {
- $templatePath = $filePaths['sharedLiquidPath'];
- $fullLiquid = File::get($templatePath);
- $markupLanguage = 'liquid';
- $fullLiquid = '
'."\n".$fullLiquid."\n".'
';
- } elseif ($filePaths['sharedBladePath']) {
- $templatePath = $filePaths['sharedBladePath'];
- $fullLiquid = File::get($templatePath);
- $markupLanguage = 'blade';
+ }
+
+ // Read shared markup separately
+ $sharedMarkup = null;
+ if (isset($filePaths['sharedLiquidPath']) && $filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) {
+ $sharedMarkup = File::get($filePaths['sharedLiquidPath']);
+ } elseif (isset($filePaths['sharedBladePath']) && $filePaths['sharedBladePath'] && File::exists($filePaths['sharedBladePath'])) {
+ $sharedMarkup = File::get($filePaths['sharedBladePath']);
+ }
+
+ // Read layout-specific markups
+ $halfHorizontalMarkup = null;
+ if (isset($filePaths['halfHorizontalLiquidPath']) && $filePaths['halfHorizontalLiquidPath'] && File::exists($filePaths['halfHorizontalLiquidPath'])) {
+ $halfHorizontalMarkup = File::get($filePaths['halfHorizontalLiquidPath']);
+ if ($markupLanguage === 'liquid') {
+ $halfHorizontalMarkup = '
'."\n".$halfHorizontalMarkup."\n".'
';
+ }
+ }
+
+ $halfVerticalMarkup = null;
+ if (isset($filePaths['halfVerticalLiquidPath']) && $filePaths['halfVerticalLiquidPath'] && File::exists($filePaths['halfVerticalLiquidPath'])) {
+ $halfVerticalMarkup = File::get($filePaths['halfVerticalLiquidPath']);
+ if ($markupLanguage === 'liquid') {
+ $halfVerticalMarkup = '
'."\n".$halfVerticalMarkup."\n".'
';
+ }
+ }
+
+ $quadrantMarkup = null;
+ if (isset($filePaths['quadrantLiquidPath']) && $filePaths['quadrantLiquidPath'] && File::exists($filePaths['quadrantLiquidPath'])) {
+ $quadrantMarkup = File::get($filePaths['quadrantLiquidPath']);
+ if ($markupLanguage === 'liquid') {
+ $quadrantMarkup = '
'."\n".$quadrantMarkup."\n".'
';
+ }
}
// Ensure custom_fields is properly formatted
@@ -322,6 +370,10 @@ class PluginImportService
'polling_body' => $settings['polling_body'] ?? null,
'markup_language' => $markupLanguage,
'render_markup' => $fullLiquid ?? null,
+ 'render_markup_half_horizontal' => $halfHorizontalMarkup,
+ 'render_markup_half_vertical' => $halfVerticalMarkup,
+ 'render_markup_quadrant' => $quadrantMarkup,
+ 'render_markup_shared' => $sharedMarkup,
'configuration_template' => $configurationTemplate,
'data_payload' => json_decode($settings['static_data'] ?? '{}', true),
'preferred_renderer' => $preferredRenderer,
@@ -357,6 +409,9 @@ class PluginImportService
$fullLiquidPath = null;
$sharedLiquidPath = null;
$sharedBladePath = null;
+ $halfHorizontalLiquidPath = null;
+ $halfVerticalLiquidPath = null;
+ $quadrantLiquidPath = null;
// If zipEntryPath is specified, look for files in that specific directory first
if ($zipEntryPath) {
@@ -377,6 +432,25 @@ class PluginImportService
} elseif (File::exists($targetDir.'/shared.blade.php')) {
$sharedBladePath = $targetDir.'/shared.blade.php';
}
+
+ // Check for layout-specific files
+ if (File::exists($targetDir.'/half_horizontal.liquid')) {
+ $halfHorizontalLiquidPath = $targetDir.'/half_horizontal.liquid';
+ } elseif (File::exists($targetDir.'/half_horizontal.blade.php')) {
+ $halfHorizontalLiquidPath = $targetDir.'/half_horizontal.blade.php';
+ }
+
+ if (File::exists($targetDir.'/half_vertical.liquid')) {
+ $halfVerticalLiquidPath = $targetDir.'/half_vertical.liquid';
+ } elseif (File::exists($targetDir.'/half_vertical.blade.php')) {
+ $halfVerticalLiquidPath = $targetDir.'/half_vertical.blade.php';
+ }
+
+ if (File::exists($targetDir.'/quadrant.liquid')) {
+ $quadrantLiquidPath = $targetDir.'/quadrant.liquid';
+ } elseif (File::exists($targetDir.'/quadrant.blade.php')) {
+ $quadrantLiquidPath = $targetDir.'/quadrant.blade.php';
+ }
}
// Check if files are in src subdirectory of target directory
@@ -394,6 +468,25 @@ class PluginImportService
} elseif (File::exists($targetDir.'/src/shared.blade.php')) {
$sharedBladePath = $targetDir.'/src/shared.blade.php';
}
+
+ // Check for layout-specific files in src
+ if (File::exists($targetDir.'/src/half_horizontal.liquid')) {
+ $halfHorizontalLiquidPath = $targetDir.'/src/half_horizontal.liquid';
+ } elseif (File::exists($targetDir.'/src/half_horizontal.blade.php')) {
+ $halfHorizontalLiquidPath = $targetDir.'/src/half_horizontal.blade.php';
+ }
+
+ if (File::exists($targetDir.'/src/half_vertical.liquid')) {
+ $halfVerticalLiquidPath = $targetDir.'/src/half_vertical.liquid';
+ } elseif (File::exists($targetDir.'/src/half_vertical.blade.php')) {
+ $halfVerticalLiquidPath = $targetDir.'/src/half_vertical.blade.php';
+ }
+
+ if (File::exists($targetDir.'/src/quadrant.liquid')) {
+ $quadrantLiquidPath = $targetDir.'/src/quadrant.liquid';
+ } elseif (File::exists($targetDir.'/src/quadrant.blade.php')) {
+ $quadrantLiquidPath = $targetDir.'/src/quadrant.blade.php';
+ }
}
// If we found the required files in the target directory, return them
@@ -425,6 +518,25 @@ class PluginImportService
} elseif (File::exists($tempDir.'/src/shared.blade.php')) {
$sharedBladePath = $tempDir.'/src/shared.blade.php';
}
+
+ // Check for layout-specific files
+ if (File::exists($tempDir.'/src/half_horizontal.liquid')) {
+ $halfHorizontalLiquidPath = $tempDir.'/src/half_horizontal.liquid';
+ } elseif (File::exists($tempDir.'/src/half_horizontal.blade.php')) {
+ $halfHorizontalLiquidPath = $tempDir.'/src/half_horizontal.blade.php';
+ }
+
+ if (File::exists($tempDir.'/src/half_vertical.liquid')) {
+ $halfVerticalLiquidPath = $tempDir.'/src/half_vertical.liquid';
+ } elseif (File::exists($tempDir.'/src/half_vertical.blade.php')) {
+ $halfVerticalLiquidPath = $tempDir.'/src/half_vertical.blade.php';
+ }
+
+ if (File::exists($tempDir.'/src/quadrant.liquid')) {
+ $quadrantLiquidPath = $tempDir.'/src/quadrant.liquid';
+ } elseif (File::exists($tempDir.'/src/quadrant.blade.php')) {
+ $quadrantLiquidPath = $tempDir.'/src/quadrant.blade.php';
+ }
} else {
// Search for the files in the extracted directory structure
$directories = new RecursiveDirectoryIterator($tempDir, RecursiveDirectoryIterator::SKIP_DOTS);
@@ -442,6 +554,12 @@ class PluginImportService
$sharedLiquidPath = $filepath;
} elseif ($filename === 'shared.blade.php') {
$sharedBladePath = $filepath;
+ } elseif ($filename === 'half_horizontal.liquid' || $filename === 'half_horizontal.blade.php') {
+ $halfHorizontalLiquidPath = $filepath;
+ } elseif ($filename === 'half_vertical.liquid' || $filename === 'half_vertical.blade.php') {
+ $halfVerticalLiquidPath = $filepath;
+ } elseif ($filename === 'quadrant.liquid' || $filename === 'quadrant.blade.php') {
+ $quadrantLiquidPath = $filepath;
}
}
@@ -485,6 +603,25 @@ class PluginImportService
$sharedBladePath = $newSrcDir.'/shared.blade.php';
}
+ // Copy layout-specific files if they exist
+ if ($halfHorizontalLiquidPath) {
+ $extension = pathinfo((string) $halfHorizontalLiquidPath, PATHINFO_EXTENSION);
+ File::copy($halfHorizontalLiquidPath, $newSrcDir.'/half_horizontal.'.$extension);
+ $halfHorizontalLiquidPath = $newSrcDir.'/half_horizontal.'.$extension;
+ }
+
+ if ($halfVerticalLiquidPath) {
+ $extension = pathinfo((string) $halfVerticalLiquidPath, PATHINFO_EXTENSION);
+ File::copy($halfVerticalLiquidPath, $newSrcDir.'/half_vertical.'.$extension);
+ $halfVerticalLiquidPath = $newSrcDir.'/half_vertical.'.$extension;
+ }
+
+ if ($quadrantLiquidPath) {
+ $extension = pathinfo((string) $quadrantLiquidPath, PATHINFO_EXTENSION);
+ File::copy($quadrantLiquidPath, $newSrcDir.'/quadrant.'.$extension);
+ $quadrantLiquidPath = $newSrcDir.'/quadrant.'.$extension;
+ }
+
// Update the paths
$settingsYamlPath = $newSrcDir.'/settings.yml';
}
@@ -496,6 +633,9 @@ class PluginImportService
'fullLiquidPath' => $fullLiquidPath,
'sharedLiquidPath' => $sharedLiquidPath,
'sharedBladePath' => $sharedBladePath,
+ 'halfHorizontalLiquidPath' => $halfHorizontalLiquidPath,
+ 'halfVerticalLiquidPath' => $halfVerticalLiquidPath,
+ 'quadrantLiquidPath' => $quadrantLiquidPath,
];
}
diff --git a/database/migrations/2026_01_28_143142_add_layout_markup_columns_to_plugins_table.php b/database/migrations/2026_01_28_143142_add_layout_markup_columns_to_plugins_table.php
new file mode 100644
index 0000000..e56751c
--- /dev/null
+++ b/database/migrations/2026_01_28_143142_add_layout_markup_columns_to_plugins_table.php
@@ -0,0 +1,38 @@
+text('render_markup_half_horizontal')->nullable()->after('render_markup');
+ $table->text('render_markup_half_vertical')->nullable()->after('render_markup_half_horizontal');
+ $table->text('render_markup_quadrant')->nullable()->after('render_markup_half_vertical');
+ $table->text('render_markup_shared')->nullable()->after('render_markup_quadrant');
+ $table->text('transform_code')->nullable()->after('render_markup_shared');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('plugins', function (Blueprint $table) {
+ $table->dropColumn([
+ 'render_markup_half_horizontal',
+ 'render_markup_half_vertical',
+ 'render_markup_quadrant',
+ 'render_markup_shared',
+ 'transform_code',
+ ]);
+ });
+ }
+};
diff --git a/resources/views/livewire/plugins/index.blade.php b/resources/views/livewire/plugins/index.blade.php
index 90f8aa0..848fc67 100644
--- a/resources/views/livewire/plugins/index.blade.php
+++ b/resources/views/livewire/plugins/index.blade.php
@@ -258,7 +258,6 @@ new class extends Component
Limitations
- - Only full view will be imported; shared markup will be prepended
- Some Liquid filters may be not supported or behave differently
- API responses in formats other than JSON are not yet supported
{{-- --}}
@@ -312,7 +311,6 @@ new class extends Component
Limitations
- - Only full view will be imported; shared markup will be prepended
- Requires trmnl-liquid-cli executable.
- API responses in formats other than JSON are not yet fully supported.
- There are limitations in payload size (Data Payload, Template).
diff --git a/resources/views/livewire/plugins/recipe.blade.php b/resources/views/livewire/plugins/recipe.blade.php
index cda019e..a7b3918 100644
--- a/resources/views/livewire/plugins/recipe.blade.php
+++ b/resources/views/livewire/plugins/recipe.blade.php
@@ -65,6 +65,12 @@ new class extends Component
public string $preview_size = 'full';
+ public array $markup_layouts = [];
+
+ public array $active_tabs = [];
+
+ public string $active_tab = 'full';
+
public function mount(): void
{
abort_unless(auth()->user()->plugins->contains($this->plugin), 403);
@@ -91,7 +97,24 @@ new class extends Component
$this->view_content = null;
}
} else {
- $this->markup_code = $this->plugin->render_markup;
+ // Initialize layout markups from plugin columns
+ $this->markup_layouts = [
+ 'full' => $this->plugin->render_markup ?? '',
+ 'half_horizontal' => $this->plugin->render_markup_half_horizontal ?? '',
+ 'half_vertical' => $this->plugin->render_markup_half_vertical ?? '',
+ 'quadrant' => $this->plugin->render_markup_quadrant ?? '',
+ 'shared' => $this->plugin->render_markup_shared ?? '',
+ ];
+
+ // Set active tabs based on which layouts have content
+ $this->active_tabs = ['full']; // Full is always active
+ foreach (['half_horizontal', 'half_vertical', 'quadrant', 'shared'] as $layout) {
+ if (! empty($this->markup_layouts[$layout])) {
+ $this->active_tabs[] = $layout;
+ }
+ }
+
+ $this->markup_code = $this->markup_layouts['full'];
$this->markup_language = $this->plugin->markup_language ?? 'blade';
}
@@ -125,12 +148,108 @@ new class extends Component
{
abort_unless(auth()->user()->plugins->contains($this->plugin), 403);
$this->validate();
+
+ // Update markup_code for the active tab
+ if (isset($this->markup_layouts[$this->active_tab])) {
+ $this->markup_layouts[$this->active_tab] = $this->markup_code ?? '';
+ }
+
+ // Save all layout markups to respective columns
$this->plugin->update([
- 'render_markup' => $this->markup_code ?? null,
+ 'render_markup' => $this->markup_layouts['full'] ?? null,
+ 'render_markup_half_horizontal' => ! empty($this->markup_layouts['half_horizontal']) ? $this->markup_layouts['half_horizontal'] : null,
+ 'render_markup_half_vertical' => ! empty($this->markup_layouts['half_vertical']) ? $this->markup_layouts['half_vertical'] : null,
+ 'render_markup_quadrant' => ! empty($this->markup_layouts['quadrant']) ? $this->markup_layouts['quadrant'] : null,
+ 'render_markup_shared' => ! empty($this->markup_layouts['shared']) ? $this->markup_layouts['shared'] : null,
'markup_language' => $this->markup_language ?? null,
]);
}
+ public function addLayoutTab(string $layout): void
+ {
+ if (! in_array($layout, $this->active_tabs, true)) {
+ $this->active_tabs[] = $layout;
+ if (! isset($this->markup_layouts[$layout])) {
+ $this->markup_layouts[$layout] = '';
+ }
+ $this->switchTab($layout);
+ }
+ }
+
+ public function removeLayoutTab(string $layout): void
+ {
+ if ($layout !== 'full') {
+ $this->active_tabs = array_values(array_filter($this->active_tabs, fn ($tab) => $tab !== $layout));
+ if (isset($this->markup_layouts[$layout])) {
+ $this->markup_layouts[$layout] = '';
+ }
+ if ($this->active_tab === $layout) {
+ $this->active_tab = 'full';
+ $this->markup_code = $this->markup_layouts['full'] ?? '';
+ }
+ }
+ }
+
+ public function switchTab(string $layout): void
+ {
+ if (in_array($layout, $this->active_tabs, true)) {
+ // Save current tab's content before switching
+ if (isset($this->markup_layouts[$this->active_tab])) {
+ $this->markup_layouts[$this->active_tab] = $this->markup_code ?? '';
+ }
+
+ $this->active_tab = $layout;
+ $this->markup_code = $this->markup_layouts[$layout] ?? '';
+ }
+ }
+
+ public function toggleLayoutTab(string $layout): void
+ {
+ if ($layout === 'full') {
+ return;
+ }
+
+ if (in_array($layout, $this->active_tabs, true)) {
+ $this->removeLayoutTab($layout);
+ } else {
+ $this->addLayoutTab($layout);
+ }
+ }
+
+ public function getAvailableLayouts(): array
+ {
+ return [
+ 'half_horizontal' => 'Half Horizontal',
+ 'half_vertical' => 'Half Vertical',
+ 'quadrant' => 'Quadrant',
+ 'shared' => 'Shared',
+ ];
+ }
+
+ public function getLayoutLabel(string $layout): string
+ {
+ return match ($layout) {
+ 'full' => $this->getFullTabLabel(),
+ 'half_horizontal' => 'Half Horizontal',
+ 'half_vertical' => 'Half Vertical',
+ 'quadrant' => 'Quadrant',
+ 'shared' => 'Shared',
+ default => ucfirst($layout),
+ };
+ }
+
+ public function getFullTabLabel(): string
+ {
+ // Return "Full" if any layout-specific markup exists, otherwise "Responsive"
+ if (! empty($this->markup_layouts['half_horizontal'])
+ || ! empty($this->markup_layouts['half_vertical'])
+ || ! empty($this->markup_layouts['quadrant'])) {
+ return 'Full';
+ }
+
+ return 'Responsive';
+ }
+
protected array $rules = [
'name' => 'required|string|max:255',
'data_stale_minutes' => 'required|integer|min:1',
@@ -1018,41 +1137,75 @@ HTML;
@if(!$plugin->render_markup_view)