Adding Components to a Preset
Once you have created a preset, you can start adding custom component implementations to it. KatluxToolkit uses a dynamic resolution system that searches for components within your `presets/{presetName}/components/` folder.
1. Preset Structure Checklist
Before adding components, ensure your preset follows the standard structure:
presets/my-brand/
├── assets/ # CSS design tokens (css-variables.css)
└── components/ # Component overrides
└── KButton/
└── KButton.vue
2. CSS Variables & Styling Reference
When styling custom components, always use semantic CSS variables instead of hardcoded values.
For full token list and styling options, see the Styling & CSS Variables Documentation.
3. How to Create a Preset Component
Create a folder with the component's exact name inside `presets/{presetName}/components/{ComponentName}/{ComponentName}.vue`.
4. The Fallback Mechanism
One of the most powerful features of Katlux is the fallback mechanism. You don't need to implement every single component for every preset.
Important: If a component is missing in your active preset, Katlux will automatically fall back to the next preset in your priority stack (and ultimately to the built-in default preset).
5. Example Implementations
Basic Component Example: KButton
In `presets/my-brand/components/KButton/KButton.vue`:
<template lang="pug">
button.KButton.my-brand-button(
@click="onClick"
:class="buttonClasses"
:disabled="isDisabled"
)
slot
</template>
<script setup lang="ts">
defineProps<{
isDisabled?: boolean
buttonClasses?: any
onClick?: (e: MouseEvent) => void
}>()
</script>
<style lang="scss" scoped>
.my-brand-button {
border-radius: var(--border-radius-md);
padding: var(--gap-sm) var(--gap-md);
background: var(--primary-500);
color: var(--font-color-primary);
border: none;
}
</style>
Advanced Component Example: KDatatable
In `presets/my-brand/components/KDatatable/KDatatable.vue`:
<template lang="pug">
.my-brand-datatable
table
thead
tr
th(v-if="multiSelect")
KCheckbox(v-model="selectAll")
th(v-for="field in activeFields" :key="field")
slot(:name="`header-\${field}`" :field="field")
| {{ columnHeaders?.[field] || field }}
tbody
tr(v-for="row in dataProvider?.pageData?.value" :key="row[idKey]")
td(v-if="multiSelect")
KCheckbox(:value="row" v-model="selectedRows")
td(v-for="field in activeFields" :key="field")
slot(:name="`cell-\${field}`" :row="row" :value="row[field]")
| {{ row[field] }}
</template>
<script setup lang="ts">
import { computed } from 'vue'
const selectedRows = defineModel<any[]>('selectedRows', { default: () => [] })
const selectAll = defineModel<boolean>('selectAll', { default: false })
const props = withDefaults(defineProps<{
dataProvider: any
visibleFields?: string[] | null
columnHeaders?: Record<string, string>
multiSelect?: boolean
idKey?: string
}>(), {
visibleFields: null,
columnHeaders: () => ({}),
multiSelect: false,
idKey: 'id'
})
const activeFields = computed(() => {
if (props.visibleFields) return props.visibleFields
const firstRow = props.dataProvider?.pageData?.value?.[0]
return firstRow ? Object.keys(firstRow) : []
})
</script>
<style lang="scss" scoped>
.my-brand-datatable {
table {
width: 100%;
border-collapse: collapse;
th, td {
padding: var(--gap-sm) var(--gap-md);
border-bottom: 1px solid var(--border-color-light);
}
}
}
</style>
6. Layout Presets & Dynamic Slot Forwarding (KLayout)
Katlux provides `KLayout` as a universal application shell. In addition to the standard semantic slots (`#header`, `#aside`, `#main`, `#footer`), `KLayout` features **dynamic slot forwarding**. Any slot you pass to `KLayout` is automatically forwarded directly to your preset's `KLayout.vue`:
Adding Custom Slots to a Preset KLayout
In your preset `presets/my-brand/components/KLayout/KLayout.vue`:
<template lang="pug">
.my-brand-layout
header.brand-header(v-if="$slots.header")
slot(name="header")
nav.brand-toolbar(v-if="$slots.toolbar")
slot(name="toolbar")
.brand-body
aside(v-if="$slots.aside")
slot(name="aside")
main
slot(name="main")
slot
aside.drawer(v-if="$slots['right-drawer']")
slot(name="right-drawer")
footer(v-if="$slots.footer")
slot(name="footer")
.brand-statusbar(v-if="$slots.statusbar")
slot(name="statusbar")
</template>
Switching Structural Subcomponents Inside KLayout
If your project or tenant requires radically different layout shells (e.g. Standard, Complex Dashboard, Fullscreen Canvas), you can create modular subcomponents in your preset and switch between them dynamically inside `KLayout.vue`:
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from '#app'
import LayoutStandard from './structures/LayoutStandard.vue'
import LayoutDashboard from './structures/LayoutDashboard.vue'
import LayoutCanvas from './structures/LayoutCanvas.vue'
const props = defineProps<{ structure?: string }>()
const route = useRoute()
const activeLayout = computed(() => {
const mode = props.structure || route.meta.layoutStructure
if (mode === 'dashboard') return LayoutDashboard
if (mode === 'canvas') return LayoutCanvas
return LayoutStandard
})
</script>
<template>
<component :is="activeLayout">
<template v-for="(_, slot) in $slots" ="scope">
<slot :name="slot" v-bind="scope" />
</template>
</component>
</template>
7. Architectural Note: Structural Preset Swapping and Slot State Reset
Component Unmounting Behavior: In Vue 3, when a parent container component is swapped via dynamic :is="presetComponent" (which occurs when switching to a preset that overrides structural components like KLayout, KPanel, or KGrid), Vue unmounts the previous template subtree and mounts the new template.
State Persistence in Applications: Slotted children inside the swapped component will be remounted. Consequently, any local ephemeral component state (such as local ref() variables, open/closed tab indices, or datatable row selections) will reset to their initial values.
Recommendation: If an application requires state to persist across preset switches, that state must be stored in an external state management store such as Pinia or Nuxt's useState() rather than inside local component refs.