refactor(ai-templates): shared ContentStylePicker used by wizard + automation node

This commit is contained in:
Paulo Castellano 2026-06-17 20:11:09 -03:00
parent 5df03f775a
commit f3965b22f1
3 changed files with 110 additions and 54 deletions

View file

@ -0,0 +1,89 @@
<script setup lang="ts">
import { IconCheck } from '@tabler/icons-vue';
interface StyleOption {
key: string;
preview: string;
name: string;
description?: string;
}
const props = withDefaults(defineProps<{
modelValue: string;
styles: StyleOption[];
compact?: boolean;
}>(), {
compact: false,
});
const emit = defineEmits<{
'update:modelValue': [string];
}>();
const select = (key: string) => {
emit('update:modelValue', key);
};
</script>
<template>
<div
class="grid gap-2"
:class="compact ? 'grid-cols-3' : 'gap-3 sm:grid-cols-3'"
>
<button
v-for="style in props.styles"
:key="style.key"
type="button"
:class="[
compact
? 'flex flex-col items-center gap-1.5 rounded-lg border p-2 text-left transition-colors'
: 'relative flex cursor-pointer flex-col overflow-hidden rounded-xl border-2 border-foreground bg-card text-left shadow-2xs transition-all hover:bg-foreground/5',
compact
? (modelValue === style.key
? 'border-violet-500 bg-violet-50 dark:bg-violet-950/30'
: 'border-border hover:border-violet-300 hover:bg-muted/50')
: (modelValue === style.key
? '!bg-violet-100 shadow-md'
: ''),
]"
@click="select(style.key)"
>
<!-- Compact variant: simple thumbnail + name + active text color -->
<template v-if="compact">
<img
:src="style.preview"
:alt="style.name"
class="w-full rounded object-cover"
/>
<span
class="text-center text-[10px] font-medium leading-tight"
:class="modelValue === style.key ? 'text-violet-700 dark:text-violet-300' : 'text-foreground/70'"
>
{{ style.name }}
</span>
</template>
<!-- Full variant: aspect-video thumbnail + name + description + check icon -->
<template v-else>
<div class="aspect-video w-full overflow-hidden bg-muted">
<img
:src="style.preview"
:alt="style.name"
class="size-full object-cover"
/>
</div>
<div class="flex items-start gap-2 p-3">
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-bold text-foreground">{{ style.name }}</p>
<p v-if="style.description" class="mt-0.5 text-xs leading-snug text-foreground/60">{{ style.description }}</p>
</div>
<IconCheck
v-if="modelValue === style.key"
class="mt-0.5 size-4 shrink-0 text-foreground"
stroke-width="3"
/>
</div>
</template>
</button>
</div>
</template>

View file

@ -1,7 +1,9 @@
<script setup lang="ts">
import { usePage } from '@inertiajs/vue3';
import { trans } from 'laravel-vue-i18n';
import { computed, ref, watch } from 'vue';
import ContentStylePicker from '@/components/ai/ContentStylePicker.vue';
import ChannelConfigurator from '@/components/ChannelConfigurator.vue';
import CodeEditor from '@/components/CodeEditor.vue';
import InputError from '@/components/InputError.vue';
@ -51,11 +53,22 @@ interface GenerateConfig {
style: string;
}
const contentStyles = [
{ key: 'image_card', preview: '/images/ai-templates/image-card.png' },
{ key: 'tweet_card', preview: '/images/ai-templates/tweet-card.png' },
{ key: 'tweet_card_image', preview: '/images/ai-templates/tweet-card-image.png' },
];
const CONTENT_STYLE_KEYS = ['image_card', 'tweet_card', 'tweet_card_image'] as const;
const CONTENT_STYLE_PREVIEWS: Record<string, string> = {
image_card: '/images/ai-templates/image-card.png',
tweet_card: '/images/ai-templates/tweet-card.png',
tweet_card_image: '/images/ai-templates/tweet-card-image.png',
};
const contentStyles = computed(() =>
CONTENT_STYLE_KEYS.map((key) => ({
key,
preview: CONTENT_STYLE_PREVIEWS[key],
name: trans(`posts.ai.templates.${key}.name`),
description: trans(`posts.ai.templates.${key}.description`),
})),
);
const props = defineProps<{
data: Record<string, unknown>;
@ -292,30 +305,7 @@ const channels = computed<Channel[]>(() =>
<div class="space-y-2">
<Label class="text-sm font-bold">{{ $t('automations.config.generate.style') }}</Label>
<div class="grid grid-cols-3 gap-2">
<button
v-for="s in contentStyles"
:key="s.key"
type="button"
class="flex flex-col items-center gap-1.5 rounded-lg border p-2 text-left transition-colors"
:class="local.style === s.key
? 'border-violet-500 bg-violet-50 dark:bg-violet-950/30'
: 'border-border hover:border-violet-300 hover:bg-muted/50'"
@click="local.style = s.key"
>
<img
:src="s.preview"
:alt="$t('posts.ai.templates.' + s.key + '.name')"
class="w-full rounded object-cover"
/>
<span
class="text-center text-[10px] font-medium leading-tight"
:class="local.style === s.key ? 'text-violet-700 dark:text-violet-300' : 'text-foreground/70'"
>
{{ $t('posts.ai.templates.' + s.key + '.name') }}
</span>
</button>
</div>
<ContentStylePicker v-model="local.style" :styles="contentStyles" compact />
</div>
<div v-if="imageCountCap >= 1" class="space-y-2">

View file

@ -10,6 +10,7 @@ import { toast } from 'vue-sonner';
import { start as startRoute } from '@/actions/App/Http/Controllers/App/PostAiCreateController';
import ContentStylePicker from '@/components/ai/ContentStylePicker.vue';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
@ -272,31 +273,7 @@ const startGeneration = async () => {
<!-- Visual style — shown only for single-image formats (not carousel) -->
<div v-if="selectedFormat && !formatBoundTemplate" class="space-y-2">
<Label class="text-sm font-bold">{{ $t('posts.create.steps.template_picker_title') }}</Label>
<div class="grid gap-3 sm:grid-cols-3">
<button
v-for="template in styleTemplates"
:key="template.key"
type="button"
class="relative flex cursor-pointer flex-col overflow-hidden rounded-xl border-2 border-foreground bg-card text-left shadow-2xs transition-all hover:bg-foreground/5"
:class="{ '!bg-violet-100 shadow-md': selectedStyle === template.key }"
@click="selectedStyle = template.key"
>
<div class="aspect-video w-full overflow-hidden bg-muted">
<img
:src="template.preview"
:alt="template.name"
class="size-full object-cover"
/>
</div>
<div class="flex items-start gap-2 p-3">
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-bold text-foreground">{{ template.name }}</p>
<p class="mt-0.5 text-xs leading-snug text-foreground/60">{{ template.description }}</p>
</div>
<IconCheck v-if="selectedStyle === template.key" class="mt-0.5 size-4 shrink-0 text-foreground" stroke-width="3" />
</div>
</button>
</div>
<ContentStylePicker v-model="selectedStyle" :styles="styleTemplates" />
</div>
<!-- Account (when template needs_account OR there's a choice to make) -->