diff --git a/DESIGN.md b/DESIGN.md index 569af972..48286761 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -470,4 +470,4 @@ These are editorial sets, not necessarily database collections yet. If a set has - **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 and Make a skill 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`. Harlan asked for this on 2026-10-06. - **The Developers menu is a bento**: the three ways to connect stack as compact rows, each a lucide icon tile, a name and one line. Make a skill fills the second column as the one large card, with an ember atmosphere and the File minimap in its corner. It names both routes: the guides and Skillgen. `/make-skill` is the one entry for authors and routes to either, so Skillgen has no card of its own. The mobile menu lists the same four items as a plain group. Harlan chose this over five uniform cards 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() {