@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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +153 -0
  3. package/bin/create-dashboard.js +12 -0
  4. package/dist/cli.d.ts +7 -0
  5. package/dist/cli.js +223 -0
  6. package/dist/configBuilder.d.ts +8 -0
  7. package/dist/configBuilder.js +573 -0
  8. package/dist/generator.d.ts +9 -0
  9. package/dist/generator.js +223 -0
  10. package/dist/presets.d.ts +8 -0
  11. package/dist/presets.js +72 -0
  12. package/dist/prompts.d.ts +2 -0
  13. package/dist/prompts.js +121 -0
  14. package/dist/templateEngine.d.ts +3 -0
  15. package/dist/templateEngine.js +31 -0
  16. package/dist/types.d.ts +54 -0
  17. package/dist/types.js +1 -0
  18. package/dist/validation.d.ts +16 -0
  19. package/dist/validation.js +73 -0
  20. package/package.json +56 -0
  21. package/templates/source/components/controls/Button.tsx +69 -0
  22. package/templates/source/components/controls/Input.tsx +59 -0
  23. package/templates/source/components/controls/SearchBar.tsx +44 -0
  24. package/templates/source/components/controls/Select.tsx +61 -0
  25. package/templates/source/components/controls/ThemeToggle.tsx +23 -0
  26. package/templates/source/components/feedback/NotificationCenter.tsx +151 -0
  27. package/templates/source/components/feedback/Skeleton.tsx +22 -0
  28. package/templates/source/components/feedback/StatusBadge.tsx +35 -0
  29. package/templates/source/components/feedback/statusBadgeStyles.ts +28 -0
  30. package/templates/source/components/icons/IconSet.tsx +235 -0
  31. package/templates/source/components/layout/AppShell.tsx +70 -0
  32. package/templates/source/components/layout/Header.tsx +90 -0
  33. package/templates/source/components/layout/PageHeader.tsx +27 -0
  34. package/templates/source/components/layout/Sidebar.tsx +202 -0
  35. package/templates/source/components/metrics/MetricRow.tsx +61 -0
  36. package/templates/source/components/metrics/StatCard.tsx +49 -0
  37. package/templates/source/components/navigation/NavItem.tsx +75 -0
  38. package/templates/source/components/navigation/ShortcutPill.tsx +39 -0
  39. package/templates/source/components/surfaces/EmptyState.tsx +36 -0
  40. package/templates/source/components/surfaces/HeroSurface.tsx +64 -0
  41. package/templates/source/components/surfaces/Panel.tsx +43 -0
  42. package/templates/source/components/surfaces/PriorityCard.tsx +75 -0
  43. package/templates/source/components/surfaces/QuietListCard.tsx +146 -0
  44. package/templates/source/design-system/animations.css +25 -0
  45. package/templates/source/design-system/tokens.css +74 -0
  46. package/templates/source/design-system/typography.css +34 -0
  47. package/templates/source/theme/ThemeContext.tsx +82 -0
  48. package/templates/source/theme/useTheme.ts +10 -0
