mgv-backoffice 1.38.0 → 1.41.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.
Files changed (35) hide show
  1. package/README.md +91 -3
  2. package/dist/components/BaseAuthLayout.vue.d.ts +27 -0
  3. package/dist/components/BaseBooleanBadge.vue.d.ts +12 -0
  4. package/dist/components/BaseCheckbox.vue.d.ts +28 -0
  5. package/dist/components/BaseDetailList.vue.d.ts +13 -0
  6. package/dist/components/BasePageHeader.vue.d.ts +3 -0
  7. package/dist/components/BaseSidebarUser.vue.d.ts +23 -0
  8. package/dist/components/BaseToggle.vue.d.ts +33 -0
  9. package/dist/index.d.ts +7 -0
  10. package/dist/types/detail.d.ts +10 -0
  11. package/dist/ui-lib.css +1 -1
  12. package/dist/ui-lib.js +847 -602
  13. package/dist/ui-lib.umd.cjs +1 -1
  14. package/package.json +1 -1
  15. package/src/components/BaseAuthLayout.test.ts +48 -0
  16. package/src/components/BaseAuthLayout.vue +50 -0
  17. package/src/components/BaseBooleanBadge.vue +32 -0
  18. package/src/components/BaseButton.vue +1 -1
  19. package/src/components/BaseCheckbox.test.ts +30 -0
  20. package/src/components/BaseCheckbox.vue +51 -0
  21. package/src/components/BaseDetailList.test.ts +36 -0
  22. package/src/components/BaseDetailList.vue +43 -0
  23. package/src/components/BasePageHeader.vue +13 -2
  24. package/src/components/BaseSelect.test.ts +1 -1
  25. package/src/components/BaseSelect.vue +3 -2
  26. package/src/components/BaseSidebarUser.test.ts +26 -0
  27. package/src/components/BaseSidebarUser.vue +75 -0
  28. package/src/components/BaseSpinner.vue +76 -76
  29. package/src/components/BaseToggle.test.ts +39 -0
  30. package/src/components/BaseToggle.vue +73 -0
  31. package/src/components/Pagination.test.ts +93 -85
  32. package/src/components/Pagination.vue +87 -82
  33. package/src/index.ts +7 -0
  34. package/src/style.css +58 -58
  35. package/src/types/detail.ts +10 -0
