dsh-better-workspace 0.11.3 → 0.11.4

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.3",
3
+ "version": "0.11.4",
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.3",
3
+ "version": "0.11.4",
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
@@ -127,6 +127,9 @@ window.__ModuleLoader__.load({
127
127
  'browse.showHidden': '显示隐藏文件',
128
128
  'browse.editPath': '编辑路径',
129
129
  'browse.select': '选择此文件夹',
130
+ 'browse.enter': '进入',
131
+ 'browse.drives': '盘符',
132
+ 'browse.selectNamed': '选择「{name}」',
130
133
  'error.title': '出错了',
131
134
  'cancel': '取消',
132
135
  'create': '创建',
@@ -246,6 +249,9 @@ window.__ModuleLoader__.load({
246
249
  'browse.showHidden': 'Show hidden files',
247
250
  'browse.editPath': 'Edit path',
248
251
  'browse.select': 'Use this folder',
252
+ 'browse.enter': 'Open',
253
+ 'browse.drives': 'Drives',
254
+ 'browse.selectNamed': 'Use "{name}"',
249
255
  'error.title': 'Something went wrong',
250
256
  'cancel': 'Cancel',
251
257
  'create': 'Create',
@@ -373,6 +379,9 @@ window.__ModuleLoader__.load({
373
379
  'browse.showHidden': 'إظهار الملفات المخفية',
374
380
  'browse.editPath': 'تعديل المسار',
375
381
  'browse.select': 'استخدام هذا المجلد',
382
+ 'browse.enter': 'فتح',
383
+ 'browse.drives': 'الأقراص',
384
+ 'browse.selectNamed': 'استخدام "{name}"',
376
385
  'error.title': 'حدث خطأ ما',
377
386
  'cancel': 'إلغاء',
378
387
  'create': 'إنشاء',
@@ -492,6 +501,9 @@ window.__ModuleLoader__.load({
492
501
  'browse.showHidden': 'Versteckte Dateien anzeigen',
493
502
  'browse.editPath': 'Pfad bearbeiten',
494
503
  'browse.select': 'Diesen Ordner verwenden',
504
+ 'browse.enter': 'Öffnen',
505
+ 'browse.drives': 'Laufwerke',
506
+ 'browse.selectNamed': '"{name}" verwenden',
495
507
  'error.title': 'Etwas ist schiefgelaufen',
496
508
  'cancel': 'Abbrechen',
497
509
  'create': 'Erstellen',
@@ -611,6 +623,9 @@ window.__ModuleLoader__.load({
611
623
  'browse.showHidden': 'Afficher les fichiers cachés',
612
624
  'browse.editPath': 'Modifier le chemin',
613
625
  'browse.select': 'Utiliser ce dossier',
626
+ 'browse.enter': 'Ouvrir',
627
+ 'browse.drives': 'Lecteurs',
628
+ 'browse.selectNamed': 'Utiliser « {name} »',
614
629
  'error.title': 'Une erreur est survenue',
615
630
  'cancel': 'Annuler',
616
631
  'create': 'Créer',
@@ -730,6 +745,9 @@ window.__ModuleLoader__.load({
730
745
  'browse.showHidden': 'छिपी फ़ाइलें दिखाएँ',
731
746
  'browse.editPath': 'पथ संपादित करें',
732
747
  'browse.select': 'यह फ़ोल्डर चुनें',
748
+ 'browse.enter': 'खोलें',
749
+ 'browse.drives': 'ड्राइव',
750
+ 'browse.selectNamed': '"{name}" चुनें',
733
751
  'error.title': 'कुछ गड़बड़ हो गई',
734
752
  'cancel': 'रद्द करें',
735
753
  'create': 'बनाएँ',
@@ -849,6 +867,9 @@ window.__ModuleLoader__.load({
849
867
  'browse.showHidden': 'Tampilkan file tersembunyi',
850
868
  'browse.editPath': 'Edit jalur',
851
869
  'browse.select': 'Gunakan folder ini',
870
+ 'browse.enter': 'Buka',
871
+ 'browse.drives': 'Drive',
872
+ 'browse.selectNamed': 'Gunakan "{name}"',
852
873
  'error.title': 'Terjadi kesalahan',
853
874
  'cancel': 'Batal',
854
875
  'create': 'Buat',
@@ -968,6 +989,9 @@ window.__ModuleLoader__.load({
968
989
  'browse.showHidden': 'Mostra file nascosti',
969
990
  'browse.editPath': 'Modifica percorso',
970
991
  'browse.select': 'Usa questa cartella',
992
+ 'browse.enter': 'Apri',
993
+ 'browse.drives': 'Unità',
994
+ 'browse.selectNamed': 'Usa "{name}"',
971
995
  'error.title': 'Si è verificato un errore',
972
996
  'cancel': 'Annulla',
973
997
  'create': 'Crea',
@@ -1087,6 +1111,9 @@ window.__ModuleLoader__.load({
1087
1111
  'browse.showHidden': '隠しファイルを表示',
1088
1112
  'browse.editPath': 'パスを編集',
1089
1113
  'browse.select': 'このフォルダーを選択',
1114
+ 'browse.enter': '開く',
1115
+ 'browse.drives': 'ドライブ',
1116
+ 'browse.selectNamed': '「{name}」を選択',
1090
1117
  'error.title': 'エラーが発生しました',
1091
1118
  'cancel': 'キャンセル',
1092
1119
  'create': '作成',
@@ -1206,6 +1233,9 @@ window.__ModuleLoader__.load({
1206
1233
  'browse.showHidden': '숨김 파일 표시',
1207
1234
  'browse.editPath': '경로 편집',
1208
1235
  'browse.select': '이 폴더 선택',
1236
+ 'browse.enter': '열기',
1237
+ 'browse.drives': '드라이브',
1238
+ 'browse.selectNamed': '"{name}" 선택',
1209
1239
  'error.title': '문제가 발생했습니다',
1210
1240
  'cancel': '취소',
1211
1241
  'create': '만들기',
@@ -1325,6 +1355,9 @@ window.__ModuleLoader__.load({
1325
1355
  'browse.showHidden': 'Verborgen bestanden tonen',
1326
1356
  'browse.editPath': 'Pad bewerken',
1327
1357
  'browse.select': 'Deze map gebruiken',
1358
+ 'browse.enter': 'Openen',
1359
+ 'browse.drives': 'Stations',
1360
+ 'browse.selectNamed': '"{name}" gebruiken',
1328
1361
  'error.title': 'Er is iets misgegaan',
1329
1362
  'cancel': 'Annuleren',
1330
1363
  'create': 'Aanmaken',
@@ -1444,6 +1477,9 @@ window.__ModuleLoader__.load({
1444
1477
  'browse.showHidden': 'Pokaż ukryte pliki',
1445
1478
  'browse.editPath': 'Edytuj ścieżkę',
1446
1479
  'browse.select': 'Użyj tego folderu',
1480
+ 'browse.enter': 'Otwórz',
1481
+ 'browse.drives': 'Dyski',
1482
+ 'browse.selectNamed': 'Użyj "{name}"',
1447
1483
  'error.title': 'Coś poszło nie tak',
1448
1484
  'cancel': 'Anuluj',
1449
1485
  'create': 'Utwórz',
@@ -1563,6 +1599,9 @@ window.__ModuleLoader__.load({
1563
1599
  'browse.showHidden': 'Mostrar arquivos ocultos',
1564
1600
  'browse.editPath': 'Editar caminho',
1565
1601
  'browse.select': 'Usar esta pasta',
1602
+ 'browse.enter': 'Abrir',
1603
+ 'browse.drives': 'Unidades',
1604
+ 'browse.selectNamed': 'Usar "{name}"',
1566
1605
  'error.title': 'Algo deu errado',
1567
1606
  'cancel': 'Cancelar',
1568
1607
  'create': 'Criar',
@@ -1682,6 +1721,9 @@ window.__ModuleLoader__.load({
1682
1721
  'browse.showHidden': 'Показывать скрытые файлы',
1683
1722
  'browse.editPath': 'Изменить путь',
1684
1723
  'browse.select': 'Выбрать эту папку',
1724
+ 'browse.enter': 'Открыть',
1725
+ 'browse.drives': 'Диски',
1726
+ 'browse.selectNamed': 'Выбрать «{name}»',
1685
1727
  'error.title': 'Что-то пошло не так',
1686
1728
  'cancel': 'Отмена',
1687
1729
  'create': 'Создать',
@@ -1801,6 +1843,9 @@ window.__ModuleLoader__.load({
1801
1843
  'browse.showHidden': 'Visa dolda filer',
1802
1844
  'browse.editPath': 'Redigera sökväg',
1803
1845
  'browse.select': 'Använd den här mappen',
1846
+ 'browse.enter': 'Öppna',
1847
+ 'browse.drives': 'Enheter',
1848
+ 'browse.selectNamed': 'Använd "{name}"',
1804
1849
  'error.title': 'Något gick fel',
1805
1850
  'cancel': 'Avbryt',
1806
1851
  'create': 'Skapa',
@@ -1920,6 +1965,9 @@ window.__ModuleLoader__.load({
1920
1965
  'browse.showHidden': 'แสดงไฟล์ที่ซ่อนอยู่',
1921
1966
  'browse.editPath': 'แก้ไขเส้นทาง',
1922
1967
  'browse.select': 'ใช้โฟลเดอร์นี้',
1968
+ 'browse.enter': 'เปิด',
1969
+ 'browse.drives': 'ไดรฟ์',
1970
+ 'browse.selectNamed': 'ใช้ "{name}"',
1923
1971
  'error.title': 'เกิดข้อผิดพลาด',
1924
1972
  'cancel': 'ยกเลิก',
1925
1973
  'create': 'สร้าง',
@@ -2039,6 +2087,9 @@ window.__ModuleLoader__.load({
2039
2087
  'browse.showHidden': 'Gizli dosyaları göster',
2040
2088
  'browse.editPath': 'Yolu düzenle',
2041
2089
  'browse.select': 'Bu klasörü kullan',
2090
+ 'browse.enter': 'Aç',
2091
+ 'browse.drives': 'Sürücüler',
2092
+ 'browse.selectNamed': '"{name}" kullan',
2042
2093
  'error.title': 'Bir şeyler ters gitti',
2043
2094
  'cancel': 'İptal',
2044
2095
  'create': 'Oluştur',
@@ -2158,6 +2209,9 @@ window.__ModuleLoader__.load({
2158
2209
  'browse.showHidden': 'Hiện tệp ẩn',
2159
2210
  'browse.editPath': 'Sửa đường dẫn',
2160
2211
  'browse.select': 'Dùng thư mục này',
2212
+ 'browse.enter': 'Mở',
2213
+ 'browse.drives': 'Ổ đĩa',
2214
+ 'browse.selectNamed': 'Dùng "{name}"',
2161
2215
  'error.title': 'Đã xảy ra lỗi',
2162
2216
  'cancel': 'Hủy',
2163
2217
  'create': 'Tạo',
@@ -2277,6 +2331,9 @@ window.__ModuleLoader__.load({
2277
2331
  'browse.showHidden': '顯示隱藏檔案',
2278
2332
  'browse.editPath': '編輯路徑',
2279
2333
  'browse.select': '揀呢個資料夾',
2334
+ 'browse.enter': '入去',
2335
+ 'browse.drives': '磁碟',
2336
+ 'browse.selectNamed': '揀「{name}」',
2280
2337
  'error.title': '出咗錯',
2281
2338
  'cancel': '取消',
2282
2339
  'create': '建立',
@@ -2396,6 +2453,9 @@ window.__ModuleLoader__.load({
2396
2453
  'browse.showHidden': '顯示隱藏檔案',
2397
2454
  'browse.editPath': '編輯路徑',
2398
2455
  'browse.select': '揀呢個資料夾',
2456
+ 'browse.enter': '入去',
2457
+ 'browse.drives': '磁碟',
2458
+ 'browse.selectNamed': '揀「{name}」',
2399
2459
  'error.title': '出咗錯',
2400
2460
  'cancel': '取消',
2401
2461
  'create': '建立',
@@ -2515,6 +2575,9 @@ window.__ModuleLoader__.load({
2515
2575
  'browse.showHidden': '顯示隱藏檔案',
2516
2576
  'browse.editPath': '編輯路徑',
2517
2577
  'browse.select': '選擇此資料夾',
2578
+ 'browse.enter': '進入',
2579
+ 'browse.drives': '磁碟',
2580
+ 'browse.selectNamed': '選擇「{name}」',
2518
2581
  'error.title': '發生錯誤',
2519
2582
  'cancel': '取消',
2520
2583
  'create': '建立',
@@ -3048,10 +3111,17 @@ window.__ModuleLoader__.load({
3048
3111
  '.bw-browse-edit svg{width:14px;height:14px}',
3049
3112
  '.bw-browse-edit-row{display:flex;gap:6px;align-items:center}',
3050
3113
  '.bw-browse-list{display:flex;flex-direction:column;gap:1px;height:240px;overflow:auto;padding:4px;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.22));border-radius:8px;background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.06))}',
3051
- '.bw-browse-row{display:flex;align-items:center;gap:8px;width:100%;flex:none;text-align:left;border:none;background:transparent;color:var(--dsw-alias-label-primary,#e6e6e6);font:inherit;font-size:12.5px;padding:5px 8px;border-radius:6px;cursor:pointer}',
3114
+ '.bw-browse-row{display:flex;align-items:center;gap:8px;width:100%;flex:none;text-align:left;border:none;background:transparent;color:var(--dsw-alias-label-primary,#e6e6e6);font:inherit;font-size:12.5px;padding:5px 8px;border-radius:6px;cursor:pointer;user-select:none}',
3052
3115
  '.bw-browse-row:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}',
3053
3116
  '.bw-browse-row:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5b8def);outline-offset:-2px}',
3054
3117
  '.bw-browse-row svg{width:15px;height:15px;flex:none;color:var(--dsw-alias-label-secondary,#b8b8b8)}',
3118
+ '.bw-browse-row-on{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));box-shadow:inset 0 0 0 1px var(--dsw-alias-brand-primary,#5b8def)}',
3119
+ '.bw-browse-open{flex:none;width:22px;height:22px;padding:0;border:none;border-radius:5px;background:transparent;color:var(--dsw-alias-label-tertiary,#9a9a9a);font:inherit;font-size:15px;line-height:1;cursor:pointer}',
3120
+ '.bw-browse-open:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.16));color:var(--dsw-alias-label-primary,#e6e6e6)}',
3121
+ '.bw-browse-drives{display:flex;align-items:center;gap:4px;margin-left:auto}',
3122
+ '.bw-browse-drive{min-width:30px;padding:3px 6px;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.25));border-radius:5px;background:transparent;color:var(--dsw-alias-label-secondary,#b8b8b8);font:inherit;font-size:11.5px;cursor:pointer}',
3123
+ '.bw-browse-drive:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary,#e6e6e6)}',
3124
+ '.bw-browse-drive-on{border-color:var(--dsw-alias-brand-primary,#5b8def);color:var(--dsw-alias-label-primary,#e6e6e6)}',
3055
3125
  '.bw-browse-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
3056
3126
  '.bw-browse-note{font-size:12px;color:var(--dsw-alias-label-tertiary,#9a9a9a);padding:10px 8px;text-align:center}',
3057
3127
  '.bw-browse-new-row{display:flex;gap:6px;align-items:center}',
@@ -3439,6 +3509,14 @@ window.__ModuleLoader__.load({
3439
3509
  * owner's error surface belongs to the pick/create conversation, and a
3440
3510
  * denied listing must never close the flow.
3441
3511
  */
3512
+ /**
3513
+ * Page-lifetime cache of the Windows drive letters this Host actually
3514
+ * serves. The browse API can only list a path, so the first open probes the
3515
+ * usual letters once (six cheap listings, a missing drive fails silently)
3516
+ * and every later open renders the chips straight from here.
3517
+ */
3518
+ const browseDrives = { probed: false, probing: false, list: [] }
3519
+
3442
3520
  function DirectoryBrowseDialog(props) {
3443
3521
  const { open, busy, listDirectory, createDirectory, onPick, onClose, t } = props
3444
3522
  const [listing, setListing] = React.useState(null)
@@ -3451,14 +3529,18 @@ window.__ModuleLoader__.load({
3451
3529
  const [createName, setCreateName] = React.useState('')
3452
3530
  const [createBusy, setCreateBusy] = React.useState(false)
3453
3531
  const [createError, setCreateError] = React.useState('')
3532
+ const [selected, setSelected] = React.useState('')
3533
+ const [drives, setDrives] = React.useState(browseDrives.list)
3454
3534
  // Guards a superseded scan: a slow level must not overwrite a newer one.
3455
3535
  const seqRef = React.useRef(0)
3536
+ const crumbsRef = React.useRef(null)
3456
3537
 
3457
- const go = (path) => {
3538
+ const go = (path, keepSelected) => {
3458
3539
  const seq = seqRef.current + 1
3459
3540
  seqRef.current = seq
3460
3541
  setLoading(true)
3461
3542
  setError('')
3543
+ if (keepSelected !== true) setSelected('')
3462
3544
  Promise.resolve()
3463
3545
  .then(() => listDirectory(path, undefined))
3464
3546
  .then((next) => {
@@ -3484,27 +3566,59 @@ window.__ModuleLoader__.load({
3484
3566
  setCreateBusy(false)
3485
3567
  setCreateError('')
3486
3568
  setShowHidden(false)
3569
+ setSelected('')
3487
3570
  go(undefined)
3488
3571
  return () => { seqRef.current += 1 }
3489
3572
  }, [open])
3490
3573
 
3574
+ const currentPath = listing && typeof listing.path === 'string' ? listing.path : ''
3575
+ const homePath = listing && typeof listing.home === 'string' ? listing.home : ''
3576
+ const windowsHost = homePath.indexOf('\\') !== -1
3577
+ const hasListing = listing !== null
3578
+
3579
+ // Windows drives are separate filesystem roots and the browse API can only
3580
+ // list a path, so probe the usual letters once per page (cached above) and
3581
+ // offer the ones that answered. A drive nobody probed is still reachable
3582
+ // through the path editor, which is the only general answer anyway.
3583
+ React.useEffect(() => {
3584
+ if (!open || !hasListing || !windowsHost) return undefined
3585
+ if (browseDrives.probed || browseDrives.probing) {
3586
+ if (browseDrives.list.length > 0) setDrives(browseDrives.list)
3587
+ return undefined
3588
+ }
3589
+ browseDrives.probing = true
3590
+ let alive = true
3591
+ const letters = ['C:', 'D:', 'E:', 'F:', 'G:', 'H:']
3592
+ Promise.all(letters.map((letter) => Promise.resolve()
3593
+ .then(() => listDirectory(letter + '\\', undefined))
3594
+ .then(() => letter, () => null)))
3595
+ .then((found) => {
3596
+ browseDrives.list = found.filter(Boolean)
3597
+ browseDrives.probed = true
3598
+ browseDrives.probing = false
3599
+ if (alive) setDrives(browseDrives.list)
3600
+ }, () => { browseDrives.probing = false })
3601
+ return () => { alive = false }
3602
+ }, [open, hasListing, windowsHost])
3603
+
3604
+ // A deep path must show its TAIL — that is where you are — so the chain
3605
+ // keeps its right edge in view instead of cutting the last crumb off.
3606
+ React.useEffect(() => {
3607
+ const node = crumbsRef.current
3608
+ if (node) node.scrollLeft = node.scrollWidth
3609
+ }, [listing])
3610
+
3491
3611
  if (!open) return null
3492
3612
 
3493
- // Home-rooted chain: inside the host account's home subtree the display
3494
- // starts at a localized Home crumb, outside it the full ancestry shows.
3495
- const crumbs = (() => {
3496
- const all = listing && Array.isArray(listing.crumbs) ? listing.crumbs : []
3497
- const home = listing && typeof listing.home === 'string' ? listing.home : ''
3498
- const at = home === '' ? -1 : all.findIndex((crumb) => crumb.path === home)
3499
- const tail = at === -1 ? all : all.slice(at)
3500
- return tail.map((crumb, index) => (index === 0 && at !== -1
3501
- ? { path: crumb.path, name: t('browse.home') }
3502
- : crumb))
3503
- })()
3504
- const currentPath = listing && typeof listing.path === 'string' ? listing.path : ''
3613
+ // The whole ancestry from the filesystem ROOT: rooting the chain at Home
3614
+ // (the official dialog's choice) hides where the listed level actually is,
3615
+ // and the drive crumb answers "which disk am I on" at a glance.
3616
+ const crumbs = listing && Array.isArray(listing.crumbs) ? listing.crumbs : []
3505
3617
  const parentPath = crumbs.length > 1 ? crumbs[crumbs.length - 2].path : ''
3506
3618
  const entries = (listing && Array.isArray(listing.entries) ? listing.entries : [])
3507
3619
  .filter((entry) => showHidden || !entry.hidden)
3620
+ const selectedEntry = entries.find((entry) => entry.path === selected) || null
3621
+ const targetPath = selectedEntry ? selectedEntry.path : currentPath
3508
3622
 
3509
3623
  const submitDraft = () => {
3510
3624
  const path = draft.trim()
@@ -3518,12 +3632,14 @@ window.__ModuleLoader__.load({
3518
3632
  setCreateBusy(true)
3519
3633
  setCreateError('')
3520
3634
  Promise.resolve()
3521
- .then(() => createDirectory(currentPath, name))
3522
- .then(() => {
3635
+ .then((created) => {
3523
3636
  setCreateBusy(false)
3524
3637
  setCreating(false)
3525
3638
  setCreateName('')
3526
- go(currentPath)
3639
+ // Land on the folder just made: it is almost always the one the
3640
+ // operator wants to adopt next.
3641
+ if (typeof created === 'string' && created !== '') setSelected(created)
3642
+ go(currentPath, true)
3527
3643
  })
3528
3644
  .catch((reason) => {
3529
3645
  setCreateBusy(false)
@@ -3556,7 +3672,7 @@ window.__ModuleLoader__.load({
3556
3672
  disabled: parentPath === '',
3557
3673
  onClick: () => { if (parentPath !== '') go(parentPath) },
3558
3674
  }, '↑'),
3559
- E('div', { className: 'bw-browse-crumbs' },
3675
+ E('div', { className: 'bw-browse-crumbs', ref: crumbsRef },
3560
3676
  crumbs.map((crumb, index) => (index === crumbs.length - 1
3561
3677
  ? E('span', { key: crumb.path, className: 'bw-browse-crumb bw-browse-current', title: crumb.path }, crumb.name)
3562
3678
  : E('button', {
@@ -3576,13 +3692,32 @@ window.__ModuleLoader__.load({
3576
3692
  }, icon('IconEditOutline16')),
3577
3693
  )
3578
3694
 
3579
- const rows = entries.map((entry) => E('button', {
3695
+ // One click SELECTS a row (the footer button then adopts it — no need to
3696
+ // step inside just to pick a folder), a double click or the row's chevron
3697
+ // enters it. Touch needs the chevron: a double tap is not a gesture.
3698
+ const rows = entries.map((entry) => E('div', {
3580
3699
  key: entry.path,
3581
- type: 'button',
3582
- className: 'bw-browse-row',
3700
+ className: cls('bw-browse-row', selected === entry.path && 'bw-browse-row-on'),
3583
3701
  title: entry.path,
3584
- onClick: () => go(entry.path),
3585
- }, icon('IconFolderClose16'), E('span', { className: 'bw-browse-name' }, entry.name)))
3702
+ role: 'button',
3703
+ tabIndex: 0,
3704
+ onClick: () => setSelected((current) => (current === entry.path ? '' : entry.path)),
3705
+ onDoubleClick: () => go(entry.path),
3706
+ onKeyDown: (event) => {
3707
+ if (event.key === 'Enter') go(entry.path)
3708
+ else if (event.key === ' ') { event.preventDefault(); setSelected((current) => (current === entry.path ? '' : entry.path)) }
3709
+ },
3710
+ },
3711
+ icon('IconFolderClose16'),
3712
+ E('span', { className: 'bw-browse-name' }, entry.name),
3713
+ E('button', {
3714
+ type: 'button',
3715
+ className: 'bw-browse-open',
3716
+ title: t('browse.enter'),
3717
+ 'aria-label': t('browse.enter'),
3718
+ onClick: (event) => { event.stopPropagation(); go(entry.path) },
3719
+ }, '›'),
3720
+ ))
3586
3721
 
3587
3722
  const body = loading
3588
3723
  ? E('div', { className: 'bw-browse-note' }, t('browse.loading'))
@@ -3619,6 +3754,12 @@ window.__ModuleLoader__.load({
3619
3754
  // two browsing tools sit on their own row above the list instead of
3620
3755
  // sharing the footer with the commit pair (they used to wrap there).
3621
3756
  const tools = E('div', { className: 'bw-browse-tools' },
3757
+ E('button', {
3758
+ type: 'button',
3759
+ className: cls('bw-browse-tool', currentPath !== '' && currentPath === homePath && 'bw-browse-tool-on'),
3760
+ disabled: homePath === '' || currentPath === homePath,
3761
+ onClick: () => { if (homePath !== '') go(homePath) },
3762
+ }, t('browse.home')),
3622
3763
  E('button', {
3623
3764
  type: 'button',
3624
3765
  className: cls('bw-browse-tool', showHidden && 'bw-browse-tool-on'),
@@ -3631,15 +3772,29 @@ window.__ModuleLoader__.load({
3631
3772
  disabled: creating || currentPath === '',
3632
3773
  onClick: () => { setCreating(true); setCreateName(''); setCreateError('') },
3633
3774
  }, t('browse.newFolder')),
3775
+ drives.length > 1
3776
+ ? E('div', {
3777
+ className: 'bw-browse-drives',
3778
+ title: t('browse.drives'),
3779
+ role: 'group',
3780
+ 'aria-label': t('browse.drives'),
3781
+ }, drives.map((drive) => E('button', {
3782
+ key: drive,
3783
+ type: 'button',
3784
+ className: cls('bw-browse-drive', currentPath.slice(0, 2).toUpperCase() === drive && 'bw-browse-drive-on'),
3785
+ title: drive + '\\',
3786
+ onClick: () => go(drive + '\\'),
3787
+ }, drive)))
3788
+ : null,
3634
3789
  )
3635
3790
 
3636
3791
  const footer = E('div', { className: 'bw-browse-actions' },
3637
3792
  E(BTN, { variant: 'outline', onClick: onClose, disabled: busy === true }, t('cancel')),
3638
3793
  E(BTN, {
3639
3794
  variant: 'primary',
3640
- disabled: busy === true || currentPath === '',
3641
- onClick: () => { if (currentPath !== '') onPick(currentPath) },
3642
- }, t('browse.select')),
3795
+ disabled: busy === true || targetPath === '',
3796
+ onClick: () => { if (targetPath !== '') onPick(targetPath) },
3797
+ }, selectedEntry !== null ? t('browse.selectNamed', { name: selectedEntry.name }) : t('browse.select')),
3643
3798
  )
3644
3799
 
3645
3800
  return E(ui.Modal, {