Files
Joshen Lim 93d9d80535 joshen/fe 4149 explorer support adding removing cells in notebook (#49037)
## Context

Related to Explorer/Notebooks - this PR adds the functionality to add /
remove cells

Separately - am thinking we can shift a lot of the "cell update" logic
into notebook-state, mainly so that each UI component doesn't need to be
aware of the notebook's `cells` but just its own cell.

I'll do it separately though to prevent bloating this PR, already left
comments where i think can be refactored

<img width="250" alt="image"
src="https://github.com/user-attachments/assets/91aae5f9-07b9-4327-8e21-8e9d71098aa6"
/>
<img width="250" alt="image"
src="https://github.com/user-attachments/assets/677de208-5c52-4155-90c6-bc0879070693"
/>
<img width="250" alt="image"
src="https://github.com/user-attachments/assets/17c3dae7-87bc-40bb-bfa0-15df101006ce"
/>
<img width="1091" height="262" alt="image"
src="https://github.com/user-attachments/assets/a09c3d23-696a-4b55-a1f8-4602e56a60dc"
/>



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## New Features
- Add query and Markdown cells directly within notebooks.
- Move, remove, and reorder cells using drag-and-drop or cell controls.
- Add cells from empty states and notebook toolbar actions.
- Edit Markdown cells, mark edits as complete, and see placeholders for
empty content.

## Improvements
- Cell controls and hover interactions are more consistent and
responsive.
- Moving cells is disabled at the top or bottom of a notebook.
- Sample cells now use standardized content and formatting.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-14 00:17:29 +07:00

171 lines
4.7 KiB
TypeScript

import { Edit } from 'lucide-react'
import React, { useState } from 'react'
import { Button, cn } from 'ui'
import { Input } from 'ui-patterns/DataInputs/Input'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
export type ExplorerToolbarProps = React.ComponentProps<'div'>
/**
* Shared toolbar row for Explorer resources such as notebooks and queries.
* Defaults to 40px and follows `--header-height` from the `md` breakpoint up, so
* a consumer that defines its own header height gets toolbars matching it. The
* fallback keeps the row at its default height in apps that leave the variable
* unset, rather than dropping the rule.
*/
const ExplorerToolbar = ({ className, role = 'toolbar', ...props }: ExplorerToolbarProps) => (
<div
data-slot="explorer-toolbar"
role={role}
className={cn(
'flex h-10 w-full shrink-0 items-center gap-2 border-b bg-transparent px-3',
className
)}
{...props}
/>
)
ExplorerToolbar.displayName = 'ExplorerToolbar'
export type ExplorerToolbarIconProps = React.ComponentProps<'span'>
/** Decorative resource icon shown before the toolbar title. */
const ExplorerToolbarIcon = ({
'aria-hidden': ariaHidden = true,
className,
...props
}: ExplorerToolbarIconProps) => (
<span
data-slot="explorer-toolbar-icon"
aria-hidden={ariaHidden}
className={cn('shrink-0 text-foreground-muted [&_svg]:size-3.5', className)}
{...props}
/>
)
ExplorerToolbarIcon.displayName = 'ExplorerToolbarIcon'
export type ExplorerToolbarTitleProps = Omit<React.ComponentProps<'div'>, 'children'> & {
children: string
onSaveTitle?: (value: string) => void
}
/** Flexible title region for static text or an editable resource name. */
const ExplorerToolbarTitle = ({
children: title,
className,
onSaveTitle,
...props
}: ExplorerToolbarTitleProps) => {
const [isEditingTitle, setIsEditingTitle] = useState(false)
const [value, setValue] = useState(title)
const handleStartEditing = () => {
setValue(title)
setIsEditingTitle(true)
}
return (
<div
data-slot="explorer-toolbar-title"
className={cn('min-w-0 flex-1 truncate text-sm', className)}
{...props}
>
{isEditingTitle ? (
<Input
autoFocus
size="tiny"
containerClassName="max-w-64 has-[[data-slot=input-group-control]:focus-visible]:ring-0"
className="outline-none"
value={value}
onChange={(e) => setValue(e.target.value)}
onBlur={() => {
if (isEditingTitle) {
setIsEditingTitle(false)
onSaveTitle?.(value)
}
}}
onKeyDown={(e) => {
if (e.key === 'Escape') {
e.preventDefault()
e.stopPropagation()
setIsEditingTitle(false)
} else if (e.key === 'Enter') {
e.preventDefault()
e.stopPropagation()
setIsEditingTitle(false)
onSaveTitle?.(value)
}
}}
/>
) : onSaveTitle ? (
<Button
variant="text"
className="group/title"
onClick={handleStartEditing}
iconRight={<Edit className="opacity-0 group-hover/title:opacity-100 transition" />}
>
{title}
</Button>
) : (
title
)}
</div>
)
}
ExplorerToolbarTitle.displayName = 'ExplorerToolbarTitle'
export type ExplorerToolbarActionsProps = React.ComponentProps<'div'>
/**
* Trailing region for direct actions and composite controls such as badges,
* source menus, and display configuration.
*/
const ExplorerToolbarActions = ({ className, ...props }: ExplorerToolbarActionsProps) => (
<div
data-slot="explorer-toolbar-actions"
className={cn('ml-auto flex shrink-0 items-center gap-px', className)}
{...props}
/>
)
ExplorerToolbarActions.displayName = 'ExplorerToolbarActions'
export type ExplorerToolbarActionProps = Omit<
React.ComponentPropsWithRef<typeof Button>,
'size' | 'variant'
> & {
tooltip?: string
}
/**
* The standard tiny, text-style button used for a direct toolbar action.
* Icon-only actions receive the prototype's compact 28px width automatically.
*/
const ExplorerToolbarAction = ({
children,
className,
ref,
tooltip,
...props
}: ExplorerToolbarActionProps) => (
<ButtonTooltip
ref={ref}
data-slot="explorer-toolbar-action"
variant="text"
size="tiny"
className={cn(children == null && 'w-7 px-0', className)}
tooltip={{ content: { side: 'bottom', text: tooltip } }}
{...props}
>
{children}
</ButtonTooltip>
)
ExplorerToolbarAction.displayName = 'ExplorerToolbarAction'
export {
ExplorerToolbar,
ExplorerToolbarAction,
ExplorerToolbarActions,
ExplorerToolbarIcon,
ExplorerToolbarTitle,
}