mirror of
https://github.com/ToolJet/ToolJet
synced 2026-04-21 21:47:17 +00:00
feat(rocket): add AlertDialog, Collapsible, Sheet, Sonner + Dialog overflow border (#15854)
* feat(rocket/alert-dialog): implement AlertDialog component with variants and documentation * feat(rocket/collapsible): add Rocket Collapsible component Bordered and ghost variants with animated expand/collapse via CSS grid-rows. Includes styled trigger with auto-rotating chevron, variant context, and Storybook stories. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(storybook): fix quote style and centered layout sizing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(rocket/toaster): implement Sonner toast notification system with documentation and stories * feat(rocket/sheet): add Rocket Sheet component Right-side slide-in panel for multi-step forms (e.g. add datasource). Three sizes (small/default/large), header/body/footer structure mirroring Dialog, conditional footer overflow border via ResizeObserver context, preventClose support. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(rocket/table): add Rocket Table primitive Low-level table primitive with 8 sub-components (Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption). Density variants (default 52px / compact 36px) via TableDensityContext. Borderless rows with rounded pill hover/selected highlights via first/last cell rounded corners. Header bottom border on cells (border-separate mode). All ToolJet tokens. Brought forward from PR #14498 with improved organization and Figma-aligned defaults from the apps list design. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(rocket): add Skeleton primitive + DataTable block - Skeleton: Rocket primitive with pulsing bg-interactive-hover token - TableSkeleton: TanStack-agnostic skeleton rows for use inside <Table> - DataTable: TanStack-driven table block (header, body, loading, empty states) brought from PR #14498 with ToolJet token cleanup and proper file structure Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(rocket): add Checkbox and RadioGroup components with specifications and stories * chore: update dependencies and remove unused packages - Removed @base-ui/react and cmdk from dependencies. - Updated various @radix-ui packages to lower versions for compatibility. - Adjusted versions for @floating-ui packages. - Cleaned up package-lock.json by removing unnecessary entries. * feat(rocket): add Spinner and Textarea components with stories and update AlertDialog and Combobox for new props --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4611b51d2c
commit
4e07c24060
50 changed files with 3768 additions and 20 deletions
|
|
@ -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],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
212
frontend/src/components/ui/Rocket/AlertDialog/AlertDialog.jsx
Normal file
212
frontend/src/components/ui/Rocket/AlertDialog/AlertDialog.jsx
Normal file
|
|
@ -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 (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
data-slot="alert-dialog-overlay"
|
||||
className={cn(
|
||||
'tw-fixed tw-inset-0 tw-z-50 tw-bg-black/20',
|
||||
'data-[state=open]:tw-animate-in data-[state=closed]:tw-animate-out data-[state=closed]:tw-fade-out-0 data-[state=open]:tw-fade-in-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
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 (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
ref={ref}
|
||||
data-slot="alert-dialog-content"
|
||||
data-size={size || 'default'}
|
||||
onInteractOutside={handleInteractOutside}
|
||||
onEscapeKeyDown={handleEscapeKeyDown}
|
||||
className={cn(
|
||||
'tw-group/alert-dialog-content tw-fixed tw-inset-0 tw-z-50 tw-m-auto tw-h-fit tw-w-full tw-max-w-[calc(100%-2rem)] tw-outline-none',
|
||||
'data-[state=open]:tw-animate-in data-[state=closed]:tw-animate-out data-[state=closed]:tw-fade-out-0 data-[state=open]:tw-fade-in-0 data-[state=closed]:tw-zoom-out-95 data-[state=open]:tw-zoom-in-95',
|
||||
alertDialogContentVariants({ size }),
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</AlertDialogPrimitive.Content>
|
||||
</AlertDialogPortal>
|
||||
);
|
||||
});
|
||||
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 (
|
||||
<div
|
||||
data-slot="alert-dialog-media"
|
||||
className={cn('tw-flex tw-items-center tw-justify-center tw-size-10 tw-shrink-0', className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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 (
|
||||
<div
|
||||
data-slot="alert-dialog-header"
|
||||
className={cn(
|
||||
'tw-flex tw-flex-col tw-gap-0.5',
|
||||
// 8px total between media and title: 6px margin + 2px gap
|
||||
'[&>[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}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
AlertDialogHeader.displayName = 'AlertDialogHeader';
|
||||
|
||||
// ── AlertDialogTitle ────────────────────────────────────────────────────────
|
||||
|
||||
const AlertDialogTitle = forwardRef(function AlertDialogTitle({ className, ...props }, ref) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Title
|
||||
ref={ref}
|
||||
data-slot="alert-dialog-title"
|
||||
className={cn('tw-font-title-x-large tw-text-text-default tw-mb-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
AlertDialogTitle.displayName = 'AlertDialogTitle';
|
||||
|
||||
// ── AlertDialogDescription ──────────────────────────────────────────────────
|
||||
|
||||
const AlertDialogDescription = forwardRef(function AlertDialogDescription({ className, ...props }, ref) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Description
|
||||
ref={ref}
|
||||
data-slot="alert-dialog-description"
|
||||
className={cn('tw-font-body-default tw-text-text-default tw-m-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
AlertDialogDescription.displayName = 'AlertDialogDescription';
|
||||
|
||||
// ── AlertDialogFooter ───────────────────────────────────────────────────────
|
||||
|
||||
function AlertDialogFooter({ className, ...props }) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-footer"
|
||||
className={cn('tw-flex tw-items-center tw-justify-between tw-gap-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
AlertDialogFooter.displayName = 'AlertDialogFooter';
|
||||
|
||||
// ── AlertDialogAction ───────────────────────────────────────────────────────
|
||||
|
||||
const AlertDialogAction = forwardRef(function AlertDialogAction({ className, ...props }, ref) {
|
||||
return <AlertDialogPrimitive.Action ref={ref} data-slot="alert-dialog-action" className={cn(className)} {...props} />;
|
||||
});
|
||||
AlertDialogAction.displayName = 'AlertDialogAction';
|
||||
|
||||
// ── AlertDialogCancel ───────────────────────────────────────────────────────
|
||||
|
||||
const AlertDialogCancel = forwardRef(function AlertDialogCancel({ className, ...props }, ref) {
|
||||
return <AlertDialogPrimitive.Cancel ref={ref} data-slot="alert-dialog-cancel" className={cn(className)} {...props} />;
|
||||
});
|
||||
AlertDialogCancel.displayName = 'AlertDialogCancel';
|
||||
|
||||
// ── Exports ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
alertDialogContentVariants,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogMedia,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
};
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
# AlertDialog — Rocket Design Spec
|
||||
<!-- figma: https://www.figma.com/design/XQdM9x8x9YHcj1lUK8abUG/Rocket-components?node-id=8996-1761 -->
|
||||
<!-- synced: 2026-04-01 -->
|
||||
|
||||
## 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
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="primary" danger>Delete</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogMedia><WarningIcon /></AlertDialogMedia>
|
||||
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>This action cannot be undone.</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction asChild>
|
||||
<Button variant="primary" danger>Delete</Button>
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
|
@ -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: () => (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="primary">Open Alert</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogMedia>
|
||||
<TriangleAlert className="tw-size-10 tw-text-icon-brand" />
|
||||
</AlertDialogMedia>
|
||||
<AlertDialogTitle>Lorem ipsum dolor sit amet.</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer elementum mattis arcu, non vulputate est
|
||||
ornare vitae.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</AlertDialogCancel>
|
||||
<div className="tw-flex tw-gap-2">
|
||||
<Button variant="secondary">Secondary action</Button>
|
||||
<AlertDialogAction asChild>
|
||||
<Button variant="primary">Primary action</Button>
|
||||
</AlertDialogAction>
|
||||
</div>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
),
|
||||
};
|
||||
|
||||
// ── Danger ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export const Danger = {
|
||||
render: () => (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="primary" danger>
|
||||
Delete Item
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogMedia>
|
||||
<TriangleAlert className="tw-size-10 tw-text-icon-danger" />
|
||||
</AlertDialogMedia>
|
||||
<AlertDialogTitle>Are you sure you want to delete?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete the item and all associated data.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</AlertDialogCancel>
|
||||
<div className="tw-flex tw-gap-2">
|
||||
<Button variant="secondary" danger>
|
||||
Secondary action
|
||||
</Button>
|
||||
<AlertDialogAction asChild>
|
||||
<Button variant="primary" danger>
|
||||
Delete
|
||||
</Button>
|
||||
</AlertDialogAction>
|
||||
</div>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
),
|
||||
};
|
||||
|
||||
// ── Simple (Cancel + Action only) ───────────────────────────────────────────
|
||||
|
||||
export const Simple = {
|
||||
render: () => (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline">Discard Changes</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogMedia>
|
||||
<TriangleAlert className="tw-size-10 tw-text-icon-brand" />
|
||||
</AlertDialogMedia>
|
||||
<AlertDialogTitle>Discard unsaved changes?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
You have unsaved changes that will be lost if you leave this page.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction asChild>
|
||||
<Button variant="primary">Discard</Button>
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
),
|
||||
};
|
||||
|
||||
// ── Small ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export const Small = {
|
||||
render: () => (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline">Small Alert</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent size="small">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogMedia>
|
||||
<TriangleAlert className="tw-size-10 tw-text-icon-brand" />
|
||||
</AlertDialogMedia>
|
||||
<AlertDialogTitle>Delete this item?</AlertDialogTitle>
|
||||
<AlertDialogDescription>This action cannot be undone.</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction asChild>
|
||||
<Button variant="primary" danger>
|
||||
Delete
|
||||
</Button>
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
),
|
||||
};
|
||||
|
||||
// ── Without Media ───────────────────────────────────────────────────────────
|
||||
|
||||
export const WithoutMedia = {
|
||||
render: () => (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline">Confirm Action</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Confirm this action?</AlertDialogTitle>
|
||||
<AlertDialogDescription>Please confirm you want to proceed with this operation.</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction asChild>
|
||||
<Button variant="primary">Confirm</Button>
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
),
|
||||
};
|
||||
90
frontend/src/components/ui/Rocket/Checkbox/Checkbox.jsx
Normal file
90
frontend/src/components/ui/Rocket/Checkbox/Checkbox.jsx
Normal file
|
|
@ -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 (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
data-slot="checkbox"
|
||||
className={cn(checkboxVariants({ size }), className)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="tw-grid tw-place-content-center"
|
||||
forceMount
|
||||
>
|
||||
{/* Both icons rendered; CSS shows the right one based on data-state */}
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
checkIconSizeClasses[size],
|
||||
'tw-stroke-[3]',
|
||||
'group-data-[state=indeterminate]:tw-hidden group-data-[state=unchecked]:tw-hidden'
|
||||
)}
|
||||
/>
|
||||
<MinusIcon
|
||||
className={cn(
|
||||
checkIconSizeClasses[size],
|
||||
'tw-stroke-[3]',
|
||||
'group-data-[state=checked]:tw-hidden group-data-[state=unchecked]:tw-hidden'
|
||||
)}
|
||||
/>
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
);
|
||||
});
|
||||
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 };
|
||||
56
frontend/src/components/ui/Rocket/Checkbox/Checkbox.spec.md
Normal file
56
frontend/src/components/ui/Rocket/Checkbox/Checkbox.spec.md
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# Checkbox — Rocket Design Spec
|
||||
<!-- figma: https://www.figma.com/design/XQdM9x8x9YHcj1lUK8abUG/Rocket-components?node-id=93-52292 -->
|
||||
<!-- synced: 2026-04-07 -->
|
||||
|
||||
## 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 `<label>` + `<Checkbox>` themselves.
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Values | Default |
|
||||
|---|---|---|---|
|
||||
| size | string | large \| default | default |
|
||||
| checked | boolean \| 'indeterminate' | — | false |
|
||||
| disabled | boolean | — | false |
|
||||
| className | string | — | — |
|
||||
|
||||
Plus standard Radix Checkbox props (`onCheckedChange`, `name`, `value`, `required`, `defaultChecked`).
|
||||
|
||||
## Sizes
|
||||
|
||||
| Value | Box | Border radius | Check icon |
|
||||
|---|---|---|---|
|
||||
| default | 16px (`tw-size-4`) | 5px (`tw-rounded-[5px]`) | 10px |
|
||||
| large | 20px (`tw-size-5`) | 7px (`tw-rounded-[7px]`) | 12px |
|
||||
|
||||
## Token Mapping
|
||||
|
||||
| Element | State | Figma token | ToolJet class |
|
||||
|---|---|---|---|
|
||||
| bg | unchecked | `--bg-surface-layer-01` | `tw-bg-background-surface-layer-01` |
|
||||
| border | unchecked | `border/default` | `tw-border-solid tw-border tw-border-border-default` |
|
||||
| bg | checked / indeterminate | `button/primary` | `data-[state=checked]:tw-bg-button-primary data-[state=indeterminate]:tw-bg-button-primary` |
|
||||
| border | checked / indeterminate | `button/primary` | `data-[state=checked]:tw-border-button-primary data-[state=indeterminate]:tw-border-button-primary` |
|
||||
| icon (check / minus) | checked / indeterminate | `icon/on-solid` | `tw-text-text-on-solid` |
|
||||
| bg | disabled | `controls/base-inactive` | `disabled:tw-bg-controls-base-inactive disabled:tw-border-transparent` |
|
||||
| icon | disabled | (muted) | `disabled:tw-text-icon-default` |
|
||||
| focus ring | focused | `interactive/focusActive` | `focus-visible:tw-ring-2 focus-visible:tw-ring-interactive-focus-outline focus-visible:tw-ring-offset-1` |
|
||||
| cursor | disabled | — | `disabled:tw-cursor-not-allowed` |
|
||||
| opacity | disabled | — | `disabled:tw-opacity-50` (only on icon — bg uses its own disabled token) |
|
||||
|
||||
## CVA Shape
|
||||
|
||||
Shape C — sizes only (no variants).
|
||||
|
||||
## Notes
|
||||
|
||||
- Wraps Radix `Checkbox.Root` + `Checkbox.Indicator` via shadcn.
|
||||
- **Indeterminate** state set via `checked="indeterminate"` (Radix native). Renders a `MinusIcon` instead of `CheckIcon`. Determined inside the `Indicator` via `data-state`.
|
||||
- Focus ring uses `tw-ring-offset-1` so the ring sits just outside the box border (matches Figma's 2px outer ring).
|
||||
- Border radius is intentionally per-size — `5px` for default and `7px` for large match Figma exactly (not standard `tw-rounded-md`).
|
||||
- Disabled state: bg becomes `controls-base-inactive` (subtle gray), border removed (transparent), icon muted. Both checked and unchecked disabled states use the same bg.
|
||||
- Consumer composes the label: `<label className="tw-flex tw-items-center tw-gap-2"><Checkbox /> <span>Label</span></label>`.
|
||||
- Typography (for the label text) is the consumer's responsibility — `tw-font-body-default` is the typical choice.
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import React from 'react';
|
||||
import { Checkbox } from './Checkbox';
|
||||
|
||||
export default {
|
||||
title: 'Rocket/Checkbox',
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'centered' },
|
||||
};
|
||||
|
||||
// ── Default ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export const Default = {
|
||||
render: () => <Checkbox />,
|
||||
};
|
||||
|
||||
export const Checked = {
|
||||
render: () => <Checkbox defaultChecked />,
|
||||
};
|
||||
|
||||
export const Indeterminate = {
|
||||
render: () => <Checkbox checked="indeterminate" />,
|
||||
};
|
||||
|
||||
export const Disabled = {
|
||||
render: () => (
|
||||
<div className="tw-flex tw-gap-3">
|
||||
<Checkbox disabled />
|
||||
<Checkbox disabled defaultChecked />
|
||||
<Checkbox disabled checked="indeterminate" />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
// ── Sizes ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export const Sizes = {
|
||||
render: () => (
|
||||
<div className="tw-flex tw-items-center tw-gap-4">
|
||||
<Checkbox size="default" defaultChecked />
|
||||
<Checkbox size="large" defaultChecked />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
// ── With Label ──────────────────────────────────────────────────────────────
|
||||
|
||||
export const WithLabel = {
|
||||
render: () => (
|
||||
<label className="tw-flex tw-items-center tw-gap-2 tw-cursor-pointer tw-font-body-default tw-text-text-default">
|
||||
<Checkbox />
|
||||
<span>Accept terms and conditions</span>
|
||||
</label>
|
||||
),
|
||||
};
|
||||
|
||||
// ── All States ──────────────────────────────────────────────────────────────
|
||||
|
||||
export const AllStates = {
|
||||
render: () => (
|
||||
<div className="tw-flex tw-flex-col tw-gap-4">
|
||||
<div className="tw-flex tw-items-center tw-gap-3">
|
||||
<span className="tw-w-32 tw-font-body-default tw-text-text-default">Unchecked</span>
|
||||
<Checkbox />
|
||||
<Checkbox size="large" />
|
||||
</div>
|
||||
<div className="tw-flex tw-items-center tw-gap-3">
|
||||
<span className="tw-w-32 tw-font-body-default tw-text-text-default">Checked</span>
|
||||
<Checkbox defaultChecked />
|
||||
<Checkbox size="large" defaultChecked />
|
||||
</div>
|
||||
<div className="tw-flex tw-items-center tw-gap-3">
|
||||
<span className="tw-w-32 tw-font-body-default tw-text-text-default">Indeterminate</span>
|
||||
<Checkbox checked="indeterminate" />
|
||||
<Checkbox size="large" checked="indeterminate" />
|
||||
</div>
|
||||
<div className="tw-flex tw-items-center tw-gap-3">
|
||||
<span className="tw-w-32 tw-font-body-default tw-text-text-default">Disabled</span>
|
||||
<Checkbox disabled />
|
||||
<Checkbox size="large" disabled />
|
||||
</div>
|
||||
<div className="tw-flex tw-items-center tw-gap-3">
|
||||
<span className="tw-w-32 tw-font-body-default tw-text-text-default">Disabled checked</span>
|
||||
<Checkbox disabled defaultChecked />
|
||||
<Checkbox size="large" disabled defaultChecked />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
159
frontend/src/components/ui/Rocket/Collapsible/Collapsible.jsx
Normal file
159
frontend/src/components/ui/Rocket/Collapsible/Collapsible.jsx
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
import React, { createContext, forwardRef, useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';
|
||||
import { cva } from 'class-variance-authority';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
// ── Variant context ─────────────────────────────────────────────────────────
|
||||
|
||||
const CollapsibleVariantContext = createContext('bordered');
|
||||
|
||||
// ── Collapsible (root) ──────────────────────────────────────────────────────
|
||||
|
||||
const collapsibleVariants = cva('', {
|
||||
variants: {
|
||||
variant: {
|
||||
bordered: 'tw-border-solid tw-border tw-border-border-weak tw-rounded-lg tw-bg-background-surface-layer-01',
|
||||
ghost: '',
|
||||
},
|
||||
},
|
||||
defaultVariants: { variant: 'bordered' },
|
||||
});
|
||||
|
||||
const Collapsible = forwardRef(function Collapsible({ className, variant = 'bordered', ...props }, ref) {
|
||||
return (
|
||||
<CollapsibleVariantContext.Provider value={variant}>
|
||||
<CollapsiblePrimitive.Root
|
||||
ref={ref}
|
||||
data-slot="collapsible"
|
||||
data-variant={variant}
|
||||
className={cn(collapsibleVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
</CollapsibleVariantContext.Provider>
|
||||
);
|
||||
});
|
||||
Collapsible.displayName = 'Collapsible';
|
||||
Collapsible.propTypes = {
|
||||
variant: PropTypes.oneOf(['bordered', 'ghost']),
|
||||
defaultOpen: PropTypes.bool,
|
||||
open: PropTypes.bool,
|
||||
onOpenChange: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
// ── CollapsibleTrigger ──────────────────────────────────────────────────────
|
||||
|
||||
const collapsibleTriggerVariants = cva(
|
||||
[
|
||||
// Reset (preflight is off)
|
||||
'tw-appearance-none tw-border-0 tw-bg-transparent tw-outline-none',
|
||||
'tw-group tw-flex tw-w-full tw-items-center tw-justify-between tw-cursor-pointer',
|
||||
'tw-font-title-default tw-text-text-default',
|
||||
'hover:tw-bg-interactive-hover tw-transition-colors',
|
||||
],
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
bordered:
|
||||
'tw-px-4 tw-py-3 tw-rounded-t-md tw-border-solid tw-border-0 data-[state=open]:tw-border-b data-[state=open]:tw-border-border-weak data-[state=closed]:tw-rounded-b-md data-[state=closed]:tw-border-transparent',
|
||||
ghost: 'tw-py-2 tw-px-2 tw-rounded-md',
|
||||
},
|
||||
},
|
||||
defaultVariants: { variant: 'bordered' },
|
||||
}
|
||||
);
|
||||
|
||||
const CollapsibleTrigger = forwardRef(function CollapsibleTrigger({ className, children, ...props }, ref) {
|
||||
const variant = useContext(CollapsibleVariantContext);
|
||||
|
||||
return (
|
||||
<CollapsiblePrimitive.Trigger
|
||||
ref={ref}
|
||||
data-slot="collapsible-trigger"
|
||||
className={cn(collapsibleTriggerVariants({ variant }), className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<CollapsibleIcon />
|
||||
</CollapsiblePrimitive.Trigger>
|
||||
);
|
||||
});
|
||||
CollapsibleTrigger.displayName = 'CollapsibleTrigger';
|
||||
CollapsibleTrigger.propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
// ── CollapsibleIcon ─────────────────────────────────────────────────────────
|
||||
// Auto-rotating chevron. Reads data-state from the nearest Radix Collapsible root.
|
||||
|
||||
function CollapsibleIcon({ className, ...props }) {
|
||||
return (
|
||||
<ChevronDown
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'tw-size-4 tw-text-icon-default tw-shrink-0',
|
||||
'tw-transition-transform tw-duration-200',
|
||||
// Radix sets data-state on the trigger — parent selector
|
||||
'group-data-[state=open]:tw-rotate-180',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
CollapsibleIcon.displayName = 'CollapsibleIcon';
|
||||
|
||||
// ── CollapsibleContent ──────────────────────────────────────────────────────
|
||||
|
||||
const collapsibleContentVariants = cva(
|
||||
[
|
||||
'tw-grid tw-transition-[grid-template-rows,padding] tw-duration-200 tw-ease-in-out',
|
||||
'data-[state=closed]:tw-grid-rows-[0fr] data-[state=closed]:!tw-p-0',
|
||||
'data-[state=open]:tw-grid-rows-[1fr]',
|
||||
],
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
bordered: 'tw-px-4 tw-py-3',
|
||||
ghost: 'tw-py-2',
|
||||
},
|
||||
},
|
||||
defaultVariants: { variant: 'bordered' },
|
||||
}
|
||||
);
|
||||
|
||||
const CollapsibleContent = forwardRef(function CollapsibleContent({ className, children, ...props }, ref) {
|
||||
const variant = useContext(CollapsibleVariantContext);
|
||||
|
||||
return (
|
||||
<CollapsiblePrimitive.Content
|
||||
ref={ref}
|
||||
forceMount
|
||||
data-slot="collapsible-content"
|
||||
className={cn(collapsibleContentVariants({ variant }), className)}
|
||||
{...props}
|
||||
>
|
||||
<div className="tw-overflow-hidden tw-min-h-0">{children}</div>
|
||||
</CollapsiblePrimitive.Content>
|
||||
);
|
||||
});
|
||||
CollapsibleContent.displayName = 'CollapsibleContent';
|
||||
CollapsibleContent.propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
// ── Exports ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export {
|
||||
Collapsible,
|
||||
collapsibleVariants,
|
||||
CollapsibleTrigger,
|
||||
collapsibleTriggerVariants,
|
||||
CollapsibleIcon,
|
||||
CollapsibleContent,
|
||||
collapsibleContentVariants,
|
||||
};
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
# Collapsible — Rocket Design Spec
|
||||
<!-- figma: https://www.figma.com/design/lOW96V8fTBx9J6yolOLhdC/01-Dashboards---Applications?node-id=4216-38603 -->
|
||||
<!-- synced: 2026-04-01 -->
|
||||
|
||||
## Overview
|
||||
|
||||
Collapsible is a disclosure component — a trigger row that expands/collapses hidden content. Used inside modals, settings panels, and detail views to group secondary information.
|
||||
|
||||
Wraps Radix Collapsible primitives via shadcn.
|
||||
|
||||
## Props (Collapsible — root)
|
||||
|
||||
| Prop | Type | Values | Default |
|
||||
|---|---|---|---|
|
||||
| variant | string | bordered \| ghost | bordered |
|
||||
| defaultOpen | boolean | — | false |
|
||||
| open | boolean | — | — |
|
||||
| onOpenChange | function | — | — |
|
||||
|
||||
## Sub-components
|
||||
|
||||
| Component | Wraps | Styling |
|
||||
|---|---|---|
|
||||
| `Collapsible` | Radix Collapsible.Root | variant CVA (bordered vs ghost) |
|
||||
| `CollapsibleTrigger` | Radix Trigger (re-export) | none — consumer styles the trigger content |
|
||||
| `CollapsibleContent` | Radix Content | padding, animated height |
|
||||
|
||||
## Token Mapping
|
||||
|
||||
| Element | Variant | State | ToolJet class |
|
||||
|---|---|---|---|
|
||||
| root container | bordered | default | `tw-border-solid tw-border tw-border-border-weak tw-rounded-lg` |
|
||||
| root container | ghost | default | no border, no bg |
|
||||
| trigger row | all | default | `tw-flex tw-items-center tw-justify-between tw-w-full tw-cursor-pointer` |
|
||||
| trigger row | bordered | default | `tw-px-4 tw-py-3` |
|
||||
| trigger row | ghost | default | `tw-py-2` |
|
||||
| trigger text | all | default | `tw-font-title-default tw-text-text-default` |
|
||||
| trigger chevron | all | default | `tw-text-icon-default tw-size-4` |
|
||||
| trigger chevron | all | open | `tw-rotate-180` (via data-[state=open]) |
|
||||
| content area | bordered | default | `tw-px-4 tw-pb-3` |
|
||||
| content area | ghost | default | `tw-py-2` |
|
||||
| content text | all | default | `tw-font-body-default tw-text-text-default` |
|
||||
|
||||
## Slots
|
||||
|
||||
- trigger (required, `ReactNode`) — the clickable header content (label, count badge, etc.)
|
||||
- children (required, `ReactNode`) — the collapsible content area
|
||||
|
||||
## CVA Shape
|
||||
|
||||
Shape B — variants only (`bordered` | `ghost`). No sizes.
|
||||
|
||||
## Usage Pattern
|
||||
|
||||
```jsx
|
||||
<Collapsible variant="bordered">
|
||||
<CollapsibleTrigger>
|
||||
<span>Missing user groups (5)</span>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<p>Group 1, Group 2, Group 3, Group 4, Group 5</p>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Radix handles open/close state, keyboard (Enter/Space), and aria-expanded automatically.
|
||||
- The chevron rotation is handled via `data-[state=open]:tw-rotate-180` on the icon inside the trigger. Since the trigger is consumer-composed, Rocket provides a `CollapsibleIcon` helper that auto-rotates.
|
||||
- Animated height transition on CollapsibleContent via `tw-overflow-hidden` + CSS `grid-template-rows` animation (same pattern as shadcn accordion).
|
||||
- Typography uses `tw-font-*` plugin utilities.
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
import React from 'react';
|
||||
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from './Collapsible';
|
||||
|
||||
export default {
|
||||
title: 'Rocket/Collapsible',
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'centered' },
|
||||
};
|
||||
|
||||
// ── Bordered (default) ──────────────────────────────────────────────────────
|
||||
|
||||
export const Bordered = {
|
||||
render: () => (
|
||||
<div className="tw-w-[400px]">
|
||||
<Collapsible>
|
||||
<CollapsibleTrigger>
|
||||
<span>Missing user groups (5)</span>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<p className="tw-font-body-default tw-text-text-default">Group 1, Group 2, Group 3, Group 4, Group 5</p>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
// ── Ghost ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export const Ghost = {
|
||||
render: () => (
|
||||
<div className="tw-w-[400px]">
|
||||
<Collapsible variant="ghost">
|
||||
<CollapsibleTrigger>
|
||||
<span>Advanced settings</span>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<p className="tw-font-body-default tw-text-text-default">Additional configuration options would go here.</p>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
// ── Default Open ────────────────────────────────────────────────────────────
|
||||
|
||||
export const DefaultOpen = {
|
||||
render: () => (
|
||||
<div className="tw-w-[400px]">
|
||||
<Collapsible defaultOpen>
|
||||
<CollapsibleTrigger>
|
||||
<span>Missing user groups (5)</span>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<p className="tw-font-body-default tw-text-text-default">Group 1, Group 2, Group 3, Group 4, Group 5</p>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
// ── Multiple ────────────────────────────────────────────────────────────────
|
||||
|
||||
export const Multiple = {
|
||||
render: () => (
|
||||
<div className="tw-w-[400px] tw-flex tw-flex-col tw-gap-2">
|
||||
<Collapsible>
|
||||
<CollapsibleTrigger>
|
||||
<span>Section 1</span>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<p className="tw-font-body-default tw-text-text-default">Content for section 1.</p>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
<Collapsible>
|
||||
<CollapsibleTrigger>
|
||||
<span>Section 2</span>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<p className="tw-font-body-default tw-text-text-default">Content for section 2.</p>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
|
@ -139,14 +139,18 @@ ComboboxInput.propTypes = {
|
|||
|
||||
// ── ComboboxContent ───────────────────────────────────────────────────────
|
||||
|
||||
const ComboboxContent = forwardRef(function ComboboxContent({ className, ...props }, ref) {
|
||||
const ComboboxContent = forwardRef(function ComboboxContent({ anchor, className, ...props }, ref) {
|
||||
const anchorRef = useContext(ComboboxAnchorContext);
|
||||
// Use custom anchor prop if provided. Default to the context ref (full-width input wrapper).
|
||||
// Pass `anchor={false}` to skip the custom anchor and let Base UI anchor to the trigger element
|
||||
// (useful when using ComboboxTrigger with a custom render button).
|
||||
const resolvedAnchor = anchor !== undefined ? anchor || undefined : anchorRef;
|
||||
return (
|
||||
<ShadcnComboboxContent
|
||||
ref={ref}
|
||||
anchor={anchorRef}
|
||||
anchor={resolvedAnchor}
|
||||
className={cn(
|
||||
'tw-ring-interactive-weak tw-bg-background-surface-layer-01 tw-rounded-[10px] tw-shadow-elevation-300 tw-p-2',
|
||||
'tw-ring-interactive-weak tw-bg-background-surface-layer-01 tw-rounded-lg tw-shadow-elevation-300 tw-p-2',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxInput,
|
||||
ComboboxTrigger,
|
||||
ComboboxValue,
|
||||
ComboboxContent,
|
||||
ComboboxList,
|
||||
ComboboxItem,
|
||||
|
|
@ -11,6 +14,7 @@ import {
|
|||
ComboboxSeparator,
|
||||
} from './Combobox';
|
||||
import { Field, FieldLabel, FieldDescription, FieldError } from '../Field/Field';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
|
||||
const frameworks = ['React', 'Vue', 'Angular', 'Svelte', 'Solid', 'Qwik'];
|
||||
|
||||
|
|
@ -321,6 +325,57 @@ export const EmptyState = {
|
|||
parameters: { layout: 'padded' },
|
||||
};
|
||||
|
||||
// ── Custom Trigger ───────────────────────────────────────────────────────
|
||||
// Button-style trigger (e.g. workspace selector) instead of the default input.
|
||||
export const CustomTrigger = {
|
||||
render: () => {
|
||||
const workspaces = [
|
||||
{ label: 'ABC cargo main team', value: 'abc-cargo-main-team' },
|
||||
{ label: 'Design system', value: 'design-system' },
|
||||
{ label: 'Backend API', value: 'backend-api' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="tw-w-72 tw-p-4">
|
||||
<Combobox value={workspaces[0]} items={workspaces}>
|
||||
<ComboboxTrigger
|
||||
render={
|
||||
<button
|
||||
className={cn(
|
||||
'tw-flex tw-items-center tw-gap-1.5 tw-px-2 tw-py-1.5',
|
||||
'tw-font-title-default tw-text-text-default tw-max-w-48',
|
||||
'tw-rounded-md tw-border-0 tw-bg-transparent',
|
||||
'hover:tw-bg-interactive-default',
|
||||
'data-[pressed]:tw-bg-interactive-selected',
|
||||
'data-[popup-open]:tw-bg-interactive-selected'
|
||||
)}
|
||||
>
|
||||
<ComboboxValue>
|
||||
{(value) => <span className="tw-flex-1 tw-truncate">{value?.label ?? ''}</span>}
|
||||
</ComboboxValue>
|
||||
<ChevronDown className="tw-size-4 tw-text-icon-default tw-shrink-0" />
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
|
||||
<ComboboxContent anchor={false} align="start" className="tw-flex tw-flex-col tw-gap-2">
|
||||
<ComboboxInput showTrigger={false} placeholder="Search workspace..." />
|
||||
<ComboboxList>
|
||||
{(item) => (
|
||||
<ComboboxItem key={item.value} value={item}>
|
||||
{item.label}
|
||||
</ComboboxItem>
|
||||
)}
|
||||
</ComboboxList>
|
||||
<ComboboxEmpty>No workspaces found.</ComboboxEmpty>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
parameters: { layout: 'padded' },
|
||||
};
|
||||
|
||||
// ── All States ────────────────────────────────────────────────────────────
|
||||
export const AllStates = {
|
||||
render: () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { forwardRef } from 'react';
|
||||
import React, { createContext, forwardRef, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { cva } from 'class-variance-authority';
|
||||
|
|
@ -13,6 +13,10 @@ import {
|
|||
DialogPortal,
|
||||
} from '@/components/ui/Rocket/shadcn/dialog';
|
||||
|
||||
// ── Overflow context (DialogBody → DialogFooter) ────────────────────────────
|
||||
|
||||
const DialogOverflowContext = createContext({ overflowing: false, setOverflowing: () => {} });
|
||||
|
||||
// ── DialogOverlay ────────────────────────────────────────────────────────────
|
||||
|
||||
const DialogOverlay = forwardRef(function DialogOverlay({ className, ...props }, ref) {
|
||||
|
|
@ -61,6 +65,12 @@ const DialogContent = forwardRef(function DialogContent(
|
|||
{ className, children, size, showCloseButton = true, preventClose = false, ...props },
|
||||
ref
|
||||
) {
|
||||
const [bodyOverflowing, setBodyOverflowing] = useState(false);
|
||||
const overflowCtx = useMemo(
|
||||
() => ({ overflowing: bodyOverflowing, setOverflowing: setBodyOverflowing }),
|
||||
[bodyOverflowing]
|
||||
);
|
||||
|
||||
const handleInteractOutside = (e) => {
|
||||
if (preventClose) e.preventDefault();
|
||||
props.onInteractOutside?.(e);
|
||||
|
|
@ -87,7 +97,7 @@ const DialogContent = forwardRef(function DialogContent(
|
|||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogOverflowContext.Provider value={overflowCtx}>{children}</DialogOverflowContext.Provider>
|
||||
{showCloseButton && (
|
||||
<DialogClose asChild>
|
||||
<Button
|
||||
|
|
@ -140,13 +150,37 @@ const DialogBody = forwardRef(function DialogBody(
|
|||
{ className, noPadding = false, scrollable = false, children, ...props },
|
||||
ref
|
||||
) {
|
||||
const innerRef = useRef(null);
|
||||
const { setOverflowing } = useContext(DialogOverflowContext);
|
||||
|
||||
const mergedRef = useCallback(
|
||||
(node) => {
|
||||
innerRef.current = node;
|
||||
if (typeof ref === 'function') ref(node);
|
||||
else if (ref) ref.current = node;
|
||||
},
|
||||
[ref]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const node = innerRef.current;
|
||||
if (!node) return;
|
||||
|
||||
const check = () => setOverflowing(node.scrollHeight > node.clientHeight);
|
||||
const observer = new ResizeObserver(check);
|
||||
observer.observe(node);
|
||||
check();
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [setOverflowing]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
ref={mergedRef}
|
||||
data-slot="dialog-body"
|
||||
className={cn(
|
||||
'tw-flex-1 tw-min-h-0',
|
||||
!noPadding && 'tw-p-6',
|
||||
!noPadding && 'tw-px-6 tw-py-4',
|
||||
scrollable && 'tw-overflow-y-auto tw-max-h-[85vh]',
|
||||
className
|
||||
)}
|
||||
|
|
@ -167,13 +201,16 @@ DialogBody.propTypes = {
|
|||
// ── DialogFooter ─────────────────────────────────────────────────────────────
|
||||
|
||||
function DialogFooter({ className, ...props }) {
|
||||
const { overflowing } = useContext(DialogOverflowContext);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
'tw-px-6 tw-py-4',
|
||||
'tw-flex tw-flex-row tw-items-center tw-justify-between tw-gap-2',
|
||||
'tw-border-solid tw-border-0 tw-border-t tw-border-border-weak',
|
||||
'tw-border-solid tw-border-0 tw-border-t',
|
||||
overflowing ? 'tw-border-border-weak' : 'tw-border-transparent',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -220,6 +220,62 @@ export const Scrollable = {
|
|||
),
|
||||
};
|
||||
|
||||
// ── Overflow Border ─────────────────────────────────────────────────────────
|
||||
// Footer border only appears when body content overflows.
|
||||
|
||||
export const OverflowBorder = {
|
||||
render: () => (
|
||||
<div className="tw-flex tw-gap-3">
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">Short Content</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent size="small">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Short Content</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogBody scrollable>
|
||||
<p className="tw-text-sm tw-text-text-default">This content fits — no footer border.</p>
|
||||
</DialogBody>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
<Button variant="primary">Confirm</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">Long Content</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent size="small">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Long Content</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogBody scrollable>
|
||||
<div className="tw-flex tw-flex-col tw-gap-4 tw-text-sm tw-text-text-default">
|
||||
{Array.from({ length: 15 }, (_, i) => (
|
||||
<p key={i}>
|
||||
Paragraph {i + 1}: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor
|
||||
incididunt ut labore et dolore magna aliqua.
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</DialogBody>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
<Button variant="primary">Confirm</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
// ── No Padding ───────────────────────────────────────────────────────────────
|
||||
|
||||
export const NoPadding = {
|
||||
|
|
|
|||
84
frontend/src/components/ui/Rocket/RadioGroup/RadioGroup.jsx
Normal file
84
frontend/src/components/ui/Rocket/RadioGroup/RadioGroup.jsx
Normal file
|
|
@ -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 (
|
||||
<RadioGroupPrimitive.Root
|
||||
ref={ref}
|
||||
data-slot="radio-group"
|
||||
className={cn('tw-grid tw-gap-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
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 (
|
||||
<RadioGroupPrimitive.Item
|
||||
ref={ref}
|
||||
data-slot="radio-group-item"
|
||||
className={cn(radioGroupItemVariants({ size }), className)}
|
||||
{...props}
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator
|
||||
data-slot="radio-group-indicator"
|
||||
className="tw-flex tw-items-center tw-justify-center"
|
||||
>
|
||||
<span className={cn('tw-rounded-full tw-bg-text-on-solid', innerDotSizeClasses[size])} />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
);
|
||||
});
|
||||
RadioGroupItem.displayName = 'RadioGroupItem';
|
||||
RadioGroupItem.propTypes = {
|
||||
size: PropTypes.oneOf(['large', 'default']),
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export { RadioGroup, RadioGroupItem, radioGroupItemVariants };
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
# RadioGroup — Rocket Design Spec
|
||||
<!-- figma: https://www.figma.com/design/XQdM9x8x9YHcj1lUK8abUG/Rocket-components?node-id=93-52292 -->
|
||||
<!-- synced: 2026-04-07 -->
|
||||
|
||||
## 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 `<label>` + `<RadioGroupItem>`.
|
||||
|
||||
## Props
|
||||
|
||||
### RadioGroup
|
||||
|
||||
| Prop | Type | Default |
|
||||
|---|---|---|
|
||||
| value | string | — |
|
||||
| defaultValue | string | — |
|
||||
| onValueChange | function | — |
|
||||
| disabled | boolean | false |
|
||||
| name | string | — |
|
||||
| className | string | — |
|
||||
|
||||
### RadioGroupItem
|
||||
|
||||
| Prop | Type | Values | Default |
|
||||
|---|---|---|---|
|
||||
| size | string | large \| default | default |
|
||||
| value | string (required) | — | — |
|
||||
| disabled | boolean | — | false |
|
||||
| className | string | — | — |
|
||||
|
||||
## Sizes
|
||||
|
||||
| Value | Box | Inner dot |
|
||||
|---|---|---|
|
||||
| default | 16px (`tw-size-4`) | small filled circle |
|
||||
| large | 20px (`tw-size-5`) | small filled circle |
|
||||
|
||||
## Token Mapping
|
||||
|
||||
| Element | State | Figma token | ToolJet class |
|
||||
|---|---|---|---|
|
||||
| bg | unchecked | `--bg-surface-layer-01` | `tw-bg-background-surface-layer-01` |
|
||||
| border | unchecked | `border/default` | `tw-border-solid tw-border tw-border-border-default` |
|
||||
| bg | checked | `button/primary` | `data-[state=checked]:tw-bg-button-primary` |
|
||||
| border | checked | `button/primary` | `data-[state=checked]:tw-border-button-primary` |
|
||||
| inner dot | checked | `icon/on-solid` (white) | `tw-bg-text-on-solid` |
|
||||
| bg | disabled | `controls/base-inactive` | `disabled:tw-bg-controls-base-inactive disabled:tw-border-transparent` |
|
||||
| focus ring | focused | `interactive/focusActive` | `focus-visible:tw-ring-2 focus-visible:tw-ring-interactive-focus-outline focus-visible:tw-ring-offset-2` |
|
||||
| cursor | disabled | — | `disabled:tw-cursor-not-allowed` |
|
||||
|
||||
## CVA Shape
|
||||
|
||||
Shape C — sizes only, applied to `RadioGroupItem`.
|
||||
|
||||
## Notes
|
||||
|
||||
- Wraps Radix `RadioGroup.Root` + `RadioGroup.Item` + `RadioGroup.Indicator` via shadcn.
|
||||
- Both circular: `tw-rounded-full` for default and large.
|
||||
- Inner dot rendered via `<RadioGroup.Indicator>` containing a smaller filled circle.
|
||||
- Inner dot size: `tw-size-1.5` (6px) for default, `tw-size-2` (8px) for large.
|
||||
- `RadioGroup` wrapper provides selection state via Radix context — items must be wrapped in a `RadioGroup`.
|
||||
- Disabled state: bg becomes `controls-base-inactive` (subtle gray), border removed (transparent).
|
||||
- Same token tokens as Checkbox — consistent UX across binary controls.
|
||||
- Consumer composes label: `<label className="tw-flex tw-items-center tw-gap-2"><RadioGroupItem value="x" /> <span>Label</span></label>`.
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
import React from 'react';
|
||||
import { RadioGroup, RadioGroupItem } from './RadioGroup';
|
||||
|
||||
export default {
|
||||
title: 'Rocket/RadioGroup',
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'centered' },
|
||||
};
|
||||
|
||||
// ── Default ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export const Default = {
|
||||
render: () => (
|
||||
<RadioGroup defaultValue="option-1">
|
||||
<RadioGroupItem value="option-1" />
|
||||
<RadioGroupItem value="option-2" />
|
||||
<RadioGroupItem value="option-3" />
|
||||
</RadioGroup>
|
||||
),
|
||||
};
|
||||
|
||||
// ── With Labels ─────────────────────────────────────────────────────────────
|
||||
|
||||
export const WithLabels = {
|
||||
render: () => (
|
||||
<RadioGroup defaultValue="comfortable">
|
||||
<label className="tw-flex tw-items-center tw-gap-2 tw-cursor-pointer tw-font-body-default tw-text-text-default">
|
||||
<RadioGroupItem value="default" />
|
||||
<span>Default density</span>
|
||||
</label>
|
||||
<label className="tw-flex tw-items-center tw-gap-2 tw-cursor-pointer tw-font-body-default tw-text-text-default">
|
||||
<RadioGroupItem value="comfortable" />
|
||||
<span>Comfortable density</span>
|
||||
</label>
|
||||
<label className="tw-flex tw-items-center tw-gap-2 tw-cursor-pointer tw-font-body-default tw-text-text-default">
|
||||
<RadioGroupItem value="compact" />
|
||||
<span>Compact density</span>
|
||||
</label>
|
||||
</RadioGroup>
|
||||
),
|
||||
};
|
||||
|
||||
// ── Sizes ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export const Sizes = {
|
||||
render: () => (
|
||||
<div className="tw-flex tw-items-center tw-gap-4">
|
||||
<RadioGroup defaultValue="a">
|
||||
<RadioGroupItem value="a" size="default" />
|
||||
</RadioGroup>
|
||||
<RadioGroup defaultValue="b">
|
||||
<RadioGroupItem value="b" size="large" />
|
||||
</RadioGroup>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
// ── Disabled ────────────────────────────────────────────────────────────────
|
||||
|
||||
export const Disabled = {
|
||||
render: () => (
|
||||
<RadioGroup defaultValue="option-2" disabled>
|
||||
<label className="tw-flex tw-items-center tw-gap-2 tw-font-body-default tw-text-text-default">
|
||||
<RadioGroupItem value="option-1" />
|
||||
<span>Option 1</span>
|
||||
</label>
|
||||
<label className="tw-flex tw-items-center tw-gap-2 tw-font-body-default tw-text-text-default">
|
||||
<RadioGroupItem value="option-2" />
|
||||
<span>Option 2 (selected)</span>
|
||||
</label>
|
||||
</RadioGroup>
|
||||
),
|
||||
};
|
||||
|
||||
// ── All States ──────────────────────────────────────────────────────────────
|
||||
|
||||
export const AllStates = {
|
||||
render: () => (
|
||||
<div className="tw-flex tw-flex-col tw-gap-4">
|
||||
<div className="tw-flex tw-items-center tw-gap-3">
|
||||
<span className="tw-w-32 tw-font-body-default tw-text-text-default">Unchecked</span>
|
||||
<RadioGroup>
|
||||
<div className="tw-flex tw-items-center tw-gap-3">
|
||||
<RadioGroupItem value="a" />
|
||||
<RadioGroupItem value="a" size="large" />
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<div className="tw-flex tw-items-center tw-gap-3">
|
||||
<span className="tw-w-32 tw-font-body-default tw-text-text-default">Checked</span>
|
||||
<RadioGroup defaultValue="a">
|
||||
<div className="tw-flex tw-items-center tw-gap-3">
|
||||
<RadioGroupItem value="a" />
|
||||
<RadioGroupItem value="a" size="large" />
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<div className="tw-flex tw-items-center tw-gap-3">
|
||||
<span className="tw-w-32 tw-font-body-default tw-text-text-default">Disabled</span>
|
||||
<RadioGroup disabled>
|
||||
<div className="tw-flex tw-items-center tw-gap-3">
|
||||
<RadioGroupItem value="a" />
|
||||
<RadioGroupItem value="a" size="large" />
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<div className="tw-flex tw-items-center tw-gap-3">
|
||||
<span className="tw-w-32 tw-font-body-default tw-text-text-default">Disabled checked</span>
|
||||
<RadioGroup defaultValue="a" disabled>
|
||||
<div className="tw-flex tw-items-center tw-gap-3">
|
||||
<RadioGroupItem value="a" />
|
||||
<RadioGroupItem value="a" size="large" />
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
254
frontend/src/components/ui/Rocket/Sheet/Sheet.jsx
Normal file
254
frontend/src/components/ui/Rocket/Sheet/Sheet.jsx
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
import React, { createContext, forwardRef, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as SheetPrimitive from '@radix-ui/react-dialog';
|
||||
import { cva } from 'class-variance-authority';
|
||||
import { X } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/Rocket/Button/Button';
|
||||
|
||||
import { SheetTrigger, SheetClose, SheetPortal } from '@/components/ui/Rocket/shadcn/sheet';
|
||||
|
||||
// ── Overflow context (SheetBody → SheetFooter) ──────────────────────────────
|
||||
|
||||
const SheetOverflowContext = createContext({ overflowing: false, setOverflowing: () => {} });
|
||||
|
||||
// ── Sheet (root pass-through) ───────────────────────────────────────────────
|
||||
|
||||
const Sheet = SheetPrimitive.Root;
|
||||
|
||||
// ── SheetOverlay ────────────────────────────────────────────────────────────
|
||||
|
||||
const SheetOverlay = forwardRef(function SheetOverlay({ className, ...props }, ref) {
|
||||
return (
|
||||
<SheetPrimitive.Overlay
|
||||
ref={ref}
|
||||
data-slot="sheet-overlay"
|
||||
className={cn(
|
||||
'tw-fixed tw-inset-0 tw-z-50 tw-bg-black/50',
|
||||
'data-[state=open]:tw-animate-in data-[state=closed]:tw-animate-out data-[state=closed]:tw-fade-out-0 data-[state=open]:tw-fade-in-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SheetOverlay.displayName = 'SheetOverlay';
|
||||
|
||||
// ── SheetContent ────────────────────────────────────────────────────────────
|
||||
|
||||
const sheetContentVariants = cva(
|
||||
[
|
||||
'tw-fixed tw-inset-y-0 tw-right-0 tw-z-50 tw-h-full',
|
||||
'tw-bg-background-surface-layer-01',
|
||||
'tw-shadow-elevation-400',
|
||||
'tw-border-solid tw-border-l tw-border-border-weak',
|
||||
'tw-flex tw-flex-col',
|
||||
'tw-outline-none',
|
||||
// Animations
|
||||
'data-[state=open]:tw-animate-in data-[state=closed]:tw-animate-out',
|
||||
'data-[state=open]:tw-slide-in-from-right data-[state=closed]:tw-slide-out-to-right',
|
||||
'tw-duration-200',
|
||||
],
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
large: 'tw-w-[720px]',
|
||||
default: 'tw-w-[560px]',
|
||||
small: 'tw-w-[400px]',
|
||||
},
|
||||
},
|
||||
defaultVariants: { size: 'default' },
|
||||
}
|
||||
);
|
||||
|
||||
const SheetContent = forwardRef(function SheetContent(
|
||||
{ className, children, size, showCloseButton = true, preventClose = false, ...props },
|
||||
ref
|
||||
) {
|
||||
const [bodyOverflowing, setBodyOverflowing] = useState(false);
|
||||
const overflowCtx = useMemo(
|
||||
() => ({ overflowing: bodyOverflowing, setOverflowing: setBodyOverflowing }),
|
||||
[bodyOverflowing]
|
||||
);
|
||||
|
||||
const handleInteractOutside = (e) => {
|
||||
if (preventClose) e.preventDefault();
|
||||
props.onInteractOutside?.(e);
|
||||
};
|
||||
|
||||
const handleEscapeKeyDown = (e) => {
|
||||
if (preventClose) e.preventDefault();
|
||||
props.onEscapeKeyDown?.(e);
|
||||
};
|
||||
|
||||
return (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
ref={ref}
|
||||
data-slot="sheet-content"
|
||||
onInteractOutside={handleInteractOutside}
|
||||
onEscapeKeyDown={handleEscapeKeyDown}
|
||||
className={cn(sheetContentVariants({ size }), className)}
|
||||
{...props}
|
||||
>
|
||||
<SheetOverflowContext.Provider value={overflowCtx}>{children}</SheetOverflowContext.Provider>
|
||||
{showCloseButton && (
|
||||
<SheetClose asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
iconOnly
|
||||
size="small"
|
||||
className="tw-absolute tw-right-3 tw-top-4"
|
||||
aria-label="Close"
|
||||
>
|
||||
<X className="tw-size-4" />
|
||||
</Button>
|
||||
</SheetClose>
|
||||
)}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
);
|
||||
});
|
||||
SheetContent.displayName = 'SheetContent';
|
||||
SheetContent.propTypes = {
|
||||
size: PropTypes.oneOf(['large', 'default', 'small']),
|
||||
showCloseButton: PropTypes.bool,
|
||||
preventClose: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
// ── SheetHeader ─────────────────────────────────────────────────────────────
|
||||
|
||||
function SheetHeader({ className, ...props }) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-header"
|
||||
className={cn(
|
||||
'tw-flex tw-flex-row tw-items-center',
|
||||
'tw-h-14 tw-px-6 tw-py-0',
|
||||
'tw-gap-0 tw-shrink-0',
|
||||
'tw-border-solid tw-border-0 tw-border-b tw-border-border-weak',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
SheetHeader.displayName = 'SheetHeader';
|
||||
|
||||
// ── SheetBody ───────────────────────────────────────────────────────────────
|
||||
|
||||
const SheetBody = forwardRef(function SheetBody({ className, noPadding = false, children, ...props }, ref) {
|
||||
const innerRef = useRef(null);
|
||||
const { setOverflowing } = useContext(SheetOverflowContext);
|
||||
|
||||
const mergedRef = useCallback(
|
||||
(node) => {
|
||||
innerRef.current = node;
|
||||
if (typeof ref === 'function') ref(node);
|
||||
else if (ref) ref.current = node;
|
||||
},
|
||||
[ref]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const node = innerRef.current;
|
||||
if (!node) return;
|
||||
|
||||
const check = () => setOverflowing(node.scrollHeight > node.clientHeight);
|
||||
const observer = new ResizeObserver(check);
|
||||
observer.observe(node);
|
||||
node.addEventListener('scroll', check);
|
||||
check();
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
node.removeEventListener('scroll', check);
|
||||
};
|
||||
}, [setOverflowing]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={mergedRef}
|
||||
data-slot="sheet-body"
|
||||
className={cn('tw-flex-1 tw-min-h-0 tw-overflow-y-auto', !noPadding && 'tw-p-6', className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
SheetBody.displayName = 'SheetBody';
|
||||
SheetBody.propTypes = {
|
||||
noPadding: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
// ── SheetFooter ─────────────────────────────────────────────────────────────
|
||||
|
||||
function SheetFooter({ className, ...props }) {
|
||||
const { overflowing } = useContext(SheetOverflowContext);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-footer"
|
||||
className={cn(
|
||||
'tw-px-6 tw-py-4 tw-shrink-0',
|
||||
'tw-flex tw-flex-row tw-items-center tw-justify-between tw-gap-2',
|
||||
'tw-border-solid tw-border-0 tw-border-t',
|
||||
overflowing ? 'tw-border-border-weak' : 'tw-border-transparent',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
SheetFooter.displayName = 'SheetFooter';
|
||||
|
||||
// ── SheetTitle ──────────────────────────────────────────────────────────────
|
||||
|
||||
const SheetTitle = forwardRef(function SheetTitle({ className, ...props }, ref) {
|
||||
return (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
data-slot="sheet-title"
|
||||
className={cn('tw-font-title-large tw-text-text-default tw-mb-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SheetTitle.displayName = 'SheetTitle';
|
||||
|
||||
// ── SheetDescription ────────────────────────────────────────────────────────
|
||||
|
||||
const SheetDescription = forwardRef(function SheetDescription({ className, ...props }, ref) {
|
||||
return (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
data-slot="sheet-description"
|
||||
className={cn('tw-font-body-small tw-text-text-placeholder', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SheetDescription.displayName = 'SheetDescription';
|
||||
|
||||
// ── Exports ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetPortal,
|
||||
SheetOverlay,
|
||||
SheetContent,
|
||||
sheetContentVariants,
|
||||
SheetHeader,
|
||||
SheetBody,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
};
|
||||
74
frontend/src/components/ui/Rocket/Sheet/Sheet.spec.md
Normal file
74
frontend/src/components/ui/Rocket/Sheet/Sheet.spec.md
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# Sheet — Rocket Design Spec
|
||||
<!-- synced: 2026-04-07 -->
|
||||
|
||||
## Overview
|
||||
|
||||
Sheet is a slide-in panel from the right edge of the viewport. Used for multi-step forms or detail views that need more space than a Dialog can provide (e.g. adding a new datasource through multiple steps).
|
||||
|
||||
Wraps Radix Dialog primitives via shadcn (sheet is a Dialog with side animations). Mirrors Dialog's structure (Header / Body / Footer) including the conditional footer overflow border.
|
||||
|
||||
## Props (SheetContent)
|
||||
|
||||
| Prop | Type | Values | Default |
|
||||
|---|---|---|---|
|
||||
| size | string | large \| default \| small | default |
|
||||
| showCloseButton | boolean | — | true |
|
||||
| preventClose | boolean | — | false |
|
||||
|
||||
## Sizes
|
||||
|
||||
| Value | Width | Tailwind |
|
||||
|---|---|---|
|
||||
| small | 400px | tw-w-[400px] |
|
||||
| default | 560px | tw-w-[560px] |
|
||||
| large | 720px | tw-w-[720px] |
|
||||
|
||||
## Sub-components
|
||||
|
||||
| Component | Wraps | Styling |
|
||||
|---|---|---|
|
||||
| `Sheet` | Radix Dialog.Root (pass-through) | none |
|
||||
| `SheetTrigger` | Re-export from shadcn | none |
|
||||
| `SheetClose` | Re-export from shadcn | none |
|
||||
| `SheetPortal` | Re-export from shadcn | none |
|
||||
| `SheetOverlay` | Radix Overlay (direct) | tw-bg-black/50, animations |
|
||||
| `SheetContent` | Radix Content (direct) | size CVA, slide-in from right, tokens |
|
||||
| `SheetHeader` | plain div | border-bottom, fixed 56px height, horizontal padding |
|
||||
| `SheetBody` | plain div (custom) | scrollable, ResizeObserver for overflow detection |
|
||||
| `SheetFooter` | plain div | border-top (conditional on body overflow), padding |
|
||||
| `SheetTitle` | Radix Title (direct) | tw-font-title-large + color |
|
||||
| `SheetDescription` | Radix Description (direct) | tw-font-body-small + color |
|
||||
|
||||
## Token Mapping
|
||||
|
||||
| Element | State | ToolJet class |
|
||||
|---|---|---|
|
||||
| content bg | default | tw-bg-background-surface-layer-01 |
|
||||
| content shadow | default | tw-shadow-elevation-400 |
|
||||
| content border-left | default | tw-border-solid tw-border-l tw-border-border-weak |
|
||||
| overlay bg | default | tw-bg-black/50 |
|
||||
| header border | default | tw-border-solid tw-border-b tw-border-border-weak |
|
||||
| header height | default | tw-h-14 |
|
||||
| header padding | default | tw-px-6 tw-py-0 |
|
||||
| title text | default | tw-font-title-large tw-text-text-default |
|
||||
| description text | default | tw-font-body-small tw-text-text-placeholder |
|
||||
| body padding | default | tw-p-6 |
|
||||
| footer border (overflowing) | default | tw-border-solid tw-border-t tw-border-border-weak |
|
||||
| footer border (no overflow) | default | tw-border-transparent |
|
||||
| footer padding | default | tw-px-6 tw-py-4 |
|
||||
| footer layout | default | tw-flex tw-justify-between tw-items-center |
|
||||
| close button position | default | absolute top-right of content |
|
||||
|
||||
## CVA Shape
|
||||
|
||||
Shape C — sizes only. `sheetContentVariants` has `size` CVA for max-width. Rest use static `cn()`.
|
||||
|
||||
## Notes
|
||||
|
||||
- Right-side only for now (not exposing `side` prop). Sheet always slides in from the right edge.
|
||||
- Slide-in animation via `data-[state=open]:tw-slide-in-from-right` and `data-[state=closed]:tw-slide-out-to-right`.
|
||||
- **Overflow border** on footer mirrors Dialog: ResizeObserver on SheetBody detects when content overflows, sets context, footer reads context for `border-t` color.
|
||||
- `preventClose` blocks overlay click via `onInteractOutside` + Escape via `onEscapeKeyDown` (same as Dialog).
|
||||
- Close button is positioned absolute top-right of content (same as Dialog).
|
||||
- Full viewport height — sheet stretches from top to bottom of the screen.
|
||||
- Typography uses `tw-font-*` plugin utilities.
|
||||
190
frontend/src/components/ui/Rocket/Sheet/Sheet.stories.jsx
Normal file
190
frontend/src/components/ui/Rocket/Sheet/Sheet.stories.jsx
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
Sheet,
|
||||
SheetTrigger,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetBody,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
SheetClose,
|
||||
} from './Sheet';
|
||||
import { Button } from '@/components/ui/Rocket/Button/Button';
|
||||
|
||||
export default {
|
||||
title: 'Rocket/Sheet',
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'centered' },
|
||||
};
|
||||
|
||||
// ── Default ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export const Default = {
|
||||
render: () => (
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="primary">Open Sheet</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Add new datasource</SheetTitle>
|
||||
</SheetHeader>
|
||||
<SheetBody>
|
||||
<p className="tw-text-sm tw-text-text-default">Sheet body content goes here.</p>
|
||||
</SheetBody>
|
||||
<SheetFooter>
|
||||
<SheetClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</SheetClose>
|
||||
<Button variant="primary">Continue</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
),
|
||||
};
|
||||
|
||||
// ── Sizes ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export const Sizes = {
|
||||
render: () => (
|
||||
<div className="tw-flex tw-gap-3">
|
||||
{['small', 'default', 'large'].map((size) => (
|
||||
<Sheet key={size}>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="outline">{size}</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent size={size}>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Size: {size}</SheetTitle>
|
||||
</SheetHeader>
|
||||
<SheetBody>
|
||||
<p className="tw-text-sm tw-text-text-default">
|
||||
This sheet uses the <code>{size}</code> size variant.
|
||||
</p>
|
||||
</SheetBody>
|
||||
<SheetFooter>
|
||||
<SheetClose asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
</SheetClose>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
// ── With Description ────────────────────────────────────────────────────────
|
||||
|
||||
export const WithDescription = {
|
||||
render: () => (
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="primary">Add Datasource</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<div className="tw-flex tw-flex-col tw-gap-1">
|
||||
<SheetTitle>Connect a new datasource</SheetTitle>
|
||||
<SheetDescription>Choose a datasource type to get started.</SheetDescription>
|
||||
</div>
|
||||
</SheetHeader>
|
||||
<SheetBody>
|
||||
<p className="tw-text-sm tw-text-text-default">
|
||||
Sheet body content for selecting and configuring a datasource.
|
||||
</p>
|
||||
</SheetBody>
|
||||
<SheetFooter>
|
||||
<SheetClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</SheetClose>
|
||||
<Button variant="primary">Next</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
),
|
||||
};
|
||||
|
||||
// ── Overflow Border ─────────────────────────────────────────────────────────
|
||||
// Footer border only appears when body content overflows.
|
||||
|
||||
export const OverflowBorder = {
|
||||
render: () => (
|
||||
<div className="tw-flex tw-gap-3">
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="outline">Short Content</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Short Content</SheetTitle>
|
||||
</SheetHeader>
|
||||
<SheetBody>
|
||||
<p className="tw-text-sm tw-text-text-default">This content fits — no footer border.</p>
|
||||
</SheetBody>
|
||||
<SheetFooter>
|
||||
<SheetClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</SheetClose>
|
||||
<Button variant="primary">Confirm</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="outline">Long Content</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Long Content</SheetTitle>
|
||||
</SheetHeader>
|
||||
<SheetBody>
|
||||
<div className="tw-flex tw-flex-col tw-gap-4 tw-text-sm tw-text-text-default">
|
||||
{Array.from({ length: 30 }, (_, i) => (
|
||||
<p key={i}>
|
||||
Paragraph {i + 1}: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor
|
||||
incididunt ut labore et dolore magna aliqua.
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</SheetBody>
|
||||
<SheetFooter>
|
||||
<SheetClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</SheetClose>
|
||||
<Button variant="primary">Confirm</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
// ── Prevent Close ───────────────────────────────────────────────────────────
|
||||
|
||||
export const PreventClose = {
|
||||
render: () => (
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="outline">Prevent Close</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent preventClose>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Multi-step form</SheetTitle>
|
||||
</SheetHeader>
|
||||
<SheetBody>
|
||||
<p className="tw-text-sm tw-text-text-default">
|
||||
Clicking outside or pressing Escape won't close this sheet. Use the footer button.
|
||||
</p>
|
||||
</SheetBody>
|
||||
<SheetFooter>
|
||||
<SheetClose asChild>
|
||||
<Button variant="primary">Done</Button>
|
||||
</SheetClose>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
),
|
||||
};
|
||||
16
frontend/src/components/ui/Rocket/Skeleton/Skeleton.jsx
Normal file
16
frontend/src/components/ui/Rocket/Skeleton/Skeleton.jsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Skeleton as ShadcnSkeleton } from '@/components/ui/Rocket/shadcn/skeleton';
|
||||
|
||||
const skeletonClasses = 'tw-animate-pulse tw-rounded-md tw-bg-interactive-hover';
|
||||
|
||||
function Skeleton({ className, ...props }) {
|
||||
return <ShadcnSkeleton className={cn(skeletonClasses, className)} {...props} />;
|
||||
}
|
||||
Skeleton.displayName = 'Skeleton';
|
||||
Skeleton.propTypes = {
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export { Skeleton, skeletonClasses };
|
||||
34
frontend/src/components/ui/Rocket/Skeleton/Skeleton.spec.md
Normal file
34
frontend/src/components/ui/Rocket/Skeleton/Skeleton.spec.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# Skeleton — Rocket Design Spec
|
||||
<!-- synced: 2026-04-07 -->
|
||||
|
||||
## Overview
|
||||
|
||||
Skeleton is a loading placeholder primitive — a pulsing rectangle. Used in loading states for content (text, lists, tables, cards) before real data arrives.
|
||||
|
||||
Wraps shadcn skeleton with ToolJet token override.
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default |
|
||||
|---|---|---|
|
||||
| className | string | — |
|
||||
|
||||
Sized via Tailwind utilities (e.g. `tw-h-4 tw-w-32`).
|
||||
|
||||
## Token Mapping
|
||||
|
||||
| Element | ToolJet class |
|
||||
|---|---|
|
||||
| bg | `tw-bg-interactive-hover` |
|
||||
| animation | `tw-animate-pulse` |
|
||||
| radius | `tw-rounded-md` |
|
||||
|
||||
## CVA Shape
|
||||
|
||||
Shape D — no CVA. Single static class set.
|
||||
|
||||
## Notes
|
||||
|
||||
- Brought from PR #14498 (`Rocket/skeleton.jsx`).
|
||||
- Background uses `tw-bg-interactive-hover` (subtle gray) instead of shadcn's `tw-bg-muted` semantic token.
|
||||
- Consumer sets dimensions via className.
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import React from 'react';
|
||||
import { Skeleton } from './Skeleton';
|
||||
|
||||
export default {
|
||||
title: 'Rocket/Skeleton',
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'padded' },
|
||||
};
|
||||
|
||||
export const Default = {
|
||||
render: () => <Skeleton className="tw-h-4 tw-w-48" />,
|
||||
};
|
||||
|
||||
export const Card = {
|
||||
render: () => (
|
||||
<div className="tw-flex tw-flex-col tw-gap-3 tw-w-[320px]">
|
||||
<Skeleton className="tw-h-32 tw-w-full" />
|
||||
<Skeleton className="tw-h-4 tw-w-3/4" />
|
||||
<Skeleton className="tw-h-4 tw-w-1/2" />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const ListRow = {
|
||||
render: () => (
|
||||
<div className="tw-flex tw-items-center tw-gap-3 tw-w-[400px]">
|
||||
<Skeleton className="tw-size-10 tw-rounded-full" />
|
||||
<div className="tw-flex tw-flex-col tw-gap-2 tw-flex-1">
|
||||
<Skeleton className="tw-h-4 tw-w-3/4" />
|
||||
<Skeleton className="tw-h-3 tw-w-1/2" />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
82
frontend/src/components/ui/Rocket/Sonner/Sonner.jsx
Normal file
82
frontend/src/components/ui/Rocket/Sonner/Sonner.jsx
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Toaster as SonnerToaster, toast } from 'sonner';
|
||||
import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon, X } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
// ── Toaster ─────────────────────────────────────────────────────────────────
|
||||
// Place once in the app root. All toast() calls will render here.
|
||||
|
||||
const toastIcons = {
|
||||
success: <CircleCheckIcon className="tw-size-5" />,
|
||||
info: <InfoIcon className="tw-size-5" />,
|
||||
warning: <TriangleAlertIcon className="tw-size-5" />,
|
||||
error: <OctagonXIcon className="tw-size-5" />,
|
||||
loading: <Loader2Icon className="tw-size-5 tw-animate-spin" />,
|
||||
close: <X className="tw-size-4" />,
|
||||
};
|
||||
|
||||
const toastClassNames = {
|
||||
toast: cn(
|
||||
// Layout
|
||||
'tw-flex tw-items-center tw-gap-2 tw-px-3 tw-py-2.5 tw-pr-10 tw-w-[var(--width)]',
|
||||
// Visual
|
||||
'tw-bg-background-surface-layer-01',
|
||||
'tw-border-solid tw-border tw-border-border-weak',
|
||||
'tw-shadow-elevation-300',
|
||||
'tw-rounded-md'
|
||||
),
|
||||
title: 'tw-font-title-large tw-text-text-default',
|
||||
description: 'tw-font-body-default tw-text-text-default',
|
||||
content: 'tw-flex tw-flex-col tw-gap-0.5',
|
||||
icon: 'tw-flex tw-items-center tw-justify-center tw-shrink-0',
|
||||
closeButton: cn(
|
||||
'tw-absolute tw-right-2 tw-flex tw-items-center tw-justify-center tw-shrink-0 tw-ml-auto',
|
||||
'tw-size-6 tw-rounded-md tw-border-0 tw-bg-transparent tw-cursor-pointer',
|
||||
'tw-text-icon-default hover:tw-bg-interactive-hover hover:tw-text-icon-strong',
|
||||
'tw-transition-colors'
|
||||
),
|
||||
actionButton: cn(
|
||||
'tw-font-title-default tw-ml-auto tw-shrink-0',
|
||||
'tw-rounded-md tw-px-2 tw-h-6 tw-border-0 tw-cursor-pointer',
|
||||
'tw-bg-transparent tw-text-text-brand hover:tw-bg-interactive-hover',
|
||||
'tw-transition-colors'
|
||||
),
|
||||
};
|
||||
|
||||
function Toaster({
|
||||
position = 'top-center',
|
||||
closeButton = true,
|
||||
richColors = true,
|
||||
duration = 4000,
|
||||
className,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<SonnerToaster
|
||||
position={position}
|
||||
closeButton={closeButton}
|
||||
richColors={richColors}
|
||||
duration={duration}
|
||||
icons={toastIcons}
|
||||
className={cn('tw-toaster tw-group', className)}
|
||||
toastOptions={{
|
||||
unstyled: true,
|
||||
classNames: toastClassNames,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Toaster.displayName = 'Toaster';
|
||||
Toaster.propTypes = {
|
||||
position: PropTypes.string,
|
||||
closeButton: PropTypes.bool,
|
||||
richColors: PropTypes.bool,
|
||||
duration: PropTypes.number,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
// ── Exports ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export { Toaster, toast };
|
||||
84
frontend/src/components/ui/Rocket/Sonner/Sonner.spec.md
Normal file
84
frontend/src/components/ui/Rocket/Sonner/Sonner.spec.md
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# Sonner — Rocket Design Spec
|
||||
<!-- figma: https://www.figma.com/design/SZKm4ecuzgBwFq0v4sfrO5/Sanitation---Miscellaneous?node-id=2868-334374 -->
|
||||
<!-- synced: 2026-04-02 -->
|
||||
|
||||
## Overview
|
||||
|
||||
Sonner is the toast notification system for Rocket. It wraps the `sonner` library with ToolJet tokens. Consists of two parts:
|
||||
1. `<Toaster />` — provider component, placed once in the app root
|
||||
2. `toast()` — imperative API to trigger toasts from anywhere
|
||||
|
||||
## Toaster Props
|
||||
|
||||
| Prop | Type | Values | Default |
|
||||
|---|---|---|---|
|
||||
| position | string | top-center \| top-right \| bottom-right \| etc. | top-center |
|
||||
| closeButton | boolean | — | true |
|
||||
| richColors | boolean | — | true |
|
||||
| duration | number | ms | 4000 |
|
||||
|
||||
## Toast Types
|
||||
|
||||
| Type | Icon | Color intent |
|
||||
|---|---|---|
|
||||
| default | none | neutral (surface bg) |
|
||||
| success | CircleCheckIcon | green/success |
|
||||
| error | OctagonXIcon | red/danger |
|
||||
| warning | TriangleAlertIcon | orange/warning |
|
||||
| info | InfoIcon | blue/brand |
|
||||
| loading | Loader2Icon (spinning) | neutral |
|
||||
|
||||
## Token Mapping (from Figma)
|
||||
|
||||
| Element | Figma token | ToolJet class |
|
||||
|---|---|---|
|
||||
| toast bg | Base/Light/white 00 | `tw-bg-background-surface-layer-01` |
|
||||
| toast border | slate/light/05 (#E6E8EB) | `tw-border-solid tw-border tw-border-border-weak` |
|
||||
| toast shadow | Shadow/06 (0 32 64 -12) | `tw-shadow-[0px_32px_64px_-12px_rgba(16,24,40,0.14)]` |
|
||||
| toast radius | 6px | `tw-rounded-md` |
|
||||
| toast padding | 16px | `tw-p-4` |
|
||||
| toast text | 14px Medium / 20px (slate/light/12) | `tw-font-title-large tw-text-text-default` |
|
||||
| icon gap | 8px | `tw-gap-2` |
|
||||
| close icon | slate/light/09 (#889096) | `tw-text-icon-default` |
|
||||
| close icon size | 16px | `tw-size-4` |
|
||||
| type icon size | 20px | `tw-size-5` |
|
||||
|
||||
## Usage Pattern
|
||||
|
||||
```jsx
|
||||
// In app root (once)
|
||||
<Toaster />
|
||||
|
||||
// Anywhere in the app
|
||||
import { toast } from 'sonner';
|
||||
|
||||
toast('Default notification');
|
||||
toast.success('Item saved');
|
||||
toast.error('Workspace already exists');
|
||||
toast.warning('Proceed with caution');
|
||||
toast.info('New version available');
|
||||
toast.loading('Uploading...');
|
||||
|
||||
// With description
|
||||
toast.success('Saved', { description: 'Your changes have been saved.' });
|
||||
|
||||
// With action
|
||||
toast('Item deleted', {
|
||||
action: { label: 'Undo', onClick: () => undoDelete() },
|
||||
});
|
||||
```
|
||||
|
||||
## CVA Shape
|
||||
|
||||
Shape D — no CVA. Sonner handles its own rendering. Rocket HOC overrides styles via `toastOptions.classNames` and inline style variables.
|
||||
|
||||
## Notes
|
||||
|
||||
- `sonner` library handles all toast rendering, stacking, swipe-to-dismiss, and auto-dismiss.
|
||||
- No `next-themes` — ToolJet uses `.dark-theme` class. Theme not passed to sonner.
|
||||
- Icons from `lucide-react` — type icons are 20px (`tw-size-5`), close icon is 16px (`tw-size-4`).
|
||||
- `closeButton={true}` by default — all toasts get a close button.
|
||||
- Position defaults to `top-center`.
|
||||
- `richColors={true}` enables sonner's built-in semantic coloring for typed toasts.
|
||||
- Shadow is heavier than standard elevation tokens — uses custom shadow from Figma.
|
||||
- Typography uses `tw-font-title-large` (14px Medium / 20px) — not the default body font.
|
||||
136
frontend/src/components/ui/Rocket/Sonner/Sonner.stories.jsx
Normal file
136
frontend/src/components/ui/Rocket/Sonner/Sonner.stories.jsx
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import React from 'react';
|
||||
import { Toaster, toast } from './Sonner';
|
||||
import { Button } from '@/components/ui/Rocket/Button/Button';
|
||||
|
||||
export default {
|
||||
title: 'Rocket/Sonner',
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'centered' },
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<>
|
||||
<Toaster />
|
||||
<Story />
|
||||
</>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
// ── Default ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export const Default = {
|
||||
render: () => (
|
||||
<Button variant="outline" onClick={() => toast('This is a default toast')}>
|
||||
Default Toast
|
||||
</Button>
|
||||
),
|
||||
};
|
||||
|
||||
// ── Success ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export const Success = {
|
||||
render: () => (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => toast.success('Item saved', { description: 'Your changes have been saved successfully.' })}
|
||||
>
|
||||
Success Toast
|
||||
</Button>
|
||||
),
|
||||
};
|
||||
|
||||
// ── Error ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export const Error = {
|
||||
render: () => (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => toast.error('Something went wrong', { description: 'Please try again later.' })}
|
||||
>
|
||||
Error Toast
|
||||
</Button>
|
||||
),
|
||||
};
|
||||
|
||||
// ── Warning ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export const Warning = {
|
||||
render: () => (
|
||||
<Button variant="outline" onClick={() => toast.warning('Proceed with caution')}>
|
||||
Warning Toast
|
||||
</Button>
|
||||
),
|
||||
};
|
||||
|
||||
// ── Info ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export const Info = {
|
||||
render: () => (
|
||||
<Button variant="outline" onClick={() => toast.info('New version available')}>
|
||||
Info Toast
|
||||
</Button>
|
||||
),
|
||||
};
|
||||
|
||||
// ── Loading ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export const Loading = {
|
||||
render: () => (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
const id = toast.loading('Uploading...');
|
||||
setTimeout(() => toast.success('Upload complete', { id }), 2000);
|
||||
}}
|
||||
>
|
||||
Loading Toast
|
||||
</Button>
|
||||
),
|
||||
};
|
||||
|
||||
// ── With Action ─────────────────────────────────────────────────────────────
|
||||
|
||||
export const WithAction = {
|
||||
render: () => (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
toast('Item deleted', {
|
||||
action: {
|
||||
label: 'Undo',
|
||||
onClick: () => toast.success('Restored'),
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
Toast with Action
|
||||
</Button>
|
||||
),
|
||||
};
|
||||
|
||||
// ── All Types ───────────────────────────────────────────────────────────────
|
||||
|
||||
export const AllTypes = {
|
||||
render: () => (
|
||||
<div className="tw-flex tw-flex-wrap tw-gap-2">
|
||||
<Button variant="outline" onClick={() => toast('Default notification')}>
|
||||
Default
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => toast.success('Success!')}>
|
||||
Success
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => toast.error('Error!')}>
|
||||
Error
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => toast.warning('Warning!')}>
|
||||
Warning
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => toast.info('Info!')}>
|
||||
Info
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => toast.loading('Loading...')}>
|
||||
Loading
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
38
frontend/src/components/ui/Rocket/Spinner/Spinner.jsx
Normal file
38
frontend/src/components/ui/Rocket/Spinner/Spinner.jsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import React, { forwardRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { cva } from 'class-variance-authority';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
const spinnerVariants = cva(['tw-animate-spin tw-text-icon-default tw-shrink-0'], {
|
||||
variants: {
|
||||
size: {
|
||||
large: 'tw-size-6',
|
||||
default: 'tw-size-4',
|
||||
small: 'tw-size-3',
|
||||
},
|
||||
},
|
||||
defaultVariants: { size: 'default' },
|
||||
});
|
||||
|
||||
const Spinner = forwardRef(function Spinner({ className, size, label = 'Loading', ...props }, ref) {
|
||||
return (
|
||||
<Loader2
|
||||
ref={ref}
|
||||
role="status"
|
||||
aria-label={label}
|
||||
className={cn(spinnerVariants({ size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
Spinner.displayName = 'Spinner';
|
||||
|
||||
Spinner.propTypes = {
|
||||
size: PropTypes.oneOf(['large', 'default', 'small']),
|
||||
label: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export { Spinner, spinnerVariants };
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
import React from 'react';
|
||||
import { Spinner } from './Spinner';
|
||||
import { Button } from '../Button/Button';
|
||||
|
||||
export default {
|
||||
title: 'Rocket/Spinner',
|
||||
component: Spinner,
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'centered' },
|
||||
argTypes: {
|
||||
size: {
|
||||
control: 'select',
|
||||
options: ['large', 'default', 'small'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// ── Default ───────────────────────────────────────────────────────────────
|
||||
export const Default = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
// ── Sizes ─────────────────────────────────────────────────────────────────
|
||||
export const Sizes = {
|
||||
render: () => (
|
||||
<div className="tw-flex tw-items-center tw-gap-4 tw-p-4">
|
||||
<div className="tw-flex tw-flex-col tw-items-center tw-gap-1">
|
||||
<Spinner size="large" />
|
||||
<span className="tw-text-xs tw-text-text-placeholder">Large</span>
|
||||
</div>
|
||||
<div className="tw-flex tw-flex-col tw-items-center tw-gap-1">
|
||||
<Spinner size="default" />
|
||||
<span className="tw-text-xs tw-text-text-placeholder">Default</span>
|
||||
</div>
|
||||
<div className="tw-flex tw-flex-col tw-items-center tw-gap-1">
|
||||
<Spinner size="small" />
|
||||
<span className="tw-text-xs tw-text-text-placeholder">Small</span>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
parameters: { layout: 'padded' },
|
||||
};
|
||||
|
||||
// ── Custom Color ─────────────────────────────────────────────────────────
|
||||
export const CustomColor = {
|
||||
render: () => (
|
||||
<div className="tw-flex tw-items-center tw-gap-4 tw-p-4">
|
||||
<Spinner className="tw-text-icon-default" />
|
||||
<Spinner className="tw-text-text-brand" />
|
||||
<Spinner className="tw-text-icon-danger" />
|
||||
<Spinner className="tw-text-white" />
|
||||
</div>
|
||||
),
|
||||
parameters: { layout: 'padded' },
|
||||
};
|
||||
|
||||
// ── Inline with Text ─────────────────────────────────────────────────────
|
||||
export const InlineWithText = {
|
||||
render: () => (
|
||||
<div className="tw-flex tw-flex-col tw-gap-3 tw-p-4">
|
||||
<div className="tw-flex tw-items-center tw-gap-2 tw-text-text-default tw-text-sm">
|
||||
<Spinner size="small" />
|
||||
<span>Loading data...</span>
|
||||
</div>
|
||||
<div className="tw-flex tw-items-center tw-gap-2 tw-text-text-placeholder tw-text-sm">
|
||||
<Spinner size="small" className="tw-text-text-placeholder" />
|
||||
<span>Saving changes...</span>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
parameters: { layout: 'padded' },
|
||||
};
|
||||
|
||||
// ── In Button ────────────────────────────────────────────────────────────
|
||||
export const InButton = {
|
||||
render: () => (
|
||||
<div className="tw-flex tw-items-center tw-gap-3 tw-p-4">
|
||||
<Button disabled>
|
||||
<Spinner size="small" className="tw-text-current" />
|
||||
Saving...
|
||||
</Button>
|
||||
<Button variant="outline" disabled>
|
||||
<Spinner size="small" className="tw-text-current" />
|
||||
Loading...
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
parameters: { layout: 'padded' },
|
||||
};
|
||||
170
frontend/src/components/ui/Rocket/Table/Table.jsx
Normal file
170
frontend/src/components/ui/Rocket/Table/Table.jsx
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
import React, { createContext, forwardRef, useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import {
|
||||
Table as ShadcnTable,
|
||||
TableHeader as ShadcnTableHeader,
|
||||
TableBody as ShadcnTableBody,
|
||||
TableFooter as ShadcnTableFooter,
|
||||
TableRow as ShadcnTableRow,
|
||||
TableHead as ShadcnTableHead,
|
||||
TableCell as ShadcnTableCell,
|
||||
TableCaption as ShadcnTableCaption,
|
||||
} from '@/components/ui/Rocket/shadcn/table';
|
||||
|
||||
// ── Density context ─────────────────────────────────────────────────────────
|
||||
|
||||
const TableDensityContext = createContext('default');
|
||||
|
||||
// ── Table (root) ────────────────────────────────────────────────────────────
|
||||
|
||||
const Table = forwardRef(function Table({ className, density = 'default', ...props }, ref) {
|
||||
return (
|
||||
<TableDensityContext.Provider value={density}>
|
||||
<ShadcnTable
|
||||
ref={ref}
|
||||
data-density={density}
|
||||
className={cn(
|
||||
'tw-w-full tw-caption-bottom',
|
||||
'tw-border-separate tw-border-spacing-0',
|
||||
'tw-font-body-default tw-text-text-default',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</TableDensityContext.Provider>
|
||||
);
|
||||
});
|
||||
Table.displayName = 'Table';
|
||||
Table.propTypes = {
|
||||
density: PropTypes.oneOf(['default', 'compact']),
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
// ── TableHeader ─────────────────────────────────────────────────────────────
|
||||
|
||||
const TableHeader = forwardRef(function TableHeader({ className, ...props }, ref) {
|
||||
return (
|
||||
<ShadcnTableHeader
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'[&_th]:tw-border-solid [&_th]:tw-border-0 [&_th]:tw-border-b [&_th]:tw-border-border-weak',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
TableHeader.displayName = 'TableHeader';
|
||||
|
||||
// ── TableBody ───────────────────────────────────────────────────────────────
|
||||
|
||||
const TableBody = forwardRef(function TableBody({ className, ...props }, ref) {
|
||||
return <ShadcnTableBody ref={ref} className={cn(className)} {...props} />;
|
||||
});
|
||||
TableBody.displayName = 'TableBody';
|
||||
|
||||
// ── TableFooter ─────────────────────────────────────────────────────────────
|
||||
|
||||
const TableFooter = forwardRef(function TableFooter({ className, ...props }, ref) {
|
||||
return (
|
||||
<ShadcnTableFooter
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'tw-border-solid tw-border-0 tw-border-t tw-border-border-weak',
|
||||
'tw-bg-background-surface-layer-02 tw-font-title-default',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
TableFooter.displayName = 'TableFooter';
|
||||
|
||||
// ── TableRow ────────────────────────────────────────────────────────────────
|
||||
|
||||
const TableRow = forwardRef(function TableRow({ className, ...props }, ref) {
|
||||
return (
|
||||
<ShadcnTableRow
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'tw-transition-colors',
|
||||
'hover:tw-bg-interactive-hover',
|
||||
'data-[state=selected]:tw-bg-interactive-selected',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
TableRow.displayName = 'TableRow';
|
||||
|
||||
// ── TableHead ───────────────────────────────────────────────────────────────
|
||||
|
||||
const tableHeadDensityClasses = {
|
||||
default: 'tw-h-10 tw-px-3.5 tw-py-0',
|
||||
compact: 'tw-h-8 tw-px-3 tw-py-0',
|
||||
};
|
||||
|
||||
const TableHead = forwardRef(function TableHead({ className, ...props }, ref) {
|
||||
const density = useContext(TableDensityContext);
|
||||
|
||||
return (
|
||||
<ShadcnTableHead
|
||||
ref={ref}
|
||||
className={cn(
|
||||
tableHeadDensityClasses[density],
|
||||
'tw-text-left tw-align-middle',
|
||||
'tw-font-title-default tw-text-text-default',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
TableHead.displayName = 'TableHead';
|
||||
|
||||
// ── TableCell ───────────────────────────────────────────────────────────────
|
||||
|
||||
const tableCellDensityClasses = {
|
||||
default: 'tw-h-[52px] tw-p-3.5',
|
||||
compact: 'tw-h-9 tw-px-3 tw-py-2',
|
||||
};
|
||||
|
||||
const TableCell = forwardRef(function TableCell({ className, ...props }, ref) {
|
||||
const density = useContext(TableDensityContext);
|
||||
|
||||
return (
|
||||
<ShadcnTableCell
|
||||
ref={ref}
|
||||
className={cn(
|
||||
tableCellDensityClasses[density],
|
||||
'tw-align-middle tw-text-text-default',
|
||||
// Rounded pill effect: first/last cell corners get rounded so the
|
||||
// hover/selected row bg shows as a pill highlight.
|
||||
'first:tw-rounded-l-[10px] last:tw-rounded-r-[10px]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
TableCell.displayName = 'TableCell';
|
||||
|
||||
// ── TableCaption ────────────────────────────────────────────────────────────
|
||||
|
||||
const TableCaption = forwardRef(function TableCaption({ className, ...props }, ref) {
|
||||
return (
|
||||
<ShadcnTableCaption
|
||||
ref={ref}
|
||||
className={cn('tw-mt-4 tw-font-body-small tw-text-text-placeholder', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
TableCaption.displayName = 'TableCaption';
|
||||
|
||||
// ── Exports ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export { Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption };
|
||||
86
frontend/src/components/ui/Rocket/Table/Table.spec.md
Normal file
86
frontend/src/components/ui/Rocket/Table/Table.spec.md
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
# Table — Rocket Design Spec
|
||||
<!-- synced: 2026-04-07 -->
|
||||
|
||||
## Overview
|
||||
|
||||
Table is the low-level Rocket primitive for tabular data. Wraps shadcn's `<table>` primitive with ToolJet token overrides. Used as the structural base for higher-level compositions like `DataTable` (TanStack-driven) and feature tables (apps list, datasources list, workflows list, etc.).
|
||||
|
||||
The HOC layer **only handles structure + visual tokens**. State (sorting, filtering, pagination, selection) is the responsibility of the consumer or the `DataTable` block.
|
||||
|
||||
Brought forward from PR #14498 with improved organization and ToolJet token usage.
|
||||
|
||||
## Props
|
||||
|
||||
### Table (root)
|
||||
|
||||
| Prop | Type | Values | Default |
|
||||
|---|---|---|---|
|
||||
| density | string | default \| compact | default |
|
||||
| className | string | — | — |
|
||||
|
||||
### TableRow
|
||||
|
||||
| Prop | Type | Values | Default |
|
||||
|---|---|---|---|
|
||||
| data-state | string | "selected" \| undefined | — |
|
||||
| className | string | — | — |
|
||||
|
||||
## Density (Sizes)
|
||||
|
||||
| Value | Header height | Header padding | Cell height | Cell padding | Font |
|
||||
|---|---|---|---|---|---|
|
||||
| default | 40px | 0 / 14px | 52px | 14px | `tw-text-base` (12px) |
|
||||
| compact | 32px | 0 / 12px | 36px | 8px / 12px | `tw-text-base` (12px) |
|
||||
|
||||
Density is set via context on `Table` and read by `TableHead` / `TableCell`.
|
||||
|
||||
## Sub-components
|
||||
|
||||
| Component | Element | Token overrides |
|
||||
|---|---|---|
|
||||
| `Table` | `<table>` inside scrollable `<div>` | bg, w-full, font tokens |
|
||||
| `TableHeader` | `<thead>` | border-bottom on rows |
|
||||
| `TableBody` | `<tbody>` | last row no border |
|
||||
| `TableFooter` | `<tfoot>` | border-top, surface bg, medium font |
|
||||
| `TableRow` | `<tr>` | border-bottom, hover bg, selected bg |
|
||||
| `TableHead` | `<th>` | header height (density), title font, text-default, left-align |
|
||||
| `TableCell` | `<td>` | cell padding (density), body font, text-default |
|
||||
| `TableCaption` | `<caption>` | mt-4, body small, text-placeholder |
|
||||
|
||||
## Token Mapping
|
||||
|
||||
| Element | State | ToolJet class |
|
||||
|---|---|---|
|
||||
| Table container | default | `tw-relative tw-w-full tw-overflow-auto` |
|
||||
| Table | default | `tw-w-full tw-caption-bottom tw-border-separate tw-border-spacing-0 tw-font-body-default tw-text-text-default` |
|
||||
| TableHeader | default | `tw-border-solid tw-border-0 tw-border-b tw-border-border-weak` |
|
||||
| TableRow | default | `tw-transition-colors` |
|
||||
| TableRow | hover | `hover:tw-bg-interactive-hover` |
|
||||
| TableRow | selected | `data-[state=selected]:tw-bg-interactive-selected` |
|
||||
| TableHead | default | `tw-h-10 tw-px-3.5 tw-py-0 tw-text-left tw-align-middle tw-font-title-default tw-text-text-default` |
|
||||
| TableHead | compact | `tw-h-8 tw-px-3 tw-py-0` |
|
||||
| TableCell | default | `tw-h-[52px] tw-p-3.5 tw-align-middle tw-text-text-default first:tw-rounded-l-[10px] last:tw-rounded-r-[10px]` |
|
||||
| TableCell | compact | `tw-h-9 tw-px-3 tw-py-2 first:tw-rounded-l-[10px] last:tw-rounded-r-[10px]` |
|
||||
| TableFooter | default | `tw-border-solid tw-border-0 tw-border-t tw-border-border-weak tw-bg-background-surface-layer-02 tw-font-title-default` |
|
||||
| TableCaption | default | `tw-mt-4 tw-font-body-small tw-text-text-placeholder` |
|
||||
|
||||
## CVA Shape
|
||||
|
||||
Shape C — sizes only (`density`). Plus a `TableDensityContext` so child components (`TableHead`, `TableCell`) read density from the root `Table`.
|
||||
|
||||
## Slots
|
||||
|
||||
Standard HTML table slots — consumer composes via sub-components.
|
||||
|
||||
## Notes
|
||||
|
||||
- Wraps the shadcn `table` primitive (installed at `Rocket/shadcn/table.jsx`).
|
||||
- Replaces all shadcn semantic classes (`tw-bg-muted`, `tw-text-foreground`) with ToolJet tokens via className override.
|
||||
- `density` prop controls row/cell heights via context — font stays `12px Medium` (`tw-font-title-default`) on header and `12px Regular` (`tw-font-body-default`) on body across both densities.
|
||||
- **Borderless rows** — rows do NOT have bottom borders. Visual separation comes from the hover/selected background which uses `rounded-[10px]` corners on the first/last cells, creating a "pill" highlight.
|
||||
- The `<table>` itself uses `tw-border-separate tw-border-spacing-0` so the rounded cell corners render correctly (the default `border-collapse` collapses cell borders and breaks the rounded corners).
|
||||
- Header has a single `border-bottom` (`tw-border-border-weak`) on `TableHeader` — separates header from body.
|
||||
- Hover and selected states use `tw-bg-interactive-hover` and `tw-bg-interactive-selected`.
|
||||
- `TableFooter` uses surface-layer-02 bg + medium font weight (slightly emphasized).
|
||||
- Higher-level features (sorting indicators, sticky header, loading skeleton, empty state, pagination) live in the `DataTable` block (layer 2), not here.
|
||||
- Typography uses `tw-font-*` plugin utilities — never manual font combos.
|
||||
157
frontend/src/components/ui/Rocket/Table/Table.stories.jsx
Normal file
157
frontend/src/components/ui/Rocket/Table/Table.stories.jsx
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import React from 'react';
|
||||
import { Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption } from './Table';
|
||||
|
||||
export default {
|
||||
title: 'Rocket/Table',
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'padded' },
|
||||
};
|
||||
|
||||
const sampleRows = [
|
||||
{ name: 'My first app', editedBy: 'Alice', editedAt: 'Edited 2m ago', type: 'App' },
|
||||
{ name: 'Customer dashboard', editedBy: 'Bob', editedAt: 'Edited 1h ago', type: 'App' },
|
||||
{ name: 'Sales pipeline', editedBy: 'Carol', editedAt: 'Edited yesterday', type: 'Workflow' },
|
||||
{ name: 'HR onboarding', editedBy: 'Dan', editedAt: 'Edited 3 days ago', type: 'Workflow' },
|
||||
];
|
||||
|
||||
// ── Default ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export const Default = {
|
||||
render: () => (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Edited by</TableHead>
|
||||
<TableHead>Edited at</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{sampleRows.map((row) => (
|
||||
<TableRow key={row.name}>
|
||||
<TableCell>{row.name}</TableCell>
|
||||
<TableCell>{row.type}</TableCell>
|
||||
<TableCell>{row.editedBy}</TableCell>
|
||||
<TableCell>{row.editedAt}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
),
|
||||
};
|
||||
|
||||
// ── Compact density ─────────────────────────────────────────────────────────
|
||||
|
||||
export const Compact = {
|
||||
render: () => (
|
||||
<Table density="compact">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Edited by</TableHead>
|
||||
<TableHead>Edited at</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{sampleRows.map((row) => (
|
||||
<TableRow key={row.name}>
|
||||
<TableCell>{row.name}</TableCell>
|
||||
<TableCell>{row.type}</TableCell>
|
||||
<TableCell>{row.editedBy}</TableCell>
|
||||
<TableCell>{row.editedAt}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
),
|
||||
};
|
||||
|
||||
// ── Selected row ────────────────────────────────────────────────────────────
|
||||
|
||||
export const SelectedRow = {
|
||||
render: () => (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Edited by</TableHead>
|
||||
<TableHead>Edited at</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{sampleRows.map((row, i) => (
|
||||
<TableRow key={row.name} data-state={i === 1 ? 'selected' : undefined}>
|
||||
<TableCell>{row.name}</TableCell>
|
||||
<TableCell>{row.type}</TableCell>
|
||||
<TableCell>{row.editedBy}</TableCell>
|
||||
<TableCell>{row.editedAt}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
),
|
||||
};
|
||||
|
||||
// ── With Footer ─────────────────────────────────────────────────────────────
|
||||
|
||||
export const WithFooter = {
|
||||
render: () => (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Item</TableHead>
|
||||
<TableHead>Quantity</TableHead>
|
||||
<TableHead>Price</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>Apples</TableCell>
|
||||
<TableCell>3</TableCell>
|
||||
<TableCell>$3.00</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Oranges</TableCell>
|
||||
<TableCell>2</TableCell>
|
||||
<TableCell>$4.00</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
<TableCell>Total</TableCell>
|
||||
<TableCell>5</TableCell>
|
||||
<TableCell>$7.00</TableCell>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
),
|
||||
};
|
||||
|
||||
// ── With Caption ────────────────────────────────────────────────────────────
|
||||
|
||||
export const WithCaption = {
|
||||
render: () => (
|
||||
<Table>
|
||||
<TableCaption>A list of recent apps from your workspace.</TableCaption>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Edited by</TableHead>
|
||||
<TableHead>Edited at</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{sampleRows.slice(0, 3).map((row) => (
|
||||
<TableRow key={row.name}>
|
||||
<TableCell>{row.name}</TableCell>
|
||||
<TableCell>{row.editedBy}</TableCell>
|
||||
<TableCell>{row.editedAt}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
),
|
||||
};
|
||||
47
frontend/src/components/ui/Rocket/Textarea/Textarea.jsx
Normal file
47
frontend/src/components/ui/Rocket/Textarea/Textarea.jsx
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import React, { forwardRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { cva } from 'class-variance-authority';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Textarea as ShadcnTextarea } from '@/components/ui/Rocket/shadcn/textarea';
|
||||
|
||||
const textareaVariants = cva(
|
||||
[
|
||||
// Resets not covered by shadcn base (preflight is off)
|
||||
'tw-appearance-none tw-border-solid tw-outline-none',
|
||||
// Base tokens
|
||||
'tw-bg-background-surface-layer-01 tw-border-border-default tw-text-text-default tw-shadow-elevation-none',
|
||||
'tw-text-base',
|
||||
'placeholder:tw-text-text-placeholder',
|
||||
// Override shadcn focus ring with ToolJet token
|
||||
'focus-visible:tw-ring-2 focus-visible:tw-ring-interactive-focus-outline !focus-visible:tw-outline-none',
|
||||
// Hover
|
||||
'hover:tw-border-border-strong',
|
||||
// Error (via aria-invalid)
|
||||
'aria-[invalid=true]:tw-border-border-danger-strong aria-[invalid=true]:tw-bg-background-error-weak',
|
||||
// Disabled
|
||||
'disabled:tw-bg-background-surface-layer-02 disabled:tw-text-text-disabled disabled:tw-border-border-disabled',
|
||||
],
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
large: 'tw-px-3 tw-py-2.5 tw-text-lg',
|
||||
default: 'tw-px-3 tw-py-2 tw-text-base',
|
||||
small: 'tw-px-3 tw-py-1.5 tw-text-base',
|
||||
},
|
||||
},
|
||||
defaultVariants: { size: 'default' },
|
||||
}
|
||||
);
|
||||
|
||||
const Textarea = forwardRef(function Textarea({ className, size, ...props }, ref) {
|
||||
return <ShadcnTextarea ref={ref} className={cn(textareaVariants({ size }), className)} {...props} />;
|
||||
});
|
||||
|
||||
Textarea.displayName = 'Textarea';
|
||||
|
||||
Textarea.propTypes = {
|
||||
size: PropTypes.oneOf(['large', 'default', 'small']),
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export { Textarea, textareaVariants };
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
import React from 'react';
|
||||
import { Textarea } from './Textarea';
|
||||
import { Field, FieldLabel, FieldDescription, FieldError } from '../Field/Field';
|
||||
|
||||
export default {
|
||||
title: 'Rocket/Textarea',
|
||||
component: Textarea,
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'centered' },
|
||||
argTypes: {
|
||||
size: {
|
||||
control: 'select',
|
||||
options: ['large', 'default', 'small'],
|
||||
},
|
||||
disabled: { control: 'boolean' },
|
||||
},
|
||||
};
|
||||
|
||||
// ── Default ───────────────────────────────────────────────────────────────
|
||||
export const Default = {
|
||||
args: { placeholder: 'Enter text...' },
|
||||
};
|
||||
|
||||
// ── Sizes ─────────────────────────────────────────────────────────────────
|
||||
export const Sizes = {
|
||||
render: () => (
|
||||
<div className="tw-flex tw-flex-col tw-gap-3 tw-w-72 tw-p-4">
|
||||
<Textarea size="large" placeholder="Large" />
|
||||
<Textarea size="default" placeholder="Default" />
|
||||
<Textarea size="small" placeholder="Small" />
|
||||
</div>
|
||||
),
|
||||
parameters: { layout: 'padded' },
|
||||
};
|
||||
|
||||
// ── States ────────────────────────────────────────────────────────────────
|
||||
export const States = {
|
||||
render: () => (
|
||||
<div className="tw-flex tw-flex-col tw-gap-3 tw-w-72 tw-p-4">
|
||||
<Textarea placeholder="Default" />
|
||||
<Textarea defaultValue="With content - this textarea has some longer text to show how it wraps across multiple lines." />
|
||||
<Textarea placeholder="With error" aria-invalid="true" />
|
||||
<Textarea placeholder="Disabled" disabled />
|
||||
</div>
|
||||
),
|
||||
parameters: { layout: 'padded' },
|
||||
};
|
||||
|
||||
// ── With Field ────────────────────────────────────────────────────────────
|
||||
export const WithField = {
|
||||
render: () => (
|
||||
<div className="tw-w-80 tw-flex tw-flex-col tw-gap-6 tw-p-4">
|
||||
<Field>
|
||||
<FieldLabel>Description</FieldLabel>
|
||||
<Textarea placeholder="Enter a description..." />
|
||||
<FieldDescription>Brief summary of the item.</FieldDescription>
|
||||
</Field>
|
||||
|
||||
<Field data-invalid="true">
|
||||
<FieldLabel>Notes</FieldLabel>
|
||||
<Textarea placeholder="Add notes..." aria-invalid="true" />
|
||||
<FieldError>This field is required.</FieldError>
|
||||
</Field>
|
||||
</div>
|
||||
),
|
||||
parameters: { layout: 'padded' },
|
||||
};
|
||||
|
||||
// ── Resizable ─────────────────────────────────────────────────────────────
|
||||
export const Resizable = {
|
||||
render: () => (
|
||||
<div className="tw-flex tw-flex-col tw-gap-3 tw-w-72 tw-p-4">
|
||||
<Textarea placeholder="Vertical resize (default)" />
|
||||
<Textarea placeholder="No resize" className="tw-resize-none" />
|
||||
<Textarea placeholder="Both directions" className="tw-resize" />
|
||||
</div>
|
||||
),
|
||||
parameters: { layout: 'padded' },
|
||||
};
|
||||
|
|
@ -148,3 +148,56 @@ export {
|
|||
tabsContentClasses,
|
||||
} from './Tabs/Tabs';
|
||||
export { Switch, switchClasses } from './Switch/Switch';
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
alertDialogContentVariants,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogMedia,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
} from './AlertDialog/AlertDialog';
|
||||
export {
|
||||
Collapsible,
|
||||
collapsibleVariants,
|
||||
CollapsibleTrigger,
|
||||
collapsibleTriggerVariants,
|
||||
CollapsibleIcon,
|
||||
CollapsibleContent,
|
||||
collapsibleContentVariants,
|
||||
} from './Collapsible/Collapsible';
|
||||
export { Toaster, toast } from './Sonner/Sonner';
|
||||
export {
|
||||
Sheet,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetPortal,
|
||||
SheetOverlay,
|
||||
SheetContent,
|
||||
sheetContentVariants,
|
||||
SheetHeader,
|
||||
SheetBody,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
} from './Sheet/Sheet';
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableRow,
|
||||
TableHead,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
} from './Table/Table';
|
||||
export { Skeleton, skeletonClasses } from './Skeleton/Skeleton';
|
||||
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';
|
||||
|
|
|
|||
143
frontend/src/components/ui/Rocket/shadcn/alert-dialog.jsx
Normal file
143
frontend/src/components/ui/Rocket/shadcn/alert-dialog.jsx
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
import * as React from 'react';
|
||||
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/Rocket/shadcn/button';
|
||||
|
||||
function AlertDialog({ ...props }) {
|
||||
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
|
||||
}
|
||||
|
||||
function AlertDialogTrigger({ ...props }) {
|
||||
return <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function AlertDialogPortal({ ...props }) {
|
||||
return <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />;
|
||||
}
|
||||
|
||||
function AlertDialogOverlay({ className, ...props }) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
data-slot="alert-dialog-overlay"
|
||||
className={cn(
|
||||
'tw-fixed tw-inset-0 tw-z-50 tw-bg-black/10 tw-duration-100 supports-backdrop-filter:tw-backdrop-blur-xs data-[open]:tw-animate-in data-[open]:tw-fade-in-0 data-[closed]:tw-animate-out data-[closed]:tw-fade-out-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogContent({ className, size = 'default', ...props }) {
|
||||
return (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
data-slot="alert-dialog-content"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
'tw-group/alert-dialog-content tw-fixed tw-top-1/2 tw-left-1/2 tw-z-50 tw-grid tw-w-full tw--translate-x-1/2 tw--translate-y-1/2 tw-gap-4 tw-rounded-xl tw-bg-popover tw-p-4 tw-text-popover-foreground tw-ring-1 tw-ring-foreground/10 tw-duration-100 tw-outline-none data-[size=default]:tw-max-w-xs data-[size=sm]:tw-max-w-xs data-[size=default]:sm:tw-max-w-sm data-[open]:tw-animate-in data-[open]:tw-fade-in-0 data-[open]:tw-zoom-in-95 data-[closed]:tw-animate-out data-[closed]:tw-fade-out-0 data-[closed]:tw-zoom-out-95',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogHeader({ className, ...props }) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-header"
|
||||
className={cn(
|
||||
'tw-grid tw-grid-rows-[auto_1fr] tw-place-items-center tw-gap-1.5 tw-text-center has-[[data-slot=alert-dialog-media]]:tw-grid-rows-[auto_auto_1fr] has-[[data-slot=alert-dialog-media]]:tw-gap-x-4 sm:group-data-[size=default]/alert-dialog-content:tw-place-items-start sm:group-data-[size=default]/alert-dialog-content:tw-text-left sm:group-data-[size=default]/alert-dialog-content:has-[[data-slot=alert-dialog-media]]:tw-grid-rows-[auto_1fr]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogFooter({ className, ...props }) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-footer"
|
||||
className={cn(
|
||||
'tw--mx-4 tw--mb-4 tw-flex tw-flex-col-reverse tw-gap-2 tw-rounded-b-xl tw-border-t tw-bg-muted/50 tw-p-4 group-data-[size=sm]/alert-dialog-content:tw-grid group-data-[size=sm]/alert-dialog-content:tw-grid-cols-2 sm:tw-flex-row sm:tw-justify-end',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogMedia({ className, ...props }) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-media"
|
||||
className={cn(
|
||||
'tw-mb-2 tw-inline-flex tw-size-10 tw-items-center tw-justify-center tw-rounded-md tw-bg-muted sm:group-data-[size=default]/alert-dialog-content:tw-row-span-2 *:[svg:not([class*=size-])]:tw-size-6',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogTitle({ className, ...props }) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Title
|
||||
data-slot="alert-dialog-title"
|
||||
className={cn(
|
||||
'tw-text-base tw-font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-[[data-slot=alert-dialog-media]]/alert-dialog-content:tw-col-start-2',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogDescription({ className, ...props }) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Description
|
||||
data-slot="alert-dialog-description"
|
||||
className={cn(
|
||||
'tw-text-sm tw-text-balance tw-text-muted-foreground md:tw-text-pretty *:[a]:tw-underline *:[a]:tw-underline-offset-3 *:[a]:hover:tw-text-foreground',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogAction({ className, variant = 'default', size = 'default', ...props }) {
|
||||
return (
|
||||
<Button variant={variant} size={size} asChild>
|
||||
<AlertDialogPrimitive.Action data-slot="alert-dialog-action" className={cn(className)} {...props} />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogCancel({ className, variant = 'outline', size = 'default', ...props }) {
|
||||
return (
|
||||
<Button variant={variant} size={size} asChild>
|
||||
<AlertDialogPrimitive.Cancel data-slot="alert-dialog-cancel" className={cn(className)} {...props} />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogMedia,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogPortal,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
};
|
||||
27
frontend/src/components/ui/Rocket/shadcn/checkbox.jsx
Normal file
27
frontend/src/components/ui/Rocket/shadcn/checkbox.jsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import * as React from 'react';
|
||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { CheckIcon } from 'lucide-react';
|
||||
|
||||
function Checkbox({ className, ...props }) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
'tw-peer tw-relative tw-flex tw-size-4 tw-shrink-0 tw-items-center tw-justify-center tw-rounded-[4px] tw-border tw-border-input tw-transition-colors tw-outline-none focus-visible:tw-border-ring focus-visible:tw-ring-2 focus-visible:tw-ring-ring/50 disabled:tw-cursor-not-allowed disabled:tw-opacity-50 aria-invalid:tw-border-destructive aria-invalid:tw-ring-2 aria-invalid:tw-ring-destructive/20 data-[state=checked]:tw-border-primary data-[state=checked]:tw-bg-primary data-[state=checked]:tw-text-primary-foreground data-[state=indeterminate]:tw-border-primary data-[state=indeterminate]:tw-bg-primary data-[state=indeterminate]:tw-text-primary-foreground',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="tw-grid tw-place-content-center tw-text-current tw-transition-none [&>svg]:tw-size-3.5"
|
||||
>
|
||||
<CheckIcon />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export { Checkbox };
|
||||
16
frontend/src/components/ui/Rocket/shadcn/collapsible.jsx
Normal file
16
frontend/src/components/ui/Rocket/shadcn/collapsible.jsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import * as React from 'react';
|
||||
import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';
|
||||
|
||||
function Collapsible({ ...props }) {
|
||||
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
|
||||
}
|
||||
|
||||
function CollapsibleTrigger({ ...props }) {
|
||||
return <CollapsiblePrimitive.Trigger data-slot="collapsible-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function CollapsibleContent({ ...props }) {
|
||||
return <CollapsiblePrimitive.Content data-slot="collapsible-content" {...props} />;
|
||||
}
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
||||
36
frontend/src/components/ui/Rocket/shadcn/radio-group.jsx
Normal file
36
frontend/src/components/ui/Rocket/shadcn/radio-group.jsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import * as React from 'react';
|
||||
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function RadioGroup({ className, ...props }) {
|
||||
return (
|
||||
<RadioGroupPrimitive.Root
|
||||
data-slot="radio-group"
|
||||
className={cn('tw-grid tw-w-full tw-gap-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function RadioGroupItem({ className, ...props }) {
|
||||
return (
|
||||
<RadioGroupPrimitive.Item
|
||||
data-slot="radio-group-item"
|
||||
className={cn(
|
||||
'tw-peer tw-relative tw-flex tw-aspect-square tw-size-4 tw-shrink-0 tw-rounded-full tw-border tw-border-input tw-outline-none focus-visible:tw-border-ring focus-visible:tw-ring-2 focus-visible:tw-ring-ring/50 disabled:tw-cursor-not-allowed disabled:tw-opacity-50 data-[state=checked]:tw-border-primary data-[state=checked]:tw-bg-primary data-[state=checked]:tw-text-primary-foreground',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator
|
||||
data-slot="radio-group-indicator"
|
||||
className="tw-flex tw-size-4 tw-items-center tw-justify-center"
|
||||
>
|
||||
<span className="tw-absolute tw-top-1/2 tw-left-1/2 tw-size-2 tw--translate-x-1/2 tw--translate-y-1/2 tw-rounded-full tw-bg-primary-foreground" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
);
|
||||
}
|
||||
|
||||
export { RadioGroup, RadioGroupItem };
|
||||
109
frontend/src/components/ui/Rocket/shadcn/sheet.jsx
Normal file
109
frontend/src/components/ui/Rocket/shadcn/sheet.jsx
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import * as React from 'react';
|
||||
import * as SheetPrimitive from '@radix-ui/react-dialog';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/Rocket/shadcn/button';
|
||||
import { XIcon } from 'lucide-react';
|
||||
|
||||
function Sheet({ ...props }) {
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
|
||||
}
|
||||
|
||||
function SheetTrigger({ ...props }) {
|
||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function SheetClose({ ...props }) {
|
||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
|
||||
}
|
||||
|
||||
function SheetPortal({ ...props }) {
|
||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
|
||||
}
|
||||
|
||||
function SheetOverlay({ className, ...props }) {
|
||||
return (
|
||||
<SheetPrimitive.Overlay
|
||||
data-slot="sheet-overlay"
|
||||
className={cn(
|
||||
'tw-fixed tw-inset-0 tw-z-50 tw-bg-black/10 tw-duration-100 supports-backdrop-filter:tw-backdrop-blur-sm data-[state=open]:tw-animate-in data-[state=open]:tw-fade-in-0 data-[state=closed]:tw-animate-out data-[state=closed]:tw-fade-out-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SheetContent({ className, children, side = 'right', showCloseButton = true, ...props }) {
|
||||
return (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
data-slot="sheet-content"
|
||||
data-side={side}
|
||||
className={cn(
|
||||
'tw-fixed tw-z-50 tw-flex tw-flex-col tw-gap-4 tw-bg-popover tw-bg-clip-padding tw-text-sm tw-text-popover-foreground tw-shadow-lg tw-transition tw-duration-200 tw-ease-in-out data-[side=bottom]:tw-inset-x-0 data-[side=bottom]:tw-bottom-0 data-[side=bottom]:tw-h-auto data-[side=bottom]:tw-border-t data-[side=left]:tw-inset-y-0 data-[side=left]:tw-left-0 data-[side=left]:tw-h-full data-[side=left]:tw-w-3/4 data-[side=left]:tw-border-r data-[side=right]:tw-inset-y-0 data-[side=right]:tw-right-0 data-[side=right]:tw-h-full data-[side=right]:tw-w-3/4 data-[side=right]:tw-border-l data-[side=top]:tw-inset-x-0 data-[side=top]:tw-top-0 data-[side=top]:tw-h-auto data-[side=top]:tw-border-b data-[side=left]:sm:tw-max-w-sm data-[side=right]:sm:tw-max-w-sm data-[state=open]:tw-animate-in data-[state=open]:tw-fade-in-0 data-[side=bottom]:data-[state=open]:tw-slide-in-from-bottom-10 data-[side=left]:data-[state=open]:tw-slide-in-from-left-10 data-[side=right]:data-[state=open]:tw-slide-in-from-right-10 data-[side=top]:data-[state=open]:tw-slide-in-from-top-10 data-[state=closed]:tw-animate-out data-[state=closed]:tw-fade-out-0 data-[side=bottom]:data-[state=closed]:tw-slide-out-to-bottom-10 data-[side=left]:data-[state=closed]:tw-slide-out-to-left-10 data-[side=right]:data-[state=closed]:tw-slide-out-to-right-10 data-[side=top]:data-[state=closed]:tw-slide-out-to-top-10',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<SheetPrimitive.Close data-slot="sheet-close" asChild>
|
||||
<Button variant="ghost" className="tw-absolute tw-top-3 tw-right-3" size="icon-sm">
|
||||
<XIcon />
|
||||
<span className="tw-sr-only">Close</span>
|
||||
</Button>
|
||||
</SheetPrimitive.Close>
|
||||
)}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function SheetHeader({ className, ...props }) {
|
||||
return <div data-slot="sheet-header" className={cn('tw-flex tw-flex-col tw-gap-0.5 tw-p-4', className)} {...props} />;
|
||||
}
|
||||
|
||||
function SheetFooter({ className, ...props }) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-footer"
|
||||
className={cn('tw-mt-auto tw-flex tw-flex-col tw-gap-2 tw-p-4', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SheetTitle({ className, ...props }) {
|
||||
return (
|
||||
<SheetPrimitive.Title
|
||||
data-slot="sheet-title"
|
||||
className={cn('tw-text-base tw-font-medium tw-text-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SheetDescription({ className, ...props }) {
|
||||
return (
|
||||
<SheetPrimitive.Description
|
||||
data-slot="sheet-description"
|
||||
className={cn('tw-text-sm tw-text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetPortal,
|
||||
SheetOverlay,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
};
|
||||
10
frontend/src/components/ui/Rocket/shadcn/skeleton.jsx
Normal file
10
frontend/src/components/ui/Rocket/shadcn/skeleton.jsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import * as React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Skeleton({ className, ...props }) {
|
||||
return (
|
||||
<div data-slot="skeleton" className={cn('tw-animate-pulse tw-rounded-md tw-bg-muted', className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export { Skeleton };
|
||||
32
frontend/src/components/ui/Rocket/shadcn/sonner.jsx
Normal file
32
frontend/src/components/ui/Rocket/shadcn/sonner.jsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import * as React from 'react';
|
||||
import { Toaster as Sonner } from 'sonner';
|
||||
import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from 'lucide-react';
|
||||
|
||||
const Toaster = ({ ...props }) => {
|
||||
return (
|
||||
<Sonner
|
||||
className="tw-toaster tw-group"
|
||||
icons={{
|
||||
success: <CircleCheckIcon className="tw-size-4" />,
|
||||
info: <InfoIcon className="tw-size-4" />,
|
||||
warning: <TriangleAlertIcon className="tw-size-4" />,
|
||||
error: <OctagonXIcon className="tw-size-4" />,
|
||||
loading: <Loader2Icon className="tw-size-4 tw-animate-spin" />,
|
||||
}}
|
||||
style={{
|
||||
'--normal-bg': 'var(--popover)',
|
||||
'--normal-text': 'var(--popover-foreground)',
|
||||
'--normal-border': 'var(--border)',
|
||||
'--border-radius': 'var(--radius)',
|
||||
}}
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast: 'cn-toast',
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { Toaster };
|
||||
77
frontend/src/components/ui/Rocket/shadcn/table.jsx
Normal file
77
frontend/src/components/ui/Rocket/shadcn/table.jsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Table({ className, ...props }) {
|
||||
return (
|
||||
<div data-slot="table-container" className="tw-relative tw-w-full tw-overflow-x-auto">
|
||||
<table data-slot="table" className={cn('tw-w-full tw-caption-bottom tw-text-sm', className)} {...props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TableHeader({ className, ...props }) {
|
||||
return <thead data-slot="table-header" className={cn('[&_tr]:tw-border-b', className)} {...props} />;
|
||||
}
|
||||
|
||||
function TableBody({ className, ...props }) {
|
||||
return <tbody data-slot="table-body" className={cn('[&_tr:last-child]:tw-border-0', className)} {...props} />;
|
||||
}
|
||||
|
||||
function TableFooter({ className, ...props }) {
|
||||
return (
|
||||
<tfoot
|
||||
data-slot="table-footer"
|
||||
className={cn('tw-border-t tw-bg-muted/50 tw-font-medium [&>tr]:last:tw-border-b-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableRow({ className, ...props }) {
|
||||
return (
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
className={cn(
|
||||
'tw-border-b tw-transition-colors hover:tw-bg-muted/50 has-[[aria-expanded]]:tw-bg-muted/50 data-[state=selected]:tw-bg-muted',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableHead({ className, ...props }) {
|
||||
return (
|
||||
<th
|
||||
data-slot="table-head"
|
||||
className={cn(
|
||||
'tw-h-10 tw-px-2 tw-text-left tw-align-middle tw-font-medium tw-whitespace-nowrap tw-text-foreground [&:has([role=checkbox])]:tw-pr-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableCell({ className, ...props }) {
|
||||
return (
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
className={cn('tw-p-2 tw-align-middle tw-whitespace-nowrap [&:has([role=checkbox])]:tw-pr-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableCaption({ className, ...props }) {
|
||||
return (
|
||||
<caption
|
||||
data-slot="table-caption"
|
||||
className={cn('tw-mt-4 tw-text-sm tw-text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };
|
||||
78
frontend/src/components/ui/blocks/DataTable/DataTable.jsx
Normal file
78
frontend/src/components/ui/blocks/DataTable/DataTable.jsx
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import * as React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { flexRender } from '@tanstack/react-table';
|
||||
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/Rocket/Table/Table';
|
||||
import { TableSkeleton } from '@/components/ui/blocks/TableSkeleton';
|
||||
|
||||
/**
|
||||
* DataTable — TanStack-driven table block.
|
||||
*
|
||||
* Pass a TanStack `table` instance (from `useReactTable`). This block renders
|
||||
* the header, body (or skeleton during loading), and a "no results" empty state.
|
||||
*
|
||||
* Higher-level features (sorting indicators, pagination controls, toolbar) are
|
||||
* the responsibility of the consuming feature, not this block.
|
||||
*/
|
||||
function DataTableInternal({ table, isLoading = false, skeleton, density = 'default', emptyMessage = 'No results.' }) {
|
||||
const columnCount = table?.getAllColumns()?.length || 4;
|
||||
|
||||
return (
|
||||
<div className="tw-w-full">
|
||||
<Table density={density}>
|
||||
<TableHeader className="tw-sticky tw-top-0 tw-z-10">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
colSpan={header.colSpan}
|
||||
style={header.getSize() ? { width: `${header.getSize()}px` } : undefined}
|
||||
>
|
||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
|
||||
{isLoading ? (
|
||||
skeleton || <TableSkeleton rowCount={5} columnCount={columnCount} />
|
||||
) : (
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id} data-state={row.getIsSelected() ? 'selected' : undefined}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columnCount}
|
||||
className="tw-h-24 tw-text-center tw-text-text-placeholder tw-font-body-default"
|
||||
>
|
||||
{emptyMessage}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
)}
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
DataTableInternal.displayName = 'DataTable';
|
||||
DataTableInternal.propTypes = {
|
||||
// TanStack `table` instance — required, but PropTypes can't validate object shape easily
|
||||
// eslint-disable-next-line react/forbid-prop-types
|
||||
table: PropTypes.object.isRequired,
|
||||
isLoading: PropTypes.bool,
|
||||
skeleton: PropTypes.node,
|
||||
density: PropTypes.oneOf(['default', 'compact']),
|
||||
emptyMessage: PropTypes.string,
|
||||
};
|
||||
|
||||
export const DataTable = React.memo(DataTableInternal);
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import React from 'react';
|
||||
import { useReactTable, getCoreRowModel, createColumnHelper } from '@tanstack/react-table';
|
||||
import { DataTable } from './DataTable';
|
||||
|
||||
export default {
|
||||
title: 'Blocks/DataTable',
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'padded' },
|
||||
};
|
||||
|
||||
const sampleData = [
|
||||
{ id: 1, name: 'My first app', type: 'App', editedBy: 'Alice', editedAt: 'Edited 2m ago' },
|
||||
{ id: 2, name: 'Customer dashboard', type: 'App', editedBy: 'Bob', editedAt: 'Edited 1h ago' },
|
||||
{ id: 3, name: 'Sales pipeline', type: 'Workflow', editedBy: 'Carol', editedAt: 'Edited yesterday' },
|
||||
{ id: 4, name: 'HR onboarding', type: 'Workflow', editedBy: 'Dan', editedAt: 'Edited 3 days ago' },
|
||||
{ id: 5, name: 'Inventory tracker', type: 'App', editedBy: 'Eve', editedAt: 'Edited last week' },
|
||||
];
|
||||
|
||||
const columnHelper = createColumnHelper();
|
||||
const columns = [
|
||||
columnHelper.accessor('name', { header: 'Name', cell: (info) => info.getValue() }),
|
||||
columnHelper.accessor('type', { header: 'Type', cell: (info) => info.getValue() }),
|
||||
columnHelper.accessor('editedBy', { header: 'Edited by', cell: (info) => info.getValue() }),
|
||||
columnHelper.accessor('editedAt', { header: 'Edited at', cell: (info) => info.getValue() }),
|
||||
];
|
||||
|
||||
function useTable(data) {
|
||||
return useReactTable({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
}
|
||||
|
||||
// ── Default ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export const Default = {
|
||||
render: () => {
|
||||
const table = useTable(sampleData);
|
||||
return <DataTable table={table} />;
|
||||
},
|
||||
};
|
||||
|
||||
// ── Compact ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export const Compact = {
|
||||
render: () => {
|
||||
const table = useTable(sampleData);
|
||||
return <DataTable table={table} density="compact" />;
|
||||
},
|
||||
};
|
||||
|
||||
// ── Loading ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export const Loading = {
|
||||
render: () => {
|
||||
const table = useTable([]);
|
||||
return <DataTable table={table} isLoading />;
|
||||
},
|
||||
};
|
||||
|
||||
// ── Empty ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export const Empty = {
|
||||
render: () => {
|
||||
const table = useTable([]);
|
||||
return <DataTable table={table} emptyMessage="No apps found. Create one to get started." />;
|
||||
},
|
||||
};
|
||||
1
frontend/src/components/ui/blocks/DataTable/index.js
Normal file
1
frontend/src/components/ui/blocks/DataTable/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { DataTable } from './DataTable';
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import * as React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { TableBody, TableCell, TableRow } from '@/components/ui/Rocket/Table/Table';
|
||||
import { Skeleton } from '@/components/ui/Rocket/Skeleton/Skeleton';
|
||||
|
||||
/**
|
||||
* Generic table skeleton — renders pulsing placeholder rows.
|
||||
* Designed to be used inside a `<Table>` while data is loading,
|
||||
* replacing the real `<TableBody>`.
|
||||
*/
|
||||
function TableSkeleton({ rowCount = 5, columnCount = 4 }) {
|
||||
const rows = React.useMemo(
|
||||
() => Array.from({ length: rowCount }, (_, i) => ({ id: `table-skeleton-${i}` })),
|
||||
[rowCount]
|
||||
);
|
||||
|
||||
return (
|
||||
<TableBody>
|
||||
{rows.map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
{Array.from({ length: columnCount }, (_, colIndex) => (
|
||||
<TableCell key={colIndex}>
|
||||
<Skeleton className="tw-h-4 tw-w-full" />
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
);
|
||||
}
|
||||
TableSkeleton.displayName = 'TableSkeleton';
|
||||
TableSkeleton.propTypes = {
|
||||
rowCount: PropTypes.number,
|
||||
columnCount: PropTypes.number,
|
||||
};
|
||||
|
||||
export { TableSkeleton };
|
||||
1
frontend/src/components/ui/blocks/TableSkeleton/index.js
Normal file
1
frontend/src/components/ui/blocks/TableSkeleton/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { TableSkeleton } from './TableSkeleton';
|
||||
Loading…
Reference in a new issue