@stack-spot/ai-chat-widget 0.11.0 → 1.0.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.
Files changed (108) hide show
  1. package/dist/chat-interceptors/quick-commands.d.ts.map +1 -1
  2. package/dist/chat-interceptors/quick-commands.js +23 -22
  3. package/dist/chat-interceptors/quick-commands.js.map +1 -1
  4. package/dist/chat-interceptors/send-message.d.ts.map +1 -1
  5. package/dist/chat-interceptors/send-message.js +14 -10
  6. package/dist/chat-interceptors/send-message.js.map +1 -1
  7. package/dist/components/AdaptiveTextArea.d.ts +1 -1
  8. package/dist/components/AdaptiveTextArea.d.ts.map +1 -1
  9. package/dist/components/AdaptiveTextArea.js +6 -4
  10. package/dist/components/AdaptiveTextArea.js.map +1 -1
  11. package/dist/components/AutoFocus.d.ts +6 -0
  12. package/dist/components/AutoFocus.d.ts.map +1 -0
  13. package/dist/components/AutoFocus.js +15 -0
  14. package/dist/components/AutoFocus.js.map +1 -0
  15. package/dist/components/Fading.d.ts +15 -0
  16. package/dist/components/Fading.d.ts.map +1 -0
  17. package/dist/components/Fading.js +31 -0
  18. package/dist/components/Fading.js.map +1 -0
  19. package/dist/components/FallbackBoundary/ErrorBoundary.d.ts +3 -0
  20. package/dist/components/FallbackBoundary/ErrorBoundary.d.ts.map +1 -1
  21. package/dist/components/FallbackBoundary/ErrorBoundary.js +18 -4
  22. package/dist/components/FallbackBoundary/ErrorBoundary.js.map +1 -1
  23. package/dist/components/FallbackBoundary/Loading.js +1 -1
  24. package/dist/components/FallbackBoundary/Loading.js.map +1 -1
  25. package/dist/components/FallbackBoundary/index.d.ts +6 -1
  26. package/dist/components/FallbackBoundary/index.d.ts.map +1 -1
  27. package/dist/components/FallbackBoundary/index.js +1 -1
  28. package/dist/components/FallbackBoundary/index.js.map +1 -1
  29. package/dist/components/OverlayMenu.d.ts +1 -1
  30. package/dist/components/OverlayMenu.d.ts.map +1 -1
  31. package/dist/components/OverlayMenu.js +26 -9
  32. package/dist/components/OverlayMenu.js.map +1 -1
  33. package/dist/components/RightPanelForm.d.ts.map +1 -1
  34. package/dist/components/RightPanelForm.js +5 -4
  35. package/dist/components/RightPanelForm.js.map +1 -1
  36. package/dist/components/Tooltip/Tooltip.d.ts +3 -1
  37. package/dist/components/Tooltip/Tooltip.d.ts.map +1 -1
  38. package/dist/components/Tooltip/Tooltip.js +14 -5
  39. package/dist/components/Tooltip/Tooltip.js.map +1 -1
  40. package/dist/components/Tooltip/TooltipAPI.d.ts +2 -2
  41. package/dist/components/Tooltip/TooltipAPI.d.ts.map +1 -1
  42. package/dist/components/Tooltip/TooltipAPI.js +51 -51
  43. package/dist/components/Tooltip/TooltipAPI.js.map +1 -1
  44. package/dist/layout.css +5 -0
  45. package/dist/regex.d.ts +2 -0
  46. package/dist/regex.d.ts.map +1 -0
  47. package/dist/regex.js +2 -0
  48. package/dist/regex.js.map +1 -0
  49. package/dist/right-panel/DefaultPanel.d.ts.map +1 -1
  50. package/dist/right-panel/DefaultPanel.js +3 -1
  51. package/dist/right-panel/DefaultPanel.js.map +1 -1
  52. package/dist/right-panel/constants.d.ts +2 -0
  53. package/dist/right-panel/constants.d.ts.map +1 -0
  54. package/dist/right-panel/constants.js +2 -0
  55. package/dist/right-panel/constants.js.map +1 -0
  56. package/dist/right-panel/hooks.d.ts.map +1 -1
  57. package/dist/right-panel/hooks.js +2 -1
  58. package/dist/right-panel/hooks.js.map +1 -1
  59. package/dist/utils/chat.js +1 -1
  60. package/dist/utils/url.d.ts +2 -0
  61. package/dist/utils/url.d.ts.map +1 -0
  62. package/dist/utils/url.js +8 -0
  63. package/dist/utils/url.js.map +1 -0
  64. package/dist/views/Chat/ChatMessage.d.ts.map +1 -1
  65. package/dist/views/Chat/ChatMessage.js +24 -2
  66. package/dist/views/Chat/ChatMessage.js.map +1 -1
  67. package/dist/views/ChatHistory/ChatHistoryPanel.d.ts.map +1 -1
  68. package/dist/views/ChatHistory/ChatHistoryPanel.js +2 -1
  69. package/dist/views/ChatHistory/ChatHistoryPanel.js.map +1 -1
  70. package/dist/views/ChatHistory/HistoryItem.d.ts.map +1 -1
  71. package/dist/views/ChatHistory/HistoryItem.js +10 -1
  72. package/dist/views/ChatHistory/HistoryItem.js.map +1 -1
  73. package/dist/views/MessageInput/QuickCommandSelector.d.ts +6 -0
  74. package/dist/views/MessageInput/QuickCommandSelector.d.ts.map +1 -0
  75. package/dist/views/MessageInput/QuickCommandSelector.js +137 -0
  76. package/dist/views/MessageInput/QuickCommandSelector.js.map +1 -0
  77. package/dist/views/MessageInput/index.d.ts.map +1 -1
  78. package/dist/views/MessageInput/index.js +6 -4
  79. package/dist/views/MessageInput/index.js.map +1 -1
  80. package/dist/views/MessageInput/styled.d.ts.map +1 -1
  81. package/dist/views/MessageInput/styled.js +137 -0
  82. package/dist/views/MessageInput/styled.js.map +1 -1
  83. package/package.json +2 -2
  84. package/src/chat-interceptors/quick-commands.ts +24 -23
  85. package/src/chat-interceptors/send-message.ts +23 -11
  86. package/src/components/AdaptiveTextArea.tsx +9 -4
  87. package/src/components/AutoFocus.tsx +20 -0
  88. package/src/components/Fading.tsx +46 -0
  89. package/src/components/FallbackBoundary/ErrorBoundary.tsx +26 -3
  90. package/src/components/FallbackBoundary/Loading.tsx +1 -1
  91. package/src/components/FallbackBoundary/index.tsx +7 -2
  92. package/src/components/OverlayMenu.tsx +59 -19
  93. package/src/components/RightPanelForm.tsx +12 -9
  94. package/src/components/Tooltip/Tooltip.tsx +24 -5
  95. package/src/components/Tooltip/TooltipAPI.ts +42 -42
  96. package/src/layout.css +5 -0
  97. package/src/regex.ts +1 -0
  98. package/src/right-panel/DefaultPanel.tsx +14 -9
  99. package/src/right-panel/constants.ts +1 -0
  100. package/src/right-panel/hooks.tsx +2 -1
  101. package/src/utils/chat.ts +1 -1
  102. package/src/utils/url.ts +8 -0
  103. package/src/views/Chat/ChatMessage.tsx +29 -5
  104. package/src/views/ChatHistory/ChatHistoryPanel.tsx +3 -2
  105. package/src/views/ChatHistory/HistoryItem.tsx +11 -2
  106. package/src/views/MessageInput/QuickCommandSelector.tsx +210 -0
  107. package/src/views/MessageInput/index.tsx +8 -4
  108. package/src/views/MessageInput/styled.ts +137 -0
