npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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-backoffice

Peer 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-router 5.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.js is the single source of truth. tailwind.safelist.css is generated from it via npm run safelist (runs automatically before npm 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 BaseConfirmModal for confirm/cancel flows or BaseModalShell for 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) so BaseAppLayout can shrink the content offset from lg:ml-60 to lg:ml-16 in 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 a BaseNotificationPanel in your app. Both share state through useNotifications(), 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.