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.
Files changed (60) hide show
  1. package/README.md +1794 -1719
  2. package/dist/components/BaseBadge.vue.d.ts +1 -11
  3. package/dist/components/BaseDropdown.vue.d.ts +1 -1
  4. package/dist/ui-lib.js +942 -925
  5. package/dist/ui-lib.umd.cjs +1 -1
  6. package/dist/utils/specForm.d.ts +1 -1
  7. package/package.json +54 -54
  8. package/src/components/BaseAlert.vue +104 -104
  9. package/src/components/BaseBadge.vue +12 -2
  10. package/src/components/BaseBarDistribution.test.ts +51 -51
  11. package/src/components/BaseBarDistribution.vue +73 -73
  12. package/src/components/BaseChipButton.vue +42 -42
  13. package/src/components/BaseCodeBlock.test.ts +70 -70
  14. package/src/components/BaseCodeBlock.vue +74 -74
  15. package/src/components/BaseCollapsibleSection.vue +113 -113
  16. package/src/components/BaseConfirmModal.vue +114 -114
  17. package/src/components/BaseCredentialsForm.test.ts +108 -108
  18. package/src/components/BaseCredentialsForm.vue +241 -241
  19. package/src/components/BaseDropdown.test.ts +87 -87
  20. package/src/components/BaseDropdown.vue +172 -172
  21. package/src/components/BaseEntityPickerModal.vue +330 -330
  22. package/src/components/BaseFileDropzone.vue +94 -94
  23. package/src/components/BaseFilterChip.test.ts +44 -44
  24. package/src/components/BaseFilterChip.vue +67 -67
  25. package/src/components/BaseModalShell.vue +209 -209
  26. package/src/components/BasePageHeader.vue +120 -120
  27. package/src/components/BasePillPickerModal.test.ts +95 -95
  28. package/src/components/BaseRemoveButton.vue +22 -22
  29. package/src/components/BaseSpecFields.test.ts +39 -0
  30. package/src/components/BaseSpecFields.vue +27 -3
  31. package/src/components/BaseStatBreakdown.test.ts +56 -56
  32. package/src/components/BaseStatBreakdown.vue +49 -49
  33. package/src/components/BaseStatusPill.vue +54 -54
  34. package/src/components/BaseTextInputModal.vue +140 -140
  35. package/src/components/BaseToolbarButton.test.ts +48 -48
  36. package/src/components/BaseToolbarButton.vue +94 -94
  37. package/src/composables/useFieldClasses.test.ts +55 -55
  38. package/src/composables/useFieldClasses.ts +56 -56
  39. package/src/composables/usePolling.test.ts +112 -112
  40. package/src/composables/usePolling.ts +102 -102
  41. package/src/composables/useQueryParamSync.test.ts +56 -56
  42. package/src/composables/useQueryParamSync.ts +42 -42
  43. package/src/composables/useThemeClasses.test.ts +34 -34
  44. package/src/composables/useThemeClasses.ts +137 -137
  45. package/src/index.ts +138 -138
  46. package/src/style.css +32 -32
  47. package/src/types/distribution.ts +7 -7
  48. package/src/types/pillPicker.ts +14 -14
  49. package/src/types/specField.ts +30 -30
  50. package/src/types/statBreakdown.ts +6 -6
  51. package/src/utils/format.test.ts +264 -264
  52. package/src/utils/format.ts +216 -216
  53. package/src/utils/httpColors.test.ts +100 -100
  54. package/src/utils/httpColors.ts +99 -99
  55. package/src/utils/kvRows.test.ts +47 -47
  56. package/src/utils/kvRows.ts +34 -34
  57. package/src/utils/specForm.test.ts +64 -53
  58. package/src/utils/specForm.ts +49 -48
  59. package/src/utils/validate.test.ts +82 -82
  60. 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
+ }