diff --git a/frontend/.storybook/preview.js b/frontend/.storybook/preview.js index 680653af50..e268674daf 100644 --- a/frontend/.storybook/preview.js +++ b/frontend/.storybook/preview.js @@ -1,12 +1,12 @@ /** @type { import('@storybook/react-webpack5').Preview } */ -import "../src/_styles/theme.scss"; -import "./preview.scss"; -import { withColorScheme, withRouter } from "./decorators"; // Import the decorators +import '../src/_styles/theme.scss'; +import './preview.scss'; +import { withColorScheme, withRouter } from './decorators'; // Import the decorators const preview = { parameters: { - actions: { argTypesRegex: "^on[A-Z].*" }, + actions: { argTypesRegex: '^on[A-Z].*' }, controls: { matchers: { color: /(background|color)$/i, @@ -16,20 +16,20 @@ const preview = { }, globalTypes: { theme: { - description: "Color scheme", + description: 'Color scheme', toolbar: { - title: "Theme", - icon: "mirror", + title: 'Theme', + icon: 'mirror', items: [ - { value: "light", title: "Light", icon: "sun" }, - { value: "dark", title: "Dark", icon: "moon" }, + { value: 'light', title: 'Light', icon: 'sun' }, + { value: 'dark', title: 'Dark', icon: 'moon' }, ], dynamicTitle: true, }, }, }, initialGlobals: { - theme: "light", + theme: 'light', }, decorators: [withRouter, withColorScheme], }; diff --git a/frontend/.storybook/preview.scss b/frontend/.storybook/preview.scss index c917b8ab71..27b8094d58 100644 --- a/frontend/.storybook/preview.scss +++ b/frontend/.storybook/preview.scss @@ -13,4 +13,9 @@ body { .storybook-preview-wrapper, .sb-show-main { background-color: var(--background-surface-layer-01) !important; +} + +.sb-show-main.sb-main-centered #storybook-root { + width: 100%; + height: stretch; } \ No newline at end of file diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs index fa8893a820..48a8df0747 100644 --- a/frontend/eslint.config.mjs +++ b/frontend/eslint.config.mjs @@ -86,7 +86,14 @@ export default [ react: { version: 'detect', }, - 'import-x/resolver': 'webpack', + 'import-x/resolver': { + node: { + extensions: ['.js', '.jsx', '.ts', '.tsx'], + }, + webpack: { + config: new URL('./webpack.config.js', import.meta.url).pathname, + }, + }, }, rules: { diff --git a/frontend/package.json b/frontend/package.json index 49ae5ae8ec..95f9c8fc18 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,13 +21,16 @@ "@mdxeditor/editor": "^3.38.0", "@microsoft/fetch-event-source": "^2.0.1", "@radix-ui/colors": "^0.1.8", + "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-checkbox": "^1.0.4", + "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-navigation-menu": "^1.2.14", "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slider": "^1.1.2", @@ -155,6 +158,7 @@ "rfdc": "^1.3.1", "rxjs": "^7.8.0", "semver": "^7.3.8", + "sonner": "^2.0.7", "string-hash": "^1.1.3", "superstruct": "^1.0.3", "tailwind-merge": "^2.6.0", @@ -178,6 +182,7 @@ "@babel/preset-env": "^7.20.2", "@babel/preset-react": "^7.18.6", "@babel/preset-typescript": "^7.28.5", + "@eslint/js": "^9.26.0", "@pmmmwh/react-refresh-webpack-plugin": "^0.6.0", "@storybook/addon-docs": "^9.1.5", "@storybook/addon-links": "^9.1.5", @@ -200,7 +205,6 @@ "css-loader": "^6.7.3", "css-minimizer-webpack-plugin": "^7.0.2", "esbuild": "0.25.9", - "@eslint/js": "^9.26.0", "eslint": "^9.26.0", "eslint-config-prettier": "^8.10.2", "eslint-import-resolver-webpack": "^0.13.10", diff --git a/frontend/src/components/ui/Rocket/AlertDialog/AlertDialog.jsx b/frontend/src/components/ui/Rocket/AlertDialog/AlertDialog.jsx new file mode 100644 index 0000000000..c169198070 --- /dev/null +++ b/frontend/src/components/ui/Rocket/AlertDialog/AlertDialog.jsx @@ -0,0 +1,212 @@ +import React, { forwardRef } from 'react'; +import PropTypes from 'prop-types'; +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'; +import { cva } from 'class-variance-authority'; +import { cn } from '@/lib/utils'; + +import { AlertDialogTrigger, AlertDialogPortal } from '@/components/ui/Rocket/shadcn/alert-dialog'; + +// ── AlertDialog (root pass-through) ───────────────────────────────────────── + +const AlertDialog = AlertDialogPrimitive.Root; + +// ── AlertDialogOverlay ────────────────────────────────────────────────────── + +const AlertDialogOverlay = forwardRef(function AlertDialogOverlay({ className, ...props }, ref) { + return ( + + ); +}); +AlertDialogOverlay.displayName = 'AlertDialogOverlay'; + +// ── AlertDialogContent ────────────────────────────────────────────────────── + +const alertDialogContentVariants = cva( + [ + 'tw-bg-background-surface-layer-01', + 'tw-shadow-elevation-400', + 'tw-rounded-lg', + 'tw-border-solid tw-border tw-border-border-weak', + 'tw-flex tw-flex-col tw-gap-6', + 'tw-p-6', + ], + { + variants: { + size: { + default: 'tw-max-w-[460px]', + small: 'tw-max-w-[320px]', + }, + }, + defaultVariants: { size: 'default' }, + } +); + +const AlertDialogContent = forwardRef(function AlertDialogContent( + { className, children, size, preventClose = true, ...props }, + ref +) { + const handleInteractOutside = (e) => { + if (preventClose) e.preventDefault(); + props.onInteractOutside?.(e); + }; + + const handleEscapeKeyDown = (e) => { + if (preventClose) e.preventDefault(); + props.onEscapeKeyDown?.(e); + }; + + return ( + + + + {children} + + + ); +}); +AlertDialogContent.displayName = 'AlertDialogContent'; +AlertDialogContent.propTypes = { + size: PropTypes.oneOf(['default', 'small']), + preventClose: PropTypes.bool, + className: PropTypes.string, + children: PropTypes.node, +}; + +// ── AlertDialogMedia ──────────────────────────────────────────────────────── +// Icon or image slot. Placed inside AlertDialogHeader. +// Default size: uses media beside title on default, stacked on small. + +function AlertDialogMedia({ className, children, ...props }) { + return ( +
+ {children} +
+ ); +} +AlertDialogMedia.displayName = 'AlertDialogMedia'; +AlertDialogMedia.propTypes = { + className: PropTypes.string, + children: PropTypes.node, +}; + +// ── AlertDialogHeader ─────────────────────────────────────────────────────── +// Groups media + title + description. +// Layout adapts based on size (via group-data) and media presence (via has-[]). + +function AlertDialogHeader({ className, children, ...props }) { + return ( +
[data-slot=alert-dialog-media]]:tw-mb-1.5', + // When small: center text + 'group-data-[size=small]/alert-dialog-content:tw-items-center group-data-[size=small]/alert-dialog-content:tw-text-center', + className + )} + {...props} + > + {children} +
+ ); +} +AlertDialogHeader.displayName = 'AlertDialogHeader'; + +// ── AlertDialogTitle ──────────────────────────────────────────────────────── + +const AlertDialogTitle = forwardRef(function AlertDialogTitle({ className, ...props }, ref) { + return ( + + ); +}); +AlertDialogTitle.displayName = 'AlertDialogTitle'; + +// ── AlertDialogDescription ────────────────────────────────────────────────── + +const AlertDialogDescription = forwardRef(function AlertDialogDescription({ className, ...props }, ref) { + return ( + + ); +}); +AlertDialogDescription.displayName = 'AlertDialogDescription'; + +// ── AlertDialogFooter ─────────────────────────────────────────────────────── + +function AlertDialogFooter({ className, ...props }) { + return ( +
+ ); +} +AlertDialogFooter.displayName = 'AlertDialogFooter'; + +// ── AlertDialogAction ─────────────────────────────────────────────────────── + +const AlertDialogAction = forwardRef(function AlertDialogAction({ className, ...props }, ref) { + return ; +}); +AlertDialogAction.displayName = 'AlertDialogAction'; + +// ── AlertDialogCancel ─────────────────────────────────────────────────────── + +const AlertDialogCancel = forwardRef(function AlertDialogCancel({ className, ...props }, ref) { + return ; +}); +AlertDialogCancel.displayName = 'AlertDialogCancel'; + +// ── Exports ───────────────────────────────────────────────────────────────── + +export { + AlertDialog, + AlertDialogTrigger, + AlertDialogContent, + alertDialogContentVariants, + AlertDialogOverlay, + AlertDialogMedia, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogAction, + AlertDialogCancel, +}; diff --git a/frontend/src/components/ui/Rocket/AlertDialog/AlertDialog.spec.md b/frontend/src/components/ui/Rocket/AlertDialog/AlertDialog.spec.md new file mode 100644 index 0000000000..4ad337adb0 --- /dev/null +++ b/frontend/src/components/ui/Rocket/AlertDialog/AlertDialog.spec.md @@ -0,0 +1,105 @@ +# AlertDialog — Rocket Design Spec + + + +## Overview + +AlertDialog is a modal confirmation dialog that **cannot be dismissed** by pressing Escape or clicking outside. The user must explicitly choose an action (confirm or cancel). Used for destructive or irreversible operations. + +Structurally simpler than Dialog — no header/footer borders, no body scroll. Uses Radix AlertDialog primitives which enforce the non-dismissible behavior natively. + +## Props (AlertDialogContent) + +| Prop | Type | Values | Default | +|---|---|---|---| +| size | string | default \| small | default | +| className | string | — | — | + +## Sizes + +| Value | Max-width | Header alignment | Footer layout | +|---|---|---|---| +| default | 460px | left-aligned | horizontal, justify-between | +| small | 360px | centered | stacked, reverse order | + +## Sub-components + +| Component | Wraps | Styling | +|---|---|---| +| `AlertDialog` | Radix AlertDialog.Root (pass-through) | none | +| `AlertDialogTrigger` | Re-export from shadcn | none | +| `AlertDialogPortal` | Re-export from shadcn | none | +| `AlertDialogOverlay` | Radix Overlay (direct) | tw-bg-black/50, animations | +| `AlertDialogContent` | Radix Content (direct) | card with padding, shadow, rounded, size CVA | +| `AlertDialogMedia` | plain div | 40px icon/image slot | +| `AlertDialogHeader` | plain div | groups media + title + description | +| `AlertDialogTitle` | Radix Title (direct) | tw-font-title-x-large + color | +| `AlertDialogDescription` | Radix Description (direct) | tw-font-body-default + color | +| `AlertDialogFooter` | plain div | justify-between layout, no border | +| `AlertDialogAction` | Radix Action (direct) | accepts Rocket Button as child via asChild | +| `AlertDialogCancel` | Radix Cancel (direct) | accepts Rocket Button as child via asChild | + +## 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 padding | default | 24px | tw-p-6 | +| content gap | default | 24px (between body and footer) | tw-gap-6 | +| overlay bg | default | black/50 | tw-bg-black/50 | +| media size | default | 40px | tw-size-10 | +| body gap | default | 8px (between media/title/desc) | tw-gap-2 | +| title text | default | text/default, 16px Medium 24px | tw-font-title-x-large tw-text-text-default | +| description text | default | text/default, 12px Regular 18px | tw-font-body-default tw-text-text-default | +| footer layout | default | justify-between | tw-flex tw-justify-between tw-items-center | + +## Slots + +- media (optional, `ReactNode`) — 40px icon or image at top, via `AlertDialogMedia` +- title (required, `string`) — heading text +- description (optional, `string`) — supporting text +- cancel (required) — Rocket Button (typically `variant="outline"`) +- action (required) — Rocket Button (typically `variant="primary"` or `variant="primary" danger`) +- secondary action (optional) — additional Rocket Button grouped with action on the right + +## Usage Pattern + +```jsx + + + + + + + + Are you sure? + This action cannot be undone. + + + + + + + + + + + +``` + +## CVA Shape + +Shape C — sizes only. `alertDialogContentVariants` has `size` CVA for max-width. Rest use static `cn()` with `group-data-[size=...]` responsive classes. + +## Notes + +- **Non-dismissible by design**: Radix AlertDialog natively prevents Escape and overlay click dismiss. No `preventClose` prop needed (unlike Dialog). +- No header/footer borders — simpler layout than Dialog. +- No close button (X) — users must use Cancel or Action. +- `small` size: header text centers, footer stacks buttons vertically (reversed so action is on top). +- Footer uses Rocket Button components directly via `asChild` on Action/Cancel. +- The right side of the footer (default size) can hold multiple action buttons (secondary + primary) grouped with `tw-gap-2`. +- Typography uses `tw-font-*` plugin utilities — never manual font combos. +- `data-size` attribute on Content enables `group-data-[size=small]` selectors on child components. diff --git a/frontend/src/components/ui/Rocket/AlertDialog/AlertDialog.stories.jsx b/frontend/src/components/ui/Rocket/AlertDialog/AlertDialog.stories.jsx new file mode 100644 index 0000000000..0cb1f97ab6 --- /dev/null +++ b/frontend/src/components/ui/Rocket/AlertDialog/AlertDialog.stories.jsx @@ -0,0 +1,184 @@ +import React from 'react'; +import { TriangleAlert } from 'lucide-react'; +import { + AlertDialog, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogMedia, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogAction, + AlertDialogCancel, +} from './AlertDialog'; +import { Button } from '@/components/ui/Rocket/Button/Button'; + +export default { + title: 'Rocket/AlertDialog', + tags: ['autodocs'], + parameters: { layout: 'centered' }, +}; + +// ── Default ───────────────────────────────────────────────────────────────── + +export const Default = { + render: () => ( + + + + + + + + + + Lorem ipsum dolor sit amet. + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer elementum mattis arcu, non vulputate est + ornare vitae. + + + + + + +
+ + + + +
+
+
+
+ ), +}; + +// ── Danger ────────────────────────────────────────────────────────────────── + +export const Danger = { + render: () => ( + + + + + + + + + + Are you sure you want to delete? + + This action cannot be undone. This will permanently delete the item and all associated data. + + + + + + +
+ + + + +
+
+
+
+ ), +}; + +// ── Simple (Cancel + Action only) ─────────────────────────────────────────── + +export const Simple = { + render: () => ( + + + + + + + + + + Discard unsaved changes? + + You have unsaved changes that will be lost if you leave this page. + + + + + + + + + + + + + ), +}; + +// ── Small ─────────────────────────────────────────────────────────────────── + +export const Small = { + render: () => ( + + + + + + + + + + Delete this item? + This action cannot be undone. + + + + + + + + + + + + ), +}; + +// ── Without Media ─────────────────────────────────────────────────────────── + +export const WithoutMedia = { + render: () => ( + + + + + + + Confirm this action? + Please confirm you want to proceed with this operation. + + + + + + + + + + + + ), +}; diff --git a/frontend/src/components/ui/Rocket/Checkbox/Checkbox.jsx b/frontend/src/components/ui/Rocket/Checkbox/Checkbox.jsx new file mode 100644 index 0000000000..182daa87f0 --- /dev/null +++ b/frontend/src/components/ui/Rocket/Checkbox/Checkbox.jsx @@ -0,0 +1,90 @@ +import React, { forwardRef } from 'react'; +import PropTypes from 'prop-types'; +import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; +import { cva } from 'class-variance-authority'; +import { CheckIcon, MinusIcon } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +// ── Checkbox ──────────────────────────────────────────────────────────────── + +const checkboxVariants = cva( + [ + // Layout + 'tw-group tw-peer tw-relative tw-flex tw-shrink-0 tw-items-center tw-justify-center', + // Reset (preflight off) + 'tw-appearance-none tw-outline-none', + // Default border + bg + 'tw-border-solid tw-border tw-border-border-default', + 'tw-bg-background-surface-layer-01', + 'tw-transition-colors', + // Checked + indeterminate + 'data-[state=checked]:tw-bg-button-primary data-[state=checked]:tw-border-button-primary', + 'data-[state=indeterminate]:tw-bg-button-primary data-[state=indeterminate]:tw-border-button-primary', + // Icon color when checked + 'tw-text-text-on-solid', + // Disabled + 'disabled:tw-cursor-not-allowed', + 'disabled:tw-bg-controls-base-inactive disabled:tw-border-transparent', + 'disabled:data-[state=checked]:tw-bg-controls-base-inactive disabled:data-[state=checked]:tw-border-transparent', + 'disabled:data-[state=indeterminate]:tw-bg-controls-base-inactive disabled:data-[state=indeterminate]:tw-border-transparent', + 'disabled:tw-text-icon-default', + // Focus ring + 'focus-visible:tw-ring-2 focus-visible:tw-ring-interactive-focus-outline focus-visible:tw-ring-offset-2', + ], + { + variants: { + size: { + large: 'tw-size-5 tw-rounded-[7px]', + default: 'tw-size-4 tw-rounded-[5px]', + }, + }, + defaultVariants: { size: 'default' }, + } +); + +const checkIconSizeClasses = { + large: 'tw-size-3', + default: 'tw-size-2.5', +}; + +const Checkbox = forwardRef(function Checkbox({ className, size = 'default', ...props }, ref) { + return ( + + + {/* Both icons rendered; CSS shows the right one based on data-state */} + + + + + ); +}); +Checkbox.displayName = 'Checkbox'; +Checkbox.propTypes = { + size: PropTypes.oneOf(['large', 'default']), + checked: PropTypes.oneOfType([PropTypes.bool, PropTypes.oneOf(['indeterminate'])]), + disabled: PropTypes.bool, + className: PropTypes.string, +}; + +export { Checkbox, checkboxVariants }; diff --git a/frontend/src/components/ui/Rocket/Checkbox/Checkbox.spec.md b/frontend/src/components/ui/Rocket/Checkbox/Checkbox.spec.md new file mode 100644 index 0000000000..e640eb9fb9 --- /dev/null +++ b/frontend/src/components/ui/Rocket/Checkbox/Checkbox.spec.md @@ -0,0 +1,56 @@ +# Checkbox — Rocket Design Spec + + + +## Overview + +Binary input control. Wraps Radix Checkbox via shadcn. Supports `checked`, `indeterminate`, and `disabled` states. Sized via context-free `size` prop. + +Label is **not** baked in — consumers compose `
+ ), +}; + // ── No Padding ─────────────────────────────────────────────────────────────── export const NoPadding = { diff --git a/frontend/src/components/ui/Rocket/RadioGroup/RadioGroup.jsx b/frontend/src/components/ui/Rocket/RadioGroup/RadioGroup.jsx new file mode 100644 index 0000000000..32af8c54c5 --- /dev/null +++ b/frontend/src/components/ui/Rocket/RadioGroup/RadioGroup.jsx @@ -0,0 +1,84 @@ +import React, { forwardRef } from 'react'; +import PropTypes from 'prop-types'; +import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'; +import { cva } from 'class-variance-authority'; +import { cn } from '@/lib/utils'; + +// ── RadioGroup ────────────────────────────────────────────────────────────── + +const RadioGroup = forwardRef(function RadioGroup({ className, ...props }, ref) { + return ( + + ); +}); +RadioGroup.displayName = 'RadioGroup'; +RadioGroup.propTypes = { + className: PropTypes.string, +}; + +// ── RadioGroupItem ────────────────────────────────────────────────────────── + +const radioGroupItemVariants = cva( + [ + // Layout + 'tw-relative tw-flex tw-shrink-0 tw-items-center tw-justify-center tw-rounded-full', + // Reset + 'tw-appearance-none tw-outline-none', + // Default border + bg + 'tw-border-solid tw-border tw-border-border-default', + 'tw-bg-background-surface-layer-01', + 'tw-transition-colors', + // Checked + 'data-[state=checked]:tw-bg-button-primary data-[state=checked]:tw-border-button-primary', + // Disabled + 'disabled:tw-cursor-not-allowed', + 'disabled:tw-bg-controls-base-inactive disabled:tw-border-transparent', + 'disabled:data-[state=checked]:tw-bg-controls-base-inactive disabled:data-[state=checked]:tw-border-transparent', + // Focus ring + 'focus-visible:tw-ring-2 focus-visible:tw-ring-interactive-focus-outline focus-visible:tw-ring-offset-2 focus-visible:tw-ring-offset-background', + ], + { + variants: { + size: { + large: 'tw-size-5', + default: 'tw-size-4', + }, + }, + defaultVariants: { size: 'default' }, + } +); + +const innerDotSizeClasses = { + large: 'tw-size-2', + default: 'tw-size-1.5', +}; + +const RadioGroupItem = forwardRef(function RadioGroupItem({ className, size = 'default', ...props }, ref) { + return ( + + + + + + ); +}); +RadioGroupItem.displayName = 'RadioGroupItem'; +RadioGroupItem.propTypes = { + size: PropTypes.oneOf(['large', 'default']), + className: PropTypes.string, +}; + +export { RadioGroup, RadioGroupItem, radioGroupItemVariants }; diff --git a/frontend/src/components/ui/Rocket/RadioGroup/RadioGroup.spec.md b/frontend/src/components/ui/Rocket/RadioGroup/RadioGroup.spec.md new file mode 100644 index 0000000000..0c3ef98308 --- /dev/null +++ b/frontend/src/components/ui/Rocket/RadioGroup/RadioGroup.spec.md @@ -0,0 +1,70 @@ +# RadioGroup — Rocket Design Spec + + + +## Overview + +Single-selection group of radio inputs. Wraps Radix RadioGroup via shadcn. The visual design mirrors Checkbox but uses circular shapes — same sizes, same color tokens, same focus/disabled states. + +Two sub-components: +- `RadioGroup` — group container, manages selection state +- `RadioGroupItem` — individual radio circle + +Label is **not** baked in — consumers compose `