Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/components/Button/Base.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const props = withDefaults(
/** @default "secondary" */
variant?: 'primary' | 'secondary'
/** @default "medium" */
size?: 'small' | 'medium'
size?: 'small' | 'medium' | 'square'
/** Keyboard shortcut hint */
ariaKeyshortcuts?: string
/** Forces the button to occupy the entire width of its container. */
Expand Down Expand Up @@ -43,6 +43,7 @@ defineExpose({
'flex': block,
'text-sm px-4 py-2': size === 'medium',
'text-xs px-2 py-0.5': size === 'small',
'p-1': size === 'square',
'bg-transparent text-fg hover:enabled:(bg-fg/10) focus-visible:enabled:(bg-fg/10) aria-pressed:(bg-fg/10 border-fg/20 hover:enabled:(bg-fg/20 text-fg/50))':
variant === 'secondary',
'text-bg bg-fg hover:enabled:(bg-fg/50) focus-visible:enabled:(bg-fg/50) aria-pressed:(bg-fg text-bg border-fg hover:enabled:(text-bg/50))':
Expand Down
12 changes: 6 additions & 6 deletions app/components/Package/SkillsModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -131,14 +131,14 @@ function getWarningTooltip(skill: SkillListItem): string | undefined {
<span class="text-fg">npx </span>
<span class="text-fg-muted">skills add {{ baseUrl }}/{{ packageName }}</span>
</code>
<button
<ButtonBase
type="button"
class="absolute top-0 inset-ie-0 px-2 py-0.5 font-mono text-xs text-fg-muted bg-bg-subtle/80 border border-border rounded transition-colors duration-200 opacity-0 group-hover/cmd:opacity-100 hover:(text-fg border-border-hover) active:scale-95 focus-visible:opacity-100 focus-visible:outline-accent/70"
size="square"
class="absolute top-0 inset-ie-0 px-2 py-0.5 font-mono text-xs text-fg-muted bg-bg-subtle/80 border-border media-mouse:opacity-0 media-mouse:group-hover:opacity-100 media-mouse:focus-within:opacity-100 active:scale-95 focus-visible:opacity-100"
:aria-label="$t('package.get_started.copy_command')"
:classicon="copied ? 'i-lucide:check' : 'i-lucide:copy'"
@click.stop="copyCommand"
>
<span aria-live="polite">{{ copied ? $t('common.copied') : $t('common.copy') }}</span>
</button>
/>
</div>
</div>
</div>
Expand Down Expand Up @@ -205,7 +205,7 @@ function getWarningTooltip(skill: SkillListItem): string | undefined {
<span v-if="skill.fileCounts?.references" class="text-fg-subtle">
<span class="i-lucide:file-text size-3 align-[-2px] me-0.5" />{{
$t(
'package.skills.file_counts.refs',
'package.skills.file_counts.references',
{ count: skill.fileCounts.references },
skill.fileCounts.references,
)
Expand Down
10 changes: 5 additions & 5 deletions app/components/Terminal/Execute.vue
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,14 @@ const copyExecuteCommand = () => copyExecute(getFullExecuteCommand())
>{{ i > 0 ? ' ' : '' }}{{ part }}</span
></code
>
<button
<ButtonBase
type="button"
class="px-2 py-0.5 font-mono text-xs text-fg-muted bg-bg-subtle/80 border border-border rounded transition-colors duration-200 opacity-0 group-hover/executecmd:opacity-100 hover:(text-fg border-border-hover) active:scale-95 focus-visible:opacity-100 focus-visible:outline-accent/70"
size="square"
class="text-fg-muted bg-bg-subtle/80 border-border media-mouse:opacity-0 media-mouse:group-hover:opacity-100 media-mouse:focus-within:opacity-100 active:scale-95 focus-visible:opacity-100 select-none"
:aria-label="$t('package.get_started.copy_command')"
:classicon="executeCopied ? 'i-lucide:check' : 'i-lucide:copy'"
@click.stop="copyExecuteCommand"
>
{{ executeCopied ? $t('common.copied') : $t('common.copy') }}
</button>
/>
</div>
</div>
</div>
Expand Down
44 changes: 20 additions & 24 deletions app/components/Terminal/Install.vue
Original file line number Diff line number Diff line change
Expand Up @@ -151,14 +151,14 @@ const copyDevInstallCommand = () =>
>{{ i > 0 ? ' ' : '' }}{{ part }}</span
></code
>
<button
<ButtonBase
type="button"
class="px-2 py-0.5 font-mono text-xs text-fg-muted bg-bg-subtle/80 border border-border rounded transition-colors duration-200 opacity-0 group-hover/installcmd:opacity-100 hover:(text-fg border-border-hover) active:scale-95 focus-visible:opacity-100 focus-visible:outline-accent/70 select-none"
size="square"
class="text-fg-muted bg-bg-subtle/80 border-border media-mouse:opacity-0 media-mouse:group-hover:opacity-100 media-mouse:focus-within:opacity-100 active:scale-95 focus-visible:opacity-100 select-none"
:aria-label="$t('package.get_started.copy_command')"
:classicon="copied ? 'i-lucide:check' : 'i-lucide:copy'"
@click.stop="copyInstallCommand"
>
<span aria-live="polite">{{ copied ? $t('common.copied') : $t('common.copy') }}</span>
</button>
/>
</div>

<!-- Suggested dev dependency install command -->
Expand All @@ -185,15 +185,12 @@ const copyDevInstallCommand = () =>
>
<ButtonBase
type="button"
size="small"
class="text-fg-muted bg-bg-subtle/80 border-border opacity-0 group-hover/devinstallcmd:opacity-100 active:scale-95 focus-visible:opacity-100 select-none"
size="square"
class="text-fg-muted bg-bg-subtle/80 border-border media-mouse:opacity-0 media-mouse:group-hover:opacity-100 media-mouse:focus-within:opacity-100 active:scale-95 focus-visible:opacity-100 select-none"
:aria-label="$t('package.get_started.copy_dev_command')"
:classicon="devInstallCopied ? 'i-lucide:check' : 'i-lucide:copy'"
@click.stop="copyDevInstallCommand"
>
<span aria-live="polite">{{
devInstallCopied ? $t('common.copied') : $t('common.copy')
}}</span>
</ButtonBase>
/>
</div>
</template>

Expand Down Expand Up @@ -249,13 +246,14 @@ const copyDevInstallCommand = () =>
>{{ i > 0 ? ' ' : '' }}{{ part }}</span
></code
>
<button
<ButtonBase
type="button"
class="px-2 py-0.5 font-mono text-xs text-fg-muted bg-bg-subtle/80 border border-border rounded transition-colors duration-200 opacity-0 group-hover/runcmd:opacity-100 hover:(text-fg border-border-hover) active:scale-95 focus-visible:opacity-100 focus-visible:outline-accent/70 select-none"
size="square"
class="text-fg-muted bg-bg-subtle/80 border-border media-mouse:opacity-0 media-mouse:group-hover:opacity-100 media-mouse:focus-within:opacity-100 active:scale-95 focus-visible:opacity-100 select-none"
:aria-label="$t('package.run.copy_command')"
:classicon="runCopied ? 'i-lucide:check' : 'i-lucide:copy'"
@click.stop="copyRunCommand(executableInfo?.primaryCommand)"
>
{{ runCopied ? $t('common.copied') : $t('common.copy') }}
</button>
/>
</div>
</template>

Expand Down Expand Up @@ -294,16 +292,14 @@ const copyDevInstallCommand = () =>
>{{ i > 0 ? ' ' : '' }}{{ part }}</span
></code
>
<button
<ButtonBase
type="button"
class="px-2 py-0.5 font-mono text-xs text-fg-muted bg-bg-subtle/80 border border-border rounded transition-colors duration-200 opacity-0 group-hover/createcmd:opacity-100 hover:(text-fg border-border-hover) active:scale-95 focus-visible:opacity-100 focus-visible:outline-accent/70 select-none"
size="square"
class="text-fg-muted bg-bg-subtle/80 border-border media-mouse:opacity-0 media-mouse:group-hover:opacity-100 media-mouse:focus-within:opacity-100 active:scale-95 focus-visible:opacity-100 select-none"
:aria-label="$t('package.create.copy_command')"
:classicon="createCopied ? 'i-lucide:check' : 'i-lucide:copy'"
@click.stop="copyCreateCommand"
>
<span aria-live="polite">{{
createCopied ? $t('common.copied') : $t('common.copy')
}}</span>
</button>
/>
</div>
</template>
</div>
Expand Down
6 changes: 5 additions & 1 deletion i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,11 @@
},
"run": {
"title": "Run",
"locally": "Run locally"
"locally": "Run locally",
"copy_command": "Copy command to run locally"
},
"command": {
"copied": "Command copied"
},
"readme": {
"title": "Readme",
Expand Down
12 changes: 12 additions & 0 deletions i18n/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -966,6 +966,18 @@
},
"locally": {
"type": "string"
},
"copy_command": {
"type": "string"
}
},
"additionalProperties": false
},
"command": {
"type": "object",
"properties": {
"copied": {
"type": "string"
}
},
"additionalProperties": false
Expand Down
91 changes: 68 additions & 23 deletions test/e2e/create-command.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ test.describe('Create Command', () => {
})

test.describe('Copy Functionality', () => {
test('hovering create command shows copy button', async ({ page, goto }) => {
test('copy button is accessible and keyboard discoverable', async ({ page, goto }) => {
await goto('/package/vite', { waitUntil: 'hydration' })

await expect(page.locator('h1')).toContainText('vite', { timeout: 15000 })
Expand All @@ -75,15 +75,13 @@ test.describe('Create Command', () => {
const createCommandContainer = page.locator('.group\\/createcmd').first()
await expect(createCommandContainer).toBeVisible({ timeout: 20000 })

// Copy button should initially be hidden (opacity-0)
// Copy button should be in the DOM and accessible to screen readers
const copyButton = createCommandContainer.locator('button')
await expect(copyButton).toHaveCSS('opacity', '0')
await expect(copyButton).toBeAttached()

// Hover over the container
await createCommandContainer.hover()

// Copy button should become visible
await expect(copyButton).toHaveCSS('opacity', '1')
// Focus the button to verify it's keyboard accessible
await copyButton.focus()
await expect(copyButton).toBeFocused()
})

test('clicking copy button copies create command and shows confirmation', async ({
Expand All @@ -104,9 +102,6 @@ test.describe('Create Command', () => {
const createCommandContainer = page.locator('.group\\/createcmd').first()
await expect(createCommandContainer).toBeVisible({ timeout: 20000 })

await createCommandContainer.hover()

// Click the copy button
const copyButton = createCommandContainer.locator('button')
await copyButton.click()

Expand All @@ -123,22 +118,20 @@ test.describe('Create Command', () => {
})

test.describe('Install Command Copy', () => {
test('hovering install command shows copy button', async ({ page, goto }) => {
test('copy button is accessible and keyboard discoverable', async ({ page, goto }) => {
await goto('/package/is-odd', { waitUntil: 'hydration' })

// Find the install command container
const installCommandContainer = page.locator('.group\\/installcmd').first()
await expect(installCommandContainer).toBeVisible()

// Copy button should initially be hidden
// Copy button should be in the DOM and accessible to screen readers
const copyButton = installCommandContainer.locator('button')
await expect(copyButton).toHaveCSS('opacity', '0')

// Hover over the container
await installCommandContainer.hover()
await expect(copyButton).toBeAttached()

// Copy button should become visible
await expect(copyButton).toHaveCSS('opacity', '1')
// Focus the button to verify it's keyboard accessible
await copyButton.focus()
await expect(copyButton).toBeFocused()
})

test('clicking copy button copies install command and shows confirmation', async ({
Expand All @@ -151,11 +144,7 @@ test.describe('Create Command', () => {

await goto('/package/is-odd', { waitUntil: 'hydration' })

// Find and hover over the install command container
const installCommandContainer = page.locator('.group\\/installcmd').first()
await installCommandContainer.hover()

// Click the copy button
const copyButton = installCommandContainer.locator('button')
await copyButton.click()

Expand All @@ -170,4 +159,60 @@ test.describe('Create Command', () => {
await expect(copyButton).not.toContainText(/copied/i)
})
})

test.describe('Run Command Copy', () => {
test('copy button is accessible and keyboard discoverable', async ({ page, goto }) => {
await goto('/package/vite', { waitUntil: 'hydration' })

await expect(page.locator('h1')).toContainText('vite', { timeout: 15000 })

await expect(page.locator('main header').locator('text=/v\\d+\\.\\d+/')).toBeVisible({
timeout: 15000,
})

// Find the run command container
const runCommandContainer = page.locator('.group\\/runcmd').first()
await expect(runCommandContainer).toBeVisible({ timeout: 20000 })

// Copy button should be in the DOM and accessible to screen readers
const copyButton = runCommandContainer.locator('button')
await expect(copyButton).toBeAttached()

// Focus the button to verify it's keyboard accessible
await copyButton.focus()
await expect(copyButton).toBeFocused()
})

test('clicking copy button copies run command and shows confirmation', async ({
page,
goto,
context,
}) => {
// Grant clipboard permissions
await context.grantPermissions(['clipboard-read', 'clipboard-write'])

await goto('/package/vite', { waitUntil: 'hydration' })
await expect(page.locator('h1')).toContainText('vite', { timeout: 15000 })

await expect(page.locator('main header').locator('text=/v\\d+\\.\\d+/')).toBeVisible({
timeout: 15000,
})

const runCommandContainer = page.locator('.group\\/runcmd').first()
await expect(runCommandContainer).toBeVisible({ timeout: 20000 })

const copyButton = runCommandContainer.locator('button')
await copyButton.click()

// Button text should change to "copied!"
await expect(copyButton).toContainText(/copied/i)

// Verify clipboard content contains the run command
const clipboardContent = await page.evaluate(() => navigator.clipboard.readText())
expect(clipboardContent).toMatch(/npx vite/i)

await expect(copyButton).toContainText(/copy/i, { timeout: 5000 })
await expect(copyButton).not.toContainText(/copied/i)
})
})
})
Loading