├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── build-desktop.yml │ ├── build-site.yml │ ├── release-desktop.yml │ └── unit-tests.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .nvmrc ├── .prettierignore ├── .vscode ├── extensions.json └── settings.json ├── CODE_OF_CONDUCT.md ├── README.md ├── docs ├── 2D Graphics.md └── assets │ └── noya-screenshot.png ├── modular ├── setupEnvironment.ts └── setupTests.ts ├── package.json ├── packages ├── README.md ├── app │ ├── package.json │ ├── public │ │ ├── AlphaMasks.sketch │ │ ├── Artboard.sketch │ │ ├── BackdropFilter.sketch │ │ ├── BooleanOperations.sketch │ │ ├── BooleanOperationsAdvanced.sketch │ │ ├── ColorAsset.sketch │ │ ├── Demo.sketch │ │ ├── ExportOptions.sketch │ │ ├── FixedRadius.sketch │ │ ├── Gradient.sketch │ │ ├── Gradients.sketch │ │ ├── GroupedStyles.sketch │ │ ├── GroupedSwatches.sketch │ │ ├── Image.sketch │ │ ├── ImageFills.sketch │ │ ├── InnerShadows.sketch │ │ ├── Masks.sketch │ │ ├── MultipleStyles.sketch │ │ ├── Oval.sketch │ │ ├── PDF.sketch │ │ ├── Pages.sketch │ │ ├── Path.sketch │ │ ├── Polygons.sketch │ │ ├── Rectangle.sketch │ │ ├── Rotation.sketch │ │ ├── SamplePath.sketch │ │ ├── SavedGradient.sketch │ │ ├── Scaling.sketch │ │ ├── Shaders.sketch │ │ ├── Shadows.sketch │ │ ├── ShapeGroup.sketch │ │ ├── SharedStyles.sketch │ │ ├── Slices.sketch │ │ ├── Symbols NoCanOverride.sketch │ │ ├── Symbols.sketch │ │ ├── TextLayers.sketch │ │ ├── TextStyles.sketch │ │ ├── Tints.sketch │ │ ├── WindingRule.sketch │ │ ├── _headers │ │ ├── favicon.ico │ │ ├── fonts │ │ │ └── roboto │ │ │ │ ├── LICENSE │ │ │ │ └── Roboto-Regular.ttf │ │ ├── index.html │ │ ├── line.sketch │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ ├── robots.txt │ │ └── wasm │ │ │ ├── canvaskit.wasm │ │ │ └── pathkit.wasm │ ├── src │ │ ├── App.tsx │ │ ├── __tests__ │ │ │ ├── App.test.tsx │ │ │ ├── __image_snapshots__ │ │ │ │ ├── renderer-test-tsx-alpha-masks-1-snap.png │ │ │ │ ├── renderer-test-tsx-backdrop-filter-1-snap.png │ │ │ │ ├── renderer-test-tsx-boolean-operations-1-snap.png │ │ │ │ ├── renderer-test-tsx-boolean-operations-advanced-1-snap.png │ │ │ │ ├── renderer-test-tsx-demo-1-snap.png │ │ │ │ ├── renderer-test-tsx-gradient-1-snap.png │ │ │ │ ├── renderer-test-tsx-gradient-editor-linear-gradient-1-snap.png │ │ │ │ ├── renderer-test-tsx-gradient-editor-radial-gradient-1-snap.png │ │ │ │ ├── renderer-test-tsx-image-1-snap.png │ │ │ │ ├── renderer-test-tsx-image-fills-1-snap.png │ │ │ │ ├── renderer-test-tsx-inner-shadows-1-snap.png │ │ │ │ ├── renderer-test-tsx-line-editor-selected-line-in-artboard-1-snap.png │ │ │ │ ├── renderer-test-tsx-masks-1-snap.png │ │ │ │ ├── renderer-test-tsx-rotation-0-1-snap.png │ │ │ │ ├── renderer-test-tsx-rotation-1-1-snap.png │ │ │ │ ├── renderer-test-tsx-rotation-2-1-snap.png │ │ │ │ ├── renderer-test-tsx-rotation-3-1-snap.png │ │ │ │ ├── renderer-test-tsx-rotation-4-1-snap.png │ │ │ │ ├── renderer-test-tsx-sample-path-0-1-snap.png │ │ │ │ ├── renderer-test-tsx-sample-path-1-1-snap.png │ │ │ │ ├── renderer-test-tsx-sample-path-2-1-snap.png │ │ │ │ ├── renderer-test-tsx-sample-path-3-1-snap.png │ │ │ │ ├── renderer-test-tsx-sample-path-4-1-snap.png │ │ │ │ ├── renderer-test-tsx-sample-path-5-1-snap.png │ │ │ │ ├── renderer-test-tsx-shaders-1-snap.png │ │ │ │ ├── renderer-test-tsx-shadows-1-snap.png │ │ │ │ ├── renderer-test-tsx-symbols-1-snap.png │ │ │ │ ├── renderer-test-tsx-symbols-2-1-snap.png │ │ │ │ ├── renderer-test-tsx-text-layers-1-snap.png │ │ │ │ ├── renderer-test-tsx-tints-1-snap.png │ │ │ │ └── renderer-test-tsx-winding-rule-1-snap.png │ │ │ └── renderer.test.tsx │ │ ├── containers │ │ │ ├── AlignmentInspector.tsx │ │ │ ├── ArtboardSizeList.tsx │ │ │ ├── BlurInspector.tsx │ │ │ ├── BorderInspector.tsx │ │ │ ├── ColorControlsInspector.tsx │ │ │ ├── ControlPointCoordinatesInspector.tsx │ │ │ ├── ExportInspector.tsx │ │ │ ├── FillInspector.tsx │ │ │ ├── InnerShadowInspector.tsx │ │ │ ├── Inspector.tsx │ │ │ ├── LayerList.tsx │ │ │ ├── LinkedStyleInspector.tsx │ │ │ ├── LinkedTextStyleInspector.tsx │ │ │ ├── Menubar.tsx │ │ │ ├── OpacityInspector.tsx │ │ │ ├── PageList.tsx │ │ │ ├── PagePreviewItem.tsx │ │ │ ├── PagesGrid.tsx │ │ │ ├── PointControlsInspector.tsx │ │ │ ├── PointCoordinatesInspector.tsx │ │ │ ├── RadiusInspector.tsx │ │ │ ├── ShadowInspector.tsx │ │ │ ├── SwatchesInspector.tsx │ │ │ ├── SymbolInstanceInspector.tsx │ │ │ ├── SymbolMasterInspector.tsx │ │ │ ├── TextStyleInspector.tsx │ │ │ ├── ThemeGroups.tsx │ │ │ ├── ThemeInspector.tsx │ │ │ ├── ThemeStyleInspector.tsx │ │ │ ├── ThemeSymbolsInspector.tsx │ │ │ ├── ThemeTextStyleInspector.tsx │ │ │ ├── ThemeToolbar.tsx │ │ │ ├── ThemeWindow.tsx │ │ │ ├── Toolbar.tsx │ │ │ └── Workspace.tsx │ │ ├── hooks │ │ │ ├── useEnvironmentParameters.ts │ │ │ └── useFileManager.ts │ │ ├── index.css │ │ ├── index.tsx │ │ ├── logo.svg │ │ └── react-app-env.d.ts │ └── tsconfig.json ├── canvaskit-sandbox │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ ├── src │ │ ├── App.tsx │ │ ├── index.tsx │ │ └── react-app-env.d.ts │ └── tsconfig.json ├── canvaskit │ ├── README.md │ ├── package.json │ └── src │ │ ├── canvaskit.js │ │ ├── emscriptenRequireInterop.ts │ │ └── index.ts ├── noya-api │ ├── package.json │ └── src │ │ ├── core │ │ ├── client.ts │ │ ├── collection.ts │ │ ├── error.ts │ │ ├── localStorageClient.ts │ │ ├── memoryClient.ts │ │ ├── networkClient.ts │ │ ├── schema.ts │ │ └── throttleAsync.ts │ │ ├── index.ts │ │ └── react │ │ ├── context.ts │ │ └── hooks.ts ├── noya-app-state-context │ ├── README.md │ ├── package.json │ └── src │ │ ├── ApplicationStateContext.tsx │ │ ├── index.tsx │ │ ├── useHistory.tsx │ │ └── useWorkspace.tsx ├── noya-canvas-preview │ ├── package.json │ └── src │ │ ├── components │ │ └── CanvasViewer.tsx │ │ └── index.ts ├── noya-canvas │ ├── package.json │ └── src │ │ ├── components │ │ ├── Canvas.tsx │ │ ├── CanvasElement.tsx │ │ ├── CanvasKitRenderer.tsx │ │ ├── SimpleCanvas.tsx │ │ └── types.ts │ │ ├── hooks │ │ ├── useArrowKeyShortcuts.ts │ │ ├── useAutomaticCanvasSize.ts │ │ ├── useCanvasShortcuts.ts │ │ ├── useCopyHandler.tsx │ │ ├── useInteractionHandlers.tsx │ │ ├── useLayerMenu.tsx │ │ ├── useMultipleClickCount.ts │ │ └── usePasteHandler.ts │ │ ├── index.ts │ │ ├── interactions │ │ ├── clipboard.ts │ │ ├── defaultCursor.ts │ │ ├── drawing.ts │ │ ├── duplicate.ts │ │ ├── editBlock.ts │ │ ├── editText.ts │ │ ├── escape.ts │ │ ├── focus.ts │ │ ├── history.ts │ │ ├── index.ts │ │ ├── insertMode.ts │ │ ├── marquee.ts │ │ ├── move.ts │ │ ├── pan.ts │ │ ├── reorder.ts │ │ ├── scale.ts │ │ ├── selection.ts │ │ ├── selectionMode.ts │ │ ├── types.ts │ │ └── zoom.ts │ │ └── utils │ │ ├── convertPoint.ts │ │ ├── importImageFile.ts │ │ └── isMoving.ts ├── noya-colorpicker │ ├── LICENSE │ ├── README.md │ ├── package.json │ └── src │ │ ├── components │ │ ├── Alpha.tsx │ │ ├── ColorPicker.tsx │ │ ├── Gradient.tsx │ │ ├── HexColorInput.tsx │ │ ├── Hue.tsx │ │ ├── Interactive.tsx │ │ ├── Pointer.tsx │ │ └── Saturation.tsx │ │ ├── contexts │ │ └── ColorPickerContext.ts │ │ ├── hooks │ │ ├── useEventCallback.ts │ │ └── useIsomorphicLayoutEffect.ts │ │ ├── index.tsx │ │ ├── types.ts │ │ └── utils │ │ ├── clamp.ts │ │ ├── compare.ts │ │ ├── convert.ts │ │ ├── format.ts │ │ ├── interpolateRgba.ts │ │ ├── round.ts │ │ └── validate.ts ├── noya-compiler │ ├── package.json │ └── src │ │ └── index.ts ├── noya-designsystem │ ├── README.md │ ├── package.json │ └── src │ │ ├── components │ │ ├── Avatar.tsx │ │ ├── Button.tsx │ │ ├── Chip.tsx │ │ ├── ColorPicker.tsx │ │ ├── ContextMenu.tsx │ │ ├── Dialog.tsx │ │ ├── Divider.tsx │ │ ├── DropdownMenu.tsx │ │ ├── FillInputField.tsx │ │ ├── FillPreviewBackground.tsx │ │ ├── GradientPicker.tsx │ │ ├── Grid.tsx │ │ ├── GridView.tsx │ │ ├── IconButton.tsx │ │ ├── InputField.tsx │ │ ├── Label.tsx │ │ ├── LabeledElementView.tsx │ │ ├── ListView.tsx │ │ ├── Popover.tsx │ │ ├── Progress.tsx │ │ ├── RadioGroup.tsx │ │ ├── ScrollArea.tsx │ │ ├── Select.tsx │ │ ├── Slider.tsx │ │ ├── Sortable.tsx │ │ ├── Spacer.tsx │ │ ├── Stack.tsx │ │ ├── Switch.tsx │ │ ├── Text.tsx │ │ ├── Toast.tsx │ │ ├── Tooltip.tsx │ │ ├── TreeView.tsx │ │ └── internal │ │ │ ├── Menu.tsx │ │ │ ├── TextInput.tsx │ │ │ └── __tests__ │ │ │ └── TextInput.test.tsx │ │ ├── contexts │ │ ├── DesignSystemConfiguration.tsx │ │ ├── DialogContext.tsx │ │ └── GlobalInputBlurContext.tsx │ │ ├── hooks │ │ ├── __tests__ │ │ │ └── mergeEventHandlers.test.ts │ │ ├── mergeEventHandlers.ts │ │ ├── useHover.ts │ │ ├── useObjectURL.ts │ │ └── usePlatform.ts │ │ ├── index.tsx │ │ ├── mediaQuery.ts │ │ ├── theme │ │ ├── dark.ts │ │ ├── index.ts │ │ └── light.ts │ │ └── utils │ │ ├── breakpoints.ts │ │ ├── createSectionedMenu.ts │ │ ├── getGradientBackground.tsx │ │ ├── handleNudge.ts │ │ ├── mouseEvent.ts │ │ ├── sketchColor.ts │ │ ├── sketchPattern.ts │ │ └── withSeparatorElements.ts ├── noya-desktop │ ├── .eslintrc.js │ ├── .gitignore │ ├── README.md │ ├── entitlements.plist │ ├── forge.config.ts │ ├── package.json │ ├── src │ │ ├── actions │ │ │ ├── doubleClickToolbar.ts │ │ │ ├── openFile.ts │ │ │ ├── saveFile.ts │ │ │ └── setMenu.ts │ │ ├── main.ts │ │ ├── preload.ts │ │ ├── types.ts │ │ └── version.ts │ ├── tools │ │ └── add-macos-cert.sh │ ├── webpack.main.config.ts │ └── webpack.renderer.config.ts ├── noya-embedded │ ├── README.md │ ├── package.json │ └── src │ │ ├── applicationMenu.ts │ │ ├── fileManager.ts │ │ ├── hostApp.ts │ │ ├── index.ts │ │ └── types.ts ├── noya-file-format │ ├── README.md │ ├── package.json │ └── src │ │ ├── index.ts │ │ └── types.ts ├── noya-filesystem │ ├── package.json │ └── src │ │ ├── file.ts │ │ ├── fileSystem.ts │ │ ├── index.ts │ │ ├── volume.ts │ │ └── zip.ts ├── noya-fonts │ ├── README.md │ ├── package.json │ └── src │ │ ├── Emitter.ts │ │ ├── __tests__ │ │ ├── fontDescriptor.test.ts │ │ ├── fontManager.test.ts │ │ ├── fontTraits.test.ts │ │ └── index.test.ts │ │ ├── fontDescriptor.ts │ │ ├── fontManager.ts │ │ ├── fontTraits.ts │ │ ├── fontWeight.ts │ │ ├── index.ts │ │ └── types.ts ├── noya-generate-image │ ├── README.md │ ├── package.json │ └── src │ │ └── index.tsx ├── noya-geometry │ ├── README.md │ ├── package.json │ └── src │ │ ├── AffineTransform.ts │ │ ├── __tests__ │ │ ├── AffineTransform.test.ts │ │ ├── index.test.ts │ │ ├── line.test.ts │ │ └── resize.test.ts │ │ ├── getLineOrientation.ts │ │ ├── index.ts │ │ ├── line.ts │ │ ├── point.ts │ │ ├── radians.ts │ │ ├── rect.ts │ │ ├── resize.ts │ │ └── types.ts ├── noya-google-fonts │ ├── README.md │ ├── package.json │ └── src │ │ ├── FontRegistry.ts │ │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ └── index.test.ts.snap │ │ ├── index.test.ts │ │ └── variant.test.ts │ │ ├── fonts.json │ │ ├── index.ts │ │ ├── provider.ts │ │ ├── types.ts │ │ └── variant.ts ├── noya-graphql-server │ ├── README.md │ ├── Test.sketch │ ├── package.json │ ├── src │ │ ├── __tests__ │ │ │ └── index.test.ts │ │ ├── index.ts │ │ └── schema.graphql │ └── webpack.config.js ├── noya-icons │ ├── README.md │ ├── package.json │ └── src │ │ ├── icons │ │ ├── BorderCenterIcon.tsx │ │ ├── BorderInsideIcon.tsx │ │ ├── BorderOutsideIcon.tsx │ │ ├── FlipHorizontalIcon.tsx │ │ ├── FlipVerticalIcon.tsx │ │ ├── LineIcon.tsx │ │ └── PointModeIcon.tsx │ │ └── index.ts ├── noya-import-svg │ ├── README.md │ ├── package.json │ └── src │ │ ├── PathBuilder.ts │ │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ └── index.test.ts.snap │ │ ├── bowtie-viewbox.svg │ │ ├── bowtie.svg │ │ ├── circle.svg │ │ ├── demo.svg │ │ └── index.test.ts │ │ └── index.ts ├── noya-inspector │ ├── package.json │ └── src │ │ ├── components │ │ ├── ArrayController.tsx │ │ ├── BlurRow.tsx │ │ ├── BorderRow.tsx │ │ ├── CheckboxArrayController.tsx │ │ ├── ColorControlsRow.tsx │ │ ├── ColorInspector.tsx │ │ ├── CoordinatesInspector.tsx │ │ ├── DimensionInput.tsx │ │ ├── DimensionSliderRow.tsx │ │ ├── DimensionsInspector.tsx │ │ ├── EnableableElementController.tsx │ │ ├── ExportFormatsRow.tsx │ │ ├── FillInputFieldWithPicker.tsx │ │ ├── FillRow.tsx │ │ ├── FlipControls.tsx │ │ ├── GradientInspector.tsx │ │ ├── InspectorPrimitives.tsx │ │ ├── LayerIcon.tsx │ │ ├── LineInspector.tsx │ │ ├── LinkedSymbolRow.tsx │ │ ├── NameInspector.tsx │ │ ├── PatternInspector.tsx │ │ ├── PickerAssetGrid.tsx │ │ ├── PickerGradients.tsx │ │ ├── PickerPatterns.tsx │ │ ├── PickerSwatches.tsx │ │ ├── ShaderInspector.tsx │ │ ├── ShaderVariableRow.tsx │ │ ├── ShadowRow.tsx │ │ ├── SymbolInstanceOverridesRow.tsx │ │ ├── SymbolLayoutRow.tsx │ │ ├── SymbolMasterOverrideRow.tsx │ │ ├── SymbolSourceRow.tsx │ │ ├── TextLayoutRow.tsx │ │ ├── TextOptionsRow.tsx │ │ └── TextStyleRow.tsx │ │ ├── hooks │ │ └── usePreviewLayer.ts │ │ └── index.ts ├── noya-keymap │ ├── README.md │ ├── package.json │ └── src │ │ ├── __tests__ │ │ ├── keyMap.test.ts │ │ ├── names.test.ts │ │ ├── platform.test.ts │ │ └── shortcuts.test.ts │ │ ├── events.ts │ │ ├── hooks.ts │ │ ├── index.ts │ │ ├── keyMap.ts │ │ ├── names.ts │ │ ├── platform.ts │ │ └── shortcuts.ts ├── noya-log │ ├── package.json │ └── src │ │ └── index.ts ├── noya-pdf │ ├── README.md │ ├── package.json │ └── src │ │ └── index.ts ├── noya-public-path │ ├── README.md │ ├── package.json │ └── src │ │ └── index.ts ├── noya-react-canvaskit │ ├── README.md │ ├── package.json │ └── src │ │ ├── components │ │ ├── Group.tsx │ │ ├── Image.tsx │ │ ├── Path.tsx │ │ ├── Polyline.tsx │ │ ├── Rect.tsx │ │ └── Text.tsx │ │ ├── filters │ │ └── ImageFilter.ts │ │ ├── hooks │ │ ├── useBlurMaskFilter.ts │ │ ├── useColor.ts │ │ ├── useDeletable.ts │ │ ├── useFill.ts │ │ ├── usePaint.ts │ │ ├── useRect.ts │ │ ├── useStable4ElementArray.ts │ │ └── useStroke.ts │ │ ├── index.ts │ │ ├── reconciler.tsx │ │ ├── types.ts │ │ └── utils │ │ └── makePath.ts ├── noya-react-utils │ ├── README.md │ ├── package.json │ └── src │ │ ├── __tests__ │ │ ├── useDeepArray.test.ts │ │ ├── useMutableState.test.ts │ │ └── useShallowArray.test.ts │ │ ├── components │ │ ├── AutoSizer.tsx │ │ └── FileDropTarget.tsx │ │ ├── hooks │ │ ├── useDeepArray.ts │ │ ├── useFetch.ts │ │ ├── useFileDropTarget.ts │ │ ├── useIsMounted.ts │ │ ├── useLazyValue.ts │ │ ├── useMutableState.ts │ │ ├── usePixelRatio.ts │ │ ├── useResource.ts │ │ ├── useShallowArray.ts │ │ ├── useSize.ts │ │ ├── useSystemColorScheme.ts │ │ └── useUrlHashParameters.ts │ │ ├── index.ts │ │ └── utils │ │ ├── PromiseState.ts │ │ ├── SuspendedValue.ts │ │ └── fetchData.ts ├── noya-renderer │ ├── README.md │ ├── package.json │ └── src │ │ ├── ClippedLayerContext.tsx │ │ ├── ComponentsContext.tsx │ │ ├── FontManagerContext.tsx │ │ ├── ImageCache.tsx │ │ ├── RenderingModeContext.tsx │ │ ├── RootScaleContext.tsx │ │ ├── ZoomContext.tsx │ │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ ├── guides.test.ts.snap │ │ │ └── index.test.ts.snap │ │ ├── guides.test.ts │ │ └── index.test.ts │ │ ├── colorMatrix.ts │ │ ├── components │ │ ├── AreaMeasurementLabel.tsx │ │ ├── BoundingRect.tsx │ │ ├── Design.tsx │ │ ├── DistanceMeasurementLabel.tsx │ │ ├── DragHandles.tsx │ │ ├── EditablePath.tsx │ │ ├── GradientEditor.tsx │ │ ├── Guides.tsx │ │ ├── HoverOutline.tsx │ │ ├── InsertPointOverlay.tsx │ │ ├── LayerPreview.tsx │ │ ├── Marquee.tsx │ │ ├── Oval.tsx │ │ ├── PixelGrid.tsx │ │ ├── PseudoPathLine.tsx │ │ ├── PseudoPoint.tsx │ │ ├── RotatedBoundingRect.tsx │ │ ├── Rulers.tsx │ │ ├── SnapGuides.tsx │ │ ├── effects │ │ │ ├── BlurGroup.tsx │ │ │ ├── ColorControlsGroup.tsx │ │ │ ├── DropShadowGroup.tsx │ │ │ └── SketchBorder.tsx │ │ └── layers │ │ │ ├── SketchArtboard.tsx │ │ │ ├── SketchBitmap.tsx │ │ │ ├── SketchGroup.tsx │ │ │ ├── SketchLayer.tsx │ │ │ ├── SketchShape.tsx │ │ │ ├── SketchSlice.tsx │ │ │ ├── SketchSymbolInstance.tsx │ │ │ ├── SketchText.tsx │ │ │ └── types.ts │ │ ├── context.ts │ │ ├── guides.ts │ │ ├── hooks │ │ ├── useCanvasKit.tsx │ │ ├── useCanvasRect.ts │ │ ├── useCheckeredFill.ts │ │ ├── useCompileShader.tsx │ │ ├── useDotFill.ts │ │ ├── useLayerFrameRect.ts │ │ ├── useLayerPath.ts │ │ ├── useRootScaleTransform.tsx │ │ └── useTintColorFilter.tsx │ │ ├── index.ts │ │ ├── loadCanvasKit.ts │ │ ├── pixelAlignment.ts │ │ ├── shaders.ts │ │ └── utils │ │ └── getCombinedLayerPaths.ts ├── noya-sketch-file │ ├── README.md │ ├── package.json │ └── src │ │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ └── index.test.ts.snap │ │ ├── fixtures │ │ │ └── Rectangle.sketch │ │ └── index.test.ts │ │ └── index.ts ├── noya-sketch-model │ ├── README.md │ ├── package.json │ └── src │ │ ├── PointString.ts │ │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ └── index.test.ts.snap │ │ └── index.test.ts │ │ ├── debugDescription.ts │ │ └── index.ts ├── noya-state │ ├── README.md │ ├── package.json │ └── src │ │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ ├── path.test.ts.snap │ │ │ └── snapping.test.ts.snap │ │ ├── editableStyles.test.ts │ │ ├── path.test.ts │ │ ├── primitives.test.ts │ │ └── snapping.test.ts │ │ ├── actions.ts │ │ ├── checkeredBackground.ts │ │ ├── editableStyles.ts │ │ ├── exportOptions.ts │ │ ├── groupLayouts.ts │ │ ├── index.ts │ │ ├── layer.ts │ │ ├── layers.ts │ │ ├── overrides.ts │ │ ├── primitives.ts │ │ ├── primitives │ │ ├── path.ts │ │ └── pathCommand.ts │ │ ├── reducers │ │ ├── __tests__ │ │ │ ├── __snapshots__ │ │ │ │ ├── canvasReducer.test.ts.snap │ │ │ │ ├── layerPropertyReducer.test.ts.snap │ │ │ │ └── layerReducer.test.ts.snap │ │ │ ├── canvasReducer.test.ts │ │ │ ├── layerPropertyReducer.test.ts │ │ │ ├── layerReducer.test.ts │ │ │ └── pageReducer.test.ts │ │ ├── alignmentReducer.ts │ │ ├── applicationReducer.ts │ │ ├── blockReducer.ts │ │ ├── blurReducer.ts │ │ ├── canvasReducer.ts │ │ ├── colorControlsReducer.ts │ │ ├── exportReducer.ts │ │ ├── gradientReducer.ts │ │ ├── historyReducer.ts │ │ ├── interactionReducer.ts │ │ ├── layerPropertyReducer.ts │ │ ├── layerReducer.ts │ │ ├── pageReducer.ts │ │ ├── pointReducer.ts │ │ ├── shaderReducer.ts │ │ ├── stringAttributeReducer.ts │ │ ├── styleReducer.ts │ │ ├── symbolsReducer.ts │ │ ├── textEditorReducer.ts │ │ ├── textStyleReducer.ts │ │ ├── themeReducer.ts │ │ └── workspaceReducer.ts │ │ ├── selection.ts │ │ ├── selectors │ │ ├── __tests__ │ │ │ ├── __snapshots__ │ │ │ │ ├── attributedStringSelectors.test.ts.snap │ │ │ │ ├── gradientSelectors.test.ts.snap │ │ │ │ └── layerSelectors.test.ts.snap │ │ │ ├── attributedStringSelectors.test.ts │ │ │ ├── geometrySelectors.test.ts │ │ │ ├── gradientSelectors.test.ts │ │ │ └── layerSelectors.test.ts │ │ ├── attributedStringSelectors.ts │ │ ├── elementSelectors.ts │ │ ├── geometrySelectors.ts │ │ ├── gradientSelectors.ts │ │ ├── index.ts │ │ ├── indexPathSelectors.ts │ │ ├── layerSelectors.ts │ │ ├── overridesSelectors.ts │ │ ├── pageSelectors.ts │ │ ├── pointSelectors.ts │ │ ├── styleSelectors.ts │ │ ├── textEditorSelectors.ts │ │ ├── textSelectors.ts │ │ ├── textStyleSelectors.ts │ │ ├── themeSelectors.ts │ │ ├── transformSelectors.ts │ │ └── workspaceSelectors.ts │ │ ├── sketchFile.ts │ │ ├── snapping.ts │ │ ├── types.ts │ │ └── utils │ │ ├── getMultiNumberValue.ts │ │ ├── getMultiValue.ts │ │ ├── moveArrayItem.ts │ │ ├── radians.ts │ │ ├── selection.ts │ │ └── zoom.ts ├── noya-svg-renderer │ ├── README.md │ ├── package.json │ └── src │ │ ├── ElementIdContext.tsx │ │ ├── SVGRenderer.tsx │ │ └── index.ts ├── noya-svgkit │ ├── README.md │ ├── package.json │ └── src │ │ ├── Embind.ts │ │ ├── JSMaskFilter.ts │ │ ├── JSPaint.ts │ │ ├── JSParagraph.ts │ │ ├── JSParagraphBuilder.ts │ │ ├── JSPath.ts │ │ ├── JSShaderFactory.ts │ │ ├── SVGKit.ts │ │ ├── __tests__ │ │ └── index.test.ts │ │ ├── constants.ts │ │ ├── index.ts │ │ └── parseColor.ts ├── noya-theme-editor │ ├── package.json │ └── src │ │ ├── components │ │ ├── CanvasPreviewItem.tsx │ │ ├── ColorSwatch.tsx │ │ ├── SwatchesGrid.tsx │ │ ├── Symbol.tsx │ │ ├── SymbolsGrid.tsx │ │ ├── TextStyle.tsx │ │ ├── TextStylesGrid.tsx │ │ ├── ThemeStyle.tsx │ │ └── ThemeStylesGrid.tsx │ │ ├── index.ts │ │ └── utils │ │ ├── menuItems.ts │ │ └── themeTree.ts ├── noya-utils │ ├── README.md │ ├── package.json │ └── src │ │ ├── __tests__ │ │ ├── base64.test.ts │ │ ├── cartesianProduct.test.ts │ │ ├── chunkBy.test.ts │ │ ├── clamp.test.ts │ │ ├── clipboard.test.ts │ │ ├── delimitedPath.test.ts │ │ ├── fileType.test.ts │ │ ├── getIncrementedName.test.ts │ │ ├── groupBy.test.ts │ │ ├── interpolate.test.ts │ │ ├── invert.test.ts │ │ ├── isDeepEqual.test.ts │ │ ├── isEqualIgnoringUndefinedKeys.test.ts │ │ ├── isNumberEqual.test.ts │ │ ├── isShallowEqual.test.ts │ │ ├── lerp.test.ts │ │ ├── range.test.ts │ │ ├── rotate.test.ts │ │ ├── round.test.ts │ │ ├── sortBy.test.ts │ │ ├── sum.test.ts │ │ ├── url.test.ts │ │ ├── utf16.test.ts │ │ ├── windowsOf.test.ts │ │ └── zip.test.ts │ │ ├── base64.ts │ │ ├── cartesianProduct.ts │ │ ├── chunkBy.ts │ │ ├── clamp.ts │ │ ├── clipboard.ts │ │ ├── debounce.ts │ │ ├── delimitedPath.ts │ │ ├── fileType.ts │ │ ├── getIncrementedName.ts │ │ ├── groupBy.ts │ │ ├── index.ts │ │ ├── internal │ │ └── isEqual.ts │ │ ├── interpolate.ts │ │ ├── invert.ts │ │ ├── isDeepEqual.ts │ │ ├── isEqualIgnoringUndefinedKeys.ts │ │ ├── isNumberEqual.ts │ │ ├── isShallowEqual.ts │ │ ├── lerp.ts │ │ ├── memoize.ts │ │ ├── memoizedGetter.ts │ │ ├── partition.ts │ │ ├── range.ts │ │ ├── rotate.ts │ │ ├── round.ts │ │ ├── sortBy.ts │ │ ├── sum.ts │ │ ├── types.ts │ │ ├── unique.ts │ │ ├── upperFirst.ts │ │ ├── url.ts │ │ ├── utf16.ts │ │ ├── windowsOf.ts │ │ └── zip.ts ├── pathkit │ ├── README.md │ ├── package.json │ └── src │ │ ├── index.ts │ │ └── pathkit.js └── site │ ├── .babelrc │ ├── guidebook.d.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── public │ └── SocialCard.png │ ├── safelist.txt │ ├── searchIndex.d.ts │ ├── src │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ ├── editor.test.ts.snap │ │ │ ├── fuzzyScorer.test.ts.snap │ │ │ └── renderBlock.test.ts.snap │ │ ├── editor.test.ts │ │ ├── fuzzyScorer.test.ts │ │ ├── parse.test.ts │ │ ├── renderBlock.test.ts │ │ └── tailwind.test.ts │ ├── assets │ │ ├── ConfigureBlockText.webp │ │ ├── ConfigureBlockType.webp │ │ ├── InsertBlock.webp │ │ ├── PremiumTemplateMosaic.png │ │ ├── docs │ │ │ ├── InsertTool.png │ │ │ ├── ProjectConfiguration.png │ │ │ ├── ProjectContextMenu.png │ │ │ ├── RegionTool.png │ │ │ └── RegionToolExample.webp │ │ └── noya-icon-square.svg │ ├── ayon │ │ ├── AttributionCard.tsx │ │ ├── Content.tsx │ │ ├── DOMRenderer.tsx │ │ ├── ModalMenu.tsx │ │ ├── Panel.tsx │ │ ├── Widget.tsx │ │ ├── blocks │ │ │ ├── AvatarBlock.tsx │ │ │ ├── BoxBlock.tsx │ │ │ ├── ButtonBlock.tsx │ │ │ ├── CardBlock.tsx │ │ │ ├── CheckboxBlock.tsx │ │ │ ├── HeaderBarBlock.tsx │ │ │ ├── HeadingBlock.tsx │ │ │ ├── HeroBlock.tsx │ │ │ ├── HeroBlockV1.tsx │ │ │ ├── IconBlock.tsx │ │ │ ├── ImageBlock.tsx │ │ │ ├── InputBlock.tsx │ │ │ ├── LinkBlock.tsx │ │ │ ├── RadioBlock.tsx │ │ │ ├── SelectBlock.tsx │ │ │ ├── SidebarBlock.tsx │ │ │ ├── SignInBlock.tsx │ │ │ ├── SpacerBlock.tsx │ │ │ ├── SwitchBlock.tsx │ │ │ ├── TableBlock.tsx │ │ │ ├── TextBlock.tsx │ │ │ ├── TextareaBlock.tsx │ │ │ ├── TileCardBlock.tsx │ │ │ ├── WriteBlock.tsx │ │ │ ├── blockMetadata.ts │ │ │ ├── blockTheme.ts │ │ │ ├── blocks.ts │ │ │ ├── colors.ts │ │ │ ├── flattenPassthroughLayers.tsx │ │ │ ├── render.tsx │ │ │ ├── score.ts │ │ │ ├── symbolIds.ts │ │ │ ├── symbols.ts │ │ │ ├── tailwind.ts │ │ │ └── tailwindColors.ts │ │ ├── createAyonDocument.ts │ │ ├── editor │ │ │ ├── BlockEditor.tsx │ │ │ ├── BlockEditorV1.tsx │ │ │ ├── ControlledEditor.tsx │ │ │ ├── ElementComponent.tsx │ │ │ ├── commands.ts │ │ │ ├── resetNodes.tsx │ │ │ ├── serialization.tsx │ │ │ ├── types.ts │ │ │ └── withLayout.tsx │ │ ├── fuzzyScorer.ts │ │ ├── inferBlock.ts │ │ ├── parse.ts │ │ ├── resolve │ │ │ ├── GenerateResolver.ts │ │ │ ├── IconResolver.ts │ │ │ ├── RandomImageResolver.ts │ │ │ ├── RedirectResolver.ts │ │ │ └── resolve.ts │ │ ├── stacking.ts │ │ ├── types.ts │ │ └── useCompletionMenu.tsx │ ├── components │ │ ├── Analytics.tsx │ │ ├── AppLayout.tsx │ │ ├── Ayon.tsx │ │ ├── Interstitial.tsx │ │ ├── Logo.tsx │ │ ├── NavigationLinks.tsx │ │ ├── OnboardingAnimation.tsx │ │ ├── OptionalNoyaAPIProvider.tsx │ │ ├── ProjectMenu.tsx │ │ ├── ProjectTitle.tsx │ │ ├── Projects.tsx │ │ ├── ShareMenu.tsx │ │ ├── Subscription.tsx │ │ ├── Toolbar.tsx │ │ ├── UpgradeDialog.tsx │ │ └── UpgradeInfo.tsx │ ├── contexts │ │ ├── OnboardingContext.tsx │ │ └── ProjectContext.tsx │ ├── docs │ │ ├── BlockGrid.tsx │ │ ├── Docs.tsx │ │ ├── InteractiveBlockPreview.tsx │ │ ├── getHeadTags.tsx │ │ ├── search.ts │ │ └── socialConfig.ts │ ├── hooks │ │ ├── useOnboardingUpsellExperiment.ts │ │ └── useToggleTimer.tsx │ ├── pages │ │ ├── _app.tsx │ │ ├── _document.tsx │ │ ├── account.tsx │ │ ├── docs │ │ │ ├── blocks.mdx │ │ │ ├── blocks │ │ │ │ ├── application.mdx │ │ │ │ ├── application │ │ │ │ │ ├── header-bar.mdx │ │ │ │ │ ├── sidebar.mdx │ │ │ │ │ ├── sign-in.mdx │ │ │ │ │ └── table.mdx │ │ │ │ ├── config.json │ │ │ │ ├── elements.mdx │ │ │ │ ├── elements │ │ │ │ │ ├── avatar.mdx │ │ │ │ │ ├── box.mdx │ │ │ │ │ ├── button.mdx │ │ │ │ │ ├── checkbox.mdx │ │ │ │ │ ├── heading.mdx │ │ │ │ │ ├── icon.mdx │ │ │ │ │ ├── image.mdx │ │ │ │ │ ├── input.mdx │ │ │ │ │ ├── link.mdx │ │ │ │ │ ├── radio.mdx │ │ │ │ │ ├── select.mdx │ │ │ │ │ ├── switch.mdx │ │ │ │ │ ├── text.mdx │ │ │ │ │ ├── textarea.mdx │ │ │ │ │ └── write.mdx │ │ │ │ ├── marketing.mdx │ │ │ │ └── marketing │ │ │ │ │ ├── card.mdx │ │ │ │ │ ├── hero.mdx │ │ │ │ │ └── tile-card.mdx │ │ │ ├── config.json │ │ │ ├── index.mdx │ │ │ ├── overview.mdx │ │ │ └── overview │ │ │ │ ├── ai-generation.mdx │ │ │ │ ├── config.json │ │ │ │ ├── creating-blocks.mdx │ │ │ │ ├── design-systems.mdx │ │ │ │ ├── export.mdx │ │ │ │ ├── projects.mdx │ │ │ │ ├── region-tool.mdx │ │ │ │ ├── sharing.mdx │ │ │ │ ├── styling-blocks.mdx │ │ │ │ └── views.mdx │ │ ├── download.tsx │ │ ├── index.tsx │ │ ├── projects │ │ │ ├── [id].tsx │ │ │ └── [id] │ │ │ │ ├── duplicate.tsx │ │ │ │ └── preview.tsx │ │ └── share │ │ │ ├── [shareId].tsx │ │ │ └── [shareId] │ │ │ ├── duplicate.tsx │ │ │ └── preview.tsx │ ├── styles │ │ └── index.css │ ├── tsconfig.json │ └── utils │ │ ├── clientStorage.ts │ │ ├── cookies.ts │ │ ├── download.ts │ │ ├── measureImage.ts │ │ └── noyaClient.ts │ ├── tailwind.config.js │ └── tsconfig.json ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | max_line_length = 80 11 | trim_trailing_whitespace = true 12 | 13 | [*.{md,mdx}] 14 | max_line_length = 0 15 | trim_trailing_whitespace = false 16 | 17 | [COMMIT_EDITMSG] 18 | max_line_length = 0 19 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /packages/*/build 13 | /packages/*/dist-cjs/ 14 | /packages/*/dist-es/ 15 | /packages/*/dist-types/ 16 | 17 | # misc 18 | .DS_Store 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # noya files 29 | pathkit.js 30 | canvaskit.js 31 | -------------------------------------------------------------------------------- /.github/workflows/build-desktop.yml: -------------------------------------------------------------------------------- 1 | name: Build Desktop App 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | 9 | strategy: 10 | matrix: 11 | os: [macos-latest, windows-latest] 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@main 16 | with: 17 | node-version: 16.18.1 18 | - run: yarn 19 | - run: | 20 | yarn build:desktop ${{ matrix.os == 'macos-latest' && '--arch=universal' || '' }} 21 | -------------------------------------------------------------------------------- /.github/workflows/build-site.yml: -------------------------------------------------------------------------------- 1 | name: Build Site 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-node@main 12 | with: 13 | node-version: 16.18.1 14 | - run: yarn 15 | - run: yarn build:site 16 | -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yml: -------------------------------------------------------------------------------- 1 | name: Unit tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | container: 10 | image: node:16.18.1 11 | env: 12 | CI: true 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - run: yarn 17 | - run: yarn test 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /packages/*/build 13 | /packages/*/dist-cjs/ 14 | /packages/*/dist-es/ 15 | /packages/*/dist-types/ 16 | /packages/*/.next 17 | /packages/*/out 18 | 19 | # misc 20 | .DS_Store 21 | .env.local 22 | .env.development.local 23 | .env.test.local 24 | .env.production.local 25 | 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | dist 31 | 32 | # custom 33 | .env 34 | .eslintcache -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint-staged 5 | 6 | echo "\nChecking TypeScript types...\n" 7 | npx tsc --noEmit 8 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16.18.1 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | useHover.ts 2 | packages/noya-colorpicker 3 | packages/canvaskit/src/index.ts 4 | canvaskit.js 5 | pathkit.js 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "wayou.vscode-todo-highlight" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /docs/assets/noya-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/docs/assets/noya-screenshot.png -------------------------------------------------------------------------------- /modular/setupEnvironment.ts: -------------------------------------------------------------------------------- 1 | // Allows for adding setup configuration to Jest 2 | export {}; 3 | -------------------------------------------------------------------------------- /packages/README.md: -------------------------------------------------------------------------------- 1 | This will be the readme inside /packages 2 | -------------------------------------------------------------------------------- /packages/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@dnd-kit/core": "^3.1.1", 7 | "@dnd-kit/modifiers": "^3.0.0", 8 | "@dnd-kit/sortable": "^4.0.0", 9 | "@radix-ui/react-aspect-ratio": "^1.0.1", 10 | "browser-fs-access": "^0.17.2", 11 | "kiwi.js": "^1.1.2", 12 | "react-use-gesture": "^9.1.3", 13 | "styled-components": "^5.2.1", 14 | "tree-visit": "^0.1.3" 15 | }, 16 | "browserslist": { 17 | "production": [ 18 | ">0.2%", 19 | "not dead", 20 | "not op_mini all" 21 | ], 22 | "development": [ 23 | "last 1 chrome version", 24 | "last 1 firefox version", 25 | "last 1 safari version" 26 | ] 27 | }, 28 | "modular": { 29 | "type": "app" 30 | }, 31 | "devDependencies": { 32 | "@types/resize-observer-browser": "^0.1.5", 33 | "@types/styled-components": "^5.1.7" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/app/public/AlphaMasks.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/AlphaMasks.sketch -------------------------------------------------------------------------------- /packages/app/public/Artboard.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/Artboard.sketch -------------------------------------------------------------------------------- /packages/app/public/BackdropFilter.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/BackdropFilter.sketch -------------------------------------------------------------------------------- /packages/app/public/BooleanOperations.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/BooleanOperations.sketch -------------------------------------------------------------------------------- /packages/app/public/BooleanOperationsAdvanced.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/BooleanOperationsAdvanced.sketch -------------------------------------------------------------------------------- /packages/app/public/ColorAsset.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/ColorAsset.sketch -------------------------------------------------------------------------------- /packages/app/public/Demo.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/Demo.sketch -------------------------------------------------------------------------------- /packages/app/public/ExportOptions.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/ExportOptions.sketch -------------------------------------------------------------------------------- /packages/app/public/FixedRadius.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/FixedRadius.sketch -------------------------------------------------------------------------------- /packages/app/public/Gradient.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/Gradient.sketch -------------------------------------------------------------------------------- /packages/app/public/Gradients.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/Gradients.sketch -------------------------------------------------------------------------------- /packages/app/public/GroupedStyles.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/GroupedStyles.sketch -------------------------------------------------------------------------------- /packages/app/public/GroupedSwatches.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/GroupedSwatches.sketch -------------------------------------------------------------------------------- /packages/app/public/Image.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/Image.sketch -------------------------------------------------------------------------------- /packages/app/public/ImageFills.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/ImageFills.sketch -------------------------------------------------------------------------------- /packages/app/public/InnerShadows.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/InnerShadows.sketch -------------------------------------------------------------------------------- /packages/app/public/Masks.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/Masks.sketch -------------------------------------------------------------------------------- /packages/app/public/MultipleStyles.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/MultipleStyles.sketch -------------------------------------------------------------------------------- /packages/app/public/Oval.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/Oval.sketch -------------------------------------------------------------------------------- /packages/app/public/PDF.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/PDF.sketch -------------------------------------------------------------------------------- /packages/app/public/Pages.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/Pages.sketch -------------------------------------------------------------------------------- /packages/app/public/Path.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/Path.sketch -------------------------------------------------------------------------------- /packages/app/public/Polygons.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/Polygons.sketch -------------------------------------------------------------------------------- /packages/app/public/Rectangle.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/Rectangle.sketch -------------------------------------------------------------------------------- /packages/app/public/Rotation.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/Rotation.sketch -------------------------------------------------------------------------------- /packages/app/public/SamplePath.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/SamplePath.sketch -------------------------------------------------------------------------------- /packages/app/public/SavedGradient.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/SavedGradient.sketch -------------------------------------------------------------------------------- /packages/app/public/Scaling.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/Scaling.sketch -------------------------------------------------------------------------------- /packages/app/public/Shaders.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/Shaders.sketch -------------------------------------------------------------------------------- /packages/app/public/Shadows.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/Shadows.sketch -------------------------------------------------------------------------------- /packages/app/public/ShapeGroup.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/ShapeGroup.sketch -------------------------------------------------------------------------------- /packages/app/public/SharedStyles.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/SharedStyles.sketch -------------------------------------------------------------------------------- /packages/app/public/Slices.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/Slices.sketch -------------------------------------------------------------------------------- /packages/app/public/Symbols NoCanOverride.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/Symbols NoCanOverride.sketch -------------------------------------------------------------------------------- /packages/app/public/Symbols.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/Symbols.sketch -------------------------------------------------------------------------------- /packages/app/public/TextLayers.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/TextLayers.sketch -------------------------------------------------------------------------------- /packages/app/public/TextStyles.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/TextStyles.sketch -------------------------------------------------------------------------------- /packages/app/public/Tints.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/Tints.sketch -------------------------------------------------------------------------------- /packages/app/public/WindingRule.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/WindingRule.sketch -------------------------------------------------------------------------------- /packages/app/public/_headers: -------------------------------------------------------------------------------- 1 | /wasm/* 2 | Access-Control-Allow-Origin: * 3 | 4 | /fonts/* 5 | Access-Control-Allow-Origin: * 6 | -------------------------------------------------------------------------------- /packages/app/public/fonts/roboto/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/fonts/roboto/Roboto-Regular.ttf -------------------------------------------------------------------------------- /packages/app/public/line.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/line.sketch -------------------------------------------------------------------------------- /packages/app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Noya", 3 | "name": "Noya - The open interface design tool", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /packages/app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /packages/app/public/wasm/canvaskit.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/wasm/canvaskit.wasm -------------------------------------------------------------------------------- /packages/app/public/wasm/pathkit.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/public/wasm/pathkit.wasm -------------------------------------------------------------------------------- /packages/app/src/__tests__/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from '../App'; 4 | 5 | test('Pages', () => { 6 | const { getByText } = render( 7 | 8 | 9 | , 10 | ); 11 | const linkElement = getByText('Loading...'); 12 | expect(linkElement).toBeInTheDocument(); 13 | }); 14 | -------------------------------------------------------------------------------- /packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-alpha-masks-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-alpha-masks-1-snap.png -------------------------------------------------------------------------------- /packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-backdrop-filter-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-backdrop-filter-1-snap.png -------------------------------------------------------------------------------- /packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-boolean-operations-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-boolean-operations-1-snap.png -------------------------------------------------------------------------------- /packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-boolean-operations-advanced-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-boolean-operations-advanced-1-snap.png -------------------------------------------------------------------------------- /packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-demo-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-demo-1-snap.png -------------------------------------------------------------------------------- /packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-gradient-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-gradient-1-snap.png -------------------------------------------------------------------------------- /packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-gradient-editor-linear-gradient-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-gradient-editor-linear-gradient-1-snap.png -------------------------------------------------------------------------------- /packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-gradient-editor-radial-gradient-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-gradient-editor-radial-gradient-1-snap.png -------------------------------------------------------------------------------- /packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-image-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-image-1-snap.png -------------------------------------------------------------------------------- /packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-image-fills-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-image-fills-1-snap.png -------------------------------------------------------------------------------- /packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-inner-shadows-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-inner-shadows-1-snap.png -------------------------------------------------------------------------------- /packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-line-editor-selected-line-in-artboard-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-line-editor-selected-line-in-artboard-1-snap.png -------------------------------------------------------------------------------- /packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-masks-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-masks-1-snap.png -------------------------------------------------------------------------------- /packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-rotation-0-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-rotation-0-1-snap.png -------------------------------------------------------------------------------- /packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-rotation-1-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-rotation-1-1-snap.png -------------------------------------------------------------------------------- /packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-rotation-2-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-rotation-2-1-snap.png -------------------------------------------------------------------------------- /packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-rotation-3-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-rotation-3-1-snap.png -------------------------------------------------------------------------------- /packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-rotation-4-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-rotation-4-1-snap.png -------------------------------------------------------------------------------- /packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-sample-path-0-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-sample-path-0-1-snap.png -------------------------------------------------------------------------------- /packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-sample-path-1-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-sample-path-1-1-snap.png -------------------------------------------------------------------------------- /packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-sample-path-2-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-sample-path-2-1-snap.png -------------------------------------------------------------------------------- /packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-sample-path-3-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-sample-path-3-1-snap.png -------------------------------------------------------------------------------- /packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-sample-path-4-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-sample-path-4-1-snap.png -------------------------------------------------------------------------------- /packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-sample-path-5-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-sample-path-5-1-snap.png -------------------------------------------------------------------------------- /packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-shaders-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-shaders-1-snap.png -------------------------------------------------------------------------------- /packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-shadows-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-shadows-1-snap.png -------------------------------------------------------------------------------- /packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-symbols-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-symbols-1-snap.png -------------------------------------------------------------------------------- /packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-symbols-2-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-symbols-2-1-snap.png -------------------------------------------------------------------------------- /packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-text-layers-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-text-layers-1-snap.png -------------------------------------------------------------------------------- /packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-tints-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-tints-1-snap.png -------------------------------------------------------------------------------- /packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-winding-rule-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/app/src/__tests__/__image_snapshots__/renderer-test-tsx-winding-rule-1-snap.png -------------------------------------------------------------------------------- /packages/app/src/hooks/useFileManager.ts: -------------------------------------------------------------------------------- 1 | import { fileOpen, fileSave } from 'browser-fs-access'; 2 | import { fileManager } from 'noya-embedded'; 3 | import { useMemo } from 'react'; 4 | import { useEnvironmentParameter } from './useEnvironmentParameters'; 5 | 6 | export function useFileManager() { 7 | const isElectron = useEnvironmentParameter('isElectron'); 8 | 9 | return useMemo( 10 | () => ({ 11 | open: isElectron ? fileManager.open : fileOpen, 12 | save: isElectron ? fileManager.save : fileSave, 13 | }), 14 | [isElectron], 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /packages/app/src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 8 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 9 | sans-serif; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | text-rendering: optimizelegibility; 13 | height: 100vh; 14 | width: 100vw; 15 | overflow: hidden; 16 | display: flex; 17 | flex-direction: row; 18 | align-items: stretch; 19 | 20 | /* Disable 2-finger swipe-to-navigate-back gesture */ 21 | overscroll-behavior-x: none; 22 | } 23 | 24 | #root { 25 | flex: 1; 26 | display: flex; 27 | flex-direction: row; 28 | } 29 | 30 | code { 31 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 32 | monospace; 33 | } 34 | -------------------------------------------------------------------------------- /packages/app/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { StrictMode, Suspense } from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | 4 | import App from './App'; 5 | import './index.css'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | 11 | 12 | , 13 | document.getElementById('root'), 14 | ); 15 | 16 | // Disable native context menu on non-input element 17 | document.oncontextmenu = (event: MouseEvent) => { 18 | if ( 19 | event.target instanceof HTMLInputElement || 20 | event.target instanceof HTMLTextAreaElement 21 | ) 22 | return; 23 | 24 | event.preventDefault(); 25 | 26 | // This lets us open another context menu when one is currently open. 27 | // This may only be needed if the pointer is a pen. 28 | document.body.style.pointerEvents = ''; 29 | }; 30 | -------------------------------------------------------------------------------- /packages/app/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "esnext" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/canvaskit-sandbox/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "canvaskit-sandbox", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": {}, 6 | "browserslist": { 7 | "production": [ 8 | ">0.2%", 9 | "not dead", 10 | "not op_mini all" 11 | ], 12 | "development": [ 13 | "last 1 chrome version", 14 | "last 1 firefox version", 15 | "last 1 safari version" 16 | ] 17 | }, 18 | "modular": { 19 | "type": "app" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/canvaskit-sandbox/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "CanvasKit Sandbox", 3 | "name": "CanvasKit Sandbox", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /packages/canvaskit-sandbox/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /packages/canvaskit-sandbox/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | 4 | import App from './App'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root'), 11 | ); 12 | -------------------------------------------------------------------------------- /packages/canvaskit-sandbox/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/canvaskit-sandbox/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "esnext" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/canvaskit/README.md: -------------------------------------------------------------------------------- 1 | This is where our custom canvaskit-wasm build lives. 2 | -------------------------------------------------------------------------------- /packages/canvaskit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "canvaskit", 3 | "version": "1.0.0", 4 | "main": "src/index.ts", 5 | "license": "UNLICENSED" 6 | } 7 | -------------------------------------------------------------------------------- /packages/canvaskit/src/emscriptenRequireInterop.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This fixes a bundling issue where `exports` isn't defined in the production 3 | * build, and as a result the required module doesn't load correctly. 4 | * 5 | * Since the module also looks for an amd `define` function, we can get access 6 | * to the exports that way in a production build. 7 | */ 8 | export function emscriptenRequireInterop(requireFunction: () => T) { 9 | return new Promise((resolve) => { 10 | function amdDefine(_deps: unknown[], thunk: () => T) { 11 | resolve(thunk()); 12 | } 13 | 14 | amdDefine['amd'] = true; 15 | 16 | (globalThis as any).define = amdDefine; 17 | 18 | const result = requireFunction(); 19 | 20 | // If the result is a function, call it. 21 | // Otherwise, we wait for the amdDefine to resolve. 22 | if (typeof result === 'function') { 23 | resolve(result); 24 | } 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /packages/noya-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noya-api", 3 | "private": false, 4 | "modular": { 5 | "type": "package" 6 | }, 7 | "main": "./src/index.ts", 8 | "version": "1.0.0", 9 | "dependencies": { 10 | "@legendapp/state": "^0.23.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/noya-api/src/core/error.ts: -------------------------------------------------------------------------------- 1 | type NoyaAPIErrorType = 2 | | 'unauthorized' 3 | | 'unknown' 4 | | 'internalServerError' 5 | | 'timeout'; 6 | 7 | export class NoyaAPIError extends Error { 8 | constructor(public type: NoyaAPIErrorType, message: string) { 9 | super(message); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/noya-api/src/core/localStorageClient.ts: -------------------------------------------------------------------------------- 1 | import { NoyaMemoryClient } from './memoryClient'; 2 | 3 | const STORAGE_KEY = 'noya-dev-storage'; 4 | 5 | type IStorage = Pick; 6 | 7 | const DEFAULT_STORAGE: IStorage = 8 | typeof localStorage !== 'undefined' 9 | ? localStorage 10 | : { getItem: () => null, setItem: () => {} }; 11 | 12 | export class NoyaLocalStorageClient extends NoyaMemoryClient { 13 | constructor({ storage = DEFAULT_STORAGE }: { storage?: IStorage } = {}) { 14 | const stored = storage.getItem(STORAGE_KEY); 15 | 16 | super(stored ? NoyaMemoryClient.parse(stored) : undefined, { 17 | onChange: () => storage.setItem(STORAGE_KEY, this.stringify()), 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/noya-api/src/core/throttleAsync.ts: -------------------------------------------------------------------------------- 1 | export const throttleAsync: ThrottleAsync = require('@jcoreio/async-throttle'); 2 | 3 | type ThrottleAsync = ( 4 | fn: (...args: Args) => Value | Promise, 5 | _wait?: number | null, 6 | options?: { 7 | getNextArgs?: (args0: Args, args1: Args) => Args; 8 | }, 9 | ) => { 10 | (...args: Args): Promise; 11 | cancel: () => Promise; 12 | flush: () => Promise; 13 | }; 14 | -------------------------------------------------------------------------------- /packages/noya-api/src/react/context.ts: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react'; 2 | import { NoyaClient } from '../core/client'; 3 | 4 | type NoyaAPIContextValue = NoyaClient; 5 | 6 | const NoyaAPIContext = createContext( 7 | undefined, 8 | ); 9 | 10 | export const NoyaAPIProvider = NoyaAPIContext.Provider; 11 | 12 | export function useNoyaClient() { 13 | const value = useContext(NoyaAPIContext); 14 | 15 | if (!value) { 16 | throw new Error('Missing NoyaAPIContextValue'); 17 | } 18 | 19 | return value; 20 | } 21 | 22 | export function useOptionalNoyaClient() { 23 | return useContext(NoyaAPIContext); 24 | } 25 | -------------------------------------------------------------------------------- /packages/noya-app-state-context/README.md: -------------------------------------------------------------------------------- 1 | This is a component 2 | -------------------------------------------------------------------------------- /packages/noya-app-state-context/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noya-app-state-context", 3 | "version": "1.0.0", 4 | "main": "src/index.tsx", 5 | "license": "UNLICENSED" 6 | } 7 | -------------------------------------------------------------------------------- /packages/noya-app-state-context/src/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './ApplicationStateContext'; 2 | export * from './useHistory'; 3 | export * from './useWorkspace'; 4 | -------------------------------------------------------------------------------- /packages/noya-app-state-context/src/useHistory.tsx: -------------------------------------------------------------------------------- 1 | import { useWorkspaceState } from 'noya-app-state-context'; 2 | import { useMemo } from 'react'; 3 | 4 | export function useHistory() { 5 | const state = useWorkspaceState(); 6 | const canRedo = state.history.future.length > 0; 7 | const canUndo = state.history.past.length > 0; 8 | 9 | return useMemo( 10 | () => ({ 11 | canRedo, 12 | canUndo, 13 | }), 14 | [canRedo, canUndo], 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /packages/noya-canvas-preview/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noya-canvas-preview", 3 | "private": false, 4 | "modular": { 5 | "type": "package" 6 | }, 7 | "main": "./src/index.ts", 8 | "version": "1.0.0" 9 | } 10 | -------------------------------------------------------------------------------- /packages/noya-canvas-preview/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components/CanvasViewer'; 2 | -------------------------------------------------------------------------------- /packages/noya-canvas/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noya-canvas", 3 | "private": false, 4 | "modular": { 5 | "type": "package" 6 | }, 7 | "main": "./src/index.ts", 8 | "version": "1.0.0" 9 | } 10 | -------------------------------------------------------------------------------- /packages/noya-canvas/src/components/types.ts: -------------------------------------------------------------------------------- 1 | export interface ICanvasElement { 2 | focus(): void; 3 | setPointerCapture(pointerId: number): void; 4 | releasePointerCapture(pointerId: number): void; 5 | } 6 | -------------------------------------------------------------------------------- /packages/noya-canvas/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components/Canvas'; 2 | export * from './components/CanvasKitRenderer'; 3 | export * from './components/SimpleCanvas'; 4 | export * from './hooks/useArrowKeyShortcuts'; 5 | export * from './hooks/useCopyHandler'; 6 | export * from './hooks/useLayerMenu'; 7 | export * from './hooks/useMultipleClickCount'; 8 | export * from './hooks/usePasteHandler'; 9 | export * from './interactions'; 10 | export * from './utils/convertPoint'; 11 | -------------------------------------------------------------------------------- /packages/noya-canvas/src/interactions/defaultCursor.ts: -------------------------------------------------------------------------------- 1 | import { ReactEventHandlers } from 'noya-designsystem'; 2 | import { handleActionType, InteractionState } from 'noya-state'; 3 | import { CSSProperties } from 'react'; 4 | import { InteractionAPI } from './types'; 5 | 6 | export interface DefaultCursorActions { 7 | setCursor: (cursor: CSSProperties['cursor'] | undefined) => void; 8 | } 9 | 10 | export function defaultCursorInteraction({ setCursor }: DefaultCursorActions) { 11 | return handleActionType< 12 | InteractionState, 13 | [InteractionAPI], 14 | ReactEventHandlers 15 | >({ 16 | none: (interactionState, api) => ({ 17 | onPointerMove: (event) => { 18 | setCursor(undefined); 19 | 20 | event.preventDefault(); 21 | }, 22 | }), 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /packages/noya-canvas/src/interactions/escape.ts: -------------------------------------------------------------------------------- 1 | import { ReactEventHandlers } from 'noya-designsystem'; 2 | import { handleActionType, InteractionState, SelectionType } from 'noya-state'; 3 | import { InteractionAPI } from './types'; 4 | 5 | export interface EscapeActions { 6 | selectLayer: (id: string[], selectionType?: SelectionType) => void; 7 | reset: () => void; 8 | } 9 | 10 | export function escapeInteraction(actions: EscapeActions) { 11 | return handleActionType< 12 | InteractionState, 13 | [InteractionAPI], 14 | ReactEventHandlers 15 | >({ 16 | none: (interactionState, api) => ({ 17 | onKeyDown: api.handleKeyboardEvent({ 18 | Escape: () => { 19 | actions.selectLayer([]); 20 | actions.reset(); 21 | }, 22 | }), 23 | }), 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /packages/noya-canvas/src/interactions/focus.ts: -------------------------------------------------------------------------------- 1 | import { ReactEventHandlers } from 'noya-designsystem'; 2 | import { handleActionType, InteractionState } from 'noya-state'; 3 | import { InteractionAPI } from './types'; 4 | 5 | export interface FocusActions {} 6 | 7 | export function focusInteraction(actions: FocusActions) { 8 | return handleActionType< 9 | InteractionState, 10 | [InteractionAPI], 11 | ReactEventHandlers 12 | >({ 13 | none: (interactionState, api) => ({ 14 | onPointerDown: (event) => { 15 | if (api.selectedGradient) return; 16 | 17 | api.focus?.(); 18 | }, 19 | }), 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /packages/noya-canvas/src/utils/convertPoint.ts: -------------------------------------------------------------------------------- 1 | import { AffineTransform, Point } from 'noya-geometry'; 2 | 3 | // Event coordinates are relative to (0,0), but we want them to include 4 | // the current page's zoom and offset from the origin 5 | export function convertPoint( 6 | scrollOrigin: Point, 7 | zoomValue: number, 8 | point: Point, 9 | targetCoordinateSystem: 'screen' | 'canvas', 10 | ): Point { 11 | const transform = AffineTransform.scale(1 / zoomValue).translate( 12 | -scrollOrigin.x, 13 | -scrollOrigin.y, 14 | ); 15 | 16 | switch (targetCoordinateSystem) { 17 | case 'canvas': 18 | return transform.applyTo(point); 19 | case 'screen': 20 | return transform.invert().applyTo(point); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/noya-canvas/src/utils/isMoving.ts: -------------------------------------------------------------------------------- 1 | import { Point } from 'noya-geometry'; 2 | 3 | export function isMoving( 4 | point: Point, 5 | origin: Point, 6 | zoomValue: number, 7 | ): boolean { 8 | const threshold = 2 / zoomValue; 9 | 10 | return ( 11 | Math.abs(point.x - origin.x) > threshold || 12 | Math.abs(point.y - origin.y) > threshold 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /packages/noya-colorpicker/README.md: -------------------------------------------------------------------------------- 1 | This is derived from: https://github.com/omgovich/react-colorful 2 | -------------------------------------------------------------------------------- /packages/noya-colorpicker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noya-colorpicker", 3 | "version": "1.0.0", 4 | "main": "src/index.tsx", 5 | "license": "UNLICENSED", 6 | "modular": { 7 | "type": "view" 8 | }, 9 | "dependencies": { 10 | "styled-components": "^5.2.1" 11 | }, 12 | "devDependencies": { 13 | "@types/styled-components": "^5.1.7" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/noya-colorpicker/src/contexts/ColorPickerContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react'; 2 | import { HsvaColor } from '../types'; 3 | 4 | export type ColorPickerContextValue = [ 5 | hsva: HsvaColor, 6 | onChange: ( 7 | color: Partial, 8 | ) => void, 9 | ]; 10 | 11 | const ColorPickerContext = createContext( 12 | undefined, 13 | ); 14 | 15 | export const ColorPickerProvider = ColorPickerContext.Provider; 16 | 17 | export function useColorPicker(): ColorPickerContextValue { 18 | const value = useContext(ColorPickerContext); 19 | 20 | if (!value) { 21 | throw new Error('Missing ColorPickerProvider'); 22 | } 23 | 24 | return value; 25 | } -------------------------------------------------------------------------------- /packages/noya-colorpicker/src/hooks/useEventCallback.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from 'react'; 2 | 3 | // Saves incoming handler to the ref in order to avoid "useCallback hell" 4 | function useEventCallback( 5 | handler?: (...values: T) => void, 6 | ): (...values: T) => void { 7 | const callbackRef = useRef(handler); 8 | 9 | useEffect(() => { 10 | callbackRef.current = handler; 11 | }); 12 | 13 | return useCallback( 14 | (...values: T) => callbackRef.current && callbackRef.current(...values), 15 | [], 16 | ); 17 | } 18 | 19 | export { useEventCallback }; 20 | -------------------------------------------------------------------------------- /packages/noya-colorpicker/src/hooks/useIsomorphicLayoutEffect.ts: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect, useEffect } from "react"; 2 | 3 | // React currently throws a warning when using useLayoutEffect on the server. 4 | // To get around it, we can conditionally useEffect on the server (no-op) and 5 | // useLayoutEffect in the browser. 6 | export const useIsomorphicLayoutEffect = 7 | typeof window !== "undefined" ? useLayoutEffect : useEffect; 8 | -------------------------------------------------------------------------------- /packages/noya-colorpicker/src/index.tsx: -------------------------------------------------------------------------------- 1 | import Alpha from './components/Alpha'; 2 | import Hue from './components/Hue'; 3 | import Saturation from './components/Saturation'; 4 | import Gradient from './components/Gradient'; 5 | import Pointer from './components/Pointer'; 6 | import HexColorInput from './components/HexColorInput'; 7 | import ColorPicker from './components/ColorPicker'; 8 | 9 | export { Alpha, Hue, Gradient, Pointer, HexColorInput, Saturation, ColorPicker }; 10 | 11 | export * from './components/Interactive'; 12 | export * from './utils/convert'; 13 | export * from './utils/compare'; 14 | export * from './utils/validate'; 15 | export * from './utils/interpolateRgba'; 16 | export * from './types'; 17 | -------------------------------------------------------------------------------- /packages/noya-colorpicker/src/utils/clamp.ts: -------------------------------------------------------------------------------- 1 | // Clamps a value between an upper and lower bound. 2 | // We use ternary operators because it makes the minified code 3 | // 2 times shorter then `Math.min(Math.max(a,b),c)` 4 | export const clamp = (number: number, min = 0, max = 1): number => { 5 | return number > max ? max : number < min ? min : number; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/noya-colorpicker/src/utils/format.ts: -------------------------------------------------------------------------------- 1 | export const formatClassName = (names: unknown[]): string => names.filter(Boolean).join(" "); 2 | -------------------------------------------------------------------------------- /packages/noya-colorpicker/src/utils/interpolateRgba.ts: -------------------------------------------------------------------------------- 1 | import { interpolate } from 'noya-utils'; 2 | import { RgbaColor } from '../types'; 3 | 4 | export interface GradientStopRgba { 5 | color: RgbaColor; 6 | position: number; 7 | } 8 | 9 | const RGBA_COMPONENTS = ['r', 'g', 'b', 'a'] as const 10 | 11 | export function interpolateRgba(stops: GradientStopRgba[], pos: number): RgbaColor { 12 | const color: RgbaColor = { r: 0, g: 0, b: 0, a: 1 }; 13 | 14 | if (stops.length === 0) return color 15 | 16 | const sorted = [...stops].sort((a, b) => a.position - b.position); 17 | 18 | RGBA_COMPONENTS.forEach(component => { 19 | const inputRange = sorted.map(stop => stop.position) 20 | const outputRange = sorted.map(stop => stop.color[component]) 21 | 22 | color[component] = interpolate(pos, { inputRange, outputRange }) 23 | }) 24 | 25 | return color; 26 | } -------------------------------------------------------------------------------- /packages/noya-colorpicker/src/utils/round.ts: -------------------------------------------------------------------------------- 1 | export const round = (number: number, digits = 0, base = Math.pow(10, digits)): number => { 2 | return Math.round(base * number) / base; 3 | }; 4 | -------------------------------------------------------------------------------- /packages/noya-colorpicker/src/utils/validate.ts: -------------------------------------------------------------------------------- 1 | const hex3 = /^#?[0-9A-F]{3}$/i; 2 | const hex6 = /^#?[0-9A-F]{6}$/i; 3 | 4 | export const validHex = (color: string): boolean => hex6.test(color) || hex3.test(color); 5 | -------------------------------------------------------------------------------- /packages/noya-compiler/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noya-compiler", 3 | "private": false, 4 | "modular": { 5 | "type": "package" 6 | }, 7 | "main": "./src/index.ts", 8 | "version": "1.0.0" 9 | } 10 | -------------------------------------------------------------------------------- /packages/noya-designsystem/README.md: -------------------------------------------------------------------------------- 1 | This is a regular package 2 | -------------------------------------------------------------------------------- /packages/noya-designsystem/src/components/Chip.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Chip = styled.span<{ variant: 'primary' | 'secondary' }>( 4 | ({ theme, variant }) => ({ 5 | ...theme.textStyles.label, 6 | lineHeight: 'inherit', 7 | padding: '4px', 8 | borderRadius: 4, 9 | userSelect: 'none', 10 | ...(variant === 'primary' && { 11 | color: theme.colors.primary, 12 | background: 'rgb(238, 229, 255)', 13 | }), 14 | ...(variant === 'secondary' && { 15 | color: theme.colors.secondary, 16 | background: 'rgb(205, 238, 231)', 17 | }), 18 | }), 19 | ); 20 | -------------------------------------------------------------------------------- /packages/noya-designsystem/src/hooks/useObjectURL.ts: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect, useMemo } from 'react'; 2 | 3 | export function useObjectURL(object?: ArrayBuffer | Uint8Array | Blob) { 4 | const objectURL = useMemo(() => { 5 | if (object instanceof Blob) return URL.createObjectURL(object); 6 | 7 | const bytes = 8 | object instanceof Uint8Array 9 | ? object 10 | : object !== undefined 11 | ? new Uint8Array(object) 12 | : new Uint8Array(); 13 | 14 | return URL.createObjectURL(new Blob([bytes])); 15 | }, [object]); 16 | 17 | useLayoutEffect(() => { 18 | return () => URL.revokeObjectURL(objectURL); 19 | }, [objectURL]); 20 | 21 | return objectURL; 22 | } 23 | -------------------------------------------------------------------------------- /packages/noya-designsystem/src/hooks/usePlatform.ts: -------------------------------------------------------------------------------- 1 | import { useDesignSystemConfiguration } from '../contexts/DesignSystemConfiguration'; 2 | 3 | export function usePlatform() { 4 | return useDesignSystemConfiguration().platform; 5 | } 6 | 7 | /** 8 | * Either ctrl or meta, depending on the platform 9 | */ 10 | export function usePlatformModKey(): 'ctrlKey' | 'metaKey' { 11 | const platform = useDesignSystemConfiguration().platform; 12 | return platform === 'mac' ? 'metaKey' : 'ctrlKey'; 13 | } 14 | -------------------------------------------------------------------------------- /packages/noya-designsystem/src/mediaQuery.ts: -------------------------------------------------------------------------------- 1 | export const size = { 2 | medium: '800px', 3 | large: '1280px', 4 | xlarge: '1550px', 5 | xxlarge: '1680px', 6 | }; 7 | 8 | export const mediaQuery = { 9 | small: `@media (max-width: ${size.medium})`, 10 | medium: `@media (max-width: ${size.large}) and (min-width: ${size.medium})`, 11 | large: `@media (max-width: ${size.xlarge}) and (min-width: ${size.large})`, 12 | xlarge: `@media (min-width: ${size.xlarge})`, 13 | }; 14 | -------------------------------------------------------------------------------- /packages/noya-designsystem/src/theme/index.ts: -------------------------------------------------------------------------------- 1 | import * as lightTheme from './light'; 2 | 3 | export type Theme = typeof lightTheme; 4 | 5 | declare module 'styled-components' { 6 | export interface DefaultTheme extends Theme {} 7 | } 8 | 9 | export interface Colors {} 10 | 11 | type PickByValue = Pick< 12 | T, 13 | // Remove any value that extends V 14 | { [K in keyof T]-?: T[K] extends V ? K : never }[keyof T] 15 | >; 16 | 17 | export type ThemeColorName = keyof PickByValue; 18 | -------------------------------------------------------------------------------- /packages/noya-designsystem/src/utils/handleNudge.ts: -------------------------------------------------------------------------------- 1 | // Given a KeyboardEvent, returns the nudge amount 2 | export default function handleNudge(e: { 3 | key: string; 4 | shiftKey: boolean; 5 | altKey: boolean; 6 | }): number | undefined { 7 | let handled = false; 8 | let amount = 0; 9 | 10 | switch (e.key) { 11 | case 'ArrowUp': 12 | amount = 1; 13 | handled = true; 14 | break; 15 | case 'ArrowDown': 16 | amount = -1; 17 | handled = true; 18 | break; 19 | } 20 | 21 | if (!handled) return; 22 | 23 | if (e.shiftKey) { 24 | amount *= 10; 25 | } else if (e.altKey) { 26 | amount *= 0.1; 27 | } 28 | 29 | return handled ? amount : undefined; 30 | } 31 | -------------------------------------------------------------------------------- /packages/noya-designsystem/src/utils/mouseEvent.ts: -------------------------------------------------------------------------------- 1 | export function isLeftButtonClicked(event: React.MouseEvent) { 2 | return event.button === 0; 3 | } 4 | 5 | export function isRightButtonClicked(event: React.MouseEvent) { 6 | return event.button === 2; 7 | } 8 | -------------------------------------------------------------------------------- /packages/noya-designsystem/src/utils/sketchPattern.ts: -------------------------------------------------------------------------------- 1 | import Sketch from 'noya-file-format'; 2 | 3 | export const SUPPORTED_IMAGE_UPLOAD_TYPES = [ 4 | 'image/png' as const, 5 | 'image/jpeg' as const, 6 | 'image/webp' as const, 7 | 'application/pdf' as const, 8 | 'image/svg+xml' as const, 9 | ]; 10 | 11 | export const SUPPORTED_CANVAS_UPLOAD_TYPES = [ 12 | ...SUPPORTED_IMAGE_UPLOAD_TYPES, 13 | '' as const, 14 | ]; 15 | 16 | export type SupportedImageUploadType = 17 | typeof SUPPORTED_IMAGE_UPLOAD_TYPES[number]; 18 | 19 | export type SupportedCanvasUploadType = 20 | typeof SUPPORTED_CANVAS_UPLOAD_TYPES[number]; 21 | 22 | export type SketchPattern = { 23 | // This _class doesn't exist in Sketch, but it's convenient in `switch` 24 | // statements to be able to reference _class. 25 | _class: 'pattern'; 26 | image?: Sketch.FileRef | Sketch.DataRef; 27 | patternFillType: Sketch.PatternFillType; 28 | patternTileScale: number; 29 | }; 30 | -------------------------------------------------------------------------------- /packages/noya-designsystem/src/utils/withSeparatorElements.ts: -------------------------------------------------------------------------------- 1 | import { Children, cloneElement, isValidElement, ReactNode } from 'react'; 2 | 3 | function createKey(key: string | number) { 4 | return `s-${key}`; 5 | } 6 | 7 | export default function withSeparatorElements( 8 | elements: ReactNode, 9 | separator: ReactNode | (() => ReactNode), 10 | ) { 11 | const childrenArray = Children.toArray(elements); 12 | 13 | for (let i = childrenArray.length - 1; i > 0; i--) { 14 | let sep = 15 | typeof separator === 'function' 16 | ? separator() 17 | : isValidElement(separator) 18 | ? cloneElement(separator, { key: createKey(i) }) 19 | : separator; 20 | 21 | if (isValidElement(sep) && sep.key == null) { 22 | sep = cloneElement(sep, { key: createKey(i) }); 23 | } 24 | 25 | childrenArray.splice(i, 0, sep); 26 | } 27 | 28 | return childrenArray; 29 | } 30 | -------------------------------------------------------------------------------- /packages/noya-desktop/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['../../.eslintrc.js', 'plugin:import/electron'], 3 | env: { 4 | browser: true, 5 | es6: true, 6 | node: true, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /packages/noya-desktop/README.md: -------------------------------------------------------------------------------- 1 | # Noya Desktop 2 | 3 | The Noya web app wrapped with Electron. 4 | 5 | This project uses [Electron Forge](https://www.electronforge.io/) for building 6 | and packaging the app. 7 | -------------------------------------------------------------------------------- /packages/noya-desktop/entitlements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-jit 6 | 7 | com.apple.security.cs.debugger 8 | 9 | 10 | -------------------------------------------------------------------------------- /packages/noya-desktop/src/actions/doubleClickToolbar.ts: -------------------------------------------------------------------------------- 1 | import { systemPreferences } from 'electron'; 2 | import { MessageFromEmbedded } from 'noya-embedded'; 3 | import { ActionContext } from '../types'; 4 | 5 | // Copied from Electron Fiddle (MIT) 6 | // https://github.com/electron/fiddle/blob/1af0e91b73e77dff8e19527aafe175c34182a63a/src/main/main.ts#L83 7 | export function doubleClickToolbar( 8 | data: Extract, 9 | { browserWindow }: ActionContext, 10 | ) { 11 | if (process.platform !== 'darwin') return; 12 | 13 | const doubleClickAction = systemPreferences.getUserDefault( 14 | 'AppleActionOnDoubleClick', 15 | 'string', 16 | ); 17 | 18 | if (doubleClickAction === 'Minimize') { 19 | browserWindow.minimize(); 20 | } else if (doubleClickAction === 'Maximize') { 21 | if (!browserWindow.isMaximized()) { 22 | browserWindow.maximize(); 23 | } else { 24 | browserWindow.unmaximize(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/noya-desktop/src/preload.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron'; 2 | 3 | window.addEventListener('message', (event: MessageEvent) => { 4 | ipcRenderer.send('rendererProcessMessage', event.data); 5 | }); 6 | 7 | ipcRenderer.on('mainProcessMessage', (event, data) => { 8 | window.postMessage(data, window.location.origin); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/noya-desktop/src/types.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow } from 'electron'; 2 | import { MessageFromHost, MessageFromEmbedded } from 'noya-embedded'; 3 | 4 | declare global { 5 | namespace Electron { 6 | interface IpcMain { 7 | on( 8 | type: 'rendererProcessMessage', 9 | callback: (event: IpcMainEvent, data: MessageFromEmbedded) => void, 10 | ): void; 11 | } 12 | 13 | interface WebContents extends NodeJS.EventEmitter { 14 | send(channel: 'mainProcessMessage', data: MessageFromHost): void; 15 | } 16 | 17 | interface IpcRenderer { 18 | on( 19 | type: 'mainProcessMessage', 20 | callback: (event: IpcRendererEvent, data: MessageFromHost) => void, 21 | ): void; 22 | 23 | send(channel: 'rendererProcessMessage', data: MessageFromEmbedded): void; 24 | } 25 | } 26 | } 27 | 28 | export type ActionContext = { 29 | browserWindow: BrowserWindow; 30 | sendMessage: (message: MessageFromHost) => void; 31 | }; 32 | -------------------------------------------------------------------------------- /packages/noya-desktop/src/version.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'electron'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | 5 | export function getAppVersion(): string | undefined { 6 | try { 7 | const packagePath = path.join(app.getAppPath(), 'package.json'); 8 | const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8')); 9 | return packageJson.version; 10 | } catch (e) { 11 | console.info('Failed to read app version from package.json'); 12 | return undefined; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/noya-desktop/tools/add-macos-cert.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | KEY_CHAIN=build.keychain 4 | MACOS_CERT_P12_FILE=certificate.p12 5 | 6 | # Recreate the certificate from the secure environment variable 7 | echo $MACOS_CERT_P12 | base64 --decode > $MACOS_CERT_P12_FILE 8 | 9 | #create a keychain 10 | security create-keychain -p actions $KEY_CHAIN 11 | 12 | # Make the keychain the default so identities are found 13 | security default-keychain -s $KEY_CHAIN 14 | 15 | # Unlock the keychain 16 | security unlock-keychain -p actions $KEY_CHAIN 17 | 18 | security import $MACOS_CERT_P12_FILE -k $KEY_CHAIN -P $MACOS_CERT_PASSWORD -T /usr/bin/codesign; 19 | 20 | security set-key-partition-list -S apple-tool:,apple: -s -k actions $KEY_CHAIN 21 | 22 | # remove certs 23 | rm -fr *.p12 -------------------------------------------------------------------------------- /packages/noya-desktop/webpack.renderer.config.ts: -------------------------------------------------------------------------------- 1 | import type { Configuration } from 'webpack'; 2 | 3 | import baseConfig from './webpack.main.config'; 4 | 5 | const { entry, ...rest } = baseConfig; 6 | 7 | const config: Configuration = { 8 | ...rest, 9 | externals: { 10 | fs: 'commonjs2 fs', 11 | path: 'commonjs2 path', 12 | child_process: 'commonjs2 child_process', 13 | os: 'commonjs2 os', 14 | util: 'commonjs2 util', 15 | electron: 'commonjs2 electron', 16 | }, 17 | }; 18 | 19 | export default config; 20 | -------------------------------------------------------------------------------- /packages/noya-embedded/README.md: -------------------------------------------------------------------------------- 1 | # Noya Embedded 2 | 3 | This package enables communication between the Noya app and a parent frame or 4 | electron host. 5 | -------------------------------------------------------------------------------- /packages/noya-embedded/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noya-embedded", 3 | "version": "1.0.0", 4 | "main": "src/index.ts", 5 | "license": "UNLICENSED" 6 | } 7 | -------------------------------------------------------------------------------- /packages/noya-embedded/src/index.ts: -------------------------------------------------------------------------------- 1 | import { hostApp } from './hostApp'; 2 | 3 | export * from './applicationMenu'; 4 | export * from './fileManager'; 5 | export * from './types'; 6 | 7 | export function doubleClickToolbar() { 8 | hostApp.sendMessage({ type: 'doubleClickToolbar' }); 9 | } 10 | -------------------------------------------------------------------------------- /packages/noya-file-format/README.md: -------------------------------------------------------------------------------- 1 | This is a regular package 2 | -------------------------------------------------------------------------------- /packages/noya-file-format/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noya-file-format", 3 | "version": "1.0.0", 4 | "main": "src/index.ts", 5 | "license": "UNLICENSED" 6 | } 7 | -------------------------------------------------------------------------------- /packages/noya-file-format/src/index.ts: -------------------------------------------------------------------------------- 1 | export * as default from './types'; 2 | -------------------------------------------------------------------------------- /packages/noya-filesystem/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noya-filesystem", 3 | "private": false, 4 | "modular": { 5 | "type": "package" 6 | }, 7 | "main": "./src/index.ts", 8 | "version": "1.0.0", 9 | "dependencies": { 10 | "fflate": "^0.7.4", 11 | "imfs": "^0.1.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/noya-filesystem/src/file.ts: -------------------------------------------------------------------------------- 1 | import { FileMap, unzip, zip } from './zip'; 2 | 3 | export async function toZipFile(data: FileMap, name: string) { 4 | const bytes = await zip(data); 5 | 6 | return new File([bytes], name, { type: 'application/zip' }); 7 | } 8 | 9 | export async function fromZipFile(file: File): Promise { 10 | const arrayBuffer = await file.arrayBuffer(); 11 | 12 | return unzip(new Uint8Array(arrayBuffer)); 13 | } 14 | -------------------------------------------------------------------------------- /packages/noya-filesystem/src/fileSystem.ts: -------------------------------------------------------------------------------- 1 | import { Entries, Entry } from 'imfs'; 2 | import { withOptions } from 'tree-visit'; 3 | 4 | export const FileSystem = withOptions>({ 5 | getChildren: Entries.getEntries, 6 | }); 7 | -------------------------------------------------------------------------------- /packages/noya-filesystem/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './file'; 2 | export * from './fileSystem'; 3 | export * from './volume'; 4 | export * from './zip'; 5 | -------------------------------------------------------------------------------- /packages/noya-filesystem/src/volume.ts: -------------------------------------------------------------------------------- 1 | import { Directory, Entries, Node, Volume } from 'imfs'; 2 | import { FileSystem } from './fileSystem'; 3 | import { FileMap } from './zip'; 4 | 5 | export function toVolume(zip: FileMap) { 6 | const volume = Volume.create() as Directory; 7 | 8 | for (const [filename, bytes] of Object.entries(zip)) { 9 | if (filename.endsWith('/')) { 10 | Volume.makeDirectory(volume, filename); 11 | } else { 12 | Volume.writeFile(volume, filename, bytes, { 13 | makeIntermediateDirectories: true, 14 | }); 15 | } 16 | } 17 | 18 | return volume; 19 | } 20 | 21 | export function fromVolume(volume: Node) { 22 | const zip: FileMap = {}; 23 | 24 | FileSystem.visit(Entries.createEntry('/', volume), ([filename, node]) => { 25 | if (filename === '/') return; 26 | 27 | if (node.type === 'file') { 28 | zip[filename] = node.data; 29 | } 30 | }); 31 | 32 | return zip; 33 | } 34 | -------------------------------------------------------------------------------- /packages/noya-filesystem/src/zip.ts: -------------------------------------------------------------------------------- 1 | import * as fflate from 'fflate'; 2 | 3 | export type FileMap = fflate.Unzipped; 4 | 5 | export function unzip(bytes: Uint8Array): Promise { 6 | return new Promise((resolve, reject) => { 7 | fflate.unzip(bytes, (error, zip) => { 8 | if (error) { 9 | reject(error); 10 | } else { 11 | resolve(zip); 12 | } 13 | }); 14 | }); 15 | } 16 | 17 | export function zip(zip: FileMap): Promise { 18 | return new Promise((resolve, reject) => { 19 | fflate.zip(zip, (error, bytes) => { 20 | if (error) { 21 | reject(error); 22 | } else { 23 | resolve(bytes); 24 | } 25 | }); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /packages/noya-fonts/README.md: -------------------------------------------------------------------------------- 1 | # Noya Fonts 2 | 3 | A utility for managing fonts 4 | -------------------------------------------------------------------------------- /packages/noya-fonts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noya-fonts", 3 | "version": "1.0.0", 4 | "main": "src/index.ts", 5 | "license": "UNLICENSED" 6 | } 7 | -------------------------------------------------------------------------------- /packages/noya-fonts/src/__tests__/fontDescriptor.test.ts: -------------------------------------------------------------------------------- 1 | import { FontFamilyId } from 'noya-fonts'; 2 | import { 3 | descriptorToFontId, 4 | FontDescriptor, 5 | fontIdToDescriptor, 6 | } from '../fontDescriptor'; 7 | 8 | test('encode and decodes descriptor', () => { 9 | const descriptor: FontDescriptor = { 10 | fontFamilyId: 'roboto' as FontFamilyId, 11 | fontSlant: 'italic', 12 | fontWeight: 'bold', 13 | }; 14 | 15 | const id = descriptorToFontId(descriptor); 16 | const result = fontIdToDescriptor(id); 17 | 18 | expect(descriptor).toEqual(result); 19 | }); 20 | -------------------------------------------------------------------------------- /packages/noya-fonts/src/__tests__/fontManager.test.ts: -------------------------------------------------------------------------------- 1 | import { FontFamilyId } from 'noya-fonts'; 2 | import { GoogleFontProvider } from 'noya-google-fonts'; 3 | import { FontDescriptor } from '../fontDescriptor'; 4 | import { FontManager } from '../fontManager'; 5 | 6 | const robotoDescriptor: FontDescriptor = { 7 | fontFamilyId: 'roboto' as FontFamilyId, 8 | fontSlant: 'italic', 9 | fontWeight: 'bold', 10 | }; 11 | 12 | const fontManager = new FontManager(GoogleFontProvider); 13 | 14 | test('get font family id', () => { 15 | expect(fontManager.getFontFamilyId('Roboto')).toEqual('roboto'); 16 | }); 17 | 18 | test('get font family name', () => { 19 | expect(fontManager.getFontFamilyName('roboto' as FontFamilyId)).toEqual( 20 | 'Roboto', 21 | ); 22 | }); 23 | 24 | test('get font file url', () => { 25 | expect(fontManager.getFontFileUrl(robotoDescriptor)).toEqual( 26 | 'https://fonts.gstatic.com/s/roboto/v27/KFOjCnqEu92Fr1Mu51TzBhc9AMX6lJBP.ttf', 27 | ); 28 | }); 29 | -------------------------------------------------------------------------------- /packages/noya-fonts/src/__tests__/fontTraits.test.ts: -------------------------------------------------------------------------------- 1 | import { getTraitsDisplayName } from 'noya-fonts'; 2 | 3 | test('displays trait names', () => { 4 | expect( 5 | getTraitsDisplayName({ 6 | fontSlant: 'upright', 7 | fontWeight: 'bold', 8 | }), 9 | ).toEqual('Bold'); 10 | 11 | expect( 12 | getTraitsDisplayName({ 13 | fontSlant: 'italic', 14 | fontWeight: 'bold', 15 | }), 16 | ).toEqual('Bold Italic'); 17 | 18 | expect( 19 | getTraitsDisplayName({ 20 | fontSlant: 'upright', 21 | fontWeight: 'regular', 22 | }), 23 | ).toEqual('Regular'); 24 | 25 | expect( 26 | getTraitsDisplayName({ 27 | fontSlant: 'italic', 28 | fontWeight: 'regular', 29 | }), 30 | ).toEqual('Italic'); 31 | }); 32 | -------------------------------------------------------------------------------- /packages/noya-fonts/src/fontDescriptor.ts: -------------------------------------------------------------------------------- 1 | import { FontFamilyId, FontWeight } from 'noya-fonts'; 2 | import { FontId } from './types'; 3 | 4 | export type FontSlant = 'upright' | 'italic'; 5 | 6 | export type FontDescriptor = { 7 | fontFamilyId: FontFamilyId; 8 | fontSlant: FontSlant; 9 | fontWeight: FontWeight; 10 | }; 11 | 12 | export type FontTraits = Omit; 13 | 14 | export function descriptorToFontId(descriptor: FontDescriptor): FontId { 15 | const { fontFamilyId: fontFamily, fontSlant, fontWeight } = descriptor; 16 | 17 | // Create object inline to guarantee specific property order 18 | return JSON.stringify({ fontFamily, fontSlant, fontWeight }) as FontId; 19 | } 20 | 21 | export function fontIdToDescriptor(fontId: FontId): FontDescriptor { 22 | const { fontFamily, fontSlant, fontWeight } = JSON.parse(fontId); 23 | 24 | return { fontFamilyId: fontFamily, fontSlant, fontWeight }; 25 | } 26 | -------------------------------------------------------------------------------- /packages/noya-fonts/src/fontWeight.ts: -------------------------------------------------------------------------------- 1 | export const ALL_FONT_WEIGHTS = [ 2 | 'ultralight', 3 | 'thin', 4 | 'light', 5 | 'regular', 6 | 'medium', 7 | 'semibold', 8 | 'bold', 9 | 'heavy', 10 | 'black', 11 | ]; 12 | 13 | export type FontWeight = 14 | | 'ultralight' 15 | | 'thin' 16 | | 'light' 17 | | 'regular' 18 | | 'medium' 19 | | 'semibold' 20 | | 'bold' 21 | | 'heavy' 22 | | 'black'; 23 | 24 | export function isValidFontWeight(string: string): string is FontWeight { 25 | switch (string) { 26 | case 'ultralight': 27 | case 'thin': 28 | case 'light': 29 | case 'regular': 30 | case 'medium': 31 | case 'semibold': 32 | case 'bold': 33 | case 'heavy': 34 | case 'black': 35 | return true; 36 | default: 37 | return false; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/noya-fonts/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Emitter'; 2 | export * from './fontDescriptor'; 3 | export * from './fontManager'; 4 | export * from './fontTraits'; 5 | export * from './fontWeight'; 6 | export * from './types'; 7 | 8 | export function formatFontFamilyId(fontFamily: string) { 9 | return fontFamily.toLowerCase().replace(/[ _-]/g, ''); 10 | } 11 | -------------------------------------------------------------------------------- /packages/noya-fonts/src/types.ts: -------------------------------------------------------------------------------- 1 | import { Brand } from 'noya-utils'; 2 | 3 | export type FontId = Brand; 4 | 5 | export type FontFamilyId = Brand; 6 | -------------------------------------------------------------------------------- /packages/noya-generate-image/README.md: -------------------------------------------------------------------------------- 1 | Generate an image file 2 | -------------------------------------------------------------------------------- /packages/noya-generate-image/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noya-generate-image", 3 | "version": "1.0.0", 4 | "main": "src/index.tsx", 5 | "license": "UNLICENSED" 6 | } 7 | -------------------------------------------------------------------------------- /packages/noya-geometry/README.md: -------------------------------------------------------------------------------- 1 | This is a regular package 2 | -------------------------------------------------------------------------------- /packages/noya-geometry/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noya-geometry", 3 | "version": "1.0.0", 4 | "main": "src/index.ts", 5 | "license": "UNLICENSED" 6 | } 7 | -------------------------------------------------------------------------------- /packages/noya-geometry/src/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { distance } from '../index'; 2 | 3 | test('calculate distance', () => { 4 | const a = { x: 0, y: 0 }; 5 | const b = { x: 5, y: 5 }; 6 | expect(distance(a, b)).toEqual(5 * Math.sqrt(2)); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/noya-geometry/src/getLineOrientation.ts: -------------------------------------------------------------------------------- 1 | import { Orientation, Point } from './types'; 2 | 3 | // Returns the orientation of a line, assuming the line is either vertical or horizontal 4 | export function getLineOrientation(points: [Point, Point]): Orientation { 5 | return points[0].x === points[1].x ? 'vertical' : 'horizontal'; 6 | } 7 | -------------------------------------------------------------------------------- /packages/noya-geometry/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './radians'; 3 | export * from './point'; 4 | export * from './line'; 5 | export * from './rect'; 6 | export * from './resize'; 7 | export * from './getLineOrientation'; 8 | export * from './AffineTransform'; 9 | -------------------------------------------------------------------------------- /packages/noya-geometry/src/point.ts: -------------------------------------------------------------------------------- 1 | import { round } from 'noya-utils'; 2 | import { Point } from './types'; 3 | 4 | export function distance( 5 | { x: x1, y: y1 }: Point, 6 | { x: x2, y: y2 }: Point, 7 | ): number { 8 | const a = x2 - x1; 9 | const b = y2 - y1; 10 | 11 | return Math.sqrt(a * a + b * b); 12 | } 13 | 14 | export function pointSum( 15 | { x: x1, y: y1 }: Point, 16 | { x: x2, y: y2 }: Point, 17 | ): Point { 18 | return { 19 | x: x1 + x2, 20 | y: y1 + y2, 21 | }; 22 | } 23 | 24 | export function roundPoint({ x, y }: Point, precision?: number) { 25 | return { 26 | x: round(x, precision), 27 | y: round(y, precision), 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /packages/noya-geometry/src/radians.ts: -------------------------------------------------------------------------------- 1 | export function toRadians(degrees: number) { 2 | return (degrees * Math.PI) / 180; 3 | } 4 | 5 | export function toDegrees(radians: number) { 6 | return (radians * 180) / Math.PI; 7 | } 8 | -------------------------------------------------------------------------------- /packages/noya-geometry/src/types.ts: -------------------------------------------------------------------------------- 1 | export type Point = { x: number; y: number }; 2 | 3 | export type Size = { width: number; height: number }; 4 | 5 | export type Rect = { x: number; y: number; width: number; height: number }; 6 | 7 | export type Bounds = { 8 | minX: number; 9 | midX: number; 10 | maxX: number; 11 | minY: number; 12 | midY: number; 13 | maxY: number; 14 | }; 15 | 16 | export type Insets = { 17 | top: number; 18 | right: number; 19 | bottom: number; 20 | left: number; 21 | }; 22 | 23 | export type Axis = 'x' | 'y'; 24 | 25 | export type Orientation = 'horizontal' | 'vertical'; 26 | 27 | export type ResizePosition = 28 | | 'top' 29 | | 'right top' 30 | | 'right' 31 | | 'right bottom' 32 | | 'bottom' 33 | | 'left bottom' 34 | | 'left' 35 | | 'left top'; 36 | -------------------------------------------------------------------------------- /packages/noya-google-fonts/README.md: -------------------------------------------------------------------------------- 1 | # Noya Google Fonts 2 | 3 | Utilities for working with Google Fonts. We include a pre-downloaded list of 4 | fonts so that they can be ready as soon as possible. 5 | -------------------------------------------------------------------------------- /packages/noya-google-fonts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noya-google-fonts", 3 | "version": "1.0.0", 4 | "main": "src/index.ts", 5 | "license": "UNLICENSED" 6 | } 7 | -------------------------------------------------------------------------------- /packages/noya-google-fonts/src/__tests__/variant.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | decodeGoogleFontVariant, 3 | encodeGoogleFontVariant, 4 | getGoogleFontVariantWeight, 5 | } from '../variant'; 6 | 7 | test('it should get font variant weight', () => { 8 | expect(getGoogleFontVariantWeight('100')).toEqual('ultralight'); 9 | expect(getGoogleFontVariantWeight('100italic')).toEqual('ultralight'); 10 | expect(getGoogleFontVariantWeight('regular')).toEqual('regular'); 11 | expect(getGoogleFontVariantWeight('italic')).toEqual('regular'); 12 | }); 13 | 14 | test('it should decode font variant', () => { 15 | expect(decodeGoogleFontVariant('100italic')).toEqual({ 16 | fontWeight: 'ultralight', 17 | fontSlant: 'italic', 18 | }); 19 | }); 20 | 21 | test('it should encode font variant', () => { 22 | expect(encodeGoogleFontVariant('italic', 'ultralight')).toEqual('100italic'); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/noya-google-fonts/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './provider'; 2 | export * from './types'; 3 | -------------------------------------------------------------------------------- /packages/noya-graphql-server/README.md: -------------------------------------------------------------------------------- 1 | A proof-of-concept GraphQL server 2 | -------------------------------------------------------------------------------- /packages/noya-graphql-server/Test.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/noya-graphql-server/Test.sketch -------------------------------------------------------------------------------- /packages/noya-graphql-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noya-graphql-server", 3 | "version": "1.0.0", 4 | "main": "src/index.ts", 5 | "license": "UNLICENSED", 6 | "scripts": { 7 | "clean": "rm -rf build", 8 | "build": "webpack --env production", 9 | "build:dev": "webpack --env development", 10 | "start": "node build/bundle.js", 11 | "dev": "yarn clean && yarn build:dev && yarn start" 12 | }, 13 | "dependencies": { 14 | "apollo-server": "^2.25.2", 15 | "atob": "^2.1.2", 16 | "babel-loader": "^8.2.2", 17 | "copy-webpack-plugin": "^9.0.1", 18 | "graphql": "^15.5.1", 19 | "next": "^11.0.1", 20 | "webpack": "^5.44.0", 21 | "webpack-cli": "^4.7.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/noya-icons/README.md: -------------------------------------------------------------------------------- 1 | # Noya Icons 2 | 3 | This package contains the icons used in the Noya app. 4 | 5 | Most of the icons come directly from [Radix Icons](https://icons.modulz.app/), 6 | but we've added a few of our own. 7 | -------------------------------------------------------------------------------- /packages/noya-icons/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noya-icons", 3 | "version": "1.0.0", 4 | "main": "src/index.ts", 5 | "license": "UNLICENSED", 6 | "dependencies": { 7 | "@radix-ui/react-icons": "^1.1.1" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/noya-icons/src/icons/BorderCenterIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from '@radix-ui/react-icons/dist/types'; 2 | import React, { memo } from 'react'; 3 | 4 | export const BorderCenterIcon = memo(function ({ 5 | color = 'currentColor', 6 | ...props 7 | }: IconProps) { 8 | return ( 9 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | ); 27 | }); 28 | -------------------------------------------------------------------------------- /packages/noya-icons/src/icons/BorderInsideIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from '@radix-ui/react-icons/dist/types'; 2 | import React, { memo } from 'react'; 3 | 4 | export const BorderInsideIcon = memo(function BorderInsideIcon({ 5 | color = 'currentColor', 6 | ...props 7 | }: IconProps) { 8 | return ( 9 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/noya-icons/src/icons/LineIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from '@radix-ui/react-icons/dist/types'; 2 | import React, { memo } from 'react'; 3 | 4 | export const LineIcon = memo(function LineIcon({ 5 | color = 'currentColor', 6 | ...props 7 | }: IconProps) { 8 | return ( 9 | 17 | 18 | 19 | 27 | 28 | ); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/noya-icons/src/icons/PointModeIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from '@radix-ui/react-icons/dist/types'; 2 | import React, { memo } from 'react'; 3 | 4 | export const PointModeIcon = memo(function PointModeIcon({ 5 | color = 'currentColor', 6 | ...props 7 | }: IconProps) { 8 | return ( 9 | 17 | 18 | 19 | 20 | 28 | 29 | ); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/noya-icons/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './icons/BorderCenterIcon'; 2 | export * from './icons/BorderInsideIcon'; 3 | export * from './icons/BorderOutsideIcon'; 4 | export * from './icons/FlipHorizontalIcon'; 5 | export * from './icons/FlipVerticalIcon'; 6 | export * from './icons/LineIcon'; 7 | export * from './icons/PointModeIcon'; 8 | 9 | // All icons should be imported from 'noya-icons'. We don't allow 10 | // importing radix-ui icons directly outside of this package. 11 | // 12 | // eslint-disable-next-line no-restricted-imports 13 | export * from '@radix-ui/react-icons'; 14 | -------------------------------------------------------------------------------- /packages/noya-import-svg/README.md: -------------------------------------------------------------------------------- 1 | This is a regular package 2 | -------------------------------------------------------------------------------- /packages/noya-import-svg/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noya-import-svg", 3 | "version": "1.0.0", 4 | "main": "src/index.ts", 5 | "license": "UNLICENSED", 6 | "dependencies": { 7 | "@lona/svg-model": "^3.0.0-alpha.10" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/noya-import-svg/src/__tests__/bowtie-viewbox.svg: -------------------------------------------------------------------------------- 1 | 3 | Path 3 Copy 2 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/noya-import-svg/src/__tests__/bowtie.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | Path 3 Copy 2 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /packages/noya-import-svg/src/__tests__/circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | Artboard 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/noya-inspector/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noya-inspector", 3 | "private": false, 4 | "modular": { 5 | "type": "package" 6 | }, 7 | "main": "./src/index.ts", 8 | "version": "1.0.0" 9 | } 10 | -------------------------------------------------------------------------------- /packages/noya-keymap/README.md: -------------------------------------------------------------------------------- 1 | # Noya Keymap 2 | 3 | This package implements keyboard shortcuts for Noya. It's based heavily on 4 | CodeMirror's keyboard shortcut handling: 5 | https://github.com/codemirror/view/blob/b0527d6fe66e14ac0f191ad70024a6668037f76a/src/keymap.ts 6 | (MIT) 7 | -------------------------------------------------------------------------------- /packages/noya-keymap/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noya-keymap", 3 | "version": "1.0.0", 4 | "main": "src/index.ts", 5 | "license": "UNLICENSED", 6 | "dependencies": { 7 | "w3c-keyname": "^2.2.4" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/noya-keymap/src/__tests__/keyMap.test.ts: -------------------------------------------------------------------------------- 1 | import { createKeyMap } from '../keyMap'; 2 | 3 | test('creates a keymap from an array', () => { 4 | const aCallback = () => {}; 5 | const bCallback = () => {}; 6 | expect( 7 | createKeyMap( 8 | [ 9 | ['a', aCallback], 10 | ['b', bCallback], 11 | ], 12 | 'key', 13 | ), 14 | ).toEqual({ 15 | a: aCallback, 16 | b: bCallback, 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/noya-keymap/src/__tests__/platform.test.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentPlatform } from '../platform'; 2 | 3 | test('determines the platform name', () => { 4 | expect(getCurrentPlatform()).toEqual('key'); 5 | expect(getCurrentPlatform({ platform: 'MacIntel' })).toEqual('mac'); 6 | }); 7 | -------------------------------------------------------------------------------- /packages/noya-keymap/src/events.ts: -------------------------------------------------------------------------------- 1 | import { createKeyMap, FALLTHROUGH, KeyShortcuts } from './keyMap'; 2 | import { PlatformName } from './platform'; 3 | import { getEventShortcutNames } from './shortcuts'; 4 | 5 | export const handleKeyboardEvent = ( 6 | event: KeyboardEvent, 7 | platformName: PlatformName, 8 | shortcuts: KeyShortcuts, 9 | ) => { 10 | const keyMap = createKeyMap(shortcuts, platformName); 11 | 12 | const eventShortcutNames = getEventShortcutNames(event, platformName); 13 | 14 | const matchingName = eventShortcutNames.find((name) => name in keyMap); 15 | 16 | if (!matchingName) return; 17 | 18 | const command = keyMap[matchingName]; 19 | 20 | const result = command(); 21 | 22 | if (result !== FALLTHROUGH) { 23 | event.preventDefault(); 24 | event.stopImmediatePropagation(); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /packages/noya-keymap/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './events'; 2 | export * from './hooks'; 3 | export * from './keyMap'; 4 | export { FALLTHROUGH } from './keyMap'; 5 | export * from './names'; 6 | export type { KeyModifiers } from './names'; 7 | export * from './platform'; 8 | -------------------------------------------------------------------------------- /packages/noya-keymap/src/platform.ts: -------------------------------------------------------------------------------- 1 | export type PlatformName = 'mac' | 'win' | 'linux' | 'key'; 2 | 3 | export const getCurrentPlatform = (navigator?: { 4 | platform: string; 5 | }): PlatformName => 6 | typeof navigator === 'undefined' 7 | ? 'key' 8 | : /Mac/.test(navigator.platform) 9 | ? 'mac' 10 | : /Win/.test(navigator.platform) 11 | ? 'win' 12 | : /Linux|X11/.test(navigator.platform) 13 | ? 'linux' 14 | : 'key'; 15 | -------------------------------------------------------------------------------- /packages/noya-log/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noya-log", 3 | "private": false, 4 | "modular": { 5 | "type": "package" 6 | }, 7 | "main": "./src/index.ts", 8 | "version": "1.0.0", 9 | "dependencies": { 10 | "@amplitude/analytics-browser": "^1.8.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/noya-log/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as amplitude from '@amplitude/analytics-browser'; 2 | 3 | export { amplitude }; 4 | 5 | export type ILogEvent = ( 6 | ...args: Parameters 7 | ) => void; 8 | -------------------------------------------------------------------------------- /packages/noya-pdf/README.md: -------------------------------------------------------------------------------- 1 | # noya-pdf 2 | 3 | This package handles PDF decoding using 4 | [pdf.js](https://github.com/mozilla/pdf.js). We load the library lazily to 5 | reduce bundle size. 6 | -------------------------------------------------------------------------------- /packages/noya-pdf/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noya-pdf", 3 | "version": "1.0.0", 4 | "main": "src/index.ts", 5 | "license": "UNLICENSED", 6 | "dependencies": { 7 | "pdfjs-dist": "^2.8.335" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/noya-public-path/README.md: -------------------------------------------------------------------------------- 1 | We exposed the public path for simpler dependency injection in tests and for 2 | loading from other packages that run in node 3 | -------------------------------------------------------------------------------- /packages/noya-public-path/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noya-public-path", 3 | "version": "1.0.0", 4 | "main": "src/index.ts", 5 | "license": "UNLICENSED" 6 | } 7 | -------------------------------------------------------------------------------- /packages/noya-public-path/src/index.ts: -------------------------------------------------------------------------------- 1 | // TODO: There's an import order issue. This fixes it for now 2 | import 'noya-utils'; 3 | 4 | let PUBLIC_PATH = '/'; 5 | 6 | export function getPublicPath() { 7 | return PUBLIC_PATH; 8 | } 9 | 10 | export function setPublicPath(path: string) { 11 | PUBLIC_PATH = path + '/'; 12 | } 13 | -------------------------------------------------------------------------------- /packages/noya-react-canvaskit/README.md: -------------------------------------------------------------------------------- 1 | This is a regular package 2 | -------------------------------------------------------------------------------- /packages/noya-react-canvaskit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noya-react-canvaskit", 3 | "version": "1.0.0", 4 | "main": "src/index.ts", 5 | "license": "UNLICENSED", 6 | "dependencies": { 7 | "react-reconciler": "^0.26.1" 8 | }, 9 | "devDependencies": { 10 | "@types/react-reconciler": "^0.26.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/noya-react-canvaskit/src/components/Path.tsx: -------------------------------------------------------------------------------- 1 | import { createElement, memo, useMemo } from 'react'; 2 | import usePaint from '../hooks/usePaint'; 3 | import { PathComponentProps } from '../types'; 4 | 5 | export default memo(function Path(props: PathComponentProps) { 6 | const paint = usePaint(props.paint); 7 | const elementProps: PathComponentProps = useMemo( 8 | () => ({ 9 | paint, 10 | path: props.path, 11 | }), 12 | [paint, props.path], 13 | ); 14 | 15 | return createElement('Path', elementProps); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/noya-react-canvaskit/src/components/Polyline.tsx: -------------------------------------------------------------------------------- 1 | import { Paint } from 'canvaskit'; 2 | import { Point } from 'noya-geometry'; 3 | import { useCanvasKit } from 'noya-renderer'; 4 | import React, { memo, useMemo } from 'react'; 5 | import useDeletable from '../hooks/useDeletable'; 6 | import usePaint from '../hooks/usePaint'; 7 | import makePath from '../utils/makePath'; 8 | import RCKPath from './Path'; 9 | 10 | interface PolylineProps { 11 | points: Point[]; 12 | paint: Paint; 13 | } 14 | 15 | export default memo(function Polyline(props: PolylineProps) { 16 | const CanvasKit = useCanvasKit(); 17 | const paint = usePaint(props.paint); 18 | const path = useMemo( 19 | () => makePath(CanvasKit, props.points), 20 | [CanvasKit, props.points], 21 | ); 22 | useDeletable(path); 23 | 24 | return ; 25 | }); 26 | -------------------------------------------------------------------------------- /packages/noya-react-canvaskit/src/components/Rect.tsx: -------------------------------------------------------------------------------- 1 | import { Paint } from 'canvaskit'; 2 | import { createElement, memo, useMemo } from 'react'; 3 | import usePaint from '../hooks/usePaint'; 4 | import useRect, { RectParameters } from '../hooks/useRect'; 5 | import { RectComponentProps } from '../types'; 6 | 7 | interface RectProps { 8 | rect: RectParameters; 9 | cornerRadius?: number; 10 | paint: Paint; 11 | } 12 | 13 | export default memo(function Rect(props: RectProps) { 14 | const rect = useRect(props.rect); 15 | const paint = usePaint(props.paint); 16 | 17 | const elementProps: RectComponentProps = useMemo( 18 | () => ({ 19 | rect, 20 | paint, 21 | cornerRadius: props.cornerRadius, 22 | }), 23 | [rect, paint, props.cornerRadius], 24 | ); 25 | 26 | return createElement('Rect', elementProps); 27 | }); 28 | -------------------------------------------------------------------------------- /packages/noya-react-canvaskit/src/components/Text.tsx: -------------------------------------------------------------------------------- 1 | import { Paragraph } from 'canvaskit'; 2 | import { createElement, memo, useMemo } from 'react'; 3 | import useRect, { RectParameters } from '../hooks/useRect'; 4 | import { TextComponentProps } from '../types'; 5 | 6 | interface TextProps { 7 | rect: RectParameters; 8 | paragraph: Paragraph; 9 | } 10 | 11 | export default memo(function Text(props: TextProps) { 12 | const rect = useRect(props.rect); 13 | const elementProps: TextComponentProps = useMemo( 14 | () => ({ 15 | paragraph: props.paragraph, 16 | rect, 17 | }), 18 | [props.paragraph, rect], 19 | ); 20 | 21 | return createElement('Text', elementProps); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/noya-react-canvaskit/src/filters/ImageFilter.ts: -------------------------------------------------------------------------------- 1 | import type { CanvasKit, InputColor } from 'canvaskit'; 2 | import { Point } from 'noya-geometry'; 3 | 4 | export type DropShadow = { 5 | type: 'dropShadow'; 6 | offset: Point; 7 | radius: number; 8 | color: InputColor; 9 | }; 10 | 11 | export function MakeDropShadowOnly(CanvasKit: CanvasKit, shadow: DropShadow) { 12 | const { offset, radius, color } = shadow; 13 | 14 | return CanvasKit.ImageFilter.MakeDropShadowOnly( 15 | offset.x, 16 | offset.y, 17 | radius, 18 | radius, 19 | color, 20 | null, 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /packages/noya-react-canvaskit/src/hooks/useBlurMaskFilter.ts: -------------------------------------------------------------------------------- 1 | import { BlurStyle, MaskFilter } from 'canvaskit'; 2 | import { useCanvasKit } from 'noya-renderer'; 3 | import { useMemo } from 'react'; 4 | import useDeletable from './useDeletable'; 5 | 6 | export type BlurMaskFilterParameters = { 7 | style: BlurStyle; 8 | sigma: number; 9 | respectCTM: boolean; 10 | }; 11 | 12 | export default function useBlurMaskFilter( 13 | parameters: BlurMaskFilterParameters, 14 | ): MaskFilter { 15 | const CanvasKit = useCanvasKit(); 16 | 17 | const maskFilter = useMemo( 18 | () => 19 | CanvasKit.MaskFilter.MakeBlur( 20 | parameters.style, 21 | parameters.sigma, 22 | parameters.respectCTM, 23 | ), 24 | [ 25 | CanvasKit.MaskFilter, 26 | parameters.respectCTM, 27 | parameters.sigma, 28 | parameters.style, 29 | ], 30 | ); 31 | 32 | return useDeletable(maskFilter); 33 | } 34 | -------------------------------------------------------------------------------- /packages/noya-react-canvaskit/src/hooks/useRect.ts: -------------------------------------------------------------------------------- 1 | import { Rect } from 'canvaskit'; 2 | import useStable4ElementArray from './useStable4ElementArray'; 3 | 4 | export type RectParameters = Float32Array; 5 | 6 | export default function useRect(parameters: RectParameters): Rect { 7 | return useStable4ElementArray(parameters); 8 | } 9 | -------------------------------------------------------------------------------- /packages/noya-react-canvaskit/src/hooks/useStable4ElementArray.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | 3 | export default function useStable4ElementArray( 4 | value: Float32Array, 5 | ): Float32Array; 6 | export default function useStable4ElementArray( 7 | value: Float32Array | undefined, 8 | ): Float32Array | undefined; 9 | export default function useStable4ElementArray( 10 | value: Float32Array | undefined, 11 | ): Float32Array | undefined { 12 | return useMemo( 13 | () => value, 14 | // eslint-disable-next-line react-hooks/exhaustive-deps 15 | [value?.[0], value?.[1], value?.[2], value?.[3]], 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /packages/noya-react-canvaskit/src/hooks/useStroke.ts: -------------------------------------------------------------------------------- 1 | import { Paint } from 'canvaskit'; 2 | import { useCanvasKit } from 'noya-renderer'; 3 | import { useMemo } from 'react'; 4 | import usePaint, { PaintParameters } from './usePaint'; 5 | 6 | export function useStroke(parameters: Omit): Paint { 7 | const CanvasKit = useCanvasKit(); 8 | 9 | const parametersWithStyle = useMemo( 10 | () => ({ 11 | ...parameters, 12 | style: CanvasKit.PaintStyle.Stroke, 13 | }), 14 | [CanvasKit.PaintStyle.Stroke, parameters], 15 | ); 16 | 17 | return usePaint(parametersWithStyle); 18 | } 19 | -------------------------------------------------------------------------------- /packages/noya-react-canvaskit/src/utils/makePath.ts: -------------------------------------------------------------------------------- 1 | import { CanvasKit, Path } from 'canvaskit'; 2 | import { Point } from 'noya-geometry'; 3 | 4 | export default function makePath(CanvasKit: CanvasKit, points: Point[]): Path { 5 | const path = new CanvasKit.Path(); 6 | 7 | const [first, ...rest] = points; 8 | 9 | if (!first) return path; 10 | 11 | path.moveTo(first.x, first.y); 12 | 13 | rest.forEach((point) => { 14 | path.lineTo(point.x, point.y); 15 | }); 16 | 17 | path.close(); 18 | 19 | return path; 20 | } 21 | -------------------------------------------------------------------------------- /packages/noya-react-utils/README.md: -------------------------------------------------------------------------------- 1 | # Noya React Utils 2 | 3 | A set of react utilities used throughout Noya's React components 4 | -------------------------------------------------------------------------------- /packages/noya-react-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noya-react-utils", 3 | "version": "1.0.0", 4 | "main": "src/index.ts", 5 | "license": "UNLICENSED" 6 | } 7 | -------------------------------------------------------------------------------- /packages/noya-react-utils/src/__tests__/useDeepArray.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react-hooks'; 2 | import { useDeepMemo } from '../hooks/useDeepArray'; 3 | 4 | test('returns the same array', () => { 5 | const array1 = [{ name: 'a' }]; 6 | const array2 = [{ name: 'a' }]; 7 | 8 | const { result, rerender } = renderHook(({ value }) => useDeepMemo(value), { 9 | initialProps: { value: array1 }, 10 | }); 11 | 12 | rerender({ value: array2 }); 13 | 14 | expect(result.current).toBe(array1); 15 | }); 16 | 17 | test('returns a different array', () => { 18 | const array1 = [{ name: 'a' }]; 19 | const array2 = [{ name: 'b' }]; 20 | 21 | const { result, rerender } = renderHook(({ value }) => useDeepMemo(value), { 22 | initialProps: { value: array1 }, 23 | }); 24 | 25 | rerender({ value: array2 }); 26 | 27 | expect(result.current).toBe(array2); 28 | }); 29 | -------------------------------------------------------------------------------- /packages/noya-react-utils/src/__tests__/useShallowArray.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react-hooks'; 2 | import { useShallowArray } from '../hooks/useShallowArray'; 3 | 4 | test('returns the same array', () => { 5 | const array1 = [1, 2, 3]; 6 | const array2 = [1, 2, 3]; 7 | 8 | const { result, rerender } = renderHook( 9 | ({ value }) => useShallowArray(value), 10 | { initialProps: { value: array1 } }, 11 | ); 12 | 13 | rerender({ value: array2 }); 14 | 15 | expect(result.current).toBe(array1); 16 | }); 17 | 18 | test('returns a different array', () => { 19 | const array1 = [1, 2, 3]; 20 | const array2 = [1, 2]; 21 | 22 | const { result, rerender } = renderHook( 23 | ({ value }) => useShallowArray(value), 24 | { initialProps: { value: array1 } }, 25 | ); 26 | 27 | rerender({ value: array2 }); 28 | 29 | expect(result.current).toBe(array2); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/noya-react-utils/src/components/AutoSizer.tsx: -------------------------------------------------------------------------------- 1 | import { Size } from 'noya-geometry'; 2 | import { useSize } from 'noya-react-utils'; 3 | import React, { memo, ReactNode, useRef } from 'react'; 4 | import styled from 'styled-components'; 5 | 6 | const Container = styled.div({ 7 | display: 'flex', 8 | flex: '1 0 0', 9 | flexDirection: 'column', 10 | }); 11 | 12 | interface Props { 13 | children: (size: Size) => ReactNode; 14 | } 15 | 16 | export const AutoSizer = memo(function AutoSizer({ children }: Props) { 17 | const containerRef = useRef(null); 18 | const containerSize = useSize(containerRef); 19 | 20 | return ( 21 | 22 | {containerSize && 23 | containerSize.width > 0 && 24 | containerSize.height > 0 && 25 | children(containerSize)} 26 | 27 | ); 28 | }); 29 | -------------------------------------------------------------------------------- /packages/noya-react-utils/src/hooks/useDeepArray.ts: -------------------------------------------------------------------------------- 1 | import { isDeepEqual } from 'noya-utils'; 2 | import { useRef } from 'react'; 3 | 4 | /** 5 | * Memoize an array using deep equality comparison (by converting to JSON). 6 | */ 7 | export function useDeepMemo(array: T) { 8 | const ref = useRef(array); 9 | 10 | if (!isDeepEqual(ref.current, array)) { 11 | ref.current = array; 12 | } 13 | 14 | return ref.current; 15 | } 16 | -------------------------------------------------------------------------------- /packages/noya-react-utils/src/hooks/useIsMounted.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | export function useIsMounted() { 4 | const isMounted = useRef(true); 5 | 6 | useEffect(() => { 7 | return () => { 8 | isMounted.current = false; 9 | }; 10 | }, []); 11 | 12 | return isMounted; 13 | } 14 | -------------------------------------------------------------------------------- /packages/noya-react-utils/src/hooks/useLazyValue.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | 3 | export function useLazyValue(f: () => T): T { 4 | const didInitialize = useRef(false); 5 | const value = useRef(undefined); 6 | 7 | if (!didInitialize.current) { 8 | didInitialize.current = true; 9 | 10 | value.current = f(); 11 | } 12 | 13 | return value.current!; 14 | } 15 | -------------------------------------------------------------------------------- /packages/noya-react-utils/src/hooks/usePixelRatio.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export function usePixelRatio() { 4 | const [pixelRatio, setPixelRatio] = useState(() => window.devicePixelRatio); 5 | 6 | useEffect(() => { 7 | const mediaQuery = matchMedia(`(resolution: ${pixelRatio}dppx)`); 8 | 9 | const handler = () => { 10 | setPixelRatio(window.devicePixelRatio); 11 | }; 12 | 13 | mediaQuery.addEventListener('change', handler, { once: true }); 14 | 15 | return () => { 16 | mediaQuery.removeEventListener('change', handler); 17 | }; 18 | }, [pixelRatio]); 19 | 20 | return pixelRatio; 21 | } 22 | -------------------------------------------------------------------------------- /packages/noya-react-utils/src/hooks/useResource.ts: -------------------------------------------------------------------------------- 1 | import { SuspendedValue } from 'noya-react-utils'; 2 | import { fetchData, ResponseEncoding } from '../utils/fetchData'; 3 | 4 | const resourceCache: { [key: string]: SuspendedValue } = {}; 5 | 6 | /** 7 | * Fetch JSON from a url. 8 | * 9 | * The response will be cached forever using the url as a key. 10 | * 11 | * @param url 12 | */ 13 | export function useResource(url: string, encoding: ResponseEncoding): T { 14 | if (!(url in resourceCache)) { 15 | resourceCache[url] = new SuspendedValue( 16 | (fetchData as any)(url, encoding), 17 | ); 18 | } 19 | 20 | return resourceCache[url].getValueOrThrow(); 21 | } 22 | -------------------------------------------------------------------------------- /packages/noya-react-utils/src/hooks/useShallowArray.ts: -------------------------------------------------------------------------------- 1 | import { isShallowEqual } from 'noya-utils'; 2 | import { useRef } from 'react'; 3 | 4 | /** 5 | * Memoize an array using shallow comparison. 6 | */ 7 | export function useShallowArray(array: T[]) { 8 | const ref = useRef(array); 9 | 10 | if (!isShallowEqual(ref.current, array)) { 11 | ref.current = array; 12 | } 13 | 14 | return ref.current; 15 | } 16 | -------------------------------------------------------------------------------- /packages/noya-react-utils/src/hooks/useSystemColorScheme.ts: -------------------------------------------------------------------------------- 1 | import { useLazyValue } from 'noya-react-utils'; 2 | import { useEffect, useState } from 'react'; 3 | 4 | const preferDarkQuery = '(prefers-color-scheme: dark)'; 5 | 6 | type ColorScheme = 'light' | 'dark'; 7 | 8 | export function useSystemColorScheme() { 9 | const mediaQuery = useLazyValue(() => global.matchMedia(preferDarkQuery)); 10 | 11 | const [colorScheme, setColorScheme] = useState( 12 | mediaQuery.matches ? 'dark' : 'light', 13 | ); 14 | 15 | useEffect(() => { 16 | const listener = ({ matches }: MediaQueryListEvent) => { 17 | setColorScheme(matches ? 'dark' : 'light'); 18 | }; 19 | 20 | mediaQuery.addEventListener('change', listener); 21 | 22 | return () => { 23 | mediaQuery.removeEventListener('change', listener); 24 | }; 25 | }, [mediaQuery]); 26 | 27 | return colorScheme; 28 | } 29 | -------------------------------------------------------------------------------- /packages/noya-react-utils/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components/AutoSizer'; 2 | export * from './components/FileDropTarget'; 3 | export * from './hooks/useDeepArray'; 4 | export * from './hooks/useFetch'; 5 | export * from './hooks/useFileDropTarget'; 6 | export * from './hooks/useIsMounted'; 7 | export * from './hooks/useLazyValue'; 8 | export * from './hooks/useMutableState'; 9 | export * from './hooks/usePixelRatio'; 10 | export * from './hooks/useResource'; 11 | export * from './hooks/useShallowArray'; 12 | export * from './hooks/useSize'; 13 | export * from './hooks/useSystemColorScheme'; 14 | export * from './hooks/useUrlHashParameters'; 15 | export * from './utils/fetchData'; 16 | export * from './utils/PromiseState'; 17 | export * from './utils/SuspendedValue'; 18 | -------------------------------------------------------------------------------- /packages/noya-react-utils/src/utils/PromiseState.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Imitate the internal state of a promise. 3 | * 4 | * This is useful if we want a representation of a promise-like object 5 | * that we can use synchronously. 6 | */ 7 | export type PromiseState = 8 | | { 9 | type: 'pending'; 10 | } 11 | | { 12 | type: 'success'; 13 | value: T; 14 | } 15 | | { 16 | type: 'failure'; 17 | value: Error; 18 | }; 19 | -------------------------------------------------------------------------------- /packages/noya-renderer/README.md: -------------------------------------------------------------------------------- 1 | This is a regular package 2 | -------------------------------------------------------------------------------- /packages/noya-renderer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noya-renderer", 3 | "version": "1.0.0", 4 | "main": "src/index.ts", 5 | "license": "UNLICENSED", 6 | "dependencies": { 7 | "uuid": "^8.3.2" 8 | }, 9 | "devDependencies": { 10 | "@types/uuid": "^8.3.0", 11 | "canvas": "^2.6.1" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/noya-renderer/src/ClippedLayerContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContextSelector } from 'use-context-selector'; 2 | 3 | type ClippedLayerMap = Record; 4 | 5 | /** 6 | * A context containing the ids of layers outside the visible area of the canvas. 7 | * 8 | * A layer must be explicitly marked clipped with `true`, otherwise it will be visible. 9 | * This is to make it easier to work with layers that are created ad-hoc, e.g. in symbols, 10 | * where we don't know the id ahead of time. 11 | */ 12 | const ClippedLayerContext = createContext({}); 13 | 14 | export function useIsLayerClipped(layerId: string): boolean { 15 | return useContextSelector( 16 | ClippedLayerContext, 17 | (layerIsClipped) => layerIsClipped[layerId] ?? false, 18 | ); 19 | } 20 | 21 | export const ClippedLayerProvider = ClippedLayerContext.Provider; 22 | -------------------------------------------------------------------------------- /packages/noya-renderer/src/RenderingModeContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react'; 2 | 3 | export type RenderingMode = 'static' | 'interactive'; 4 | 5 | const RenderingModeContext = createContext('static'); 6 | 7 | export const RenderingModeProvider = RenderingModeContext.Provider; 8 | 9 | export function useRenderingMode(): RenderingMode { 10 | return useContext(RenderingModeContext); 11 | } 12 | -------------------------------------------------------------------------------- /packages/noya-renderer/src/RootScaleContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react'; 2 | 3 | /** 4 | * The scale of the entire rendering surface. 5 | * 6 | * We set this to support high pixel densities. 7 | */ 8 | const RootScaleContext = createContext(1); 9 | 10 | export function useRootScale() { 11 | return useContext(RootScaleContext); 12 | } 13 | 14 | export const RootScaleProvider = RootScaleContext.Provider; 15 | -------------------------------------------------------------------------------- /packages/noya-renderer/src/ZoomContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react'; 2 | 3 | /** 4 | * The current zoom level 5 | */ 6 | const ZoomContext = createContext(1); 7 | 8 | export function useZoom() { 9 | return useContext(ZoomContext); 10 | } 11 | 12 | export const ZoomProvider = ZoomContext.Provider; 13 | -------------------------------------------------------------------------------- /packages/noya-renderer/src/__tests__/__snapshots__/guides.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`create guides 1`] = ` 4 | Object { 5 | "extension": Array [ 6 | Object { 7 | "x": 200, 8 | "y": 0, 9 | }, 10 | Object { 11 | "x": 200, 12 | "y": 0, 13 | }, 14 | ], 15 | "measurement": Array [ 16 | Object { 17 | "x": 100, 18 | "y": 50, 19 | }, 20 | Object { 21 | "x": 200, 22 | "y": 50, 23 | }, 24 | ], 25 | } 26 | `; 27 | -------------------------------------------------------------------------------- /packages/noya-renderer/src/__tests__/__snapshots__/index.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`converts color 1`] = ` 4 | Float32Array [ 5 | 0.5, 6 | 0.5, 7 | 0.5, 8 | 0.5, 9 | ] 10 | `; 11 | 12 | exports[`converts fill 1`] = ` 13 | Float32Array [ 14 | 0.28056401014328003, 15 | 0.6895309686660767, 16 | 1, 17 | 1, 18 | ] 19 | `; 20 | 21 | exports[`converts rect 1`] = ` 22 | Float32Array [ 23 | 10, 24 | 20, 25 | 40, 26 | 60, 27 | ] 28 | `; 29 | -------------------------------------------------------------------------------- /packages/noya-renderer/src/__tests__/guides.test.ts: -------------------------------------------------------------------------------- 1 | import { getGuides } from '../guides'; 2 | 3 | test('create guides', () => { 4 | expect( 5 | getGuides( 6 | '-', 7 | 'x', 8 | { x: 0, y: 0, width: 100, height: 100 }, 9 | { x: 200, y: 0, width: 100, height: 100 }, 10 | ), 11 | ).toMatchSnapshot(); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/noya-renderer/src/components/PseudoPathLine.tsx: -------------------------------------------------------------------------------- 1 | import Sketch from 'noya-file-format'; 2 | import { useStroke } from 'noya-react-canvaskit'; 3 | import { Primitives } from 'noya-state'; 4 | import React from 'react'; 5 | import { useTheme } from 'styled-components'; 6 | import { Path } from '../ComponentsContext'; 7 | import { useCanvasKit } from '../hooks/useCanvasKit'; 8 | 9 | interface EditablePathPointProps { 10 | frame: Sketch.Rect; 11 | points: Sketch.CurvePoint[]; 12 | } 13 | 14 | export default function PseudoPathLine({ 15 | points, 16 | frame, 17 | }: EditablePathPointProps) { 18 | const CanvasKit = useCanvasKit(); 19 | const { primary } = useTheme().colors; 20 | const stroke = useStroke({ color: primary }); 21 | 22 | const pseudoPath = Primitives.path(CanvasKit, points, frame, false); 23 | 24 | return ; 25 | } 26 | -------------------------------------------------------------------------------- /packages/noya-renderer/src/components/PseudoPoint.tsx: -------------------------------------------------------------------------------- 1 | import { Point } from 'noya-geometry'; 2 | import { useFill, useStroke } from 'noya-react-canvaskit'; 3 | import React from 'react'; 4 | import { useTheme } from 'styled-components'; 5 | import { useCanvasKit } from '../hooks/useCanvasKit'; 6 | import { EditablePathPoint } from './EditablePath'; 7 | 8 | interface PseudoPointProps { 9 | point: Point; 10 | } 11 | 12 | export default function PseudoPoint({ point }: PseudoPointProps) { 13 | const CanvasKit = useCanvasKit(); 14 | const { primary } = useTheme().colors; 15 | 16 | const fill = useFill({ color: CanvasKit.WHITE }); 17 | const stroke = useStroke({ color: primary }); 18 | 19 | return ; 20 | } 21 | -------------------------------------------------------------------------------- /packages/noya-renderer/src/components/effects/DropShadowGroup.tsx: -------------------------------------------------------------------------------- 1 | import Sketch from 'noya-file-format'; 2 | import { DropShadow } from 'noya-react-canvaskit'; 3 | import { Primitives } from 'noya-state'; 4 | import React, { memo, ReactNode, useMemo } from 'react'; 5 | import { Group } from '../../ComponentsContext'; 6 | import { useCanvasKit } from '../../hooks/useCanvasKit'; 7 | 8 | interface Props { 9 | shadow: Sketch.Shadow; 10 | children: ReactNode; 11 | } 12 | 13 | export default memo(function DropShadowGroup({ shadow, children }: Props) { 14 | const CanvasKit = useCanvasKit(); 15 | 16 | const imageFilter = useMemo( 17 | (): DropShadow => ({ 18 | type: 'dropShadow', 19 | color: Primitives.color(CanvasKit, shadow.color), 20 | offset: { x: shadow.offsetX, y: shadow.offsetY }, 21 | radius: shadow.blurRadius / 2, 22 | }), 23 | [CanvasKit, shadow], 24 | ); 25 | 26 | return {children}; 27 | }); 28 | -------------------------------------------------------------------------------- /packages/noya-renderer/src/components/layers/types.ts: -------------------------------------------------------------------------------- 1 | import Sketch from 'noya-file-format'; 2 | import { PageLayer } from 'noya-state'; 3 | 4 | interface Props { 5 | layer: PageLayer | Sketch.Page; 6 | } 7 | 8 | type SketchLayerType = (props: Props) => JSX.Element | null; 9 | 10 | export type BaseLayerProps = { 11 | SketchLayer: SketchLayerType; 12 | }; 13 | -------------------------------------------------------------------------------- /packages/noya-renderer/src/context.ts: -------------------------------------------------------------------------------- 1 | import { Canvas, CanvasKit } from 'canvaskit'; 2 | import { ApplicationState } from 'noya-state'; 3 | 4 | export interface Context { 5 | state: ApplicationState; 6 | CanvasKit: CanvasKit; 7 | canvas: Canvas; 8 | canvasSize: { 9 | width: number; 10 | height: number; 11 | }; 12 | theme: { 13 | textColor: string; 14 | backgroundColor: string; 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /packages/noya-renderer/src/hooks/useCanvasRect.ts: -------------------------------------------------------------------------------- 1 | import { useWorkspace } from 'noya-app-state-context'; 2 | import { useMemo } from 'react'; 3 | import { useCanvasKit } from './useCanvasKit'; 4 | 5 | export function useCanvasRect() { 6 | const { canvasSize, canvasInsets } = useWorkspace(); 7 | const CanvasKit = useCanvasKit(); 8 | const canvasRect = useMemo( 9 | () => 10 | CanvasKit.XYWHRect( 11 | canvasInsets.left, 12 | 0, 13 | canvasSize.width, 14 | canvasSize.height, 15 | ), 16 | [CanvasKit, canvasInsets.left, canvasSize.height, canvasSize.width], 17 | ); 18 | return canvasRect; 19 | } 20 | -------------------------------------------------------------------------------- /packages/noya-renderer/src/hooks/useLayerFrameRect.ts: -------------------------------------------------------------------------------- 1 | import Sketch from 'noya-file-format'; 2 | import { Primitives } from 'noya-state'; 3 | import { useMemo } from 'react'; 4 | import { useCanvasKit } from './useCanvasKit'; 5 | 6 | export default function useLayerFrameRect(layer: Sketch.AnyLayer) { 7 | const CanvasKit = useCanvasKit(); 8 | 9 | return useMemo(() => { 10 | return Primitives.rect(CanvasKit, layer.frame); 11 | }, [CanvasKit, layer]); 12 | } 13 | -------------------------------------------------------------------------------- /packages/noya-renderer/src/hooks/useRootScaleTransform.tsx: -------------------------------------------------------------------------------- 1 | import { AffineTransform } from 'noya-geometry'; 2 | import { useMemo } from 'react'; 3 | import { useRootScale } from '../RootScaleContext'; 4 | 5 | export function useRootScaleTransform() { 6 | const rootScale = useRootScale(); 7 | const rootScaleTransform = useMemo( 8 | () => AffineTransform.scale(rootScale), 9 | [rootScale], 10 | ); 11 | return rootScaleTransform; 12 | } 13 | -------------------------------------------------------------------------------- /packages/noya-renderer/src/hooks/useTintColorFilter.tsx: -------------------------------------------------------------------------------- 1 | import Sketch from 'noya-file-format'; 2 | import { useDeletable } from 'noya-react-canvaskit'; 3 | import { Primitives } from 'noya-state'; 4 | import { useMemo } from 'react'; 5 | import { useCanvasKit } from './useCanvasKit'; 6 | 7 | export function useTintColorFilter(tintColor: Sketch.Color | undefined) { 8 | const CanvasKit = useCanvasKit(); 9 | 10 | const colorFilter = useMemo(() => { 11 | return tintColor 12 | ? CanvasKit.ColorFilter.MakeBlend( 13 | Primitives.color(CanvasKit, tintColor), 14 | CanvasKit.BlendMode.SrcIn, 15 | ) 16 | : undefined; 17 | }, [CanvasKit, tintColor]); 18 | 19 | useDeletable(colorFilter); 20 | 21 | return colorFilter; 22 | } 23 | -------------------------------------------------------------------------------- /packages/noya-renderer/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Context } from './context'; 2 | export * from './colorMatrix'; 3 | export * from './components/Design'; 4 | export * from './components/LayerPreview'; 5 | export { default as SketchArtboard } from './components/layers/SketchArtboard'; 6 | export { default as SketchGroup } from './components/layers/SketchGroup'; 7 | export { default as SketchLayer } from './components/layers/SketchLayer'; 8 | export { useTextLayerParagraph } from './components/layers/SketchText'; 9 | export * from './ComponentsContext'; 10 | export * from './FontManagerContext'; 11 | export * from './hooks/useCanvasKit'; 12 | export * from './hooks/useCompileShader'; 13 | export { ImageCacheProvider, useSketchImage } from './ImageCache'; 14 | export * from './loadCanvasKit'; 15 | export * from './RenderingModeContext'; 16 | export * from './RootScaleContext'; 17 | export * from './shaders'; 18 | export * from './ZoomContext'; 19 | export type { Context }; 20 | -------------------------------------------------------------------------------- /packages/noya-sketch-file/README.md: -------------------------------------------------------------------------------- 1 | This is a regular package 2 | -------------------------------------------------------------------------------- /packages/noya-sketch-file/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noya-sketch-file", 3 | "version": "1.0.0", 4 | "main": "src/index.ts", 5 | "license": "UNLICENSED", 6 | "dependencies": { 7 | "jszip": "^3.5.0" 8 | }, 9 | "devDependencies": { 10 | "@types/jszip": "^3.4.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/noya-sketch-file/src/__tests__/fixtures/Rectangle.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/noya-sketch-file/src/__tests__/fixtures/Rectangle.sketch -------------------------------------------------------------------------------- /packages/noya-sketch-file/src/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { decode, encode } from '../index'; 4 | 5 | const sketchFile = fs.readFileSync( 6 | path.join(__dirname, 'fixtures/Rectangle.sketch'), 7 | ); 8 | 9 | test('it should decode', async () => { 10 | const decoded = await decode(sketchFile); 11 | 12 | expect(decoded).toMatchSnapshot(); 13 | }); 14 | 15 | // We decode, re-encode, and re-decode the fixture, making sure both decoded 16 | // versions match. If they do our `encode` probably works correctly. 17 | test('it should encode', async () => { 18 | const decoded = await decode(sketchFile); 19 | const encoded = await encode(decoded); 20 | const decoded2 = await decode(Buffer.from(encoded)); 21 | 22 | expect(decoded).toEqual(decoded2); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/noya-sketch-model/README.md: -------------------------------------------------------------------------------- 1 | This is a regular package 2 | -------------------------------------------------------------------------------- /packages/noya-sketch-model/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noya-sketch-model", 3 | "version": "1.0.0", 4 | "main": "src/index.ts", 5 | "license": "UNLICENSED" 6 | } 7 | -------------------------------------------------------------------------------- /packages/noya-sketch-model/src/PointString.ts: -------------------------------------------------------------------------------- 1 | import { Point } from 'noya-geometry'; 2 | 3 | export const PointString = { 4 | decode(pointString: string): Point { 5 | const [x, y] = pointString.slice(1, -1).split(','); 6 | 7 | return { x: parseFloat(x), y: parseFloat(y) }; 8 | }, 9 | 10 | encode({ x, y }: Point): string { 11 | return `{${x.toString()},${y.toString()}}`; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /packages/noya-state/README.md: -------------------------------------------------------------------------------- 1 | This is a regular package 2 | -------------------------------------------------------------------------------- /packages/noya-state/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noya-state", 3 | "version": "1.0.0", 4 | "main": "src/index.ts", 5 | "license": "UNLICENSED", 6 | "dependencies": { 7 | "immer": "^9.0.5", 8 | "tree-visit": "^0.1.3" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/noya-state/src/checkeredBackground.ts: -------------------------------------------------------------------------------- 1 | // A simple, unoptimized decoder for small images 2 | function decodeBase64(string: string) { 3 | return new Uint8Array( 4 | atob(string) 5 | .split('') 6 | .map((char) => char.charCodeAt(0)), 7 | ); 8 | } 9 | 10 | const CHECKERED_BACKGROUND = `iVBORw0KGgoAAAANSUhEUgAAABgAAAAYAQMAAADaua+7AAAABlBMVEUAAAAAAAClZ7nPAAAAAXRSTlMAQObYZgAAABNJREFUCNdjYOD/TxL+/4GBFAwAvMsj3bQ3H74AAAAASUVORK5CYII=`; 11 | 12 | export const CHECKERED_BACKGROUND_BYTES = decodeBase64(CHECKERED_BACKGROUND); 13 | -------------------------------------------------------------------------------- /packages/noya-state/src/groupLayouts.ts: -------------------------------------------------------------------------------- 1 | import type Sketch from 'noya-file-format'; 2 | 3 | const isInferredLayout = ( 4 | groupLayout: Sketch.FreeformGroupLayout | Sketch.InferredGroupLayout, 5 | ): groupLayout is Sketch.InferredGroupLayout => { 6 | return groupLayout._class === 'MSImmutableInferredGroupLayout'; 7 | }; 8 | 9 | export const GroupLayouts = { 10 | isInferredLayout, 11 | }; 12 | -------------------------------------------------------------------------------- /packages/noya-state/src/layer.ts: -------------------------------------------------------------------------------- 1 | export * as Layers from './layers'; 2 | -------------------------------------------------------------------------------- /packages/noya-state/src/reducers/__tests__/__snapshots__/layerPropertyReducer.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`setLayerWidth set width 1`] = ` 4 | " Page { x: 0, y: 0, w: 0, h: 0 } 5 | └── Group { x: 0, y: 0, w: 100, h: 100 } 6 | └── Rectangle { x: 0, y: 0, w: 100, h: 100 } 7 | 8 | Page { x: 0, y: 0, w: 0, h: 0 } 9 | └── Group { x: 0, y: 0, w: 200, h: 100 } 10 | └── Rectangle { x: 0, y: 0, w: 200, h: 100 }" 11 | `; 12 | -------------------------------------------------------------------------------- /packages/noya-state/src/reducers/__tests__/pageReducer.test.ts: -------------------------------------------------------------------------------- 1 | import { SketchModel } from 'noya-sketch-model'; 2 | import { createInitialState, createSketchFile } from 'noya-state'; 3 | import { pageReducer } from '../pageReducer'; 4 | 5 | describe('setPageName', () => { 6 | test('rename one', () => { 7 | const page = SketchModel.page(); 8 | const state = createInitialState(createSketchFile(page)); 9 | 10 | expect(state.sketch.pages[0].name).toEqual('Page'); 11 | 12 | const updated = pageReducer(state, [ 13 | 'setPageName', 14 | page.do_objectID, 15 | 'Test', 16 | ]); 17 | 18 | expect(updated.sketch.pages[0].name).toEqual('Test'); 19 | }); 20 | 21 | test('fails silently when renaming missing id', () => { 22 | const state = createInitialState(createSketchFile(SketchModel.page())); 23 | 24 | pageReducer(state, ['setPageName', 'bad', 'Test']); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/noya-state/src/selectors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './geometrySelectors'; 2 | export * from './indexPathSelectors'; 3 | export * from './layerSelectors'; 4 | export * from './pageSelectors'; 5 | export * from './styleSelectors'; 6 | export * from './themeSelectors'; 7 | export * from './transformSelectors'; 8 | export * from './workspaceSelectors'; 9 | export * from './pointSelectors'; 10 | export * from './elementSelectors'; 11 | export * from './gradientSelectors'; 12 | export * from './textStyleSelectors'; 13 | export * from './textSelectors'; 14 | export * from './textEditorSelectors'; 15 | export * from './attributedStringSelectors'; 16 | export * from './overridesSelectors'; 17 | -------------------------------------------------------------------------------- /packages/noya-state/src/selectors/workspaceSelectors.ts: -------------------------------------------------------------------------------- 1 | import { Draft } from 'immer'; 2 | import type { 3 | ApplicationState, 4 | ThemeTab, 5 | WorkspaceTab, 6 | } from '../reducers/applicationReducer'; 7 | 8 | export const getCurrentTab = ( 9 | state: ApplicationState | Draft, 10 | ): WorkspaceTab => { 11 | return state.currentTab; 12 | }; 13 | 14 | export const getCurrentComponentsTab = (state: ApplicationState): ThemeTab => { 15 | return state.currentThemeTab; 16 | }; 17 | -------------------------------------------------------------------------------- /packages/noya-state/src/sketchFile.ts: -------------------------------------------------------------------------------- 1 | import Sketch from 'noya-file-format'; 2 | import { SketchFile } from 'noya-sketch-file'; 3 | import { SketchModel } from 'noya-sketch-model'; 4 | 5 | export function createSketchFile( 6 | page: Sketch.Page = SketchModel.page(), 7 | ): SketchFile { 8 | return { 9 | document: SketchModel.document(), 10 | images: {}, 11 | meta: SketchModel.meta(), 12 | pages: [page], 13 | user: SketchModel.user({ 14 | [page.do_objectID]: { 15 | scrollOrigin: '{0, 0}', 16 | zoomValue: 1, 17 | }, 18 | }), 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /packages/noya-state/src/types.ts: -------------------------------------------------------------------------------- 1 | export type UUID = string; 2 | 3 | export type SetNumberMode = 'replace' | 'adjust'; 4 | -------------------------------------------------------------------------------- /packages/noya-state/src/utils/getMultiNumberValue.ts: -------------------------------------------------------------------------------- 1 | export function getMultiNumberValue(values: number[]): number | undefined { 2 | if (values.length === 1) { 3 | return values[0]; 4 | } else if (values.length > 1) { 5 | const min = Math.min(...values); 6 | const max = Math.max(...values); 7 | 8 | return max - min < Number.EPSILON ? min : undefined; 9 | } else { 10 | return undefined; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/noya-state/src/utils/getMultiValue.ts: -------------------------------------------------------------------------------- 1 | export function getMultiValue( 2 | values: T[], 3 | isEqual: (a: T, b: T) => boolean = (a, b) => a === b, 4 | ): T | undefined { 5 | if (values.length === 1) { 6 | return values[0]; 7 | } else if (values.length > 1) { 8 | const first = values[0]; 9 | 10 | return values.every((v) => isEqual(v, first)) ? first : undefined; 11 | } else { 12 | return undefined; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/noya-state/src/utils/moveArrayItem.ts: -------------------------------------------------------------------------------- 1 | export function moveArrayItem( 2 | array: T[], 3 | sourceIndex: number, 4 | destinationIndex: number, 5 | ) { 6 | const sourceItem = array[sourceIndex]; 7 | 8 | array.splice(sourceIndex, 1); 9 | 10 | array.splice( 11 | sourceIndex < destinationIndex ? destinationIndex - 1 : destinationIndex, 12 | 0, 13 | sourceItem, 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /packages/noya-state/src/utils/radians.ts: -------------------------------------------------------------------------------- 1 | export function toRadians(degrees: number) { 2 | return (degrees * Math.PI) / 180; 3 | } 4 | 5 | export function toDegrees(radians: number) { 6 | return (radians * 180) / Math.PI; 7 | } 8 | -------------------------------------------------------------------------------- /packages/noya-state/src/utils/zoom.ts: -------------------------------------------------------------------------------- 1 | import { clamp } from 'noya-utils'; 2 | 3 | const ZOOM_LEVELS = [ 4 | 0.015625, 0.03125, 0.0625, 0.125, 0.25, 0.5, 1, 2, 4, 8, 16, 32, 64, 128, 256, 5 | ]; 6 | 7 | function nearestLevel(zoom: number) { 8 | return ZOOM_LEVELS.reduce((prev, curr) => 9 | Math.abs(curr - zoom) < Math.abs(prev - zoom) ? curr : prev, 10 | ); 11 | } 12 | 13 | export const Zoom = { 14 | min: ZOOM_LEVELS[0], 15 | max: ZOOM_LEVELS[ZOOM_LEVELS.length - 1], 16 | nearestLevel, 17 | clamp: (zoom: number) => clamp(zoom, Zoom.min, Zoom.max), 18 | }; 19 | -------------------------------------------------------------------------------- /packages/noya-svg-renderer/README.md: -------------------------------------------------------------------------------- 1 | Generate SVG files 2 | -------------------------------------------------------------------------------- /packages/noya-svg-renderer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noya-svg-renderer", 3 | "version": "1.0.0", 4 | "main": "src/index.ts", 5 | "license": "UNLICENSED" 6 | } 7 | -------------------------------------------------------------------------------- /packages/noya-svg-renderer/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as SVGRenderer } from './SVGRenderer'; 2 | -------------------------------------------------------------------------------- /packages/noya-svgkit/README.md: -------------------------------------------------------------------------------- 1 | A TypeScript implementation of CanvasKit, used for our SVG renderer. 2 | -------------------------------------------------------------------------------- /packages/noya-svgkit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noya-svgkit", 3 | "version": "1.0.0", 4 | "main": "src/index.ts", 5 | "license": "UNLICENSED", 6 | "dependencies": { 7 | "color-parse": "^1.4.2" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/noya-svgkit/src/JSMaskFilter.ts: -------------------------------------------------------------------------------- 1 | import { BlurStyle, MaskFilter } from 'canvaskit'; 2 | import { JSEmbindObject } from './Embind'; 3 | 4 | export class JSMaskFilter extends JSEmbindObject implements MaskFilter { 5 | static MakeBlur( 6 | style: BlurStyle, 7 | sigma: number, 8 | respectCTM: boolean, 9 | ): MaskFilter { 10 | return new JSMaskFilter(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/noya-svgkit/src/index.ts: -------------------------------------------------------------------------------- 1 | import { getPublicPath } from 'noya-public-path'; 2 | import { PathKitInit } from 'pathkit'; 3 | import { createJSPath } from './JSPath'; 4 | import { SVGKit } from './SVGKit'; 5 | 6 | let loadingPromise: Promise | undefined = undefined; 7 | 8 | export function loadSVGKit() { 9 | if (loadingPromise) return loadingPromise; 10 | 11 | loadingPromise = new Promise(async (resolve) => { 12 | const PathKit = await PathKitInit({ 13 | locateFile: (file: string) => getPublicPath() + 'wasm/' + file, 14 | }); 15 | 16 | (SVGKit as any).Path = createJSPath(PathKit); 17 | 18 | resolve(SVGKit); 19 | }); 20 | 21 | return loadingPromise; 22 | } 23 | -------------------------------------------------------------------------------- /packages/noya-svgkit/src/parseColor.ts: -------------------------------------------------------------------------------- 1 | export default function parseColor(value: string): { 2 | space: string; 3 | values: number[]; 4 | alpha: number; 5 | } { 6 | const parseColor = require('color-parse'); 7 | 8 | return parseColor(value); 9 | } 10 | -------------------------------------------------------------------------------- /packages/noya-theme-editor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noya-theme-editor", 3 | "private": false, 4 | "modular": { 5 | "type": "package" 6 | }, 7 | "main": "./src/index.ts", 8 | "version": "1.0.0" 9 | } 10 | -------------------------------------------------------------------------------- /packages/noya-theme-editor/src/components/Symbol.tsx: -------------------------------------------------------------------------------- 1 | import Sketch from 'noya-file-format'; 2 | import { LayerPreview as RCKLayerPreview } from 'noya-renderer'; 3 | import React, { memo } from 'react'; 4 | import { CanvasPreviewItem } from './CanvasPreviewItem'; 5 | 6 | interface Props { 7 | layer: Sketch.SymbolMaster; 8 | } 9 | 10 | export const Symbol = memo(function Symbol({ layer }: Props) { 11 | return ( 12 | ( 14 | 21 | )} 22 | /> 23 | ); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/noya-theme-editor/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components/CanvasPreviewItem'; 2 | export * from './components/ColorSwatch'; 3 | export * from './components/SwatchesGrid'; 4 | export * from './components/Symbol'; 5 | export * from './components/SymbolsGrid'; 6 | export * from './components/TextStyle'; 7 | export * from './components/TextStylesGrid'; 8 | export * from './components/ThemeStyle'; 9 | export * from './components/ThemeStylesGrid'; 10 | export * from './utils/themeTree'; 11 | -------------------------------------------------------------------------------- /packages/noya-theme-editor/src/utils/menuItems.ts: -------------------------------------------------------------------------------- 1 | import { MenuItem, SEPARATOR_ITEM } from 'noya-designsystem'; 2 | 3 | export type ThemeMenuItemType = 'duplicate' | 'group' | 'ungroup' | 'delete'; 4 | 5 | export const menuItems: MenuItem[] = [ 6 | { value: 'duplicate', title: 'Duplicate' }, 7 | { value: 'group', title: 'Group' }, 8 | { value: 'ungroup', title: 'Ungroup' }, 9 | SEPARATOR_ITEM, 10 | { value: 'delete', title: 'Delete' }, 11 | ]; 12 | -------------------------------------------------------------------------------- /packages/noya-utils/README.md: -------------------------------------------------------------------------------- 1 | This is a regular package 2 | -------------------------------------------------------------------------------- /packages/noya-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noya-utils", 3 | "version": "1.0.0", 4 | "main": "src/index.ts", 5 | "license": "UNLICENSED", 6 | "dependencies": { 7 | "uuid": "^9.0.0" 8 | }, 9 | "devDependencies": { 10 | "@types/uuid": "^9.0.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/noya-utils/src/__tests__/base64.test.ts: -------------------------------------------------------------------------------- 1 | import { Base64 } from '../index'; 2 | 3 | const CHECKERED_BACKGROUND = `iVBORw0KGgoAAAANSUhEUgAAABgAAAAYAQMAAADaua+7AAAABlBMVEUAAAAAAAClZ7nPAAAAAXRSTlMAQObYZgAAABNJREFUCNdjYOD/TxL+/4GBFAwAvMsj3bQ3H74AAAAASUVORK5CYII=`; 4 | 5 | test('symmetric serialization', () => { 6 | const decoded = Base64.decode(CHECKERED_BACKGROUND); 7 | const reencoded = Base64.encode(decoded); 8 | expect(CHECKERED_BACKGROUND === reencoded).toEqual(true); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/noya-utils/src/__tests__/chunkBy.test.ts: -------------------------------------------------------------------------------- 1 | import { chunkBy } from '../index'; 2 | 3 | test('chunk empty array', () => { 4 | expect(chunkBy([], (a, b) => a <= b)).toEqual([]); 5 | }); 6 | 7 | test('chunk one value', () => { 8 | expect(chunkBy([1], (a, b) => a <= b)).toEqual([[1]]); 9 | }); 10 | 11 | test('chunk two values', () => { 12 | expect(chunkBy([1, 2], (a, b) => a <= b)).toEqual([[1, 2]]); 13 | expect(chunkBy([2, 1], (a, b) => a <= b)).toEqual([[2], [1]]); 14 | }); 15 | 16 | test('chunk three values', () => { 17 | expect(chunkBy([1, 3, 2], (a, b) => a <= b)).toEqual([[1, 3], [2]]); 18 | expect(chunkBy([2, 1, 3], (a, b) => a <= b)).toEqual([[2], [1, 3]]); 19 | }); 20 | 21 | test('chunk numbers', () => { 22 | expect(chunkBy([10, 20, 30, 10, 40, 40, 10, 20], (a, b) => a <= b)).toEqual([ 23 | [10, 20, 30], 24 | [10, 40, 40], 25 | [10, 20], 26 | ]); 27 | }); 28 | -------------------------------------------------------------------------------- /packages/noya-utils/src/__tests__/clamp.test.ts: -------------------------------------------------------------------------------- 1 | import { clamp } from '../index'; 2 | 3 | test('above max', () => { 4 | expect(clamp(17, 0, 10)).toEqual(10); 5 | }); 6 | 7 | test('below min', () => { 8 | expect(clamp(-7, 0, 10)).toEqual(0); 9 | }); 10 | 11 | test('within range', () => { 12 | expect(clamp(7, 0, 10)).toEqual(7); 13 | }); 14 | -------------------------------------------------------------------------------- /packages/noya-utils/src/__tests__/clipboard.test.ts: -------------------------------------------------------------------------------- 1 | import { ClipboardUtils } from '../clipboard'; 2 | 3 | test('serializes to json', () => { 4 | const value = { a: 123 }; 5 | const encoded = ClipboardUtils.toEncodedHTML(value); 6 | const decoded = ClipboardUtils.fromEncodedHTML(encoded); 7 | expect(value).toEqual(decoded); 8 | }); 9 | -------------------------------------------------------------------------------- /packages/noya-utils/src/__tests__/getIncrementedName.test.ts: -------------------------------------------------------------------------------- 1 | import { getIncrementedName } from '../index'; 2 | 3 | test('empty Space', () => { 4 | expect(getIncrementedName('', [''])).toEqual(' 2'); 5 | }); 6 | 7 | test('one word', () => { 8 | expect(getIncrementedName('A', ['A'])).toEqual('A 2'); 9 | }); 10 | 11 | test('one digit number', () => { 12 | expect(getIncrementedName('A 5', ['A 5'])).toEqual('A 6'); 13 | }); 14 | 15 | test('two digit number', () => { 16 | expect(getIncrementedName('A 15', ['A 15'])).toEqual('A 16'); 17 | }); 18 | 19 | test('invalid number at the end', () => { 20 | expect(getIncrementedName('A2', ['A2'])).toEqual('A2 2'); 21 | }); 22 | 23 | test('bigger number in array', () => { 24 | expect(getIncrementedName('A', ['A', 'A 3'])).toEqual('A 4'); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/noya-utils/src/__tests__/groupBy.test.ts: -------------------------------------------------------------------------------- 1 | import { groupBy } from '../index'; 2 | 3 | test('group by even or odd', () => { 4 | expect( 5 | groupBy([0, 1, 2, 3, 4, 5], (value) => (value % 2 === 0 ? 'even' : 'odd')), 6 | ).toEqual({ 7 | even: [0, 2, 4], 8 | odd: [1, 3, 5], 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /packages/noya-utils/src/__tests__/invert.test.ts: -------------------------------------------------------------------------------- 1 | import { invert } from '../invert'; 2 | 3 | test('invert object', () => { 4 | const input = { a: 1, b: 2 } as const; 5 | const output = { 1: 'a', 2: 'b' } as const; 6 | 7 | expect(invert(input)).toEqual(output); 8 | }); 9 | -------------------------------------------------------------------------------- /packages/noya-utils/src/__tests__/isEqualIgnoringUndefinedKeys.test.ts: -------------------------------------------------------------------------------- 1 | import { isEqualIgnoringUndefinedKeys } from '../isEqualIgnoringUndefinedKeys'; 2 | 3 | test('handles undefined keys specially', () => { 4 | expect(isEqualIgnoringUndefinedKeys({ a: undefined }, {})).toEqual(true); 5 | expect(isEqualIgnoringUndefinedKeys({}, { a: undefined })).toEqual(true); 6 | expect( 7 | isEqualIgnoringUndefinedKeys({ a: undefined }, { a: undefined }), 8 | ).toEqual(true); 9 | expect(isEqualIgnoringUndefinedKeys({ a: 123 }, { a: undefined })).toEqual( 10 | false, 11 | ); 12 | expect(isEqualIgnoringUndefinedKeys({ a: undefined }, { b: 123 })).toEqual( 13 | false, 14 | ); 15 | }); 16 | -------------------------------------------------------------------------------- /packages/noya-utils/src/__tests__/isNumberEqual.test.ts: -------------------------------------------------------------------------------- 1 | import { isNumberEqual } from '../index'; 2 | 3 | test('equal', () => { 4 | expect(isNumberEqual(160000, 160000)).toEqual(true); 5 | expect(isNumberEqual(160000.00000000006, 160000)).toEqual(true); 6 | }); 7 | -------------------------------------------------------------------------------- /packages/noya-utils/src/__tests__/lerp.test.ts: -------------------------------------------------------------------------------- 1 | import { lerp } from '..'; 2 | 3 | test('lerp', () => { 4 | expect(lerp(0, 100, -0.5)).toEqual(-50); 5 | expect(lerp(0, 100, 0)).toEqual(0); 6 | expect(lerp(0, 100, 0.5)).toEqual(50); 7 | expect(lerp(0, 100, 1)).toEqual(100); 8 | expect(lerp(0, 100, 1.5)).toEqual(150); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/noya-utils/src/__tests__/range.test.ts: -------------------------------------------------------------------------------- 1 | import { range } from '..'; 2 | 3 | test('range', () => { 4 | expect(range(4)).toEqual([0, 1, 2, 3]); 5 | expect(range(-4)).toEqual([0, -1, -2, -3]); 6 | expect(range(1, 5)).toEqual([1, 2, 3, 4]); 7 | expect(range(0, 20, 5)).toEqual([0, 5, 10, 15]); 8 | expect(range(0, -4, -1)).toEqual([0, -1, -2, -3]); 9 | expect(range(1, 4, 0)).toEqual([1, 1, 1]); 10 | expect(range(0)).toEqual([]); 11 | }); 12 | -------------------------------------------------------------------------------- /packages/noya-utils/src/__tests__/rotate.test.ts: -------------------------------------------------------------------------------- 1 | import { rotate } from '../index'; 2 | 3 | test('rotate empty array', () => { 4 | expect(rotate([], 10)).toEqual([]); 5 | }); 6 | 7 | test('rotate one value', () => { 8 | expect(rotate([1], 10)).toEqual([1]); 9 | }); 10 | 11 | test('rotate two values', () => { 12 | expect(rotate([1, 2], -3)).toEqual([2, 1]); 13 | expect(rotate([1, 2], -2)).toEqual([1, 2]); 14 | expect(rotate([1, 2], -1)).toEqual([2, 1]); 15 | expect(rotate([1, 2], 0)).toEqual([1, 2]); 16 | expect(rotate([1, 2], 1)).toEqual([2, 1]); 17 | expect(rotate([1, 2], 2)).toEqual([1, 2]); 18 | }); 19 | 20 | test('rotate three values', () => { 21 | expect(rotate([1, 2, 3], -1)).toEqual([3, 1, 2]); 22 | expect(rotate([1, 2, 3], 0)).toEqual([1, 2, 3]); 23 | expect(rotate([1, 2, 3], 1)).toEqual([2, 3, 1]); 24 | expect(rotate([1, 2, 3], 2)).toEqual([3, 1, 2]); 25 | expect(rotate([1, 2, 3], 3)).toEqual([1, 2, 3]); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/noya-utils/src/__tests__/round.test.ts: -------------------------------------------------------------------------------- 1 | import { round } from '..'; 2 | 3 | test('rounds down to precision', () => { 4 | const number = 1.1111111; 5 | expect(round(number, 0)).toEqual(1); 6 | expect(round(number, 1)).toEqual(1.1); 7 | expect(round(number, 2)).toEqual(1.11); 8 | expect(round(number, 3)).toEqual(1.111); 9 | expect(round(number, 4)).toEqual(1.1111); 10 | }); 11 | 12 | test('rounds whole numbers', () => { 13 | expect(round(0, 0)).toEqual(0); 14 | expect(round(0, 1)).toEqual(0); 15 | expect(round(42, 0)).toEqual(42); 16 | expect(round(42, 1)).toEqual(42); 17 | }); 18 | 19 | test('rounds up to precision', () => { 20 | const number = 5.5555555; 21 | expect(round(number, 0)).toEqual(6); 22 | expect(round(number, 1)).toEqual(5.6); 23 | expect(round(number, 2)).toEqual(5.56); 24 | expect(round(number, 3)).toEqual(5.556); 25 | expect(round(number, 4)).toEqual(5.5556); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/noya-utils/src/__tests__/sum.test.ts: -------------------------------------------------------------------------------- 1 | import { sum } from '../index'; 2 | 3 | test('empty array', () => { 4 | expect(sum([])).toEqual(0); 5 | }); 6 | 7 | test('sum', () => { 8 | expect(sum([-7, 2, 10])).toEqual(5); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/noya-utils/src/__tests__/url.test.ts: -------------------------------------------------------------------------------- 1 | import { decodeQueryParameters, parseUrl } from '../url'; 2 | 3 | test('parse url', () => { 4 | const result = parseUrl('https://noya.design?foo=bar&a=123#hello'); 5 | 6 | expect(result).toEqual({ 7 | pathname: '/', 8 | query: 'foo=bar&a=123', 9 | fragment: 'hello', 10 | }); 11 | }); 12 | 13 | test('parse query parameters', () => { 14 | const result = parseUrl('https://noya.design?foo=bar&a=123#hello'); 15 | const parameters = decodeQueryParameters(result.query); 16 | 17 | expect(parameters).toEqual({ 18 | foo: 'bar', 19 | a: '123', 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/noya-utils/src/__tests__/utf16.test.ts: -------------------------------------------------------------------------------- 1 | import { UTF16 } from '../index'; 2 | import util from 'util'; 3 | 4 | beforeAll(() => { 5 | window.TextEncoder = window.TextEncoder ?? util.TextEncoder; 6 | }); 7 | 8 | test('converts UTF16 to UTF8', () => { 9 | expect([...UTF16.toUTF8('A')]).toEqual([65]); 10 | expect([...UTF16.toUTF8('ë')]).toEqual([195, 171]); 11 | expect([...UTF16.toUTF8('€')]).toEqual([226, 130, 172]); 12 | expect([...UTF16.toUTF8('👨‍👩‍👧‍👦')]).toEqual([ 13 | 240, 14 | 159, 15 | 145, 16 | 168, 17 | 226, 18 | 128, 19 | 141, 20 | 240, 21 | 159, 22 | 145, 23 | 169, 24 | 226, 25 | 128, 26 | 141, 27 | 240, 28 | 159, 29 | 145, 30 | 167, 31 | 226, 32 | 128, 33 | 141, 34 | 240, 35 | 159, 36 | 145, 37 | 166, 38 | ]); 39 | }); 40 | -------------------------------------------------------------------------------- /packages/noya-utils/src/__tests__/windowsOf.test.ts: -------------------------------------------------------------------------------- 1 | import { windowsOf } from '../index'; 2 | 3 | test('pairs', () => { 4 | expect(windowsOf([], 2)).toEqual([]); 5 | expect(windowsOf([1], 2)).toEqual([]); 6 | expect(windowsOf([1, 2, 3, 4, 5], 2)).toEqual([ 7 | [1, 2], 8 | [2, 3], 9 | [3, 4], 10 | [4, 5], 11 | ]); 12 | expect(windowsOf([1, 2, 3, 4, 5], 2, true)).toEqual([ 13 | [1, 2], 14 | [2, 3], 15 | [3, 4], 16 | [4, 5], 17 | [5, 1], 18 | ]); 19 | }); 20 | 21 | test('triples', () => { 22 | expect(windowsOf([], 3)).toEqual([]); 23 | expect(windowsOf([1], 3)).toEqual([]); 24 | expect(windowsOf([1, 2], 3)).toEqual([]); 25 | expect(windowsOf([1, 2, 3, 4, 5], 3)).toEqual([ 26 | [1, 2, 3], 27 | [2, 3, 4], 28 | [3, 4, 5], 29 | ]); 30 | expect(windowsOf([1, 2, 3, 4, 5], 3, true)).toEqual([ 31 | [1, 2, 3], 32 | [2, 3, 4], 33 | [3, 4, 5], 34 | [4, 5, 1], 35 | [5, 1, 2], 36 | ]); 37 | }); 38 | -------------------------------------------------------------------------------- /packages/noya-utils/src/base64.ts: -------------------------------------------------------------------------------- 1 | // A simple, unoptimized decoder for small images 2 | function decode(string: string) { 3 | return new Uint8Array( 4 | atob(string) 5 | .split('') 6 | .map((char) => char.charCodeAt(0)), 7 | ); 8 | } 9 | 10 | function encode(buffer: ArrayBuffer): string { 11 | const bytes = new Uint8Array(buffer); 12 | 13 | let binary = ''; 14 | const length = bytes.byteLength; 15 | 16 | for (let i = 0; i < length; i++) { 17 | binary += String.fromCharCode(bytes[i]); 18 | } 19 | 20 | return window.btoa(binary); 21 | } 22 | 23 | export const Base64 = { 24 | encode, 25 | decode, 26 | }; 27 | -------------------------------------------------------------------------------- /packages/noya-utils/src/chunkBy.ts: -------------------------------------------------------------------------------- 1 | // Loosely based on: 2 | // https://github.com/apple/swift-algorithms/blob/0e2941ef50e7ebdf165150e3959453330946fd7d/Sources/Algorithms/Chunked.swift#L236 3 | export function chunkBy( 4 | values: T[], 5 | belongInSameGroup: (a: T, b: T) => boolean, 6 | ): T[][] { 7 | if (values.length === 0) return []; 8 | 9 | const result: T[][] = []; 10 | 11 | let start = 0; 12 | const end = values.length; 13 | 14 | for (let i = start + 1; i < end; i++) { 15 | const prev = values[i - 1]; 16 | const next = values[i]; 17 | 18 | if (!belongInSameGroup(prev, next)) { 19 | result.push(values.slice(start, i)); 20 | start = i; 21 | } 22 | } 23 | 24 | result.push(values.slice(start, end)); 25 | 26 | return result; 27 | } 28 | -------------------------------------------------------------------------------- /packages/noya-utils/src/clamp.ts: -------------------------------------------------------------------------------- 1 | export const clamp = (number: number, min = 0, max = 1): number => { 2 | return number > max ? max : number < min ? min : number; 3 | }; 4 | -------------------------------------------------------------------------------- /packages/noya-utils/src/clipboard.ts: -------------------------------------------------------------------------------- 1 | import { UTF16 } from 'noya-utils'; 2 | import { Base64 } from './base64'; 3 | 4 | /** 5 | * Serialize data to the clipboard. 6 | * 7 | * We convert the data into a base64-encoded string, wrapped in a paragraph tag. 8 | * This seems to work for every major browser. 9 | */ 10 | export const ClipboardUtils = { 11 | toEncodedHTML: (data: T): string => { 12 | const json = JSON.stringify(data); 13 | const utf8 = UTF16.toUTF8(json); 14 | const base64 = Base64.encode(utf8); 15 | const html = `