@@ -0,0 +1,39 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import BaseToggle from './BaseToggle.vue'
4
+
5
+ describe('BaseToggle', () => {
6
+ it('reflects the value in aria-checked and the track colour', () => {
7
+ const off = mount(BaseToggle, { props: { modelValue: false } })
8
+ expect(off.find('[role=switch]').attributes('aria-checked')).toBe('false')
9
+ expect(off.find('[role=switch]').classes()).toContain('bg-slate-200')
10
+
11
+ const on = mount(BaseToggle, { props: { modelValue: true, color: 'red' } })
12
+ expect(on.find('[role=switch]').attributes('aria-checked')).toBe('true')
13
+ expect(on.find('[role=switch]').classes()).toContain('bg-red-600')
14
+ })
15
+
16
+ it('uses the red off track with offTone="red"', () => {
17
+ const w = mount(BaseToggle, { props: { modelValue: false, offTone: 'red' } })
18
+ expect(w.find('[role=switch]').classes()).toContain('bg-red-200')
19
+ })
20
+
21
+ it('emits update:modelValue and change with the flipped value', async () => {
22
+ const w = mount(BaseToggle, { props: { modelValue: false } })
23
+ await w.find('[role=switch]').trigger('click')
24
+ expect(w.emitted('update:modelValue')).toEqual([[true]])
25
+ expect(w.emitted('change')).toEqual([[true]])
26
+ })
27
+
28
+ it('does nothing while disabled', async () => {
29
+ const w = mount(BaseToggle, { props: { modelValue: true, disabled: true } })
30
+ await w.find('[role=switch]').trigger('click')
31
+ expect(w.emitted('update:modelValue')).toBeUndefined()
32
+ })
33
+
34
+ it('renders the label, or uses ariaLabel when there is none', () => {
35
+ expect(mount(BaseToggle, { props: { label: 'Crawler' } }).text()).toBe('Crawler')
36
+ const w = mount(BaseToggle, { props: { ariaLabel: 'Alive' } })
37
+ expect(w.find('[role=switch]').attributes('aria-label')).toBe('Alive')
38
+ })
39
+ })
@@ -0,0 +1,73 @@
1
+ <template>
2
+ <label
3
+ class="inline-flex items-center gap-3 select-none"
4
+ :class="disabled ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'"
5
+ >
6
+ <button
7
+ type="button"
8
+ role="switch"
9
+ :aria-checked="modelValue"
10
+ :aria-label="label ? undefined : ariaLabel || undefined"
11
+ :disabled="disabled"
12
+ class="relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-slate-900 disabled:cursor-not-allowed"
13
+ :class="modelValue ? ON[color] : offTone === 'red' ? 'bg-red-200 dark:bg-red-900/50' : 'bg-slate-200 dark:bg-slate-700'"
14
+ @click="toggle"
15
+ >
16
+ <span
17
+ class="inline-block h-5 w-5 rounded-full bg-white shadow ring-1 ring-black/5 transition-transform"
18
+ :class="modelValue ? 'translate-x-[22px]' : 'translate-x-0.5'"
19
+ aria-hidden="true"
20
+ />
21
+ </button>
22
+ <span v-if="label" class="text-sm font-medium text-slate-700 dark:text-slate-200">{{ label }}</span>
23
+ </label>
24
+ </template>
25
+
26
+ <script setup lang="ts">
27
+ /**
28
+ * On/off switch (role="switch"). `v-model` is the boolean; `change` fires
29
+ * after every user toggle with the new value, so callers can persist it.
30
+ * `color` tints the on state; `offTone="red"` shows a red track while off
31
+ * (e.g. "account is down" states).
32
+ */
33
+ type Color = 'emerald' | 'red' | 'sky' | 'amber'
34
+
35
+ interface Props {
36
+ modelValue?: boolean
37
+ /** Visible label beside the switch (also its accessible name). */
38
+ label?: string
39
+ /** Accessible name when there is no visible label. */
40
+ ariaLabel?: string
41
+ color?: Color
42
+ offTone?: 'slate' | 'red'
43
+ disabled?: boolean
44
+ }
45
+
46
+ const props = withDefaults(defineProps<Props>(), {
47
+ modelValue: false,
48
+ label: '',
49
+ ariaLabel: '',
50
+ color: 'emerald',
51
+ offTone: 'slate',
52
+ disabled: false,
53
+ })
54
+
55
+ const emit = defineEmits<{
56
+ (e: 'update:modelValue', value: boolean): void
57
+ (e: 'change', value: boolean): void
58
+ }>()
59
+
60
+ const ON: Record<Color, string> = {
61
+ emerald: 'bg-emerald-500',
62
+ red: 'bg-red-600',
63
+ sky: 'bg-sky-500',
64
+ amber: 'bg-amber-500',
65
+ }
66
+
67
+ function toggle() {
68
+ if (props.disabled) return
69
+ const next = !props.modelValue
70
+ emit('update:modelValue', next)
71
+ emit('change', next)
72
+ }
73
+ </script>
@@ -1,85 +1,93 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { mount } from '@vue/test-utils'
3
- import Pagination from './Pagination.vue'
4
-
5
- /** Visible cell labels in DOM order ('…' for the ellipsis span). */
6
- function cellLabels(wrapper: ReturnType<typeof mount>): string[] {
7
- return wrapper.findAll('li').map(li => li.text().trim())
8
- }
9
-
10
- async function clickPage(wrapper: ReturnType<typeof mount>, page: number) {
11
- const link = wrapper.findAll('button').find(b => b.text().trim() === String(page))
12
- expect(link, `expected a visible cell for page ${page}`).toBeTruthy()
13
- await link!.trigger('click')
14
- }
15
-
16
- describe('Pagination', () => {
17
- it('renders every page without an ellipsis when totalPages <= 5', () => {
18
- const wrapper = mount(Pagination, {
19
- props: { totalItems: 100, itemsPerPage: 20 },
20
- })
21
- expect(cellLabels(wrapper)).toEqual(['1', '2', '3', '4', '5'])
22
- })
23
-
24
- it('renders a window with a trailing ellipsis on the first page', () => {
25
- const wrapper = mount(Pagination, {
26
- props: { totalItems: 200, itemsPerPage: 20 },
27
- })
28
- expect(cellLabels(wrapper)).toEqual(['1', '2', '3', '...', '10'])
29
- })
30
-
31
- it('keeps cells in ascending order around a middle page', async () => {
32
- const wrapper = mount(Pagination, {
33
- props: { totalItems: 200, itemsPerPage: 20 },
34
- })
35
- await clickPage(wrapper, 3)
36
- expect(cellLabels(wrapper)).toEqual(['1', '2', '3', '4', '5', '...', '10'])
37
-
38
- await clickPage(wrapper, 5)
39
- expect(cellLabels(wrapper)).toEqual(['1', '...', '3', '4', '5', '6', '7', '...', '10'])
40
-
41
- expect(wrapper.emitted('page-changed')).toEqual([[3], [5]])
42
- })
43
-
44
- it('omits the ellipsis when no pages are hidden next to the edges', async () => {
45
- const wrapper = mount(Pagination, {
46
- props: { totalItems: 200, itemsPerPage: 20 },
47
- })
48
- await clickPage(wrapper, 3)
49
- await clickPage(wrapper, 4)
50
- // window is 2..6, so only page 1 precedes it — no gap, no ellipsis
51
- expect(cellLabels(wrapper)).toEqual(['1', '2', '3', '4', '5', '6', '...', '10'])
52
- })
53
-
54
- it('renders the trailing window on the last page', async () => {
55
- const wrapper = mount(Pagination, {
56
- props: { totalItems: 200, itemsPerPage: 20 },
57
- })
58
- await clickPage(wrapper, 3)
59
- await clickPage(wrapper, 5)
60
- await clickPage(wrapper, 7)
61
- await clickPage(wrapper, 9)
62
- await clickPage(wrapper, 10)
63
- expect(cellLabels(wrapper)).toEqual(['1', '...', '8', '9', '10'])
64
- })
65
-
66
- it('does not emit when the active page is clicked again', async () => {
67
- const wrapper = mount(Pagination, {
68
- props: { totalItems: 200, itemsPerPage: 20 },
69
- })
70
- await clickPage(wrapper, 1)
71
- expect(wrapper.emitted('page-changed')).toBeUndefined()
72
- })
73
-
74
- it('clamps the current page when the item count shrinks', async () => {
75
- const wrapper = mount(Pagination, {
76
- props: { totalItems: 200, itemsPerPage: 20 },
77
- })
78
- await clickPage(wrapper, 3)
79
- await clickPage(wrapper, 5)
80
- await clickPage(wrapper, 7)
81
- await wrapper.setProps({ totalItems: 40 })
82
- expect(cellLabels(wrapper)).toEqual(['1', '2'])
83
- expect(wrapper.find('button.active').text()).toBe('2')
84
- })
85
- })
1
+ import { describe, it, expect } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import Pagination from './Pagination.vue'
4
+
5
+ /** Visible cell labels in DOM order ('…' for the ellipsis span). */
6
+ function cellLabels(wrapper: ReturnType<typeof mount>): string[] {
7
+ return wrapper.findAll('li').map(li => li.text().trim())
8
+ }
9
+
10
+ async function clickPage(wrapper: ReturnType<typeof mount>, page: number) {
11
+ const link = wrapper.findAll('button').find(b => b.text().trim() === String(page))
12
+ expect(link, `expected a visible cell for page ${page}`).toBeTruthy()
13
+ await link!.trigger('click')
14
+ }
15
+
16
+ describe('Pagination', () => {
17
+ it('renders every page without an ellipsis when totalPages <= 5', () => {
18
+ const wrapper = mount(Pagination, {
19
+ props: { totalItems: 100, itemsPerPage: 20 },
20
+ })
21
+ expect(cellLabels(wrapper)).toEqual(['1', '2', '3', '4', '5'])
22
+ })
23
+
24
+ it('renders a window with a trailing ellipsis on the first page', () => {
25
+ const wrapper = mount(Pagination, {
26
+ props: { totalItems: 200, itemsPerPage: 20 },
27
+ })
28
+ expect(cellLabels(wrapper)).toEqual(['1', '2', '3', '...', '10'])
29
+ })
30
+
31
+ it('keeps cells in ascending order around a middle page', async () => {
32
+ const wrapper = mount(Pagination, {
33
+ props: { totalItems: 200, itemsPerPage: 20 },
34
+ })
35
+ await clickPage(wrapper, 3)
36
+ expect(cellLabels(wrapper)).toEqual(['1', '2', '3', '4', '5', '...', '10'])
37
+
38
+ await clickPage(wrapper, 5)
39
+ expect(cellLabels(wrapper)).toEqual(['1', '...', '3', '4', '5', '6', '7', '...', '10'])
40
+
41
+ expect(wrapper.emitted('page-changed')).toEqual([[3], [5]])
42
+ })
43
+
44
+ it('omits the ellipsis when no pages are hidden next to the edges', async () => {
45
+ const wrapper = mount(Pagination, {
46
+ props: { totalItems: 200, itemsPerPage: 20 },
47
+ })
48
+ await clickPage(wrapper, 3)
49
+ await clickPage(wrapper, 4)
50
+ // window is 2..6, so only page 1 precedes it — no gap, no ellipsis
51
+ expect(cellLabels(wrapper)).toEqual(['1', '2', '3', '4', '5', '6', '...', '10'])
52
+ })
53
+
54
+ it('renders the trailing window on the last page', async () => {
55
+ const wrapper = mount(Pagination, {
56
+ props: { totalItems: 200, itemsPerPage: 20 },
57
+ })
58
+ await clickPage(wrapper, 3)
59
+ await clickPage(wrapper, 5)
60
+ await clickPage(wrapper, 7)
61
+ await clickPage(wrapper, 9)
62
+ await clickPage(wrapper, 10)
63
+ expect(cellLabels(wrapper)).toEqual(['1', '...', '8', '9', '10'])
64
+ })
65
+
66
+ it('does not emit when the active page is clicked again', async () => {
67
+ const wrapper = mount(Pagination, {
68
+ props: { totalItems: 200, itemsPerPage: 20 },
69
+ })
70
+ await clickPage(wrapper, 1)
71
+ expect(wrapper.emitted('page-changed')).toBeUndefined()
72
+ })
73
+
74
+ it('clamps the current page when the item count shrinks', async () => {
75
+ const wrapper = mount(Pagination, {
76
+ props: { totalItems: 200, itemsPerPage: 20 },
77
+ })
78
+ await clickPage(wrapper, 3)
79
+ await clickPage(wrapper, 5)
80
+ await clickPage(wrapper, 7)
81
+ await wrapper.setProps({ totalItems: 40 })
82
+ expect(cellLabels(wrapper)).toEqual(['1', '2'])
83
+ expect(wrapper.find('button.active').text()).toBe('2')
84
+ })
85
+
86
+ it('falls back to 20 per page when itemsPerPage is null or 0 (no "Infinity")', () => {
87
+ for (const itemsPerPage of [null, 0]) {
88
+ const wrapper = mount(Pagination, { props: { totalItems: 16, itemsPerPage: itemsPerPage as unknown as number } })
89
+ expect(wrapper.text()).not.toContain('Infinity')
90
+ expect(wrapper.findAll('button').map((b) => b.text()).filter((t) => /^\d+$/.test(t))).toEqual(['1'])
91
+ }
92
+ })
93
+ })
@@ -1,82 +1,87 @@
1
- <template>
2
- <nav aria-label="Pagination">
3
- <ul class="flex list-none p-0 m-0">
4
- <li v-for="(cell, index) in cells" :key="index" class="mr-1">
5
- <button
6
- v-if="cell !== 0"
7
- type="button"
8
- :aria-current="cell === currentPage ? 'page' : undefined"
9
- :aria-label="`Go to page ${cell}`"
10
- @click="changePage(cell)"
11
- class="inline-block min-w-[44px] border border-gray-300 dark:border-gray-600 rounded-lg p-3 text-black dark:text-gray-200 cursor-pointer transition-colors hover:bg-gray-300 dark:hover:bg-gray-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500"
12
- :class="{ 'active font-bold bg-gray-200 dark:bg-gray-700': cell === currentPage }"
13
- >{{ cell }}</button>
14
- <span v-else aria-hidden="true" class="inline-block border border-gray-300 dark:border-gray-600 rounded-lg p-3 text-black dark:text-gray-200 cursor-not-allowed">...</span>
15
- </li>
16
- </ul>
17
- </nav>
18
- </template>
19
-
20
- <script lang="ts" setup>
21
- import { ref, watch } from "vue";
22
-
23
- const props = defineProps({
24
- totalItems: {
25
- type: Number,
26
- required: true
27
- },
28
- itemsPerPage: {
29
- type: Number,
30
- default: 20
31
- }
32
- })
33
-
34
- const currentPage = ref(1);
35
- const totalPages = ref(0);
36
- const cells = ref([] as number[]);
37
- const emit = defineEmits(['page-changed']);
38
-
39
- // Rebuild the visible cells around `page`. A cell value of 0 renders as
40
- // an ellipsis. The window is the current page ± 2 neighbours, plus the
41
- // first/last page with an ellipsis only when pages are actually hidden.
42
- function buildCells(page: number) {
43
- if (totalPages.value <= 5) {
44
- cells.value = Array.from({ length: totalPages.value }, (_, i) => i + 1);
45
- return;
46
- }
47
-
48
- const out: number[] = [];
49
- const start = Math.max(1, page - 2);
50
- const end = Math.min(totalPages.value, page + 2);
51
-
52
- if (start > 1) {
53
- out.push(1);
54
- if (start > 2) out.push(0);
55
- }
56
- for (let i = start; i <= end; i++) {
57
- out.push(i);
58
- }
59
- if (end < totalPages.value) {
60
- if (end < totalPages.value - 1) out.push(0);
61
- out.push(totalPages.value);
62
- }
63
- cells.value = out;
64
- }
65
-
66
- function changePage(pageNumber: number) {
67
- if (pageNumber === currentPage.value || pageNumber < 1 || pageNumber > totalPages.value) {
68
- return;
69
- }
70
- currentPage.value = pageNumber;
71
- buildCells(pageNumber);
72
- emit('page-changed', pageNumber);
73
- }
74
-
75
- watch(() => [props.totalItems, props.itemsPerPage], () => {
76
- totalPages.value = Math.ceil(props.totalItems / props.itemsPerPage);
77
- if (currentPage.value > totalPages.value) {
78
- currentPage.value = Math.max(1, totalPages.value);
79
- }
80
- buildCells(currentPage.value);
81
- }, { immediate: true })
82
- </script>
1
+ <template>
2
+ <nav aria-label="Pagination">
3
+ <ul class="flex list-none p-0 m-0">
4
+ <li v-for="(cell, index) in cells" :key="index" class="mr-1">
5
+ <button
6
+ v-if="cell !== 0"
7
+ type="button"
8
+ :aria-current="cell === currentPage ? 'page' : undefined"
9
+ :aria-label="`Go to page ${cell}`"
10
+ @click="changePage(cell)"
11
+ class="inline-block min-w-[44px] border border-gray-300 dark:border-gray-600 rounded-lg p-3 text-black dark:text-gray-200 cursor-pointer transition-colors hover:bg-gray-300 dark:hover:bg-gray-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500"
12
+ :class="{ 'active font-bold bg-gray-200 dark:bg-gray-700': cell === currentPage }"
13
+ >{{ cell }}</button>
14
+ <span v-else aria-hidden="true" class="inline-block border border-gray-300 dark:border-gray-600 rounded-lg p-3 text-black dark:text-gray-200 cursor-not-allowed">...</span>
15
+ </li>
16
+ </ul>
17
+ </nav>
18
+ </template>
19
+
20
+ <script lang="ts" setup>
21
+ import { ref, watch } from "vue";
22
+
23
+ const props = defineProps({
24
+ totalItems: {
25
+ type: Number,
26
+ required: true
27
+ },
28
+ itemsPerPage: {
29
+ type: Number,
30
+ default: 20
31
+ }
32
+ })
33
+
34
+ const currentPage = ref(1);
35
+ const totalPages = ref(0);
36
+ const cells = ref([] as number[]);
37
+ const emit = defineEmits(['page-changed']);
38
+
39
+ // Rebuild the visible cells around `page`. A cell value of 0 renders as
40
+ // an ellipsis. The window is the current page ± 2 neighbours, plus the
41
+ // first/last page with an ellipsis only when pages are actually hidden.
42
+ function buildCells(page: number) {
43
+ if (totalPages.value <= 5) {
44
+ cells.value = Array.from({ length: totalPages.value }, (_, i) => i + 1);
45
+ return;
46
+ }
47
+
48
+ const out: number[] = [];
49
+ const start = Math.max(1, page - 2);
50
+ const end = Math.min(totalPages.value, page + 2);
51
+
52
+ if (start > 1) {
53
+ out.push(1);
54
+ if (start > 2) out.push(0);
55
+ }
56
+ for (let i = start; i <= end; i++) {
57
+ out.push(i);
58
+ }
59
+ if (end < totalPages.value) {
60
+ if (end < totalPages.value - 1) out.push(0);
61
+ out.push(totalPages.value);
62
+ }
63
+ cells.value = out;
64
+ }
65
+
66
+ function changePage(pageNumber: number) {
67
+ if (pageNumber === currentPage.value || pageNumber < 1 || pageNumber > totalPages.value) {
68
+ return;
69
+ }
70
+ currentPage.value = pageNumber;
71
+ buildCells(pageNumber);
72
+ emit('page-changed', pageNumber);
73
+ }
74
+
75
+ watch(() => [props.totalItems, props.itemsPerPage], () => {
76
+ // Guard against a missing/zero page size or total (e.g. a reset form that
77
+ // nulled them): fall back to 20 per page / 0 items instead of rendering
78
+ // "Infinity" or "NaN" pages.
79
+ const perPage = Number(props.itemsPerPage) > 0 ? Number(props.itemsPerPage) : 20;
80
+ const total = Number(props.totalItems) > 0 ? Number(props.totalItems) : 0;
81
+ totalPages.value = Math.ceil(total / perPage);
82
+ if (currentPage.value > totalPages.value) {
83
+ currentPage.value = Math.max(1, totalPages.value);
84
+ }
85
+ buildCells(currentPage.value);
86
+ }, { immediate: true })
87
+ </script>
package/src/index.ts CHANGED
@@ -60,6 +60,13 @@ export { default as BaseSearchSelect } from './components/BaseSearchSelect.vue'
60
60
  export { default as BaseCard } from './components/BaseCard.vue'
