@littlebearapps/platform-admin-sdk 2.1.0 → 2.2.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 (115) hide show
  1. package/README.md +2 -5
  2. package/dist/templates.d.ts +1 -1
  3. package/dist/templates.js +121 -3
  4. package/package.json +1 -1
  5. package/templates/full/dashboard/src/components/notifications/NotificationDropdown.tsx +130 -0
  6. package/templates/full/dashboard/src/components/notifications/NotificationItem.tsx +264 -0
  7. package/templates/full/dashboard/src/components/patterns/PatternInfoButton.tsx +60 -0
  8. package/templates/full/dashboard/src/components/reports/FeatureUsageReport.tsx +339 -0
  9. package/templates/full/dashboard/src/components/search/SearchResultGroup.tsx +46 -0
  10. package/templates/full/dashboard/src/components/search/SearchResultItem.tsx +212 -0
  11. package/templates/full/dashboard/src/pages/api/patterns/[id]/approve.ts +49 -0
  12. package/templates/full/dashboard/src/pages/api/patterns/[id]/reject.ts +50 -0
  13. package/templates/full/dashboard/src/pages/api/reports/digests/stats.ts +38 -0
  14. package/templates/full/dashboard/src/pages/api/reports/digests.ts +39 -0
  15. package/templates/full/dashboard/src/pages/api/search/reindex/[type].ts +56 -0
  16. package/templates/full/dashboard/src/pages/api/test-reports/[id].ts +102 -0
  17. package/templates/full/dashboard/src/pages/feedback.astro +365 -0
  18. package/templates/full/dashboard/src/pages/kiosk.astro +206 -0
  19. package/templates/full/dashboard/src/pages/map.astro +561 -0
  20. package/templates/full/dashboard/src/pages/revenue.astro +72 -0
  21. package/templates/full/dashboard/src/pages/tests.astro +431 -0
  22. package/templates/full/scripts/ops/audit-cost-anomaly.ts +430 -0
  23. package/templates/full/scripts/ops/verify-account-total.ts +256 -0
  24. package/templates/full/tests/integration/feedback-schema.test.ts +361 -0
  25. package/templates/full/tests/integration/r2-archive.test.ts +108 -0
  26. package/templates/shared/.github/workflows/dependabot-automerge.yml +41 -0
  27. package/templates/shared/.github/workflows/validate-controls.yml +27 -0
  28. package/templates/shared/dashboard/src/components/Breadcrumbs.astro +101 -0
  29. package/templates/shared/dashboard/src/components/EmptyState.astro +46 -0
  30. package/templates/shared/dashboard/src/components/ErrorBoundary.astro +79 -0
  31. package/templates/shared/dashboard/src/components/LoadingSkeleton.astro +105 -0
  32. package/templates/shared/dashboard/src/components/PageShell.astro +72 -0
  33. package/templates/shared/dashboard/src/components/SkipLinks.astro +22 -0
  34. package/templates/shared/dashboard/src/components/Toast.astro +170 -0
  35. package/templates/shared/dashboard/src/components/ToastContainer.astro +156 -0
  36. package/templates/shared/dashboard/src/components/costs/ProviderCostsGrid.tsx +401 -0
  37. package/templates/shared/dashboard/src/components/costs/index.ts +4 -0
  38. package/templates/shared/dashboard/src/components/overview/AlertBanner.tsx +94 -0
  39. package/templates/shared/dashboard/src/components/overview/index.ts +9 -0
  40. package/templates/shared/dashboard/src/components/resources/CostChart.tsx +170 -0
  41. package/templates/shared/dashboard/src/components/resources/ProviderCard.tsx +272 -0
  42. package/templates/shared/dashboard/src/components/resources/ProviderDetail.tsx +293 -0
  43. package/templates/shared/dashboard/src/components/settings/SettingsCard.astro +102 -0
  44. package/templates/shared/dashboard/src/components/usage/AllowanceGauge.astro +170 -0
  45. package/templates/shared/dashboard/src/components/usage/AnomalyAlerts.astro +633 -0
  46. package/templates/shared/dashboard/src/components/usage/BillingCycleCountdown.astro +192 -0
  47. package/templates/shared/dashboard/src/components/usage/BurnRateHero.astro +539 -0
  48. package/templates/shared/dashboard/src/components/usage/CircuitBreakerEventLog.astro +542 -0
  49. package/templates/shared/dashboard/src/components/usage/CircuitBreakerPanel.tsx +292 -0
  50. package/templates/shared/dashboard/src/components/usage/CircuitBreakerStatus.astro +669 -0
  51. package/templates/shared/dashboard/src/components/usage/CompactThresholdBanner.astro +531 -0
  52. package/templates/shared/dashboard/src/components/usage/ComparisonModeSelector.astro +651 -0
  53. package/templates/shared/dashboard/src/components/usage/CostBreakdownChart.astro +381 -0
  54. package/templates/shared/dashboard/src/components/usage/CostBreakdownTable.astro +210 -0
  55. package/templates/shared/dashboard/src/components/usage/CostDataTable.astro +0 -0
  56. package/templates/shared/dashboard/src/components/usage/CostDonutChart.astro +311 -0
  57. package/templates/shared/dashboard/src/components/usage/DailyCostChart.astro +632 -0
  58. package/templates/shared/dashboard/src/components/usage/ExportButton.astro +114 -0
  59. package/templates/shared/dashboard/src/components/usage/FeatureBudgetsTable.astro +872 -0
  60. package/templates/shared/dashboard/src/components/usage/FilterBar.astro +190 -0
  61. package/templates/shared/dashboard/src/components/usage/FilterToggles.astro +175 -0
  62. package/templates/shared/dashboard/src/components/usage/GitHubUsageCard.astro +537 -0
  63. package/templates/shared/dashboard/src/components/usage/OverageCostCard.astro +212 -0
  64. package/templates/shared/dashboard/src/components/usage/PlanUtilizationCard.astro +193 -0
  65. package/templates/shared/dashboard/src/components/usage/ProjectCard.astro +640 -0
  66. package/templates/shared/dashboard/src/components/usage/ProjectCardsGrid.astro +272 -0
  67. package/templates/shared/dashboard/src/components/usage/ResourceSearch.astro +279 -0
  68. package/templates/shared/dashboard/src/components/usage/ServiceUtilizationList.astro +604 -0
  69. package/templates/shared/dashboard/src/components/usage/SparklineCard.astro +399 -0
  70. package/templates/shared/dashboard/src/components/usage/StatsHero.astro +600 -0
  71. package/templates/shared/dashboard/src/components/usage/TableFilters.astro +1033 -0
  72. package/templates/shared/dashboard/src/components/usage/ThresholdAlert.astro +271 -0
  73. package/templates/shared/dashboard/src/components/usage/ThresholdSettings.astro +618 -0
  74. package/templates/shared/dashboard/src/components/usage/TopSpenderCard.astro +170 -0
  75. package/templates/shared/dashboard/src/components/usage/UnifiedResourceTable.astro +1737 -0
  76. package/templates/shared/dashboard/src/components/usage/UsageCard.astro +135 -0
  77. package/templates/shared/dashboard/src/components/usage/UsageHealthBanner.astro +387 -0
  78. package/templates/shared/dashboard/src/components/usage/UtilizationBar.astro +159 -0
  79. package/templates/shared/dashboard/src/components/usage/WorkersBreakdownTable.astro +659 -0
  80. package/templates/shared/dashboard/src/components/usage/daily/CostChart.astro +461 -0
  81. package/templates/shared/dashboard/src/components/usage/daily/CostTable.astro +946 -0
  82. package/templates/shared/dashboard/src/components/usage/daily/DailyOverview.astro +1079 -0
  83. package/templates/shared/dashboard/src/components/usage/design-tokens.ts +187 -0
  84. package/templates/shared/dashboard/src/components/usage/filters/InlineDateRange.astro +285 -0
  85. package/templates/shared/dashboard/src/components/usage/filters/PeriodButtons.astro +157 -0
  86. package/templates/shared/dashboard/src/components/usage/filters/ProjectSelect.astro +284 -0
  87. package/templates/shared/dashboard/src/components/usage/scripts/ai-tab-controller.ts +419 -0
  88. package/templates/shared/dashboard/src/components/usage/scripts/constants.ts +60 -0
  89. package/templates/shared/dashboard/src/components/usage/scripts/formatters.ts +62 -0
  90. package/templates/shared/dashboard/src/components/usage/scripts/overview-controller.ts +1633 -0
  91. package/templates/shared/dashboard/src/components/usage/scripts/resource-table-builder.ts +294 -0
  92. package/templates/shared/dashboard/src/components/usage/scripts/tabs-filters-controller.ts +464 -0
  93. package/templates/shared/dashboard/src/components/usage/state/index.ts +55 -0
  94. package/templates/shared/dashboard/src/components/usage/state/usageActions.ts +439 -0
  95. package/templates/shared/dashboard/src/components/usage/state/usageStore.ts +376 -0
  96. package/templates/shared/dashboard/src/components/usage/types.ts +283 -0
  97. package/templates/shared/dashboard/src/components/usage/usage-colors.ts +292 -0
  98. package/templates/shared/dashboard/src/pages/api/usage/ai-models.ts +235 -0
  99. package/templates/shared/dashboard/src/pages/api/usage/billing-context.ts +296 -0
  100. package/templates/shared/scripts/test-telemetry-flow.ts +464 -0
  101. package/templates/shared/tests/e2e/usage-export.test.ts +784 -0
  102. package/templates/shared/tests/e2e/usage-mobile.test.ts +531 -0
  103. package/templates/standard/dashboard/src/components/errors/PriorityBadge.astro +27 -0
  104. package/templates/standard/dashboard/src/components/infrastructure/HealthchecksStatus.tsx +293 -0
  105. package/templates/standard/dashboard/src/components/infrastructure/InfrastructureTabs.tsx +268 -0
  106. package/templates/standard/dashboard/src/pages/analytics.astro +64 -0
  107. package/templates/standard/dashboard/src/pages/api/infrastructure/alerts.ts +85 -0
  108. package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks/[id]/flips.ts +110 -0
  109. package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks.ts +101 -0
  110. package/templates/standard/dashboard/src/pages/api/infrastructure/uptime/[id]/response-times.ts +121 -0
  111. package/templates/standard/dashboard/src/pages/api/infrastructure/uptime.ts +89 -0
  112. package/templates/standard/dashboard/src/pages/api/test/service-auth.ts +178 -0
  113. package/templates/standard/tests/integration/connectors.test.ts +241 -0
  114. package/templates/standard/tests/integration/github-monitor.test.ts +143 -0
  115. package/templates/standard/tests/integration/ingestion.test.ts +211 -0
