diff --git a/app/components/Button/Base.vue b/app/components/Button/Base.vue
index f37d57be06..ee610f941d 100644
--- a/app/components/Button/Base.vue
+++ b/app/components/Button/Base.vue
@@ -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. */
@@ -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))':
diff --git a/app/components/Package/SkillsModal.vue b/app/components/Package/SkillsModal.vue
index d67464e5bb..a988ca77da 100644
--- a/app/components/Package/SkillsModal.vue
+++ b/app/components/Package/SkillsModal.vue
@@ -131,14 +131,14 @@ function getWarningTooltip(skill: SkillListItem): string | undefined {
npx
skills add {{ baseUrl }}/{{ packageName }}
-
+ />
@@ -205,7 +205,7 @@ function getWarningTooltip(skill: SkillListItem): string | undefined {
{{
$t(
- 'package.skills.file_counts.refs',
+ 'package.skills.file_counts.references',
{ count: skill.fileCounts.references },
skill.fileCounts.references,
)
diff --git a/app/components/Terminal/Execute.vue b/app/components/Terminal/Execute.vue
index 0484ef6466..c4f7e955a9 100644
--- a/app/components/Terminal/Execute.vue
+++ b/app/components/Terminal/Execute.vue
@@ -68,14 +68,14 @@ const copyExecuteCommand = () => copyExecute(getFullExecuteCommand())
>{{ i > 0 ? ' ' : '' }}{{ part }}
-
+ />
diff --git a/app/components/Terminal/Install.vue b/app/components/Terminal/Install.vue
index 162dc84ec5..36de783c09 100644
--- a/app/components/Terminal/Install.vue
+++ b/app/components/Terminal/Install.vue
@@ -151,14 +151,14 @@ const copyDevInstallCommand = () =>
>{{ i > 0 ? ' ' : '' }}{{ part }}
-
+ />
@@ -185,15 +185,12 @@ const copyDevInstallCommand = () =>
>
- {{
- devInstallCopied ? $t('common.copied') : $t('common.copy')
- }}
-
+ />
@@ -249,13 +246,14 @@ const copyDevInstallCommand = () =>
>{{ i > 0 ? ' ' : '' }}{{ part }}
-
+ />
@@ -294,16 +292,14 @@ const copyDevInstallCommand = () =>
>{{ i > 0 ? ' ' : '' }}{{ part }}
-
+ />
diff --git a/i18n/locales/en.json b/i18n/locales/en.json
index c187265c73..4635ec06ed 100644
--- a/i18n/locales/en.json
+++ b/i18n/locales/en.json
@@ -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",
diff --git a/i18n/schema.json b/i18n/schema.json
index 1ee624a990..660af20c95 100644
--- a/i18n/schema.json
+++ b/i18n/schema.json
@@ -966,6 +966,18 @@
},
"locally": {
"type": "string"
+ },
+ "copy_command": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "command": {
+ "type": "object",
+ "properties": {
+ "copied": {
+ "type": "string"
}
},
"additionalProperties": false
diff --git a/test/e2e/create-command.spec.ts b/test/e2e/create-command.spec.ts
index 15f00ee4ce..d541407f7d 100644
--- a/test/e2e/create-command.spec.ts
+++ b/test/e2e/create-command.spec.ts
@@ -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 })
@@ -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 ({
@@ -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()
@@ -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 ({
@@ -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()
@@ -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)
+ })
+ })
})