@@ -1,6 +1,7 @@
1
1
  import { IconBox, Input } from '@citric/core'
2
2
  import { Check, Download, EllipsisHorizontal, Pencil, Trash } from '@citric/icons'
3
3
  import { IconButton, LoadingCircular } from '@citric/ui'
4
+ import { focusNextIgnoringChildren } from '@stack-spot/portal-components'
4
5
  import { aiClient } from '@stack-spot/portal-network'
5
6
  import { ConversationResponse } from '@stack-spot/portal-network/api/ai'
6
7
  import { theme } from '@stack-spot/portal-theme'
@@ -26,6 +27,7 @@ export const HistoryItem = ({ item, interceptors }: { item: ConversationResponse
26
27
  const [isDeleted, setDeleted] = useState(false)
27
28
  const renameInput = useRef<HTMLInputElement>(null)
28
29
  const widget = useWidget()
30
+ const overlayRef = useRef<HTMLDivElement>(null)
29
31
 
30
32
  useEffect(() => {
31
33
  if (isRenaming) renameInput.current?.focus()
@@ -52,6 +54,7 @@ export const HistoryItem = ({ item, interceptors }: { item: ConversationResponse
52
54
  }
53
55
 
54
56
  const onDownload = useCallback(async () => {
57
+ setTimeout(() => focusNextIgnoringChildren(overlayRef.current), 10)
55
58
  setLoading(true)
56
59
  try {
57
60
  const content = await aiClient.downloadChat.mutate({ conversationId: item.id })
@@ -65,6 +68,7 @@ export const HistoryItem = ({ item, interceptors }: { item: ConversationResponse
65
68
 
66
69
  const onDelete = useCallback(async () => {
67
70
  setDeleted(true)
71
+ setTimeout(() => overlayRef.current?.focus(), 10)
68
72
  try {
69
73
  await aiClient.deleteChat.mutate({ conversationId: item.id })
70
74
  aiClient.chats.invalidate()
@@ -124,14 +128,19 @@ export const HistoryItem = ({ item, interceptors }: { item: ConversationResponse
124
128
  ref={renameInput}
125
129
  value={renamed}
126
130
  onChange={e => setRenamed(e.target.value)}
127
- onKeyDown={e => e.key === 'Enter' && onSubmitRename()}
131
+ onKeyDown={(e) => {
132
+ if (['Enter', 'Escape'].includes(e.key)) {
133
+ e.key === 'Enter' ? onSubmitRename() : setRenaming(false)
134
+ setTimeout(() => overlayRef.current?.focus(), 10)
135
+ }
136
+ }}
128
137
  />
129
138
  <IconButton onClick={onSubmitRename}><Check /></IconButton>
130
139
  </>
131
140
  ) : (
132
141
  <>
133
142
  <button className="label" onClick={onSelect} disabled={isLoading}>{title}</button>
134
- {isLoading ? <LoadingCircular size="xs" /> : <OverlayMenu actions={actions} position="left">
143
+ {isLoading ? <LoadingCircular size="xs" /> : <OverlayMenu actions={actions} position="left" ref={overlayRef}>
135
144
  <IconBox><EllipsisHorizontal /></IconBox>
136
145
  </OverlayMenu>}
137
146
  </>
@@ -0,0 +1,210 @@
1
+ import { IconBox, Text } from '@citric/core'
2
+ import { ExternalLink, QuickCommand } from '@citric/icons'
3
+ import { IconButton } from '@citric/ui'
4
+ import { useKeyboardControls } from '@stack-spot/portal-components'
5
+ import { aiClient } from '@stack-spot/portal-network'
6
+ import { QuickCommandListResponse, VisibilityLevelEnum } from '@stack-spot/portal-network/api/ai'
7
+ import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
8
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
9
+ import { Fading } from '../../components/Fading'
10
+ import { FallbackBoundary } from '../../components/FallbackBoundary'
11
+ import { useCurrentChat, useCurrentChatState } from '../../context/hooks'
12
+ import { quickCommandRegex } from '../../regex'
13
+ import { getUrlToStackSpotAI } from '../../utils/url'
14
+
15
+ interface Props {
16
+ inputRef: React.RefObject<HTMLTextAreaElement | HTMLInputElement>,
17
+ }
18
+
19
+ interface ContentProps extends Props {
20
+ filter?: string,
21
+ onClose: () => void,
22
+ }
23
+
24
+ interface ListProps {
25
+ filter?: string,
26
+ visibility?: VisibilityLevelEnum,
27
+ onSelect: (slug: string) => void,
28
+ }
29
+
30
+ interface ItemProps {
31
+ qc: QuickCommandListResponse,
32
+ onSelect: (slug: string) => void,
33
+ }
34
+
35
+ const sections = [undefined, 'personal', 'workspace', 'account', 'shared'] as const
36
+
37
+ const CommandListItem = ({ qc, onSelect }: ItemProps) => {
38
+ const t = useTranslate(dictionary)
39
+ return (
40
+ <li>
41
+ <button
42
+ className="qc"
43
+ onClick={() => onSelect(qc.slug)}
44
+ // the following line prevents a new line character in the message when the user presses enter to select a qc.
45
+ onKeyDown={e => e.key === 'Enter' && e.preventDefault()}
46
+ onFocus={e => e.target.closest('li')?.classList.add('focus')}
47
+ onBlur={e => e.target.closest('li')?.classList.remove('focus')}
48
+ >
49
+ <p className="qc-title">/{qc.slug}</p>
50
+ <p className="qc-description">{qc.description}</p>
51
+ </button>
52
+ <IconButton as="a" title={t.openQC} aria-label={t.openQC} href={`${getUrlToStackSpotAI()}/quick-command/${qc.slug}`} target="_blank">
53
+ <ExternalLink />
54
+ </IconButton>
55
+ </li>
56
+ )
57
+ }
58
+
59
+ const CommandList = ({ filter, visibility, onSelect }: ListProps) => {
60
+ const t = useTranslate(dictionary)
61
+ const quickCommands = aiClient.quickCommands.useQuery({ order: 'a-to-z' })
62
+ let filtered = quickCommands
63
+
64
+ if (visibility || filter) {
65
+ const lowerFilter = filter?.toLocaleLowerCase()
66
+ filtered = quickCommands.filter(
67
+ qc => (!lowerFilter || qc.slug.toLocaleLowerCase().startsWith(lowerFilter)) && (!visibility || qc.visibility_level === visibility),
68
+ )
69
+ }
70
+ if (!quickCommands.length) return <Text className="empty" colorScheme="light.700">{t.noData}</Text>
71
+ if (!filtered.length) return <Text className="empty" colorScheme="light.700">{t.noResults}</Text>
72
+ return (
73
+ <ul className="command-list">
74
+ {filtered.map(qc => <CommandListItem key={qc.id} qc={qc} onSelect={onSelect} />)}
75
+ </ul>
76
+ )
77
+ }
78
+
79
+ const SelectorContent = ({ filter, onClose, inputRef }: ContentProps) => {
80
+ const t = useTranslate(dictionary)
81
+ const ref = useRef<HTMLDivElement>(null)
82
+ const chat = useCurrentChat()
83
+ const [visibility, setVisibility] = useState<VisibilityLevelEnum | undefined>()
84
+
85
+ const onSelectQC = useCallback((slug: string) => {
86
+ const newValue = `/${slug}`
87
+ chat.set('nextMessage', newValue)
88
+ onClose()
89
+ if (!inputRef.current) return
90
+ // the following line prevents bugs by setting the text area value before react gets the chance to.
91
+ inputRef.current.value = newValue
92
+ inputRef.current.focus()
93
+ }, [])
94
+
95
+ useKeyboardControls({
96
+ querySelectors: '.tabs button, button.qc',
97
+ disableTabBehavior: true,
98
+ onPressEscape: onClose,
99
+ onPressArrowLeft: () => (ref.current?.querySelector('.tabs button.active') as HTMLElement)?.focus(),
100
+ onPressArrowRight: () => (ref.current?.querySelector('button.qc') as HTMLElement)?.focus(),
101
+ ref,
102
+ }, [])
103
+
104
+ function createSectionItem(action: VisibilityLevelEnum | undefined) {
105
+ return (
106
+ <li key={action ?? 'all'}>
107
+ <button className={visibility === action ? 'active' : ''} onFocus={() => setVisibility(action)}>
108
+ {t[action || 'all']}
109
+ </button>
110
+ </li>
111
+ )
112
+ }
113
+
114
+ return (
115
+ <div ref={ref}>
116
+ <header>
117
+ <IconBox><QuickCommand /></IconBox>
118
+ <Text as="h3">QUICK COMMANDS</Text>
119
+ </header>
120
+ <div className="body">
121
+ <ul className="tabs">{sections.map(createSectionItem)}</ul>
122
+ <FallbackBoundary message={t.error} mini>
123
+ <CommandList onSelect={onSelectQC} filter={filter} visibility={visibility} />
124
+ </FallbackBoundary>
125
+ </div>
126
+ </div>
127
+ )
128
+ }
129
+
130
+ export const QuickCommandSelector = ({ inputRef }: Props) => {
131
+ const value = useCurrentChatState('nextMessage') ?? ''
132
+ const filter = useMemo(() => value === '/' || quickCommandRegex.test(value) ? value.substring(1) : undefined, [value])
133
+ const [isClosed, setClosed] = useState(false)
134
+ const selectorRef = useRef<HTMLDivElement>(null)
135
+ const shouldRender = filter !== undefined && !isClosed
136
+
137
+ // Resets the closed state whenever the message input is cleared
138
+ useEffect(() => {
139
+ if (!value) setClosed(false)
140
+ }, [value])
141
+
142
+ // Creates the following behavior while the user types in the message input:
143
+ // auto-complete on tab; move focus to the qc panel on press up or down; and close the qc panel on esc.
144
+ useEffect(() => {
145
+ function getFirst() {
146
+ return selectorRef.current?.querySelector('.qc') as HTMLElement | null
147
+ }
148
+
149
+ function onKeyDown(event: Event) {
150
+ const key = (event as KeyboardEvent).key
151
+ if (!selectorRef.current) return
152
+ if (key === 'Tab') {
153
+ getFirst()?.click()
154
+ event.preventDefault()
155
+ }
156
+ else if (key === 'ArrowDown' || key === 'ArrowUp') {
157
+ getFirst()?.focus()
158
+ event.preventDefault()
159
+ }
160
+ if (key === 'Escape') {
161
+ setClosed(true)
162
+ }
163
+ }
164
+
165
+ inputRef.current?.addEventListener('keydown', onKeyDown)
166
+ return () => inputRef.current?.removeEventListener('keydown', onKeyDown)
167
+ }, [])
168
+
169
+ // Closes the panel when the user clicks outside the qc panel or the message input.
170
+ useEffect(() => {
171
+ if (!shouldRender) return
172
+ function onClickOut(e: Event) {
173
+ const target = e.target as HTMLElement | null
174
+ if (!selectorRef.current?.contains(target) && !inputRef.current?.contains(target)) setClosed(true)
175
+ }
176
+ document.addEventListener('click', onClickOut)
177
+ return () => document.removeEventListener('click', onClickOut)
178
+ }, [shouldRender])
179
+
180
+ return (
181
+ <Fading visible={shouldRender} ref={selectorRef} className="quick-command-selector">
182
+ <SelectorContent filter={filter} onClose={() => setClosed(true)} inputRef={inputRef} />
183
+ </Fading>
184
+ )
185
+ }
186
+
187
+ const dictionary = {
188
+ en: {
189
+ all: 'All',
190
+ personal: 'Personal',
191
+ account: 'Account',
192
+ shared: 'Shared',
193
+ workspace: 'Workspace',
194
+ error: 'Could not load the quick commands.',
195
+ noData: 'You don\'t have any quick command yet.',
196
+ noResults: 'There are no quick commands to show here.',
197
+ openQC: 'Open this quick command\'s settings in a new tab.',
198
+ },
199
+ pt: {
200
+ all: 'Todos',
201
+ personal: 'Pessoal',
202
+ account: 'Conta',
203
+ shared: 'Compartilhado',
204
+ workspace: 'Workspace',
205
+ error: 'Não foi possível carregar os quick commands.',
206
+ noData: 'Você ainda não possui quick commands.',
207
+ noResults: 'Não há quick commands para mostrar aqui.',
208
+ openQC: 'Abra as configurações deste quick command em uma nova aba.',
209
+ },
210
+ } satisfies Dictionary
@@ -4,10 +4,12 @@ import { AdaptiveTextArea } from '../../components/AdaptiveTextArea'
4
4
  import { ProgressBar } from '../../components/ProgressBar'
5
5
  import { useCurrentChat, useCurrentChatState, useWidgetState } from '../../context/hooks'
6
6
  import { MessageInputFeatures } from '../../features'
7
+ import { quickCommandRegex } from '../../regex'
7
8
  import { ChatEntry } from '../../state/ChatEntry'
8
9
  import { ButtonGroup } from './ButtonGroup'
9
10
  import { useMessageInputDictionary } from './dictionary'
10
11
  import { InfoBar } from './InfoBar'
12
+ import { QuickCommandSelector } from './QuickCommandSelector'
11
13
  import { MAX_INPUT_HEIGHT, MessageInputBox, MIN_INPUT_HEIGHT } from './styled'
12
14
 
13
15
  interface Props {
@@ -23,14 +25,14 @@ export const MessageInput = ({ features }: Props) => {
23
25
  const isLoading = useCurrentChatState('isLoading') ?? false
24
26
  const value = useCurrentChatState('nextMessage') ?? ''
25
27
  const isMinimized = useWidgetState('isMinimized')
26
- const elementRef = useRef<HTMLDivElement>(null)
28
+ const textAreaRef = useRef<HTMLTextAreaElement>(null)
27
29
 
28
30
  const onSend = useCallback(async () => {
29
31
  const message = chat.get('nextMessage')
30
32
  if (!message) return
31
33
  const code = chat.get('codeSelection')
32
34
  const language = chat.get('codeLanguage')
33
- const prompt = code && !message.startsWith('/') ? `${message}\n\`\`\`${language}\n${code}\n\`\`\`` : message
35
+ const prompt = code && !quickCommandRegex.test(message) ? `${message}\n\`\`\`${language}\n${code}\n\`\`\`` : message
34
36
  chat.pushMessage(ChatEntry.createUserEntry(prompt, true))
35
37
  chat.set('nextMessage', '')
36
38
  setFocused(false)
@@ -44,15 +46,17 @@ export const MessageInput = ({ features }: Props) => {
44
46
  }, [onSend])
45
47
 
46
48
  useEffect(() => {
47
- if (!isLoading) elementRef.current?.querySelector('textarea')?.focus()
49
+ if (!isLoading) textAreaRef.current?.focus()
48
50
  }, [isLoading])
49
51
 
50
52
  return (
51
- <MessageInputBox ref={elementRef} aria-busy={isLoading} className="message-input">
53
+ <MessageInputBox aria-busy={isLoading} className="message-input">
52
54
  <ProgressBar visible={isLoading} shimmer />
53
55
  <InfoBar />
54
56
  <div className={listToClass(['action-box', focused && 'focused', isLoading && 'disabled'])}>
57
+ <QuickCommandSelector inputRef={textAreaRef} />
55
58
  <AdaptiveTextArea
59
+ ref={textAreaRef}
56
60
  disabled={isLoading}
57
61
  placeholder={t.placeholder}
58
62
  onChange={e => chat.set('nextMessage', e.target.value)}
@@ -82,6 +82,7 @@ export const MessageInputBox = styled.div`
82
82
 
83
83
  .action-box {
84
84
  display: flex;
85
+ position: relative;
85
86
  flex-direction: row;
86
87
  gap: 4px;
87
88
  align-items: end;
@@ -201,4 +202,140 @@ export const MessageInputBox = styled.div`
201
202
  box-shadow: none;
202
203
  }
203
204
  }
205
+
206
+ .quick-command-selector {
207
+ position: absolute;
208
+ border-radius: 4px;
209
+ border: 1px solid ${theme.color.light[600]};
210
+ background-color: ${theme.color.light[500]};
211
+ box-shadow: 0px 2px 16px 0px #0000005C;
212
+ display: flex;
213
+ flex-direction: column;
214
+ width: 480px;
215
+ bottom: 55px;
216
+
217
+ .loading, .error {
218
+ padding-bottom: 26px;
219
+ p {
220
+ width: 200px;
221
+ text-align: center;
222
+ line-height: 20px;
223
+ }
224
+ }
225
+
226
+ .empty {
227
+ padding-bottom: 26px;
228
+ width: 200px;
229
+ text-align: center;
230
+ line-height: 20px;
231
+ margin: auto;
232
+ }
233
+
234
+ header {
235
+ display: flex;
236
+ flex-direction: row;
237
+ gap: 8px;
238
+ align-items: center;
239
+ padding: 8px;
240
+ font-family: 'San Francisco';
241
+ font-weight: 500;
242
+ font-size: 11px;
243
+ }
244
+
245
+ .body {
246
+ display: flex;
247
+ flex-direction: row;
248
+ align-items: center;
249
+ }
250
+
251
+ ul {
252
+ margin: 0;
253
+ padding: 0;
254
+ list-style: none;
255
+ }
256
+
257
+ ul.tabs {
258
+ display: flex;
259
+ flex-direction: column;
260
+
261
+ li {
262
+ display: flex;
263
+ flex-direction: column;
264
+ }
265
+
266
+ button {
267
+ box-sizing: border-box;
268
+ color: ${theme.color.light[700]};
269
+ text-align: left;
270
+ padding: 10px;
271
+ font-weight: 600;
272
+ font-size: 12px;
273
+ transition: background-color 0.3s;
274
+ border-top-right-radius: 4px;
275
+ border-bottom-right-radius: 4px;
276
+ background-color: transparent;
277
+ border: none;
278
+ cursor: pointer;
279
+ outline: none;
280
+
281
+ &:hover, &.active, &:focus {
282
+ background-color: ${theme.color.light[600]};
283
+ }
284
+
285
+ &.active {
286
+ border-left: 1px solid ${theme.color.light.contrastText};
287
+ color: ${theme.color.light.contrastText};
288
+ }
289
+ }
290
+ }
291
+
292
+ ul.command-list {
293
+ align-self: stretch;
294
+ display: flex;
295
+ flex-direction: column;
296
+ gap: 2px;
297
+ overflow-y: auto;
298
+ flex: 1;
299
+ max-height: 170px;
300
+
301
+ li {
302
+ display: flex;
303
+ flex-direction: row;
304
+ align-items: center;
305
+ gap: 8px;
306
+ padding: 8px;
307
+ border-radius: 4px;
308
+
309
+ &:hover, &.focus {
310
+ background-color: ${theme.color.light[600]};
311
+ }
312
+
313
+ button.qc {
314
+ flex: 1;
315
+ border: none;
316
+ text-align: left;
317
+ background-color: transparent;
318
+ text-align: left;
319
+ outline: none;
320
+ overflow: hidden;
321
+ cursor: pointer;
322
+
323
+ .qc-title {
324
+ font-size: 11px;
325
+ margin: 0 0 4px 0;
326
+ color: ${theme.color.light.contrastText};
327
+ text-transform: uppercase;
328
+ text-overflow: ellipsis;
329
+ overflow: hidden;
330
+ }
331
+
332
+ .qc-description {
333
+ color: ${theme.color.light[700]};
334
+ font-size: 12px;
335
+ margin: 0;
336
+ }
337
+ }
338
+ }
339
+ }
340
+ }
204
341
  `