@lijian-ui/dsh-term 0.1.2 → 0.3.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 (39) hide show
  1. package/lib/client.js +698 -43875
  2. package/lib/client.js.map +1 -1
  3. package/lib/index.js +455 -30
  4. package/lib/tsconfig.client.tsbuildinfo +1 -1
  5. package/lib/tsconfig.host.tsbuildinfo +1 -1
  6. package/lib/types/client/client-i18n.d.ts +29 -0
  7. package/lib/types/client/client-i18n.d.ts.map +1 -0
  8. package/lib/types/client/i18n-seat.d.ts +15 -0
  9. package/lib/types/client/i18n-seat.d.ts.map +1 -0
  10. package/lib/types/client/index.d.ts +7 -0
  11. package/lib/types/client/index.d.ts.map +1 -1
  12. package/lib/types/client/term/AnimatedDock.d.ts +10 -20
  13. package/lib/types/client/term/AnimatedDock.d.ts.map +1 -1
  14. package/lib/types/client/term/DockItem.d.ts +15 -0
  15. package/lib/types/client/term/DockItem.d.ts.map +1 -0
  16. package/lib/types/client/term/TerminalPanel.d.ts +8 -3
  17. package/lib/types/client/term/TerminalPanel.d.ts.map +1 -1
  18. package/lib/types/core/types.d.ts +1 -0
  19. package/lib/types/core/types.d.ts.map +1 -1
  20. package/lib/types/gateway/i18n.d.ts +17 -0
  21. package/lib/types/gateway/i18n.d.ts.map +1 -0
  22. package/lib/types/host/routes.d.ts +3 -1
  23. package/lib/types/host/routes.d.ts.map +1 -1
  24. package/lib/types/index.d.ts.map +1 -1
  25. package/package.json +1 -1
  26. package/src/client/client-i18n.ts +68 -0
  27. package/src/client/i18n-seat.ts +27 -0
  28. package/src/client/index.ts +139 -46
  29. package/src/client/term/AnimatedDock.tsx +27 -114
  30. package/src/client/term/DockItem.tsx +42 -0
  31. package/src/client/term/TerminalPanel.tsx +339 -32
  32. package/src/client/term/api.ts +17 -2
  33. package/src/client/term/chat-helper.ts +28 -0
  34. package/src/client/term/term.module.css +137 -0
  35. package/src/core/types.ts +22 -4
  36. package/src/gateway/i18n.ts +67 -0
  37. package/src/host/pty-service.ts +160 -17
  38. package/src/host/routes.ts +44 -4
  39. package/src/index.ts +31 -1
@@ -29,12 +29,22 @@ import type {} from '@deepseek-ai/dsh-client-locale/client'
29
29
  // `conversation.session.header.utilities`) onto the SlotMap so we can register
30
30
  // the header tool-dock against the official, typed slot.
31
31
  import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
32
+
33
+ /** Augment the locale namespace map so ctx.locale.register/bind accept 'dsh-term'. */
34
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
35
+ interface LocaleNamespaceMap {
36
+ /** Terminal panel copy. */
37
+ 'dsh-term': 'ui.panel.title' | 'ui.panel.addTabTitle' | 'ui.panel.collapseTitle' | 'ui.panel.emptyHint' | 'ui.tab.closeAria' | 'msg.sessionExited' | 'msg.spawnFailed' | 'ui.dock.label' | 'ui.panel.shellTitle' | 'ui.shell.bash' | 'ui.shell.zsh' | 'ui.shell.powershell' | 'ui.shell.cmd' | 'ui.shell.gitbash' | 'ui.panel.reopenTitle' | 'ui.panel.backgroundSessions' | 'ui.panel.noBackground' | 'ui.panel.addToChat'
38
+ }
39
+ }
40
+ import { zh as clientZh, en as clientEn } from './client-i18n.ts'
32
41
  import { TermApi } from './term/api.ts'
33
42
  import { TerminalPanel } from './term/TerminalPanel.tsx'
34
43
  import { AnimatedDock } from './term/AnimatedDock.tsx'
44
+ import { bindI18n } from './i18n-seat.ts'
35
45
 
36
- /** Required services: sessions (for the workspace cwd). */
37
- export const inject = ['sessions']
46
+ /** Required services: sessions (for the workspace cwd), conversation (for add-to-chat). */
47
+ export const inject = ['sessions', 'locale', 'slots', 'conversation']
38
48
 
