11'use client'
22
3- import { useCallback , useMemo , useState } from 'react'
4- import { Check , Clipboard , Info } from 'lucide-react'
3+ import { type ReactNode , useCallback , useMemo , useState } from 'react'
4+ import { Check , ChevronRight , Clipboard , Info } from 'lucide-react'
55import { useShallow } from 'zustand/react/shallow'
66import {
77 Button ,
@@ -38,7 +38,6 @@ const GROUP_ORDER: readonly SlackCapabilityGroup[] = ['trigger', 'action'] as co
3838
3939const STEP_TITLES = [
4040 'Configure your bot' ,
41- 'Copy your manifest' ,
4241 'Create the app in Slack' ,
4342 'Paste your Signing Secret' ,
4443 'Install and paste your Bot Token' ,
@@ -47,6 +46,8 @@ const STEP_TITLES = [
4746
4847const STEP_COUNT = STEP_TITLES . length
4948
49+ const MODAL_HEIGHT_CLASS = 'h-[580px]'
50+
5051interface SlackSetupWizardProps {
5152 blockId : string
5253 isPreview ?: boolean
@@ -58,31 +59,35 @@ interface SlackSetupWizardProps {
5859 *
5960 * @remarks
6061 * The panel renders a single launcher button. The wizard lives in a modal
61- * that walks the user through: configuring the bot, copying the manifest,
62- * creating the app in Slack, pasting the Signing Secret, and pasting the
63- * Bot Token. Credentials are written directly into the sibling
64- * `signingSecret` and `botToken` sub-blocks via the shared sub-block store,
65- * so those fields in the panel are populated by the time the user clicks
66- * Done.
62+ * with a fixed-height body so navigating between steps doesn't resize the
63+ * dialog. Credentials are written directly into the sibling `signingSecret`
64+ * and `botToken` sub-blocks via the shared sub-block store, so those fields
65+ * in the panel are populated by the time the user clicks Done.
6766 */
6867export function SlackSetupWizard ( {
6968 blockId,
7069 isPreview = false ,
7170 disabled = false ,
7271} : SlackSetupWizardProps ) {
7372 const [ open , setOpen ] = useState < boolean > ( false )
73+ const launcherDisabled = isPreview || disabled
7474
7575 return (
7676 < >
77- < Button
77+ < button
7878 type = 'button'
79- variant = 'primary'
8079 onClick = { ( ) => setOpen ( true ) }
81- disabled = { isPreview || disabled }
82- className = 'w-full'
80+ disabled = { launcherDisabled }
81+ className = { cn (
82+ 'flex w-full items-center justify-between rounded-md border border-[var(--border-muted)] bg-[var(--surface-1)] px-3 py-2 text-left transition-colors' ,
83+ launcherDisabled
84+ ? 'cursor-not-allowed opacity-70'
85+ : 'cursor-pointer hover-hover:bg-[var(--surface-hover)]'
86+ ) }
8387 >
84- Set up Slack app
85- </ Button >
88+ < span className = 'font-medium text-[var(--text-primary)] text-sm' > Setup Slack App</ span >
89+ < ChevronRight className = 'h-4 w-4 text-[var(--text-muted)]' />
90+ </ button >
8691
8792 < WizardModal
8893 blockId = { blockId }
@@ -153,7 +158,7 @@ function WizardModal({ blockId, open, onOpenChange, isPreview, disabled }: Wizar
153158
154159 return (
155160 < Modal open = { open } onOpenChange = { handleOpenChange } >
156- < ModalContent size = 'lg' >
161+ < ModalContent size = 'lg' className = { MODAL_HEIGHT_CLASS } >
157162 < ModalHeader >
158163 < div className = 'flex items-baseline justify-between gap-3' >
159164 < span > { STEP_TITLES [ step ] } </ span >
@@ -163,9 +168,9 @@ function WizardModal({ blockId, open, onOpenChange, isPreview, disabled }: Wizar
163168 </ div >
164169 </ ModalHeader >
165170
166- < StepProgress current = { step } total = { STEP_COUNT } />
171+ < StepProgress current = { step } />
167172
168- < ModalBody className = 'min-h-[280px]' >
173+ < ModalBody >
169174 { step === 0 && (
170175 < StepConfigure
171176 blockId = { blockId }
@@ -177,9 +182,8 @@ function WizardModal({ blockId, open, onOpenChange, isPreview, disabled }: Wizar
177182 disabled = { controlsDisabled }
178183 />
179184 ) }
180- { step === 1 && < StepCopy manifestJson = { manifestJson } canCopy = { canCopy } /> }
181- { step === 2 && < StepCreate /> }
182- { step === 3 && (
185+ { step === 1 && < StepCreate manifestJson = { manifestJson } canCopy = { canCopy } /> }
186+ { step === 2 && (
183187 < StepSecret
184188 blockId = { blockId }
185189 value = { signingSecret ?? '' }
@@ -189,7 +193,7 @@ function WizardModal({ blockId, open, onOpenChange, isPreview, disabled }: Wizar
189193 disabled = { controlsDisabled }
190194 />
191195 ) }
192- { step === 4 && (
196+ { step === 3 && (
193197 < StepToken
194198 blockId = { blockId }
195199 value = { botToken ?? '' }
@@ -199,7 +203,7 @@ function WizardModal({ blockId, open, onOpenChange, isPreview, disabled }: Wizar
199203 disabled = { controlsDisabled }
200204 />
201205 ) }
202- { step === 5 && (
206+ { step === 4 && (
203207 < StepDone hasSigningSecret = { Boolean ( signingSecret ) } hasBotToken = { Boolean ( botToken ) } />
204208 ) }
205209 </ ModalBody >
@@ -225,10 +229,9 @@ function WizardModal({ blockId, open, onOpenChange, isPreview, disabled }: Wizar
225229
226230interface StepProgressProps {
227231 current : number
228- total : number
229232}
230233
231- function StepProgress ( { current, total : _total } : StepProgressProps ) {
234+ function StepProgress ( { current } : StepProgressProps ) {
232235 return (
233236 < div className = 'flex gap-1.5 px-6 pb-4' >
234237 { STEP_TITLES . map ( ( title , i ) => (
@@ -244,6 +247,30 @@ function StepProgress({ current, total: _total }: StepProgressProps) {
244247 )
245248}
246249
250+ interface SubStepListProps {
251+ children : ReactNode
252+ }
253+
254+ function SubStepList ( { children } : SubStepListProps ) {
255+ return < ol className = 'space-y-2.5' > { children } </ ol >
256+ }
257+
258+ interface SubStepProps {
259+ n : number
260+ children : ReactNode
261+ }
262+
263+ function SubStep ( { n, children } : SubStepProps ) {
264+ return (
265+ < li className = 'flex gap-2.5' >
266+ < span className = 'mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-[var(--surface-5)] font-medium text-[var(--text-secondary)] text-xs tabular-nums' >
267+ { n }
268+ </ span >
269+ < div className = 'flex-1 text-[var(--text-secondary)] text-sm leading-relaxed' > { children } </ div >
270+ </ li >
271+ )
272+ }
273+
247274interface StepConfigureProps {
248275 blockId : string
249276 appName : string
@@ -281,7 +308,7 @@ function StepConfigure({
281308 className = 'h-9 text-sm'
282309 />
283310 </ div >
284- < div className = 'space-y-3 ' >
311+ < div className = 'grid grid-cols-2 gap-x-4 gap-y-4 ' >
285312 { GROUP_ORDER . map ( ( group ) => {
286313 const items = SLACK_CAPABILITIES . filter ( ( c ) => c . group === group )
287314 if ( items . length === 0 ) return null
@@ -301,12 +328,12 @@ function StepConfigure({
301328 )
302329}
303330
304- interface StepCopyProps {
331+ interface StepCreateProps {
305332 manifestJson : string
306333 canCopy : boolean
307334}
308335
309- function StepCopy ( { manifestJson, canCopy } : StepCopyProps ) {
336+ function StepCreate ( { manifestJson, canCopy } : StepCreateProps ) {
310337 const [ copied , setCopied ] = useState < boolean > ( false )
311338
312339 const handleCopy = useCallback ( ( ) => {
@@ -317,52 +344,52 @@ function StepCopy({ manifestJson, canCopy }: StepCopyProps) {
317344 } , [ canCopy , manifestJson ] )
318345
319346 return (
320- < div className = 'space-y-3 ' >
321- < p className = 'text-[var(--text-secondary)] text-sm leading-relaxed' >
322- Copy the generated manifest. You'll paste it into Slack in the next step.
323- </ p >
324- < button
325- type = 'button'
326- onClick = { handleCopy }
327- disabled = { ! canCopy }
328- className = { cn (
329- 'flex w-full items-center justify-between rounded-md border border-[var(--border-muted)] bg-[var(--surface-1)] px-3 py-2 text-left transition-colors',
330- canCopy
331- ? 'cursor-pointer hover-hover:bg-[var(--surface-hover)]'
332- : 'cursor-not-allowed opacity-70'
333- ) }
334- >
335- < span className = 'font-medium text-[var(--text-secondary)] text-sm' >
336- { canCopy ? 'Click to copy manifest' : 'Deploy once to lock in the webhook URL' }
337- </ span >
338- { canCopy &&
339- ( copied ? (
340- < Check className = 'h-3 w-3 text-[var(--text-success)]' />
341- ) : (
342- < Clipboard className = 'h-3 w-3 text-[var(--text-muted)]' />
343- ) ) }
344- </ button >
345- </ div >
346- )
347- }
348-
349- function StepCreate ( ) {
350- return (
351- < div className = 'space-y-3 text-[var(--text-secondary)] text-sm leading-relaxed' >
352- < p >
353- Open the { ' ' }
354- < a
355- href = 'https://api.slack.com/apps'
356- target = '_blank'
357- rel = 'noopener noreferrer'
358- className = 'text-[var(--brand-secondary)] underline underline-offset-2'
359- >
360- Slack Apps page
361- </ a >
362- , click < strong > Create New App </ strong > → < strong > From a manifest </ strong > , pick your
363- workspace, paste the manifest, and click < strong > Create</ strong > .
364- </ p >
365- < p > Leave that Slack tab open — you'll pull a couple of values out of it in the next steps. </ p >
347+ < div className = 'space-y-4 ' >
348+ < SubStepList >
349+ < SubStep n = { 1 } >
350+ < div > Copy your manifest: </ div >
351+ < button
352+ type = 'button'
353+ onClick = { handleCopy }
354+ disabled = { ! canCopy }
355+ className = { cn (
356+ 'mt-2 inline-flex items-center gap-2 rounded-md border border-[var(--border-muted)] bg-[var(--surface-1)] px-3 py-1.5 text-left transition-colors',
357+ canCopy
358+ ? 'cursor-pointer hover-hover:bg-[var(--surface-hover)]'
359+ : 'cursor-not-allowed opacity-70'
360+ ) }
361+ >
362+ < span className = 'font-medium text-[var(--text-secondary)] text-sm' >
363+ { canCopy ? 'Click to copy manifest' : 'Deploy once to lock in the webhook URL' }
364+ </ span >
365+ { canCopy &&
366+ ( copied ? (
367+ < Check className = 'h-3 w-3 text-[var(--text-success)]' />
368+ ) : (
369+ < Clipboard className = 'h-3 w-3 text-[var(--text-muted)]' />
370+ ) ) }
371+ </ button >
372+ </ SubStep >
373+ < SubStep n = { 2 } >
374+ Open the { ' ' }
375+ < a
376+ href = 'https://api.slack.com/apps'
377+ target = '_blank'
378+ rel = 'noopener noreferrer'
379+ className = 'text-[var(--brand-secondary)] underline underline-offset-2'
380+ >
381+ Slack Apps page
382+ </ a >
383+ .
384+ </ SubStep >
385+ < SubStep n = { 3 } >
386+ Click < strong > Create New App </ strong > → < strong > From a manifest </ strong > and pick your
387+ workspace.
388+ </ SubStep >
389+ < SubStep n = { 4 } >
390+ Paste your manifest, then click < strong > Next </ strong > → < strong > Create</ strong > .
391+ </ SubStep >
392+ </ SubStepList >
366393 </ div >
367394 )
368395}
@@ -376,11 +403,16 @@ interface StepSecretProps {
376403
377404function StepSecret ( { blockId, value, onChange, disabled } : StepSecretProps ) {
378405 return (
379- < div className = 'space-y-3' >
380- < p className = 'text-[var(--text-secondary)] text-sm leading-relaxed' >
381- In your new Slack app, open < strong > Basic Information</ strong > , find the{ ' ' }
382- < strong > Signing Secret</ strong > , and paste it here.
383- </ p >
406+ < div className = 'space-y-4' >
407+ < SubStepList >
408+ < SubStep n = { 1 } >
409+ In your new Slack app, open < strong > Basic Information</ strong > .
410+ </ SubStep >
411+ < SubStep n = { 2 } >
412+ Find < strong > Signing Secret</ strong > and click < strong > Show</ strong > , then copy it.
413+ </ SubStep >
414+ < SubStep n = { 3 } > Paste it into the field below.</ SubStep >
415+ </ SubStepList >
384416 < div className = 'space-y-1.5' >
385417 < Label
386418 htmlFor = { `${ blockId } -wizard-signing-secret` }
@@ -411,12 +443,17 @@ interface StepTokenProps {
411443
412444function StepToken ( { blockId, value, onChange, disabled } : StepTokenProps ) {
413445 return (
414- < div className = 'space-y-3' >
415- < p className = 'text-[var(--text-secondary)] text-sm leading-relaxed' >
416- In Slack, open < strong > Install App</ strong > → < strong > Install to Workspace</ strong > and
417- authorize. Then copy the < strong > Bot User OAuth Token</ strong > (starts with{ ' ' }
418- < code > xoxb-</ code > ) and paste it here.
419- </ p >
446+ < div className = 'space-y-4' >
447+ < SubStepList >
448+ < SubStep n = { 1 } >
449+ In Slack, open < strong > Install App</ strong > → < strong > Install to Workspace</ strong > and
450+ authorize.
451+ </ SubStep >
452+ < SubStep n = { 2 } >
453+ Copy the < strong > Bot User OAuth Token</ strong > (starts with < code > xoxb-</ code > ).
454+ </ SubStep >
455+ < SubStep n = { 3 } > Paste it into the field below.</ SubStep >
456+ </ SubStepList >
420457 < div className = 'space-y-1.5' >
421458 < Label
422459 htmlFor = { `${ blockId } -wizard-bot-token` }
@@ -445,16 +482,20 @@ interface StepDoneProps {
445482
446483function StepDone ( { hasSigningSecret, hasBotToken } : StepDoneProps ) {
447484 return (
448- < div className = 'space-y-3 text-[var(--text-secondary)] text-sm leading-relaxed ' >
449- < p >
450- Your Slack app is set up. The Signing Secret and Bot Token have been saved to the trigger —
451- you can edit them anytime from the panel .
485+ < div className = 'space-y-4 ' >
486+ < p className = 'text-[var(--text-secondary)] text-sm leading-relaxed' >
487+ Your Slack app is set up. Save the workflow and Slack will verify the webhook URL
488+ automatically .
452489 </ p >
453- < ul className = 'space-y-1' >
454- < StatusRow label = 'Signing Secret' ok = { hasSigningSecret } />
455- < StatusRow label = 'Bot Token' ok = { hasBotToken } />
456- </ ul >
457- < p > Save the workflow and Slack will verify the webhook URL automatically.</ p >
490+ < SubStepList >
491+ < SubStep n = { 1 } >
492+ < StatusRow label = 'Signing Secret' ok = { hasSigningSecret } />
493+ </ SubStep >
494+ < SubStep n = { 2 } >
495+ < StatusRow label = 'Bot Token' ok = { hasBotToken } />
496+ </ SubStep >
497+ < SubStep n = { 3 } > Click Done and save this workflow.</ SubStep >
498+ </ SubStepList >
458499 </ div >
459500 )
460501}
@@ -466,7 +507,7 @@ interface StatusRowProps {
466507
467508function StatusRow ( { label, ok } : StatusRowProps ) {
468509 return (
469- < li className = 'flex items-center gap-2' >
510+ < span className = 'flex items-center gap-2' >
470511 < Check
471512 className = { cn (
472513 'h-[14px] w-[14px]' ,
@@ -475,9 +516,9 @@ function StatusRow({ label, ok }: StatusRowProps) {
475516 />
476517 < span >
477518 { label }
478- { ! ok && < span className = 'ml-1 text-[var(--text-muted)]' > — missing </ span > }
519+ { ! ok && < span className = 'ml-1 text-[var(--text-muted)]' > — not saved yet </ span > }
479520 </ span >
480- </ li >
521+ </ span >
481522 )
482523}
483524
0 commit comments