dsh-better-workspace 0.10.0 → 0.10.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/README.md +6 -1
- package/README_EN.md +6 -1
- package/dsh.plugin.json +1 -1
- package/package.json +1 -1
- package/src/client.js +191 -22
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
|
|
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>
|
|
@@ -106,6 +106,11 @@ DSH 的侧边栏浏览区是一个公开插槽 `sidebar.workspaces`(single),官
|
|
|
106
106
|
|
|
107
107
|
同属 `dsh-better-*` 家族,但互不重叠:[dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) 是**右侧**的 VSCode 式面板(资源管理器/终端/Git);本插件只接管**左侧**工作区列表。两者可同装。
|
|
108
108
|
|
|
109
|
+
## 版本兼容
|
|
110
|
+
|
|
111
|
+
- **dsh 0.1.6-alpha.1**:图标集与浏览器注入面都变了,本插件 **0.10.2 起自动适配**——图标网格只列本宿主真正导出的字形(退役的 `IconSendOutline16` 自动落到等价的 `IconSendOutline14`,已保存的旧选择不会变成空图标位),会话拖拽排序在宿主不再注入 `insertSessionBefore` 时改用浏览器本地顺序,**功能不丢**。升级 dsh 与升级插件谁先谁后都收敛。
|
|
112
|
+
- **跨端**:web 与桌面共用同一套客户端资产;外观 / 分组 / 开关可经设置卡片手动跨端同步,**折叠状态与会话本地序每端本地**(设计如此)。
|
|
113
|
+
|
|
109
114
|
## 限制与路线图
|
|
110
115
|
|
|
111
116
|
- 会话拖到**另一分组内的会话行**仅做扁平重排(标题分组归属不变);要改分组请拖到分组行或走重命名。
|
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
|
|
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 & 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>
|
|
@@ -90,6 +90,11 @@ View state (folder collapse, session expansion, explicit empty folders), custom
|
|
|
90
90
|
|
|
91
91
|
Same `dsh-better-*` family, zero overlap: [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) is the VSCode-like panel on the right; this plugin only takes over the left workspace list. They can be installed together.
|
|
92
92
|
|
|
93
|
+
## Version compatibility
|
|
94
|
+
|
|
95
|
+
- **dsh 0.1.6-alpha.1**: both the primitives icon set and the browser injection face changed; this plugin **adapts automatically since 0.10.2**. The icon grid only lists glyphs this host actually exports (the retired `IconSendOutline16` resolves to the equivalent `IconSendOutline14`, so a previously saved choice never turns into an empty icon slot), and session drag-to-reorder falls back to a browser-local order when the host no longer injects `insertSessionBefore` — **no feature is lost**. Either upgrade order (plugin first or dsh first) converges.
|
|
96
|
+
- **Across surfaces**: web and desktop share one set of client assets; appearance / groups / switches sync manually through the settings card, while **collapse state and the local session order stay per-browser** by design.
|
|
97
|
+
|
|
93
98
|
## Limitations / roadmap
|
|
94
99
|
|
|
95
100
|
- A session dragged onto a session row **inside another group** only gets reordered in the flat list (its title group stays); drag onto the group row or rename to move it.
|
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-external/dsh-better-workspace",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.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.10.
|
|
3
|
+
"version": "0.10.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
|
@@ -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
|
|
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',
|
|
@@ -313,6 +317,28 @@ window.__ModuleLoader__.load({
|
|
|
313
317
|
}
|
|
314
318
|
const normPath = (p) => splitTitleSegs(p).join('/')
|
|
315
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Glyphs a host may have RETIRED, mapped to the surviving equivalents in
|
|
322
|
+
* preference order. dsh 0.1.6-alpha.1 replaced IconSendOutline16 with
|
|
323
|
+
* IconPaperPlaneOutline14 and deleted the old export, so a saved custom
|
|
324
|
+
* icon naming it must land on something real rather than an empty cell.
|
|
325
|
+
*/
|
|
326
|
+
const ICON_ALIASES = { IconSendOutline16: ['IconSendOutline14'] }
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Resolve one configured icon value to a glyph the LOADED primitives
|
|
330
|
+
* actually export (pure). Retired names follow their alias chain, unknown
|
|
331
|
+
* names degrade to '' — so both the picker grid and a choice persisted
|
|
332
|
+
* before an upgrade survive a host that renamed its icon set.
|
|
333
|
+
*/
|
|
334
|
+
const resolveIconName = (name) => {
|
|
335
|
+
if (typeof name !== 'string' || name === '') return ''
|
|
336
|
+
if (ui[name]) return name
|
|
337
|
+
const aliases = ICON_ALIASES[name]
|
|
338
|
+
if (aliases) for (const alias of aliases) if (ui[alias]) return alias
|
|
339
|
+
return ''
|
|
340
|
+
}
|
|
341
|
+
|
|
316
342
|
/** Render a primitives icon by name; unknown names degrade to null, never crash. */
|
|
317
343
|
const icon = (name, size) => {
|
|
318
344
|
const C = ui[name]
|
|
@@ -452,6 +478,20 @@ window.__ModuleLoader__.load({
|
|
|
452
478
|
return root
|
|
453
479
|
}
|
|
454
480
|
|
|
481
|
+
/**
|
|
482
|
+
* Move one id inside a flat list: before `anchor`, or to the end when the
|
|
483
|
+
* anchor is missing (pure). Mirrors the host action's insertSessionBefore
|
|
484
|
+
* semantics, so the browser-local fallback orders rows exactly the way the
|
|
485
|
+
* host would have.
|
|
486
|
+
*/
|
|
487
|
+
function reorderIds(ids, id, anchor) {
|
|
488
|
+
const next = (ids || []).filter((x) => x !== id)
|
|
489
|
+
const at = anchor === undefined ? -1 : next.indexOf(anchor)
|
|
490
|
+
if (at === -1) next.push(id)
|
|
491
|
+
else next.splice(at, 0, id)
|
|
492
|
+
return next
|
|
493
|
+
}
|
|
494
|
+
|
|
455
495
|
const countSessionTree = (node) => node.sessions.length + node.groups.reduce((sum, group) => sum + countSessionTree(group), 0)
|
|
456
496
|
|
|
457
497
|
const collectSessionRows = (node) => {
|
|
@@ -598,9 +638,13 @@ window.__ModuleLoader__.load({
|
|
|
598
638
|
'.bw-row-icon{flex:none;display:grid;place-items:center;color:var(--dsw-alias-label-tertiary,#9a9a9a)}',
|
|
599
639
|
'.bw-chevron{flex:none;display:grid;place-items:center;color:var(--dsw-alias-label-tertiary,#9a9a9a);transition:transform .15s ease}',
|
|
600
640
|
'.bw-chevron-open{transform:rotate(90deg)}',
|
|
601
|
-
'.bw-row-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
641
|
+
'.bw-row-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:1px;margin:-1px}',
|
|
602
642
|
'.bw-row-count{flex:none;font-size:11px;color:var(--dsw-alias-label-quaternary,#8a8a8a)}',
|
|
603
643
|
'.bw-row-time{flex:none;font-size:11px;color:var(--dsw-alias-label-quaternary,#8a8a8a)}',
|
|
644
|
+
// The outline is inherited from the row: the 11px meta column (session
|
|
645
|
+
// count / relative time) opts out — stroked meta text outshines the title
|
|
646
|
+
// it is supposed to support.
|
|
647
|
+
'.bw-row-count,.bw-row-time{-webkit-text-stroke-width:0}',
|
|
604
648
|
'.bw-schedule-badge{flex:none;display:inline-flex;align-items:center;color:var(--dsw-alias-label-tertiary,#9a9a9a);margin:0 6px}',
|
|
605
649
|
'.bw-row-actions{flex:none;display:none;align-items:center;gap:2px}',
|
|
606
650
|
'.bw-row:hover .bw-row-actions{display:flex}',
|
|
@@ -612,6 +656,7 @@ window.__ModuleLoader__.load({
|
|
|
612
656
|
'.bw-session-row:hover{color:var(--dsw-alias-label-primary,#e6e6e6)}',
|
|
613
657
|
'.bw-empty{padding:28px 12px;text-align:center;font-size:12px;color:var(--dsw-alias-label-dimmed,#7a7a7a)}',
|
|
614
658
|
'.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}',
|
|
659
|
+
'.bw-swatch-wide{width:auto;min-width:38px;padding:0 8px;font-size:11px;color:var(--dsw-alias-label-secondary,#b8b8b8)}',
|
|
615
660
|
'.bw-swatch-active{outline:2px solid var(--dsw-alias-brand-primary,#5b8def);outline-offset:1px}',
|
|
616
661
|
'.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
662
|
'.bw-seg{display:flex;gap:6px;flex-wrap:wrap}',
|
|
@@ -696,7 +741,7 @@ window.__ModuleLoader__.load({
|
|
|
696
741
|
/* ========================== view store =========================== */
|
|
697
742
|
|
|
698
743
|
const createViewStore = () => storeKit.defineStore({
|
|
699
|
-
init: () => ({ folders: [], expanded: {}, sessionsExpanded: {}, sessionGroups: {}, prefs: { compactChains: true }, styling: {} }),
|
|
744
|
+
init: () => ({ folders: [], expanded: {}, sessionsExpanded: {}, sessionGroups: {}, sessionOrder: {}, prefs: { compactChains: true }, styling: {} }),
|
|
700
745
|
// NOTE: hydration REPLACES the state with the persisted whole value —
|
|
701
746
|
// init defaults never merge. Every action must tolerate a missing key
|
|
702
747
|
// (states persisted by older plugin versions lack sessionGroups), and
|
|
@@ -734,6 +779,16 @@ window.__ModuleLoader__.load({
|
|
|
734
779
|
setSessionGroupExpanded: (d, key, value) => { if (!d.sessionGroups) d.sessionGroups = {}; d.sessionGroups[key] = value },
|
|
735
780
|
setPref: (d, key, value) => { if (!d.prefs) d.prefs = {}; d.prefs[key] = value },
|
|
736
781
|
setStyling: (d, key, value) => { if (!d.styling) d.styling = {}; if (value === null) delete d.styling[key]; else d.styling[key] = value },
|
|
782
|
+
// Browser-local session order (v0.10.2): the fallback channel for
|
|
783
|
+
// drag-to-reorder on hosts that no longer inject insertSessionBefore
|
|
784
|
+
// (dsh 0.1.6-alpha.1). Per-workspace and per-browser on purpose — the
|
|
785
|
+
// host order stays authoritative wherever the action exists, and this
|
|
786
|
+
// never rides the cross-device sync.
|
|
787
|
+
setSessionOrder: (d, workspaceId, order) => {
|
|
788
|
+
if (!d.sessionOrder || typeof d.sessionOrder !== 'object') d.sessionOrder = {}
|
|
789
|
+
if (!Array.isArray(order) || order.length === 0) delete d.sessionOrder[workspaceId]
|
|
790
|
+
else d.sessionOrder[workspaceId] = order.slice()
|
|
791
|
+
},
|
|
737
792
|
addFolder: (d, path) => {
|
|
738
793
|
if (!Array.isArray(d.folders)) d.folders = []
|
|
739
794
|
const p = normPath(path)
|
|
@@ -1105,12 +1160,16 @@ window.__ModuleLoader__.load({
|
|
|
1105
1160
|
/* ============================== rows ============================== */
|
|
1106
1161
|
|
|
1107
1162
|
/** Folder glyph variants from the primitives family (solid / outline / hidden). */
|
|
1108
|
-
/**
|
|
1163
|
+
/**
|
|
1164
|
+
* Custom icon value → glyph: legacy slots (solid/outline/none) or any
|
|
1165
|
+
* primitives icon name, resolved through ICON_ALIASES so a value persisted
|
|
1166
|
+
* before a host renamed its icon set still renders something.
|
|
1167
|
+
*/
|
|
1109
1168
|
const iconOf = (mode, expanded) => {
|
|
1110
1169
|
if (!mode || mode === 'none') return null
|
|
1111
1170
|
if (mode === 'solid') return icon(expanded ? 'IconFolderOpen16' : 'IconFolderClose16')
|
|
1112
1171
|
if (mode === 'outline') return icon('IconFolderOpenOutline16')
|
|
1113
|
-
return icon(mode)
|
|
1172
|
+
return icon(resolveIconName(mode))
|
|
1114
1173
|
}
|
|
1115
1174
|
|
|
1116
1175
|
const colorToRgb = (hex) => {
|
|
@@ -1134,8 +1193,11 @@ window.__ModuleLoader__.load({
|
|
|
1134
1193
|
// --bw-stroke-color, sampled from the live computed style (see the sampler
|
|
1135
1194
|
// in BetterBrowser) so a palette flip needs no React render.
|
|
1136
1195
|
const STROKE_MAX = 4
|
|
1137
|
-
|
|
1138
|
-
|
|
1196
|
+
// The outline color defaults to a plain gray: it reads on light and dark
|
|
1197
|
+
// backgrounds alike without shouting like pure black/white. "auto" derives
|
|
1198
|
+
// the contrasting pole from the font color instead.
|
|
1199
|
+
const DEFAULT_APPEARANCE = { color: '', glow: 0, weight: 0, shadow: false, stroke: true, strokeWidth: 1, strokeColor: '#808080' }
|
|
1200
|
+
const APPEARANCE_FIELDS = ['color', 'glow', 'weight', 'shadow', 'stroke', 'strokeWidth', 'strokeColor']
|
|
1139
1201
|
const clampStrokeWidth = (raw) => {
|
|
1140
1202
|
const n = Number(raw)
|
|
1141
1203
|
if (!isFinite(n) || n <= 0) return DEFAULT_APPEARANCE.strokeWidth
|
|
@@ -1153,6 +1215,7 @@ window.__ModuleLoader__.load({
|
|
|
1153
1215
|
shadow: src.shadow === true,
|
|
1154
1216
|
stroke: src.stroke !== false,
|
|
1155
1217
|
strokeWidth: src.strokeWidth === undefined ? DEFAULT_APPEARANCE.strokeWidth : clampStrokeWidth(src.strokeWidth),
|
|
1218
|
+
strokeColor: typeof src.strokeColor === 'string' && src.strokeColor !== '' ? src.strokeColor : DEFAULT_APPEARANCE.strokeColor,
|
|
1156
1219
|
}
|
|
1157
1220
|
}
|
|
1158
1221
|
// A row's own entry overrides the default appearance FIELD BY FIELD, so a
|
|
@@ -1189,14 +1252,28 @@ window.__ModuleLoader__.load({
|
|
|
1189
1252
|
return (luminance + 0.05) / 0.05 >= 1.05 / (luminance + 0.05) ? '#000000' : '#ffffff'
|
|
1190
1253
|
}
|
|
1191
1254
|
const STROKE_FALLBACK = 'var(--bw-stroke-color, rgba(0,0,0,.75))'
|
|
1255
|
+
const STROKE_AUTO = 'auto'
|
|
1256
|
+
// A picked color wins; "auto" derives the contrasting pole from the row's
|
|
1257
|
+
// font color — computed in JS when the row carries one, read from the
|
|
1258
|
+
// sampled --bw-stroke-color variable otherwise (so a theme or background
|
|
1259
|
+
// plugin flip needs no render).
|
|
1260
|
+
const strokeColorOf = (appearance) => {
|
|
1261
|
+
const picked = appearance && typeof appearance.strokeColor === 'string' && appearance.strokeColor !== ''
|
|
1262
|
+
? appearance.strokeColor
|
|
1263
|
+
: DEFAULT_APPEARANCE.strokeColor
|
|
1264
|
+
if (picked === STROKE_AUTO) {
|
|
1265
|
+
const rgb = appearance.color ? colorToRgb(appearance.color) : null
|
|
1266
|
+
return rgb ? contrastStrokeColor(rgb) : STROKE_FALLBACK
|
|
1267
|
+
}
|
|
1268
|
+
return colorToRgb(picked) ? picked : DEFAULT_APPEARANCE.strokeColor
|
|
1269
|
+
}
|
|
1192
1270
|
// paint-order keeps the stroke UNDER the fill, so the glyph does not thin
|
|
1193
1271
|
// out — the whole point of an outer outline.
|
|
1194
1272
|
const strokeStyleOf = (appearance) => {
|
|
1195
1273
|
if (!appearance || appearance.stroke === false) return null
|
|
1196
|
-
const rgb = appearance.color ? colorToRgb(appearance.color) : null
|
|
1197
1274
|
return {
|
|
1198
1275
|
WebkitTextStrokeWidth: clampStrokeWidth(appearance.strokeWidth) + 'px',
|
|
1199
|
-
WebkitTextStrokeColor:
|
|
1276
|
+
WebkitTextStrokeColor: strokeColorOf(appearance),
|
|
1200
1277
|
paintOrder: 'stroke fill',
|
|
1201
1278
|
}
|
|
1202
1279
|
}
|
|
@@ -1350,6 +1427,10 @@ window.__ModuleLoader__.load({
|
|
|
1350
1427
|
/* --------------------- customization dialog ----------------------- */
|
|
1351
1428
|
|
|
1352
1429
|
const SWATCHES = ['', '#5b8def', '#3fb950', '#d29922', '#f85149', '#a371f7', '#39c5cf', '#ec6cb9', '#ff9f45', '#6e7681']
|
|
1430
|
+
// Outline-color presets: gray (the default), black, white, brand blue — the
|
|
1431
|
+
// native picker next to them covers everything else, and "auto" derives the
|
|
1432
|
+
// contrasting pole from the font color.
|
|
1433
|
+
const STROKE_PRESETS = ['#808080', '#000000', '#ffffff', '#5b8def']
|
|
1353
1434
|
const GLOW_MAX = 14
|
|
1354
1435
|
const ICON_CHOICES = [
|
|
1355
1436
|
'solid', 'outline', 'none',
|
|
@@ -1366,20 +1447,42 @@ window.__ModuleLoader__.load({
|
|
|
1366
1447
|
'IconLoadingOutline16', 'IconPanelLeftOutline16', 'IconPaperclipOutline16', 'IconPersonalizationOutline16',
|
|
1367
1448
|
'IconPlayOutline16', 'IconPauseOutline16', 'IconPlusOutline16', 'IconQuestionOutline14',
|
|
1368
1449
|
'IconQueueOutline14', 'IconRefreshOutline14', 'IconRefreshOutline16', 'IconRightUpOutline14',
|
|
1369
|
-
'IconSearchOutline16', 'IconSendOutline14', '
|
|
1450
|
+
'IconSearchOutline16', 'IconSendOutline14', 'IconSettingsOutline14',
|
|
1370
1451
|
'IconSettingsOutline16', 'IconShareOutline16', 'IconStopFill16', 'IconThinkOutline14',
|
|
1371
1452
|
'IconThinkOutline16', 'IconTrashOutline16', 'IconUserOutline16', 'IconWarningOutline16',
|
|
1372
1453
|
'IconLikeOutline16', 'IconDislikeOutline16', 'IconFollowsystemOutline16',
|
|
1373
1454
|
'IconAlarmClockOutline16', 'IconClockOutline16', 'IconDatabaseOutline16',
|
|
1455
|
+
// Added with dsh 0.1.6-alpha.1 — absent on older hosts, where the picker
|
|
1456
|
+
// filters them out (see ICON_PICKER_CHOICES).
|
|
1457
|
+
'IconPaperPlaneOutline14', 'IconShieldOutline16', 'IconPlanOutline14',
|
|
1458
|
+
'IconWrapLinesOutline16', 'IconCompactOutline16',
|
|
1374
1459
|
]
|
|
1375
1460
|
|
|
1461
|
+
/**
|
|
1462
|
+
* What the picker actually offers on THIS host: every legacy slot, plus the
|
|
1463
|
+
* primitives this page's icon module really exports, deduped by resolved
|
|
1464
|
+
* name so a retired alias can never double a cell. Computed once — the icon
|
|
1465
|
+
* module is fixed for the lifetime of the page.
|
|
1466
|
+
*/
|
|
1467
|
+
const ICON_PICKER_CHOICES = (() => {
|
|
1468
|
+
const seen = new Set()
|
|
1469
|
+
return ICON_CHOICES.filter((mode) => {
|
|
1470
|
+
if (mode === 'solid' || mode === 'outline' || mode === 'none') return true
|
|
1471
|
+
const name = resolveIconName(mode)
|
|
1472
|
+
if (name === '' || seen.has(name)) return false
|
|
1473
|
+
seen.add(name)
|
|
1474
|
+
return true
|
|
1475
|
+
})
|
|
1476
|
+
})()
|
|
1477
|
+
|
|
1376
1478
|
/**
|
|
1377
1479
|
* Appearance controls shared by the per-row dialog and the settings card's
|
|
1378
1480
|
* "default appearance" section: color, glow, weight, shadow, the text
|
|
1379
|
-
* outline (on/off + width) and — where the row actually renders
|
|
1380
|
-
* folder glyph.
|
|
1381
|
-
*
|
|
1382
|
-
*
|
|
1481
|
+
* outline (on/off + width + color) and — where the row actually renders
|
|
1482
|
+
* one — the folder glyph. Since v0.10.1 the outline color is a normal
|
|
1483
|
+
* choice (presets + picker, gray by default); "auto" is the mode that
|
|
1484
|
+
* derives the contrasting pole from the effective font color, so a palette
|
|
1485
|
+
* flip keeps working without a re-render.
|
|
1383
1486
|
*/
|
|
1384
1487
|
function AppearanceControls({ value, onChange, allowIcon, t }) {
|
|
1385
1488
|
const appearance = readAppearance(value)
|
|
@@ -1389,7 +1492,11 @@ window.__ModuleLoader__.load({
|
|
|
1389
1492
|
const shadow = appearance.shadow
|
|
1390
1493
|
const stroke = appearance.stroke
|
|
1391
1494
|
const strokeWidth = appearance.strokeWidth
|
|
1495
|
+
const strokeColor = appearance.strokeColor
|
|
1392
1496
|
const iconMode = value && value.icon ? value.icon : 'solid'
|
|
1497
|
+
// The glyph this choice actually resolves to on THIS host: a value saved
|
|
1498
|
+
// before the host renamed its icon set still highlights a real cell.
|
|
1499
|
+
const activeGlyph = resolveIconName(iconMode)
|
|
1393
1500
|
const patch = (next) => { if (typeof onChange === 'function') onChange(next) }
|
|
1394
1501
|
const channels = colorToRgb(color)
|
|
1395
1502
|
const setChannel = (index, raw) => {
|
|
@@ -1507,15 +1614,44 @@ window.__ModuleLoader__.load({
|
|
|
1507
1614
|
}),
|
|
1508
1615
|
E('span', { className: 'bw-glow-value' }, strokeWidth + 'px'),
|
|
1509
1616
|
),
|
|
1617
|
+
),
|
|
1618
|
+
E('div', { className: 'bw-field' },
|
|
1619
|
+
t('custom.strokeColor'),
|
|
1620
|
+
E('div', { className: 'bw-dialog-input-row' },
|
|
1621
|
+
E('button', {
|
|
1622
|
+
type: 'button',
|
|
1623
|
+
className: cls('bw-swatch', 'bw-swatch-wide', strokeColor === STROKE_AUTO && 'bw-swatch-active'),
|
|
1624
|
+
title: t('custom.strokeColor.auto'),
|
|
1625
|
+
onClick: () => patch({ strokeColor: STROKE_AUTO }),
|
|
1626
|
+
}, t('custom.strokeColor.auto')),
|
|
1627
|
+
STROKE_PRESETS.map((value) => E('button', {
|
|
1628
|
+
key: value,
|
|
1629
|
+
type: 'button',
|
|
1630
|
+
className: cls('bw-swatch', strokeColor === value && 'bw-swatch-active'),
|
|
1631
|
+
style: { background: value },
|
|
1632
|
+
'aria-label': value,
|
|
1633
|
+
title: value,
|
|
1634
|
+
onClick: () => patch({ strokeColor: value }),
|
|
1635
|
+
})),
|
|
1636
|
+
E('input', {
|
|
1637
|
+
type: 'color',
|
|
1638
|
+
className: 'bw-color-input',
|
|
1639
|
+
value: colorToRgb(strokeColor) ? strokeColor : DEFAULT_APPEARANCE.strokeColor,
|
|
1640
|
+
onChange: (e) => patch({ strokeColor: e.target.value }),
|
|
1641
|
+
}),
|
|
1642
|
+
),
|
|
1510
1643
|
E('div', { className: 'bw-hint' }, t('custom.stroke.hint')),
|
|
1511
1644
|
),
|
|
1512
1645
|
allowIcon ? E('div', { className: 'bw-field' },
|
|
1513
1646
|
t('custom.icon'),
|
|
1514
1647
|
E('div', { className: 'bw-icon-grid' },
|
|
1515
|
-
|
|
1648
|
+
ICON_PICKER_CHOICES.map((mode) => E('button', {
|
|
1516
1649
|
key: mode,
|
|
1517
1650
|
type: 'button',
|
|
1518
|
-
|
|
1651
|
+
// A value saved before an alias flip still highlights the cell it
|
|
1652
|
+
// now resolves to, instead of leaving the grid with no selection.
|
|
1653
|
+
className: cls('bw-icon-cell',
|
|
1654
|
+
(iconMode === mode || (activeGlyph !== '' && resolveIconName(mode) === activeGlyph)) && 'bw-icon-cell-active'),
|
|
1519
1655
|
title: mode === 'solid' ? t('custom.icon.solid') : (mode === 'outline' ? t('custom.icon.outline') : mode),
|
|
1520
1656
|
'aria-label': mode === 'solid' ? t('custom.icon.solid') : (mode === 'outline' ? t('custom.icon.outline') : mode),
|
|
1521
1657
|
onClick: () => patch({ icon: mode }),
|
|
@@ -1761,6 +1897,7 @@ window.__ModuleLoader__.load({
|
|
|
1761
1897
|
const sessionGroupsMap = useStore ? (useStore(s => s.sessionGroups) || {}) : {}
|
|
1762
1898
|
const prefsMap = useStore ? (useStore(s => s.prefs) || {}) : {}
|
|
1763
1899
|
const stylingMap = useStore ? (useStore(s => s.styling) || {}) : {}
|
|
1900
|
+
const sessionOrderMap = useStore ? (useStore(s => s.sessionOrder) || {}) : {}
|
|
1764
1901
|
// Dual-write wrappers: every preference mutation lands in the local
|
|
1765
1902
|
// store (immediate echo + scope-less fallback) AND the host settings
|
|
1766
1903
|
// store (durable cross-device copy for the manual pull on the other
|
|
@@ -2013,6 +2150,13 @@ window.__ModuleLoader__.load({
|
|
|
2013
2150
|
return { ...built, folders: built.folders.map((f) => materializeChain(compressTree(f))), workspaces: built.workspaces }
|
|
2014
2151
|
}, [items, storeFolders, compactChains, draggingWorkspace])
|
|
2015
2152
|
|
|
2153
|
+
/** Browser-local flat session order for one workspace (fallback channel). */
|
|
2154
|
+
const sessionOrderOf = (workspaceId) => {
|
|
2155
|
+
const map = sessionOrderMap && typeof sessionOrderMap === 'object' ? sessionOrderMap : {}
|
|
2156
|
+
const list = map[workspaceId]
|
|
2157
|
+
return Array.isArray(list) ? list : []
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2016
2160
|
const sessionsOf = (workspace) => {
|
|
2017
2161
|
const rows = []
|
|
2018
2162
|
for (const id of workspace.sessionIds || []) {
|
|
@@ -2031,7 +2175,21 @@ window.__ModuleLoader__.load({
|
|
|
2031
2175
|
pending: pendingKindOf(pending, id),
|
|
2032
2176
|
})
|
|
2033
2177
|
}
|
|
2034
|
-
|
|
2178
|
+
// Browser-local reorder fallback (dsh 0.1.6-alpha.1 stopped injecting
|
|
2179
|
+
// insertSessionBefore): the local flat order wins for the workspaces the
|
|
2180
|
+
// user dragged in, and is empty everywhere else — so a host that still
|
|
2181
|
+
// exposes the action keeps its authoritative order untouched.
|
|
2182
|
+
// Host order stays authoritative wherever the action exists.
|
|
2183
|
+
const local = typeof insertSessionBefore === 'function' ? [] : sessionOrderOf(workspace.workspaceId)
|
|
2184
|
+
if (local.length === 0) return rows
|
|
2185
|
+
const remaining = new Map(rows.map((row) => [row.id, row]))
|
|
2186
|
+
const out = []
|
|
2187
|
+
for (const id of local) {
|
|
2188
|
+
const row = remaining.get(id)
|
|
2189
|
+
if (row) { out.push(row); remaining.delete(id) }
|
|
2190
|
+
}
|
|
2191
|
+
for (const row of rows) if (remaining.has(row.id)) out.push(row)
|
|
2192
|
+
return out
|
|
2035
2193
|
}
|
|
2036
2194
|
|
|
2037
2195
|
const searchAgent = (agent) => {
|
|
@@ -2137,7 +2295,11 @@ window.__ModuleLoader__.load({
|
|
|
2137
2295
|
const canDragWorkspace = typeof insertWorkspaceBefore === 'function'
|
|
2138
2296
|
// Session REORDER needs the host insertSessionBefore action; dragging a
|
|
2139
2297
|
// session onto a sub-group row (a pure rename) stays available without it.
|
|
2140
|
-
|
|
2298
|
+
// Session reorder keeps working either way: the host action is preferred
|
|
2299
|
+
// (authoritative, visible to every surface), while hosts that no longer
|
|
2300
|
+
// inject it — dsh 0.1.6-alpha.1 dropped insertSessionBefore from the
|
|
2301
|
+
// browser contract — fall back to the browser-local flat order.
|
|
2302
|
+
const canReorderSessions = true
|
|
2141
2303
|
|
|
2142
2304
|
/* --------------------- workspace drag & drop -------------------- */
|
|
2143
2305
|
|
|
@@ -2315,9 +2477,16 @@ window.__ModuleLoader__.load({
|
|
|
2315
2477
|
const anchor = half === 'after'
|
|
2316
2478
|
? (index === -1 ? undefined : (flat[index + 1] ? flat[index + 1].id : undefined))
|
|
2317
2479
|
: targetSessionId
|
|
2318
|
-
|
|
2319
|
-
.
|
|
2320
|
-
|
|
2480
|
+
if (typeof insertSessionBefore === 'function') {
|
|
2481
|
+
Promise.resolve()
|
|
2482
|
+
.then(() => (anchor !== undefined ? insertSessionBefore(workspaceId, source.sessionId, anchor) : insertSessionBefore(workspaceId, source.sessionId)))
|
|
2483
|
+
.catch(fail)
|
|
2484
|
+
return
|
|
2485
|
+
}
|
|
2486
|
+
// Host channel gone: commit the same move into the browser-local order.
|
|
2487
|
+
if (actions && typeof actions.setSessionOrder === 'function') {
|
|
2488
|
+
actions.setSessionOrder(workspaceId, reorderIds(flat.map((s) => s.id), source.sessionId, anchor))
|
|
2489
|
+
}
|
|
2321
2490
|
}
|
|
2322
2491
|
const commitSessionMoveInto = (workspaceId, groupPath) => {
|
|
2323
2492
|
const source = drag.source
|