@@ -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