Overlays, tabs and admin
Windows, dialogs, menus, tabs, the confirm prompt, settings rows and the admin panel shell are the pieces that frame everything else on screen. Every preview below is the real component. Dialogs, the confirm prompt and menus draw over the page, so those previews open when you click and close again.
KWindow
The standard panel frame: an optional header with an icon tile, title, subtitle and close button, a body, and an optional footer. A window sits in the normal page flow and fills the space its parent gives it, so size it with its container. For a centered modal use KDialog.
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | none | Header title. The header shows when a title or the header slot is given. |
subtitle | string | none | Smaller line under the title. |
icon | string | none | Icon in the tinted tile before the title, for example lucide:warehouse. |
tone | 'neutral' | 'blue' | 'green' | 'red' | 'amber' | 'violet' | 'teal' | 'blue' | Color of the icon tile. |
closable | boolean | true | Shows the close button in the header. |
Events: close when the close button is pressed. Slots: default (the body), header (replaces the icon, title and subtitle), actions (buttons beside the close button), footer (a bar under the body).
<KWindow title="Garage" subtitle="3 bays" icon="lucide:warehouse" class="h-full" @close="close">
<template #actions><KBadge tone="green" label="Open" dot /></template>
<div class="k-scroll min-h-0 flex-1 p-4">Content</div>
<template #footer><KButton variant="primary" label="Save" /></template>
</KWindow>KDialog
A centered modal window over a dimmed backdrop, built on KWindow. It closes on Escape or a click on the backdrop.
KDialog covers the whole screen, so this preview opens on click and closes with Escape, the close button or the backdrop.
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | required | Shows or hides the dialog. |
title | string | none | Header title. |
subtitle | string | none | Smaller line under the title. |
icon | string | none | Header icon. |
tone | Tone | 'blue' | Color of the icon tile. |
size | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Maximum width of the window. |
dismissable | boolean | true | Close on a backdrop click. Escape always closes. |
flush | boolean | false | Removes the body padding, for full width lists. |
bodyClass | string | none | Extra classes for the body. |
Events: close on Escape, the close button or a backdrop click (when dismissable). Your code sets open to false. Slots: default (the body), actions (header buttons), footer (laid out as a row, put buttons here).
<KDialog :open="open" title="Rename vehicle" icon="lucide:car" size="sm" @close="open = false">
<KInput v-model="name" label="Name" />
<template #footer>
<KButton variant="ghost" label="Cancel" @click="open = false" />
<KButton variant="primary" label="Save" @click="save" />
</template>
</KDialog>KMenu
A dropdown action menu. By default the trigger is a small ghost button with an ellipsis icon, and the menu opens in a floating panel under it. Menus with more than 8 actions get a search box automatically.
Each entry in items is a MenuItem:
| Field | Type | Description |
|---|---|---|
label | string | Text of the action. |
icon | string | Icon before the label. |
hint | string | Small text on the right, such as a shortcut. It is also searched. |
tone | 'default' | 'danger' | 'danger' colors the action red. |
disabled | boolean | Dims the action and ignores clicks. |
hidden | boolean | Leaves the entry out. |
divider | boolean | Draws a separator line instead of an action. |
heading | string | Draws a small group heading instead of an action. |
action | () => unknown | Runs when the action is picked. The menu closes first. |
| Prop | Type | Default | Description |
|---|---|---|---|
items | MenuItem[] | required | The entries to show. |
icon | string | 'lucide:ellipsis' | Icon of the default trigger button. |
label | string | none | Text of the default trigger button. |
title | string | "More actions" | Tooltip of the default trigger button. |
variant | 'secondary' | 'ghost' | 'ghost' | Style of the default trigger button. |
size | 'xs' | 'sm' | 'md' | 'sm' | Size of the default trigger button. |
align | 'start' | 'end' | 'end' | Which edge of the trigger the panel lines up with. |
searchable | boolean | automatic | Force the search box on or off. When left out it appears for more than 8 actions. |
Slots: default, replaces the trigger button. It receives open (a boolean), and clicking anything inside it toggles the menu.
<KMenu :items="[
{ label: 'Repair', icon: 'lucide:wrench', action: repair },
{ divider: true },
{ label: 'Delete', icon: 'lucide:trash-2', tone: 'danger', action: remove }
]" />KTabs
A row of tabs with a highlighted underline for the active one. When there are more tabs than the width allows, the row scrolls sideways and keeps the selected tab in view.
Each entry in tabs is a TabItem:
| Field | Type | Description |
|---|---|---|
id | string | Value of v-model when the tab is selected. |
label | string | Text of the tab. |
icon | string | Icon before the label. |
dot | boolean | Small amber dot, for a tab that needs attention. |
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | string | required | The id of the selected tab. Use v-model. |
tabs | TabItem[] | required | The tabs to show. |
Events: update:modelValue with the picked tab's id.
<KTabs v-model="tab" :tabs="[
{ id: 'overview', label: 'Overview', icon: 'lucide:layout-dashboard' },
{ id: 'payroll', label: 'Payroll', dot: true }
]" />confirm and KConfirmHost
confirm() asks the player or staff member a yes or no question and returns a promise that resolves to true or false. It replaces the browser's window.confirm, which does not work inside the game. Mount KConfirmHost once in your root component and call confirm() from anywhere.
Like KDialog the prompt covers the screen, so these buttons open it over the page. Enter confirms, Escape cancels.
confirm(options) takes these options and returns Promise<boolean>:
| Option | Type | Default | Description |
|---|---|---|---|
title | string | required | The question. |
message | string | none | Longer explanation under the title. |
confirmLabel | string | "Confirm" | Text of the confirm button. |
cancelLabel | string | "Cancel" | Text of the cancel button. |
tone | 'primary' | 'danger' | 'primary' | 'danger' uses a red icon and button. |
icon | string | question mark, or a warning triangle for 'danger' | Icon beside the title. |
requireText | string | none | The user must type this exact text before the confirm button enables. Use it for destructive actions. |
Calling confirm() while another prompt is open cancels the open one. KConfirmHost has no props.
<!-- App.vue: once -->
<KConfirmHost />import { confirm } from '@/kit'
if (await confirm({ title: 'Delete this business?', tone: 'danger', requireText: 'Harbor Garage' })) {
// go ahead
}KSettingRow
One row of a settings panel. It shows a label and hint, a control, an "Unsaved" badge while the value is being edited, a "Changed" badge when it differs from the default, and a revert button.
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | required | Name of the setting. |
hint | string | none | Explanation under the label. |
dirty | boolean | false | The value has an unsaved edit. Tints the row amber and shows "Unsaved". |
modified | boolean | false | The value differs from the default. Shows "Changed" and the revert button. |
defaultText | string | none | Shown as "Default: ..." while modified. |
revertTitle | string | "Back to the default" | Tooltip of the revert button. |
Events: revert when the revert button is pressed. Slots: control (a small control that sits beside the label, with the revert button after it), badges (extra badges next to the label), default (a wide editor such as a list or picker, drawn under the label row; the revert button then shows as a labeled "Default" button).
<KSettingRow label="Fuel burn" hint="Per second" :dirty="changed('fuelBurn')" :modified="modified" default-text="1.2" @revert="revert">
<template #control><KInput v-model="draft.fuelBurn" type="number" size="sm" /></template>
</KSettingRow>useDraft
useDraft(build) keeps an editable copy of a record and tracks what changed, which is what the dirty prop of a setting row needs. build is a function that returns the current saved values.
| Returned | Description |
|---|---|
draft | A ref holding the editable copy. Bind your inputs to its fields. |
base | A ref holding the saved baseline the draft is compared with. |
dirty | A computed boolean: the draft differs from the baseline. |
changed(...keys) | Returns true if any of the listed fields differ from the baseline. |
reset() | Rebuilds both draft and baseline from build(). Call it when a different record is selected. |
commit() | Marks the current draft as saved, without waiting for the source to refresh. |
rebase() | Refreshes untouched fields from the source while keeping local edits. |
const { draft, dirty, changed, commit, reset } = useDraft(() => ({ fuelBurn: settings.fuelBurn }))
async function save() {
await post('saveSettings', { fuelBurn: draft.value.fuelBurn })
commit()
}KAdminShell
The frame of an admin panel: a window with a side menu of sections on the left and the active section on the right. Each section is a named slot whose name is the section id. A section mounts the first time it is opened and then stays alive, so searches and selections survive switching between sections. The side menu can shrink to an icon rail, and the choice is remembered.
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | string | required | The id of the open section. Use v-model. |
sections | AdminSection[] | required | The sections in the side menu. |
title | string | required | Window title. |
subtitle | string | none | Smaller line under the title. |
icon | string | 'lucide:shield' | Window icon. |
tone | Tone | 'blue' | Color of the icon tile. |
adminBack | boolean | false | Shows an "Admin panel" button that closes this panel and reopens the main admin panel in game. |
switchable | boolean | false | Shows "Switch panel" and answers Ctrl+K, for panels that draw their own back button. |
beforeLeave | () => boolean | Promise<boolean> | none | Asked before the panel closes for Switch panel, Ctrl+K or Admin panel. Return false to stay open, for example with unsaved edits. |
peekable | boolean | false | Adds a Hide button that hides the panel temporarily, so the player can look at the world. Press E to bring it back. |
persist | boolean | false | Remembers the open section, and each list's search, page and scroll, across closes. |
collapsible | boolean | true | Lets the side menu shrink to an icon rail. |
Each entry in sections is an AdminSection:
| Field | Type | Description |
|---|---|---|
id | string | Section id, and the name of its slot. |
label | string | Text in the side menu. |
icon | string | Menu icon, for example lucide:settings. |
count | number | null | Small count shown beside the label (a dot on the icon rail). |
heading | string | Starts a new menu group with a small heading above this entry. |
Events: update:modelValue with the picked section id, and close. Slots: one slot per section id, actions (extra header buttons), nav-footer (content at the bottom of the side menu, receives collapsed). The Switch panel, Admin panel and Hide buttons talk to the game, so in this preview they are not shown.
<KAdminShell v-model="section" :sections="sections" title="Garage admin" icon="lucide:warehouse" persist @close="close">
<template #general><GeneralPanel /></template>
<template #jobs><JobsPanel /></template>
</KAdminShell>const section = ref('general')
const sections: AdminSection[] = [
{ id: 'general', label: 'General', icon: 'lucide:settings', heading: 'Setup' },
{ id: 'jobs', label: 'Jobs', icon: 'lucide:briefcase', count: 3 }
]KMasterDetail, KListItem and KDetail
The usual admin layout is a searchable list on the left and the selected record on the right. KMasterDetail provides the list column (search box, filters, empty and loading states, optional pagination, and a collapse button) and a pane for the detail. KListItem is one row of the list, and KDetail is the detail pane with a header, optional tabs, a scrolling body and an unsaved changes bar.
KMasterDetail
| Prop | Type | Default | Description |
|---|---|---|---|
search | string | required | Text of the search box. Use v-model:search. |
searchPlaceholder | string | "Search..." | Placeholder of the search box. |
loading | boolean | false | Shows a loading state while the list is empty. |
empty | boolean | false | Shows the empty state instead of the list. |
emptyTitle | string | "Nothing here yet" | Title of the empty state. |
emptyDescription | string | none | Description of the empty state. |
emptyIcon | string | none | Icon of the empty state. |
total | number | null | null | Total number of records. Set it to show the pagination footer. |
page | number | 1 | Current page. Use v-model:page. |
pageSize | number | 50 | Records per page. |
noun | string | none | Word after the count in the footer, for example jobs. |
width | 'sm' | 'md' | 'md' | Width of the list column. |
stateKey | string | none | Tells apart several lists inside one panel section, so each remembers its own search, page and scroll. |
collapsible | boolean | true | Lets the list shrink to a thin rail so the detail pane gets more room. |
Events: update:search, update:page. Slots: default (the list rows), detail (the right pane), toolbar (buttons beside the search box), filters (a row under the search box), empty (replaces the default empty state). Inside a KAdminShell with persist, the list remembers its search, page and scroll.
KListItem
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | none | Main text. |
subtitle | string | none | Smaller line under the title. |
icon | string | none | Icon in the tile before the text. |
tone | Tone | 'neutral' | Color of the icon tile. |
active | boolean | false | Highlights the row as the selected one. |
muted | boolean | false | Dims the row, for disabled records. |
Events: click. Slots: leading (replaces the icon tile, for a picture or avatar), title and subtitle (replace the text props), meta (badges or counts on the right).
KDetail
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | required | Heading of the pane. |
subtitle | string | none | Smaller line under the heading. |
eyebrow | string | none | Small label above the heading. |
icon | string | none | Icon in the tile before the heading. |
tone | Tone | 'blue' | Color of the icon tile. |
tabs | TabItem[] | none | Shows a KTabs row under the header. |
tab | string | first tab | The selected tab. Use v-model:tab. |
dirty | boolean | false | Shows the unsaved changes bar at the bottom. |
saveLabel | string | "Save changes" | Text of the save button. |
saveAction | () => unknown | none | Async handler for the save button, which shows a spinner until it settles. |
resetKey | string | number | null | none | When this changes (for example the record id) the body scrolls back to the top. |
Events: update:tab, discard. Slots: default (the body, laid out in a padded column), badges (under the heading), actions (buttons on the right of the header), footer (replaces the unsaved changes bar).
<KMasterDetail v-model:search="search" v-model:page="page" :page-size="50" :total="total" noun="jobs" :empty="rows.length === 0">
<KListItem v-for="job in rows" :key="job.id" :title="job.label" icon="lucide:briefcase" :active="job.id === picked" @click="picked = job.id" />
<template #detail>
<KDetail :title="draft.label" :dirty="dirty" :save-action="save" @discard="reset">
<KInput v-model="draft.label" label="Display name" />
</KDetail>
</template>
</KMasterDetail>KPagination
A compact "1-50 of 240 jobs" label with previous and next buttons. The buttons only appear when there is more than one page. KMasterDetail uses it for its footer, and you can use it on its own.
| Prop | Type | Default | Description |
|---|---|---|---|
page | number | required | Current page, starting at 1. Use v-model:page. |
pageSize | number | required | Records per page. |
total | number | required | Total number of records. |
noun | string | '' | Word shown after the count. |
Events: update:page with the new page number, kept within range.
<KPagination v-model:page="page" :page-size="25" :total="240" noun="jobs" />