vome-core 0.0.24 → 0.0.25

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 (48) hide show
  1. package/dist/admin/components/vm-action-btn.vue +109 -0
  2. package/dist/admin/components/vm-aside-list-item.vue +124 -0
  3. package/dist/admin/components/vm-card-grid.vue +36 -0
  4. package/dist/admin/components/vm-card.vue +100 -0
  5. package/dist/admin/components/vm-perm-section.vue +57 -0
  6. package/dist/admin/components/vm-split-layout.vue +85 -0
  7. package/dist/admin/config/plugin-dev.js +1 -1
  8. package/dist/admin/crud/components/vm-avatar.vue +103 -0
  9. package/dist/admin/crud/components/vm-copy-btn.vue +101 -0
  10. package/dist/admin/crud/components/vm-datetime-picker.vue +9 -0
  11. package/dist/admin/crud/components/vm-ellipsis-text.vue +34 -0
  12. package/dist/admin/crud/components/vm-form-hint.vue +63 -0
  13. package/dist/admin/crud/components/vm-json-code.vue +40 -0
  14. package/dist/admin/crud/components/vm-json-editor.vue +128 -0
  15. package/dist/admin/crud/components/vm-json-viewer.vue +147 -0
  16. package/dist/admin/crud/components/vm-status-tag.vue +157 -0
  17. package/dist/admin/crud/components/vm-tag-list.vue +82 -0
  18. package/dist/admin/crud/components/vm-text-link.vue +51 -0
  19. package/dist/admin/crud/config.js +1 -1
  20. package/dist/admin/crud/confirm.js +1 -1
  21. package/dist/admin/crud/dict.js +1 -1
  22. package/dist/admin/crud/index.d.ts +11 -0
  23. package/dist/admin/crud/index.js +1 -1
  24. package/dist/admin/crud/key.js +1 -1
  25. package/dist/admin/crud/mitt.js +1 -1
  26. package/dist/admin/crud/plugins.js +1 -1
  27. package/dist/admin/crud/span.js +1 -1
  28. package/dist/admin/crud/style.js +1 -1
  29. package/dist/admin/crud/validate.js +1 -1
  30. package/dist/admin/directives/perm.js +1 -1
  31. package/dist/admin/hooks/useUpload.js +1 -1
  32. package/dist/admin/lib/browser.js +1 -1
  33. package/dist/admin/lib/cn.js +1 -1
  34. package/dist/admin/lib/dialog-float.js +1 -1
  35. package/dist/admin/lib/export-excel.js +1 -1
  36. package/dist/admin/lib/file-preview.js +1 -1
  37. package/dist/admin/lib/json.d.ts +12 -0
  38. package/dist/admin/lib/json.js +1 -0
  39. package/dist/admin/lib/menu.js +1 -1
  40. package/dist/admin/lib/tree.js +1 -1
  41. package/dist/admin/lib/upload.js +1 -1
  42. package/dist/admin/lib/video-frame.js +1 -1
  43. package/dist/index.js +1 -1
  44. package/dist/server/index.js +1 -1
  45. package/dist/shared/excel.js +1 -1
  46. package/dist/shared/index.js +1 -1
  47. package/dist/shared/tree.js +1 -1
  48. package/package.json +1 -1
