@proyecto-viviana/ui 0.1.0 → 0.1.2

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 (39) hide show
  1. package/dist/index.js +2 -2
  2. package/dist/index.js.map +1 -1
  3. package/package.json +6 -5
  4. package/src/alert/index.tsx +48 -0
  5. package/src/assets/favicon.png +0 -0
  6. package/src/assets/fire.gif +0 -0
  7. package/src/avatar/index.tsx +75 -0
  8. package/src/badge/index.tsx +43 -0
  9. package/src/button/Button.tsx +75 -0
  10. package/src/button/index.ts +2 -0
  11. package/src/button/types.ts +24 -0
  12. package/src/checkbox/index.tsx +258 -0
  13. package/src/components.css +975 -0
  14. package/src/custom/calendar-card/index.tsx +66 -0
  15. package/src/custom/chip/index.tsx +32 -0
  16. package/src/custom/conversation/index.tsx +105 -0
  17. package/src/custom/event-card/index.tsx +126 -0
  18. package/src/custom/lateral-nav/index.tsx +88 -0
  19. package/src/custom/logo/index.tsx +54 -0
  20. package/src/custom/nav-header/index.tsx +42 -0
  21. package/src/custom/page-layout/index.tsx +20 -0
  22. package/src/custom/profile-card/index.tsx +58 -0
  23. package/src/custom/project-card/index.tsx +59 -0
  24. package/src/custom/timeline-item/index.tsx +81 -0
  25. package/src/dialog/index.tsx +69 -0
  26. package/src/icon/icons/GitHubIcon.tsx +20 -0
  27. package/src/icon/index.tsx +48 -0
  28. package/src/index.ts +107 -0
  29. package/src/link/index.tsx +131 -0
  30. package/src/progress-bar/index.tsx +169 -0
  31. package/src/radio/index.tsx +219 -0
  32. package/src/separator/index.tsx +141 -0
  33. package/src/styles.css +450 -0
  34. package/src/switch/ToggleSwitch.tsx +113 -0
  35. package/src/switch/index.tsx +90 -0
  36. package/src/test-utils/index.ts +32 -0
  37. package/src/textfield/index.tsx +211 -0
  38. package/src/theme.css +101 -0
  39. package/src/tooltip/index.tsx +31 -0
@@ -0,0 +1,66 @@
1
+ import { Show, For } from 'solid-js'
2
+ import { Chip } from '../chip'
3
+
4
+ export interface CalendarCardProps {
5
+ title: string
6
+ image?: string
7
+ tags?: string[]
8
+ followers?: { name: string }[]
9
+ followerCount?: number
10
+ class?: string
11
+ }
12
+
13
+ export function CalendarCard(props: CalendarCardProps) {
14
+ const displayedFollowers = () => props.followers?.slice(0, 2) ?? []
15
+ const remainingCount = () => {
16
+ const total = props.followerCount ?? props.followers?.length ?? 0
17
+ const displayed = displayedFollowers().length
18
+ return total - displayed
19
+ }
20
+
21
+ return (
22
+ <div class={`flex h-[100px] w-full max-w-[500px] items-center rounded-xl border border-primary-600 border-b-accent-500 bg-bg-300 p-2 ${props.class ?? ''}`}>
23
+ <Show when={props.image}>
24
+ <div class="relative h-[80px] w-[80px] flex-shrink-0 overflow-hidden rounded-xl border-2 border-accent-200">
25
+ <img src={props.image} alt={props.title} class="h-full w-full object-cover" />
26
+ </div>
27
+ </Show>
28
+ <div class="relative h-full flex-1 flex-col pl-3">
29
+ <Show when={props.tags && props.tags.length > 0}>
30
+ <div class="absolute bottom-[-20px] h-[30px] w-full">
31
+ <div class="flex h-full w-full flex-1 justify-end gap-1">
32
+ <For each={props.tags}>
33
+ {(tag) => <Chip text={tag} variant="primary" />}
34
+ </For>
35
+ </div>
36
+ </div>
37
+ </Show>
38
+ <div class="flex h-full flex-col items-end justify-center pb-3 pr-5 pt-2">
39
+ <div class="flex flex-1">
40
+ <span class="text-lg font-semibold text-white drop-shadow-sm">
41
+ {props.title}
42
+ </span>
43
+ </div>
44
+ <Show when={displayedFollowers().length > 0}>
45
+ <div class="flex flex-1">
46
+ <span class="text-base font-normal text-primary-500">
47
+ seguida por{' '}
48
+ <For each={displayedFollowers()}>
49
+ {(follower, index) => (
50
+ <>
51
+ <span class="font-semibold text-accent-200">{follower.name}</span>
52
+ {index() < displayedFollowers().length - 1 && ', '}
53
+ </>
54
+ )}
55
+ </For>
56
+ <Show when={remainingCount() > 0}>
57
+ {' '}y <span class="font-semibold text-accent-200">{remainingCount()} más</span>
58
+ </Show>
59
+ </span>
60
+ </div>
61
+ </Show>
62
+ </div>
63
+ </div>
64
+ </div>
65
+ )
66
+ }
@@ -0,0 +1,32 @@
1
+ import type { JSX } from 'solid-js'
2
+
3
+ export type ChipVariant = 'primary' | 'secondary' | 'accent' | 'outline'
4
+
5
+ export interface ChipProps {
6
+ text: string
7
+ variant?: ChipVariant
8
+ onClick?: () => void
9
+ icon?: JSX.Element
10
+ class?: string
11
+ }
12
+
13
+ const variantStyles: Record<ChipVariant, string> = {
14
+ primary: 'bg-primary-700 text-primary-200 shadow-primary-chip',
15
+ secondary: 'bg-primary-600 text-primary-100 hover:bg-primary-500',
16
+ accent: 'bg-accent text-white',
17
+ outline: 'bg-transparent border border-primary-500 text-primary-300',
18
+ }
19
+
20
+ export function Chip(props: ChipProps) {
21
+ const variant = () => props.variant ?? 'primary'
22
+
23
+ return (
24
+ <button
25
+ class={`flex justify-center items-center h-6 w-auto rounded-full px-4 py-1 font-medium text-sm tracking-wide transition-colors ${variantStyles[variant()]} ${props.class ?? ''}`}
26
+ onClick={props.onClick}
27
+ >
28
+ {props.icon && <span class="mr-1.5">{props.icon}</span>}
29
+ <span>{props.text}</span>
30
+ </button>
31
+ )
32
+ }
@@ -0,0 +1,105 @@
1
+ import { Show, For } from 'solid-js'
2
+ import { Avatar } from '../../avatar'
3
+
4
+ export interface Message {
5
+ id: string
6
+ content: string
7
+ sender: 'user' | 'other'
8
+ timestamp?: string
9
+ }
10
+
11
+ export interface ConversationPreviewProps {
12
+ user: {
13
+ name: string
14
+ avatar?: string
15
+ online?: boolean
16
+ }
17
+ lastMessage?: string
18
+ unreadCount?: number
19
+ timestamp?: string
20
+ onClick?: () => void
21
+ class?: string
22
+ }
23
+
24
+ export function ConversationPreview(props: ConversationPreviewProps) {
25
+ return (
26
+ <button
27
+ class={`flex w-full items-center gap-3 rounded-xl p-3 hover:bg-bg-300 transition-colors text-left ${props.class ?? ''}`}
28
+ onClick={props.onClick}
29
+ >
30
+ <Avatar
31
+ src={props.user.avatar}
32
+ alt={props.user.name}
33
+ online={props.user.online}
34
+ size="md"
35
+ />
36
+ <div class="flex-1 min-w-0">
37
+ <div class="flex items-center justify-between">
38
+ <span class="font-semibold text-primary-100 truncate">{props.user.name}</span>
39
+ <Show when={props.timestamp}>
40
+ <span class="text-xs text-primary-500">{props.timestamp}</span>
41
+ </Show>
42
+ </div>
43
+ <Show when={props.lastMessage}>
44
+ <p class="text-sm text-primary-400 truncate">{props.lastMessage}</p>
45
+ </Show>
46
+ </div>
47
+ <Show when={props.unreadCount && props.unreadCount > 0}>
48
+ <span class="flex h-5 w-5 items-center justify-center rounded-full bg-accent text-xs font-bold text-white">
49
+ {props.unreadCount}
50
+ </span>
51
+ </Show>
52
+ </button>
53
+ )
54
+ }
55
+
56
+ export interface ConversationBubbleProps {
57
+ content: string
58
+ sender: 'user' | 'other'
59
+ timestamp?: string
60
+ class?: string
61
+ }
62
+
63
+ export function ConversationBubble(props: ConversationBubbleProps) {
64
+ const isUser = () => props.sender === 'user'
65
+
66
+ return (
67
+ <div class={`flex ${isUser() ? 'justify-end' : 'justify-start'} ${props.class ?? ''}`}>
68
+ <div
69
+ class={`max-w-[70%] rounded-2xl px-4 py-2 ${
70
+ isUser()
71
+ ? 'bg-accent text-white rounded-br-sm'
72
+ : 'bg-bg-300 text-primary-100 rounded-bl-sm'
73
+ }`}
74
+ >
75
+ <p>{props.content}</p>
76
+ <Show when={props.timestamp}>
77
+ <span class={`text-xs ${isUser() ? 'text-white/70' : 'text-primary-500'}`}>
78
+ {props.timestamp}
79
+ </span>
80
+ </Show>
81
+ </div>
82
+ </div>
83
+ )
84
+ }
85
+
86
+ export interface ConversationProps {
87
+ messages: Message[]
88
+ class?: string
89
+ }
90
+
91
+ export function Conversation(props: ConversationProps) {
92
+ return (
93
+ <div class={`flex flex-col gap-2 p-4 ${props.class ?? ''}`}>
94
+ <For each={props.messages}>
95
+ {(message) => (
96
+ <ConversationBubble
97
+ content={message.content}
98
+ sender={message.sender}
99
+ timestamp={message.timestamp}
100
+ />
101
+ )}
102
+ </For>
103
+ </div>
104
+ )
105
+ }
@@ -0,0 +1,126 @@
1
+ import type { JSX } from 'solid-js'
2
+ import { Show, For } from 'solid-js'
3
+ import { Avatar } from '../../avatar'
4
+
5
+ export interface EventCardProps {
6
+ title: string
7
+ image?: string
8
+ date?: string
9
+ author?: string
10
+ authorAvatar?: string
11
+ attendees?: { avatar?: string; name: string }[]
12
+ attendeeCount?: number
13
+ decorationImage?: string
14
+ actions?: JSX.Element
15
+ class?: string
16
+ }
17
+
18
+ export function EventCard(props: EventCardProps) {
19
+ const displayedAttendees = () => props.attendees?.slice(0, 3) ?? []
20
+ const remainingCount = () => {
21
+ const total = props.attendeeCount ?? props.attendees?.length ?? 0
22
+ const displayed = displayedAttendees().length
23
+ return total - displayed
24
+ }
25
+
26
+ return (
27
+ <div class={`relative bg-bg-200 rounded-3xl overflow-hidden ${props.class ?? ''}`}>
28
+ {/* Decoration image (fire gif, etc) */}
29
+ <Show when={props.decorationImage}>
30
+ <div class="absolute -top-2 -right-2 z-10 flex flex-col gap-1">
31
+ <img src={props.decorationImage} alt="" class="w-8 h-8 object-contain" />
32
+ <img src={props.decorationImage} alt="" class="w-6 h-6 object-contain ml-2" />
33
+ <img src={props.decorationImage} alt="" class="w-5 h-5 object-contain" />
34
+ </div>
35
+ </Show>
36
+
37
+ <Show when={props.image}>
38
+ <div class="relative h-32 w-full">
39
+ <img
40
+ src={props.image}
41
+ alt={props.title}
42
+ class="w-full h-full object-cover"
43
+ />
44
+ <div class="absolute inset-0 bg-gradient-to-t from-bg-200 via-transparent to-transparent" />
45
+ </div>
46
+ </Show>
47
+
48
+ <div class="p-4 pt-2">
49
+ <h3 class="font-bold text-xl leading-tight bg-gradient-to-r from-accent to-accent-300 bg-clip-text text-transparent">
50
+ {props.title}
51
+ </h3>
52
+
53
+ <Show when={props.date || props.author}>
54
+ <div class="flex items-center gap-4 mt-3 text-sm text-primary-300">
55
+ <Show when={props.author}>
56
+ <div class="flex items-center gap-1.5">
57
+ <span class="text-accent">@</span>
58
+ <span>{props.author}</span>
59
+ </div>
60
+ </Show>
61
+ <Show when={props.date}>
62
+ <div class="flex items-center gap-1.5">
63
+ <span class="text-accent">⏱</span>
64
+ <span>{props.date}</span>
65
+ </div>
66
+ </Show>
67
+ </div>
68
+ </Show>
69
+
70
+ <Show when={displayedAttendees().length > 0}>
71
+ <div class="flex items-center mt-3">
72
+ <div class="flex -space-x-2">
73
+ <For each={displayedAttendees()}>
74
+ {(attendee) => (
75
+ <Avatar src={attendee.avatar} alt={attendee.name} size="sm" />
76
+ )}
77
+ </For>
78
+ </div>
79
+ <Show when={remainingCount() > 0}>
80
+ <span class="ml-2 text-sm text-primary-300">
81
+ +{remainingCount()} más
82
+ </span>
83
+ </Show>
84
+ </div>
85
+ </Show>
86
+
87
+ <Show when={props.actions}>
88
+ <div class="mt-4 flex gap-2">{props.actions}</div>
89
+ </Show>
90
+ </div>
91
+ </div>
92
+ )
93
+ }
94
+
95
+ export interface EventListItemProps {
96
+ title: string
97
+ image?: string
98
+ subtitle?: string
99
+ onClick?: () => void
100
+ class?: string
101
+ }
102
+
103
+ export function EventListItem(props: EventListItemProps) {
104
+ return (
105
+ <button
106
+ class={`flex items-center gap-3 w-full p-2 rounded-lg hover:bg-bg-300 transition-colors text-left ${props.class ?? ''}`}
107
+ onClick={props.onClick}
108
+ >
109
+ <Show when={props.image}>
110
+ <div class="w-12 h-12 rounded-lg overflow-hidden flex-shrink-0">
111
+ <img
112
+ src={props.image}
113
+ alt={props.title}
114
+ class="w-full h-full object-cover"
115
+ />
116
+ </div>
117
+ </Show>
118
+ <div class="flex-1 min-w-0">
119
+ <h4 class="font-medium text-primary-100 truncate">{props.title}</h4>
120
+ <Show when={props.subtitle}>
121
+ <p class="text-sm text-primary-300 truncate">{props.subtitle}</p>
122
+ </Show>
123
+ </div>
124
+ </button>
125
+ )
126
+ }
@@ -0,0 +1,88 @@
1
+ import type { JSX } from 'solid-js'
2
+ import { Show, For } from 'solid-js'
3
+
4
+ export interface NavItemProps {
5
+ title: string
6
+ children?: JSX.Element
7
+ class?: string
8
+ }
9
+
10
+ export function NavItem(props: NavItemProps) {
11
+ return (
12
+ <li class={`flex items-center ${props.class ?? ''}`}>
13
+ <span class="text-lg font-bold text-primary-200">{props.title}</span>
14
+ {props.children}
15
+ </li>
16
+ )
17
+ }
18
+
19
+ export interface NavLinkProps {
20
+ href: string
21
+ children: JSX.Element
22
+ active?: boolean
23
+ class?: string
24
+ }
25
+
26
+ export function NavLink(props: NavLinkProps) {
27
+ const activeStyles = 'font-medium text-primary-300 underline underline-offset-4'
28
+ const inactiveStyles = 'font-normal text-gray-200 underline-offset-4 hover:text-gray-100 hover:underline'
29
+
30
+ return (
31
+ <a
32
+ href={props.href}
33
+ class={`${props.active ? activeStyles : inactiveStyles} ${props.class ?? ''}`}
34
+ >
35
+ {props.children}
36
+ </a>
37
+ )
38
+ }
39
+
40
+ export interface NavSectionProps {
41
+ title: string
42
+ links?: { href: string; label: string; active?: boolean }[]
43
+ children?: JSX.Element
44
+ class?: string
45
+ }
46
+
47
+ export function NavSection(props: NavSectionProps) {
48
+ return (
49
+ <div class={props.class ?? ''}>
50
+ <NavItem title={props.title} />
51
+ <div class="flex h-full">
52
+ <div class="h-5 w-1 bg-accent-300" />
53
+ <ul class="flex h-full flex-1 flex-col gap-1 pl-4">
54
+ <Show when={props.links}>
55
+ <For each={props.links}>
56
+ {(link) => (
57
+ <li>
58
+ <NavLink href={link.href} active={link.active}>
59
+ {link.label}
60
+ </NavLink>
61
+ </li>
62
+ )}
63
+ </For>
64
+ </Show>
65
+ {props.children}
66
+ </ul>
67
+ </div>
68
+ </div>
69
+ )
70
+ }
71
+
72
+ export interface LateralNavProps {
73
+ transparent?: boolean
74
+ children?: JSX.Element
75
+ class?: string
76
+ }
77
+
78
+ export function LateralNav(props: LateralNavProps) {
79
+ const bgColor = () => (props.transparent ? '' : 'bg-bg-200')
80
+
81
+ return (
82
+ <div
83
+ class={`hidden w-[300px] md:block ${bgColor()} m-0 border-r border-primary-600 p-3 ${props.class ?? ''}`}
84
+ >
85
+ {props.children}
86
+ </div>
87
+ )
88
+ }
@@ -0,0 +1,54 @@
1
+ import { JSX } from "solid-js";
2
+
3
+ export type LogoSize = "sm" | "md" | "lg" | "xl";
4
+
5
+ export interface LogoProps {
6
+ /** First word (light weight, muted color) */
7
+ firstWord?: string;
8
+ /** Second word (bold, with 3D effect) */
9
+ secondWord?: string;
10
+ /** Size variant of the logo */
11
+ size?: LogoSize;
12
+ /** Additional CSS classes */
13
+ class?: string;
14
+ }
15
+
16
+ /**
17
+ * Two-word logo with retro synthwave 3D effect.
18
+ * First word is light/muted, second word has bold styling with pink 3D shadow.
19
+ *
20
+ * @example
21
+ * // Default usage
22
+ * <Logo />
23
+ *
24
+ * @example
25
+ * // Custom text
26
+ * <Logo firstWord="My" secondWord="Brand" size="lg" />
27
+ */
28
+ export function Logo(props: LogoProps): JSX.Element {
29
+ const sizeClass = () => {
30
+ switch (props.size) {
31
+ case "sm":
32
+ return "vui-logo--sm";
33
+ case "lg":
34
+ return "vui-logo--lg";
35
+ case "xl":
36
+ return "vui-logo--xl";
37
+ case "md":
38
+ default:
39
+ return "vui-logo--md";
40
+ }
41
+ };
42
+
43
+ const firstWord = () => props.firstWord ?? "Proyecto";
44
+ const secondWord = () => props.secondWord ?? "Viviana";
45
+
46
+ return (
47
+ <span class={`vui-logo ${sizeClass()} ${props.class ?? ""}`}>
48
+ <span class="vui-logo__first">{firstWord()}</span>
49
+ <span class="vui-logo__second" data-text={secondWord()}>
50
+ {secondWord()}
51
+ </span>
52
+ </span>
53
+ );
54
+ }
@@ -0,0 +1,42 @@
1
+ import type { JSX } from 'solid-js'
2
+ import { Show } from 'solid-js'
3
+
4
+ export interface NavHeaderProps {
5
+ logo?: string
6
+ logoAlt?: string
7
+ logoText?: string
8
+ children?: JSX.Element
9
+ menuIcon?: JSX.Element
10
+ onMenuClick?: () => void
11
+ class?: string
12
+ }
13
+
14
+ export function NavHeader(props: NavHeaderProps) {
15
+ return (
16
+ <nav class={`flex items-center bg-bg-400 h-[70px] border-b-4 border-accent-500 ${props.class ?? ''}`}>
17
+ <div class="pl-1 md:pl-8 flex items-center">
18
+ <Show when={props.logo} fallback={
19
+ <Show when={props.logoText}>
20
+ <span class="text-[34px] font-light text-primary-700 flex items-center">
21
+ {props.logoText}
22
+ </span>
23
+ </Show>
24
+ }>
25
+ <img src={props.logo} alt={props.logoAlt ?? 'Logo'} class="h-[42px] w-auto" />
26
+ </Show>
27
+ </div>
28
+
29
+ <div class="flex-1 flex justify-end items-center pr-1 md:pr-8">
30
+ {props.children}
31
+ <Show when={props.menuIcon}>
32
+ <button
33
+ class="md:hidden flex items-center justify-center"
34
+ onClick={props.onMenuClick}
35
+ >
36
+ {props.menuIcon}
37
+ </button>
38
+ </Show>
39
+ </div>
40
+ </nav>
41
+ )
42
+ }
@@ -0,0 +1,20 @@
1
+ import { JSX, splitProps } from 'solid-js'
2
+
3
+ export interface PageLayoutProps extends JSX.HTMLAttributes<HTMLDivElement> {
4
+ /** Content of the page */
5
+ children: JSX.Element
6
+ }
7
+
8
+ /**
9
+ * PageLayout provides consistent page structure with proper background and font styling.
10
+ * Use this as the root wrapper for all pages.
11
+ */
12
+ export function PageLayout(props: PageLayoutProps) {
13
+ const [local, rest] = splitProps(props, ['children', 'class'])
14
+
15
+ return (
16
+ <div class={`vui-page ${local.class ?? ''}`} {...rest}>
17
+ {local.children}
18
+ </div>
19
+ )
20
+ }
@@ -0,0 +1,58 @@
1
+ import type { JSX } from 'solid-js'
2
+ import { Show } from 'solid-js'
3
+ import { Avatar } from '../../avatar'
4
+
5
+ export interface ProfileCardProps {
6
+ username: string
7
+ avatar?: string
8
+ bio?: string
9
+ followers?: number
10
+ following?: number
11
+ actions?: JSX.Element
12
+ class?: string
13
+ }
14
+
15
+ export function ProfileCard(props: ProfileCardProps) {
16
+ const formatNumber = (num: number) => {
17
+ if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`
18
+ if (num >= 1000) return `${(num / 1000).toFixed(1)}K`
19
+ return num.toString()
20
+ }
21
+
22
+ return (
23
+ <div class={`bg-bg-200 rounded-xl p-4 ${props.class ?? ''}`}>
24
+ <div class="flex items-start gap-4">
25
+ <Avatar src={props.avatar} alt={props.username} size="lg" />
26
+ <div class="flex-1 min-w-0">
27
+ <h3 class="font-semibold text-primary-100 text-lg truncate">
28
+ {props.username}
29
+ </h3>
30
+ <Show when={props.bio}>
31
+ <p class="text-primary-300 text-sm mt-1 line-clamp-2">{props.bio}</p>
32
+ </Show>
33
+ <div class="flex gap-4 mt-2 text-sm">
34
+ <Show when={props.followers !== undefined}>
35
+ <span class="text-primary-300">
36
+ <span class="font-semibold text-primary-100">
37
+ {formatNumber(props.followers!)}
38
+ </span>{' '}
39
+ seguidores
40
+ </span>
41
+ </Show>
42
+ <Show when={props.following !== undefined}>
43
+ <span class="text-primary-300">
44
+ <span class="font-semibold text-primary-100">
45
+ {formatNumber(props.following!)}
46
+ </span>{' '}
47
+ siguiendo
48
+ </span>
49
+ </Show>
50
+ </div>
51
+ </div>
52
+ </div>
53
+ <Show when={props.actions}>
54
+ <div class="mt-4 flex gap-2">{props.actions}</div>
55
+ </Show>
56
+ </div>
57
+ )
58
+ }
@@ -0,0 +1,59 @@
1
+ import type { JSX } from 'solid-js'
2
+
3
+ export type ProjectCardSize = 'sm' | 'md' | 'lg'
4
+
5
+ export interface ProjectCardProps {
6
+ /** Project name shown in tooltip on hover */
7
+ name: string
8
+ /** Image source for the project logo */
9
+ imageSrc: string
10
+ /** Alt text for the image */
11
+ imageAlt?: string
12
+ /** Optional link to the project */
13
+ href?: string
14
+ /** Size of the card */
15
+ size?: ProjectCardSize
16
+ /** Whether the project is inactive/greyed out */
17
+ inactive?: boolean
18
+ /** Additional CSS class */
19
+ class?: string
20
+ }
21
+
22
+ /**
23
+ * Project card with logo and hover tooltip.
24
+ * Used for showcasing ecosystem projects.
25
+ */
26
+ export function ProjectCard(props: ProjectCardProps): JSX.Element {
27
+ const size = () => props.size ?? 'sm'
28
+ const inactive = () => props.inactive ?? false
29
+
30
+ const cardContent = () => (
31
+ <>
32
+ <div class="vui-project-card__tooltip">
33
+ <span>{props.name}</span>
34
+ </div>
35
+ <img
36
+ class="vui-project-card__image"
37
+ src={props.imageSrc}
38
+ alt={props.imageAlt ?? props.name}
39
+ />
40
+ </>
41
+ )
42
+
43
+ const cardClasses = () =>
44
+ `vui-project-card vui-project-card--${size()} ${inactive() ? 'vui-project-card--inactive' : ''} ${props.class ?? ''}`
45
+
46
+ if (props.href) {
47
+ return (
48
+ <a href={props.href} target="_blank" rel="noopener noreferrer" class={cardClasses()}>
49
+ {cardContent()}
50
+ </a>
51
+ )
52
+ }
53
+
54
+ return (
55
+ <div class={cardClasses()}>
56
+ {cardContent()}
57
+ </div>
58
+ )
59
+ }