dsh-better-workspace 0.10.0 → 0.10.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.md CHANGED
@@ -42,7 +42,7 @@ web/ ← 虚拟分组(不是真实目录,纯命名层级)
42
42
  <table>
43
43
  <tr>
44
44
  <td align="center" width="58%"><img src="docs/screenshots/3-appearance-dialog.png" alt="外观自定义弹窗(实时预览)"/></td>
45
- <td valign="top"><b>外观自定义</b><br/>右键任意一层(工作区分组 / 工作区 / 会话 / 会话分组)→「自定义外观」:颜色(预设色板 + 取色器 + RGB)、发光强度(0..14 拖动条,只作用于文字)、字体粗细、字体阴影开关、**字体描边**(开关 + 0.5–4px 粗细,**默认开启**;描边颜色自动取该行字体颜色的反差色——浅色字配黑边、深色字配白边,跟随主题与背景插件的界面明暗,不用手选)、图标(与 dsh 图标库同族的 69 个,工作区与工作区分组专属),底部带实时预览。清除自定义一键复原。</td>
45
+ <td valign="top"><b>外观自定义</b><br/>右键任意一层(工作区分组 / 工作区 / 会话 / 会话分组)→「自定义外观」:颜色(预设色板 + 取色器 + RGB)、发光强度(0..14 拖动条,只作用于文字)、字体粗细、字体阴影开关、**字体描边**(开关 + 0.5–4px 粗细,**默认开启**;描边颜色默认**灰**,可改黑 / 白 / 任意取色,或选「自动」按字体颜色取反差色——浅色字配黑边、深色字配白边,并跟随主题与背景插件的界面明暗)、图标(与 dsh 图标库同族的 69 个,工作区与工作区分组专属),底部带实时预览。清除自定义一键复原。</td>
46
46
  </tr>
47
47
  <tr>
48
48
  <td align="center" width="58%"><img src="docs/screenshots/2-customized-tree.png" alt="外观自定义效果"/></td>
package/README_EN.md CHANGED
@@ -40,7 +40,7 @@ Session titles nest on `/` too (e.g. `test1/plugin maintenance`). Session groups
40
40
  <table>
41
41
  <tr>
42
42
  <td align="center" width="58%"><img src="docs/screenshots/3-appearance-dialog.png" alt="Appearance dialog with live preview"/></td>
43
- <td valign="top"><b>Appearance customization</b><br/>Right-click any row (workspace folder / workspace / session / session group) → Customize: color (swatches + picker + RGB), glow strength (0–14 slider, text only), font weight, font shadow, a <b>text outline</b> (toggle + 0.5–4px width, <b>on by default</b>; its color is derived from the row's own font color — light text gets a black rim, dark text a white one — and follows the theme or a background plugin's light/dark switch, never picked by hand), an icon grid (69 icons from the dsh primitives family, workspace &amp; folder rows only), with a live preview at the bottom. Reset returns to default in one click.</td>
43
+ <td valign="top"><b>Appearance customization</b><br/>Right-click any row (workspace folder / workspace / session / session group) → Customize: color (swatches + picker + RGB), glow strength (0–14 slider, text only), font weight, font shadow, a <b>text outline</b> (toggle + 0.5–4px width, <b>on by default</b>; its color is gray by default, with black / white / any-color picker and an Auto mode that derives the contrasting pole from the row's font color — light text gets a black rim, dark text a white one — following the theme or a background plugin's light/dark switch), an icon grid (69 icons from the dsh primitives family, workspace &amp; folder rows only), with a live preview at the bottom. Reset returns to default in one click.</td>
44
44
  </tr>
45
45
  <tr>
