@chatcode/cco-better-sidebar 0.18.0-rc.4 → 0.18.0-rc.6
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 +15 -1
- package/README_EN.md +15 -1
- package/lib/client-editor.js +338 -266
- package/lib/client-mermaid.js +328 -260
- package/lib/client-registry.js +1049 -542
- package/lib/client-terminal.js +337 -265
- package/lib/client.js +1048 -541
- package/lib/index.js +162 -11
- package/lib/types/client/LogoutConfirmDialog.d.ts +5 -0
- package/lib/types/client/PluginProjectPicker.d.ts +13 -0
- package/lib/types/client/api.d.ts +7 -5
- package/lib/types/client/locales.d.ts +14 -0
- package/lib/types/client/ui-tweaks/branding.d.ts +1 -1
- package/lib/types/client/ui-tweaks/sidebar-header.d.ts +6 -0
- package/lib/types/login-page-script.d.ts +2 -0
- package/lib/types/login-route.d.ts +2 -0
- package/lib/types/plugin-market-shared.d.ts +22 -0
- package/package.json +1 -1
- package/src/client/AccountMenu.tsx +12 -2
- package/src/client/LogoutConfirmDialog.tsx +39 -0
- package/src/client/PluginMarketView.module.css +14 -0
- package/src/client/PluginMarketView.tsx +86 -24
- package/src/client/PluginProjectPicker.tsx +73 -0
- package/src/client/api.ts +7 -5
- package/src/client/locales-ja.ts +15 -1
- package/src/client/locales.ts +66 -3
- package/src/client/sidebar.module.css +81 -19
- package/src/client/ui-tweaks/branding.tsx +10 -9
- package/src/client/ui-tweaks/sidebar-header.ts +73 -0
- package/src/login-page-script.ts +107 -0
- package/src/login-route.ts +7 -4
- package/src/plugin-market-api.ts +55 -9
- package/src/plugin-market-shared.ts +9 -0
|
@@ -7,30 +7,38 @@ import { IconChevronLeftOutline14, IconChevronRightOutline14, IconRefreshOutline
|
|
|
7
7
|
import type { PluginMarketCatalog } from './plugin-market-catalog.ts'
|
|
8
8
|
import type { InstalledMarketplacePlugin, MarketplacePlugin, PluginMarketScope } from '../plugin-market-shared.ts'
|
|
9
9
|
import css from './PluginMarketView.module.css'
|
|
10
|
+
import { PluginProjectPicker, type PluginProjectRequest } from './PluginProjectPicker.tsx'
|
|
10
11
|
|
|
11
12
|
export function PluginMarketView({ scope, visible, catalog }: { scope: SessionScope; visible: boolean; catalog: PluginMarketCatalog }): JSX.Element {
|
|
12
13
|
const snapshot = useSyncExternalStore(catalog.subscribe, catalog.getSnapshot)
|
|
13
14
|
const [marketId, setMarketId] = useState<string | null>(null)
|
|
14
15
|
const [tab, setTab] = useState<'markets' | 'installed'>('markets')
|
|
15
16
|
const [query, setQuery] = useState('')
|
|
16
|
-
const [
|
|
17
|
-
const [recordsReady, setRecordsReady] = useState(false)
|
|
18
|
-
const [recordsError, setRecordsError] = useState('')
|
|
17
|
+
const [recordState, setRecordState] = useState<{ key: string; records: InstalledMarketplacePlugin[]; ready: boolean; error: string }>()
|
|
19
18
|
const [message, setMessage] = useState<{ text: string; error: boolean }>()
|
|
20
|
-
const [working, setWorking] = useState<{ id: string; scope: PluginMarketScope }>()
|
|
19
|
+
const [working, setWorking] = useState<{ id: string; name: string; scope: PluginMarketScope; uninstall: boolean; choosing?: boolean }>()
|
|
20
|
+
const [selection, setSelection] = useState<{ key: string; path: string }>()
|
|
21
|
+
const [picker, setPicker] = useState<PluginProjectRequest>()
|
|
22
|
+
const pickerRef = useRef<PluginProjectRequest>()
|
|
21
23
|
const [revision, setRevision] = useState(0)
|
|
22
24
|
const pending = useRef(false)
|
|
23
25
|
const breadcrumbNode = useRef<HTMLDivElement>(null)
|
|
24
26
|
const mutationVersion = useRef(0)
|
|
25
27
|
const focusBack = useRef(false)
|
|
26
28
|
const scopeKey = `${scope.sessionId}\0${scope.cwd ?? ''}`
|
|
29
|
+
const projectPath = selection?.key === scopeKey ? selection.path : scope.cwd
|
|
30
|
+
const recordsKey = `${scopeKey}\0${projectPath ?? ''}`
|
|
31
|
+
const installed = recordState?.key === recordsKey ? recordState.records : []
|
|
32
|
+
const recordsReady = recordState?.key === recordsKey && recordState.ready
|
|
33
|
+
const recordsError = recordState?.key === recordsKey ? recordState.error : ''
|
|
27
34
|
const latestScope = useRef(scopeKey)
|
|
28
35
|
latestScope.current = scopeKey
|
|
29
36
|
const markets = snapshot.markets?.data?.marketplaces ?? []
|
|
30
37
|
const market = markets.find(item => item.id === marketId)
|
|
31
38
|
const catalogEntry = tab === 'markets' && marketId ? snapshot.plugins[marketId] : snapshot.markets
|
|
32
39
|
const loading = snapshot.refresh?.status === 'loading' || (tab === 'markets' && (!catalogEntry || catalogEntry.status === 'loading'))
|
|
33
|
-
const
|
|
40
|
+
const operating = !!working && !working.choosing
|
|
41
|
+
const blocked = loading || snapshot.refresh?.status === 'error' || !!working
|
|
34
42
|
|
|
35
43
|
useEffect(() => {
|
|
36
44
|
if (!visible) return
|
|
@@ -40,9 +48,11 @@ export function PluginMarketView({ scope, visible, catalog }: { scope: SessionSc
|
|
|
40
48
|
useEffect(() => { setQuery('') }, [tab, marketId])
|
|
41
49
|
useEffect(() => { if (focusBack.current) { breadcrumbNode.current?.querySelector('button')?.focus(); focusBack.current = false } }, [marketId])
|
|
42
50
|
|
|
43
|
-
useEffect(() => {
|
|
44
|
-
|
|
45
|
-
|
|
51
|
+
useEffect(() => { setMessage(undefined) }, [scopeKey])
|
|
52
|
+
useEffect(() => () => {
|
|
53
|
+
pickerRef.current?.controller.abort()
|
|
54
|
+
pickerRef.current?.finish(null)
|
|
55
|
+
}, [])
|
|
46
56
|
useEffect(() => {
|
|
47
57
|
if (!visible) return
|
|
48
58
|
const controller = new AbortController()
|
|
@@ -53,10 +63,10 @@ export function PluginMarketView({ scope, visible, catalog }: { scope: SessionSc
|
|
|
53
63
|
reading = true
|
|
54
64
|
const version = mutationVersion.current
|
|
55
65
|
try {
|
|
56
|
-
const records = await api.pluginsInstalled(scope, controller.signal)
|
|
57
|
-
if (active && !pending.current && version === mutationVersion.current) {
|
|
66
|
+
const records = await api.pluginsInstalled(scope, controller.signal, projectPath)
|
|
67
|
+
if (active && !pending.current && version === mutationVersion.current) setRecordState({ key: recordsKey, records, ready: true, error: '' })
|
|
58
68
|
} catch (cause) {
|
|
59
|
-
if (active && version === mutationVersion.current)
|
|
69
|
+
if (active && version === mutationVersion.current) setRecordState(previous => ({ key: recordsKey, records: previous?.key === recordsKey ? previous.records : [], ready: previous?.key === recordsKey && previous.ready, error: cause instanceof Error ? cause.message : String(cause) }))
|
|
60
70
|
} finally { reading = false }
|
|
61
71
|
}
|
|
62
72
|
void read()
|
|
@@ -64,28 +74,70 @@ export function PluginMarketView({ scope, visible, catalog }: { scope: SessionSc
|
|
|
64
74
|
// Remote catalogs are never polled or refreshed by navigation.
|
|
65
75
|
const timer = setInterval(() => { void read() }, 2000)
|
|
66
76
|
return () => { active = false; clearInterval(timer); controller.abort() }
|
|
67
|
-
}, [
|
|
77
|
+
}, [recordsKey, visible, revision])
|
|
78
|
+
|
|
79
|
+
const chooseDirectory = (): Promise<string | null> => new Promise(finish => {
|
|
80
|
+
const controller = new AbortController()
|
|
81
|
+
const request = { result: api.pluginProjectPick(controller.signal), controller, finish }
|
|
82
|
+
pickerRef.current = request
|
|
83
|
+
setPicker(request)
|
|
84
|
+
})
|
|
85
|
+
const finishPicker = (path: string | null): void => {
|
|
86
|
+
pickerRef.current?.finish(path)
|
|
87
|
+
pickerRef.current?.controller.abort()
|
|
88
|
+
pickerRef.current = undefined
|
|
89
|
+
setPicker(undefined)
|
|
90
|
+
}
|
|
91
|
+
const chooseProject = async (): Promise<void> => {
|
|
92
|
+
if (pending.current || blocked) return
|
|
93
|
+
pending.current = true
|
|
94
|
+
mutationVersion.current++
|
|
95
|
+
setMessage(undefined)
|
|
96
|
+
const key = scopeKey
|
|
97
|
+
try {
|
|
98
|
+
const path = await chooseDirectory()
|
|
99
|
+
if (path && latestScope.current === key) setSelection({ key, path })
|
|
100
|
+
} finally { pending.current = false; setRevision(value => value + 1) }
|
|
101
|
+
}
|
|
68
102
|
|
|
69
103
|
const perform = async (plugin: MarketplacePlugin, installScope: PluginMarketScope): Promise<void> => {
|
|
70
104
|
const refreshStatus = catalog.getSnapshot().refresh?.status
|
|
71
105
|
if (pending.current || !recordsReady || blocked || refreshStatus === 'loading' || refreshStatus === 'error') return
|
|
72
106
|
const key = scopeKey
|
|
73
|
-
const
|
|
107
|
+
const record = installed.find(item => item.id === plugin.id && item.scope === installScope)
|
|
108
|
+
const present = !!record
|
|
74
109
|
pending.current = true
|
|
75
110
|
mutationVersion.current++
|
|
76
|
-
|
|
111
|
+
const operation = { id: plugin.id, name: plugin.name, scope: installScope, uninstall: present }
|
|
112
|
+
setWorking({ ...operation, choosing: installScope === 'project' && !present })
|
|
77
113
|
setMessage(undefined)
|
|
78
114
|
try {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
115
|
+
let target = record?.scope === 'project' ? record.projectPath : projectPath
|
|
116
|
+
if (installScope === 'project' && !present) {
|
|
117
|
+
const chosen = await chooseDirectory()
|
|
118
|
+
if (!chosen || latestScope.current !== key) return
|
|
119
|
+
target = chosen
|
|
120
|
+
setSelection({ key, path: target })
|
|
121
|
+
setWorking(operation)
|
|
122
|
+
}
|
|
123
|
+
let installedPath = ''
|
|
124
|
+
if (present) await api.pluginUninstall(scope, plugin.id, installScope, target)
|
|
125
|
+
else {
|
|
126
|
+
const result = await api.pluginInstall(scope, plugin.id, installScope, target)
|
|
127
|
+
installedPath = result.installPath
|
|
128
|
+
if (installScope === 'project') {
|
|
129
|
+
target = result.projectPath
|
|
130
|
+
if (latestScope.current === key) setSelection({ key, path: target })
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const records = await api.pluginsInstalled(scope, undefined, target)
|
|
82
134
|
if (latestScope.current === key) {
|
|
83
|
-
|
|
84
|
-
setMessage({ text:
|
|
135
|
+
setRecordState({ key: `${key}\0${target ?? ''}`, records, ready: true, error: '' })
|
|
136
|
+
setMessage({ text: present ? t('pluginMarketUninstalled') : t('pluginMarketInstalledAt', { path: installedPath }), error: false })
|
|
85
137
|
}
|
|
86
138
|
} catch (cause) {
|
|
87
139
|
if (latestScope.current === key) setMessage({ text: cause instanceof Error ? cause.message : String(cause), error: true })
|
|
88
|
-
} finally { pending.current = false; setWorking(undefined) }
|
|
140
|
+
} finally { pending.current = false; setWorking(undefined); setRevision(value => value + 1) }
|
|
89
141
|
}
|
|
90
142
|
const refresh = (): void => {
|
|
91
143
|
if (loading || pending.current) return
|
|
@@ -104,12 +156,15 @@ export function PluginMarketView({ scope, visible, catalog }: { scope: SessionSc
|
|
|
104
156
|
const warning = [snapshot.markets?.data?.warning, marketId ? snapshot.plugins[marketId]?.warning : ''].filter(Boolean).join('\n')
|
|
105
157
|
const hasData = tab === 'installed' ? recordsReady : !!catalogEntry?.data
|
|
106
158
|
|
|
107
|
-
return <section className={css.root} aria-label={t('pluginMarketTitle')} aria-busy={loading} data-chatcode-plugin-market>
|
|
159
|
+
return <section className={css.root} aria-label={t('pluginMarketTitle')} aria-busy={loading || operating} data-chatcode-plugin-market>
|
|
108
160
|
<header className={css.head}>
|
|
109
161
|
<div><h2 className={css.title}>{t('pluginMarketTitle')}</h2><p className={css.subtitle}>{t('pluginMarketSubtitle')}</p></div>
|
|
110
162
|
<Input disabled={blocked} value={query} onChange={event => setQuery(event.target.value)} placeholder={t('pluginMarketSearch')} aria-label={t('pluginMarketSearch')} />
|
|
111
163
|
</header>
|
|
112
|
-
|
|
164
|
+
<div className={css.projectRow}>
|
|
165
|
+
<p className={css.project}>{projectPath ? t('pluginMarketProjectDirectory', { path: projectPath }) : t('pluginMarketProjectHint')}</p>
|
|
166
|
+
<Button size="sm" variant="outline" disabled={blocked || !!working || !!picker} onClick={() => { void chooseProject() }}>{t('pluginMarketChooseProject')}</Button>
|
|
167
|
+
</div>
|
|
113
168
|
<div className={css.toolbar}>
|
|
114
169
|
<div className={css.tabs}>
|
|
115
170
|
<Button size="sm" variant="ghost" disabled={blocked} aria-pressed={tab === 'markets'} onClick={() => setTab('markets')}>{t('pluginMarketTitle')}</Button>
|
|
@@ -117,6 +172,10 @@ export function PluginMarketView({ scope, visible, catalog }: { scope: SessionSc
|
|
|
117
172
|
</div>
|
|
118
173
|
<Button size="sm" variant="outline" aria-label={t('pluginMarketRefresh')} disabled={snapshot.markets?.status === 'loading' || loading || !!working} onClick={refresh}><span aria-hidden="true" className={loading ? css.spinning : undefined}><IconRefreshOutline14 /></span>{t(loading ? 'pluginMarketRefreshing' : 'pluginMarketRefresh')}</Button>
|
|
119
174
|
</div>
|
|
175
|
+
{operating ? <div className={css.operation} role="status">
|
|
176
|
+
<span className={css.spinning} aria-hidden="true"><IconRefreshOutline14 /></span>
|
|
177
|
+
<span>{t(working.uninstall ? 'pluginMarketUninstallProgress' : 'pluginMarketInstallProgress', { name: working.name })}</span>
|
|
178
|
+
</div> : null}
|
|
120
179
|
{warning && tab === 'markets' ? <p className={css.warning} role="status">{warning}</p> : null}
|
|
121
180
|
{error ? <p className={css.error} role="status">{error}</p> : null}
|
|
122
181
|
{message ? <p className={message.error ? css.error : css.notice} role="status">{message.text}</p> : null}
|
|
@@ -125,7 +184,7 @@ export function PluginMarketView({ scope, visible, catalog }: { scope: SessionSc
|
|
|
125
184
|
<span aria-hidden="true">/</span><span>{market?.name ?? marketId}</span>
|
|
126
185
|
</div> : null}
|
|
127
186
|
{loading ? <p className={css.state} role="status">{t(hasData ? 'pluginMarketRefreshing' : 'pluginMarketLoading')}</p> : null}
|
|
128
|
-
<div className={css.grid} aria-busy={loading}>
|
|
187
|
+
<div className={css.grid} aria-busy={loading || operating}>
|
|
129
188
|
{showMarkets ? filteredMarkets.map(item => <button key={item.id} type="button" disabled={blocked} className={`${css.card} ${css.marketCard}`} onClick={() => { focusBack.current = true; setMarketId(item.id) }}>
|
|
130
189
|
<div className={css.cardHead}><h3>{item.name}</h3>{item.official ? <Pill>{t('pluginMarketDefault')}</Pill> : null}</div>
|
|
131
190
|
<p className={css.description}>{item.description || t('pluginMarketNoDescription')}</p>
|
|
@@ -137,12 +196,14 @@ export function PluginMarketView({ scope, visible, catalog }: { scope: SessionSc
|
|
|
137
196
|
<div className={css.cardHead}><h3>{item.name}</h3>{records.length ? <Pill>{t('pluginMarketInstalledTab')}</Pill> : null}</div>
|
|
138
197
|
<p className={css.description}>{item.description || t('pluginMarketNoDescription')}</p>
|
|
139
198
|
<p className={css.meta}>{marketName}{tab === 'installed' && records[0]?.version ? ` · ${records[0].version}` : ''}</p>
|
|
199
|
+
{records.map(record => <p key={record.scope} className={css.installPath}>{t('pluginMarketInstallPath', { scope: t(record.scope === 'project' ? 'pluginMarketProject' : 'pluginMarketUser'), path: record.installPath })}</p>)}
|
|
140
200
|
<div className={css.actions}>
|
|
141
201
|
{(['project', 'user'] as const).map(installScope => {
|
|
142
202
|
const present = records.some(record => record.scope === installScope)
|
|
143
203
|
const busy = working?.id === item.id && working.scope === installScope
|
|
144
204
|
return <Button key={installScope} size="sm" variant={installScope === 'project' && !present ? 'primary' : 'outline'} className={installScope === 'project' && !present ? css.projectInstall : undefined} disabled={blocked || !!working || !recordsReady} onClick={() => { void perform(item, installScope) }}>
|
|
145
|
-
{
|
|
205
|
+
{busy && operating ? <span className={css.spinning} aria-hidden="true"><IconRefreshOutline14 /></span> : null}
|
|
206
|
+
{t(busy ? working?.choosing ? 'pluginMarketChoosingDirectory' : working?.uninstall ? 'pluginMarketUninstalling' : 'pluginMarketInstalling' : present ? 'pluginMarketUninstall' : 'pluginMarketInstall')} · {t(installScope === 'project' ? 'pluginMarketProject' : 'pluginMarketUser')}
|
|
146
207
|
</Button>
|
|
147
208
|
})}
|
|
148
209
|
</div>
|
|
@@ -150,5 +211,6 @@ export function PluginMarketView({ scope, visible, catalog }: { scope: SessionSc
|
|
|
150
211
|
})}
|
|
151
212
|
</div>
|
|
152
213
|
{!loading && hasData && (showMarkets ? filteredMarkets : filteredPlugins).length === 0 ? <p className={css.state}>{t('pluginMarketEmpty')}</p> : null}
|
|
214
|
+
{picker ? <PluginProjectPicker request={picker} initialPath={projectPath} onPicked={finishPicker} onCancel={() => finishPicker(null)} onError={text => { setMessage({ text, error: true }); finishPicker(null) }} /> : null}
|
|
153
215
|
</section>
|
|
154
216
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** Native chooser first; the host browse backend supplies the remote Web dialog. */
|
|
2
|
+
import { useEffect, useRef, useState } from 'react'
|
|
3
|
+
import { Button, Input, Modal } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
4
|
+
import { api } from './api.ts'
|
|
5
|
+
import { t } from './locales.ts'
|
|
6
|
+
import { IconFolderOpen16 } from './icons.tsx'
|
|
7
|
+
import type { PluginProjectDirectories, PluginProjectPick } from '../plugin-market-shared.ts'
|
|
8
|
+
import css from './PluginMarketView.module.css'
|
|
9
|
+
|
|
10
|
+
export interface PluginProjectRequest {
|
|
11
|
+
result: Promise<PluginProjectPick>
|
|
12
|
+
controller: AbortController
|
|
13
|
+
finish: (path: string | null) => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function PluginProjectPicker({ request, initialPath, onPicked, onCancel, onError }: {
|
|
17
|
+
request: PluginProjectRequest
|
|
18
|
+
initialPath?: string
|
|
19
|
+
onPicked: (path: string) => void
|
|
20
|
+
onCancel: () => void
|
|
21
|
+
onError: (message: string) => void
|
|
22
|
+
}): JSX.Element | null {
|
|
23
|
+
const [browse, setBrowse] = useState(false)
|
|
24
|
+
const [path, setPath] = useState(initialPath ?? '')
|
|
25
|
+
const [draft, setDraft] = useState(initialPath ?? '')
|
|
26
|
+
const [listing, setListing] = useState<PluginProjectDirectories>()
|
|
27
|
+
const [loading, setLoading] = useState(true)
|
|
28
|
+
const [error, setError] = useState('')
|
|
29
|
+
const [revision, setRevision] = useState(0)
|
|
30
|
+
const callbacks = useRef({ onPicked, onCancel, onError })
|
|
31
|
+
callbacks.current = { onPicked, onCancel, onError }
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
let active = true
|
|
34
|
+
void request.result.then(result => {
|
|
35
|
+
if (!active) return
|
|
36
|
+
if (result.kind === 'browse') setBrowse(true)
|
|
37
|
+
else if (result.path === null) callbacks.current.onCancel()
|
|
38
|
+
else callbacks.current.onPicked(result.path)
|
|
39
|
+
}).catch(cause => { if (active) callbacks.current.onError(cause instanceof Error ? cause.message : String(cause)) })
|
|
40
|
+
return () => { active = false }
|
|
41
|
+
}, [request])
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (!browse) return
|
|
44
|
+
const controller = new AbortController()
|
|
45
|
+
let active = true
|
|
46
|
+
setLoading(true); setListing(undefined); setError('')
|
|
47
|
+
void api.pluginProjectDirectories(path || undefined, controller.signal).then(value => {
|
|
48
|
+
if (active) { setListing(value); setDraft(value.path); setLoading(false) }
|
|
49
|
+
}).catch(cause => {
|
|
50
|
+
if (active) { setError(cause instanceof Error ? cause.message : String(cause)); setLoading(false) }
|
|
51
|
+
})
|
|
52
|
+
return () => { active = false; controller.abort() }
|
|
53
|
+
}, [browse, path, revision])
|
|
54
|
+
const navigate = (next: string): void => { setLoading(true); setListing(undefined); setPath(next); setRevision(value => value + 1) }
|
|
55
|
+
if (!browse) return null
|
|
56
|
+
return <Modal open title={t('pluginMarketChooseProject')} description={t('pluginMarketProjectHint')} closeLabel={t('cancel')} onClose={onCancel} className={css.projectPicker}
|
|
57
|
+
footer={<><Button variant="outline" onClick={onCancel}>{t('cancel')}</Button><Button variant="primary" disabled={loading || !listing} onClick={() => { if (listing) onPicked(listing.path) }}>{t('pluginMarketSelectDirectory')}</Button></>}>
|
|
58
|
+
<form className={css.directoryPath} onSubmit={event => { event.preventDefault(); navigate(draft) }}>
|
|
59
|
+
<Input data-modal-autofocus disabled={loading} aria-label={t('pluginMarketDirectoryPath')} value={draft} onChange={event => setDraft(event.target.value)} />
|
|
60
|
+
<Button type="submit" variant="outline" disabled={loading}>{t('pluginMarketOpenDirectory')}</Button>
|
|
61
|
+
</form>
|
|
62
|
+
<nav className={css.directoryCrumbs} aria-label={t('pluginMarketDirectoryPath')}>
|
|
63
|
+
<Button size="sm" variant="ghost" disabled={loading} onClick={() => navigate('')}>{t('pluginMarketHomeDirectory')}</Button>
|
|
64
|
+
{listing?.crumbs.map(crumb => <Button key={crumb.path} size="sm" variant="ghost" disabled={loading} onClick={() => navigate(crumb.path)}>{crumb.name}</Button>)}
|
|
65
|
+
</nav>
|
|
66
|
+
{error ? <p className={css.error} role="alert">{error}</p> : null}
|
|
67
|
+
{loading ? <p className={css.state} role="status">{t('pluginMarketLoading')}</p> : null}
|
|
68
|
+
<div className={css.directoryList} aria-busy={loading}>
|
|
69
|
+
{listing?.entries.map(entry => <button key={entry.path} type="button" onClick={() => navigate(entry.path)}><IconFolderOpen16 />{entry.name}</button>)}
|
|
70
|
+
</div>
|
|
71
|
+
{listing?.truncated ? <p className={css.state}>{t('pluginMarketDirectoryTruncated')}</p> : null}
|
|
72
|
+
</Modal>
|
|
73
|
+
}
|
package/src/client/api.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { encodeHtmlUrl } from '../html-route.ts'
|
|
10
10
|
import type { BrowserProbeResult } from './browser.ts'
|
|
11
|
-
import type { MarketplaceCatalog, MarketplacePlugin, InstalledMarketplacePlugin, PluginMarketScope } from '../plugin-market-shared.ts'
|
|
11
|
+
import type { MarketplaceCatalog, MarketplacePlugin, InstalledMarketplacePlugin, PluginMarketScope, PluginProjectPick, PluginProjectDirectories } from '../plugin-market-shared.ts'
|
|
12
12
|
|
|
13
13
|
/** One wire failure. */
|
|
14
14
|
export class SidebarApiError extends Error {
|
|
@@ -288,12 +288,14 @@ function gitPayload(scope: SessionScope, worktree: string | undefined, extra: Re
|
|
|
288
288
|
|
|
289
289
|
/** The sidebar API surface (session scope threaded through every call). */
|
|
290
290
|
export const api = {
|
|
291
|
-
pluginMarketContext: (scope: SessionScope, signal?: AbortSignal) => call<{ sessionId: string; cwd
|
|
291
|
+
pluginMarketContext: (scope: SessionScope, signal?: AbortSignal) => call<{ sessionId: string; cwd?: string }>('plugins.context', scopePayload(scope, {}), signal),
|
|
292
|
+
pluginProjectPick: (signal?: AbortSignal) => call<PluginProjectPick>('plugins.project.pick', {}, signal),
|
|
293
|
+
pluginProjectDirectories: (path?: string, signal?: AbortSignal) => call<PluginProjectDirectories>('plugins.project.directories', { path }, signal),
|
|
292
294
|
pluginMarketplaces: (signal?: AbortSignal) => call<MarketplaceCatalog>('plugins.marketplaces', {}, signal),
|
|
293
295
|
pluginCatalog: (marketplaceId: string, signal?: AbortSignal) => call<MarketplacePlugin[] | { plugins: MarketplacePlugin[]; warning: string }>('plugins.catalog', { marketplaceId, includeMetadata: true }, signal),
|
|
294
|
-
pluginsInstalled: (scope: SessionScope, signal?: AbortSignal) => call<InstalledMarketplacePlugin[]>('plugins.installed', scopePayload(scope, {}), signal),
|
|
295
|
-
pluginInstall: (scope: SessionScope, pluginId: string, installScope: PluginMarketScope) => call<InstalledMarketplacePlugin>('plugins.install', scopePayload(scope, { pluginId, scope: installScope })),
|
|
296
|
-
pluginUninstall: (scope: SessionScope, pluginId: string, installScope: PluginMarketScope) => call<{ ok: true }>('plugins.uninstall', scopePayload(scope, { pluginId, scope: installScope })),
|
|
296
|
+
pluginsInstalled: (scope: SessionScope, signal?: AbortSignal, projectPath?: string) => call<InstalledMarketplacePlugin[]>('plugins.installed', scopePayload(scope, { projectPath }), signal),
|
|
297
|
+
pluginInstall: (scope: SessionScope, pluginId: string, installScope: PluginMarketScope, projectPath?: string) => call<InstalledMarketplacePlugin>('plugins.install', scopePayload(scope, { pluginId, scope: installScope, projectPath })),
|
|
298
|
+
pluginUninstall: (scope: SessionScope, pluginId: string, installScope: PluginMarketScope, projectPath?: string) => call<{ ok: true }>('plugins.uninstall', scopePayload(scope, { pluginId, scope: installScope, projectPath })),
|
|
297
299
|
chatcodeStatus: (signal?: AbortSignal) => call<ChatCodeAuthState>('chatcode.status', {}, signal),
|
|
298
300
|
chatcodeStart: () => call<{ sessionId: string; url: string }>('chatcode.start', {}),
|
|
299
301
|
chatcodeCancel: (sessionId: string) => call<{ ok: true }>('chatcode.cancel', { sessionId }),
|
package/src/client/locales-ja.ts
CHANGED
|
@@ -30,15 +30,29 @@ export const ja: Record<string, string> = {
|
|
|
30
30
|
pluginMarketUser: 'ユーザー',
|
|
31
31
|
pluginMarketInstalled: 'プラグインをインストールしました。',
|
|
32
32
|
pluginMarketUninstalled: 'プラグインをアンインストールしました。',
|
|
33
|
-
pluginMarketSubtitle: '
|
|
33
|
+
pluginMarketSubtitle: 'マーケットを参照し、プロジェクトディレクトリを選ぶかユーザー環境にインストールします。',
|
|
34
|
+
pluginMarketChooseProject: 'プロジェクトディレクトリを選択',
|
|
35
|
+
pluginMarketProjectHint: 'プロジェクトにインストールする場合は対象フォルダーを選択してください。',
|
|
36
|
+
pluginMarketChoosingDirectory: 'ディレクトリを選択中…',
|
|
37
|
+
pluginMarketInstalledAt: 'プラグインのインストール先:{path}',
|
|
38
|
+
pluginMarketInstallPath: '{scope}のインストール先:{path}',
|
|
39
|
+
pluginMarketDirectoryPath: 'ディレクトリパス',
|
|
40
|
+
pluginMarketOpenDirectory: 'ディレクトリを開く',
|
|
41
|
+
pluginMarketSelectDirectory: 'このディレクトリを選択',
|
|
42
|
+
pluginMarketHomeDirectory: 'ホーム',
|
|
43
|
+
pluginMarketDirectoryTruncated: '一部のフォルダーのみ表示しています。完全なパスを入力してプロジェクトを開けます。',
|
|
34
44
|
pluginMarketSearch: 'マーケットやプラグインを検索',
|
|
35
45
|
pluginMarketDefault: 'デフォルト',
|
|
36
46
|
pluginMarketBrowse: 'プラグインを見る',
|
|
37
47
|
pluginMarketNoDescription: '説明なし',
|
|
38
48
|
pluginMarketInstalling: 'インストール中…',
|
|
39
49
|
pluginMarketUninstalling: 'アンインストール中…',
|
|
50
|
+
pluginMarketInstallProgress: '{name} をインストールしています。しばらくお待ちください…',
|
|
51
|
+
pluginMarketUninstallProgress: '{name} をアンインストールしています。しばらくお待ちください…',
|
|
40
52
|
accountMenuSettings: '設定',
|
|
41
53
|
accountMenuLogout: 'ログアウト',
|
|
54
|
+
accountMenuLogoutConfirmTitle: 'ログアウトしますか?',
|
|
55
|
+
accountMenuLogoutConfirmDescription: 'ChatCode CLI からログアウトしますか?続行するには再度ログインする必要があります。',
|
|
42
56
|
accountMenuLabel: 'アカウントメニュー',
|
|
43
57
|
updateMenu: '更新を確認 / 今すぐ更新',
|
|
44
58
|
updateNav: '更新',
|
package/src/client/locales.ts
CHANGED
|
@@ -37,15 +37,29 @@ export const zh = {
|
|
|
37
37
|
pluginMarketUser: '用户级',
|
|
38
38
|
pluginMarketInstalled: '插件安装成功。',
|
|
39
39
|
pluginMarketUninstalled: '插件卸载成功。',
|
|
40
|
-
pluginMarketSubtitle: '
|
|
40
|
+
pluginMarketSubtitle: '浏览插件市场,选择项目目录安装,或安装到用户环境。',
|
|
41
|
+
pluginMarketChooseProject: '选择项目目录',
|
|
42
|
+
pluginMarketProjectHint: '安装到项目时,请先选择目标项目文件夹。',
|
|
43
|
+
pluginMarketChoosingDirectory: '正在选择目录…',
|
|
44
|
+
pluginMarketInstalledAt: '插件已安装到:{path}',
|
|
45
|
+
pluginMarketInstallPath: '{scope}安装目录:{path}',
|
|
46
|
+
pluginMarketDirectoryPath: '目录路径',
|
|
47
|
+
pluginMarketOpenDirectory: '打开目录',
|
|
48
|
+
pluginMarketSelectDirectory: '选择此目录',
|
|
49
|
+
pluginMarketHomeDirectory: '主目录',
|
|
50
|
+
pluginMarketDirectoryTruncated: '目录过多,仅显示部分结果;可输入完整路径打开项目。',
|
|
41
51
|
pluginMarketSearch: '搜索市场或插件',
|
|
42
52
|
pluginMarketDefault: '默认',
|
|
43
53
|
pluginMarketBrowse: '查看插件',
|
|
44
54
|
pluginMarketNoDescription: '暂无描述',
|
|
45
55
|
pluginMarketInstalling: '安装中…',
|
|
46
56
|
pluginMarketUninstalling: '卸载中…',
|
|
57
|
+
pluginMarketInstallProgress: '正在安装 {name},请稍候…',
|
|
58
|
+
pluginMarketUninstallProgress: '正在卸载 {name},请稍候…',
|
|
47
59
|
accountMenuSettings: '设置',
|
|
48
60
|
accountMenuLogout: '退出登录',
|
|
61
|
+
accountMenuLogoutConfirmTitle: '退出登录?',
|
|
62
|
+
accountMenuLogoutConfirmDescription: '确定要退出 ChatCode CLI 吗?退出后需要重新登录才能继续使用。',
|
|
49
63
|
accountMenuLabel: '账户菜单',
|
|
50
64
|
updateMenu: '检查更新 / 一键更新',
|
|
51
65
|
updateNav: '更新',
|
|
@@ -529,15 +543,29 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
529
543
|
pluginMarketUser: 'User',
|
|
530
544
|
pluginMarketInstalled: 'Plugin installed.',
|
|
531
545
|
pluginMarketUninstalled: 'Plugin uninstalled.',
|
|
532
|
-
pluginMarketSubtitle: 'Browse marketplaces
|
|
546
|
+
pluginMarketSubtitle: 'Browse marketplaces, choose a project directory, or install for your user.',
|
|
547
|
+
pluginMarketChooseProject: 'Choose project directory',
|
|
548
|
+
pluginMarketProjectHint: 'Choose a target project folder when installing for a project.',
|
|
549
|
+
pluginMarketChoosingDirectory: 'Choosing directory…',
|
|
550
|
+
pluginMarketInstalledAt: 'Plugin installed at: {path}',
|
|
551
|
+
pluginMarketInstallPath: '{scope} installation directory: {path}',
|
|
552
|
+
pluginMarketDirectoryPath: 'Directory path',
|
|
553
|
+
pluginMarketOpenDirectory: 'Open directory',
|
|
554
|
+
pluginMarketSelectDirectory: 'Select this directory',
|
|
555
|
+
pluginMarketHomeDirectory: 'Home',
|
|
556
|
+
pluginMarketDirectoryTruncated: 'Only some folders are shown. Enter the full path to open your project.',
|
|
533
557
|
pluginMarketSearch: 'Search marketplaces or plugins',
|
|
534
558
|
pluginMarketDefault: 'Default',
|
|
535
559
|
pluginMarketBrowse: 'Browse plugins',
|
|
536
560
|
pluginMarketNoDescription: 'No description',
|
|
537
561
|
pluginMarketInstalling: 'Installing…',
|
|
538
562
|
pluginMarketUninstalling: 'Uninstalling…',
|
|
563
|
+
pluginMarketInstallProgress: 'Installing {name}, please wait…',
|
|
564
|
+
pluginMarketUninstallProgress: 'Uninstalling {name}, please wait…',
|
|
539
565
|
accountMenuSettings: 'Settings',
|
|
540
566
|
accountMenuLogout: 'Log out',
|
|
567
|
+
accountMenuLogoutConfirmTitle: 'Log out?',
|
|
568
|
+
accountMenuLogoutConfirmDescription: 'Are you sure you want to log out of ChatCode CLI? You will need to sign in again to continue.',
|
|
541
569
|
accountMenuLabel: 'Account menu',
|
|
542
570
|
updateMenu: 'Check for updates / Update now',
|
|
543
571
|
updateNav: 'Update',
|
|
@@ -1034,9 +1062,44 @@ import { zhHK as zhHKDict } from './locales-zh-HK.ts'
|
|
|
1034
1062
|
import { zhTW as zhTWDict } from './locales-zh-TW.ts'
|
|
1035
1063
|
import { zhMO as zhMODict } from './locales-zh-MO.ts'
|
|
1036
1064
|
|
|
1037
|
-
// English fallback for the new account/model surfaces until third-language translations are supplied.
|
|
1065
|
+
// English fallback for the new account/model/marketplace surfaces until third-language translations are supplied.
|
|
1038
1066
|
// Keep this list explicit so the dictionary parity check still catches other missing keys.
|
|
1039
1067
|
const accountAndModelFallback = {
|
|
1068
|
+
pluginMarketTitle: en.pluginMarketTitle,
|
|
1069
|
+
pluginMarketProjectDirectory: en.pluginMarketProjectDirectory,
|
|
1070
|
+
pluginMarketInstalledTab: en.pluginMarketInstalledTab,
|
|
1071
|
+
pluginMarketRefresh: en.pluginMarketRefresh,
|
|
1072
|
+
pluginMarketLoading: en.pluginMarketLoading,
|
|
1073
|
+
pluginMarketRefreshing: en.pluginMarketRefreshing,
|
|
1074
|
+
pluginMarketEmpty: en.pluginMarketEmpty,
|
|
1075
|
+
pluginMarketBack: en.pluginMarketBack,
|
|
1076
|
+
pluginMarketInstall: en.pluginMarketInstall,
|
|
1077
|
+
pluginMarketUninstall: en.pluginMarketUninstall,
|
|
1078
|
+
pluginMarketProject: en.pluginMarketProject,
|
|
1079
|
+
pluginMarketUser: en.pluginMarketUser,
|
|
1080
|
+
pluginMarketInstalled: en.pluginMarketInstalled,
|
|
1081
|
+
pluginMarketUninstalled: en.pluginMarketUninstalled,
|
|
1082
|
+
pluginMarketSubtitle: en.pluginMarketSubtitle,
|
|
1083
|
+
pluginMarketChooseProject: en.pluginMarketChooseProject,
|
|
1084
|
+
pluginMarketProjectHint: en.pluginMarketProjectHint,
|
|
1085
|
+
pluginMarketChoosingDirectory: en.pluginMarketChoosingDirectory,
|
|
1086
|
+
pluginMarketInstalledAt: en.pluginMarketInstalledAt,
|
|
1087
|
+
pluginMarketInstallPath: en.pluginMarketInstallPath,
|
|
1088
|
+
pluginMarketDirectoryPath: en.pluginMarketDirectoryPath,
|
|
1089
|
+
pluginMarketOpenDirectory: en.pluginMarketOpenDirectory,
|
|
1090
|
+
pluginMarketSelectDirectory: en.pluginMarketSelectDirectory,
|
|
1091
|
+
pluginMarketHomeDirectory: en.pluginMarketHomeDirectory,
|
|
1092
|
+
pluginMarketDirectoryTruncated: en.pluginMarketDirectoryTruncated,
|
|
1093
|
+
pluginMarketSearch: en.pluginMarketSearch,
|
|
1094
|
+
pluginMarketDefault: en.pluginMarketDefault,
|
|
1095
|
+
pluginMarketBrowse: en.pluginMarketBrowse,
|
|
1096
|
+
pluginMarketNoDescription: en.pluginMarketNoDescription,
|
|
1097
|
+
pluginMarketInstalling: en.pluginMarketInstalling,
|
|
1098
|
+
pluginMarketUninstalling: en.pluginMarketUninstalling,
|
|
1099
|
+
pluginMarketInstallProgress: en.pluginMarketInstallProgress,
|
|
1100
|
+
pluginMarketUninstallProgress: en.pluginMarketUninstallProgress,
|
|
1101
|
+
accountMenuLogoutConfirmTitle: en.accountMenuLogoutConfirmTitle,
|
|
1102
|
+
accountMenuLogoutConfirmDescription: en.accountMenuLogoutConfirmDescription,
|
|
1040
1103
|
chatcodeGateChecking: en.chatcodeGateChecking,
|
|
1041
1104
|
chatcodeGateUnverified: en.chatcodeGateUnverified,
|
|
1042
1105
|
chatcodeGatePending: en.chatcodeGatePending,
|
|
@@ -442,13 +442,11 @@
|
|
|
442
442
|
box-sizing: border-box;
|
|
443
443
|
gap: 8px;
|
|
444
444
|
width: 100%;
|
|
445
|
-
min-height:
|
|
446
|
-
padding:
|
|
445
|
+
min-height: 44px;
|
|
446
|
+
padding: 8px 12px max(8px, env(safe-area-inset-bottom));
|
|
447
447
|
border-radius: 0;
|
|
448
448
|
background: var(--dsw-alias-bg-layer-1);
|
|
449
|
-
font
|
|
450
|
-
font-size: var(--dsw-font-size-body-sm);
|
|
451
|
-
line-height: var(--dsw-font-line-height-body-sm);
|
|
449
|
+
font: var(--dsw-font-xs-13);
|
|
452
450
|
transition: background var(--ds-transition-duration-slow) var(--ds-ease-in-out);
|
|
453
451
|
}
|
|
454
452
|
|
|
@@ -466,18 +464,21 @@
|
|
|
466
464
|
.accountAvatar {
|
|
467
465
|
position: relative;
|
|
468
466
|
display: grid;
|
|
469
|
-
|
|
470
|
-
|
|
467
|
+
box-sizing: border-box;
|
|
468
|
+
width: 24px;
|
|
469
|
+
height: 24px;
|
|
471
470
|
flex: none;
|
|
472
471
|
place-items: center;
|
|
473
472
|
border-radius: 50%;
|
|
474
473
|
background: var(--dsw-alias-bg-layer-3);
|
|
475
474
|
border: 1px solid var(--dsw-alias-border-l2);
|
|
476
475
|
color: var(--dsw-alias-fg-accent);
|
|
476
|
+
font: var(--dsw-font-xxs-12);
|
|
477
477
|
font-weight: 600;
|
|
478
478
|
}
|
|
479
479
|
|
|
480
480
|
.accountName {
|
|
481
|
+
min-width: 0;
|
|
481
482
|
overflow: hidden;
|
|
482
483
|
text-overflow: ellipsis;
|
|
483
484
|
white-space: nowrap;
|
|
@@ -490,10 +491,10 @@
|
|
|
490
491
|
left: 8px;
|
|
491
492
|
display: grid;
|
|
492
493
|
box-sizing: border-box;
|
|
493
|
-
width: min(
|
|
494
|
-
padding:
|
|
494
|
+
width: min(224px, calc(100% - 16px));
|
|
495
|
+
padding: 8px 6px 6px;
|
|
495
496
|
border: 1px solid var(--dsw-alias-border-l2);
|
|
496
|
-
border-radius:
|
|
497
|
+
border-radius: 10px;
|
|
497
498
|
background: var(--dsw-alias-bg-layer-2);
|
|
498
499
|
z-index: 1;
|
|
499
500
|
}
|
|
@@ -502,16 +503,15 @@
|
|
|
502
503
|
display: flex;
|
|
503
504
|
align-items: center;
|
|
504
505
|
gap: 8px;
|
|
505
|
-
min-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
font
|
|
509
|
-
line-height: var(--dsw-font-line-height-body-sm);
|
|
506
|
+
min-width: 0;
|
|
507
|
+
min-height: 28px;
|
|
508
|
+
padding: 0 6px 6px;
|
|
509
|
+
font: var(--dsw-font-xs-13);
|
|
510
510
|
}
|
|
511
511
|
|
|
512
512
|
.accountMenuDivider {
|
|
513
513
|
height: 1px;
|
|
514
|
-
margin: 0 4px
|
|
514
|
+
margin: 0 4px 4px;
|
|
515
515
|
background: var(--dsw-alias-border-l2);
|
|
516
516
|
}
|
|
517
517
|
|
|
@@ -519,12 +519,12 @@
|
|
|
519
519
|
display: flex;
|
|
520
520
|
align-items: center;
|
|
521
521
|
gap: 8px;
|
|
522
|
-
min-height:
|
|
523
|
-
padding: 0
|
|
522
|
+
min-height: 30px;
|
|
523
|
+
padding: 0 8px;
|
|
524
524
|
border-radius: 7px;
|
|
525
525
|
background: transparent;
|
|
526
526
|
text-align: left;
|
|
527
|
-
font: var(--dsw-font-
|
|
527
|
+
font: var(--dsw-font-xs-13);
|
|
528
528
|
}
|
|
529
529
|
|
|
530
530
|
.accountItem:hover {
|
|
@@ -546,6 +546,68 @@
|
|
|
546
546
|
border-color: var(--dsw-alias-bg-layer-1);
|
|
547
547
|
}
|
|
548
548
|
|
|
549
|
+
/* The native top layer is viewport-centered even though the account rail is positioned. */
|
|
550
|
+
.accountLogoutDialog {
|
|
551
|
+
box-sizing: border-box;
|
|
552
|
+
width: min(400px, calc(100vw - 32px));
|
|
553
|
+
max-width: calc(100vw - 32px);
|
|
554
|
+
max-height: calc(100dvh - 32px);
|
|
555
|
+
margin: auto;
|
|
556
|
+
padding: 24px;
|
|
557
|
+
overflow: auto;
|
|
558
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
559
|
+
border-radius: 16px;
|
|
560
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
561
|
+
color: var(--dsw-alias-fg-base);
|
|
562
|
+
font: var(--dsw-font-s-14);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
.accountLogoutDialog::backdrop {
|
|
566
|
+
background: var(--dsw-alias-bg-mask-1);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.accountLogoutTitle {
|
|
570
|
+
margin: 0;
|
|
571
|
+
font: var(--dsw-font-base-strong-16);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
.accountLogoutDescription {
|
|
575
|
+
margin: 12px 0 24px;
|
|
576
|
+
color: var(--dsw-alias-label-secondary);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
.accountLogoutActions {
|
|
580
|
+
display: flex;
|
|
581
|
+
flex-wrap: wrap;
|
|
582
|
+
justify-content: flex-end;
|
|
583
|
+
gap: 8px;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
.accountLogoutActions button {
|
|
587
|
+
min-height: 34px;
|
|
588
|
+
padding: 6px 14px;
|
|
589
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
590
|
+
border-radius: 8px;
|
|
591
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
592
|
+
color: var(--dsw-alias-fg-base);
|
|
593
|
+
font: var(--dsw-font-xs-13);
|
|
594
|
+
cursor: pointer;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
.accountLogoutActions button:hover {
|
|
598
|
+
background: var(--dsw-alias-bg-hover);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
.accountLogoutActions button:focus-visible {
|
|
602
|
+
outline: 2px solid var(--dsw-alias-border-accent);
|
|
603
|
+
outline-offset: 2px;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
.accountLogoutActions .accountLogoutConfirm {
|
|
607
|
+
border-color: var(--dsw-alias-state-error-primary);
|
|
608
|
+
color: var(--dsw-alias-state-error-primary);
|
|
609
|
+
}
|
|
610
|
+
|
|
549
611
|
/* The compatibility action cluster used when the host exposes no native
|
|
550
612
|
right-sidebar affordance. It stays pinned to the viewport corner, sitting
|
|
551
613
|
flush in the 34px tab strip (28px buttons at top: 3) whose right end it
|