@deepseek-ai/dsh-client-ui-dockkit 0.1.7-rc.1 → 0.2.0-rc.1
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/README.i18n.yaml +39 -5
- package/README.md +9 -1
- package/README.zh.md +9 -1
- package/lib/components/dockkit.module.css +43 -34
- package/lib/index.js +81 -37
- package/lib/types/contract/adapter.d.ts +8 -0
- package/package.json +4 -4
package/README.i18n.yaml
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
|
-
# Bilingual-pair consistency record (docs/i18n/README.md):
|
|
2
|
-
#
|
|
3
|
-
#
|
|
1
|
+
# Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
|
|
2
|
+
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
|
3
|
+
# After editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/client/ui-dockkit/README.md
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/:
|
|
6
|
+
en: 93fc2e9e86fc50f8
|
|
7
|
+
zh: 61c7c38a66935ad6
|
|
8
|
+
/deepseek-ai-dsh-client-ui-dockkit:
|
|
9
|
+
en: 9d34bd85fc9e76b4
|
|
10
|
+
zh: 615d1d2eab451c87
|
|
11
|
+
/deepseek-ai-dsh-client-ui-dockkit/summary:
|
|
12
|
+
en: 3ee4584a73215ef4
|
|
13
|
+
zh: 051170a6d0993fa5
|
|
14
|
+
/deepseek-ai-dsh-client-ui-dockkit/table-of-contents:
|
|
15
|
+
en: 42038fdc402b9c2a
|
|
16
|
+
zh: 99890f8f908624ce
|
|
17
|
+
/deepseek-ai-dsh-client-ui-dockkit/the-two-layers:
|
|
18
|
+
en: ea872ff2c2089037
|
|
19
|
+
zh: b3ec3a218eacd5c8
|
|
20
|
+
/deepseek-ai-dsh-client-ui-dockkit/embedding-it:
|
|
21
|
+
en: 47b9852393643b6a
|
|
22
|
+
zh: ce5f2ab60eec9564
|
|
23
|
+
/deepseek-ai-dsh-client-ui-dockkit/interaction-rules-worth-keeping:
|
|
24
|
+
en: 7e5c1e9a9f9f244f
|
|
25
|
+
zh: 7e3a8fcef86bc7dd
|
|
26
|
+
/deepseek-ai-dsh-client-ui-dockkit/build-shape:
|
|
27
|
+
en: 21216ba5798d7f5f
|
|
28
|
+
zh: ac505fdcd5c114d4
|
|
29
|
+
/deepseek-ai-dsh-client-ui-dockkit/model-experience:
|
|
30
|
+
en: cdebb91645623401
|
|
31
|
+
zh: fe262db316714fa8
|
|
32
|
+
/deepseek-ai-dsh-client-ui-dockkit/model-experience/kv-cache-effect:
|
|
33
|
+
en: ca75c51c89c2c9b0
|
|
34
|
+
zh: f57cc48a1b5cbfa9
|
|
35
|
+
/deepseek-ai-dsh-client-ui-dockkit/known-limitations-and-deferred-work:
|
|
36
|
+
en: 4323810ca5a87e6c
|
|
37
|
+
zh: 3a24af2ea4467ab7
|
|
38
|
+
/deepseek-ai-dsh-client-ui-dockkit/known-limitations-and-deferred-work/dev-note:
|
|
39
|
+
en: 9eda9e1ea3785ca7
|
|
40
|
+
zh: b734eb644b27e474
|
package/README.md
CHANGED
|
@@ -39,6 +39,8 @@ A docking layout kit: a split tree of tabbed panes with invertible operations, a
|
|
|
39
39
|
|
|
40
40
|
**The components** render a layout snapshot and report settled intents — one per gesture, never a drag frame. A drag previews in local state while the gesture's own facts stay in its closure; on release the net result leaves through one `DockIntents` call — a strip release reports the caret slot as drawn, the dragged chip counted, and `planPlaceTab` turns that into the reorder or the move. That is what lets an embedder record exactly one history entry per gesture. The strip follows the WAI-ARIA tabs pattern with manual activation: the selected chip is in the tab order; Left and Right (wrapping), Home, and End move focus between chips without selecting; Enter or Space selects the focused chip through the same intent as a click. A chip is a capsule carrying one control, its close; the context menu (a secondary press on the chip) carries the same close plus the embedder's items — a menu that would hold no item at all never shows — and renders in a portal positioned against the chip because the chip box clips its overflow on purpose (see below). After the chips sits the add control, which asks the embedder (`DockIntents.addTab`) to seat its seeded tab; the embedder's `canAddTab(paneId)` decides per pane whether the control is drawn at all. Copying a tab has no kit control — it is the embedder's API — and floating is the drag released clear of the surface.
|
|
41
41
|
|
|
42
|
+
Tab context menus show the close action name without a shortcut hint: they act on the clicked tab, while shortcuts can target another focused pane. An unmodified Escape dismisses the foreground tab menu without closing the tab and returns focus from a menu item to its tab without drawing a focus outline. Tab and arrow navigation retain the tab’s visible focus indicator. Composition and held-key repeats do not dismiss it.
|
|
43
|
+
|
|
42
44
|
<a id="embedding-it"></a>
|
|
43
45
|
## Embedding it
|
|
44
46
|
|
|
@@ -58,11 +60,13 @@ A tab's `kind` is an opaque string. Seeded tabs are factories (`DockControllerOp
|
|
|
58
60
|
|
|
59
61
|
`DockLayout`, used by the Sidebar, keeps each tab in a stable DOM cell across selection, pane moves and floating. It accepts one docked pane or two horizontal panes; CSS Grid resolves their widths. `keepMounted(tab)` retains visited bodies and `active` controls Session visibility without reparenting content.
|
|
60
62
|
|
|
61
|
-
`DockSurface` is the docked area for the separate recursive renderer. Chrome around it — a rail, a collapsed presentation, any history controls — belongs to the embedder, which reads `state.expanded` and decides; the kit ships no undo/redo control of its own. Surface-wide controls the embedder does want on the surface go through the `chrome` prop, which the kit places at the far end of the top-right pane's tab strip (the last child of every row split, the first of every column split), so a surface needs no header row of its own. `FloatLayer` owns its own gestures and positions panels in viewport coordinates, so it may be mounted anywhere, including a portal.
|
|
63
|
+
`DockSurface` is the docked area for the separate recursive renderer. Chrome around it — a rail, a collapsed presentation, any history controls — belongs to the embedder, which reads `state.expanded` and decides; the kit ships no undo/redo control of its own. Surface-wide controls the embedder does want on the surface go through the `chrome` prop, which the kit places at the far end of the top-right pane's tab strip (the last child of every row split, the first of every column split), so a surface needs no header row of its own. `FloatLayer` owns its own gestures and positions panels in viewport coordinates, so it may be mounted anywhere, including a portal. Both float renderers keep the header at least 20px below the Windows caption (20px below the viewport top in native fullscreen), including newly opened and restored panels. Dragging and resizing start at the displayed position.
|
|
62
64
|
|
|
63
65
|
<a id="interaction-rules-worth-keeping"></a>
|
|
64
66
|
## Interaction rules worth keeping
|
|
65
67
|
|
|
68
|
+
The split control accepts localized tooltip text, separate effective keys through `splitPaneKeys`, and an ARIA combination from its embedder. Close controls receive their keys through `closeTabKeys`. A disabled control has a keyboard-focusable wrapper that explains the pane-budget or width restriction. Docked and floating pane containers can receive programmatic focus without entering the normal tab sequence or drawing a focus outline; their controls retain their own keyboard focus indicators. Tab navigation and selection use unmodified keys and leave composition input to its owner.
|
|
69
|
+
|
|
66
70
|
These are not stylistic; each one fixes a defect found in a real browser.
|
|
67
71
|
|
|
68
72
|
- **Capture the pointer** when a gesture starts. Without it any scroll container the pointer crosses can claim the gesture, which the browser reports as a cancelled pointer and an abandoned drag. Capture is hardening — the window listeners carry the gesture either way, so an environment without the API still works.
|
|
@@ -99,11 +103,15 @@ None; this package neither assembles nor sends a provider request.
|
|
|
99
103
|
<a id="dev-note"></a>
|
|
100
104
|
### Dev Note
|
|
101
105
|
|
|
106
|
+
Menus use the shared `MenuSurface` material, including the macOS backing for background blur; custom content follows the [menu rules](../../../docs/web-styling.md#component-rules).
|
|
107
|
+
|
|
102
108
|
<details>
|
|
103
109
|
<summary>Working context for maintainers — click to expand</summary>
|
|
104
110
|
|
|
105
111
|
None.
|
|
106
112
|
|
|
113
|
+
The embedder supplies effective close keycaps and ARIA combinations for tab and floating close controls. Close controls expose the same tooltip on hover and keyboard focus. Tab close tooltips stay hidden while their pane's context menu is open.
|
|
114
|
+
|
|
107
115
|
</details>
|
|
108
116
|
|
|
109
117
|
**Runtime invariant:** No companion is published. The engine is pure functions over plain data and the components report intents only; the operation sequence's invertibility and the settle rule are asserted directly by this package's engine specs, and no cordis service is provided or observed.
|
package/README.zh.md
CHANGED
|
@@ -39,6 +39,8 @@ kind: "package-reference"
|
|
|
39
39
|
|
|
40
40
|
**组件**渲染布局快照并上报已落定的意图——每次手势一条,绝不上报拖动帧。拖动过程中在本地状态里预览,手势自身的事实留在它的闭包里;松手时净结果通过一次 `DockIntents` 调用离开——在标签条上松手上报的是按绘制顺序数出的插入槽位(被拖的 chip 也计入),由 `planPlaceTab` 换算成重排或移动。正是这一点让嵌入方能为每次手势记录恰好一条历史。标签条遵循 WAI-ARIA tabs 模式的手动激活:选中的 chip 在 Tab 键序里;左右方向键(循环)、Home、End 只在 chip 之间移动焦点而不选中;Enter 或空格选中当前聚焦的 chip,走与点击相同的意图。chip 是一个胶囊,携带唯一的控件——它的关闭按钮;上下文菜单(在 chip 上的次键按下)携带同样的关闭项加上嵌入方的条目——一个连一项都没有的菜单绝不展示——并渲染在按 chip 定位的 portal 里,因为 chip 盒会故意裁掉溢出(见下文)。chip 之后是添加控件,它请嵌入方(`DockIntents.addTab`)安放其种子 tab;嵌入方的 `canAddTab(paneId)` 按格决定是否绘制该控件。复制 tab 没有套件控件——那是嵌入方的 API——而浮出就是把拖动松手在停靠区之外。
|
|
41
41
|
|
|
42
|
+
标签上下文菜单只显示关闭操作名,不显示快捷键:菜单操作针对被点击的标签,快捷键则可能作用于其他焦点分栏。无修饰键的 Esc 关闭前台标签菜单而不关闭标签,并将菜单项的焦点交还标签,不绘制焦点外轮廓线。Tab 和方向键导航保留标签页的可见焦点提示。输入法组合输入和长按重复事件不关闭菜单。
|
|
43
|
+
|
|
42
44
|
<a id="embedding-it"></a>
|
|
43
45
|
## 如何嵌入
|
|
44
46
|
|
|
@@ -58,11 +60,13 @@ tab 的 `kind` 是不透明字符串。种子 tab 是工厂(`DockControllerOpt
|
|
|
58
60
|
|
|
59
61
|
Sidebar 使用的 `DockLayout` 在选中项变化、跨格移动与浮动期间,把每个 tab 保留在稳定的 DOM 容器中。它接受一个停靠格或左右两个格,由 CSS Grid 计算宽度。`keepMounted(tab)` 保留已访问正文,`active` 控制 Session 可见性,不改变内容的父子关系。
|
|
60
62
|
|
|
61
|
-
`DockSurface` 是独立递归渲染器的停靠区。它周围的 chrome——轨道、折叠形态、任何历史控件——属于嵌入方,由嵌入方读取 `state.expanded` 后自行决定;套件不自带撤销/重做控件。嵌入方确实想放到面上的整面控件通过 `chrome` prop 传入,套件把它放在右上格 tab 条的最末端(每个横向分裂的最后一个子节点、每个纵向分裂的第一个子节点),因此停靠面不需要自己的标题行。`FloatLayer` 拥有自己的手势并以视口坐标定位浮窗,因此可以挂在任何位置,包括 portal 里。
|
|
63
|
+
`DockSurface` 是独立递归渲染器的停靠区。它周围的 chrome——轨道、折叠形态、任何历史控件——属于嵌入方,由嵌入方读取 `state.expanded` 后自行决定;套件不自带撤销/重做控件。嵌入方确实想放到面上的整面控件通过 `chrome` prop 传入,套件把它放在右上格 tab 条的最末端(每个横向分裂的最后一个子节点、每个纵向分裂的第一个子节点),因此停靠面不需要自己的标题行。`FloatLayer` 拥有自己的手势并以视口坐标定位浮窗,因此可以挂在任何位置,包括 portal 里。 两种浮窗渲染器都让标题栏与 Windows 顶栏至少间隔 20px(原生全屏时距视口顶部 20px),新建和恢复的浮窗均适用。拖动和调整大小从实际显示位置开始。
|
|
62
64
|
|
|
63
65
|
<a id="interaction-rules-worth-keeping"></a>
|
|
64
66
|
## 值得保留的交互规则
|
|
65
67
|
|
|
68
|
+
分栏控件接收嵌入方传入的本地化提示文本、`splitPaneKeys` 独立按键及 ARIA 组合。关闭控件通过 `closeTabKeys` 接收按键。禁用控件由可键盘聚焦的容器承载,说明分栏数量或宽度限制。停靠与浮动面板容器支持程序设置焦点,不进入正常 Tab 顺序,也不绘制焦点外框;内部控件保留各自的键盘焦点提示。标签导航和选择只使用无修饰键组合,输入法按键交回其所属输入区域。
|
|
69
|
+
|
|
66
70
|
这些不是风格偏好;每一条都修复了在真实浏览器里发现的缺陷。
|
|
67
71
|
|
|
68
72
|
- **手势开始时捕获指针。** 不捕获的话,指针经过的任何滚动容器都可能接管手势,浏览器会将其报告为指针取消和拖动中止。捕获是加固——无论如何都由 window 监听器承载手势,所以没有该 API 的环境照样可用。
|
|
@@ -99,11 +103,15 @@ Sidebar 使用的 `DockLayout` 在选中项变化、跨格移动与浮动期间
|
|
|
99
103
|
<a id="dev-note"></a>
|
|
100
104
|
### 开发备注
|
|
101
105
|
|
|
106
|
+
菜单采用共享 `MenuSurface` 材质,包括用于背景模糊的 macOS 底层;自定义内容遵循[菜单规则](../../../docs/web-styling.zh.md#component-rules)。
|
|
107
|
+
|
|
102
108
|
<details>
|
|
103
109
|
<summary>维护者工作上下文——点击展开</summary>
|
|
104
110
|
|
|
105
111
|
无。
|
|
106
112
|
|
|
113
|
+
嵌入方为标签和浮动面板的关闭控件提供有效关闭键帽及 ARIA 组合。关闭控件在悬停和键盘聚焦时显示相同 Tooltip。分栏的上下文菜单打开时,该分栏的标签关闭 Tooltip 保持隐藏。
|
|
114
|
+
|
|
107
115
|
</details>
|
|
108
116
|
|
|
109
117
|
**运行时不变量:** 不发布 companion。引擎是作用于纯数据的纯函数,组件只上报意图;操作序列的可逆性与 settle 规则由本包的引擎 spec 直接断言,不提供也不观察任何 Cordis 服务。
|
|
@@ -215,6 +215,11 @@
|
|
|
215
215
|
overflow: hidden;
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
.pane:focus,
|
|
219
|
+
.float:focus {
|
|
220
|
+
outline: none;
|
|
221
|
+
}
|
|
222
|
+
|
|
218
223
|
/* A 38px row whose 28px content band sits at its bottom: chips fill the band,
|
|
219
224
|
so they end flush with the strip's bottom edge, and the 28px add control, the
|
|
220
225
|
28px split control, and the embedder's 28px chrome centre on the band's one
|
|
@@ -297,16 +302,17 @@
|
|
|
297
302
|
margin-left: 4px;
|
|
298
303
|
}
|
|
299
304
|
|
|
300
|
-
/* macOS desktop: an embedder may pin this surface
|
|
301
|
-
|
|
302
|
-
in
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
non-visible boxes when it collects
|
|
305
|
+
/* macOS desktop: an embedder may pin this surface at the window's top edge (the
|
|
306
|
+
right sidebar does, in both presentations). The strip row is marked
|
|
307
|
+
`data-window-drag` in markup (TabPanel), so ui-web base.css's single darwin
|
|
308
|
+
rule drags it over its own box; the chip run, the strip-end chrome, the pane
|
|
309
|
+
body, and the split divider subtract their whole boxes so tabs, controls,
|
|
310
|
+
content, and the resize grab stay usable wherever they overlap that row. The
|
|
311
|
+
divider must be named here: it is a plain div, so ui-web base.css's
|
|
312
|
+
interactive-element subtraction never matches it. A floating pane subtracts its
|
|
313
|
+
own box the same way, so the strip row never drags the window over it. Hidden
|
|
314
|
+
surfaces contribute nothing: Blink skips non-visible boxes when it collects
|
|
315
|
+
draggable regions. */
|
|
310
316
|
:global(html[data-platform='darwin']) .stripTabs,
|
|
311
317
|
:global(html[data-platform='darwin']) .stripChrome,
|
|
312
318
|
:global(html[data-platform='darwin']) .paneBody,
|
|
@@ -334,8 +340,8 @@
|
|
|
334
340
|
background: var(--dsw-alias-border-l4);
|
|
335
341
|
}
|
|
336
342
|
|
|
337
|
-
/* The active chip is a filled
|
|
338
|
-
slots beside it go blank, so the row reads as the
|
|
343
|
+
/* The active chip is a filled tab and needs no rule against it: the two
|
|
344
|
+
slots beside it go blank, so the row reads as the tab between bare
|
|
339
345
|
chips. A caret in either slot still draws. */
|
|
340
346
|
.tabActive + .slot:not(.slotCaret)::before,
|
|
341
347
|
.slot:not(.slotCaret):has(+ .tabActive)::before {
|
|
@@ -348,7 +354,7 @@
|
|
|
348
354
|
background: var(--dsw-alias-brand-primary-new-colorprimary-new-color);
|
|
349
355
|
}
|
|
350
356
|
|
|
351
|
-
/* The chip: a 28px
|
|
357
|
+
/* The chip: a 28px tab whose title runs to the right inset. The close
|
|
352
358
|
control is not in the flow — it sits over the title's last 14px and shows
|
|
353
359
|
while the chip is active, hovered, or holds focus — so a chip is the same
|
|
354
360
|
width with and without it and nothing shifts on hover. */
|
|
@@ -365,7 +371,7 @@
|
|
|
365
371
|
font-size: var(--dsh-content-font-size-secondary, 13px);
|
|
366
372
|
line-height: 1;
|
|
367
373
|
white-space: nowrap;
|
|
368
|
-
border-radius:
|
|
374
|
+
border-radius: var(--dsw-radius-sm);
|
|
369
375
|
cursor: pointer;
|
|
370
376
|
touch-action: none;
|
|
371
377
|
user-select: none;
|
|
@@ -422,7 +428,7 @@
|
|
|
422
428
|
line-height: 1;
|
|
423
429
|
background: transparent;
|
|
424
430
|
border: none;
|
|
425
|
-
border-radius:
|
|
431
|
+
border-radius: var(--dsw-radius-sm);
|
|
426
432
|
cursor: pointer;
|
|
427
433
|
opacity: 0;
|
|
428
434
|
/* Hidden means untouchable too: without this a touch press on the chip's
|
|
@@ -439,7 +445,7 @@
|
|
|
439
445
|
pointer-events: auto;
|
|
440
446
|
}
|
|
441
447
|
|
|
442
|
-
/* The chip's height and corner, so it reads as one more
|
|
448
|
+
/* The chip's height and corner, so it reads as one more tab in the row. */
|
|
443
449
|
.addTab {
|
|
444
450
|
display: flex;
|
|
445
451
|
flex: none;
|
|
@@ -452,7 +458,7 @@
|
|
|
452
458
|
line-height: 1;
|
|
453
459
|
background: transparent;
|
|
454
460
|
border: none;
|
|
455
|
-
border-radius:
|
|
461
|
+
border-radius: var(--dsw-radius-sm);
|
|
456
462
|
cursor: pointer;
|
|
457
463
|
}
|
|
458
464
|
|
|
@@ -470,13 +476,13 @@
|
|
|
470
476
|
background: var(--dsw-alias-interactive-bg-hover);
|
|
471
477
|
}
|
|
472
478
|
|
|
473
|
-
/* The active chip is the filled
|
|
479
|
+
/* The active chip is the filled tab, in the tag fill; the rest are bare text. */
|
|
474
480
|
.tabActive {
|
|
475
481
|
color: var(--dsw-alias-label-primary);
|
|
476
482
|
background: var(--dsw-alias-markdown-tag);
|
|
477
483
|
}
|
|
478
484
|
|
|
479
|
-
/* A pane's lone unclosable chip is a label, not a choice: no
|
|
485
|
+
/* A pane's lone unclosable chip is a label, not a choice: no tab, no
|
|
480
486
|
hover fill — the primary ink on the pane's own ground. Its title never
|
|
481
487
|
yields to a close control, so it keeps the clipped fade alone. */
|
|
482
488
|
.tab.tabQuiet,
|
|
@@ -514,7 +520,7 @@
|
|
|
514
520
|
line-height: 1;
|
|
515
521
|
background: transparent;
|
|
516
522
|
border: none;
|
|
517
|
-
border-radius:
|
|
523
|
+
border-radius: var(--dsw-radius-sm);
|
|
518
524
|
cursor: pointer;
|
|
519
525
|
}
|
|
520
526
|
|
|
@@ -544,11 +550,8 @@
|
|
|
544
550
|
display: flex;
|
|
545
551
|
flex-direction: column;
|
|
546
552
|
min-width: 96px;
|
|
547
|
-
padding:
|
|
548
|
-
background: var(--dsw-specific-menu);
|
|
549
|
-
backdrop-filter: var(--dsw-menu-backdrop-filter);
|
|
553
|
+
padding: 4px;
|
|
550
554
|
border: 0.5px solid var(--dsw-alias-border-l2);
|
|
551
|
-
border-radius: 8px;
|
|
552
555
|
}
|
|
553
556
|
|
|
554
557
|
.menu:not(:has([role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio'])) {
|
|
@@ -562,7 +565,7 @@
|
|
|
562
565
|
text-align: left;
|
|
563
566
|
background: transparent;
|
|
564
567
|
border: none;
|
|
565
|
-
border-radius:
|
|
568
|
+
border-radius: var(--dsw-radius-md);
|
|
566
569
|
cursor: pointer;
|
|
567
570
|
}
|
|
568
571
|
|
|
@@ -667,7 +670,7 @@
|
|
|
667
670
|
overflow: hidden;
|
|
668
671
|
color: var(--dsw-alias-label-tertiary);
|
|
669
672
|
border: 1.5px dashed var(--dsw-alias-border-l2);
|
|
670
|
-
border-radius:
|
|
673
|
+
border-radius: var(--dsw-radius-lg);
|
|
671
674
|
transition: color 120ms ease-out, background-color 120ms ease-out, border-color 120ms ease-out;
|
|
672
675
|
animation: dockHintIn 140ms ease-out;
|
|
673
676
|
}
|
|
@@ -736,6 +739,14 @@
|
|
|
736
739
|
height: 40%;
|
|
737
740
|
}
|
|
738
741
|
|
|
742
|
+
:global(html[data-windows-titlebar]) .float {
|
|
743
|
+
--dsh-dockkit-float-top: calc(var(--dsh-windows-titlebar-height) + 20px);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
:global(html[data-windows-titlebar][data-fullscreen]) .float {
|
|
747
|
+
--dsh-dockkit-float-top: 20px;
|
|
748
|
+
}
|
|
749
|
+
|
|
739
750
|
.float {
|
|
740
751
|
position: fixed;
|
|
741
752
|
display: flex;
|
|
@@ -744,7 +755,7 @@
|
|
|
744
755
|
to its own corners. */
|
|
745
756
|
overflow: hidden;
|
|
746
757
|
background: var(--dsw-alias-bg-layer-2);
|
|
747
|
-
border-radius:
|
|
758
|
+
border-radius: var(--dsw-radius-panel);
|
|
748
759
|
/* The menu's shadow with the stroke rebound one step lighter than the
|
|
749
760
|
default (l2): the panel reads as the same kind of raised surface, and the
|
|
750
761
|
shadow's hairline outlines it, so the frame draws no border of its own. */
|
|
@@ -801,20 +812,18 @@
|
|
|
801
812
|
touch-action: none;
|
|
802
813
|
}
|
|
803
814
|
|
|
804
|
-
/* The grip
|
|
805
|
-
|
|
806
|
-
while the pointer is over the panel — an idle panel keeps a clean corner —
|
|
807
|
-
and `:active` keeps it lit while a drag holds the pointer capture. */
|
|
815
|
+
/* The grip follows the floating panel's corner with a 4px inset. Pointer
|
|
816
|
+
hover and an active drag reveal it. */
|
|
808
817
|
.floatResize::after {
|
|
809
818
|
content: '';
|
|
810
819
|
position: absolute;
|
|
811
820
|
right: 4px;
|
|
812
821
|
bottom: 4px;
|
|
813
|
-
width:
|
|
814
|
-
height:
|
|
822
|
+
width: calc(var(--dsw-radius-panel) - 4px);
|
|
823
|
+
height: calc(var(--dsw-radius-panel) - 4px);
|
|
815
824
|
border-right: 1.5px solid var(--dsw-alias-label-caption);
|
|
816
825
|
border-bottom: 1.5px solid var(--dsw-alias-label-caption);
|
|
817
|
-
border-bottom-right-radius:
|
|
826
|
+
border-bottom-right-radius: calc(var(--dsw-radius-panel) - 4px);
|
|
818
827
|
opacity: 0;
|
|
819
828
|
transition: opacity 120ms ease-out;
|
|
820
829
|
}
|
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Children, Fragment as Fragment$1, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
3
3
|
import clsx from "clsx";
|
|
4
|
-
import { IconCloseFillRegular, IconCloseOutlineRegular, IconPanelLeftOutlineRegular, IconPlusOutlineRegular, Tooltip } from "@deepseek-ai/dsh-client-ui-primitives";
|
|
4
|
+
import { IconCloseFillRegular, IconCloseOutlineRegular, IconPanelLeftOutlineRegular, IconPlusOutlineRegular, MenuSurface, Tooltip, focusWithoutRing, modalSelector, observeComposition } from "@deepseek-ai/dsh-client-ui-primitives";
|
|
5
5
|
import { createPortal } from "react-dom";
|
|
6
6
|
import css from "./components/dockkit.module.css";
|
|
7
7
|
//#region lib/types/engine/tree.js
|
|
@@ -2230,7 +2230,7 @@ function useGesture(reset) {
|
|
|
2230
2230
|
* released clear of the surface. A menu that would hold no item at all renders
|
|
2231
2231
|
* no popup, so a secondary press on a chip with nothing to offer shows nothing.
|
|
2232
2232
|
* Presentational — it renders what its props supply and dismisses itself on
|
|
2233
|
-
* outside presses.
|
|
2233
|
+
* outside presses or an unmodified Escape, returning menu focus to its tab.
|
|
2234
2234
|
*
|
|
2235
2235
|
* It renders in a portal, positioned against the control that opened it. The tab
|
|
2236
2236
|
* strip clips its overflow on purpose (so it never becomes a scroll container
|
|
@@ -2264,17 +2264,35 @@ function TabMenu({ labels, anchor, onClose, onDismiss, extras }) {
|
|
|
2264
2264
|
const menu = self.current;
|
|
2265
2265
|
/* v8 ignore next -- the ref is attached by effect time: the menu renders unconditionally. */
|
|
2266
2266
|
if (menu === null) return void 0;
|
|
2267
|
+
const document = menu.ownerDocument;
|
|
2268
|
+
const composition = observeComposition(document);
|
|
2269
|
+
const onKeyDown = (event) => {
|
|
2270
|
+
if (composition.guards(event) || event.defaultPrevented || event.key !== "Escape" || event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) return;
|
|
2271
|
+
if ([...document.querySelectorAll(modalSelector)].at(-1) !== menu) return;
|
|
2272
|
+
event.preventDefault();
|
|
2273
|
+
if (event.repeat) return;
|
|
2274
|
+
const restoreFocus = menu.contains(document.activeElement);
|
|
2275
|
+
onDismiss();
|
|
2276
|
+
if (restoreFocus) focusWithoutRing(anchor);
|
|
2277
|
+
};
|
|
2278
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
2267
2279
|
const onPointerDown = (event) => {
|
|
2268
2280
|
if (event.target instanceof Node && menu.contains(event.target)) return;
|
|
2269
2281
|
onDismiss();
|
|
2270
2282
|
};
|
|
2271
2283
|
window.addEventListener("pointerdown", onPointerDown, true);
|
|
2272
2284
|
return () => {
|
|
2285
|
+
composition.dispose();
|
|
2286
|
+
document.removeEventListener("keydown", onKeyDown, true);
|
|
2273
2287
|
window.removeEventListener("pointerdown", onPointerDown, true);
|
|
2274
2288
|
};
|
|
2275
|
-
}, [
|
|
2289
|
+
}, [
|
|
2290
|
+
anchor,
|
|
2291
|
+
onDismiss,
|
|
2292
|
+
hasItems
|
|
2293
|
+
]);
|
|
2276
2294
|
if (!hasItems) return null;
|
|
2277
|
-
return createPortal(jsxs(
|
|
2295
|
+
return createPortal(jsxs(MenuSurface, {
|
|
2278
2296
|
className: css.menu,
|
|
2279
2297
|
ref: self,
|
|
2280
2298
|
role: "menu",
|
|
@@ -2565,6 +2583,7 @@ function TabStrip({ state, pane, callbacks }) {
|
|
|
2565
2583
|
className: css.tabStrip,
|
|
2566
2584
|
role: "tablist",
|
|
2567
2585
|
"data-dockkit-strip": pane.id,
|
|
2586
|
+
"data-window-drag": state.floats.length === 0 ? true : void 0,
|
|
2568
2587
|
children: [
|
|
2569
2588
|
jsxs("div", {
|
|
2570
2589
|
ref: stripTabs,
|
|
@@ -2600,6 +2619,7 @@ function TabStrip({ state, pane, callbacks }) {
|
|
|
2600
2619
|
},
|
|
2601
2620
|
onKeyDown: (event) => {
|
|
2602
2621
|
if (event.target !== event.currentTarget) return;
|
|
2622
|
+
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey || event.nativeEvent.isComposing) return;
|
|
2603
2623
|
const next = chipToFocus(event.key, pane.tabs, tabId);
|
|
2604
2624
|
if (next !== void 0) {
|
|
2605
2625
|
event.preventDefault();
|
|
@@ -2621,19 +2641,27 @@ function TabStrip({ state, pane, callbacks }) {
|
|
|
2621
2641
|
},
|
|
2622
2642
|
children: [
|
|
2623
2643
|
jsx(TabTitle, { children: callbacks.renderTabTitle?.(tab) ?? tab.title }),
|
|
2624
|
-
closable && jsx(
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
"
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
callbacks.
|
|
2635
|
-
|
|
2636
|
-
|
|
2644
|
+
closable && jsx(Tooltip, {
|
|
2645
|
+
disabled: menu !== void 0,
|
|
2646
|
+
label: callbacks.labels.closeTab,
|
|
2647
|
+
shortcutKeys: callbacks.labels.closeTabKeys,
|
|
2648
|
+
side: "bottom",
|
|
2649
|
+
delayMs: 500,
|
|
2650
|
+
children: jsx("button", {
|
|
2651
|
+
type: "button",
|
|
2652
|
+
className: css.tabClose,
|
|
2653
|
+
"aria-label": callbacks.labels.closeTab,
|
|
2654
|
+
"aria-keyshortcuts": callbacks.labels.closeTabShortcut,
|
|
2655
|
+
"data-dockkit-tab-close": tabId,
|
|
2656
|
+
onPointerDown: (event) => {
|
|
2657
|
+
event.stopPropagation();
|
|
2658
|
+
},
|
|
2659
|
+
onClick: (event) => {
|
|
2660
|
+
event.stopPropagation();
|
|
2661
|
+
callbacks.onCloseTab(tabId);
|
|
2662
|
+
},
|
|
2663
|
+
children: jsx(IconCloseFillRegular, { size: 14 })
|
|
2664
|
+
})
|
|
2637
2665
|
}),
|
|
2638
2666
|
menu?.tabId === tabId && jsx(TabMenu, {
|
|
2639
2667
|
labels: callbacks.labels,
|
|
@@ -2677,23 +2705,27 @@ function TabStrip({ state, pane, callbacks }) {
|
|
|
2677
2705
|
"data-dockkit-strip-fill": true
|
|
2678
2706
|
}),
|
|
2679
2707
|
!(callbacks.hideSplitWhenBlocked && block !== void 0) && jsx(Tooltip, {
|
|
2680
|
-
label: callbacks.labels.splitPane,
|
|
2708
|
+
label: block === void 0 ? callbacks.labels.splitPane : splitBlockedTitle(callbacks.labels, block),
|
|
2709
|
+
shortcutKeys: callbacks.labels.splitPaneKeys,
|
|
2681
2710
|
side: "bottom",
|
|
2682
2711
|
delayMs: 500,
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2712
|
+
children: jsx("span", {
|
|
2713
|
+
tabIndex: block === void 0 ? void 0 : 0,
|
|
2714
|
+
"aria-label": block === void 0 ? void 0 : splitBlockedTitle(callbacks.labels, block),
|
|
2715
|
+
children: jsx("button", {
|
|
2716
|
+
type: "button",
|
|
2717
|
+
className: css.iconButton,
|
|
2718
|
+
"aria-label": callbacks.labels.splitPane,
|
|
2719
|
+
"aria-keyshortcuts": callbacks.labels.splitPaneShortcut,
|
|
2720
|
+
disabled: block !== void 0,
|
|
2721
|
+
"data-dockkit-split-button": pane.id,
|
|
2722
|
+
"data-dockkit-split-blocked": block,
|
|
2723
|
+
onClick: (event) => {
|
|
2724
|
+
event.stopPropagation();
|
|
2725
|
+
callbacks.onSplitPane(pane.id);
|
|
2726
|
+
},
|
|
2727
|
+
children: jsx(SplitGlyph, {})
|
|
2728
|
+
})
|
|
2697
2729
|
})
|
|
2698
2730
|
}),
|
|
2699
2731
|
pane.id === callbacks.chromePaneId && callbacks.chrome !== void 0 && jsx("div", {
|
|
@@ -2735,6 +2767,7 @@ function TabPanel({ state, pane, callbacks }) {
|
|
|
2735
2767
|
return jsxs("section", {
|
|
2736
2768
|
className: css.pane,
|
|
2737
2769
|
"data-dockkit-pane": pane.id,
|
|
2770
|
+
tabIndex: -1,
|
|
2738
2771
|
"data-dockkit-pane-active": state.activePaneId === pane.id || void 0,
|
|
2739
2772
|
onClick: () => {
|
|
2740
2773
|
if (state.activePaneId !== pane.id) callbacks.onFocusPane(pane.id);
|
|
@@ -2844,11 +2877,19 @@ function useFloatGestures(state, intents) {
|
|
|
2844
2877
|
/** Start a move or resize from a press on the panel's grip or corner; a release that moved nothing is a click. */
|
|
2845
2878
|
const drag = (mode, paneId, event) => {
|
|
2846
2879
|
event.stopPropagation();
|
|
2880
|
+
const root = document.documentElement;
|
|
2881
|
+
const windows = root.hasAttribute("data-windows-titlebar");
|
|
2882
|
+
const caption = windows && !root.hasAttribute("data-fullscreen") ? Number.parseFloat(root.style.getPropertyValue("--dsh-windows-titlebar-height")) : 0;
|
|
2883
|
+
const saved = floatRect(getPane(state, paneId));
|
|
2884
|
+
const rect = {
|
|
2885
|
+
...saved,
|
|
2886
|
+
y: Math.max(saved.y, windows ? caption + 20 : 0)
|
|
2887
|
+
};
|
|
2847
2888
|
const start = {
|
|
2848
2889
|
mode,
|
|
2849
2890
|
originX: event.clientX,
|
|
2850
2891
|
originY: event.clientY,
|
|
2851
|
-
rect
|
|
2892
|
+
rect
|
|
2852
2893
|
};
|
|
2853
2894
|
begin(event.currentTarget, event.pointerId, {
|
|
2854
2895
|
move: (moved) => {
|
|
@@ -2909,13 +2950,14 @@ function FloatHeader({ paneId, tab, labels, intents, renderTabTitle, canCloseTab
|
|
|
2909
2950
|
})
|
|
2910
2951
|
}),
|
|
2911
2952
|
(canCloseTab?.(tab.id) ?? true) && jsx(Tooltip, {
|
|
2912
|
-
label: labels.
|
|
2953
|
+
label: labels.closeTab,
|
|
2954
|
+
shortcutKeys: labels.closeTabKeys,
|
|
2913
2955
|
side: "bottom",
|
|
2914
2956
|
delayMs: 500,
|
|
2915
2957
|
children: jsx("button", {
|
|
2916
2958
|
type: "button",
|
|
2917
2959
|
className: css.iconButton,
|
|
2918
|
-
"aria-label": labels.
|
|
2960
|
+
"aria-label": labels.closeTab,
|
|
2919
2961
|
"data-dockkit-float-close": paneId,
|
|
2920
2962
|
onPointerDown: (event) => {
|
|
2921
2963
|
event.stopPropagation();
|
|
@@ -2940,10 +2982,11 @@ function FloatLayer({ state, intents, labels, renderTab, renderTabTitle, canClos
|
|
|
2940
2982
|
return jsxs("div", {
|
|
2941
2983
|
className: css.float,
|
|
2942
2984
|
"data-dockkit-float": paneId,
|
|
2985
|
+
tabIndex: -1,
|
|
2943
2986
|
"data-dockkit-float-active": state.activePaneId === paneId || void 0,
|
|
2944
2987
|
style: {
|
|
2945
2988
|
left: live.x,
|
|
2946
|
-
top: live.y
|
|
2989
|
+
top: `max(var(--dsh-dockkit-float-top, 0px), ${live.y}px)`,
|
|
2947
2990
|
width: live.width,
|
|
2948
2991
|
height: live.height,
|
|
2949
2992
|
zIndex: lifted === void 0 ? depth + 1 : state.floats.length + 1
|
|
@@ -3038,6 +3081,7 @@ function TabHost({ state, callbacks, intents, tab, pane, column, floats, focusRe
|
|
|
3038
3081
|
},
|
|
3039
3082
|
children: jsxs("section", {
|
|
3040
3083
|
ref: host,
|
|
3084
|
+
tabIndex: -1,
|
|
3041
3085
|
className: clsx(css.tabHost, floating ? css.float : css.pane),
|
|
3042
3086
|
"aria-hidden": !visible || void 0,
|
|
3043
3087
|
"data-dockkit-content": tab.id,
|
|
@@ -3048,7 +3092,7 @@ function TabHost({ state, callbacks, intents, tab, pane, column, floats, focusRe
|
|
|
3048
3092
|
"data-dockkit-column": !floating ? column : void 0,
|
|
3049
3093
|
style: rect === void 0 ? void 0 : {
|
|
3050
3094
|
left: rect.x,
|
|
3051
|
-
top: rect.y
|
|
3095
|
+
top: `max(var(--dsh-dockkit-float-top, 0px), ${rect.y}px)`,
|
|
3052
3096
|
width: rect.width,
|
|
3053
3097
|
height: rect.height
|
|
3054
3098
|
},
|
|
@@ -18,12 +18,20 @@ export interface DockLabels {
|
|
|
18
18
|
readonly emptyPane: string;
|
|
19
19
|
/** The split control, while splitting is allowed. */
|
|
20
20
|
readonly splitPane: string;
|
|
21
|
+
/** ARIA combination from the effective binding, when bound. */
|
|
22
|
+
readonly splitPaneShortcut?: string | undefined;
|
|
23
|
+
/** Effective split keys shown as separate tooltip keycaps. */
|
|
24
|
+
readonly splitPaneKeys?: readonly string[] | undefined;
|
|
21
25
|
/** The split control, once the pane budget is spent. */
|
|
22
26
|
readonly splitPaneDisabled: string;
|
|
23
27
|
/** The split control, while the pane is too narrow for two working halves. */
|
|
24
28
|
readonly splitPaneNarrow: string;
|
|
25
29
|
/** Destroy a tab: the chip's close control and the menu's close item. */
|
|
26
30
|
readonly closeTab: string;
|
|
31
|
+
/** Effective close combination, when bound. */
|
|
32
|
+
readonly closeTabShortcut?: string | undefined;
|
|
33
|
+
/** Effective close keys shown as separate tooltip keycaps. */
|
|
34
|
+
readonly closeTabKeys?: readonly string[] | undefined;
|
|
27
35
|
/** The strip's add control, which seats the embedder's seeded tab. */
|
|
28
36
|
readonly addTab: string;
|
|
29
37
|
/** Send a floating panel back into the docked tree. */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-client-ui-dockkit",
|
|
3
3
|
"description": "Docking layout kit: split-tree engine with invertible operations, and the React components that render and drive it (zero cordis)",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0-rc.1",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
"react-dom": "^18.2.0",
|
|
30
30
|
"react": "^18.2.0",
|
|
31
31
|
"clsx": "^2.0.0",
|
|
32
|
-
"@deepseek-ai/
|
|
33
|
-
"@deepseek-ai/dsh-
|
|
34
|
-
"@deepseek-ai/
|
|
32
|
+
"@deepseek-ai/dsh-brand": "0.2.0-rc.1",
|
|
33
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.1",
|
|
34
|
+
"@deepseek-ai/cordis": "~4.0.4"
|
|
35
35
|
},
|
|
36
36
|
"files": [
|
|
37
37
|
"lib/index.js",
|