mgv-backoffice 1.20.0 → 1.22.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
@@ -414,29 +414,43 @@ import { ColoredSquares, ColorsEnums } from 'mgv-backoffice'
414
414
 
415
415
  ### EarningsCard
416
416
 
417
- Earnings summary card with formatted currency display.
417
+ Earnings summary card with formatted currency display. Supports a signed P&L
418
+ mode that renders a red loss theme (and a downward trend glyph) for negative
419
+ amounts.
418
420
 
419
421
  **Props:**
420
422
 
421
423
  | Prop | Type | Default | Description |
422
424
  | ---------- | -------- |-------------------------|----------------------|
423
425
  | `title` | `String` | `'TOTAL EARNINGS'` | Card heading |
424
- | `amount` | `Number` | `0` | Monetary value |
426
+ | `amount` | `Number` | `0` | Monetary value (a stringified number is coerced) |
425
427
  | `subtitle` | `String` | `'Lifetime commission'` | Subheading text |
426
428
  | `badge` | `String` | `''` | Optional badge label |
427
429
  | `currency` | `String` | `'$'` | Currency symbol |
428
430
  | `decimals` | `Number` | `2` | Fraction digits shown for the amount |
431
+ | `accent` | `'orange' \| 'emerald' \| 'red'` | `'orange'` | Card theme. `emerald` tints it green; `red` is the loss theme. |
432
+ | `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. |
429
433
 
430
434
  **Example:**
431
435
 
432
436
  ```vue
433
437
  <template>
438
+ <!-- Always-positive total: original behaviour. -->
434
439
  <EarningsCard
435
440
  title="Monthly Revenue"
436
441
  :amount="12500"
437
442
  subtitle="April 2026"
438
443
  currency="€"
439
444
  />
445
+
446
+ <!-- Signed P&L: renders red + a down arrow when the amount is negative. -->
447
+ <EarningsCard
448
+ title="TOTAL P&L"
449
+ :amount="-128.4"
450
+ subtitle="Realised + unrealised"
451
+ accent="emerald"
452
+ signed
453
+ />
440
454
  </template>
441
455
 
442
456
  <script setup lang="ts">
@@ -1150,6 +1164,135 @@ import { BaseCopyButton, BaseToastEnum } from 'mgv-backoffice'
1150
1164
 
1151
1165
  ---
1152
1166
 