39
49
  /** Cross-plugin event names shared with the AnimatedDock header group. */
40
50
  const EV = {
@@ -42,8 +52,36 @@ const EV = {
42
52
  terminalState: 'dsh-dock:terminal-state',
43
53
  } as const
44
54
 
45
- /** Width of the docked terminal column, in px (the 6th grid track). */
46
- const TERMINAL_WIDTH = 300
55
+ /** Terminal column width bounds and persistence. */
56
+ const MIN_TERM_WIDTH = 200
57
+ const MAX_TERM_WIDTH = 600
58
+ const DEFAULT_TERM_WIDTH = 300
59
+ const TERM_WIDTH_KEY = 'dsh-term-width-px'
60
+ const TERM_HANDLE_WIDTH = 8
61
+
62
+ function readTermWidth(): number {
63
+ try {
64
+ const raw = localStorage.getItem(TERM_WIDTH_KEY)
65
+ if (raw === null) return DEFAULT_TERM_WIDTH
66
+ const v = Number(raw)
67
+ if (!Number.isFinite(v) || v < MIN_TERM_WIDTH || v > MAX_TERM_WIDTH) return DEFAULT_TERM_WIDTH
68
+ return v
69
+ } catch { return DEFAULT_TERM_WIDTH }
70
+ }
71
+
72
+ function writeTermWidth(v: number): void {
73
+ try { localStorage.setItem(TERM_WIDTH_KEY, String(Math.round(v))) } catch { /* best-effort */ }
74
+ }
75
+
76
+ /** Inject the drag-handle visual styles once. */
77
+ function adoptHandleStyles(): void {
78
+ const id = 'dsh-term-handle-style'
79
+ if (document.getElementById(id) !== null) return
80
+ const tag = document.createElement('style')
81
+ tag.id = id
82
+ tag.textContent = '.dsh-term-handle{touch-action:none;cursor:col-resize}'
83
+ document.head.appendChild(tag)
84
+ }
47
85
 
48
86
  /** Locate the frame grid (same heuristic file-manager uses). */
49
87
  function findFrame(): HTMLElement | null {
@@ -73,34 +111,6 @@ function whenFrameReady(cb: (frame: HTMLElement) => void): () => void {
73
111
  return () => obs.disconnect()
74
112
  }
75
113
 
76
- /** Inject the launcher button styles once (the button is a raw DOM node). */
77
- function adoptLauncherStyles(): void {
78
- const id = 'dsh-term-launcher-style'
79
- if (document.getElementById(id) !== null) return
80
- const tag = document.createElement('style')
81
- tag.id = id
82
- tag.textContent = `
83
- .dsh-term-launcher {
84
- position: fixed;
85
- right: 12px;
86
- bottom: 12px;
87
- z-index: 40;
88
- height: 30px;
89
- padding: 0 14px;
90
- border-radius: 6px;
91
- border: 1px solid var(--aion-bg-3, #e5e6eb);
92
- background: var(--aion-bg-1, #ffffff);
93
- color: var(--aion-fg-1, #1f2329);
94
- font-size: 13px;
95
- font-family: var(--aion-font-sans, -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif);
96
- cursor: pointer;
97
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
98
- }
99
- .dsh-term-launcher:hover {
100
- background: var(--aion-bg-2, #f2f3f5);
101
- }`
102
- document.head.appendChild(tag)
103
- }
104
114
 
105
115
  /**
106
116
  * Parse a `grid-template-columns` string into raw tokens. Named lines
@@ -143,6 +153,15 @@ function stripTerminalTrack(tokens: readonly string[]): string[] {
143
153
 
144
154
  /** Apply the browser half. */
145
155
  export function apply(ctx: ClientContext): void {
156
+ // Register client-side bilingual dictionaries.
157
+ const I18N_NS = 'dsh-term'
158
+ ctx.effect(
159
+ () => ctx.locale.register(I18N_NS, { zh: clientZh, en: clientEn }),
160
+ 'dsh-term: client dictionaries'
161
+ )
162
+ const t = ctx.locale.bind(I18N_NS)
163
+ bindI18n(t) // publish to module-level seat for use by components outside slots
164
+
146
165
  // 会话页 header 工具坞:紧贴「Session log」左侧的放大按钮组
147
166
  ctx.inject(['slots'], (scope: ClientContext) => {
148
167
  scope.slots.inject('conversation.session.header.utilities', () =>
@@ -165,6 +184,7 @@ export function apply(ctx: ClientContext): void {
165
184
  col.dataset.dshTermCol = ''
166
185
  col.style.minWidth = '0'
167
186
  col.style.height = '100%'
187
+ col.style.display = 'none'
168
188
  frame.appendChild(col)
169
189
 
170
190
  // Keep the terminal the rightmost column even if file-manager appends
@@ -174,18 +194,24 @@ export function apply(ctx: ClientContext): void {
174
194
  })
175
195
  keepLast.observe(frame, { childList: true })
176
196
 
177
- // Re-open button (after a collapse). Hidden while the column is open.
178
- adoptLauncherStyles()
179
- const launcher = document.createElement('button')
180
- launcher.type = 'button'
181
- launcher.className = 'dsh-term-launcher'
182
- launcher.textContent = '终端'
183
- launcher.setAttribute('aria-label', '打开终端')
184
- launcher.style.display = 'none'
185
- document.body.appendChild(launcher)
186
197
 
187
198
  // Open/closed state. Closed by default (the column is a docked track).
188
199
  let terminalOpen = false
200
+ let terminalWidth = readTermWidth()
201
+
202
+ // The drag handle on the terminal column's left edge (drag left = wider).
203
+ adoptHandleStyles()
204
+ const handle = document.createElement('div')
205
+ handle.className = 'dsh-term-handle'
206
+ handle.style.position = 'absolute'
207
+ handle.style.top = '0'
208
+ handle.style.bottom = '0'
209
+ handle.style.width = `${TERM_HANDLE_WIDTH}px`
210
+ handle.style.marginLeft = `${-TERM_HANDLE_WIDTH / 2}px`
211
+ handle.style.zIndex = '30'
212
+ handle.style.cursor = 'col-resize'
213
+ handle.style.display = 'none'
214
+ frame.appendChild(handle)
189
215
 
190
216
  /**
191
217
  * Re-assert the frame grid: keep file-manager's tracks, append our
@@ -205,11 +231,19 @@ export function apply(ctx: ClientContext): void {
205
231
  // (e.g. the shell's own 3-track write landed before file-manager
206
232
  // re-applied). Touching it now would desync file-manager's width math.
207
233
  if (tokens.length !== baseLen) return
208
- if (terminalOpen) tokens.push('[dsh-term]', `${TERMINAL_WIDTH}px`)
234
+ if (terminalOpen) tokens.push('[dsh-term]', `${terminalWidth}px`)
209
235
  const next = tokens.join(' ')
210
236
  if (next !== frame.style.gridTemplateColumns) {
211
237
  frame.style.gridTemplateColumns = next
212
238
  }
239
+ // Keep the drag handle glued to the terminal column's left edge.
240
+ if (terminalOpen) {
241
+ const frameRect = frame.getBoundingClientRect()
242
+ const colRect = col.getBoundingClientRect()
243
+ const leftEdge = colRect.left - frameRect.left
244
+ handle.style.left = `${Math.round(leftEdge)}px`
245
+ }
246
+ handle.style.display = terminalOpen ? 'block' : 'none'
213
247
  }
214
248
 
215
249
  // Re-assert after file-manager rewrites the grid (drag / resize / collapse).
@@ -223,14 +257,73 @@ export function apply(ctx: ClientContext): void {
223
257
  const setVisible = (open: boolean): void => {
224
258
  terminalOpen = open
225
259
  col.style.display = open ? 'flex' : 'none'
226
- launcher.style.display = open ? 'none' : 'flex'
227
260
  reconcileGrid()
228
261
  window.dispatchEvent(new CustomEvent(EV.terminalState, { detail: open }))
229
262
  }
230
263
  const onToggleTerminal = (): void => setVisible(!terminalOpen)
231
264
  window.addEventListener(EV.toggleTerminal, onToggleTerminal)
232
- launcher.addEventListener('click', () => setVisible(true))
233
- root.render(createElement(TerminalPanel, { ctx, api, onClose: () => setVisible(false) }))
265
+
266
+ // Drag the handle to resize the terminal column (left = wider).
267
+ handle.addEventListener('pointerdown', (event: PointerEvent): void => {
268
+ if (event.button !== 0) return
269
+ event.preventDefault()
270
+ handle.setPointerCapture(event.pointerId)
271
+ const startX = event.clientX
272
+ const startWidth = terminalWidth
273
+ let rafId: number | null = null
274
+ let pendingWidth: number | null = null
275
+ let latestWidth = startWidth
276
+
277
+ document.body.style.userSelect = 'none'
278
+ document.body.style.cursor = 'col-resize'
279
+ frame.setAttribute('data-dragging', '')
280
+ handle.setAttribute('data-dragging', '')
281
+
282
+ const flush = (): void => {
283
+ if (pendingWidth === null) return
284
+ latestWidth = pendingWidth
285
+ terminalWidth = pendingWidth
286
+ reconcileGrid()
287
+ }
288
+
289
+ const computeWidth = (clientX: number): number => {
290
+ const deltaX = startX - clientX
291
+ return Math.min(MAX_TERM_WIDTH, Math.max(MIN_TERM_WIDTH, startWidth + deltaX))
292
+ }
293
+
294
+ const finish = (clientX: number | null): void => {
295
+ handle.removeEventListener('pointermove', onMove)
296
+ handle.removeEventListener('pointerup', onUp)
297
+ handle.removeEventListener('pointercancel', onCancel)
298
+ try { handle.releasePointerCapture(event.pointerId) } catch { /* already released */ }
299
+ document.body.style.userSelect = ''
300
+ document.body.style.cursor = ''
301
+ frame.removeAttribute('data-dragging')
302
+ handle.removeAttribute('data-dragging')
303
+ if (rafId !== null) { cancelAnimationFrame(rafId); rafId = null }
304
+ flush()
305
+ const finalWidth = clientX === null ? latestWidth : computeWidth(clientX)
306
+ terminalWidth = finalWidth
307
+ reconcileGrid()
308
+ writeTermWidth(finalWidth)
309
+ }
310
+
311
+ const onMove = (e: PointerEvent): void => {
312
+ if (e.buttons === 0) { finish(e.clientX); return }
313
+ pendingWidth = computeWidth(e.clientX)
314
+ if (rafId === null) {
315
+ rafId = requestAnimationFrame(() => { rafId = null; flush() })
316
+ }
317
+ }
318
+ const onUp = (e: PointerEvent): void => finish(e.clientX)
319
+ const onCancel = (): void => finish(null)
320
+
321
+ handle.addEventListener('pointermove', onMove)
322
+ handle.addEventListener('pointerup', onUp)
323
+ handle.addEventListener('pointercancel', onCancel)
324
+ })
325
+
326
+ root.render(createElement(TerminalPanel, { ctx, api, onClose: () => setVisible(false), t }))
234
327
 
235
328
  disposeFrame = () => {
236
329
  keepLast.disconnect()
@@ -238,7 +331,7 @@ export function apply(ctx: ClientContext): void {
238
331
  window.removeEventListener(EV.toggleTerminal, onToggleTerminal)
239
332
  root.unmount()
240
333
  col.remove()
241
- launcher.remove()
334
+ handle.remove()
242
335
  }
243
336
  })
244
337
  return () => {
@@ -1,132 +1,45 @@
1
1
  /**
2
- * AnimatedDock — a macOS-style magnification button group rendered into the
3
- * conversation session header (the `conversation.session.header.utilities`
4
- * slot, ordered to the LEFT of the official "Session log" button).
2
+ * AnimatedDock — the terminal dock button rendered into the conversation
3
+ * session header (the `conversation.session.header.utilities` slot, ordered to
4
+ * the LEFT of the official "Session log" button).
5
5
  *
6
- * This is a faithful port of the 21st.dev "Animated Dock" component: it uses
7
- * framer-motion's `useMotionValue` + `useSpring` + `useTransform` so each
8
- * circular icon scales toward the cursor with real spring physics the
9
- * buttery feel you can't get from a CSS transition. Magnification is driven by
10
- * `transform: scale()` (NOT `width`), so it never reflows the layout and the
11
- * dock stays perfectly stable under the cursor.
12
- * Icons come from lucide-react. The dock itself is a frosted-glass pill.
6
+ * Each plugin owns its own dock button independently: dsh-term renders this
7
+ * terminal button, dsh-file-manager renders its own file-panel button. The two
8
+ * are fully decoupled install either alone and you get just its button;
9
+ * install both and both buttons appear side by side with shared magnification
10
+ * (coordinated through window CustomEvents, see DockItem.tsx).
13
11
  *
14
- * Two entries wire to the two right-side panels through window CustomEvents
15
- * (a cross-plugin bridge so this component never imports the file-manager
16
- * bundle):
17
- *
18
- * - terminal -> `dsh-dock:toggle-terminal` (handled in dsh-term index)
19
- * - file -> `dsh-dock:toggle-filepanel` (handled in dsh-file-manager)
20
- *
21
- * Active (open) state is mirrored back through `dsh-dock:terminal-state` /
22
- * `dsh-dock:filepanel-state` so the icons highlight in lockstep with the
23
- * actual panels.
12
+ * Clicking broadcasts `dsh-dock:toggle-terminal` (handled in dsh-term index);
13
+ * the open state is mirrored back through `dsh-dock:terminal-state`.
24
14
  * @module dsh-term/client/AnimatedDock
25
15
  */
16
+ import { useEffect, useState, type ReactElement } from 'react'
17
+ import { DockItem } from './DockItem.tsx'
18
+ import { getT } from '../i18n-seat.ts'
26
19
 
27
- import { useEffect, useRef, useState, type ReactElement, type ReactNode } from 'react'
28
- import { motion, useMotionValue, useSpring, useTransform, type MotionValue } from 'framer-motion'
29
- import { Terminal, FolderOpen } from 'lucide-react'
30
- import styles from './animated-dock.module.css'
31
-
32
- /** Cross-plugin event names (kept as literals to avoid cross-bundle imports). */
33
20
  const EV = {
34
21
  toggleTerminal: 'dsh-dock:toggle-terminal',
35
- toggleFile: 'dsh-dock:toggle-filepanel',
36
22
  terminalState: 'dsh-dock:terminal-state',
37
- fileState: 'dsh-dock:filepanel-state',
38
23
  } as const
39
24
 
40
- /** Resting / peak icon size (px). */
41
- const BASE = 40
42
- const MAX = 58
43
- /** Magnification factor at the cursor (MAX / BASE). Driven via CSS `scale()`
44
- * so it never reflows the layout — this is what removed the old "jitter". */
45
- const MAX_SCALE = MAX / BASE
46
- /** Cursor distance (px) over which magnification falls to zero. */
47
- const DIST = 120
48
- /** Spring tuning. Near-critically damped (ratio ≈ 0.94) so the icon settles
49
- * smoothly with essentially no overshoot. The old `damping: 0.35` was
50
- * effectively zero damping and caused the wild oscillation. */
51
- const DAMPING = 28
52
- const STIFFNESS = 220
53
-
54
25
  export function AnimatedDock(): ReactElement {
55
- const mouseX = useMotionValue<number>(Infinity)
56
- const [terminalActive, setTerminalActive] = useState(false)
57
- const [fileActive, setFileActive] = useState(false)
58
-
59
- // Mirror the real panel open-state back into the icon highlight.
26
+ const t = getT()
27
+ const [active, setActive] = useState(false)
60
28
  useEffect(() => {
61
- const onTerm = (e: Event): void => setTerminalActive(Boolean((e as CustomEvent).detail))
62
- const onFile = (e: Event): void => setFileActive(Boolean((e as CustomEvent).detail))
63
- window.addEventListener(EV.terminalState, onTerm)
64
- window.addEventListener(EV.fileState, onFile)
65
- return () => {
66
- window.removeEventListener(EV.terminalState, onTerm)
67
- window.removeEventListener(EV.fileState, onFile)
68
- }
29
+ const onState = (e: Event): void => setActive(Boolean((e as CustomEvent).detail))
30
+ window.addEventListener(EV.terminalState, onState)
31
+ return () => window.removeEventListener(EV.terminalState, onState)
69
32
  }, [])
70
-
71
- return (
72
- <motion.div
73
- className={styles.dock}
74
- onMouseMove={(e) => mouseX.set(e.clientX)}
75
- onMouseLeave={() => mouseX.set(Infinity)}
76
- aria-label="工具坞"
77
- >
78
- <DockItem
79
- mouseX={mouseX}
80
- active={terminalActive}
81
- label="终端"
82
- onClick={() => window.dispatchEvent(new CustomEvent(EV.toggleTerminal))}
83
- >
84
- <Terminal size={20} strokeWidth={2} />
85
- </DockItem>
86
- <DockItem
87
- mouseX={mouseX}
88
- active={fileActive}
89
- label="文件面板"
90
- onClick={() => window.dispatchEvent(new CustomEvent(EV.toggleFile))}
91
- >
92
- <FolderOpen size={20} strokeWidth={2} />
93
- </DockItem>
94
- </motion.div>
95
- )
96
- }
97
-
98
- /** A single magnifying icon. Its `scale` is a spring driven by cursor distance.
99
- * Because we animate `transform` (not `width`), the layout never reflows, so
100
- * there is no neighbour-push feedback loop and the dock stays rock-steady. */
101
- function DockItem(props: {
102
- mouseX: MotionValue<number>
103
- active: boolean
104
- label: string
105
- onClick: () => void
106
- children: ReactNode
107
- }): ReactElement {
108
- const ref = useRef<HTMLButtonElement>(null)
109
- const distance = useTransform(props.mouseX, (val) => {
110
- const bounds = ref.current?.getBoundingClientRect() ?? { x: 0, width: BASE }
111
- return val - bounds.x - bounds.width / 2
112
- })
113
- const scale = useSpring(
114
- useTransform(distance, [-DIST, 0, DIST], [1, MAX_SCALE, 1]),
115
- { damping: DAMPING, stiffness: STIFFNESS },
116
- )
117
33
  return (
118
- <motion.button
119
- ref={ref}
120
- type="button"
121
- style={{ scale }}
122
- className={`${styles.item}${props.active ? ` ${styles.active}` : ''}`}
123
- onClick={props.onClick}
124
- aria-label={props.label}
125
- aria-pressed={props.active}
126
- title={props.label}
34
+ <DockItem
35
+ active={active}
36
+ label={t('ui.dock.label')}
37
+ onClick={() => window.dispatchEvent(new CustomEvent(EV.toggleTerminal))}
127
38
  >
128
- <span className={styles.tooltip}>{props.label}</span>
129
- {props.children}
130
- </motion.button>
39
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
40
+ <polyline points="4 17 10 11 4 5" />
41
+ <line x1="12" y1="19" x2="20" y2="19" />
42
+ </svg>
43
+ </DockItem>
131
44
  )
132
45
  }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * DockItem — a round dock button for the conversation session header.
3
+ * Each plugin owns its own DockItem copy (terminal in dsh-term, file-panel in
4
+ * dsh-file-manager). Hover scales the button up via pure CSS — no JS event
5
+ * coordination, no shared motion value.
6
+ * @module dsh-term/client/DockItem
7
+ */
8
+ import { type ReactElement, type ReactNode } from 'react'
9
+
10
+ const STYLE_ID = 'dsh-dock-item-style'
11
+
12
+ function ensureStyle(): void {
13
+ if (typeof document === 'undefined') return
14
+ if (document.getElementById(STYLE_ID) !== null) return
15
+ const tag = document.createElement('style')
16
+ tag.id = STYLE_ID
17
+ tag.textContent = '.dsh-dock-item{width:32px;height:32px;border:none;border-radius:9999px;background:#000;color:#fff;cursor:pointer;position:relative;transition:transform .15s ease,background .18s ease;display:inline-flex;align-items:center;justify-content:center}.dsh-dock-item:hover{transform:scale(1.15);background:#1a1a1a}.dsh-dock-item-active{background:var(--aion-primary,#6ea0ff)}.dsh-dock-tooltip{position:absolute;top:calc(100% + 6px);left:50%;transform:translateX(-50%);padding:2px 8px;font-size:12px;line-height:1.4;white-space:nowrap;color:#fff;background:rgba(20,20,30,.9);border:1px solid rgba(255,255,255,.12);border-radius:6px;opacity:0;pointer-events:none;transition:opacity .15s}.dsh-dock-item:hover .dsh-dock-tooltip{opacity:1}'
18
+ document.head.appendChild(tag)
19
+ }
20
+
21
+ ensureStyle()
22
+
23
+ export function DockItem(props: {
24
+ active: boolean
25
+ label: string
26
+ onClick: () => void
27
+ children: ReactNode
28
+ }): ReactElement {
29
+ return (
30
+ <button
31
+ type="button"
32
+ className={`dsh-dock-item${props.active ? ' dsh-dock-item-active' : ''}`}
33
+ onClick={props.onClick}
34
+ aria-label={props.label}
35
+ aria-pressed={props.active}
36
+ title={props.label}
37
+ >
38
+ <span className="dsh-dock-tooltip">{props.label}</span>
39
+ {props.children}
40
+ </button>
41
+ )
42
+ }