Skip to content

Commit efe6db6

Browse files
committed
feat(loops): june-style landing with bottom composer and template polish
1 parent 747bca6 commit efe6db6

5 files changed

Lines changed: 173 additions & 110 deletions

File tree

packages/ui/src/features/loops/components/LoopFormPrimitives.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export function SectionCard({
5555
gap="4"
5656
className="rounded-(--radius-3) border border-border bg-(--color-panel-solid) p-4"
5757
>
58-
<Flex align="center" gap="2.5">
58+
<Flex align="center" className="gap-2.5">
5959
<Flex
6060
align="center"
6161
justify="center"

packages/ui/src/features/loops/components/LoopRow.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export function LoopRow({ loop }: { loop: LoopSchemas.Loop }) {
2525
>
2626
<Flex align="center" gap="3" className="min-w-0">
2727
<RepeatIcon size={20} className="shrink-0 text-gray-11" />
28-
<Flex direction="column" gap="0.5" className="min-w-0">
28+
<Flex direction="column" className="min-w-0 gap-0.5">
2929
<Flex align="center" gap="2" className="min-w-0">
3030
<Text className="truncate font-medium text-[13px] text-gray-12">
3131
{loop.name}

packages/ui/src/features/loops/components/LoopRunRow.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export function LoopRunRow({ run }: { run: LoopSchemas.LoopRun }) {
3434
gap="3"
3535
className="rounded-(--radius-2) border border-border bg-(--color-panel-solid) px-3 py-2.5"
3636
>
37-
<Flex direction="column" gap="0.5" className="min-w-0">
37+
<Flex direction="column" className="min-w-0 gap-0.5">
3838
<Flex align="center" gap="2">
3939
<Badge color={statusColor(run.status)}>{run.status}</Badge>
4040
<Text className="text-[12px] text-gray-11">

packages/ui/src/features/loops/components/LoopsListView.tsx

Lines changed: 162 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import {
2-
ArrowRightIcon,
2+
ArrowUpIcon,
33
ClockIcon,
4+
LightningIcon,
5+
PlugsIcon,
46
PlusIcon,
57
RepeatIcon,
68
} from "@phosphor-icons/react";
79
import { useSetHeaderContent } from "@posthog/ui/hooks/useSetHeaderContent";
810
import { Button } from "@posthog/ui/primitives/Button";
911
import { 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";
1113
import { useMemo, useState } from "react";
1214
import { useLoops } from "../hooks/useLoops";
1315
import { 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+
190199
function 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

Comments
 (0)