@opentiny/tiny-robot-cli 0.5.2-alpha.15 → 0.5.2-alpha.17

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 (51) hide show
  1. package/README.md +1 -38
  2. package/bin/commands/add.js +268 -552
  3. package/bin/utils.js +44 -144
  4. package/package.json +3 -5
  5. package/templates/basic/.env.example +0 -8
  6. package/templates/basic/README.md +22 -53
  7. package/templates/basic/index.html +2 -2
  8. package/templates/basic/package.json +16 -12
  9. package/templates/basic/public/modelcontextprotocol.png +0 -0
  10. package/templates/basic/src/App.vue +119 -112
  11. package/templates/basic/src/components/ChatList.vue +82 -0
  12. package/templates/basic/src/components/ChatSender.vue +125 -0
  13. package/templates/basic/src/components/ConversationHistory.vue +136 -0
  14. package/templates/basic/src/components/HistoryDrawerButton.vue +43 -0
  15. package/templates/basic/src/components/McpServerPickerButton.vue +278 -0
  16. package/templates/basic/src/components/ThemeToggleButton.vue +44 -0
  17. package/templates/basic/src/components/icons/IconDeepThink.vue +29 -0
  18. package/templates/basic/src/components/icons/IconModelAliyunBailian.vue +51 -0
  19. package/templates/basic/src/components/icons/IconModelDeepseek.vue +29 -0
  20. package/templates/basic/src/components/icons/IconMoon.vue +29 -0
  21. package/templates/basic/src/components/icons/IconPlugin.vue +29 -0
  22. package/templates/basic/src/components/icons/IconSun.vue +35 -0
  23. package/templates/basic/src/components/icons/IconWebSearch.vue +36 -0
  24. package/templates/basic/src/components/icons/index.ts +7 -0
  25. package/templates/basic/src/composables/useChat.ts +129 -0
  26. package/templates/basic/src/composables/useMcp.ts +170 -0
  27. package/templates/basic/src/composables/useModel.ts +82 -0
  28. package/templates/basic/src/main.ts +2 -2
  29. package/templates/basic/src/mcpServers.ts +40 -0
  30. package/templates/basic/src/models.ts +81 -0
  31. package/templates/basic/tsconfig.app.json +5 -2
  32. package/templates/basic/tsconfig.node.json +6 -3
  33. package/templates/basic/vite.config.ts +1 -6
  34. package/templates/chat/.env.example +0 -9
  35. package/templates/chat/src/TinyRobotChat.vue +27 -88
  36. package/templates/basic/public/favicon.svg +0 -1
  37. package/templates/basic/public/icons.svg +0 -24
  38. package/templates/chat/README.md +0 -46
  39. package/templates/chat/index.html +0 -12
  40. package/templates/chat/package.json +0 -28
  41. package/templates/chat/src/components/ComposerTools.vue +0 -99
  42. package/templates/chat/src/components/WindowHeader.vue +0 -192
  43. package/templates/chat/src/composables/useWindow.ts +0 -83
  44. package/templates/chat/src/config/chat-runtime.ts +0 -82
  45. package/templates/chat/src/config/chat-ui.ts +0 -290
  46. package/templates/chat/src/index.css +0 -116
  47. package/templates/chat/src/main.ts +0 -7
  48. package/templates/chat/tsconfig.app.json +0 -13
  49. package/templates/chat/tsconfig.json +0 -7
  50. package/templates/chat/tsconfig.node.json +0 -19
  51. package/templates/chat/vite.config.ts +0 -16
@@ -1,123 +1,130 @@
1
+ <template>
2
+ <TrTheme>
3
+ <main class="app chat-card">
4
+ <ConversationHistory />
5
+
6
+ <section class="chat-panel">
7
+ <div class="chat-panel-content">
8
+ <p v-if="!hasApiConfig" class="config-warning">
9
+ 缺少 API 配置,请在 <code>.env</code> 中设置当前模型服务商对应的 Key。
10
+ </p>
11
+ <header class="chat-header">
12
+ <HistoryDrawerButton @click="historyDrawerOpen = true" />
13
+ <h3 v-if="currentConversationTitle">{{ currentConversationTitle }}</h3>
14
+ <ThemeToggleButton v-if="isWelcomePage" />
15
+ </header>
16
+
17
+ <ChatList />
18
+
19
+ <ChatSender />
20
+ </div>
21
+ </section>
22
+ </main>
23
+ </TrTheme>
24
+ </template>
25
+
1
26
  <script setup lang="ts">
