@annondeveloper/ui-kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +215 -0
  3. package/dist/chunk-5OKSXPWK.js +270 -0
  4. package/dist/chunk-5OKSXPWK.js.map +1 -0
  5. package/dist/cli/index.js +430 -0
  6. package/dist/form.d.ts +65 -0
  7. package/dist/form.js +148 -0
  8. package/dist/form.js.map +1 -0
  9. package/dist/index.d.ts +942 -0
  10. package/dist/index.js +2812 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/select-nnBJUO8U.d.ts +26 -0
  13. package/package.json +114 -0
  14. package/src/components/animated-counter.stories.tsx +68 -0
  15. package/src/components/animated-counter.tsx +85 -0
  16. package/src/components/avatar.tsx +106 -0
  17. package/src/components/badge.stories.tsx +70 -0
  18. package/src/components/badge.tsx +97 -0
  19. package/src/components/button.stories.tsx +101 -0
  20. package/src/components/button.tsx +67 -0
  21. package/src/components/card.tsx +128 -0
  22. package/src/components/checkbox.stories.tsx +64 -0
  23. package/src/components/checkbox.tsx +58 -0
  24. package/src/components/confirm-dialog.stories.tsx +96 -0
  25. package/src/components/confirm-dialog.tsx +145 -0
  26. package/src/components/data-table.stories.tsx +125 -0
  27. package/src/components/data-table.tsx +791 -0
  28. package/src/components/dropdown-menu.tsx +111 -0
  29. package/src/components/empty-state.stories.tsx +42 -0
  30. package/src/components/empty-state.tsx +43 -0
  31. package/src/components/filter-pill.stories.tsx +71 -0
  32. package/src/components/filter-pill.tsx +45 -0
  33. package/src/components/form-input.stories.tsx +91 -0
  34. package/src/components/form-input.tsx +77 -0
  35. package/src/components/log-viewer.tsx +212 -0
  36. package/src/components/metric-card.tsx +141 -0
  37. package/src/components/pipeline-stage.tsx +134 -0
  38. package/src/components/popover.tsx +72 -0
  39. package/src/components/port-status-grid.tsx +102 -0
  40. package/src/components/progress.tsx +128 -0
  41. package/src/components/radio-group.tsx +162 -0
  42. package/src/components/select.stories.tsx +52 -0
  43. package/src/components/select.tsx +92 -0
  44. package/src/components/severity-timeline.tsx +125 -0
  45. package/src/components/sheet.tsx +164 -0
  46. package/src/components/skeleton.stories.tsx +64 -0
  47. package/src/components/skeleton.tsx +62 -0
  48. package/src/components/slider.tsx +208 -0
  49. package/src/components/sparkline.tsx +104 -0
  50. package/src/components/status-badge.stories.tsx +84 -0
  51. package/src/components/status-badge.tsx +71 -0
  52. package/src/components/status-pulse.stories.tsx +56 -0
  53. package/src/components/status-pulse.tsx +78 -0
  54. package/src/components/success-checkmark.stories.tsx +67 -0
  55. package/src/components/success-checkmark.tsx +53 -0
  56. package/src/components/tabs.tsx +177 -0
  57. package/src/components/threshold-gauge.tsx +149 -0
  58. package/src/components/time-range-selector.tsx +86 -0
  59. package/src/components/toast.stories.tsx +70 -0
  60. package/src/components/toast.tsx +48 -0
  61. package/src/components/toggle-switch.stories.tsx +66 -0
  62. package/src/components/toggle-switch.tsx +51 -0
  63. package/src/components/tooltip.tsx +62 -0
  64. package/src/components/truncated-text.stories.tsx +56 -0
  65. package/src/components/truncated-text.tsx +80 -0
  66. package/src/components/uptime-tracker.tsx +138 -0
  67. package/src/components/utilization-bar.tsx +103 -0
  68. package/src/theme.css +178 -0
  69. package/src/utils.ts +123 -0
