├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── assets └── solid.png ├── deno.json ├── deno.lock ├── icon ├── icon-1024.png ├── icon-128.png ├── icon-16.png ├── icon-256.png ├── icon-32.png ├── icon-512.png └── icon-64.png ├── import_map.json ├── jsx └── index.d.ts ├── native ├── core │ ├── application.ts │ ├── dialogs │ │ ├── color │ │ │ └── color-dialog.ts │ │ └── file │ │ │ └── file-dialog.ts │ ├── dom │ │ ├── dom-utils.ts │ │ ├── environment.ts │ │ ├── index.d.ts │ │ └── index.ts │ ├── index.ts │ ├── layout │ │ └── index.ts │ ├── style │ │ ├── index.ts │ │ ├── properties.ts │ │ └── utils │ │ │ ├── color.ts │ │ │ └── parse.ts │ └── views │ │ ├── button │ │ ├── button.ts │ │ └── native-button.ts │ │ ├── checkbox │ │ └── checkbox.ts │ │ ├── coloropenbutton │ │ ├── coloropenbutton.ts │ │ └── native-coloropenbutton.ts │ │ ├── combobox │ │ ├── combobox.ts │ │ └── native-combobox.ts │ │ ├── common │ │ └── native-target.ts │ │ ├── date-picker │ │ └── date-picker.ts │ │ ├── decorators │ │ ├── native.ts │ │ ├── overrides.ts │ │ ├── property.ts │ │ └── view.ts │ │ ├── fileopenbutton │ │ ├── fileopenbutton.ts │ │ └── native-fileopenbutton.ts │ │ ├── filesavebutton │ │ ├── filesavebutton.ts │ │ └── native-filesavebutton.ts │ │ ├── image │ │ └── image.ts │ │ ├── menu │ │ ├── menu-item.ts │ │ ├── menu-section-header.ts │ │ ├── menu-separator.ts │ │ └── menu.ts │ │ ├── outline │ │ └── outline.ts │ │ ├── popover │ │ └── popover.ts │ │ ├── progress │ │ └── progress.ts │ │ ├── radiobutton │ │ └── radiobutton.ts │ │ ├── scroll-view │ │ └── scroll-view.ts │ │ ├── slider │ │ └── slider.ts │ │ ├── split-view │ │ ├── content-list.ts │ │ ├── sidebar.ts │ │ ├── split-view-controller.ts │ │ ├── split-view-item.ts │ │ └── split-view.ts │ │ ├── status-bar │ │ └── status-bar.ts │ │ ├── switch │ │ └── switch.ts │ │ ├── table │ │ └── table-cell.ts │ │ ├── text-field │ │ └── text-field.ts │ │ ├── text-view │ │ └── text-view.ts │ │ ├── text │ │ ├── text-base.ts │ │ └── text.ts │ │ ├── toolbar │ │ ├── toolbar-flexible-space.ts │ │ ├── toolbar-group.ts │ │ ├── toolbar-item.ts │ │ ├── toolbar-sidebar-tracking-separator.ts │ │ ├── toolbar-space.ts │ │ ├── toolbar-toggle-sidebar.ts │ │ └── toolbar.ts │ │ ├── view │ │ ├── native-view.ts │ │ ├── view-base.ts │ │ └── view.ts │ │ ├── webview │ │ └── webview.ts │ │ └── window │ │ ├── native-window.ts │ │ └── window.ts └── index.ts ├── package.json ├── scripts └── bundle-solidjs.ts ├── snippets ├── .bolt │ └── config.json ├── .gitignore ├── README.md ├── dist │ ├── assets │ │ ├── index-IZm_L_TO.js │ │ └── index-eucFxVK-.css │ ├── index.html │ └── vite.svg ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ └── vite.svg ├── src │ ├── App.tsx │ ├── assets │ │ └── solid.svg │ ├── components │ │ ├── CodeSnippet.tsx │ │ ├── CodeSnippetLight.tsx │ │ ├── prisim-atom-dark.css │ │ └── prisim-one-light.css │ ├── index.css │ ├── index.tsx │ └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.app.json ├── tsconfig.app.tsbuildinfo ├── tsconfig.json ├── tsconfig.node.json ├── tsconfig.node.tsbuildinfo └── vite.config.ts ├── solid-native └── renderer.js ├── src ├── app-menus.tsx ├── app.tsx ├── components │ ├── Switch.tsx │ ├── button.tsx │ ├── checkbox.tsx │ ├── color-picker.tsx │ ├── combobox.tsx │ ├── date-picker.tsx │ ├── file-dialog.tsx │ ├── image.tsx │ ├── modal.tsx │ ├── popover.tsx │ ├── progress.tsx │ ├── radio-button.tsx │ ├── save-file.tsx │ ├── slider.tsx │ ├── text-field.tsx │ ├── text-view.tsx │ ├── text.tsx │ ├── webview.tsx │ └── window.tsx ├── contentview.tsx ├── examples │ ├── counter.tsx │ ├── index.tsx │ └── todo.tsx ├── hooks │ └── use-color-scheme.ts ├── pages │ ├── common.ts │ ├── components.tsx │ ├── getting-started.tsx │ ├── overview.tsx │ └── setup.tsx ├── sidebar.tsx ├── state.tsx ├── toolbar.tsx ├── utils │ └── colors.ts └── webdisplay.tsx └── vendor └── undom-ng ├── LICENSE ├── README.md ├── package.json └── src ├── namespaces.js ├── serializer.js ├── undom-ng.js ├── undom.js └── utils.js /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Setup repo 14 | uses: actions/checkout@v2 15 | 16 | - name: Setup Deno 17 | uses: denoland/setup-deno@main 18 | with: 19 | deno-version: 'v2.x' 20 | 21 | - name: Check Formatting 22 | run: deno fmt --check 23 | 24 | - name: Lint 25 | run: deno lint 26 | 27 | test-build: 28 | runs-on: macos-latest 29 | steps: 30 | - name: Setup repo 31 | uses: actions/checkout@v2 32 | 33 | - name: Setup Deno 34 | uses: denoland/setup-deno@main 35 | with: 36 | deno-version: 'v2.x' 37 | 38 | - name: Build 39 | run: deno task bundle 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .vscode 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Solid for macOS 3 |

4 | 5 |

Solid for macOS

