@aryacode/dsh-aryacode-polish 0.11.2 → 0.12.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/CHANGELOG.md +22 -0
- package/client/workspace.js +228 -1
- package/docs/better-workspace.md +21 -0
- package/lib/client.js +229 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.12.1] - 2026-10-28
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- The Hero list now only appears when the workspace chip is open, and it paints a
|
|
8
|
+
card of its own instead of an unstyled list floating over the Hero copy.
|
|
9
|
+
|
|
10
|
+
## [0.12.0] - 2026-10-28
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Colour and weight per workspace.** Right-click a workspace → "Colour and
|
|
15
|
+
weight…" paints it one of six tones (slate, blue, green, amber, rose, violet) or
|
|
16
|
+
none, and can make its label bold. The choice is stored under
|
|
17
|
+
`aryacode-polish:workspace-appearance` in the app's own storage, so it survives a
|
|
18
|
+
restart, and a broken or missing store degrades to the plain tree instead of
|
|
19
|
+
throwing.
|
|
20
|
+
- **The same workspaces in the Hero picker.** The blank-session Hero now offers this
|
|
21
|
+
bundle's workspace list (tone dots, bold labels, session counts) instead of the
|
|
22
|
+
stock picker, through `conversation.hero.workspace` at priority -1 — same data,
|
|
23
|
+
same colours, one switch.
|
|
24
|
+
|
|
3
25
|
## [0.11.2] - 2026-10-28
|
|
4
26
|
|
|
5
27
|
### Verified
|
package/client/workspace.js
CHANGED
|
@@ -382,6 +382,82 @@ export function moveBefore(list, movingId, beforeId) {
|
|
|
382
382
|
return next
|
|
383
383
|
}
|
|
384
384
|
|
|
385
|
+
const LOOK_KEY = 'aryacode-polish:workspace-appearance.v1'
|
|
386
|
+
|
|
387
|
+
export const TONE_ORDER = ['slate', 'blue', 'green', 'amber', 'rose', 'violet']
|
|
388
|
+
|
|
389
|
+
export const TONE_COLOR = {
|
|
390
|
+
slate: '#8a8f98',
|
|
391
|
+
blue: '#4a72d0',
|
|
392
|
+
green: '#2f9e6b',
|
|
393
|
+
amber: '#c8892f',
|
|
394
|
+
rose: '#c2496a',
|
|
395
|
+
violet: '#7a5cc4',
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function appearanceOf(map, workspaceId) {
|
|
399
|
+
const entry = (map && map[workspaceId]) || {}
|
|
400
|
+
return {
|
|
401
|
+
tone: TONE_COLOR[entry.tone] ? entry.tone : undefined,
|
|
402
|
+
strong: entry.strong === true,
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function withAppearance(map, workspaceId, patch) {
|
|
407
|
+
const next = Object.assign({}, map || {})
|
|
408
|
+
const merged = Object.assign({}, appearanceOf(next, workspaceId), patch || {})
|
|
409
|
+
if (!merged.tone && merged.strong !== true) delete next[workspaceId]
|
|
410
|
+
else next[workspaceId] = { tone: merged.tone, strong: merged.strong === true }
|
|
411
|
+
return next
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export function readAppearance(storage) {
|
|
415
|
+
try {
|
|
416
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
417
|
+
if (!store) return {}
|
|
418
|
+
const raw = store.getItem(LOOK_KEY)
|
|
419
|
+
if (!raw) return {}
|
|
420
|
+
const parsed = JSON.parse(raw)
|
|
421
|
+
return parsed && typeof parsed === 'object' ? parsed : {}
|
|
422
|
+
} catch {
|
|
423
|
+
return {}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export function writeAppearance(map, storage) {
|
|
428
|
+
try {
|
|
429
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
430
|
+
if (!store) return
|
|
431
|
+
store.setItem(LOOK_KEY, JSON.stringify(map || {}))
|
|
432
|
+
} catch {
|
|
433
|
+
/* a locked-down webview must not break the tree */
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const WS_TONE = {
|
|
438
|
+
flex: 'none',
|
|
439
|
+
width: '6px',
|
|
440
|
+
height: '6px',
|
|
441
|
+
borderRadius: '50%',
|
|
442
|
+
boxShadow: '0 0 0 2px var(--dsw-alias-bg-base, transparent)',
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const WS_SWATCH = {
|
|
446
|
+
width: '18px',
|
|
447
|
+
height: '18px',
|
|
448
|
+
borderRadius: '50%',
|
|
449
|
+
border: '0.5px solid var(--dsw-alias-border-l2, rgba(0,0,0,0.12))',
|
|
450
|
+
cursor: 'pointer',
|
|
451
|
+
padding: 0,
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const WS_PAINT_ROW = {
|
|
455
|
+
display: 'flex',
|
|
456
|
+
alignItems: 'center',
|
|
457
|
+
gap: '6px',
|
|
458
|
+
padding: '6px 8px',
|
|
459
|
+
}
|
|
460
|
+
|
|
385
461
|
const WS_FILTER = {
|
|
386
462
|
margin: '2px 2px 6px',
|
|
387
463
|
padding: '4px 8px',
|
|
@@ -447,6 +523,14 @@ export function BetterWorkspace(props) {
|
|
|
447
523
|
const [hover, setHover] = React.useState(null)
|
|
448
524
|
const [drag, setDrag] = React.useState(null)
|
|
449
525
|
const [dropping, setDropping] = React.useState(null)
|
|
526
|
+
const [look, setLook] = React.useState(() => readAppearance())
|
|
527
|
+
const paintLook = (workspaceId, patch) => {
|
|
528
|
+
setLook((prev) => {
|
|
529
|
+
const next = withAppearance(prev, workspaceId, patch)
|
|
530
|
+
writeAppearance(next)
|
|
531
|
+
return next
|
|
532
|
+
})
|
|
533
|
+
}
|
|
450
534
|
const filterRef = React.useRef(null)
|
|
451
535
|
|
|
452
536
|
const workspaceState = useLive(services.workspacesLive)
|
|
@@ -588,6 +672,15 @@ export function BetterWorkspace(props) {
|
|
|
588
672
|
try { await navigator.clipboard.writeText(menu.workspace.path) } catch {}
|
|
589
673
|
},
|
|
590
674
|
}, 'Copy path'),
|
|
675
|
+
React.createElement('button', {
|
|
676
|
+
type: 'button',
|
|
677
|
+
'data-polish-ws-appearance': 'open',
|
|
678
|
+
style: WS_MENU_ITEM,
|
|
679
|
+
onClick: () => {
|
|
680
|
+
const workspace = menu.workspace
|
|
681
|
+
setMenu({ kind: 'paint', workspace, anchor: menu.anchor })
|
|
682
|
+
},
|
|
683
|
+
}, 'Colour and weight…'),
|
|
591
684
|
React.createElement('button', {
|
|
592
685
|
type: 'button',
|
|
593
686
|
style: Object.assign({}, WS_MENU_ITEM, { color: 'var(--dsw-alias-label-error, #d92d20)' }),
|
|
@@ -604,6 +697,44 @@ export function BetterWorkspace(props) {
|
|
|
604
697
|
}, armed && armed === menu.workspace.workspaceId ? 'Remove workspace?' : 'Remove workspace'))
|
|
605
698
|
: null
|
|
606
699
|
|
|
700
|
+
const paintMenu = menu && menu.kind === 'paint'
|
|
701
|
+
? (() => {
|
|
702
|
+
const workspace = menu.workspace
|
|
703
|
+
const current = appearanceOf(look, workspace.workspaceId)
|
|
704
|
+
return React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
|
|
705
|
+
React.createElement('div', { style: WS_PAINT_ROW },
|
|
706
|
+
TONE_ORDER.map((tone) => React.createElement('button', {
|
|
707
|
+
key: tone,
|
|
708
|
+
type: 'button',
|
|
709
|
+
'data-polish-tone': tone,
|
|
710
|
+
'aria-label': 'Colour ' + tone,
|
|
711
|
+
title: tone,
|
|
712
|
+
style: Object.assign({}, WS_SWATCH, {
|
|
713
|
+
background: TONE_COLOR[tone],
|
|
714
|
+
boxShadow: current.tone === tone
|
|
715
|
+
? '0 0 0 1.5px var(--dsw-alias-label-secondary, #61666b)'
|
|
716
|
+
: undefined,
|
|
717
|
+
}),
|
|
718
|
+
onClick: () => paintLook(workspace.workspaceId, { tone }),
|
|
719
|
+
})),
|
|
720
|
+
React.createElement('button', {
|
|
721
|
+
key: 'none',
|
|
722
|
+
type: 'button',
|
|
723
|
+
'data-polish-tone': 'none',
|
|
724
|
+
'aria-label': 'No colour',
|
|
725
|
+
title: 'No colour',
|
|
726
|
+
style: Object.assign({}, WS_SWATCH, { background: 'transparent' }),
|
|
727
|
+
onClick: () => paintLook(workspace.workspaceId, { tone: undefined }),
|
|
728
|
+
})),
|
|
729
|
+
React.createElement('button', {
|
|
730
|
+
type: 'button',
|
|
731
|
+
'data-polish-ws-strong': current.strong ? 'on' : 'off',
|
|
732
|
+
style: WS_MENU_ITEM,
|
|
733
|
+
onClick: () => paintLook(workspace.workspaceId, { strong: !current.strong }),
|
|
734
|
+
}, current.strong ? 'Weight: bold ✓' : 'Weight: normal'))
|
|
735
|
+
})()
|
|
736
|
+
: null
|
|
737
|
+
|
|
607
738
|
const sessionMenu = menu && menu.kind === 'session'
|
|
608
739
|
? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
|
|
609
740
|
entries.map((entry, index) => React.createElement(RowBoundary, { key: (entry.options && entry.options.id) || index },
|
|
@@ -724,7 +855,16 @@ export function BetterWorkspace(props) {
|
|
|
724
855
|
},
|
|
725
856
|
React.createElement(CaretGlyph, { open }),
|
|
726
857
|
React.createElement(FolderGlyph),
|
|
727
|
-
|
|
858
|
+
look[workspace.workspaceId] && appearanceOf(look, workspace.workspaceId).tone
|
|
859
|
+
? React.createElement('span', {
|
|
860
|
+
style: Object.assign({}, WS_TONE, { background: TONE_COLOR[appearanceOf(look, workspace.workspaceId).tone] }),
|
|
861
|
+
})
|
|
862
|
+
: null,
|
|
863
|
+
React.createElement('span', {
|
|
864
|
+
style: Object.assign({}, WS_TITLE, {
|
|
865
|
+
fontWeight: appearanceOf(look, workspace.workspaceId).strong ? 600 : undefined,
|
|
866
|
+
}),
|
|
867
|
+
}, workspaceLabel(workspace)),
|
|
728
868
|
React.createElement('span', { style: WS_COUNT }, String(sessions.length)),
|
|
729
869
|
hovered || focused
|
|
730
870
|
? React.createElement('button', {
|
|
@@ -806,9 +946,81 @@ export function BetterWorkspace(props) {
|
|
|
806
946
|
? React.createElement('div', { key: 'count', style: WS_COUNT_NOTE }, String(shownCount) + ' of ' + String(totalCount) + ' sessions')
|
|
807
947
|
: null,
|
|
808
948
|
sessionMenu,
|
|
949
|
+
paintMenu,
|
|
809
950
|
workspaceMenu)
|
|
810
951
|
}
|
|
811
952
|
|
|
953
|
+
const WS_HERO = {
|
|
954
|
+
display: 'flex',
|
|
955
|
+
flexDirection: 'column',
|
|
956
|
+
gap: '1px',
|
|
957
|
+
minWidth: '220px',
|
|
958
|
+
maxHeight: '320px',
|
|
959
|
+
overflowY: 'auto',
|
|
960
|
+
padding: '4px',
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
const WS_HERO_TITLE = {
|
|
964
|
+
fontSize: 'calc(' + "var(--dsh-content-font-size-secondary, 13px)" + ' - 2px)',
|
|
965
|
+
letterSpacing: '0.02em',
|
|
966
|
+
textTransform: 'uppercase',
|
|
967
|
+
color: 'var(--dsw-alias-label-quaternary, #8a8a8a)',
|
|
968
|
+
padding: '4px 6px 2px',
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* The Hero's workspace picker. Same data and same colours as the sidebar tree,
|
|
973
|
+
* reachable before a session exists — the Hero slot wants a list that calls
|
|
974
|
+
* `onPick(workspaceId)`, so this is a flat list rather than the tree.
|
|
975
|
+
*/
|
|
976
|
+
export function HeroWorkspacePicker(props) {
|
|
977
|
+
const services = (props && props.services) || {}
|
|
978
|
+
const workspaceState = useLive(services.workspacesLive)
|
|
979
|
+
const look = React.useMemo(() => readAppearance(), [])
|
|
980
|
+
const list = (workspaceState && (workspaceState.items || workspaceState.workspaces)) || []
|
|
981
|
+
// The slot hands over `open`: the chip is closed most of the time, and rendering
|
|
982
|
+
// the list unconditionally left it floating over the Hero copy.
|
|
983
|
+
if (!props || props.open !== true || list.length === 0) return null
|
|
984
|
+
const anchor = props.anchorRef && props.anchorRef.current
|
|
985
|
+
const spot = anchor && anchor.getBoundingClientRect
|
|
986
|
+
? (() => {
|
|
987
|
+
const box = anchor.getBoundingClientRect()
|
|
988
|
+
return {
|
|
989
|
+
position: 'fixed',
|
|
990
|
+
top: Math.round(box.bottom + 6) + 'px',
|
|
991
|
+
left: Math.max(8, Math.round(box.left)) + 'px',
|
|
992
|
+
zIndex: 1200,
|
|
993
|
+
}
|
|
994
|
+
})()
|
|
995
|
+
: {}
|
|
996
|
+
return ReactDOM.createPortal(React.createElement('div', { 'data-polish-hero': 'true', style: Object.assign({}, WS_MENU, WS_HERO, spot, { maxHeight: '320px' }) },
|
|
997
|
+
React.createElement('div', { style: WS_HERO_TITLE }, 'Workspaces'),
|
|
998
|
+
list.map((workspace) => {
|
|
999
|
+
const looks = appearanceOf(look, workspace.workspaceId)
|
|
1000
|
+
return React.createElement('button', {
|
|
1001
|
+
key: workspace.workspaceId,
|
|
1002
|
+
type: 'button',
|
|
1003
|
+
'data-polish-hero-row': workspace.workspaceId,
|
|
1004
|
+
style: Object.assign({}, WS_ROW, {
|
|
1005
|
+
fontWeight: looks.strong ? 600 : 500,
|
|
1006
|
+
background: 'transparent',
|
|
1007
|
+
border: 'none',
|
|
1008
|
+
textAlign: 'left',
|
|
1009
|
+
}),
|
|
1010
|
+
onClick: () => {
|
|
1011
|
+
if (props.onPick) props.onPick(workspace.workspaceId)
|
|
1012
|
+
if (props.onClose) props.onClose()
|
|
1013
|
+
},
|
|
1014
|
+
},
|
|
1015
|
+
React.createElement(FolderGlyph),
|
|
1016
|
+
looks.tone
|
|
1017
|
+
? React.createElement('span', { style: Object.assign({}, WS_TONE, { background: TONE_COLOR[looks.tone] }) })
|
|
1018
|
+
: null,
|
|
1019
|
+
React.createElement('span', { style: WS_TITLE }, workspaceLabel(workspace)),
|
|
1020
|
+
React.createElement('span', { style: WS_COUNT }, String((workspace.sessionIds || []).length)))
|
|
1021
|
+
})), document.body)
|
|
1022
|
+
}
|
|
1023
|
+
|
|
812
1024
|
/**
|
|
813
1025
|
* Add and dispose the seat with the switch. A `single` slot renders its LOWEST
|
|
814
1026
|
* priority occupant, so presence is the only way to stand aside: without this the
|
|
@@ -816,6 +1028,7 @@ export function BetterWorkspace(props) {
|
|
|
816
1028
|
*/
|
|
817
1029
|
export function installWorkspaceSeat(ctx, configForms, extras = {}) {
|
|
818
1030
|
let dispose
|
|
1031
|
+
let disposeHero
|
|
819
1032
|
let unlisten
|
|
820
1033
|
const scope = () => {
|
|
821
1034
|
try {
|
|
@@ -868,6 +1081,13 @@ export function installWorkspaceSeat(ctx, configForms, extras = {}) {
|
|
|
868
1081
|
priority: -1,
|
|
869
1082
|
inject: () => ({ services }),
|
|
870
1083
|
}, BetterWorkspace))
|
|
1084
|
+
// The Hero picker is its own single slot, so it needs its own -1 seat;
|
|
1085
|
+
// both live and die with the one switch.
|
|
1086
|
+
disposeHero = seated.slots.inject('conversation.hero.workspace', () => seated.slots.register({
|
|
1087
|
+
name: 'conversation.hero.workspace',
|
|
1088
|
+
priority: -1,
|
|
1089
|
+
inject: () => ({ services }),
|
|
1090
|
+
}, HeroWorkspacePicker))
|
|
871
1091
|
},
|
|
872
1092
|
)
|
|
873
1093
|
} catch (error) {
|
|
@@ -880,6 +1100,12 @@ export function installWorkspaceSeat(ctx, configForms, extras = {}) {
|
|
|
880
1100
|
install()
|
|
881
1101
|
return
|
|
882
1102
|
}
|
|
1103
|
+
if (typeof disposeHero === 'function') {
|
|
1104
|
+
try {
|
|
1105
|
+
disposeHero()
|
|
1106
|
+
} catch {}
|
|
1107
|
+
}
|
|
1108
|
+
disposeHero = undefined
|
|
883
1109
|
if (typeof dispose === 'function') {
|
|
884
1110
|
try {
|
|
885
1111
|
dispose()
|
|
@@ -897,6 +1123,7 @@ export function installWorkspaceSeat(ctx, configForms, extras = {}) {
|
|
|
897
1123
|
}
|
|
898
1124
|
return () => {
|
|
899
1125
|
if (typeof unlisten === 'function') unlisten()
|
|
1126
|
+
if (typeof disposeHero === 'function') disposeHero()
|
|
900
1127
|
if (typeof dispose === 'function') dispose()
|
|
901
1128
|
}
|
|
902
1129
|
}
|
package/docs/better-workspace.md
CHANGED
|
@@ -93,3 +93,24 @@ So the tree refuses with "A session stays in its workspace; drag it within that
|
|
|
93
93
|
workspace to reorder." rather than attempting a call that throws. If the Host ever
|
|
94
94
|
exposes attach/detach, the gesture becomes a one-line change here — the drag payload
|
|
95
95
|
already carries `{ kind: 'session', id, workspaceId }`.
|
|
96
|
+
|
|
97
|
+
## Colour and weight
|
|
98
|
+
|
|
99
|
+
Right-click a workspace → **Colour and weight…** opens a six-tone palette (slate, blue,
|
|
100
|
+
green, amber, rose, violet) plus "none", and toggles a bold label. The choice is kept in
|
|
101
|
+
the app's own storage under `aryacode-polish:workspace-appearance.v1` as
|
|
102
|
+
`{ [workspaceId]: { tone, strong } }`, so it survives a restart without asking the Host
|
|
103
|
+
to remember anything. `readAppearance`/`writeAppearance` swallow every failure
|
|
104
|
+
(missing storage, quota, a locked-down webview), so a broken store costs the colours
|
|
105
|
+
and nothing else. `appearanceOf` ignores a tone it does not know, which keeps an old
|
|
106
|
+
entry from breaking the row if the palette ever changes.
|
|
107
|
+
|
|
108
|
+
## The Hero picker
|
|
109
|
+
|
|
110
|
+
The blank-session Hero's workspace chip is the same list, not a second implementation:
|
|
111
|
+
`conversation.hero.workspace` is a `single` slot at the root, so this bundle shadows it
|
|
112
|
+
at `priority: -1` beside `sidebar.workspaces` and both seats come and go with the one
|
|
113
|
+
switch. The slot hands over `{ open, anchorRef, selectedId, onPick, onClose }`: the card
|
|
114
|
+
is portalled to the anchor's bottom-left only while `open` is true (an unconditional
|
|
115
|
+
list floated over the Hero copy), it shows the same tone dots and bold labels as the
|
|
116
|
+
sidebar, and picking a row calls `onPick(workspaceId)` then `onClose()`.
|
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.12.1"
|
|
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. */
|
|
@@ -583,6 +583,82 @@ window.__ModuleLoader__.load({
|
|
|
583
583
|
return next
|
|
584
584
|
}
|
|
585
585
|
|
|
586
|
+
const LOOK_KEY = 'aryacode-polish:workspace-appearance.v1'
|
|
587
|
+
|
|
588
|
+
const TONE_ORDER = ['slate', 'blue', 'green', 'amber', 'rose', 'violet']
|
|
589
|
+
|
|
590
|
+
const TONE_COLOR = {
|
|
591
|
+
slate: '#8a8f98',
|
|
592
|
+
blue: '#4a72d0',
|
|
593
|
+
green: '#2f9e6b',
|
|
594
|
+
amber: '#c8892f',
|
|
595
|
+
rose: '#c2496a',
|
|
596
|
+
violet: '#7a5cc4',
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function appearanceOf(map, workspaceId) {
|
|
600
|
+
const entry = (map && map[workspaceId]) || {}
|
|
601
|
+
return {
|
|
602
|
+
tone: TONE_COLOR[entry.tone] ? entry.tone : undefined,
|
|
603
|
+
strong: entry.strong === true,
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function withAppearance(map, workspaceId, patch) {
|
|
608
|
+
const next = Object.assign({}, map || {})
|
|
609
|
+
const merged = Object.assign({}, appearanceOf(next, workspaceId), patch || {})
|
|
610
|
+
if (!merged.tone && merged.strong !== true) delete next[workspaceId]
|
|
611
|
+
else next[workspaceId] = { tone: merged.tone, strong: merged.strong === true }
|
|
612
|
+
return next
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function readAppearance(storage) {
|
|
616
|
+
try {
|
|
617
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
618
|
+
if (!store) return {}
|
|
619
|
+
const raw = store.getItem(LOOK_KEY)
|
|
620
|
+
if (!raw) return {}
|
|
621
|
+
const parsed = JSON.parse(raw)
|
|
622
|
+
return parsed && typeof parsed === 'object' ? parsed : {}
|
|
623
|
+
} catch {
|
|
624
|
+
return {}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function writeAppearance(map, storage) {
|
|
629
|
+
try {
|
|
630
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
631
|
+
if (!store) return
|
|
632
|
+
store.setItem(LOOK_KEY, JSON.stringify(map || {}))
|
|
633
|
+
} catch {
|
|
634
|
+
/* a locked-down webview must not break the tree */
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const WS_TONE = {
|
|
639
|
+
flex: 'none',
|
|
640
|
+
width: '6px',
|
|
641
|
+
height: '6px',
|
|
642
|
+
borderRadius: '50%',
|
|
643
|
+
boxShadow: '0 0 0 2px var(--dsw-alias-bg-base, transparent)',
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const WS_SWATCH = {
|
|
647
|
+
width: '18px',
|
|
648
|
+
height: '18px',
|
|
649
|
+
borderRadius: '50%',
|
|
650
|
+
border: '0.5px solid var(--dsw-alias-border-l2, rgba(0,0,0,0.12))',
|
|
651
|
+
cursor: 'pointer',
|
|
652
|
+
padding: 0,
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const WS_PAINT_ROW = {
|
|
656
|
+
display: 'flex',
|
|
657
|
+
alignItems: 'center',
|
|
658
|
+
gap: '6px',
|
|
659
|
+
padding: '6px 8px',
|
|
660
|
+
}
|
|
661
|
+
|
|
586
662
|
const WS_FILTER = {
|
|
587
663
|
margin: '2px 2px 6px',
|
|
588
664
|
padding: '4px 8px',
|
|
@@ -648,6 +724,14 @@ window.__ModuleLoader__.load({
|
|
|
648
724
|
const [hover, setHover] = React.useState(null)
|
|
649
725
|
const [drag, setDrag] = React.useState(null)
|
|
650
726
|
const [dropping, setDropping] = React.useState(null)
|
|
727
|
+
const [look, setLook] = React.useState(() => readAppearance())
|
|
728
|
+
const paintLook = (workspaceId, patch) => {
|
|
729
|
+
setLook((prev) => {
|
|
730
|
+
const next = withAppearance(prev, workspaceId, patch)
|
|
731
|
+
writeAppearance(next)
|
|
732
|
+
return next
|
|
733
|
+
})
|
|
734
|
+
}
|
|
651
735
|
const filterRef = React.useRef(null)
|
|
652
736
|
|
|
653
737
|
const workspaceState = useLive(services.workspacesLive)
|
|
@@ -789,6 +873,15 @@ window.__ModuleLoader__.load({
|
|
|
789
873
|
try { await navigator.clipboard.writeText(menu.workspace.path) } catch {}
|
|
790
874
|
},
|
|
791
875
|
}, 'Copy path'),
|
|
876
|
+
React.createElement('button', {
|
|
877
|
+
type: 'button',
|
|
878
|
+
'data-polish-ws-appearance': 'open',
|
|
879
|
+
style: WS_MENU_ITEM,
|
|
880
|
+
onClick: () => {
|
|
881
|
+
const workspace = menu.workspace
|
|
882
|
+
setMenu({ kind: 'paint', workspace, anchor: menu.anchor })
|
|
883
|
+
},
|
|
884
|
+
}, 'Colour and weight…'),
|
|
792
885
|
React.createElement('button', {
|
|
793
886
|
type: 'button',
|
|
794
887
|
style: Object.assign({}, WS_MENU_ITEM, { color: 'var(--dsw-alias-label-error, #d92d20)' }),
|
|
@@ -805,6 +898,44 @@ window.__ModuleLoader__.load({
|
|
|
805
898
|
}, armed && armed === menu.workspace.workspaceId ? 'Remove workspace?' : 'Remove workspace'))
|
|
806
899
|
: null
|
|
807
900
|
|
|
901
|
+
const paintMenu = menu && menu.kind === 'paint'
|
|
902
|
+
? (() => {
|
|
903
|
+
const workspace = menu.workspace
|
|
904
|
+
const current = appearanceOf(look, workspace.workspaceId)
|
|
905
|
+
return React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
|
|
906
|
+
React.createElement('div', { style: WS_PAINT_ROW },
|
|
907
|
+
TONE_ORDER.map((tone) => React.createElement('button', {
|
|
908
|
+
key: tone,
|
|
909
|
+
type: 'button',
|
|
910
|
+
'data-polish-tone': tone,
|
|
911
|
+
'aria-label': 'Colour ' + tone,
|
|
912
|
+
title: tone,
|
|
913
|
+
style: Object.assign({}, WS_SWATCH, {
|
|
914
|
+
background: TONE_COLOR[tone],
|
|
915
|
+
boxShadow: current.tone === tone
|
|
916
|
+
? '0 0 0 1.5px var(--dsw-alias-label-secondary, #61666b)'
|
|
917
|
+
: undefined,
|
|
918
|
+
}),
|
|
919
|
+
onClick: () => paintLook(workspace.workspaceId, { tone }),
|
|
920
|
+
})),
|
|
921
|
+
React.createElement('button', {
|
|
922
|
+
key: 'none',
|
|
923
|
+
type: 'button',
|
|
924
|
+
'data-polish-tone': 'none',
|
|
925
|
+
'aria-label': 'No colour',
|
|
926
|
+
title: 'No colour',
|
|
927
|
+
style: Object.assign({}, WS_SWATCH, { background: 'transparent' }),
|
|
928
|
+
onClick: () => paintLook(workspace.workspaceId, { tone: undefined }),
|
|
929
|
+
})),
|
|
930
|
+
React.createElement('button', {
|
|
931
|
+
type: 'button',
|
|
932
|
+
'data-polish-ws-strong': current.strong ? 'on' : 'off',
|
|
933
|
+
style: WS_MENU_ITEM,
|
|
934
|
+
onClick: () => paintLook(workspace.workspaceId, { strong: !current.strong }),
|
|
935
|
+
}, current.strong ? 'Weight: bold ✓' : 'Weight: normal'))
|
|
936
|
+
})()
|
|
937
|
+
: null
|
|
938
|
+
|
|
808
939
|
const sessionMenu = menu && menu.kind === 'session'
|
|
809
940
|
? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
|
|
810
941
|
entries.map((entry, index) => React.createElement(RowBoundary, { key: (entry.options && entry.options.id) || index },
|
|
@@ -925,7 +1056,16 @@ window.__ModuleLoader__.load({
|
|
|
925
1056
|
},
|
|
926
1057
|
React.createElement(CaretGlyph, { open }),
|
|
927
1058
|
React.createElement(FolderGlyph),
|
|
928
|
-
|
|
1059
|
+
look[workspace.workspaceId] && appearanceOf(look, workspace.workspaceId).tone
|
|
1060
|
+
? React.createElement('span', {
|
|
1061
|
+
style: Object.assign({}, WS_TONE, { background: TONE_COLOR[appearanceOf(look, workspace.workspaceId).tone] }),
|
|
1062
|
+
})
|
|
1063
|
+
: null,
|
|
1064
|
+
React.createElement('span', {
|
|
1065
|
+
style: Object.assign({}, WS_TITLE, {
|
|
1066
|
+
fontWeight: appearanceOf(look, workspace.workspaceId).strong ? 600 : undefined,
|
|
1067
|
+
}),
|
|
1068
|
+
}, workspaceLabel(workspace)),
|
|
929
1069
|
React.createElement('span', { style: WS_COUNT }, String(sessions.length)),
|
|
930
1070
|
hovered || focused
|
|
931
1071
|
? React.createElement('button', {
|
|
@@ -1007,9 +1147,81 @@ window.__ModuleLoader__.load({
|
|
|
1007
1147
|
? React.createElement('div', { key: 'count', style: WS_COUNT_NOTE }, String(shownCount) + ' of ' + String(totalCount) + ' sessions')
|
|
1008
1148
|
: null,
|
|
1009
1149
|
sessionMenu,
|
|
1150
|
+
paintMenu,
|
|
1010
1151
|
workspaceMenu)
|
|
1011
1152
|
}
|
|
1012
1153
|
|
|
1154
|
+
const WS_HERO = {
|
|
1155
|
+
display: 'flex',
|
|
1156
|
+
flexDirection: 'column',
|
|
1157
|
+
gap: '1px',
|
|
1158
|
+
minWidth: '220px',
|
|
1159
|
+
maxHeight: '320px',
|
|
1160
|
+
overflowY: 'auto',
|
|
1161
|
+
padding: '4px',
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
const WS_HERO_TITLE = {
|
|
1165
|
+
fontSize: 'calc(' + "var(--dsh-content-font-size-secondary, 13px)" + ' - 2px)',
|
|
1166
|
+
letterSpacing: '0.02em',
|
|
1167
|
+
textTransform: 'uppercase',
|
|
1168
|
+
color: 'var(--dsw-alias-label-quaternary, #8a8a8a)',
|
|
1169
|
+
padding: '4px 6px 2px',
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
/**
|
|
1173
|
+
* The Hero's workspace picker. Same data and same colours as the sidebar tree,
|
|
1174
|
+
* reachable before a session exists — the Hero slot wants a list that calls
|
|
1175
|
+
* `onPick(workspaceId)`, so this is a flat list rather than the tree.
|
|
1176
|
+
*/
|
|
1177
|
+
function HeroWorkspacePicker(props) {
|
|
1178
|
+
const services = (props && props.services) || {}
|
|
1179
|
+
const workspaceState = useLive(services.workspacesLive)
|
|
1180
|
+
const look = React.useMemo(() => readAppearance(), [])
|
|
1181
|
+
const list = (workspaceState && (workspaceState.items || workspaceState.workspaces)) || []
|
|
1182
|
+
// The slot hands over `open`: the chip is closed most of the time, and rendering
|
|
1183
|
+
// the list unconditionally left it floating over the Hero copy.
|
|
1184
|
+
if (!props || props.open !== true || list.length === 0) return null
|
|
1185
|
+
const anchor = props.anchorRef && props.anchorRef.current
|
|
1186
|
+
const spot = anchor && anchor.getBoundingClientRect
|
|
1187
|
+
? (() => {
|
|
1188
|
+
const box = anchor.getBoundingClientRect()
|
|
1189
|
+
return {
|
|
1190
|
+
position: 'fixed',
|
|
1191
|
+
top: Math.round(box.bottom + 6) + 'px',
|
|
1192
|
+
left: Math.max(8, Math.round(box.left)) + 'px',
|
|
1193
|
+
zIndex: 1200,
|
|
1194
|
+
}
|
|
1195
|
+
})()
|
|
1196
|
+
: {}
|
|
1197
|
+
return ReactDOM.createPortal(React.createElement('div', { 'data-polish-hero': 'true', style: Object.assign({}, WS_MENU, WS_HERO, spot, { maxHeight: '320px' }) },
|
|
1198
|
+
React.createElement('div', { style: WS_HERO_TITLE }, 'Workspaces'),
|
|
1199
|
+
list.map((workspace) => {
|
|
1200
|
+
const looks = appearanceOf(look, workspace.workspaceId)
|
|
1201
|
+
return React.createElement('button', {
|
|
1202
|
+
key: workspace.workspaceId,
|
|
1203
|
+
type: 'button',
|
|
1204
|
+
'data-polish-hero-row': workspace.workspaceId,
|
|
1205
|
+
style: Object.assign({}, WS_ROW, {
|
|
1206
|
+
fontWeight: looks.strong ? 600 : 500,
|
|
1207
|
+
background: 'transparent',
|
|
1208
|
+
border: 'none',
|
|
1209
|
+
textAlign: 'left',
|
|
1210
|
+
}),
|
|
1211
|
+
onClick: () => {
|
|
1212
|
+
if (props.onPick) props.onPick(workspace.workspaceId)
|
|
1213
|
+
if (props.onClose) props.onClose()
|
|
1214
|
+
},
|
|
1215
|
+
},
|
|
1216
|
+
React.createElement(FolderGlyph),
|
|
1217
|
+
looks.tone
|
|
1218
|
+
? React.createElement('span', { style: Object.assign({}, WS_TONE, { background: TONE_COLOR[looks.tone] }) })
|
|
1219
|
+
: null,
|
|
1220
|
+
React.createElement('span', { style: WS_TITLE }, workspaceLabel(workspace)),
|
|
1221
|
+
React.createElement('span', { style: WS_COUNT }, String((workspace.sessionIds || []).length)))
|
|
1222
|
+
})), document.body)
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1013
1225
|
/**
|
|
1014
1226
|
* Add and dispose the seat with the switch. A `single` slot renders its LOWEST
|
|
1015
1227
|
* priority occupant, so presence is the only way to stand aside: without this the
|
|
@@ -1017,6 +1229,7 @@ window.__ModuleLoader__.load({
|
|
|
1017
1229
|
*/
|
|
1018
1230
|
function installWorkspaceSeat(ctx, configForms, extras = {}) {
|
|
1019
1231
|
let dispose
|
|
1232
|
+
let disposeHero
|
|
1020
1233
|
let unlisten
|
|
1021
1234
|
const scope = () => {
|
|
1022
1235
|
try {
|
|
@@ -1069,6 +1282,13 @@ window.__ModuleLoader__.load({
|
|
|
1069
1282
|
priority: -1,
|
|
1070
1283
|
inject: () => ({ services }),
|
|
1071
1284
|
}, BetterWorkspace))
|
|
1285
|
+
// The Hero picker is its own single slot, so it needs its own -1 seat;
|
|
1286
|
+
// both live and die with the one switch.
|
|
1287
|
+
disposeHero = seated.slots.inject('conversation.hero.workspace', () => seated.slots.register({
|
|
1288
|
+
name: 'conversation.hero.workspace',
|
|
1289
|
+
priority: -1,
|
|
1290
|
+
inject: () => ({ services }),
|
|
1291
|
+
}, HeroWorkspacePicker))
|
|
1072
1292
|
},
|
|
1073
1293
|
)
|
|
1074
1294
|
} catch (error) {
|
|
@@ -1081,6 +1301,12 @@ window.__ModuleLoader__.load({
|
|
|
1081
1301
|
install()
|
|
1082
1302
|
return
|
|
1083
1303
|
}
|
|
1304
|
+
if (typeof disposeHero === 'function') {
|
|
1305
|
+
try {
|
|
1306
|
+
disposeHero()
|
|
1307
|
+
} catch {}
|
|
1308
|
+
}
|
|
1309
|
+
disposeHero = undefined
|
|
1084
1310
|
if (typeof dispose === 'function') {
|
|
1085
1311
|
try {
|
|
1086
1312
|
dispose()
|
|
@@ -1098,6 +1324,7 @@ window.__ModuleLoader__.load({
|
|
|
1098
1324
|
}
|
|
1099
1325
|
return () => {
|
|
1100
1326
|
if (typeof unlisten === 'function') unlisten()
|
|
1327
|
+
if (typeof disposeHero === 'function') disposeHero()
|
|
1101
1328
|
if (typeof dispose === 'function') dispose()
|
|
1102
1329
|
}
|
|
1103
1330
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aryacode/dsh-aryacode-polish",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
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",
|