mgv-backoffice 1.33.0 → 1.35.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
@@ -669,6 +669,24 @@ Consistency checks for dynamic key/value grids (header lists, query params,
669
669
  metadata rows). Rows with `matcherType: 'absent'` are exempt — an absent
670
670
  matcher intentionally carries no value.
671
671
 
672
+ ### Input validators
673
+
674
+ ```ts
675
+ import { isValidAbsoluteUrl, isValidJson, isValidXml, isValidBase64 } from 'mgv-backoffice'
676
+ ```
677
+
678
+ Pure, dependency-free form-input validators. The payload validators treat
679
+ empty/whitespace-only input as **valid** — required-ness is a separate rule
680
+ from well-formedness; `isValidAbsoluteUrl` validates a value that must exist,
681
+ so empty is invalid there.
682
+
683
+ | Function | Signature | Returns |
684
+ | -------------------- | ---------------------------- | ------- |
685
+ | `isValidAbsoluteUrl` | `(value: string) => boolean` | `true` for an absolute `http://` / `https://` URL (other schemes rejected). |
686
+ | `isValidJson` | `(str: string) => boolean` | `true` when empty or parseable as JSON. |
687
+ | `isValidXml` | `(str: string) => boolean` | `true` when empty or well-formed XML (DOMParser `<parsererror>` check; browser-only). |
688
+ | `isValidBase64` | `(str: string) => boolean` | `true` when empty or well-formed base64 (whitespace stripped, length/alphabet checked, then `atob` as the final authority). |
689
+
672
690
  ### HTML sanitizer
673
691
 
674
692
  ```ts
@@ -724,6 +742,8 @@ API values can be passed without pre-sanitising.
724
742
  | `fmtPrice` | `(n: number) => string` | Price with precision that scales to magnitude (more decimals for sub-cent values). |
725
743
  | `fmtPct` | `(n: number, digits = 2) => string` | Percentage with explicit sign, e.g. `"+2.50%"`. |
726
744
  | `fmtUsd` | `(v: number) => string` | Signed USD amount with leading sign, e.g. `"+$5.00"`. |
745
+ | `formatJson` | `(content: string) => string` | Pretty-prints parseable JSON with 2-space indentation; returns anything else verbatim. |
746
+ | `stringifyValue` | `(value: unknown) => string` | Display string for an unknown value: strings pass through, null/undefined → `''`, everything else JSON-serialized (`String()` fallback). |
727
747
 
728
748
  ### Spec-form helpers
729
749
 
@@ -1106,7 +1126,7 @@ a page header. Optional leading icon (via slot) plus a label.
1106
1126
  | Prop | Type | Default | Description |
1107
1127
  | ---------- | --------- | ----------- | ----------- |
1108
1128
  | `label` | `String` | `''` | Button text. Omit for an icon-only button. |
1109
- | `variant` | `String` | `'neutral'` | `'neutral'` (grey) or `'danger'` (solid red). |
1129
+ | `variant` | `String` | `'neutral'` | `'neutral'` (grey), `'danger'` (solid red) or `'ghost'` (slate h-9 outline — toolbar/modal-footer buttons). |
1110
1130
  | `disabled` | `Boolean` | `false` | Greys out and blocks the click. |
1111
1131
  | `title` | `String` | `undefined` | Native tooltip / a11y text. |
1112
1132
  | `type` | `String` | `'button'` | Native button type. |
@@ -1304,6 +1324,26 @@ selection, so picking the same file twice still emits.
1304
1324
  />
1305
1325
  ```
1306
1326
 
