Skip to content

Commit 0e8a420

Browse files
rolznzreneaaron
andauthored
feat: improve first channel screen UI (#1925)
* feat: improve first channel screen UI * fix: error message when no payment method is chosen * chore: make channel copy more clear * fix: toast text colors * fix: decrease margin * fix: improve qr code overlay * fix: decrease qr code rounding * fix: preset amounts * fix: preset amounts * fix: use field * fix: remove radio group * fix: add cursor-pointer on field --------- Co-authored-by: René Aaron <rene@twentyuno.net>
1 parent d01ced8 commit 0e8a420

File tree

18 files changed

+1028
-200
lines changed

18 files changed

+1028
-200
lines changed

alby/models.go

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -97,24 +97,25 @@ type AlbyMe struct {
9797
}
9898

9999
type ChannelPeerSuggestion struct {
100-
Network string `json:"network"`
101-
PaymentMethod string `json:"paymentMethod"`
102-
Pubkey string `json:"pubkey"`
103-
Host string `json:"host"`
104-
MinimumChannelSize uint64 `json:"minimumChannelSize"`
105-
MaximumChannelSize uint64 `json:"maximumChannelSize"`
106-
Name string `json:"name"`
107-
Image string `json:"image"`
108-
Identifier string `json:"identifier"`
109-
ContactUrl string `json:"contactUrl"`
110-
Type string `json:"type"`
111-
Terms string `json:"terms"`
112-
Description string `json:"description"`
113-
Note string `json:"note"`
114-
PublicChannelsAllowed bool `json:"publicChannelsAllowed"`
115-
FeeTotalSat1m *uint32 `json:"feeTotalSat1m"`
116-
FeeTotalSat2m *uint32 `json:"feeTotalSat2m"`
117-
FeeTotalSat3m *uint32 `json:"feeTotalSat3m"`
100+
Network string `json:"network"`
101+
PaymentMethod string `json:"paymentMethod"`
102+
Pubkey string `json:"pubkey"`
103+
Host string `json:"host"`
104+
MinimumChannelSize uint64 `json:"minimumChannelSize"`
105+
MaximumChannelSize uint64 `json:"maximumChannelSize"`
106+
MaximumChannelExpiryBlocks *uint32 `json:"maximumChannelExpiryBlocks"`
107+
Name string `json:"name"`
108+
Image string `json:"image"`
109+
Identifier string `json:"identifier"`
110+
ContactUrl string `json:"contactUrl"`
111+
Type string `json:"type"`
112+
Terms string `json:"terms"`
113+
Description string `json:"description"`
114+
Note string `json:"note"`
115+
PublicChannelsAllowed bool `json:"publicChannelsAllowed"`
116+
FeeTotalSat1m *uint32 `json:"feeTotalSat1m"`
117+
FeeTotalSat2m *uint32 `json:"feeTotalSat2m"`
118+
FeeTotalSat3m *uint32 `json:"feeTotalSat3m"`
118119
}
119120

120121
type LSPChannelOffer struct {

frontend/public/images/illustrations/lightning-network-dark.svg

Lines changed: 299 additions & 1 deletion
Loading

frontend/public/images/illustrations/lightning-network-light.svg

Lines changed: 227 additions & 1 deletion
Loading

frontend/src/components/PayLightningInvoice.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export function PayLightningInvoice({ invoice }: PayLightningInvoiceProps) {
3535
</div>
3636
<div className="w-full relative flex items-center justify-center">
3737
<QRCode value={invoice} className="w-full" />
38-
<div className="bg-primary-foreground absolute">
38+
<div className="bg-white absolute rounded-full p-1">
3939
<LightningIcon className="w-12 h-12" />
4040
</div>
4141
</div>

frontend/src/components/QRCode.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ function QRCode({ value, size, level, className }: Props) {
3030
size={size}
3131
fgColor={fgColor}
3232
bgColor={bgColor}
33-
className={cn("rounded-md", className)}
33+
className={cn("rounded", className)}
3434
level={level}
3535
/>
3636
</div>

frontend/src/components/channels/LSPTermsDialog.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,22 @@ type LSPTermsDialogProps = {
1717
contactUrl: string;
1818
terms: string | undefined;
1919
trigger: React.ReactNode;
20+
maximumChannelExpiryBlocks?: number;
2021
};
2122
export function LSPTermsDialog({
2223
name,
2324
description,
2425
contactUrl,
2526
terms,
2627
trigger,
28+
maximumChannelExpiryBlocks = 12960 /* 3 months */,
2729
}: LSPTermsDialogProps) {
30+
const months = Math.round((10 * maximumChannelExpiryBlocks) / (60 * 24 * 30));
31+
2832
return (
2933
<AlertDialog>
3034
<AlertDialogTrigger asChild>
31-
<div className="cursor-pointer">{trigger}</div>
35+
<div className="cursor-pointer inline">{trigger}</div>
3236
</AlertDialogTrigger>
3337
<AlertDialogContent>
3438
<AlertDialogHeader>
@@ -44,7 +48,7 @@ export function LSPTermsDialog({
4448
</p>
4549

4650
<div className="flex items-center gap-2">
47-
Duration: at least 3 months
51+
Duration: at least {months} months
4852
<ExternalLink to="https://guides.getalby.com/user-guide/alby-hub/faq/how-to-open-a-payment-channel">
4953
<InfoIcon className="size-4 text-muted-foreground" />
5054
</ExternalLink>
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
import { useMemo } from "react";
2+
import { cva, type VariantProps } from "class-variance-authority";
3+
4+
import { cn } from "src/lib/utils";
5+
import { Label } from "src/components/ui/label";
6+
import { Separator } from "src/components/ui/separator";
7+
8+
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
9+
return (
10+
<fieldset
11+
data-slot="field-set"
12+
className={cn(
13+
"flex flex-col gap-6",
14+
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
15+
className
16+
)}
17+
{...props}
18+
/>
19+
);
20+
}
21+
22+
function FieldLegend({
23+
className,
24+
variant = "legend",
25+
...props
26+
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
27+
return (
28+
<legend
29+
data-slot="field-legend"
30+
data-variant={variant}
31+
className={cn(
32+
"mb-3 font-medium",
33+
"data-[variant=legend]:text-base",
34+
"data-[variant=label]:text-sm",
35+
className
36+
)}
37+
{...props}
38+
/>
39+
);
40+
}
41+
42+
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
43+
return (
44+
<div
45+
data-slot="field-group"
46+
className={cn(
47+
"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
48+
className
49+
)}
50+
{...props}
51+
/>
52+
);
53+
}
54+
55+
const fieldVariants = cva(
56+
"group/field data-[invalid=true]:text-destructive flex w-full gap-3",
57+
{
58+
variants: {
59+
orientation: {
60+
vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
61+
horizontal: [
62+
"flex-row items-center",
63+
"[&>[data-slot=field-label]]:flex-auto",
64+
"has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px has-[>[data-slot=field-content]]:items-start",
65+
],
66+
responsive: [
67+
"@md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto flex-col [&>*]:w-full [&>.sr-only]:w-auto",
68+
"@md/field-group:[&>[data-slot=field-label]]:flex-auto",
69+
"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
70+
],
71+
},
72+
},
73+
defaultVariants: {
74+
orientation: "vertical",
75+
},
76+
}
77+
);
78+
79+
function Field({
80+
className,
81+
orientation = "vertical",
82+
...props
83+
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
84+
return (
85+
<div
86+
role="group"
87+
data-slot="field"
88+
data-orientation={orientation}
89+
className={cn(fieldVariants({ orientation }), className)}
90+
{...props}
91+
/>
92+
);
93+
}
94+
95+
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
96+
return (
97+
<div
98+
data-slot="field-content"
99+
className={cn(
100+
"group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
101+
className
102+
)}
103+
{...props}
104+
/>
105+
);
106+
}
107+
108+
function FieldLabel({
109+
className,
110+
...props
111+
}: React.ComponentProps<typeof Label>) {
112+
return (
113+
<Label
114+
data-slot="field-label"
115+
className={cn(
116+
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
117+
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>[data-slot=field]]:p-4",
118+
"has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10",
119+
className
120+
)}
121+
{...props}
122+
/>
123+
);
124+
}
125+
126+
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
127+
return (
128+
<div
129+
data-slot="field-label"
130+
className={cn(
131+
"flex w-fit items-center gap-2 text-sm font-medium leading-snug group-data-[disabled=true]/field:opacity-50",
132+
className
133+
)}
134+
{...props}
135+
/>
136+
);
137+
}
138+
139+
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
140+
return (
141+
<p
142+
data-slot="field-description"
143+
className={cn(
144+
"text-muted-foreground text-sm font-normal leading-normal group-has-[[data-orientation=horizontal]]/field:text-balance",
145+
"nth-last-2:-mt-1 last:mt-0 [[data-variant=legend]+&]:-mt-1.5",
146+
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
147+
className
148+
)}
149+
{...props}
150+
/>
151+
);
152+
}
153+
154+
function FieldSeparator({
155+
children,
156+
className,
157+
...props
158+
}: React.ComponentProps<"div"> & {
159+
children?: React.ReactNode;
160+
}) {
161+
return (
162+
<div
163+
data-slot="field-separator"
164+
data-content={!!children}
165+
className={cn(
166+
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
167+
className
168+
)}
169+
{...props}
170+
>
171+
<Separator className="absolute inset-0 top-1/2" />
172+
{children && (
173+
<span
174+
className="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
175+
data-slot="field-separator-content"
176+
>
177+
{children}
178+
</span>
179+
)}
180+
</div>
181+
);
182+
}
183+
184+
function FieldError({
185+
className,
186+
children,
187+
errors,
188+
...props
189+
}: React.ComponentProps<"div"> & {
190+
errors?: Array<{ message?: string } | undefined>;
191+
}) {
192+
const content = useMemo(() => {
193+
if (children) {
194+
return children;
195+
}
196+
197+
if (!errors) {
198+
return null;
199+
}
200+
201+
if (errors?.length === 1 && errors[0]?.message) {
202+
return errors[0].message;
203+
}
204+
205+
return (
206+
<ul className="ml-4 flex list-disc flex-col gap-1">
207+
{errors.map(
208+
(error, index) =>
209+
error?.message && <li key={index}>{error.message}</li>
210+
)}
211+
</ul>
212+
);
213+
}, [children, errors]);
214+
215+
if (!content) {
216+
return null;
217+
}
218+
219+
return (
220+
<div
221+
role="alert"
222+
data-slot="field-error"
223+
className={cn("text-destructive text-sm font-normal", className)}
224+
{...props}
225+
>
226+
{content}
227+
</div>
228+
);
229+
}
230+
231+
export {
232+
Field,
233+
FieldLabel,
234+
FieldDescription,
235+
FieldError,
236+
FieldGroup,
237+
FieldLegend,
238+
FieldSeparator,
239+
FieldSet,
240+
FieldContent,
241+
FieldTitle,
242+
};

frontend/src/index.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ body {
3131

3232
@layer base {
3333
[role="menuitem"]:not([disabled]),
34-
[data-slot="command-item"] {
34+
[data-slot="command-item"],
35+
[data-slot="field"] {
3536
cursor: pointer !important;
3637
}
3738
}

frontend/src/screens/channels/CurrentChannelOrder.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,12 @@ function PayLightningChannelOrder({ order }: { order: NewChannelOrder }) {
678678
</TableBody>
679679
</Table>
680680
</div>
681+
<div className="flex justify-center w-full -mb-5">
682+
<p className="text-center text-xs text-muted-foreground max-w-sm">
683+
By proceeding, you consent the channel opens immediately and
684+
that you lose the right to revoke once it is open.
685+
</p>
686+
</div>
681687
<>
682688
{canPayInternally && (
683689
<>
@@ -729,7 +735,9 @@ function PayLightningChannelOrder({ order }: { order: NewChannelOrder }) {
729735
)}
730736

731737
{(payExternally || !canPayInternally) && (
732-
<PayLightningInvoice invoice={lspOrderResponse.invoice} />
738+
<div className="flex flex-row justify-center">
739+
<PayLightningInvoice invoice={lspOrderResponse.invoice} />
740+
</div>
733741
)}
734742

735743
<div className="flex-1 flex flex-col justify-end items-center gap-4">

0 commit comments

Comments
 (0)