diff --git a/frontend/src/components/ui/Rocket/Popover/Popover.jsx b/frontend/src/components/ui/Rocket/Popover/Popover.jsx new file mode 100644 index 0000000000..3c6531ff7f --- /dev/null +++ b/frontend/src/components/ui/Rocket/Popover/Popover.jsx @@ -0,0 +1,104 @@ +import React, { forwardRef } from 'react'; +import PropTypes from 'prop-types'; +import * as PopoverPrimitive from '@radix-ui/react-popover'; +import { cn } from '@/lib/utils'; + +import { PopoverTrigger } from '@/components/ui/Rocket/shadcn/popover'; + +// ── PopoverContent ─────────────────────────────────────────────────────────── + +const PopoverContent = forwardRef(function PopoverContent( + { className, align = 'center', side = 'bottom', sideOffset = 4, children, ...props }, + ref +) { + return ( + + + {children} + + + ); +}); +PopoverContent.displayName = 'PopoverContent'; +PopoverContent.propTypes = { + align: PropTypes.oneOf(['start', 'center', 'end']), + side: PropTypes.oneOf(['top', 'right', 'bottom', 'left']), + sideOffset: PropTypes.number, + className: PropTypes.string, + children: PropTypes.node, +}; + +// ── PopoverHeader ──────────────────────────────────────────────────────────── + +function PopoverHeader({ className, ...props }) { + return
; +} +PopoverHeader.displayName = 'PopoverHeader'; +PopoverHeader.propTypes = { + className: PropTypes.string, +}; + +// ── PopoverTitle ───────────────────────────────────────────────────────────── + +const PopoverTitle = forwardRef(function PopoverTitle({ className, ...props }, ref) { + return ( +
+ ); +}); +PopoverTitle.displayName = 'PopoverTitle'; +PopoverTitle.propTypes = { + className: PropTypes.string, +}; + +// ── PopoverDescription ─────────────────────────────────────────────────────── + +const PopoverDescription = forwardRef(function PopoverDescription({ className, ...props }, ref) { + return ( +

+ ); +}); +PopoverDescription.displayName = 'PopoverDescription'; +PopoverDescription.propTypes = { + className: PropTypes.string, +}; + +// ── Popover (root pass-through) ────────────────────────────────────────────── + +const Popover = PopoverPrimitive.Root; + +// ── Exports ────────────────────────────────────────────────────────────────── + +export { Popover, PopoverContent, PopoverHeader, PopoverTitle, PopoverDescription, PopoverTrigger }; diff --git a/frontend/src/components/ui/Rocket/Popover/Popover.spec.md b/frontend/src/components/ui/Rocket/Popover/Popover.spec.md new file mode 100644 index 0000000000..da6d3dd2ff --- /dev/null +++ b/frontend/src/components/ui/Rocket/Popover/Popover.spec.md @@ -0,0 +1,88 @@ +# Popover — Rocket Design Spec + + + +## Overview + +Popover is a lightweight, non-modal overlay anchored to a trigger element. Used to surface supplementary information, simple forms, or quick actions without blocking the rest of the UI. Unlike Dialog, there is no backdrop overlay, no focus trap escalation, and no close button — clicking outside dismisses. + +Structurally identical to the shadcn primitive's sub-component set. Styling matches Dialog's card tokens (surface, elevation, radius) with tighter padding to suit its smaller footprint. + +## Props (PopoverContent) + +| Prop | Type | Values | Default | +|---|---|---|---| +| align | string | start \| center \| end | center | +| side | string | top \| right \| bottom \| left | bottom | +| sideOffset | number | — | 4 | +| className | string | — | — | + +(All other props forward to Radix `Popover.Content` — `onOpenAutoFocus`, `onInteractOutside`, etc.) + +## Sub-components + +| Component | Wraps | Styling | +|---|---|---| +| `Popover` | Radix Popover.Root (pass-through) | none | +| `PopoverTrigger` | Re-export from shadcn | none | +| `PopoverContent` | Radix Content (direct, via Portal) | card tokens, padding, animations | +| `PopoverHeader` | plain div | flex column, tight gap | +| `PopoverTitle` | plain div | tw-font-title-large + color | +| `PopoverDescription` | plain div | tw-font-body-small + color | + +## Token Mapping + +| Element | State | Figma token | ToolJet class | +|---|---|---|---| +| content bg | default | bg-surface-layer-01 | tw-bg-background-surface-layer-01 | +| content shadow | default | Elevations/400 | tw-shadow-elevation-400 | +| content radius | default | 8px | tw-rounded-lg | +| content border | default | border/weak | tw-border-solid tw-border tw-border-border-weak | +| content padding | default | 12px | tw-p-3 | +| content width | default | 288px (overridable via className) | tw-w-72 | +| content gap | default | 10px (between header and other content) | tw-gap-2.5 | +| header gap | default | 2px (between title and description) | tw-gap-0.5 | +| title text | default | text/default 14px Medium | tw-font-title-large tw-text-text-default | +| description text | default | text/placeholder 11px Regular | tw-font-body-small tw-text-text-placeholder | + +## Slots + +- trigger (required) — any element that opens the popover, via `PopoverTrigger` + `asChild` +- content (required) — arbitrary children inside `PopoverContent` +- header (optional) — `PopoverHeader` groups title + description +- title (optional) — `PopoverTitle`, typically inside header +- description (optional) — `PopoverDescription`, typically inside header + +## Usage Pattern + +```jsx + + + + + + + Dimensions + Set the width and height. + + {/* arbitrary body content */} + + +``` + +## CVA Shape + +Shape E — compound/multi-part, but **no CVA on any sub-component** (no variants, no sizes). All sub-components use static `cn()` calls. No `popoverVariants` export. + +## Notes + +- No overlay / backdrop — Radix Popover is non-modal; the page behind stays interactive. +- No close button — dismiss by clicking outside, pressing Escape, or re-clicking the trigger (native Radix behavior). +- No arrow indicator — not requested. +- No `PopoverAnchor` re-export — the trigger IS the anchor (Radix default). If a consumer needs a separate anchor later, add as a pass-through re-export. +- Typography uses plugin token utilities (`tw-font-title-large`, `tw-font-body-small`) — matches Dialog scale, not AlertDialog's larger scale. +- Padding is `tw-p-3` (12px), tighter than Dialog's 24px body padding — popovers are compact. +- Default width `tw-w-72` (288px) from shadcn — overridable via `className` on `PopoverContent`. +- Animations inherited from shadcn: `data-[state=open]:tw-animate-in`, `data-[state=closed]:tw-animate-out`, side-based slide-in. +- `forwardRef` is required on `PopoverContent` — Radix internally uses `React.cloneElement` for positioning refs; missing forwardRef silently breaks alignment. +- Header/Title/Description wrap plain `div`/`p` elements (not Radix primitives) — there is no `Popover.Title`/`Popover.Description` in Radix UI. shadcn's generated file also uses plain divs. diff --git a/frontend/src/components/ui/Rocket/Popover/Popover.stories.jsx b/frontend/src/components/ui/Rocket/Popover/Popover.stories.jsx new file mode 100644 index 0000000000..5632ecfc51 --- /dev/null +++ b/frontend/src/components/ui/Rocket/Popover/Popover.stories.jsx @@ -0,0 +1,115 @@ +import React from 'react'; +import { Popover, PopoverContent, PopoverHeader, PopoverTitle, PopoverDescription, PopoverTrigger } from './Popover'; +import { Button } from '../Button/Button'; + +export default { + title: 'Rocket/Popover', + component: Popover, + tags: ['autodocs'], + parameters: { layout: 'centered' }, +}; + +// ── Default ────────────────────────────────────────────────────────────────── + +export const Default = { + render: () => ( + + + + + + + Dimensions + Set the width and height for the layer. + + + + ), +}; + +// ── Title only ─────────────────────────────────────────────────────────────── + +export const TitleOnly = { + render: () => ( + + + + + + + Quick settings + + + + ), +}; + +// ── With body content ──────────────────────────────────────────────────────── + +export const WithBodyContent = { + render: () => ( + + + + + + + Share this page + Anyone with the link can view. + +

