@@ -2,81 +2,21 @@ 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 (EventHandler, handler, handler_, unsafeEventFn)
17−import React.Basic.Hooks (useState, useState', useEffect, useRef, readRef, writeRef, (/\))
11+import React.Basic.Hooks (useState', (/\))
1812import React.Basic.Hooks as React
1913import Yoga.React (component)
20−import Yoga.React.Native (nativeEvent, registerComponent, safeAreaView, text, tw, view)
14+import Yoga.React.Native (registerComponent, safeAreaView, tw, view)
2115import 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)
3716import Yoga.React.Native.MacOS.Sidebar (sidebarLayout)
38−import Yoga.React.Native.MacOS.ContextMenu (nativeContextMenu)
39−
40−import Yoga.React.Native.MacOS.FilePicker (nativeFilePicker)
41−import Yoga.React.Native.MacOS.VideoPlayer (nativeVideoPlayer)
42−import Yoga.React.Native.MacOS.AnimatedImage (nativeAnimatedImage)
43−import Yoga.React.Native.MacOS.PatternBackground (nativePatternBackground)
44−import Yoga.React.Native.MacOS.SplitView (nativeSplitView)
45−import Yoga.React.Native.MacOS.TabView (nativeTabView)
46−import Yoga.React.Native.MacOS.ComboBox (nativeComboBox)
47−import Yoga.React.Native.MacOS.Stepper (nativeStepper)
48−import Yoga.React.Native.MacOS.Box (nativeBox)
49−import Yoga.React.Native.MacOS.Popover (nativePopover)
50−import Yoga.React.Native.MacOS.Image (nativeImage)
51−import Yoga.React.Native.MacOS.Alert (macosAlert)
52−import Yoga.React.Native.MacOS.Checkbox (nativeCheckbox)
53−import Yoga.React.Native.MacOS.RadioButton (nativeRadioButton)
54−import Yoga.React.Native.MacOS.SearchField (nativeSearchField)
55−import Yoga.React.Native.MacOS.TokenField (nativeTokenField)
56−import Yoga.React.Native.MacOS.Separator (nativeSeparator)
57−import Yoga.React.Native.MacOS.HelpButton (nativeHelpButton)
58−import Yoga.React.Native.MacOS.PathControl (nativePathControl)
59−import Yoga.React.Native.MacOS.Sheet (nativeSheet)
60−import Yoga.React.Native.MacOS.TableView (nativeTableView)
61−import Yoga.React.Native.MacOS.Menu (macosShowMenu)
62−import Yoga.React.Native.MacOS.Pasteboard (copyToClipboard)
63−import Foreign (unsafeToForeign)
64−import Yoga.React.Native.MacOS.OutlineView (nativeOutlineView)
65−import Yoga.React.Native.MacOS.ShareService (macosShare)
66−import Yoga.React.Native.MacOS.UserNotification (macosNotify)
67−import Yoga.React.Native.MacOS.Sound (macosPlaySound, macosBeep)
68−import Yoga.React.Native.MacOS.StatusBarItem (macosSetStatusBarItem, macosRemoveStatusBarItem)
69−import Yoga.React.Native.MacOS.MapView (nativeMapView)
70−import Yoga.React.Native.MacOS.PDFView (nativePDFView)
71−import Yoga.React.Native.MacOS.QuickLook (macosQuickLook)
72−import Yoga.React.Native.MacOS.SpeechSynthesizer (macosSay, macosStopSpeaking)
73−import Yoga.React.Native.MacOS.ColorPanel (macosShowColorPanel)
74−import Yoga.React.Native.MacOS.FontPanel (macosShowFontPanel)
75−import Yoga.React.Native.MacOS.OCR (macosOCR)
76−import Yoga.React.Native.MacOS.SpeechRecognition (macosStartListening, macosStopListening, macosGetTranscript)
77−import Yoga.React.Native.MacOS.NaturalLanguage (macosDetectLanguage, macosAnalyzeSentiment, macosTokenize)
17+import Yoga.React.Native.MacOS.Toolbar (nativeToolbar)
7818import Yoga.React.Native.MacOS.Types as T
79−import Yoga.React.Native.Matrix as Matrix
19+import Yoga.React.Native.MacOS.VisualEffect (nativeVisualEffect)
8020import Yoga.React.Native.Style as Style
8121
8222main :: Effect Unit
@@ -84,23 +24,14 @@ main = registerComponent "YogaReactExample" \_ -> app {}
8424
8525app :: {} -> JSX
8626app = component "App" \_ -> React.do
87− activeTab /\ setActiveTab <- useState' "controls"
88− sliderValue /\ setSliderValue <- useState' 50.0
89− switchOn /\ setSwitchOn <- useState' false
90− selectedColor /\ setSelectedColor <- useState' "#FF6600"
91− popUpIndex /\ setPopUpIndex <- useState' 0
92− popUpTitle /\ setPopUpTitle <- useState' "Small"
93− searchText /\ setSearchText <- useState' ""
94− buttonStatus /\ setButtonStatus <- useState' "Ready"
95− dateText /\ setDateText <- useState' ""
96− browserUrl /\ setBrowserUrl <- useState' "https://pursuit.purescript.org"
97− urlBarText /\ setUrlBarText <- useState' "https://pursuit.purescript.org"
27+ selectedItem /\ setSelectedItem <- useState' "button"
9828 colorScheme <- useColorScheme
9929 let isDark = toNullable (Just "dark") == colorScheme
10030 let fg = if isDark then "#FFFFFF" else "#000000"
10131 let dimFg = if isDark then "#999999" else "#666666"
10232 let cardBg = if isDark then "#2A2A2A" else "#F0F0F0"
10333 let bg = if isDark then "#1E1E1E" else "#FFFFFF"
34+ let dp = { fg, dimFg, cardBg, bg, isDark } :: DemoProps
10435 pure do
10536 nativeVisualEffect
10637 { materialName: T.windowBackground
@@ -109,1535 +40,18 @@ app = component "App" \_ -> React.do
10940 ( safeAreaView { style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
11041 ( view { style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
11142 [ nativeToolbar
112− { items:
113− [ { id: "controls", label: "Controls", sfSymbol: "slider.horizontal.3" }
114− , { id: "editor", label: "Editor", sfSymbol: "doc.richtext" }
115− , { id: "browser", label: "Browser", sfSymbol: "globe" }
116− , { id: "rive", label: "Rive", sfSymbol: "play.circle" }
117− , { id: "system", label: "System", sfSymbol: "gearshape.2" }
118− , { id: "ai", label: "AI", sfSymbol: "brain" }
119− , { id: "chat", label: "Chat", sfSymbol: "bubble.left.and.bubble.right" }
120− ]
121− , selectedItem: activeTab
43+ { items: []
44+ , selectedItem: ""
12245 , toolbarStyle: T.unified
12346 , windowTitle: "PureScript React Native"
124− , onSelectItem: extractString "itemId" setActiveTab
47+ , onSelectItem: \_ -> pure unit
12548 , style: Style.style { height: 0.0, width: 0.0 }
12649 }
127− , view { style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
128− [ if activeTab == "controls" then controlsTab
129− { sliderValue
130− , setSliderValue
131− , switchOn
132− , setSwitchOn
133− , selectedColor
134− , setSelectedColor
135− , popUpIndex
136− , setPopUpIndex
137− , popUpTitle
138− , setPopUpTitle
139− , searchText
140− , setSearchText
141− , buttonStatus
142− , setButtonStatus
143− , dateText
144− , setDateText
145− , fg
146− , dimFg
147− , cardBg
148− }
149− else if activeTab == "editor" then editorTab { fg }
150− else if activeTab == "browser" then browserTab
151− { browserUrl
152− , setBrowserUrl
153− , urlBarText
154− , setUrlBarText
155− , fg
156− , dimFg
157− }
158− else if activeTab == "rive" then riveTab { fg, bg }
159− else if activeTab == "system" then systemTab { fg, dimFg, cardBg, bg }
160− else if activeTab == "ai" then aiTab { fg, dimFg, cardBg, bg }
161− else chatTab { fg, dimFg, cardBg, bg, isDark }
162− ]
163− ]
164− )
165− )
166−
167−-- Helpers for extracting native event values
168−extractNumber :: String -> (Number -> Effect Unit) -> EventHandler
169−extractNumber key cb = handler (nativeEvent >>> unsafeEventFn \e -> getField key e) cb
170−
171−extractInt :: String -> (Int -> Effect Unit) -> EventHandler
172−extractInt key cb = handler (nativeEvent >>> unsafeEventFn \e -> getFieldInt key e) cb
173−
174−extractString :: String -> (String -> Effect Unit) -> EventHandler
175−extractString key cb = handler (nativeEvent >>> unsafeEventFn \e -> getFieldStr key e) cb
176−
177−extractBool :: String -> (Boolean -> Effect Unit) -> EventHandler
178−extractBool key cb = handler (nativeEvent >>> unsafeEventFn \e -> getFieldBool key e) cb
179−
180−foreign import getField :: String -> forall r. r -> Number
181−foreign import getFieldInt :: String -> forall r. r -> Int
182−foreign import getFieldStr :: String -> forall r. r -> String
183−foreign import getFieldBool :: String -> forall r. r -> Boolean
184−foreign import getFieldArray :: String -> forall r. r -> Array String
185−
186−-- Tab 0: Controls
187−type ControlsProps =
188− { sliderValue :: Number
189− , setSliderValue :: Number -> Effect Unit
190− , switchOn :: Boolean
191− , setSwitchOn :: Boolean -> Effect Unit
192− , selectedColor :: String
193− , setSelectedColor :: String -> Effect Unit
194− , popUpIndex :: Int
195− , setPopUpIndex :: Int -> Effect Unit
196− , popUpTitle :: String
197− , setPopUpTitle :: String -> Effect Unit
198− , searchText :: String
199− , setSearchText :: String -> Effect Unit
200− , buttonStatus :: String
201− , setButtonStatus :: String -> Effect Unit
202− , dateText :: String
203− , setDateText :: String -> Effect Unit
204− , fg :: String
205− , dimFg :: String
206− , cardBg :: String
207− }
208−
209−sidebarItem :: String -> String -> String -> Boolean -> (String -> Effect Unit) -> JSX
210−sidebarItem _ sfSymbolName title selected setCategory =
211− nativeButton
212− { title
213− , sfSymbol: sfSymbolName
214− , bezelStyle: T.toolbar
215− , primary: selected
216− , onPress: handler_ (setCategory title)
217− , style: Style.style { height: 28.0, marginHorizontal: 8.0, marginVertical: 1.0 }
218− }
219−
220−controlsTab :: ControlsProps -> JSX
221−controlsTab = component "ControlsTab" \p -> React.do
222− category /\ setCategory <- useState' "All"
223− let categories = [ "All", "Button", "Switch", "Slider", "Pop Up", "Color", "Date", "Text" ]
224− let
225− sidebar = view { style: tw "pt-2" }
226− (categories <#> \cat -> sidebarItem p.fg "" cat (category == cat) setCategory)
227− let show' name = category == "All" || category == name
228− let
229− buttonSection =
230− [ sectionTitle p.fg "Button"
231− , card p.cardBg
232− [ view { style: tw "flex-row items-center" }
233− [ nativeButton
234− { title: "Say Hello"
235− , sfSymbol: "hand.wave"
236− , bezelStyle: T.push
237− , primary: true
238− , onPress: handler_ (p.setButtonStatus "Hello from PureScript!")
239− , style: Style.style { height: 24.0, width: 140.0 }
240− }
241− , nativeButton
242− { title: "Reset"
243− , sfSymbol: "arrow.counterclockwise"
244− , bezelStyle: T.push
245− , onPress: handler_ (p.setButtonStatus "Ready")
246− , style: Style.style { height: 24.0, width: 100.0, marginLeft: 8.0 }
247− }
248− , label p.dimFg p.buttonStatus
249− ]
250− ]
251− ]
252− let
253− switchSection =
254− [ sectionTitle p.fg "Switch"
255− , card p.cardBg
256− [ view { style: tw "flex-row items-center" }
257− [ nativeSwitch
258− { on: p.switchOn
259− , onChange: extractBool "on" p.setSwitchOn
260− , style: Style.style { height: 24.0, width: 48.0 }
261− }
262− , label p.dimFg (if p.switchOn then "On" else "Off")
263− ]
264− ]
265− ]
266− let
267− sliderSection =
268− [ sectionTitle p.fg "Slider + Level Indicator + Progress"
269− , card p.cardBg
270− [ nativeSlider
271− { value: p.sliderValue
272− , minValue: 0.0
273− , maxValue: 100.0
274− , numberOfTickMarks: 11
275− , onChange: extractNumber "value" p.setSliderValue
276− , style: Style.style { height: 24.0 }
277− }
278− , label p.dimFg ("Value: " <> show (round p.sliderValue) <> " / 100")
279− , nativeLevelIndicator
280− { value: p.sliderValue
281− , minValue: 0.0
282− , maxValue: 100.0
283− , warningValue: 70.0
284− , criticalValue: 90.0
285− , style: Style.style { height: 18.0, marginTop: 8.0 }
286− }
287− , nativeProgress
288− { value: p.sliderValue
289− , style: Style.style { height: 18.0, marginTop: 8.0 }
290− }
291− ]
292− ]
293− let
294− popUpSection =
295− [ sectionTitle p.fg "Pop Up Button"
296− , card p.cardBg
297− [ view { style: tw "flex-row items-center" }
298− [ nativePopUp
299− { items: [ "Small", "Medium", "Large", "Extra Large" ]
300− , selectedIndex: p.popUpIndex
301− , onChange: handler
302− ( nativeEvent >>> unsafeEventFn \e ->
303− { idx: (getFieldInt "selectedIndex" e), title: (getFieldStr "title" e) }
304− )
305− \r -> do
306− p.setPopUpIndex r.idx
307− p.setPopUpTitle r.title
308− , style: Style.style { height: 24.0, width: 160.0 }
309− }
310− , label p.dimFg ("Selected: " <> p.popUpTitle)
311− ]
312− ]
313− ]
314− let
315− colorSection =
316− [ sectionTitle p.fg "Color Well"
317− , card p.cardBg
318− [ view { style: tw "flex-row items-center" }
319− [ nativeColorWell
320− { color: p.selectedColor
321− , minimal: true
322− , onChange: extractString "color" p.setSelectedColor
323− , style: Style.style { height: 32.0, width: 48.0 }
324− }
325− , view
326− { style: tw "ml-3 rounded" <> Style.style
327− { width: 24.0, height: 24.0, backgroundColor: p.selectedColor }
328− }
329− []
330− , label p.dimFg p.selectedColor
331− ]
332− ]
333− ]
334− let
335− dateSection =
336− [ sectionTitle p.fg "Date Picker"
337− , card p.cardBg
338− [ nativeDatePicker
339− { graphical: false
340− , onChange: extractString "date" p.setDateText
341− , style: Style.style { height: 24.0, width: 200.0 }
342− }
343− , if p.dateText == "" then mempty
344− else label p.dimFg ("Picked: " <> p.dateText)
345− ]
346− ]
347− let
348− textSection =
349− [ sectionTitle p.fg "Text Field"
350− , card p.cardBg
351− [ nativeTextField
352− { placeholder: "Type something..."
353− , search: true
354− , text: p.searchText
355− , onChangeText: extractString "text" p.setSearchText
356− , style: Style.style { height: 24.0 }
357− }
358− , if p.searchText == "" then mempty
359− else label p.dimFg ("You typed: " <> p.searchText)
360− ]
361− ]
362− let
363− sections = join
364− [ if show' "Button" then buttonSection else []
365− , if show' "Switch" then switchSection else []
366− , if show' "Slider" then sliderSection else []
367− , if show' "Pop Up" then popUpSection else []
368− , if show' "Color" then colorSection else []
369− , if show' "Date" then dateSection else []
370− , if show' "Text" then textSection else []
371− ]
372− pure do
373− sidebarLayout
374− { sidebar
375− , sidebarWidth: 140.0
376− , content: nativeScrollView { style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
377− (view { style: tw "px-4 pb-4" } sections)
378− }
379−
380−-- Tab 1: Editor
381−editorTab :: { fg :: String } -> JSX
382−editorTab = component "EditorTab" \p -> React.do
383− pure do
384− view { style: tw "flex-1 px-4" }
385− [ sectionTitle p.fg "Rich Text Editor"
386− , nativeTextEditor
387− { text: "Welcome to the PureScript-driven native text editor.\n\nThis uses NSTextView with rich text support and a formatting ruler."
388− , richText: true
389− , showsRuler: true
390− , style: tw "flex-1" <> Style.style { minHeight: 300.0 }
391− }
392− ]
393−
394−-- Tab 2: Browser
395−type BrowserProps =
396− { browserUrl :: String
397− , setBrowserUrl :: String -> Effect Unit
398− , urlBarText :: String
399− , setUrlBarText :: String -> Effect Unit
400− , fg :: String
401− , dimFg :: String
402− }
403−
404−browserTab :: BrowserProps -> JSX
405−browserTab = component "BrowserTab" \p -> React.do
406− pure do
407− view { style: tw "flex-1 px-4" }
408− [ sectionTitle p.fg "Web Browser"
409− , nativeTextField
410− { text: p.urlBarText
411− , placeholder: "Enter URL..."
412− , search: false
413− , onChangeText: extractString "text" p.setUrlBarText
414− , onSubmit: extractString "text" p.setBrowserUrl
415− , style: Style.style { height: 24.0, marginBottom: 8.0 }
416− }
417− , nativeWebView
418− { url: p.browserUrl
419− , onFinishLoad: handler
420− ( nativeEvent >>> unsafeEventFn \e ->
421− { url: getFieldStr "url" e, title: getFieldStr "title" e }
422− )
423− \r -> p.setUrlBarText r.url
424− , style: tw "flex-1" <> Style.style { minHeight: 400.0 }
425− }
426− ]
427−
428−-- Tab 3: Rive Animation
429−riveTab :: { fg :: String, bg :: String } -> JSX
430−riveTab = component "RiveTab" \p -> React.do
431− pure do
432− nativeScrollView { style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
433− ( view { style: tw "px-4 pb-4" }
434− [ sectionTitle p.fg "Mouse Tracking"
435− , text { style: tw "text-xs mb-2" <> Style.style { color: p.fg } }
436− "Move your cursor over the cat — it follows your mouse!"
437− , nativeRiveView_
438− { resourceName: "cat_following_mouse"
439− , stateMachineName: "State Machine 1"
440− , fit: T.contain
441− , autoplay: true
442− , style: Style.style { height: 300.0 }
443− }
444− , sectionTitle p.fg "Cursor Tracking"
445− , text { style: tw "text-xs mb-2" <> Style.style { color: p.fg } }
446− "Shapes follow your cursor via Pointer Move listeners"
447− , nativeRiveView_
448− { resourceName: "cursor_tracking"
449− , stateMachineName: "State Machine 1"
450− , fit: T.contain
451− , autoplay: true
452− , style: Style.style { height: 300.0 }
453− }
454− , sectionTitle p.fg "Interactive Rive Animations"
455− , text { style: tw "text-xs mb-2" <> Style.style { color: p.fg } }
456− "Click the stars to rate! State machine handles interaction."
457− , nativeRiveView_
458− { resourceName: "rating_animation"
459− , stateMachineName: "State Machine 1"
460− , fit: T.contain
461− , autoplay: true
462− , style: Style.style { height: 200.0 }
463− }
464− , sectionTitle p.fg "Light Switch"
465− , text { style: tw "text-xs mb-2" <> Style.style { color: p.fg } }
466− "Click to toggle"
467− , nativeRiveView_
468− { resourceName: "switch_event_example"
469− , stateMachineName: "State Machine 1"
470− , fit: T.contain
471− , autoplay: true
472− , style: Style.style { height: 200.0 }
473− }
474− , sectionTitle p.fg "Windy Tree"
475− , nativeRiveView_
476− { resourceName: "windy_tree"
477− , fit: T.cover
478− , autoplay: true
479− , style: tw "flex-1" <> Style.style { minHeight: 300.0, backgroundColor: p.bg }
480− }
481− ]
482− )
483−
484−-- Tab 4: System (Context Menu, Drop Zone, File Picker)
485−type SystemProps =
486− { fg :: String
487− , dimFg :: String
488− , cardBg :: String
489− , bg :: String
490− }
491−
492−systemTab :: SystemProps -> JSX
493−systemTab = component "SystemTab" \p -> React.do
494− menuResult /\ setMenuResult <- useState' ""
495− dropStatus /\ setDropStatus <- useState' "Drop files here"
496− droppedFiles /\ setDroppedFiles <- useState' ""
497− isDragging /\ setIsDragging <- useState' false
498− pickedFiles /\ setPickedFiles <- useState' ""
499− videoPlaying /\ setVideoPlaying <- useState' true
500− comboText /\ setComboText <- useState' ""
501− comboResult /\ setComboResult <- useState' ""
502− stepperValue /\ setStepperValue <- useState' 5.0
503− popoverVisible /\ setPopoverVisible <- useState' false
504− checkboxA /\ setCheckboxA <- useState' true
505− checkboxB /\ setCheckboxB <- useState' false
506− radioChoice /\ setRadioChoice <- useState' "option1"
507− searchQuery /\ setSearchQuery <- useState' ""
508− searchResult /\ setSearchResult <- useState' ""
509− tagTokens /\ setTagTokens <- useState' ([ "PureScript", "React", "macOS" ] :: Array String)
510− sheetVisible /\ setSheetVisible <- useState' false
511− menuResult2 /\ setMenuResult2 <- useState' ""
512− selectedRow /\ setSelectedRow <- useState' ""
513− outlineSelection /\ setOutlineSelection <- useState' ""
514− statusBarActive /\ setStatusBarActive <- useState' false
515− let
516− accentBorder = if isDragging then "#007AFF" else p.dimFg
517− pure do
518− nativeScrollView { style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
519− ( view { style: tw "px-4 pb-4" }
520− [ sectionTitle p.fg "Context Menu"
521− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
522− "Right-click the area below to open a context menu"
523− , nativeContextMenu
524− { items:
525− [ { id: "cut", title: "Cut", sfSymbol: "scissors" }
526− , { id: "copy", title: "Copy", sfSymbol: "doc.on.doc" }
527− , { id: "paste", title: "Paste", sfSymbol: "doc.on.clipboard" }
528− , { id: "sep", title: "-", sfSymbol: "" }
529− , { id: "delete", title: "Delete", sfSymbol: "trash" }
530− , { id: "selectAll", title: "Select All", sfSymbol: "selection.pin.in.out" }
531− ]
532− , onSelectItem: extractString "itemId" setMenuResult
533− , style: Style.style {}
534− }
535− ( card p.cardBg
536− [ view { style: tw "items-center justify-center" <> Style.style { minHeight: 80.0 } }
537− [ text { style: tw "text-sm" <> Style.style { color: p.fg } }
538− "Right-click me!"
539− , if menuResult == "" then mempty
540− else label p.dimFg ("Selected: " <> menuResult)
541− ]
542− ]
543− )
544− , sectionTitle p.fg "Drop Zone"
545− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
546− "Drag files from Finder into the area below"
547− , view
548− { draggedTypes: [ "NSFilenamesPboardType" ]
549− , onDrop: handler
550− (nativeEvent >>> unsafeEventFn \e -> getFieldJSON "dataTransfer" e)
551− \r -> do
552− setDroppedFiles r
553− setDropStatus "Drop files here"
554− setIsDragging false
555− , onDragEnter: handler_ do
556− setDropStatus "Release to drop!"
557− setIsDragging true
558− , onDragLeave: handler_ do
559− setDropStatus "Drop files here"
560− setIsDragging false
561− , style: tw "rounded-lg items-center justify-center mb-2"
562− <> Style.style
563− { minHeight: 100.0
564− , borderWidth: 2.0
565− , borderColor: accentBorder
566− , backgroundColor: p.cardBg
567− }
568− }
569− [ text { style: tw "text-sm" <> Style.style { color: p.fg } } dropStatus
570− , if droppedFiles == "" then mempty
571− else text { style: tw "text-xs mt-2 px-4" <> Style.style { color: p.dimFg } }
572− ("Dropped: " <> droppedFiles)
573− ]
574− , sectionTitle p.fg "File Picker"
575− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
576− "Click buttons to open native file panels"
577− , card p.cardBg
578− [ view { style: tw "flex-row items-center" }
579− [ nativeFilePicker
580− { mode: T.openFile
581− , title: "Open Files"
582− , sfSymbol: "doc.badge.plus"
583− , allowMultiple: true
584− , allowedTypes: [ "public.image", "public.text" ]
585− , message: "Select files to open"
586− , onPickFiles: handler
587− (nativeEvent >>> unsafeEventFn \e -> getFieldJSON "files" e)
588− setPickedFiles
589− , onCancel: handler_ (setPickedFiles "Cancelled")
590− , style: Style.style { height: 24.0, width: 120.0 }
591− }
592− , nativeFilePicker
593− { mode: T.openFile
594− , title: "Choose Folder"
595− , sfSymbol: "folder"
596− , canChooseDirectories: true
597− , message: "Select a folder"
598− , onPickFiles: handler
599− (nativeEvent >>> unsafeEventFn \e -> getFieldJSON "files" e)
600− setPickedFiles
601− , onCancel: handler_ (setPickedFiles "Cancelled")
602− , style: Style.style { height: 24.0, width: 140.0, marginLeft: 8.0 }
603− }
604− , nativeFilePicker
605− { mode: T.saveFile
606− , title: "Save As..."
607− , sfSymbol: "square.and.arrow.down"
608− , defaultName: "Untitled.txt"
609− , allowedTypes: [ "public.plain-text" ]
610− , message: "Choose save location"
611− , onPickFiles: handler
612− (nativeEvent >>> unsafeEventFn \e -> getFieldJSON "files" e)
613− setPickedFiles
614− , onCancel: handler_ (setPickedFiles "Cancelled")
615− , style: Style.style { height: 24.0, width: 120.0, marginLeft: 8.0 }
616− }
617− ]
618− , if pickedFiles == "" then mempty
619− else label p.dimFg pickedFiles
620− ]
621− , sectionTitle p.fg "Video Player"
622− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
623− "Native AVPlayerView with floating controls"
624− , nativeVideoPlayer
625− { source: "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/720/Big_Buck_Bunny_720_10s_1MB.mp4"
626− , playing: videoPlaying
627− , looping: true
628− , muted: false
629− , style: Style.style { height: 240.0 } <> tw "rounded-lg overflow-hidden mb-2"
630− }
631− , view { style: tw "flex-row mb-2" }
632− [ nativeButton
633− { title: if videoPlaying then "Pause" else "Play"
634− , sfSymbol: if videoPlaying then "pause.fill" else "play.fill"
635− , bezelStyle: T.push
636− , onPress: handler_ (setVideoPlaying (not videoPlaying))
637− , style: Style.style { height: 24.0, width: 100.0 }
638− }
639− ]
640− , sectionTitle p.fg "Animated Image"
641− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
642− "Native NSImageView with animated GIF support"
643− , nativeAnimatedImage
644− { source: "https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif"
645− , animating: true
646− , style: Style.style { height: 200.0 } <> tw "rounded-lg overflow-hidden mb-2"
647− }
648− , nativeAnimatedImage
649− { source: "https://media.giphy.com/media/13CoXDiaCcCoyk/giphy.gif"
650− , animating: true
651− , style: Style.style { height: 200.0 } <> tw "rounded-lg overflow-hidden mb-2"
652− }
653−
654− -- New components
655− , sectionTitle p.fg "Split View"
656− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
657− "Native NSSplitView with resizable divider"
658− , nativeSplitView
659− { isVertical: true
660− , style: Style.style { height: 150.0 } <> tw "rounded-lg overflow-hidden mb-2"
661− }
662− [ view { style: tw "flex-1 p-4" <> Style.style { backgroundColor: p.cardBg } }
663− [ text { style: tw "text-sm font-semibold" <> Style.style { color: p.fg } } "Left Pane"
664− , text { style: tw "text-xs mt-1" <> Style.style { color: p.dimFg } } "Drag the divider to resize"
665− ]
666− , view { style: tw "flex-1 p-4" <> Style.style { backgroundColor: p.cardBg } }
667− [ text { style: tw "text-sm font-semibold" <> Style.style { color: p.fg } } "Right Pane"
668− , text { style: tw "text-xs mt-1" <> Style.style { color: p.dimFg } } "Content fills available space"
669− ]
670− ]
671−
672− , sectionTitle p.fg "Tab View"
673− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
674− "Native segmented tab bar"
675− , nativeTabView
676− { items:
677− [ { id: "general", label: "General" }
678− , { id: "advanced", label: "Advanced" }
679− , { id: "about", label: "About" }
680− ]
681− , selectedItem: "general"
682− , onSelectTab: extractString "tabId" \_ -> pure unit
683− , style: Style.style { height: 32.0 } <> tw "mb-2"
684− }
685−
686− , sectionTitle p.fg "Combo Box"
687− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
688− "Editable dropdown with autocomplete"
689− , card p.cardBg
690− [ nativeComboBox
691− { items: [ "Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig", "Grape" ]
692− , text: comboText
693− , placeholder: "Type a fruit..."
694− , onChangeText: extractString "text" setComboText
695− , onSelectItem: extractString "text" \t -> do
696− setComboText t
697− setComboResult ("Selected: " <> t)
698− , style: Style.style { height: 28.0 } <> tw "mb-2"
50+ , sidebarLayout
51+ { sidebar: outlineSidebar selectedItem setSelectedItem
52+ , sidebarWidth: 200.0
53+ , content: demoContent dp selectedItem
69954 }
700− , if comboResult == "" then mempty
701− else label p.dimFg comboResult
70255 ]
703−
704− , sectionTitle p.fg "Stepper"
705− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
706− "Up/down increment control with label"
707− , card p.cardBg
708− [ view { style: tw "flex-row items-center" }
709− [ text { style: tw "text-sm mr-3" <> Style.style { color: p.fg } } "Quantity:"
710− , nativeStepper
711− { value: stepperValue
712− , minValue: 0.0
713− , maxValue: 50.0
714− , increment: 1.0
715− , onChange: extractNumber "value" setStepperValue
716− , style: Style.style { width: 100.0, height: 24.0 }
717− }
718− ]
719− ]
720−
721− , sectionTitle p.fg "Box"
722− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
723− "Native NSBox with title and border"
724− , nativeBox
725− { boxTitle: "Settings"
726− , fillColorStr: p.cardBg
727− , borderColorStr: p.dimFg
728− , cornerRadiusValue: 8.0
729− , style: Style.style { height: 100.0 } <> tw "mb-2"
730− }
731− [ view { style: tw "flex-row items-center p-2" }
732− [ text { style: tw "text-sm" <> Style.style { color: p.fg } } "Content inside a native box" ]
733− ]
734−
735− , sectionTitle p.fg "Popover"
736− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
737− "Click button to toggle a popover"
738− , nativePopover
739− { visible: popoverVisible
740− , preferredEdge: T.bottom
741− , behavior: T.transient
742− , onClose: handler_ (setPopoverVisible false)
743− , style: tw "mb-2"
744− }
745− [ nativeButton
746− { title: if popoverVisible then "Hide Popover" else "Show Popover"
747− , bezelStyle: T.push
748− , onPress: handler_ (setPopoverVisible (not popoverVisible))
749− , style: Style.style { height: 24.0, width: 140.0 }
750− }
751− , view { style: tw "p-4" <> Style.style { width: 200.0, height: 80.0 } }
752− [ text { style: tw "text-sm font-semibold" <> Style.style { color: p.fg } } "Popover Content"
753− , text { style: tw "text-xs mt-1" <> Style.style { color: p.dimFg } } "Click outside to dismiss"
754− ]
755− ]
756−
757− , sectionTitle p.fg "Image"
758− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
759− "Native NSImageView with URL loading and corner radius"
760− , nativeImage
761− { source: "https://placedog.net/640/400"
762− , contentMode: T.scaleProportionally
763− , cornerRadius: 12.0
764− , style: Style.style { height: 200.0 } <> tw "mb-2"
765− }
766−
767− , sectionTitle p.fg "Checkbox"
768− , card p.cardBg
769− [ nativeCheckbox
770− { checked: checkboxA
771− , title: "Enable notifications"
772− , enabled: true
773− , onChange: extractBool "checked" setCheckboxA
774− , style: Style.style { height: 24.0 } <> tw "mb-1"
775− }
776− , nativeCheckbox
777− { checked: checkboxB
778− , title: "Dark mode"
779− , enabled: true
780− , onChange: extractBool "checked" setCheckboxB
781− , style: Style.style { height: 24.0 }
782− }
783− ]
784−
785− , sectionTitle p.fg "Radio Button"
786− , card p.cardBg
787− [ nativeRadioButton
788− { selected: radioChoice == "option1"
789− , title: "Small"
790− , enabled: true
791− , onChange: handler_ (setRadioChoice "option1")
792− , style: Style.style { height: 24.0 } <> tw "mb-1"
793− }
794− , nativeRadioButton
795− { selected: radioChoice == "option2"
796− , title: "Medium"
797− , enabled: true
798− , onChange: handler_ (setRadioChoice "option2")
799− , style: Style.style { height: 24.0 } <> tw "mb-1"
800− }
801− , nativeRadioButton
802− { selected: radioChoice == "option3"
803− , title: "Large"
804− , enabled: true
805− , onChange: handler_ (setRadioChoice "option3")
806− , style: Style.style { height: 24.0 }
807− }
808− , label p.dimFg ("Selected: " <> radioChoice)
809− ]
810−
811− , sectionTitle p.fg "Search Field"
812− , nativeSearchField
813− { text: searchQuery
814− , placeholder: "Search..."
815− , onChangeText: extractString "text" setSearchQuery
816− , onSearch: extractString "text" \t -> setSearchResult ("Searched: " <> t)
817− , style: Style.style { height: 28.0 } <> tw "mb-2"
818− }
819− , if searchResult == "" then mempty
820− else label p.dimFg searchResult
821−
822− , sectionTitle p.fg "Token Field"
823− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
824− "Type text and press Enter to add tokens"
825− , nativeTokenField
826− { tokens: tagTokens
827− , placeholder: "Add tags..."
828− , onChangeTokens: handler
829− (nativeEvent >>> unsafeEventFn \e -> getFieldArray "tokens" e)
830− setTagTokens
831− , style: Style.style { height: 28.0 } <> tw "mb-2"
832− }
833−
834− , sectionTitle p.fg "Separator"
835− , nativeSeparator
836− { vertical: false
837− , style: Style.style { height: 2.0 } <> tw "my-2"
838− }
839−
840− , sectionTitle p.fg "Help Button"
841− , view { style: tw "flex-row items-center mb-2" }
842− [ nativeHelpButton
843− { onPress: handler_ (macosAlert T.informational "Help" "This is the help button. It opens contextual help." [ "OK" ])
844− , style: Style.style { width: 24.0, height: 24.0 }
845− }
846− , text { style: tw "text-xs ml-2" <> Style.style { color: p.dimFg } } "Click for help"
847− ]
848−
849− , sectionTitle p.fg "Path Control"
850− , nativePathControl
851− { url: "/Users"
852− , pathStyle: T.standardPath
853− , onSelectPath: extractString "url" \_ -> pure unit
854− , style: Style.style { height: 24.0 } <> tw "mb-2"
855− }
856−
857− , sectionTitle p.fg "Sheet"
858− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
859− "Modal sheet attached to the window"
860− , nativeButton
861− { title: "Show Sheet"
862− , bezelStyle: T.push
863− , onPress: handler_ (setSheetVisible true)
864− , style: Style.style { height: 24.0, width: 120.0 } <> tw "mb-2"
865− }
866− , nativeSheet
867− { visible: sheetVisible
868− , onDismiss: handler_ (setSheetVisible false)
869− }
870− [ view { style: tw "p-6" <> Style.style { width: 400.0, height: 200.0 } }
871− [ text { style: tw "text-lg font-semibold mb-2" <> Style.style { color: p.fg } } "Sheet Content"
872− , text { style: tw "text-sm mb-4" <> Style.style { color: p.dimFg } } "This is a native macOS sheet."
873− , nativeButton
874− { title: "Dismiss"
875− , bezelStyle: T.push
876− , onPress: handler_ (setSheetVisible false)
877− , style: Style.style { height: 24.0, width: 100.0 }
878− }
879− ]
880− ]
881−
882− , sectionTitle p.fg "Menu"
883− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
884− "Imperative popup menu at mouse position"
885− , view { style: tw "flex-row items-center mb-2" }
886− [ nativeButton
887− { title: "Show Menu"
888− , bezelStyle: T.push
889− , onPress: handler_
890− ( macosShowMenu
891− [ { title: "New File", id: "new" }
892− , { title: "Open...", id: "open" }
893− , { title: "-", id: "sep" }
894− , { title: "Save", id: "save" }
895− , { title: "Save As...", id: "saveAs" }
896− ]
897− \itemId -> setMenuResult2 ("Menu: " <> itemId)
898− )
899− , style: Style.style { height: 24.0, width: 120.0 }
900− }
901− , if menuResult2 == "" then mempty
902− else text { style: tw "text-xs ml-3" <> Style.style { color: p.dimFg } } menuResult2
903− ]
904−
905− , sectionTitle p.fg "Table View"
906− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
907− "Native NSTableView with columns and rows"
908− , nativeTableView
909− { columns:
910− [ { id: "name", title: "Name", width: 150.0 }
911− , { id: "type", title: "Type", width: 100.0 }
912− , { id: "size", title: "Size", width: 80.0 }
913− ]
914− , rows:
915− [ [ "Main.purs", "PureScript", "12 KB" ]
916− , [ "App.tsx", "TypeScript", "8 KB" ]
917− , [ "style.css", "CSS", "3 KB" ]
918− , [ "index.html", "HTML", "1 KB" ]
919− , [ "package.json", "JSON", "2 KB" ]
920− ]
921− , headerVisible: true
922− , alternatingRows: true
923− , onSelectRow: extractInt "rowIndex" \i -> setSelectedRow ("Row " <> show i)
924− , onDoubleClickRow: extractInt "rowIndex" \i -> setSelectedRow ("Double-clicked row " <> show i)
925− , style: Style.style { height: 180.0 } <> tw "rounded-lg overflow-hidden mb-2"
926− }
927− , if selectedRow == "" then mempty
928− else label p.dimFg selectedRow
929−
930− , sectionTitle p.fg "Clipboard"
931− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
932− "Read and write the system clipboard"
933− , view { style: tw "flex-row items-center mb-2" }
934− [ nativeButton
935− { title: "Copy 'Hello'"
936− , bezelStyle: T.push
937− , onPress: handler_ (copyToClipboard "Hello from PureScript!")
938− , style: Style.style { height: 24.0, width: 120.0 }
939− }
940− ]
941−
942− , sectionTitle p.fg "Alert"
943− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
944− "Native alert dialog"
945− , nativeButton
946− { title: "Show Alert"
947− , bezelStyle: T.push
948− , onPress: handler_ (macosAlert T.warning "Are you sure?" "This action cannot be undone." [ "Cancel", "OK" ])
949− , style: Style.style { height: 24.0, width: 120.0 } <> tw "mb-4"
950− }
951−
952− , sectionTitle p.fg "Outline View"
953− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
954− "Hierarchical tree list (NSOutlineView)"
955− , nativeOutlineView
956− { items: unsafeToForeign
957− [ { id: "src"
958− , title: "src"
959− , sfSymbol: "folder"
960− , children:
961− [ { id: "main", title: "Main.purs", sfSymbol: "doc", children: [] }
962− , { id: "macos"
963− , title: "MacOS"
964− , sfSymbol: "folder"
965− , children:
966− [ { id: "btn", title: "Button.purs", sfSymbol: "doc", children: [] }
967− , { id: "sl", title: "Slider.purs", sfSymbol: "doc", children: [] }
968− , { id: "sw", title: "Switch.purs", sfSymbol: "doc", children: [] }
969− ]
970− }
971− ]
972− }
973− , { id: "test"
974− , title: "test"
975− , sfSymbol: "folder"
976− , children:
977− [ { id: "t1", title: "MacOSComponents.test.js", sfSymbol: "doc", children: [] }
978− , { id: "t2", title: "MacOSSnapshots.test.js", sfSymbol: "doc", children: [] }
979− ]
980− }
981− , { id: "pkg", title: "package.json", sfSymbol: "doc.text", children: [] }
982− ]
983− , headerVisible: false
984− , onSelectItem: extractString "id" setOutlineSelection
985− , style: Style.style { height: 200.0 } <> tw "rounded-lg overflow-hidden mb-2"
986− }
987− , if outlineSelection == "" then mempty
988− else label p.dimFg ("Selected: " <> outlineSelection)
989−
990− , sectionTitle p.fg "Share"
991− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
992− "System share picker"
993− , nativeButton
994− { title: "Share Text"
995− , sfSymbol: "square.and.arrow.up"
996− , bezelStyle: T.push
997− , onPress: handler_ (macosShare [ "Hello from PureScript React Native!" ])
998− , style: Style.style { height: 24.0, width: 120.0 } <> tw "mb-2"
999− }
1000−
1001− , sectionTitle p.fg "Notifications"
1002− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
1003− "System notifications (requires permission)"
1004− , nativeButton
1005− { title: "Send Notification"
1006− , sfSymbol: "bell"
1007− , bezelStyle: T.push
1008− , onPress: handler_ (macosNotify "PureScript" "Hello from React Native macOS!")
1009− , style: Style.style { height: 24.0, width: 160.0 } <> tw "mb-2"
1010− }
1011−
1012− , sectionTitle p.fg "Sound"
1013− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
1014− "Play system sounds"
1015− , view { style: tw "flex-row items-center mb-2" }
1016− [ nativeButton
1017− { title: "Glass"
1018− , bezelStyle: T.push
1019− , onPress: handler_ (macosPlaySound "Glass")
1020− , style: Style.style { height: 24.0, width: 80.0 }
1021− }
1022− , nativeButton
1023− { title: "Ping"
1024− , bezelStyle: T.push
1025− , onPress: handler_ (macosPlaySound "Ping")
1026− , style: Style.style { height: 24.0, width: 80.0, marginLeft: 8.0 }
1027− }
1028− , nativeButton
1029− { title: "Beep"
1030− , bezelStyle: T.push
1031− , onPress: handler_ macosBeep
1032− , style: Style.style { height: 24.0, width: 80.0, marginLeft: 8.0 }
1033− }
1034− ]
1035−
1036− , sectionTitle p.fg "Status Bar"
1037− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
1038− "Menu bar icon with dropdown"
1039− , view { style: tw "flex-row items-center mb-4" }
1040− [ nativeButton
1041− { title: if statusBarActive then "Remove" else "Add to Menu Bar"
1042− , sfSymbol: if statusBarActive then "minus.circle" else "plus.circle"
1043− , bezelStyle: T.push
1044− , onPress: handler_
1045− ( if statusBarActive then do
1046− macosRemoveStatusBarItem
1047− setStatusBarActive false
1048− else do
1049− macosSetStatusBarItem
1050− { title: ""
1051− , sfSymbol: "swift"
1052− , menuItems:
1053− [ { id: "about", title: "About PureScript RN" }
1054− , { id: "sep", title: "-" }
1055− , { id: "quit", title: "Quit" }
1056− ]
1057− }
1058− setStatusBarActive true
1059− )
1060− , style: Style.style { height: 24.0, width: 180.0 }
1061− }
1062− ]
1063− ]
1064− )
1065−
1066−-- Tab 6: AI & Tools
1067−type AIProps =
1068− { fg :: String
1069− , dimFg :: String
1070− , cardBg :: String
1071− , bg :: String
1072− }
1073−
1074−aiTab :: AIProps -> JSX
1075−aiTab = component "AITab" \p -> React.do
1076− speechText /\ setSpeechText <- useState' "Hello from PureScript React Native on macOS!"
1077− ocrResult /\ setOcrResult <- useState' ""
1078− listening /\ setListening <- useState' false
1079− transcript /\ setTranscript <- useState' ""
1080− nlText /\ setNlText <- useState' "I love this amazing app! C'est magnifique."
1081− nlResult /\ setNlResult <- useState' ""
1082− useEffect listening do
1083− if listening then do
1084− timerId <- setInterval 500 do
1085− macosGetTranscript \t -> setTranscript t
1086− pure (clearInterval timerId)
1087− else pure (pure unit)
1088− pure do
1089− nativeScrollView { style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
1090− ( view { style: tw "px-4 pb-4" }
1091− [ sectionTitle p.fg "Map"
1092− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
1093− "Embedded MKMapView (San Francisco)"
1094− , nativeMapView
1095− { latitude: 37.7749
1096− , longitude: -122.4194
1097− , latitudeDelta: 0.05
1098− , longitudeDelta: 0.05
1099− , mapType: "standard"
1100− , showsUserLocation: false
1101− , annotations: unsafeToForeign
1102− [ { latitude: 37.7749, longitude: -122.4194, title: "San Francisco", subtitle: "California" }
1103− , { latitude: 37.8199, longitude: -122.4783, title: "Golden Gate Bridge", subtitle: "" }
1104− ]
1105− , onRegionChange: handler_ (pure unit)
1106− , onSelectAnnotation: handler_ (pure unit)
1107− , style: Style.style { height: 250.0 }
1108− }
1109− , sectionTitle p.fg "PDF Viewer"
1110− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
1111− "PDFKit document viewer"
1112− , nativePDFView
1113− { source: "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
1114− , autoScales: true
1115− , displayMode: "singlePageContinuous"
1116− , onPageChange: handler_ (pure unit)
1117− , style: Style.style { height: 300.0 }
1118− }
1119− , sectionTitle p.fg "Quick Look"
1120− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
1121− "Preview files with QLPreviewPanel"
1122− , view { style: tw "flex-row items-center mb-4" }
1123− [ nativeButton
1124− { title: "Preview /etc/hosts"
1125− , sfSymbol: "eye"
1126− , bezelStyle: T.push
1127− , onPress: handler_ (macosQuickLook "/etc/hosts")
1128− , style: Style.style { height: 24.0, width: 180.0 }
1129− }
1130− ]
1131− , sectionTitle p.fg "Speech"
1132− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
1133− "Text-to-speech (NSSpeechSynthesizer)"
1134− , nativeTextField
1135− { text: speechText
1136− , placeholder: "Text to speak..."
1137− , onChangeText: extractString "text" setSpeechText
1138− , style: Style.style { height: 24.0 } <> tw "mb-2"
1139− }
1140− , view { style: tw "flex-row items-center mb-4" }
1141− [ nativeButton
1142− { title: "Speak"
1143− , sfSymbol: "speaker.wave.2"
1144− , bezelStyle: T.push
1145− , onPress: handler_ (macosSay speechText)
1146− , style: Style.style { height: 24.0, width: 100.0 }
1147− }
1148− , nativeButton
1149− { title: "Stop"
1150− , sfSymbol: "stop.circle"
1151− , bezelStyle: T.push
1152− , onPress: handler_ macosStopSpeaking
1153− , style: Style.style { height: 24.0, width: 100.0, marginLeft: 8.0 }
1154− }
1155− ]
1156− , sectionTitle p.fg "Color Picker"
1157− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
1158− "System color panel (NSColorPanel)"
1159− , view { style: tw "flex-row items-center mb-4" }
1160− [ nativeButton
1161− { title: "Show Color Panel"
1162− , sfSymbol: "paintpalette"
1163− , bezelStyle: T.push
1164− , onPress: handler_ macosShowColorPanel
1165− , style: Style.style { height: 24.0, width: 180.0 }
1166− }
1167− ]
1168− , sectionTitle p.fg "Font Panel"
1169− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
1170− "System font panel (NSFontPanel)"
1171− , view { style: tw "flex-row items-center mb-4" }
1172− [ nativeButton
1173− { title: "Show Font Panel"
1174− , sfSymbol: "textformat"
1175− , bezelStyle: T.push
1176− , onPress: handler_ macosShowFontPanel
1177− , style: Style.style { height: 24.0, width: 180.0 }
1178− }
1179− ]
1180− , sectionTitle p.fg "OCR (Vision)"
1181− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
1182− "Recognize text in images (VNRecognizeTextRequest)"
1183− , view { style: tw "flex-row items-center mb-2" }
1184− [ nativeFilePicker
1185− { mode: T.openFile
1186− , allowedTypes: [ "public.image" ]
1187− , title: "Pick Image"
1188− , sfSymbol: "photo"
1189− , onPickFiles: handler
1190− (nativeEvent >>> unsafeEventFn \e -> getFieldArray "files" e)
1191− \paths -> for_ paths \path -> do
1192− setOcrResult "Recognizing..."
1193− macosOCR path \result -> setOcrResult result
1194− , onCancel: handler_ (pure unit)
1195− , style: Style.style { height: 24.0, width: 140.0 }
1196− }
1197− ]
1198− , if ocrResult /= "" then view { style: tw "p-2 rounded mb-4" <> Style.style { backgroundColor: p.cardBg } }
1199− [ text { style: tw "text-xs font-mono" <> Style.style { color: p.fg } } ocrResult ]
1200− else view {} []
1201− , sectionTitle p.fg "Speech Recognition"
1202− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
1203− "Live microphone-to-text (click Start, speak, click Stop)"
1204− , view { style: tw "flex-row items-center mb-2" }
1205− [ nativeButton
1206− { title: if listening then "Listening..." else "Start"
1207− , sfSymbol: if listening then "mic.fill" else "mic"
1208− , bezelStyle: T.push
1209− , onPress: handler_ do
1210− setListening true
1211− setTranscript ""
1212− macosStartListening
1213− , style: Style.style { height: 24.0, width: 130.0 }
1214− }
1215− , nativeButton
1216− { title: "Stop"
1217− , sfSymbol: "stop.circle"
1218− , bezelStyle: T.push
1219− , onPress: handler_ do
1220− setListening false
1221− macosStopListening \finalText -> do
1222− setTranscript finalText
1223− , style: Style.style { height: 24.0, width: 80.0, marginLeft: 8.0 }
1224− }
1225− ]
1226− , if transcript /= "" then view { style: tw "p-2 rounded mb-4" <> Style.style { backgroundColor: p.cardBg } }
1227− [ text { style: tw "text-xs font-mono" <> Style.style { color: p.fg } } transcript ]
1228− else view {} []
1229− , sectionTitle p.fg "Natural Language"
1230− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
1231− "Language detection, sentiment, tokenization (NaturalLanguage.framework)"
1232− , nativeTextField
1233− { text: nlText
1234− , placeholder: "Enter text to analyze..."
1235− , onChangeText: extractString "text" setNlText
1236− , style: Style.style { height: 24.0 } <> tw "mb-2"
1237− }
1238− , view { style: tw "flex-row items-center mb-2" }
1239− [ nativeButton
1240− { title: "Language"
1241− , sfSymbol: "globe"
1242− , bezelStyle: T.push
1243− , onPress: handler_ do
1244− macosDetectLanguage nlText \lang ->
1245− setNlResult ("Language: " <> lang)
1246− , style: Style.style { height: 24.0, width: 110.0 }
1247− }
1248− , nativeButton
1249− { title: "Sentiment"
1250− , sfSymbol: "face.smiling"
1251− , bezelStyle: T.push
1252− , onPress: handler_ do
1253− macosAnalyzeSentiment nlText \score ->
1254− setNlResult ("Sentiment: " <> show score)
1255− , style: Style.style { height: 24.0, width: 110.0, marginLeft: 8.0 }
1256− }
1257− , nativeButton
1258− { title: "Tokenize"
1259− , sfSymbol: "text.word.spacing"
1260− , bezelStyle: T.push
1261− , onPress: handler_ do
1262− macosTokenize nlText \tokens ->
1263− setNlResult ("Tokens: " <> joinWith ", " tokens)
1264− , style: Style.style { height: 24.0, width: 110.0, marginLeft: 8.0 }
1265− }
1266− ]
1267− , if nlResult /= "" then view { style: tw "p-2 rounded mb-4" <> Style.style { backgroundColor: p.cardBg } }
1268− [ text { style: tw "text-xs font-mono" <> Style.style { color: p.fg } } nlResult ]
1269− else view {} []
1270− ]
1271− )
1272−
1273−-- Tab 7: Chat (Matrix)
1274−type ChatProps =
1275− { fg :: String
1276− , dimFg :: String
1277− , cardBg :: String
1278− , bg :: String
1279− , isDark :: Boolean
1280− }
1281−
1282−type Message =
1283− { sender :: String
1284− , body :: String
1285− , kind :: String -- "text" | "sticker" | "gif" | "video"
1286− , eventId :: String
1287− }
1288−
1289−type Session =
1290− { homeserver :: String
1291− , accessToken :: String
1292− , userId :: String
1293− }
1294−
1295−type Room =
1296− { roomId :: String
1297− , name :: String
1298− , lastMessage :: String
1299− }
1300−
1301−avatarColors :: Array String
1302−avatarColors = [ "#5856D6", "#FF9500", "#34C759", "#FF2D55", "#5AC8FA", "#FF6B6B", "#4ECDC4", "#45B7D1" ]
1303−
1304−avatarColor :: String -> String
1305−avatarColor roomId = fromMaybe "#5856D6" (avatarColors !! (mod (hashCode roomId) (length avatarColors)))
1306− where
1307− hashCode s = abs (foldl (\acc c -> acc * 31 + c) 0 (charCodes s))
1308−
1309−foreign import charCodes :: String -> Array Int
1310−foreign import abs :: Int -> Int
1311−
1312−chatTab :: ChatProps -> JSX
1313−chatTab = component "ChatTab" \p -> React.do
1314− session /\ setSession <- useState' (Nothing :: Maybe Session)
1315− rooms /\ setRooms <- useState' ([] :: Array Room)
1316− activeRoom /\ setActiveRoom <- useState' (Nothing :: Maybe String)
1317− messages /\ setMessages <- useState ([] :: Array Message)
1318− inputText /\ setInputText <- useState' ""
1319− loginError /\ setLoginError <- useState' ""
1320− loginServer /\ setLoginServer <- useState' "https://matrix.org"
1321− loginUser /\ setLoginUser <- useState' ""
1322− loginPass /\ setLoginPass <- useState' ""
1323− syncTokenRef <- useRef ""
1324− activeRoomRef <- useRef (Nothing :: Maybe String)
1325− sessionRef <- useRef (Nothing :: Maybe Session)
1326− seenIdsRef <- useRef ([] :: Array String)
1327−
1328− -- Sync polling effect
1329− useEffect session do
1330− case session of
1331− Nothing -> pure mempty
1332− Just sess -> do
1333− writeRef sessionRef (Just sess)
1334− let
1335− doSync = launchAff_ do
1336− token <- readRef syncTokenRef # liftEffect
1337− result <- try (Matrix.sync sess.homeserver sess.accessToken token 5000)
1338− case result of
1339− Left _ -> pure unit
1340− Right resp -> liftEffect do
1341− let nextBatch = Matrix.getSyncNextBatch resp
1342− writeRef syncTokenRef nextBatch
1343− let roomIds = Matrix.getSyncJoinedRoomIds resp
1344− currentRoom <- readRef activeRoomRef
1345− for_ roomIds \rid -> do
1346− let events = Matrix.getSyncTimelineEvents resp rid
1347− let msgEvents = filter (\ev -> Matrix.getEventType ev == "m.room.message") events
1348− case currentRoom of
1349− Just cr | cr == rid -> do
1350− seen <- readRef seenIdsRef
1351− let newMsgs = filter (\ev -> not (Matrix.getEventId ev `elem` seen)) msgEvents
1352− let
1353− converted = newMsgs <#> \ev ->
1354− { sender: Matrix.getEventSender ev
1355− , body: Matrix.getEventBody ev
1356− , kind: eventKind ev
1357− , eventId: Matrix.getEventId ev
1358− }
1359− if null converted then pure unit
1360− else do
1361− let newIds = converted <#> _.eventId
1362− writeRef seenIdsRef (seen <> newIds)
1363− setMessages \msgs -> msgs <> converted
1364− _ -> pure unit
1365− timerId <- setInterval 3000 doSync
1366− doSync
1367− pure (clearInterval timerId)
1368−
1369− let
1370− doLogin = launchAff_ do
1371− result <- try (Matrix.login loginServer loginUser loginPass)
1372− case result of
1373− Left _ -> liftEffect (setLoginError "Login failed. Check credentials.")
1374− Right resp -> do
1375− let sess = { homeserver: loginServer, accessToken: resp.access_token, userId: resp.user_id }
1376− roomIds <- try (Matrix.joinedRooms loginServer resp.access_token)
1377− case roomIds of
1378− Left _ -> liftEffect do
1379− setSession (Just sess)
1380− setLoginError ""
1381− Right rids -> do
1382− names <- for rids \rid -> do
1383− n <- try (Matrix.roomName loginServer resp.access_token rid)
1384− let
1385− name = case n of
1386− Left _ -> rid
1387− Right "" -> rid
1388− Right nm -> nm
1389− pure { roomId: rid, name, lastMessage: "" }
1390− liftEffect do
1391− setSession (Just sess)
1392− setRooms names
1393− setLoginError ""
1394−
1395− selectRoom sess rid = do
1396− setActiveRoom (Just rid)
1397− writeRef activeRoomRef (Just rid)
1398− setMessages (const [])
1399− writeRef seenIdsRef []
1400− launchAff_ do
1401− result <- try (Matrix.sync sess.homeserver sess.accessToken "" 0)
1402− case result of
1403− Left _ -> pure unit
1404− Right resp -> liftEffect do
1405− let events = Matrix.getSyncTimelineEvents resp rid
1406− let msgEvents = filter (\ev -> Matrix.getEventType ev == "m.room.message") events
1407− let
1408− converted = msgEvents <#> \ev ->
1409− { sender: Matrix.getEventSender ev
1410− , body: Matrix.getEventBody ev
1411− , kind: eventKind ev
1412− , eventId: Matrix.getEventId ev
1413− }
1414− let ids = converted <#> _.eventId
1415− writeRef seenIdsRef ids
1416− setMessages (const converted)
1417−
1418− sendMessage sess rid msg = do
1419− if msg == "" then pure unit
1420− else do
1421− setInputText ""
1422− launchAff_ do
1423− txnId <- Matrix.genTxnId # liftEffect
1424− _ <- try (Matrix.sendMessage sess.homeserver sess.accessToken rid txnId msg)
1425− pure unit
1426−
1427− sentBubbleBg = if p.isDark then "#65B86A" else "#5CB85C"
1428− receivedBubbleBg = if p.isDark then "#3B3B3D" else "#FFFFFF"
1429− chatBg = if p.isDark then "#17212B" else "#E8DFD3"
1430−
1431− loginForm = view
1432− { style: tw "flex-1 items-center justify-center"
1433− <> Style.style { backgroundColor: chatBg }
1434− }
1435− [ view { style: tw "rounded-xl p-6" <> Style.style { backgroundColor: p.cardBg, width: 340.0 } }
1436− [ text { style: tw "text-lg font-bold mb-4 text-center" <> Style.style { color: p.fg } } "Matrix Login"
1437− , text { style: tw "text-xs mb-1" <> Style.style { color: p.dimFg } } "Homeserver"
1438− , nativeTextField
1439− { text: loginServer
1440− , placeholder: "https://matrix.org"
1441− , search: false
1442− , rounded: true
1443− , onChangeText: extractString "text" setLoginServer
1444− , onSubmit: handler_ (pure unit)
1445− , style: tw "mb-3" <> Style.style { height: 28.0 }
1446− }
1447− , text { style: tw "text-xs mb-1" <> Style.style { color: p.dimFg } } "Username"
1448− , nativeTextField
1449− { text: loginUser
1450− , placeholder: "username"
1451− , search: false
1452− , rounded: true
1453− , onChangeText: extractString "text" setLoginUser
1454− , onSubmit: handler_ (pure unit)
1455− , style: tw "mb-3" <> Style.style { height: 28.0 }
1456− }
1457− , text { style: tw "text-xs mb-1" <> Style.style { color: p.dimFg } } "Password"
1458− , nativeTextField
1459− { text: loginPass
1460− , placeholder: "password"
1461− , search: false
1462− , rounded: true
1463− , onChangeText: extractString "text" setLoginPass
1464− , onSubmit: handler_ doLogin
1465− , style: tw "mb-4" <> Style.style { height: 28.0 }
1466− }
1467− , if loginError /= "" then
1468− text { style: tw "text-xs mb-2 text-center" <> Style.style { color: "#FF3B30" } } loginError
1469− else text { style: Style.style { height: 0.0 } } ""
1470− , nativeButton
1471− { title: "Sign In"
1472− , bezelStyle: T.push
1473− , sfSymbol: "arrow.right.circle.fill"
1474− , onPress: handler_ doLogin
1475− , style: Style.style { height: 32.0 }
1476− }
1477− ]
1478− ]
1479−
1480− roomSidebar sess = view { style: tw "pt-2" }
1481− ( rooms <#> \r ->
1482− view
1483− { style: tw "flex-row items-center px-3 py-2 mx-2 rounded-lg"
1484− <> Style.style
1485− { backgroundColor: if activeRoom == Just r.roomId then (if p.isDark then "#2B5278" else "#419FD9") else "transparent" }
1486− }
1487− [ view
1488− { style: tw "rounded-full items-center justify-center"
1489− <> Style.style { width: 32.0, height: 32.0, backgroundColor: avatarColor r.roomId }
1490− }
1491− [ text { style: tw "text-xs font-bold" <> Style.style { color: "#FFFFFF" } } (take 1 r.name) ]
1492− , view { style: tw "ml-2 flex-1" }
1493− [ text
1494− { style: tw "text-sm font-semibold"
1495− <> Style.style { color: if activeRoom == Just r.roomId then "#FFFFFF" else p.fg }
1496− }
1497− r.name
1498− , text
1499− { style: tw "text-xs"
1500− <> Style.style { color: if activeRoom == Just r.roomId then "#FFFFFFCC" else p.dimFg }
1501− }
1502− r.lastMessage
1503− ]
1504− , nativeButton
1505− { title: ""
1506− , bezelStyle: T.toolbar
1507− , sfSymbol: ""
1508− , onPress: handler_ (selectRoom sess r.roomId)
1509− , style: Style.style { position: "absolute", top: 0.0, left: 0.0, right: 0.0, bottom: 0.0, opacity: 0.0 }
1510− }
1511− ]
56+ )
151257 )
1513−
1514− messageBubble sess _ msg = do
1515− let
1516− isMine = case sess of
1517− Just s -> msg.sender == s.userId
1518− Nothing -> false
1519− let align = if isMine then "flex-end" else "flex-start"
1520− let bigEmoji = msg.kind == "text" && isSingleEmoji msg.body
1521− let senderLabel = if isMine then "" else msg.sender
1522− nativeContextMenu
1523− { items:
1524− [ { id: "copy", title: "Copy", sfSymbol: "doc.on.doc" }
1525− , { id: "reply", title: "Reply", sfSymbol: "arrowshape.turn.up.left" }
1526− , { id: "sep", title: "-", sfSymbol: "" }
1527− , { id: "delete", title: "Delete", sfSymbol: "trash" }
1528− ]
1529− , onSelectItem: handler_ (pure unit)
1530− , style: tw "px-3 mb-1"
1531− }
1532− ( view { style: Style.style { alignSelf: align, maxWidth: 320.0 } }
1533− [ if senderLabel == "" then text { style: Style.style { height: 0.0 } } ""
1534− else text { style: tw "text-xs mb-0.5" <> Style.style { color: p.dimFg } } senderLabel
1535− , if bigEmoji then
1536− text
1537− { style: Style.style { fontSize: 64.0, lineHeight: 72.0 } }
1538− msg.body
1539− else
1540− view
1541− { style: tw "rounded-2xl px-3 py-2"
1542− <> Style.style
1543− { backgroundColor: if isMine then sentBubbleBg else receivedBubbleBg }
1544− }
1545− [ text
1546− { style: tw "text-sm"
1547− <> Style.style { color: if isMine then "#FFFFFF" else (if p.isDark then "#FFFFFF" else "#000000") }
1548− }
1549− msg.body
1550− ]
1551− ]
1552− )
1553−
1554− activeRoomName = case activeRoom of
1555− Nothing -> "Select a room"
1556− Just rid -> fromMaybe rid (map _.name (filter (\r -> r.roomId == rid) rooms !! 0))
1557−
1558− chatView sess = sidebarLayout
1559− { sidebar: roomSidebar sess
1560− , sidebarWidth: 220.0
1561− , content: view { style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
1562− [ view
1563− { style: tw "px-4 py-2 border-b"
1564− <> Style.style { borderBottomWidth: 0.5, borderColor: p.dimFg, backgroundColor: "transparent" }
1565− }
1566− [ text { style: tw "text-base font-semibold" <> Style.style { color: p.fg } } activeRoomName ]
1567− , nativePatternBackground
1568− { patternColor: if p.isDark then "#FFFFFF" else "#000000"
1569− , backgroundColor2: chatBg
1570− , patternOpacity: if p.isDark then 0.04 else 0.06
1571− , patternScale: 1.0
1572− , style: tw "flex-1"
1573− }
1574− ( nativeScrollView { scrollToBottom: length messages, style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
1575− ( view { style: tw "py-2 pr-3" }
1576− (mapWithIndex (messageBubble session) messages)
1577− )
1578− )
1579− , view
1580− { style: tw "flex-row items-center px-3 py-2"
1581− <> Style.style { borderTopWidth: 0.5, borderColor: p.dimFg, backgroundColor: "transparent" }
1582− }
1583− ( case activeRoom of
1584− Nothing ->
1585− [ text { style: tw "text-sm" <> Style.style { color: p.dimFg } } "Select a room to start chatting" ]
1586− Just rid ->
1587− [ nativeTextField
1588− { text: inputText
1589− , placeholder: "Message..."
1590− , search: false
1591− , rounded: true
1592− , onChangeText: extractString "text" setInputText
1593− , onSubmit: extractString "text" \t -> sendMessage sess rid t
1594− , style: tw "flex-1" <> Style.style { height: 28.0 }
1595− }
1596− , nativeButton
1597− { title: ""
1598− , sfSymbol: "paperplane.fill"
1599− , bezelStyle: T.toolbar
1600− , onPress: handler_ (sendMessage sess rid inputText)
1601− , style: Style.style { height: 28.0, width: 36.0, marginLeft: 8.0 }
1602− }
1603− ]
1604− )
1605− ]
1606− }
1607−
1608− pure do
1609− case session of
1610− Nothing -> loginForm
1611− Just sess -> chatView sess
1612−
1613−eventKind :: Matrix.MatrixEvent -> String
1614−eventKind ev = case Matrix.getEventMsgType ev of
1615− "m.image" -> "gif"
1616− _ -> "text"
1617−
1618−elem :: forall a. Eq a => a -> Array a -> Boolean
1619−elem x xs = not (null (filter (_ == x) xs))
1620−
1621−foreign import setInterval :: Int -> Effect Unit -> Effect Int
1622−foreign import clearInterval :: Int -> Effect Unit
1623−
1624−-- UI helpers
1625−sectionTitle :: String -> String -> JSX
1626−sectionTitle color title =
1627− text { style: tw "text-sm font-semibold mt-4 mb-1" <> Style.style { color } } title
1628−
1629−card :: String -> Array JSX -> JSX
1630−card bg children =
1631− view { style: tw "rounded-lg p-3 mb-2" <> Style.style { backgroundColor: bg, overflow: "hidden" } }
1632− children
1633−
1634−label :: String -> String -> JSX
1635−label color str =
1636− text { style: tw "ml-3 text-sm" <> Style.style { color } } str
1637−
1638−round :: Number -> Int
1639−round n = unsafeRound n
1640−
1641−foreign import unsafeRound :: Number -> Int
1642−foreign import getFieldJSON :: String -> forall r. r -> String
1643−foreign import isSingleEmoji :: String -> Boolean