feat: useLayoutWrapper

This commit is contained in:
2026-02-11 09:24:05 -05:00
parent 71a48de945
commit 3ec98fdc2d
17 changed files with 259 additions and 162 deletions

View File

@@ -96,7 +96,11 @@
"min": "",
"max": "",
"acfe_flexible_modal_edit_size": "",
"acfe_flexible_settings": ["group_layout_contained"],
"acfe_flexible_settings": [
"group_layout_colored",
"group_layout_contained",
"group_layout_padded"
],
"acfe_flexible_settings_size": "large",
"acfe_flexible_render_template": false,
"acfe_flexible_render_style": false,
@@ -190,7 +194,7 @@
"min": "",
"max": "",
"acfe_flexible_modal_edit_size": "",
"acfe_flexible_settings": "",
"acfe_flexible_settings": ["group_layout_colored", "group_layout_padded"],
"acfe_flexible_settings_size": "large",
"acfe_flexible_render_template": false,
"acfe_flexible_render_style": false,
@@ -236,5 +240,5 @@
"graphql_types": "",
"acfe_meta": "",
"acfe_note": "",
"modified": 1769779666
"modified": 1770740697
}

View File

@@ -0,0 +1,64 @@
{
"key": "group_layout_colored",
"title": "Layout - Colored",
"fields": [
{
"key": "field_693c8c945ce51",
"label": "Variante de couleur",
"name": "color",
"aria-label": "",
"type": "button_group",
"instructions": "",
"required": 1,
"conditional_logic": 0,
"wrapper": {
"width": "",
"class": "",
"id": ""
},
"choices": {
"default": "Par défaut",
"muted": "Atténué",
"elevated": "Surélevé",
"accented": "Accentué",
"inverted": "Inversé",
"primary": "Couleur principale"
},
"default_value": "default",
"return_format": "value",
"allow_null": 0,
"allow_in_bindings": 0,
"layout": "horizontal",
"show_in_graphql": 1,
"graphql_description": "",
"graphql_field_name": "color",
"graphql_non_null": 1
}
],
"location": [
[
{
"param": "abstract"
}
]
],
"menu_order": 0,
"position": "normal",
"style": "seamless",
"label_placement": "top",
"instruction_placement": "label",
"hide_on_screen": "",
"active": true,
"description": "",
"show_in_rest": 0,
"display_title": "",
"acfe_autosync": ["json"],
"acfe_form": 0,
"show_in_graphql": 1,
"graphql_field_name": "GroupLayoutColored",
"map_graphql_types_from_location_rules": 0,
"graphql_types": "",
"acfe_meta": "",
"acfe_note": "",
"modified": 1770132035
}

View File

@@ -4,10 +4,10 @@
"fields": [
{
"key": "field_68dc29d78941c",
"label": "Conteneur",
"label": "Largeur du contenu",
"name": "container",
"aria-label": "",
"type": "select",
"type": "button_group",
"instructions": "",
"required": 1,
"conditional_logic": 0,
@@ -21,101 +21,17 @@
"xl": "1280px",
"lg": "1024px",
"fluid": "Largeur fluide",
"none": "Pleine largeur"
"fullbleed": "Pleine largeur"
},
"default_value": "default",
"return_format": "value",
"multiple": 0,
"max": "",
"prepend": "",
"append": "",
"required_message": "",
"allow_null": 0,
"allow_in_bindings": 0,
"ui": 0,
"layout": "horizontal",
"show_in_graphql": 1,
"graphql_description": "",
"graphql_field_name": "container",
"graphql_non_null": 1,
"ajax": 0,
"placeholder": "",
"create_options": 0,
"save_options": 0,
"allow_custom": 0,
"search_placeholder": "",
"min": ""
},
{
"key": "field_693c8c3b5ce50",
"label": "Espacement vertical",
"name": "vertical_padding",
"aria-label": "",
"type": "select",
"instructions": "",
"required": 1,
"conditional_logic": 0,
"wrapper": {
"width": "",
"class": "",
"id": ""
},
"choices": {
"sm": "Petit (12px)",
"md": "Medium (24px)",
"lg": "Grand (48px)"
},
"default_value": "md",
"return_format": "value",
"multiple": 0,
"allow_null": 0,
"allow_in_bindings": 0,
"ui": 0,
"show_in_graphql": 1,
"graphql_description": "",
"graphql_field_name": "verticalPadding",
"graphql_non_null": 1,
"ajax": 0,
"placeholder": "",
"create_options": 0,
"save_options": 0,
"allow_custom": 0,
"search_placeholder": ""
},
{
"key": "field_693c8c945ce51",
"label": "Couleur d'arrière-plan",
"name": "bg_color",
"aria-label": "",
"type": "select",
"instructions": "",
"required": 1,
"conditional_logic": 0,
"wrapper": {
"width": "",
"class": "",
"id": ""
},
"choices": {
"default": "Par défaut",
"muted": "Atténué",
"inverted": "Inversé"
},
"default_value": "default",
"return_format": "value",
"multiple": 0,
"allow_null": 0,
"allow_in_bindings": 0,
"ui": 0,
"show_in_graphql": 1,
"graphql_description": "",
"graphql_field_name": "bgColor",
"graphql_non_null": 1,
"ajax": 0,
"placeholder": "",
"create_options": 0,
"save_options": 0,
"allow_custom": 0,
"search_placeholder": ""
"graphql_non_null": 1
}
],
"location": [
@@ -143,5 +59,5 @@
"graphql_types": "",
"acfe_meta": "",
"acfe_note": "",
"modified": 1768358794
"modified": 1770740626
}

