diff --git a/src/main/frontend/app/app.css b/src/main/frontend/app/app.css index 39c7b627..70b27c44 100644 --- a/src/main/frontend/app/app.css +++ b/src/main/frontend/app/app.css @@ -233,6 +233,34 @@ body { color: #808080 !important; } +.react-flow__panel.react-flow__controls { + background-color: var(--color-background); + border: 1px solid var(--color-border); + border-radius: 6px; + box-shadow: none; + overflow: hidden; +} + +.react-flow__panel.react-flow__controls .react-flow__controls-button { + background-color: var(--color-background); + border-bottom: 1px solid var(--color-border); + fill: var(--color-foreground); +} + +.react-flow__panel.react-flow__controls .react-flow__controls-button:last-child { + border-bottom: none; +} + +.react-flow__panel.react-flow__controls .react-flow__controls-button:hover { + background-color: var(--color-hover); +} + +.react-flow__panel.react-flow__controls .react-flow__controls-button svg { + fill: var(--color-foreground); + max-width: 12px; + max-height: 12px; +} + :root { /* Allotment Styling */ --focus-border: var(--color-brand); @@ -251,3 +279,26 @@ body { line-height: 1.4; white-space: nowrap; } + +* { + scrollbar-width: thin; + scrollbar-color: var(--color-border) transparent; +} + +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: var(--color-border); + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--color-foreground-muted); +} diff --git a/src/main/frontend/app/components/datamapper/forms/add-mapping-form.tsx b/src/main/frontend/app/components/datamapper/forms/add-mapping-form.tsx index 7fd4c754..94612f64 100644 --- a/src/main/frontend/app/components/datamapper/forms/add-mapping-form.tsx +++ b/src/main/frontend/app/components/datamapper/forms/add-mapping-form.tsx @@ -228,7 +228,7 @@ function AddMappingForm({ onSave, sources, targets, initialData }: MappingModalP {/* Target → Output */}
- +
- +

Select Directory

- +
diff --git a/src/main/frontend/app/components/file-structure/editor-file-structure.tsx b/src/main/frontend/app/components/file-structure/editor-file-structure.tsx index 5ba101f3..dc44985d 100644 --- a/src/main/frontend/app/components/file-structure/editor-file-structure.tsx +++ b/src/main/frontend/app/components/file-structure/editor-file-structure.tsx @@ -30,6 +30,7 @@ import { useTreeStore } from '~/stores/tree-store' import EditorFilesDataProvider, { type FileNode } from './editor-data-provider' import { useFileTreeContextMenu } from './use-file-tree-context-menu' import FileTreeDialogs from './file-tree-dialogs' +import TreeActionButton from './tree-action-button' const TREE_ID = 'editor-files-tree' @@ -366,8 +367,6 @@ export default function EditorFileStructure() { const isHighlighted = highlightedItemId === item.index - const actionBtnClass = 'cursor-pointer rounded p-0.5 hover:bg-hover flex-shrink-0' - return (
{item.isFolder && ( -
{ - mouseEvent.stopPropagation() - triggerItemAction(item.index, editorContextMenu.handleNewFile) - }} - onKeyDown={(keyboardEvent) => - keyboardEvent.key === 'Enter' && triggerItemAction(item.index, editorContextMenu.handleNewFile) - } + onAction={() => triggerItemAction(item.index, editorContextMenu.handleNewFile)} > -
+ )} {item.isFolder && ( -
{ - mouseEvent.stopPropagation() - triggerItemAction(item.index, editorContextMenu.handleNewFolder) - }} - onKeyDown={(keyboardEvent) => - keyboardEvent.key === 'Enter' && triggerItemAction(item.index, editorContextMenu.handleNewFolder) - } + onAction={() => triggerItemAction(item.index, editorContextMenu.handleNewFolder)} > -
+ )} {!isRoot && ( -
{ - mouseEvent.stopPropagation() - triggerItemAction(item.index, editorContextMenu.handleRename) - }} - onKeyDown={(keyboardEvent) => - keyboardEvent.key === 'Enter' && triggerItemAction(item.index, editorContextMenu.handleRename) - } + onAction={() => triggerItemAction(item.index, editorContextMenu.handleRename)} > - -
+ + )} {!isRoot && ( -
{ - mouseEvent.stopPropagation() - triggerItemAction(item.index, editorContextMenu.handleDelete) - }} - onKeyDown={(keyboardEvent) => - keyboardEvent.key === 'Enter' && triggerItemAction(item.index, editorContextMenu.handleDelete) - } + onAction={() => triggerItemAction(item.index, editorContextMenu.handleDelete)} > -
+ )}
diff --git a/src/main/frontend/app/components/file-structure/inline-rename-input.tsx b/src/main/frontend/app/components/file-structure/inline-rename-input.tsx index 3493140c..5dd1458f 100644 --- a/src/main/frontend/app/components/file-structure/inline-rename-input.tsx +++ b/src/main/frontend/app/components/file-structure/inline-rename-input.tsx @@ -1,4 +1,5 @@ import type { TreeItemIndex } from 'react-complex-tree' +import Input from '~/components/inputs/input' interface InlineRenameInputProps { icon: React.ComponentType<{ className?: string }> @@ -20,9 +21,10 @@ export default function InlineRenameInput({ return (
e.preventDefault()}> - onChange(e.target.value)} onKeyDown={(e) => { diff --git a/src/main/frontend/app/components/file-structure/studio-file-structure.tsx b/src/main/frontend/app/components/file-structure/studio-file-structure.tsx index 16676ca7..3242d9cd 100644 --- a/src/main/frontend/app/components/file-structure/studio-file-structure.tsx +++ b/src/main/frontend/app/components/file-structure/studio-file-structure.tsx @@ -33,6 +33,7 @@ import { useProjectStore } from '~/stores/project-store' import { useTreeStore } from '~/stores/tree-store' import { useStudioContextMenu, detectItemType, getItemName, resolveItemPaths } from './use-studio-context-menu' import StudioFileTreeDialogs from './studio-file-tree-dialogs' +import TreeActionButton from './tree-action-button' const TREE_ID = 'studio-files-tree' @@ -410,7 +411,6 @@ export default function StudioFileStructure() { } const isHighlighted = highlightedItemId == item.index - const actionBtnClass = 'cursor-pointer rounded p-0.5 hover:bg-hover flex-shrink-0' return (
{(isRoot || isPlainFolder) && ( -
{ - mouseEvent.stopPropagation() - triggerItemAction(item.index, studioContextMenu.handleNewConfiguration) - }} - onKeyDown={(keyboardEvent) => - keyboardEvent.key === 'Enter' && triggerItemAction(item.index, studioContextMenu.handleNewConfiguration) - } + onAction={() => triggerItemAction(item.index, studioContextMenu.handleNewConfiguration)} > -
+ )} {(isRoot || isPlainFolder) && ( -
{ - mouseEvent.stopPropagation() - triggerItemAction(item.index, studioContextMenu.handleNewFolder) - }} - onKeyDown={(keyboardEvent) => - keyboardEvent.key === 'Enter' && triggerItemAction(item.index, studioContextMenu.handleNewFolder) - } + onAction={() => triggerItemAction(item.index, studioContextMenu.handleNewFolder)} > -
+ )} {isConfigFile && ( -
{ - mouseEvent.stopPropagation() - triggerItemAction(item.index, studioContextMenu.handleNewAdapter) - }} - onKeyDown={(keyboardEvent) => - keyboardEvent.key === 'Enter' && triggerItemAction(item.index, studioContextMenu.handleNewAdapter) - } + onAction={() => triggerItemAction(item.index, studioContextMenu.handleNewAdapter)} > -
+ )} {!isRoot && ( -
{ - mouseEvent.stopPropagation() - triggerItemAction(item.index, studioContextMenu.handleRename) - }} - onKeyDown={(keyboardEvent) => - keyboardEvent.key === 'Enter' && triggerItemAction(item.index, studioContextMenu.handleRename) - } + onAction={() => triggerItemAction(item.index, studioContextMenu.handleRename)} > -
+ )} {!isRoot && ( -
{ - mouseEvent.stopPropagation() - triggerItemAction(item.index, studioContextMenu.handleDelete) - }} - onKeyDown={(keyboardEvent) => - keyboardEvent.key === 'Enter' && triggerItemAction(item.index, studioContextMenu.handleDelete) - } + onAction={() => triggerItemAction(item.index, studioContextMenu.handleDelete)} > -
+ )}
) } - if (!project) return