@@ -0,0 +1,67 @@
1
+ 'use client'
2
+
3
+ import { forwardRef, type ButtonHTMLAttributes } from 'react'
4
+ import { Loader2 } from 'lucide-react'
5
+ import { cn } from '../utils'
6
+
7
+ export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'outline' | 'ghost'
8
+ export type ButtonSize = 'sm' | 'md' | 'lg' | 'icon'
9
+
10
+ const variantClasses: Record<ButtonVariant, string> = {
11
+ primary:
12
+ 'bg-[hsl(var(--brand-primary))] text-[hsl(var(--text-on-brand))] hover:bg-[hsl(var(--brand-primary)/0.85)] active:bg-[hsl(var(--brand-primary)/0.75)]',
13
+ secondary:
14
+ 'bg-[hsl(var(--bg-elevated))] text-[hsl(var(--text-primary))] border border-[hsl(var(--border-default))] hover:bg-[hsl(var(--bg-overlay))] active:bg-[hsl(var(--border-subtle))]',
15
+ danger:
16
+ 'bg-[hsl(var(--status-critical))] text-[hsl(var(--text-on-brand))] hover:bg-[hsl(var(--status-critical)/0.85)] active:bg-[hsl(var(--status-critical)/0.75)]',
17
+ outline:
18
+ 'bg-transparent text-[hsl(var(--text-primary))] border border-[hsl(var(--border-default))] hover:bg-[hsl(var(--bg-elevated))] active:bg-[hsl(var(--bg-overlay))]',
19
+ ghost:
20
+ 'bg-transparent text-[hsl(var(--text-secondary))] hover:bg-[hsl(var(--bg-elevated))] hover:text-[hsl(var(--text-primary))] active:bg-[hsl(var(--bg-overlay))]',
21
+ }
22
+
23
+ const sizeClasses: Record<ButtonSize, string> = {
24
+ sm: 'h-8 px-3 text-xs gap-1.5 rounded-lg',
25
+ md: 'h-9 px-4 text-sm gap-2 rounded-lg',
26
+ lg: 'h-11 px-6 text-sm gap-2 rounded-xl',
27
+ icon: 'h-9 w-9 rounded-lg',
28
+ }
29
+
30
+ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
31
+ /** Visual variant of the button. */
32
+ variant?: ButtonVariant
33
+ /** Size preset. */
34
+ size?: ButtonSize
35
+ /** Show a loading spinner and disable interaction. */
36
+ loading?: boolean
37
+ }
38
+
39
+ /**
40
+ * @description A themed button with variant, size, and loading support.
41
+ * Uses CSS custom property tokens for dark/light mode compatibility.
42
+ */
43
+ const Button = forwardRef<HTMLButtonElement, ButtonProps>(
44
+ ({ variant = 'primary', size = 'md', loading, disabled, className, children, ...props }, ref) => (
45
+ <button
46
+ ref={ref}
47
+ disabled={disabled || loading}
48
+ className={cn(
49
+ 'inline-flex items-center justify-center font-medium transition-colors duration-150',
50
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--brand-primary))] focus-visible:ring-offset-2 focus-visible:ring-offset-[hsl(var(--bg-base))]',
51
+ 'disabled:pointer-events-none disabled:opacity-50',
52
+ 'cursor-pointer select-none',
53
+ variantClasses[variant],
54
+ sizeClasses[size],
55
+ className,
56
+ )}
57
+ {...props}
58
+ >
59
+ {loading && <Loader2 className="h-4 w-4 animate-spin" />}
60
+ {children}
61
+ </button>
62
+ ),
63
+ )
64
+
65
+ Button.displayName = 'Button'
66
+
67
+ export { Button }
@@ -0,0 +1,128 @@
1
+ 'use client'
2
+
3
+ import { forwardRef, type HTMLAttributes, type ReactNode } from 'react'
4
+ import { cn } from '../utils'
5
+
6
+ /* ── Variant & padding maps ───────────────────────────────────────────────── */
7
+
8
+ const variantClasses = {
9
+ default:
10
+ 'bg-[hsl(var(--bg-surface))] border border-[hsl(var(--border-subtle))] shadow-sm',
11
+ elevated:
12
+ 'bg-[hsl(var(--bg-elevated))] border border-[hsl(var(--border-default))] shadow-lg',
13
+ outlined:
14
+ 'bg-transparent border border-[hsl(var(--border-default))]',
15
+ interactive:
16
+ 'bg-[hsl(var(--bg-surface))] border border-[hsl(var(--border-subtle))] shadow-sm hover:bg-[hsl(var(--bg-elevated))] hover:border-[hsl(var(--border-default))] hover:shadow-md transition-all duration-150 cursor-pointer',
17
+ } as const
18
+
19
+ const paddingClasses = {
20
+ none: '',
21
+ sm: 'p-3',
22
+ md: 'p-5',
23
+ lg: 'p-6',
24
+ } as const
25
+
26
+ export interface CardProps extends HTMLAttributes<HTMLDivElement> {
27
+ /** Visual variant. */
28
+ variant?: keyof typeof variantClasses
29
+ /** Padding preset. */
30
+ padding?: keyof typeof paddingClasses
31
+ /** Card content. */
32
+ children: ReactNode
33
+ }
34
+
35
+ /**
36
+ * @description A styled card container with variant and padding presets.
37
+ * Use with CardHeader, CardTitle, CardDescription, CardContent, and CardFooter
38
+ * subcomponents for semantic structure.
39
+ */
40
+ const Card = forwardRef<HTMLDivElement, CardProps>(
41
+ ({ variant = 'default', padding = 'md', className, children, ...props }, ref) => (
42
+ <div
43
+ ref={ref}
44
+ className={cn('rounded-2xl', variantClasses[variant], paddingClasses[padding], className)}
45
+ {...props}
46
+ >
47
+ {children}
48
+ </div>
49
+ ),
50
+ )
51
+ Card.displayName = 'Card'
52
+
53
+ /* ── Subcomponents ────────────────────────────────────────────────────────── */
54
+
55
+ export interface CardSubProps extends HTMLAttributes<HTMLDivElement> {
56
+ children: ReactNode
57
+ }
58
+
59
+ /** Header section of a Card (flex row for title + actions). */
60
+ const CardHeader = forwardRef<HTMLDivElement, CardSubProps>(
61
+ ({ className, children, ...props }, ref) => (
62
+ <div
63
+ ref={ref}
64
+ className={cn('flex items-start justify-between gap-4', className)}
65
+ {...props}
66
+ >
67
+ {children}
68
+ </div>
69
+ ),
70
+ )
71
+ CardHeader.displayName = 'CardHeader'
72
+
73
+ /** Title within a CardHeader. */
74
+ const CardTitle = forwardRef<HTMLHeadingElement, HTMLAttributes<HTMLHeadingElement> & { children: ReactNode }>(
75
+ ({ className, children, ...props }, ref) => (
76
+ <h3
77
+ ref={ref}
78
+ className={cn('text-base font-semibold text-[hsl(var(--text-primary))]', className)}
79
+ {...props}
80
+ >
81
+ {children}
82
+ </h3>
83
+ ),
84
+ )
85
+ CardTitle.displayName = 'CardTitle'
86
+
87
+ /** Description text within a CardHeader. */
88
+ const CardDescription = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement> & { children: ReactNode }>(
89
+ ({ className, children, ...props }, ref) => (
90
+ <p
91
+ ref={ref}
92
+ className={cn('text-sm text-[hsl(var(--text-secondary))]', className)}
93
+ {...props}
94
+ >
95
+ {children}
96
+ </p>
97
+ ),
98
+ )
99
+ CardDescription.displayName = 'CardDescription'
100
+
101
+ /** Main content area of a Card. */
102
+ const CardContent = forwardRef<HTMLDivElement, CardSubProps>(
103
+ ({ className, children, ...props }, ref) => (
104
+ <div ref={ref} className={cn('mt-4', className)} {...props}>
105
+ {children}
106
+ </div>
107
+ ),
108
+ )
109
+ CardContent.displayName = 'CardContent'
110
+
111
+ /** Footer section of a Card (typically for actions). */
112
+ const CardFooter = forwardRef<HTMLDivElement, CardSubProps>(
113
+ ({ className, children, ...props }, ref) => (
114
+ <div
115
+ ref={ref}
116
+ className={cn(
117
+ 'mt-4 pt-4 flex items-center gap-3 border-t border-[hsl(var(--border-subtle))]',
118
+ className,
119
+ )}
120
+ {...props}
121
+ >
122
+ {children}
123
+ </div>
124
+ ),
125
+ )
126
+ CardFooter.displayName = 'CardFooter'
127
+
128
+ export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter }
@@ -0,0 +1,64 @@
1
+ import type { Meta, StoryObj } from '@storybook/react'
2
+ import { useState } from 'react'
3
+ import { Checkbox } from './checkbox'
4
+
5
+ const meta: Meta<typeof Checkbox> = {
6
+ title: 'Components/Checkbox',
7
+ component: Checkbox,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ size: { control: 'select', options: ['sm', 'md'] },
11
+ indeterminate: { control: 'boolean' },
12
+ disabled: { control: 'boolean' },
13
+ },
14
+ }
15
+ export default meta
16
+ type Story = StoryObj<typeof Checkbox>
17
+
18
+ export const Default: Story = {
19
+ render: (args) => {
20
+ const [checked, setChecked] = useState(false)
21
+ return <Checkbox {...args} checked={checked} onChange={() => setChecked(!checked)} />
22
+ },
23
+ }
24
+
25
+ export const Checked: Story = {
26
+ render: () => {
27
+ const [checked, setChecked] = useState(true)
28
+ return <Checkbox checked={checked} onChange={() => setChecked(!checked)} />
29
+ },
30
+ }
31
+
32
+ export const Unchecked: Story = {
33
+ render: () => {
34
+ const [checked, setChecked] = useState(false)
35
+ return <Checkbox checked={checked} onChange={() => setChecked(!checked)} />
36
+ },
37
+ }
38
+
39
+ export const Indeterminate: Story = {
40
+ args: { indeterminate: true, checked: false },
41
+ }
42
+
43
+ export const Disabled: Story = {
44
+ args: { disabled: true, checked: true },
45
+ }
46
+
47
+ export const SmallSize: Story = {
48
+ render: () => {
49
+ const [checked, setChecked] = useState(true)
50
+ return <Checkbox size="sm" checked={checked} onChange={() => setChecked(!checked)} />
51
+ },
52
+ }
53
+
54
+ export const AllStates: Story = {
55
+ render: () => (
56
+ <div style={{ display: 'flex', gap: '1.5rem', alignItems: 'center' }}>
57
+ <Checkbox checked={false} onChange={() => {}} />
58
+ <Checkbox checked={true} onChange={() => {}} />
59
+ <Checkbox indeterminate checked={false} onChange={() => {}} />
60
+ <Checkbox disabled checked={false} onChange={() => {}} />
61
+ <Checkbox disabled checked={true} onChange={() => {}} />
62
+ </div>
63
+ ),
64
+ }
@@ -0,0 +1,58 @@
1
+ 'use client'
2
+
3
+ import { forwardRef, type InputHTMLAttributes } from 'react'
4
+ import { Check, Minus } from 'lucide-react'
5
+ import { cn } from '../utils'
6
+
7
+ export interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type' | 'size'> {
8
+ /** Show an indeterminate (minus) indicator instead of a checkmark. */
9
+ indeterminate?: boolean
10
+ /** Size variant. */
11
+ size?: 'sm' | 'md'
12
+ }
13
+
14
+ /**
15
+ * @description A themed checkbox with indeterminate state support.
16
+ * Uses CSS custom property tokens for dark/light mode compatibility.
17
+ */
18
+ const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
19
+ ({ indeterminate, checked, size = 'md', className, ...props }, ref) => {
20
+ const isChecked = checked || indeterminate
21
+ const sizeClass = size === 'sm' ? 'h-3.5 w-3.5' : 'h-4 w-4'
22
+ const iconSize = size === 'sm' ? 'h-2.5 w-2.5' : 'h-3 w-3'
23
+
24
+ return (
25
+ <label className={cn('relative inline-flex items-center cursor-pointer', className)}>
26
+ <input
27
+ ref={ref}
28
+ type="checkbox"
29
+ checked={checked}
30
+ className="sr-only peer"
31
+ {...props}
32
+ />
33
+ <div
34
+ className={cn(
35
+ sizeClass,
36
+ 'rounded border transition-colors duration-150',
37
+ 'flex items-center justify-center shrink-0',
38
+ isChecked
39
+ ? 'bg-[hsl(var(--brand-primary))] border-[hsl(var(--brand-primary))]'
40
+ : 'bg-transparent border-[hsl(var(--border-strong))] hover:border-[hsl(var(--brand-primary))]',
41
+ 'peer-focus-visible:ring-2 peer-focus-visible:ring-[hsl(var(--brand-primary))] peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-[hsl(var(--bg-base))]',
42
+ 'peer-disabled:opacity-50 peer-disabled:pointer-events-none',
43
+ )}
44
+ >
45
+ {indeterminate ? (
46
+ <Minus className={cn(iconSize, 'text-[hsl(var(--text-on-brand))] stroke-[3]')} />
47
+ ) : checked ? (
48
+ <Check className={cn(iconSize, 'text-[hsl(var(--text-on-brand))] stroke-[3]')} />
49
+ ) : null}
50
+ </div>
51
+ </label>
52
+ )
53
+ },
54
+ )
55
+
56
+ Checkbox.displayName = 'Checkbox'
57
+
58
+ export { Checkbox }
@@ -0,0 +1,96 @@
1
+ import type { Meta, StoryObj } from '@storybook/react'
2
+ import { useState } from 'react'
3
+ import { ConfirmDialog } from './confirm-dialog'
4
+ import { Button } from './button'
5
+
6
+ const meta: Meta<typeof ConfirmDialog> = {
7
+ title: 'Components/ConfirmDialog',
8
+ component: ConfirmDialog,
9
+ tags: ['autodocs'],
10
+ argTypes: {
11
+ variant: { control: 'select', options: ['danger', 'warning', 'default'] },
12
+ loading: { control: 'boolean' },
13
+ },
14
+ }
15
+ export default meta
16
+ type Story = StoryObj<typeof ConfirmDialog>
17
+
18
+ export const Default: Story = {
19
+ render: (args) => {
20
+ const [open, setOpen] = useState(true)
21
+ return (
22
+ <>
23
+ <Button onClick={() => setOpen(true)}>Open Dialog</Button>
24
+ <ConfirmDialog
25
+ {...args}
26
+ open={open}
27
+ onOpenChange={setOpen}
28
+ title="Delete Entity"
29
+ description="Are you sure you want to delete this entity? This action cannot be undone."
30
+ onConfirm={() => setOpen(false)}
31
+ />
32
+ </>
33
+ )
34
+ },
35
+ }
36
+
37
+ export const DangerVariant: Story = {
38
+ render: () => {
39
+ const [open, setOpen] = useState(true)
40
+ return (
41
+ <>
42
+ <Button variant="danger" onClick={() => setOpen(true)}>Delete</Button>
43
+ <ConfirmDialog
44
+ open={open}
45
+ onOpenChange={setOpen}
46
+ variant="danger"
47
+ title="Delete All Data"
48
+ description="This will permanently delete all monitoring data for this device. This action cannot be undone."
49
+ confirmLabel="Delete Everything"
50
+ onConfirm={() => setOpen(false)}
51
+ />
52
+ </>
53
+ )
54
+ },
55
+ }
56
+
57
+ export const WarningVariant: Story = {
58
+ render: () => {
59
+ const [open, setOpen] = useState(true)
60
+ return (
61
+ <>
62
+ <Button variant="secondary" onClick={() => setOpen(true)}>Restart</Button>
63
+ <ConfirmDialog
64
+ open={open}
65
+ onOpenChange={setOpen}
66
+ variant="warning"
67
+ title="Restart Collector"
68
+ description="Restarting the collector will cause a brief gap in monitoring data. Continue?"
69
+ confirmLabel="Restart"
70
+ onConfirm={() => setOpen(false)}
71
+ />
72
+ </>
73
+ )
74
+ },
75
+ }
76
+
77
+ export const WithLoading: Story = {
78
+ render: () => {
79
+ const [open, setOpen] = useState(true)
80
+ return (
81
+ <>
82
+ <Button onClick={() => setOpen(true)}>Open Dialog</Button>
83
+ <ConfirmDialog
84
+ open={open}
85
+ onOpenChange={setOpen}
86
+ variant="danger"
87
+ title="Decommission Device"
88
+ description="This will decommission the device and stop all monitoring."
89
+ confirmLabel="Decommission"
90
+ loading={true}
91
+ onConfirm={() => {}}
92
+ />
93
+ </>
94
+ )
95
+ },
96
+ }
@@ -0,0 +1,145 @@
1
+ 'use client'
2
+
3
+ import * as AlertDialog from '@radix-ui/react-alert-dialog'
4
+ import { motion, AnimatePresence } from 'framer-motion'
5
+ import { AlertTriangle, Loader2 } from 'lucide-react'
6
+ import { cn } from '../utils'
7
+
8
+ export interface ConfirmDialogProps {
9
+ /** Whether the dialog is open. */
10
+ open: boolean
11
+ /** Callback to control open state. */
12
+ onOpenChange: (open: boolean) => void
13
+ /** Dialog title. */
14
+ title: string
15
+ /** Dialog description text. */
16
+ description: string
17
+ /** Label for the confirm button. */
18
+ confirmLabel?: string
19
+ /** Label for the cancel button. */
20
+ cancelLabel?: string
21
+ /** Visual variant affecting the icon and confirm button color. */
22
+ variant?: 'danger' | 'warning' | 'default'
23
+ /** Show a loading spinner on the confirm button. */
24
+ loading?: boolean
25
+ /** Callback when confirmed. */
26
+ onConfirm: () => void
27
+ }
28
+
29
+ const variantStyles = {
30
+ danger: {
31
+ icon: 'text-[hsl(var(--status-critical))] bg-[hsl(var(--status-critical))]/10',
32
+ button: 'bg-[hsl(var(--status-critical))] hover:bg-[hsl(var(--status-critical))]/90 text-[hsl(var(--text-on-brand))]',
33
+ },
34
+ warning: {
35
+ icon: 'text-[hsl(var(--status-warning))] bg-[hsl(var(--status-warning))]/10',
36
+ button: 'bg-[hsl(var(--status-warning))] hover:bg-[hsl(var(--status-warning))]/90 text-[hsl(var(--text-on-brand))]',
37
+ },
38
+ default: {
39
+ icon: 'text-[hsl(var(--brand-primary))] bg-[hsl(var(--brand-primary))]/10',
40
+ button: 'bg-[hsl(var(--brand-primary))] hover:bg-[hsl(var(--brand-primary))]/90 text-[hsl(var(--text-on-brand))]',
41
+ },
42
+ }
43
+
44
+ /**
45
+ * @description A confirmation dialog built on Radix AlertDialog with Framer Motion animations.
46
+ * Supports danger, warning, and default variants with loading state.
47
+ */
48
+ export function ConfirmDialog({
49
+ open,
50
+ onOpenChange,
51
+ title,
52
+ description,
53
+ confirmLabel = 'Confirm',
54
+ cancelLabel = 'Cancel',
55
+ variant = 'danger',
56
+ loading = false,
57
+ onConfirm,
58
+ }: ConfirmDialogProps) {
59
+ const styles = variantStyles[variant]
60
+
61
+ return (
62
+ <AlertDialog.Root open={open} onOpenChange={onOpenChange}>
63
+ <AnimatePresence>
64
+ {open && (
65
+ <AlertDialog.Portal forceMount>
66
+ <AlertDialog.Overlay asChild>
67
+ <motion.div
68
+ initial={{ opacity: 0 }}
69
+ animate={{ opacity: 1 }}
70
+ exit={{ opacity: 0 }}
71
+ transition={{ duration: 0.15 }}
72
+ className="fixed inset-0 z-50 bg-[hsl(var(--bg-base)/0.7)] backdrop-blur-sm"
73
+ />
74
+ </AlertDialog.Overlay>
75
+ <AlertDialog.Content asChild>
76
+ <motion.div
77
+ initial={{ opacity: 0, scale: 0.95, y: 8 }}
78
+ animate={{ opacity: 1, scale: 1, y: 0 }}
79
+ exit={{ opacity: 0, scale: 0.95, y: 8 }}
80
+ transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
81
+ className={cn(
82
+ 'fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2',
83
+ 'w-full max-w-md p-6 rounded-2xl',
84
+ 'border border-[hsl(var(--border-default))]',
85
+ 'bg-[hsl(var(--bg-elevated))] shadow-2xl',
86
+ 'focus:outline-none',
87
+ )}
88
+ >
89
+ <div className="flex gap-4">
90
+ <div className={cn('flex-shrink-0 p-2.5 rounded-xl h-fit', styles.icon)}>
91
+ <AlertTriangle className="w-5 h-5" />
92
+ </div>
93
+ <div className="flex-1 min-w-0">
94
+ <AlertDialog.Title className="text-base font-semibold text-[hsl(var(--text-primary))]">
95
+ {title}
96
+ </AlertDialog.Title>
97
+ <AlertDialog.Description className="mt-2 text-sm text-[hsl(var(--text-secondary))] leading-relaxed">
98
+ {description}
99
+ </AlertDialog.Description>
100
+ </div>
101
+ </div>
102
+
103
+ <div className="flex justify-end gap-3 mt-6">
104
+ <AlertDialog.Cancel
105
+ className={cn(
106
+ 'px-4 py-2 text-sm font-medium rounded-lg',
107
+ 'border border-[hsl(var(--border-default))]',
108
+ 'text-[hsl(var(--text-primary))]',
109
+ 'hover:bg-[hsl(var(--bg-overlay))] transition-colors',
110
+ 'focus:outline-none focus:ring-2 focus:ring-[hsl(var(--brand-primary))]',
111
+ )}
112
+ >
113
+ {cancelLabel}
114
+ </AlertDialog.Cancel>
115
+ <AlertDialog.Action
116
+ onClick={(e) => {
117
+ e.preventDefault()
118
+ onConfirm()
119
+ }}
120
+ disabled={loading}
121
+ className={cn(
122
+ 'px-4 py-2 text-sm font-medium rounded-lg transition-all',
123
+ 'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-[hsl(var(--bg-elevated))]',
124
+ styles.button,
125
+ loading && 'opacity-70 cursor-not-allowed',
126
+ )}
127
+ >
128
+ {loading ? (
129
+ <span className="flex items-center gap-2">
130
+ <Loader2 className="w-4 h-4 animate-spin" />
131
+ {confirmLabel}
132
+ </span>
133
+ ) : (
134
+ confirmLabel
135
+ )}
136
+ </AlertDialog.Action>
137
+ </div>
138
+ </motion.div>
139
+ </AlertDialog.Content>
140
+ </AlertDialog.Portal>
141
+ )}
142
+ </AnimatePresence>
143
+ </AlertDialog.Root>
144
+ )
145
+ }
@@ -0,0 +1,125 @@
1
+ import type { Meta, StoryObj } from '@storybook/react'
2
+ import type { ColumnDef } from '@tanstack/react-table'
3
+ import { Server, Wifi } from 'lucide-react'
4
+ import { DataTable } from './data-table'
5
+ import { StatusBadge } from './status-badge'
6
+ import { Badge } from './badge'
7
+
8
+ interface Device {
9
+ id: string
10
+ hostname: string
11
+ ip: string
12
+ vendor: string
13
+ type: string
14
+ status: string
15
+ uptime: string
16
+ }
17
+
18
+ const sampleData: Device[] = [
19
+ { id: '1', hostname: 'core-sw-01', ip: '10.0.0.1', vendor: 'Cisco', type: 'Switch', status: 'ok', uptime: '142d 3h' },
20
+ { id: '2', hostname: 'core-sw-02', ip: '10.0.0.2', vendor: 'Cisco', type: 'Switch', status: 'ok', uptime: '89d 12h' },
21
+ { id: '3', hostname: 'fw-edge-01', ip: '10.0.0.10', vendor: 'FortiGate', type: 'Firewall', status: 'warning', uptime: '30d 1h' },
22
+ { id: '4', hostname: 'esxi-host-01', ip: '192.168.1.10', vendor: 'VMware', type: 'Hypervisor', status: 'ok', uptime: '210d 7h' },
23
+ { id: '5', hostname: 'esxi-host-02', ip: '192.168.1.11', vendor: 'VMware', type: 'Hypervisor', status: 'critical', uptime: '0d 0h' },
24
+ { id: '6', hostname: 'dist-sw-01', ip: '10.0.1.1', vendor: 'Arista', type: 'Switch', status: 'ok', uptime: '67d 5h' },
25
+ { id: '7', hostname: 'dist-sw-02', ip: '10.0.1.2', vendor: 'Arista', type: 'Switch', status: 'maintenance', uptime: '0d 0h' },
26
+ { id: '8', hostname: 'lb-prod-01', ip: '10.0.2.1', vendor: 'F5', type: 'Load Balancer', status: 'ok', uptime: '45d 9h' },
27
+ { id: '9', hostname: 'san-sw-01', ip: '10.0.3.1', vendor: 'Brocade', type: 'SAN Switch', status: 'ok', uptime: '300d 2h' },
28
+ { id: '10', hostname: 'router-gw-01', ip: '10.0.0.254', vendor: 'Juniper', type: 'Router', status: 'ok', uptime: '180d 11h' },
29
+ { id: '11', hostname: 'access-sw-01', ip: '10.1.0.1', vendor: 'HP', type: 'Switch', status: 'stale', uptime: '—' },
30
+ { id: '12', hostname: 'access-sw-02', ip: '10.1.0.2', vendor: 'HP', type: 'Switch', status: 'ok', uptime: '55d 8h' },
31
+ ]
32
+
33
+ const columns: ColumnDef<Device, unknown>[] = [
34
+ { accessorKey: 'hostname', header: 'Hostname' },
35
+ { accessorKey: 'ip', header: 'IP Address' },
36
+ { accessorKey: 'vendor', header: 'Vendor' },
37
+ {
38
+ accessorKey: 'type',
39
+ header: 'Type',
40
+ cell: ({ getValue }) => <Badge color="blue" size="xs">{getValue() as string}</Badge>,
41
+ },
42
+ {
43
+ accessorKey: 'status',
44
+ header: 'Status',
45
+ cell: ({ getValue }) => <StatusBadge status={getValue() as string} size="sm" />,
46
+ },
47
+ { accessorKey: 'uptime', header: 'Uptime' },
48
+ ]
49
+
50
+ const meta: Meta<typeof DataTable<Device>> = {
51
+ title: 'Components/DataTable',
52
+ component: DataTable,
53
+ tags: ['autodocs'],
54
+ parameters: { layout: 'padded' },
55
+ }
56
+ export default meta
57
+ type Story = StoryObj<typeof DataTable<Device>>
58
+
59
+ export const Default: Story = {
60
+ args: {
61
+ columns,
62
+ data: sampleData,
63
+ },
64
+ }
65
+
66
+ export const WithExport: Story = {
67
+ args: {
68
+ columns,
69
+ data: sampleData,
70
+ exportFilename: 'devices',
71
+ },
72
+ }
73
+
74
+ export const WithRowClick: Story = {
75
+ args: {
76
+ columns,
77
+ data: sampleData,
78
+ onRowClick: (row: Device) => alert(`Clicked: ${row.hostname}`),
79
+ },
80
+ }
81
+
82
+ export const WithDefaultSort: Story = {
83
+ args: {
84
+ columns,
85
+ data: sampleData,
86
+ defaultSort: { id: 'hostname', desc: false },
87
+ },
88
+ }
89
+
90
+ export const Loading: Story = {
91
+ args: {
92
+ columns,
93
+ data: [],
94
+ isLoading: true,
95
+ },
96
+ }
97
+
98
+ export const Empty: Story = {
99
+ args: {
100
+ columns,
101
+ data: [],
102
+ emptyState: {
103
+ icon: Server,
104
+ title: 'No devices found',
105
+ description: 'Run a discovery scan to find devices on your network.',
106
+ },
107
+ },
108
+ }
109
+
110
+ export const SmallPageSize: Story = {
111
+ args: {
112
+ columns,
113
+ data: sampleData,
114
+ defaultPageSize: 5,
115
+ },
116
+ }
117
+
118
+ export const StickyFirstColumn: Story = {
119
+ args: {
120
+ columns,
121
+ data: sampleData,
122
+ stickyFirstColumn: true,
123
+ },
124
+ parameters: { layout: 'padded' },
125
+ }