+ + +
+ + + ), +}; + +// ── Alignment ──────────────────────────────────────────────────────────────── + +export const Alignment = { + render: () => ( +
+ {['start', 'center', 'end'].map((align) => ( + + + + + + + Aligned: {align} + Content is anchored to the {align} edge of the trigger. + + + + ))} +
+ ), +}; + +// ── Sides ──────────────────────────────────────────────────────────────────── + +export const Sides = { + render: () => ( +
+ {['top', 'right', 'bottom', 'left'].map((side) => ( + + + + + + + Side: {side} + Popover opens to the {side} of the trigger. + + + + ))} +
+ ), +}; diff --git a/frontend/src/components/ui/Rocket/index.js b/frontend/src/components/ui/Rocket/index.js index 76313b24aa..7ec9fe63f4 100644 --- a/frontend/src/components/ui/Rocket/index.js +++ b/frontend/src/components/ui/Rocket/index.js @@ -201,3 +201,11 @@ export { Checkbox, checkboxVariants } from './Checkbox/Checkbox'; export { RadioGroup, RadioGroupItem, radioGroupItemVariants } from './RadioGroup/RadioGroup'; export { Textarea, textareaVariants } from './Textarea/Textarea'; export { Spinner, spinnerVariants } from './Spinner/Spinner'; +export { + Popover, + PopoverContent, + PopoverHeader, + PopoverTitle, + PopoverDescription, + PopoverTrigger, +} from './Popover/Popover'; diff --git a/frontend/src/components/ui/Rocket/shadcn/popover.jsx b/frontend/src/components/ui/Rocket/shadcn/popover.jsx new file mode 100644 index 0000000000..e6498d91db --- /dev/null +++ b/frontend/src/components/ui/Rocket/shadcn/popover.jsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import * as PopoverPrimitive from '@radix-ui/react-popover'; + +import { cn } from '@/lib/utils'; + +function Popover({ ...props }) { + return ; +} + +function PopoverTrigger({ ...props }) { + return ; +} + +function PopoverContent({ className, align = 'center', sideOffset = 4, ...props }) { + return ( + + + + ); +} + +function PopoverAnchor({ ...props }) { + return ; +} + +function PopoverHeader({ className, ...props }) { + return ( +
+ ); +} + +function PopoverTitle({ className, ...props }) { + return
; +} + +function PopoverDescription({ className, ...props }) { + return

; +} + +export { Popover, PopoverAnchor, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger };