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) + }) + }) })