@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.
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
- package/src/alert/index.tsx +48 -0
- package/src/assets/favicon.png +0 -0
- package/src/assets/fire.gif +0 -0
- package/src/avatar/index.tsx +75 -0
- package/src/badge/index.tsx +43 -0
- package/src/button/Button.tsx +75 -0
- package/src/button/index.ts +2 -0
- package/src/button/types.ts +24 -0
- package/src/checkbox/index.tsx +258 -0
- package/src/components.css +975 -0
- package/src/custom/calendar-card/index.tsx +66 -0
- package/src/custom/chip/index.tsx +32 -0
- package/src/custom/conversation/index.tsx +105 -0
- package/src/custom/event-card/index.tsx +126 -0
- package/src/custom/lateral-nav/index.tsx +88 -0
- package/src/custom/logo/index.tsx +54 -0
- package/src/custom/nav-header/index.tsx +42 -0
- package/src/custom/page-layout/index.tsx +20 -0
- package/src/custom/profile-card/index.tsx +58 -0
- package/src/custom/project-card/index.tsx +59 -0
- package/src/custom/timeline-item/index.tsx +81 -0
- package/src/dialog/index.tsx +69 -0
- package/src/icon/icons/GitHubIcon.tsx +20 -0
- package/src/icon/index.tsx +48 -0
- package/src/index.ts +107 -0
- package/src/link/index.tsx +131 -0
- package/src/progress-bar/index.tsx +169 -0
- package/src/radio/index.tsx +219 -0
- package/src/separator/index.tsx +141 -0
- package/src/styles.css +450 -0
- package/src/switch/ToggleSwitch.tsx +113 -0
- package/src/switch/index.tsx +90 -0
- package/src/test-utils/index.ts +32 -0
- package/src/textfield/index.tsx +211 -0
- package/src/theme.css +101 -0
- package/src/tooltip/index.tsx +31 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { JSX } from 'solid-js'
|
|
2
|
+
import { Show } from 'solid-js'
|
|
3
|
+
import { Avatar } from '../../avatar'
|
|
4
|
+
|
|
5
|
+
export type TimelineEventType = 'follow' | 'like' | 'comment' | 'event' | 'custom'
|
|
6
|
+
|
|
7
|
+
export interface TimelineItemProps {
|
|
8
|
+
type?: TimelineEventType
|
|
9
|
+
icon?: JSX.Element
|
|
10
|
+
leftUser?: {
|
|
11
|
+
name: string
|
|
12
|
+
avatar?: string
|
|
13
|
+
}
|
|
14
|
+
rightUser?: {
|
|
15
|
+
name: string
|
|
16
|
+
avatar?: string
|
|
17
|
+
}
|
|
18
|
+
message?: JSX.Element
|
|
19
|
+
class?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const eventMessages: Record<TimelineEventType, (left: string, right: string) => JSX.Element> = {
|
|
23
|
+
follow: (left, right) => (
|
|
24
|
+
<>
|
|
25
|
+
<span class="font-semibold text-accent-200">{left}</span>
|
|
26
|
+
{' ha empezado a seguir a '}
|
|
27
|
+
<span class="font-semibold text-accent-200">{right}</span>
|
|
28
|
+
</>
|
|
29
|
+
),
|
|
30
|
+
like: (left, right) => (
|
|
31
|
+
<>
|
|
32
|
+
<span class="font-semibold text-accent-200">{left}</span>
|
|
33
|
+
{' le ha dado like a '}
|
|
34
|
+
<span class="font-semibold text-accent-200">{right}</span>
|
|
35
|
+
</>
|
|
36
|
+
),
|
|
37
|
+
comment: (left, right) => (
|
|
38
|
+
<>
|
|
39
|
+
<span class="font-semibold text-accent-200">{left}</span>
|
|
40
|
+
{' ha comentado en '}
|
|
41
|
+
<span class="font-semibold text-accent-200">{right}</span>
|
|
42
|
+
</>
|
|
43
|
+
),
|
|
44
|
+
event: (left, right) => (
|
|
45
|
+
<>
|
|
46
|
+
<span class="font-semibold text-accent-200">{left}</span>
|
|
47
|
+
{' asistirá al evento de '}
|
|
48
|
+
<span class="font-semibold text-accent-200">{right}</span>
|
|
49
|
+
</>
|
|
50
|
+
),
|
|
51
|
+
custom: () => null,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function TimelineItem(props: TimelineItemProps) {
|
|
55
|
+
const type = () => props.type ?? 'follow'
|
|
56
|
+
const leftName = () => props.leftUser?.name ?? ''
|
|
57
|
+
const rightName = () => props.rightUser?.name ?? ''
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div class={`inline-flex w-auto flex-col gap-5 rounded-2xl border border-primary-700 bg-bg-200 p-5 hover:bg-bg-300 transition-colors ${props.class ?? ''}`}>
|
|
61
|
+
<div class="flex items-center justify-around gap-3">
|
|
62
|
+
<Show when={props.leftUser}>
|
|
63
|
+
<Avatar src={props.leftUser!.avatar} alt={props.leftUser!.name} />
|
|
64
|
+
</Show>
|
|
65
|
+
<Show when={props.icon}>
|
|
66
|
+
{props.icon}
|
|
67
|
+
</Show>
|
|
68
|
+
<Show when={props.rightUser}>
|
|
69
|
+
<Avatar src={props.rightUser!.avatar} alt={props.rightUser!.name} />
|
|
70
|
+
</Show>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="flex items-center justify-center gap-3 text-center">
|
|
73
|
+
<span class="font-light text-primary-300">
|
|
74
|
+
<Show when={props.message} fallback={eventMessages[type()](leftName(), rightName())}>
|
|
75
|
+
{props.message}
|
|
76
|
+
</Show>
|
|
77
|
+
</span>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { JSX } from 'solid-js'
|
|
2
|
+
import { Show } from 'solid-js'
|
|
3
|
+
import { Button } from '../button'
|
|
4
|
+
import { Chip } from '../custom/chip'
|
|
5
|
+
|
|
6
|
+
export interface DialogProps {
|
|
7
|
+
title: string
|
|
8
|
+
subtitle?: string
|
|
9
|
+
children: JSX.Element
|
|
10
|
+
tags?: string[]
|
|
11
|
+
primaryAction?: {
|
|
12
|
+
label: string
|
|
13
|
+
onClick: () => void
|
|
14
|
+
}
|
|
15
|
+
secondaryAction?: {
|
|
16
|
+
label: string
|
|
17
|
+
onClick: () => void
|
|
18
|
+
}
|
|
19
|
+
class?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function Dialog(props: DialogProps) {
|
|
23
|
+
return (
|
|
24
|
+
<div class={`flex flex-col w-full max-w-[540px] ${props.class ?? ''}`}>
|
|
25
|
+
<span class="relative left-8 top-4 bg-gradient-to-b from-primary-100 to-primary-500 bg-clip-text text-4xl font-extrabold text-transparent">
|
|
26
|
+
{props.title}
|
|
27
|
+
</span>
|
|
28
|
+
<div class="flex h-[50px] flex-col justify-end rounded-t-[26px] bg-primary-800">
|
|
29
|
+
<div class="flex h-[40px] flex-col justify-end rounded-t-[30px] bg-primary-600">
|
|
30
|
+
<div class="flex h-[20px] items-center justify-end rounded-t-[100px] bg-primary-700 pr-4">
|
|
31
|
+
<Show when={props.subtitle}>
|
|
32
|
+
<span class="text-base font-normal text-primary-300">
|
|
33
|
+
{props.subtitle}
|
|
34
|
+
</span>
|
|
35
|
+
</Show>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="mt-1 flex min-h-[200px] flex-col rounded-b-[26px] rounded-t-lg border-b border-accent-500 bg-bg-300">
|
|
40
|
+
<div class="flex flex-1 flex-col p-4">
|
|
41
|
+
<div class="text-lg text-gray-100">{props.children}</div>
|
|
42
|
+
<Show when={props.primaryAction || props.secondaryAction}>
|
|
43
|
+
<div class="flex flex-1 flex-row items-end justify-center gap-5 py-5">
|
|
44
|
+
<Show when={props.secondaryAction}>
|
|
45
|
+
<Button variant="primary" buttonStyle="outline" onPress={props.secondaryAction!.onClick}>
|
|
46
|
+
{props.secondaryAction!.label}
|
|
47
|
+
</Button>
|
|
48
|
+
</Show>
|
|
49
|
+
<Show when={props.primaryAction}>
|
|
50
|
+
<Button variant="primary" onPress={props.primaryAction!.onClick}>
|
|
51
|
+
{props.primaryAction!.label}
|
|
52
|
+
</Button>
|
|
53
|
+
</Show>
|
|
54
|
+
</div>
|
|
55
|
+
</Show>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
<Show when={props.tags && props.tags.length > 0}>
|
|
59
|
+
<div class="relative bottom-4 right-4 w-full">
|
|
60
|
+
<div class="flex flex-row justify-end gap-2 pr-4">
|
|
61
|
+
{props.tags!.map((tag) => (
|
|
62
|
+
<Chip variant="primary" text={tag} />
|
|
63
|
+
))}
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</Show>
|
|
67
|
+
</div>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { JSX } from 'solid-js';
|
|
2
|
+
|
|
3
|
+
export interface GitHubIconProps {
|
|
4
|
+
size?: number;
|
|
5
|
+
color?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function GitHubIcon(props: GitHubIconProps): JSX.Element {
|
|
9
|
+
return (
|
|
10
|
+
<svg
|
|
11
|
+
viewBox="0 0 24 24"
|
|
12
|
+
width={props.size}
|
|
13
|
+
height={props.size}
|
|
14
|
+
fill={props.color}
|
|
15
|
+
aria-hidden="true"
|
|
16
|
+
>
|
|
17
|
+
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
|
18
|
+
</svg>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Component, JSX } from 'solid-js'
|
|
2
|
+
|
|
3
|
+
export interface IconProps {
|
|
4
|
+
/** The icon component to render (should accept size and color props) */
|
|
5
|
+
icon: Component<{ size?: string | number; color?: string }>
|
|
6
|
+
/** Size of the icon (e.g., '24px' or 24) */
|
|
7
|
+
size?: string | number
|
|
8
|
+
/** Color of the icon */
|
|
9
|
+
color?: string
|
|
10
|
+
/** Whether to show the accent shadow effect (4px offset to bottom) */
|
|
11
|
+
withShadow?: boolean
|
|
12
|
+
/** Additional CSS class */
|
|
13
|
+
class?: string
|
|
14
|
+
/** Click handler */
|
|
15
|
+
onClick?: () => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Icon wrapper component with optional accent shadow effect.
|
|
20
|
+
*
|
|
21
|
+
* The shadow effect creates a 4px offset accent-colored duplicate
|
|
22
|
+
* of the icon behind it for a stylized look.
|
|
23
|
+
*/
|
|
24
|
+
export function Icon(props: IconProps): JSX.Element {
|
|
25
|
+
const size = () => props.size ?? 24
|
|
26
|
+
const color = () => props.color ?? 'var(--color-primary-500)'
|
|
27
|
+
const IconComponent = props.icon
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
class={`vui-icon ${props.withShadow ? 'vui-icon--with-shadow' : ''} ${props.class ?? ''}`}
|
|
32
|
+
onClick={props.onClick}
|
|
33
|
+
>
|
|
34
|
+
{props.withShadow && (
|
|
35
|
+
<div class="vui-icon__shadow">
|
|
36
|
+
<IconComponent size={size()} color="var(--color-accent)" />
|
|
37
|
+
</div>
|
|
38
|
+
)}
|
|
39
|
+
<div class="vui-icon__main">
|
|
40
|
+
<IconComponent size={size()} color={color()} />
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Re-export common icons
|
|
47
|
+
export { GitHubIcon } from './icons/GitHubIcon'
|
|
48
|
+
export type { GitHubIconProps } from './icons/GitHubIcon'
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// SPECTRUM-BASED COMPONENTS
|
|
3
|
+
// ============================================
|
|
4
|
+
|
|
5
|
+
// Button
|
|
6
|
+
export { Button } from './button';
|
|
7
|
+
export type { ButtonProps, ButtonVariant, ButtonStyle, StaticColor } from './button';
|
|
8
|
+
|
|
9
|
+
// Badge
|
|
10
|
+
export { Badge } from './badge';
|
|
11
|
+
export type { BadgeProps, BadgeVariant, BadgeSize } from './badge';
|
|
12
|
+
|
|
13
|
+
// Alert
|
|
14
|
+
export { Alert } from './alert';
|
|
15
|
+
export type { AlertProps, AlertVariant } from './alert';
|
|
16
|
+
|
|
17
|
+
// Avatar
|
|
18
|
+
export { Avatar, AvatarGroup } from './avatar';
|
|
19
|
+
export type { AvatarProps, AvatarGroupProps, AvatarSize } from './avatar';
|
|
20
|
+
|
|
21
|
+
// Switch
|
|
22
|
+
export { TabSwitch, ToggleSwitch } from './switch';
|
|
23
|
+
export type { TabSwitchProps, ToggleSwitchProps, SwitchSize } from './switch';
|
|
24
|
+
|
|
25
|
+
// Checkbox
|
|
26
|
+
export { Checkbox, CheckboxGroup } from './checkbox';
|
|
27
|
+
export type { CheckboxProps, CheckboxGroupProps, CheckboxSize } from './checkbox';
|
|
28
|
+
|
|
29
|
+
// Radio
|
|
30
|
+
export { RadioGroup, Radio } from './radio';
|
|
31
|
+
export type { RadioGroupProps, RadioProps, RadioGroupOrientation, RadioGroupSize } from './radio';
|
|
32
|
+
|
|
33
|
+
// Dialog
|
|
34
|
+
export { Dialog } from './dialog';
|
|
35
|
+
export type { DialogProps } from './dialog';
|
|
36
|
+
|
|
37
|
+
// Icon
|
|
38
|
+
export { Icon, GitHubIcon } from './icon';
|
|
39
|
+
export type { IconProps, GitHubIconProps } from './icon';
|
|
40
|
+
|
|
41
|
+
// Tooltip
|
|
42
|
+
export { Tooltip } from './tooltip';
|
|
43
|
+
export type { TooltipProps } from './tooltip';
|
|
44
|
+
|
|
45
|
+
// TextField
|
|
46
|
+
export { TextField } from './textfield';
|
|
47
|
+
export type { TextFieldProps, TextFieldSize, TextFieldVariant } from './textfield';
|
|
48
|
+
|
|
49
|
+
// Link
|
|
50
|
+
export { Link } from './link';
|
|
51
|
+
export type { LinkProps, LinkVariant } from './link';
|
|
52
|
+
|
|
53
|
+
// ProgressBar
|
|
54
|
+
export { ProgressBar } from './progress-bar';
|
|
55
|
+
export type { ProgressBarProps, ProgressBarSize, ProgressBarVariant } from './progress-bar';
|
|
56
|
+
|
|
57
|
+
// Separator
|
|
58
|
+
export { Separator } from './separator';
|
|
59
|
+
export type { SeparatorProps, SeparatorVariant, SeparatorSize } from './separator';
|
|
60
|
+
|
|
61
|
+
// ============================================
|
|
62
|
+
// CUSTOM COMPONENTS
|
|
63
|
+
// ============================================
|
|
64
|
+
|
|
65
|
+
// Chip
|
|
66
|
+
export { Chip } from './custom/chip';
|
|
67
|
+
export type { ChipProps, ChipVariant } from './custom/chip';
|
|
68
|
+
|
|
69
|
+
// NavHeader
|
|
70
|
+
export { NavHeader } from './custom/nav-header';
|
|
71
|
+
export type { NavHeaderProps } from './custom/nav-header';
|
|
72
|
+
|
|
73
|
+
// LateralNav
|
|
74
|
+
export { LateralNav, NavItem, NavLink, NavSection } from './custom/lateral-nav';
|
|
75
|
+
export type { LateralNavProps, NavItemProps, NavLinkProps, NavSectionProps } from './custom/lateral-nav';
|
|
76
|
+
|
|
77
|
+
// TimelineItem
|
|
78
|
+
export { TimelineItem } from './custom/timeline-item';
|
|
79
|
+
export type { TimelineItemProps, TimelineEventType } from './custom/timeline-item';
|
|
80
|
+
|
|
81
|
+
// Conversation
|
|
82
|
+
export { Conversation, ConversationPreview, ConversationBubble } from './custom/conversation';
|
|
83
|
+
export type { ConversationProps, ConversationPreviewProps, ConversationBubbleProps, Message } from './custom/conversation';
|
|
84
|
+
|
|
85
|
+
// ProfileCard
|
|
86
|
+
export { ProfileCard } from './custom/profile-card';
|
|
87
|
+
export type { ProfileCardProps } from './custom/profile-card';
|
|
88
|
+
|
|
89
|
+
// EventCard
|
|
90
|
+
export { EventCard, EventListItem } from './custom/event-card';
|
|
91
|
+
export type { EventCardProps, EventListItemProps } from './custom/event-card';
|
|
92
|
+
|
|
93
|
+
// CalendarCard
|
|
94
|
+
export { CalendarCard } from './custom/calendar-card';
|
|
95
|
+
export type { CalendarCardProps } from './custom/calendar-card';
|
|
96
|
+
|
|
97
|
+
// Logo
|
|
98
|
+
export { Logo } from './custom/logo';
|
|
99
|
+
export type { LogoProps, LogoSize } from './custom/logo';
|
|
100
|
+
|
|
101
|
+
// ProjectCard
|
|
102
|
+
export { ProjectCard } from './custom/project-card';
|
|
103
|
+
export type { ProjectCardProps, ProjectCardSize } from './custom/project-card';
|
|
104
|
+
|
|
105
|
+
// PageLayout
|
|
106
|
+
export { PageLayout } from './custom/page-layout';
|
|
107
|
+
export type { PageLayoutProps } from './custom/page-layout';
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Link component for proyecto-viviana-ui
|
|
3
|
+
*
|
|
4
|
+
* Styled link component built on top of solidaria-components.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { type JSX, splitProps } from 'solid-js';
|
|
8
|
+
import {
|
|
9
|
+
Link as HeadlessLink,
|
|
10
|
+
type LinkProps as HeadlessLinkProps,
|
|
11
|
+
type LinkRenderProps,
|
|
12
|
+
} from '@proyecto-viviana/solidaria-components';
|
|
13
|
+
|
|
14
|
+
// ============================================
|
|
15
|
+
// TYPES
|
|
16
|
+
// ============================================
|
|
17
|
+
|
|
18
|
+
export type LinkVariant = 'primary' | 'secondary' | 'subtle';
|
|
19
|
+
|
|
20
|
+
export interface LinkProps extends Omit<HeadlessLinkProps, 'class' | 'style' | 'children'> {
|
|
21
|
+
/** The visual style of the link. @default 'primary' */
|
|
22
|
+
variant?: LinkVariant;
|
|
23
|
+
/** Whether the link is on its own vs inside a longer string of text. */
|
|
24
|
+
isStandalone?: boolean;
|
|
25
|
+
/** Whether the link should be displayed with a quiet style (no underline by default). */
|
|
26
|
+
isQuiet?: boolean;
|
|
27
|
+
/** Additional CSS class name. */
|
|
28
|
+
class?: string;
|
|
29
|
+
/** The content of the link. */
|
|
30
|
+
children?: JSX.Element;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ============================================
|
|
34
|
+
// STYLES
|
|
35
|
+
// ============================================
|
|
36
|
+
|
|
37
|
+
const variantStyles = {
|
|
38
|
+
primary: 'text-accent hover:text-accent-300',
|
|
39
|
+
secondary: 'text-primary-300 hover:text-primary-200',
|
|
40
|
+
subtle: 'text-primary-400 hover:text-primary-300',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// ============================================
|
|
44
|
+
// LINK COMPONENT
|
|
45
|
+
// ============================================
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Links allow users to navigate to a different location.
|
|
49
|
+
* They can be presented inline inside a paragraph or as standalone text.
|
|
50
|
+
*
|
|
51
|
+
* Built on solidaria-components Link for full accessibility support.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```tsx
|
|
55
|
+
* <Link href="/about">About Us</Link>
|
|
56
|
+
*
|
|
57
|
+
* // Secondary variant
|
|
58
|
+
* <Link href="/help" variant="secondary">Help</Link>
|
|
59
|
+
*
|
|
60
|
+
* // Standalone (bold, no underline until hover)
|
|
61
|
+
* <Link href="/home" isStandalone isQuiet>Home</Link>
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export function Link(props: LinkProps): JSX.Element {
|
|
65
|
+
const [local, headlessProps] = splitProps(props, [
|
|
66
|
+
'variant',
|
|
67
|
+
'isStandalone',
|
|
68
|
+
'isQuiet',
|
|
69
|
+
'class',
|
|
70
|
+
'children',
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
const variant = local.variant ?? 'primary';
|
|
74
|
+
const customClass = local.class ?? '';
|
|
75
|
+
|
|
76
|
+
// Generate class based on render props
|
|
77
|
+
const getClassName = (renderProps: LinkRenderProps): string => {
|
|
78
|
+
const base = 'transition-colors duration-200 cursor-pointer rounded-sm outline-none';
|
|
79
|
+
|
|
80
|
+
// Variant colors
|
|
81
|
+
const variantClass = variantStyles[variant];
|
|
82
|
+
|
|
83
|
+
// Underline behavior
|
|
84
|
+
let underlineClass: string;
|
|
85
|
+
if (local.isStandalone && local.isQuiet) {
|
|
86
|
+
// Quiet standalone: no underline by default, underline on hover/focus
|
|
87
|
+
underlineClass = renderProps.isHovered || renderProps.isFocusVisible
|
|
88
|
+
? 'underline'
|
|
89
|
+
: 'no-underline';
|
|
90
|
+
} else {
|
|
91
|
+
// Inline links always have underline for accessibility
|
|
92
|
+
underlineClass = 'underline';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Font weight for standalone
|
|
96
|
+
const weightClass = local.isStandalone ? 'font-medium' : '';
|
|
97
|
+
|
|
98
|
+
// Focus ring
|
|
99
|
+
const focusClass = renderProps.isFocusVisible
|
|
100
|
+
? 'ring-2 ring-accent-300 ring-offset-2 ring-offset-bg-400'
|
|
101
|
+
: '';
|
|
102
|
+
|
|
103
|
+
// Disabled state
|
|
104
|
+
const disabledClass = renderProps.isDisabled
|
|
105
|
+
? 'opacity-50 cursor-not-allowed'
|
|
106
|
+
: '';
|
|
107
|
+
|
|
108
|
+
// Pressed state
|
|
109
|
+
const pressedClass = renderProps.isPressed ? 'opacity-80' : '';
|
|
110
|
+
|
|
111
|
+
return [
|
|
112
|
+
base,
|
|
113
|
+
variantClass,
|
|
114
|
+
underlineClass,
|
|
115
|
+
weightClass,
|
|
116
|
+
focusClass,
|
|
117
|
+
disabledClass,
|
|
118
|
+
pressedClass,
|
|
119
|
+
customClass,
|
|
120
|
+
].filter(Boolean).join(' ');
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<HeadlessLink
|
|
125
|
+
{...headlessProps}
|
|
126
|
+
class={getClassName}
|
|
127
|
+
>
|
|
128
|
+
{local.children}
|
|
129
|
+
</HeadlessLink>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProgressBar component for proyecto-viviana-ui
|
|
3
|
+
*
|
|
4
|
+
* Styled progress bar component built on top of the solidaria hook directly.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { type JSX, splitProps, Show, createMemo } from 'solid-js';
|
|
8
|
+
import { createProgressBar } from '@proyecto-viviana/solidaria';
|
|
9
|
+
|
|
10
|
+
// ============================================
|
|
11
|
+
// TYPES
|
|
12
|
+
// ============================================
|
|
13
|
+
|
|
14
|
+
export type ProgressBarSize = 'sm' | 'md' | 'lg';
|
|
15
|
+
export type ProgressBarVariant = 'primary' | 'accent' | 'success' | 'warning' | 'danger';
|
|
16
|
+
|
|
17
|
+
export interface ProgressBarProps {
|
|
18
|
+
/** The current value (controlled). @default 0 */
|
|
19
|
+
value?: number;
|
|
20
|
+
/** The smallest value allowed. @default 0 */
|
|
21
|
+
minValue?: number;
|
|
22
|
+
/** The largest value allowed. @default 100 */
|
|
23
|
+
maxValue?: number;
|
|
24
|
+
/** The content to display as the value's label (e.g. "1 of 4"). */
|
|
25
|
+
valueLabel?: string;
|
|
26
|
+
/** Whether presentation is indeterminate when progress isn't known. */
|
|
27
|
+
isIndeterminate?: boolean;
|
|
28
|
+
/** The size of the progress bar. @default 'md' */
|
|
29
|
+
size?: ProgressBarSize;
|
|
30
|
+
/** The visual style variant. @default 'primary' */
|
|
31
|
+
variant?: ProgressBarVariant;
|
|
32
|
+
/** The label to display above the progress bar. */
|
|
33
|
+
label?: string;
|
|
34
|
+
/** Whether to show the value text. @default true for determinate progress */
|
|
35
|
+
showValueLabel?: boolean;
|
|
36
|
+
/** Additional CSS class name. */
|
|
37
|
+
class?: string;
|
|
38
|
+
/** An accessibility label. */
|
|
39
|
+
'aria-label'?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ============================================
|
|
43
|
+
// STYLES
|
|
44
|
+
// ============================================
|
|
45
|
+
|
|
46
|
+
const sizeStyles = {
|
|
47
|
+
sm: {
|
|
48
|
+
track: 'h-1',
|
|
49
|
+
text: 'text-xs',
|
|
50
|
+
},
|
|
51
|
+
md: {
|
|
52
|
+
track: 'h-2',
|
|
53
|
+
text: 'text-sm',
|
|
54
|
+
},
|
|
55
|
+
lg: {
|
|
56
|
+
track: 'h-3',
|
|
57
|
+
text: 'text-base',
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const variantStyles = {
|
|
62
|
+
primary: 'bg-primary-500',
|
|
63
|
+
accent: 'bg-accent',
|
|
64
|
+
success: 'bg-green-500',
|
|
65
|
+
warning: 'bg-yellow-500',
|
|
66
|
+
danger: 'bg-red-500',
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// ============================================
|
|
70
|
+
// UTILITIES
|
|
71
|
+
// ============================================
|
|
72
|
+
|
|
73
|
+
function clamp(value: number, min: number, max: number): number {
|
|
74
|
+
return Math.min(Math.max(value, min), max);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ============================================
|
|
78
|
+
// PROGRESSBAR COMPONENT
|
|
79
|
+
// ============================================
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Progress bars show either determinate or indeterminate progress of an operation
|
|
83
|
+
* over time.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```tsx
|
|
87
|
+
* <ProgressBar value={50} label="Loading..." />
|
|
88
|
+
*
|
|
89
|
+
* // Indeterminate
|
|
90
|
+
* <ProgressBar isIndeterminate label="Processing..." />
|
|
91
|
+
*
|
|
92
|
+
* // Different variants
|
|
93
|
+
* <ProgressBar value={75} variant="success" />
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
export function ProgressBar(props: ProgressBarProps): JSX.Element {
|
|
97
|
+
const [local, ariaProps] = splitProps(props, [
|
|
98
|
+
'size',
|
|
99
|
+
'variant',
|
|
100
|
+
'label',
|
|
101
|
+
'showValueLabel',
|
|
102
|
+
'class',
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
const size = () => local.size ?? 'md';
|
|
106
|
+
const variant = () => local.variant ?? 'primary';
|
|
107
|
+
const isIndeterminate = () => ariaProps.isIndeterminate ?? false;
|
|
108
|
+
const showValueLabel = () => local.showValueLabel ?? !isIndeterminate();
|
|
109
|
+
|
|
110
|
+
// Create progress bar aria props
|
|
111
|
+
const progressAria = createProgressBar({
|
|
112
|
+
get value() { return ariaProps.value; },
|
|
113
|
+
get minValue() { return ariaProps.minValue; },
|
|
114
|
+
get maxValue() { return ariaProps.maxValue; },
|
|
115
|
+
get valueLabel() { return ariaProps.valueLabel; },
|
|
116
|
+
get isIndeterminate() { return ariaProps.isIndeterminate; },
|
|
117
|
+
get label() { return local.label; },
|
|
118
|
+
get 'aria-label'() { return ariaProps['aria-label']; },
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Calculate percentage
|
|
122
|
+
const percentage = createMemo(() => {
|
|
123
|
+
if (isIndeterminate()) {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
const value = ariaProps.value ?? 0;
|
|
127
|
+
const minValue = ariaProps.minValue ?? 0;
|
|
128
|
+
const maxValue = ariaProps.maxValue ?? 100;
|
|
129
|
+
const clampedValue = clamp(value, minValue, maxValue);
|
|
130
|
+
return ((clampedValue - minValue) / (maxValue - minValue)) * 100;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Get value text from aria props
|
|
134
|
+
const valueText = () => progressAria.progressBarProps['aria-valuetext'] as string | undefined;
|
|
135
|
+
|
|
136
|
+
const sizeConfig = () => sizeStyles[size()];
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div
|
|
140
|
+
{...progressAria.progressBarProps}
|
|
141
|
+
class={`w-full ${local.class ?? ''}`}
|
|
142
|
+
>
|
|
143
|
+
{/* Label and value row */}
|
|
144
|
+
<Show when={local.label || showValueLabel()}>
|
|
145
|
+
<div class={`flex justify-between items-center mb-1 ${sizeConfig().text}`}>
|
|
146
|
+
<Show when={local.label}>
|
|
147
|
+
<span class="text-primary-200 font-medium">{local.label}</span>
|
|
148
|
+
</Show>
|
|
149
|
+
<Show when={showValueLabel() && !isIndeterminate()}>
|
|
150
|
+
<span class="text-primary-300">{valueText()}</span>
|
|
151
|
+
</Show>
|
|
152
|
+
</div>
|
|
153
|
+
</Show>
|
|
154
|
+
|
|
155
|
+
{/* Track */}
|
|
156
|
+
<div class={`w-full ${sizeConfig().track} bg-bg-300 rounded-full overflow-hidden`}>
|
|
157
|
+
{/* Fill */}
|
|
158
|
+
<div
|
|
159
|
+
class={`h-full rounded-full transition-all duration-300 ${variantStyles[variant()]} ${
|
|
160
|
+
isIndeterminate() ? 'animate-progress-indeterminate' : ''
|
|
161
|
+
}`}
|
|
162
|
+
style={{
|
|
163
|
+
width: isIndeterminate() ? '30%' : `${percentage()}%`,
|
|
164
|
+
}}
|
|
165
|
+
/>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
}
|