Skip to content

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.

KWindow
PropTypeDefaultDescription
titlestringnoneHeader title. The header shows when a title or the header slot is given.
subtitlestringnoneSmaller line under the title.
iconstringnoneIcon 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.
closablebooleantrueShows 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).

vue
<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.

KDialog
PropTypeDefaultDescription
openbooleanrequiredShows or hides the dialog.
titlestringnoneHeader title.
subtitlestringnoneSmaller line under the title.
iconstringnoneHeader icon.
toneTone'blue'Color of the icon tile.
size'sm' | 'md' | 'lg' | 'xl''md'Maximum width of the window.
dismissablebooleantrueClose on a backdrop click. Escape always closes.
flushbooleanfalseRemoves the body padding, for full width lists.
bodyClassstringnoneExtra 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).

vue
<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.

KMenu

Each entry in items is a MenuItem:

FieldTypeDescription
labelstringText of the action.
iconstringIcon before the label.
hintstringSmall text on the right, such as a shortcut. It is also searched.
tone'default' | 'danger''danger' colors the action red.
disabledbooleanDims the action and ignores clicks.
hiddenbooleanLeaves the entry out.
dividerbooleanDraws a separator line instead of an action.
headingstringDraws a small group heading instead of an action.
action() => unknownRuns when the action is picked. The menu closes first.
PropTypeDefaultDescription
itemsMenuItem[]requiredThe entries to show.
iconstring'lucide:ellipsis'Icon of the default trigger button.
labelstringnoneText of the default trigger button.
titlestring"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.
searchablebooleanautomaticForce 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.

vue
<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.

KTabs

Each entry in tabs is a TabItem:

FieldTypeDescription
idstringValue of v-model when the tab is selected.
labelstringText of the tab.
iconstringIcon before the label.
dotbooleanSmall amber dot, for a tab that needs attention.
PropTypeDefaultDescription
modelValuestringrequiredThe id of the selected tab. Use v-model.
tabsTabItem[]requiredThe tabs to show.

Events: update:modelValue with the picked tab's id.

vue
<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()

confirm(options) takes these options and returns Promise<boolean>:

OptionTypeDefaultDescription
titlestringrequiredThe question.
messagestringnoneLonger explanation under the title.
confirmLabelstring"Confirm"Text of the confirm button.
cancelLabelstring"Cancel"Text of the cancel button.
tone'primary' | 'danger''primary''danger' uses a red icon and button.
iconstringquestion mark, or a warning triangle for 'danger'Icon beside the title.
requireTextstringnoneThe 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.

vue
<!-- App.vue: once -->
<KConfirmHost />
ts
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.

KSettingRow
PropTypeDefaultDescription
labelstringrequiredName of the setting.
hintstringnoneExplanation under the label.
dirtybooleanfalseThe value has an unsaved edit. Tints the row amber and shows "Unsaved".
modifiedbooleanfalseThe value differs from the default. Shows "Changed" and the revert button.
defaultTextstringnoneShown as "Default: ..." while modified.
revertTitlestring"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).

vue
<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.

ReturnedDescription
draftA ref holding the editable copy. Bind your inputs to its fields.
baseA ref holding the saved baseline the draft is compared with.
dirtyA 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.
ts
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.

KAdminShell
PropTypeDefaultDescription
modelValuestringrequiredThe id of the open section. Use v-model.
sectionsAdminSection[]requiredThe sections in the side menu.
titlestringrequiredWindow title.
subtitlestringnoneSmaller line under the title.
iconstring'lucide:shield'Window icon.
toneTone'blue'Color of the icon tile.
adminBackbooleanfalseShows an "Admin panel" button that closes this panel and reopens the main admin panel in game.
switchablebooleanfalseShows "Switch panel" and answers Ctrl+K, for panels that draw their own back button.
beforeLeave() => boolean | Promise<boolean>noneAsked before the panel closes for Switch panel, Ctrl+K or Admin panel. Return false to stay open, for example with unsaved edits.
peekablebooleanfalseAdds a Hide button that hides the panel temporarily, so the player can look at the world. Press E to bring it back.
persistbooleanfalseRemembers the open section, and each list's search, page and scroll, across closes.
collapsiblebooleantrueLets the side menu shrink to an icon rail.

Each entry in sections is an AdminSection:

FieldTypeDescription
idstringSection id, and the name of its slot.
labelstringText in the side menu.
iconstringMenu icon, for example lucide:settings.
countnumber | nullSmall count shown beside the label (a dot on the icon rail).
headingstringStarts 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.

vue
<KAdminShell v-model="section" :sections="sections" title="Garage admin" icon="lucide:warehouse" persist @close="close">
  <template #general><GeneralPanel /></template>
  <template #jobs><JobsPanel /></template>
</KAdminShell>
ts
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

KMasterDetail ​

PropTypeDefaultDescription
searchstringrequiredText of the search box. Use v-model:search.
searchPlaceholderstring"Search..."Placeholder of the search box.
loadingbooleanfalseShows a loading state while the list is empty.
emptybooleanfalseShows the empty state instead of the list.
emptyTitlestring"Nothing here yet"Title of the empty state.
emptyDescriptionstringnoneDescription of the empty state.
emptyIconstringnoneIcon of the empty state.
totalnumber | nullnullTotal number of records. Set it to show the pagination footer.
pagenumber1Current page. Use v-model:page.
pageSizenumber50Records per page.
nounstringnoneWord after the count in the footer, for example jobs.
width'sm' | 'md''md'Width of the list column.
stateKeystringnoneTells apart several lists inside one panel section, so each remembers its own search, page and scroll.
collapsiblebooleantrueLets 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 ​

PropTypeDefaultDescription
titlestringnoneMain text.
subtitlestringnoneSmaller line under the title.
iconstringnoneIcon in the tile before the text.
toneTone'neutral'Color of the icon tile.
activebooleanfalseHighlights the row as the selected one.
mutedbooleanfalseDims 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 ​

PropTypeDefaultDescription
titlestringrequiredHeading of the pane.
subtitlestringnoneSmaller line under the heading.
eyebrowstringnoneSmall label above the heading.
iconstringnoneIcon in the tile before the heading.
toneTone'blue'Color of the icon tile.
tabsTabItem[]noneShows a KTabs row under the header.
tabstringfirst tabThe selected tab. Use v-model:tab.
dirtybooleanfalseShows the unsaved changes bar at the bottom.
saveLabelstring"Save changes"Text of the save button.
saveAction() => unknownnoneAsync handler for the save button, which shows a spinner until it settles.
resetKeystring | number | nullnoneWhen 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).

vue
<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.

KPagination
PropTypeDefaultDescription
pagenumberrequiredCurrent page, starting at 1. Use v-model:page.
pageSizenumberrequiredRecords per page.
totalnumberrequiredTotal number of records.
nounstring''Word shown after the count.

Events: update:page with the new page number, kept within range.

vue
<KPagination v-model:page="page" :page-size="25" :total="240" noun="jobs" />