@aryacode/dsh-aryacode-polish 0.13.1 → 0.14.0
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/CHANGELOG.md +11 -0
- package/client/workspace.js +201 -2
- package/docs/better-workspace.md +18 -0
- package/lib/client.js +202 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.14.0] - 2026-10-28
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Quick switcher.** Ctrl/⌘-K (or Cmd-K) opens a palette over everything: every session
|
|
8
|
+
in every workspace, ranked so a prefix beats a mention, arrows to walk, Enter to open,
|
|
9
|
+
Escape to dismiss. Rows show the session title and the workspace it lives in.
|
|
10
|
+
- **Folded state persists.** Which workspaces and which folders are collapsed is kept
|
|
11
|
+
under `aryacode-polish:folded.v1`, so reopening the app looks like it did on the way
|
|
12
|
+
out. A storage that refuses costs the memory and nothing else.
|
|
13
|
+
|
|
3
14
|
## [0.13.1] - 2026-10-28
|
|
4
15
|
|
|
5
16
|
### Added
|
package/client/workspace.js
CHANGED
|
@@ -384,6 +384,121 @@ export function moveBefore(list, movingId, beforeId) {
|
|
|
384
384
|
|
|
385
385
|
const LOOK_KEY = 'aryacode-polish:workspace-appearance.v1'
|
|
386
386
|
const ORDER_KEY = 'aryacode-polish:workspace-order.v1'
|
|
387
|
+
const FOLD_KEY = 'aryacode-polish:folded.v1'
|
|
388
|
+
|
|
389
|
+
/** Which workspaces and folders are folded, kept so a restart looks like yesterday. */
|
|
390
|
+
export function readFolded(storage) {
|
|
391
|
+
try {
|
|
392
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
393
|
+
if (!store) return { workspaces: {}, folders: {} }
|
|
394
|
+
const raw = store.getItem(FOLD_KEY)
|
|
395
|
+
if (!raw) return { workspaces: {}, folders: {} }
|
|
396
|
+
const parsed = JSON.parse(raw)
|
|
397
|
+
return {
|
|
398
|
+
workspaces: (parsed && parsed.workspaces) || {},
|
|
399
|
+
folders: (parsed && parsed.folders) || {},
|
|
400
|
+
}
|
|
401
|
+
} catch {
|
|
402
|
+
return { workspaces: {}, folders: {} }
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function writeFolded(state, storage) {
|
|
407
|
+
try {
|
|
408
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
409
|
+
if (!store) return
|
|
410
|
+
const clean = {
|
|
411
|
+
workspaces: (state && state.workspaces) || {},
|
|
412
|
+
folders: (state && state.folders) || {},
|
|
413
|
+
}
|
|
414
|
+
store.setItem(FOLD_KEY, JSON.stringify(clean))
|
|
415
|
+
} catch {
|
|
416
|
+
/* folding is a convenience; a store that refuses must not break the tree */
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Rank the quick switcher's rows. A title that starts with the query wins over one
|
|
422
|
+
* that merely contains it, and equal kinds keep the order they arrived in — so
|
|
423
|
+
* typing "gre" puts Greeting above anything that mentions greeting later on.
|
|
424
|
+
*/
|
|
425
|
+
export function rankMatches(items, query, text = (item) => String(item.title || item.id || '')) {
|
|
426
|
+
const list = Array.isArray(items) ? items : []
|
|
427
|
+
const needle = String(query || '').trim().toLowerCase()
|
|
428
|
+
if (!needle) return list.slice(0, 40)
|
|
429
|
+
const scored = []
|
|
430
|
+
list.forEach((item, order) => {
|
|
431
|
+
const hay = text(item).toLowerCase()
|
|
432
|
+
const at = hay.indexOf(needle)
|
|
433
|
+
if (at === -1) return
|
|
434
|
+
scored.push({ item, rank: at === 0 ? 0 : 1, order })
|
|
435
|
+
})
|
|
436
|
+
scored.sort((a, b) => (a.rank - b.rank) || (a.order - b.order))
|
|
437
|
+
return scored.slice(0, 40).map((entry) => entry.item)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const SWITCHER_BACKDROP = {
|
|
441
|
+
position: 'fixed',
|
|
442
|
+
inset: 0,
|
|
443
|
+
zIndex: 1300,
|
|
444
|
+
display: 'flex',
|
|
445
|
+
alignItems: 'flex-start',
|
|
446
|
+
justifyContent: 'center',
|
|
447
|
+
paddingTop: '12vh',
|
|
448
|
+
background: 'rgba(16, 18, 22, 0.28)',
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const SWITCHER_CARD = {
|
|
452
|
+
width: 'min(560px, calc(100vw - 32px))',
|
|
453
|
+
maxHeight: '60vh',
|
|
454
|
+
overflow: 'hidden',
|
|
455
|
+
display: 'flex',
|
|
456
|
+
flexDirection: 'column',
|
|
457
|
+
borderRadius: 'var(--dsw-radius-lg, 10px)',
|
|
458
|
+
border: '0.5px solid var(--dsw-alias-border-l2, rgba(0,0,0,0.12))',
|
|
459
|
+
background: 'Canvas',
|
|
460
|
+
boxShadow: 'var(--dsw-elevation-prominent, 0 8px 24px rgba(0,0,0,0.18))',
|
|
461
|
+
color: 'var(--dsw-alias-label-primary, #1b1d21)',
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const SWITCHER_INPUT = {
|
|
465
|
+
border: 'none',
|
|
466
|
+
borderBottom: '0.5px solid var(--dsw-alias-border-l2, rgba(0,0,0,0.12))',
|
|
467
|
+
background: 'transparent',
|
|
468
|
+
color: 'inherit',
|
|
469
|
+
fontSize: '14px',
|
|
470
|
+
padding: '12px 14px',
|
|
471
|
+
outline: 'none',
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const SWITCHER_LIST = {
|
|
475
|
+
overflowY: 'auto',
|
|
476
|
+
padding: '4px',
|
|
477
|
+
display: 'flex',
|
|
478
|
+
flexDirection: 'column',
|
|
479
|
+
gap: '1px',
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const SWITCHER_ROW = {
|
|
483
|
+
display: 'flex',
|
|
484
|
+
alignItems: 'center',
|
|
485
|
+
gap: '8px',
|
|
486
|
+
padding: '7px 10px',
|
|
487
|
+
border: 'none',
|
|
488
|
+
borderRadius: 'var(--dsw-radius-sm, 6px)',
|
|
489
|
+
background: 'transparent',
|
|
490
|
+
color: 'inherit',
|
|
491
|
+
fontSize: '13px',
|
|
492
|
+
textAlign: 'left',
|
|
493
|
+
cursor: 'pointer',
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const SWITCHER_HINT = {
|
|
497
|
+
flex: 'none',
|
|
498
|
+
color: 'var(--dsw-alias-label-quaternary, #8a8a8a)',
|
|
499
|
+
fontSize: '11px',
|
|
500
|
+
fontVariantNumeric: 'tabular-nums',
|
|
501
|
+
}
|
|
387
502
|
|
|
388
503
|
/**
|
|
389
504
|
* A workspace whose label carries a path ("Work/Client A") files itself under
|
|
@@ -586,7 +701,7 @@ export function BetterWorkspace(props) {
|
|
|
586
701
|
const services = props.services || {}
|
|
587
702
|
const { ctx, slots } = services
|
|
588
703
|
const standard = { wide: props.wide === true, expandSidebar: props.expandSidebar }
|
|
589
|
-
const [expanded, setExpanded] = React.useState(
|
|
704
|
+
const [expanded, setExpanded] = React.useState(() => readFolded().workspaces)
|
|
590
705
|
const [menu, setMenu] = React.useState(null)
|
|
591
706
|
const [armed, setArmed] = React.useState(null)
|
|
592
707
|
const [adding, setAdding] = React.useState(false)
|
|
@@ -597,7 +712,10 @@ export function BetterWorkspace(props) {
|
|
|
597
712
|
const [drag, setDrag] = React.useState(null)
|
|
598
713
|
const [dropping, setDropping] = React.useState(null)
|
|
599
714
|
const [look, setLook] = React.useState(() => readAppearance())
|
|
600
|
-
const
|
|
715
|
+
const folded = React.useMemo(() => readFolded(), [])
|
|
716
|
+
const [folders, setFolders] = React.useState(folded.folders)
|
|
717
|
+
const [switcher, setSwitcher] = React.useState(null)
|
|
718
|
+
React.useEffect(() => { writeFolded({ workspaces: expanded, folders }) }, [expanded, folders])
|
|
601
719
|
const [renaming, setRenaming] = React.useState(null)
|
|
602
720
|
const commitRename = async () => {
|
|
603
721
|
const draft = renaming
|
|
@@ -664,6 +782,36 @@ export function BetterWorkspace(props) {
|
|
|
664
782
|
return () => document.removeEventListener('keydown', onShortcut)
|
|
665
783
|
}, [])
|
|
666
784
|
|
|
785
|
+
React.useEffect(() => {
|
|
786
|
+
const onOpen = (event) => {
|
|
787
|
+
const key = (event.key || '').toLowerCase()
|
|
788
|
+
if (!(event.ctrlKey || event.metaKey) || key !== 'k') return
|
|
789
|
+
event.preventDefault()
|
|
790
|
+
setSwitcher((prev) => (prev ? null : { query: '', index: 0 }))
|
|
791
|
+
}
|
|
792
|
+
const onEscape = (event) => {
|
|
793
|
+
if (event.key === 'Escape') setSwitcher(null)
|
|
794
|
+
}
|
|
795
|
+
document.addEventListener('keydown', onOpen)
|
|
796
|
+
document.addEventListener('keydown', onEscape)
|
|
797
|
+
return () => {
|
|
798
|
+
document.removeEventListener('keydown', onOpen)
|
|
799
|
+
document.removeEventListener('keydown', onEscape)
|
|
800
|
+
}
|
|
801
|
+
}, [])
|
|
802
|
+
|
|
803
|
+
const switchItems = React.useMemo(() => {
|
|
804
|
+
const items = []
|
|
805
|
+
allRows.forEach((group) => {
|
|
806
|
+
const workspace = workspaceLabel(group.workspace)
|
|
807
|
+
group.sessions.forEach((row) => {
|
|
808
|
+
items.push({ id: row.id, title: row.displayTitle || row.id, workspace })
|
|
809
|
+
})
|
|
810
|
+
})
|
|
811
|
+
return items
|
|
812
|
+
}, [allRows])
|
|
813
|
+
const switchHits = switcher ? rankMatches(switchItems, switcher.query) : []
|
|
814
|
+
|
|
667
815
|
const openMenu = (event, row, workspace) => {
|
|
668
816
|
event.preventDefault()
|
|
669
817
|
event.stopPropagation()
|
|
@@ -882,6 +1030,56 @@ export function BetterWorkspace(props) {
|
|
|
882
1030
|
null)
|
|
883
1031
|
: null
|
|
884
1032
|
|
|
1033
|
+
const switcherCard = switcher
|
|
1034
|
+
? ReactDOM.createPortal(React.createElement('div', {
|
|
1035
|
+
'data-polish-switcher': 'open',
|
|
1036
|
+
style: SWITCHER_BACKDROP,
|
|
1037
|
+
onPointerDown: (event) => { if (event.target === event.currentTarget) setSwitcher(null) },
|
|
1038
|
+
},
|
|
1039
|
+
React.createElement('div', { style: SWITCHER_CARD },
|
|
1040
|
+
React.createElement('input', {
|
|
1041
|
+
'data-polish-switcher-input': 'true',
|
|
1042
|
+
autoFocus: true,
|
|
1043
|
+
value: switcher.query,
|
|
1044
|
+
placeholder: 'Jump to a session…',
|
|
1045
|
+
'aria-label': 'Jump to a session',
|
|
1046
|
+
style: SWITCHER_INPUT,
|
|
1047
|
+
onChange: (event) => setSwitcher(Object.assign({}, switcher, { query: event.target.value, index: 0 })),
|
|
1048
|
+
onKeyDown: (event) => {
|
|
1049
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
1050
|
+
if (switchHits.length === 0) return
|
|
1051
|
+
event.preventDefault()
|
|
1052
|
+
const step = event.key === 'ArrowDown' ? 1 : -1
|
|
1053
|
+
const next = (switcher.index + step + switchHits.length) % switchHits.length
|
|
1054
|
+
setSwitcher(Object.assign({}, switcher, { index: next }))
|
|
1055
|
+
return
|
|
1056
|
+
}
|
|
1057
|
+
if (event.key === 'Enter') {
|
|
1058
|
+
const hit = switchHits[switcher.index]
|
|
1059
|
+
if (!hit) return
|
|
1060
|
+
event.preventDefault()
|
|
1061
|
+
setSwitcher(null)
|
|
1062
|
+
services.openSession(hit.id)
|
|
1063
|
+
}
|
|
1064
|
+
},
|
|
1065
|
+
}),
|
|
1066
|
+
React.createElement('div', { 'data-polish-switcher-list': 'true', style: SWITCHER_LIST },
|
|
1067
|
+
switchHits.length === 0
|
|
1068
|
+
? React.createElement('div', { style: Object.assign({}, SWITCHER_ROW, { cursor: 'default' }) }, 'No session matches that.')
|
|
1069
|
+
: switchHits.map((hit, at) => React.createElement('button', {
|
|
1070
|
+
key: hit.id,
|
|
1071
|
+
type: 'button',
|
|
1072
|
+
'data-polish-switcher-row': hit.id,
|
|
1073
|
+
style: Object.assign({}, SWITCHER_ROW, {
|
|
1074
|
+
background: at === switcher.index ? 'var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.06))' : 'transparent',
|
|
1075
|
+
}),
|
|
1076
|
+
onPointerDown: (event) => event.preventDefault(),
|
|
1077
|
+
onClick: () => { setSwitcher(null); services.openSession(hit.id) },
|
|
1078
|
+
},
|
|
1079
|
+
React.createElement('span', { style: WS_TITLE }, hit.title),
|
|
1080
|
+
React.createElement('span', { style: SWITCHER_HINT }, hit.workspace)))))), document.body)
|
|
1081
|
+
: null
|
|
1082
|
+
|
|
885
1083
|
const children = []
|
|
886
1084
|
|
|
887
1085
|
children.push(React.createElement('div', { key: 'header', 'data-polish-ws': 'header', style: WS_HEADER },
|
|
@@ -1125,6 +1323,7 @@ export function BetterWorkspace(props) {
|
|
|
1125
1323
|
shownCount < totalCount
|
|
1126
1324
|
? React.createElement('div', { key: 'count', style: WS_COUNT_NOTE }, String(shownCount) + ' of ' + String(totalCount) + ' sessions')
|
|
1127
1325
|
: null,
|
|
1326
|
+
switcherCard,
|
|
1128
1327
|
sessionMenu,
|
|
1129
1328
|
paintMenu,
|
|
1130
1329
|
workspaceMenu)
|
package/docs/better-workspace.md
CHANGED
|
@@ -134,3 +134,21 @@ offers exactly one reorder verb (`insertBefore(movingId, beforeId)`), so restori
|
|
|
134
134
|
one move per workspace that sits in the wrong slot. Ids the Host no longer has are
|
|
135
135
|
filtered out first, so a preset that names a deleted workspace cannot shuffle the
|
|
136
136
|
survivors into the wrong places.
|
|
137
|
+
|
|
138
|
+
## Quick switcher
|
|
139
|
+
|
|
140
|
+
Ctrl/⌘-K opens a palette over every session in every workspace: the list is
|
|
141
|
+
`rankMatches(allSessions, query)`, which scores a title that *starts* with the query
|
|
142
|
+
above one that merely mentions it and keeps arrival order inside a rank (so "gre" puts
|
|
143
|
+
Greeting above later mentions). Arrows walk, Enter opens the session through
|
|
144
|
+
`uiWorkspace.openSession`, Escape or a backdrop click dismisses. It is portalled to
|
|
145
|
+
`document.body` with its own backdrop, so neither the sidebar's scroll container nor a
|
|
146
|
+
panel's stacking context can clip it.
|
|
147
|
+
|
|
148
|
+
## Folded state
|
|
149
|
+
|
|
150
|
+
`expanded` (workspaces) and `folders` (title folders) are seeded from
|
|
151
|
+
`aryacode-polish:folded.v1` and written back whenever either changes, so a restart looks
|
|
152
|
+
like the way out. Verified across a real restart: folding `WORK` left
|
|
153
|
+
`{"workspaces":{},"folders":{"Work":true}}` in storage and the folder still read `› WORK 2`
|
|
154
|
+
with its workspace hidden.
|
package/lib/client.js
CHANGED
|
@@ -53,7 +53,7 @@ window.__ModuleLoader__.load({
|
|
|
53
53
|
]
|
|
54
54
|
const ROW_ID = "polish"
|
|
55
55
|
const ROW_KEY = "@aryacode/dsh-aryacode-polish" + '#' + ROW_ID
|
|
56
|
-
const CLIENT_VERSION = "0.
|
|
56
|
+
const CLIENT_VERSION = "0.14.0"
|
|
57
57
|
|
|
58
58
|
/* html2canvas 1.4.1 — MIT, vendored from vendor/html2canvas.min.js.
|
|
59
59
|
* A UMD build, so it assigns itself to globalThis; face.js reads it lazily. */
|
|
@@ -585,6 +585,121 @@ window.__ModuleLoader__.load({
|
|
|
585
585
|
|
|
586
586
|
const LOOK_KEY = 'aryacode-polish:workspace-appearance.v1'
|
|
587
587
|
const ORDER_KEY = 'aryacode-polish:workspace-order.v1'
|
|
588
|
+
const FOLD_KEY = 'aryacode-polish:folded.v1'
|
|
589
|
+
|
|
590
|
+
/** Which workspaces and folders are folded, kept so a restart looks like yesterday. */
|
|
591
|
+
function readFolded(storage) {
|
|
592
|
+
try {
|
|
593
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
594
|
+
if (!store) return { workspaces: {}, folders: {} }
|
|
595
|
+
const raw = store.getItem(FOLD_KEY)
|
|
596
|
+
if (!raw) return { workspaces: {}, folders: {} }
|
|
597
|
+
const parsed = JSON.parse(raw)
|
|
598
|
+
return {
|
|
599
|
+
workspaces: (parsed && parsed.workspaces) || {},
|
|
600
|
+
folders: (parsed && parsed.folders) || {},
|
|
601
|
+
}
|
|
602
|
+
} catch {
|
|
603
|
+
return { workspaces: {}, folders: {} }
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function writeFolded(state, storage) {
|
|
608
|
+
try {
|
|
609
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
610
|
+
if (!store) return
|
|
611
|
+
const clean = {
|
|
612
|
+
workspaces: (state && state.workspaces) || {},
|
|
613
|
+
folders: (state && state.folders) || {},
|
|
614
|
+
}
|
|
615
|
+
store.setItem(FOLD_KEY, JSON.stringify(clean))
|
|
616
|
+
} catch {
|
|
617
|
+
/* folding is a convenience; a store that refuses must not break the tree */
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Rank the quick switcher's rows. A title that starts with the query wins over one
|
|
623
|
+
* that merely contains it, and equal kinds keep the order they arrived in — so
|
|
624
|
+
* typing "gre" puts Greeting above anything that mentions greeting later on.
|
|
625
|
+
*/
|
|
626
|
+
function rankMatches(items, query, text = (item) => String(item.title || item.id || '')) {
|
|
627
|
+
const list = Array.isArray(items) ? items : []
|
|
628
|
+
const needle = String(query || '').trim().toLowerCase()
|
|
629
|
+
if (!needle) return list.slice(0, 40)
|
|
630
|
+
const scored = []
|
|
631
|
+
list.forEach((item, order) => {
|
|
632
|
+
const hay = text(item).toLowerCase()
|
|
633
|
+
const at = hay.indexOf(needle)
|
|
634
|
+
if (at === -1) return
|
|
635
|
+
scored.push({ item, rank: at === 0 ? 0 : 1, order })
|
|
636
|
+
})
|
|
637
|
+
scored.sort((a, b) => (a.rank - b.rank) || (a.order - b.order))
|
|
638
|
+
return scored.slice(0, 40).map((entry) => entry.item)
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const SWITCHER_BACKDROP = {
|
|
642
|
+
position: 'fixed',
|
|
643
|
+
inset: 0,
|
|
644
|
+
zIndex: 1300,
|
|
645
|
+
display: 'flex',
|
|
646
|
+
alignItems: 'flex-start',
|
|
647
|
+
justifyContent: 'center',
|
|
648
|
+
paddingTop: '12vh',
|
|
649
|
+
background: 'rgba(16, 18, 22, 0.28)',
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const SWITCHER_CARD = {
|
|
653
|
+
width: 'min(560px, calc(100vw - 32px))',
|
|
654
|
+
maxHeight: '60vh',
|
|
655
|
+
overflow: 'hidden',
|
|
656
|
+
display: 'flex',
|
|
657
|
+
flexDirection: 'column',
|
|
658
|
+
borderRadius: 'var(--dsw-radius-lg, 10px)',
|
|
659
|
+
border: '0.5px solid var(--dsw-alias-border-l2, rgba(0,0,0,0.12))',
|
|
660
|
+
background: 'Canvas',
|
|
661
|
+
boxShadow: 'var(--dsw-elevation-prominent, 0 8px 24px rgba(0,0,0,0.18))',
|
|
662
|
+
color: 'var(--dsw-alias-label-primary, #1b1d21)',
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const SWITCHER_INPUT = {
|
|
666
|
+
border: 'none',
|
|
667
|
+
borderBottom: '0.5px solid var(--dsw-alias-border-l2, rgba(0,0,0,0.12))',
|
|
668
|
+
background: 'transparent',
|
|
669
|
+
color: 'inherit',
|
|
670
|
+
fontSize: '14px',
|
|
671
|
+
padding: '12px 14px',
|
|
672
|
+
outline: 'none',
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const SWITCHER_LIST = {
|
|
676
|
+
overflowY: 'auto',
|
|
677
|
+
padding: '4px',
|
|
678
|
+
display: 'flex',
|
|
679
|
+
flexDirection: 'column',
|
|
680
|
+
gap: '1px',
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const SWITCHER_ROW = {
|
|
684
|
+
display: 'flex',
|
|
685
|
+
alignItems: 'center',
|
|
686
|
+
gap: '8px',
|
|
687
|
+
padding: '7px 10px',
|
|
688
|
+
border: 'none',
|
|
689
|
+
borderRadius: 'var(--dsw-radius-sm, 6px)',
|
|
690
|
+
background: 'transparent',
|
|
691
|
+
color: 'inherit',
|
|
692
|
+
fontSize: '13px',
|
|
693
|
+
textAlign: 'left',
|
|
694
|
+
cursor: 'pointer',
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const SWITCHER_HINT = {
|
|
698
|
+
flex: 'none',
|
|
699
|
+
color: 'var(--dsw-alias-label-quaternary, #8a8a8a)',
|
|
700
|
+
fontSize: '11px',
|
|
701
|
+
fontVariantNumeric: 'tabular-nums',
|
|
702
|
+
}
|
|
588
703
|
|
|
589
704
|
/**
|
|
590
705
|
* A workspace whose label carries a path ("Work/Client A") files itself under
|
|
@@ -787,7 +902,7 @@ window.__ModuleLoader__.load({
|
|
|
787
902
|
const services = props.services || {}
|
|
788
903
|
const { ctx, slots } = services
|
|
789
904
|
const standard = { wide: props.wide === true, expandSidebar: props.expandSidebar }
|
|
790
|
-
const [expanded, setExpanded] = React.useState(
|
|
905
|
+
const [expanded, setExpanded] = React.useState(() => readFolded().workspaces)
|
|
791
906
|
const [menu, setMenu] = React.useState(null)
|
|
792
907
|
const [armed, setArmed] = React.useState(null)
|
|
793
908
|
const [adding, setAdding] = React.useState(false)
|
|
@@ -798,7 +913,10 @@ window.__ModuleLoader__.load({
|
|
|
798
913
|
const [drag, setDrag] = React.useState(null)
|
|
799
914
|
const [dropping, setDropping] = React.useState(null)
|
|
800
915
|
const [look, setLook] = React.useState(() => readAppearance())
|
|
801
|
-
const
|
|
916
|
+
const folded = React.useMemo(() => readFolded(), [])
|
|
917
|
+
const [folders, setFolders] = React.useState(folded.folders)
|
|
918
|
+
const [switcher, setSwitcher] = React.useState(null)
|
|
919
|
+
React.useEffect(() => { writeFolded({ workspaces: expanded, folders }) }, [expanded, folders])
|
|
802
920
|
const [renaming, setRenaming] = React.useState(null)
|
|
803
921
|
const commitRename = async () => {
|
|
804
922
|
const draft = renaming
|
|
@@ -865,6 +983,36 @@ window.__ModuleLoader__.load({
|
|
|
865
983
|
return () => document.removeEventListener('keydown', onShortcut)
|
|
866
984
|
}, [])
|
|
867
985
|
|
|
986
|
+
React.useEffect(() => {
|
|
987
|
+
const onOpen = (event) => {
|
|
988
|
+
const key = (event.key || '').toLowerCase()
|
|
989
|
+
if (!(event.ctrlKey || event.metaKey) || key !== 'k') return
|
|
990
|
+
event.preventDefault()
|
|
991
|
+
setSwitcher((prev) => (prev ? null : { query: '', index: 0 }))
|
|
992
|
+
}
|
|
993
|
+
const onEscape = (event) => {
|
|
994
|
+
if (event.key === 'Escape') setSwitcher(null)
|
|
995
|
+
}
|
|
996
|
+
document.addEventListener('keydown', onOpen)
|
|
997
|
+
document.addEventListener('keydown', onEscape)
|
|
998
|
+
return () => {
|
|
999
|
+
document.removeEventListener('keydown', onOpen)
|
|
1000
|
+
document.removeEventListener('keydown', onEscape)
|
|
1001
|
+
}
|
|
1002
|
+
}, [])
|
|
1003
|
+
|
|
1004
|
+
const switchItems = React.useMemo(() => {
|
|
1005
|
+
const items = []
|
|
1006
|
+
allRows.forEach((group) => {
|
|
1007
|
+
const workspace = workspaceLabel(group.workspace)
|
|
1008
|
+
group.sessions.forEach((row) => {
|
|
1009
|
+
items.push({ id: row.id, title: row.displayTitle || row.id, workspace })
|
|
1010
|
+
})
|
|
1011
|
+
})
|
|
1012
|
+
return items
|
|
1013
|
+
}, [allRows])
|
|
1014
|
+
const switchHits = switcher ? rankMatches(switchItems, switcher.query) : []
|
|
1015
|
+
|
|
868
1016
|
const openMenu = (event, row, workspace) => {
|
|
869
1017
|
event.preventDefault()
|
|
870
1018
|
event.stopPropagation()
|
|
@@ -1083,6 +1231,56 @@ window.__ModuleLoader__.load({
|
|
|
1083
1231
|
null)
|
|
1084
1232
|
: null
|
|
1085
1233
|
|
|
1234
|
+
const switcherCard = switcher
|
|
1235
|
+
? ReactDOM.createPortal(React.createElement('div', {
|
|
1236
|
+
'data-polish-switcher': 'open',
|
|
1237
|
+
style: SWITCHER_BACKDROP,
|
|
1238
|
+
onPointerDown: (event) => { if (event.target === event.currentTarget) setSwitcher(null) },
|
|
1239
|
+
},
|
|
1240
|
+
React.createElement('div', { style: SWITCHER_CARD },
|
|
1241
|
+
React.createElement('input', {
|
|
1242
|
+
'data-polish-switcher-input': 'true',
|
|
1243
|
+
autoFocus: true,
|
|
1244
|
+
value: switcher.query,
|
|
1245
|
+
placeholder: 'Jump to a session…',
|
|
1246
|
+
'aria-label': 'Jump to a session',
|
|
1247
|
+
style: SWITCHER_INPUT,
|
|
1248
|
+
onChange: (event) => setSwitcher(Object.assign({}, switcher, { query: event.target.value, index: 0 })),
|
|
1249
|
+
onKeyDown: (event) => {
|
|
1250
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
1251
|
+
if (switchHits.length === 0) return
|
|
1252
|
+
event.preventDefault()
|
|
1253
|
+
const step = event.key === 'ArrowDown' ? 1 : -1
|
|
1254
|
+
const next = (switcher.index + step + switchHits.length) % switchHits.length
|
|
1255
|
+
setSwitcher(Object.assign({}, switcher, { index: next }))
|
|
1256
|
+
return
|
|
1257
|
+
}
|
|
1258
|
+
if (event.key === 'Enter') {
|
|
1259
|
+
const hit = switchHits[switcher.index]
|
|
1260
|
+
if (!hit) return
|
|
1261
|
+
event.preventDefault()
|
|
1262
|
+
setSwitcher(null)
|
|
1263
|
+
services.openSession(hit.id)
|
|
1264
|
+
}
|
|
1265
|
+
},
|
|
1266
|
+
}),
|
|
1267
|
+
React.createElement('div', { 'data-polish-switcher-list': 'true', style: SWITCHER_LIST },
|
|
1268
|
+
switchHits.length === 0
|
|
1269
|
+
? React.createElement('div', { style: Object.assign({}, SWITCHER_ROW, { cursor: 'default' }) }, 'No session matches that.')
|
|
1270
|
+
: switchHits.map((hit, at) => React.createElement('button', {
|
|
1271
|
+
key: hit.id,
|
|
1272
|
+
type: 'button',
|
|
1273
|
+
'data-polish-switcher-row': hit.id,
|
|
1274
|
+
style: Object.assign({}, SWITCHER_ROW, {
|
|
1275
|
+
background: at === switcher.index ? 'var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.06))' : 'transparent',
|
|
1276
|
+
}),
|
|
1277
|
+
onPointerDown: (event) => event.preventDefault(),
|
|
1278
|
+
onClick: () => { setSwitcher(null); services.openSession(hit.id) },
|
|
1279
|
+
},
|
|
1280
|
+
React.createElement('span', { style: WS_TITLE }, hit.title),
|
|
1281
|
+
React.createElement('span', { style: SWITCHER_HINT }, hit.workspace)))))), document.body)
|
|
1282
|
+
: null
|
|
1283
|
+
|
|
1086
1284
|
const children = []
|
|
1087
1285
|
|
|
1088
1286
|
children.push(React.createElement('div', { key: 'header', 'data-polish-ws': 'header', style: WS_HEADER },
|
|
@@ -1326,6 +1524,7 @@ window.__ModuleLoader__.load({
|
|
|
1326
1524
|
shownCount < totalCount
|
|
1327
1525
|
? React.createElement('div', { key: 'count', style: WS_COUNT_NOTE }, String(shownCount) + ' of ' + String(totalCount) + ' sessions')
|
|
1328
1526
|
: null,
|
|
1527
|
+
switcherCard,
|
|
1329
1528
|
sessionMenu,
|
|
1330
1529
|
paintMenu,
|
|
1331
1530
|
workspaceMenu)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aryacode/dsh-aryacode-polish",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "AryaCode Polish: one install for the small things that make AryaCode feel finished. One Plugins row, one switch per quality-of-life feature.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|