From e932d704cb6dfd2cb0436e19cd5bd05453b8aefc Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Mon, 27 Apr 2026 19:52:31 +0530 Subject: [PATCH] feat(rocket): add Popover component (#16009) Adds shadcn Popover primitive and Rocket HOC wrapping it with ToolJet design tokens, plus Storybook stories and spec. Co-authored-by: Claude Opus 4.7 (1M context) --- .../components/ui/Rocket/Popover/Popover.jsx | 104 ++++++++++++++++ .../ui/Rocket/Popover/Popover.spec.md | 88 ++++++++++++++ .../ui/Rocket/Popover/Popover.stories.jsx | 115 ++++++++++++++++++ frontend/src/components/ui/Rocket/index.js | 8 ++ .../components/ui/Rocket/shadcn/popover.jsx | 49 ++++++++ 5 files changed, 364 insertions(+) create mode 100644 frontend/src/components/ui/Rocket/Popover/Popover.jsx create mode 100644 frontend/src/components/ui/Rocket/Popover/Popover.spec.md create mode 100644 frontend/src/components/ui/Rocket/Popover/Popover.stories.jsx create mode 100644 frontend/src/components/ui/Rocket/shadcn/popover.jsx 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 };