Skip to content

Commit d6c7e0e

Browse files
authored
feat(web): 2단계 설치 안내를 위한 InstallModal 추가 (#2)
사용자가 플러그인을 설치할 때 marketplace 추가와 플러그인 설치라는 두 가지 명령어를 순서대로 실행해야 합니다. 이를 명확하게 안내하기 위해 InstallModal 컴포넌트를 추가했습니다. 주요 변경사항: - InstallModal 컴포넌트 신규 추가 - 단계별 설치 안내 (1. Marketplace 추가, 2. 플러그인 설치) - 각 명령어별 개별 복사 버튼 - 전체 명령어 한 번에 복사 기능 - Clipboard API 지원 여부 감지 및 경고 - 복사 실패 시 toast 알림 표시 - 수동 복사 안내 메시지 추가 - PluginCard 컴포넌트 수정 - Install 버튼 클릭 시 modal 열기로 변경 - 기존 단일 명령어 복사 기능 제거 에러 처리 개선: - Clipboard API 사용 전 지원 여부 확인 - 복사 실패 시 사용자에게 toast로 알림 - 실패 시 성공 상태로 표시되지 않도록 수정 - 상세한 에러 로깅 (명령어, 타입, 환경 정보 포함) - 수동 복사 대체 방법 안내
1 parent 892c03b commit d6c7e0e

2 files changed

Lines changed: 229 additions & 20 deletions

File tree

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
<template>
2+
<UModal
3+
v-model:open="isOpen"
4+
title="Installation Instructions"
5+
description="Run these two commands in order"
6+
>
7+
<template #body>
8+
<div class="space-y-4">
9+
<!-- Clipboard Not Supported Warning -->
10+
<UAlert
11+
v-if="!isClipboardSupported"
12+
color="warning"
13+
variant="soft"
14+
icon="i-heroicons-exclamation-triangle"
15+
title="Clipboard Not Available"
16+
description="Your browser doesn't support automatic copying. Please copy the commands manually using Ctrl+C (Cmd+C on Mac)."
17+
/>
18+
19+
<!-- Step 1: Add Marketplace -->
20+
<div class="space-y-2">
21+
<div class="flex items-center gap-2 text-sm font-medium">
22+
<UBadge color="primary" variant="soft" size="sm">1</UBadge>
23+
<span>Add Marketplace</span>
24+
</div>
25+
<div class="relative">
26+
<pre class="bg-default border border-default rounded-lg p-3 text-sm overflow-x-auto"><code>{{ marketplaceCommand }}</code></pre>
27+
<UButton
28+
@click="copyCommand(marketplaceCommand, 'marketplace')"
29+
:variant="copiedStates.marketplace ? 'soft' : 'ghost'"
30+
:color="copiedStates.marketplace ? 'success' : 'neutral'"
31+
size="xs"
32+
:icon="copiedStates.marketplace ? 'i-heroicons-check' : 'i-heroicons-clipboard-document'"
33+
class="absolute top-2 right-2"
34+
:title="copiedStates.marketplace ? 'Copied!' : 'Copy command'"
35+
/>
36+
</div>
37+
</div>
38+
39+
<!-- Step 2: Install Plugin -->
40+
<div class="space-y-2">
41+
<div class="flex items-center gap-2 text-sm font-medium">
42+
<UBadge color="primary" variant="soft" size="sm">2</UBadge>
43+
<span>Install Plugin</span>
44+
</div>
45+
<div class="relative">
46+
<pre class="bg-default border border-default rounded-lg p-3 text-sm overflow-x-auto"><code>{{ installCommand }}</code></pre>
47+
<UButton
48+
@click="copyCommand(installCommand, 'install')"
49+
:variant="copiedStates.install ? 'soft' : 'ghost'"
50+
:color="copiedStates.install ? 'success' : 'neutral'"
51+
size="xs"
52+
:icon="copiedStates.install ? 'i-heroicons-check' : 'i-heroicons-clipboard-document'"
53+
class="absolute top-2 right-2"
54+
:title="copiedStates.install ? 'Copied!' : 'Copy command'"
55+
/>
56+
</div>
57+
</div>
58+
59+
<!-- Copy All Button -->
60+
<div class="pt-2 border-t border-default">
61+
<UButton
62+
@click="copyAllCommands"
63+
:variant="copiedStates.all ? 'soft' : 'outline'"
64+
:color="copiedStates.all ? 'success' : 'primary'"
65+
block
66+
:icon="copiedStates.all ? 'i-heroicons-check-circle' : 'i-heroicons-clipboard-document-list'"
67+
>
68+
{{ copiedStates.all ? 'All Commands Copied!' : 'Copy All Commands' }}
69+
</UButton>
70+
</div>
71+
72+
<!-- Manual Copy Tip -->
73+
<div class="text-sm text-muted flex items-start gap-2">
74+
<UIcon name="i-heroicons-information-circle" class="shrink-0 mt-0.5" />
75+
<span>Tip: You can also select and copy the commands manually (Ctrl+C / Cmd+C)</span>
76+
</div>
77+
</div>
78+
</template>
79+
80+
<template #footer="{ close }">
81+
<div class="flex justify-end">
82+
<UButton
83+
label="Close"
84+
color="neutral"
85+
variant="outline"
86+
@click="close"
87+
/>
88+
</div>
89+
</template>
90+
</UModal>
91+
</template>
92+
93+
<script setup lang="ts">
94+
interface Props {
95+
pluginName: string
96+
open?: boolean
97+
}
98+
99+
interface Emits {
100+
(e: 'update:open', value: boolean): void
101+
}
102+
103+
const props = withDefaults(defineProps<Props>(), {
104+
open: false
105+
})
106+
107+
const emit = defineEmits<Emits>()
108+
109+
const isOpen = computed({
110+
get: () => props.open,
111+
set: (value) => emit('update:open', value)
112+
})
113+
114+
const marketplaceCommand = '/plugin marketplace add pleaseai/claude-code-plugins'
115+
const installCommand = computed(() => `/plugin install ${props.pluginName}@pleaseai`)
116+
117+
const copiedStates = reactive({
118+
marketplace: false,
119+
install: false,
120+
all: false
121+
})
122+
123+
// Check if clipboard API is supported
124+
const isClipboardSupported = computed(() => {
125+
return typeof navigator !== 'undefined' &&
126+
'clipboard' in navigator &&
127+
typeof navigator.clipboard?.writeText === 'function'
128+
})
129+
130+
const copyCommand = async (command: string, type: keyof typeof copiedStates) => {
131+
try {
132+
// Check if clipboard API is available
133+
if (!isClipboardSupported.value) {
134+
throw new Error('Clipboard API not available')
135+
}
136+
137+
await navigator.clipboard.writeText(command)
138+
copiedStates[type] = true
139+
140+
setTimeout(() => {
141+
copiedStates[type] = false
142+
}, 2000)
143+
} catch (err) {
144+
// Log error with full context for debugging
145+
console.error('Failed to copy command to clipboard:', {
146+
command,
147+
type,
148+
error: err instanceof Error ? err.message : String(err),
149+
clipboardAvailable: isClipboardSupported.value,
150+
isSecureContext: typeof window !== 'undefined' ? window.isSecureContext : false
151+
})
152+
153+
// Show user-facing error notification
154+
const toast = useToast()
155+
toast.add({
156+
title: 'Copy Failed',
157+
description: 'Could not copy to clipboard. Please copy the command manually.',
158+
color: 'error',
159+
icon: 'i-heroicons-exclamation-triangle'
160+
})
161+
162+
// DO NOT set success state on failure
163+
copiedStates[type] = false
164+
}
165+
}
166+
167+
const copyAllCommands = async () => {
168+
const allCommands = `${marketplaceCommand}\n${installCommand.value}`
169+
170+
try {
171+
// Check if clipboard API is available
172+
if (!isClipboardSupported.value) {
173+
throw new Error('Clipboard API not available')
174+
}
175+
176+
await navigator.clipboard.writeText(allCommands)
177+
copiedStates.all = true
178+
179+
setTimeout(() => {
180+
copiedStates.all = false
181+
}, 2000)
182+
} catch (err) {
183+
// Log error with full context for debugging
184+
console.error('Failed to copy all commands to clipboard:', {
185+
marketplaceCommand,
186+
installCommand: installCommand.value,
187+
error: err instanceof Error ? err.message : String(err),
188+
clipboardAvailable: isClipboardSupported.value,
189+
isSecureContext: typeof window !== 'undefined' ? window.isSecureContext : false
190+
})
191+
192+
// Show user-facing error notification
193+
const toast = useToast()
194+
toast.add({
195+
title: 'Copy Failed',
196+
description: 'Could not copy commands to clipboard. Please copy them manually from above.',
197+
color: 'error',
198+
icon: 'i-heroicons-exclamation-triangle'
199+
})
200+
201+
// DO NOT set success state on failure
202+
copiedStates.all = false
203+
}
204+
}
205+
206+
// Reset copied states when modal is closed
207+
watch(isOpen, (newValue) => {
208+
if (!newValue) {
209+
copiedStates.marketplace = false
210+
copiedStates.install = false
211+
copiedStates.all = false
212+
}
213+
})
214+
</script>

apps/web/app/components/PluginCard.vue

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -74,19 +74,25 @@
7474
</UButton>
7575

7676
<UButton
77-
@click="copyInstallCommand"
78-
:variant="copied ? 'soft' : 'solid'"
79-
:color="copied ? 'green' : 'primary'"
77+
@click="openInstallModal"
78+
variant="solid"
79+
color="primary"
8080
size="sm"
81-
:icon="copied ? 'i-heroicons-check-circle' : 'i-heroicons-clipboard-document'"
81+
icon="i-heroicons-arrow-down-tray"
8282
class="flex-1 justify-center"
83-
:title="copied ? 'Copied to clipboard!' : 'Copy install command'"
83+
title="View installation instructions"
8484
>
85-
{{ copied ? 'Copied!' : 'Install' }}
85+
Install
8686
</UButton>
8787
</div>
8888
</template>
8989
</UCard>
90+
91+
<!-- Install Modal -->
92+
<InstallModal
93+
v-model:open="isModalOpen"
94+
:plugin-name="plugin.name"
95+
/>
9096
</template>
9197

9298
<script setup lang="ts">
@@ -115,7 +121,7 @@ const props = defineProps<{
115121
plugin: Plugin
116122
}>()
117123
118-
const copied = ref(false)
124+
const isModalOpen = ref(false)
119125
const pluginMetadata = ref<PluginMetadata | null>(null)
120126
const loading = ref(false)
121127
@@ -166,18 +172,7 @@ onMounted(() => {
166172
fetchPluginMetadata()
167173
})
168174
169-
const copyInstallCommand = async () => {
170-
const command = `claude-code plugins install https://github.com/${props.plugin.source.repo}`
171-
172-
try {
173-
await navigator.clipboard.writeText(command)
174-
copied.value = true
175-
176-
setTimeout(() => {
177-
copied.value = false
178-
}, 2000)
179-
} catch (err) {
180-
console.error('Failed to copy:', err)
181-
}
175+
const openInstallModal = () => {
176+
isModalOpen.value = true
182177
}
183178
</script>

0 commit comments

Comments
 (0)