@mohammad-irfan/create-dashboard-kit 1.0.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 +153 -0
- package/bin/create-dashboard.js +12 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +223 -0
- package/dist/configBuilder.d.ts +8 -0
- package/dist/configBuilder.js +573 -0
- package/dist/generator.d.ts +9 -0
- package/dist/generator.js +223 -0
- package/dist/presets.d.ts +8 -0
- package/dist/presets.js +72 -0
- package/dist/prompts.d.ts +2 -0
- package/dist/prompts.js +121 -0
- package/dist/templateEngine.d.ts +3 -0
- package/dist/templateEngine.js +31 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.js +1 -0
- package/dist/validation.d.ts +16 -0
- package/dist/validation.js +73 -0
- package/package.json +56 -0
- package/templates/source/components/controls/Button.tsx +69 -0
- package/templates/source/components/controls/Input.tsx +59 -0
- package/templates/source/components/controls/SearchBar.tsx +44 -0
- package/templates/source/components/controls/Select.tsx +61 -0
- package/templates/source/components/controls/ThemeToggle.tsx +23 -0
- package/templates/source/components/feedback/NotificationCenter.tsx +151 -0
- package/templates/source/components/feedback/Skeleton.tsx +22 -0
- package/templates/source/components/feedback/StatusBadge.tsx +35 -0
- package/templates/source/components/feedback/statusBadgeStyles.ts +28 -0
- package/templates/source/components/icons/IconSet.tsx +235 -0
- package/templates/source/components/layout/AppShell.tsx +70 -0
- package/templates/source/components/layout/Header.tsx +90 -0
- package/templates/source/components/layout/PageHeader.tsx +27 -0
- package/templates/source/components/layout/Sidebar.tsx +202 -0
- package/templates/source/components/metrics/MetricRow.tsx +61 -0
- package/templates/source/components/metrics/StatCard.tsx +49 -0
- package/templates/source/components/navigation/NavItem.tsx +75 -0
- package/templates/source/components/navigation/ShortcutPill.tsx +39 -0
- package/templates/source/components/surfaces/EmptyState.tsx +36 -0
- package/templates/source/components/surfaces/HeroSurface.tsx +64 -0
- package/templates/source/components/surfaces/Panel.tsx +43 -0
- package/templates/source/components/surfaces/PriorityCard.tsx +75 -0
- package/templates/source/components/surfaces/QuietListCard.tsx +146 -0
- package/templates/source/design-system/animations.css +25 -0
- package/templates/source/design-system/tokens.css +74 -0
- package/templates/source/design-system/typography.css +34 -0
- package/templates/source/theme/ThemeContext.tsx +82 -0
- package/templates/source/theme/useTheme.ts +10 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface StatCardProps {
|
|
4
|
+
label: string
|
|
5
|
+
value: ReactNode
|
|
6
|
+
icon?: ReactNode
|
|
7
|
+
iconClassName?: string
|
|
8
|
+
hint?: ReactNode
|
|
9
|
+
onClick?: () => void
|
|
10
|
+
className?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function StatCard({
|
|
14
|
+
label,
|
|
15
|
+
value,
|
|
16
|
+
icon,
|
|
17
|
+
iconClassName,
|
|
18
|
+
hint,
|
|
19
|
+
onClick,
|
|
20
|
+
className = '',
|
|
21
|
+
}: StatCardProps) {
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
onClick={onClick}
|
|
25
|
+
className={`group rounded-2xl border border-border/40 bg-surface dark:bg-surface-elevated/40 p-5 shadow-2xs transition-all duration-200 hover:-translate-y-0.5 hover:border-primary/40 hover:shadow-md ${
|
|
26
|
+
onClick ? 'cursor-pointer' : ''
|
|
27
|
+
} ${className}`}
|
|
28
|
+
>
|
|
29
|
+
<div className="flex items-start justify-between gap-3">
|
|
30
|
+
<div className="min-w-0">
|
|
31
|
+
<p className="text-sm font-medium text-muted-foreground">{label}</p>
|
|
32
|
+
<p className="mt-2 text-3xl font-bold tracking-tight text-foreground tabular-nums group-hover:text-primary transition-colors">
|
|
33
|
+
{value}
|
|
34
|
+
</p>
|
|
35
|
+
{hint ? <p className="mt-1 text-xs text-muted-foreground/70">{hint}</p> : null}
|
|
36
|
+
</div>
|
|
37
|
+
{icon ? (
|
|
38
|
+
<span
|
|
39
|
+
className={`flex h-11 w-11 shrink-0 items-center justify-center rounded-xl transition-transform duration-200 group-hover:scale-105 ${
|
|
40
|
+
iconClassName ?? 'bg-primary/10 text-primary'
|
|
41
|
+
}`}
|
|
42
|
+
>
|
|
43
|
+
{icon}
|
|
44
|
+
</span>
|
|
45
|
+
) : null}
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { ComponentType, SVGProps } from 'react'
|
|
2
|
+
import { renderNamedIcon } from '../icons/IconSet.tsx'
|
|
3
|
+
|
|
4
|
+
export interface NavItemProps {
|
|
5
|
+
label: string
|
|
6
|
+
to: string
|
|
7
|
+
icon?: string | ComponentType<SVGProps<SVGSVGElement>>
|
|
8
|
+
badge?: string | number
|
|
9
|
+
active?: boolean
|
|
10
|
+
collapsed?: boolean
|
|
11
|
+
onClick?: () => void
|
|
12
|
+
className?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function NavItem({
|
|
16
|
+
label,
|
|
17
|
+
to,
|
|
18
|
+
icon: IconComponent,
|
|
19
|
+
badge,
|
|
20
|
+
active = false,
|
|
21
|
+
collapsed = false,
|
|
22
|
+
onClick,
|
|
23
|
+
className = '',
|
|
24
|
+
}: NavItemProps) {
|
|
25
|
+
const base = `group relative flex items-center rounded-full text-sm transition-all duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
|
|
26
|
+
collapsed ? 'justify-center h-10 w-10 mx-auto' : 'gap-3.5 px-4 py-2.5 mx-2'
|
|
27
|
+
}`
|
|
28
|
+
|
|
29
|
+
const stateClass = active
|
|
30
|
+
? `${base} bg-primary/12 font-medium text-primary shadow-2xs`
|
|
31
|
+
: `${base} font-normal text-muted-foreground hover:bg-muted/60 hover:text-foreground`
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<a
|
|
35
|
+
href={to}
|
|
36
|
+
onClick={(e) => {
|
|
37
|
+
if (onClick) {
|
|
38
|
+
e.preventDefault()
|
|
39
|
+
onClick()
|
|
40
|
+
}
|
|
41
|
+
}}
|
|
42
|
+
title={collapsed ? label : undefined}
|
|
43
|
+
aria-label={collapsed ? label : undefined}
|
|
44
|
+
aria-current={active ? 'page' : undefined}
|
|
45
|
+
className={`${stateClass} ${className}`}
|
|
46
|
+
>
|
|
47
|
+
{IconComponent ? (
|
|
48
|
+
typeof IconComponent === 'function' ? (
|
|
49
|
+
<IconComponent className="h-5 w-5 shrink-0" />
|
|
50
|
+
) : typeof IconComponent === 'string' ? (
|
|
51
|
+
renderNamedIcon(IconComponent, { className: 'h-5 w-5 shrink-0' })
|
|
52
|
+
) : (
|
|
53
|
+
<span className="h-5 w-5 shrink-0 flex items-center justify-center font-bold text-xs">
|
|
54
|
+
{IconComponent}
|
|
55
|
+
</span>
|
|
56
|
+
)
|
|
57
|
+
) : null}
|
|
58
|
+
|
|
59
|
+
{!collapsed ? <span className="truncate">{label}</span> : null}
|
|
60
|
+
|
|
61
|
+
{badge !== undefined ? (
|
|
62
|
+
<span
|
|
63
|
+
aria-hidden="true"
|
|
64
|
+
className={`inline-flex items-center justify-center rounded-full bg-primary font-bold text-primary-foreground ${
|
|
65
|
+
collapsed
|
|
66
|
+
? 'absolute -right-0.5 -top-0.5 h-4 min-w-4 px-1 text-[10px]'
|
|
67
|
+
: 'ml-auto h-5 min-w-5 px-1.5 text-xs'
|
|
68
|
+
}`}
|
|
69
|
+
>
|
|
70
|
+
{badge}
|
|
71
|
+
</span>
|
|
72
|
+
) : null}
|
|
73
|
+
</a>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ComponentType, ReactNode, SVGProps } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface ShortcutPillProps {
|
|
4
|
+
label: string
|
|
5
|
+
to: string
|
|
6
|
+
icon?: ComponentType<SVGProps<SVGSVGElement>> | ReactNode
|
|
7
|
+
onClick?: () => void
|
|
8
|
+
className?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ShortcutPill({
|
|
12
|
+
label,
|
|
13
|
+
to,
|
|
14
|
+
icon: IconComponent,
|
|
15
|
+
onClick,
|
|
16
|
+
className = '',
|
|
17
|
+
}: ShortcutPillProps) {
|
|
18
|
+
return (
|
|
19
|
+
<a
|
|
20
|
+
href={to}
|
|
21
|
+
onClick={(e) => {
|
|
22
|
+
if (onClick) {
|
|
23
|
+
e.preventDefault()
|
|
24
|
+
onClick()
|
|
25
|
+
}
|
|
26
|
+
}}
|
|
27
|
+
className={`inline-flex items-center gap-2 rounded-full border border-border/60 bg-surface dark:bg-surface-elevated/40 px-3.5 py-1.5 text-xs font-medium text-muted-foreground hover:border-primary/40 hover:bg-muted/60 hover:text-foreground transition-all shadow-2xs ${className}`}
|
|
28
|
+
>
|
|
29
|
+
{IconComponent ? (
|
|
30
|
+
typeof IconComponent === 'function' ? (
|
|
31
|
+
<IconComponent className="h-3.5 w-3.5 text-muted-foreground/80" />
|
|
32
|
+
) : (
|
|
33
|
+
IconComponent
|
|
34
|
+
)
|
|
35
|
+
) : null}
|
|
36
|
+
<span>{label}</span>
|
|
37
|
+
</a>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface EmptyStateProps {
|
|
4
|
+
title: string
|
|
5
|
+
description?: string
|
|
6
|
+
icon?: ReactNode
|
|
7
|
+
action?: ReactNode
|
|
8
|
+
className?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function EmptyState({
|
|
12
|
+
title,
|
|
13
|
+
description,
|
|
14
|
+
icon,
|
|
15
|
+
action,
|
|
16
|
+
className = '',
|
|
17
|
+
}: EmptyStateProps) {
|
|
18
|
+
return (
|
|
19
|
+
<div
|
|
20
|
+
className={`rounded-2xl border border-dashed border-border/70 bg-surface dark:bg-surface-elevated/40 p-10 sm:p-14 text-center shadow-2xs ${className}`}
|
|
21
|
+
>
|
|
22
|
+
{icon ? (
|
|
23
|
+
<div className="mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded-full bg-primary/10 text-primary">
|
|
24
|
+
{icon}
|
|
25
|
+
</div>
|
|
26
|
+
) : null}
|
|
27
|
+
<h3 className="text-base sm:text-lg font-bold text-foreground">{title}</h3>
|
|
28
|
+
{description ? (
|
|
29
|
+
<p className="mx-auto mt-1 max-w-md text-xs sm:text-sm text-muted-foreground leading-relaxed">
|
|
30
|
+
{description}
|
|
31
|
+
</p>
|
|
32
|
+
) : null}
|
|
33
|
+
{action ? <div className="mt-5 flex flex-wrap items-center justify-center gap-3">{action}</div> : null}
|
|
34
|
+
</div>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface HeroSurfaceProps {
|
|
4
|
+
greeting: string
|
|
5
|
+
title?: string
|
|
6
|
+
description?: string
|
|
7
|
+
statusContext?: ReactNode
|
|
8
|
+
actions?: ReactNode
|
|
9
|
+
illustration?: ReactNode
|
|
10
|
+
className?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function HeroSurface({
|
|
14
|
+
greeting,
|
|
15
|
+
title,
|
|
16
|
+
description,
|
|
17
|
+
statusContext,
|
|
18
|
+
actions,
|
|
19
|
+
illustration,
|
|
20
|
+
className = '',
|
|
21
|
+
}: HeroSurfaceProps) {
|
|
22
|
+
return (
|
|
23
|
+
<section
|
|
24
|
+
className={`relative overflow-hidden rounded-[28px] border border-border/40 bg-surface dark:bg-surface-elevated/70 p-8 sm:p-10 lg:p-12 shadow-2xs transition-colors ${className}`}
|
|
25
|
+
>
|
|
26
|
+
<div className="flex flex-col justify-between gap-8 lg:flex-row lg:items-center">
|
|
27
|
+
{/* LEFT COPY & ACTIONS */}
|
|
28
|
+
<div className="max-w-xl space-y-4">
|
|
29
|
+
<div className="space-y-2">
|
|
30
|
+
<h1 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl lg:text-4xl">
|
|
31
|
+
{greeting}
|
|
32
|
+
</h1>
|
|
33
|
+
{title ? (
|
|
34
|
+
<p className="text-lg font-medium text-foreground/90">{title}</p>
|
|
35
|
+
) : null}
|
|
36
|
+
{description ? (
|
|
37
|
+
<p className="text-sm sm:text-base text-muted-foreground leading-relaxed">
|
|
38
|
+
{description}
|
|
39
|
+
</p>
|
|
40
|
+
) : null}
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
{actions ? <div className="flex flex-wrap items-center gap-3 pt-2">{actions}</div> : null}
|
|
44
|
+
|
|
45
|
+
{statusContext ? (
|
|
46
|
+
<div className="pt-2 text-xs font-medium text-muted-foreground">
|
|
47
|
+
{statusContext}
|
|
48
|
+
</div>
|
|
49
|
+
) : null}
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
{/* RIGHT ILLUSTRATION / GRAPHIC SLOT */}
|
|
53
|
+
{illustration ? (
|
|
54
|
+
<div
|
|
55
|
+
aria-hidden="true"
|
|
56
|
+
className="relative flex shrink-0 items-center justify-center select-none py-4 lg:py-0"
|
|
57
|
+
>
|
|
58
|
+
{illustration}
|
|
59
|
+
</div>
|
|
60
|
+
) : null}
|
|
61
|
+
</div>
|
|
62
|
+
</section>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface PanelProps {
|
|
4
|
+
title: string
|
|
5
|
+
titleId?: string
|
|
6
|
+
icon?: ReactNode
|
|
7
|
+
action?: ReactNode
|
|
8
|
+
children: ReactNode
|
|
9
|
+
className?: string
|
|
10
|
+
bodyClassName?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function Panel({
|
|
14
|
+
title,
|
|
15
|
+
titleId,
|
|
16
|
+
icon,
|
|
17
|
+
action,
|
|
18
|
+
children,
|
|
19
|
+
className = '',
|
|
20
|
+
bodyClassName = 'p-5',
|
|
21
|
+
}: PanelProps) {
|
|
22
|
+
return (
|
|
23
|
+
<section
|
|
24
|
+
aria-labelledby={titleId}
|
|
25
|
+
className={`rounded-2xl border border-border bg-surface dark:bg-surface-elevated/50 shadow-2xs transition-colors ${className}`}
|
|
26
|
+
>
|
|
27
|
+
<div className="flex items-center justify-between gap-4 border-b border-border/40 px-5 py-4">
|
|
28
|
+
<div className="flex min-w-0 items-center gap-2.5">
|
|
29
|
+
{icon ? (
|
|
30
|
+
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-xl bg-muted text-muted-foreground">
|
|
31
|
+
{icon}
|
|
32
|
+
</span>
|
|
33
|
+
) : null}
|
|
34
|
+
<h2 id={titleId} className="truncate text-sm font-semibold text-foreground">
|
|
35
|
+
{title}
|
|
36
|
+
</h2>
|
|
37
|
+
</div>
|
|
38
|
+
{action}
|
|
39
|
+
</div>
|
|
40
|
+
<div className={bodyClassName}>{children}</div>
|
|
41
|
+
</section>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { SparklesIcon } from '../icons/IconSet.tsx'
|
|
3
|
+
|
|
4
|
+
export interface PriorityCardProps {
|
|
5
|
+
heading?: string
|
|
6
|
+
badgeText?: string
|
|
7
|
+
badgeClassName?: string
|
|
8
|
+
icon?: ReactNode
|
|
9
|
+
title: string
|
|
10
|
+
subtitle?: string
|
|
11
|
+
description: string
|
|
12
|
+
action?: ReactNode
|
|
13
|
+
className?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function PriorityCard({
|
|
17
|
+
heading = 'Next Best Action',
|
|
18
|
+
badgeText,
|
|
19
|
+
badgeClassName = 'border-primary/20 bg-primary/10 text-primary',
|
|
20
|
+
icon,
|
|
21
|
+
title,
|
|
22
|
+
subtitle,
|
|
23
|
+
description,
|
|
24
|
+
action,
|
|
25
|
+
className = '',
|
|
26
|
+
}: PriorityCardProps) {
|
|
27
|
+
return (
|
|
28
|
+
<section className={`space-y-3 ${className}`}>
|
|
29
|
+
{/* SECTION HEADER WITH CIRCULAR ACCENT BADGE */}
|
|
30
|
+
<div className="flex items-center justify-between">
|
|
31
|
+
<div className="flex items-center gap-2.5">
|
|
32
|
+
<span
|
|
33
|
+
aria-hidden="true"
|
|
34
|
+
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-primary/12 text-primary"
|
|
35
|
+
>
|
|
36
|
+
{icon || <SparklesIcon className="h-4 w-4" />}
|
|
37
|
+
</span>
|
|
38
|
+
<h2 className="text-sm font-semibold tracking-normal text-foreground">
|
|
39
|
+
{heading}
|
|
40
|
+
</h2>
|
|
41
|
+
</div>
|
|
42
|
+
{badgeText ? (
|
|
43
|
+
<span
|
|
44
|
+
className={`inline-flex items-center rounded-full border px-2.5 py-0.5 text-[11px] font-medium ${badgeClassName}`}
|
|
45
|
+
>
|
|
46
|
+
{badgeText}
|
|
47
|
+
</span>
|
|
48
|
+
) : null}
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
{/* QUIET FEATURED CARD */}
|
|
52
|
+
<div className="rounded-2xl border border-border/40 bg-surface dark:bg-surface-elevated/60 p-6 shadow-2xs transition-colors">
|
|
53
|
+
<div className="flex flex-col justify-between gap-4 sm:flex-row sm:items-start">
|
|
54
|
+
<div className="space-y-2 min-w-0 max-w-xl">
|
|
55
|
+
<div>
|
|
56
|
+
<h3 className="text-base sm:text-lg font-bold text-foreground tracking-tight">
|
|
57
|
+
{title}
|
|
58
|
+
</h3>
|
|
59
|
+
{subtitle ? (
|
|
60
|
+
<p className="mt-0.5 text-xs font-medium text-muted-foreground">
|
|
61
|
+
{subtitle}
|
|
62
|
+
</p>
|
|
63
|
+
) : null}
|
|
64
|
+
</div>
|
|
65
|
+
<p className="text-xs sm:text-sm text-muted-foreground leading-relaxed">
|
|
66
|
+
{description}
|
|
67
|
+
</p>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
{action ? <div className="shrink-0 pt-1 sm:pt-0">{action}</div> : null}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</section>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface ListItemData {
|
|
4
|
+
id: string
|
|
5
|
+
title: string
|
|
6
|
+
subtitle?: string
|
|
7
|
+
avatarText?: string
|
|
8
|
+
avatarIcon?: ReactNode
|
|
9
|
+
statusBadge?: ReactNode
|
|
10
|
+
metaText?: string
|
|
11
|
+
action?: ReactNode
|
|
12
|
+
onClick?: () => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface QuietListCardProps {
|
|
16
|
+
title: string
|
|
17
|
+
icon?: ReactNode
|
|
18
|
+
iconClassName?: string
|
|
19
|
+
viewAllLink?: {
|
|
20
|
+
label?: string
|
|
21
|
+
to: string
|
|
22
|
+
onClick?: () => void
|
|
23
|
+
}
|
|
24
|
+
items: ListItemData[]
|
|
25
|
+
emptyMessage?: string
|
|
26
|
+
emptySubtext?: string
|
|
27
|
+
emptyAction?: ReactNode
|
|
28
|
+
className?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function QuietListCard({
|
|
32
|
+
title,
|
|
33
|
+
icon,
|
|
34
|
+
iconClassName = 'bg-primary/12 text-primary',
|
|
35
|
+
viewAllLink,
|
|
36
|
+
items,
|
|
37
|
+
emptyMessage = 'No items found',
|
|
38
|
+
emptySubtext = 'Get started by creating your first item.',
|
|
39
|
+
emptyAction,
|
|
40
|
+
className = '',
|
|
41
|
+
}: QuietListCardProps) {
|
|
42
|
+
return (
|
|
43
|
+
<section className={`space-y-3 ${className}`}>
|
|
44
|
+
{/* SECTION HEADER */}
|
|
45
|
+
<div className="flex items-center justify-between">
|
|
46
|
+
<div className="flex items-center gap-2.5">
|
|
47
|
+
{icon ? (
|
|
48
|
+
<span
|
|
49
|
+
aria-hidden="true"
|
|
50
|
+
className={`flex h-7 w-7 shrink-0 items-center justify-center rounded-full ${iconClassName}`}
|
|
51
|
+
>
|
|
52
|
+
{icon}
|
|
53
|
+
</span>
|
|
54
|
+
) : null}
|
|
55
|
+
<h2 className="text-sm font-semibold tracking-normal text-foreground">
|
|
56
|
+
{title}
|
|
57
|
+
</h2>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
{viewAllLink ? (
|
|
61
|
+
<a
|
|
62
|
+
href={viewAllLink.to}
|
|
63
|
+
onClick={(e) => {
|
|
64
|
+
if (viewAllLink.onClick) {
|
|
65
|
+
e.preventDefault()
|
|
66
|
+
viewAllLink.onClick()
|
|
67
|
+
}
|
|
68
|
+
}}
|
|
69
|
+
className="inline-flex items-center gap-1 text-xs font-semibold text-primary hover:text-primary/80 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-md px-1 py-0.5"
|
|
70
|
+
>
|
|
71
|
+
<span>{viewAllLink.label || 'View all'}</span>
|
|
72
|
+
<span aria-hidden="true">→</span>
|
|
73
|
+
</a>
|
|
74
|
+
) : null}
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
{/* QUIET LIST CONTAINER */}
|
|
78
|
+
<div className="rounded-2xl border border-border/40 bg-surface dark:bg-surface-elevated/60 shadow-2xs overflow-hidden transition-colors">
|
|
79
|
+
{items.length === 0 ? (
|
|
80
|
+
<div className="p-8 text-center">
|
|
81
|
+
{icon ? (
|
|
82
|
+
<div className="mx-auto mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
|
83
|
+
{icon}
|
|
84
|
+
</div>
|
|
85
|
+
) : null}
|
|
86
|
+
<p className="text-sm font-semibold text-foreground">{emptyMessage}</p>
|
|
87
|
+
{emptySubtext ? (
|
|
88
|
+
<p className="mt-1 text-xs text-muted-foreground max-w-sm mx-auto">
|
|
89
|
+
{emptySubtext}
|
|
90
|
+
</p>
|
|
91
|
+
) : null}
|
|
92
|
+
{emptyAction ? <div className="mt-4">{emptyAction}</div> : null}
|
|
93
|
+
</div>
|
|
94
|
+
) : (
|
|
95
|
+
<ul className="divide-y divide-border/30">
|
|
96
|
+
{items.map((item) => (
|
|
97
|
+
<li
|
|
98
|
+
key={item.id}
|
|
99
|
+
onClick={item.onClick}
|
|
100
|
+
className={`group flex flex-col justify-between gap-3 px-5 py-3.5 hover:bg-muted/30 transition-colors sm:flex-row sm:items-center ${
|
|
101
|
+
item.onClick ? 'cursor-pointer' : ''
|
|
102
|
+
}`}
|
|
103
|
+
>
|
|
104
|
+
<div className="flex items-center gap-3.5 min-w-0 flex-1">
|
|
105
|
+
{item.avatarText ? (
|
|
106
|
+
<span
|
|
107
|
+
aria-hidden="true"
|
|
108
|
+
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-muted/60 text-xs font-bold text-foreground border border-border/40 group-hover:border-primary/40 group-hover:bg-primary/10 group-hover:text-primary transition-colors"
|
|
109
|
+
>
|
|
110
|
+
{item.avatarText}
|
|
111
|
+
</span>
|
|
112
|
+
) : item.avatarIcon ? (
|
|
113
|
+
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-muted/60 text-muted-foreground border border-border/40">
|
|
114
|
+
{item.avatarIcon}
|
|
115
|
+
</span>
|
|
116
|
+
) : null}
|
|
117
|
+
|
|
118
|
+
<div className="min-w-0 flex-1">
|
|
119
|
+
<p className="truncate text-sm font-semibold text-foreground">
|
|
120
|
+
{item.title}
|
|
121
|
+
</p>
|
|
122
|
+
{item.subtitle ? (
|
|
123
|
+
<p className="truncate text-xs text-muted-foreground">
|
|
124
|
+
{item.subtitle}
|
|
125
|
+
</p>
|
|
126
|
+
) : null}
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<div className="flex shrink-0 items-center justify-between gap-3 sm:gap-4 pl-12 sm:pl-0">
|
|
131
|
+
{item.statusBadge}
|
|
132
|
+
{item.metaText ? (
|
|
133
|
+
<span className="text-xs tabular-nums text-muted-foreground/80 whitespace-nowrap">
|
|
134
|
+
{item.metaText}
|
|
135
|
+
</span>
|
|
136
|
+
) : null}
|
|
137
|
+
{item.action}
|
|
138
|
+
</div>
|
|
139
|
+
</li>
|
|
140
|
+
))}
|
|
141
|
+
</ul>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
</section>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
* Dashboard Starter Kit — Animations & Transitions
|
|
3
|
+
* ============================================================================
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/* Theme-flip cross-fade easing (~200ms) */
|
|
7
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
8
|
+
.theme-transition {
|
|
9
|
+
transition-property: background-color, border-color, color, fill, stroke, box-shadow, outline-color;
|
|
10
|
+
transition-duration: 200ms;
|
|
11
|
+
transition-timing-function: ease;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* Reusable utility to hide visible scrollbars while preserving scrolling */
|
|
16
|
+
.no-scrollbar,
|
|
17
|
+
.scrollbar-none {
|
|
18
|
+
-ms-overflow-style: none;
|
|
19
|
+
scrollbar-width: none;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.no-scrollbar::-webkit-scrollbar,
|
|
23
|
+
.scrollbar-none::-webkit-scrollbar {
|
|
24
|
+
display: none;
|
|
25
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
* Dashboard Starter Kit — Design Tokens
|
|
3
|
+
* ============================================================================
|
|
4
|
+
* Single source of truth for semantic color tokens across Light & Dark modes.
|
|
5
|
+
* Mapped to Tailwind color utilities via `@theme inline` so components consume
|
|
6
|
+
* `bg-surface`, `text-foreground`, `border-border`, `bg-primary`, etc.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
:root,
|
|
10
|
+
[data-theme="light"] {
|
|
11
|
+
--background: #f4f5f7;
|
|
12
|
+
--foreground: #0f172a;
|
|
13
|
+
--muted-foreground: #64748b;
|
|
14
|
+
--surface: #ffffff;
|
|
15
|
+
--surface-elevated: #ffffff;
|
|
16
|
+
--muted: #eef1f4;
|
|
17
|
+
--border: #e3e7ec;
|
|
18
|
+
--input: #ffffff;
|
|
19
|
+
--canvas-background: #eef3fb;
|
|
20
|
+
|
|
21
|
+
/* Brand Primary */
|
|
22
|
+
--primary: #2563eb;
|
|
23
|
+
--primary-foreground: #ffffff;
|
|
24
|
+
--ring: #2563eb;
|
|
25
|
+
|
|
26
|
+
/* Semantic Feedback */
|
|
27
|
+
--success: #059669;
|
|
28
|
+
--success-fg: #047857;
|
|
29
|
+
--success-bg: #ecfdf5;
|
|
30
|
+
--warning: #d97706;
|
|
31
|
+
--warning-fg: #b45309;
|
|
32
|
+
--warning-bg: #fffbeb;
|
|
33
|
+
--danger: #dc2626;
|
|
34
|
+
--danger-fg: #b91c1c;
|
|
35
|
+
--danger-bg: #fef2f2;
|
|
36
|
+
--info: #0284c7;
|
|
37
|
+
--info-fg: #0369a1;
|
|
38
|
+
--info-bg: #f0f9ff;
|
|
39
|
+
|
|
40
|
+
color-scheme: light;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
[data-theme="dark"] {
|
|
44
|
+
--background: #0b0c0e;
|
|
45
|
+
--foreground: #f3f5f7;
|
|
46
|
+
--muted-foreground: #9aa4b2;
|
|
47
|
+
--surface: #151719;
|
|
48
|
+
--surface-elevated: #1c1f22;
|
|
49
|
+
--muted: #22262b;
|
|
50
|
+
--border: #2a2f35;
|
|
51
|
+
--input: #1a1a1a;
|
|
52
|
+
--canvas-background: #181a1f;
|
|
53
|
+
|
|
54
|
+
/* Brand Primary */
|
|
55
|
+
--primary: #3b82f6;
|
|
56
|
+
--primary-foreground: #ffffff;
|
|
57
|
+
--ring: #3b82f6;
|
|
58
|
+
|
|
59
|
+
/* Semantic Feedback */
|
|
60
|
+
--success: #10b981;
|
|
61
|
+
--success-fg: #6ee7b7;
|
|
62
|
+
--success-bg: rgba(16, 185, 129, 0.15);
|
|
63
|
+
--warning: #f59e0b;
|
|
64
|
+
--warning-fg: #fcd34d;
|
|
65
|
+
--warning-bg: rgba(245, 158, 11, 0.15);
|
|
66
|
+
--danger: #ef4444;
|
|
67
|
+
--danger-fg: #fca5a5;
|
|
68
|
+
--danger-bg: rgba(239, 68, 68, 0.15);
|
|
69
|
+
--info: #38bdf8;
|
|
70
|
+
--info-fg: #7dd3fc;
|
|
71
|
+
--info-bg: rgba(56, 189, 248, 0.15);
|
|
72
|
+
|
|
73
|
+
color-scheme: dark;
|
|
74
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
* Dashboard Starter Kit — Typography System
|
|
3
|
+
* ============================================================================
|
|
4
|
+
* Standard typographic rules, line heights, and tabular layout utilities.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/* Antialiasing for crisp text rendering across dark/light themes */
|
|
8
|
+
.dashboard-font-base {
|
|
9
|
+
font-family: 'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
10
|
+
-webkit-font-smoothing: antialiased;
|
|
11
|
+
-moz-osx-font-smoothing: grayscale;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* Numeric tabular display ensures uniform number column widths */
|
|
15
|
+
.tabular-nums {
|
|
16
|
+
font-variant-numeric: tabular-nums;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* Heading letter-spacing tokens */
|
|
20
|
+
.tracking-tight {
|
|
21
|
+
letter-spacing: -0.025em;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.tracking-tighter {
|
|
25
|
+
letter-spacing: -0.05em;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.tracking-wide {
|
|
29
|
+
letter-spacing: 0.025em;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.tracking-wider {
|
|
33
|
+
letter-spacing: 0.05em;
|
|
34
|
+
}
|