git.iko.soy/rowtype-yoga/purescript-yoga-react-native
on main help sign in
Restructure Main.purs into Demo module tree, chat demo with emoji/reactions/replies, full-pane MapView/PDFView/Camera/Video, sidebar search
b4743a7Mark
example-macos/macos/YogaReactExample-macOS/RCTMacOSControlsManager.m+27
@@ -815,6 +815,8 @@ RCT_EXPORT_VIEW_PROPERTY(criticalValue, double)
815@interface RCTNativeScrollView : NSScrollView
815@interface RCTNativeScrollView : NSScrollView
816@property (nonatomic, strong) RCTFlippedDocumentView *docView;
816@property (nonatomic, strong) RCTFlippedDocumentView *docView;
817@property (nonatomic, assign) NSInteger scrollToBottom;
817@property (nonatomic, assign) NSInteger scrollToBottom;
818+@property (nonatomic, assign) CGFloat scrollToY;
819+@property (nonatomic, assign) NSInteger scrollToYTrigger;
818@end
820@end
819
821
820@implementation RCTNativeScrollView
822@implementation RCTNativeScrollView
@@ -883,6 +885,29 @@ RCT_EXPORT_VIEW_PROPERTY(criticalValue, double)
883 }
885 }
884}
886}
885
887
888+- (void)setScrollToYTrigger:(NSInteger)scrollToYTrigger {
889+ if (scrollToYTrigger == _scrollToYTrigger) return;
890+ _scrollToYTrigger = scrollToYTrigger;
891+ [self performSelector:@selector(doScrollToY) withObject:nil afterDelay:0.1];
892+}
893+
894+- (void)setScrollToY:(CGFloat)scrollToY {
895+ _scrollToY = scrollToY;
896+}
897+
898+- (void)doScrollToY {
899+ [self updateDocumentSize];
900+ CGFloat maxScroll = _docView.frame.size.height - self.bounds.size.height;
901+ CGFloat y = MIN(MAX(_scrollToY, 0), maxScroll);
902+ [NSAnimationContext runAnimationGroup:^(NSAnimationContext *ctx) {
903+ ctx.duration = 0.3;
904+ ctx.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
905+ [self.contentView.animator setBoundsOrigin:NSMakePoint(0, y)];
906+ } completionHandler:^{
907+ [self reflectScrolledClipView:self.contentView];
908+ }];
909+}
910+
886- (void)layout {
911- (void)layout {
887 [super layout];
912 [super layout];
888 [self updateDocumentSize];
913 [self updateDocumentSize];
@@ -896,6 +921,8 @@ RCT_EXPORT_VIEW_PROPERTY(criticalValue, double)
896RCT_EXPORT_MODULE(MacOSScrollView)
921RCT_EXPORT_MODULE(MacOSScrollView)
897- (NSView *)view { return [[RCTNativeScrollView alloc] initWithFrame:CGRectZero]; }
922- (NSView *)view { return [[RCTNativeScrollView alloc] initWithFrame:CGRectZero]; }
898RCT_EXPORT_VIEW_PROPERTY(scrollToBottom, NSInteger)
923RCT_EXPORT_VIEW_PROPERTY(scrollToBottom, NSInteger)
924+RCT_EXPORT_VIEW_PROPERTY(scrollToY, double)
925+RCT_EXPORT_VIEW_PROPERTY(scrollToYTrigger, NSInteger)
899@end
926@end
900
927
901// ============================================================
928// ============================================================
example-macos/src/Demo/AiMl.pursnew file+157
@@ -0,0 +1,157 @@
1+module Demo.AiMl
2+ ( ocrDemo
3+ , speechRecognitionDemo
4+ , naturalLanguageDemo
5+ , cameraDemo
6+ ) where
7+
8+import Prelude
9+
10+import Data.Foldable (for_)
11+import Data.String (joinWith)
12+import Demo.Shared (DemoProps, desc, scrollWrap, sectionTitle)
13+import Effect.Aff (launchAff_)
14+import Effect.Class (liftEffect)
15+import React.Basic (JSX)
16+import React.Basic.Events (handler, handler_, unsafeEventFn)
17+import React.Basic.Hooks (useState', (/\))
18+import React.Basic.Hooks as React
19+import Yoga.React (component)
20+import Yoga.React.Native (nativeEvent, text, tw, view)
21+import Yoga.React.Native.MacOS.Button (nativeButton)
22+import Yoga.React.Native.MacOS.CameraView (nativeCameraView)
23+import Yoga.React.Native.MacOS.Events as E
24+import Yoga.React.Native.MacOS.FilePicker (nativeFilePicker)
25+import Yoga.React.Native.MacOS.NaturalLanguage (analyzeSentiment, detectLanguage, tokenize)
26+import Yoga.React.Native.MacOS.OCR (recognizeText)
27+import Yoga.React.Native.MacOS.SpeechRecognition (useSpeechRecognition)
28+import Yoga.React.Native.MacOS.TextField (nativeTextField)
29+import Yoga.React.Native.MacOS.Types as T
30+import Yoga.React.Native.Style as Style
31+
32+ocrDemo :: DemoProps -> JSX
33+ocrDemo = component "OCRDemo" \dp -> React.do
34+ result /\ setResult <- useState' ""
35+ pure do
36+ scrollWrap dp
37+ [ sectionTitle dp.fg "OCR (Vision)"
38+ , desc dp "Recognize text in images (VNRecognizeTextRequest)"
39+ , view { style: tw "flex-row items-center mb-2" }
40+ [ nativeFilePicker
41+ { mode: T.openFile
42+ , allowedTypes: [ "public.image" ]
43+ , title: "Pick Image"
44+ , sfSymbol: "photo"
45+ , onPickFiles: handler
46+ (nativeEvent >>> unsafeEventFn \e -> E.getFieldArray "files" e)
47+ \paths -> for_ paths \path -> do
48+ setResult "Recognizing..."
49+ launchAff_ do
50+ r <- recognizeText path
51+ liftEffect (setResult r)
52+ , style: Style.style { height: 24.0, width: 140.0 }
53+ }
54+ ]
55+ , if result /= "" then view { style: tw "p-2 rounded mb-4" <> Style.style { backgroundColor: dp.cardBg } }
56+ [ text { style: tw "text-xs font-mono" <> Style.style { color: dp.fg } } result ]
57+ else mempty
58+ ]
59+
60+speechRecognitionDemo :: DemoProps -> JSX
61+speechRecognitionDemo = component "SpeechRecognitionDemo" \dp -> React.do
62+ speech <- useSpeechRecognition
63+ pure do
64+ scrollWrap dp
65+ [ sectionTitle dp.fg "Speech Recognition"
66+ , desc dp "Live microphone-to-text (click Start, speak, click Stop)"
67+ , view { style: tw "flex-row items-center mb-2" }
68+ [ nativeButton
69+ { title: if speech.listening then "Listening..." else "Start"
70+ , sfSymbol: if speech.listening then "mic.fill" else "mic"
71+ , bezelStyle: T.push
72+ , onPress: handler_ speech.start
73+ , style: Style.style { height: 24.0, width: 130.0 }
74+ }
75+ , nativeButton
76+ { title: "Stop"
77+ , sfSymbol: "stop.circle"
78+ , bezelStyle: T.push
79+ , onPress: handler_ speech.stop
80+ , style: Style.style { height: 24.0, width: 80.0, marginLeft: 8.0 }
81+ }
82+ ]
83+ , if speech.transcript /= "" then view { style: tw "p-2 rounded mb-4" <> Style.style { backgroundColor: dp.cardBg } }
84+ [ text { style: tw "text-xs font-mono" <> Style.style { color: dp.fg } } speech.transcript ]
85+ else mempty
86+ ]
87+
88+naturalLanguageDemo :: DemoProps -> JSX
89+naturalLanguageDemo = component "NaturalLanguageDemo" \dp -> React.do
90+ txt /\ setTxt <- useState' "I love this amazing app! C'est magnifique."
91+ result /\ setResult <- useState' ""
92+ pure do
93+ scrollWrap dp
94+ [ sectionTitle dp.fg "Natural Language"
95+ , desc dp "Language detection, sentiment, tokenization (NaturalLanguage.framework)"
96+ , nativeTextField
97+ { text: txt
98+ , placeholder: "Enter text to analyze..."
99+ , onChangeText: E.onString "text" setTxt
100+ , style: Style.style { height: 24.0 } <> tw "mb-2"
101+ }
102+ , view { style: tw "flex-row items-center mb-2" }
103+ [ nativeButton
104+ { title: "Language"
105+ , sfSymbol: "globe"
106+ , bezelStyle: T.push
107+ , onPress: handler_ $ launchAff_ do
108+ lang <- detectLanguage txt
109+ liftEffect (setResult ("Language: " <> lang))
110+ , style: Style.style { height: 24.0, width: 110.0 }
111+ }
112+ , nativeButton
113+ { title: "Sentiment"
114+ , sfSymbol: "face.smiling"
115+ , bezelStyle: T.push
116+ , onPress: handler_ $ launchAff_ do
117+ score <- analyzeSentiment txt
118+ liftEffect (setResult ("Sentiment: " <> show score))
119+ , style: Style.style { height: 24.0, width: 110.0, marginLeft: 8.0 }
120+ }
121+ , nativeButton
122+ { title: "Tokenize"
123+ , sfSymbol: "text.word.spacing"
124+ , bezelStyle: T.push
125+ , onPress: handler_ $ launchAff_ do
126+ tokens <- tokenize txt
127+ liftEffect (setResult ("Tokens: " <> joinWith ", " tokens))
128+ , style: Style.style { height: 24.0, width: 110.0, marginLeft: 8.0 }
129+ }
130+ ]
131+ , if result /= "" then view { style: tw "p-2 rounded mb-4" <> Style.style { backgroundColor: dp.cardBg } }
132+ [ text { style: tw "text-xs font-mono" <> Style.style { color: dp.fg } } result ]
133+ else mempty
134+ ]
135+
136+cameraDemo :: DemoProps -> JSX
137+cameraDemo = component "CameraDemo" \dp -> React.do
138+ on /\ setOn <- useState' false
139+ pure do
140+ view { style: tw "flex-1" }
141+ [ view { style: tw "flex-row items-center px-3 py-2" <> Style.style { backgroundColor: "transparent" } }
142+ [ text { style: tw "text-sm font-semibold mr-3" <> Style.style { color: dp.fg } } "Camera"
143+ , nativeButton
144+ { title: if on then "Stop" else "Start"
145+ , sfSymbol: if on then "video.slash" else "video"
146+ , bezelStyle: T.push
147+ , onPress: handler_ (setOn (not on))
148+ , style: Style.style { height: 24.0, width: 100.0 }
149+ }
150+ ]
151+ , if on then nativeCameraView
152+ { active: true
153+ , style: tw "flex-1"
154+ }
155+ else view { style: tw "flex-1 items-center justify-center" }
156+ [ text { style: tw "text-sm" <> Style.style { color: dp.dimFg } } "Press Start to activate camera" ]
157+ ]
example-macos/src/Demo/Animation.pursnew file+66
@@ -0,0 +1,66 @@
1+module Demo.Animation (riveDemo) where
2+
3+import Prelude
4+
5+import Demo.Shared (DemoProps, sectionTitle)
6+import React.Basic (JSX)
7+import Yoga.React (component)
8+import Yoga.React.Native (text, tw, view)
9+import Yoga.React.Native.MacOS.Rive (nativeRiveView_)
10+import Yoga.React.Native.MacOS.ScrollView (nativeScrollView)
11+import Yoga.React.Native.MacOS.Types as T
12+import Yoga.React.Native.Style as Style
13+
14+riveDemo :: DemoProps -> JSX
15+riveDemo = component "RiveDemo" \dp -> pure do
16+ nativeScrollView { style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
17+ ( view { style: tw "px-4 pb-4" }
18+ [ sectionTitle dp.fg "Mouse Tracking"
19+ , text { style: tw "text-xs mb-2" <> Style.style { color: dp.fg } }
20+ "Move your cursor over the cat โ€” it follows your mouse!"
21+ , nativeRiveView_
22+ { resourceName: "cat_following_mouse"
23+ , stateMachineName: "State Machine 1"
24+ , fit: T.contain
25+ , autoplay: true
26+ , style: Style.style { height: 300.0 }
27+ }
28+ , sectionTitle dp.fg "Cursor Tracking"
29+ , text { style: tw "text-xs mb-2" <> Style.style { color: dp.fg } }
30+ "Shapes follow your cursor via Pointer Move listeners"
31+ , nativeRiveView_
32+ { resourceName: "cursor_tracking"
33+ , stateMachineName: "State Machine 1"
34+ , fit: T.contain
35+ , autoplay: true
36+ , style: Style.style { height: 300.0 }
37+ }
38+ , sectionTitle dp.fg "Interactive Rive Animations"
39+ , text { style: tw "text-xs mb-2" <> Style.style { color: dp.fg } }
40+ "Click the stars to rate! State machine handles interaction."
41+ , nativeRiveView_
42+ { resourceName: "rating_animation"
43+ , stateMachineName: "State Machine 1"
44+ , fit: T.contain
45+ , autoplay: true
46+ , style: Style.style { height: 200.0 }
47+ }
48+ , sectionTitle dp.fg "Light Switch"
49+ , text { style: tw "text-xs mb-2" <> Style.style { color: dp.fg } }
50+ "Click to toggle"
51+ , nativeRiveView_
52+ { resourceName: "switch_event_example"
53+ , stateMachineName: "State Machine 1"
54+ , fit: T.contain
55+ , autoplay: true
56+ , style: Style.style { height: 200.0 }
57+ }
58+ , sectionTitle dp.fg "Windy Tree"
59+ , nativeRiveView_
60+ { resourceName: "windy_tree"
61+ , fit: T.cover
62+ , autoplay: true
63+ , style: tw "flex-1" <> Style.style { minHeight: 300.0, backgroundColor: dp.bg }
64+ }
65+ ]
66+ )
example-macos/src/Demo/Chat.jsnew file+141
@@ -0,0 +1,141 @@
1+export const isSingleEmoji = (str) => {
2+ var trimmed = str.trim();
3+ var re =
4+ /^(?:\p{Emoji_Presentation}|\p{Emoji}\uFE0F)(?:\u200D(?:\p{Emoji_Presentation}|\p{Emoji}\uFE0F))*$/u;
5+ return trimmed.length <= 10 && re.test(trimmed);
6+};
7+export const charCodes = (str) => Array.from(str).map((c) => c.charCodeAt(0));
8+export const abs = (n) => Math.abs(n);
9+
10+var emojiMap = {
11+ "+1": "๐Ÿ‘",
12+ "-1": "๐Ÿ‘Ž",
13+ heart: "โค๏ธ",
14+ broken_heart: "๐Ÿ’”",
15+ fire: "๐Ÿ”ฅ",
16+ rocket: "๐Ÿš€",
17+ star: "โญ",
18+ sparkles: "โœจ",
19+ tada: "๐ŸŽ‰",
20+ party_popper: "๐ŸŽ‰",
21+ smile: "๐Ÿ˜„",
22+ grin: "๐Ÿ˜",
23+ laugh: "๐Ÿ˜‚",
24+ joy: "๐Ÿ˜‚",
25+ wink: "๐Ÿ˜‰",
26+ blush: "๐Ÿ˜Š",
27+ heart_eyes: "๐Ÿ˜",
28+ kissing_heart: "๐Ÿ˜˜",
29+ thinking: "๐Ÿค”",
30+ shush: "๐Ÿคซ",
31+ rolling_eyes: "๐Ÿ™„",
32+ grimace: "๐Ÿ˜ฌ",
33+ sob: "๐Ÿ˜ญ",
34+ angry: "๐Ÿ˜ ",
35+ skull: "๐Ÿ’€",
36+ clown: "๐Ÿคก",
37+ ghost: "๐Ÿ‘ป",
38+ alien: "๐Ÿ‘ฝ",
39+ robot: "๐Ÿค–",
40+ poop: "๐Ÿ’ฉ",
41+ wave: "๐Ÿ‘‹",
42+ clap: "๐Ÿ‘",
43+ pray: "๐Ÿ™",
44+ muscle: "๐Ÿ’ช",
45+ eyes: "๐Ÿ‘€",
46+ brain: "๐Ÿง ",
47+ tongue: "๐Ÿ‘…",
48+ sun: "โ˜€๏ธ",
49+ moon: "๐ŸŒ™",
50+ cloud: "โ˜๏ธ",
51+ rainbow: "๐ŸŒˆ",
52+ snowflake: "โ„๏ธ",
53+ zap: "โšก",
54+ cyclone: "๐ŸŒ€",
55+ umbrella: "โ˜‚๏ธ",
56+ dog: "๐Ÿถ",
57+ cat: "๐Ÿฑ",
58+ unicorn: "๐Ÿฆ„",
59+ bee: "๐Ÿ",
60+ butterfly: "๐Ÿฆ‹",
61+ crab: "๐Ÿฆ€",
62+ octopus: "๐Ÿ™",
63+ snake: "๐Ÿ",
64+ turtle: "๐Ÿข",
65+ apple: "๐ŸŽ",
66+ pizza: "๐Ÿ•",
67+ taco: "๐ŸŒฎ",
68+ coffee: "โ˜•",
69+ beer: "๐Ÿบ",
70+ wine: "๐Ÿท",
71+ cake: "๐ŸŽ‚",
72+ cookie: "๐Ÿช",
73+ icecream: "๐Ÿฆ",
74+ soccer: "โšฝ",
75+ basketball: "๐Ÿ€",
76+ football: "๐Ÿˆ",
77+ tennis: "๐ŸŽพ",
78+ check: "โœ…",
79+ x: "โŒ",
80+ warning: "โš ๏ธ",
81+ question: "โ“",
82+ exclamation: "โ—",
83+ bulb: "๐Ÿ’ก",
84+ pin: "๐Ÿ“Œ",
85+ link: "๐Ÿ”—",
86+ lock: "๐Ÿ”’",
87+ key: "๐Ÿ”‘",
88+ hammer: "๐Ÿ”จ",
89+ wrench: "๐Ÿ”ง",
90+ gear: "โš™๏ธ",
91+ gem: "๐Ÿ’Ž",
92+ trophy: "๐Ÿ†",
93+ gift: "๐ŸŽ",
94+ balloon: "๐ŸŽˆ",
95+ confetti: "๐ŸŽŠ",
96+ music: "๐ŸŽต",
97+ mic: "๐ŸŽค",
98+ art: "๐ŸŽจ",
99+ film: "๐ŸŽฌ",
100+ camera: "๐Ÿ“ท",
101+ phone: "๐Ÿ“ฑ",
102+ laptop: "๐Ÿ’ป",
103+ mail: "๐Ÿ“ง",
104+ memo: "๐Ÿ“",
105+ book: "๐Ÿ“–",
106+ clock: "๐Ÿ•",
107+ hourglass: "โณ",
108+ 100: "๐Ÿ’ฏ",
109+ zzz: "๐Ÿ’ค",
110+ boom: "๐Ÿ’ฅ",
111+ sweat_drops: "๐Ÿ’ฆ",
112+ dash: "๐Ÿ’จ",
113+ point_up: "โ˜๏ธ",
114+ point_down: "๐Ÿ‘‡",
115+ point_left: "๐Ÿ‘ˆ",
116+ point_right: "๐Ÿ‘‰",
117+ ok_hand: "๐Ÿ‘Œ",
118+ peace: "โœŒ๏ธ",
119+ crossed_fingers: "๐Ÿคž",
120+ metal: "๐Ÿค˜",
121+ thumbsup: "๐Ÿ‘",
122+ thumbsdown: "๐Ÿ‘Ž",
123+ raised_hands: "๐Ÿ™Œ",
124+ handshake: "๐Ÿค",
125+ hugs: "๐Ÿค—",
126+ salute: "๐Ÿซก",
127+ shrug: "๐Ÿคท",
128+ purescript: "โŠณ",
129+ haskell: "ฮป",
130+ rust: "๐Ÿฆ€",
131+ js: "๐Ÿ“œ",
132+ ts: "๐Ÿ”ท",
133+};
134+export const replaceEmoji = (str) =>
135+ str.replace(
136+ /:([a-z0-9_]+):/gi,
137+ (m, code) => emojiMap[code.toLowerCase()] || m
138+ );
139+export const setTimeout_ = (ms, fn) => {
140+ setTimeout(fn, ms);
141+};
example-macos/src/Demo/Chat.pursnew file+379
@@ -0,0 +1,379 @@
1+module Demo.Chat (chatDemo) where
2+
3+import Prelude
4+
5+import Data.Array (mapWithIndex, length, filter, snoc, (!!), modifyAt)
6+import Data.Foldable (foldl)
7+import Data.Int (toNumber)
8+import Data.Maybe (Maybe(..), fromMaybe)
9+import Data.String (take)
10+import Data.String as String
11+import Demo.Shared (DemoProps)
12+import Effect (Effect)
13+import Effect.Uncurried (EffectFn2, runEffectFn2)
14+import React.Basic (JSX)
15+import React.Basic.Events (handler_)
16+import React.Basic.Hooks (useState, useState', (/\))
17+import React.Basic.Hooks as React
18+import Yoga.React (component)
19+import Yoga.React.Native (text, tw, view)
20+import Yoga.React.Native.MacOS.Button (nativeButton)
21+import Yoga.React.Native.MacOS.Events as E
22+import Yoga.React.Native.MacOS.PatternBackground (nativePatternBackground)
23+import Yoga.React.Native.MacOS.Popover (nativePopover)
24+import Yoga.React.Native.MacOS.ScrollView (nativeScrollView)
25+import Yoga.React.Native.MacOS.Sidebar (sidebarLayout)
26+import Yoga.React.Native.MacOS.TextField (nativeTextField)
27+import Yoga.React.Native.MacOS.Types as T
28+import Yoga.React.Native.Style as Style
29+
30+type Reaction = { emoji :: String, count :: Int }
31+
32+type Message =
33+ { sender :: String
34+ , body :: String
35+ , isMine :: Boolean
36+ , reactions :: Array Reaction
37+ , replyTo :: Maybe Int
38+ }
39+
40+type Room =
41+ { roomId :: String
42+ , name :: String
43+ , lastMessage :: String
44+ }
45+
46+reactionEmoji :: Array String
47+reactionEmoji = [ "๐Ÿ‘", "โค๏ธ", "๐Ÿ˜‚", "๐Ÿ˜ฎ", "๐Ÿ˜ข", "๐Ÿ”ฅ" ]
48+
49+avatarColors :: Array String
50+avatarColors = [ "#5856D6", "#FF9500", "#34C759", "#FF2D55", "#5AC8FA", "#FF6B6B", "#4ECDC4", "#45B7D1" ]
51+
52+avatarColor :: String -> String
53+avatarColor roomId = fromMaybe "#5856D6" (avatarColors !! (mod (hashCode roomId) (length avatarColors)))
54+ where
55+ hashCode s = abs (foldl (\acc c -> acc * 31 + c) 0 (charCodes s))
56+
57+foreign import charCodes :: String -> Array Int
58+foreign import abs :: Int -> Int
59+foreign import isSingleEmoji :: String -> Boolean
60+foreign import replaceEmoji :: String -> String
61+foreign import setTimeout_ :: EffectFn2 Int (Effect Unit) Unit
62+
63+addReaction :: String -> Array Reaction -> Array Reaction
64+addReaction emoji rs = case filter (\r -> r.emoji == emoji) rs of
65+ [] -> snoc rs { emoji, count: 1 }
66+ _ -> map (\r -> if r.emoji == emoji then r { count = r.count + 1 } else r) rs
67+
68+mockRooms :: Array Room
69+mockRooms =
70+ [ { roomId: "general", name: "General", lastMessage: "Welcome!" }
71+ , { roomId: "purescript", name: "PureScript", lastMessage: "Types are great" }
72+ , { roomId: "react-native", name: "React Native", lastMessage: "Check out macOS support" }
73+ , { roomId: "random", name: "Random", lastMessage: "Anyone here?" }
74+ ]
75+
76+mockMessages :: String -> Array Message
77+mockMessages room = map (\m -> m { body = replaceEmoji m.body }) (mockMessagesRaw room)
78+
79+noReactions :: Array Reaction
80+noReactions = []
81+
82+mockMessagesRaw :: String -> Array Message
83+mockMessagesRaw = case _ of
84+ "general" ->
85+ [ { sender: "Alice", body: "Hey everyone! Welcome to the chat demo :wave:", isMine: false, reactions: [ { emoji: "๐Ÿ‘‹", count: 3 } ], replyTo: Nothing }
86+ , { sender: "Bob", body: "This is built with PureScript + React Native macOS :purescript:", isMine: false, reactions: [ { emoji: "๐Ÿ”ฅ", count: 2 }, { emoji: "โค๏ธ", count: 1 } ], replyTo: Nothing }
87+ , { sender: "You", body: "That's awesome! :fire:", isMine: true, reactions: noReactions, replyTo: Just 1 }
88+ , { sender: "Alice", body: "Try typing :rocket: or :heart: in a message", isMine: false, reactions: noReactions, replyTo: Nothing }
89+ , { sender: "You", body: "The bubbles look great :sparkles:", isMine: true, reactions: [ { emoji: "๐Ÿ‘", count: 2 } ], replyTo: Nothing }
90+ , { sender: "Bob", body: "Click the smiley to react!", isMine: false, reactions: noReactions, replyTo: Just 4 }
91+ , { sender: "Alice", body: ":tada:", isMine: false, reactions: [ { emoji: "๐ŸŽ‰", count: 5 } ], replyTo: Nothing }
92+ ]
93+ "purescript" ->
94+ [ { sender: "Phil", body: "Has anyone tried the new compiler release?", isMine: false, reactions: [ { emoji: "๐Ÿ‘", count: 4 } ], replyTo: Nothing }
95+ , { sender: "You", body: "Yes! The build times are much better", isMine: true, reactions: noReactions, replyTo: Just 0 }
96+ , { sender: "Phil", body: "Row polymorphism makes FFI so clean", isMine: false, reactions: [ { emoji: "โค๏ธ", count: 3 } ], replyTo: Nothing }
97+ , { sender: "Jordan", body: "The ecosystem keeps getting better", isMine: false, reactions: noReactions, replyTo: Nothing }
98+ , { sender: "You", body: "Agreed, especially for React Native", isMine: true, reactions: [ { emoji: "๐Ÿ”ฅ", count: 1 } ], replyTo: Just 2 }
99+ ]
100+ "react-native" ->
101+ [ { sender: "Christoph", body: "macOS support is looking solid", isMine: false, reactions: [ { emoji: "๐Ÿ”ฅ", count: 3 } ], replyTo: Nothing }
102+ , { sender: "You", body: "Native controls feel right at home", isMine: true, reactions: noReactions, replyTo: Just 0 }
103+ , { sender: "Christoph", body: "NSOutlineView, NSSplitView, toolbars...", isMine: false, reactions: noReactions, replyTo: Nothing }
104+ , { sender: "You", body: "Even the vibrancy effects work", isMine: true, reactions: [ { emoji: "๐Ÿ˜ฎ", count: 2 } ], replyTo: Nothing }
105+ , { sender: "Christoph", body: "๐Ÿš€", isMine: false, reactions: [ { emoji: "๐Ÿš€", count: 7 } ], replyTo: Nothing }
106+ ]
107+ "random" ->
108+ [ { sender: "Eve", body: "Anyone here?", isMine: false, reactions: noReactions, replyTo: Nothing }
109+ , { sender: "You", body: "๐Ÿ‘‹", isMine: true, reactions: [ { emoji: "๐Ÿ‘‹", count: 1 } ], replyTo: Just 0 }
110+ ]
111+ _ -> []
112+
113+truncate :: Int -> String -> String
114+truncate n s = if String.length s <= n then s else take n s <> "โ€ฆ"
115+
116+estimatedY :: Int -> Number
117+estimatedY idx = 8.0 + 70.0 * toNumber idx
118+
119+chatDemo :: DemoProps -> JSX
120+chatDemo = component "ChatDemo" \dp -> React.do
121+ activeRoom /\ setActiveRoom <- useState' (Just "general")
122+ messages /\ setMessages <- useState (mockMessages "general")
123+ inputText /\ setInputText <- useState' ""
124+ reactPopover /\ setReactPopover <- useState' (Nothing :: Maybe Int)
125+ replyingTo /\ setReplyingTo <- useState' (Nothing :: Maybe Int)
126+ highlightIdx /\ setHighlightIdx <- useState' (Nothing :: Maybe Int)
127+ scrollBottomTrigger /\ setScrollBottomTrigger <- useState 1
128+ scrollY /\ setScrollY <- useState' 0.0
129+ scrollTrigger /\ setScrollTrigger <- useState 0
130+ let
131+ selectRoom rid = do
132+ setActiveRoom (Just rid)
133+ setMessages (const (mockMessages rid))
134+ setReactPopover Nothing
135+ setReplyingTo Nothing
136+ setScrollBottomTrigger (_ + 1)
137+
138+ sendMessage _rid msg = do
139+ if msg == "" then pure unit
140+ else do
141+ let newMsg = { sender: "You", body: replaceEmoji msg, isMine: true, reactions: noReactions, replyTo: replyingTo }
142+ setInputText ""
143+ setReplyingTo Nothing
144+ setMessages \msgs -> snoc msgs newMsg
145+ setScrollBottomTrigger (_ + 1)
146+
147+ reactToMessage idx emoji = do
148+ setMessages \msgs -> fromMaybe msgs (modifyAt idx (\m -> m { reactions = addReaction emoji m.reactions }) msgs)
149+ setReactPopover Nothing
150+
151+ scrollToMessage idx = do
152+ setScrollY (estimatedY idx)
153+ setScrollTrigger (_ + 1)
154+ setHighlightIdx (Just idx)
155+ runEffectFn2 setTimeout_ 1500 (setHighlightIdx Nothing)
156+
157+ sentBubbleBg = if dp.isDark then "#65B86A" else "#5CB85C"
158+ receivedBubbleBg = if dp.isDark then "#3B3B3D" else "#FFFFFF"
159+ reactionBg = if dp.isDark then "#3B3B3D" else "#F0F0F0"
160+ replyQuoteBg = if dp.isDark then "#2A2A2C" else "#F0EDE8"
161+ replyBarColor = if dp.isDark then "#65B86A" else "#5CB85C"
162+ highlightBg = if dp.isDark then "#3A3A1E" else "#FFF9C4"
163+ chatBg = if dp.isDark then "#17212B" else "#E8DFD3"
164+
165+ roomSidebar = view { style: tw "pt-2" }
166+ ( mockRooms <#> \r ->
167+ view
168+ { style: tw "flex-row items-center px-3 py-2 mx-2 rounded-lg"
169+ <> Style.style
170+ { backgroundColor: if activeRoom == Just r.roomId then (if dp.isDark then "#2B5278" else "#419FD9") else "transparent" }
171+ }
172+ [ view
173+ { style: tw "rounded-full items-center justify-center"
174+ <> Style.style { width: 32.0, height: 32.0, backgroundColor: avatarColor r.roomId }
175+ }
176+ [ text { style: tw "text-xs font-bold" <> Style.style { color: "#FFFFFF" } } (take 1 r.name) ]
177+ , view { style: tw "ml-2 flex-1" }
178+ [ text
179+ { style: tw "text-sm font-semibold"
180+ <> Style.style { color: if activeRoom == Just r.roomId then "#FFFFFF" else dp.fg }
181+ }
182+ r.name
183+ , text
184+ { style: tw "text-xs"
185+ <> Style.style { color: if activeRoom == Just r.roomId then "#FFFFFFCC" else dp.dimFg }
186+ }
187+ r.lastMessage
188+ ]
189+ , nativeButton
190+ { title: ""
191+ , bezelStyle: T.toolbar
192+ , sfSymbol: ""
193+ , onPress: handler_ (selectRoom r.roomId)
194+ , style: Style.style { position: "absolute", top: 0.0, left: 0.0, right: 0.0, bottom: 0.0, opacity: 0.0 }
195+ }
196+ ]
197+ )
198+
199+ reactionPills msg =
200+ if length msg.reactions == 0 then mempty
201+ else view { style: tw "flex-row mt-1" <> Style.style { alignSelf: if msg.isMine then "flex-end" else "flex-start" } }
202+ ( msg.reactions <#> \r ->
203+ view
204+ { style: tw "flex-row items-center rounded-full px-2 py-0.5 mr-1"
205+ <> Style.style { backgroundColor: reactionBg }
206+ }
207+ [ text { style: Style.style { fontSize: 12.0 } } r.emoji
208+ , text { style: tw "text-xs ml-1" <> Style.style { color: dp.dimFg } } (show r.count)
209+ ]
210+ )
211+
212+ reactionPicker idx =
213+ nativePopover
214+ { visible: reactPopover == Just idx
215+ , preferredEdge: T.bottom
216+ , behavior: T.transient
217+ , onClose: handler_ (setReactPopover Nothing)
218+ , style: Style.style {}
219+ }
220+ [ nativeButton
221+ { title: "โ˜บ"
222+ , bezelStyle: T.toolbar
223+ , onPress: handler_
224+ ( if reactPopover == Just idx then setReactPopover Nothing
225+ else setReactPopover (Just idx)
226+ )
227+ , style: Style.style { height: 20.0, width: 24.0 }
228+ }
229+ , view { style: tw "flex-row items-center p-2" <> Style.style { height: 40.0 } }
230+ ( reactionEmoji <#> \emoji ->
231+ nativeButton
232+ { title: emoji
233+ , bezelStyle: T.toolbar
234+ , onPress: handler_ (reactToMessage idx emoji)
235+ , style: Style.style { height: 32.0, width: 32.0 }
236+ }
237+ )
238+ ]
239+
240+ replyQuote msg = case msg.replyTo of
241+ Nothing -> mempty
242+ Just origIdx -> case messages !! origIdx of
243+ Nothing -> mempty
244+ Just orig ->
245+ view
246+ { style: tw "flex-row rounded-lg mb-1 overflow-hidden"
247+ <> Style.style { backgroundColor: replyQuoteBg }
248+ }
249+ [ view { style: Style.style { width: 3.0, backgroundColor: replyBarColor } } []
250+ , view { style: tw "px-2 py-1" }
251+ [ text { style: tw "text-xs font-semibold" <> Style.style { color: replyBarColor } } orig.sender
252+ , text { style: tw "text-xs" <> Style.style { color: dp.dimFg } } (truncate 80 orig.body)
253+ ]
254+ , nativeButton
255+ { title: ""
256+ , bezelStyle: T.toolbar
257+ , onPress: handler_ (scrollToMessage origIdx)
258+ , style: Style.style { position: "absolute", top: 0.0, left: 0.0, right: 0.0, bottom: 0.0, opacity: 0.0 }
259+ }
260+ ]
261+
262+ messageBubble idx msg = do
263+ let align = if msg.isMine then "flex-end" else "flex-start"
264+ let bigEmoji = isSingleEmoji msg.body
265+ let senderLabel = if msg.isMine then "" else msg.sender
266+ let isHighlighted = highlightIdx == Just idx
267+ view { style: tw "px-3 mb-1 rounded-lg" <> Style.style { backgroundColor: if isHighlighted then highlightBg else "transparent" } }
268+ [ view { style: tw "flex-row items-end" <> Style.style { alignSelf: align } }
269+ [ view
270+ { onDoubleClick: handler_ (setReplyingTo (Just idx))
271+ , style: Style.style { maxWidth: 360.0 }
272+ }
273+ [ if senderLabel == "" then text { style: Style.style { height: 0.0 } } ""
274+ else text { style: tw "text-xs mb-0.5" <> Style.style { color: dp.dimFg } } senderLabel
275+ , replyQuote msg
276+ , if bigEmoji then
277+ text
278+ { style: Style.style { fontSize: 64.0, lineHeight: 72.0 } }
279+ msg.body
280+ else
281+ view
282+ { style: tw "rounded-2xl px-3 py-2"
283+ <> Style.style
284+ { backgroundColor: if msg.isMine then sentBubbleBg else receivedBubbleBg }
285+ }
286+ [ text
287+ { style: tw "text-sm"
288+ <> Style.style { color: if msg.isMine then "#FFFFFF" else (if dp.isDark then "#FFFFFF" else "#000000") }
289+ }
290+ msg.body
291+ ]
292+ , reactionPills msg
293+ ]
294+ , if msg.isMine then mempty else reactionPicker idx
295+ ]
296+ ]
297+
298+ replyPreview = case replyingTo of
299+ Nothing -> mempty
300+ Just idx -> case messages !! idx of
301+ Nothing -> mempty
302+ Just orig ->
303+ view
304+ { style: tw "flex-row items-center px-3 py-2"
305+ <> Style.style { borderTopWidth: 0.5, borderColor: dp.dimFg, backgroundColor: "transparent" }
306+ }
307+ [ view { style: Style.style { width: 3.0, height: 28.0, borderRadius: 2.0, backgroundColor: replyBarColor, marginRight: 8.0 } } []
308+ , view { style: tw "flex-1" }
309+ [ text { style: tw "text-xs font-semibold" <> Style.style { color: replyBarColor } } ("Reply to " <> orig.sender)
310+ , text { style: tw "text-xs" <> Style.style { color: dp.dimFg } } (truncate 80 orig.body)
311+ ]
312+ , nativeButton
313+ { title: "โœ•"
314+ , bezelStyle: T.toolbar
315+ , onPress: handler_ (setReplyingTo Nothing)
316+ , style: Style.style { height: 20.0, width: 24.0 }
317+ }
318+ ]
319+
320+ activeRoomName = case activeRoom of
321+ Nothing -> "Select a room"
322+ Just rid -> fromMaybe rid (map _.name (filter (\r -> r.roomId == rid) mockRooms !! 0))
323+
324+ pure do
325+ sidebarLayout
326+ { sidebar: roomSidebar
327+ , sidebarWidth: 220.0
328+ , content: view { style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
329+ [ view
330+ { style: tw "px-4 py-2 border-b"
331+ <> Style.style { borderBottomWidth: 0.5, borderColor: dp.dimFg, backgroundColor: "transparent" }
332+ }
333+ [ text { style: tw "text-base font-semibold" <> Style.style { color: dp.fg } } activeRoomName ]
334+ , nativePatternBackground
335+ { patternColor: if dp.isDark then "#FFFFFF" else "#000000"
336+ , background: chatBg
337+ , patternOpacity: if dp.isDark then 0.04 else 0.06
338+ , patternScale: 1.0
339+ , style: tw "flex-1"
340+ }
341+ ( nativeScrollView
342+ { scrollToBottom: scrollBottomTrigger
343+ , scrollToY: scrollY
344+ , scrollToYTrigger: scrollTrigger
345+ , style: tw "flex-1" <> Style.style { backgroundColor: "transparent" }
346+ }
347+ ( view { style: tw "py-2 pr-3" }
348+ (mapWithIndex messageBubble messages)
349+ )
350+ )
351+ , replyPreview
352+ , view
353+ { style: tw "flex-row items-center px-3 py-2"
354+ <> Style.style { borderTopWidth: 0.5, borderColor: dp.dimFg, backgroundColor: "transparent" }
355+ }
356+ ( case activeRoom of
357+ Nothing ->
358+ [ text { style: tw "text-sm" <> Style.style { color: dp.dimFg } } "Select a room to start chatting" ]
359+ Just rid ->
360+ [ nativeTextField
361+ { text: inputText
362+ , placeholder: "Message..."
363+ , search: false
364+ , rounded: true
365+ , onChangeText: E.onString "text" \t -> setInputText (replaceEmoji t)
366+ , onSubmit: E.onString "text" \t -> sendMessage rid t
367+ , style: tw "flex-1" <> Style.style { height: 28.0 }
368+ }
369+ , nativeButton
370+ { title: ""
371+ , sfSymbol: "paperplane.fill"
372+ , bezelStyle: T.toolbar
373+ , onPress: handler_ (sendMessage rid inputText)
374+ , style: Style.style { height: 28.0, width: 36.0, marginLeft: 8.0 }
375+ }
376+ ]
377+ )
378+ ]
379+ }
example-macos/src/Demo/DataViews.pursnew file+77
@@ -0,0 +1,77 @@
1+module Demo.DataViews (tableViewDemo, outlineViewDemo) where
2+
3+import Prelude
4+
5+import Demo.Shared (DemoProps, desc, label, scrollWrap, sectionTitle)
6+import React.Basic (JSX)
7+import React.Basic.Hooks (useState', (/\))
8+import React.Basic.Hooks as React
9+import Yoga.React (component)
10+import Yoga.React.Native (tw)
11+import Yoga.React.Native.MacOS.Events as E
12+import Yoga.React.Native.MacOS.OutlineView (OutlineItem(..), nativeOutlineView)
13+import Yoga.React.Native.MacOS.TableView (nativeTableView)
14+import Yoga.React.Native.Style as Style
15+
16+tableViewDemo :: DemoProps -> JSX
17+tableViewDemo = component "TableViewDemo" \dp -> React.do
18+ selected /\ setSelected <- useState' ""
19+ pure do
20+ scrollWrap dp
21+ [ sectionTitle dp.fg "Table View"
22+ , desc dp "Native NSTableView with columns and rows"
23+ , nativeTableView
24+ { columns:
25+ [ { id: "name", title: "Name", width: 150.0 }
26+ , { id: "type", title: "Type", width: 100.0 }
27+ , { id: "size", title: "Size", width: 80.0 }
28+ ]
29+ , rows:
30+ [ [ "Main.purs", "PureScript", "12 KB" ]
31+ , [ "App.tsx", "TypeScript", "8 KB" ]
32+ , [ "style.css", "CSS", "3 KB" ]
33+ , [ "index.html", "HTML", "1 KB" ]
34+ , [ "package.json", "JSON", "2 KB" ]
35+ ]
36+ , headerVisible: true
37+ , alternatingRows: true
38+ , onSelectRow: E.onInt "rowIndex" \i -> setSelected ("Row " <> show i)
39+ , onDoubleClickRow: E.onInt "rowIndex" \i -> setSelected ("Double-clicked row " <> show i)
40+ , style: Style.style { height: 180.0 } <> tw "rounded-lg overflow-hidden mb-2"
41+ }
42+ , if selected == "" then mempty
43+ else label dp.dimFg selected
44+ ]
45+
46+outlineViewDemo :: DemoProps -> JSX
47+outlineViewDemo = component "OutlineViewDemo" \dp -> React.do
48+ selection /\ setSelection <- useState' ""
49+ pure do
50+ scrollWrap dp
51+ [ sectionTitle dp.fg "Outline View"
52+ , desc dp "Hierarchical tree list (NSOutlineView)"
53+ , nativeOutlineView
54+ { items: do
55+ let file id title = OutlineItem { id, title, sfSymbol: "doc", children: [] }
56+ let folder id title children = OutlineItem { id, title, sfSymbol: "folder", children }
57+ [ folder "src" "src"
58+ [ file "main" "Main.purs"
59+ , folder "macos" "MacOS"
60+ [ file "btn" "Button.purs"
61+ , file "sl" "Slider.purs"
62+ , file "sw" "Switch.purs"
63+ ]
64+ ]
65+ , folder "test" "test"
66+ [ file "t1" "MacOSComponents.test.js"
67+ , file "t2" "MacOSSnapshots.test.js"
68+ ]
69+ , OutlineItem { id: "pkg", title: "package.json", sfSymbol: "doc.text", children: [] }
70+ ]
71+ , headerVisible: false
72+ , onSelectItem: E.onString "id" setSelection
73+ , style: Style.style { height: 200.0 } <> tw "rounded-lg overflow-hidden mb-2"
74+ }
75+ , if selection == "" then mempty
76+ else label dp.dimFg ("Selected: " <> selection)
77+ ]
example-macos/src/Demo/Display.pursnew file+148
@@ -0,0 +1,148 @@
1+module Demo.Display
2+ ( imageDemo
3+ , animatedImageDemo
4+ , videoPlayerDemo
5+ , levelIndicatorDemo
6+ , progressDemo
7+ , separatorDemo
8+ , pathControlDemo
9+ ) where
10+
11+import Prelude
12+
13+import Demo.Shared (DemoProps, card, desc, scrollWrap, sectionTitle)
14+import React.Basic (JSX)
15+import React.Basic.Hooks (useState', (/\))
16+import React.Basic.Hooks as React
17+import Yoga.React (component)
18+import Yoga.React.Native (text, tw, view)
19+import Yoga.React.Native.MacOS.AnimatedImage (nativeAnimatedImage)
20+import Yoga.React.Native.MacOS.Events as E
21+import Yoga.React.Native.MacOS.Image (nativeImage)
22+import Yoga.React.Native.MacOS.LevelIndicator (nativeLevelIndicator)
23+import Yoga.React.Native.MacOS.PathControl (nativePathControl)
24+import Yoga.React.Native.MacOS.Progress (nativeProgress)
25+import Yoga.React.Native.MacOS.Separator (nativeSeparator)
26+import Yoga.React.Native.MacOS.Slider (nativeSlider)
27+import Yoga.React.Native.MacOS.Types as T
28+import Yoga.React.Native.MacOS.VideoPlayer (nativeVideoPlayer)
29+import Yoga.React.Native.Style as Style
30+
31+imageDemo :: DemoProps -> JSX
32+imageDemo = component "ImageDemo" \dp -> pure do
33+ scrollWrap dp
34+ [ sectionTitle dp.fg "Image"
35+ , desc dp "Native NSImageView with URL loading and corner radius"
36+ , nativeImage
37+ { source: "https://placedog.net/640/400"
38+ , contentMode: T.scaleProportionally
39+ , cornerRadius: 12.0
40+ , style: Style.style { height: 200.0 } <> tw "mb-2"
41+ }
42+ ]
43+
44+animatedImageDemo :: DemoProps -> JSX
45+animatedImageDemo = component "AnimatedImageDemo" \dp -> pure do
46+ scrollWrap dp
47+ [ sectionTitle dp.fg "Animated Image"
48+ , desc dp "Native NSImageView with animated GIF support"
49+ , nativeAnimatedImage
50+ { source: "https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif"
51+ , animating: true
52+ , style: Style.style { height: 200.0 } <> tw "rounded-lg overflow-hidden mb-2"
53+ }
54+ , nativeAnimatedImage
55+ { source: "https://media.giphy.com/media/13CoXDiaCcCoyk/giphy.gif"
56+ , animating: true
57+ , style: Style.style { height: 200.0 } <> tw "rounded-lg overflow-hidden mb-2"
58+ }
59+ ]
60+
61+videoPlayerDemo :: DemoProps -> JSX
62+videoPlayerDemo = component "VideoPlayerDemo" \_ -> pure do
63+ view { style: tw "flex-1" }
64+ [ nativeVideoPlayer
65+ { source: "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/720/Big_Buck_Bunny_720_10s_1MB.mp4"
66+ , playing: true
67+ , looping: true
68+ , muted: false
69+ , controlsStyle: T.floatingControls
70+ , style: tw "flex-1"
71+ }
72+ ]
73+
74+levelIndicatorDemo :: DemoProps -> JSX
75+levelIndicatorDemo = component "LevelIndicatorDemo" \dp -> React.do
76+ value /\ setValue <- useState' 50.0
77+ pure do
78+ scrollWrap dp
79+ [ sectionTitle dp.fg "Level Indicator"
80+ , card dp.cardBg
81+ [ nativeSlider
82+ { value
83+ , minValue: 0.0
84+ , maxValue: 100.0
85+ , numberOfTickMarks: 11
86+ , onChange: E.onNumber "value" setValue
87+ , style: Style.style { height: 24.0 }
88+ }
89+ , nativeLevelIndicator
90+ { value
91+ , minValue: 0.0
92+ , maxValue: 100.0
93+ , warningValue: 70.0
94+ , criticalValue: 90.0
95+ , style: Style.style { height: 18.0, marginTop: 8.0 }
96+ }
97+ ]
98+ ]
99+
100+progressDemo :: DemoProps -> JSX
101+progressDemo = component "ProgressDemo" \dp -> React.do
102+ value /\ setValue <- useState' 50.0
103+ pure do
104+ scrollWrap dp
105+ [ sectionTitle dp.fg "Progress"
106+ , card dp.cardBg
107+ [ nativeSlider
108+ { value
109+ , minValue: 0.0
110+ , maxValue: 100.0
111+ , numberOfTickMarks: 11
112+ , onChange: E.onNumber "value" setValue
113+ , style: Style.style { height: 24.0 }
114+ }
115+ , nativeProgress
116+ { value
117+ , style: Style.style { height: 18.0, marginTop: 8.0 }
118+ }
119+ ]
120+ ]
121+
122+separatorDemo :: DemoProps -> JSX
123+separatorDemo = component "SeparatorDemo" \dp -> pure do
124+ scrollWrap dp
125+ [ sectionTitle dp.fg "Separator"
126+ , desc dp "Horizontal and vertical separators"
127+ , nativeSeparator
128+ { vertical: false
129+ , style: Style.style { height: 2.0 } <> tw "my-2"
130+ }
131+ , text { style: tw "text-xs mb-2" <> Style.style { color: dp.dimFg } } "Content between separators"
132+ , nativeSeparator
133+ { vertical: false
134+ , style: Style.style { height: 2.0 } <> tw "my-2"
135+ }
136+ ]
137+
138+pathControlDemo :: DemoProps -> JSX
139+pathControlDemo = component "PathControlDemo" \dp -> pure do
140+ scrollWrap dp
141+ [ sectionTitle dp.fg "Path Control"
142+ , nativePathControl
143+ { url: "/Users"
144+ , pathStyle: T.standardPath
145+ , onSelectPath: E.onString "url" \_ -> pure unit
146+ , style: Style.style { height: 24.0 } <> tw "mb-2"
147+ }
148+ ]
example-macos/src/Demo/DragDropFiles.pursnew file+106
@@ -0,0 +1,106 @@
1+module Demo.DragDropFiles (dropZoneDemo, filePickerDemo) where
2+
3+import Prelude
4+
5+import Demo.Shared (DemoProps, card, desc, getFieldJSON, label, scrollWrap, sectionTitle)
6+import React.Basic (JSX)
7+import React.Basic.Events (handler, handler_, unsafeEventFn)
8+import React.Basic.Hooks (useState', (/\))
9+import React.Basic.Hooks as React
10+import Yoga.React (component)
11+import Yoga.React.Native (nativeEvent, text, tw, view)
12+import Yoga.React.Native.MacOS.FilePicker (nativeFilePicker)
13+import Yoga.React.Native.MacOS.Types as T
14+import Yoga.React.Native.Style as Style
15+
16+dropZoneDemo :: DemoProps -> JSX
17+dropZoneDemo = component "DropZoneDemo" \dp -> React.do
18+ status /\ setStatus <- useState' "Drop files here"
19+ dropped /\ setDropped <- useState' ""
20+ dragging /\ setDragging <- useState' false
21+ pure do
22+ let accentBorder = if dragging then "#007AFF" else dp.dimFg
23+ scrollWrap dp
24+ [ sectionTitle dp.fg "Drop Zone"
25+ , desc dp "Drag files from Finder into the area below"
26+ , view
27+ { draggedTypes: [ "NSFilenamesPboardType" ]
28+ , onDrop: handler
29+ (nativeEvent >>> unsafeEventFn \e -> getFieldJSON "dataTransfer" e)
30+ \r -> do
31+ setDropped r
32+ setStatus "Drop files here"
33+ setDragging false
34+ , onDragEnter: handler_ do
35+ setStatus "Release to drop!"
36+ setDragging true
37+ , onDragLeave: handler_ do
38+ setStatus "Drop files here"
39+ setDragging false
40+ , style: tw "rounded-lg items-center justify-center mb-2"
41+ <> Style.style
42+ { minHeight: 100.0
43+ , borderWidth: 2.0
44+ , borderColor: accentBorder
45+ , backgroundColor: dp.cardBg
46+ }
47+ }
48+ [ text { style: tw "text-sm" <> Style.style { color: dp.fg } } status
49+ , if dropped == "" then mempty
50+ else text { style: tw "text-xs mt-2 px-4" <> Style.style { color: dp.dimFg } }
51+ ("Dropped: " <> dropped)
52+ ]
53+ ]
54+
55+filePickerDemo :: DemoProps -> JSX
56+filePickerDemo = component "FilePickerDemo" \dp -> React.do
57+ picked /\ setPicked <- useState' ""
58+ pure do
59+ scrollWrap dp
60+ [ sectionTitle dp.fg "File Picker"
61+ , desc dp "Click buttons to open native file panels"
62+ , card dp.cardBg
63+ [ view { style: tw "flex-row items-center" }
64+ [ nativeFilePicker
65+ { mode: T.openFile
66+ , title: "Open Files"
67+ , sfSymbol: "doc.badge.plus"
68+ , allowMultiple: true
69+ , allowedTypes: [ "public.image", "public.text" ]
70+ , message: "Select files to open"
71+ , onPickFiles: handler
72+ (nativeEvent >>> unsafeEventFn \e -> getFieldJSON "files" e)
73+ setPicked
74+ , onCancel: handler_ (setPicked "Cancelled")
75+ , style: Style.style { height: 24.0, width: 120.0 }
76+ }
77+ , nativeFilePicker
78+ { mode: T.openFile
79+ , title: "Choose Folder"
80+ , sfSymbol: "folder"
81+ , canChooseDirectories: true
82+ , message: "Select a folder"
83+ , onPickFiles: handler
84+ (nativeEvent >>> unsafeEventFn \e -> getFieldJSON "files" e)
85+ setPicked
86+ , onCancel: handler_ (setPicked "Cancelled")
87+ , style: Style.style { height: 24.0, width: 140.0, marginLeft: 8.0 }
88+ }
89+ , nativeFilePicker
90+ { mode: T.saveFile
91+ , title: "Save As..."
92+ , sfSymbol: "square.and.arrow.down"
93+ , defaultName: "Untitled.txt"
94+ , allowedTypes: [ "public.plain-text" ]
95+ , message: "Choose save location"
96+ , onPickFiles: handler
97+ (nativeEvent >>> unsafeEventFn \e -> getFieldJSON "files" e)
98+ setPicked
99+ , onCancel: handler_ (setPicked "Cancelled")
100+ , style: Style.style { height: 24.0, width: 120.0, marginLeft: 8.0 }
101+ }
102+ ]
103+ , if picked == "" then mempty
104+ else label dp.dimFg picked
105+ ]
106+ ]
example-macos/src/Demo/InputControls.pursnew file+321
@@ -0,0 +1,321 @@
1+module Demo.InputControls
2+ ( buttonDemo
3+ , switchDemo
4+ , sliderDemo
5+ , popUpDemo
6+ , comboBoxDemo
7+ , stepperDemo
8+ , datePickerDemo
9+ , colorWellDemo
10+ , checkboxDemo
11+ , radioButtonDemo
12+ , searchFieldDemo
13+ , tokenFieldDemo
14+ ) where
15+
16+import Prelude
17+
18+import Demo.Shared (DemoProps, card, desc, label, round, scrollWrap, sectionTitle)
19+import React.Basic (JSX)
20+import React.Basic.Events (handler, handler_, unsafeEventFn)
21+import React.Basic.Hooks (useState', (/\))
22+import React.Basic.Hooks as React
23+import Yoga.React (component)
24+import Yoga.React.Native (nativeEvent, text, tw, view)
25+import Yoga.React.Native.MacOS.Button (nativeButton)
26+import Yoga.React.Native.MacOS.Checkbox (nativeCheckbox)
27+import Yoga.React.Native.MacOS.ColorWell (nativeColorWell)
28+import Yoga.React.Native.MacOS.ComboBox (nativeComboBox)
29+import Yoga.React.Native.MacOS.DatePicker (nativeDatePicker)
30+import Yoga.React.Native.MacOS.Events as E
31+import Yoga.React.Native.MacOS.LevelIndicator (nativeLevelIndicator)
32+import Yoga.React.Native.MacOS.PopUp (nativePopUp)
33+import Yoga.React.Native.MacOS.Progress (nativeProgress)
34+import Yoga.React.Native.MacOS.RadioButton (nativeRadioButton)
35+import Yoga.React.Native.MacOS.SearchField (nativeSearchField)
36+import Yoga.React.Native.MacOS.Slider (nativeSlider)
37+import Yoga.React.Native.MacOS.Stepper (nativeStepper)
38+import Yoga.React.Native.MacOS.Switch (nativeSwitch)
39+import Yoga.React.Native.MacOS.TokenField (nativeTokenField)
40+import Yoga.React.Native.MacOS.Types as T
41+import Yoga.React.Native.Style as Style
42+
43+buttonDemo :: DemoProps -> JSX
44+buttonDemo = component "ButtonDemo" \dp -> React.do
45+ status /\ setStatus <- useState' "Ready"
46+ pure do
47+ scrollWrap dp
48+ [ sectionTitle dp.fg "Button"
49+ , card dp.cardBg
50+ [ view { style: tw "flex-row items-center" }
51+ [ nativeButton
52+ { title: "Say Hello"
53+ , sfSymbol: "hand.wave"
54+ , bezelStyle: T.push
55+ , primary: true
56+ , onPress: handler_ (setStatus "Hello from PureScript!")
57+ , style: Style.style { height: 24.0, width: 140.0 }
58+ }
59+ , nativeButton
60+ { title: "Reset"
61+ , sfSymbol: "arrow.counterclockwise"
62+ , bezelStyle: T.push
63+ , onPress: handler_ (setStatus "Ready")
64+ , style: Style.style { height: 24.0, width: 100.0, marginLeft: 8.0 }
65+ }
66+ , label dp.dimFg status
67+ ]
68+ ]
69+ ]
70+
71+switchDemo :: DemoProps -> JSX
72+switchDemo = component "SwitchDemo" \dp -> React.do
73+ on /\ setOn <- useState' false
74+ pure do
75+ scrollWrap dp
76+ [ sectionTitle dp.fg "Switch"
77+ , card dp.cardBg
78+ [ view { style: tw "flex-row items-center" }
79+ [ nativeSwitch
80+ { on
81+ , onChange: E.onBool "on" setOn
82+ , style: Style.style { height: 24.0, width: 48.0 }
83+ }
84+ , label dp.dimFg (if on then "On" else "Off")
85+ ]
86+ ]
87+ ]
88+
89+sliderDemo :: DemoProps -> JSX
90+sliderDemo = component "SliderDemo" \dp -> React.do
91+ value /\ setValue <- useState' 50.0
92+ pure do
93+ scrollWrap dp
94+ [ sectionTitle dp.fg "Slider + Level Indicator + Progress"
95+ , card dp.cardBg
96+ [ nativeSlider
97+ { value
98+ , minValue: 0.0
99+ , maxValue: 100.0
100+ , numberOfTickMarks: 11
101+ , onChange: E.onNumber "value" setValue
102+ , style: Style.style { height: 24.0 }
103+ }
104+ , label dp.dimFg ("Value: " <> show (round value) <> " / 100")
105+ , nativeLevelIndicator
106+ { value
107+ , minValue: 0.0
108+ , maxValue: 100.0
109+ , warningValue: 70.0
110+ , criticalValue: 90.0
111+ , style: Style.style { height: 18.0, marginTop: 8.0 }
112+ }
113+ , nativeProgress
114+ { value
115+ , style: Style.style { height: 18.0, marginTop: 8.0 }
116+ }
117+ ]
118+ ]
119+
120+popUpDemo :: DemoProps -> JSX
121+popUpDemo = component "PopUpDemo" \dp -> React.do
122+ idx /\ setIdx <- useState' 0
123+ title /\ setTitle <- useState' "Small"
124+ pure do
125+ scrollWrap dp
126+ [ sectionTitle dp.fg "Pop Up Button"
127+ , card dp.cardBg
128+ [ view { style: tw "flex-row items-center" }
129+ [ nativePopUp
130+ { items: [ "Small", "Medium", "Large", "Extra Large" ]
131+ , selectedIndex: idx
132+ , onChange: handler
133+ ( nativeEvent >>> unsafeEventFn \e ->
134+ { idx: E.getFieldInt "selectedIndex" e, title: E.getFieldStr "title" e }
135+ )
136+ \r -> do
137+ setIdx r.idx
138+ setTitle r.title
139+ , style: Style.style { height: 24.0, width: 160.0 }
140+ }
141+ , label dp.dimFg ("Selected: " <> title)
142+ ]
143+ ]
144+ ]
145+
146+comboBoxDemo :: DemoProps -> JSX
147+comboBoxDemo = component "ComboBoxDemo" \dp -> React.do
148+ txt /\ setTxt <- useState' ""
149+ result /\ setResult <- useState' ""
150+ pure do
151+ scrollWrap dp
152+ [ sectionTitle dp.fg "Combo Box"
153+ , desc dp "Editable dropdown with autocomplete"
154+ , card dp.cardBg
155+ [ nativeComboBox
156+ { items: [ "Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig", "Grape" ]
157+ , text: txt
158+ , placeholder: "Type a fruit..."
159+ , onChangeText: E.onString "text" setTxt
160+ , onSelectItem: E.onString "text" \t -> do
161+ setTxt t
162+ setResult ("Selected: " <> t)
163+ , style: Style.style { height: 28.0 } <> tw "mb-2"
164+ }
165+ , if result == "" then mempty
166+ else label dp.dimFg result
167+ ]
168+ ]
169+
170+stepperDemo :: DemoProps -> JSX
171+stepperDemo = component "StepperDemo" \dp -> React.do
172+ value /\ setValue <- useState' 5.0
173+ pure do
174+ scrollWrap dp
175+ [ sectionTitle dp.fg "Stepper"
176+ , desc dp "Up/down increment control"
177+ , card dp.cardBg
178+ [ view { style: tw "flex-row items-center" }
179+ [ text { style: tw "text-sm mr-3" <> Style.style { color: dp.fg } } "Quantity:"
180+ , nativeStepper
181+ { value
182+ , minValue: 0.0
183+ , maxValue: 50.0
184+ , increment: 1.0
185+ , onChange: E.onNumber "value" setValue
186+ , style: Style.style { width: 100.0, height: 24.0 }
187+ }
188+ ]
189+ ]
190+ ]
191+
192+datePickerDemo :: DemoProps -> JSX
193+datePickerDemo = component "DatePickerDemo" \dp -> React.do
194+ dateText /\ setDateText <- useState' ""
195+ pure do
196+ scrollWrap dp
197+ [ sectionTitle dp.fg "Date Picker"
198+ , card dp.cardBg
199+ [ nativeDatePicker
200+ { graphical: false
201+ , onChange: E.onString "date" setDateText
202+ , style: Style.style { height: 24.0, width: 200.0 }
203+ }
204+ , if dateText == "" then mempty
205+ else label dp.dimFg ("Picked: " <> dateText)
206+ ]
207+ ]
208+
209+colorWellDemo :: DemoProps -> JSX
210+colorWellDemo = component "ColorWellDemo" \dp -> React.do
211+ color /\ setColor <- useState' "#FF6600"
212+ pure do
213+ scrollWrap dp
214+ [ sectionTitle dp.fg "Color Well"
215+ , card dp.cardBg
216+ [ view { style: tw "flex-row items-center" }
217+ [ nativeColorWell
218+ { color
219+ , minimal: true
220+ , onChange: E.onString "color" setColor
221+ , style: Style.style { height: 32.0, width: 48.0 }
222+ }
223+ , view
224+ { style: tw "ml-3 rounded" <> Style.style
225+ { width: 24.0, height: 24.0, backgroundColor: color }
226+ }
227+ []
228+ , label dp.dimFg color
229+ ]
230+ ]
231+ ]
232+
233+checkboxDemo :: DemoProps -> JSX
234+checkboxDemo = component "CheckboxDemo" \dp -> React.do
235+ a /\ setA <- useState' true
236+ b /\ setB <- useState' false
237+ pure do
238+ scrollWrap dp
239+ [ sectionTitle dp.fg "Checkbox"
240+ , card dp.cardBg
241+ [ nativeCheckbox
242+ { checked: a
243+ , title: "Enable notifications"
244+ , enabled: true
245+ , onChange: E.onBool "checked" setA
246+ , style: Style.style { height: 24.0 } <> tw "mb-1"
247+ }
248+ , nativeCheckbox
249+ { checked: b
250+ , title: "Dark mode"
251+ , enabled: true
252+ , onChange: E.onBool "checked" setB
253+ , style: Style.style { height: 24.0 }
254+ }
255+ ]
256+ ]
257+
258+radioButtonDemo :: DemoProps -> JSX
259+radioButtonDemo = component "RadioButtonDemo" \dp -> React.do
260+ choice /\ setChoice <- useState' "option1"
261+ pure do
262+ scrollWrap dp
263+ [ sectionTitle dp.fg "Radio Button"
264+ , card dp.cardBg
265+ [ nativeRadioButton
266+ { selected: choice == "option1"
267+ , title: "Small"
268+ , enabled: true
269+ , onChange: handler_ (setChoice "option1")
270+ , style: Style.style { height: 24.0 } <> tw "mb-1"
271+ }
272+ , nativeRadioButton
273+ { selected: choice == "option2"
274+ , title: "Medium"
275+ , enabled: true
276+ , onChange: handler_ (setChoice "option2")
277+ , style: Style.style { height: 24.0 } <> tw "mb-1"
278+ }
279+ , nativeRadioButton
280+ { selected: choice == "option3"
281+ , title: "Large"
282+ , enabled: true
283+ , onChange: handler_ (setChoice "option3")
284+ , style: Style.style { height: 24.0 }
285+ }
286+ , label dp.dimFg ("Selected: " <> choice)
287+ ]
288+ ]
289+
290+searchFieldDemo :: DemoProps -> JSX
291+searchFieldDemo = component "SearchFieldDemo" \dp -> React.do
292+ query /\ setQuery <- useState' ""
293+ result /\ setResult <- useState' ""
294+ pure do
295+ scrollWrap dp
296+ [ sectionTitle dp.fg "Search Field"
297+ , nativeSearchField
298+ { text: query
299+ , placeholder: "Search..."
300+ , onChangeText: E.onString "text" setQuery
301+ , onSearch: E.onString "text" \t -> setResult ("Searched: " <> t)
302+ , style: Style.style { height: 28.0 } <> tw "mb-2"
303+ }
304+ , if result == "" then mempty
305+ else label dp.dimFg result
306+ ]
307+
308+tokenFieldDemo :: DemoProps -> JSX
309+tokenFieldDemo = component "TokenFieldDemo" \dp -> React.do
310+ tokens /\ setTokens <- useState' ([ "PureScript", "React", "macOS" ] :: Array String)
311+ pure do
312+ scrollWrap dp
313+ [ sectionTitle dp.fg "Token Field"
314+ , desc dp "Type text and press Enter to add tokens"
315+ , nativeTokenField
316+ { tokens
317+ , placeholder: "Add tags..."
318+ , onChangeTokens: E.onStrings "tokens" setTokens
319+ , style: Style.style { height: 28.0 } <> tw "mb-2"
320+ }
321+ ]
example-macos/src/Demo/Layout.pursnew file+71
@@ -0,0 +1,71 @@
1+module Demo.Layout (boxDemo, splitViewDemo, tabViewDemo) where
2+
3+import Prelude
4+
5+import Demo.Shared (DemoProps, desc, scrollWrap, sectionTitle)
6+import React.Basic (JSX)
7+import React.Basic.Hooks (useState', (/\))
8+import React.Basic.Hooks as React
9+import Yoga.React (component)
10+import Yoga.React.Native (text, tw, view)
11+import Yoga.React.Native.MacOS.Box (nativeBox)
12+import Yoga.React.Native.MacOS.Events as E
13+import Yoga.React.Native.MacOS.SplitView (nativeSplitView)
14+import Yoga.React.Native.MacOS.TabView (nativeTabView)
15+import Yoga.React.Native.Style as Style
16+
17+boxDemo :: DemoProps -> JSX
18+boxDemo = component "BoxDemo" \dp -> pure do
19+ scrollWrap dp
20+ [ sectionTitle dp.fg "Box"
21+ , desc dp "Native NSBox with title and border"
22+ , nativeBox
23+ { boxTitle: "Settings"
24+ , fillColor2: dp.cardBg
25+ , borderColor2: dp.dimFg
26+ , radius: 8.0
27+ , style: Style.style { height: 100.0 } <> tw "mb-2"
28+ }
29+ [ view { style: tw "flex-row items-center p-2" }
30+ [ text { style: tw "text-sm" <> Style.style { color: dp.fg } } "Content inside a native box" ]
31+ ]
32+ ]
33+
34+splitViewDemo :: DemoProps -> JSX
35+splitViewDemo = component "SplitViewDemo" \dp -> pure do
36+ scrollWrap dp
37+ [ sectionTitle dp.fg "Split View"
38+ , desc dp "Native NSSplitView with resizable divider"
39+ , nativeSplitView
40+ { isVertical: true
41+ , style: Style.style { height: 150.0 } <> tw "rounded-lg overflow-hidden mb-2"
42+ }
43+ [ view { style: tw "flex-1 p-4" <> Style.style { backgroundColor: dp.cardBg } }
44+ [ text { style: tw "text-sm font-semibold" <> Style.style { color: dp.fg } } "Left Pane"
45+ , text { style: tw "text-xs mt-1" <> Style.style { color: dp.dimFg } } "Drag the divider to resize"
46+ ]
47+ , view { style: tw "flex-1 p-4" <> Style.style { backgroundColor: dp.cardBg } }
48+ [ text { style: tw "text-sm font-semibold" <> Style.style { color: dp.fg } } "Right Pane"
49+ , text { style: tw "text-xs mt-1" <> Style.style { color: dp.dimFg } } "Content fills available space"
50+ ]
51+ ]
52+ ]
53+
54+tabViewDemo :: DemoProps -> JSX
55+tabViewDemo = component "TabViewDemo" \dp -> React.do
56+ selected /\ setSelected <- useState' "general"
57+ pure do
58+ scrollWrap dp
59+ [ sectionTitle dp.fg "Tab View"
60+ , desc dp "Native segmented tab bar"
61+ , nativeTabView
62+ { items:
63+ [ { id: "general", label: "General" }
64+ , { id: "advanced", label: "Advanced" }
65+ , { id: "about", label: "About" }
66+ ]
67+ , selectedItem: selected
68+ , onSelectTab: E.onString "tabId" setSelected
69+ , style: Style.style { height: 32.0 } <> tw "mb-2"
70+ }
71+ ]
example-macos/src/Demo/MapsDocs.pursnew file+63
@@ -0,0 +1,63 @@
1+module Demo.MapsDocs (mapViewDemo, pdfViewDemo) where
2+
3+import Prelude
4+
5+import Demo.Shared (DemoProps)
6+import React.Basic (JSX)
7+import React.Basic.Hooks (useState', (/\))
8+import React.Basic.Hooks as React
9+import Yoga.React (component)
10+import Yoga.React.Native (text, tw, view)
11+import Yoga.React.Native.MacOS.Events as E
12+import Yoga.React.Native.MacOS.MapView (nativeMapView)
13+import Yoga.React.Native.MacOS.PDFView (nativePDFView)
14+import Yoga.React.Native.MacOS.Segmented (nativeSegmented)
15+import Yoga.React.Native.MacOS.Types as T
16+import Yoga.React.Native.Style as Style
17+
18+mapViewDemo :: DemoProps -> JSX
19+mapViewDemo = component "MapViewDemo" \dp -> React.do
20+ mapType /\ setMapType <- useState' 0
21+ let
22+ mt = case mapType of
23+ 1 -> T.satelliteMap
24+ 2 -> T.hybridMap
25+ _ -> T.standardMap
26+ pure do
27+ view { style: tw "flex-1" }
28+ [ view { style: tw "flex-row items-center px-3 py-2" <> Style.style { backgroundColor: "transparent" } }
29+ [ text { style: tw "text-sm font-semibold mr-3" <> Style.style { color: dp.fg } } "Map View"
30+ , nativeSegmented
31+ { labels: [ "Standard", "Satellite", "Hybrid" ]
32+ , selectedIndex: mapType
33+ , onChange: E.onInt "selectedIndex" setMapType
34+ , style: Style.style { width: 240.0, height: 24.0 }
35+ }
36+ ]
37+ , nativeMapView
38+ { latitude: 37.7749
39+ , longitude: -122.4194
40+ , latitudeDelta: 0.05
41+ , longitudeDelta: 0.05
42+ , mapType: mt
43+ , showsUserLocation: false
44+ , annotations:
45+ [ { latitude: 37.7749, longitude: -122.4194, title: "San Francisco", subtitle: "California" }
46+ , { latitude: 37.8199, longitude: -122.4783, title: "Golden Gate Bridge", subtitle: "" }
47+ ]
48+ , style: tw "flex-1"
49+ }
50+ ]
51+
52+pdfViewDemo :: DemoProps -> JSX
53+pdfViewDemo = component "PDFViewDemo" \dp -> pure do
54+ view { style: tw "flex-1" }
55+ [ view { style: tw "flex-row items-center px-3 py-2" <> Style.style { backgroundColor: "transparent" } }
56+ [ text { style: tw "text-sm font-semibold" <> Style.style { color: dp.fg } } "PDF Viewer" ]
57+ , nativePDFView
58+ { source: "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
59+ , autoScales: true
60+ , displayMode: T.singlePageContinuous
61+ , style: tw "flex-1"
62+ }
63+ ]
example-macos/src/Demo/Navigation.pursnew file+207
@@ -0,0 +1,207 @@
1+module Demo.Navigation (outlineSidebar, demoContent) where
2+
3+import Prelude
4+
5+import Data.Array (concatMap, null)
6+import Data.String (toLower, contains, Pattern(..))
7+import Demo.AiMl (cameraDemo, naturalLanguageDemo, ocrDemo, speechRecognitionDemo)
8+import Demo.Animation (riveDemo)
9+import Demo.Chat (chatDemo)
10+import Demo.DataViews (outlineViewDemo, tableViewDemo)
11+import Demo.Display (animatedImageDemo, imageDemo, levelIndicatorDemo, pathControlDemo, progressDemo, separatorDemo, videoPlayerDemo)
12+import Demo.DragDropFiles (dropZoneDemo, filePickerDemo)
13+import Demo.InputControls (buttonDemo, checkboxDemo, colorWellDemo, comboBoxDemo, datePickerDemo, popUpDemo, radioButtonDemo, searchFieldDemo, sliderDemo, stepperDemo, switchDemo, tokenFieldDemo)
14+import Demo.Layout (boxDemo, splitViewDemo, tabViewDemo)
15+import Demo.MapsDocs (mapViewDemo, pdfViewDemo)
16+import Demo.Overlays (alertDemo, contextMenuDemo, menuDemo, popoverDemo, sheetDemo)
17+import Demo.Shared (DemoProps)
18+import Demo.SystemServices (clipboardDemo, colorPanelDemo, fontPanelDemo, notificationsDemo, quickLookDemo, shareDemo, soundDemo, speechDemo, statusBarDemo)
19+import Demo.TextEditing (textEditorDemo, textFieldDemo)
20+import Demo.WebBrowser (webViewDemo)
21+import Effect (Effect)
22+import React.Basic (JSX)
23+import React.Basic.Hooks (useState', (/\))
24+import React.Basic.Hooks as React
25+import Yoga.React (component)
26+import Yoga.React.Native (text, tw, view)
27+import Yoga.React.Native.MacOS.Events as E
28+import Yoga.React.Native.MacOS.OutlineView (OutlineItem(..), nativeOutlineView)
29+import Yoga.React.Native.MacOS.SearchField (nativeSearchField)
30+import Yoga.React.Native.Style as Style
31+
32+outlineTree :: Array OutlineItem
33+outlineTree = do
34+ let leaf id title symbol = OutlineItem { id, title, sfSymbol: symbol, children: [] }
35+ let folder id title symbol children = OutlineItem { id, title, sfSymbol: symbol, children }
36+ [ folder "input" "Input Controls" "slider.horizontal.3"
37+ [ leaf "button" "Button" "button.horizontal"
38+ , leaf "switch" "Switch" "switch.2"
39+ , leaf "slider" "Slider" "slider.horizontal.below.rectangle"
40+ , leaf "popup" "Pop Up" "chevron.up.chevron.down"
41+ , leaf "combobox" "Combo Box" "rectangle.and.pencil.and.ellipsis"
42+ , leaf "stepper" "Stepper" "plus.forwardslash.minus"
43+ , leaf "datepicker" "Date Picker" "calendar"
44+ , leaf "colorwell" "Color Well" "paintpalette"
45+ , leaf "checkbox" "Checkbox" "checkmark.square"
46+ , leaf "radiobutton" "Radio Button" "circle.inset.filled"
47+ , leaf "searchfield" "Search Field" "magnifyingglass"
48+ , leaf "tokenfield" "Token Field" "tag"
49+ ]
50+ , folder "text" "Text & Editing" "doc.richtext"
51+ [ leaf "textfield" "Text Field" "character.cursor.ibeam"
52+ , leaf "texteditor" "Text Editor" "doc.richtext"
53+ ]
54+ , folder "display" "Display" "photo"
55+ [ leaf "image" "Image" "photo"
56+ , leaf "animatedimage" "Animated Image" "photo.on.rectangle.angled"
57+ , leaf "videoplayer" "Video Player" "play.rectangle"
58+ , leaf "levelindicator" "Level Indicator" "gauge.with.dots.needle.33percent"
59+ , leaf "progress" "Progress" "chart.bar.fill"
60+ , leaf "separator" "Separator" "minus"
61+ , leaf "pathcontrol" "Path Control" "chevron.compact.right"
62+ ]
63+ , folder "layout" "Layout & Containers" "rectangle.split.3x1"
64+ [ leaf "box" "Box" "square.dashed"
65+ , leaf "splitview" "Split View" "rectangle.split.2x1"
66+ , leaf "tabview" "Tab View" "rectangle.topthird.inset.filled"
67+ ]
68+ , folder "overlays" "Overlays & Dialogs" "rectangle.on.rectangle"
69+ [ leaf "alert" "Alert" "exclamationmark.triangle"
70+ , leaf "sheet" "Sheet" "rectangle.bottomhalf.inset.filled"
71+ , leaf "popover" "Popover" "text.bubble"
72+ , leaf "contextmenu" "Context Menu" "contextualmenu.and.cursorarrow"
73+ , leaf "menu" "Menu" "list.bullet"
74+ ]
75+ , folder "dataviews" "Data Views" "tablecells"
76+ [ leaf "tableview" "Table View" "tablecells"
77+ , leaf "outlineview" "Outline View" "list.bullet.indent"
78+ ]
79+ , folder "dragdrop" "Drag & Drop / Files" "doc.badge.plus"
80+ [ leaf "dropzone" "Drop Zone" "square.and.arrow.down"
81+ , leaf "filepicker" "File Picker" "folder"
82+ ]
83+ , folder "animation" "Animation" "play.circle"
84+ [ leaf "rive" "Rive" "play.circle"
85+ ]
86+ , folder "system" "System Services" "gearshape.2"
87+ [ leaf "clipboard" "Clipboard" "doc.on.clipboard"
88+ , leaf "share" "Share" "square.and.arrow.up"
89+ , leaf "notifications" "Notifications" "bell"
90+ , leaf "sound" "Sound" "speaker.wave.2"
91+ , leaf "statusbar" "Status Bar" "menubar.rectangle"
92+ , leaf "quicklook" "Quick Look" "eye"
93+ , leaf "colorpanel" "Color Panel" "paintpalette"
94+ , leaf "fontpanel" "Font Panel" "textformat"
95+ , leaf "speech" "Speech Synthesis" "speaker.wave.2"
96+ ]
97+ , folder "aiml" "AI & ML" "brain"
98+ [ leaf "ocr" "OCR" "doc.text.viewfinder"
99+ , leaf "speechrecognition" "Speech Recognition" "mic"
100+ , leaf "naturallanguage" "Natural Language" "text.magnifyingglass"
101+ , leaf "camera" "Camera" "video"
102+ ]
103+ , folder "mapsdocs" "Maps & Documents" "map"
104+ [ leaf "mapview" "Map View" "map"
105+ , leaf "pdfview" "PDF View" "doc.richtext"
106+ ]
107+ , folder "webbrowser" "Web Browser" "globe"
108+ [ leaf "webview" "Web View" "globe"
109+ ]
110+ , folder "chat" "Chat" "bubble.left.and.bubble.right"
111+ [ leaf "matrix" "Matrix" "bubble.left.and.bubble.right"
112+ ]
113+ ]
114+
115+filterTree :: String -> Array OutlineItem -> Array OutlineItem
116+filterTree query items = do
117+ let q = toLower query
118+ let matchesLeaf (OutlineItem r) = contains (Pattern q) (toLower r.title)
119+ let
120+ go (OutlineItem r) = case r.children of
121+ [] -> if matchesLeaf (OutlineItem r) then [ OutlineItem r ] else []
122+ kids -> do
123+ let matched = concatMap go kids
124+ if null matched then [] else [ OutlineItem (r { children = matched }) ]
125+ concatMap go items
126+
127+outlineSidebar :: String -> (String -> Effect Unit) -> JSX
128+outlineSidebar _ setSelectedItem = sidebarComponent { setSelectedItem }
129+ where
130+ sidebarComponent = component "OutlineSidebar" \props -> React.do
131+ search /\ setSearch <- useState' ""
132+ let items = if search == "" then outlineTree else filterTree search outlineTree
133+ pure do
134+ view { style: tw "flex-1 pt-2" }
135+ [ nativeSearchField
136+ { text: search
137+ , placeholder: "Filter..."
138+ , onChangeText: E.onString "text" setSearch
139+ , style: Style.style { height: 24.0, marginLeft: 8.0, marginRight: 8.0, marginBottom: 4.0 }
140+ }
141+ , nativeOutlineView
142+ { items
143+ , headerVisible: false
144+ , onSelectItem: E.onString "id" props.setSelectedItem
145+ , style: tw "flex-1"
146+ }
147+ ]
148+
149+demoContent :: DemoProps -> String -> JSX
150+demoContent dp = case _ of
151+ "button" -> buttonDemo dp
152+ "switch" -> switchDemo dp
153+ "slider" -> sliderDemo dp
154+ "popup" -> popUpDemo dp
155+ "combobox" -> comboBoxDemo dp
156+ "stepper" -> stepperDemo dp
157+ "datepicker" -> datePickerDemo dp
158+ "colorwell" -> colorWellDemo dp
159+ "checkbox" -> checkboxDemo dp
160+ "radiobutton" -> radioButtonDemo dp
161+ "searchfield" -> searchFieldDemo dp
162+ "tokenfield" -> tokenFieldDemo dp
163+ "textfield" -> textFieldDemo dp
164+ "texteditor" -> textEditorDemo dp
165+ "image" -> imageDemo dp
166+ "animatedimage" -> animatedImageDemo dp
167+ "videoplayer" -> videoPlayerDemo dp
168+ "levelindicator" -> levelIndicatorDemo dp
169+ "progress" -> progressDemo dp
170+ "separator" -> separatorDemo dp
171+ "pathcontrol" -> pathControlDemo dp
172+ "box" -> boxDemo dp
173+ "splitview" -> splitViewDemo dp
174+ "tabview" -> tabViewDemo dp
175+ "alert" -> alertDemo dp
176+ "sheet" -> sheetDemo dp
177+ "popover" -> popoverDemo dp
178+ "contextmenu" -> contextMenuDemo dp
179+ "menu" -> menuDemo dp
180+ "tableview" -> tableViewDemo dp
181+ "outlineview" -> outlineViewDemo dp
182+ "dropzone" -> dropZoneDemo dp
183+ "filepicker" -> filePickerDemo dp
184+ "rive" -> riveDemo dp
185+ "clipboard" -> clipboardDemo dp
186+ "share" -> shareDemo dp
187+ "notifications" -> notificationsDemo dp
188+ "sound" -> soundDemo dp
189+ "statusbar" -> statusBarDemo dp
190+ "quicklook" -> quickLookDemo dp
191+ "colorpanel" -> colorPanelDemo dp
192+ "fontpanel" -> fontPanelDemo dp
193+ "speech" -> speechDemo dp
194+ "ocr" -> ocrDemo dp
195+ "speechrecognition" -> speechRecognitionDemo dp
196+ "naturallanguage" -> naturalLanguageDemo dp
197+ "camera" -> cameraDemo dp
198+ "mapview" -> mapViewDemo dp
199+ "pdfview" -> pdfViewDemo dp
200+ "webview" -> webViewDemo dp
201+ "matrix" -> chatDemo dp
202+ _ -> placeholder dp
203+
204+placeholder :: DemoProps -> JSX
205+placeholder dp =
206+ view { style: tw "flex-1 items-center justify-center" }
207+ [ text { style: tw "text-sm" <> Style.style { color: dp.dimFg } } "Select a component from the sidebar" ]
example-macos/src/Demo/Overlays.pursnew file+153
@@ -0,0 +1,153 @@
1+module Demo.Overlays
2+ ( alertDemo
3+ , sheetDemo
4+ , popoverDemo
5+ , contextMenuDemo
6+ , menuDemo
7+ ) where
8+
9+import Prelude
10+
11+import Demo.Shared (DemoProps, card, desc, label, scrollWrap, sectionTitle)
12+import React.Basic (JSX)
13+import React.Basic.Events (handler_)
14+import React.Basic.Hooks (useState', (/\))
15+import React.Basic.Hooks as React
16+import Yoga.React (component)
17+import Yoga.React.Native (text, tw, view)
18+import Yoga.React.Native.MacOS.Alert (macosAlert)
19+import Yoga.React.Native.MacOS.Button (nativeButton)
20+import Yoga.React.Native.MacOS.ContextMenu (nativeContextMenu)
21+import Yoga.React.Native.MacOS.Events as E
22+import Yoga.React.Native.MacOS.Menu (macosShowMenu)
23+import Yoga.React.Native.MacOS.Popover (nativePopover)
24+import Yoga.React.Native.MacOS.Sheet (nativeSheet)
25+import Yoga.React.Native.MacOS.Types as T
26+import Yoga.React.Native.Style as Style
27+
28+alertDemo :: DemoProps -> JSX
29+alertDemo = component "AlertDemo" \dp -> pure do
30+ scrollWrap dp
31+ [ sectionTitle dp.fg "Alert"
32+ , desc dp "Native alert dialog"
33+ , nativeButton
34+ { title: "Show Alert"
35+ , bezelStyle: T.push
36+ , onPress: handler_ (macosAlert T.warning "Are you sure?" "This action cannot be undone." [ "Cancel", "OK" ])
37+ , style: Style.style { height: 24.0, width: 120.0 } <> tw "mb-4"
38+ }
39+ ]
40+
41+sheetDemo :: DemoProps -> JSX
42+sheetDemo = component "SheetDemo" \dp -> React.do
43+ visible /\ setVisible <- useState' false
44+ pure do
45+ scrollWrap dp
46+ [ sectionTitle dp.fg "Sheet"
47+ , desc dp "Modal sheet attached to the window"
48+ , nativeButton
49+ { title: "Show Sheet"
50+ , bezelStyle: T.push
51+ , onPress: handler_ (setVisible true)
52+ , style: Style.style { height: 24.0, width: 120.0 } <> tw "mb-2"
53+ }
54+ , nativeSheet
55+ { visible
56+ , onDismiss: handler_ (setVisible false)
57+ }
58+ [ view { style: tw "p-6" <> Style.style { width: 400.0, height: 200.0 } }
59+ [ text { style: tw "text-lg font-semibold mb-2" <> Style.style { color: dp.fg } } "Sheet Content"
60+ , text { style: tw "text-sm mb-4" <> Style.style { color: dp.dimFg } } "This is a native macOS sheet."
61+ , nativeButton
62+ { title: "Dismiss"
63+ , bezelStyle: T.push
64+ , onPress: handler_ (setVisible false)
65+ , style: Style.style { height: 24.0, width: 100.0 }
66+ }
67+ ]
68+ ]
69+ ]
70+
71+popoverDemo :: DemoProps -> JSX
72+popoverDemo = component "PopoverDemo" \dp -> React.do
73+ visible /\ setVisible <- useState' false
74+ pure do
75+ scrollWrap dp
76+ [ sectionTitle dp.fg "Popover"
77+ , desc dp "Click button to toggle a popover"
78+ , nativePopover
79+ { visible
80+ , preferredEdge: T.bottom
81+ , behavior: T.transient
82+ , onClose: handler_ (setVisible false)
83+ , style: tw "mb-2"
84+ }
85+ [ nativeButton
86+ { title: if visible then "Hide Popover" else "Show Popover"
87+ , bezelStyle: T.push
88+ , onPress: handler_ (setVisible (not visible))
89+ , style: Style.style { height: 24.0, width: 140.0 }
90+ }
91+ , view { style: tw "p-4" <> Style.style { width: 200.0, height: 80.0 } }
92+ [ text { style: tw "text-sm font-semibold" <> Style.style { color: dp.fg } } "Popover Content"
93+ , text { style: tw "text-xs mt-1" <> Style.style { color: dp.dimFg } } "Click outside to dismiss"
94+ ]
95+ ]
96+ ]
97+
98+contextMenuDemo :: DemoProps -> JSX
99+contextMenuDemo = component "ContextMenuDemo" \dp -> React.do
100+ result /\ setResult <- useState' ""
101+ pure do
102+ scrollWrap dp
103+ [ sectionTitle dp.fg "Context Menu"
104+ , desc dp "Right-click the area below to open a context menu"
105+ , nativeContextMenu
106+ { items:
107+ [ { id: "cut", title: "Cut", sfSymbol: "scissors" }
108+ , { id: "copy", title: "Copy", sfSymbol: "doc.on.doc" }
109+ , { id: "paste", title: "Paste", sfSymbol: "doc.on.clipboard" }
110+ , { id: "sep", title: "-", sfSymbol: "" }
111+ , { id: "delete", title: "Delete", sfSymbol: "trash" }
112+ , { id: "selectAll", title: "Select All", sfSymbol: "selection.pin.in.out" }
113+ ]
114+ , onSelectItem: E.onString "itemId" setResult
115+ , style: Style.style {}
116+ }
117+ ( card dp.cardBg
118+ [ view { style: tw "items-center justify-center" <> Style.style { minHeight: 80.0 } }
119+ [ text { style: tw "text-sm" <> Style.style { color: dp.fg } } "Right-click me!"
120+ , if result == "" then mempty
121+ else label dp.dimFg ("Selected: " <> result)
122+ ]
123+ ]
124+ )
125+ ]
126+
127+menuDemo :: DemoProps -> JSX
128+menuDemo = component "MenuDemo" \dp -> React.do
129+ result /\ setResult <- useState' ""
130+ pure do
131+ scrollWrap dp
132+ [ sectionTitle dp.fg "Menu"
133+ , desc dp "Imperative popup menu at mouse position"
134+ , view { style: tw "flex-row items-center mb-2" }
135+ [ nativeButton
136+ { title: "Show Menu"
137+ , bezelStyle: T.push
138+ , onPress: handler_
139+ ( macosShowMenu
140+ [ { title: "New File", id: "new" }
141+ , { title: "Open...", id: "open" }
142+ , { title: "-", id: "sep" }
143+ , { title: "Save", id: "save" }
144+ , { title: "Save As...", id: "saveAs" }
145+ ]
146+ \itemId -> setResult ("Menu: " <> itemId)
147+ )
148+ , style: Style.style { height: 24.0, width: 120.0 }
149+ }
150+ , if result == "" then mempty
151+ else text { style: tw "text-xs ml-3" <> Style.style { color: dp.dimFg } } result
152+ ]
153+ ]
example-macos/src/Demo/Shared.jsnew file+6
@@ -0,0 +1,6 @@
1+export const unsafeRound = (n) => Math.round(n);
2+export const getFieldJSON = (key) => (obj) => {
3+ var v = obj[key];
4+ if (v === undefined || v === null) return "";
5+ return JSON.stringify(v);
6+};
example-macos/src/Demo/Shared.pursnew file+53
@@ -0,0 +1,53 @@
1+module Demo.Shared
2+ ( DemoProps
3+ , scrollWrap
4+ , sectionTitle
5+ , desc
6+ , card
7+ , label
8+ , round
9+ , getFieldJSON
10+ ) where
11+
12+import Prelude
13+
14+import React.Basic (JSX)
15+import Yoga.React.Native (text, tw, view)
16+import Yoga.React.Native.MacOS.ScrollView (nativeScrollView)
17+import Yoga.React.Native.Style as Style
18+
19+type DemoProps =
20+ { fg :: String
21+ , dimFg :: String
22+ , cardBg :: String
23+ , bg :: String
24+ , isDark :: Boolean
25+ }
26+
27+scrollWrap :: DemoProps -> Array JSX -> JSX
28+scrollWrap _ children =
29+ nativeScrollView { style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
30+ (view { style: tw "px-4 pb-4" } children)
31+
32+sectionTitle :: String -> String -> JSX
33+sectionTitle color title =
34+ text { style: tw "text-sm font-semibold mt-4 mb-1" <> Style.style { color } } title
35+
36+desc :: DemoProps -> String -> JSX
37+desc dp str =
38+ text { style: tw "text-xs mb-2" <> Style.style { color: dp.dimFg } } str
39+
40+card :: String -> Array JSX -> JSX
41+card bg children =
42+ view { style: tw "rounded-lg p-3 mb-2" <> Style.style { backgroundColor: bg, overflow: "hidden" } }
43+ children
44+
45+label :: String -> String -> JSX
46+label color str =
47+ text { style: tw "ml-3 text-sm" <> Style.style { color } } str
48+
49+round :: Number -> Int
50+round n = unsafeRound n
51+
52+foreign import unsafeRound :: Number -> Int
53+foreign import getFieldJSON :: String -> forall r. r -> String
example-macos/src/Demo/SystemServices.pursnew file+217
@@ -0,0 +1,217 @@
1+module Demo.SystemServices
2+ ( clipboardDemo
3+ , shareDemo
4+ , notificationsDemo
5+ , soundDemo
6+ , statusBarDemo
7+ , quickLookDemo
8+ , colorPanelDemo
9+ , fontPanelDemo
10+ , speechDemo
11+ ) where
12+
13+import Prelude
14+
15+import Demo.Shared (DemoProps, desc, scrollWrap, sectionTitle)
16+import React.Basic (JSX)
17+import React.Basic.Events (handler_)
18+import React.Basic.Hooks (useState', (/\))
19+import React.Basic.Hooks as React
20+import Yoga.React (component)
21+import Yoga.React.Native (tw, view)
22+import Yoga.React.Native.MacOS.Button (nativeButton)
23+import Yoga.React.Native.MacOS.ColorPanel (macosShowColorPanel)
24+import Yoga.React.Native.MacOS.Events as E
25+import Yoga.React.Native.MacOS.FontPanel (macosShowFontPanel)
26+import Yoga.React.Native.MacOS.Pasteboard (copyToClipboard)
27+import Yoga.React.Native.MacOS.QuickLook (macosQuickLook)
28+import Yoga.React.Native.MacOS.ShareService (macosShare)
29+import Yoga.React.Native.MacOS.Sound (macosBeep, macosPlaySound)
30+import Yoga.React.Native.MacOS.SpeechSynthesizer (say, stopSpeaking)
31+import Yoga.React.Native.MacOS.StatusBarItem (macosRemoveStatusBarItem, macosSetStatusBarItem)
32+import Yoga.React.Native.MacOS.TextField (nativeTextField)
33+import Yoga.React.Native.MacOS.Types as T
34+import Yoga.React.Native.MacOS.UserNotification (macosNotify)
35+import Yoga.React.Native.Style as Style
36+
37+clipboardDemo :: DemoProps -> JSX
38+clipboardDemo = component "ClipboardDemo" \dp -> pure do
39+ scrollWrap dp
40+ [ sectionTitle dp.fg "Clipboard"
41+ , desc dp "Read and write the system clipboard"
42+ , view { style: tw "flex-row items-center mb-2" }
43+ [ nativeButton
44+ { title: "Copy 'Hello'"
45+ , bezelStyle: T.push
46+ , onPress: handler_ (copyToClipboard "Hello from PureScript!")
47+ , style: Style.style { height: 24.0, width: 120.0 }
48+ }
49+ ]
50+ ]
51+
52+shareDemo :: DemoProps -> JSX
53+shareDemo = component "ShareDemo" \dp -> pure do
54+ scrollWrap dp
55+ [ sectionTitle dp.fg "Share"
56+ , desc dp "System share picker"
57+ , nativeButton
58+ { title: "Share Text"
59+ , sfSymbol: "square.and.arrow.up"
60+ , bezelStyle: T.push
61+ , onPress: handler_ (macosShare [ "Hello from PureScript React Native!" ])
62+ , style: Style.style { height: 24.0, width: 120.0 } <> tw "mb-2"
63+ }
64+ ]
65+
66+notificationsDemo :: DemoProps -> JSX
67+notificationsDemo = component "NotificationsDemo" \dp -> pure do
68+ scrollWrap dp
69+ [ sectionTitle dp.fg "Notifications"
70+ , desc dp "System notifications (requires permission)"
71+ , nativeButton
72+ { title: "Send Notification"
73+ , sfSymbol: "bell"
74+ , bezelStyle: T.push
75+ , onPress: handler_ (macosNotify "PureScript" "Hello from React Native macOS!")
76+ , style: Style.style { height: 24.0, width: 160.0 } <> tw "mb-2"
77+ }
78+ ]
79+
80+soundDemo :: DemoProps -> JSX
81+soundDemo = component "SoundDemo" \dp -> pure do
82+ scrollWrap dp
83+ [ sectionTitle dp.fg "Sound"
84+ , desc dp "Play system sounds"
85+ , view { style: tw "flex-row items-center mb-2" }
86+ [ nativeButton
87+ { title: "Glass"
88+ , bezelStyle: T.push
89+ , onPress: handler_ (macosPlaySound "Glass")
90+ , style: Style.style { height: 24.0, width: 80.0 }
91+ }
92+ , nativeButton
93+ { title: "Ping"
94+ , bezelStyle: T.push
95+ , onPress: handler_ (macosPlaySound "Ping")
96+ , style: Style.style { height: 24.0, width: 80.0, marginLeft: 8.0 }
97+ }
98+ , nativeButton
99+ { title: "Beep"
100+ , bezelStyle: T.push
101+ , onPress: handler_ macosBeep
102+ , style: Style.style { height: 24.0, width: 80.0, marginLeft: 8.0 }
103+ }
104+ ]
105+ ]
106+
107+statusBarDemo :: DemoProps -> JSX
108+statusBarDemo = component "StatusBarDemo" \dp -> React.do
109+ active /\ setActive <- useState' false
110+ pure do
111+ scrollWrap dp
112+ [ sectionTitle dp.fg "Status Bar"
113+ , desc dp "Menu bar icon with dropdown"
114+ , view { style: tw "flex-row items-center mb-4" }
115+ [ nativeButton
116+ { title: if active then "Remove" else "Add to Menu Bar"
117+ , sfSymbol: if active then "minus.circle" else "plus.circle"
118+ , bezelStyle: T.push
119+ , onPress: handler_
120+ ( if active then do
121+ macosRemoveStatusBarItem
122+ setActive false
123+ else do
124+ macosSetStatusBarItem
125+ { title: ""
126+ , sfSymbol: "swift"
127+ , menuItems:
128+ [ { id: "about", title: "About PureScript RN" }
129+ , { id: "sep", title: "-" }
130+ , { id: "quit", title: "Quit" }
131+ ]
132+ }
133+ setActive true
134+ )
135+ , style: Style.style { height: 24.0, width: 180.0 }
136+ }
137+ ]
138+ ]
139+
140+quickLookDemo :: DemoProps -> JSX
141+quickLookDemo = component "QuickLookDemo" \dp -> pure do
142+ scrollWrap dp
143+ [ sectionTitle dp.fg "Quick Look"
144+ , desc dp "Preview files with QLPreviewPanel"
145+ , view { style: tw "flex-row items-center mb-4" }
146+ [ nativeButton
147+ { title: "Preview /etc/hosts"
148+ , sfSymbol: "eye"
149+ , bezelStyle: T.push
150+ , onPress: handler_ (macosQuickLook "/etc/hosts")
151+ , style: Style.style { height: 24.0, width: 180.0 }
152+ }
153+ ]
154+ ]
155+
156+colorPanelDemo :: DemoProps -> JSX
157+colorPanelDemo = component "ColorPanelDemo" \dp -> pure do
158+ scrollWrap dp
159+ [ sectionTitle dp.fg "Color Picker"
160+ , desc dp "System color panel (NSColorPanel)"
161+ , view { style: tw "flex-row items-center mb-4" }
162+ [ nativeButton
163+ { title: "Show Color Panel"
164+ , sfSymbol: "paintpalette"
165+ , bezelStyle: T.push
166+ , onPress: handler_ macosShowColorPanel
167+ , style: Style.style { height: 24.0, width: 180.0 }
168+ }
169+ ]
170+ ]
171+
172+fontPanelDemo :: DemoProps -> JSX
173+fontPanelDemo = component "FontPanelDemo" \dp -> pure do
174+ scrollWrap dp
175+ [ sectionTitle dp.fg "Font Panel"
176+ , desc dp "System font panel (NSFontPanel)"
177+ , view { style: tw "flex-row items-center mb-4" }
178+ [ nativeButton
179+ { title: "Show Font Panel"
180+ , sfSymbol: "textformat"
181+ , bezelStyle: T.push
182+ , onPress: handler_ macosShowFontPanel
183+ , style: Style.style { height: 24.0, width: 180.0 }
184+ }
185+ ]
186+ ]
187+
188+speechDemo :: DemoProps -> JSX
189+speechDemo = component "SpeechDemo" \dp -> React.do
190+ txt /\ setTxt <- useState' "Hello from PureScript React Native on macOS!"
191+ pure do
192+ scrollWrap dp
193+ [ sectionTitle dp.fg "Speech Synthesis"
194+ , desc dp "Text-to-speech (NSSpeechSynthesizer)"
195+ , nativeTextField
196+ { text: txt
197+ , placeholder: "Text to speak..."
198+ , onChangeText: E.onString "text" setTxt
199+ , style: Style.style { height: 24.0 } <> tw "mb-2"
200+ }
201+ , view { style: tw "flex-row items-center mb-4" }
202+ [ nativeButton
203+ { title: "Speak"
204+ , sfSymbol: "speaker.wave.2"
205+ , bezelStyle: T.push
206+ , onPress: handler_ (say txt)
207+ , style: Style.style { height: 24.0, width: 100.0 }
208+ }
209+ , nativeButton
210+ { title: "Stop"
211+ , sfSymbol: "stop.circle"
212+ , bezelStyle: T.push
213+ , onPress: handler_ stopSpeaking
214+ , style: Style.style { height: 24.0, width: 100.0, marginLeft: 8.0 }
215+ }
216+ ]
217+ ]
example-macos/src/Demo/TextEditing.pursnew file+45
@@ -0,0 +1,45 @@
1+module Demo.TextEditing (textFieldDemo, textEditorDemo) where
2+
3+import Prelude
4+
5+import Demo.Shared (DemoProps, card, label, scrollWrap, sectionTitle)
6+import React.Basic (JSX)
7+import React.Basic.Hooks (useState', (/\))
8+import React.Basic.Hooks as React
9+import Yoga.React (component)
10+import Yoga.React.Native (tw, view)
11+import Yoga.React.Native.MacOS.Events as E
12+import Yoga.React.Native.MacOS.TextField (nativeTextField)
13+import Yoga.React.Native.MacOS.TextEditor (nativeTextEditor)
14+import Yoga.React.Native.Style as Style
15+
16+textFieldDemo :: DemoProps -> JSX
17+textFieldDemo = component "TextFieldDemo" \dp -> React.do
18+ txt /\ setTxt <- useState' ""
19+ pure do
20+ scrollWrap dp
21+ [ sectionTitle dp.fg "Text Field"
22+ , card dp.cardBg
23+ [ nativeTextField
24+ { placeholder: "Type something..."
25+ , search: true
26+ , text: txt
27+ , onChangeText: E.onString "text" setTxt
28+ , style: Style.style { height: 24.0 }
29+ }
30+ , if txt == "" then mempty
31+ else label dp.dimFg ("You typed: " <> txt)
32+ ]
33+ ]
34+
35+textEditorDemo :: DemoProps -> JSX
36+textEditorDemo = component "TextEditorDemo" \dp -> pure do
37+ view { style: tw "flex-1 px-4" }
38+ [ sectionTitle dp.fg "Rich Text Editor"
39+ , nativeTextEditor
40+ { text: "Welcome to the PureScript-driven native text editor.\n\nThis uses NSTextView with rich text support and a formatting ruler."
41+ , richText: true
42+ , showsRuler: true
43+ , style: tw "flex-1" <> Style.style { minHeight: 300.0 }
44+ }
45+ ]
example-macos/src/Demo/WebBrowser.pursnew file+41
@@ -0,0 +1,41 @@
1+module Demo.WebBrowser (webViewDemo) where
2+
3+import Prelude
4+
5+import Demo.Shared (DemoProps, sectionTitle)
6+import React.Basic (JSX)
7+import React.Basic.Events (handler, unsafeEventFn)
8+import React.Basic.Hooks (useState', (/\))
9+import React.Basic.Hooks as React
10+import Yoga.React (component)
11+import Yoga.React.Native (nativeEvent, tw, view)
12+import Yoga.React.Native.MacOS.Events as E
13+import Yoga.React.Native.MacOS.TextField (nativeTextField)
14+import Yoga.React.Native.MacOS.WebView (nativeWebView)
15+import Yoga.React.Native.Style as Style
16+
17+webViewDemo :: DemoProps -> JSX
18+webViewDemo = component "WebViewDemo" \dp -> React.do
19+ url /\ setUrl <- useState' "https://pursuit.purescript.org"
20+ urlBar /\ setUrlBar <- useState' "https://pursuit.purescript.org"
21+ pure do
22+ view { style: tw "flex-1 px-4" }
23+ [ sectionTitle dp.fg "Web Browser"
24+ , nativeTextField
25+ { text: urlBar
26+ , placeholder: "Enter URL..."
27+ , search: false
28+ , onChangeText: E.onString "text" setUrlBar
29+ , onSubmit: E.onString "text" setUrl
30+ , style: Style.style { height: 24.0, marginBottom: 8.0 }
31+ }
32+ , nativeWebView
33+ { url
34+ , onFinishLoad: handler
35+ ( nativeEvent >>> unsafeEventFn \e ->
36+ { url: E.getFieldStr "url" e, title: E.getFieldStr "title" e }
37+ )
38+ \r -> setUrlBar r.url
39+ , style: tw "flex-1" <> Style.style { minHeight: 400.0 }
40+ }
41+ ]
example-macos/src/Main.jsdeletedโˆ’16
@@ -1,16 +0,0 @@
1โˆ’export const unsafeRound = (n) => Math.round(n);
2โˆ’export const isSingleEmoji = (str) => {
3โˆ’ var trimmed = str.trim();
4โˆ’ var re =
5โˆ’ /^(?:\p{Emoji_Presentation}|\p{Emoji}\uFE0F)(?:\u200D(?:\p{Emoji_Presentation}|\p{Emoji}\uFE0F))*$/u;
6โˆ’ return trimmed.length <= 10 && re.test(trimmed);
7โˆ’};
8โˆ’export const getFieldJSON = (key) => (obj) => {
9โˆ’ var v = obj[key];
10โˆ’ if (v === undefined || v === null) return "";
11โˆ’ return JSON.stringify(v);
12โˆ’};
13โˆ’export const charCodes = (str) => Array.from(str).map((c) => c.charCodeAt(0));
14โˆ’export const abs = (n) => Math.abs(n);
15โˆ’export const setInterval = (ms) => (fn) => () => globalThis.setInterval(fn, ms);
16โˆ’export const clearInterval = (id) => () => globalThis.clearInterval(id);
example-macos/src/Main.purs+8โˆ’1852
@@ -2,92 +2,24 @@ module Main where
2
2
3import Prelude
3import Prelude
4
4
5โˆ’import Data.Array (mapWithIndex, length, filter, null, (!!))
5+import Data.Maybe (Maybe(..))
6import Data.Nullable (toNullable)
6import Data.Nullable (toNullable)
7โˆ’import Data.Maybe (Maybe(..), fromMaybe)
7+import Demo.Navigation (demoContent, outlineSidebar)
8โˆ’import Data.String (take, joinWith)
8+import Demo.Shared (DemoProps)
9โˆ’import Data.Foldable (for_, foldl)
10โˆ’import Data.Traversable (for)
11import Effect (Effect)
9import Effect (Effect)
12โˆ’import Data.Either (Either(..))
13โˆ’import Effect.Aff (launchAff_, try)
14โˆ’import Effect.Class (liftEffect)
15import React.Basic (JSX)
10import React.Basic (JSX)
16โˆ’import React.Basic.Events (handler, handler_, unsafeEventFn)
11+import React.Basic.Events (handler_)
17โˆ’import React.Basic.Hooks (useState, useState', useEffect, useRef, readRef, writeRef, (/\))
12+import React.Basic.Hooks (useState', (/\))
18import React.Basic.Hooks as React
13import React.Basic.Hooks as React
19import Yoga.React (component)
14import Yoga.React (component)
20โˆ’import Yoga.React.Native (nativeEvent, registerComponent, safeAreaView, text, tw, view)
15+import Yoga.React.Native (registerComponent, safeAreaView, tw, view)
21import Yoga.React.Native.Appearance (useColorScheme)
16import Yoga.React.Native.Appearance (useColorScheme)
22โˆ’import Yoga.React.Native.MacOS.Button (nativeButton)
23โˆ’import Yoga.React.Native.MacOS.ColorWell (nativeColorWell)
24โˆ’import Yoga.React.Native.MacOS.DatePicker (nativeDatePicker)
25โˆ’import Yoga.React.Native.MacOS.LevelIndicator (nativeLevelIndicator)
26โˆ’import Yoga.React.Native.MacOS.PopUp (nativePopUp)
27โˆ’import Yoga.React.Native.MacOS.Progress (nativeProgress)
28โˆ’import Yoga.React.Native.MacOS.Slider (nativeSlider)
29โˆ’import Yoga.React.Native.MacOS.Switch (nativeSwitch)
30โˆ’import Yoga.React.Native.MacOS.TextField (nativeTextField)
31โˆ’import Yoga.React.Native.MacOS.TextEditor (nativeTextEditor)
32โˆ’import Yoga.React.Native.MacOS.WebView (nativeWebView)
33โˆ’import Yoga.React.Native.MacOS.ScrollView (nativeScrollView)
34โˆ’import Yoga.React.Native.MacOS.Rive (nativeRiveView_)
35โˆ’import Yoga.React.Native.MacOS.Toolbar (nativeToolbar)
36โˆ’import Yoga.React.Native.MacOS.VisualEffect (nativeVisualEffect)
37import Yoga.React.Native.MacOS.Sidebar (sidebarLayout)
17import Yoga.React.Native.MacOS.Sidebar (sidebarLayout)
38โˆ’import Yoga.React.Native.MacOS.ContextMenu (nativeContextMenu)
18+import Yoga.React.Native.MacOS.Toolbar (nativeToolbar)
39โˆ’import Yoga.React.Native.MacOS.FilePicker (nativeFilePicker)
40โˆ’import Yoga.React.Native.MacOS.VideoPlayer (nativeVideoPlayer)
41โˆ’import Yoga.React.Native.MacOS.AnimatedImage (nativeAnimatedImage)
42โˆ’import Yoga.React.Native.MacOS.PatternBackground (nativePatternBackground)
43โˆ’import Yoga.React.Native.MacOS.SplitView (nativeSplitView)
44โˆ’import Yoga.React.Native.MacOS.TabView (nativeTabView)
45โˆ’import Yoga.React.Native.MacOS.ComboBox (nativeComboBox)
46โˆ’import Yoga.React.Native.MacOS.Stepper (nativeStepper)
47โˆ’import Yoga.React.Native.MacOS.Box (nativeBox)
48โˆ’import Yoga.React.Native.MacOS.Popover (nativePopover)
49โˆ’import Yoga.React.Native.MacOS.Image (nativeImage)
50โˆ’import Yoga.React.Native.MacOS.Alert (macosAlert)
51โˆ’import Yoga.React.Native.MacOS.Checkbox (nativeCheckbox)
52โˆ’import Yoga.React.Native.MacOS.RadioButton (nativeRadioButton)
53โˆ’import Yoga.React.Native.MacOS.SearchField (nativeSearchField)
54โˆ’import Yoga.React.Native.MacOS.TokenField (nativeTokenField)
55โˆ’import Yoga.React.Native.MacOS.Separator (nativeSeparator)
56โˆ’import Yoga.React.Native.MacOS.HelpButton (nativeHelpButton)
57โˆ’import Yoga.React.Native.MacOS.PathControl (nativePathControl)
58โˆ’import Yoga.React.Native.MacOS.Sheet (nativeSheet)
59โˆ’import Yoga.React.Native.MacOS.TableView (nativeTableView)
60โˆ’import Yoga.React.Native.MacOS.Menu (macosShowMenu)
61โˆ’import Yoga.React.Native.MacOS.Pasteboard (copyToClipboard)
62โˆ’import Yoga.React.Native.MacOS.OutlineView (nativeOutlineView, OutlineItem(..))
63โˆ’import Yoga.React.Native.MacOS.ShareService (macosShare)
64โˆ’import Yoga.React.Native.MacOS.UserNotification (macosNotify)
65โˆ’import Yoga.React.Native.MacOS.Sound (macosPlaySound, macosBeep)
66โˆ’import Yoga.React.Native.MacOS.StatusBarItem (macosSetStatusBarItem, macosRemoveStatusBarItem)
67โˆ’import Yoga.React.Native.MacOS.MapView (nativeMapView)
68โˆ’import Yoga.React.Native.MacOS.PDFView (nativePDFView)
69โˆ’import Yoga.React.Native.MacOS.QuickLook (macosQuickLook)
70โˆ’import Yoga.React.Native.MacOS.SpeechSynthesizer (say, stopSpeaking)
71โˆ’import Yoga.React.Native.MacOS.ColorPanel (macosShowColorPanel)
72โˆ’import Yoga.React.Native.MacOS.FontPanel (macosShowFontPanel)
73โˆ’import Yoga.React.Native.MacOS.OCR (recognizeText)
74โˆ’import Yoga.React.Native.MacOS.SpeechRecognition (useSpeechRecognition)
75โˆ’import Yoga.React.Native.MacOS.NaturalLanguage (detectLanguage, analyzeSentiment, tokenize)
76โˆ’import Yoga.React.Native.MacOS.CameraView (nativeCameraView)
77โˆ’import Yoga.React.Native.MacOS.Events as E
78import Yoga.React.Native.MacOS.Types as T
19import Yoga.React.Native.MacOS.Types as T
79โˆ’import Yoga.React.Native.Matrix as Matrix
20+import Yoga.React.Native.MacOS.VisualEffect (nativeVisualEffect)
80import Yoga.React.Native.Style as Style
21import Yoga.React.Native.Style as Style
81
22
82โˆ’-- Shared props for all leaf demos
83โˆ’type DemoProps =
84โˆ’ { fg :: String
85โˆ’ , dimFg :: String
86โˆ’ , cardBg :: String
87โˆ’ , bg :: String
88โˆ’ , isDark :: Boolean
89โˆ’ }
90โˆ’
91main :: Effect Unit
23main :: Effect Unit
92main = registerComponent "YogaReactExample" \_ -> app {}
24main = registerComponent "YogaReactExample" \_ -> app {}
93
25
@@ -124,1779 +56,3 @@ app = component "App" \_ -> React.do
124 ]
56 ]
125 )
57 )
126 )
58 )
127โˆ’
128โˆ’-- Outline tree
129โˆ’outlineTree :: Array OutlineItem
130โˆ’outlineTree =
131โˆ’ let
132โˆ’ leaf id title symbol = OutlineItem { id, title, sfSymbol: symbol, children: [] }
133โˆ’ folder id title symbol children = OutlineItem { id, title, sfSymbol: symbol, children }
134โˆ’ in
135โˆ’ [ folder "input" "Input Controls" "slider.horizontal.3"
136โˆ’ [ leaf "button" "Button" "button.horizontal"
137โˆ’ , leaf "switch" "Switch" "switch.2"
138โˆ’ , leaf "slider" "Slider" "slider.horizontal.below.rectangle"
139โˆ’ , leaf "popup" "Pop Up" "chevron.up.chevron.down"
140โˆ’ , leaf "combobox" "Combo Box" "rectangle.and.pencil.and.ellipsis"
141โˆ’ , leaf "stepper" "Stepper" "plus.forwardslash.minus"
142โˆ’ , leaf "datepicker" "Date Picker" "calendar"
143โˆ’ , leaf "colorwell" "Color Well" "paintpalette"
144โˆ’ , leaf "checkbox" "Checkbox" "checkmark.square"
145โˆ’ , leaf "radiobutton" "Radio Button" "circle.inset.filled"
146โˆ’ , leaf "searchfield" "Search Field" "magnifyingglass"
147โˆ’ , leaf "tokenfield" "Token Field" "tag"
148โˆ’ ]
149โˆ’ , folder "text" "Text & Editing" "doc.richtext"
150โˆ’ [ leaf "textfield" "Text Field" "character.cursor.ibeam"
151โˆ’ , leaf "texteditor" "Text Editor" "doc.richtext"
152โˆ’ ]
153โˆ’ , folder "display" "Display" "photo"
154โˆ’ [ leaf "image" "Image" "photo"
155โˆ’ , leaf "animatedimage" "Animated Image" "photo.on.rectangle.angled"
156โˆ’ , leaf "levelindicator" "Level Indicator" "gauge.with.dots.needle.33percent"
157โˆ’ , leaf "progress" "Progress" "chart.bar.fill"
158โˆ’ , leaf "separator" "Separator" "minus"
159โˆ’ , leaf "pathcontrol" "Path Control" "chevron.compact.right"
160โˆ’ ]
161โˆ’ , folder "layout" "Layout & Containers" "rectangle.split.3x1"
162โˆ’ [ leaf "box" "Box" "square.dashed"
163โˆ’ , leaf "splitview" "Split View" "rectangle.split.2x1"
164โˆ’ , leaf "tabview" "Tab View" "rectangle.topthird.inset.filled"
165โˆ’ ]
166โˆ’ , folder "overlays" "Overlays & Dialogs" "rectangle.on.rectangle"
167โˆ’ [ leaf "alert" "Alert" "exclamationmark.triangle"
168โˆ’ , leaf "sheet" "Sheet" "rectangle.bottomhalf.inset.filled"
169โˆ’ , leaf "popover" "Popover" "text.bubble"
170โˆ’ , leaf "contextmenu" "Context Menu" "contextualmenu.and.cursorarrow"
171โˆ’ , leaf "menu" "Menu" "list.bullet"
172โˆ’ ]
173โˆ’ , folder "dataviews" "Data Views" "tablecells"
174โˆ’ [ leaf "tableview" "Table View" "tablecells"
175โˆ’ , leaf "outlineview" "Outline View" "list.bullet.indent"
176โˆ’ ]
177โˆ’ , folder "dragdrop" "Drag & Drop / Files" "doc.badge.plus"
178โˆ’ [ leaf "dropzone" "Drop Zone" "square.and.arrow.down"
179โˆ’ , leaf "filepicker" "File Picker" "folder"
180โˆ’ ]
181โˆ’ , folder "animation" "Animation" "play.circle"
182โˆ’ [ leaf "rive" "Rive" "play.circle"
183โˆ’ ]
184โˆ’ , folder "system" "System Services" "gearshape.2"
185โˆ’ [ leaf "clipboard" "Clipboard" "doc.on.clipboard"
186โˆ’ , leaf "share" "Share" "square.and.arrow.up"
187โˆ’ , leaf "notifications" "Notifications" "bell"
188โˆ’ , leaf "sound" "Sound" "speaker.wave.2"
189โˆ’ , leaf "statusbar" "Status Bar" "menubar.rectangle"
190โˆ’ , leaf "quicklook" "Quick Look" "eye"
191โˆ’ , leaf "colorpanel" "Color Panel" "paintpalette"
192โˆ’ , leaf "fontpanel" "Font Panel" "textformat"
193โˆ’ , leaf "speech" "Speech Synthesis" "speaker.wave.2"
194โˆ’ ]
195โˆ’ , folder "aiml" "AI & ML" "brain"
196โˆ’ [ leaf "ocr" "OCR" "doc.text.viewfinder"
197โˆ’ , leaf "speechrecognition" "Speech Recognition" "mic"
198โˆ’ , leaf "naturallanguage" "Natural Language" "text.magnifyingglass"
199โˆ’ , leaf "camera" "Camera" "video"
200โˆ’ ]
201โˆ’ , folder "mapsdocs" "Maps & Documents" "map"
202โˆ’ [ leaf "mapview" "Map View" "map"
203โˆ’ , leaf "pdfview" "PDF View" "doc.richtext"
204โˆ’ ]
205โˆ’ , folder "webbrowser" "Web Browser" "globe"
206โˆ’ [ leaf "webview" "Web View" "globe"
207โˆ’ ]
208โˆ’ , folder "chat" "Chat" "bubble.left.and.bubble.right"
209โˆ’ [ leaf "matrix" "Matrix" "bubble.left.and.bubble.right"
210โˆ’ ]
211โˆ’ ]
212โˆ’
213โˆ’outlineSidebar :: String -> (String -> Effect Unit) -> JSX
214โˆ’outlineSidebar selectedItem setSelectedItem =
215โˆ’ view { style: tw "flex-1 pt-2" }
216โˆ’ [ nativeOutlineView
217โˆ’ { items: outlineTree
218โˆ’ , headerVisible: false
219โˆ’ , onSelectItem: E.onString "id" setSelectedItem
220โˆ’ , style: tw "flex-1"
221โˆ’ }
222โˆ’ ]
223โˆ’
224โˆ’-- Content dispatch
225โˆ’demoContent :: DemoProps -> String -> JSX
226โˆ’demoContent dp = case _ of
227โˆ’ "button" -> buttonDemo dp
228โˆ’ "switch" -> switchDemo dp
229โˆ’ "slider" -> sliderDemo dp
230โˆ’ "popup" -> popUpDemo dp
231โˆ’ "combobox" -> comboBoxDemo dp
232โˆ’ "stepper" -> stepperDemo dp
233โˆ’ "datepicker" -> datePickerDemo dp
234โˆ’ "colorwell" -> colorWellDemo dp
235โˆ’ "checkbox" -> checkboxDemo dp
236โˆ’ "radiobutton" -> radioButtonDemo dp
237โˆ’ "searchfield" -> searchFieldDemo dp
238โˆ’ "tokenfield" -> tokenFieldDemo dp
239โˆ’ "textfield" -> textFieldDemo dp
240โˆ’ "texteditor" -> textEditorDemo dp
241โˆ’ "image" -> imageDemo dp
242โˆ’ "animatedimage" -> animatedImageDemo dp
243โˆ’ "levelindicator" -> levelIndicatorDemo dp
244โˆ’ "progress" -> progressDemo dp
245โˆ’ "separator" -> separatorDemo dp
246โˆ’ "pathcontrol" -> pathControlDemo dp
247โˆ’ "box" -> boxDemo dp
248โˆ’ "splitview" -> splitViewDemo dp
249โˆ’ "tabview" -> tabViewDemo dp
250โˆ’ "alert" -> alertDemo dp
251โˆ’ "sheet" -> sheetDemo dp
252โˆ’ "popover" -> popoverDemo dp
253โˆ’ "contextmenu" -> contextMenuDemo dp
254โˆ’ "menu" -> menuDemo dp
255โˆ’ "tableview" -> tableViewDemo dp
256โˆ’ "outlineview" -> outlineViewDemo dp
257โˆ’ "dropzone" -> dropZoneDemo dp
258โˆ’ "filepicker" -> filePickerDemo dp
259โˆ’ "rive" -> riveDemo dp
260โˆ’ "clipboard" -> clipboardDemo dp
261โˆ’ "share" -> shareDemo dp
262โˆ’ "notifications" -> notificationsDemo dp
263โˆ’ "sound" -> soundDemo dp
264โˆ’ "statusbar" -> statusBarDemo dp
265โˆ’ "quicklook" -> quickLookDemo dp
266โˆ’ "colorpanel" -> colorPanelDemo dp
267โˆ’ "fontpanel" -> fontPanelDemo dp
268โˆ’ "speech" -> speechDemo dp
269โˆ’ "ocr" -> ocrDemo dp
270โˆ’ "speechrecognition" -> speechRecognitionDemo dp
271โˆ’ "naturallanguage" -> naturalLanguageDemo dp
272โˆ’ "camera" -> cameraDemo dp
273โˆ’ "mapview" -> mapViewDemo dp
274โˆ’ "pdfview" -> pdfViewDemo dp
275โˆ’ "webview" -> webViewDemo dp
276โˆ’ "matrix" -> matrixDemo dp
277โˆ’ _ -> placeholder dp
278โˆ’
279โˆ’placeholder :: DemoProps -> JSX
280โˆ’placeholder dp =
281โˆ’ view { style: tw "flex-1 items-center justify-center" }
282โˆ’ [ text { style: tw "text-sm" <> Style.style { color: dp.dimFg } } "Select a component from the sidebar" ]
283โˆ’
284โˆ’-- Input Controls
285โˆ’
286โˆ’buttonDemo :: DemoProps -> JSX
287โˆ’buttonDemo = component "ButtonDemo" \dp -> React.do
288โˆ’ status /\ setStatus <- useState' "Ready"
289โˆ’ pure do
290โˆ’ scrollWrap dp
291โˆ’ [ sectionTitle dp.fg "Button"
292โˆ’ , card dp.cardBg
293โˆ’ [ view { style: tw "flex-row items-center" }
294โˆ’ [ nativeButton
295โˆ’ { title: "Say Hello"
296โˆ’ , sfSymbol: "hand.wave"
297โˆ’ , bezelStyle: T.push
298โˆ’ , primary: true
299โˆ’ , onPress: handler_ (setStatus "Hello from PureScript!")
300โˆ’ , style: Style.style { height: 24.0, width: 140.0 }
301โˆ’ }
302โˆ’ , nativeButton
303โˆ’ { title: "Reset"
304โˆ’ , sfSymbol: "arrow.counterclockwise"
305โˆ’ , bezelStyle: T.push
306โˆ’ , onPress: handler_ (setStatus "Ready")
307โˆ’ , style: Style.style { height: 24.0, width: 100.0, marginLeft: 8.0 }
308โˆ’ }
309โˆ’ , label dp.dimFg status
310โˆ’ ]
311โˆ’ ]
312โˆ’ ]
313โˆ’
314โˆ’switchDemo :: DemoProps -> JSX
315โˆ’switchDemo = component "SwitchDemo" \dp -> React.do
316โˆ’ on /\ setOn <- useState' false
317โˆ’ pure do
318โˆ’ scrollWrap dp
319โˆ’ [ sectionTitle dp.fg "Switch"
320โˆ’ , card dp.cardBg
321โˆ’ [ view { style: tw "flex-row items-center" }
322โˆ’ [ nativeSwitch
323โˆ’ { on
324โˆ’ , onChange: E.onBool "on" setOn
325โˆ’ , style: Style.style { height: 24.0, width: 48.0 }
326โˆ’ }
327โˆ’ , label dp.dimFg (if on then "On" else "Off")
328โˆ’ ]
329โˆ’ ]
330โˆ’ ]
331โˆ’
332โˆ’sliderDemo :: DemoProps -> JSX
333โˆ’sliderDemo = component "SliderDemo" \dp -> React.do
334โˆ’ value /\ setValue <- useState' 50.0
335โˆ’ pure do
336โˆ’ scrollWrap dp
337โˆ’ [ sectionTitle dp.fg "Slider + Level Indicator + Progress"
338โˆ’ , card dp.cardBg
339โˆ’ [ nativeSlider
340โˆ’ { value
341โˆ’ , minValue: 0.0
342โˆ’ , maxValue: 100.0
343โˆ’ , numberOfTickMarks: 11
344โˆ’ , onChange: E.onNumber "value" setValue
345โˆ’ , style: Style.style { height: 24.0 }
346โˆ’ }
347โˆ’ , label dp.dimFg ("Value: " <> show (round value) <> " / 100")
348โˆ’ , nativeLevelIndicator
349โˆ’ { value
350โˆ’ , minValue: 0.0
351โˆ’ , maxValue: 100.0
352โˆ’ , warningValue: 70.0
353โˆ’ , criticalValue: 90.0
354โˆ’ , style: Style.style { height: 18.0, marginTop: 8.0 }
355โˆ’ }
356โˆ’ , nativeProgress
357โˆ’ { value
358โˆ’ , style: Style.style { height: 18.0, marginTop: 8.0 }
359โˆ’ }
360โˆ’ ]
361โˆ’ ]
362โˆ’
363โˆ’popUpDemo :: DemoProps -> JSX
364โˆ’popUpDemo = component "PopUpDemo" \dp -> React.do
365โˆ’ idx /\ setIdx <- useState' 0
366โˆ’ title /\ setTitle <- useState' "Small"
367โˆ’ pure do
368โˆ’ scrollWrap dp
369โˆ’ [ sectionTitle dp.fg "Pop Up Button"
370โˆ’ , card dp.cardBg
371โˆ’ [ view { style: tw "flex-row items-center" }
372โˆ’ [ nativePopUp
373โˆ’ { items: [ "Small", "Medium", "Large", "Extra Large" ]
374โˆ’ , selectedIndex: idx
375โˆ’ , onChange: handler
376โˆ’ ( nativeEvent >>> unsafeEventFn \e ->
377โˆ’ { idx: E.getFieldInt "selectedIndex" e, title: E.getFieldStr "title" e }
378โˆ’ )
379โˆ’ \r -> do
380โˆ’ setIdx r.idx
381โˆ’ setTitle r.title
382โˆ’ , style: Style.style { height: 24.0, width: 160.0 }
383โˆ’ }
384โˆ’ , label dp.dimFg ("Selected: " <> title)
385โˆ’ ]
386โˆ’ ]
387โˆ’ ]
388โˆ’
389โˆ’comboBoxDemo :: DemoProps -> JSX
390โˆ’comboBoxDemo = component "ComboBoxDemo" \dp -> React.do
391โˆ’ txt /\ setTxt <- useState' ""
392โˆ’ result /\ setResult <- useState' ""
393โˆ’ pure do
394โˆ’ scrollWrap dp
395โˆ’ [ sectionTitle dp.fg "Combo Box"
396โˆ’ , desc dp "Editable dropdown with autocomplete"
397โˆ’ , card dp.cardBg
398โˆ’ [ nativeComboBox
399โˆ’ { items: [ "Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig", "Grape" ]
400โˆ’ , text: txt
401โˆ’ , placeholder: "Type a fruit..."
402โˆ’ , onChangeText: E.onString "text" setTxt
403โˆ’ , onSelectItem: E.onString "text" \t -> do
404โˆ’ setTxt t
405โˆ’ setResult ("Selected: " <> t)
406โˆ’ , style: Style.style { height: 28.0 } <> tw "mb-2"
407โˆ’ }
408โˆ’ , if result == "" then mempty
409โˆ’ else label dp.dimFg result
410โˆ’ ]
411โˆ’ ]
412โˆ’
413โˆ’stepperDemo :: DemoProps -> JSX
414โˆ’stepperDemo = component "StepperDemo" \dp -> React.do
415โˆ’ value /\ setValue <- useState' 5.0
416โˆ’ pure do
417โˆ’ scrollWrap dp
418โˆ’ [ sectionTitle dp.fg "Stepper"
419โˆ’ , desc dp "Up/down increment control"
420โˆ’ , card dp.cardBg
421โˆ’ [ view { style: tw "flex-row items-center" }
422โˆ’ [ text { style: tw "text-sm mr-3" <> Style.style { color: dp.fg } } "Quantity:"
423โˆ’ , nativeStepper
424โˆ’ { value
425โˆ’ , minValue: 0.0
426โˆ’ , maxValue: 50.0
427โˆ’ , increment: 1.0
428โˆ’ , onChange: E.onNumber "value" setValue
429โˆ’ , style: Style.style { width: 100.0, height: 24.0 }
430โˆ’ }
431โˆ’ ]
432โˆ’ ]
433โˆ’ ]
434โˆ’
435โˆ’datePickerDemo :: DemoProps -> JSX
436โˆ’datePickerDemo = component "DatePickerDemo" \dp -> React.do
437โˆ’ dateText /\ setDateText <- useState' ""
438โˆ’ pure do
439โˆ’ scrollWrap dp
440โˆ’ [ sectionTitle dp.fg "Date Picker"
441โˆ’ , card dp.cardBg
442โˆ’ [ nativeDatePicker
443โˆ’ { graphical: false
444โˆ’ , onChange: E.onString "date" setDateText
445โˆ’ , style: Style.style { height: 24.0, width: 200.0 }
446โˆ’ }
447โˆ’ , if dateText == "" then mempty
448โˆ’ else label dp.dimFg ("Picked: " <> dateText)
449โˆ’ ]
450โˆ’ ]
451โˆ’
452โˆ’colorWellDemo :: DemoProps -> JSX
453โˆ’colorWellDemo = component "ColorWellDemo" \dp -> React.do
454โˆ’ color /\ setColor <- useState' "#FF6600"
455โˆ’ pure do
456โˆ’ scrollWrap dp
457โˆ’ [ sectionTitle dp.fg "Color Well"
458โˆ’ , card dp.cardBg
459โˆ’ [ view { style: tw "flex-row items-center" }
460โˆ’ [ nativeColorWell
461โˆ’ { color
462โˆ’ , minimal: true
463โˆ’ , onChange: E.onString "color" setColor
464โˆ’ , style: Style.style { height: 32.0, width: 48.0 }
465โˆ’ }
466โˆ’ , view
467โˆ’ { style: tw "ml-3 rounded" <> Style.style
468โˆ’ { width: 24.0, height: 24.0, backgroundColor: color }
469โˆ’ }
470โˆ’ []
471โˆ’ , label dp.dimFg color
472โˆ’ ]
473โˆ’ ]
474โˆ’ ]
475โˆ’
476โˆ’checkboxDemo :: DemoProps -> JSX
477โˆ’checkboxDemo = component "CheckboxDemo" \dp -> React.do
478โˆ’ a /\ setA <- useState' true
479โˆ’ b /\ setB <- useState' false
480โˆ’ pure do
481โˆ’ scrollWrap dp
482โˆ’ [ sectionTitle dp.fg "Checkbox"
483โˆ’ , card dp.cardBg
484โˆ’ [ nativeCheckbox
485โˆ’ { checked: a
486โˆ’ , title: "Enable notifications"
487โˆ’ , enabled: true
488โˆ’ , onChange: E.onBool "checked" setA
489โˆ’ , style: Style.style { height: 24.0 } <> tw "mb-1"
490โˆ’ }
491โˆ’ , nativeCheckbox
492โˆ’ { checked: b
493โˆ’ , title: "Dark mode"
494โˆ’ , enabled: true
495โˆ’ , onChange: E.onBool "checked" setB
496โˆ’ , style: Style.style { height: 24.0 }
497โˆ’ }
498โˆ’ ]
499โˆ’ ]
500โˆ’
501โˆ’radioButtonDemo :: DemoProps -> JSX
502โˆ’radioButtonDemo = component "RadioButtonDemo" \dp -> React.do
503โˆ’ choice /\ setChoice <- useState' "option1"
504โˆ’ pure do
505โˆ’ scrollWrap dp
506โˆ’ [ sectionTitle dp.fg "Radio Button"
507โˆ’ , card dp.cardBg
508โˆ’ [ nativeRadioButton
509โˆ’ { selected: choice == "option1"
510โˆ’ , title: "Small"
511โˆ’ , enabled: true
512โˆ’ , onChange: handler_ (setChoice "option1")
513โˆ’ , style: Style.style { height: 24.0 } <> tw "mb-1"
514โˆ’ }
515โˆ’ , nativeRadioButton
516โˆ’ { selected: choice == "option2"
517โˆ’ , title: "Medium"
518โˆ’ , enabled: true
519โˆ’ , onChange: handler_ (setChoice "option2")
520โˆ’ , style: Style.style { height: 24.0 } <> tw "mb-1"
521โˆ’ }
522โˆ’ , nativeRadioButton
523โˆ’ { selected: choice == "option3"
524โˆ’ , title: "Large"
525โˆ’ , enabled: true
526โˆ’ , onChange: handler_ (setChoice "option3")
527โˆ’ , style: Style.style { height: 24.0 }
528โˆ’ }
529โˆ’ , label dp.dimFg ("Selected: " <> choice)
530โˆ’ ]
531โˆ’ ]
532โˆ’
533โˆ’searchFieldDemo :: DemoProps -> JSX
534โˆ’searchFieldDemo = component "SearchFieldDemo" \dp -> React.do
535โˆ’ query /\ setQuery <- useState' ""
536โˆ’ result /\ setResult <- useState' ""
537โˆ’ pure do
538โˆ’ scrollWrap dp
539โˆ’ [ sectionTitle dp.fg "Search Field"
540โˆ’ , nativeSearchField
541โˆ’ { text: query
542โˆ’ , placeholder: "Search..."
543โˆ’ , onChangeText: E.onString "text" setQuery
544โˆ’ , onSearch: E.onString "text" \t -> setResult ("Searched: " <> t)
545โˆ’ , style: Style.style { height: 28.0 } <> tw "mb-2"
546โˆ’ }
547โˆ’ , if result == "" then mempty
548โˆ’ else label dp.dimFg result
549โˆ’ ]
550โˆ’
551โˆ’tokenFieldDemo :: DemoProps -> JSX
552โˆ’tokenFieldDemo = component "TokenFieldDemo" \dp -> React.do
553โˆ’ tokens /\ setTokens <- useState' ([ "PureScript", "React", "macOS" ] :: Array String)
554โˆ’ pure do
555โˆ’ scrollWrap dp
556โˆ’ [ sectionTitle dp.fg "Token Field"
557โˆ’ , desc dp "Type text and press Enter to add tokens"
558โˆ’ , nativeTokenField
559โˆ’ { tokens
560โˆ’ , placeholder: "Add tags..."
561โˆ’ , onChangeTokens: E.onStrings "tokens" setTokens
562โˆ’ , style: Style.style { height: 28.0 } <> tw "mb-2"
563โˆ’ }
564โˆ’ ]
565โˆ’
566โˆ’-- Text & Editing
567โˆ’
568โˆ’textFieldDemo :: DemoProps -> JSX
569โˆ’textFieldDemo = component "TextFieldDemo" \dp -> React.do
570โˆ’ txt /\ setTxt <- useState' ""
571โˆ’ pure do
572โˆ’ scrollWrap dp
573โˆ’ [ sectionTitle dp.fg "Text Field"
574โˆ’ , card dp.cardBg
575โˆ’ [ nativeTextField
576โˆ’ { placeholder: "Type something..."
577โˆ’ , search: true
578โˆ’ , text: txt
579โˆ’ , onChangeText: E.onString "text" setTxt
580โˆ’ , style: Style.style { height: 24.0 }
581โˆ’ }
582โˆ’ , if txt == "" then mempty
583โˆ’ else label dp.dimFg ("You typed: " <> txt)
584โˆ’ ]
585โˆ’ ]
586โˆ’
587โˆ’textEditorDemo :: DemoProps -> JSX
588โˆ’textEditorDemo = component "TextEditorDemo" \dp -> React.do
589โˆ’ pure do
590โˆ’ view { style: tw "flex-1 px-4" }
591โˆ’ [ sectionTitle dp.fg "Rich Text Editor"
592โˆ’ , nativeTextEditor
593โˆ’ { text: "Welcome to the PureScript-driven native text editor.\n\nThis uses NSTextView with rich text support and a formatting ruler."
594โˆ’ , richText: true
595โˆ’ , showsRuler: true
596โˆ’ , style: tw "flex-1" <> Style.style { minHeight: 300.0 }
597โˆ’ }
598โˆ’ ]
599โˆ’
600โˆ’-- Display
601โˆ’
602โˆ’imageDemo :: DemoProps -> JSX
603โˆ’imageDemo = component "ImageDemo" \dp -> React.do
604โˆ’ pure do
605โˆ’ scrollWrap dp
606โˆ’ [ sectionTitle dp.fg "Image"
607โˆ’ , desc dp "Native NSImageView with URL loading and corner radius"
608โˆ’ , nativeImage
609โˆ’ { source: "https://placedog.net/640/400"
610โˆ’ , contentMode: T.scaleProportionally
611โˆ’ , cornerRadius: 12.0
612โˆ’ , style: Style.style { height: 200.0 } <> tw "mb-2"
613โˆ’ }
614โˆ’ ]
615โˆ’
616โˆ’animatedImageDemo :: DemoProps -> JSX
617โˆ’animatedImageDemo = component "AnimatedImageDemo" \dp -> React.do
618โˆ’ pure do
619โˆ’ scrollWrap dp
620โˆ’ [ sectionTitle dp.fg "Animated Image"
621โˆ’ , desc dp "Native NSImageView with animated GIF support"
622โˆ’ , nativeAnimatedImage
623โˆ’ { source: "https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif"
624โˆ’ , animating: true
625โˆ’ , style: Style.style { height: 200.0 } <> tw "rounded-lg overflow-hidden mb-2"
626โˆ’ }
627โˆ’ , nativeAnimatedImage
628โˆ’ { source: "https://media.giphy.com/media/13CoXDiaCcCoyk/giphy.gif"
629โˆ’ , animating: true
630โˆ’ , style: Style.style { height: 200.0 } <> tw "rounded-lg overflow-hidden mb-2"
631โˆ’ }
632โˆ’ ]
633โˆ’
634โˆ’levelIndicatorDemo :: DemoProps -> JSX
635โˆ’levelIndicatorDemo = component "LevelIndicatorDemo" \dp -> React.do
636โˆ’ value /\ setValue <- useState' 50.0
637โˆ’ pure do
638โˆ’ scrollWrap dp
639โˆ’ [ sectionTitle dp.fg "Level Indicator"
640โˆ’ , card dp.cardBg
641โˆ’ [ nativeSlider
642โˆ’ { value
643โˆ’ , minValue: 0.0
644โˆ’ , maxValue: 100.0
645โˆ’ , numberOfTickMarks: 11
646โˆ’ , onChange: E.onNumber "value" setValue
647โˆ’ , style: Style.style { height: 24.0 }
648โˆ’ }
649โˆ’ , nativeLevelIndicator
650โˆ’ { value
651โˆ’ , minValue: 0.0
652โˆ’ , maxValue: 100.0
653โˆ’ , warningValue: 70.0
654โˆ’ , criticalValue: 90.0
655โˆ’ , style: Style.style { height: 18.0, marginTop: 8.0 }
656โˆ’ }
657โˆ’ ]
658โˆ’ ]
659โˆ’
660โˆ’progressDemo :: DemoProps -> JSX
661โˆ’progressDemo = component "ProgressDemo" \dp -> React.do
662โˆ’ value /\ setValue <- useState' 50.0
663โˆ’ pure do
664โˆ’ scrollWrap dp
665โˆ’ [ sectionTitle dp.fg "Progress"
666โˆ’ , card dp.cardBg
667โˆ’ [ nativeSlider
668โˆ’ { value
669โˆ’ , minValue: 0.0
670โˆ’ , maxValue: 100.0
671โˆ’ , numberOfTickMarks: 11
672โˆ’ , onChange: E.onNumber "value" setValue
673โˆ’ , style: Style.style { height: 24.0 }
674โˆ’ }
675โˆ’ , nativeProgress
676โˆ’ { value
677โˆ’ , style: Style.style { height: 18.0, marginTop: 8.0 }
678โˆ’ }
679โˆ’ ]
680โˆ’ ]
681โˆ’
682โˆ’separatorDemo :: DemoProps -> JSX
683โˆ’separatorDemo = component "SeparatorDemo" \dp -> React.do
684โˆ’ pure do
685โˆ’ scrollWrap dp
686โˆ’ [ sectionTitle dp.fg "Separator"
687โˆ’ , desc dp "Horizontal and vertical separators"
688โˆ’ , nativeSeparator
689โˆ’ { vertical: false
690โˆ’ , style: Style.style { height: 2.0 } <> tw "my-2"
691โˆ’ }
692โˆ’ , text { style: tw "text-xs mb-2" <> Style.style { color: dp.dimFg } } "Content between separators"
693โˆ’ , nativeSeparator
694โˆ’ { vertical: false
695โˆ’ , style: Style.style { height: 2.0 } <> tw "my-2"
696โˆ’ }
697โˆ’ ]
698โˆ’
699โˆ’pathControlDemo :: DemoProps -> JSX
700โˆ’pathControlDemo = component "PathControlDemo" \dp -> React.do
701โˆ’ pure do
702โˆ’ scrollWrap dp
703โˆ’ [ sectionTitle dp.fg "Path Control"
704โˆ’ , nativePathControl
705โˆ’ { url: "/Users"
706โˆ’ , pathStyle: T.standardPath
707โˆ’ , onSelectPath: E.onString "url" \_ -> pure unit
708โˆ’ , style: Style.style { height: 24.0 } <> tw "mb-2"
709โˆ’ }
710โˆ’ ]
711โˆ’
712โˆ’-- Layout & Containers
713โˆ’
714โˆ’boxDemo :: DemoProps -> JSX
715โˆ’boxDemo = component "BoxDemo" \dp -> React.do
716โˆ’ pure do
717โˆ’ scrollWrap dp
718โˆ’ [ sectionTitle dp.fg "Box"
719โˆ’ , desc dp "Native NSBox with title and border"
720โˆ’ , nativeBox
721โˆ’ { boxTitle: "Settings"
722โˆ’ , fillColor2: dp.cardBg
723โˆ’ , borderColor2: dp.dimFg
724โˆ’ , radius: 8.0
725โˆ’ , style: Style.style { height: 100.0 } <> tw "mb-2"
726โˆ’ }
727โˆ’ [ view { style: tw "flex-row items-center p-2" }
728โˆ’ [ text { style: tw "text-sm" <> Style.style { color: dp.fg } } "Content inside a native box" ]
729โˆ’ ]
730โˆ’ ]
731โˆ’
732โˆ’splitViewDemo :: DemoProps -> JSX
733โˆ’splitViewDemo = component "SplitViewDemo" \dp -> React.do
734โˆ’ pure do
735โˆ’ scrollWrap dp
736โˆ’ [ sectionTitle dp.fg "Split View"
737โˆ’ , desc dp "Native NSSplitView with resizable divider"
738โˆ’ , nativeSplitView
739โˆ’ { isVertical: true
740โˆ’ , style: Style.style { height: 150.0 } <> tw "rounded-lg overflow-hidden mb-2"
741โˆ’ }
742โˆ’ [ view { style: tw "flex-1 p-4" <> Style.style { backgroundColor: dp.cardBg } }
743โˆ’ [ text { style: tw "text-sm font-semibold" <> Style.style { color: dp.fg } } "Left Pane"
744โˆ’ , text { style: tw "text-xs mt-1" <> Style.style { color: dp.dimFg } } "Drag the divider to resize"
745โˆ’ ]
746โˆ’ , view { style: tw "flex-1 p-4" <> Style.style { backgroundColor: dp.cardBg } }
747โˆ’ [ text { style: tw "text-sm font-semibold" <> Style.style { color: dp.fg } } "Right Pane"
748โˆ’ , text { style: tw "text-xs mt-1" <> Style.style { color: dp.dimFg } } "Content fills available space"
749โˆ’ ]
750โˆ’ ]
751โˆ’ ]
752โˆ’
753โˆ’tabViewDemo :: DemoProps -> JSX
754โˆ’tabViewDemo = component "TabViewDemo" \dp -> React.do
755โˆ’ selected /\ setSelected <- useState' "general"
756โˆ’ pure do
757โˆ’ scrollWrap dp
758โˆ’ [ sectionTitle dp.fg "Tab View"
759โˆ’ , desc dp "Native segmented tab bar"
760โˆ’ , nativeTabView
761โˆ’ { items:
762โˆ’ [ { id: "general", label: "General" }
763โˆ’ , { id: "advanced", label: "Advanced" }
764โˆ’ , { id: "about", label: "About" }
765โˆ’ ]
766โˆ’ , selectedItem: selected
767โˆ’ , onSelectTab: E.onString "tabId" setSelected
768โˆ’ , style: Style.style { height: 32.0 } <> tw "mb-2"
769โˆ’ }
770โˆ’ ]
771โˆ’
772โˆ’-- Overlays & Dialogs
773โˆ’
774โˆ’alertDemo :: DemoProps -> JSX
775โˆ’alertDemo = component "AlertDemo" \dp -> React.do
776โˆ’ pure do
777โˆ’ scrollWrap dp
778โˆ’ [ sectionTitle dp.fg "Alert"
779โˆ’ , desc dp "Native alert dialog"
780โˆ’ , nativeButton
781โˆ’ { title: "Show Alert"
782โˆ’ , bezelStyle: T.push
783โˆ’ , onPress: handler_ (macosAlert T.warning "Are you sure?" "This action cannot be undone." [ "Cancel", "OK" ])
784โˆ’ , style: Style.style { height: 24.0, width: 120.0 } <> tw "mb-4"
785โˆ’ }
786โˆ’ ]
787โˆ’
788โˆ’sheetDemo :: DemoProps -> JSX
789โˆ’sheetDemo = component "SheetDemo" \dp -> React.do
790โˆ’ visible /\ setVisible <- useState' false
791โˆ’ pure do
792โˆ’ scrollWrap dp
793โˆ’ [ sectionTitle dp.fg "Sheet"
794โˆ’ , desc dp "Modal sheet attached to the window"
795โˆ’ , nativeButton
796โˆ’ { title: "Show Sheet"
797โˆ’ , bezelStyle: T.push
798โˆ’ , onPress: handler_ (setVisible true)
799โˆ’ , style: Style.style { height: 24.0, width: 120.0 } <> tw "mb-2"
800โˆ’ }
801โˆ’ , nativeSheet
802โˆ’ { visible
803โˆ’ , onDismiss: handler_ (setVisible false)
804โˆ’ }
805โˆ’ [ view { style: tw "p-6" <> Style.style { width: 400.0, height: 200.0 } }
806โˆ’ [ text { style: tw "text-lg font-semibold mb-2" <> Style.style { color: dp.fg } } "Sheet Content"
807โˆ’ , text { style: tw "text-sm mb-4" <> Style.style { color: dp.dimFg } } "This is a native macOS sheet."
808โˆ’ , nativeButton
809โˆ’ { title: "Dismiss"
810โˆ’ , bezelStyle: T.push
811โˆ’ , onPress: handler_ (setVisible false)
812โˆ’ , style: Style.style { height: 24.0, width: 100.0 }
813โˆ’ }
814โˆ’ ]
815โˆ’ ]
816โˆ’ ]
817โˆ’
818โˆ’popoverDemo :: DemoProps -> JSX
819โˆ’popoverDemo = component "PopoverDemo" \dp -> React.do
820โˆ’ visible /\ setVisible <- useState' false
821โˆ’ pure do
822โˆ’ scrollWrap dp
823โˆ’ [ sectionTitle dp.fg "Popover"
824โˆ’ , desc dp "Click button to toggle a popover"
825โˆ’ , nativePopover
826โˆ’ { visible
827โˆ’ , preferredEdge: T.bottom
828โˆ’ , behavior: T.transient
829โˆ’ , onClose: handler_ (setVisible false)
830โˆ’ , style: tw "mb-2"
831โˆ’ }
832โˆ’ [ nativeButton
833โˆ’ { title: if visible then "Hide Popover" else "Show Popover"
834โˆ’ , bezelStyle: T.push
835โˆ’ , onPress: handler_ (setVisible (not visible))
836โˆ’ , style: Style.style { height: 24.0, width: 140.0 }
837โˆ’ }
838โˆ’ , view { style: tw "p-4" <> Style.style { width: 200.0, height: 80.0 } }
839โˆ’ [ text { style: tw "text-sm font-semibold" <> Style.style { color: dp.fg } } "Popover Content"
840โˆ’ , text { style: tw "text-xs mt-1" <> Style.style { color: dp.dimFg } } "Click outside to dismiss"
841โˆ’ ]
842โˆ’ ]
843โˆ’ ]
844โˆ’
845โˆ’contextMenuDemo :: DemoProps -> JSX
846โˆ’contextMenuDemo = component "ContextMenuDemo" \dp -> React.do
847โˆ’ result /\ setResult <- useState' ""
848โˆ’ pure do
849โˆ’ scrollWrap dp
850โˆ’ [ sectionTitle dp.fg "Context Menu"
851โˆ’ , desc dp "Right-click the area below to open a context menu"
852โˆ’ , nativeContextMenu
853โˆ’ { items:
854โˆ’ [ { id: "cut", title: "Cut", sfSymbol: "scissors" }
855โˆ’ , { id: "copy", title: "Copy", sfSymbol: "doc.on.doc" }
856โˆ’ , { id: "paste", title: "Paste", sfSymbol: "doc.on.clipboard" }
857โˆ’ , { id: "sep", title: "-", sfSymbol: "" }
858โˆ’ , { id: "delete", title: "Delete", sfSymbol: "trash" }
859โˆ’ , { id: "selectAll", title: "Select All", sfSymbol: "selection.pin.in.out" }
860โˆ’ ]
861โˆ’ , onSelectItem: E.onString "itemId" setResult
862โˆ’ , style: Style.style {}
863โˆ’ }
864โˆ’ ( card dp.cardBg
865โˆ’ [ view { style: tw "items-center justify-center" <> Style.style { minHeight: 80.0 } }
866โˆ’ [ text { style: tw "text-sm" <> Style.style { color: dp.fg } } "Right-click me!"
867โˆ’ , if result == "" then mempty
868โˆ’ else label dp.dimFg ("Selected: " <> result)
869โˆ’ ]
870โˆ’ ]
871โˆ’ )
872โˆ’ ]
873โˆ’
874โˆ’menuDemo :: DemoProps -> JSX
875โˆ’menuDemo = component "MenuDemo" \dp -> React.do
876โˆ’ result /\ setResult <- useState' ""
877โˆ’ pure do
878โˆ’ scrollWrap dp
879โˆ’ [ sectionTitle dp.fg "Menu"
880โˆ’ , desc dp "Imperative popup menu at mouse position"
881โˆ’ , view { style: tw "flex-row items-center mb-2" }
882โˆ’ [ nativeButton
883โˆ’ { title: "Show Menu"
884โˆ’ , bezelStyle: T.push
885โˆ’ , onPress: handler_
886โˆ’ ( macosShowMenu
887โˆ’ [ { title: "New File", id: "new" }
888โˆ’ , { title: "Open...", id: "open" }
889โˆ’ , { title: "-", id: "sep" }
890โˆ’ , { title: "Save", id: "save" }
891โˆ’ , { title: "Save As...", id: "saveAs" }
892โˆ’ ]
893โˆ’ \itemId -> setResult ("Menu: " <> itemId)
894โˆ’ )
895โˆ’ , style: Style.style { height: 24.0, width: 120.0 }
896โˆ’ }
897โˆ’ , if result == "" then mempty
898โˆ’ else text { style: tw "text-xs ml-3" <> Style.style { color: dp.dimFg } } result
899โˆ’ ]
900โˆ’ ]
901โˆ’
902โˆ’-- Data Views
903โˆ’
904โˆ’tableViewDemo :: DemoProps -> JSX
905โˆ’tableViewDemo = component "TableViewDemo" \dp -> React.do
906โˆ’ selected /\ setSelected <- useState' ""
907โˆ’ pure do
908โˆ’ scrollWrap dp
909โˆ’ [ sectionTitle dp.fg "Table View"
910โˆ’ , desc dp "Native NSTableView with columns and rows"
911โˆ’ , nativeTableView
912โˆ’ { columns:
913โˆ’ [ { id: "name", title: "Name", width: 150.0 }
914โˆ’ , { id: "type", title: "Type", width: 100.0 }
915โˆ’ , { id: "size", title: "Size", width: 80.0 }
916โˆ’ ]
917โˆ’ , rows:
918โˆ’ [ [ "Main.purs", "PureScript", "12 KB" ]
919โˆ’ , [ "App.tsx", "TypeScript", "8 KB" ]
920โˆ’ , [ "style.css", "CSS", "3 KB" ]
921โˆ’ , [ "index.html", "HTML", "1 KB" ]
922โˆ’ , [ "package.json", "JSON", "2 KB" ]
923โˆ’ ]
924โˆ’ , headerVisible: true
925โˆ’ , alternatingRows: true
926โˆ’ , onSelectRow: E.onInt "rowIndex" \i -> setSelected ("Row " <> show i)
927โˆ’ , onDoubleClickRow: E.onInt "rowIndex" \i -> setSelected ("Double-clicked row " <> show i)
928โˆ’ , style: Style.style { height: 180.0 } <> tw "rounded-lg overflow-hidden mb-2"
929โˆ’ }
930โˆ’ , if selected == "" then mempty
931โˆ’ else label dp.dimFg selected
932โˆ’ ]
933โˆ’
934โˆ’outlineViewDemo :: DemoProps -> JSX
935โˆ’outlineViewDemo = component "OutlineViewDemo" \dp -> React.do
936โˆ’ selection /\ setSelection <- useState' ""
937โˆ’ pure do
938โˆ’ scrollWrap dp
939โˆ’ [ sectionTitle dp.fg "Outline View"
940โˆ’ , desc dp "Hierarchical tree list (NSOutlineView)"
941โˆ’ , nativeOutlineView
942โˆ’ { items:
943โˆ’ let
944โˆ’ file id title = OutlineItem { id, title, sfSymbol: "doc", children: [] }
945โˆ’ folder id title children = OutlineItem { id, title, sfSymbol: "folder", children }
946โˆ’ in
947โˆ’ [ folder "src" "src"
948โˆ’ [ file "main" "Main.purs"
949โˆ’ , folder "macos" "MacOS"
950โˆ’ [ file "btn" "Button.purs"
951โˆ’ , file "sl" "Slider.purs"
952โˆ’ , file "sw" "Switch.purs"
953โˆ’ ]
954โˆ’ ]
955โˆ’ , folder "test" "test"
956โˆ’ [ file "t1" "MacOSComponents.test.js"
957โˆ’ , file "t2" "MacOSSnapshots.test.js"
958โˆ’ ]
959โˆ’ , OutlineItem { id: "pkg", title: "package.json", sfSymbol: "doc.text", children: [] }
960โˆ’ ]
961โˆ’ , headerVisible: false
962โˆ’ , onSelectItem: E.onString "id" setSelection
963โˆ’ , style: Style.style { height: 200.0 } <> tw "rounded-lg overflow-hidden mb-2"
964โˆ’ }
965โˆ’ , if selection == "" then mempty
966โˆ’ else label dp.dimFg ("Selected: " <> selection)
967โˆ’ ]
968โˆ’
969โˆ’-- Drag & Drop / Files
970โˆ’
971โˆ’dropZoneDemo :: DemoProps -> JSX
972โˆ’dropZoneDemo = component "DropZoneDemo" \dp -> React.do
973โˆ’ status /\ setStatus <- useState' "Drop files here"
974โˆ’ dropped /\ setDropped <- useState' ""
975โˆ’ dragging /\ setDragging <- useState' false
976โˆ’ pure do
977โˆ’ let accentBorder = if dragging then "#007AFF" else dp.dimFg
978โˆ’ scrollWrap dp
979โˆ’ [ sectionTitle dp.fg "Drop Zone"
980โˆ’ , desc dp "Drag files from Finder into the area below"
981โˆ’ , view
982โˆ’ { draggedTypes: [ "NSFilenamesPboardType" ]
983โˆ’ , onDrop: handler
984โˆ’ (nativeEvent >>> unsafeEventFn \e -> getFieldJSON "dataTransfer" e)
985โˆ’ \r -> do
986โˆ’ setDropped r
987โˆ’ setStatus "Drop files here"
988โˆ’ setDragging false
989โˆ’ , onDragEnter: handler_ do
990โˆ’ setStatus "Release to drop!"
991โˆ’ setDragging true
992โˆ’ , onDragLeave: handler_ do
993โˆ’ setStatus "Drop files here"
994โˆ’ setDragging false
995โˆ’ , style: tw "rounded-lg items-center justify-center mb-2"
996โˆ’ <> Style.style
997โˆ’ { minHeight: 100.0
998โˆ’ , borderWidth: 2.0
999โˆ’ , borderColor: accentBorder
1000โˆ’ , backgroundColor: dp.cardBg
1001โˆ’ }
1002โˆ’ }
1003โˆ’ [ text { style: tw "text-sm" <> Style.style { color: dp.fg } } status
1004โˆ’ , if dropped == "" then mempty
1005โˆ’ else text { style: tw "text-xs mt-2 px-4" <> Style.style { color: dp.dimFg } }
1006โˆ’ ("Dropped: " <> dropped)
1007โˆ’ ]
1008โˆ’ ]
1009โˆ’
1010โˆ’filePickerDemo :: DemoProps -> JSX
1011โˆ’filePickerDemo = component "FilePickerDemo" \dp -> React.do
1012โˆ’ picked /\ setPicked <- useState' ""
1013โˆ’ pure do
1014โˆ’ scrollWrap dp
1015โˆ’ [ sectionTitle dp.fg "File Picker"
1016โˆ’ , desc dp "Click buttons to open native file panels"
1017โˆ’ , card dp.cardBg
1018โˆ’ [ view { style: tw "flex-row items-center" }
1019โˆ’ [ nativeFilePicker
1020โˆ’ { mode: T.openFile
1021โˆ’ , title: "Open Files"
1022โˆ’ , sfSymbol: "doc.badge.plus"
1023โˆ’ , allowMultiple: true
1024โˆ’ , allowedTypes: [ "public.image", "public.text" ]
1025โˆ’ , message: "Select files to open"
1026โˆ’ , onPickFiles: handler
1027โˆ’ (nativeEvent >>> unsafeEventFn \e -> getFieldJSON "files" e)
1028โˆ’ setPicked
1029โˆ’ , onCancel: handler_ (setPicked "Cancelled")
1030โˆ’ , style: Style.style { height: 24.0, width: 120.0 }
1031โˆ’ }
1032โˆ’ , nativeFilePicker
1033โˆ’ { mode: T.openFile
1034โˆ’ , title: "Choose Folder"
1035โˆ’ , sfSymbol: "folder"
1036โˆ’ , canChooseDirectories: true
1037โˆ’ , message: "Select a folder"
1038โˆ’ , onPickFiles: handler
1039โˆ’ (nativeEvent >>> unsafeEventFn \e -> getFieldJSON "files" e)
1040โˆ’ setPicked
1041โˆ’ , onCancel: handler_ (setPicked "Cancelled")
1042โˆ’ , style: Style.style { height: 24.0, width: 140.0, marginLeft: 8.0 }
1043โˆ’ }
1044โˆ’ , nativeFilePicker
1045โˆ’ { mode: T.saveFile
1046โˆ’ , title: "Save As..."
1047โˆ’ , sfSymbol: "square.and.arrow.down"
1048โˆ’ , defaultName: "Untitled.txt"
1049โˆ’ , allowedTypes: [ "public.plain-text" ]
1050โˆ’ , message: "Choose save location"
1051โˆ’ , onPickFiles: handler
1052โˆ’ (nativeEvent >>> unsafeEventFn \e -> getFieldJSON "files" e)
1053โˆ’ setPicked
1054โˆ’ , onCancel: handler_ (setPicked "Cancelled")
1055โˆ’ , style: Style.style { height: 24.0, width: 120.0, marginLeft: 8.0 }
1056โˆ’ }
1057โˆ’ ]
1058โˆ’ , if picked == "" then mempty
1059โˆ’ else label dp.dimFg picked
1060โˆ’ ]
1061โˆ’ ]
1062โˆ’
1063โˆ’-- Animation
1064โˆ’
1065โˆ’riveDemo :: DemoProps -> JSX
1066โˆ’riveDemo = component "RiveDemo" \dp -> React.do
1067โˆ’ pure do
1068โˆ’ nativeScrollView { style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
1069โˆ’ ( view { style: tw "px-4 pb-4" }
1070โˆ’ [ sectionTitle dp.fg "Mouse Tracking"
1071โˆ’ , text { style: tw "text-xs mb-2" <> Style.style { color: dp.fg } }
1072โˆ’ "Move your cursor over the cat โ€” it follows your mouse!"
1073โˆ’ , nativeRiveView_
1074โˆ’ { resourceName: "cat_following_mouse"
1075โˆ’ , stateMachineName: "State Machine 1"
1076โˆ’ , fit: T.contain
1077โˆ’ , autoplay: true
1078โˆ’ , style: Style.style { height: 300.0 }
1079โˆ’ }
1080โˆ’ , sectionTitle dp.fg "Cursor Tracking"
1081โˆ’ , text { style: tw "text-xs mb-2" <> Style.style { color: dp.fg } }
1082โˆ’ "Shapes follow your cursor via Pointer Move listeners"
1083โˆ’ , nativeRiveView_
1084โˆ’ { resourceName: "cursor_tracking"
1085โˆ’ , stateMachineName: "State Machine 1"
1086โˆ’ , fit: T.contain
1087โˆ’ , autoplay: true
1088โˆ’ , style: Style.style { height: 300.0 }
1089โˆ’ }
1090โˆ’ , sectionTitle dp.fg "Interactive Rive Animations"
1091โˆ’ , text { style: tw "text-xs mb-2" <> Style.style { color: dp.fg } }
1092โˆ’ "Click the stars to rate! State machine handles interaction."
1093โˆ’ , nativeRiveView_
1094โˆ’ { resourceName: "rating_animation"
1095โˆ’ , stateMachineName: "State Machine 1"
1096โˆ’ , fit: T.contain
1097โˆ’ , autoplay: true
1098โˆ’ , style: Style.style { height: 200.0 }
1099โˆ’ }
1100โˆ’ , sectionTitle dp.fg "Light Switch"
1101โˆ’ , text { style: tw "text-xs mb-2" <> Style.style { color: dp.fg } }
1102โˆ’ "Click to toggle"
1103โˆ’ , nativeRiveView_
1104โˆ’ { resourceName: "switch_event_example"
1105โˆ’ , stateMachineName: "State Machine 1"
1106โˆ’ , fit: T.contain
1107โˆ’ , autoplay: true
1108โˆ’ , style: Style.style { height: 200.0 }
1109โˆ’ }
1110โˆ’ , sectionTitle dp.fg "Windy Tree"
1111โˆ’ , nativeRiveView_
1112โˆ’ { resourceName: "windy_tree"
1113โˆ’ , fit: T.cover
1114โˆ’ , autoplay: true
1115โˆ’ , style: tw "flex-1" <> Style.style { minHeight: 300.0, backgroundColor: dp.bg }
1116โˆ’ }
1117โˆ’ ]
1118โˆ’ )
1119โˆ’
1120โˆ’-- System Services
1121โˆ’
1122โˆ’clipboardDemo :: DemoProps -> JSX
1123โˆ’clipboardDemo = component "ClipboardDemo" \dp -> React.do
1124โˆ’ pure do
1125โˆ’ scrollWrap dp
1126โˆ’ [ sectionTitle dp.fg "Clipboard"
1127โˆ’ , desc dp "Read and write the system clipboard"
1128โˆ’ , view { style: tw "flex-row items-center mb-2" }
1129โˆ’ [ nativeButton
1130โˆ’ { title: "Copy 'Hello'"
1131โˆ’ , bezelStyle: T.push
1132โˆ’ , onPress: handler_ (copyToClipboard "Hello from PureScript!")
1133โˆ’ , style: Style.style { height: 24.0, width: 120.0 }
1134โˆ’ }
1135โˆ’ ]
1136โˆ’ ]
1137โˆ’
1138โˆ’shareDemo :: DemoProps -> JSX
1139โˆ’shareDemo = component "ShareDemo" \dp -> React.do
1140โˆ’ pure do
1141โˆ’ scrollWrap dp
1142โˆ’ [ sectionTitle dp.fg "Share"
1143โˆ’ , desc dp "System share picker"
1144โˆ’ , nativeButton
1145โˆ’ { title: "Share Text"
1146โˆ’ , sfSymbol: "square.and.arrow.up"
1147โˆ’ , bezelStyle: T.push
1148โˆ’ , onPress: handler_ (macosShare [ "Hello from PureScript React Native!" ])
1149โˆ’ , style: Style.style { height: 24.0, width: 120.0 } <> tw "mb-2"
1150โˆ’ }
1151โˆ’ ]
1152โˆ’
1153โˆ’notificationsDemo :: DemoProps -> JSX
1154โˆ’notificationsDemo = component "NotificationsDemo" \dp -> React.do
1155โˆ’ pure do
1156โˆ’ scrollWrap dp
1157โˆ’ [ sectionTitle dp.fg "Notifications"
1158โˆ’ , desc dp "System notifications (requires permission)"
1159โˆ’ , nativeButton
1160โˆ’ { title: "Send Notification"
1161โˆ’ , sfSymbol: "bell"
1162โˆ’ , bezelStyle: T.push
1163โˆ’ , onPress: handler_ (macosNotify "PureScript" "Hello from React Native macOS!")
1164โˆ’ , style: Style.style { height: 24.0, width: 160.0 } <> tw "mb-2"
1165โˆ’ }
1166โˆ’ ]
1167โˆ’
1168โˆ’soundDemo :: DemoProps -> JSX
1169โˆ’soundDemo = component "SoundDemo" \dp -> React.do
1170โˆ’ pure do
1171โˆ’ scrollWrap dp
1172โˆ’ [ sectionTitle dp.fg "Sound"
1173โˆ’ , desc dp "Play system sounds"
1174โˆ’ , view { style: tw "flex-row items-center mb-2" }
1175โˆ’ [ nativeButton
1176โˆ’ { title: "Glass"
1177โˆ’ , bezelStyle: T.push
1178โˆ’ , onPress: handler_ (macosPlaySound "Glass")
1179โˆ’ , style: Style.style { height: 24.0, width: 80.0 }
1180โˆ’ }
1181โˆ’ , nativeButton
1182โˆ’ { title: "Ping"
1183โˆ’ , bezelStyle: T.push
1184โˆ’ , onPress: handler_ (macosPlaySound "Ping")
1185โˆ’ , style: Style.style { height: 24.0, width: 80.0, marginLeft: 8.0 }
1186โˆ’ }
1187โˆ’ , nativeButton
1188โˆ’ { title: "Beep"
1189โˆ’ , bezelStyle: T.push
1190โˆ’ , onPress: handler_ macosBeep
1191โˆ’ , style: Style.style { height: 24.0, width: 80.0, marginLeft: 8.0 }
1192โˆ’ }
1193โˆ’ ]
1194โˆ’ ]
1195โˆ’
1196โˆ’statusBarDemo :: DemoProps -> JSX
1197โˆ’statusBarDemo = component "StatusBarDemo" \dp -> React.do
1198โˆ’ active /\ setActive <- useState' false
1199โˆ’ pure do
1200โˆ’ scrollWrap dp
1201โˆ’ [ sectionTitle dp.fg "Status Bar"
1202โˆ’ , desc dp "Menu bar icon with dropdown"
1203โˆ’ , view { style: tw "flex-row items-center mb-4" }
1204โˆ’ [ nativeButton
1205โˆ’ { title: if active then "Remove" else "Add to Menu Bar"
1206โˆ’ , sfSymbol: if active then "minus.circle" else "plus.circle"
1207โˆ’ , bezelStyle: T.push
1208โˆ’ , onPress: handler_
1209โˆ’ ( if active then do
1210โˆ’ macosRemoveStatusBarItem
1211โˆ’ setActive false
1212โˆ’ else do
1213โˆ’ macosSetStatusBarItem
1214โˆ’ { title: ""
1215โˆ’ , sfSymbol: "swift"
1216โˆ’ , menuItems:
1217โˆ’ [ { id: "about", title: "About PureScript RN" }
1218โˆ’ , { id: "sep", title: "-" }
1219โˆ’ , { id: "quit", title: "Quit" }
1220โˆ’ ]
1221โˆ’ }
1222โˆ’ setActive true
1223โˆ’ )
1224โˆ’ , style: Style.style { height: 24.0, width: 180.0 }
1225โˆ’ }
1226โˆ’ ]
1227โˆ’ ]
1228โˆ’
1229โˆ’quickLookDemo :: DemoProps -> JSX
1230โˆ’quickLookDemo = component "QuickLookDemo" \dp -> React.do
1231โˆ’ pure do
1232โˆ’ scrollWrap dp
1233โˆ’ [ sectionTitle dp.fg "Quick Look"
1234โˆ’ , desc dp "Preview files with QLPreviewPanel"
1235โˆ’ , view { style: tw "flex-row items-center mb-4" }
1236โˆ’ [ nativeButton
1237โˆ’ { title: "Preview /etc/hosts"
1238โˆ’ , sfSymbol: "eye"
1239โˆ’ , bezelStyle: T.push
1240โˆ’ , onPress: handler_ (macosQuickLook "/etc/hosts")
1241โˆ’ , style: Style.style { height: 24.0, width: 180.0 }
1242โˆ’ }
1243โˆ’ ]
1244โˆ’ ]
1245โˆ’
1246โˆ’colorPanelDemo :: DemoProps -> JSX
1247โˆ’colorPanelDemo = component "ColorPanelDemo" \dp -> React.do
1248โˆ’ pure do
1249โˆ’ scrollWrap dp
1250โˆ’ [ sectionTitle dp.fg "Color Picker"
1251โˆ’ , desc dp "System color panel (NSColorPanel)"
1252โˆ’ , view { style: tw "flex-row items-center mb-4" }
1253โˆ’ [ nativeButton
1254โˆ’ { title: "Show Color Panel"
1255โˆ’ , sfSymbol: "paintpalette"
1256โˆ’ , bezelStyle: T.push
1257โˆ’ , onPress: handler_ macosShowColorPanel
1258โˆ’ , style: Style.style { height: 24.0, width: 180.0 }
1259โˆ’ }
1260โˆ’ ]
1261โˆ’ ]
1262โˆ’
1263โˆ’fontPanelDemo :: DemoProps -> JSX
1264โˆ’fontPanelDemo = component "FontPanelDemo" \dp -> React.do
1265โˆ’ pure do
1266โˆ’ scrollWrap dp
1267โˆ’ [ sectionTitle dp.fg "Font Panel"
1268โˆ’ , desc dp "System font panel (NSFontPanel)"
1269โˆ’ , view { style: tw "flex-row items-center mb-4" }
1270โˆ’ [ nativeButton
1271โˆ’ { title: "Show Font Panel"
1272โˆ’ , sfSymbol: "textformat"
1273โˆ’ , bezelStyle: T.push
1274โˆ’ , onPress: handler_ macosShowFontPanel
1275โˆ’ , style: Style.style { height: 24.0, width: 180.0 }
1276โˆ’ }
1277โˆ’ ]
1278โˆ’ ]
1279โˆ’
1280โˆ’speechDemo :: DemoProps -> JSX
1281โˆ’speechDemo = component "SpeechDemo" \dp -> React.do
1282โˆ’ txt /\ setTxt <- useState' "Hello from PureScript React Native on macOS!"
1283โˆ’ pure do
1284โˆ’ scrollWrap dp
1285โˆ’ [ sectionTitle dp.fg "Speech Synthesis"
1286โˆ’ , desc dp "Text-to-speech (NSSpeechSynthesizer)"
1287โˆ’ , nativeTextField
1288โˆ’ { text: txt
1289โˆ’ , placeholder: "Text to speak..."
1290โˆ’ , onChangeText: E.onString "text" setTxt
1291โˆ’ , style: Style.style { height: 24.0 } <> tw "mb-2"
1292โˆ’ }
1293โˆ’ , view { style: tw "flex-row items-center mb-4" }
1294โˆ’ [ nativeButton
1295โˆ’ { title: "Speak"
1296โˆ’ , sfSymbol: "speaker.wave.2"
1297โˆ’ , bezelStyle: T.push
1298โˆ’ , onPress: handler_ (say txt)
1299โˆ’ , style: Style.style { height: 24.0, width: 100.0 }
1300โˆ’ }
1301โˆ’ , nativeButton
1302โˆ’ { title: "Stop"
1303โˆ’ , sfSymbol: "stop.circle"
1304โˆ’ , bezelStyle: T.push
1305โˆ’ , onPress: handler_ stopSpeaking
1306โˆ’ , style: Style.style { height: 24.0, width: 100.0, marginLeft: 8.0 }
1307โˆ’ }
1308โˆ’ ]
1309โˆ’ ]
1310โˆ’
1311โˆ’-- AI & ML
1312โˆ’
1313โˆ’ocrDemo :: DemoProps -> JSX
1314โˆ’ocrDemo = component "OCRDemo" \dp -> React.do
1315โˆ’ result /\ setResult <- useState' ""
1316โˆ’ pure do
1317โˆ’ scrollWrap dp
1318โˆ’ [ sectionTitle dp.fg "OCR (Vision)"
1319โˆ’ , desc dp "Recognize text in images (VNRecognizeTextRequest)"
1320โˆ’ , view { style: tw "flex-row items-center mb-2" }
1321โˆ’ [ nativeFilePicker
1322โˆ’ { mode: T.openFile
1323โˆ’ , allowedTypes: [ "public.image" ]
1324โˆ’ , title: "Pick Image"
1325โˆ’ , sfSymbol: "photo"
1326โˆ’ , onPickFiles: handler
1327โˆ’ (nativeEvent >>> unsafeEventFn \e -> E.getFieldArray "files" e)
1328โˆ’ \paths -> for_ paths \path -> do
1329โˆ’ setResult "Recognizing..."
1330โˆ’ launchAff_ do
1331โˆ’ r <- recognizeText path
1332โˆ’ liftEffect (setResult r)
1333โˆ’ , style: Style.style { height: 24.0, width: 140.0 }
1334โˆ’ }
1335โˆ’ ]
1336โˆ’ , if result /= "" then view { style: tw "p-2 rounded mb-4" <> Style.style { backgroundColor: dp.cardBg } }
1337โˆ’ [ text { style: tw "text-xs font-mono" <> Style.style { color: dp.fg } } result ]
1338โˆ’ else mempty
1339โˆ’ ]
1340โˆ’
1341โˆ’speechRecognitionDemo :: DemoProps -> JSX
1342โˆ’speechRecognitionDemo = component "SpeechRecognitionDemo" \dp -> React.do
1343โˆ’ speech <- useSpeechRecognition
1344โˆ’ pure do
1345โˆ’ scrollWrap dp
1346โˆ’ [ sectionTitle dp.fg "Speech Recognition"
1347โˆ’ , desc dp "Live microphone-to-text (click Start, speak, click Stop)"
1348โˆ’ , view { style: tw "flex-row items-center mb-2" }
1349โˆ’ [ nativeButton
1350โˆ’ { title: if speech.listening then "Listening..." else "Start"
1351โˆ’ , sfSymbol: if speech.listening then "mic.fill" else "mic"
1352โˆ’ , bezelStyle: T.push
1353โˆ’ , onPress: handler_ speech.start
1354โˆ’ , style: Style.style { height: 24.0, width: 130.0 }
1355โˆ’ }
1356โˆ’ , nativeButton
1357โˆ’ { title: "Stop"
1358โˆ’ , sfSymbol: "stop.circle"
1359โˆ’ , bezelStyle: T.push
1360โˆ’ , onPress: handler_ speech.stop
1361โˆ’ , style: Style.style { height: 24.0, width: 80.0, marginLeft: 8.0 }
1362โˆ’ }
1363โˆ’ ]
1364โˆ’ , if speech.transcript /= "" then view { style: tw "p-2 rounded mb-4" <> Style.style { backgroundColor: dp.cardBg } }
1365โˆ’ [ text { style: tw "text-xs font-mono" <> Style.style { color: dp.fg } } speech.transcript ]
1366โˆ’ else mempty
1367โˆ’ ]
1368โˆ’
1369โˆ’naturalLanguageDemo :: DemoProps -> JSX
1370โˆ’naturalLanguageDemo = component "NaturalLanguageDemo" \dp -> React.do
1371โˆ’ txt /\ setTxt <- useState' "I love this amazing app! C'est magnifique."
1372โˆ’ result /\ setResult <- useState' ""
1373โˆ’ pure do
1374โˆ’ scrollWrap dp
1375โˆ’ [ sectionTitle dp.fg "Natural Language"
1376โˆ’ , desc dp "Language detection, sentiment, tokenization (NaturalLanguage.framework)"
1377โˆ’ , nativeTextField
1378โˆ’ { text: txt
1379โˆ’ , placeholder: "Enter text to analyze..."
1380โˆ’ , onChangeText: E.onString "text" setTxt
1381โˆ’ , style: Style.style { height: 24.0 } <> tw "mb-2"
1382โˆ’ }
1383โˆ’ , view { style: tw "flex-row items-center mb-2" }
1384โˆ’ [ nativeButton
1385โˆ’ { title: "Language"
1386โˆ’ , sfSymbol: "globe"
1387โˆ’ , bezelStyle: T.push
1388โˆ’ , onPress: handler_ $ launchAff_ do
1389โˆ’ lang <- detectLanguage txt
1390โˆ’ liftEffect (setResult ("Language: " <> lang))
1391โˆ’ , style: Style.style { height: 24.0, width: 110.0 }
1392โˆ’ }
1393โˆ’ , nativeButton
1394โˆ’ { title: "Sentiment"
1395โˆ’ , sfSymbol: "face.smiling"
1396โˆ’ , bezelStyle: T.push
1397โˆ’ , onPress: handler_ $ launchAff_ do
1398โˆ’ score <- analyzeSentiment txt
1399โˆ’ liftEffect (setResult ("Sentiment: " <> show score))
1400โˆ’ , style: Style.style { height: 24.0, width: 110.0, marginLeft: 8.0 }
1401โˆ’ }
1402โˆ’ , nativeButton
1403โˆ’ { title: "Tokenize"
1404โˆ’ , sfSymbol: "text.word.spacing"
1405โˆ’ , bezelStyle: T.push
1406โˆ’ , onPress: handler_ $ launchAff_ do
1407โˆ’ tokens <- tokenize txt
1408โˆ’ liftEffect (setResult ("Tokens: " <> joinWith ", " tokens))
1409โˆ’ , style: Style.style { height: 24.0, width: 110.0, marginLeft: 8.0 }
1410โˆ’ }
1411โˆ’ ]
1412โˆ’ , if result /= "" then view { style: tw "p-2 rounded mb-4" <> Style.style { backgroundColor: dp.cardBg } }
1413โˆ’ [ text { style: tw "text-xs font-mono" <> Style.style { color: dp.fg } } result ]
1414โˆ’ else mempty
1415โˆ’ ]
1416โˆ’
1417โˆ’cameraDemo :: DemoProps -> JSX
1418โˆ’cameraDemo = component "CameraDemo" \dp -> React.do
1419โˆ’ on /\ setOn <- useState' false
1420โˆ’ pure do
1421โˆ’ scrollWrap dp
1422โˆ’ [ sectionTitle dp.fg "Camera"
1423โˆ’ , desc dp "Live camera preview (AVCaptureVideoPreviewLayer)"
1424โˆ’ , nativeButton
1425โˆ’ { title: if on then "Stop Camera" else "Start Camera"
1426โˆ’ , sfSymbol: if on then "video.slash" else "video"
1427โˆ’ , bezelStyle: T.push
1428โˆ’ , onPress: handler_ (setOn (not on))
1429โˆ’ , style: Style.style { height: 24.0, width: 140.0 } <> tw "mb-2"
1430โˆ’ }
1431โˆ’ , if on then nativeCameraView
1432โˆ’ { active: true
1433โˆ’ , style: Style.style { height: 240.0 } <> tw "rounded-lg overflow-hidden mb-2"
1434โˆ’ }
1435โˆ’ else mempty
1436โˆ’ ]
1437โˆ’
1438โˆ’-- Maps & Documents
1439โˆ’
1440โˆ’mapViewDemo :: DemoProps -> JSX
1441โˆ’mapViewDemo = component "MapViewDemo" \dp -> React.do
1442โˆ’ pure do
1443โˆ’ scrollWrap dp
1444โˆ’ [ sectionTitle dp.fg "Map View"
1445โˆ’ , desc dp "Embedded MKMapView (San Francisco)"
1446โˆ’ , nativeMapView
1447โˆ’ { latitude: 37.7749
1448โˆ’ , longitude: -122.4194
1449โˆ’ , latitudeDelta: 0.05
1450โˆ’ , longitudeDelta: 0.05
1451โˆ’ , mapType: T.standardMap
1452โˆ’ , showsUserLocation: false
1453โˆ’ , annotations:
1454โˆ’ [ { latitude: 37.7749, longitude: -122.4194, title: "San Francisco", subtitle: "California" }
1455โˆ’ , { latitude: 37.8199, longitude: -122.4783, title: "Golden Gate Bridge", subtitle: "" }
1456โˆ’ ]
1457โˆ’ , style: Style.style { height: 250.0 }
1458โˆ’ }
1459โˆ’ ]
1460โˆ’
1461โˆ’pdfViewDemo :: DemoProps -> JSX
1462โˆ’pdfViewDemo = component "PDFViewDemo" \dp -> React.do
1463โˆ’ pure do
1464โˆ’ scrollWrap dp
1465โˆ’ [ sectionTitle dp.fg "PDF Viewer"
1466โˆ’ , desc dp "PDFKit document viewer"
1467โˆ’ , nativePDFView
1468โˆ’ { source: "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
1469โˆ’ , autoScales: true
1470โˆ’ , displayMode: T.singlePageContinuous
1471โˆ’ , style: Style.style { height: 300.0 }
1472โˆ’ }
1473โˆ’ ]
1474โˆ’
1475โˆ’-- Web Browser
1476โˆ’
1477โˆ’webViewDemo :: DemoProps -> JSX
1478โˆ’webViewDemo = component "WebViewDemo" \dp -> React.do
1479โˆ’ url /\ setUrl <- useState' "https://pursuit.purescript.org"
1480โˆ’ urlBar /\ setUrlBar <- useState' "https://pursuit.purescript.org"
1481โˆ’ pure do
1482โˆ’ view { style: tw "flex-1 px-4" }
1483โˆ’ [ sectionTitle dp.fg "Web Browser"
1484โˆ’ , nativeTextField
1485โˆ’ { text: urlBar
1486โˆ’ , placeholder: "Enter URL..."
1487โˆ’ , search: false
1488โˆ’ , onChangeText: E.onString "text" setUrlBar
1489โˆ’ , onSubmit: E.onString "text" setUrl
1490โˆ’ , style: Style.style { height: 24.0, marginBottom: 8.0 }
1491โˆ’ }
1492โˆ’ , nativeWebView
1493โˆ’ { url
1494โˆ’ , onFinishLoad: handler
1495โˆ’ ( nativeEvent >>> unsafeEventFn \e ->
1496โˆ’ { url: E.getFieldStr "url" e, title: E.getFieldStr "title" e }
1497โˆ’ )
1498โˆ’ \r -> setUrlBar r.url
1499โˆ’ , style: tw "flex-1" <> Style.style { minHeight: 400.0 }
1500โˆ’ }
1501โˆ’ ]
1502โˆ’
1503โˆ’-- Video Player
1504โˆ’
1505โˆ’videoPlayerDemo :: DemoProps -> JSX
1506โˆ’videoPlayerDemo = component "VideoPlayerDemo" \dp -> React.do
1507โˆ’ playing /\ setPlaying <- useState' true
1508โˆ’ pure do
1509โˆ’ scrollWrap dp
1510โˆ’ [ sectionTitle dp.fg "Video Player"
1511โˆ’ , desc dp "Native AVPlayerView with floating controls"
1512โˆ’ , nativeVideoPlayer
1513โˆ’ { source: "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/720/Big_Buck_Bunny_720_10s_1MB.mp4"
1514โˆ’ , playing
1515โˆ’ , looping: true
1516โˆ’ , muted: false
1517โˆ’ , style: Style.style { height: 240.0 } <> tw "rounded-lg overflow-hidden mb-2"
1518โˆ’ }
1519โˆ’ , view { style: tw "flex-row mb-2" }
1520โˆ’ [ nativeButton
1521โˆ’ { title: if playing then "Pause" else "Play"
1522โˆ’ , sfSymbol: if playing then "pause.fill" else "play.fill"
1523โˆ’ , bezelStyle: T.push
1524โˆ’ , onPress: handler_ (setPlaying (not playing))
1525โˆ’ , style: Style.style { height: 24.0, width: 100.0 }
1526โˆ’ }
1527โˆ’ ]
1528โˆ’ ]
1529โˆ’
1530โˆ’-- Chat (Matrix)
1531โˆ’
1532โˆ’type Message =
1533โˆ’ { sender :: String
1534โˆ’ , body :: String
1535โˆ’ , kind :: String
1536โˆ’ , eventId :: String
1537โˆ’ }
1538โˆ’
1539โˆ’type Session =
1540โˆ’ { homeserver :: String
1541โˆ’ , accessToken :: String
1542โˆ’ , userId :: String
1543โˆ’ }
1544โˆ’
1545โˆ’type Room =
1546โˆ’ { roomId :: String
1547โˆ’ , name :: String
1548โˆ’ , lastMessage :: String
1549โˆ’ }
1550โˆ’
1551โˆ’avatarColors :: Array String
1552โˆ’avatarColors = [ "#5856D6", "#FF9500", "#34C759", "#FF2D55", "#5AC8FA", "#FF6B6B", "#4ECDC4", "#45B7D1" ]
1553โˆ’
1554โˆ’avatarColor :: String -> String
1555โˆ’avatarColor roomId = fromMaybe "#5856D6" (avatarColors !! (mod (hashCode roomId) (length avatarColors)))
1556โˆ’ where
1557โˆ’ hashCode s = abs (foldl (\acc c -> acc * 31 + c) 0 (charCodes s))
1558โˆ’
1559โˆ’foreign import charCodes :: String -> Array Int
1560โˆ’foreign import abs :: Int -> Int
1561โˆ’
1562โˆ’matrixDemo :: DemoProps -> JSX
1563โˆ’matrixDemo = component "MatrixDemo" \dp -> React.do
1564โˆ’ session /\ setSession <- useState' (Nothing :: Maybe Session)
1565โˆ’ rooms /\ setRooms <- useState' ([] :: Array Room)
1566โˆ’ activeRoom /\ setActiveRoom <- useState' (Nothing :: Maybe String)
1567โˆ’ messages /\ setMessages <- useState ([] :: Array Message)
1568โˆ’ inputText /\ setInputText <- useState' ""
1569โˆ’ loginError /\ setLoginError <- useState' ""
1570โˆ’ loginServer /\ setLoginServer <- useState' "https://matrix.org"
1571โˆ’ loginUser /\ setLoginUser <- useState' ""
1572โˆ’ loginPass /\ setLoginPass <- useState' ""
1573โˆ’ syncTokenRef <- useRef ""
1574โˆ’ activeRoomRef <- useRef (Nothing :: Maybe String)
1575โˆ’ sessionRef <- useRef (Nothing :: Maybe Session)
1576โˆ’ seenIdsRef <- useRef ([] :: Array String)
1577โˆ’
1578โˆ’ useEffect session do
1579โˆ’ case session of
1580โˆ’ Nothing -> pure mempty
1581โˆ’ Just sess -> do
1582โˆ’ writeRef sessionRef (Just sess)
1583โˆ’ let
1584โˆ’ doSync = launchAff_ do
1585โˆ’ token <- readRef syncTokenRef # liftEffect
1586โˆ’ result <- try (Matrix.sync sess.homeserver sess.accessToken token 5000)
1587โˆ’ case result of
1588โˆ’ Left _ -> pure unit
1589โˆ’ Right resp -> liftEffect do
1590โˆ’ let nextBatch = Matrix.getSyncNextBatch resp
1591โˆ’ writeRef syncTokenRef nextBatch
1592โˆ’ let roomIds = Matrix.getSyncJoinedRoomIds resp
1593โˆ’ currentRoom <- readRef activeRoomRef
1594โˆ’ for_ roomIds \rid -> do
1595โˆ’ let events = Matrix.getSyncTimelineEvents resp rid
1596โˆ’ let msgEvents = filter (\ev -> Matrix.getEventType ev == "m.room.message") events
1597โˆ’ case currentRoom of
1598โˆ’ Just cr | cr == rid -> do
1599โˆ’ seen <- readRef seenIdsRef
1600โˆ’ let newMsgs = filter (\ev -> not (Matrix.getEventId ev `elem` seen)) msgEvents
1601โˆ’ let
1602โˆ’ converted = newMsgs <#> \ev ->
1603โˆ’ { sender: Matrix.getEventSender ev
1604โˆ’ , body: Matrix.getEventBody ev
1605โˆ’ , kind: eventKind ev
1606โˆ’ , eventId: Matrix.getEventId ev
1607โˆ’ }
1608โˆ’ if null converted then pure unit
1609โˆ’ else do
1610โˆ’ let newIds = converted <#> _.eventId
1611โˆ’ writeRef seenIdsRef (seen <> newIds)
1612โˆ’ setMessages \msgs -> msgs <> converted
1613โˆ’ _ -> pure unit
1614โˆ’ timerId <- setInterval 3000 doSync
1615โˆ’ doSync
1616โˆ’ pure (clearInterval timerId)
1617โˆ’
1618โˆ’ let
1619โˆ’ doLogin = launchAff_ do
1620โˆ’ result <- try (Matrix.login loginServer loginUser loginPass)
1621โˆ’ case result of
1622โˆ’ Left _ -> liftEffect (setLoginError "Login failed. Check credentials.")
1623โˆ’ Right resp -> do
1624โˆ’ let sess = { homeserver: loginServer, accessToken: resp.access_token, userId: resp.user_id }
1625โˆ’ roomIds <- try (Matrix.joinedRooms loginServer resp.access_token)
1626โˆ’ case roomIds of
1627โˆ’ Left _ -> liftEffect do
1628โˆ’ setSession (Just sess)
1629โˆ’ setLoginError ""
1630โˆ’ Right rids -> do
1631โˆ’ names <- for rids \rid -> do
1632โˆ’ n <- try (Matrix.roomName loginServer resp.access_token rid)
1633โˆ’ let
1634โˆ’ name = case n of
1635โˆ’ Left _ -> rid
1636โˆ’ Right "" -> rid
1637โˆ’ Right nm -> nm
1638โˆ’ pure { roomId: rid, name, lastMessage: "" }
1639โˆ’ liftEffect do
1640โˆ’ setSession (Just sess)
1641โˆ’ setRooms names
1642โˆ’ setLoginError ""
1643โˆ’
1644โˆ’ selectRoom sess rid = do
1645โˆ’ setActiveRoom (Just rid)
1646โˆ’ writeRef activeRoomRef (Just rid)
1647โˆ’ setMessages (const [])
1648โˆ’ writeRef seenIdsRef []
1649โˆ’ launchAff_ do
1650โˆ’ result <- try (Matrix.sync sess.homeserver sess.accessToken "" 0)
1651โˆ’ case result of
1652โˆ’ Left _ -> pure unit
1653โˆ’ Right resp -> liftEffect do
1654โˆ’ let events = Matrix.getSyncTimelineEvents resp rid
1655โˆ’ let msgEvents = filter (\ev -> Matrix.getEventType ev == "m.room.message") events
1656โˆ’ let
1657โˆ’ converted = msgEvents <#> \ev ->
1658โˆ’ { sender: Matrix.getEventSender ev
1659โˆ’ , body: Matrix.getEventBody ev
1660โˆ’ , kind: eventKind ev
1661โˆ’ , eventId: Matrix.getEventId ev
1662โˆ’ }
1663โˆ’ let ids = converted <#> _.eventId
1664โˆ’ writeRef seenIdsRef ids
1665โˆ’ setMessages (const converted)
1666โˆ’
1667โˆ’ sendMessage sess rid msg = do
1668โˆ’ if msg == "" then pure unit
1669โˆ’ else do
1670โˆ’ setInputText ""
1671โˆ’ launchAff_ do
1672โˆ’ txnId <- Matrix.genTxnId # liftEffect
1673โˆ’ _ <- try (Matrix.sendMessage sess.homeserver sess.accessToken rid txnId msg)
1674โˆ’ pure unit
1675โˆ’
1676โˆ’ sentBubbleBg = if dp.isDark then "#65B86A" else "#5CB85C"
1677โˆ’ receivedBubbleBg = if dp.isDark then "#3B3B3D" else "#FFFFFF"
1678โˆ’ chatBg = if dp.isDark then "#17212B" else "#E8DFD3"
1679โˆ’
1680โˆ’ loginForm = view
1681โˆ’ { style: tw "flex-1 items-center justify-center"
1682โˆ’ <> Style.style { backgroundColor: chatBg }
1683โˆ’ }
1684โˆ’ [ view { style: tw "rounded-xl p-6" <> Style.style { backgroundColor: dp.cardBg, width: 340.0 } }
1685โˆ’ [ text { style: tw "text-lg font-bold mb-4 text-center" <> Style.style { color: dp.fg } } "Matrix Login"
1686โˆ’ , text { style: tw "text-xs mb-1" <> Style.style { color: dp.dimFg } } "Homeserver"
1687โˆ’ , nativeTextField
1688โˆ’ { text: loginServer
1689โˆ’ , placeholder: "https://matrix.org"
1690โˆ’ , search: false
1691โˆ’ , rounded: true
1692โˆ’ , onChangeText: E.onString "text" setLoginServer
1693โˆ’ , onSubmit: handler_ (pure unit)
1694โˆ’ , style: tw "mb-3" <> Style.style { height: 28.0 }
1695โˆ’ }
1696โˆ’ , text { style: tw "text-xs mb-1" <> Style.style { color: dp.dimFg } } "Username"
1697โˆ’ , nativeTextField
1698โˆ’ { text: loginUser
1699โˆ’ , placeholder: "username"
1700โˆ’ , search: false
1701โˆ’ , rounded: true
1702โˆ’ , onChangeText: E.onString "text" setLoginUser
1703โˆ’ , onSubmit: handler_ (pure unit)
1704โˆ’ , style: tw "mb-3" <> Style.style { height: 28.0 }
1705โˆ’ }
1706โˆ’ , text { style: tw "text-xs mb-1" <> Style.style { color: dp.dimFg } } "Password"
1707โˆ’ , nativeTextField
1708โˆ’ { text: loginPass
1709โˆ’ , placeholder: "password"
1710โˆ’ , search: false
1711โˆ’ , rounded: true
1712โˆ’ , onChangeText: E.onString "text" setLoginPass
1713โˆ’ , onSubmit: handler_ doLogin
1714โˆ’ , style: tw "mb-4" <> Style.style { height: 28.0 }
1715โˆ’ }
1716โˆ’ , if loginError /= "" then
1717โˆ’ text { style: tw "text-xs mb-2 text-center" <> Style.style { color: "#FF3B30" } } loginError
1718โˆ’ else text { style: Style.style { height: 0.0 } } ""
1719โˆ’ , nativeButton
1720โˆ’ { title: "Sign In"
1721โˆ’ , bezelStyle: T.push
1722โˆ’ , sfSymbol: "arrow.right.circle.fill"
1723โˆ’ , onPress: handler_ doLogin
1724โˆ’ , style: Style.style { height: 32.0 }
1725โˆ’ }
1726โˆ’ ]
1727โˆ’ ]
1728โˆ’
1729โˆ’ roomSidebar sess = view { style: tw "pt-2" }
1730โˆ’ ( rooms <#> \r ->
1731โˆ’ view
1732โˆ’ { style: tw "flex-row items-center px-3 py-2 mx-2 rounded-lg"
1733โˆ’ <> Style.style
1734โˆ’ { backgroundColor: if activeRoom == Just r.roomId then (if dp.isDark then "#2B5278" else "#419FD9") else "transparent" }
1735โˆ’ }
1736โˆ’ [ view
1737โˆ’ { style: tw "rounded-full items-center justify-center"
1738โˆ’ <> Style.style { width: 32.0, height: 32.0, backgroundColor: avatarColor r.roomId }
1739โˆ’ }
1740โˆ’ [ text { style: tw "text-xs font-bold" <> Style.style { color: "#FFFFFF" } } (take 1 r.name) ]
1741โˆ’ , view { style: tw "ml-2 flex-1" }
1742โˆ’ [ text
1743โˆ’ { style: tw "text-sm font-semibold"
1744โˆ’ <> Style.style { color: if activeRoom == Just r.roomId then "#FFFFFF" else dp.fg }
1745โˆ’ }
1746โˆ’ r.name
1747โˆ’ , text
1748โˆ’ { style: tw "text-xs"
1749โˆ’ <> Style.style { color: if activeRoom == Just r.roomId then "#FFFFFFCC" else dp.dimFg }
1750โˆ’ }
1751โˆ’ r.lastMessage
1752โˆ’ ]
1753โˆ’ , nativeButton
1754โˆ’ { title: ""
1755โˆ’ , bezelStyle: T.toolbar
1756โˆ’ , sfSymbol: ""
1757โˆ’ , onPress: handler_ (selectRoom sess r.roomId)
1758โˆ’ , style: Style.style { position: "absolute", top: 0.0, left: 0.0, right: 0.0, bottom: 0.0, opacity: 0.0 }
1759โˆ’ }
1760โˆ’ ]
1761โˆ’ )
1762โˆ’
1763โˆ’ messageBubble sess _ msg = do
1764โˆ’ let
1765โˆ’ isMine = case sess of
1766โˆ’ Just s -> msg.sender == s.userId
1767โˆ’ Nothing -> false
1768โˆ’ let align = if isMine then "flex-end" else "flex-start"
1769โˆ’ let bigEmoji = msg.kind == "text" && isSingleEmoji msg.body
1770โˆ’ let senderLabel = if isMine then "" else msg.sender
1771โˆ’ nativeContextMenu
1772โˆ’ { items:
1773โˆ’ [ { id: "copy", title: "Copy", sfSymbol: "doc.on.doc" }
1774โˆ’ , { id: "reply", title: "Reply", sfSymbol: "arrowshape.turn.up.left" }
1775โˆ’ , { id: "sep", title: "-", sfSymbol: "" }
1776โˆ’ , { id: "delete", title: "Delete", sfSymbol: "trash" }
1777โˆ’ ]
1778โˆ’ , onSelectItem: handler_ (pure unit)
1779โˆ’ , style: tw "px-3 mb-1"
1780โˆ’ }
1781โˆ’ ( view { style: Style.style { alignSelf: align, maxWidth: 320.0 } }
1782โˆ’ [ if senderLabel == "" then text { style: Style.style { height: 0.0 } } ""
1783โˆ’ else text { style: tw "text-xs mb-0.5" <> Style.style { color: dp.dimFg } } senderLabel
1784โˆ’ , if bigEmoji then
1785โˆ’ text
1786โˆ’ { style: Style.style { fontSize: 64.0, lineHeight: 72.0 } }
1787โˆ’ msg.body
1788โˆ’ else
1789โˆ’ view
1790โˆ’ { style: tw "rounded-2xl px-3 py-2"
1791โˆ’ <> Style.style
1792โˆ’ { backgroundColor: if isMine then sentBubbleBg else receivedBubbleBg }
1793โˆ’ }
1794โˆ’ [ text
1795โˆ’ { style: tw "text-sm"
1796โˆ’ <> Style.style { color: if isMine then "#FFFFFF" else (if dp.isDark then "#FFFFFF" else "#000000") }
1797โˆ’ }
1798โˆ’ msg.body
1799โˆ’ ]
1800โˆ’ ]
1801โˆ’ )
1802โˆ’
1803โˆ’ activeRoomName = case activeRoom of
1804โˆ’ Nothing -> "Select a room"
1805โˆ’ Just rid -> fromMaybe rid (map _.name (filter (\r -> r.roomId == rid) rooms !! 0))
1806โˆ’
1807โˆ’ chatView sess = sidebarLayout
1808โˆ’ { sidebar: roomSidebar sess
1809โˆ’ , sidebarWidth: 220.0
1810โˆ’ , content: view { style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
1811โˆ’ [ view
1812โˆ’ { style: tw "px-4 py-2 border-b"
1813โˆ’ <> Style.style { borderBottomWidth: 0.5, borderColor: dp.dimFg, backgroundColor: "transparent" }
1814โˆ’ }
1815โˆ’ [ text { style: tw "text-base font-semibold" <> Style.style { color: dp.fg } } activeRoomName ]
1816โˆ’ , nativePatternBackground
1817โˆ’ { patternColor: if dp.isDark then "#FFFFFF" else "#000000"
1818โˆ’ , background: chatBg
1819โˆ’ , patternOpacity: if dp.isDark then 0.04 else 0.06
1820โˆ’ , patternScale: 1.0
1821โˆ’ , style: tw "flex-1"
1822โˆ’ }
1823โˆ’ ( nativeScrollView { scrollToBottom: length messages, style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
1824โˆ’ ( view { style: tw "py-2 pr-3" }
1825โˆ’ (mapWithIndex (messageBubble session) messages)
1826โˆ’ )
1827โˆ’ )
1828โˆ’ , view
1829โˆ’ { style: tw "flex-row items-center px-3 py-2"
1830โˆ’ <> Style.style { borderTopWidth: 0.5, borderColor: dp.dimFg, backgroundColor: "transparent" }
1831โˆ’ }
1832โˆ’ ( case activeRoom of
1833โˆ’ Nothing ->
1834โˆ’ [ text { style: tw "text-sm" <> Style.style { color: dp.dimFg } } "Select a room to start chatting" ]
1835โˆ’ Just rid ->
1836โˆ’ [ nativeTextField
1837โˆ’ { text: inputText
1838โˆ’ , placeholder: "Message..."
1839โˆ’ , search: false
1840โˆ’ , rounded: true
1841โˆ’ , onChangeText: E.onString "text" setInputText
1842โˆ’ , onSubmit: E.onString "text" \t -> sendMessage sess rid t
1843โˆ’ , style: tw "flex-1" <> Style.style { height: 28.0 }
1844โˆ’ }
1845โˆ’ , nativeButton
1846โˆ’ { title: ""
1847โˆ’ , sfSymbol: "paperplane.fill"
1848โˆ’ , bezelStyle: T.toolbar
1849โˆ’ , onPress: handler_ (sendMessage sess rid inputText)
1850โˆ’ , style: Style.style { height: 28.0, width: 36.0, marginLeft: 8.0 }
1851โˆ’ }
1852โˆ’ ]
1853โˆ’ )
1854โˆ’ ]
1855โˆ’ }
1856โˆ’
1857โˆ’ pure do
1858โˆ’ case session of
1859โˆ’ Nothing -> loginForm
1860โˆ’ Just sess -> chatView sess
1861โˆ’
1862โˆ’eventKind :: Matrix.MatrixEvent -> String
1863โˆ’eventKind ev = case Matrix.getEventMsgType ev of
1864โˆ’ "m.image" -> "gif"
1865โˆ’ _ -> "text"
1866โˆ’
1867โˆ’elem :: forall a. Eq a => a -> Array a -> Boolean
1868โˆ’elem x xs = not (null (filter (_ == x) xs))
1869โˆ’
1870โˆ’foreign import setInterval :: Int -> Effect Unit -> Effect Int
1871โˆ’foreign import clearInterval :: Int -> Effect Unit
1872โˆ’
1873โˆ’-- UI helpers
1874โˆ’
1875โˆ’scrollWrap :: DemoProps -> Array JSX -> JSX
1876โˆ’scrollWrap dp children =
1877โˆ’ nativeScrollView { style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
1878โˆ’ (view { style: tw "px-4 pb-4" } children)
1879โˆ’
1880โˆ’sectionTitle :: String -> String -> JSX
1881โˆ’sectionTitle color title =
1882โˆ’ text { style: tw "text-sm font-semibold mt-4 mb-1" <> Style.style { color } } title
1883โˆ’
1884โˆ’desc :: DemoProps -> String -> JSX
1885โˆ’desc dp str =
1886โˆ’ text { style: tw "text-xs mb-2" <> Style.style { color: dp.dimFg } } str
1887โˆ’
1888โˆ’card :: String -> Array JSX -> JSX
1889โˆ’card bg children =
1890โˆ’ view { style: tw "rounded-lg p-3 mb-2" <> Style.style { backgroundColor: bg, overflow: "hidden" } }
1891โˆ’ children
1892โˆ’
1893โˆ’label :: String -> String -> JSX
1894โˆ’label color str =
1895โˆ’ text { style: tw "ml-3 text-sm" <> Style.style { color } } str
1896โˆ’
1897โˆ’round :: Number -> Int
1898โˆ’round n = unsafeRound n
1899โˆ’
1900โˆ’foreign import unsafeRound :: Number -> Int
1901โˆ’foreign import getFieldJSON :: String -> forall r. r -> String
1902โˆ’foreign import isSingleEmoji :: String -> Boolean
src/Yoga/React/Native/MacOS/ScrollView.purs+2
@@ -11,4 +11,6 @@ nativeScrollView = createNativeElement nativeScrollViewImpl
11
11
12type NativeScrollViewAttributes = BaseAttributes
12type NativeScrollViewAttributes = BaseAttributes
13 ( scrollToBottom :: Int
13 ( scrollToBottom :: Int
14+ , scrollToY :: Number
15+ , scrollToYTrigger :: Int
14 )
16 )