mirror of
https://github.com/iflytek/skillhub.git
synced 2026-11-14 09:01:16 +00:00
fix(web): align my skills page with approved prototype
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
parent
ac593567ca
commit
ce42c883ba
10 changed files with 307 additions and 73 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"schema": 1,
|
||||
"description": "Source declarations, not runtime DOM counts, pageviews, successful outcomes or a closed runtime enum. Forwarding sites require explicit callers. See frontend-analytics.md for gaps and event policy.",
|
||||
"counts": {"routes":54,"uniqueIds":804,"literalJsxSites":822,"literalJsxValues":976,"navigationDefinitions":29,"forwardingSites":36},
|
||||
"counts": {"routes":54,"uniqueIds":804,"literalJsxSites":823,"literalJsxValues":977,"navigationDefinitions":29,"forwardingSites":36},
|
||||
"routes": [
|
||||
{"name":"landingRoute","path":"/","declaration":{"file":"web/src/app/router.tsx","line":258},"implementation":{"file":"web/src/pages/landing.tsx","line":257,"export":"LandingPage"},"guarded":false,"roleProtected":false},
|
||||
{"name":"skillsRoute","path":"skills","declaration":{"file":"web/src/app/router.tsx","line":264},"implementation":{"file":"web/src/pages/home.tsx","line":12,"export":"HomePage"},"guarded":false,"roleProtected":false},
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
{"name":"skillDetailRoute","path":"/space/$namespace/$slug","declaration":{"file":"web/src/app/router.tsx","line":364},"implementation":{"file":"web/src/pages/skill-detail.tsx","line":133,"export":"SkillDetailPage"},"guarded":false,"roleProtected":false},
|
||||
{"name":"skillVersionCompareRoute","path":"/space/$namespace/$slug/compare","declaration":{"file":"web/src/app/router.tsx","line":374},"implementation":{"file":"web/src/pages/skill-version-compare.tsx","line":5,"export":"SkillVersionComparePage"},"guarded":false,"roleProtected":false},
|
||||
{"name":"dashboardRoute","path":"dashboard","declaration":{"file":"web/src/app/router.tsx","line":384},"implementation":{"file":"web/src/pages/dashboard.tsx","line":98,"export":"DashboardPage"},"guarded":true,"roleProtected":false},
|
||||
{"name":"dashboardSkillsRoute","path":"dashboard/skills","declaration":{"file":"web/src/app/router.tsx","line":391},"implementation":{"file":"web/src/pages/dashboard/my-skills.tsx","line":53,"export":"MySkillsPage"},"guarded":true,"roleProtected":false},
|
||||
{"name":"dashboardSkillsRoute","path":"dashboard/skills","declaration":{"file":"web/src/app/router.tsx","line":391},"implementation":{"file":"web/src/pages/dashboard/my-skills.tsx","line":54,"export":"MySkillsPage"},"guarded":true,"roleProtected":false},
|
||||
{"name":"dashboardPublishRoute","path":"dashboard/publish","declaration":{"file":"web/src/app/router.tsx","line":404},"implementation":{"file":"web/src/pages/dashboard/publish.tsx","line":41,"export":"PublishPage"},"guarded":true,"roleProtected":false},
|
||||
{"name":"dashboardSuiteCreateRoute","path":"dashboard/suites/new","declaration":{"file":"web/src/app/router.tsx","line":430},"implementation":{"file":"web/src/pages/dashboard/suite-editor.tsx","line":716,"export":"SuiteCreatePage"},"guarded":true,"roleProtected":false},
|
||||
{"name":"dashboardSuitesRoute","path":"dashboard/suites","declaration":{"file":"web/src/app/router.tsx","line":437},"implementation":{"file":"web/src/pages/dashboard/my-suites.tsx","line":21,"export":"MySuitesPage"},"guarded":true,"roleProtected":false},
|
||||
|
|
@ -547,33 +547,34 @@
|
|||
{"file":"web/src/pages/dashboard/my-namespaces.tsx","line":138,"host":"Button","prop":"data-analytics-id","event":"click","ids":["github.namespace.open"]},
|
||||
{"file":"web/src/pages/dashboard/my-namespaces.tsx","line":148,"host":"Pagination","prop":"analyticsId","event":"click","ids":["namespace.list.paginate"]},
|
||||
{"file":"web/src/pages/dashboard/my-namespaces.tsx","line":157,"host":"Button","prop":"data-analytics-id","event":"click","ids":["namespace.create.open"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":358,"host":"form","prop":"data-analytics-id","event":"submit","ids":["skill.owned.search"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":383,"host":"SelectTrigger","prop":"data-analytics-id","event":"click+keydown-intent","ids":["skill.owned.namespace.interact"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":387,"host":"SelectItem","prop":"data-analytics-id","event":"radix-item-intent","ids":["skill.owned.namespace"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":389,"host":"SelectItem","prop":"data-analytics-id","event":"radix-item-intent","ids":["skill.owned.namespace"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":401,"host":"Button","prop":"data-analytics-id","event":"click","ids":["skill.owned.filter"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":419,"host":"Button","prop":"data-analytics-id","event":"click","ids":["skill.owned.clear"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":453,"host":"Card","prop":"data-analytics-id","event":"click","ids":["skill.owned.open"],"conditional":true},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":477,"host":"Button","prop":"data-analytics-id","event":"click","ids":["skill.update.open"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":503,"host":"DropdownMenuItem","prop":"data-analytics-id","event":"click","ids":["skill.related.open"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":506,"host":"DropdownMenuItem","prop":"data-analytics-id","event":"click","ids":["skill.visibility.open"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":510,"host":"DropdownMenuItem","prop":"data-analytics-id","event":"click","ids":["skill.review.withdraw"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":512,"host":"DropdownMenuItem","prop":"data-analytics-id","event":"click","ids":["skill.promotion.create.request","skill.promotion.update.request"],"conditional":true},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":515,"host":"DropdownMenuItem","prop":"data-analytics-id","event":"click","ids":["skill.unarchive.request"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":517,"host":"DropdownMenuItem","prop":"data-analytics-id","event":"click","ids":["skill.archive.request"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":520,"host":"DropdownMenuItem","prop":"data-analytics-id","event":"click","ids":["skill.restore.request"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":549,"host":"Pagination","prop":"analyticsId","event":"click","ids":["skill.owned.paginate"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":558,"host":"Button","prop":"data-analytics-id","event":"click","ids":["skill.owned.clear"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":580,"host":"ConfirmDialog","prop":"confirmAnalyticsId","event":"click","ids":["skill.restore.confirm"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":580,"host":"ConfirmDialog","prop":"cancelAnalyticsId","event":"click","ids":["skill.restore.cancel"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":593,"host":"ConfirmDialog","prop":"confirmAnalyticsId","event":"click","ids":["skill.promotion.create.confirm","skill.promotion.update.confirm"],"conditional":true},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":593,"host":"ConfirmDialog","prop":"cancelAnalyticsId","event":"click","ids":["skill.promotion.create.cancel","skill.promotion.update.cancel"],"conditional":true},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":609,"host":"ConfirmDialog","prop":"confirmAnalyticsId","event":"click","ids":["skill.archive.confirm"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":609,"host":"ConfirmDialog","prop":"cancelAnalyticsId","event":"click","ids":["skill.archive.cancel"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":622,"host":"ConfirmDialog","prop":"confirmAnalyticsId","event":"click","ids":["skill.unarchive.confirm"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":622,"host":"ConfirmDialog","prop":"cancelAnalyticsId","event":"click","ids":["skill.unarchive.cancel"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":635,"host":"ConfirmDialog","prop":"confirmAnalyticsId","event":"click","ids":["skill.review.withdraw.confirm"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":635,"host":"ConfirmDialog","prop":"cancelAnalyticsId","event":"click","ids":["skill.review.withdraw.cancel"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":362,"host":"form","prop":"data-analytics-id","event":"submit","ids":["skill.owned.search"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":387,"host":"SelectTrigger","prop":"data-analytics-id","event":"click+keydown-intent","ids":["skill.owned.namespace.interact"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":391,"host":"SelectItem","prop":"data-analytics-id","event":"radix-item-intent","ids":["skill.owned.namespace"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":393,"host":"SelectItem","prop":"data-analytics-id","event":"radix-item-intent","ids":["skill.owned.namespace"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":405,"host":"Button","prop":"data-analytics-id","event":"click","ids":["skill.owned.filter"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":423,"host":"Button","prop":"data-analytics-id","event":"click","ids":["skill.owned.clear"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":457,"host":"Card","prop":"data-analytics-id","event":"click","ids":["skill.owned.open"],"conditional":true},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":481,"host":"Button","prop":"data-analytics-id","event":"click","ids":["skill.update.open"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":507,"host":"DropdownMenuItem","prop":"data-analytics-id","event":"click","ids":["skill.update.open"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":511,"host":"DropdownMenuItem","prop":"data-analytics-id","event":"click","ids":["skill.related.open"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":514,"host":"DropdownMenuItem","prop":"data-analytics-id","event":"click","ids":["skill.visibility.open"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":518,"host":"DropdownMenuItem","prop":"data-analytics-id","event":"click","ids":["skill.review.withdraw"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":520,"host":"DropdownMenuItem","prop":"data-analytics-id","event":"click","ids":["skill.promotion.create.request","skill.promotion.update.request"],"conditional":true},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":523,"host":"DropdownMenuItem","prop":"data-analytics-id","event":"click","ids":["skill.unarchive.request"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":525,"host":"DropdownMenuItem","prop":"data-analytics-id","event":"click","ids":["skill.archive.request"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":528,"host":"DropdownMenuItem","prop":"data-analytics-id","event":"click","ids":["skill.restore.request"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":562,"host":"Pagination","prop":"analyticsId","event":"click","ids":["skill.owned.paginate"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":571,"host":"Button","prop":"data-analytics-id","event":"click","ids":["skill.owned.clear"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":593,"host":"ConfirmDialog","prop":"confirmAnalyticsId","event":"click","ids":["skill.restore.confirm"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":593,"host":"ConfirmDialog","prop":"cancelAnalyticsId","event":"click","ids":["skill.restore.cancel"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":606,"host":"ConfirmDialog","prop":"confirmAnalyticsId","event":"click","ids":["skill.promotion.create.confirm","skill.promotion.update.confirm"],"conditional":true},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":606,"host":"ConfirmDialog","prop":"cancelAnalyticsId","event":"click","ids":["skill.promotion.create.cancel","skill.promotion.update.cancel"],"conditional":true},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":622,"host":"ConfirmDialog","prop":"confirmAnalyticsId","event":"click","ids":["skill.archive.confirm"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":622,"host":"ConfirmDialog","prop":"cancelAnalyticsId","event":"click","ids":["skill.archive.cancel"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":635,"host":"ConfirmDialog","prop":"confirmAnalyticsId","event":"click","ids":["skill.unarchive.confirm"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":635,"host":"ConfirmDialog","prop":"cancelAnalyticsId","event":"click","ids":["skill.unarchive.cancel"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":648,"host":"ConfirmDialog","prop":"confirmAnalyticsId","event":"click","ids":["skill.review.withdraw.confirm"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":648,"host":"ConfirmDialog","prop":"cancelAnalyticsId","event":"click","ids":["skill.review.withdraw.cancel"]},
|
||||
{"file":"web/src/pages/dashboard/my-suites.tsx","line":55,"host":"Button","prop":"data-analytics-id","event":"click","ids":["suite.create.open"]},
|
||||
{"file":"web/src/pages/dashboard/my-suites.tsx","line":57,"host":"form","prop":"data-analytics-id","event":"submit","ids":["suite.owned.search"]},
|
||||
{"file":"web/src/pages/dashboard/my-suites.tsx","line":63,"host":"button","prop":"data-analytics-id","event":"click","ids":["suite.query.clear"]},
|
||||
|
|
|
|||
|
|
@ -37,3 +37,18 @@
|
|||
## 验证方式
|
||||
|
||||
先完成一个可执行筛选切片并创建 Draft PR,再完成卡片和操作。实现收敛后执行一次集中独立 Review,随后冻结代码,形成需求追踪矩阵并运行源检查、精确提交镜像及浏览器关键业务路径。截图只证明视觉结果,不替代接口与最终状态断言。
|
||||
|
||||
|
||||
## 原型视觉基线与页面隔离
|
||||
|
||||
以已确认原生 Figma 稿及最终执行生成脚本为视觉来源,原生 Medium 字重采用500;本地 HTML 仅作辅助对照;旧覆盖前的30px操作高度不作为基线。所有尺寸仅作用于“我的技能”,不改变共享组件或其他管理页。
|
||||
|
||||
| 区域 | 本页基线 | 生产适配 |
|
||||
| --- | --- | --- |
|
||||
| 标题/简介/元数据/状态 | 15px/24px、12px/20px、11px/18px、10px/20px | 窄屏不进一步缩字,标题最多两行,完整名称保留于title及详情 |
|
||||
| 状态与图标 | 状态圆角5px、内边距0/7px、低饱和底色;元数据图标13px | 小字前景色加深到4.5:1对比;真实头像或首字回退32px,不推断技能类别 |
|
||||
| 卡片与操作 | 圆角12px、卡片间距12px、桌面内边距18px/20px、移动14px | 更新72×32px、更多32×32px;768px以下更新进入菜单,保留相同权限及预填参数 |
|
||||
| 页面框架 | 仅/dashboard/skills使用1440px外壳,桌面右栏左右40px、顶部36px | 手机左右16px、顶部18px;仅本页侧栏添加桌面20px水平/32px顶部及移动16px内边距,公共导航及侧栏行为和其他管理页保持原样 |
|
||||
| 筛选与菜单 | 筛选控件40px、胶囊32px;菜单宽208px、圆角10px、条目36px、图标16px;发布按钮36px | 不伪造状态计数;浅色与深色分别适配 |
|
||||
|
||||
原型中的彩色类别头像与固定技能数据是演示样例,不能据此生成生产语义;真实技能图片、生命周期、只读状态与审核目标仍来自现有接口。短可见性标签和图标旁保留完整范围说明,长坐标可截断但保留完整悬停文本及复制操作。公共导航高度及内容继续遵循应用外壳,不宣称整个应用逐像素复刻原型。
|
||||
|
|
|
|||
|
|
@ -51,3 +51,9 @@
|
|||
- **WHEN** 菜单操作完成,查询结果移除了原技能行及其操作入口
|
||||
- **THEN** 若焦点因入口消失落到页面主体,焦点返回列表搜索控件
|
||||
- **AND** 原行保留时恢复原入口;用户已主动聚焦其他控件时不抢夺焦点
|
||||
|
||||
|
||||
#### Scenario: 原型视觉基线与其他页面隔离
|
||||
- **WHEN** 用户打开“我的技能”列表
|
||||
- **THEN** 卡片、筛选、标签和图标按设计文档的本页量化视觉基线展示,窄屏保留菜单内更新操作
|
||||
- **AND** 仅该路由使用宽版内容框架,其他管理页及公共导航不受影响
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
## 3. 验证与交付
|
||||
- [x] 筛选首片针对性测试与静态检查
|
||||
- [x] 创建 Draft PR 并持续更新(#1062)
|
||||
- [ ] 集中独立 Review 与修复
|
||||
- [x] 集中独立 Review 与修复(功能、子路径与焦点问题已修复并复核)
|
||||
- [ ] 原型视觉专项:图标、字号、标签、路由框架及明暗/移动端浏览器对照
|
||||
- [ ] 冻结代码后的需求场景矩阵、源检查与精确提交运行时验证
|
||||
- [ ] 保留人工预览入口并交付验证结果
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@ import {
|
|||
CENTERED_MAIN_CLASS_NAME,
|
||||
CENTERED_SEARCH_CONTENT_CLASS_NAME,
|
||||
DEFAULT_MAIN_CLASS_NAME,
|
||||
MY_SKILLS_MAIN_CLASS_NAME,
|
||||
MY_SKILLS_CONTENT_CLASS_NAME,
|
||||
MY_SKILLS_SIDEBAR_ROW_CLASS_NAME,
|
||||
MY_SKILLS_SIDEBAR_CONTENT_CLASS_NAME,
|
||||
SPLIT_AUTH_MAIN_CLASS_NAME,
|
||||
getAppMainContentLayout,
|
||||
resolveAppMainContentPathname,
|
||||
|
|
@ -28,8 +32,8 @@ describe('getAppMainContentLayout', () => {
|
|||
expect(layout.contentClassName).toContain('max-w-[1200px]')
|
||||
})
|
||||
|
||||
it('centers all dashboard sub-pages within a slightly narrower content frame', () => {
|
||||
const layout = getAppMainContentLayout('/dashboard/skills')
|
||||
it.each(['/dashboard/tokens', '/dashboard/stars', '/settings/profile', '/dashboard/skills/unrelated'])('keeps %s in the existing dashboard frame', (pathname) => {
|
||||
const layout = getAppMainContentLayout(pathname)
|
||||
|
||||
expect(layout).toEqual({
|
||||
mainClassName: CENTERED_MAIN_CLASS_NAME,
|
||||
|
|
@ -38,6 +42,16 @@ describe('getAppMainContentLayout', () => {
|
|||
expect(layout.contentClassName).toContain('max-w-[1100px]')
|
||||
})
|
||||
|
||||
it('isolates the prototype-width frame and sidebar gutters to My Skills', () => {
|
||||
expect(getAppMainContentLayout('/dashboard/skills')).toEqual({
|
||||
mainClassName: MY_SKILLS_MAIN_CLASS_NAME,
|
||||
contentClassName: MY_SKILLS_CONTENT_CLASS_NAME,
|
||||
sidebarRowClassName: MY_SKILLS_SIDEBAR_ROW_CLASS_NAME,
|
||||
sidebarContentClassName: MY_SKILLS_SIDEBAR_CONTENT_CLASS_NAME,
|
||||
})
|
||||
expect(MY_SKILLS_CONTENT_CLASS_NAME).toContain('max-w-[1440px]')
|
||||
})
|
||||
|
||||
it('uses the full-bleed split layout for login and registration', () => {
|
||||
for (const pathname of ['/login', '/register']) {
|
||||
expect(getAppMainContentLayout(pathname)).toEqual({
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@ export const DEFAULT_MAIN_CLASS_NAME = 'flex-1 relative z-10 px-6 py-10 md:px-12
|
|||
export const CENTERED_MAIN_CLASS_NAME = 'flex-1 relative z-10 px-4 py-8 sm:px-6 md:px-12 md:py-10'
|
||||
export const CENTERED_SEARCH_CONTENT_CLASS_NAME = 'mx-auto w-full max-w-[1200px]'
|
||||
export const CENTERED_DASHBOARD_CONTENT_CLASS_NAME = 'mx-auto min-h-[calc(100vh-11rem)] w-full max-w-[1100px]'
|
||||
export const MY_SKILLS_MAIN_CLASS_NAME = 'flex-1 relative z-10 bg-background'
|
||||
export const MY_SKILLS_CONTENT_CLASS_NAME = 'mx-auto min-h-[calc(100vh-11rem)] w-full max-w-[1440px]'
|
||||
export const MY_SKILLS_SIDEBAR_ROW_CLASS_NAME = 'flex flex-col lg:flex-row gap-0 [&>aside]:p-4 lg:[&>aside]:px-5 lg:[&>aside]:pt-8 lg:[&>aside]:pb-6 lg:[&>aside]:border-r lg:[&>aside]:border-border/40'
|
||||
export const MY_SKILLS_SIDEBAR_CONTENT_CLASS_NAME = 'flex-1 min-w-0 px-4 pb-7 pt-[18px] lg:px-10 lg:pt-9'
|
||||
export const REVIEW_DETAIL_CONTENT_CLASS_NAME = 'mx-auto min-h-[calc(100vh-11rem)] w-full max-w-[1500px]'
|
||||
|
||||
export const DASHBOARD_PATH_PREFIXES = ['/dashboard', '/settings/'] as const
|
||||
|
|
@ -14,6 +18,8 @@ export const SPLIT_AUTH_PATHS = ['/login', '/register'] as const
|
|||
interface AppMainContentLayout {
|
||||
mainClassName: string
|
||||
contentClassName: string
|
||||
sidebarRowClassName?: string
|
||||
sidebarContentClassName?: string
|
||||
}
|
||||
|
||||
export function resolveAppMainContentPathname(
|
||||
|
|
@ -59,6 +65,15 @@ export function getAppMainContentLayout(pathname: string): AppMainContentLayout
|
|||
}
|
||||
}
|
||||
|
||||
if (pathname === '/dashboard/skills') {
|
||||
return {
|
||||
mainClassName: MY_SKILLS_MAIN_CLASS_NAME,
|
||||
contentClassName: MY_SKILLS_CONTENT_CLASS_NAME,
|
||||
sidebarRowClassName: MY_SKILLS_SIDEBAR_ROW_CLASS_NAME,
|
||||
sidebarContentClassName: MY_SKILLS_SIDEBAR_CONTENT_CLASS_NAME,
|
||||
}
|
||||
}
|
||||
|
||||
if (pathname === '/dashboard' || pathname.startsWith('/dashboard/') || pathname.startsWith('/settings/')) {
|
||||
return {
|
||||
mainClassName: CENTERED_MAIN_CLASS_NAME,
|
||||
|
|
|
|||
|
|
@ -219,9 +219,9 @@ export function Layout() {
|
|||
>
|
||||
<div className={mainContentLayout.contentClassName}>
|
||||
{showSidebar ? (
|
||||
<div className="flex flex-col lg:flex-row gap-6">
|
||||
<div className={mainContentLayout.sidebarRowClassName ?? "flex flex-col lg:flex-row gap-6"}>
|
||||
<DashboardSidebar groups={filteredDashboardGroups} user={user} t={t} pathname={pathname} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className={mainContentLayout.sidebarContentClassName ?? "flex-1 min-w-0"}>
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -282,4 +282,16 @@ describe('My Skills interactions with real menu and dialog', () => {
|
|||
expect(document.activeElement?.isConnected).toBe(true)
|
||||
})
|
||||
|
||||
|
||||
it('opens the existing prefilled update form from the mobile menu action', async () => {
|
||||
render(<MySkillsPage />)
|
||||
await openMenu()
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: 'mySkills.update' }))
|
||||
expect(navigate).toHaveBeenCalledOnce()
|
||||
expect(navigate).toHaveBeenCalledWith({
|
||||
to: '/dashboard/publish',
|
||||
search: { namespace: 'team', updateSkill: 'demo', updateName: 'Demo', visibility: 'PRIVATE' },
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
|
|
|||
157
web/src/pages/dashboard/my-skills.module.css
Normal file
157
web/src/pages/dashboard/my-skills.module.css
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
/* Page-local density follows the confirmed My Skills prototype, not shared UI defaults. */
|
||||
.page .filterPanel {
|
||||
border-radius: 12px;
|
||||
margin-top: 26px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-color: #e9e9ed;
|
||||
}
|
||||
|
||||
.page .filterControl {
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
box-shadow: none;
|
||||
border-color: #dedee5;
|
||||
}
|
||||
|
||||
.page .filterButton {
|
||||
height: 32px;
|
||||
min-width: 85px;
|
||||
padding: 0 14px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.page .resultCount {
|
||||
gap: 12px;
|
||||
font-size: 11px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.page .clearButton {
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.page .skillCard {
|
||||
padding: 18px 20px;
|
||||
border-radius: 12px;
|
||||
box-shadow: none;
|
||||
border-color: #dedee5;
|
||||
}
|
||||
|
||||
.page .skillTitle {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
letter-spacing: .05px;
|
||||
}
|
||||
|
||||
.page .status {
|
||||
display: inline-flex;
|
||||
margin: 0;
|
||||
padding: 0 7px;
|
||||
border-radius: 5px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
color: #666670;
|
||||
background: #f1f2f4;
|
||||
}
|
||||
|
||||
.page .statusPublished { color: #357548; background: #eef7f0; }
|
||||
.page .statusReview { color: #8b631f; background: #fff5e6; }
|
||||
.page .statusRejected { color: #a64d46; background: #fff0ef; }
|
||||
|
||||
:global(.dark) .page .status { color: #b9b9c2; background: #303039; }
|
||||
:global(.dark) .page .statusPublished { color: #97d5ac; background: #193329; }
|
||||
:global(.dark) .page .statusReview { color: #e7bf78; background: #3b2d19; }
|
||||
:global(.dark) .page .statusRejected { color: #efa49b; background: #402625; }
|
||||
|
||||
.page .description {
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
color: #666670;
|
||||
}
|
||||
|
||||
.page .metadata {
|
||||
gap: 8px 17px;
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
color: #73737d;
|
||||
}
|
||||
|
||||
.page .metadataItem {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.page .coordinate { max-width: 210px; color: #60738d; }
|
||||
:global(.dark) .page .coordinate { color: #a1b5d0; }
|
||||
.page .pendingVersion { color: #8b631f; }
|
||||
:global(.dark) .page .pendingVersion { color: #d9b77e; }
|
||||
|
||||
.page .updateButton,
|
||||
.page .moreButton {
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
box-shadow: none;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.page .updateButton { gap: 6px; padding: 0 8px; min-width: 72px; }
|
||||
.page .moreButton { width: 32px; padding: 0; color: hsl(var(--muted-foreground)); }
|
||||
.page .note { font-size: 12px; line-height: 20px; }
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.page .filterPanel,
|
||||
.page .skillCard { padding: 14px; }
|
||||
.page .filterButton { min-width: 0; padding: 0 12px; }
|
||||
.page .metadata { gap: 6px 12px; }
|
||||
.page .coordinate { max-width: 150px; }
|
||||
}
|
||||
|
||||
:global(.dark) .page .filterPanel { background: hsl(var(--muted) / .3); border-color: hsl(var(--border) / .6); }
|
||||
:global(.dark) .page .skillCard,
|
||||
:global(.dark) .page .filterControl { border-color: hsl(var(--border) / .7); }
|
||||
:global(.dark) .page .description,
|
||||
:global(.dark) .page .metadata { color: hsl(var(--muted-foreground)); }
|
||||
|
||||
.page h1 { font-size: 27px; line-height: 36px; font-weight: 500; letter-spacing: -.8px; }
|
||||
.page .publishButton { height: 36px; padding: 0 12px; border-radius: 8px; font-size: 12px; }
|
||||
.moreMenu.moreMenu { width: 208px; padding: 6px; border-radius: 10px; }
|
||||
.moreMenu [role="menuitem"] { min-height: 36px; padding: 0 10px; font-size: 12px; }
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.page .desktopUpdate { display: none; }
|
||||
.page .skillTitle { white-space: normal; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
|
||||
.page .skillTitle a { white-space: normal; overflow: visible; }
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.moreMenu .mobileUpdate { display: none; }
|
||||
}
|
||||
|
||||
.page .skillList { margin-top: 20px; }
|
||||
.page h1 + p { font-size: 13px; line-height: 20px; }
|
||||
.page .skillCard:hover { border-color: #c7c7d1; }
|
||||
:global(.dark) .page .skillCard:hover { border-color: hsl(var(--border)); }
|
||||
.page .updateButton:hover,
|
||||
.page .moreButton:hover,
|
||||
.page .publishButton { box-shadow: none; }
|
||||
.page .updateButton svg,
|
||||
.page .publishButton svg,
|
||||
.moreMenu svg { stroke-width: 1.65; }
|
||||
|
||||
.page .filterControl::placeholder { color: #73737d; opacity: 1; }
|
||||
:global(.dark) .page .filterControl::placeholder { color: #aaaab5; }
|
||||
.moreMenu .dangerAction,
|
||||
.moreMenu .dangerAction:focus,
|
||||
.moreMenu .dangerAction[data-highlighted] { color: #b42318; }
|
||||
:global(.dark) .moreMenu .dangerAction,
|
||||
:global(.dark) .moreMenu .dangerAction:focus,
|
||||
:global(.dark) .moreMenu .dangerAction[data-highlighted] { color: #ffb4ab; }
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Archive, ArrowUpRight, Copy, Download, Eye, GitCompareArrows, Globe, MoreHorizontal, RotateCcw, Search, Undo2, Upload } from 'lucide-react'
|
||||
import { Archive, ArrowUpRight, Copy, Download, Eye, GitCompareArrows, Globe, LockKeyhole, MoreHorizontal, Package, Plus, RotateCcw, Search, Undo2, Upload, Users } from 'lucide-react'
|
||||
import styles from './my-skills.module.css'
|
||||
import { useLocation, useNavigate, useSearch } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAuth } from '@/features/auth/use-auth'
|
||||
|
|
@ -201,28 +202,28 @@ export function MySkillsPage() {
|
|||
|
||||
const resolveStatusClassName = (status?: string) => {
|
||||
if (status === 'HIDDEN') {
|
||||
return 'status-pill status-pill--archived'
|
||||
return 'status-pill'
|
||||
}
|
||||
if (status === 'ARCHIVED') {
|
||||
return 'status-pill status-pill--archived'
|
||||
return 'status-pill'
|
||||
}
|
||||
if (status === 'PENDING_REVIEW') {
|
||||
return 'status-pill status-pill--review'
|
||||
return `status-pill ${styles.statusReview}`
|
||||
}
|
||||
if (status === 'PUBLISHED') {
|
||||
return 'status-pill status-pill--published'
|
||||
return `status-pill ${styles.statusPublished}`
|
||||
}
|
||||
if (status === 'REJECTED') {
|
||||
return 'status-pill status-pill--rejected'
|
||||
return `status-pill ${styles.statusRejected}`
|
||||
}
|
||||
if (status === 'SCANNING') {
|
||||
return 'status-pill status-pill--review'
|
||||
return `status-pill ${styles.statusReview}`
|
||||
}
|
||||
if (status === 'SCAN_FAILED') {
|
||||
return 'status-pill status-pill--rejected'
|
||||
return `status-pill ${styles.statusRejected}`
|
||||
}
|
||||
if (status === 'UPLOADED') {
|
||||
return 'status-pill status-pill--review'
|
||||
return `status-pill ${styles.statusReview}`
|
||||
}
|
||||
return 'status-pill'
|
||||
}
|
||||
|
|
@ -347,13 +348,16 @@ export function MySkillsPage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-fade-up">
|
||||
<div className={`${styles.page} animate-fade-up`}>
|
||||
<DashboardPageHeader
|
||||
title={t('mySkills.title')}
|
||||
subtitle={t('mySkills.subtitle')}
|
||||
actions={<Button size="sm" className={styles.publishButton} onClick={() => navigate({ to: '/dashboard/publish' })}>
|
||||
<Plus aria-hidden="true" className="h-4 w-4" />{t('mySkills.publishNew')}
|
||||
</Button>}
|
||||
/>
|
||||
|
||||
<section aria-label={t('mySkills.filterLabel')} className="rounded-[12px] border border-border/60 bg-muted/30 p-4">
|
||||
<section aria-label={t('mySkills.filterLabel')} className={`${styles.filterPanel} border border-border/60`}>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
||||
<form data-analytics-id="skill.owned.search" className="flex min-w-0 flex-1 gap-2" onSubmit={event => {
|
||||
event.preventDefault()
|
||||
|
|
@ -368,10 +372,10 @@ export function MySkillsPage() {
|
|||
onChange={(event) => setKeywordInput(event.target.value)}
|
||||
placeholder={t('mySkills.searchPlaceholder')}
|
||||
aria-label={t('mySkills.searchPlaceholder')}
|
||||
className="h-10 bg-background pl-9"
|
||||
className={`${styles.filterControl} bg-background pl-9`}
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" variant="outline" className="h-10">{t('nav.search')}</Button>
|
||||
<Button type="submit" variant="outline" className={styles.filterControl}>{t('nav.search')}</Button>
|
||||
</form>
|
||||
{filter === 'HIDDEN' ? null : (
|
||||
<Select
|
||||
|
|
@ -380,7 +384,7 @@ export function MySkillsPage() {
|
|||
updateSearch({ namespace: value === ALL_NAMESPACES_VALUE ? undefined : value, page: 0 })
|
||||
}}
|
||||
>
|
||||
<SelectTrigger data-analytics-id="skill.owned.namespace.interact" aria-label={t('mySkills.namespaceFilterLabel')} className="h-10 bg-background sm:w-60 sm:shrink-0">
|
||||
<SelectTrigger data-analytics-id="skill.owned.namespace.interact" aria-label={t('mySkills.namespaceFilterLabel')} className={`${styles.filterControl} bg-background sm:w-60 sm:shrink-0`}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
|
@ -402,7 +406,7 @@ export function MySkillsPage() {
|
|||
key={option}
|
||||
type="button"
|
||||
size="sm"
|
||||
className="rounded-full px-4 text-sm shadow-none"
|
||||
className={styles.filterButton}
|
||||
variant={filter === option ? 'default' : 'outline'}
|
||||
aria-pressed={filter === option}
|
||||
onClick={() => {
|
||||
|
|
@ -413,10 +417,10 @@ export function MySkillsPage() {
|
|||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-sm text-muted-foreground">
|
||||
<div className={`${styles.resultCount} flex items-center text-muted-foreground`}>
|
||||
{skillPage ? <span aria-live="polite" aria-atomic="true">{t('mySkills.resultCount', { count: skillPage.total })}</span> : null}
|
||||
{hasActiveFilters ? (
|
||||
<Button data-analytics-id="skill.owned.clear" type="button" size="sm" variant="ghost" onClick={handleClearFilters}>
|
||||
<Button data-analytics-id="skill.owned.clear" type="button" size="sm" variant="ghost" className={styles.clearButton} onClick={handleClearFilters}>
|
||||
{t('mySkills.clearSearch')}
|
||||
</Button>
|
||||
) : null}
|
||||
|
|
@ -426,7 +430,7 @@ export function MySkillsPage() {
|
|||
|
||||
{skillPage && skillPage.total > 0 ? (
|
||||
<>
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
<div className={`${styles.skillList} grid grid-cols-1 gap-3`}>
|
||||
{skills.map((skill) => {
|
||||
const ownerReadOnly = skill.managementState?.ownerReadOnly === true
|
||||
const hidden = filter === 'HIDDEN'
|
||||
|
|
@ -451,16 +455,16 @@ export function MySkillsPage() {
|
|||
|
||||
return (
|
||||
<Card key={skill.id} data-analytics-id={hidden ? undefined : 'skill.owned.open'}
|
||||
className={`relative rounded-[12px] border-border/70 p-4 shadow-none hover:shadow-none sm:p-5 ${hidden ? '' : 'cursor-pointer hover:border-border'}`}
|
||||
className={`${styles.skillCard} relative border-border/70 hover:shadow-none ${hidden ? '' : 'cursor-pointer hover:border-border'}`}
|
||||
onClick={hidden ? undefined : openDetail}>
|
||||
<div className="flex items-start gap-3">
|
||||
<SkillAvatar name={displayName} url={skill.avatarUrl} />
|
||||
<div className="min-w-0 flex-1 space-y-3">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-x-3 gap-y-2">
|
||||
<h3 className="min-w-0 break-words text-lg font-semibold [overflow-wrap:anywhere]">
|
||||
<SkillAvatar name={displayName} url={skill.avatarUrl} size="h-8 w-8 text-xs" />
|
||||
<div className="min-w-0 flex-1 space-y-2">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-3">
|
||||
<h3 className={`${styles.skillTitle} min-w-0 flex-1 truncate`} title={displayName}>
|
||||
{hidden ? displayName : <a href={withBasePath(`/space/${skill.namespace}/${encodeURIComponent(skill.slug)}?${detailSearch}`)}
|
||||
className="rounded-sm hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
className="block truncate rounded-sm hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return
|
||||
|
|
@ -468,28 +472,28 @@ export function MySkillsPage() {
|
|||
openDetail()
|
||||
}}>{displayName}</a>}
|
||||
</h3>
|
||||
{mainStatus && <span className={`${resolveStatusClassName(mainStatus)} shrink-0`}>
|
||||
{mainStatus && <span className={`${resolveStatusClassName(mainStatus)} ${styles.status} shrink-0`}>
|
||||
{hasPendingPreview && publishedVersion && mainStatus === 'PENDING_REVIEW'
|
||||
? t('mySkills.statusUpdatePendingReview') : resolveStatusLabel(mainStatus)}
|
||||
</span>}
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2" onClick={event => event.stopPropagation()}>
|
||||
{writable && skill.status !== 'ARCHIVED' && <Button data-analytics-id="skill.update.open" size="sm" variant="outline"
|
||||
className="text-sm" onClick={event => {
|
||||
className={`${styles.updateButton} ${styles.desktopUpdate}`} onClick={event => {
|
||||
event.stopPropagation()
|
||||
handleUpdateSkill(skill.namespace, skill.slug, displayName, skill.visibility ?? 'PUBLIC')
|
||||
}}><Upload aria-hidden="true" className="h-4 w-4" />{t('mySkills.update')}</Button>}
|
||||
{/* Keep the menu nonmodal so a new Dialog owns the focus trap during the handoff. */}
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="icon" variant="outline" className="h-8 w-8"
|
||||
<Button size="icon" variant="outline" className={styles.moreButton}
|
||||
title={t('mySkills.moreActions')} aria-label={t('mySkills.moreActionsFor', { skill: displayName })}
|
||||
onClick={event => { event.stopPropagation(); compareTrigger.current = event.currentTarget }}
|
||||
onKeyDown={event => { compareTrigger.current = event.currentTarget }}>
|
||||
<MoreHorizontal aria-hidden="true" className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56 p-1.5" onClick={event => event.stopPropagation()}
|
||||
<DropdownMenuContent align="end" className={styles.moreMenu} onClick={event => event.stopPropagation()}
|
||||
onCloseAutoFocus={event => { if (compareTarget || visibilityTarget || archiveTarget || unarchiveTarget || withdrawTarget || promotionTarget || restoreHiddenTarget) event.preventDefault() }}>
|
||||
{!hidden && <DropdownMenuItem onSelect={openDetail}><Eye aria-hidden="true" className="mr-2 h-4 w-4" />{t('mySkills.viewDetail')}</DropdownMenuItem>}
|
||||
<DropdownMenuItem onSelect={async () => {
|
||||
|
|
@ -500,6 +504,10 @@ export function MySkillsPage() {
|
|||
setCompareVersions({ from: '', to: '' })
|
||||
setCompareTarget({ namespace: skill.namespace, slug: skill.slug, name: displayName })
|
||||
}}><GitCompareArrows aria-hidden="true" className="mr-2 h-4 w-4" />{t('mySkills.compareVersions')}</DropdownMenuItem>}
|
||||
{writable && skill.status !== 'ARCHIVED' && <DropdownMenuItem data-analytics-id="skill.update.open" className={styles.mobileUpdate}
|
||||
onSelect={() => handleUpdateSkill(skill.namespace, skill.slug, displayName, skill.visibility ?? 'PUBLIC')}>
|
||||
<Upload aria-hidden="true" className="mr-2 h-4 w-4" />{t('mySkills.update')}
|
||||
</DropdownMenuItem>}
|
||||
{skill.managementState?.promotionRole && relatedNamespace && relatedSlug && <DropdownMenuItem data-analytics-id="skill.related.open" onSelect={() => handleSkillClick(relatedNamespace, relatedSlug)}>
|
||||
<ArrowUpRight aria-hidden="true" className="mr-2 h-4 w-4" />{t(skill.managementState?.promotionRole === 'TEAM_SOURCE' ? 'mySkills.viewGlobalCopy' : 'mySkills.viewTeamSource')}
|
||||
</DropdownMenuItem>}
|
||||
|
|
@ -514,7 +522,7 @@ export function MySkillsPage() {
|
|||
<Globe aria-hidden="true" className="mr-2 h-4 w-4" />{t(skill.managementState?.promotionRole === 'TEAM_SOURCE' ? 'mySkills.updateGlobalVersion' : 'mySkills.promoteToGlobal')}
|
||||
</DropdownMenuItem> : skill.status === 'ARCHIVED' ? <><DropdownMenuSeparator /><DropdownMenuItem data-analytics-id="skill.unarchive.request" onSelect={() => setUnarchiveTarget({ namespace: skill.namespace, slug: skill.slug, name: displayName })}>
|
||||
<RotateCcw aria-hidden="true" className="mr-2 h-4 w-4" />{t('mySkills.unarchive')}
|
||||
</DropdownMenuItem></> : publishedVersion ? <><DropdownMenuSeparator /><DropdownMenuItem data-analytics-id="skill.archive.request" className="text-destructive focus:text-destructive" onSelect={() => setArchiveTarget({ namespace: skill.namespace, slug: skill.slug, name: displayName })}>
|
||||
</DropdownMenuItem></> : publishedVersion ? <><DropdownMenuSeparator /><DropdownMenuItem data-analytics-id="skill.archive.request" className={styles.dangerAction} onSelect={() => setArchiveTarget({ namespace: skill.namespace, slug: skill.slug, name: displayName })}>
|
||||
<Archive aria-hidden="true" className="mr-2 h-4 w-4" />{t('mySkills.archive')}
|
||||
</DropdownMenuItem></> : null)}
|
||||
{canRestoreHidden && <><DropdownMenuSeparator /><DropdownMenuItem data-analytics-id="skill.restore.request" onSelect={() => setRestoreHiddenTarget({ skillId: skill.id, name: displayName })}>
|
||||
|
|
@ -524,18 +532,23 @@ export function MySkillsPage() {
|
|||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
{summary && <p className="line-clamp-2 break-words text-sm leading-relaxed text-muted-foreground [overflow-wrap:anywhere]">{summary}</p>}
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-muted-foreground">
|
||||
<span className="min-w-0 break-all">{coordinate}</span>
|
||||
<span>{t('skillVisibility.current', { visibility: t(`skillVisibility.${skill.visibility ?? 'PUBLIC'}`) })}</span>
|
||||
{preview && publishedVersion ? <span>{t('mySkills.versionDifference', { version: preview.version, published: publishedVersion.version })}</span>
|
||||
{summary && <p className={`${styles.description} line-clamp-2 break-words text-muted-foreground [overflow-wrap:anywhere]`}>{summary}</p>}
|
||||
<div className={`${styles.metadata} flex flex-wrap items-center text-muted-foreground`}>
|
||||
<span className={`${styles.metadataItem} ${styles.coordinate}`} title={coordinate}><Package aria-hidden="true" strokeWidth={1.65} className="h-[13px] w-[13px] shrink-0" /><span className="truncate">{coordinate}</span></span>
|
||||
<span className={styles.metadataItem} title={t('skillVisibility.current', { visibility: t(`skillVisibility.${skill.visibility ?? 'PUBLIC'}`) })}>
|
||||
{skill.visibility === 'PRIVATE' ? <LockKeyhole aria-hidden="true" strokeWidth={1.65} className="h-[13px] w-[13px] shrink-0" />
|
||||
: skill.visibility === 'NAMESPACE_ONLY' ? <Users aria-hidden="true" strokeWidth={1.65} className="h-[13px] w-[13px] shrink-0" />
|
||||
: <Globe aria-hidden="true" strokeWidth={1.65} className="h-[13px] w-[13px] shrink-0" />}
|
||||
{t(`publish.visibilityOptions.${skill.visibility === 'PRIVATE' ? 'private' : skill.visibility === 'NAMESPACE_ONLY' ? 'namespaceOnly' : 'public'}`)}
|
||||
</span>
|
||||
{preview && publishedVersion ? <span className={styles.pendingVersion}>{t('mySkills.versionDifference', { version: preview.version, published: publishedVersion.version })}</span>
|
||||
: headlineVersion ? <span className="font-mono">v{headlineVersion.version}</span> : null}
|
||||
<span className="inline-flex items-center gap-1" title={t('mySkills.downloadCount')}><Download aria-hidden="true" className="h-4 w-4" />{formatCompactCount(skill.downloadCount)}</span>
|
||||
<span className={styles.metadataItem} title={t('mySkills.downloadCount')}><Download aria-hidden="true" strokeWidth={1.65} className="h-[13px] w-[13px] shrink-0" />{formatCompactCount(skill.downloadCount)}</span>
|
||||
</div>
|
||||
{ownerReadOnly && <p role="status" className="text-sm text-muted-foreground">{t('mySkills.ownerReadOnly')}</p>}
|
||||
{skill.managementState?.promotionRole && <p className="text-sm text-muted-foreground">{t(`mySkills.promotionRoles.${skill.managementState.promotionRole}`)}</p>}
|
||||
{ownerReadOnly && <p role="status" className={`${styles.note} text-muted-foreground`}>{t('mySkills.ownerReadOnly')}</p>}
|
||||
{skill.managementState?.promotionRole && <p className={`${styles.note} text-muted-foreground`}>{t(`mySkills.promotionRoles.${skill.managementState.promotionRole}`)}</p>}
|
||||
{skill.status === 'ACTIVE' && !hidden && <SkillAvatarManager namespace={skill.namespace} slug={skill.slug} previewOnly />}
|
||||
{skill.managementState?.requestedVisibility && <p role="status" className="text-sm text-muted-foreground">
|
||||
{skill.managementState?.requestedVisibility && <p role="status" className={`${styles.note} text-muted-foreground`}>
|
||||
{t('skillVisibility.pendingList', { visibility: t(`skillVisibility.${skill.managementState.requestedVisibility}`) })}
|
||||
</p>}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue