@littlebearapps/platform-admin-sdk 2.0.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 (185) hide show
  1. package/README.md +4 -7
  2. package/dist/templates.d.ts +1 -1
  3. package/dist/templates.js +206 -4
  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/DigestStats.tsx +151 -0
  9. package/templates/full/dashboard/src/components/reports/FeatureUsageReport.tsx +339 -0
  10. package/templates/full/dashboard/src/components/reports/HealthTrendsReport.tsx +192 -0
  11. package/templates/full/dashboard/src/components/search/SearchResultGroup.tsx +46 -0
  12. package/templates/full/dashboard/src/components/search/SearchResultItem.tsx +212 -0
  13. package/templates/full/dashboard/src/components/usage/AIModelBreakdown.tsx +364 -0
  14. package/templates/full/dashboard/src/components/usage/unified/Recommendations.tsx +149 -0
  15. package/templates/full/dashboard/src/lib/cloudflare/alerting.ts +486 -0
  16. package/templates/full/dashboard/src/lib/cloudflare/graphql.ts +4785 -0
  17. package/templates/full/dashboard/src/lib/cloudflare/project-registry.ts +451 -0
  18. package/templates/full/dashboard/src/lib/notifications/api.ts +197 -0
  19. package/templates/full/dashboard/src/lib/notifications/types.ts.hbs +97 -0
  20. package/templates/full/dashboard/src/lib/patterns/api.ts +120 -0
  21. package/templates/full/dashboard/src/lib/patterns/types.ts +127 -0
  22. package/templates/full/dashboard/src/lib/reports/types.ts +231 -0
  23. package/templates/full/dashboard/src/lib/search/api.ts +258 -0
  24. package/templates/full/dashboard/src/lib/search/types.ts.hbs +115 -0
  25. package/templates/full/dashboard/src/lib/settings/api.ts.hbs +201 -0
  26. package/templates/full/dashboard/src/lib/settings/types.ts.hbs +104 -0
  27. package/templates/full/dashboard/src/lib/usage/allowance-config.ts.hbs +547 -0
  28. package/templates/full/dashboard/src/lib/usage/providers.ts +331 -0
  29. package/templates/full/dashboard/src/pages/api/patterns/[id]/approve.ts +49 -0
  30. package/templates/full/dashboard/src/pages/api/patterns/[id]/reject.ts +50 -0
  31. package/templates/full/dashboard/src/pages/api/reports/digests/stats.ts +38 -0
  32. package/templates/full/dashboard/src/pages/api/reports/digests.ts +39 -0
  33. package/templates/full/dashboard/src/pages/api/search/reindex/[type].ts +56 -0
  34. package/templates/full/dashboard/src/pages/api/test-reports/[id].ts +102 -0
  35. package/templates/full/dashboard/src/pages/feedback.astro +365 -0
  36. package/templates/full/dashboard/src/pages/kiosk.astro +206 -0
  37. package/templates/full/dashboard/src/pages/map.astro +561 -0
  38. package/templates/full/dashboard/src/pages/revenue.astro +72 -0
  39. package/templates/full/dashboard/src/pages/tests.astro +431 -0
  40. package/templates/full/scripts/ops/audit-cost-anomaly.ts +430 -0
  41. package/templates/full/scripts/ops/verify-account-total.ts +256 -0
  42. package/templates/full/tests/integration/feedback-schema.test.ts +361 -0
  43. package/templates/full/tests/integration/r2-archive.test.ts +108 -0
  44. package/templates/shared/.github/workflows/dependabot-automerge.yml +41 -0
  45. package/templates/shared/.github/workflows/validate-controls.yml +27 -0
  46. package/templates/shared/dashboard/src/components/Breadcrumbs.astro +101 -0
  47. package/templates/shared/dashboard/src/components/EmptyState.astro +46 -0
  48. package/templates/shared/dashboard/src/components/ErrorBoundary.astro +79 -0
  49. package/templates/shared/dashboard/src/components/LoadingSkeleton.astro +105 -0
  50. package/templates/shared/dashboard/src/components/PageShell.astro +72 -0
  51. package/templates/shared/dashboard/src/components/SkipLinks.astro +22 -0
  52. package/templates/shared/dashboard/src/components/Toast.astro +170 -0
  53. package/templates/shared/dashboard/src/components/ToastContainer.astro +156 -0
  54. package/templates/shared/dashboard/src/components/costs/ProviderCostsGrid.tsx +401 -0
  55. package/templates/shared/dashboard/src/components/costs/index.ts +4 -0
  56. package/templates/shared/dashboard/src/components/overview/AlertBanner.tsx +94 -0
  57. package/templates/shared/dashboard/src/components/overview/index.ts +9 -0
  58. package/templates/shared/dashboard/src/components/reports/ReportInfoButton.tsx +98 -0
  59. package/templates/shared/dashboard/src/components/resources/CostChart.tsx +170 -0
  60. package/templates/shared/dashboard/src/components/resources/ProviderCard.tsx +272 -0
  61. package/templates/shared/dashboard/src/components/resources/ProviderDetail.tsx +293 -0
  62. package/templates/shared/dashboard/src/components/settings/SettingsCard.astro +102 -0
  63. package/templates/shared/dashboard/src/components/usage/AllowanceGauge.astro +170 -0
  64. package/templates/shared/dashboard/src/components/usage/AnomalyAlerts.astro +633 -0
  65. package/templates/shared/dashboard/src/components/usage/BillingCycleCountdown.astro +192 -0
  66. package/templates/shared/dashboard/src/components/usage/BurnRateHero.astro +539 -0
  67. package/templates/shared/dashboard/src/components/usage/CircuitBreakerEventLog.astro +542 -0
  68. package/templates/shared/dashboard/src/components/usage/CircuitBreakerPanel.tsx +292 -0
  69. package/templates/shared/dashboard/src/components/usage/CircuitBreakerStatus.astro +669 -0
  70. package/templates/shared/dashboard/src/components/usage/CompactThresholdBanner.astro +531 -0
  71. package/templates/shared/dashboard/src/components/usage/ComparisonModeSelector.astro +651 -0
  72. package/templates/shared/dashboard/src/components/usage/CostBreakdownChart.astro +381 -0
  73. package/templates/shared/dashboard/src/components/usage/CostBreakdownTable.astro +210 -0
  74. package/templates/shared/dashboard/src/components/usage/CostDataTable.astro +0 -0
  75. package/templates/shared/dashboard/src/components/usage/CostDonutChart.astro +311 -0
  76. package/templates/shared/dashboard/src/components/usage/DailyCostChart.astro +632 -0
  77. package/templates/shared/dashboard/src/components/usage/ExportButton.astro +114 -0
  78. package/templates/shared/dashboard/src/components/usage/FeatureBudgetsTable.astro +872 -0
  79. package/templates/shared/dashboard/src/components/usage/FilterBar.astro +190 -0
  80. package/templates/shared/dashboard/src/components/usage/FilterToggles.astro +175 -0
  81. package/templates/shared/dashboard/src/components/usage/GitHubUsageCard.astro +537 -0
  82. package/templates/shared/dashboard/src/components/usage/OverageCostCard.astro +212 -0
  83. package/templates/shared/dashboard/src/components/usage/PlanUtilizationCard.astro +193 -0
  84. package/templates/shared/dashboard/src/components/usage/ProjectCard.astro +640 -0
  85. package/templates/shared/dashboard/src/components/usage/ProjectCardsGrid.astro +272 -0
  86. package/templates/shared/dashboard/src/components/usage/ResourceSearch.astro +279 -0
  87. package/templates/shared/dashboard/src/components/usage/ServiceUtilizationList.astro +604 -0
  88. package/templates/shared/dashboard/src/components/usage/SparklineCard.astro +399 -0
  89. package/templates/shared/dashboard/src/components/usage/StatsHero.astro +600 -0
  90. package/templates/shared/dashboard/src/components/usage/TableFilters.astro +1033 -0
  91. package/templates/shared/dashboard/src/components/usage/ThresholdAlert.astro +271 -0
  92. package/templates/shared/dashboard/src/components/usage/ThresholdSettings.astro +618 -0
  93. package/templates/shared/dashboard/src/components/usage/TopSpenderCard.astro +170 -0
  94. package/templates/shared/dashboard/src/components/usage/UnifiedResourceTable.astro +1737 -0
  95. package/templates/shared/dashboard/src/components/usage/UsageCard.astro +135 -0
  96. package/templates/shared/dashboard/src/components/usage/UsageHealthBanner.astro +387 -0
  97. package/templates/shared/dashboard/src/components/usage/UtilizationBar.astro +159 -0
  98. package/templates/shared/dashboard/src/components/usage/WorkersBreakdownTable.astro +659 -0
  99. package/templates/shared/dashboard/src/components/usage/daily/CostChart.astro +461 -0
  100. package/templates/shared/dashboard/src/components/usage/daily/CostTable.astro +946 -0
  101. package/templates/shared/dashboard/src/components/usage/daily/DailyOverview.astro +1079 -0
  102. package/templates/shared/dashboard/src/components/usage/design-tokens.ts +187 -0
  103. package/templates/shared/dashboard/src/components/usage/filters/InlineDateRange.astro +285 -0
  104. package/templates/shared/dashboard/src/components/usage/filters/PeriodButtons.astro +157 -0
  105. package/templates/shared/dashboard/src/components/usage/filters/ProjectSelect.astro +284 -0
  106. package/templates/shared/dashboard/src/components/usage/react/DashboardShell.tsx +263 -0
  107. package/templates/shared/dashboard/src/components/usage/react/StatusBadge.tsx +77 -0
  108. package/templates/shared/dashboard/src/components/usage/react/UsageChart.tsx +391 -0
  109. package/templates/shared/dashboard/src/components/usage/react/index.ts.hbs +30 -0
  110. package/templates/shared/dashboard/src/components/usage/react/types.ts +137 -0
  111. package/templates/shared/dashboard/src/components/usage/scripts/ai-tab-controller.ts +419 -0
  112. package/templates/shared/dashboard/src/components/usage/scripts/constants.ts +60 -0
  113. package/templates/shared/dashboard/src/components/usage/scripts/formatters.ts +62 -0
  114. package/templates/shared/dashboard/src/components/usage/scripts/overview-controller.ts +1633 -0
  115. package/templates/shared/dashboard/src/components/usage/scripts/resource-table-builder.ts +294 -0
  116. package/templates/shared/dashboard/src/components/usage/scripts/tabs-filters-controller.ts +464 -0
  117. package/templates/shared/dashboard/src/components/usage/state/index.ts +55 -0
  118. package/templates/shared/dashboard/src/components/usage/state/usageActions.ts +439 -0
  119. package/templates/shared/dashboard/src/components/usage/state/usageStore.ts +376 -0
  120. package/templates/shared/dashboard/src/components/usage/transformers.ts +478 -0
  121. package/templates/shared/dashboard/src/components/usage/types.ts +283 -0
  122. package/templates/shared/dashboard/src/components/usage/unified/AlertBanner.tsx +172 -0
  123. package/templates/shared/dashboard/src/components/usage/unified/HeroCardsRow.tsx +757 -0
  124. package/templates/shared/dashboard/src/components/usage/unified/LiveHeader.tsx +169 -0
  125. package/templates/shared/dashboard/src/components/usage/unified/ProjectsTable.tsx +448 -0
  126. package/templates/shared/dashboard/src/components/usage/unified/ResourceBreakdown.tsx +236 -0
  127. package/templates/shared/dashboard/src/components/usage/unified/Sparkline.tsx +127 -0
  128. package/templates/shared/dashboard/src/components/usage/unified/UnifiedShell.tsx +893 -0
  129. package/templates/shared/dashboard/src/components/usage/unified/index.ts.hbs +50 -0
  130. package/templates/shared/dashboard/src/components/usage/unified/types.ts +416 -0
  131. package/templates/shared/dashboard/src/components/usage/usage-colors.ts +292 -0
  132. package/templates/shared/dashboard/src/lib/cloudflare/analytics.ts +310 -0
  133. package/templates/shared/dashboard/src/lib/cloudflare/d1.ts +55 -0
  134. package/templates/shared/dashboard/src/lib/cloudflare/index.ts.hbs +120 -0
  135. package/templates/shared/dashboard/src/lib/infrastructure/types.ts +116 -0
  136. package/templates/shared/dashboard/src/lib/usage/fetchWithDedup.ts +101 -0
  137. package/templates/shared/dashboard/src/lib/usage/index.ts.hbs +12 -0
  138. package/templates/shared/dashboard/src/pages/api/usage/ai-models.ts +235 -0
  139. package/templates/shared/dashboard/src/pages/api/usage/billing-context.ts +296 -0
  140. package/templates/shared/scripts/test-telemetry-flow.ts +464 -0
  141. package/templates/shared/tests/e2e/usage-api.test.ts +909 -0
  142. package/templates/shared/tests/e2e/usage-export.test.ts +784 -0
  143. package/templates/shared/tests/e2e/usage-mobile.test.ts +531 -0
  144. package/templates/shared/tests/helpers/mock-storage.ts +166 -0
  145. package/templates/shared/tests/integration/kv-cache.test.ts +252 -0
  146. package/templates/shared/tests/integration/platform-usage.test.ts +956 -0
  147. package/templates/shared/tests/unit/billing.test.ts +331 -0
  148. package/templates/shared/tests/unit/cloudflare/graphql.test.ts +217 -0
  149. package/templates/shared/tests/unit/components/usage-transformers.test.ts +473 -0
  150. package/templates/shared/tests/unit/control.test.ts +226 -0
  151. package/templates/shared/tests/unit/cost-calculator.test.ts +141 -0
  152. package/templates/shared/tests/unit/economics.test.ts +365 -0
  153. package/templates/shared/tests/unit/telemetry-sampling.test.ts +401 -0
  154. package/templates/standard/dashboard/src/components/errors/PriorityBadge.astro +27 -0
  155. package/templates/standard/dashboard/src/components/infrastructure/HealthchecksStatus.tsx +293 -0
  156. package/templates/standard/dashboard/src/components/infrastructure/InfrastructureTabs.tsx +268 -0
  157. package/templates/standard/dashboard/src/components/reports/CircuitBreakerReport.tsx +474 -0
  158. package/templates/standard/dashboard/src/components/reports/CostTrendsReport.tsx +229 -0
  159. package/templates/standard/dashboard/src/components/reports/ErrorTrendsReport.tsx +244 -0
  160. package/templates/standard/dashboard/src/components/reports/ProjectHealthTable.tsx +251 -0
  161. package/templates/standard/dashboard/src/components/reports/WarningDigestsTable.tsx +298 -0
  162. package/templates/standard/dashboard/src/components/usage/react/UsageTable.tsx +385 -0
  163. package/templates/standard/dashboard/src/components/usage/unified/CircuitBreakerEvents.tsx +305 -0
  164. package/templates/standard/dashboard/src/components/usage/unified/FeatureBudgets.tsx +472 -0
  165. package/templates/standard/dashboard/src/lib/errors/api.ts +84 -0
  166. package/templates/standard/dashboard/src/lib/errors/types.ts +75 -0
  167. package/templates/standard/dashboard/src/lib/infrastructure/api.ts +141 -0
  168. package/templates/standard/dashboard/src/lib/infrastructure/gatus.ts.hbs +112 -0
  169. package/templates/standard/dashboard/src/lib/services/proxy/index.ts +20 -0
  170. package/templates/standard/dashboard/src/lib/services/proxy/proxy.ts +244 -0
  171. package/templates/standard/dashboard/src/lib/services/proxy/types.ts +81 -0
  172. package/templates/standard/dashboard/src/pages/analytics.astro +64 -0
  173. package/templates/standard/dashboard/src/pages/api/infrastructure/alerts.ts +85 -0
  174. package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks/[id]/flips.ts +110 -0
  175. package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks.ts +101 -0
  176. package/templates/standard/dashboard/src/pages/api/infrastructure/uptime/[id]/response-times.ts +121 -0
  177. package/templates/standard/dashboard/src/pages/api/infrastructure/uptime.ts +89 -0
  178. package/templates/standard/dashboard/src/pages/api/test/service-auth.ts +178 -0
  179. package/templates/standard/tests/integration/connectors.test.ts +241 -0
  180. package/templates/standard/tests/integration/github-monitor.test.ts +143 -0
  181. package/templates/standard/tests/integration/ingestion.test.ts +211 -0
  182. package/templates/standard/tests/integration/platform-sentinel.test.ts +497 -0
  183. package/templates/standard/tests/unit/cloudflare/alerting.test.ts +480 -0
  184. package/templates/standard/tests/unit/error-collector/dedup.test.ts +350 -0
  185. package/templates/standard/tests/unit/error-collector/github.test.ts +187 -0