View File

@@ -0,0 +1,62 @@
{
"key": "group_layout_padded",
"title": "Layout - Padded",
"fields": [
{
"key": "field_693c8c3b5ce50",
"label": "Espacement intérieur vertical",
"name": "vertical_padding",
"aria-label": "",
"type": "button_group",
"instructions": "",
"required": 1,
"conditional_logic": 0,
"wrapper": {
"width": "",
"class": "",
"id": ""
},
"choices": {
"none": "Aucun",
"sm": "Petit (12px)",
"md": "Medium (24px)",
"lg": "Grand (48px)"
},
"default_value": "md",
"return_format": "value",
"allow_null": 0,
"allow_in_bindings": 0,
"layout": "horizontal",
"show_in_graphql": 1,
"graphql_description": "",
"graphql_field_name": "verticalPadding",
"graphql_non_null": 1
}
],
"location": [
[
{
"param": "abstract"
}
]
],
"menu_order": 0,
"position": "normal",
"style": "default",
"label_placement": "left",
"instruction_placement": "label",
"hide_on_screen": "",
"active": true,
"description": "",
"show_in_rest": 0,
"display_title": "",
"acfe_autosync": ["json"],
"acfe_form": 0,
"show_in_graphql": 1,
"graphql_field_name": "GroupLayoutPadded",
"map_graphql_types_from_location_rules": 0,
"graphql_types": "",
"acfe_meta": "",
"acfe_note": "",
"modified": 1770740636
}

View File

@@ -0,0 +1,3 @@
fragment LayoutColored on GroupLayoutColored_Fields {
color
}

View File

@@ -1,5 +1,3 @@
fragment LayoutContained on GroupLayoutContained_Fields {
container
verticalPadding
bgColor
}

View File

@@ -1,50 +0,0 @@
<script setup lang="ts">
import type { LayoutContainedFragment } from "#graphql/operations";
import { tv, type VariantProps } from "tailwind-variants";
const props = defineProps<LayoutContainedFragment>();
const layoutWrapperVariants = tv({
slots: {
base: "",
inner: "",
},
variants: {
container: {
default: { inner: "container" },
lg: { inner: "container-lg" },
xl: { inner: "container-xl" },
fluid: { inner: "container-fluid" },
none: { inner: "container-none" },
},
verticalPadding: {
sm: { base: "py-3" },
md: { base: "py-6" },
lg: { base: "py-12" },
},
bgColor: {
default: { base: "bg-default" },
muted: { base: "bg-muted" },
inverted: { base: "bg-inverted text-inverted" },
},
},
defaultVariants: {
container: "default",
verticalPadding: "md",
bgColor: "default",
},
});
const { base, inner } = layoutWrapperVariants({
container: props.container[0],
verticalPadding: props.verticalPadding[0],
bgColor: props.bgColor[0],
} as VariantProps<typeof layoutWrapperVariants>);
</script>
<template>
<section :class="base()">
<div :class="inner()">
<slot />
</div>
</section>
</template>

View File

@@ -0,0 +1,3 @@
fragment LayoutPadded on GroupLayoutPadded_Fields {
verticalPadding
}

View File

@@ -0,0 +1,12 @@
<script setup lang="ts">
const props = defineProps<{ layoutSettings?: LayoutSettings }>();
const { base, inner } = useLayoutWrapper(props.layoutSettings);
</script>
<template>
<section :class="base()">
<div :class="inner()">
<slot />
</div>
</section>
</template>

View File

@@ -6,9 +6,7 @@ defineProps<NodePageFragment>();
<template>
<div id="node-page">
<h1 v-if="!isFrontPage" class="text-4xl font-bold">
{{ title }}
</h1>
<PageHeader v-if="!isFrontPage" :title="title"></PageHeader>
<BuilderSections :sections="groupPostPage?.sections || []" />
</div>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
defineProps<{
title?: string;
}>();
</script>
<template>
<header v-if="title" class="bg-accented py-6">
<div class="container">
<h1 class="heading-1">
{{ title }}
</h1>
</div>
</header>
</template>

View File

@@ -1,11 +1,13 @@
<script setup lang="ts">
const { isLoggedIn } = useAuth();
const { isRedirecting } = useAuthConnexion();
const layoutSettings: LayoutWrapperProps = {
container: "lg",
};
</script>
<template>
<section data-section-name="auth-connexion" class="py-12">
<div class="container-sm">
<LayoutWrapper data-section-name="auth-connexion" :layout-settings="layoutSettings">
<AuthState>
<AuthRedirecting v-if="isRedirecting" />
<template v-else>
@@ -13,6 +15,5 @@ const { isRedirecting } = useAuthConnexion();
<AuthLoginForm v-else />
</template>
</AuthState>
</div>
</section>
</LayoutWrapper>
</template>

View File

@@ -2,4 +2,8 @@ fragment SectionHeroSplit on GroupAbstractBuilderSectionsHeroSplitLayout {
content
reverse
...AcfMedia
layoutSettings {
...LayoutColored
...LayoutPadded
}
}

View File

@@ -3,8 +3,8 @@ import { tv, type VariantProps } from "tailwind-variants";
import type { SectionHeroSplitFragment } from "#graphql/operations";
const tvSectionHeroSplit = tv({
extend: tvLayoutWrapper,
slots: {
base: "py-6",
container: "container flex flex-col items-center gap-6",
content: "flex-1",
media: "w-full basis-1/2",
@@ -27,6 +27,7 @@ const tvSectionHeroSplit = tv({
const props = defineProps<SectionHeroSplitFragment>();
const classes = tvSectionHeroSplit({
reverse: props.reverse,
...props.layoutSettings,
} as VariantProps<typeof tvSectionHeroSplit>);
</script>

View File

@@ -1,6 +1,8 @@
fragment SectionTextBlock on GroupAbstractBuilderSectionsTextBlockLayout {
content
layoutSettings {
...LayoutColored
...LayoutContained
...LayoutPadded
}
}

View File

@@ -5,7 +5,7 @@ defineProps<SectionTextBlockFragment>();
</script>
<template>
<LayoutContained data-section-type="text-block" v-bind="layoutSettings!">
<LayoutWrapper data-section-type="text-block" :layout-settings="layoutSettings">
<UiProse :content="content" />
</LayoutContained>
</LayoutWrapper>
</template>

View File

@@ -0,0 +1,64 @@
import { tv, type VariantProps } from "tailwind-variants";
import * as z from "zod";
// Tailwind Variants for LayoutWrapper
export const tvLayoutWrapper = tv({
slots: {
base: "",
inner: "",
},
variants: {
// LayoutColored
color: {
default: { base: "bg-default" },
muted: { base: "bg-muted" },
elevated: { base: "bg-elevated" },
accented: { base: "bg-accented" },
inverted: { base: "bg-inverted text-inverted" },
primary: { base: "bg-primary text-inverted" },
},
// LayoutContained
container: {
default: { inner: "container" },
xl: { inner: "container-xl" },
lg: { inner: "container-lg" },
fluid: { inner: "container-fluid" },
fullbleed: { inner: "container-fullbleed" },
},
// LayoutPadded
verticalPadding: {
sm: { base: "py-3" },
md: { base: "py-6" },
lg: { base: "py-12" },
none: {},
},
},
defaultVariants: {
color: "default",
container: "default",
verticalPadding: "md",
},
});
export type LayoutWrapperProps = VariantProps<typeof tvLayoutWrapper>;
// Zod schemas for validating layout settings
const colorEnum = z.enum(["default", "muted", "elevated", "accented", "inverted", "primary"]);
const containerEnum = z.enum(["default", "xl", "lg", "fluid", "fullbleed"]);
const verticalPaddingEnum = z.enum(["sm", "md", "lg", "none"]);
const layoutSettingsSchema = z.object({
color: z.string().pipe(colorEnum).optional(),
container: z.string().pipe(containerEnum).optional(),
verticalPadding: z.string().pipe(verticalPaddingEnum).optional(),
});
export type LayoutSettings = z.input<typeof layoutSettingsSchema>;
export function useLayoutWrapper(input?: LayoutSettings) {
try {
return tvLayoutWrapper(layoutSettingsSchema.parse(input));
} catch {
return tvLayoutWrapper();
}
}