on master help sign in
Extract UI component constants and create reusable text field components with button variants
commitb10cab736c3266299676f436aae815327e1e1364 authoriko <mail@iko.soy> dateWed 15 Apr 2026, 01:27 (6 months ago) parent095199e
5 files changed+85−39
client/App.fs+37−26 client/Buttons.fs+24−13 client/Constants.fs+3 client/Skrepka.fsproj+1 client/TextFields.fs+20
client/App.fs+37−26
@@ -16,6 +16,7 @@ module App =
1616 open Crypto
1717 open ApiClient
1818 open Buttons
19+ open TextFields
1920
2021 // ── Domain Types ──
2122
@@ -273,8 +274,7 @@ module App =
273274 Error = None
274275 PollStatus = "polling..."
275276 FlushingOutbox = true },
276− [ CmdPoll(session, model.PollCursor)
277− CmdFlushOutbox session ]
277+ [ CmdPoll(session, model.PollCursor); CmdFlushOutbox session ]
278278 | _ -> model, []
279279
280280 | AuthErr err ->
@@ -320,9 +320,7 @@ module App =
320320 model, []
321321 else
322322 match trySession model with
323− | Some session ->
324− { model with FlushingOutbox = true },
325− [ CmdFlushOutbox session ]
323+ | Some session -> { model with FlushingOutbox = true }, [ CmdFlushOutbox session ]
326324 | None -> model, []
327325
328326 | FlushResult sent ->
@@ -556,7 +554,10 @@ module App =
556554 let x = (skBmp.Width - size) / 2
557555 let y = (skBmp.Height - size) / 2
558556 use cropped = new SkiaSharp.SKBitmap(size, size)
559− skBmp.ExtractSubset(cropped, SkiaSharp.SKRectI(x, y, x + size, y + size)) |> ignore
557+
558+ skBmp.ExtractSubset(cropped, SkiaSharp.SKRectI(x, y, x + size, y + size))
559+ |> ignore
560+
560561 use img = SkiaSharp.SKImage.FromBitmap(cropped)
561562 use data = img.Encode(SkiaSharp.SKEncodedImageFormat.Png, 90)
562563 data.ToArray()
@@ -630,7 +631,10 @@ module App =
630631 | Some item ->
631632 try
632633 let blob =
633− Crypto.encrypt session.Identity.PrivKey (Crypto.fromHex item.RecipientHex) item.EnvelopeJson
634+ Crypto.encrypt
635+ session.Identity.PrivKey
636+ (Crypto.fromHex item.RecipientHex)
637+ item.EnvelopeJson
634638
635639 let ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
636640 do! sendMessage session.Url session.Token item.RecipientHex (Crypto.toHex blob) ts
@@ -640,8 +644,7 @@ module App =
640644 log $"outbox send error: {ex.Message}"
641645 do! Async.Sleep 3000
642646 dispatch (FlushResult true)
643− | None ->
644− dispatch (FlushResult false)
647+ | None -> dispatch (FlushResult false)
645648 }
646649 |> Async.Start)
647650
@@ -780,7 +783,7 @@ module App =
780783
781784 TextBlock("").height (50.)
782785
783− button "Next" GenIdentity
786+ button Primary "Next" GenIdentity
784787 })
785788 .margin(30.)
786789 .centerVertical ()
@@ -936,29 +939,37 @@ module App =
936939 .margin (20.)
937940
938941 let private viewEditProfile model displayName bio (photo: byte[] option) =
939− ScrollViewer(
940− (VStack(16.) {
941− HStack(8.) {
942− Button("< Back", SetPage Settings)
943− TextBlock("Edit Profile").fontSize (20.)
944− }
942+ (Grid([], [ Dimension.Star; Dimension.Auto ]) {
943+ ScrollViewer(
944+ (VStack(16.) {
945+ viewErrorBanner model.Error
945946
946− viewErrorBanner model.Error
947+ imageButton photo DoPickPhoto
947948
948− imageButton photo DoPickPhoto
949+ TextBlock("Name").fontFamily(Constants.fontFamily).fontWeight(FontWeight.Bold).margin (0, 24, 0, -8)
950+ (textField displayName (fun text -> SetPage(EditProfile(text, bio, photo)))).margin (0, 0, 0, 8)
949951
950− TextBlock("Display Name:")
951− TextBox(displayName, fun text -> SetPage(EditProfile(text, bio, photo))).watermark ("Your name")
952952
953− TextBlock("Bio:")
953+ TextBlock("Bio").fontFamily(Constants.fontFamily).fontWeight(FontWeight.Bold).margin (0, 0, 0, -8)
954954
955− TextBox(bio, fun text -> SetPage(EditProfile(displayName, text, photo)))
956− .watermark ("Tell something about yourself")
955+ (multilineTextField bio (fun text -> SetPage(EditProfile(displayName, text, photo))))
956+ .margin (0, 0, 0, 8)
957+ })
958+ .margin (20.)
959+ )
960+ .gridRow (0)
961+
962+ (Grid() {
963+ Rectangle().stroke(SolidColorBrush(Colors.Black)).strokeThickness(4.).fill (Constants.accentColor)
964+
965+ HStack(8.) {
966+ (button Secondary "back" (SetPage Settings)).margin (8.)
957967
958− Button("Save Profile", DoSaveProfile).centerHorizontal ()
968+ (button Secondary "save" DoSaveProfile).margin (8.)
969+ }
959970 })
960− .margin (20.)
961− )
971+ .gridRow (1)
972+ })
962973
963974 let private pageContent model =
964975 let page =
client/Buttons.fs+24−13
@@ -9,18 +9,17 @@ open type Fabulous.Avalonia.View
99module Buttons =
1010 open Avalonia.Media.Transformation
1111 open System
12− open Avalonia.Animation.Easings
13−
14− let private animationDuration: int64 = 60
15− let private easing = CubicEaseOut()
1612
13+ type ButtonPriority =
14+ | Primary
15+ | Secondary
1716
1817 let private translate x y =
1918 let b = TransformOperations.CreateBuilder 1
2019 b.AppendTranslate(x, y)
2120 b.Build() :> ITransform
2221
23− let button (text: string) (msg: 'msg) =
22+ let button priority (text: string) (msg: 'msg) =
2423 Component(text) {
2524 let! pressed = Context.State(false)
2625 let offset = if pressed.Current then 4. else 8.
@@ -32,17 +31,27 @@ module Buttons =
3231 .transition (
3332 TransformOperationsTransition(
3433 Avalonia.Visual.RenderTransformProperty,
35− TimeSpan.FromMilliseconds animationDuration
34+ TimeSpan.FromMilliseconds Constants.animationDuration
3635 )
37− .easing (easing)
36+ .easing (Constants.easing)
3837 )
3938
40− Rectangle().fill (SolidColorBrush(Constants.accentColor))
39+ Rectangle()
40+ .fill (
41+ match priority with
42+ | Primary -> SolidColorBrush(Constants.accentColor)
43+ | Secondary -> SolidColorBrush(Colors.White)
44+ )
4145
4246 TextBlock(text)
43− .foreground(SolidColorBrush(Colors.White))
47+ .foreground(
48+ match priority with
49+ | Primary -> SolidColorBrush(Colors.White)
50+ | Secondary -> SolidColorBrush(Colors.Black)
51+
52+ )
4453 .fontSize(24.)
45− .fontFamily(FontFamily("avares://Skrepka/Assets/Fonts#Unbounded"))
54+ .fontFamily(FontFamily(Constants.fontFamily))
4655 .fontWeight(FontWeight.Bold)
4756 .center()
4857 .margin (8.)
@@ -53,6 +62,7 @@ module Buttons =
5362 .onPointerPressed(fun _ -> pressed.Set true)
5463 .onPointerReleased(fun _ -> pressed.Set false)
5564 .onPointerExited(fun _ -> pressed.Set false)
65+ .margin(16.)
5666 .onTapped (fun _ -> msg)
5767 }
5868
@@ -73,9 +83,9 @@ module Buttons =
7383 .transition (
7484 TransformOperationsTransition(
7585 Avalonia.Visual.RenderTransformProperty,
76− TimeSpan.FromMilliseconds animationDuration
86+ TimeSpan.FromMilliseconds Constants.animationDuration
7787 )
78− .easing (easing)
88+ .easing (Constants.easing)
7989 )
8090
8191 Rectangle().fill (SolidColorBrush(Colors.White))
@@ -89,7 +99,7 @@ module Buttons =
8999 .margin (0., 20., 0., 0.)
90100
91101 Label("Upload")
92− .fontFamily(FontFamily("avares://Skrepka/Assets/Fonts#Unbounded"))
102+ .fontFamily(FontFamily(Constants.fontFamily))
93103 .fontWeight(FontWeight.Bold)
94104 .fontSize(24.)
95105 .centerContentHorizontal ()
@@ -109,5 +119,6 @@ module Buttons =
109119 .onPointerExited(fun _ -> pressed.Set false)
110120 .width(200.)
111121 .height(200.)
122+ .margin(16.)
112123 .onTapped (fun _ -> msg)
113124 }
client/Constants.fs+3
@@ -4,3 +4,6 @@ open Avalonia.Media
44
55module Constants =
66 let accentColor = Color.FromRgb(98uy, 54uy, 255uy)
7+ let animationDuration: int64 = 60
8+ let easing = Avalonia.Animation.Easings.CubicEaseOut()
9+ let fontFamily = "avares://Skrepka/Assets/Fonts#Unbounded"
client/Skrepka.fsproj+1
@@ -33,6 +33,7 @@
3333 <Compile Include="ApiClient.fs" />
3434 <Compile Include="Phonemic.fs" />
3535 <Compile Include="Store.fs" />
36+ <Compile Include="TextFields.fs" />
3637 <Compile Include="Buttons.fs" />
3738 <Compile Include="$(iOSPlatformFolder)PhotoPicker.fs" Condition="$(AvaloniaPlatform) == 'iOS'" />
3839 <Compile Include="App.fs" />
client/TextFields.fsnew file+20
@@ -0,0 +1,20 @@
1+namespace Skrepka
2+
3+open Avalonia.Media
4+open Fabulous
5+open Fabulous.Avalonia
6+
7+open type Fabulous.Avalonia.View
8+
9+module TextFields =
10+ let textField value msg =
11+ Grid() {
12+ TextBox(value, msg).fontFamily (Constants.fontFamily)
13+ Rectangle().stroke(Colors.Black).strokeThickness (4.)
14+ }
15+
16+ let multilineTextField value msg =
17+ Grid() {
18+ TextBox(value, msg).fontFamily(Constants.fontFamily).acceptsReturn(true).height (150)
19+ Rectangle().stroke(Colors.Black).strokeThickness (4.)
20+ }