dsh-better-workspace 0.11.1 → 0.11.2

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.11.1",
3
+ "version": "0.11.2",
4
4
  "main": "./src/index.js",
5
5
  "description": "侧边栏工作区层级树:名称中的 / 即虚拟分组(web/前端 · web/后端),添加工作区的目录流附所属分组弹窗,重命名即时重排层级,可新建空分组。Hierarchy tree for the DSH sidebar workspace list.",
6
6
  "engines": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-better-workspace",
3
- "version": "0.11.1",
3
+ "version": "0.11.2",
4
4
  "description": "DSH web plugin: a hierarchical workspace tree for the sidebar. Workspace titles containing \"/\" group into virtual folders (web/前端 · web/后端); the add-workspace directory flow gains a parent-group popup; renames re-derive the tree; a new-folder button creates empty levels.",
5
5
  "type": "module",
6
6
  "repository": {
package/src/client.js CHANGED
@@ -2699,16 +2699,16 @@ window.__ModuleLoader__.load({
2699
2699
  '.bw-row-icon{flex:none;display:grid;place-items:center;color:var(--dsw-alias-label-tertiary,#9a9a9a)}',
2700
2700
  '.bw-chevron{flex:none;display:grid;place-items:center;color:var(--dsw-alias-label-tertiary,#9a9a9a);transition:transform .15s ease}',
2701
2701
  '.bw-chevron-open{transform:rotate(90deg)}',
2702
- // The outline is a text-shadow now, and overflow:hidden clips shadows just
2703
- // as it clipped the stroke: 3px of padding covers the diagonal reach of the
2704
- // widest rim. margin cancels it, so text starts and truncates where it did.
2702
+ // overflow:hidden (needed for the ellipsis) clips the outline: 3px of
2703
+ // padding covers the widest rim (2px at the top of the slider). margin
2704
+ // cancels it, so text still starts and truncates where it did.
2705
2705
  '.bw-row-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:3px;margin:-3px}',
2706
2706
  '.bw-row-count{flex:none;font-size:11px;color:var(--dsw-alias-label-quaternary,#8a8a8a)}',
2707
2707
  '.bw-row-time{flex:none;font-size:11px;color:var(--dsw-alias-label-quaternary,#8a8a8a)}',
2708
- // The rim is inherited from the row through text-shadow, and the 11px meta
2709
- // column (session count / relative time) opts out of it — stroked meta text
2710
- // outshines the title it supports while keeping the halo the row carries.
2711
- '.bw-row-count,.bw-row-time{text-shadow:var(--bw-glow-shadow,none)}',
2708
+ // The outline is inherited from the row: the 11px meta column (session
2709
+ // count / relative time) opts out — stroked meta text outshines the title
2710
+ // it is supposed to support.
2711
+ '.bw-row-count,.bw-row-time{-webkit-text-stroke-width:0}',
2712
2712
  '.bw-schedule-badge{flex:none;display:inline-flex;align-items:center;color:var(--dsw-alias-label-tertiary,#9a9a9a);margin:0 6px}',
2713
2713
  '.bw-row-actions{flex:none;display:none;align-items:center;gap:2px}',
2714
2714
  '.bw-row:hover .bw-row-actions{display:flex}',
@@ -2790,10 +2790,7 @@ window.__ModuleLoader__.load({
2790
2790
  '.bw-pulse{animation:bw-breathe 1.8s ease-in-out infinite}',
2791
2791
  '@keyframes bw-breathe{0%,100%{filter:drop-shadow(0 0 1px var(--bw-pulse-color));opacity:.55}50%{filter:drop-shadow(0 0 6px var(--bw-pulse-color));opacity:1}}',
2792
2792
  '.bw-pulse-text{animation:bw-breathe-text 1.8s ease-in-out infinite}',
2793
- // The keyframes replace text-shadow wholesale, so the row's rim rides along
2794
- // in --bw-stroke-shadow (set by rowStyleOf, comma-terminated) and the empty
2795
- // fallback keeps a row without an outline breathing exactly as before.
2796
- '@keyframes bw-breathe-text{0%,100%{text-shadow:var(--bw-stroke-shadow,) 0 0 1px var(--bw-pulse-color);opacity:.65}50%{text-shadow:var(--bw-stroke-shadow,) 0 0 7px var(--bw-pulse-color);opacity:1}}',
2793
+ '@keyframes bw-breathe-text{0%,100%{text-shadow:0 0 1px var(--bw-pulse-color);opacity:.65}50%{text-shadow:0 0 7px var(--bw-pulse-color);opacity:1}}',
2797
2794
  '.bw-sync-modes{display:flex;gap:6px;margin-top:8px}',
2798
2795
  '.bw-sync-mode{flex:1;padding:5px 10px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.3));background:transparent;color:var(--dsw-alias-label-secondary,#b8b8b8);cursor:pointer;font-size:12px}',
2799
2796
  '.bw-sync-mode-on{border-color:var(--dsw-alias-brand-primary,#5b8def);color:var(--dsw-alias-label-primary,#e6e6e6);background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.12))}',
@@ -3259,7 +3256,9 @@ window.__ModuleLoader__.load({
3259
3256
  // and returns the opposite pole; rows without a custom color inherit
3260
3257
  // --bw-stroke-color, sampled from the live computed style (see the sampler
3261
3258
  // in BetterBrowser) so a palette flip needs no React render.
3262
- const STROKE_MAX = 4
3259
+ // The slider is the rim the eye actually sees (0.5..2px). strokeStyleOf
3260
+ // paints twice that because paint-order hides the inner half of the band.
3261
+ const STROKE_MAX = 2
3263
3262
  // The outline color defaults to a plain gray: it reads on light and dark
3264
3263
  // backgrounds alike without shouting like pure black/white. "auto" derives
3265
3264
  // the contrasting pole from the font color instead.
@@ -3334,33 +3333,26 @@ window.__ModuleLoader__.load({
3334
3333
  }
3335
3334
  return colorToRgb(picked) ? picked : DEFAULT_APPEARANCE.strokeColor
3336
3335
  }
3337
- // The outline is painted as eight offset copies of the glyph mask instead of
3338
- // with -webkit-text-stroke. That stroke is a GEOMETRIC band centred on the
3339
- // glyph outline: the fill (paint-order: stroke fill) covers its inner half,
3340
- // and the surviving outer half lands wherever the outline happens to sit —
3341
- // usually between device pixels. The rasteriser then spreads that half pixel
3342
- // over two or three of them, the rim never reaches its own colour, and the
3343
- // edge reads as pale and uneven — the "jagged" outline.
3336
+ // paint-order keeps the stroke UNDER the fill so the glyph does not thin out
3337
+ // the whole point of an outer outline. The price is half the band:
3338
+ // -webkit-text-stroke centres it on the outline and the fill covers the inner
3339
+ // half, so the painted width must be TWICE the rim the slider promises.
3340
+ // Painting the configured value (0.10.x) left a 0.5px rim that antialiasing
3341
+ // blended away into a pale, uneven edge.
3344
3342
  //
3345
- // An offset glyph copy snaps to whole device pixels instead (0.5px at 2x is
3346
- // exactly one device pixel), so the rim is solid. Measured on the same 13px
3347
- // row at 2x, the share of rim pixels reaching the picked colour goes
3348
- // 0.19 -> 0.69 while the painted area grows by only ~10%: the outline keeps
3349
- // its width and simply stops being blended away. Cost is nil a 400-row
3350
- // scroll repaints at the same 16.7ms median either way.
3351
- const STROKE_DIRECTIONS = [[1, 0], [-1, 0], [0, 1], [0, -1], [1, 1], [-1, 1], [1, -1], [-1, -1]]
3352
- // One device pixel expressed in CSS pixels. An offset below it is blended
3353
- // away again, so a thin setting floors here instead of fading out.
3354
- const strokeOffsetOf = (appearance) => {
3355
- const width = clampStrokeWidth(appearance.strokeWidth)
3356
- const dpr = typeof window !== 'undefined' && window.devicePixelRatio > 0 ? window.devicePixelRatio : 1
3357
- return Math.max(width / 2, 1 / dpr)
3358
- }
3359
- const strokeShadowsOf = (appearance) => {
3360
- if (!appearance || appearance.stroke === false) return []
3361
- const color = strokeColorOf(appearance)
3362
- const offset = strokeOffsetOf(appearance)
3363
- return STROKE_DIRECTIONS.map(([dx, dy]) => (dx * offset) + 'px ' + (dy * offset) + 'px ' + color)
3343
+ // 0.11.1 replaced this with eight offset glyph copies. It measured better and
3344
+ // looked worse: eight directions are only four diagonal samples, so every
3345
+ // slope and curve came out as a string of detached blocks instead of a filled
3346
+ // rim. A geometric stroke is continuous by construction it really is a path
3347
+ // stroke so the answer is to give it enough width to survive antialiasing,
3348
+ // not to fake it out of copies.
3349
+ const strokeStyleOf = (appearance) => {
3350
+ if (!appearance || appearance.stroke === false) return null
3351
+ return {
3352
+ WebkitTextStrokeWidth: clampStrokeWidth(appearance.strokeWidth) * 2 + 'px',
3353
+ WebkitTextStrokeColor: strokeColorOf(appearance),
3354
+ paintOrder: 'stroke fill',
3355
+ }
3364
3356
  }
3365
3357
 
3366
3358
  function FolderRow({ node, depth, expanded, onToggle, onContextMenu, dropInto, dragEvents, custStyle, iconMode, pulse, t }) {
@@ -3383,7 +3375,7 @@ window.__ModuleLoader__.load({
3383
3375
  E('span', { className: cls('bw-chevron', expanded && 'bw-chevron-open') }, icon('IconTriangleRightFill14', 14)),
3384
3376
  E('span', { className: 'bw-row-icon' }, iconChild),
3385
3377
  // A row that already carries a custom label glow breathes in sync.
3386
- E('span', { className: cls('bw-row-label', pulse && custStyle && custStyle['--bw-text-glow'] && 'bw-pulse-text') }, node.name),
3378
+ E('span', { className: cls('bw-row-label', pulse && custStyle && custStyle.textShadow && 'bw-pulse-text') }, node.name),
3387
3379
  total > 0 ? E('span', { className: 'bw-row-count' }, String(total)) : null,
3388
3380
  )
3389
3381
  }
@@ -3404,7 +3396,7 @@ window.__ModuleLoader__.load({
3404
3396
  },
3405
3397
  E('span', { className: cls('bw-chevron', sessionsOpen && 'bw-chevron-open') }, icon('IconTriangleRightFill14', 14)),
3406
3398
  E('span', { className: 'bw-row-icon' }, iconChild),
3407
- E('span', { className: cls('bw-row-label', pulse && custStyle && custStyle['--bw-text-glow'] && 'bw-pulse-text'), title: workspace.title || workspace.leaf }, workspace.leaf),
3399
+ E('span', { className: cls('bw-row-label', pulse && custStyle && custStyle.textShadow && 'bw-pulse-text'), title: workspace.title || workspace.leaf }, workspace.leaf),
3408
3400
  count > 0 ? E('span', { className: 'bw-row-count' }, String(count)) : null,
3409
3401
  E('span', { className: 'bw-row-actions', onClick: (e) => e.stopPropagation() },
3410
3402
  E('button', { type: 'button', className: 'bw-icon-btn', 'aria-label': t('session.new'), onClick: (e) => { e.stopPropagation(); onStart() } }, icon('IconPlusOutline16')),
@@ -3591,7 +3583,7 @@ window.__ModuleLoader__.load({
3591
3583
  next[index] = n
3592
3584
  patch({ color: rgbToHex(next[0], next[1], next[2]) })
3593
3585
  }
3594
- const previewShadows = strokeShadowsOf(appearance)
3586
+ const previewShadows = []
3595
3587
  if (glow > 0 && color) previewShadows.push('0 0 ' + glow + 'px ' + color)
3596
3588
  if (shadow) previewShadows.push('1px 1px 2px rgba(0,0,0,.85)')
3597
3589
  // The preview must show the REAL outline: with no custom color the pole
@@ -3611,6 +3603,7 @@ window.__ModuleLoader__.load({
3611
3603
  const poll = setInterval(sample, 1500)
3612
3604
  return () => clearInterval(poll)
3613
3605
  }, [color, stroke])
3606
+ const previewStroke = strokeStyleOf(appearance)
3614
3607
  return E('div', { className: 'bw-appearance' },
3615
3608
  E('div', { className: 'bw-field' },
3616
3609
  t('custom.color'),
@@ -3751,6 +3744,7 @@ window.__ModuleLoader__.load({
3751
3744
  color: color || undefined,
3752
3745
  fontWeight: weight > 0 ? weight : undefined,
3753
3746
  textShadow: previewShadows.length > 0 ? previewShadows.join(',') : undefined,
3747
+ ...(previewStroke || null),
3754
3748
  },
3755
3749
  },
3756
3750
  allowIcon ? E('span', { className: 'bw-preview-icon' }, iconOf(iconMode, true)) : null,
@@ -4177,27 +4171,15 @@ window.__ModuleLoader__.load({
4177
4171
  const glow = Number(appearance.glow) || 0
4178
4172
  const weight = Number(appearance.weight) || 0
4179
4173
  const shadow = appearance.shadow === true
4180
- // text-shadow paints its first entry on top, so the rim goes in front of
4181
- // the halo and the drop shadow that sit outside it.
4182
- const outline = strokeShadowsOf(appearance)
4183
- const halos = []
4184
- if (glow > 0 && color) halos.push('0 0 ' + glow + 'px ' + color)
4185
- if (shadow) halos.push('1px 1px 2px rgba(0,0,0,.85)')
4186
- const shadows = outline.concat(halos)
4174
+ const shadows = []
4175
+ if (glow > 0 && color) shadows.push('0 0 ' + glow + 'px ' + color)
4176
+ if (shadow) shadows.push('1px 1px 2px rgba(0,0,0,.85)')
4187
4177
  const style = {}
4188
4178
  if (color) style.color = color
4189
4179
  if (weight > 0) style.fontWeight = weight
4190
4180
  if (shadows.length > 0) style.textShadow = shadows.join(',')
4191
- // The rim is published twice: inside the row's own text-shadow, and as a
4192
- // variable for the places that cannot inherit it. The trailing comma is
4193
- // load-bearing — the pulse keyframes append the halo after it.
4194
- if (outline.length > 0) style['--bw-stroke-shadow'] = outline.join(',') + ','
4195
- // Meta text (session count / relative time) opts out of the rim but keeps
4196
- // the halo, which it can only read from here.
4197
- if (halos.length > 0) style['--bw-glow-shadow'] = halos.join(',')
4198
- // The label breathes its text only when the row actually carries a halo;
4199
- // a rim alone is not a reason to pulse.
4200
- if (halos.length > 0) style['--bw-text-glow'] = '1'
4181
+ const stroke = strokeStyleOf(appearance)
4182
+ if (stroke) Object.assign(style, stroke)
4201
4183
  return Object.keys(style).length > 0 ? style : null
4202
4184
  }
4203
4185
  const keyOf = (kind, payload) => {