simian 0.1.1

A command-line tool for exploring and implementing Machine Learning algorithms in Rust.
Documentation
import { FC, Fragment, useCallback, useMemo, useRef, useState } from 'react'
import clsx from 'clsx'
import { Node, Transforms } from 'slate'
import { ReactEditor, useSlateStatic } from 'slate-react'

import WidthFull from '/width-full.svg'
import WidthStandard from '/width-standard.svg'
import WidthWide from '/width-wide.svg'
import { Button } from '@/components/ui/button'

import { useMode } from '../../context'
import { useBlock } from '../context'

import { BlockMenuButtonProps, BlockMenuItem, BlockMenuProps } from './types'

//////////////////////////////////////////////////
// Utilitary Components
//////////////////////////////////////////////////
const DefaultMenuButton: FC<BlockMenuButtonProps> = ({ item, ...props }) => {
  return (
    <Button
      {...props}
      onClick={item.onClick ?? props.onClick}
      variant="ghost"
      size="icon"
      // @ts-ignore
className={clsx(
        'h-8 w-8 rounded-full transition-colors duration-200',

        // LIGHT MODE (Dark Menu):
        // We use a light overlay with very low opacity so it looks like
        // a subtle "lift" from the dark background.
        'text-zinc-400 hover:text-white hover:bg-white/10',

        // DARK MODE (Light Menu):
        // Standard subtle dark tint.
        'dark:text-zinc-500 dark:hover:text-zinc-950 dark:hover:bg-zinc-100',

        // ACTIVE STATE:
        // When the button represents the current width, we make it slightly more visible.
        item.isActive &&
          'text-white bg-white/20 dark:text-zinc-950 dark:bg-zinc-200',
      )}
    />
  )
}

//////////////////////////////////////////////////
// Main Component
//////////////////////////////////////////////////
export const BlockMenu: FC<BlockMenuProps> = ({
  children,
  MenuButton,
  items,
}) => {
  const ctx = useBlock()
  const mode = useMode()
  const editor = useSlateStatic()
  const [isMenuOpen, setIsMenuOpen] = useState(false)
  const timeoutRef = useRef<NodeJS.Timeout | null>(null)

  const setWidth = useCallback(
    (width: string) => {
      const newBlocks = {
        ...(ctx.element.blocks ?? {}),
        [ctx.blockId]: {
          ...(ctx.element.blocks?.[ctx.blockId] ?? {}),
          width,
        },
      }

      console.info(
        {
          element: ctx.element,
          blockId: ctx.blockId,
          width,
          newBlocks,
        },
        'setWidh',
      )

      const path = ReactEditor.findPath(editor, ctx.element)

      Transforms.setNodes(
        editor,
        {
          blocks: newBlocks,
        } as Partial<Node>,
        { at: path },
      )

      if ('width' in ctx.element) {
        Transforms.unsetNodes(editor, 'width', { at: path })
      }
    },
    [editor, ctx.blockId, ctx.element],
  )

  const handleMouseEnter = useCallback(() => {
    console.info('handling mouse enter')
    if (timeoutRef.current) clearTimeout(timeoutRef.current)
    setIsMenuOpen(true)
  }, [])

  const handleMouseLeave = useCallback(() => {
    console.info('handling mouse leave')
    // Small delay (200ms) prevents the menu from flickering if the
    // mouse just grazes the edge of the buffer
    timeoutRef.current = setTimeout(() => {
      setIsMenuOpen(false)
    }, 0)
  }, [])

  const baseItems = useMemo<BlockMenuItem[][]>(
    () => [
      ...(ctx.isResizable
        ? [
            [
              {
                id: 'width-standard',
                icon: <WidthStandard // @ts-ignore
className="h-4 w-4" />,
                isActive: !ctx.width || ctx.width === 'standard',
                onClick: () => setWidth('standard'),
              },
              {
                id: 'width-wide',
                icon: <WidthWide // @ts-ignore
className="h-4 w-4" />,
                isActive: ctx.width === 'wide',
                onClick: () => setWidth('wide'),
              },
              {
                id: 'width-full',
                icon: <WidthFull // @ts-ignore
className="h-4 w-4" />,
                isActive: ctx.width === 'full',
                onClick: () => setWidth('full'),
              },
            ] satisfies BlockMenuItem[],
          ]
        : []),
    ],
    [ctx, setWidth],
  )

  const resolvedItems = useMemo(() => {
    if (!items) {
      return null
    }
    if (items === true) {
      return baseItems
    }
    if (typeof items === 'function') {
      return items(baseItems)
    }
    if (items.length === 0) {
      return baseItems
    }

    const groups: BlockMenuItem[][] = []
    let currGroup: BlockMenuItem[] = []

    for (const item of items) {
      if (Array.isArray(item)) {
        if (item.length) {
          if (currGroup.length > 0) {
            groups.push(currGroup)
            currGroup = []
          }

          groups.push(item)
        }
      } else {
        currGroup.push(item)
      }
    }

    if (currGroup.length) {
      groups.push(currGroup) // Push the last group if needed.
    }

    return groups
  }, [baseItems, items])

  // Skip rendering the menu.
  if (!resolvedItems || resolvedItems.length === 0) {
    return <>{children}</>
  }

  return (
    <div // @ts-ignore
className="relative" onMouseLeave={handleMouseLeave}>
      {/* Floating Menu */}
      {mode !== 'read' && (
        <div
          // @ts-ignore
className={clsx(
            // "bg-yellow-400",
            'absolute -top-12 left-0 w-full h-12 z-10',
            'flex items-start justify-center', // Centering logic
            isMenuOpen ? 'pointer-events-auto' : 'pointer-events-none',
          )}
          onMouseEnter={handleMouseEnter}
        >
          {/* THE ACTUAL MENU */}
          <div
            // @ts-ignore
className={clsx(
              'flex items-center gap-1 p-1 rounded-full border shadow-xl',
              'transition-all duration-200 ease-out',

              // Inverted Colors
              'bg-zinc-950/90 text-white border-zinc-800 backdrop-blur-md',
              'dark:bg-white/95 dark:text-zinc-950 dark:border-white',

              // Visibility
              isMenuOpen
                ? 'opacity-100 translate-y-0'
                : 'opacity-0 translate-y-2',
            )}
            onMouseLeave={handleMouseLeave}
          >
            {resolvedItems.map((items, idx) => (
              <Fragment key={`group-${items[0].id}`}>
                {idx > 0 ? (
                  <div
                    // @ts-ignore
className={clsx([
                      'w-px h-4 bg-zinc-200 dark:bg-zinc-800 mx-1',
                    ])}
                  />
                ) : null}

                {items.map((item) =>
                  MenuButton ? (
                    <MenuButton
                      key={item.id}
                      item={item}
                      DefaultMenuButton={DefaultMenuButton}
                    />
                  ) : (
                    <DefaultMenuButton key={item.id} item={item}>
                      {item.icon}
                    </DefaultMenuButton>
                  ),
                )}
              </Fragment>
            ))}
          </div>
        </div>
      )}

      {/* Main Content */}
      <div
        //// @ts-ignore
className="bg-blue-400"
        onMouseEnter={handleMouseEnter}
      >
        {children}
      </div>
    </div>
  )
}

export * from './types'