61
61
  export { default as BaseField } from './components/BaseField.vue'
62
62
  export { default as BaseStatCard } from './components/BaseStatCard.vue'
63
+ export { default as BaseToggle } from './components/BaseToggle.vue'
64
+ export { default as BaseCheckbox } from './components/BaseCheckbox.vue'
65
+ export { default as BaseSidebarUser } from './components/BaseSidebarUser.vue'
66
+ export { default as BaseAuthLayout } from './components/BaseAuthLayout.vue'
67
+ export { default as BaseBooleanBadge } from './components/BaseBooleanBadge.vue'
68
+ export { default as BaseDetailList } from './components/BaseDetailList.vue'
69
+ export type { DetailItem } from './types/detail'
63
70
 
64
71
  // Composables
65
72
  export { useTheme, initTheme } from './composables/useTheme'
package/src/style.css CHANGED
@@ -30,61 +30,61 @@
30
30
  body {
31
31
  font-family: var(--font-sans);
32
32
  }
33
-
34
- /* BaseDateTimePicker skin: re-point @vuepic/vue-datepicker's CSS variables at
35
- the lib palette (emerald accent, slate surfaces — same as BaseInput). The
36
- picker is third-party markup, so its variables are the only styling hook;
37
- Tailwind utilities can't reach inside it. `html` prefix out-specifies the
38
- package's own `.dp--theme-*` blocks regardless of stylesheet order. */
39
- html .dp--theme-light,
40
- html .dp--theme-dark {
41
- --dp-font-family: var(--font-sans);
42
- --dp-font-size: 0.875rem;
43
- --dp-border-radius: 0.25rem;
44
- --dp-cell-border-radius: 0.25rem;
45
- --dp-input-padding: 8px 30px 8px 12px;
46
- --dp-primary-text-color: #fff;
47
- }
48
- html .dp--theme-light {
49
- --dp-background-color: #f8fafc; /* slate-50 */
50
- --dp-text-color: #0f172a; /* slate-900 */
51
- --dp-hover-color: #f1f5f9; /* slate-100 */
52
- --dp-hover-text-color: #0f172a;
53
- --dp-hover-icon-color: #64748b; /* slate-500 */
54
- --dp-icon-color: #94a3b8; /* slate-400 */
55
- --dp-primary-color: #059669; /* emerald-600 */
56
- --dp-primary-disabled-color: #6ee7b7; /* emerald-300 */
57
- --dp-secondary-color: #94a3b8;
58
- --dp-border-color: #cbd5e1; /* slate-300 */
59
- --dp-menu-border-color: #cbd5e1;
60
- --dp-border-color-hover: #94a3b8;
61
- --dp-border-color-focus: #10b981; /* emerald-500 */
62
- --dp-highlight-color: rgb(16 185 129 / 0.12);
63
- --dp-success-color: #10b981;
64
- --dp-danger-color: #ef4444; /* red-500 */
65
- --dp-marker-color: #ef4444;
66
- }
67
- html .dp--theme-dark {
68
- --dp-background-color: #0f172a; /* slate-900 */
69
- --dp-text-color: #f1f5f9; /* slate-100 */
70
- --dp-hover-color: #1e293b; /* slate-800 */
71
- --dp-hover-text-color: #f1f5f9;
72
- --dp-hover-icon-color: #cbd5e1; /* slate-300 */
73
- --dp-icon-color: #64748b; /* slate-500 */
74
- --dp-primary-color: #059669; /* emerald-600 */
75
- --dp-primary-disabled-color: #065f46; /* emerald-800 */
76
- --dp-secondary-color: #64748b;
77
- --dp-border-color: #475569; /* slate-600 */
78
- --dp-menu-border-color: #334155; /* slate-700 */
79
- --dp-border-color-hover: #64748b;
80
- --dp-border-color-focus: #10b981; /* emerald-500 */
81
- --dp-highlight-color: rgb(16 185 129 / 0.2);
82
- --dp-disabled-color: #1e293b;
83
- --dp-disabled-color-text: #64748b;
84
- --dp-scroll-bar-background: #0f172a;
85
- --dp-scroll-bar-color: #334155;
86
- --dp-success-color: #10b981;
87
- --dp-danger-color: #ef4444;
88
- --dp-marker-color: #ef4444;
89
- --dp-tooltip-color: #1e293b;
90
- }
33
+
34
+ /* BaseDateTimePicker skin: re-point @vuepic/vue-datepicker's CSS variables at
35
+ the lib palette (emerald accent, slate surfaces — same as BaseInput). The
36
+ picker is third-party markup, so its variables are the only styling hook;
37
+ Tailwind utilities can't reach inside it. `html` prefix out-specifies the
38
+ package's own `.dp--theme-*` blocks regardless of stylesheet order. */
39
+ html .dp--theme-light,
40
+ html .dp--theme-dark {
41
+ --dp-font-family: var(--font-sans);
42
+ --dp-font-size: 0.875rem;
43
+ --dp-border-radius: 0.5rem; /* rounded-lg, like BaseInput */
44
+ --dp-cell-border-radius: 0.25rem;
45
+ --dp-input-padding: 8px 30px 8px 12px;
46
+ --dp-primary-text-color: #fff;
47
+ }
48
+ html .dp--theme-light {
49
+ --dp-background-color: #fff; /* white, like BaseInput */
50
+ --dp-text-color: #0f172a; /* slate-900 */
51
+ --dp-hover-color: #f1f5f9; /* slate-100 */
52
+ --dp-hover-text-color: #0f172a;
53
+ --dp-hover-icon-color: #64748b; /* slate-500 */
54
+ --dp-icon-color: #94a3b8; /* slate-400 */
55
+ --dp-primary-color: #059669; /* emerald-600 */
56
+ --dp-primary-disabled-color: #6ee7b7; /* emerald-300 */
57
+ --dp-secondary-color: #94a3b8;
58
+ --dp-border-color: #cbd5e1; /* slate-300 */
59
+ --dp-menu-border-color: #cbd5e1;
60
+ --dp-border-color-hover: #94a3b8;
61
+ --dp-border-color-focus: #10b981; /* emerald-500 */
62
+ --dp-highlight-color: rgb(16 185 129 / 0.12);
63
+ --dp-success-color: #10b981;
64
+ --dp-danger-color: #ef4444; /* red-500 */
65
+ --dp-marker-color: #ef4444;
66
+ }
67
+ html .dp--theme-dark {
68
+ --dp-background-color: #0f172a; /* slate-900 */
69
+ --dp-text-color: #f1f5f9; /* slate-100 */
70
+ --dp-hover-color: #1e293b; /* slate-800 */
71
+ --dp-hover-text-color: #f1f5f9;
72
+ --dp-hover-icon-color: #cbd5e1; /* slate-300 */
73
+ --dp-icon-color: #64748b; /* slate-500 */
74
+ --dp-primary-color: #059669; /* emerald-600 */
75
+ --dp-primary-disabled-color: #065f46; /* emerald-800 */
76
+ --dp-secondary-color: #64748b;
77
+ --dp-border-color: #475569; /* slate-600 */
78
+ --dp-menu-border-color: #334155; /* slate-700 */
79
+ --dp-border-color-hover: #64748b;
80
+ --dp-border-color-focus: #10b981; /* emerald-500 */
81
+ --dp-highlight-color: rgb(16 185 129 / 0.2);
82
+ --dp-disabled-color: #1e293b;
83
+ --dp-disabled-color-text: #64748b;
84
+ --dp-scroll-bar-background: #0f172a;
85
+ --dp-scroll-bar-color: #334155;
86
+ --dp-success-color: #10b981;
87
+ --dp-danger-color: #ef4444;
88
+ --dp-marker-color: #ef4444;
89
+ --dp-tooltip-color: #1e293b;
90
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * One row of a BaseDetailList. `href` renders the value as an external link;
3
+ * `mono` sets it in the monospace face.
4
+ */
5
+ export interface DetailItem {
6
+ label: string
7
+ value?: string | number | boolean | null
8
+ href?: string
9
+ mono?: boolean
10
+ }