mgv-backoffice 1.46.0 → 1.48.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 +2082 -2048
- package/dist/components/BaseIdChip.vue.d.ts +24 -0
- package/dist/components/BasePageHeader.vue.d.ts +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/ui-lib.css +1 -1
- package/dist/ui-lib.js +350 -289
- package/dist/ui-lib.umd.cjs +1 -1
- package/dist/utils/format.d.ts +15 -0
- package/package.json +1 -1
- package/src/components/BaseIdChip.test.ts +26 -0
- package/src/components/BaseIdChip.vue +51 -0
- package/src/components/BasePageHeader.test.ts +29 -0
- package/src/components/BasePageHeader.vue +137 -131
- package/src/index.ts +3 -0
- package/src/style.css +132 -36
- package/src/utils/format.test.ts +322 -290
- package/src/utils/format.ts +42 -0
package/dist/utils/format.d.ts
CHANGED
|
@@ -53,6 +53,21 @@ export declare function fmtUsd(v: number): string;
|
|
|
53
53
|
* strings so BigDecimal-as-string payloads pass through.
|
|
54
54
|
*/
|
|
55
55
|
export declare function fmtMoney(n: number | string | null | undefined, digits?: number, currency?: string): string;
|
|
56
|
+
/**
|
|
57
|
+
* Display symbol for an ISO / ticker currency code: "€" for EUR, "£" for
|
|
58
|
+
* GBP, "$" for USD and the USD stablecoins — and for a missing code, so
|
|
59
|
+
* rows with no currency keep reading as dollars. Any other code (BTC,
|
|
60
|
+
* ETH, …) has no symbol and returns null; callers suffix the code instead.
|
|
61
|
+
*/
|
|
62
|
+
export declare function currencySymbol(code: string | null | undefined): string | null;
|
|
63
|
+
/**
|
|
64
|
+
* Format an amount in its own currency: fiat with a symbol goes through
|
|
65
|
+
* fmtMoney ("€20.00", "-$1.50"), a USD stablecoin keeps its ticker after
|
|
66
|
+
* the dollar figure ("$25.00 USDT") so it isn't mistaken for cash, and any
|
|
67
|
+
* other asset prints as a plain quantity with its code ("0.5 BTC", up to 8
|
|
68
|
+
* decimals). Em-dash for missing / non-finite amounts, like fmtMoney.
|
|
69
|
+
*/
|
|
70
|
+
export declare function fmtCurrencyAmount(n: number | string | null | undefined, code: string | null | undefined, digits?: number): string;
|
|
56
71
|
/**
|
|
57
72
|
* Format an ISO date string / epoch as a "Mon D, YYYY" calendar label
|
|
58
73
|
* (en-US, e.g. "Jan 5, 2026"). Falls back to the raw value on parse
|
package/package.json
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import BaseIdChip from './BaseIdChip.vue'
|
|
4
|
+
|
|
5
|
+
describe('BaseIdChip', () => {
|
|
6
|
+
it('shows the label and the full id in the tooltip', () => {
|
|
7
|
+
const w = mount(BaseIdChip, { props: { label: 'ord', value: 'abc-123-def' } })
|
|
8
|
+
expect(w.text()).toContain('ord')
|
|
9
|
+
expect(w.get('code').text()).toBe('abc-123-def')
|
|
10
|
+
expect(w.get('code').attributes('title')).toBe('abc-123-def')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('labels the copy button from label + value, or copyLabel when given', () => {
|
|
14
|
+
const plain = mount(BaseIdChip, { props: { label: 'id', value: 'x1' } })
|
|
15
|
+
expect(plain.get('button[aria-label="Copy id x1"]').exists()).toBe(true)
|
|
16
|
+
const custom = mount(BaseIdChip, { props: { label: 'id', value: 'x1', copyLabel: 'tx id' } })
|
|
17
|
+
expect(custom.get('button[aria-label="Copy tx id"]').exists()).toBe(true)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('renders the action button only with actionLabel, emitting the value', async () => {
|
|
21
|
+
expect(mount(BaseIdChip, { props: { value: 'x1' } }).findAll('button')).toHaveLength(1)
|
|
22
|
+
const w = mount(BaseIdChip, { props: { value: 'x1', actionLabel: 'Search this id' } })
|
|
23
|
+
await w.get('button[aria-label="Search this id"]').trigger('click')
|
|
24
|
+
expect(w.emitted('action')).toEqual([['x1']])
|
|
25
|
+
})
|
|
26
|
+
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!--
|
|
3
|
+
Compact id cell: a small uppercase label, the id truncated in a <code>
|
|
4
|
+
pill (full value in the tooltip), a copy button, and — when
|
|
5
|
+
`actionLabel` is set — a magnifier button that emits `action` (e.g.
|
|
6
|
+
"search the log for this id"). Built for dense rows that carry several
|
|
7
|
+
ids side by side (transaction / buy-order / sell-order).
|
|
8
|
+
-->
|
|
9
|
+
<div class="inline-flex items-center gap-1 font-mono text-[11px]">
|
|
10
|
+
<span v-if="label" class="text-slate-500 dark:text-slate-400 uppercase tracking-wide">{{ label }}</span>
|
|
11
|
+
<code
|
|
12
|
+
class="truncate rounded bg-slate-100 px-1 text-slate-600 dark:bg-slate-800 dark:text-slate-300"
|
|
13
|
+
:class="maxWidthClass"
|
|
14
|
+
:title="value"
|
|
15
|
+
>{{ value }}</code>
|
|
16
|
+
<BaseCopyButton :text="value" :label="copyLabel ?? `${label} ${value}`.trim()" icon-class="w-3.5 h-3.5" />
|
|
17
|
+
<button
|
|
18
|
+
v-if="actionLabel"
|
|
19
|
+
type="button"
|
|
20
|
+
:title="actionLabel"
|
|
21
|
+
:aria-label="actionLabel"
|
|
22
|
+
class="rounded px-0.5 text-slate-400 hover:bg-slate-100 hover:text-emerald-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 dark:hover:bg-slate-800 dark:hover:text-emerald-400"
|
|
23
|
+
@click="emit('action', value)"
|
|
24
|
+
>
|
|
25
|
+
<MagnifyingGlassIcon class="h-3.5 w-3.5" aria-hidden="true" />
|
|
26
|
+
</button>
|
|
27
|
+
</div>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script setup lang="ts">
|
|
31
|
+
import { MagnifyingGlassIcon } from '@heroicons/vue/24/outline'
|
|
32
|
+
import BaseCopyButton from './BaseCopyButton.vue'
|
|
33
|
+
|
|
34
|
+
withDefaults(
|
|
35
|
+
defineProps<{
|
|
36
|
+
/** The id itself — shown truncated, copied in full. */
|
|
37
|
+
value: string
|
|
38
|
+
/** Short uppercase prefix ("id", "ord", "sell"). Omit for no prefix. */
|
|
39
|
+
label?: string
|
|
40
|
+
/** Copy button's tooltip / aria noun; defaults to "<label> <value>". */
|
|
41
|
+
copyLabel?: string
|
|
42
|
+
/** Tooltip / aria-label of the action button; the button only renders when set. */
|
|
43
|
+
actionLabel?: string
|
|
44
|
+
/** Width cap of the truncated id. */
|
|
45
|
+
maxWidthClass?: string
|
|
46
|
+
}>(),
|
|
47
|
+
{ label: '', copyLabel: undefined, actionLabel: undefined, maxWidthClass: 'max-w-[90px]' },
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
const emit = defineEmits<{ action: [value: string] }>()
|
|
51
|
+
</script>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import BasePageHeader from './BasePageHeader.vue'
|
|
4
|
+
|
|
5
|
+
const hasGauge = (wrapper: ReturnType<typeof mount>) =>
|
|
6
|
+
wrapper.find('h1 span[aria-hidden="true"]').exists()
|
|
7
|
+
|
|
8
|
+
describe('BasePageHeader accent', () => {
|
|
9
|
+
it('shows the gauge bar by default on icon-less headers', () => {
|
|
10
|
+
expect(hasGauge(mount(BasePageHeader, { props: { title: 'Dashboard' } }))).toBe(true)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('hides the gauge bar when an icon badge is present', () => {
|
|
14
|
+
const wrapper = mount(BasePageHeader, {
|
|
15
|
+
props: { title: 'Dashboard' },
|
|
16
|
+
slots: { icon: '<svg />' },
|
|
17
|
+
})
|
|
18
|
+
expect(hasGauge(wrapper)).toBe(false)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('honours an explicit opt-out and opt-in', () => {
|
|
22
|
+
expect(hasGauge(mount(BasePageHeader, { props: { title: 'A', accent: false } }))).toBe(false)
|
|
23
|
+
const withIcon = mount(BasePageHeader, {
|
|
24
|
+
props: { title: 'A', accent: true },
|
|
25
|
+
slots: { icon: '<svg />' },
|
|
26
|
+
})
|
|
27
|
+
expect(hasGauge(withIcon)).toBe(true)
|
|
28
|
+
})
|
|
29
|
+
})
|
|
@@ -1,131 +1,137 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<!--
|
|
3
|
-
Page-level header. Standardises the "icon badge + title/subtitle on
|
|
4
|
-
the left, action buttons on the right" pattern for top-level views.
|
|
5
|
-
|
|
6
|
-
Slots:
|
|
7
|
-
• icon — the page-specific Heroicon. Receives an `iconClass`
|
|
8
|
-
slot prop carrying the theme-aware text colour, so callers can
|
|
9
|
-
bind `:class="iconClass"` on their icon component. The badge
|
|
10
|
-
square only renders when this slot is filled, so icon-less apps
|
|
11
|
-
get a plain title/subtitle header.
|
|
12
|
-
• subtitle — rich subtitle content (links, emphasis); overrides
|
|
13
|
-
the `subtitle` prop.
|
|
14
|
-
• actions — refresh / destructive buttons rendered on the right.
|
|
15
|
-
|
|
16
|
-
Props:
|
|
17
|
-
• title — the H1 text.
|
|
18
|
-
• subtitle — optional muted line below the title.
|
|
19
|
-
• iconColor — badge background + icon text colour
|
|
20
|
-
('emerald' | 'sky' | 'red' | 'amber'). Defaults to 'emerald'.
|
|
21
|
-
• maxWidthClass — Tailwind max-w utility constraining the header
|
|
22
|
-
width so sibling views can share a footprint. Defaults to
|
|
23
|
-
'max-w-4xl'; pass '' to skip the width wrapper entirely (the
|
|
24
|
-
header then spans whatever container it sits in).
|
|
25
|
-
• align — vertical alignment of the title block vs the actions:
|
|
26
|
-
'center' (default) or 'end' (actions sit on the title baseline).
|
|
27
|
-
• marginClass — space under the header. Defaults to 'mb-8'; pass
|
|
28
|
-
'' when the parent already manages vertical rhythm (space-y-*).
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
</
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
import {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
1
|
+
<template>
|
|
2
|
+
<!--
|
|
3
|
+
Page-level header. Standardises the "icon badge + title/subtitle on
|
|
4
|
+
the left, action buttons on the right" pattern for top-level views.
|
|
5
|
+
|
|
6
|
+
Slots:
|
|
7
|
+
• icon — the page-specific Heroicon. Receives an `iconClass`
|
|
8
|
+
slot prop carrying the theme-aware text colour, so callers can
|
|
9
|
+
bind `:class="iconClass"` on their icon component. The badge
|
|
10
|
+
square only renders when this slot is filled, so icon-less apps
|
|
11
|
+
get a plain title/subtitle header.
|
|
12
|
+
• subtitle — rich subtitle content (links, emphasis); overrides
|
|
13
|
+
the `subtitle` prop.
|
|
14
|
+
• actions — refresh / destructive buttons rendered on the right.
|
|
15
|
+
|
|
16
|
+
Props:
|
|
17
|
+
• title — the H1 text.
|
|
18
|
+
• subtitle — optional muted line below the title.
|
|
19
|
+
• iconColor — badge background + icon text colour
|
|
20
|
+
('emerald' | 'sky' | 'red' | 'amber'). Defaults to 'emerald'.
|
|
21
|
+
• maxWidthClass — Tailwind max-w utility constraining the header
|
|
22
|
+
width so sibling views can share a footprint. Defaults to
|
|
23
|
+
'max-w-4xl'; pass '' to skip the width wrapper entirely (the
|
|
24
|
+
header then spans whatever container it sits in).
|
|
25
|
+
• align — vertical alignment of the title block vs the actions:
|
|
26
|
+
'center' (default) or 'end' (actions sit on the title baseline).
|
|
27
|
+
• marginClass — space under the header. Defaults to 'mb-8'; pass
|
|
28
|
+
'' when the parent already manages vertical rhythm (space-y-*).
|
|
29
|
+
• accent — brand-green gauge bar in front of the title. Defaults to
|
|
30
|
+
on for icon-less headers; pass :accent="false" to opt out.
|
|
31
|
+
-->
|
|
32
|
+
<header :class="maxWidthClass ? ['mx-auto px-4 sm:px-6', maxWidthClass] : undefined">
|
|
33
|
+
<!--
|
|
34
|
+
flex-wrap + gap so a long title and the action buttons reflow onto
|
|
35
|
+
separate rows on narrow (mobile) viewports instead of the actions
|
|
36
|
+
overflowing off the right edge of the screen. min-w-0 on the title
|
|
37
|
+
group lets a long title truncate rather than shoving the actions out.
|
|
38
|
+
-->
|
|
39
|
+
<div
|
|
40
|
+
class="flex flex-wrap justify-between gap-3"
|
|
41
|
+
:class="[align === 'end' ? 'items-end' : 'items-center', marginClass]"
|
|
42
|
+
>
|
|
43
|
+
<div class="flex items-center gap-3 min-w-0">
|
|
44
|
+
<div
|
|
45
|
+
v-if="$slots.icon"
|
|
46
|
+
class="w-10 h-10 rounded-lg flex items-center justify-center"
|
|
47
|
+
:class="badgeBgClass"
|
|
48
|
+
>
|
|
49
|
+
<slot name="icon" :icon-class="iconTextClass" />
|
|
50
|
+
</div>
|
|
51
|
+
<div>
|
|
52
|
+
<h1
|
|
53
|
+
class="text-2xl font-bold"
|
|
54
|
+
:class="[t.primaryTextSoft, showAccent ? 'relative pl-3.5 tracking-tight' : '']"
|
|
55
|
+
>
|
|
56
|
+
<span
|
|
57
|
+
v-if="showAccent"
|
|
58
|
+
class="absolute left-0 top-[0.14em] h-[1.1em] w-1 rounded-full bg-gradient-to-b from-emerald-400 to-emerald-600"
|
|
59
|
+
aria-hidden="true"
|
|
60
|
+
/>
|
|
61
|
+
{{ title }}
|
|
62
|
+
</h1>
|
|
63
|
+
<p v-if="subtitle || $slots.subtitle" class="max-w-[72ch] text-sm" :class="t.dimTextAlt">
|
|
64
|
+
<slot name="subtitle">{{ subtitle }}</slot>
|
|
65
|
+
</p>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="flex items-center gap-2">
|
|
69
|
+
<slot name="actions" />
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</header>
|
|
73
|
+
</template>
|
|
74
|
+
|
|
75
|
+
<script setup lang="ts">
|
|
76
|
+
import { computed, useSlots } from 'vue'
|
|
77
|
+
import { useTheme } from '../composables/useTheme'
|
|
78
|
+
import { useThemeClasses } from '../composables/useThemeClasses'
|
|
79
|
+
|
|
80
|
+
type IconColor = 'emerald' | 'sky' | 'red' | 'amber'
|
|
81
|
+
|
|
82
|
+
const props = withDefaults(
|
|
83
|
+
defineProps<{
|
|
84
|
+
title: string
|
|
85
|
+
subtitle?: string
|
|
86
|
+
iconColor?: IconColor
|
|
87
|
+
maxWidthClass?: string
|
|
88
|
+
align?: 'center' | 'end'
|
|
89
|
+
marginClass?: string
|
|
90
|
+
/** Brand-green gauge bar in front of the title. Defaults to on when there is no icon. */
|
|
91
|
+
accent?: boolean
|
|
92
|
+
}>(),
|
|
93
|
+
{
|
|
94
|
+
subtitle: '',
|
|
95
|
+
iconColor: 'emerald',
|
|
96
|
+
maxWidthClass: 'max-w-4xl',
|
|
97
|
+
align: 'center',
|
|
98
|
+
marginClass: 'mb-8',
|
|
99
|
+
// Explicit undefined: an absent Boolean prop would otherwise cast to false.
|
|
100
|
+
accent: undefined,
|
|
101
|
+
},
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
const { isDark } = useTheme()
|
|
105
|
+
const t = useThemeClasses()
|
|
106
|
+
const slots = useSlots()
|
|
107
|
+
|
|
108
|
+
const showAccent = computed(() => props.accent ?? !slots.icon)
|
|
109
|
+
|
|
110
|
+
// Maps the named colour palette to the badge-background classes for
|
|
111
|
+
// dark/light themes. Callers just pass `iconColor="sky"` and the
|
|
112
|
+
// component resolves both modes.
|
|
113
|
+
const badgeBgClass = computed(() => {
|
|
114
|
+
const palette: Record<IconColor, { dark: string; light: string }> = {
|
|
115
|
+
emerald: { dark: 'bg-emerald-500/10', light: 'bg-emerald-50' },
|
|
116
|
+
sky: { dark: 'bg-sky-500/10', light: 'bg-sky-50' },
|
|
117
|
+
red: { dark: 'bg-red-500/10', light: 'bg-red-50' },
|
|
118
|
+
amber: { dark: 'bg-amber-500/10', light: 'bg-amber-50' },
|
|
119
|
+
}
|
|
120
|
+
const entry = palette[props.iconColor]
|
|
121
|
+
return isDark.value ? entry.dark : entry.light
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
// Icon text colour, routed through the same name → palette mapping so
|
|
125
|
+
// callers don't need to know the exact Tailwind shade per theme. Exposed
|
|
126
|
+
// to the `icon` slot via the `iconClass` slot prop.
|
|
127
|
+
const iconTextClass = computed(() => {
|
|
128
|
+
const palette: Record<IconColor, { dark: string; light: string }> = {
|
|
129
|
+
emerald: { dark: 'text-emerald-400', light: 'text-emerald-600' },
|
|
130
|
+
sky: { dark: 'text-sky-400', light: 'text-sky-600' },
|
|
131
|
+
red: { dark: 'text-red-400', light: 'text-red-500' },
|
|
132
|
+
amber: { dark: 'text-amber-400', light: 'text-amber-600' },
|
|
133
|
+
}
|
|
134
|
+
const entry = palette[props.iconColor]
|
|
135
|
+
return isDark.value ? entry.dark : entry.light
|
|
136
|
+
})
|
|
137
|
+
</script>
|
package/src/index.ts
CHANGED
|
@@ -52,6 +52,7 @@ export { default as BaseCredentialsForm } from './components/BaseCredentialsForm
|
|
|
52
52
|
export { default as BasePillPickerModal } from './components/BasePillPickerModal.vue'
|
|
53
53
|
export { default as BaseBarDistribution } from './components/BaseBarDistribution.vue'
|
|
54
54
|
export { default as BaseGroupedChecklist } from './components/BaseGroupedChecklist.vue'
|
|
55
|
+
export { default as BaseIdChip } from './components/BaseIdChip.vue'
|
|
55
56
|
export type { CredentialsView, CredentialsUpdate } from './components/BaseCredentialsForm.vue'
|
|
56
57
|
|
|
57
58
|
// Components — third-party wrappers
|
|
@@ -144,6 +145,8 @@ export {
|
|
|
144
145
|
fmtPct,
|
|
145
146
|
fmtUsd,
|
|
146
147
|
fmtMoney,
|
|
148
|
+
currencySymbol,
|
|
149
|
+
fmtCurrencyAmount,
|
|
147
150
|
fmtDuration,
|
|
148
151
|
formatJson,
|
|
149
152
|
stringifyValue,
|
package/src/style.css
CHANGED
|
@@ -31,10 +31,106 @@
|
|
|
31
31
|
over plain base utilities like bg-slate-50 on a cascade tie. */
|
|
32
32
|
@custom-variant dark (&:is(.dark, .dark *));
|
|
33
33
|
|
|
34
|
+
/* Design system ("Night Terminal"): remapping the gray/slate/emerald ramps
|
|
35
|
+
restyles every lib component and every consumer utility, since they all
|
|
36
|
+
resolve colours via var() at runtime.
|
|
37
|
+
- gray/slate: one blue-tinted ramp (deep navy dark mode, cool paper light mode)
|
|
38
|
+
- emerald: terminal green, the brand accent
|
|
39
|
+
Deliberately a plain block, NOT `@theme`: `@theme` output lands in
|
|
40
|
+
`@layer theme`, where whichever stylesheet loads last wins, so a consumer
|
|
41
|
+
importing this file before its own `@import "tailwindcss"` would silently
|
|
42
|
+
get the default palette back. An unlayered rule beats every layer in any
|
|
43
|
+
import order. Consumers re-brand with their own plain `:root` block loaded
|
|
44
|
+
after this stylesheet. */
|
|
45
|
+
:root, :host {
|
|
46
|
+
--color-gray-50: #f6f8fc;
|
|
47
|
+
--color-gray-100: #eef1f8;
|
|
48
|
+
--color-gray-200: #dee4ef;
|
|
49
|
+
--color-gray-300: #c4cede;
|
|
50
|
+
--color-gray-400: #94a3bd;
|
|
51
|
+
--color-gray-500: #65748f;
|
|
52
|
+
--color-gray-600: #475572;
|
|
53
|
+
--color-gray-700: #33415c;
|
|
54
|
+
--color-gray-800: #1c2942;
|
|
55
|
+
--color-gray-900: #101a30;
|
|
56
|
+
--color-gray-950: #070d1d;
|
|
57
|
+
|
|
58
|
+
--color-slate-50: #f6f8fc;
|
|
59
|
+
--color-slate-100: #eef1f8;
|
|
60
|
+
--color-slate-200: #dee4ef;
|
|
61
|
+
--color-slate-300: #c4cede;
|
|
62
|
+
--color-slate-400: #94a3bd;
|
|
63
|
+
--color-slate-500: #65748f;
|
|
64
|
+
--color-slate-600: #475572;
|
|
65
|
+
--color-slate-700: #33415c;
|
|
66
|
+
--color-slate-800: #1c2942;
|
|
67
|
+
--color-slate-900: #101a30;
|
|
68
|
+
--color-slate-950: #070d1d;
|
|
69
|
+
|
|
70
|
+
--color-emerald-50: #eafbf4;
|
|
71
|
+
--color-emerald-100: #d0f7e6;
|
|
72
|
+
--color-emerald-200: #a4efd0;
|
|
73
|
+
--color-emerald-300: #6fe3b5;
|
|
74
|
+
--color-emerald-400: #3ad597;
|
|
75
|
+
--color-emerald-500: #16c784;
|
|
76
|
+
--color-emerald-600: #0e9f68;
|
|
77
|
+
--color-emerald-700: #0b7d52;
|
|
78
|
+
--color-emerald-800: #0c6343;
|
|
79
|
+
--color-emerald-900: #0a5138;
|
|
80
|
+
--color-emerald-950: #042e1f;
|
|
81
|
+
|
|
82
|
+
--radius-lg: 0.625rem;
|
|
83
|
+
--radius-xl: 0.875rem;
|
|
84
|
+
}
|
|
85
|
+
|
|
34
86
|
/* Apply the brand font app-wide. Plain body rule (not preflight-dependent) so
|
|
35
|
-
it also styles consumers whose own Tailwind preflight came earlier.
|
|
87
|
+
it also styles consumers whose own Tailwind preflight came earlier.
|
|
88
|
+
Tabular numerals line up digits in KPI cards and tables. */
|
|
36
89
|
body {
|
|
37
90
|
font-family: var(--font-sans);
|
|
91
|
+
font-variant-numeric: tabular-nums;
|
|
92
|
+
text-rendering: optimizeLegibility;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* Global polish. In `base` so component utilities (focus:outline-none,
|
|
96
|
+
cursor-*) still win. */
|
|
97
|
+
@layer base {
|
|
98
|
+
/* Keyboard-focus affordance in the accent colour (mouse clicks stay clean). */
|
|
99
|
+
:focus-visible {
|
|
100
|
+
outline: 2px solid var(--color-emerald-500);
|
|
101
|
+
outline-offset: 2px;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
::selection {
|
|
105
|
+
background: color-mix(in srgb, var(--color-emerald-500) 30%, transparent);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* Slim, theme-aware scrollbars for data-dense tables and logs. */
|
|
109
|
+
* {
|
|
110
|
+
scrollbar-width: thin;
|
|
111
|
+
scrollbar-color: var(--color-gray-300) transparent;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.dark * {
|
|
115
|
+
scrollbar-color: var(--color-gray-700) transparent;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* Tailwind v4 dropped the pointer cursor on buttons; restore it everywhere. */
|
|
119
|
+
button:not(:disabled),
|
|
120
|
+
[role="button"]:not([aria-disabled="true"]),
|
|
121
|
+
input[type="button"]:not(:disabled),
|
|
122
|
+
input[type="submit"]:not(:disabled),
|
|
123
|
+
input[type="reset"]:not(:disabled) {
|
|
124
|
+
cursor: pointer;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
button:disabled,
|
|
128
|
+
[role="button"][aria-disabled="true"],
|
|
129
|
+
input[type="button"]:disabled,
|
|
130
|
+
input[type="submit"]:disabled,
|
|
131
|
+
input[type="reset"]:disabled {
|
|
132
|
+
cursor: not-allowed;
|
|
133
|
+
}
|
|
38
134
|
}
|
|
39
135
|
|
|
40
136
|
/* BaseDateTimePicker skin: re-point @vuepic/vue-datepicker's CSS variables at
|
|
@@ -53,46 +149,46 @@ html .dp--theme-dark {
|
|
|
53
149
|
}
|
|
54
150
|
html .dp--theme-light {
|
|
55
151
|
--dp-background-color: #fff; /* white, like BaseInput */
|
|
56
|
-
--dp-text-color:
|
|
57
|
-
--dp-hover-color:
|
|
58
|
-
--dp-hover-text-color:
|
|
59
|
-
--dp-hover-icon-color:
|
|
60
|
-
--dp-icon-color:
|
|
61
|
-
--dp-primary-color:
|
|
62
|
-
--dp-primary-disabled-color:
|
|
63
|
-
--dp-secondary-color:
|
|
64
|
-
--dp-border-color:
|
|
65
|
-
--dp-menu-border-color:
|
|
66
|
-
--dp-border-color-hover:
|
|
67
|
-
--dp-border-color-focus:
|
|
68
|
-
--dp-highlight-color:
|
|
69
|
-
--dp-success-color:
|
|
152
|
+
--dp-text-color: var(--color-slate-900);
|
|
153
|
+
--dp-hover-color: var(--color-slate-100);
|
|
154
|
+
--dp-hover-text-color: var(--color-slate-900);
|
|
155
|
+
--dp-hover-icon-color: var(--color-slate-500);
|
|
156
|
+
--dp-icon-color: var(--color-slate-400);
|
|
157
|
+
--dp-primary-color: var(--color-emerald-600);
|
|
158
|
+
--dp-primary-disabled-color: var(--color-emerald-300);
|
|
159
|
+
--dp-secondary-color: var(--color-slate-400);
|
|
160
|
+
--dp-border-color: var(--color-slate-300);
|
|
161
|
+
--dp-menu-border-color: var(--color-slate-300);
|
|
162
|
+
--dp-border-color-hover: var(--color-slate-400);
|
|
163
|
+
--dp-border-color-focus: var(--color-emerald-500);
|
|
164
|
+
--dp-highlight-color: color-mix(in srgb, var(--color-emerald-500) 12%, transparent);
|
|
165
|
+
--dp-success-color: var(--color-emerald-500);
|
|
70
166
|
--dp-danger-color: #ef4444; /* red-500 */
|
|
71
167
|
--dp-marker-color: #ef4444;
|
|
72
168
|
}
|
|
73
169
|
html .dp--theme-dark {
|
|
74
|
-
--dp-background-color:
|
|
75
|
-
--dp-text-color:
|
|
76
|
-
--dp-hover-color:
|
|
77
|
-
--dp-hover-text-color:
|
|
78
|
-
--dp-hover-icon-color:
|
|
79
|
-
--dp-icon-color:
|
|
80
|
-
--dp-primary-color:
|
|
81
|
-
--dp-primary-disabled-color:
|
|
82
|
-
--dp-secondary-color:
|
|
83
|
-
--dp-border-color:
|
|
84
|
-
--dp-menu-border-color:
|
|
85
|
-
--dp-border-color-hover:
|
|
86
|
-
--dp-border-color-focus:
|
|
87
|
-
--dp-highlight-color:
|
|
88
|
-
--dp-disabled-color:
|
|
89
|
-
--dp-disabled-color-text:
|
|
90
|
-
--dp-scroll-bar-background:
|
|
91
|
-
--dp-scroll-bar-color:
|
|
92
|
-
--dp-success-color:
|
|
170
|
+
--dp-background-color: var(--color-slate-900);
|
|
171
|
+
--dp-text-color: var(--color-slate-100);
|
|
172
|
+
--dp-hover-color: var(--color-slate-800);
|
|
173
|
+
--dp-hover-text-color: var(--color-slate-100);
|
|
174
|
+
--dp-hover-icon-color: var(--color-slate-300);
|
|
175
|
+
--dp-icon-color: var(--color-slate-500);
|
|
176
|
+
--dp-primary-color: var(--color-emerald-600);
|
|
177
|
+
--dp-primary-disabled-color: var(--color-emerald-800);
|
|
178
|
+
--dp-secondary-color: var(--color-slate-500);
|
|
179
|
+
--dp-border-color: var(--color-slate-600);
|
|
180
|
+
--dp-menu-border-color: var(--color-slate-700);
|
|
181
|
+
--dp-border-color-hover: var(--color-slate-500);
|
|
182
|
+
--dp-border-color-focus: var(--color-emerald-500);
|
|
183
|
+
--dp-highlight-color: color-mix(in srgb, var(--color-emerald-500) 20%, transparent);
|
|
184
|
+
--dp-disabled-color: var(--color-slate-800);
|
|
185
|
+
--dp-disabled-color-text: var(--color-slate-500);
|
|
186
|
+
--dp-scroll-bar-background: var(--color-slate-900);
|
|
187
|
+
--dp-scroll-bar-color: var(--color-slate-700);
|
|
188
|
+
--dp-success-color: var(--color-emerald-500);
|
|
93
189
|
--dp-danger-color: #ef4444;
|
|
94
190
|
--dp-marker-color: #ef4444;
|
|
95
|
-
--dp-tooltip-color:
|
|
191
|
+
--dp-tooltip-color: var(--color-slate-800);
|
|
96
192
|
}
|
|
97
193
|
|
|
98
194
|
/* BaseSelect: rounded, themed dropdown panel. Opt-in customizable <select>
|
|
@@ -161,7 +257,7 @@ html .dp--theme-dark {
|
|
|
161
257
|
}
|
|
162
258
|
|
|
163
259
|
.dark .base-select option:checked {
|
|
164
|
-
background-color:
|
|
260
|
+
background-color: color-mix(in srgb, var(--color-emerald-500) 15%, transparent);
|
|
165
261
|
color: var(--color-emerald-300);
|
|
166
262
|
}
|
|
167
263
|
}
|