git.iko.soy/rowtype-yoga/purescript-yoga-react-native
on main help sign in
160 files changed+6110−2509
since splitsnapshots
unifiedsplit
.github/workflows/ci.yml+57 .gitignore+1 README.md+80−5 bun.lock+22−6 docs/index.html+33 docs/index.js+2 docs/spago.yaml+10 docs/src/Docs/App.purs+43 docs/src/Docs/Components/CodeBlock.purs+12 docs/src/Docs/Components/Layout.purs+54 docs/src/Docs/Components/PropsTable.purs+33 docs/src/Docs/Components/Sidebar.purs+47 docs/src/Docs/Main.purs+10 docs/src/Docs/Pages/AiMl.purs+77 docs/src/Docs/Pages/DataViews.purs+73 docs/src/Docs/Pages/Display.purs+121 docs/src/Docs/Pages/FilesAndDragDrop.purs+65 docs/src/Docs/Pages/InputControls.purs+278 docs/src/Docs/Pages/LayoutPage.purs+166 docs/src/Docs/Pages/Overlays.purs+123 docs/src/Docs/Pages/Overview.purs+58 docs/src/Docs/Pages/QuickStart.purs+72 docs/src/Docs/Pages/RichMedia.purs+129 docs/src/Docs/Pages/SystemServices.purs+141 docs/src/Docs/Pages/TextEditing.purs+50 docs/src/Docs/Pages/Types.purs+19 docs/vite.config.js+8 example-macos/macos/YogaReactExample-macOS/Info.plist+2 example-macos/macos/YogaReactExample-macOS/RCTMacOSControlsManager.m+154−24 example-macos/macos/YogaReactExample-macOS/YogaReactExample.entitlements+2 example-macos/src/Demo/AiMl.purs+147 example-macos/src/Demo/Animation.purs+66 example-macos/src/Demo/Chat.js+141 example-macos/src/Demo/Chat.purs+375 example-macos/src/Demo/DataViews.purs+76 example-macos/src/Demo/Display.purs+147 example-macos/src/Demo/DragDropFiles.purs+106 example-macos/src/Demo/InputControls.purs+319 example-macos/src/Demo/Layout.purs+70 example-macos/src/Demo/MapsDocs.purs+62 example-macos/src/Demo/Navigation.purs+209 example-macos/src/Demo/Overlays.purs+151 example-macos/src/Demo/Shared.js+6 example-macos/src/Demo/Shared.purs+53 example-macos/src/Demo/SpringAnimation.purs+123 example-macos/src/Demo/SystemServices.purs+207 example-macos/src/Demo/TextEditing.purs+44 example-macos/src/Demo/WebBrowser.purs+35 example-macos/src/Main.js−27 example-macos/src/Main.purs+17−1603 package.json+2 spago.lock+617−42 spago.yaml+41−13 src/Yoga/React/Native/Animated.js+24−1 src/Yoga/React/Native/Animated.purs+9 src/Yoga/React/Native/MacOS/Alert.js+6−1 src/Yoga/React/Native/MacOS/Alert.purs+18−4 src/Yoga/React/Native/MacOS/AnimatedImage.js+2−16 src/Yoga/React/Native/MacOS/AnimatedImage.purs+2−2 src/Yoga/React/Native/MacOS/Box.js+2−6 src/Yoga/React/Native/MacOS/Box.purs+5−5 src/Yoga/React/Native/MacOS/Button.js+10−14 src/Yoga/React/Native/MacOS/Button.purs+5−4 src/Yoga/React/Native/MacOS/CameraView.js+2 src/Yoga/React/Native/MacOS/CameraView.purs+17 src/Yoga/React/Native/MacOS/Checkbox.js+10−6 src/Yoga/React/Native/MacOS/Checkbox.purs+5−4 src/Yoga/React/Native/MacOS/ColorWell.js+10−6 src/Yoga/React/Native/MacOS/ColorWell.purs+5−4 src/Yoga/React/Native/MacOS/ComboBox.js+13−6 src/Yoga/React/Native/MacOS/ComboBox.purs+6−5 src/Yoga/React/Native/MacOS/ContextMenu.js+11−16 src/Yoga/React/Native/MacOS/ContextMenu.purs+5−4 src/Yoga/React/Native/MacOS/DatePicker.js+10−6 src/Yoga/React/Native/MacOS/DatePicker.purs+5−4 src/Yoga/React/Native/MacOS/DropZone.js+22−16 src/Yoga/React/Native/MacOS/DropZone.purs+13−7 src/Yoga/React/Native/MacOS/Events.js+6 src/Yoga/React/Native/MacOS/Events.purs+39 src/Yoga/React/Native/MacOS/FilePicker.js+14−16 src/Yoga/React/Native/MacOS/FilePicker.purs+12−6 src/Yoga/React/Native/MacOS/HelpButton.js+10−6 src/Yoga/React/Native/MacOS/HelpButton.purs+5−4 src/Yoga/React/Native/MacOS/Image.js+2−6 src/Yoga/React/Native/MacOS/Image.purs+2−2 src/Yoga/React/Native/MacOS/LevelIndicator.js+2−6 src/Yoga/React/Native/MacOS/LevelIndicator.purs+2−2 src/Yoga/React/Native/MacOS/MapView.js+22−6 src/Yoga/React/Native/MacOS/MapView.purs+29−9 src/Yoga/React/Native/MacOS/Menu.js+1−1 src/Yoga/React/Native/MacOS/Menu.purs+10−4 src/Yoga/React/Native/MacOS/NaturalLanguage.js+18−18 src/Yoga/React/Native/MacOS/NaturalLanguage.purs+15−15 src/Yoga/React/Native/MacOS/OCR.js+10−6 src/Yoga/React/Native/MacOS/OCR.purs+6−8 src/Yoga/React/Native/MacOS/OutlineView.js+10−6 src/Yoga/React/Native/MacOS/OutlineView.purs+14−6 src/Yoga/React/Native/MacOS/PDFView.js+11−6 src/Yoga/React/Native/MacOS/PDFView.purs+11−6 src/Yoga/React/Native/MacOS/PathControl.js+10−6 src/Yoga/React/Native/MacOS/PathControl.purs+5−4 src/Yoga/React/Native/MacOS/PatternBackground.js+2−16 src/Yoga/React/Native/MacOS/PatternBackground.purs+3−3 src/Yoga/React/Native/MacOS/PopUp.js+11−6 src/Yoga/React/Native/MacOS/PopUp.purs+5−4 src/Yoga/React/Native/MacOS/Popover.js+10−6 src/Yoga/React/Native/MacOS/Popover.purs+5−4 src/Yoga/React/Native/MacOS/Progress.js+2−6 src/Yoga/React/Native/MacOS/Progress.purs+2−2 src/Yoga/React/Native/MacOS/RadioButton.js+10−6 src/Yoga/React/Native/MacOS/RadioButton.purs+5−4 src/Yoga/React/Native/MacOS/Rive.js+2−16 src/Yoga/React/Native/MacOS/Rive.purs+3−3 src/Yoga/React/Native/MacOS/ScrollView.js+2−16 src/Yoga/React/Native/MacOS/ScrollView.purs+4−2 src/Yoga/React/Native/MacOS/SearchField.js+13−6 src/Yoga/React/Native/MacOS/SearchField.purs+6−5 src/Yoga/React/Native/MacOS/Segmented.js+11−6 src/Yoga/React/Native/MacOS/Segmented.purs+5−4 src/Yoga/React/Native/MacOS/Separator.js+2−6 src/Yoga/React/Native/MacOS/Separator.purs+2−2 src/Yoga/React/Native/MacOS/Sheet.js+10−6 src/Yoga/React/Native/MacOS/Sheet.purs+5−4 src/Yoga/React/Native/MacOS/Slider.js+12−6 src/Yoga/React/Native/MacOS/Slider.purs+5−4 src/Yoga/React/Native/MacOS/SpeechRecognition.js+15−6 src/Yoga/React/Native/MacOS/SpeechRecognition.purs+46−13 src/Yoga/React/Native/MacOS/SpeechSynthesizer.purs+14−11 src/Yoga/React/Native/MacOS/SplitView.js+2−6 src/Yoga/React/Native/MacOS/SplitView.purs+2−2 src/Yoga/React/Native/MacOS/Stepper.js+10−6 src/Yoga/React/Native/MacOS/Stepper.purs+5−4 src/Yoga/React/Native/MacOS/Switch.js+12−6 src/Yoga/React/Native/MacOS/Switch.purs+5−4 src/Yoga/React/Native/MacOS/TabView.js+10−6 src/Yoga/React/Native/MacOS/TabView.purs+5−4 src/Yoga/React/Native/MacOS/TableView.js+15−6 src/Yoga/React/Native/MacOS/TableView.purs+6−5 src/Yoga/React/Native/MacOS/TextEditor.js+10−6 src/Yoga/React/Native/MacOS/TextEditor.purs+5−4 src/Yoga/React/Native/MacOS/TextField.js+15−6 src/Yoga/React/Native/MacOS/TextField.purs+6−5 src/Yoga/React/Native/MacOS/TokenField.js+11−6 src/Yoga/React/Native/MacOS/TokenField.purs+5−4 src/Yoga/React/Native/MacOS/Toolbar.js+11−16 src/Yoga/React/Native/MacOS/Toolbar.purs+5−4 src/Yoga/React/Native/MacOS/Types.purs+42 src/Yoga/React/Native/MacOS/UserNotification.js+1−1 src/Yoga/React/Native/MacOS/UserNotification.purs+16−4 src/Yoga/React/Native/MacOS/VideoPlayer.js+2−16 src/Yoga/React/Native/MacOS/VideoPlayer.purs+2−2 src/Yoga/React/Native/MacOS/VisualEffect.js+2−16 src/Yoga/React/Native/MacOS/VisualEffect.purs+2−2 src/Yoga/React/Native/MacOS/WebView.js+13−6 src/Yoga/React/Native/MacOS/WebView.purs+6−5 src/Yoga/React/Native/MacOS/nativeComponent.js+15 test/MacOSComponents.test.js+85−76 test/MacOSImperative.test.js+43−27 test/MacOSSnapshots.test.js+81−75 test/__snapshots__/MacOSSnapshots.test.js.snap+9
.github/workflows/ci.ymlnew file+57
@@ -0,0 +1,57 @@
1+name: CI
2+on: [push, pull_request]
3+
4+permissions:
5+ contents: read
6+
7+jobs:
8+ build:
9+ runs-on: ubuntu-latest
10+ steps:
11+ - uses: actions/checkout@v4
12+ - uses: purescript-contrib/setup-purescript@main
13+ with:
14+ purescript: "0.15.x"
15+ - uses: oven-sh/setup-bun@v2
16+ - run: bun install
17+ - uses: actions/cache@v4
18+ with:
19+ path: |
20+ .spago
21+ output
22+ key: spago-${{ hashFiles('spago.yaml', 'spago.lock') }}
23+ - run: bunx spago build
24+ - run: bun test
25+
26+ docs:
27+ runs-on: ubuntu-latest
28+ if: github.ref == 'refs/heads/main' && github.event_name == 'push'
29+ needs: build
30+ permissions:
31+ pages: write
32+ id-token: write
33+ environment:
34+ name: github-pages
35+ url: ${{ steps.deployment.outputs.page_url }}
36+ steps:
37+ - uses: actions/checkout@v4
38+ - uses: purescript-contrib/setup-purescript@main
39+ with:
40+ purescript: "0.15.x"
41+ - uses: oven-sh/setup-bun@v2
42+ - run: bun install
43+ - uses: actions/cache@v4
44+ with:
45+ path: |
46+ .spago
47+ output
48+ key: spago-${{ hashFiles('spago.yaml', 'spago.lock') }}
49+ - run: bunx spago bundle -p yoga-react-native-docs --bundle-type app --platform browser --outfile dist/app.js --minify --module Docs.Main
50+ - run: cp docs/index.html docs/dist/
51+ - run: |
52+ sed -i 's|<script type="module" src="./index.js">|<script src="./app.js">|' docs/dist/index.html
53+ - uses: actions/upload-pages-artifact@v3
54+ with:
55+ path: docs/dist
56+ - id: deployment
57+ uses: actions/deploy-pages@v4
.gitignore+1
@@ -4,3 +4,4 @@ node_modules
44.psc-ide-port
55.DS_Store
66test/visual/diff/
7+docs/dist/
README.md+80−5
@@ -1,15 +1,90 @@
11# purescript-yoga-react-native
22
3−To install dependencies:
3+PureScript bindings for React Native macOS with 55+ native AppKit components.
4+
5+## Quick Start
46
57```bash
8+# Clone and install
9+git clone https://github.com/rowtype-yoga/purescript-yoga-react-native.git
10+cd purescript-yoga-react-native
611bun install
12+cd example-macos && bun install && cd ..
13+
14+# Install CocoaPods
15+cd example-macos/macos && bundle exec pod install && cd ../..
16+
17+# Build PureScript
18+cd example-macos && bunx spago build && cd ..
19+
20+# Run the example app
21+cd example-macos && npx react-native run-macos
722```
823
9−To run:
24+## Using in Your Project
1025
11−```bash
12−bun run index.ts
26+Add to `spago.yaml`:
27+
28+```yaml
29+extraPackages:
30+ yoga-react-native:
31+ git: https://github.com/rowtype-yoga/purescript-yoga-react-native.git
32+ ref: main
33+ yoga-react:
34+ git: https://github.com/rowtype-yoga/purescript-yoga-react.git
35+ ref: main
1336```
1437
15−This project was created using `bun init` in bun v1.2.21. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
38+```purescript
39+import Yoga.React.Native (text, tw, view)
40+import Yoga.React.Native.MacOS.Button (nativeButton)
41+import Yoga.React.Native.MacOS.Types as T
42+
43+myButton = nativeButton
44+ { title: "Click me"
45+ , bezelStyle: T.push
46+ , onPress: handler_ doSomething
47+ , style: Style.style { height: 24.0 }
48+ }
49+```
50+
51+## Components
52+
53+### Input Controls
54+`nativeButton` `nativeSwitch` `nativeSlider` `nativePopUp` `nativeComboBox` `nativeStepper` `nativeDatePicker` `nativeColorWell` `nativeCheckbox` `nativeRadioButton` `nativeSearchField` `nativeTokenField` `nativeSegmented`
55+
56+### Text
57+`nativeTextField` `nativeTextEditor`
58+
59+### Display
60+`nativeImage` `nativeAnimatedImage` `nativeVideoPlayer` `nativeLevelIndicator` `nativeProgress` `nativeSeparator` `nativePathControl`
61+
62+### Layout
63+`nativeBox` `nativeSplitView` `nativeTabView` `nativeScrollView` `sidebarLayout` `nativeToolbar` `nativeVisualEffect` `nativePatternBackground`
64+
65+### Overlays
66+`macosAlert` `nativeSheet` `nativePopover` `nativeContextMenu` `macosShowMenu`
67+
68+### Data Views
69+`nativeTableView` `nativeOutlineView`
70+
71+### Files & Drag/Drop
72+`nativeDropZone` `nativeFilePicker`
73+
74+### Rich Media
75+`nativeMapView` `nativePDFView` `nativeWebView` `nativeCameraView` `nativeRiveView`
76+
77+### System Services
78+`copyToClipboard` `macosShare` `macosNotify` `macosPlaySound` `macosSetStatusBarItem` `macosQuickLook` `macosShowColorPanel` `macosShowFontPanel` `say`
79+
80+### AI & ML
81+`recognizeText` (OCR) `useSpeechRecognition` `detectLanguage` `analyzeSentiment` `tokenize`
82+
83+## Prerequisites
84+
85+- macOS 14+
86+- Xcode 16+
87+- Node.js 20+ / Bun
88+- PureScript 0.15+ and Spago
89+- CocoaPods
90+
bun.lock+22−6
@@ -10,7 +10,9 @@
1010 },
1111 "devDependencies": {
1212 "@types/bun": "latest",
13+ "playwright": "^1.58.2",
1314 "react": "^19.2.4",
15+ "vite": "^7.3.1",
1416 "vitest": "^4.0.18",
1517 },
1618 "peerDependencies": {
@@ -371,7 +373,7 @@
371373
372374 "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
373375
374− "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
376+ "commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
375377
376378 "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
377379
@@ -439,7 +441,7 @@
439441
440442 "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
441443
442− "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
444+ "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
443445
444446 "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
445447
@@ -633,6 +635,10 @@
633635
634636 "pixelmatch": ["pixelmatch@7.1.0", "", { "dependencies": { "pngjs": "^7.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng=="],
635637
638+ "playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="],
639+
640+ "playwright-core": ["playwright-core@1.58.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="],
641+
636642 "pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="],
637643
638644 "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
@@ -693,7 +699,7 @@
693699
694700 "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
695701
696− "source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="],
702+ "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
697703
698704 "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
699705
@@ -809,6 +815,8 @@
809815
810816 "istanbul-lib-instrument/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
811817
818+ "jest-haste-map/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
819+
812820 "jest-message-util/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="],
813821
814822 "jest-util/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="],
@@ -821,23 +829,31 @@
821829
822830 "lighthouse-logger/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
823831
832+ "metro/source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="],
833+
834+ "metro-source-map/source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="],
835+
836+ "metro-symbolicate/source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="],
837+
824838 "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
825839
840+ "react-native/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
841+
826842 "react-native/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="],
827843
828844 "react-test-renderer/react-is": ["react-is@19.2.4", "", {}, "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA=="],
829845
846+ "rollup/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
847+
830848 "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
831849
832850 "send/on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
833851
834852 "send/statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
835853
836− "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
837−
838854 "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="],
839855
840− "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
856+ "vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
841857
842858 "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
843859
docs/index.htmlnew file+33
@@ -0,0 +1,33 @@
1+<!doctype html>
2+<html lang="en" class="dark">
3+ <head>
4+ <meta charset="UTF-8" />
5+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+ <title>purescript-yoga-react-native</title>
7+ <script src="https://cdn.tailwindcss.com"></script>
8+ <script>
9+ tailwind.config = {
10+ darkMode: "class",
11+ theme: {
12+ extend: {
13+ colors: {
14+ brand: "#7c6ef6",
15+ "brand-dim": "#5a4fd4",
16+ },
17+ fontFamily: {
18+ mono: [
19+ '"SF Mono"',
20+ '"Fira Code"',
21+ '"JetBrains Mono"',
22+ "monospace",
23+ ],
24+ },
25+ },
26+ },
27+ };
28+ </script>
29+ </head>
30+ <body class="bg-gray-950 text-gray-200 font-sans antialiased">
31+ <script type="module" src="./index.js"></script>
32+ </body>
33+</html>
docs/index.jsnew file+2
@@ -0,0 +1,2 @@
1+import { main } from "../output/Docs.Main/index.js";
2+main();
docs/spago.yamlnew file+10
@@ -0,0 +1,10 @@
1+package:
2+ name: yoga-react-native-docs
3+ dependencies:
4+ - deku-core
5+ - deku-dom
6+ - deku-spa
7+ - effect
8+ - hyrule
9+ - prelude
10+ - tuples
docs/src/Docs/App.pursnew file+43
@@ -0,0 +1,43 @@
1+module Docs.App where
2+
3+import Data.Tuple.Nested ((/\))
4+import Deku.Core (Nut)
5+import Deku.Hooks ((<#~>), useState)
6+import Deku.Do as Deku
7+import Docs.Components.Layout as L
8+import Docs.Components.Sidebar as Sidebar
9+import Docs.Pages.AiMl as AiMl
10+import Docs.Pages.DataViews as DataViews
11+import Docs.Pages.Display as Display
12+import Docs.Pages.FilesAndDragDrop as FilesAndDragDrop
13+import Docs.Pages.InputControls as InputControls
14+import Docs.Pages.LayoutPage as LayoutPage
15+import Docs.Pages.Overlays as Overlays
16+import Docs.Pages.Overview as Overview
17+import Docs.Pages.QuickStart as QuickStart
18+import Docs.Pages.RichMedia as RichMedia
19+import Docs.Pages.SystemServices as SystemServices
20+import Docs.Pages.TextEditing as TextEditing
21+import Docs.Pages.Types (Page(..))
22+
23+app :: Nut
24+app = Deku.do
25+ setPage /\ currentPage <- useState Overview
26+ L.pageShell
27+ (Sidebar.sidebar setPage)
28+ (currentPage <#~> renderPage)
29+
30+renderPage :: Page -> Nut
31+renderPage = case _ of
32+ Overview -> Overview.page
33+ QuickStart -> QuickStart.page
34+ InputControls -> InputControls.page
35+ TextEditing -> TextEditing.page
36+ Display -> Display.page
37+ LayoutPage -> LayoutPage.page
38+ Overlays -> Overlays.page
39+ DataViews -> DataViews.page
40+ FilesAndDragDrop -> FilesAndDragDrop.page
41+ RichMedia -> RichMedia.page
42+ SystemServices -> SystemServices.page
43+ AiMl -> AiMl.page
docs/src/Docs/Components/CodeBlock.pursnew file+12
@@ -0,0 +1,12 @@
1+module Docs.Components.CodeBlock where
2+
3+import Deku.Core (Nut)
4+import Deku.DOM as D
5+import Deku.DOM.Attributes as DA
6+
7+codeBlock :: String -> Nut
8+codeBlock code =
9+ D.pre [ DA.klass_ "bg-gray-950 border border-gray-800 rounded-md p-4 overflow-x-auto mb-4" ]
10+ [ D.code [ DA.klass_ "font-mono text-sm text-gray-200 whitespace-pre" ]
11+ [ D.text_ code ]
12+ ]
docs/src/Docs/Components/Layout.pursnew file+54
@@ -0,0 +1,54 @@
1+module Docs.Components.Layout where
2+
3+import Prelude
4+
5+import Deku.Core (Nut)
6+import Deku.DOM as D
7+import Deku.DOM.Attributes as DA
8+
9+pageShell :: Nut -> Nut -> Nut
10+pageShell sidebar content =
11+ D.div [ DA.klass_ "flex flex-col min-h-screen" ]
12+ [ header
13+ , D.div [ DA.klass_ "flex flex-1" ]
14+ [ D.aside [ DA.klass_ "w-64 min-w-[16rem] bg-gray-900 border-r border-gray-800 sticky top-14 h-[calc(100vh-3.5rem)] overflow-y-auto" ]
15+ [ sidebar ]
16+ , D.main [ DA.klass_ "flex-1 max-w-4xl p-8 overflow-y-auto" ]
17+ [ content ]
18+ ]
19+ ]
20+
21+header :: Nut
22+header =
23+ D.header [ DA.klass_ "sticky top-0 z-50 bg-gray-900 border-b border-gray-800 px-6 py-3" ]
24+ [ D.div [ DA.klass_ "flex items-baseline gap-4 max-w-7xl mx-auto" ]
25+ [ D.h1 [ DA.klass_ "text-lg font-semibold text-gray-100 font-mono" ]
26+ [ D.text_ "purescript-yoga-react-native" ]
27+ , D.span [ DA.klass_ "text-sm text-gray-500" ]
28+ [ D.text_ "PureScript bindings for React Native macOS" ]
29+ ]
30+ ]
31+
32+section :: String -> Array Nut -> Nut
33+section title children =
34+ D.section [ DA.klass_ "mb-10" ]
35+ ( [ D.h2 [ DA.klass_ "text-2xl font-semibold text-gray-100 mb-4 pb-2 border-b border-gray-800" ]
36+ [ D.text_ title ]
37+ ] <> children
38+ )
39+
40+componentDoc :: String -> String -> String -> Array Nut -> Nut
41+componentDoc name importPath example children =
42+ D.div [ DA.klass_ "mb-8 bg-gray-900 border border-gray-800 rounded-lg p-6" ]
43+ [ D.h3 [ DA.klass_ "text-lg font-semibold text-brand font-mono mb-2" ]
44+ [ D.text_ name ]
45+ , D.p [ DA.klass_ "mb-3" ]
46+ [ D.code [ DA.klass_ "text-xs text-gray-500 font-mono" ]
47+ [ D.text_ ("import " <> importPath) ]
48+ ]
49+ , D.pre [ DA.klass_ "bg-gray-950 border border-gray-800 rounded-md p-4 overflow-x-auto mb-4" ]
50+ [ D.code [ DA.klass_ "font-mono text-sm text-gray-200 whitespace-pre" ]
51+ [ D.text_ example ]
52+ ]
53+ , D.div_ children
54+ ]
docs/src/Docs/Components/PropsTable.pursnew file+33
@@ -0,0 +1,33 @@
1+module Docs.Components.PropsTable where
2+
3+import Prelude
4+
5+import Deku.Core (Nut)
6+import Deku.DOM as D
7+import Deku.DOM.Attributes as DA
8+
9+type Prop =
10+ { name :: String
11+ , type_ :: String
12+ , description :: String
13+ }
14+
15+propsTable :: Array Prop -> Nut
16+propsTable props =
17+ D.table [ DA.klass_ "w-full text-sm border-collapse" ]
18+ [ D.thead_
19+ [ D.tr_
20+ [ D.th [ DA.klass_ "text-left px-3 py-2 border-b-2 border-gray-800 text-gray-500 font-semibold text-xs uppercase tracking-wide" ] [ D.text_ "Prop" ]
21+ , D.th [ DA.klass_ "text-left px-3 py-2 border-b-2 border-gray-800 text-gray-500 font-semibold text-xs uppercase tracking-wide" ] [ D.text_ "Type" ]
22+ , D.th [ DA.klass_ "text-left px-3 py-2 border-b-2 border-gray-800 text-gray-500 font-semibold text-xs uppercase tracking-wide" ] [ D.text_ "Description" ]
23+ ]
24+ ]
25+ , D.tbody_ (map propRow props)
26+ ]
27+ where
28+ propRow p =
29+ D.tr_
30+ [ D.td [ DA.klass_ "px-3 py-2 border-b border-gray-800 align-top" ] [ D.code [ DA.klass_ "font-mono text-xs text-brand" ] [ D.text_ p.name ] ]
31+ , D.td [ DA.klass_ "px-3 py-2 border-b border-gray-800 align-top" ] [ D.code [ DA.klass_ "font-mono text-xs text-gray-400" ] [ D.text_ p.type_ ] ]
32+ , D.td [ DA.klass_ "px-3 py-2 border-b border-gray-800 align-top text-gray-400" ] [ D.text_ p.description ]
33+ ]
docs/src/Docs/Components/Sidebar.pursnew file+47
@@ -0,0 +1,47 @@
1+module Docs.Components.Sidebar where
2+
3+import Prelude
4+
5+import Deku.Core (Nut)
6+import Deku.DOM as D
7+import Deku.DOM.Attributes as DA
8+import Deku.DOM.Listeners as DL
9+import Docs.Pages.Types (Page(..))
10+import Effect (Effect)
11+
12+sidebar :: (Page -> Effect Unit) -> Nut
13+sidebar setPage =
14+ D.nav [ DA.klass_ "flex flex-col py-4" ]
15+ [ navSection "Getting Started"
16+ [ navItem Overview "Overview"
17+ , navItem QuickStart "Quick Start"
18+ ]
19+ , navSection "macOS Components"
20+ [ navItem InputControls "Input Controls"
21+ , navItem TextEditing "Text"
22+ , navItem Display "Display"
23+ , navItem LayoutPage "Layout"
24+ , navItem Overlays "Overlays"
25+ , navItem DataViews "Data Views"
26+ , navItem FilesAndDragDrop "Files & Drag/Drop"
27+ , navItem RichMedia "Rich Media"
28+ ]
29+ , navSection "APIs"
30+ [ navItem SystemServices "System Services"
31+ , navItem AiMl "AI & ML"
32+ ]
33+ ]
34+ where
35+ navSection title items =
36+ D.div [ DA.klass_ "mb-2" ]
37+ ( [ D.h4 [ DA.klass_ "text-[11px] font-semibold uppercase tracking-wide text-gray-500 px-5 py-1" ]
38+ [ D.text_ title ]
39+ ] <> items
40+ )
41+
42+ navItem pg label =
43+ D.a
44+ [ DA.klass_ "block px-5 pl-7 py-1.5 text-sm text-gray-300 cursor-pointer hover:bg-gray-800 hover:text-brand transition-colors"
45+ , DL.click_ \_ -> setPage pg
46+ ]
47+ [ D.text_ label ]
docs/src/Docs/Main.pursnew file+10
@@ -0,0 +1,10 @@
1+module Docs.Main where
2+
3+import Prelude
4+
5+import Deku.SPA (runInBody)
6+import Docs.App (app)
7+import Effect (Effect)
8+
9+main :: Effect Unit
10+main = void $ runInBody app
docs/src/Docs/Pages/AiMl.pursnew file+77
@@ -0,0 +1,77 @@
1+module Docs.Pages.AiMl where
2+
3+import Deku.Core (Nut)
4+import Deku.DOM as D
5+import Deku.DOM.Attributes as DA
6+import Docs.Components.Layout as L
7+import Docs.Components.PropsTable (propsTable)
8+
9+page :: Nut
10+page =
11+ D.div_
12+ [ L.section "AI & ML" [ D.p_ [ D.text_ "On-device machine learning powered by Apple's native frameworks." ] ]
13+ , ocr
14+ , speechRecognition
15+ , languageDetection
16+ , sentimentAnalysis
17+ , tokenization
18+ ]
19+
20+ocr :: Nut
21+ocr = L.componentDoc "recognizeText" "Yoga.React.Native.MacOS.OCR (recognizeText)"
22+ """-- Aff-based, returns recognized text
23+result <- recognizeText "/path/to/image.png" # liftAff"""
24+ [ D.p [ DA.klass_ "text-sm text-gray-400 mt-2" ]
25+ [ D.text_ "Signature: String -> Aff String" ]
26+ , D.p [ DA.klass_ "text-sm text-gray-400" ]
27+ [ D.text_ "Uses Apple Vision framework for optical character recognition." ]
28+ ]
29+
30+speechRecognition :: Nut
31+speechRecognition = L.componentDoc "useSpeechRecognition" "Yoga.React.Native.MacOS.SpeechRecognition (useSpeechRecognition)"
32+ """-- React hook for live speech-to-text
33+{ listening, transcript, start, stop } <- useSpeechRecognition"""
34+ [ D.p [ DA.klass_ "text-sm text-gray-400 mt-2" ]
35+ [ D.text_ "Hook UseSpeechRecognition SpeechRecognition" ]
36+ , propsTable
37+ [ { name: "listening", type_: "Boolean", description: "Whether recognition is active" }
38+ , { name: "transcript", type_: "String", description: "Live transcription text" }
39+ , { name: "start", type_: "Effect Unit", description: "Start recognition" }
40+ , { name: "stop", type_: "Effect Unit", description: "Stop recognition" }
41+ ]
42+ , D.p [ DA.klass_ "text-sm text-gray-400" ]
43+ [ D.text_ "Uses Apple Speech framework. Requires microphone permission." ]
44+ ]
45+
46+languageDetection :: Nut
47+languageDetection = L.componentDoc "detectLanguage" "Yoga.React.Native.MacOS.NaturalLanguage (detectLanguage)"
48+ """-- Aff-based, returns ISO language code
49+lang <- detectLanguage "Bonjour le monde" # liftAff
50+-- lang :: String (e.g. "fr")"""
51+ [ D.p [ DA.klass_ "text-sm text-gray-400 mt-2" ]
52+ [ D.text_ "Signature: String -> Aff String" ]
53+ , D.p [ DA.klass_ "text-sm text-gray-400" ]
54+ [ D.text_ "Detects the dominant language using NLLanguageRecognizer." ]
55+ ]
56+
57+sentimentAnalysis :: Nut
58+sentimentAnalysis = L.componentDoc "analyzeSentiment" "Yoga.React.Native.MacOS.NaturalLanguage (analyzeSentiment)"
59+ """-- Aff-based, returns sentiment score
60+score <- analyzeSentiment "This is great!" # liftAff
61+-- score :: Number (-1.0 to 1.0)"""
62+ [ D.p [ DA.klass_ "text-sm text-gray-400 mt-2" ]
63+ [ D.text_ "Signature: String -> Aff Number" ]
64+ , D.p [ DA.klass_ "text-sm text-gray-400" ]
65+ [ D.text_ "Returns a sentiment score from -1.0 (negative) to 1.0 (positive)." ]
66+ ]
67+
68+tokenization :: Nut
69+tokenization = L.componentDoc "tokenize" "Yoga.React.Native.MacOS.NaturalLanguage (tokenize)"
70+ """-- Aff-based, returns array of tokens
71+tokens <- tokenize "Hello world" # liftAff
72+-- tokens :: Array String"""
73+ [ D.p [ DA.klass_ "text-sm text-gray-400 mt-2" ]
74+ [ D.text_ "Signature: String -> Aff (Array String)" ]
75+ , D.p [ DA.klass_ "text-sm text-gray-400" ]
76+ [ D.text_ "Splits text into linguistic tokens using NLTokenizer." ]
77+ ]
docs/src/Docs/Pages/DataViews.pursnew file+73
@@ -0,0 +1,73 @@
1+module Docs.Pages.DataViews where
2+
3+import Deku.Core (Nut)
4+import Deku.DOM as D
5+import Docs.Components.Layout as L
6+import Docs.Components.PropsTable (propsTable)
7+
8+page :: Nut
9+page =
10+ D.div_
11+ [ L.section "Data Views" [ D.p_ [ D.text_ "Structured data display with tables and outlines." ] ]
12+ , tableView
13+ , outlineView
14+ ]
15+
16+tableView :: Nut
17+tableView = L.componentDoc "nativeTableView" "Yoga.React.Native.MacOS.TableView (nativeTableView)"
18+ """selected /\ setSelected <- useState' ""
19+
20+nativeTableView
21+ { columns:
22+ [ { id: "name", title: "Name", width: 150.0 }
23+ , { id: "type", title: "Type", width: 100.0 }
24+ , { id: "size", title: "Size", width: 80.0 }
25+ ]
26+ , rows:
27+ [ [ "Main.purs", "PureScript", "12 KB" ]
28+ , [ "App.tsx", "TypeScript", "8 KB" ]
29+ , [ "style.css", "CSS", "3 KB" ]
30+ , [ "index.html", "HTML", "1 KB" ]
31+ ]
32+ , headerVisible: true
33+ , alternatingRows: true
34+ , onSelectRow: \i -> setSelected ("Row " <> show i)
35+ , onDoubleClickRow: \i -> setSelected ("Double-clicked row " <> show i)
36+ }"""
37+ [ propsTable
38+ [ { name: "columns", type_: "Array TableColumn", description: "Column definitions ({ id, title, width })" }
39+ , { name: "rows", type_: "Array (Array String)", description: "Row data as arrays of strings" }
40+ , { name: "headerVisible", type_: "Boolean", description: "Show column headers" }
41+ , { name: "alternatingRows", type_: "Boolean", description: "Alternating row backgrounds" }
42+ , { name: "onSelectRow", type_: "Int -> Effect Unit", description: "Row selection callback" }
43+ , { name: "onDoubleClickRow", type_: "Int -> Effect Unit", description: "Row double-click callback" }
44+ ]
45+ ]
46+
47+outlineView :: Nut
48+outlineView = L.componentDoc "nativeOutlineView" "Yoga.React.Native.MacOS.OutlineView (nativeOutlineView)"
49+ """selection /\ setSelection <- useState' ""
50+let file id title = OutlineItem { id, title, sfSymbol: "doc", children: [] }
51+let folder id title children = OutlineItem { id, title, sfSymbol: "folder", children }
52+
53+nativeOutlineView
54+ { items:
55+ [ folder "src" "src"
56+ [ file "main" "Main.purs"
57+ , folder "macos" "MacOS"
58+ [ file "btn" "Button.purs"
59+ , file "sl" "Slider.purs"
60+ ]
61+ ]
62+ , folder "test" "test"
63+ [ file "t1" "MacOSComponents.test.js" ]
64+ ]
65+ , headerVisible: false
66+ , onSelectItem: setSelection
67+ }"""
68+ [ propsTable
69+ [ { name: "items", type_: "Array OutlineItem", description: "Hierarchical tree data (newtype with { id, title, sfSymbol, children })" }
70+ , { name: "headerVisible", type_: "Boolean", description: "Show header" }
71+ , { name: "onSelectItem", type_: "String -> Effect Unit", description: "Item selection callback" }
72+ ]
73+ ]
docs/src/Docs/Pages/Display.pursnew file+121
@@ -0,0 +1,121 @@
1+module Docs.Pages.Display where
2+
3+import Deku.Core (Nut)
4+import Deku.DOM as D
5+import Docs.Components.Layout as L
6+import Docs.Components.PropsTable (propsTable)
7+
8+page :: Nut
9+page =
10+ D.div_
11+ [ L.section "Display" [ D.p_ [ D.text_ "Visual display components for images, video, progress, and more." ] ]
12+ , image
13+ , animatedImage
14+ , videoPlayer
15+ , levelIndicator
16+ , progress
17+ , separator
18+ , pathControl
19+ ]
20+
21+image :: Nut
22+image = L.componentDoc "nativeImage" "Yoga.React.Native.MacOS.Image (nativeImage)"
23+ """nativeImage
24+ { source: "path/to/image.png"
25+ , contentMode: T.scaleAspectFit
26+ , cornerRadius: 8.0
27+ }"""
28+ [ propsTable
29+ [ { name: "source", type_: "String", description: "Image file path or URL" }
30+ , { name: "contentMode", type_: "ImageContentMode", description: "How the image fills its bounds" }
31+ , { name: "cornerRadius", type_: "Number", description: "Corner radius in points" }
32+ ]
33+ ]
34+
35+animatedImage :: Nut
36+animatedImage = L.componentDoc "nativeAnimatedImage" "Yoga.React.Native.MacOS.AnimatedImage (nativeAnimatedImage)"
37+ """nativeAnimatedImage
38+ { source: "loading.gif"
39+ , animating: true
40+ , cornerRadius: 4.0
41+ }"""
42+ [ propsTable
43+ [ { name: "source", type_: "String", description: "Animated image file path" }
44+ , { name: "animating", type_: "Boolean", description: "Start/stop animation" }
45+ , { name: "cornerRadius", type_: "Number", description: "Corner radius in points" }
46+ ]
47+ ]
48+
49+videoPlayer :: Nut
50+videoPlayer = L.componentDoc "nativeVideoPlayer" "Yoga.React.Native.MacOS.VideoPlayer (nativeVideoPlayer)"
51+ """nativeVideoPlayer
52+ { source: "https://example.com/video.mp4"
53+ , playing: true
54+ , looping: false
55+ , muted: false
56+ , controlsStyle: T.floating
57+ }"""
58+ [ propsTable
59+ [ { name: "source", type_: "String", description: "Video file path or URL" }
60+ , { name: "playing", type_: "Boolean", description: "Play/pause state" }
61+ , { name: "looping", type_: "Boolean", description: "Loop playback" }
62+ , { name: "muted", type_: "Boolean", description: "Mute audio" }
63+ , { name: "cornerRadius", type_: "Number", description: "Corner radius in points" }
64+ , { name: "controlsStyle", type_: "ControlsStyle", description: "Player controls style" }
65+ ]
66+ ]
67+
68+levelIndicator :: Nut
69+levelIndicator = L.componentDoc "nativeLevelIndicator" "Yoga.React.Native.MacOS.LevelIndicator (nativeLevelIndicator)"
70+ """nativeLevelIndicator
71+ { value: 7.0
72+ , minValue: 0.0
73+ , maxValue: 10.0
74+ , warningValue: 7.0
75+ , criticalValue: 9.0
76+ }"""
77+ [ propsTable
78+ [ { name: "value", type_: "Number", description: "Current level" }
79+ , { name: "minValue", type_: "Number", description: "Minimum value" }
80+ , { name: "maxValue", type_: "Number", description: "Maximum value" }
81+ , { name: "warningValue", type_: "Number", description: "Warning threshold" }
82+ , { name: "criticalValue", type_: "Number", description: "Critical threshold" }
83+ ]
84+ ]
85+
86+progress :: Nut
87+progress = L.componentDoc "nativeProgress" "Yoga.React.Native.MacOS.Progress (nativeProgress)"
88+ """nativeProgress
89+ { value: 0.65
90+ , indeterminate: false
91+ , spinning: false
92+ }"""
93+ [ propsTable
94+ [ { name: "value", type_: "Number", description: "Progress value 0.0\x20131.0" }
95+ , { name: "indeterminate", type_: "Boolean", description: "Indeterminate mode (ignores value)" }
96+ , { name: "spinning", type_: "Boolean", description: "Spinning indicator style" }
97+ ]
98+ ]
99+
100+separator :: Nut
101+separator = L.componentDoc "nativeSeparator" "Yoga.React.Native.MacOS.Separator (nativeSeparator)"
102+ """nativeSeparator
103+ { vertical: false }"""
104+ [ propsTable
105+ [ { name: "vertical", type_: "Boolean", description: "Vertical or horizontal separator" }
106+ ]
107+ ]
108+
109+pathControl :: Nut
110+pathControl = L.componentDoc "nativePathControl" "Yoga.React.Native.MacOS.PathControl (nativePathControl)"
111+ """nativePathControl
112+ { url: "/Users/you/Documents"
113+ , pathStyle: T.standard
114+ , onSelectPath: setPath
115+ }"""
116+ [ propsTable
117+ [ { name: "url", type_: "String", description: "File path URL to display" }
118+ , { name: "pathStyle", type_: "PathControlStyle", description: "Visual style of the path control" }
119+ , { name: "onSelectPath", type_: "String -> Effect Unit", description: "Path segment selection callback" }
120+ ]
121+ ]
docs/src/Docs/Pages/FilesAndDragDrop.pursnew file+65
@@ -0,0 +1,65 @@
1+module Docs.Pages.FilesAndDragDrop where
2+
3+import Deku.Core (Nut)
4+import Deku.DOM as D
5+import Deku.DOM.Attributes as DA
6+import Docs.Components.Layout as L
7+import Docs.Components.PropsTable (propsTable)
8+
9+page :: Nut
10+page =
11+ D.div_
12+ [ L.section "Files & Drag/Drop" [ D.p_ [ D.text_ "File system access and drag-and-drop support." ] ]
13+ , filePicker
14+ , dropZone
15+ ]
16+
17+filePicker :: Nut
18+filePicker = L.componentDoc "nativeFilePicker" "Yoga.React.Native.MacOS.FilePicker (nativeFilePicker)"
19+ """picked /\ setPicked <- useState' ""
20+
21+nativeFilePicker
22+ { mode: T.openFile
23+ , title: "Open Files"
24+ , sfSymbol: "doc.badge.plus"
25+ , allowMultiple: true
26+ , allowedTypes: [ "public.image", "public.text" ]
27+ , message: "Select files to open"
28+ , onPickFiles: \files -> setPicked (joinWith ", " (map _.path files))
29+ , onCancel: setPicked "Cancelled"
30+ }"""
31+ [ propsTable
32+ [ { name: "mode", type_: "FilePickerMode", description: "openFile or saveFile" }
33+ , { name: "title", type_: "String", description: "Dialog title" }
34+ , { name: "sfSymbol", type_: "String", description: "SF Symbol icon for the button" }
35+ , { name: "message", type_: "String", description: "Dialog message" }
36+ , { name: "defaultName", type_: "String", description: "Default file name (save mode)" }
37+ , { name: "allowedTypes", type_: "Array String", description: "Allowed file extensions" }
38+ , { name: "allowMultiple", type_: "Boolean", description: "Allow selecting multiple files" }
39+ , { name: "canChooseDirectories", type_: "Boolean", description: "Allow choosing directories" }
40+ , { name: "onPickFiles", type_: "Array PickedFile -> Effect Unit", description: "Files selected callback ({ path, name })" }
41+ , { name: "onCancel", type_: "Effect Unit", description: "Cancel callback" }
42+ ]
43+ ]
44+
45+dropZone :: Nut
46+dropZone = L.componentDoc "nativeDropZone" "Yoga.React.Native.MacOS.DropZone (nativeDropZone)"
47+ """dropped /\ setDropped <- useState' ""
48+dragging /\ setDragging <- useState' false
49+
50+nativeDropZone
51+ { onFileDrop: \d -> do
52+ setDropped (joinWith ", " (map _.name d.files))
53+ setDragging false
54+ , onFilesDragEnter: setDragging true
55+ , onFilesDragExit: setDragging false
56+ }
57+ [ text_ (if dragging then "Release to drop!" else "Drop files here") ]"""
58+ [ propsTable
59+ [ { name: "onFileDrop", type_: "DroppedData -> Effect Unit", description: "Drop callback ({ files, strings })" }
60+ , { name: "onFilesDragEnter", type_: "Effect Unit", description: "Drag enter callback" }
61+ , { name: "onFilesDragExit", type_: "Effect Unit", description: "Drag exit callback" }
62+ ]
63+ , D.p [ DA.klass_ "text-sm text-gray-400 mt-2" ]
64+ [ D.text_ "The drop target content is passed as the second argument (IsJSX)." ]
65+ ]
docs/src/Docs/Pages/InputControls.pursnew file+278
@@ -0,0 +1,278 @@
1+module Docs.Pages.InputControls where
2+
3+import Deku.Core (Nut)
4+import Deku.DOM as D
5+import Docs.Components.Layout as L
6+import Docs.Components.PropsTable (propsTable)
7+
8+page :: Nut
9+page =
10+ D.div_
11+ [ L.section "Input Controls" [ D.p_ [ D.text_ "Native macOS AppKit input controls." ] ]
12+ , button
13+ , switch
14+ , slider
15+ , stepper
16+ , checkbox
17+ , radioButton
18+ , segmented
19+ , popUp
20+ , comboBox
21+ , searchField
22+ , tokenField
23+ , datePicker
24+ , colorWell
25+ , helpButton
26+ ]
27+
28+button :: Nut
29+button = L.componentDoc "nativeButton" "Yoga.React.Native.MacOS.Button (nativeButton)"
30+ """status /\ setStatus <- useState' "Ready"
31+
32+nativeButton
33+ { title: "Say Hello"
34+ , bezelStyle: T.push
35+ , primary: true
36+ , onPress: setStatus "Hello from PureScript!"
37+ }"""
38+ [ propsTable
39+ [ { name: "title", type_: "String", description: "Button label text (mutually exclusive with sfSymbol)" }
40+ , { name: "sfSymbol", type_: "String", description: "SF Symbol icon (mutually exclusive with title)" }
41+ , { name: "bezelStyle", type_: "BezelStyle", description: "push | toolbar | texturedSquare | inline" }
42+ , { name: "destructive", type_: "Boolean", description: "Red destructive styling" }
43+ , { name: "primary", type_: "Boolean", description: "Primary action button" }
44+ , { name: "buttonEnabled", type_: "Boolean", description: "Enable/disable the button" }
45+ , { name: "onPress", type_: "Effect Unit", description: "Press callback" }
46+ ]
47+ ]
48+
49+switch :: Nut
50+switch = L.componentDoc "nativeSwitch" "Yoga.React.Native.MacOS.Switch (nativeSwitch)"
51+ """on /\ setOn <- useState' false
52+
53+nativeSwitch
54+ { on
55+ , onChange: setOn
56+ }"""
57+ [ propsTable
58+ [ { name: "on", type_: "Boolean", description: "Current on/off state" }
59+ , { name: "onChange", type_: "Boolean -> Effect Unit", description: "Toggle callback" }
60+ ]
61+ ]
62+
63+slider :: Nut
64+slider = L.componentDoc "nativeSlider" "Yoga.React.Native.MacOS.Slider (nativeSlider)"
65+ """value /\ setValue <- useState' 50.0
66+
67+nativeSlider
68+ { value
69+ , minValue: 0.0
70+ , maxValue: 100.0
71+ , numberOfTickMarks: 11
72+ , onChange: setValue
73+ }"""
74+ [ propsTable
75+ [ { name: "value", type_: "Number", description: "Current value" }
76+ , { name: "minValue", type_: "Number", description: "Range minimum" }
77+ , { name: "maxValue", type_: "Number", description: "Range maximum" }
78+ , { name: "numberOfTickMarks", type_: "Int", description: "Number of tick marks" }
79+ , { name: "allowsTickMarkValuesOnly", type_: "Boolean", description: "Snap to tick marks only" }
80+ , { name: "onChange", type_: "Number -> Effect Unit", description: "Value change callback" }
81+ ]
82+ ]
83+
84+stepper :: Nut
85+stepper = L.componentDoc "nativeStepper" "Yoga.React.Native.MacOS.Stepper (nativeStepper)"
86+ """value /\ setValue <- useState' 5.0
87+
88+nativeStepper
89+ { value
90+ , minValue: 0.0
91+ , maxValue: 50.0
92+ , increment: 1.0
93+ , onChange: setValue
94+ }"""
95+ [ propsTable
96+ [ { name: "value", type_: "Number", description: "Current value" }
97+ , { name: "minValue", type_: "Number", description: "Range minimum" }
98+ , { name: "maxValue", type_: "Number", description: "Range maximum" }
99+ , { name: "increment", type_: "Number", description: "Step increment" }
100+ , { name: "onChange", type_: "Number -> Effect Unit", description: "Value change callback" }
101+ ]
102+ ]
103+
104+checkbox :: Nut
105+checkbox = L.componentDoc "nativeCheckbox" "Yoga.React.Native.MacOS.Checkbox (nativeCheckbox)"
106+ """a /\ setA <- useState' true
107+
108+nativeCheckbox
109+ { checked: a
110+ , title: "Enable notifications"
111+ , enabled: true
112+ , onChange: setA
113+ }"""
114+ [ propsTable
115+ [ { name: "title", type_: "String", description: "Label text" }
116+ , { name: "checked", type_: "Boolean", description: "Checked state" }
117+ , { name: "enabled", type_: "Boolean", description: "Enable/disable" }
118+ , { name: "onChange", type_: "Boolean -> Effect Unit", description: "Toggle callback" }
119+ ]
120+ ]
121+
122+radioButton :: Nut
123+radioButton = L.componentDoc "nativeRadioButton" "Yoga.React.Native.MacOS.RadioButton (nativeRadioButton)"
124+ """choice /\ setChoice <- useState' "option1"
125+
126+nativeRadioButton
127+ { selected: choice == "option1"
128+ , title: "Small"
129+ , enabled: true
130+ , onChange: \_ -> setChoice "option1"
131+ }
132+nativeRadioButton
133+ { selected: choice == "option2"
134+ , title: "Medium"
135+ , enabled: true
136+ , onChange: \_ -> setChoice "option2"
137+ }"""
138+ [ propsTable
139+ [ { name: "title", type_: "String", description: "Label text" }
140+ , { name: "selected", type_: "Boolean", description: "Selection state" }
141+ , { name: "enabled", type_: "Boolean", description: "Enable/disable" }
142+ , { name: "onChange", type_: "Boolean -> Effect Unit", description: "Selection callback" }
143+ ]
144+ ]
145+
146+segmented :: Nut
147+segmented = L.componentDoc "nativeSegmented" "Yoga.React.Native.MacOS.Segmented (nativeSegmented)"
148+ """mapType /\ setMapType <- useState' 0
149+
150+nativeSegmented
151+ { labels: [ "Standard", "Satellite", "Hybrid" ]
152+ , selectedIndex: mapType
153+ , onChange: setMapType
154+ }"""
155+ [ propsTable
156+ [ { name: "labels", type_: "Array String", description: "Segment labels" }
157+ , { name: "sfSymbols", type_: "Array String", description: "SF Symbol icons per segment" }
158+ , { name: "selectedIndex", type_: "Int", description: "Currently selected index" }
159+ , { name: "onChange", type_: "Int -> Effect Unit", description: "Selection callback" }
160+ ]
161+ ]
162+
163+popUp :: Nut
164+popUp = L.componentDoc "nativePopUp" "Yoga.React.Native.MacOS.PopUp (nativePopUp)"
165+ """idx /\ setIdx <- useState' 0
166+title /\ setTitle <- useState' "Small"
167+let items = [ "Small", "Medium", "Large", "Extra Large" ]
168+
169+nativePopUp
170+ { items
171+ , selectedIndex: idx
172+ , onChange: \i -> do
173+ setIdx i
174+ setTitle (fromMaybe "" (items !! i))
175+ }"""
176+ [ propsTable
177+ [ { name: "items", type_: "Array String", description: "Menu items" }
178+ , { name: "selectedIndex", type_: "Int", description: "Selected item index" }
179+ , { name: "onChange", type_: "Int -> Effect Unit", description: "Selection callback" }
180+ ]
181+ ]
182+
183+comboBox :: Nut
184+comboBox = L.componentDoc "nativeComboBox" "Yoga.React.Native.MacOS.ComboBox (nativeComboBox)"
185+ """txt /\ setTxt <- useState' ""
186+result /\ setResult <- useState' ""
187+
188+nativeComboBox
189+ { items: [ "Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig", "Grape" ]
190+ , text: txt
191+ , placeholder: "Type a fruit..."
192+ , onChangeText: setTxt
193+ , onSelectItem: \t -> do
194+ setTxt t
195+ setResult ("Selected: " <> t)
196+ }"""
197+ [ propsTable
198+ [ { name: "items", type_: "Array String", description: "Autocomplete suggestions" }
199+ , { name: "text", type_: "String", description: "Current text value" }
200+ , { name: "placeholder", type_: "String", description: "Placeholder text" }
201+ , { name: "onChangeText", type_: "String -> Effect Unit", description: "Text change callback" }
202+ , { name: "onSelectItem", type_: "String -> Effect Unit", description: "Item selection callback" }
203+ ]
204+ ]
205+
206+searchField :: Nut
207+searchField = L.componentDoc "nativeSearchField" "Yoga.React.Native.MacOS.SearchField (nativeSearchField)"
208+ """query /\ setQuery <- useState' ""
209+result /\ setResult <- useState' ""
210+
211+nativeSearchField
212+ { text: query
213+ , placeholder: "Search..."
214+ , onChangeText: setQuery
215+ , onSearch: \t -> setResult ("Searched: " <> t)
216+ }"""
217+ [ propsTable
218+ [ { name: "text", type_: "String", description: "Current search text" }
219+ , { name: "placeholder", type_: "String", description: "Placeholder text" }
220+ , { name: "onChangeText", type_: "String -> Effect Unit", description: "Text change callback" }
221+ , { name: "onSearch", type_: "String -> Effect Unit", description: "Search submit callback" }
222+ ]
223+ ]
224+
225+tokenField :: Nut
226+tokenField = L.componentDoc "nativeTokenField" "Yoga.React.Native.MacOS.TokenField (nativeTokenField)"
227+ """tokens /\ setTokens <- useState' [ "PureScript", "React", "macOS" ]
228+
229+nativeTokenField
230+ { tokens
231+ , placeholder: "Add tags..."
232+ , onChangeTokens: setTokens
233+ }"""
234+ [ propsTable
235+ [ { name: "tokens", type_: "Array String", description: "Current tokens" }
236+ , { name: "placeholder", type_: "String", description: "Placeholder text" }
237+ , { name: "onChangeTokens", type_: "Array String -> Effect Unit", description: "Tokens change callback" }
238+ ]
239+ ]
240+
241+datePicker :: Nut
242+datePicker = L.componentDoc "nativeDatePicker" "Yoga.React.Native.MacOS.DatePicker (nativeDatePicker)"
243+ """dateText /\ setDateText <- useState' ""
244+
245+nativeDatePicker
246+ { graphical: false
247+ , onChange: setDateText
248+ }"""
249+ [ propsTable
250+ [ { name: "graphical", type_: "Boolean", description: "Show graphical calendar picker" }
251+ , { name: "onChange", type_: "String -> Effect Unit", description: "Date change callback" }
252+ ]
253+ ]
254+
255+colorWell :: Nut
256+colorWell = L.componentDoc "nativeColorWell" "Yoga.React.Native.MacOS.ColorWell (nativeColorWell)"
257+ """color /\ setColor <- useState' "#FF6600"
258+
259+nativeColorWell
260+ { color
261+ , minimal: true
262+ , onChange: setColor
263+ }"""
264+ [ propsTable
265+ [ { name: "color", type_: "String", description: "Current color hex value" }
266+ , { name: "minimal", type_: "Boolean", description: "Minimal inline style" }
267+ , { name: "onChange", type_: "String -> Effect Unit", description: "Color change callback" }
268+ ]
269+ ]
270+
271+helpButton :: Nut
272+helpButton = L.componentDoc "nativeHelpButton" "Yoga.React.Native.MacOS.HelpButton (nativeHelpButton)"
273+ """nativeHelpButton
274+ { onPress: showHelp }"""
275+ [ propsTable
276+ [ { name: "onPress", type_: "Effect Unit", description: "Press callback" }
277+ ]
278+ ]
docs/src/Docs/Pages/LayoutPage.pursnew file+166
@@ -0,0 +1,166 @@
1+module Docs.Pages.LayoutPage where
2+
3+import Deku.Core (Nut)
4+import Deku.DOM as D
5+import Deku.DOM.Attributes as DA
6+import Docs.Components.Layout as L
7+import Docs.Components.PropsTable (propsTable)
8+
9+page :: Nut
10+page =
11+ D.div_
12+ [ L.section "Layout" [ D.p_ [ D.text_ "Container and layout components for structuring macOS interfaces." ] ]
13+ , box
14+ , splitView
15+ , tabView
16+ , scrollView
17+ , sidebarLayout
18+ , toolbar
19+ , visualEffect
20+ , patternBackground
21+ ]
22+
23+box :: Nut
24+box = L.componentDoc "nativeBox" "Yoga.React.Native.MacOS.Box (nativeBox)"
25+ """-- FFINativeComponent: takes props then children
26+nativeBox
27+ { boxTitle: "Settings"
28+ , fillColor2: "#1a1a2e"
29+ , radius: 8.0
30+ }
31+ [ child1, child2 ]"""
32+ [ propsTable
33+ [ { name: "boxTitle", type_: "String", description: "Box title" }
34+ , { name: "fillColor2", type_: "String", description: "Fill color hex" }
35+ , { name: "borderColor2", type_: "String", description: "Border color hex" }
36+ , { name: "radius", type_: "Number", description: "Corner radius" }
37+ ]
38+ , D.p [ DA.klass_ "text-sm text-gray-400 mt-2" ]
39+ [ D.text_ "Children are passed as the second argument (IsJSX: JSX, String, or Array JSX)." ]
40+ ]
41+
42+splitView :: Nut
43+splitView = L.componentDoc "nativeSplitView" "Yoga.React.Native.MacOS.SplitView (nativeSplitView)"
44+ """-- FFINativeComponent: takes props then children
45+nativeSplitView
46+ { isVertical: true
47+ , dividerThicknessValue: 1.0
48+ }
49+ [ leftPanel, rightPanel ]"""
50+ [ propsTable
51+ [ { name: "isVertical", type_: "Boolean", description: "Vertical or horizontal split" }
52+ , { name: "dividerThicknessValue", type_: "Number", description: "Divider thickness in points" }
53+ ]
54+ , D.p [ DA.klass_ "text-sm text-gray-400 mt-2" ]
55+ [ D.text_ "Children are the split panes, passed as the second argument." ]
56+ ]
57+
58+tabView :: Nut
59+tabView = L.componentDoc "nativeTabView" "Yoga.React.Native.MacOS.TabView (nativeTabView)"
60+ """selected /\ setSelected <- useState' "general"
61+
62+nativeTabView
63+ { items:
64+ [ { id: "general", label: "General" }
65+ , { id: "advanced", label: "Advanced" }
66+ , { id: "about", label: "About" }
67+ ]
68+ , selectedItem: selected
69+ , onSelectTab: setSelected
70+ }"""
71+ [ propsTable
72+ [ { name: "items", type_: "Array TabItem", description: "Tab items ({ id :: String, label :: String })" }
73+ , { name: "selectedItem", type_: "String", description: "Selected tab id" }
74+ , { name: "onSelectTab", type_: "String -> Effect Unit", description: "Tab selection callback" }
75+ ]
76+ ]
77+
78+scrollView :: Nut
79+scrollView = L.componentDoc "nativeScrollView" "Yoga.React.Native.MacOS.ScrollView (nativeScrollView)"
80+ """-- FFINativeComponent: takes props then children
81+nativeScrollView
82+ { scrollToBottom: 0
83+ , scrollToY: 0.0
84+ }
85+ [ longContent ]"""
86+ [ propsTable
87+ [ { name: "scrollToBottom", type_: "Int", description: "Increment to scroll to bottom" }
88+ , { name: "scrollToY", type_: "Number", description: "Scroll to Y position" }
89+ , { name: "scrollToYTrigger", type_: "Int", description: "Increment to trigger scrollToY" }
90+ ]
91+ , D.p [ DA.klass_ "text-sm text-gray-400 mt-2" ]
92+ [ D.text_ "Children are the scrollable content, passed as the second argument." ]
93+ ]
94+
95+sidebarLayout :: Nut
96+sidebarLayout = L.componentDoc "sidebarLayout" "Yoga.React.Native.MacOS.Sidebar (sidebarLayout)"
97+ """-- Record-based function, not a component
98+sidebarLayout
99+ { sidebar: navigationList
100+ , content: detailView
101+ , sidebarWidth: 250.0
102+ }"""
103+ [ propsTable
104+ [ { name: "sidebar", type_: "JSX", description: "Sidebar panel content" }
105+ , { name: "content", type_: "JSX", description: "Main panel content" }
106+ , { name: "sidebarWidth", type_: "Number", description: "Sidebar width in points" }
107+ ]
108+ , D.p [ DA.klass_ "text-sm text-gray-400 mt-2" ]
109+ [ D.text_ "This is a plain function { sidebar, content, sidebarWidth } -> JSX, not an FFINativeComponent." ]
110+ ]
111+
112+toolbar :: Nut
113+toolbar = L.componentDoc "nativeToolbar" "Yoga.React.Native.MacOS.Toolbar (nativeToolbar)"
114+ """nativeToolbar
115+ { items: []
116+ , selectedItem: ""
117+ , toolbarStyle: T.unified
118+ , windowTitle: "PureScript React Native"
119+ , onSelectItem: \_ -> pure unit
120+ }"""
121+ [ propsTable
122+ [ { name: "items", type_: "Array ToolbarItem", description: "Toolbar items ({ id, label, sfSymbol })" }
123+ , { name: "selectedItem", type_: "String", description: "Selected item id" }
124+ , { name: "toolbarStyle", type_: "ToolbarStyle", description: "Toolbar visual style" }
125+ , { name: "windowTitle", type_: "String", description: "Window title" }
126+ , { name: "onSelectItem", type_: "String -> Effect Unit", description: "Item selection callback" }
127+ ]
128+ ]
129+
130+visualEffect :: Nut
131+visualEffect = L.componentDoc "nativeVisualEffect" "Yoga.React.Native.MacOS.VisualEffect (nativeVisualEffect)"
132+ """-- FFINativeComponent: takes props then children
133+nativeVisualEffect
134+ { materialName: T.sidebar
135+ , blendingModeName: T.behindWindow
136+ , stateName: T.active
137+ }
138+ [ content ]"""
139+ [ propsTable
140+ [ { name: "materialName", type_: "VisualEffectMaterial", description: "Material type (sidebar, headerView, titlebar, hudWindow, menu, popover, sheet, ...)" }
141+ , { name: "blendingModeName", type_: "BlendingMode", description: "behindWindow or withinWindow" }
142+ , { name: "stateName", type_: "VisualEffectState", description: "Visual effect state" }
143+ ]
144+ , D.p [ DA.klass_ "text-sm text-gray-400 mt-2" ]
145+ [ D.text_ "Children receive the vibrancy effect, passed as the second argument." ]
146+ ]
147+
148+patternBackground :: Nut
149+patternBackground = L.componentDoc "nativePatternBackground" "Yoga.React.Native.MacOS.PatternBackground (nativePatternBackground)"
150+ """-- FFINativeComponent: takes props then children
151+nativePatternBackground
152+ { patternColor: "#ffffff"
153+ , background: "#000000"
154+ , patternOpacity: 0.1
155+ , patternScale: 1.0
156+ }
157+ [ content ]"""
158+ [ propsTable
159+ [ { name: "patternColor", type_: "String", description: "Pattern color hex" }
160+ , { name: "background", type_: "String", description: "Background color hex" }
161+ , { name: "patternOpacity", type_: "Number", description: "Pattern opacity (0.0\x20131.0)" }
162+ , { name: "patternScale", type_: "Number", description: "Pattern scale factor" }
163+ ]
164+ , D.p [ DA.klass_ "text-sm text-gray-400 mt-2" ]
165+ [ D.text_ "Children are rendered over the pattern, passed as the second argument." ]
166+ ]
docs/src/Docs/Pages/Overlays.pursnew file+123
@@ -0,0 +1,123 @@
1+module Docs.Pages.Overlays where
2+
3+import Deku.Core (Nut)
4+import Deku.DOM as D
5+import Deku.DOM.Attributes as DA
6+import Docs.Components.Layout as L
7+import Docs.Components.PropsTable (propsTable)
8+
9+page :: Nut
10+page =
11+ D.div_
12+ [ L.section "Overlays" [ D.p_ [ D.text_ "Modal overlays, sheets, popovers, and context menus." ] ]
13+ , alert
14+ , sheet
15+ , popover
16+ , contextMenu
17+ , menu
18+ ]
19+
20+alert :: Nut
21+alert = L.componentDoc "macosAlert" "Yoga.React.Native.MacOS.Alert (macosAlert)"
22+ """macosAlert
23+ { style: T.critical
24+ , title: "Delete file?"
25+ , message: "This cannot be undone."
26+ , buttons: ["Delete", "Cancel"]
27+ }"""
28+ [ propsTable
29+ [ { name: "style", type_: "AlertStyle", description: "informational | warning | critical" }
30+ , { name: "title", type_: "String", description: "Alert title" }
31+ , { name: "message", type_: "String", description: "Alert message body" }
32+ , { name: "buttons", type_: "Array String", description: "Button labels (default: [\"OK\"])" }
33+ ]
34+ ]
35+
36+sheet :: Nut
37+sheet = L.componentDoc "nativeSheet" "Yoga.React.Native.MacOS.Sheet (nativeSheet)"
38+ """visible /\ setVisible <- useState' false
39+
40+nativeButton
41+ { title: "Show Sheet"
42+ , bezelStyle: T.push
43+ , onPress: setVisible true
44+ }
45+nativeSheet
46+ { visible
47+ , onDismiss: setVisible false
48+ }
49+ [ nativeButton
50+ { title: "Dismiss"
51+ , onPress: setVisible false
52+ }
53+ ]"""
54+ [ propsTable
55+ [ { name: "visible", type_: "Boolean", description: "Show/hide the sheet" }
56+ , { name: "onDismiss", type_: "Effect Unit", description: "Dismiss callback" }
57+ ]
58+ , D.p [ DA.klass_ "text-sm text-gray-400 mt-2" ]
59+ [ D.text_ "Sheet content is passed as the second argument (IsJSX)." ]
60+ ]
61+
62+popover :: Nut
63+popover = L.componentDoc "nativePopover" "Yoga.React.Native.MacOS.Popover (nativePopover)"
64+ """visible /\ setVisible <- useState' false
65+
66+nativePopover
67+ { visible
68+ , preferredEdge: T.bottom
69+ , behavior: T.transient
70+ , onClose: setVisible false
71+ }
72+ [ nativeButton
73+ { title: if visible then "Hide" else "Show Popover"
74+ , onPress: setVisible (not visible)
75+ }
76+ ]"""
77+ [ propsTable
78+ [ { name: "visible", type_: "Boolean", description: "Show/hide" }
79+ , { name: "preferredEdge", type_: "PopoverEdge", description: "Preferred edge for popover placement" }
80+ , { name: "behavior", type_: "PopoverBehavior", description: "Popover behavior" }
81+ , { name: "onClose", type_: "Effect Unit", description: "Close callback" }
82+ ]
83+ , D.p [ DA.klass_ "text-sm text-gray-400 mt-2" ]
84+ [ D.text_ "Popover content is passed as the second argument (IsJSX)." ]
85+ ]
86+
87+contextMenu :: Nut
88+contextMenu = L.componentDoc "nativeContextMenu" "Yoga.React.Native.MacOS.ContextMenu (nativeContextMenu)"
89+ """result /\ setResult <- useState' ""
90+
91+nativeContextMenu
92+ { items:
93+ [ { id: "cut", title: "Cut", sfSymbol: "scissors" }
94+ , { id: "copy", title: "Copy", sfSymbol: "doc.on.doc" }
95+ , { id: "paste", title: "Paste", sfSymbol: "doc.on.clipboard" }
96+ , { id: "sep", title: "-", sfSymbol: "" }
97+ , { id: "delete", title: "Delete", sfSymbol: "trash" }
98+ ]
99+ , onSelectItem: setResult
100+ }
101+ [ text_ "Right-click me!" ]"""
102+ [ propsTable
103+ [ { name: "items", type_: "Array ContextMenuItem", description: "Menu items ({ id, title, sfSymbol })" }
104+ , { name: "onSelectItem", type_: "String -> Effect Unit", description: "Item selection callback" }
105+ ]
106+ , D.p [ DA.klass_ "text-sm text-gray-400 mt-2" ]
107+ [ D.text_ "The right-click target element is passed as the second argument (IsJSX)." ]
108+ ]
109+
110+menu :: Nut
111+menu = L.componentDoc "macosShowMenu" "Yoga.React.Native.MacOS.Menu (macosShowMenu)"
112+ """macosShowMenu
113+ { items:
114+ [ { title: "New", id: "new" }
115+ , { title: "Open", id: "open" }
116+ ]
117+ , onSelectItem: \selectedId -> log selectedId
118+ }"""
119+ [ propsTable
120+ [ { name: "items", type_: "Array MenuItem", description: "Menu items ({ title, id })" }
121+ , { name: "onSelectItem", type_: "String -> Effect Unit", description: "Callback receiving the selected item id" }
122+ ]
123+ ]
docs/src/Docs/Pages/Overview.pursnew file+58
@@ -0,0 +1,58 @@
1+module Docs.Pages.Overview where
2+
3+import Prelude
4+
5+import Deku.Core (Nut)
6+import Deku.DOM as D
7+import Deku.DOM.Attributes as DA
8+
9+page :: Nut
10+page =
11+ D.div_
12+ [ hero
13+ , featureGrid
14+ ]
15+
16+hero :: Nut
17+hero =
18+ D.div [ DA.klass_ "mb-12" ]
19+ [ D.h1 [ DA.klass_ "text-4xl font-bold text-gray-100 font-mono mb-3" ]
20+ [ D.text_ "purescript-yoga-react-native" ]
21+ , D.p [ DA.klass_ "text-lg text-gray-400 max-w-xl mb-5" ]
22+ [ D.text_ "55+ native AppKit components for React Native macOS, wrapped in PureScript's type system." ]
23+ , D.div [ DA.klass_ "flex gap-3 flex-wrap" ]
24+ [ badge "PureScript" "0.15+"
25+ , badge "macOS" "14+"
26+ , badge "React Native" "0.81"
27+ ]
28+ ]
29+ where
30+ badge label value =
31+ D.span [ DA.klass_ "bg-gray-800 border border-gray-700 rounded-md px-3 py-1 text-sm text-gray-400" ]
32+ [ D.strong [ DA.klass_ "text-brand" ] [ D.text_ label ]
33+ , D.text_ (" " <> value)
34+ ]
35+
36+featureGrid :: Nut
37+featureGrid =
38+ D.div [ DA.klass_ "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4" ]
39+ [ feature "Input Controls" "13" "Buttons, sliders, steppers, checkboxes, radio buttons, segmented controls, combo boxes, and more."
40+ , feature "Text" "2" "Native text fields and rich text editors with full AppKit integration."
41+ , feature "Display" "7" "Images, animated GIFs, video players, level indicators, progress bars, separators, and path controls."
42+ , feature "Layout" "8" "Split views, tab views, scroll views, sidebars, toolbars, visual effects, and pattern backgrounds."
43+ , feature "Overlays" "5" "Alerts, sheets, popovers, context menus, and programmatic menus."
44+ , feature "Data Views" "2" "Table views and outline views for structured data."
45+ , feature "Files & Drag/Drop" "2" "Native file pickers and drop zones."
46+ , feature "Rich Media" "5" "Maps, PDFs, web views, camera, and Rive animations."
47+ , feature "System Services" "10" "Clipboard, sharing, notifications, sounds, speech synthesis, status bar, Quick Look, color and font panels."
48+ , feature "AI & ML" "5" "OCR, speech recognition, language detection, sentiment analysis, and tokenization."
49+ ]
50+ where
51+ feature title count description =
52+ D.div [ DA.klass_ "bg-gray-900 border border-gray-800 rounded-lg p-5" ]
53+ [ D.div [ DA.klass_ "flex justify-between items-baseline mb-2" ]
54+ [ D.h3 [ DA.klass_ "text-sm font-semibold text-gray-100" ] [ D.text_ title ]
55+ , D.span [ DA.klass_ "bg-brand-dim text-white text-xs font-semibold px-2 py-0.5 rounded-full" ] [ D.text_ count ]
56+ ]
57+ , D.p [ DA.klass_ "text-xs text-gray-500 leading-relaxed" ] [ D.text_ description ]
58+ ]
docs/src/Docs/Pages/QuickStart.pursnew file+72
@@ -0,0 +1,72 @@
1+module Docs.Pages.QuickStart where
2+
3+import Deku.Core (Nut)
4+import Deku.DOM as D
5+import Deku.DOM.Attributes as DA
6+import Docs.Components.CodeBlock (codeBlock)
7+import Docs.Components.Layout as L
8+
9+page :: Nut
10+page =
11+ D.div_
12+ [ L.section "Installation"
13+ [ D.p [ DA.klass_ "text-gray-400 mb-3" ] [ D.text_ "Add to your spago.yaml:" ]
14+ , codeBlock installYaml
15+ ]
16+ , L.section "Basic Usage"
17+ [ D.p [ DA.klass_ "text-gray-400 mb-3" ] [ D.text_ "Import and use components:" ]
18+ , codeBlock basicUsage
19+ ]
20+ , L.section "Running the Example App"
21+ [ D.p [ DA.klass_ "text-gray-400 mb-3" ] [ D.text_ "Clone and build the example macOS app:" ]
22+ , codeBlock exampleSteps
23+ ]
24+ , L.section "Prerequisites"
25+ [ D.ul [ DA.klass_ "space-y-1.5" ]
26+ [ prereq "macOS 14+"
27+ , prereq "Xcode 16+"
28+ , prereq "Node.js 20+ / Bun"
29+ , prereq "PureScript 0.15+ and Spago"
30+ , prereq "CocoaPods"
31+ ]
32+ ]
33+ ]
34+ where
35+ prereq t =
36+ D.li [ DA.klass_ "flex items-center gap-2 text-gray-300" ]
37+ [ D.span [ DA.klass_ "text-brand" ] [ D.text_ "\x2713" ]
38+ , D.text_ t
39+ ]
40+
41+installYaml :: String
42+installYaml =
43+ """extraPackages:
44+ yoga-react-native:
45+ git: https://github.com/rowtype-yoga/purescript-yoga-react-native.git
46+ ref: main
47+ yoga-react:
48+ git: https://github.com/rowtype-yoga/purescript-yoga-react.git
49+ ref: main"""
50+
51+basicUsage :: String
52+basicUsage =
53+ """import Yoga.React.Native (text, tw, view)
54+import Yoga.React.Native.MacOS.Button (nativeButton)
55+import Yoga.React.Native.MacOS.Types as T
56+
57+myButton = nativeButton
58+ { title: "Click me"
59+ , bezelStyle: T.push
60+ , onPress: doSomething
61+ , style: Style.style { height: 24.0 }
62+ }"""
63+
64+exampleSteps :: String
65+exampleSteps =
66+ """git clone https://github.com/rowtype-yoga/purescript-yoga-react-native.git
67+cd purescript-yoga-react-native
68+bun install
69+cd example-macos && bun install && cd ..
70+cd example-macos/macos && bundle exec pod install && cd ../..
71+cd example-macos && bunx spago build && cd ..
72+cd example-macos && npx react-native run-macos"""
docs/src/Docs/Pages/RichMedia.pursnew file+129
@@ -0,0 +1,129 @@
1+module Docs.Pages.RichMedia where
2+
3+import Deku.Core (Nut)
4+import Deku.DOM as D
5+import Deku.DOM.Attributes as DA
6+import Docs.Components.Layout as L
7+import Docs.Components.PropsTable (propsTable)
8+
9+page :: Nut
10+page =
11+ D.div_
12+ [ L.section "Rich Media" [ D.p_ [ D.text_ "Maps, documents, web content, camera, and animations." ] ]
13+ , mapView
14+ , pdfView
15+ , webView
16+ , cameraView
17+ , riveView
18+ ]
19+
20+mapView :: Nut
21+mapView = L.componentDoc "nativeMapView" "Yoga.React.Native.MacOS.MapView (nativeMapView)"
22+ """mapType /\ setMapType <- useState' 0
23+let mt = case mapType of
24+ 1 -> T.satelliteMap
25+ 2 -> T.hybridMap
26+ _ -> T.standardMap
27+
28+nativeMapView
29+ { latitude: 37.7749
30+ , longitude: -122.4194
31+ , latitudeDelta: 0.05
32+ , longitudeDelta: 0.05
33+ , mapType: mt
34+ , showsUserLocation: false
35+ , annotations:
36+ [ { latitude: 37.7749, longitude: -122.4194
37+ , title: "San Francisco", subtitle: "California"
38+ }
39+ , { latitude: 37.8199, longitude: -122.4783
40+ , title: "Golden Gate Bridge", subtitle: ""
41+ }
42+ ]
43+ }"""
44+ [ propsTable
45+ [ { name: "latitude", type_: "Number", description: "Center latitude" }
46+ , { name: "longitude", type_: "Number", description: "Center longitude" }
47+ , { name: "latitudeDelta", type_: "Number", description: "Latitude span" }
48+ , { name: "longitudeDelta", type_: "Number", description: "Longitude span" }
49+ , { name: "mapType", type_: "MapType", description: "Map display type" }
50+ , { name: "showsUserLocation", type_: "Boolean", description: "Show user location dot" }
51+ , { name: "annotations", type_: "Array MapAnnotation", description: "Map pins ({ latitude, longitude, title, subtitle })" }
52+ , { name: "onRegionChange", type_: "MapRegion -> Effect Unit", description: "Region change callback ({ latitude, longitude, latitudeDelta, longitudeDelta })" }
53+ , { name: "onSelectAnnotation", type_: "String -> Effect Unit", description: "Annotation tap callback" }
54+ ]
55+ ]
56+
57+pdfView :: Nut
58+pdfView = L.componentDoc "nativePDFView" "Yoga.React.Native.MacOS.PDFView (nativePDFView)"
59+ """nativePDFView
60+ { source: "https://example.com/document.pdf"
61+ , autoScales: true
62+ , displayMode: T.singlePageContinuous
63+ , onPageChange: setPage
64+ }"""
65+ [ propsTable
66+ [ { name: "source", type_: "String", description: "PDF file path or URL" }
67+ , { name: "autoScales", type_: "Boolean", description: "Auto-fit to view" }
68+ , { name: "displayMode", type_: "PDFDisplayMode", description: "PDF display mode" }
69+ , { name: "onPageChange", type_: "Int -> Effect Unit", description: "Page change callback" }
70+ ]
71+ ]
72+
73+webView :: Nut
74+webView = L.componentDoc "nativeWebView" "Yoga.React.Native.MacOS.WebView (nativeWebView)"
75+ """url /\ setUrl <- useState' "https://pursuit.purescript.org"
76+urlBar /\ setUrlBar <- useState' "https://pursuit.purescript.org"
77+
78+nativeTextField
79+ { text: urlBar
80+ , onChangeText: setUrlBar
81+ , onSubmit: setUrl
82+ }
83+nativeWebView
84+ { url
85+ , onFinishLoad: setUrlBar
86+ }"""
87+ [ propsTable
88+ [ { name: "url", type_: "String", description: "URL to load" }
89+ , { name: "onNavigate", type_: "String -> Effect Unit", description: "Navigation callback" }
90+ , { name: "onFinishLoad", type_: "String -> Effect Unit", description: "Page load complete callback" }
91+ ]
92+ ]
93+
94+cameraView :: Nut
95+cameraView = L.componentDoc "nativeCameraView" "Yoga.React.Native.MacOS.CameraView (nativeCameraView)"
96+ """nativeCameraView
97+ { active: true }"""
98+ [ propsTable
99+ [ { name: "active", type_: "Boolean", description: "Activate/deactivate camera capture" }
100+ ]
101+ ]
102+
103+riveView :: Nut
104+riveView = L.componentDoc "nativeRiveView / nativeRiveView_" "Yoga.React.Native.MacOS.Rive (nativeRiveView, nativeRiveView_)"
105+ """-- nativeRiveView_ (no children)
106+nativeRiveView_
107+ { resourceName: "animation"
108+ , stateMachineName: "State Machine 1"
109+ , fit: T.contain
110+ , autoplay: true
111+ }
112+
113+-- nativeRiveView (with children, FFINativeComponent)
114+nativeRiveView
115+ { url: "https://example.com/animation.riv"
116+ , autoplay: true
117+ }
118+ [ overlayContent ]"""
119+ [ propsTable
120+ [ { name: "resourceName", type_: "String", description: "Bundled .riv resource name" }
121+ , { name: "url", type_: "String", description: "Remote .riv file URL" }
122+ , { name: "artboardName", type_: "String", description: "Artboard name" }
123+ , { name: "stateMachineName", type_: "String", description: "State machine name" }
124+ , { name: "fit", type_: "RiveFit", description: "How the animation fits its bounds" }
125+ , { name: "autoplay", type_: "Boolean", description: "Start playing automatically" }
126+ ]
127+ , D.p [ DA.klass_ "text-sm text-gray-400 mt-2" ]
128+ [ D.text_ "nativeRiveView takes children as second arg; nativeRiveView_ takes props only." ]
129+ ]
docs/src/Docs/Pages/SystemServices.pursnew file+141
@@ -0,0 +1,141 @@
1+module Docs.Pages.SystemServices where
2+
3+import Deku.Core (Nut)
4+import Deku.DOM as D
5+import Deku.DOM.Attributes as DA
6+import Docs.Components.Layout as L
7+import Docs.Components.PropsTable (propsTable)
8+
9+page :: Nut
10+page =
11+ D.div_
12+ [ L.section "System Services" [ D.p_ [ D.text_ "macOS system service APIs for clipboard, sharing, notifications, sound, and more." ] ]
13+ , clipboard
14+ , share
15+ , notification
16+ , sound
17+ , speechSynthesizer
18+ , speechRecognition
19+ , statusBarItem
20+ , quickLook
21+ , colorPanel
22+ , fontPanel
23+ ]
24+
25+clipboard :: Nut
26+clipboard = L.componentDoc "copyToClipboard / readClipboard" "Yoga.React.Native.MacOS.Pasteboard (copyToClipboard, readClipboard)"
27+ """copyToClipboard "Hello, world!" # liftEffect
28+
29+text <- readClipboard # liftEffect"""
30+ [ D.p [ DA.klass_ "text-sm text-gray-400 mt-2" ]
31+ [ D.text_ "copyToClipboard :: String -> Effect Unit" ]
32+ , D.p [ DA.klass_ "text-sm text-gray-400" ]
33+ [ D.text_ "readClipboard :: Effect String" ]
34+ ]
35+
36+share :: Nut
37+share = L.componentDoc "macosShare" "Yoga.React.Native.MacOS.ShareService (macosShare)"
38+ """-- Takes an Array String of items to share
39+macosShare ["Check out this library!", "https://github.com/rowtype-yoga/purescript-yoga-react-native"]
40+ # liftEffect"""
41+ [ D.p [ DA.klass_ "text-sm text-gray-400 mt-2" ]
42+ [ D.text_ "Signature: Array String -> Effect Unit" ]
43+ ]
44+
45+notification :: Nut
46+notification = L.componentDoc "macosNotify" "Yoga.React.Native.MacOS.UserNotification (macosNotify)"
47+ """macosNotify
48+ { title: "Build Complete"
49+ , body: "Your project compiled successfully."
50+ }"""
51+ [ propsTable
52+ [ { name: "title", type_: "String", description: "Notification title" }
53+ , { name: "body", type_: "String", description: "Notification body text" }
54+ ]
55+ ]
56+
57+sound :: Nut
58+sound = L.componentDoc "macosPlaySound / macosBeep" "Yoga.React.Native.MacOS.Sound (macosPlaySound, macosBeep)"
59+ """macosPlaySound "Glass" # liftEffect
60+
61+macosBeep # liftEffect"""
62+ [ D.p [ DA.klass_ "text-sm text-gray-400 mt-2" ]
63+ [ D.text_ "macosPlaySound :: String -> Effect Unit" ]
64+ , D.p [ DA.klass_ "text-sm text-gray-400" ]
65+ [ D.text_ "macosBeep :: Effect Unit" ]
66+ , D.p [ DA.klass_ "text-sm text-gray-400" ]
67+ [ D.text_ "Sound names: Basso, Blow, Bottle, Frog, Glass, Hero, Morse, Ping, Pop, Purr, Sosumi, Submarine, Tink" ]
68+ ]
69+
70+speechSynthesizer :: Nut
71+speechSynthesizer = L.componentDoc "say / sayWithVoice / stopSpeaking" "Yoga.React.Native.MacOS.SpeechSynthesizer (say, sayWithVoice, stopSpeaking)"
72+ """say "Hello from PureScript!" # liftEffect
73+
74+sayWithVoice "Bonjour" T.french # liftEffect
75+
76+stopSpeaking # liftEffect"""
77+ [ D.p [ DA.klass_ "text-sm text-gray-400 mt-2" ]
78+ [ D.text_ "say :: String -> Effect Unit" ]
79+ , D.p [ DA.klass_ "text-sm text-gray-400" ]
80+ [ D.text_ "sayWithVoice :: String -> VoiceIdentifier -> Effect Unit" ]
81+ , D.p [ DA.klass_ "text-sm text-gray-400" ]
82+ [ D.text_ "stopSpeaking :: Effect Unit" ]
83+ ]
84+
85+speechRecognition :: Nut
86+speechRecognition = L.componentDoc "useSpeechRecognition" "Yoga.React.Native.MacOS.SpeechRecognition (useSpeechRecognition)"
87+ """-- React hook returning a record
88+{ listening, transcript, start, stop } <- useSpeechRecognition"""
89+ [ D.p [ DA.klass_ "text-sm text-gray-400 mt-2" ]
90+ [ D.text_ "Hook UseSpeechRecognition SpeechRecognition" ]
91+ , propsTable
92+ [ { name: "listening", type_: "Boolean", description: "Whether recognition is active" }
93+ , { name: "transcript", type_: "String", description: "Live transcription text" }
94+ , { name: "start", type_: "Effect Unit", description: "Start recognition" }
95+ , { name: "stop", type_: "Effect Unit", description: "Stop recognition" }
96+ ]
97+ ]
98+
99+statusBarItem :: Nut
100+statusBarItem = L.componentDoc "macosSetStatusBarItem / macosRemoveStatusBarItem" "Yoga.React.Native.MacOS.StatusBarItem (macosSetStatusBarItem, macosRemoveStatusBarItem)"
101+ """macosSetStatusBarItem
102+ { title: "MyApp"
103+ , sfSymbol: "gear"
104+ , menuItems: [ { id: "prefs", title: "Preferences..." } ]
105+ }
106+ # liftEffect
107+
108+macosRemoveStatusBarItem # liftEffect"""
109+ [ D.p [ DA.klass_ "text-sm text-gray-400 mt-2" ]
110+ [ D.text_ "macosSetStatusBarItem :: StatusBarConfig -> Effect Unit" ]
111+ , propsTable
112+ [ { name: "title", type_: "String", description: "Status bar title text" }
113+ , { name: "sfSymbol", type_: "String", description: "SF Symbol icon name" }
114+ , { name: "menuItems", type_: "Array StatusBarMenuItem", description: "Dropdown items ({ id, title })" }
115+ ]
116+ ]
117+
118+quickLook :: Nut
119+quickLook = L.componentDoc "macosQuickLook" "Yoga.React.Native.MacOS.QuickLook (macosQuickLook)"
120+ """macosQuickLook "/path/to/file.pdf" # liftEffect"""
121+ [ D.p [ DA.klass_ "text-sm text-gray-400 mt-2" ]
122+ [ D.text_ "Signature: String -> Effect Unit" ]
123+ ]
124+
125+colorPanel :: Nut
126+colorPanel = L.componentDoc "macosShowColorPanel / macosHideColorPanel" "Yoga.React.Native.MacOS.ColorPanel (macosShowColorPanel, macosHideColorPanel)"
127+ """macosShowColorPanel # liftEffect
128+
129+macosHideColorPanel # liftEffect"""
130+ [ D.p [ DA.klass_ "text-sm text-gray-400 mt-2" ]
131+ [ D.text_ "Both are Effect Unit \x2014 no arguments." ]
132+ ]
133+
134+fontPanel :: Nut
135+fontPanel = L.componentDoc "macosShowFontPanel / macosHideFontPanel" "Yoga.React.Native.MacOS.FontPanel (macosShowFontPanel, macosHideFontPanel)"
136+ """macosShowFontPanel # liftEffect
137+
138+macosHideFontPanel # liftEffect"""
139+ [ D.p [ DA.klass_ "text-sm text-gray-400 mt-2" ]
140+ [ D.text_ "Both are Effect Unit \x2014 no arguments." ]
141+ ]
docs/src/Docs/Pages/TextEditing.pursnew file+50
@@ -0,0 +1,50 @@
1+module Docs.Pages.TextEditing where
2+
3+import Deku.Core (Nut)
4+import Deku.DOM as D
5+import Docs.Components.Layout as L
6+import Docs.Components.PropsTable (propsTable)
7+
8+page :: Nut
9+page =
10+ D.div_
11+ [ L.section "Text" [ D.p_ [ D.text_ "Native macOS text input components." ] ]
12+ , textField
13+ , textEditor
14+ ]
15+
16+textField :: Nut
17+textField = L.componentDoc "nativeTextField" "Yoga.React.Native.MacOS.TextField (nativeTextField)"
18+ """txt /\ setTxt <- useState' ""
19+
20+nativeTextField
21+ { placeholder: "Type something..."
22+ , search: true
23+ , text: txt
24+ , onChangeText: setTxt
25+ }"""
26+ [ propsTable
27+ [ { name: "text", type_: "String", description: "Current text value" }
28+ , { name: "placeholder", type_: "String", description: "Placeholder text" }
29+ , { name: "secure", type_: "Boolean", description: "Password field (hides input)" }
30+ , { name: "search", type_: "Boolean", description: "Search field style" }
31+ , { name: "rounded", type_: "Boolean", description: "Rounded bezel style" }
32+ , { name: "onChangeText", type_: "String -> Effect Unit", description: "Text change callback" }
33+ , { name: "onSubmit", type_: "String -> Effect Unit", description: "Submit callback (Enter key)" }
34+ ]
35+ ]
36+
37+textEditor :: Nut
38+textEditor = L.componentDoc "nativeTextEditor" "Yoga.React.Native.MacOS.TextEditor (nativeTextEditor)"
39+ """nativeTextEditor
40+ { text: "Welcome to the PureScript-driven native text editor."
41+ , richText: true
42+ , showsRuler: true
43+ }"""
44+ [ propsTable
45+ [ { name: "text", type_: "String", description: "Current text content" }
46+ , { name: "richText", type_: "Boolean", description: "Enable rich text formatting" }
47+ , { name: "showsRuler", type_: "Boolean", description: "Show the formatting ruler" }
48+ , { name: "onChangeText", type_: "String -> Effect Unit", description: "Text change callback" }
49+ ]
50+ ]
docs/src/Docs/Pages/Types.pursnew file+19
@@ -0,0 +1,19 @@
1+module Docs.Pages.Types where
2+
3+import Prelude
4+
5+data Page
6+ = Overview
7+ | QuickStart
8+ | InputControls
9+ | TextEditing
10+ | Display
11+ | LayoutPage
12+ | Overlays
13+ | DataViews
14+ | FilesAndDragDrop
15+ | RichMedia
16+ | SystemServices
17+ | AiMl
18+
19+derive instance Eq Page
docs/vite.config.jsnew file+8
@@ -0,0 +1,8 @@
1+import { defineConfig } from "vite";
2+
3+export default defineConfig({
4+ root: ".",
5+ build: {
6+ outDir: "dist",
7+ },
8+});
example-macos/macos/YogaReactExample-macOS/Info.plist+2
@@ -49,5 +49,7 @@
4949 <string>This app uses the microphone for speech recognition.</string>
5050 <key>NSSpeechRecognitionUsageDescription</key>
5151 <string>This app uses speech recognition to convert speech to text.</string>
52+ <key>NSCameraUsageDescription</key>
53+ <string>This app uses the camera for live video preview.</string>
5254</dict>
5355</plist>
example-macos/macos/YogaReactExample-macOS/RCTMacOSControlsManager.m+154−24
@@ -49,13 +49,28 @@
4949
5050- (void)setTitle:(NSString *)title {
5151 _title = title;
52− _button.title = title ?: @"";
52+ if (title.length > 0) {
53+ _button.image = nil;
54+ _button.title = title;
55+ } else {
56+ _button.title = @"";
57+ }
5358}
5459
5560- (void)setSfSymbol:(NSString *)sfSymbol {
5661 _sfSymbol = sfSymbol;
57− // SF symbols in push buttons aren't idiomatic macOS (HIG: title-only for push buttons).
58− // Kept as no-op for API compatibility; use toolbar-style buttons for icon-only.
62+ if (sfSymbol.length > 0) {
63+ NSImage *img = [NSImage imageWithSystemSymbolName:sfSymbol accessibilityDescription:sfSymbol];
64+ if (img) {
65+ _button.image = img;
66+ _button.title = @"";
67+ _button.imagePosition = NSImageOnly;
68+ }
69+ } else {
70+ _button.image = nil;
71+ _button.title = _title ?: @"";
72+ _button.imagePosition = NSNoImage;
73+ }
5974}
6075
6176- (void)setBezelStyle:(NSString *)bezelStyle {
@@ -815,6 +830,8 @@ RCT_EXPORT_VIEW_PROPERTY(criticalValue, double)
815830@interface RCTNativeScrollView : NSScrollView
816831@property (nonatomic, strong) RCTFlippedDocumentView *docView;
817832@property (nonatomic, assign) NSInteger scrollToBottom;
833+@property (nonatomic, assign) CGFloat scrollToY;
834+@property (nonatomic, assign) NSInteger scrollToYTrigger;
818835@end
819836
820837@implementation RCTNativeScrollView
@@ -883,6 +900,29 @@ RCT_EXPORT_VIEW_PROPERTY(criticalValue, double)
883900 }
884901}
885902
903+- (void)setScrollToYTrigger:(NSInteger)scrollToYTrigger {
904+ if (scrollToYTrigger == _scrollToYTrigger) return;
905+ _scrollToYTrigger = scrollToYTrigger;
906+ [self performSelector:@selector(doScrollToY) withObject:nil afterDelay:0.1];
907+}
908+
909+- (void)setScrollToY:(CGFloat)scrollToY {
910+ _scrollToY = scrollToY;
911+}
912+
913+- (void)doScrollToY {
914+ [self updateDocumentSize];
915+ CGFloat maxScroll = _docView.frame.size.height - self.bounds.size.height;
916+ CGFloat y = MIN(MAX(_scrollToY, 0), maxScroll);
917+ [NSAnimationContext runAnimationGroup:^(NSAnimationContext *ctx) {
918+ ctx.duration = 0.3;
919+ ctx.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
920+ [self.contentView.animator setBoundsOrigin:NSMakePoint(0, y)];
921+ } completionHandler:^{
922+ [self reflectScrolledClipView:self.contentView];
923+ }];
924+}
925+
886926- (void)layout {
887927 [super layout];
888928 [self updateDocumentSize];
@@ -896,6 +936,8 @@ RCT_EXPORT_VIEW_PROPERTY(criticalValue, double)
896936RCT_EXPORT_MODULE(MacOSScrollView)
897937- (NSView *)view { return [[RCTNativeScrollView alloc] initWithFrame:CGRectZero]; }
898938RCT_EXPORT_VIEW_PROPERTY(scrollToBottom, NSInteger)
939+RCT_EXPORT_VIEW_PROPERTY(scrollToY, double)
940+RCT_EXPORT_VIEW_PROPERTY(scrollToYTrigger, NSInteger)
899941@end
900942
901943// ============================================================
@@ -1783,7 +1825,7 @@ RCT_EXPORT_VIEW_PROPERTY(cornerRadius, CGFloat)
17831825
17841826@interface RCTPatternBackgroundView : NSView
17851827@property (nonatomic, copy) NSString *patternColor;
1786−@property (nonatomic, copy) NSString *backgroundColor2;
1828+@property (nonatomic, copy) NSString *background;
17871829@property (nonatomic, assign) CGFloat patternOpacity;
17881830@property (nonatomic, assign) CGFloat patternScale;
17891831@end
@@ -1793,7 +1835,7 @@ RCT_EXPORT_VIEW_PROPERTY(cornerRadius, CGFloat)
17931835- (instancetype)initWithFrame:(NSRect)frame {
17941836 if (self = [super initWithFrame:frame]) {
17951837 _patternColor = @"#FFFFFF";
1796− _backgroundColor2 = @"#17212B";
1838+ _background = @"#17212B";
17971839 _patternOpacity = 0.06;
17981840 _patternScale = 1.0;
17991841 }
@@ -1809,8 +1851,8 @@ RCT_EXPORT_VIEW_PROPERTY(cornerRadius, CGFloat)
18091851 [self setNeedsDisplay:YES];
18101852}
18111853
1812−- (void)setBackgroundColor2:(NSString *)backgroundColor2 {
1813− _backgroundColor2 = backgroundColor2;
1854+- (void)setBackground:(NSString *)background {
1855+ _background = background;
18141856 [self setNeedsDisplay:YES];
18151857}
18161858
@@ -1841,7 +1883,7 @@ RCT_EXPORT_VIEW_PROPERTY(cornerRadius, CGFloat)
18411883 NSRect clip = NSIntersectionRect(dirtyRect, bounds);
18421884 if (NSIsEmptyRect(clip)) return;
18431885
1844− NSColor *bgColor = [self colorFromHex:_backgroundColor2];
1886+ NSColor *bgColor = [self colorFromHex:_background];
18451887 [bgColor setFill];
18461888 NSRectFill(clip);
18471889
@@ -1902,7 +1944,7 @@ RCT_EXPORT_VIEW_PROPERTY(cornerRadius, CGFloat)
19021944RCT_EXPORT_MODULE(MacOSPatternBackground)
19031945- (NSView *)view { return [[RCTPatternBackgroundView alloc] initWithFrame:CGRectZero]; }
19041946RCT_EXPORT_VIEW_PROPERTY(patternColor, NSString)
1905−RCT_EXPORT_VIEW_PROPERTY(backgroundColor2, NSString)
1947+RCT_EXPORT_VIEW_PROPERTY(background, NSString)
19061948RCT_EXPORT_VIEW_PROPERTY(patternOpacity, CGFloat)
19071949RCT_EXPORT_VIEW_PROPERTY(patternScale, CGFloat)
19081950@end
@@ -2280,9 +2322,9 @@ RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock)
22802322
22812323@interface RCTBoxView : NSBox
22822324@property (nonatomic, copy) NSString *boxTitle;
2283−@property (nonatomic, copy) NSString *fillColorStr;
2284−@property (nonatomic, copy) NSString *borderColorStr;
2285−@property (nonatomic, assign) CGFloat cornerRadiusValue;
2325+@property (nonatomic, copy) NSString *fillColor2;
2326+@property (nonatomic, copy) NSString *borderColor2;
2327+@property (nonatomic, assign) CGFloat radius;
22862328@end
22872329
22882330@implementation RCTBoxView
@@ -2309,19 +2351,19 @@ RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock)
23092351 }
23102352}
23112353
2312−- (void)setFillColorStr:(NSString *)fillColorStr {
2313− _fillColorStr = fillColorStr;
2314− self.fillColor = [RCTBoxView colorFromHex:fillColorStr];
2354+- (void)setFillColor2:(NSString *)fillColor2 {
2355+ _fillColor2 = fillColor2;
2356+ self.fillColor = [RCTBoxView colorFromHex:fillColor2];
23152357}
23162358
2317−- (void)setBorderColorStr:(NSString *)borderColorStr {
2318− _borderColorStr = borderColorStr;
2319− self.borderColor = [RCTBoxView colorFromHex:borderColorStr];
2359+- (void)setBorderColor2:(NSString *)borderColor2 {
2360+ _borderColor2 = borderColor2;
2361+ self.borderColor = [RCTBoxView colorFromHex:borderColor2];
23202362}
23212363
2322−- (void)setCornerRadiusValue:(CGFloat)cornerRadiusValue {
2323− _cornerRadiusValue = cornerRadiusValue;
2324− self.cornerRadius = cornerRadiusValue;
2364+- (void)setRadius:(CGFloat)radius {
2365+ _radius = radius;
2366+ self.cornerRadius = radius;
23252367}
23262368
23272369+ (NSColor *)colorFromHex:(NSString *)hex {
@@ -2364,9 +2406,9 @@ RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock)
23642406RCT_EXPORT_MODULE(MacOSBox)
23652407- (NSView *)view { return [[RCTBoxView alloc] initWithFrame:CGRectZero]; }
23662408RCT_EXPORT_VIEW_PROPERTY(boxTitle, NSString)
2367−RCT_EXPORT_VIEW_PROPERTY(fillColorStr, NSString)
2368−RCT_EXPORT_VIEW_PROPERTY(borderColorStr, NSString)
2369−RCT_EXPORT_VIEW_PROPERTY(cornerRadiusValue, CGFloat)
2409+RCT_EXPORT_VIEW_PROPERTY(fillColor2, NSString)
2410+RCT_EXPORT_VIEW_PROPERTY(borderColor2, NSString)
2411+RCT_EXPORT_VIEW_PROPERTY(radius, CGFloat)
23702412@end
23712413
23722414// ===========================================================================
@@ -4201,3 +4243,91 @@ RCT_EXPORT_METHOD(tokenize:(NSString *)text
42014243}
42024244
42034245@end
4246+
4247+// ── Section 54: Camera View ─────────────────────────────────────────────────
4248+// Live camera preview using AVCaptureVideoPreviewLayer
4249+
4250+@interface RCTCameraView : NSView
4251+@property (nonatomic, assign) BOOL active;
4252+@property (nonatomic, strong) AVCaptureSession *session;
4253+@property (nonatomic, strong) AVCaptureVideoPreviewLayer *previewLayer;
4254+@end
4255+
4256+@implementation RCTCameraView
4257+
4258+- (instancetype)initWithFrame:(NSRect)frame {
4259+ if (self = [super initWithFrame:frame]) {
4260+ self.wantsLayer = YES;
4261+ _active = NO;
4262+ }
4263+ return self;
4264+}
4265+
4266+- (NSSize)intrinsicContentSize {
4267+ return NSMakeSize(NSViewNoIntrinsicMetric, NSViewNoIntrinsicMetric);
4268+}
4269+
4270+- (void)setActive:(BOOL)active {
4271+ _active = active;
4272+ if (active) {
4273+ [self startCapture];
4274+ } else {
4275+ [self stopCapture];
4276+ }
4277+}
4278+
4279+- (void)startCapture {
4280+ if (_session) return;
4281+
4282+ AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
4283+ if (!device) {
4284+ NSLog(@"[CameraView] No video device found");
4285+ return;
4286+ }
4287+
4288+ NSError *error = nil;
4289+ AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error];
4290+ if (error) {
4291+ NSLog(@"[CameraView] Input error: %@", error);
4292+ return;
4293+ }
4294+
4295+ _session = [[AVCaptureSession alloc] init];
4296+ _session.sessionPreset = AVCaptureSessionPresetMedium;
4297+ [_session addInput:input];
4298+
4299+ _previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:_session];
4300+ _previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
4301+ _previewLayer.frame = self.bounds;
4302+ [self.layer addSublayer:_previewLayer];
4303+
4304+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
4305+ [self->_session startRunning];
4306+ });
4307+}
4308+
4309+- (void)stopCapture {
4310+ if (!_session) return;
4311+ [_session stopRunning];
4312+ [_previewLayer removeFromSuperlayer];
4313+ _previewLayer = nil;
4314+ _session = nil;
4315+}
4316+
4317+- (void)layout {
4318+ [super layout];
4319+ _previewLayer.frame = self.bounds;
4320+}
4321+
4322+- (void)dealloc {
4323+ [self stopCapture];
4324+}
4325+
4326+@end
4327+
4328+@interface RCTCameraViewManager : RCTViewManager @end
4329+@implementation RCTCameraViewManager
4330+RCT_EXPORT_MODULE(MacOSCameraView)
4331+- (NSView *)view { return [[RCTCameraView alloc] initWithFrame:CGRectZero]; }
4332+RCT_EXPORT_VIEW_PROPERTY(active, BOOL)
4333+@end
example-macos/macos/YogaReactExample-macOS/YogaReactExample.entitlements+2
@@ -10,5 +10,7 @@
1010 <true/>
1111 <key>com.apple.security.device.audio-input</key>
1212 <true/>
13+ <key>com.apple.security.device.camera</key>
14+ <true/>
1315</dict>
1416</plist>
example-macos/src/Demo/AiMl.pursnew file+147
@@ -0,0 +1,147 @@
1+module Demo.AiMl
2+ ( ocrDemo
3+ , speechRecognitionDemo
4+ , naturalLanguageDemo
5+ , cameraDemo
6+ ) where
7+
8+import Prelude
9+
10+import Data.Foldable (for_)
11+import Data.String (joinWith)
12+import Demo.Shared (DemoProps, desc, scrollWrap, sectionTitle)
13+import Effect.Aff (launchAff_)
14+import Effect.Class (liftEffect)
15+import React.Basic (JSX)
16+import React.Basic.Hooks (useState', (/\))
17+import React.Basic.Hooks as React
18+import Yoga.React (component)
19+import Yoga.React.Native (text, tw, view)
20+import Yoga.React.Native.MacOS.Button (nativeButton)
21+import Yoga.React.Native.MacOS.CameraView (nativeCameraView)
22+import Yoga.React.Native.MacOS.FilePicker (nativeFilePicker)
23+import Yoga.React.Native.MacOS.NaturalLanguage (analyzeSentiment, detectLanguage, tokenize)
24+import Yoga.React.Native.MacOS.OCR (recognizeText)
25+import Yoga.React.Native.MacOS.SpeechRecognition (useSpeechRecognition)
26+import Yoga.React.Native.MacOS.TextField (nativeTextField)
27+import Yoga.React.Native.MacOS.Types as T
28+import Yoga.React.Native.Style as Style
29+
30+ocrDemo :: DemoProps -> JSX
31+ocrDemo = component "OCRDemo" \dp -> React.do
32+ result /\ setResult <- useState' ""
33+ pure do
34+ scrollWrap dp
35+ [ sectionTitle dp.fg "OCR (Vision)"
36+ , desc dp "Recognize text in images (VNRecognizeTextRequest)"
37+ , view { style: tw "flex-row items-center mb-2" }
38+ [ nativeFilePicker
39+ { mode: T.openFile
40+ , allowedTypes: [ "public.image" ]
41+ , title: "Pick Image"
42+ , sfSymbol: "photo"
43+ , onPickFiles: \files -> for_ files \file -> do
44+ setResult "Recognizing..."
45+ launchAff_ do
46+ r <- recognizeText file.path
47+ liftEffect (setResult r)
48+ , style: Style.style { height: 24.0, width: 140.0 }
49+ }
50+ ]
51+ , if result /= "" then view { style: tw "p-2 rounded mb-4" <> Style.style { backgroundColor: dp.cardBg } }
52+ [ text { style: tw "text-xs font-mono" <> Style.style { color: dp.fg } } result ]
53+ else mempty
54+ ]
55+
56+speechRecognitionDemo :: DemoProps -> JSX
57+speechRecognitionDemo = component "SpeechRecognitionDemo" \dp -> React.do
58+ speech <- useSpeechRecognition
59+ pure do
60+ scrollWrap dp
61+ [ sectionTitle dp.fg "Speech Recognition"
62+ , desc dp "Live microphone-to-text (click Start, speak, click Stop)"
63+ , view { style: tw "flex-row items-center mb-2" }
64+ [ nativeButton
65+ { title: if speech.listening then "Listening..." else "Start"
66+ , bezelStyle: T.push
67+ , onPress: speech.start
68+ , style: Style.style { height: 24.0, width: 130.0 }
69+ }
70+ , nativeButton
71+ { title: "Stop"
72+ , bezelStyle: T.push
73+ , onPress: speech.stop
74+ , style: Style.style { height: 24.0, width: 80.0, marginLeft: 8.0 }
75+ }
76+ ]
77+ , if speech.transcript /= "" then view { style: tw "p-2 rounded mb-4" <> Style.style { backgroundColor: dp.cardBg } }
78+ [ text { style: tw "text-xs font-mono" <> Style.style { color: dp.fg } } speech.transcript ]
79+ else mempty
80+ ]
81+
82+naturalLanguageDemo :: DemoProps -> JSX
83+naturalLanguageDemo = component "NaturalLanguageDemo" \dp -> React.do
84+ txt /\ setTxt <- useState' "I love this amazing app! C'est magnifique."
85+ result /\ setResult <- useState' ""
86+ pure do
87+ scrollWrap dp
88+ [ sectionTitle dp.fg "Natural Language"
89+ , desc dp "Language detection, sentiment, tokenization (NaturalLanguage.framework)"
90+ , nativeTextField
91+ { text: txt
92+ , placeholder: "Enter text to analyze..."
93+ , onChangeText: setTxt
94+ , style: Style.style { height: 24.0 } <> tw "mb-2"
95+ }
96+ , view { style: tw "flex-row items-center mb-2" }
97+ [ nativeButton
98+ { title: "Language"
99+ , bezelStyle: T.push
100+ , onPress: launchAff_ do
101+ lang <- detectLanguage txt
102+ liftEffect (setResult ("Language: " <> lang))
103+ , style: Style.style { height: 24.0, width: 110.0 }
104+ }
105+ , nativeButton
106+ { title: "Sentiment"
107+ , bezelStyle: T.push
108+ , onPress: launchAff_ do
109+ score <- analyzeSentiment txt
110+ liftEffect (setResult ("Sentiment: " <> show score))
111+ , style: Style.style { height: 24.0, width: 110.0, marginLeft: 8.0 }
112+ }
113+ , nativeButton
114+ { title: "Tokenize"
115+ , bezelStyle: T.push
116+ , onPress: launchAff_ do
117+ tokens <- tokenize txt
118+ liftEffect (setResult ("Tokens: " <> joinWith ", " tokens))
119+ , style: Style.style { height: 24.0, width: 110.0, marginLeft: 8.0 }
120+ }
121+ ]
122+ , if result /= "" then view { style: tw "p-2 rounded mb-4" <> Style.style { backgroundColor: dp.cardBg } }
123+ [ text { style: tw "text-xs font-mono" <> Style.style { color: dp.fg } } result ]
124+ else mempty
125+ ]
126+
127+cameraDemo :: DemoProps -> JSX
128+cameraDemo = component "CameraDemo" \dp -> React.do
129+ on /\ setOn <- useState' false
130+ pure do
131+ view { style: tw "flex-1" }
132+ [ view { style: tw "flex-row items-center px-3 py-2" <> Style.style { backgroundColor: "transparent" } }
133+ [ text { style: tw "text-sm font-semibold mr-3" <> Style.style { color: dp.fg } } "Camera"
134+ , nativeButton
135+ { title: if on then "Stop" else "Start"
136+ , bezelStyle: T.push
137+ , onPress: setOn (not on)
138+ , style: Style.style { height: 24.0, width: 100.0 }
139+ }
140+ ]
141+ , if on then nativeCameraView
142+ { active: true
143+ , style: tw "flex-1"
144+ }
145+ else view { style: tw "flex-1 items-center justify-center" }
146+ [ text { style: tw "text-sm" <> Style.style { color: dp.dimFg } } "Press Start to activate camera" ]
147+ ]
example-macos/src/Demo/Animation.pursnew file+66
@@ -0,0 +1,66 @@
1+module Demo.Animation (riveDemo) where
2+
3+import Prelude
4+
5+import Demo.Shared (DemoProps, sectionTitle)
6+import React.Basic (JSX)
7+import Yoga.React (component)
8+import Yoga.React.Native (text, tw, view)
9+import Yoga.React.Native.MacOS.Rive (nativeRiveView_)
10+import Yoga.React.Native.MacOS.ScrollView (nativeScrollView)
11+import Yoga.React.Native.MacOS.Types as T
12+import Yoga.React.Native.Style as Style
13+
14+riveDemo :: DemoProps -> JSX
15+riveDemo = component "RiveDemo" \dp -> pure do
16+ nativeScrollView { style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
17+ ( view { style: tw "px-4 pb-4" }
18+ [ sectionTitle dp.fg "Mouse Tracking"
19+ , text { style: tw "text-xs mb-2" <> Style.style { color: dp.fg } }
20+ "Move your cursor over the cat — it follows your mouse!"
21+ , nativeRiveView_
22+ { resourceName: "cat_following_mouse"
23+ , stateMachineName: "State Machine 1"
24+ , fit: T.contain
25+ , autoplay: true
26+ , style: Style.style { height: 300.0 }
27+ }
28+ , sectionTitle dp.fg "Cursor Tracking"
29+ , text { style: tw "text-xs mb-2" <> Style.style { color: dp.fg } }
30+ "Shapes follow your cursor via Pointer Move listeners"
31+ , nativeRiveView_
32+ { resourceName: "cursor_tracking"
33+ , stateMachineName: "State Machine 1"
34+ , fit: T.contain
35+ , autoplay: true
36+ , style: Style.style { height: 300.0 }
37+ }
38+ , sectionTitle dp.fg "Interactive Rive Animations"
39+ , text { style: tw "text-xs mb-2" <> Style.style { color: dp.fg } }
40+ "Click the stars to rate! State machine handles interaction."
41+ , nativeRiveView_
42+ { resourceName: "rating_animation"
43+ , stateMachineName: "State Machine 1"
44+ , fit: T.contain
45+ , autoplay: true
46+ , style: Style.style { height: 200.0 }
47+ }
48+ , sectionTitle dp.fg "Light Switch"
49+ , text { style: tw "text-xs mb-2" <> Style.style { color: dp.fg } }
50+ "Click to toggle"
51+ , nativeRiveView_
52+ { resourceName: "switch_event_example"
53+ , stateMachineName: "State Machine 1"
54+ , fit: T.contain
55+ , autoplay: true
56+ , style: Style.style { height: 200.0 }
57+ }
58+ , sectionTitle dp.fg "Windy Tree"
59+ , nativeRiveView_
60+ { resourceName: "windy_tree"
61+ , fit: T.cover
62+ , autoplay: true
63+ , style: tw "flex-1" <> Style.style { minHeight: 300.0, backgroundColor: dp.bg }
64+ }
65+ ]
66+ )
example-macos/src/Demo/Chat.jsnew file+141
@@ -0,0 +1,141 @@
1+export const isSingleEmoji = (str) => {
2+ var trimmed = str.trim();
3+ var re =
4+ /^(?:\p{Emoji_Presentation}|\p{Emoji}\uFE0F)(?:\u200D(?:\p{Emoji_Presentation}|\p{Emoji}\uFE0F))*$/u;
5+ return trimmed.length <= 10 && re.test(trimmed);
6+};
7+export const charCodes = (str) => Array.from(str).map((c) => c.charCodeAt(0));
8+export const abs = (n) => Math.abs(n);
9+
10+var emojiMap = {
11+ "+1": "👍",
12+ "-1": "👎",
13+ heart: "❤️",
14+ broken_heart: "💔",
15+ fire: "🔥",
16+ rocket: "🚀",
17+ star: "⭐",
18+ sparkles: "✨",
19+ tada: "🎉",
20+ party_popper: "🎉",
21+ smile: "😄",
22+ grin: "😁",
23+ laugh: "😂",
24+ joy: "😂",
25+ wink: "😉",
26+ blush: "😊",
27+ heart_eyes: "😍",
28+ kissing_heart: "😘",
29+ thinking: "🤔",
30+ shush: "🤫",
31+ rolling_eyes: "🙄",
32+ grimace: "😬",
33+ sob: "😭",
34+ angry: "😠",
35+ skull: "💀",
36+ clown: "🤡",
37+ ghost: "👻",
38+ alien: "👽",
39+ robot: "🤖",
40+ poop: "💩",
41+ wave: "👋",
42+ clap: "👏",
43+ pray: "🙏",
44+ muscle: "💪",
45+ eyes: "👀",
46+ brain: "🧠",
47+ tongue: "👅",
48+ sun: "☀️",
49+ moon: "🌙",
50+ cloud: "☁️",
51+ rainbow: "🌈",
52+ snowflake: "❄️",
53+ zap: "⚡",
54+ cyclone: "🌀",
55+ umbrella: "☂️",
56+ dog: "🐶",
57+ cat: "🐱",
58+ unicorn: "🦄",
59+ bee: "🐝",
60+ butterfly: "🦋",
61+ crab: "🦀",
62+ octopus: "🐙",
63+ snake: "🐍",
64+ turtle: "🐢",
65+ apple: "🍎",
66+ pizza: "🍕",
67+ taco: "🌮",
68+ coffee: "☕",
69+ beer: "🍺",
70+ wine: "🍷",
71+ cake: "🎂",
72+ cookie: "🍪",
73+ icecream: "🍦",
74+ soccer: "⚽",
75+ basketball: "🏀",
76+ football: "🏈",
77+ tennis: "🎾",
78+ check: "✅",
79+ x: "❌",
80+ warning: "⚠️",
81+ question: "❓",
82+ exclamation: "❗",
83+ bulb: "💡",
84+ pin: "📌",
85+ link: "🔗",
86+ lock: "🔒",
87+ key: "🔑",
88+ hammer: "🔨",
89+ wrench: "🔧",
90+ gear: "⚙️",
91+ gem: "💎",
92+ trophy: "🏆",
93+ gift: "🎁",
94+ balloon: "🎈",
95+ confetti: "🎊",
96+ music: "🎵",
97+ mic: "🎤",
98+ art: "🎨",
99+ film: "🎬",
100+ camera: "📷",
101+ phone: "📱",
102+ laptop: "💻",
103+ mail: "📧",
104+ memo: "📝",
105+ book: "📖",
106+ clock: "🕐",
107+ hourglass: "⏳",
108+ 100: "💯",
109+ zzz: "💤",
110+ boom: "💥",
111+ sweat_drops: "💦",
112+ dash: "💨",
113+ point_up: "☝️",
114+ point_down: "👇",
115+ point_left: "👈",
116+ point_right: "👉",
117+ ok_hand: "👌",
118+ peace: "✌️",
119+ crossed_fingers: "🤞",
120+ metal: "🤘",
121+ thumbsup: "👍",
122+ thumbsdown: "👎",
123+ raised_hands: "🙌",
124+ handshake: "🤝",
125+ hugs: "🤗",
126+ salute: "🫡",
127+ shrug: "🤷",
128+ purescript: "⊳",
129+ haskell: "λ",
130+ rust: "🦀",
131+ js: "📜",
132+ ts: "🔷",
133+};
134+export const replaceEmoji = (str) =>
135+ str.replace(
136+ /:([a-z0-9_]+):/gi,
137+ (m, code) => emojiMap[code.toLowerCase()] || m
138+ );
139+export const setTimeout_ = (ms, fn) => {
140+ setTimeout(fn, ms);
141+};
example-macos/src/Demo/Chat.pursnew file+375
@@ -0,0 +1,375 @@
1+module Demo.Chat (chatDemo) where
2+
3+import Prelude
4+
5+import Data.Array (mapWithIndex, length, filter, snoc, (!!), modifyAt)
6+import Data.Foldable (foldl)
7+import Data.Int (toNumber)
8+import Data.Maybe (Maybe(..), fromMaybe)
9+import Data.String (take)
10+import Data.String as String
11+import Demo.Shared (DemoProps)
12+import Effect (Effect)
13+import Effect.Uncurried (EffectFn2, runEffectFn2)
14+import React.Basic (JSX)
15+import React.Basic.Events (handler_)
16+import React.Basic.Hooks (useState, useState', (/\))
17+import React.Basic.Hooks as React
18+import Yoga.React (component)
19+import Yoga.React.Native (text, tw, view)
20+import Yoga.React.Native.MacOS.Button (nativeButton)
21+import Yoga.React.Native.MacOS.PatternBackground (nativePatternBackground)
22+import Yoga.React.Native.MacOS.Popover (nativePopover)
23+import Yoga.React.Native.MacOS.ScrollView (nativeScrollView)
24+import Yoga.React.Native.MacOS.Sidebar (sidebarLayout)
25+import Yoga.React.Native.MacOS.TextField (nativeTextField)
26+import Yoga.React.Native.MacOS.Types as T
27+import Yoga.React.Native.Style as Style
28+
29+type Reaction = { emoji :: String, count :: Int }
30+
31+type Message =
32+ { sender :: String
33+ , body :: String
34+ , isMine :: Boolean
35+ , reactions :: Array Reaction
36+ , replyTo :: Maybe Int
37+ }
38+
39+type Room =
40+ { roomId :: String
41+ , name :: String
42+ , lastMessage :: String
43+ }
44+
45+reactionEmoji :: Array String
46+reactionEmoji = [ "👍", "❤️", "😂", "😮", "😢", "🔥" ]
47+
48+avatarColors :: Array String
49+avatarColors = [ "#5856D6", "#FF9500", "#34C759", "#FF2D55", "#5AC8FA", "#FF6B6B", "#4ECDC4", "#45B7D1" ]
50+
51+avatarColor :: String -> String
52+avatarColor roomId = fromMaybe "#5856D6" (avatarColors !! (mod (hashCode roomId) (length avatarColors)))
53+ where
54+ hashCode s = abs (foldl (\acc c -> acc * 31 + c) 0 (charCodes s))
55+
56+foreign import charCodes :: String -> Array Int
57+foreign import abs :: Int -> Int
58+foreign import isSingleEmoji :: String -> Boolean
59+foreign import replaceEmoji :: String -> String
60+foreign import setTimeout_ :: EffectFn2 Int (Effect Unit) Unit
61+
62+addReaction :: String -> Array Reaction -> Array Reaction
63+addReaction emoji rs = case filter (\r -> r.emoji == emoji) rs of
64+ [] -> snoc rs { emoji, count: 1 }
65+ _ -> map (\r -> if r.emoji == emoji then r { count = r.count + 1 } else r) rs
66+
67+mockRooms :: Array Room
68+mockRooms =
69+ [ { roomId: "general", name: "General", lastMessage: "Welcome!" }
70+ , { roomId: "purescript", name: "PureScript", lastMessage: "Types are great" }
71+ , { roomId: "react-native", name: "React Native", lastMessage: "Check out macOS support" }
72+ , { roomId: "random", name: "Random", lastMessage: "Anyone here?" }
73+ ]
74+
75+mockMessages :: String -> Array Message
76+mockMessages room = map (\m -> m { body = replaceEmoji m.body }) (mockMessagesRaw room)
77+
78+noReactions :: Array Reaction
79+noReactions = []
80+
81+mockMessagesRaw :: String -> Array Message
82+mockMessagesRaw = case _ of
83+ "general" ->
84+ [ { sender: "Alice", body: "Hey everyone! Welcome to the chat demo :wave:", isMine: false, reactions: [ { emoji: "👋", count: 3 } ], replyTo: Nothing }
85+ , { sender: "Bob", body: "This is built with PureScript + React Native macOS :purescript:", isMine: false, reactions: [ { emoji: "🔥", count: 2 }, { emoji: "❤️", count: 1 } ], replyTo: Nothing }
86+ , { sender: "You", body: "That's awesome! :fire:", isMine: true, reactions: noReactions, replyTo: Just 1 }
87+ , { sender: "Alice", body: "Try typing :rocket: or :heart: in a message", isMine: false, reactions: noReactions, replyTo: Nothing }
88+ , { sender: "You", body: "The bubbles look great :sparkles:", isMine: true, reactions: [ { emoji: "👍", count: 2 } ], replyTo: Nothing }
89+ , { sender: "Bob", body: "Click the smiley to react!", isMine: false, reactions: noReactions, replyTo: Just 4 }
90+ , { sender: "Alice", body: ":tada:", isMine: false, reactions: [ { emoji: "🎉", count: 5 } ], replyTo: Nothing }
91+ ]
92+ "purescript" ->
93+ [ { sender: "Phil", body: "Has anyone tried the new compiler release?", isMine: false, reactions: [ { emoji: "👍", count: 4 } ], replyTo: Nothing }
94+ , { sender: "You", body: "Yes! The build times are much better", isMine: true, reactions: noReactions, replyTo: Just 0 }
95+ , { sender: "Phil", body: "Row polymorphism makes FFI so clean", isMine: false, reactions: [ { emoji: "❤️", count: 3 } ], replyTo: Nothing }
96+ , { sender: "Jordan", body: "The ecosystem keeps getting better", isMine: false, reactions: noReactions, replyTo: Nothing }
97+ , { sender: "You", body: "Agreed, especially for React Native", isMine: true, reactions: [ { emoji: "🔥", count: 1 } ], replyTo: Just 2 }
98+ ]
99+ "react-native" ->
100+ [ { sender: "Christoph", body: "macOS support is looking solid", isMine: false, reactions: [ { emoji: "🔥", count: 3 } ], replyTo: Nothing }
101+ , { sender: "You", body: "Native controls feel right at home", isMine: true, reactions: noReactions, replyTo: Just 0 }
102+ , { sender: "Christoph", body: "NSOutlineView, NSSplitView, toolbars...", isMine: false, reactions: noReactions, replyTo: Nothing }
103+ , { sender: "You", body: "Even the vibrancy effects work", isMine: true, reactions: [ { emoji: "😮", count: 2 } ], replyTo: Nothing }
104+ , { sender: "Christoph", body: "🚀", isMine: false, reactions: [ { emoji: "🚀", count: 7 } ], replyTo: Nothing }
105+ ]
106+ "random" ->
107+ [ { sender: "Eve", body: "Anyone here?", isMine: false, reactions: noReactions, replyTo: Nothing }
108+ , { sender: "You", body: "👋", isMine: true, reactions: [ { emoji: "👋", count: 1 } ], replyTo: Just 0 }
109+ ]
110+ _ -> []
111+
112+truncate :: Int -> String -> String
113+truncate n s = if String.length s <= n then s else take n s <> "…"
114+
115+estimatedY :: Int -> Number
116+estimatedY idx = 8.0 + 70.0 * toNumber idx
117+
118+chatDemo :: DemoProps -> JSX
119+chatDemo = component "ChatDemo" \dp -> React.do
120+ activeRoom /\ setActiveRoom <- useState' (Just "general")
121+ messages /\ setMessages <- useState (mockMessages "general")
122+ inputText /\ setInputText <- useState' ""
123+ reactPopover /\ setReactPopover <- useState' (Nothing :: Maybe Int)
124+ replyingTo /\ setReplyingTo <- useState' (Nothing :: Maybe Int)
125+ highlightIdx /\ setHighlightIdx <- useState' (Nothing :: Maybe Int)
126+ scrollBottomTrigger /\ setScrollBottomTrigger <- useState 1
127+ scrollY /\ setScrollY <- useState' 0.0
128+ scrollTrigger /\ setScrollTrigger <- useState 0
129+ let
130+ selectRoom rid = do
131+ setActiveRoom (Just rid)
132+ setMessages (const (mockMessages rid))
133+ setReactPopover Nothing
134+ setReplyingTo Nothing
135+ setScrollBottomTrigger (_ + 1)
136+
137+ sendMessage _rid msg = do
138+ if msg == "" then pure unit
139+ else do
140+ let newMsg = { sender: "You", body: replaceEmoji msg, isMine: true, reactions: noReactions, replyTo: replyingTo }
141+ setInputText ""
142+ setReplyingTo Nothing
143+ setMessages \msgs -> snoc msgs newMsg
144+ setScrollBottomTrigger (_ + 1)
145+
146+ reactToMessage idx emoji = do
147+ setMessages \msgs -> fromMaybe msgs (modifyAt idx (\m -> m { reactions = addReaction emoji m.reactions }) msgs)
148+ setReactPopover Nothing
149+
150+ scrollToMessage idx = do
151+ setScrollY (estimatedY idx)
152+ setScrollTrigger (_ + 1)
153+ setHighlightIdx (Just idx)
154+ runEffectFn2 setTimeout_ 1500 (setHighlightIdx Nothing)
155+
156+ sentBubbleBg = if dp.isDark then "#65B86A" else "#5CB85C"
157+ receivedBubbleBg = if dp.isDark then "#3B3B3D" else "#FFFFFF"
158+ reactionBg = if dp.isDark then "#3B3B3D" else "#F0F0F0"
159+ replyQuoteBg = if dp.isDark then "#2A2A2C" else "#F0EDE8"
160+ replyBarColor = if dp.isDark then "#65B86A" else "#5CB85C"
161+ highlightBg = if dp.isDark then "#3A3A1E" else "#FFF9C4"
162+ chatBg = if dp.isDark then "#17212B" else "#E8DFD3"
163+
164+ roomSidebar = view { style: tw "pt-2" }
165+ ( mockRooms <#> \r ->
166+ view
167+ { style: tw "flex-row items-center px-3 py-2 mx-2 rounded-lg"
168+ <> Style.style
169+ { backgroundColor: if activeRoom == Just r.roomId then (if dp.isDark then "#2B5278" else "#419FD9") else "transparent" }
170+ }
171+ [ view
172+ { style: tw "rounded-full items-center justify-center"
173+ <> Style.style { width: 32.0, height: 32.0, backgroundColor: avatarColor r.roomId }
174+ }
175+ [ text { style: tw "text-xs font-bold" <> Style.style { color: "#FFFFFF" } } (take 1 r.name) ]
176+ , view { style: tw "ml-2 flex-1" }
177+ [ text
178+ { style: tw "text-sm font-semibold"
179+ <> Style.style { color: if activeRoom == Just r.roomId then "#FFFFFF" else dp.fg }
180+ }
181+ r.name
182+ , text
183+ { style: tw "text-xs"
184+ <> Style.style { color: if activeRoom == Just r.roomId then "#FFFFFFCC" else dp.dimFg }
185+ }
186+ r.lastMessage
187+ ]
188+ , nativeButton
189+ { title: ""
190+ , bezelStyle: T.toolbar
191+ , onPress: selectRoom r.roomId
192+ , style: Style.style { position: "absolute", top: 0.0, left: 0.0, right: 0.0, bottom: 0.0, opacity: 0.0 }
193+ }
194+ ]
195+ )
196+
197+ reactionPills msg =
198+ if length msg.reactions == 0 then mempty
199+ else view { style: tw "flex-row mt-1" <> Style.style { alignSelf: if msg.isMine then "flex-end" else "flex-start" } }
200+ ( msg.reactions <#> \r ->
201+ view
202+ { style: tw "flex-row items-center rounded-full px-2 py-0.5 mr-1"
203+ <> Style.style { backgroundColor: reactionBg }
204+ }
205+ [ text { style: Style.style { fontSize: 12.0 } } r.emoji
206+ , text { style: tw "text-xs ml-1" <> Style.style { color: dp.dimFg } } (show r.count)
207+ ]
208+ )
209+
210+ reactionPicker idx =
211+ nativePopover
212+ { visible: reactPopover == Just idx
213+ , preferredEdge: T.bottom
214+ , behavior: T.transient
215+ , onClose: setReactPopover Nothing
216+ , style: Style.style {}
217+ }
218+ [ nativeButton
219+ { title: "☺"
220+ , bezelStyle: T.toolbar
221+ , onPress:
222+ if reactPopover == Just idx then setReactPopover Nothing
223+ else setReactPopover (Just idx)
224+ , style: Style.style { height: 20.0, width: 24.0 }
225+ }
226+ , view { style: tw "flex-row items-center p-2" <> Style.style { height: 40.0 } }
227+ ( reactionEmoji <#> \emoji ->
228+ nativeButton
229+ { title: emoji
230+ , bezelStyle: T.toolbar
231+ , onPress: reactToMessage idx emoji
232+ , style: Style.style { height: 32.0, width: 32.0 }
233+ }
234+ )
235+ ]
236+
237+ replyQuote msg = case msg.replyTo of
238+ Nothing -> mempty
239+ Just origIdx -> case messages !! origIdx of
240+ Nothing -> mempty
241+ Just orig ->
242+ view
243+ { style: tw "flex-row rounded-lg mb-1 overflow-hidden"
244+ <> Style.style { backgroundColor: replyQuoteBg }
245+ }
246+ [ view { style: Style.style { width: 3.0, backgroundColor: replyBarColor } } []
247+ , view { style: tw "px-2 py-1" }
248+ [ text { style: tw "text-xs font-semibold" <> Style.style { color: replyBarColor } } orig.sender
249+ , text { style: tw "text-xs" <> Style.style { color: dp.dimFg } } (truncate 80 orig.body)
250+ ]
251+ , nativeButton
252+ { title: ""
253+ , bezelStyle: T.toolbar
254+ , onPress: scrollToMessage origIdx
255+ , style: Style.style { position: "absolute", top: 0.0, left: 0.0, right: 0.0, bottom: 0.0, opacity: 0.0 }
256+ }
257+ ]
258+
259+ messageBubble idx msg = do
260+ let align = if msg.isMine then "flex-end" else "flex-start"
261+ let bigEmoji = isSingleEmoji msg.body
262+ let senderLabel = if msg.isMine then "" else msg.sender
263+ let isHighlighted = highlightIdx == Just idx
264+ view { style: tw "px-3 mb-1 rounded-lg" <> Style.style { backgroundColor: if isHighlighted then highlightBg else "transparent" } }
265+ [ view { style: tw "flex-row items-end" <> Style.style { alignSelf: align } }
266+ [ view
267+ { onDoubleClick: handler_ (setReplyingTo (Just idx))
268+ , style: Style.style { maxWidth: 360.0 }
269+ }
270+ [ if senderLabel == "" then text { style: Style.style { height: 0.0 } } ""
271+ else text { style: tw "text-xs mb-0.5" <> Style.style { color: dp.dimFg } } senderLabel
272+ , replyQuote msg
273+ , if bigEmoji then
274+ text
275+ { style: Style.style { fontSize: 64.0, lineHeight: 72.0 } }
276+ msg.body
277+ else
278+ view
279+ { style: tw "rounded-2xl px-3 py-2"
280+ <> Style.style
281+ { backgroundColor: if msg.isMine then sentBubbleBg else receivedBubbleBg }
282+ }
283+ [ text
284+ { style: tw "text-sm"
285+ <> Style.style { color: if msg.isMine then "#FFFFFF" else (if dp.isDark then "#FFFFFF" else "#000000") }
286+ }
287+ msg.body
288+ ]
289+ , reactionPills msg
290+ ]
291+ , if msg.isMine then mempty else reactionPicker idx
292+ ]
293+ ]
294+
295+ replyPreview = case replyingTo of
296+ Nothing -> mempty
297+ Just idx -> case messages !! idx of
298+ Nothing -> mempty
299+ Just orig ->
300+ view
301+ { style: tw "flex-row items-center px-3 py-2"
302+ <> Style.style { borderTopWidth: 0.5, borderColor: dp.dimFg, backgroundColor: "transparent" }
303+ }
304+ [ view { style: Style.style { width: 3.0, height: 28.0, borderRadius: 2.0, backgroundColor: replyBarColor, marginRight: 8.0 } } []
305+ , view { style: tw "flex-1" }
306+ [ text { style: tw "text-xs font-semibold" <> Style.style { color: replyBarColor } } ("Reply to " <> orig.sender)
307+ , text { style: tw "text-xs" <> Style.style { color: dp.dimFg } } (truncate 80 orig.body)
308+ ]
309+ , nativeButton
310+ { title: "✕"
311+ , bezelStyle: T.toolbar
312+ , onPress: setReplyingTo Nothing
313+ , style: Style.style { height: 20.0, width: 24.0 }
314+ }
315+ ]
316+
317+ activeRoomName = case activeRoom of
318+ Nothing -> "Select a room"
319+ Just rid -> fromMaybe rid (map _.name (filter (\r -> r.roomId == rid) mockRooms !! 0))
320+
321+ pure do
322+ sidebarLayout
323+ { sidebar: roomSidebar
324+ , sidebarWidth: 220.0
325+ , content: view { style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
326+ [ view
327+ { style: tw "px-4 py-2 border-b"
328+ <> Style.style { borderBottomWidth: 0.5, borderColor: dp.dimFg, backgroundColor: "transparent" }
329+ }
330+ [ text { style: tw "text-base font-semibold" <> Style.style { color: dp.fg } } activeRoomName ]
331+ , nativePatternBackground
332+ { patternColor: if dp.isDark then "#FFFFFF" else "#000000"
333+ , background: chatBg
334+ , patternOpacity: if dp.isDark then 0.04 else 0.06
335+ , patternScale: 1.0
336+ , style: tw "flex-1"
337+ }
338+ ( nativeScrollView
339+ { scrollToBottom: scrollBottomTrigger
340+ , scrollToY: scrollY
341+ , scrollToYTrigger: scrollTrigger
342+ , style: tw "flex-1" <> Style.style { backgroundColor: "transparent" }
343+ }
344+ ( view { style: tw "py-2 pr-3" }
345+ (mapWithIndex messageBubble messages)
346+ )
347+ )
348+ , replyPreview
349+ , view
350+ { style: tw "flex-row items-center px-3 py-2"
351+ <> Style.style { borderTopWidth: 0.5, borderColor: dp.dimFg, backgroundColor: "transparent" }
352+ }
353+ ( case activeRoom of
354+ Nothing ->
355+ [ text { style: tw "text-sm" <> Style.style { color: dp.dimFg } } "Select a room to start chatting" ]
356+ Just rid ->
357+ [ nativeTextField
358+ { text: inputText
359+ , placeholder: "Message..."
360+ , search: false
361+ , rounded: true
362+ , onChangeText: \t -> setInputText (replaceEmoji t)
363+ , onSubmit: \t -> sendMessage rid t
364+ , style: tw "flex-1" <> Style.style { height: 28.0 }
365+ }
366+ , nativeButton
367+ { sfSymbol: "paperplane.fill"
368+ , bezelStyle: T.toolbar
369+ , onPress: sendMessage rid inputText
370+ , style: Style.style { height: 28.0, width: 36.0, marginLeft: 8.0 }
371+ }
372+ ]
373+ )
374+ ]
375+ }
example-macos/src/Demo/DataViews.pursnew file+76
@@ -0,0 +1,76 @@
1+module Demo.DataViews (tableViewDemo, outlineViewDemo) where
2+
3+import Prelude
4+
5+import Demo.Shared (DemoProps, desc, label, scrollWrap, sectionTitle)
6+import React.Basic (JSX)
7+import React.Basic.Hooks (useState', (/\))
8+import React.Basic.Hooks as React
9+import Yoga.React (component)
10+import Yoga.React.Native (tw)
11+import Yoga.React.Native.MacOS.OutlineView (OutlineItem(..), nativeOutlineView)
12+import Yoga.React.Native.MacOS.TableView (nativeTableView)
13+import Yoga.React.Native.Style as Style
14+
15+tableViewDemo :: DemoProps -> JSX
16+tableViewDemo = component "TableViewDemo" \dp -> React.do
17+ selected /\ setSelected <- useState' ""
18+ pure do
19+ scrollWrap dp
20+ [ sectionTitle dp.fg "Table View"
21+ , desc dp "Native NSTableView with columns and rows"
22+ , nativeTableView
23+ { columns:
24+ [ { id: "name", title: "Name", width: 150.0 }
25+ , { id: "type", title: "Type", width: 100.0 }
26+ , { id: "size", title: "Size", width: 80.0 }
27+ ]
28+ , rows:
29+ [ [ "Main.purs", "PureScript", "12 KB" ]
30+ , [ "App.tsx", "TypeScript", "8 KB" ]
31+ , [ "style.css", "CSS", "3 KB" ]
32+ , [ "index.html", "HTML", "1 KB" ]
33+ , [ "package.json", "JSON", "2 KB" ]
34+ ]
35+ , headerVisible: true
36+ , alternatingRows: true
37+ , onSelectRow: \i -> setSelected ("Row " <> show i)
38+ , onDoubleClickRow: \i -> setSelected ("Double-clicked row " <> show i)
39+ , style: Style.style { height: 180.0 } <> tw "rounded-lg overflow-hidden mb-2"
40+ }
41+ , if selected == "" then mempty
42+ else label dp.dimFg selected
43+ ]
44+
45+outlineViewDemo :: DemoProps -> JSX
46+outlineViewDemo = component "OutlineViewDemo" \dp -> React.do
47+ selection /\ setSelection <- useState' ""
48+ pure do
49+ scrollWrap dp
50+ [ sectionTitle dp.fg "Outline View"
51+ , desc dp "Hierarchical tree list (NSOutlineView)"
52+ , nativeOutlineView
53+ { items: do
54+ let file id title = OutlineItem { id, title, sfSymbol: "doc", children: [] }
55+ let folder id title children = OutlineItem { id, title, sfSymbol: "folder", children }
56+ [ folder "src" "src"
57+ [ file "main" "Main.purs"
58+ , folder "macos" "MacOS"
59+ [ file "btn" "Button.purs"
60+ , file "sl" "Slider.purs"
61+ , file "sw" "Switch.purs"
62+ ]
63+ ]
64+ , folder "test" "test"
65+ [ file "t1" "MacOSComponents.test.js"
66+ , file "t2" "MacOSSnapshots.test.js"
67+ ]
68+ , OutlineItem { id: "pkg", title: "package.json", sfSymbol: "doc.text", children: [] }
69+ ]
70+ , headerVisible: false
71+ , onSelectItem: setSelection
72+ , style: Style.style { height: 200.0 } <> tw "rounded-lg overflow-hidden mb-2"
73+ }
74+ , if selection == "" then mempty
75+ else label dp.dimFg ("Selected: " <> selection)
76+ ]
example-macos/src/Demo/Display.pursnew file+147
@@ -0,0 +1,147 @@
1+module Demo.Display
2+ ( imageDemo
3+ , animatedImageDemo
4+ , videoPlayerDemo
5+ , levelIndicatorDemo
6+ , progressDemo
7+ , separatorDemo
8+ , pathControlDemo
9+ ) where
10+
11+import Prelude
12+
13+import Demo.Shared (DemoProps, card, desc, scrollWrap, sectionTitle)
14+import React.Basic (JSX)
15+import React.Basic.Hooks (useState', (/\))
16+import React.Basic.Hooks as React
17+import Yoga.React (component)
18+import Yoga.React.Native (text, tw, view)
19+import Yoga.React.Native.MacOS.AnimatedImage (nativeAnimatedImage)
20+import Yoga.React.Native.MacOS.Image (nativeImage)
21+import Yoga.React.Native.MacOS.LevelIndicator (nativeLevelIndicator)
22+import Yoga.React.Native.MacOS.PathControl (nativePathControl)
23+import Yoga.React.Native.MacOS.Progress (nativeProgress)
24+import Yoga.React.Native.MacOS.Separator (nativeSeparator)
25+import Yoga.React.Native.MacOS.Slider (nativeSlider)
26+import Yoga.React.Native.MacOS.Types as T
27+import Yoga.React.Native.MacOS.VideoPlayer (nativeVideoPlayer)
28+import Yoga.React.Native.Style as Style
29+
30+imageDemo :: DemoProps -> JSX
31+imageDemo = component "ImageDemo" \dp -> pure do
32+ scrollWrap dp
33+ [ sectionTitle dp.fg "Image"
34+ , desc dp "Native NSImageView with URL loading and corner radius"
35+ , nativeImage
36+ { source: "https://placedog.net/640/400"
37+ , contentMode: T.scaleProportionally
38+ , cornerRadius: 12.0
39+ , style: Style.style { height: 200.0 } <> tw "mb-2"
40+ }
41+ ]
42+
43+animatedImageDemo :: DemoProps -> JSX
44+animatedImageDemo = component "AnimatedImageDemo" \dp -> pure do
45+ scrollWrap dp
46+ [ sectionTitle dp.fg "Animated Image"
47+ , desc dp "Native NSImageView with animated GIF support"
48+ , nativeAnimatedImage
49+ { source: "https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif"
50+ , animating: true
51+ , style: Style.style { height: 200.0 } <> tw "rounded-lg overflow-hidden mb-2"
52+ }
53+ , nativeAnimatedImage
54+ { source: "https://media.giphy.com/media/13CoXDiaCcCoyk/giphy.gif"
55+ , animating: true
56+ , style: Style.style { height: 200.0 } <> tw "rounded-lg overflow-hidden mb-2"
57+ }
58+ ]
59+
60+videoPlayerDemo :: DemoProps -> JSX
61+videoPlayerDemo = component "VideoPlayerDemo" \_ -> pure do
62+ view { style: tw "flex-1" }
63+ [ nativeVideoPlayer
64+ { source: "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/720/Big_Buck_Bunny_720_10s_1MB.mp4"
65+ , playing: true
66+ , looping: true
67+ , muted: false
68+ , controlsStyle: T.floatingControls
69+ , style: tw "flex-1"
70+ }
71+ ]
72+
73+levelIndicatorDemo :: DemoProps -> JSX
74+levelIndicatorDemo = component "LevelIndicatorDemo" \dp -> React.do
75+ value /\ setValue <- useState' 50.0
76+ pure do
77+ scrollWrap dp
78+ [ sectionTitle dp.fg "Level Indicator"
79+ , card dp.cardBg
80+ [ nativeSlider
81+ { value
82+ , minValue: 0.0
83+ , maxValue: 100.0
84+ , numberOfTickMarks: 11
85+ , onChange: setValue
86+ , style: Style.style { height: 24.0 }
87+ }
88+ , nativeLevelIndicator
89+ { value
90+ , minValue: 0.0
91+ , maxValue: 100.0
92+ , warningValue: 70.0
93+ , criticalValue: 90.0
94+ , style: Style.style { height: 18.0, marginTop: 8.0 }
95+ }
96+ ]
97+ ]
98+
99+progressDemo :: DemoProps -> JSX
100+progressDemo = component "ProgressDemo" \dp -> React.do
101+ value /\ setValue <- useState' 50.0
102+ pure do
103+ scrollWrap dp
104+ [ sectionTitle dp.fg "Progress"
105+ , card dp.cardBg
106+ [ nativeSlider
107+ { value
108+ , minValue: 0.0
109+ , maxValue: 100.0
110+ , numberOfTickMarks: 11
111+ , onChange: setValue
112+ , style: Style.style { height: 24.0 }
113+ }
114+ , nativeProgress
115+ { value
116+ , style: Style.style { height: 18.0, marginTop: 8.0 }
117+ }
118+ ]
119+ ]
120+
121+separatorDemo :: DemoProps -> JSX
122+separatorDemo = component "SeparatorDemo" \dp -> pure do
123+ scrollWrap dp
124+ [ sectionTitle dp.fg "Separator"
125+ , desc dp "Horizontal and vertical separators"
126+ , nativeSeparator
127+ { vertical: false
128+ , style: Style.style { height: 2.0 } <> tw "my-2"
129+ }
130+ , text { style: tw "text-xs mb-2" <> Style.style { color: dp.dimFg } } "Content between separators"
131+ , nativeSeparator
132+ { vertical: false
133+ , style: Style.style { height: 2.0 } <> tw "my-2"
134+ }
135+ ]
136+
137+pathControlDemo :: DemoProps -> JSX
138+pathControlDemo = component "PathControlDemo" \dp -> pure do
139+ scrollWrap dp
140+ [ sectionTitle dp.fg "Path Control"
141+ , nativePathControl
142+ { url: "/Users"
143+ , pathStyle: T.standardPath
144+ , onSelectPath: \_ -> pure unit
145+ , style: Style.style { height: 24.0 } <> tw "mb-2"
146+ }
147+ ]
example-macos/src/Demo/DragDropFiles.pursnew file+106
@@ -0,0 +1,106 @@
1+module Demo.DragDropFiles (dropZoneDemo, filePickerDemo) where
2+
3+import Prelude
4+
5+import Data.String (joinWith)
6+import Demo.Shared (DemoProps, card, desc, label, scrollWrap, sectionTitle)
7+import React.Basic (JSX)
8+import React.Basic.Hooks (useState', (/\))
9+import React.Basic.Hooks as React
10+import Yoga.React (component)
11+import Yoga.React.Native (text, tw, view)
12+import Yoga.React.Native.MacOS.DropZone (nativeDropZone)
13+import Yoga.React.Native.MacOS.FilePicker (nativeFilePicker)
14+import Yoga.React.Native.MacOS.Types as T
15+import Yoga.React.Native.Style as Style
16+
17+dropZoneDemo :: DemoProps -> JSX
18+dropZoneDemo = component "DropZoneDemo" \dp -> React.do
19+ status /\ setStatus <- useState' "Drop files here"
20+ dropped /\ setDropped <- useState' ""
21+ dragging /\ setDragging <- useState' false
22+ pure do
23+ let accentBorder = if dragging then "#007AFF" else dp.dimFg
24+ scrollWrap dp
25+ [ sectionTitle dp.fg "Drop Zone"
26+ , desc dp "Drag files from Finder into the area below"
27+ , nativeDropZone
28+ { onFileDrop: \d -> do
29+ let fileNames = joinWith ", " (map _.name d.files)
30+ let stringItems = joinWith ", " d.strings
31+ let
32+ info = case fileNames, stringItems of
33+ "", "" -> "Empty drop"
34+ fs, "" -> "Files: " <> fs
35+ "", ss -> "Strings: " <> ss
36+ fs, ss -> "Files: " <> fs <> " | Strings: " <> ss
37+ setDropped info
38+ setStatus "Drop files here"
39+ setDragging false
40+ , onFilesDragEnter: do
41+ setStatus "Release to drop!"
42+ setDragging true
43+ , onFilesDragExit: do
44+ setStatus "Drop files here"
45+ setDragging false
46+ , style: tw "rounded-lg items-center justify-center mb-2"
47+ <> Style.style
48+ { minHeight: 100.0
49+ , borderWidth: 2.0
50+ , borderColor: accentBorder
51+ , backgroundColor: dp.cardBg
52+ }
53+ }
54+ [ text { style: tw "text-sm" <> Style.style { color: dp.fg } } status
55+ , if dropped == "" then mempty
56+ else text { style: tw "text-xs mt-2 px-4" <> Style.style { color: dp.dimFg } }
57+ dropped
58+ ]
59+ ]
60+
61+filePickerDemo :: DemoProps -> JSX
62+filePickerDemo = component "FilePickerDemo" \dp -> React.do
63+ picked /\ setPicked <- useState' ""
64+ pure do
65+ scrollWrap dp
66+ [ sectionTitle dp.fg "File Picker"
67+ , desc dp "Click buttons to open native file panels"
68+ , card dp.cardBg
69+ [ view { style: tw "flex-row items-center" }
70+ [ nativeFilePicker
71+ { mode: T.openFile
72+ , title: "Open Files"
73+ , sfSymbol: "doc.badge.plus"
74+ , allowMultiple: true
75+ , allowedTypes: [ "public.image", "public.text" ]
76+ , message: "Select files to open"
77+ , onPickFiles: \files -> setPicked (joinWith ", " (map _.path files))
78+ , onCancel: setPicked "Cancelled"
79+ , style: Style.style { height: 24.0, width: 120.0 }
80+ }
81+ , nativeFilePicker
82+ { mode: T.openFile
83+ , title: "Choose Folder"
84+ , sfSymbol: "folder"
85+ , canChooseDirectories: true
86+ , message: "Select a folder"
87+ , onPickFiles: \files -> setPicked (joinWith ", " (map _.path files))
88+ , onCancel: setPicked "Cancelled"
89+ , style: Style.style { height: 24.0, width: 140.0, marginLeft: 8.0 }
90+ }
91+ , nativeFilePicker
92+ { mode: T.saveFile
93+ , title: "Save As..."
94+ , sfSymbol: "square.and.arrow.down"
95+ , defaultName: "Untitled.txt"
96+ , allowedTypes: [ "public.plain-text" ]
97+ , message: "Choose save location"
98+ , onPickFiles: \files -> setPicked (joinWith ", " (map _.path files))
99+ , onCancel: setPicked "Cancelled"
100+ , style: Style.style { height: 24.0, width: 120.0, marginLeft: 8.0 }
101+ }
102+ ]
103+ , if picked == "" then mempty
104+ else label dp.dimFg picked
105+ ]
106+ ]
example-macos/src/Demo/InputControls.pursnew file+319
@@ -0,0 +1,319 @@
1+module Demo.InputControls
2+ ( buttonDemo
3+ , switchDemo
4+ , sliderDemo
5+ , popUpDemo
6+ , comboBoxDemo
7+ , stepperDemo
8+ , datePickerDemo
9+ , colorWellDemo
10+ , checkboxDemo
11+ , radioButtonDemo
12+ , searchFieldDemo
13+ , tokenFieldDemo
14+ ) where
15+
16+import Prelude
17+
18+import Data.Array
19+ ( (!!)
20+ )
21+import Data.Maybe (fromMaybe)
22+import Demo.Shared (DemoProps, card, desc, label, round, scrollWrap, sectionTitle)
23+import React.Basic (JSX)
24+
25+import React.Basic.Hooks (useState', (/\))
26+import React.Basic.Hooks as React
27+import Yoga.React (component)
28+import Yoga.React.Native (text, tw, view)
29+import Yoga.React.Native.MacOS.Button (nativeButton)
30+import Yoga.React.Native.MacOS.Checkbox (nativeCheckbox)
31+import Yoga.React.Native.MacOS.ColorWell (nativeColorWell)
32+import Yoga.React.Native.MacOS.ComboBox (nativeComboBox)
33+import Yoga.React.Native.MacOS.DatePicker (nativeDatePicker)
34+import Yoga.React.Native.MacOS.LevelIndicator (nativeLevelIndicator)
35+import Yoga.React.Native.MacOS.PopUp (nativePopUp)
36+import Yoga.React.Native.MacOS.Progress (nativeProgress)
37+import Yoga.React.Native.MacOS.RadioButton (nativeRadioButton)
38+import Yoga.React.Native.MacOS.SearchField (nativeSearchField)
39+import Yoga.React.Native.MacOS.Slider (nativeSlider)
40+import Yoga.React.Native.MacOS.Stepper (nativeStepper)
41+import Yoga.React.Native.MacOS.Switch (nativeSwitch)
42+import Yoga.React.Native.MacOS.TokenField (nativeTokenField)
43+import Yoga.React.Native.MacOS.Types as T
44+import Yoga.React.Native.Style as Style
45+
46+buttonDemo :: DemoProps -> JSX
47+buttonDemo = component "ButtonDemo" \dp -> React.do
48+ status /\ setStatus <- useState' "Ready"
49+ pure do
50+ scrollWrap dp
51+ [ sectionTitle dp.fg "Button"
52+ , card dp.cardBg
53+ [ view { style: tw "flex-row items-center" }
54+ [ nativeButton
55+ { title: "Say Hello"
56+ , bezelStyle: T.push
57+ , primary: true
58+ , onPress: setStatus "Hello from PureScript!"
59+ , style: Style.style { height: 24.0, width: 140.0 }
60+ }
61+ , nativeButton
62+ { title: "Reset"
63+ , bezelStyle: T.push
64+ , onPress: setStatus "Ready"
65+ , style: Style.style { height: 24.0, width: 100.0, marginLeft: 8.0 }
66+ }
67+ , label dp.dimFg status
68+ ]
69+ ]
70+ ]
71+
72+switchDemo :: DemoProps -> JSX
73+switchDemo = component "SwitchDemo" \dp -> React.do
74+ on /\ setOn <- useState' false
75+ pure do
76+ scrollWrap dp
77+ [ sectionTitle dp.fg "Switch"
78+ , card dp.cardBg
79+ [ view { style: tw "flex-row items-center" }
80+ [ nativeSwitch
81+ { on
82+ , onChange: setOn
83+ , style: Style.style { height: 24.0, width: 48.0 }
84+ }
85+ , label dp.dimFg (if on then "On" else "Off")
86+ ]
87+ ]
88+ ]
89+
90+sliderDemo :: DemoProps -> JSX
91+sliderDemo = component "SliderDemo" \dp -> React.do
92+ value /\ setValue <- useState' 50.0
93+ pure do
94+ scrollWrap dp
95+ [ sectionTitle dp.fg "Slider + Level Indicator + Progress"
96+ , card dp.cardBg
97+ [ nativeSlider
98+ { value
99+ , minValue: 0.0
100+ , maxValue: 100.0
101+ , numberOfTickMarks: 11
102+ , onChange: setValue
103+ , style: Style.style { height: 24.0 }
104+ }
105+ , label dp.dimFg ("Value: " <> show (round value) <> " / 100")
106+ , nativeLevelIndicator
107+ { value
108+ , minValue: 0.0
109+ , maxValue: 100.0
110+ , warningValue: 70.0
111+ , criticalValue: 90.0
112+ , style: Style.style { height: 18.0, marginTop: 8.0 }
113+ }
114+ , nativeProgress
115+ { value
116+ , style: Style.style { height: 18.0, marginTop: 8.0 }
117+ }
118+ ]
119+ ]
120+
121+popUpDemo :: DemoProps -> JSX
122+popUpDemo = component "PopUpDemo" \dp -> React.do
123+ idx /\ setIdx <- useState' 0
124+ title /\ setTitle <- useState' "Small"
125+ let items = [ "Small", "Medium", "Large", "Extra Large" ]
126+ pure do
127+ scrollWrap dp
128+ [ sectionTitle dp.fg "Pop Up Button"
129+ , card dp.cardBg
130+ [ view { style: tw "flex-row items-center" }
131+ [ nativePopUp
132+ { items
133+ , selectedIndex: idx
134+ , onChange: \i -> do
135+ setIdx i
136+ setTitle (fromMaybe "" (items !! i))
137+ , style: Style.style { height: 24.0, width: 160.0 }
138+ }
139+ , label dp.dimFg ("Selected: " <> title)
140+ ]
141+ ]
142+ ]
143+
144+comboBoxDemo :: DemoProps -> JSX
145+comboBoxDemo = component "ComboBoxDemo" \dp -> React.do
146+ txt /\ setTxt <- useState' ""
147+ result /\ setResult <- useState' ""
148+ pure do
149+ scrollWrap dp
150+ [ sectionTitle dp.fg "Combo Box"
151+ , desc dp "Editable dropdown with autocomplete"
152+ , card dp.cardBg
153+ [ nativeComboBox
154+ { items: [ "Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig", "Grape" ]
155+ , text: txt
156+ , placeholder: "Type a fruit..."
157+ , onChangeText: setTxt
158+ , onSelectItem: \t -> do
159+ setTxt t
160+ setResult ("Selected: " <> t)
161+ , style: Style.style { height: 28.0 } <> tw "mb-2"
162+ }
163+ , if result == "" then mempty
164+ else label dp.dimFg result
165+ ]
166+ ]
167+
168+stepperDemo :: DemoProps -> JSX
169+stepperDemo = component "StepperDemo" \dp -> React.do
170+ value /\ setValue <- useState' 5.0
171+ pure do
172+ scrollWrap dp
173+ [ sectionTitle dp.fg "Stepper"
174+ , desc dp "Up/down increment control"
175+ , card dp.cardBg
176+ [ view { style: tw "flex-row items-center" }
177+ [ text { style: tw "text-sm mr-3" <> Style.style { color: dp.fg } } "Quantity:"
178+ , nativeStepper
179+ { value
180+ , minValue: 0.0
181+ , maxValue: 50.0
182+ , increment: 1.0
183+ , onChange: setValue
184+ , style: Style.style { width: 100.0, height: 24.0 }
185+ }
186+ ]
187+ ]
188+ ]
189+
190+datePickerDemo :: DemoProps -> JSX
191+datePickerDemo = component "DatePickerDemo" \dp -> React.do
192+ dateText /\ setDateText <- useState' ""
193+ pure do
194+ scrollWrap dp
195+ [ sectionTitle dp.fg "Date Picker"
196+ , card dp.cardBg
197+ [ nativeDatePicker
198+ { graphical: false
199+ , onChange: setDateText
200+ , style: Style.style { height: 24.0, width: 200.0 }
201+ }
202+ , if dateText == "" then mempty
203+ else label dp.dimFg ("Picked: " <> dateText)
204+ ]
205+ ]
206+
207+colorWellDemo :: DemoProps -> JSX
208+colorWellDemo = component "ColorWellDemo" \dp -> React.do
209+ color /\ setColor <- useState' "#FF6600"
210+ pure do
211+ scrollWrap dp
212+ [ sectionTitle dp.fg "Color Well"
213+ , card dp.cardBg
214+ [ view { style: tw "flex-row items-center" }
215+ [ nativeColorWell
216+ { color
217+ , minimal: true
218+ , onChange: setColor
219+ , style: Style.style { height: 32.0, width: 48.0 }
220+ }
221+ , view
222+ { style: tw "ml-3 rounded" <> Style.style
223+ { width: 24.0, height: 24.0, backgroundColor: color }
224+ }
225+ []
226+ , label dp.dimFg color
227+ ]
228+ ]
229+ ]
230+
231+checkboxDemo :: DemoProps -> JSX
232+checkboxDemo = component "CheckboxDemo" \dp -> React.do
233+ a /\ setA <- useState' true
234+ b /\ setB <- useState' false
235+ pure do
236+ scrollWrap dp
237+ [ sectionTitle dp.fg "Checkbox"
238+ , card dp.cardBg
239+ [ nativeCheckbox
240+ { checked: a
241+ , title: "Enable notifications"
242+ , enabled: true
243+ , onChange: setA
244+ , style: Style.style { height: 24.0 } <> tw "mb-1"
245+ }
246+ , nativeCheckbox
247+ { checked: b
248+ , title: "Dark mode"
249+ , enabled: true
250+ , onChange: setB
251+ , style: Style.style { height: 24.0 }
252+ }
253+ ]
254+ ]
255+
256+radioButtonDemo :: DemoProps -> JSX
257+radioButtonDemo = component "RadioButtonDemo" \dp -> React.do
258+ choice /\ setChoice <- useState' "option1"
259+ pure do
260+ scrollWrap dp
261+ [ sectionTitle dp.fg "Radio Button"
262+ , card dp.cardBg
263+ [ nativeRadioButton
264+ { selected: choice == "option1"
265+ , title: "Small"
266+ , enabled: true
267+ , onChange: \_ -> setChoice "option1"
268+ , style: Style.style { height: 24.0 } <> tw "mb-1"
269+ }
270+ , nativeRadioButton
271+ { selected: choice == "option2"
272+ , title: "Medium"
273+ , enabled: true
274+ , onChange: \_ -> setChoice "option2"
275+ , style: Style.style { height: 24.0 } <> tw "mb-1"
276+ }
277+ , nativeRadioButton
278+ { selected: choice == "option3"
279+ , title: "Large"
280+ , enabled: true
281+ , onChange: \_ -> setChoice "option3"
282+ , style: Style.style { height: 24.0 }
283+ }
284+ , label dp.dimFg ("Selected: " <> choice)
285+ ]
286+ ]
287+
288+searchFieldDemo :: DemoProps -> JSX
289+searchFieldDemo = component "SearchFieldDemo" \dp -> React.do
290+ query /\ setQuery <- useState' ""
291+ result /\ setResult <- useState' ""
292+ pure do
293+ scrollWrap dp
294+ [ sectionTitle dp.fg "Search Field"
295+ , nativeSearchField
296+ { text: query
297+ , placeholder: "Search..."
298+ , onChangeText: setQuery
299+ , onSearch: \t -> setResult ("Searched: " <> t)
300+ , style: Style.style { height: 28.0 } <> tw "mb-2"
301+ }
302+ , if result == "" then mempty
303+ else label dp.dimFg result
304+ ]
305+
306+tokenFieldDemo :: DemoProps -> JSX
307+tokenFieldDemo = component "TokenFieldDemo" \dp -> React.do
308+ tokens /\ setTokens <- useState' ([ "PureScript", "React", "macOS" ] :: Array String)
309+ pure do
310+ scrollWrap dp
311+ [ sectionTitle dp.fg "Token Field"
312+ , desc dp "Type text and press Enter to add tokens"
313+ , nativeTokenField
314+ { tokens
315+ , placeholder: "Add tags..."
316+ , onChangeTokens: setTokens
317+ , style: Style.style { height: 28.0 } <> tw "mb-2"
318+ }
319+ ]
example-macos/src/Demo/Layout.pursnew file+70
@@ -0,0 +1,70 @@
1+module Demo.Layout (boxDemo, splitViewDemo, tabViewDemo) where
2+
3+import Prelude
4+
5+import Demo.Shared (DemoProps, desc, scrollWrap, sectionTitle)
6+import React.Basic (JSX)
7+import React.Basic.Hooks (useState', (/\))
8+import React.Basic.Hooks as React
9+import Yoga.React (component)
10+import Yoga.React.Native (text, tw, view)
11+import Yoga.React.Native.MacOS.Box (nativeBox)
12+import Yoga.React.Native.MacOS.SplitView (nativeSplitView)
13+import Yoga.React.Native.MacOS.TabView (nativeTabView)
14+import Yoga.React.Native.Style as Style
15+
16+boxDemo :: DemoProps -> JSX
17+boxDemo = component "BoxDemo" \dp -> pure do
18+ scrollWrap dp
19+ [ sectionTitle dp.fg "Box"
20+ , desc dp "Native NSBox with title and border"
21+ , nativeBox
22+ { boxTitle: "Settings"
23+ , fillColor2: dp.cardBg
24+ , borderColor2: dp.dimFg
25+ , radius: 8.0
26+ , style: Style.style { height: 100.0 } <> tw "mb-2"
27+ }
28+ [ view { style: tw "flex-row items-center p-2" }
29+ [ text { style: tw "text-sm" <> Style.style { color: dp.fg } } "Content inside a native box" ]
30+ ]
31+ ]
32+
33+splitViewDemo :: DemoProps -> JSX
34+splitViewDemo = component "SplitViewDemo" \dp -> pure do
35+ scrollWrap dp
36+ [ sectionTitle dp.fg "Split View"
37+ , desc dp "Native NSSplitView with resizable divider"
38+ , nativeSplitView
39+ { isVertical: true
40+ , style: Style.style { height: 150.0 } <> tw "rounded-lg overflow-hidden mb-2"
41+ }
42+ [ view { style: tw "flex-1 p-4" <> Style.style { backgroundColor: dp.cardBg } }
43+ [ text { style: tw "text-sm font-semibold" <> Style.style { color: dp.fg } } "Left Pane"
44+ , text { style: tw "text-xs mt-1" <> Style.style { color: dp.dimFg } } "Drag the divider to resize"
45+ ]
46+ , view { style: tw "flex-1 p-4" <> Style.style { backgroundColor: dp.cardBg } }
47+ [ text { style: tw "text-sm font-semibold" <> Style.style { color: dp.fg } } "Right Pane"
48+ , text { style: tw "text-xs mt-1" <> Style.style { color: dp.dimFg } } "Content fills available space"
49+ ]
50+ ]
51+ ]
52+
53+tabViewDemo :: DemoProps -> JSX
54+tabViewDemo = component "TabViewDemo" \dp -> React.do
55+ selected /\ setSelected <- useState' "general"
56+ pure do
57+ scrollWrap dp
58+ [ sectionTitle dp.fg "Tab View"
59+ , desc dp "Native segmented tab bar"
60+ , nativeTabView
61+ { items:
62+ [ { id: "general", label: "General" }
63+ , { id: "advanced", label: "Advanced" }
64+ , { id: "about", label: "About" }
65+ ]
66+ , selectedItem: selected
67+ , onSelectTab: setSelected
68+ , style: Style.style { height: 32.0 } <> tw "mb-2"
69+ }
70+ ]
example-macos/src/Demo/MapsDocs.pursnew file+62
@@ -0,0 +1,62 @@
1+module Demo.MapsDocs (mapViewDemo, pdfViewDemo) where
2+
3+import Prelude
4+
5+import Demo.Shared (DemoProps)
6+import React.Basic (JSX)
7+import React.Basic.Hooks (useState', (/\))
8+import React.Basic.Hooks as React
9+import Yoga.React (component)
10+import Yoga.React.Native (text, tw, view)
11+import Yoga.React.Native.MacOS.MapView (nativeMapView)
12+import Yoga.React.Native.MacOS.PDFView (nativePDFView)
13+import Yoga.React.Native.MacOS.Segmented (nativeSegmented)
14+import Yoga.React.Native.MacOS.Types as T
15+import Yoga.React.Native.Style as Style
16+
17+mapViewDemo :: DemoProps -> JSX
18+mapViewDemo = component "MapViewDemo" \dp -> React.do
19+ mapType /\ setMapType <- useState' 0
20+ let
21+ mt = case mapType of
22+ 1 -> T.satelliteMap
23+ 2 -> T.hybridMap
24+ _ -> T.standardMap
25+ pure do
26+ view { style: tw "flex-1" }
27+ [ view { style: tw "flex-row items-center px-3 py-2" <> Style.style { backgroundColor: "transparent" } }
28+ [ text { style: tw "text-sm font-semibold mr-3" <> Style.style { color: dp.fg } } "Map View"
29+ , nativeSegmented
30+ { labels: [ "Standard", "Satellite", "Hybrid" ]
31+ , selectedIndex: mapType
32+ , onChange: setMapType
33+ , style: Style.style { width: 240.0, height: 24.0 }
34+ }
35+ ]
36+ , nativeMapView
37+ { latitude: 37.7749
38+ , longitude: -122.4194
39+ , latitudeDelta: 0.05
40+ , longitudeDelta: 0.05
41+ , mapType: mt
42+ , showsUserLocation: false
43+ , annotations:
44+ [ { latitude: 37.7749, longitude: -122.4194, title: "San Francisco", subtitle: "California" }
45+ , { latitude: 37.8199, longitude: -122.4783, title: "Golden Gate Bridge", subtitle: "" }
46+ ]
47+ , style: tw "flex-1"
48+ }
49+ ]
50+
51+pdfViewDemo :: DemoProps -> JSX
52+pdfViewDemo = component "PDFViewDemo" \dp -> pure do
53+ view { style: tw "flex-1" }
54+ [ view { style: tw "flex-row items-center px-3 py-2" <> Style.style { backgroundColor: "transparent" } }
55+ [ text { style: tw "text-sm font-semibold" <> Style.style { color: dp.fg } } "PDF Viewer" ]
56+ , nativePDFView
57+ { source: "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
58+ , autoScales: true
59+ , displayMode: T.singlePageContinuous
60+ , style: tw "flex-1"
61+ }
62+ ]
example-macos/src/Demo/Navigation.pursnew file+209
@@ -0,0 +1,209 @@
1+module Demo.Navigation (outlineSidebar, demoContent) where
2+
3+import Prelude
4+
5+import Data.Array (concatMap, null)
6+import Data.String (toLower, contains, Pattern(..))
7+import Demo.AiMl (cameraDemo, naturalLanguageDemo, ocrDemo, speechRecognitionDemo)
8+import Demo.Animation (riveDemo)
9+import Demo.SpringAnimation (springDemo)
10+import Demo.Chat (chatDemo)
11+import Demo.DataViews (outlineViewDemo, tableViewDemo)
12+import Demo.Display (animatedImageDemo, imageDemo, levelIndicatorDemo, pathControlDemo, progressDemo, separatorDemo, videoPlayerDemo)
13+import Demo.DragDropFiles (dropZoneDemo, filePickerDemo)
14+import Demo.InputControls (buttonDemo, checkboxDemo, colorWellDemo, comboBoxDemo, datePickerDemo, popUpDemo, radioButtonDemo, searchFieldDemo, sliderDemo, stepperDemo, switchDemo, tokenFieldDemo)
15+import Demo.Layout (boxDemo, splitViewDemo, tabViewDemo)
16+import Demo.MapsDocs (mapViewDemo, pdfViewDemo)
17+import Demo.Overlays (alertDemo, contextMenuDemo, menuDemo, popoverDemo, sheetDemo)
18+import Demo.Shared (DemoProps)
19+import Demo.SystemServices (clipboardDemo, colorPanelDemo, fontPanelDemo, notificationsDemo, quickLookDemo, shareDemo, soundDemo, speechDemo, statusBarDemo)
20+import Demo.TextEditing (textEditorDemo, textFieldDemo)
21+import Demo.WebBrowser (webViewDemo)
22+import Effect (Effect)
23+import React.Basic (JSX)
24+import React.Basic.Hooks (useState', (/\))
25+import React.Basic.Hooks as React
26+import Yoga.React (component)
27+import Yoga.React.Native (text, tw, view)
28+import Yoga.React.Native.MacOS.OutlineView (OutlineItem(..), nativeOutlineView)
29+import Yoga.React.Native.MacOS.SearchField (nativeSearchField)
30+import Yoga.React.Native.Style as Style
31+
32+outlineTree :: Array OutlineItem
33+outlineTree = do
34+ let leaf id title symbol = OutlineItem { id, title, sfSymbol: symbol, children: [] }
35+ let folder id title symbol children = OutlineItem { id, title, sfSymbol: symbol, children }
36+ [ folder "input" "Input Controls" "slider.horizontal.3"
37+ [ leaf "button" "Button" "button.horizontal"
38+ , leaf "switch" "Switch" "switch.2"
39+ , leaf "slider" "Slider" "slider.horizontal.below.rectangle"
40+ , leaf "popup" "Pop Up" "chevron.up.chevron.down"
41+ , leaf "combobox" "Combo Box" "rectangle.and.pencil.and.ellipsis"
42+ , leaf "stepper" "Stepper" "plus.forwardslash.minus"
43+ , leaf "datepicker" "Date Picker" "calendar"
44+ , leaf "colorwell" "Color Well" "paintpalette"
45+ , leaf "checkbox" "Checkbox" "checkmark.square"
46+ , leaf "radiobutton" "Radio Button" "circle.inset.filled"
47+ , leaf "searchfield" "Search Field" "magnifyingglass"
48+ , leaf "tokenfield" "Token Field" "tag"
49+ ]
50+ , folder "text" "Text & Editing" "doc.richtext"
51+ [ leaf "textfield" "Text Field" "character.cursor.ibeam"
52+ , leaf "texteditor" "Text Editor" "doc.richtext"
53+ ]
54+ , folder "display" "Display" "photo"
55+ [ leaf "image" "Image" "photo"
56+ , leaf "animatedimage" "Animated Image" "photo.on.rectangle.angled"
57+ , leaf "videoplayer" "Video Player" "play.rectangle"
58+ , leaf "levelindicator" "Level Indicator" "gauge.with.dots.needle.33percent"
59+ , leaf "progress" "Progress" "chart.bar.fill"
60+ , leaf "separator" "Separator" "minus"
61+ , leaf "pathcontrol" "Path Control" "chevron.compact.right"
62+ ]
63+ , folder "layout" "Layout & Containers" "rectangle.split.3x1"
64+ [ leaf "box" "Box" "square.dashed"
65+ , leaf "splitview" "Split View" "rectangle.split.2x1"
66+ , leaf "tabview" "Tab View" "rectangle.topthird.inset.filled"
67+ ]
68+ , folder "overlays" "Overlays & Dialogs" "rectangle.on.rectangle"
69+ [ leaf "alert" "Alert" "exclamationmark.triangle"
70+ , leaf "sheet" "Sheet" "rectangle.bottomhalf.inset.filled"
71+ , leaf "popover" "Popover" "text.bubble"
72+ , leaf "contextmenu" "Context Menu" "contextualmenu.and.cursorarrow"
73+ , leaf "menu" "Menu" "list.bullet"
74+ ]
75+ , folder "dataviews" "Data Views" "tablecells"
76+ [ leaf "tableview" "Table View" "tablecells"
77+ , leaf "outlineview" "Outline View" "list.bullet.indent"
78+ ]
79+ , folder "dragdrop" "Drag & Drop / Files" "doc.badge.plus"
80+ [ leaf "dropzone" "Drop Zone" "square.and.arrow.down"
81+ , leaf "filepicker" "File Picker" "folder"
82+ ]
83+ , folder "animation" "Animation" "play.circle"
84+ [ leaf "springs" "Spring Physics" "sparkles"
85+ , leaf "rive" "Rive" "play.circle"
86+ ]
87+ , folder "system" "System Services" "gearshape.2"
88+ [ leaf "clipboard" "Clipboard" "doc.on.clipboard"
89+ , leaf "share" "Share" "square.and.arrow.up"
90+ , leaf "notifications" "Notifications" "bell"
91+ , leaf "sound" "Sound" "speaker.wave.2"
92+ , leaf "statusbar" "Status Bar" "menubar.rectangle"
93+ , leaf "quicklook" "Quick Look" "eye"
94+ , leaf "colorpanel" "Color Panel" "paintpalette"
95+ , leaf "fontpanel" "Font Panel" "textformat"
96+ , leaf "speech" "Speech Synthesis" "speaker.wave.2"
97+ ]
98+ , folder "aiml" "AI & ML" "brain"
99+ [ leaf "ocr" "OCR" "doc.text.viewfinder"
100+ , leaf "speechrecognition" "Speech Recognition" "mic"
101+ , leaf "naturallanguage" "Natural Language" "text.magnifyingglass"
102+ , leaf "camera" "Camera" "video"
103+ ]
104+ , folder "mapsdocs" "Maps & Documents" "map"
105+ [ leaf "mapview" "Map View" "map"
106+ , leaf "pdfview" "PDF View" "doc.richtext"
107+ ]
108+ , folder "webbrowser" "Web Browser" "globe"
109+ [ leaf "webview" "Web View" "globe"
110+ ]
111+ , folder "chat" "Chat" "bubble.left.and.bubble.right"
112+ [ leaf "matrix" "Matrix" "bubble.left.and.bubble.right"
113+ ]
114+ ]
115+
116+filterTree :: String -> Array OutlineItem -> Array OutlineItem
117+filterTree query items = do
118+ let q = toLower query
119+ let matchesLeaf (OutlineItem r) = contains (Pattern q) (toLower r.title)
120+ let
121+ go (OutlineItem r) = case r.children of
122+ [] -> if matchesLeaf (OutlineItem r) then [ OutlineItem r ] else []
123+ kids -> do
124+ let matched = concatMap go kids
125+ if null matched then [] else [ OutlineItem (r { children = matched }) ]
126+ concatMap go items
127+
128+outlineSidebar :: String -> (String -> Effect Unit) -> JSX
129+outlineSidebar _ setSelectedItem = sidebarComponent { setSelectedItem }
130+ where
131+ sidebarComponent = component "OutlineSidebar" \props -> React.do
132+ search /\ setSearch <- useState' ""
133+ let items = if search == "" then outlineTree else filterTree search outlineTree
134+ pure do
135+ view { style: tw "flex-1 pt-2" }
136+ [ nativeSearchField
137+ { text: search
138+ , placeholder: "Filter..."
139+ , onChangeText: setSearch
140+ , style: Style.style { height: 24.0, marginLeft: 8.0, marginRight: 8.0, marginBottom: 4.0 }
141+ }
142+ , nativeOutlineView
143+ { items
144+ , headerVisible: false
145+ , onSelectItem: props.setSelectedItem
146+ , style: tw "flex-1"
147+ }
148+ ]
149+
150+demoContent :: DemoProps -> String -> JSX
151+demoContent dp = case _ of
152+ "button" -> buttonDemo dp
153+ "switch" -> switchDemo dp
154+ "slider" -> sliderDemo dp
155+ "popup" -> popUpDemo dp
156+ "combobox" -> comboBoxDemo dp
157+ "stepper" -> stepperDemo dp
158+ "datepicker" -> datePickerDemo dp
159+ "colorwell" -> colorWellDemo dp
160+ "checkbox" -> checkboxDemo dp
161+ "radiobutton" -> radioButtonDemo dp
162+ "searchfield" -> searchFieldDemo dp
163+ "tokenfield" -> tokenFieldDemo dp
164+ "textfield" -> textFieldDemo dp
165+ "texteditor" -> textEditorDemo dp
166+ "image" -> imageDemo dp
167+ "animatedimage" -> animatedImageDemo dp
168+ "videoplayer" -> videoPlayerDemo dp
169+ "levelindicator" -> levelIndicatorDemo dp
170+ "progress" -> progressDemo dp
171+ "separator" -> separatorDemo dp
172+ "pathcontrol" -> pathControlDemo dp
173+ "box" -> boxDemo dp
174+ "splitview" -> splitViewDemo dp
175+ "tabview" -> tabViewDemo dp
176+ "alert" -> alertDemo dp
177+ "sheet" -> sheetDemo dp
178+ "popover" -> popoverDemo dp
179+ "contextmenu" -> contextMenuDemo dp
180+ "menu" -> menuDemo dp
181+ "tableview" -> tableViewDemo dp
182+ "outlineview" -> outlineViewDemo dp
183+ "dropzone" -> dropZoneDemo dp
184+ "filepicker" -> filePickerDemo dp
185+ "springs" -> springDemo dp
186+ "rive" -> riveDemo dp
187+ "clipboard" -> clipboardDemo dp
188+ "share" -> shareDemo dp
189+ "notifications" -> notificationsDemo dp
190+ "sound" -> soundDemo dp
191+ "statusbar" -> statusBarDemo dp
192+ "quicklook" -> quickLookDemo dp
193+ "colorpanel" -> colorPanelDemo dp
194+ "fontpanel" -> fontPanelDemo dp
195+ "speech" -> speechDemo dp
196+ "ocr" -> ocrDemo dp
197+ "speechrecognition" -> speechRecognitionDemo dp
198+ "naturallanguage" -> naturalLanguageDemo dp
199+ "camera" -> cameraDemo dp
200+ "mapview" -> mapViewDemo dp
201+ "pdfview" -> pdfViewDemo dp
202+ "webview" -> webViewDemo dp
203+ "matrix" -> chatDemo dp
204+ _ -> placeholder dp
205+
206+placeholder :: DemoProps -> JSX
207+placeholder dp =
208+ view { style: tw "flex-1 items-center justify-center" }
209+ [ text { style: tw "text-sm" <> Style.style { color: dp.dimFg } } "Select a component from the sidebar" ]
example-macos/src/Demo/Overlays.pursnew file+151
@@ -0,0 +1,151 @@
1+module Demo.Overlays
2+ ( alertDemo
3+ , sheetDemo
4+ , popoverDemo
5+ , contextMenuDemo
6+ , menuDemo
7+ ) where
8+
9+import Prelude
10+
11+import Demo.Shared (DemoProps, card, desc, label, scrollWrap, sectionTitle)
12+import React.Basic (JSX)
13+import React.Basic.Hooks (useState', (/\))
14+import React.Basic.Hooks as React
15+import Yoga.React (component)
16+import Yoga.React.Native (text, tw, view)
17+import Yoga.React.Native.MacOS.Alert (macosAlert)
18+import Yoga.React.Native.MacOS.Button (nativeButton)
19+import Yoga.React.Native.MacOS.ContextMenu (nativeContextMenu)
20+import Yoga.React.Native.MacOS.Menu (macosShowMenu)
21+import Yoga.React.Native.MacOS.Popover (nativePopover)
22+import Yoga.React.Native.MacOS.Sheet (nativeSheet)
23+import Yoga.React.Native.MacOS.Types as T
24+import Yoga.React.Native.Style as Style
25+
26+alertDemo :: DemoProps -> JSX
27+alertDemo = component "AlertDemo" \dp -> pure do
28+ scrollWrap dp
29+ [ sectionTitle dp.fg "Alert"
30+ , desc dp "Native alert dialog"
31+ , nativeButton
32+ { title: "Show Alert"
33+ , bezelStyle: T.push
34+ , onPress: macosAlert { style: T.warning, title: "Are you sure?", message: "This action cannot be undone.", buttons: [ "Cancel", "OK" ] }
35+ , style: Style.style { height: 24.0, width: 120.0 } <> tw "mb-4"
36+ }
37+ ]
38+
39+sheetDemo :: DemoProps -> JSX
40+sheetDemo = component "SheetDemo" \dp -> React.do
41+ visible /\ setVisible <- useState' false
42+ pure do
43+ scrollWrap dp
44+ [ sectionTitle dp.fg "Sheet"
45+ , desc dp "Modal sheet attached to the window"
46+ , nativeButton
47+ { title: "Show Sheet"
48+ , bezelStyle: T.push
49+ , onPress: setVisible true
50+ , style: Style.style { height: 24.0, width: 120.0 } <> tw "mb-2"
51+ }
52+ , nativeSheet
53+ { visible
54+ , onDismiss: setVisible false
55+ }
56+ [ view { style: tw "p-6" <> Style.style { width: 400.0, height: 200.0 } }
57+ [ text { style: tw "text-lg font-semibold mb-2" <> Style.style { color: dp.fg } } "Sheet Content"
58+ , text { style: tw "text-sm mb-4" <> Style.style { color: dp.dimFg } } "This is a native macOS sheet."
59+ , nativeButton
60+ { title: "Dismiss"
61+ , bezelStyle: T.push
62+ , onPress: setVisible false
63+ , style: Style.style { height: 24.0, width: 100.0 }
64+ }
65+ ]
66+ ]
67+ ]
68+
69+popoverDemo :: DemoProps -> JSX
70+popoverDemo = component "PopoverDemo" \dp -> React.do
71+ visible /\ setVisible <- useState' false
72+ pure do
73+ scrollWrap dp
74+ [ sectionTitle dp.fg "Popover"
75+ , desc dp "Click button to toggle a popover"
76+ , nativePopover
77+ { visible
78+ , preferredEdge: T.bottom
79+ , behavior: T.transient
80+ , onClose: setVisible false
81+ , style: tw "mb-2"
82+ }
83+ [ nativeButton
84+ { title: if visible then "Hide Popover" else "Show Popover"
85+ , bezelStyle: T.push
86+ , onPress: setVisible (not visible)
87+ , style: Style.style { height: 24.0, width: 140.0 }
88+ }
89+ , view { style: tw "p-4" <> Style.style { width: 200.0, height: 80.0 } }
90+ [ text { style: tw "text-sm font-semibold" <> Style.style { color: dp.fg } } "Popover Content"
91+ , text { style: tw "text-xs mt-1" <> Style.style { color: dp.dimFg } } "Click outside to dismiss"
92+ ]
93+ ]
94+ ]
95+
96+contextMenuDemo :: DemoProps -> JSX
97+contextMenuDemo = component "ContextMenuDemo" \dp -> React.do
98+ result /\ setResult <- useState' ""
99+ pure do
100+ scrollWrap dp
101+ [ sectionTitle dp.fg "Context Menu"
102+ , desc dp "Right-click the area below to open a context menu"
103+ , nativeContextMenu
104+ { items:
105+ [ { id: "cut", title: "Cut", sfSymbol: "scissors" }
106+ , { id: "copy", title: "Copy", sfSymbol: "doc.on.doc" }
107+ , { id: "paste", title: "Paste", sfSymbol: "doc.on.clipboard" }
108+ , { id: "sep", title: "-", sfSymbol: "" }
109+ , { id: "delete", title: "Delete", sfSymbol: "trash" }
110+ , { id: "selectAll", title: "Select All", sfSymbol: "selection.pin.in.out" }
111+ ]
112+ , onSelectItem: setResult
113+ , style: Style.style {}
114+ }
115+ ( card dp.cardBg
116+ [ view { style: tw "items-center justify-center" <> Style.style { minHeight: 80.0 } }
117+ [ text { style: tw "text-sm" <> Style.style { color: dp.fg } } "Right-click me!"
118+ , if result == "" then mempty
119+ else label dp.dimFg ("Selected: " <> result)
120+ ]
121+ ]
122+ )
123+ ]
124+
125+menuDemo :: DemoProps -> JSX
126+menuDemo = component "MenuDemo" \dp -> React.do
127+ result /\ setResult <- useState' ""
128+ pure do
129+ scrollWrap dp
130+ [ sectionTitle dp.fg "Menu"
131+ , desc dp "Imperative popup menu at mouse position"
132+ , view { style: tw "flex-row items-center mb-2" }
133+ [ nativeButton
134+ { title: "Show Menu"
135+ , bezelStyle: T.push
136+ , onPress: macosShowMenu
137+ { items:
138+ [ { title: "New File", id: "new" }
139+ , { title: "Open...", id: "open" }
140+ , { title: "-", id: "sep" }
141+ , { title: "Save", id: "save" }
142+ , { title: "Save As...", id: "saveAs" }
143+ ]
144+ , onSelectItem: \itemId -> setResult ("Menu: " <> itemId)
145+ }
146+ , style: Style.style { height: 24.0, width: 120.0 }
147+ }
148+ , if result == "" then mempty
149+ else text { style: tw "text-xs ml-3" <> Style.style { color: dp.dimFg } } result
150+ ]
151+ ]
example-macos/src/Demo/Shared.jsnew file+6
@@ -0,0 +1,6 @@
1+export const unsafeRound = (n) => Math.round(n);
2+export const getFieldJSON = (key) => (obj) => {
3+ var v = obj[key];
4+ if (v === undefined || v === null) return "";
5+ return JSON.stringify(v);
6+};
example-macos/src/Demo/Shared.pursnew file+53
@@ -0,0 +1,53 @@
1+module Demo.Shared
2+ ( DemoProps
3+ , scrollWrap
4+ , sectionTitle
5+ , desc
6+ , card
7+ , label
8+ , round
9+ , getFieldJSON
10+ ) where
11+
12+import Prelude
13+
14+import React.Basic (JSX)
15+import Yoga.React.Native (text, tw, view)
16+import Yoga.React.Native.MacOS.ScrollView (nativeScrollView)
17+import Yoga.React.Native.Style as Style
18+
19+type DemoProps =
20+ { fg :: String
21+ , dimFg :: String
22+ , cardBg :: String
23+ , bg :: String
24+ , isDark :: Boolean
25+ }
26+
27+scrollWrap :: DemoProps -> Array JSX -> JSX
28+scrollWrap _ children =
29+ nativeScrollView { style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
30+ (view { style: tw "px-4 pb-4" } children)
31+
32+sectionTitle :: String -> String -> JSX
33+sectionTitle color title =
34+ text { style: tw "text-sm font-semibold mt-4 mb-1" <> Style.style { color } } title
35+
36+desc :: DemoProps -> String -> JSX
37+desc dp str =
38+ text { style: tw "text-xs mb-2" <> Style.style { color: dp.dimFg } } str
39+
40+card :: String -> Array JSX -> JSX
41+card bg children =
42+ view { style: tw "rounded-lg p-3 mb-2" <> Style.style { backgroundColor: bg, overflow: "hidden" } }
43+ children
44+
45+label :: String -> String -> JSX
46+label color str =
47+ text { style: tw "ml-3 text-sm" <> Style.style { color } } str
48+
49+round :: Number -> Int
50+round n = unsafeRound n
51+
52+foreign import unsafeRound :: Number -> Int
53+foreign import getFieldJSON :: String -> forall r. r -> String
example-macos/src/Demo/SpringAnimation.pursnew file+123
@@ -0,0 +1,123 @@
1+module Demo.SpringAnimation (springDemo) where
2+
3+import Prelude
4+
5+import Demo.Shared (DemoProps, card, desc, scrollWrap, sectionTitle)
6+import React.Basic (JSX)
7+import React.Basic.Hooks (useState', (/\))
8+import React.Basic.Hooks as React
9+import Yoga.React (component)
10+import Yoga.React.Native (text, tw, view)
11+import Yoga.React.Native.Animated (animatedView, animatedView_, interpolate, useSpring)
12+import React.Basic.Events (handler_)
13+import Yoga.React.Native.Pressable (pressable)
14+import Yoga.React.Native.MacOS.Button (nativeButton)
15+import Yoga.React.Native.MacOS.Types as T
16+import Yoga.React.Native.Style as Style
17+
18+springDemo :: DemoProps -> JSX
19+springDemo = component "SpringDemo" \dp -> React.do
20+ pressed /\ setPressed <- useState' false
21+ toggled /\ setToggled <- useState' false
22+ fadeIn /\ setFadeIn <- useState' false
23+ showItems /\ setShowItems <- useState' false
24+ let scaleTarget = if pressed then 0.92 else 1.0
25+ scale <- useSpring scaleTarget { stiffness: 400.0, damping: 15.0 }
26+ let toggleTarget = if toggled then 200.0 else 0.0
27+ translateX <- useSpring toggleTarget { stiffness: 180.0, damping: 14.0 }
28+ let fadeTarget = if fadeIn then 1.0 else 0.0
29+ opacity <- useSpring fadeTarget { stiffness: 120.0, damping: 20.0 }
30+ item0 <- useSpring (if showItems then 1.0 else 0.0) { stiffness: 200.0, damping: 18.0 }
31+ item1 <- useSpring (if showItems then 1.0 else 0.0) { stiffness: 180.0, damping: 18.0, delay: 50 }
32+ item2 <- useSpring (if showItems then 1.0 else 0.0) { stiffness: 160.0, damping: 18.0, delay: 100 }
33+ item3 <- useSpring (if showItems then 1.0 else 0.0) { stiffness: 140.0, damping: 18.0, delay: 150 }
34+ pure do
35+ scrollWrap dp
36+ [ sectionTitle dp.fg "Spring Animations"
37+ , desc dp "Declarative spring physics using useSpring"
38+
39+ , sectionTitle dp.fg "Scale on Press"
40+ , desc dp "Hold down the card — it springs to 0.92x, release to bounce back"
41+ , pressable
42+ { onPressIn: handler_ (setPressed true)
43+ , onPressOut: handler_ (setPressed false)
44+ }
45+ [ animatedView
46+ { style: Style.style { transform: [ { scale } ] } }
47+ [ card dp.cardBg
48+ [ view { style: tw "items-center py-4" }
49+ [ text { style: tw "text-sm font-semibold" <> Style.style { color: dp.fg } }
50+ (if pressed then "Pressing..." else "Hold me!")
51+ , text { style: tw "text-xs mt-1" <> Style.style { color: dp.dimFg } }
52+ "useSpring { stiffness: 400, damping: 15 }"
53+ ]
54+ ]
55+ ]
56+ ]
57+
58+ , sectionTitle dp.fg "Spring Toggle"
59+ , desc dp "Slides a box 200px to the right with a spring"
60+ , nativeButton
61+ { title: if toggled then "Spring Left" else "Spring Right"
62+ , bezelStyle: T.push
63+ , onPress: setToggled (not toggled)
64+ , style: Style.style { height: 24.0, width: 140.0, marginBottom: 8.0 }
65+ }
66+ , animatedView_
67+ { style: tw "rounded-lg"
68+ <> Style.style
69+ { width: 60.0
70+ , height: 60.0
71+ , backgroundColor: "#007AFF"
72+ , transform: [ { translateX } ]
73+ }
74+ }
75+
76+ , sectionTitle dp.fg "Fade In"
77+ , desc dp "Springs opacity from 0 to 1"
78+ , nativeButton
79+ { title: if fadeIn then "Fade Out" else "Fade In"
80+ , bezelStyle: T.push
81+ , onPress: setFadeIn (not fadeIn)
82+ , style: Style.style { height: 24.0, width: 120.0, marginBottom: 8.0 }
83+ }
84+ , animatedView
85+ { style: Style.style { opacity } }
86+ [ card dp.cardBg
87+ [ view { style: tw "items-center py-3" }
88+ [ text { style: tw "text-sm" <> Style.style { color: dp.fg } } "I fade with a spring curve"
89+ ]
90+ ]
91+ ]
92+
93+ , sectionTitle dp.fg "Staggered Items"
94+ , desc dp "Each item springs in with increasing delay"
95+ , nativeButton
96+ { title: if showItems then "Hide Items" else "Show Items"
97+ , bezelStyle: T.push
98+ , onPress: setShowItems (not showItems)
99+ , style: Style.style { height: 24.0, width: 120.0, marginBottom: 8.0 }
100+ }
101+ , staggeredItem dp "PureScript" "#007AFF" item0
102+ , staggeredItem dp "React Native" "#34C759" item1
103+ , staggeredItem dp "macOS" "#FF9500" item2
104+ , staggeredItem dp "Spring Physics" "#AF52DE" item3
105+ ]
106+ where
107+ staggeredItem _ title color anim =
108+ animatedView
109+ { style: Style.style
110+ { opacity: anim
111+ , transform: [ { translateX: slideX } ]
112+ , marginBottom: 4.0
113+ }
114+ }
115+ [ view
116+ { style: tw "rounded-lg px-4 py-2 flex-row items-center"
117+ <> Style.style { backgroundColor: color }
118+ }
119+ [ text { style: tw "text-sm font-semibold" <> Style.style { color: "#FFFFFF" } } title
120+ ]
121+ ]
122+ where
123+ slideX = interpolate anim { inputRange: [ 0.0, 1.0 ], outputRange: [ -30.0, 0.0 ], extrapolate: "clamp" }
example-macos/src/Demo/SystemServices.pursnew file+207
@@ -0,0 +1,207 @@
1+module Demo.SystemServices
2+ ( clipboardDemo
3+ , shareDemo
4+ , notificationsDemo
5+ , soundDemo
6+ , statusBarDemo
7+ , quickLookDemo
8+ , colorPanelDemo
9+ , fontPanelDemo
10+ , speechDemo
11+ ) where
12+
13+import Prelude
14+
15+import Demo.Shared (DemoProps, desc, scrollWrap, sectionTitle)
16+import React.Basic (JSX)
17+
18+import React.Basic.Hooks (useState', (/\))
19+import React.Basic.Hooks as React
20+import Yoga.React (component)
21+import Yoga.React.Native (tw, view)
22+import Yoga.React.Native.MacOS.Button (nativeButton)
23+import Yoga.React.Native.MacOS.ColorPanel (macosShowColorPanel)
24+import Yoga.React.Native.MacOS.FontPanel (macosShowFontPanel)
25+import Yoga.React.Native.MacOS.Pasteboard (copyToClipboard)
26+import Yoga.React.Native.MacOS.QuickLook (macosQuickLook)
27+import Yoga.React.Native.MacOS.ShareService (macosShare)
28+import Yoga.React.Native.MacOS.Sound (macosBeep, macosPlaySound)
29+import Yoga.React.Native.MacOS.SpeechSynthesizer (say, stopSpeaking)
30+import Yoga.React.Native.MacOS.StatusBarItem (macosRemoveStatusBarItem, macosSetStatusBarItem)
31+import Yoga.React.Native.MacOS.TextField (nativeTextField)
32+import Yoga.React.Native.MacOS.Types as T
33+import Yoga.React.Native.MacOS.UserNotification (macosNotify)
34+import Yoga.React.Native.Style as Style
35+
36+clipboardDemo :: DemoProps -> JSX
37+clipboardDemo = component "ClipboardDemo" \dp -> pure do
38+ scrollWrap dp
39+ [ sectionTitle dp.fg "Clipboard"
40+ , desc dp "Read and write the system clipboard"
41+ , view { style: tw "flex-row items-center mb-2" }
42+ [ nativeButton
43+ { title: "Copy 'Hello'"
44+ , bezelStyle: T.push
45+ , onPress: copyToClipboard "Hello from PureScript!"
46+ , style: Style.style { height: 24.0, width: 120.0 }
47+ }
48+ ]
49+ ]
50+
51+shareDemo :: DemoProps -> JSX
52+shareDemo = component "ShareDemo" \dp -> pure do
53+ scrollWrap dp
54+ [ sectionTitle dp.fg "Share"
55+ , desc dp "System share picker"
56+ , nativeButton
57+ { title: "Share Text"
58+ , bezelStyle: T.push
59+ , onPress: macosShare [ "Hello from PureScript React Native!" ]
60+ , style: Style.style { height: 24.0, width: 120.0 } <> tw "mb-2"
61+ }
62+ ]
63+
64+notificationsDemo :: DemoProps -> JSX
65+notificationsDemo = component "NotificationsDemo" \dp -> pure do
66+ scrollWrap dp
67+ [ sectionTitle dp.fg "Notifications"
68+ , desc dp "System notifications (requires permission)"
69+ , nativeButton
70+ { title: "Send Notification"
71+ , bezelStyle: T.push
72+ , onPress: macosNotify { title: "PureScript", body: "Hello from React Native macOS!" }
73+ , style: Style.style { height: 24.0, width: 160.0 } <> tw "mb-2"
74+ }
75+ ]
76+
77+soundDemo :: DemoProps -> JSX
78+soundDemo = component "SoundDemo" \dp -> pure do
79+ scrollWrap dp
80+ [ sectionTitle dp.fg "Sound"
81+ , desc dp "Play system sounds"
82+ , view { style: tw "flex-row items-center mb-2" }
83+ [ nativeButton
84+ { title: "Glass"
85+ , bezelStyle: T.push
86+ , onPress: macosPlaySound "Glass"
87+ , style: Style.style { height: 24.0, width: 80.0 }
88+ }
89+ , nativeButton
90+ { title: "Ping"
91+ , bezelStyle: T.push
92+ , onPress: macosPlaySound "Ping"
93+ , style: Style.style { height: 24.0, width: 80.0, marginLeft: 8.0 }
94+ }
95+ , nativeButton
96+ { title: "Beep"
97+ , bezelStyle: T.push
98+ , onPress: macosBeep
99+ , style: Style.style { height: 24.0, width: 80.0, marginLeft: 8.0 }
100+ }
101+ ]
102+ ]
103+
104+statusBarDemo :: DemoProps -> JSX
105+statusBarDemo = component "StatusBarDemo" \dp -> React.do
106+ active /\ setActive <- useState' false
107+ pure do
108+ scrollWrap dp
109+ [ sectionTitle dp.fg "Status Bar"
110+ , desc dp "Menu bar icon with dropdown"
111+ , view { style: tw "flex-row items-center mb-4" }
112+ [ nativeButton
113+ { title: if active then "Remove" else "Add to Menu Bar"
114+ , bezelStyle: T.push
115+ , onPress:
116+ if active then do
117+ macosRemoveStatusBarItem
118+ setActive false
119+ else do
120+ macosSetStatusBarItem
121+ { title: ""
122+ , sfSymbol: "swift"
123+ , menuItems:
124+ [ { id: "about", title: "About PureScript RN" }
125+ , { id: "sep", title: "-" }
126+ , { id: "quit", title: "Quit" }
127+ ]
128+ }
129+ setActive true
130+ , style: Style.style { height: 24.0, width: 180.0 }
131+ }
132+ ]
133+ ]
134+
135+quickLookDemo :: DemoProps -> JSX
136+quickLookDemo = component "QuickLookDemo" \dp -> pure do
137+ scrollWrap dp
138+ [ sectionTitle dp.fg "Quick Look"
139+ , desc dp "Preview files with QLPreviewPanel"
140+ , view { style: tw "flex-row items-center mb-4" }
141+ [ nativeButton
142+ { title: "Preview /etc/hosts"
143+ , bezelStyle: T.push
144+ , onPress: macosQuickLook "/etc/hosts"
145+ , style: Style.style { height: 24.0, width: 180.0 }
146+ }
147+ ]
148+ ]
149+
150+colorPanelDemo :: DemoProps -> JSX
151+colorPanelDemo = component "ColorPanelDemo" \dp -> pure do
152+ scrollWrap dp
153+ [ sectionTitle dp.fg "Color Picker"
154+ , desc dp "System color panel (NSColorPanel)"
155+ , view { style: tw "flex-row items-center mb-4" }
156+ [ nativeButton
157+ { title: "Show Color Panel"
158+ , bezelStyle: T.push
159+ , onPress: macosShowColorPanel
160+ , style: Style.style { height: 24.0, width: 180.0 }
161+ }
162+ ]
163+ ]
164+
165+fontPanelDemo :: DemoProps -> JSX
166+fontPanelDemo = component "FontPanelDemo" \dp -> pure do
167+ scrollWrap dp
168+ [ sectionTitle dp.fg "Font Panel"
169+ , desc dp "System font panel (NSFontPanel)"
170+ , view { style: tw "flex-row items-center mb-4" }
171+ [ nativeButton
172+ { title: "Show Font Panel"
173+ , bezelStyle: T.push
174+ , onPress: macosShowFontPanel
175+ , style: Style.style { height: 24.0, width: 180.0 }
176+ }
177+ ]
178+ ]
179+
180+speechDemo :: DemoProps -> JSX
181+speechDemo = component "SpeechDemo" \dp -> React.do
182+ txt /\ setTxt <- useState' "Hello from PureScript React Native on macOS!"
183+ pure do
184+ scrollWrap dp
185+ [ sectionTitle dp.fg "Speech Synthesis"
186+ , desc dp "Text-to-speech (NSSpeechSynthesizer)"
187+ , nativeTextField
188+ { text: txt
189+ , placeholder: "Text to speak..."
190+ , onChangeText: setTxt
191+ , style: Style.style { height: 24.0 } <> tw "mb-2"
192+ }
193+ , view { style: tw "flex-row items-center mb-4" }
194+ [ nativeButton
195+ { title: "Speak"
196+ , bezelStyle: T.push
197+ , onPress: say txt
198+ , style: Style.style { height: 24.0, width: 100.0 }
199+ }
200+ , nativeButton
201+ { title: "Stop"
202+ , bezelStyle: T.push
203+ , onPress: stopSpeaking
204+ , style: Style.style { height: 24.0, width: 100.0, marginLeft: 8.0 }
205+ }
206+ ]
207+ ]
example-macos/src/Demo/TextEditing.pursnew file+44
@@ -0,0 +1,44 @@
1+module Demo.TextEditing (textFieldDemo, textEditorDemo) where
2+
3+import Prelude
4+
5+import Demo.Shared (DemoProps, card, label, scrollWrap, sectionTitle)
6+import React.Basic (JSX)
7+import React.Basic.Hooks (useState', (/\))
8+import React.Basic.Hooks as React
9+import Yoga.React (component)
10+import Yoga.React.Native (tw, view)
11+import Yoga.React.Native.MacOS.TextField (nativeTextField)
12+import Yoga.React.Native.MacOS.TextEditor (nativeTextEditor)
13+import Yoga.React.Native.Style as Style
14+
15+textFieldDemo :: DemoProps -> JSX
16+textFieldDemo = component "TextFieldDemo" \dp -> React.do
17+ txt /\ setTxt <- useState' ""
18+ pure do
19+ scrollWrap dp
20+ [ sectionTitle dp.fg "Text Field"
21+ , card dp.cardBg
22+ [ nativeTextField
23+ { placeholder: "Type something..."
24+ , search: true
25+ , text: txt
26+ , onChangeText: setTxt
27+ , style: Style.style { height: 24.0 }
28+ }
29+ , if txt == "" then mempty
30+ else label dp.dimFg ("You typed: " <> txt)
31+ ]
32+ ]
33+
34+textEditorDemo :: DemoProps -> JSX
35+textEditorDemo = component "TextEditorDemo" \dp -> pure do
36+ view { style: tw "flex-1 px-4" }
37+ [ sectionTitle dp.fg "Rich Text Editor"
38+ , nativeTextEditor
39+ { text: "Welcome to the PureScript-driven native text editor.\n\nThis uses NSTextView with rich text support and a formatting ruler."
40+ , richText: true
41+ , showsRuler: true
42+ , style: tw "flex-1" <> Style.style { minHeight: 300.0 }
43+ }
44+ ]
example-macos/src/Demo/WebBrowser.pursnew file+35
@@ -0,0 +1,35 @@
1+module Demo.WebBrowser (webViewDemo) where
2+
3+import Prelude
4+
5+import Demo.Shared (DemoProps, sectionTitle)
6+import React.Basic (JSX)
7+import React.Basic.Hooks (useState', (/\))
8+import React.Basic.Hooks as React
9+import Yoga.React (component)
10+import Yoga.React.Native (tw, view)
11+import Yoga.React.Native.MacOS.TextField (nativeTextField)
12+import Yoga.React.Native.MacOS.WebView (nativeWebView)
13+import Yoga.React.Native.Style as Style
14+
15+webViewDemo :: DemoProps -> JSX
16+webViewDemo = component "WebViewDemo" \dp -> React.do
17+ url /\ setUrl <- useState' "https://pursuit.purescript.org"
18+ urlBar /\ setUrlBar <- useState' "https://pursuit.purescript.org"
19+ pure do
20+ view { style: tw "flex-1 px-4" }
21+ [ sectionTitle dp.fg "Web Browser"
22+ , nativeTextField
23+ { text: urlBar
24+ , placeholder: "Enter URL..."
25+ , search: false
26+ , onChangeText: setUrlBar
27+ , onSubmit: setUrl
28+ , style: Style.style { height: 24.0, marginBottom: 8.0 }
29+ }
30+ , nativeWebView
31+ { url
32+ , onFinishLoad: setUrlBar
33+ , style: tw "flex-1" <> Style.style { minHeight: 400.0 }
34+ }
35+ ]
example-macos/src/Main.jsdeleted−27
@@ -1,27 +0,0 @@
1−export const getField = (key) => (obj) => obj[key] ?? 0;
2−export const getFieldInt = (key) => (obj) => obj[key] | 0;
3−export const getFieldStr = (key) => (obj) => String(obj[key] ?? "");
4−export const getFieldBool = (key) => (obj) => Boolean(obj[key]);
5−export const getFieldArray = (key) => (obj) =>
6− Array.isArray(obj[key]) ? obj[key].map(String) : [];
7−export const unsafeRound = (n) => Math.round(n);
8−export const isSingleEmoji = (str) => {
9− var trimmed = str.trim();
10− var re =
11− /^(?:\p{Emoji_Presentation}|\p{Emoji}\uFE0F)(?:\u200D(?:\p{Emoji_Presentation}|\p{Emoji}\uFE0F))*$/u;
12− return trimmed.length <= 10 && re.test(trimmed);
13−};
14−export const colonQuery = (str) => {
15− var idx = str.lastIndexOf(":");
16− if (idx === -1) return "";
17− return str.slice(idx + 1).toLowerCase();
18−};
19−export const getFieldJSON = (key) => (obj) => {
20− var v = obj[key];
21− if (v === undefined || v === null) return "";
22− return JSON.stringify(v);
23−};
24−export const charCodes = (str) => Array.from(str).map((c) => c.charCodeAt(0));
25−export const abs = (n) => Math.abs(n);
26−export const setInterval = (ms) => (fn) => () => globalThis.setInterval(fn, ms);
27−export const clearInterval = (id) => () => globalThis.clearInterval(id);
example-macos/src/Main.purs+17−1603
@@ -2,81 +2,21 @@ module Main where
22
33import Prelude
44
5−import Data.Array (mapWithIndex, length, filter, null, (!!))
5+import Data.Maybe (Maybe(..))
66import Data.Nullable (toNullable)
7−import Data.Maybe (Maybe(..), fromMaybe)
8−import Data.String (take, joinWith)
9−import Data.Foldable (for_, foldl)
10−import Data.Traversable (for)
7+import Demo.Navigation (demoContent, outlineSidebar)
8+import Demo.Shared (DemoProps)
119import Effect (Effect)
12−import Data.Either (Either(..))
13−import Effect.Aff (launchAff_, try)
14−import Effect.Class (liftEffect)
1510import React.Basic (JSX)
16−import React.Basic.Events (EventHandler, handler, handler_, unsafeEventFn)
17−import React.Basic.Hooks (useState, useState', useEffect, useRef, readRef, writeRef, (/\))
11+import React.Basic.Hooks (useState', (/\))
1812import React.Basic.Hooks as React
1913import Yoga.React (component)
20−import Yoga.React.Native (nativeEvent, registerComponent, safeAreaView, text, tw, view)
14+import Yoga.React.Native (registerComponent, safeAreaView, tw, view)
2115import Yoga.React.Native.Appearance (useColorScheme)
22−import Yoga.React.Native.MacOS.Button (nativeButton)
23−import Yoga.React.Native.MacOS.ColorWell (nativeColorWell)
24−import Yoga.React.Native.MacOS.DatePicker (nativeDatePicker)
25−import Yoga.React.Native.MacOS.LevelIndicator (nativeLevelIndicator)
26−import Yoga.React.Native.MacOS.PopUp (nativePopUp)
27−import Yoga.React.Native.MacOS.Progress (nativeProgress)
28−import Yoga.React.Native.MacOS.Slider (nativeSlider)
29−import Yoga.React.Native.MacOS.Switch (nativeSwitch)
30−import Yoga.React.Native.MacOS.TextField (nativeTextField)
31−import Yoga.React.Native.MacOS.TextEditor (nativeTextEditor)
32−import Yoga.React.Native.MacOS.WebView (nativeWebView)
33−import Yoga.React.Native.MacOS.ScrollView (nativeScrollView)
34−import Yoga.React.Native.MacOS.Rive (nativeRiveView_)
35−import Yoga.React.Native.MacOS.Toolbar (nativeToolbar)
36−import Yoga.React.Native.MacOS.VisualEffect (nativeVisualEffect)
3716import Yoga.React.Native.MacOS.Sidebar (sidebarLayout)
38−import Yoga.React.Native.MacOS.ContextMenu (nativeContextMenu)
39−
40−import Yoga.React.Native.MacOS.FilePicker (nativeFilePicker)
41−import Yoga.React.Native.MacOS.VideoPlayer (nativeVideoPlayer)
42−import Yoga.React.Native.MacOS.AnimatedImage (nativeAnimatedImage)
43−import Yoga.React.Native.MacOS.PatternBackground (nativePatternBackground)
44−import Yoga.React.Native.MacOS.SplitView (nativeSplitView)
45−import Yoga.React.Native.MacOS.TabView (nativeTabView)
46−import Yoga.React.Native.MacOS.ComboBox (nativeComboBox)
47−import Yoga.React.Native.MacOS.Stepper (nativeStepper)
48−import Yoga.React.Native.MacOS.Box (nativeBox)
49−import Yoga.React.Native.MacOS.Popover (nativePopover)
50−import Yoga.React.Native.MacOS.Image (nativeImage)
51−import Yoga.React.Native.MacOS.Alert (macosAlert)
52−import Yoga.React.Native.MacOS.Checkbox (nativeCheckbox)
53−import Yoga.React.Native.MacOS.RadioButton (nativeRadioButton)
54−import Yoga.React.Native.MacOS.SearchField (nativeSearchField)
55−import Yoga.React.Native.MacOS.TokenField (nativeTokenField)
56−import Yoga.React.Native.MacOS.Separator (nativeSeparator)
57−import Yoga.React.Native.MacOS.HelpButton (nativeHelpButton)
58−import Yoga.React.Native.MacOS.PathControl (nativePathControl)
59−import Yoga.React.Native.MacOS.Sheet (nativeSheet)
60−import Yoga.React.Native.MacOS.TableView (nativeTableView)
61−import Yoga.React.Native.MacOS.Menu (macosShowMenu)
62−import Yoga.React.Native.MacOS.Pasteboard (copyToClipboard)
63−import Foreign (unsafeToForeign)
64−import Yoga.React.Native.MacOS.OutlineView (nativeOutlineView)
65−import Yoga.React.Native.MacOS.ShareService (macosShare)
66−import Yoga.React.Native.MacOS.UserNotification (macosNotify)
67−import Yoga.React.Native.MacOS.Sound (macosPlaySound, macosBeep)
68−import Yoga.React.Native.MacOS.StatusBarItem (macosSetStatusBarItem, macosRemoveStatusBarItem)
69−import Yoga.React.Native.MacOS.MapView (nativeMapView)
70−import Yoga.React.Native.MacOS.PDFView (nativePDFView)
71−import Yoga.React.Native.MacOS.QuickLook (macosQuickLook)
72−import Yoga.React.Native.MacOS.SpeechSynthesizer (macosSay, macosStopSpeaking)
73−import Yoga.React.Native.MacOS.ColorPanel (macosShowColorPanel)
74−import Yoga.React.Native.MacOS.FontPanel (macosShowFontPanel)
75−import Yoga.React.Native.MacOS.OCR (macosOCR)
76−import Yoga.React.Native.MacOS.SpeechRecognition (macosStartListening, macosStopListening, macosGetTranscript)
77−import Yoga.React.Native.MacOS.NaturalLanguage (macosDetectLanguage, macosAnalyzeSentiment, macosTokenize)
17+import Yoga.React.Native.MacOS.Toolbar (nativeToolbar)
7818import Yoga.React.Native.MacOS.Types as T
79−import Yoga.React.Native.Matrix as Matrix
19+import Yoga.React.Native.MacOS.VisualEffect (nativeVisualEffect)
8020import Yoga.React.Native.Style as Style
8121
8222main :: Effect Unit
@@ -84,23 +24,14 @@ main = registerComponent "YogaReactExample" \_ -> app {}
8424
8525app :: {} -> JSX
8626app = component "App" \_ -> React.do
87− activeTab /\ setActiveTab <- useState' "controls"
88− sliderValue /\ setSliderValue <- useState' 50.0
89− switchOn /\ setSwitchOn <- useState' false
90− selectedColor /\ setSelectedColor <- useState' "#FF6600"
91− popUpIndex /\ setPopUpIndex <- useState' 0
92− popUpTitle /\ setPopUpTitle <- useState' "Small"
93− searchText /\ setSearchText <- useState' ""
94− buttonStatus /\ setButtonStatus <- useState' "Ready"
95− dateText /\ setDateText <- useState' ""
96− browserUrl /\ setBrowserUrl <- useState' "https://pursuit.purescript.org"
97− urlBarText /\ setUrlBarText <- useState' "https://pursuit.purescript.org"
27+ selectedItem /\ setSelectedItem <- useState' "button"
9828 colorScheme <- useColorScheme
9929 let isDark = toNullable (Just "dark") == colorScheme
10030 let fg = if isDark then "#FFFFFF" else "#000000"
10131 let dimFg = if isDark then "#999999" else "#666666"
10232 let cardBg = if isDark then "#2A2A2A" else "#F0F0F0"
10333 let bg = if isDark then "#1E1E1E" else "#FFFFFF"
34+ let dp = { fg, dimFg, cardBg, bg, isDark } :: DemoProps
10435 pure do
10536 nativeVisualEffect
10637 { materialName: T.windowBackground
@@ -109,1535 +40,18 @@ app = component "App" \_ -> React.do
10940 ( safeAreaView { style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
11041 ( view { style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
11142 [ nativeToolbar
112− { items:
113− [ { id: "controls", label: "Controls", sfSymbol: "slider.horizontal.3" }
114− , { id: "editor", label: "Editor", sfSymbol: "doc.richtext" }
115− , { id: "browser", label: "Browser", sfSymbol: "globe" }
116− , { id: "rive", label: "Rive", sfSymbol: "play.circle" }
117− , { id: "system", label: "System", sfSymbol: "gearshape.2" }
118− , { id: "ai", label: "AI", sfSymbol: "brain" }
119− , { id: "chat", label: "Chat", sfSymbol: "bubble.left.and.bubble.right" }
120− ]
121− , selectedItem: activeTab
43+ { items: []
44+ , selectedItem: ""
12245 , toolbarStyle: T.unified
12346 , windowTitle: "PureScript React Native"
124− , onSelectItem: extractString "itemId" setActiveTab
47+ , onSelectItem: \_ -> pure unit
12548 , style: Style.style { height: 0.0, width: 0.0 }
12649 }
127− , view { style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
128− [ if activeTab == "controls" then controlsTab
129− { sliderValue
130− , setSliderValue
131− , switchOn
132− , setSwitchOn
133− , selectedColor
134− , setSelectedColor
135− , popUpIndex
136− , setPopUpIndex
137− , popUpTitle
138− , setPopUpTitle
139− , searchText
140− , setSearchText
141− , buttonStatus
142− , setButtonStatus
143− , dateText
144− , setDateText
145− , fg
146− , dimFg
147− , cardBg
148− }
149− else if activeTab == "editor" then editorTab { fg }
150− else if activeTab == "browser" then browserTab
151− { browserUrl
152− , setBrowserUrl
153− , urlBarText
154− , setUrlBarText
155− , fg
156− , dimFg
157− }
158− else if activeTab == "rive" then riveTab { fg, bg }
159− else if activeTab == "system" then systemTab { fg, dimFg, cardBg, bg }
160− else if activeTab == "ai" then aiTab { fg, dimFg, cardBg, bg }
161− else chatTab { fg, dimFg, cardBg, bg, isDark }
162− ]
163− ]
164− )
165− )
166−
167−-- Helpers for extracting native event values
168−extractNumber :: String -> (Number -> Effect Unit) -> EventHandler
169−extractNumber key cb = handler (nativeEvent >>> unsafeEventFn \e -> getField key e) cb
170−
171−extractInt :: String -> (Int -> Effect Unit) -> EventHandler
172−extractInt key cb = handler (nativeEvent >>> unsafeEventFn \e -> getFieldInt key e) cb
173−
174−extractString :: String -> (String -> Effect Unit) -> EventHandler
175−extractString key cb = handler (nativeEvent >>> unsafeEventFn \e -> getFieldStr key e) cb
176−
177−extractBool :: String -> (Boolean -> Effect Unit) -> EventHandler
178−extractBool key cb = handler (nativeEvent >>> unsafeEventFn \e -> getFieldBool key e) cb
179−
180−foreign import getField :: String -> forall r. r -> Number
181−foreign import getFieldInt :: String -> forall r. r -> Int
182−foreign import getFieldStr :: String -> forall r. r -> String
183−foreign import getFieldBool :: String -> forall r. r -> Boolean
184−foreign import getFieldArray :: String -> forall r. r -> Array String
185−
186−-- Tab 0: Controls
187−type ControlsProps =
188− { sliderValue :: Number
189− , setSliderValue :: Number -> Effect Unit
190− , switchOn :: Boolean
191− , setSwitchOn :: Boolean -> Effect Unit
192− , selectedColor :: String
193− , setSelectedColor :: String -> Effect Unit
194− , popUpIndex :: Int
195− , setPopUpIndex :: Int -> Effect Unit
196− , popUpTitle :: String
197− , setPopUpTitle :: String -> Effect Unit
198− , searchText :: String
199− , setSearchText :: String -> Effect Unit
200− , buttonStatus :: String
201− , setButtonStatus :: String -> Effect Unit
202− , dateText :: String
203− , setDateText :: String -> Effect Unit
204− , fg :: String
205− , dimFg :: String
206− , cardBg :: String
207− }
208−
209−sidebarItem :: String -> String -> String -> Boolean -> (String -> Effect Unit) -> JSX
210−sidebarItem _ sfSymbolName title selected setCategory =
211− nativeButton
212− { title
213− , sfSymbol: sfSymbolName
214− , bezelStyle: T.toolbar
215− , primary: selected
216− , onPress: handler_ (setCategory title)
217− , style: Style.style { height: 28.0, marginHorizontal: 8.0, marginVertical: 1.0 }
218− }
219−
220−controlsTab :: ControlsProps -> JSX
221−controlsTab = component "ControlsTab" \p -> React.do
222− category /\ setCategory <- useState' "All"
223− let categories = [ "All", "Button", "Switch", "Slider", "Pop Up", "Color", "Date", "Text" ]
224− let
225− sidebar = view { style: tw "pt-2" }
226− (categories <#> \cat -> sidebarItem p.fg "" cat (category == cat) setCategory)
227− let show' name = category == "All" || category == name
228− let
229− buttonSection =
230− [ sectionTitle p.fg "Button"
231− , card p.cardBg
232− [ view { style: tw "flex-row items-center" }
233− [ nativeButton
234− { title: "Say Hello"
235− , sfSymbol: "hand.wave"
236− , bezelStyle: T.push
237− , primary: true
238− , onPress: handler_ (p.setButtonStatus "Hello from PureScript!")
239− , style: Style.style { height: 24.0, width: 140.0 }
240− }
241− , nativeButton
242− { title: "Reset"
243− , sfSymbol: "arrow.counterclockwise"
244− , bezelStyle: T.push
245− , onPress: handler_ (p.setButtonStatus "Ready")
246− , style: Style.style { height: 24.0, width: 100.0, marginLeft: 8.0 }
247− }
248− , label p.dimFg p.buttonStatus
249− ]
250− ]
251− ]
252− let
253− switchSection =
254− [ sectionTitle p.fg "Switch"
255− , card p.cardBg
256− [ view { style: tw "flex-row items-center" }
257− [ nativeSwitch
258− { on: p.switchOn
259− , onChange: extractBool "on" p.setSwitchOn
260− , style: Style.style { height: 24.0, width: 48.0 }
261− }
262− , label p.dimFg (if p.switchOn then "On" else "Off")
263− ]
264− ]
265− ]
266− let
267− sliderSection =
268− [ sectionTitle p.fg "Slider + Level Indicator + Progress"
269− , card p.cardBg
270− [ nativeSlider
271− { value: p.sliderValue
272− , minValue: 0.0
273− , maxValue: 100.0
274− , numberOfTickMarks: 11
275− , onChange: extractNumber "value" p.setSliderValue
276− , style: Style.style { height: 24.0 }
277− }
278− , label p.dimFg ("Value: " <> show (round p.sliderValue) <> " / 100")
279− , nativeLevelIndicator
280− { value: p.sliderValue
281− , minValue: 0.0
282− , maxValue: 100.0
283− , warningValue: 70.0
284− , criticalValue: 90.0
285− , style: Style.style { height: 18.0, marginTop: 8.0 }
286− }
287− , nativeProgress
288− { value: p.sliderValue
289− , style: Style.style { height: 18.0, marginTop: 8.0 }
290− }
291− ]
292− ]
293− let
294− popUpSection =
295− [ sectionTitle p.fg "Pop Up Button"
296− , card p.cardBg
297− [ view { style: tw "flex-row items-center" }
298− [ nativePopUp
299− { items: [ "Small", "Medium", "Large", "Extra Large" ]
300− , selectedIndex: p.popUpIndex
301− , onChange: handler
302− ( nativeEvent >>> unsafeEventFn \e ->
303− { idx: (getFieldInt "selectedIndex" e), title: (getFieldStr "title" e) }
304− )
305− \r -> do
306− p.setPopUpIndex r.idx
307− p.setPopUpTitle r.title
308− , style: Style.style { height: 24.0, width: 160.0 }
309− }
310− , label p.dimFg ("Selected: " <> p.popUpTitle)
311− ]
312− ]
313− ]
314− let
315− colorSection =
316− [ sectionTitle p.fg "Color Well"
317− , card p.cardBg
318− [ view { style: tw "flex-row items-center" }
319− [ nativeColorWell
320− { color: p.selectedColor
321− , minimal: true
322− , onChange: extractString "color" p.setSelectedColor
323− , style: Style.style { height: 32.0, width: 48.0 }
324− }
325− , view
326− { style: tw "ml-3 rounded" <> Style.style
327− { width: 24.0, height: 24.0, backgroundColor: p.selectedColor }
328− }
329− []
330− , label p.dimFg p.selectedColor
331− ]
332− ]
333− ]
334− let
335− dateSection =
336− [ sectionTitle p.fg "Date Picker"
337− , card p.cardBg
338− [ nativeDatePicker
339− { graphical: false
340− , onChange: extractString "date" p.setDateText
341− , style: Style.style { height: 24.0, width: 200.0 }
342− }
343− , if p.dateText == "" then mempty
344− else label p.dimFg ("Picked: " <> p.dateText)
345− ]
346− ]
347− let
348− textSection =
349− [ sectionTitle p.fg "Text Field"
350− , card p.cardBg
351− [ nativeTextField
352− { placeholder: "Type something..."
353− , search: true
354− , text: p.searchText
355− , onChangeText: extractString "text" p.setSearchText
356− , style: Style.style { height: 24.0 }
357− }
358− , if p.searchText == "" then mempty
359− else label p.dimFg ("You typed: " <> p.searchText)
360− ]
361− ]
362− let
363− sections = join
364− [ if show' "Button" then buttonSection else []
365− , if show' "Switch" then switchSection else []
366− , if show' "Slider" then sliderSection else []
367− , if show' "Pop Up" then popUpSection else []
368− , if show' "Color" then colorSection else []
369− , if show' "Date" then dateSection else []
370− , if show' "Text" then textSection else []
371− ]
372− pure do
373− sidebarLayout
374− { sidebar
375− , sidebarWidth: 140.0
376− , content: nativeScrollView { style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
377− (view { style: tw "px-4 pb-4" } sections)
378− }
379−
380−-- Tab 1: Editor
381−editorTab :: { fg :: String } -> JSX
382−editorTab = component "EditorTab" \p -> React.do
383− pure do
384− view { style: tw "flex-1 px-4" }
385− [ sectionTitle p.fg "Rich Text Editor"
386− , nativeTextEditor
387− { text: "Welcome to the PureScript-driven native text editor.\n\nThis uses NSTextView with rich text support and a formatting ruler."
388− , richText: true
389− , showsRuler: true
390− , style: tw "flex-1" <> Style.style { minHeight: 300.0 }
391− }
392− ]
393−
394−-- Tab 2: Browser
395−type BrowserProps =
396− { browserUrl :: String
397− , setBrowserUrl :: String -> Effect Unit
398− , urlBarText :: String
399− , setUrlBarText :: String -> Effect Unit
400− , fg :: String
401− , dimFg :: String
402− }
403−
404−browserTab :: BrowserProps -> JSX
405−browserTab = component "BrowserTab" \p -> React.do
406− pure do
407− view { style: tw "flex-1 px-4" }
408− [ sectionTitle p.fg "Web Browser"
409− , nativeTextField
410− { text: p.urlBarText
411− , placeholder: "Enter URL..."
412− , search: false
413− , onChangeText: extractString "text" p.setUrlBarText
414− , onSubmit: extractString "text" p.setBrowserUrl
415− , style: Style.style { height: 24.0, marginBottom: 8.0 }
416− }
417− , nativeWebView
418− { url: p.browserUrl
419− , onFinishLoad: handler
420− ( nativeEvent >>> unsafeEventFn \e ->
421− { url: getFieldStr "url" e, title: getFieldStr "title" e }
422− )
423− \r -> p.setUrlBarText r.url
424− , style: tw "flex-1" <> Style.style { minHeight: 400.0 }
425− }
426− ]
427−
428−-- Tab 3: Rive Animation
429−riveTab :: { fg :: String, bg :: String } -> JSX
430−riveTab = component "RiveTab" \p -> React.do
431− pure do
432− nativeScrollView { style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
433− ( view { style: tw "px-4 pb-4" }
434− [ sectionTitle p.fg "Mouse Tracking"
435− , text { style: tw "text-xs mb-2" <> Style.style { color: p.fg } }
436− "Move your cursor over the cat — it follows your mouse!"
437− , nativeRiveView_
438− { resourceName: "cat_following_mouse"
439− , stateMachineName: "State Machine 1"
440− , fit: T.contain
441− , autoplay: true
442− , style: Style.style { height: 300.0 }
443− }
444− , sectionTitle p.fg "Cursor Tracking"
445− , text { style: tw "text-xs mb-2" <> Style.style { color: p.fg } }
446− "Shapes follow your cursor via Pointer Move listeners"
447− , nativeRiveView_
448− { resourceName: "cursor_tracking"
449− , stateMachineName: "State Machine 1"
450− , fit: T.contain
451− , autoplay: true
452− , style: Style.style { height: 300.0 }
453− }
454− , sectionTitle p.fg "Interactive Rive Animations"
455− , text { style: tw "text-xs mb-2" <> Style.style { color: p.fg } }
456− "Click the stars to rate! State machine handles interaction."
457− , nativeRiveView_
458− { resourceName: "rating_animation"
459− , stateMachineName: "State Machine 1"
460− , fit: T.contain
461− , autoplay: true
462− , style: Style.style { height: 200.0 }
463− }
464− , sectionTitle p.fg "Light Switch"
465− , text { style: tw "text-xs mb-2" <> Style.style { color: p.fg } }
466− "Click to toggle"
467− , nativeRiveView_
468− { resourceName: "switch_event_example"
469− , stateMachineName: "State Machine 1"
470− , fit: T.contain
471− , autoplay: true
472− , style: Style.style { height: 200.0 }
473− }
474− , sectionTitle p.fg "Windy Tree"
475− , nativeRiveView_
476− { resourceName: "windy_tree"
477− , fit: T.cover
478− , autoplay: true
479− , style: tw "flex-1" <> Style.style { minHeight: 300.0, backgroundColor: p.bg }
480− }
481− ]
482− )
483−
484−-- Tab 4: System (Context Menu, Drop Zone, File Picker)
485−type SystemProps =
486− { fg :: String
487− , dimFg :: String
488− , cardBg :: String
489− , bg :: String
490− }
491−
492−systemTab :: SystemProps -> JSX
493−systemTab = component "SystemTab" \p -> React.do
494− menuResult /\ setMenuResult <- useState' ""
495− dropStatus /\ setDropStatus <- useState' "Drop files here"
496− droppedFiles /\ setDroppedFiles <- useState' ""
497− isDragging /\ setIsDragging <- useState' false
498− pickedFiles /\ setPickedFiles <- useState' ""
499− videoPlaying /\ setVideoPlaying <- useState' true
500− comboText /\ setComboText <- useState' ""
501− comboResult /\ setComboResult <- useState' ""
502− stepperValue /\ setStepperValue <- useState' 5.0
503− popoverVisible /\ setPopoverVisible <- useState' false
504− checkboxA /\ setCheckboxA <- useState' true
505− checkboxB /\ setCheckboxB <- useState' false
506− radioChoice /\ setRadioChoice <- useState' "option1"
507− searchQuery /\ setSearchQuery <- useState' ""
508− searchResult /\ setSearchResult <- useState' ""
509− tagTokens /\ setTagTokens <- useState' ([ "PureScript", "React", "macOS" ] :: Array String)
510− sheetVisible /\ setSheetVisible <- useState' false
511− menuResult2 /\ setMenuResult2 <- useState' ""
512− selectedRow /\ setSelectedRow <- useState' ""
513− outlineSelection /\ setOutlineSelection <- useState' ""
514− statusBarActive /\ setStatusBarActive <- useState' false
515− let
516− accentBorder = if isDragging then "#007AFF" else p.dimFg
517− pure do
518− nativeScrollView { style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
519− ( view { style: tw "px-4 pb-4" }
520− [ sectionTitle p.fg "Context Menu"
521− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
522− "Right-click the area below to open a context menu"
523− , nativeContextMenu
524− { items:
525− [ { id: "cut", title: "Cut", sfSymbol: "scissors" }
526− , { id: "copy", title: "Copy", sfSymbol: "doc.on.doc" }
527− , { id: "paste", title: "Paste", sfSymbol: "doc.on.clipboard" }
528− , { id: "sep", title: "-", sfSymbol: "" }
529− , { id: "delete", title: "Delete", sfSymbol: "trash" }
530− , { id: "selectAll", title: "Select All", sfSymbol: "selection.pin.in.out" }
531− ]
532− , onSelectItem: extractString "itemId" setMenuResult
533− , style: Style.style {}
534− }
535− ( card p.cardBg
536− [ view { style: tw "items-center justify-center" <> Style.style { minHeight: 80.0 } }
537− [ text { style: tw "text-sm" <> Style.style { color: p.fg } }
538− "Right-click me!"
539− , if menuResult == "" then mempty
540− else label p.dimFg ("Selected: " <> menuResult)
541− ]
542− ]
543− )
544− , sectionTitle p.fg "Drop Zone"
545− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
546− "Drag files from Finder into the area below"
547− , view
548− { draggedTypes: [ "NSFilenamesPboardType" ]
549− , onDrop: handler
550− (nativeEvent >>> unsafeEventFn \e -> getFieldJSON "dataTransfer" e)
551− \r -> do
552− setDroppedFiles r
553− setDropStatus "Drop files here"
554− setIsDragging false
555− , onDragEnter: handler_ do
556− setDropStatus "Release to drop!"
557− setIsDragging true
558− , onDragLeave: handler_ do
559− setDropStatus "Drop files here"
560− setIsDragging false
561− , style: tw "rounded-lg items-center justify-center mb-2"
562− <> Style.style
563− { minHeight: 100.0
564− , borderWidth: 2.0
565− , borderColor: accentBorder
566− , backgroundColor: p.cardBg
567− }
568− }
569− [ text { style: tw "text-sm" <> Style.style { color: p.fg } } dropStatus
570− , if droppedFiles == "" then mempty
571− else text { style: tw "text-xs mt-2 px-4" <> Style.style { color: p.dimFg } }
572− ("Dropped: " <> droppedFiles)
573− ]
574− , sectionTitle p.fg "File Picker"
575− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
576− "Click buttons to open native file panels"
577− , card p.cardBg
578− [ view { style: tw "flex-row items-center" }
579− [ nativeFilePicker
580− { mode: T.openFile
581− , title: "Open Files"
582− , sfSymbol: "doc.badge.plus"
583− , allowMultiple: true
584− , allowedTypes: [ "public.image", "public.text" ]
585− , message: "Select files to open"
586− , onPickFiles: handler
587− (nativeEvent >>> unsafeEventFn \e -> getFieldJSON "files" e)
588− setPickedFiles
589− , onCancel: handler_ (setPickedFiles "Cancelled")
590− , style: Style.style { height: 24.0, width: 120.0 }
591− }
592− , nativeFilePicker
593− { mode: T.openFile
594− , title: "Choose Folder"
595− , sfSymbol: "folder"
596− , canChooseDirectories: true
597− , message: "Select a folder"
598− , onPickFiles: handler
599− (nativeEvent >>> unsafeEventFn \e -> getFieldJSON "files" e)
600− setPickedFiles
601− , onCancel: handler_ (setPickedFiles "Cancelled")
602− , style: Style.style { height: 24.0, width: 140.0, marginLeft: 8.0 }
603− }
604− , nativeFilePicker
605− { mode: T.saveFile
606− , title: "Save As..."
607− , sfSymbol: "square.and.arrow.down"
608− , defaultName: "Untitled.txt"
609− , allowedTypes: [ "public.plain-text" ]
610− , message: "Choose save location"
611− , onPickFiles: handler
612− (nativeEvent >>> unsafeEventFn \e -> getFieldJSON "files" e)
613− setPickedFiles
614− , onCancel: handler_ (setPickedFiles "Cancelled")
615− , style: Style.style { height: 24.0, width: 120.0, marginLeft: 8.0 }
616− }
617− ]
618− , if pickedFiles == "" then mempty
619− else label p.dimFg pickedFiles
620− ]
621− , sectionTitle p.fg "Video Player"
622− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
623− "Native AVPlayerView with floating controls"
624− , nativeVideoPlayer
625− { source: "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/720/Big_Buck_Bunny_720_10s_1MB.mp4"
626− , playing: videoPlaying
627− , looping: true
628− , muted: false
629− , style: Style.style { height: 240.0 } <> tw "rounded-lg overflow-hidden mb-2"
630− }
631− , view { style: tw "flex-row mb-2" }
632− [ nativeButton
633− { title: if videoPlaying then "Pause" else "Play"
634− , sfSymbol: if videoPlaying then "pause.fill" else "play.fill"
635− , bezelStyle: T.push
636− , onPress: handler_ (setVideoPlaying (not videoPlaying))
637− , style: Style.style { height: 24.0, width: 100.0 }
638− }
639− ]
640− , sectionTitle p.fg "Animated Image"
641− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
642− "Native NSImageView with animated GIF support"
643− , nativeAnimatedImage
644− { source: "https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif"
645− , animating: true
646− , style: Style.style { height: 200.0 } <> tw "rounded-lg overflow-hidden mb-2"
647− }
648− , nativeAnimatedImage
649− { source: "https://media.giphy.com/media/13CoXDiaCcCoyk/giphy.gif"
650− , animating: true
651− , style: Style.style { height: 200.0 } <> tw "rounded-lg overflow-hidden mb-2"
652− }
653−
654− -- New components
655− , sectionTitle p.fg "Split View"
656− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
657− "Native NSSplitView with resizable divider"
658− , nativeSplitView
659− { isVertical: true
660− , style: Style.style { height: 150.0 } <> tw "rounded-lg overflow-hidden mb-2"
661− }
662− [ view { style: tw "flex-1 p-4" <> Style.style { backgroundColor: p.cardBg } }
663− [ text { style: tw "text-sm font-semibold" <> Style.style { color: p.fg } } "Left Pane"
664− , text { style: tw "text-xs mt-1" <> Style.style { color: p.dimFg } } "Drag the divider to resize"
665− ]
666− , view { style: tw "flex-1 p-4" <> Style.style { backgroundColor: p.cardBg } }
667− [ text { style: tw "text-sm font-semibold" <> Style.style { color: p.fg } } "Right Pane"
668− , text { style: tw "text-xs mt-1" <> Style.style { color: p.dimFg } } "Content fills available space"
669− ]
670− ]
671−
672− , sectionTitle p.fg "Tab View"
673− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
674− "Native segmented tab bar"
675− , nativeTabView
676− { items:
677− [ { id: "general", label: "General" }
678− , { id: "advanced", label: "Advanced" }
679− , { id: "about", label: "About" }
680− ]
681− , selectedItem: "general"
682− , onSelectTab: extractString "tabId" \_ -> pure unit
683− , style: Style.style { height: 32.0 } <> tw "mb-2"
684− }
685−
686− , sectionTitle p.fg "Combo Box"
687− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
688− "Editable dropdown with autocomplete"
689− , card p.cardBg
690− [ nativeComboBox
691− { items: [ "Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig", "Grape" ]
692− , text: comboText
693− , placeholder: "Type a fruit..."
694− , onChangeText: extractString "text" setComboText
695− , onSelectItem: extractString "text" \t -> do
696− setComboText t
697− setComboResult ("Selected: " <> t)
698− , style: Style.style { height: 28.0 } <> tw "mb-2"
50+ , sidebarLayout
51+ { sidebar: outlineSidebar selectedItem setSelectedItem
52+ , sidebarWidth: 200.0
53+ , content: demoContent dp selectedItem
69954 }
700− , if comboResult == "" then mempty
701− else label p.dimFg comboResult
70255 ]
703−
704− , sectionTitle p.fg "Stepper"
705− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
706− "Up/down increment control with label"
707− , card p.cardBg
708− [ view { style: tw "flex-row items-center" }
709− [ text { style: tw "text-sm mr-3" <> Style.style { color: p.fg } } "Quantity:"
710− , nativeStepper
711− { value: stepperValue
712− , minValue: 0.0
713− , maxValue: 50.0
714− , increment: 1.0
715− , onChange: extractNumber "value" setStepperValue
716− , style: Style.style { width: 100.0, height: 24.0 }
717− }
718− ]
719− ]
720−
721− , sectionTitle p.fg "Box"
722− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
723− "Native NSBox with title and border"
724− , nativeBox
725− { boxTitle: "Settings"
726− , fillColorStr: p.cardBg
727− , borderColorStr: p.dimFg
728− , cornerRadiusValue: 8.0
729− , style: Style.style { height: 100.0 } <> tw "mb-2"
730− }
731− [ view { style: tw "flex-row items-center p-2" }
732− [ text { style: tw "text-sm" <> Style.style { color: p.fg } } "Content inside a native box" ]
733− ]
734−
735− , sectionTitle p.fg "Popover"
736− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
737− "Click button to toggle a popover"
738− , nativePopover
739− { visible: popoverVisible
740− , preferredEdge: T.bottom
741− , behavior: T.transient
742− , onClose: handler_ (setPopoverVisible false)
743− , style: tw "mb-2"
744− }
745− [ nativeButton
746− { title: if popoverVisible then "Hide Popover" else "Show Popover"
747− , bezelStyle: T.push
748− , onPress: handler_ (setPopoverVisible (not popoverVisible))
749− , style: Style.style { height: 24.0, width: 140.0 }
750− }
751− , view { style: tw "p-4" <> Style.style { width: 200.0, height: 80.0 } }
752− [ text { style: tw "text-sm font-semibold" <> Style.style { color: p.fg } } "Popover Content"
753− , text { style: tw "text-xs mt-1" <> Style.style { color: p.dimFg } } "Click outside to dismiss"
754− ]
755− ]
756−
757− , sectionTitle p.fg "Image"
758− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
759− "Native NSImageView with URL loading and corner radius"
760− , nativeImage
761− { source: "https://placedog.net/640/400"
762− , contentMode: T.scaleProportionally
763− , cornerRadius: 12.0
764− , style: Style.style { height: 200.0 } <> tw "mb-2"
765− }
766−
767− , sectionTitle p.fg "Checkbox"
768− , card p.cardBg
769− [ nativeCheckbox
770− { checked: checkboxA
771− , title: "Enable notifications"
772− , enabled: true
773− , onChange: extractBool "checked" setCheckboxA
774− , style: Style.style { height: 24.0 } <> tw "mb-1"
775− }
776− , nativeCheckbox
777− { checked: checkboxB
778− , title: "Dark mode"
779− , enabled: true
780− , onChange: extractBool "checked" setCheckboxB
781− , style: Style.style { height: 24.0 }
782− }
783− ]
784−
785− , sectionTitle p.fg "Radio Button"
786− , card p.cardBg
787− [ nativeRadioButton
788− { selected: radioChoice == "option1"
789− , title: "Small"
790− , enabled: true
791− , onChange: handler_ (setRadioChoice "option1")
792− , style: Style.style { height: 24.0 } <> tw "mb-1"
793− }
794− , nativeRadioButton
795− { selected: radioChoice == "option2"
796− , title: "Medium"
797− , enabled: true
798− , onChange: handler_ (setRadioChoice "option2")
799− , style: Style.style { height: 24.0 } <> tw "mb-1"
800− }
801− , nativeRadioButton
802− { selected: radioChoice == "option3"
803− , title: "Large"
804− , enabled: true
805− , onChange: handler_ (setRadioChoice "option3")
806− , style: Style.style { height: 24.0 }
807− }
808− , label p.dimFg ("Selected: " <> radioChoice)
809− ]
810−
811− , sectionTitle p.fg "Search Field"
812− , nativeSearchField
813− { text: searchQuery
814− , placeholder: "Search..."
815− , onChangeText: extractString "text" setSearchQuery
816− , onSearch: extractString "text" \t -> setSearchResult ("Searched: " <> t)
817− , style: Style.style { height: 28.0 } <> tw "mb-2"
818− }
819− , if searchResult == "" then mempty
820− else label p.dimFg searchResult
821−
822− , sectionTitle p.fg "Token Field"
823− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
824− "Type text and press Enter to add tokens"
825− , nativeTokenField
826− { tokens: tagTokens
827− , placeholder: "Add tags..."
828− , onChangeTokens: handler
829− (nativeEvent >>> unsafeEventFn \e -> getFieldArray "tokens" e)
830− setTagTokens
831− , style: Style.style { height: 28.0 } <> tw "mb-2"
832− }
833−
834− , sectionTitle p.fg "Separator"
835− , nativeSeparator
836− { vertical: false
837− , style: Style.style { height: 2.0 } <> tw "my-2"
838− }
839−
840− , sectionTitle p.fg "Help Button"
841− , view { style: tw "flex-row items-center mb-2" }
842− [ nativeHelpButton
843− { onPress: handler_ (macosAlert T.informational "Help" "This is the help button. It opens contextual help." [ "OK" ])
844− , style: Style.style { width: 24.0, height: 24.0 }
845− }
846− , text { style: tw "text-xs ml-2" <> Style.style { color: p.dimFg } } "Click for help"
847− ]
848−
849− , sectionTitle p.fg "Path Control"
850− , nativePathControl
851− { url: "/Users"
852− , pathStyle: T.standardPath
853− , onSelectPath: extractString "url" \_ -> pure unit
854− , style: Style.style { height: 24.0 } <> tw "mb-2"
855− }
856−
857− , sectionTitle p.fg "Sheet"
858− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
859− "Modal sheet attached to the window"
860− , nativeButton
861− { title: "Show Sheet"
862− , bezelStyle: T.push
863− , onPress: handler_ (setSheetVisible true)
864− , style: Style.style { height: 24.0, width: 120.0 } <> tw "mb-2"
865− }
866− , nativeSheet
867− { visible: sheetVisible
868− , onDismiss: handler_ (setSheetVisible false)
869− }
870− [ view { style: tw "p-6" <> Style.style { width: 400.0, height: 200.0 } }
871− [ text { style: tw "text-lg font-semibold mb-2" <> Style.style { color: p.fg } } "Sheet Content"
872− , text { style: tw "text-sm mb-4" <> Style.style { color: p.dimFg } } "This is a native macOS sheet."
873− , nativeButton
874− { title: "Dismiss"
875− , bezelStyle: T.push
876− , onPress: handler_ (setSheetVisible false)
877− , style: Style.style { height: 24.0, width: 100.0 }
878− }
879− ]
880− ]
881−
882− , sectionTitle p.fg "Menu"
883− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
884− "Imperative popup menu at mouse position"
885− , view { style: tw "flex-row items-center mb-2" }
886− [ nativeButton
887− { title: "Show Menu"
888− , bezelStyle: T.push
889− , onPress: handler_
890− ( macosShowMenu
891− [ { title: "New File", id: "new" }
892− , { title: "Open...", id: "open" }
893− , { title: "-", id: "sep" }
894− , { title: "Save", id: "save" }
895− , { title: "Save As...", id: "saveAs" }
896− ]
897− \itemId -> setMenuResult2 ("Menu: " <> itemId)
898− )
899− , style: Style.style { height: 24.0, width: 120.0 }
900− }
901− , if menuResult2 == "" then mempty
902− else text { style: tw "text-xs ml-3" <> Style.style { color: p.dimFg } } menuResult2
903− ]
904−
905− , sectionTitle p.fg "Table View"
906− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
907− "Native NSTableView with columns and rows"
908− , nativeTableView
909− { columns:
910− [ { id: "name", title: "Name", width: 150.0 }
911− , { id: "type", title: "Type", width: 100.0 }
912− , { id: "size", title: "Size", width: 80.0 }
913− ]
914− , rows:
915− [ [ "Main.purs", "PureScript", "12 KB" ]
916− , [ "App.tsx", "TypeScript", "8 KB" ]
917− , [ "style.css", "CSS", "3 KB" ]
918− , [ "index.html", "HTML", "1 KB" ]
919− , [ "package.json", "JSON", "2 KB" ]
920− ]
921− , headerVisible: true
922− , alternatingRows: true
923− , onSelectRow: extractInt "rowIndex" \i -> setSelectedRow ("Row " <> show i)
924− , onDoubleClickRow: extractInt "rowIndex" \i -> setSelectedRow ("Double-clicked row " <> show i)
925− , style: Style.style { height: 180.0 } <> tw "rounded-lg overflow-hidden mb-2"
926− }
927− , if selectedRow == "" then mempty
928− else label p.dimFg selectedRow
929−
930− , sectionTitle p.fg "Clipboard"
931− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
932− "Read and write the system clipboard"
933− , view { style: tw "flex-row items-center mb-2" }
934− [ nativeButton
935− { title: "Copy 'Hello'"
936− , bezelStyle: T.push
937− , onPress: handler_ (copyToClipboard "Hello from PureScript!")
938− , style: Style.style { height: 24.0, width: 120.0 }
939− }
940− ]
941−
942− , sectionTitle p.fg "Alert"
943− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
944− "Native alert dialog"
945− , nativeButton
946− { title: "Show Alert"
947− , bezelStyle: T.push
948− , onPress: handler_ (macosAlert T.warning "Are you sure?" "This action cannot be undone." [ "Cancel", "OK" ])
949− , style: Style.style { height: 24.0, width: 120.0 } <> tw "mb-4"
950− }
951−
952− , sectionTitle p.fg "Outline View"
953− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
954− "Hierarchical tree list (NSOutlineView)"
955− , nativeOutlineView
956− { items: unsafeToForeign
957− [ { id: "src"
958− , title: "src"
959− , sfSymbol: "folder"
960− , children:
961− [ { id: "main", title: "Main.purs", sfSymbol: "doc", children: [] }
962− , { id: "macos"
963− , title: "MacOS"
964− , sfSymbol: "folder"
965− , children:
966− [ { id: "btn", title: "Button.purs", sfSymbol: "doc", children: [] }
967− , { id: "sl", title: "Slider.purs", sfSymbol: "doc", children: [] }
968− , { id: "sw", title: "Switch.purs", sfSymbol: "doc", children: [] }
969− ]
970− }
971− ]
972− }
973− , { id: "test"
974− , title: "test"
975− , sfSymbol: "folder"
976− , children:
977− [ { id: "t1", title: "MacOSComponents.test.js", sfSymbol: "doc", children: [] }
978− , { id: "t2", title: "MacOSSnapshots.test.js", sfSymbol: "doc", children: [] }
979− ]
980− }
981− , { id: "pkg", title: "package.json", sfSymbol: "doc.text", children: [] }
982− ]
983− , headerVisible: false
984− , onSelectItem: extractString "id" setOutlineSelection
985− , style: Style.style { height: 200.0 } <> tw "rounded-lg overflow-hidden mb-2"
986− }
987− , if outlineSelection == "" then mempty
988− else label p.dimFg ("Selected: " <> outlineSelection)
989−
990− , sectionTitle p.fg "Share"
991− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
992− "System share picker"
993− , nativeButton
994− { title: "Share Text"
995− , sfSymbol: "square.and.arrow.up"
996− , bezelStyle: T.push
997− , onPress: handler_ (macosShare [ "Hello from PureScript React Native!" ])
998− , style: Style.style { height: 24.0, width: 120.0 } <> tw "mb-2"
999− }
1000−
1001− , sectionTitle p.fg "Notifications"
1002− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
1003− "System notifications (requires permission)"
1004− , nativeButton
1005− { title: "Send Notification"
1006− , sfSymbol: "bell"
1007− , bezelStyle: T.push
1008− , onPress: handler_ (macosNotify "PureScript" "Hello from React Native macOS!")
1009− , style: Style.style { height: 24.0, width: 160.0 } <> tw "mb-2"
1010− }
1011−
1012− , sectionTitle p.fg "Sound"
1013− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
1014− "Play system sounds"
1015− , view { style: tw "flex-row items-center mb-2" }
1016− [ nativeButton
1017− { title: "Glass"
1018− , bezelStyle: T.push
1019− , onPress: handler_ (macosPlaySound "Glass")
1020− , style: Style.style { height: 24.0, width: 80.0 }
1021− }
1022− , nativeButton
1023− { title: "Ping"
1024− , bezelStyle: T.push
1025− , onPress: handler_ (macosPlaySound "Ping")
1026− , style: Style.style { height: 24.0, width: 80.0, marginLeft: 8.0 }
1027− }
1028− , nativeButton
1029− { title: "Beep"
1030− , bezelStyle: T.push
1031− , onPress: handler_ macosBeep
1032− , style: Style.style { height: 24.0, width: 80.0, marginLeft: 8.0 }
1033− }
1034− ]
1035−
1036− , sectionTitle p.fg "Status Bar"
1037− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
1038− "Menu bar icon with dropdown"
1039− , view { style: tw "flex-row items-center mb-4" }
1040− [ nativeButton
1041− { title: if statusBarActive then "Remove" else "Add to Menu Bar"
1042− , sfSymbol: if statusBarActive then "minus.circle" else "plus.circle"
1043− , bezelStyle: T.push
1044− , onPress: handler_
1045− ( if statusBarActive then do
1046− macosRemoveStatusBarItem
1047− setStatusBarActive false
1048− else do
1049− macosSetStatusBarItem
1050− { title: ""
1051− , sfSymbol: "swift"
1052− , menuItems:
1053− [ { id: "about", title: "About PureScript RN" }
1054− , { id: "sep", title: "-" }
1055− , { id: "quit", title: "Quit" }
1056− ]
1057− }
1058− setStatusBarActive true
1059− )
1060− , style: Style.style { height: 24.0, width: 180.0 }
1061− }
1062− ]
1063− ]
1064− )
1065−
1066−-- Tab 6: AI & Tools
1067−type AIProps =
1068− { fg :: String
1069− , dimFg :: String
1070− , cardBg :: String
1071− , bg :: String
1072− }
1073−
1074−aiTab :: AIProps -> JSX
1075−aiTab = component "AITab" \p -> React.do
1076− speechText /\ setSpeechText <- useState' "Hello from PureScript React Native on macOS!"
1077− ocrResult /\ setOcrResult <- useState' ""
1078− listening /\ setListening <- useState' false
1079− transcript /\ setTranscript <- useState' ""
1080− nlText /\ setNlText <- useState' "I love this amazing app! C'est magnifique."
1081− nlResult /\ setNlResult <- useState' ""
1082− useEffect listening do
1083− if listening then do
1084− timerId <- setInterval 500 do
1085− macosGetTranscript \t -> setTranscript t
1086− pure (clearInterval timerId)
1087− else pure (pure unit)
1088− pure do
1089− nativeScrollView { style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
1090− ( view { style: tw "px-4 pb-4" }
1091− [ sectionTitle p.fg "Map"
1092− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
1093− "Embedded MKMapView (San Francisco)"
1094− , nativeMapView
1095− { latitude: 37.7749
1096− , longitude: -122.4194
1097− , latitudeDelta: 0.05
1098− , longitudeDelta: 0.05
1099− , mapType: "standard"
1100− , showsUserLocation: false
1101− , annotations: unsafeToForeign
1102− [ { latitude: 37.7749, longitude: -122.4194, title: "San Francisco", subtitle: "California" }
1103− , { latitude: 37.8199, longitude: -122.4783, title: "Golden Gate Bridge", subtitle: "" }
1104− ]
1105− , onRegionChange: handler_ (pure unit)
1106− , onSelectAnnotation: handler_ (pure unit)
1107− , style: Style.style { height: 250.0 }
1108− }
1109− , sectionTitle p.fg "PDF Viewer"
1110− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
1111− "PDFKit document viewer"
1112− , nativePDFView
1113− { source: "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
1114− , autoScales: true
1115− , displayMode: "singlePageContinuous"
1116− , onPageChange: handler_ (pure unit)
1117− , style: Style.style { height: 300.0 }
1118− }
1119− , sectionTitle p.fg "Quick Look"
1120− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
1121− "Preview files with QLPreviewPanel"
1122− , view { style: tw "flex-row items-center mb-4" }
1123− [ nativeButton
1124− { title: "Preview /etc/hosts"
1125− , sfSymbol: "eye"
1126− , bezelStyle: T.push
1127− , onPress: handler_ (macosQuickLook "/etc/hosts")
1128− , style: Style.style { height: 24.0, width: 180.0 }
1129− }
1130− ]
1131− , sectionTitle p.fg "Speech"
1132− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
1133− "Text-to-speech (NSSpeechSynthesizer)"
1134− , nativeTextField
1135− { text: speechText
1136− , placeholder: "Text to speak..."
1137− , onChangeText: extractString "text" setSpeechText
1138− , style: Style.style { height: 24.0 } <> tw "mb-2"
1139− }
1140− , view { style: tw "flex-row items-center mb-4" }
1141− [ nativeButton
1142− { title: "Speak"
1143− , sfSymbol: "speaker.wave.2"
1144− , bezelStyle: T.push
1145− , onPress: handler_ (macosSay speechText)
1146− , style: Style.style { height: 24.0, width: 100.0 }
1147− }
1148− , nativeButton
1149− { title: "Stop"
1150− , sfSymbol: "stop.circle"
1151− , bezelStyle: T.push
1152− , onPress: handler_ macosStopSpeaking
1153− , style: Style.style { height: 24.0, width: 100.0, marginLeft: 8.0 }
1154− }
1155− ]
1156− , sectionTitle p.fg "Color Picker"
1157− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
1158− "System color panel (NSColorPanel)"
1159− , view { style: tw "flex-row items-center mb-4" }
1160− [ nativeButton
1161− { title: "Show Color Panel"
1162− , sfSymbol: "paintpalette"
1163− , bezelStyle: T.push
1164− , onPress: handler_ macosShowColorPanel
1165− , style: Style.style { height: 24.0, width: 180.0 }
1166− }
1167− ]
1168− , sectionTitle p.fg "Font Panel"
1169− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
1170− "System font panel (NSFontPanel)"
1171− , view { style: tw "flex-row items-center mb-4" }
1172− [ nativeButton
1173− { title: "Show Font Panel"
1174− , sfSymbol: "textformat"
1175− , bezelStyle: T.push
1176− , onPress: handler_ macosShowFontPanel
1177− , style: Style.style { height: 24.0, width: 180.0 }
1178− }
1179− ]
1180− , sectionTitle p.fg "OCR (Vision)"
1181− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
1182− "Recognize text in images (VNRecognizeTextRequest)"
1183− , view { style: tw "flex-row items-center mb-2" }
1184− [ nativeFilePicker
1185− { mode: T.openFile
1186− , allowedTypes: [ "public.image" ]
1187− , title: "Pick Image"
1188− , sfSymbol: "photo"
1189− , onPickFiles: handler
1190− (nativeEvent >>> unsafeEventFn \e -> getFieldArray "files" e)
1191− \paths -> for_ paths \path -> do
1192− setOcrResult "Recognizing..."
1193− macosOCR path \result -> setOcrResult result
1194− , onCancel: handler_ (pure unit)
1195− , style: Style.style { height: 24.0, width: 140.0 }
1196− }
1197− ]
1198− , if ocrResult /= "" then view { style: tw "p-2 rounded mb-4" <> Style.style { backgroundColor: p.cardBg } }
1199− [ text { style: tw "text-xs font-mono" <> Style.style { color: p.fg } } ocrResult ]
1200− else view {} []
1201− , sectionTitle p.fg "Speech Recognition"
1202− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
1203− "Live microphone-to-text (click Start, speak, click Stop)"
1204− , view { style: tw "flex-row items-center mb-2" }
1205− [ nativeButton
1206− { title: if listening then "Listening..." else "Start"
1207− , sfSymbol: if listening then "mic.fill" else "mic"
1208− , bezelStyle: T.push
1209− , onPress: handler_ do
1210− setListening true
1211− setTranscript ""
1212− macosStartListening
1213− , style: Style.style { height: 24.0, width: 130.0 }
1214− }
1215− , nativeButton
1216− { title: "Stop"
1217− , sfSymbol: "stop.circle"
1218− , bezelStyle: T.push
1219− , onPress: handler_ do
1220− setListening false
1221− macosStopListening \finalText -> do
1222− setTranscript finalText
1223− , style: Style.style { height: 24.0, width: 80.0, marginLeft: 8.0 }
1224− }
1225− ]
1226− , if transcript /= "" then view { style: tw "p-2 rounded mb-4" <> Style.style { backgroundColor: p.cardBg } }
1227− [ text { style: tw "text-xs font-mono" <> Style.style { color: p.fg } } transcript ]
1228− else view {} []
1229− , sectionTitle p.fg "Natural Language"
1230− , text { style: tw "text-xs mb-2" <> Style.style { color: p.dimFg } }
1231− "Language detection, sentiment, tokenization (NaturalLanguage.framework)"
1232− , nativeTextField
1233− { text: nlText
1234− , placeholder: "Enter text to analyze..."
1235− , onChangeText: extractString "text" setNlText
1236− , style: Style.style { height: 24.0 } <> tw "mb-2"
1237− }
1238− , view { style: tw "flex-row items-center mb-2" }
1239− [ nativeButton
1240− { title: "Language"
1241− , sfSymbol: "globe"
1242− , bezelStyle: T.push
1243− , onPress: handler_ do
1244− macosDetectLanguage nlText \lang ->
1245− setNlResult ("Language: " <> lang)
1246− , style: Style.style { height: 24.0, width: 110.0 }
1247− }
1248− , nativeButton
1249− { title: "Sentiment"
1250− , sfSymbol: "face.smiling"
1251− , bezelStyle: T.push
1252− , onPress: handler_ do
1253− macosAnalyzeSentiment nlText \score ->
1254− setNlResult ("Sentiment: " <> show score)
1255− , style: Style.style { height: 24.0, width: 110.0, marginLeft: 8.0 }
1256− }
1257− , nativeButton
1258− { title: "Tokenize"
1259− , sfSymbol: "text.word.spacing"
1260− , bezelStyle: T.push
1261− , onPress: handler_ do
1262− macosTokenize nlText \tokens ->
1263− setNlResult ("Tokens: " <> joinWith ", " tokens)
1264− , style: Style.style { height: 24.0, width: 110.0, marginLeft: 8.0 }
1265− }
1266− ]
1267− , if nlResult /= "" then view { style: tw "p-2 rounded mb-4" <> Style.style { backgroundColor: p.cardBg } }
1268− [ text { style: tw "text-xs font-mono" <> Style.style { color: p.fg } } nlResult ]
1269− else view {} []
1270− ]
1271− )
1272−
1273−-- Tab 7: Chat (Matrix)
1274−type ChatProps =
1275− { fg :: String
1276− , dimFg :: String
1277− , cardBg :: String
1278− , bg :: String
1279− , isDark :: Boolean
1280− }
1281−
1282−type Message =
1283− { sender :: String
1284− , body :: String
1285− , kind :: String -- "text" | "sticker" | "gif" | "video"
1286− , eventId :: String
1287− }
1288−
1289−type Session =
1290− { homeserver :: String
1291− , accessToken :: String
1292− , userId :: String
1293− }
1294−
1295−type Room =
1296− { roomId :: String
1297− , name :: String
1298− , lastMessage :: String
1299− }
1300−
1301−avatarColors :: Array String
1302−avatarColors = [ "#5856D6", "#FF9500", "#34C759", "#FF2D55", "#5AC8FA", "#FF6B6B", "#4ECDC4", "#45B7D1" ]
1303−
1304−avatarColor :: String -> String
1305−avatarColor roomId = fromMaybe "#5856D6" (avatarColors !! (mod (hashCode roomId) (length avatarColors)))
1306− where
1307− hashCode s = abs (foldl (\acc c -> acc * 31 + c) 0 (charCodes s))
1308−
1309−foreign import charCodes :: String -> Array Int
1310−foreign import abs :: Int -> Int
1311−
1312−chatTab :: ChatProps -> JSX
1313−chatTab = component "ChatTab" \p -> React.do
1314− session /\ setSession <- useState' (Nothing :: Maybe Session)
1315− rooms /\ setRooms <- useState' ([] :: Array Room)
1316− activeRoom /\ setActiveRoom <- useState' (Nothing :: Maybe String)
1317− messages /\ setMessages <- useState ([] :: Array Message)
1318− inputText /\ setInputText <- useState' ""
1319− loginError /\ setLoginError <- useState' ""
1320− loginServer /\ setLoginServer <- useState' "https://matrix.org"
1321− loginUser /\ setLoginUser <- useState' ""
1322− loginPass /\ setLoginPass <- useState' ""
1323− syncTokenRef <- useRef ""
1324− activeRoomRef <- useRef (Nothing :: Maybe String)
1325− sessionRef <- useRef (Nothing :: Maybe Session)
1326− seenIdsRef <- useRef ([] :: Array String)
1327−
1328− -- Sync polling effect
1329− useEffect session do
1330− case session of
1331− Nothing -> pure mempty
1332− Just sess -> do
1333− writeRef sessionRef (Just sess)
1334− let
1335− doSync = launchAff_ do
1336− token <- readRef syncTokenRef # liftEffect
1337− result <- try (Matrix.sync sess.homeserver sess.accessToken token 5000)
1338− case result of
1339− Left _ -> pure unit
1340− Right resp -> liftEffect do
1341− let nextBatch = Matrix.getSyncNextBatch resp
1342− writeRef syncTokenRef nextBatch
1343− let roomIds = Matrix.getSyncJoinedRoomIds resp
1344− currentRoom <- readRef activeRoomRef
1345− for_ roomIds \rid -> do
1346− let events = Matrix.getSyncTimelineEvents resp rid
1347− let msgEvents = filter (\ev -> Matrix.getEventType ev == "m.room.message") events
1348− case currentRoom of
1349− Just cr | cr == rid -> do
1350− seen <- readRef seenIdsRef
1351− let newMsgs = filter (\ev -> not (Matrix.getEventId ev `elem` seen)) msgEvents
1352− let
1353− converted = newMsgs <#> \ev ->
1354− { sender: Matrix.getEventSender ev
1355− , body: Matrix.getEventBody ev
1356− , kind: eventKind ev
1357− , eventId: Matrix.getEventId ev
1358− }
1359− if null converted then pure unit
1360− else do
1361− let newIds = converted <#> _.eventId
1362− writeRef seenIdsRef (seen <> newIds)
1363− setMessages \msgs -> msgs <> converted
1364− _ -> pure unit
1365− timerId <- setInterval 3000 doSync
1366− doSync
1367− pure (clearInterval timerId)
1368−
1369− let
1370− doLogin = launchAff_ do
1371− result <- try (Matrix.login loginServer loginUser loginPass)
1372− case result of
1373− Left _ -> liftEffect (setLoginError "Login failed. Check credentials.")
1374− Right resp -> do
1375− let sess = { homeserver: loginServer, accessToken: resp.access_token, userId: resp.user_id }
1376− roomIds <- try (Matrix.joinedRooms loginServer resp.access_token)
1377− case roomIds of
1378− Left _ -> liftEffect do
1379− setSession (Just sess)
1380− setLoginError ""
1381− Right rids -> do
1382− names <- for rids \rid -> do
1383− n <- try (Matrix.roomName loginServer resp.access_token rid)
1384− let
1385− name = case n of
1386− Left _ -> rid
1387− Right "" -> rid
1388− Right nm -> nm
1389− pure { roomId: rid, name, lastMessage: "" }
1390− liftEffect do
1391− setSession (Just sess)
1392− setRooms names
1393− setLoginError ""
1394−
1395− selectRoom sess rid = do
1396− setActiveRoom (Just rid)
1397− writeRef activeRoomRef (Just rid)
1398− setMessages (const [])
1399− writeRef seenIdsRef []
1400− launchAff_ do
1401− result <- try (Matrix.sync sess.homeserver sess.accessToken "" 0)
1402− case result of
1403− Left _ -> pure unit
1404− Right resp -> liftEffect do
1405− let events = Matrix.getSyncTimelineEvents resp rid
1406− let msgEvents = filter (\ev -> Matrix.getEventType ev == "m.room.message") events
1407− let
1408− converted = msgEvents <#> \ev ->
1409− { sender: Matrix.getEventSender ev
1410− , body: Matrix.getEventBody ev
1411− , kind: eventKind ev
1412− , eventId: Matrix.getEventId ev
1413− }
1414− let ids = converted <#> _.eventId
1415− writeRef seenIdsRef ids
1416− setMessages (const converted)
1417−
1418− sendMessage sess rid msg = do
1419− if msg == "" then pure unit
1420− else do
1421− setInputText ""
1422− launchAff_ do
1423− txnId <- Matrix.genTxnId # liftEffect
1424− _ <- try (Matrix.sendMessage sess.homeserver sess.accessToken rid txnId msg)
1425− pure unit
1426−
1427− sentBubbleBg = if p.isDark then "#65B86A" else "#5CB85C"
1428− receivedBubbleBg = if p.isDark then "#3B3B3D" else "#FFFFFF"
1429− chatBg = if p.isDark then "#17212B" else "#E8DFD3"
1430−
1431− loginForm = view
1432− { style: tw "flex-1 items-center justify-center"
1433− <> Style.style { backgroundColor: chatBg }
1434− }
1435− [ view { style: tw "rounded-xl p-6" <> Style.style { backgroundColor: p.cardBg, width: 340.0 } }
1436− [ text { style: tw "text-lg font-bold mb-4 text-center" <> Style.style { color: p.fg } } "Matrix Login"
1437− , text { style: tw "text-xs mb-1" <> Style.style { color: p.dimFg } } "Homeserver"
1438− , nativeTextField
1439− { text: loginServer
1440− , placeholder: "https://matrix.org"
1441− , search: false
1442− , rounded: true
1443− , onChangeText: extractString "text" setLoginServer
1444− , onSubmit: handler_ (pure unit)
1445− , style: tw "mb-3" <> Style.style { height: 28.0 }
1446− }
1447− , text { style: tw "text-xs mb-1" <> Style.style { color: p.dimFg } } "Username"
1448− , nativeTextField
1449− { text: loginUser
1450− , placeholder: "username"
1451− , search: false
1452− , rounded: true
1453− , onChangeText: extractString "text" setLoginUser
1454− , onSubmit: handler_ (pure unit)
1455− , style: tw "mb-3" <> Style.style { height: 28.0 }
1456− }
1457− , text { style: tw "text-xs mb-1" <> Style.style { color: p.dimFg } } "Password"
1458− , nativeTextField
1459− { text: loginPass
1460− , placeholder: "password"
1461− , search: false
1462− , rounded: true
1463− , onChangeText: extractString "text" setLoginPass
1464− , onSubmit: handler_ doLogin
1465− , style: tw "mb-4" <> Style.style { height: 28.0 }
1466− }
1467− , if loginError /= "" then
1468− text { style: tw "text-xs mb-2 text-center" <> Style.style { color: "#FF3B30" } } loginError
1469− else text { style: Style.style { height: 0.0 } } ""
1470− , nativeButton
1471− { title: "Sign In"
1472− , bezelStyle: T.push
1473− , sfSymbol: "arrow.right.circle.fill"
1474− , onPress: handler_ doLogin
1475− , style: Style.style { height: 32.0 }
1476− }
1477− ]
1478− ]
1479−
1480− roomSidebar sess = view { style: tw "pt-2" }
1481− ( rooms <#> \r ->
1482− view
1483− { style: tw "flex-row items-center px-3 py-2 mx-2 rounded-lg"
1484− <> Style.style
1485− { backgroundColor: if activeRoom == Just r.roomId then (if p.isDark then "#2B5278" else "#419FD9") else "transparent" }
1486− }
1487− [ view
1488− { style: tw "rounded-full items-center justify-center"
1489− <> Style.style { width: 32.0, height: 32.0, backgroundColor: avatarColor r.roomId }
1490− }
1491− [ text { style: tw "text-xs font-bold" <> Style.style { color: "#FFFFFF" } } (take 1 r.name) ]
1492− , view { style: tw "ml-2 flex-1" }
1493− [ text
1494− { style: tw "text-sm font-semibold"
1495− <> Style.style { color: if activeRoom == Just r.roomId then "#FFFFFF" else p.fg }
1496− }
1497− r.name
1498− , text
1499− { style: tw "text-xs"
1500− <> Style.style { color: if activeRoom == Just r.roomId then "#FFFFFFCC" else p.dimFg }
1501− }
1502− r.lastMessage
1503− ]
1504− , nativeButton
1505− { title: ""
1506− , bezelStyle: T.toolbar
1507− , sfSymbol: ""
1508− , onPress: handler_ (selectRoom sess r.roomId)
1509− , style: Style.style { position: "absolute", top: 0.0, left: 0.0, right: 0.0, bottom: 0.0, opacity: 0.0 }
1510− }
1511− ]
56+ )
151257 )
1513−
1514− messageBubble sess _ msg = do
1515− let
1516− isMine = case sess of
1517− Just s -> msg.sender == s.userId
1518− Nothing -> false
1519− let align = if isMine then "flex-end" else "flex-start"
1520− let bigEmoji = msg.kind == "text" && isSingleEmoji msg.body
1521− let senderLabel = if isMine then "" else msg.sender
1522− nativeContextMenu
1523− { items:
1524− [ { id: "copy", title: "Copy", sfSymbol: "doc.on.doc" }
1525− , { id: "reply", title: "Reply", sfSymbol: "arrowshape.turn.up.left" }
1526− , { id: "sep", title: "-", sfSymbol: "" }
1527− , { id: "delete", title: "Delete", sfSymbol: "trash" }
1528− ]
1529− , onSelectItem: handler_ (pure unit)
1530− , style: tw "px-3 mb-1"
1531− }
1532− ( view { style: Style.style { alignSelf: align, maxWidth: 320.0 } }
1533− [ if senderLabel == "" then text { style: Style.style { height: 0.0 } } ""
1534− else text { style: tw "text-xs mb-0.5" <> Style.style { color: p.dimFg } } senderLabel
1535− , if bigEmoji then
1536− text
1537− { style: Style.style { fontSize: 64.0, lineHeight: 72.0 } }
1538− msg.body
1539− else
1540− view
1541− { style: tw "rounded-2xl px-3 py-2"
1542− <> Style.style
1543− { backgroundColor: if isMine then sentBubbleBg else receivedBubbleBg }
1544− }
1545− [ text
1546− { style: tw "text-sm"
1547− <> Style.style { color: if isMine then "#FFFFFF" else (if p.isDark then "#FFFFFF" else "#000000") }
1548− }
1549− msg.body
1550− ]
1551− ]
1552− )
1553−
1554− activeRoomName = case activeRoom of
1555− Nothing -> "Select a room"
1556− Just rid -> fromMaybe rid (map _.name (filter (\r -> r.roomId == rid) rooms !! 0))
1557−
1558− chatView sess = sidebarLayout
1559− { sidebar: roomSidebar sess
1560− , sidebarWidth: 220.0
1561− , content: view { style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
1562− [ view
1563− { style: tw "px-4 py-2 border-b"
1564− <> Style.style { borderBottomWidth: 0.5, borderColor: p.dimFg, backgroundColor: "transparent" }
1565− }
1566− [ text { style: tw "text-base font-semibold" <> Style.style { color: p.fg } } activeRoomName ]
1567− , nativePatternBackground
1568− { patternColor: if p.isDark then "#FFFFFF" else "#000000"
1569− , backgroundColor2: chatBg
1570− , patternOpacity: if p.isDark then 0.04 else 0.06
1571− , patternScale: 1.0
1572− , style: tw "flex-1"
1573− }
1574− ( nativeScrollView { scrollToBottom: length messages, style: tw "flex-1" <> Style.style { backgroundColor: "transparent" } }
1575− ( view { style: tw "py-2 pr-3" }
1576− (mapWithIndex (messageBubble session) messages)
1577− )
1578− )
1579− , view
1580− { style: tw "flex-row items-center px-3 py-2"
1581− <> Style.style { borderTopWidth: 0.5, borderColor: p.dimFg, backgroundColor: "transparent" }
1582− }
1583− ( case activeRoom of
1584− Nothing ->
1585− [ text { style: tw "text-sm" <> Style.style { color: p.dimFg } } "Select a room to start chatting" ]
1586− Just rid ->
1587− [ nativeTextField
1588− { text: inputText
1589− , placeholder: "Message..."
1590− , search: false
1591− , rounded: true
1592− , onChangeText: extractString "text" setInputText
1593− , onSubmit: extractString "text" \t -> sendMessage sess rid t
1594− , style: tw "flex-1" <> Style.style { height: 28.0 }
1595− }
1596− , nativeButton
1597− { title: ""
1598− , sfSymbol: "paperplane.fill"
1599− , bezelStyle: T.toolbar
1600− , onPress: handler_ (sendMessage sess rid inputText)
1601− , style: Style.style { height: 28.0, width: 36.0, marginLeft: 8.0 }
1602− }
1603− ]
1604− )
1605− ]
1606− }
1607−
1608− pure do
1609− case session of
1610− Nothing -> loginForm
1611− Just sess -> chatView sess
1612−
1613−eventKind :: Matrix.MatrixEvent -> String
1614−eventKind ev = case Matrix.getEventMsgType ev of
1615− "m.image" -> "gif"
1616− _ -> "text"
1617−
1618−elem :: forall a. Eq a => a -> Array a -> Boolean
1619−elem x xs = not (null (filter (_ == x) xs))
1620−
1621−foreign import setInterval :: Int -> Effect Unit -> Effect Int
1622−foreign import clearInterval :: Int -> Effect Unit
1623−
1624−-- UI helpers
1625−sectionTitle :: String -> String -> JSX
1626−sectionTitle color title =
1627− text { style: tw "text-sm font-semibold mt-4 mb-1" <> Style.style { color } } title
1628−
1629−card :: String -> Array JSX -> JSX
1630−card bg children =
1631− view { style: tw "rounded-lg p-3 mb-2" <> Style.style { backgroundColor: bg, overflow: "hidden" } }
1632− children
1633−
1634−label :: String -> String -> JSX
1635−label color str =
1636− text { style: tw "ml-3 text-sm" <> Style.style { color } } str
1637−
1638−round :: Number -> Int
1639−round n = unsafeRound n
1640−
1641−foreign import unsafeRound :: Number -> Int
1642−foreign import getFieldJSON :: String -> forall r. r -> String
1643−foreign import isSingleEmoji :: String -> Boolean
package.json+2
@@ -5,7 +5,9 @@
55 "private": true,
66 "devDependencies": {
77 "@types/bun": "latest",
8+ "playwright": "^1.58.2",
89 "react": "^19.2.4",
10+ "vite": "^7.3.1",
911 "vitest": "^4.0.18"
1012 },
1113 "peerDependencies": {
spago.lock+617−42
@@ -5,17 +5,65 @@
55 "path": "./",
66 "core": {
77 "dependencies": [
8− "aff",
8+ {
9+ "aff": ">=7.1.0 <8.0.0"
10+ },
11+ {
12+ "effect": ">=4.0.0 <5.0.0"
13+ },
14+ {
15+ "foreign": ">=7.0.0 <8.0.0"
16+ },
17+ {
18+ "functions": ">=6.0.0 <7.0.0"
19+ },
20+ {
21+ "maybe": ">=6.0.0 <7.0.0"
22+ },
23+ {
24+ "newtype": ">=5.0.0 <6.0.0"
25+ },
26+ {
27+ "nullable": ">=6.0.0 <7.0.0"
28+ },
29+ {
30+ "prelude": ">=6.0.1 <7.0.0"
31+ },
32+ {
33+ "react-basic": ">=17.0.0 <18.0.0"
34+ },
35+ {
36+ "react-basic-hooks": ">=8.2.0 <9.0.0"
37+ },
38+ {
39+ "tuples": ">=7.0.0 <8.0.0"
40+ },
41+ {
42+ "type-equality": ">=4.0.1 <5.0.0"
43+ },
44+ {
45+ "typelevel-prelude": ">=7.0.0 <8.0.0"
46+ },
47+ {
48+ "unsafe-coerce": ">=6.0.0 <7.0.0"
49+ }
50+ ]
51+ },
52+ "test": {
53+ "dependencies": []
54+ }
55+ },
56+ "yoga-react-native-docs": {
57+ "path": "docs",
58+ "core": {
59+ "dependencies": [
60+ "deku-core",
61+ "deku-dom",
62+ "deku-spa",
963 "effect",
10− "foreign",
11− "functions",
12− "nullable",
64+ "hyrule",
1365 "prelude",
14− "react-basic",
15− "react-basic-hooks",
16− "type-equality",
17− "typelevel-prelude",
18− "unsafe-coerce"
66+ "tuples"
1967 ]
2068 },
2169 "test": {
@@ -51,7 +99,7 @@
5199 },
52100 "package_set": {
53101 "address": {
54− "registry": "51.2.0"
102+ "registry": "58.0.0"
55103 },
56104 "compiler": ">=0.15.15 <0.16.0",
57105 "content": {
@@ -77,7 +125,7 @@
77125 "argonaut-traversals": "10.0.0",
78126 "argparse-basic": "2.0.0",
79127 "array-builder": "0.1.2",
80− "array-search": "0.5.6",
128+ "array-search": "0.6.0",
81129 "arraybuffer": "13.2.0",
82130 "arraybuffer-builder": "3.1.0",
83131 "arraybuffer-types": "3.0.2",
@@ -86,7 +134,7 @@
86134 "arrays-zipper": "2.0.1",
87135 "ask": "1.0.0",
88136 "assert": "6.0.0",
89− "assert-multiple": "0.3.4",
137+ "assert-multiple": "0.4.0",
90138 "avar": "5.0.0",
91139 "b64": "0.0.8",
92140 "barbies": "1.0.1",
@@ -101,7 +149,7 @@
101149 "canvas-action": "9.0.0",
102150 "cartesian": "1.0.6",
103151 "catenable-lists": "7.0.0",
104− "cbor-stream": "1.0.4",
152+ "cbor-stream": "1.3.0",
105153 "chameleon": "1.0.0",
106154 "chameleon-halogen": "1.0.3",
107155 "chameleon-react-basic": "1.1.0",
@@ -130,7 +178,7 @@
130178 "coroutines": "7.0.0",
131179 "css": "6.0.0",
132180 "css-frameworks": "1.0.1",
133− "csv-stream": "2.0.2",
181+ "csv-stream": "2.3.0",
134182 "data-mvc": "0.0.2",
135183 "datetime": "6.1.0",
136184 "datetime-parsing": "0.2.0",
@@ -148,23 +196,23 @@
148196 "dotenv": "4.0.3",
149197 "droplet": "0.6.0",
150198 "dts": "1.0.0",
151− "dual-numbers": "1.0.2",
199+ "dual-numbers": "1.0.3",
152200 "dynamic-buffer": "3.0.1",
153201 "echarts-simple": "0.0.1",
154202 "effect": "4.0.0",
155203 "either": "6.1.0",
156− "elmish": "0.11.3",
204+ "elmish": "0.13.0",
157205 "elmish-enzyme": "0.1.1",
158− "elmish-hooks": "0.10.0",
159− "elmish-html": "0.8.2",
206+ "elmish-hooks": "0.10.3",
207+ "elmish-html": "0.9.0",
160208 "elmish-testing-library": "0.3.2",
161209 "email-validate": "7.0.0",
162210 "encoding": "0.0.9",
163211 "enums": "6.0.1",
164− "env-names": "0.3.4",
212+ "env-names": "0.4.0",
165213 "error": "2.0.0",
166214 "eta-conversion": "0.3.2",
167− "exceptions": "6.0.0",
215+ "exceptions": "6.1.0",
168216 "exists": "6.0.0",
169217 "exitcodes": "4.0.0",
170218 "expect-inferred": "3.0.0",
@@ -207,7 +255,7 @@
207255 "generate-values": "1.0.1",
208256 "generic-router": "0.0.1",
209257 "geojson": "0.0.5",
210− "geometry-plane": "1.0.3",
258+ "geometria": "2.2.0",
211259 "gojs": "0.1.1",
212260 "grain": "3.0.0",
213261 "grain-router": "3.0.0",
@@ -237,6 +285,7 @@
237285 "homogeneous": "0.4.0",
238286 "http-methods": "6.0.0",
239287 "httpurple": "4.0.0",
288+ "huffman": "0.4.0",
240289 "humdrum": "0.0.1",
241290 "hyrule": "2.3.8",
242291 "identity": "6.0.0",
@@ -284,7 +333,7 @@
284333 "logging-journald": "0.4.0",
285334 "lumi-components": "18.0.0",
286335 "machines": "7.0.0",
287− "maps-eager": "0.4.1",
336+ "maps-eager": "0.5.0",
288337 "marionette": "1.0.0",
289338 "marionette-react-basic-hooks": "0.1.1",
290339 "marked": "0.1.0",
@@ -320,7 +369,7 @@
320369 "node-child-process": "11.1.0",
321370 "node-event-emitter": "3.0.0",
322371 "node-execa": "5.0.0",
323− "node-fs": "9.1.0",
372+ "node-fs": "9.2.0",
324373 "node-glob-basic": "1.3.0",
325374 "node-http": "9.1.0",
326375 "node-http2": "1.1.1",
@@ -329,9 +378,9 @@
329378 "node-os": "5.1.0",
330379 "node-path": "5.0.0",
331380 "node-process": "11.2.0",
332− "node-readline": "8.1.0",
381+ "node-readline": "8.1.1",
333382 "node-sqlite3": "8.0.0",
334− "node-stream-pipes": "1.4.1",
383+ "node-stream-pipes": "2.1.6",
335384 "node-streams": "9.0.0",
336385 "node-tls": "0.3.1",
337386 "node-url": "7.0.1",
@@ -340,15 +389,17 @@
340389 "now": "6.0.0",
341390 "npm-package-json": "2.0.0",
342391 "nullable": "6.0.0",
343− "numberfield": "0.1.0",
392+ "numberfield": "0.2.2",
344393 "numbers": "9.0.1",
345394 "oak": "3.1.1",
346395 "oak-debug": "1.2.2",
347396 "object-maps": "0.3.0",
348397 "ocarina": "1.5.4",
349− "open-folds": "6.3.0",
350− "open-memoize": "6.1.0",
351− "open-pairing": "6.1.0",
398+ "oooooooooorrrrrrrmm-lib": "0.0.1",
399+ "open-folds": "6.4.0",
400+ "open-memoize": "6.2.0",
401+ "open-mkdirp-aff": "1.2.0",
402+ "open-pairing": "6.2.0",
352403 "options": "7.0.0",
353404 "optparse": "5.0.1",
354405 "ordered-collections": "3.2.0",
@@ -371,11 +422,12 @@
371422 "pointed-list": "0.5.1",
372423 "polymorphic-vectors": "4.0.0",
373424 "posix-types": "6.0.0",
374− "postgresql": "1.6.3",
425+ "postgresql": "2.0.19",
375426 "precise": "6.0.0",
376427 "precise-datetime": "7.0.0",
377428 "prelude": "6.0.1",
378429 "prettier-printer": "3.0.0",
430+ "priority-queue": "0.1.2",
379431 "profunctor": "6.0.1",
380432 "profunctor-lenses": "8.0.0",
381433 "protobuf": "4.3.0",
@@ -415,6 +467,7 @@
415467 "record-studio": "1.0.4",
416468 "refs": "6.0.0",
417469 "remotedata": "5.0.1",
470+ "repr": "0.5.0",
418471 "resize-observer": "1.0.0",
419472 "resource": "2.0.1",
420473 "resourcet": "1.0.0",
@@ -422,6 +475,7 @@
422475 "return": "0.2.0",
423476 "ring-modules": "5.0.1",
424477 "rito": "0.3.4",
478+ "roman": "0.4.0",
425479 "rough-notation": "1.0.2",
426480 "routing": "11.0.0",
427481 "routing-duplex": "0.7.0",
@@ -440,14 +494,17 @@
440494 "simple-ulid": "3.0.0",
441495 "sized-matrices": "1.0.0",
442496 "sized-vectors": "5.0.2",
443− "slug": "3.0.8",
497+ "slug": "3.1.0",
444498 "small-ffi": "4.0.1",
445499 "soundfonts": "4.1.0",
446− "sparse-matrices": "1.3.0",
447− "sparse-polynomials": "2.0.5",
448− "spec": "7.6.0",
449− "spec-mocha": "5.1.0",
450− "spec-quickcheck": "5.0.0",
500+ "sparse-matrices": "2.0.1",
501+ "sparse-polynomials": "3.0.1",
502+ "spec": "8.0.0",
503+ "spec-discovery": "8.3.0",
504+ "spec-mocha": "5.1.1",
505+ "spec-node": "0.0.1",
506+ "spec-quickcheck": "5.0.2",
507+ "spec-reporter-xunit": "0.7.1",
451508 "splitmix": "2.1.0",
452509 "ssrs": "1.0.0",
453510 "st": "6.2.0",
@@ -469,9 +526,11 @@
469526 "thermite": "6.3.1",
470527 "thermite-dom": "0.3.1",
471528 "these": "6.0.0",
529+ "threading": "0.0.3",
530+ "tldr": "0.0.0",
472531 "toestand": "0.9.0",
473532 "transformation-matrix": "1.0.1",
474− "transformers": "6.0.0",
533+ "transformers": "6.1.0",
475534 "tree-rose": "4.0.2",
476535 "ts-bridge": "4.0.0",
477536 "tuples": "7.0.0",
@@ -510,7 +569,7 @@
510569 "vectors": "2.1.0",
511570 "versions": "7.0.0",
512571 "visx": "0.0.2",
513− "web-clipboard": "5.0.0",
572+ "web-clipboard": "6.0.0",
514573 "web-cssom": "2.0.0",
515574 "web-cssom-view": "0.1.0",
516575 "web-dom": "6.0.0",
@@ -549,6 +608,30 @@
549608 }
550609 },
551610 "extra_packages": {
611+ "deku-core": {
612+ "git": "https://github.com/mikesol/purescript-deku.git",
613+ "ref": "main",
614+ "subdir": "deku-core"
615+ },
616+ "deku-css": {
617+ "git": "https://github.com/mikesol/purescript-deku.git",
618+ "ref": "main",
619+ "subdir": "deku-css"
620+ },
621+ "deku-dom": {
622+ "git": "https://github.com/mikesol/purescript-deku.git",
623+ "ref": "main",
624+ "subdir": "deku-dom"
625+ },
626+ "deku-spa": {
627+ "git": "https://github.com/mikesol/purescript-deku.git",
628+ "ref": "main",
629+ "subdir": "deku-spa"
630+ },
631+ "hyrule": {
632+ "git": "https://github.com/mikesol/purescript-hyrule.git",
633+ "ref": "main"
634+ },
552635 "yoga-react": {
553636 "git": "https://github.com/rowtype-yoga/purescript-yoga-react.git",
554637 "ref": "main"
@@ -613,6 +696,20 @@
613696 "unsafe-coerce"
614697 ]
615698 },
699+ "avar": {
700+ "type": "registry",
701+ "version": "5.0.0",
702+ "integrity": "sha256-9tAeYrHAFSPkbJ7UCtpyLq7QawSrwf/nwLT9BcmfUx8=",
703+ "dependencies": [
704+ "aff",
705+ "effect",
706+ "either",
707+ "exceptions",
708+ "functions",
709+ "maybe",
710+ "prelude"
711+ ]
712+ },
616713 "bifunctors": {
617714 "type": "registry",
618715 "version": "6.0.0",
@@ -625,6 +722,20 @@
625722 "tuples"
626723 ]
627724 },
725+ "catenable-lists": {
726+ "type": "registry",
727+ "version": "7.0.0",
728+ "integrity": "sha256-7+i/MHBLIRh604iCNT4ENWRWE/wuHIotER4kpdX9Ve0=",
729+ "dependencies": [
730+ "control",
731+ "foldable-traversable",
732+ "lists",
733+ "maybe",
734+ "prelude",
735+ "tuples",
736+ "unfoldable"
737+ ]
738+ },
628739 "console": {
629740 "type": "registry",
630741 "version": "6.1.0",
@@ -688,6 +799,85 @@
688799 "tuples"
689800 ]
690801 },
802+ "debug": {
803+ "type": "registry",
804+ "version": "6.0.2",
805+ "integrity": "sha256-d/EzRm/J2JyzvlIeo2Ex78Y7HRB0IXBitnVJywL4gHk=",
806+ "dependencies": [
807+ "functions",
808+ "prelude"
809+ ]
810+ },
811+ "deku-core": {
812+ "type": "git",
813+ "url": "https://github.com/mikesol/purescript-deku.git",
814+ "rev": "4d18f6ccebb4a59cdd2a6d2d9e2466349bf8df20",
815+ "subdir": "deku-core",
816+ "dependencies": [
817+ "aff",
818+ "arrays",
819+ "catenable-lists",
820+ "control",
821+ "debug",
822+ "effect",
823+ "either",
824+ "fast-vect",
825+ "filterable",
826+ "foldable-traversable",
827+ "foreign-object",
828+ "free",
829+ "heterogeneous",
830+ "hyrule",
831+ "maybe",
832+ "newtype",
833+ "nullable",
834+ "ordered-collections",
835+ "prelude",
836+ "profunctor",
837+ "quickcheck",
838+ "record",
839+ "safe-coerce",
840+ "st",
841+ "strings",
842+ "stringutils",
843+ "tldr",
844+ "transformers",
845+ "tuples",
846+ "typelevel-prelude",
847+ "unsafe-coerce",
848+ "unsafe-reference",
849+ "untagged-union",
850+ "web-dom",
851+ "web-dom-parser",
852+ "web-events",
853+ "web-html",
854+ "web-uievents",
855+ "yoga-json"
856+ ]
857+ },
858+ "deku-dom": {
859+ "type": "git",
860+ "url": "https://github.com/mikesol/purescript-deku.git",
861+ "rev": "4d18f6ccebb4a59cdd2a6d2d9e2466349bf8df20",
862+ "subdir": "deku-dom",
863+ "dependencies": [
864+ "deku-core",
865+ "hyrule",
866+ "web-html",
867+ "web-pointerevents",
868+ "web-touchevents",
869+ "web-uievents"
870+ ]
871+ },
872+ "deku-spa": {
873+ "type": "git",
874+ "url": "https://github.com/mikesol/purescript-deku.git",
875+ "rev": "4d18f6ccebb4a59cdd2a6d2d9e2466349bf8df20",
876+ "subdir": "deku-spa",
877+ "dependencies": [
878+ "deku-core"
879+ ]
880+ },
691881 "distributive": {
692882 "type": "registry",
693883 "version": "6.0.0",
@@ -738,8 +928,8 @@
738928 },
739929 "exceptions": {
740930 "type": "registry",
741− "version": "6.0.0",
742− "integrity": "sha256-z65UIKhOyVXzV8QUxtky4OiYVESNPegtuBFUdAddbTM=",
931+ "version": "6.1.0",
932+ "integrity": "sha256-nRrr+N0wk0TUFOWLR7e1n1oxcXo/X345bQUtoY9lW6A=",
743933 "dependencies": [
744934 "effect",
745935 "either",
@@ -755,6 +945,44 @@
755945 "unsafe-coerce"
756946 ]
757947 },
948+ "fast-vect": {
949+ "type": "registry",
950+ "version": "1.2.0",
951+ "integrity": "sha256-nGfcPN8IPIH6EmmCLTLNnAui0KcEt9RsH7O0vZ8HuvU=",
952+ "dependencies": [
953+ "arrays",
954+ "distributive",
955+ "filterable",
956+ "foldable-traversable",
957+ "lists",
958+ "maybe",
959+ "ordered-collections",
960+ "prelude",
961+ "profunctor",
962+ "tuples",
963+ "unfoldable",
964+ "unsafe-coerce"
965+ ]
966+ },
967+ "filterable": {
968+ "type": "registry",
969+ "version": "5.0.0",
970+ "integrity": "sha256-be7ci1Bzwud2waqoCtNGsTBZbt7fi+gCLvGddQQRJ3A=",
971+ "dependencies": [
972+ "arrays",
973+ "control",
974+ "either",
975+ "foldable-traversable",
976+ "identity",
977+ "lists",
978+ "maybe",
979+ "newtype",
980+ "ordered-collections",
981+ "prelude",
982+ "st",
983+ "tuples"
984+ ]
985+ },
758986 "foldable-traversable": {
759987 "type": "registry",
760988 "version": "6.0.0",
@@ -789,6 +1017,47 @@
7891017 "unsafe-coerce"
7901018 ]
7911019 },
1020+ "foreign-object": {
1021+ "type": "registry",
1022+ "version": "4.1.0",
1023+ "integrity": "sha256-x/Q7r80z/vmHRKvPwhYmHP/DnJciPtsyGfRTMzayKIU=",
1024+ "dependencies": [
1025+ "arrays",
1026+ "foldable-traversable",
1027+ "functions",
1028+ "gen",
1029+ "lists",
1030+ "maybe",
1031+ "prelude",
1032+ "st",
1033+ "tailrec",
1034+ "tuples",
1035+ "typelevel-prelude",
1036+ "unfoldable",
1037+ "unsafe-coerce"
1038+ ]
1039+ },
1040+ "free": {
1041+ "type": "registry",
1042+ "version": "7.1.0",
1043+ "integrity": "sha256-tUBInfUpRQEw4u94GWcYW7EYdmSdDHSRn88a+C0eltU=",
1044+ "dependencies": [
1045+ "catenable-lists",
1046+ "control",
1047+ "distributive",
1048+ "either",
1049+ "exists",
1050+ "foldable-traversable",
1051+ "invariant",
1052+ "lazy",
1053+ "maybe",
1054+ "prelude",
1055+ "tailrec",
1056+ "transformers",
1057+ "tuples",
1058+ "unsafe-coerce"
1059+ ]
1060+ },
7921061 "functions": {
7931062 "type": "registry",
7941063 "version": "6.0.0",
@@ -834,6 +1103,58 @@
8341103 "unfoldable"
8351104 ]
8361105 },
1106+ "heterogeneous": {
1107+ "type": "registry",
1108+ "version": "0.6.0",
1109+ "integrity": "sha256-nQgk2iHYe5LIi6ZzREgWaCU3y8oCwdhDSukT6xVMWdk=",
1110+ "dependencies": [
1111+ "either",
1112+ "foldable-traversable",
1113+ "functors",
1114+ "prelude",
1115+ "record",
1116+ "tuples",
1117+ "variant"
1118+ ]
1119+ },
1120+ "hyrule": {
1121+ "type": "git",
1122+ "url": "https://github.com/mikesol/purescript-hyrule.git",
1123+ "rev": "f508363b9ad7e2389ea8b3433f7e2ee1b1f8fa25",
1124+ "dependencies": [
1125+ "aff",
1126+ "arrays",
1127+ "avar",
1128+ "contravariant",
1129+ "control",
1130+ "datetime",
1131+ "effect",
1132+ "either",
1133+ "filterable",
1134+ "foldable-traversable",
1135+ "foreign-object",
1136+ "free",
1137+ "functors",
1138+ "js-timers",
1139+ "maybe",
1140+ "newtype",
1141+ "now",
1142+ "ordered-collections",
1143+ "partial",
1144+ "prelude",
1145+ "profunctor",
1146+ "random",
1147+ "refs",
1148+ "safe-coerce",
1149+ "st",
1150+ "tailrec",
1151+ "tuples",
1152+ "unsafe-coerce",
1153+ "web-events",
1154+ "web-html",
1155+ "web-uievents"
1156+ ]
1157+ },
8371158 "identity": {
8381159 "type": "registry",
8391160 "version": "6.0.0",
@@ -873,6 +1194,16 @@
8731194 "prelude"
8741195 ]
8751196 },
1197+ "js-bigints": {
1198+ "type": "registry",
1199+ "version": "2.2.1",
1200+ "integrity": "sha256-r1rE6Olv5b3JrP32lY2uKJ+eBRTsrVnC983K8VWwqe4=",
1201+ "dependencies": [
1202+ "integers",
1203+ "maybe",
1204+ "prelude"
1205+ ]
1206+ },
8761207 "js-date": {
8771208 "type": "registry",
8781209 "version": "8.0.0",
@@ -888,6 +1219,15 @@
8881219 "prelude"
8891220 ]
8901221 },
1222+ "js-timers": {
1223+ "type": "registry",
1224+ "version": "6.1.0",
1225+ "integrity": "sha256-mYEL4zjFOzrDeVCnR9Hg3pcZaBZilB6zPGaZ4fzRmzg=",
1226+ "dependencies": [
1227+ "effect",
1228+ "prelude"
1229+ ]
1230+ },
8911231 "lazy": {
8921232 "type": "registry",
8931233 "version": "6.0.0",
@@ -899,6 +1239,20 @@
8991239 "prelude"
9001240 ]
9011241 },
1242+ "lcg": {
1243+ "type": "registry",
1244+ "version": "4.0.0",
1245+ "integrity": "sha256-bUYyfXYmjTn7zSpj2hTiePywzsuD80fCjkonsrpdGOw=",
1246+ "dependencies": [
1247+ "effect",
1248+ "integers",
1249+ "maybe",
1250+ "numbers",
1251+ "partial",
1252+ "prelude",
1253+ "random"
1254+ ]
1255+ },
9021256 "lists": {
9031257 "type": "registry",
9041258 "version": "7.0.0",
@@ -918,6 +1272,20 @@
9181272 "unfoldable"
9191273 ]
9201274 },
1275+ "literals": {
1276+ "type": "registry",
1277+ "version": "1.0.2",
1278+ "integrity": "sha256-ZkGRFD6gb8etYvyMYpX1Z7lx1Tql7R1VGnD1Dzinup8=",
1279+ "dependencies": [
1280+ "integers",
1281+ "maybe",
1282+ "numbers",
1283+ "partial",
1284+ "prelude",
1285+ "typelevel-prelude",
1286+ "unsafe-coerce"
1287+ ]
1288+ },
9211289 "maybe": {
9221290 "type": "registry",
9231291 "version": "6.0.0",
@@ -1065,6 +1433,49 @@
10651433 "tuples"
10661434 ]
10671435 },
1436+ "quickcheck": {
1437+ "type": "registry",
1438+ "version": "8.0.1",
1439+ "integrity": "sha256-SfW+maesCt0jnnFhfrgJZGPcWLj8oFcGbcDIVklmdzc=",
1440+ "dependencies": [
1441+ "arrays",
1442+ "console",
1443+ "control",
1444+ "effect",
1445+ "either",
1446+ "enums",
1447+ "exceptions",
1448+ "foldable-traversable",
1449+ "gen",
1450+ "identity",
1451+ "integers",
1452+ "lazy",
1453+ "lcg",
1454+ "lists",
1455+ "maybe",
1456+ "newtype",
1457+ "nonempty",
1458+ "numbers",
1459+ "partial",
1460+ "prelude",
1461+ "record",
1462+ "st",
1463+ "strings",
1464+ "tailrec",
1465+ "transformers",
1466+ "tuples"
1467+ ]
1468+ },
1469+ "random": {
1470+ "type": "registry",
1471+ "version": "6.0.0",
1472+ "integrity": "sha256-7sZRo6P9UIiXDZ9s2QL9T8h3wlIwLdQrR4+oZKOijQw=",
1473+ "dependencies": [
1474+ "effect",
1475+ "integers",
1476+ "prelude"
1477+ ]
1478+ },
10681479 "react-basic": {
10691480 "type": "registry",
10701481 "version": "17.0.0",
@@ -1170,6 +1581,20 @@
11701581 "unsafe-coerce"
11711582 ]
11721583 },
1584+ "stringutils": {
1585+ "type": "registry",
1586+ "version": "0.0.12",
1587+ "integrity": "sha256-byI+JsFtMjrxwfNJJTOGPIE6e+LuFG/ztjUr+YhUGlQ=",
1588+ "dependencies": [
1589+ "arrays",
1590+ "functions",
1591+ "integers",
1592+ "maybe",
1593+ "prelude",
1594+ "strings",
1595+ "unsafe-coerce"
1596+ ]
1597+ },
11731598 "tailrec": {
11741599 "type": "registry",
11751600 "version": "6.1.0",
@@ -1185,10 +1610,18 @@
11851610 "refs"
11861611 ]
11871612 },
1613+ "tldr": {
1614+ "type": "registry",
1615+ "version": "0.0.0",
1616+ "integrity": "sha256-cgG3625BnwVtM+CbuDtBt++AX4vu8e1QFarmDiAKtE8=",
1617+ "dependencies": [
1618+ "prelude"
1619+ ]
1620+ },
11881621 "transformers": {
11891622 "type": "registry",
1190− "version": "6.0.0",
1191− "integrity": "sha256-Nl3yXAIeeTzP6MyrkazkK7pbgeVqUm+ynLv7U5cpJYQ=",
1623+ "version": "6.1.0",
1624+ "integrity": "sha256-QJmgNT/y7ljPHSsXaW4zxF/982BFiQ90KNL1g/NtEOQ=",
11921625 "dependencies": [
11931626 "control",
11941627 "distributive",
@@ -1201,6 +1634,7 @@
12011634 "maybe",
12021635 "newtype",
12031636 "prelude",
1637+ "st",
12041638 "tailrec",
12051639 "tuples",
12061640 "unfoldable"
@@ -1257,6 +1691,38 @@
12571691 "prelude"
12581692 ]
12591693 },
1694+ "untagged-union": {
1695+ "type": "registry",
1696+ "version": "1.0.0",
1697+ "integrity": "sha256-MQvpb13TZEBoi9n/AoQ7ugniSQqbN6XQmAjii9pqJRk=",
1698+ "dependencies": [
1699+ "either",
1700+ "foreign",
1701+ "foreign-object",
1702+ "literals",
1703+ "maybe",
1704+ "newtype",
1705+ "prelude",
1706+ "tuples",
1707+ "unsafe-coerce"
1708+ ]
1709+ },
1710+ "variant": {
1711+ "type": "registry",
1712+ "version": "8.0.0",
1713+ "integrity": "sha256-Ydrgl8liWEwYQOWW76PdZtg3vxKpoKgsnmFyTLgZkoU=",
1714+ "dependencies": [
1715+ "control",
1716+ "enums",
1717+ "foldable-traversable",
1718+ "lists",
1719+ "maybe",
1720+ "partial",
1721+ "prelude",
1722+ "type-equality",
1723+ "unsafe-coerce"
1724+ ]
1725+ },
12601726 "web-dom": {
12611727 "type": "registry",
12621728 "version": "6.0.0",
@@ -1272,6 +1738,19 @@
12721738 "web-events"
12731739 ]
12741740 },
1741+ "web-dom-parser": {
1742+ "type": "registry",
1743+ "version": "8.0.0",
1744+ "integrity": "sha256-g8utHk8RtXOyM6B+O0ko247v4K5fdbaj0wjztHEc53U=",
1745+ "dependencies": [
1746+ "arrays",
1747+ "effect",
1748+ "either",
1749+ "maybe",
1750+ "prelude",
1751+ "web-dom"
1752+ ]
1753+ },
12751754 "web-events": {
12761755 "type": "registry",
12771756 "version": "4.0.0",
@@ -1333,6 +1812,21 @@
13331812 "web-storage"
13341813 ]
13351814 },
1815+ "web-pointerevents": {
1816+ "type": "registry",
1817+ "version": "2.0.0",
1818+ "integrity": "sha256-mQbm1V36u3It2WdYMAXtzLBJl3uRAhN4BjeZTw3LWN8=",
1819+ "dependencies": [
1820+ "effect",
1821+ "maybe",
1822+ "prelude",
1823+ "unsafe-coerce",
1824+ "web-dom",
1825+ "web-events",
1826+ "web-html",
1827+ "web-uievents"
1828+ ]
1829+ },
13361830 "web-storage": {
13371831 "type": "registry",
13381832 "version": "5.0.0",
@@ -1346,6 +1840,73 @@
13461840 "web-events"
13471841 ]
13481842 },
1843+ "web-touchevents": {
1844+ "type": "registry",
1845+ "version": "4.0.0",
1846+ "integrity": "sha256-H02c53BJlHDlAgogj/x8bM7PagJjhMMrMPYy9I7Rf0s=",
1847+ "dependencies": [
1848+ "functions",
1849+ "maybe",
1850+ "nullable",
1851+ "prelude",
1852+ "unsafe-coerce",
1853+ "web-events",
1854+ "web-uievents"
1855+ ]
1856+ },
1857+ "web-uievents": {
1858+ "type": "registry",
1859+ "version": "5.0.0",
1860+ "integrity": "sha256-yh9uVLIGu8wE1+jwLehtfsRfR1eyiShhIMMhBPqKybE=",
1861+ "dependencies": [
1862+ "effect",
1863+ "enums",
1864+ "maybe",
1865+ "nullable",
1866+ "prelude",
1867+ "unsafe-coerce",
1868+ "web-events",
1869+ "web-html"
1870+ ]
1871+ },
1872+ "yoga-json": {
1873+ "type": "registry",
1874+ "version": "5.1.0",
1875+ "integrity": "sha256-DK+KyaQ7YyWo8p3SuZPPEMoVKy6HF1iV4rGFPXgBygw=",
1876+ "dependencies": [
1877+ "arrays",
1878+ "bifunctors",
1879+ "control",
1880+ "datetime",
1881+ "effect",
1882+ "either",
1883+ "exceptions",
1884+ "foldable-traversable",
1885+ "foreign",
1886+ "foreign-object",
1887+ "free",
1888+ "identity",
1889+ "integers",
1890+ "js-bigints",
1891+ "js-date",
1892+ "lists",
1893+ "maybe",
1894+ "newtype",
1895+ "nullable",
1896+ "numbers",
1897+ "ordered-collections",
1898+ "partial",
1899+ "prelude",
1900+ "record",
1901+ "strings",
1902+ "transformers",
1903+ "tuples",
1904+ "typelevel-prelude",
1905+ "unsafe-coerce",
1906+ "variant",
1907+ "yoga-tree"
1908+ ]
1909+ },
13491910 "yoga-react": {
13501911 "type": "git",
13511912 "url": "https://github.com/rowtype-yoga/purescript-yoga-react.git",
@@ -1356,6 +1917,20 @@
13561917 "react-basic",
13571918 "react-basic-hooks"
13581919 ]
1920+ },
1921+ "yoga-tree": {
1922+ "type": "registry",
1923+ "version": "1.0.0",
1924+ "integrity": "sha256-s8rIVEQkRi1VrMc92T9ceXcY0X6r5A4QqT6f7GgmDQI=",
1925+ "dependencies": [
1926+ "arrays",
1927+ "control",
1928+ "foldable-traversable",
1929+ "free",
1930+ "maybe",
1931+ "prelude",
1932+ "tailrec"
1933+ ]
13591934 }
13601935 }
13611936}
spago.yaml+41−13
@@ -1,21 +1,49 @@
11package:
2− dependencies:
3− - aff
4− - effect
5− - foreign
6− - functions
7− - nullable
8− - prelude
9− - react-basic
10− - react-basic-hooks
11− - type-equality
12− - typelevel-prelude
13− - unsafe-coerce
142 name: yoga-react-native
3+ publish:
4+ version: 0.2.1
5+ license: MIT
6+ location:
7+ githubOwner: rowtype-yoga
8+ githubRepo: purescript-yoga-react-native
9+ dependencies:
10+ - aff: ">=7.1.0 <8.0.0"
11+ - effect: ">=4.0.0 <5.0.0"
12+ - foreign: ">=7.0.0 <8.0.0"
13+ - functions: ">=6.0.0 <7.0.0"
14+ - maybe: ">=6.0.0 <7.0.0"
15+ - newtype: ">=5.0.0 <6.0.0"
16+ - nullable: ">=6.0.0 <7.0.0"
17+ - prelude: ">=6.0.1 <7.0.0"
18+ - react-basic: ">=17.0.0 <18.0.0"
19+ - react-basic-hooks: ">=8.2.0 <9.0.0"
20+ - tuples: ">=7.0.0 <8.0.0"
21+ - type-equality: ">=4.0.1 <5.0.0"
22+ - typelevel-prelude: ">=7.0.0 <8.0.0"
23+ - unsafe-coerce: ">=6.0.0 <7.0.0"
1524workspace:
1625 packageSet:
17− registry: 51.2.0
26+ registry: 58.0.0
1827 extraPackages:
1928 yoga-react:
2029 git: https://github.com/rowtype-yoga/purescript-yoga-react.git
2130 ref: main
31+ deku-core:
32+ git: https://github.com/mikesol/purescript-deku.git
33+ ref: main
34+ subdir: deku-core
35+ deku-dom:
36+ git: https://github.com/mikesol/purescript-deku.git
37+ ref: main
38+ subdir: deku-dom
39+ deku-css:
40+ git: https://github.com/mikesol/purescript-deku.git
41+ ref: main
42+ subdir: deku-css
43+ deku-spa:
44+ git: https://github.com/mikesol/purescript-deku.git
45+ ref: main
46+ subdir: deku-spa
47+ hyrule:
48+ git: https://github.com/mikesol/purescript-hyrule.git
49+ ref: main
src/Yoga/React/Native/Animated.js+24−1
@@ -1,4 +1,5 @@
11import { Animated, useAnimatedValue } from "react-native";
2+import { useRef, useEffect } from "react";
23
34export const newValueImpl = (n) => new Animated.Value(n);
45export const newValueXYImpl = (x, y) => new Animated.ValueXY({ x, y });
@@ -27,9 +28,31 @@ export const subtractImpl = (a) => (b) => Animated.subtract(a, b);
2728export const multiplyImpl = (a) => (b) => Animated.multiply(a, b);
2829export const divideImpl = (a) => (b) => Animated.divide(a, b);
2930export const moduloImpl = (a) => (n) => Animated.modulo(a, n);
30−export const diffClampImpl = (a) => (min) => (max) => Animated.diffClamp(a, min, max);
31+export const diffClampImpl = (a) => (min) => (max) =>
32+ Animated.diffClamp(a, min, max);
3133export const _animatedViewImpl = Animated.View;
3234export const _animatedTextImpl = Animated.Text;
3335export const _animatedImageImpl = Animated.Image;
3436export const _animatedScrollViewImpl = Animated.ScrollView;
3537export const useAnimatedValueImpl = (n) => () => useAnimatedValue(n);
38+
39+export const useSpringImpl = (target) => (config) => () => {
40+ const valueRef = useRef(null);
41+ const prevTargetRef = useRef(target);
42+ if (valueRef.current === null) {
43+ valueRef.current = new Animated.Value(target);
44+ }
45+ useEffect(() => {
46+ if (prevTargetRef.current !== target) {
47+ prevTargetRef.current = target;
48+ const anim = Animated.spring(valueRef.current, {
49+ toValue: target,
50+ useNativeDriver: false,
51+ ...config,
52+ });
53+ anim.start();
54+ return () => anim.stop();
55+ }
56+ });
57+ return valueRef.current;
58+};
src/Yoga/React/Native/Animated.purs+9
@@ -39,6 +39,8 @@ module Yoga.React.Native.Animated
3939 , animatedScrollView_
4040 , useAnimatedValue
4141 , UseAnimatedValue
42+ , useSpring
43+ , UseSpring
4244 , TimingConfig
4345 , SpringConfig
4446 , DecayConfig
@@ -272,3 +274,10 @@ foreign import data UseAnimatedValue :: Type -> Type
272274
273275useAnimatedValue :: Number -> Hook UseAnimatedValue AnimatedValue
274276useAnimatedValue n = unsafeHook (useAnimatedValueImpl n)
277+
278+foreign import useSpringImpl :: forall r. Number -> { | r } -> Effect AnimatedValue
279+
280+foreign import data UseSpring :: Type -> Type
281+
282+useSpring :: forall r. Number -> { | r } -> Hook UseSpring AnimatedValue
283+useSpring target config = unsafeHook (useSpringImpl target config)
src/Yoga/React/Native/MacOS/Alert.js+6−1
@@ -1,6 +1,11 @@
11import { NativeModules, Platform } from "react-native";
22
3−export const alertImpl = (style, title, message, buttons) => {
3+export const alertImpl = ({
4+ style = "warning",
5+ title = "",
6+ message = "",
7+ buttons = ["OK"],
8+}) => {
49 if (Platform.OS !== "macos") return;
510 const mod = NativeModules.MacOSAlertModule;
611 if (mod && mod.show) {
src/Yoga/React/Native/MacOS/Alert.purs+18−4
@@ -1,14 +1,28 @@
11module Yoga.React.Native.MacOS.Alert
22 ( macosAlert
3+ , AlertProps
34 ) where
45
56import Prelude
67
78import Effect (Effect)
8−import Effect.Uncurried (EffectFn4, runEffectFn4)
9+import Effect.Uncurried (EffectFn1, runEffectFn1)
10+import Prim.Row (class Union)
11+import Unsafe.Coerce (unsafeCoerce)
912import Yoga.React.Native.MacOS.Types (AlertStyle)
1013
11−foreign import alertImpl :: EffectFn4 AlertStyle String String (Array String) Unit
14+type AlertProps =
15+ ( style :: AlertStyle
16+ , title :: String
17+ , message :: String
18+ , buttons :: Array String
19+ )
1220
13−macosAlert :: AlertStyle -> String -> String -> Array String -> Effect Unit
14−macosAlert = runEffectFn4 alertImpl
21+foreign import alertImpl :: EffectFn1 (Record AlertProps) Unit
22+
23+macosAlert
24+ :: forall given missing
25+ . Union given missing AlertProps
26+ => { | given }
27+ -> Effect Unit
28+macosAlert r = runEffectFn1 alertImpl (unsafeCoerce r)
src/Yoga/React/Native/MacOS/AnimatedImage.js+2−16
@@ -1,16 +1,2 @@
1−import {
2− View,
3− Platform,
4− UIManager,
5− requireNativeComponent,
6−} from "react-native";
7−
8−var c = View;
9−if (Platform.OS === "macos") {
10− try {
11− if (UIManager.getViewManagerConfig("MacOSAnimatedImage"))
12− c = requireNativeComponent("MacOSAnimatedImage");
13− } catch (e) {}
14−}
15−
16−export const _animatedImageImpl = c;
1+import { requireNativeComponent } from "react-native";
2+export const animatedImageImpl = requireNativeComponent("MacOSAnimatedImage");
src/Yoga/React/Native/MacOS/AnimatedImage.purs+2−2
@@ -4,10 +4,10 @@ import React.Basic (ReactComponent)
44import Yoga.React.Native.Attributes (BaseAttributes)
55import Yoga.React.Native.Internal (FFINativeComponent_, createNativeElement_)
66
7−foreign import _animatedImageImpl :: forall props. ReactComponent props
7+foreign import animatedImageImpl :: forall props. ReactComponent props
88
99nativeAnimatedImage :: FFINativeComponent_ NativeAnimatedImageAttributes
10−nativeAnimatedImage = createNativeElement_ _animatedImageImpl
10+nativeAnimatedImage = createNativeElement_ animatedImageImpl
1111
1212type NativeAnimatedImageAttributes = BaseAttributes
1313 ( source :: String
src/Yoga/React/Native/MacOS/Box.js+2−6
@@ -1,6 +1,2 @@
1−import { requireNativeComponent, Platform, UIManager, View } from "react-native";
2−let c = View;
3−if (Platform.OS === "macos") {
4− try { if (UIManager.getViewManagerConfig("MacOSBox")) c = requireNativeComponent("MacOSBox"); } catch (e) {}
5−}
6−export const _boxImpl = c;
1+import { requireNativeComponent } from "react-native";
2+export const boxImpl = requireNativeComponent("MacOSBox");
src/Yoga/React/Native/MacOS/Box.purs+5−5
@@ -7,14 +7,14 @@ import React.Basic (ReactComponent)
77import Yoga.React.Native.Attributes (BaseAttributes)
88import Yoga.React.Native.Internal (FFINativeComponent, createNativeElement)
99
10−foreign import _boxImpl :: forall props. ReactComponent props
10+foreign import boxImpl :: forall props. ReactComponent props
1111
1212nativeBox :: FFINativeComponent NativeBoxAttributes
13−nativeBox = createNativeElement _boxImpl
13+nativeBox = createNativeElement boxImpl
1414
1515type NativeBoxAttributes = BaseAttributes
1616 ( boxTitle :: String
17− , fillColorStr :: String
18− , borderColorStr :: String
19− , cornerRadiusValue :: Number
17+ , fillColor2 :: String
18+ , borderColor2 :: String
19+ , radius :: Number
2020 )
src/Yoga/React/Native/MacOS/Button.js+10−14
@@ -1,14 +1,10 @@
1−import {
2− requireNativeComponent,
3− Platform,
4− UIManager,
5− View,
6−} from "react-native";
7−let c = View;
8−if (Platform.OS === "macos") {
9− try {
10− if (UIManager.getViewManagerConfig("NativeButton"))
11− c = requireNativeComponent("NativeButton");
12− } catch (e) {}
13−}
14−export const _buttonImpl = c;
1+import React from "react";
2+import { requireNativeComponent } from "react-native";
3+const NativeButton = requireNativeComponent("NativeButton");
4+export const buttonImpl = React.forwardRef((props, ref) => {
5+ const nativeProps = { ...props };
6+ if (typeof props.onPress === "function") {
7+ nativeProps.onPress = () => props.onPress();
8+ }
9+ return React.createElement(NativeButton, { ...nativeProps, ref });
10+});
src/Yoga/React/Native/MacOS/Button.purs+5−4
@@ -1,15 +1,16 @@
11module Yoga.React.Native.MacOS.Button (nativeButton, NativeButtonAttributes) where
22
3+import Prelude
4+import Effect (Effect)
35import React.Basic (ReactComponent)
4−import React.Basic.Events (EventHandler)
56import Yoga.React.Native.Attributes (BaseAttributes)
67import Yoga.React.Native.Internal (FFINativeComponent_, createNativeElement_)
78import Yoga.React.Native.MacOS.Types (BezelStyle)
89
9−foreign import _buttonImpl :: forall props. ReactComponent props
10+foreign import buttonImpl :: forall props. ReactComponent props
1011
1112nativeButton :: FFINativeComponent_ NativeButtonAttributes
12−nativeButton = createNativeElement_ _buttonImpl
13+nativeButton = createNativeElement_ buttonImpl
1314
1415type NativeButtonAttributes = BaseAttributes
1516 ( title :: String
@@ -18,5 +19,5 @@ type NativeButtonAttributes = BaseAttributes
1819 , destructive :: Boolean
1920 , primary :: Boolean
2021 , buttonEnabled :: Boolean
21− , onPress :: EventHandler
22+ , onPress :: Effect Unit
2223 )
src/Yoga/React/Native/MacOS/CameraView.jsnew file+2
@@ -0,0 +1,2 @@
1+import { requireNativeComponent } from "react-native";
2+export const cameraViewImpl = requireNativeComponent("MacOSCameraView");
src/Yoga/React/Native/MacOS/CameraView.pursnew file+17
@@ -0,0 +1,17 @@
1+module Yoga.React.Native.MacOS.CameraView
2+ ( nativeCameraView
3+ , NativeCameraViewAttributes
4+ ) where
5+
6+import React.Basic (ReactComponent)
7+import Yoga.React.Native.Attributes (BaseAttributes)
8+import Yoga.React.Native.Internal (FFINativeComponent_, createNativeElement_)
9+
10+foreign import cameraViewImpl :: forall props. ReactComponent props
11+
12+nativeCameraView :: FFINativeComponent_ NativeCameraViewAttributes
13+nativeCameraView = createNativeElement_ cameraViewImpl
14+
15+type NativeCameraViewAttributes = BaseAttributes
16+ ( active :: Boolean
17+ )
src/Yoga/React/Native/MacOS/Checkbox.js+10−6
@@ -1,6 +1,10 @@
1−import { requireNativeComponent, Platform, UIManager, View } from "react-native";
2−let c = View;
3−if (Platform.OS === "macos") {
4− try { if (UIManager.getViewManagerConfig("MacOSCheckbox")) c = requireNativeComponent("MacOSCheckbox"); } catch (e) {}
5−}
6−export const _checkboxImpl = c;
1+import React from "react";
2+import { requireNativeComponent } from "react-native";
3+const NativeCheckbox = requireNativeComponent("MacOSCheckbox");
4+export const checkboxImpl = React.forwardRef((props, ref) => {
5+ const nativeProps = { ...props };
6+ if (typeof props.onChange === "function") {
7+ nativeProps.onChange = (e) => props.onChange(!!e.nativeEvent.checked)();
8+ }
9+ return React.createElement(NativeCheckbox, { ...nativeProps, ref });
10+});
src/Yoga/React/Native/MacOS/Checkbox.purs+5−4
@@ -3,19 +3,20 @@ module Yoga.React.Native.MacOS.Checkbox
33 , NativeCheckboxAttributes
44 ) where
55
6+import Prelude
7+import Effect (Effect)
68import React.Basic (ReactComponent)
7−import React.Basic.Events (EventHandler)
89import Yoga.React.Native.Attributes (BaseAttributes)
910import Yoga.React.Native.Internal (FFINativeComponent_, createNativeElement_)
1011
11−foreign import _checkboxImpl :: forall props. ReactComponent props
12+foreign import checkboxImpl :: forall props. ReactComponent props
1213
1314nativeCheckbox :: FFINativeComponent_ NativeCheckboxAttributes
14−nativeCheckbox = createNativeElement_ _checkboxImpl
15+nativeCheckbox = createNativeElement_ checkboxImpl
1516
1617type NativeCheckboxAttributes = BaseAttributes
1718 ( checked :: Boolean
1819 , title :: String
1920 , enabled :: Boolean
20− , onChange :: EventHandler
21+ , onChange :: Boolean -> Effect Unit
2122 )
src/Yoga/React/Native/MacOS/ColorWell.js+10−6
@@ -1,6 +1,10 @@
1−import { requireNativeComponent, Platform, UIManager, View } from "react-native";
2−let c = View;
3−if (Platform.OS === "macos") {
4− try { if (UIManager.getViewManagerConfig("NativeColorWell")) c = requireNativeComponent("NativeColorWell"); } catch (e) {}
5−}
6−export const _colorWellImpl = c;
1+import React from "react";
2+import { requireNativeComponent } from "react-native";
3+const NativeColorWell = requireNativeComponent("NativeColorWell");
4+export const colorWellImpl = React.forwardRef((props, ref) => {
5+ const nativeProps = { ...props };
6+ if (typeof props.onChange === "function") {
7+ nativeProps.onChange = (e) => props.onChange(e.nativeEvent.color)();
8+ }
9+ return React.createElement(NativeColorWell, { ...nativeProps, ref });
10+});
src/Yoga/React/Native/MacOS/ColorWell.purs+5−4
@@ -1,17 +1,18 @@
11module Yoga.React.Native.MacOS.ColorWell (nativeColorWell, NativeColorWellAttributes) where
22
3+import Prelude
4+import Effect (Effect)
35import React.Basic (ReactComponent)
4−import React.Basic.Events (EventHandler)
56import Yoga.React.Native.Attributes (BaseAttributes)
67import Yoga.React.Native.Internal (FFINativeComponent_, createNativeElement_)
78
8−foreign import _colorWellImpl :: forall props. ReactComponent props
9+foreign import colorWellImpl :: forall props. ReactComponent props
910
1011nativeColorWell :: FFINativeComponent_ NativeColorWellAttributes
11−nativeColorWell = createNativeElement_ _colorWellImpl
12+nativeColorWell = createNativeElement_ colorWellImpl
1213
1314type NativeColorWellAttributes = BaseAttributes
1415 ( color :: String
1516 , minimal :: Boolean
16− , onChange :: EventHandler
17+ , onChange :: String -> Effect Unit
1718 )
src/Yoga/React/Native/MacOS/ComboBox.js+13−6
@@ -1,6 +1,13 @@
1−import { requireNativeComponent, Platform, UIManager, View } from "react-native";
2−let c = View;
3−if (Platform.OS === "macos") {
4− try { if (UIManager.getViewManagerConfig("MacOSComboBox")) c = requireNativeComponent("MacOSComboBox"); } catch (e) {}
5−}
6−export const _comboBoxImpl = c;
1+import React from "react";
2+import { requireNativeComponent } from "react-native";
3+const NativeComboBox = requireNativeComponent("MacOSComboBox");
4+export const comboBoxImpl = React.forwardRef((props, ref) => {
5+ const nativeProps = { ...props };
6+ if (typeof props.onChangeText === "function") {
7+ nativeProps.onChangeText = (e) => props.onChangeText(e.nativeEvent.text)();
8+ }
9+ if (typeof props.onSelectItem === "function") {
10+ nativeProps.onSelectItem = (e) => props.onSelectItem(e.nativeEvent.text)();
11+ }
12+ return React.createElement(NativeComboBox, { ...nativeProps, ref });
13+});
src/Yoga/React/Native/MacOS/ComboBox.purs+6−5
@@ -3,20 +3,21 @@ module Yoga.React.Native.MacOS.ComboBox
33 , NativeComboBoxAttributes
44 ) where
55
6+import Prelude
7+import Effect (Effect)
68import React.Basic (ReactComponent)
7−import React.Basic.Events (EventHandler)
89import Yoga.React.Native.Attributes (BaseAttributes)
910import Yoga.React.Native.Internal (FFINativeComponent_, createNativeElement_)
1011
11−foreign import _comboBoxImpl :: forall props. ReactComponent props
12+foreign import comboBoxImpl :: forall props. ReactComponent props
1213
1314nativeComboBox :: FFINativeComponent_ NativeComboBoxAttributes
14−nativeComboBox = createNativeElement_ _comboBoxImpl
15+nativeComboBox = createNativeElement_ comboBoxImpl
1516
1617type NativeComboBoxAttributes = BaseAttributes
1718 ( items :: Array String
1819 , text :: String
1920 , placeholder :: String
20− , onChangeText :: EventHandler
21− , onSelectItem :: EventHandler
21+ , onChangeText :: String -> Effect Unit
22+ , onSelectItem :: String -> Effect Unit
2223 )
src/Yoga/React/Native/MacOS/ContextMenu.js+11−16
@@ -1,16 +1,11 @@
1−import {
2− View,
3− Platform,
4− UIManager,
5− requireNativeComponent,
6−} from "react-native";
7−
8−var c = View;
9−if (Platform.OS === "macos") {
10− try {
11− if (UIManager.getViewManagerConfig("MacOSContextMenu"))
12− c = requireNativeComponent("MacOSContextMenu");
13− } catch (e) {}
14−}
15−
16−export const _contextMenuImpl = c;
1+import React from "react";
2+import { requireNativeComponent } from "react-native";
3+const NativeContextMenu = requireNativeComponent("MacOSContextMenu");
4+export const contextMenuImpl = React.forwardRef((props, ref) => {
5+ const nativeProps = { ...props };
6+ if (typeof props.onSelectItem === "function") {
7+ nativeProps.onSelectItem = (e) =>
8+ props.onSelectItem(e.nativeEvent.itemId)();
9+ }
10+ return React.createElement(NativeContextMenu, { ...nativeProps, ref });
11+});
src/Yoga/React/Native/MacOS/ContextMenu.purs+5−4
@@ -1,14 +1,15 @@
11module Yoga.React.Native.MacOS.ContextMenu (nativeContextMenu, NativeContextMenuAttributes, ContextMenuItem) where
22
3+import Prelude
4+import Effect (Effect)
35import React.Basic (ReactComponent)
4−import React.Basic.Events (EventHandler)
56import Yoga.React.Native.Attributes (BaseAttributes)
67import Yoga.React.Native.Internal (FFINativeComponent, createNativeElement)
78
8−foreign import _contextMenuImpl :: forall props. ReactComponent props
9+foreign import contextMenuImpl :: forall props. ReactComponent props
910
1011nativeContextMenu :: FFINativeComponent NativeContextMenuAttributes
11−nativeContextMenu = createNativeElement _contextMenuImpl
12+nativeContextMenu = createNativeElement contextMenuImpl
1213
1314type ContextMenuItem =
1415 { id :: String
@@ -18,5 +19,5 @@ type ContextMenuItem =
1819
1920type NativeContextMenuAttributes = BaseAttributes
2021 ( items :: Array ContextMenuItem
21− , onSelectItem :: EventHandler
22+ , onSelectItem :: String -> Effect Unit
2223 )
src/Yoga/React/Native/MacOS/DatePicker.js+10−6
@@ -1,6 +1,10 @@
1−import { requireNativeComponent, Platform, UIManager, View } from "react-native";
2−let c = View;
3−if (Platform.OS === "macos") {
4− try { if (UIManager.getViewManagerConfig("NativeDatePicker")) c = requireNativeComponent("NativeDatePicker"); } catch (e) {}
5−}
6−export const _datePickerImpl = c;
1+import React from "react";
2+import { requireNativeComponent } from "react-native";
3+const NativeDatePicker = requireNativeComponent("NativeDatePicker");
4+export const datePickerImpl = React.forwardRef((props, ref) => {
5+ const nativeProps = { ...props };
6+ if (typeof props.onChange === "function") {
7+ nativeProps.onChange = (e) => props.onChange(e.nativeEvent.date)();
8+ }
9+ return React.createElement(NativeDatePicker, { ...nativeProps, ref });
10+});
src/Yoga/React/Native/MacOS/DatePicker.purs+5−4
@@ -1,16 +1,17 @@
11module Yoga.React.Native.MacOS.DatePicker (nativeDatePicker, NativeDatePickerAttributes) where
22
3+import Prelude
4+import Effect (Effect)
35import React.Basic (ReactComponent)
4−import React.Basic.Events (EventHandler)
56import Yoga.React.Native.Attributes (BaseAttributes)
67import Yoga.React.Native.Internal (FFINativeComponent_, createNativeElement_)
78
8−foreign import _datePickerImpl :: forall props. ReactComponent props
9+foreign import datePickerImpl :: forall props. ReactComponent props
910
1011nativeDatePicker :: FFINativeComponent_ NativeDatePickerAttributes
11−nativeDatePicker = createNativeElement_ _datePickerImpl
12+nativeDatePicker = createNativeElement_ datePickerImpl
1213
1314type NativeDatePickerAttributes = BaseAttributes
1415 ( graphical :: Boolean
15− , onChange :: EventHandler
16+ , onChange :: String -> Effect Unit
1617 )
src/Yoga/React/Native/MacOS/DropZone.js+22−16
@@ -1,16 +1,22 @@
1−import {
2− View,
3− Platform,
4− UIManager,
5− requireNativeComponent,
6−} from "react-native";
7−
8−var c = View;
9−if (Platform.OS === "macos") {
10− try {
11− if (UIManager.getViewManagerConfig("MacOSDropZone"))
12− c = requireNativeComponent("MacOSDropZone");
13− } catch (e) {}
14−}
15−
16−export const _dropZoneImpl = c;
1+import React from "react";
2+import { requireNativeComponent } from "react-native";
3+const NativeDropZone = requireNativeComponent("MacOSDropZone");
4+export const dropZoneImpl = React.forwardRef((props, ref) => {
5+ const nativeProps = { ...props };
6+ if (typeof props.onFileDrop === "function") {
7+ nativeProps.onFileDrop = (e) => {
8+ const data = {
9+ files: e.nativeEvent.files || [],
10+ strings: e.nativeEvent.strings || [],
11+ };
12+ props.onFileDrop(data)();
13+ };
14+ }
15+ if (typeof props.onFilesDragEnter === "function") {
16+ nativeProps.onFilesDragEnter = () => props.onFilesDragEnter();
17+ }
18+ if (typeof props.onFilesDragExit === "function") {
19+ nativeProps.onFilesDragExit = () => props.onFilesDragExit();
20+ }
21+ return React.createElement(NativeDropZone, { ...nativeProps, ref });
22+});
src/Yoga/React/Native/MacOS/DropZone.purs+13−7
@@ -1,17 +1,23 @@
1−module Yoga.React.Native.MacOS.DropZone (nativeDropZone, NativeDropZoneAttributes) where
1+module Yoga.React.Native.MacOS.DropZone (nativeDropZone, NativeDropZoneAttributes, DroppedData) where
22
3+import Prelude
4+import Effect (Effect)
35import React.Basic (ReactComponent)
4−import React.Basic.Events (EventHandler)
56import Yoga.React.Native.Attributes (BaseAttributes)
67import Yoga.React.Native.Internal (FFINativeComponent, createNativeElement)
78
8−foreign import _dropZoneImpl :: forall props. ReactComponent props
9+foreign import dropZoneImpl :: forall props. ReactComponent props
910
1011nativeDropZone :: FFINativeComponent NativeDropZoneAttributes
11−nativeDropZone = createNativeElement _dropZoneImpl
12+nativeDropZone = createNativeElement dropZoneImpl
13+
14+type DroppedData =
15+ { files :: Array { path :: String, name :: String }
16+ , strings :: Array String
17+ }
1218
1319type NativeDropZoneAttributes = BaseAttributes
14− ( onFileDrop :: EventHandler
15− , onFilesDragEnter :: EventHandler
16− , onFilesDragExit :: EventHandler
20+ ( onFileDrop :: DroppedData -> Effect Unit
21+ , onFilesDragEnter :: Effect Unit
22+ , onFilesDragExit :: Effect Unit
1723 )
src/Yoga/React/Native/MacOS/Events.jsnew file+6
@@ -0,0 +1,6 @@
1+export const getField = (key) => (obj) => obj[key] ?? 0;
2+export const getFieldInt = (key) => (obj) => obj[key] | 0;
3+export const getFieldStr = (key) => (obj) => String(obj[key] ?? "");
4+export const getFieldBool = (key) => (obj) => Boolean(obj[key]);
5+export const getFieldArray = (key) => (obj) =>
6+ Array.isArray(obj[key]) ? obj[key].map(String) : [];
src/Yoga/React/Native/MacOS/Events.pursnew file+39
@@ -0,0 +1,39 @@
1+module Yoga.React.Native.MacOS.Events
2+ ( onNumber
3+ , onInt
4+ , onString
5+ , onBool
6+ , onStrings
7+ , getField
8+ , getFieldInt
9+ , getFieldStr
10+ , getFieldBool
11+ , getFieldArray
12+ ) where
13+
14+import Prelude
15+
16+import Effect (Effect)
17+import React.Basic.Events (EventHandler, handler, unsafeEventFn)
18+import Yoga.React.Native.Events (nativeEvent)
19+
20+foreign import getField :: forall r. String -> r -> Number
21+foreign import getFieldInt :: forall r. String -> r -> Int
22+foreign import getFieldStr :: forall r. String -> r -> String
23+foreign import getFieldBool :: forall r. String -> r -> Boolean
24+foreign import getFieldArray :: forall r. String -> r -> Array String
25+
26+onNumber :: String -> (Number -> Effect Unit) -> EventHandler
27+onNumber key cb = handler (nativeEvent >>> unsafeEventFn (getField key)) cb
28+
29+onInt :: String -> (Int -> Effect Unit) -> EventHandler
30+onInt key cb = handler (nativeEvent >>> unsafeEventFn (getFieldInt key)) cb
31+
32+onString :: String -> (String -> Effect Unit) -> EventHandler
33+onString key cb = handler (nativeEvent >>> unsafeEventFn (getFieldStr key)) cb
34+
35+onBool :: String -> (Boolean -> Effect Unit) -> EventHandler
36+onBool key cb = handler (nativeEvent >>> unsafeEventFn (getFieldBool key)) cb
37+
38+onStrings :: String -> (Array String -> Effect Unit) -> EventHandler
39+onStrings key cb = handler (nativeEvent >>> unsafeEventFn (getFieldArray key)) cb
src/Yoga/React/Native/MacOS/FilePicker.js+14−16
@@ -1,16 +1,14 @@
1−import {
2− View,
3− Platform,
4− UIManager,
5− requireNativeComponent,
6−} from "react-native";
7−
8−var c = View;
9−if (Platform.OS === "macos") {
10− try {
11− if (UIManager.getViewManagerConfig("MacOSFilePicker"))
12− c = requireNativeComponent("MacOSFilePicker");
13− } catch (e) {}
14−}
15−
16−export const _filePickerImpl = c;
1+import React from "react";
2+import { requireNativeComponent } from "react-native";
3+const NativeFilePicker = requireNativeComponent("MacOSFilePicker");
4+export const filePickerImpl = React.forwardRef((props, ref) => {
5+ const nativeProps = { ...props };
6+ if (typeof props.onPickFiles === "function") {
7+ nativeProps.onPickFiles = (e) =>
8+ props.onPickFiles(e.nativeEvent.files || [])();
9+ }
10+ if (typeof props.onCancel === "function") {
11+ nativeProps.onCancel = () => props.onCancel();
12+ }
13+ return React.createElement(NativeFilePicker, { ...nativeProps, ref });
14+});
src/Yoga/React/Native/MacOS/FilePicker.purs+12−6
@@ -1,15 +1,21 @@
1−module Yoga.React.Native.MacOS.FilePicker (nativeFilePicker, NativeFilePickerAttributes) where
1+module Yoga.React.Native.MacOS.FilePicker (nativeFilePicker, NativeFilePickerAttributes, PickedFile) where
22
3+import Prelude
4+import Effect (Effect)
35import React.Basic (ReactComponent)
4−import React.Basic.Events (EventHandler)
56import Yoga.React.Native.Attributes (BaseAttributes)
67import Yoga.React.Native.Internal (FFINativeComponent_, createNativeElement_)
78import Yoga.React.Native.MacOS.Types (FilePickerMode)
89
9−foreign import _filePickerImpl :: forall props. ReactComponent props
10+foreign import filePickerImpl :: forall props. ReactComponent props
1011
1112nativeFilePicker :: FFINativeComponent_ NativeFilePickerAttributes
12−nativeFilePicker = createNativeElement_ _filePickerImpl
13+nativeFilePicker = createNativeElement_ filePickerImpl
14+
15+type PickedFile =
16+ { path :: String
17+ , name :: String
18+ }
1319
1420type NativeFilePickerAttributes = BaseAttributes
1521 ( mode :: FilePickerMode
@@ -20,6 +26,6 @@ type NativeFilePickerAttributes = BaseAttributes
2026 , allowedTypes :: Array String
2127 , allowMultiple :: Boolean
2228 , canChooseDirectories :: Boolean
23− , onPickFiles :: EventHandler
24− , onCancel :: EventHandler
29+ , onPickFiles :: Array PickedFile -> Effect Unit
30+ , onCancel :: Effect Unit
2531 )
src/Yoga/React/Native/MacOS/HelpButton.js+10−6
@@ -1,6 +1,10 @@
1−import { requireNativeComponent, Platform, UIManager, View } from "react-native";
2−let c = View;
3−if (Platform.OS === "macos") {
4− try { if (UIManager.getViewManagerConfig("MacOSHelpButton")) c = requireNativeComponent("MacOSHelpButton"); } catch (e) {}
5−}
6−export const _helpButtonImpl = c;
1+import React from "react";
2+import { requireNativeComponent } from "react-native";
3+const NativeHelpButton = requireNativeComponent("MacOSHelpButton");
4+export const helpButtonImpl = React.forwardRef((props, ref) => {
5+ const nativeProps = { ...props };
6+ if (typeof props.onPress === "function") {
7+ nativeProps.onPress = () => props.onPress();
8+ }
9+ return React.createElement(NativeHelpButton, { ...nativeProps, ref });
10+});
src/Yoga/React/Native/MacOS/HelpButton.purs+5−4
@@ -3,16 +3,17 @@ module Yoga.React.Native.MacOS.HelpButton
33 , NativeHelpButtonAttributes
44 ) where
55
6+import Prelude
7+import Effect (Effect)
68import React.Basic (ReactComponent)
7−import React.Basic.Events (EventHandler)
89import Yoga.React.Native.Attributes (BaseAttributes)
910import Yoga.React.Native.Internal (FFINativeComponent_, createNativeElement_)
1011
11−foreign import _helpButtonImpl :: forall props. ReactComponent props
12+foreign import helpButtonImpl :: forall props. ReactComponent props
1213
1314nativeHelpButton :: FFINativeComponent_ NativeHelpButtonAttributes
14−nativeHelpButton = createNativeElement_ _helpButtonImpl
15+nativeHelpButton = createNativeElement_ helpButtonImpl
1516
1617type NativeHelpButtonAttributes = BaseAttributes
17− ( onPress :: EventHandler
18+ ( onPress :: Effect Unit
1819 )
src/Yoga/React/Native/MacOS/Image.js+2−6
@@ -1,6 +1,2 @@
1−import { requireNativeComponent, Platform, UIManager, View } from "react-native";
2−let c = View;
3−if (Platform.OS === "macos") {
4− try { if (UIManager.getViewManagerConfig("MacOSImage")) c = requireNativeComponent("MacOSImage"); } catch (e) {}
5−}
6−export const _imageImpl = c;
1+import { requireNativeComponent } from "react-native";
2+export const imageImpl = requireNativeComponent("MacOSImage");
src/Yoga/React/Native/MacOS/Image.purs+2−2
@@ -8,10 +8,10 @@ import Yoga.React.Native.Attributes (BaseAttributes)
88import Yoga.React.Native.Internal (FFINativeComponent_, createNativeElement_)
99import Yoga.React.Native.MacOS.Types (ImageContentMode)
1010
11−foreign import _imageImpl :: forall props. ReactComponent props
11+foreign import imageImpl :: forall props. ReactComponent props
1212
1313nativeImage :: FFINativeComponent_ NativeImageAttributes
14−nativeImage = createNativeElement_ _imageImpl
14+nativeImage = createNativeElement_ imageImpl
1515
1616type NativeImageAttributes = BaseAttributes
1717 ( source :: String
src/Yoga/React/Native/MacOS/LevelIndicator.js+2−6
@@ -1,6 +1,2 @@
1−import { requireNativeComponent, Platform, UIManager, View } from "react-native";
2−let c = View;
3−if (Platform.OS === "macos") {
4− try { if (UIManager.getViewManagerConfig("NativeLevelIndicator")) c = requireNativeComponent("NativeLevelIndicator"); } catch (e) {}
5−}
6−export const _levelIndicatorImpl = c;
1+import { requireNativeComponent } from "react-native";
2+export const levelIndicatorImpl = requireNativeComponent("NativeLevelIndicator");
src/Yoga/React/Native/MacOS/LevelIndicator.purs+2−2
@@ -4,10 +4,10 @@ import React.Basic (ReactComponent)
44import Yoga.React.Native.Attributes (BaseAttributes)
55import Yoga.React.Native.Internal (FFINativeComponent_, createNativeElement_)
66
7−foreign import _levelIndicatorImpl :: forall props. ReactComponent props
7+foreign import levelIndicatorImpl :: forall props. ReactComponent props
88
99nativeLevelIndicator :: FFINativeComponent_ NativeLevelIndicatorAttributes
10−nativeLevelIndicator = createNativeElement_ _levelIndicatorImpl
10+nativeLevelIndicator = createNativeElement_ levelIndicatorImpl
1111
1212type NativeLevelIndicatorAttributes = BaseAttributes
1313 ( value :: Number
src/Yoga/React/Native/MacOS/MapView.js+22−6
@@ -1,6 +1,22 @@
1−import { requireNativeComponent, Platform, UIManager, View } from "react-native";
2−let c = View;
3−if (Platform.OS === "macos") {
4− try { if (UIManager.getViewManagerConfig("MacOSMapView")) c = requireNativeComponent("MacOSMapView"); } catch (e) {}
5−}
6−export const _mapViewImpl = c;
1+import React from "react";
2+import { requireNativeComponent } from "react-native";
3+const NativeMapView = requireNativeComponent("MacOSMapView");
4+export const mapViewImpl = React.forwardRef((props, ref) => {
5+ const nativeProps = { ...props };
6+ if (typeof props.onRegionChange === "function") {
7+ nativeProps.onRegionChange = (e) => {
8+ const r = e.nativeEvent;
9+ props.onRegionChange({
10+ latitude: r.latitude ?? 0,
11+ longitude: r.longitude ?? 0,
12+ latitudeDelta: r.latitudeDelta ?? 0,
13+ longitudeDelta: r.longitudeDelta ?? 0,
14+ })();
15+ };
16+ }
17+ if (typeof props.onSelectAnnotation === "function") {
18+ nativeProps.onSelectAnnotation = (e) =>
19+ props.onSelectAnnotation(e.nativeEvent.title || "")();
20+ }
21+ return React.createElement(NativeMapView, { ...nativeProps, ref });
22+});
src/Yoga/React/Native/MacOS/MapView.purs+29−9
@@ -1,24 +1,44 @@
1−module Yoga.React.Native.MacOS.MapView (nativeMapView, NativeMapViewAttributes) where
1+module Yoga.React.Native.MacOS.MapView
2+ ( nativeMapView
3+ , NativeMapViewAttributes
4+ , MapAnnotation
5+ , MapRegion
6+ ) where
27
3−import Foreign (Foreign)
8+import Prelude
9+import Effect (Effect)
410import React.Basic (ReactComponent)
5−import React.Basic.Events (EventHandler)
611import Yoga.React.Native.Attributes (BaseAttributes)
712import Yoga.React.Native.Internal (FFINativeComponent_, createNativeElement_)
13+import Yoga.React.Native.MacOS.Types (MapType)
814
9−foreign import _mapViewImpl :: forall props. ReactComponent props
15+foreign import mapViewImpl :: forall props. ReactComponent props
1016
1117nativeMapView :: FFINativeComponent_ NativeMapViewAttributes
12−nativeMapView = createNativeElement_ _mapViewImpl
18+nativeMapView = createNativeElement_ mapViewImpl
19+
20+type MapAnnotation =
21+ { latitude :: Number
22+ , longitude :: Number
23+ , title :: String
24+ , subtitle :: String
25+ }
26+
27+type MapRegion =
28+ { latitude :: Number
29+ , longitude :: Number
30+ , latitudeDelta :: Number
31+ , longitudeDelta :: Number
32+ }
1333
1434type NativeMapViewAttributes = BaseAttributes
1535 ( latitude :: Number
1636 , longitude :: Number
1737 , latitudeDelta :: Number
1838 , longitudeDelta :: Number
19− , mapType :: String
39+ , mapType :: MapType
2040 , showsUserLocation :: Boolean
21− , annotations :: Foreign
22− , onRegionChange :: EventHandler
23− , onSelectAnnotation :: EventHandler
41+ , annotations :: Array MapAnnotation
42+ , onRegionChange :: MapRegion -> Effect Unit
43+ , onSelectAnnotation :: String -> Effect Unit
2444 )
src/Yoga/React/Native/MacOS/Menu.js+1−1
@@ -1,6 +1,6 @@
11import { NativeModules, Platform } from "react-native";
22
3−export const showMenuImpl = (items, callback) => {
3+export const showMenuImpl = ({ items, onSelectItem: callback }) => {
44 if (Platform.OS !== "macos") return;
55 const mod = NativeModules.MacOSMenuModule;
66 if (mod && mod.show) {
src/Yoga/React/Native/MacOS/Menu.purs+10−4
@@ -1,19 +1,25 @@
11module Yoga.React.Native.MacOS.Menu
22 ( macosShowMenu
33 , MenuItem
4+ , MenuProps
45 ) where
56
67import Prelude
78
89import Effect (Effect)
9−import Effect.Uncurried (EffectFn2, runEffectFn2)
10+import Effect.Uncurried (EffectFn1, runEffectFn1)
1011
1112type MenuItem =
1213 { title :: String
1314 , id :: String
1415 }
1516
16−foreign import showMenuImpl :: EffectFn2 (Array MenuItem) (String -> Effect Unit) Unit
17+type MenuProps =
18+ { items :: Array MenuItem
19+ , onSelectItem :: String -> Effect Unit
20+ }
21+
22+foreign import showMenuImpl :: EffectFn1 MenuProps Unit
1723
18−macosShowMenu :: Array MenuItem -> (String -> Effect Unit) -> Effect Unit
19−macosShowMenu = runEffectFn2 showMenuImpl
24+macosShowMenu :: MenuProps -> Effect Unit
25+macosShowMenu = runEffectFn1 showMenuImpl
src/Yoga/React/Native/MacOS/NaturalLanguage.js+18−18
@@ -1,34 +1,34 @@
11import { NativeModules, Platform } from "react-native";
22
3−export const detectLanguageImpl = (text, callback) => {
4− if (Platform.OS !== "macos") return;
3+const mkAff = (promise) => (onError, onSuccess) => {
4+ promise.then(onSuccess, onError);
5+ return (cancelError, onCancelerError, onCancelerSuccess) =>
6+ onCancelerSuccess();
7+};
8+
9+export const detectLanguageImpl = (text) => {
10+ if (Platform.OS !== "macos") return mkAff(Promise.resolve("unknown"));
511 const mod = NativeModules.MacOSNLModule;
612 if (mod && mod.detectLanguage) {
7− mod
8− .detectLanguage(text)
9− .then((lang) => callback(lang)())
10− .catch((e) => console.error("[macosDetectLanguage]", e));
13+ return mkAff(mod.detectLanguage(text));
1114 }
15+ return mkAff(Promise.resolve("unknown"));
1216};
1317
14−export const sentimentImpl = (text, callback) => {
15− if (Platform.OS !== "macos") return;
18+export const analyzeSentimentImpl = (text) => {
19+ if (Platform.OS !== "macos") return mkAff(Promise.resolve(0));
1620 const mod = NativeModules.MacOSNLModule;
1721 if (mod && mod.sentiment) {
18− mod
19− .sentiment(text)
20− .then((score) => callback(score)())
21− .catch((e) => console.error("[macosAnalyzeSentiment]", e));
22+ return mkAff(mod.sentiment(text));
2223 }
24+ return mkAff(Promise.resolve(0));
2325};
2426
25−export const tokenizeImpl = (text, callback) => {
26− if (Platform.OS !== "macos") return;
27+export const tokenizeImpl = (text) => {
28+ if (Platform.OS !== "macos") return mkAff(Promise.resolve([]));
2729 const mod = NativeModules.MacOSNLModule;
2830 if (mod && mod.tokenize) {
29− mod
30− .tokenize(text)
31− .then((tokens) => callback(tokens)())
32− .catch((e) => console.error("[macosTokenize]", e));
31+ return mkAff(mod.tokenize(text));
3332 }
33+ return mkAff(Promise.resolve([]));
3434};
src/Yoga/React/Native/MacOS/NaturalLanguage.purs+15−15
@@ -1,23 +1,23 @@
11module Yoga.React.Native.MacOS.NaturalLanguage
2− ( macosDetectLanguage
3− , macosAnalyzeSentiment
4− , macosTokenize
2+ ( detectLanguage
3+ , analyzeSentiment
4+ , tokenize
55 ) where
66
7−import Prelude
7+import Effect.Aff (Aff)
8+import Effect.Aff.Compat (EffectFnAff, fromEffectFnAff)
89
9−import Effect (Effect)
10−import Effect.Uncurried (EffectFn2, runEffectFn2)
10+foreign import detectLanguageImpl :: String -> EffectFnAff String
1111
12−foreign import detectLanguageImpl :: EffectFn2 String (String -> Effect Unit) Unit
13−foreign import sentimentImpl :: EffectFn2 String (Number -> Effect Unit) Unit
14−foreign import tokenizeImpl :: EffectFn2 String (Array String -> Effect Unit) Unit
12+detectLanguage :: String -> Aff String
13+detectLanguage text = fromEffectFnAff (detectLanguageImpl text)
1514
16−macosDetectLanguage :: String -> (String -> Effect Unit) -> Effect Unit
17−macosDetectLanguage = runEffectFn2 detectLanguageImpl
15+foreign import analyzeSentimentImpl :: String -> EffectFnAff Number
1816
19−macosAnalyzeSentiment :: String -> (Number -> Effect Unit) -> Effect Unit
20−macosAnalyzeSentiment = runEffectFn2 sentimentImpl
17+analyzeSentiment :: String -> Aff Number
18+analyzeSentiment text = fromEffectFnAff (analyzeSentimentImpl text)
2119
22−macosTokenize :: String -> (Array String -> Effect Unit) -> Effect Unit
23−macosTokenize = runEffectFn2 tokenizeImpl
20+foreign import tokenizeImpl :: String -> EffectFnAff (Array String)
21+
22+tokenize :: String -> Aff (Array String)
23+tokenize text = fromEffectFnAff (tokenizeImpl text)
src/Yoga/React/Native/MacOS/OCR.js+10−6
@@ -1,12 +1,16 @@
11import { NativeModules, Platform } from "react-native";
22
3−export const recognizeImpl = (imagePath, callback) => {
4− if (Platform.OS !== "macos") return;
3+const mkAff = (promise) => (onError, onSuccess) => {
4+ promise.then(onSuccess, onError);
5+ return (cancelError, onCancelerError, onCancelerSuccess) =>
6+ onCancelerSuccess();
7+};
8+
9+export const recognizeTextImpl = (imagePath) => {
10+ if (Platform.OS !== "macos") return mkAff(Promise.resolve(""));
511 const mod = NativeModules.MacOSOCRModule;
612 if (mod && mod.recognize) {
7− mod
8− .recognize(imagePath)
9− .then((text) => callback(text)())
10− .catch((e) => console.error("[macosOCR]", e));
13+ return mkAff(mod.recognize(imagePath));
1114 }
15+ return mkAff(Promise.resolve(""));
1216};
src/Yoga/React/Native/MacOS/OCR.purs+6−8
@@ -1,13 +1,11 @@
11module Yoga.React.Native.MacOS.OCR
2− ( macosOCR
2+ ( recognizeText
33 ) where
44
5−import Prelude
5+import Effect.Aff (Aff)
6+import Effect.Aff.Compat (EffectFnAff, fromEffectFnAff)
67
7−import Effect (Effect)
8−import Effect.Uncurried (EffectFn2, runEffectFn2)
8+foreign import recognizeTextImpl :: String -> EffectFnAff String
99
10−foreign import recognizeImpl :: EffectFn2 String (String -> Effect Unit) Unit
11−
12−macosOCR :: String -> (String -> Effect Unit) -> Effect Unit
13−macosOCR = runEffectFn2 recognizeImpl
10+recognizeText :: String -> Aff String
11+recognizeText path = fromEffectFnAff (recognizeTextImpl path)
src/Yoga/React/Native/MacOS/OutlineView.js+10−6
@@ -1,6 +1,10 @@
1−import { requireNativeComponent, Platform, UIManager, View } from "react-native";
2−let c = View;
3−if (Platform.OS === "macos") {
4− try { if (UIManager.getViewManagerConfig("MacOSOutlineView")) c = requireNativeComponent("MacOSOutlineView"); } catch (e) {}
5−}
6−export const _outlineViewImpl = c;
1+import React from "react";
2+import { requireNativeComponent } from "react-native";
3+const NativeOutlineView = requireNativeComponent("MacOSOutlineView");
4+export const outlineViewImpl = React.forwardRef((props, ref) => {
5+ const nativeProps = { ...props };
6+ if (typeof props.onSelectItem === "function") {
7+ nativeProps.onSelectItem = (e) => props.onSelectItem(e.nativeEvent.id)();
8+ }
9+ return React.createElement(NativeOutlineView, { ...nativeProps, ref });
10+});
src/Yoga/React/Native/MacOS/OutlineView.purs+14−6
@@ -1,21 +1,29 @@
11module Yoga.React.Native.MacOS.OutlineView
22 ( nativeOutlineView
33 , NativeOutlineViewAttributes
4+ , OutlineItem(..)
45 ) where
56
6−import Foreign (Foreign)
7+import Prelude
8+import Effect (Effect)
79import React.Basic (ReactComponent)
8−import React.Basic.Events (EventHandler)
910import Yoga.React.Native.Attributes (BaseAttributes)
1011import Yoga.React.Native.Internal (FFINativeComponent_, createNativeElement_)
1112
12−foreign import _outlineViewImpl :: forall props. ReactComponent props
13+foreign import outlineViewImpl :: forall props. ReactComponent props
1314
1415nativeOutlineView :: FFINativeComponent_ NativeOutlineViewAttributes
15−nativeOutlineView = createNativeElement_ _outlineViewImpl
16+nativeOutlineView = createNativeElement_ outlineViewImpl
17+
18+newtype OutlineItem = OutlineItem
19+ { id :: String
20+ , title :: String
21+ , sfSymbol :: String
22+ , children :: Array OutlineItem
23+ }
1624
1725type NativeOutlineViewAttributes = BaseAttributes
18− ( items :: Foreign
26+ ( items :: Array OutlineItem
1927 , headerVisible :: Boolean
20− , onSelectItem :: EventHandler
28+ , onSelectItem :: String -> Effect Unit
2129 )
src/Yoga/React/Native/MacOS/PDFView.js+11−6
@@ -1,6 +1,11 @@
1−import { requireNativeComponent, Platform, UIManager, View } from "react-native";
2−let c = View;
3−if (Platform.OS === "macos") {
4− try { if (UIManager.getViewManagerConfig("MacOSPDFView")) c = requireNativeComponent("MacOSPDFView"); } catch (e) {}
5−}
6−export const _pdfViewImpl = c;
1+import React from "react";
2+import { requireNativeComponent } from "react-native";
3+const NativePDFView = requireNativeComponent("MacOSPDFView");
4+export const pdfViewImpl = React.forwardRef((props, ref) => {
5+ const nativeProps = { ...props };
6+ if (typeof props.onPageChange === "function") {
7+ nativeProps.onPageChange = (e) =>
8+ props.onPageChange(e.nativeEvent.pageIndex | 0)();
9+ }
10+ return React.createElement(NativePDFView, { ...nativeProps, ref });
11+});
src/Yoga/React/Native/MacOS/PDFView.purs+11−6
@@ -1,18 +1,23 @@
1−module Yoga.React.Native.MacOS.PDFView (nativePDFView, NativePDFViewAttributes) where
1+module Yoga.React.Native.MacOS.PDFView
2+ ( nativePDFView
3+ , NativePDFViewAttributes
4+ ) where
25
6+import Prelude
7+import Effect (Effect)
38import React.Basic (ReactComponent)
4−import React.Basic.Events (EventHandler)
59import Yoga.React.Native.Attributes (BaseAttributes)
610import Yoga.React.Native.Internal (FFINativeComponent_, createNativeElement_)
11+import Yoga.React.Native.MacOS.Types (PDFDisplayMode)
712
8−foreign import _pdfViewImpl :: forall props. ReactComponent props
13+foreign import pdfViewImpl :: forall props. ReactComponent props
914
1015nativePDFView :: FFINativeComponent_ NativePDFViewAttributes
11−nativePDFView = createNativeElement_ _pdfViewImpl
16+nativePDFView = createNativeElement_ pdfViewImpl
1217
1318type NativePDFViewAttributes = BaseAttributes
1419 ( source :: String
1520 , autoScales :: Boolean
16− , displayMode :: String
17− , onPageChange :: EventHandler
21+ , displayMode :: PDFDisplayMode
22+ , onPageChange :: Int -> Effect Unit
1823 )
src/Yoga/React/Native/MacOS/PathControl.js+10−6
@@ -1,6 +1,10 @@
1−import { requireNativeComponent, Platform, UIManager, View } from "react-native";
2−let c = View;
3−if (Platform.OS === "macos") {
4− try { if (UIManager.getViewManagerConfig("MacOSPathControl")) c = requireNativeComponent("MacOSPathControl"); } catch (e) {}
5−}
6−export const _pathControlImpl = c;
1+import React from "react";
2+import { requireNativeComponent } from "react-native";
3+const NativePathControl = requireNativeComponent("MacOSPathControl");
4+export const pathControlImpl = React.forwardRef((props, ref) => {
5+ const nativeProps = { ...props };
6+ if (typeof props.onSelectPath === "function") {
7+ nativeProps.onSelectPath = (e) => props.onSelectPath(e.nativeEvent.url)();
8+ }
9+ return React.createElement(NativePathControl, { ...nativeProps, ref });
10+});
src/Yoga/React/Native/MacOS/PathControl.purs+5−4
@@ -3,19 +3,20 @@ module Yoga.React.Native.MacOS.PathControl
33 , NativePathControlAttributes
44 ) where
55
6+import Prelude
7+import Effect (Effect)
68import React.Basic (ReactComponent)
7−import React.Basic.Events (EventHandler)
89import Yoga.React.Native.Attributes (BaseAttributes)
910import Yoga.React.Native.Internal (FFINativeComponent_, createNativeElement_)
1011import Yoga.React.Native.MacOS.Types (PathControlStyle)
1112
12−foreign import _pathControlImpl :: forall props. ReactComponent props
13+foreign import pathControlImpl :: forall props. ReactComponent props
1314
1415nativePathControl :: FFINativeComponent_ NativePathControlAttributes
15−nativePathControl = createNativeElement_ _pathControlImpl
16+nativePathControl = createNativeElement_ pathControlImpl
1617
1718type NativePathControlAttributes = BaseAttributes
1819 ( url :: String
1920 , pathStyle :: PathControlStyle
20− , onSelectPath :: EventHandler
21+ , onSelectPath :: String -> Effect Unit
2122 )
src/Yoga/React/Native/MacOS/PatternBackground.js+2−16
@@ -1,16 +1,2 @@
1−import {
2− View,
3− Platform,
4− UIManager,
5− requireNativeComponent,
6−} from "react-native";
7−
8−var c = View;
9−if (Platform.OS === "macos") {
10− try {
11− if (UIManager.getViewManagerConfig("MacOSPatternBackground"))
12− c = requireNativeComponent("MacOSPatternBackground");
13− } catch (e) {}
14−}
15−
16−export const _patternBackgroundImpl = c;
1+import { requireNativeComponent } from "react-native";
2+export const patternBackgroundImpl = requireNativeComponent("MacOSPatternBackground");
src/Yoga/React/Native/MacOS/PatternBackground.purs+3−3
@@ -4,14 +4,14 @@ import React.Basic (ReactComponent)
44import Yoga.React.Native.Attributes (BaseAttributes)
55import Yoga.React.Native.Internal (FFINativeComponent, createNativeElement)
66
7−foreign import _patternBackgroundImpl :: forall props. ReactComponent props
7+foreign import patternBackgroundImpl :: forall props. ReactComponent props
88
99nativePatternBackground :: FFINativeComponent NativePatternBackgroundAttributes
10−nativePatternBackground = createNativeElement _patternBackgroundImpl
10+nativePatternBackground = createNativeElement patternBackgroundImpl
1111
1212type NativePatternBackgroundAttributes = BaseAttributes
1313 ( patternColor :: String
14− , backgroundColor2 :: String
14+ , background :: String
1515 , patternOpacity :: Number
1616 , patternScale :: Number
1717 )
src/Yoga/React/Native/MacOS/PopUp.js+11−6
@@ -1,6 +1,11 @@
1−import { requireNativeComponent, Platform, UIManager, View } from "react-native";
2−let c = View;
3−if (Platform.OS === "macos") {
4− try { if (UIManager.getViewManagerConfig("NativePopUp")) c = requireNativeComponent("NativePopUp"); } catch (e) {}
5−}
6−export const _popUpImpl = c;
1+import React from "react";
2+import { requireNativeComponent } from "react-native";
3+const NativePopUp = requireNativeComponent("NativePopUp");
4+export const popUpImpl = React.forwardRef((props, ref) => {
5+ const nativeProps = { ...props };
6+ if (typeof props.onChange === "function") {
7+ nativeProps.onChange = (e) =>
8+ props.onChange(e.nativeEvent.selectedIndex | 0)();
9+ }
10+ return React.createElement(NativePopUp, { ...nativeProps, ref });
11+});
src/Yoga/React/Native/MacOS/PopUp.purs+5−4
@@ -1,17 +1,18 @@
11module Yoga.React.Native.MacOS.PopUp (nativePopUp, NativePopUpAttributes) where
22
3+import Prelude
4+import Effect (Effect)
35import React.Basic (ReactComponent)
4−import React.Basic.Events (EventHandler)
56import Yoga.React.Native.Attributes (BaseAttributes)
67import Yoga.React.Native.Internal (FFINativeComponent_, createNativeElement_)
78
8−foreign import _popUpImpl :: forall props. ReactComponent props
9+foreign import popUpImpl :: forall props. ReactComponent props
910
1011nativePopUp :: FFINativeComponent_ NativePopUpAttributes
11−nativePopUp = createNativeElement_ _popUpImpl
12+nativePopUp = createNativeElement_ popUpImpl
1213
1314type NativePopUpAttributes = BaseAttributes
1415 ( items :: Array String
1516 , selectedIndex :: Int
16− , onChange :: EventHandler
17+ , onChange :: Int -> Effect Unit
1718 )
src/Yoga/React/Native/MacOS/Popover.js+10−6
@@ -1,6 +1,10 @@
1−import { requireNativeComponent, Platform, UIManager, View } from "react-native";
2−let c = View;
3−if (Platform.OS === "macos") {
4− try { if (UIManager.getViewManagerConfig("MacOSPopover")) c = requireNativeComponent("MacOSPopover"); } catch (e) {}
5−}
6−export const _popoverImpl = c;
1+import React from "react";
2+import { requireNativeComponent } from "react-native";
3+const NativePopover = requireNativeComponent("MacOSPopover");
4+export const popoverImpl = React.forwardRef((props, ref) => {
5+ const nativeProps = { ...props };
6+ if (typeof props.onClose === "function") {
7+ nativeProps.onClose = () => props.onClose();
8+ }
9+ return React.createElement(NativePopover, { ...nativeProps, ref });
10+});
src/Yoga/React/Native/MacOS/Popover.purs+5−4
@@ -3,20 +3,21 @@ module Yoga.React.Native.MacOS.Popover
33 , NativePopoverAttributes
44 ) where
55
6+import Prelude
7+import Effect (Effect)
68import React.Basic (ReactComponent)
7−import React.Basic.Events (EventHandler)
89import Yoga.React.Native.Attributes (BaseAttributes)
910import Yoga.React.Native.Internal (FFINativeComponent, createNativeElement)
1011import Yoga.React.Native.MacOS.Types (PopoverEdge, PopoverBehavior)
1112
12−foreign import _popoverImpl :: forall props. ReactComponent props
13+foreign import popoverImpl :: forall props. ReactComponent props
1314
1415nativePopover :: FFINativeComponent NativePopoverAttributes
15−nativePopover = createNativeElement _popoverImpl
16+nativePopover = createNativeElement popoverImpl
1617
1718type NativePopoverAttributes = BaseAttributes
1819 ( visible :: Boolean
1920 , preferredEdge :: PopoverEdge
2021 , behavior :: PopoverBehavior
21− , onClose :: EventHandler
22+ , onClose :: Effect Unit
2223 )
src/Yoga/React/Native/MacOS/Progress.js+2−6
@@ -1,6 +1,2 @@
1−import { requireNativeComponent, Platform, UIManager, View } from "react-native";
2−let c = View;
3−if (Platform.OS === "macos") {
4− try { if (UIManager.getViewManagerConfig("NativeProgress")) c = requireNativeComponent("NativeProgress"); } catch (e) {}
5−}
6−export const _progressImpl = c;
1+import { requireNativeComponent } from "react-native";
2+export const progressImpl = requireNativeComponent("NativeProgress");
src/Yoga/React/Native/MacOS/Progress.purs+2−2
@@ -4,10 +4,10 @@ import React.Basic (ReactComponent)
44import Yoga.React.Native.Attributes (BaseAttributes)
55import Yoga.React.Native.Internal (FFINativeComponent_, createNativeElement_)
66
7−foreign import _progressImpl :: forall props. ReactComponent props
7+foreign import progressImpl :: forall props. ReactComponent props
88
99nativeProgress :: FFINativeComponent_ NativeProgressAttributes
10−nativeProgress = createNativeElement_ _progressImpl
10+nativeProgress = createNativeElement_ progressImpl
1111
1212type NativeProgressAttributes = BaseAttributes
1313 ( value :: Number
src/Yoga/React/Native/MacOS/RadioButton.js+10−6
@@ -1,6 +1,10 @@
1−import { requireNativeComponent, Platform, UIManager, View } from "react-native";
2−let c = View;
3−if (Platform.OS === "macos") {
4− try { if (UIManager.getViewManagerConfig("MacOSRadioButton")) c = requireNativeComponent("MacOSRadioButton"); } catch (e) {}
5−}
6−export const _radioButtonImpl = c;
1+import React from "react";
2+import { requireNativeComponent } from "react-native";
3+const NativeRadioButton = requireNativeComponent("MacOSRadioButton");
4+export const radioButtonImpl = React.forwardRef((props, ref) => {
5+ const nativeProps = { ...props };
6+ if (typeof props.onChange === "function") {
7+ nativeProps.onChange = (e) => props.onChange(!!e.nativeEvent.selected)();
8+ }
9+ return React.createElement(NativeRadioButton, { ...nativeProps, ref });
10+});
src/Yoga/React/Native/MacOS/RadioButton.purs+5−4
@@ -3,19 +3,20 @@ module Yoga.React.Native.MacOS.RadioButton
33 , NativeRadioButtonAttributes
44 ) where
55
6+import Prelude
7+import Effect (Effect)
68import React.Basic (ReactComponent)
7−import React.Basic.Events (EventHandler)
89import Yoga.React.Native.Attributes (BaseAttributes)
910import Yoga.React.Native.Internal (FFINativeComponent_, createNativeElement_)
1011
11−foreign import _radioButtonImpl :: forall props. ReactComponent props
12+foreign import radioButtonImpl :: forall props. ReactComponent props
1213
1314nativeRadioButton :: FFINativeComponent_ NativeRadioButtonAttributes
14−nativeRadioButton = createNativeElement_ _radioButtonImpl
15+nativeRadioButton = createNativeElement_ radioButtonImpl
1516
1617type NativeRadioButtonAttributes = BaseAttributes
1718 ( selected :: Boolean
1819 , title :: String
1920 , enabled :: Boolean
20− , onChange :: EventHandler
21+ , onChange :: Boolean -> Effect Unit
2122 )
src/Yoga/React/Native/MacOS/Rive.js+2−16
@@ -1,16 +1,2 @@
1−import {
2− View,
3− Platform,
4− UIManager,
5− requireNativeComponent,
6−} from "react-native";
7−
8−var c = View;
9−if (Platform.OS === "macos") {
10− try {
11− if (UIManager.getViewManagerConfig("MacOSRiveView"))
12− c = requireNativeComponent("MacOSRiveView");
13− } catch (e) {}
14−}
15−
16−export const _nativeRiveViewImpl = c;
1+import { requireNativeComponent } from "react-native";
2+export const nativeRiveViewImpl = requireNativeComponent("MacOSRiveView");
src/Yoga/React/Native/MacOS/Rive.purs+3−3
@@ -5,13 +5,13 @@ import Yoga.React.Native.Attributes (BaseAttributes)
55import Yoga.React.Native.Internal (FFINativeComponent, FFINativeComponent_, createNativeElement, createNativeElement_)
66import Yoga.React.Native.MacOS.Types (RiveFit)
77
8−foreign import _nativeRiveViewImpl :: forall props. ReactComponent props
8+foreign import nativeRiveViewImpl :: forall props. ReactComponent props
99
1010nativeRiveView :: FFINativeComponent NativeRiveViewAttributes
11−nativeRiveView = createNativeElement _nativeRiveViewImpl
11+nativeRiveView = createNativeElement nativeRiveViewImpl
1212
1313nativeRiveView_ :: FFINativeComponent_ NativeRiveViewAttributes
14−nativeRiveView_ = createNativeElement_ _nativeRiveViewImpl
14+nativeRiveView_ = createNativeElement_ nativeRiveViewImpl
1515
1616type NativeRiveViewAttributes = BaseAttributes
1717 ( resourceName :: String
src/Yoga/React/Native/MacOS/ScrollView.js+2−16
@@ -1,16 +1,2 @@
1−import {
2− View,
3− Platform,
4− UIManager,
5− requireNativeComponent,
6−} from "react-native";
7−
8−var c = View;
9−if (Platform.OS === "macos") {
10− try {
11− if (UIManager.getViewManagerConfig("MacOSScrollView"))
12− c = requireNativeComponent("MacOSScrollView");
13− } catch (e) {}
14−}
15−
16−export const _nativeScrollViewImpl = c;
1+import { requireNativeComponent } from "react-native";
2+export const nativeScrollViewImpl = requireNativeComponent("MacOSScrollView");
src/Yoga/React/Native/MacOS/ScrollView.purs+4−2
@@ -4,11 +4,13 @@ import React.Basic (ReactComponent)
44import Yoga.React.Native.Attributes (BaseAttributes)
55import Yoga.React.Native.Internal (FFINativeComponent, createNativeElement)
66
7−foreign import _nativeScrollViewImpl :: forall props. ReactComponent props
7+foreign import nativeScrollViewImpl :: forall props. ReactComponent props
88
99nativeScrollView :: FFINativeComponent NativeScrollViewAttributes
10−nativeScrollView = createNativeElement _nativeScrollViewImpl
10+nativeScrollView = createNativeElement nativeScrollViewImpl
1111
1212type NativeScrollViewAttributes = BaseAttributes
1313 ( scrollToBottom :: Int
14+ , scrollToY :: Number
15+ , scrollToYTrigger :: Int
1416 )
src/Yoga/React/Native/MacOS/SearchField.js+13−6
@@ -1,6 +1,13 @@
1−import { requireNativeComponent, Platform, UIManager, View } from "react-native";
2−let c = View;
3−if (Platform.OS === "macos") {
4− try { if (UIManager.getViewManagerConfig("MacOSSearchField")) c = requireNativeComponent("MacOSSearchField"); } catch (e) {}
5−}
6−export const _searchFieldImpl = c;
1+import React from "react";
2+import { requireNativeComponent } from "react-native";
3+const NativeSearchField = requireNativeComponent("MacOSSearchField");
4+export const searchFieldImpl = React.forwardRef((props, ref) => {
5+ const nativeProps = { ...props };
6+ if (typeof props.onChangeText === "function") {
7+ nativeProps.onChangeText = (e) => props.onChangeText(e.nativeEvent.text)();
8+ }
9+ if (typeof props.onSearch === "function") {
10+ nativeProps.onSearch = (e) => props.onSearch(e.nativeEvent.text)();
11+ }
12+ return React.createElement(NativeSearchField, { ...nativeProps, ref });
13+});
src/Yoga/React/Native/MacOS/SearchField.purs+6−5
@@ -3,19 +3,20 @@ module Yoga.React.Native.MacOS.SearchField
33 , NativeSearchFieldAttributes
44 ) where
55
6+import Prelude
7+import Effect (Effect)
68import React.Basic (ReactComponent)
7−import React.Basic.Events (EventHandler)
89import Yoga.React.Native.Attributes (BaseAttributes)
910import Yoga.React.Native.Internal (FFINativeComponent_, createNativeElement_)
1011
11−foreign import _searchFieldImpl :: forall props. ReactComponent props
12+foreign import searchFieldImpl :: forall props. ReactComponent props
1213
1314nativeSearchField :: FFINativeComponent_ NativeSearchFieldAttributes
14−nativeSearchField = createNativeElement_ _searchFieldImpl
15+nativeSearchField = createNativeElement_ searchFieldImpl
1516
1617type NativeSearchFieldAttributes = BaseAttributes
1718 ( text :: String
1819 , placeholder :: String
19− , onChangeText :: EventHandler
20− , onSearch :: EventHandler
20+ , onChangeText :: String -> Effect Unit
21+ , onSearch :: String -> Effect Unit
2122 )
src/Yoga/React/Native/MacOS/Segmented.js+11−6
@@ -1,6 +1,11 @@
1−import { requireNativeComponent, Platform, UIManager, View } from "react-native";
2−let c = View;
3−if (Platform.OS === "macos") {
4− try { if (UIManager.getViewManagerConfig("NativeSegmented")) c = requireNativeComponent("NativeSegmented"); } catch (e) {}
5−}
6−export const _segmentedImpl = c;
1+import React from "react";
2+import { requireNativeComponent } from "react-native";
3+const NativeSegmented = requireNativeComponent("NativeSegmented");
4+export const segmentedImpl = React.forwardRef((props, ref) => {
5+ const nativeProps = { ...props };
6+ if (typeof props.onChange === "function") {
7+ nativeProps.onChange = (e) =>
8+ props.onChange(e.nativeEvent.selectedIndex | 0)();
9+ }
10+ return React.createElement(NativeSegmented, { ...nativeProps, ref });
11+});
src/Yoga/React/Native/MacOS/Segmented.purs+5−4
@@ -1,18 +1,19 @@
11module Yoga.React.Native.MacOS.Segmented (nativeSegmented, NativeSegmentedAttributes) where
22
3+import Prelude
4+import Effect (Effect)
35import React.Basic (ReactComponent)
4−import React.Basic.Events (EventHandler)
56import Yoga.React.Native.Attributes (BaseAttributes)
67import Yoga.React.Native.Internal (FFINativeComponent_, createNativeElement_)
78
8−foreign import _segmentedImpl :: forall props. ReactComponent props
9+foreign import segmentedImpl :: forall props. ReactComponent props
910
1011nativeSegmented :: FFINativeComponent_ NativeSegmentedAttributes
11−nativeSegmented = createNativeElement_ _segmentedImpl
12+nativeSegmented = createNativeElement_ segmentedImpl
1213
1314type NativeSegmentedAttributes = BaseAttributes
1415 ( labels :: Array String
1516 , sfSymbols :: Array String
1617 , selectedIndex :: Int
17− , onChange :: EventHandler
18+ , onChange :: Int -> Effect Unit
1819 )
src/Yoga/React/Native/MacOS/Separator.js+2−6
@@ -1,6 +1,2 @@
1−import { requireNativeComponent, Platform, UIManager, View } from "react-native";
2−let c = View;
3−if (Platform.OS === "macos") {
4− try { if (UIManager.getViewManagerConfig("MacOSSeparator")) c = requireNativeComponent("MacOSSeparator"); } catch (e) {}
5−}
6−export const _separatorImpl = c;
1+import { requireNativeComponent } from "react-native";
2+export const separatorImpl = requireNativeComponent("MacOSSeparator");
src/Yoga/React/Native/MacOS/Separator.purs+2−2
@@ -7,10 +7,10 @@ import React.Basic (ReactComponent)
77import Yoga.React.Native.Attributes (BaseAttributes)
88import Yoga.React.Native.Internal (FFINativeComponent_, createNativeElement_)
99
10−foreign import _separatorImpl :: forall props. ReactComponent props
10+foreign import separatorImpl :: forall props. ReactComponent props
1111
1212nativeSeparator :: FFINativeComponent_ NativeSeparatorAttributes
13−nativeSeparator = createNativeElement_ _separatorImpl
13+nativeSeparator = createNativeElement_ separatorImpl
1414
1515type NativeSeparatorAttributes = BaseAttributes
1616 ( vertical :: Boolean
src/Yoga/React/Native/MacOS/Sheet.js+10−6
@@ -1,6 +1,10 @@
1−import { requireNativeComponent, Platform, UIManager, View } from "react-native";
2−let c = View;
3−if (Platform.OS === "macos") {
4− try { if (UIManager.getViewManagerConfig("MacOSSheet")) c = requireNativeComponent("MacOSSheet"); } catch (e) {}
5−}
6−export const _sheetImpl = c;
1+import React from "react";
2+import { requireNativeComponent } from "react-native";
3+const NativeSheet = requireNativeComponent("MacOSSheet");
4+export const sheetImpl = React.forwardRef((props, ref) => {
5+ const nativeProps = { ...props };
6+ if (typeof props.onDismiss === "function") {
7+ nativeProps.onDismiss = () => props.onDismiss();
8+ }
9+ return React.createElement(NativeSheet, { ...nativeProps, ref });
10+});
src/Yoga/React/Native/MacOS/Sheet.purs+5−4
@@ -3,17 +3,18 @@ module Yoga.React.Native.MacOS.Sheet
33 , NativeSheetAttributes
44 ) where
55
6+import Prelude
7+import Effect (Effect)
68import React.Basic (ReactComponent)
7−import React.Basic.Events (EventHandler)
89import Yoga.React.Native.Attributes (BaseAttributes)
910import Yoga.React.Native.Internal (FFINativeComponent, createNativeElement)
1011
11−foreign import _sheetImpl :: forall props. ReactComponent props
12+foreign import sheetImpl :: forall props. ReactComponent props
1213
1314nativeSheet :: FFINativeComponent NativeSheetAttributes
14−nativeSheet = createNativeElement _sheetImpl
15+nativeSheet = createNativeElement sheetImpl
1516
1617type NativeSheetAttributes = BaseAttributes
1718 ( visible :: Boolean
18− , onDismiss :: EventHandler
19+ , onDismiss :: Effect Unit
1920 )
src/Yoga/React/Native/MacOS/Slider.js+12−6
@@ -1,6 +1,12 @@
1−import { requireNativeComponent, Platform, UIManager, View } from "react-native";
2−let c = View;
3−if (Platform.OS === "macos") {
4− try { if (UIManager.getViewManagerConfig("NativeSlider")) c = requireNativeComponent("NativeSlider"); } catch (e) {}
5−}
6−export const _sliderImpl = c;
1+import React from "react";
2+import { requireNativeComponent } from "react-native";
3+
4+const NativeSlider = requireNativeComponent("NativeSlider");
5+
6+export const sliderImpl = React.forwardRef((props, ref) => {
7+ const nativeProps = { ...props };
8+ if (typeof props.onChange === "function") {
9+ nativeProps.onChange = (e) => props.onChange(e.nativeEvent.value ?? 0)();
10+ }
11+ return React.createElement(NativeSlider, { ...nativeProps, ref });
12+});
src/Yoga/React/Native/MacOS/Slider.purs+5−4
@@ -1,14 +1,15 @@
11module Yoga.React.Native.MacOS.Slider (nativeSlider, NativeSliderAttributes) where
22
3+import Prelude
4+import Effect (Effect)
35import React.Basic (ReactComponent)
4−import React.Basic.Events (EventHandler)
56import Yoga.React.Native.Attributes (BaseAttributes)
67import Yoga.React.Native.Internal (FFINativeComponent_, createNativeElement_)
78
8−foreign import _sliderImpl :: forall props. ReactComponent props
9+foreign import sliderImpl :: forall props. ReactComponent props
910
1011nativeSlider :: FFINativeComponent_ NativeSliderAttributes
11−nativeSlider = createNativeElement_ _sliderImpl
12+nativeSlider = createNativeElement_ sliderImpl
1213
1314type NativeSliderAttributes = BaseAttributes
1415 ( value :: Number
@@ -16,5 +17,5 @@ type NativeSliderAttributes = BaseAttributes
1617 , maxValue :: Number
1718 , numberOfTickMarks :: Int
1819 , allowsTickMarkValuesOnly :: Boolean
19− , onChange :: EventHandler
20+ , onChange :: Number -> Effect Unit
2021 )
src/Yoga/React/Native/MacOS/SpeechRecognition.js+15−6
@@ -1,25 +1,30 @@
11import { NativeModules, Platform } from "react-native";
22
3−export const startListeningImpl = () => {
3+export const startImpl = () => {
44 if (Platform.OS !== "macos") return;
55 const mod = NativeModules.MacOSSpeechRecognitionModule;
66 if (mod && mod.start) {
7− mod.start().catch((e) => console.error("[macosStartListening]", e));
7+ mod.start().catch((e) => console.error("[SpeechRecognition]", e));
88 }
99};
1010
11−export const stopListeningImpl = (callback) => {
12− if (Platform.OS !== "macos") return;
11+export const stopImpl = (callback) => {
12+ if (Platform.OS !== "macos") {
13+ callback("")();
14+ return;
15+ }
1316 const mod = NativeModules.MacOSSpeechRecognitionModule;
1417 if (mod && mod.stop) {
1518 mod
1619 .stop()
1720 .then((text) => callback(text)())
18− .catch((e) => console.error("[macosStopListening]", e));
21+ .catch(() => callback("")());
22+ } else {
23+ callback("")();
1924 }
2025};
2126
22−export const getTranscriptImpl = (callback) => {
27+export const pollTranscriptImpl = (callback) => {
2328 if (Platform.OS !== "macos") return;
2429 const mod = NativeModules.MacOSSpeechRecognitionModule;
2530 if (mod && mod.getTranscript) {
@@ -29,3 +34,7 @@ export const getTranscriptImpl = (callback) => {
2934 .catch(() => {});
3035 }
3136};
37+
38+export const setIntervalImpl = (ms, cb) => setInterval(cb, ms);
39+
40+export const clearIntervalImpl = (id) => clearInterval(id);
src/Yoga/React/Native/MacOS/SpeechRecognition.purs+46−13
@@ -1,23 +1,56 @@
11module Yoga.React.Native.MacOS.SpeechRecognition
2− ( macosStartListening
3− , macosStopListening
4− , macosGetTranscript
2+ ( useSpeechRecognition
3+ , UseSpeechRecognition
4+ , SpeechRecognition
55 ) where
66
77import Prelude
88
9+import Data.Newtype (class Newtype)
10+import Data.Tuple.Nested ((/\))
911import Effect (Effect)
10−import Effect.Uncurried (EffectFn1, runEffectFn1)
12+import Effect.Uncurried (EffectFn1, EffectFn2, runEffectFn1, runEffectFn2)
13+import React.Basic.Hooks (Hook, UseEffect, UseState, useState', useEffect)
14+import React.Basic.Hooks as React
15+import React.Basic.Hooks.Internal (coerceHook)
1116
12−foreign import startListeningImpl :: Effect Unit
13−foreign import stopListeningImpl :: EffectFn1 (String -> Effect Unit) Unit
14−foreign import getTranscriptImpl :: EffectFn1 (String -> Effect Unit) Unit
17+foreign import startImpl :: Effect Unit
18+foreign import stopImpl :: EffectFn1 (String -> Effect Unit) Unit
19+foreign import pollTranscriptImpl :: EffectFn1 (String -> Effect Unit) Unit
20+foreign import setIntervalImpl :: EffectFn2 Int (Effect Unit) Int
21+foreign import clearIntervalImpl :: EffectFn1 Int Unit
1522
16−macosStartListening :: Effect Unit
17−macosStartListening = startListeningImpl
23+type SpeechRecognition =
24+ { listening :: Boolean
25+ , transcript :: String
26+ , start :: Effect Unit
27+ , stop :: Effect Unit
28+ }
1829
19−macosStopListening :: (String -> Effect Unit) -> Effect Unit
20−macosStopListening = runEffectFn1 stopListeningImpl
30+newtype UseSpeechRecognition hooks = UseSpeechRecognition
31+ (UseEffect Boolean (UseState String (UseState Boolean hooks)))
2132
22−macosGetTranscript :: (String -> Effect Unit) -> Effect Unit
23−macosGetTranscript = runEffectFn1 getTranscriptImpl
33+derive instance Newtype (UseSpeechRecognition hooks) _
34+
35+useSpeechRecognition :: Hook UseSpeechRecognition SpeechRecognition
36+useSpeechRecognition = coerceHook React.do
37+ listening /\ setListening <- useState' false
38+ transcript /\ setTranscript <- useState' ""
39+ useEffect listening do
40+ if listening then do
41+ timerId <- runEffectFn2 setIntervalImpl 500 do
42+ runEffectFn1 pollTranscriptImpl \t -> setTranscript t
43+ pure (runEffectFn1 clearIntervalImpl timerId)
44+ else
45+ pure (pure unit)
46+ pure
47+ { listening
48+ , transcript
49+ , start: do
50+ setListening true
51+ setTranscript ""
52+ startImpl
53+ , stop: do
54+ setListening false
55+ runEffectFn1 stopImpl \finalText -> setTranscript finalText
56+ }
src/Yoga/React/Native/MacOS/SpeechSynthesizer.purs+14−11
@@ -1,23 +1,26 @@
11module Yoga.React.Native.MacOS.SpeechSynthesizer
2− ( macosSay
3− , macosSayWithVoice
4− , macosStopSpeaking
2+ ( say
3+ , sayWithVoice
4+ , stopSpeaking
55 ) where
66
77import Prelude
88
99import Effect (Effect)
1010import Effect.Uncurried (EffectFn1, EffectFn2, runEffectFn1, runEffectFn2)
11+import Yoga.React.Native.MacOS.Types (VoiceIdentifier(..))
12+
13+say :: String -> Effect Unit
14+say = runEffectFn1 sayImpl
1115
1216foreign import sayImpl :: EffectFn1 String Unit
13−foreign import sayWithVoiceImpl :: EffectFn2 String String Unit
14−foreign import stopSpeakingImpl :: Effect Unit
1517
16−macosSay :: String -> Effect Unit
17−macosSay = runEffectFn1 sayImpl
18+sayWithVoice :: String -> VoiceIdentifier -> Effect Unit
19+sayWithVoice text (VoiceIdentifier voice) = runEffectFn2 sayWithVoiceImpl text voice
1820
19−macosSayWithVoice :: String -> String -> Effect Unit
20−macosSayWithVoice = runEffectFn2 sayWithVoiceImpl
21+foreign import sayWithVoiceImpl :: EffectFn2 String String Unit
22+
23+stopSpeaking :: Effect Unit
24+stopSpeaking = stopSpeakingImpl
2125
22−macosStopSpeaking :: Effect Unit
23−macosStopSpeaking = stopSpeakingImpl
26+foreign import stopSpeakingImpl :: Effect Unit
src/Yoga/React/Native/MacOS/SplitView.js+2−6
@@ -1,6 +1,2 @@
1−import { requireNativeComponent, Platform, UIManager, View } from "react-native";
2−let c = View;
3−if (Platform.OS === "macos") {
4− try { if (UIManager.getViewManagerConfig("MacOSSplitView")) c = requireNativeComponent("MacOSSplitView"); } catch (e) {}
5−}
6−export const _splitViewImpl = c;
1+import { requireNativeComponent } from "react-native";
2+export const splitViewImpl = requireNativeComponent("MacOSSplitView");
src/Yoga/React/Native/MacOS/SplitView.purs+2−2
@@ -7,10 +7,10 @@ import React.Basic (ReactComponent)
77import Yoga.React.Native.Attributes (BaseAttributes)
88import Yoga.React.Native.Internal (FFINativeComponent, createNativeElement)
99
10−foreign import _splitViewImpl :: forall props. ReactComponent props
10+foreign import splitViewImpl :: forall props. ReactComponent props
1111
1212nativeSplitView :: FFINativeComponent NativeSplitViewAttributes
13−nativeSplitView = createNativeElement _splitViewImpl
13+nativeSplitView = createNativeElement splitViewImpl
1414
1515type NativeSplitViewAttributes = BaseAttributes
1616 ( isVertical :: Boolean
src/Yoga/React/Native/MacOS/Stepper.js+10−6
@@ -1,6 +1,10 @@
1−import { requireNativeComponent, Platform, UIManager, View } from "react-native";
2−let c = View;
3−if (Platform.OS === "macos") {
4− try { if (UIManager.getViewManagerConfig("MacOSStepper")) c = requireNativeComponent("MacOSStepper"); } catch (e) {}
5−}
6−export const _stepperImpl = c;
1+import React from "react";
2+import { requireNativeComponent } from "react-native";
3+const NativeStepper = requireNativeComponent("MacOSStepper");
4+export const stepperImpl = React.forwardRef((props, ref) => {
5+ const nativeProps = { ...props };
6+ if (typeof props.onChange === "function") {
7+ nativeProps.onChange = (e) => props.onChange(e.nativeEvent.value ?? 0)();
8+ }
9+ return React.createElement(NativeStepper, { ...nativeProps, ref });
10+});
src/Yoga/React/Native/MacOS/Stepper.purs+5−4
@@ -3,20 +3,21 @@ module Yoga.React.Native.MacOS.Stepper
33 , NativeStepperAttributes
44 ) where
55
6+import Prelude
7+import Effect (Effect)
68import React.Basic (ReactComponent)
7−import React.Basic.Events (EventHandler)
89import Yoga.React.Native.Attributes (BaseAttributes)
910import Yoga.React.Native.Internal (FFINativeComponent_, createNativeElement_)
1011
11−foreign import _stepperImpl :: forall props. ReactComponent props
12+foreign import stepperImpl :: forall props. ReactComponent props
1213
1314nativeStepper :: FFINativeComponent_ NativeStepperAttributes
14−nativeStepper = createNativeElement_ _stepperImpl
15+nativeStepper = createNativeElement_ stepperImpl
1516
1617type NativeStepperAttributes = BaseAttributes
1718 ( value :: Number
1819 , minValue :: Number
1920 , maxValue :: Number
2021 , increment :: Number
21− , onChange :: EventHandler
22+ , onChange :: Number -> Effect Unit
2223 )
src/Yoga/React/Native/MacOS/Switch.js+12−6
@@ -1,6 +1,12 @@
1−import { requireNativeComponent, Platform, UIManager, View } from "react-native";
2−let c = View;
3−if (Platform.OS === "macos") {
4− try { if (UIManager.getViewManagerConfig("NativeSwitch")) c = requireNativeComponent("NativeSwitch"); } catch (e) {}
5−}
6−export const _switchImpl = c;
1+import React from "react";
2+import { requireNativeComponent } from "react-native";
3+
4+const NativeSwitch = requireNativeComponent("NativeSwitch");
5+
6+export const switchImpl = React.forwardRef((props, ref) => {
7+ const nativeProps = { ...props };
8+ if (typeof props.onChange === "function") {
9+ nativeProps.onChange = (e) => props.onChange(!!e.nativeEvent.on)();
10+ }
11+ return React.createElement(NativeSwitch, { ...nativeProps, ref });
12+});
src/Yoga/React/Native/MacOS/Switch.purs+5−4
@@ -1,16 +1,17 @@
11module Yoga.React.Native.MacOS.Switch (nativeSwitch, NativeSwitchAttributes) where
22
3+import Prelude
4+import Effect (Effect)
35import React.Basic (ReactComponent)
4−import React.Basic.Events (EventHandler)
56import Yoga.React.Native.Attributes (BaseAttributes)
67import Yoga.React.Native.Internal (FFINativeComponent_, createNativeElement_)
78
8−foreign import _switchImpl :: forall props. ReactComponent props
9+foreign import switchImpl :: forall props. ReactComponent props
910
1011nativeSwitch :: FFINativeComponent_ NativeSwitchAttributes
11−nativeSwitch = createNativeElement_ _switchImpl
12+nativeSwitch = createNativeElement_ switchImpl
1213
1314type NativeSwitchAttributes = BaseAttributes
1415 ( on :: Boolean
15− , onChange :: EventHandler
16+ , onChange :: Boolean -> Effect Unit
1617 )
src/Yoga/React/Native/MacOS/TabView.js+10−6
@@ -1,6 +1,10 @@
1−import { requireNativeComponent, Platform, UIManager, View } from "react-native";
2−let c = View;
3−if (Platform.OS === "macos") {
4− try { if (UIManager.getViewManagerConfig("MacOSTabView")) c = requireNativeComponent("MacOSTabView"); } catch (e) {}
5−}
6−export const _tabViewImpl = c;
1+import React from "react";
2+import { requireNativeComponent } from "react-native";
3+const NativeTabView = requireNativeComponent("MacOSTabView");
4+export const tabViewImpl = React.forwardRef((props, ref) => {
5+ const nativeProps = { ...props };
6+ if (typeof props.onSelectTab === "function") {
7+ nativeProps.onSelectTab = (e) => props.onSelectTab(e.nativeEvent.tabId)();
8+ }
9+ return React.createElement(NativeTabView, { ...nativeProps, ref });
10+});
src/Yoga/React/Native/MacOS/TabView.purs+5−4
@@ -4,15 +4,16 @@ module Yoga.React.Native.MacOS.TabView
44 , TabItem
55 ) where
66
7+import Prelude
8+import Effect (Effect)
79import React.Basic (ReactComponent)
8−import React.Basic.Events (EventHandler)
910import Yoga.React.Native.Attributes (BaseAttributes)
1011import Yoga.React.Native.Internal (FFINativeComponent_, createNativeElement_)
1112
12−foreign import _tabViewImpl :: forall props. ReactComponent props
13+foreign import tabViewImpl :: forall props. ReactComponent props
1314
1415nativeTabView :: FFINativeComponent_ NativeTabViewAttributes
15−nativeTabView = createNativeElement_ _tabViewImpl
16+nativeTabView = createNativeElement_ tabViewImpl
1617
1718type TabItem =
1819 { id :: String
@@ -22,5 +23,5 @@ type TabItem =
2223type NativeTabViewAttributes = BaseAttributes
2324 ( items :: Array TabItem
2425 , selectedItem :: String
25− , onSelectTab :: EventHandler
26+ , onSelectTab :: String -> Effect Unit
2627 )
src/Yoga/React/Native/MacOS/TableView.js+15−6
@@ -1,6 +1,15 @@
1−import { requireNativeComponent, Platform, UIManager, View } from "react-native";
2−let c = View;
3−if (Platform.OS === "macos") {
4− try { if (UIManager.getViewManagerConfig("MacOSTableView")) c = requireNativeComponent("MacOSTableView"); } catch (e) {}
5−}
6−export const _tableViewImpl = c;
1+import React from "react";
2+import { requireNativeComponent } from "react-native";
3+const NativeTableView = requireNativeComponent("MacOSTableView");
4+export const tableViewImpl = React.forwardRef((props, ref) => {
5+ const nativeProps = { ...props };
6+ if (typeof props.onSelectRow === "function") {
7+ nativeProps.onSelectRow = (e) =>
8+ props.onSelectRow(e.nativeEvent.rowIndex | 0)();
9+ }
10+ if (typeof props.onDoubleClickRow === "function") {
11+ nativeProps.onDoubleClickRow = (e) =>
12+ props.onDoubleClickRow(e.nativeEvent.rowIndex | 0)();
13+ }
14+ return React.createElement(NativeTableView, { ...nativeProps, ref });
15+});
src/Yoga/React/Native/MacOS/TableView.purs+6−5
@@ -4,15 +4,16 @@ module Yoga.React.Native.MacOS.TableView
44 , TableColumn
55 ) where
66
7+import Prelude
8+import Effect (Effect)
79import React.Basic (ReactComponent)
8−import React.Basic.Events (EventHandler)
910import Yoga.React.Native.Attributes (BaseAttributes)
1011import Yoga.React.Native.Internal (FFINativeComponent_, createNativeElement_)
1112
12−foreign import _tableViewImpl :: forall props. ReactComponent props
13+foreign import tableViewImpl :: forall props. ReactComponent props
1314
1415nativeTableView :: FFINativeComponent_ NativeTableViewAttributes
15−nativeTableView = createNativeElement_ _tableViewImpl
16+nativeTableView = createNativeElement_ tableViewImpl
1617
1718type TableColumn =
1819 { id :: String
@@ -25,6 +26,6 @@ type NativeTableViewAttributes = BaseAttributes
2526 , rows :: Array (Array String)
2627 , headerVisible :: Boolean
2728 , alternatingRows :: Boolean
28− , onSelectRow :: EventHandler
29− , onDoubleClickRow :: EventHandler
29+ , onSelectRow :: Int -> Effect Unit
30+ , onDoubleClickRow :: Int -> Effect Unit
3031 )
src/Yoga/React/Native/MacOS/TextEditor.js+10−6
@@ -1,6 +1,10 @@
1−import { requireNativeComponent, Platform, UIManager, View } from "react-native";
2−let c = View;
3−if (Platform.OS === "macos") {
4− try { if (UIManager.getViewManagerConfig("NativeTextEditor")) c = requireNativeComponent("NativeTextEditor"); } catch (e) {}
5−}
6−export const _textEditorImpl = c;
1+import React from "react";
2+import { requireNativeComponent } from "react-native";
3+const NativeTextEditor = requireNativeComponent("NativeTextEditor");
4+export const textEditorImpl = React.forwardRef((props, ref) => {
5+ const nativeProps = { ...props };
6+ if (typeof props.onChangeText === "function") {
7+ nativeProps.onChangeText = (e) => props.onChangeText(e.nativeEvent.text)();
8+ }
9+ return React.createElement(NativeTextEditor, { ...nativeProps, ref });
10+});
src/Yoga/React/Native/MacOS/TextEditor.purs+5−4
@@ -1,18 +1,19 @@
11module Yoga.React.Native.MacOS.TextEditor (nativeTextEditor, NativeTextEditorAttributes) where
22
3+import Prelude
4+import Effect (Effect)
35import React.Basic (ReactComponent)
4−import React.Basic.Events (EventHandler)
56import Yoga.React.Native.Attributes (BaseAttributes)
67import Yoga.React.Native.Internal (FFINativeComponent_, createNativeElement_)
78
8−foreign import _textEditorImpl :: forall props. ReactComponent props
9+foreign import textEditorImpl :: forall props. ReactComponent props
910
1011nativeTextEditor :: FFINativeComponent_ NativeTextEditorAttributes
11−nativeTextEditor = createNativeElement_ _textEditorImpl
12+nativeTextEditor = createNativeElement_ textEditorImpl
1213
1314type NativeTextEditorAttributes = BaseAttributes
1415 ( text :: String
1516 , richText :: Boolean
1617 , showsRuler :: Boolean
17− , onChangeText :: EventHandler
18+ , onChangeText :: String -> Effect Unit
1819 )
src/Yoga/React/Native/MacOS/TextField.js+15−6
@@ -1,6 +1,15 @@
1−import { requireNativeComponent, Platform, UIManager, View } from "react-native";
2−let c = View;
3−if (Platform.OS === "macos") {
4− try { if (UIManager.getViewManagerConfig("NativeTextField")) c = requireNativeComponent("NativeTextField"); } catch (e) {}
5−}
6−export const _textFieldImpl = c;
1+import React from "react";
2+import { requireNativeComponent } from "react-native";
3+
4+const NativeTextField = requireNativeComponent("NativeTextField");
5+
6+export const textFieldImpl = React.forwardRef((props, ref) => {
7+ const nativeProps = { ...props };
8+ if (typeof props.onChangeText === "function") {
9+ nativeProps.onChangeText = (e) => props.onChangeText(e.nativeEvent.text)();
10+ }
11+ if (typeof props.onSubmit === "function") {
12+ nativeProps.onSubmit = (e) => props.onSubmit(e.nativeEvent.text)();
13+ }
14+ return React.createElement(NativeTextField, { ...nativeProps, ref });
15+});
src/Yoga/React/Native/MacOS/TextField.purs+6−5
@@ -1,14 +1,15 @@
11module Yoga.React.Native.MacOS.TextField (nativeTextField, NativeTextFieldAttributes) where
22
3+import Prelude
4+import Effect (Effect)
35import React.Basic (ReactComponent)
4−import React.Basic.Events (EventHandler)
56import Yoga.React.Native.Attributes (BaseAttributes)
67import Yoga.React.Native.Internal (FFINativeComponent_, createNativeElement_)
78
8−foreign import _textFieldImpl :: forall props. ReactComponent props
9+foreign import textFieldImpl :: forall props. ReactComponent props
910
1011nativeTextField :: FFINativeComponent_ NativeTextFieldAttributes
11−nativeTextField = createNativeElement_ _textFieldImpl
12+nativeTextField = createNativeElement_ textFieldImpl
1213
1314type NativeTextFieldAttributes = BaseAttributes
1415 ( text :: String
@@ -16,6 +17,6 @@ type NativeTextFieldAttributes = BaseAttributes
1617 , secure :: Boolean
1718 , search :: Boolean
1819 , rounded :: Boolean
19− , onChangeText :: EventHandler
20− , onSubmit :: EventHandler
20+ , onChangeText :: String -> Effect Unit
21+ , onSubmit :: String -> Effect Unit
2122 )
src/Yoga/React/Native/MacOS/TokenField.js+11−6
@@ -1,6 +1,11 @@
1−import { requireNativeComponent, Platform, UIManager, View } from "react-native";
2−let c = View;
3−if (Platform.OS === "macos") {
4− try { if (UIManager.getViewManagerConfig("MacOSTokenField")) c = requireNativeComponent("MacOSTokenField"); } catch (e) {}
5−}
6−export const _tokenFieldImpl = c;
1+import React from "react";
2+import { requireNativeComponent } from "react-native";
3+const NativeTokenField = requireNativeComponent("MacOSTokenField");
4+export const tokenFieldImpl = React.forwardRef((props, ref) => {
5+ const nativeProps = { ...props };
6+ if (typeof props.onChangeTokens === "function") {
7+ nativeProps.onChangeTokens = (e) =>
8+ props.onChangeTokens(e.nativeEvent.tokens || [])();
9+ }
10+ return React.createElement(NativeTokenField, { ...nativeProps, ref });
11+});
src/Yoga/React/Native/MacOS/TokenField.purs+5−4
@@ -3,18 +3,19 @@ module Yoga.React.Native.MacOS.TokenField
33 , NativeTokenFieldAttributes
44 ) where
55
6+import Prelude
7+import Effect (Effect)
68import React.Basic (ReactComponent)
7−import React.Basic.Events (EventHandler)
89import Yoga.React.Native.Attributes (BaseAttributes)
910import Yoga.React.Native.Internal (FFINativeComponent_, createNativeElement_)
1011
11−foreign import _tokenFieldImpl :: forall props. ReactComponent props
12+foreign import tokenFieldImpl :: forall props. ReactComponent props
1213
1314nativeTokenField :: FFINativeComponent_ NativeTokenFieldAttributes
14−nativeTokenField = createNativeElement_ _tokenFieldImpl
15+nativeTokenField = createNativeElement_ tokenFieldImpl
1516
1617type NativeTokenFieldAttributes = BaseAttributes
1718 ( tokens :: Array String
1819 , placeholder :: String
19− , onChangeTokens :: EventHandler
20+ , onChangeTokens :: Array String -> Effect Unit
2021 )
src/Yoga/React/Native/MacOS/Toolbar.js+11−16
@@ -1,16 +1,11 @@
1−import {
2− View,
3− Platform,
4− UIManager,
5− requireNativeComponent,
6−} from "react-native";
7−
8−var c = View;
9−if (Platform.OS === "macos") {
10− try {
11− if (UIManager.getViewManagerConfig("MacOSToolbar"))
12− c = requireNativeComponent("MacOSToolbar");
13− } catch (e) {}
14−}
15−
16−export const _toolbarImpl = c;
1+import React from "react";
2+import { requireNativeComponent } from "react-native";
3+const NativeToolbar = requireNativeComponent("MacOSToolbar");
4+export const toolbarImpl = React.forwardRef((props, ref) => {
5+ const nativeProps = { ...props };
6+ if (typeof props.onSelectItem === "function") {
7+ nativeProps.onSelectItem = (e) =>
8+ props.onSelectItem(e.nativeEvent.itemId)();
9+ }
10+ return React.createElement(NativeToolbar, { ...nativeProps, ref });
11+});
src/Yoga/React/Native/MacOS/Toolbar.purs+5−4
@@ -1,15 +1,16 @@
11module Yoga.React.Native.MacOS.Toolbar (nativeToolbar, NativeToolbarAttributes, ToolbarItem) where
22
3+import Prelude
4+import Effect (Effect)
35import React.Basic (ReactComponent)
4−import React.Basic.Events (EventHandler)
56import Yoga.React.Native.Attributes (BaseAttributes)
67import Yoga.React.Native.Internal (FFINativeComponent_, createNativeElement_)
78import Yoga.React.Native.MacOS.Types (ToolbarStyle)
89
9−foreign import _toolbarImpl :: forall props. ReactComponent props
10+foreign import toolbarImpl :: forall props. ReactComponent props
1011
1112nativeToolbar :: FFINativeComponent_ NativeToolbarAttributes
12−nativeToolbar = createNativeElement_ _toolbarImpl
13+nativeToolbar = createNativeElement_ toolbarImpl
1314
1415type ToolbarItem =
1516 { id :: String
@@ -22,5 +23,5 @@ type NativeToolbarAttributes = BaseAttributes
2223 , selectedItem :: String
2324 , toolbarStyle :: ToolbarStyle
2425 , windowTitle :: String
25− , onSelectItem :: EventHandler
26+ , onSelectItem :: String -> Effect Unit
2627 )
src/Yoga/React/Native/MacOS/Types.purs+42
@@ -184,3 +184,45 @@ standardPath = PathControlStyle "standard"
184184
185185popupPath :: PathControlStyle
186186popupPath = PathControlStyle "popup"
187+
188+newtype MapType = MapType String
189+
190+standardMap :: MapType
191+standardMap = MapType "standard"
192+
193+satelliteMap :: MapType
194+satelliteMap = MapType "satellite"
195+
196+hybridMap :: MapType
197+hybridMap = MapType "hybrid"
198+
199+newtype VoiceIdentifier = VoiceIdentifier String
200+
201+alex :: VoiceIdentifier
202+alex = VoiceIdentifier "com.apple.speech.synthesis.voice.Alex"
203+
204+samantha :: VoiceIdentifier
205+samantha = VoiceIdentifier "com.apple.speech.synthesis.voice.samantha"
206+
207+daniel :: VoiceIdentifier
208+daniel = VoiceIdentifier "com.apple.speech.synthesis.voice.daniel.premium"
209+
210+karen :: VoiceIdentifier
211+karen = VoiceIdentifier "com.apple.speech.synthesis.voice.karen.premium"
212+
213+moira :: VoiceIdentifier
214+moira = VoiceIdentifier "com.apple.speech.synthesis.voice.moira.premium"
215+
216+newtype PDFDisplayMode = PDFDisplayMode String
217+
218+singlePage :: PDFDisplayMode
219+singlePage = PDFDisplayMode "singlePage"
220+
221+singlePageContinuous :: PDFDisplayMode
222+singlePageContinuous = PDFDisplayMode "singlePageContinuous"
223+
224+twoUp :: PDFDisplayMode
225+twoUp = PDFDisplayMode "twoUp"
226+
227+twoUpContinuous :: PDFDisplayMode
228+twoUpContinuous = PDFDisplayMode "twoUpContinuous"
src/Yoga/React/Native/MacOS/UserNotification.js+1−1
@@ -1,6 +1,6 @@
11import { NativeModules, Platform } from "react-native";
22
3−export const notifyImpl = (title, body) => {
3+export const notifyImpl = ({ title = "", body = "" }) => {
44 if (Platform.OS !== "macos") return;
55 const mod = NativeModules.MacOSNotificationModule;
66 if (mod && mod.notify) {
src/Yoga/React/Native/MacOS/UserNotification.purs+16−4
@@ -1,13 +1,25 @@
11module Yoga.React.Native.MacOS.UserNotification
22 ( macosNotify
3+ , NotifyProps
34 ) where
45
56import Prelude
67
78import Effect (Effect)
8−import Effect.Uncurried (EffectFn2, runEffectFn2)
9+import Effect.Uncurried (EffectFn1, runEffectFn1)
10+import Prim.Row (class Union)
11+import Unsafe.Coerce (unsafeCoerce)
912
10−foreign import notifyImpl :: EffectFn2 String String Unit
13+type NotifyProps =
14+ ( title :: String
15+ , body :: String
16+ )
1117
12−macosNotify :: String -> String -> Effect Unit
13−macosNotify = runEffectFn2 notifyImpl
18+foreign import notifyImpl :: EffectFn1 (Record NotifyProps) Unit
19+
20+macosNotify
21+ :: forall given missing
22+ . Union given missing NotifyProps
23+ => { | given }
24+ -> Effect Unit
25+macosNotify r = runEffectFn1 notifyImpl (unsafeCoerce r)
src/Yoga/React/Native/MacOS/VideoPlayer.js+2−16
@@ -1,16 +1,2 @@
1−import {
2− View,
3− Platform,
4− UIManager,
5− requireNativeComponent,
6−} from "react-native";
7−
8−var c = View;
9−if (Platform.OS === "macos") {
10− try {
11− if (UIManager.getViewManagerConfig("MacOSVideoPlayer"))
12− c = requireNativeComponent("MacOSVideoPlayer");
13− } catch (e) {}
14−}
15−
16−export const _videoPlayerImpl = c;
1+import { requireNativeComponent } from "react-native";
2+export const videoPlayerImpl = requireNativeComponent("MacOSVideoPlayer");
src/Yoga/React/Native/MacOS/VideoPlayer.purs+2−2
@@ -5,10 +5,10 @@ import Yoga.React.Native.Attributes (BaseAttributes)
55import Yoga.React.Native.Internal (FFINativeComponent_, createNativeElement_)
66import Yoga.React.Native.MacOS.Types (ControlsStyle)
77
8−foreign import _videoPlayerImpl :: forall props. ReactComponent props
8+foreign import videoPlayerImpl :: forall props. ReactComponent props
99
1010nativeVideoPlayer :: FFINativeComponent_ NativeVideoPlayerAttributes
11−nativeVideoPlayer = createNativeElement_ _videoPlayerImpl
11+nativeVideoPlayer = createNativeElement_ videoPlayerImpl
1212
1313type NativeVideoPlayerAttributes = BaseAttributes
1414 ( source :: String
src/Yoga/React/Native/MacOS/VisualEffect.js+2−16
@@ -1,16 +1,2 @@
1−import {
2− View,
3− Platform,
4− UIManager,
5− requireNativeComponent,
6−} from "react-native";
7−
8−var c = View;
9−if (Platform.OS === "macos") {
10− try {
11− if (UIManager.getViewManagerConfig("MacOSVisualEffectView"))
12− c = requireNativeComponent("MacOSVisualEffectView");
13− } catch (e) {}
14−}
15−
16−export const _visualEffectImpl = c;
1+import { requireNativeComponent } from "react-native";
2+export const visualEffectImpl = requireNativeComponent("MacOSVisualEffectView");
src/Yoga/React/Native/MacOS/VisualEffect.purs+2−2
@@ -5,10 +5,10 @@ import Yoga.React.Native.Attributes (BaseAttributes)
55import Yoga.React.Native.Internal (FFINativeComponent, createNativeElement)
66import Yoga.React.Native.MacOS.Types (VisualEffectMaterial, BlendingMode, VisualEffectState)
77
8−foreign import _visualEffectImpl :: forall props. ReactComponent props
8+foreign import visualEffectImpl :: forall props. ReactComponent props
99
1010nativeVisualEffect :: FFINativeComponent NativeVisualEffectAttributes
11−nativeVisualEffect = createNativeElement _visualEffectImpl
11+nativeVisualEffect = createNativeElement visualEffectImpl
1212
1313type NativeVisualEffectAttributes = BaseAttributes
1414 ( materialName :: VisualEffectMaterial
src/Yoga/React/Native/MacOS/WebView.js+13−6
@@ -1,6 +1,13 @@
1−import { requireNativeComponent, Platform, UIManager, View } from "react-native";
2−let c = View;
3−if (Platform.OS === "macos") {
4− try { if (UIManager.getViewManagerConfig("NativeWebView")) c = requireNativeComponent("NativeWebView"); } catch (e) {}
5−}
6−export const _webViewImpl = c;
1+import React from "react";
2+import { requireNativeComponent } from "react-native";
3+const NativeWebView = requireNativeComponent("NativeWebView");
4+export const webViewImpl = React.forwardRef((props, ref) => {
5+ const nativeProps = { ...props };
6+ if (typeof props.onNavigate === "function") {
7+ nativeProps.onNavigate = (e) => props.onNavigate(e.nativeEvent.url)();
8+ }
9+ if (typeof props.onFinishLoad === "function") {
10+ nativeProps.onFinishLoad = (e) => props.onFinishLoad(e.nativeEvent.url)();
11+ }
12+ return React.createElement(NativeWebView, { ...nativeProps, ref });
13+});
src/Yoga/React/Native/MacOS/WebView.purs+6−5
@@ -1,17 +1,18 @@
11module Yoga.React.Native.MacOS.WebView (nativeWebView, NativeWebViewAttributes) where
22
3+import Prelude
4+import Effect (Effect)
35import React.Basic (ReactComponent)
4−import React.Basic.Events (EventHandler)
56import Yoga.React.Native.Attributes (BaseAttributes)
67import Yoga.React.Native.Internal (FFINativeComponent_, createNativeElement_)
78
8−foreign import _webViewImpl :: forall props. ReactComponent props
9+foreign import webViewImpl :: forall props. ReactComponent props
910
1011nativeWebView :: FFINativeComponent_ NativeWebViewAttributes
11−nativeWebView = createNativeElement_ _webViewImpl
12+nativeWebView = createNativeElement_ webViewImpl
1213
1314type NativeWebViewAttributes = BaseAttributes
1415 ( url :: String
15− , onNavigate :: EventHandler
16− , onFinishLoad :: EventHandler
16+ , onNavigate :: String -> Effect Unit
17+ , onFinishLoad :: String -> Effect Unit
1718 )
src/Yoga/React/Native/MacOS/nativeComponent.jsnew file+15
@@ -0,0 +1,15 @@
1+import {
2+ requireNativeComponent,
3+ Platform,
4+ UIManager,
5+ View,
6+} from "react-native";
7+
8+export const nativeComponent = (name) => {
9+ if (Platform.OS !== "macos") return View;
10+ try {
11+ if (UIManager.getViewManagerConfig(name))
12+ return requireNativeComponent(name);
13+ } catch (_) {}
14+ return View;
15+};
test/MacOSComponents.test.js+85−76
@@ -1,207 +1,216 @@
11import { describe, it, expect } from "vitest";
22
33// Batch 1 — existing macOS native components
4−import { _buttonImpl } from "../src/Yoga/React/Native/MacOS/Button.js";
5−import { _segmentedImpl } from "../src/Yoga/React/Native/MacOS/Segmented.js";
6−import { _textFieldImpl } from "../src/Yoga/React/Native/MacOS/TextField.js";
7−import { _sliderImpl } from "../src/Yoga/React/Native/MacOS/Slider.js";
8−import { _switchImpl } from "../src/Yoga/React/Native/MacOS/Switch.js";
9−import { _progressImpl } from "../src/Yoga/React/Native/MacOS/Progress.js";
10−import { _datePickerImpl } from "../src/Yoga/React/Native/MacOS/DatePicker.js";
11−import { _colorWellImpl } from "../src/Yoga/React/Native/MacOS/ColorWell.js";
12−import { _popUpImpl } from "../src/Yoga/React/Native/MacOS/PopUp.js";
13−import { _textEditorImpl } from "../src/Yoga/React/Native/MacOS/TextEditor.js";
14−import { _webViewImpl } from "../src/Yoga/React/Native/MacOS/WebView.js";
15−import { _levelIndicatorImpl } from "../src/Yoga/React/Native/MacOS/LevelIndicator.js";
16−import { _nativeScrollViewImpl } from "../src/Yoga/React/Native/MacOS/ScrollView.js";
17−import { _visualEffectImpl } from "../src/Yoga/React/Native/MacOS/VisualEffect.js";
18−import { _contextMenuImpl } from "../src/Yoga/React/Native/MacOS/ContextMenu.js";
19−import { _filePickerImpl } from "../src/Yoga/React/Native/MacOS/FilePicker.js";
20−import { _videoPlayerImpl } from "../src/Yoga/React/Native/MacOS/VideoPlayer.js";
21−import { _animatedImageImpl } from "../src/Yoga/React/Native/MacOS/AnimatedImage.js";
22−import { _patternBackgroundImpl } from "../src/Yoga/React/Native/MacOS/PatternBackground.js";
23−import { _splitViewImpl } from "../src/Yoga/React/Native/MacOS/SplitView.js";
24−import { _tabViewImpl } from "../src/Yoga/React/Native/MacOS/TabView.js";
25−import { _comboBoxImpl } from "../src/Yoga/React/Native/MacOS/ComboBox.js";
26−import { _stepperImpl } from "../src/Yoga/React/Native/MacOS/Stepper.js";
27−import { _boxImpl } from "../src/Yoga/React/Native/MacOS/Box.js";
28−import { _popoverImpl } from "../src/Yoga/React/Native/MacOS/Popover.js";
29−import { _imageImpl } from "../src/Yoga/React/Native/MacOS/Image.js";
4+import { buttonImpl } from "../src/Yoga/React/Native/MacOS/Button.js";
5+import { segmentedImpl } from "../src/Yoga/React/Native/MacOS/Segmented.js";
6+import { textFieldImpl } from "../src/Yoga/React/Native/MacOS/TextField.js";
7+import { sliderImpl } from "../src/Yoga/React/Native/MacOS/Slider.js";
8+import { switchImpl } from "../src/Yoga/React/Native/MacOS/Switch.js";
9+import { progressImpl } from "../src/Yoga/React/Native/MacOS/Progress.js";
10+import { datePickerImpl } from "../src/Yoga/React/Native/MacOS/DatePicker.js";
11+import { colorWellImpl } from "../src/Yoga/React/Native/MacOS/ColorWell.js";
12+import { popUpImpl } from "../src/Yoga/React/Native/MacOS/PopUp.js";
13+import { textEditorImpl } from "../src/Yoga/React/Native/MacOS/TextEditor.js";
14+import { webViewImpl } from "../src/Yoga/React/Native/MacOS/WebView.js";
15+import { levelIndicatorImpl } from "../src/Yoga/React/Native/MacOS/LevelIndicator.js";
16+import { nativeScrollViewImpl } from "../src/Yoga/React/Native/MacOS/ScrollView.js";
17+import { visualEffectImpl } from "../src/Yoga/React/Native/MacOS/VisualEffect.js";
18+import { contextMenuImpl } from "../src/Yoga/React/Native/MacOS/ContextMenu.js";
19+import { filePickerImpl } from "../src/Yoga/React/Native/MacOS/FilePicker.js";
20+import { videoPlayerImpl } from "../src/Yoga/React/Native/MacOS/VideoPlayer.js";
21+import { animatedImageImpl } from "../src/Yoga/React/Native/MacOS/AnimatedImage.js";
22+import { patternBackgroundImpl } from "../src/Yoga/React/Native/MacOS/PatternBackground.js";
23+import { splitViewImpl } from "../src/Yoga/React/Native/MacOS/SplitView.js";
24+import { tabViewImpl } from "../src/Yoga/React/Native/MacOS/TabView.js";
25+import { comboBoxImpl } from "../src/Yoga/React/Native/MacOS/ComboBox.js";
26+import { stepperImpl } from "../src/Yoga/React/Native/MacOS/Stepper.js";
27+import { boxImpl } from "../src/Yoga/React/Native/MacOS/Box.js";
28+import { popoverImpl } from "../src/Yoga/React/Native/MacOS/Popover.js";
29+import { imageImpl } from "../src/Yoga/React/Native/MacOS/Image.js";
3030
3131// Batch 2 — new macOS native components
32−import { _checkboxImpl } from "../src/Yoga/React/Native/MacOS/Checkbox.js";
33−import { _radioButtonImpl } from "../src/Yoga/React/Native/MacOS/RadioButton.js";
34−import { _searchFieldImpl } from "../src/Yoga/React/Native/MacOS/SearchField.js";
35−import { _tokenFieldImpl } from "../src/Yoga/React/Native/MacOS/TokenField.js";
36−import { _separatorImpl } from "../src/Yoga/React/Native/MacOS/Separator.js";
37−import { _helpButtonImpl } from "../src/Yoga/React/Native/MacOS/HelpButton.js";
38−import { _pathControlImpl } from "../src/Yoga/React/Native/MacOS/PathControl.js";
39−import { _sheetImpl } from "../src/Yoga/React/Native/MacOS/Sheet.js";
40−import { _tableViewImpl } from "../src/Yoga/React/Native/MacOS/TableView.js";
32+import { checkboxImpl } from "../src/Yoga/React/Native/MacOS/Checkbox.js";
33+import { radioButtonImpl } from "../src/Yoga/React/Native/MacOS/RadioButton.js";
34+import { searchFieldImpl } from "../src/Yoga/React/Native/MacOS/SearchField.js";
35+import { tokenFieldImpl } from "../src/Yoga/React/Native/MacOS/TokenField.js";
36+import { separatorImpl } from "../src/Yoga/React/Native/MacOS/Separator.js";
37+import { helpButtonImpl } from "../src/Yoga/React/Native/MacOS/HelpButton.js";
38+import { pathControlImpl } from "../src/Yoga/React/Native/MacOS/PathControl.js";
39+import { sheetImpl } from "../src/Yoga/React/Native/MacOS/Sheet.js";
40+import { tableViewImpl } from "../src/Yoga/React/Native/MacOS/TableView.js";
4141
4242describe("macOS native component FFI exports (batch 1)", () => {
4343 it("Button resolves to NativeButton", () => {
44− expect(_buttonImpl).toBe("NativeButton");
44+ expect(buttonImpl).toBe("NativeButton");
4545 });
4646
4747 it("Segmented resolves to NativeSegmented", () => {
48− expect(_segmentedImpl).toBe("NativeSegmented");
48+ expect(segmentedImpl).toBe("NativeSegmented");
4949 });
5050
5151 it("TextField resolves to NativeTextField", () => {
52− expect(_textFieldImpl).toBe("NativeTextField");
52+ expect(textFieldImpl).toBe("NativeTextField");
5353 });
5454
5555 it("Slider resolves to NativeSlider", () => {
56− expect(_sliderImpl).toBe("NativeSlider");
56+ expect(sliderImpl).toBe("NativeSlider");
5757 });
5858
5959 it("Switch resolves to NativeSwitch", () => {
60− expect(_switchImpl).toBe("NativeSwitch");
60+ expect(switchImpl).toBe("NativeSwitch");
6161 });
6262
6363 it("Progress resolves to NativeProgress", () => {
64− expect(_progressImpl).toBe("NativeProgress");
64+ expect(progressImpl).toBe("NativeProgress");
6565 });
6666
6767 it("DatePicker resolves to NativeDatePicker", () => {
68− expect(_datePickerImpl).toBe("NativeDatePicker");
68+ expect(datePickerImpl).toBe("NativeDatePicker");
6969 });
7070
7171 it("ColorWell resolves to NativeColorWell", () => {
72− expect(_colorWellImpl).toBe("NativeColorWell");
72+ expect(colorWellImpl).toBe("NativeColorWell");
7373 });
7474
7575 it("PopUp resolves to NativePopUp", () => {
76− expect(_popUpImpl).toBe("NativePopUp");
76+ expect(popUpImpl).toBe("NativePopUp");
7777 });
7878
7979 it("TextEditor resolves to NativeTextEditor", () => {
80− expect(_textEditorImpl).toBe("NativeTextEditor");
80+ expect(textEditorImpl).toBe("NativeTextEditor");
8181 });
8282
8383 it("WebView resolves to NativeWebView", () => {
84− expect(_webViewImpl).toBe("NativeWebView");
84+ expect(webViewImpl).toBe("NativeWebView");
8585 });
8686
8787 it("LevelIndicator resolves to NativeLevelIndicator", () => {
88− expect(_levelIndicatorImpl).toBe("NativeLevelIndicator");
88+ expect(levelIndicatorImpl).toBe("NativeLevelIndicator");
8989 });
9090
9191 it("ScrollView resolves to MacOSScrollView", () => {
92− expect(_nativeScrollViewImpl).toBe("MacOSScrollView");
92+ expect(nativeScrollViewImpl).toBe("MacOSScrollView");
9393 });
9494
9595 it("VisualEffect resolves to MacOSVisualEffectView", () => {
96− expect(_visualEffectImpl).toBe("MacOSVisualEffectView");
96+ expect(visualEffectImpl).toBe("MacOSVisualEffectView");
9797 });
9898
9999 it("ContextMenu resolves to MacOSContextMenu", () => {
100− expect(_contextMenuImpl).toBe("MacOSContextMenu");
100+ expect(contextMenuImpl).toBe("MacOSContextMenu");
101101 });
102102
103103 it("FilePicker resolves to MacOSFilePicker", () => {
104− expect(_filePickerImpl).toBe("MacOSFilePicker");
104+ expect(filePickerImpl).toBe("MacOSFilePicker");
105105 });
106106
107107 it("VideoPlayer resolves to MacOSVideoPlayer", () => {
108− expect(_videoPlayerImpl).toBe("MacOSVideoPlayer");
108+ expect(videoPlayerImpl).toBe("MacOSVideoPlayer");
109109 });
110110
111111 it("AnimatedImage resolves to MacOSAnimatedImage", () => {
112− expect(_animatedImageImpl).toBe("MacOSAnimatedImage");
112+ expect(animatedImageImpl).toBe("MacOSAnimatedImage");
113113 });
114114
115115 it("PatternBackground resolves to MacOSPatternBackground", () => {
116− expect(_patternBackgroundImpl).toBe("MacOSPatternBackground");
116+ expect(patternBackgroundImpl).toBe("MacOSPatternBackground");
117117 });
118118
119119 it("SplitView resolves to MacOSSplitView", () => {
120− expect(_splitViewImpl).toBe("MacOSSplitView");
120+ expect(splitViewImpl).toBe("MacOSSplitView");
121121 });
122122
123123 it("TabView resolves to MacOSTabView", () => {
124− expect(_tabViewImpl).toBe("MacOSTabView");
124+ expect(tabViewImpl).toBe("MacOSTabView");
125125 });
126126
127127 it("ComboBox resolves to MacOSComboBox", () => {
128− expect(_comboBoxImpl).toBe("MacOSComboBox");
128+ expect(comboBoxImpl).toBe("MacOSComboBox");
129129 });
130130
131131 it("Stepper resolves to MacOSStepper", () => {
132− expect(_stepperImpl).toBe("MacOSStepper");
132+ expect(stepperImpl).toBe("MacOSStepper");
133133 });
134134
135135 it("Box resolves to MacOSBox", () => {
136− expect(_boxImpl).toBe("MacOSBox");
136+ expect(boxImpl).toBe("MacOSBox");
137137 });
138138
139139 it("Popover resolves to MacOSPopover", () => {
140− expect(_popoverImpl).toBe("MacOSPopover");
140+ expect(popoverImpl).toBe("MacOSPopover");
141141 });
142142
143143 it("Image resolves to MacOSImage", () => {
144− expect(_imageImpl).toBe("MacOSImage");
144+ expect(imageImpl).toBe("MacOSImage");
145145 });
146146});
147147
148148describe("macOS native component FFI exports (batch 2)", () => {
149149 it("Checkbox resolves to MacOSCheckbox", () => {
150− expect(_checkboxImpl).toBe("MacOSCheckbox");
150+ expect(checkboxImpl).toBe("MacOSCheckbox");
151151 });
152152
153153 it("RadioButton resolves to MacOSRadioButton", () => {
154− expect(_radioButtonImpl).toBe("MacOSRadioButton");
154+ expect(radioButtonImpl).toBe("MacOSRadioButton");
155155 });
156156
157157 it("SearchField resolves to MacOSSearchField", () => {
158− expect(_searchFieldImpl).toBe("MacOSSearchField");
158+ expect(searchFieldImpl).toBe("MacOSSearchField");
159159 });
160160
161161 it("TokenField resolves to MacOSTokenField", () => {
162− expect(_tokenFieldImpl).toBe("MacOSTokenField");
162+ expect(tokenFieldImpl).toBe("MacOSTokenField");
163163 });
164164
165165 it("Separator resolves to MacOSSeparator", () => {
166− expect(_separatorImpl).toBe("MacOSSeparator");
166+ expect(separatorImpl).toBe("MacOSSeparator");
167167 });
168168
169169 it("HelpButton resolves to MacOSHelpButton", () => {
170− expect(_helpButtonImpl).toBe("MacOSHelpButton");
170+ expect(helpButtonImpl).toBe("MacOSHelpButton");
171171 });
172172
173173 it("PathControl resolves to MacOSPathControl", () => {
174− expect(_pathControlImpl).toBe("MacOSPathControl");
174+ expect(pathControlImpl).toBe("MacOSPathControl");
175175 });
176176
177177 it("Sheet resolves to MacOSSheet", () => {
178− expect(_sheetImpl).toBe("MacOSSheet");
178+ expect(sheetImpl).toBe("MacOSSheet");
179179 });
180180
181181 it("TableView resolves to MacOSTableView", () => {
182− expect(_tableViewImpl).toBe("MacOSTableView");
182+ expect(tableViewImpl).toBe("MacOSTableView");
183183 });
184184});
185185
186186// Batch 3
187−import { _outlineViewImpl } from "../src/Yoga/React/Native/MacOS/OutlineView.js";
187+import { outlineViewImpl } from "../src/Yoga/React/Native/MacOS/OutlineView.js";
188188
189189describe("macOS native component FFI exports (batch 3)", () => {
190190 it("OutlineView resolves to MacOSOutlineView", () => {
191− expect(_outlineViewImpl).toBe("MacOSOutlineView");
191+ expect(outlineViewImpl).toBe("MacOSOutlineView");
192192 });
193193});
194194
195195// Batch 4
196−import { _mapViewImpl } from "../src/Yoga/React/Native/MacOS/MapView.js";
197−import { _pdfViewImpl } from "../src/Yoga/React/Native/MacOS/PDFView.js";
196+import { mapViewImpl } from "../src/Yoga/React/Native/MacOS/MapView.js";
197+import { pdfViewImpl } from "../src/Yoga/React/Native/MacOS/PDFView.js";
198198
199199describe("macOS native component FFI exports (batch 4)", () => {
200200 it("MapView resolves to MacOSMapView", () => {
201− expect(_mapViewImpl).toBe("MacOSMapView");
201+ expect(mapViewImpl).toBe("MacOSMapView");
202202 });
203203
204204 it("PDFView resolves to MacOSPDFView", () => {
205− expect(_pdfViewImpl).toBe("MacOSPDFView");
205+ expect(pdfViewImpl).toBe("MacOSPDFView");
206+ });
207+});
208+
209+// Batch 5
210+import { cameraViewImpl } from "../src/Yoga/React/Native/MacOS/CameraView.js";
211+
212+describe("macOS native component FFI exports (batch 5)", () => {
213+ it("CameraView resolves to MacOSCameraView", () => {
214+ expect(cameraViewImpl).toBe("MacOSCameraView");
206215 });
207216});
test/MacOSImperative.test.js+43−27
@@ -30,15 +30,15 @@ import {
3030 showFontPanelImpl,
3131 hideFontPanelImpl,
3232} from "../src/Yoga/React/Native/MacOS/FontPanel.js";
33−import { recognizeImpl } from "../src/Yoga/React/Native/MacOS/OCR.js";
33+import { recognizeTextImpl } from "../src/Yoga/React/Native/MacOS/OCR.js";
3434import {
35− startListeningImpl,
36− stopListeningImpl,
37− getTranscriptImpl,
35+ startImpl,
36+ stopImpl,
37+ pollTranscriptImpl,
3838} from "../src/Yoga/React/Native/MacOS/SpeechRecognition.js";
3939import {
4040 detectLanguageImpl,
41− sentimentImpl,
41+ analyzeSentimentImpl,
4242 tokenizeImpl,
4343} from "../src/Yoga/React/Native/MacOS/NaturalLanguage.js";
4444
@@ -240,12 +240,16 @@ describe("macOS FontPanel FFI", () => {
240240});
241241
242242describe("macOS OCR FFI", () => {
243− it("recognizeImpl calls NativeModules.MacOSOCRModule.recognize with callback", async () => {
243+ it("recognizeTextImpl returns an Aff that calls NativeModules.MacOSOCRModule.recognize", async () => {
244244 const spy = vi.spyOn(NativeModules.MacOSOCRModule, "recognize");
245+ const aff = recognizeTextImpl("/tmp/test.png");
245246 let result = "";
246− recognizeImpl("/tmp/test.png", (text) => () => {
247− result = text;
248− });
247+ aff(
248+ (err) => {},
249+ (val) => {
250+ result = val;
251+ },
252+ );
249253 await new Promise((r) => setTimeout(r, 10));
250254 expect(spy).toHaveBeenCalledWith("/tmp/test.png");
251255 expect(result).toBe("Recognized text from /tmp/test.png");
@@ -254,17 +258,17 @@ describe("macOS OCR FFI", () => {
254258});
255259
256260describe("macOS SpeechRecognition FFI", () => {
257− it("startListeningImpl calls NativeModules.MacOSSpeechRecognitionModule.start", () => {
261+ it("startImpl calls NativeModules.MacOSSpeechRecognitionModule.start", () => {
258262 const spy = vi.spyOn(NativeModules.MacOSSpeechRecognitionModule, "start");
259− startListeningImpl();
263+ startImpl();
260264 expect(spy).toHaveBeenCalled();
261265 spy.mockRestore();
262266 });
263267
264− it("stopListeningImpl calls stop and invokes callback with transcript", async () => {
268+ it("stopImpl calls stop and invokes callback with transcript", async () => {
265269 const spy = vi.spyOn(NativeModules.MacOSSpeechRecognitionModule, "stop");
266270 let result = "";
267− stopListeningImpl((text) => () => {
271+ stopImpl((text) => () => {
268272 result = text;
269273 });
270274 await new Promise((r) => setTimeout(r, 10));
@@ -273,13 +277,13 @@ describe("macOS SpeechRecognition FFI", () => {
273277 spy.mockRestore();
274278 });
275279
276− it("getTranscriptImpl calls getTranscript and invokes callback", async () => {
280+ it("pollTranscriptImpl calls getTranscript and invokes callback", async () => {
277281 const spy = vi.spyOn(
278282 NativeModules.MacOSSpeechRecognitionModule,
279283 "getTranscript",
280284 );
281285 let result = "";
282− getTranscriptImpl((text) => () => {
286+ pollTranscriptImpl((text) => () => {
283287 result = text;
284288 });
285289 await new Promise((r) => setTimeout(r, 10));
@@ -290,36 +294,48 @@ describe("macOS SpeechRecognition FFI", () => {
290294});
291295
292296describe("macOS NaturalLanguage FFI", () => {
293− it("detectLanguageImpl calls detectLanguage and invokes callback", async () => {
297+ it("detectLanguageImpl returns an Aff that resolves with language code", async () => {
294298 const spy = vi.spyOn(NativeModules.MacOSNLModule, "detectLanguage");
299+ const aff = detectLanguageImpl("Hello world");
295300 let result = "";
296− detectLanguageImpl("Hello world", (lang) => () => {
297− result = lang;
298− });
301+ aff(
302+ (err) => {},
303+ (val) => {
304+ result = val;
305+ },
306+ );
299307 await new Promise((r) => setTimeout(r, 10));
300308 expect(spy).toHaveBeenCalledWith("Hello world");
301309 expect(result).toBe("en");
302310 spy.mockRestore();
303311 });
304312
305− it("sentimentImpl calls sentiment and invokes callback with score", async () => {
313+ it("analyzeSentimentImpl returns an Aff that resolves with score", async () => {
306314 const spy = vi.spyOn(NativeModules.MacOSNLModule, "sentiment");
315+ const aff = analyzeSentimentImpl("I love this");
307316 let result = -1;
308− sentimentImpl("I love this", (score) => () => {
309− result = score;
310− });
317+ aff(
318+ (err) => {},
319+ (val) => {
320+ result = val;
321+ },
322+ );
311323 await new Promise((r) => setTimeout(r, 10));
312324 expect(spy).toHaveBeenCalledWith("I love this");
313325 expect(result).toBe(0.5);
314326 spy.mockRestore();
315327 });
316328
317− it("tokenizeImpl calls tokenize and invokes callback with tokens", async () => {
329+ it("tokenizeImpl returns an Aff that resolves with tokens", async () => {
318330 const spy = vi.spyOn(NativeModules.MacOSNLModule, "tokenize");
331+ const aff = tokenizeImpl("Hello world");
319332 let result = [];
320− tokenizeImpl("Hello world", (tokens) => () => {
321− result = tokens;
322− });
333+ aff(
334+ (err) => {},
335+ (val) => {
336+ result = val;
337+ },
338+ );
323339 await new Promise((r) => setTimeout(r, 10));
324340 expect(spy).toHaveBeenCalledWith("Hello world");
325341 expect(result).toEqual(["Hello", "world"]);
test/MacOSSnapshots.test.js+81−75
@@ -46,44 +46,45 @@ import {
4646} from "../src/Yoga/React/Native/Internal.js";
4747
4848// Leaf components (no children)
49−import { _buttonImpl } from "../src/Yoga/React/Native/MacOS/Button.js";
50−import { _checkboxImpl } from "../src/Yoga/React/Native/MacOS/Checkbox.js";
51−import { _radioButtonImpl } from "../src/Yoga/React/Native/MacOS/RadioButton.js";
52−import { _searchFieldImpl } from "../src/Yoga/React/Native/MacOS/SearchField.js";
53−import { _tokenFieldImpl } from "../src/Yoga/React/Native/MacOS/TokenField.js";
54−import { _separatorImpl } from "../src/Yoga/React/Native/MacOS/Separator.js";
55−import { _helpButtonImpl } from "../src/Yoga/React/Native/MacOS/HelpButton.js";
56−import { _pathControlImpl } from "../src/Yoga/React/Native/MacOS/PathControl.js";
57−import { _textFieldImpl } from "../src/Yoga/React/Native/MacOS/TextField.js";
58−import { _sliderImpl } from "../src/Yoga/React/Native/MacOS/Slider.js";
59−import { _switchImpl } from "../src/Yoga/React/Native/MacOS/Switch.js";
60−import { _progressImpl } from "../src/Yoga/React/Native/MacOS/Progress.js";
61−import { _datePickerImpl } from "../src/Yoga/React/Native/MacOS/DatePicker.js";
62−import { _colorWellImpl } from "../src/Yoga/React/Native/MacOS/ColorWell.js";
63−import { _popUpImpl } from "../src/Yoga/React/Native/MacOS/PopUp.js";
64−import { _textEditorImpl } from "../src/Yoga/React/Native/MacOS/TextEditor.js";
65−import { _levelIndicatorImpl } from "../src/Yoga/React/Native/MacOS/LevelIndicator.js";
66−import { _segmentedImpl } from "../src/Yoga/React/Native/MacOS/Segmented.js";
67−import { _stepperImpl } from "../src/Yoga/React/Native/MacOS/Stepper.js";
68−import { _comboBoxImpl } from "../src/Yoga/React/Native/MacOS/ComboBox.js";
69−import { _imageImpl } from "../src/Yoga/React/Native/MacOS/Image.js";
70−import { _animatedImageImpl } from "../src/Yoga/React/Native/MacOS/AnimatedImage.js";
71−import { _videoPlayerImpl } from "../src/Yoga/React/Native/MacOS/VideoPlayer.js";
72−import { _tableViewImpl } from "../src/Yoga/React/Native/MacOS/TableView.js";
73−import { _outlineViewImpl } from "../src/Yoga/React/Native/MacOS/OutlineView.js";
74−import { _mapViewImpl } from "../src/Yoga/React/Native/MacOS/MapView.js";
75−import { _pdfViewImpl } from "../src/Yoga/React/Native/MacOS/PDFView.js";
49+import { buttonImpl } from "../src/Yoga/React/Native/MacOS/Button.js";
50+import { checkboxImpl } from "../src/Yoga/React/Native/MacOS/Checkbox.js";
51+import { radioButtonImpl } from "../src/Yoga/React/Native/MacOS/RadioButton.js";
52+import { searchFieldImpl } from "../src/Yoga/React/Native/MacOS/SearchField.js";
53+import { tokenFieldImpl } from "../src/Yoga/React/Native/MacOS/TokenField.js";
54+import { separatorImpl } from "../src/Yoga/React/Native/MacOS/Separator.js";
55+import { helpButtonImpl } from "../src/Yoga/React/Native/MacOS/HelpButton.js";
56+import { pathControlImpl } from "../src/Yoga/React/Native/MacOS/PathControl.js";
57+import { textFieldImpl } from "../src/Yoga/React/Native/MacOS/TextField.js";
58+import { sliderImpl } from "../src/Yoga/React/Native/MacOS/Slider.js";
59+import { switchImpl } from "../src/Yoga/React/Native/MacOS/Switch.js";
60+import { progressImpl } from "../src/Yoga/React/Native/MacOS/Progress.js";
61+import { datePickerImpl } from "../src/Yoga/React/Native/MacOS/DatePicker.js";
62+import { colorWellImpl } from "../src/Yoga/React/Native/MacOS/ColorWell.js";
63+import { popUpImpl } from "../src/Yoga/React/Native/MacOS/PopUp.js";
64+import { textEditorImpl } from "../src/Yoga/React/Native/MacOS/TextEditor.js";
65+import { levelIndicatorImpl } from "../src/Yoga/React/Native/MacOS/LevelIndicator.js";
66+import { segmentedImpl } from "../src/Yoga/React/Native/MacOS/Segmented.js";
67+import { stepperImpl } from "../src/Yoga/React/Native/MacOS/Stepper.js";
68+import { comboBoxImpl } from "../src/Yoga/React/Native/MacOS/ComboBox.js";
69+import { imageImpl } from "../src/Yoga/React/Native/MacOS/Image.js";
70+import { animatedImageImpl } from "../src/Yoga/React/Native/MacOS/AnimatedImage.js";
71+import { videoPlayerImpl } from "../src/Yoga/React/Native/MacOS/VideoPlayer.js";
72+import { tableViewImpl } from "../src/Yoga/React/Native/MacOS/TableView.js";
73+import { outlineViewImpl } from "../src/Yoga/React/Native/MacOS/OutlineView.js";
74+import { mapViewImpl } from "../src/Yoga/React/Native/MacOS/MapView.js";
75+import { pdfViewImpl } from "../src/Yoga/React/Native/MacOS/PDFView.js";
76+import { cameraViewImpl } from "../src/Yoga/React/Native/MacOS/CameraView.js";
7677
7778// Container components (with children)
78−import { _sheetImpl } from "../src/Yoga/React/Native/MacOS/Sheet.js";
79−import { _boxImpl } from "../src/Yoga/React/Native/MacOS/Box.js";
80−import { _popoverImpl } from "../src/Yoga/React/Native/MacOS/Popover.js";
81−import { _nativeScrollViewImpl } from "../src/Yoga/React/Native/MacOS/ScrollView.js";
82−import { _visualEffectImpl } from "../src/Yoga/React/Native/MacOS/VisualEffect.js";
83−import { _contextMenuImpl } from "../src/Yoga/React/Native/MacOS/ContextMenu.js";
84−import { _splitViewImpl } from "../src/Yoga/React/Native/MacOS/SplitView.js";
85−import { _tabViewImpl } from "../src/Yoga/React/Native/MacOS/TabView.js";
86−import { _patternBackgroundImpl } from "../src/Yoga/React/Native/MacOS/PatternBackground.js";
79+import { sheetImpl } from "../src/Yoga/React/Native/MacOS/Sheet.js";
80+import { boxImpl } from "../src/Yoga/React/Native/MacOS/Box.js";
81+import { popoverImpl } from "../src/Yoga/React/Native/MacOS/Popover.js";
82+import { nativeScrollViewImpl } from "../src/Yoga/React/Native/MacOS/ScrollView.js";
83+import { visualEffectImpl } from "../src/Yoga/React/Native/MacOS/VisualEffect.js";
84+import { contextMenuImpl } from "../src/Yoga/React/Native/MacOS/ContextMenu.js";
85+import { splitViewImpl } from "../src/Yoga/React/Native/MacOS/SplitView.js";
86+import { tabViewImpl } from "../src/Yoga/React/Native/MacOS/TabView.js";
87+import { patternBackgroundImpl } from "../src/Yoga/React/Native/MacOS/PatternBackground.js";
8788
8889const noop = () => {};
8990
@@ -92,7 +93,7 @@ describe("macOS component snapshots", () => {
9293
9394 it("Button", () => {
9495 const { toJSON } = render(
95− el(_buttonImpl, {
96+ el(buttonImpl, {
9697 title: "Click Me",
9798 bezelStyle: "rounded",
9899 onPress: noop,
@@ -103,7 +104,7 @@ describe("macOS component snapshots", () => {
103104
104105 it("Checkbox checked", () => {
105106 const { toJSON } = render(
106− el(_checkboxImpl, {
107+ el(checkboxImpl, {
107108 checked: true,
108109 title: "Accept terms",
109110 enabled: true,
@@ -114,7 +115,7 @@ describe("macOS component snapshots", () => {
114115
115116 it("Checkbox unchecked", () => {
116117 const { toJSON } = render(
117− el(_checkboxImpl, {
118+ el(checkboxImpl, {
118119 checked: false,
119120 title: "Accept terms",
120121 enabled: true,
@@ -125,7 +126,7 @@ describe("macOS component snapshots", () => {
125126
126127 it("RadioButton selected", () => {
127128 const { toJSON } = render(
128− el(_radioButtonImpl, {
129+ el(radioButtonImpl, {
129130 selected: true,
130131 title: "Option A",
131132 enabled: true,
@@ -136,7 +137,7 @@ describe("macOS component snapshots", () => {
136137
137138 it("RadioButton unselected", () => {
138139 const { toJSON } = render(
139− el(_radioButtonImpl, {
140+ el(radioButtonImpl, {
140141 selected: false,
141142 title: "Option B",
142143 enabled: true,
@@ -146,13 +147,13 @@ describe("macOS component snapshots", () => {
146147 });
147148
148149 it("Switch on", () => {
149− const { toJSON } = render(el(_switchImpl, { value: true, onChange: noop }));
150+ const { toJSON } = render(el(switchImpl, { value: true, onChange: noop }));
150151 expect(toJSON()).toMatchSnapshot();
151152 });
152153
153154 it("Slider", () => {
154155 const { toJSON } = render(
155− el(_sliderImpl, {
156+ el(sliderImpl, {
156157 value: 50,
157158 minimumValue: 0,
158159 maximumValue: 100,
@@ -164,7 +165,7 @@ describe("macOS component snapshots", () => {
164165
165166 it("Stepper", () => {
166167 const { toJSON } = render(
167− el(_stepperImpl, {
168+ el(stepperImpl, {
168169 value: 5,
169170 minValue: 0,
170171 maxValue: 10,
@@ -176,7 +177,7 @@ describe("macOS component snapshots", () => {
176177
177178 it("Segmented", () => {
178179 const { toJSON } = render(
179− el(_segmentedImpl, {
180+ el(segmentedImpl, {
180181 values: ["A", "B", "C"],
181182 selectedIndex: 1,
182183 onChange: noop,
@@ -186,13 +187,13 @@ describe("macOS component snapshots", () => {
186187 });
187188
188189 it("HelpButton", () => {
189− const { toJSON } = render(el(_helpButtonImpl, { onPress: noop }));
190+ const { toJSON } = render(el(helpButtonImpl, { onPress: noop }));
190191 expect(toJSON()).toMatchSnapshot();
191192 });
192193
193194 it("PopUp", () => {
194195 const { toJSON } = render(
195− el(_popUpImpl, {
196+ el(popUpImpl, {
196197 items: ["One", "Two"],
197198 selectedIndex: 0,
198199 onChange: noop,
@@ -203,7 +204,7 @@ describe("macOS component snapshots", () => {
203204
204205 it("ComboBox", () => {
205206 const { toJSON } = render(
206− el(_comboBoxImpl, {
207+ el(comboBoxImpl, {
207208 items: ["foo", "bar"],
208209 text: "foo",
209210 onChangeText: noop,
@@ -216,7 +217,7 @@ describe("macOS component snapshots", () => {
216217
217218 it("TextField", () => {
218219 const { toJSON } = render(
219− el(_textFieldImpl, {
220+ el(textFieldImpl, {
220221 text: "Hello",
221222 placeholder: "Type here",
222223 onChangeText: noop,
@@ -227,7 +228,7 @@ describe("macOS component snapshots", () => {
227228
228229 it("SearchField", () => {
229230 const { toJSON } = render(
230− el(_searchFieldImpl, {
231+ el(searchFieldImpl, {
231232 text: "",
232233 placeholder: "Search...",
233234 onChangeText: noop,
@@ -238,7 +239,7 @@ describe("macOS component snapshots", () => {
238239
239240 it("TokenField", () => {
240241 const { toJSON } = render(
241− el(_tokenFieldImpl, {
242+ el(tokenFieldImpl, {
242243 tokens: ["swift", "purescript"],
243244 placeholder: "Add tag",
244245 }),
@@ -248,7 +249,7 @@ describe("macOS component snapshots", () => {
248249
249250 it("TextEditor", () => {
250251 const { toJSON } = render(
251− el(_textEditorImpl, { text: "Some long text", onChangeText: noop }),
252+ el(textEditorImpl, { text: "Some long text", onChangeText: noop }),
252253 );
253254 expect(toJSON()).toMatchSnapshot();
254255 });
@@ -256,13 +257,13 @@ describe("macOS component snapshots", () => {
256257 // -- Indicators --
257258
258259 it("Progress determinate", () => {
259− const { toJSON } = render(el(_progressImpl, { value: 0.6, style: "bar" }));
260+ const { toJSON } = render(el(progressImpl, { value: 0.6, style: "bar" }));
260261 expect(toJSON()).toMatchSnapshot();
261262 });
262263
263264 it("LevelIndicator", () => {
264265 const { toJSON } = render(
265− el(_levelIndicatorImpl, { value: 3, minValue: 0, maxValue: 5 }),
266+ el(levelIndicatorImpl, { value: 3, minValue: 0, maxValue: 5 }),
266267 );
267268 expect(toJSON()).toMatchSnapshot();
268269 });
@@ -271,21 +272,21 @@ describe("macOS component snapshots", () => {
271272
272273 it("DatePicker", () => {
273274 const { toJSON } = render(
274− el(_datePickerImpl, { date: "2025-01-15", onChange: noop }),
275+ el(datePickerImpl, { date: "2025-01-15", onChange: noop }),
275276 );
276277 expect(toJSON()).toMatchSnapshot();
277278 });
278279
279280 it("ColorWell", () => {
280281 const { toJSON } = render(
281− el(_colorWellImpl, { color: "#ff0000", onChange: noop }),
282+ el(colorWellImpl, { color: "#ff0000", onChange: noop }),
282283 );
283284 expect(toJSON()).toMatchSnapshot();
284285 });
285286
286287 it("PathControl", () => {
287288 const { toJSON } = render(
288− el(_pathControlImpl, {
289+ el(pathControlImpl, {
289290 url: "/Users/test/file.txt",
290291 pathStyle: "standard",
291292 }),
@@ -297,19 +298,19 @@ describe("macOS component snapshots", () => {
297298
298299 it("Image", () => {
299300 const { toJSON } = render(
300− el(_imageImpl, { source: "test.png", resizeMode: "cover" }),
301+ el(imageImpl, { source: "test.png", resizeMode: "cover" }),
301302 );
302303 expect(toJSON()).toMatchSnapshot();
303304 });
304305
305306 it("AnimatedImage", () => {
306− const { toJSON } = render(el(_animatedImageImpl, { source: "anim.gif" }));
307+ const { toJSON } = render(el(animatedImageImpl, { source: "anim.gif" }));
307308 expect(toJSON()).toMatchSnapshot();
308309 });
309310
310311 it("VideoPlayer", () => {
311312 const { toJSON } = render(
312− el(_videoPlayerImpl, { source: "video.mp4", autoPlay: false }),
313+ el(videoPlayerImpl, { source: "video.mp4", autoPlay: false }),
313314 );
314315 expect(toJSON()).toMatchSnapshot();
315316 });
@@ -317,12 +318,12 @@ describe("macOS component snapshots", () => {
317318 // -- Layout --
318319
319320 it("Separator horizontal", () => {
320− const { toJSON } = render(el(_separatorImpl, { vertical: false }));
321+ const { toJSON } = render(el(separatorImpl, { vertical: false }));
321322 expect(toJSON()).toMatchSnapshot();
322323 });
323324
324325 it("Separator vertical", () => {
325− const { toJSON } = render(el(_separatorImpl, { vertical: true }));
326+ const { toJSON } = render(el(separatorImpl, { vertical: true }));
326327 expect(toJSON()).toMatchSnapshot();
327328 });
328329
@@ -330,7 +331,7 @@ describe("macOS component snapshots", () => {
330331
331332 it("TableView", () => {
332333 const { toJSON } = render(
333− el(_tableViewImpl, {
334+ el(tableViewImpl, {
334335 columns: [
335336 { title: "Name", width: 100 },
336337 { title: "Age", width: 50 },
@@ -348,7 +349,7 @@ describe("macOS component snapshots", () => {
348349
349350 it("OutlineView", () => {
350351 const { toJSON } = render(
351− el(_outlineViewImpl, {
352+ el(outlineViewImpl, {
352353 items: [
353354 {
354355 id: "1",
@@ -368,7 +369,7 @@ describe("macOS component snapshots", () => {
368369
369370 it("MapView", () => {
370371 const { toJSON } = render(
371− el(_mapViewImpl, {
372+ el(mapViewImpl, {
372373 latitude: 37.7749,
373374 longitude: -122.4194,
374375 latitudeDelta: 0.05,
@@ -385,7 +386,7 @@ describe("macOS component snapshots", () => {
385386
386387 it("PDFView", () => {
387388 const { toJSON } = render(
388− el(_pdfViewImpl, {
389+ el(pdfViewImpl, {
389390 source: "/tmp/test.pdf",
390391 autoScales: true,
391392 displayMode: "singlePageContinuous",
@@ -399,7 +400,7 @@ describe("macOS component snapshots", () => {
399400
400401 it("Sheet with child", () => {
401402 const { toJSON } = render(
402− elK(_sheetImpl, { visible: true, onDismiss: noop }, [
403+ elK(sheetImpl, { visible: true, onDismiss: noop }, [
403404 el("View", { key: "1" }),
404405 ]),
405406 );
@@ -408,28 +409,28 @@ describe("macOS component snapshots", () => {
408409
409410 it("Box with child", () => {
410411 const { toJSON } = render(
411− elK(_boxImpl, { title: "Group" }, [el("View", { key: "1" })]),
412+ elK(boxImpl, { title: "Group" }, [el("View", { key: "1" })]),
412413 );
413414 expect(toJSON()).toMatchSnapshot();
414415 });
415416
416417 it("Popover with child", () => {
417418 const { toJSON } = render(
418− elK(_popoverImpl, { visible: true }, [el("View", { key: "1" })]),
419+ elK(popoverImpl, { visible: true }, [el("View", { key: "1" })]),
419420 );
420421 expect(toJSON()).toMatchSnapshot();
421422 });
422423
423424 it("ScrollView with child", () => {
424425 const { toJSON } = render(
425− elK(_nativeScrollViewImpl, {}, [el("View", { key: "1" })]),
426+ elK(nativeScrollViewImpl, {}, [el("View", { key: "1" })]),
426427 );
427428 expect(toJSON()).toMatchSnapshot();
428429 });
429430
430431 it("VisualEffect with child", () => {
431432 const { toJSON } = render(
432− elK(_visualEffectImpl, { material: "sidebar" }, [
433+ elK(visualEffectImpl, { material: "sidebar" }, [
433434 el("View", { key: "1" }),
434435 ]),
435436 );
@@ -438,7 +439,7 @@ describe("macOS component snapshots", () => {
438439
439440 it("ContextMenu with child", () => {
440441 const { toJSON } = render(
441− elK(_contextMenuImpl, { menuItems: [{ title: "Copy" }] }, [
442+ elK(contextMenuImpl, { menuItems: [{ title: "Copy" }] }, [
442443 el("View", { key: "1" }),
443444 ]),
444445 );
@@ -447,7 +448,7 @@ describe("macOS component snapshots", () => {
447448
448449 it("SplitView with children", () => {
449450 const { toJSON } = render(
450− elK(_splitViewImpl, { vertical: false }, [
451+ elK(splitViewImpl, { vertical: false }, [
451452 el("View", { key: "1" }),
452453 el("View", { key: "2" }),
453454 ]),
@@ -457,17 +458,22 @@ describe("macOS component snapshots", () => {
457458
458459 it("TabView with children", () => {
459460 const { toJSON } = render(
460− elK(_tabViewImpl, { selectedIndex: 0 }, [el("View", { key: "1" })]),
461+ elK(tabViewImpl, { selectedIndex: 0 }, [el("View", { key: "1" })]),
461462 );
462463 expect(toJSON()).toMatchSnapshot();
463464 });
464465
465466 it("PatternBackground with child", () => {
466467 const { toJSON } = render(
467− elK(_patternBackgroundImpl, { pattern: "dots" }, [
468+ elK(patternBackgroundImpl, { pattern: "dots" }, [
468469 el("View", { key: "1" }),
469470 ]),
470471 );
471472 expect(toJSON()).toMatchSnapshot();
472473 });
474+
475+ it("CameraView", () => {
476+ const { toJSON } = render(el(cameraViewImpl, { active: true }));
477+ expect(toJSON()).toMatchSnapshot();
478+ });
473479});
test/__snapshots__/MacOSSnapshots.test.js.snap+9
@@ -33,6 +33,15 @@ exports[`macOS component snapshots > Button 1`] = `
3333}
3434`;
3535
36+exports[`macOS component snapshots > CameraView 1`] = `
37+{
38+ "props": {
39+ "active": true,
40+ },
41+ "type": "MacOSCameraView",
42+}
43+`;
44+
3645exports[`macOS component snapshots > Checkbox checked 1`] = `
3746{
3847 "props": {