mgv-backoffice 1.37.0 → 1.38.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.
@@ -1,55 +1,69 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { mount } from '@vue/test-utils'
3
- import BaseTable from './BaseTable.vue'
4
- import type { TableColumn } from '../types/table'
5
-
6
- const columns: TableColumn[] = [
7
- { label: 'Symbol' },
8
- { label: 'Qty', align: 'right' },
9
- { label: 'Bought' },
10
- ]
11
-
12
- describe('BaseTable', () => {
13
- it('renders header cells with right-alignment where requested', () => {
14
- const wrapper = mount(BaseTable, { props: { columns } })
15
- const ths = wrapper.findAll('th')
16
- expect(ths.map((th) => th.text())).toEqual(['Symbol', 'Qty', 'Bought'])
17
- expect(ths[1].classes()).toContain('text-right')
18
- expect(ths[0].classes()).not.toContain('text-right')
19
- expect(ths[0].classes()).toEqual(expect.arrayContaining(['px-4', 'py-3']))
20
- })
21
-
22
- it('renders caller rows through the default slot', () => {
23
- const wrapper = mount(BaseTable, {
24
- props: { columns },
25
- slots: { default: '<tr><td>AMD</td><td>3</td><td>today</td></tr>' },
26
- })
27
- expect(wrapper.find('tbody td').text()).toBe('AMD')
28
- expect(wrapper.find('table').classes()).toEqual(expect.arrayContaining(['w-full', 'text-sm']))
29
- })
30
-
31
- it('renders the empty-state row spanning every column when empty', () => {
32
- const wrapper = mount(BaseTable, {
33
- props: { columns, empty: true, emptyText: 'Nothing here.' },
34
- })
35
- const td = wrapper.find('tbody td')
36
- expect(td.attributes('colspan')).toBe('3')
37
- expect(td.text()).toBe('Nothing here.')
38
- })
39
-
40
- it('prefers the empty slot over emptyText', () => {
41
- const wrapper = mount(BaseTable, {
42
- props: { columns, empty: true, emptyText: 'fallback' },
43
- slots: { empty: 'No trades match your filters.' },
44
- })
45
- expect(wrapper.find('tbody td').text()).toBe('No trades match your filters.')
46
- })
47
-
48
- it('renders no empty-state row when not empty', () => {
49
- const wrapper = mount(BaseTable, {
50
- props: { columns, empty: false },
51
- slots: { default: '<tr><td>row</td></tr>' },
52
- })
53
- expect(wrapper.findAll('tbody tr')).toHaveLength(1)
54
- })
55
- })
1
+ import { describe, it, expect } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import BaseTable from './BaseTable.vue'
4
+ import type { TableColumn } from '../types/table'
5
+
6
+ const columns: TableColumn[] = [
7
+ { label: 'Symbol' },
8
+ { label: 'Qty', align: 'right' },
9
+ { label: 'Bought' },
10
+ ]
11
+
12
+ describe('BaseTable', () => {
13
+ it('renders header cells with right-alignment where requested', () => {
14
+ const wrapper = mount(BaseTable, { props: { columns } })
15
+ const ths = wrapper.findAll('th')
16
+ expect(ths.map((th) => th.text())).toEqual(['Symbol', 'Qty', 'Bought'])
17
+ expect(ths[1].classes()).toContain('text-right')
18
+ expect(ths[0].classes()).not.toContain('text-right')
19
+ expect(ths[0].classes()).toEqual(expect.arrayContaining(['px-4', 'py-3']))
20
+ })
21
+
22
+ it('renders caller rows through the default slot', () => {
23
+ const wrapper = mount(BaseTable, {
24
+ props: { columns },
25
+ slots: { default: '<tr><td>AMD</td><td>3</td><td>today</td></tr>' },
26
+ })
27
+ expect(wrapper.find('tbody td').text()).toBe('AMD')
28
+ expect(wrapper.find('table').classes()).toEqual(expect.arrayContaining(['w-full', 'text-sm']))
29
+ })
30
+
31
+ it('renders the empty-state row spanning every column when empty', () => {
32
+ const wrapper = mount(BaseTable, {
33
+ props: { columns, empty: true, emptyText: 'Nothing here.' },
34
+ })
35
+ const td = wrapper.find('tbody td')
36
+ expect(td.attributes('colspan')).toBe('3')
37
+ expect(td.text()).toBe('Nothing here.')
38
+ })
39
+
40
+ it('prefers the empty slot over emptyText', () => {
41
+ const wrapper = mount(BaseTable, {
42
+ props: { columns, empty: true, emptyText: 'fallback' },
43
+ slots: { empty: 'No trades match your filters.' },
44
+ })
45
+ expect(wrapper.find('tbody td').text()).toBe('No trades match your filters.')
46
+ })
47
+
48
+ it('renders no empty-state row when not empty', () => {
49
+ const wrapper = mount(BaseTable, {
50
+ props: { columns, empty: false },
51
+ slots: { default: '<tr><td>row</td></tr>' },
52
+ })
53
+ expect(wrapper.findAll('tbody tr')).toHaveLength(1)
54
+ })
55
+
56
+ it('has no card chrome by default and adds it with `card`', () => {
57
+ const plain = mount(BaseTable, { props: { columns } })
58
+ expect(plain.classes()).not.toContain('border')
59
+ const card = mount(BaseTable, { props: { columns, card: true } })
60
+ expect(card.classes()).toEqual(
61
+ expect.arrayContaining(['overflow-x-auto', 'rounded-xl', 'border']),
62
+ )
63
+ })
64
+
65
+ it('draws row dividers on the body', () => {
66
+ const wrapper = mount(BaseTable, { props: { columns } })
67
+ expect(wrapper.find('tbody').classes()).toEqual(expect.arrayContaining(['divide-y']))
68
+ })
69
+ })
@@ -1,50 +1,62 @@
1
- <template>
2
- <!--
3
- Styling shell for data tables — NOT a data grid. Owns the table skin
4
- (slate header band, px-4 py-3 header cells, empty-state row); rows are
5
- the caller's own <tr> markup via the default slot, so cell content and
6
- per-cell styling stay fully in the caller's hands.
7
-
8
- Wrap it yourself for scrolling/card chrome (e.g. inside a BaseRow with
9
- overflow-x-auto). Same `dark:`-variant note as BaseInput.
10
- -->
11
- <table class="w-full text-sm">
12
- <thead class="bg-slate-50 dark:bg-slate-800/60 text-slate-600 dark:text-slate-300">
13
- <tr class="text-left">
14
- <th
15
- v-for="(col, i) in columns"
16
- :key="i"
17
- class="px-4 py-3"
18
- :class="col.align === 'right' ? 'text-right' : ''"
19
- >
20
- {{ col.label }}
21
- </th>
22
- </tr>
23
- </thead>
24
- <tbody>
25
- <tr v-if="empty">
26
- <td :colspan="columns.length" class="px-4 py-12 text-center text-slate-500">
27
- <slot name="empty">{{ emptyText }}</slot>
28
- </td>
29
- </tr>
30
- <slot />
31
- </tbody>
32
- </table>
33
- </template>
34
-
35
- <script setup lang="ts">
36
- import type { TableColumn } from '../types/table'
37
-
38
- interface Props {
39
- columns: TableColumn[]
40
- /** True when there are no rows — renders the empty-state row. */
41
- empty?: boolean
42
- /** Fallback empty-state text; the `empty` slot overrides it. */
43
- emptyText?: string
44
- }
45
-
46
- withDefaults(defineProps<Props>(), {
47
- empty: false,
48
- emptyText: 'No rows.',
49
- })
50
- </script>
1
+ <template>
2
+ <div :class="card ? CARD_CLASS : undefined">
3
+ <!--
4
+ Styling shell for data tables — NOT a data grid. Owns the table skin
5
+ (slate header band, px-4 py-3 header cells, row dividers + hover,
6
+ empty-state row); rows are the caller's own <tr> markup via the default
7
+ slot, so cell content and per-cell styling stay fully in the caller's hands.
8
+
9
+ `card` adds the shared card chrome (rounded border, surface, horizontal
10
+ scroll). Leave it off when the caller already wraps the table itself
11
+ (e.g. inside a BaseRow with overflow-x-auto). Same `dark:`-variant note
12
+ as BaseInput.
13
+ -->
14
+ <table class="w-full text-sm">
15
+ <thead class="bg-slate-50 dark:bg-slate-800/60 text-slate-600 dark:text-slate-300">
16
+ <tr class="text-left border-b border-slate-200 dark:border-slate-700">
17
+ <th
18
+ v-for="(col, i) in columns"
19
+ :key="i"
20
+ class="px-4 py-3"
21
+ :class="col.align === 'right' ? 'text-right' : ''"
22
+ >
23
+ {{ col.label }}
24
+ </th>
25
+ </tr>
26
+ </thead>
27
+ <tbody
28
+ class="divide-y divide-slate-200 dark:divide-slate-700 [&>tr]:transition-colors [&>tr:hover]:bg-slate-50 dark:[&>tr:hover]:bg-slate-800/40"
29
+ >
30
+ <tr v-if="empty">
31
+ <td :colspan="columns.length" class="px-4 py-12 text-center text-slate-500">
32
+ <slot name="empty">{{ emptyText }}</slot>
33
+ </td>
34
+ </tr>
35
+ <slot />
36
+ </tbody>
37
+ </table>
38
+ </div>
39
+ </template>
40
+
41
+ <script setup lang="ts">
42
+ import type { TableColumn } from '../types/table'
43
+
44
+ const CARD_CLASS =
45
+ 'overflow-x-auto rounded-xl border border-slate-200 bg-white shadow-sm dark:border-slate-700 dark:bg-slate-900'
46
+
47
+ interface Props {
48
+ columns: TableColumn[]
49
+ /** True when there are no rows — renders the empty-state row. */
50
+ empty?: boolean
51
+ /** Fallback empty-state text; the `empty` slot overrides it. */
52
+ emptyText?: string
53
+ /** Wrap the table in the rounded, bordered, horizontally scrolling card. */
54
+ card?: boolean
55
+ }
56
+
57
+ withDefaults(defineProps<Props>(), {
58
+ empty: false,
59
+ emptyText: 'No rows.',
60
+ card: false,
61
+ })
62
+ </script>
package/src/index.ts CHANGED
@@ -56,6 +56,10 @@ export type { CredentialsView, CredentialsUpdate } from './components/BaseCreden
56
56
  // Components — third-party wrappers
57
57
  export { default as BaseDateTimePicker } from './components/BaseDateTimePicker.vue'
58
58
  export type { DateTimePickerMode } from './components/BaseDateTimePicker.vue'
59
+ export { default as BaseSearchSelect } from './components/BaseSearchSelect.vue'
60
+ export { default as BaseCard } from './components/BaseCard.vue'
61
+ export { default as BaseField } from './components/BaseField.vue'
62
+ export { default as BaseStatCard } from './components/BaseStatCard.vue'
59
63
 
60
64
  // Composables
61
65
  export { useTheme, initTheme } from './composables/useTheme'