mgv-backoffice
v1.25.0
Published
Shared Vue 3 UI component library
Readme
mgv-backoffice
Shared Vue 3 UI component library built with TypeScript and Tailwind CSS.
Installation
npm install mgv-backofficePeer Dependencies
These must be installed in your project:
npm install vue@^3.5.0 vue-router@^5.0.0 @heroicons/vue@^2.0.0
vue-router5.x is required (peer range^5.0.0) — it's what the library is developed and tested against. Upgrade from Router 4 before installing this library.
Import Styles
Include the library's stylesheet in your app entry point:
import 'mgv-backoffice/dist/style.css'Tailwind Safelist
If your project uses Tailwind, import the safelist so dynamic classes used by this library are generated correctly:
// In your Tailwind config
import safelist from 'mgv-backoffice/tailwind.safelist'Or include the pre-built CSS safelist:
@import 'mgv-backoffice/tailwind.safelist.css';Maintainers:
tailwind.safelist.jsis the single source of truth.tailwind.safelist.cssis generated from it vianpm run safelist(runs automatically beforenpm run build) — don't edit it by hand.
Components
BaseAlert
Dismissible alert banner with color-coded variants.
Props:
| Prop | Type | Default | Description |
| ------- | ----------- | ------------------ | ------------------------ |
| title | String | AlertEnum.ERROR | Text displayed in alert |
| color | AlertEnum | AlertEnum.ERROR | Alert color variant |
Example:
<template>
<BaseAlert title="Operation successful" :color="AlertEnum.SUCCESS" />
<BaseAlert title="Something went wrong" :color="AlertEnum.ERROR" />
</template>
<script setup lang="ts">
import { BaseAlert, AlertEnum } from 'mgv-backoffice'
</script>BaseBadge
Colored status badge/pill.
Props:
| Prop | Type | Default | Description |
| ------- | -------- | ------- | --------------------------------- |
| color | String | — | Color variant (use ColorsEnums) |
Slots: default — badge label content.
Example:
<template>
<BaseBadge :color="ColorsEnums.GREEN">Active</BaseBadge>
<BaseBadge :color="ColorsEnums.RED">Inactive</BaseBadge>
</template>
<script setup lang="ts">
import { BaseBadge, ColorsEnums } from 'mgv-backoffice'
</script>BaseBreadcrumb
Breadcrumb navigation. Provide items manually or pass a URL path for auto-generation.
Props:
| Prop | Type | Default | Description |
| ------- | --------------- | ----------- | ------------------------------------------ |
| items | BreadCrumb[] | undefined | Manual breadcrumb entries |
| path | String | undefined | URL path for auto-generated breadcrumbs |
BreadCrumb type:
interface BreadCrumb {
name: string
url: string
}Example:
<template>
<!-- Manual -->
<BaseBreadcrumb :items="[
{ name: 'Home', url: '/' },
{ name: 'Users', url: '/users' },
{ name: 'Profile', url: '/users/1' }
]" />
<!-- Auto-generated from path -->
<BaseBreadcrumb path="/users/settings/profile" />
</template>
<script setup lang="ts">
import { BaseBreadcrumb } from 'mgv-backoffice'
import type { BreadCrumb } from 'mgv-backoffice'
</script>BaseButton
Button with color, size, loading state, and Vue Router integration.
Props:
| Prop | Type | Default | Description |
| ------------- | ----------------------------------- | --------------------- | ------------------------------------ |
| description | String | required | Button label text |
| color | String | BaseButtonEnum.BLUE | Color variant (BLUE/WHITE/DARK/GREEN/EMERALD/RED/YELLOW/PURPLE/SKY/GRAY/AMBER) |
| outline | Boolean | false | Outlined/secondary style — transparent fill, coloured text + border, tinted hover (theme-aware) |
| ghost | Boolean | false | Ghost/borderless style — no border or fill, coloured text + tinted hover (theme-aware). For compact toolbar/action buttons |
| to | String | — | Vue Router path (renders <router-link>) |
| type | 'button' \| 'submit' \| 'reset' | 'button' | HTML button type |
| iconLeft | Boolean | false | Render the slot icon before the label |
| isRounded | Boolean | — | Fully rounded corners |
| isDisable | Boolean | — | Disabled state |
| size | String | — | Size variant (use BaseButtonSizeEnum) |
| isLoading | Boolean | — | Show loading spinner |
Slots: default
Example:
<template>
<BaseButton description="Submit" :color="BaseButtonEnum.GREEN" type="submit" />
<BaseButton description="Go to Users" :to="'/users'" />
<BaseButton description="Saving..." :isLoading="true" :isDisable="true" />
<BaseButton
description="Delete"
:color="BaseButtonEnum.RED"
:size="BaseButtonSizeEnum.SMALL"
/>
<!-- Outlined / secondary -->
<BaseButton description="Import" :color="BaseButtonEnum.EMERALD" outline iconLeft>
<ArrowUpTrayIcon class="w-4 h-4 mr-1.5" />
</BaseButton>
<!-- Ghost / borderless toolbar action -->
<BaseButton description="Logs" :color="BaseButtonEnum.SKY" ghost iconLeft :size="BaseButtonSizeEnum.SMALL">
<ClipboardDocumentListIcon class="w-4 h-4 mr-1.5" />
</BaseButton>
</template>
<script setup lang="ts">
import { BaseButton, BaseButtonEnum, BaseButtonSizeEnum } from 'mgv-backoffice'
</script>BaseLine
Horizontal divider with style variants.
Props:
| Prop | Type | Default | Description |
| ------ | -------- | --------------- | ----------------- |
| mode | String | LineEnum.BASE | Divider style |
Example:
<template>
<BaseLine />
<BaseLine :mode="LineEnum.SQUARE" />
</template>
<script setup lang="ts">
import { BaseLine, LineEnum } from 'mgv-backoffice'
</script>BaseLogo
SVG brand logo component.
Props:
| Prop | Type | Default | Description |
| ------ | -------- | ---------------------- | ------------ |
| size | String | BaseLogoEnum.MEDIUM | Logo size |
Example:
<template>
<BaseLogo :size="BaseLogoEnum.LARGE" />
</template>
<script setup lang="ts">
import { BaseLogo, BaseLogoEnum } from 'mgv-backoffice'
</script>BaseModal
⚠️ Deprecated. Prefer
BaseConfirmModalfor confirm/cancel flows orBaseModalShellfor custom dialogs — they support dark mode, teleport to<body>, and slot-based composition. Kept for backward compatibility.
Confirmation dialog with support for delete and success modes. DELETE mode renders
the confirm/cancel pair; any other mode renders the title, optional description,
the default slot, and a single OK button that emits closeModal.
Props:
| Prop | Type | Default | Description |
| ------------- | -------- | ----------- |-------------------------------------|
| title | String | required| Modal heading |
| description | String | — | Body text |
| to | String | "/" | Redirect path on confirm |
| mode | String | 'SUCCESS' | Modal variant (use BaseModalEnum) |
Events:
| Event | Description |
| -------------- | ------------------------------- |
| closeModal | Emitted when modal is dismissed |
| confirmModal | Emitted on confirm action |
Example:
<template>
<BaseModal
title="Delete this item?"
description="This action cannot be undone."
:mode="BaseModalEnum.DELETE"
@closeModal="showModal = false"
@confirmModal="handleDelete"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { BaseModal, BaseModalEnum } from 'mgv-backoffice'
const showModal = ref(true)
const handleDelete = () => { /* ... */ }
</script>BaseRow
Card-like content container with border and shadow.
Props:
| Prop | Type | Default | Description |
| --------- | -------- | --------- | ---------------------- |
| bgColor | String | "white" | Background color class |
Slots: default — row content.
Example:
<template>
<BaseRow>
<p>Card content goes here</p>
</BaseRow>
</template>
<script setup lang="ts">
import { BaseRow } from 'mgv-backoffice'
</script>BaseSpinner
Animated loading spinner — a neutral ring with a coloured leading arc.
Props:
| Prop | Type | Default | Description |
|---------|----------------------------------------------------------------------------|----------|-------------------------------------------------------------|
| size | 'sm' \| 'md' \| 'lg' \| 'xl' | 'sm' | Diameter + ring thickness — 16 / 24 / 32 / 48px. |
| color | 'blue' \| 'emerald' \| 'sky' \| 'indigo' \| 'teal' \| 'purple' \| 'red' \| 'amber' | 'blue' | Colour of the spinning arc. The track stays neutral gray. |
With no props it renders the original 16px blue spinner, so existing call sites are unaffected.
Example:
<template>
<!-- legacy default -->
<BaseSpinner />
<!-- larger, themed -->
<BaseSpinner size="lg" color="emerald" />
</template>
<script setup lang="ts">
import { BaseSpinner } from 'mgv-backoffice'
</script>BaseToast
Toast notification with positioning and auto-dismiss.
Props:
| Prop | Type | Default | Description |
| -------------- | ---------------- | --------- | ------------------------------------------ |
| mode | BaseToastEnum | required | Toast variant (SUCCESS, WARNING, ERROR) |
| description | String | required | Message text |
| hasCloseIcon | Boolean | true | Show close button |
| positioning | String | 'right' | Screen position (use PositioningEnum) |
Example:
<template>
<BaseToast
:mode="BaseToastEnum.SUCCESS"
description="Changes saved successfully!"
:positioning="PositioningEnum.TOP_RIGHT"
/>
</template>
<script setup lang="ts">
import { BaseToast, BaseToastEnum, PositioningEnum } from 'mgv-backoffice'
</script>ColoredSquares
Colored square indicator with randomized pastel accent.
Props:
| Prop | Type | Default | Description |
| ------- | -------- | ------- | --------------------------------- |
| color | String | — | Color variant (use ColorsEnums) |
Slots: default — label content.
Example:
<template>
<ColoredSquares :color="ColorsEnums.BLUE">Category A</ColoredSquares>
</template>
<script setup lang="ts">
import { ColoredSquares, ColorsEnums } from 'mgv-backoffice'
</script>EarningsCard
Earnings summary card with formatted currency display. Supports a signed P&L mode that renders a red loss theme (and a downward trend glyph) for negative amounts.
Props:
| Prop | Type | Default | Description |
| ---------- | -------- |-------------------------|----------------------|
| title | String | 'TOTAL EARNINGS' | Card heading |
| amount | Number | 0 | Monetary value (a stringified number is coerced) |
| subtitle | String | 'Lifetime commission' | Subheading text |
| badge | String | '' | Optional badge label |
| currency | String | '$' | Currency symbol |
| decimals | Number | 2 | Fraction digits shown for the amount |
| accent | 'orange' \| 'emerald' \| 'red' | 'orange' | Card theme. emerald tints it green; red is the loss theme. |
| signed | Boolean | false | Treat amount as a signed P&L figure: a negative value automatically switches to the red loss theme and flips the trend glyph to point down; a non-negative value keeps the chosen accent and the upward glyph. |
Example:
<template>
<!-- Always-positive total: original behaviour. -->
<EarningsCard
title="Monthly Revenue"
:amount="12500"
subtitle="April 2026"
currency="€"
/>
<!-- Signed P&L: renders red + a down arrow when the amount is negative. -->
<EarningsCard
title="TOTAL P&L"
:amount="-128.4"
subtitle="Realised + unrealised"
accent="emerald"
signed
/>
</template>
<script setup lang="ts">
import { EarningsCard } from 'mgv-backoffice'
</script>EuroAmount
Formatted euro currency display with conditional color coding.
Props:
| Prop | Type | Default | Description |
| -------------- | --------- | ------- |-------------------------------------------------|
| amount | Number | — | Value to display |
| beforeAmount | Number | null | Previous value (green if amount > beforeAmount) |
| showCurrency | Boolean | true | Show euro symbol |
Example:
<template>
<!-- Shows green (amount > beforeAmount) -->
<EuroAmount :amount="1500" :beforeAmount="1200" />
<!-- Shows red (negative) -->
<EuroAmount :amount="-300" />
<!-- Without currency symbol -->
<EuroAmount :amount="800" :showCurrency="false" />
</template>
<script setup lang="ts">
import { EuroAmount } from 'mgv-backoffice'
</script>Pagination
Page navigation with smart ellipsis for large page counts.
Props:
| Prop | Type | Default | Description |
| -------------- | -------- | ------- | ----------------------- |
| totalItems | Number | 10 | Total number of items |
| itemsPerPage | Number | 20 | Items shown per page |
Events:
| Event | Payload | Description |
| -------------- | -------- |----------------------------------|
| page-changed | Number | Emitted with the new page number |
Example:
<template>
<Pagination
:totalItems="200"
:itemsPerPage="10"
@page-changed="onPageChange"
/>
</template>
<script setup lang="ts">
import { Pagination } from 'mgv-backoffice'
const onPageChange = (page: number) => {
console.log('Page:', page)
}
</script>TrendArrow
Up/down trend indicator displayed as a colored badge.
Props:
| Prop | Type | Default | Description |
| -------- | -------- | ------- |------------------------------------------------------|
| number | Number | — | Positive = green arrow up, negative = red arrow down, zero = neutral gray dash |
| icon | String | — | Optional suffix appended after the number (e.g. "%") |
Example:
<template>
<TrendArrow :number="12.5" /> <!-- Green up arrow -->
<TrendArrow :number="-3.2" /> <!-- Red down arrow -->
<TrendArrow :number="0" /> <!-- Neutral gray dash -->
</template>
<script setup lang="ts">
import { TrendArrow } from 'mgv-backoffice'
</script>Enums
All enums are importable directly from the package:
import {
AlertEnum,
BaseBadgeEnum,
BaseButtonEnum,
BaseButtonSizeEnum,
BaseLogoEnum,
BaseModalEnum,
BaseToastEnum,
ColorsEnums,
LineEnum,
PositioningEnum
} from 'mgv-backoffice'| Enum | Values |
| -------------------- |---------------------------------------------------------------|
| AlertEnum | WARNING, ERROR, SUCCESS, INFO |
| BaseBadgeEnum | WIN, LOSE |
| BaseButtonEnum | RED, BLUE, WHITE, DARK, GREEN, EMERALD, YELLOW, PURPLE, SKY, GRAY, AMBER |
| BaseButtonSizeEnum | EXTRA_SMALL, SMALL, BASE, LARGE, EXTRA_LARGE |
| BaseLogoEnum | SMALL, MEDIUM, LARGE (BaseLoginEnum is a deprecated alias) |
| BaseModalEnum | DELETE, SUCCESS |
| BaseToastEnum | SUCCESS, WARNING, ERROR |
| ColorsEnums | NONE, RED, YELLOW, BLACK, GRAY, GREEN, BLUE |
| LineEnum | BASE, BASE_SHORTER, SQUARE |
| PositioningEnum | TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT |
Types
import type { BreadCrumb, PnL, PnLInputs } from 'mgv-backoffice'| Type | Shape |
| ---------------- | -------------------------------------- |
| BreadCrumb | { name: string; url: string } |
| DropdownOption | { value: string \| number; label: string; title?: string; disabled?: boolean } |
| PnLInputs | { buyPrice; lastPrice; filledQty } (each number \| string \| null \| undefined) |
| PnL | { pnlUsd: number \| null; pnlPct: number \| null } |
Utilities
import { getBaseColor, getBaseColorOf } from 'mgv-backoffice'| Function | Signature | Returns |
| ---------------- | ---------------------------------- | ----------------------------------- |
| getBaseColor | (c: AlertEnum) => string | Tailwind color name for alert type |
| getBaseColorOf | (c: ColorsEnums) => string | Tailwind color name for color enum |
HTTP colours
import {
methodBadgeSolid,
methodBadgeBright,
statusBadgeSolid,
statusBadgeTinted,
} from 'mgv-backoffice'Tailwind class helpers for HTTP method and status code badges. Solid variants
return saturated bg-*-600 classes for use on neutral surfaces; Bright /
Tinted variants return softer combinations suitable for cards. statusBadgeTinted
takes (status, isDark) to adapt between themes.
HTML sanitizer
import { sanitizeHtml, isSafeHref } from 'mgv-backoffice'| Function | Signature | Returns |
| -------------- | ----------------------------------------------- | ------- |
| sanitizeHtml | (raw: string \| undefined \| null) => string | Allow-list–sanitised HTML safe for v-html. |
| isSafeHref | (value: string) => boolean | true if the href uses a safe scheme (http/https/mailto/tel, root-relative, or anchor). |
Allow-list sanitizer for strings bound into v-html. Keeps a small set of
formatting tags (a, b/strong, i/em, code, pre, p, ul/ol/li,
span, div, br), strips all other elements (unwrapping to text, or dropping
content entirely for script/style/iframe/etc.), removes every attribute
except href/title on anchors, rejects unsafe href schemes
(javascript:/data:/vbscript:/file:), and hardens surviving links with
rel="noopener noreferrer" target="_blank". Browser-only (uses DOMParser).
sanitizeHtml('<p>Hi<script>alert(1)<\/script></p>') // '<p>Hi</p>'Display formatters
import {
fmtNumber,
fmtDate,
fmtDateTime,
fmtDateShort,
fmtPrice,
fmtPct,
fmtUsd,
} from 'mgv-backoffice'Locale-aware, pure, dependency-free formatters for tables, logs and charts. They handle missing/non-finite input gracefully (rendering an em-dash) so raw API values can be passed without pre-sanitising.
| Function | Signature | Returns |
| -------------- | --------------------------------------------------------------- | ------- |
| fmtNumber | (n: number \| string \| null \| undefined, digits = 4) => string | Fixed-fraction number; em-dash for null/undefined/non-finite. Accepts numeric strings. |
| fmtDate | (s: string \| number \| null \| undefined) => string | Locale date-time from ISO string or epoch; em-dash on empty, raw value on parse failure. |
| fmtDateTime | (ms: number) => string | Compact "Mon D, HH:MM" label from epoch-millis (chart axes/tooltips). |
| fmtDateShort | (ms: number) => string | Short "Mon D" calendar label from epoch-millis. |
| fmtPrice | (n: number) => string | Price with precision that scales to magnitude (more decimals for sub-cent values). |
| fmtPct | (n: number, digits = 2) => string | Percentage with explicit sign, e.g. "+2.50%". |
| fmtUsd | (v: number) => string | Signed USD amount with leading sign, e.g. "+$5.00". |
Profit & loss
import { computePnL } from 'mgv-backoffice'
import type { PnL, PnLInputs } from 'mgv-backoffice'| Function | Signature | Returns |
| ------------ | ------------------------------- | ------- |
| computePnL | (row: PnLInputs) => PnL | Unrealised mark-to-market PnL in absolute USD and percent. Returns { pnlUsd: null, pnlPct: null } when any input is missing, non-finite, or buyPrice <= 0. |
interface PnLInputs {
buyPrice: number | string | null | undefined
lastPrice: number | string | null | undefined
filledQty: number | string | null | undefined
}
interface PnL {
pnlUsd: number | null
pnlPct: number | null
}computePnL({ buyPrice: 100, lastPrice: 110, filledQty: 5 })
// { pnlUsd: 50, pnlPct: 10 }Layout & shells (Tier 2 — full backoffice chrome)
BaseAppLayout
Root layout: dark/light page background, skip link, <main>-with-inert wrapper.
The <main> content offset tracks the sidebar width automatically —
lg:ml-60 when expanded, lg:ml-16 when collapsed (via useSidebarCollapse()).
Props:
| Prop | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| showSidebar | Boolean | true | Render the sidebar slot. Set false for full-bleed pages. |
| skipLinkLabel | String | 'Skip to main content' | Label for the accessibility skip link. |
Slots: sidebar, default (page content).
<BaseAppLayout :show-sidebar="route.name !== 'presentation'">
<template #sidebar><AppSidebar /></template>
<RouterView />
</BaseAppLayout>BaseSidebar
Responsive sidebar with desktop fixed-positioning and mobile off-canvas behavior, focus management, optional theme toggle, a desktop collapse toggle (icon-only rail), an optional notifications bell, and configurable nav sections.
Props:
| Prop | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| sections | NavSection[] | required | Grouped nav items. |
| homeRouteName | String | 'home' | Route name for the logo / "go home" click. |
| appName | String | '' | Optional app name in the footer. |
| version | String | '' | Optional version string in the footer. |
| showThemeToggle | Boolean | true | Toggle the dark/light switch in the footer. |
| collapsible | Boolean | true | Show the desktop collapse toggle that shrinks the sidebar to an icon-only rail. |
| showNotifications | Boolean | false | Show the notifications bell (with unread badge) that toggles BaseNotificationPanel. |
Collapse state is shared via
useSidebarCollapse()(and persisted to localStorage) soBaseAppLayoutcan shrink the content offset fromlg:ml-60tolg:ml-16in step with the rail. Collapsing only affects desktop (lg+); on mobile the sidebar stays a full off-canvas panel.
Notifications: set
:show-notifications="true"to render the bell, then drop aBaseNotificationPanelin your app. Both share state throughuseNotifications(), so the unread badge and the panel stay in sync.
Slots:
| Slot | Slot props | Description |
| -------- | ---------- | ----------- |
| logo | { size } | Brand logo. Receives a size hint (28px in mobile bar, 52px in sidebar). |
| status | — | Footer status row (e.g. health indicator, sync state). |
| footer | — | Replaces the default appName v0 line. |
Types:
import type { NavItem, NavSection } from 'mgv-backoffice'
interface NavItem {
name: string // Vue Router route name
label: string // display text
icon: Component // typically a Heroicon
}
interface NavSection {
title: string
items: NavItem[]
}<BaseSidebar :sections="navSections" home-route-name="projects" app-name="WireMate UI" :version="appVersion">
<template #logo="{ size }"><WireMateLogo :size="size" /></template>
<template #status>
<HealthIndicator />
</template>
</BaseSidebar>BaseNotificationPanel
Left-anchored notification drawer (teleported to <body>, slides in from
the left, backdrop + Escape to close). Open/close state and the list live
in useNotifications(), so the sidebar bell and the panel stay in sync.
Enable the bell on the sidebar with :show-notifications="true", drop one
<BaseNotificationPanel /> anywhere in your app, and feed it data via the
composable.
Props:
| Prop | Type | Default | Description |
| ----------------- | --------- | ------------------------------ | ----------- |
| title | String | 'Notifications' | Panel heading. |
| emptyText | String | 'You have no notifications.' | Shown when the list is empty. |
| showMarkAllRead | Boolean | true | Render the "Mark all as read" action when there are unread items. |
Emits: select (the clicked notification's id; the row is also marked read).
<script setup lang="ts">
import { BaseNotificationPanel, useNotifications } from 'mgv-backoffice'
const { setNotifications } = useNotifications()
setNotifications([
{ id: 1, title: 'New comment', message: 'Alice replied to your post', time: '2m ago', type: 'info' },
{ id: 2, title: 'Build passed', time: '1h ago', read: true, type: 'success' },
])
</script>
<template>
<BaseSidebar :sections="navSections" :show-notifications="true" />
<BaseNotificationPanel @select="(id) => goTo(id)" />
</template>import type { NotificationItem } from 'mgv-backoffice'
interface NotificationItem {
id: string | number
title: string
message?: string
time?: string // pre-formatted by you
read?: boolean
type?: 'info' | 'success' | 'warning' | 'error' // status dot colour
}Authentication
BaseGoogleSignInButton
Google-branded "Sign in with Google" button (official multi-colour "G",
dark-mode surface swap). Purely presentational — it runs no OAuth itself;
listen on click and start your own Google Identity / Firebase / backend
flow there.
Props:
| Prop | Type | Default | Description |
| ---------- | --------- | -------------------------- | ----------- |
| label | String | 'Sign in with Google' | Button text. |
| loading | Boolean | false | Disables and shows a spinner. |
| disabled | Boolean | false | Disables without the spinner. |
| block | Boolean | true | Full-width layout. |
Emits: click (only when not disabled/loading).
BaseLoginForm
Presentational sign-in card: email + password (with show/hide), an optional
"Remember me" checkbox, an error banner, the Google button + "or" divider,
and logo / forgot / footer slots. Owns its input state and emits
submit / google-sign-in; the app handles the actual request and feeds
back loading / error.
Props:
| Prop | Type | Default | Description |
| --------------- | --------- | ----------- | ----------- |
| title | String | 'Sign in' | Card heading. |
| subtitle | String | '' | Muted line under the heading. |
| submitLabel | String | 'Sign in' | Submit button text. |
| loading | Boolean | false | Disables the form, spinner on submit. |
| googleLoading | Boolean | false | Disables the form, spinner on the Google button. |
| error | String | '' | Error banner above the form. |
| showGoogle | Boolean | true | Render the Google button + divider. |
| showRemember | Boolean | false | Render the "Remember me" checkbox. |
Emits: submit (LoginCredentials), google-sign-in.
Slots: logo, forgot (next to the password label), footer.
<script setup lang="ts">
import { BaseLoginForm } from 'mgv-backoffice'
import type { LoginCredentials } from 'mgv-backoffice'
async function onSubmit(creds: LoginCredentials) { /* call your API */ }
function onGoogle() { /* start Google OAuth */ }
</script>
<template>
<BaseLoginForm
subtitle="Welcome back"
:show-remember="true"
@submit="onSubmit"
@google-sign-in="onGoogle"
>
<template #logo><MyLogo /></template>
<template #forgot><a href="/forgot" class="text-sm text-emerald-600">Forgot?</a></template>
<template #footer>No account? <a href="/signup" class="text-emerald-600">Sign up</a></template>
</BaseLoginForm>
</template>Modals & sections
BaseModalShell
Shared modal chrome — Teleport to body, backdrop, themed card, escape key,
aria-modal. Compose this rather than building modals from scratch.
Props:
| Prop | Type | Default | Description |
| --------------- | --------- | ----------- | ----------- |
| title | String | required | Modal heading. |
| maxWidthClass | String | 'max-w-md' | Tailwind max-w utility for the card. |
| manualClose | Boolean | false | If true, backdrop click and Escape do NOT auto-emit cancel. |
| scrollable | Boolean | false | Switch to the large-content layout: a flex column capped at 90vh with a fixed header/footer and a scrolling body. |
| subtitle | String | '' | Muted line under the title (scrollable layout only). |
Slots: icon, default, footer, and (scrollable layout) header-actions — content on the right of the header, e.g. a close button.
Events: cancel, backdrop.
BaseConfirmModal
Confirmation dialog built on BaseModalShell. Variant chooses red (danger) or
amber (warning) styling.
Props: title, message, confirmText, cancelText, submittingText,
variant: 'danger' | 'warning', submitting.
Events: confirm, cancel.
BaseTextInputModal
"Ask the user for a single string and confirm" dialog. Preserves typed input on stray backdrop clicks; Escape always cancels.
Props: title, message, initialValue, placeholder, inputLabel,
confirmText, cancelText, submittingText, submitting.
Slots: icon — override the default emerald document icon.
Events: confirm(value: string), cancel.
BaseEntityPickerModal
Searchable "pick one from a list" dialog. Pass items directly or an async
loader that runs on mount.
Props: title, message?, items?: EntityPickerItem[],
loader?: () => Promise<EntityPickerItem[]>, excludeId?,
variant: 'emerald' | 'purple' | 'blue' | 'red' | 'amber',
searchPlaceholder, emptyMessage, noMatchMessage, confirmText,
cancelText, submittingText, submitting.
Events: confirm(itemId: string), cancel.
interface EntityPickerItem { id: string; label: string }BaseCollapsibleSection
Section wrapper with a clickable header, optional badge, and a default slot
for the body. Parent owns the collapsed state.
Props: title, collapsed, badge?, bodyClass?.
Events: toggle.
BaseNotFoundPage
Drop-in 404 view.
Props: code ('404'), message ('Page not found'),
homeRouteName ('home'), homeLabel ('Go home').
BasePageHeader
Page-level header: icon badge + title/subtitle on the left, action buttons on the right. Gives top-level views a consistent header shape and width.
Props:
| Prop | Type | Default | Description |
| --------------- | -------- | ------------- | ----------- |
| title | String | required | H1 text. |
| subtitle | String | '' | Muted line below the title. |
| iconColor | String | 'emerald' | Badge + icon colour: 'emerald' \| 'sky' \| 'red' \| 'amber'. |
| maxWidthClass | String | 'max-w-4xl' | Tailwind max-w utility constraining header width. |
Slots:
| Slot | Slot props | Description |
| --------- | ----------------- | ----------- |
| icon | { iconClass } | Page Heroicon. Bind :class="iconClass" for the theme-aware colour. |
| actions | — | Buttons rendered on the right (refresh, destructive, etc.). |
<template>
<BasePageHeader title="Request Journal" subtitle="Recent matched requests" icon-color="sky">
<template #icon="{ iconClass }">
<DocumentTextIcon class="w-5 h-5" :class="iconClass" />
</template>
<template #actions>
<BaseButton description="Refresh" @click="reload" />
</template>
</BasePageHeader>
</template>
<script setup lang="ts">
import { BasePageHeader, BaseButton } from 'mgv-backoffice'
import { DocumentTextIcon } from '@heroicons/vue/24/outline'
</script>BaseToolbarButton
Bordered toolbar button — the "Refresh / Delete All" row that sits under a page header. Optional leading icon (via slot) plus a label.
Props:
| Prop | Type | Default | Description |
| ---------- | --------- | ----------- | ----------- |
| label | String | '' | Button text. Omit for an icon-only button. |
| variant | String | 'neutral' | 'neutral' (grey) or 'danger' (solid red). |
| disabled | Boolean | false | Greys out and blocks the click. |
| title | String | undefined | Native tooltip / a11y text. |
| type | String | 'button' | Native button type. |
Slots:
| Slot | Slot props | Description |
| ------ | --------------- | ----------- |
| icon | { iconClass } | Leading Heroicon. Bind :class="iconClass" (w-4 h-4); add state classes as needed. |
Emits: click (native MouseEvent).
<template>
<BaseToolbarButton label="Refresh" :disabled="isLoading" title="Refresh" @click="reload">
<template #icon="{ iconClass }">
<ArrowPathIcon :class="[iconClass, { 'animate-spin': isLoading }]" />
</template>
</BaseToolbarButton>
<BaseToolbarButton label="Delete All" variant="danger" @click="deleteAll">
<template #icon="{ iconClass }">
<TrashIcon :class="iconClass" />
</template>
</BaseToolbarButton>
</template>BaseActionButton
Compact ghost action button — the colour-coded "Edit / Logs / Stub / Delete" actions on a card footer or action row. No border/fill at rest; a tinted hover background keyed to the semantic colour.
Props:
| Prop | Type | Default | Description |
| ----------- | --------- | ----------- | ----------- |
| label | String | '' | Button text. Omit for an icon-only button. |
| color | String | 'emerald' | 'emerald' \| 'sky' \| 'indigo' \| 'teal' \| 'purple' \| 'red' \| 'amber' \| 'amberStrong'. |
| disabled | Boolean | false | Dims via opacity and suppresses the hover tint. |
| fullWidth | Boolean | false | Stretch to fill its flex row (flex-1). |
| title | String | undefined | Native tooltip. |
| ariaLabel | String | undefined | Accessible label. |
| type | String | 'button' | Native button type. |
Slots:
| Slot | Slot props | Description |
| ------ | --------------- | ----------- |
| icon | { iconClass } | Leading Heroicon. Bind :class="iconClass" (w-4 h-4). |
Emits: click (native MouseEvent).
<template>
<BaseActionButton label="Edit" color="emerald" full-width title="Edit this mock" @click="edit">
<template #icon="{ iconClass }">
<PencilSquareIcon :class="iconClass" />
</template>
</BaseActionButton>
<BaseActionButton label="Delete" color="red" @click="remove">
<template #icon="{ iconClass }">
<TrashIcon :class="iconClass" />
</template>
</BaseActionButton>
</template>BaseCopyButton
Copy-to-clipboard icon button with transient "copied" feedback — clicks
write text to the clipboard, swap the clipboard icon for a checkmark
for resetMs, then revert. Uses the async Clipboard API with a
hidden-textarea execCommand fallback for insecure origins. Emits
copied / error so the parent can fire its own toast.
Props:
| Prop | Type | Default | Description |
| ----------- | --------- | --------- | ----------- |
| text | String | required | Value written to the clipboard. |
| label | String | '' | Used in the tooltip / aria-label (Copy {label}). |
| variant | String | 'ghost' | 'ghost' (borderless p-1 icon) or 'bordered' (w-9 h-9 boxed, turns emerald while copied). |
| resetMs | Number | 1500 | How long the checkmark stays before reverting. |
| iconClass | String | 'w-4 h-4' | Icon size class. |
Emits: copied, error(err).
<template>
<!-- Inline ID copy, parent fires the toast -->
<BaseCopyButton
:text="stub.id"
label="Stub ID"
@copied="showToastMessage('Stub ID copied to clipboard', BaseToastEnum.SUCCESS)"
@error="showToastMessage('Failed to copy stub ID', BaseToastEnum.ERROR)"
/>
<!-- Boxed copy next to a read-only input -->
<BaseCopyButton :text="mock.id" label="Mock ID" variant="bordered" :reset-ms="2000" />
</template>
<script setup lang="ts">
import { BaseCopyButton, BaseToastEnum } from 'mgv-backoffice'
</script>Forms & tables
These components use dark: Tailwind variants, so the consuming app must map
the dark variant to the .dark class that useTheme() toggles (see
Tailwind setup for consumers).
BaseInput
Themed text/number input carrying the shared field skin (slate border,
bg-slate-50 / dark bg-slate-900 surface). Everything else — placeholder,
id, disabled, step/min, extra classes like font-mono or
placeholder:* — falls through via attrs and Vue class merging.
Props:
| Prop | Type | Default | Description |
| ------------ | ------------------ | -------- | ----------- |
| modelValue | String \| Number | '' | v-model value. |
| type | String | 'text' | Native input type. |
| size | String | 'md' | 'md' = px-3 py-2, 'sm' = px-2 py-1.5. |
| block | Boolean | true | Full-width (w-full); set false for inline fields. |
Emits: update:modelValue(value: string) — always the raw string; parse
numbers in the owner.
<BaseInput v-model="query" placeholder="e.g. AMD or BTC" class="font-mono" />BaseSelect
Themed <select> sharing BaseInput's field skin. Options come from the
default slot so callers keep full control of <option> rendering.
Props:
| Prop | Type | Default | Description |
| ------------ | ------------------ | ------- | ----------- |
| modelValue | String \| Number | '' | v-model value. |
| size | String | 'sm' | 'sm' = px-2 py-1.5, 'md' = px-3 py-2. |
| block | Boolean | true | Full-width; set false for inline selects. |
Slots: default — the <option> elements.
Emits: update:modelValue(value: string).
<BaseSelect v-model="strategyType">
<option v-for="e in catalog" :key="e.type" :value="e.type" :title="e.description">
{{ e.label }}
</option>
</BaseSelect>BaseDropdown
Button-style single-select dropdown ("Select Social User ⌄"). Unlike
BaseSelect (a native <select>), this renders a trigger button plus a
floating menu, so the closed control shows a placeholder and a chevron that
rotates while open — matching the app's filter dropdowns. Selecting a row
emits its value and closes the menu; Escape and an outside click also close
it.
Props:
| Prop | Type | Default | Description |
| ------------- | -------------------------- | ------------ | ----------- |
| options | DropdownOption[] | required | { value, label, title?, disabled? } per row. |
| modelValue | String \| Number \| null | null | Selected option's value (v-model). |
| placeholder | String | 'Select' | Trigger text shown when nothing is selected. |
| size | String | 'md' | 'md' = px-4 py-2.5 (app filter height), 'sm' = px-3 py-2. |
| block | Boolean | true | Full-width; set false for an inline, content-width dropdown. |
| disabled | Boolean | false | Disables the trigger. |
| ariaLabel | String | '' | Accessible name for the trigger/listbox when there is no visible label. |
Emits: update:modelValue(value).
<BaseDropdown
v-model="socialUserId"
:options="socialUsers.map((u) => ({ value: u.id, label: u.name }))"
placeholder="Select Social User"
aria-label="Social user"
/>BaseSegmentedControl
Segmented button group ("All | Stock | Crypto"). One button per option; the
selected one gets the filled treatment and aria-pressed="true".
Props:
| Prop | Type | Default | Description |
| ------------- | ------------------- | -------- | ----------- |
| options | SegmentedOption[] | required | { value, label, title? } per button. |
| modelValue | String \| Number | required | Selected option's value (v-model). |
| variant | String | 'base' | 'base' (px-3 py-2, emerald-500 fill), 'wide' (px-4 py-2, emerald-600 fill), 'toolbar' (h-9 uppercase text-xs with focus-visible rings). |
| ariaLabel | String | '' | When set, the wrapper renders role="group" + aria-label. |
| optionClass | Function | — | (option, active) => string override for per-button fill classes (e.g. severity colours); layout stays owned by the variant. |
Emits: update:modelValue(value).
<BaseSegmentedControl v-model="assetFilter" :options="ASSET_FILTERS" />
<BaseSegmentedControl v-model="exchange" :options="EXCHANGES" variant="wide" aria-label="Exchange" />BaseTable
Styling shell for data tables — not a data grid. Owns the table skin
(slate header band, px-4 py-3 header cells, empty-state row); body rows are
the caller's own <tr> markup via the default slot. Wrap it yourself for
scrolling/card chrome (e.g. a BaseRow with overflow-x-auto).
Props:
| Prop | Type | Default | Description |
| ----------- | --------------- | ------------ | ----------- |
| columns | TableColumn[] | required | { label, align? }; align: 'right' right-aligns the header cell. |
| empty | Boolean | false | True renders the empty-state row spanning every column. |
| emptyText | String | 'No rows.' | Fallback empty-state text. |
Slots: default — the <tr> rows; empty — custom empty-state content.
<BaseTable :columns="COLUMNS" :empty="rows.length === 0">
<template #empty>No trades match your filters.</template>
<tr v-for="row in rows" :key="row.id" class="border-t border-slate-200 dark:border-slate-700">
…
</tr>
</BaseTable>BaseSpecFields
Spec-driven form fields: renders a select / checkbox / number input per
SpecField, with labels and help text, in a responsive two-column grid. Feed
it a backend-described catalogue and every form editing those values stays in
lockstep. Never mutates params — every edit is emitted as (key, value)
and the owner writes it back into its own state.
Props:
| Prop | Type | Default | Description |
| -------- | -------------------------------- | ------------ | ----------- |
| specs | SpecField[] | required | { key, label, type: 'decimal' \| 'integer' \| 'boolean' \| 'select', options?, step?, min?, help? }. |
| params | Record<string, SpecFieldValue> | required | Current values keyed by spec.key. |
Slots: after ({ spec }) — extra content under each field (e.g. a live
preview attached to one key).
Emits: update(key: string, value: SpecFieldValue) — numbers are parsed
(parseFloat); unparseable input passes through raw so the owner's
validation can catch it.
<BaseSpecFields :specs="entry.params" :params="form.params"
@update="(key, value) => (form.params[key] = value)" />Composables
import {
initTheme,
useTheme,
useThemeClasses,
useEscapeKey,
useDebouncedRef,
useToast,
useMobileSidebar,
useSidebarCollapse,
useNotifications,
} from 'mgv-backoffice'| Composable | Purpose |
| ---------- | ------- |
| initTheme({ storageKey? }) | Explicitly initialize the theme singleton. Call in your app entry point before mounting when you need a custom storage key — library components call useTheme() internally, so a component mounting first would otherwise lock in the default key (a dev-mode warning fires if that happens). |
| useTheme({ storageKey? }) | Singleton dark/light controller. Toggles <html class="dark"> and persists via localStorage (default key 'mgv-theme'). Prefer initTheme at app entry for custom keys. |
| useThemeClasses() | Named Tailwind class roles for dark/light (card, border, primaryText, mutedText, dimText, input, ghostButton, emeraldText, redText, …). Returns computed refs auto-unwrapped in templates. |
| useEscapeKey(handler) | Component-scoped Escape key listener. |
| useDebouncedRef(source, delay?) | Debounced mirror of a ref. Timer cleared on scope dispose. |
| useToast(durationMs?) | Per-component toast state: { showToast, toastMessage, toastType, showToastMessage }. |
| useMobileSidebar() | Singleton state shared between BaseSidebar and BaseAppLayout for the off-canvas open/closed flag. |
| useSidebarCollapse({ storageKey? }) | Singleton collapsed/expanded state for the desktop sidebar rail, shared between BaseSidebar and BaseAppLayout and persisted to localStorage (default key 'mgv-sidebar-collapsed'). |
| useNotifications() | Singleton notification state shared by the sidebar bell and BaseNotificationPanel: { notifications, unreadCount, open, openPanel, closePanel, togglePanel, setNotifications, add, remove, markRead, markAllRead, clear }. |
Tailwind setup for consumers
The lib's components rely on Tailwind utility classes (including dark-mode
variants). Consumers should add the lib's dist output to their Tailwind
content paths so the JIT can see the class names:
// tailwind.config.js
export default {
content: [
'./index.html',
'./src/**/*.{vue,ts}',
'./node_modules/mgv-backoffice/dist/**/*.{js,mjs,cjs,vue}',
],
}The legacy tailwind.safelist.js only covers the v1 components; the
recommended path for v4+ is the content glob above.