46
46
  <td align="center" width="58%"><img src="docs/screenshots/2-customized-tree.png" alt="Custom appearance result"/></td>
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-better-workspace",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
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.10.0",
3
+ "version": "0.10.1",
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
@@ -84,8 +84,10 @@ window.__ModuleLoader__.load({
84
84
  'custom.weight.bold': '粗',
85
85
  'custom.shadow': '字体阴影',
86
86
  'custom.stroke': '字体描边',
87
- 'custom.stroke.hint': '描边颜色自动取字体颜色的反差色(浅色字配黑描边、深色字配白描边),跟随主题明暗与背景插件的界面明暗,无需手动指定。',
87
+ 'custom.stroke.hint': '描边颜色默认灰色,可改黑 / 白 / 任意取色,或选「自动」按字体颜色取反差色(浅色字配黑边、深色字配白边);自动模式跟随主题明暗与背景插件的界面明暗。',
88
88
  'custom.strokeWidth': '描边粗细',
89
+ 'custom.strokeColor': '描边颜色',
90
+ 'custom.strokeColor.auto': '自动',
89
91
  'custom.weak': '弱',
90
92
  'custom.medium': '中',
91
93
  'custom.strong': '强',
@@ -190,8 +192,10 @@ window.__ModuleLoader__.load({
190
192
  'custom.weight.bold': 'Bold',
191
193
  'custom.shadow': 'Font shadow',
192
194
  'custom.stroke': 'Font outline',
193
- 'custom.stroke.hint': 'The outline color is derived from the font color (light text gets a black outline, dark text a white one) and follows the theme or a background plugin\'s light/dark switch — no manual color to pick.',
195
+ 'custom.stroke.hint': 'The outline is gray by default — pick black, white, any color, or Auto to derive the contrasting pole from the font color (light text gets a black rim, dark text a white one); Auto follows the theme and a background plugin\'s light/dark switch.',
194
196
  'custom.strokeWidth': 'Outline width',
197
+ 'custom.strokeColor': 'Outline color',
198
+ 'custom.strokeColor.auto': 'Auto',
195
199
  'custom.weak': 'Subtle',
196
200
  'custom.medium': 'Medium',
197
201
  'custom.strong': 'Strong',
@@ -598,9 +602,13 @@ window.__ModuleLoader__.load({
598
602
  '.bw-row-icon{flex:none;display:grid;place-items:center;color:var(--dsw-alias-label-tertiary,#9a9a9a)}',
599
603
  '.bw-chevron{flex:none;display:grid;place-items:center;color:var(--dsw-alias-label-tertiary,#9a9a9a);transition:transform .15s ease}',
600
604
  '.bw-chevron-open{transform:rotate(90deg)}',
601
- '.bw-row-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
605
+ '.bw-row-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:1px;margin:-1px}',
602
606
  '.bw-row-count{flex:none;font-size:11px;color:var(--dsw-alias-label-quaternary,#8a8a8a)}',
603
607
  '.bw-row-time{flex:none;font-size:11px;color:var(--dsw-alias-label-quaternary,#8a8a8a)}',
608
+ // The outline is inherited from the row: the 11px meta column (session
609
+ // count / relative time) opts out — stroked meta text outshines the title
610
+ // it is supposed to support.
611
+ '.bw-row-count,.bw-row-time{-webkit-text-stroke-width:0}',
604
612
  '.bw-schedule-badge{flex:none;display:inline-flex;align-items:center;color:var(--dsw-alias-label-tertiary,#9a9a9a);margin:0 6px}',
605
613
  '.bw-row-actions{flex:none;display:none;align-items:center;gap:2px}',
606
614
  '.bw-row:hover .bw-row-actions{display:flex}',
@@ -612,6 +620,7 @@ window.__ModuleLoader__.load({
612
620
  '.bw-session-row:hover{color:var(--dsw-alias-label-primary,#e6e6e6)}',
613
621
  '.bw-empty{padding:28px 12px;text-align:center;font-size:12px;color:var(--dsw-alias-label-dimmed,#7a7a7a)}',
614
622
  '.bw-swatch{width:20px;height:20px;border-radius:6px;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.25));cursor:pointer;flex:none;background:transparent;padding:0}',
623
+ '.bw-swatch-wide{width:auto;min-width:38px;padding:0 8px;font-size:11px;color:var(--dsw-alias-label-secondary,#b8b8b8)}',
615
624
  '.bw-swatch-active{outline:2px solid var(--dsw-alias-brand-primary,#5b8def);outline-offset:1px}',
616
625
  '.bw-color-input{width:36px;height:26px;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.25));border-radius:6px;background:transparent;cursor:pointer;padding:0}',
617
626
  '.bw-seg{display:flex;gap:6px;flex-wrap:wrap}',
@@ -1134,8 +1143,11 @@ window.__ModuleLoader__.load({
1134
1143
  // --bw-stroke-color, sampled from the live computed style (see the sampler
1135
1144
  // in BetterBrowser) so a palette flip needs no React render.
1136
1145
  const STROKE_MAX = 4
1137
- const DEFAULT_APPEARANCE = { color: '', glow: 0, weight: 0, shadow: false, stroke: true, strokeWidth: 1 }
1138
- const APPEARANCE_FIELDS = ['color', 'glow', 'weight', 'shadow', 'stroke', 'strokeWidth']
1146
+ // The outline color defaults to a plain gray: it reads on light and dark
1147
+ // backgrounds alike without shouting like pure black/white. "auto" derives
1148
+ // the contrasting pole from the font color instead.
1149
+ const DEFAULT_APPEARANCE = { color: '', glow: 0, weight: 0, shadow: false, stroke: true, strokeWidth: 1, strokeColor: '#808080' }
1150
+ const APPEARANCE_FIELDS = ['color', 'glow', 'weight', 'shadow', 'stroke', 'strokeWidth', 'strokeColor']
1139
1151
  const clampStrokeWidth = (raw) => {
1140
1152
  const n = Number(raw)
1141
1153
  if (!isFinite(n) || n <= 0) return DEFAULT_APPEARANCE.strokeWidth
@@ -1153,6 +1165,7 @@ window.__ModuleLoader__.load({
1153
1165
  shadow: src.shadow === true,
1154
1166
  stroke: src.stroke !== false,
1155
1167
  strokeWidth: src.strokeWidth === undefined ? DEFAULT_APPEARANCE.strokeWidth : clampStrokeWidth(src.strokeWidth),
1168
+ strokeColor: typeof src.strokeColor === 'string' && src.strokeColor !== '' ? src.strokeColor : DEFAULT_APPEARANCE.strokeColor,
1156
1169
  }
1157
1170
  }
1158
1171
  // A row's own entry overrides the default appearance FIELD BY FIELD, so a
@@ -1189,14 +1202,28 @@ window.__ModuleLoader__.load({
1189
1202
  return (luminance + 0.05) / 0.05 >= 1.05 / (luminance + 0.05) ? '#000000' : '#ffffff'
1190
1203
  }
1191
1204
  const STROKE_FALLBACK = 'var(--bw-stroke-color, rgba(0,0,0,.75))'
1205
+ const STROKE_AUTO = 'auto'
1206
+ // A picked color wins; "auto" derives the contrasting pole from the row's
1207
+ // font color — computed in JS when the row carries one, read from the
1208
+ // sampled --bw-stroke-color variable otherwise (so a theme or background
1209
+ // plugin flip needs no render).
1210
+ const strokeColorOf = (appearance) => {
1211
+ const picked = appearance && typeof appearance.strokeColor === 'string' && appearance.strokeColor !== ''
1212
+ ? appearance.strokeColor
1213
+ : DEFAULT_APPEARANCE.strokeColor
1214
+ if (picked === STROKE_AUTO) {
1215
+ const rgb = appearance.color ? colorToRgb(appearance.color) : null
1216
+ return rgb ? contrastStrokeColor(rgb) : STROKE_FALLBACK
1217
+ }
1218
+ return colorToRgb(picked) ? picked : DEFAULT_APPEARANCE.strokeColor
1219
+ }
1192
1220
  // paint-order keeps the stroke UNDER the fill, so the glyph does not thin
1193
1221
  // out — the whole point of an outer outline.
1194
1222
  const strokeStyleOf = (appearance) => {
1195
1223
  if (!appearance || appearance.stroke === false) return null
1196
- const rgb = appearance.color ? colorToRgb(appearance.color) : null
1197
1224
  return {
1198
1225
  WebkitTextStrokeWidth: clampStrokeWidth(appearance.strokeWidth) + 'px',
1199
- WebkitTextStrokeColor: rgb ? contrastStrokeColor(rgb) : STROKE_FALLBACK,
1226
+ WebkitTextStrokeColor: strokeColorOf(appearance),
1200
1227
  paintOrder: 'stroke fill',
1201
1228
  }
1202
1229
  }
@@ -1350,6 +1377,10 @@ window.__ModuleLoader__.load({
1350
1377
  /* --------------------- customization dialog ----------------------- */
1351
1378
 
1352
1379
  const SWATCHES = ['', '#5b8def', '#3fb950', '#d29922', '#f85149', '#a371f7', '#39c5cf', '#ec6cb9', '#ff9f45', '#6e7681']
1380
+ // Outline-color presets: gray (the default), black, white, brand blue — the
1381
+ // native picker next to them covers everything else, and "auto" derives the
1382
+ // contrasting pole from the font color.
1383
+ const STROKE_PRESETS = ['#808080', '#000000', '#ffffff', '#5b8def']
1353
1384
  const GLOW_MAX = 14
1354
1385
  const ICON_CHOICES = [
1355
1386
  'solid', 'outline', 'none',
@@ -1389,6 +1420,7 @@ window.__ModuleLoader__.load({
1389
1420
  const shadow = appearance.shadow
1390
1421
  const stroke = appearance.stroke
1391
1422
  const strokeWidth = appearance.strokeWidth
1423
+ const strokeColor = appearance.strokeColor
1392
1424
  const iconMode = value && value.icon ? value.icon : 'solid'
1393
1425
  const patch = (next) => { if (typeof onChange === 'function') onChange(next) }
1394
1426
  const channels = colorToRgb(color)
@@ -1507,6 +1539,32 @@ window.__ModuleLoader__.load({
1507
1539
  }),
1508
1540
  E('span', { className: 'bw-glow-value' }, strokeWidth + 'px'),
1509
1541
  ),
1542
+ ),
1543
+ E('div', { className: 'bw-field' },
1544
+ t('custom.strokeColor'),
1545
+ E('div', { className: 'bw-dialog-input-row' },
1546
+ E('button', {
1547
+ type: 'button',
1548
+ className: cls('bw-swatch', 'bw-swatch-wide', strokeColor === STROKE_AUTO && 'bw-swatch-active'),
1549
+ title: t('custom.strokeColor.auto'),
1550
+ onClick: () => patch({ strokeColor: STROKE_AUTO }),
1551
+ }, t('custom.strokeColor.auto')),
1552
+ STROKE_PRESETS.map((value) => E('button', {
1553
+ key: value,
1554
+ type: 'button',
1555
+ className: cls('bw-swatch', strokeColor === value && 'bw-swatch-active'),
1556
+ style: { background: value },
1557
+ 'aria-label': value,
1558
+ title: value,
1559
+ onClick: () => patch({ strokeColor: value }),
1560
+ })),
1561
+ E('input', {
1562
+ type: 'color',
1563
+ className: 'bw-color-input',
1564
+ value: colorToRgb(strokeColor) ? strokeColor : DEFAULT_APPEARANCE.strokeColor,
1565
+ onChange: (e) => patch({ strokeColor: e.target.value }),
1566
+ }),
1567
+ ),
1510
1568
  E('div', { className: 'bw-hint' }, t('custom.stroke.hint')),
1511
1569
  ),
1512
1570
  allowIcon ? E('div', { className: 'bw-field' },