@lijian-ui/dsh-file-manager 0.2.2 → 0.2.4

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 (101) hide show
  1. package/LICENSE +38 -38
  2. package/README.i18n.yaml +6 -6
  3. package/README.md +66 -66
  4. package/README.zh.md +66 -66
  5. package/cordis.patch.yml +13 -13
  6. package/lib/client.js +27354 -355
  7. package/lib/client.js.map +1 -1
  8. package/lib/index.js +474 -42
  9. package/lib/tsconfig.client.tsbuildinfo +1 -1
  10. package/lib/tsconfig.host.tsbuildinfo +1 -1
  11. package/lib/types/client/file-source.d.ts +10 -0
  12. package/lib/types/client/file-source.d.ts.map +1 -1
  13. package/lib/types/client/fileType.d.ts +19 -4
  14. package/lib/types/client/fileType.d.ts.map +1 -1
  15. package/lib/types/client/index.d.ts +0 -1
  16. package/lib/types/client/index.d.ts.map +1 -1
  17. package/lib/types/client/locales.d.ts +3 -0
  18. package/lib/types/client/locales.d.ts.map +1 -1
  19. package/lib/types/client/mount.d.ts +1 -1
  20. package/lib/types/client/mount.d.ts.map +1 -1
  21. package/lib/types/client/picker/FilePickerModal.d.ts +8 -6
  22. package/lib/types/client/picker/FilePickerModal.d.ts.map +1 -1
  23. package/lib/types/client/preview/PreviewPanel.d.ts +2 -1
  24. package/lib/types/client/preview/PreviewPanel.d.ts.map +1 -1
  25. package/lib/types/client/preview/PreviewToolbar.d.ts +3 -1
  26. package/lib/types/client/preview/PreviewToolbar.d.ts.map +1 -1
  27. package/lib/types/client/preview/content.d.ts +3 -1
  28. package/lib/types/client/preview/content.d.ts.map +1 -1
  29. package/lib/types/client/preview/office.d.ts +23 -0
  30. package/lib/types/client/preview/office.d.ts.map +1 -0
  31. package/lib/types/client/reference.d.ts +23 -10
  32. package/lib/types/client/reference.d.ts.map +1 -1
  33. package/lib/types/client/store.d.ts.map +1 -1
  34. package/lib/types/host/fs-service.d.ts.map +1 -1
  35. package/lib/types/host/office-preview.d.ts +13 -0
  36. package/lib/types/host/office-preview.d.ts.map +1 -0
  37. package/lib/types/host/routes.d.ts.map +1 -1
  38. package/lib/types/index.d.ts.map +1 -1
  39. package/package.json +19 -4
  40. package/src/client/DockItem.tsx +1 -1
  41. package/src/client/FileManagerSettingsCard.tsx +117 -117
  42. package/src/client/PluginSettingsCard.tsx +337 -337
  43. package/src/client/chat/file-ref.ts +144 -144
  44. package/src/client/chat/mermaid-chat.tsx +102 -102
  45. package/src/client/chat/placeholder-hint.tsx +46 -46
  46. package/src/client/components/ExplorerPanel.tsx +534 -534
  47. package/src/client/components/FileIcon.tsx +46 -46
  48. package/src/client/components/ScmPanel.tsx +480 -480
  49. package/src/client/components/a11y.ts +20 -20
  50. package/src/client/components/icons.tsx +274 -274
  51. package/src/client/components/overlay.tsx +225 -225
  52. package/src/client/drag/DragFileInlay.tsx +89 -89
  53. package/src/client/drag/file-drag.ts +67 -67
  54. package/src/client/drag.ts +154 -154
  55. package/src/client/file-source.ts +34 -0
  56. package/src/client/fileType.ts +32 -5
  57. package/src/client/hooks/useResizableSplit.ts +91 -91
  58. package/src/client/hooks/useStore.ts +15 -15
  59. package/src/client/index.ts +45 -17
  60. package/src/client/layout.ts +523 -523
  61. package/src/client/locales.ts +6 -0
  62. package/src/client/maximize.ts +34 -34
  63. package/src/client/mount.tsx +2 -1
  64. package/src/client/persist.ts +194 -194
  65. package/src/client/picker/FilePickerModal.tsx +526 -507
  66. package/src/client/picker/file-picker.ts +86 -86
  67. package/src/client/preview/PreviewPanel.tsx +8 -1
  68. package/src/client/preview/PreviewTabs.tsx +159 -159
  69. package/src/client/preview/PreviewToolbar.tsx +3 -2
  70. package/src/client/preview/content.tsx +7 -2
  71. package/src/client/preview/markdown.ts +351 -351
  72. package/src/client/preview/mermaid.ts +274 -274
  73. package/src/client/preview/office.tsx +321 -0
  74. package/src/client/reference.ts +62 -8
  75. package/src/client/settings-card.module.css +316 -316
  76. package/src/client/settings-form.ts +451 -451
  77. package/src/client/store.ts +17 -12
  78. package/src/client/styles/drag.module.css +30 -30
  79. package/src/client/styles/explorer.module.css +371 -371
  80. package/src/client/styles/picker.module.css +344 -344
  81. package/src/client/styles/preview.module.css +18 -11
  82. package/src/client/styles/scm.module.css +380 -380
  83. package/src/client/styles/tokens.module.css +376 -367
  84. package/src/core/types.ts +162 -162
  85. package/src/host/fs-service.ts +7 -1
  86. package/src/host/gate.ts +73 -73
  87. package/src/host/git-runner.ts +116 -116
  88. package/src/host/git-service.ts +395 -395
  89. package/src/host/loopback.ts +63 -63
  90. package/src/host/office-preview.ts +399 -0
  91. package/src/host/poll-guard.ts +109 -109
  92. package/src/host/routes.ts +72 -1
  93. package/src/index.ts +3 -1
  94. package/src/mount-once.ts +48 -48
  95. package/lib/types/client/chat/AttachedFilesDock.d.ts +0 -51
  96. package/lib/types/client/chat/AttachedFilesDock.d.ts.map +0 -1
  97. package/lib/types/client/floating.d.ts +0 -27
  98. package/lib/types/client/floating.d.ts.map +0 -1
  99. package/lib/types/client/mention.d.ts +0 -44
  100. package/lib/types/client/mention.d.ts.map +0 -1
  101. package/src/client/styles/chip.module.css +0 -38
