mgv-backoffice 1.46.0 → 1.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,2048 +1,2082 @@
1
- # mgv-backoffice
2
-
3
- Shared Vue 3 UI component library built with TypeScript and Tailwind CSS.
4
-
5
- ## Installation
6
-
7
- ```bash
8
- npm install mgv-backoffice
9
- ```
10
-
11
- ### Peer Dependencies
12
-
13
- These must be installed in your project:
14
-
15
- ```bash
16
- npm install vue@^3.5.0 vue-router@^5.0.0 @heroicons/vue@^2.0.0
17
- ```
18
-
19
- > `vue-router` 5.x is required (peer range `^5.0.0`) — it's what the library is
20
- > developed and tested against. Upgrade from Router 4 before installing this library.
21
-
22
- ### Import Styles
23
-
24
- Include the library's stylesheet in your app entry point:
25
-
26
- ```ts
27
- import 'mgv-backoffice/dist/style.css'
28
- ```
29
-
30
- ### Tailwind Safelist
31
-
32
- If your project uses Tailwind, import the safelist so dynamic classes used by this library are generated correctly:
33
-
34
- ```js
35
- // In your Tailwind config
36
- import safelist from 'mgv-backoffice/tailwind.safelist'
37
- ```
38
-
39
- Or include the pre-built CSS safelist:
40
-
41
- ```css
42
- @import 'mgv-backoffice/tailwind.safelist.css';
43
- ```
44
-
45
- > Maintainers: `tailwind.safelist.js` is the single source of truth.
46
- > `tailwind.safelist.css` is generated from it via `npm run safelist`
47
- > (runs automatically before `npm run build`) — don't edit it by hand.
48
-
49
- ---
50
-
51
- ## Components
52
-
53
- ### BaseAlert
54
-
55
- Inline notice panel with color-coded variants (error / warning / success /
56
- info), theme-aware via the shared `isDark` ref. The optional default slot
57
- renders body content under the title, and `compact` gives a slim text-xs
58
- variant for in-form warnings.
59
-
60
- **Props:**
61
-
62
- | Prop | Type | Default | Description |
63
- | --------- | ----------- | ------------------ | ------------------------ |
64
- | `title` | `String` | `''` | Bold headline (optional when the slot carries the message). |
65
- | `color` | `AlertEnum` | `AlertEnum.ERROR` | Alert color variant. |
66
- | `compact` | `Boolean` | `false` | Slim variant: text-xs, smaller icon/padding. |
67
-
68
- **Slots:** default — body content rendered under the title.
69
-
70
- **Example:**
71
-
72
- ```vue
73
- <template>
74
- <BaseAlert title="Operation successful" :color="AlertEnum.SUCCESS" />
75
- <BaseAlert title="Proxying is active." :color="AlertEnum.WARNING">
76
- <p class="mt-0.5 text-xs opacity-90">The canned response below is ignored.</p>
77
- </BaseAlert>
78
- <BaseAlert compact :color="AlertEnum.WARNING">
79
- Chunked dribble is ignored while Fault Simulation is active.
80
- </BaseAlert>
81
- </template>
82
-
83
- <script setup lang="ts">
84
- import { BaseAlert, AlertEnum } from 'mgv-backoffice'
85
- </script>
86
- ```
87
-
88
- ---
89
-
90
- ### BaseBadge
91
-
92
- Colored status badge/pill. Renders nothing when the default slot is empty;
93
- an omitted or unrecognized `color` falls back to the red palette.
94
-
95
- **Props:**
96
-
97
- | Prop | Type | Default | Description |
98
- | ------- | -------- | ------- | --------------------------------- |
99
- | `color` | `String` | — | Color variant (use `ColorsEnums`) |
100
-
101
- **Slots:** `default` — badge label content.
102
-
103
- **Example:**
104
-
105
- ```vue
106
- <template>
107
- <BaseBadge :color="ColorsEnums.GREEN">Active</BaseBadge>
108
- <BaseBadge :color="ColorsEnums.RED">Inactive</BaseBadge>
109
- </template>
110
-
111
- <script setup lang="ts">
112
- import { BaseBadge, ColorsEnums } from 'mgv-backoffice'
113
- </script>
114
- ```
115
-
116
- ---
117
-
118
- ### BaseBreadcrumb
119
-
120
- Breadcrumb navigation. Provide items manually or pass a URL path for
121
- auto-generation; with neither prop it auto-generates from
122
- `window.location.pathname`. A "Home" crumb linking to `/` is always prepended.
123
-
124
- **Props:**
125
-
126
- | Prop | Type | Default | Description |
127
- | ------- | --------------- | ----------- | ------------------------------------------ |
128
- | `items` | `BreadCrumb[]` | `undefined` | Manual breadcrumb entries |
129
- | `path` | `String` | `undefined` | URL path for auto-generated breadcrumbs |
130
-
131
- **BreadCrumb type:**
132
-
133
- ```ts
134
- interface BreadCrumb {
135
- name: string
136
- url: string
137
- }
138
- ```
139
-
140
- **Example:**
141
-
142
- ```vue
143
- <template>
144
- <!-- Manual -->
145
- <BaseBreadcrumb :items="[
146
- { name: 'Home', url: '/' },
147
- { name: 'Users', url: '/users' },
148
- { name: 'Profile', url: '/users/1' }
149
- ]" />
150
-
151
- <!-- Auto-generated from path -->
152
- <BaseBreadcrumb path="/users/settings/profile" />
153
- </template>
154
-
155
- <script setup lang="ts">
156
- import { BaseBreadcrumb } from 'mgv-backoffice'
157
- import type { BreadCrumb } from 'mgv-backoffice'
158
- </script>
159
- ```
160
-
161
- ---
162
-
163
- ### BaseButton
164
-
165
- Button with color, size, loading state, and Vue Router integration.
166
-
167
- **Props:**
168
-
169
- | Prop | Type | Default | Description |
170
- | ------------- | ----------------------------------- | --------------------- | ------------------------------------ |
171
- | `description` | `String` | **required** | Button label text |
172
- | `color` | `String` | `BaseButtonEnum.GREEN` | Color variant (`BLUE`/`WHITE`/`DARK`/`GREEN`/`EMERALD`/`RED`/`YELLOW`/`PURPLE`/`SKY`/`GRAY`/`AMBER`) |
173
- | `outline` | `Boolean` | `false` | Outlined/secondary style — transparent fill, coloured text + border, tinted hover (theme-aware) |
174
- | `ghost` | `Boolean` | `false` | Ghost/borderless style — no border or fill, coloured text + tinted hover (theme-aware). For compact toolbar/action buttons |
175
- | `to` | `String` | — | Vue Router path (renders `<router-link>`) |
176
- | `type` | `'button' \| 'submit' \| 'reset'` | `'button'` | HTML button type |
177
- | `iconLeft` | `Boolean` | `false` | Render the slot icon before the label |
178
- | `isRounded` | `Boolean` | — | Fully rounded corners |
179
- | `isDisable` | `Boolean` | — | Disabled state |
180
- | `size` | `String` | — | Size variant (use `BaseButtonSizeEnum`) |
181
- | `isLoading` | `Boolean` | — | Show loading spinner |
182
-
183
- **Slots:** `default`
184
-
185
- **Example:**
186
-
187
- ```vue
188
- <template>
189
- <BaseButton description="Submit" :color="BaseButtonEnum.GREEN" type="submit" />
190
- <BaseButton description="Go to Users" :to="'/users'" />
191
- <BaseButton description="Saving..." :isLoading="true" :isDisable="true" />
192
- <BaseButton
193
- description="Delete"
194
- :color="BaseButtonEnum.RED"
195
- :size="BaseButtonSizeEnum.SMALL"
196
- />
197
- <!-- Outlined / secondary -->
198
- <BaseButton description="Import" :color="BaseButtonEnum.EMERALD" outline iconLeft>
199
- <ArrowUpTrayIcon class="w-4 h-4 mr-1.5" />
200
- </BaseButton>
201
- <!-- Ghost / borderless toolbar action -->
202
- <BaseButton description="Logs" :color="BaseButtonEnum.SKY" ghost iconLeft :size="BaseButtonSizeEnum.SMALL">
203
- <ClipboardDocumentListIcon class="w-4 h-4 mr-1.5" />
204
- </BaseButton>
205
- </template>
206
-
207
- <script setup lang="ts">
208
- import { BaseButton, BaseButtonEnum, BaseButtonSizeEnum } from 'mgv-backoffice'
209
- </script>
210
- ```
211
-
212
- ---
213
-
214
- ### BaseLine
215
-
216
- Horizontal divider with style variants.
217
-
218
- **Props:**
219
-
220
- | Prop | Type | Default | Description |
221
- | ------ | -------- | --------------- | ----------------- |
222
- | `mode` | `String` | `LineEnum.BASE` | Divider style |
223
-
224
- **Example:**
225
-
226
- ```vue
227
- <template>
228
- <BaseLine />
229
- <BaseLine :mode="LineEnum.SQUARE" />
230
- </template>
231
-
232
- <script setup lang="ts">
233
- import { BaseLine, LineEnum } from 'mgv-backoffice'
234
- </script>
235
- ```
236
-
237
- ---
238
-
239
- ### BaseLogo
240
-
241
- SVG brand logo component.
242
-
243
- **Props:**
244
-
245
- | Prop | Type | Default | Description |
246
- | ------ | -------- | ---------------------- | ------------ |
247
- | `size` | `String` | `BaseLogoEnum.MEDIUM` | Logo size |
248
-
249
- **Example:**
250
-
251
- ```vue
252
- <template>
253
- <BaseLogo :size="BaseLogoEnum.LARGE" />
254
- </template>
255
-
256
- <script setup lang="ts">
257
- import { BaseLogo, BaseLogoEnum } from 'mgv-backoffice'
258
- </script>
259
- ```
260
-
261
- ---
262
-
263
- ### BaseModal
264
-
265
- > ⚠️ **Deprecated.** Prefer [`BaseConfirmModal`](#baseconfirmmodal) for confirm/cancel
266
- > flows or [`BaseModalShell`](#basemodalshell) for custom dialogs — they support dark
267
- > mode, teleport to `<body>`, and slot-based composition. Kept for backward
268
- > compatibility.
269
-
270
- Confirmation dialog with support for delete and success modes. `DELETE` mode renders
271
- the confirm/cancel pair; any other mode renders the title, optional `description`,
272
- the default slot, and a single OK button that emits `closeModal`.
273
-
274
- **Props:**
275
-
276
- | Prop | Type | Default | Description |
277
- | ------------- | -------- | ----------- |-------------------------------------|
278
- | `title` | `String` | **required**| Modal heading |
279
- | `description` | `String` | — | Body text |
280
- | `to` | `String` | `"/"` | Unused — declared for backward compatibility only; confirm just emits `confirmModal`, no navigation happens |
281
- | `mode` | `String` | `'SUCCESS'` | Modal variant (use `BaseModalEnum`) |
282
-
283
- **Events:**
284
-
285
- | Event | Description |
286
- | -------------- | ------------------------------- |
287
- | `closeModal` | Emitted when modal is dismissed (cancel/OK button, backdrop click, or Escape) |
288
- | `confirmModal` | Emitted on confirm action |
289
-
290
- **Example:**
291
-
292
- ```vue
293
- <template>
294
- <BaseModal
295
- title="Delete this item?"
296
- description="This action cannot be undone."
297
- :mode="BaseModalEnum.DELETE"
298
- @closeModal="showModal = false"
299
- @confirmModal="handleDelete"
300
- />
301
- </template>
302
-
303
- <script setup lang="ts">
304
- import { ref } from 'vue'
305
- import { BaseModal, BaseModalEnum } from 'mgv-backoffice'
306
-
307
- const showModal = ref(true)
308
- const handleDelete = () => { /* ... */ }
309
- </script>
310
- ```
311
-
312
- ---
313
-
314
- ### BaseRow
315
-
316
- Card-like content container with border and shadow.
317
-
318
- **Props:**
319
-
320
- | Prop | Type | Default | Description |
321
- | --------- | -------- | --------- | ---------------------- |
322
- | `bgColor` | `String` | `"bg-white"` | Background color — a full Tailwind class (e.g. `"bg-slate-50"`), not a bare color name |
323
-
324
- **Slots:** `default` — row content.
325
-
326
- **Example:**
327
-
328
- ```vue
329
- <template>
330
- <BaseRow>
331
- <p>Card content goes here</p>
332
- </BaseRow>
333
- </template>
334
-
335
- <script setup lang="ts">
336
- import { BaseRow } from 'mgv-backoffice'
337
- </script>
338
- ```
339
-
340
- ---
341
-
342
- ### BaseSpinner
343
-
344
- Animated loading spinner — a neutral ring with a coloured leading arc.
345
-
346
- **Props:**
347
-
348
- | Prop | Type | Default | Description |
349
- |---------|----------------------------------------------------------------------------|----------|-------------------------------------------------------------|
350
- | `size` | `'sm' \| 'md' \| 'lg' \| 'xl'` | `'sm'` | Diameter + ring thickness — 16 / 24 / 32 / 48px. |
351
- | `color` | `'blue' \| 'emerald' \| 'sky' \| 'indigo' \| 'teal' \| 'purple' \| 'red' \| 'amber'` | `'emerald'` | Colour of the spinning arc. The track stays neutral gray. |
352
-
353
- With no props it renders a 16px emerald spinner. For page loaders use `size="xl"` centered, e.g. `<div class="flex justify-center py-12"><BaseSpinner size="xl" /></div>`.
354
-
355
- **Example:**
356
-
357
- ```vue
358
- <template>
359
- <!-- legacy default -->
360
- <BaseSpinner />
361
- <!-- larger, themed -->
362
- <BaseSpinner size="lg" color="emerald" />
363
- </template>
364
-
365
- <script setup lang="ts">
366
- import { BaseSpinner } from 'mgv-backoffice'
367
- </script>
368
- ```
369
-
370
- ---
371
-
372
- ### BaseToast
373
-
374
- Toast notification with positioning and auto-dismiss.
375
-
376
- **Props:**
377
-
378
- | Prop | Type | Default | Description |
379
- | -------------- | ---------------- | --------- | ------------------------------------------ |
380
- | `mode` | `BaseToastEnum` | **required** | Toast variant (SUCCESS, WARNING, ERROR) |
381
- | `description` | `String` | **required** | Message text |
382
- | `hasCloseIcon` | `Boolean` | `true` | Show close button |
383
- | `positioning` | `String` | `PositioningEnum.TOP_RIGHT` | Screen position (use `PositioningEnum`) |
384
- | `duration` | `Number` | `5000` | Auto-dismiss delay in milliseconds |
385
-
386
- **Example:**
387
-
388
- ```vue
389
- <template>
390
- <BaseToast
391
- :mode="BaseToastEnum.SUCCESS"
392
- description="Changes saved successfully!"
393
- :positioning="PositioningEnum.TOP_RIGHT"
394
- />
395
- </template>
396
-
397
- <script setup lang="ts">
398
- import { BaseToast, BaseToastEnum, PositioningEnum } from 'mgv-backoffice'
399
- </script>
400
- ```
401
-
402
- ---
403
-
404
- ### ColoredSquares
405
-
406
- Colored square indicator with randomized pastel accent.
407
-
408
- **Props:**
409
-
410
- | Prop | Type | Default | Description |
411
- | ------- | -------- | ------- | --------------------------------- |
412
- | `color` | `String` | — | Color variant (use `ColorsEnums`) |
413
-
414
- **Slots:** `default` — label content.
415
-
416
- **Example:**
417
-
418
- ```vue
419
- <template>
420
- <ColoredSquares :color="ColorsEnums.BLUE">Category A</ColoredSquares>
421
- </template>
422
-
423
- <script setup lang="ts">
424
- import { ColoredSquares, ColorsEnums } from 'mgv-backoffice'
425
- </script>
426
- ```
427
-
428
- ---
429
-
430
- ### EarningsCard
431
-
432
- Earnings summary card with formatted currency display. Supports a signed P&L
433
- mode that renders a red loss theme (and a downward trend glyph) for negative
434
- amounts.
435
-
436
- **Props:**
437
-
438
- | Prop | Type | Default | Description |
439
- | ---------- | -------- |-------------------------|----------------------|
440
- | `title` | `String` | `'TOTAL EARNINGS'` | Card heading |
441
- | `amount` | `Number` | `0` | Monetary value (a stringified number is coerced) |
442
- | `subtitle` | `String` | `'Lifetime commission'` | Subheading text |
443
- | `badge` | `String` | `''` | Optional badge label |
444
- | `currency` | `String` | `'$'` | Currency symbol |
445
- | `decimals` | `Number` | `2` | Fraction digits shown for the amount |
446
- | `accent` | `'orange' \| 'emerald' \| 'red'` | `'orange'` | Card theme. `emerald` tints it green; `red` is the loss theme. |
447
- | `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. |
448
- | `compact` | `Boolean` | `false` | Dense variant for dashboards that tile many cards on one row: tighter padding, smaller type, trend glyph shrunk into the top-right corner. Don't combine with `badge` — both occupy the top-right corner. |
449
-
450
- **Example:**
451
-
452
- ```vue
453
- <template>
454
- <!-- Always-positive total: original behaviour. -->
455
- <EarningsCard
456
- title="Monthly Revenue"
457
- :amount="12500"
458
- subtitle="April 2026"
459
- currency="€"
460
- />
461
-
462
- <!-- Signed P&L: renders red + a down arrow when the amount is negative. -->
463
- <EarningsCard
464
- title="TOTAL P&L"
465
- :amount="-128.4"
466
- subtitle="Realised + unrealised"
467
- accent="emerald"
468
- signed
469
- />
470
- </template>
471
-
472
- <script setup lang="ts">
473
- import { EarningsCard } from 'mgv-backoffice'
474
- </script>
475
- ```
476
-
477
- ---
478
-
479
- ### EuroAmount
480
-
481
- Formatted euro currency display with conditional color coding.
482
-
483
- **Props:**
484
-
485
- | Prop | Type | Default | Description |
486
- | -------------- | --------- | ------- |-------------------------------------------------|
487
- | `amount` | `Number` | — | Value to display |
488
- | `beforeAmount` | `Number` | `null` | Previous value: red if `amount < beforeAmount`, green if `amount >= beforeAmount`. When omitted, a non-negative amount renders neutral (never green); a negative amount is always red. |
489
- | `showCurrency` | `Boolean` | `true` | Show euro symbol |
490
-
491
- **Example:**
492
-
493
- ```vue
494
- <template>
495
- <!-- Shows green (amount > beforeAmount) -->
496
- <EuroAmount :amount="1500" :beforeAmount="1200" />
497
-
498
- <!-- Shows red (negative) -->
499
- <EuroAmount :amount="-300" />
500
-
501
- <!-- Without currency symbol -->
502
- <EuroAmount :amount="800" :showCurrency="false" />
503
- </template>
504
-
505
- <script setup lang="ts">
506
- import { EuroAmount } from 'mgv-backoffice'
507
- </script>
508
- ```
509
-
510
- ---
511
-
512
- ### Pagination
513
-
514
- Page navigation with smart ellipsis for large page counts.
515
-
516
- **Props:**
517
-
518
- | Prop | Type | Default | Description |
519
- | -------------- | -------- | ------- | ----------------------- |
520
- | `totalItems` | `Number` | **required** | Total number of items |
521
- | `itemsPerPage` | `Number` | `20` | Items shown per page |
522
- | `page` | `Number` | — | Optional **controlled** current page (1-based, since 1.45.0). When set, the highlight follows this prop instead of internal state — pass it whenever the page can be restored from the URL, or the pager shows page 1 over page N's rows. Out-of-range values clamp to the last page. Omit for the old uncontrolled behaviour. |
523
-
524
- **Events:**
525
-
526
- | Event | Payload | Description |
527
- | -------------- | -------- |----------------------------------|
528
- | `page-changed` | `Number` | Emitted with the new page number |
529
-
530
- **Example:**
531
-
532
- ```vue
533
- <template>
534
- <Pagination
535
- :totalItems="200"
536
- :itemsPerPage="10"
537
- @page-changed="onPageChange"
538
- />
539
- <!-- Controlled: the parent owns the page (e.g. mirrored to ?page=). -->
540
- <Pagination
541
- :page="page"
542
- :totalItems="200"
543
- :itemsPerPage="10"
544
- @page-changed="(p) => (page = p)"
545
- />
546
- </template>
547
-
548
- <script setup lang="ts">
549
- import { ref } from 'vue'
550
- import { Pagination } from 'mgv-backoffice'
551
-
552
- const page = ref(3)
553
- const onPageChange = (page: number) => {
554
- console.log('Page:', page)
555
- }
556
- </script>
557
- ```
558
-
559
- ---
560
-
561
- ### TrendArrow
562
-
563
- Up/down trend indicator displayed as a colored badge.
564
-
565
- **Props:**
566
-
567
- | Prop | Type | Default | Description |
568
- | -------- | -------- | ------- |------------------------------------------------------|
569
- | `number` | `Number` | — | Positive = green arrow up, negative = red arrow down, zero = neutral gray dash |
570
- | `icon` | `String` | — | Optional suffix appended after the number (e.g. `"%"`) |
571
-
572
- **Example:**
573
-
574
- ```vue
575
- <template>
576
- <TrendArrow :number="12.5" /> <!-- Green up arrow -->
577
- <TrendArrow :number="-3.2" /> <!-- Red down arrow -->
578
- <TrendArrow :number="0" /> <!-- Neutral gray dash -->
579
- </template>
580
-
581
- <script setup lang="ts">
582
- import { TrendArrow } from 'mgv-backoffice'
583
- </script>
584
- ```
585
-
586
- ---
587
-
588
- ## Enums
589
-
590
- All enums are importable directly from the package:
591
-
592
- ```ts
593
- import {
594
- AlertEnum,
595
- BaseBadgeEnum,
596
- BaseButtonEnum,
597
- BaseButtonSizeEnum,
598
- BaseLogoEnum,
599
- BaseModalEnum,
600
- BaseToastEnum,
601
- ColorsEnums,
602
- LineEnum,
603
- PositioningEnum
604
- } from 'mgv-backoffice'
605
- ```
606
-
607
- | Enum | Values |
608
- | -------------------- |---------------------------------------------------------------|
609
- | `AlertEnum` | `WARNING`, `ERROR`, `SUCCESS`, `INFO` |
610
- | `BaseBadgeEnum` | `WIN`, `LOSE` |
611
- | `BaseButtonEnum` | `RED`, `BLUE`, `WHITE`, `DARK`, `GREEN`, `EMERALD`, `YELLOW`, `PURPLE`, `SKY`, `GRAY`, `AMBER` |
612
- | `BaseButtonSizeEnum` | `EXTRA_SMALL`, `SMALL`, `BASE`, `LARGE`, `EXTRA_LARGE` |
613
- | `BaseLogoEnum` | `SMALL`, `MEDIUM`, `LARGE` (`BaseLoginEnum` is a deprecated alias) |
614
- | `BaseModalEnum` | `DELETE`, `SUCCESS` |
615
- | `BaseToastEnum` | `SUCCESS`, `WARNING`, `ERROR` |
616
- | `ColorsEnums` | `NONE`, `RED`, `YELLOW`, `BLACK`, `GRAY`, `GREEN`, `BLUE` |
617
- | `LineEnum` | `BASE`, `BASE_SHORTER`, `SQUARE` |
618
- | `PositioningEnum` | `TOP_LEFT`, `TOP_RIGHT`, `BOTTOM_LEFT`, `BOTTOM_RIGHT` |
619
-
620
- ---
621
-
622
- ## Types
623
-
624
- ```ts
625
- import type { BreadCrumb, DropdownOption, PnL, PnLInputs } from 'mgv-backoffice'
626
- ```
627
-
628
- | Type | Shape |
629
- | ---------------- | -------------------------------------- |
630
- | `BreadCrumb` | `{ name: string; url: string }` |
631
- | `DropdownOption` | `{ value: string \| number; label: string; title?: string; disabled?: boolean }` |
632
- | `PnLInputs` | `{ buyPrice; lastPrice; filledQty }` (each `number \| string \| null \| undefined`) |
633
- | `PnL` | `{ pnlUsd: number \| null; pnlPct: number \| null }` |
634
-
635
- The other exported types — `NavItem`, `NavSection`, `EntityPickerItem`,
636
- `LoginCredentials`, `NotificationItem`, `SegmentedOption`, `TableColumn`,
637
- `SpecField`, `SpecFieldType`, `SpecFieldValue`, `StatBreakdownItem`,
638
- `PillPickerItem`, `DistributionBar`, `CredentialsView`, `CredentialsUpdate` —
639
- are documented in their component's section.
640
-
641
- ---
642
-
643
- ## Utilities
644
-
645
- ```ts
646
- import { getBaseColor, getBaseColorOf } from 'mgv-backoffice'
647
- ```
648
-
649
- | Function | Signature | Returns |
650
- | ---------------- | ---------------------------------- | ----------------------------------- |
651
- | `getBaseColor` | `(c: AlertEnum) => string` | Tailwind color name for alert type |
652
- | `getBaseColorOf` | `(c: ColorsEnums) => string` | Tailwind color name for color enum |
653
-
654
- ### HTTP colours
655
-
656
- ```ts
657
- import {
658
- methodBadgeSolid,
659
- methodBadgeBright,
660
- methodBadgeTinted,
661
- statusBadgeSolid,
662
- statusBadgeTinted,
663
- statusBadgeSoft,
664
- } from 'mgv-backoffice'
665
- ```
666
-
667
- Tailwind class helpers for HTTP method and status code badges. `Solid` variants
668
- return saturated `bg-*-600` classes for use on neutral surfaces; `Bright` /
669
- `Tinted` variants return softer combinations suitable for cards. `statusBadgeTinted`
670
- takes `(status, isDark)` to adapt between themes.
671
-
672
- `methodBadgeTinted(method, isDark)` gives each method its own hue on a soft
673
- tinted surface (`bg-*-500/15` dark / `bg-*-100` light; GET blue, POST emerald,
674
- PUT amber, DELETE red, PATCH purple, HEAD sky) — the card-chip palette used by
675
- WireMate's mock/stub cards. `statusBadgeSoft(status, isDark)` is its status
676
- companion keyed by status class (emerald 2xx / sky 3xx / amber 4xx / red 5xx).
677
-
678
- ### Key/value row validators
679
-
680
- ```ts
681
- import { rowKeyMissing, rowValueMissing } from 'mgv-backoffice'
682
- import type { KeyValueRowLike } from 'mgv-backoffice'
683
- ```
684
-
685
- | Function | Signature | Returns |
686
- | ----------------- | ---------------------------------------------------------------------- | ------- |
687
- | `rowValueMissing` | `(row: { key?, value?, matcherType? }) => boolean` | `true` when the row has a key but no value. |
688
- | `rowKeyMissing` | `(row: { key?, value?, matcherType? }) => boolean` | `true` when the row has a value but no key. |
689
-
690
- Consistency checks for dynamic key/value grids (header lists, query params,
691
- metadata rows). Rows with `matcherType: 'absent'` are exempt — an absent
692
- matcher intentionally carries no value.
693
-
694
- ### Input validators
695
-
696
- ```ts
697
- import { isValidAbsoluteUrl, isValidJson, isValidXml, isValidBase64 } from 'mgv-backoffice'
698
- ```
699
-
700
- Pure, dependency-free form-input validators. The payload validators treat
701
- empty/whitespace-only input as **valid** — required-ness is a separate rule
702
- from well-formedness; `isValidAbsoluteUrl` validates a value that must exist,
703
- so empty is invalid there.
704
-
705
- | Function | Signature | Returns |
706
- | -------------------- | ---------------------------- | ------- |
707
- | `isValidAbsoluteUrl` | `(value: string) => boolean` | `true` for an absolute `http://` / `https://` URL (other schemes rejected). |
708
- | `isValidJson` | `(str: string) => boolean` | `true` when empty or parseable as JSON. |
709
- | `isValidXml` | `(str: string) => boolean` | `true` when empty or well-formed XML (DOMParser `<parsererror>` check; browser-only). |
710
- | `isValidBase64` | `(str: string) => boolean` | `true` when empty or well-formed base64 (whitespace stripped, length/alphabet checked, then `atob` as the final authority). |
711
-
712
- ### HTML sanitizer
713
-
714
- ```ts
715
- import { sanitizeHtml, isSafeHref } from 'mgv-backoffice'
716
- ```
717
-
718
- | Function | Signature | Returns |
719
- | -------------- | ----------------------------------------------- | ------- |
720
- | `sanitizeHtml` | `(raw: string \| undefined \| null) => string` | Allow-list–sanitised HTML safe for `v-html`. |
721
- | `isSafeHref` | `(value: string) => boolean` | `true` if the href uses a safe scheme (http/https/mailto/tel, root-relative, or anchor) or is empty/whitespace-only. |
722
-
723
- Allow-list sanitizer for strings bound into `v-html`. Keeps a small set of
724
- formatting tags (`a`, `b`/`strong`, `i`/`em`, `code`, `pre`, `p`, `ul`/`ol`/`li`,
725
- `span`, `div`, `br`), strips all other elements (unwrapping to text, or dropping
726
- content entirely for `script`/`style`/`iframe`/etc.), removes every attribute
727
- except `href`/`title` on anchors, keeps only allow-listed hrefs (http/https/
728
- mailto/tel, root-relative `/`, anchors `#`, or empty — every other scheme such
729
- as `javascript:`, `data:`, `ftp:` is stripped), and hardens surviving links with
730
- `rel="noopener noreferrer" target="_blank"`. Browser-only (uses `DOMParser`).
731
-
732
- ```ts
733
- sanitizeHtml('<p>Hi<script>alert(1)<\/script></p>') // '<p>Hi</p>'
734
- ```
735
-
736
- ### Display formatters
737
-
738
- ```ts
739
- import {
740
- fmtNumber,
741
- fmtDate,
742
- fmtDateTime,
743
- fmtDateShort,
744
- fmtPrice,
745
- fmtPct,
746
- fmtUsd,
747
- fmtMoney,
748
- // …plus fmtDateTimeMs, fmtCalendarDate, fmtCalendarDateTime,
749
- // fmtMsAsSeconds, fmtBytes, fmtDuration, formatJson, stringifyValue
750
- } from 'mgv-backoffice'
751
- ```
752
-
753
- Locale-aware, pure, dependency-free formatters for tables, logs and charts.
754
- `fmtNumber`, `fmtMoney`, `fmtDate`, `fmtCalendarDate`, `fmtCalendarDateTime` and
755
- `fmtDuration` handle missing/non-finite input gracefully (rendering an
756
- em-dash) so raw API values can be passed without pre-sanitising; the
757
- epoch/numeric formatters (`fmtDateTime`, `fmtDateShort`, `fmtDateTimeMs`,
758
- `fmtPrice`, `fmtPct`, `fmtUsd`) expect valid input and will render
759
- `"Invalid Date"` / `"NaN"` otherwise.
760
-
761
- | Function | Signature | Returns |
762
- | -------------- | --------------------------------------------------------------- | ------- |
763
- | `fmtNumber` | `(n: number \| string \| null \| undefined, digits = 4) => string` | Fixed-fraction number; em-dash for null/undefined/non-finite. Accepts numeric strings. |
764
- | `fmtDate` | `(s: string \| number \| null \| undefined) => string` | Locale date-time from ISO string or epoch; em-dash on empty, raw value on parse failure. |
765
- | `fmtDateTime` | `(ms: number) => string` | Compact `"Mon D, HH:MM"` label from epoch-millis (chart axes/tooltips). |
766
- | `fmtDateTimeMs`| `(s: string \| number) => string` | Full 24-hour locale date-time WITH the millisecond fraction — for dense feeds where same-second rows must stay distinguishable. |
767
- | `fmtDateShort` | `(ms: number) => string` | Short `"Mon D"` calendar label from epoch-millis. |
768
- | `fmtCalendarDate` | `(s: string \| number \| null \| undefined) => string` | `"Mon D, YYYY"` en-US calendar label; em-dash on empty, raw value on parse failure. |
769
- | `fmtCalendarDateTime` | `(s: string \| number \| null \| undefined) => string` | `"Mon D, YYYY, HH:MM"` en-US calendar label with time of day. |
770
- | `fmtMsAsSeconds` | `(ms: number \| null \| undefined) => string` | `"= 1.50 s"` magnitude hint for millisecond inputs (3 decimals below 1 s); `''` for non-positive input. |
771
- | `fmtBytes` | `(bytes: number \| null \| undefined) => string` | `"512 B"` / `"1.5 KB"` / `"2.0 MB"`; `''` for zero/falsy input. |
772
- | `fmtDuration` | `(ms: number \| null \| undefined, maxUnits = 2) => string` | Compact day/hour/minute duration, e.g. `"3d 5h"` / `"5h 12m"` / `"12m"`. Zero-value leading units are dropped; `maxUnits` caps how many units render. Em-dash for null/undefined/non-finite. |
773
- | `fmtPrice` | `(n: number) => string` | Price with precision that scales to magnitude (more decimals for sub-cent values). |
774
- | `fmtPct` | `(n: number, digits = 2) => string` | Percentage with explicit sign, e.g. `"+2.50%"`. |
775
- | `fmtUsd` | `(v: number) => string` | Signed USD amount with leading sign, e.g. `"+$5.00"`. |
776
- | `fmtMoney` | `(n: number \| string \| null \| undefined, digits = 2, currency = '$') => string` | Money with the minus BEFORE the currency (`"-$2.26"`, never `"$-2.26"`), digit grouping, no `+` on gains; a value rounding to zero reads unsigned; em-dash for missing input (since 1.45.0). |
777
- | `formatJson` | `(content: string) => string` | Pretty-prints parseable JSON with 2-space indentation; returns anything else verbatim. |
778
- | `stringifyValue` | `(value: unknown) => string` | Display string for an unknown value: strings pass through, null/undefined → `''`, everything else JSON-serialized (`String()` fallback). |
779
-
780
- ### Spec-form helpers
781
-
782
- ```ts
783
- import { buildSpecParams, firstInvalidNumericSpec } from 'mgv-backoffice'
784
- ```
785
-
786
- Value-map helpers for spec-driven forms (the state behind `BaseSpecFields`).
787
- A spec whose `default` is `null` is treated as OPTIONAL — blank means "knob
788
- disabled" and passes validation.
789
-
790
- | Function | Signature | Returns |
791
- | ------------------------ | --------- | ------- |
792
- | `buildSpecParams` | `(specs: SpecField[] \| undefined, existing: Record<string, SpecFieldValue>) => Record<string, SpecFieldValue>` | Value map seeded from each spec's `default`, keeping non-null overlapping values the caller already has (an existing `null` is re-seeded from the spec's `default`). |
793
- | `firstInvalidNumericSpec`| `(specs: SpecField[] \| undefined, params: Record<string, SpecFieldValue>) => string \| null` | Label of the first blank / NaN numeric field, or `null` when all numerics are valid. |
794
-
795
- ### Profit & loss
796
-
797
- ```ts
798
- import { computePnL } from 'mgv-backoffice'
799
- import type { PnL, PnLInputs } from 'mgv-backoffice'
800
- ```
801
-
802
- | Function | Signature | Returns |
803
- | ------------ | ------------------------------- | ------- |
804
- | `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` / `lastPrice <= 0`. |
805
-
806
- ```ts
807
- interface PnLInputs {
808
- buyPrice: number | string | null | undefined
809
- lastPrice: number | string | null | undefined
810
- filledQty: number | string | null | undefined
811
- }
812
-
813
- interface PnL {
814
- pnlUsd: number | null
815
- pnlPct: number | null
816
- }
817
- ```
818
-
819
- ```ts
820
- computePnL({ buyPrice: 100, lastPrice: 110, filledQty: 5 })
821
- // { pnlUsd: 50, pnlPct: 10 }
822
- ```
823
-
824
- ---
825
-
826
- ## Layout & shells (Tier 2 — full backoffice chrome)
827
-
828
- ### BaseAppLayout
829
-
830
- Root layout: dark/light page background, skip link, `<main>`-with-inert wrapper.
831
- The `<main>` content offset tracks the sidebar width automatically —
832
- `lg:ml-60` when expanded, `lg:ml-16` when collapsed (via `useSidebarCollapse()`) —
833
- and adds `pt-14 lg:pt-0` for the mobile top bar while the sidebar is shown.
834
-
835
- **Props:**
836
-
837
- | Prop | Type | Default | Description |
838
- | ---- | ---- | ------- | ----------- |
839
- | `showSidebar` | `Boolean` | `true` | Render the `sidebar` slot. Set false for full-bleed pages. |
840
- | `skipLinkLabel` | `String` | `'Skip to main content'` | Label for the accessibility skip link. |
841
-
842
- **Slots:** `sidebar`, `default` (page content).
843
-
844
- ```vue
845
- <BaseAppLayout :show-sidebar="route.name !== 'presentation'">
846
- <template #sidebar><AppSidebar /></template>
847
- <RouterView />
848
- </BaseAppLayout>
849
- ```
850
-
851
- ### BaseSidebar
852
-
853
- Responsive sidebar with desktop fixed-positioning and mobile off-canvas
854
- behavior, focus management, optional theme toggle, a desktop collapse
855
- toggle (icon-only rail), an optional notifications bell, and configurable
856
- nav sections.
857
-
858
- **Props:**
859
-
860
- | Prop | Type | Default | Description |
861
- | ---- | ---- | ------- | ----------- |
862
- | `sections` | `NavSection[]` | **required** | Grouped nav items. |
863
- | `homeRouteName` | `String` | `'home'` | Route name for the logo / "go home" click. |
864
- | `appName` | `String` | `''` | Optional app name in the footer. |
865
- | `version` | `String` | `''` | Optional version string in the footer. |
866
- | `showThemeToggle` | `Boolean` | `true` | Toggle the dark/light switch in the footer. |
867
- | `collapsible` | `Boolean` | `true` | Show the desktop collapse toggle that shrinks the sidebar to an icon-only rail. |
868
- | `showNotifications` | `Boolean` | `false` | Show the notifications bell (with unread badge) that toggles `BaseNotificationPanel`. |
869
-
870
- > **Collapse state** is shared via `useSidebarCollapse()` (and persisted to
871
- > localStorage) so `BaseAppLayout` can shrink the content offset from
872
- > `lg:ml-60` to `lg:ml-16` in step with the rail. Collapsing only affects
873
- > desktop (`lg+`); on mobile the sidebar stays a full off-canvas panel.
874
-
875
- > **Notifications:** set `:show-notifications="true"` to render the bell,
876
- > then drop a [`BaseNotificationPanel`](#basenotificationpanel) in your app.
877
- > Both share state through `useNotifications()`, so the unread badge and the
878
- > panel stay in sync.
879
-
880
- **Slots:**
881
-
882
- | Slot | Slot props | Description |
883
- | -------- | ---------- | ----------- |
884
- | `logo` | `{ size }` | Brand logo. Receives a `size` hint (28px in mobile bar, 52px in expanded sidebar, 36px in the collapsed rail). |
885
- | `status` | — | Footer status row (e.g. health indicator, sync state). |
886
- | `footer` | — | Replaces the default `appName` + `v{version}` line. |
887
-
888
- **Types:**
889
-
890
- ```ts
891
- import type { NavItem, NavSection } from 'mgv-backoffice'
892
-
893
- interface NavItem {
894
- name: string // Vue Router route name
895
- label: string // display text
896
- icon: Component // typically a Heroicon
897
- }
898
-
899
- interface NavSection {
900
- title: string
901
- items: NavItem[]
902
- }
903
- ```
904
-
905
- ```vue
906
- <BaseSidebar :sections="navSections" home-route-name="projects" app-name="WireMate UI" :version="appVersion">
907
- <template #logo="{ size }"><WireMateLogo :size="size" /></template>
908
- <template #status>
909
- <HealthIndicator />
910
- </template>
911
- </BaseSidebar>
912
- ```
913
-
914
- ---
915
-
916
- ### BaseSidebarUser
917
-
918
- Signed-in user block for the `BaseSidebar` `footer` slot: initials avatar,
919
- name and email (a link to the profile page when `to` is set) and a Logout
920
- button that emits `logout`.
921
-
922
- **Props:** `name` (required), `email`, `to` (profile route), `showLogout` (`true`),
923
- `logoutLabel` (`'Logout'`), `profileLabel` (`'View profile'`, tooltip).
924
- **Emits:** `logout`.
925
-
926
- ```vue
927
- <BaseSidebar :sections="nav">
928
- <template #footer>
929
- <BaseSidebarUser :name="me.username" :email="me.email" :to="{ name: 'Profile' }" @logout="logOut" />
930
- </template>
931
- </BaseSidebar>
932
- ```
933
-
934
- ### BaseNotificationPanel
935
-
936
- Left-anchored notification drawer (teleported to `<body>`, slides in from
937
- the left, backdrop + Escape to close). Open/close state and the list live
938
- in `useNotifications()`, so the sidebar bell and the panel stay in sync.
939
-
940
- Enable the bell on the sidebar with `:show-notifications="true"`, drop one
941
- `<BaseNotificationPanel />` anywhere in your app, and feed it data via the
942
- composable.
943
-
944
- **Props:**
945
-
946
- | Prop | Type | Default | Description |
947
- | ----------------- | --------- | ------------------------------ | ----------- |
948
- | `title` | `String` | `'Notifications'` | Panel heading. |
949
- | `emptyText` | `String` | `'You have no notifications.'` | Shown when the list is empty. |
950
- | `showMarkAllRead` | `Boolean` | `true` | Render the "Mark all as read" action when there are unread items. |
951
-
952
- **Emits:** `select` (the clicked notification's `id`; the row is also marked read).
953
-
954
- ```vue
955
- <script setup lang="ts">
956
- import { BaseNotificationPanel, useNotifications } from 'mgv-backoffice'
957
- const { setNotifications } = useNotifications()
958
- setNotifications([
959
- { id: 1, title: 'New comment', message: 'Alice replied to your post', time: '2m ago', type: 'info' },
960
- { id: 2, title: 'Build passed', time: '1h ago', read: true, type: 'success' },
961
- ])
962
- </script>
963
-
964
- <template>
965
- <BaseSidebar :sections="navSections" :show-notifications="true" />
966
- <BaseNotificationPanel @select="(id) => goTo(id)" />
967
- </template>
968
- ```
969
-
970
- ```ts
971
- import type { NotificationItem } from 'mgv-backoffice'
972
-
973
- interface NotificationItem {
974
- id: string | number
975
- title: string
976
- message?: string
977
- time?: string // pre-formatted by you
978
- read?: boolean
979
- type?: 'info' | 'success' | 'warning' | 'error' // status dot colour
980
- }
981
- ```
982
-
983
- ---
984
-
985
- ## Authentication
986
-
987
- ### BaseGoogleSignInButton
988
-
989
- Google-branded "Sign in with Google" button (official multi-colour "G",
990
- dark-mode surface swap). Purely presentational — it runs no OAuth itself;
991
- listen on `click` and start your own Google Identity / Firebase / backend
992
- flow there.
993
-
994
- **Props:**
995
-
996
- | Prop | Type | Default | Description |
997
- | ---------- | --------- | -------------------------- | ----------- |
998
- | `label` | `String` | `'Sign in with Google'` | Button text. |
999
- | `loading` | `Boolean` | `false` | Disables and shows a spinner. |
1000
- | `disabled` | `Boolean` | `false` | Disables without the spinner. |
1001
- | `block` | `Boolean` | `true` | Full-width layout. |
1002
-
1003
- **Emits:** `click` (only when not disabled/loading).
1004
-
1005
- ### BaseLoginForm
1006
-
1007
- Presentational sign-in card: email + password (with show/hide), an optional
1008
- "Remember me" checkbox, an error banner, the Google button + "or" divider,
1009
- and `logo` / `forgot` / `footer` slots. Owns its input state and emits
1010
- `submit` / `google-sign-in`; the app handles the actual request and feeds
1011
- back `loading` / `error`.
1012
-
1013
- **Props:**
1014
-
1015
- | Prop | Type | Default | Description |
1016
- | --------------- | --------- | ----------- | ----------- |
1017
- | `title` | `String` | `'Sign in'` | Card heading. |
1018
- | `subtitle` | `String` | `''` | Muted line under the heading. |
1019
- | `submitLabel` | `String` | `'Sign in'` | Submit button text. |
1020
- | `loading` | `Boolean` | `false` | Disables the form, spinner on submit. |
1021
- | `googleLoading` | `Boolean` | `false` | Disables the form, spinner on the Google button. |
1022
- | `error` | `String` | `''` | Error banner above the form. |
1023
- | `showGoogle` | `Boolean` | `true` | Render the Google button + divider. |
1024
- | `showRemember` | `Boolean` | `false` | Render the "Remember me" checkbox. |
1025
-
1026
- **Emits:** `submit` (`LoginCredentials`), `google-sign-in`.
1027
-
1028
- **Slots:** `logo`, `forgot` (next to the password label), `footer`.
1029
-
1030
- ```vue
1031
- <script setup lang="ts">
1032
- import { BaseLoginForm } from 'mgv-backoffice'
1033
- import type { LoginCredentials } from 'mgv-backoffice'
1034
-
1035
- async function onSubmit(creds: LoginCredentials) { /* call your API */ }
1036
- function onGoogle() { /* start Google OAuth */ }
1037
- </script>
1038
-
1039
- <template>
1040
- <BaseLoginForm
1041
- subtitle="Welcome back"
1042
- :show-remember="true"
1043
- @submit="onSubmit"
1044
- @google-sign-in="onGoogle"
1045
- >
1046
- <template #logo><MyLogo /></template>
1047
- <template #forgot><a href="/forgot" class="text-sm text-emerald-600">Forgot?</a></template>
1048
- <template #footer>No account? <a href="/signup" class="text-emerald-600">Sign up</a></template>
1049
- </BaseLoginForm>
1050
- </template>
1051
- ```
1052
-
1053
- ---
1054
-
1055
- ## Modals & sections
1056
-
1057
- ### BaseModalShell
1058
-
1059
- Shared modal chrome — `Teleport` to body, backdrop, themed card, escape key,
1060
- aria-modal. Compose this rather than building modals from scratch.
1061
-
1062
- **Props:**
1063
-
1064
- | Prop | Type | Default | Description |
1065
- | --------------- | --------- | ----------- | ----------- |
1066
- | `title` | `String` | **required** | Modal heading. |
1067
- | `icon` | `Component` | **required** | Heroicon rendered in a tinted circular chip left of the title (the `icon` slot can override the whole chip). |
1068
- | `iconBgClass` | `String` | `''` | Background classes of the icon chip; empty falls back to the emerald tint (dark-mode aware). |
1069
- | `iconClass` | `String` | `'text-emerald-600'` | Classes applied to the icon itself. |
1070
- | `maxWidthClass` | `String` | `'max-w-md'` | Tailwind max-w utility for the card. |
1071
- | `manualClose` | `Boolean` | `false` | If true, backdrop click and Escape do NOT auto-emit `cancel`. |
1072
- | `scrollable` | `Boolean` | `false` | Switch to the large-content layout: a flex column capped at `90vh` with a fixed header/footer and a scrolling body. |
1073
- | `subtitle` | `String` | `''` | Muted line under the title (scrollable layout only). |
1074
-
1075
- **Slots:** `icon`, `default`, `footer`, and (scrollable layout) `header-actions` — content on the right of the header, e.g. a close button.
1076
- **Events:** `cancel`, `backdrop`.
1077
-
1078
- ### BaseConfirmModal
1079
-
1080
- Confirmation dialog built on `BaseModalShell`. Variant chooses red (danger) or
1081
- amber (warning) styling.
1082
-
1083
- **Props:** `title`, `message`, `confirmText`, `cancelText`, `submittingText`,
1084
- `variant: 'danger' | 'warning'`, `submitting`. While `submitting` is true,
1085
- backdrop clicks and Escape stop dismissing the dialog.
1086
-
1087
- **Slots:** `message` — rich markup replacing the plain `message` string;
1088
- `default` — extra content below the message (warning banner, opt-in checkbox).
1089
- **Events:** `confirm`, `cancel`.
1090
-
1091
- ### BaseTextInputModal
1092
-
1093
- "Ask the user for a single string and confirm" dialog. Preserves typed input
1094
- on stray backdrop clicks; Escape always cancels.
1095
-
1096
- **Props:** `title`, `message`, `initialValue`, `placeholder`, `inputLabel`,
1097
- `confirmText`, `cancelText`, `submittingText`, `submitting`.
1098
-
1099
- **Slots:** `icon` — override the default emerald document icon.
1100
- **Events:** `confirm(value: string)`, `cancel`.
1101
-
1102
- ### BaseEntityPickerModal
1103
-
1104
- Searchable "pick one from a list" dialog. Pass `items` directly or an async
1105
- `loader` that runs on mount.
1106
-
1107
- **Props:** `title`, `message?`, `items?: EntityPickerItem[]`,
1108
- `loader?: () => Promise<EntityPickerItem[]>`, `excludeId?`,
1109
- `variant: 'emerald' | 'purple' | 'blue' | 'red' | 'amber'`,
1110
- `searchPlaceholder`, `emptyMessage`, `noMatchMessage`, `confirmText`,
1111
- `cancelText`, `submittingText`, `submitting`.
1112
-
1113
- **Slots:** `icon` — override the default icon chip.
1114
- **Events:** `confirm(itemId: string)`, `cancel`.
1115
-
1116
- ```ts
1117
- interface EntityPickerItem { id: string; label: string }
1118
- ```
1119
-
1120
- ### BaseCollapsibleSection
1121
-
1122
- Section wrapper with a clickable header, optional badge, and a `default` slot
1123
- for the body. Parent owns the `collapsed` state.
1124
-
1125
- **Props:** `title`, `collapsed`, `badge?`, `bodyClass?`.
1126
- **Events:** `toggle`.
1127
-
1128
- ### BaseNotFoundPage
1129
-
1130
- Drop-in 404 view.
1131
-
1132
- **Props:** `code` (`'404'`), `message` (`'Page not found'`),
1133
- `homeRouteName` (`'home'`), `homeLabel` (`'Go home'`).
1134
-
1135
- ### BasePageHeader
1136
-
1137
- Page-level header: icon badge + title/subtitle on the left, action
1138
- buttons on the right. Gives top-level views a consistent header shape
1139
- and width. The icon badge only renders when the `icon` slot is filled,
1140
- so icon-less apps get a plain title/subtitle header.
1141
-
1142
- **Props:**
1143
-
1144
- | Prop | Type | Default | Description |
1145
- | --------------- | -------- | ------------- | ----------- |
1146
- | `title` | `String` | **required** | H1 text. |
1147
- | `subtitle` | `String` | `''` | Muted line below the title. |
1148
- | `iconColor` | `String` | `'emerald'` | Badge + icon colour: `'emerald' \| 'sky' \| 'red' \| 'amber'`. |
1149
- | `maxWidthClass` | `String` | `'max-w-4xl'` | Tailwind max-w utility constraining header width. Pass `''` to skip the width wrapper entirely — the header then spans its container. |
1150
- | `align` | `String` | `'center'` | Vertical alignment of the title block vs the actions: `'center'` or `'end'` (actions sit on the title baseline). |
1151
- | `marginClass` | `String` | `'mb-8'` | Space under the header. Pass `''` when the parent manages vertical rhythm (`space-y-*`). |
1152
- | `accent` | `Boolean` | `false` | Brand-green gauge bar in front of the title (icon-less headers). |
1153
-
1154
- **Slots:**
1155
-
1156
- | Slot | Slot props | Description |
1157
- | ---------- | ----------------- | ----------- |
1158
- | `icon` | `{ iconClass }` | Page Heroicon. Bind `:class="iconClass"` for the theme-aware colour. Badge square renders only when this slot is filled. |
1159
- | `subtitle` | — | Rich subtitle content (links, `<strong>`, interpolation); overrides the `subtitle` prop. |
1160
- | `actions` | — | Buttons rendered on the right (refresh, destructive, etc.). |
1161
-
1162
- ```vue
1163
- <template>
1164
- <BasePageHeader title="Request Journal" subtitle="Recent matched requests" icon-color="sky">
1165
- <template #icon="{ iconClass }">
1166
- <DocumentTextIcon class="w-5 h-5" :class="iconClass" />
1167
- </template>
1168
- <template #actions>
1169
- <BaseButton description="Refresh" @click="reload" />
1170
- </template>
1171
- </BasePageHeader>
1172
- </template>
1173
-
1174
- <script setup lang="ts">
1175
- import { BasePageHeader, BaseButton } from 'mgv-backoffice'
1176
- import { DocumentTextIcon } from '@heroicons/vue/24/outline'
1177
- </script>
1178
- ```
1179
-
1180
- ---
1181
-
1182
- ### BaseToolbarButton
1183
-
1184
- Bordered toolbar button — the "Refresh / Delete All" row that sits under
1185
- a page header. Optional leading icon (via slot) plus a label.
1186
-
1187
- **Props:**
1188
-
1189
- | Prop | Type | Default | Description |
1190
- | ---------- | --------- | ----------- | ----------- |
1191
- | `label` | `String` | `''` | Button text. Omit for an icon-only button. |
1192
- | `variant` | `String` | `'neutral'` | `'neutral'` (grey), `'danger'` (solid red) or `'ghost'` (slate h-9 outline — toolbar/modal-footer buttons). |
1193
- | `disabled` | `Boolean` | `false` | Greys out and blocks the click. |
1194
- | `title` | `String` | `undefined` | Native tooltip / a11y text. |
1195
- | `type` | `String` | `'button'` | Native button type. |
1196
-
1197
- **Slots:**
1198
-
1199
- | Slot | Slot props | Description |
1200
- | ------ | --------------- | ----------- |
1201
- | `icon` | `{ iconClass }` | Leading Heroicon. Bind `:class="iconClass"` (`w-4 h-4`); add state classes as needed. |
1202
-
1203
- **Emits:** `click` (native `MouseEvent`).
1204
-
1205
- ```vue
1206
- <template>
1207
- <BaseToolbarButton label="Refresh" :disabled="isLoading" title="Refresh" @click="reload">
1208
- <template #icon="{ iconClass }">
1209
- <ArrowPathIcon :class="[iconClass, { 'animate-spin': isLoading }]" />
1210
- </template>
1211
- </BaseToolbarButton>
1212
- <BaseToolbarButton label="Delete All" variant="danger" @click="deleteAll">
1213
- <template #icon="{ iconClass }">
1214
- <TrashIcon :class="iconClass" />
1215
- </template>
1216
- </BaseToolbarButton>
1217
- </template>
1218
- ```
1219
-
1220
- ---
1221
-
1222
- ### BaseActionButton
1223
-
1224
- Compact ghost action button — the colour-coded "Edit / Logs / Stub /
1225
- Delete" actions on a card footer or action row. No border/fill at rest;
1226
- a tinted hover background keyed to the semantic colour.
1227
-
1228
- **Props:**
1229
-
1230
- | Prop | Type | Default | Description |
1231
- | ----------- | --------- | ----------- | ----------- |
1232
- | `label` | `String` | `''` | Button text. Omit for an icon-only button. |
1233
- | `color` | `String` | `'emerald'` | `'emerald' \| 'sky' \| 'indigo' \| 'teal' \| 'purple' \| 'red' \| 'amber' \| 'amberStrong'`. |
1234
- | `disabled` | `Boolean` | `false` | Dims via opacity and suppresses the hover tint. |
1235
- | `fullWidth` | `Boolean` | `false` | Stretch to fill its flex row (`flex-1`). |
1236
- | `title` | `String` | `undefined` | Native tooltip. |
1237
- | `ariaLabel` | `String` | `undefined` | Accessible label. |
1238
- | `type` | `String` | `'button'` | Native button type. |
1239
-
1240
- **Slots:**
1241
-
1242
- | Slot | Slot props | Description |
1243
- | ------ | --------------- | ----------- |
1244
- | `icon` | `{ iconClass }` | Leading Heroicon. Bind `:class="iconClass"` (`w-4 h-4`). |
1245
-
1246
- **Emits:** `click` (native `MouseEvent`).
1247
-
1248
- ```vue
1249
- <template>
1250
- <BaseActionButton label="Edit" color="emerald" full-width title="Edit this mock" @click="edit">
1251
- <template #icon="{ iconClass }">
1252
- <PencilSquareIcon :class="iconClass" />
1253
- </template>
1254
- </BaseActionButton>
1255
- <BaseActionButton label="Delete" color="red" @click="remove">
1256
- <template #icon="{ iconClass }">
1257
- <TrashIcon :class="iconClass" />
1258
- </template>
1259
- </BaseActionButton>
1260
- </template>
1261
- ```
1262
-
1263
- ---
1264
-
1265
- ### BaseCopyButton
1266
-
1267
- Copy-to-clipboard icon button with transient "copied" feedback — clicks
1268
- write `text` to the clipboard, swap the clipboard icon for a checkmark
1269
- for `resetMs`, then revert. Uses the async Clipboard API with a
1270
- hidden-textarea `execCommand` fallback for insecure origins. Emits
1271
- `copied` / `error` so the parent can fire its own toast.
1272
-
1273
- **Props:**
1274
-
1275
- | Prop | Type | Default | Description |
1276
- | ----------- | --------- | --------- | ----------- |
1277
- | `text` | `String` | **required** | Value written to the clipboard. |
1278
- | `label` | `String` | `''` | Used in the tooltip / aria-label (`Copy {label}`). |
1279
- | `variant` | `String` | `'ghost'` | `'ghost'` (borderless `p-1` icon) or `'bordered'` (`w-9 h-9` boxed, turns emerald while copied). |
1280
- | `resetMs` | `Number` | `1500` | How long the checkmark stays before reverting. |
1281
- | `iconClass` | `String` | `'w-4 h-4'` | Icon size class. |
1282
-
1283
- **Emits:** `copied`, `error(err)`.
1284
-
1285
- ```vue
1286
- <template>
1287
- <!-- Inline ID copy, parent fires the toast -->
1288
- <BaseCopyButton
1289
- :text="stub.id"
1290
- label="Stub ID"
1291
- @copied="showToastMessage('Stub ID copied to clipboard', BaseToastEnum.SUCCESS)"
1292
- @error="showToastMessage('Failed to copy stub ID', BaseToastEnum.ERROR)"
1293
- />
1294
- <!-- Boxed copy next to a read-only input -->
1295
- <BaseCopyButton :text="mock.id" label="Mock ID" variant="bordered" :reset-ms="2000" />
1296
- </template>
1297
-
1298
- <script setup lang="ts">
1299
- import { BaseCopyButton, BaseToastEnum } from 'mgv-backoffice'
1300
- </script>
1301
- ```
1302
-
1303
- ---
1304
-
1305
- ### BaseChipButton
1306
-
1307
- Small tinted emerald "chip" action button — the compact "+ Add" pill used
1308
- above repeatable form rows. Label comes from the default slot.
1309
-
1310
- **Props:**
1311
-
1312
- | Prop | Type | Default | Description |
1313
- | ---------- | --------- | ------- | ----------- |
1314
- | `size` | `String` | `'sm'` | `'sm'` = `px-2.5 py-1`; `'xs'` = `px-2 py-0.5` for tight corners. |
1315
- | `disabled` | `Boolean` | `false` | Dims the chip and blocks clicks. |
1316
-
1317
- **Emits:** `click`.
1318
-
1319
- ```vue
1320
- <BaseChipButton @click="addRow(rows)">+ Add</BaseChipButton>
1321
- <BaseChipButton size="xs" @click="addNamespace">+ Add</BaseChipButton>
1322
- ```
1323
-
1324
- ---
1325
-
1326
- ### BaseRemoveButton
1327
-
1328
- The red "×" remove-row affordance used beside repeatable form rows. Name it
1329
- for screen readers via `aria-label`; `title`, `disabled` and extra classes
1330
- (`pt-1`, `self-start`, …) fall through as attrs.
1331
-
1332
- **Emits:** `click`.
1333
-
1334
- ```vue
1335
- <BaseRemoveButton :aria-label="`Remove header ${i + 1}`" @click="rows.splice(i, 1)" />
1336
- ```
1337
-
1338
- ---
1339
-
1340
- ### BaseStatusPill
1341
-
1342
- Connection/health status pill: a colored dot (pulsing while `ok`) next to a
1343
- short label on a tinted background.
1344
-
1345
- **Props:**
1346
-
1347
- | Prop | Type | Default | Description |
1348
- | -------- | -------- | ------- | ----------- |
1349
- | `status` | `String` | **required** | `'ok'` (emerald, pulsing), `'error'` (red), `'unknown'` (gray). |
1350
- | `label` | `String` | **required** | Short text next to the dot, e.g. `WireMock Connected`. |
1351
-
1352
- ```vue
1353
- <BaseStatusPill :status="healthy ? 'ok' : 'error'" :label="healthy ? 'Connected' : 'Disconnected'" />
1354
- ```
1355
-
1356
- ---
1357
-
1358
- ### BaseFileDropzone
1359
-
1360
- Dashed "click to select a file" upload zone (extracted from WireMate's
1361
- Postman-import modal). Renders a document-arrow-up icon (overridable via the
1362
- `#icon` slot), a label line, and an optional dimmed hint line. Clicking opens
1363
- the native file picker; dragging files onto the zone also works (the border
1364
- highlights emerald while dragging). The hidden input resets after every
1365
- selection, so picking the same file twice still emits.
1366
-
1367
- **Props:**
1368
-
1369
- | Prop | Type | Default | Description |
1370
- | ---------- | --------- | ------- | ----------- |
1371
- | `label` | `String` | **required** | Main line, e.g. `Click to select a Postman collection (.json)`. |
1372
- | `hint` | `String` | `''` | Dimmed helper line below the label. |
1373
- | `accept` | `String` | `''` | Forwarded to the input's `accept`. Dropped files are **not** filtered by it. |
1374
- | `multiple` | `Boolean` | `false` | Allow multi-select; when `false`, a multi-file drop emits only the first file. |
1375
- | `disabled` | `Boolean` | `false` | Dims the zone and ignores clicks/drops. |
1376
-
1377
- **Emits:** `files` (`File[]`, never empty).
1378
-
1379
- **Slots:** `icon` — replaces the default upload icon.
1380
-
1381
- ```vue
1382
- <BaseFileDropzone
1383
- accept="application/json,.json"
1384
- label="Click to select a Postman collection (.json)"
1385
- hint="Exported from Postman → Export → Collection v2.1"
1386
- @files="onFiles"
1387
- />
1388
- ```
1389
-
1390
- ### BaseCodeBlock
1391
-
1392
- Themed monospace `<pre>` for JSON payloads, request dumps and code snippets
1393
- (extracted from WireMate's stub/request detail views). Preserves whitespace
1394
- verbatim, scrolls both axes, and adapts to the theme. Extra classes (margins
1395
- etc.) fall through via the normal class merge.
1396
-
1397
- **Props:**
1398
-
1399
- | Prop | Type | Default | Description |
1400
- | ---------------- | -------- | -------- | ----------- |
1401
- | `code` | `String` | **required** | The raw text to render. |
1402
- | `variant` | `String` | `'soft'` | `'soft'` = tinted fill, no border (in-card look); `'bordered'` = bordered card fill (standalone look). |
1403
- | `size` | `String` | `'sm'` | `'sm'` = `text-sm px-5 py-4`; `'xs'` = dense `text-xs p-3`. |
1404
- | `maxHeightClass` | `String` | `''` | Optional Tailwind max-height utility, e.g. `max-h-96`. |
1405
-
1406
- ```vue
1407
- <BaseCodeBlock :code="formatJson(response.body)" size="xs" max-height-class="max-h-64" />
1408
- ```
1409
-
1410
- ---
1411
-
1412
- ## Forms & tables
1413
-
1414
- These components use `dark:` Tailwind variants, so the consuming app must map
1415
- the `dark` variant to the `.dark` class that `useTheme()` toggles (see
1416
- [Tailwind setup for consumers](#tailwind-setup-for-consumers)).
1417
-
1418
- ### BaseInput
1419
-
1420
- Themed text/number input carrying the shared field skin (slate border,
1421
- `bg-slate-50` / dark `bg-slate-900` surface). Everything else — `placeholder`,
1422
- `id`, `disabled`, `step`/`min`, extra classes like `font-mono` or
1423
- `placeholder:*` — falls through via attrs and Vue class merging.
1424
-
1425
- **Props:**
1426
-
1427
- | Prop | Type | Default | Description |
1428
- | ------------ | ------------------ | -------- | ----------- |
1429
- | `modelValue` | `String \| Number \| null` | `''` | `v-model` value. |
1430
- | `type` | `String` | `'text'` | Native input type. |
1431
- | `size` | `String` | `'md'` | `'md'` = `px-3 py-2`, `'sm'` = `px-2 py-1.5`. |
1432
- | `block` | `Boolean` | `true` | Full-width (`w-full`); set `false` for inline fields. |
1433
-
1434
- **Emits:** `update:modelValue(value: string)` — always the raw string; parse
1435
- numbers in the owner.
1436
-
1437
- ```vue
1438
- <BaseInput v-model="query" placeholder="e.g. AMD or BTC" class="font-mono" />
1439
- ```
1440
-
1441
- ### BaseSelect
1442
-
1443
- Themed `<select>` sharing BaseInput's field skin. Options come from the
1444
- default slot so callers keep full control of `<option>` rendering.
1445
-
1446
- **Props:**
1447
-
1448
- | Prop | Type | Default | Description |
1449
- | ------------ | ------------------ | ------- | ----------- |
1450
- | `modelValue` | `String \| null` | `undefined` | `v-model` value. When left undefined the browser keeps its own default selection. |
1451
- | `size` | `String` | `'sm'` | `'sm'` = `px-2 py-1.5`, `'md'` = `px-3 py-2`. |
1452
- | `block` | `Boolean` | `true` | Full-width; set `false` for inline selects. |
1453
-
1454
- **Slots:** `default` — the `<option>` elements.
1455
- **Emits:** `update:modelValue(value: string)`.
1456
-
1457
- ```vue
1458
- <BaseSelect v-model="strategyType">
1459
- <option v-for="e in catalog" :key="e.type" :value="e.type" :title="e.description">
1460
- {{ e.label }}
1461
- </option>
1462
- </BaseSelect>
1463
- ```
1464
-
1465
- ### BaseDropdown
1466
-
1467
- Button-style single-select dropdown ("Select Social User ⌄"). Unlike
1468
- `BaseSelect` (a native `<select>`), this renders a trigger button plus a
1469
- floating menu, so the closed control shows a placeholder and a chevron that
1470
- rotates while open — matching the app's filter dropdowns. Selecting a row
1471
- emits its `value` and closes the menu; Escape and an outside click also close
1472
- it.
1473
-
1474
- **Props:**
1475
-
1476
- | Prop | Type | Default | Description |
1477
- | ------------- | -------------------------- | ------------ | ----------- |
1478
- | `options` | `DropdownOption[]` | **required** | `{ value, label, title?, disabled? }` per row. |
1479
- | `modelValue` | `String \| Number \| null` | `null` | Selected option's `value` (`v-model`). |
1480
- | `placeholder` | `String` | `'Select'` | Trigger text shown when nothing is selected. |
1481
- | `size` | `String` | `'md'` | `'md'` = `px-4 py-2.5` (app filter height), `'sm'` = `px-3 py-2`. Ignored when `triggerClass` is set. |
1482
- | `block` | `Boolean` | `true` | Full-width; set `false` for an inline, content-width dropdown. |
1483
- | `disabled` | `Boolean` | `false` | Disables the trigger. |
1484
- | `ariaLabel` | `String` | `''` | Accessible name for the trigger/listbox when there is no visible label. |
1485
- | `triggerClass`| `String` | `''` | Replaces the trigger's default slate skin entirely (including the `size` padding). |
1486
- | `chevronClass`| `String` | `'w-5 h-5 text-slate-500 dark:text-slate-400'` | Classes for the chevron icon. |
1487
- | `tone` | `'neutral' \| 'success' \| 'danger'` | `'neutral'` | Tints the default skin (slate / emerald / red) while keeping the `size` padding — e.g. an Active/Paused status dropdown. Ignored when `triggerClass` is set. |
1488
-
1489
- **Emits:** `update:modelValue(value)`.
1490
-
1491
- ```vue
1492
- <BaseDropdown
1493
- v-model="socialUserId"
1494
- :options="socialUsers.map((u) => ({ value: u.id, label: u.name }))"
1495
- placeholder="Select Social User"
1496
- aria-label="Social user"
1497
- />
1498
- ```
1499
-
1500
- ### BaseDateTimePicker
1501
-
1502
- Date / date-time / time picker built on
1503
- [@vuepic/vue-datepicker](https://vue3datepicker.com). The package is a regular
1504
- dependency of this lib (installed automatically) and its CSS ships inside
1505
- `ui-lib.css`, so consumers install and import nothing extra. Dark mode follows
1506
- `useTheme()`; the skin matches BaseInput (slate surfaces, emerald accent).
1507
-
1508
- **Props:**
1509
-
1510
- | Prop | Type | Default | Description |
1511
- | ------------ | ------------------ | ------------ | ----------- |
1512
- | `modelValue` | `ModelValue` | `null` | `v-model` value — `Date`, `Date[]` for ranges, a time object in `'time'` mode, or a string/number with `model-type`. |
1513
- | `mode` | `'date' \| 'datetime' \| 'time'` | `'datetime'` | Calendar only, calendar + time, or time only. |
1514
- | `format` | `String` | per mode | date-fns input pattern. Defaults: `dd/MM/yyyy`, `dd/MM/yyyy HH:mm`, `HH:mm` (`hh:mm a` when `is24` is false). |
1515
- | `is24` | `Boolean` | `true` | 24-hour clock. |
1516
- | `autoApply` | `Boolean` | `false` | Select on click, without the Cancel/Select row. |
1517
- | `teleport` | `Boolean \| String \| HTMLElement` | `true` | Menu mount target; `true` = body, so modals don't clip it. |
1518
- | `timeConfig` | `Partial<TimeConfig>` | — | Extra time options (seconds, increments…), merged over the defaults. |
1519
-
1520
- Every other VueDatePicker prop (`range`, `min-date`, `max-date`,
1521
- `disabled-dates`, `placeholder`, `model-type`, …), event and slot is passed
1522
- straight through.
1523
-
1524
- **Emits:** `update:modelValue(value)`.
1525
-
1526
- ```vue
1527
- <BaseDateTimePicker v-model="startsAt" placeholder="Start" />
1528
- <BaseDateTimePicker v-model="day" mode="date" :min-date="new Date()" auto-apply />
1529
- <BaseDateTimePicker v-model="period" mode="date" range />
1530
- ```
1531
-
1532
- ### BaseSegmentedControl
1533
-
1534
- Segmented button group ("All | Stock | Crypto"). One button per option; the
1535
- selected one gets the filled treatment and `aria-pressed="true"`.
1536
-
1537
- **Props:**
1538
-
1539
- | Prop | Type | Default | Description |
1540
- | ------------- | ------------------- | -------- | ----------- |
1541
- | `options` | `SegmentedOption[]` | **required** | `{ value, label, title? }` per button. |
1542
- | `modelValue` | `String \| Number` | **required** | Selected option's `value` (`v-model`). |
1543
- | `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). |
1544
- | `ariaLabel` | `String` | `''` | When set, the wrapper renders `role="group"` + `aria-label`. |
1545
- | `optionClass` | `Function` | — | `(option, active) => string` override for per-button fill classes (e.g. severity colours); layout stays owned by the variant. |
1546
-
1547
- **Emits:** `update:modelValue(value)`.
1548
-
1549
- ```vue
1550
- <BaseSegmentedControl v-model="assetFilter" :options="ASSET_FILTERS" />
1551
- <BaseSegmentedControl v-model="exchange" :options="EXCHANGES" variant="wide" aria-label="Exchange" />
1552
- ```
1553
-
1554
- ### BaseTable
1555
-
1556
- Styling shell for data tables — **not** a data grid. Owns the table skin
1557
- (slate header band, `px-4 py-3` header cells, row dividers + hover, empty-state
1558
- row); body rows are the caller's own `<tr>` markup via the default slot. Pass
1559
- `card` for the rounded, bordered, horizontally scrolling card chrome, or wrap
1560
- it yourself (e.g. a `BaseRow` with `overflow-x-auto`).
1561
-
1562
- **Props:**
1563
-
1564
- | Prop | Type | Default | Description |
1565
- | ----------- | --------------- | ------------ | ----------- |
1566
- | `columns` | `TableColumn[]` | **required** | `{ label, align? }`; `align: 'right'` right-aligns the header cell. |
1567
- | `empty` | `Boolean` | `false` | True renders the empty-state row spanning every column. |
1568
- | `emptyText` | `String` | `'No rows.'` | Fallback empty-state text. |
1569
- | `card` | `Boolean` | `false` | Wrap in the rounded, bordered, scrolling card. |
1570
-
1571
- **Slots:** `default` — the `<tr>` rows; `empty` — custom empty-state content.
1572
-
1573
- ```vue
1574
- <BaseTable :columns="COLUMNS" :empty="rows.length === 0">
1575
- <template #empty>No trades match your filters.</template>
1576
- <tr v-for="row in rows" :key="row.id" class="border-t border-slate-200 dark:border-slate-700">
1577
- …
1578
- </tr>
1579
- </BaseTable>
1580
- ```
1581
-
1582
- ### BaseSpecFields
1583
-
1584
- Spec-driven form fields: renders a select / checkbox / number input per
1585
- `SpecField`, with labels and help text, in a responsive two-column grid. Feed
1586
- it a backend-described catalogue and every form editing those values stays in
1587
- lockstep. Never mutates `params` — every edit is emitted as `(key, value)`
1588
- and the owner writes it back into its own state.
1589
-
1590
- **Props:**
1591
-
1592
- | Prop | Type | Default | Description |
1593
- | -------- | -------------------------------- | ------------ | ----------- |
1594
- | `specs` | `SpecField[]` | **required** | `{ key, label, type: 'decimal' \| 'integer' \| 'boolean' \| 'select', default?, options?, step?, min?, help? }` (optional members are nullable). |
1595
- | `params` | `Record<string, SpecFieldValue>` | **required** | Current values keyed by `spec.key`. |
1596
-
1597
- **Slots:** `after` (`{ spec }`) — extra content under each field (e.g. a live
1598
- preview attached to one key).
1599
- **Emits:** `update(key: string, value: SpecFieldValue)` — numbers are parsed
1600
- (`parseFloat`); unparseable input passes through raw so the owner's
1601
- validation can catch it.
1602
-
1603
- ```vue
1604
- <BaseSpecFields :specs="entry.params" :params="form.params"
1605
- @update="(key, value) => (form.params[key] = value)" />
1606
- ```
1607
-
1608
- ### BaseStatBreakdown
1609
-
1610
- Compact per-item breakdown meant to sit under a summary/stat card (pairs with
1611
- [`EarningsCard`](#earningscard)). Each item renders on its own line — label
1612
- left, value right in monospace. A `null`/`undefined` value (a source that is
1613
- unconfigured, unreachable, or has no matching rows) shows an em-dash rather
1614
- than a misleading 0. With `signed`, values gain an explicit "+" and are
1615
- coloured green/red by sign (same `"-$3.00"` / `"+$5.00"` shape as `fmtUsd`).
1616
-
1617
- **Props:**
1618
-
1619
- | Prop | Type | Default | Description |
1620
- | ---------- | --------------------- | ------------ | ----------- |
1621
- | `items` | `StatBreakdownItem[]` | **required** | `{ label, value }` per row. |
1622
- | `currency` | `String` | `''` | Currency symbol placed after the sign, e.g. `'$'`. |
1623
- | `decimals` | `Number` | `2` | Fraction digits shown for each value. |
1624
- | `signed` | `Boolean` | `false` | Show an explicit "+" on non-negative values and colour rows green/red by sign. |
1625
-
1626
- ```ts
1627
- import type { StatBreakdownItem } from 'mgv-backoffice'
1628
-
1629
- interface StatBreakdownItem {
1630
- label: string // row label rendered on the left
1631
- value: number | null | undefined // null/undefined renders as an em-dash
1632
- }
1633
- ```
1634
-
1635
- ```vue
1636
- <EarningsCard title="TOTAL P&L" :amount="totalPnl" signed />
1637
- <BaseStatBreakdown
1638
- :items="[
1639
- { label: 'Alpaca', value: 42.5 },
1640
- { label: 'Binance', value: -3.1 },
1641
- { label: 'Kraken', value: null },
1642
- ]"
1643
- currency="$"
1644
- signed
1645
- />
1646
- ```
1647
-
1648
- ### BaseFilterChip
1649
-
1650
- Colour-coded toggleable filter chip — one-click event/category filters above
1651
- a data feed. Idle renders a tinted border/background in the semantic colour;
1652
- active renders a solid fill with white text (`aria-pressed` reflects the
1653
- state). Layout classes (`h-9 flex-1`, …) pass through the class attribute;
1654
- click handlers bind natively on the component.
1655
-
1656
- **Props:**
1657
-
1658
- | Prop | Type | Default | Description |
1659
- | ---------- | --------- | --------- | ----------- |
1660
- | `label` | `String` | `''` | Chip text; the default slot overrides it. |
1661
- | `color` | `'emerald' \| 'sky' \| 'amber' \| 'red' \| 'slate'` | `'slate'` | Semantic colour of the idle tint and active fill. |
1662
- | `active` | `Boolean` | `false` | Whether the chip's filter is applied (solid fill). |
1663
- | `disabled` | `Boolean` | `false` | Greys out + blocks the click. |
1664
- | `title` | `String` | — | Native tooltip. |
1665
-
1666
- ```vue
1667
- <BaseFilterChip
1668
- v-for="f in filters"
1669
- :key="f.key"
1670
- class="h-9 flex-1"
1671
- :color="f.color"
1672
- :active="isActive(f)"
1673
- :title="f.title"
1674
- @click="toggle(f)"
1675
- >{{ f.label }}</BaseFilterChip>
1676
- ```
1677
-
1678
- ### BaseCredentialsForm
1679
-
1680
- One service's API-credentials card: key id + secret + base/data URLs, with
1681
- the has-secret handling (placeholder dots, blank-keeps-stored-secret), the
1682
- save-validation ladder and a saving spinner. Load/save results are EMITTED —
1683
- the parent owns toasts / error banners. Exposes `load()` so a parent Reload
1684
- button can re-pull several cards in parallel.
1685
-
1686
- **Props:** `title` + `idPrefix` + `fetchFn: () => Promise<CredentialsView>` +
1687
- `updateFn: (body: CredentialsUpdate) => Promise<CredentialsView>` +
1688
- `defaults: { baseUrl, dataUrl }` (required); `subtitle`, `keyLabel`,
1689
- `keyPlaceholder`, `secretLabel`, `secretPlaceholder`, `secretSetHint`,
1690
- `permissionsHint`, `requiredKeyMessage`, `requiredSecretMessage`,
1691
- `savedMessage`, `saveLabel` (optional copy overrides).
1692
-
1693
- **Slots:** `no-secret-hint` — rich help while no secret is stored;
1694
- `base-url-extra` (`{ form }`) — extras under the Base URL field (e.g.
1695
- live/paper shortcut buttons that write into the form); `footer` — extra
1696
- content at the card's bottom.
1697
-
1698
- **Emits:** `saved(message)`, `error(message)`, `load-error(message)`.
1699
-
1700
- ```vue
1701
- <BaseCredentialsForm
1702
- ref="card"
1703
- title="Alpaca API"
1704
- id-prefix="alpaca"
1705
- :fetch-fn="fetchAlpaca"
1706
- :update-fn="updateAlpaca"
1707
- :defaults="{ baseUrl: LIVE_BASE, dataUrl: DATA_URL }"
1708
- @saved="onSaved"
1709
- @error="onError"
1710
- @load-error="onLoadError"
1711
- />
1712
- ```
1713
-
1714
- ---
1715
-
1716
- ### BasePillPickerModal
1717
-
1718
- "Pick one of many" modal: every item rendered as a clickable pill, narrowed
1719
- by a free-text filter and an optional segmented group toggle. Clicking a pill
1720
- emits `pick` with the item; backdrop / Escape / the footer Close emit `close`.
1721
- Narrowing state lives inside, so a `v-if`-mounted instance always opens fresh.
1722
-
1723
- **Props:** `title` + `items: PillPickerItem[]` (required);
1724
- `groups?: SegmentedOption<string>[]` (renders the group toggle with an
1725
- `allLabel` option prepended, narrowing by each item's `group`); `icon?`
1726
- (defaults to the magnifying glass), `subtitle?`, `searchPlaceholder`,
1727
- `emptyMessage`, `noMatchMessage`, `mono` (mono font for the filter input and
1728
- pills — symbols, codes, ids), `maxWidthClass` (default `max-w-2xl`),
1729
- `closeText`, `groupAriaLabel`, `allLabel`.
1730
-
1731
- **Emits:** `pick(item: PillPickerItem)`, `close`.
1732
-
1733
- ```ts
1734
- interface PillPickerItem {
1735
- id: string // unique key; identifies the pick
1736
- label: string // pill text; what the filter matches
1737
- group?: string // segmented-toggle bucket
1738
- title?: string // pill tooltip
1739
- }
1740
- ```
1741
-
1742
- ```vue
1743
- <BasePillPickerModal
1744
- v-if="open"
1745
- title="Symbols"
1746
- :items="symbols.map(s => ({ id: s.id, label: s.symbol, group: s.assetClass }))"
1747
- :groups="[{ value: 'STOCK', label: 'STOCK' }, { value: 'CRYPTO', label: 'CRYPTO' }]"
1748
- mono
1749
- @pick="apply"
1750
- @close="open = false"
1751
- />
1752
- ```
1753
-
1754
- ---
1755
-
1756
- ### BaseBarDistribution
1757
-
1758
- Compact value-distribution chart: one thin rounded bar per distinct
1759
- value, count labelled on top and the value underneath — scrolls
1760
- sideways when there are many bars. Pure Tailwind, no chart library.
1761
- Extracted from TradeAutomation's variant-stats modal.
1762
-
1763
- **Props:**
1764
-
1765
- | Prop | Type | Default | Description |
1766
- | -------------- | -------- | ---------------------- | ----------- |
1767
- | `bars` | `Array` | **required** | `DistributionBar[]` — `{ label, count }` per bar, in display order (sort ascending for numeric values). |
1768
- | `ariaLabel` | `String` | `'Value distribution'` | Accessible description of the chart. |
1769
- | `countNoun` | `String` | `'item'` | Noun for each bar's tooltip count, e.g. `'variant'` → "3 variants". |
1770
- | `titlePrefix` | `String` | `''` | Tooltip prefix before the value, e.g. the field name. |
1771
- | `maxBarHeight` | `Number` | `56` | Height of the tallest bar, in px. |
1772
- | `barClass` | `String` | emerald fill | Tailwind classes for the bar fill. |
1773
-
1774
- ```vue
1775
- <BaseBarDistribution
1776
- :bars="[{ label: '0.5', count: 1 }, { label: '1', count: 4 }]"
1777
- aria-label="Distribution of Take profit across variants"
1778
- title-prefix="Take profit"
1779
- count-noun="variant"
1780
- />
1781
- ```
1782
-
1783
- ---
1784
-
1785
- ### BaseGroupedChecklist
1786
-
1787
- Grouped multi-select checklist (since 1.46.0): a master "Select all (N)"
1788
- checkbox, a per-group select-all, and one checkbox per item. Built for
1789
- "pick which records this action applies to" confirmation modals. Empty groups
1790
- are not rendered; items are plain strings, unique across groups.
1791
-
1792
- | Prop | Type | Default | Description |
1793
- | ---------------- | -------------------------- | ------------------ | ----------- |
1794
- | `groups` | `ChecklistGroup[]` | **required** | `{ key, label, items: string[] }` per group, in display order. |
1795
- | `modelValue` | `string[]` | **required** | Selected items (`v-model`); emitted in group order. |
1796
- | `allLabel` | `string` | `'Select all'` | Master checkbox text; group checkboxes are labelled `"<allLabel> <group label>"`. |
1797
- | `allAriaLabel` | `string` | `'Select all items'` | Accessible name of the master checkbox. |
1798
- | `itemAriaLabel` | `(item: string) => string` | the item itself | Accessible name per item checkbox. |
1799
- | `mono` | `boolean` | `false` | Monospace item labels (tickers, ids). |
1800
- | `maxHeightClass` | `string` | `'max-h-72'` | Scroll cap of the group list. |
1801
-
1802
- ```vue
1803
- <BaseGroupedChecklist
1804
- v-model="selected"
1805
- :groups="[{ key: 'STOCK', label: 'Stocks', items: ['AMD', 'NVDA'] }]"
1806
- all-aria-label="Select all symbols"
1807
- :item-aria-label="(s) => `Apply to ${s}`"
1808
- mono
1809
- />
1810
- ```
1811
-
1812
- ---
1813
-
1814
- ### BaseSearchSelect
1815
-
1816
- Searchable select (combobox): type to filter, pick with the mouse or
1817
- ArrowUp/ArrowDown + Enter. `multiple` turns it into a tag picker with removable
1818
- chips (Backspace on an empty search removes the last one). Same field skin as
1819
- `BaseInput` / `BaseSelect`; Escape and an outside click close the menu.
1820
-
1821
- **Props:**
1822
-
1823
- | Prop | Type | Default | Description |
1824
- | --------------- | ----------------------------- | --------------- | ----------- |
1825
- | `options` | `DropdownOption[]` | **required** | `{ value, label, title?, disabled? }` per row. |
1826
- | `modelValue` | `value \| null \| value[]` | `null` | Selected value, or values with `multiple`. |
1827
- | `multiple` | `Boolean` | `false` | Pick several (chips). |
1828
- | `placeholder` | `String` | `'Select'` | Shown when nothing is selected. |
1829
- | `searchable` | `Boolean` | `true` | Typing filters the options. |
1830
- | `clearable` | `Boolean` | `true` | Show the × clear button while something is selected. |
1831
- | `block` | `Boolean` | `true` | Full-width. |
1832
- | `disabled` | `Boolean` | `false` | |
1833
- | `ariaLabel` | `String` | `''` | Accessible name without a visible label. |
1834
- | `noResultsText` | `String` | `'No matches.'` | |
1835
- | `emptyText` | `String` | `'No options.'` | |
1836
-
1837
- **Emits:** `update:modelValue` — the value (single; `null` when cleared) or the
1838
- array of values (`multiple`).
1839
-
1840
- ```vue
1841
- <BaseSearchSelect v-model="userId" :options="users.map(u => ({ value: u.id, label: u.name }))" placeholder="Select User" />
1842
- <BaseSearchSelect v-model="tags" :options="TAGS" multiple />
1843
- ```
1844
-
1845
- ### BaseCard
1846
-
1847
- Bordered surface card — the shared chrome for forms, filter/search panels and
1848
- grouped content.
1849
-
1850
- **Props:** `title`, `subtitle`, `padding: 'md' | 'sm' | 'none'` (`'md'`).
1851
- **Slots:** `default` (body), `actions` (header, right), `footer` (bottom, right-aligned — submit/cancel).
1852
-
1853
- ```vue
1854
- <BaseCard title="Template">
1855
- <form>…</form>
1856
- <template #footer><BaseButton type="submit" description="Save" /></template>
1857
- </BaseCard>
1858
- ```
1859
-
1860
- ### BaseField
1861
-
1862
- Label + control + message wrapper for forms and filter bars.
1863
-
1864
- **Props:** `label`, `labelFor` (the control's id), `required`, `error`
1865
- (red, replaces the hint), `hint`, `compact` (small uppercase filter-bar label),
1866
- `grow` (takes the free space in a flex row).
1867
-
1868
- ```vue
1869
- <BaseCard padding="sm">
1870
- <div class="flex flex-wrap items-end gap-4">
1871
- <BaseField label="Search symbol" label-for="q" compact grow>
1872
- <BaseInput id="q" v-model="query" />
1873
- </BaseField>
1874
- <BaseField label="Class" compact>
1875
- <BaseSegmentedControl v-model="cls" :options="CLASSES" />
1876
- </BaseField>
1877
- </div>
1878
- </BaseCard>
1879
- ```
1880
-
1881
- ### BaseStatCard
1882
-
1883
- Dashboard stat tile in the `EarningsCard` style (dashed accent border, bold
1884
- title, big value) for any value — counts, labels, pre-formatted numbers.
1885
-
1886
- **Props:** `title` (required), `value`, `subtitle`,
1887
- `accent: 'emerald' | 'sky' | 'amber' | 'red' | 'slate'` (`'emerald'`),
1888
- `to` (router location — makes the whole card a link).
1889
- **Slots:** `icon` (fills the tinted chip; receives `iconClass`), `default` (extra content).
1890
-
1891
- ```vue
1892
- <BaseStatCard title="Categories" :value="71" accent="sky" :to="{ name: 'Category List' }">
1893
- <template #icon="{ iconClass }"><SwatchIcon :class="iconClass" /></template>
1894
- </BaseStatCard>
1895
- ```
1896
-
1897
- ### BaseCheckbox
1898
-
1899
- Rounded checkbox, emerald when checked (works with or without a forms plugin).
1900
- `v-model` is the boolean; `change` fires with the new value; attrs (`id`, `name`)
1901
- go to the `<input>`.
1902
-
1903
- **Props:** `modelValue`, `label`, `disabled`. **Slots:** `default` (rich label).
1904
- **Emits:** `update:modelValue(value)`, `change(value)`.
1905
-
1906
- ```vue
1907
- <BaseCheckbox v-model="remember" id="remember" label="Remember me" />
1908
- ```
1909
-
1910
- ### BaseToggle
1911
-
1912
- On/off switch (`role="switch"`). `v-model` is the boolean; `change` fires after
1913
- every user toggle with the new value.
1914
-
1915
- **Props:** `modelValue`, `label` (visible label, also the accessible name),
1916
- `ariaLabel` (when there is no label), `color: 'emerald' | 'red' | 'sky' | 'amber'`
1917
- (on state, `'emerald'`), `offTone: 'slate' | 'red'` (off track, `'slate'`), `disabled`.
1918
- **Emits:** `update:modelValue(value)`, `change(value)`.
1919
-
1920
- ```vue
1921
- <BaseToggle v-model="task.alive" :label="task.name" color="red" @change="save(task)" />
1922
- <BaseToggle v-model="account.alive" off-tone="red" aria-label="Alive" />
1923
- ```
1924
-
1925
- ### BaseAuthLayout
1926
-
1927
- Full-page, centred shell for sign-in / sign-up / password-recovery screens:
1928
- logo on top (defaults to `BaseLogo`), then a bordered card with the title,
1929
- content and an optional footer.
1930
-
1931
- **Props:** `title`, `subtitle`, `wide` (max-w-3xl card for multi-column forms; default max-w-md).
1932
- **Slots:** `default` (form), `logo` (replaces `BaseLogo`), `footer` ("No account? Sign up").
1933
-
1934
- ```vue
1935
- <BaseAuthLayout title="Forgot password">
1936
- <form>…</form>
1937
- <template #footer>No account? <RouterLink to="/register">Sign up</RouterLink></template>
1938
- </BaseAuthLayout>
1939
- ```
1940
-
1941
- ### BaseBooleanBadge
1942
-
1943
- Yes/no pill for boolean columns: green check when true, red cross when false.
1944
-
1945
- **Props:** `value` (`boolean | null`), `trueLabel` (`'Yes'`), `falseLabel` (`'No'`).
1946
-
1947
- ```vue
1948
- <BaseBooleanBadge :value="user.hasVerifiedEmail" true-label="Verified" false-label="Not verified" />
1949
- ```
1950
-
1951
- ### BaseDetailList
1952
-
1953
- Read-only label/value grid for detail pages (the "view" counterpart of a form).
1954
- Empty values show `emptyText`; `href` renders the value as an external link;
1955
- `mono` uses the monospace face.
1956
-
1957
- **Props:** `items: DetailItem[]` (`{ label, value?, href?, mono? }`, required),
1958
- `columns: 1 | 2 | 3` (`2`, from `sm` up), `emptyText` (`'—'`).
1959
-
1960
- ```vue
1961
- <BaseCard title="Details">
1962
- <BaseDetailList :items="[{ label: 'Status', value: post.status }, { label: 'Link', value: post.link, href: post.link }]" />
1963
- </BaseCard>
1964
- ```
1965
-
1966
- ## Composables
1967
-
1968
- ```ts
1969
- import {
1970
- initTheme,
1971
- useTheme,
1972
- useThemeClasses,
1973
- useEscapeKey,
1974
- useDebouncedRef,
1975
- useToast,
1976
- useMobileSidebar,
1977
- useSidebarCollapse,
1978
- useNotifications,
1979
- useQueryParamSync,
1980
- useFieldClasses,
1981
- usePolling,
1982
- useLatestRequest,
1983
- isAbortError,
1984
- useDirtyState,
1985
- stableStringify,
1986
- } from 'mgv-backoffice'
1987
- import type {
1988
- UseThemeOptions,
1989
- UseSidebarCollapseOptions,
1990
- UsePollingOptions,
1991
- LatestRequestToken,
1992
- } from 'mgv-backoffice'
1993
- ```
1994
-
1995
- | Composable | Purpose |
1996
- | ---------- | ------- |
1997
- | `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). |
1998
- | `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. |
1999
- | `useThemeClasses()` | Named Tailwind class roles for dark/light (card, border, primaryText, mutedText, dimText, input, ghostButton, emeraldText, redText, …). Since 1.34.0 returns a `reactive` object of plain strings — bind `t.card` directly, never `t.card.value` (the old ComputedRef shape leaked ref internals into `:class` bindings). |
2000
- | `useEscapeKey(handler)` | Component-scoped Escape key listener. |
2001
- | `useDebouncedRef(source, delay?)` | Debounced mirror of a ref. Timer cleared on scope dispose. |
2002
- | `useToast(durationMs?)` | Per-component toast state: `{ showToast, toastMessage, toastType, showToastMessage }`. `showToastMessage` also accepts a per-call duration override. |
2003
- | `useMobileSidebar()` | Singleton state shared between `BaseSidebar` and `BaseAppLayout` for the off-canvas open/closed flag. |
2004
- | `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'`). |
2005
- | `useNotifications()` | Singleton notification state shared by the sidebar bell and `BaseNotificationPanel`: `{ notifications, unreadCount, open, openPanel, closePanel, togglePanel, setNotifications, add, remove, markRead, markAllRead, clear }`. |
2006
- | `useQueryParamSync()` | URL-query mirroring for filterable views: `{ qparam(name), qenum(name, allowed, fallback), replaceQuery(next) }`. Read filters from the query string once on setup, write changes back with `router.replace` (no-op when unchanged) so filtered views stay shareable without polluting history. |
2007
- | `useFieldClasses()` | Shared form-field class strings for the gray/emerald form skin: `{ label, input, requiredInput(value) }`. `requiredInput` returns a red border+ring skin while the value is empty and the standard skin otherwise. |
2008
- | `usePolling(fn, intervalMs, { immediate?, pauseWhenHidden? })` | Visibility-gated polling loop bound to the component lifecycle: starts on mount, stops on unmount, pauses while the tab is hidden and refreshes + resumes on return to visible (both default on). Pass `intervalMs: null` for refresh-only mode (run on mount + each return-to-visible, no timer). Returns `{ start, stop, active }`. A loop stopped via `stop()` stays stopped across hide/show cycles (since 1.36.0) — only a visibility-paused loop auto-resumes. Catch errors inside `fn` — the loop never swallows rejections. |
2009
- | `useLatestRequest()` | "Latest request wins" guard for views that refetch on filter changes / polls / Refresh (since 1.45.0). `begin()` aborts the previous in-flight request and returns `{ signal, isLatest() }` — pass `signal` to `fetch`, and check `isLatest()` after every `await` before writing state (and before clearing a loading flag in `finally`). `cancel()` aborts + invalidates without starting a new request. Auto-cancels on unmount when called in setup. Pair with `isAbortError(e)` to swallow superseded requests silently. |
2010
- | `useDirtyState(snapshot)` | Unsaved-changes tracking for edit forms (since 1.46.0): `snapshot()` returns the form state (read reactively); `dirty` is true while it differs from the last baseline, `markClean()` makes the current state the baseline (call after load / save). Structural, key-order-insensitive comparison via `stableStringify` (also exported), so a server echo that reorders keys never reads as an edit. |
2011
-
2012
- ---
2013
-
2014
- ## Typography
2015
-
2016
- Since 1.33.0 the library ships the shared brand typography: the stylesheet
2017
- loads **Fira Sans** (UI text) and **Fira Code** (numerals/data) from Google
2018
- Fonts via `@import`, registers them as the Tailwind `--font-sans` /
2019
- `--font-mono` theme defaults, and applies `font-family: var(--font-sans)` to
2020
- `body`. Consumers get the fonts just by importing the lib CSS — remove any
2021
- app-local Google Fonts `<link rel="stylesheet">` and `--font-sans`/`--font-mono`
2022
- overrides. Keep (or add) the preconnect hints in `index.html` for a faster
2023
- first paint:
2024
-
2025
- ```html
2026
- <link rel="preconnect" href="https://fonts.googleapis.com" />
2027
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
2028
- ```
2029
-
2030
- ## Tailwind setup for consumers
2031
-
2032
- The lib's components rely on Tailwind utility classes (including dark-mode
2033
- variants). Consumers should add the lib's `dist` output to their Tailwind
2034
- `content` paths so the JIT can see the class names:
2035
-
2036
- ```js
2037
- // tailwind.config.js
2038
- export default {
2039
- content: [
2040
- './index.html',
2041
- './src/**/*.{vue,ts}',
2042
- './node_modules/mgv-backoffice/dist/**/*.{js,mjs,cjs,vue}',
2043
- ],
2044
- }
2045
- ```
2046
-
2047
- The legacy `tailwind.safelist.js` only covers the v1 components; the
2048
- recommended path for v4+ is the `content` glob above.
1
+ # mgv-backoffice
2
+
3
+ Shared Vue 3 UI component library built with TypeScript and Tailwind CSS.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install mgv-backoffice
9
+ ```
10
+
11
+ ### Peer Dependencies
12
+
13
+ These must be installed in your project:
14
+
15
+ ```bash
16
+ npm install vue@^3.5.0 vue-router@^5.0.0 @heroicons/vue@^2.0.0
17
+ ```
18
+
19
+ > `vue-router` 5.x is required (peer range `^5.0.0`) — it's what the library is
20
+ > developed and tested against. Upgrade from Router 4 before installing this library.
21
+
22
+ ### Import Styles
23
+
24
+ Include the library's stylesheet in your app entry point:
25
+
26
+ ```ts
27
+ import 'mgv-backoffice/dist/style.css'
28
+ ```
29
+
30
+ ### Tailwind Safelist
31
+
32
+ If your project uses Tailwind, import the safelist so dynamic classes used by this library are generated correctly:
33
+
34
+ ```js
35
+ // In your Tailwind config
36
+ import safelist from 'mgv-backoffice/tailwind.safelist'
37
+ ```
38
+
39
+ Or include the pre-built CSS safelist:
40
+
41
+ ```css
42
+ @import 'mgv-backoffice/tailwind.safelist.css';
43
+ ```
44
+
45
+ > Maintainers: `tailwind.safelist.js` is the single source of truth.
46
+ > `tailwind.safelist.css` is generated from it via `npm run safelist`
47
+ > (runs automatically before `npm run build`) — don't edit it by hand.
48
+
49
+ ---
50
+
51
+ ## Components
52
+
53
+ ### BaseAlert
54
+
55
+ Inline notice panel with color-coded variants (error / warning / success /
56
+ info), theme-aware via the shared `isDark` ref. The optional default slot
57
+ renders body content under the title, and `compact` gives a slim text-xs
58
+ variant for in-form warnings.
59
+
60
+ **Props:**
61
+
62
+ | Prop | Type | Default | Description |
63
+ | --------- | ----------- | ------------------ | ------------------------ |
64
+ | `title` | `String` | `''` | Bold headline (optional when the slot carries the message). |
65
+ | `color` | `AlertEnum` | `AlertEnum.ERROR` | Alert color variant. |
66
+ | `compact` | `Boolean` | `false` | Slim variant: text-xs, smaller icon/padding. |
67
+
68
+ **Slots:** default — body content rendered under the title.
69
+
70
+ **Example:**
71
+
72
+ ```vue
73
+ <template>
74
+ <BaseAlert title="Operation successful" :color="AlertEnum.SUCCESS" />
75
+ <BaseAlert title="Proxying is active." :color="AlertEnum.WARNING">
76
+ <p class="mt-0.5 text-xs opacity-90">The canned response below is ignored.</p>
77
+ </BaseAlert>
78
+ <BaseAlert compact :color="AlertEnum.WARNING">
79
+ Chunked dribble is ignored while Fault Simulation is active.
80
+ </BaseAlert>
81
+ </template>
82
+
83
+ <script setup lang="ts">
84
+ import { BaseAlert, AlertEnum } from 'mgv-backoffice'
85
+ </script>
86
+ ```
87
+
88
+ ---
89
+
90
+ ### BaseBadge
91
+
92
+ Colored status badge/pill. Renders nothing when the default slot is empty;
93
+ an omitted or unrecognized `color` falls back to the red palette.
94
+
95
+ **Props:**
96
+
97
+ | Prop | Type | Default | Description |
98
+ | ------- | -------- | ------- | --------------------------------- |
99
+ | `color` | `String` | — | Color variant (use `ColorsEnums`) |
100
+
101
+ **Slots:** `default` — badge label content.
102
+
103
+ **Example:**
104
+
105
+ ```vue
106
+ <template>
107
+ <BaseBadge :color="ColorsEnums.GREEN">Active</BaseBadge>
108
+ <BaseBadge :color="ColorsEnums.RED">Inactive</BaseBadge>
109
+ </template>
110
+
111
+ <script setup lang="ts">
112
+ import { BaseBadge, ColorsEnums } from 'mgv-backoffice'
113
+ </script>
114
+ ```
115
+
116
+ ---
117
+
118
+ ### BaseBreadcrumb
119
+
120
+ Breadcrumb navigation. Provide items manually or pass a URL path for
121
+ auto-generation; with neither prop it auto-generates from
122
+ `window.location.pathname`. A "Home" crumb linking to `/` is always prepended.
123
+
124
+ **Props:**
125
+
126
+ | Prop | Type | Default | Description |
127
+ | ------- | --------------- | ----------- | ------------------------------------------ |
128
+ | `items` | `BreadCrumb[]` | `undefined` | Manual breadcrumb entries |
129
+ | `path` | `String` | `undefined` | URL path for auto-generated breadcrumbs |
130
+
131
+ **BreadCrumb type:**
132
+
133
+ ```ts
134
+ interface BreadCrumb {
135
+ name: string
136
+ url: string
137
+ }
138
+ ```
139
+
140
+ **Example:**
141
+
142
+ ```vue
143
+ <template>
144
+ <!-- Manual -->
145
+ <BaseBreadcrumb :items="[
146
+ { name: 'Home', url: '/' },
147
+ { name: 'Users', url: '/users' },
148
+ { name: 'Profile', url: '/users/1' }
149
+ ]" />
150
+
151
+ <!-- Auto-generated from path -->
152
+ <BaseBreadcrumb path="/users/settings/profile" />
153
+ </template>
154
+
155
+ <script setup lang="ts">
156
+ import { BaseBreadcrumb } from 'mgv-backoffice'
157
+ import type { BreadCrumb } from 'mgv-backoffice'
158
+ </script>
159
+ ```
160
+
161
+ ---
162
+
163
+ ### BaseButton
164
+
165
+ Button with color, size, loading state, and Vue Router integration.
166
+
167
+ **Props:**
168
+
169
+ | Prop | Type | Default | Description |
170
+ | ------------- | ----------------------------------- | --------------------- | ------------------------------------ |
171
+ | `description` | `String` | **required** | Button label text |
172
+ | `color` | `String` | `BaseButtonEnum.GREEN` | Color variant (`BLUE`/`WHITE`/`DARK`/`GREEN`/`EMERALD`/`RED`/`YELLOW`/`PURPLE`/`SKY`/`GRAY`/`AMBER`) |
173
+ | `outline` | `Boolean` | `false` | Outlined/secondary style — transparent fill, coloured text + border, tinted hover (theme-aware) |
174
+ | `ghost` | `Boolean` | `false` | Ghost/borderless style — no border or fill, coloured text + tinted hover (theme-aware). For compact toolbar/action buttons |
175
+ | `to` | `String` | — | Vue Router path (renders `<router-link>`) |
176
+ | `type` | `'button' \| 'submit' \| 'reset'` | `'button'` | HTML button type |
177
+ | `iconLeft` | `Boolean` | `false` | Render the slot icon before the label |
178
+ | `isRounded` | `Boolean` | — | Fully rounded corners |
179
+ | `isDisable` | `Boolean` | — | Disabled state |
180
+ | `size` | `String` | — | Size variant (use `BaseButtonSizeEnum`) |
181
+ | `isLoading` | `Boolean` | — | Show loading spinner |
182
+
183
+ **Slots:** `default`
184
+
185
+ **Example:**
186
+
187
+ ```vue
188
+ <template>
189
+ <BaseButton description="Submit" :color="BaseButtonEnum.GREEN" type="submit" />
190
+ <BaseButton description="Go to Users" :to="'/users'" />
191
+ <BaseButton description="Saving..." :isLoading="true" :isDisable="true" />
192
+ <BaseButton
193
+ description="Delete"
194
+ :color="BaseButtonEnum.RED"
195
+ :size="BaseButtonSizeEnum.SMALL"
196
+ />
197
+ <!-- Outlined / secondary -->
198
+ <BaseButton description="Import" :color="BaseButtonEnum.EMERALD" outline iconLeft>
199
+ <ArrowUpTrayIcon class="w-4 h-4 mr-1.5" />
200
+ </BaseButton>
201
+ <!-- Ghost / borderless toolbar action -->
202
+ <BaseButton description="Logs" :color="BaseButtonEnum.SKY" ghost iconLeft :size="BaseButtonSizeEnum.SMALL">
203
+ <ClipboardDocumentListIcon class="w-4 h-4 mr-1.5" />
204
+ </BaseButton>
205
+ </template>
206
+
207
+ <script setup lang="ts">
208
+ import { BaseButton, BaseButtonEnum, BaseButtonSizeEnum } from 'mgv-backoffice'
209
+ </script>
210
+ ```
211
+
212
+ ---
213
+
214
+ ### BaseLine
215
+
216
+ Horizontal divider with style variants.
217
+
218
+ **Props:**
219
+
220
+ | Prop | Type | Default | Description |
221
+ | ------ | -------- | --------------- | ----------------- |
222
+ | `mode` | `String` | `LineEnum.BASE` | Divider style |
223
+
224
+ **Example:**
225
+
226
+ ```vue
227
+ <template>
228
+ <BaseLine />
229
+ <BaseLine :mode="LineEnum.SQUARE" />
230
+ </template>
231
+
232
+ <script setup lang="ts">
233
+ import { BaseLine, LineEnum } from 'mgv-backoffice'
234
+ </script>
235
+ ```
236
+
237
+ ---
238
+
239
+ ### BaseLogo
240
+
241
+ SVG brand logo component.
242
+
243
+ **Props:**
244
+
245
+ | Prop | Type | Default | Description |
246
+ | ------ | -------- | ---------------------- | ------------ |
247
+ | `size` | `String` | `BaseLogoEnum.MEDIUM` | Logo size |
248
+
249
+ **Example:**
250
+
251
+ ```vue
252
+ <template>
253
+ <BaseLogo :size="BaseLogoEnum.LARGE" />
254
+ </template>
255
+
256
+ <script setup lang="ts">
257
+ import { BaseLogo, BaseLogoEnum } from 'mgv-backoffice'
258
+ </script>
259
+ ```
260
+
261
+ ---
262
+
263
+ ### BaseModal
264
+
265
+ > ⚠️ **Deprecated.** Prefer [`BaseConfirmModal`](#baseconfirmmodal) for confirm/cancel
266
+ > flows or [`BaseModalShell`](#basemodalshell) for custom dialogs — they support dark
267
+ > mode, teleport to `<body>`, and slot-based composition. Kept for backward
268
+ > compatibility.
269
+
270
+ Confirmation dialog with support for delete and success modes. `DELETE` mode renders
271
+ the confirm/cancel pair; any other mode renders the title, optional `description`,
272
+ the default slot, and a single OK button that emits `closeModal`.
273
+
274
+ **Props:**
275
+
276
+ | Prop | Type | Default | Description |
277
+ | ------------- | -------- | ----------- |-------------------------------------|
278
+ | `title` | `String` | **required**| Modal heading |
279
+ | `description` | `String` | — | Body text |
280
+ | `to` | `String` | `"/"` | Unused — declared for backward compatibility only; confirm just emits `confirmModal`, no navigation happens |
281
+ | `mode` | `String` | `'SUCCESS'` | Modal variant (use `BaseModalEnum`) |
282
+
283
+ **Events:**
284
+
285
+ | Event | Description |
286
+ | -------------- | ------------------------------- |
287
+ | `closeModal` | Emitted when modal is dismissed (cancel/OK button, backdrop click, or Escape) |
288
+ | `confirmModal` | Emitted on confirm action |
289
+
290
+ **Example:**
291
+
292
+ ```vue
293
+ <template>
294
+ <BaseModal
295
+ title="Delete this item?"
296
+ description="This action cannot be undone."
297
+ :mode="BaseModalEnum.DELETE"
298
+ @closeModal="showModal = false"
299
+ @confirmModal="handleDelete"
300
+ />
301
+ </template>
302
+
303
+ <script setup lang="ts">
304
+ import { ref } from 'vue'
305
+ import { BaseModal, BaseModalEnum } from 'mgv-backoffice'
306
+
307
+ const showModal = ref(true)
308
+ const handleDelete = () => { /* ... */ }
309
+ </script>
310
+ ```
311
+
312
+ ---
313
+
314
+ ### BaseRow
315
+
316
+ Card-like content container with border and shadow.
317
+
318
+ **Props:**
319
+
320
+ | Prop | Type | Default | Description |
321
+ | --------- | -------- | --------- | ---------------------- |
322
+ | `bgColor` | `String` | `"bg-white"` | Background color — a full Tailwind class (e.g. `"bg-slate-50"`), not a bare color name |
323
+
324
+ **Slots:** `default` — row content.
325
+
326
+ **Example:**
327
+
328
+ ```vue
329
+ <template>
330
+ <BaseRow>
331
+ <p>Card content goes here</p>
332
+ </BaseRow>
333
+ </template>
334
+
335
+ <script setup lang="ts">
336
+ import { BaseRow } from 'mgv-backoffice'
337
+ </script>
338
+ ```
339
+
340
+ ---
341
+
342
+ ### BaseSpinner
343
+
344
+ Animated loading spinner — a neutral ring with a coloured leading arc.
345
+
346
+ **Props:**
347
+
348
+ | Prop | Type | Default | Description |
349
+ |---------|----------------------------------------------------------------------------|----------|-------------------------------------------------------------|
350
+ | `size` | `'sm' \| 'md' \| 'lg' \| 'xl'` | `'sm'` | Diameter + ring thickness — 16 / 24 / 32 / 48px. |
351
+ | `color` | `'blue' \| 'emerald' \| 'sky' \| 'indigo' \| 'teal' \| 'purple' \| 'red' \| 'amber'` | `'emerald'` | Colour of the spinning arc. The track stays neutral gray. |
352
+
353
+ With no props it renders a 16px emerald spinner. For page loaders use `size="xl"` centered, e.g. `<div class="flex justify-center py-12"><BaseSpinner size="xl" /></div>`.
354
+
355
+ **Example:**
356
+
357
+ ```vue
358
+ <template>
359
+ <!-- legacy default -->
360
+ <BaseSpinner />
361
+ <!-- larger, themed -->
362
+ <BaseSpinner size="lg" color="emerald" />
363
+ </template>
364
+
365
+ <script setup lang="ts">
366
+ import { BaseSpinner } from 'mgv-backoffice'
367
+ </script>
368
+ ```
369
+
370
+ ---
371
+
372
+ ### BaseToast
373
+
374
+ Toast notification with positioning and auto-dismiss.
375
+
376
+ **Props:**
377
+
378
+ | Prop | Type | Default | Description |
379
+ | -------------- | ---------------- | --------- | ------------------------------------------ |
380
+ | `mode` | `BaseToastEnum` | **required** | Toast variant (SUCCESS, WARNING, ERROR) |
381
+ | `description` | `String` | **required** | Message text |
382
+ | `hasCloseIcon` | `Boolean` | `true` | Show close button |
383
+ | `positioning` | `String` | `PositioningEnum.TOP_RIGHT` | Screen position (use `PositioningEnum`) |
384
+ | `duration` | `Number` | `5000` | Auto-dismiss delay in milliseconds |
385
+
386
+ **Example:**
387
+
388
+ ```vue
389
+ <template>
390
+ <BaseToast
391
+ :mode="BaseToastEnum.SUCCESS"
392
+ description="Changes saved successfully!"
393
+ :positioning="PositioningEnum.TOP_RIGHT"
394
+ />
395
+ </template>
396
+
397
+ <script setup lang="ts">
398
+ import { BaseToast, BaseToastEnum, PositioningEnum } from 'mgv-backoffice'
399
+ </script>
400
+ ```
401
+
402
+ ---
403
+
404
+ ### ColoredSquares
405
+
406
+ Colored square indicator with randomized pastel accent.
407
+
408
+ **Props:**
409
+
410
+ | Prop | Type | Default | Description |
411
+ | ------- | -------- | ------- | --------------------------------- |
412
+ | `color` | `String` | — | Color variant (use `ColorsEnums`) |
413
+
414
+ **Slots:** `default` — label content.
415
+
416
+ **Example:**
417
+
418
+ ```vue
419
+ <template>
420
+ <ColoredSquares :color="ColorsEnums.BLUE">Category A</ColoredSquares>
421
+ </template>
422
+
423
+ <script setup lang="ts">
424
+ import { ColoredSquares, ColorsEnums } from 'mgv-backoffice'
425
+ </script>
426
+ ```
427
+
428
+ ---
429
+
430
+ ### EarningsCard
431
+
432
+ Earnings summary card with formatted currency display. Supports a signed P&L
433
+ mode that renders a red loss theme (and a downward trend glyph) for negative
434
+ amounts.
435
+
436
+ **Props:**
437
+
438
+ | Prop | Type | Default | Description |
439
+ | ---------- | -------- |-------------------------|----------------------|
440
+ | `title` | `String` | `'TOTAL EARNINGS'` | Card heading |
441
+ | `amount` | `Number` | `0` | Monetary value (a stringified number is coerced) |
442
+ | `subtitle` | `String` | `'Lifetime commission'` | Subheading text |
443
+ | `badge` | `String` | `''` | Optional badge label |
444
+ | `currency` | `String` | `'$'` | Currency symbol |
445
+ | `decimals` | `Number` | `2` | Fraction digits shown for the amount |
446
+ | `accent` | `'orange' \| 'emerald' \| 'red'` | `'orange'` | Card theme. `emerald` tints it green; `red` is the loss theme. |
447
+ | `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. |
448
+ | `compact` | `Boolean` | `false` | Dense variant for dashboards that tile many cards on one row: tighter padding, smaller type, trend glyph shrunk into the top-right corner. Don't combine with `badge` — both occupy the top-right corner. |
449
+
450
+ **Example:**
451
+
452
+ ```vue
453
+ <template>
454
+ <!-- Always-positive total: original behaviour. -->
455
+ <EarningsCard
456
+ title="Monthly Revenue"
457
+ :amount="12500"
458
+ subtitle="April 2026"
459
+ currency="€"
460
+ />
461
+
462
+ <!-- Signed P&L: renders red + a down arrow when the amount is negative. -->
463
+ <EarningsCard
464
+ title="TOTAL P&L"
465
+ :amount="-128.4"
466
+ subtitle="Realised + unrealised"
467
+ accent="emerald"
468
+ signed
469
+ />
470
+ </template>
471
+
472
+ <script setup lang="ts">
473
+ import { EarningsCard } from 'mgv-backoffice'
474
+ </script>
475
+ ```
476
+
477
+ ---
478
+
479
+ ### EuroAmount
480
+
481
+ Formatted euro currency display with conditional color coding.
482
+
483
+ **Props:**
484
+
485
+ | Prop | Type | Default | Description |
486
+ | -------------- | --------- | ------- |-------------------------------------------------|
487
+ | `amount` | `Number` | — | Value to display |
488
+ | `beforeAmount` | `Number` | `null` | Previous value: red if `amount < beforeAmount`, green if `amount >= beforeAmount`. When omitted, a non-negative amount renders neutral (never green); a negative amount is always red. |
489
+ | `showCurrency` | `Boolean` | `true` | Show euro symbol |
490
+
491
+ **Example:**
492
+
493
+ ```vue
494
+ <template>
495
+ <!-- Shows green (amount > beforeAmount) -->
496
+ <EuroAmount :amount="1500" :beforeAmount="1200" />
497
+
498
+ <!-- Shows red (negative) -->
499
+ <EuroAmount :amount="-300" />
500
+
501
+ <!-- Without currency symbol -->
502
+ <EuroAmount :amount="800" :showCurrency="false" />
503
+ </template>
504
+
505
+ <script setup lang="ts">
506
+ import { EuroAmount } from 'mgv-backoffice'
507
+ </script>
508
+ ```
509
+
510
+ ---
511
+
512
+ ### Pagination
513
+
514
+ Page navigation with smart ellipsis for large page counts.
515
+
516
+ **Props:**
517
+
518
+ | Prop | Type | Default | Description |
519
+ | -------------- | -------- | ------- | ----------------------- |
520
+ | `totalItems` | `Number` | **required** | Total number of items |
521
+ | `itemsPerPage` | `Number` | `20` | Items shown per page |
522
+ | `page` | `Number` | — | Optional **controlled** current page (1-based, since 1.45.0). When set, the highlight follows this prop instead of internal state — pass it whenever the page can be restored from the URL, or the pager shows page 1 over page N's rows. Out-of-range values clamp to the last page. Omit for the old uncontrolled behaviour. |
523
+
524
+ **Events:**
525
+
526
+ | Event | Payload | Description |
527
+ | -------------- | -------- |----------------------------------|
528
+ | `page-changed` | `Number` | Emitted with the new page number |
529
+
530
+ **Example:**
531
+
532
+ ```vue
533
+ <template>
534
+ <Pagination
535
+ :totalItems="200"
536
+ :itemsPerPage="10"
537
+ @page-changed="onPageChange"
538
+ />
539
+ <!-- Controlled: the parent owns the page (e.g. mirrored to ?page=). -->
540
+ <Pagination
541
+ :page="page"
542
+ :totalItems="200"
543
+ :itemsPerPage="10"
544
+ @page-changed="(p) => (page = p)"
545
+ />
546
+ </template>
547
+
548
+ <script setup lang="ts">
549
+ import { ref } from 'vue'
550
+ import { Pagination } from 'mgv-backoffice'
551
+
552
+ const page = ref(3)
553
+ const onPageChange = (page: number) => {
554
+ console.log('Page:', page)
555
+ }
556
+ </script>
557
+ ```
558
+
559
+ ---
560
+
561
+ ### TrendArrow
562
+
563
+ Up/down trend indicator displayed as a colored badge.
564
+
565
+ **Props:**
566
+
567
+ | Prop | Type | Default | Description |
568
+ | -------- | -------- | ------- |------------------------------------------------------|
569
+ | `number` | `Number` | — | Positive = green arrow up, negative = red arrow down, zero = neutral gray dash |
570
+ | `icon` | `String` | — | Optional suffix appended after the number (e.g. `"%"`) |
571
+
572
+ **Example:**
573
+
574
+ ```vue
575
+ <template>
576
+ <TrendArrow :number="12.5" /> <!-- Green up arrow -->
577
+ <TrendArrow :number="-3.2" /> <!-- Red down arrow -->
578
+ <TrendArrow :number="0" /> <!-- Neutral gray dash -->
579
+ </template>
580
+
581
+ <script setup lang="ts">
582
+ import { TrendArrow } from 'mgv-backoffice'
583
+ </script>
584
+ ```
585
+
586
+ ---
587
+
588
+ ## Enums
589
+
590
+ All enums are importable directly from the package:
591
+
592
+ ```ts
593
+ import {
594
+ AlertEnum,
595
+ BaseBadgeEnum,
596
+ BaseButtonEnum,
597
+ BaseButtonSizeEnum,
598
+ BaseLogoEnum,
599
+ BaseModalEnum,
600
+ BaseToastEnum,
601
+ ColorsEnums,
602
+ LineEnum,
603
+ PositioningEnum
604
+ } from 'mgv-backoffice'
605
+ ```
606
+
607
+ | Enum | Values |
608
+ | -------------------- |---------------------------------------------------------------|
609
+ | `AlertEnum` | `WARNING`, `ERROR`, `SUCCESS`, `INFO` |
610
+ | `BaseBadgeEnum` | `WIN`, `LOSE` |
611
+ | `BaseButtonEnum` | `RED`, `BLUE`, `WHITE`, `DARK`, `GREEN`, `EMERALD`, `YELLOW`, `PURPLE`, `SKY`, `GRAY`, `AMBER` |
612
+ | `BaseButtonSizeEnum` | `EXTRA_SMALL`, `SMALL`, `BASE`, `LARGE`, `EXTRA_LARGE` |
613
+ | `BaseLogoEnum` | `SMALL`, `MEDIUM`, `LARGE` (`BaseLoginEnum` is a deprecated alias) |
614
+ | `BaseModalEnum` | `DELETE`, `SUCCESS` |
615
+ | `BaseToastEnum` | `SUCCESS`, `WARNING`, `ERROR` |
616
+ | `ColorsEnums` | `NONE`, `RED`, `YELLOW`, `BLACK`, `GRAY`, `GREEN`, `BLUE` |
617
+ | `LineEnum` | `BASE`, `BASE_SHORTER`, `SQUARE` |
618
+ | `PositioningEnum` | `TOP_LEFT`, `TOP_RIGHT`, `BOTTOM_LEFT`, `BOTTOM_RIGHT` |
619
+
620
+ ---
621
+
622
+ ## Types
623
+
624
+ ```ts
625
+ import type { BreadCrumb, DropdownOption, PnL, PnLInputs } from 'mgv-backoffice'
626
+ ```
627
+
628
+ | Type | Shape |
629
+ | ---------------- | -------------------------------------- |
630
+ | `BreadCrumb` | `{ name: string; url: string }` |
631
+ | `DropdownOption` | `{ value: string \| number; label: string; title?: string; disabled?: boolean }` |
632
+ | `PnLInputs` | `{ buyPrice; lastPrice; filledQty }` (each `number \| string \| null \| undefined`) |
633
+ | `PnL` | `{ pnlUsd: number \| null; pnlPct: number \| null }` |
634
+
635
+ The other exported types — `NavItem`, `NavSection`, `EntityPickerItem`,
636
+ `LoginCredentials`, `NotificationItem`, `SegmentedOption`, `TableColumn`,
637
+ `SpecField`, `SpecFieldType`, `SpecFieldValue`, `StatBreakdownItem`,
638
+ `PillPickerItem`, `DistributionBar`, `CredentialsView`, `CredentialsUpdate` —
639
+ are documented in their component's section.
640
+
641
+ ---
642
+
643
+ ## Utilities
644
+
645
+ ```ts
646
+ import { getBaseColor, getBaseColorOf } from 'mgv-backoffice'
647
+ ```
648
+
649
+ | Function | Signature | Returns |
650
+ | ---------------- | ---------------------------------- | ----------------------------------- |
651
+ | `getBaseColor` | `(c: AlertEnum) => string` | Tailwind color name for alert type |
652
+ | `getBaseColorOf` | `(c: ColorsEnums) => string` | Tailwind color name for color enum |
653
+
654
+ ### HTTP colours
655
+
656
+ ```ts
657
+ import {
658
+ methodBadgeSolid,
659
+ methodBadgeBright,
660
+ methodBadgeTinted,
661
+ statusBadgeSolid,
662
+ statusBadgeTinted,
663
+ statusBadgeSoft,
664
+ } from 'mgv-backoffice'
665
+ ```
666
+
667
+ Tailwind class helpers for HTTP method and status code badges. `Solid` variants
668
+ return saturated `bg-*-600` classes for use on neutral surfaces; `Bright` /
669
+ `Tinted` variants return softer combinations suitable for cards. `statusBadgeTinted`
670
+ takes `(status, isDark)` to adapt between themes.
671
+
672
+ `methodBadgeTinted(method, isDark)` gives each method its own hue on a soft
673
+ tinted surface (`bg-*-500/15` dark / `bg-*-100` light; GET blue, POST emerald,
674
+ PUT amber, DELETE red, PATCH purple, HEAD sky) — the card-chip palette used by
675
+ WireMate's mock/stub cards. `statusBadgeSoft(status, isDark)` is its status
676
+ companion keyed by status class (emerald 2xx / sky 3xx / amber 4xx / red 5xx).
677
+
678
+ ### Key/value row validators
679
+
680
+ ```ts
681
+ import { rowKeyMissing, rowValueMissing } from 'mgv-backoffice'
682
+ import type { KeyValueRowLike } from 'mgv-backoffice'
683
+ ```
684
+
685
+ | Function | Signature | Returns |
686
+ | ----------------- | ---------------------------------------------------------------------- | ------- |
687
+ | `rowValueMissing` | `(row: { key?, value?, matcherType? }) => boolean` | `true` when the row has a key but no value. |
688
+ | `rowKeyMissing` | `(row: { key?, value?, matcherType? }) => boolean` | `true` when the row has a value but no key. |
689
+
690
+ Consistency checks for dynamic key/value grids (header lists, query params,
691
+ metadata rows). Rows with `matcherType: 'absent'` are exempt — an absent
692
+ matcher intentionally carries no value.
693
+
694
+ ### Input validators
695
+
696
+ ```ts
697
+ import { isValidAbsoluteUrl, isValidJson, isValidXml, isValidBase64 } from 'mgv-backoffice'
698
+ ```
699
+
700
+ Pure, dependency-free form-input validators. The payload validators treat
701
+ empty/whitespace-only input as **valid** — required-ness is a separate rule
702
+ from well-formedness; `isValidAbsoluteUrl` validates a value that must exist,
703
+ so empty is invalid there.
704
+
705
+ | Function | Signature | Returns |
706
+ | -------------------- | ---------------------------- | ------- |
707
+ | `isValidAbsoluteUrl` | `(value: string) => boolean` | `true` for an absolute `http://` / `https://` URL (other schemes rejected). |
708
+ | `isValidJson` | `(str: string) => boolean` | `true` when empty or parseable as JSON. |
709
+ | `isValidXml` | `(str: string) => boolean` | `true` when empty or well-formed XML (DOMParser `<parsererror>` check; browser-only). |
710
+ | `isValidBase64` | `(str: string) => boolean` | `true` when empty or well-formed base64 (whitespace stripped, length/alphabet checked, then `atob` as the final authority). |
711
+
712
+ ### HTML sanitizer
713
+
714
+ ```ts
715
+ import { sanitizeHtml, isSafeHref } from 'mgv-backoffice'
716
+ ```
717
+
718
+ | Function | Signature | Returns |
719
+ | -------------- | ----------------------------------------------- | ------- |
720
+ | `sanitizeHtml` | `(raw: string \| undefined \| null) => string` | Allow-list–sanitised HTML safe for `v-html`. |
721
+ | `isSafeHref` | `(value: string) => boolean` | `true` if the href uses a safe scheme (http/https/mailto/tel, root-relative, or anchor) or is empty/whitespace-only. |
722
+
723
+ Allow-list sanitizer for strings bound into `v-html`. Keeps a small set of
724
+ formatting tags (`a`, `b`/`strong`, `i`/`em`, `code`, `pre`, `p`, `ul`/`ol`/`li`,
725
+ `span`, `div`, `br`), strips all other elements (unwrapping to text, or dropping
726
+ content entirely for `script`/`style`/`iframe`/etc.), removes every attribute
727
+ except `href`/`title` on anchors, keeps only allow-listed hrefs (http/https/
728
+ mailto/tel, root-relative `/`, anchors `#`, or empty — every other scheme such
729
+ as `javascript:`, `data:`, `ftp:` is stripped), and hardens surviving links with
730
+ `rel="noopener noreferrer" target="_blank"`. Browser-only (uses `DOMParser`).
731
+
732
+ ```ts
733
+ sanitizeHtml('<p>Hi<script>alert(1)<\/script></p>') // '<p>Hi</p>'
734
+ ```
735
+
736
+ ### Display formatters
737
+
738
+ ```ts
739
+ import {
740
+ fmtNumber,
741
+ fmtDate,
742
+ fmtDateTime,
743
+ fmtDateShort,
744
+ fmtPrice,
745
+ fmtPct,
746
+ fmtUsd,
747
+ fmtMoney,
748
+ // …plus fmtDateTimeMs, fmtCalendarDate, fmtCalendarDateTime,
749
+ // fmtMsAsSeconds, fmtBytes, fmtDuration, formatJson, stringifyValue
750
+ } from 'mgv-backoffice'
751
+ ```
752
+
753
+ Locale-aware, pure, dependency-free formatters for tables, logs and charts.
754
+ `fmtNumber`, `fmtMoney`, `fmtDate`, `fmtCalendarDate`, `fmtCalendarDateTime` and
755
+ `fmtDuration` handle missing/non-finite input gracefully (rendering an
756
+ em-dash) so raw API values can be passed without pre-sanitising; the
757
+ epoch/numeric formatters (`fmtDateTime`, `fmtDateShort`, `fmtDateTimeMs`,
758
+ `fmtPrice`, `fmtPct`, `fmtUsd`) expect valid input and will render
759
+ `"Invalid Date"` / `"NaN"` otherwise.
760
+
761
+ | Function | Signature | Returns |
762
+ | -------------- | --------------------------------------------------------------- | ------- |
763
+ | `fmtNumber` | `(n: number \| string \| null \| undefined, digits = 4) => string` | Fixed-fraction number; em-dash for null/undefined/non-finite. Accepts numeric strings. |
764
+ | `fmtDate` | `(s: string \| number \| null \| undefined) => string` | Locale date-time from ISO string or epoch; em-dash on empty, raw value on parse failure. |
765
+ | `fmtDateTime` | `(ms: number) => string` | Compact `"Mon D, HH:MM"` label from epoch-millis (chart axes/tooltips). |
766
+ | `fmtDateTimeMs`| `(s: string \| number) => string` | Full 24-hour locale date-time WITH the millisecond fraction — for dense feeds where same-second rows must stay distinguishable. |
767
+ | `fmtDateShort` | `(ms: number) => string` | Short `"Mon D"` calendar label from epoch-millis. |
768
+ | `fmtCalendarDate` | `(s: string \| number \| null \| undefined) => string` | `"Mon D, YYYY"` en-US calendar label; em-dash on empty, raw value on parse failure. |
769
+ | `fmtCalendarDateTime` | `(s: string \| number \| null \| undefined) => string` | `"Mon D, YYYY, HH:MM"` en-US calendar label with time of day. |
770
+ | `fmtMsAsSeconds` | `(ms: number \| null \| undefined) => string` | `"= 1.50 s"` magnitude hint for millisecond inputs (3 decimals below 1 s); `''` for non-positive input. |
771
+ | `fmtBytes` | `(bytes: number \| null \| undefined) => string` | `"512 B"` / `"1.5 KB"` / `"2.0 MB"`; `''` for zero/falsy input. |
772
+ | `fmtDuration` | `(ms: number \| null \| undefined, maxUnits = 2) => string` | Compact day/hour/minute duration, e.g. `"3d 5h"` / `"5h 12m"` / `"12m"`. Zero-value leading units are dropped; `maxUnits` caps how many units render. Em-dash for null/undefined/non-finite. |
773
+ | `fmtPrice` | `(n: number) => string` | Price with precision that scales to magnitude (more decimals for sub-cent values). |
774
+ | `fmtPct` | `(n: number, digits = 2) => string` | Percentage with explicit sign, e.g. `"+2.50%"`. |
775
+ | `fmtUsd` | `(v: number) => string` | Signed USD amount with leading sign, e.g. `"+$5.00"`. |
776
+ | `fmtMoney` | `(n: number \| string \| null \| undefined, digits = 2, currency = '$') => string` | Money with the minus BEFORE the currency (`"-$2.26"`, never `"$-2.26"`), digit grouping, no `+` on gains; a value rounding to zero reads unsigned; em-dash for missing input (since 1.45.0). |
777
+ | `currencySymbol` | `(code: string \| null \| undefined) => string \| null` | Display symbol for a currency code: `€` EUR, `£` GBP, `$` for USD, USDT, USDC and a missing code; `null` for anything else (BTC, ETH…) so the caller suffixes the code (since 1.47.0). |
778
+ | `fmtCurrencyAmount` | `(n, code, digits = 2) => string` | An amount in its own currency: `"€20.00"`, `"-$1.50"`, `"$25.00 USDT"` (stablecoin ticker kept), `"0.5 BTC"` (plain quantity, up to 8 decimals); em-dash for missing input (since 1.47.0). |
779
+ | `formatJson` | `(content: string) => string` | Pretty-prints parseable JSON with 2-space indentation; returns anything else verbatim. |
780
+ | `stringifyValue` | `(value: unknown) => string` | Display string for an unknown value: strings pass through, null/undefined → `''`, everything else JSON-serialized (`String()` fallback). |
781
+
782
+ ### Spec-form helpers
783
+
784
+ ```ts
785
+ import { buildSpecParams, firstInvalidNumericSpec } from 'mgv-backoffice'
786
+ ```
787
+
788
+ Value-map helpers for spec-driven forms (the state behind `BaseSpecFields`).
789
+ A spec whose `default` is `null` is treated as OPTIONAL — blank means "knob
790
+ disabled" and passes validation.
791
+
792
+ | Function | Signature | Returns |
793
+ | ------------------------ | --------- | ------- |
794
+ | `buildSpecParams` | `(specs: SpecField[] \| undefined, existing: Record<string, SpecFieldValue>) => Record<string, SpecFieldValue>` | Value map seeded from each spec's `default`, keeping non-null overlapping values the caller already has (an existing `null` is re-seeded from the spec's `default`). |
795
+ | `firstInvalidNumericSpec`| `(specs: SpecField[] \| undefined, params: Record<string, SpecFieldValue>) => string \| null` | Label of the first blank / NaN numeric field, or `null` when all numerics are valid. |
796
+
797
+ ### Profit & loss
798
+
799
+ ```ts
800
+ import { computePnL } from 'mgv-backoffice'
801
+ import type { PnL, PnLInputs } from 'mgv-backoffice'
802
+ ```
803
+
804
+ | Function | Signature | Returns |
805
+ | ------------ | ------------------------------- | ------- |
806
+ | `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` / `lastPrice <= 0`. |
807
+
808
+ ```ts
809
+ interface PnLInputs {
810
+ buyPrice: number | string | null | undefined
811
+ lastPrice: number | string | null | undefined
812
+ filledQty: number | string | null | undefined
813
+ }
814
+
815
+ interface PnL {
816
+ pnlUsd: number | null
817
+ pnlPct: number | null
818
+ }
819
+ ```
820
+
821
+ ```ts
822
+ computePnL({ buyPrice: 100, lastPrice: 110, filledQty: 5 })
823
+ // { pnlUsd: 50, pnlPct: 10 }
824
+ ```
825
+
826
+ ---
827
+
828
+ ## Layout & shells (Tier 2 — full backoffice chrome)
829
+
830
+ ### BaseAppLayout
831
+
832
+ Root layout: dark/light page background, skip link, `<main>`-with-inert wrapper.
833
+ The `<main>` content offset tracks the sidebar width automatically —
834
+ `lg:ml-60` when expanded, `lg:ml-16` when collapsed (via `useSidebarCollapse()`) —
835
+ and adds `pt-14 lg:pt-0` for the mobile top bar while the sidebar is shown.
836
+
837
+ **Props:**
838
+
839
+ | Prop | Type | Default | Description |
840
+ | ---- | ---- | ------- | ----------- |
841
+ | `showSidebar` | `Boolean` | `true` | Render the `sidebar` slot. Set false for full-bleed pages. |
842
+ | `skipLinkLabel` | `String` | `'Skip to main content'` | Label for the accessibility skip link. |
843
+
844
+ **Slots:** `sidebar`, `default` (page content).
845
+
846
+ ```vue
847
+ <BaseAppLayout :show-sidebar="route.name !== 'presentation'">
848
+ <template #sidebar><AppSidebar /></template>
849
+ <RouterView />
850
+ </BaseAppLayout>
851
+ ```
852
+
853
+ ### BaseSidebar
854
+
855
+ Responsive sidebar with desktop fixed-positioning and mobile off-canvas
856
+ behavior, focus management, optional theme toggle, a desktop collapse
857
+ toggle (icon-only rail), an optional notifications bell, and configurable
858
+ nav sections.
859
+
860
+ **Props:**
861
+
862
+ | Prop | Type | Default | Description |
863
+ | ---- | ---- | ------- | ----------- |
864
+ | `sections` | `NavSection[]` | **required** | Grouped nav items. |
865
+ | `homeRouteName` | `String` | `'home'` | Route name for the logo / "go home" click. |
866
+ | `appName` | `String` | `''` | Optional app name in the footer. |
867
+ | `version` | `String` | `''` | Optional version string in the footer. |
868
+ | `showThemeToggle` | `Boolean` | `true` | Toggle the dark/light switch in the footer. |
869
+ | `collapsible` | `Boolean` | `true` | Show the desktop collapse toggle that shrinks the sidebar to an icon-only rail. |
870
+ | `showNotifications` | `Boolean` | `false` | Show the notifications bell (with unread badge) that toggles `BaseNotificationPanel`. |
871
+
872
+ > **Collapse state** is shared via `useSidebarCollapse()` (and persisted to
873
+ > localStorage) so `BaseAppLayout` can shrink the content offset from
874
+ > `lg:ml-60` to `lg:ml-16` in step with the rail. Collapsing only affects
875
+ > desktop (`lg+`); on mobile the sidebar stays a full off-canvas panel.
876
+
877
+ > **Notifications:** set `:show-notifications="true"` to render the bell,
878
+ > then drop a [`BaseNotificationPanel`](#basenotificationpanel) in your app.
879
+ > Both share state through `useNotifications()`, so the unread badge and the
880
+ > panel stay in sync.
881
+
882
+ **Slots:**
883
+
884
+ | Slot | Slot props | Description |
885
+ | -------- | ---------- | ----------- |
886
+ | `logo` | `{ size }` | Brand logo. Receives a `size` hint (28px in mobile bar, 52px in expanded sidebar, 36px in the collapsed rail). |
887
+ | `status` | — | Footer status row (e.g. health indicator, sync state). |
888
+ | `footer` | — | Replaces the default `appName` + `v{version}` line. |
889
+
890
+ **Types:**
891
+
892
+ ```ts
893
+ import type { NavItem, NavSection } from 'mgv-backoffice'
894
+
895
+ interface NavItem {
896
+ name: string // Vue Router route name
897
+ label: string // display text
898
+ icon: Component // typically a Heroicon
899
+ }
900
+
901
+ interface NavSection {
902
+ title: string
903
+ items: NavItem[]
904
+ }
905
+ ```
906
+
907
+ ```vue
908
+ <BaseSidebar :sections="navSections" home-route-name="projects" app-name="WireMate UI" :version="appVersion">
909
+ <template #logo="{ size }"><WireMateLogo :size="size" /></template>
910
+ <template #status>
911
+ <HealthIndicator />
912
+ </template>
913
+ </BaseSidebar>
914
+ ```
915
+
916
+ ---
917
+
918
+ ### BaseSidebarUser
919
+
920
+ Signed-in user block for the `BaseSidebar` `footer` slot: initials avatar,
921
+ name and email (a link to the profile page when `to` is set) and a Logout
922
+ button that emits `logout`.
923
+
924
+ **Props:** `name` (required), `email`, `to` (profile route), `showLogout` (`true`),
925
+ `logoutLabel` (`'Logout'`), `profileLabel` (`'View profile'`, tooltip).
926
+ **Emits:** `logout`.
927
+
928
+ ```vue
929
+ <BaseSidebar :sections="nav">
930
+ <template #footer>
931
+ <BaseSidebarUser :name="me.username" :email="me.email" :to="{ name: 'Profile' }" @logout="logOut" />
932
+ </template>
933
+ </BaseSidebar>
934
+ ```
935
+
936
+ ### BaseNotificationPanel
937
+
938
+ Left-anchored notification drawer (teleported to `<body>`, slides in from
939
+ the left, backdrop + Escape to close). Open/close state and the list live
940
+ in `useNotifications()`, so the sidebar bell and the panel stay in sync.
941
+
942
+ Enable the bell on the sidebar with `:show-notifications="true"`, drop one
943
+ `<BaseNotificationPanel />` anywhere in your app, and feed it data via the
944
+ composable.
945
+
946
+ **Props:**
947
+
948
+ | Prop | Type | Default | Description |
949
+ | ----------------- | --------- | ------------------------------ | ----------- |
950
+ | `title` | `String` | `'Notifications'` | Panel heading. |
951
+ | `emptyText` | `String` | `'You have no notifications.'` | Shown when the list is empty. |
952
+ | `showMarkAllRead` | `Boolean` | `true` | Render the "Mark all as read" action when there are unread items. |
953
+
954
+ **Emits:** `select` (the clicked notification's `id`; the row is also marked read).
955
+
956
+ ```vue
957
+ <script setup lang="ts">
958
+ import { BaseNotificationPanel, useNotifications } from 'mgv-backoffice'
959
+ const { setNotifications } = useNotifications()
960
+ setNotifications([
961
+ { id: 1, title: 'New comment', message: 'Alice replied to your post', time: '2m ago', type: 'info' },
962
+ { id: 2, title: 'Build passed', time: '1h ago', read: true, type: 'success' },
963
+ ])
964
+ </script>
965
+
966
+ <template>
967
+ <BaseSidebar :sections="navSections" :show-notifications="true" />
968
+ <BaseNotificationPanel @select="(id) => goTo(id)" />
969
+ </template>
970
+ ```
971
+
972
+ ```ts
973
+ import type { NotificationItem } from 'mgv-backoffice'
974
+
975
+ interface NotificationItem {
976
+ id: string | number
977
+ title: string
978
+ message?: string
979
+ time?: string // pre-formatted by you
980
+ read?: boolean
981
+ type?: 'info' | 'success' | 'warning' | 'error' // status dot colour
982
+ }
983
+ ```
984
+
985
+ ---
986
+
987
+ ## Authentication
988
+
989
+ ### BaseGoogleSignInButton
990
+
991
+ Google-branded "Sign in with Google" button (official multi-colour "G",
992
+ dark-mode surface swap). Purely presentational — it runs no OAuth itself;
993
+ listen on `click` and start your own Google Identity / Firebase / backend
994
+ flow there.
995
+
996
+ **Props:**
997
+
998
+ | Prop | Type | Default | Description |
999
+ | ---------- | --------- | -------------------------- | ----------- |
1000
+ | `label` | `String` | `'Sign in with Google'` | Button text. |
1001
+ | `loading` | `Boolean` | `false` | Disables and shows a spinner. |
1002
+ | `disabled` | `Boolean` | `false` | Disables without the spinner. |
1003
+ | `block` | `Boolean` | `true` | Full-width layout. |
1004
+
1005
+ **Emits:** `click` (only when not disabled/loading).
1006
+
1007
+ ### BaseLoginForm
1008
+
1009
+ Presentational sign-in card: email + password (with show/hide), an optional
1010
+ "Remember me" checkbox, an error banner, the Google button + "or" divider,
1011
+ and `logo` / `forgot` / `footer` slots. Owns its input state and emits
1012
+ `submit` / `google-sign-in`; the app handles the actual request and feeds
1013
+ back `loading` / `error`.
1014
+
1015
+ **Props:**
1016
+
1017
+ | Prop | Type | Default | Description |
1018
+ | --------------- | --------- | ----------- | ----------- |
1019
+ | `title` | `String` | `'Sign in'` | Card heading. |
1020
+ | `subtitle` | `String` | `''` | Muted line under the heading. |
1021
+ | `submitLabel` | `String` | `'Sign in'` | Submit button text. |
1022
+ | `loading` | `Boolean` | `false` | Disables the form, spinner on submit. |
1023
+ | `googleLoading` | `Boolean` | `false` | Disables the form, spinner on the Google button. |
1024
+ | `error` | `String` | `''` | Error banner above the form. |
1025
+ | `showGoogle` | `Boolean` | `true` | Render the Google button + divider. |
1026
+ | `showRemember` | `Boolean` | `false` | Render the "Remember me" checkbox. |
1027
+
1028
+ **Emits:** `submit` (`LoginCredentials`), `google-sign-in`.
1029
+
1030
+ **Slots:** `logo`, `forgot` (next to the password label), `footer`.
1031
+
1032
+ ```vue
1033
+ <script setup lang="ts">
1034
+ import { BaseLoginForm } from 'mgv-backoffice'
1035
+ import type { LoginCredentials } from 'mgv-backoffice'
1036
+
1037
+ async function onSubmit(creds: LoginCredentials) { /* call your API */ }
1038
+ function onGoogle() { /* start Google OAuth */ }
1039
+ </script>
1040
+
1041
+ <template>
1042
+ <BaseLoginForm
1043
+ subtitle="Welcome back"
1044
+ :show-remember="true"
1045
+ @submit="onSubmit"
1046
+ @google-sign-in="onGoogle"
1047
+ >
1048
+ <template #logo><MyLogo /></template>
1049
+ <template #forgot><a href="/forgot" class="text-sm text-emerald-600">Forgot?</a></template>
1050
+ <template #footer>No account? <a href="/signup" class="text-emerald-600">Sign up</a></template>
1051
+ </BaseLoginForm>
1052
+ </template>
1053
+ ```
1054
+
1055
+ ---
1056
+
1057
+ ## Modals & sections
1058
+
1059
+ ### BaseModalShell
1060
+
1061
+ Shared modal chrome — `Teleport` to body, backdrop, themed card, escape key,
1062
+ aria-modal. Compose this rather than building modals from scratch.
1063
+
1064
+ **Props:**
1065
+
1066
+ | Prop | Type | Default | Description |
1067
+ | --------------- | --------- | ----------- | ----------- |
1068
+ | `title` | `String` | **required** | Modal heading. |
1069
+ | `icon` | `Component` | **required** | Heroicon rendered in a tinted circular chip left of the title (the `icon` slot can override the whole chip). |
1070
+ | `iconBgClass` | `String` | `''` | Background classes of the icon chip; empty falls back to the emerald tint (dark-mode aware). |
1071
+ | `iconClass` | `String` | `'text-emerald-600'` | Classes applied to the icon itself. |
1072
+ | `maxWidthClass` | `String` | `'max-w-md'` | Tailwind max-w utility for the card. |
1073
+ | `manualClose` | `Boolean` | `false` | If true, backdrop click and Escape do NOT auto-emit `cancel`. |
1074
+ | `scrollable` | `Boolean` | `false` | Switch to the large-content layout: a flex column capped at `90vh` with a fixed header/footer and a scrolling body. |
1075
+ | `subtitle` | `String` | `''` | Muted line under the title (scrollable layout only). |
1076
+
1077
+ **Slots:** `icon`, `default`, `footer`, and (scrollable layout) `header-actions` — content on the right of the header, e.g. a close button.
1078
+ **Events:** `cancel`, `backdrop`.
1079
+
1080
+ ### BaseConfirmModal
1081
+
1082
+ Confirmation dialog built on `BaseModalShell`. Variant chooses red (danger) or
1083
+ amber (warning) styling.
1084
+
1085
+ **Props:** `title`, `message`, `confirmText`, `cancelText`, `submittingText`,
1086
+ `variant: 'danger' | 'warning'`, `submitting`. While `submitting` is true,
1087
+ backdrop clicks and Escape stop dismissing the dialog.
1088
+
1089
+ **Slots:** `message` — rich markup replacing the plain `message` string;
1090
+ `default` — extra content below the message (warning banner, opt-in checkbox).
1091
+ **Events:** `confirm`, `cancel`.
1092
+
1093
+ ### BaseTextInputModal
1094
+
1095
+ "Ask the user for a single string and confirm" dialog. Preserves typed input
1096
+ on stray backdrop clicks; Escape always cancels.
1097
+
1098
+ **Props:** `title`, `message`, `initialValue`, `placeholder`, `inputLabel`,
1099
+ `confirmText`, `cancelText`, `submittingText`, `submitting`.
1100
+
1101
+ **Slots:** `icon` — override the default emerald document icon.
1102
+ **Events:** `confirm(value: string)`, `cancel`.
1103
+
1104
+ ### BaseEntityPickerModal
1105
+
1106
+ Searchable "pick one from a list" dialog. Pass `items` directly or an async
1107
+ `loader` that runs on mount.
1108
+
1109
+ **Props:** `title`, `message?`, `items?: EntityPickerItem[]`,
1110
+ `loader?: () => Promise<EntityPickerItem[]>`, `excludeId?`,
1111
+ `variant: 'emerald' | 'purple' | 'blue' | 'red' | 'amber'`,
1112
+ `searchPlaceholder`, `emptyMessage`, `noMatchMessage`, `confirmText`,
1113
+ `cancelText`, `submittingText`, `submitting`.
1114
+
1115
+ **Slots:** `icon` — override the default icon chip.
1116
+ **Events:** `confirm(itemId: string)`, `cancel`.
1117
+
1118
+ ```ts
1119
+ interface EntityPickerItem { id: string; label: string }
1120
+ ```
1121
+
1122
+ ### BaseCollapsibleSection
1123
+
1124
+ Section wrapper with a clickable header, optional badge, and a `default` slot
1125
+ for the body. Parent owns the `collapsed` state.
1126
+
1127
+ **Props:** `title`, `collapsed`, `badge?`, `bodyClass?`.
1128
+ **Events:** `toggle`.
1129
+
1130
+ ### BaseNotFoundPage
1131
+
1132
+ Drop-in 404 view.
1133
+
1134
+ **Props:** `code` (`'404'`), `message` (`'Page not found'`),
1135
+ `homeRouteName` (`'home'`), `homeLabel` (`'Go home'`).
1136
+
1137
+ ### BasePageHeader
1138
+
1139
+ Page-level header: icon badge + title/subtitle on the left, action
1140
+ buttons on the right. Gives top-level views a consistent header shape
1141
+ and width. The icon badge only renders when the `icon` slot is filled,
1142
+ so icon-less apps get a plain title/subtitle header.
1143
+
1144
+ **Props:**
1145
+
1146
+ | Prop | Type | Default | Description |
1147
+ | --------------- | -------- | ------------- | ----------- |
1148
+ | `title` | `String` | **required** | H1 text. |
1149
+ | `subtitle` | `String` | `''` | Muted line below the title. |
1150
+ | `iconColor` | `String` | `'emerald'` | Badge + icon colour: `'emerald' \| 'sky' \| 'red' \| 'amber'`. |
1151
+ | `maxWidthClass` | `String` | `'max-w-4xl'` | Tailwind max-w utility constraining header width. Pass `''` to skip the width wrapper entirely — the header then spans its container. |
1152
+ | `align` | `String` | `'center'` | Vertical alignment of the title block vs the actions: `'center'` or `'end'` (actions sit on the title baseline). |
1153
+ | `marginClass` | `String` | `'mb-8'` | Space under the header. Pass `''` when the parent manages vertical rhythm (`space-y-*`). |
1154
+ | `accent` | `Boolean` | on when no `icon` slot | Brand-green gauge bar in front of the title. Pass `:accent="false"` to opt out. |
1155
+
1156
+ **Slots:**
1157
+
1158
+ | Slot | Slot props | Description |
1159
+ | ---------- | ----------------- | ----------- |
1160
+ | `icon` | `{ iconClass }` | Page Heroicon. Bind `:class="iconClass"` for the theme-aware colour. Badge square renders only when this slot is filled. |
1161
+ | `subtitle` | — | Rich subtitle content (links, `<strong>`, interpolation); overrides the `subtitle` prop. |
1162
+ | `actions` | — | Buttons rendered on the right (refresh, destructive, etc.). |
1163
+
1164
+ ```vue
1165
+ <template>
1166
+ <BasePageHeader title="Request Journal" subtitle="Recent matched requests" icon-color="sky">
1167
+ <template #icon="{ iconClass }">
1168
+ <DocumentTextIcon class="w-5 h-5" :class="iconClass" />
1169
+ </template>
1170
+ <template #actions>
1171
+ <BaseButton description="Refresh" @click="reload" />
1172
+ </template>
1173
+ </BasePageHeader>
1174
+ </template>
1175
+
1176
+ <script setup lang="ts">
1177
+ import { BasePageHeader, BaseButton } from 'mgv-backoffice'
1178
+ import { DocumentTextIcon } from '@heroicons/vue/24/outline'
1179
+ </script>
1180
+ ```
1181
+
1182
+ ---
1183
+
1184
+ ### BaseToolbarButton
1185
+
1186
+ Bordered toolbar button — the "Refresh / Delete All" row that sits under
1187
+ a page header. Optional leading icon (via slot) plus a label.
1188
+
1189
+ **Props:**
1190
+
1191
+ | Prop | Type | Default | Description |
1192
+ | ---------- | --------- | ----------- | ----------- |
1193
+ | `label` | `String` | `''` | Button text. Omit for an icon-only button. |
1194
+ | `variant` | `String` | `'neutral'` | `'neutral'` (grey), `'danger'` (solid red) or `'ghost'` (slate h-9 outline — toolbar/modal-footer buttons). |
1195
+ | `disabled` | `Boolean` | `false` | Greys out and blocks the click. |
1196
+ | `title` | `String` | `undefined` | Native tooltip / a11y text. |
1197
+ | `type` | `String` | `'button'` | Native button type. |
1198
+
1199
+ **Slots:**
1200
+
1201
+ | Slot | Slot props | Description |
1202
+ | ------ | --------------- | ----------- |
1203
+ | `icon` | `{ iconClass }` | Leading Heroicon. Bind `:class="iconClass"` (`w-4 h-4`); add state classes as needed. |
1204
+
1205
+ **Emits:** `click` (native `MouseEvent`).
1206
+
1207
+ ```vue
1208
+ <template>
1209
+ <BaseToolbarButton label="Refresh" :disabled="isLoading" title="Refresh" @click="reload">
1210
+ <template #icon="{ iconClass }">
1211
+ <ArrowPathIcon :class="[iconClass, { 'animate-spin': isLoading }]" />
1212
+ </template>
1213
+ </BaseToolbarButton>
1214
+ <BaseToolbarButton label="Delete All" variant="danger" @click="deleteAll">
1215
+ <template #icon="{ iconClass }">
1216
+ <TrashIcon :class="iconClass" />
1217
+ </template>
1218
+ </BaseToolbarButton>
1219
+ </template>
1220
+ ```
1221
+
1222
+ ---
1223
+
1224
+ ### BaseActionButton
1225
+
1226
+ Compact ghost action button — the colour-coded "Edit / Logs / Stub /
1227
+ Delete" actions on a card footer or action row. No border/fill at rest;
1228
+ a tinted hover background keyed to the semantic colour.
1229
+
1230
+ **Props:**
1231
+
1232
+ | Prop | Type | Default | Description |
1233
+ | ----------- | --------- | ----------- | ----------- |
1234
+ | `label` | `String` | `''` | Button text. Omit for an icon-only button. |
1235
+ | `color` | `String` | `'emerald'` | `'emerald' \| 'sky' \| 'indigo' \| 'teal' \| 'purple' \| 'red' \| 'amber' \| 'amberStrong'`. |
1236
+ | `disabled` | `Boolean` | `false` | Dims via opacity and suppresses the hover tint. |
1237
+ | `fullWidth` | `Boolean` | `false` | Stretch to fill its flex row (`flex-1`). |
1238
+ | `title` | `String` | `undefined` | Native tooltip. |
1239
+ | `ariaLabel` | `String` | `undefined` | Accessible label. |
1240
+ | `type` | `String` | `'button'` | Native button type. |
1241
+
1242
+ **Slots:**
1243
+
1244
+ | Slot | Slot props | Description |
1245
+ | ------ | --------------- | ----------- |
1246
+ | `icon` | `{ iconClass }` | Leading Heroicon. Bind `:class="iconClass"` (`w-4 h-4`). |
1247
+
1248
+ **Emits:** `click` (native `MouseEvent`).
1249
+
1250
+ ```vue
1251
+ <template>
1252
+ <BaseActionButton label="Edit" color="emerald" full-width title="Edit this mock" @click="edit">
1253
+ <template #icon="{ iconClass }">
1254
+ <PencilSquareIcon :class="iconClass" />
1255
+ </template>
1256
+ </BaseActionButton>
1257
+ <BaseActionButton label="Delete" color="red" @click="remove">
1258
+ <template #icon="{ iconClass }">
1259
+ <TrashIcon :class="iconClass" />
1260
+ </template>
1261
+ </BaseActionButton>
1262
+ </template>
1263
+ ```
1264
+
1265
+ ---
1266
+
1267
+ ### BaseCopyButton
1268
+
1269
+ Copy-to-clipboard icon button with transient "copied" feedback — clicks
1270
+ write `text` to the clipboard, swap the clipboard icon for a checkmark
1271
+ for `resetMs`, then revert. Uses the async Clipboard API with a
1272
+ hidden-textarea `execCommand` fallback for insecure origins. Emits
1273
+ `copied` / `error` so the parent can fire its own toast.
1274
+
1275
+ **Props:**
1276
+
1277
+ | Prop | Type | Default | Description |
1278
+ | ----------- | --------- | --------- | ----------- |
1279
+ | `text` | `String` | **required** | Value written to the clipboard. |
1280
+ | `label` | `String` | `''` | Used in the tooltip / aria-label (`Copy {label}`). |
1281
+ | `variant` | `String` | `'ghost'` | `'ghost'` (borderless `p-1` icon) or `'bordered'` (`w-9 h-9` boxed, turns emerald while copied). |
1282
+ | `resetMs` | `Number` | `1500` | How long the checkmark stays before reverting. |
1283
+ | `iconClass` | `String` | `'w-4 h-4'` | Icon size class. |
1284
+
1285
+ **Emits:** `copied`, `error(err)`.
1286
+
1287
+ ```vue
1288
+ <template>
1289
+ <!-- Inline ID copy, parent fires the toast -->
1290
+ <BaseCopyButton
1291
+ :text="stub.id"
1292
+ label="Stub ID"
1293
+ @copied="showToastMessage('Stub ID copied to clipboard', BaseToastEnum.SUCCESS)"
1294
+ @error="showToastMessage('Failed to copy stub ID', BaseToastEnum.ERROR)"
1295
+ />
1296
+ <!-- Boxed copy next to a read-only input -->
1297
+ <BaseCopyButton :text="mock.id" label="Mock ID" variant="bordered" :reset-ms="2000" />
1298
+ </template>
1299
+
1300
+ <script setup lang="ts">
1301
+ import { BaseCopyButton, BaseToastEnum } from 'mgv-backoffice'
1302
+ </script>
1303
+ ```
1304
+
1305
+ ---
1306
+
1307
+ ### BaseChipButton
1308
+
1309
+ Small tinted emerald "chip" action button — the compact "+ Add" pill used
1310
+ above repeatable form rows. Label comes from the default slot.
1311
+
1312
+ **Props:**
1313
+
1314
+ | Prop | Type | Default | Description |
1315
+ | ---------- | --------- | ------- | ----------- |
1316
+ | `size` | `String` | `'sm'` | `'sm'` = `px-2.5 py-1`; `'xs'` = `px-2 py-0.5` for tight corners. |
1317
+ | `disabled` | `Boolean` | `false` | Dims the chip and blocks clicks. |
1318
+
1319
+ **Emits:** `click`.
1320
+
1321
+ ```vue
1322
+ <BaseChipButton @click="addRow(rows)">+ Add</BaseChipButton>
1323
+ <BaseChipButton size="xs" @click="addNamespace">+ Add</BaseChipButton>
1324
+ ```
1325
+
1326
+ ---
1327
+
1328
+ ### BaseRemoveButton
1329
+
1330
+ The red "×" remove-row affordance used beside repeatable form rows. Name it
1331
+ for screen readers via `aria-label`; `title`, `disabled` and extra classes
1332
+ (`pt-1`, `self-start`, …) fall through as attrs.
1333
+
1334
+ **Emits:** `click`.
1335
+
1336
+ ```vue
1337
+ <BaseRemoveButton :aria-label="`Remove header ${i + 1}`" @click="rows.splice(i, 1)" />
1338
+ ```
1339
+
1340
+ ---
1341
+
1342
+ ### BaseStatusPill
1343
+
1344
+ Connection/health status pill: a colored dot (pulsing while `ok`) next to a
1345
+ short label on a tinted background.
1346
+
1347
+ **Props:**
1348
+
1349
+ | Prop | Type | Default | Description |
1350
+ | -------- | -------- | ------- | ----------- |
1351
+ | `status` | `String` | **required** | `'ok'` (emerald, pulsing), `'error'` (red), `'unknown'` (gray). |
1352
+ | `label` | `String` | **required** | Short text next to the dot, e.g. `WireMock Connected`. |
1353
+
1354
+ ```vue
1355
+ <BaseStatusPill :status="healthy ? 'ok' : 'error'" :label="healthy ? 'Connected' : 'Disconnected'" />
1356
+ ```
1357
+
1358
+ ---
1359
+
1360
+ ### BaseFileDropzone
1361
+
1362
+ Dashed "click to select a file" upload zone (extracted from WireMate's
1363
+ Postman-import modal). Renders a document-arrow-up icon (overridable via the
1364
+ `#icon` slot), a label line, and an optional dimmed hint line. Clicking opens
1365
+ the native file picker; dragging files onto the zone also works (the border
1366
+ highlights emerald while dragging). The hidden input resets after every
1367
+ selection, so picking the same file twice still emits.
1368
+
1369
+ **Props:**
1370
+
1371
+ | Prop | Type | Default | Description |
1372
+ | ---------- | --------- | ------- | ----------- |
1373
+ | `label` | `String` | **required** | Main line, e.g. `Click to select a Postman collection (.json)`. |
1374
+ | `hint` | `String` | `''` | Dimmed helper line below the label. |
1375
+ | `accept` | `String` | `''` | Forwarded to the input's `accept`. Dropped files are **not** filtered by it. |
1376
+ | `multiple` | `Boolean` | `false` | Allow multi-select; when `false`, a multi-file drop emits only the first file. |
1377
+ | `disabled` | `Boolean` | `false` | Dims the zone and ignores clicks/drops. |
1378
+
1379
+ **Emits:** `files` (`File[]`, never empty).
1380
+
1381
+ **Slots:** `icon` — replaces the default upload icon.
1382
+
1383
+ ```vue
1384
+ <BaseFileDropzone
1385
+ accept="application/json,.json"
1386
+ label="Click to select a Postman collection (.json)"
1387
+ hint="Exported from Postman → Export → Collection v2.1"
1388
+ @files="onFiles"
1389
+ />
1390
+ ```
1391
+
1392
+ ### BaseCodeBlock
1393
+
1394
+ Themed monospace `<pre>` for JSON payloads, request dumps and code snippets
1395
+ (extracted from WireMate's stub/request detail views). Preserves whitespace
1396
+ verbatim, scrolls both axes, and adapts to the theme. Extra classes (margins
1397
+ etc.) fall through via the normal class merge.
1398
+
1399
+ **Props:**
1400
+
1401
+ | Prop | Type | Default | Description |
1402
+ | ---------------- | -------- | -------- | ----------- |
1403
+ | `code` | `String` | **required** | The raw text to render. |
1404
+ | `variant` | `String` | `'soft'` | `'soft'` = tinted fill, no border (in-card look); `'bordered'` = bordered card fill (standalone look). |
1405
+ | `size` | `String` | `'sm'` | `'sm'` = `text-sm px-5 py-4`; `'xs'` = dense `text-xs p-3`. |
1406
+ | `maxHeightClass` | `String` | `''` | Optional Tailwind max-height utility, e.g. `max-h-96`. |
1407
+
1408
+ ```vue
1409
+ <BaseCodeBlock :code="formatJson(response.body)" size="xs" max-height-class="max-h-64" />
1410
+ ```
1411
+
1412
+ ---
1413
+
1414
+ ## Forms & tables
1415
+
1416
+ These components use `dark:` Tailwind variants, so the consuming app must map
1417
+ the `dark` variant to the `.dark` class that `useTheme()` toggles (see
1418
+ [Tailwind setup for consumers](#tailwind-setup-for-consumers)).
1419
+
1420
+ ### BaseInput
1421
+
1422
+ Themed text/number input carrying the shared field skin (slate border,
1423
+ `bg-slate-50` / dark `bg-slate-900` surface). Everything else — `placeholder`,
1424
+ `id`, `disabled`, `step`/`min`, extra classes like `font-mono` or
1425
+ `placeholder:*` — falls through via attrs and Vue class merging.
1426
+
1427
+ **Props:**
1428
+
1429
+ | Prop | Type | Default | Description |
1430
+ | ------------ | ------------------ | -------- | ----------- |
1431
+ | `modelValue` | `String \| Number \| null` | `''` | `v-model` value. |
1432
+ | `type` | `String` | `'text'` | Native input type. |
1433
+ | `size` | `String` | `'md'` | `'md'` = `px-3 py-2`, `'sm'` = `px-2 py-1.5`. |
1434
+ | `block` | `Boolean` | `true` | Full-width (`w-full`); set `false` for inline fields. |
1435
+
1436
+ **Emits:** `update:modelValue(value: string)` — always the raw string; parse
1437
+ numbers in the owner.
1438
+
1439
+ ```vue
1440
+ <BaseInput v-model="query" placeholder="e.g. AMD or BTC" class="font-mono" />
1441
+ ```
1442
+
1443
+ ### BaseSelect
1444
+
1445
+ Themed `<select>` sharing BaseInput's field skin. Options come from the
1446
+ default slot so callers keep full control of `<option>` rendering.
1447
+
1448
+ **Props:**
1449
+
1450
+ | Prop | Type | Default | Description |
1451
+ | ------------ | ------------------ | ------- | ----------- |
1452
+ | `modelValue` | `String \| null` | `undefined` | `v-model` value. When left undefined the browser keeps its own default selection. |
1453
+ | `size` | `String` | `'sm'` | `'sm'` = `px-2 py-1.5`, `'md'` = `px-3 py-2`. |
1454
+ | `block` | `Boolean` | `true` | Full-width; set `false` for inline selects. |
1455
+
1456
+ **Slots:** `default` — the `<option>` elements.
1457
+ **Emits:** `update:modelValue(value: string)`.
1458
+
1459
+ ```vue
1460
+ <BaseSelect v-model="strategyType">
1461
+ <option v-for="e in catalog" :key="e.type" :value="e.type" :title="e.description">
1462
+ {{ e.label }}
1463
+ </option>
1464
+ </BaseSelect>
1465
+ ```
1466
+
1467
+ ### BaseDropdown
1468
+
1469
+ Button-style single-select dropdown ("Select Social User ⌄"). Unlike
1470
+ `BaseSelect` (a native `<select>`), this renders a trigger button plus a
1471
+ floating menu, so the closed control shows a placeholder and a chevron that
1472
+ rotates while open — matching the app's filter dropdowns. Selecting a row
1473
+ emits its `value` and closes the menu; Escape and an outside click also close
1474
+ it.
1475
+
1476
+ **Props:**
1477
+
1478
+ | Prop | Type | Default | Description |
1479
+ | ------------- | -------------------------- | ------------ | ----------- |
1480
+ | `options` | `DropdownOption[]` | **required** | `{ value, label, title?, disabled? }` per row. |
1481
+ | `modelValue` | `String \| Number \| null` | `null` | Selected option's `value` (`v-model`). |
1482
+ | `placeholder` | `String` | `'Select'` | Trigger text shown when nothing is selected. |
1483
+ | `size` | `String` | `'md'` | `'md'` = `px-4 py-2.5` (app filter height), `'sm'` = `px-3 py-2`. Ignored when `triggerClass` is set. |
1484
+ | `block` | `Boolean` | `true` | Full-width; set `false` for an inline, content-width dropdown. |
1485
+ | `disabled` | `Boolean` | `false` | Disables the trigger. |
1486
+ | `ariaLabel` | `String` | `''` | Accessible name for the trigger/listbox when there is no visible label. |
1487
+ | `triggerClass`| `String` | `''` | Replaces the trigger's default slate skin entirely (including the `size` padding). |
1488
+ | `chevronClass`| `String` | `'w-5 h-5 text-slate-500 dark:text-slate-400'` | Classes for the chevron icon. |
1489
+ | `tone` | `'neutral' \| 'success' \| 'danger'` | `'neutral'` | Tints the default skin (slate / emerald / red) while keeping the `size` padding — e.g. an Active/Paused status dropdown. Ignored when `triggerClass` is set. |
1490
+
1491
+ **Emits:** `update:modelValue(value)`.
1492
+
1493
+ ```vue
1494
+ <BaseDropdown
1495
+ v-model="socialUserId"
1496
+ :options="socialUsers.map((u) => ({ value: u.id, label: u.name }))"
1497
+ placeholder="Select Social User"
1498
+ aria-label="Social user"
1499
+ />
1500
+ ```
1501
+
1502
+ ### BaseDateTimePicker
1503
+
1504
+ Date / date-time / time picker built on
1505
+ [@vuepic/vue-datepicker](https://vue3datepicker.com). The package is a regular
1506
+ dependency of this lib (installed automatically) and its CSS ships inside
1507
+ `ui-lib.css`, so consumers install and import nothing extra. Dark mode follows
1508
+ `useTheme()`; the skin matches BaseInput (slate surfaces, emerald accent).
1509
+
1510
+ **Props:**
1511
+
1512
+ | Prop | Type | Default | Description |
1513
+ | ------------ | ------------------ | ------------ | ----------- |
1514
+ | `modelValue` | `ModelValue` | `null` | `v-model` value — `Date`, `Date[]` for ranges, a time object in `'time'` mode, or a string/number with `model-type`. |
1515
+ | `mode` | `'date' \| 'datetime' \| 'time'` | `'datetime'` | Calendar only, calendar + time, or time only. |
1516
+ | `format` | `String` | per mode | date-fns input pattern. Defaults: `dd/MM/yyyy`, `dd/MM/yyyy HH:mm`, `HH:mm` (`hh:mm a` when `is24` is false). |
1517
+ | `is24` | `Boolean` | `true` | 24-hour clock. |
1518
+ | `autoApply` | `Boolean` | `false` | Select on click, without the Cancel/Select row. |
1519
+ | `teleport` | `Boolean \| String \| HTMLElement` | `true` | Menu mount target; `true` = body, so modals don't clip it. |
1520
+ | `timeConfig` | `Partial<TimeConfig>` | — | Extra time options (seconds, increments…), merged over the defaults. |
1521
+
1522
+ Every other VueDatePicker prop (`range`, `min-date`, `max-date`,
1523
+ `disabled-dates`, `placeholder`, `model-type`, …), event and slot is passed
1524
+ straight through.
1525
+
1526
+ **Emits:** `update:modelValue(value)`.
1527
+
1528
+ ```vue
1529
+ <BaseDateTimePicker v-model="startsAt" placeholder="Start" />
1530
+ <BaseDateTimePicker v-model="day" mode="date" :min-date="new Date()" auto-apply />
1531
+ <BaseDateTimePicker v-model="period" mode="date" range />
1532
+ ```
1533
+
1534
+ ### BaseSegmentedControl
1535
+
1536
+ Segmented button group ("All | Stock | Crypto"). One button per option; the
1537
+ selected one gets the filled treatment and `aria-pressed="true"`.
1538
+
1539
+ **Props:**
1540
+
1541
+ | Prop | Type | Default | Description |
1542
+ | ------------- | ------------------- | -------- | ----------- |
1543
+ | `options` | `SegmentedOption[]` | **required** | `{ value, label, title? }` per button. |
1544
+ | `modelValue` | `String \| Number` | **required** | Selected option's `value` (`v-model`). |
1545
+ | `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). |
1546
+ | `ariaLabel` | `String` | `''` | When set, the wrapper renders `role="group"` + `aria-label`. |
1547
+ | `optionClass` | `Function` | — | `(option, active) => string` override for per-button fill classes (e.g. severity colours); layout stays owned by the variant. |
1548
+
1549
+ **Emits:** `update:modelValue(value)`.
1550
+
1551
+ ```vue
1552
+ <BaseSegmentedControl v-model="assetFilter" :options="ASSET_FILTERS" />
1553
+ <BaseSegmentedControl v-model="exchange" :options="EXCHANGES" variant="wide" aria-label="Exchange" />
1554
+ ```
1555
+
1556
+ ### BaseTable
1557
+
1558
+ Styling shell for data tables — **not** a data grid. Owns the table skin
1559
+ (slate header band, `px-4 py-3` header cells, row dividers + hover, empty-state
1560
+ row); body rows are the caller's own `<tr>` markup via the default slot. Pass
1561
+ `card` for the rounded, bordered, horizontally scrolling card chrome, or wrap
1562
+ it yourself (e.g. a `BaseRow` with `overflow-x-auto`).
1563
+
1564
+ **Props:**
1565
+
1566
+ | Prop | Type | Default | Description |
1567
+ | ----------- | --------------- | ------------ | ----------- |
1568
+ | `columns` | `TableColumn[]` | **required** | `{ label, align? }`; `align: 'right'` right-aligns the header cell. |
1569
+ | `empty` | `Boolean` | `false` | True renders the empty-state row spanning every column. |
1570
+ | `emptyText` | `String` | `'No rows.'` | Fallback empty-state text. |
1571
+ | `card` | `Boolean` | `false` | Wrap in the rounded, bordered, scrolling card. |
1572
+
1573
+ **Slots:** `default` — the `<tr>` rows; `empty` — custom empty-state content.
1574
+
1575
+ ```vue
1576
+ <BaseTable :columns="COLUMNS" :empty="rows.length === 0">
1577
+ <template #empty>No trades match your filters.</template>
1578
+ <tr v-for="row in rows" :key="row.id" class="border-t border-slate-200 dark:border-slate-700">
1579
+ …
1580
+ </tr>
1581
+ </BaseTable>
1582
+ ```
1583
+
1584
+ ### BaseSpecFields
1585
+
1586
+ Spec-driven form fields: renders a select / checkbox / number input per
1587
+ `SpecField`, with labels and help text, in a responsive two-column grid. Feed
1588
+ it a backend-described catalogue and every form editing those values stays in
1589
+ lockstep. Never mutates `params` — every edit is emitted as `(key, value)`
1590
+ and the owner writes it back into its own state.
1591
+
1592
+ **Props:**
1593
+
1594
+ | Prop | Type | Default | Description |
1595
+ | -------- | -------------------------------- | ------------ | ----------- |
1596
+ | `specs` | `SpecField[]` | **required** | `{ key, label, type: 'decimal' \| 'integer' \| 'boolean' \| 'select', default?, options?, step?, min?, help? }` (optional members are nullable). |
1597
+ | `params` | `Record<string, SpecFieldValue>` | **required** | Current values keyed by `spec.key`. |
1598
+
1599
+ **Slots:** `after` (`{ spec }`) — extra content under each field (e.g. a live
1600
+ preview attached to one key).
1601
+ **Emits:** `update(key: string, value: SpecFieldValue)` — numbers are parsed
1602
+ (`parseFloat`); unparseable input passes through raw so the owner's
1603
+ validation can catch it.
1604
+
1605
+ ```vue
1606
+ <BaseSpecFields :specs="entry.params" :params="form.params"
1607
+ @update="(key, value) => (form.params[key] = value)" />
1608
+ ```
1609
+
1610
+ ### BaseStatBreakdown
1611
+
1612
+ Compact per-item breakdown meant to sit under a summary/stat card (pairs with
1613
+ [`EarningsCard`](#earningscard)). Each item renders on its own line — label
1614
+ left, value right in monospace. A `null`/`undefined` value (a source that is
1615
+ unconfigured, unreachable, or has no matching rows) shows an em-dash rather
1616
+ than a misleading 0. With `signed`, values gain an explicit "+" and are
1617
+ coloured green/red by sign (same `"-$3.00"` / `"+$5.00"` shape as `fmtUsd`).
1618
+
1619
+ **Props:**
1620
+
1621
+ | Prop | Type | Default | Description |
1622
+ | ---------- | --------------------- | ------------ | ----------- |
1623
+ | `items` | `StatBreakdownItem[]` | **required** | `{ label, value }` per row. |
1624
+ | `currency` | `String` | `''` | Currency symbol placed after the sign, e.g. `'$'`. |
1625
+ | `decimals` | `Number` | `2` | Fraction digits shown for each value. |
1626
+ | `signed` | `Boolean` | `false` | Show an explicit "+" on non-negative values and colour rows green/red by sign. |
1627
+
1628
+ ```ts
1629
+ import type { StatBreakdownItem } from 'mgv-backoffice'
1630
+
1631
+ interface StatBreakdownItem {
1632
+ label: string // row label rendered on the left
1633
+ value: number | null | undefined // null/undefined renders as an em-dash
1634
+ }
1635
+ ```
1636
+
1637
+ ```vue
1638
+ <EarningsCard title="TOTAL P&L" :amount="totalPnl" signed />
1639
+ <BaseStatBreakdown
1640
+ :items="[
1641
+ { label: 'Alpaca', value: 42.5 },
1642
+ { label: 'Binance', value: -3.1 },
1643
+ { label: 'Kraken', value: null },
1644
+ ]"
1645
+ currency="$"
1646
+ signed
1647
+ />
1648
+ ```
1649
+
1650
+ ### BaseFilterChip
1651
+
1652
+ Colour-coded toggleable filter chip — one-click event/category filters above
1653
+ a data feed. Idle renders a tinted border/background in the semantic colour;
1654
+ active renders a solid fill with white text (`aria-pressed` reflects the
1655
+ state). Layout classes (`h-9 flex-1`, …) pass through the class attribute;
1656
+ click handlers bind natively on the component.
1657
+
1658
+ **Props:**
1659
+
1660
+ | Prop | Type | Default | Description |
1661
+ | ---------- | --------- | --------- | ----------- |
1662
+ | `label` | `String` | `''` | Chip text; the default slot overrides it. |
1663
+ | `color` | `'emerald' \| 'sky' \| 'amber' \| 'red' \| 'slate'` | `'slate'` | Semantic colour of the idle tint and active fill. |
1664
+ | `active` | `Boolean` | `false` | Whether the chip's filter is applied (solid fill). |
1665
+ | `disabled` | `Boolean` | `false` | Greys out + blocks the click. |
1666
+ | `title` | `String` | — | Native tooltip. |
1667
+
1668
+ ```vue
1669
+ <BaseFilterChip
1670
+ v-for="f in filters"
1671
+ :key="f.key"
1672
+ class="h-9 flex-1"
1673
+ :color="f.color"
1674
+ :active="isActive(f)"
1675
+ :title="f.title"
1676
+ @click="toggle(f)"
1677
+ >{{ f.label }}</BaseFilterChip>
1678
+ ```
1679
+
1680
+ ### BaseCredentialsForm
1681
+
1682
+ One service's API-credentials card: key id + secret + base/data URLs, with
1683
+ the has-secret handling (placeholder dots, blank-keeps-stored-secret), the
1684
+ save-validation ladder and a saving spinner. Load/save results are EMITTED —
1685
+ the parent owns toasts / error banners. Exposes `load()` so a parent Reload
1686
+ button can re-pull several cards in parallel.
1687
+
1688
+ **Props:** `title` + `idPrefix` + `fetchFn: () => Promise<CredentialsView>` +
1689
+ `updateFn: (body: CredentialsUpdate) => Promise<CredentialsView>` +
1690
+ `defaults: { baseUrl, dataUrl }` (required); `subtitle`, `keyLabel`,
1691
+ `keyPlaceholder`, `secretLabel`, `secretPlaceholder`, `secretSetHint`,
1692
+ `permissionsHint`, `requiredKeyMessage`, `requiredSecretMessage`,
1693
+ `savedMessage`, `saveLabel` (optional copy overrides).
1694
+
1695
+ **Slots:** `no-secret-hint` — rich help while no secret is stored;
1696
+ `base-url-extra` (`{ form }`) — extras under the Base URL field (e.g.
1697
+ live/paper shortcut buttons that write into the form); `footer` — extra
1698
+ content at the card's bottom.
1699
+
1700
+ **Emits:** `saved(message)`, `error(message)`, `load-error(message)`.
1701
+
1702
+ ```vue
1703
+ <BaseCredentialsForm
1704
+ ref="card"
1705
+ title="Alpaca API"
1706
+ id-prefix="alpaca"
1707
+ :fetch-fn="fetchAlpaca"
1708
+ :update-fn="updateAlpaca"
1709
+ :defaults="{ baseUrl: LIVE_BASE, dataUrl: DATA_URL }"
1710
+ @saved="onSaved"
1711
+ @error="onError"
1712
+ @load-error="onLoadError"
1713
+ />
1714
+ ```
1715
+
1716
+ ---
1717
+
1718
+ ### BasePillPickerModal
1719
+
1720
+ "Pick one of many" modal: every item rendered as a clickable pill, narrowed
1721
+ by a free-text filter and an optional segmented group toggle. Clicking a pill
1722
+ emits `pick` with the item; backdrop / Escape / the footer Close emit `close`.
1723
+ Narrowing state lives inside, so a `v-if`-mounted instance always opens fresh.
1724
+
1725
+ **Props:** `title` + `items: PillPickerItem[]` (required);
1726
+ `groups?: SegmentedOption<string>[]` (renders the group toggle with an
1727
+ `allLabel` option prepended, narrowing by each item's `group`); `icon?`
1728
+ (defaults to the magnifying glass), `subtitle?`, `searchPlaceholder`,
1729
+ `emptyMessage`, `noMatchMessage`, `mono` (mono font for the filter input and
1730
+ pills — symbols, codes, ids), `maxWidthClass` (default `max-w-2xl`),
1731
+ `closeText`, `groupAriaLabel`, `allLabel`.
1732
+
1733
+ **Emits:** `pick(item: PillPickerItem)`, `close`.
1734
+
1735
+ ```ts
1736
+ interface PillPickerItem {
1737
+ id: string // unique key; identifies the pick
1738
+ label: string // pill text; what the filter matches
1739
+ group?: string // segmented-toggle bucket
1740
+ title?: string // pill tooltip
1741
+ }
1742
+ ```
1743
+
1744
+ ```vue
1745
+ <BasePillPickerModal
1746
+ v-if="open"
1747
+ title="Symbols"
1748
+ :items="symbols.map(s => ({ id: s.id, label: s.symbol, group: s.assetClass }))"
1749
+ :groups="[{ value: 'STOCK', label: 'STOCK' }, { value: 'CRYPTO', label: 'CRYPTO' }]"
1750
+ mono
1751
+ @pick="apply"
1752
+ @close="open = false"
1753
+ />
1754
+ ```
1755
+
1756
+ ---
1757
+
1758
+ ### BaseBarDistribution
1759
+
1760
+ Compact value-distribution chart: one thin rounded bar per distinct
1761
+ value, count labelled on top and the value underneath — scrolls
1762
+ sideways when there are many bars. Pure Tailwind, no chart library.
1763
+ Extracted from TradeAutomation's variant-stats modal.
1764
+
1765
+ **Props:**
1766
+
1767
+ | Prop | Type | Default | Description |
1768
+ | -------------- | -------- | ---------------------- | ----------- |
1769
+ | `bars` | `Array` | **required** | `DistributionBar[]` — `{ label, count }` per bar, in display order (sort ascending for numeric values). |
1770
+ | `ariaLabel` | `String` | `'Value distribution'` | Accessible description of the chart. |
1771
+ | `countNoun` | `String` | `'item'` | Noun for each bar's tooltip count, e.g. `'variant'` → "3 variants". |
1772
+ | `titlePrefix` | `String` | `''` | Tooltip prefix before the value, e.g. the field name. |
1773
+ | `maxBarHeight` | `Number` | `56` | Height of the tallest bar, in px. |
1774
+ | `barClass` | `String` | emerald fill | Tailwind classes for the bar fill. |
1775
+
1776
+ ```vue
1777
+ <BaseBarDistribution
1778
+ :bars="[{ label: '0.5', count: 1 }, { label: '1', count: 4 }]"
1779
+ aria-label="Distribution of Take profit across variants"
1780
+ title-prefix="Take profit"
1781
+ count-noun="variant"
1782
+ />
1783
+ ```
1784
+
1785
+ ---
1786
+
1787
+ ### BaseGroupedChecklist
1788
+
1789
+ Grouped multi-select checklist (since 1.46.0): a master "Select all (N)"
1790
+ checkbox, a per-group select-all, and one checkbox per item. Built for
1791
+ "pick which records this action applies to" confirmation modals. Empty groups
1792
+ are not rendered; items are plain strings, unique across groups.
1793
+
1794
+ | Prop | Type | Default | Description |
1795
+ | ---------------- | -------------------------- | ------------------ | ----------- |
1796
+ | `groups` | `ChecklistGroup[]` | **required** | `{ key, label, items: string[] }` per group, in display order. |
1797
+ | `modelValue` | `string[]` | **required** | Selected items (`v-model`); emitted in group order. |
1798
+ | `allLabel` | `string` | `'Select all'` | Master checkbox text; group checkboxes are labelled `"<allLabel> <group label>"`. |
1799
+ | `allAriaLabel` | `string` | `'Select all items'` | Accessible name of the master checkbox. |
1800
+ | `itemAriaLabel` | `(item: string) => string` | the item itself | Accessible name per item checkbox. |
1801
+ | `mono` | `boolean` | `false` | Monospace item labels (tickers, ids). |
1802
+ | `maxHeightClass` | `string` | `'max-h-72'` | Scroll cap of the group list. |
1803
+
1804
+ ```vue
1805
+ <BaseGroupedChecklist
1806
+ v-model="selected"
1807
+ :groups="[{ key: 'STOCK', label: 'Stocks', items: ['AMD', 'NVDA'] }]"
1808
+ all-aria-label="Select all symbols"
1809
+ :item-aria-label="(s) => `Apply to ${s}`"
1810
+ mono
1811
+ />
1812
+ ```
1813
+
1814
+ ---
1815
+
1816
+ ### BaseIdChip
1817
+
1818
+ Compact id cell (since 1.47.0): a small uppercase label, the id truncated in a
1819
+ `<code>` pill with the full value in its tooltip, a copy button, and an optional
1820
+ magnifier button. Built for dense rows that carry several ids side by side.
1821
+
1822
+ | Prop | Type | Default | Description |
1823
+ | --------------- | -------- | -------------------- | ----------- |
1824
+ | `value` | `string` | **required** | The id — shown truncated, copied in full. |
1825
+ | `label` | `string` | `''` | Uppercase prefix (`"id"`, `"ord"`). |
1826
+ | `copyLabel` | `string` | `"<label> <value>"` | Noun for the copy button's tooltip / aria-label. |
1827
+ | `actionLabel` | `string` | — | Tooltip / aria-label of the magnifier button; the button renders only when set. |
1828
+ | `maxWidthClass` | `string` | `'max-w-[90px]'` | Width cap of the truncated id. |
1829
+
1830
+ Emits `action(value)` when the magnifier is clicked.
1831
+
1832
+ ```vue
1833
+ <BaseIdChip label="ord" :value="row.buyOrderId" action-label="Search this order id" @action="search" />
1834
+ ```
1835
+
1836
+ ---
1837
+
1838
+ ### BaseSearchSelect
1839
+
1840
+ Searchable select (combobox): type to filter, pick with the mouse or
1841
+ ArrowUp/ArrowDown + Enter. `multiple` turns it into a tag picker with removable
1842
+ chips (Backspace on an empty search removes the last one). Same field skin as
1843
+ `BaseInput` / `BaseSelect`; Escape and an outside click close the menu.
1844
+
1845
+ **Props:**
1846
+
1847
+ | Prop | Type | Default | Description |
1848
+ | --------------- | ----------------------------- | --------------- | ----------- |
1849
+ | `options` | `DropdownOption[]` | **required** | `{ value, label, title?, disabled? }` per row. |
1850
+ | `modelValue` | `value \| null \| value[]` | `null` | Selected value, or values with `multiple`. |
1851
+ | `multiple` | `Boolean` | `false` | Pick several (chips). |
1852
+ | `placeholder` | `String` | `'Select'` | Shown when nothing is selected. |
1853
+ | `searchable` | `Boolean` | `true` | Typing filters the options. |
1854
+ | `clearable` | `Boolean` | `true` | Show the × clear button while something is selected. |
1855
+ | `block` | `Boolean` | `true` | Full-width. |
1856
+ | `disabled` | `Boolean` | `false` | |
1857
+ | `ariaLabel` | `String` | `''` | Accessible name without a visible label. |
1858
+ | `noResultsText` | `String` | `'No matches.'` | |
1859
+ | `emptyText` | `String` | `'No options.'` | |
1860
+
1861
+ **Emits:** `update:modelValue` — the value (single; `null` when cleared) or the
1862
+ array of values (`multiple`).
1863
+
1864
+ ```vue
1865
+ <BaseSearchSelect v-model="userId" :options="users.map(u => ({ value: u.id, label: u.name }))" placeholder="Select User" />
1866
+ <BaseSearchSelect v-model="tags" :options="TAGS" multiple />
1867
+ ```
1868
+
1869
+ ### BaseCard
1870
+
1871
+ Bordered surface card — the shared chrome for forms, filter/search panels and
1872
+ grouped content.
1873
+
1874
+ **Props:** `title`, `subtitle`, `padding: 'md' | 'sm' | 'none'` (`'md'`).
1875
+ **Slots:** `default` (body), `actions` (header, right), `footer` (bottom, right-aligned — submit/cancel).
1876
+
1877
+ ```vue
1878
+ <BaseCard title="Template">
1879
+ <form>…</form>
1880
+ <template #footer><BaseButton type="submit" description="Save" /></template>
1881
+ </BaseCard>
1882
+ ```
1883
+
1884
+ ### BaseField
1885
+
1886
+ Label + control + message wrapper for forms and filter bars.
1887
+
1888
+ **Props:** `label`, `labelFor` (the control's id), `required`, `error`
1889
+ (red, replaces the hint), `hint`, `compact` (small uppercase filter-bar label),
1890
+ `grow` (takes the free space in a flex row).
1891
+
1892
+ ```vue
1893
+ <BaseCard padding="sm">
1894
+ <div class="flex flex-wrap items-end gap-4">
1895
+ <BaseField label="Search symbol" label-for="q" compact grow>
1896
+ <BaseInput id="q" v-model="query" />
1897
+ </BaseField>
1898
+ <BaseField label="Class" compact>
1899
+ <BaseSegmentedControl v-model="cls" :options="CLASSES" />
1900
+ </BaseField>
1901
+ </div>
1902
+ </BaseCard>
1903
+ ```
1904
+
1905
+ ### BaseStatCard
1906
+
1907
+ Dashboard stat tile in the `EarningsCard` style (dashed accent border, bold
1908
+ title, big value) for any value — counts, labels, pre-formatted numbers.
1909
+
1910
+ **Props:** `title` (required), `value`, `subtitle`,
1911
+ `accent: 'emerald' | 'sky' | 'amber' | 'red' | 'slate'` (`'emerald'`),
1912
+ `to` (router location — makes the whole card a link).
1913
+ **Slots:** `icon` (fills the tinted chip; receives `iconClass`), `default` (extra content).
1914
+
1915
+ ```vue
1916
+ <BaseStatCard title="Categories" :value="71" accent="sky" :to="{ name: 'Category List' }">
1917
+ <template #icon="{ iconClass }"><SwatchIcon :class="iconClass" /></template>
1918
+ </BaseStatCard>
1919
+ ```
1920
+
1921
+ ### BaseCheckbox
1922
+
1923
+ Rounded checkbox, emerald when checked (works with or without a forms plugin).
1924
+ `v-model` is the boolean; `change` fires with the new value; attrs (`id`, `name`)
1925
+ go to the `<input>`.
1926
+
1927
+ **Props:** `modelValue`, `label`, `disabled`. **Slots:** `default` (rich label).
1928
+ **Emits:** `update:modelValue(value)`, `change(value)`.
1929
+
1930
+ ```vue
1931
+ <BaseCheckbox v-model="remember" id="remember" label="Remember me" />
1932
+ ```
1933
+
1934
+ ### BaseToggle
1935
+
1936
+ On/off switch (`role="switch"`). `v-model` is the boolean; `change` fires after
1937
+ every user toggle with the new value.
1938
+
1939
+ **Props:** `modelValue`, `label` (visible label, also the accessible name),
1940
+ `ariaLabel` (when there is no label), `color: 'emerald' | 'red' | 'sky' | 'amber'`
1941
+ (on state, `'emerald'`), `offTone: 'slate' | 'red'` (off track, `'slate'`), `disabled`.
1942
+ **Emits:** `update:modelValue(value)`, `change(value)`.
1943
+
1944
+ ```vue
1945
+ <BaseToggle v-model="task.alive" :label="task.name" color="red" @change="save(task)" />
1946
+ <BaseToggle v-model="account.alive" off-tone="red" aria-label="Alive" />
1947
+ ```
1948
+
1949
+ ### BaseAuthLayout
1950
+
1951
+ Full-page, centred shell for sign-in / sign-up / password-recovery screens:
1952
+ logo on top (defaults to `BaseLogo`), then a bordered card with the title,
1953
+ content and an optional footer.
1954
+
1955
+ **Props:** `title`, `subtitle`, `wide` (max-w-3xl card for multi-column forms; default max-w-md).
1956
+ **Slots:** `default` (form), `logo` (replaces `BaseLogo`), `footer` ("No account? Sign up").
1957
+
1958
+ ```vue
1959
+ <BaseAuthLayout title="Forgot password">
1960
+ <form>…</form>
1961
+ <template #footer>No account? <RouterLink to="/register">Sign up</RouterLink></template>
1962
+ </BaseAuthLayout>
1963
+ ```
1964
+
1965
+ ### BaseBooleanBadge
1966
+
1967
+ Yes/no pill for boolean columns: green check when true, red cross when false.
1968
+
1969
+ **Props:** `value` (`boolean | null`), `trueLabel` (`'Yes'`), `falseLabel` (`'No'`).
1970
+
1971
+ ```vue
1972
+ <BaseBooleanBadge :value="user.hasVerifiedEmail" true-label="Verified" false-label="Not verified" />
1973
+ ```
1974
+
1975
+ ### BaseDetailList
1976
+
1977
+ Read-only label/value grid for detail pages (the "view" counterpart of a form).
1978
+ Empty values show `emptyText`; `href` renders the value as an external link;
1979
+ `mono` uses the monospace face.
1980
+
1981
+ **Props:** `items: DetailItem[]` (`{ label, value?, href?, mono? }`, required),
1982
+ `columns: 1 | 2 | 3` (`2`, from `sm` up), `emptyText` (`'—'`).
1983
+
1984
+ ```vue
1985
+ <BaseCard title="Details">
1986
+ <BaseDetailList :items="[{ label: 'Status', value: post.status }, { label: 'Link', value: post.link, href: post.link }]" />
1987
+ </BaseCard>
1988
+ ```
1989
+
1990
+ ## Composables
1991
+
1992
+ ```ts
1993
+ import {
1994
+ initTheme,
1995
+ useTheme,
1996
+ useThemeClasses,
1997
+ useEscapeKey,
1998
+ useDebouncedRef,
1999
+ useToast,
2000
+ useMobileSidebar,
2001
+ useSidebarCollapse,
2002
+ useNotifications,
2003
+ useQueryParamSync,
2004
+ useFieldClasses,
2005
+ usePolling,
2006
+ useLatestRequest,
2007
+ isAbortError,
2008
+ useDirtyState,
2009
+ stableStringify,
2010
+ } from 'mgv-backoffice'
2011
+ import type {
2012
+ UseThemeOptions,
2013
+ UseSidebarCollapseOptions,
2014
+ UsePollingOptions,
2015
+ LatestRequestToken,
2016
+ } from 'mgv-backoffice'
2017
+ ```
2018
+
2019
+ | Composable | Purpose |
2020
+ | ---------- | ------- |
2021
+ | `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). |
2022
+ | `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. |
2023
+ | `useThemeClasses()` | Named Tailwind class roles for dark/light (card, border, primaryText, mutedText, dimText, input, ghostButton, emeraldText, redText, …). Since 1.34.0 returns a `reactive` object of plain strings — bind `t.card` directly, never `t.card.value` (the old ComputedRef shape leaked ref internals into `:class` bindings). |
2024
+ | `useEscapeKey(handler)` | Component-scoped Escape key listener. |
2025
+ | `useDebouncedRef(source, delay?)` | Debounced mirror of a ref. Timer cleared on scope dispose. |
2026
+ | `useToast(durationMs?)` | Per-component toast state: `{ showToast, toastMessage, toastType, showToastMessage }`. `showToastMessage` also accepts a per-call duration override. |
2027
+ | `useMobileSidebar()` | Singleton state shared between `BaseSidebar` and `BaseAppLayout` for the off-canvas open/closed flag. |
2028
+ | `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'`). |
2029
+ | `useNotifications()` | Singleton notification state shared by the sidebar bell and `BaseNotificationPanel`: `{ notifications, unreadCount, open, openPanel, closePanel, togglePanel, setNotifications, add, remove, markRead, markAllRead, clear }`. |
2030
+ | `useQueryParamSync()` | URL-query mirroring for filterable views: `{ qparam(name), qenum(name, allowed, fallback), replaceQuery(next) }`. Read filters from the query string once on setup, write changes back with `router.replace` (no-op when unchanged) so filtered views stay shareable without polluting history. |
2031
+ | `useFieldClasses()` | Shared form-field class strings for the gray/emerald form skin: `{ label, input, requiredInput(value) }`. `requiredInput` returns a red border+ring skin while the value is empty and the standard skin otherwise. |
2032
+ | `usePolling(fn, intervalMs, { immediate?, pauseWhenHidden? })` | Visibility-gated polling loop bound to the component lifecycle: starts on mount, stops on unmount, pauses while the tab is hidden and refreshes + resumes on return to visible (both default on). Pass `intervalMs: null` for refresh-only mode (run on mount + each return-to-visible, no timer). Returns `{ start, stop, active }`. A loop stopped via `stop()` stays stopped across hide/show cycles (since 1.36.0) — only a visibility-paused loop auto-resumes. Catch errors inside `fn` — the loop never swallows rejections. |
2033
+ | `useLatestRequest()` | "Latest request wins" guard for views that refetch on filter changes / polls / Refresh (since 1.45.0). `begin()` aborts the previous in-flight request and returns `{ signal, isLatest() }` — pass `signal` to `fetch`, and check `isLatest()` after every `await` before writing state (and before clearing a loading flag in `finally`). `cancel()` aborts + invalidates without starting a new request. Auto-cancels on unmount when called in setup. Pair with `isAbortError(e)` to swallow superseded requests silently. |
2034
+ | `useDirtyState(snapshot)` | Unsaved-changes tracking for edit forms (since 1.46.0): `snapshot()` returns the form state (read reactively); `dirty` is true while it differs from the last baseline, `markClean()` makes the current state the baseline (call after load / save). Structural, key-order-insensitive comparison via `stableStringify` (also exported), so a server echo that reorders keys never reads as an edit. |
2035
+
2036
+ ---
2037
+
2038
+ ## Typography
2039
+
2040
+ Since 1.33.0 the library ships the shared brand typography: the stylesheet
2041
+ loads **Fira Sans** (UI text) and **Fira Code** (numerals/data) from Google
2042
+ Fonts via `@import`, registers them as the Tailwind `--font-sans` /
2043
+ `--font-mono` theme defaults, and applies `font-family: var(--font-sans)` to
2044
+ `body`. Consumers get the fonts just by importing the lib CSS — remove any
2045
+ app-local Google Fonts `<link rel="stylesheet">` and `--font-sans`/`--font-mono`
2046
+ overrides. Keep (or add) the preconnect hints in `index.html` for a faster
2047
+ first paint:
2048
+
2049
+ ```html
2050
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
2051
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
2052
+ ```
2053
+
2054
+ ## Design system
2055
+
2056
+ `dist/style.css` ships the shared "Night Terminal" look, so importing it is all a
2057
+ consumer needs: remapped `gray`/`slate`/`emerald` colour ramps and `lg`/`xl`
2058
+ radii, Fira Sans / Fira Code, tabular numerals, an accent keyboard-focus outline
2059
+ and text selection, slim theme-aware scrollbars, and pointer cursors on buttons.
2060
+
2061
+ The tokens are a plain, unlayered `:root` block, so they apply whatever the
2062
+ import order. To re-brand, add your own plain `:root` block after this stylesheet.
2063
+
2064
+ ## Tailwind setup for consumers
2065
+
2066
+ The lib's components rely on Tailwind utility classes (including dark-mode
2067
+ variants). Consumers should add the lib's `dist` output to their Tailwind
2068
+ `content` paths so the JIT can see the class names:
2069
+
2070
+ ```js
2071
+ // tailwind.config.js
2072
+ export default {
2073
+ content: [
2074
+ './index.html',
2075
+ './src/**/*.{vue,ts}',
2076
+ './node_modules/mgv-backoffice/dist/**/*.{js,mjs,cjs,vue}',
2077
+ ],
2078
+ }
2079
+ ```
2080
+
2081
+ The legacy `tailwind.safelist.js` only covers the v1 components; the
2082
+ recommended path for v4+ is the `content` glob above.