@@ -0,0 +1,101 @@
1
+ <template>
2
+ <button
3
+ type="button"
4
+ class="vm-copy-btn"
5
+ :class="[`is-${size}`]"
6
+ :disabled="disabled || copying || !text"
7
+ @click="copy"
8
+ >
9
+ <i :class="copying ? 'ri-loader-4-line is-spin' : icon" aria-hidden="true" />
10
+ <span>{{ copying ? copyingText : label }}</span>
11
+ </button>
12
+ </template>
13
+
14
+ <script setup lang="ts">
15
+ import { ref } from 'vue'
16
+
17
+ defineOptions({ name: 'vm-copy-btn' })
18
+
19
+ const props = withDefaults(
20
+ defineProps<{
21
+ text?: string
22
+ label?: string
23
+ copyingText?: string
24
+ icon?: string
25
+ size?: 'sm' | 'md'
26
+ disabled?: boolean
27
+ }>(),
28
+ {
29
+ label: '复制',
30
+ copyingText: '复制中…',
31
+ icon: 'ri-file-copy-line',
32
+ size: 'md',
33
+ disabled: false,
34
+ },
35
+ )
36
+
37
+ const emit = defineEmits<{
38
+ copied: []
39
+ error: [e: unknown]
40
+ }>()
41
+
42
+ const copying = ref(false)
43
+
44
+ async function copy() {
45
+ if (!props.text || copying.value) return
46
+ copying.value = true
47
+ try {
48
+ await navigator.clipboard.writeText(props.text)
49
+ emit('copied')
50
+ } catch (e) {
51
+ emit('error', e)
52
+ } finally {
53
+ copying.value = false
54
+ }
55
+ }
56
+ </script>
57
+
58
+ <style lang="scss" scoped>
59
+ .vm-copy-btn {
60
+ display: inline-flex;
61
+ align-items: center;
62
+ gap: 4px;
63
+ border: none;
64
+ border-radius: 8px;
65
+ background: color-mix(in srgb, var(--brand) 10%, var(--card));
66
+ color: var(--brand);
67
+ font-size: 12px;
68
+ font-weight: 600;
69
+ cursor: pointer;
70
+
71
+ &:hover:not(:disabled) {
72
+ background: color-mix(in srgb, var(--brand) 18%, var(--card));
73
+ }
74
+
75
+ &:disabled {
76
+ opacity: 0.45;
77
+ cursor: not-allowed;
78
+ }
79
+
80
+ &.is-md {
81
+ height: 32px;
82
+ padding: 0 12px;
83
+ }
84
+
85
+ &.is-sm {
86
+ height: 28px;
87
+ padding: 0 10px;
88
+ font-size: 11px;
89
+ }
90
+
91
+ i.is-spin {
92
+ animation: vm-copy-spin 0.8s linear infinite;
93
+ }
94
+ }
95
+
96
+ @keyframes vm-copy-spin {
97
+ to {
98
+ transform: rotate(360deg);
99
+ }
100
+ }
101
+ </style>
@@ -0,0 +1,9 @@
1
+ <template>
2
+ <vm-date-picker v-bind="$attrs" type="datetime" />
3
+ </template>
4
+
5
+ <script setup lang="ts">
6
+ import VmDatePicker from './vm-date-picker.vue'
7
+
8
+ defineOptions({ name: 'vm-datetime-picker', inheritAttrs: false })
9
+ </script>
@@ -0,0 +1,34 @@
1
+ <template>
2
+ <span class="vm-ellipsis-text" :title="titleText">{{ text }}</span>
3
+ </template>
4
+
5
+ <script setup lang="ts">
6
+ import { computed } from 'vue'
7
+
8
+ defineOptions({ name: 'vm-ellipsis-text' })
9
+
10
+ const props = defineProps<{
11
+ modelValue?: unknown
12
+ title?: string
13
+ }>()
14
+
15
+ const text = computed(() => {
16
+ const v = props.modelValue
17
+ if (v == null || v === '') return '—'
18
+ return String(v)
19
+ })
20
+
21
+ const titleText = computed(() => props.title ?? (text.value === '—' ? undefined : text.value))
22
+ </script>
23
+
24
+ <style lang="scss" scoped>
25
+ .vm-ellipsis-text {
26
+ display: inline-block;
27
+ max-width: 100%;
28
+ overflow: hidden;
29
+ text-overflow: ellipsis;
30
+ white-space: nowrap;
31
+ font-size: 12px;
32
+ color: var(--muted-foreground);
33
+ }
34
+ </style>
@@ -0,0 +1,63 @@
1
+ <template>
2
+ <p class="vm-form-hint" :class="[`is-${variant}`]">
3
+ <i v-if="icon" :class="icon" aria-hidden="true" />
4
+ <slot>{{ text }}</slot>
5
+ </p>
6
+ </template>
7
+
8
+ <script setup lang="ts">
9
+ defineOptions({ name: 'vm-form-hint' })
10
+
11
+ withDefaults(
12
+ defineProps<{
13
+ text?: string
14
+ variant?: 'info' | 'warn' | 'success'
15
+ icon?: string
16
+ }>(),
17
+ {
18
+ text: '',
19
+ variant: 'info',
20
+ icon: 'ri-information-line',
21
+ },
22
+ )
23
+ </script>
24
+
25
+ <style lang="scss" scoped>
26
+ .vm-form-hint {
27
+ display: flex;
28
+ align-items: flex-start;
29
+ gap: 8px;
30
+ margin: 0;
31
+ padding: 10px 12px;
32
+ border-radius: 10px;
33
+ font-size: 13px;
34
+ line-height: 1.5;
35
+ color: var(--muted-foreground);
36
+ background: color-mix(in srgb, var(--brand) 8%, var(--card));
37
+
38
+ i {
39
+ flex-shrink: 0;
40
+ margin-top: 2px;
41
+ font-size: 15px;
42
+ color: var(--brand);
43
+ }
44
+
45
+ &.is-warn {
46
+ background: color-mix(in srgb, #f59e0b 10%, var(--card));
47
+ color: color-mix(in srgb, var(--foreground) 72%, #d97706);
48
+
49
+ i {
50
+ color: #d97706;
51
+ }
52
+ }
53
+
54
+ &.is-success {
55
+ background: var(--success-soft);
56
+ color: var(--success);
57
+
58
+ i {
59
+ color: var(--success);
60
+ }
61
+ }
62
+ }
63
+ </style>
@@ -0,0 +1,40 @@
1
+ <template>
2
+ <pre class="vm-json-code vm-scroll">{{ body }}</pre>
3
+ </template>
4
+
5
+ <script setup lang="ts">
6
+ import { computed } from 'vue'
7
+ import { prettyJson } from '../../lib/json'
8
+
9
+ defineOptions({ name: 'vm-json-code' })
10
+
11
+ const props = withDefaults(
12
+ defineProps<{
13
+ modelValue?: unknown
14
+ empty?: string
15
+ }>(),
16
+ {
17
+ empty: '(空)',
18
+ },
19
+ )
20
+
21
+ const body = computed(() => prettyJson(props.modelValue, props.empty))
22
+ </script>
23
+
24
+ <style lang="scss" scoped>
25
+ .vm-json-code {
26
+ flex: 1;
27
+ margin: 0;
28
+ min-height: 0;
29
+ overflow: auto;
30
+ padding: 16px 22px;
31
+ border-radius: var(--radius);
32
+ background: var(--muted);
33
+ color: var(--foreground);
34
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
35
+ font-size: 12px;
36
+ line-height: 1.55;
37
+ white-space: pre-wrap;
38
+ word-break: break-word;
39
+ }
40
+ </style>
@@ -0,0 +1,128 @@
1
+ <template>
2
+ <div class="vm-json-editor">
3
+ <textarea
4
+ class="vm-json-editor__input vm-scroll"
5
+ :class="{ 'is-error': Boolean(error) }"
6
+ :value="inner"
7
+ :disabled="disabled"
8
+ :placeholder="placeholder"
9
+ :rows="rows"
10
+ @input="onInput"
11
+ @blur="validate"
12
+ />
13
+ <p v-if="error" class="vm-json-editor__error">{{ error }}</p>
14
+ </div>
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ import { ref, watch } from 'vue'
19
+ import { parseJsonInput, prettyJson, textOfJson } from '../../lib/json'
20
+
21
+ defineOptions({ name: 'vm-json-editor' })
22
+
23
+ const props = withDefaults(
24
+ defineProps<{
25
+ modelValue?: unknown
26
+ placeholder?: string
27
+ rows?: number
28
+ disabled?: boolean
29
+ /** blur 时自动格式化 */
30
+ formatOnBlur?: boolean
31
+ }>(),
32
+ {
33
+ placeholder: '请输入 JSON',
34
+ rows: 8,
35
+ formatOnBlur: true,
36
+ },
37
+ )
38
+
39
+ const emit = defineEmits<{
40
+ 'update:modelValue': [v: unknown]
41
+ change: [v: unknown]
42
+ error: [msg: string]
43
+ }>()
44
+
45
+ const inner = ref('')
46
+ const error = ref('')
47
+
48
+ function syncFromModel() {
49
+ inner.value = prettyJson(props.modelValue, '')
50
+ error.value = ''
51
+ }
52
+
53
+ watch(
54
+ () => props.modelValue,
55
+ () => syncFromModel(),
56
+ { immediate: true },
57
+ )
58
+
59
+ function onInput(e: Event) {
60
+ inner.value = (e.target as HTMLTextAreaElement).value
61
+ error.value = ''
62
+ }
63
+
64
+ function validate() {
65
+ const parsed = parseJsonInput(inner.value)
66
+ if (!parsed.ok) {
67
+ error.value = parsed.error
68
+ emit('error', parsed.error)
69
+ return
70
+ }
71
+ if (props.formatOnBlur) {
72
+ inner.value = prettyJson(parsed.value, '')
73
+ }
74
+ error.value = ''
75
+ emit('update:modelValue', parsed.value)
76
+ emit('change', parsed.value)
77
+ }
78
+
79
+ defineExpose({
80
+ validate,
81
+ getText: () => inner.value,
82
+ setText: (v: unknown) => {
83
+ inner.value = textOfJson(v)
84
+ },
85
+ })
86
+ </script>
87
+
88
+ <style lang="scss" scoped>
89
+ .vm-json-editor {
90
+ display: grid;
91
+ gap: 6px;
92
+ min-width: 0;
93
+ }
94
+
95
+ .vm-json-editor__input {
96
+ width: 100%;
97
+ min-height: 120px;
98
+ padding: 10px 12px;
99
+ border: 1px solid var(--border);
100
+ border-radius: var(--vm-control-radius);
101
+ background: var(--muted);
102
+ color: var(--foreground);
103
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
104
+ font-size: 12px;
105
+ line-height: 1.55;
106
+ resize: vertical;
107
+ outline: none;
108
+
109
+ &:focus {
110
+ border-color: var(--focus-border, var(--brand));
111
+ }
112
+
113
+ &.is-error {
114
+ border-color: var(--danger);
115
+ }
116
+
117
+ &:disabled {
118
+ opacity: 0.6;
119
+ cursor: not-allowed;
120
+ }
121
+ }
122
+
123
+ .vm-json-editor__error {
124
+ margin: 0;
125
+ font-size: 12px;
126
+ color: var(--danger);
127
+ }
128
+ </style>
@@ -0,0 +1,147 @@
1
+ <template>
2
+ <template v-if="mode === 'code'">
3
+ <vm-json-code :model-value="modelValue" :empty="empty" />
4
+ </template>
5
+ <template v-else>
6
+ <vm-text-link
7
+ v-if="hasContent"
8
+ :label="label"
9
+ @click="open = true"
10
+ />
11
+ <span v-else class="vm-json-viewer is-empty">—</span>
12
+ </template>
13
+
14
+ <Teleport to="body">
15
+ <div
16
+ v-if="open"
17
+ class="vm-json-viewer__dialog"
18
+ role="dialog"
19
+ aria-modal="true"
20
+ @click.self="open = false"
21
+ >
22
+ <header class="vm-json-viewer__head">
23
+ <span class="vm-json-viewer__title">{{ title }}</span>
24
+ <div class="vm-json-viewer__actions">
25
+ <vm-copy-btn :text="body" size="sm" label="复制" />
26
+ <button type="button" class="vm-json-viewer__close" title="关闭" @click="open = false">
27
+ <i class="ri-close-line" />
28
+ </button>
29
+ </div>
30
+ </header>
31
+ <div class="vm-json-viewer__body vm-scroll">
32
+ <vm-json-code :model-value="modelValue" :empty="empty" />
33
+ </div>
34
+ </div>
35
+ </Teleport>
36
+ </template>
37
+
38
+ <script setup lang="ts">
39
+ import { computed, ref } from 'vue'
40
+ import { hasJsonContent, prettyJson } from '../../lib/json'
41
+ import VmJsonCode from './vm-json-code.vue'
42
+ import VmTextLink from './vm-text-link.vue'
43
+ import VmCopyBtn from './vm-copy-btn.vue'
44
+
45
+ defineOptions({ name: 'vm-json-viewer' })
46
+
47
+ const props = withDefaults(
48
+ defineProps<{
49
+ modelValue?: unknown
50
+ title?: string
51
+ label?: string
52
+ /** button 打开浮层;code 仅渲染代码块(upsert fill) */
53
+ mode?: 'button' | 'code'
54
+ empty?: string
55
+ }>(),
56
+ {
57
+ title: 'JSON',
58
+ label: '查看',
59
+ mode: 'button',
60
+ empty: '(空)',
61
+ },
62
+ )
63
+
64
+ const open = ref(false)
65
+ const hasContent = computed(() => hasJsonContent(props.modelValue))
66
+ const body = computed(() => prettyJson(props.modelValue, props.empty))
67
+ </script>
68
+
69
+ <style lang="scss" scoped>
70
+ .vm-json-viewer.is-empty {
71
+ color: var(--muted-foreground);
72
+ }
73
+
74
+ .vm-json-viewer__dialog {
75
+ position: fixed;
76
+ inset: 0;
77
+ z-index: 5200;
78
+ display: flex;
79
+ flex-direction: column;
80
+ padding: 24px;
81
+ background: color-mix(in srgb, var(--foreground) 42%, transparent);
82
+ }
83
+
84
+ .vm-json-viewer__head {
85
+ display: flex;
86
+ flex-shrink: 0;
87
+ align-items: center;
88
+ justify-content: space-between;
89
+ gap: 12px;
90
+ margin: 0 auto;
91
+ width: min(960px, 100%);
92
+ padding: 12px 16px;
93
+ border-radius: 14px 14px 0 0;
94
+ background: var(--card);
95
+ border: 1px solid color-mix(in srgb, var(--foreground) 8%, transparent);
96
+ border-bottom: none;
97
+ }
98
+
99
+ .vm-json-viewer__title {
100
+ font-size: 14px;
101
+ font-weight: 700;
102
+ color: var(--foreground);
103
+ }
104
+
105
+ .vm-json-viewer__actions {
106
+ display: inline-flex;
107
+ align-items: center;
108
+ gap: 8px;
109
+ }
110
+
111
+ .vm-json-viewer__close {
112
+ display: inline-flex;
113
+ width: 32px;
114
+ height: 32px;
115
+ align-items: center;
116
+ justify-content: center;
117
+ border: none;
118
+ border-radius: 8px;
119
+ background: transparent;
120
+ color: var(--muted-foreground);
121
+ cursor: pointer;
122
+
123
+ &:hover {
124
+ background: var(--muted);
125
+ color: var(--foreground);
126
+ }
127
+ }
128
+
129
+ .vm-json-viewer__body {
130
+ flex: 1;
131
+ min-height: 0;
132
+ margin: 0 auto;
133
+ width: min(960px, 100%);
134
+ display: flex;
135
+ flex-direction: column;
136
+ border-radius: 0 0 14px 14px;
137
+ background: var(--card);
138
+ border: 1px solid color-mix(in srgb, var(--foreground) 8%, transparent);
139
+ border-top: none;
140
+ overflow: hidden;
141
+
142
+ :deep(.vm-json-code) {
143
+ border-radius: 0;
144
+ background: var(--muted);
145
+ }
146
+ }
147
+ </style>
@@ -0,0 +1,157 @@
1
+ <template>
2
+ <button
3
+ v-if="clickable"
4
+ type="button"
5
+ class="vm-status-tag"
6
+ :class="[resolved.variant, { 'is-clickable': clickable }]"
7
+ :disabled="disabled"
8
+ @click="emit('click', $event)"
9
+ >
10
+ {{ resolved.label }}
11
+ </button>
12
+ <span v-else class="vm-status-tag" :class="resolved.variant">
13
+ {{ resolved.label }}
14
+ </span>
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ import { computed } from 'vue'
19
+
20
+ defineOptions({ name: 'vm-status-tag' })
21
+
22
+ type Variant = 'success' | 'warning' | 'danger' | 'info' | 'brand' | 'neutral'
23
+
24
+ const props = withDefaults(
25
+ defineProps<{
26
+ modelValue?: unknown
27
+ label?: string
28
+ variant?: Variant
29
+ /** http | queue | task 内置语义映射 */
30
+ preset?: 'http' | 'queue' | 'task'
31
+ /** 表格列传入的行数据,配合 labelProp 取展示文案 */
32
+ scope?: Record<string, unknown>
33
+ labelProp?: string
34
+ clickable?: boolean
35
+ disabled?: boolean
36
+ }>(),
37
+ {
38
+ clickable: false,
39
+ disabled: false,
40
+ },
41
+ )
42
+
43
+ const emit = defineEmits<{
44
+ click: [e: MouseEvent]
45
+ }>()
46
+
47
+ function resolveHttp(v: unknown): { variant: Variant; label: string } {
48
+ const n = Number(v)
49
+ const label = props.label ?? (v == null || v === '' ? '—' : String(v))
50
+ if (!Number.isFinite(n)) return { variant: 'neutral', label }
51
+ if (n >= 500) return { variant: 'danger', label }
52
+ if (n >= 400) return { variant: 'warning', label }
53
+ if (n >= 200 && n < 300) return { variant: 'success', label }
54
+ return { variant: 'neutral', label }
55
+ }
56
+
57
+ function resolveQueue(v: unknown): { variant: Variant; label: string } {
58
+ const key = String(v ?? '')
59
+ const label = props.label ?? (key || '—')
60
+ if (key === 'failed') return { variant: 'danger', label }
61
+ if (key === 'completed') return { variant: 'success', label }
62
+ if (key === 'active') return { variant: 'brand', label }
63
+ if (key === 'waiting' || key === 'delayed') return { variant: 'warning', label }
64
+ return { variant: 'neutral', label }
65
+ }
66
+
67
+ function resolveTask(v: unknown): { variant: Variant; label: string } {
68
+ const on = Number(v) === 1
69
+ const label = props.label ?? (on ? '运行中' : '已停止')
70
+ return { variant: on ? 'success' : 'neutral', label }
71
+ }
72
+
73
+ const resolved = computed(() => {
74
+ const scopeLabel =
75
+ props.labelProp && props.scope ? props.scope[props.labelProp] : undefined
76
+ const fallbackLabel =
77
+ props.label ??
78
+ (scopeLabel != null && scopeLabel !== '' ? String(scopeLabel) : undefined)
79
+
80
+ if (props.variant) {
81
+ return {
82
+ variant: props.variant,
83
+ label: fallbackLabel ?? (props.modelValue == null ? '—' : String(props.modelValue)),
84
+ }
85
+ }
86
+ if (props.preset === 'http') {
87
+ const r = resolveHttp(props.modelValue)
88
+ return { ...r, label: fallbackLabel ?? r.label }
89
+ }
90
+ if (props.preset === 'queue') {
91
+ const r = resolveQueue(props.modelValue)
92
+ return { ...r, label: fallbackLabel ?? r.label }
93
+ }
94
+ if (props.preset === 'task') {
95
+ const r = resolveTask(props.modelValue)
96
+ return { ...r, label: fallbackLabel ?? r.label }
97
+ }
98
+ return {
99
+ variant: 'neutral' as Variant,
100
+ label: fallbackLabel ?? (props.modelValue == null ? '—' : String(props.modelValue)),
101
+ }
102
+ })
103
+ </script>
104
+
105
+ <style lang="scss" scoped>
106
+ .vm-status-tag {
107
+ display: inline-flex;
108
+ min-width: 40px;
109
+ justify-content: center;
110
+ padding: 2px 8px;
111
+ border: none;
112
+ border-radius: 8px;
113
+ font-size: 12px;
114
+ font-weight: 650;
115
+ line-height: 1.4;
116
+ background: color-mix(in srgb, var(--foreground) 8%, transparent);
117
+ color: var(--muted-foreground);
118
+
119
+ &.is-success {
120
+ background: var(--success-soft);
121
+ color: var(--success);
122
+ }
123
+
124
+ &.is-warning {
125
+ background: color-mix(in srgb, #f59e0b 16%, transparent);
126
+ color: #d97706;
127
+ }
128
+
129
+ &.is-danger {
130
+ background: var(--danger-soft);
131
+ color: var(--danger);
132
+ }
133
+
134
+ &.is-info {
135
+ background: color-mix(in srgb, var(--brand) 12%, transparent);
136
+ color: var(--brand);
137
+ }
138
+
139
+ &.is-brand {
140
+ background: color-mix(in srgb, var(--brand) 14%, transparent);
141
+ color: var(--brand);
142
+ }
143
+
144
+ &.is-clickable {
145
+ cursor: pointer;
146
+
147
+ &:hover:not(:disabled) {
148
+ filter: brightness(0.96);
149
+ }
150
+
151
+ &:disabled {
152
+ opacity: 0.5;
153
+ cursor: not-allowed;
154
+ }
155
+ }
156
+ }
157
+ </style>