@@ -0,0 +1,69 @@
1
+ import type { ButtonHTMLAttributes, ReactNode } from 'react'
2
+ import type { ButtonShape } from '../../config/dashboard.config.ts'
3
+ import { getButtonShapeClass } from '../../config/dashboard.config.ts'
4
+
5
+ export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger'
6
+ export type ButtonSize = 'sm' | 'md' | 'lg'
7
+
8
+ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
9
+ variant?: ButtonVariant
10
+ size?: ButtonSize
11
+ shape?: ButtonShape
12
+ leftIcon?: ReactNode
13
+ rightIcon?: ReactNode
14
+ isLoading?: boolean
15
+ children?: ReactNode
16
+ }
17
+
18
+ export function Button({
19
+ variant = 'primary',
20
+ size = 'md',
21
+ shape = 'pill',
22
+ leftIcon,
23
+ rightIcon,
24
+ isLoading,
25
+ disabled,
26
+ className = '',
27
+ children,
28
+ ...props
29
+ }: ButtonProps) {
30
+ const shapeClass = getButtonShapeClass(shape)
31
+
32
+ const sizeClasses = {
33
+ sm: 'px-3 py-1.5 text-xs gap-1.5',
34
+ md: 'px-4 py-2 text-sm gap-2',
35
+ lg: 'px-5 py-2.5 text-base gap-2.5 font-semibold',
36
+ }[size]
37
+
38
+ const variantClasses = {
39
+ primary:
40
+ 'bg-primary text-primary-foreground shadow-2xs hover:bg-primary/90 hover:shadow-xs active:scale-[0.99]',
41
+ secondary:
42
+ 'border border-border/80 bg-surface text-foreground shadow-2xs hover:bg-muted/70 active:scale-[0.99]',
43
+ outline:
44
+ 'border border-border bg-transparent text-foreground hover:bg-muted/60 active:scale-[0.99]',
45
+ ghost:
46
+ 'text-muted-foreground hover:bg-muted/60 hover:text-foreground active:scale-[0.99]',
47
+ danger:
48
+ 'bg-danger text-white shadow-2xs hover:bg-danger/90 active:scale-[0.99]',
49
+ }[variant]
50
+
51
+ return (
52
+ <button
53
+ disabled={disabled || isLoading}
54
+ className={`inline-flex items-center justify-center font-medium transition-all duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60 select-none ${shapeClass} ${sizeClasses} ${variantClasses} ${className}`}
55
+ {...props}
56
+ >
57
+ {isLoading ? (
58
+ <span
59
+ aria-hidden="true"
60
+ className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent"
61
+ />
62
+ ) : leftIcon ? (
63
+ <span className="shrink-0">{leftIcon}</span>
64
+ ) : null}
65
+ {children}
66
+ {!isLoading && rightIcon ? <span className="shrink-0">{rightIcon}</span> : null}
67
+ </button>
68
+ )
69
+ }
@@ -0,0 +1,59 @@
1
+ import type { InputHTMLAttributes, ReactNode } from 'react'
2
+
3
+ export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
4
+ label?: string
5
+ error?: string
6
+ helperText?: string
7
+ leftIcon?: ReactNode
8
+ rightIcon?: ReactNode
9
+ }
10
+
11
+ export function Input({
12
+ id,
13
+ label,
14
+ error,
15
+ helperText,
16
+ leftIcon,
17
+ rightIcon,
18
+ className = '',
19
+ ...props
20
+ }: InputProps) {
21
+ const inputId = id || (label ? label.toLowerCase().replace(/\s+/g, '-') : undefined)
22
+
23
+ return (
24
+ <div className="w-full">
25
+ {label ? (
26
+ <label htmlFor={inputId} className="block text-sm font-medium text-foreground mb-1">
27
+ {label}
28
+ </label>
29
+ ) : null}
30
+ <div className="relative flex items-center">
31
+ {leftIcon ? (
32
+ <span className="pointer-events-none absolute left-3 flex items-center text-muted-foreground">
33
+ {leftIcon}
34
+ </span>
35
+ ) : null}
36
+ <input
37
+ id={inputId}
38
+ aria-invalid={Boolean(error)}
39
+ className={`block w-full rounded-lg border bg-input px-3.5 py-2 text-sm text-foreground placeholder:text-muted-foreground shadow-2xs transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-60 ${
40
+ error
41
+ ? 'border-danger focus-visible:ring-danger'
42
+ : 'border-border focus:border-primary'
43
+ } ${leftIcon ? 'pl-10' : ''} ${rightIcon ? 'pr-10' : ''} ${className}`}
44
+ {...props}
45
+ />
46
+ {rightIcon ? (
47
+ <span className="absolute right-3 flex items-center text-muted-foreground">
48
+ {rightIcon}
49
+ </span>
50
+ ) : null}
51
+ </div>
52
+ {error ? (
53
+ <p className="mt-1 text-xs text-danger-fg">{error}</p>
54
+ ) : helperText ? (
55
+ <p className="mt-1 text-xs text-muted-foreground">{helperText}</p>
56
+ ) : null}
57
+ </div>
58
+ )
59
+ }
@@ -0,0 +1,44 @@
1
+ import type { ChangeEvent } from 'react'
2
+ import { CloseIcon, SearchIcon } from '../icons/IconSet.tsx'
3
+
4
+ export interface SearchBarProps {
5
+ value: string
6
+ onChange: (value: string) => void
7
+ placeholder?: string
8
+ ariaLabel?: string
9
+ className?: string
10
+ }
11
+
12
+ export function SearchBar({
13
+ value,
14
+ onChange,
15
+ placeholder = 'Search...',
16
+ ariaLabel = 'Search records',
17
+ className = '',
18
+ }: SearchBarProps) {
19
+ return (
20
+ <div className={`relative flex items-center ${className}`}>
21
+ <span className="pointer-events-none absolute left-3 flex items-center text-muted-foreground">
22
+ <SearchIcon className="h-4 w-4" />
23
+ </span>
24
+ <input
25
+ type="search"
26
+ value={value}
27
+ onChange={(e: ChangeEvent<HTMLInputElement>) => onChange(e.target.value)}
28
+ placeholder={placeholder}
29
+ aria-label={ariaLabel}
30
+ className="block w-full rounded-lg border border-border bg-input pl-9 pr-8 py-1.5 text-sm text-foreground placeholder:text-muted-foreground shadow-2xs transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus:border-primary"
31
+ />
32
+ {value ? (
33
+ <button
34
+ type="button"
35
+ onClick={() => onChange('')}
36
+ aria-label="Clear search"
37
+ className="absolute right-2.5 flex items-center justify-center rounded-full p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground focus:outline-none"
38
+ >
39
+ <CloseIcon className="h-3.5 w-3.5" />
40
+ </button>
41
+ ) : null}
42
+ </div>
43
+ )
44
+ }
@@ -0,0 +1,61 @@
1
+ import type { ReactNode, SelectHTMLAttributes } from 'react'
2
+
3
+ export interface SelectOption {
4
+ value: string | number
5
+ label: string
6
+ disabled?: boolean
7
+ }
8
+
9
+ export interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
10
+ label?: string
11
+ options?: SelectOption[]
12
+ error?: string
13
+ helperText?: string
14
+ children?: ReactNode
15
+ }
16
+
17
+ export function Select({
18
+ id,
19
+ label,
20
+ options,
21
+ error,
22
+ helperText,
23
+ className = '',
24
+ children,
25
+ ...props
26
+ }: SelectProps) {
27
+ const selectId = id || (label ? label.toLowerCase().replace(/\s+/g, '-') : undefined)
28
+
29
+ return (
30
+ <div className="w-full">
31
+ {label ? (
32
+ <label htmlFor={selectId} className="block text-sm font-medium text-foreground mb-1">
33
+ {label}
34
+ </label>
35
+ ) : null}
36
+ <select
37
+ id={selectId}
38
+ aria-invalid={Boolean(error)}
39
+ className={`block w-full rounded-lg border bg-input px-3 py-2 text-sm font-medium text-foreground shadow-2xs transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-60 ${
40
+ error
41
+ ? 'border-danger focus-visible:ring-danger'
42
+ : 'border-border focus:border-primary'
43
+ } ${className}`}
44
+ {...props}
45
+ >
46
+ {options
47
+ ? options.map((opt) => (
48
+ <option key={opt.value} value={opt.value} disabled={opt.disabled}>
49
+ {opt.label}
50
+ </option>
51
+ ))
52
+ : children}
53
+ </select>
54
+ {error ? (
55
+ <p className="mt-1 text-xs text-danger-fg">{error}</p>
56
+ ) : helperText ? (
57
+ <p className="mt-1 text-xs text-muted-foreground">{helperText}</p>
58
+ ) : null}
59
+ </div>
60
+ )
61
+ }
@@ -0,0 +1,23 @@
1
+ import { MoonIcon, SunIcon } from '../icons/IconSet.tsx'
2
+ import { useTheme } from '../../theme/useTheme.ts'
3
+
4
+ export interface ThemeToggleProps {
5
+ className?: string
6
+ }
7
+
8
+ export function ThemeToggle({ className = '' }: ThemeToggleProps) {
9
+ const { isDark, toggleTheme } = useTheme()
10
+ const label = isDark ? 'Switch to light theme' : 'Switch to dark theme'
11
+
12
+ return (
13
+ <button
14
+ type="button"
15
+ onClick={toggleTheme}
16
+ aria-label={label}
17
+ title={label}
18
+ className={`inline-flex items-center justify-center rounded-xl p-2 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring ${className}`}
19
+ >
20
+ {isDark ? <MoonIcon className="h-5 w-5" /> : <SunIcon className="h-5 w-5" />}
21
+ </button>
22
+ )
23
+ }
@@ -0,0 +1,151 @@
1
+ import { useEffect, useRef, useState, type CSSProperties } from 'react'
2
+ import { BellIcon, CheckIcon } from '../icons/IconSet.tsx'
3
+
4
+ export interface NotificationItemData {
5
+ id: string
6
+ title: string
7
+ message: string
8
+ timestamp: string
9
+ read?: boolean
10
+ link?: string
11
+ }
12
+
13
+ export interface NotificationCenterProps {
14
+ notifications?: NotificationItemData[]
15
+ onMarkAllRead?: () => void
16
+ onItemClick?: (item: NotificationItemData) => void
17
+ className?: string
18
+ }
19
+
20
+ export function NotificationCenter({
21
+ notifications = [],
22
+ onMarkAllRead,
23
+ onItemClick,
24
+ className = '',
25
+ }: NotificationCenterProps) {
26
+ const [open, setOpen] = useState(false)
27
+ const [position, setPosition] = useState<CSSProperties>({})
28
+ const buttonRef = useRef<HTMLButtonElement>(null)
29
+ const panelRef = useRef<HTMLDivElement>(null)
30
+
31
+ const unreadCount = notifications.filter((n) => !n.read).length
32
+
33
+ const handleToggle = () => {
34
+ if (!open && buttonRef.current) {
35
+ const rect = buttonRef.current.getBoundingClientRect()
36
+ setPosition({
37
+ top: rect.bottom + 8,
38
+ right: Math.max(12, window.innerWidth - rect.right),
39
+ })
40
+ }
41
+ setOpen((prev) => !prev)
42
+ }
43
+
44
+ useEffect(() => {
45
+ if (!open) return
46
+
47
+ function onPointerDown(event: PointerEvent) {
48
+ if (
49
+ panelRef.current &&
50
+ !panelRef.current.contains(event.target as Node) &&
51
+ buttonRef.current &&
52
+ !buttonRef.current.contains(event.target as Node)
53
+ ) {
54
+ setOpen(false)
55
+ }
56
+ }
57
+
58
+ function onKeyDown(event: KeyboardEvent) {
59
+ if (event.key === 'Escape') {
60
+ setOpen(false)
61
+ buttonRef.current?.focus()
62
+ }
63
+ }
64
+
65
+ document.addEventListener('pointerdown', onPointerDown)
66
+ document.addEventListener('keydown', onKeyDown)
67
+
68
+ return () => {
69
+ document.removeEventListener('pointerdown', onPointerDown)
70
+ document.removeEventListener('keydown', onKeyDown)
71
+ }
72
+ }, [open])
73
+
74
+ return (
75
+ <div className={`relative inline-block ${className}`}>
76
+ <button
77
+ ref={buttonRef}
78
+ type="button"
79
+ onClick={handleToggle}
80
+ aria-label={`Notifications (${unreadCount} unread)`}
81
+ aria-haspopup="dialog"
82
+ aria-expanded={open}
83
+ className="relative inline-flex items-center justify-center rounded-xl p-2 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
84
+ >
85
+ <BellIcon className="h-5 w-5" />
86
+ {unreadCount > 0 ? (
87
+ <span
88
+ aria-hidden="true"
89
+ className="absolute -right-0.5 -top-0.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-primary px-1 text-[10px] font-bold text-primary-foreground"
90
+ >
91
+ {unreadCount > 9 ? '9+' : unreadCount}
92
+ </span>
93
+ ) : null}
94
+ </button>
95
+
96
+ {open ? (
97
+ <div
98
+ ref={panelRef}
99
+ role="dialog"
100
+ aria-label="Notification center"
101
+ style={position}
102
+ className="fixed z-50 w-[380px] max-w-[calc(100vw-1.5rem)] overflow-hidden rounded-xl border border-border bg-surface text-foreground shadow-lg"
103
+ >
104
+ <div className="flex items-center justify-between border-b border-border px-4 py-3">
105
+ <h3 className="text-sm font-semibold text-foreground">Notifications</h3>
106
+ {onMarkAllRead && unreadCount > 0 ? (
107
+ <button
108
+ type="button"
109
+ onClick={onMarkAllRead}
110
+ className="inline-flex items-center gap-1.5 rounded-md text-xs font-medium text-primary hover:text-primary/80 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
111
+ >
112
+ <CheckIcon className="h-3.5 w-3.5" />
113
+ <span>Mark all read</span>
114
+ </button>
115
+ ) : null}
116
+ </div>
117
+
118
+ <div className="max-h-80 overflow-y-auto divide-y divide-border/40">
119
+ {notifications.length === 0 ? (
120
+ <div className="p-8 text-center text-muted-foreground">
121
+ <BellIcon className="mx-auto h-6 w-6 mb-2 text-muted-foreground/60" />
122
+ <p className="text-xs font-medium">All caught up!</p>
123
+ <p className="text-[11px] text-muted-foreground/70 mt-0.5">
124
+ No new notifications right now.
125
+ </p>
126
+ </div>
127
+ ) : (
128
+ notifications.map((item) => (
129
+ <div
130
+ key={item.id}
131
+ onClick={() => onItemClick?.(item)}
132
+ className={`p-3 text-left transition-colors cursor-pointer hover:bg-muted/40 ${
133
+ !item.read ? 'bg-primary/5' : ''
134
+ }`}
135
+ >
136
+ <div className="flex items-start justify-between gap-2">
137
+ <p className="text-xs font-bold text-foreground">{item.title}</p>
138
+ <span className="text-[10px] text-muted-foreground whitespace-nowrap">
139
+ {item.timestamp}
140
+ </span>
141
+ </div>
142
+ <p className="mt-1 text-xs text-muted-foreground line-clamp-2">{item.message}</p>
143
+ </div>
144
+ ))
145
+ )}
146
+ </div>
147
+ </div>
148
+ ) : null}
149
+ </div>
150
+ )
151
+ }
@@ -0,0 +1,22 @@
1
+ export interface SkeletonProps {
2
+ className?: string
3
+ }
4
+
5
+ export function Skeleton({ className = '' }: SkeletonProps) {
6
+ return (
7
+ <div
8
+ aria-hidden="true"
9
+ className={`animate-pulse rounded-xl bg-muted/60 dark:bg-muted/40 ${className}`}
10
+ />
11
+ )
12
+ }
13
+
14
+ export function CardSkeleton() {
15
+ return (
16
+ <div className="rounded-2xl border border-border/40 bg-surface p-6 shadow-2xs space-y-4">
17
+ <Skeleton className="h-6 w-1/3" />
18
+ <Skeleton className="h-4 w-2/3" />
19
+ <Skeleton className="h-12 w-full" />
20
+ </div>
21
+ )
22
+ }
@@ -0,0 +1,35 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ import {
4
+ STATUS_VARIANT_STYLES,
5
+ type StatusVariant,
6
+ } from './statusBadgeStyles.ts'
7
+
8
+ export { STATUS_VARIANT_STYLES, type StatusVariant }
9
+
10
+ export interface StatusBadgeProps {
11
+ label?: string
12
+ variant?: StatusVariant
13
+ dot?: boolean
14
+ className?: string
15
+ children?: ReactNode
16
+ }
17
+
18
+ export function StatusBadge({
19
+ label,
20
+ variant = 'neutral',
21
+ dot = false,
22
+ className = '',
23
+ children,
24
+ }: StatusBadgeProps) {
25
+ const styles = STATUS_VARIANT_STYLES[variant] || STATUS_VARIANT_STYLES.neutral
26
+
27
+ return (
28
+ <span
29
+ className={`inline-flex shrink-0 items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium ring-1 ring-inset ${styles.badge} ${className}`}
30
+ >
31
+ {dot ? <span className={`h-1.5 w-1.5 rounded-full shrink-0 ${styles.dot}`} /> : null}
32
+ {children || label}
33
+ </span>
34
+ )
35
+ }
@@ -0,0 +1,28 @@
1
+ export type StatusVariant = 'success' | 'warning' | 'danger' | 'info' | 'neutral' | 'purple'
2
+
3
+ export const STATUS_VARIANT_STYLES: Record<StatusVariant, { badge: string; dot: string }> = {
4
+ success: {
5
+ badge: 'bg-emerald-100 text-emerald-700 ring-emerald-200 dark:bg-emerald-500/15 dark:text-emerald-300 dark:ring-emerald-500/25',
6
+ dot: 'bg-emerald-500',
7
+ },
8
+ warning: {
9
+ badge: 'bg-amber-100 text-amber-800 ring-amber-200 dark:bg-amber-500/15 dark:text-amber-300 dark:ring-amber-500/25',
10
+ dot: 'bg-amber-500',
11
+ },
12
+ danger: {
13
+ badge: 'bg-red-100 text-red-700 ring-red-200 dark:bg-red-500/15 dark:text-red-300 dark:ring-red-500/25',
14
+ dot: 'bg-red-500',
15
+ },
16
+ info: {
17
+ badge: 'bg-blue-100 text-blue-700 ring-blue-200 dark:bg-blue-500/15 dark:text-blue-300 dark:ring-blue-500/25',
18
+ dot: 'bg-blue-500',
19
+ },
20
+ neutral: {
21
+ badge: 'bg-slate-100 text-slate-700 ring-slate-200 dark:bg-slate-400/15 dark:text-slate-300 dark:ring-slate-400/25',
22
+ dot: 'bg-slate-400',
23
+ },
24
+ purple: {
25
+ badge: 'bg-purple-100 text-purple-700 ring-purple-200 dark:bg-purple-500/15 dark:text-purple-300 dark:ring-purple-500/25',
26
+ dot: 'bg-purple-500',
27
+ },
28
+ }