@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.
- package/LICENSE +21 -0
- package/README.md +215 -0
- package/dist/chunk-5OKSXPWK.js +270 -0
- package/dist/chunk-5OKSXPWK.js.map +1 -0
- package/dist/cli/index.js +430 -0
- package/dist/form.d.ts +65 -0
- package/dist/form.js +148 -0
- package/dist/form.js.map +1 -0
- package/dist/index.d.ts +942 -0
- package/dist/index.js +2812 -0
- package/dist/index.js.map +1 -0
- package/dist/select-nnBJUO8U.d.ts +26 -0
- package/package.json +114 -0
- package/src/components/animated-counter.stories.tsx +68 -0
- package/src/components/animated-counter.tsx +85 -0
- package/src/components/avatar.tsx +106 -0
- package/src/components/badge.stories.tsx +70 -0
- package/src/components/badge.tsx +97 -0
- package/src/components/button.stories.tsx +101 -0
- package/src/components/button.tsx +67 -0
- package/src/components/card.tsx +128 -0
- package/src/components/checkbox.stories.tsx +64 -0
- package/src/components/checkbox.tsx +58 -0
- package/src/components/confirm-dialog.stories.tsx +96 -0
- package/src/components/confirm-dialog.tsx +145 -0
- package/src/components/data-table.stories.tsx +125 -0
- package/src/components/data-table.tsx +791 -0
- package/src/components/dropdown-menu.tsx +111 -0
- package/src/components/empty-state.stories.tsx +42 -0
- package/src/components/empty-state.tsx +43 -0
- package/src/components/filter-pill.stories.tsx +71 -0
- package/src/components/filter-pill.tsx +45 -0
- package/src/components/form-input.stories.tsx +91 -0
- package/src/components/form-input.tsx +77 -0
- package/src/components/log-viewer.tsx +212 -0
- package/src/components/metric-card.tsx +141 -0
- package/src/components/pipeline-stage.tsx +134 -0
- package/src/components/popover.tsx +72 -0
- package/src/components/port-status-grid.tsx +102 -0
- package/src/components/progress.tsx +128 -0
- package/src/components/radio-group.tsx +162 -0
- package/src/components/select.stories.tsx +52 -0
- package/src/components/select.tsx +92 -0
- package/src/components/severity-timeline.tsx +125 -0
- package/src/components/sheet.tsx +164 -0
- package/src/components/skeleton.stories.tsx +64 -0
- package/src/components/skeleton.tsx +62 -0
- package/src/components/slider.tsx +208 -0
- package/src/components/sparkline.tsx +104 -0
- package/src/components/status-badge.stories.tsx +84 -0
- package/src/components/status-badge.tsx +71 -0
- package/src/components/status-pulse.stories.tsx +56 -0
- package/src/components/status-pulse.tsx +78 -0
- package/src/components/success-checkmark.stories.tsx +67 -0
- package/src/components/success-checkmark.tsx +53 -0
- package/src/components/tabs.tsx +177 -0
- package/src/components/threshold-gauge.tsx +149 -0
- package/src/components/time-range-selector.tsx +86 -0
- package/src/components/toast.stories.tsx +70 -0
- package/src/components/toast.tsx +48 -0
- package/src/components/toggle-switch.stories.tsx +66 -0
- package/src/components/toggle-switch.tsx +51 -0
- package/src/components/tooltip.tsx +62 -0
- package/src/components/truncated-text.stories.tsx +56 -0
- package/src/components/truncated-text.tsx +80 -0
- package/src/components/uptime-tracker.tsx +138 -0
- package/src/components/utilization-bar.tsx +103 -0
- package/src/theme.css +178 -0
- 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
|
+
}
|