@oh-just-another/react-ui 0.60.0 → 0.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +137 -0
- package/README.md +3 -3
- package/dist/.tsbuildinfo +1 -1
- package/dist/{color-palette.d.ts → color/color-palette.d.ts} +5 -0
- package/dist/color/color-palette.d.ts.map +1 -0
- package/dist/{color-palette.js → color/color-palette.js} +5 -0
- package/dist/color/color-palette.js.map +1 -0
- package/dist/color/color-swatch-picker.d.ts.map +1 -0
- package/dist/{color-swatch-picker.js → color/color-swatch-picker.js} +8 -17
- package/dist/color/color-swatch-picker.js.map +1 -0
- package/dist/{constants.d.ts → core/constants.d.ts} +70 -21
- package/dist/core/constants.d.ts.map +1 -0
- package/dist/{constants.js → core/constants.js} +104 -16
- package/dist/core/constants.js.map +1 -0
- package/dist/core/context.d.ts.map +1 -0
- package/dist/core/context.js.map +1 -0
- package/dist/core/hooks.d.ts.map +1 -0
- package/dist/core/hooks.js.map +1 -0
- package/dist/core/portal-container.d.ts.map +1 -0
- package/dist/core/portal-container.js.map +1 -0
- package/dist/core/use-fullscreen.d.ts +14 -0
- package/dist/core/use-fullscreen.d.ts.map +1 -0
- package/dist/core/use-fullscreen.js +38 -0
- package/dist/core/use-fullscreen.js.map +1 -0
- package/dist/core/use-quiet-gesture.d.ts +11 -0
- package/dist/core/use-quiet-gesture.d.ts.map +1 -0
- package/dist/core/use-quiet-gesture.js +61 -0
- package/dist/core/use-quiet-gesture.js.map +1 -0
- package/dist/core/use-quiet-viewport.d.ts +10 -0
- package/dist/core/use-quiet-viewport.d.ts.map +1 -0
- package/dist/core/use-quiet-viewport.js +40 -0
- package/dist/core/use-quiet-viewport.js.map +1 -0
- package/dist/index.d.ts +55 -50
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +55 -50
- package/dist/index.js.map +1 -1
- package/dist/layout/diagram-bars.d.ts.map +1 -0
- package/dist/layout/diagram-bars.js.map +1 -0
- package/dist/layout/diagram-canvas.d.ts.map +1 -0
- package/dist/layout/diagram-canvas.js.map +1 -0
- package/dist/layout/diagram-root.d.ts.map +1 -0
- package/dist/{diagram-root.js → layout/diagram-root.js} +2 -2
- package/dist/layout/diagram-root.js.map +1 -0
- package/dist/{sidebar.d.ts → layout/sidebar.d.ts} +1 -0
- package/dist/layout/sidebar.d.ts.map +1 -0
- package/dist/layout/sidebar.js +38 -0
- package/dist/layout/sidebar.js.map +1 -0
- package/dist/layout/ui-layer.d.ts.map +1 -0
- package/dist/layout/ui-layer.js.map +1 -0
- package/dist/menus/command-palette.d.ts.map +1 -0
- package/dist/{command-palette.js → menus/command-palette.js} +3 -3
- package/dist/menus/command-palette.js.map +1 -0
- package/dist/menus/context-menu-controller.d.ts.map +1 -0
- package/dist/menus/context-menu-controller.js.map +1 -0
- package/dist/{context-menu.d.ts → menus/context-menu.d.ts} +12 -0
- package/dist/menus/context-menu.d.ts.map +1 -0
- package/dist/menus/context-menu.js +617 -0
- package/dist/menus/context-menu.js.map +1 -0
- package/dist/menus/link-drop-shape-menu.d.ts.map +1 -0
- package/dist/{link-drop-shape-menu.js → menus/link-drop-shape-menu.js} +4 -4
- package/dist/menus/link-drop-shape-menu.js.map +1 -0
- package/dist/{main-menu.d.ts → menus/main-menu.d.ts} +45 -2
- package/dist/menus/main-menu.d.ts.map +1 -0
- package/dist/menus/main-menu.js +260 -0
- package/dist/menus/main-menu.js.map +1 -0
- package/dist/overlays/file-drop-overlay.d.ts +15 -0
- package/dist/overlays/file-drop-overlay.d.ts.map +1 -0
- package/dist/overlays/file-drop-overlay.js +27 -0
- package/dist/overlays/file-drop-overlay.js.map +1 -0
- package/dist/overlays/frame-name-editor-overlay.d.ts.map +1 -0
- package/dist/{frame-name-editor-overlay.js → overlays/frame-name-editor-overlay.js} +1 -1
- package/dist/overlays/frame-name-editor-overlay.js.map +1 -0
- package/dist/overlays/help-button.d.ts.map +1 -0
- package/dist/{help-button.js → overlays/help-button.js} +3 -4
- package/dist/overlays/help-button.js.map +1 -0
- package/dist/overlays/help-dialog.d.ts.map +1 -0
- package/dist/{help-dialog.js → overlays/help-dialog.js} +4 -24
- package/dist/overlays/help-dialog.js.map +1 -0
- package/dist/overlays/link-badges.d.ts +14 -0
- package/dist/overlays/link-badges.d.ts.map +1 -0
- package/dist/overlays/link-badges.js +60 -0
- package/dist/overlays/link-badges.js.map +1 -0
- package/dist/overlays/link-caption-editor.d.ts.map +1 -0
- package/dist/{link-caption-editor.js → overlays/link-caption-editor.js} +3 -3
- package/dist/overlays/link-caption-editor.js.map +1 -0
- package/dist/overlays/link-hover-popup.d.ts.map +1 -0
- package/dist/{link-hover-popup.js → overlays/link-hover-popup.js} +5 -4
- package/dist/overlays/link-hover-popup.js.map +1 -0
- package/dist/overlays/merge-dialog.d.ts.map +1 -0
- package/dist/overlays/merge-dialog.js +39 -0
- package/dist/overlays/merge-dialog.js.map +1 -0
- package/dist/overlays/search-overlay.d.ts.map +1 -0
- package/dist/{search-overlay.js → overlays/search-overlay.js} +2 -2
- package/dist/overlays/search-overlay.js.map +1 -0
- package/dist/overlays/sticky-reactions.d.ts +2 -0
- package/dist/overlays/sticky-reactions.d.ts.map +1 -0
- package/dist/overlays/sticky-reactions.js +94 -0
- package/dist/overlays/sticky-reactions.js.map +1 -0
- package/dist/overlays/text-editor-overlay.d.ts.map +1 -0
- package/dist/{text-editor-overlay.js → overlays/text-editor-overlay.js} +37 -5
- package/dist/overlays/text-editor-overlay.js.map +1 -0
- package/dist/{comments-panel.d.ts → panels/comments-panel.d.ts} +4 -1
- package/dist/panels/comments-panel.d.ts.map +1 -0
- package/dist/panels/comments-panel.js +51 -0
- package/dist/panels/comments-panel.js.map +1 -0
- package/dist/panels/diff-panel.d.ts.map +1 -0
- package/dist/panels/diff-panel.js.map +1 -0
- package/dist/panels/drawing-panel.d.ts.map +1 -0
- package/dist/{drawing-panel.js → panels/drawing-panel.js} +3 -3
- package/dist/panels/drawing-panel.js.map +1 -0
- package/dist/panels/edge-style-panel.d.ts.map +1 -0
- package/dist/{edge-style-panel.js → panels/edge-style-panel.js} +1 -1
- package/dist/panels/edge-style-panel.js.map +1 -0
- package/dist/panels/frame-panel.d.ts.map +1 -0
- package/dist/{frame-panel.js → panels/frame-panel.js} +1 -1
- package/dist/panels/frame-panel.js.map +1 -0
- package/dist/{layer-panel.d.ts → panels/layer-panel.d.ts} +5 -1
- package/dist/panels/layer-panel.d.ts.map +1 -0
- package/dist/panels/layer-panel.js +52 -0
- package/dist/panels/layer-panel.js.map +1 -0
- package/dist/panels/library-panel.d.ts.map +1 -0
- package/dist/{library-panel.js → panels/library-panel.js} +4 -5
- package/dist/panels/library-panel.js.map +1 -0
- package/dist/panels/property-panel.d.ts.map +1 -0
- package/dist/panels/property-panel.js +1397 -0
- package/dist/panels/property-panel.js.map +1 -0
- package/dist/panels/selection-floating-panel.d.ts.map +1 -0
- package/dist/{selection-floating-panel.js → panels/selection-floating-panel.js} +24 -13
- package/dist/panels/selection-floating-panel.js.map +1 -0
- package/dist/panels/stats-panel.d.ts.map +1 -0
- package/dist/{stats-panel.js → panels/stats-panel.js} +1 -1
- package/dist/panels/stats-panel.js.map +1 -0
- package/dist/primitives/bottom-sheet.d.ts.map +1 -0
- package/dist/{bottom-sheet.js → primitives/bottom-sheet.js} +9 -22
- package/dist/primitives/bottom-sheet.js.map +1 -0
- package/dist/primitives/button-group.d.ts.map +1 -0
- package/dist/primitives/button-group.js.map +1 -0
- package/dist/primitives/css-var.d.ts +8 -0
- package/dist/primitives/css-var.d.ts.map +1 -0
- package/dist/primitives/css-var.js +11 -0
- package/dist/primitives/css-var.js.map +1 -0
- package/dist/primitives/float-panel.d.ts +35 -0
- package/dist/primitives/float-panel.d.ts.map +1 -0
- package/dist/primitives/float-panel.js +42 -0
- package/dist/primitives/float-panel.js.map +1 -0
- package/dist/primitives/icon-button.d.ts.map +1 -0
- package/dist/primitives/icon-button.js.map +1 -0
- package/dist/primitives/markdown.d.ts.map +1 -0
- package/dist/primitives/markdown.js.map +1 -0
- package/dist/primitives/modal.d.ts.map +1 -0
- package/dist/{modal.js → primitives/modal.js} +11 -9
- package/dist/primitives/modal.js.map +1 -0
- package/dist/{popover.d.ts → primitives/popover.d.ts} +1 -1
- package/dist/primitives/popover.d.ts.map +1 -0
- package/dist/{popover.js → primitives/popover.js} +7 -18
- package/dist/primitives/popover.js.map +1 -0
- package/dist/primitives/segmented-control.d.ts.map +1 -0
- package/dist/primitives/segmented-control.js.map +1 -0
- package/dist/primitives/slider.d.ts.map +1 -0
- package/dist/primitives/slider.js.map +1 -0
- package/dist/primitives/switch.d.ts +24 -0
- package/dist/primitives/switch.d.ts.map +1 -0
- package/dist/primitives/switch.js +17 -0
- package/dist/primitives/switch.js.map +1 -0
- package/dist/primitives/toast.d.ts.map +1 -0
- package/dist/{toast.js → primitives/toast.js} +12 -33
- package/dist/primitives/toast.js.map +1 -0
- package/dist/primitives/tooltip.d.ts.map +1 -0
- package/dist/{tooltip.js → primitives/tooltip.js} +3 -3
- package/dist/primitives/tooltip.js.map +1 -0
- package/dist/{palette.d.ts → toolbar/palette.d.ts} +6 -1
- package/dist/toolbar/palette.d.ts.map +1 -0
- package/dist/{palette.js → toolbar/palette.js} +39 -5
- package/dist/toolbar/palette.js.map +1 -0
- package/dist/{toolbar.d.ts → toolbar/toolbar.d.ts} +9 -0
- package/dist/toolbar/toolbar.d.ts.map +1 -0
- package/dist/{toolbar.js → toolbar/toolbar.js} +132 -62
- package/dist/toolbar/toolbar.js.map +1 -0
- package/dist/utils/format-time.d.ts.map +1 -0
- package/dist/utils/format-time.js.map +1 -0
- package/dist/utils/use-dismiss-timer.d.ts.map +1 -0
- package/dist/utils/use-dismiss-timer.js.map +1 -0
- package/dist/widgets/minimap.d.ts.map +1 -0
- package/dist/{minimap.js → widgets/minimap.js} +50 -36
- package/dist/widgets/minimap.js.map +1 -0
- package/dist/widgets/reset-to-content-button.d.ts +11 -0
- package/dist/widgets/reset-to-content-button.d.ts.map +1 -0
- package/dist/{reset-to-content-button.js → widgets/reset-to-content-button.js} +8 -9
- package/dist/widgets/reset-to-content-button.js.map +1 -0
- package/dist/widgets/versioning.d.ts.map +1 -0
- package/dist/{versioning.js → widgets/versioning.js} +4 -4
- package/dist/widgets/versioning.js.map +1 -0
- package/dist/widgets/zen-mode.d.ts.map +1 -0
- package/dist/widgets/zen-mode.js.map +1 -0
- package/package.json +9 -9
- package/src/styles.css +1622 -287
- package/dist/bottom-sheet.d.ts.map +0 -1
- package/dist/bottom-sheet.js.map +0 -1
- package/dist/button-group.d.ts.map +0 -1
- package/dist/button-group.js.map +0 -1
- package/dist/color-palette.d.ts.map +0 -1
- package/dist/color-palette.js.map +0 -1
- package/dist/color-swatch-picker.d.ts.map +0 -1
- package/dist/color-swatch-picker.js.map +0 -1
- package/dist/command-palette.d.ts.map +0 -1
- package/dist/command-palette.js.map +0 -1
- package/dist/comments-panel.d.ts.map +0 -1
- package/dist/comments-panel.js +0 -142
- package/dist/comments-panel.js.map +0 -1
- package/dist/constants.d.ts.map +0 -1
- package/dist/constants.js.map +0 -1
- package/dist/context-menu-controller.d.ts.map +0 -1
- package/dist/context-menu-controller.js.map +0 -1
- package/dist/context-menu.d.ts.map +0 -1
- package/dist/context-menu.js +0 -368
- package/dist/context-menu.js.map +0 -1
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js.map +0 -1
- package/dist/diagram-bars.d.ts.map +0 -1
- package/dist/diagram-bars.js.map +0 -1
- package/dist/diagram-canvas.d.ts.map +0 -1
- package/dist/diagram-canvas.js.map +0 -1
- package/dist/diagram-root.d.ts.map +0 -1
- package/dist/diagram-root.js.map +0 -1
- package/dist/diff-panel.d.ts.map +0 -1
- package/dist/diff-panel.js.map +0 -1
- package/dist/drawing-panel.d.ts.map +0 -1
- package/dist/drawing-panel.js.map +0 -1
- package/dist/edge-style-panel.d.ts.map +0 -1
- package/dist/edge-style-panel.js.map +0 -1
- package/dist/format-time.d.ts.map +0 -1
- package/dist/format-time.js.map +0 -1
- package/dist/frame-name-editor-overlay.d.ts.map +0 -1
- package/dist/frame-name-editor-overlay.js.map +0 -1
- package/dist/frame-panel.d.ts.map +0 -1
- package/dist/frame-panel.js.map +0 -1
- package/dist/help-button.d.ts.map +0 -1
- package/dist/help-button.js.map +0 -1
- package/dist/help-dialog.d.ts.map +0 -1
- package/dist/help-dialog.js.map +0 -1
- package/dist/hooks.d.ts.map +0 -1
- package/dist/hooks.js.map +0 -1
- package/dist/icon-button.d.ts.map +0 -1
- package/dist/icon-button.js.map +0 -1
- package/dist/layer-panel.d.ts.map +0 -1
- package/dist/layer-panel.js +0 -117
- package/dist/layer-panel.js.map +0 -1
- package/dist/library-panel.d.ts.map +0 -1
- package/dist/library-panel.js.map +0 -1
- package/dist/link-caption-editor.d.ts.map +0 -1
- package/dist/link-caption-editor.js.map +0 -1
- package/dist/link-drop-shape-menu.d.ts.map +0 -1
- package/dist/link-drop-shape-menu.js.map +0 -1
- package/dist/link-hover-popup.d.ts.map +0 -1
- package/dist/link-hover-popup.js.map +0 -1
- package/dist/main-menu.d.ts.map +0 -1
- package/dist/main-menu.js +0 -249
- package/dist/main-menu.js.map +0 -1
- package/dist/markdown.d.ts.map +0 -1
- package/dist/markdown.js.map +0 -1
- package/dist/merge-dialog.d.ts.map +0 -1
- package/dist/merge-dialog.js +0 -50
- package/dist/merge-dialog.js.map +0 -1
- package/dist/minimap.d.ts.map +0 -1
- package/dist/minimap.js.map +0 -1
- package/dist/modal.d.ts.map +0 -1
- package/dist/modal.js.map +0 -1
- package/dist/palette.d.ts.map +0 -1
- package/dist/palette.js.map +0 -1
- package/dist/popover.d.ts.map +0 -1
- package/dist/popover.js.map +0 -1
- package/dist/portal-container.d.ts.map +0 -1
- package/dist/portal-container.js.map +0 -1
- package/dist/property-panel.d.ts.map +0 -1
- package/dist/property-panel.js +0 -917
- package/dist/property-panel.js.map +0 -1
- package/dist/reset-to-content-button.d.ts +0 -10
- package/dist/reset-to-content-button.d.ts.map +0 -1
- package/dist/reset-to-content-button.js.map +0 -1
- package/dist/search-overlay.d.ts.map +0 -1
- package/dist/search-overlay.js.map +0 -1
- package/dist/segmented-control.d.ts.map +0 -1
- package/dist/segmented-control.js.map +0 -1
- package/dist/selection-floating-panel.d.ts.map +0 -1
- package/dist/selection-floating-panel.js.map +0 -1
- package/dist/sidebar.d.ts.map +0 -1
- package/dist/sidebar.js +0 -68
- package/dist/sidebar.js.map +0 -1
- package/dist/slider.d.ts.map +0 -1
- package/dist/slider.js.map +0 -1
- package/dist/stats-panel.d.ts.map +0 -1
- package/dist/stats-panel.js.map +0 -1
- package/dist/text-editor-overlay.d.ts.map +0 -1
- package/dist/text-editor-overlay.js.map +0 -1
- package/dist/toast.d.ts.map +0 -1
- package/dist/toast.js.map +0 -1
- package/dist/toolbar.d.ts.map +0 -1
- package/dist/toolbar.js.map +0 -1
- package/dist/tooltip.d.ts.map +0 -1
- package/dist/tooltip.js.map +0 -1
- package/dist/ui-layer.d.ts.map +0 -1
- package/dist/ui-layer.js.map +0 -1
- package/dist/use-dismiss-timer.d.ts.map +0 -1
- package/dist/use-dismiss-timer.js.map +0 -1
- package/dist/versioning.d.ts.map +0 -1
- package/dist/versioning.js.map +0 -1
- package/dist/zen-mode.d.ts.map +0 -1
- package/dist/zen-mode.js.map +0 -1
- /package/dist/{color-swatch-picker.d.ts → color/color-swatch-picker.d.ts} +0 -0
- /package/dist/{context.d.ts → core/context.d.ts} +0 -0
- /package/dist/{context.js → core/context.js} +0 -0
- /package/dist/{hooks.d.ts → core/hooks.d.ts} +0 -0
- /package/dist/{hooks.js → core/hooks.js} +0 -0
- /package/dist/{portal-container.d.ts → core/portal-container.d.ts} +0 -0
- /package/dist/{portal-container.js → core/portal-container.js} +0 -0
- /package/dist/{diagram-bars.d.ts → layout/diagram-bars.d.ts} +0 -0
- /package/dist/{diagram-bars.js → layout/diagram-bars.js} +0 -0
- /package/dist/{diagram-canvas.d.ts → layout/diagram-canvas.d.ts} +0 -0
- /package/dist/{diagram-canvas.js → layout/diagram-canvas.js} +0 -0
- /package/dist/{diagram-root.d.ts → layout/diagram-root.d.ts} +0 -0
- /package/dist/{ui-layer.d.ts → layout/ui-layer.d.ts} +0 -0
- /package/dist/{ui-layer.js → layout/ui-layer.js} +0 -0
- /package/dist/{command-palette.d.ts → menus/command-palette.d.ts} +0 -0
- /package/dist/{context-menu-controller.d.ts → menus/context-menu-controller.d.ts} +0 -0
- /package/dist/{context-menu-controller.js → menus/context-menu-controller.js} +0 -0
- /package/dist/{link-drop-shape-menu.d.ts → menus/link-drop-shape-menu.d.ts} +0 -0
- /package/dist/{frame-name-editor-overlay.d.ts → overlays/frame-name-editor-overlay.d.ts} +0 -0
- /package/dist/{help-button.d.ts → overlays/help-button.d.ts} +0 -0
- /package/dist/{help-dialog.d.ts → overlays/help-dialog.d.ts} +0 -0
- /package/dist/{link-caption-editor.d.ts → overlays/link-caption-editor.d.ts} +0 -0
- /package/dist/{link-hover-popup.d.ts → overlays/link-hover-popup.d.ts} +0 -0
- /package/dist/{merge-dialog.d.ts → overlays/merge-dialog.d.ts} +0 -0
- /package/dist/{search-overlay.d.ts → overlays/search-overlay.d.ts} +0 -0
- /package/dist/{text-editor-overlay.d.ts → overlays/text-editor-overlay.d.ts} +0 -0
- /package/dist/{diff-panel.d.ts → panels/diff-panel.d.ts} +0 -0
- /package/dist/{diff-panel.js → panels/diff-panel.js} +0 -0
- /package/dist/{drawing-panel.d.ts → panels/drawing-panel.d.ts} +0 -0
- /package/dist/{edge-style-panel.d.ts → panels/edge-style-panel.d.ts} +0 -0
- /package/dist/{frame-panel.d.ts → panels/frame-panel.d.ts} +0 -0
- /package/dist/{library-panel.d.ts → panels/library-panel.d.ts} +0 -0
- /package/dist/{property-panel.d.ts → panels/property-panel.d.ts} +0 -0
- /package/dist/{selection-floating-panel.d.ts → panels/selection-floating-panel.d.ts} +0 -0
- /package/dist/{stats-panel.d.ts → panels/stats-panel.d.ts} +0 -0
- /package/dist/{bottom-sheet.d.ts → primitives/bottom-sheet.d.ts} +0 -0
- /package/dist/{button-group.d.ts → primitives/button-group.d.ts} +0 -0
- /package/dist/{button-group.js → primitives/button-group.js} +0 -0
- /package/dist/{icon-button.d.ts → primitives/icon-button.d.ts} +0 -0
- /package/dist/{icon-button.js → primitives/icon-button.js} +0 -0
- /package/dist/{markdown.d.ts → primitives/markdown.d.ts} +0 -0
- /package/dist/{markdown.js → primitives/markdown.js} +0 -0
- /package/dist/{modal.d.ts → primitives/modal.d.ts} +0 -0
- /package/dist/{segmented-control.d.ts → primitives/segmented-control.d.ts} +0 -0
- /package/dist/{segmented-control.js → primitives/segmented-control.js} +0 -0
- /package/dist/{slider.d.ts → primitives/slider.d.ts} +0 -0
- /package/dist/{slider.js → primitives/slider.js} +0 -0
- /package/dist/{toast.d.ts → primitives/toast.d.ts} +0 -0
- /package/dist/{tooltip.d.ts → primitives/tooltip.d.ts} +0 -0
- /package/dist/{format-time.d.ts → utils/format-time.d.ts} +0 -0
- /package/dist/{format-time.js → utils/format-time.js} +0 -0
- /package/dist/{use-dismiss-timer.d.ts → utils/use-dismiss-timer.d.ts} +0 -0
- /package/dist/{use-dismiss-timer.js → utils/use-dismiss-timer.js} +0 -0
- /package/dist/{minimap.d.ts → widgets/minimap.d.ts} +0 -0
- /package/dist/{versioning.d.ts → widgets/versioning.d.ts} +0 -0
- /package/dist/{zen-mode.d.ts → widgets/zen-mode.d.ts} +0 -0
- /package/dist/{zen-mode.js → widgets/zen-mode.js} +0 -0
package/dist/property-panel.js
DELETED
|
@@ -1,917 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useRef, useState } from "react";
|
|
3
|
-
import { AlignCenter, AlignCenterHorizontal, AlignCenterVertical, AlignEndHorizontal, AlignEndVertical, AlignHorizontalDistributeCenter, AlignLeft, AlignRight, AlignStartHorizontal, AlignStartVertical, AlignVerticalDistributeCenter, Bold, CaseSensitive, ChevronsDown, ChevronsUp, Circle, Copy as CopyIcon, Crop, Diamond, FlipHorizontal2 as FlipHorizontalIcon, FlipVertical2 as FlipVerticalIcon, Group as GroupIcon, Italic, Link as LinkIcon, Minus, MoreHorizontal, MoreVertical, MoveDown, MoveRight, MoveUp, Spline, Square, SquareDashed, SquareDot, Strikethrough, Trash2, Underline, Ungroup as UngroupIcon, Waypoints, } from "lucide-react";
|
|
4
|
-
import { isGroup, isText, isImage, isFrame, isRectangle, isEllipse, isBrush, isPolygon, sliceRuns, } from "@oh-just-another/scene";
|
|
5
|
-
import { useDiagramOptional, useReadOnly, useScene, useSelectedLink, useSelection, } from "./hooks.js";
|
|
6
|
-
import { useEditorSelector } from "./context.js";
|
|
7
|
-
import { useContextMenuController } from "./context-menu-controller.js";
|
|
8
|
-
import { ColorSwatchPicker } from "./color-swatch-picker.js";
|
|
9
|
-
import { Popover } from "./popover.js";
|
|
10
|
-
import { SegmentedControl } from "./segmented-control.js";
|
|
11
|
-
import { Slider } from "./slider.js";
|
|
12
|
-
import { BRUSH_WIDTH_MAX, BRUSH_WIDTH_MIN, TEXT_FONT_SIZE_MAX, TEXT_FONT_SIZE_MIN, TEXT_FONT_SIZE_PRESETS, TEXT_FONT_STACKS, } from "./constants.js";
|
|
13
|
-
export const PropertyPanel = ({ style, className, mobile = false }) => {
|
|
14
|
-
const selection = useSelection();
|
|
15
|
-
const selectedLinkId = useSelectedLink();
|
|
16
|
-
const scene = useScene();
|
|
17
|
-
const readOnly = useReadOnly();
|
|
18
|
-
// Every control here mutates the selection (style / text / z-order / align /
|
|
19
|
-
// convert / delete / link). In read-only / view mode the whole panel is
|
|
20
|
-
// suppressed — the canvas is view-only, so there's nothing to edit.
|
|
21
|
-
if (readOnly)
|
|
22
|
-
return null;
|
|
23
|
-
// Dispatcher: edge wins only when no shape is selected — if both
|
|
24
|
-
// happen to be set (rare), the shape panel is more useful. Each branch
|
|
25
|
-
// splits its controls into `primary` (always visible) and `overflow`
|
|
26
|
-
// (behind the ⋮ on mobile; inline on desktop).
|
|
27
|
-
if (selection.size > 0) {
|
|
28
|
-
const shapes = [...selection]
|
|
29
|
-
.map((id) => scene.elements.get(id))
|
|
30
|
-
.filter((s) => s !== undefined);
|
|
31
|
-
if (shapes.length === 0)
|
|
32
|
-
return null;
|
|
33
|
-
const allText = shapes.every((s) => isText(s));
|
|
34
|
-
const allImage = shapes.every((s) => isImage(s));
|
|
35
|
-
const allBrush = shapes.every((s) => isBrush(s));
|
|
36
|
-
const allFrame = shapes.every((s) => isFrame(s));
|
|
37
|
-
const primary = [];
|
|
38
|
-
const overflow = [];
|
|
39
|
-
if (allFrame) {
|
|
40
|
-
// A frame's border is fixed chrome (dashed outline); only its body fill
|
|
41
|
-
// is user-configurable. No stroke / width / dash / roundness controls.
|
|
42
|
-
primary.push(_jsx(FillControl, { shapes: shapes }, "fill"));
|
|
43
|
-
}
|
|
44
|
-
else if (allText) {
|
|
45
|
-
primary.push(_jsx(FontSizeControl, { shapes: shapes }, "size"), _jsx(ColorOpacityControl, { shapes: shapes }, "color"), _jsx(TextAlignControl, { shapes: shapes }, "align"));
|
|
46
|
-
overflow.push(_jsx(FontFamilyControl, { shapes: shapes }, "family"), _jsx(TextDecorationControl, { shapes: shapes }, "decor"));
|
|
47
|
-
}
|
|
48
|
-
else if (allImage) {
|
|
49
|
-
// An image's pixels are the content — fill/stroke make no sense.
|
|
50
|
-
primary.push(_jsx(OpacityControl, { shapes: shapes }, "opacity"), _jsx(CropControl, { shapes: shapes }, "crop"));
|
|
51
|
-
}
|
|
52
|
-
else {
|
|
53
|
-
primary.push(_jsx(FillControl, { shapes: shapes }, "fill"), _jsx(StrokeControl, { shapes: shapes }, "stroke"),
|
|
54
|
-
// Brush widths are baked per point — `style.strokeWidth` has no
|
|
55
|
-
// effect, so brush-only selections get a slider that re-bases the
|
|
56
|
-
// baked widths instead of the Thin/Medium/Thick segmented control.
|
|
57
|
-
allBrush ? (_jsx(BrushWidthControl, { shapes: shapes }, "width")) : (_jsx(StrokeWidthControl, { shapes: shapes }, "width")));
|
|
58
|
-
overflow.push(_jsx(StrokeStyleControl, { shapes: shapes }, "dash"), _jsx(RoundnessControl, { shapes: shapes }, "round"), _jsx(OpacityControl, { shapes: shapes }, "opacity"), _jsx(ConvertTypeControl, { shapes: shapes }, "convert"));
|
|
59
|
-
}
|
|
60
|
-
// Common trailing controls for every shape type.
|
|
61
|
-
overflow.push(_jsx(ZOrderControl, {}, "z"), _jsx(LinkControl, { shapes: shapes }, "link"));
|
|
62
|
-
// Alignment needs a reference box — only meaningful for 2+ shapes.
|
|
63
|
-
if (shapes.length >= 2)
|
|
64
|
-
overflow.push(_jsx(AlignControl, {}, "align"));
|
|
65
|
-
overflow.push(_jsx(ActionsControl, { shapes: shapes }, "actions"), _jsx(MoreButton, {}, "more"));
|
|
66
|
-
return (_jsx(PanelShell, { mobile: mobile, primary: primary, overflow: overflow, className: className, style: style }));
|
|
67
|
-
}
|
|
68
|
-
if (selectedLinkId !== null) {
|
|
69
|
-
const edge = scene.links.get(selectedLinkId);
|
|
70
|
-
if (!edge)
|
|
71
|
-
return null;
|
|
72
|
-
const primary = [
|
|
73
|
-
_jsx(LinkStrokeColorControl, { edge: edge }, "color"),
|
|
74
|
-
_jsx(LinkStrokeWidthControl, { edge: edge }, "width"),
|
|
75
|
-
_jsx(LinkArrowheadControl, { edge: edge, side: "to" }, "arrow-to"),
|
|
76
|
-
];
|
|
77
|
-
const overflow = [
|
|
78
|
-
_jsx(LinkStrokeStyleControl, { edge: edge }, "dash"),
|
|
79
|
-
_jsx(LinkRoutingControl, { edge: edge }, "routing"),
|
|
80
|
-
_jsx(LinkLineKindControl, { edge: edge }, "kind"),
|
|
81
|
-
_jsx(LinkArrowheadControl, { edge: edge, side: "from" }, "arrow-from"),
|
|
82
|
-
_jsx(LinkAutoRouteControl, { edge: edge }, "auto"),
|
|
83
|
-
_jsx(LinkDeleteControl, {}, "delete"),
|
|
84
|
-
_jsx(MoreButton, {}, "more"),
|
|
85
|
-
];
|
|
86
|
-
return (_jsx(PanelShell, { mobile: mobile, primary: primary, overflow: overflow, className: className, style: style }));
|
|
87
|
-
}
|
|
88
|
-
return null;
|
|
89
|
-
};
|
|
90
|
-
/**
|
|
91
|
-
* Lays out the primary / overflow control groups. Desktop = one floating
|
|
92
|
-
* pill row (primary · divider · overflow). Mobile = a primary row with a
|
|
93
|
-
* vertical-dots ⋮ that expands a wrapped overflow grid below it.
|
|
94
|
-
*/
|
|
95
|
-
const PanelShell = ({ mobile, primary, overflow, className, style, }) => {
|
|
96
|
-
const [expanded, setExpanded] = useState(false);
|
|
97
|
-
const panelRef = useRef(null);
|
|
98
|
-
// Collapse the expanded overflow sheet on a tap outside the panel.
|
|
99
|
-
// The ⋮ button and the grabber collapse it directly; this covers taps
|
|
100
|
-
// on the canvas / elsewhere.
|
|
101
|
-
useEffect(() => {
|
|
102
|
-
if (!mobile || !expanded)
|
|
103
|
-
return undefined;
|
|
104
|
-
const onDown = (e) => {
|
|
105
|
-
const el = panelRef.current;
|
|
106
|
-
if (el && e.target instanceof Node && !el.contains(e.target))
|
|
107
|
-
setExpanded(false);
|
|
108
|
-
};
|
|
109
|
-
// `capture` so we see the tap even if something stops propagation.
|
|
110
|
-
document.addEventListener("pointerdown", onDown, true);
|
|
111
|
-
return () => {
|
|
112
|
-
document.removeEventListener("pointerdown", onDown, true);
|
|
113
|
-
};
|
|
114
|
-
}, [mobile, expanded]);
|
|
115
|
-
if (!mobile) {
|
|
116
|
-
return (_jsxs("div", { className: `du-sel-panel ${className ?? ""}`.trim(), style: style, children: [primary, overflow.length > 0 ? _jsx(Divider, {}) : null, overflow] }));
|
|
117
|
-
}
|
|
118
|
-
return (_jsxs("div", { ref: panelRef, className: `du-sel-panel du-sel-panel-mobile ${className ?? ""}`.trim(), style: style, children: [_jsxs("div", { className: "du-sel-mobile-row", children: [_jsx("div", { className: "du-sel-mobile-primary", children: primary }), overflow.length > 0 ? (_jsx("button", { type: "button", className: `du-sel-icon-button du-sel-mobile-expand${expanded ? " is-active" : ""}`, "aria-expanded": expanded, "aria-label": expanded ? "Hide more properties" : "More properties", title: "More properties", onClick: () => {
|
|
119
|
-
setExpanded((v) => !v);
|
|
120
|
-
}, children: _jsx(MoreVertical, { size: 18, strokeWidth: 1.75, "aria-hidden": true }) })) : null] }), expanded && overflow.length > 0 ? (_jsx("div", { className: "du-sel-mobile-overflow", children: overflow })) : null] }));
|
|
121
|
-
};
|
|
122
|
-
/**
|
|
123
|
-
* Element link control. One trigger (chain icon, active when a link is
|
|
124
|
-
* set) opens a popover with a URL field plus Save / Open / Remove. Works
|
|
125
|
-
* for any shape — the href lives on `ElementBase`. The URL is normalised
|
|
126
|
-
* (`normalizeHref`: adds `https://`, `mailto:`, rejects `javascript:`)
|
|
127
|
-
* before storing. Multi-select applies to all.
|
|
128
|
-
*/
|
|
129
|
-
const LinkControl = ({ shapes }) => {
|
|
130
|
-
const editor = useDiagramOptional();
|
|
131
|
-
const inputRef = useRef(null);
|
|
132
|
-
if (!editor)
|
|
133
|
-
return null;
|
|
134
|
-
const ids = shapes.map((s) => s.id);
|
|
135
|
-
const current = sharedString(shapes, (s) => s.href);
|
|
136
|
-
const hasLink = shapes.some((s) => Boolean(s.href));
|
|
137
|
-
const save = (raw) => {
|
|
138
|
-
editor.setLink(ids, raw);
|
|
139
|
-
};
|
|
140
|
-
return (_jsx(Popover, { ariaLabel: "Link", trigger: _jsx("button", { type: "button", className: `du-sel-icon-button${hasLink ? " is-active" : ""}`, title: "Link", "aria-label": "Link", children: _jsx(LinkIcon, { size: 14, strokeWidth: 1.75, "aria-hidden": true }) }), children: _jsxs("div", { className: "du-sel-popover-section", children: [_jsx("header", { className: "du-sel-popover-label", children: "Link" }), _jsx("input", { ref: inputRef, className: "du-sel-link-input", type: "text", placeholder: "https://\u2026 \u00B7 name@mail", defaultValue: current ?? "", "aria-label": "Link URL", onKeyDown: (ev) => {
|
|
141
|
-
if (ev.key === "Enter") {
|
|
142
|
-
ev.preventDefault();
|
|
143
|
-
save(ev.currentTarget.value);
|
|
144
|
-
}
|
|
145
|
-
} }), _jsxs("div", { style: { display: "flex", gap: 2 }, children: [_jsx("button", { type: "button", className: "du-sel-text-button", onClick: () => {
|
|
146
|
-
save(inputRef.current?.value ?? "");
|
|
147
|
-
}, children: "Save" }), hasLink ? (_jsx("button", { type: "button", className: "du-sel-text-button", onClick: () => {
|
|
148
|
-
editor.openLink(current);
|
|
149
|
-
}, children: "Open" })) : null, hasLink ? (_jsx("button", { type: "button", className: "du-sel-text-button", onClick: () => {
|
|
150
|
-
editor.setLink(ids, null);
|
|
151
|
-
}, children: "Remove" })) : null] })] }) }));
|
|
152
|
-
};
|
|
153
|
-
const MoreButton = () => {
|
|
154
|
-
const editor = useDiagramOptional();
|
|
155
|
-
const controller = useContextMenuController();
|
|
156
|
-
if (!editor || !controller)
|
|
157
|
-
return null;
|
|
158
|
-
return (_jsx("button", { type: "button", className: "du-sel-icon-button", title: "More actions", "aria-label": "More actions", onClick: (ev) => {
|
|
159
|
-
const rect = ev.currentTarget.getBoundingClientRect();
|
|
160
|
-
const screenPoint = { x: rect.left, y: rect.bottom + 4 };
|
|
161
|
-
const host = editor.hostElement;
|
|
162
|
-
const hostRect = host?.getBoundingClientRect();
|
|
163
|
-
const worldPoint = hostRect
|
|
164
|
-
? editor.screenToWorld({
|
|
165
|
-
x: screenPoint.x - hostRect.left,
|
|
166
|
-
y: screenPoint.y - hostRect.top,
|
|
167
|
-
})
|
|
168
|
-
: { x: 0, y: 0 };
|
|
169
|
-
controller.open({ screenPoint, worldPoint });
|
|
170
|
-
}, children: _jsx(MoreHorizontal, { size: 14, strokeWidth: 1.75, "aria-hidden": true }) }));
|
|
171
|
-
};
|
|
172
|
-
// ---------------------------------------------------------------------------
|
|
173
|
-
// Inline controls
|
|
174
|
-
// ---------------------------------------------------------------------------
|
|
175
|
-
/**
|
|
176
|
-
* Color trigger: a 24×24 square button with a colored fill plus an
|
|
177
|
-
* outer ring, opens the full swatch picker in a popover.
|
|
178
|
-
*/
|
|
179
|
-
const ColorTrigger = ({ label, color, onChange, ariaLabel, onEyedrop, }) => (_jsx(Popover, { ariaLabel: ariaLabel, trigger: _jsx("button", { type: "button", className: "du-sel-color-trigger", title: label, "aria-label": ariaLabel, children: _jsx("span", { className: "du-sel-color-swatch", style: {
|
|
180
|
-
background: !color || color === "transparent"
|
|
181
|
-
? "repeating-conic-gradient(#ccc 0% 25%, #fff 0% 50%) 50% / 6px 6px"
|
|
182
|
-
: color,
|
|
183
|
-
} }) }), children: _jsxs("div", { className: "du-sel-popover-section", children: [_jsx("header", { className: "du-sel-popover-label", children: label }), _jsx(ColorSwatchPicker, { value: color, onChange: onChange, ...(onEyedrop ? { onEyedrop } : {}) })] }) }));
|
|
184
|
-
const useTextRunRange = (shapes) => {
|
|
185
|
-
const editingId = useEditorSelector((e) => e.editingTextElement, null);
|
|
186
|
-
const sel = useEditorSelector((e) => e.editingTextSelection, null);
|
|
187
|
-
if (editingId === null || sel === null || sel.start === sel.end)
|
|
188
|
-
return null;
|
|
189
|
-
const target = shapes.find((s) => s.id === editingId);
|
|
190
|
-
if (target === undefined || !isText(target))
|
|
191
|
-
return null;
|
|
192
|
-
return { target, from: Math.min(sel.start, sel.end), to: Math.max(sel.start, sel.end) };
|
|
193
|
-
};
|
|
194
|
-
/**
|
|
195
|
-
* Combined color & opacity control — a single swatch trigger whose
|
|
196
|
-
* popover has both the palette and an opacity slider. Used for the text
|
|
197
|
-
* panel in place of separate Fill + Opacity triggers. Writes `fill` and
|
|
198
|
-
* `opacity` through `editor.updateStyle`.
|
|
199
|
-
*/
|
|
200
|
-
const ColorOpacityControl = ({ shapes }) => {
|
|
201
|
-
const editor = useDiagramOptional();
|
|
202
|
-
const runRange = useTextRunRange(shapes);
|
|
203
|
-
if (!editor)
|
|
204
|
-
return null;
|
|
205
|
-
const ids = shapes.map((s) => s.id);
|
|
206
|
-
const color = sharedString(shapes, (s) => s.style.fill);
|
|
207
|
-
const opacity = sharedValue(shapes, (s) => s.style.opacity ?? 1);
|
|
208
|
-
const pct = opacity === null ? null : Math.round(opacity * 100);
|
|
209
|
-
const swatchBg = !color || color === "transparent"
|
|
210
|
-
? "repeating-conic-gradient(#ccc 0% 25%, #fff 0% 50%) 50% / 6px 6px"
|
|
211
|
-
: color;
|
|
212
|
-
return (_jsx(Popover, { ariaLabel: "Text color and opacity", trigger: _jsx("button", { type: "button", className: "du-sel-color-trigger", title: "Color & opacity", "aria-label": "Text color and opacity", children: _jsx("span", { className: "du-sel-color-swatch", style: { background: swatchBg, opacity: opacity ?? 1 } }) }), children: _jsxs("div", { className: "du-sel-popover-section", children: [_jsx("header", { className: "du-sel-popover-label", children: "Color" }), _jsx(ColorSwatchPicker, { value: color, onChange: (v) => {
|
|
213
|
-
// In-edit text selection → colour just those characters (runs);
|
|
214
|
-
// otherwise colour the whole element(s).
|
|
215
|
-
if (runRange) {
|
|
216
|
-
editor.applyTextStyleToRange(runRange.target.id, runRange.from, runRange.to, {
|
|
217
|
-
fill: v ?? "transparent",
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
else {
|
|
221
|
-
editor.updateStyle(ids, { fill: v ?? "transparent" });
|
|
222
|
-
}
|
|
223
|
-
}, onEyedrop: (cb) => {
|
|
224
|
-
editor.beginEyedropperPick(cb);
|
|
225
|
-
} }), _jsx("header", { className: "du-sel-popover-label", children: "Opacity" }), _jsx(Slider, { value: pct, min: 0, max: 100, step: 5, ariaLabel: "Opacity", valueLabel: pct === null ? "—" : `${pct}%`, onChange: (v) => {
|
|
226
|
-
editor.updateStyle(ids, { opacity: v / 100 });
|
|
227
|
-
} })] }) }));
|
|
228
|
-
};
|
|
229
|
-
const FillControl = ({ shapes }) => {
|
|
230
|
-
const editor = useDiagramOptional();
|
|
231
|
-
if (!editor || !shapes.some(hasFill))
|
|
232
|
-
return null;
|
|
233
|
-
const value = sharedString(shapes, (s) => s.style.fill);
|
|
234
|
-
const ids = shapes.map((s) => s.id);
|
|
235
|
-
return (_jsx(ColorTrigger, { label: "Fill", ariaLabel: "Fill color", color: value, onChange: (v) => {
|
|
236
|
-
editor.updateStyle(ids, { fill: v ?? "transparent" });
|
|
237
|
-
}, onEyedrop: (cb) => {
|
|
238
|
-
editor.beginEyedropperPick(cb);
|
|
239
|
-
} }));
|
|
240
|
-
};
|
|
241
|
-
// ---------------------------------------------------------------------------
|
|
242
|
-
// Text controls — rendered only for a text-only selection (see the
|
|
243
|
-
// dispatcher in `PropertyPanel`). `fontSize` / `fontFamily` are
|
|
244
|
-
// top-level `TextElement` fields written via `editor.updateTextProps`;
|
|
245
|
-
// `textAlign` / `textBaseline` are `TextStyle` fields written via
|
|
246
|
-
// `editor.updateStyle`.
|
|
247
|
-
// ---------------------------------------------------------------------------
|
|
248
|
-
const FontSizeControl = ({ shapes }) => {
|
|
249
|
-
const editor = useDiagramOptional();
|
|
250
|
-
if (!editor)
|
|
251
|
-
return null;
|
|
252
|
-
const ids = shapes.map((s) => s.id);
|
|
253
|
-
const value = sharedValue(shapes,
|
|
254
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- cast asserts TextElement; non-text shapes lack fontSize at runtime
|
|
255
|
-
(s) => s.fontSize ?? null);
|
|
256
|
-
const presetValue = value !== null && TEXT_FONT_SIZE_PRESETS.some((p) => p.value === value) ? value : null;
|
|
257
|
-
return (_jsx(Popover, { ariaLabel: "Font size", trigger: _jsx("button", { type: "button", className: "du-sel-text-button", title: "Font size", "aria-label": `Font size ${value ?? "mixed"}`, children: value === null ? "—" : `${Math.round(value)}` }), children: _jsxs("div", { className: "du-sel-popover-section", children: [_jsx("header", { className: "du-sel-popover-label", children: "Font size" }), _jsx(SegmentedControl, { ariaLabel: "Font size preset", value: presetValue, options: TEXT_FONT_SIZE_PRESETS.map((p) => ({
|
|
258
|
-
value: p.value,
|
|
259
|
-
label: p.label,
|
|
260
|
-
icon: _jsx("span", { style: { fontSize: 11, fontWeight: 600 }, children: p.label }),
|
|
261
|
-
})), onChange: (v) => {
|
|
262
|
-
editor.updateTextProps(ids, { fontSize: v });
|
|
263
|
-
} }), _jsx(Slider, { value: value, min: TEXT_FONT_SIZE_MIN, max: TEXT_FONT_SIZE_MAX, step: 1, ariaLabel: "Font size", valueLabel: value === null ? "—" : `${value}px`, onChange: (v) => {
|
|
264
|
-
editor.updateTextProps(ids, { fontSize: v });
|
|
265
|
-
} })] }) }));
|
|
266
|
-
};
|
|
267
|
-
const FontFamilyControl = ({ shapes }) => {
|
|
268
|
-
const editor = useDiagramOptional();
|
|
269
|
-
if (!editor)
|
|
270
|
-
return null;
|
|
271
|
-
const ids = shapes.map((s) => s.id);
|
|
272
|
-
const value = sharedString(shapes, (s) => s.fontFamily);
|
|
273
|
-
const current = TEXT_FONT_STACKS.find((f) => f.value === value);
|
|
274
|
-
const label = value === null ? "Mixed" : (current?.label ?? "Custom");
|
|
275
|
-
return (_jsx(Popover, { ariaLabel: "Font family", trigger: _jsx("button", { type: "button", className: "du-sel-text-button", title: "Font family", "aria-label": `Font family ${label}`, style: value ? { fontFamily: value } : undefined, children: label }), children: _jsxs("div", { className: "du-sel-popover-section", children: [_jsx("header", { className: "du-sel-popover-label", children: "Font family" }), TEXT_FONT_STACKS.map((f) => (_jsx("button", { type: "button", role: "menuitemradio", "aria-checked": f.value === value, className: `du-sel-menu-row${f.value === value ? " is-active" : ""}`, style: { fontFamily: f.value }, onClick: () => {
|
|
276
|
-
editor.updateTextProps(ids, { fontFamily: f.value });
|
|
277
|
-
}, children: f.label }, f.value)))] }) }));
|
|
278
|
-
};
|
|
279
|
-
const TextAlignControl = ({ shapes }) => {
|
|
280
|
-
const editor = useDiagramOptional();
|
|
281
|
-
if (!editor)
|
|
282
|
-
return null;
|
|
283
|
-
const ids = shapes.map((s) => s.id);
|
|
284
|
-
const value = sharedValue(shapes, (s) => s.style.textAlign ?? "left");
|
|
285
|
-
return (_jsx(SegmentedControl, { ariaLabel: "Text alignment", value: value, options: [
|
|
286
|
-
{ value: "left", label: "Left", icon: _jsx(AlignLeft, { size: 14, strokeWidth: 1.75 }) },
|
|
287
|
-
{ value: "center", label: "Center", icon: _jsx(AlignCenter, { size: 14, strokeWidth: 1.75 }) },
|
|
288
|
-
{ value: "right", label: "Right", icon: _jsx(AlignRight, { size: 14, strokeWidth: 1.75 }) },
|
|
289
|
-
], onChange: (v) => {
|
|
290
|
-
editor.updateStyle(ids, { textAlign: v });
|
|
291
|
-
} }));
|
|
292
|
-
};
|
|
293
|
-
/**
|
|
294
|
-
* One decoration toggle inside {@link TextDecorationControl}'s popover.
|
|
295
|
-
* MUST stay a module-level component: defining it inside its parent would
|
|
296
|
-
* give it a fresh identity on every parent re-render, so React would
|
|
297
|
-
* remount the `<button>` each frame. During inline text editing the panel
|
|
298
|
-
* re-renders constantly (caret blink), and a remount between `mousedown`
|
|
299
|
-
* and `mouseup` swallows the synthesized `click` — the toggle would then
|
|
300
|
-
* silently never fire.
|
|
301
|
-
*/
|
|
302
|
-
const TextStyleToggle = ({ active, label, icon, onClick, }) => (_jsx("button", { type: "button", className: `du-sel-icon-button${active ? " is-active" : ""}`, title: label, "aria-label": label, "aria-pressed": active, onClick: onClick, children: icon }));
|
|
303
|
-
/**
|
|
304
|
-
* Bold / Italic / Underline / Strikethrough. One trigger (Aa) opens a
|
|
305
|
-
* popover with four independent toggles. Each writes through
|
|
306
|
-
* `editor.updateStyle`: bold→`fontWeight`, italic→`fontStyle`,
|
|
307
|
-
* underline/strikethrough→merged `textDecoration`. Active = every
|
|
308
|
-
* selected shape already has that decoration on.
|
|
309
|
-
*/
|
|
310
|
-
const TextDecorationControl = ({ shapes }) => {
|
|
311
|
-
const editor = useDiagramOptional();
|
|
312
|
-
const runRange = useTextRunRange(shapes);
|
|
313
|
-
if (!editor)
|
|
314
|
-
return null;
|
|
315
|
-
const ids = shapes.map((s) => s.id);
|
|
316
|
-
// Active state: over an in-edit range, read the effective per-run style
|
|
317
|
-
// (run overlay ?? element style); otherwise read the whole-element style.
|
|
318
|
-
const rangeSegs = runRange ? sliceRuns(runRange.target, runRange.from, runRange.to) : [];
|
|
319
|
-
const base = runRange?.target.style;
|
|
320
|
-
const segEvery = (pred) => rangeSegs.length > 0 && rangeSegs.every((r) => pred(r.style));
|
|
321
|
-
const allBold = runRange
|
|
322
|
-
? segEvery((st) => (st?.fontWeight ?? base?.fontWeight) === "bold")
|
|
323
|
-
: shapes.every((s) => s.style?.fontWeight === "bold");
|
|
324
|
-
const allItalic = runRange
|
|
325
|
-
? segEvery((st) => (st?.fontStyle ?? base?.fontStyle) === "italic")
|
|
326
|
-
: shapes.every((s) => s.style?.fontStyle === "italic");
|
|
327
|
-
const allUnderline = runRange
|
|
328
|
-
? segEvery((st) => (st?.textDecoration ?? base?.textDecoration)?.underline === true)
|
|
329
|
-
: shapes.every((s) => s.style?.textDecoration?.underline === true);
|
|
330
|
-
const allStrike = runRange
|
|
331
|
-
? segEvery((st) => (st?.textDecoration ?? base?.textDecoration)?.strikethrough === true)
|
|
332
|
-
: shapes.every((s) => s.style?.textDecoration?.strikethrough === true);
|
|
333
|
-
// Apply a partial text style to the in-edit range (rich text) or, with no
|
|
334
|
-
// active range, to the whole selected element(s).
|
|
335
|
-
const applyPartial = (partial) => {
|
|
336
|
-
if (runRange) {
|
|
337
|
-
editor.applyTextStyleToRange(runRange.target.id, runRange.from, runRange.to, partial);
|
|
338
|
-
}
|
|
339
|
-
else {
|
|
340
|
-
editor.updateStyle(ids, partial);
|
|
341
|
-
}
|
|
342
|
-
};
|
|
343
|
-
// Toggling underline/strikethrough must preserve the other flag. Range mode
|
|
344
|
-
// replaces the whole `textDecoration`, so rebuild both flags from the
|
|
345
|
-
// range's current state; whole-element mode merges per shape.
|
|
346
|
-
const setDecoration = (key, on) => {
|
|
347
|
-
if (runRange) {
|
|
348
|
-
editor.applyTextStyleToRange(runRange.target.id, runRange.from, runRange.to, {
|
|
349
|
-
textDecoration: { underline: allUnderline, strikethrough: allStrike, [key]: on },
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
else {
|
|
353
|
-
for (const s of shapes) {
|
|
354
|
-
const cur = s.style?.textDecoration ?? {};
|
|
355
|
-
editor.updateStyle([s.id], { textDecoration: { ...cur, [key]: on } });
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
};
|
|
359
|
-
return (_jsx(Popover, { ariaLabel: "Text style", trigger: _jsx("button", { type: "button", className: "du-sel-icon-button", title: "Text style (bold / italic / \u2026)", "aria-label": "Text style", children: _jsx(CaseSensitive, { size: 16, strokeWidth: 1.75, "aria-hidden": true }) }), children: _jsxs("div", { className: "du-sel-popover-section", children: [_jsx("header", { className: "du-sel-popover-label", children: "Style" }), _jsxs("div", { style: { display: "flex", gap: 2 }, children: [_jsx(TextStyleToggle, { active: allBold, label: "Bold", icon: _jsx(Bold, { size: 14, strokeWidth: 1.75 }), onClick: () => {
|
|
360
|
-
applyPartial({ fontWeight: allBold ? "normal" : "bold" });
|
|
361
|
-
} }), _jsx(TextStyleToggle, { active: allItalic, label: "Italic", icon: _jsx(Italic, { size: 14, strokeWidth: 1.75 }), onClick: () => {
|
|
362
|
-
applyPartial({ fontStyle: allItalic ? "normal" : "italic" });
|
|
363
|
-
} }), _jsx(TextStyleToggle, { active: allUnderline, label: "Underline", icon: _jsx(Underline, { size: 14, strokeWidth: 1.75 }), onClick: () => {
|
|
364
|
-
setDecoration("underline", !allUnderline);
|
|
365
|
-
} }), _jsx(TextStyleToggle, { active: allStrike, label: "Strikethrough", icon: _jsx(Strikethrough, { size: 14, strokeWidth: 1.75 }), onClick: () => {
|
|
366
|
-
setDecoration("strikethrough", !allStrike);
|
|
367
|
-
} })] })] }) }));
|
|
368
|
-
};
|
|
369
|
-
const StrokeControl = ({ shapes }) => {
|
|
370
|
-
const editor = useDiagramOptional();
|
|
371
|
-
if (!editor || !shapes.some(hasStroke))
|
|
372
|
-
return null;
|
|
373
|
-
const value = sharedString(shapes, (s) => s.style.stroke);
|
|
374
|
-
const ids = shapes.map((s) => s.id);
|
|
375
|
-
return (_jsx(ColorTrigger, { label: "Stroke", ariaLabel: "Stroke color", color: value, onChange: (v) => {
|
|
376
|
-
editor.updateStyle(ids, { stroke: v ?? "transparent" });
|
|
377
|
-
}, onEyedrop: (cb) => {
|
|
378
|
-
editor.beginEyedropperPick(cb);
|
|
379
|
-
} }));
|
|
380
|
-
};
|
|
381
|
-
/**
|
|
382
|
-
* Width control for brush strokes: a popover with a range slider driving
|
|
383
|
-
* `editor.setBrushWidth`, which re-bases the baked per-point widths while
|
|
384
|
-
* keeping the stroke's pressure profile. Rendered instead of
|
|
385
|
-
* {@link StrokeWidthControl} when the whole selection is brush strokes.
|
|
386
|
-
*/
|
|
387
|
-
const BrushWidthControl = ({ shapes }) => {
|
|
388
|
-
const editor = useDiagramOptional();
|
|
389
|
-
if (!editor)
|
|
390
|
-
return null;
|
|
391
|
-
const ids = shapes.map((s) => s.id);
|
|
392
|
-
const value = sharedValue(shapes, (s) => {
|
|
393
|
-
const brush = s;
|
|
394
|
-
const base = brush.baseWidth ?? brush.points.reduce((m, p) => Math.max(m, p.width), 0);
|
|
395
|
-
return base > 0 ? Math.round(base) : null;
|
|
396
|
-
});
|
|
397
|
-
return (_jsx(Popover, { ariaLabel: "Brush width", trigger: _jsx("button", { type: "button", className: "du-sel-icon-button", title: "Brush width", "aria-label": "Brush width", children: _jsx(StrokeWidthIcon, { thickness: 2.5 }) }), children: _jsxs("div", { className: "du-sel-popover-section", children: [_jsx("header", { className: "du-sel-popover-label", children: `Width ${value !== null ? String(value) : "mixed"}` }), _jsx("input", { type: "range", min: BRUSH_WIDTH_MIN, max: BRUSH_WIDTH_MAX, step: 1, value: value ?? BRUSH_WIDTH_MIN, onChange: (e) => {
|
|
398
|
-
editor.setBrushWidth(ids, e.currentTarget.valueAsNumber);
|
|
399
|
-
}, style: { width: "100%" }, "aria-label": "Brush width" })] }) }));
|
|
400
|
-
};
|
|
401
|
-
const StrokeWidthControl = ({ shapes }) => {
|
|
402
|
-
const editor = useDiagramOptional();
|
|
403
|
-
if (!editor || !shapes.some((s) => s.style.stroke !== undefined))
|
|
404
|
-
return null;
|
|
405
|
-
const value = sharedValue(shapes, (s) => s.style.strokeWidth ?? null);
|
|
406
|
-
const ids = shapes.map((s) => s.id);
|
|
407
|
-
return (_jsx(SegmentedControl, { ariaLabel: "Stroke width", value: value, options: [
|
|
408
|
-
{ value: 1, label: "Thin", icon: _jsx(StrokeWidthIcon, { thickness: 1 }) },
|
|
409
|
-
{ value: 2, label: "Medium", icon: _jsx(StrokeWidthIcon, { thickness: 2.5 }) },
|
|
410
|
-
{ value: 4, label: "Thick", icon: _jsx(StrokeWidthIcon, { thickness: 4 }) },
|
|
411
|
-
], onChange: (v) => {
|
|
412
|
-
editor.updateStyle(ids, { strokeWidth: v });
|
|
413
|
-
} }));
|
|
414
|
-
};
|
|
415
|
-
const StrokeStyleControl = ({ shapes }) => {
|
|
416
|
-
const editor = useDiagramOptional();
|
|
417
|
-
if (!editor || !shapes.some(hasStroke))
|
|
418
|
-
return null;
|
|
419
|
-
// Tolerant detection: any 2-element array with first ≤ 3 → dotted;
|
|
420
|
-
// anything else with first > 3 → dashed. Lets the panel recognise
|
|
421
|
-
// template-authored arrays like `[6, 4]` (auto-grid frames) as
|
|
422
|
-
// "dashed" instead of "mixed".
|
|
423
|
-
const value = sharedValue(shapes, (s) => {
|
|
424
|
-
const da = s.style.dashArray;
|
|
425
|
-
if (!da || da.length === 0)
|
|
426
|
-
return "solid";
|
|
427
|
-
const first = da[0] ?? 0;
|
|
428
|
-
return first <= 3 ? "dotted" : "dashed";
|
|
429
|
-
});
|
|
430
|
-
const ids = shapes.map((s) => s.id);
|
|
431
|
-
return (_jsx(SegmentedControl, { ariaLabel: "Stroke style", value: value, options: [
|
|
432
|
-
{ value: "solid", label: "Solid", icon: _jsx(Square, { size: 14, strokeWidth: 1.75 }) },
|
|
433
|
-
{ value: "dashed", label: "Dashed", icon: _jsx(SquareDashed, { size: 14, strokeWidth: 1.75 }) },
|
|
434
|
-
{ value: "dotted", label: "Dotted", icon: _jsx(SquareDot, { size: 14, strokeWidth: 1.75 }) },
|
|
435
|
-
], onChange: (v) => {
|
|
436
|
-
// An empty `dashArray: []` renders solid (Canvas2D `setLineDash([])`),
|
|
437
|
-
// and an empty array is truthy in JS so the renderer's dash call still
|
|
438
|
-
// goes through. Always pass an array.
|
|
439
|
-
const dashArray = v === "solid" ? [] : v === "dashed" ? [8, 4] : [2, 4];
|
|
440
|
-
editor.updateStyle(ids, { dashArray });
|
|
441
|
-
} }));
|
|
442
|
-
};
|
|
443
|
-
/**
|
|
444
|
-
* Corner control: an icon button (sharp / round) opens a popover with
|
|
445
|
-
* the round-radius slider plus an auto checkbox. Renders nothing when no
|
|
446
|
-
* corner-capable shapes are selected.
|
|
447
|
-
*/
|
|
448
|
-
const RoundnessControl = ({ shapes }) => {
|
|
449
|
-
const editor = useDiagramOptional();
|
|
450
|
-
if (!editor)
|
|
451
|
-
return null;
|
|
452
|
-
const supports = shapes.every((s) => isRectangle(s) || s.type === "container");
|
|
453
|
-
if (!supports)
|
|
454
|
-
return null;
|
|
455
|
-
const type = sharedValue(shapes, (s) => s.style.roundness?.type ?? "sharp");
|
|
456
|
-
const radius = sharedValue(shapes, (s) => s.style.roundness?.value ?? null);
|
|
457
|
-
const ids = shapes.map((s) => s.id);
|
|
458
|
-
const isAuto = radius === null;
|
|
459
|
-
return (_jsx(Popover, { ariaLabel: "Corners", trigger: _jsx("button", { type: "button", className: "du-sel-icon-button", title: "Corners", "aria-label": "Corners", children: _jsx(CornerIcon, { kind: type ?? "sharp" }) }), children: _jsxs("div", { className: "du-sel-popover-section", children: [_jsx("header", { className: "du-sel-popover-label", children: "Corners" }), _jsx(SegmentedControl, { ariaLabel: "Corner roundness", value: type, options: [
|
|
460
|
-
{ value: "sharp", label: "Sharp", icon: _jsx(CornerIcon, { kind: "sharp" }) },
|
|
461
|
-
{ value: "round", label: "Round", icon: _jsx(CornerIcon, { kind: "round" }) },
|
|
462
|
-
], onChange: (v) => {
|
|
463
|
-
editor.updateStyle(ids, { roundness: { type: v } });
|
|
464
|
-
} }), type === "round" ? (_jsxs(_Fragment, { children: [_jsxs("label", { className: "du-prop-checkbox", children: [_jsx("input", { type: "checkbox", checked: isAuto, onChange: (ev) => {
|
|
465
|
-
if (ev.target.checked) {
|
|
466
|
-
editor.updateStyle(ids, { roundness: { type: "round" } });
|
|
467
|
-
}
|
|
468
|
-
else {
|
|
469
|
-
editor.updateStyle(ids, {
|
|
470
|
-
roundness: { type: "round", value: radius ?? 8 },
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
} }), _jsx("span", { children: "Auto radius" })] }), !isAuto ? (_jsx(Slider, { value: radius, min: 0, max: 64, step: 1, ariaLabel: "Corner radius", valueLabel: `${radius}px`, onChange: (v) => {
|
|
474
|
-
editor.updateStyle(ids, { roundness: { type: "round", value: v } });
|
|
475
|
-
} })) : null] })) : null] }) }));
|
|
476
|
-
};
|
|
477
|
-
/**
|
|
478
|
-
* Opacity control: percentage badge button opens a popover with a
|
|
479
|
-
* slider. The trigger is always rendered (even at implicit opacity 1) so
|
|
480
|
-
* the user can jump to 50% without a multi-step interaction.
|
|
481
|
-
*/
|
|
482
|
-
const OpacityControl = ({ shapes }) => {
|
|
483
|
-
const editor = useDiagramOptional();
|
|
484
|
-
if (!editor)
|
|
485
|
-
return null;
|
|
486
|
-
const value = sharedValue(shapes, (s) => s.style.opacity ?? 1);
|
|
487
|
-
const ids = shapes.map((s) => s.id);
|
|
488
|
-
const percent = value === null ? null : Math.round(value * 100);
|
|
489
|
-
const label = percent === null ? "—" : `${percent}%`;
|
|
490
|
-
return (_jsx(Popover, { ariaLabel: "Opacity", trigger: _jsx("button", { type: "button", className: "du-sel-text-button", title: "Opacity", "aria-label": `Opacity ${label}`, children: label }), children: _jsxs("div", { className: "du-sel-popover-section", children: [_jsx("header", { className: "du-sel-popover-label", children: "Opacity" }), _jsx(Slider, { value: percent, min: 0, max: 100, step: 5, ariaLabel: "Opacity", valueLabel: label, onChange: (v) => {
|
|
491
|
-
editor.updateStyle(ids, { opacity: v / 100 });
|
|
492
|
-
} })] }) }));
|
|
493
|
-
};
|
|
494
|
-
const ZOrderControl = () => {
|
|
495
|
-
const editor = useDiagramOptional();
|
|
496
|
-
if (!editor)
|
|
497
|
-
return null;
|
|
498
|
-
return (_jsx(SegmentedControl, { ariaLabel: "Z-order", value: null, options: [
|
|
499
|
-
{
|
|
500
|
-
value: "back",
|
|
501
|
-
label: "Send to back",
|
|
502
|
-
icon: _jsx(ChevronsDown, { size: 14, strokeWidth: 1.75 }),
|
|
503
|
-
},
|
|
504
|
-
{
|
|
505
|
-
value: "backward",
|
|
506
|
-
label: "Send backward",
|
|
507
|
-
icon: _jsx(MoveDown, { size: 14, strokeWidth: 1.75 }),
|
|
508
|
-
},
|
|
509
|
-
{ value: "forward", label: "Bring forward", icon: _jsx(MoveUp, { size: 14, strokeWidth: 1.75 }) },
|
|
510
|
-
{
|
|
511
|
-
value: "front",
|
|
512
|
-
label: "Bring to front",
|
|
513
|
-
icon: _jsx(ChevronsUp, { size: 14, strokeWidth: 1.75 }),
|
|
514
|
-
},
|
|
515
|
-
], onChange: (v) => {
|
|
516
|
-
if (v === "back")
|
|
517
|
-
editor.sendToBack();
|
|
518
|
-
else if (v === "backward")
|
|
519
|
-
editor.sendBackward();
|
|
520
|
-
else if (v === "forward")
|
|
521
|
-
editor.bringForward();
|
|
522
|
-
else
|
|
523
|
-
editor.bringToFront();
|
|
524
|
-
} }));
|
|
525
|
-
};
|
|
526
|
-
/**
|
|
527
|
-
* Convert-type control (F9): switch every selected rectangle / ellipse /
|
|
528
|
-
* diamond to another of those types, preserving position, size and style.
|
|
529
|
-
* Renders only when every selected shape is convertible. The active type is
|
|
530
|
-
* shown as the pressed segment (or `null` for a mixed selection).
|
|
531
|
-
*/
|
|
532
|
-
const ConvertTypeControl = ({ shapes }) => {
|
|
533
|
-
const editor = useDiagramOptional();
|
|
534
|
-
if (!editor)
|
|
535
|
-
return null;
|
|
536
|
-
const convertible = shapes.every((s) => isRectangle(s) || isEllipse(s) || isPolygon(s));
|
|
537
|
-
if (!convertible)
|
|
538
|
-
return null;
|
|
539
|
-
const value = sharedValue(shapes, (s) => isRectangle(s) ? "rectangle" : isEllipse(s) ? "ellipse" : "polygon");
|
|
540
|
-
return (_jsx(SegmentedControl, { ariaLabel: "Shape type", value: value, options: [
|
|
541
|
-
{ value: "rectangle", label: "Rectangle", icon: _jsx(Square, { size: 14, strokeWidth: 1.75 }) },
|
|
542
|
-
{ value: "ellipse", label: "Ellipse", icon: _jsx(Circle, { size: 14, strokeWidth: 1.75 }) },
|
|
543
|
-
{ value: "polygon", label: "Diamond", icon: _jsx(Diamond, { size: 14, strokeWidth: 1.75 }) },
|
|
544
|
-
], onChange: (v) => {
|
|
545
|
-
editor.convertSelection(v);
|
|
546
|
-
} }));
|
|
547
|
-
};
|
|
548
|
-
/**
|
|
549
|
-
* Crop control (F10): a button that enters image-crop mode for the single
|
|
550
|
-
* selected image. Renders only for a lone image selection (crop is
|
|
551
|
-
* single-target). Double-clicking the image on the canvas does the same.
|
|
552
|
-
*/
|
|
553
|
-
const CropControl = ({ shapes }) => {
|
|
554
|
-
const editor = useDiagramOptional();
|
|
555
|
-
if (!editor)
|
|
556
|
-
return null;
|
|
557
|
-
const only = shapes.length === 1 ? shapes[0] : undefined;
|
|
558
|
-
if (only === undefined || !isImage(only))
|
|
559
|
-
return null;
|
|
560
|
-
const id = only.id;
|
|
561
|
-
return (_jsx("button", { type: "button", className: "du-icon-button", title: "Crop image (double-click)", "aria-label": "Crop image", onClick: () => {
|
|
562
|
-
editor.beginImageCrop(id);
|
|
563
|
-
}, children: _jsx(Crop, { size: 16, strokeWidth: 1.75 }) }));
|
|
564
|
-
};
|
|
565
|
-
const ActionsControl = ({ shapes }) => {
|
|
566
|
-
const editor = useDiagramOptional();
|
|
567
|
-
if (!editor)
|
|
568
|
-
return null;
|
|
569
|
-
const canGroup = shapes.length >= 2;
|
|
570
|
-
const canUngroup = shapes.some((s) => isGroup(s));
|
|
571
|
-
const options = [
|
|
572
|
-
{ value: "duplicate", label: "Duplicate", icon: _jsx(CopyIcon, { size: 14, strokeWidth: 1.75 }) },
|
|
573
|
-
{ value: "delete", label: "Delete", icon: _jsx(Trash2, { size: 14, strokeWidth: 1.75 }) },
|
|
574
|
-
];
|
|
575
|
-
if (canGroup) {
|
|
576
|
-
options.push({
|
|
577
|
-
value: "group",
|
|
578
|
-
label: "Group",
|
|
579
|
-
icon: _jsx(GroupIcon, { size: 14, strokeWidth: 1.75 }),
|
|
580
|
-
});
|
|
581
|
-
}
|
|
582
|
-
if (canUngroup) {
|
|
583
|
-
options.push({
|
|
584
|
-
value: "ungroup",
|
|
585
|
-
label: "Ungroup",
|
|
586
|
-
icon: _jsx(UngroupIcon, { size: 14, strokeWidth: 1.75 }),
|
|
587
|
-
});
|
|
588
|
-
}
|
|
589
|
-
options.push({
|
|
590
|
-
value: "flip-h",
|
|
591
|
-
label: "Flip horizontal",
|
|
592
|
-
icon: _jsx(FlipHorizontalIcon, { size: 14, strokeWidth: 1.75 }),
|
|
593
|
-
}, {
|
|
594
|
-
value: "flip-v",
|
|
595
|
-
label: "Flip vertical",
|
|
596
|
-
icon: _jsx(FlipVerticalIcon, { size: 14, strokeWidth: 1.75 }),
|
|
597
|
-
});
|
|
598
|
-
return (_jsx(SegmentedControl, { ariaLabel: "Element actions", value: null, options: options, onChange: (v) => {
|
|
599
|
-
if (v === "duplicate")
|
|
600
|
-
editor.duplicateSelected();
|
|
601
|
-
else if (v === "delete")
|
|
602
|
-
editor.deleteSelected();
|
|
603
|
-
else if (v === "group")
|
|
604
|
-
editor.groupSelected();
|
|
605
|
-
else if (v === "ungroup")
|
|
606
|
-
editor.ungroup();
|
|
607
|
-
else if (v === "flip-h")
|
|
608
|
-
editor.flipSelection("horizontal");
|
|
609
|
-
else
|
|
610
|
-
editor.flipSelection("vertical");
|
|
611
|
-
} }));
|
|
612
|
-
};
|
|
613
|
-
const ALIGN_OPTIONS = [
|
|
614
|
-
{ edge: "left", label: "Align left", icon: _jsx(AlignStartVertical, { size: 16, strokeWidth: 1.75 }) },
|
|
615
|
-
{
|
|
616
|
-
edge: "h-center",
|
|
617
|
-
label: "Align horizontal centres",
|
|
618
|
-
icon: _jsx(AlignCenterVertical, { size: 16, strokeWidth: 1.75 }),
|
|
619
|
-
},
|
|
620
|
-
{ edge: "right", label: "Align right", icon: _jsx(AlignEndVertical, { size: 16, strokeWidth: 1.75 }) },
|
|
621
|
-
{ edge: "top", label: "Align top", icon: _jsx(AlignStartHorizontal, { size: 16, strokeWidth: 1.75 }) },
|
|
622
|
-
{
|
|
623
|
-
edge: "v-center",
|
|
624
|
-
label: "Align vertical centres",
|
|
625
|
-
icon: _jsx(AlignCenterHorizontal, { size: 16, strokeWidth: 1.75 }),
|
|
626
|
-
},
|
|
627
|
-
{
|
|
628
|
-
edge: "bottom",
|
|
629
|
-
label: "Align bottom",
|
|
630
|
-
icon: _jsx(AlignEndHorizontal, { size: 16, strokeWidth: 1.75 }),
|
|
631
|
-
},
|
|
632
|
-
];
|
|
633
|
-
const AlignControl = () => {
|
|
634
|
-
const editor = useDiagramOptional();
|
|
635
|
-
const selection = useSelection();
|
|
636
|
-
if (!editor)
|
|
637
|
-
return null;
|
|
638
|
-
// Distribution needs three+ elements; alignment is offered from two.
|
|
639
|
-
const canDistribute = selection.size >= 3;
|
|
640
|
-
return (_jsx(Popover, { ariaLabel: "Align and distribute", trigger: _jsx("button", { type: "button", className: "du-sel-icon-button", title: "Align", "aria-label": "Align", children: _jsx(AlignCenterVertical, { size: 16, strokeWidth: 1.75 }) }), children: _jsxs("div", { className: "du-sel-popover-section", children: [_jsx("header", { className: "du-sel-popover-label", children: "Align" }), _jsx("div", { style: { display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 4 }, children: ALIGN_OPTIONS.map((o) => (_jsx("button", { type: "button", className: "du-sel-icon-button", title: o.label, "aria-label": o.label, onClick: () => {
|
|
641
|
-
editor.alignSelection(o.edge);
|
|
642
|
-
}, children: o.icon }, o.edge))) }), canDistribute ? (_jsxs(_Fragment, { children: [_jsx("header", { className: "du-sel-popover-label", children: "Distribute" }), _jsxs("div", { style: { display: "flex", gap: 4 }, children: [_jsx("button", { type: "button", className: "du-sel-icon-button", title: "Distribute horizontally", "aria-label": "Distribute horizontally", onClick: () => {
|
|
643
|
-
editor.distributeSelection("horizontal");
|
|
644
|
-
}, children: _jsx(AlignHorizontalDistributeCenter, { size: 16, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "du-sel-icon-button", title: "Distribute vertically", "aria-label": "Distribute vertically", onClick: () => {
|
|
645
|
-
editor.distributeSelection("vertical");
|
|
646
|
-
}, children: _jsx(AlignVerticalDistributeCenter, { size: 16, strokeWidth: 1.75 }) })] })] })) : null] }) }));
|
|
647
|
-
};
|
|
648
|
-
// ---------------------------------------------------------------------------
|
|
649
|
-
// Link controls — horizontal compact set used when an edge (not a
|
|
650
|
-
// shape) is the active selection. Mirrors the shape control surface
|
|
651
|
-
// (color triggers + segmented row controls) so the floating panel
|
|
652
|
-
// reads consistently regardless of selection type.
|
|
653
|
-
// ---------------------------------------------------------------------------
|
|
654
|
-
const LinkStrokeColorControl = ({ edge }) => {
|
|
655
|
-
const editor = useDiagramOptional();
|
|
656
|
-
if (!editor)
|
|
657
|
-
return null;
|
|
658
|
-
const color = typeof edge.style.stroke === "string" ? edge.style.stroke : null;
|
|
659
|
-
return (_jsx(ColorTrigger, { label: "Stroke", ariaLabel: "Link stroke color", color: color, onChange: (v) => {
|
|
660
|
-
editor.updateSelectedLink((e) => ({
|
|
661
|
-
...e,
|
|
662
|
-
style: { ...e.style, stroke: v ?? "transparent" },
|
|
663
|
-
}));
|
|
664
|
-
}, onEyedrop: (cb) => {
|
|
665
|
-
editor.beginEyedropperPick(cb);
|
|
666
|
-
} }));
|
|
667
|
-
};
|
|
668
|
-
const LinkStrokeWidthControl = ({ edge }) => {
|
|
669
|
-
const editor = useDiagramOptional();
|
|
670
|
-
if (!editor)
|
|
671
|
-
return null;
|
|
672
|
-
const value = typeof edge.style.strokeWidth === "number" ? edge.style.strokeWidth : null;
|
|
673
|
-
return (_jsx(SegmentedControl, { ariaLabel: "Link stroke width", value: value, options: [
|
|
674
|
-
{ value: 1, label: "Thin", icon: _jsx(StrokeWidthIcon, { thickness: 1 }) },
|
|
675
|
-
{ value: 2, label: "Medium", icon: _jsx(StrokeWidthIcon, { thickness: 2.5 }) },
|
|
676
|
-
{ value: 4, label: "Thick", icon: _jsx(StrokeWidthIcon, { thickness: 4 }) },
|
|
677
|
-
], onChange: (v) => {
|
|
678
|
-
editor.updateSelectedLink((e) => ({
|
|
679
|
-
...e,
|
|
680
|
-
style: { ...e.style, strokeWidth: v },
|
|
681
|
-
}));
|
|
682
|
-
} }));
|
|
683
|
-
};
|
|
684
|
-
const LinkStrokeStyleControl = ({ edge }) => {
|
|
685
|
-
const editor = useDiagramOptional();
|
|
686
|
-
if (!editor)
|
|
687
|
-
return null;
|
|
688
|
-
const da = edge.style.dashArray;
|
|
689
|
-
const value = (() => {
|
|
690
|
-
if (!da || da.length === 0)
|
|
691
|
-
return "solid";
|
|
692
|
-
const first = da[0] ?? 0;
|
|
693
|
-
return first <= 3 ? "dotted" : "dashed";
|
|
694
|
-
})();
|
|
695
|
-
return (_jsx(SegmentedControl, { ariaLabel: "Link stroke style", value: value, options: [
|
|
696
|
-
{ value: "solid", label: "Solid", icon: _jsx(Square, { size: 14, strokeWidth: 1.75 }) },
|
|
697
|
-
{ value: "dashed", label: "Dashed", icon: _jsx(SquareDashed, { size: 14, strokeWidth: 1.75 }) },
|
|
698
|
-
{ value: "dotted", label: "Dotted", icon: _jsx(SquareDot, { size: 14, strokeWidth: 1.75 }) },
|
|
699
|
-
], onChange: (v) => {
|
|
700
|
-
const dashArray = v === "solid" ? [] : v === "dashed" ? [8, 4] : [2, 4];
|
|
701
|
-
editor.updateSelectedLink((e) => ({
|
|
702
|
-
...e,
|
|
703
|
-
style: { ...e.style, dashArray },
|
|
704
|
-
}));
|
|
705
|
-
} }));
|
|
706
|
-
};
|
|
707
|
-
const LinkLineKindControl = ({ edge }) => {
|
|
708
|
-
const editor = useDiagramOptional();
|
|
709
|
-
if (!editor)
|
|
710
|
-
return null;
|
|
711
|
-
const value = edge.lineKind ?? "line";
|
|
712
|
-
return (_jsx(SegmentedControl, { ariaLabel: "Link body", value: value, options: [
|
|
713
|
-
{ value: "line", label: "Line", icon: _jsx(Minus, { size: 14, strokeWidth: 1.75 }) },
|
|
714
|
-
{
|
|
715
|
-
value: "block-arrow",
|
|
716
|
-
label: "Block arrow",
|
|
717
|
-
icon: _jsx(MoveRight, { size: 14, strokeWidth: 2.5 }),
|
|
718
|
-
},
|
|
719
|
-
], onChange: (v) => {
|
|
720
|
-
editor.updateSelectedLink((e) => ({ ...e, lineKind: v }));
|
|
721
|
-
} }));
|
|
722
|
-
};
|
|
723
|
-
const LinkRoutingControl = ({ edge }) => {
|
|
724
|
-
const editor = useDiagramOptional();
|
|
725
|
-
if (!editor)
|
|
726
|
-
return null;
|
|
727
|
-
const value = edge.routing ?? "straight";
|
|
728
|
-
return (_jsx(SegmentedControl, { ariaLabel: "Link routing", value: value, options: [
|
|
729
|
-
{ value: "straight", label: "Straight", icon: _jsx(RoutingIcon, { kind: "straight" }) },
|
|
730
|
-
{ value: "orthogonal", label: "Elbow", icon: _jsx(RoutingIcon, { kind: "orthogonal" }) },
|
|
731
|
-
{ value: "bezier", label: "Curved", icon: _jsx(Spline, { size: 14, strokeWidth: 1.75 }) },
|
|
732
|
-
], onChange: (v) => {
|
|
733
|
-
editor.updateSelectedLink((e) => ({ ...e, routing: v }));
|
|
734
|
-
} }));
|
|
735
|
-
};
|
|
736
|
-
const LinkArrowheadControl = ({ edge, side, }) => {
|
|
737
|
-
const editor = useDiagramOptional();
|
|
738
|
-
const [showErd, setShowErd] = useState(false);
|
|
739
|
-
if (!editor)
|
|
740
|
-
return null;
|
|
741
|
-
const current = edge.arrowheads?.[side] ?? "none";
|
|
742
|
-
const pick = (style) => {
|
|
743
|
-
editor.updateSelectedLink((e) => ({
|
|
744
|
-
...e,
|
|
745
|
-
arrowheads: { ...(e.arrowheads ?? {}), [side]: style },
|
|
746
|
-
}));
|
|
747
|
-
};
|
|
748
|
-
const Option = ({ style }) => (_jsx("button", { type: "button", className: `du-arrowhead-option${style === current ? " du-arrowhead-option--active" : ""}`, title: ARROWHEAD_LABELS[style], "aria-label": ARROWHEAD_LABELS[style], "aria-pressed": style === current, onClick: () => {
|
|
749
|
-
pick(style);
|
|
750
|
-
}, children: style === "none" ? (_jsx("span", { className: "du-arrowhead-none", children: "\u2205" })) : arrowheadGlyphFamily(style) === "none" ? (_jsx("span", { className: "du-arrowhead-erd-label", children: ARROWHEAD_LABELS[style].replace(/^ERD /, "") })) : (_jsx(ArrowheadGlyph, { kind: style, side: side })) }));
|
|
751
|
-
return (_jsx(Popover, { trigger: _jsx("button", { type: "button", className: "du-sel-icon-button", title: `Arrow ${side}: ${ARROWHEAD_LABELS[current]}`, "aria-label": `Arrow ${side}`, children: _jsx(ArrowheadGlyph, { kind: current, side: side }) }), children: _jsxs("div", { className: "du-sel-popover-section", children: [_jsxs("header", { className: "du-sel-popover-label", children: ["Arrow ", side] }), _jsx("div", { className: "du-arrowhead-grid", children: BASIC_ARROWHEADS.map((s) => (_jsx(Option, { style: s }, s))) }), _jsxs("button", { type: "button", className: "du-arrowhead-erd-toggle", "aria-expanded": showErd, onClick: () => {
|
|
752
|
-
setShowErd((v) => !v);
|
|
753
|
-
}, children: [showErd ? "▾" : "▸", " ER diagram"] }), showErd && (_jsx("div", { className: "du-arrowhead-grid", children: ERD_ARROWHEADS.map((s) => (_jsx(Option, { style: s }, s))) }))] }) }));
|
|
754
|
-
};
|
|
755
|
-
const LinkAutoRouteControl = ({ edge }) => {
|
|
756
|
-
const editor = useDiagramOptional();
|
|
757
|
-
if (!editor)
|
|
758
|
-
return null;
|
|
759
|
-
// Obstacle-avoidance produces an orthogonal path, so it only makes sense
|
|
760
|
-
// for straight / elbow links — a curved (bezier) link can't carry the
|
|
761
|
-
// routed polyline. Hide the toggle for curved.
|
|
762
|
-
const routing = edge.routing ?? "straight";
|
|
763
|
-
if (routing === "bezier")
|
|
764
|
-
return null;
|
|
765
|
-
const on = edge.avoidObstacles === true;
|
|
766
|
-
return (_jsx("button", { type: "button", className: `du-sel-icon-button${on ? " is-active" : ""}`, title: "Route around shapes", "aria-label": "Route around shapes", "aria-pressed": on, onClick: () => {
|
|
767
|
-
editor.setSelectedLinkAvoidObstacles(!on);
|
|
768
|
-
}, children: _jsx(Waypoints, { size: 14, strokeWidth: 1.75, "aria-hidden": true }) }));
|
|
769
|
-
};
|
|
770
|
-
const LinkDeleteControl = () => {
|
|
771
|
-
const editor = useDiagramOptional();
|
|
772
|
-
if (!editor)
|
|
773
|
-
return null;
|
|
774
|
-
return (_jsx("button", { type: "button", className: "du-sel-icon-button", title: "Delete edge", "aria-label": "Delete edge", onClick: () => {
|
|
775
|
-
editor.deleteSelected();
|
|
776
|
-
}, children: _jsx(Trash2, { size: 14, strokeWidth: 1.75, "aria-hidden": true }) }));
|
|
777
|
-
};
|
|
778
|
-
// Inline SVG glyph for routing variant — Lucide has `Spline` for the
|
|
779
|
-
// curve but no clean "straight" / "elbow" line variants.
|
|
780
|
-
const RoutingIcon = ({ kind }) => {
|
|
781
|
-
if (kind === "straight") {
|
|
782
|
-
return (_jsx("svg", { width: 14, height: 14, viewBox: "0 0 14 14", fill: "none", "aria-hidden": true, children: _jsx("line", { x1: 2, y1: 11, x2: 12, y2: 3, stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round" }) }));
|
|
783
|
-
}
|
|
784
|
-
// orthogonal (elbow)
|
|
785
|
-
return (_jsx("svg", { width: 14, height: 14, viewBox: "0 0 14 14", fill: "none", "aria-hidden": true, children: _jsx("path", { d: "M 2 11 L 2 7 L 12 7 L 12 3", stroke: "currentColor", strokeWidth: 1.5, fill: "none", strokeLinecap: "round", strokeLinejoin: "round" }) }));
|
|
786
|
-
};
|
|
787
|
-
// Which compact glyph family renders a given arrowhead style.
|
|
788
|
-
const arrowheadGlyphFamily = (kind) => {
|
|
789
|
-
switch (kind) {
|
|
790
|
-
case "arrow":
|
|
791
|
-
case "openArrow":
|
|
792
|
-
case "roundedArrow":
|
|
793
|
-
case "arcArrow":
|
|
794
|
-
return "open";
|
|
795
|
-
case "triangle":
|
|
796
|
-
case "filledArrow":
|
|
797
|
-
return "triangle";
|
|
798
|
-
case "diamond":
|
|
799
|
-
case "rhombus":
|
|
800
|
-
case "filledRhombus":
|
|
801
|
-
return "diamond";
|
|
802
|
-
case "circle":
|
|
803
|
-
case "filledCircle":
|
|
804
|
-
return "circle";
|
|
805
|
-
default:
|
|
806
|
-
return "none"; // none + ERD caps (labelled by name in the picker)
|
|
807
|
-
}
|
|
808
|
-
};
|
|
809
|
-
// Picker option sets. ERD caps are gated behind a toggle.
|
|
810
|
-
const BASIC_ARROWHEADS = [
|
|
811
|
-
"none",
|
|
812
|
-
"arrow",
|
|
813
|
-
"openArrow",
|
|
814
|
-
"roundedArrow",
|
|
815
|
-
"arcArrow",
|
|
816
|
-
"filledArrow",
|
|
817
|
-
"triangle",
|
|
818
|
-
"circle",
|
|
819
|
-
"filledCircle",
|
|
820
|
-
"rhombus",
|
|
821
|
-
"filledRhombus",
|
|
822
|
-
];
|
|
823
|
-
const ERD_ARROWHEADS = [
|
|
824
|
-
"erdOne",
|
|
825
|
-
"erdOnlyOne",
|
|
826
|
-
"erdMany",
|
|
827
|
-
"erdOneOrMany",
|
|
828
|
-
"erdZeroOrOne",
|
|
829
|
-
"erdZeroOrMany",
|
|
830
|
-
];
|
|
831
|
-
const ARROWHEAD_LABELS = {
|
|
832
|
-
none: "None",
|
|
833
|
-
arrow: "Arrow",
|
|
834
|
-
openArrow: "Open arrow",
|
|
835
|
-
roundedArrow: "Rounded arrow",
|
|
836
|
-
arcArrow: "Arc arrow",
|
|
837
|
-
triangle: "Triangle",
|
|
838
|
-
filledArrow: "Filled arrow",
|
|
839
|
-
circle: "Circle",
|
|
840
|
-
filledCircle: "Filled circle",
|
|
841
|
-
diamond: "Diamond",
|
|
842
|
-
rhombus: "Rhombus",
|
|
843
|
-
filledRhombus: "Filled rhombus",
|
|
844
|
-
erdOne: "ERD one",
|
|
845
|
-
erdOnlyOne: "ERD only one",
|
|
846
|
-
erdMany: "ERD many",
|
|
847
|
-
erdOneOrMany: "ERD one or many",
|
|
848
|
-
erdZeroOrOne: "ERD zero or one",
|
|
849
|
-
erdZeroOrMany: "ERD zero or many",
|
|
850
|
-
};
|
|
851
|
-
const ArrowheadGlyph = ({ kind, side, }) => {
|
|
852
|
-
// Build a 14×14 horizontal line with the head at right (for `to`) or
|
|
853
|
-
// left (for `from`), all in stroke / fill currentColor.
|
|
854
|
-
const flipped = side === "from";
|
|
855
|
-
const lineX1 = flipped ? 12 : 2;
|
|
856
|
-
const lineX2 = flipped ? 5 : 9;
|
|
857
|
-
const headCx = flipped ? 3 : 11;
|
|
858
|
-
const headBase = flipped ? 5 : 9;
|
|
859
|
-
const stroke = (_jsx("line", { x1: lineX1, y1: 7, x2: lineX2, y2: 7, stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round" }));
|
|
860
|
-
// Map the full vocabulary onto the four drawable glyph families. ERD caps
|
|
861
|
-
// have no compact glyph — they fall through to a bare line; the picker
|
|
862
|
-
// labels them by name so they stay distinguishable.
|
|
863
|
-
const fam = arrowheadGlyphFamily(kind);
|
|
864
|
-
let head = null;
|
|
865
|
-
if (fam === "open") {
|
|
866
|
-
head = (_jsx("polyline", { points: flipped
|
|
867
|
-
? `${headCx + 3},4 ${headCx},7 ${headCx + 3},10`
|
|
868
|
-
: `${headCx - 3},4 ${headCx},7 ${headCx - 3},10`, stroke: "currentColor", strokeWidth: 1.5, fill: "none", strokeLinecap: "round", strokeLinejoin: "round" }));
|
|
869
|
-
}
|
|
870
|
-
else if (fam === "triangle") {
|
|
871
|
-
head = (_jsx("polygon", { points: flipped
|
|
872
|
-
? `${headCx},7 ${headBase},4 ${headBase},10`
|
|
873
|
-
: `${headCx},7 ${headBase},4 ${headBase},10`, fill: "currentColor" }));
|
|
874
|
-
}
|
|
875
|
-
else if (fam === "diamond") {
|
|
876
|
-
head = (_jsx("polygon", { points: `${headCx},4 ${headCx + (flipped ? -3 : 3)},7 ${headCx},10 ${headCx + (flipped ? 3 : -3)},7`, fill: kind === "filledRhombus" ? "currentColor" : "none", stroke: "currentColor", strokeWidth: 1.2 }));
|
|
877
|
-
}
|
|
878
|
-
else if (fam === "circle") {
|
|
879
|
-
head = (_jsx("circle", { cx: headCx, cy: 7, r: 2.2, fill: kind === "filledCircle" ? "currentColor" : "none", stroke: "currentColor", strokeWidth: 1.2 }));
|
|
880
|
-
}
|
|
881
|
-
return (_jsxs("svg", { width: 14, height: 14, viewBox: "0 0 14 14", fill: "none", "aria-hidden": true, children: [stroke, head] }));
|
|
882
|
-
};
|
|
883
|
-
// ---------------------------------------------------------------------------
|
|
884
|
-
// Layout primitives
|
|
885
|
-
// ---------------------------------------------------------------------------
|
|
886
|
-
const Divider = () => _jsx("span", { className: "du-sel-divider", "aria-hidden": true });
|
|
887
|
-
// ---------------------------------------------------------------------------
|
|
888
|
-
// Inline SVG glyphs
|
|
889
|
-
// ---------------------------------------------------------------------------
|
|
890
|
-
const StrokeWidthIcon = ({ thickness }) => (_jsx("svg", { width: 14, height: 14, viewBox: "0 0 14 14", fill: "none", "aria-hidden": true, children: _jsx("line", { x1: 2, y1: 7, x2: 12, y2: 7, stroke: "currentColor", strokeWidth: thickness, strokeLinecap: "round" }) }));
|
|
891
|
-
const CornerIcon = ({ kind }) => {
|
|
892
|
-
if (kind === "sharp") {
|
|
893
|
-
return _jsx(Square, { size: 14, strokeWidth: 1.75, "aria-hidden": true });
|
|
894
|
-
}
|
|
895
|
-
return (_jsx("svg", { width: 14, height: 14, viewBox: "0 0 14 14", fill: "none", "aria-hidden": true, children: _jsx("rect", { x: 2, y: 2, width: 10, height: 10, rx: 3, ry: 3, stroke: "currentColor", strokeWidth: 1.5 }) }));
|
|
896
|
-
};
|
|
897
|
-
// ---------------------------------------------------------------------------
|
|
898
|
-
// Helpers
|
|
899
|
-
// ---------------------------------------------------------------------------
|
|
900
|
-
const sharedValue = (elements, pick) => {
|
|
901
|
-
const set = new Set();
|
|
902
|
-
for (const s of elements)
|
|
903
|
-
set.add(pick(s));
|
|
904
|
-
if (set.size !== 1)
|
|
905
|
-
return null;
|
|
906
|
-
const v = set.values().next().value;
|
|
907
|
-
return v ?? null;
|
|
908
|
-
};
|
|
909
|
-
const sharedString = (elements, pick) => {
|
|
910
|
-
const value = sharedValue(elements, (s) => pick(s));
|
|
911
|
-
return typeof value === "string" ? value : null;
|
|
912
|
-
};
|
|
913
|
-
// Frames are always fillable (white by default) even when `style.fill` is
|
|
914
|
-
// unset.
|
|
915
|
-
const hasFill = (shape) => shape.style.fill !== undefined || isFrame(shape);
|
|
916
|
-
const hasStroke = (shape) => shape.style.stroke !== undefined;
|
|
917
|
-
//# sourceMappingURL=property-panel.js.map
|