@@ -1,86 +1,86 @@
1
- /**
2
- * File-picker bridge: the single channel between the '@' trigger source
3
- * (which asks for a directory) and the tree-browser modal (which renders it).
4
- *
5
- * Why a modal and not the inline trigger menu: the shell's trigger menu is a
6
- * flat candidate list with no tree affordance — picking a directory cannot
7
- * expand it (the menu closes on pick, and the shell only re-runs trigger
8
- * detection on keyboard input, not on programmatic draft writes). The modal
9
- * is the pi-desktop-style answer: a real directory tree with lazy expansion,
10
- * a search box, and one-click insertion.
11
- *
12
- * The bridge is framework-free (subscribe/getSnapshot/open/close) so the
13
- * trigger source can open the picker without touching React; the modal host
14
- * component subscribes and renders. Insertion happens through the
15
- * conversation input shell, replacing the exact `@` token span the picker
16
- * was opened from.
17
- * @module dsh-filemgr/client/picker/file-picker
18
- */
19
-
20
- import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'
21
-
22
- /** The `@` token span captured when the picker opened (replace-on-insert). */
23
- export interface PickSpan {
24
- /** Start offset of the `@` token in the draft. */
25
- start: number
26
- /** End offset (the caret). */
27
- end: number
28
- }
29
-
30
- /** What the picker is currently anchored to. */
31
- export interface PickRequest {
32
- /** Session whose draft the picker will edit. */
33
- sessionId: SessionId
34
- /** Project root (canonical cwd) everything is relative to. */
35
- root: string
36
- /** Directory to start browsing at ('' = root). */
37
- initialDir: string
38
- /** The `@` token span to replace on insert. */
39
- span: PickSpan
40
- }
41
-
42
- /** The bridge's public snapshot. */
43
- export interface PickState {
44
- open: boolean
45
- req: PickRequest | null
46
- }
47
-
48
- /** Minimal listener shape. */
49
- type Listener = (state: PickState) => void
50
-
51
- /** Framework-free singleton store backing the modal. */
52
- class FilePickerBridge {
53
- private state: PickState = { open: false, req: null }
54
- private readonly listeners = new Set<Listener>()
55
-
56
- /** Current snapshot (stable reference until the next open/close). */
57
- getSnapshot(): PickState {
58
- return this.state
59
- }
60
-
61
- /** Subscribe to open/close transitions. */
62
- subscribe(listener: Listener): () => void {
63
- this.listeners.add(listener)
64
- return () => { this.listeners.delete(listener) }
65
- }
66
-
67
- /** Open the picker anchored to one `@` token. */
68
- open(req: PickRequest): void {
69
- this.state = { open: true, req }
70
- this.emit()
71
- }
72
-
73
- /** Close the picker without inserting anything. */
74
- close(): void {
75
- if (!this.state.open) return
76
- this.state = { open: false, req: null }
77
- this.emit()
78
- }
79
-
80
- private emit(): void {
81
- for (const listener of this.listeners) listener(this.state)
82
- }
83
- }
84
-
85
- /** The shared singleton instance. */
86
- export const filePicker = new FilePickerBridge()
1
+ /**
2
+ * File-picker bridge: the single channel between the '@' trigger source
3
+ * (which asks for a directory) and the tree-browser modal (which renders it).
4
+ *
5
+ * Why a modal and not the inline trigger menu: the shell's trigger menu is a
6
+ * flat candidate list with no tree affordance — picking a directory cannot
7
+ * expand it (the menu closes on pick, and the shell only re-runs trigger
8
+ * detection on keyboard input, not on programmatic draft writes). The modal
9
+ * is the pi-desktop-style answer: a real directory tree with lazy expansion,
10
+ * a search box, and one-click insertion.
11
+ *
12
+ * The bridge is framework-free (subscribe/getSnapshot/open/close) so the
13
+ * trigger source can open the picker without touching React; the modal host
14
+ * component subscribes and renders. Insertion happens through the
15
+ * conversation input shell, replacing the exact `@` token span the picker
16
+ * was opened from.
17
+ * @module dsh-filemgr/client/picker/file-picker
18
+ */
19
+
20
+ import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'
21
+
22
+ /** The `@` token span captured when the picker opened (replace-on-insert). */
23
+ export interface PickSpan {
24
+ /** Start offset of the `@` token in the draft. */
25
+ start: number
26
+ /** End offset (the caret). */
27
+ end: number
28
+ }
29
+
30
+ /** What the picker is currently anchored to. */
31
+ export interface PickRequest {
32
+ /** Session whose draft the picker will edit. */
33
+ sessionId: SessionId
34
+ /** Project root (canonical cwd) everything is relative to. */
35
+ root: string
36
+ /** Directory to start browsing at ('' = root). */
37
+ initialDir: string
38
+ /** The `@` token span to replace on insert. */
39
+ span: PickSpan
40
+ }
41
+
42
+ /** The bridge's public snapshot. */
43
+ export interface PickState {
44
+ open: boolean
45
+ req: PickRequest | null
46
+ }
47
+
48
+ /** Minimal listener shape. */
49
+ type Listener = (state: PickState) => void
50
+
51
+ /** Framework-free singleton store backing the modal. */
52
+ class FilePickerBridge {
53
+ private state: PickState = { open: false, req: null }
54
+ private readonly listeners = new Set<Listener>()
55
+
56
+ /** Current snapshot (stable reference until the next open/close). */
57
+ getSnapshot(): PickState {
58
+ return this.state
59
+ }
60
+
61
+ /** Subscribe to open/close transitions. */
62
+ subscribe(listener: Listener): () => void {
63
+ this.listeners.add(listener)
64
+ return () => { this.listeners.delete(listener) }
65
+ }
66
+
67
+ /** Open the picker anchored to one `@` token. */
68
+ open(req: PickRequest): void {
69
+ this.state = { open: true, req }
70
+ this.emit()
71
+ }
72
+
73
+ /** Close the picker without inserting anything. */
74
+ close(): void {
75
+ if (!this.state.open) return
76
+ this.state = { open: false, req: null }
77
+ this.emit()
78
+ }
79
+
80
+ private emit(): void {
81
+ for (const listener of this.listeners) listener(this.state)
82
+ }
83
+ }
84
+
85
+ /** The shared singleton instance. */
86
+ export const filePicker = new FilePickerBridge()
@@ -28,7 +28,11 @@ interface AddToChatState {
28
28
  }
