@@ -36,7 +36,6 @@ import Yoga.React.Native.MacOS.Toolbar (nativeToolbar)
3636import Yoga.React.Native.MacOS.VisualEffect (nativeVisualEffect)
3737import Yoga.React.Native.MacOS.Sidebar (sidebarLayout)
3838import Yoga.React.Native.MacOS.ContextMenu (nativeContextMenu)
39−
4039import Yoga.React.Native.MacOS.FilePicker (nativeFilePicker)
4140import Yoga.React.Native.MacOS.VideoPlayer (nativeVideoPlayer)
4241import Yoga.React.Native.MacOS.AnimatedImage (nativeAnimatedImage)
@@ -80,28 +79,28 @@ import Yoga.React.Native.MacOS.Types as T
8079import Yoga.React.Native.Matrix as Matrix
8180import Yoga.React.Native.Style as Style
8281
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+
8391main :: Effect Unit
8492main = registerComponent "YogaReactExample" \_ -> app {}
8593
8694app :: {} -> JSX
8795app = component "App" \_ -> React.do
88− activeTab /\ setActiveTab <- useState' "controls"
89− sliderValue /\ setSliderValue <- useState' 50.0
90− switchOn /\ setSwitchOn <- useState' false
91− selectedColor /\ setSelectedColor <- useState' "#FF6600"
92− popUpIndex /\ setPopUpIndex <- useState' 0
93− popUpTitle /\ setPopUpTitle <- useState' "Small"
94− searchText /\ setSearchText <- useState' ""
95− buttonStatus /\ setButtonStatus <- useState' "Ready"
96− dateText /\ setDateText <- useState' ""
97− browserUrl /\ setBrowserUrl <- useState' "https://pursuit.purescript.org"
98− urlBarText /\ setUrlBarText <- useState' "https://pursuit.purescript.org"
96+ selectedItem /\ setSelectedItem <- useState' "button"
9997 colorScheme <- useColorScheme
10098 let isDark = toNullable (Just "dark") == colorScheme
10199 let fg = if isDark then "#FFFFFF" else "#000000"
102100 let dimFg = if isDark then "#999999" else "#666666"
103101 let cardBg = if isDark then "#2A2A2A" else "#F0F0F0"
104102 let bg = if isDark then "#1E1E1E" else "#FFFFFF"
103+ let dp = { fg, dimFg, cardBg, bg, isDark } :: DemoProps
105104 pure do
106105 nativeVisualEffect
107106 { materialName: T.windowBackground
@@ -110,156 +109,244 @@ app = component "App" \_ -> React.do
110109 ( safeAreaView { style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
111110 ( view { style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
112111 [ nativeToolbar
113− { items:
114− [ { id: "controls", label: "Controls", sfSymbol: "slider.horizontal.3" }
115− , { id: "editor", label: "Editor", sfSymbol: "doc.richtext" }
116− , { id: "browser", label: "Browser", sfSymbol: "globe" }
117− , { id: "rive", label: "Rive", sfSymbol: "play.circle" }
118− , { id: "system", label: "System", sfSymbol: "gearshape.2" }
119− , { id: "ai", label: "AI", sfSymbol: "brain" }
120− , { id: "chat", label: "Chat", sfSymbol: "bubble.left.and.bubble.right" }
121− ]
122− , selectedItem: activeTab
112+ { items: []
113+ , selectedItem: ""
123114 , toolbarStyle: T.unified
124115 , windowTitle: "PureScript React Native"
125− , onSelectItem: E.onString "itemId" setActiveTab
116+ , onSelectItem: handler_ (pure unit)
126117 , style: Style.style { height: 0.0, width: 0.0 }
127118 }
128− , view { style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
129− [ if activeTab == "controls" then controlsTab
130− { sliderValue
131− , setSliderValue
132− , switchOn
133− , setSwitchOn
134− , selectedColor
135− , setSelectedColor
136− , popUpIndex
137− , setPopUpIndex
138− , popUpTitle
139− , setPopUpTitle
140− , searchText
141− , setSearchText
142− , buttonStatus
143− , setButtonStatus
144− , dateText
145− , setDateText
146− , fg
147− , dimFg
148− , cardBg
149− }
150− else if activeTab == "editor" then editorTab { fg }
151− else if activeTab == "browser" then browserTab
152− { browserUrl
153− , setBrowserUrl
154− , urlBarText
155− , setUrlBarText
156− , fg
157− , dimFg
158− }
159− else if activeTab == "rive" then riveTab { fg, bg }
160− else if activeTab == "system" then systemTab { fg, dimFg, cardBg, bg }
161− else if activeTab == "ai" then aiTab { fg, dimFg, cardBg, bg }
162− else chatTab { fg, dimFg, cardBg, bg, isDark }
163− ]
119+ , sidebarLayout
120+ { sidebar: outlineSidebar selectedItem setSelectedItem
121+ , sidebarWidth: 200.0
122+ , content: demoContent dp selectedItem
123+ }
164124 ]
165125 )
166126 )
167127
168−-- Tab 0: Controls
169−type ControlsProps =
170− { sliderValue :: Number
171− , setSliderValue :: Number -> Effect Unit
172− , switchOn :: Boolean
173− , setSwitchOn :: Boolean -> Effect Unit
174− , selectedColor :: String
175− , setSelectedColor :: String -> Effect Unit
176− , popUpIndex :: Int
177− , setPopUpIndex :: Int -> Effect Unit
178− , popUpTitle :: String
179− , setPopUpTitle :: String -> Effect Unit
180− , searchText :: String
181− , setSearchText :: String -> Effect Unit
182− , buttonStatus :: String
183− , setButtonStatus :: String -> Effect Unit
184− , dateText :: String
185− , setDateText :: String -> Effect Unit
186− , fg :: String
187− , dimFg :: String
188− , cardBg :: String
189− }
190−
191−sidebarItem :: String -> String -> String -> Boolean -> (String -> Effect Unit) -> JSX
192−sidebarItem _ sfSymbolName title selected setCategory =
193− nativeButton
194− { title
195− , sfSymbol: sfSymbolName
196− , bezelStyle: T.toolbar
197− , primary: selected
198− , onPress: handler_ (setCategory title)
199− , style: Style.style { height: 28.0, marginHorizontal: 8.0, marginVertical: 1.0 }
200− }
201−
202−controlsTab :: ControlsProps -> JSX
203−controlsTab = component "ControlsTab" \p -> React.do
204− category /\ setCategory <- useState' "All"
205− let categories = [ "All", "Button", "Switch", "Slider", "Pop Up", "Color", "Date", "Text" ]
128+-- Outline tree
129+outlineTree :: Array OutlineItem
130+outlineTree =
206131 let
207− sidebar = view { style: tw "pt-2" }
208− (categories <#> \cat -> sidebarItem p.fg "" cat (category == cat) setCategory)
209− let show' name = category == "All" || category == name
210− let
211− buttonSection =
212− [ sectionTitle p.fg "Button"
213− , card p.cardBg
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
214293 [ view { style: tw "flex-row items-center" }
215294 [ nativeButton
216295 { title: "Say Hello"
217296 , sfSymbol: "hand.wave"
218297 , bezelStyle: T.push
219298 , primary: true
220− , onPress: handler_ (p.setButtonStatus "Hello from PureScript!")
299+ , onPress: handler_ (setStatus "Hello from PureScript!")
221300 , style: Style.style { height: 24.0, width: 140.0 }
222301 }
223302 , nativeButton
224303 { title: "Reset"
225304 , sfSymbol: "arrow.counterclockwise"
226305 , bezelStyle: T.push
227− , onPress: handler_ (p.setButtonStatus "Ready")
306+ , onPress: handler_ (setStatus "Ready")
228307 , style: Style.style { height: 24.0, width: 100.0, marginLeft: 8.0 }
229308 }
230− , label p.dimFg p.buttonStatus
309+ , label dp.dimFg status
231310 ]
232311 ]
233312 ]
234− let
235− switchSection =
236− [ sectionTitle p.fg "Switch"
237− , card p.cardBg
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
238321 [ view { style: tw "flex-row items-center" }
239322 [ nativeSwitch
240− { on: p.switchOn
241− , onChange: E.onBool "on" p.setSwitchOn
323+ { on
324+ , onChange: E.onBool "on" setOn
242325 , style: Style.style { height: 24.0, width: 48.0 }
243326 }
244− , label p.dimFg (if p.switchOn then "On" else "Off")
327+ , label dp.dimFg (if on then "On" else "Off")
245328 ]
246329 ]
247330 ]
248− let
249− sliderSection =
250− [ sectionTitle p.fg "Slider + Level Indicator + Progress"
251− , card p.cardBg
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
252339 [ nativeSlider
253− { value: p.sliderValue
340+ { value
254341 , minValue: 0.0
255342 , maxValue: 100.0
256343 , numberOfTickMarks: 11
257− , onChange: E.onNumber "value" p.setSliderValue
344+ , onChange: E.onNumber "value" setValue
258345 , style: Style.style { height: 24.0 }
259346 }
260− , label p.dimFg ("Value: " <> show (round p.sliderValue) <> " / 100")
347+ , label dp.dimFg ("Value: " <> show (round value) <> " / 100")
261348 , nativeLevelIndicator
262− { value: p.sliderValue
349+ { value
263350 , minValue: 0.0
264351 , maxValue: 100.0
265352 , warningValue: 70.0
@@ -267,104 +354,241 @@ controlsTab = component "ControlsTab" \p -> React.do
267354 , style: Style.style { height: 18.0, marginTop: 8.0 }
268355 }
269356 , nativeProgress
270− { value: p.sliderValue
357+ { value
271358 , style: Style.style { height: 18.0, marginTop: 8.0 }
272359 }
273360 ]
274361 ]
275− let
276− popUpSection =
277− [ sectionTitle p.fg "Pop Up Button"
278− , card p.cardBg
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
279371 [ view { style: tw "flex-row items-center" }
280372 [ nativePopUp
281373 { items: [ "Small", "Medium", "Large", "Extra Large" ]
282− , selectedIndex: p.popUpIndex
374+ , selectedIndex: idx
283375 , onChange: handler
284376 ( nativeEvent >>> unsafeEventFn \e ->
285− { idx: (E.getFieldInt "selectedIndex" e), title: (E.getFieldStr "title" e) }
377+ { idx: E.getFieldInt "selectedIndex" e, title: E.getFieldStr "title" e }
286378 )
287379 \r -> do
288− p.setPopUpIndex r.idx
289− p.setPopUpTitle r.title
380+ setIdx r.idx
381+ setTitle r.title
290382 , style: Style.style { height: 24.0, width: 160.0 }
291383 }
292− , label p.dimFg ("Selected: " <> p.popUpTitle)
384+ , label dp.dimFg ("Selected: " <> title)
293385 ]
294386 ]
295387 ]
296− let
297− colorSection =
298− [ sectionTitle p.fg "Color Well"
299− , card p.cardBg
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
300459 [ view { style: tw "flex-row items-center" }
301460 [ nativeColorWell
302− { color: p.selectedColor
461+ { color
303462 , minimal: true
304− , onChange: E.onString "color" p.setSelectedColor
463+ , onChange: E.onString "color" setColor
305464 , style: Style.style { height: 32.0, width: 48.0 }
306465 }
307466 , view
308467 { style: tw "ml-3 rounded" <> Style.style
309− { width: 24.0, height: 24.0, backgroundColor: p.selectedColor }
468+ { width: 24.0, height: 24.0, backgroundColor: color }
310469 }
311470 []
312− , label p.dimFg p.selectedColor
471+ , label dp.dimFg color
313472 ]
314473 ]
315474 ]
316− let
317− dateSection =
318− [ sectionTitle p.fg "Date Picker"
319− , card p.cardBg
320− [ nativeDatePicker
321− { graphical: false
322− , onChange: E.onString "date" p.setDateText
323− , style: Style.style { height: 24.0, width: 200.0 }
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 }
324497 }
325− , if p.dateText == "" then mempty
326− else label p.dimFg ("Picked: " <> p.dateText)
327498 ]
328499 ]
329− let
330− textSection =
331− [ sectionTitle p.fg "Text Field"
332− , card p.cardBg
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
333575 [ nativeTextField
334576 { placeholder: "Type something..."
335577 , search: true
336− , text: p.searchText
337− , onChangeText: E.onString "text" p.setSearchText
578+ , text: txt
579+ , onChangeText: E.onString "text" setTxt
338580 , style: Style.style { height: 24.0 }
339581 }
340− , if p.searchText == "" then mempty
341− else label p.dimFg ("You typed: " <> p.searchText)
582+ , if txt == "" then mempty
583+ else label dp.dimFg ("You typed: " <> txt)
342584 ]
343585 ]
344− let
345− sections = join
346− [ if show' "Button" then buttonSection else []
347− , if show' "Switch" then switchSection else []
348− , if show' "Slider" then sliderSection else []
349− , if show' "Pop Up" then popUpSection else []
350− , if show' "Color" then colorSection else []
351− , if show' "Date" then dateSection else []
352− , if show' "Text" then textSection else []
353− ]
354− pure do
355− sidebarLayout
356− { sidebar
357− , sidebarWidth: 140.0
358− , content: nativeScrollView { style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
359− (view { style: tw "px-4 pb-4" } sections)
360− }
361586
362−-- Tab 1: Editor
363−editorTab :: { fg :: String } -> JSX
364−editorTab = component "EditorTab" \p -> React.do
587+textEditorDemo :: DemoProps -> JSX
588+textEditorDemo = component "TextEditorDemo" \dp -> React.do
365589 pure do
366590 view { style: tw "flex-1 px-4" }
367− [ sectionTitle p.fg "Rich Text Editor"
591+ [ sectionTitle dp.fg "Rich Text Editor"
368592 , nativeTextEditor
369593 { text: "Welcome to the PureScript-driven native text editor.\n\nThis uses NSTextView with rich text support and a formatting ruler."
370594 , richText: true
@@ -373,48 +597,478 @@ editorTab = component "EditorTab" \p -> React.do
373597 }
374598 ]
375599
376−-- Tab 2: Browser
377−type BrowserProps =
378− { browserUrl :: String
379− , setBrowserUrl :: String -> Effect Unit
380− , urlBarText :: String
381− , setUrlBarText :: String -> Effect Unit
382− , fg :: String
383− , dimFg :: String
384− }
600+-- Display
385601
386−browserTab :: BrowserProps -> JSX
387−browserTab = component "BrowserTab" \p -> React.do
602+imageDemo :: DemoProps -> JSX
603+imageDemo = component "ImageDemo" \dp -> React.do
388604 pure do
389− view { style: tw "flex-1 px-4" }
390− [ sectionTitle p.fg "Web Browser"
391− , nativeTextField
392− { text: p.urlBarText
393− , placeholder: "Enter URL..."
394− , search: false
395− , onChangeText: E.onString "text" p.setUrlBarText
396− , onSubmit: E.onString "text" p.setBrowserUrl
397− , style: Style.style { height: 24.0, marginBottom: 8.0 }
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"
398613 }
399− , nativeWebView
400− { url: p.browserUrl
401− , onFinishLoad: handler
402− ( nativeEvent >>> unsafeEventFn \e ->
403− { url: E.getFieldStr "url" e, title: E.getFieldStr "title" e }
404− )
405− \r -> p.setUrlBarText r.url
406− , style: tw "flex-1" <> Style.style { minHeight: 400.0 }
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"
407831 }
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+ ]
408843 ]
409844
410−-- Tab 3: Rive Animation
411−riveTab :: { fg :: String, bg :: String } -> JSX
412−riveTab = component "RiveTab" \p -> React.do
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
4131067 pure do
4141068 nativeScrollView { style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
4151069 ( view { style: tw "px-4 pb-4" }
416− [ sectionTitle p.fg "Mouse Tracking"
417− , text { style: tw "text-xs mb-2" <> Style.style { color: p.fg } }
1070+ [ sectionTitle dp.fg "Mouse Tracking"
1071+ , text { style: tw "text-xs mb-2" <> Style.style { color: dp.fg } }
4181072 "Move your cursor over the cat — it follows your mouse!"
4191073 , nativeRiveView_
4201074 { resourceName: "cat_following_mouse"
@@ -423,8 +1077,8 @@ riveTab = component "RiveTab" \p -> React.do
4231077 , autoplay: true
4241078 , style: Style.style { height: 300.0 }
4251079 }
426− , sectionTitle p.fg "Cursor Tracking"
427− , text { style: tw "text-xs mb-2" <> Style.style { color: p.fg } }
1080+ , sectionTitle dp.fg "Cursor Tracking"
1081+ , text { style: tw "text-xs mb-2" <> Style.style { color: dp.fg } }
4281082 "Shapes follow your cursor via Pointer Move listeners"
4291083 , nativeRiveView_
4301084 { resourceName: "cursor_tracking"
@@ -433,8 +1087,8 @@ riveTab = component "RiveTab" \p -> React.do
4331087 , autoplay: true
4341088 , style: Style.style { height: 300.0 }
4351089 }
436− , sectionTitle p.fg "Interactive Rive Animations"
437− , text { style: tw "text-xs mb-2" <> Style.style { color: p.fg } }
1090+ , sectionTitle dp.fg "Interactive Rive Animations"
1091+ , text { style: tw "text-xs mb-2" <> Style.style { color: dp.fg } }
4381092 "Click the stars to rate! State machine handles interaction."
4391093 , nativeRiveView_
4401094 { resourceName: "rating_animation"
@@ -443,8 +1097,8 @@ riveTab = component "RiveTab" \p -> React.do
4431097 , autoplay: true
4441098 , style: Style.style { height: 200.0 }
4451099 }
446− , sectionTitle p.fg "Light Switch"
447− , text { style: tw "text-xs mb-2" <> Style.style { color: p.fg } }
1100+ , sectionTitle dp.fg "Light Switch"
1101+ , text { style: tw "text-xs mb-2" <> Style.style { color: dp.fg } }
4481102 "Click to toggle"
4491103 , nativeRiveView_
4501104 { resourceName: "switch_event_example"
@@ -453,810 +1107,432 @@ riveTab = component "RiveTab" \p -> React.do
4531107 , autoplay: true
4541108 , style: Style.style { height: 200.0 }
4551109 }
456− , sectionTitle p.fg "Windy Tree"
1110+ , sectionTitle dp.fg "Windy Tree"
4571111 , nativeRiveView_
4581112 { resourceName: "windy_tree"
4591113 , fit: T.cover
4601114 , autoplay: true
461− , style: tw "flex-1" <> Style.style { minHeight: 300.0, backgroundColor: p.bg }
1115+ , style: tw "flex-1" <> Style.style { minHeight: 300.0, backgroundColor: dp.bg }
4621116 }
4631117 ]
4641118 )
4651119
466−-- Tab 4: System (Context Menu, Drop Zone, File Picker)
467−type SystemProps =
468− { fg :: String
469− , dimFg :: String
470− , cardBg :: String
471− , bg :: String
472− }
1120+-- System Services
4731121
474−systemTab :: SystemProps -> JSX
475−systemTab = component "SystemTab" \p -> React.do
476− menuResult /\ setMenuResult <- useState' ""
477− dropStatus /\ setDropStatus <- useState' "Drop files here"
478− droppedFiles /\ setDroppedFiles <- useState' ""
479− isDragging /\ setIsDragging <- useState' false
480− pickedFiles /\ setPickedFiles <- useState' ""
481− videoPlaying /\ setVideoPlaying <- useState' true
482− comboText /\ setComboText <- useState' ""
483− comboResult /\ setComboResult <- useState' ""
484− stepperValue /\ setStepperValue <- useState' 5.0
485− popoverVisible /\ setPopoverVisible <- useState' false
486− checkboxA /\ setCheckboxA <- useState' true
487− checkboxB /\ setCheckboxB <- useState' false
488− radioChoice /\ setRadioChoice <- useState' "option1"
489− searchQuery /\ setSearchQuery <- useState' ""
490− searchResult /\ setSearchResult <- useState' ""
491− tagTokens /\ setTagTokens <- useState' ([ "PureScript", "React", "macOS" ] :: Array String)
492− sheetVisible /\ setSheetVisible <- useState' false
493− menuResult2 /\ setMenuResult2 <- useState' ""
494− selectedRow /\ setSelectedRow <- useState' ""
495− outlineSelection /\ setOutlineSelection <- useState' ""
496− statusBarActive /\ setStatusBarActive <- useState' false
497− let
498− accentBorder = if isDragging then "#007AFF" else p.dimFg
1122+clipboardDemo :: DemoProps -> JSX
1123+clipboardDemo = component "ClipboardDemo" \dp -> React.do
4991124 pure do
500− nativeScrollView { style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
501− ( view { style: tw "px-4 pb-4" }
502− [ sectionTitle p.fg "Context Menu"
503− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
504− "Right-click the area below to open a context menu"
505− , nativeContextMenu
506− { items:
507− [ { id: "cut", title: "Cut", sfSymbol: "scissors" }
508− , { id: "copy", title: "Copy", sfSymbol: "doc.on.doc" }
509− , { id: "paste", title: "Paste", sfSymbol: "doc.on.clipboard" }
510− , { id: "sep", title: "-", sfSymbol: "" }
511− , { id: "delete", title: "Delete", sfSymbol: "trash" }
512− , { id: "selectAll", title: "Select All", sfSymbol: "selection.pin.in.out" }
513− ]
514− , onSelectItem: E.onString "itemId" setMenuResult
515− , style: Style.style {}
516− }
517− ( card p.cardBg
518− [ view { style: tw "items-center justify-center" <> Style.style { minHeight: 80.0 } }
519− [ text { style: tw "text-sm" <> Style.style { color: p.fg } }
520− "Right-click me!"
521− , if menuResult == "" then mempty
522− else label p.dimFg ("Selected: " <> menuResult)
523− ]
524− ]
525− )
526− , sectionTitle p.fg "Drop Zone"
527− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
528− "Drag files from Finder into the area below"
529− , view
530− { draggedTypes: [ "NSFilenamesPboardType" ]
531− , onDrop: handler
532− (nativeEvent >>> unsafeEventFn \e -> getFieldJSON "dataTransfer" e)
533− \r -> do
534− setDroppedFiles r
535− setDropStatus "Drop files here"
536− setIsDragging false
537− , onDragEnter: handler_ do
538− setDropStatus "Release to drop!"
539− setIsDragging true
540− , onDragLeave: handler_ do
541− setDropStatus "Drop files here"
542− setIsDragging false
543− , style: tw "rounded-lg items-center justify-center mb-2"
544− <> Style.style
545− { minHeight: 100.0
546− , borderWidth: 2.0
547− , borderColor: accentBorder
548− , backgroundColor: p.cardBg
549− }
550− }
551− [ text { style: tw "text-sm" <> Style.style { color: p.fg } } dropStatus
552− , if droppedFiles == "" then mempty
553− else text { style: tw "text-xs mt-2 px-4" <> Style.style { color: p.dimFg } }
554− ("Dropped: " <> droppedFiles)
555− ]
556− , sectionTitle p.fg "File Picker"
557− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
558− "Click buttons to open native file panels"
559− , card p.cardBg
560− [ view { style: tw "flex-row items-center" }
561− [ nativeFilePicker
562− { mode: T.openFile
563− , title: "Open Files"
564− , sfSymbol: "doc.badge.plus"
565− , allowMultiple: true
566− , allowedTypes: [ "public.image", "public.text" ]
567− , message: "Select files to open"
568− , onPickFiles: handler
569− (nativeEvent >>> unsafeEventFn \e -> getFieldJSON "files" e)
570− setPickedFiles
571− , onCancel: handler_ (setPickedFiles "Cancelled")
572− , style: Style.style { height: 24.0, width: 120.0 }
573− }
574− , nativeFilePicker
575− { mode: T.openFile
576− , title: "Choose Folder"
577− , sfSymbol: "folder"
578− , canChooseDirectories: true
579− , message: "Select a folder"
580− , onPickFiles: handler
581− (nativeEvent >>> unsafeEventFn \e -> getFieldJSON "files" e)
582− setPickedFiles
583− , onCancel: handler_ (setPickedFiles "Cancelled")
584− , style: Style.style { height: 24.0, width: 140.0, marginLeft: 8.0 }
585− }
586− , nativeFilePicker
587− { mode: T.saveFile
588− , title: "Save As..."
589− , sfSymbol: "square.and.arrow.down"
590− , defaultName: "Untitled.txt"
591− , allowedTypes: [ "public.plain-text" ]
592− , message: "Choose save location"
593− , onPickFiles: handler
594− (nativeEvent >>> unsafeEventFn \e -> getFieldJSON "files" e)
595− setPickedFiles
596− , onCancel: handler_ (setPickedFiles "Cancelled")
597− , style: Style.style { height: 24.0, width: 120.0, marginLeft: 8.0 }
598− }
599− ]
600− , if pickedFiles == "" then mempty
601− else label p.dimFg pickedFiles
602− ]
603− , sectionTitle p.fg "Video Player"
604− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
605− "Native AVPlayerView with floating controls"
606− , nativeVideoPlayer
607− { source: "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/720/Big_Buck_Bunny_720_10s_1MB.mp4"
608− , playing: videoPlaying
609− , looping: true
610− , muted: false
611− , style: Style.style { height: 240.0 } <> tw "rounded-lg overflow-hidden mb-2"
612− }
613− , view { style: tw "flex-row mb-2" }
614− [ nativeButton
615− { title: if videoPlaying then "Pause" else "Play"
616− , sfSymbol: if videoPlaying then "pause.fill" else "play.fill"
617− , bezelStyle: T.push
618− , onPress: handler_ (setVideoPlaying (not videoPlaying))
619− , style: Style.style { height: 24.0, width: 100.0 }
620− }
621− ]
622− , sectionTitle p.fg "Animated Image"
623− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
624− "Native NSImageView with animated GIF support"
625− , nativeAnimatedImage
626− { source: "https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif"
627− , animating: true
628− , style: Style.style { height: 200.0 } <> tw "rounded-lg overflow-hidden mb-2"
629− }
630− , nativeAnimatedImage
631− { source: "https://media.giphy.com/media/13CoXDiaCcCoyk/giphy.gif"
632− , animating: true
633− , style: Style.style { height: 200.0 } <> tw "rounded-lg overflow-hidden mb-2"
634− }
635−
636− -- New components
637− , sectionTitle p.fg "Split View"
638− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
639− "Native NSSplitView with resizable divider"
640− , nativeSplitView
641− { isVertical: true
642− , style: Style.style { height: 150.0 } <> tw "rounded-lg overflow-hidden mb-2"
643− }
644− [ view { style: tw "flex-1 p-4" <> Style.style { backgroundColor: p.cardBg } }
645− [ text { style: tw "text-sm font-semibold" <> Style.style { color: p.fg } } "Left Pane"
646− , text { style: tw "text-xs mt-1" <> Style.style { color: p.dimFg } } "Drag the divider to resize"
647− ]
648− , view { style: tw "flex-1 p-4" <> Style.style { backgroundColor: p.cardBg } }
649− [ text { style: tw "text-sm font-semibold" <> Style.style { color: p.fg } } "Right Pane"
650− , text { style: tw "text-xs mt-1" <> Style.style { color: p.dimFg } } "Content fills available space"
651− ]
652− ]
653−
654− , sectionTitle p.fg "Tab View"
655− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
656− "Native segmented tab bar"
657− , nativeTabView
658− { items:
659− [ { id: "general", label: "General" }
660− , { id: "advanced", label: "Advanced" }
661− , { id: "about", label: "About" }
662− ]
663− , selectedItem: "general"
664− , onSelectTab: E.onString "tabId" \_ -> pure unit
665− , style: Style.style { height: 32.0 } <> tw "mb-2"
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 }
6661134 }
1135+ ]
1136+ ]
6671137
668− , sectionTitle p.fg "Combo Box"
669− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
670− "Editable dropdown with autocomplete"
671− , card p.cardBg
672− [ nativeComboBox
673− { items: [ "Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig", "Grape" ]
674− , text: comboText
675− , placeholder: "Type a fruit..."
676− , onChangeText: E.onString "text" setComboText
677− , onSelectItem: E.onString "text" \t -> do
678− setComboText t
679− setComboResult ("Selected: " <> t)
680− , style: Style.style { height: 28.0 } <> tw "mb-2"
681− }
682− , if comboResult == "" then mempty
683− else label p.dimFg comboResult
684− ]
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+ ]
6851152
686− , sectionTitle p.fg "Stepper"
687− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
688− "Up/down increment control with label"
689− , card p.cardBg
690− [ view { style: tw "flex-row items-center" }
691− [ text { style: tw "text-sm mr-3" <> Style.style { color: p.fg } } "Quantity:"
692− , nativeStepper
693− { value: stepperValue
694− , minValue: 0.0
695− , maxValue: 50.0
696− , increment: 1.0
697− , onChange: E.onNumber "value" setStepperValue
698− , style: Style.style { width: 100.0, height: 24.0 }
699− }
700− ]
701− ]
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+ ]
7021167
703− , sectionTitle p.fg "Box"
704− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
705− "Native NSBox with title and border"
706− , nativeBox
707− { boxTitle: "Settings"
708− , fillColor2: p.cardBg
709− , borderColor2: p.dimFg
710− , radius: 8.0
711− , style: Style.style { height: 100.0 } <> tw "mb-2"
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 }
7121180 }
713− [ view { style: tw "flex-row items-center p-2" }
714− [ text { style: tw "text-sm" <> Style.style { color: p.fg } } "Content inside a native box" ]
715− ]
716−
717− , sectionTitle p.fg "Popover"
718− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
719− "Click button to toggle a popover"
720− , nativePopover
721− { visible: popoverVisible
722− , preferredEdge: T.bottom
723− , behavior: T.transient
724− , onClose: handler_ (setPopoverVisible false)
725− , style: tw "mb-2"
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 }
7261186 }
727− [ nativeButton
728− { title: if popoverVisible then "Hide Popover" else "Show Popover"
729− , bezelStyle: T.push
730− , onPress: handler_ (setPopoverVisible (not popoverVisible))
731− , style: Style.style { height: 24.0, width: 140.0 }
732− }
733− , view { style: tw "p-4" <> Style.style { width: 200.0, height: 80.0 } }
734− [ text { style: tw "text-sm font-semibold" <> Style.style { color: p.fg } } "Popover Content"
735− , text { style: tw "text-xs mt-1" <> Style.style { color: p.dimFg } } "Click outside to dismiss"
736− ]
737− ]
738−
739− , sectionTitle p.fg "Image"
740− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
741− "Native NSImageView with URL loading and corner radius"
742− , nativeImage
743− { source: "https://placedog.net/640/400"
744− , contentMode: T.scaleProportionally
745− , cornerRadius: 12.0
746− , style: Style.style { height: 200.0 } <> tw "mb-2"
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 }
7471192 }
1193+ ]
1194+ ]
7481195
749− , sectionTitle p.fg "Checkbox"
750− , card p.cardBg
751− [ nativeCheckbox
752− { checked: checkboxA
753− , title: "Enable notifications"
754− , enabled: true
755− , onChange: E.onBool "checked" setCheckboxA
756− , style: Style.style { height: 24.0 } <> tw "mb-1"
757− }
758− , nativeCheckbox
759− { checked: checkboxB
760− , title: "Dark mode"
761− , enabled: true
762− , onChange: E.onBool "checked" setCheckboxB
763− , style: Style.style { height: 24.0 }
764− }
765− ]
766−
767− , sectionTitle p.fg "Radio Button"
768− , card p.cardBg
769− [ nativeRadioButton
770− { selected: radioChoice == "option1"
771− , title: "Small"
772− , enabled: true
773− , onChange: handler_ (setRadioChoice "option1")
774− , style: Style.style { height: 24.0 } <> tw "mb-1"
775− }
776− , nativeRadioButton
777− { selected: radioChoice == "option2"
778− , title: "Medium"
779− , enabled: true
780− , onChange: handler_ (setRadioChoice "option2")
781− , style: Style.style { height: 24.0 } <> tw "mb-1"
782− }
783− , nativeRadioButton
784− { selected: radioChoice == "option3"
785− , title: "Large"
786− , enabled: true
787− , onChange: handler_ (setRadioChoice "option3")
788− , style: Style.style { height: 24.0 }
789− }
790− , label p.dimFg ("Selected: " <> radioChoice)
791− ]
792−
793− , sectionTitle p.fg "Search Field"
794− , nativeSearchField
795− { text: searchQuery
796− , placeholder: "Search..."
797− , onChangeText: E.onString "text" setSearchQuery
798− , onSearch: E.onString "text" \t -> setSearchResult ("Searched: " <> t)
799− , style: Style.style { height: 28.0 } <> tw "mb-2"
800− }
801− , if searchResult == "" then mempty
802− else label p.dimFg searchResult
803−
804− , sectionTitle p.fg "Token Field"
805− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
806− "Type text and press Enter to add tokens"
807− , nativeTokenField
808− { tokens: tagTokens
809− , placeholder: "Add tags..."
810− , onChangeTokens: E.onStrings "tokens" setTagTokens
811− , style: Style.style { height: 28.0 } <> tw "mb-2"
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 }
8121225 }
1226+ ]
1227+ ]
8131228
814− , sectionTitle p.fg "Separator"
815− , nativeSeparator
816− { vertical: false
817− , style: Style.style { height: 2.0 } <> tw "my-2"
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 }
8181242 }
1243+ ]
1244+ ]
8191245
820− , sectionTitle p.fg "Help Button"
821− , view { style: tw "flex-row items-center mb-2" }
822− [ nativeHelpButton
823− { onPress: handler_ (macosAlert T.informational "Help" "This is the help button. It opens contextual help." [ "OK" ])
824− , style: Style.style { width: 24.0, height: 24.0 }
825− }
826− , text { style: tw "text-xs ml-2" <> Style.style { color: p.dimFg } } "Click for help"
827− ]
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+ ]
8281262
829− , sectionTitle p.fg "Path Control"
830− , nativePathControl
831− { url: "/Users"
832− , pathStyle: T.standardPath
833− , onSelectPath: E.onString "url" \_ -> pure unit
834− , style: Style.style { height: 24.0 } <> tw "mb-2"
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 }
8351276 }
1277+ ]
1278+ ]
8361279
837− , sectionTitle p.fg "Sheet"
838− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
839− "Modal sheet attached to the window"
840− , nativeButton
841− { title: "Show Sheet"
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"
8421297 , bezelStyle: T.push
843− , onPress: handler_ (setSheetVisible true)
844− , style: Style.style { height: 24.0, width: 120.0 } <> tw "mb-2"
1298+ , onPress: handler_ (say txt)
1299+ , style: Style.style { height: 24.0, width: 100.0 }
8451300 }
846− , nativeSheet
847− { visible: sheetVisible
848− , onDismiss: handler_ (setSheetVisible false)
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 }
8491307 }
850− [ view { style: tw "p-6" <> Style.style { width: 400.0, height: 200.0 } }
851− [ text { style: tw "text-lg font-semibold mb-2" <> Style.style { color: p.fg } } "Sheet Content"
852− , text { style: tw "text-sm mb-4" <> Style.style { color: p.dimFg } } "This is a native macOS sheet."
853− , nativeButton
854− { title: "Dismiss"
855− , bezelStyle: T.push
856− , onPress: handler_ (setSheetVisible false)
857− , style: Style.style { height: 24.0, width: 100.0 }
858− }
859− ]
860− ]
1308+ ]
1309+ ]
8611310
862− , sectionTitle p.fg "Menu"
863− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
864− "Imperative popup menu at mouse position"
865− , view { style: tw "flex-row items-center mb-2" }
866− [ nativeButton
867− { title: "Show Menu"
868− , bezelStyle: T.push
869− , onPress: handler_
870− ( macosShowMenu
871− [ { title: "New File", id: "new" }
872− , { title: "Open...", id: "open" }
873− , { title: "-", id: "sep" }
874− , { title: "Save", id: "save" }
875− , { title: "Save As...", id: "saveAs" }
876− ]
877− \itemId -> setMenuResult2 ("Menu: " <> itemId)
878− )
879− , style: Style.style { height: 24.0, width: 120.0 }
880− }
881− , if menuResult2 == "" then mempty
882− else text { style: tw "text-xs ml-3" <> Style.style { color: p.dimFg } } menuResult2
883− ]
1311+-- AI & ML
8841312
885− , sectionTitle p.fg "Table View"
886− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
887− "Native NSTableView with columns and rows"
888− , nativeTableView
889− { columns:
890− [ { id: "name", title: "Name", width: 150.0 }
891− , { id: "type", title: "Type", width: 100.0 }
892− , { id: "size", title: "Size", width: 80.0 }
893− ]
894− , rows:
895− [ [ "Main.purs", "PureScript", "12 KB" ]
896− , [ "App.tsx", "TypeScript", "8 KB" ]
897− , [ "style.css", "CSS", "3 KB" ]
898− , [ "index.html", "HTML", "1 KB" ]
899− , [ "package.json", "JSON", "2 KB" ]
900− ]
901− , headerVisible: true
902− , alternatingRows: true
903− , onSelectRow: E.onInt "rowIndex" \i -> setSelectedRow ("Row " <> show i)
904− , onDoubleClickRow: E.onInt "rowIndex" \i -> setSelectedRow ("Double-clicked row " <> show i)
905− , style: Style.style { height: 180.0 } <> tw "rounded-lg overflow-hidden mb-2"
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 }
9061334 }
907− , if selectedRow == "" then mempty
908− else label p.dimFg selectedRow
909−
910− , sectionTitle p.fg "Clipboard"
911− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
912− "Read and write the system clipboard"
913− , view { style: tw "flex-row items-center mb-2" }
914− [ nativeButton
915− { title: "Copy 'Hello'"
916− , bezelStyle: T.push
917− , onPress: handler_ (copyToClipboard "Hello from PureScript!")
918− , style: Style.style { height: 24.0, width: 120.0 }
919− }
920− ]
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+ ]
9211340
922− , sectionTitle p.fg "Alert"
923− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
924− "Native alert dialog"
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+ }
9251356 , nativeButton
926− { title: "Show Alert"
1357+ { title: "Stop"
1358+ , sfSymbol: "stop.circle"
9271359 , bezelStyle: T.push
928− , onPress: handler_ (macosAlert T.warning "Are you sure?" "This action cannot be undone." [ "Cancel", "OK" ])
929− , style: Style.style { height: 24.0, width: 120.0 } <> tw "mb-4"
1360+ , onPress: handler_ speech.stop
1361+ , style: Style.style { height: 24.0, width: 80.0, marginLeft: 8.0 }
9301362 }
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+ ]
9311368
932− , sectionTitle p.fg "Outline View"
933− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
934− "Hierarchical tree list (NSOutlineView)"
935− , nativeOutlineView
936− { items:
937− let
938− file id title = OutlineItem { id, title, sfSymbol: "doc", children: [] }
939− folder id title children = OutlineItem { id, title, sfSymbol: "folder", children }
940− in
941− [ folder "src" "src"
942− [ file "main" "Main.purs"
943− , folder "macos" "MacOS"
944− [ file "btn" "Button.purs"
945− , file "sl" "Slider.purs"
946− , file "sw" "Switch.purs"
947− ]
948− ]
949− , folder "test" "test"
950− [ file "t1" "MacOSComponents.test.js"
951− , file "t2" "MacOSSnapshots.test.js"
952− ]
953− , OutlineItem { id: "pkg", title: "package.json", sfSymbol: "doc.text", children: [] }
954− ]
955− , headerVisible: false
956− , onSelectItem: E.onString "id" setOutlineSelection
957− , style: Style.style { height: 200.0 } <> tw "rounded-lg overflow-hidden mb-2"
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 }
9581392 }
959− , if outlineSelection == "" then mempty
960− else label p.dimFg ("Selected: " <> outlineSelection)
961−
962− , sectionTitle p.fg "Share"
963− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
964− "System share picker"
9651393 , nativeButton
966− { title: "Share Text"
967− , sfSymbol: "square.and.arrow.up"
1394+ { title: "Sentiment"
1395+ , sfSymbol: "face.smiling"
9681396 , bezelStyle: T.push
969− , onPress: handler_ (macosShare [ "Hello from PureScript React Native!" ])
970− , style: Style.style { height: 24.0, width: 120.0 } <> tw "mb-2"
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 }
9711401 }
972−
973− , sectionTitle p.fg "Notifications"
974− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
975− "System notifications (requires permission)"
9761402 , nativeButton
977− { title: "Send Notification"
978− , sfSymbol: "bell"
1403+ { title: "Tokenize"
1404+ , sfSymbol: "text.word.spacing"
9791405 , bezelStyle: T.push
980− , onPress: handler_ (macosNotify "PureScript" "Hello from React Native macOS!")
981− , style: Style.style { height: 24.0, width: 160.0 } <> tw "mb-2"
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 }
9821410 }
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+ ]
9831416
984− , sectionTitle p.fg "Sound"
985− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
986− "Play system sounds"
987− , view { style: tw "flex-row items-center mb-2" }
988− [ nativeButton
989− { title: "Glass"
990− , bezelStyle: T.push
991− , onPress: handler_ (macosPlaySound "Glass")
992− , style: Style.style { height: 24.0, width: 80.0 }
993− }
994− , nativeButton
995− { title: "Ping"
996− , bezelStyle: T.push
997− , onPress: handler_ (macosPlaySound "Ping")
998− , style: Style.style { height: 24.0, width: 80.0, marginLeft: 8.0 }
999− }
1000− , nativeButton
1001− { title: "Beep"
1002− , bezelStyle: T.push
1003− , onPress: handler_ macosBeep
1004− , style: Style.style { height: 24.0, width: 80.0, marginLeft: 8.0 }
1005− }
1006− ]
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+ ]
10071437
1008− , sectionTitle p.fg "Status Bar"
1009− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
1010− "Menu bar icon with dropdown"
1011− , view { style: tw "flex-row items-center mb-4" }
1012− [ nativeButton
1013− { title: if statusBarActive then "Remove" else "Add to Menu Bar"
1014− , sfSymbol: if statusBarActive then "minus.circle" else "plus.circle"
1015− , bezelStyle: T.push
1016− , onPress: handler_
1017− ( if statusBarActive then do
1018− macosRemoveStatusBarItem
1019− setStatusBarActive false
1020− else do
1021− macosSetStatusBarItem
1022− { title: ""
1023− , sfSymbol: "swift"
1024− , menuItems:
1025− [ { id: "about", title: "About PureScript RN" }
1026− , { id: "sep", title: "-" }
1027− , { id: "quit", title: "Quit" }
1028− ]
1029− }
1030− setStatusBarActive true
1031− )
1032− , style: Style.style { height: 24.0, width: 180.0 }
1033− }
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: "" }
10341456 ]
1035− ]
1036− )
1457+ , style: Style.style { height: 250.0 }
1458+ }
1459+ ]
10371460
1038−-- Tab 6: AI & Tools
1039−type AIProps =
1040− { fg :: String
1041− , dimFg :: String
1042− , cardBg :: String
1043− , bg :: String
1044− }
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+ ]
10451474
1046−aiTab :: AIProps -> JSX
1047−aiTab = component "AITab" \p -> React.do
1048− speechText /\ setSpeechText <- useState' "Hello from PureScript React Native on macOS!"
1049− ocrResult /\ setOcrResult <- useState' ""
1050− speech <- useSpeechRecognition
1051− nlText /\ setNlText <- useState' "I love this amazing app! C'est magnifique."
1052− nlResult /\ setNlResult <- useState' ""
1053− cameraOn /\ setCameraOn <- useState' false
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"
10541481 pure do
1055− nativeScrollView { style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
1056− ( view { style: tw "px-4 pb-4" }
1057− [ sectionTitle p.fg "Map"
1058− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
1059− "Embedded MKMapView (San Francisco)"
1060− , nativeMapView
1061− { latitude: 37.7749
1062− , longitude: -122.4194
1063− , latitudeDelta: 0.05
1064− , longitudeDelta: 0.05
1065− , mapType: T.standardMap
1066− , showsUserLocation: false
1067− , annotations:
1068− [ { latitude: 37.7749, longitude: -122.4194, title: "San Francisco", subtitle: "California" }
1069− , { latitude: 37.8199, longitude: -122.4783, title: "Golden Gate Bridge", subtitle: "" }
1070− ]
1071− , style: Style.style { height: 250.0 }
1072− }
1073− , sectionTitle p.fg "PDF Viewer"
1074− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
1075− "PDFKit document viewer"
1076− , nativePDFView
1077− { source: "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
1078− , autoScales: true
1079− , displayMode: T.singlePageContinuous
1080− , style: Style.style { height: 300.0 }
1081− }
1082− , sectionTitle p.fg "Quick Look"
1083− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
1084− "Preview files with QLPreviewPanel"
1085− , view { style: tw "flex-row items-center mb-4" }
1086− [ nativeButton
1087− { title: "Preview /etc/hosts"
1088− , sfSymbol: "eye"
1089− , bezelStyle: T.push
1090− , onPress: handler_ (macosQuickLook "/etc/hosts")
1091− , style: Style.style { height: 24.0, width: 180.0 }
1092− }
1093− ]
1094− , sectionTitle p.fg "Speech"
1095− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
1096− "Text-to-speech (NSSpeechSynthesizer)"
1097− , nativeTextField
1098− { text: speechText
1099− , placeholder: "Text to speak..."
1100− , onChangeText: E.onString "text" setSpeechText
1101− , style: Style.style { height: 24.0 } <> tw "mb-2"
1102− }
1103− , view { style: tw "flex-row items-center mb-4" }
1104− [ nativeButton
1105− { title: "Speak"
1106− , sfSymbol: "speaker.wave.2"
1107− , bezelStyle: T.push
1108− , onPress: handler_ (say speechText)
1109− , style: Style.style { height: 24.0, width: 100.0 }
1110− }
1111− , nativeButton
1112− { title: "Stop"
1113− , sfSymbol: "stop.circle"
1114− , bezelStyle: T.push
1115− , onPress: handler_ stopSpeaking
1116− , style: Style.style { height: 24.0, width: 100.0, marginLeft: 8.0 }
1117− }
1118− ]
1119− , sectionTitle p.fg "Color Picker"
1120− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
1121− "System color panel (NSColorPanel)"
1122− , view { style: tw "flex-row items-center mb-4" }
1123− [ nativeButton
1124− { title: "Show Color Panel"
1125− , sfSymbol: "paintpalette"
1126− , bezelStyle: T.push
1127− , onPress: handler_ macosShowColorPanel
1128− , style: Style.style { height: 24.0, width: 180.0 }
1129− }
1130− ]
1131− , sectionTitle p.fg "Font Panel"
1132− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
1133− "System font panel (NSFontPanel)"
1134− , view { style: tw "flex-row items-center mb-4" }
1135− [ nativeButton
1136− { title: "Show Font Panel"
1137− , sfSymbol: "textformat"
1138− , bezelStyle: T.push
1139− , onPress: handler_ macosShowFontPanel
1140− , style: Style.style { height: 24.0, width: 180.0 }
1141− }
1142− ]
1143− , sectionTitle p.fg "OCR (Vision)"
1144− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
1145− "Recognize text in images (VNRecognizeTextRequest)"
1146− , view { style: tw "flex-row items-center mb-2" }
1147− [ nativeFilePicker
1148− { mode: T.openFile
1149− , allowedTypes: [ "public.image" ]
1150− , title: "Pick Image"
1151− , sfSymbol: "photo"
1152− , onPickFiles: handler
1153− (nativeEvent >>> unsafeEventFn \e -> E.getFieldArray "files" e)
1154− \paths -> for_ paths \path -> do
1155− setOcrResult "Recognizing..."
1156− launchAff_ do
1157− result <- recognizeText path
1158− liftEffect (setOcrResult result)
1159− , style: Style.style { height: 24.0, width: 140.0 }
1160− }
1161− ]
1162− , if ocrResult /= "" then view { style: tw "p-2 rounded mb-4" <> Style.style { backgroundColor: p.cardBg } }
1163− [ text { style: tw "text-xs font-mono" <> Style.style { color: p.fg } } ocrResult ]
1164− else view {} []
1165− , sectionTitle p.fg "Speech Recognition"
1166− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
1167− "Live microphone-to-text (click Start, speak, click Stop)"
1168− , view { style: tw "flex-row items-center mb-2" }
1169− [ nativeButton
1170− { title: if speech.listening then "Listening..." else "Start"
1171− , sfSymbol: if speech.listening then "mic.fill" else "mic"
1172− , bezelStyle: T.push
1173− , onPress: handler_ speech.start
1174− , style: Style.style { height: 24.0, width: 130.0 }
1175− }
1176− , nativeButton
1177− { title: "Stop"
1178− , sfSymbol: "stop.circle"
1179− , bezelStyle: T.push
1180− , onPress: handler_ speech.stop
1181− , style: Style.style { height: 24.0, width: 80.0, marginLeft: 8.0 }
1182− }
1183− ]
1184− , if speech.transcript /= "" then view { style: tw "p-2 rounded mb-4" <> Style.style { backgroundColor: p.cardBg } }
1185− [ text { style: tw "text-xs font-mono" <> Style.style { color: p.fg } } speech.transcript ]
1186− else view {} []
1187− , sectionTitle p.fg "Natural Language"
1188− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
1189− "Language detection, sentiment, tokenization (NaturalLanguage.framework)"
1190− , nativeTextField
1191− { text: nlText
1192− , placeholder: "Enter text to analyze..."
1193− , onChangeText: E.onString "text" setNlText
1194− , style: Style.style { height: 24.0 } <> tw "mb-2"
1195− }
1196− , view { style: tw "flex-row items-center mb-2" }
1197− [ nativeButton
1198− { title: "Language"
1199− , sfSymbol: "globe"
1200− , bezelStyle: T.push
1201− , onPress: handler_ $ launchAff_ do
1202− lang <- detectLanguage nlText
1203− liftEffect (setNlResult ("Language: " <> lang))
1204− , style: Style.style { height: 24.0, width: 110.0 }
1205− }
1206− , nativeButton
1207− { title: "Sentiment"
1208− , sfSymbol: "face.smiling"
1209− , bezelStyle: T.push
1210− , onPress: handler_ $ launchAff_ do
1211− score <- analyzeSentiment nlText
1212− liftEffect (setNlResult ("Sentiment: " <> show score))
1213− , style: Style.style { height: 24.0, width: 110.0, marginLeft: 8.0 }
1214− }
1215− , nativeButton
1216− { title: "Tokenize"
1217− , sfSymbol: "text.word.spacing"
1218− , bezelStyle: T.push
1219− , onPress: handler_ $ launchAff_ do
1220− tokens <- tokenize nlText
1221− liftEffect (setNlResult ("Tokens: " <> joinWith ", " tokens))
1222− , style: Style.style { height: 24.0, width: 110.0, marginLeft: 8.0 }
1223− }
1224− ]
1225− , if nlResult /= "" then view { style: tw "p-2 rounded mb-4" <> Style.style { backgroundColor: p.cardBg } }
1226− [ text { style: tw "text-xs font-mono" <> Style.style { color: p.fg } } nlResult ]
1227− else view {} []
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+ ]
12281502
1229− , sectionTitle p.fg "Camera"
1230− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
1231− "Live camera preview (AVCaptureVideoPreviewLayer)"
1232− , nativeButton
1233− { title: if cameraOn then "Stop Camera" else "Start Camera"
1234− , sfSymbol: if cameraOn then "video.slash" else "video"
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"
12351523 , bezelStyle: T.push
1236− , onPress: handler_ (setCameraOn (not cameraOn))
1237− , style: Style.style { height: 24.0, width: 140.0 } <> tw "mb-2"
1238− }
1239− , if cameraOn then nativeCameraView
1240− { active: true
1241− , style: Style.style { height: 240.0 } <> tw "rounded-lg overflow-hidden mb-2"
1524+ , onPress: handler_ (setPlaying (not playing))
1525+ , style: Style.style { height: 24.0, width: 100.0 }
12421526 }
1243− else view {} []
12441527 ]
1245− )
1528+ ]
12461529
1247−-- Tab 7: Chat (Matrix)
1248−type ChatProps =
1249− { fg :: String
1250− , dimFg :: String
1251− , cardBg :: String
1252− , bg :: String
1253− , isDark :: Boolean
1254− }
1530+-- Chat (Matrix)
12551531
12561532type Message =
12571533 { sender :: String
12581534 , body :: String
1259− , kind :: String -- "text" | "sticker" | "gif" | "video"
1535+ , kind :: String
12601536 , eventId :: String
12611537 }
12621538
@@ -1283,8 +1559,8 @@ avatarColor roomId = fromMaybe "#5856D6" (avatarColors !! (mod (hashCode roomId)
12831559foreign import charCodes :: String -> Array Int
12841560foreign import abs :: Int -> Int
12851561
1286−chatTab :: ChatProps -> JSX
1287−chatTab = component "ChatTab" \p -> React.do
1562+matrixDemo :: DemoProps -> JSX
1563+matrixDemo = component "MatrixDemo" \dp -> React.do
12881564 session /\ setSession <- useState' (Nothing :: Maybe Session)
12891565 rooms /\ setRooms <- useState' ([] :: Array Room)
12901566 activeRoom /\ setActiveRoom <- useState' (Nothing :: Maybe String)
@@ -1299,7 +1575,6 @@ chatTab = component "ChatTab" \p -> React.do
12991575 sessionRef <- useRef (Nothing :: Maybe Session)
13001576 seenIdsRef <- useRef ([] :: Array String)
13011577
1302− -- Sync polling effect
13031578 useEffect session do
13041579 case session of
13051580 Nothing -> pure mempty
@@ -1398,17 +1673,17 @@ chatTab = component "ChatTab" \p -> React.do
13981673 _ <- try (Matrix.sendMessage sess.homeserver sess.accessToken rid txnId msg)
13991674 pure unit
14001675
1401− sentBubbleBg = if p.isDark then "#65B86A" else "#5CB85C"
1402− receivedBubbleBg = if p.isDark then "#3B3B3D" else "#FFFFFF"
1403− chatBg = if p.isDark then "#17212B" else "#E8DFD3"
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"
14041679
14051680 loginForm = view
14061681 { style: tw "flex-1 items-center justify-center"
14071682 <> Style.style { backgroundColor: chatBg }
14081683 }
1409− [ view { style: tw "rounded-xl p-6" <> Style.style { backgroundColor: p.cardBg, width: 340.0 } }
1410− [ text { style: tw "text-lg font-bold mb-4 text-center" <> Style.style { color: p.fg } } "Matrix Login"
1411− , text { style: tw "text-xs mb-1" <> Style.style { color: p.dimFg } } "Homeserver"
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"
14121687 , nativeTextField
14131688 { text: loginServer
14141689 , placeholder: "https://matrix.org"
@@ -1418,7 +1693,7 @@ chatTab = component "ChatTab" \p -> React.do
14181693 , onSubmit: handler_ (pure unit)
14191694 , style: tw "mb-3" <> Style.style { height: 28.0 }
14201695 }
1421− , text { style: tw "text-xs mb-1" <> Style.style { color: p.dimFg } } "Username"
1696+ , text { style: tw "text-xs mb-1" <> Style.style { color: dp.dimFg } } "Username"
14221697 , nativeTextField
14231698 { text: loginUser
14241699 , placeholder: "username"
@@ -1428,7 +1703,7 @@ chatTab = component "ChatTab" \p -> React.do
14281703 , onSubmit: handler_ (pure unit)
14291704 , style: tw "mb-3" <> Style.style { height: 28.0 }
14301705 }
1431− , text { style: tw "text-xs mb-1" <> Style.style { color: p.dimFg } } "Password"
1706+ , text { style: tw "text-xs mb-1" <> Style.style { color: dp.dimFg } } "Password"
14321707 , nativeTextField
14331708 { text: loginPass
14341709 , placeholder: "password"
@@ -1456,7 +1731,7 @@ chatTab = component "ChatTab" \p -> React.do
14561731 view
14571732 { style: tw "flex-row items-center px-3 py-2 mx-2 rounded-lg"
14581733 <> Style.style
1459− { backgroundColor: if activeRoom == Just r.roomId then (if p.isDark then "#2B5278" else "#419FD9") else "transparent" }
1734+ { backgroundColor: if activeRoom == Just r.roomId then (if dp.isDark then "#2B5278" else "#419FD9") else "transparent" }
14601735 }
14611736 [ view
14621737 { style: tw "rounded-full items-center justify-center"
@@ -1466,12 +1741,12 @@ chatTab = component "ChatTab" \p -> React.do
14661741 , view { style: tw "ml-2 flex-1" }
14671742 [ text
14681743 { style: tw "text-sm font-semibold"
1469− <> Style.style { color: if activeRoom == Just r.roomId then "#FFFFFF" else p.fg }
1744+ <> Style.style { color: if activeRoom == Just r.roomId then "#FFFFFF" else dp.fg }
14701745 }
14711746 r.name
14721747 , text
14731748 { style: tw "text-xs"
1474− <> Style.style { color: if activeRoom == Just r.roomId then "#FFFFFFCC" else p.dimFg }
1749+ <> Style.style { color: if activeRoom == Just r.roomId then "#FFFFFFCC" else dp.dimFg }
14751750 }
14761751 r.lastMessage
14771752 ]
@@ -1505,7 +1780,7 @@ chatTab = component "ChatTab" \p -> React.do
15051780 }
15061781 ( view { style: Style.style { alignSelf: align, maxWidth: 320.0 } }
15071782 [ if senderLabel == "" then text { style: Style.style { height: 0.0 } } ""
1508− else text { style: tw "text-xs mb-0.5" <> Style.style { color: p.dimFg } } senderLabel
1783+ else text { style: tw "text-xs mb-0.5" <> Style.style { color: dp.dimFg } } senderLabel
15091784 , if bigEmoji then
15101785 text
15111786 { style: Style.style { fontSize: 64.0, lineHeight: 72.0 } }
@@ -1518,7 +1793,7 @@ chatTab = component "ChatTab" \p -> React.do
15181793 }
15191794 [ text
15201795 { style: tw "text-sm"
1521− <> Style.style { color: if isMine then "#FFFFFF" else (if p.isDark then "#FFFFFF" else "#000000") }
1796+ <> Style.style { color: if isMine then "#FFFFFF" else (if dp.isDark then "#FFFFFF" else "#000000") }
15221797 }
15231798 msg.body
15241799 ]
@@ -1535,13 +1810,13 @@ chatTab = component "ChatTab" \p -> React.do
15351810 , content: view { style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
15361811 [ view
15371812 { style: tw "px-4 py-2 border-b"
1538− <> Style.style { borderBottomWidth: 0.5, borderColor: p.dimFg, backgroundColor: "transparent" }
1813+ <> Style.style { borderBottomWidth: 0.5, borderColor: dp.dimFg, backgroundColor: "transparent" }
15391814 }
1540− [ text { style: tw "text-base font-semibold" <> Style.style { color: p.fg } } activeRoomName ]
1815+ [ text { style: tw "text-base font-semibold" <> Style.style { color: dp.fg } } activeRoomName ]
15411816 , nativePatternBackground
1542− { patternColor: if p.isDark then "#FFFFFF" else "#000000"
1817+ { patternColor: if dp.isDark then "#FFFFFF" else "#000000"
15431818 , background: chatBg
1544− , patternOpacity: if p.isDark then 0.04 else 0.06
1819+ , patternOpacity: if dp.isDark then 0.04 else 0.06
15451820 , patternScale: 1.0
15461821 , style: tw "flex-1"
15471822 }
@@ -1552,11 +1827,11 @@ chatTab = component "ChatTab" \p -> React.do
15521827 )
15531828 , view
15541829 { style: tw "flex-row items-center px-3 py-2"
1555− <> Style.style { borderTopWidth: 0.5, borderColor: p.dimFg, backgroundColor: "transparent" }
1830+ <> Style.style { borderTopWidth: 0.5, borderColor: dp.dimFg, backgroundColor: "transparent" }
15561831 }
15571832 ( case activeRoom of
15581833 Nothing ->
1559− [ text { style: tw "text-sm" <> Style.style { color: p.dimFg } } "Select a room to start chatting" ]
1834+ [ text { style: tw "text-sm" <> Style.style { color: dp.dimFg } } "Select a room to start chatting" ]
15601835 Just rid ->
15611836 [ nativeTextField
15621837 { text: inputText
@@ -1596,10 +1871,20 @@ foreign import setInterval :: Int -> Effect Unit -> Effect Int
15961871foreign import clearInterval :: Int -> Effect Unit
15971872
15981873-- 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+
15991880sectionTitle :: String -> String -> JSX
16001881sectionTitle color title =
16011882 text { style: tw "text-sm font-semibold mt-4 mb-1" <> Style.style { color } } title
16021883
1884+desc :: DemoProps -> String -> JSX
1885+desc dp str =
1886+ text { style: tw "text-xs mb-2" <> Style.style { color: dp.dimFg } } str
1887+
16031888card :: String -> Array JSX -> JSX
16041889card bg children =
16051890 view { style: tw "rounded-lg p-3 mb-2" <> Style.style { backgroundColor: bg, overflow: "hidden" } }