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:

TEXT
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`:

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`:

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`:

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`:

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.