6 | 7 | Solid for macOS empowers you to build truly native desktop apps, leveraging 8 | native [AppKit](https://developer.apple.com/documentation/appkit) components for 9 | a seamless and performant user experience. Unlike purely webview based 10 | cross-platform frameworks (_or frameworks that attempt to recreate the entire 11 | platform interface_), Solid for macOS directly integrates with native APIs, 12 | ensuring your apps behave, look and feel right at home on macOS. This 13 | integration allows you to develop fully native apps that utilize _all_ the 14 | nuanced capabilities of the entire platform, providing users with a smooth and 15 | responsive experience. 16 | 17 | The first macOS app built with [Solid](https://www.solidjs.com/) is already 18 | available on the 19 | [Mac App Store here](https://apps.apple.com/us/app/solid-for-macos/id1574916360). 20 | We welcome you to install and give it a try (_compatible with macOS 14+ and 21 | M-series arm64 machines_). 22 | 23 | You can also try out the example app in this repository. To run the example, 24 | clone this repository and run the following commands: 25 | 26 | ```bash 27 | deno task start 28 | ``` 29 | 30 | Yes, **you don't need Xcode installed** to run and develop the app for macOS. 31 | You can start the app directly from the terminal, it's as simple as that. Only 32 | when you are ready to release your app would you need Xcode and an Apple 33 | developer account to publish to the store. 34 | 35 | ## Architecture 36 | 37 | Let's explore the architecture of Solid for macOS. There are a lot of components 38 | working together behind the scenes to make Solid for macOS possible. 39 | 40 | ### Runtime 41 | 42 | The runtime is a critical component that connects macOS APIs to JavaScript. 43 | Written in Objective-C++, it leverages 44 | [Node-API](https://nodejs.org/api/n-api.html#node-api) to facilitate seamless 45 | communication with any JavaScript engine that implements engine-agnostic 46 | Node-API layer. This open-source runtime, available at 47 | [macos-node-api](https://github.com/NativeScript/runtime-node-api), handles the 48 | task of bringing all the native APIs seamlessly to JavaScript land. 49 | 50 | ## DOM 51 | 52 | DOM provides the simplest possible implementation of a 53 | [DOM Document](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model), 54 | just enough to expose the most basic DOM Apis needed by Solid. We are using 55 | [undom-ng](https://github.com/ClassicOldSong/undom-ng) as a simple and 56 | lightweight DOM implementation. 57 | 58 | ## Foundation 59 | 60 | Foundation integrates the concepts of web and native, making the process of 61 | building native macOS apps intuitive and familiar for web developers. By 62 | combining the best ideas from both worlds, it ensures that developers can 63 | leverage their existing web development skills to create high-quality apps from 64 | day one. 65 | 66 | One of foundation's primary responsibilities is to handle various aspects of the 67 | application, including styling, layout, rendering, windowing, menubars, and the 68 | overall application lifecycle. It ensures that all essential elements of a macOS 69 | app are seamlessly managed, providing a cohesive development experience. 70 | 71 | To achieve this, foundation makes each AppKit UI component to be a DOM element 72 | and registers it with the global document object, allowing developers to 73 | interact with native components using familiar web development paradigms. 74 | 75 | For layout management, foundation utilizes the flexbox layout engine provided by 76 | [Meta's Yoga](https://github.com/facebook/yoga), enabling developers to apply 77 | almost all flexbox properties to DOM elements. This approach simplifies the 78 | process of creating complex and responsive layouts. 79 | 80 | The most basic UI element provided by foundation is the view, which internally 81 | creates an [NSView](https://developer.apple.com/documentation/appkit/nsview) 82 | which serves as a building block for more complex components and user 83 | interfaces. Foundation already includes a comprehensive set of basic AppKit 84 | components, and ongoing efforts are being made to expand this library further, 85 | ensuring that developers have access to all AppKit has to offer. 86 | 87 | ## Solid Renderer 88 | 89 | A custom renderer, implemented at [renderer](./solid-native/renderer.js), is 90 | used to transform JSX into our DOM implementation. This renderer seamlessly 91 | integrates Solid's reactive capabilities with native macOS components. 92 | 93 | ## Deployment 94 | 95 | If you have noticed, our 96 | [Solid Desktop app on Mac App Store](https://apps.apple.com/us/app/solid-for-macos/id1574916360) 97 | is just 5.5 MB in size. That is possible with 98 | [Hermes](https://github.com/facebook/hermes), a JavaScript engine developed by 99 | Meta. Credit to [Tzvetan Mikov](https://github.com/tmikov) for his continued 100 | excellence on the engine. Hermes provides optimal performance while having 101 | minimal footprint. 102 | 103 | https://github.com/user-attachments/assets/ad087d8c-f303-485a-bbe3-889430286bd9 104 | 105 | ## Contributors 106 | 107 | - [Ammar Ahmed](https://github.com/ammarahm-ed) 108 | - [Diljit Singh](https://github.com/DjDeveloperr) 109 | - [Nathan Walker](https://github.com/NathanWalker) 110 | 111 | ## License 112 | 113 | Apache-2.0 114 | -------------------------------------------------------------------------------- /assets/solid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ammarahm-ed/nativescript-macos-solid/e86e63aee31f05cdcf3dea5b0a742f5ac3df436e/assets/solid.png -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "start": "deno task bundle && deno task run-native", 4 | "bundle": "deno run -A ./scripts/bundle-solidjs.ts \"src/app.tsx\" \"dist/out.js\"", 5 | "run-native": "deno run -A --unstable-sloppy-imports ./native/index.ts" 6 | }, 7 | "license": "Apache-2.0", 8 | "importMap": "./import_map.json", 9 | "lint": { 10 | "include": [ 11 | "jsx", 12 | "native", 13 | "scripts", 14 | "src" 15 | ], 16 | "rules": { 17 | "exclude": [ 18 | "no-explicit-any", 19 | "no-var", 20 | "no-empty-interface", 21 | "ban-types" 22 | ] 23 | } 24 | }, 25 | "fmt": { 26 | "include": [ 27 | "jsx", 28 | "native", 29 | "scripts", 30 | "src", 31 | "deno.json", 32 | "import_map.json", 33 | "README.md" 34 | ] 35 | }, 36 | "compilerOptions": { 37 | "jsxImportSource": "@jsx", 38 | "jsx": "react-jsx", 39 | "experimentalDecorators": true, 40 | "emitDecoratorMetadata": true, 41 | "noImplicitOverride": false 42 | }, 43 | "nodeModulesDir": "auto" 44 | } 45 | -------------------------------------------------------------------------------- /icon/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ammarahm-ed/nativescript-macos-solid/e86e63aee31f05cdcf3dea5b0a742f5ac3df436e/icon/icon-1024.png -------------------------------------------------------------------------------- /icon/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ammarahm-ed/nativescript-macos-solid/e86e63aee31f05cdcf3dea5b0a742f5ac3df436e/icon/icon-128.png -------------------------------------------------------------------------------- /icon/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ammarahm-ed/nativescript-macos-solid/e86e63aee31f05cdcf3dea5b0a742f5ac3df436e/icon/icon-16.png -------------------------------------------------------------------------------- /icon/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ammarahm-ed/nativescript-macos-solid/e86e63aee31f05cdcf3dea5b0a742f5ac3df436e/icon/icon-256.png -------------------------------------------------------------------------------- /icon/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ammarahm-ed/nativescript-macos-solid/e86e63aee31f05cdcf3dea5b0a742f5ac3df436e/icon/icon-32.png -------------------------------------------------------------------------------- /icon/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ammarahm-ed/nativescript-macos-solid/e86e63aee31f05cdcf3dea5b0a742f5ac3df436e/icon/icon-512.png -------------------------------------------------------------------------------- /icon/icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ammarahm-ed/nativescript-macos-solid/e86e63aee31f05cdcf3dea5b0a742f5ac3df436e/icon/icon-64.png -------------------------------------------------------------------------------- /import_map.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "@jsx/jsx-runtime": "./jsx/index.d.ts", 4 | "undom-ng": "./vendor/undom-ng/src/undom-ng.js", 5 | "dom": "./native/core/dom/index.ts", 6 | "dom-types": "./native/core/dom/index.d.ts", 7 | "app": "./dist/out.js", 8 | "@nativescript/macos-node-api": "npm:@nativescript/macos-node-api@~0.1.3", 9 | "solid-native-renderer": "./solid-native/renderer.js" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /native/core/application.ts: -------------------------------------------------------------------------------- 1 | import "@nativescript/macos-node-api"; 2 | import type { NativeWindow } from "./views/window/native-window.ts"; 3 | import type { Window } from "./views/window/window.ts"; 4 | objc.import("AppKit"); 5 | 6 | @NativeClass 7 | class AppDelegate extends NSObject implements NSApplicationDelegate { 8 | window?: NativeWindow; 9 | running = true; 10 | isActive = true; 11 | static windowTitle: string; 12 | static ObjCProtocols = [NSApplicationDelegate]; 13 | static ObjCExposedMethods = { 14 | showMainWindow: { returns: interop.types.void, params: [interop.types.id] }, 15 | themeChanged: { returns: interop.types.void, params: [interop.types.id] }, 16 | }; 17 | 18 | applicationDidFinishLaunching(_notification: NSNotification) { 19 | NSApp.activateIgnoringOtherApps(false); 20 | NSApp.stop(this); 21 | // Allow users to customize the app's Touch Bar items 22 | NSApplication.sharedApplication 23 | .isAutomaticCustomizeTouchBarMenuItemEnabled = true; 24 | NSDistributedNotificationCenter.defaultCenter.addObserverSelectorNameObject( 25 | this, 26 | "themeChanged", 27 | "AppleInterfaceThemeChangedNotification", 28 | null, 29 | ); 30 | RunLoop(); 31 | } 32 | 33 | applicationShouldHandleReopenHasVisibleWindows( 34 | sender: NSApplication, 35 | hasVisibleWindows: boolean, 36 | ): boolean { 37 | if (!hasVisibleWindows) { 38 | (sender.windows.firstObject as NSWindow).makeKeyAndOrderFront(sender); 39 | } 40 | return true; 41 | } 42 | 43 | applicationWillTerminate(_notification: NSNotification): void { 44 | this.running = false; 45 | } 46 | 47 | showMainWindow(_id: this) { 48 | NativeScriptApplication.showMainWindow(); 49 | } 50 | 51 | themeChanged(_id: this) { 52 | console.log( 53 | "themeChanged", 54 | NSApp.effectiveAppearance.name === "NSAppearanceNameDarkAqua" 55 | ? "dark" 56 | : "light", 57 | ); 58 | } 59 | } 60 | 61 | function RunLoop() { 62 | let delay = 2; 63 | let lastEventTime = 0; 64 | 65 | const loop = () => { 66 | const event = NSApp.nextEventMatchingMaskUntilDateInModeDequeue( 67 | NSEventMask.Any, 68 | null, 69 | "kCFRunLoopDefaultMode", 70 | true, 71 | ); 72 | 73 | const timeSinceLastEvent = Date.now() - lastEventTime; 74 | if (event != null) { 75 | NSApp.sendEvent(event); 76 | delay = timeSinceLastEvent < 32 ? 2 : 8; 77 | lastEventTime = Date.now(); 78 | } else { 79 | delay = timeSinceLastEvent > 6000 80 | ? 128 81 | : timeSinceLastEvent > 4000 82 | ? 64 83 | : timeSinceLastEvent > 2000 84 | ? 16 85 | : 8; 86 | } 87 | 88 | if (NativeScriptApplication.delegate.running) { 89 | setTimeout(loop, NativeScriptApplication.ensure60FPS ? 8 : delay); 90 | } 91 | }; 92 | setTimeout(loop, 0); 93 | } 94 | 95 | export class Application { 96 | static delegate: AppDelegate; 97 | static application: NSApplication; 98 | static rootView: HTMLViewElement; 99 | static window: Window; 100 | static appMenu: NSMenu; 101 | static ensure60FPS: boolean; 102 | static initEditMenu: boolean; 103 | 104 | static launch() { 105 | if (!(document.body instanceof HTMLElement)) { 106 | throw new Error("document.body instance of NSView"); 107 | } 108 | Application.rootView = document.body as unknown as HTMLViewElement; 109 | Application.rootView?.connectedCallback(); 110 | 111 | if (NativeScriptApplication.window) { 112 | NativeScriptApplication.window.open(); 113 | } else { 114 | throw new Error("Window is not initialized"); 115 | } 116 | 117 | Application.application = NSApplication.sharedApplication; 118 | Application.delegate = AppDelegate.new(); 119 | Application.delegate.window = NativeScriptApplication.window.nativeView; 120 | Application.createMenu(); 121 | NSApp.delegate = Application.delegate; 122 | NSApp.setActivationPolicy(NSApplicationActivationPolicy.Regular); 123 | NSApp.run(); 124 | } 125 | 126 | static createMenu() { 127 | if (!Application.appMenu) { 128 | const menu = NSMenu.new(); 129 | NSApp.mainMenu = menu; 130 | Application.appMenu = menu; 131 | } 132 | } 133 | static showMainWindow() { 134 | // override 135 | } 136 | } 137 | 138 | declare global { 139 | var NativeScriptApplication: typeof Application; 140 | } 141 | globalThis.NativeScriptApplication = Application; 142 | -------------------------------------------------------------------------------- /native/core/dialogs/color/color-dialog.ts: -------------------------------------------------------------------------------- 1 | import "@nativescript/macos-node-api"; 2 | 3 | type ChangeCallback = ((color: string) => void | undefined) | undefined; 4 | export interface ColorDialogOptions { 5 | color?: string; 6 | change?: ChangeCallback; 7 | } 8 | let colorDialog: NSColorPanel; 9 | let colorTarget: NativeColorTarget; 10 | 11 | @NativeClass 12 | class NativeColorTarget extends NSObject { 13 | static ObjCExposedMethods = { 14 | changeColor: { returns: interop.types.void, params: [interop.types.id] }, 15 | }; 16 | 17 | declare _resolve: Function; 18 | declare _changeCallback: ChangeCallback; 19 | static initWithResolve(resolve: Function, changeCallback: ChangeCallback) { 20 | const delegate = NativeColorTarget.new(); 21 | delegate._resolve = resolve; 22 | delegate._changeCallback = changeCallback; 23 | return delegate; 24 | } 25 | 26 | changeColor(_id: this) { 27 | if (this._changeCallback) { 28 | this._changeCallback(nsColorToHex(colorDialog.color)); 29 | } else if (this._resolve) { 30 | this._resolve(nsColorToHex(colorDialog.color)); 31 | } 32 | } 33 | } 34 | 35 | export function openColorDialog(options: ColorDialogOptions) { 36 | return new Promise((resolve) => { 37 | colorDialog = NSColorPanel.new(); 38 | colorDialog.setIsVisible(true); 39 | colorDialog.isContinuous = true; 40 | if (options.color) { 41 | colorDialog.color = hexToNSColor(options.color); 42 | } 43 | colorTarget = NativeColorTarget.initWithResolve(resolve, options.change); 44 | colorDialog.setTarget(colorTarget); 45 | colorDialog.setAction("changeColor"); 46 | }); 47 | } 48 | 49 | export function hexToNSColor(hex: string) { 50 | // Ensure that the hex string is in the format "#RRGGBB" or "RRGGBB" 51 | hex = hex.replace(/^#/, ""); 52 | 53 | // Parse the red, green, and blue components 54 | var red = parseInt(hex.substring(0, 2), 16) / 255.0; 55 | var green = parseInt(hex.substring(2, 4), 16) / 255.0; 56 | var blue = parseInt(hex.substring(4, 6), 16) / 255.0; 57 | 58 | // Create and return an NSColor object 59 | return NSColor.colorWithSRGBRedGreenBlueAlpha(red, green, blue, 1.0); // Alpha is set to 1.0 for full opacity 60 | } 61 | 62 | export function nsColorToHex(color: NSColor) { 63 | // Get the color components 64 | var red = color.redComponent; 65 | var green = color.greenComponent; 66 | var blue = color.blueComponent; 67 | 68 | // Convert to 255 scale and then to hex 69 | var r = Math.round(red * 255).toString(16).padStart(2, "0"); 70 | var g = Math.round(green * 255).toString(16).padStart(2, "0"); 71 | var b = Math.round(blue * 255).toString(16).padStart(2, "0"); 72 | 73 | // Return the hex color code 74 | return `#${r}${g}${b}`; 75 | } 76 | -------------------------------------------------------------------------------- /native/core/dialogs/file/file-dialog.ts: -------------------------------------------------------------------------------- 1 | export interface FileDialogOptions { 2 | chooseFiles?: boolean; 3 | chooseDirectories?: boolean; 4 | multiple?: boolean; 5 | fileTypes?: Array; 6 | directoryUrl?: NSURL; 7 | } 8 | let fileDialog: NSOpenPanel; 9 | export interface SaveFileDialogOptions { 10 | createDirectories?: boolean; 11 | filename?: string; 12 | fileTypes?: Array; 13 | directoryUrl?: NSURL; 14 | } 15 | let saveDialog: NSSavePanel; 16 | 17 | export function openFileDialog(options: FileDialogOptions) { 18 | return new Promise>((resolve, reject) => { 19 | fileDialog = NSOpenPanel.new(); 20 | fileDialog.canChooseFiles = options?.chooseFiles || false; 21 | fileDialog.canChooseDirectories = options?.chooseDirectories || false; 22 | fileDialog.allowsMultipleSelection = options?.multiple || false; 23 | fileDialog.allowedFileTypes = options?.fileTypes || ["*"]; 24 | if (options?.directoryUrl) { 25 | fileDialog.directoryURL = options.directoryUrl; 26 | } else { 27 | fileDialog.directoryURL = NSURL.fileURLWithPath( 28 | NSSearchPathForDirectoriesInDomains( 29 | NSSearchPathDirectory.Desktop, 30 | NSSearchPathDomainMask.UserDomain, 31 | true, 32 | ).firstObject, 33 | ); 34 | } 35 | 36 | const response = fileDialog.runModal(); 37 | if (response === NSModalResponseOK) { 38 | const urls: Array = []; 39 | for (let i = 0; i < fileDialog.URLs.count; i++) { 40 | const url = fileDialog.URLs.objectAtIndex(i) as NSURL; 41 | urls.push(url.absoluteString); 42 | } 43 | resolve(urls); 44 | } else { 45 | reject([]); 46 | } 47 | }); 48 | } 49 | 50 | export function saveFileDialog(options: SaveFileDialogOptions) { 51 | return new Promise((resolve, reject) => { 52 | saveDialog = NSSavePanel.new(); 53 | saveDialog.canCreateDirectories = options?.createDirectories || false; 54 | saveDialog.nameFieldStringValue = options?.filename || ""; 55 | saveDialog.allowedFileTypes = options?.fileTypes || ["*"]; 56 | if (options?.directoryUrl) { 57 | saveDialog.directoryURL = options.directoryUrl; 58 | } else { 59 | saveDialog.directoryURL = NSURL.fileURLWithPath( 60 | NSSearchPathForDirectoriesInDomains( 61 | NSSearchPathDirectory.Desktop, 62 | NSSearchPathDomainMask.UserDomain, 63 | true, 64 | ).firstObject, 65 | ); 66 | } 67 | 68 | const response = saveDialog.runModal(); 69 | if (response === NSModalResponseOK) { 70 | resolve(saveDialog.URL.absoluteString); 71 | } else { 72 | reject(); 73 | } 74 | }); 75 | } 76 | -------------------------------------------------------------------------------- /native/core/dom/dom-utils.ts: -------------------------------------------------------------------------------- 1 | export { createEvent, getAttributeNS, setAttributeNS } from "undom-ng"; 2 | export { Event }; 3 | import { Event as UndomEvent } from "undom-ng"; 4 | 5 | const Event = UndomEvent as unknown as typeof globalThis.Event; 6 | -------------------------------------------------------------------------------- /native/core/dom/environment.ts: -------------------------------------------------------------------------------- 1 | import { createEnvironment } from "undom-ng"; 2 | const initDocument = (document: any) => { 3 | document.body = document.createElement("view"); 4 | }; 5 | 6 | const { 7 | scope, 8 | createDocument, 9 | createElement, 10 | registerElement: registerElement, 11 | } = createEnvironment({ 12 | silent: true, 13 | initDocument: initDocument, 14 | onSetTextContent(_text: string) { 15 | this.updateTextContent?.(); 16 | }, 17 | onSetData(_data: any) { 18 | if (this.parentNode) { 19 | let parentElement = this.parentNode; 20 | while (parentElement.nodeType !== 1) { 21 | parentElement = parentElement.parentNode; 22 | } 23 | (parentElement as any).updateTextContent?.(); 24 | } 25 | }, 26 | } as any) as unknown as { 27 | scope: typeof Scope; 28 | createDocument: (namespace: string, documentElementTag: string) => Document; 29 | createElement: (tagName: string) => Node; 30 | registerElement: (tagName: string, element: Node) => void; 31 | }; 32 | 33 | globalThis.registerElement = registerElement; 34 | globalThis.createElement = createElement; 35 | globalThis.Scope = scope; 36 | globalThis.HTMLElement = scope.HTMLElement; 37 | globalThis.Node = scope.Node; 38 | globalThis.EventTarget = scope.EventTarget; 39 | globalThis.Element = scope.Element; 40 | globalThis.Text = scope.Text; 41 | 42 | const window = { 43 | Scope: scope, 44 | registerElement: registerElement, 45 | createElement: createElement, 46 | } as any; 47 | 48 | globalThis.window = window; 49 | 50 | const registerGlobalDocument = (_global = globalThis) => { 51 | const document = createDocument("http://www.w3.org/1999/xhtml", "#view"); 52 | window.document = document; 53 | globalThis.document = document; 54 | }; 55 | 56 | export { 57 | createDocument, 58 | createElement, 59 | registerElement, 60 | registerGlobalDocument, 61 | scope, 62 | }; 63 | -------------------------------------------------------------------------------- /native/core/dom/index.d.ts: -------------------------------------------------------------------------------- 1 | import { ViewBase } from "../views/view/view-base.ts"; 2 | import { View } from "../views/view/view.ts"; 3 | import type { Slider } from "../views/slider/slider.ts"; 4 | import type { Progress } from "../views/progress/progress.ts"; 5 | import type { TextField } from "../views/text-field/text-field.ts"; 6 | import type { Checkbox } from "../views/checkbox/checkbox.ts"; 7 | import type { Window } from "../views/window/window.ts"; 8 | import type { Popover } from "../views/popover/popover.ts"; 9 | import type { Switch } from "../views/switch/switch.ts"; 10 | import type { DatePicker } from "../views/date-picker/date-picker.ts"; 11 | import type { WebView } from "../views/webview/webview.ts"; 12 | import type { Menu } from "../views/menu/menu.ts"; 13 | 14 | declare global { 15 | // undom 16 | var Scope: { 17 | Node: typeof Node; 18 | Element: typeof Element; 19 | Text: typeof Text; 20 | Comment: typeof Comment; 21 | Document: typeof Document; 22 | DocumentFragment: typeof DocumentFragment; 23 | HTMLElement: typeof HTMLElement; 24 | EventTarget: typeof EventTarget; 25 | CharacterData: typeof CharacterData; 26 | }; 27 | 28 | // Defining views as elements is done as following 29 | interface HTMLViewBaseElement extends ViewBase {} 30 | var HTMLViewBaseElement: { 31 | new (): HTMLViewBaseElement; 32 | prototype: HTMLViewBaseElement; 33 | }; 34 | 35 | interface HTMLViewElement extends View {} 36 | var HTMLViewElement: { 37 | new (): HTMLViewElement; 38 | prototype: HTMLViewBaseElement; 39 | }; 40 | 41 | // Register your view here if needed, this is not required for JSX since JSX Intrinsic elements 42 | // are registered separately in jsx/index.d.ts 43 | interface HTMlButtonElement extends HTMLViewBaseElement {} 44 | 45 | var HTMlButtonElement: { 46 | new (): HTMlButtonElement; 47 | prototype: HTMlButtonElement; 48 | }; 49 | 50 | interface HTMLWindowElement extends Window {} 51 | 52 | interface HTMLWebViewElement extends WebView {} 53 | 54 | var HTMLWindowElement: { 55 | new (): HTMLWindowElement; 56 | prototype: HTMLWindowElement; 57 | }; 58 | 59 | interface HTMLScrollViewElement extends View {} 60 | 61 | var HTMLScrollViewElement: { 62 | new (): HTMLScrollViewElement; 63 | prototype: HTMLViewBaseElement; 64 | }; 65 | 66 | interface HTMLImageElement extends View {} 67 | 68 | var HTMLImageElement: { 69 | new (): HTMLImageElement; 70 | prototype: HTMLImageElement; 71 | }; 72 | 73 | interface HTMLTableCellElement extends View {} 74 | 75 | var HTMLTableCellElement: { 76 | new (): HTMLTableCellElement; 77 | prototype: HTMLTableCellElement; 78 | }; 79 | 80 | interface HTMLOutlineElement extends View {} 81 | 82 | var HTMLOutlineElement: { 83 | new (): HTMLOutlineElement; 84 | prototype: HTMLOutlineElement; 85 | }; 86 | 87 | interface HTMlTextElement extends Text {} 88 | 89 | var HTMlTextElement: { 90 | new (): HTMlTextElement; 91 | prototype: HTMlTextElement; 92 | }; 93 | 94 | interface HTMLSliderElement extends Slider {} 95 | var HTMLSliderElement: { 96 | new (): HTMLSliderElement; 97 | prototype: HTMLSliderElement; 98 | }; 99 | 100 | interface HTMLProgressElement extends Progress {} 101 | var HTMLProgressElement: { 102 | new (): HTMLProgressElement; 103 | prototype: HTMLProgressElement; 104 | }; 105 | 106 | interface HTMLTextFieldElement extends TextField {} 107 | var HTMLTextFieldElement: { 108 | new (): HTMLTextFieldElement; 109 | prototype: HTMLTextFieldElement; 110 | }; 111 | 112 | interface HTMLTextViewElement extends TextField {} 113 | var HTMLTextViewElement: { 114 | new (): HTMLTextViewElement; 115 | prototype: HTMLTextViewElement; 116 | }; 117 | 118 | interface HTMLCheckboxElement extends Checkbox {} 119 | var HTMLCheckboxElement: { 120 | new (): HTMLCheckboxElement; 121 | prototype: HTMLCheckboxElement; 122 | }; 123 | 124 | interface HTMLPopoverElement extends Popover {} 125 | var HTMLPopoverElement: { 126 | new (): HTMLPopoverElement; 127 | prototype: HTMLPopoverElement; 128 | }; 129 | 130 | interface HTMLSwitchElement extends Switch {} 131 | var HTMLSwitchElement: { 132 | new (): HTMLSwitchElement; 133 | prototype: HTMLSwitchElement; 134 | }; 135 | 136 | interface HTMLDatePickerElement extends DatePicker {} 137 | var HTMLDatePickerElement: { 138 | new (): HTMLDatePickerElement; 139 | prototype: HTMLDatePickerElement; 140 | }; 141 | 142 | interface HTMLNSMenuElement extends Menu {} 143 | var HTMLContextMenuElement: { 144 | new (): HTMLNSMenuElement; 145 | prototype: HTMLNSMenuElement; 146 | }; 147 | 148 | interface HTMLElementTagNameMap { 149 | view: HTMLViewElement; 150 | } 151 | 152 | function registerElement(tagName: string, element: any): void; 153 | function createElement(tagName: string): Node; 154 | 155 | 156 | interface Node { 157 | connectedCallback?(): void; 158 | disconnectedCallback?(): void; 159 | updateTextContent?(): void; 160 | } 161 | } 162 | 163 | export {}; 164 | -------------------------------------------------------------------------------- /native/core/dom/index.ts: -------------------------------------------------------------------------------- 1 | import { Button } from "../views/button/button.ts"; 2 | import { ContentList } from "../views/split-view/content-list.ts"; 3 | import { Image } from "../views/image/image.ts"; 4 | import { Outline } from "../views/outline/outline.ts"; 5 | import { ScrollView } from "../views/scroll-view/scroll-view.ts"; 6 | import { SideBar } from "../views/split-view/sidebar.ts"; 7 | import { Slider } from "../views/slider/slider.ts"; 8 | import SplitView from "../views/split-view/split-view.ts"; 9 | import { TableCell } from "../views/table/table-cell.ts"; 10 | import { Text } from "../views/text/text.ts"; 11 | import { View } from "../views/view/view.ts"; 12 | import { Window } from "../views/window/window.ts"; 13 | import { WebView } from "../views/webview/webview.ts"; 14 | import { registerGlobalDocument } from "./environment.ts"; 15 | import { Checkbox } from "../views/checkbox/checkbox.ts"; 16 | import { ComboBox } from "../views/combobox/combobox.ts"; 17 | import { Progress } from "../views/progress/progress.ts"; 18 | import { Toolbar } from "../views/toolbar/toolbar.ts"; 19 | import { ToolbarItem } from "../views/toolbar/toolbar-item.ts"; 20 | import { ToolbarToggleSidebar } from "../views/toolbar/toolbar-toggle-sidebar.ts"; 21 | import { ToolbarSidebarTrackingSeparator } from "../views/toolbar/toolbar-sidebar-tracking-separator.ts"; 22 | import { ToolbarFlexibleSpace } from "../views/toolbar/toolbar-flexible-space.ts"; 23 | import { ToolbarGroup } from "../views/toolbar/toolbar-group.ts"; 24 | import { RadioButton } from "../views/radiobutton/radiobutton.ts"; 25 | import { FileOpenButton } from "../views/fileopenbutton/fileopenbutton.ts"; 26 | import { ColorOpenButton } from "../views/coloropenbutton/coloropenbutton.ts"; 27 | import { FileSaveButton } from "../views/filesavebutton/filesavebutton.ts"; 28 | import { TextField } from "../views/text-field/text-field.ts"; 29 | import { TextView } from "../views/text-view/text-view.ts"; 30 | import { Menu } from "../views/menu/menu.ts"; 31 | import { MenuItem } from "../views/menu/menu-item.ts"; 32 | import { MenuSeparator } from "../views/menu/menu-separator.ts"; 33 | import { MenuSectionHeader } from "../views/menu/menu-section-header.ts"; 34 | import { StatusBar } from "../views/status-bar/status-bar.ts"; 35 | import { Popover } from "../views/popover/popover.ts"; 36 | import { Switch } from "../views/switch/switch.ts"; 37 | import { DatePicker } from "../views/date-picker/date-picker.ts"; 38 | Button.register(); 39 | Checkbox.register(); 40 | ColorOpenButton.register(); 41 | ComboBox.register(); 42 | ContentList.register(); 43 | FileOpenButton.register(); 44 | FileSaveButton.register(); 45 | Image.register(); 46 | Outline.register(); 47 | Progress.register(); 48 | RadioButton.register(); 49 | ScrollView.register(); 50 | SplitView.register(); 51 | SideBar.register(); 52 | Slider.register(); 53 | TableCell.register(); 54 | Text.register(); 55 | Toolbar.register(); 56 | ToolbarItem.register(); 57 | ToolbarToggleSidebar.register(); 58 | ToolbarSidebarTrackingSeparator.register(); 59 | ToolbarFlexibleSpace.register(); 60 | ToolbarGroup.register(); 61 | WebView.register(); 62 | Window.register(); 63 | View.register(); 64 | TextField.register(); 65 | TextView.register(); 66 | Menu.register(); 67 | MenuItem.register(); 68 | MenuSeparator.register(); 69 | MenuSectionHeader.register(); 70 | StatusBar.register(); 71 | Popover.register(); 72 | Switch.register(); 73 | DatePicker.register(); 74 | registerGlobalDocument(); 75 | -------------------------------------------------------------------------------- /native/core/index.ts: -------------------------------------------------------------------------------- 1 | import "@nativescript/macos-node-api"; 2 | import "dom"; 3 | import "dom-types"; 4 | export { Application } from "./application.ts"; 5 | -------------------------------------------------------------------------------- /native/core/style/utils/parse.ts: -------------------------------------------------------------------------------- 1 | export function parsePercent(percentString: string): number { 2 | if (percentString.endsWith("%")) { 3 | const numericString = percentString.slice(0, -1); 4 | const numericValue = parseFloat(numericString); 5 | if (!isNaN(numericValue)) { 6 | return numericValue; 7 | } 8 | } 9 | throw new Error("Invalid percent string"); 10 | } 11 | -------------------------------------------------------------------------------- /native/core/views/button/button.ts: -------------------------------------------------------------------------------- 1 | import { Layout, type YogaNodeLayout } from "../../layout/index.ts"; 2 | import { Color } from "../../style/utils/color.ts"; 3 | import type { NativePropertyConfig } from "../decorators/native.ts"; 4 | import { native } from "../decorators/native.ts"; 5 | import { overrides } from "../decorators/overrides.ts"; 6 | import { view } from "../decorators/view.ts"; 7 | import { TextBase } from "../text/text-base.ts"; 8 | import { NativeButton } from "./native-button.ts"; 9 | 10 | const TitleProperty: NativePropertyConfig = { 11 | setNative: (view: Button, _key, value) => { 12 | if (view.nativeView && !view.firstChild) { 13 | view.nativeView.setTitle(value || ""); 14 | } 15 | }, 16 | shouldLayout: true, 17 | }; 18 | 19 | export type ButtonEvents = "click"; 20 | 21 | @view({ 22 | name: "HTMLButtonElement", 23 | tagName: "button", 24 | }) 25 | export class Button extends TextBase { 26 | 27 | declare nativeView?: NativeButton; 28 | 29 | override get isLeaf(): boolean { 30 | return true; 31 | } 32 | 33 | public override initNativeView(): NativeButton { 34 | this.nativeView = NativeButton.initWithOwner(new WeakRef(this)); 35 | return this.nativeView; 36 | } 37 | 38 | public override prepareNativeView(nativeView: NativeButton): void { 39 | nativeView.target = this.nativeView; 40 | nativeView.action = "clicked"; 41 | } 42 | 43 | override updateTextContent() { 44 | if (this.nativeView && this.firstChild) { 45 | this.nativeView.setTitle(this.textContent || ""); 46 | Layout.computeAndLayout(this); 47 | } 48 | } 49 | 50 | @native(TitleProperty) 51 | declare title: string; 52 | 53 | @overrides("color") 54 | setColor(key: string, value: string, config: NativePropertyConfig) { 55 | const nativeValue = config.converter?.toNative?.(key, value) as 56 | | NSColor 57 | | undefined; 58 | if (nativeValue && this.nativeView) { 59 | this.nativeView.setTitleColor(nativeValue); 60 | } 61 | } 62 | 63 | @overrides("backgroundColor") 64 | setBackgroundColor( 65 | _key: string, 66 | value: string, 67 | _config: NativePropertyConfig, 68 | ) { 69 | if (this.nativeView) { 70 | const nativeValue = !value ? undefined : new Color(value).toNSColor(); 71 | this.nativeView.bezelColor = nativeValue!; 72 | if ( 73 | this.nativeView.bezelStyle === NSBezelStyle.TexturedSquare || 74 | this.nativeView.bezelStyle === NSBezelStyle.TexturedRounded 75 | ) { 76 | this.nativeView.wantsLayer = true; 77 | this.nativeView.layer.backgroundColor = nativeValue?.CGColor as any; 78 | } 79 | } 80 | } 81 | 82 | @native({ 83 | setNative(view: Button, _key, value) { 84 | if (view.nativeView) { 85 | view.nativeView.bezelStyle = value; 86 | } 87 | }, 88 | }) 89 | declare bezelStyle: number; 90 | 91 | @native({ 92 | setNative(view: Button, _key, value) { 93 | if (view.nativeView) { 94 | view.nativeView.setButtonType(value); 95 | } 96 | }, 97 | }) 98 | declare buttonType: number; 99 | 100 | @native({ 101 | setNative(view: Button, _key, value) { 102 | if (view.nativeView) { 103 | let img: NSImage; 104 | if (typeof value === "string" && value?.indexOf(" -1) { 105 | const svgData = 106 | NSString.stringWithCString(value).dataUsingEncoding( 107 | NSUTF8StringEncoding 108 | ); 109 | img = NSImage.alloc().initWithData(svgData); 110 | } else if (typeof value === "string" && value?.indexOf("http") > -1) { 111 | img = NSImage.alloc().initWithContentsOfURL( 112 | NSURL.URLWithString(value) 113 | ); 114 | } else { 115 | img = NSImage.alloc().initWithContentsOfFile( 116 | (value instanceof URL ? value.pathname : value).replace( 117 | "file://", 118 | "" 119 | ) 120 | ); 121 | } 122 | view.nativeView.image = img; 123 | view.nativeView.imageScaling = NSImageScaling.ImageScaleProportionallyDown; 124 | } 125 | }, 126 | }) 127 | declare image: string; 128 | 129 | @native({ 130 | setNative(view: Button, _key, value) { 131 | if (view.nativeView) { 132 | //@ts-expect-error can be null; 133 | view.nativeView.image = !value 134 | ? null 135 | : NSImage.imageWithSystemSymbolNameAccessibilityDescription( 136 | value, 137 | null, 138 | ); 139 | } 140 | 141 | }, 142 | shouldLayout: true, 143 | }) 144 | declare icon: string; 145 | 146 | onMeasureFunction( 147 | width: number, 148 | widthMode: any, 149 | height: number, 150 | heightMode: any, 151 | ): { width: number; height: number } { 152 | return super.onMeasureFunction(width, widthMode, height, heightMode); 153 | } 154 | 155 | applyLayout(parentLayout?: YogaNodeLayout): void { 156 | super.applyLayout(parentLayout); 157 | if (this.nativeView) { 158 | this.nativeView.translatesAutoresizingMaskIntoConstraints = true; 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /native/core/views/button/native-button.ts: -------------------------------------------------------------------------------- 1 | import "@nativescript/macos-node-api"; 2 | import { Event } from "../../dom/dom-utils.ts"; 3 | import { MouseDownEvent } from "../view/native-view.ts"; 4 | import type { Button } from "./button.ts"; 5 | 6 | export class ButtonClickEvent extends Event { 7 | declare state?: boolean; 8 | constructor(state: boolean, eventDict?: EventInit) { 9 | super("click", eventDict); 10 | this.state = state; 11 | } 12 | } 13 | 14 | @NativeClass 15 | export class NativeButton extends NSButton { 16 | static ObjCExposedMethods = { 17 | clicked: { returns: interop.types.void, params: [interop.types.id] }, 18 | }; 19 | _attributedTitle?: NSMutableAttributedString; 20 | _color?: NSColor; 21 | _title?: string; 22 | _owner?: WeakRef`, 39 | // }) 40 | // ); 41 | // }, 1000); 42 | 43 | function App() { 44 | return ( 45 |
46 | 47 | {(snippet, index) => ( 48 |
49 | {snippet.dark ? ( 50 | 56 | ) : ( 57 | 63 | )} 64 |
65 | )} 66 |
67 |
68 | ); 69 | } 70 | 71 | export default App; 72 | -------------------------------------------------------------------------------- /snippets/src/assets/solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /snippets/src/components/CodeSnippet.tsx: -------------------------------------------------------------------------------- 1 | import Prism from "prismjs"; 2 | import "prismjs/components/prism-javascript"; 3 | import "prismjs/components/prism-jsx"; 4 | import { BiSolidCheckCircle, BiSolidCopy } from "solid-icons/bi"; 5 | import { createSignal, onMount } from "solid-js"; 6 | import "./prisim-atom-dark.css"; 7 | 8 | interface CodeSnippetProps { 9 | title: string; 10 | language: string; 11 | code: string; 12 | index: number; 13 | } 14 | 15 | const CodeSnippet = (props: CodeSnippetProps) => { 16 | const [copied, setCopied] = createSignal(false); 17 | 18 | onMount(() => { 19 | Prism.highlightAll(); 20 | }); 21 | 22 | const copyToClipboard = async () => { 23 | try { 24 | await navigator.clipboard.writeText(props.code); 25 | setCopied(true); 26 | setTimeout(() => setCopied(false), 2000); 27 | } catch (err) { 28 | console.error("Failed to copy text: ", err); 29 | } 30 | }; 31 | 32 | return ( 33 |
34 |
35 |
41 |
{props.title}
42 | 52 |
53 |
54 |           {props.code}
55 |         
56 |
57 |
58 | ); 59 | }; 60 | 61 | export default CodeSnippet; 62 | -------------------------------------------------------------------------------- /snippets/src/components/CodeSnippetLight.tsx: -------------------------------------------------------------------------------- 1 | import Prism from "prismjs"; 2 | import "prismjs/components/prism-javascript"; 3 | import "prismjs/components/prism-jsx"; 4 | import { BiSolidCheckCircle, BiSolidCopy } from "solid-icons/bi"; 5 | import { createSignal, onMount } from "solid-js"; 6 | import "./prisim-one-light.css"; 7 | 8 | interface CodeSnippetProps { 9 | title: string; 10 | language: string; 11 | code: string; 12 | index: number; 13 | } 14 | 15 | const CodeSnippetLight = (props: CodeSnippetProps) => { 16 | const [copied, setCopied] = createSignal(false); 17 | 18 | onMount(() => { 19 | Prism.highlightAll(); 20 | 21 | }); 22 | 23 | const copyToClipboard = async () => { 24 | try { 25 | await navigator.clipboard.writeText(props.code); 26 | setCopied(true); 27 | setTimeout(() => setCopied(false), 2000); 28 | } catch (err) { 29 | console.error("Failed to copy text: ", err); 30 | } 31 | }; 32 | 33 | return ( 34 |
35 |
36 |
42 |
{props.title}
43 | 53 |
54 |
55 |           {props.code}
56 |         
57 |
58 |
59 | ); 60 | }; 61 | 62 | export default CodeSnippetLight; 63 | -------------------------------------------------------------------------------- /snippets/src/components/prisim-atom-dark.css: -------------------------------------------------------------------------------- 1 | /** 2 | * atom-dark theme for `prism.js` 3 | * Based on Atom's `atom-dark` theme: https://github.com/atom/atom-dark-syntax 4 | * @author Joe Gibson (@gibsjose) 5 | */ 6 | 7 | .dark code[class*="language-"], 8 | .dark pre[class*="language-"] { 9 | color: #c5c8c6; 10 | text-shadow: 0 1px rgba(0, 0, 0, 0.3); 11 | font-family: Inconsolata, Monaco, Consolas, 'Courier New', Courier, monospace; 12 | direction: ltr; 13 | text-align: left; 14 | white-space: pre; 15 | word-spacing: normal; 16 | word-break: normal; 17 | line-height: 1.5; 18 | 19 | -moz-tab-size: 4; 20 | -o-tab-size: 4; 21 | tab-size: 4; 22 | 23 | -webkit-hyphens: none; 24 | -moz-hyphens: none; 25 | -ms-hyphens: none; 26 | hyphens: none; 27 | } 28 | 29 | /* Code blocks */ 30 | .dark pre[class*="language-"] { 31 | padding: 1em; 32 | margin: .5em 0; 33 | overflow: auto; 34 | border-radius: 0.3em; 35 | } 36 | 37 | .dark :not(pre) > code[class*="language-"], 38 | .dark pre[class*="language-"] { 39 | background: transparent; 40 | } 41 | 42 | /* Inline code */ 43 | .dark :not(pre) > code[class*="language-"] { 44 | padding: .1em; 45 | border-radius: .3em; 46 | } 47 | 48 | .dark .token.comment, 49 | .dark .token.prolog, 50 | .dark .token.doctype, 51 | .dark .token.cdata { 52 | color: #7C7C7C; 53 | } 54 | 55 | .dark .token.punctuation { 56 | color: #c5c8c6; 57 | } 58 | 59 | .dark .namespace { 60 | opacity: .7; 61 | } 62 | 63 | .dark .token.property, 64 | .dark .token.keyword, 65 | .dark .token.tag { 66 | color: #96CBFE; 67 | } 68 | 69 | .dark .token.class-name { 70 | color: #FFFFB6; 71 | text-decoration: underline; 72 | } 73 | 74 | .dark .token.boolean, 75 | .token.constant { 76 | color: #99CC99; 77 | } 78 | 79 | .dark .token.symbol, 80 | .dark .token.deleted { 81 | color: #f92672; 82 | } 83 | 84 | .dark .token.number { 85 | color: #FF73FD; 86 | } 87 | 88 | .dark .token.selector, 89 | .dark .token.attr-name, 90 | .dark .token.string, 91 | .dark .token.char, 92 | .dark .token.builtin, 93 | .dark .token.inserted { 94 | color: #A8FF60; 95 | } 96 | 97 | .dark .token.variable { 98 | color: #C6C5FE; 99 | } 100 | 101 | .dark .token.operator { 102 | color: #EDEDED; 103 | } 104 | 105 | .dark .token.entity { 106 | color: #FFFFB6; 107 | cursor: help; 108 | } 109 | 110 | .dark .token.url { 111 | color: #96CBFE; 112 | } 113 | 114 | .dark .language-css .token.string, 115 | .dark .style .token.string { 116 | color: #87C38A; 117 | } 118 | 119 | .dark .token.atrule, 120 | .dark .token.attr-value { 121 | color: #F9EE98; 122 | } 123 | 124 | .dark .token.function { 125 | color: #DAD085; 126 | } 127 | 128 | .dark .token.regex { 129 | color: #E9C062; 130 | } 131 | 132 | .dark .token.important { 133 | color: #fd971f; 134 | } 135 | 136 | .dark .token.important, 137 | .dark .token.bold { 138 | font-weight: bold; 139 | } 140 | 141 | .dark .token.italic { 142 | font-style: italic; 143 | } 144 | 145 | .dark .token.plain-text { 146 | color: white !important; 147 | } -------------------------------------------------------------------------------- /snippets/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 7 | line-height: 1.5; 8 | font-weight: 400; 9 | width: 100%; 10 | 11 | color-scheme: light dark; 12 | color: rgba(255, 255, 255, 0.87); 13 | /* background-color: #242424; */ 14 | 15 | font-synthesis: none; 16 | text-rendering: optimizeLegibility; 17 | -webkit-font-smoothing: antialiased; 18 | -moz-osx-font-smoothing: grayscale; 19 | } 20 | 21 | body { 22 | margin: 0; 23 | display: block; 24 | place-items: center; 25 | min-width: 320px; 26 | min-height: 100vh; 27 | } 28 | 29 | pre, 30 | pre[class*="language-"] { 31 | margin: 0 !important; 32 | background-color: #383e49; 33 | } 34 | 35 | /* @media (prefers-color-scheme: light) { 36 | :root { 37 | color: #213547; 38 | background-color: #ffffff; 39 | } 40 | } */ 41 | -------------------------------------------------------------------------------- /snippets/src/index.tsx: -------------------------------------------------------------------------------- 1 | /* @refresh reload */ 2 | import { render } from 'solid-js/web' 3 | 4 | import './index.css' 5 | import App from './App' 6 | 7 | const root = document.getElementById('root') 8 | 9 | render(() => , root!) 10 | -------------------------------------------------------------------------------- /snippets/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /snippets/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./src/**/*.{js,jsx,ts,tsx}'], 3 | theme: { 4 | extend: {}, 5 | }, 6 | plugins: [], 7 | }; 8 | -------------------------------------------------------------------------------- /snippets/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | "jsx": "preserve", 16 | "jsxImportSource": "solid-js", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true 23 | }, 24 | "include": ["src"] 25 | } 26 | -------------------------------------------------------------------------------- /snippets/tsconfig.app.tsbuildinfo: -------------------------------------------------------------------------------- 1 | {"root":["./src/app.tsx","./src/index.tsx","./src/vite-env.d.ts","./src/components/codesnippet.tsx","./src/components/codesnippetlight.tsx"],"version":"5.6.3"} -------------------------------------------------------------------------------- /snippets/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /snippets/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["ES2023"], 5 | "module": "ESNext", 6 | "skipLibCheck": true, 7 | 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "isolatedModules": true, 12 | "moduleDetection": "force", 13 | "noEmit": true, 14 | 15 | /* Linting */ 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noFallthroughCasesInSwitch": true 20 | }, 21 | "include": ["vite.config.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /snippets/tsconfig.node.tsbuildinfo: -------------------------------------------------------------------------------- 1 | {"root":["./vite.config.ts"],"version":"5.6.3"} -------------------------------------------------------------------------------- /snippets/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import solid from 'vite-plugin-solid' 3 | 4 | export default defineConfig({ 5 | plugins: [solid()], 6 | base: '', 7 | }) 8 | -------------------------------------------------------------------------------- /solid-native/renderer.js: -------------------------------------------------------------------------------- 1 | import { createRenderer } from "npm:solid-js/universal"; 2 | 3 | export const { 4 | render, 5 | effect, 6 | memo, 7 | createComponent, 8 | createElement, 9 | createTextNode, 10 | insertNode, 11 | insert, 12 | spread, 13 | setProp, 14 | mergeProps, 15 | } = createRenderer({ 16 | createElement(string) { 17 | return document.createElement(string); 18 | }, 19 | createTextNode(value) { 20 | return document.createTextNode(value); 21 | }, 22 | replaceText(textNode, value) { 23 | textNode.nodeValue = value; 24 | }, 25 | setProperty(node, name, value, prev) { 26 | if (value === prev) return; 27 | 28 | if (name.startsWith("on")) { 29 | const event = name.slice(2,3).toLowerCase() + name.slice(3); 30 | if (prev) node.removeEventListener(event, prev); 31 | node.addEventListener(event, value); 32 | return; 33 | } 34 | 35 | if (name === "ref") return value(node); 36 | if (name === "style") { 37 | node.style = value; 38 | } else { 39 | node.setAttribute(name, value); 40 | } 41 | }, 42 | insertNode(parent, node, anchor) { 43 | parent.insertBefore(node, anchor); 44 | }, 45 | isTextNode(node) { 46 | return node.nodeType === 3; 47 | }, 48 | removeNode(parent, node) { 49 | parent.removeChild(node); 50 | }, 51 | getParentNode(node) { 52 | return node.parentNode; 53 | }, 54 | getFirstChild(node) { 55 | return node.firstChild; 56 | }, 57 | getNextSibling(node) { 58 | return node.nextSibling; 59 | }, 60 | }); 61 | 62 | export function use(fn, args) { 63 | return fn?.(args); 64 | } 65 | 66 | export { 67 | ErrorBoundary, 68 | For, 69 | Index, 70 | Match, 71 | Show, 72 | Suspense, 73 | SuspenseList, 74 | Switch, 75 | } from "npm:solid-js"; 76 | -------------------------------------------------------------------------------- /src/app-menus.tsx: -------------------------------------------------------------------------------- 1 | import { getSolidLogo } from "./pages/common.ts"; 2 | 3 | export default function AppMenus() { 4 | function openDocs() { 5 | NSWorkspace.sharedWorkspace.openURL( 6 | NSURL.URLWithString("https://solidjs.com"), 7 | ); 8 | } 9 | 10 | function openGithub() { 11 | NSWorkspace.sharedWorkspace.openURL( 12 | NSURL.URLWithString("https://github.com/solidjs/solid"), 13 | ); 14 | } 15 | 16 | function openDiscord() { 17 | NSWorkspace.sharedWorkspace.openURL( 18 | NSURL.URLWithString("https://discord.com/invite/solidjs"), 19 | ); 20 | } 21 | 22 | return ( 23 | <> 24 | 25 | { 28 | NSApp.orderFrontStandardAboutPanel({ 29 | [NSAboutPanelOptionApplicationName]: "Solid Desktop", 30 | [NSAboutPanelOptionApplicationVersion]: "1.0.0", 31 | [NSAboutPanelOptionApplicationIcon]: getSolidLogo(), 32 | }); 33 | }} 34 | /> 35 | { 39 | NSApp.terminate(NSApp); 40 | }} 41 | /> 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 57 | 58 | 66 | 73 | 74 | 80 | Hello Solid macOS 81 | 82 | 83 | 88 | Let's build something 89 | 90 | 91 | 92 | 93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | import { createSignal } from "npm:solid-js"; 3 | import { render } from "../solid-native/renderer.js"; 4 | import AppMenus from "./app-menus.tsx"; 5 | import ContentView from "./contentview.tsx"; 6 | import Sidebar, { type SidebarItem } from "./sidebar.tsx"; 7 | import { selectedView } from "./state.tsx"; 8 | import Toolbar from "./toolbar.tsx"; 9 | 10 | function App() { 11 | const [currentSidebarItem, setCurrentSidebarItem] = createSignal< 12 | SidebarItem 13 | >(); 14 | return ( 15 | 31 | 32 | 33 | 34 | 40 | { 43 | setCurrentSidebarItem(item); 44 | }} 45 | selectedItem={currentSidebarItem()} 46 | /> 47 | 48 | 57 | 58 | 59 | 60 | 61 | ); 62 | } 63 | 64 | /** 65 | * A function export is required here to launch the app 66 | * in the correct context of the native app. This is because the 67 | * solid app needs to be pre-built seperately using esbuild. 68 | * See ./scripts/bundle.solid.ts for more information. 69 | */ 70 | export function startApp() { 71 | render(() => , document.body); 72 | } 73 | -------------------------------------------------------------------------------- /src/components/Switch.tsx: -------------------------------------------------------------------------------- 1 | import { createSignal } from "npm:solid-js"; 2 | 3 | function Switch() { 4 | const [on, setOn] = createSignal(false); 5 | return ( 6 | 17 | 22 | Turn on lights 23 | 24 | { 26 | setOn(!on()); 27 | }} 28 | /> 29 | 30 | ); 31 | } 32 | 33 | Switch.code = `function Switch() { 34 | const [on, setOn] = createSignal(false); 35 | return ( 36 | 47 | Turn on lights 50 | { 51 | setOn(!on()); 52 | }} /> 53 | 54 | ); 55 | }`; 56 | 57 | export default Switch; 58 | -------------------------------------------------------------------------------- /src/components/button.tsx: -------------------------------------------------------------------------------- 1 | import { createSignal } from "npm:solid-js"; 2 | 3 | function Button() { 4 | const [taps, setTaps] = createSignal(0); 5 | return ( 6 | 14 | ); 15 | } 16 | 17 | Button.code = `function Button() { 18 | const [taps, setTaps] = createSignal(0); 19 | return ( 20 | 27 | ); 28 | }`; 29 | 30 | export default Button; 31 | -------------------------------------------------------------------------------- /src/components/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import { createSignal } from "npm:solid-js"; 2 | 3 | function Checkbox() { 4 | const [checked, setChecked] = createSignal(false); 5 | return ( 6 | { 8 | setChecked(!checked()); 9 | }} 10 | checked={checked()} 11 | > 12 | Check me if you ❤️ Solid 13 | 14 | ); 15 | } 16 | 17 | Checkbox.code = `function Checkbox() { 18 | const [checked, setChecked] = createSignal(false); 19 | return ( 20 | { 22 | setChecked(!checked()); 23 | }} 24 | checked={checked()} 25 | > 26 | Check me if you ❤️ Solid 27 | 28 | ); 29 | }`; 30 | 31 | export default Checkbox; 32 | -------------------------------------------------------------------------------- /src/components/color-picker.tsx: -------------------------------------------------------------------------------- 1 | import { createSignal } from "npm:solid-js"; 2 | 3 | function ColorDialog() { 4 | const [chosenColor, setChosenColor] = createSignal("white"); 5 | return ( 6 | 14 | 22 | { 29 | setChosenColor(color); 30 | }, 31 | }} 32 | > 33 | Open Color Picker 34 | 35 | 36 | ); 37 | } 38 | 39 | ColorDialog.code = `function ColorPicker() { 40 | const [chosenColor, setChosenColor] = createSignal("white"); 41 | return ( 42 | 50 | 58 | { 65 | setChosenColor(color); 66 | }, 67 | }} 68 | > 69 | Open Color Picker 70 | 71 | 72 | ); 73 | }`; 74 | 75 | export default ColorDialog; 76 | -------------------------------------------------------------------------------- /src/components/combobox.tsx: -------------------------------------------------------------------------------- 1 | import { createSignal } from "npm:solid-js"; 2 | 3 | const comboItems = [ 4 | "Ryan Carniato", 5 | "David Di Biase", 6 | "Alexandre Mouton Brady", 7 | "Milo M.", 8 | "Ryan Turnquist", 9 | "Nikhil Saraf", 10 | ]; 11 | 12 | function Combobox() { 13 | const [selected, setSelected] = createSignal(0); 14 | return ( 15 | 21 | { 25 | setSelected(event.index); 26 | }} 27 | /> 28 | Selected: {comboItems[selected()]} 29 | 30 | ); 31 | } 32 | 33 | Combobox.code = `function Combobox() { 34 | const [selected, setSelected] = createSignal(0); 35 | return ( 36 | 42 | { 46 | setSelected(event.index); 47 | }} 48 | /> 49 | Selected: {comboItems[selected()]} 50 | 51 | ); 52 | }`; 53 | 54 | export default Combobox; 55 | -------------------------------------------------------------------------------- /src/components/date-picker.tsx: -------------------------------------------------------------------------------- 1 | import { createSignal } from "npm:solid-js"; 2 | 3 | function DatePicker() { 4 | const [date, setDate] = createSignal(); 5 | return ( 6 | 11 | { 13 | setDate(event.date); 14 | }} 15 | /> 16 | {date()?.toLocaleString()} 17 | 18 | ); 19 | } 20 | 21 | DatePicker.code = `function DatePicker() { 22 | const [date, setDate] = createSignal(); 23 | return ( 24 | 29 | { 31 | setDate(event.date); 32 | }} 33 | /> 34 | {date} 35 | 36 | ); 37 | }`; 38 | 39 | export default DatePicker; 40 | -------------------------------------------------------------------------------- /src/components/file-dialog.tsx: -------------------------------------------------------------------------------- 1 | import { createSignal, Show } from "npm:solid-js"; 2 | 3 | function FileDialog() { 4 | const [chosenFiles, setChosenFiles] = createSignal(); 5 | return ( 6 | 13 | { 39 | setChosenFiles(event.paths?.join("\n")); 40 | }} 41 | > 42 | Open File Dialog... 43 | 44 | 45 | {chosenFiles()} 46 | 47 | 48 | ); 49 | } 50 | 51 | FileDialog.code = `function FileDialog() { 52 | const [chosenFiles, setChosenFiles] = createSignal(); 53 | return ( 54 | 61 | { 87 | setChosenFiles(event.paths?.join("\n")); 88 | }} 89 | > 90 | Open File Dialog... 91 | 92 | 93 | {chosenFiles()} 94 | 95 | 96 | ); 97 | }`; 98 | 99 | export default FileDialog; 100 | -------------------------------------------------------------------------------- /src/components/image.tsx: -------------------------------------------------------------------------------- 1 | function Image() { 2 | return ( 3 | 12 | ); 13 | } 14 | 15 | Image.code = `function Image() { 16 | return ( 17 | 26 | ); 27 | }`; 28 | 29 | export default Image; 30 | -------------------------------------------------------------------------------- /src/components/modal.tsx: -------------------------------------------------------------------------------- 1 | function Modal() { 2 | let modalRef: HTMLWindowElement; 3 | return ( 4 | 5 | (modalRef = el)} 7 | title="Modal" 8 | styleMask={NSWindowStyleMask.Titled | 9 | NSWindowStyleMask.Closable | 10 | NSWindowStyleMask.Resizable} 11 | style={{ 12 | width: 200, 13 | height: 200, 14 | }} 15 | > 16 | 25 | A Solid Modal 26 | 27 | 25 | 26 | 31 | {count()} 32 | 33 | 34 | 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/examples/index.tsx: -------------------------------------------------------------------------------- 1 | import { getSolidLogo } from "../pages/common.ts"; 2 | 3 | export default function Examples() { 4 | return ( 5 | 11 | 18 | 26 | 27 | 33 | Solid Desktop Examples 34 | 35 | 36 | 45 | 51 | Play with the examples to learn how to build macOS apps with Solid and 52 | NativeScript. 53 | 54 | 55 | 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /src/hooks/use-color-scheme.ts: -------------------------------------------------------------------------------- 1 | export function useColorScheme() { 2 | const colorScheme = NSApp.effectiveAppearance 3 | .bestMatchFromAppearancesWithNames([ 4 | NSAppearanceNameDarkAqua, 5 | NSAppearanceNameVibrantDark, 6 | ]); 7 | 8 | return colorScheme ? "dark" : "light"; 9 | } 10 | -------------------------------------------------------------------------------- /src/pages/common.ts: -------------------------------------------------------------------------------- 1 | export function getSolidLogo() { 2 | if ( 3 | NSBundle.mainBundle?.objectForInfoDictionaryKey("NativeScriptApplication") 4 | ) { 5 | const logo = NSBundle.mainBundle.pathForResourceOfType("solid", "png"); 6 | console.log("logo:", logo); 7 | return logo; 8 | } else { 9 | return import.meta.resolve("../assets/solid.png"); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/pages/components.tsx: -------------------------------------------------------------------------------- 1 | import { getSolidLogo } from "./common.ts"; 2 | 3 | export default function Components() { 4 | return ( 5 | 11 | 19 | 20 | 26 | Solid AppKit Components 27 | 28 | 35 | Try AppKit for yourself 36 | 37 | 45 | Copy Solid component snippets for your own use 46 | 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/pages/getting-started.tsx: -------------------------------------------------------------------------------- 1 | import { getSolidLogo } from "./common.ts"; 2 | 3 | export default function GettingStarted() { 4 | return ( 5 | 11 | 19 | 25 | Hello Solid macOS 26 | 27 | 33 | Let's build something Solid ❤️ together 34 | 35 | 43 | 50 | 1. Explore AppKit components to use in your app 51 | 52 | 60 | 2. Explore complete examples by switching views on top 61 | 62 | 70 | 3. Share videos of your app on X, Discord, GitHub, LinkedIn, etc. 71 | 72 | 73 | 74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /src/pages/overview.tsx: -------------------------------------------------------------------------------- 1 | import { getSolidLogo } from "./common.ts"; 2 | export default function Overview() { 3 | return ( 4 | 10 | 17 | 25 | 26 | 32 | Solid macOS Overview 33 | 34 | 35 | 36 | 42 | 51 | 57 | Solid macOS is a macOS app built with Solid and NativeScript. 58 | 59 | 66 | It uses native components from Apple's AppKit, used to build the 67 | user interface for a macOS app. 68 | 69 | 70 | 71 | 72 | 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /src/pages/setup.tsx: -------------------------------------------------------------------------------- 1 | import { getSolidLogo } from "./common.ts"; 2 | 3 | export default function Setup() { 4 | return ( 5 | 11 | 18 | 26 | 27 | 33 | Solid macOS Setup 34 | 35 | 36 | 37 | 43 | 44 | 53 | 60 | Setup your own Solid macOS app by cloning this template: 61 | 62 | 68 | 84 | 85 | 86 | 94 | 101 | This template provides all the necessary setup to get started 102 | with Solid macOS. 103 | 104 | 112 | You can explore all the UI components available here in this app 113 | on the left. Use the copy snippet button to use any UI control 114 | for your own Solid macOS app. 115 | 116 | 117 | 118 | 119 | 120 | 121 | ); 122 | } 123 | -------------------------------------------------------------------------------- /src/state.tsx: -------------------------------------------------------------------------------- 1 | import { createSignal } from "npm:solid-js"; 2 | 3 | export const [selectedView, setSelectedView] = createSignal(0); 4 | export function changeToolbar(index: number) { 5 | setSelectedView(index); 6 | } 7 | -------------------------------------------------------------------------------- /src/toolbar.tsx: -------------------------------------------------------------------------------- 1 | import { changeToolbar } from "./state.tsx"; 2 | 3 | const URLS = [ 4 | "https://docs.solidjs.com", 5 | "https://github.com/solidjs/solid", 6 | "https://discord.gg/solidjs", 7 | ]; 8 | 9 | export default function Toolbar() { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | changeToolbar(event.selectedIndex)} 26 | /> 27 | 28 | 29 | 30 | 37 | NSWorkspace.sharedWorkspace.openURL( 38 | NSURL.URLWithString(URLS[event.selectedIndex]), 39 | )} 40 | /> 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/utils/colors.ts: -------------------------------------------------------------------------------- 1 | export const SolidColors = { 2 | primary: "#2c4f7c", 3 | secondary: "#335d92", 4 | light: "#446b9e", 5 | accent: "#66e6ac", 6 | secondaryAccent: "#0CDC73", 7 | }; 8 | -------------------------------------------------------------------------------- /src/webdisplay.tsx: -------------------------------------------------------------------------------- 1 | import { type Component, createSignal, Show } from "npm:solid-js"; 2 | 3 | interface WebDisplayProps { 4 | url: string; 5 | } 6 | 7 | const WebDisplay: Component = (props) => { 8 | const [isLoading, setIsLoading] = createSignal(false); 9 | return ( 10 | 11 | 12 | 19 | 20 | { 24 | setIsLoading(true); 25 | }} 26 | onLoadFinished={() => { 27 | setIsLoading(false); 28 | }} 29 | > 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default WebDisplay; 36 | -------------------------------------------------------------------------------- /vendor/undom-ng/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jason Miller 4 | Copyright (c) 2022 Yukino Song 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /vendor/undom-ng/README.md: -------------------------------------------------------------------------------- 1 | # undom-ng 2 | 3 | [![NPM](https://img.shields.io/npm/v/undom-ng.svg?style=flat)](https://www.npmjs.org/package/undom-ng) 4 | 5 | ### **The Next Gen minimally viable DOM Document implementation** 6 | 7 | **NOTE** THIS IS A FORK OF UNDOM WITH SOME HUGE CHANGES THAT MIGHT NOT FIT THE GOAL OF THE [ORIGINAL PROJECT](https://github.com/developit/undom). 8 | 9 | > A bare-bones HTML DOM in a box. If you want the DOM but not a parser, this might be for you. 10 | > 11 | > Works in Node and browsers, plugin ready! 12 | 13 | --- 14 | 15 | 16 | ## Project Goals 17 | 18 | Undom aims to find a sweet spot between size/performance and utility. The goal is to provide the simplest possible implementation of a DOM Document, such that libraries relying on the DOM can run in places where there isn't one available. 19 | 20 | The intent to keep things as simple as possible means undom lacks some DOM features like HTML parsing & serialization, Web Components, etc. These features can be added through additional libraries. 21 | 22 | 23 | --- 24 | 25 | 26 | ## Installation 27 | 28 | Via npm: 29 | 30 | `npm install undom-ng` 31 | 32 | 33 | --- 34 | 35 | ## Derivatives 36 | 37 | [DOMiNATIVE](https://github.com/SudoMaker/DOMiNATIVE) Generic DOM implementation for [NativeScript](https://nativescript.org/) 38 | 39 | 40 | --- 41 | 42 | 43 | ## Usage 44 | 45 | ```js 46 | import { createEnvironment, HTMLNS } from 'undom-ng' 47 | 48 | const { createDocument } = createEnvironment() 49 | 50 | const document = createDocument(HTMLNS, 'html') 51 | 52 | const foo = document.createElementNS(HTMLNS, 'foo') 53 | foo.appendChild(document.createTextNode('Hello, World!')) 54 | document.body.appendChild(foo); 55 | ``` 56 | 57 | --- 58 | 59 | 60 | ## Serialize to HTML 61 | 62 | ```js 63 | import { serialize } from 'undom-ng' 64 | 65 | console.log(serialize(element)) 66 | ``` 67 | -------------------------------------------------------------------------------- /vendor/undom-ng/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "undom-ng", 3 | "amdName": "undom-ng", 4 | "version": "1.1.2", 5 | "description": "The Next Gen minimally viable DOM Document implementation", 6 | "main": "src/undom-ng.js", 7 | "type": "module", 8 | "scripts": {}, 9 | "keywords": [ 10 | "dom", 11 | "document", 12 | "shim" 13 | ], 14 | "repository": "ClassicOldSong/undom-ng", 15 | "author": "Yukino Song ", 16 | "license": "MIT", 17 | "bugs": "https://github.com/ClassicOldSong/undom-ng/issues", 18 | "homepage": "https://github.com/ClassicOldSong/undom-ng" 19 | } 20 | -------------------------------------------------------------------------------- /vendor/undom-ng/src/namespaces.js: -------------------------------------------------------------------------------- 1 | export const HTMLNS = 'http://www.w3.org/1999/xhtml' 2 | export const SVGNS = 'http://www.w3.org/2000/svg' 3 | -------------------------------------------------------------------------------- /vendor/undom-ng/src/serializer.js: -------------------------------------------------------------------------------- 1 | const selfClosingTags = { 2 | area: true, 3 | base: true, 4 | br: true, 5 | col: true, 6 | command: true, 7 | embed: true, 8 | hr: true, 9 | img: true, 10 | input: true, 11 | keygen: true, 12 | link: true, 13 | menuitem: true, 14 | meta: true, 15 | param: true, 16 | source: true, 17 | track: true, 18 | wbr: true 19 | } 20 | 21 | // const serializerRegexp = /[&'"<>\u00a0-\u00b6\u00b8-\u00ff\u0152\u0153\u0160\u0161\u0178\u0192\u02c6\u02dc\u0391-\u03a1\u03a3-\u03a9\u03b1-\u03c9\u03d1\u03d2\u03d6\u2002\u2003\u2009\u200c-\u200f\u2013\u2014\u2018-\u201a\u201c-\u201e\u2020-\u2022\u2026\u2030\u2032\u2033\u2039\u203a\u203e\u20ac\u2122\u2190-\u2194\u21b5\u2200\u2202\u2203\u2205\u2207-\u2209\u220b\u220f\u2211\u2212\u2217\u221a\u221d\u221e\u2220\u2227-\u222b\u2234\u223c\u2245\u2248\u2260\u2261\u2264\u2265\u2282-\u2284\u2286\u2287\u2295\u2297\u22a5\u22c5\u2308-\u230b\u25ca\u2660\u2663\u2665\u2666]/g 22 | const serializerRegexp = /[&'"<>]/g 23 | 24 | const enc = s => `${s}`.replace(serializerRegexp, a => `&#${a.codePointAt(0)};`) 25 | 26 | const attr = (a) => { 27 | if (a.value !== '') { 28 | if (a.ns) { 29 | return ` ${a.ns}:${a.name}="${enc(a.value)}"` 30 | } 31 | return ` ${a.name}="${enc(a.value)}"` 32 | } 33 | return ` ${a.name}` 34 | } 35 | 36 | const serialize = (el, useRawName) => { 37 | switch (el.nodeType) { 38 | case 3: { 39 | if (el.data) { 40 | if (el.parentNode && ['SCRIPT', 'STYLE'].indexOf(el.parentNode.nodeName) > -1) return el.data 41 | return enc(el.data) 42 | } 43 | return '' 44 | } 45 | 46 | case 8: { 47 | if (el.data) return `` 48 | return '' 49 | } 50 | 51 | default: { 52 | if (!el.nodeName) return '' 53 | 54 | const {nodeName, localName, attributes, firstChild} = el 55 | const xmlStringFrags = [] 56 | 57 | let tag = nodeName 58 | 59 | if (useRawName) tag = localName 60 | else tag = nodeName.toLowerCase() 61 | 62 | if (tag && tag[0] === '#') tag = tag.substring(1) 63 | 64 | if (tag) xmlStringFrags.push(`<${tag}`) 65 | if (attributes) xmlStringFrags.push(...attributes.map(attr)) 66 | if (firstChild) { 67 | if (tag) xmlStringFrags.push('>') 68 | 69 | let currentNode = firstChild 70 | while (currentNode) { 71 | xmlStringFrags.push(serialize(currentNode, useRawName)) 72 | currentNode = currentNode.nextSibling 73 | } 74 | 75 | if (tag) xmlStringFrags.push(``) 76 | } else if (tag) { 77 | if (selfClosingTags[tag]) xmlStringFrags.push('/>') 78 | else xmlStringFrags.push(`>`) 79 | } 80 | 81 | return ''.concat(...xmlStringFrags) 82 | } 83 | } 84 | } 85 | 86 | export default serialize 87 | -------------------------------------------------------------------------------- /vendor/undom-ng/src/undom-ng.js: -------------------------------------------------------------------------------- 1 | export * from './undom.js' 2 | export * from './namespaces.js' 3 | export { default as serialize } from './serializer.js' 4 | -------------------------------------------------------------------------------- /vendor/undom-ng/src/utils.js: -------------------------------------------------------------------------------- 1 | export const toLower = str => String(str).toLowerCase() 2 | 3 | // eslint-disable-next-line max-params 4 | export const findWhere = (arr, fn, returnIndex, byValue) => { 5 | let i = arr.length 6 | while (i) { 7 | i -= 1 8 | const val = arr[i] 9 | if (byValue) { 10 | if (val === fn) return returnIndex ? i : val 11 | } else if (fn(val)) return returnIndex ? i : val 12 | } 13 | } 14 | 15 | // eslint-disable-next-line max-params 16 | export const splice = (arr, item, add, byValue) => { 17 | let i = arr ? findWhere(arr, item, true, byValue) : -1 18 | if (i > -1) { 19 | if (add) arr.splice(i, 0, add) 20 | else arr.splice(i, 1) 21 | } 22 | return i 23 | } 24 | 25 | export const createAttributeFilter = (ns, name) => o => o.ns === ns && toLower(o.name) === toLower(name) 26 | 27 | export const named = (key, extender) => { 28 | key = `__undom_is_${key}` 29 | 30 | const maker = (_, ...args) => { 31 | if (_ && _.prototype[key]) return _ 32 | const extendedClass = extender(_, ...args) 33 | Object.defineProperty(extendedClass.prototype, key, { 34 | enumerable: false, 35 | value: true 36 | }) 37 | return extendedClass 38 | } 39 | 40 | maker.master = (...args) => { 41 | const extendedClass = maker(...args) 42 | 43 | Object.defineProperty(extendedClass, Symbol.hasInstance, { 44 | value(instance) { 45 | return instance && instance[key] 46 | } 47 | }) 48 | 49 | return extendedClass 50 | } 51 | 52 | return maker 53 | } 54 | --------------------------------------------------------------------------------