Creating a New Preset

KatluxToolkit's preset system allows you to create completely different visual styles for your components while maintaining the same underlying logic.

Unified Architecture: Katlux does not differentiate between "Token Presets" and "Structural Template Presets". Any preset can be token-only (e.g. only defining CSS variables in assets/css-variables.scss without overriding any component templates, like @katlux/preset-dark), or it can optionally provide component overrides. All presets follow the exact same architecture and cascading resolution.

1. Scaffold Your Preset with the Katlux CLI

The easiest way to create a custom preset is using the official toolkit CLI scaffolding command:

Bash
# Scaffolding based on an existing preset (e.g. 'modern'):
npx @katlux/toolkit create-preset cyber-brand --base=modern

# Scaffolding without a base preset (falls back directly to 'default'):
npx @katlux/toolkit create-preset cyber-brand
Zero-Copy Fallback Architecture:When a preset is based on another preset (e.g. --base=modern), do not copy any files or components from the base preset. In Katlux, inheritance is handled dynamically through the preset selector in app.config.ts. Any component or token not defined in your custom preset will automatically fall back to the base preset, and then to default.

2. Preset Folder Structure

Custom presets reside in presets/{presetName}/ (or app/presets/{presetName}/). The scaffolding command generates an empty components/ directory and an assets/css-variables.scss file containing the entire design token catalog:

TEXT
presets/cyber-brand/
├── assets/
│   └── css-variables.scss      # All design tokens pre-pasted in comments
└── components/                 # Empty scaffolding folder for custom component overrides

3. Register & Activate Your Preset

Declare your preset in app.config.ts based on whether you specified a base preset:

A. When Based on Another Preset (Multi-Preset Cascade)

Use a multi-preset array. The leftmost preset takes highest priority and overrides the base preset:

TypeScript
// app.config.ts
export default defineAppConfig({
  katluxToolkit: {
    presetList: [
      { name: 'Default', value: 'default' },
      { name: 'Modern', value: 'modern' },
      { name: 'Cyber Brand', value: ['cyber-brand', 'modern'] }
    ],
    // Multi-preset configuration: cyber-brand overrides modern, then falls back to default
    activePreset: ['cyber-brand', 'modern'],
  },
})

B. When No Base Preset is Provided (Direct Default Fallback)

Use a single preset string. Any token or component not defined in your preset automatically falls back directly to the built-in default preset:

TypeScript
// app.config.ts
export default defineAppConfig({
  katluxToolkit: {
    presetList: [
      { name: 'Default', value: 'default' },
      { name: 'Cyber Brand', value: 'cyber-brand' }
    ],
    // Single preset: falls back directly to 'default'
    activePreset: 'cyber-brand',
  },
})

4. CSS Variables & Design Tokens

When you scaffold a preset, assets/css-variables.scss is pre-populated with all Katlux design tokens in commented-out format organized into 22 categories (primary colors, neutral palette, semantic status colors, typography, buttons, inputs, checkboxes, tables, panels, overlays, and layout dimensions).

Developers can simply uncomment any token and provide a new value without needing to search external documentation.

To explore all tokens in detail, visit the Full CSS Variable & Design Tokens Documentation.

SCSS
/* presets/cyber-brand/assets/css-variables.scss */
[data-preset~="cyber-brand"] {
  // Uncomment and change any token:
  --primary-500: #6366f1;
  --background-color: #0f172a;
  --font-color-primary: #f8fafc;
  --border-radius-md: 8px;
  // --button-bg-primary: #6366f1;
  // --input-border-radius: 6px;
}
Best Practice:Always use space-separated attribute selectors (e.g. [data-preset~="cyber-brand"]) rather than substring matching (*=). Avoid applying !important to theme rules, as it prevents smooth dynamic switching between active presets.

5. Creating Custom Preset Components

To override a component in your preset, create a Vue 3 SFC at `presets/{presetName}/components/{ComponentName}/{ComponentName}.vue`.

Basic Example: KButton

A basic component receives props from the logic layer and renders styled markup with slots:

Vue
<template lang="pug">
button.KButton.my-brand-button(
  @click="onClick" 
  :class="buttonClasses" 
  :disabled="isDisabled"
)
  slot
</template>

<script lang="ts" setup>
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;
  transition: transform 0.2s ease;
}
</style>

Advanced Example: KDatatable

Advanced components handle complex data structures, two-way model bindings (`defineModel`), dynamic slot forwarding, and internal actions:

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 lang="ts" setup>
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>