1167
+ ## Forms & tables
1168
+
1169
+ These components use `dark:` Tailwind variants, so the consuming app must map
1170
+ the `dark` variant to the `.dark` class that `useTheme()` toggles (see
1171
+ [Tailwind setup for consumers](#tailwind-setup-for-consumers)).
1172
+
1173
+ ### BaseInput
1174
+
1175
+ Themed text/number input carrying the shared field skin (slate border,
1176
+ `bg-slate-50` / dark `bg-slate-900` surface). Everything else — `placeholder`,
1177
+ `id`, `disabled`, `step`/`min`, extra classes like `font-mono` or
1178
+ `placeholder:*` — falls through via attrs and Vue class merging.
1179
+
1180
+ **Props:**
1181
+
1182
+ | Prop | Type | Default | Description |
1183
+ | ------------ | ------------------ | -------- | ----------- |
1184
+ | `modelValue` | `String \| Number` | `''` | `v-model` value. |
1185
+ | `type` | `String` | `'text'` | Native input type. |
1186
+ | `size` | `String` | `'md'` | `'md'` = `px-3 py-2`, `'sm'` = `px-2 py-1.5`. |
1187
+ | `block` | `Boolean` | `true` | Full-width (`w-full`); set `false` for inline fields. |
1188
+
1189
+ **Emits:** `update:modelValue(value: string)` — always the raw string; parse
1190
+ numbers in the owner.
1191
+
1192
+ ```vue
1193
+ <BaseInput v-model="query" placeholder="e.g. AMD or BTC" class="font-mono" />
1194
+ ```
1195
+
1196
+ ### BaseSelect
1197
+
1198
+ Themed `<select>` sharing BaseInput's field skin. Options come from the
1199
+ default slot so callers keep full control of `<option>` rendering.
1200
+
1201
+ **Props:**
1202
+
1203
+ | Prop | Type | Default | Description |
1204
+ | ------------ | ------------------ | ------- | ----------- |
1205
+ | `modelValue` | `String \| Number` | `''` | `v-model` value. |
1206
+ | `size` | `String` | `'sm'` | `'sm'` = `px-2 py-1.5`, `'md'` = `px-3 py-2`. |
1207
+ | `block` | `Boolean` | `true` | Full-width; set `false` for inline selects. |
1208
+
1209
+ **Slots:** `default` — the `<option>` elements.
1210
+ **Emits:** `update:modelValue(value: string)`.
1211
+
1212
+ ```vue
1213
+ <BaseSelect v-model="strategyType">
1214
+ <option v-for="e in catalog" :key="e.type" :value="e.type" :title="e.description">
1215
+ {{ e.label }}
1216
+ </option>
1217
+ </BaseSelect>
1218
+ ```
1219
+
1220
+ ### BaseSegmentedControl
1221
+
1222
+ Segmented button group ("All | Stock | Crypto"). One button per option; the
1223
+ selected one gets the filled treatment and `aria-pressed="true"`.
1224
+
1225
+ **Props:**
1226
+
1227
+ | Prop | Type | Default | Description |
1228
+ | ------------- | ------------------- | -------- | ----------- |
1229
+ | `options` | `SegmentedOption[]` | **required** | `{ value, label, title? }` per button. |
1230
+ | `modelValue` | `String \| Number` | **required** | Selected option's `value` (`v-model`). |
1231
+ | `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). |
1232
+ | `ariaLabel` | `String` | `''` | When set, the wrapper renders `role="group"` + `aria-label`. |
1233
+ | `optionClass` | `Function` | — | `(option, active) => string` override for per-button fill classes (e.g. severity colours); layout stays owned by the variant. |
1234
+
1235
+ **Emits:** `update:modelValue(value)`.
1236
+
1237
+ ```vue
1238
+ <BaseSegmentedControl v-model="assetFilter" :options="ASSET_FILTERS" />
1239
+ <BaseSegmentedControl v-model="exchange" :options="EXCHANGES" variant="wide" aria-label="Exchange" />
1240
+ ```
1241
+
1242
+ ### BaseTable
1243
+
1244
+ Styling shell for data tables — **not** a data grid. Owns the table skin
1245
+ (slate header band, `px-4 py-3` header cells, empty-state row); body rows are
1246
+ the caller's own `<tr>` markup via the default slot. Wrap it yourself for
1247
+ scrolling/card chrome (e.g. a `BaseRow` with `overflow-x-auto`).
1248
+
1249
+ **Props:**
1250
+
1251
+ | Prop | Type | Default | Description |
1252
+ | ----------- | --------------- | ------------ | ----------- |
1253
+ | `columns` | `TableColumn[]` | **required** | `{ label, align? }`; `align: 'right'` right-aligns the header cell. |
1254
+ | `empty` | `Boolean` | `false` | True renders the empty-state row spanning every column. |
1255
+ | `emptyText` | `String` | `'No rows.'` | Fallback empty-state text. |
1256
+
1257
+ **Slots:** `default` — the `<tr>` rows; `empty` — custom empty-state content.
1258
+
1259
+ ```vue
1260
+ <BaseTable :columns="COLUMNS" :empty="rows.length === 0">
1261
+ <template #empty>No trades match your filters.</template>
1262
+ <tr v-for="row in rows" :key="row.id" class="border-t border-slate-200 dark:border-slate-700">
1263
+
1264
+ </tr>
1265
+ </BaseTable>
1266
+ ```
1267
+
1268
+ ### BaseSpecFields
1269
+
1270
+ Spec-driven form fields: renders a select / checkbox / number input per
1271
+ `SpecField`, with labels and help text, in a responsive two-column grid. Feed
1272
+ it a backend-described catalogue and every form editing those values stays in
1273
+ lockstep. Never mutates `params` — every edit is emitted as `(key, value)`
1274
+ and the owner writes it back into its own state.
1275
+
1276
+ **Props:**
1277
+
1278
+ | Prop | Type | Default | Description |
1279
+ | -------- | -------------------------------- | ------------ | ----------- |
1280
+ | `specs` | `SpecField[]` | **required** | `{ key, label, type: 'decimal' \| 'integer' \| 'boolean' \| 'select', options?, step?, min?, help? }`. |
1281
+ | `params` | `Record<string, SpecFieldValue>` | **required** | Current values keyed by `spec.key`. |
1282
+
1283
+ **Slots:** `after` (`{ spec }`) — extra content under each field (e.g. a live
1284
+ preview attached to one key).
1285
+ **Emits:** `update(key: string, value: SpecFieldValue)` — numbers are parsed
1286
+ (`parseFloat`); unparseable input passes through raw so the owner's
1287
+ validation can catch it.
1288
+
1289
+ ```vue
1290
+ <BaseSpecFields :specs="entry.params" :params="form.params"
1291
+ @update="(key, value) => (form.params[key] = value)" />
1292
+ ```
1293
+
1294
+ ---
1295
+
1153
1296
  ## Composables
1154
1297
 
1155
1298
  ```ts
@@ -0,0 +1,21 @@
1
+ interface Props {
2
+ modelValue?: string | number | null;
3
+ /** Native input type ('text', 'number', 'password', …). */
4
+ type?: string;
5
+ /** 'md' = px-3 py-2 (default); 'sm' = px-2 py-1.5 compact. */
6
+ size?: 'sm' | 'md';
7
+ /** Full-width (w-full). Set false for inline fields. */
8
+ block?: boolean;
9
+ }
10
+ declare const __VLS_export: import('vue').DefineComponent<Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {} & {
11
+ "update:modelValue": (value: string) => any;
12
+ }, string, import('vue').PublicProps, Readonly<Props> & Readonly<{
13
+ "onUpdate:modelValue"?: ((value: string) => any) | undefined;
14
+ }>, {
15
+ type: string;
16
+ size: "sm" | "md";
17
+ block: boolean;
18
+ modelValue: string | number | null;
19
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
20
+ declare const _default: typeof __VLS_export;
21
+ export default _default;
@@ -0,0 +1,25 @@
1
+ import { SegmentedOption } from '../types/segmented';
2
+ interface Props {
3
+ options: readonly SegmentedOption[];
4
+ modelValue: string | number;
5
+ variant?: 'base' | 'wide' | 'toolbar';
6
+ /** When set, the wrapper announces itself as a labelled group. */
7
+ ariaLabel?: string;
8
+ /**
9
+ * Override the fill (colour) classes per button — receives the option and
10
+ * whether it is the selected one. Defaults to the variant's emerald/slate
11
+ * treatment.
12
+ */
13
+ optionClass?: (option: SegmentedOption, active: boolean) => string;
14
+ }
15
+ declare const __VLS_export: import('vue').DefineComponent<Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {} & {
16
+ "update:modelValue": (value: any) => any;
17
+ }, string, import('vue').PublicProps, Readonly<Props> & Readonly<{
18
+ "onUpdate:modelValue"?: ((value: any) => any) | undefined;
19
+ }>, {
20
+ variant: "base" | "wide" | "toolbar";
21
+ ariaLabel: string;
22
+ optionClass: (option: SegmentedOption, active: boolean) => string;
23
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
24
+ declare const _default: typeof __VLS_export;
25
+ export default _default;
@@ -0,0 +1,28 @@
1
+ interface Props {
2
+ modelValue?: string | null;
3
+ /** 'sm' = px-2 py-1.5 (default, the usual select height); 'md' = px-3 py-2. */
4
+ size?: 'sm' | 'md';
5
+ /** Full-width (w-full). Set false for inline selects. */
6
+ block?: boolean;
7
+ }
8
+ declare var __VLS_1: {};
9
+ type __VLS_Slots = {} & {
10
+ default?: (props: typeof __VLS_1) => any;
11
+ };
12
+ declare const __VLS_base: import('vue').DefineComponent<Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {} & {
13
+ "update:modelValue": (value: any) => any;
14
+ }, string, import('vue').PublicProps, Readonly<Props> & Readonly<{
15
+ "onUpdate:modelValue"?: ((value: any) => any) | undefined;
16
+ }>, {
17
+ size: "sm" | "md";
18
+ block: boolean;
19
+ modelValue: string | null;
20
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
21
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
22
+ declare const _default: typeof __VLS_export;
23
+ export default _default;
24
+ type __VLS_WithSlots<T, S> = T & {
25
+ new (): {
26
+ $slots: S;
27
+ };
28
+ };
@@ -0,0 +1,23 @@
1
+ import { SpecField, SpecFieldValue } from '../types/specField';
2
+ type __VLS_Props = {
3
+ specs: SpecField[];
4
+ params: Record<string, SpecFieldValue>;
5
+ };
6
+ type __VLS_Slots = {
7
+ after?: (props: {
8
+ spec: SpecField;
9
+ }) => unknown;
10
+ };
11
+ declare const __VLS_base: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {} & {
12
+ update: (key: string, value: SpecFieldValue) => any;
13
+ }, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{
14
+ onUpdate?: ((key: string, value: SpecFieldValue) => any) | undefined;
15
+ }>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
16
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
17
+ declare const _default: typeof __VLS_export;
18
+ export default _default;
19
+ type __VLS_WithSlots<T, S> = T & {
20
+ new (): {
21
+ $slots: S;
22
+ };
23
+ };
@@ -0,0 +1,26 @@
1
+ import { TableColumn } from '../types/table';
2
+ interface Props {
3
+ columns: TableColumn[];
4
+ /** True when there are no rows — renders the empty-state row. */
5
+ empty?: boolean;
6
+ /** Fallback empty-state text; the `empty` slot overrides it. */
7
+ emptyText?: string;
8
+ }
9
+ declare var __VLS_1: {}, __VLS_3: {};
10
+ type __VLS_Slots = {} & {
11
+ empty?: (props: typeof __VLS_1) => any;
12
+ } & {
13
+ default?: (props: typeof __VLS_3) => any;
14
+ };
15
+ declare const __VLS_base: import('vue').DefineComponent<Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<Props> & Readonly<{}>, {
16
+ emptyText: string;
17
+ empty: boolean;
18
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
19
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
20
+ declare const _default: typeof __VLS_export;
21
+ export default _default;
22
+ type __VLS_WithSlots<T, S> = T & {
23
+ new (): {
24
+ $slots: S;
25
+ };
26
+ };
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * Accent colour of the card. Defaults to `orange` so existing consumers are
3
- * unaffected; `emerald` lets a host app tint the card to a green-themed brand.
4
- * Each accent carries the full literal Tailwind class strings (rather than a
5
- * single interpolated hue) so the classes are statically detectable and the
6
- * two themes can differ in more than hue e.g. orange renders the amount in
7
- * neutral grey while emerald renders it in the accent itself.
3
+ * unaffected; `emerald` lets a host app tint the card to a green-themed brand,
4
+ * and `red` renders a loss/negative theme (also selected automatically by
5
+ * `signed`, see below). Each accent carries the full literal Tailwind class
6
+ * strings (rather than a single interpolated hue) so the classes are statically
7
+ * detectable and the themes can differ in more than hue — e.g. orange renders
8
+ * the amount in neutral grey while emerald/red render it in the accent itself.
8
9
  */
9
- type Accent = 'orange' | 'emerald';
10
+ type Accent = 'orange' | 'emerald' | 'red';
10
11
  interface Props {
11
12
  title?: string;
12
13
  amount?: number;
@@ -15,6 +16,15 @@ interface Props {
15
16
  currency?: string;
16
17
  decimals?: number;
17
18
  accent?: Accent;
19
+ /**
20
+ * Treat the card as a signed P&L figure. When `true` and the amount is
21
+ * negative, the card renders the loss theme automatically: the `red` accent
22
+ * (border / amount / subtitle / icon-box recoloured) AND the trend glyph
23
+ * flipped to point DOWN. A non-negative amount keeps the chosen `accent` and
24
+ * the upward glyph. Defaults to `false` so existing consumers — which use the
25
+ * card for always-positive totals like lifetime earnings — are unaffected.
26
+ */
27
+ signed?: boolean;
18
28
  }
19
29
  declare const __VLS_export: import('vue').DefineComponent<Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<Props> & Readonly<{}>, {
20
30
  title: string;
@@ -24,6 +34,7 @@ declare const __VLS_export: import('vue').DefineComponent<Props, {}, {}, {}, {},
24
34
  currency: string;
25
35
  decimals: number;
26
36
  accent: Accent;
37
+ signed: boolean;
27
38
  }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
28
39
  declare const _default: typeof __VLS_export;
29
40
  export default _default;
package/dist/index.d.ts CHANGED
@@ -28,6 +28,11 @@ export { default as BaseCopyButton } from './components/BaseCopyButton.vue';
28
28
  export { default as BaseGoogleSignInButton } from './components/BaseGoogleSignInButton.vue';
29
29
  export { default as BaseLoginForm } from './components/BaseLoginForm.vue';
30
30
  export { default as BaseNotificationPanel } from './components/BaseNotificationPanel.vue';
31
+ export { default as BaseInput } from './components/BaseInput.vue';
32
+ export { default as BaseSelect } from './components/BaseSelect.vue';
33
+ export { default as BaseSegmentedControl } from './components/BaseSegmentedControl.vue';
34
+ export { default as BaseTable } from './components/BaseTable.vue';
35
+ export { default as BaseSpecFields } from './components/BaseSpecFields.vue';
31
36
  export { useTheme, initTheme } from './composables/useTheme';
32
37
  export type { UseThemeOptions } from './composables/useTheme';
33
38
  export { useThemeClasses } from './composables/useThemeClasses';
@@ -54,6 +59,9 @@ export type { NavItem, NavSection } from './types/sidebar';
54
59
  export type { EntityPickerItem } from './types/entityPicker';
55
60
  export type { LoginCredentials } from './types/auth';
56
61
  export type { NotificationItem } from './types/notification';
62
+ export type { SegmentedOption } from './types/segmented';
63
+ export type { TableColumn } from './types/table';
64
+ export type { SpecField, SpecFieldType, SpecFieldValue } from './types/specField';
57
65
  export { getBaseColor, getBaseColorOf } from './utils/util';
58
66
  export { methodBadgeSolid, methodBadgeBright, statusBadgeSolid, statusBadgeTinted, } from './utils/httpColors';
59
67
  export { sanitizeHtml, isSafeHref } from './utils/sanitizeHtml';
@@ -0,0 +1,11 @@
1
+ /**
2
+ * One choice in a BaseSegmentedControl. `value` is what the control emits,
3
+ * `label` what the button shows; `title` becomes the button's tooltip.
4
+ * Generic so callers with union-typed values ('all' | 'STOCK' | …) keep
5
+ * full type-safety through v-model.
6
+ */
7
+ export interface SegmentedOption<T extends string | number = string | number> {
8
+ value: T;
9
+ label: string;
10
+ title?: string;
11
+ }
@@ -0,0 +1,23 @@
1
+ /** Which input a BaseSpecFields entry renders. */
2
+ export type SpecFieldType = 'decimal' | 'integer' | 'boolean' | 'select';
3
+ /**
4
+ * One field of a spec-driven form: the field's key in the value map, its
5
+ * label, which input to render, and optional validation/rendering hints.
6
+ * Members are loose (`| null` allowed) so backend-described catalogues can be
7
+ * passed straight through without mapping.
8
+ */
9
+ export interface SpecField {
10
+ key: string;
11
+ label: string;
12
+ type: SpecFieldType;
13
+ /** Choices for `type: 'select'`. */
14
+ options?: string[] | null;
15
+ /** Step for numeric inputs; defaults to 1 (integer) / 0.01 (decimal). */
16
+ step?: number | null;
17
+ /** Minimum for numeric inputs. */
18
+ min?: number | null;
19
+ /** Help text rendered under the field. */
20
+ help?: string | null;
21
+ }
22
+ /** A field's current value in the owner's form state. */
23
+ export type SpecFieldValue = number | boolean | string;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * One header cell of a BaseTable. `align: 'right'` right-aligns the header
3
+ * (numeric columns); body cells are the caller's markup and align themselves.
4
+ */
5
+ export interface TableColumn {
6
+ label: string;
7
+ align?: 'left' | 'right';
8
+ }