dsh-skill-hub 0.2.6 → 0.3.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/README.md +68 -229
- package/README.zh.md +67 -203
- package/lib/client.js +1955 -413
- package/lib/client.js.map +1 -1
- package/lib/index.js +441 -84
- package/lib/types/client/SkillHubSettingsCard.d.ts +1 -1
- package/lib/types/client/api.d.ts +12 -2
- package/lib/types/client/index.d.ts +6 -1
- package/lib/types/client/locales.d.ts +13 -0
- package/lib/types/client/panel/ScenesView.d.ts +1 -1
- package/lib/types/client/panel/SourcesView.d.ts +1 -1
- package/lib/types/client/panel/useSkillHub.d.ts +20 -2
- package/lib/types/client/settings-form.d.ts +1 -1
- package/lib/types/client/slash-dots.d.ts +2 -1
- package/lib/types/index.d.ts +2 -1
- package/lib/types/protocol.d.ts +93 -6
- package/lib/types/repo.d.ts +18 -11
- package/lib/types/store.d.ts +14 -2
- package/package.json +38 -29
- package/src/client/SkillHubSettingsCard.tsx +1 -1
- package/src/client/api.ts +61 -1
- package/src/client/index.tsx +10 -1
- package/src/client/locales.ts +26 -0
- package/src/client/panel/MarketView.tsx +201 -47
- package/src/client/panel/ScenesView.tsx +27 -2
- package/src/client/panel/SkillHubPanel.tsx +18 -19
- package/src/client/panel/SkillRow.tsx +8 -8
- package/src/client/panel/SourcesView.tsx +203 -26
- package/src/client/panel/panel.module.css +17 -6
- package/src/client/panel/useSkillHub.ts +219 -12
- package/src/client/settings-form.ts +2 -1
- package/src/client/slash-dots.test.ts +24 -6
- package/src/client/slash-dots.tsx +10 -1
- package/src/index.ts +20 -2
- package/src/protocol.ts +89 -6
- package/src/repo.test.ts +61 -19
- package/src/repo.ts +86 -23
- package/src/routes.test.ts +18 -4
- package/src/routes.ts +269 -47
- package/src/skillfs.ts +17 -4
- package/src/store.ts +72 -4
|
@@ -24,7 +24,6 @@ import { TagEditorView } from './TagEditorView.tsx'
|
|
|
24
24
|
import { SourcesView } from './SourcesView.tsx'
|
|
25
25
|
import { ScenesView } from './ScenesView.tsx'
|
|
26
26
|
import { MarketView } from './MarketView.tsx'
|
|
27
|
-
import { DisabledRow } from './DisabledRow.tsx'
|
|
28
27
|
import { BranchChoiceDialog, ConfirmDialog, ConflictDialog, MarketSyncDialog } from './dialogs.tsx'
|
|
29
28
|
import { useSkillHub } from './useSkillHub.ts'
|
|
30
29
|
import css from './panel.module.css'
|
|
@@ -42,14 +41,14 @@ export function SkillHubPanel(props: SkillHubPanelProps): React.JSX.Element {
|
|
|
42
41
|
formRoot, formBusy, formMessage, hubConfig, tab, skillView, sourceFilter, sortKey, search,
|
|
43
42
|
workspace, setWorkspace,
|
|
44
43
|
sourcesState, tagBusy, busyNames, normalized, origins, sourceOptions, filtered,
|
|
45
|
-
conflictDialog, confirmDialog, deleteSkillDialog, confirmClearTrash, branchChoice, branchBusy, marketSyncDialog,
|
|
44
|
+
conflictDialog, confirmDialog, deleteSkillDialog, deleteGroupDialog, confirmClearTrash, branchChoice, branchBusy, marketSyncDialog,
|
|
46
45
|
syncBusy, editingTag, editName, membersDraft, editSearch, uses, groupsState, sourceCheck, checkingSource, syncingSource,
|
|
47
|
-
showLegend,
|
|
46
|
+
showLegend, editMode,
|
|
48
47
|
setLoadError, setSuccessBanner, setDetail, setShowForm, setFormName, setFormDesc, setFormRoot, setFormMessage, setTab,
|
|
49
|
-
setSkillView, setSourceFilter, setSortKey, setSearch, setConflictDialog, setConfirmDialog, setDeleteSkillDialog,
|
|
48
|
+
setSkillView, setSourceFilter, setSortKey, setSearch, setConflictDialog, setConfirmDialog, setDeleteSkillDialog, setDeleteGroupDialog,
|
|
50
49
|
setConfirmClearTrash, setBranchChoice, setMarketSyncDialog, setEditingTag, setEditName, setMembersDraft, setEditSearch,
|
|
51
|
-
setShowLegend,
|
|
52
|
-
checkUpdate, loadMarket, checkSources, requestSync, requestDelete, restoreTrash, clearTrash, runDeleteSkill,
|
|
50
|
+
setShowLegend, setEditMode,
|
|
51
|
+
checkUpdate, loadMarket, checkSources, requestSync, requestDelete, restoreTrash, clearTrash, runDeleteSkill, runDeleteGroup,
|
|
53
52
|
runConfirmed, resolveConflict, confirmBranchChoice, confirmMarketSync, create, saveTag, deleteTag, enableDisabled,
|
|
54
53
|
} = hub
|
|
55
54
|
|
|
@@ -236,6 +235,7 @@ export function SkillHubPanel(props: SkillHubPanelProps): React.JSX.Element {
|
|
|
236
235
|
<option value='uses'>{tt('sort.uses')}</option>
|
|
237
236
|
</select>
|
|
238
237
|
<input className={css.search} value={search} onChange={(event) => { setSearch(event.target.value) }} placeholder={tt('panel.search')} />
|
|
238
|
+
<button type='button' className={css.button + (editMode ? ' ' + css.primary : '')} style={{ marginLeft:'auto' }} onClick={() => setEditMode((v) => !v)}>{editMode ? '完成' : '编辑'}</button>
|
|
239
239
|
</div>
|
|
240
240
|
|
|
241
241
|
{filtered.length === 0 && search.trim() !== '' ? <div className={css.empty}>{tt('panel.empty')}</div> : null}
|
|
@@ -263,19 +263,6 @@ export function SkillHubPanel(props: SkillHubPanelProps): React.JSX.Element {
|
|
|
263
263
|
</section>
|
|
264
264
|
) : null}
|
|
265
265
|
|
|
266
|
-
{catalog.disabled.length > 0 ? (
|
|
267
|
-
<section className={css.section}>
|
|
268
|
-
<div className={css.sectionTitle}>{tt('panel.disabled')}</div>
|
|
269
|
-
{catalog.disabled
|
|
270
|
-
.filter((record) =>
|
|
271
|
-
(normalized.length === 0 || record.name.toLocaleLowerCase().includes(normalized) || record.description.toLocaleLowerCase().includes(normalized))
|
|
272
|
-
&& (sourceFilter === 'all' || (origins[record.name] ?? PRIVATE_SOURCE) === sourceFilter))
|
|
273
|
-
.map((record) => (
|
|
274
|
-
<DisabledRow key={record.name} record={record} busy={busyNames.has(record.name)} onEnable={() => { void enableDisabled(record) }} />
|
|
275
|
-
))}
|
|
276
|
-
</section>
|
|
277
|
-
) : null}
|
|
278
|
-
|
|
279
266
|
{catalog.diagnostics.length > 0 ? (
|
|
280
267
|
<section className={css.section}>
|
|
281
268
|
<div className={css.sectionTitle}>{tt('panel.diagnostics')}</div>
|
|
@@ -349,6 +336,18 @@ export function SkillHubPanel(props: SkillHubPanelProps): React.JSX.Element {
|
|
|
349
336
|
/>
|
|
350
337
|
) : null}
|
|
351
338
|
|
|
339
|
+
{deleteGroupDialog !== null ? (
|
|
340
|
+
<ConfirmDialog
|
|
341
|
+
title={tt('source.deleteGroupTitle')}
|
|
342
|
+
text={tt('source.deleteGroupText', { name: deleteGroupDialog.name, count: deleteGroupDialog.skillNames.length })}
|
|
343
|
+
items={deleteGroupDialog.skillNames}
|
|
344
|
+
confirmLabel={tt('source.deleteGroup')}
|
|
345
|
+
danger
|
|
346
|
+
onCancel={() => { setDeleteGroupDialog(null) }}
|
|
347
|
+
onConfirm={() => { void runDeleteGroup() }}
|
|
348
|
+
/>
|
|
349
|
+
) : null}
|
|
350
|
+
|
|
352
351
|
{confirmClearTrash ? (
|
|
353
352
|
<ConfirmDialog
|
|
354
353
|
title={tt('source.clearTrashConfirmTitle')}
|
|
@@ -50,14 +50,6 @@ export function SkillRow(props: { skill: CatalogSkill; hub: SkillHubState }): JS
|
|
|
50
50
|
</div>
|
|
51
51
|
{skill.writable
|
|
52
52
|
? <>
|
|
53
|
-
<button
|
|
54
|
-
type='button'
|
|
55
|
-
className={css.opBtn + ' ' + css.opDanger + ' ' + css.iconBtn}
|
|
56
|
-
disabled={hub.busyNames.has(skill.name) || hub.tagBusy}
|
|
57
|
-
title={tt('row.delete')}
|
|
58
|
-
aria-label={tt('row.delete')}
|
|
59
|
-
onClick={(event) => { event.stopPropagation(); hub.requestDeleteSkill(skill.name) }}
|
|
60
|
-
><IconTrashOutline16 size={14} /></button>
|
|
61
53
|
<button
|
|
62
54
|
type='button'
|
|
63
55
|
role='switch'
|
|
@@ -67,6 +59,14 @@ export function SkillRow(props: { skill: CatalogSkill; hub: SkillHubState }): JS
|
|
|
67
59
|
disabled={hub.busyNames.has(skill.name)}
|
|
68
60
|
onClick={(event) => { event.stopPropagation(); void hub.toggle(skill, false) }}
|
|
69
61
|
><span className={css.switchThumb} /></button>
|
|
62
|
+
{hub.editMode ? <button
|
|
63
|
+
type='button'
|
|
64
|
+
className={css.opBtn + ' ' + css.opDanger + ' ' + css.iconBtn}
|
|
65
|
+
disabled={hub.busyNames.has(skill.name) || hub.tagBusy}
|
|
66
|
+
title={tt('row.delete')}
|
|
67
|
+
aria-label={tt('row.delete')}
|
|
68
|
+
onClick={(event) => { event.stopPropagation(); hub.requestDeleteSkill(skill.name) }}
|
|
69
|
+
><IconTrashOutline16 size={14} /></button> : null}
|
|
70
70
|
</>
|
|
71
71
|
: <span className={css.badge + ' ' + css.badgeReadonly}>{tt('row.readonly')}</span>}
|
|
72
72
|
</div>
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* "personal" card (project skills never count as personal).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type
|
|
9
|
+
import { useState, type JSX } from 'react'
|
|
10
10
|
import { tt } from '../helpers.ts'
|
|
11
11
|
import { filterBySource, groupSwitchView, isProjectSource, PRIVATE_SOURCE } from '../grouping.ts'
|
|
12
12
|
import { SourceStatusBadge } from './SourceStatusBadge.tsx'
|
|
@@ -101,17 +101,151 @@ function ProjectTree(props: { hub: SkillHubState }): JSX.Element | null {
|
|
|
101
101
|
|
|
102
102
|
export function SourcesView(props: { hub: SkillHubState }): JSX.Element {
|
|
103
103
|
const { hub } = props
|
|
104
|
-
const { catalog, groupsState, skillView, sourceFilter, origins, sorted, normalized, collapsedGroups, viewNames, sourceCheck, actionNames, checkingSource, syncingSource, batchBusy, busyNames, toggleGroupCollapse, checkSources, requestSync, requestDelete, toggleGroup, enableDisabled } = hub
|
|
104
|
+
const { catalog, groupsState, skillView, sourceFilter, origins, sorted, normalized, collapsedGroups, viewNames, sourceCheck, actionNames, checkingSource, syncingSource, batchBusy, busyNames, toggleGroupCollapse, checkSources, requestSync, requestDelete, requestDeleteGroup, toggleGroup, enableDisabled } = hub
|
|
105
|
+
const [topDragKey, setTopDragKey] = useState<string | null>(null)
|
|
106
|
+
const [topOverKey, setTopOverKey] = useState<string | null>(null)
|
|
105
107
|
|
|
106
108
|
if (skillView === 'flat') {
|
|
107
109
|
return <>{filterBySource(sorted, sourceFilter, origins).map((skill) => <SkillRow key={skill.name} skill={skill} hub={hub} />)}</>
|
|
108
110
|
}
|
|
109
111
|
|
|
112
|
+
// ----- 顶层分组统一拖拽(project / col:xxx / personal 全部可拖) -----
|
|
113
|
+
const projectSkillsAll = filterBySource(sorted, sourceFilter, origins).filter((skill) => isProjectSource(skill.source))
|
|
114
|
+
const hasProject = projectSkillsAll.length > 0
|
|
115
|
+
const collections = groupsState?.collections ?? []
|
|
116
|
+
const uncategorized = filterBySource(sorted, sourceFilter, origins).filter((skill) => origins[skill.name] === undefined && !isProjectSource(skill.source))
|
|
117
|
+
const personalDisabledAll = (catalog?.disabled ?? []).filter((record) => origins[record.name] === undefined)
|
|
118
|
+
.filter((record) => normalized.length === 0 || record.name.toLocaleLowerCase().includes(normalized) || record.description.toLocaleLowerCase().includes(normalized))
|
|
119
|
+
.filter((record) => sourceFilter === 'all' || sourceFilter === PRIVATE_SOURCE)
|
|
120
|
+
const allPersonalNamesAll = [...uncategorized.map((s) => s.name), ...personalDisabledAll.map((r) => r.name)]
|
|
121
|
+
const hasPersonal = allPersonalNamesAll.length > 0
|
|
122
|
+
const defaultTopKeys: string[] = [
|
|
123
|
+
...(hasProject ? ['project'] : []),
|
|
124
|
+
...collections.map((c) => 'col:' + c.name),
|
|
125
|
+
...(hasPersonal ? ['uncategorized-source'] : []),
|
|
126
|
+
]
|
|
127
|
+
const storedTopOrder = groupsState?.sourceGroupOrder ?? []
|
|
128
|
+
const topOrderedKeys = (() => {
|
|
129
|
+
if (storedTopOrder.length === 0) return defaultTopKeys
|
|
130
|
+
const set = new Set(storedTopOrder)
|
|
131
|
+
const result = storedTopOrder.filter((k) => defaultTopKeys.includes(k))
|
|
132
|
+
for (const k of defaultTopKeys) if (!set.has(k)) result.push(k)
|
|
133
|
+
// 兼容旧 collectionOrder:若 storedTopOrder 为空但旧 order 有值,已在 defaultTopKeys 中体现 collection 顺序
|
|
134
|
+
if (result.length === 0) return defaultTopKeys
|
|
135
|
+
return result
|
|
136
|
+
})()
|
|
137
|
+
const handleTopDrop = (targetKey: string): void => {
|
|
138
|
+
if (topDragKey === null || topDragKey === targetKey) return
|
|
139
|
+
const from = topOrderedKeys.indexOf(topDragKey)
|
|
140
|
+
const to = topOrderedKeys.indexOf(targetKey)
|
|
141
|
+
if (from === -1 || to === -1) return
|
|
142
|
+
const next = [...topOrderedKeys]
|
|
143
|
+
const [moved] = next.splice(from, 1)
|
|
144
|
+
next.splice(to, 0, moved)
|
|
145
|
+
void hub.reorderSourceGroups(next)
|
|
146
|
+
}
|
|
147
|
+
// 兼容旧逻辑:若用户拖的是 collection 且未使用顶层排序,回退到 collectionReorder(保留旧数据兼容)
|
|
148
|
+
const handleCollectionDropLegacy = (targetName: string): void => {
|
|
149
|
+
// 仅当顶层排序未启用时(storedTopOrder 为空)才用旧接口
|
|
150
|
+
if ((groupsState?.sourceGroupOrder?.length ?? 0) > 0) { handleTopDrop('col:' + targetName); return }
|
|
151
|
+
const dragCol = topDragKey !== null && topDragKey.startsWith('col:') ? topDragKey.slice(4) : null
|
|
152
|
+
if (dragCol === null || dragCol === targetName || groupsState === null) return
|
|
153
|
+
const names = groupsState.collections.map((c) => c.name)
|
|
154
|
+
const from = names.indexOf(dragCol)
|
|
155
|
+
const to = names.indexOf(targetName)
|
|
156
|
+
if (from === -1 || to === -1) return
|
|
157
|
+
const next = [...names]
|
|
158
|
+
const [moved] = next.splice(from, 1)
|
|
159
|
+
next.splice(to, 0, moved)
|
|
160
|
+
void hub.reorderCollections(next)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 空状态:没有任何分组时提示
|
|
164
|
+
const isEmptyTop = !hasProject && collections.length === 0 && !hasPersonal
|
|
110
165
|
return (
|
|
111
166
|
<>
|
|
112
|
-
<
|
|
113
|
-
{
|
|
114
|
-
|
|
167
|
+
{isEmptyTop ? <div className={css.empty}>{tt('groups.noCollections')}</div> : null}
|
|
168
|
+
{topOrderedKeys.map((topKey) => {
|
|
169
|
+
// Project 顶层卡片(可拖)
|
|
170
|
+
if (topKey === 'project' && hasProject) {
|
|
171
|
+
const topCollapsed = collapsedGroups.has('project')
|
|
172
|
+
const isDragging = topDragKey === 'project'
|
|
173
|
+
const isOver = topOverKey === 'project' && topDragKey !== 'project'
|
|
174
|
+
// 按 workspace 聚合,与 ProjectTree 逻辑一致
|
|
175
|
+
const byProject = new Map<string, { title: string; skills: typeof projectSkillsAll }>()
|
|
176
|
+
for (const skill of projectSkillsAll) {
|
|
177
|
+
const key = skill.workspace ?? skill.source
|
|
178
|
+
const entry = byProject.get(key)
|
|
179
|
+
if (entry === undefined) byProject.set(key, { title: skill.workspaceTitle ?? skill.workspace ?? tt('groups.project'), skills: [skill] })
|
|
180
|
+
else entry.skills.push(skill)
|
|
181
|
+
}
|
|
182
|
+
return (
|
|
183
|
+
<section
|
|
184
|
+
key="project"
|
|
185
|
+
className={css.section + (isDragging ? ' ' + css.dragging : '') + (isOver ? ' ' + css.dragOver : '')}
|
|
186
|
+
draggable
|
|
187
|
+
onDragStart={(e) => { setTopDragKey('project'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', 'project') }}
|
|
188
|
+
onDragOver={(e) => { e.preventDefault(); if (topOverKey !== 'project') setTopOverKey('project') }}
|
|
189
|
+
onDragLeave={() => { if (topOverKey === 'project') setTopOverKey(null) }}
|
|
190
|
+
onDrop={(e) => { e.preventDefault(); handleTopDrop('project'); setTopOverKey(null) }}
|
|
191
|
+
onDragEnd={() => { setTopDragKey(null); setTopOverKey(null) }}
|
|
192
|
+
>
|
|
193
|
+
<div className={css.groupHead}>
|
|
194
|
+
<span className={css.dragHandle} aria-hidden title="拖拽调整顺序">⋮⋮</span>
|
|
195
|
+
<button type='button' className={css.disclosure} aria-expanded={!topCollapsed} onClick={() => { toggleGroupCollapse('project') }}>
|
|
196
|
+
<span className={css.chevron + (topCollapsed ? ' ' + css.chevronCollapsed : '')} />
|
|
197
|
+
<span className={css.groupTitle}>{tt('groups.project')} · {byProject.size}</span>
|
|
198
|
+
</button>
|
|
199
|
+
</div>
|
|
200
|
+
{!topCollapsed ? [...byProject.entries()].map(([key, proj]) => {
|
|
201
|
+
const projKey = 'project:' + key
|
|
202
|
+
const projCollapsed = collapsedGroups.has(projKey)
|
|
203
|
+
const subdivided = hub.subdividedProjects.has(key)
|
|
204
|
+
return (
|
|
205
|
+
<div key={projKey} className={css.projectNest}>
|
|
206
|
+
<div className={css.groupHead}>
|
|
207
|
+
<button type='button' className={css.disclosure} aria-expanded={!projCollapsed} onClick={() => { toggleGroupCollapse(projKey) }}>
|
|
208
|
+
<span className={css.chevron + (projCollapsed ? ' ' + css.chevronCollapsed : '')} />
|
|
209
|
+
<span className={css.groupTitle}>{proj.title} · {proj.skills.length}<GroupSummary members={proj.skills.map((s) => s.name)} hub={hub} /></span>
|
|
210
|
+
</button>
|
|
211
|
+
<span className={css.groupOps}>
|
|
212
|
+
<button type='button' className={css.opBtn} onClick={(event) => { event.stopPropagation(); hub.toggleSubdivide(key) }}>{subdivided ? tt('groups.merge') : tt('groups.subdivide')}</button>
|
|
213
|
+
</span>
|
|
214
|
+
</div>
|
|
215
|
+
{!projCollapsed ? (
|
|
216
|
+
subdivided ? (
|
|
217
|
+
<div className={css.projectNest}>
|
|
218
|
+
{(['project-dsh', 'project-agents'] as const).map((source) => {
|
|
219
|
+
const list = proj.skills.filter((skill) => skill.source === source)
|
|
220
|
+
if (list.length === 0) return null
|
|
221
|
+
const srcKey = projKey + ':' + source
|
|
222
|
+
const srcCollapsed = collapsedGroups.has(srcKey)
|
|
223
|
+
return (
|
|
224
|
+
<div key={srcKey} className={css.projectNest}>
|
|
225
|
+
<div className={css.groupHead}>
|
|
226
|
+
<button type='button' className={css.disclosure} aria-expanded={!srcCollapsed} onClick={() => { toggleGroupCollapse(srcKey) }}>
|
|
227
|
+
<span className={css.chevron + (srcCollapsed ? ' ' + css.chevronCollapsed : '')} />
|
|
228
|
+
<span className={css.groupTitle}>{tt(('badge.source.' + source) as 'badge.source.project-dsh' | 'badge.source.project-agents')} · {list.length}</span>
|
|
229
|
+
</button>
|
|
230
|
+
</div>
|
|
231
|
+
{!srcCollapsed ? list.map((skill) => <SkillRow key={skill.name} skill={skill} hub={hub} />) : null}
|
|
232
|
+
</div>
|
|
233
|
+
)
|
|
234
|
+
})}
|
|
235
|
+
</div>
|
|
236
|
+
) : proj.skills.map((skill) => <SkillRow key={skill.name} skill={skill} hub={hub} />)
|
|
237
|
+
) : null}
|
|
238
|
+
</div>
|
|
239
|
+
)
|
|
240
|
+
}) : null}
|
|
241
|
+
</section>
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
// Collection 卡片(可拖,归属顶层排序)
|
|
245
|
+
if (topKey.startsWith('col:')) {
|
|
246
|
+
const colName = topKey.slice(4)
|
|
247
|
+
const collection = collections.find((c) => c.name === colName)
|
|
248
|
+
if (collection === undefined) return null
|
|
115
249
|
const skills = filterBySource(sorted, sourceFilter, origins).filter((skill) => collection.skillNames.includes(skill.name))
|
|
116
250
|
const disabledMembers = (catalog?.disabled ?? []).filter((record) =>
|
|
117
251
|
collection.skillNames.includes(record.name)
|
|
@@ -121,9 +255,21 @@ export function SourcesView(props: { hub: SkillHubState }): JSX.Element {
|
|
|
121
255
|
const view = groupSwitchView(collection.skillNames, viewNames)
|
|
122
256
|
const check = sourceCheck[collection.name]
|
|
123
257
|
const hasWritable = collection.skillNames.some((name) => actionNames.has(name))
|
|
258
|
+
const isDragging = topDragKey === topKey
|
|
259
|
+
const isOver = topOverKey === topKey && topDragKey !== topKey
|
|
124
260
|
return (
|
|
125
|
-
<section
|
|
261
|
+
<section
|
|
262
|
+
key={'col:' + collection.name}
|
|
263
|
+
className={css.section + (isDragging ? ' ' + css.dragging : '') + (isOver ? ' ' + css.dragOver : '')}
|
|
264
|
+
draggable
|
|
265
|
+
onDragStart={(e) => { setTopDragKey(topKey); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', topKey) }}
|
|
266
|
+
onDragOver={(e) => { e.preventDefault(); if (topOverKey !== topKey) setTopOverKey(topKey) }}
|
|
267
|
+
onDragLeave={() => { if (topOverKey === topKey) setTopOverKey(null) }}
|
|
268
|
+
onDrop={(e) => { e.preventDefault(); handleTopDrop(topKey); setTopOverKey(null) }}
|
|
269
|
+
onDragEnd={() => { setTopDragKey(null); setTopOverKey(null) }}
|
|
270
|
+
>
|
|
126
271
|
<div className={css.groupHead}>
|
|
272
|
+
<span className={css.dragHandle} aria-hidden title="拖拽调整顺序">⋮⋮</span>
|
|
127
273
|
<button type='button' className={css.disclosure} aria-expanded={!collapsed} onClick={() => { toggleGroupCollapse('col:' + collection.name) }}>
|
|
128
274
|
<span className={css.chevron + (collapsed ? ' ' + css.chevronCollapsed : '')} />
|
|
129
275
|
<span className={css.groupTitle}>
|
|
@@ -153,6 +299,14 @@ export function SourcesView(props: { hub: SkillHubState }): JSX.Element {
|
|
|
153
299
|
onClick={(event) => { event.stopPropagation(); toggleGroup('col:' + collection.name, collection.name, view.state) }}>
|
|
154
300
|
<span className={css.switchThumb} />
|
|
155
301
|
</button>
|
|
302
|
+
{hub.editMode ? <button
|
|
303
|
+
type='button'
|
|
304
|
+
className={css.opBtn + ' ' + css.opDanger}
|
|
305
|
+
title={tt('source.deleteGroupHint', { count: collection.skillNames.length })}
|
|
306
|
+
onClick={(event) => { event.stopPropagation(); requestDeleteGroup(collection.name, collection.skillNames) }}
|
|
307
|
+
>
|
|
308
|
+
{tt('source.deleteGroup')}
|
|
309
|
+
</button> : null}
|
|
156
310
|
</span>
|
|
157
311
|
</div>
|
|
158
312
|
{!collapsed ? (
|
|
@@ -165,27 +319,50 @@ export function SourcesView(props: { hub: SkillHubState }): JSX.Element {
|
|
|
165
319
|
) : null}
|
|
166
320
|
</section>
|
|
167
321
|
)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
322
|
+
}
|
|
323
|
+
// Personal 顶层卡片(可拖)
|
|
324
|
+
if (topKey === 'uncategorized-source' && hasPersonal) {
|
|
325
|
+
const uncategorized = filterBySource(sorted, sourceFilter, origins).filter((skill) => origins[skill.name] === undefined && !isProjectSource(skill.source))
|
|
326
|
+
const personalDisabled = (catalog?.disabled ?? []).filter((record) => origins[record.name] === undefined)
|
|
327
|
+
.filter((record) => normalized.length === 0 || record.name.toLocaleLowerCase().includes(normalized) || record.description.toLocaleLowerCase().includes(normalized))
|
|
328
|
+
.filter((record) => sourceFilter === 'all' || sourceFilter === PRIVATE_SOURCE)
|
|
329
|
+
const allPersonalNames = [...uncategorized.map((s) => s.name), ...personalDisabled.map((r) => r.name)]
|
|
330
|
+
if (allPersonalNames.length === 0) return null
|
|
331
|
+
const collapsed = collapsedGroups.has('uncategorized-source')
|
|
332
|
+
const isDragging = topDragKey === 'uncategorized-source'
|
|
333
|
+
const isOver = topOverKey === 'uncategorized-source' && topDragKey !== 'uncategorized-source'
|
|
334
|
+
return (
|
|
335
|
+
<section
|
|
336
|
+
key="uncategorized-source"
|
|
337
|
+
className={css.section + (isDragging ? ' ' + css.dragging : '') + (isOver ? ' ' + css.dragOver : '')}
|
|
338
|
+
draggable
|
|
339
|
+
onDragStart={(e) => { setTopDragKey('uncategorized-source'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', 'uncategorized-source') }}
|
|
340
|
+
onDragOver={(e) => { e.preventDefault(); if (topOverKey !== 'uncategorized-source') setTopOverKey('uncategorized-source') }}
|
|
341
|
+
onDragLeave={() => { if (topOverKey === 'uncategorized-source') setTopOverKey(null) }}
|
|
342
|
+
onDrop={(e) => { e.preventDefault(); handleTopDrop('uncategorized-source'); setTopOverKey(null) }}
|
|
343
|
+
onDragEnd={() => { setTopDragKey(null); setTopOverKey(null) }}
|
|
344
|
+
>
|
|
345
|
+
<div className={css.groupHead}>
|
|
346
|
+
<span className={css.dragHandle} aria-hidden title="拖拽调整顺序">⋮⋮</span>
|
|
347
|
+
<button type='button' className={css.disclosure} aria-expanded={!collapsed} onClick={() => { toggleGroupCollapse('uncategorized-source') }}>
|
|
348
|
+
<span className={css.chevron + (collapsed ? ' ' + css.chevronCollapsed : '')} />
|
|
349
|
+
<span className={css.groupTitle}>{tt('groups.personal')} · {allPersonalNames.length}<GroupSummary members={allPersonalNames} hub={hub} /></span>
|
|
350
|
+
</button>
|
|
351
|
+
<span className={css.groupOps}>
|
|
352
|
+
{hub.editMode ? <button type='button' className={css.opBtn + ' ' + css.opDanger} title={tt('source.deleteGroupHint', { count: allPersonalNames.length })} onClick={(event) => { event.stopPropagation(); requestDeleteGroup(tt('groups.personal'), allPersonalNames) }}>{tt('source.deleteGroup')}</button> : null}
|
|
182
353
|
</span>
|
|
183
|
-
</
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
354
|
+
</div>
|
|
355
|
+
{!collapsed ? (
|
|
356
|
+
<>
|
|
357
|
+
{uncategorized.map((skill) => <SkillRow key={skill.name} skill={skill} hub={hub} />)}
|
|
358
|
+
{personalDisabled.map((record) => (<DisabledRow key={record.name} record={record} busy={busyNames.has(record.name)} onEnable={() => { void enableDisabled(record) }} />))}
|
|
359
|
+
</>
|
|
360
|
+
) : null}
|
|
361
|
+
</section>
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
return null
|
|
365
|
+
})}
|
|
189
366
|
</>
|
|
190
367
|
)
|
|
191
368
|
}
|
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
/* Inset grouped list: one rounded card per group. */
|
|
23
23
|
.section { display: flex; flex-direction: column; background: rgba(128,128,128,.06); border-radius: 12px; overflow: hidden; margin-top: 4px; box-shadow: 0 1px 2px rgba(0,0,0,.03), 0 4px 12px rgba(0,0,0,.04); }
|
|
24
24
|
.sectionTitle { font-size: 13px; font-weight: 700; margin: 14px 16px 2px; }
|
|
25
|
-
.groupHead { display: flex; align-items: center; gap: 8px; padding:
|
|
26
|
-
.groupTitle { font-size: 13px; font-weight: 600; opacity: .92; flex: 1; min-width: 0; }
|
|
27
|
-
.groupOps { flex: none; display: inline-flex; gap: 6px; }
|
|
25
|
+
.groupHead { display: flex; align-items: center; gap: 8px; padding: 10px 14px; min-height: 44px; box-sizing: border-box; }
|
|
26
|
+
.groupTitle { font-size: 13px; font-weight: 600; opacity: .92; flex: 1; min-width: 0; display: inline-flex; align-items: center; gap: 6px; line-height: 1; }
|
|
27
|
+
.groupOps { flex: none; display: inline-flex; gap: 6px; align-items: center; }
|
|
28
28
|
|
|
29
29
|
/* Small neutral pill buttons (group-level actions). */
|
|
30
30
|
.opBtn { font-size: 11px; padding: 3px 11px; border-radius: 999px; border: none; background: rgba(128,128,128,.13); color: inherit; cursor: pointer; transition: background .15s ease; }
|
|
@@ -113,10 +113,10 @@
|
|
|
113
113
|
.badgeDisabled { background: rgba(128,128,128,.1); border: none; opacity: .58; }
|
|
114
114
|
|
|
115
115
|
/* Collapsible drawer headers (groups view). */
|
|
116
|
-
.disclosure { flex: 1; min-width: 0; display: flex; align-items: center; gap: 7px; padding: 2px 0; border: none; background: none; color: inherit; cursor: pointer; text-align: left; }
|
|
116
|
+
.disclosure { flex: 1; min-width: 0; display: flex; align-items: center; gap: 7px; padding: 2px 0; border: none; background: none; color: inherit; cursor: pointer; text-align: left; min-height: 24px; }
|
|
117
117
|
.disclosure:hover .groupTitle { opacity: 1; }
|
|
118
|
-
.chevron { flex: none; width: 8px; height: 8px; border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor; transform: rotate(45deg); transition: transform .15s ease; opacity: .55; margin-right: 2px; }
|
|
119
|
-
.chevronCollapsed { transform: rotate(-45deg); }
|
|
118
|
+
.chevron { flex: none; width: 8px; height: 8px; border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor; transform: rotate(45deg) translateY(-1px); transition: transform .15s ease; opacity: .55; margin-right: 2px; align-self: center; }
|
|
119
|
+
.chevronCollapsed { transform: rotate(-45deg) translateY(-1px); }
|
|
120
120
|
|
|
121
121
|
/* Reduced header: title + quiet count on one line, controls on the next. */
|
|
122
122
|
.headerCount { font-size: 12px; opacity: .5; margin-left: 2px; white-space: nowrap; }
|
|
@@ -226,3 +226,14 @@
|
|
|
226
226
|
/* Project-level three-tier tree: outer card (the .section), inner levels are
|
|
227
227
|
flat — indent + a hairline guide only, no nested card surfaces. */
|
|
228
228
|
.projectNest { display: flex; flex-direction: column; margin: 2px 0 0 14px; border-left: 1px solid rgba(128,128,128,.14); }
|
|
229
|
+
|
|
230
|
+
/* Drag handle + reorder states */
|
|
231
|
+
.dragHandle { cursor: grab; opacity: .32; font-size: 11px; letter-spacing: 1px; padding: 0 4px; user-select: none; flex: none; display: inline-flex; align-items: center; justify-content: center; height: 20px; align-self: center; }
|
|
232
|
+
.dragHandle:active { cursor: grabbing; }
|
|
233
|
+
.dragging { opacity: .45; }
|
|
234
|
+
.dragOver { outline: 2px dashed var(--hub-model, #2f81f7); outline-offset: -2px; background: rgba(47,129,247,.07); }
|
|
235
|
+
|
|
236
|
+
/* Scan list for 276-skill import — theme-agnostic, replaces inline styles */
|
|
237
|
+
.scanList { max-height: 360px; overflow: auto; border: 0.5px solid rgba(128,128,128,.13); border-radius: 12px; margin-top: 8px; background: rgba(128,128,128,.06); }
|
|
238
|
+
.scanProgressTrack { flex: 1; height: 6px; background: rgba(128,128,128,.13); border-radius: 4px; overflow: hidden; }
|
|
239
|
+
.scanProgressFill { height: 100%; background: var(--hub-model, #2f81f7); transition: width .2s; }
|