diff --git a/apps/console/src/app/(protected)/programs/controls/page.tsx b/apps/console/src/app/(protected)/programs/controls/page.tsx index 06fb1145..e3fac38c 100644 --- a/apps/console/src/app/(protected)/programs/controls/page.tsx +++ b/apps/console/src/app/(protected)/programs/controls/page.tsx @@ -1,8 +1,14 @@ import React from 'react' import { PageHeading } from '@repo/ui/page-heading' +import ControlsTable from '@/components/pages/protected/program/controls/controls-table' const Page: React.FC = () => { - return + return ( + <> + + + + ) } export default Page diff --git a/apps/console/src/components/pages/protected/program/controls/controls-table.tsx b/apps/console/src/components/pages/protected/program/controls/controls-table.tsx new file mode 100644 index 00000000..cf6c172c --- /dev/null +++ b/apps/console/src/components/pages/protected/program/controls/controls-table.tsx @@ -0,0 +1,189 @@ +'use client' + +import { Avatar, AvatarFallback, AvatarImage } from '@repo/ui/avatar' +import { Badge } from '@repo/ui/badge' +import { Button } from '@repo/ui/button' +import { DataTable } from '@repo/ui/data-table' +import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@repo/ui/sheet' +import { ColumnDef } from '@tanstack/table-core' +import { DownloadIcon, PencilIcon } from 'lucide-react' +import React, { useState } from 'react' + +// Sample data +const data = [ + { + name: 'CC1.2', + ref: 'CC1.2', + description: 'The board of directors demonstrates independence from management and exercises oversight of the development and performance of internal control. (COSO Principle 2)', + tags: ['Security', 'CC1.2', 'Control Environment'], + status: 'Overdue', + updatedBy: 'Sarah Funkhouser', + updatedAt: 'less than a day', + createdBy: 'Kelsey Waters', + createdAt: 'January 7, 2024 1:22 PM', + owners: [{ avatar: '/path/to/avatar1.png', fallback: 'K' }], + }, + { + name: 'CC1.3', + ref: 'CC1.3', + description: 'Management establishes, with board oversight, structures, reporting lines, and appropriate authorities and responsibilities. (COSO Principle 3)', + tags: ['Governance', 'CC1.3'], + status: 'In Progress', + updatedBy: 'John Doe', + updatedAt: '2 days ago', + createdBy: 'Kelsey Waters', + createdAt: 'January 5, 2024 10:15 AM', + owners: [{ avatar: '/path/to/avatar2.png', fallback: 'S' }], + }, +] + +// Columns definition +const columns: ColumnDef[] = [ + { + header: 'Name', + accessorKey: 'name', + cell: ({ row }) =>
{row.getValue('name')}
, + }, + { + header: 'Ref', + accessorKey: 'ref', + cell: ({ row }) =>
{row.getValue('ref')}
, + }, + { + header: 'Description', + accessorKey: 'description', + cell: ({ row }) => ( +
+

{row.getValue('description')}

+
+ {row.original.tags.map((tag: string, index: number) => ( + + {tag} + + ))} +
+
+ ), + }, + { + header: 'Status', + accessorKey: 'status', + cell: ({ row }) => {row.getValue('status')}, + }, + { + header: 'Owners', + accessorKey: 'owners', + cell: ({ row }) => ( +
+ {row.getValue('owners').map((owner: any, index: number) => ( + + + {owner.fallback} + + ))} +
+ ), + }, +] + +// CSV export utility +const exportToCSV = (data: any[], fileName: string) => { + const csvRows = [] + + csvRows.push(['Name', 'Ref', 'Description', 'Tags', 'Status', 'Owners'].join(',')) + + data.forEach((row) => { + const owners = row.owners.map((o: any) => o.fallback).join(' | ') + csvRows.push([row.name, row.ref, row.description, row.tags.join('; '), row.status, owners].join(',')) + }) + + const blob = new Blob([csvRows.join('\n')], { type: 'text/csv' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `${fileName}.csv` + a.click() + URL.revokeObjectURL(url) +} + +const ControlsTable: React.FC = () => { + const [isSheetOpen, setSheetOpen] = useState(false) + const [currentRow, setCurrentRow] = useState(null) + + const handleRowClick = (row: any) => { + setCurrentRow(row) + setSheetOpen(true) + } + + return ( +
+
+

Controls Table