29
29
 
30
30
  /** The preview panel (mounted in the preview grid column). */
31
- export function PreviewPanel({ stores, onAddFileReference }: { stores: PanelStores; onAddFileReference: (selection: FileReferenceSelection) => boolean }): JSX.Element {
31
+ export function PreviewPanel({ stores, onAddFileReference, onInsertSheetReference }: {
32
+ stores: PanelStores
33
+ onAddFileReference: (selection: FileReferenceSelection) => boolean
34
+ onInsertSheetReference: (path: string, sheetName: string, range: string) => boolean
35
+ }): JSX.Element {
32
36
  const preview = stores.preview
33
37
  const state = useStore(preview)
34
38
  const layoutState = useStore(stores.layout)
@@ -70,6 +74,7 @@ export function PreviewPanel({ stores, onAddFileReference }: { stores: PanelStor
70
74
  }
71
75
  const ref = findFileReferenceSelection(el, selection, activeTab)
72
76
  if (ref === null) {
77
+
73
78
  setAddToChat(null)
74
79
  return
75
80
  }
@@ -188,6 +193,7 @@ export function PreviewPanel({ stores, onAddFileReference }: { stores: PanelStor
188
193
  canToggleView={activeTab.contentType === 'markdown' || activeTab.contentType === 'html'}
189
194
  split={split}
190
195
  canSplit={isEditableType(activeTab.contentType) && activeTab.content !== null}
196
+ editable={isEditableType(activeTab.contentType)}
191
197
  onViewModeChange={setViewMode}
192
198
  onSplitChange={setSplit}
193
199
  onRefresh={() => void preview.reloadTab(activeTab.id)}
@@ -198,6 +204,7 @@ export function PreviewPanel({ stores, onAddFileReference }: { stores: PanelStor
198
204
  tab={activeTab}
199
205
  viewMode={viewMode}
200
206
  split={split}
207
+ onInsertSheetReference={onInsertSheetReference}
201
208
  onContentChange={(content) => preview.updateContent(activeTab.id, content)}
202
209
  onSave={() => void preview.saveTab(activeTab.id)}
203
210
  />
@@ -1,159 +1,159 @@
1
- /**
2
- * The preview tab strip: 36px bar, tabs capped at 180px (padding 0 10, gap 6,
3
- * 12px title), dirty dot (6px, primary), close glyph (16px box, 12px icon),
4
- * middle-click close, right-click menu (close left/right/others/all), the
5
- * 32px left/right overflow fade indicators (ResizeObserver + scroll), the
6
- * new-URL-tab plus, and the panel collapse button.
7
- * @module dsh-filemgr/client/preview/PreviewTabs
8
- */
9
-
10
- import { useEffect, useRef, useState } from 'react'
11
- import type { JSX } from 'react'
12
- import type { PreviewTabState } from '../store.ts'
13
- import { t } from '../locales.ts'
14
- import { CloseIcon, MaximizeIcon, PlusIcon, RestoreIcon, ShrinkIcon } from '../components/icons.tsx'
15
- import { activateOnKey } from '../components/a11y.ts'
16
- import previewCss from '../styles/preview.module.css'
17
-
18
- /** Tab width cap (FileManager measured). */
19
- export const MAX_TAB_WIDTH_PX = 180
20
- /** Fade indicator width. */
21
- const FADE_WIDTH = 32
22
-
23
- /** Left/right overflow state. */
24
- export interface TabFadeState {
25
- left: boolean
26
- right: boolean
27
- }
28
-
29
- /** The tab strip. */
30
- export function PreviewTabs({
31
- tabs,
32
- activeTabId,
33
- onSwitch,
34
- onClose,
35
- onContextMenu,
36
- onNewUrlTab,
37
- onClosePanel,
38
- maximized,
39
- onMaximize,
40
- }: {
41
- tabs: PreviewTabState[]
42
- activeTabId: string | null
43
- onSwitch: (id: string) => void
44
- onClose: (id: string) => void
45
- onContextMenu: (event: React.MouseEvent, tab: PreviewTabState) => void
46
- onNewUrlTab: () => void
47
- onClosePanel: () => void
48
- maximized: boolean
49
- onMaximize: () => void
50
- }): JSX.Element {
51
- const scrollRef = useRef<HTMLDivElement>(null)
52
- const [fade, setFade] = useState<TabFadeState>({ left: false, right: false })
53
-
54
- // Overflow fades: ResizeObserver + scroll listener, setState only on change.
55
- useEffect(() => {
56
- const el = scrollRef.current
57
- if (el === null) return
58
- const update = (): void => {
59
- const next = {
60
- left: el.scrollLeft > 1,
61
- right: el.scrollLeft + el.clientWidth < el.scrollWidth - 1,
62
- }
63
- setFade((prev) => (prev.left === next.left && prev.right === next.right ? prev : next))
64
- }
65
- const observer = new ResizeObserver(update)
66
- observer.observe(el)
67
- el.addEventListener('scroll', update, { passive: true })
68
- window.addEventListener('resize', update)
69
- update()
70
- return () => {
71
- observer.disconnect()
72
- el.removeEventListener('scroll', update)
73
- window.removeEventListener('resize', update)
74
- }
75
- }, [tabs.length])
76
-
77
- return (
78
- <div className={previewCss.tabBar}>
79
- <div ref={scrollRef} className={previewCss.tabScroll}>
80
- {tabs.length === 0 && <div className={previewCss.noTabs}>{t('preview.noTabs')}</div>}
81
- {tabs.map((tab) => (
82
- <div
83
- key={tab.id}
84
- className={`${previewCss.tab}${tab.id === activeTabId ? ` ${previewCss.tabActive}` : ` ${previewCss.tabInactive}`}`}
85
- style={{ maxWidth: MAX_TAB_WIDTH_PX }}
86
- role="button"
87
- tabIndex={0}
88
- title={tab.path}
89
- aria-label={tab.title}
90
- onClick={() => onSwitch(tab.id)}
91
- onKeyDown={activateOnKey(() => { onSwitch(tab.id) })}
92
- onContextMenu={(event) => onContextMenu(event, tab)}
93
- onAuxClick={(event) => {
94
- if (event.button !== 1) return
95
- event.preventDefault()
96
- event.stopPropagation()
97
- onClose(tab.id)
98
- }}
99
- >
100
- <span className={previewCss.tabTitle} title={tab.path}>{tab.title}</span>
101
- {tab.dirty && <span className={previewCss.tabDotDirty} title={t('preview.dirty')} />}
102
- <span
103
- className={previewCss.tabClose}
104
- role="button"
105
- tabIndex={0}
106
- title={t('common.close')}
107
- aria-label={t('common.close')}
108
- onClick={(event) => {
109
- event.stopPropagation()
110
- onClose(tab.id)
111
- }}
112
- onKeyDown={activateOnKey(() => { onClose(tab.id) })}
113
- >
114
- <CloseIcon size={12} />
115
- </span>
116
- </div>
117
- ))}
118
- <div
119
- className={previewCss.tabPlus}
120
- role="button"
121
- tabIndex={0}
122
- onClick={onNewUrlTab}
123
- onKeyDown={activateOnKey(onNewUrlTab)}
124
- title={t('preview.newUrlTab')}
125
- >
126
- <PlusIcon size={14} />
127
- </div>
128
- </div>
129
- <div className={previewCss.tabBarRight}>
130
- {/* Maximize/restore (issue #315): the layout controller owns the grid
131
- takeover, Esc and the restore path. */}
132
- <div
133
- className={previewCss.panelMaximize}
134
- role="button"
135
- tabIndex={0}
136
- onClick={onMaximize}
137
- onKeyDown={activateOnKey(onMaximize)}
138
- title={t(maximized ? 'preview.restore' : 'preview.maximize')}
139
- aria-label={t(maximized ? 'preview.restore' : 'preview.maximize')}
140
- >
141
- {maximized ? <RestoreIcon size={14} /> : <MaximizeIcon size={14} />}
142
- </div>
143
- <div
144
- className={previewCss.panelCollapse}
145
- role="button"
146
- tabIndex={0}
147
- onClick={onClosePanel}
148
- onKeyDown={activateOnKey(onClosePanel)}
149
- title={t('preview.collapsePanel')}
150
- aria-label={t('preview.collapsePanel')}
151
- >
152
- <ShrinkIcon size={14} />
153
- </div>
154
- </div>
155
- {fade.left && <div className={previewCss.tabFadeLeft} style={{ width: FADE_WIDTH }} />}
156
- {fade.right && <div className={previewCss.tabFadeRight} style={{ width: FADE_WIDTH }} />}
157
- </div>
158
- )
159
- }
1
+ /**
2
+ * The preview tab strip: 36px bar, tabs capped at 180px (padding 0 10, gap 6,
3
+ * 12px title), dirty dot (6px, primary), close glyph (16px box, 12px icon),
4
+ * middle-click close, right-click menu (close left/right/others/all), the
5
+ * 32px left/right overflow fade indicators (ResizeObserver + scroll), the
6
+ * new-URL-tab plus, and the panel collapse button.
7
+ * @module dsh-filemgr/client/preview/PreviewTabs
8
+ */
9
+
10
+ import { useEffect, useRef, useState } from 'react'
11
+ import type { JSX } from 'react'
12
+ import type { PreviewTabState } from '../store.ts'
13
+ import { t } from '../locales.ts'
14
+ import { CloseIcon, MaximizeIcon, PlusIcon, RestoreIcon, ShrinkIcon } from '../components/icons.tsx'
15
+ import { activateOnKey } from '../components/a11y.ts'
16
+ import previewCss from '../styles/preview.module.css'
17
+
18
+ /** Tab width cap (FileManager measured). */
19
+ export const MAX_TAB_WIDTH_PX = 180
20
+ /** Fade indicator width. */
21
+ const FADE_WIDTH = 32
22
+
23
+ /** Left/right overflow state. */
24
+ export interface TabFadeState {
25
+ left: boolean
26
+ right: boolean
27
+ }
28
+
29
+ /** The tab strip. */
30
+ export function PreviewTabs({
31
+ tabs,
32
+ activeTabId,
33
+ onSwitch,
34
+ onClose,
35
+ onContextMenu,
36
+ onNewUrlTab,
37
+ onClosePanel,
38
+ maximized,
39
+ onMaximize,
40
+ }: {
41
+ tabs: PreviewTabState[]
42
+ activeTabId: string | null
43
+ onSwitch: (id: string) => void
44
+ onClose: (id: string) => void
45
+ onContextMenu: (event: React.MouseEvent, tab: PreviewTabState) => void
46
+ onNewUrlTab: () => void
47
+ onClosePanel: () => void
48
+ maximized: boolean
49
+ onMaximize: () => void
50
+ }): JSX.Element {
51
+ const scrollRef = useRef<HTMLDivElement>(null)
52
+ const [fade, setFade] = useState<TabFadeState>({ left: false, right: false })
53
+
54
+ // Overflow fades: ResizeObserver + scroll listener, setState only on change.
55
+ useEffect(() => {
56
+ const el = scrollRef.current
57
+ if (el === null) return
58
+ const update = (): void => {
59
+ const next = {
60
+ left: el.scrollLeft > 1,
61
+ right: el.scrollLeft + el.clientWidth < el.scrollWidth - 1,
62
+ }
63
+ setFade((prev) => (prev.left === next.left && prev.right === next.right ? prev : next))
64
+ }
65
+ const observer = new ResizeObserver(update)
66
+ observer.observe(el)
67
+ el.addEventListener('scroll', update, { passive: true })
68
+ window.addEventListener('resize', update)
69
+ update()
70
+ return () => {
71
+ observer.disconnect()
72
+ el.removeEventListener('scroll', update)
73
+ window.removeEventListener('resize', update)
74
+ }
75
+ }, [tabs.length])
76
+
77
+ return (
78
+ <div className={previewCss.tabBar}>
79
+ <div ref={scrollRef} className={previewCss.tabScroll}>
80
+ {tabs.length === 0 && <div className={previewCss.noTabs}>{t('preview.noTabs')}</div>}
81
+ {tabs.map((tab) => (
82
+ <div
83
+ key={tab.id}
84
+ className={`${previewCss.tab}${tab.id === activeTabId ? ` ${previewCss.tabActive}` : ` ${previewCss.tabInactive}`}`}
85
+ style={{ maxWidth: MAX_TAB_WIDTH_PX }}
86
+ role="button"
87
+ tabIndex={0}
88
+ title={tab.path}
89
+ aria-label={tab.title}
90
+ onClick={() => onSwitch(tab.id)}
91
+ onKeyDown={activateOnKey(() => { onSwitch(tab.id) })}
92
+ onContextMenu={(event) => onContextMenu(event, tab)}
93
+ onAuxClick={(event) => {
94
+ if (event.button !== 1) return
95
+ event.preventDefault()
96
+ event.stopPropagation()
97
+ onClose(tab.id)
98
+ }}
99
+ >
100
+ <span className={previewCss.tabTitle} title={tab.path}>{tab.title}</span>
101
+ {tab.dirty && <span className={previewCss.tabDotDirty} title={t('preview.dirty')} />}
102
+ <span
103
+ className={previewCss.tabClose}
104
+ role="button"
105
+ tabIndex={0}
106
+ title={t('common.close')}
107
+ aria-label={t('common.close')}
108
+ onClick={(event) => {
109
+ event.stopPropagation()
110
+ onClose(tab.id)
111
+ }}
112
+ onKeyDown={activateOnKey(() => { onClose(tab.id) })}
113
+ >
114
+ <CloseIcon size={12} />
115
+ </span>
116
+ </div>
117
+ ))}
118
+ <div
119
+ className={previewCss.tabPlus}
120
+ role="button"
121
+ tabIndex={0}
122
+ onClick={onNewUrlTab}
123
+ onKeyDown={activateOnKey(onNewUrlTab)}
124
+ title={t('preview.newUrlTab')}
125
+ >
126
+ <PlusIcon size={14} />
127
+ </div>
128
+ </div>
129
+ <div className={previewCss.tabBarRight}>
130
+ {/* Maximize/restore (issue #315): the layout controller owns the grid
131
+ takeover, Esc and the restore path. */}
132
+ <div
133
+ className={previewCss.panelMaximize}
134
+ role="button"
135
+ tabIndex={0}
136
+ onClick={onMaximize}
137
+ onKeyDown={activateOnKey(onMaximize)}
138
+ title={t(maximized ? 'preview.restore' : 'preview.maximize')}
139
+ aria-label={t(maximized ? 'preview.restore' : 'preview.maximize')}
140
+ >
141
+ {maximized ? <RestoreIcon size={14} /> : <MaximizeIcon size={14} />}
142
+ </div>
143
+ <div
144
+ className={previewCss.panelCollapse}
145
+ role="button"
146
+ tabIndex={0}
147
+ onClick={onClosePanel}
148
+ onKeyDown={activateOnKey(onClosePanel)}
149
+ title={t('preview.collapsePanel')}
150
+ aria-label={t('preview.collapsePanel')}
151
+ >
152
+ <ShrinkIcon size={14} />
153
+ </div>
154
+ </div>
155
+ {fade.left && <div className={previewCss.tabFadeLeft} style={{ width: FADE_WIDTH }} />}
156
+ {fade.right && <div className={previewCss.tabFadeRight} style={{ width: FADE_WIDTH }} />}
157
+ </div>
158
+ )
159
+ }
@@ -8,7 +8,6 @@
8
8
 
9
9
  import type { JSX } from 'react'
10
10
  import type { PreviewContentType } from '../../core/types.ts'
11
- import { isEditableType } from '../fileType.ts'
12
11
  import { t } from '../locales.ts'
13
12
  import { CodeIcon, DownloadIcon, EyeIcon, RefreshIcon, SaveIcon, SplitIcon } from '../components/icons.tsx'
14
13
  import previewCss from '../styles/preview.module.css'
@@ -65,6 +64,7 @@ export function PreviewToolbar({
65
64
  canToggleView,
66
65
  split,
67
66
  canSplit,
67
+ editable,
68
68
  onViewModeChange,
69
69
  onSplitChange,
70
70
  onRefresh,
@@ -80,6 +80,8 @@ export function PreviewToolbar({
80
80
  canToggleView: boolean
81
81
  split: boolean
82
82
  canSplit: boolean
83
+ /** The tab can be edited and saved back (text kinds + editable .xlsx). */
84
+ editable: boolean
83
85
  onViewModeChange: (mode: 'source' | 'preview') => void
84
86
  onSplitChange: (split: boolean) => void
85
87
  onRefresh: () => void
@@ -87,7 +89,6 @@ export function PreviewToolbar({
87
89
  onDownload: () => void
88
90
  }): JSX.Element {
89
91
  const refreshState = refreshStateFor(contentType, hasContent, loading, updated)
90
- const editable = isEditableType(contentType)
91
92
 
92
93
  return (
93
94
  <div className={previewCss.toolbar}>
@@ -23,6 +23,7 @@ import {
23
23
  watchShellTheme,
24
24
  } from './mermaid.ts'
25
25
  import previewCss from '../styles/preview.module.css'
26
+ import { OfficeViewer } from './office.tsx'
26
27
 
27
28
  /** Split-ratio persistence key (FileManager contract). */
28
29
  export const KEY_SPLIT_RATIO = 'preview-panel-split-ratio'
@@ -32,12 +33,15 @@ export function TabContent({
32
33
  tab,
33
34
  viewMode,
34
35
  split,
36
+ onInsertSheetReference,
35
37
  onContentChange,
36
38
  onSave,
37
39
  }: {
38
40
  tab: PreviewTabState
39
41
  viewMode: 'source' | 'preview'
40
42
  split: boolean
43
+ /** Insert a 文件!Sheet!Range chip from an Excel selection. */
44
+ onInsertSheetReference: (path: string, sheetName: string, range: string) => boolean
41
45
  onContentChange: (content: string) => void
42
46
  onSave: () => void
43
47
  }): JSX.Element {
@@ -93,9 +97,10 @@ export function TabContent({
93
97
  )}
94
98
  {tab.contentType === 'pdf' && tab.content !== null && <PdfViewer dataUrl={tab.content} title={tab.title} />}
95
99
  {tab.contentType === 'url' && <UrlViewer tab={tab} />}
96
- {(tab.contentType === 'word' || tab.contentType === 'excel' || tab.contentType === 'ppt' || tab.contentType === 'unsupported') && (
97
- <UnsupportedViewer tab={tab} />
100
+ {(tab.contentType === 'word' || tab.contentType === 'excel' || tab.contentType === 'ppt') && tab.content !== null && (
101
+ <OfficeViewer key={tab.id} tab={tab} onInsertSheetReference={onInsertSheetReference} />
98
102
  )}
103
+ {tab.contentType === 'unsupported' && <UnsupportedViewer tab={tab} />}
99
104
  {tab.content === null && !tab.loading && (
100
105
  <div className={previewCss.placeholder}>
101
106
  <div className={previewCss.placeholderTitle}>{tab.title}</div>