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.
@@ -3,13 +3,14 @@ import { computed } from 'vue'
3
3
 
4
4
  /**
5
5
  * Accent colour of the card. Defaults to `orange` so existing consumers are
6
- * unaffected; `emerald` lets a host app tint the card to a green-themed brand.
7
- * Each accent carries the full literal Tailwind class strings (rather than a
8
- * single interpolated hue) so the classes are statically detectable and the
9
- * two themes can differ in more than hue — e.g. orange renders the amount in
10
- * neutral grey while emerald renders it in the accent itself.
6
+ * unaffected; `emerald` lets a host app tint the card to a green-themed brand,
7
+ * and `red` renders a loss/negative theme (also selected automatically by
8
+ * `signed`, see below). Each accent carries the full literal Tailwind class
9
+ * strings (rather than a single interpolated hue) so the classes are statically
10
+ * detectable and the themes can differ in more than hue — e.g. orange renders
11
+ * the amount in neutral grey while emerald/red render it in the accent itself.
11
12
  */
12
- type Accent = 'orange' | 'emerald'
13
+ type Accent = 'orange' | 'emerald' | 'red'
13
14
 
14
15
  interface Props {
15
16
  title?: string
@@ -19,6 +20,15 @@ interface Props {
19
20
  currency?: string
20
21
  decimals?: number
21
22
  accent?: Accent
23
+ /**
24
+ * Treat the card as a signed P&L figure. When `true` and the amount is
25
+ * negative, the card renders the loss theme automatically: the `red` accent
26
+ * (border / amount / subtitle / icon-box recoloured) AND the trend glyph
27
+ * flipped to point DOWN. A non-negative amount keeps the chosen `accent` and
28
+ * the upward glyph. Defaults to `false` so existing consumers — which use the
29
+ * card for always-positive totals like lifetime earnings — are unaffected.
30
+ */
31
+ signed?: boolean
22
32
  }
23
33
 
24
34
  const props = withDefaults(defineProps<Props>(), {
@@ -29,6 +39,7 @@ const props = withDefaults(defineProps<Props>(), {
29
39
  currency: '$',
30
40
  decimals: 2,
31
41
  accent: 'orange',
42
+ signed: false,
32
43
  })
33
44
 
34
45
  const ACCENTS: Record<Accent, {
@@ -55,22 +66,40 @@ const ACCENTS: Record<Accent, {
55
66
  iconBox: 'bg-emerald-50 dark:bg-emerald-900/30',
56
67
  icon: 'text-emerald-500',
57
68
  },
69
+ red: {
70
+ border: 'border-red-300 dark:border-red-700',
71
+ badge: 'bg-red-100 text-red-600 dark:bg-red-900/40 dark:text-red-400',
72
+ amount: 'text-red-500 dark:text-red-400',
73
+ subtitle: 'text-red-500 dark:text-red-400',
74
+ iconBox: 'bg-red-50 dark:bg-red-900/30',
75
+ icon: 'text-red-500',
76
+ },
58
77
  }
59
78
 
60
- const cls = computed(() => ACCENTS[props.accent])
79
+ // Coerce once: the amount can arrive as a JSON string (e.g. "-0.01"); fall back
80
+ // to 0 for non-finite input (NaN/Infinity) so the card never renders "NaN".
81
+ const numericAmount = computed(() => {
82
+ const n = Number(props.amount)
83
+ return Number.isFinite(n) ? n : 0
84
+ })
85
+
86
+ // A loss only when the card is in signed mode and the value is below zero.
87
+ const isLoss = computed(() => props.signed && numericAmount.value < 0)
61
88
 
62
- const formattedAmount = computed(() => {
63
- // Guard against NaN/Infinity reaching toLocaleString (which would render the
64
- // literal "NaN"); fall back to a zeroed amount so the card stays readable.
65
- const amount = Number.isFinite(props.amount) ? props.amount : 0
66
- return (
89
+ // In signed mode a negative amount forces the red loss theme; otherwise the
90
+ // caller-chosen accent stands.
91
+ const effectiveAccent = computed<Accent>(() => (isLoss.value ? 'red' : props.accent))
92
+
93
+ const cls = computed(() => ACCENTS[effectiveAccent.value])
94
+
95
+ const formattedAmount = computed(
96
+ () =>
67
97
  props.currency +
68
- amount.toLocaleString('en-US', {
98
+ numericAmount.value.toLocaleString('en-US', {
69
99
  minimumFractionDigits: props.decimals,
70
100
  maximumFractionDigits: props.decimals,
71
- })
72
- )
73
- })
101
+ }),
102
+ )
74
103
  </script>
75
104
 
76
105
  <template>
@@ -121,8 +150,16 @@ const formattedAmount = computed(() => {
121
150
  stroke-linecap="round"
122
151
  stroke-linejoin="round"
123
152
  >
124
- <polyline points="22 7 13.5 15.5 8.5 10.5 2 17" />
125
- <polyline points="16 7 22 7 22 13" />
153
+ <!-- Loss: a downward trend line + down-right arrowhead (the upward
154
+ glyph mirrored vertically). Otherwise the standard up-trend. -->
155
+ <template v-if="isLoss">
156
+ <polyline points="22 17 13.5 8.5 8.5 13.5 2 7" />
157
+ <polyline points="16 17 22 17 22 11" />
158
+ </template>
159
+ <template v-else>
160
+ <polyline points="22 7 13.5 15.5 8.5 10.5 2 17" />
161
+ <polyline points="16 7 22 7 22 13" />
162
+ </template>
126
163
  </svg>
127
164
  </div>
128
165
  </div>
package/src/index.ts CHANGED
@@ -34,6 +34,13 @@ export { default as BaseGoogleSignInButton } from './components/BaseGoogleSignIn
34
34
  export { default as BaseLoginForm } from './components/BaseLoginForm.vue'
35
35
  export { default as BaseNotificationPanel } from './components/BaseNotificationPanel.vue'
36
36
 
37
+ // Components — extracted from TradeAutomation
38
+ export { default as BaseInput } from './components/BaseInput.vue'
39
+ export { default as BaseSelect } from './components/BaseSelect.vue'
40
+ export { default as BaseSegmentedControl } from './components/BaseSegmentedControl.vue'
41
+ export { default as BaseTable } from './components/BaseTable.vue'
42
+ export { default as BaseSpecFields } from './components/BaseSpecFields.vue'
43
+
37
44
  // Composables
38
45
  export { useTheme, initTheme } from './composables/useTheme'
39
46
  export type { UseThemeOptions } from './composables/useTheme'
@@ -65,6 +72,9 @@ export type { NavItem, NavSection } from './types/sidebar'
65
72
  export type { EntityPickerItem } from './types/entityPicker'
66
73
  export type { LoginCredentials } from './types/auth'
67
74
  export type { NotificationItem } from './types/notification'
75
+ export type { SegmentedOption } from './types/segmented'
76
+ export type { TableColumn } from './types/table'
77
+ export type { SpecField, SpecFieldType, SpecFieldValue } from './types/specField'
68
78
 
69
79
  // Utils
70
80
  export { getBaseColor, getBaseColorOf } from './utils/util'
@@ -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,25 @@
1
+ /** Which input a BaseSpecFields entry renders. */
2
+ export type SpecFieldType = 'decimal' | 'integer' | 'boolean' | 'select'
3
+
4
+ /**
5
+ * One field of a spec-driven form: the field's key in the value map, its
6
+ * label, which input to render, and optional validation/rendering hints.
7
+ * Members are loose (`| null` allowed) so backend-described catalogues can be
8
+ * passed straight through without mapping.
9
+ */
10
+ export interface SpecField {
11
+ key: string
12
+ label: string
13
+ type: SpecFieldType
14
+ /** Choices for `type: 'select'`. */
15
+ options?: string[] | null
16
+ /** Step for numeric inputs; defaults to 1 (integer) / 0.01 (decimal). */
17
+ step?: number | null
18
+ /** Minimum for numeric inputs. */
19
+ min?: number | null
20
+ /** Help text rendered under the field. */
21
+ help?: string | null
22
+ }
23
+
24
+ /** A field's current value in the owner's form state. */
25
+ 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
+ }