11import {
2- ArrowRightIcon ,
2+ ArrowUpIcon ,
33 ClockIcon ,
4+ LightningIcon ,
5+ PlugsIcon ,
46 PlusIcon ,
57 RepeatIcon ,
68} from "@phosphor-icons/react" ;
79import { useSetHeaderContent } from "@posthog/ui/hooks/useSetHeaderContent" ;
810import { Button } from "@posthog/ui/primitives/Button" ;
911import { navigateToNewLoop } from "@posthog/ui/router/navigationBridge" ;
10- import { Box , Flex , Text , TextArea } from "@radix-ui/themes" ;
12+ import { Flex , Heading , IconButton , Text } from "@radix-ui/themes" ;
1113import { useMemo , useState } from "react" ;
1214import { useLoops } from "../hooks/useLoops" ;
1315import { useLoopDraftStore } from "../loopDraftStore" ;
@@ -64,97 +66,58 @@ export function LoopsListView() {
6466 < div className = "min-h-0 flex-1 overflow-auto" >
6567 < Flex
6668 direction = "column"
67- gap = "8 "
69+ gap = "6 "
6870 className = "mx-auto w-full max-w-5xl px-8 py-8"
6971 >
70- < Flex align = "start " justify = "between" gap = "3" >
72+ < Flex align = "center " justify = "between" gap = "3" >
7173 < Flex direction = "column" gap = "1" className = "min-w-0" >
72- < Text className = "font-semibold text-[18px] text-gray-12" >
73- Loops
74- </ Text >
75- < Text className = "text-[12.5px] text-gray-10 leading-snug" >
74+ < Heading className = "font-bold text-2xl" > Loops</ Heading >
75+ < Text color = "gray" className = "text-sm" >
7676 Automations that run your instructions unattended, on a
7777 schedule, a GitHub event, or an API call.
7878 </ Text >
7979 </ Flex >
80- < Button
81- variant = "soft"
82- color = "gray"
83- size = "2"
84- className = "shrink-0 gap-1.5"
85- onClick = { startBlank }
86- >
87- < PlusIcon size = { 13 } />
80+ < Button variant = "solid" size = "2" onClick = { startBlank } >
81+ < PlusIcon size = { 14 } />
8882 New loop
8983 </ Button >
9084 </ Flex >
9185
92- < Flex direction = "column" gap = "2" >
93- < TextArea
94- value = { prompt }
95- variant = "soft"
96- color = "gray"
97- size = "3"
98- placeholder = "What do you want automated?"
99- className = "min-h-[64px] text-[13px] leading-relaxed"
100- onChange = { ( e ) => setPrompt ( e . target . value ) }
101- onKeyDown = { ( e ) => {
102- if ( ( e . metaKey || e . ctrlKey ) && e . key === "Enter" ) {
103- startFromPrompt ( ) ;
104- }
105- } }
106- />
107- < Flex align = "center" justify = "between" gap = "3" >
108- < Flex gap = "2" wrap = "wrap" className = "min-w-0 flex-1" >
109- { EXAMPLE_PROMPTS . map ( ( example ) => (
110- < button
111- key = { example }
112- type = "button"
113- onClick = { ( ) => setPrompt ( example ) }
114- className = "rounded-(--radius-2) bg-(--gray-a3) px-3 py-1.5 text-[12px] text-gray-11 leading-none transition-colors hover:bg-(--gray-a4) hover:text-gray-12"
115- >
116- { example }
117- </ button >
118- ) ) }
119- </ Flex >
120- < Button
121- variant = "solid"
122- size = "2"
123- className = "shrink-0 gap-1.5"
124- disabled = { ! prompt . trim ( ) }
125- onClick = { startFromPrompt }
126- >
127- Draft loop
128- < ArrowRightIcon size = { 13 } />
129- </ Button >
130- </ Flex >
131- </ Flex >
132-
133- < Section title = "Your loops" >
134- { isLoading ? (
135- < LoopsSkeleton />
136- ) : isError ? (
137- < Notice >
138- { error instanceof Error
86+ { isLoading ? (
87+ < LoopsSkeleton />
88+ ) : isError ? (
89+ < EmptyNotice
90+ title = "Couldn't load loops."
91+ hint = {
92+ error instanceof Error
13993 ? error . message
140- : "The loops API returned an error." }
141- </ Notice >
142- ) : allLoops . length === 0 ? (
143- < Notice >
144- No loops yet. Describe one above, or start from a template
145- below.
146- </ Notice >
147- ) : (
94+ : "The loops API returned an error."
95+ }
96+ />
97+ ) : allLoops . length > 0 ? (
98+ < Flex direction = "column" gap = "3" >
99+ < Text className = "font-medium text-[12px] text-gray-10 uppercase tracking-wide" >
100+ Your loops
101+ </ Text >
148102 < Flex direction = "column" gap = "2" >
149103 { allLoops . map ( ( loop ) => (
150104 < LoopRow key = { loop . id } loop = { loop } />
151105 ) ) }
152106 </ Flex >
153- ) }
154- </ Section >
107+ </ Flex >
108+ ) : (
109+ < EmptyNotice
110+ icon = { < RepeatIcon size = { 15 } /> }
111+ title = "No loops yet"
112+ hint = "Describe what you want automated below, or start from a template."
113+ />
114+ ) }
155115
156- < Section title = "Start from a template" >
157- < div className = "grid grid-cols-1 gap-2 sm:grid-cols-2" >
116+ < Flex direction = "column" gap = "3" >
117+ < Text className = "font-medium text-[12px] text-gray-10 uppercase tracking-wide" >
118+ Start from a template
119+ </ Text >
120+ < div className = "grid grid-cols-1 gap-3 sm:grid-cols-2" >
158121 { LOOP_TEMPLATES . map ( ( template ) => (
159122 < TemplateCard
160123 key = { template . id }
@@ -163,30 +126,76 @@ export function LoopsListView() {
163126 />
164127 ) ) }
165128 </ div >
166- </ Section >
129+ </ Flex >
167130 </ Flex >
168131 </ div >
169- </ Flex >
170- ) ;
171- }
172132
173- function Section ( {
174- title,
175- children,
176- } : {
177- title : string ;
178- children : React . ReactNode ;
179- } ) {
180- return (
181- < Flex direction = "column" gap = "3" >
182- < Text className = "font-medium text-[12px] text-gray-10 uppercase tracking-wide" >
183- { title }
184- </ Text >
185- { children }
133+ < div className = "shrink-0" >
134+ < Flex
135+ direction = "column"
136+ gap = "2"
137+ className = "mx-auto w-full max-w-5xl px-8 pb-6"
138+ >
139+ < Flex gap = "2" wrap = "wrap" >
140+ { EXAMPLE_PROMPTS . map ( ( example ) => (
141+ < button
142+ key = { example }
143+ type = "button"
144+ onClick = { ( ) => setPrompt ( example ) }
145+ className = "rounded-full border border-gray-5 bg-gray-2 px-3 py-1 text-gray-11 text-xs transition-colors hover:border-gray-7 hover:bg-gray-3"
146+ >
147+ { example }
148+ </ button >
149+ ) ) }
150+ </ Flex >
151+ < Flex
152+ direction = "column"
153+ gap = "2"
154+ className = "rounded-(--radius-4) border border-border bg-(--color-panel-solid) p-3 transition-colors focus-within:border-(--gray-8)"
155+ >
156+ < textarea
157+ value = { prompt }
158+ rows = { 2 }
159+ placeholder = "What do you want automated?"
160+ className = "w-full resize-none bg-transparent text-[13px] text-gray-12 leading-relaxed outline-none placeholder:text-gray-9"
161+ onChange = { ( e ) => setPrompt ( e . target . value ) }
162+ onKeyDown = { ( e ) => {
163+ if ( e . key === "Enter" && ! e . shiftKey ) {
164+ e . preventDefault ( ) ;
165+ startFromPrompt ( ) ;
166+ }
167+ } }
168+ />
169+ < Flex align = "center" justify = "between" gap = "3" >
170+ < Text className = "text-[11px] text-gray-9" >
171+ Drafts a loop you can review before it runs
172+ </ Text >
173+ < IconButton
174+ variant = "solid"
175+ size = "1"
176+ aria-label = "Draft loop"
177+ disabled = { ! prompt . trim ( ) }
178+ onClick = { startFromPrompt }
179+ >
180+ < ArrowUpIcon size = { 13 } weight = "bold" />
181+ </ IconButton >
182+ </ Flex >
183+ </ Flex >
184+ </ Flex >
185+ </ div >
186186 </ Flex >
187187 ) ;
188188}
189189
190+ const TONE_CLASSES : Record < LoopTemplate [ "tone" ] , string > = {
191+ blue : "bg-(--blue-a3) text-(--blue-11)" ,
192+ red : "bg-(--red-a3) text-(--red-11)" ,
193+ purple : "bg-(--purple-a3) text-(--purple-11)" ,
194+ teal : "bg-(--teal-a3) text-(--teal-11)" ,
195+ amber : "bg-(--amber-a3) text-(--amber-11)" ,
196+ green : "bg-(--green-a3) text-(--green-11)" ,
197+ } ;
198+
190199function TemplateCard ( {
191200 template,
192201 onSelect,
@@ -195,39 +204,85 @@ function TemplateCard({
195204 onSelect : ( ) => void ;
196205} ) {
197206 const Icon = template . icon ;
207+ const TriggerIcon = template . triggerLabel . startsWith ( "Triggered" )
208+ ? LightningIcon
209+ : ClockIcon ;
198210 return (
199211 < button
200212 type = "button"
201213 onClick = { onSelect }
202214 className = "flex flex-col gap-2 rounded-(--radius-3) border border-border bg-(--color-panel-solid) p-4 text-left transition-colors hover:border-(--gray-6) hover:bg-(--gray-2)"
203215 >
204- < Flex align = "center" gap = "2" >
205- < Icon size = { 16 } className = "shrink-0 text-gray-11" />
206- < Text className = "font-medium text-[13px] text-gray-12" >
216+ < Flex align = "center" className = "gap-2.5" >
217+ < Flex
218+ align = "center"
219+ justify = "center"
220+ className = { `size-7 shrink-0 rounded-(--radius-2) ${ TONE_CLASSES [ template . tone ] } ` }
221+ >
222+ < Icon size = { 15 } />
223+ </ Flex >
224+ < Text className = "font-medium text-[14px] text-gray-12" >
207225 { template . name }
208226 </ Text >
209227 </ Flex >
210- < Text className = "text-[12px ] text-gray-10 leading-snug" >
228+ < Text className = "text-[12.5px ] text-gray-11 leading-snug" >
211229 { template . description }
212230 </ Text >
213- < Flex direction = "column" gap = "0.5" className = "text-gray-10" >
214- < Flex align = "center" gap = "1" >
215- < ClockIcon size = { 11 } className = "shrink-0" />
216- < Text className = "text-[11px]" > { template . triggerLabel } </ Text >
231+ < Flex
232+ align = "center"
233+ justify = "between"
234+ gap = "3"
235+ className = "mt-auto w-full text-gray-10"
236+ >
237+ < Flex align = "center" className = "min-w-0 gap-1.5" >
238+ < TriggerIcon size = { 12 } className = "shrink-0" />
239+ < Text className = "truncate text-[11.5px]" >
240+ { template . triggerLabel }
241+ </ Text >
242+ </ Flex >
243+ < Flex align = "center" className = "shrink-0 gap-1.5" >
244+ < PlugsIcon size = { 12 } className = "shrink-0" />
245+ < Text className = "text-[11.5px]" >
246+ Works with { template . worksWith . join ( " · " ) }
247+ </ Text >
217248 </ Flex >
218- < Text className = "text-[11px]" >
219- Works with { template . worksWith . join ( " · " ) }
220- </ Text >
221249 </ Flex >
222250 </ button >
223251 ) ;
224252}
225253
226- function Notice ( { children } : { children : React . ReactNode } ) {
254+ function EmptyNotice ( {
255+ icon,
256+ title,
257+ hint,
258+ } : {
259+ icon ?: React . ReactNode ;
260+ title : string ;
261+ hint : string ;
262+ } ) {
227263 return (
228- < Box className = "rounded-(--radius-3) border border-(--gray-5) border-dashed px-4 py-6 text-center text-[12.5px] text-gray-10" >
229- { children }
230- </ Box >
264+ < Flex
265+ align = "center"
266+ justify = "center"
267+ direction = "column"
268+ gap = "1"
269+ py = "6"
270+ className = "rounded border border-gray-6 border-dashed"
271+ >
272+ { icon ? (
273+ < Flex
274+ align = "center"
275+ justify = "center"
276+ className = "mb-1 size-8 rounded-(--radius-2) bg-(--gray-3) text-gray-11"
277+ >
278+ { icon }
279+ </ Flex >
280+ ) : null }
281+ < Text className = "font-medium text-sm" > { title } </ Text >
282+ < Text color = "gray" className = "text-[13px]" >
283+ { hint }
284+ </ Text >
285+ </ Flex >
231286 ) ;
232287}
233288
0 commit comments