@@ -0,0 +1,651 @@
1
+ ---
2
+ /**
3
+ * ComparisonModeSelector Component (task-17.10, enhanced in task-17.18)
4
+ *
5
+ * Dropdown to select comparison mode for historical views.
6
+ * Options: Previous Period, Same Period Last Month, Custom Range.
7
+ *
8
+ * Task-17.18 Enhancements:
9
+ * - Date range presets (Last 7d, 30d, 90d, This month, Last month)
10
+ * - Inline validation feedback (no alerts)
11
+ * - Improved mobile styling
12
+ */
13
+
14
+ import type { CompareMode } from './types';
15
+
16
+ export interface Props {
17
+ currentMode: CompareMode;
18
+ currentPeriod: string;
19
+ customStartDate?: string;
20
+ customEndDate?: string;
21
+ }
22
+
23
+ const {
24
+ currentMode = 'none',
25
+ currentPeriod = '30d',
26
+ customStartDate = '',
27
+ customEndDate = '',
28
+ } = Astro.props;
29
+
30
+ // Mode labels
31
+ const modeLabels: Record<CompareMode, string> = {
32
+ none: 'No Comparison',
33
+ lastMonth: 'vs Same Period Last Month',
34
+ custom: 'Custom Date Range',
35
+ };
36
+
37
+ // Mode descriptions
38
+ const modeDescriptions: Record<CompareMode, string> = {
39
+ none: 'Show current period only',
40
+ lastMonth: 'Compare with the same period from last month',
41
+ custom: 'Select a custom date range to compare',
42
+ };
43
+
44
+ // Date presets for quick selection
45
+ const datePresets = [
46
+ { id: 'last7d', label: 'Last 7d', days: 7 },
47
+ { id: 'last30d', label: 'Last 30d', days: 30 },
48
+ { id: 'last90d', label: 'Last 90d', days: 90 },
49
+ { id: 'thisMonth', label: 'This month', days: 0 }, // Special handling
50
+ { id: 'lastMonth', label: 'Last month', days: -1 }, // Special handling
51
+ ];
52
+ ---
53
+
54
+ <div class="comparison-mode-selector" data-component="comparison-mode-selector">
55
+ <div class="selector-group">
56
+ <label for="compare-mode" class="selector-label">Compare</label>
57
+ <div class="select-wrapper">
58
+ <select
59
+ id="compare-mode"
60
+ name="compare"
61
+ class="selector-select"
62
+ aria-describedby="compare-mode-desc"
63
+ >
64
+ {
65
+ Object.entries(modeLabels).map(([mode, label]) => (
66
+ <option value={mode} selected={mode === currentMode}>
67
+ {label}
68
+ </option>
69
+ ))
70
+ }
71
+ </select>
72
+ <span class="select-icon" aria-hidden="true">
73
+ <svg
74
+ width="12"
75
+ height="12"
76
+ viewBox="0 0 12 12"
77
+ fill="none"
78
+ xmlns="http://www.w3.org/2000/svg"
79
+ >
80
+ <path
81
+ d="M3 4.5L6 7.5L9 4.5"
82
+ stroke="currentColor"
83
+ stroke-width="1.5"
84
+ stroke-linecap="round"
85
+ stroke-linejoin="round"></path>
86
+ </svg>
87
+ </span>
88
+ </div>
89
+ </div>
90
+
91
+ <!-- Custom date range inputs (hidden by default) -->
92
+ <div
93
+ id="custom-date-range"
94
+ class="custom-date-range"
95
+ style={currentMode !== 'custom' ? 'display: none;' : ''}
96
+ >
97
+ <!-- Date Range Presets (Task-17.18) -->
98
+ <div class="date-presets">
99
+ <span class="presets-label">Quick:</span>
100
+ <div class="presets-buttons">
101
+ {
102
+ datePresets.map((preset) => (
103
+ <button
104
+ type="button"
105
+ class="preset-button"
106
+ data-preset={preset.id}
107
+ data-days={preset.days}
108
+ >
109
+ {preset.label}
110
+ </button>
111
+ ))
112
+ }
113
+ </div>
114
+ </div>
115
+
116
+ <div class="date-inputs-row">
117
+ <div class="date-input-group">
118
+ <label for="custom-start-date" class="date-label">From</label>
119
+ <input
120
+ type="date"
121
+ id="custom-start-date"
122
+ name="startDate"
123
+ class="date-input"
124
+ value={customStartDate}
125
+ max={new Date().toISOString().split('T')[0]}
126
+ />
127
+ </div>
128
+ <div class="date-input-group">
129
+ <label for="custom-end-date" class="date-label">To</label>
130
+ <input
131
+ type="date"
132
+ id="custom-end-date"
133
+ name="endDate"
134
+ class="date-input"
135
+ value={customEndDate}
136
+ max={new Date().toISOString().split('T')[0]}
137
+ />
138
+ </div>
139
+ <button type="button" id="apply-custom-range" class="apply-button"> Apply </button>
140
+ </div>
141
+
142
+ <!-- Validation feedback (Task-17.18) -->
143
+ <div id="date-validation-feedback" class="validation-feedback" role="alert" aria-live="polite">
144
+ </div>
145
+ </div>
146
+
147
+ <p id="compare-mode-desc" class="selector-description">
148
+ {modeDescriptions[currentMode]}
149
+ </p>
150
+ </div>
151
+
152
+ <style>
153
+ .comparison-mode-selector {
154
+ display: flex;
155
+ flex-direction: column;
156
+ gap: 0.5rem;
157
+ }
158
+
159
+ .selector-group {
160
+ display: flex;
161
+ align-items: center;
162
+ gap: 0.5rem;
163
+ }
164
+
165
+ .selector-label {
166
+ font-size: 0.75rem;
167
+ font-weight: 500;
168
+ color: #6b7280;
169
+ text-transform: uppercase;
170
+ letter-spacing: 0.05em;
171
+ }
172
+
173
+ :global(.dark) .selector-label {
174
+ color: #9ca3af;
175
+ }
176
+
177
+ .select-wrapper {
178
+ position: relative;
179
+ display: inline-flex;
180
+ }
181
+
182
+ .selector-select {
183
+ appearance: none;
184
+ background-color: white;
185
+ border: 1px solid #d1d5db;
186
+ border-radius: 0.375rem;
187
+ padding: 0.5rem 2rem 0.5rem 0.75rem;
188
+ font-size: 0.875rem;
189
+ color: #374151;
190
+ cursor: pointer;
191
+ transition: all 0.15s ease;
192
+ min-width: 200px;
193
+ }
194
+
195
+ :global(.dark) .selector-select {
196
+ background-color: #1f2937;
197
+ border-color: #374151;
198
+ color: #f9fafb;
199
+ }
200
+
201
+ .selector-select:hover {
202
+ border-color: #9ca3af;
203
+ }
204
+
205
+ .selector-select:focus {
206
+ outline: none;
207
+ border-color: #3b82f6;
208
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
209
+ }
210
+
211
+ .select-icon {
212
+ position: absolute;
213
+ right: 0.75rem;
214
+ top: 50%;
215
+ transform: translateY(-50%);
216
+ pointer-events: none;
217
+ color: #6b7280;
218
+ }
219
+
220
+ .custom-date-range {
221
+ display: flex;
222
+ flex-direction: column;
223
+ gap: 0.75rem;
224
+ padding: 0.75rem;
225
+ background-color: #f9fafb;
226
+ border-radius: 0.375rem;
227
+ margin-top: 0.25rem;
228
+ }
229
+
230
+ :global(.dark) .custom-date-range {
231
+ background-color: #111827;
232
+ }
233
+
234
+ /* Date Presets (Task-17.18) */
235
+ .date-presets {
236
+ display: flex;
237
+ align-items: center;
238
+ gap: 0.5rem;
239
+ flex-wrap: wrap;
240
+ }
241
+
242
+ .presets-label {
243
+ font-size: 0.625rem;
244
+ font-weight: 500;
245
+ color: #6b7280;
246
+ text-transform: uppercase;
247
+ letter-spacing: 0.05em;
248
+ }
249
+
250
+ :global(.dark) .presets-label {
251
+ color: #9ca3af;
252
+ }
253
+
254
+ .presets-buttons {
255
+ display: flex;
256
+ gap: 0.25rem;
257
+ flex-wrap: wrap;
258
+ }
259
+
260
+ .preset-button {
261
+ padding: 0.25rem 0.5rem;
262
+ background-color: white;
263
+ border: 1px solid #e5e7eb;
264
+ border-radius: 0.25rem;
265
+ font-size: 0.6875rem;
266
+ color: #6b7280;
267
+ cursor: pointer;
268
+ transition: all 0.15s ease;
269
+ white-space: nowrap;
270
+ }
271
+
272
+ .preset-button:hover {
273
+ background-color: #f3f4f6;
274
+ border-color: #d1d5db;
275
+ color: #374151;
276
+ }
277
+
278
+ .preset-button:active,
279
+ .preset-button.active {
280
+ background-color: #3b82f6;
281
+ border-color: #3b82f6;
282
+ color: white;
283
+ }
284
+
285
+ :global(.dark) .preset-button {
286
+ background-color: #1f2937;
287
+ border-color: #374151;
288
+ color: #9ca3af;
289
+ }
290
+
291
+ :global(.dark) .preset-button:hover {
292
+ background-color: #374151;
293
+ color: #f9fafb;
294
+ }
295
+
296
+ :global(.dark) .preset-button:active,
297
+ :global(.dark) .preset-button.active {
298
+ background-color: #3b82f6;
299
+ border-color: #3b82f6;
300
+ color: white;
301
+ }
302
+
303
+ /* Date Inputs Row */
304
+ .date-inputs-row {
305
+ display: flex;
306
+ align-items: flex-end;
307
+ gap: 0.75rem;
308
+ }
309
+
310
+ .date-input-group {
311
+ display: flex;
312
+ flex-direction: column;
313
+ gap: 0.25rem;
314
+ }
315
+
316
+ .date-label {
317
+ font-size: 0.625rem;
318
+ font-weight: 500;
319
+ color: #6b7280;
320
+ text-transform: uppercase;
321
+ letter-spacing: 0.05em;
322
+ }
323
+
324
+ :global(.dark) .date-label {
325
+ color: #9ca3af;
326
+ }
327
+
328
+ .date-input {
329
+ appearance: none;
330
+ background-color: white;
331
+ border: 1px solid #d1d5db;
332
+ border-radius: 0.25rem;
333
+ padding: 0.375rem 0.5rem;
334
+ font-size: 0.75rem;
335
+ color: #374151;
336
+ min-width: 130px;
337
+ }
338
+
339
+ .date-input.invalid {
340
+ border-color: #ef4444;
341
+ }
342
+
343
+ :global(.dark) .date-input {
344
+ background-color: #1f2937;
345
+ border-color: #374151;
346
+ color: #f9fafb;
347
+ }
348
+
349
+ .date-input:focus {
350
+ outline: none;
351
+ border-color: #3b82f6;
352
+ }
353
+
354
+ .apply-button {
355
+ padding: 0.375rem 0.75rem;
356
+ background-color: #3b82f6;
357
+ color: white;
358
+ border: none;
359
+ border-radius: 0.25rem;
360
+ font-size: 0.75rem;
361
+ font-weight: 500;
362
+ cursor: pointer;
363
+ transition: background-color 0.15s ease;
364
+ white-space: nowrap;
365
+ }
366
+
367
+ .apply-button:hover {
368
+ background-color: #2563eb;
369
+ }
370
+
371
+ .apply-button:active {
372
+ background-color: #1d4ed8;
373
+ }
374
+
375
+ .apply-button:disabled {
376
+ background-color: #9ca3af;
377
+ cursor: not-allowed;
378
+ }
379
+
380
+ /* Validation Feedback (Task-17.18) */
381
+ .validation-feedback {
382
+ font-size: 0.6875rem;
383
+ padding: 0.375rem 0.5rem;
384
+ border-radius: 0.25rem;
385
+ display: none;
386
+ }
387
+
388
+ .validation-feedback.error {
389
+ display: block;
390
+ background-color: #fef2f2;
391
+ color: #b91c1c;
392
+ border: 1px solid #fecaca;
393
+ }
394
+
395
+ .validation-feedback.info {
396
+ display: block;
397
+ background-color: #eff6ff;
398
+ color: #1d4ed8;
399
+ border: 1px solid #bfdbfe;
400
+ }
401
+
402
+ :global(.dark) .validation-feedback.error {
403
+ background-color: #450a0a;
404
+ color: #fca5a5;
405
+ border-color: #7f1d1d;
406
+ }
407
+
408
+ :global(.dark) .validation-feedback.info {
409
+ background-color: #172554;
410
+ color: #93c5fd;
411
+ border-color: #1e40af;
412
+ }
413
+
414
+ .selector-description {
415
+ font-size: 0.625rem;
416
+ color: #9ca3af;
417
+ margin: 0;
418
+ }
419
+
420
+ /* Mobile responsive (Task-17.18 enhanced) */
421
+ @media (max-width: 640px) {
422
+ .comparison-mode-selector {
423
+ width: 100%;
424
+ }
425
+
426
+ .selector-select {
427
+ width: 100%;
428
+ min-width: unset;
429
+ }
430
+
431
+ .date-presets {
432
+ flex-direction: column;
433
+ align-items: flex-start;
434
+ }
435
+
436
+ .presets-buttons {
437
+ width: 100%;
438
+ }
439
+
440
+ .preset-button {
441
+ flex: 1;
442
+ text-align: center;
443
+ padding: 0.5rem 0.25rem;
444
+ }
445
+
446
+ .date-inputs-row {
447
+ flex-wrap: wrap;
448
+ }
449
+
450
+ .date-input-group {
451
+ flex: 1;
452
+ min-width: 45%;
453
+ }
454
+
455
+ .date-input {
456
+ width: 100%;
457
+ min-width: unset;
458
+ }
459
+
460
+ .apply-button {
461
+ width: 100%;
462
+ margin-top: 0.5rem;
463
+ padding: 0.5rem;
464
+ }
465
+ }
466
+ </style>
467
+
468
+ <script>
469
+ document.addEventListener('DOMContentLoaded', () => {
470
+ const selector = document.querySelector('[data-component="comparison-mode-selector"]');
471
+ if (!selector) return;
472
+
473
+ const modeSelect = selector.querySelector('#compare-mode') as HTMLSelectElement;
474
+ const customRange = selector.querySelector('#custom-date-range') as HTMLElement;
475
+ const startDateInput = selector.querySelector('#custom-start-date') as HTMLInputElement;
476
+ const endDateInput = selector.querySelector('#custom-end-date') as HTMLInputElement;
477
+ const applyButton = selector.querySelector('#apply-custom-range') as HTMLButtonElement;
478
+ const description = selector.querySelector('.selector-description') as HTMLElement;
479
+ const validationFeedback = selector.querySelector('#date-validation-feedback') as HTMLElement;
480
+ const presetButtons = selector.querySelectorAll('.preset-button');
481
+
482
+ const descriptions: Record<string, string> = {
483
+ none: 'Show current period only',
484
+ lastMonth: 'Compare with the same period from last month',
485
+ custom: 'Select a custom date range to compare',
486
+ };
487
+
488
+ // Helper: Format date as YYYY-MM-DD
489
+ function formatDate(date: Date): string {
490
+ return date.toISOString().split('T')[0];
491
+ }
492
+
493
+ // Helper: Show validation feedback
494
+ function showFeedback(message: string, type: 'error' | 'info') {
495
+ validationFeedback.textContent = message;
496
+ validationFeedback.className = 'validation-feedback ' + type;
497
+ }
498
+
499
+ // Helper: Hide validation feedback
500
+ function hideFeedback() {
501
+ validationFeedback.textContent = '';
502
+ validationFeedback.className = 'validation-feedback';
503
+ }
504
+
505
+ // Helper: Validate date range
506
+ function validateDateRange(): { valid: boolean; error?: string; days?: number } {
507
+ const startDate = startDateInput.value;
508
+ const endDate = endDateInput.value;
509
+
510
+ if (!startDate || !endDate) {
511
+ return { valid: false, error: 'Please select both start and end dates' };
512
+ }
513
+
514
+ const start = new Date(startDate);
515
+ const end = new Date(endDate);
516
+
517
+ if (start > end) {
518
+ startDateInput.classList.add('invalid');
519
+ return { valid: false, error: 'Start date must be before end date' };
520
+ }
521
+
522
+ const diffDays = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24));
523
+
524
+ if (diffDays > 90) {
525
+ return {
526
+ valid: false,
527
+ error: 'Maximum date range is 90 days. Selected: ' + diffDays + ' days',
528
+ };
529
+ }
530
+
531
+ startDateInput.classList.remove('invalid');
532
+ endDateInput.classList.remove('invalid');
533
+ return { valid: true, days: diffDays };
534
+ }
535
+
536
+ // Handle mode change
537
+ modeSelect.addEventListener('change', () => {
538
+ const mode = modeSelect.value;
539
+
540
+ // Toggle custom date range visibility
541
+ if (mode === 'custom') {
542
+ customRange.style.display = 'flex';
543
+ hideFeedback();
544
+ } else {
545
+ customRange.style.display = 'none';
546
+ // Update URL and trigger fetch for non-custom modes
547
+ updateUrlAndFetch(mode);
548
+ }
549
+
550
+ // Update description
551
+ description.textContent = descriptions[mode] || '';
552
+ });
553
+
554
+ // Handle preset button clicks (Task-17.18)
555
+ presetButtons.forEach((button) => {
556
+ button.addEventListener('click', () => {
557
+ const presetId = button.getAttribute('data-preset');
558
+ const days = parseInt(button.getAttribute('data-days') || '0', 10);
559
+ const today = new Date();
560
+ let startDate: Date;
561
+ let endDate: Date;
562
+
563
+ // Clear active state from all buttons
564
+ presetButtons.forEach((btn) => btn.classList.remove('active'));
565
+ button.classList.add('active');
566
+
567
+ if (presetId === 'thisMonth') {
568
+ // This month: 1st of current month to today
569
+ startDate = new Date(today.getFullYear(), today.getMonth(), 1);
570
+ endDate = today;
571
+ } else if (presetId === 'lastMonth') {
572
+ // Last month: 1st to last day of previous month
573
+ startDate = new Date(today.getFullYear(), today.getMonth() - 1, 1);
574
+ endDate = new Date(today.getFullYear(), today.getMonth(), 0);
575
+ } else {
576
+ // Last N days
577
+ endDate = today;
578
+ startDate = new Date(today);
579
+ startDate.setDate(startDate.getDate() - days);
580
+ }
581
+
582
+ startDateInput.value = formatDate(startDate);
583
+ endDateInput.value = formatDate(endDate);
584
+
585
+ // Validate and show info
586
+ const validation = validateDateRange();
587
+ if (validation.valid) {
588
+ showFeedback('Selected ' + validation.days + ' days', 'info');
589
+ }
590
+ });
591
+ });
592
+
593
+ // Real-time validation on date change (Task-17.18)
594
+ function handleDateChange() {
595
+ const validation = validateDateRange();
596
+ if (startDateInput.value && endDateInput.value) {
597
+ if (!validation.valid && validation.error) {
598
+ showFeedback(validation.error, 'error');
599
+ applyButton.disabled = true;
600
+ } else {
601
+ showFeedback('Selected ' + validation.days + ' days', 'info');
602
+ applyButton.disabled = false;
603
+ }
604
+ } else {
605
+ hideFeedback();
606
+ applyButton.disabled = false;
607
+ }
608
+ }
609
+
610
+ startDateInput.addEventListener('change', handleDateChange);
611
+ endDateInput.addEventListener('change', handleDateChange);
612
+
613
+ // Handle apply button click
614
+ applyButton.addEventListener('click', () => {
615
+ const validation = validateDateRange();
616
+
617
+ if (!validation.valid) {
618
+ if (validation.error) {
619
+ showFeedback(validation.error, 'error');
620
+ }
621
+ return;
622
+ }
623
+
624
+ hideFeedback();
625
+ updateUrlAndFetch('custom', startDateInput.value, endDateInput.value);
626
+ });
627
+
628
+ function updateUrlAndFetch(mode: string, startDate?: string, endDate?: string) {
629
+ const url = new URL(window.location.href);
630
+ url.searchParams.set('compare', mode);
631
+
632
+ if (mode === 'custom' && startDate && endDate) {
633
+ url.searchParams.set('startDate', startDate);
634
+ url.searchParams.set('endDate', endDate);
635
+ } else {
636
+ url.searchParams.delete('startDate');
637
+ url.searchParams.delete('endDate');
638
+ }
639
+
640
+ // Update URL without reload
641
+ window.history.pushState({}, '', url);
642
+
643
+ // Dispatch custom event for parent to handle
644
+ const event = new CustomEvent('compare-mode-change', {
645
+ bubbles: true,
646
+ detail: { mode, startDate, endDate },
647
+ });
648
+ selector.dispatchEvent(event);
649
+ }
650
+ });
651
+ </script>