diff --git a/src/web-ui/src/app/scenes/skills/SkillsScene.scss b/src/web-ui/src/app/scenes/skills/SkillsScene.scss index 1cf662291..5fbec799c 100644 --- a/src/web-ui/src/app/scenes/skills/SkillsScene.scss +++ b/src/web-ui/src/app/scenes/skills/SkillsScene.scss @@ -1,11 +1,6 @@ @use '../../../component-library/styles/tokens' as *; @use '../../../component-library/styles/btn-primary-tokens.scss' as btn-primary; -// ── Layout variables (mirrors acp-layout in NurseryView.scss) ───────────────── -$skills-gutter: clamp(36px, 5.5vw, 72px); -$skills-column-gap: clamp(20px, 2.2vw, 32px); -$skills-content-top: clamp(36px, 4.5vh, 48px); - // ══════════════════════════════════════════════════════════════════════════════ // Root wrapper // ══════════════════════════════════════════════════════════════════════════════ @@ -18,540 +13,258 @@ $skills-content-top: clamp(36px, 4.5vh, 48px); min-width: 0; min-height: 0; overflow: hidden; + background: var(--color-bg-base); + color: var(--color-text-primary); } // ══════════════════════════════════════════════════════════════════════════════ -// Two-column split -// ══════════════════════════════════════════════════════════════════════════════ - -.skills-split { - flex: 1; - min-height: 0; - display: flex; - overflow: hidden; - padding: 0 $skills-gutter; - gap: $skills-column-gap; - - // Left market block height (section + 2×card grid + pagination) — right frame = 2× this - --skills-market-card-h: clamp(140px, 13vh, 170px); - --skills-left-body-h: calc( - var(--skills-market-card-h) * 2 - + #{$size-gap-3} - + 52px - + #{$size-gap-4} + #{$size-gap-2} - + #{$size-gap-5} + 34px + #{$size-gap-3} - ); - - // ── LEFT column: search header + market, vertically centered ────────────── - &__left { - flex: 0 0 58%; - min-width: 0; - display: flex; - flex-direction: column; - justify-content: center; - overflow: hidden; - } - - // ── RIGHT column: installed skills, vertically centered ─────────────────── - &__right { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - justify-content: center; - align-items: stretch; - overflow: hidden; - } -} - -// Bordered panel: height = 2 × left market body block -.skills-split__right-frame { - display: flex; - flex-direction: column; - width: 100%; - min-width: 0; - box-sizing: border-box; - height: calc(var(--skills-left-body-h) * 2); - max-height: calc(var(--skills-left-body-h) * 2); - border: 1px solid var(--border-medium); - border-radius: $size-radius-xl; - padding: $size-gap-3 $size-gap-4 $size-gap-2; - overflow: hidden; -} - -// ══════════════════════════════════════════════════════════════════════════════ -// LEFT: sticky header +// Top-left tabs bar // ══════════════════════════════════════════════════════════════════════════════ -.skills-split__left-header { +.skills-tabs-bar { flex-shrink: 0; - padding: 0 0 $size-gap-5; - display: flex; - flex-direction: column; - gap: $size-gap-4; -} - -.skills-split__left-title-row { display: flex; - align-items: flex-end; - gap: $size-gap-3; -} - -.skills-split__left-identity { - flex: 1; - min-width: 0; - text-align: center; -} - -.skills-split__title { - margin: 0 0 $size-gap-1; - font-size: clamp(28px, 3vw, 34px); - font-weight: $font-weight-semibold; - color: var(--color-text-primary); - letter-spacing: -0.03em; - line-height: 1.12; -} - -.skills-split__subtitle { - margin: 0; - font-size: var(--font-size-xs); - color: var(--color-text-muted); - line-height: $line-height-relaxed; + align-items: center; + padding: 8px 12px; + background: var(--color-bg-base); } -// ── Toolbar: component-library Search (capsule, scoped sizing) ───────────── - -.skills-split__toolbar { +.skills-tabs-bar__tabs { display: flex; align-items: center; - justify-content: center; + gap: 0; } -.skills-split__search.search { - width: 100%; - max-width: min(600px, 100%); - margin-inline: auto; - - .search__wrapper { - border-radius: $size-radius-full; - min-height: 48px; - padding-inline: $size-gap-4; - padding-block: 10px; - background: color-mix(in srgb, var(--element-bg-soft) 92%, transparent); - border-color: var(--border-medium); - transition: - border-color $motion-fast $easing-standard, - background $motion-fast $easing-standard, - box-shadow $motion-fast $easing-standard; - } - - &.search--hovered:not(.search--disabled) .search__wrapper { - border-color: var(--border-strong, var(--border-medium)); - background: var(--element-bg-soft); - } - - &.search--focused:not(.search--disabled) .search__wrapper { - box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-accent-500) 12%, transparent); - } +.skills-tabs-bar__divider { + width: 1px; + height: 12px; + margin: 0 6px; + background: var(--border-medium); + flex-shrink: 0; } -.skills-split__add-btn { - flex-shrink: 0; +.skills-tabs-bar__tab { display: inline-flex; align-items: center; - gap: $size-gap-1; - height: 24px; - padding: 0 $size-gap-2; - border: 1px solid color-mix(in srgb, var(--color-accent-500) 20%, var(--border-subtle)); - border-radius: $size-radius-full; - background: color-mix(in srgb, var(--color-accent-500) 8%, transparent); - color: var(--color-accent-600, var(--color-text-secondary)); - font-size: var(--font-size-xs); - font-weight: $font-weight-medium; + padding: 2px 3px; + margin: 0; + border: none; + border-radius: 4px; + background: transparent; + color: var(--color-text-muted); + font-size: 12px; + font-weight: $font-weight-normal; + line-height: 1.4; cursor: pointer; - white-space: nowrap; - transition: - background $motion-fast $easing-standard, - color $motion-fast $easing-standard, - border-color $motion-fast $easing-standard; + transition: color $motion-fast $easing-standard; - &:hover { - background: color-mix(in srgb, var(--color-accent-500) 14%, transparent); - border-color: color-mix(in srgb, var(--color-accent-500) 30%, var(--border-medium)); - color: var(--color-accent-600, var(--color-text-primary)); + &:hover:not(.is-active) { + color: var(--color-text-secondary); } - &:active { - background: color-mix(in srgb, var(--color-accent-500) 18%, transparent); + &.is-active { + color: var(--color-text-primary); + font-weight: $font-weight-medium; } &:focus-visible { - outline: 2px solid color-mix( - in srgb, - var(--color-accent-500) 55%, - transparent - ); + outline: 2px solid var(--color-accent-500); outline-offset: 2px; } } // ══════════════════════════════════════════════════════════════════════════════ -// LEFT: body — fixed display, no scroll (4 cards shown) +// Page container // ══════════════════════════════════════════════════════════════════════════════ -.skills-split__left-body { - // No flex-grow / overflow; content is always fully visible - padding-bottom: $size-gap-4; -} - -// Section head (market title + source link) -.skills-split__section-head { +.skills-page { + flex: 1; + min-height: 0; + overflow: hidden; display: flex; - align-items: center; - gap: $size-gap-2; - margin-bottom: $size-gap-4; - padding-bottom: $size-gap-2; - border-bottom: 1px solid var(--border-subtle); -} - -.skills-split__section-title { - font-size: var(--font-size-sm); - font-weight: $font-weight-semibold; - color: var(--color-text-primary); - letter-spacing: -0.01em; + flex-direction: column; } -.skills-split__section-sub { - flex: 1; - font-size: var(--font-size-xs); - color: var(--color-text-muted); +// ══════════════════════════════════════════════════════════════════════════════ +// INSTALLED PAGE — Sidebar + Grid +// +// ┌─ Sidebar ─┐ ┌─ Main Content ──────────────────────────────┐ +// │ Header │ │ ┌───────────────────────┐ ┌──────────────┐│ +// │ Nav │ │ │ Skill Card │ │ Skill Card ││ +// │ Items │ │ └───────────────────────┘ └──────────────┘│ +// │ │ │ ┌───────────────────────┐ ┌──────────────┐│ +// │ Hint │ │ │ Skill Card │ │ Skill Card ││ +// └────────────┘ │ └───────────────────────┘ └──────────────┘│ +// └─────────────────────────────────────────────┘ +// ══════════════════════════════════════════════════════════════════════════════ - a { - color: inherit; - text-decoration: underline; - text-underline-offset: 2px; - } +.skills-installed { + display: flex; + height: 100%; + overflow: hidden; } -// Market cards — 3-column × 2-row grid (6 cards per page) -.skills-split__market-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: $size-gap-3; - align-content: start; +// ── Sidebar ─────────────────────────────────────────────────────────────────── - .skill-card { - width: 100%; - height: var(--skills-market-card-h); - } +.skills-sidebar { + width: 220px; + flex-shrink: 0; + display: flex; + flex-direction: column; + border-right: 1px solid var(--border-subtle); + background: var(--color-bg-secondary); + overflow-y: auto; } -// ── Skeleton loading (market grid + installed list) ─────────────────────────── - -.skills-split__skeleton-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: $size-gap-3; - align-content: start; +.skills-sidebar__header { + flex-shrink: 0; + padding: 20px 16px 12px; } -.skills-split__skeleton-card { - height: var(--skills-market-card-h); - border-radius: 15px; - background: var(--element-bg-subtle); - border: 1px solid var(--border-subtle); - position: relative; - overflow: hidden; - animation: skills-skeleton-card-in 0.28s $easing-decelerate both; - animation-delay: calc(var(--card-index, 0) * 60ms); - - &::after { - content: ''; - position: absolute; - inset: 0; - transform: translateX(-100%); - background: linear-gradient( - 90deg, - var(--skills-sk-shimmer-0, rgba(255, 255, 255, 0)) 0%, - var(--skills-sk-shimmer-peak, rgba(255, 255, 255, 0.1)) 50%, - var(--skills-sk-shimmer-0, rgba(255, 255, 255, 0)) 100% - ); - animation: skills-skeleton-shimmer 1.6s ease-in-out calc(var(--card-index, 0) * 0.12s) infinite; - } +.skills-sidebar__title { + margin: 0; + font-size: 13px; + font-weight: $font-weight-semibold; + color: var(--color-text-primary); + letter-spacing: -0.01em; } -.skills-split__skeleton-list { +.skills-sidebar__nav { + flex: 1; display: flex; flex-direction: column; - gap: $size-gap-1; + gap: 1px; + padding: 0 8px; } -.skills-split__skeleton-row { +.skills-sidebar__item { display: flex; align-items: center; - gap: $size-gap-3; - padding: $size-gap-3 $size-gap-2; - border-radius: $size-radius-lg; - animation: skills-skeleton-row-in 0.22s $easing-decelerate both; - animation-delay: calc(var(--row-index, 0) * 40ms); -} - -.skills-split__skeleton-row-avatar { - flex-shrink: 0; - width: 30px; - height: 30px; + gap: 10px; + width: 100%; + padding: 9px 12px; + border: none; border-radius: $size-radius-base; - background: var(--element-bg-subtle); - border: 1px solid var(--border-subtle); - position: relative; - overflow: hidden; + background: transparent; + color: var(--color-text-secondary); + font-size: var(--font-size-xs); + font-weight: $font-weight-normal; + cursor: pointer; + text-align: left; + transition: background $motion-fast $easing-standard, color $motion-fast $easing-standard; - &::after { - content: ''; - position: absolute; - inset: 0; - transform: translateX(-100%); - background: linear-gradient( - 90deg, - var(--skills-sk-shimmer-0, rgba(255, 255, 255, 0)) 0%, - var(--skills-sk-shimmer-peak, rgba(255, 255, 255, 0.1)) 50%, - var(--skills-sk-shimmer-0, rgba(255, 255, 255, 0)) 100% - ); - animation: skills-skeleton-shimmer 1.6s ease-in-out calc(var(--row-index, 0) * 0.1s) infinite; + &.is-empty { + color: var(--color-text-muted); + opacity: 0.55; + cursor: default; } -} -.skills-split__skeleton-row-lines { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - gap: 6px; -} - -.skills-split__skeleton-line { - border-radius: 4px; - background: var(--element-bg-subtle); - border: 1px solid var(--border-subtle); - position: relative; - overflow: hidden; - - &::after { - content: ''; - position: absolute; - inset: 0; - transform: translateX(-100%); - background: linear-gradient( - 90deg, - var(--skills-sk-shimmer-0, rgba(255, 255, 255, 0)) 0%, - var(--skills-sk-shimmer-peak, rgba(255, 255, 255, 0.1)) 50%, - var(--skills-sk-shimmer-0, rgba(255, 255, 255, 0)) 100% - ); - animation: skills-skeleton-shimmer 1.6s ease-in-out calc(var(--row-index, 0) * 0.1s + 0.15s) infinite; + &:hover:not(.is-empty):not(.is-active) { + background: var(--element-bg-subtle); + color: var(--color-text-primary); } - &--title { - width: 58%; - height: 11px; + &.is-active { + background: var(--element-bg-medium); + color: var(--color-text-primary); + font-weight: $font-weight-medium; } - &--desc { - width: 88%; - height: 8px; - opacity: 0.85; - - &::after { - animation-delay: calc(var(--row-index, 0) * 0.1s + 0.28s); - } + &:focus-visible { + outline: 2px solid var(--color-accent-500); + outline-offset: 2px; } } -.skills-split__skeleton-row-tail { +.skills-sidebar__item-icon { display: flex; align-items: center; - gap: $size-gap-2; flex-shrink: 0; + width: 18px; + justify-content: center; + color: inherit; + opacity: 0.7; } -.skills-split__skeleton-pill { - width: 44px; - height: 22px; - border-radius: $size-radius-full; - background: var(--element-bg-subtle); - border: 1px solid var(--border-subtle); - position: relative; - overflow: hidden; - - &::after { - content: ''; - position: absolute; - inset: 0; - transform: translateX(-100%); - background: linear-gradient( - 90deg, - var(--skills-sk-shimmer-0, rgba(255, 255, 255, 0)) 0%, - var(--skills-sk-shimmer-peak, rgba(255, 255, 255, 0.1)) 50%, - var(--skills-sk-shimmer-0, rgba(255, 255, 255, 0)) 100% - ); - animation: skills-skeleton-shimmer 1.6s ease-in-out calc(var(--row-index, 0) * 0.1s + 0.2s) infinite; - } -} - -.skills-split__skeleton-icon { - width: 26px; - height: 26px; - border-radius: $size-radius-base; - background: var(--element-bg-subtle); - border: 1px solid var(--border-subtle); - position: relative; - overflow: hidden; - - &::after { - content: ''; - position: absolute; - inset: 0; - transform: translateX(-100%); - background: linear-gradient( - 90deg, - var(--skills-sk-shimmer-0, rgba(255, 255, 255, 0)) 0%, - var(--skills-sk-shimmer-peak, rgba(255, 255, 255, 0.1)) 50%, - var(--skills-sk-shimmer-0, rgba(255, 255, 255, 0)) 100% - ); - animation: skills-skeleton-shimmer 1.6s ease-in-out calc(var(--row-index, 0) * 0.1s + 0.32s) infinite; - } -} - -:root[data-theme-type='light'] .skills-split__skeleton-card, -:root[data-theme-type='light'] .skills-split__skeleton-row-avatar, -:root[data-theme-type='light'] .skills-split__skeleton-line, -:root[data-theme-type='light'] .skills-split__skeleton-pill, -:root[data-theme-type='light'] .skills-split__skeleton-icon { - --skills-sk-shimmer-0: rgba(0, 0, 0, 0); - --skills-sk-shimmer-peak: rgba(0, 0, 0, 0.07); -} - -// Pagination row -.skills-split__pagination { - display: flex; - align-items: center; - justify-content: center; - gap: $size-gap-3; - padding: $size-gap-5 0 $size-gap-3; +.skills-sidebar__item-label { + flex: 1; } -.skills-split__page-btn { +.skills-sidebar__item-count { + min-width: 18px; + height: 16px; + padding: 0 5px; display: inline-flex; align-items: center; justify-content: center; - width: 30px; - height: 30px; - border: 1px solid var(--border-medium); - border-radius: $size-radius-base; - background: var(--element-bg-soft); - color: var(--color-text-secondary); - cursor: pointer; - transition: - background $motion-fast $easing-standard, - color $motion-fast $easing-standard; + border-radius: 999px; + background: var(--element-bg-medium); + color: var(--color-text-muted); + font-size: 10px; + font-weight: $font-weight-medium; - &:hover:not(:disabled) { - background: var(--element-bg-medium); - color: var(--color-text-primary); + .skills-sidebar__item.is-active & { + background: var(--element-bg-soft); } - &:disabled { - opacity: 0.4; - cursor: not-allowed; + .skills-sidebar__item.is-empty & { + background: transparent; } } -.skills-split__page-info { - min-width: 52px; - text-align: center; - font-size: var(--font-size-sm); - color: var(--color-text-secondary); +.skills-sidebar__footer { + flex-shrink: 0; + padding: 12px 16px 16px; + border-top: 1px solid var(--border-subtle); } -// Shared loading / empty states -.skills-split__loading { - display: flex; - align-items: center; - justify-content: center; - min-height: 140px; +.skills-sidebar__hint { + margin: 0; + font-size: 11px; color: var(--color-text-muted); + opacity: 0.75; + line-height: 1.45; } -.skills-split__spinner { - animation: skills-spin 0.8s linear infinite; -} +// ── Main Content ────────────────────────────────────────────────────────────── -.skills-split__empty { +.skills-main { + flex: 1; + min-width: 0; display: flex; flex-direction: column; - align-items: center; - justify-content: center; - gap: $size-gap-3; - min-height: 140px; - padding: $size-gap-8 $size-gap-6; - font-size: var(--font-size-sm); - color: var(--color-text-muted); - text-align: center; - - &--error { - color: var(--color-error); - } + overflow: hidden; } -// ══════════════════════════════════════════════════════════════════════════════ -// RIGHT: installed skills panel -// ══════════════════════════════════════════════════════════════════════════════ - -.skills-split__right-header { +.skills-main__toolbar { flex-shrink: 0; display: flex; - flex-direction: column; - gap: $size-gap-2; - padding-bottom: $size-gap-3; - margin-bottom: $size-gap-1; - border-bottom: 1px solid var(--border-subtle); -} - -.skills-split__right-title { - font-size: clamp(16px, 1.6vw, 18px); - font-weight: $font-weight-semibold; - color: var(--color-text-primary); - letter-spacing: -0.02em; - min-width: 0; -} - -// Filters + total count + add on one row (wraps on narrow width) -.skills-split__right-toolbar { - display: flex; - align-items: center; - gap: $size-gap-2; flex-wrap: wrap; + align-items: center; + gap: 10px 12px; + padding: 16px 24px; + border-bottom: 1px solid var(--border-subtle); } -.skills-split__filter-bar { - display: flex; - align-items: center; - gap: $size-gap-1; - flex-wrap: wrap; +.skills-main__toolbar-search { flex: 1; - min-width: 0; -} + min-width: min(260px, 100%); -.skills-split__right-toolbar .skills-split__add-btn { - flex-shrink: 0; - margin-left: auto; + .search__wrapper { + max-width: 100%; + border-radius: $size-radius-base; + min-height: 34px; + background: var(--element-bg-subtle); + border-color: var(--border-medium); + } } -.skills-split__filter-chip { +.skills-main__chip-btn { display: inline-flex; align-items: center; - gap: $size-gap-1; - height: 24px; - padding: 0 $size-gap-2; + gap: 6px; + height: 30px; + padding: 0 10px; border: 1px solid var(--border-subtle); border-radius: $size-radius-full; background: transparent; @@ -569,73 +282,87 @@ $skills-content-top: clamp(36px, 4.5vh, 48px); background: var(--element-bg-soft); } - &:focus-visible { - outline: 2px solid var(--color-accent-500); - outline-offset: -1px; - } - &.is-active { color: var(--color-accent-500); background: var(--color-accent-100); border-color: var(--color-accent-300); } + + &:focus-visible { + outline: 2px solid var(--color-accent-500); + outline-offset: 2px; + } } -.skills-split__filter-count { +.skills-main__add-btn { display: inline-flex; align-items: center; - justify-content: center; - height: 16px; - min-width: 16px; - padding: 0 4px; + gap: 6px; + height: 30px; + padding: 0 12px; + border: 1px solid color-mix(in srgb, var(--color-accent-500) 20%, var(--border-subtle)); border-radius: $size-radius-full; - background: var(--element-bg-medium); - color: var(--color-text-muted); - font-size: 10px; - transition: inherit; + background: color-mix(in srgb, var(--color-accent-500) 8%, transparent); + color: var(--color-accent-600, var(--color-text-secondary)); + font-size: var(--font-size-xs); + font-weight: $font-weight-medium; + cursor: pointer; + white-space: nowrap; + transition: + background $motion-fast $easing-standard, + border-color $motion-fast $easing-standard, + color $motion-fast $easing-standard; - .skills-split__filter-chip.is-active & { - background: color-mix(in srgb, var(--color-accent-500) 15%, transparent); - color: var(--color-accent-500); + &:hover { + background: color-mix(in srgb, var(--color-accent-500) 14%, transparent); + border-color: color-mix(in srgb, var(--color-accent-500) 35%, var(--border-medium)); + color: var(--color-accent-600, var(--color-text-primary)); + } + + &:focus-visible { + outline: 2px solid var(--color-accent-500); + outline-offset: 2px; } } -// Fills remaining space inside framed panel (below header, above pagination) -.skills-split__right-body { +// ── Card Grid ── + +.skills-main__grid { flex: 1; min-height: 0; overflow-y: auto; - overflow-x: hidden; - padding: $size-gap-1 $size-gap-1 $size-gap-2 0; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); + gap: 14px; + padding: 20px 24px; + align-content: start; - &::-webkit-scrollbar { width: 4px; } - &::-webkit-scrollbar-thumb { - background: var(--border-subtle); - border-radius: 2px; - } + &::-webkit-scrollbar { width: 5px; } + &::-webkit-scrollbar-thumb { background: var(--border-subtle); border-radius: 3px; } } -.skills-split__pagination--installed { - flex-shrink: 0; - padding: $size-gap-2 0 0; - margin-top: auto; -} +// ── Card ── -// Installed skill row -.skills-split__installed-row { +.skills-card { display: flex; - align-items: center; - gap: $size-gap-3; - padding: $size-gap-3 $size-gap-2; + flex-direction: column; + gap: 10px; + padding: 16px 18px; + border: 1px solid var(--border-subtle); border-radius: $size-radius-lg; + background: var(--color-bg-secondary); cursor: pointer; - animation: skills-row-in 0.2s $easing-decelerate both; - animation-delay: calc(var(--row-index, 0) * 25ms); + animation: skills-card-in 0.18s $easing-decelerate both; + animation-delay: calc(var(--card-index, 0) * 30ms); transition: - background $motion-fast $easing-standard; + border-color $motion-fast $easing-standard, + box-shadow $motion-fast $easing-standard, + transform $motion-fast $easing-standard; &:hover { - background: var(--element-bg-soft); + border-color: var(--border-medium); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); + transform: translateY(-1px); } &:focus-visible { @@ -644,48 +371,46 @@ $skills-content-top: clamp(36px, 4.5vh, 48px); } &.is-shadowed { - opacity: 0.6; + opacity: 0.72; background: var(--element-bg-subtle); - .skills-split__row-name { + .skills-card__name { text-decoration: line-through; - opacity: 0.7; + opacity: 0.75; } } +} - .badge { - svg { - display: inline-block; - vertical-align: middle; - margin-top: -1px; - } - } +.skills-card__top { + display: flex; + align-items: flex-start; + gap: 12px; } -.skills-split__row-icon { +.skills-card__icon { flex-shrink: 0; - width: 30px; - height: 30px; + width: 34px; + height: 34px; display: flex; align-items: center; justify-content: center; - border-radius: $size-radius-base; + border-radius: 8px; background: var(--element-bg-medium); border: 1px solid var(--border-subtle); color: var(--color-text-secondary); } -.skills-split__row-body { +.skills-card__info { flex: 1; min-width: 0; display: flex; flex-direction: column; - gap: 2px; + gap: 3px; } -.skills-split__row-name { +.skills-card__name { font-size: var(--font-size-sm); - font-weight: $font-weight-medium; + font-weight: $font-weight-semibold; color: var(--color-text-primary); line-height: $line-height-tight; white-space: nowrap; @@ -693,45 +418,77 @@ $skills-content-top: clamp(36px, 4.5vh, 48px); text-overflow: ellipsis; } -.skills-split__row-desc { +.skills-card__desc { font-size: var(--font-size-xs); color: var(--color-text-muted); - opacity: 0.72; + opacity: 0.78; line-height: $line-height-relaxed; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + line-clamp: 2; overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; +} + +.skills-card__meta { + display: flex; + align-items: center; + gap: 8px; + padding-top: 4px; + border-top: 1px solid var(--border-subtle); +} + +.skills-card__path { + flex: 1; min-width: 0; + display: inline-flex; + align-items: center; + gap: 4px; + padding: 0; + border: none; + background: none; + color: var(--color-text-muted); + font-size: 10px; + font-family: $font-family-mono; + line-height: 1; + opacity: 0.65; + text-align: left; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + cursor: pointer; + transition: opacity $motion-fast $easing-standard; + + &:hover { + opacity: 1; + color: var(--color-accent-500); + text-decoration: underline; + text-underline-offset: 2px; + } } -.skills-split__row-end { +.skills-card__actions { display: flex; align-items: center; - gap: $size-gap-2; - flex-shrink: 0; + justify-content: flex-end; + gap: 8px; + margin-top: auto; } -.skills-split__row-delete { +.skills-card__delete { display: inline-flex; align-items: center; justify-content: center; - width: 26px; - height: 26px; - padding: 0; + width: 28px; + height: 28px; border: none; border-radius: $size-radius-base; background: transparent; color: var(--color-text-muted); cursor: pointer; - opacity: 0; transition: background $motion-fast $easing-standard, - color $motion-fast $easing-standard, - opacity $motion-fast $easing-standard; - - .skills-split__installed-row:hover & { - opacity: 1; - } + color $motion-fast $easing-standard; &:hover { background: color-mix(in srgb, var(--color-error) 10%, var(--element-bg-medium)); @@ -741,12 +498,210 @@ $skills-content-top: clamp(36px, 4.5vh, 48px); &:focus-visible { outline: 2px solid var(--color-error); outline-offset: 1px; - opacity: 1; } } // ══════════════════════════════════════════════════════════════════════════════ -// Shared detail / form styles (kept from original) +// Skeleton +// ══════════════════════════════════════════════════════════════════════════════ + +.skills-main__loading { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); + gap: 14px; + padding: 20px 24px; +} + +.skills-card-skeleton { + height: 120px; + border-radius: $size-radius-lg; + background: var(--element-bg-subtle); + border: 1px solid var(--border-subtle); + animation: skills-card-in 0.2s $easing-decelerate both; + animation-delay: calc(var(--card-index, 0) * 40ms); +} + +// ══════════════════════════════════════════════════════════════════════════════ +// Pagination +// ══════════════════════════════════════════════════════════════════════════════ + +.skills-installed__pagination, +.skills-discover__pagination { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + padding: 16px 0 8px; +} + +.skills-installed__page-btn, +.skills-discover__page-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border: 1px solid var(--border-medium); + border-radius: $size-radius-base; + background: var(--element-bg-soft); + color: var(--color-text-secondary); + cursor: pointer; + transition: background $motion-fast $easing-standard, color $motion-fast $easing-standard; + + &:hover:not(:disabled) { + background: var(--element-bg-medium); + color: var(--color-text-primary); + } + + &:disabled { + opacity: 0.4; + cursor: not-allowed; + } +} + +.skills-installed__page-info, +.skills-discover__page-info { + min-width: 60px; + text-align: center; + font-size: var(--font-size-sm); + color: var(--color-text-secondary); +} + +// ══════════════════════════════════════════════════════════════════════════════ +// Empty / Error +// ══════════════════════════════════════════════════════════════════════════════ + +.skills-main__empty, +.skills-discover__empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12px; + min-height: 200px; + padding: 32px; + font-size: var(--font-size-sm); + color: var(--color-text-muted); + text-align: center; + + &--error { + color: var(--color-error); + } +} + +.skills-installed__empty, +.skills-discover__empty { + align-items: center; + justify-content: center; + gap: 12px; + min-height: 160px; + color: var(--color-text-muted); + text-align: center; +} + +// ══════════════════════════════════════════════════════════════════════════════ +// DISCOVER PAGE +// ══════════════════════════════════════════════════════════════════════════════ + +.skills-discover { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; +} + +.skills-discover__hero { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + padding: 40px 24px 32px; + background: linear-gradient(180deg, var(--color-bg-secondary) 0%, var(--color-bg-base) 100%); + border-bottom: 1px solid var(--border-subtle); +} + +.skills-discover__hero-content { + width: 100%; + max-width: 560px; + text-align: center; +} + +.skills-discover__title { + margin: 0 0 8px; + font-size: 24px; + font-weight: 600; + letter-spacing: -0.02em; + line-height: 1.2; +} + +.skills-discover__subtitle { + margin: 0 0 20px; + font-size: var(--font-size-sm); + color: var(--color-text-muted); + line-height: 1.55; +} + +.skills-discover__search-wrapper { + position: relative; +} + +.skills-discover__search { + width: 100%; + + .search__wrapper { + border-radius: $size-radius-lg; + min-height: 46px; + background: var(--element-bg-soft); + border-color: var(--border-medium); + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06); + transition: border-color $motion-fast $easing-standard, box-shadow $motion-fast $easing-standard; + } + + &.search--focused:not(.search--disabled) .search__wrapper { + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1), 0 0 0 3px color-mix(in srgb, var(--color-accent-500) 15%, transparent); + border-color: var(--color-accent-500); + } +} + +.skills-discover__content { + flex: 1; + min-height: 0; + overflow-y: auto; + padding: 20px 24px; + + &::-webkit-scrollbar { width: 6px; } + &::-webkit-scrollbar-thumb { background: var(--border-subtle); border-radius: 3px; } +} + +.skills-discover__results-info { + margin-bottom: 14px; + font-size: var(--font-size-sm); + color: var(--color-text-muted); +} + +.skills-discover__grid { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 16px; + + .skill-card { + width: 100%; + height: 150px; + } +} + +.skills-discover__skeleton-card { + height: 150px; + border-radius: $size-radius-lg; + background: var(--element-bg-subtle); + border: 1px solid var(--border-subtle); + animation: skills-card-in 0.18s $easing-decelerate both; + animation-delay: calc(var(--card-index, 0) * 40ms); +} + +// ══════════════════════════════════════════════════════════════════════════════ +// Shared styles // ══════════════════════════════════════════════════════════════════════════════ .bitfun-skills-scene { @@ -754,6 +709,8 @@ $skills-content-top: clamp(36px, 4.5vh, 48px); display: inline-flex; align-items: center; gap: 4px; + font-size: var(--font-size-xs); + color: var(--color-text-muted); } &__detail-path-btn { @@ -869,122 +826,86 @@ $skills-content-top: clamp(36px, 4.5vh, 48px); // Animations // ══════════════════════════════════════════════════════════════════════════════ -@keyframes skills-spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } +@keyframes skills-card-in { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: translateY(0); } } -@keyframes skills-skeleton-shimmer { - 0% { transform: translateX(-100%); } - 100% { transform: translateX(200%); } -} +// ══════════════════════════════════════════════════════════════════════════════ +// Responsive +// ══════════════════════════════════════════════════════════════════════════════ -@keyframes skills-skeleton-card-in { - from { - opacity: 0; - transform: translateY(8px); - } - to { - opacity: 1; - transform: translateY(0); +@media (max-width: 960px) { + .skills-sidebar { + width: 180px; } -} -@keyframes skills-skeleton-row-in { - from { - opacity: 0; - transform: translateX(6px); - } - to { - opacity: 1; - transform: translateX(0); + .skills-main__grid { + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); } } -@keyframes skills-row-in { - from { - opacity: 0; - transform: translateX(8px); - } - to { - opacity: 1; - transform: translateX(0); +@media (max-width: 640px) { + .skills-tabs-bar { + padding: 10px 16px; } -} -// ══════════════════════════════════════════════════════════════════════════════ -// Responsive -// ══════════════════════════════════════════════════════════════════════════════ + .skills-sidebar { + width: 100%; + flex-direction: row; + height: auto; + border-right: none; + border-bottom: 1px solid var(--border-subtle); -@media (max-width: 860px) { - .skills-split { - flex-direction: column; - gap: $size-gap-6; - overflow-y: auto; - padding-bottom: $size-gap-8; - - &::-webkit-scrollbar { width: 4px; } - &::-webkit-scrollbar-thumb { - background: var(--border-subtle); - border-radius: 2px; + .skills-sidebar__header, + .skills-sidebar__footer { + display: none; } - &__left, - &__right { - flex: none; - overflow: visible; - padding-top: 0; + .skills-sidebar__nav { + flex-direction: row; + padding: 8px; + overflow-x: auto; } - &__market-grid { - grid-template-columns: repeat(2, 1fr); + .skills-sidebar__item { + white-space: nowrap; + width: auto; + padding: 6px 10px; } - } - .skills-split__skeleton-grid { - grid-template-columns: repeat(2, 1fr); + .skills-sidebar__item-icon, + .skills-sidebar__item-count { + display: none; + } } - .skills-split__right-frame { - height: auto; - max-height: none; - min-height: calc(var(--skills-left-body-h) * 2); + .skills-installed { + flex-direction: column; } - .skills-split__right-body { - max-height: min(52vh, 480px); + .skills-main__grid { + grid-template-columns: 1fr; + padding: 12px 16px; } -} -@media (max-width: 640px) { - .skills-split { - padding: 0 $size-gap-4; + .skills-discover__hero { + padding: 28px 16px 20px; } - .skills-split__market-grid { - grid-template-columns: 1fr; + .skills-discover__content { + padding: 16px; } - .skills-split__skeleton-grid { + .skills-discover__grid { grid-template-columns: 1fr; } } @media (prefers-reduced-motion: reduce) { - .skills-split__installed-row, - .skills-split__spinner, - .skills-split__search .search__wrapper, - .skills-split__add-btn, - .skills-split__filter-chip, - .skills-split__page-btn, - .skills-split__row-delete, - .skills-split__skeleton-card, - .skills-split__skeleton-row, - .skills-split__skeleton-card::after, - .skills-split__skeleton-row-avatar::after, - .skills-split__skeleton-line::after, - .skills-split__skeleton-pill::after, - .skills-split__skeleton-icon::after { + .skills-card, + .skills-card-skeleton, + .skills-tabs-bar__tab { animation: none; transition: none; } diff --git a/src/web-ui/src/app/scenes/skills/SkillsScene.tsx b/src/web-ui/src/app/scenes/skills/SkillsScene.tsx index b893c32ac..eefdb48c2 100644 --- a/src/web-ui/src/app/scenes/skills/SkillsScene.tsx +++ b/src/web-ui/src/app/scenes/skills/SkillsScene.tsx @@ -1,18 +1,22 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { + ArrowRight, CheckCircle2, ChevronLeft, ChevronRight, + Download, Filter, FolderOpen, + Layers, Package, Plus, Puzzle, ShieldAlert, - Sparkles, - Store, + ShieldCheck, Trash2, TrendingUp, + User, + Zap, } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { Badge, Button, ConfirmDialog, Input, Modal, Search, Select } from '@/component-library'; @@ -28,14 +32,29 @@ import { useInstalledSkills } from './hooks/useInstalledSkills'; import { useSkillMarket } from './hooks/useSkillMarket'; import SkillCard from './components/SkillCard'; import './SkillsScene.scss'; -import { useSkillsSceneStore } from './skillsSceneStore'; +import { useSkillsSceneStore, type InstalledFilter } from './skillsSceneStore'; import { useGallerySceneAutoRefresh } from '@/app/hooks/useGallerySceneAutoRefresh'; const log = createLogger('SkillsScene'); -const SKILLS_SOURCE_URL = 'https://skills.sh'; +type SkillTab = 'installed' | 'discover'; -const INSTALLED_PAGE_SIZE = 10; +const INSTALLED_PAGE_SIZE = 12; + +interface CategoryInfo { + id: InstalledFilter; + icon: React.ReactNode; + labelKey: string; + descKey: string; +} + +const CATEGORIES: CategoryInfo[] = [ + { id: 'all', icon: , labelKey: 'filters.all', descKey: 'categories.all' }, + { id: 'builtin', icon: , labelKey: 'filters.builtin', descKey: 'categories.builtin' }, + { id: 'user', icon: , labelKey: 'filters.user', descKey: 'categories.user' }, + { id: 'project', icon: , labelKey: 'filters.project', descKey: 'categories.project' }, + { id: 'suite', icon: , labelKey: 'filters.suite', descKey: 'categories.suite' }, +]; const SkillsScene: React.FC = () => { const { t } = useTranslation('scenes/skills'); @@ -54,8 +73,10 @@ const SkillsScene: React.FC = () => { toggleAddForm, } = useSkillsSceneStore(); + const [activeTab, setActiveTab] = useState('installed'); const [deleteTarget, setDeleteTarget] = useState(null); const [installedListPage, setInstalledListPage] = useState(0); + const [installedSearch, setInstalledSearch] = useState(''); const [selectedDetail, setSelectedDetail] = useState< | { type: 'installed'; skill: SkillInfo } | { type: 'market'; skill: SkillMarketItem } @@ -63,7 +84,7 @@ const SkillsScene: React.FC = () => { >(null); const installed = useInstalledSkills({ - searchQuery: searchDraft, + searchQuery: installedSearch, activeFilter: installedFilter, }); @@ -75,7 +96,7 @@ const SkillsScene: React.FC = () => { const market = useSkillMarket({ searchQuery: marketQuery, installedSkillNames, - pageSize: 6, + pageSize: 15, onInstalledChanged: async () => { await installed.loadSkills(true); }, @@ -118,9 +139,13 @@ const SkillsScene: React.FC = () => { const selectedInstalledSkill = selectedDetail?.type === 'installed' ? selectedDetail.skill : null; const selectedMarketSkill = selectedDetail?.type === 'market' ? selectedDetail.skill : null; - const installedFiltered = hideDuplicates - ? installed.filteredSkills.filter((s) => !s.isShadowed) - : installed.filteredSkills; + const installedFiltered = useMemo(() => { + const list = hideDuplicates + ? installed.filteredSkills.filter((s) => !s.isShadowed) + : installed.filteredSkills; + return list; + }, [hideDuplicates, installed.filteredSkills]); + const installedTotalPages = Math.max( 1, Math.ceil(installedFiltered.length / INSTALLED_PAGE_SIZE), @@ -133,238 +158,110 @@ const SkillsScene: React.FC = () => { useEffect(() => { setInstalledListPage(0); - }, [installedFilter, searchDraft]); + }, [installedFilter, installedSearch, hideDuplicates]); useEffect(() => { setInstalledListPage((p) => Math.min(p, Math.max(0, installedTotalPages - 1))); }, [installedTotalPages]); - const marketSkeletonGrid = (keyPrefix: string) => ( -
- {Array.from({ length: 6 }).map((_, i) => ( -
- ))} -
- ); - return (
- {/* ── Two-column split layout ── */} -
- - {/* ══ LEFT: market skills ══ */} -
- {/* Sticky header */} -
-
-
-

{t('page.title')}

-

{t('page.subtitle')}

-
-
- -
- -
-
- - {/* Market body — fixed display, no scroll */} -
-
- {t('market.title')} - - {t('market.subtitlePrefix')} - {' '} - skills.sh - {t('market.subtitleSuffix')} - -
- - {/* Market loading — skeleton grid */} - {market.marketLoading && marketSkeletonGrid('mkt-init')} - - {/* Market error */} - {!market.marketLoading && market.marketError && ( -
- - {market.marketError} -
- )} +
+
+ + + +
+
- {/* Pagination fetch — same skeleton as initial load */} - {!market.marketLoading && !market.marketError && market.loadingMore && marketSkeletonGrid('mkt-page')} +
- {/* Market empty */} - {!market.marketLoading && !market.marketError && !market.loadingMore && market.marketSkills.length === 0 && ( -
- - {marketQuery ? t('market.empty.noMatch') : t('market.empty.noSkills')} + {activeTab === 'installed' && ( +
+
-
- - {/* ══ RIGHT: installed skills ══ */} -
-
- {/* Right header */} -
- {t('installed.titleAll')} -
-
- {([ - ['all', installed.counts.all], - ['user', installed.counts.user], - ['project', installed.counts.project], - ] as const).map(([filter, count]) => ( - ))} - -
+ ); + })} + +
+

+ {t(CATEGORIES.find((c) => c.id === installedFilter)?.descKey ?? 'categories.all')} +

+
+ + +
+
+ setInstalledSearch('')} + placeholder={t('toolbar.searchPlaceholder')} + size="small" + clearable + /> +
-
- {/* Scrollable installed body */} -
- {/* Loading — row skeletons */} {installed.loading && ( -
+
{Array.from({ length: 8 }).map((_, i) => (
-
-
-
-
-
-
-
-
-
-
+ className="skills-card-skeleton" + style={{ '--card-index': i } as React.CSSProperties} + /> ))}
)} - {/* Error */} {!installed.loading && installed.error && ( -
- +
+ {installed.error}
)} - {/* Empty */} {!installed.loading && !installed.error && installedFiltered.length === 0 && ( -
- +
+ {installed.skills.length === 0 ? t('list.empty.noSkills') @@ -373,98 +270,295 @@ const SkillsScene: React.FC = () => {
)} - {/* Installed rows */} - {!installed.loading && !installed.error && pagedInstalledSkills.map((skill, index) => ( -
setSelectedDetail({ type: 'installed', skill })} - role="button" - tabIndex={0} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - setSelectedDetail({ type: 'installed', skill }); - } - }} - aria-label={skill.name} - > -
- -
-
- {skill.name} - {skill.description?.trim() && ( - {skill.description} - )} -
-
e.stopPropagation()} - onKeyDown={(e) => e.stopPropagation()} - > - {skill.isShadowed && ( - - - - {t('list.item.shadowed')} - - + {!installed.loading && !installed.error && ( + <> +
+ {pagedInstalledSkills.map((skill, index) => ( +
setSelectedDetail({ type: 'installed', skill })} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + setSelectedDetail({ type: 'installed', skill }); + } + }} + aria-label={skill.name} + > +
+
+ +
+
+ {skill.name} + {skill.description?.trim() && ( + {skill.description} + )} +
+ {skill.isBuiltin && ( + + + {t('list.item.builtin')} + + )} +
+ +
+ {skill.isShadowed && ( + + + + {t('list.item.shadowed')} + + + )} + + {skill.level === 'user' + ? + : } + {skill.level === 'user' ? t('list.item.user') : t('list.item.project')} + + {skill.path && ( + + )} +
+ +
e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + > + + {!skill.isBuiltin && ( + + )} +
+
+ ))} +
+ + {installedFiltered.length > 0 && installedTotalPages > 1 && ( +
+ + + {t('market.pagination.info', { + current: currentInstalledPage + 1, + total: installedTotalPages, + })} + + +
)} - - {skill.level === 'user' ? t('list.item.user') : t('list.item.project')} - - + + )} +
+
+ )} + + {activeTab === 'discover' && ( +
+
+
+

{t('market.title')}

+

+ {t('market.subtitle')} +

+
+
- ))}
- {!installed.loading && !installed.error && installedFiltered.length > 0 && installedTotalPages > 1 && ( -
- - - {t('market.pagination.info', { - current: currentInstalledPage + 1, - total: installedTotalPages, - })} - - -
- )} +
+ {market.marketLoading && ( +
+ {Array.from({ length: 12 }).map((_, i) => ( +
+ ))} +
+ )} + + {!market.marketLoading && market.marketError && ( +
+ + {market.marketError} +
+ )} + + {!market.marketLoading && !market.marketError && market.loadingMore && ( +
+ {Array.from({ length: 12 }).map((_, i) => ( +
+ ))} +
+ )} + + {!market.marketLoading && !market.marketError && !market.loadingMore && market.marketSkills.length === 0 && ( +
+ + {marketQuery ? t('market.empty.noMatch') : t('market.empty.noSkills')} +
+ )} + + {!market.marketLoading && !market.marketError && !market.loadingMore && market.marketSkills.length > 0 && ( + <> + {marketQuery && ( +
+ + {t('market.resultsInfo', { query: marketQuery, count: market.totalLoaded })} + +
+ )} + +
+ {market.marketSkills.map((skill, index) => { + const isInstalled = installedSkillNames.has(skill.name); + const isDownloading = market.downloadingPackage === skill.installId; + return ( + + + {t('market.item.installed')} + + ) : null} + meta={( + + + {skill.installs ?? 0} + + )} + actions={[ + { + id: 'download', + icon: isInstalled ? : , + ariaLabel: isInstalled ? t('market.item.installed') : t('market.item.downloadProject'), + title: isDownloading + ? t('market.item.downloading') + : (isInstalled ? t('market.item.installedTooltip') : t('market.item.downloadProject')), + disabled: + isDownloading + || !market.hasWorkspace + || market.isRemoteWorkspace + || isInstalled, + tone: isInstalled ? 'success' : 'primary', + onClick: () => void market.handleDownload(skill, 'project'), + }, + ]} + onOpenDetails={() => setSelectedDetail({ type: 'market', skill })} + /> + ); + })} +
+ + {(market.totalPages > 1 || market.hasMore) && ( +
+ + + {market.hasMore + ? t('market.pagination.infoMore', { current: market.currentPage + 1 }) + : t('market.pagination.info', { current: market.currentPage + 1, total: market.totalPages })} + + +
+ )} + + )} +
-
+ )}
- {/* ── Detail modal ── */} setSelectedDetail(null)} @@ -486,6 +580,9 @@ const SkillsScene: React.FC = () => { )} + + {selectedInstalledSkill.isBuiltin ? t('list.item.builtin') : t('list.item.userInstalled')} + {selectedInstalledSkill.level === 'user' ? t('list.item.user') : t('list.item.project')} @@ -503,7 +600,7 @@ const SkillsScene: React.FC = () => { {selectedMarketSkill.installs ?? 0} ) : null} - actions={selectedInstalledSkill ? ( + actions={selectedInstalledSkill && !selectedInstalledSkill.isBuiltin ? (
- {/* ── Delete confirm ── */} setDeleteTarget(null)} diff --git a/src/web-ui/src/app/scenes/skills/components/SkillCard.scss b/src/web-ui/src/app/scenes/skills/components/SkillCard.scss index 73b1494d1..d55cf9afd 100644 --- a/src/web-ui/src/app/scenes/skills/components/SkillCard.scss +++ b/src/web-ui/src/app/scenes/skills/components/SkillCard.scss @@ -1,7 +1,7 @@ @use '../../../../component-library/styles/tokens' as *; /* ─────────────────────────────────────────────────── - SkillCard — vertical card with glassmorphism effect + SkillCard — compact card for discover tab grid DOM: .skill-card ::before (decorative circle blur) @@ -18,9 +18,10 @@ ─────────────────────────────────────────────────── */ .skill-card { - width: 360px; - height: 200px; - border-radius: 15px; + width: 100%; + height: 100%; + min-height: 150px; + border-radius: 12px; background: var(--element-bg-soft); display: flex; flex-direction: column; @@ -33,14 +34,14 @@ transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.35s ease; - // Top gradient overlay - shows on hover, covers entire card except footer + // Top gradient overlay &::before { content: ""; position: absolute; top: 0; left: 0; right: 0; - bottom: 40px; + bottom: 0; background: var(--skill-card-gradient); opacity: 0; transition: opacity 0.35s ease; @@ -48,16 +49,12 @@ z-index: 0; } - &--no-actions::before { - bottom: 0; - } - &:hover { - transform: translateY(-4px) scale(1.02); - box-shadow: 0 16px 40px rgba(0, 0, 0, 0.2); + transform: translateY(-3px) scale(1.01); + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.15); &::before { - opacity: 0.4; + opacity: 0.25; } } @@ -71,8 +68,7 @@ display: flex; align-items: flex-start; justify-content: space-between; - padding: $size-gap-3; - padding-bottom: 0; + padding: 12px 12px 8px; position: relative; z-index: 1; } @@ -81,8 +77,8 @@ display: flex; align-items: center; justify-content: center; - width: 32px; - height: 32px; + width: 30px; + height: 30px; border-radius: 8px; background: rgba(255, 255, 255, 0.12); backdrop-filter: blur(8px); @@ -104,7 +100,7 @@ // ── Body ── &__body { flex: 1; - padding: $size-gap-2 $size-gap-3; + padding: 0 12px; display: flex; flex-direction: column; gap: 4px; @@ -116,12 +112,12 @@ &__title-row { display: flex; align-items: center; - gap: $size-gap-2; + gap: 8px; min-width: 0; } &__name { - font-size: 0.92em; + font-size: 0.9em; font-weight: $font-weight-semibold; color: var(--color-text-primary); line-height: $line-height-base; @@ -165,10 +161,11 @@ display: flex; align-items: center; width: 100%; - border-radius: 0 0 15px 15px; + border-radius: 0 0 12px 12px; overflow: hidden; position: relative; z-index: 1; + margin-top: auto; // Bottom gradient blur background matching card color &::after { @@ -179,14 +176,14 @@ right: 0; bottom: 0; background: var(--skill-card-gradient); - opacity: 0.5; + opacity: 0.35; transition: opacity 0.35s ease; pointer-events: none; } } &:hover &__footer::after { - opacity: 1; + opacity: 0.6; } &__actions { @@ -202,7 +199,7 @@ display: inline-flex; align-items: center; justify-content: center; - height: 35px; + height: 32px; padding: 0; border: none; background: rgba(255, 255, 255, 0.08); @@ -244,14 +241,6 @@ } } -// ── Responsive ── -@media (max-width: 720px) { - .skill-card { - width: 100%; - min-height: 180px; - } -} - // ── Animations ── @keyframes skill-card-in { from { diff --git a/src/web-ui/src/app/scenes/skills/components/SkillCard.tsx b/src/web-ui/src/app/scenes/skills/components/SkillCard.tsx index ab0f9e436..b1683f4b6 100644 --- a/src/web-ui/src/app/scenes/skills/components/SkillCard.tsx +++ b/src/web-ui/src/app/scenes/skills/components/SkillCard.tsx @@ -41,11 +41,9 @@ const SkillCard: React.FC = ({ const Icon = iconKind === 'market' ? Package : Puzzle; const openDetails = () => onOpenDetails?.(); - const hasActions = actions.length > 0; - return (
= ({
{/* Footer: action buttons */} - {hasActions && ( + {actions.length > 0 && (
e.stopPropagation()}> {actions.map((action) => ( diff --git a/src/web-ui/src/app/scenes/skills/hooks/useInstalledSkills.ts b/src/web-ui/src/app/scenes/skills/hooks/useInstalledSkills.ts index 2623b0697..e93219bc7 100644 --- a/src/web-ui/src/app/scenes/skills/hooks/useInstalledSkills.ts +++ b/src/web-ui/src/app/scenes/skills/hooks/useInstalledSkills.ts @@ -168,7 +168,17 @@ export function useInstalledSkills({ searchQuery, activeFilter }: UseInstalledSk const filteredSkills = useMemo(() => { return skills.filter((skill) => { - const matchesFilter = activeFilter === 'all' || skill.level === activeFilter; + let matchesFilter = true; + if (activeFilter === 'user') { + matchesFilter = skill.level === 'user' && !skill.isBuiltin; + } else if (activeFilter === 'project') { + matchesFilter = skill.level === 'project' && !skill.isBuiltin; + } else if (activeFilter === 'builtin') { + matchesFilter = skill.isBuiltin; + } else if (activeFilter === 'suite') { + matchesFilter = false; + } + const matchesQuery = !normalizedQuery || [ skill.name, skill.description, @@ -180,8 +190,10 @@ export function useInstalledSkills({ searchQuery, activeFilter }: UseInstalledSk const counts = useMemo(() => ({ all: skills.length, - user: skills.filter((skill) => skill.level === 'user').length, - project: skills.filter((skill) => skill.level === 'project').length, + builtin: skills.filter((skill) => skill.isBuiltin).length, + user: skills.filter((skill) => skill.level === 'user' && !skill.isBuiltin).length, + project: skills.filter((skill) => skill.level === 'project' && !skill.isBuiltin).length, + suite: 0, }), [skills]); return { diff --git a/src/web-ui/src/app/scenes/skills/skillsSceneStore.ts b/src/web-ui/src/app/scenes/skills/skillsSceneStore.ts index b14575354..e7fada41c 100644 --- a/src/web-ui/src/app/scenes/skills/skillsSceneStore.ts +++ b/src/web-ui/src/app/scenes/skills/skillsSceneStore.ts @@ -1,6 +1,6 @@ import { create } from 'zustand'; -export type InstalledFilter = 'all' | 'user' | 'project'; +export type InstalledFilter = 'all' | 'builtin' | 'user' | 'project' | 'suite'; interface SkillsSceneState { searchDraft: string; diff --git a/src/web-ui/src/locales/en-US/scenes/skills.json b/src/web-ui/src/locales/en-US/scenes/skills.json index 3687bd097..0cee6f8c5 100644 --- a/src/web-ui/src/locales/en-US/scenes/skills.json +++ b/src/web-ui/src/locales/en-US/scenes/skills.json @@ -41,8 +41,17 @@ }, "filters": { "all": "All", + "builtin": "Built-in", "user": "User", - "project": "Project" + "project": "Project", + "suite": "Suites" + }, + "categories": { + "all": "All installed skills, including built-in, user-level, and project-level.", + "builtin": "Core skills shipped with the app. They cannot be deleted.", + "user": "User-level skills installed globally for your account.", + "project": "Project-level skills for the current workspace.", + "suite": "Curated skill bundles that package related skills together (coming soon)." }, "section": { "user": { @@ -64,6 +73,7 @@ "noMatch": "No matching marketplace skills found", "noSkills": "No marketplace skills available" }, + "resultsInfo": "{{count}} results for \"{{query}}\"", "item": { "sourceLabel": "Source: ", "installs": "Installs: {{count}}", @@ -98,7 +108,8 @@ "user": "User-level (Global)", "project": "Project-level (Current Workspace)", "projectDisabled": " - Need to open workspace first", - "currentWorkspace": "Current workspace: {{path}}" + "currentWorkspace": "Current workspace: {{path}}", + "selectedProjectPath": "Selected project path: {{path}}" }, "path": { "label": "Skill Folder Path", @@ -123,6 +134,9 @@ "item": { "user": "User", "project": "Project", + "builtin": "Built-in", + "userInstalled": "Installed", + "detail": "Details", "deleteTooltip": "Delete", "pathLabel": "Path:", "openPathInExplorer": "Open this folder in file explorer", diff --git a/src/web-ui/src/locales/zh-CN/scenes/skills.json b/src/web-ui/src/locales/zh-CN/scenes/skills.json index 106ad1f55..629e7270e 100644 --- a/src/web-ui/src/locales/zh-CN/scenes/skills.json +++ b/src/web-ui/src/locales/zh-CN/scenes/skills.json @@ -41,8 +41,17 @@ }, "filters": { "all": "全部", + "builtin": "内置", "user": "用户级", - "project": "项目级" + "project": "项目级", + "suite": "套件" + }, + "categories": { + "all": "查看所有已安装的技能,包含内置、用户和项目级。", + "builtin": "系统出厂自带的核心技能,不可删除。", + "user": "全局安装到当前账户的用户级技能。", + "project": "当前工作区下的项目级技能。", + "suite": "精选技能套件将多个相关技能打包提供(敬请期待)。" }, "section": { "user": { @@ -64,6 +73,7 @@ "noMatch": "没有找到匹配的市场技能", "noSkills": "暂时没有可展示的市场技能" }, + "resultsInfo": "关键词「{{query}}」共 {{count}} 条相关结果", "item": { "sourceLabel": "来源: ", "installs": "安装量: {{count}}", @@ -98,7 +108,8 @@ "user": "用户级(全局)", "project": "项目级(当前工作空间)", "projectDisabled": " - 需要先打开工作区", - "currentWorkspace": "当前工作区: {{path}}" + "currentWorkspace": "当前工作区: {{path}}", + "selectedProjectPath": "所选项目路径: {{path}}" }, "path": { "label": "技能文件夹路径", @@ -123,6 +134,9 @@ "item": { "user": "用户级", "project": "项目级", + "builtin": "内置", + "userInstalled": "已安装", + "detail": "详情", "deleteTooltip": "删除", "pathLabel": "路径:", "openPathInExplorer": "在资源管理器中打开此文件夹", diff --git a/src/web-ui/src/locales/zh-TW/scenes/skills.json b/src/web-ui/src/locales/zh-TW/scenes/skills.json index 33e7718ec..d05a66d4f 100644 --- a/src/web-ui/src/locales/zh-TW/scenes/skills.json +++ b/src/web-ui/src/locales/zh-TW/scenes/skills.json @@ -41,8 +41,17 @@ }, "filters": { "all": "全部", + "builtin": "內建", "user": "用戶級", - "project": "項目級" + "project": "項目級", + "suite": "套件" + }, + "categories": { + "all": "查看所有已安裝的技能,包含內建、用戶與項目級。", + "builtin": "系統出廠內建的核心技能,不可刪除。", + "user": "為目前帳戶全域安裝的用戶級技能。", + "project": "目前工作區底下的項目級技能。", + "suite": "精選技能套件將多個相關技能打包提供(敬請期待)。" }, "section": { "user": { @@ -64,6 +73,7 @@ "noMatch": "沒有找到匹配的市場技能", "noSkills": "暫時沒有可展示的市場技能" }, + "resultsInfo": "關鍵詞「{{query}}」共 {{count}} 筆相關結果", "item": { "sourceLabel": "來源: ", "installs": "安裝量: {{count}}", @@ -98,7 +108,8 @@ "user": "用戶級(全局)", "project": "項目級(當前工作空間)", "projectDisabled": " - 需要先打開工作區", - "currentWorkspace": "當前工作區: {{path}}" + "currentWorkspace": "當前工作區: {{path}}", + "selectedProjectPath": "所選項目路徑: {{path}}" }, "path": { "label": "技能文件夾路徑", @@ -123,6 +134,9 @@ "item": { "user": "用戶級", "project": "項目級", + "builtin": "內建", + "userInstalled": "已安裝", + "detail": "詳情", "deleteTooltip": "刪除", "pathLabel": "路徑:", "openPathInExplorer": "在資源管理器中打開此文件夾",