Skip to content

Commit 903cc05

Browse files
author
Lasim
committed
refactor: add DsAlert component with success alert functionality and update navigation to include success parameter
1 parent 97ccf03 commit 903cc05

File tree

7 files changed

+652
-2
lines changed

7 files changed

+652
-2
lines changed
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
<!--
2+
@component DsAlert
3+
@description A customizable alert component with multiple variants, optional icons, title, and description. Built following DeployStack design principles.
4+
5+
@example
6+
<DsAlert variant="success" title="Success!" description="Your changes have been saved." />
7+
8+
<DsAlert variant="warning">
9+
<DsAlertTitle>Warning</DsAlertTitle>
10+
<DsAlertDescription>Please review your settings before continuing.</DsAlertDescription>
11+
</DsAlert>
12+
13+
@props
14+
- variant: Alert style variant ('default' | 'success' | 'warning' | 'error' | 'info')
15+
- title: Optional title text (alternative to DsAlertTitle slot)
16+
- description: Optional description text (alternative to DsAlertDescription slot)
17+
- showIcon: Whether to show the default variant icon
18+
- dismissible: Whether to show a close button
19+
- size: Size variant ('sm' | 'md' | 'lg')
20+
21+
@emits
22+
- dismiss: Emitted when close button is clicked
23+
24+
@slots
25+
- default: Main content area
26+
- title: Custom title content (overrides title prop)
27+
- description: Custom description content (overrides description prop)
28+
- icon: Custom icon (overrides default variant icon)
29+
30+
@accessibility
31+
- Uses proper ARIA attributes for alert semantics
32+
- Screen reader friendly with sr-only labels
33+
- Supports keyboard navigation for dismissible alerts
34+
-->
35+
36+
<script setup lang="ts">
37+
import { computed } from 'vue'
38+
import { cva, type VariantProps } from 'class-variance-authority'
39+
import { cn } from '@/lib/utils'
40+
import {
41+
CheckCircle,
42+
AlertTriangle,
43+
XCircle,
44+
Info,
45+
AlertCircle,
46+
X
47+
} from 'lucide-vue-next'
48+
import DsAlertTitle from './DsAlertTitle.vue'
49+
import DsAlertDescription from './DsAlertDescription.vue'
50+
51+
export type DsAlertVariant = 'default' | 'success' | 'warning' | 'error' | 'info'
52+
53+
const alertVariants = cva(
54+
'relative w-full rounded-lg border px-4 py-3 text-sm transition-all duration-200',
55+
{
56+
variants: {
57+
variant: {
58+
default: 'bg-card text-card-foreground border-border',
59+
success: 'bg-green-50 text-green-800 border-green-200 dark:bg-green-950 dark:text-green-200 dark:border-green-800',
60+
warning: 'bg-yellow-50 text-yellow-800 border-yellow-200 dark:bg-yellow-950 dark:text-yellow-200 dark:border-yellow-800',
61+
error: 'bg-red-50 text-red-800 border-red-200 dark:bg-red-950 dark:text-red-200 dark:border-red-800',
62+
info: 'bg-blue-50 text-blue-800 border-blue-200 dark:bg-blue-950 dark:text-blue-200 dark:border-blue-800'
63+
},
64+
size: {
65+
sm: 'px-3 py-2 text-xs',
66+
md: 'px-4 py-3 text-sm',
67+
lg: 'px-6 py-4 text-base'
68+
}
69+
},
70+
defaultVariants: {
71+
variant: 'default',
72+
size: 'md'
73+
}
74+
}
75+
)
76+
77+
const iconVariants = cva(
78+
'shrink-0',
79+
{
80+
variants: {
81+
variant: {
82+
default: 'text-muted-foreground',
83+
success: 'text-green-600 dark:text-green-400',
84+
warning: 'text-yellow-600 dark:text-yellow-400',
85+
error: 'text-red-600 dark:text-red-400',
86+
info: 'text-blue-600 dark:text-blue-400'
87+
},
88+
size: {
89+
sm: 'h-3 w-3',
90+
md: 'h-4 w-4',
91+
lg: 'h-5 w-5'
92+
}
93+
},
94+
defaultVariants: {
95+
variant: 'default',
96+
size: 'md'
97+
}
98+
}
99+
)
100+
101+
interface Props {
102+
variant?: DsAlertVariant
103+
title?: string
104+
description?: string
105+
showIcon?: boolean
106+
dismissible?: boolean
107+
size?: VariantProps<typeof alertVariants>['size']
108+
}
109+
110+
const props = withDefaults(defineProps<Props>(), {
111+
variant: 'default',
112+
showIcon: true,
113+
dismissible: false,
114+
size: 'md'
115+
})
116+
117+
const emit = defineEmits<{
118+
dismiss: []
119+
}>()
120+
121+
const defaultIcons = {
122+
default: AlertCircle,
123+
success: CheckCircle,
124+
warning: AlertTriangle,
125+
error: XCircle,
126+
info: Info
127+
}
128+
129+
const defaultIcon = computed(() => defaultIcons[props.variant])
130+
131+
const hasTitle = computed(() => props.title)
132+
const hasDescription = computed(() => props.description)
133+
const hasIcon = computed(() => props.showIcon)
134+
const hasContent = computed(() => hasTitle.value || hasDescription.value)
135+
136+
const contentLayout = computed(() => {
137+
if (hasIcon.value) {
138+
return 'grid grid-cols-[auto_1fr] gap-3 items-start'
139+
}
140+
return 'flex flex-col gap-1'
141+
})
142+
143+
const contentLayoutWithDismiss = computed(() => {
144+
if (props.dismissible) {
145+
if (hasIcon.value) {
146+
return 'grid grid-cols-[auto_1fr_auto] gap-3 items-start'
147+
}
148+
return 'flex justify-between items-start gap-3'
149+
}
150+
return contentLayout.value
151+
})
152+
153+
function handleDismiss() {
154+
emit('dismiss')
155+
}
156+
</script>
157+
158+
<template>
159+
<div
160+
role="alert"
161+
:class="cn(alertVariants({ variant, size }))"
162+
>
163+
<div :class="contentLayoutWithDismiss">
164+
<!-- Icon -->
165+
<div v-if="hasIcon" class="mt-0.5">
166+
<slot name="icon">
167+
<component
168+
:is="defaultIcon"
169+
:class="cn(iconVariants({ variant, size }))"
170+
/>
171+
</slot>
172+
</div>
173+
174+
<!-- Content -->
175+
<div class="flex-1 space-y-1">
176+
<!-- Title -->
177+
<div v-if="hasTitle || $slots.title">
178+
<slot name="title">
179+
<DsAlertTitle v-if="title">{{ title }}</DsAlertTitle>
180+
</slot>
181+
</div>
182+
183+
<!-- Description -->
184+
<div v-if="hasDescription || $slots.description">
185+
<slot name="description">
186+
<DsAlertDescription v-if="description">{{ description }}</DsAlertDescription>
187+
</slot>
188+
</div>
189+
190+
<!-- Default slot for custom content -->
191+
<div v-if="$slots.default && !hasContent">
192+
<slot />
193+
</div>
194+
</div>
195+
196+
<!-- Dismiss button -->
197+
<button
198+
v-if="dismissible"
199+
type="button"
200+
:class="[
201+
'shrink-0 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100',
202+
'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
203+
'disabled:pointer-events-none',
204+
size === 'sm' ? 'h-3 w-3 mt-0.5' : '',
205+
size === 'md' ? 'h-4 w-4 mt-0.5' : '',
206+
size === 'lg' ? 'h-5 w-5 mt-0.5' : ''
207+
]"
208+
@click="handleDismiss"
209+
>
210+
<span class="sr-only">Close</span>
211+
<X :class="size === 'sm' ? 'h-3 w-3' : size === 'md' ? 'h-4 w-4' : 'h-5 w-5'" />
212+
</button>
213+
</div>
214+
</div>
215+
</template>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<!--
2+
@component DsAlertDescription
3+
@description Description component for DsAlert. Provides consistent styling for alert descriptions.
4+
5+
@example
6+
<DsAlertDescription>This is additional information about the alert.</DsAlertDescription>
7+
8+
@slots
9+
- default: Description content
10+
-->
11+
12+
<script setup lang="ts">
13+
import { cn } from '@/lib/utils'
14+
15+
interface Props {
16+
class?: string
17+
}
18+
19+
const props = defineProps<Props>()
20+
</script>
21+
22+
<template>
23+
<div :class="cn('text-sm [&_p]:leading-relaxed', props.class)">
24+
<slot />
25+
</div>
26+
</template>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<!--
2+
@component DsAlertTitle
3+
@description Title component for DsAlert. Provides consistent styling for alert titles.
4+
5+
@example
6+
<DsAlertTitle>Important Notice</DsAlertTitle>
7+
8+
@slots
9+
- default: Title content
10+
-->
11+
12+
<script setup lang="ts">
13+
import { cn } from '@/lib/utils'
14+
15+
interface Props {
16+
class?: string
17+
}
18+
19+
const props = defineProps<Props>()
20+
</script>
21+
22+
<template>
23+
<h5 :class="cn('mb-1 font-medium leading-none tracking-tight', props.class)">
24+
<slot />
25+
</h5>
26+
</template>

0 commit comments

Comments
 (0)