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
commitb4743a79107e831a729767fd901e3a49628bbc9f authorMark <mark.eibes@gmail.com> dateMon 16 Feb 2026, 18:56 (7 months ago) parent3590f54
21 files changed+2288−1868
example-macos/macos/YogaReactExample-macOS/RCTMacOSControlsManager.m+27 example-macos/src/Demo/AiMl.purs+157 example-macos/src/Demo/Animation.purs+66 example-macos/src/Demo/Chat.js+141 example-macos/src/Demo/Chat.purs+379 example-macos/src/Demo/DataViews.purs+77 example-macos/src/Demo/Display.purs+148 example-macos/src/Demo/DragDropFiles.purs+106 example-macos/src/Demo/InputControls.purs+321 example-macos/src/Demo/Layout.purs+71 example-macos/src/Demo/MapsDocs.purs+63 example-macos/src/Demo/Navigation.purs+207 example-macos/src/Demo/Overlays.purs+153 example-macos/src/Demo/Shared.js+6 example-macos/src/Demo/Shared.purs+53 example-macos/src/Demo/SystemServices.purs+217 example-macos/src/Demo/TextEditing.purs+45 example-macos/src/Demo/WebBrowser.purs+41 example-macos/src/Main.js−16 example-macos/src/Main.purs+8−1852 src/Yoga/React/Native/MacOS/ScrollView.purs+2
example-macos/macos/YogaReactExample-macOS/RCTMacOSControlsManager.m+27
@@ -815,6 +815,8 @@ RCT_EXPORT_VIEW_PROPERTY(criticalValue, double)
815815@interface RCTNativeScrollView : NSScrollView
816816@property (nonatomic, strong) RCTFlippedDocumentView *docView;
817817@property (nonatomic, assign) NSInteger scrollToBottom;
818+@property (nonatomic, assign) CGFloat scrollToY;
819+@property (nonatomic, assign) NSInteger scrollToYTrigger;
818820@end
819821
820822@implementation RCTNativeScrollView
@@ -883,6 +885,29 @@ RCT_EXPORT_VIEW_PROPERTY(criticalValue, double)
883885 }
884886}
885887
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+
886911- (void)layout {
887912 [super layout];
888913 [self updateDocumentSize];
@@ -896,6 +921,8 @@ RCT_EXPORT_VIEW_PROPERTY(criticalValue, double)
896921RCT_EXPORT_MODULE(MacOSScrollView)
897922- (NSView *)view { return [[RCTNativeScrollView alloc] initWithFrame:CGRectZero]; }
898923RCT_EXPORT_VIEW_PROPERTY(scrollToBottom, NSInteger)
924+RCT_EXPORT_VIEW_PROPERTY(scrollToY, double)
925+RCT_EXPORT_VIEW_PROPERTY(scrollToYTrigger, NSInteger)
899926@end
900927
901928// ============================================================
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
22
33import Prelude
44
5−import Data.Array (mapWithIndex, length, filter, null, (!!))
5+import Data.Maybe (Maybe(..))
66import Data.Nullable (toNullable)
7−import Data.Maybe (Maybe(..), fromMaybe)
8−import Data.String (take, joinWith)
9−import Data.Foldable (for_, foldl)
10−import Data.Traversable (for)
7+import Demo.Navigation (demoContent, outlineSidebar)
8+import Demo.Shared (DemoProps)
119import Effect (Effect)
12−import Data.Either (Either(..))
13−import Effect.Aff (launchAff_, try)
14−import Effect.Class (liftEffect)
1510import React.Basic (JSX)
16−import React.Basic.Events (handler, handler_, unsafeEventFn)
17−import React.Basic.Hooks (useState, useState', useEffect, useRef, readRef, writeRef, (/\))
11+import React.Basic.Events (handler_)
12+import React.Basic.Hooks (useState', (/\))
1813import React.Basic.Hooks as React
1914import Yoga.React (component)
20−import Yoga.React.Native (nativeEvent, registerComponent, safeAreaView, text, tw, view)
15+import Yoga.React.Native (registerComponent, safeAreaView, tw, view)
2116import 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)
3717import Yoga.React.Native.MacOS.Sidebar (sidebarLayout)
38−import Yoga.React.Native.MacOS.ContextMenu (nativeContextMenu)
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
18+import Yoga.React.Native.MacOS.Toolbar (nativeToolbar)
7819import Yoga.React.Native.MacOS.Types as T
79−import Yoga.React.Native.Matrix as Matrix
20+import Yoga.React.Native.MacOS.VisualEffect (nativeVisualEffect)
8021import Yoga.React.Native.Style as Style
8122
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−
9123main :: Effect Unit
9224main = registerComponent "YogaReactExample" \_ -> app {}
9325
@@ -124,1779 +56,3 @@ app = component "App" \_ -> React.do
12456 ]
12557 )
12658 )
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
1111
1212type NativeScrollViewAttributes = BaseAttributes
1313 ( scrollToBottom :: Int
14+ , scrollToY :: Number
15+ , scrollToYTrigger :: Int
1416 )