2
- import {
3
- TrChat,
4
- useLocalChatRuntime,
5
- type ChatMcpServers,
6
- type ChatProviderConfig,
7
- type ChatUIOptions,
8
- } from '@opentiny/tiny-robot-chat'
9
27
  import { TrThemeProvider as TrTheme } from '@opentiny/tiny-robot'
10
- import { IconBailian, IconDeepseek } from '@opentiny/tiny-robot-svgs'
11
-
12
- const modelProviders: ChatProviderConfig[] = [
13
- {
14
- type: 'qwen',
15
- label: 'DashScope',
16
- apiUrl: import.meta.env.VITE_QWEN_API_URL?.trim() || undefined,
17
- apiKey: import.meta.env.VITE_ALIYUN_DASHSCOPE_KEY?.trim(),
18
- models: [
19
- {
20
- id: 'qwen3.7-flash',
21
- label: 'Qwen3.7 Flash',
22
- icon: IconBailian,
23
- capabilities: {
24
- thinking: true,
25
- search: true,
26
- },
27
- },
28
- {
29
- id: 'qwen3.7-plus',
30
- label: 'Qwen3.7 Plus',
31
- icon: IconBailian,
32
- capabilities: {
33
- thinking: true,
34
- search: true,
35
- },
36
- },
37
- {
38
- id: 'qwen3.7-max',
39
- label: 'Qwen3.7 Max',
40
- icon: IconBailian,
41
- capabilities: {
42
- thinking: true,
43
- search: true,
44
- },
45
- },
46
- ],
47
- },
48
- {
49
- type: 'deepseek',
50
- apiUrl: import.meta.env.VITE_DEEPSEEK_API_URL?.trim() || undefined,
51
- apiKey: import.meta.env.VITE_DEEPSEEK_API_KEY?.trim(),
52
- models: [
53
- {
54
- id: 'deepseek-v4-flash',
55
- label: 'DeepSeek V4 Flash',
56
- icon: IconDeepseek,
57
- capabilities: {
58
- thinking: true,
59
- },
60
- },
61
- {
62
- id: 'deepseek-v4-pro',
63
- label: 'DeepSeek V4 Pro',
64
- icon: IconDeepseek,
65
- capabilities: {
66
- thinking: true,
67
- },
68
- },
69
- ],
70
- },
71
- ]
72
-
73
- const amapMcpApiKey = import.meta.env.VITE_ALIYUN_DASHSCOPE_KEY?.trim()
74
-
75
- const mcpServers = [
76
- {
77
- id: 'amap-maps',
78
- name: '高德地图',
79
- description: '覆盖地图、导航、地理编码、天气、路径规划、距离测量、关键词搜索和周边搜索等地理信息服务。',
80
- baseUrl: import.meta.env.VITE_AMAP_MCP_URL?.trim() || 'https://dashscope.aliyuncs.com/api/v1/mcps/amap-maps/mcp',
81
- icon: 'https://img.alicdn.com/imgextra/i4/O1CN01iPPabT1EGRN6uatHP_!!6000000000324-0-tps-512-512.jpg',
82
- headers: amapMcpApiKey ? { Authorization: `Bearer ${amapMcpApiKey}` } : undefined,
83
- },
84
- {
85
- id: 'model-context-protocol-mcp',
86
- name: 'Model Context Protocol MCP',
87
- description: 'MCP Server',
88
- baseUrl: '/modelcontextprotocol-mcp',
89
- installed: true,
90
- },
91
- ] satisfies ChatMcpServers
92
-
93
- const runtime = useLocalChatRuntime({
94
- mcpServers,
95
- modelProviders,
28
+ import { computed, provide, ref } from 'vue'
29
+ import ChatList from './components/ChatList.vue'
30
+ import ChatSender from './components/ChatSender.vue'
31
+ import ConversationHistory from './components/ConversationHistory.vue'
32
+ import HistoryDrawerButton from './components/HistoryDrawerButton.vue'
33
+ import ThemeToggleButton from './components/ThemeToggleButton.vue'
34
+ import { useChat } from './composables/useChat'
35
+ import { useModel } from './composables/useModel'
36
+
37
+ const { activeConversation, messages } = useChat()
38
+ const { hasApiConfig } = useModel()
39
+ const historyDrawerOpen = ref(false)
40
+ provide('historyDrawerOpen', historyDrawerOpen)
41
+
42
+ const isWelcomePage = computed(() => {
43
+ return messages.value.filter((item) => item.role !== 'system').length === 0
96
44
  })
97
45
 
98
- const chatUi = {
99
- layout: {
100
- rightAside: {
101
- mode: 'dock',
102
- width: 400,
103
- defaultOpen: false,
104
- },
105
- },
106
- } satisfies ChatUIOptions
46
+ const currentConversationTitle = computed(() => {
47
+ return activeConversation.value?.title
48
+ })
107
49
  </script>
108
50
 
109
- <template>
110
- <TrTheme>
111
- <div class="chat-basic">
112
- <TrChat :runtime="runtime" :ui="chatUi" />
113
- </div>
114
- </TrTheme>
115
- </template>
116
-
117
51
  <style scoped>
52
+ .app {
53
+ height: 100vh;
54
+ display: flex;
55
+ justify-content: center;
56
+ align-items: stretch;
57
+ padding: 0;
58
+ }
59
+
60
+ .chat-card {
61
+ width: 100%;
62
+ height: 100vh;
63
+ display: flex;
64
+ gap: 0;
65
+ overflow: hidden;
66
+ align-items: stretch;
67
+ flex-direction: row;
68
+ padding: 0;
69
+ margin: 0;
70
+ border-radius: 0;
71
+ background: var(--tr-container-bg-default);
72
+ box-shadow: none;
73
+ }
74
+
75
+ .chat-panel {
76
+ flex: 1;
77
+ min-width: 0;
78
+ display: flex;
79
+ padding: 12px;
80
+ }
81
+
82
+ .chat-panel-content {
83
+ width: 100%;
84
+ max-width: 980px;
85
+ margin: 0 auto;
86
+ display: flex;
87
+ flex-direction: column;
88
+ gap: 12px;
89
+ min-height: 0;
90
+ }
91
+
92
+ .chat-header h3 {
93
+ margin: 0;
94
+ flex: 1;
95
+ min-width: 0;
96
+ white-space: nowrap;
97
+ overflow: hidden;
98
+ text-overflow: ellipsis;
99
+ }
100
+
101
+ .chat-header {
102
+ display: flex;
103
+ align-items: center;
104
+ gap: 8px;
105
+ }
106
+
107
+ .config-warning {
108
+ margin: 0;
109
+ padding: 10px 12px;
110
+ border-radius: 8px;
111
+ background: var(--tr-color-warning-light);
112
+ color: var(--tr-color-warning);
113
+ font-size: 14px;
114
+ }
115
+
118
116
  @media (max-width: 959px) {
119
- .chat-basic :deep(.tr-bubble__avatar) {
120
- display: none;
117
+ .chat-header {
118
+ position: relative;
119
+ justify-content: center;
120
+ min-height: 32px;
121
+ }
122
+
123
+ .chat-header h3 {
124
+ width: 100%;
125
+ text-align: center;
126
+ padding: 0 48px;
127
+ flex: none;
121
128
  }
122
129
  }
123
130
  </style>
@@ -0,0 +1,82 @@
1
+ <template>
2
+ <tr-bubble-provider :fallback-content-renderer="BubbleRenderers.Markdown">
3
+ <tr-welcome
4
+ v-if="visibleMessages.length === 0"
5
+ title="TinyRobot AI 助手"
6
+ description="您好,我是TinyRobot,您专属的 AI 智能专家"
7
+ :icon="welcomeIcon"
8
+ class="chat-list chat-welcome"
9
+ />
10
+ <tr-bubble-list
11
+ v-else
12
+ :messages="messages"
13
+ :role-configs="roles"
14
+ :auto-scroll="true"
15
+ class="chat-list"
16
+ ></tr-bubble-list>
17
+ </tr-bubble-provider>
18
+ </template>
19
+
20
+ <script setup lang="ts">
21
+ import { BubbleRenderers, TrBubbleList, TrBubbleProvider, TrWelcome, type BubbleRoleConfig } from '@opentiny/tiny-robot'
22
+ import { IconAi, IconUser } from '@opentiny/tiny-robot-svgs'
23
+ import { computed, h } from 'vue'
24
+ import { useChat } from '../composables/useChat'
25
+
26
+ const { messages } = useChat()
27
+
28
+ const aiAvatar = h(IconAi, { style: { fontSize: '28px' } })
29
+ const userAvatar = h(IconUser, { style: { fontSize: '28px' } })
30
+ const welcomeIcon = h(IconAi, { style: { fontSize: '40px' } })
31
+ const visibleMessages = computed(() => messages.value.filter((item) => item.role !== 'system'))
32
+
33
+ const roles: Record<string, BubbleRoleConfig> = {
34
+ assistant: {
35
+ placement: 'start',
36
+ avatar: aiAvatar,
37
+ },
38
+ user: {
39
+ placement: 'end',
40
+ avatar: userAvatar,
41
+ },
42
+ system: {
43
+ hidden: true,
44
+ },
45
+ }
46
+ </script>
47
+
48
+ <style scoped>
49
+ .chat-list {
50
+ flex: 1;
51
+ min-height: 0;
52
+ overflow: auto;
53
+ border-radius: 10px;
54
+ padding: 8px;
55
+ }
56
+
57
+ .chat-welcome {
58
+ display: flex;
59
+ align-items: center;
60
+ justify-content: center;
61
+ margin-bottom: 10%;
62
+
63
+ &.tr-welcome {
64
+ --title-color: var(--tr-text-primary);
65
+ --description-color: var(--tr-text-secondary);
66
+ }
67
+ }
68
+
69
+ :deep() {
70
+ [data-box-type='box'][data-role='user'] {
71
+ --tr-bubble-box-bg: var(--tr-color-primary-light);
72
+ }
73
+
74
+ [data-box-type='box']:not([data-role='user']) {
75
+ --tr-bubble-box-bg: transparent;
76
+ }
77
+
78
+ [data-type='markdown'] p {
79
+ margin: 0;
80
+ }
81
+ }
82
+ </style>
@@ -0,0 +1,125 @@
1
+ <template>
2
+ <tr-sender
3
+ v-model="inputMessage"
4
+ mode="multiple"
5
+ :placeholder="isProcessing ? '思考中...' : '请输入你的问题...'"
6
+ :clearable="true"
7
+ :loading="isProcessing"
8
+ :show-word-limit="true"
9
+ :max-length="1000"
10
+ @submit="sendMessage"
11
+ @cancel="abortActiveRequest"
12
+ >
13
+ <template #footer>
14
+ <div class="model-actions">
15
+ <button
16
+ class="sender-action-btn sender-capability-btn"
17
+ :class="{ 'sender-capability-btn--active': thinkingEnabled }"
18
+ type="button"
19
+ :disabled="!supportsThinking"
20
+ @click="thinkingEnabled = !thinkingEnabled"
21
+ >
22
+ <IconDeepThink :size="16" class="sender-action-btn__icon" />
23
+ 深度思考
24
+ </button>
25
+ <button
26
+ class="sender-action-btn sender-capability-btn"
27
+ :class="{ 'sender-capability-btn--active': searchEnabled }"
28
+ type="button"
29
+ :disabled="!supportsSearch"
30
+ @click="searchEnabled = !searchEnabled"
31
+ >
32
+ <IconWebSearch :size="16" class="sender-action-btn__icon" />
33
+ 联网搜索
34
+ </button>
35
+ <McpServerPickerButton />
36
+ <tr-dropdown-menu :items="modelMenuItems" trigger="click" @item-click="handleModelSelect">
37
+ <template #trigger>
38
+ <button class="sender-action-btn sender-model-btn" type="button">
39
+ <component :is="selectedModel?.icon" :size="16" class="sender-action-btn__icon" />
40
+ <span>{{ selectedModel?.name || '选择模型' }}</span>
41
+ </button>
42
+ </template>
43
+ </tr-dropdown-menu>
44
+ </div>
45
+ </template>
46
+ </tr-sender>
47
+ </template>
48
+
49
+ <script setup lang="ts">
50
+ import { TrDropdownMenu, TrSender } from '@opentiny/tiny-robot'
51
+ import { computed } from 'vue'
52
+ import { useChat } from '../composables/useChat'
53
+ import { useModel } from '../composables/useModel'
54
+ import McpServerPickerButton from './McpServerPickerButton.vue'
55
+ import { IconDeepThink, IconWebSearch } from './icons'
56
+
57
+ const { inputMessage, isProcessing, sendMessage, abortActiveRequest } = useChat()
58
+ const {
59
+ modelOptions,
60
+ selectedModel,
61
+ selectedModelId,
62
+ thinkingEnabled,
63
+ searchEnabled,
64
+ supportsThinking,
65
+ supportsSearch,
66
+ } = useModel()
67
+
68
+ const modelMenuItems = computed(() =>
69
+ modelOptions.map((item) => ({
70
+ id: item.id,
71
+ text: item.name,
72
+ })),
73
+ )
74
+
75
+ function handleModelSelect(item: { id?: string }) {
76
+ if (!item.id) {
77
+ return
78
+ }
79
+ selectedModelId.value = item.id
80
+ }
81
+ </script>
82
+
83
+ <style scoped>
84
+ .model-actions {
85
+ display: flex;
86
+ align-items: center;
87
+ gap: 8px;
88
+ }
89
+
90
+ .sender-action-btn {
91
+ border: 1px solid var(--tr-border-color-disabled);
92
+ border-radius: var(--tr-radius-full);
93
+ background: var(--tr-container-bg-default);
94
+ color: var(--tr-text-secondary);
95
+ font-size: var(--tr-font-size-sm);
96
+ height: 32px;
97
+ padding: 0 10px;
98
+ line-height: 1;
99
+ cursor: pointer;
100
+ display: inline-flex;
101
+ align-items: center;
102
+ gap: 6px;
103
+ }
104
+
105
+ .sender-action-btn__icon {
106
+ flex-shrink: 0;
107
+ }
108
+
109
+ .sender-action-btn:hover:not(:disabled) {
110
+ border-color: var(--tr-border-color-hover);
111
+ color: var(--tr-text-primary);
112
+ background: var(--tr-container-bg-hover);
113
+ }
114
+
115
+ .sender-action-btn:disabled {
116
+ cursor: not-allowed;
117
+ opacity: 0.5;
118
+ }
119
+
120
+ .sender-capability-btn--active {
121
+ border-color: var(--tr-border-color-hover);
122
+ color: var(--tr-text-primary);
123
+ background: var(--tr-container-bg-default-2);
124
+ }
125
+ </style>
@@ -0,0 +1,136 @@
1
+ <template>
2
+ <div v-if="historyDrawerOpen" class="history-drawer-backdrop" @click="historyDrawerOpen = false"></div>
3
+ <aside class="history-panel" :class="{ 'history-panel--open': historyDrawerOpen }">
4
+ <div class="history-panel__header">
5
+ <h2>历史会话</h2>
6
+ <button class="new-chat-btn" type="button" @click="activeConversationId = null">新会话</button>
7
+ </div>
8
+ <tr-history
9
+ class="history-panel__list"
10
+ :data="historyData"
11
+ :selected="activeConversationId ?? undefined"
12
+ @item-click="handleItemClick"
13
+ @item-title-change="handleItemTitleChange"
14
+ @item-action="handleItemAction"
15
+ ></tr-history>
16
+ </aside>
17
+ </template>
18
+
19
+ <script setup lang="ts">
20
+ import { TrHistory } from '@opentiny/tiny-robot'
21
+ import { inject, ref, type Ref, watch } from 'vue'
22
+ import { useChat } from '../composables/useChat'
23
+
24
+ type HistoryAction = {
25
+ id: string
26
+ }
27
+
28
+ const { activeConversationId, switchConversation, updateConversationTitle, deleteConversation, conversations } =
29
+ useChat()
30
+ const historyDrawerOpen = inject<Ref<boolean>>('historyDrawerOpen', ref(false))
31
+
32
+ const historyData = ref<Array<{ id: string; title: string }>>([])
33
+
34
+ watch(
35
+ () => conversations.value.length,
36
+ () => {
37
+ const list = conversations.value
38
+ historyData.value = list.map((item) => ({
39
+ id: item.id,
40
+ title: item.title || 'New Chat',
41
+ }))
42
+ },
43
+ { immediate: true },
44
+ )
45
+
46
+ function handleItemClick(item: { id: string }) {
47
+ switchConversation(item.id)
48
+ historyDrawerOpen.value = false
49
+ }
50
+
51
+ function handleItemTitleChange(newTitle: string, item: { id: string }) {
52
+ // Keep local list writable for TrHistory inline editing.
53
+ const target = historyData.value.find((historyItem) => historyItem.id === item.id)
54
+ if (target) {
55
+ target.title = newTitle
56
+ }
57
+ updateConversationTitle(item.id, newTitle)
58
+ }
59
+
60
+ function handleItemAction(action: HistoryAction, item: { id: string }) {
61
+ if (action.id === 'delete') {
62
+ deleteConversation(item.id)
63
+ }
64
+ }
65
+ </script>
66
+
67
+ <style scoped>
68
+ .history-panel {
69
+ width: 300px;
70
+ flex-shrink: 0;
71
+ display: flex;
72
+ flex-direction: column;
73
+ border-right: 1px solid var(--tr-border-color-disabled);
74
+ }
75
+
76
+ .history-panel__header {
77
+ padding: 12px 24px 0 24px;
78
+ display: flex;
79
+ align-items: center;
80
+ justify-content: space-between;
81
+ }
82
+
83
+ .history-panel__header h2 {
84
+ margin: 0;
85
+ font-size: var(--tr-font-size-md);
86
+ }
87
+
88
+ .new-chat-btn {
89
+ border: none;
90
+ border-radius: 6px;
91
+ background: var(--tr-color-primary);
92
+ color: #fff;
93
+ font-size: 12px;
94
+ padding: 6px 10px;
95
+ cursor: pointer;
96
+ }
97
+
98
+ .history-panel__list {
99
+ flex: 1;
100
+ min-height: 0;
101
+ overflow: auto;
102
+ padding: 12px;
103
+ --tr-history-item-selected-bg: var(--tr-history-item-hover-bg);
104
+ --tr-history-item-space-y: 4px;
105
+ }
106
+
107
+ .history-drawer-backdrop {
108
+ display: none;
109
+ }
110
+
111
+ @media (max-width: 959px) {
112
+ .history-drawer-backdrop {
113
+ display: block;
114
+ position: fixed;
115
+ inset: 0;
116
+ z-index: var(--tr-z-index-drawer);
117
+ background: rgba(0, 0, 0, 0.4);
118
+ }
119
+
120
+ .history-panel {
121
+ position: fixed;
122
+ left: 0;
123
+ top: 0;
124
+ bottom: 0;
125
+ width: min(300px, 80vw);
126
+ z-index: calc(var(--tr-z-index-drawer) + 1);
127
+ background: var(--tr-container-bg-default);
128
+ transform: translateX(-100%);
129
+ transition: transform 0.2s ease;
130
+ }
131
+
132
+ .history-panel--open {
133
+ transform: translateX(0);
134
+ }
135
+ }
136
+ </style>
@@ -0,0 +1,43 @@
1
+ <template>
2
+ <button class="history-trigger-btn" type="button" aria-label="打开历史会话抽屉" @click="emit('click')">
3
+ <IconHistory style="font-size: 20px" />
4
+ </button>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import { IconHistory } from '@opentiny/tiny-robot-svgs'
9
+
10
+ const emit = defineEmits<{
11
+ (event: 'click'): void
12
+ }>()
13
+ </script>
14
+
15
+ <style scoped>
16
+ .history-trigger-btn {
17
+ display: none;
18
+ }
19
+
20
+ @media (max-width: 959px) {
21
+ .history-trigger-btn {
22
+ display: inline-flex;
23
+ align-items: center;
24
+ justify-content: center;
25
+ position: absolute;
26
+ left: 0;
27
+ top: 50%;
28
+ transform: translateY(-50%);
29
+ width: 32px;
30
+ height: 32px;
31
+ border: none;
32
+ border-radius: var(--tr-radius-full);
33
+ background: var(--tr-container-bg-default);
34
+ color: var(--tr-text-secondary);
35
+ cursor: pointer;
36
+ }
37
+
38
+ .history-trigger-btn:hover {
39
+ color: var(--tr-icon-color-hover);
40
+ background: var(--tr-container-bg-hover);
41
+ }
42
+ }
43
+ </style>