(noya)${base64}

`; 16 | 17 | return html; 18 | }, 19 | fromEncodedHTML: (html: string): T | undefined => { 20 | const match = html.match(/

\(noya\)(.*?)<\/p>/); 21 | 22 | if (!match) return; 23 | 24 | const base64 = match[1]; 25 | const utf8 = Base64.decode(base64); 26 | const json = UTF16.fromUTF8(utf8); 27 | const data = JSON.parse(json); 28 | 29 | return data; 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /packages/noya-utils/src/delimitedPath.ts: -------------------------------------------------------------------------------- 1 | export const sep = '/'; 2 | 3 | export function basename(filename: string) { 4 | return filename.slice(filename.lastIndexOf(sep) + 1); 5 | } 6 | 7 | export function dirname(filename: string) { 8 | const base = basename(filename); 9 | return filename.slice(0, -(base.length + 1)); 10 | } 11 | 12 | export function join(components: (string | null | undefined)[]) { 13 | return components.filter((component) => !!component).join(sep); 14 | } 15 | -------------------------------------------------------------------------------- /packages/noya-utils/src/getIncrementedName.ts: -------------------------------------------------------------------------------- 1 | const numberSuffixRegExp = /(.*?)(\s\d+)?$/; 2 | 3 | export function getIncrementedName( 4 | originalName: string, 5 | names: string[], 6 | ): string { 7 | const [, prefix] = originalName.match(numberSuffixRegExp) || []; 8 | 9 | const numbers = [originalName, ...names] 10 | .filter((name) => name.startsWith(prefix)) 11 | .map((name) => { 12 | const [, , number] = name.match(numberSuffixRegExp) || []; 13 | return number ? parseInt(number) : 1; 14 | }) 15 | .sort(); 16 | 17 | const maxNumber = numbers[numbers.length - 1]; 18 | 19 | return `${prefix} ${maxNumber + 1}`; 20 | } 21 | -------------------------------------------------------------------------------- /packages/noya-utils/src/groupBy.ts: -------------------------------------------------------------------------------- 1 | export function groupBy( 2 | values: T[], 3 | projection: (value: T) => U, 4 | ) { 5 | const result: { [key in PropertyKey]: T[] } = {}; 6 | 7 | values.forEach((value) => { 8 | const key = projection(value); 9 | 10 | if (key in result) { 11 | result[key].push(value); 12 | } else { 13 | result[key] = [value]; 14 | } 15 | }); 16 | 17 | return result; 18 | } 19 | -------------------------------------------------------------------------------- /packages/noya-utils/src/invert.ts: -------------------------------------------------------------------------------- 1 | export type Invert> = { 2 | [K in keyof T as T[K]]: K; 3 | }; 4 | 5 | export function invert>( 6 | record: T, 7 | ): Invert { 8 | return Object.fromEntries( 9 | Object.entries(record).map(([type, extension]) => [extension, type]), 10 | ) as Invert; 11 | } 12 | -------------------------------------------------------------------------------- /packages/noya-utils/src/isDeepEqual.ts: -------------------------------------------------------------------------------- 1 | import { isEqual } from './internal/isEqual'; 2 | 3 | export function isDeepEqual(a: T, b: T): boolean { 4 | return isEqual(a, b, true, false); 5 | } 6 | -------------------------------------------------------------------------------- /packages/noya-utils/src/isEqualIgnoringUndefinedKeys.ts: -------------------------------------------------------------------------------- 1 | import { isEqual } from './internal/isEqual'; 2 | 3 | export function isEqualIgnoringUndefinedKeys(a: T, b: T): boolean { 4 | return isEqual(a, b, true, true); 5 | } 6 | -------------------------------------------------------------------------------- /packages/noya-utils/src/isNumberEqual.ts: -------------------------------------------------------------------------------- 1 | // The built-in Number.EPSILON is too small for some of our calculations. 2 | // Possibly this is due to conversion to/from sketch files or wasm, or 3 | // because we multiply/divide which compounds this error 4 | export function isNumberEqual(a: number, b: number): boolean { 5 | return Math.abs(a - b) < 1e-6; 6 | } 7 | -------------------------------------------------------------------------------- /packages/noya-utils/src/isShallowEqual.ts: -------------------------------------------------------------------------------- 1 | import { isEqual } from './internal/isEqual'; 2 | 3 | export function isShallowEqual(a: T, b: T): boolean { 4 | return isEqual(a, b, false, false); 5 | } 6 | -------------------------------------------------------------------------------- /packages/noya-utils/src/lerp.ts: -------------------------------------------------------------------------------- 1 | export function lerp(a: number, b: number, t: number) { 2 | return a * (1 - t) + b * t; 3 | } 4 | -------------------------------------------------------------------------------- /packages/noya-utils/src/memoize.ts: -------------------------------------------------------------------------------- 1 | // TODO: Review this. Is it a good approach for Noya? 2 | export function memoize( 3 | f: (...values: I) => O, 4 | ): (...values: I) => O { 5 | const intermediateCache = new Map(); 6 | const cache: Map = new Map(); 7 | let intermediateCacheIndex = 0; 8 | 9 | return (...values: I): O => { 10 | let key = ''; 11 | 12 | for (const value of values) { 13 | // Assign each argument value a unique index 14 | if (!intermediateCache.has(value)) { 15 | intermediateCache.set(value, `${intermediateCacheIndex++}`); 16 | } 17 | 18 | // Assemble a cache key from the combined indexes 19 | key += intermediateCache.get(value)! + ':'; 20 | } 21 | 22 | if (!cache.has(key)) { 23 | cache.set(key, f(...values)); 24 | } 25 | 26 | return cache.get(key)!; 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /packages/noya-utils/src/memoizedGetter.ts: -------------------------------------------------------------------------------- 1 | export function memoizedGetter( 2 | target: unknown, 3 | propertyKey: string, 4 | value: T, 5 | ): T { 6 | Object.defineProperty(target, propertyKey, { value, writable: false }); 7 | return value; 8 | } 9 | -------------------------------------------------------------------------------- /packages/noya-utils/src/partition.ts: -------------------------------------------------------------------------------- 1 | export function partition( 2 | array: readonly T[], 3 | predicate: (item: T) => item is U, 4 | ): [U[], Exclude[]]; 5 | export function partition( 6 | array: readonly T[], 7 | predicate: (item: T) => boolean, 8 | ): [T[], T[]]; 9 | export function partition( 10 | array: readonly T[], 11 | predicate: (item: T) => boolean, 12 | ): [T[], T[]] { 13 | const left: T[] = []; 14 | const right: T[] = []; 15 | 16 | for (const item of array) { 17 | if (predicate(item)) { 18 | left.push(item); 19 | } else { 20 | right.push(item); 21 | } 22 | } 23 | 24 | return [left, right]; 25 | } 26 | -------------------------------------------------------------------------------- /packages/noya-utils/src/rotate.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Rotates the elements within the given array so that the element at 3 | * the index specified by `toStartAt` becomes the start of the array. 4 | */ 5 | export function rotate(array: T[], toStartAt: number): T[] { 6 | let start = toStartAt % array.length; 7 | 8 | if (start < 0) { 9 | start += array.length; 10 | } 11 | 12 | if (array.length < 2 || start === 0) return array; 13 | 14 | return [...array.slice(start), ...array.slice(0, start)]; 15 | } 16 | -------------------------------------------------------------------------------- /packages/noya-utils/src/round.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Round to precision, if necessary. 3 | * 4 | * Examples: 5 | * - round(1.111, 2) => 1.11 6 | * - round(1.115, 2) => 1.12 7 | * - round(1, 2) => 1 8 | * 9 | * https://stackoverflow.com/a/11832950 10 | */ 11 | export function round(number: number, precision = 0) { 12 | const base = Math.pow(10, precision); 13 | return Math.round((number + Number.EPSILON) * base) / base; 14 | } 15 | -------------------------------------------------------------------------------- /packages/noya-utils/src/sortBy.ts: -------------------------------------------------------------------------------- 1 | export function sortBy< 2 | K extends PropertyKey, 3 | Item extends { [key in K]: string } 4 | >(array: Item[], key: K) { 5 | return [...array].sort((a, b) => { 6 | const aName = a[key].toUpperCase(); 7 | const bName = b[key].toUpperCase(); 8 | 9 | return aName > bName ? 1 : aName < bName ? -1 : 0; 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /packages/noya-utils/src/sum.ts: -------------------------------------------------------------------------------- 1 | export function sum(values: number[]) { 2 | let value = 0; 3 | 4 | for (let i = 0; i < values.length; i++) { 5 | value += values[i]; 6 | } 7 | 8 | return value; 9 | } 10 | -------------------------------------------------------------------------------- /packages/noya-utils/src/types.ts: -------------------------------------------------------------------------------- 1 | // TypeScript will be adding an official `TupleOf` utility type, which will 2 | // be more performant in the case of a large N, but in the meantime, we can 3 | // use this one. 4 | // https://github.com/microsoft/TypeScript/pull/40002 5 | // https://github.com/piotrwitek/utility-types/pull/162 6 | export type TupleOf = N extends N 7 | ? number extends N 8 | ? T[] 9 | : _TupleOf 10 | : never; 11 | 12 | type _TupleOf = R['length'] extends N 13 | ? R 14 | : _TupleOf; 15 | 16 | export type Brand = K & { __brand: T }; 17 | -------------------------------------------------------------------------------- /packages/noya-utils/src/unique.ts: -------------------------------------------------------------------------------- 1 | export function unique(array: T[]) { 2 | return [...new Set(array)]; 3 | } 4 | -------------------------------------------------------------------------------- /packages/noya-utils/src/upperFirst.ts: -------------------------------------------------------------------------------- 1 | export function upperFirst(string: string) { 2 | return string.slice(0, 1).toUpperCase() + string.slice(1); 3 | } 4 | -------------------------------------------------------------------------------- /packages/noya-utils/src/utf16.ts: -------------------------------------------------------------------------------- 1 | function toUTF8(string: string) { 2 | const encoder = new TextEncoder(); 3 | return encoder.encode(string); 4 | } 5 | 6 | function fromUTF8(encoded: Uint8Array) { 7 | const decoder = new TextDecoder(); 8 | return decoder.decode(encoded); 9 | } 10 | 11 | export const UTF16 = { 12 | toUTF8, 13 | fromUTF8, 14 | }; 15 | -------------------------------------------------------------------------------- /packages/noya-utils/src/windowsOf.ts: -------------------------------------------------------------------------------- 1 | import { TupleOf } from './types'; 2 | 3 | export function windowsOf( 4 | array: T[], 5 | size: N, 6 | wrapsAround = false, 7 | ): TupleOf[] { 8 | let arr = array; 9 | 10 | if (arr.length < size) return []; 11 | 12 | if (wrapsAround) { 13 | arr = array.slice(); 14 | 15 | for (let i = 0; i < size - 1; i++) { 16 | arr.push(array[i]); 17 | } 18 | } 19 | 20 | const result: TupleOf[] = []; 21 | 22 | for (let i = 0; i <= arr.length - size; i++) { 23 | result.push(arr.slice(i, i + size) as TupleOf); 24 | } 25 | 26 | return result; 27 | } 28 | -------------------------------------------------------------------------------- /packages/pathkit/README.md: -------------------------------------------------------------------------------- 1 | This is where our custom pathkit-wasm build lives. 2 | -------------------------------------------------------------------------------- /packages/pathkit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pathkit", 3 | "version": "1.0.0", 4 | "main": "src/index.ts", 5 | "license": "UNLICENSED" 6 | } 7 | -------------------------------------------------------------------------------- /packages/pathkit/src/index.ts: -------------------------------------------------------------------------------- 1 | const init: any = require('./pathkit.js'); 2 | 3 | export { init as PathKitInit }; 4 | -------------------------------------------------------------------------------- /packages/site/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | "plugins": [["styled-components", { "ssr": true }]] 4 | } 5 | -------------------------------------------------------------------------------- /packages/site/guidebook.d.ts: -------------------------------------------------------------------------------- 1 | import type { TreeNode } from 'generate-guidebook'; 2 | 3 | const value: TreeNode; 4 | 5 | export default value; 6 | -------------------------------------------------------------------------------- /packages/site/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /packages/site/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/site/public/SocialCard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/site/public/SocialCard.png -------------------------------------------------------------------------------- /packages/site/searchIndex.d.ts: -------------------------------------------------------------------------------- 1 | const index: any; 2 | 3 | export default index; 4 | -------------------------------------------------------------------------------- /packages/site/src/__tests__/__snapshots__/fuzzyScorer.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`filter multiple matches 1`] = ` 4 | Array [ 5 | Object { 6 | "index": 1, 7 | "labelMatch": Array [ 8 | Object { 9 | "end": 1, 10 | "start": 0, 11 | }, 12 | ], 13 | "score": 131072, 14 | }, 15 | Object { 16 | "index": 0, 17 | "labelMatch": Array [ 18 | Object { 19 | "end": 6, 20 | "start": 5, 21 | }, 22 | ], 23 | "score": 32768, 24 | }, 25 | ] 26 | `; 27 | 28 | exports[`filter single match 1`] = ` 29 | Array [ 30 | Object { 31 | "index": 0, 32 | "labelMatch": Array [ 33 | Object { 34 | "end": 9, 35 | "start": 5, 36 | }, 37 | ], 38 | "score": 32768, 39 | }, 40 | ] 41 | `; 42 | -------------------------------------------------------------------------------- /packages/site/src/__tests__/tailwind.test.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import path from 'path'; 3 | import { getBlockClassName } from '../ayon/blocks/tailwind'; 4 | 5 | // Jest doesn't know how to import a text file, so we mock it 6 | jest.mock('../../safelist.txt', () => { 7 | return { 8 | default: readFileSync(path.join(__dirname, '../../safelist.txt'), 'utf8'), 9 | }; 10 | }); 11 | 12 | it('only applies last class within a group', () => { 13 | expect(getBlockClassName(['bg-red-500', 'bg-blue-500'])).toEqual( 14 | 'bg-blue-500', 15 | ); 16 | }); 17 | 18 | it('applies one class within every group', () => { 19 | expect(getBlockClassName(['text-red-500', 'bg-blue-500'])).toEqual( 20 | 'text-red-500 bg-blue-500', 21 | ); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/site/src/assets/ConfigureBlockText.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/site/src/assets/ConfigureBlockText.webp -------------------------------------------------------------------------------- /packages/site/src/assets/ConfigureBlockType.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/site/src/assets/ConfigureBlockType.webp -------------------------------------------------------------------------------- /packages/site/src/assets/InsertBlock.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/site/src/assets/InsertBlock.webp -------------------------------------------------------------------------------- /packages/site/src/assets/PremiumTemplateMosaic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/site/src/assets/PremiumTemplateMosaic.png -------------------------------------------------------------------------------- /packages/site/src/assets/docs/InsertTool.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/site/src/assets/docs/InsertTool.png -------------------------------------------------------------------------------- /packages/site/src/assets/docs/ProjectConfiguration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/site/src/assets/docs/ProjectConfiguration.png -------------------------------------------------------------------------------- /packages/site/src/assets/docs/ProjectContextMenu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/site/src/assets/docs/ProjectContextMenu.png -------------------------------------------------------------------------------- /packages/site/src/assets/docs/RegionTool.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/site/src/assets/docs/RegionTool.png -------------------------------------------------------------------------------- /packages/site/src/assets/docs/RegionToolExample.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noya-app/noya/45e9bd99d4efa84d773bb6ff3e72f323f16e85eb/packages/site/src/assets/docs/RegionToolExample.webp -------------------------------------------------------------------------------- /packages/site/src/ayon/AttributionCard.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import React from 'react'; 3 | import { Attribution } from './resolve/RandomImageResolver'; 4 | 5 | function AttributionLink({ url, children }: { url: string; children: string }) { 6 | return ( 7 | 8 | 13 | {children} 14 | 15 | 16 | ); 17 | } 18 | 19 | export function AttributionCard({ user, source }: Attribution) { 20 | return ( 21 | <> 22 | Photo by {user.name} on{' '} 23 | {source.name} 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /packages/site/src/ayon/blocks/CardBlock.tsx: -------------------------------------------------------------------------------- 1 | import { BlockDefinition } from 'noya-state'; 2 | import { BoxBlock } from './BoxBlock'; 3 | import { renderStack } from './render'; 4 | import { isWithinRectRange } from './score'; 5 | import { cardSymbol } from './symbols'; 6 | 7 | export const CardBlock: BlockDefinition = { 8 | editorVersion: 2, 9 | symbol: cardSymbol, 10 | parser: 'regular', 11 | hashtags: BoxBlock.hashtags, 12 | infer: ({ frame, blockText, siblingBlocks }) => { 13 | return Math.max( 14 | isWithinRectRange({ 15 | rect: frame, 16 | minWidth: 200, 17 | minHeight: 250, 18 | maxWidth: 300, 19 | maxHeight: 400, 20 | }) 21 | ? 1 22 | : 0, 23 | 0.1, 24 | ); 25 | }, 26 | render: (props) => renderStack({ props, block: CardBlock }), 27 | }; 28 | -------------------------------------------------------------------------------- /packages/site/src/ayon/blocks/SignInBlock.tsx: -------------------------------------------------------------------------------- 1 | import { BlockDefinition } from 'noya-state'; 2 | import { BoxBlock } from './BoxBlock'; 3 | import { renderStack } from './render'; 4 | import { signInSymbol } from './symbols'; 5 | 6 | export const SignInBlock: BlockDefinition = { 7 | editorVersion: 2, 8 | symbol: signInSymbol, 9 | parser: 'regular', 10 | hashtags: BoxBlock.hashtags, 11 | infer: ({ frame, blockText, siblingBlocks }) => { 12 | if ( 13 | siblingBlocks.find((block) => block.symbolId === signInSymbol.symbolID) 14 | ) { 15 | return 0; 16 | } 17 | 18 | return 0.1; 19 | }, 20 | render: (props) => renderStack({ props, block: SignInBlock }), 21 | }; 22 | -------------------------------------------------------------------------------- /packages/site/src/ayon/blocks/SpacerBlock.tsx: -------------------------------------------------------------------------------- 1 | import { Spacer } from '@chakra-ui/react'; 2 | import { BlockDefinition } from 'noya-state'; 3 | import React from 'react'; 4 | import { parseBlock } from '../parse'; 5 | import { spacerSymbol } from './symbols'; 6 | import { getBlockClassName, tailwindBlockClasses } from './tailwind'; 7 | 8 | export const SpacerBlock: BlockDefinition = { 9 | symbol: spacerSymbol, 10 | parser: 'regular', 11 | hashtags: tailwindBlockClasses, 12 | infer: ({ frame, blockText }) => 0, 13 | isPassthrough: true, 14 | render: (props) => { 15 | const { parameters } = parseBlock(props.blockText, 'regular'); 16 | const hashtags = Object.keys(parameters); 17 | 18 | return ( 19 | 25 | ); 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /packages/site/src/ayon/blocks/SwitchBlock.tsx: -------------------------------------------------------------------------------- 1 | import { Switch } from '@chakra-ui/react'; 2 | import { BlockDefinition } from 'noya-state'; 3 | import React from 'react'; 4 | import { parseBlock } from '../parse'; 5 | import { switchSymbol } from './symbols'; 6 | 7 | const placeholderText = '#off'; 8 | 9 | const globalHashtags = ['on', 'off', 'disabled']; 10 | 11 | const parser = 'regular'; 12 | 13 | export const SwitchBlock: BlockDefinition = { 14 | symbol: switchSymbol, 15 | parser, 16 | hashtags: globalHashtags, 17 | placeholderText, 18 | infer: ({ frame, blockText }) => 0.1, 19 | render: (props) => { 20 | const { 21 | parameters: { on, disabled }, 22 | } = parseBlock(props.blockText, parser, { 23 | placeholder: placeholderText, 24 | }); 25 | const height = props.frame?.height ?? 25; 26 | const size = height >= 35 ? 'lg' : height >= 25 ? 'md' : 'sm'; 27 | return ; 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /packages/site/src/ayon/blocks/TileCardBlock.tsx: -------------------------------------------------------------------------------- 1 | import { BlockDefinition } from 'noya-state'; 2 | import { BoxBlock } from './BoxBlock'; 3 | import { renderStack } from './render'; 4 | import { isWithinRectRange } from './score'; 5 | import { tileCardSymbol } from './symbols'; 6 | 7 | export const TileCardBlock: BlockDefinition = { 8 | editorVersion: 2, 9 | symbol: tileCardSymbol, 10 | parser: 'regular', 11 | hashtags: BoxBlock.hashtags, 12 | infer: ({ frame, blockText, siblingBlocks }) => { 13 | return Math.max( 14 | isWithinRectRange({ 15 | rect: frame, 16 | minWidth: 200, 17 | minHeight: 200, 18 | maxWidth: 250, 19 | maxHeight: 250, 20 | }) 21 | ? 1.2 22 | : 0, 23 | 0.1, 24 | ); 25 | }, 26 | render: (props) => renderStack({ props, block: TileCardBlock }), 27 | }; 28 | -------------------------------------------------------------------------------- /packages/site/src/ayon/blocks/blockTheme.ts: -------------------------------------------------------------------------------- 1 | export const accentColor = { 2 | 50: '#eff6ff', 3 | 100: '#dbeafe', 4 | 200: '#bfdbfe', 5 | 300: '#93c5fd', 6 | 400: '#60a5fa', 7 | 500: '#3b82f6', 8 | 600: '#2563eb', 9 | 700: '#1d4ed8', 10 | 800: '#1e40af', 11 | 900: '#1e3a8a', 12 | }; 13 | 14 | export const buttonColors = { 15 | default: { 16 | backgroundColor: '#f1f5f9', 17 | color: '#000', 18 | }, 19 | light: { 20 | backgroundColor: '#f1f5f9', 21 | color: '#000', 22 | }, 23 | dark: { 24 | backgroundColor: '#475569', 25 | color: '#fff', 26 | }, 27 | primary: { 28 | backgroundColor: '#15803d', 29 | color: '#fff', 30 | }, 31 | secondary: { 32 | backgroundColor: '#94a3b8', 33 | color: '#fff', 34 | }, 35 | warning: { 36 | backgroundColor: '#fde047', 37 | color: '#000', 38 | }, 39 | danger: { 40 | backgroundColor: '#dc2626', 41 | color: '#fff', 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /packages/site/src/ayon/blocks/flattenPassthroughLayers.tsx: -------------------------------------------------------------------------------- 1 | import Sketch from 'noya-file-format'; 2 | import { Layers } from 'noya-state'; 3 | import { Blocks } from './blocks'; 4 | 5 | export function flattenPassthroughLayers( 6 | symbolMaster: Sketch.SymbolMaster, 7 | ): Sketch.SymbolInstance[] { 8 | return symbolMaster.layers 9 | .filter(Layers.isSymbolInstance) 10 | .flatMap((layer) => { 11 | const block = Blocks[layer.symbolID]; 12 | 13 | if (block && block.isPassthrough) { 14 | return flattenPassthroughLayers(block.symbol); 15 | } 16 | 17 | return layer; 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /packages/site/src/ayon/createAyonDocument.ts: -------------------------------------------------------------------------------- 1 | import { SketchModel } from 'noya-sketch-model'; 2 | import { createSketchFile } from 'noya-state'; 3 | 4 | const artboard = SketchModel.artboard({ 5 | name: 'Wireframe', 6 | frame: SketchModel.rect({ 7 | x: 0, 8 | y: 0, 9 | width: 1280, 10 | height: 720, 11 | }), 12 | }); 13 | 14 | export function createAyonDocument() { 15 | const sketch = createSketchFile(SketchModel.page({ layers: [artboard] })); 16 | 17 | return sketch; 18 | } 19 | -------------------------------------------------------------------------------- /packages/site/src/ayon/editor/resetNodes.tsx: -------------------------------------------------------------------------------- 1 | import { Editor, Node, Point, Transforms } from 'slate'; 2 | import { CustomEditor } from './types'; 3 | 4 | export function resetNodes( 5 | editor: CustomEditor, 6 | options: { 7 | nodes?: Node[]; 8 | at?: Location; 9 | } = {}, 10 | ): void { 11 | const children = [...editor.children]; 12 | 13 | children.forEach((node) => 14 | editor.apply({ type: 'remove_node', path: [0], node }), 15 | ); 16 | 17 | const nodes = options.nodes; 18 | 19 | if (nodes) { 20 | Editor.withoutNormalizing(editor, () => { 21 | nodes.forEach((node, i) => 22 | editor.apply({ type: 'insert_node', path: [i], node: node }), 23 | ); 24 | }); 25 | } 26 | 27 | const point = 28 | options.at && Point.isPoint(options.at) 29 | ? options.at 30 | : Editor.end(editor, []); 31 | 32 | if (point) { 33 | Transforms.select(editor, point); 34 | } 35 | 36 | Editor.normalize(editor, { force: true }); 37 | } 38 | -------------------------------------------------------------------------------- /packages/site/src/ayon/editor/types.ts: -------------------------------------------------------------------------------- 1 | import { BaseEditor, Descendant } from 'slate'; 2 | import { HistoryEditor } from 'slate-history'; 3 | import { ReactEditor } from 'slate-react'; 4 | 5 | /** 6 | * In general, the fewer things we need in here the better, since it can be 7 | * tricky to sync editor state with the rest of the app. 8 | */ 9 | export type ParagraphElement = { 10 | type: 'paragraph'; 11 | label?: string; 12 | placeholder?: string; 13 | symbolId: string; 14 | children: Descendant[]; 15 | layerId?: string; 16 | }; 17 | 18 | export type CustomElement = ParagraphElement; 19 | 20 | export type CustomEditor = BaseEditor & 21 | ReactEditor & 22 | HistoryEditor & { 23 | symbolId: string; 24 | }; 25 | 26 | declare module 'slate' { 27 | interface CustomTypes { 28 | Editor: CustomEditor; 29 | Element: CustomElement; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/site/src/ayon/inferBlock.ts: -------------------------------------------------------------------------------- 1 | import { DrawableLayerType, InferBlockProps, InferBlockType } from 'noya-state'; 2 | import { allInsertableBlocks } from './blocks/blocks'; 3 | import { InferredBlockTypeResult } from './types'; 4 | 5 | export function inferBlockTypes( 6 | input: InferBlockProps, 7 | ): InferredBlockTypeResult[] { 8 | let results: InferredBlockTypeResult[] = []; 9 | 10 | for (const block of allInsertableBlocks) { 11 | results.push({ 12 | type: { symbolId: block.symbol.symbolID }, 13 | score: block.infer(input), 14 | }); 15 | } 16 | 17 | results.sort( 18 | ( 19 | a: { type: DrawableLayerType; score: number }, 20 | b: { type: DrawableLayerType; score: number }, 21 | ) => b.score - a.score, 22 | ); 23 | 24 | return results; 25 | } 26 | 27 | export const inferBlockType: InferBlockType = (input) => { 28 | return inferBlockTypes(input)[0].type; 29 | }; 30 | -------------------------------------------------------------------------------- /packages/site/src/ayon/stacking.ts: -------------------------------------------------------------------------------- 1 | export const Stacking = { 2 | level: { 3 | interactive: 1, 4 | overlay: 2, 5 | menu: 3, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/site/src/ayon/types.ts: -------------------------------------------------------------------------------- 1 | import { DrawableLayerType } from 'noya-state'; 2 | 3 | export type InferredBlockTypeResult = { 4 | type: DrawableLayerType; 5 | score: number; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/site/src/components/OnboardingAnimation.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export function OnboardingAnimation({ src }: { src: string }) { 4 | return ( 5 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /packages/site/src/components/OptionalNoyaAPIProvider.tsx: -------------------------------------------------------------------------------- 1 | import { NoyaAPI, NoyaAPIProvider } from 'noya-api'; 2 | import React, { useEffect, useState } from 'react'; 3 | import { networkClientThatThrows } from '../utils/noyaClient'; 4 | 5 | export function OptionalNoyaAPIProvider({ 6 | children, 7 | }: { 8 | children: React.ReactNode; 9 | }) { 10 | const [client, setClient] = useState(); 11 | 12 | useEffect(() => { 13 | async function main() { 14 | try { 15 | if (!networkClientThatThrows) return; 16 | await networkClientThatThrows.auth.session(); 17 | setClient( 18 | new NoyaAPI.Client({ networkClient: networkClientThatThrows }), 19 | ); 20 | } catch { 21 | // Ignore 22 | } 23 | } 24 | 25 | main(); 26 | }, []); 27 | 28 | return {children}; 29 | } 30 | -------------------------------------------------------------------------------- /packages/site/src/components/ProjectTitle.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Spacer, useDesignSystemTheme } from 'noya-designsystem'; 2 | import { ChevronDownIcon, DashboardIcon } from 'noya-icons'; 3 | import React, { forwardRef, ReactNode } from 'react'; 4 | 5 | interface Props { 6 | children: ReactNode; 7 | onClick?: () => void; 8 | } 9 | 10 | export const ProjectTitle = forwardRef(function ProjectTitle( 11 | { children, onClick }: Props, 12 | ref: React.Ref, 13 | ) { 14 | const theme = useDesignSystemTheme(); 15 | 16 | return ( 17 | 24 | ); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/site/src/contexts/ProjectContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, ReactNode, useContext } from 'react'; 2 | 3 | export type ProjectContextValue = { 4 | setLeftToolbar: (value: ReactNode) => void; 5 | setRightToolbar: (value: ReactNode) => void; 6 | setCenterToolbar: (value: ReactNode) => void; 7 | }; 8 | 9 | const ProjectContext = createContext({ 10 | setLeftToolbar: () => {}, 11 | setRightToolbar: () => {}, 12 | setCenterToolbar: () => {}, 13 | }); 14 | 15 | export const ProjectProvider = ProjectContext.Provider; 16 | 17 | export function useProject() { 18 | return useContext(ProjectContext); 19 | } 20 | -------------------------------------------------------------------------------- /packages/site/src/docs/socialConfig.ts: -------------------------------------------------------------------------------- 1 | import { GuidebookConfig } from 'react-guidebook'; 2 | 3 | export const socialConfig: GuidebookConfig = { 4 | title: 'Noya Documentation', 5 | location: { 6 | host: ( 7 | process.env.NEXT_PUBLIC_NOYA_WEB_URL ?? 'https://www.noya.io' 8 | ).replace(/https?:\/\//, ''), 9 | path: '/app/docs', 10 | }, 11 | author: { 12 | twitter: 'noyasoftware', 13 | }, 14 | favicons: [ 15 | { 16 | type: 'image/x-icon', 17 | path: '/favicon.ico', 18 | }, 19 | ], 20 | previewImage: { 21 | type: 'image/png', 22 | width: '1200', 23 | height: '630', 24 | alt: 'Noya Documentation', 25 | path: '/app/SocialCard.png', 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /packages/site/src/hooks/useToggleTimer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export function useToggleTimer(delay: number) { 4 | const [value, setValue] = React.useState(false); 5 | 6 | React.useEffect(() => { 7 | if (!value) return; 8 | 9 | const timeout = setTimeout(() => setValue(false), delay); 10 | 11 | return () => clearTimeout(timeout); 12 | }, [delay, value]); 13 | 14 | return { value, trigger: () => setValue(true) }; 15 | } 16 | -------------------------------------------------------------------------------- /packages/site/src/pages/docs/blocks.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Block Reference 3 | --- 4 | 5 | ## Categories 6 | 7 | There are 3 main categories of block: 8 | 9 | - [Application](/app/docs/blocks/application) - blocks that are used to build 10 | web and mobile apps 11 | - [Marketing](/app/docs/blocks/marketing) - blocks that are used to build 12 | landing pages and marketing sites 13 | - [Elements](/app/docs/blocks/elements) - smaller blocks like "Button" and 14 | "Image" that can be used to build other, bigger blocks 15 | -------------------------------------------------------------------------------- /packages/site/src/pages/docs/blocks/application.mdx: -------------------------------------------------------------------------------- 1 | ## Available blocks 2 | 3 | Here are some of the top blocks you might find useful when creating 4 | applications: 5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/site/src/pages/docs/blocks/application/sidebar.mdx: -------------------------------------------------------------------------------- 1 | import { sidebarSymbolId } from '../../../../ayon/blocks/symbolIds'; 2 | 3 | ## Live Preview 4 | 5 | You can double click the block to edit its contents. 6 | 7 | 8 | 9 | ## Configuration 10 | 11 | Each item in the sidebar should be on a separate line. To show a specific item 12 | as active, type `*` before the item name. 13 | 14 | You can further configure the sidebar's style by adding: 15 | 16 | - `#title` - to use the first item as a title 17 | - `#dark` - to use the dark theme 18 | 19 | Here's an example configuration that uses these options (double click to edit): 20 | 21 | 29 | -------------------------------------------------------------------------------- /packages/site/src/pages/docs/blocks/application/sign-in.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Sign In 3 | --- 4 | 5 | import { signInSymbolId } from '../../../../ayon/blocks/symbolIds'; 6 | 7 | ## Live Preview 8 | 9 | You can double click the block to edit its contents. 10 | 11 | 12 | -------------------------------------------------------------------------------- /packages/site/src/pages/docs/blocks/application/table.mdx: -------------------------------------------------------------------------------- 1 | import { tableSymbolId } from '../../../../ayon/blocks/symbolIds'; 2 | 3 | ## Live Preview 4 | 5 | You can double click the block to edit its contents. 6 | 7 | 8 | 9 | ## Customization 10 | 11 | You can type or paste any comma-separated or tab-separated data into the table 12 | block to customize its contents. For example: 13 | 14 | ``` 15 | Name, Age, Height 16 | John, 30, 6'0" 17 | Jane, 25, 5'8" 18 | ``` 19 | 20 | Results in the following table: 21 | 22 | 29 | -------------------------------------------------------------------------------- /packages/site/src/pages/docs/blocks/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "order": ["application", "marketing", "elements"] 3 | } 4 | -------------------------------------------------------------------------------- /packages/site/src/pages/docs/blocks/elements.mdx: -------------------------------------------------------------------------------- 1 | ## Available blocks 2 | 3 | Elements are smaller, more versatile blocks that can be used to build many 4 | different kinds of apps: 5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/site/src/pages/docs/blocks/elements/avatar.mdx: -------------------------------------------------------------------------------- 1 | import { avatarSymbolId } from '../../../../ayon/blocks/symbolIds'; 2 | 3 | ## Live Preview 4 | 5 | You can double click the block to edit its contents. 6 | 7 | 8 | 9 | ## Configuration 10 | 11 | ### Monogram 12 | 13 | Type any letter into the avatar block to display a monogram instead of an image. 14 | 15 | 16 | 17 | ### Image 18 | 19 | You can also use any url: 20 | 21 | 25 | -------------------------------------------------------------------------------- /packages/site/src/pages/docs/blocks/elements/box.mdx: -------------------------------------------------------------------------------- 1 | import { boxSymbolId } from '../../../../ayon/blocks/symbolIds'; 2 | 3 | ## Live Preview 4 | 5 | You can double click the block to edit its contents. 6 | 7 | 8 | 9 | ## Configuration 10 | 11 | The box is an extremely configurable block. It can be used to create a wide 12 | variety of layouts and styles using the many available `#` commands. 13 | 14 | 18 | -------------------------------------------------------------------------------- /packages/site/src/pages/docs/blocks/elements/button.mdx: -------------------------------------------------------------------------------- 1 | import { buttonSymbolId } from '../../../../ayon/blocks/symbolIds'; 2 | 3 | ## Live Preview 4 | 5 | You can double click the block to edit its contents. 6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/site/src/pages/docs/blocks/elements/checkbox.mdx: -------------------------------------------------------------------------------- 1 | import { checkboxSymbolId } from '../../../../ayon/blocks/symbolIds'; 2 | 3 | ## Live Preview 4 | 5 | You can double click the block to edit its contents. 6 | 7 | 8 | 9 | ## Configuration 10 | 11 | You can configure the checkbox with text, which will appear as a label to the 12 | right. If you do not want a label, resize the block to a square that fits only 13 | the checkbox. The checkbox has a few different states that can be configured by 14 | adding: 15 | 16 | - `#off` - to uncheck the checkbox (default) 17 | - `#on` - to check the checkbox 18 | - `#disabled` - to disable the checkbox 19 | 20 | Here's an example with a text label, `#on` and `#disabled`: 21 | 22 | 26 | -------------------------------------------------------------------------------- /packages/site/src/pages/docs/blocks/elements/heading.mdx: -------------------------------------------------------------------------------- 1 | import { 2 | heading1SymbolId, 3 | heading3SymbolId, 4 | } from '../../../../ayon/blocks/symbolIds'; 5 | 6 | ## Live Preview 7 | 8 | You can double click the block to edit its contents. 9 | 10 | 11 | 12 | ## Configuration 13 | 14 | There are 6 different pre-defined heading styles, each represented by a 15 | different block type. The block types are: 16 | 17 | - `Heading 1` 18 | - `Heading 2` 19 | - `Heading 3` 20 | - `Heading 4` 21 | - `Heading 5` 22 | - `Heading 6` 23 | 24 | The heading block accepts many `#` commands for styling text. For example, 25 | here's a `Heading 3` block with the styles `#text-red-600 #center`: 26 | 27 | 31 | 32 | ## Limitations 33 | 34 | There is currently no way to change the font size or font family. 35 | -------------------------------------------------------------------------------- /packages/site/src/pages/docs/blocks/elements/input.mdx: -------------------------------------------------------------------------------- 1 | import { inputSymbolId } from '../../../../ayon/blocks/symbolIds'; 2 | 3 | ## Live Preview 4 | 5 | You can double click the block to edit its contents. 6 | 7 | 8 | 9 | ## Configuration 10 | 11 | The input can be prefilled with text. 12 | 13 | You can further configure the input's style by adding: 14 | 15 | - `#dark` - to use the dark theme 16 | - `#accent` - to use the accent color variant of the theme 17 | - `#disabled` - to disable the select 18 | 19 | Here's an example configuration that uses these options (double click to edit): 20 | 21 | 25 | -------------------------------------------------------------------------------- /packages/site/src/pages/docs/blocks/elements/link.mdx: -------------------------------------------------------------------------------- 1 | import { linkSymbolId } from '../../../../ayon/blocks/symbolIds'; 2 | 3 | ## Live Preview 4 | 5 | You can double click the block to edit its contents. 6 | 7 | 8 | 9 | ## Configuration 10 | 11 | The text block accepts many `#` commands for styling text, such as 12 | `#text-red-600 #font-bold`. You can also use the `#icon-arrow-forward` command 13 | to add an arrow icon: 14 | 15 | 19 | -------------------------------------------------------------------------------- /packages/site/src/pages/docs/blocks/elements/radio.mdx: -------------------------------------------------------------------------------- 1 | import { radioSymbolId } from '../../../../ayon/blocks/symbolIds'; 2 | 3 | ## Live Preview 4 | 5 | You can double click the block to edit its contents. 6 | 7 | 8 | 9 | ## Configuration 10 | 11 | You can configure the radio with text, which will appear as a label to the 12 | right. If you do not want a label, resize the block to a square that fits only 13 | the radio. The radio has a few different states that can be configured by 14 | adding: 15 | 16 | - `#off` - to deselect the radio (default) 17 | - `#on` - to select the radio 18 | - `#disabled` - to disable the radio 19 | 20 | Here's an example with a text label, `#on` and `#disabled`: 21 | 22 | 26 | -------------------------------------------------------------------------------- /packages/site/src/pages/docs/blocks/elements/select.mdx: -------------------------------------------------------------------------------- 1 | import { selectSymbolId } from '../../../../ayon/blocks/symbolIds'; 2 | 3 | ## Live Preview 4 | 5 | You can double click the block to edit its contents. 6 | 7 | 8 | 9 | ## Configuration 10 | 11 | Each item in the select should be on a separate line. 12 | 13 | You can further configure the select's style by adding: 14 | 15 | - `#dark` - to use the dark theme 16 | - `#accent` - to use the accent color variant of the theme 17 | - `#disabled` - to disable the select 18 | 19 | Here's an example configuration that uses these options (double click to edit): 20 | 21 | 29 | -------------------------------------------------------------------------------- /packages/site/src/pages/docs/blocks/elements/switch.mdx: -------------------------------------------------------------------------------- 1 | import { switchSymbolId } from '../../../../ayon/blocks/symbolIds'; 2 | 3 | ## Live Preview 4 | 5 | You can double click the block to edit its contents. 6 | 7 | 8 | 9 | ## Configuration 10 | 11 | The switch has a few different states that can be configured by adding: 12 | 13 | - `#off` - to switch to the off position (default) 14 | - `#on` - to switch to the on position 15 | - `#disabled` - to disable the switch 16 | 17 | Here's an example with `#on` and `#disabled`: 18 | 19 | 20 | -------------------------------------------------------------------------------- /packages/site/src/pages/docs/blocks/elements/text.mdx: -------------------------------------------------------------------------------- 1 | import { textSymbolId } from '../../../../ayon/blocks/symbolIds'; 2 | 3 | ## Live Preview 4 | 5 | You can double click the block to edit its contents. 6 | 7 | 8 | 9 | ## Configuration 10 | 11 | The text block accepts many `#` commands for styling text, such as 12 | `#text-red-600 #font-bold`: 13 | 14 | 18 | 19 | ## Limitations 20 | 21 | There is currently no way to change the font size or font family. 22 | -------------------------------------------------------------------------------- /packages/site/src/pages/docs/blocks/elements/textarea.mdx: -------------------------------------------------------------------------------- 1 | import { textareaSymbolId } from '../../../../ayon/blocks/symbolIds'; 2 | 3 | ## Live Preview 4 | 5 | You can double click the block to edit its contents. 6 | 7 | 8 | 9 | ## Configuration 10 | 11 | The textarea can be prefilled with text. 12 | 13 | You can further configure the textarea's style by adding: 14 | 15 | - `#dark` - to use the dark theme 16 | - `#accent` - to use the accent color variant of the theme 17 | - `#disabled` - to disable the select 18 | 19 | Here's an example configuration that uses these options (double click to edit): 20 | 21 | 26 | -------------------------------------------------------------------------------- /packages/site/src/pages/docs/blocks/marketing.mdx: -------------------------------------------------------------------------------- 1 | ## Available blocks 2 | 3 | Here are some of the top blocks you might find useful when creating marketing 4 | pages: 5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/site/src/pages/docs/blocks/marketing/card.mdx: -------------------------------------------------------------------------------- 1 | import { cardSymbolId } from '../../../../ayon/blocks/symbolIds'; 2 | 3 | ## Live Preview 4 | 5 | You can double click the block to edit its contents. 6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/site/src/pages/docs/blocks/marketing/hero.mdx: -------------------------------------------------------------------------------- 1 | import { heroSymbolV2Id } from '../../../../ayon/blocks/symbolIds'; 2 | 3 | ## Live Preview 4 | 5 | You can double click the block to edit its contents. 6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/site/src/pages/docs/blocks/marketing/tile-card.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Tile Card 3 | --- 4 | 5 | import { tileCardSymbolId } from '../../../../ayon/blocks/symbolIds'; 6 | 7 | ## Live Preview 8 | 9 | You can double click the block to edit its contents. 10 | 11 | 12 | -------------------------------------------------------------------------------- /packages/site/src/pages/docs/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "order": ["index", "overview", "blocks"] 3 | } 4 | -------------------------------------------------------------------------------- /packages/site/src/pages/docs/overview.mdx: -------------------------------------------------------------------------------- 1 | ## A better way to wireframe 2 | 3 | Noya is a wireframing tool based around the concept of **blocks**. A **block** 4 | is a wireframe-like representation of a UI component such as "Hero" or "Button". 5 | The Noya tool automatically generates a high-fidelity output in real time by 6 | mapping low-fidelity blocks into high-fidelity design system and React 7 | components. 8 | 9 | ## Why? 10 | 11 | Traditionally, there’s a tricky tradeoff between working in low-fidelity 12 | wireframes vs. high-fidelity mockups when working on a product idea. Working in 13 | low-fidelity allows for quick iteration while hammering out the big picture, but 14 | people often have a hard time imagining the final product. 15 | 16 | Because of this, teams frequently jump to high-fidelity and stop using 17 | wireframes too early, which slows down progress dramatically as they spend an 18 | excessive amount of time perfecting unimportant details. 19 | -------------------------------------------------------------------------------- /packages/site/src/pages/docs/overview/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "order": [ 3 | "projects", 4 | "design-systems", 5 | "creating-blocks", 6 | "styling-blocks", 7 | "region-tool", 8 | "ai-generation", 9 | "export", 10 | "sharing", 11 | "views" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/site/src/pages/docs/overview/design-systems.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Design Systems 3 | --- 4 | 5 | ## Choosing a base design system 6 | 7 | Noya currently supports a single base design system, 8 | [Chakra UI](https://chakra-ui.com/). We chose Chakra for its simplicity and ease 9 | of use in React code. We plan to add first-class support for more open source 10 | design systems in the future, including [MUI](https://mui.com/) and 11 | [Ant Design](https://ant.design/). 12 | 13 | ## Importing your company's design system 14 | 15 | Importing a custom design system isn't supported yet. If you're interested in 16 | being an early adopter of this feature, please 17 | [reach out](https://noyasoftware.notion.site/Noya-Contact-9a95e0895eba4f578517dfdc4d94ccdd)! 18 | -------------------------------------------------------------------------------- /packages/site/src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | // This isn't actually used, since this tsconfig is in the "src" dir, 5 | // but this prevents VSCode "organize imports" from deleting the React import. 6 | // Next automatically sets the "jsx" option to "preserve" when it builds, 7 | // so we can't change it in the parent directory. 8 | "jsx": "react" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/site/src/utils/cookies.ts: -------------------------------------------------------------------------------- 1 | import cookie from 'cookie'; 2 | 3 | export function addShareCookie(id: string) { 4 | if (typeof document === 'undefined') return; 5 | 6 | const map = cookie.parse(document.cookie); 7 | const updatedShares = encodeListCookie(map.noya_shares, id); 8 | 9 | document.cookie = cookie.serialize('noya_shares', updatedShares, { 10 | path: '/', 11 | }); 12 | } 13 | 14 | function encodeListCookie(list: string | undefined, value: string) { 15 | const shares = (list ?? '').split(',').filter(Boolean); 16 | const sharesSet = new Set(shares); 17 | sharesSet.add(value); 18 | 19 | return [...sharesSet].join(','); 20 | } 21 | -------------------------------------------------------------------------------- /packages/site/src/utils/download.ts: -------------------------------------------------------------------------------- 1 | export function downloadBlob(blob: Blob, name: string): void; 2 | export function downloadBlob(blob: File): void; 3 | export function downloadBlob( 4 | ...params: [blob: Blob, name: string] | [blob: File] 5 | ): void { 6 | const exportUrl = URL.createObjectURL(params[0]); 7 | const name = params.length === 1 ? params[0].name : params[1]; 8 | 9 | const a = document.createElement('a'); 10 | a.href = exportUrl; 11 | a.download = name; 12 | a.style.display = 'none'; 13 | document.body.appendChild(a); 14 | a.click(); 15 | 16 | document.body.removeChild(a); 17 | URL.revokeObjectURL(exportUrl); 18 | } 19 | 20 | export async function downloadUrl(url: string, name: string) { 21 | const response = await fetch(url, { credentials: 'include' }); 22 | const blob = await response.blob(); 23 | 24 | downloadBlob(blob, name); 25 | } 26 | -------------------------------------------------------------------------------- /packages/site/src/utils/measureImage.ts: -------------------------------------------------------------------------------- 1 | export async function measureImage(data: ArrayBuffer) { 2 | const image = new Image(); 3 | image.src = URL.createObjectURL(new Blob([data])); 4 | 5 | await new Promise((resolve) => { 6 | image.onload = resolve; 7 | }); 8 | 9 | return { 10 | width() { 11 | return image.width; 12 | }, 13 | height() { 14 | return image.height; 15 | }, 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /packages/site/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "incremental": true, 6 | "jsx": "preserve" 7 | }, 8 | "include": [ 9 | "next-env.d.ts", 10 | "guidebook.d.ts", 11 | "searchIndex.d.ts", 12 | "**/*.ts", 13 | "**/*.tsx" 14 | ], 15 | "exclude": ["node_modules"] 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "modular-scripts/tsconfig.json", 3 | "include": [ 4 | "modular", 5 | "packages/**/src", 6 | "packages/**/next-env.d.ts", 7 | "packages/**/*.config.ts" 8 | ], 9 | "compilerOptions": { 10 | "paths": { 11 | "*": ["./packages/*/src"] 12 | } 13 | } 14 | } 15 | --------------------------------------------------------------------------------