mgv-backoffice 1.36.0 → 1.36.2
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 +1794 -1719
- package/dist/components/BaseBadge.vue.d.ts +1 -11
- package/dist/components/BaseDropdown.vue.d.ts +1 -1
- package/dist/ui-lib.js +942 -925
- package/dist/ui-lib.umd.cjs +1 -1
- package/dist/utils/specForm.d.ts +1 -1
- package/package.json +54 -54
- package/src/components/BaseAlert.vue +104 -104
- package/src/components/BaseBadge.vue +12 -2
- package/src/components/BaseBarDistribution.test.ts +51 -51
- package/src/components/BaseBarDistribution.vue +73 -73
- package/src/components/BaseChipButton.vue +42 -42
- package/src/components/BaseCodeBlock.test.ts +70 -70
- package/src/components/BaseCodeBlock.vue +74 -74
- package/src/components/BaseCollapsibleSection.vue +113 -113
- package/src/components/BaseConfirmModal.vue +114 -114
- package/src/components/BaseCredentialsForm.test.ts +108 -108
- package/src/components/BaseCredentialsForm.vue +241 -241
- package/src/components/BaseDropdown.test.ts +87 -87
- package/src/components/BaseDropdown.vue +172 -172
- package/src/components/BaseEntityPickerModal.vue +330 -330
- package/src/components/BaseFileDropzone.vue +94 -94
- package/src/components/BaseFilterChip.test.ts +44 -44
- package/src/components/BaseFilterChip.vue +67 -67
- package/src/components/BaseModalShell.vue +209 -209
- package/src/components/BasePageHeader.vue +120 -120
- package/src/components/BasePillPickerModal.test.ts +95 -95
- package/src/components/BaseRemoveButton.vue +22 -22
- package/src/components/BaseSpecFields.test.ts +39 -0
- package/src/components/BaseSpecFields.vue +27 -3
- package/src/components/BaseStatBreakdown.test.ts +56 -56
- package/src/components/BaseStatBreakdown.vue +49 -49
- package/src/components/BaseStatusPill.vue +54 -54
- package/src/components/BaseTextInputModal.vue +140 -140
- package/src/components/BaseToolbarButton.test.ts +48 -48
- package/src/components/BaseToolbarButton.vue +94 -94
- package/src/composables/useFieldClasses.test.ts +55 -55
- package/src/composables/useFieldClasses.ts +56 -56
- package/src/composables/usePolling.test.ts +112 -112
- package/src/composables/usePolling.ts +102 -102
- package/src/composables/useQueryParamSync.test.ts +56 -56
- package/src/composables/useQueryParamSync.ts +42 -42
- package/src/composables/useThemeClasses.test.ts +34 -34
- package/src/composables/useThemeClasses.ts +137 -137
- package/src/index.ts +138 -138
- package/src/style.css +32 -32
- package/src/types/distribution.ts +7 -7
- package/src/types/pillPicker.ts +14 -14
- package/src/types/specField.ts +30 -30
- package/src/types/statBreakdown.ts +6 -6
- package/src/utils/format.test.ts +264 -264
- package/src/utils/format.ts +216 -216
- package/src/utils/httpColors.test.ts +100 -100
- package/src/utils/httpColors.ts +99 -99
- package/src/utils/kvRows.test.ts +47 -47
- package/src/utils/kvRows.ts +34 -34
- package/src/utils/specForm.test.ts +64 -53
- package/src/utils/specForm.ts +49 -48
- package/src/utils/validate.test.ts +82 -82
- package/src/utils/validate.ts +76 -76
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
-
|
|
3
|
-
const replace = vi.fn()
|
|
4
|
-
let query: Record<string, string> = {}
|
|
5
|
-
|
|
6
|
-
vi.mock('vue-router', () => ({
|
|
7
|
-
useRoute: () => ({ query }),
|
|
8
|
-
useRouter: () => ({ replace }),
|
|
9
|
-
}))
|
|
10
|
-
|
|
11
|
-
import { useQueryParamSync } from './useQueryParamSync'
|
|
12
|
-
|
|
13
|
-
beforeEach(() => {
|
|
14
|
-
replace.mockReset()
|
|
15
|
-
query = {}
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
describe('qparam / qenum', () => {
|
|
19
|
-
it('reads string params and returns "" for absent ones', () => {
|
|
20
|
-
query = { q: 'AMD' }
|
|
21
|
-
const { qparam } = useQueryParamSync()
|
|
22
|
-
expect(qparam('q')).toBe('AMD')
|
|
23
|
-
expect(qparam('missing')).toBe('')
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it('qenum only accepts allowed values, else the fallback', () => {
|
|
27
|
-
query = { range: '7D', bogus: 'nope' }
|
|
28
|
-
const { qenum } = useQueryParamSync()
|
|
29
|
-
expect(qenum('range', ['1D', '7D', '30D'], '1D')).toBe('7D')
|
|
30
|
-
expect(qenum('bogus', ['a', 'b'], 'a')).toBe('a')
|
|
31
|
-
expect(qenum('missing', ['a', 'b'], 'b')).toBe('b')
|
|
32
|
-
})
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
describe('replaceQuery', () => {
|
|
36
|
-
it('replaces when the query differs', () => {
|
|
37
|
-
query = { q: 'old' }
|
|
38
|
-
const { replaceQuery } = useQueryParamSync()
|
|
39
|
-
replaceQuery({ q: 'new' })
|
|
40
|
-
expect(replace).toHaveBeenCalledWith({ query: { q: 'new' } })
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it('no-ops when the query already matches (same keys and values)', () => {
|
|
44
|
-
query = { q: 'AMD', range: '7D' }
|
|
45
|
-
const { replaceQuery } = useQueryParamSync()
|
|
46
|
-
replaceQuery({ q: 'AMD', range: '7D' })
|
|
47
|
-
expect(replace).not.toHaveBeenCalled()
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
it('replaces when a key is removed (defaults omitted)', () => {
|
|
51
|
-
query = { q: 'AMD', range: '7D' }
|
|
52
|
-
const { replaceQuery } = useQueryParamSync()
|
|
53
|
-
replaceQuery({ q: 'AMD' })
|
|
54
|
-
expect(replace).toHaveBeenCalledWith({ query: { q: 'AMD' } })
|
|
55
|
-
})
|
|
56
|
-
})
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
|
|
3
|
+
const replace = vi.fn()
|
|
4
|
+
let query: Record<string, string> = {}
|
|
5
|
+
|
|
6
|
+
vi.mock('vue-router', () => ({
|
|
7
|
+
useRoute: () => ({ query }),
|
|
8
|
+
useRouter: () => ({ replace }),
|
|
9
|
+
}))
|
|
10
|
+
|
|
11
|
+
import { useQueryParamSync } from './useQueryParamSync'
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
replace.mockReset()
|
|
15
|
+
query = {}
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
describe('qparam / qenum', () => {
|
|
19
|
+
it('reads string params and returns "" for absent ones', () => {
|
|
20
|
+
query = { q: 'AMD' }
|
|
21
|
+
const { qparam } = useQueryParamSync()
|
|
22
|
+
expect(qparam('q')).toBe('AMD')
|
|
23
|
+
expect(qparam('missing')).toBe('')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('qenum only accepts allowed values, else the fallback', () => {
|
|
27
|
+
query = { range: '7D', bogus: 'nope' }
|
|
28
|
+
const { qenum } = useQueryParamSync()
|
|
29
|
+
expect(qenum('range', ['1D', '7D', '30D'], '1D')).toBe('7D')
|
|
30
|
+
expect(qenum('bogus', ['a', 'b'], 'a')).toBe('a')
|
|
31
|
+
expect(qenum('missing', ['a', 'b'], 'b')).toBe('b')
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe('replaceQuery', () => {
|
|
36
|
+
it('replaces when the query differs', () => {
|
|
37
|
+
query = { q: 'old' }
|
|
38
|
+
const { replaceQuery } = useQueryParamSync()
|
|
39
|
+
replaceQuery({ q: 'new' })
|
|
40
|
+
expect(replace).toHaveBeenCalledWith({ query: { q: 'new' } })
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('no-ops when the query already matches (same keys and values)', () => {
|
|
44
|
+
query = { q: 'AMD', range: '7D' }
|
|
45
|
+
const { replaceQuery } = useQueryParamSync()
|
|
46
|
+
replaceQuery({ q: 'AMD', range: '7D' })
|
|
47
|
+
expect(replace).not.toHaveBeenCalled()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('replaces when a key is removed (defaults omitted)', () => {
|
|
51
|
+
query = { q: 'AMD', range: '7D' }
|
|
52
|
+
const { replaceQuery } = useQueryParamSync()
|
|
53
|
+
replaceQuery({ q: 'AMD' })
|
|
54
|
+
expect(replace).toHaveBeenCalledWith({ query: { q: 'AMD' } })
|
|
55
|
+
})
|
|
56
|
+
})
|
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
import { useRoute, useRouter } from 'vue-router'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* URL-query mirroring helpers for filterable views: read a filter's initial
|
|
5
|
-
* state from the query string once on setup, then write changes back with
|
|
6
|
-
* `replaceQuery` so a filtered view can be copied, shared and bookmarked
|
|
7
|
-
* without filter tweaks piling up in the browser history.
|
|
8
|
-
*/
|
|
9
|
-
export function useQueryParamSync() {
|
|
10
|
-
const route = useRoute()
|
|
11
|
-
const router = useRouter()
|
|
12
|
-
|
|
13
|
-
/** String value of a query param ('' when absent or repeated). */
|
|
14
|
-
function qparam(name: string): string {
|
|
15
|
-
const v = route.query[name]
|
|
16
|
-
return typeof v === 'string' ? v : ''
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Enum-ish param: only values from `allowed` pass; anything else in a
|
|
21
|
-
* hand-edited URL falls back to the default.
|
|
22
|
-
*/
|
|
23
|
-
function qenum<T extends string>(name: string, allowed: readonly T[], fallback: T): T {
|
|
24
|
-
const v = qparam(name)
|
|
25
|
-
return (allowed as readonly string[]).includes(v) ? (v as T) : fallback
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Mirror the given params into the URL with `router.replace` (not `push`),
|
|
30
|
-
* no-oping when nothing changed so timers/auto-refresh don't issue redundant
|
|
31
|
-
* navigations. Callers omit default values so the address stays clean.
|
|
32
|
-
*/
|
|
33
|
-
function replaceQuery(next: Record<string, string>): void {
|
|
34
|
-
const current = route.query
|
|
35
|
-
const same =
|
|
36
|
-
Object.keys(next).length === Object.keys(current).length &&
|
|
37
|
-
Object.entries(next).every(([k, v]) => current[k] === v)
|
|
38
|
-
if (!same) void router.replace({ query: next })
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return { qparam, qenum, replaceQuery }
|
|
42
|
-
}
|
|
1
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* URL-query mirroring helpers for filterable views: read a filter's initial
|
|
5
|
+
* state from the query string once on setup, then write changes back with
|
|
6
|
+
* `replaceQuery` so a filtered view can be copied, shared and bookmarked
|
|
7
|
+
* without filter tweaks piling up in the browser history.
|
|
8
|
+
*/
|
|
9
|
+
export function useQueryParamSync() {
|
|
10
|
+
const route = useRoute()
|
|
11
|
+
const router = useRouter()
|
|
12
|
+
|
|
13
|
+
/** String value of a query param ('' when absent or repeated). */
|
|
14
|
+
function qparam(name: string): string {
|
|
15
|
+
const v = route.query[name]
|
|
16
|
+
return typeof v === 'string' ? v : ''
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Enum-ish param: only values from `allowed` pass; anything else in a
|
|
21
|
+
* hand-edited URL falls back to the default.
|
|
22
|
+
*/
|
|
23
|
+
function qenum<T extends string>(name: string, allowed: readonly T[], fallback: T): T {
|
|
24
|
+
const v = qparam(name)
|
|
25
|
+
return (allowed as readonly string[]).includes(v) ? (v as T) : fallback
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Mirror the given params into the URL with `router.replace` (not `push`),
|
|
30
|
+
* no-oping when nothing changed so timers/auto-refresh don't issue redundant
|
|
31
|
+
* navigations. Callers omit default values so the address stays clean.
|
|
32
|
+
*/
|
|
33
|
+
function replaceQuery(next: Record<string, string>): void {
|
|
34
|
+
const current = route.query
|
|
35
|
+
const same =
|
|
36
|
+
Object.keys(next).length === Object.keys(current).length &&
|
|
37
|
+
Object.entries(next).every(([k, v]) => current[k] === v)
|
|
38
|
+
if (!same) void router.replace({ query: next })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { qparam, qenum, replaceQuery }
|
|
42
|
+
}
|
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { isReactive } from 'vue'
|
|
3
|
-
import { normalizeClass } from '@vue/shared'
|
|
4
|
-
import { useThemeClasses } from './useThemeClasses'
|
|
5
|
-
import { useTheme } from './useTheme'
|
|
6
|
-
|
|
7
|
-
describe('useThemeClasses', () => {
|
|
8
|
-
it('returns a reactive object whose properties are plain class strings', () => {
|
|
9
|
-
const t = useThemeClasses()
|
|
10
|
-
expect(isReactive(t)).toBe(true)
|
|
11
|
-
expect(typeof t.primaryText).toBe('string')
|
|
12
|
-
expect(t.primaryText).toMatch(/^text-/)
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
it('survives normalizeClass — the regression that leaked ref internals into class attrs', () => {
|
|
16
|
-
// Before the reactive() fix, `:class="t.primaryText"` handed a raw
|
|
17
|
-
// ComputedRef to normalizeClass, which rendered the ref's own keys
|
|
18
|
-
// ("fn dep __v_isRef …") instead of the theme classes.
|
|
19
|
-
const t = useThemeClasses()
|
|
20
|
-
const normalized = normalizeClass(t.primaryText)
|
|
21
|
-
expect(normalized).not.toContain('__v_isRef')
|
|
22
|
-
expect(normalized).toBe(t.primaryText)
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
it('tracks theme flips', () => {
|
|
26
|
-
const { isDark, toggleTheme } = useTheme()
|
|
27
|
-
const t = useThemeClasses()
|
|
28
|
-
const before = t.primaryText
|
|
29
|
-
toggleTheme()
|
|
30
|
-
expect(t.primaryText).not.toBe(before)
|
|
31
|
-
expect(t.primaryText).toBe(isDark.value ? 'text-white' : 'text-gray-900')
|
|
32
|
-
toggleTheme() // restore for other tests sharing the singleton theme
|
|
33
|
-
})
|
|
34
|
-
})
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { isReactive } from 'vue'
|
|
3
|
+
import { normalizeClass } from '@vue/shared'
|
|
4
|
+
import { useThemeClasses } from './useThemeClasses'
|
|
5
|
+
import { useTheme } from './useTheme'
|
|
6
|
+
|
|
7
|
+
describe('useThemeClasses', () => {
|
|
8
|
+
it('returns a reactive object whose properties are plain class strings', () => {
|
|
9
|
+
const t = useThemeClasses()
|
|
10
|
+
expect(isReactive(t)).toBe(true)
|
|
11
|
+
expect(typeof t.primaryText).toBe('string')
|
|
12
|
+
expect(t.primaryText).toMatch(/^text-/)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('survives normalizeClass — the regression that leaked ref internals into class attrs', () => {
|
|
16
|
+
// Before the reactive() fix, `:class="t.primaryText"` handed a raw
|
|
17
|
+
// ComputedRef to normalizeClass, which rendered the ref's own keys
|
|
18
|
+
// ("fn dep __v_isRef …") instead of the theme classes.
|
|
19
|
+
const t = useThemeClasses()
|
|
20
|
+
const normalized = normalizeClass(t.primaryText)
|
|
21
|
+
expect(normalized).not.toContain('__v_isRef')
|
|
22
|
+
expect(normalized).toBe(t.primaryText)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('tracks theme flips', () => {
|
|
26
|
+
const { isDark, toggleTheme } = useTheme()
|
|
27
|
+
const t = useThemeClasses()
|
|
28
|
+
const before = t.primaryText
|
|
29
|
+
toggleTheme()
|
|
30
|
+
expect(t.primaryText).not.toBe(before)
|
|
31
|
+
expect(t.primaryText).toBe(isDark.value ? 'text-white' : 'text-gray-900')
|
|
32
|
+
toggleTheme() // restore for other tests sharing the singleton theme
|
|
33
|
+
})
|
|
34
|
+
})
|
|
@@ -1,137 +1,137 @@
|
|
|
1
|
-
import { computed, reactive } from 'vue'
|
|
2
|
-
import { useTheme } from './useTheme'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Centralized dark/light Tailwind class strings.
|
|
6
|
-
*
|
|
7
|
-
* Named "roles" that map to dark/light class strings, so consumers don't
|
|
8
|
-
* spell out `isDark ? '...' : '...'` ternaries across every view.
|
|
9
|
-
*
|
|
10
|
-
* Usage:
|
|
11
|
-
*
|
|
12
|
-
* const t = useThemeClasses()
|
|
13
|
-
* <div :class="t.card">
|
|
14
|
-
* <p :class="t.mutedText">...</p>
|
|
15
|
-
* </div>
|
|
16
|
-
*
|
|
17
|
-
* The returned object is `reactive()`, so property access yields the
|
|
18
|
-
* plain class string while staying reactive to theme flips. (It used to
|
|
19
|
-
* be a plain object of ComputedRefs — Vue does NOT unwrap refs nested in
|
|
20
|
-
* plain objects inside template bindings, so `:class="t.card"` rendered
|
|
21
|
-
* the ref's own keys — `fn dep __v_isRef …` — instead of the classes.)
|
|
22
|
-
* Property access is string-valued now: do not append `.value`.
|
|
23
|
-
*/
|
|
24
|
-
export interface ThemeClasses {
|
|
25
|
-
// -- Surfaces --
|
|
26
|
-
/** Card surface: bg-gray-800 / bg-white, with border. */
|
|
27
|
-
card: string
|
|
28
|
-
/** Slightly darker card surface: bg-gray-900 / bg-white, with border. */
|
|
29
|
-
cardAlt: string
|
|
30
|
-
/** Page background. */
|
|
31
|
-
pageBg: string
|
|
32
|
-
|
|
33
|
-
// -- Borders --
|
|
34
|
-
/** Standard border between cards/sections. */
|
|
35
|
-
border: string
|
|
36
|
-
/** Subtle divider (lighter than border). */
|
|
37
|
-
divider: string
|
|
38
|
-
|
|
39
|
-
// -- Text hierarchy --
|
|
40
|
-
/** Page headings: white / gray-900. */
|
|
41
|
-
primaryText: string
|
|
42
|
-
/** Softer headings: gray-100 / gray-800. */
|
|
43
|
-
primaryTextSoft: string
|
|
44
|
-
/** Body copy on cards: gray-200 / gray-700. */
|
|
45
|
-
bodyText: string
|
|
46
|
-
/** Form labels: gray-300 / gray-700. */
|
|
47
|
-
label: string
|
|
48
|
-
/** Secondary text: gray-400 / gray-600. */
|
|
49
|
-
mutedText: string
|
|
50
|
-
/** Tertiary text: gray-500 / gray-600. */
|
|
51
|
-
subtleText: string
|
|
52
|
-
/** Dimmest text: gray-500 in dark / gray-400 in light. */
|
|
53
|
-
dimText: string
|
|
54
|
-
/** Slightly more contrast than dimText: gray-400 / gray-500. */
|
|
55
|
-
dimTextAlt: string
|
|
56
|
-
/** Big illustration / empty-state icon: gray-600 / gray-300. */
|
|
57
|
-
illustration: string
|
|
58
|
-
|
|
59
|
-
// -- Inputs --
|
|
60
|
-
/** Standard text input. */
|
|
61
|
-
input: string
|
|
62
|
-
/** Input + placeholder colour. */
|
|
63
|
-
inputWithPlaceholder: string
|
|
64
|
-
|
|
65
|
-
// -- Buttons --
|
|
66
|
-
/** Ghost / Cancel button. */
|
|
67
|
-
ghostButton: string
|
|
68
|
-
|
|
69
|
-
// -- Accent text colours --
|
|
70
|
-
/** Emerald accent text: emerald-400 / emerald-600. */
|
|
71
|
-
emeraldText: string
|
|
72
|
-
/** Amber accent text: amber-400 / amber-600. */
|
|
73
|
-
amberText: string
|
|
74
|
-
/** Amber heading-weight: amber-300 / amber-700. */
|
|
75
|
-
amberTextStrong: string
|
|
76
|
-
/** Red accent text: red-400 / red-500. */
|
|
77
|
-
redText: string
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function useThemeClasses(): ThemeClasses {
|
|
81
|
-
const { isDark } = useTheme()
|
|
82
|
-
|
|
83
|
-
return reactive({
|
|
84
|
-
card: computed(() =>
|
|
85
|
-
isDark.value
|
|
86
|
-
? 'bg-gray-800 border-gray-700'
|
|
87
|
-
: 'bg-white border-gray-200',
|
|
88
|
-
),
|
|
89
|
-
cardAlt: computed(() =>
|
|
90
|
-
isDark.value
|
|
91
|
-
? 'bg-gray-900 border-gray-700'
|
|
92
|
-
: 'bg-white border-gray-200',
|
|
93
|
-
),
|
|
94
|
-
pageBg: computed(() => (isDark.value ? 'bg-gray-900' : 'bg-gray-50')),
|
|
95
|
-
|
|
96
|
-
border: computed(() => (isDark.value ? 'border-gray-700' : 'border-gray-200')),
|
|
97
|
-
divider: computed(() => (isDark.value ? 'border-gray-800' : 'border-gray-100')),
|
|
98
|
-
|
|
99
|
-
primaryText: computed(() => (isDark.value ? 'text-white' : 'text-gray-900')),
|
|
100
|
-
primaryTextSoft: computed(() => (isDark.value ? 'text-gray-100' : 'text-gray-800')),
|
|
101
|
-
bodyText: computed(() => (isDark.value ? 'text-gray-200' : 'text-gray-700')),
|
|
102
|
-
label: computed(() => (isDark.value ? 'text-gray-300' : 'text-gray-700')),
|
|
103
|
-
mutedText: computed(() => (isDark.value ? 'text-gray-400' : 'text-gray-600')),
|
|
104
|
-
subtleText: computed(() => (isDark.value ? 'text-gray-500' : 'text-gray-600')),
|
|
105
|
-
dimText: computed(() => (isDark.value ? 'text-gray-500' : 'text-gray-400')),
|
|
106
|
-
dimTextAlt: computed(() => (isDark.value ? 'text-gray-400' : 'text-gray-500')),
|
|
107
|
-
illustration: computed(() => (isDark.value ? 'text-gray-600' : 'text-gray-300')),
|
|
108
|
-
|
|
109
|
-
input: computed(() =>
|
|
110
|
-
isDark.value
|
|
111
|
-
? 'bg-gray-700 border-gray-600 text-white'
|
|
112
|
-
: 'bg-gray-50 border-gray-300 text-gray-900',
|
|
113
|
-
),
|
|
114
|
-
inputWithPlaceholder: computed(() =>
|
|
115
|
-
isDark.value
|
|
116
|
-
? 'bg-gray-700 border-gray-600 text-white placeholder-gray-400'
|
|
117
|
-
: 'bg-gray-50 border-gray-300 text-gray-900 placeholder-gray-500',
|
|
118
|
-
),
|
|
119
|
-
|
|
120
|
-
ghostButton: computed(() =>
|
|
121
|
-
isDark.value
|
|
122
|
-
? 'bg-gray-800 text-gray-200 border-gray-600 hover:bg-gray-700'
|
|
123
|
-
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50',
|
|
124
|
-
),
|
|
125
|
-
|
|
126
|
-
emeraldText: computed(() =>
|
|
127
|
-
isDark.value ? 'text-emerald-400' : 'text-emerald-600',
|
|
128
|
-
),
|
|
129
|
-
amberText: computed(() =>
|
|
130
|
-
isDark.value ? 'text-amber-400' : 'text-amber-600',
|
|
131
|
-
),
|
|
132
|
-
amberTextStrong: computed(() =>
|
|
133
|
-
isDark.value ? 'text-amber-300' : 'text-amber-700',
|
|
134
|
-
),
|
|
135
|
-
redText: computed(() => (isDark.value ? 'text-red-400' : 'text-red-500')),
|
|
136
|
-
})
|
|
137
|
-
}
|
|
1
|
+
import { computed, reactive } from 'vue'
|
|
2
|
+
import { useTheme } from './useTheme'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Centralized dark/light Tailwind class strings.
|
|
6
|
+
*
|
|
7
|
+
* Named "roles" that map to dark/light class strings, so consumers don't
|
|
8
|
+
* spell out `isDark ? '...' : '...'` ternaries across every view.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
*
|
|
12
|
+
* const t = useThemeClasses()
|
|
13
|
+
* <div :class="t.card">
|
|
14
|
+
* <p :class="t.mutedText">...</p>
|
|
15
|
+
* </div>
|
|
16
|
+
*
|
|
17
|
+
* The returned object is `reactive()`, so property access yields the
|
|
18
|
+
* plain class string while staying reactive to theme flips. (It used to
|
|
19
|
+
* be a plain object of ComputedRefs — Vue does NOT unwrap refs nested in
|
|
20
|
+
* plain objects inside template bindings, so `:class="t.card"` rendered
|
|
21
|
+
* the ref's own keys — `fn dep __v_isRef …` — instead of the classes.)
|
|
22
|
+
* Property access is string-valued now: do not append `.value`.
|
|
23
|
+
*/
|
|
24
|
+
export interface ThemeClasses {
|
|
25
|
+
// -- Surfaces --
|
|
26
|
+
/** Card surface: bg-gray-800 / bg-white, with border. */
|
|
27
|
+
card: string
|
|
28
|
+
/** Slightly darker card surface: bg-gray-900 / bg-white, with border. */
|
|
29
|
+
cardAlt: string
|
|
30
|
+
/** Page background. */
|
|
31
|
+
pageBg: string
|
|
32
|
+
|
|
33
|
+
// -- Borders --
|
|
34
|
+
/** Standard border between cards/sections. */
|
|
35
|
+
border: string
|
|
36
|
+
/** Subtle divider (lighter than border). */
|
|
37
|
+
divider: string
|
|
38
|
+
|
|
39
|
+
// -- Text hierarchy --
|
|
40
|
+
/** Page headings: white / gray-900. */
|
|
41
|
+
primaryText: string
|
|
42
|
+
/** Softer headings: gray-100 / gray-800. */
|
|
43
|
+
primaryTextSoft: string
|
|
44
|
+
/** Body copy on cards: gray-200 / gray-700. */
|
|
45
|
+
bodyText: string
|
|
46
|
+
/** Form labels: gray-300 / gray-700. */
|
|
47
|
+
label: string
|
|
48
|
+
/** Secondary text: gray-400 / gray-600. */
|
|
49
|
+
mutedText: string
|
|
50
|
+
/** Tertiary text: gray-500 / gray-600. */
|
|
51
|
+
subtleText: string
|
|
52
|
+
/** Dimmest text: gray-500 in dark / gray-400 in light. */
|
|
53
|
+
dimText: string
|
|
54
|
+
/** Slightly more contrast than dimText: gray-400 / gray-500. */
|
|
55
|
+
dimTextAlt: string
|
|
56
|
+
/** Big illustration / empty-state icon: gray-600 / gray-300. */
|
|
57
|
+
illustration: string
|
|
58
|
+
|
|
59
|
+
// -- Inputs --
|
|
60
|
+
/** Standard text input. */
|
|
61
|
+
input: string
|
|
62
|
+
/** Input + placeholder colour. */
|
|
63
|
+
inputWithPlaceholder: string
|
|
64
|
+
|
|
65
|
+
// -- Buttons --
|
|
66
|
+
/** Ghost / Cancel button. */
|
|
67
|
+
ghostButton: string
|
|
68
|
+
|
|
69
|
+
// -- Accent text colours --
|
|
70
|
+
/** Emerald accent text: emerald-400 / emerald-600. */
|
|
71
|
+
emeraldText: string
|
|
72
|
+
/** Amber accent text: amber-400 / amber-600. */
|
|
73
|
+
amberText: string
|
|
74
|
+
/** Amber heading-weight: amber-300 / amber-700. */
|
|
75
|
+
amberTextStrong: string
|
|
76
|
+
/** Red accent text: red-400 / red-500. */
|
|
77
|
+
redText: string
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function useThemeClasses(): ThemeClasses {
|
|
81
|
+
const { isDark } = useTheme()
|
|
82
|
+
|
|
83
|
+
return reactive({
|
|
84
|
+
card: computed(() =>
|
|
85
|
+
isDark.value
|
|
86
|
+
? 'bg-gray-800 border-gray-700'
|
|
87
|
+
: 'bg-white border-gray-200',
|
|
88
|
+
),
|
|
89
|
+
cardAlt: computed(() =>
|
|
90
|
+
isDark.value
|
|
91
|
+
? 'bg-gray-900 border-gray-700'
|
|
92
|
+
: 'bg-white border-gray-200',
|
|
93
|
+
),
|
|
94
|
+
pageBg: computed(() => (isDark.value ? 'bg-gray-900' : 'bg-gray-50')),
|
|
95
|
+
|
|
96
|
+
border: computed(() => (isDark.value ? 'border-gray-700' : 'border-gray-200')),
|
|
97
|
+
divider: computed(() => (isDark.value ? 'border-gray-800' : 'border-gray-100')),
|
|
98
|
+
|
|
99
|
+
primaryText: computed(() => (isDark.value ? 'text-white' : 'text-gray-900')),
|
|
100
|
+
primaryTextSoft: computed(() => (isDark.value ? 'text-gray-100' : 'text-gray-800')),
|
|
101
|
+
bodyText: computed(() => (isDark.value ? 'text-gray-200' : 'text-gray-700')),
|
|
102
|
+
label: computed(() => (isDark.value ? 'text-gray-300' : 'text-gray-700')),
|
|
103
|
+
mutedText: computed(() => (isDark.value ? 'text-gray-400' : 'text-gray-600')),
|
|
104
|
+
subtleText: computed(() => (isDark.value ? 'text-gray-500' : 'text-gray-600')),
|
|
105
|
+
dimText: computed(() => (isDark.value ? 'text-gray-500' : 'text-gray-400')),
|
|
106
|
+
dimTextAlt: computed(() => (isDark.value ? 'text-gray-400' : 'text-gray-500')),
|
|
107
|
+
illustration: computed(() => (isDark.value ? 'text-gray-600' : 'text-gray-300')),
|
|
108
|
+
|
|
109
|
+
input: computed(() =>
|
|
110
|
+
isDark.value
|
|
111
|
+
? 'bg-gray-700 border-gray-600 text-white'
|
|
112
|
+
: 'bg-gray-50 border-gray-300 text-gray-900',
|
|
113
|
+
),
|
|
114
|
+
inputWithPlaceholder: computed(() =>
|
|
115
|
+
isDark.value
|
|
116
|
+
? 'bg-gray-700 border-gray-600 text-white placeholder-gray-400'
|
|
117
|
+
: 'bg-gray-50 border-gray-300 text-gray-900 placeholder-gray-500',
|
|
118
|
+
),
|
|
119
|
+
|
|
120
|
+
ghostButton: computed(() =>
|
|
121
|
+
isDark.value
|
|
122
|
+
? 'bg-gray-800 text-gray-200 border-gray-600 hover:bg-gray-700'
|
|
123
|
+
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50',
|
|
124
|
+
),
|
|
125
|
+
|
|
126
|
+
emeraldText: computed(() =>
|
|
127
|
+
isDark.value ? 'text-emerald-400' : 'text-emerald-600',
|
|
128
|
+
),
|
|
129
|
+
amberText: computed(() =>
|
|
130
|
+
isDark.value ? 'text-amber-400' : 'text-amber-600',
|
|
131
|
+
),
|
|
132
|
+
amberTextStrong: computed(() =>
|
|
133
|
+
isDark.value ? 'text-amber-300' : 'text-amber-700',
|
|
134
|
+
),
|
|
135
|
+
redText: computed(() => (isDark.value ? 'text-red-400' : 'text-red-500')),
|
|
136
|
+
})
|
|
137
|
+
}
|