dsh-better-workspace 0.34.0 → 0.34.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/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-better-workspace",
3
- "version": "0.34.0",
3
+ "version": "0.34.1",
4
4
  "main": "./src/index.js",
5
5
  "description": "侧边栏工作区树:按官方磁盘目录嵌套(官方 workspace-tree 语义),可选按名称中的 / 分组(默认关闭,设置里开启);新会话页的工作区选择器同树。Workspace tree for the DSH sidebar workspace list, nested by official disk folders with optional \"/\"-name grouping; the New Session workspace picker shows the same tree.",
6
6
  "engines": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-better-workspace",
3
- "version": "0.34.0",
3
+ "version": "0.34.1",
4
4
  "description": "侧边栏工作区树:按官方磁盘目录嵌套(官方 workspace-tree 语义),可选按名称中的 / 分组(默认关闭,设置里开启);新会话页的工作区选择器同树(同一份视图选项即时同步);添加工作区走官方目录流。Workspace tree for the DSH sidebar: disk-path nesting (official workspace-tree semantics), with optional \"/\"-name grouping (off by default), and the same tree in the New Session workspace picker; add-workspace rides the official directory flow.",
5
5
  "type": "module",
6
6
  "repository": {
package/src/client.js CHANGED
@@ -3905,6 +3905,148 @@ window.__ModuleLoader__.load({
3905
3905
  return String(r.n) + (FALLBACK_UNITS[r.unit] || '')
3906
3906
  }
3907
3907
 
3908
+ // Port of ui-workspace rows/AnimatedRows.tsx — the package entry exports
3909
+ // only apply/inject, so the component cannot be required (same discipline
3910
+ // as the row-style port). FLIP glide + entry/exit fades for keyed rows:
3911
+ // armed by the first pointer/key input, skipped when membership, order,
3912
+ // readiness or the reset key did not change. Deltas from upstream: the
3913
+ // plugin tree rolls its own indent var (--bw-indent, not
3914
+ // --dsh-workspace-indent), the exit overlay is viewport-FIXED because no
3915
+ // positioned ancestor is guaranteed here (clone math subtracts the
3916
+ // overlay rect either way), and the list div takes an attrs passthrough
3917
+ // (data-bw-density / data-bw-count) plus innerRef (autoStyle samples it).
3918
+ const ROW_FADE_MS = 100
3919
+ const ROW_GLIDE_MS = 200
3920
+ const sameRowKeys = (previous, next) => previous.length === next.length
3921
+ && previous.every((key, index) => key === next[index])
3922
+ const intersectsViewport = (rect, viewport) => rect.bottom > viewport.top && rect.top < viewport.bottom
3923
+ && rect.right > viewport.left && rect.left < viewport.right
3924
+ class AnimatedRows extends React.Component {
3925
+ constructor(props) {
3926
+ super(props)
3927
+ this.armed = false
3928
+ this.list = { current: null }
3929
+ this.overlay = React.createRef()
3930
+ this.movements = new Map()
3931
+ this.exits = new Map()
3932
+ }
3933
+ getSnapshotBeforeUpdate(previous) {
3934
+ const list = this.list.current
3935
+ if (!this.armed || sameRowKeys(previous.rowKeys, this.props.rowKeys) || previous.resetKey !== this.props.resetKey
3936
+ || !previous.ready || !this.props.ready || list === null
3937
+ || typeof list.animate !== 'function'
3938
+ || window.matchMedia('(prefers-reduced-motion: reduce)').matches) return null
3939
+ const viewport = list.getBoundingClientRect()
3940
+ const positions = this.readPositions()
3941
+ const nextKeys = new Set(this.props.rowKeys)
3942
+ const removed = new Map()
3943
+ for (const [key, row] of positions) {
3944
+ if (nextKeys.has(key) || !intersectsViewport(row.rect, viewport)) continue
3945
+ const clone = row.element.cloneNode(true)
3946
+ clone.removeAttribute('data-row-key')
3947
+ clone.inert = true
3948
+ clone.style.setProperty('--bw-indent', getComputedStyle(row.element).getPropertyValue('--bw-indent'))
3949
+ removed.set(key, Object.assign({}, row, { element: clone }))
3950
+ }
3951
+ return { positions, removed }
3952
+ }
3953
+ componentDidUpdate(previous, _state, snapshot) {
3954
+ if (snapshot === null) {
3955
+ if (!sameRowKeys(previous.rowKeys, this.props.rowKeys) || previous.resetKey !== this.props.resetKey
3956
+ || previous.ready !== this.props.ready) this.clear()
3957
+ return
3958
+ }
3959
+ this.cancelMovements()
3960
+ const list = this.list.current
3961
+ const overlay = this.overlay.current
3962
+ const viewport = list.getBoundingClientRect()
3963
+ const origin = overlay.getBoundingClientRect()
3964
+ const positions = this.readPositions()
3965
+ for (const [key, row] of positions) {
3966
+ this.removeExit(key)
3967
+ const previousRow = snapshot.positions.get(key)
3968
+ if (!intersectsViewport(row.rect, viewport)
3969
+ && (previousRow === undefined || !intersectsViewport(previousRow.rect, viewport))) continue
3970
+ if (previousRow === undefined) {
3971
+ this.move(row.element, [{ opacity: 0 }, { opacity: 1 }], ROW_FADE_MS)
3972
+ continue
3973
+ }
3974
+ const dx = previousRow.rect.left - row.rect.left
3975
+ const dy = previousRow.rect.top - row.rect.top
3976
+ if (dx === 0 && dy === 0 && previousRow.opacity === 1) continue
3977
+ this.move(row.element, [
3978
+ { transform: 'translate(' + String(dx) + 'px, ' + String(dy) + 'px)', opacity: previousRow.opacity },
3979
+ { transform: 'translate(0, 0)', opacity: 1 },
3980
+ ], ROW_GLIDE_MS)
3981
+ }
3982
+ for (const [key, row] of snapshot.removed) {
3983
+ const { element } = row
3984
+ this.removeExit(key)
3985
+ Object.assign(element.style, {
3986
+ position: 'absolute', margin: '0', transform: 'none', boxSizing: 'border-box',
3987
+ left: String(row.rect.left - origin.left) + 'px',
3988
+ top: String(row.rect.top - origin.top) + 'px',
3989
+ width: String(row.rect.width) + 'px', height: String(row.rect.height) + 'px',
3990
+ })
3991
+ overlay.append(element)
3992
+ const animation = element.animate([{ opacity: row.opacity }, { opacity: 0 }], {
3993
+ duration: ROW_FADE_MS, easing: 'ease-out', fill: 'forwards',
3994
+ })
3995
+ this.exits.set(key, { element, animation })
3996
+ animation.onfinish = () => { this.removeExit(key) }
3997
+ }
3998
+ }
3999
+ componentWillUnmount() { this.clear() }
4000
+ readPositions() {
4001
+ const list = this.list.current
4002
+ const rows = list.querySelectorAll('[data-row-key]')
4003
+ return new Map(Array.from(rows, (element) => [element.dataset.rowKey, {
4004
+ element,
4005
+ rect: element.getBoundingClientRect(),
4006
+ opacity: this.movements.has(element) ? Number(getComputedStyle(element).opacity) : 1,
4007
+ }]))
4008
+ }
4009
+ move(element, keyframes, duration) {
4010
+ const animation = element.animate(keyframes, { duration, easing: 'ease-out' })
4011
+ this.movements.set(element, animation)
4012
+ animation.onfinish = () => { this.movements.delete(element); animation.cancel() }
4013
+ }
4014
+ cancelMovements() {
4015
+ for (const animation of this.movements.values()) {
4016
+ animation.onfinish = null
4017
+ animation.cancel()
4018
+ }
4019
+ this.movements.clear()
4020
+ }
4021
+ removeExit(key) {
4022
+ const exit = this.exits.get(key)
4023
+ if (exit === undefined) return
4024
+ exit.animation.onfinish = null
4025
+ exit.animation.cancel()
4026
+ exit.element.remove()
4027
+ this.exits.delete(key)
4028
+ }
4029
+ clear() {
4030
+ this.cancelMovements()
4031
+ for (const key of Array.from(this.exits.keys())) this.removeExit(key)
4032
+ }
4033
+ render() {
4034
+ const attrs = this.props.attrs || {}
4035
+ return [
4036
+ E('div', Object.assign({
4037
+ key: 'list',
4038
+ ref: (el) => { this.list.current = el; if (this.props.innerRef) this.props.innerRef.current = el },
4039
+ className: this.props.className,
4040
+ role: 'tree',
4041
+ 'aria-label': this.props.label,
4042
+ onPointerDownCapture: () => { this.armed = true },
4043
+ onKeyDownCapture: () => { this.armed = true },
4044
+ }, attrs), this.props.children),
4045
+ E('div', { key: 'exits', ref: this.overlay, className: 'bw-rows-exits', 'aria-hidden': 'true' }),
4046
+ ]
4047
+ }
4048
+ }
4049
+
3908
4050
  const pendingKindOf = (pending, id) => {
3909
4051
  if (!pending) return undefined
3910
4052
  const p = typeof pending.get === 'function' ? pending.get(id) : pending[id]
@@ -4570,6 +4712,7 @@ window.__ModuleLoader__.load({
4570
4712
  '.bw-menu-block{display:block;width:100%}',
4571
4713
  '.bw-menu-inline{display:inline-block;width:auto;flex:none}',
4572
4714
  '.bw-tree{--bw-row-h:34px;--bw-session-h:32px;flex:1;overflow-y:auto;overflow-x:hidden;padding:2px 6px 12px;min-height:0}',
4715
+ '.bw-rows-exits{position:fixed;inset:0;contain:strict;overflow:clip;pointer-events:none}',
4573
4716
  '.bw-tree[data-bw-density="compact"]{--bw-row-h:28px;--bw-session-h:26px}',
4574
4717
  '.bw-tree[data-bw-density="roomy"]{--bw-row-h:40px;--bw-session-h:38px}',
4575
4718
  '.bw-row{display:flex;align-items:center;gap:6px;height:var(--bw-row-h,34px);padding:0 8px;padding-inline-start:calc(8px + var(--bw-indent,0px));border-radius:var(--dsw-radius-md,8px);cursor:pointer;user-select:none;font-size:14px;line-height:20px;color:var(--dsw-alias-label-primary,#e6e6e6);position:relative}',
@@ -5872,7 +6015,7 @@ window.__ModuleLoader__.load({
5872
6015
  }
5873
6016
  }
5874
6017
 
5875
- function FolderRow({ node, depth, expanded, onToggle, onContextMenu, dropInto, dragEvents, custStyle, iconMode, pulse, active, t }) {
6018
+ function FolderRow({ node, depth, expanded, onToggle, onContextMenu, dropInto, dragEvents, custStyle, iconMode, pulse, active, dataRowKey, t }) {
5876
6019
  const total = countWorkspaces(node)
5877
6020
  const iconEl = iconOf(iconMode, expanded)
5878
6021
  // Hidden status dots breathe on the icon; with the icon hidden (mode
@@ -5882,6 +6025,7 @@ window.__ModuleLoader__.load({
5882
6025
  : iconEl
5883
6026
  return E('div', {
5884
6027
  className: cls('bw-row', 'bw-container-row', dropInto && 'bw-drop-into', dropInto && 'bw-drop-into-strong'),
6028
+ 'data-row-key': dataRowKey,
5885
6029
  style: { '--bw-indent': (depth * 12) + 'px', ...(custStyle || {}), ...(pulse ? { '--bw-pulse-color': PULSE_COLORS[pulse] || PULSE_COLORS.ongoing } : null) },
5886
6030
  onClick: onToggle,
5887
6031
  onContextMenu: onContextMenu,
@@ -5900,13 +6044,14 @@ window.__ModuleLoader__.load({
5900
6044
  )
5901
6045
  }
5902
6046
 
5903
- function WorkspaceRow({ workspace, depth, count, sessionsOpen, onToggle, onStart, onMenu, onContextMenu, currentInside, menuOpen, dropHalf, dropInto, dragEvents, custStyle, iconMode, pulse, newSessionLabel, newSessionAria, menuAria, hoverBody, t }) {
6047
+ function WorkspaceRow({ workspace, depth, count, sessionsOpen, onToggle, onStart, onMenu, onContextMenu, currentInside, menuOpen, dropHalf, dropInto, dragEvents, custStyle, iconMode, pulse, newSessionLabel, newSessionAria, menuAria, hoverBody, dataRowKey, t }) {
5904
6048
  const iconEl = iconOf(iconMode, sessionsOpen)
5905
6049
  const iconChild = pulse
5906
6050
  ? E(PulseGlow, { state: pulse }, iconEl || (typeof ui.StateDot === 'function' ? E(ui.StateDot, { state: pulse, size: 10 }) : null))
5907
6051
  : iconEl
5908
6052
  let row = E('div', {
5909
6053
  className: cls('bw-row', 'bw-container-row', currentInside && 'bw-row-current', menuOpen && 'menu-open', dropHalf === 'before' && 'bw-drop-before', dropHalf === 'after' && 'bw-drop-after', dropInto && 'bw-drop-into', dropInto && 'bw-drop-into-strong'),
6054
+ 'data-row-key': dataRowKey,
5910
6055
  style: { '--bw-indent': (depth * 12) + 'px', ...(custStyle || {}), ...(pulse ? { '--bw-pulse-color': PULSE_COLORS[pulse] || PULSE_COLORS.ongoing } : null) },
5911
6056
  onClick: onToggle,
5912
6057
  onContextMenu: onContextMenu,
@@ -5937,7 +6082,7 @@ window.__ModuleLoader__.load({
5937
6082
  'aria-keyshortcuts': newSessionAria,
5938
6083
  title: newSessionLabel || t('session.new'),
5939
6084
  onClick: (e) => { e.stopPropagation(); onStart() },
5940
- }, icon('IconPlusOutline16')),
6085
+ }, icon('IconNewChatOutline16')),
5941
6086
  ),
5942
6087
  )
5943
6088
  // Official workspace hover card (Rows.tsx WorkspaceHoverContent): the
@@ -6041,7 +6186,7 @@ window.__ModuleLoader__.load({
6041
6186
  return { titleRef, onPointerEnter, onPointerLeave }
6042
6187
  }
6043
6188
 
6044
- function SessionRow({ node, depth, current, onOpen, onContextMenu, now, dropHalf, dragEvents, custStyle, breathing, sessionActions, scheduleSeat, t }) {
6189
+ function SessionRow({ node, depth, current, onOpen, onContextMenu, now, dropHalf, dragEvents, custStyle, breathing, sessionActions, scheduleSeat, dataRowKey, t }) {
6045
6190
  const state = sessionStateOf(node)
6046
6191
  const status = state === null ? null : {
6047
6192
  state,
@@ -6159,6 +6304,7 @@ window.__ModuleLoader__.load({
6159
6304
  node.archived && 'bw-row-archived',
6160
6305
  menuOpen && 'menu-open',
6161
6306
  dropHalf === 'before' && 'bw-drop-before', dropHalf === 'after' && 'bw-drop-after'),
6307
+ 'data-row-key': dataRowKey,
6162
6308
  style: { '--bw-indent': (depth * 12) + 'px', ...(custStyle || {}) },
6163
6309
  onClick: () => {
6164
6310
  if (node.archived) {
@@ -6228,9 +6374,10 @@ window.__ModuleLoader__.load({
6228
6374
  * titles). Deliberately NOT styled like a workspace folder — no folder
6229
6375
  * icon, tertiary color — so a session level never reads as a workspace.
6230
6376
  */
6231
- function SessionGroupRow({ name, depth, expanded, count, onToggle, onContextMenu, dropInto, dragEvents, custStyle, pulse, t }) {
6377
+ function SessionGroupRow({ name, depth, expanded, count, onToggle, onContextMenu, dropInto, dragEvents, custStyle, pulse, dataRowKey, t }) {
6232
6378
  return E('div', {
6233
6379
  className: cls('bw-row', 'bw-sgroup-row', dropInto && 'bw-drop-into', dropInto && 'bw-drop-into-strong'),
6380
+ 'data-row-key': dataRowKey,
6234
6381
  style: { '--bw-indent': (depth * 12) + 'px', ...(custStyle || {}) },
6235
6382
  onClick: onToggle,
6236
6383
  onContextMenu: onContextMenu,
@@ -9274,6 +9421,17 @@ window.__ModuleLoader__.load({
9274
9421
  )]
9275
9422
  : [E('div', { key: 'empty', className: 'bw-empty' }, searching ? t('empty.search') : (phase === 'pending' ? '…' : t('empty')))]
9276
9423
  }
9424
+ // Keyed-row handoff for AnimatedRows: every body row carries a stable
9425
+ // data-row-key (the element's React key when present, array index
9426
+ // otherwise) and the key list feeds the component's sameRows guard.
9427
+ bodyRows = bodyRows.filter((el) => el !== null && el !== false && el !== undefined)
9428
+ const bodyRowKeys = []
9429
+ bodyRows = bodyRows.map((el, index) => {
9430
+ if (!el || typeof el !== 'object') return el
9431
+ const rowKey = el.key != null ? String(el.key) : 'row-' + index
9432
+ bodyRowKeys.push(rowKey)
9433
+ return React.cloneElement(el, { 'data-row-key': rowKey, dataRowKey: rowKey })
9434
+ })
9277
9435
 
9278
9436
  /* ------------------------- context menu ------------------------ */
9279
9437
 
@@ -9886,13 +10044,17 @@ window.__ModuleLoader__.load({
9886
10044
  disabled: flowBusy,
9887
10045
  }, icon('IconProjectAddOutline16')) : null,
9888
10046
  ),
9889
- E('div', {
9890
- ref: treeRef,
10047
+ E(AnimatedRows, {
9891
10048
  className: 'bw-tree',
9892
- role: 'tree',
9893
- 'aria-label': t('title'),
9894
- 'data-bw-density': density,
9895
- ...(showCount ? { 'data-bw-count': '' } : {}),
10049
+ label: t('title'),
10050
+ rowKeys: bodyRowKeys,
10051
+ // Drag reorders rows itself; animating under it fights the gesture.
10052
+ ready: drag === null,
10053
+ // View-replacing switches (search, filter, density, count) settle
10054
+ // instead of gliding — official resetKey semantics.
10055
+ resetKey: JSON.stringify([density, showCount, searching, archivedOnly, normalizedQuery]),
10056
+ innerRef: treeRef,
10057
+ attrs: { 'data-bw-density': density, ...(showCount ? { 'data-bw-count': '' } : {}) },
9896
10058
  }, bodyRows),
9897
10059
  dialogElement,
9898
10060
  (ctx !== null && typeof ui.Menu === 'function') ? E(ui.Menu, {