No Project Selected

+ if (!project) return

No Project Selected

if (providerLoading) return if (!dataProvider) - return

No configurations found in src/main/configurations

+ return

No configurations found in src/main/configurations

const toolbarBtnClass = 'cursor-pointer rounded p-1 hover:bg-hover text-foreground' diff --git a/src/main/frontend/app/components/file-structure/tree-action-button.tsx b/src/main/frontend/app/components/file-structure/tree-action-button.tsx new file mode 100644 index 00000000..5615bed8 --- /dev/null +++ b/src/main/frontend/app/components/file-structure/tree-action-button.tsx @@ -0,0 +1,25 @@ +import React from 'react' + +interface TreeActionButtonProps { + title: string + onAction: () => void + children: React.ReactNode +} + +export default function TreeActionButton({ title, onAction, children }: TreeActionButtonProps) { + return ( +
{ + mouseEvent.stopPropagation() + onAction() + }} + onKeyDown={(keyboardEvent) => keyboardEvent.key === 'Enter' && onAction()} + > + {children} +
+ ) +} diff --git a/src/main/frontend/app/components/flow/add-subcomponent-modal.tsx b/src/main/frontend/app/components/flow/add-subcomponent-modal.tsx index 4d10574a..5c96b1ed 100644 --- a/src/main/frontend/app/components/flow/add-subcomponent-modal.tsx +++ b/src/main/frontend/app/components/flow/add-subcomponent-modal.tsx @@ -1,5 +1,6 @@ import { createPortal } from 'react-dom' import Button from '../inputs/button' +import Search from '~/components/search/search' import type { ElementDetails } from '@frankframework/doc-library-core' import { useMemo, useState, type ChangeEvent } from 'react' @@ -80,13 +81,7 @@ export default function AddSubcomponentModal({

Add Subcomponent

{/* Paragraph / content */} - +
    {filteredChildren.length > 0 ? ( @@ -99,7 +94,7 @@ export default function AddSubcomponentModal({ onClick={() => setSelectedElement(child)} onDoubleClick={handleAddChild} className={`cursor-pointer px-3 py-2 ${ - isSelected ? 'bg-foreground-active text-background' : 'hover:bg-foreground-active/10' + isSelected ? 'bg-foreground-active text-background' : 'hover:bg-hover' }`} > {child.name} @@ -107,7 +102,7 @@ export default function AddSubcomponentModal({ ) }) ) : ( -
  • No results found
  • +
  • No results found
  • )}
diff --git a/src/main/frontend/app/components/flow/canvas-context-menu.tsx b/src/main/frontend/app/components/flow/canvas-context-menu.tsx index 0892cd54..312c0a6c 100644 --- a/src/main/frontend/app/components/flow/canvas-context-menu.tsx +++ b/src/main/frontend/app/components/flow/canvas-context-menu.tsx @@ -46,7 +46,7 @@ export default function CanvasContextMenu({ const itemClass = 'flex items-center justify-between gap-6 px-3 py-1.5 text-sm whitespace-nowrap' const enabledClass = `${itemClass} cursor-pointer hover:bg-hover text-foreground` - const disabledClass = `${itemClass} cursor-default text-muted-foreground opacity-50` + const disabledClass = `${itemClass} cursor-default text-foreground-muted opacity-50` function menuItem(label: string, onClick: () => void, enabled: boolean, shortcutId?: string) { return ( @@ -62,7 +62,7 @@ export default function CanvasContextMenu({ } > {label} - {shortcutId && {formatShortcut(shortcutId)}} + {shortcutId && {formatShortcut(shortcutId)}}
) } diff --git a/src/main/frontend/app/components/flow/create-node-modal.tsx b/src/main/frontend/app/components/flow/create-node-modal.tsx index 9ffe6de0..fd69de47 100644 --- a/src/main/frontend/app/components/flow/create-node-modal.tsx +++ b/src/main/frontend/app/components/flow/create-node-modal.tsx @@ -3,6 +3,8 @@ import useFlowStore from '~/stores/flow-store' import useNodeContextStore from '~/stores/node-context-store' import { useFFDoc } from '@frankframework/doc-library-react' import Button from '../inputs/button' +import CloseButton from '../inputs/close-button' +import Search from '~/components/search/search' import type { Elements, FFDocJson } from '@frankframework/doc-library-core' interface CreateNodeModalProperties { @@ -120,13 +122,7 @@ function CreateNodeModal({

Add Node

Select the element to be added from the list below.

- handleOnChange(event)} - className="border-border focus:ring-foreground-active mb-3 w-full rounded border px-3 py-2 focus:ring focus:outline-none" - /> + handleOnChange(event)} />
    {filteredElements.length > 0 ? ( @@ -139,7 +135,7 @@ function CreateNodeModal({ onClick={() => setSelectedElement(element.name)} onDoubleClick={handleCreateNode} className={`cursor-pointer px-3 py-2 ${ - isSelected ? 'bg-foreground-active text-background' : 'hover:bg-foreground-active/10' + isSelected ? 'bg-foreground-active text-background' : 'hover:bg-hover' }`} > {element.name} @@ -147,14 +143,12 @@ function CreateNodeModal({ ) }) ) : ( -
  • No results found
  • +
  • No results found
  • )}
- +
diff --git a/src/main/frontend/app/components/git/git-changes.tsx b/src/main/frontend/app/components/git/git-changes.tsx index 67d13752..55cea6fa 100644 --- a/src/main/frontend/app/components/git/git-changes.tsx +++ b/src/main/frontend/app/components/git/git-changes.tsx @@ -80,7 +80,7 @@ function FileSection({ > {collapsed ? '▸' : '▾'} {title} - + {files.length} @@ -130,7 +130,7 @@ function FileSection({ {fileName} - {dirPath && {dirPath}} + {dirPath && {dirPath}}
) diff --git a/src/main/frontend/app/components/git/git-commit-box.tsx b/src/main/frontend/app/components/git/git-commit-box.tsx index 51bd7c28..f12ef81f 100644 --- a/src/main/frontend/app/components/git/git-commit-box.tsx +++ b/src/main/frontend/app/components/git/git-commit-box.tsx @@ -21,7 +21,7 @@ export default function GitCommitBox({ value={commitMessage} onChange={(e) => onMessageChange(e.target.value)} placeholder="Commit message..." - className="border-border bg-background text-foreground placeholder:text-muted-foreground w-full resize-none rounded border p-2.5 text-xs focus:outline-none" + className="border-border bg-background text-foreground placeholder:text-foreground-muted w-full resize-none rounded border p-2.5 text-xs focus:outline-none" rows={4} onKeyDown={(e) => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { diff --git a/src/main/frontend/app/components/git/git-toolbar.tsx b/src/main/frontend/app/components/git/git-toolbar.tsx index d2c292b6..2c659423 100644 --- a/src/main/frontend/app/components/git/git-toolbar.tsx +++ b/src/main/frontend/app/components/git/git-toolbar.tsx @@ -2,6 +2,7 @@ import { useState } from 'react' import clsx from 'clsx' import type { GitStatus } from '~/types/git.types' import Button from '~/components/inputs/button' +import Input from '~/components/inputs/input' interface GitToolbarProps { status: GitStatus | null @@ -97,14 +98,13 @@ export default function GitToolbar({ )} {showToken && !status?.isLocal && (
- onTokenChange(e.target.value)} placeholder={ hasStoredToken ? 'Using saved token (override here)' : 'Personal access token (for private repos)' } - className="border-border bg-background text-foreground placeholder:text-muted-foreground w-full rounded border px-2 py-1 text-xs focus:outline-none" />
)} diff --git a/src/main/frontend/app/components/inputs/button.tsx b/src/main/frontend/app/components/inputs/button.tsx index b193d0f1..448bb4cc 100644 --- a/src/main/frontend/app/components/inputs/button.tsx +++ b/src/main/frontend/app/components/inputs/button.tsx @@ -1,22 +1,27 @@ import React from 'react' import clsx from 'clsx' +type ButtonVariant = 'default' | 'ghost' + +export function buttonClasses(variant: ButtonVariant = 'default', disabled?: boolean, className?: string) { + return clsx( + 'text-foreground rounded-md px-4 py-2 transition-colors', + variant === 'default' && 'border-border bg-backdrop border', + variant === 'ghost' && 'border border-transparent bg-transparent', + !disabled && 'hover:bg-hover active:bg-selected hover:cursor-pointer', + disabled && 'text-foreground-muted', + className, + ) +} + export default function Button({ children, className, + variant = 'default', ...properties -}: React.PropsWithChildren>>) { +}: React.PropsWithChildren & { variant?: ButtonVariant }>>) { return ( - ) diff --git a/src/main/frontend/app/components/inputs/close-button.tsx b/src/main/frontend/app/components/inputs/close-button.tsx new file mode 100644 index 00000000..ac448993 --- /dev/null +++ b/src/main/frontend/app/components/inputs/close-button.tsx @@ -0,0 +1,17 @@ +import React from 'react' +import clsx from 'clsx' +import CloseIcon from '/icons/custom/Close.svg?react' + +interface CloseButtonProps { + onClick?: (event: React.MouseEvent) => void + className?: string +} + +export default function CloseButton({ onClick, className }: Readonly) { + return ( + + ) +} diff --git a/src/main/frontend/app/components/inputs/dropdown.tsx b/src/main/frontend/app/components/inputs/dropdown.tsx index 20aead4c..537344c0 100644 --- a/src/main/frontend/app/components/inputs/dropdown.tsx +++ b/src/main/frontend/app/components/inputs/dropdown.tsx @@ -39,7 +39,8 @@ export default function Dropdown({ }, [optionsArray, selectedValue]) const getSelectedLabel = () => { - return selectedValue ? options[selectedValue] : placeholder + if (selectedValue !== undefined && selectedValue in options) return options[selectedValue] + return placeholder } const toggleDropdown = useCallback(() => { @@ -179,12 +180,14 @@ export default function Dropdown({
- + {getSelectedLabel()} diff --git a/src/main/frontend/app/components/inputs/input.tsx b/src/main/frontend/app/components/inputs/input.tsx index 6b43e651..09ae36a7 100644 --- a/src/main/frontend/app/components/inputs/input.tsx +++ b/src/main/frontend/app/components/inputs/input.tsx @@ -2,7 +2,7 @@ import React from 'react' import { twMerge } from 'tailwind-merge' export type InputProperties = { - onChange: (event: React.ChangeEvent) => void + onChange?: (event: React.ChangeEvent) => void value?: string placeholder?: string wrapperClassName?: string diff --git a/src/main/frontend/app/components/inputs/kebab-menu.tsx b/src/main/frontend/app/components/inputs/kebab-menu.tsx new file mode 100644 index 00000000..7f0b5feb --- /dev/null +++ b/src/main/frontend/app/components/inputs/kebab-menu.tsx @@ -0,0 +1,94 @@ +import { type ReactNode, useEffect, useRef, useState } from 'react' +import { createPortal } from 'react-dom' +import clsx from 'clsx' +import KebabIcon from '/icons/solar/Kebab Vertical.svg?react' + +export interface KebabMenuItem { + label: string + icon?: ReactNode + onClick: () => void + className?: string +} + +interface KebabMenuProperties { + items: KebabMenuItem[] + triggerClassName?: string +} + +export default function KebabMenu({ items, triggerClassName }: Readonly) { + const [menuPosition, setMenuPosition] = useState<{ x: number; y: number } | null>(null) + const triggerRef = useRef(null) + const menuRef = useRef(null) + + useEffect(() => { + if (!menuPosition) return + + const handleMouseDown = (event: MouseEvent) => { + const clickedTrigger = triggerRef.current?.contains(event.target as Node) + const clickedMenu = menuRef.current?.contains(event.target as Node) + if (!clickedTrigger && !clickedMenu) setMenuPosition(null) + } + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') setMenuPosition(null) + } + + document.addEventListener('mousedown', handleMouseDown) + document.addEventListener('keydown', handleKeyDown) + return () => { + document.removeEventListener('mousedown', handleMouseDown) + document.removeEventListener('keydown', handleKeyDown) + } + }, [menuPosition]) + + const openMenu = (event: React.MouseEvent) => { + event.stopPropagation() + if (menuPosition) { + setMenuPosition(null) + return + } + const rect = triggerRef.current?.getBoundingClientRect() + if (rect) setMenuPosition({ x: rect.right, y: rect.bottom }) + } + + return ( + <> + + + {menuPosition && + createPortal( +
+ {items.map((item) => ( + + ))} +
, + document.body, + )} + + ) +} diff --git a/src/main/frontend/app/components/inputs/link-button.tsx b/src/main/frontend/app/components/inputs/link-button.tsx new file mode 100644 index 00000000..c439da1a --- /dev/null +++ b/src/main/frontend/app/components/inputs/link-button.tsx @@ -0,0 +1,17 @@ +import React from 'react' +import { buttonClasses } from './button' + +type ButtonVariant = 'default' | 'ghost' + +export default function LinkButton({ + children, + className, + variant = 'default', + ...properties +}: React.PropsWithChildren & { variant?: ButtonVariant }>>) { + return ( + + {children} + + ) +} diff --git a/src/main/frontend/app/components/inputs/segmented-button.tsx b/src/main/frontend/app/components/inputs/segmented-button.tsx new file mode 100644 index 00000000..59af2185 --- /dev/null +++ b/src/main/frontend/app/components/inputs/segmented-button.tsx @@ -0,0 +1,26 @@ +import React from 'react' +import clsx from 'clsx' + +interface SegmentedButtonProperties extends React.ButtonHTMLAttributes { + isActive?: boolean +} + +export default function SegmentedButton({ + isActive = false, + className, + children, + ...properties +}: Readonly) { + return ( + + ) +} diff --git a/src/main/frontend/app/components/inputs/validatedInput.tsx b/src/main/frontend/app/components/inputs/validatedInput.tsx index 2b070950..c4fa778a 100644 --- a/src/main/frontend/app/components/inputs/validatedInput.tsx +++ b/src/main/frontend/app/components/inputs/validatedInput.tsx @@ -54,7 +54,7 @@ export default function ValidatedInput({ setInputValue(newValue) validateInput(newValue) - onChange(event) + onChange?.(event) } return ( diff --git a/src/main/frontend/app/components/loading-spinner.tsx b/src/main/frontend/app/components/loading-spinner.tsx index 66ef4dd8..08cf75b6 100644 --- a/src/main/frontend/app/components/loading-spinner.tsx +++ b/src/main/frontend/app/components/loading-spinner.tsx @@ -16,7 +16,7 @@ export default function LoadingSpinner({ size = 'md', message, className }: Read return (
- {message &&

{message}

} + {message &&

{message}

}
) } diff --git a/src/main/frontend/app/components/navbar/navbar-link.tsx b/src/main/frontend/app/components/navbar/navbar-link.tsx index 3d23718c..b76d4e7a 100644 --- a/src/main/frontend/app/components/navbar/navbar-link.tsx +++ b/src/main/frontend/app/components/navbar/navbar-link.tsx @@ -27,11 +27,16 @@ export default function NavbarLink({ route, label, Icon }: Readonly {Icon && ( - + )}