1327
+ ### BaseCodeBlock
1328
+
1329
+ Themed monospace `<pre>` for JSON payloads, request dumps and code snippets
1330
+ (extracted from WireMate's stub/request detail views). Preserves whitespace
1331
+ verbatim, scrolls both axes, and adapts to the theme. Extra classes (margins
1332
+ etc.) fall through via the normal class merge.
1333
+
1334
+ **Props:**
1335
+
1336
+ | Prop | Type | Default | Description |
1337
+ | ---------------- | -------- | -------- | ----------- |
1338
+ | `code` | `String` | **required** | The raw text to render. |
1339
+ | `variant` | `String` | `'soft'` | `'soft'` = tinted fill, no border (in-card look); `'bordered'` = bordered card fill (standalone look). |
1340
+ | `size` | `String` | `'sm'` | `'sm'` = `text-sm px-5 py-4`; `'xs'` = dense `text-xs p-3`. |
1341
+ | `maxHeightClass` | `String` | `''` | Optional Tailwind max-height utility, e.g. `max-h-96`. |
1342
+
1343
+ ```vue
1344
+ <BaseCodeBlock :code="formatJson(response.body)" size="xs" max-height-class="max-h-64" />
1345
+ ```
1346
+
1307
1347
  ---
1308
1348
 
1309
1349
  ## Forms & tables
@@ -1533,6 +1573,46 @@ content at the card's bottom.
1533
1573
 
1534
1574
  ---
1535
1575
 
1576
+ ### BasePillPickerModal
1577
+
1578
+ "Pick one of many" modal: every item rendered as a clickable pill, narrowed
1579
+ by a free-text filter and an optional segmented group toggle. Clicking a pill
1580
+ emits `pick` with the item; backdrop / Escape / the footer Close emit `close`.
1581
+ Narrowing state lives inside, so a `v-if`-mounted instance always opens fresh.
1582
+
1583
+ **Props:** `title` + `items: PillPickerItem[]` (required);
1584
+ `groups?: SegmentedOption<string>[]` (renders the group toggle with an
1585
+ `allLabel` option prepended, narrowing by each item's `group`); `icon?`
1586
+ (defaults to the magnifying glass), `subtitle?`, `searchPlaceholder`,
1587
+ `emptyMessage`, `noMatchMessage`, `mono` (mono font for the filter input and
1588
+ pills — symbols, codes, ids), `maxWidthClass` (default `max-w-2xl`),
1589
+ `closeText`, `groupAriaLabel`, `allLabel`.
1590
+
1591
+ **Emits:** `pick(item: PillPickerItem)`, `close`.
1592
+
1593
+ ```ts
1594
+ interface PillPickerItem {
1595
+ id: string // unique key; identifies the pick
1596
+ label: string // pill text; what the filter matches
1597
+ group?: string // segmented-toggle bucket
1598
+ title?: string // pill tooltip
1599
+ }
1600
+ ```
1601
+
1602
+ ```vue
1603
+ <BasePillPickerModal
1604
+ v-if="open"
1605
+ title="Symbols"
1606
+ :items="symbols.map(s => ({ id: s.id, label: s.symbol, group: s.assetClass }))"
1607
+ :groups="[{ value: 'STOCK', label: 'STOCK' }, { value: 'CRYPTO', label: 'CRYPTO' }]"
1608
+ mono
1609
+ @pick="apply"
1610
+ @close="open = false"
1611
+ />
1612
+ ```
1613
+
1614
+ ---
1615
+
1536
1616
  ## Composables
1537
1617
 
1538
1618
  ```ts
@@ -1548,6 +1628,7 @@ import {
1548
1628
  useNotifications,
1549
1629
  useQueryParamSync,
1550
1630
  useFieldClasses,
1631
+ usePolling,
1551
1632
  } from 'mgv-backoffice'
1552
1633
  ```
1553
1634
 
@@ -1555,7 +1636,7 @@ import {
1555
1636
  | ---------- | ------- |
1556
1637
  | `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). |
1557
1638
  | `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. |
1558
- | `useThemeClasses()` | Named Tailwind class roles for dark/light (card, border, primaryText, mutedText, dimText, input, ghostButton, emeraldText, redText, …). Returns computed refs auto-unwrapped in templates. |
1639
+ | `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). |
1559
1640
  | `useEscapeKey(handler)` | Component-scoped Escape key listener. |
1560
1641
  | `useDebouncedRef(source, delay?)` | Debounced mirror of a ref. Timer cleared on scope dispose. |
1561
1642
  | `useToast(durationMs?)` | Per-component toast state: `{ showToast, toastMessage, toastType, showToastMessage }`. |
@@ -1564,6 +1645,7 @@ import {
1564
1645
  | `useNotifications()` | Singleton notification state shared by the sidebar bell and `BaseNotificationPanel`: `{ notifications, unreadCount, open, openPanel, closePanel, togglePanel, setNotifications, add, remove, markRead, markAllRead, clear }`. |
1565
1646
  | `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. |
1566
1647
  | `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. |
1648
+ | `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 }`. Catch errors inside `fn` — the loop never swallows rejections. |
1567
1649
 
1568
1650
  ---
1569
1651
 
@@ -0,0 +1,17 @@
1
+ type __VLS_Props = {
2
+ /** The raw text to render. Whitespace is preserved verbatim. */
3
+ code: string;
4
+ /** Visual style: tinted fill ('soft') or bordered card ('bordered'). */
5
+ variant?: 'soft' | 'bordered';
6
+ /** Text size + padding: 'sm' (roomy) or 'xs' (dense). */
7
+ size?: 'sm' | 'xs';
8
+ /** Optional Tailwind max-height utility, e.g. 'max-h-96'. */
9
+ maxHeightClass?: string;
10
+ };
11
+ declare const __VLS_export: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
12
+ size: "sm" | "xs";
13
+ variant: "soft" | "bordered";
14
+ maxHeightClass: string;
15
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
16
+ declare const _default: typeof __VLS_export;
17
+ export default _default;
@@ -0,0 +1,43 @@
1
+ import { Component } from 'vue';
2
+ import { PillPickerItem } from '../types/pillPicker';
3
+ import { SegmentedOption } from '../types/segmented';
4
+ interface Props {
5
+ title: string;
6
+ items: PillPickerItem[];
7
+ /**
8
+ * Group toggle rendered next to the text filter. When set, an "All"
9
+ * option is prepended and pills are narrowed by their `group` field.
10
+ */
11
+ groups?: SegmentedOption<string>[];
12
+ /** Icon for the modal shell. Defaults to the magnifying glass. */
13
+ icon?: Component;
14
+ subtitle?: string;
15
+ searchPlaceholder?: string;
16
+ emptyMessage?: string;
17
+ noMatchMessage?: string;
18
+ /** Render the filter input and pill labels in the mono font (symbols, codes, ids). */
19
+ mono?: boolean;
20
+ maxWidthClass?: string;
21
+ closeText?: string;
22
+ /** aria-label of the group toggle. */
23
+ groupAriaLabel?: string;
24
+ allLabel?: string;
25
+ }
26
+ declare const __VLS_export: import('vue').DefineComponent<Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {} & {
27
+ close: () => any;
28
+ pick: (item: PillPickerItem) => any;
29
+ }, string, import('vue').PublicProps, Readonly<Props> & Readonly<{
30
+ onClose?: (() => any) | undefined;
31
+ onPick?: ((item: PillPickerItem) => any) | undefined;
32
+ }>, {
33
+ maxWidthClass: string;
34
+ searchPlaceholder: string;
35
+ emptyMessage: string;
36
+ noMatchMessage: string;
37
+ mono: boolean;
38
+ closeText: string;
39
+ groupAriaLabel: string;
40
+ allLabel: string;
41
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
42
+ declare const _default: typeof __VLS_export;
43
+ export default _default;
@@ -1,4 +1,4 @@
1
- type ToolbarVariant = 'neutral' | 'danger';
1
+ type ToolbarVariant = 'neutral' | 'danger' | 'ghost';
2
2
  type __VLS_Props = {
3
3
  label?: string;
4
4
  variant?: ToolbarVariant;
@@ -0,0 +1,31 @@
1
+ import { Ref } from 'vue';
2
+ export interface UsePollingOptions {
3
+ /** Run `fn` immediately when polling starts (mount / resume). Default true. */
4
+ immediate?: boolean;
5
+ /**
6
+ * Pause the interval while the tab is hidden and refresh + resume when it
7
+ * becomes visible again — a hidden tab has no reason to keep waking the
8
+ * backend. Default true.
9
+ */
10
+ pauseWhenHidden?: boolean;
11
+ }
12
+ /**
13
+ * Visibility-gated polling loop bound to the component lifecycle.
14
+ *
15
+ * Starts on mount, stops on unmount. With `pauseWhenHidden` (the default)
16
+ * the interval is torn down on `document.visibilitychange` → hidden and
17
+ * re-armed — with an immediate refresh — when the tab is shown again.
18
+ *
19
+ * Pass `intervalMs: null` for a refresh-only mode: `fn` runs on mount and
20
+ * on every return-to-visible, but no interval is scheduled. Useful for
21
+ * "reload this badge when the user comes back" data that doesn't warrant
22
+ * a timer.
23
+ *
24
+ * Rejections from an async `fn` are the caller's to handle — catch inside
25
+ * `fn`; the loop itself never swallows or reports them.
26
+ */
27
+ export declare function usePolling(fn: () => void | Promise<void>, intervalMs: number | null, options?: UsePollingOptions): {
28
+ start: () => void;
29
+ stop: () => void;
30
+ active: Readonly<Ref<boolean>>;
31
+ };
@@ -1,4 +1,3 @@
1
- import { ComputedRef } from 'vue';
2
1
  /**
3
2
  * Centralized dark/light Tailwind class strings.
4
3
  *
@@ -12,51 +11,55 @@ import { ComputedRef } from 'vue';
12
11
  * <p :class="t.mutedText">...</p>
13
12
  * </div>
14
13
  *
15
- * Vue auto-unwraps the computed refs inside template `:class` bindings,
16
- * so callers do not write `.value`.
14
+ * The returned object is `reactive()`, so property access yields the
15
+ * plain class string while staying reactive to theme flips. (It used to
16
+ * be a plain object of ComputedRefs — Vue does NOT unwrap refs nested in
17
+ * plain objects inside template bindings, so `:class="t.card"` rendered
18
+ * the ref's own keys — `fn dep __v_isRef …` — instead of the classes.)
19
+ * Property access is string-valued now: do not append `.value`.
17
20
  */
18
21
  export interface ThemeClasses {
19
22
  /** Card surface: bg-gray-800 / bg-white, with border. */
20
- card: ComputedRef<string>;
23
+ card: string;
21
24
  /** Slightly darker card surface: bg-gray-900 / bg-white, with border. */
22
- cardAlt: ComputedRef<string>;
25
+ cardAlt: string;
23
26
  /** Page background. */
24
- pageBg: ComputedRef<string>;
27
+ pageBg: string;
25
28
  /** Standard border between cards/sections. */
26
- border: ComputedRef<string>;
29
+ border: string;
27
30
  /** Subtle divider (lighter than border). */
28
- divider: ComputedRef<string>;
31
+ divider: string;
29
32
  /** Page headings: white / gray-900. */
30
- primaryText: ComputedRef<string>;
33
+ primaryText: string;
31
34
  /** Softer headings: gray-100 / gray-800. */
32
- primaryTextSoft: ComputedRef<string>;
35
+ primaryTextSoft: string;
33
36
  /** Body copy on cards: gray-200 / gray-700. */
34
- bodyText: ComputedRef<string>;
37
+ bodyText: string;
35
38
  /** Form labels: gray-300 / gray-700. */
36
- label: ComputedRef<string>;
39
+ label: string;
37
40
  /** Secondary text: gray-400 / gray-600. */
38
- mutedText: ComputedRef<string>;
41
+ mutedText: string;
39
42
  /** Tertiary text: gray-500 / gray-600. */
40
- subtleText: ComputedRef<string>;
43
+ subtleText: string;
41
44
  /** Dimmest text: gray-500 in dark / gray-400 in light. */
42
- dimText: ComputedRef<string>;
45
+ dimText: string;
43
46
  /** Slightly more contrast than dimText: gray-400 / gray-500. */
44
- dimTextAlt: ComputedRef<string>;
47
+ dimTextAlt: string;
45
48
  /** Big illustration / empty-state icon: gray-600 / gray-300. */
46
- illustration: ComputedRef<string>;
49
+ illustration: string;
47
50
  /** Standard text input. */
48
- input: ComputedRef<string>;
51
+ input: string;
49
52
  /** Input + placeholder colour. */
50
- inputWithPlaceholder: ComputedRef<string>;
53
+ inputWithPlaceholder: string;
51
54
  /** Ghost / Cancel button. */
52
- ghostButton: ComputedRef<string>;
55
+ ghostButton: string;
53
56
  /** Emerald accent text: emerald-400 / emerald-600. */
54
- emeraldText: ComputedRef<string>;
57
+ emeraldText: string;
55
58
  /** Amber accent text: amber-400 / amber-600. */
56
- amberText: ComputedRef<string>;
59
+ amberText: string;
57
60
  /** Amber heading-weight: amber-300 / amber-700. */
58
- amberTextStrong: ComputedRef<string>;
61
+ amberTextStrong: string;
59
62
  /** Red accent text: red-400 / red-500. */
60
- redText: ComputedRef<string>;
63
+ redText: string;
61
64
  }
62
65
  export declare function useThemeClasses(): ThemeClasses;
package/dist/index.d.ts CHANGED
@@ -32,6 +32,7 @@ export { default as BaseChipButton } from './components/BaseChipButton.vue';
32
32
  export { default as BaseRemoveButton } from './components/BaseRemoveButton.vue';
33
33
  export { default as BaseStatusPill } from './components/BaseStatusPill.vue';
34
34
  export { default as BaseFileDropzone } from './components/BaseFileDropzone.vue';
35
+ export { default as BaseCodeBlock } from './components/BaseCodeBlock.vue';
35
36
  export { default as BaseInput } from './components/BaseInput.vue';
36
37
  export { default as BaseSelect } from './components/BaseSelect.vue';
37
38
  export { default as BaseDropdown } from './components/BaseDropdown.vue';
@@ -41,6 +42,7 @@ export { default as BaseSpecFields } from './components/BaseSpecFields.vue';
41
42
  export { default as BaseStatBreakdown } from './components/BaseStatBreakdown.vue';
42
43
  export { default as BaseFilterChip } from './components/BaseFilterChip.vue';
43
44
  export { default as BaseCredentialsForm } from './components/BaseCredentialsForm.vue';
45
+ export { default as BasePillPickerModal } from './components/BasePillPickerModal.vue';
44
46
  export type { CredentialsView, CredentialsUpdate } from './components/BaseCredentialsForm.vue';
45
47
  export { useTheme, initTheme } from './composables/useTheme';
46
48
  export type { UseThemeOptions } from './composables/useTheme';
@@ -56,6 +58,8 @@ export { useNotifications } from './composables/useNotifications';
56
58
  export { useQueryParamSync } from './composables/useQueryParamSync';
57
59
  export { useFieldClasses } from './composables/useFieldClasses';
58
60
  export type { FieldClasses } from './composables/useFieldClasses';
61
+ export { usePolling } from './composables/usePolling';
62
+ export type { UsePollingOptions } from './composables/usePolling';
59
63
  export { AlertEnum } from './enums/AlertEnum';
60
64
  export { BaseBadgeEnum } from './enums/BaseBadgeEnum';
61
65
  export { BaseButtonEnum } from './enums/BaseButtonEnum';
@@ -76,12 +80,14 @@ export type { DropdownOption } from './types/dropdown';
76
80
  export type { TableColumn } from './types/table';
77
81
  export type { SpecField, SpecFieldType, SpecFieldValue } from './types/specField';
78
82
  export type { StatBreakdownItem } from './types/statBreakdown';
83
+ export type { PillPickerItem } from './types/pillPicker';
79
84
  export { getBaseColor, getBaseColorOf } from './utils/util';
80
85
  export { methodBadgeSolid, methodBadgeBright, methodBadgeTinted, statusBadgeSolid, statusBadgeTinted, statusBadgeSoft, } from './utils/httpColors';
81
86
  export { rowKeyMissing, rowValueMissing } from './utils/kvRows';
82
87
  export type { KeyValueRowLike } from './utils/kvRows';
83
88
  export { sanitizeHtml, isSafeHref } from './utils/sanitizeHtml';
84
- export { fmtNumber, fmtDate, fmtDateTime, fmtDateTimeMs, fmtDateShort, fmtCalendarDate, fmtCalendarDateTime, fmtMsAsSeconds, fmtBytes, fmtPrice, fmtPct, fmtUsd, fmtDuration, } from './utils/format';
89
+ export { fmtNumber, fmtDate, fmtDateTime, fmtDateTimeMs, fmtDateShort, fmtCalendarDate, fmtCalendarDateTime, fmtMsAsSeconds, fmtBytes, fmtPrice, fmtPct, fmtUsd, fmtDuration, formatJson, stringifyValue, } from './utils/format';
90
+ export { isValidAbsoluteUrl, isValidJson, isValidXml, isValidBase64, } from './utils/validate';
85
91
  export { buildSpecParams, firstInvalidNumericSpec } from './utils/specForm';
86
92
  export { computePnL } from './utils/pnl';
87
93
  export type { PnL, PnLInputs } from './utils/pnl';
@@ -0,0 +1,14 @@
1
+ /**
2
+ * One clickable pill in a BasePillPickerModal.
3
+ *
4
+ * `id` must be unique (it keys the pill and identifies the pick),
5
+ * `label` is what the pill shows and what the free-text filter matches,
6
+ * `group` assigns the pill to one of the modal's segmented-filter groups,
7
+ * `title` becomes the pill's tooltip.
8
+ */
9
+ export interface PillPickerItem {
10
+ id: string;
11
+ label: string;
12
+ group?: string;
13
+ title?: string;
14
+ }