From f72b15ec0a5ddb974abb7aabdbb40230e1847b26 Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Tue, 6 Oct 2026 17:49:42 +1100 Subject: [PATCH] fix(skills): close the SkillCard review findings from #453 A refused clipboard left the run pill silent, so a visitor had no way to get the command. The pill now shows it selected, as the run chip does, and the card's copy returns its result so the pill can tell. The curator profile built SKILL.md links no card shows, and /me asked for one its API never sends. Both are gone. DESIGN.md now names the trending entries the card's row layout replaces. Repository rows in the home update feed kept their old style beside the compact Skill rows. They now use the card's measures. --- DESIGN.md | 2 +- app/assets/css/main.css | 90 -------------------- app/components/SkillCard.vue | 2 +- app/components/skill-card/_SkillCardRun.vue | 87 +++++++++++++++---- app/pages/@[login]/index.vue | 29 ------- app/pages/index.vue | 92 +++++++++++++++++++-- app/types/skill-card.ts | 5 +- app/utils/skill-card-view.ts | 3 +- layers/identity/app/pages/me/index.vue | 1 - test/unit/skill-card-run-fallback.test.ts | 31 +++++++ 10 files changed, 193 insertions(+), 149 deletions(-) create mode 100644 test/unit/skill-card-run-fallback.test.ts diff --git a/DESIGN.md b/DESIGN.md index 102999c1..c752982b 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -469,4 +469,4 @@ These are editorial sets, not necessarily database collections yet. If a set has - **Trending rows scan in one pass**: the rank is a plain mono number, and the first three are a step darker. Each metadata line runs source, stars, then the signal that ranked the row, and an empty slot renders nothing. Descriptions stop at two lines, the run chip is as wide as its command, and the post carousel shows whole cards only. The sidebar invitation drops the email preview and the card. Its button is the rail's one solid rose element, so the install chip beside it inks its dot. Harlan asked for this polish on 2026-10-06. - **A six-step lifecycle band replaces the four tiles**: under the hero, one band maps the whole product in its real order: find, run, install or fork, keep current, make, build on. The site and the CLI share one line. Each step is one link: a mono `01` marker and step name, then one sans line that opens with its pitch line where COPY.md has one. A line of stone dots joins the steps. Run, the default, carries the band's one rose dot, so no step lights rose on hover. Six columns from 1024px; below that a vertical list with the dots down the left. Keep the band no taller than the tiles it replaced: the hero and the band must fit a 1440×900 viewport. Harlan approved it on 2026-10-06. - **A Developers menu groups the build surfaces**: the desktop header keeps Trending Skills and Find Skills as links, and folds CLI, MCP server, SDK, Make a skill and Skillgen into one Developers menu, as nuxtseo.com does. The trigger is a button, so a keyboard opens the panel without leaving the page; the panel's last link reaches `/developers`. Each item is a card with a lucide icon tile and one `.editorial-atmosphere` layer. The three ways to connect share the top row, and the two authoring cards share the row below. CLI takes rose because it is the recommended path; the others take ember or stone. The mobile menu lists the same five items as a plain group. Harlan asked for this on 2026-10-06. -- **One Skill card**: `SkillCard` renders every Skill embed, in three layouts. Each opens with one identity unit: the avatar beside the mono name, over a byline of author and Repository. Then come at most two lines of description and one line of facts with a dashed `run` pill. The pill is the run chip's material without `npx skilld`. Its click copies the full command, and its dot stays stone until hover, focus or copy. Rows read in three zones, who, what and the facts, and stack like a borderless card under 40rem. Compact entries drop the description. Cards show no SKILL.md icon and no heart by default: with few accounts yet, a row of zero hearts reads as an empty room. Harlan chose this from five takes, one by Fable, on 2026-10-06. It supersedes the `/skills` table and its "What it does" column. Trending and track boards render the same rows: the reason a Skill ranked fills the meta line, and the posts about it sit under its words as an aside. The home freshness ledgers and the community top Skill use compact entries. Search results keep their own rows, because they are a keyboard picker, not an embed. +- **One Skill card**: `SkillCard` renders every Skill embed, in three layouts. Each opens with one identity unit: the avatar beside the mono name, over a byline of author and Repository. Then come at most two lines of description and one line of facts with a dashed `run` pill. The pill is the run chip's material without `npx skilld`. Its click copies the full command, and its dot stays stone until hover, focus or copy. Rows read in three zones, who, what and the facts, and stack like a borderless card under 40rem. Compact entries drop the description. Cards show no SKILL.md icon and no heart by default: with few accounts yet, a row of zero hearts reads as an empty room. Harlan chose this from five takes, one by Fable, on 2026-10-06. It supersedes the `/skills` table and its "What it does" column. Trending and track boards render the same rows: the reason a Skill ranked fills the meta line, and the posts about it sit under its words as an aside. The home freshness ledgers and the community top Skill use compact entries. Search results keep their own rows, because they are a keyboard picker, not an embed. This replaces the row layout in "Trending rows lead with the Skill", "Trending shares the /skills shell" and "Trending rows scan in one pass": posts now sit under the Skill's words, not beside it, and the dashed `run` pill replaces the full run chip. The /skills shell, the sidebar and the rank styling stand. diff --git a/app/assets/css/main.css b/app/assets/css/main.css index 3c9659aa..ea1e85d4 100644 --- a/app/assets/css/main.css +++ b/app/assets/css/main.css @@ -590,86 +590,6 @@ border-bottom: 1px solid var(--ui-border); } -.home-freshness-row { - display: grid; - min-width: 0; - grid-template-columns: auto minmax(0, 1fr) auto auto; - align-items: center; - gap: 0.75rem; - padding-inline: 0.75rem; - transition: background-color 200ms ease-out; -} - -.home-freshness-row--primary { - min-block-size: 5rem; -} - -.home-freshness-row--secondary { - min-block-size: 4.5rem; - padding-inline: 0.5rem; -} - -.home-freshness-row:focus-visible { - z-index: 1; - outline: 2px solid var(--ui-color-primary-500) !important; - outline-offset: -3px !important; -} - -.home-freshness-avatar { - flex: none; - border: 2px solid var(--ui-bg); - border-radius: 9999px; - outline: 1px solid var(--ui-border); - background: var(--ui-bg-muted); - object-fit: cover; -} - -.home-freshness-avatar--primary { - width: 2.25rem; - height: 2.25rem; -} - -.home-freshness-avatar--secondary { - width: 2rem; - height: 2rem; -} - -.home-freshness-row-title, -.home-freshness-row-description { - display: block; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.home-freshness-row-title { - font-family: var(--font-mono); - font-size: 0.875rem; - font-weight: 500; - line-height: 1.25rem; -} - -.home-freshness-row-description { - margin-top: 0.125rem; - font-size: 0.8125rem; - line-height: 1.25rem; - color: var(--ui-text-muted); -} - -.home-freshness-time { - flex: none; - font-family: var(--font-mono); - font-size: 0.75rem; - font-variant-numeric: tabular-nums; - line-height: 1rem; - color: var(--ui-text-muted); -} - -.home-freshness-arrow { - color: var(--ui-text-muted); - transition: color 200ms ease-out; -} - .home-freshness-state { display: flex; min-width: 0; @@ -718,16 +638,6 @@ } } -@media (hover: hover) { - .home-freshness-row:hover { - background: var(--ui-bg-muted); - } - - .home-freshness-row:hover .home-freshness-arrow { - color: var(--ui-text); - } -} - @media (forced-colors: active) { .editorial-atmosphere { display: none; diff --git a/app/components/SkillCard.vue b/app/components/SkillCard.vue index 3388729a..51f03031 100644 --- a/app/components/SkillCard.vue +++ b/app/components/SkillCard.vue @@ -80,7 +80,7 @@ const { copy, copied } = useInstallCopy( const view = computed(() => buildSkillCardView( skill, { layout, byline, metric, actions, description, rank, note, trending, surface }, - { copied: copied.value, copy: () => { void copy(runCommand.value) } }, + { copied: copied.value, copy: () => copy(runCommand.value) }, )) const rankText = computed(() => view.value.rank == null ? null : String(view.value.rank).padStart(2, '0')) diff --git a/app/components/skill-card/_SkillCardRun.vue b/app/components/skill-card/_SkillCardRun.vue index e267a3ca..26489d4b 100644 --- a/app/components/skill-card/_SkillCardRun.vue +++ b/app/components/skill-card/_SkillCardRun.vue @@ -9,6 +9,9 @@ import type { SkillCardView } from '~/types/skill-card' * * The dot stays stone until a visitor reaches for it, so a grid of cards * spends no rose at rest. + * + * If the clipboard refuses, the command appears beside the pill, selected, + * as the run chip does, so a visitor can still copy it with the keyboard. */ const { run, title } = defineProps<{ run: NonNullable @@ -17,9 +20,27 @@ const { run, title } = defineProps<{ }>() const marching = ref(false) +const refused = ref(false) +const status = ref('') +const commandRef = useTemplateRef('command') + +function selectCommand() { + const el = commandRef.value + const selection = window.getSelection() + if (el && selection) + selection.selectAllChildren(el) +} async function onCopy() { - run.copy() + const result = await run.copy() + if (result._tag === 'error') { + refused.value = true + status.value = 'Selected. Copy it with your keyboard.' + await nextTick() + selectCommand() + return + } + status.value = 'Copied.' // Restart the march if a second copy lands before the first one ends. marching.value = false await nextTick() @@ -30,24 +51,58 @@ async function onCopy() {