Skip to content

Global config ​

Every look and every feature toggle in the app is a key on one object, persisted and applied through a single pathway.

Open in demo

The store ​

ts
import { useConfig } from '@core/config/useConfig'

const cfg = useConfig()

A Pinia store holding one SmartAdminConfig, persisted to localStorage under the key sa-config. Hold the store rather than destructuring it: mergeConfig and resetConfig replace the config object, so a destructured config stops following it. See useConfig.

MemberDescription
configThe whole object, reactive.
mergeConfig(partial)Deep-merges a partial. The usual way to change several keys.
resetConfig()Back to defaultConfig.
setFeature(key, value)One feature flag.
setUi(key, value)One ui key, typed to that key's value.

The store is value-only: it never calls an engine. setupConfigWiring() watches each section and is the one pathway downward. That is what keeps the persisted config and the screen from disagreeing.

The wiring ​

Called once from main.ts, after Pinia and before the router. Every watcher is immediate: true, so the first paint already matches a hydrated config.

WatchesCalls
theme.primary / secondary / accentsetPrimary / setSecondary / setAccent, each writing an eleven-stop ramp.
theme.background.light / darkWrites --sa-app-bg-light / --sa-app-bg-dark on <html>.
fonts.primary / arabic / monosetActiveFonts for that slot, with a logged catch so a failed font cannot block boot.
typographyScalesetScale.
ui.densitysetDensity, toggling sa-density-* on <html>.
ui.borderRadiusWrites --radius on <html>.
localesetLocale, then re-applies the direction.
ui.directionsetDirection.
features.darkModeReturns to light when switched off while dark.
features.rtlReturns direction to auto when switched off while pinned to rtl.

Persisted custom fonts are registered before the font watchers run, or a hydrated fonts.primary naming one would fail to resolve on the first paint.

Every key ​

app ​

KeyTypeDefaultEffect
app.namestring'Smart Admin'Brand name in the sidebar.
app.logostring''A data URI or URL. Empty means the built-in mark.

Top level ​

KeyTypeDefaultEffect
timezonestring'UTC'An IANA zone. Stored for the General tab and read by date formatting once a backend supplies it.
localenine codes'en'The active language, which also drives direction unless ui.direction pins it.
typographyScale'compact' | 'default' | 'comfortable''default'The text size scale.

fonts ​

KeyTypeDefault
fonts.primarystring'Inter'
fonts.arabicstring'Cairo'
fonts.monostring'JetBrains Mono'
fonts.customFontsCustomFont[][]

CustomFont is { name, url, weights? }. weights defaults to [400, 500, 700].

features ​

Every flag is true by default except humanVerification.

KeyDefaultWhat reads it
darkModetrueThe dark-mode switch in the navbar, the settings page and the drawer. Enforces: off while dark returns the app to light.
rtltrueThe right-to-left switch in the drawer. Enforces: off while pinned to rtl returns direction to auto.
i18ntrueThe language switcher in the navbar.
notificationstrueThe notification center in the navbar.
activityLogtrueThe activity-log entry in the navbar.
fileUploadtrueSchemaField. Off, a file field shows a note in place of the drop zone rather than disappearing, so a required field cannot hide and block the form.
richTextEditortrueSchemaField. Off, an editor field degrades to a plain textarea, so the value is still captured, validated and saved.
commandPalettetrueThe navbar trigger, and the Ctrl + K listener in main.ts. Read inside the handler, so a change applies on the next key rather than the next reload.
responsivePreviewtrueThe device-frame preview in the navbar.
exportSystemtrueDataTable and SaExportButton. It gates export; printable is a separate prop.
skeletonLoaderstrueDataTable. Off, a loading table shows no skeleton rows or cards.
humanVerificationfalseThe sign-in and sign-up pages. Off, so the demo stays two clicks.

A flag does two things beyond its own feature: a NavItem carrying feature leaves the sidebar while it is off, and a route carrying meta.feature leaves the command palette. The route stays registered, so a direct URL still resolves — the same way a permission-gated page does.

ui ​

KeyTypeDefaultEffect
ui.layout'classic' | 'modern' | 'minimal' | 'horizontal''modern'The shell.
ui.sidebarPosition'left' | 'right''left'Which side the sidebar sits on.
ui.sidebarStyle'expanded' | 'collapsed' | 'floating''expanded'Its shape at desktop widths.
ui.navbarStyle'sticky' | 'static''sticky'Whether the navbar scrolls away.
ui.borderRadius'none' | 'small' | 'medium' | 'large''medium'Writes --radius.
ui.density'compact' | 'default' | 'comfortable''default'Spacing and control heights.
ui.animationsbooleantrueRead by the sidebar: off removes its width and shadow transitions and collapses the rail with no delay.
ui.direction'auto' | 'ltr' | 'rtl''auto'auto follows the language; the other two pin it, which previews a right-to-left layout without switching language.
ui.previewSizesPreviewSize[]four presetsThe device sizes in the responsive preview.
ui.previewZoom'fit' | number'fit'fit scales the frame to the pane; a number is a percentage of true device pixels, and the frame then scrolls inside the pane.

ResponsivePreview is the device frame itself, opened from the navbar. It takes no props and reads these keys. PreviewSize is { id, label, width, height, builtin? }. The four shipped sizes are Mobile 375×812, Tablet 768×1024, Laptop 1024×768 and Desktop 1440×900, each chosen to land in a different breakpoint band: below md is the drawer, exactly md is the icon rail, lg is the full sidebar. id is what the preview remembers, so it must stay stable across edits. Shipped sizes cannot be deleted from the settings UI; ones you add can.

theme ​

KeyTypeDefault
theme.primarystring'#1DB9C3'
theme.secondarystring'#71717a'
theme.accentstring'#0ea5e9'
theme.background.lightstring'#f1f2f7'
theme.background.darkstring'#0e1013'

Each color becomes an eleven-stop ramp. See Theming.

Change the defaults ​

Edit the defaultConfig export in src/core/config/defaultConfig.ts, or dial the look in through the settings drawer and use Copy config, which hands you that whole object as paste-ready TypeScript.

A reader's saved config wins over a new default, because it is already in their localStorage under sa-config. Rename the key, or call resetConfig(), when you need everyone on a new baseline.

Per-page overrides ​

vue
<script setup lang="ts">
import { definePageConfig } from '@core/config/definePageConfig'

definePageConfig({ ui: { layout: 'minimal' } })
</script>

The override lasts for the page's lifetime and does not touch what is persisted. ConfigProvider is the declarative form, for scoping an override to a subtree from a template:

vue
<script setup lang="ts">
import ConfigProvider from '@core/config/ConfigProvider.vue'
import PriceTable from './PriceTable.vue'
</script>

<template>
  <ConfigProvider :overrides="{ ui: { density: 'compact' } }">
    <PriceTable />
  </ConfigProvider>
</template>

Its overrides are snapshotted at setup.

Where to go next ​