@jetcrab/pi-desk 1.1.29-dev → 1.1.30-dev
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.
- package/docs/plugins.md +2 -2
- package/package.json +1 -1
- package/src/client/l1_entry/pages/l1-error-page.tsx +1 -1
- package/src/client/l1_entry/providers/l1-app-providers.tsx +2 -0
- package/src/client/l2_biz/auth/l2-login.module.css +1 -1
- package/src/client/l2_biz/plugin-host/l2-plugin-composer-panel.tsx +1 -1
- package/src/client/l2_biz/plugin-host/l2-plugin-contribution-host.tsx +1 -1
- package/src/client/l2_biz/plugin-host/l2-plugin-session-header-layout.ts +6 -3
- package/src/client/l2_biz/plugin-host/l2-plugin-session-header.tsx +75 -18
- package/src/client/l2_biz/project-preview/l2-project-preview-dialog.tsx +2 -2
- package/src/client/l2_biz/project-preview/l2-project-preview-page.tsx +1 -1
- package/src/client/l2_biz/settings/l2-settings.tsx +1 -1
- package/src/client/l2_biz/settings/l2-skills-settings.tsx +1 -1
- package/src/client/l2_biz/task-center/l2-task-center.tsx +1 -1
- package/src/client/l2_biz/workbench/file-workspace/l2-plugin-standalone-file-preview.tsx +1 -1
- package/src/client/l2_biz/workbench/file-workspace/l2-work-session-file-preview.tsx +1 -1
- package/src/client/l2_biz/workbench/l2-workbench.tsx +1 -1
- package/src/client/l4_foundation/desktop-window/l4-desktop-titlebar.tsx +188 -0
- package/src/client/l4_foundation/desktop-window/l4-desktop-window.css +107 -0
- package/src/client/l4_foundation/desktop-window/l4-desktop-window.ts +20 -0
- package/src/client/l4_foundation/ui/l4-app-dialog.tsx +2 -2
- package/temp/build/pi-desk/release/.next/BUILD_ID +1 -1
- package/temp/build/pi-desk/release/.next/build-manifest.json +3 -3
- package/temp/build/pi-desk/release/.next/required-server-files.js +2 -2
- package/temp/build/pi-desk/release/.next/required-server-files.json +2 -2
- package/temp/build/pi-desk/release/.next/server/app/_global-error/page.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/_global-error.html +1 -1
- package/temp/build/pi-desk/release/.next/server/app/_global-error.rsc +3 -3
- package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_full.segment.rsc +3 -3
- package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +3 -3
- package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/temp/build/pi-desk/release/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/api/health/route.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/page.js +5 -5
- package/temp/build/pi-desk/release/.next/server/app/page_client-reference-manifest.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/project-preview/page.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/project-preview/page_client-reference-manifest.js +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/27344.js +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/35985.js +2 -2
- package/temp/build/pi-desk/release/.next/server/middleware-build-manifest.js +1 -1
- package/temp/build/pi-desk/release/.next/server/pages/500.html +1 -1
- package/temp/build/pi-desk/release/.next/static/chunks/{65232-a8571f78020eede2.js → 65232-f5b71cec787a30ec.js} +1 -1
- package/temp/build/pi-desk/release/.next/static/chunks/app/error-1ab62762c5f4144f.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/app/layout-ca5ef5994165eb2e.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/app/not-found-b8b9b7ef5ccb7601.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/app/page-978906de76a8dcfe.js +158 -0
- package/temp/build/pi-desk/release/.next/static/chunks/app/project-preview/{page-e1dbfcfc0f333e01.js → page-aaa845016a934245.js} +1 -1
- package/temp/build/pi-desk/release/.next/static/chunks/{webpack-c0b805df46fcf8b3.js → webpack-a84310d111797bef.js} +1 -1
- package/temp/build/pi-desk/release/.next/static/css/053a8d82c09326cd.css +1 -0
- package/temp/build/pi-desk/release/.next/static/css/1cc92eec583aa639.css +1 -0
- package/temp/build/pi-desk/release/.next/static/css/3c59ac068ae404da.css +1 -0
- package/temp/build/pi-desk/release/.next/trace-build +1 -1
- package/temp/build/pi-desk/release/.next/static/chunks/app/error-216b14b5da089ed3.js +0 -1
- package/temp/build/pi-desk/release/.next/static/chunks/app/layout-2c6a573d3004516e.js +0 -1
- package/temp/build/pi-desk/release/.next/static/chunks/app/not-found-720cdf6a606b821a.js +0 -1
- package/temp/build/pi-desk/release/.next/static/chunks/app/page-f78be007d97f0d95.js +0 -158
- package/temp/build/pi-desk/release/.next/static/css/2bdcf40eca12b794.css +0 -1
- package/temp/build/pi-desk/release/.next/static/css/a677e7fbf6e2173e.css +0 -1
- /package/temp/build/pi-desk/release/.next/static/{Yyfg-r17aYmEzlYYlIwDo → ErYBfYXAir5b-6rlacGvL}/_buildManifest.js +0 -0
- /package/temp/build/pi-desk/release/.next/static/{Yyfg-r17aYmEzlYYlIwDo → ErYBfYXAir5b-6rlacGvL}/_ssgManifest.js +0 -0
package/docs/plugins.md
CHANGED
|
@@ -180,10 +180,10 @@ plugin.registerSessionHeaderItem('session-info', {
|
|
|
180
180
|
```
|
|
181
181
|
|
|
182
182
|
- `action` 只接受固定 `icon`、`label`、`onSelect({ source, host, signal })`,不提供动态图标、文字或状态订阅。宿主显示图标加文字,异步操作期间防止重复点击;失败记录局部错误,失效后的回调不得继续使用旧 Host。信号属于该展示生命周期,不会回滚服务端已经产生的副作用。
|
|
183
|
-
- `custom` 只提供 `load()`,宿主不包装点击回调。inline custom 必须额外声明正数 `minWidthRem`,单位为 `rem
|
|
183
|
+
- `custom` 只提供 `load()`,宿主不包装点击回调。inline custom 必须额外声明正数 `minWidthRem`,单位为 `rem`;有内容时才按此分配宽度。加载中和空内容不占宽度或间距,宿主只观察当前真实挂载,不复制业务视图测量。自定义内容应适应获得的容器,不能靠超宽 DOM 挤占标题。overflow custom 不声明宽度,加载中和空内容也不预留空行。
|
|
184
184
|
- 自定义 Target 为 `{ kind: 'session-header-item', source, presentation: 'inline' | 'overflow' }`。两种表现可以使用不同模块,也可以复用同一个 `{ mount }`;loader 分别按表现缓存和重试。可复用上面的 `session-info.js`,或使用公共 React/UI 构建交互视图。
|
|
185
185
|
- 标题项按插件名和注册名稳定排列;宽度不足时从末尾收进更多,宿主保留标题及必要操作。普通按钮选择后关闭更多;自定义区域内点击不会被转发,也不会自动关闭更多。Escape 或外部点击关闭浮层,键盘与触控均可操作。
|
|
186
|
-
-
|
|
186
|
+
- inline 自定义视图先静默挂载,内容出现后参与宽度计算;收纳会卸载该视图。只有更多打开时才挂载 overflow 自定义视图,关闭后释放。动态状态与运行/停止等复杂交互由自定义视图自行实现;业务状态归插件运行态,不归 mount,也不能只存于会被卸载的组件局部状态。卸载只清理视图资源,不停止项目进程。
|
|
187
187
|
|
|
188
188
|
两种注册均限定 Browser Entry 同步阶段,名称规则与其他贡献相同。它们需要支持对应方法的宿主和 SDK;旧宿主缺少能力时禁用这项入口,不使用 `registerContribution` 别名或访问宿主 DOM 代替。现有 `composer-panel` 仍是人工打开的会话弹窗,不是上下方挂件。
|
|
189
189
|
|
package/package.json
CHANGED
|
@@ -13,7 +13,7 @@ export function L1ErrorPage({
|
|
|
13
13
|
}): React.JSX.Element {
|
|
14
14
|
const { t } = useTranslation('common')
|
|
15
15
|
return (
|
|
16
|
-
<main className="flex min-h-
|
|
16
|
+
<main className="flex min-h-[var(--pi-desk-content-height)] items-center justify-center bg-background p-4 text-foreground">
|
|
17
17
|
<div className="w-full max-w-md space-y-4 text-center">
|
|
18
18
|
<h1 className="text-xl font-semibold">{t(notFound ? 'notFound' : 'pageError')}</h1>
|
|
19
19
|
<div className="flex flex-wrap items-center justify-center gap-2">
|
|
@@ -24,6 +24,7 @@ import { L4AppToastProvider } from '@client/l4_foundation/ui/l4-app-toast'
|
|
|
24
24
|
import { TooltipProvider } from '@client/l4_foundation/ui/shadcn/tooltip'
|
|
25
25
|
import { createL4BrowserUuid } from '@client/l4_foundation/lib/l4-browser-uuid'
|
|
26
26
|
import { L4ClientIdContext } from '@client/l4_foundation/lib/l4-client-id'
|
|
27
|
+
import { L4DesktopTitlebar } from '@client/l4_foundation/desktop-window/l4-desktop-titlebar'
|
|
27
28
|
|
|
28
29
|
const resources = {
|
|
29
30
|
en: {
|
|
@@ -76,6 +77,7 @@ export function L1AppProviders({
|
|
|
76
77
|
<L4CodePreferencesProvider>
|
|
77
78
|
<L4RegionProvider resources={resources} initialSettings={initialSettings}>
|
|
78
79
|
<L4AppToastProvider>
|
|
80
|
+
<L4DesktopTitlebar />
|
|
79
81
|
<L4RegionNotice />
|
|
80
82
|
<TooltipProvider>{children}</TooltipProvider>
|
|
81
83
|
</L4AppToastProvider>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
.page {
|
|
2
2
|
display: grid;
|
|
3
3
|
grid-template-rows: auto 1fr;
|
|
4
|
-
min-height: 100dvh;
|
|
4
|
+
min-height: var(--pi-desk-content-height, 100dvh);
|
|
5
5
|
padding: max(24px, env(safe-area-inset-top)) max(24px, env(safe-area-inset-right))
|
|
6
6
|
max(24px, env(safe-area-inset-bottom)) max(24px, env(safe-area-inset-left));
|
|
7
7
|
background: var(--background);
|
|
@@ -35,7 +35,7 @@ export function L2PluginComposerPanel({
|
|
|
35
35
|
<L4AppDialogRoot open onOpenChange={(open) => !open && onClose()}>
|
|
36
36
|
<L4AppDialogContent
|
|
37
37
|
finalFocus={false}
|
|
38
|
-
className="h-[min(720px,calc(100dvh-2rem))] max-w-4xl max-sm:h-
|
|
38
|
+
className="h-[min(720px,calc(100dvh-2rem))] max-w-4xl max-sm:h-[var(--pi-desk-content-height)] max-sm:max-h-[var(--pi-desk-content-height)] max-sm:w-full max-sm:max-w-none max-sm:rounded-none max-sm:border-0"
|
|
39
39
|
>
|
|
40
40
|
<L4AppDialogHeader className="border-b px-4 py-3 pr-12">
|
|
41
41
|
<div className="flex min-w-0 items-center gap-3">
|
|
@@ -237,7 +237,7 @@ export function L2PluginContributionHost({
|
|
|
237
237
|
state.status === 'error' && 'hidden'
|
|
238
238
|
)}
|
|
239
239
|
/>
|
|
240
|
-
{state.status === 'loading' ? (
|
|
240
|
+
{state.status === 'loading' && kind !== 'widget' && kind !== 'session-header-item' ? (
|
|
241
241
|
<div
|
|
242
242
|
role="status"
|
|
243
243
|
className={cn(
|
|
@@ -4,14 +4,17 @@ export function calculateL2PluginSessionHeaderInlineCount(
|
|
|
4
4
|
moreWidth: number,
|
|
5
5
|
gap: number
|
|
6
6
|
): number {
|
|
7
|
-
const
|
|
7
|
+
const occupiedCount = widths.filter((width) => width > 0).length
|
|
8
|
+
const total = widths.reduce((sum, width) => sum + width, 0) + Math.max(0, occupiedCount - 1) * gap
|
|
8
9
|
if (total <= availableWidth) return widths.length
|
|
9
10
|
|
|
10
11
|
let used = moreWidth
|
|
11
12
|
let count = 0
|
|
12
13
|
for (const width of widths) {
|
|
13
|
-
if (
|
|
14
|
-
|
|
14
|
+
if (width > 0) {
|
|
15
|
+
if (used + gap + width > availableWidth) break
|
|
16
|
+
used += gap + width
|
|
17
|
+
}
|
|
15
18
|
count += 1
|
|
16
19
|
}
|
|
17
20
|
return count
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { EllipsisIcon } from 'lucide-react'
|
|
4
|
-
import { useLayoutEffect, useMemo, useRef, useState, type ReactNode } from 'react'
|
|
4
|
+
import { useCallback, useLayoutEffect, useMemo, useRef, useState, type ReactNode } from 'react'
|
|
5
5
|
import { useTranslation } from 'react-i18next'
|
|
6
6
|
import type { BrowserSessionHeaderItemTarget, PluginSource } from '@jetcrab/pi-desk-sdk/browser'
|
|
7
7
|
import { useL4PluginRegistry } from '@client/l4_foundation/plugin-host/l4-plugin-host-context'
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
PopoverTitle,
|
|
15
15
|
PopoverTrigger
|
|
16
16
|
} from '@client/l4_foundation/ui/shadcn/popover'
|
|
17
|
+
import { cn } from '@client/l4_foundation/lib/l4-utils'
|
|
17
18
|
import { L2PluginContributionHost } from './l2-plugin-contribution-host'
|
|
18
19
|
import { calculateL2PluginSessionHeaderInlineCount } from './l2-plugin-session-header-layout'
|
|
19
20
|
import { L2PluginSessionHeaderAction } from './views/l2-plugin-session-header-action'
|
|
@@ -36,12 +37,14 @@ function HeaderItemView({
|
|
|
36
37
|
item,
|
|
37
38
|
source,
|
|
38
39
|
presentation,
|
|
39
|
-
onSelected
|
|
40
|
+
onSelected,
|
|
41
|
+
onContentChange
|
|
40
42
|
}: {
|
|
41
43
|
item: HeaderItem
|
|
42
44
|
source: PluginSource
|
|
43
45
|
presentation: 'inline' | 'overflow'
|
|
44
46
|
onSelected?: () => void
|
|
47
|
+
onContentChange?: (item: HeaderItem, hasContent: boolean) => void
|
|
45
48
|
}): React.JSX.Element {
|
|
46
49
|
const definition = item.descriptor[presentation]
|
|
47
50
|
const target = useMemo<BrowserSessionHeaderItemTarget>(
|
|
@@ -49,6 +52,22 @@ function HeaderItemView({
|
|
|
49
52
|
[presentation, source]
|
|
50
53
|
)
|
|
51
54
|
const sourceKey = JSON.stringify([source.workId, source.sessionId, source.branchId, presentation])
|
|
55
|
+
const contentRef = useRef<HTMLDivElement>(null)
|
|
56
|
+
const [hasContent, setHasContent] = useState(false)
|
|
57
|
+
|
|
58
|
+
useLayoutEffect(() => {
|
|
59
|
+
const content = contentRef.current
|
|
60
|
+
if (!content) return
|
|
61
|
+
const measure = (): void => {
|
|
62
|
+
const visible = content.getBoundingClientRect().height > 0
|
|
63
|
+
setHasContent(visible)
|
|
64
|
+
onContentChange?.(item, visible)
|
|
65
|
+
}
|
|
66
|
+
const observer = new ResizeObserver(measure)
|
|
67
|
+
observer.observe(content)
|
|
68
|
+
return () => observer.disconnect()
|
|
69
|
+
}, [item, onContentChange, sourceKey])
|
|
70
|
+
|
|
52
71
|
if (definition.type === 'action') {
|
|
53
72
|
return (
|
|
54
73
|
<L2PluginSessionHeaderAction
|
|
@@ -68,21 +87,29 @@ function HeaderItemView({
|
|
|
68
87
|
data-plugin-name={item.pluginName}
|
|
69
88
|
data-contribution-name={item.descriptor.contributionName}
|
|
70
89
|
data-presentation={presentation}
|
|
71
|
-
className=
|
|
90
|
+
className={cn(
|
|
91
|
+
'min-w-0 shrink-0',
|
|
92
|
+
presentation === 'overflow' && 'w-full',
|
|
93
|
+
!hasContent && 'pointer-events-none invisible absolute'
|
|
94
|
+
)}
|
|
95
|
+
aria-hidden={!hasContent}
|
|
96
|
+
inert={!hasContent}
|
|
72
97
|
style={
|
|
73
98
|
presentation === 'inline' && item.descriptor.inline.type === 'custom'
|
|
74
99
|
? { width: `${item.descriptor.inline.minWidthRem}rem` }
|
|
75
100
|
: undefined
|
|
76
101
|
}
|
|
77
102
|
>
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
103
|
+
<div ref={contentRef}>
|
|
104
|
+
<L2PluginContributionHost
|
|
105
|
+
pluginName={item.pluginName}
|
|
106
|
+
kind="session-header-item"
|
|
107
|
+
contributionName={item.descriptor.contributionName}
|
|
108
|
+
target={target}
|
|
109
|
+
targetKey={sourceKey}
|
|
110
|
+
layout="inline"
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
86
113
|
</div>
|
|
87
114
|
)
|
|
88
115
|
}
|
|
@@ -106,16 +133,31 @@ export function L2PluginSessionHeader({
|
|
|
106
133
|
)
|
|
107
134
|
const rootRef = useRef<HTMLDivElement>(null)
|
|
108
135
|
const measurementsRef = useRef<HTMLDivElement>(null)
|
|
136
|
+
const customContentRef = useRef(new Map<HeaderItem, boolean>())
|
|
137
|
+
const measureRef = useRef<(() => void) | null>(null)
|
|
138
|
+
const onContentChange = useCallback((item: HeaderItem, hasContent: boolean): void => {
|
|
139
|
+
if (customContentRef.current.get(item) === hasContent) return
|
|
140
|
+
customContentRef.current.set(item, hasContent)
|
|
141
|
+
measureRef.current?.()
|
|
142
|
+
}, [])
|
|
109
143
|
const [measurement, setMeasurement] = useState<{
|
|
110
144
|
items: readonly HeaderItem[]
|
|
111
145
|
count: number
|
|
146
|
+
occupied: boolean
|
|
112
147
|
} | null>(null)
|
|
113
148
|
const [open, setOpen] = useState(false)
|
|
114
|
-
const count = measurement?.items === items ? measurement.count :
|
|
149
|
+
const count = measurement?.items === items ? measurement.count : items.length
|
|
150
|
+
const occupied =
|
|
151
|
+
measurement?.items === items
|
|
152
|
+
? measurement.occupied
|
|
153
|
+
: items.some((item) => item.descriptor.inline.type === 'action')
|
|
115
154
|
const inlineItems = items.slice(0, count)
|
|
116
155
|
const overflowItems = items.slice(count)
|
|
117
156
|
|
|
118
157
|
useLayoutEffect(() => {
|
|
158
|
+
for (const item of customContentRef.current.keys()) {
|
|
159
|
+
if (!items.includes(item)) customContentRef.current.delete(item)
|
|
160
|
+
}
|
|
119
161
|
const root = rootRef.current
|
|
120
162
|
const measurements = measurementsRef.current
|
|
121
163
|
if (!root || !measurements || items.length === 0) return
|
|
@@ -127,42 +169,57 @@ export function L2PluginSessionHeader({
|
|
|
127
169
|
const gap = gapProbe.getBoundingClientRect().width
|
|
128
170
|
const available =
|
|
129
171
|
root.getBoundingClientRect().width - titleProbe.getBoundingClientRect().width - gap
|
|
172
|
+
const widths = probes.map((probe, index) =>
|
|
173
|
+
items[index].descriptor.inline.type === 'custom' &&
|
|
174
|
+
!customContentRef.current.get(items[index])
|
|
175
|
+
? 0
|
|
176
|
+
: probe.getBoundingClientRect().width
|
|
177
|
+
)
|
|
130
178
|
const nextCount = calculateL2PluginSessionHeaderInlineCount(
|
|
131
|
-
|
|
179
|
+
widths,
|
|
132
180
|
available,
|
|
133
181
|
moreProbe.getBoundingClientRect().width,
|
|
134
182
|
gap
|
|
135
183
|
)
|
|
184
|
+
const nextOccupied = widths.some((width) => width > 0)
|
|
136
185
|
if (nextCount === items.length) setOpen(false)
|
|
137
186
|
setMeasurement((current) =>
|
|
138
|
-
current?.items === items && current.count === nextCount
|
|
187
|
+
current?.items === items && current.count === nextCount && current.occupied === nextOccupied
|
|
139
188
|
? current
|
|
140
|
-
: { items, count: nextCount }
|
|
189
|
+
: { items, count: nextCount, occupied: nextOccupied }
|
|
141
190
|
)
|
|
142
191
|
}
|
|
192
|
+
measureRef.current = measure
|
|
143
193
|
const observer = new ResizeObserver(measure)
|
|
144
194
|
observer.observe(root)
|
|
145
195
|
for (const probe of [...probes, titleProbe, moreProbe, gapProbe]) observer.observe(probe)
|
|
146
196
|
measure()
|
|
147
|
-
return () =>
|
|
197
|
+
return () => {
|
|
198
|
+
measureRef.current = null
|
|
199
|
+
observer.disconnect()
|
|
200
|
+
}
|
|
148
201
|
}, [items])
|
|
149
202
|
|
|
150
203
|
return (
|
|
151
204
|
<div
|
|
152
205
|
ref={rootRef}
|
|
153
|
-
className="relative flex min-w-0 flex-1 items-center
|
|
206
|
+
className="relative flex min-w-0 flex-1 items-center"
|
|
154
207
|
data-testid="plugin-session-header"
|
|
155
208
|
>
|
|
156
209
|
<div className="min-w-[3rem] flex-1">{title}</div>
|
|
157
210
|
{items.length > 0 ? (
|
|
158
211
|
<>
|
|
159
|
-
<div
|
|
212
|
+
<div
|
|
213
|
+
className={cn('flex shrink-0 items-center gap-1', occupied && 'ml-1')}
|
|
214
|
+
data-testid="session-header-items"
|
|
215
|
+
>
|
|
160
216
|
{inlineItems.map((item) => (
|
|
161
217
|
<HeaderItemView
|
|
162
218
|
key={itemKey(item)}
|
|
163
219
|
item={item}
|
|
164
220
|
source={source}
|
|
165
221
|
presentation="inline"
|
|
222
|
+
onContentChange={onContentChange}
|
|
166
223
|
/>
|
|
167
224
|
))}
|
|
168
225
|
{overflowItems.length > 0 ? (
|
|
@@ -41,8 +41,8 @@ export function L2ProjectPreviewDialog({
|
|
|
41
41
|
data-testid="project-preview-dialog"
|
|
42
42
|
className={cn(
|
|
43
43
|
'h-[min(94dvh,1080px)] w-[min(96dvw,1600px)] max-w-none gap-0 p-0',
|
|
44
|
-
maximized && 'h-
|
|
45
|
-
'max-sm:h-
|
|
44
|
+
maximized && 'h-[var(--pi-desk-content-height)] max-h-none w-dvw rounded-none',
|
|
45
|
+
'max-sm:h-[var(--pi-desk-content-height)] max-sm:max-h-none max-sm:w-dvw max-sm:rounded-none'
|
|
46
46
|
)}
|
|
47
47
|
>
|
|
48
48
|
<L4AppDialogDescription className="sr-only">
|
|
@@ -34,7 +34,7 @@ export function L2ProjectPreviewPage({
|
|
|
34
34
|
const { t } = useTranslation('projectHistory')
|
|
35
35
|
return (
|
|
36
36
|
<main
|
|
37
|
-
className="flex h-
|
|
37
|
+
className="flex h-[var(--pi-desk-content-height)] min-w-0 flex-col overflow-hidden bg-background text-foreground"
|
|
38
38
|
data-testid="project-preview-page"
|
|
39
39
|
>
|
|
40
40
|
{cwd ? (
|
|
@@ -281,7 +281,7 @@ export function L2Settings({
|
|
|
281
281
|
}}
|
|
282
282
|
>
|
|
283
283
|
<L4AppDialogContent
|
|
284
|
-
className="h-[min(48rem,calc(100dvh-2rem))] max-w-6xl max-sm:h-
|
|
284
|
+
className="h-[min(48rem,calc(100dvh-2rem))] max-w-6xl max-sm:h-[var(--pi-desk-content-height)] max-sm:max-h-[var(--pi-desk-content-height)] max-sm:w-dvw max-sm:max-w-none max-sm:rounded-none max-sm:border-0"
|
|
285
285
|
finalFocus={false}
|
|
286
286
|
showCloseButton={!guardPending}
|
|
287
287
|
>
|
|
@@ -209,7 +209,7 @@ export function L2SkillsSettings({
|
|
|
209
209
|
</div>
|
|
210
210
|
<L4AppDialogRoot open={drawerOpen} onOpenChange={setDrawerOpen}>
|
|
211
211
|
<L4AppDialogContent
|
|
212
|
-
className="left-0 top-
|
|
212
|
+
className="left-0 top-[var(--pi-desk-window-inset)] z-[130] h-[var(--pi-desk-content-height)] max-h-[var(--pi-desk-content-height)] w-[min(360px,calc(100dvw-2rem))] max-w-none translate-x-0 translate-y-0 rounded-l-none"
|
|
213
213
|
finalFocus={false}
|
|
214
214
|
>
|
|
215
215
|
<L4AppDialogHeader className="shrink-0 border-b px-4 py-3 pr-12">
|
|
@@ -468,7 +468,7 @@ function TaskCenterPanel({
|
|
|
468
468
|
<L4AppDialogContent
|
|
469
469
|
aria-label={t('title')}
|
|
470
470
|
showCloseButton={false}
|
|
471
|
-
className="h-[calc(100dvh-2rem)] max-w-[70rem] flex-row bg-background max-lg:h-
|
|
471
|
+
className="h-[calc(100dvh-2rem)] max-w-[70rem] flex-row bg-background max-lg:h-[var(--pi-desk-content-height)] max-lg:max-h-[var(--pi-desk-content-height)] max-lg:w-dvw max-lg:max-w-none max-lg:rounded-none max-lg:border-0"
|
|
472
472
|
>
|
|
473
473
|
<div
|
|
474
474
|
className={cn(
|
|
@@ -152,7 +152,7 @@ export function L2PluginStandaloneFilePreview({
|
|
|
152
152
|
<L4AppDialogContent
|
|
153
153
|
className={
|
|
154
154
|
mobile
|
|
155
|
-
? 'inset-0 h-
|
|
155
|
+
? 'inset-x-0 top-[var(--pi-desk-window-inset)] bottom-0 h-[var(--pi-desk-content-height)] max-h-none w-dvw max-w-none translate-x-0 translate-y-0 rounded-none border-0 ring-0'
|
|
156
156
|
: 'h-[min(94dvh,1080px)] w-[min(96dvw,1600px)] max-w-none'
|
|
157
157
|
}
|
|
158
158
|
>
|
|
@@ -75,7 +75,7 @@ export function L2WorkSessionFilePreview({
|
|
|
75
75
|
}}
|
|
76
76
|
className={
|
|
77
77
|
mobile
|
|
78
|
-
? 'inset-0 h-
|
|
78
|
+
? 'inset-x-0 top-[var(--pi-desk-window-inset)] bottom-0 h-[var(--pi-desk-content-height)] max-h-none w-dvw max-w-none translate-x-0 translate-y-0 rounded-none border-0 ring-0'
|
|
79
79
|
: 'h-[min(94dvh,1080px)] w-[min(96dvw,1600px)] max-w-none'
|
|
80
80
|
}
|
|
81
81
|
finalFocus={() => {
|
|
@@ -2040,7 +2040,7 @@ export function L2Workbench({
|
|
|
2040
2040
|
)
|
|
2041
2041
|
|
|
2042
2042
|
return (
|
|
2043
|
-
<main className="pi-desk-workbench flex h-
|
|
2043
|
+
<main className="pi-desk-workbench flex h-[var(--pi-desk-content-height)] overflow-hidden bg-background">
|
|
2044
2044
|
<L2WorkbenchSidebarLayout
|
|
2045
2045
|
mobile={mobile}
|
|
2046
2046
|
open={sidebarOpen}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState, type MouseEvent } from 'react'
|
|
4
|
+
import {
|
|
5
|
+
getL4DesktopWindow,
|
|
6
|
+
type L4DesktopWindow,
|
|
7
|
+
type L4DesktopWindowAction,
|
|
8
|
+
type L4DesktopWindowState
|
|
9
|
+
} from './l4-desktop-window'
|
|
10
|
+
import './l4-desktop-window.css'
|
|
11
|
+
|
|
12
|
+
const labels = {
|
|
13
|
+
'zh-CN': {
|
|
14
|
+
window: '窗口',
|
|
15
|
+
minimize: '最小化',
|
|
16
|
+
maximize: '最大化',
|
|
17
|
+
restore: '还原',
|
|
18
|
+
close: '关闭'
|
|
19
|
+
},
|
|
20
|
+
en: {
|
|
21
|
+
window: 'Window',
|
|
22
|
+
minimize: 'Minimize',
|
|
23
|
+
maximize: 'Maximize',
|
|
24
|
+
restore: 'Restore',
|
|
25
|
+
close: 'Close'
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function L4DesktopTitlebar(): React.JSX.Element {
|
|
30
|
+
const adapter = useRef<L4DesktopWindow | null>(null)
|
|
31
|
+
const [state, setState] = useState<L4DesktopWindowState | null>(null)
|
|
32
|
+
const [title, setTitle] = useState('Pi Desk')
|
|
33
|
+
const [locale, setLocale] = useState<'zh-CN' | 'en'>('zh-CN')
|
|
34
|
+
const [focused, setFocused] = useState(true)
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const desktop = getL4DesktopWindow()
|
|
38
|
+
if (!desktop) return
|
|
39
|
+
adapter.current = desktop
|
|
40
|
+
let disposed = false
|
|
41
|
+
let frame = 0
|
|
42
|
+
const root = document.documentElement
|
|
43
|
+
const readTitle = (): void => {
|
|
44
|
+
setTitle(document.title || 'Pi Desk')
|
|
45
|
+
setLocale(root.lang.startsWith('zh') ? 'zh-CN' : 'en')
|
|
46
|
+
}
|
|
47
|
+
const refreshState = (): void => {
|
|
48
|
+
cancelAnimationFrame(frame)
|
|
49
|
+
frame = requestAnimationFrame(() => {
|
|
50
|
+
void desktop
|
|
51
|
+
.invoke('state')
|
|
52
|
+
.then((next) => {
|
|
53
|
+
if (!disposed) setState((current) => (current ? next : null))
|
|
54
|
+
})
|
|
55
|
+
.catch((error: unknown) => {
|
|
56
|
+
console.error('[desktop-window] 读取窗口状态失败', error)
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
const onFocus = (): void => {
|
|
61
|
+
setFocused(true)
|
|
62
|
+
}
|
|
63
|
+
const onBlur = (): void => {
|
|
64
|
+
setFocused(false)
|
|
65
|
+
}
|
|
66
|
+
const observer = new MutationObserver(readTitle)
|
|
67
|
+
observer.observe(document.head, { childList: true, subtree: true, characterData: true })
|
|
68
|
+
observer.observe(root, { attributes: true, attributeFilter: ['lang'] })
|
|
69
|
+
window.addEventListener('resize', refreshState)
|
|
70
|
+
window.addEventListener('focus', onFocus)
|
|
71
|
+
window.addEventListener('blur', onBlur)
|
|
72
|
+
|
|
73
|
+
// 按钮已挂载并绑定事件后才关闭系统装饰;旧页面不会调用此能力。
|
|
74
|
+
void desktop
|
|
75
|
+
.invoke('attach')
|
|
76
|
+
.then((next) => {
|
|
77
|
+
if (disposed) return
|
|
78
|
+
readTitle()
|
|
79
|
+
setFocused(document.hasFocus())
|
|
80
|
+
root.dataset.piDeskWindow = ''
|
|
81
|
+
setState(next)
|
|
82
|
+
})
|
|
83
|
+
.catch((error: unknown) => {
|
|
84
|
+
console.error('[desktop-window] 启用自定义标题栏失败,保留系统标题栏', error)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
return () => {
|
|
88
|
+
disposed = true
|
|
89
|
+
adapter.current = null
|
|
90
|
+
observer.disconnect()
|
|
91
|
+
cancelAnimationFrame(frame)
|
|
92
|
+
window.removeEventListener('resize', refreshState)
|
|
93
|
+
window.removeEventListener('focus', onFocus)
|
|
94
|
+
window.removeEventListener('blur', onBlur)
|
|
95
|
+
delete root.dataset.piDeskWindow
|
|
96
|
+
void desktop.invoke('detach').catch((error: unknown) => {
|
|
97
|
+
console.error('[desktop-window] 恢复系统标题栏失败', error)
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
}, [])
|
|
101
|
+
|
|
102
|
+
const run = async (action: L4DesktopWindowAction): Promise<void> => {
|
|
103
|
+
const desktop = adapter.current
|
|
104
|
+
if (!desktop) return
|
|
105
|
+
try {
|
|
106
|
+
const next = await desktop.invoke(action)
|
|
107
|
+
if (adapter.current === desktop) setState(next)
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error(`[desktop-window] 窗口操作失败 action=${action}`, error)
|
|
110
|
+
try {
|
|
111
|
+
await desktop.invoke('detach')
|
|
112
|
+
if (adapter.current !== desktop) return
|
|
113
|
+
delete document.documentElement.dataset.piDeskWindow
|
|
114
|
+
setState(null)
|
|
115
|
+
} catch (restoreError) {
|
|
116
|
+
console.error('[desktop-window] 恢复系统标题栏失败', restoreError)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const onDrag = (event: MouseEvent<HTMLElement>): void => {
|
|
121
|
+
if (event.button !== 0 || (event.target as Element).closest('button')) return
|
|
122
|
+
event.preventDefault()
|
|
123
|
+
void run(event.detail === 2 ? 'toggle-maximize' : 'drag')
|
|
124
|
+
}
|
|
125
|
+
const text = labels[locale]
|
|
126
|
+
const maximizeLabel = state?.maximized ? text.restore : text.maximize
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<header
|
|
130
|
+
className="pi-desk-titlebar"
|
|
131
|
+
data-testid="desktop-titlebar"
|
|
132
|
+
data-focused={focused}
|
|
133
|
+
hidden={!state}
|
|
134
|
+
aria-label={text.window}
|
|
135
|
+
onMouseDown={onDrag}
|
|
136
|
+
>
|
|
137
|
+
<span className="pi-desk-titlebar-title" title={title}>
|
|
138
|
+
{title}
|
|
139
|
+
</span>
|
|
140
|
+
<div className="pi-desk-titlebar-controls">
|
|
141
|
+
<button
|
|
142
|
+
type="button"
|
|
143
|
+
aria-label={text.minimize}
|
|
144
|
+
title={text.minimize}
|
|
145
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
146
|
+
onClick={() => {
|
|
147
|
+
void run('minimize')
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
<svg viewBox="0 0 16 16" aria-hidden="true">
|
|
151
|
+
<path d="M3 8.5h10" />
|
|
152
|
+
</svg>
|
|
153
|
+
</button>
|
|
154
|
+
<button
|
|
155
|
+
type="button"
|
|
156
|
+
aria-label={maximizeLabel}
|
|
157
|
+
title={maximizeLabel}
|
|
158
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
159
|
+
onClick={() => {
|
|
160
|
+
void run('toggle-maximize')
|
|
161
|
+
}}
|
|
162
|
+
>
|
|
163
|
+
<svg viewBox="0 0 16 16" aria-hidden="true">
|
|
164
|
+
{state?.maximized ? (
|
|
165
|
+
<path d="M5.5 5.5v-3h8v8h-3m-8-5h8v8h-8z" />
|
|
166
|
+
) : (
|
|
167
|
+
<rect x="3.5" y="3.5" width="9" height="9" />
|
|
168
|
+
)}
|
|
169
|
+
</svg>
|
|
170
|
+
</button>
|
|
171
|
+
<button
|
|
172
|
+
type="button"
|
|
173
|
+
className="pi-desk-titlebar-close"
|
|
174
|
+
aria-label={text.close}
|
|
175
|
+
title={text.close}
|
|
176
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
177
|
+
onClick={() => {
|
|
178
|
+
void run('close')
|
|
179
|
+
}}
|
|
180
|
+
>
|
|
181
|
+
<svg viewBox="0 0 16 16" aria-hidden="true">
|
|
182
|
+
<path d="m3.5 3.5 9 9m0-9-9 9" />
|
|
183
|
+
</svg>
|
|
184
|
+
</button>
|
|
185
|
+
</div>
|
|
186
|
+
</header>
|
|
187
|
+
)
|
|
188
|
+
}
|