@@ -0,0 +1,79 @@
1
+ ---
2
+ /**
3
+ * ErrorBoundary.astro
4
+ * Error state component with message and retry action
5
+ * Use for failed data fetches or component errors
6
+ */
7
+
8
+ interface Props {
9
+ title?: string;
10
+ message?: string;
11
+ retryLabel?: string;
12
+ compact?: boolean;
13
+ }
14
+
15
+ const {
16
+ title = 'Something went wrong',
17
+ message = 'We encountered an error loading this content. Please try again.',
18
+ retryLabel = 'Try again',
19
+ compact = false,
20
+ } = Astro.props;
21
+ ---
22
+
23
+ <div
24
+ class={`flex flex-col items-center justify-center text-center bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg ${compact ? 'p-4' : 'p-8'}`}
25
+ role="alert"
26
+ >
27
+ {/* Error Icon */}
28
+ <div class={`text-red-500 dark:text-red-400 ${compact ? 'mb-2' : 'mb-4'}`}>
29
+ <svg
30
+ class={`${compact ? 'w-8 h-8' : 'w-12 h-12'}`}
31
+ fill="none"
32
+ stroke="currentColor"
33
+ viewBox="0 0 24 24"
34
+ >
35
+ <path
36
+ stroke-linecap="round"
37
+ stroke-linejoin="round"
38
+ stroke-width="2"
39
+ d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
40
+ ></path>
41
+ </svg>
42
+ </div>
43
+
44
+ {/* Title */}
45
+ <h3
46
+ class={`font-semibold text-red-800 dark:text-red-200 ${compact ? 'text-base mb-1' : 'text-lg mb-2'}`}
47
+ >
48
+ {title}
49
+ </h3>
50
+
51
+ {/* Message */}
52
+ <p
53
+ class={`text-red-600 dark:text-red-300 max-w-md ${compact ? 'text-sm mb-3' : 'text-base mb-6'}`}
54
+ >
55
+ {message}
56
+ </p>
57
+
58
+ {/* Retry Button */}
59
+ <button
60
+ type="button"
61
+ class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-600 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900 min-h-[44px]"
62
+ data-error-retry
63
+ >
64
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
65
+ <path
66
+ stroke-linecap="round"
67
+ stroke-linejoin="round"
68
+ stroke-width="2"
69
+ d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
70
+ ></path>
71
+ </svg>
72
+ {retryLabel}
73
+ </button>
74
+
75
+ {/* Additional Actions Slot */}
76
+ <div class="mt-4">
77
+ <slot name="actions" />
78
+ </div>
79
+ </div>
@@ -0,0 +1,105 @@
1
+ ---
2
+ /**
3
+ * LoadingSkeleton.astro
4
+ * Skeleton loading placeholders for tables, cards, and forms
5
+ * Uses Tailwind's animate-pulse for shimmer effect
6
+ */
7
+
8
+ interface Props {
9
+ variant?: 'table' | 'card' | 'form' | 'text' | 'metric';
10
+ rows?: number;
11
+ columns?: number;
12
+ }
13
+
14
+ const { variant = 'text', rows = 3, columns = 4 } = Astro.props;
15
+ ---
16
+
17
+ {
18
+ variant === 'text' && (
19
+ <div class="animate-pulse space-y-3">
20
+ {Array.from({ length: rows }).map((_, i) => (
21
+ <div
22
+ class={`h-4 bg-gray-200 dark:bg-gray-700 rounded ${i === rows - 1 ? 'w-2/3' : 'w-full'}`}
23
+ />
24
+ ))}
25
+ </div>
26
+ )
27
+ }
28
+
29
+ {
30
+ variant === 'table' && (
31
+ <div class="animate-pulse">
32
+ {/* Table header */}
33
+ <div
34
+ class="grid gap-4 mb-4"
35
+ style={`grid-template-columns: repeat(${columns}, minmax(0, 1fr))`}
36
+ >
37
+ {Array.from({ length: columns }).map(() => (
38
+ <div class="h-4 bg-gray-300 dark:bg-gray-600 rounded" />
39
+ ))}
40
+ </div>
41
+ {/* Table rows */}
42
+ <div class="space-y-4">
43
+ {Array.from({ length: rows }).map(() => (
44
+ <div
45
+ class="grid gap-4"
46
+ style={`grid-template-columns: repeat(${columns}, minmax(0, 1fr))`}
47
+ >
48
+ {Array.from({ length: columns }).map(() => (
49
+ <div class="h-4 bg-gray-200 dark:bg-gray-700 rounded" />
50
+ ))}
51
+ </div>
52
+ ))}
53
+ </div>
54
+ </div>
55
+ )
56
+ }
57
+
58
+ {
59
+ variant === 'card' && (
60
+ <div class="animate-pulse bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6">
61
+ <div class="flex items-center gap-4 mb-4">
62
+ <div class="w-12 h-12 bg-gray-200 dark:bg-gray-700 rounded-full" />
63
+ <div class="flex-1 space-y-2">
64
+ <div class="h-4 bg-gray-200 dark:bg-gray-700 rounded w-3/4" />
65
+ <div class="h-3 bg-gray-200 dark:bg-gray-700 rounded w-1/2" />
66
+ </div>
67
+ </div>
68
+ <div class="space-y-3">
69
+ <div class="h-4 bg-gray-200 dark:bg-gray-700 rounded" />
70
+ <div class="h-4 bg-gray-200 dark:bg-gray-700 rounded w-5/6" />
71
+ </div>
72
+ </div>
73
+ )
74
+ }
75
+
76
+ {
77
+ variant === 'form' && (
78
+ <div class="animate-pulse space-y-6">
79
+ {Array.from({ length: rows }).map(() => (
80
+ <div class="space-y-2">
81
+ <div class="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/4" />
82
+ <div class="h-10 bg-gray-200 dark:bg-gray-700 rounded" />
83
+ </div>
84
+ ))}
85
+ <div class="flex gap-3 pt-4">
86
+ <div class="h-10 bg-gray-300 dark:bg-gray-600 rounded w-24" />
87
+ <div class="h-10 bg-gray-200 dark:bg-gray-700 rounded w-24" />
88
+ </div>
89
+ </div>
90
+ )
91
+ }
92
+
93
+ {
94
+ variant === 'metric' && (
95
+ <div class="animate-pulse grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
96
+ {Array.from({ length: 4 }).map(() => (
97
+ <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6">
98
+ <div class="h-3 bg-gray-200 dark:bg-gray-700 rounded w-1/2 mb-3" />
99
+ <div class="h-8 bg-gray-200 dark:bg-gray-700 rounded w-2/3 mb-2" />
100
+ <div class="h-3 bg-gray-200 dark:bg-gray-700 rounded w-1/3" />
101
+ </div>
102
+ ))}
103
+ </div>
104
+ )
105
+ }
@@ -0,0 +1,72 @@
1
+ ---
2
+ /**
3
+ * PageShell.astro
4
+ * Consistent page wrapper with title area, subtitle, and action buttons
5
+ * Use this component to wrap page content for visual consistency across all dashboard pages
6
+ */
7
+ import Breadcrumbs from './Breadcrumbs.astro';
8
+
9
+ interface BreadcrumbItem {
10
+ label: string;
11
+ href?: string;
12
+ }
13
+
14
+ interface Props {
15
+ title: string;
16
+ subtitle?: string;
17
+ breadcrumbs?: BreadcrumbItem[];
18
+ maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '4xl' | '6xl' | '7xl' | 'full';
19
+ }
20
+
21
+ const { title, subtitle, breadcrumbs, maxWidth = '7xl' } = Astro.props;
22
+
23
+ const maxWidthClasses = {
24
+ sm: 'max-w-sm',
25
+ md: 'max-w-md',
26
+ lg: 'max-w-lg',
27
+ xl: 'max-w-xl',
28
+ '2xl': 'max-w-2xl',
29
+ '4xl': 'max-w-4xl',
30
+ '6xl': 'max-w-6xl',
31
+ '7xl': 'max-w-7xl',
32
+ full: 'max-w-full',
33
+ };
34
+ ---
35
+
36
+ <div class={`${maxWidthClasses[maxWidth]} mx-auto`}>
37
+ {/* Breadcrumbs (optional) */}
38
+ {breadcrumbs && breadcrumbs.length > 0 && <Breadcrumbs items={breadcrumbs} />}
39
+
40
+ {/* Page Header */}
41
+ <div class="mb-8">
42
+ {/* Title Row - responsive: stacks on mobile */}
43
+ <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
44
+ {/* Title and Subtitle */}
45
+ <div class="min-w-0 flex-1">
46
+ <h1 class="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-white truncate">
47
+ {title}
48
+ </h1>
49
+ {
50
+ subtitle && (
51
+ <p class="mt-1 text-sm sm:text-base text-gray-600 dark:text-gray-400">{subtitle}</p>
52
+ )
53
+ }
54
+ </div>
55
+
56
+ {/* Actions Slot - renders action buttons */}
57
+ <div class="flex flex-wrap items-center gap-3 sm:flex-shrink-0">
58
+ <slot name="actions" />
59
+ </div>
60
+ </div>
61
+ </div>
62
+
63
+ {/* Page Content */}
64
+ <slot />
65
+ </div>
66
+
67
+ <style>
68
+ /* Ensure long titles don't break layout */
69
+ h1 {
70
+ word-break: break-word;
71
+ }
72
+ </style>
@@ -0,0 +1,22 @@
1
+ ---
2
+ /**
3
+ * SkipLinks.astro
4
+ * Accessibility skip links for keyboard navigation
5
+ * Only visible when focused (screen reader and keyboard users)
6
+ */
7
+ ---
8
+
9
+ <div class="skip-links">
10
+ <a
11
+ href="#main-content"
12
+ class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-[100] focus:px-4 focus:py-2 focus:bg-blue-600 focus:text-white focus:rounded-lg focus:shadow-lg focus:outline-none focus:ring-2 focus:ring-blue-400"
13
+ >
14
+ Skip to main content
15
+ </a>
16
+ <a
17
+ href="#sidebar"
18
+ class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-48 focus:z-[100] focus:px-4 focus:py-2 focus:bg-blue-600 focus:text-white focus:rounded-lg focus:shadow-lg focus:outline-none focus:ring-2 focus:ring-blue-400"
19
+ >
20
+ Skip to navigation
21
+ </a>
22
+ </div>
@@ -0,0 +1,170 @@
1
+ ---
2
+ /**
3
+ * Toast.astro
4
+ * Individual toast notification component
5
+ * Variants: success, error, warning, info
6
+ * Used by ToastContainer - do not use directly
7
+ */
8
+
9
+ interface Props {
10
+ id?: string;
11
+ type?: 'success' | 'error' | 'warning' | 'info';
12
+ title: string;
13
+ message?: string;
14
+ dismissible?: boolean;
15
+ duration?: number; // Auto-dismiss after ms (0 = no auto-dismiss)
16
+ }
17
+
18
+ const { id, type = 'info', title, message, dismissible = true, duration = 5000 } = Astro.props;
19
+
20
+ const typeStyles = {
21
+ success: {
22
+ bg: 'bg-green-50 dark:bg-green-900/30',
23
+ border: 'border-green-200 dark:border-green-800',
24
+ icon: 'text-green-500 dark:text-green-400',
25
+ title: 'text-green-800 dark:text-green-200',
26
+ message: 'text-green-600 dark:text-green-300',
27
+ },
28
+ error: {
29
+ bg: 'bg-red-50 dark:bg-red-900/30',
30
+ border: 'border-red-200 dark:border-red-800',
31
+ icon: 'text-red-500 dark:text-red-400',
32
+ title: 'text-red-800 dark:text-red-200',
33
+ message: 'text-red-600 dark:text-red-300',
34
+ },
35
+ warning: {
36
+ bg: 'bg-yellow-50 dark:bg-yellow-900/30',
37
+ border: 'border-yellow-200 dark:border-yellow-800',
38
+ icon: 'text-yellow-500 dark:text-yellow-400',
39
+ title: 'text-yellow-800 dark:text-yellow-200',
40
+ message: 'text-yellow-600 dark:text-yellow-300',
41
+ },
42
+ info: {
43
+ bg: 'bg-blue-50 dark:bg-blue-900/30',
44
+ border: 'border-blue-200 dark:border-blue-800',
45
+ icon: 'text-blue-500 dark:text-blue-400',
46
+ title: 'text-blue-800 dark:text-blue-200',
47
+ message: 'text-blue-600 dark:text-blue-300',
48
+ },
49
+ };
50
+
51
+ const styles = typeStyles[type];
52
+ ---
53
+
54
+ <div
55
+ class={`toast flex items-start gap-3 p-4 rounded-lg border shadow-lg ${styles.bg} ${styles.border} animate-slide-in`}
56
+ role="alert"
57
+ data-toast-id={id}
58
+ data-toast-duration={duration}
59
+ >
60
+ {/* Icon */}
61
+ <div class={`flex-shrink-0 ${styles.icon}`}>
62
+ {
63
+ type === 'success' && (
64
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
65
+ <path
66
+ stroke-linecap="round"
67
+ stroke-linejoin="round"
68
+ stroke-width="2"
69
+ d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
70
+ />
71
+ </svg>
72
+ )
73
+ }
74
+ {
75
+ type === 'error' && (
76
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
77
+ <path
78
+ stroke-linecap="round"
79
+ stroke-linejoin="round"
80
+ stroke-width="2"
81
+ d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
82
+ />
83
+ </svg>
84
+ )
85
+ }
86
+ {
87
+ type === 'warning' && (
88
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
89
+ <path
90
+ stroke-linecap="round"
91
+ stroke-linejoin="round"
92
+ stroke-width="2"
93
+ d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
94
+ />
95
+ </svg>
96
+ )
97
+ }
98
+ {
99
+ type === 'info' && (
100
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
101
+ <path
102
+ stroke-linecap="round"
103
+ stroke-linejoin="round"
104
+ stroke-width="2"
105
+ d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
106
+ />
107
+ </svg>
108
+ )
109
+ }
110
+ </div>
111
+
112
+ {/* Content */}
113
+ <div class="flex-1 min-w-0">
114
+ <p class={`font-medium text-sm ${styles.title}`}>{title}</p>
115
+ {message && <p class={`mt-1 text-sm ${styles.message}`}>{message}</p>}
116
+ </div>
117
+
118
+ {/* Dismiss button */}
119
+ {
120
+ dismissible && (
121
+ <button
122
+ type="button"
123
+ class={`flex-shrink-0 p-1 rounded hover:bg-black/10 dark:hover:bg-white/10 transition-colors ${styles.icon}`}
124
+ aria-label="Dismiss"
125
+ data-toast-dismiss
126
+ >
127
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
128
+ <path
129
+ stroke-linecap="round"
130
+ stroke-linejoin="round"
131
+ stroke-width="2"
132
+ d="M6 18L18 6M6 6l12 12"
133
+ />
134
+ </svg>
135
+ </button>
136
+ )
137
+ }
138
+ </div>
139
+
140
+ <style>
141
+ @keyframes slide-in {
142
+ from {
143
+ transform: translateX(100%);
144
+ opacity: 0;
145
+ }
146
+ to {
147
+ transform: translateX(0);
148
+ opacity: 1;
149
+ }
150
+ }
151
+
152
+ @keyframes slide-out {
153
+ from {
154
+ transform: translateX(0);
155
+ opacity: 1;
156
+ }
157
+ to {
158
+ transform: translateX(100%);
159
+ opacity: 0;
160
+ }
161
+ }
162
+
163
+ .animate-slide-in {
164
+ animation: slide-in 0.3s ease-out;
165
+ }
166
+
167
+ .animate-slide-out {
168
+ animation: slide-out 0.3s ease-in forwards;
169
+ }
170
+ </style>
@@ -0,0 +1,156 @@
1
+ ---
2
+ /**
3
+ * ToastContainer.astro
4
+ * Container for toast notifications - positioned top-right
5
+ * Include once in DashboardLayout, then use window.showToast() to display toasts
6
+ *
7
+ * Usage:
8
+ * window.showToast({ type: 'success', title: 'Saved!', message: 'Changes saved successfully' });
9
+ * window.showToast({ type: 'error', title: 'Error', message: 'Something went wrong' });
10
+ */
11
+ ---
12
+
13
+ <div
14
+ id="toast-container"
15
+ class="fixed top-20 right-4 z-80 flex flex-col gap-3 w-full max-w-sm pointer-events-none"
16
+ aria-live="polite"
17
+ aria-atomic="true"
18
+ >
19
+ <!-- Toasts will be injected here -->
20
+ </div>
21
+
22
+ <script is:inline>
23
+ // Toast management system
24
+ (function () {
25
+ let toastId = 0;
26
+
27
+ const typeConfig = {
28
+ success: {
29
+ bg: 'bg-green-50 dark:bg-green-900/30',
30
+ border: 'border-green-200 dark:border-green-800',
31
+ icon: 'text-green-500 dark:text-green-400',
32
+ title: 'text-green-800 dark:text-green-200',
33
+ message: 'text-green-600 dark:text-green-300',
34
+ iconSvg:
35
+ '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>',
36
+ },
37
+ error: {
38
+ bg: 'bg-red-50 dark:bg-red-900/30',
39
+ border: 'border-red-200 dark:border-red-800',
40
+ icon: 'text-red-500 dark:text-red-400',
41
+ title: 'text-red-800 dark:text-red-200',
42
+ message: 'text-red-600 dark:text-red-300',
43
+ iconSvg:
44
+ '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>',
45
+ },
46
+ warning: {
47
+ bg: 'bg-yellow-50 dark:bg-yellow-900/30',
48
+ border: 'border-yellow-200 dark:border-yellow-800',
49
+ icon: 'text-yellow-500 dark:text-yellow-400',
50
+ title: 'text-yellow-800 dark:text-yellow-200',
51
+ message: 'text-yellow-600 dark:text-yellow-300',
52
+ iconSvg:
53
+ '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>',
54
+ },
55
+ info: {
56
+ bg: 'bg-blue-50 dark:bg-blue-900/30',
57
+ border: 'border-blue-200 dark:border-blue-800',
58
+ icon: 'text-blue-500 dark:text-blue-400',
59
+ title: 'text-blue-800 dark:text-blue-200',
60
+ message: 'text-blue-600 dark:text-blue-300',
61
+ iconSvg:
62
+ '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>',
63
+ },
64
+ };
65
+
66
+ function dismissToast(id) {
67
+ const toast = document.querySelector(`[data-toast-id="${id}"]`);
68
+ if (toast) {
69
+ toast.style.animation = 'slide-out 0.3s ease-in forwards';
70
+ setTimeout(() => toast.remove(), 300);
71
+ }
72
+ }
73
+
74
+ function showToast(options) {
75
+ const { type = 'info', title, message = '', duration = 5000, dismissible = true } = options;
76
+
77
+ const id = ++toastId;
78
+ const config = typeConfig[type] || typeConfig.info;
79
+ const container = document.getElementById('toast-container');
80
+
81
+ if (!container) return;
82
+
83
+ const toast = document.createElement('div');
84
+ toast.className = `toast flex items-start gap-3 p-4 rounded-lg border shadow-lg pointer-events-auto ${config.bg} ${config.border}`;
85
+ toast.setAttribute('role', 'alert');
86
+ toast.setAttribute('data-toast-id', id);
87
+ toast.style.animation = 'slide-in 0.3s ease-out';
88
+
89
+ toast.innerHTML = `
90
+ <div class="flex-shrink-0 ${config.icon}">${config.iconSvg}</div>
91
+ <div class="flex-1 min-w-0">
92
+ <p class="font-medium text-sm ${config.title}">${title}</p>
93
+ ${message ? `<p class="mt-1 text-sm ${config.message}">${message}</p>` : ''}
94
+ </div>
95
+ ${
96
+ dismissible
97
+ ? `
98
+ <button type="button" class="flex-shrink-0 p-1 rounded hover:bg-black/10 dark:hover:bg-white/10 transition-colors ${config.icon}" aria-label="Dismiss" data-toast-dismiss>
99
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
100
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
101
+ </svg>
102
+ </button>
103
+ `
104
+ : ''
105
+ }
106
+ `;
107
+
108
+ // Add dismiss handler
109
+ const dismissBtn = toast.querySelector('[data-toast-dismiss]');
110
+ if (dismissBtn) {
111
+ dismissBtn.addEventListener('click', () => dismissToast(id));
112
+ }
113
+
114
+ // Auto-dismiss
115
+ if (duration > 0) {
116
+ setTimeout(() => dismissToast(id), duration);
117
+ }
118
+
119
+ container.appendChild(toast);
120
+ return id;
121
+ }
122
+
123
+ // Expose globally
124
+ window.showToast = showToast;
125
+ window.dismissToast = dismissToast;
126
+ })();
127
+ </script>
128
+
129
+ <style is:global>
130
+ @keyframes slide-in {
131
+ from {
132
+ transform: translateX(100%);
133
+ opacity: 0;
134
+ }
135
+ to {
136
+ transform: translateX(0);
137
+ opacity: 1;
138
+ }
139
+ }
140
+
141
+ @keyframes slide-out {
142
+ from {
143
+ transform: translateX(0);
144
+ opacity: 1;
145
+ }
146
+ to {
147
+ transform: translateX(100%);
148
+ opacity: 0;
149
+ }
150
+ }
151
+
152
+ /* Z-index 80 for toasts (above modals) */
153
+ #toast-container {
154
+ z-index: 80;
155
+ }
156
+ </style>