@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.
- package/README.md +2 -5
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +121 -3
- package/package.json +1 -1
- package/templates/full/dashboard/src/components/notifications/NotificationDropdown.tsx +130 -0
- package/templates/full/dashboard/src/components/notifications/NotificationItem.tsx +264 -0
- package/templates/full/dashboard/src/components/patterns/PatternInfoButton.tsx +60 -0
- package/templates/full/dashboard/src/components/reports/FeatureUsageReport.tsx +339 -0
- package/templates/full/dashboard/src/components/search/SearchResultGroup.tsx +46 -0
- package/templates/full/dashboard/src/components/search/SearchResultItem.tsx +212 -0
- package/templates/full/dashboard/src/pages/api/patterns/[id]/approve.ts +49 -0
- package/templates/full/dashboard/src/pages/api/patterns/[id]/reject.ts +50 -0
- package/templates/full/dashboard/src/pages/api/reports/digests/stats.ts +38 -0
- package/templates/full/dashboard/src/pages/api/reports/digests.ts +39 -0
- package/templates/full/dashboard/src/pages/api/search/reindex/[type].ts +56 -0
- package/templates/full/dashboard/src/pages/api/test-reports/[id].ts +102 -0
- package/templates/full/dashboard/src/pages/feedback.astro +365 -0
- package/templates/full/dashboard/src/pages/kiosk.astro +206 -0
- package/templates/full/dashboard/src/pages/map.astro +561 -0
- package/templates/full/dashboard/src/pages/revenue.astro +72 -0
- package/templates/full/dashboard/src/pages/tests.astro +431 -0
- package/templates/full/scripts/ops/audit-cost-anomaly.ts +430 -0
- package/templates/full/scripts/ops/verify-account-total.ts +256 -0
- package/templates/full/tests/integration/feedback-schema.test.ts +361 -0
- package/templates/full/tests/integration/r2-archive.test.ts +108 -0
- package/templates/shared/.github/workflows/dependabot-automerge.yml +41 -0
- package/templates/shared/.github/workflows/validate-controls.yml +27 -0
- package/templates/shared/dashboard/src/components/Breadcrumbs.astro +101 -0
- package/templates/shared/dashboard/src/components/EmptyState.astro +46 -0
- package/templates/shared/dashboard/src/components/ErrorBoundary.astro +79 -0
- package/templates/shared/dashboard/src/components/LoadingSkeleton.astro +105 -0
- package/templates/shared/dashboard/src/components/PageShell.astro +72 -0
- package/templates/shared/dashboard/src/components/SkipLinks.astro +22 -0
- package/templates/shared/dashboard/src/components/Toast.astro +170 -0
- package/templates/shared/dashboard/src/components/ToastContainer.astro +156 -0
- package/templates/shared/dashboard/src/components/costs/ProviderCostsGrid.tsx +401 -0
- package/templates/shared/dashboard/src/components/costs/index.ts +4 -0
- package/templates/shared/dashboard/src/components/overview/AlertBanner.tsx +94 -0
- package/templates/shared/dashboard/src/components/overview/index.ts +9 -0
- package/templates/shared/dashboard/src/components/resources/CostChart.tsx +170 -0
- package/templates/shared/dashboard/src/components/resources/ProviderCard.tsx +272 -0
- package/templates/shared/dashboard/src/components/resources/ProviderDetail.tsx +293 -0
- package/templates/shared/dashboard/src/components/settings/SettingsCard.astro +102 -0
- package/templates/shared/dashboard/src/components/usage/AllowanceGauge.astro +170 -0
- package/templates/shared/dashboard/src/components/usage/AnomalyAlerts.astro +633 -0
- package/templates/shared/dashboard/src/components/usage/BillingCycleCountdown.astro +192 -0
- package/templates/shared/dashboard/src/components/usage/BurnRateHero.astro +539 -0
- package/templates/shared/dashboard/src/components/usage/CircuitBreakerEventLog.astro +542 -0
- package/templates/shared/dashboard/src/components/usage/CircuitBreakerPanel.tsx +292 -0
- package/templates/shared/dashboard/src/components/usage/CircuitBreakerStatus.astro +669 -0
- package/templates/shared/dashboard/src/components/usage/CompactThresholdBanner.astro +531 -0
- package/templates/shared/dashboard/src/components/usage/ComparisonModeSelector.astro +651 -0
- package/templates/shared/dashboard/src/components/usage/CostBreakdownChart.astro +381 -0
- package/templates/shared/dashboard/src/components/usage/CostBreakdownTable.astro +210 -0
- package/templates/shared/dashboard/src/components/usage/CostDataTable.astro +0 -0
- package/templates/shared/dashboard/src/components/usage/CostDonutChart.astro +311 -0
- package/templates/shared/dashboard/src/components/usage/DailyCostChart.astro +632 -0
- package/templates/shared/dashboard/src/components/usage/ExportButton.astro +114 -0
- package/templates/shared/dashboard/src/components/usage/FeatureBudgetsTable.astro +872 -0
- package/templates/shared/dashboard/src/components/usage/FilterBar.astro +190 -0
- package/templates/shared/dashboard/src/components/usage/FilterToggles.astro +175 -0
- package/templates/shared/dashboard/src/components/usage/GitHubUsageCard.astro +537 -0
- package/templates/shared/dashboard/src/components/usage/OverageCostCard.astro +212 -0
- package/templates/shared/dashboard/src/components/usage/PlanUtilizationCard.astro +193 -0
- package/templates/shared/dashboard/src/components/usage/ProjectCard.astro +640 -0
- package/templates/shared/dashboard/src/components/usage/ProjectCardsGrid.astro +272 -0
- package/templates/shared/dashboard/src/components/usage/ResourceSearch.astro +279 -0
- package/templates/shared/dashboard/src/components/usage/ServiceUtilizationList.astro +604 -0
- package/templates/shared/dashboard/src/components/usage/SparklineCard.astro +399 -0
- package/templates/shared/dashboard/src/components/usage/StatsHero.astro +600 -0
- package/templates/shared/dashboard/src/components/usage/TableFilters.astro +1033 -0
- package/templates/shared/dashboard/src/components/usage/ThresholdAlert.astro +271 -0
- package/templates/shared/dashboard/src/components/usage/ThresholdSettings.astro +618 -0
- package/templates/shared/dashboard/src/components/usage/TopSpenderCard.astro +170 -0
- package/templates/shared/dashboard/src/components/usage/UnifiedResourceTable.astro +1737 -0
- package/templates/shared/dashboard/src/components/usage/UsageCard.astro +135 -0
- package/templates/shared/dashboard/src/components/usage/UsageHealthBanner.astro +387 -0
- package/templates/shared/dashboard/src/components/usage/UtilizationBar.astro +159 -0
- package/templates/shared/dashboard/src/components/usage/WorkersBreakdownTable.astro +659 -0
- package/templates/shared/dashboard/src/components/usage/daily/CostChart.astro +461 -0
- package/templates/shared/dashboard/src/components/usage/daily/CostTable.astro +946 -0
- package/templates/shared/dashboard/src/components/usage/daily/DailyOverview.astro +1079 -0
- package/templates/shared/dashboard/src/components/usage/design-tokens.ts +187 -0
- package/templates/shared/dashboard/src/components/usage/filters/InlineDateRange.astro +285 -0
- package/templates/shared/dashboard/src/components/usage/filters/PeriodButtons.astro +157 -0
- package/templates/shared/dashboard/src/components/usage/filters/ProjectSelect.astro +284 -0
- package/templates/shared/dashboard/src/components/usage/scripts/ai-tab-controller.ts +419 -0
- package/templates/shared/dashboard/src/components/usage/scripts/constants.ts +60 -0
- package/templates/shared/dashboard/src/components/usage/scripts/formatters.ts +62 -0
- package/templates/shared/dashboard/src/components/usage/scripts/overview-controller.ts +1633 -0
- package/templates/shared/dashboard/src/components/usage/scripts/resource-table-builder.ts +294 -0
- package/templates/shared/dashboard/src/components/usage/scripts/tabs-filters-controller.ts +464 -0
- package/templates/shared/dashboard/src/components/usage/state/index.ts +55 -0
- package/templates/shared/dashboard/src/components/usage/state/usageActions.ts +439 -0
- package/templates/shared/dashboard/src/components/usage/state/usageStore.ts +376 -0
- package/templates/shared/dashboard/src/components/usage/types.ts +283 -0
- package/templates/shared/dashboard/src/components/usage/usage-colors.ts +292 -0
- package/templates/shared/dashboard/src/pages/api/usage/ai-models.ts +235 -0
- package/templates/shared/dashboard/src/pages/api/usage/billing-context.ts +296 -0
- package/templates/shared/scripts/test-telemetry-flow.ts +464 -0
- package/templates/shared/tests/e2e/usage-export.test.ts +784 -0
- package/templates/shared/tests/e2e/usage-mobile.test.ts +531 -0
- package/templates/standard/dashboard/src/components/errors/PriorityBadge.astro +27 -0
- package/templates/standard/dashboard/src/components/infrastructure/HealthchecksStatus.tsx +293 -0
- package/templates/standard/dashboard/src/components/infrastructure/InfrastructureTabs.tsx +268 -0
- package/templates/standard/dashboard/src/pages/analytics.astro +64 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/alerts.ts +85 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks/[id]/flips.ts +110 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks.ts +101 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/uptime/[id]/response-times.ts +121 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/uptime.ts +89 -0
- package/templates/standard/dashboard/src/pages/api/test/service-auth.ts +178 -0
- package/templates/standard/tests/integration/connectors.test.ts +241 -0
- package/templates/standard/tests/integration/github-monitor.test.ts +143 -0
- 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>
|