+ +
+ handleRowClick(row)} /> + + + {currentRow && ( +
+ {/* Header Section */} +
+
+

{currentRow.name}

+ +
+
+
+ {/* Updated Info */} +
+ Updated: + {currentRow.updatedAt} + + + {currentRow.updatedBy[0]} + + {currentRow.updatedBy} +
+ +
+ + {/* Created Info */} +
+ Created: + {currentRow.createdAt} + + + {currentRow.createdBy[0]} + + {currentRow.createdBy} +
+
+ + {/* Description Section */} +

Point of Focus

+

{currentRow.description}

+ + {/* Tags Section */} +
+

Tags

+
+ {currentRow.tags.map((tag: string, index: number) => ( + + {tag} + + ))} +
+
+
+ )} + + +
+ ) +} + +export default ControlsTable diff --git a/bun.lockb b/bun.lockb index 253bbacc..746d11ba 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/packages/ui/package.json b/packages/ui/package.json index a3f8ba18..5c53479c 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -43,6 +43,7 @@ "./simple-form": "./src/simple-form.tsx", "./select": "./src/select/select.tsx", "./separator": "./src/separator/separator.tsx", + "./sheet": "./src/sheet/sheet.tsx", "./slider": "./src/slider/slider.tsx", "./switch": "./src/switch/switch.tsx", "./tabs": "./src/tabs/tabs.tsx", diff --git a/packages/ui/src/data-table/data-table.tsx b/packages/ui/src/data-table/data-table.tsx index 9d4742b8..ea21f8f5 100644 --- a/packages/ui/src/data-table/data-table.tsx +++ b/packages/ui/src/data-table/data-table.tsx @@ -28,9 +28,19 @@ interface DataTableProps { showVisibility?: boolean noResultsText?: string noDataMarkup?: ReactElement + onRowClick?: (rowData: TData) => void } -export function DataTable({ columns, loading = false, data, showFilter = false, showVisibility = false, noResultsText = 'No results', noDataMarkup }: DataTableProps) { +export function DataTable({ + columns, + loading = false, + data, + showFilter = false, + showVisibility = false, + noResultsText = 'No results', + noDataMarkup, + onRowClick, +}: DataTableProps) { const [sorting, setSorting] = useState([]) const [columnFilters, setColumnFilters] = useState([]) const [columnVisibility, setColumnVisibility] = useState({}) @@ -112,7 +122,7 @@ export function DataTable({ columns, loading = false, data, showF {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( - + onRowClick?.(row.original)} key={row.id} data-state={row.getIsSelected() && 'selected'}> {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} diff --git a/packages/ui/src/sheet/sheet.stories.tsx b/packages/ui/src/sheet/sheet.stories.tsx new file mode 100644 index 00000000..ebbff1b6 --- /dev/null +++ b/packages/ui/src/sheet/sheet.stories.tsx @@ -0,0 +1,71 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { Sheet, SheetTrigger, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription } from './sheet' + +const meta: Meta = { + title: 'UI/Sheet', + component: Sheet, + parameters: { + docs: { + description: { + component: 'A versatile sheet component for displaying side panels or modal-like content. Built with Radix UI Dialog and supports configurable sides.', + }, + }, + }, +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + render: () => ( + + Open Sheet + + + Default Sheet + This is a default sheet. Customize its content and behavior as needed. + +
Your content goes here.
+ + + +
+
+ ), +} + +export const WithCustomSide: Story = { + render: () => ( + + Open from Bottom + + + Bottom Sheet + This sheet slides in from the bottom of the screen. + +
Custom side content here.
+ + + +
+
+ ), +} + +export const WithDisabledClose: Story = { + render: () => ( + + Open Sheet + + + Sheet Without Close + The close button is disabled in this example. + +
Your content goes here.
+ + + +
+
+ ), +} diff --git a/packages/ui/src/sheet/sheet.tsx b/packages/ui/src/sheet/sheet.tsx new file mode 100644 index 00000000..7dffce09 --- /dev/null +++ b/packages/ui/src/sheet/sheet.tsx @@ -0,0 +1,73 @@ +import * as React from 'react' +import * as SheetPrimitive from '@radix-ui/react-dialog' +import { cva, type VariantProps } from 'class-variance-authority' +import { X } from 'lucide-react' +import { cn } from '../../lib/utils' + +const Sheet = SheetPrimitive.Root + +const SheetTrigger = SheetPrimitive.Trigger + +const SheetClose = SheetPrimitive.Close + +const SheetPortal = SheetPrimitive.Portal + +const SheetOverlay = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, ...props }, ref) => ( + +)) +SheetOverlay.displayName = SheetPrimitive.Overlay.displayName + +const sheetVariants = cva( + 'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500', + { + variants: { + side: { + top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top', + bottom: 'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom', + left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-[825px]', + right: 'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-[825px]', + }, + }, + defaultVariants: { + side: 'right', + }, + }, +) + +interface SheetContentProps extends React.ComponentPropsWithoutRef, VariantProps {} + +const SheetContent = React.forwardRef, SheetContentProps>(({ side = 'right', className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +SheetContent.displayName = SheetPrimitive.Content.displayName + +const SheetHeader = ({ className, ...props }: React.HTMLAttributes) =>
+SheetHeader.displayName = 'SheetHeader' + +const SheetFooter = ({ className, ...props }: React.HTMLAttributes) =>
+SheetFooter.displayName = 'SheetFooter' + +const SheetTitle = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, ...props }, ref) => ( + +)) +SheetTitle.displayName = SheetPrimitive.Title.displayName + +const SheetDescription = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, ...props }, ref) => ( + +)) +SheetDescription.displayName = SheetPrimitive.Description.displayName + +export { Sheet, SheetPortal, SheetOverlay, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription }