@orbit-work/ui 0.1.1 → 0.1.2-preview.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.
- package/dist/index.cjs +13 -13
- package/dist/index.d.cts +19 -2
- package/dist/index.d.ts +19 -2
- package/dist/index.js +13 -13
- package/dist/orbitwork.css +82 -18
- package/package.json +1 -1
- package/src/components/composite/ContextUsageIndicatorView/index.tsx +1 -1
- package/src/components/composite/Icons/index.tsx +10 -4
- package/src/components/composite/Markdown/MarkdownStyleSource.test.mjs +16 -1
- package/src/components/domain/desktop/DesktopLibraryAuthorizationDialog/DesktopLibraryAuthorizationDialogSource.test.mjs +22 -0
- package/src/components/domain/desktop/DesktopLibraryAuthorizationDialog/index.tsx +72 -57
- package/src/components/domain/desktop/DesktopLibraryPageView/DesktopLibraryPageView.stories.tsx +7 -0
- package/src/components/domain/desktop/DesktopLibraryPageView/index.tsx +14 -0
- package/src/components/domain/desktop/DesktopLoginView/DesktopLoginViewSource.test.mjs +8 -1
- package/src/components/domain/desktop/DesktopLoginView/index.tsx +28 -14
- package/src/components/domain/desktop/DesktopSidebarView/DesktopSidebarViewSource.test.mjs +4 -2
- package/src/components/domain/desktop/DesktopSidebarView/index.tsx +43 -7
- package/src/components/domain/desktop/DesktopSitesListView/index.tsx +2 -2
- package/src/components/primitives/Button/index.tsx +4 -1
- package/src/components/primitives/Menu/index.tsx +2 -1
- package/src/components/primitives/Select/index.tsx +103 -61
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
Folder,
|
|
19
19
|
Grid,
|
|
20
20
|
Icon,
|
|
21
|
+
Memo,
|
|
21
22
|
More,
|
|
22
23
|
Package,
|
|
23
24
|
PanelSidebar,
|
|
@@ -28,11 +29,12 @@ import {
|
|
|
28
29
|
Trash,
|
|
29
30
|
} from "../../../composite/Icons"
|
|
30
31
|
import { TextField } from "../../../composite/NativeControls"
|
|
31
|
-
import { OrbitworkLockup } from "../../../composite/OrbitworkLogo"
|
|
32
|
+
import { OrbitworkLockup, OrbitworkMark } from "../../../composite/OrbitworkLogo"
|
|
32
33
|
import { ProjectSelect } from "../../../composite/ProjectSelect"
|
|
33
34
|
import { Avatar, Button } from "../../../primitives"
|
|
34
35
|
import { MenuSurface } from "../../../primitives/Menu"
|
|
35
36
|
import type { MenuItem } from "../../../primitives/Menu"
|
|
37
|
+
import { Spinner } from "../../../primitives/Spinner"
|
|
36
38
|
import { DesktopAccountMenuView, updateRowText } from "../DesktopAccountMenuView"
|
|
37
39
|
|
|
38
40
|
export type DesktopSidebarNavId = "new" | "skills" | "sites" | "cron" | "files" | "library"
|
|
@@ -111,6 +113,12 @@ export interface DesktopSidebarViewProps {
|
|
|
111
113
|
* 宿主 `features/sidebar/Sidebar.tsx` 补 `view === 'home' → 'new'` 即可,见交接单。
|
|
112
114
|
*/
|
|
113
115
|
activeNav?: DesktopSidebarNavId
|
|
116
|
+
/**
|
|
117
|
+
* 首次会话列表加载中(宿主 `loadSessions` 首轮)。为 true 且还没有任何会话行时,
|
|
118
|
+
* 列表区出品牌 mark + spinner 的启动态,而不是一块空白——首屏读得出「在加载」
|
|
119
|
+
* 而不是「没有会话」。分页/刷新加载不改变此值(那是 `loadingMore` 的语义)。
|
|
120
|
+
*/
|
|
121
|
+
sessionsLoading?: boolean
|
|
114
122
|
/**
|
|
115
123
|
* 是否在导航里出「我的空间」(A11-B1 · 裁决 16)。
|
|
116
124
|
*
|
|
@@ -346,6 +354,17 @@ export function DesktopSidebarView(props: DesktopSidebarViewProps): React.ReactE
|
|
|
346
354
|
onClick={() => props.onNavigate("files")}
|
|
347
355
|
/>
|
|
348
356
|
)}
|
|
357
|
+
{/* 资料库:恢复的入口(曾隐藏导致已接线的 LibraryPage 不可达——App.tsx 已把
|
|
358
|
+
baseView === "library" 路由到 workbenchView 并以 active prop 挂载 LibraryPage)。
|
|
359
|
+
放在「我的空间」之后;只有 running 的 thread 才禁用移动,archived 可以移动。 */}
|
|
360
|
+
<NavItem
|
|
361
|
+
id="library"
|
|
362
|
+
label="资料库"
|
|
363
|
+
title="资料库"
|
|
364
|
+
active={props.activeNav === "library"}
|
|
365
|
+
icon={<Memo />}
|
|
366
|
+
onClick={() => props.onNavigate("library")}
|
|
367
|
+
/>
|
|
349
368
|
</nav>
|
|
350
369
|
|
|
351
370
|
<div className="owrecent-head" data-tour-anchor="projects">
|
|
@@ -369,7 +388,13 @@ export function DesktopSidebarView(props: DesktopSidebarViewProps): React.ReactE
|
|
|
369
388
|
</div>
|
|
370
389
|
</div>
|
|
371
390
|
<div className="owprojects">
|
|
372
|
-
{projects.
|
|
391
|
+
{props.sessionsLoading && projects.every((project) => project.threads.length === 0) ? (
|
|
392
|
+
<div className="owprojects-boot" role="status" aria-live="polite">
|
|
393
|
+
<OrbitworkMark variant="auto" size={40} />
|
|
394
|
+
<Spinner size="md" />
|
|
395
|
+
</div>
|
|
396
|
+
) : (
|
|
397
|
+
projects.map((project) => (
|
|
373
398
|
<div key={project.id} className="owproject">
|
|
374
399
|
{renamingProject?.id === project.id ? (
|
|
375
400
|
<div className="owproject__head renaming">
|
|
@@ -569,15 +594,26 @@ export function DesktopSidebarView(props: DesktopSidebarViewProps): React.ReactE
|
|
|
569
594
|
{/* R2 · L2-1-1:「展开显示」原来是一行没有图标、没有 chevron、
|
|
570
595
|
与会话行同缩进同字号的 --ow-text-3 灰字(浅档 2.29:1),读起来像
|
|
571
596
|
一条被禁用的会话。补一枚 14 的 ChevDown 前置图标(与同族
|
|
572
|
-
.owthread-new 的 Plus 同语法),让它读成动作;字色并轨见 CSS。
|
|
573
|
-
|
|
574
|
-
{project.loadingMore ?
|
|
597
|
+
.owthread-new 的 Plus 同语法),让它读成动作;字色并轨见 CSS。
|
|
598
|
+
加载中同理:纯文案太朴素,换成 spinner + 文案(#34)。 */}
|
|
599
|
+
{project.loadingMore ? (
|
|
600
|
+
<>
|
|
601
|
+
<Spinner size="sm" />
|
|
602
|
+
加载中…
|
|
603
|
+
</>
|
|
604
|
+
) : (
|
|
605
|
+
<>
|
|
606
|
+
<ChevDown className="lead" aria-hidden="true" />
|
|
607
|
+
{project.loadError ? "加载失败,点击重试" : "展开显示"}
|
|
608
|
+
</>
|
|
609
|
+
)}
|
|
575
610
|
</Button>
|
|
576
611
|
)}
|
|
577
612
|
</div>
|
|
578
613
|
)}
|
|
579
614
|
</div>
|
|
580
|
-
|
|
615
|
+
))
|
|
616
|
+
)}
|
|
581
617
|
</div>
|
|
582
618
|
|
|
583
619
|
{projectsMenu && (
|
|
@@ -614,7 +650,7 @@ export function DesktopSidebarView(props: DesktopSidebarViewProps): React.ReactE
|
|
|
614
650
|
key: "move",
|
|
615
651
|
label: "移动",
|
|
616
652
|
icon: <Folder />,
|
|
617
|
-
disabled: Boolean(threadById(threadMenu.id)?.running
|
|
653
|
+
disabled: Boolean(threadById(threadMenu.id)?.running),
|
|
618
654
|
},
|
|
619
655
|
...(!threadById(threadMenu.id)?.archived
|
|
620
656
|
? ([
|
|
@@ -352,8 +352,8 @@ export function DesktopSitesListView({
|
|
|
352
352
|
{(hasMore || error) && (
|
|
353
353
|
<div className="sites-grid__more">
|
|
354
354
|
{error && <InlineMessage variant="error" title="暂时无法加载更多站点" detail={error} />}
|
|
355
|
-
<Button
|
|
356
|
-
{
|
|
355
|
+
<Button loading={loading} onClick={error ? onRetry : onLoadMore}>
|
|
356
|
+
{error ? "重试" : "加载更多"}
|
|
357
357
|
</Button>
|
|
358
358
|
</div>
|
|
359
359
|
)}
|
|
@@ -207,7 +207,10 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
|
|
207
207
|
)
|
|
208
208
|
)}
|
|
209
209
|
{children && (
|
|
210
|
-
|
|
210
|
+
// min-w-0:受 max-w 约束的按钮(如 composer 模型选择器)要能在窄容器里
|
|
211
|
+
// 收缩,否则内部 nowrap 文本撑破按钮边界、溢出覆盖相邻控件。真正的省略
|
|
212
|
+
// 号由调用方的 label span(overflow-hidden text-ellipsis)承担。
|
|
213
|
+
<span data-ow-part="label" className="inline-flex min-w-0 items-center gap-[inherit] whitespace-nowrap">
|
|
211
214
|
{children}
|
|
212
215
|
</span>
|
|
213
216
|
)}
|
|
@@ -900,8 +900,9 @@ export function Menu({
|
|
|
900
900
|
/**
|
|
901
901
|
* 向上找最近的带 transform/perspective/filter/will-change 的祖先 —— 它会给
|
|
902
902
|
* fixed 元素造出新的包含块,让坐标不再相对视口。没有就返回 null。
|
|
903
|
+
* (Select 的 portal 菜单量位也复用这个。)
|
|
903
904
|
*/
|
|
904
|
-
function findFixedOffsetAncestor(el: HTMLElement): HTMLElement | null {
|
|
905
|
+
export function findFixedOffsetAncestor(el: HTMLElement): HTMLElement | null {
|
|
905
906
|
let parent = el.parentElement
|
|
906
907
|
while (parent && parent !== document.body && parent !== document.documentElement) {
|
|
907
908
|
const style = getComputedStyle(parent)
|
|
@@ -8,11 +8,14 @@ import {
|
|
|
8
8
|
forwardRef,
|
|
9
9
|
useEffect,
|
|
10
10
|
useId,
|
|
11
|
+
useLayoutEffect,
|
|
11
12
|
useRef,
|
|
12
13
|
useState,
|
|
13
14
|
} from "react"
|
|
15
|
+
import { createPortal } from "react-dom" // ui-imports-guard-disable-line
|
|
14
16
|
import { cn } from "../../../lib/utils"
|
|
15
17
|
import { Check, ChevDown } from "../../composite/Icons"
|
|
18
|
+
import { findFixedOffsetAncestor } from "../Menu"
|
|
16
19
|
|
|
17
20
|
export type SelectOption = {
|
|
18
21
|
value: string
|
|
@@ -52,22 +55,14 @@ const selectTriggerVariants = cva(
|
|
|
52
55
|
|
|
53
56
|
const selectMenuVariants = cva(
|
|
54
57
|
[
|
|
55
|
-
|
|
58
|
+
// R6:菜单纯 portal 到 document.body 做 fixed 定位(量位见下方 effect)。
|
|
59
|
+
// 原先 absolute 挂在触发器下,会被 Dialog 这类 overflow-hidden 立面裁到只剩
|
|
60
|
+
// 一行——裁剪抬 z-index 解决不了(MenuSurface portal 档注释同款结论)。
|
|
61
|
+
"p-1.5 max-h-[264px] overflow-y-auto",
|
|
56
62
|
"bg-[var(--ow-popover-bg)] border border-ow-border",
|
|
57
63
|
"shadow-[var(--ow-shadow-pop)]",
|
|
58
64
|
"[border-radius:var(--ow-radius-menu)]",
|
|
59
65
|
].join(" "),
|
|
60
|
-
{
|
|
61
|
-
variants: {
|
|
62
|
-
position: {
|
|
63
|
-
down: "top-[calc(100%+4px)]",
|
|
64
|
-
up: "bottom-[calc(100%+4px)]",
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
defaultVariants: {
|
|
68
|
-
position: "down",
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
66
|
)
|
|
72
67
|
|
|
73
68
|
const selectOptionVariants = cva(
|
|
@@ -187,20 +182,28 @@ export function Select({
|
|
|
187
182
|
className,
|
|
188
183
|
}: SelectProps) {
|
|
189
184
|
const [open, setOpen] = useState(false)
|
|
190
|
-
const [up, setUp] = useState(false)
|
|
191
185
|
const [activeIndex, setActiveIndex] = useState(-1)
|
|
186
|
+
const [menuPos, setMenuPos] = useState<{ top: number; left: number } | null>(null)
|
|
192
187
|
const wrapRef = useRef<HTMLDivElement>(null)
|
|
193
188
|
const buttonRef = useRef<HTMLButtonElement>(null)
|
|
194
189
|
const listRef = useRef<HTMLDivElement>(null)
|
|
195
190
|
const listId = useId()
|
|
196
191
|
const selected = options.find((option) => option.value === value)
|
|
197
192
|
const optionId = (index: number) => `${listId}-opt-${index}`
|
|
193
|
+
// 菜单纯 portal 到 body 后与 Dialog 同处 --ow-z-modal 层:弹层晚于对话框挂载(DOM
|
|
194
|
+
// 序在后),同层相叠画在立面之上;用 --ow-z-dropdown 会落到模态 scrim 下面。
|
|
198
195
|
|
|
199
196
|
useEffect(() => {
|
|
200
197
|
if (!open) return undefined
|
|
201
198
|
|
|
202
199
|
const closeOnOutsideClick = (event: MouseEvent) => {
|
|
203
|
-
|
|
200
|
+
const target = event.target as Node
|
|
201
|
+
// 菜单 portal 到 body,不在 wrapRef 子树里,命中面本体时不算外点。
|
|
202
|
+
if (
|
|
203
|
+
wrapRef.current &&
|
|
204
|
+
!wrapRef.current.contains(target) &&
|
|
205
|
+
!(listRef.current && listRef.current.contains(target))
|
|
206
|
+
) {
|
|
204
207
|
setOpen(false)
|
|
205
208
|
}
|
|
206
209
|
}
|
|
@@ -225,12 +228,48 @@ export function Select({
|
|
|
225
228
|
listRef.current?.querySelector(`[data-index="${activeIndex}"]`)?.scrollIntoView({ block: "nearest" })
|
|
226
229
|
}, [open, activeIndex])
|
|
227
230
|
|
|
231
|
+
// Portal 量位:贴触发器下缘,下方空间不足翻到下缘上方;滚动/缩放时跟锚点重算。
|
|
232
|
+
// transform 祖先(如 Dialog 的入场动画)会改写 fixed 的包含块,偏移要减回去。
|
|
233
|
+
useLayoutEffect(() => {
|
|
234
|
+
if (!open) return undefined
|
|
235
|
+
const el = listRef.current
|
|
236
|
+
const button = buttonRef.current
|
|
237
|
+
if (!el || !button) return undefined
|
|
238
|
+
const measure = () => {
|
|
239
|
+
const anchorRect = button.getBoundingClientRect()
|
|
240
|
+
// 菜单宽跟随触发器(原 absolute left-0 right-0 的同款语义),先写死再量高度。
|
|
241
|
+
el.style.width = `${anchorRect.width}px`
|
|
242
|
+
const dropRect = el.getBoundingClientRect()
|
|
243
|
+
const offsetParent = findFixedOffsetAncestor(el)
|
|
244
|
+
const offsetX = offsetParent ? offsetParent.getBoundingClientRect().left : 0
|
|
245
|
+
const offsetY = offsetParent ? offsetParent.getBoundingClientRect().top : 0
|
|
246
|
+
|
|
247
|
+
let top = anchorRect.bottom + 4 - offsetY
|
|
248
|
+
if (anchorRect.bottom + 4 + dropRect.height > window.innerHeight - 8) {
|
|
249
|
+
top = anchorRect.top - dropRect.height - 4 - offsetY
|
|
250
|
+
}
|
|
251
|
+
if (top + offsetY < 8) top = 8 - offsetY
|
|
252
|
+
let left = anchorRect.left - offsetX
|
|
253
|
+
if (left + offsetX + dropRect.width > window.innerWidth - 8) {
|
|
254
|
+
left = window.innerWidth - dropRect.width - 8 - offsetX
|
|
255
|
+
}
|
|
256
|
+
if (left + offsetX < 8) left = 8 - offsetX
|
|
257
|
+
|
|
258
|
+
setMenuPos((current) => (current && current.top === top && current.left === left ? current : { top, left }))
|
|
259
|
+
}
|
|
260
|
+
// 等菜单渲染完再量,否则拿不到高度。
|
|
261
|
+
const raf = requestAnimationFrame(measure)
|
|
262
|
+
window.addEventListener("resize", measure)
|
|
263
|
+
window.addEventListener("scroll", measure, true)
|
|
264
|
+
return () => {
|
|
265
|
+
cancelAnimationFrame(raf)
|
|
266
|
+
window.removeEventListener("resize", measure)
|
|
267
|
+
window.removeEventListener("scroll", measure, true)
|
|
268
|
+
}
|
|
269
|
+
}, [open, options.length])
|
|
270
|
+
|
|
228
271
|
const openMenu = () => {
|
|
229
272
|
if (disabled) return
|
|
230
|
-
if (buttonRef.current) {
|
|
231
|
-
const rect = buttonRef.current.getBoundingClientRect()
|
|
232
|
-
setUp(window.innerHeight - rect.bottom < 300)
|
|
233
|
-
}
|
|
234
273
|
const selectedIndex = options.findIndex((option) => option.value === value)
|
|
235
274
|
setActiveIndex(selectedIndex >= 0 ? selectedIndex : 0)
|
|
236
275
|
setOpen(true)
|
|
@@ -313,52 +352,55 @@ export function Select({
|
|
|
313
352
|
<span className="text-[color:var(--ow-placeholder)]">{placeholder}</span>
|
|
314
353
|
)}
|
|
315
354
|
</SelectTrigger>
|
|
316
|
-
{open &&
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
355
|
+
{open &&
|
|
356
|
+
createPortal(
|
|
357
|
+
<div
|
|
358
|
+
ref={listRef}
|
|
359
|
+
id={listId}
|
|
360
|
+
data-ow-part="list"
|
|
361
|
+
className={cn(selectMenuVariants(), "fixed z-[var(--ow-z-modal)]", !menuPos && "invisible")}
|
|
362
|
+
style={{ top: menuPos?.top ?? 0, left: menuPos?.left ?? 0 }}
|
|
363
|
+
role="listbox"
|
|
364
|
+
>
|
|
365
|
+
{options.map((option, index) => (
|
|
366
|
+
<button
|
|
367
|
+
key={option.value}
|
|
368
|
+
id={optionId(index)}
|
|
369
|
+
type="button"
|
|
370
|
+
role="option"
|
|
371
|
+
tabIndex={-1}
|
|
372
|
+
aria-selected={option.value === value}
|
|
373
|
+
data-index={index}
|
|
374
|
+
data-ow-role={option.value === value ? "selected" : "option"}
|
|
375
|
+
className={selectOptionVariants({ active: index === activeIndex })}
|
|
376
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
377
|
+
onMouseEnter={() => setActiveIndex(index)}
|
|
378
|
+
onClick={() => pick(option.value)}
|
|
379
|
+
>
|
|
380
|
+
<span className="flex-1 min-w-0 flex flex-col gap-0.5">
|
|
381
|
+
<span
|
|
382
|
+
className={cn(
|
|
383
|
+
"overflow-hidden text-ellipsis whitespace-nowrap",
|
|
384
|
+
option.value === value && "font-medium",
|
|
385
|
+
)}
|
|
386
|
+
>
|
|
387
|
+
{option.label}
|
|
388
|
+
</span>
|
|
389
|
+
{option.sub && (
|
|
390
|
+
<span className="text-ow-fs-eyebrow font-normal leading-snug text-[color:var(--ow-text-3)] overflow-hidden text-ellipsis whitespace-nowrap">
|
|
391
|
+
{option.sub}
|
|
392
|
+
</span>
|
|
344
393
|
)}
|
|
345
|
-
>
|
|
346
|
-
{option.label}
|
|
347
394
|
</span>
|
|
348
|
-
{option.
|
|
349
|
-
<
|
|
350
|
-
{option.sub}
|
|
351
|
-
</span>
|
|
395
|
+
{option.value === value && (
|
|
396
|
+
<Check className="shrink-0 w-4 h-4 text-[color:var(--ow-text)]" aria-hidden="true" />
|
|
352
397
|
)}
|
|
353
|
-
</
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
{footer}
|
|
360
|
-
</div>
|
|
361
|
-
)}
|
|
398
|
+
</button>
|
|
399
|
+
))}
|
|
400
|
+
{footer}
|
|
401
|
+
</div>,
|
|
402
|
+
document.body,
|
|
403
|
+
)}
|
|
362
404
|
</div>
|
|
363
405
|
)
|
|
364
406
|
}
|