@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,1033 @@
1
+ ---
2
+ /**
3
+ * TableFilters.astro
4
+ *
5
+ * Enhanced filtering controls for the usage dashboard.
6
+ * Features: search, resource type multi-select, project filter, status filter,
7
+ * sort controls, and URL parameter persistence.
8
+ */
9
+
10
+ interface Props {
11
+ projects?: string[];
12
+ resourceTypes?: string[];
13
+ initialFilters?: {
14
+ search?: string;
15
+ types?: string[];
16
+ project?: string;
17
+ status?: string;
18
+ sort?: string;
19
+ sortDir?: 'asc' | 'desc';
20
+ };
21
+ }
22
+
23
+ const {
24
+ projects = [],
25
+ resourceTypes = [
26
+ 'Workers',
27
+ 'D1',
28
+ 'KV',
29
+ 'R2',
30
+ 'Pages',
31
+ 'Vectorize',
32
+ 'AI Gateway',
33
+ 'Queues',
34
+ 'Workflows',
35
+ 'Durable Objects',
36
+ ],
37
+ initialFilters = {},
38
+ } = Astro.props;
39
+
40
+ // Parse URL params for initial state
41
+ const url = new URL(Astro.request.url);
42
+ const urlSearch = url.searchParams.get('search') || initialFilters.search || '';
43
+ const urlTypes =
44
+ url.searchParams.get('types')?.split(',').filter(Boolean) || initialFilters.types || [];
45
+ const urlProject = url.searchParams.get('project') || initialFilters.project || '';
46
+ const urlStatus = url.searchParams.get('status') || initialFilters.status || '';
47
+ const urlSort = url.searchParams.get('sort') || initialFilters.sort || 'cost';
48
+ const urlSortDir =
49
+ (url.searchParams.get('sortDir') as 'asc' | 'desc') || initialFilters.sortDir || 'desc';
50
+ ---
51
+
52
+ <div class="table-filters" data-filters>
53
+ <!-- Search Input -->
54
+ <div class="filter-group search-group">
55
+ <label for="filter-search" class="filter-label">Search</label>
56
+ <div class="search-input-wrapper">
57
+ <svg class="search-icon" width="16" height="16" viewBox="0 0 16 16" fill="none">
58
+ <path
59
+ d="M7 12A5 5 0 1 0 7 2a5 5 0 0 0 0 10zM14 14l-3.5-3.5"
60
+ stroke="currentColor"
61
+ stroke-width="1.5"
62
+ stroke-linecap="round"
63
+ stroke-linejoin="round"></path>
64
+ </svg>
65
+ <input
66
+ type="text"
67
+ id="filter-search"
68
+ class="filter-input search-input"
69
+ placeholder="Search resources..."
70
+ value={urlSearch}
71
+ data-filter-search
72
+ />
73
+ <button
74
+ type="button"
75
+ class="clear-search"
76
+ data-clear-search
77
+ aria-label="Clear search"
78
+ style={urlSearch ? '' : 'display: none'}
79
+ >
80
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
81
+ <path
82
+ d="M10.5 3.5L3.5 10.5M3.5 3.5l7 7"
83
+ stroke="currentColor"
84
+ stroke-width="1.5"
85
+ stroke-linecap="round"></path>
86
+ </svg>
87
+ </button>
88
+ </div>
89
+ </div>
90
+
91
+ <!-- Resource Type Multi-Select -->
92
+ <div class="filter-group">
93
+ <label class="filter-label">Resource Types</label>
94
+ <div class="multi-select" data-multi-select="types">
95
+ <button type="button" class="multi-select-trigger" data-trigger>
96
+ <span class="multi-select-value" data-value>
97
+ {
98
+ urlTypes.length === 0
99
+ ? 'All Types'
100
+ : urlTypes.length === 1
101
+ ? urlTypes[0]
102
+ : `${urlTypes.length} selected`
103
+ }
104
+ </span>
105
+ <svg class="dropdown-chevron" width="12" height="12" viewBox="0 0 12 12" fill="none">
106
+ <path
107
+ d="M3 4.5l3 3 3-3"
108
+ stroke="currentColor"
109
+ stroke-width="1.5"
110
+ stroke-linecap="round"
111
+ stroke-linejoin="round"></path>
112
+ </svg>
113
+ </button>
114
+ <div class="multi-select-dropdown" data-dropdown>
115
+ <div class="dropdown-header">
116
+ <button type="button" class="select-action" data-select-all>Select All</button>
117
+ <button type="button" class="select-action" data-clear-all>Clear</button>
118
+ </div>
119
+ <div class="dropdown-options">
120
+ {
121
+ resourceTypes.map((type) => (
122
+ <label class="dropdown-option">
123
+ <input
124
+ type="checkbox"
125
+ value={type}
126
+ checked={urlTypes.includes(type)}
127
+ data-type-checkbox
128
+ />
129
+ <span class="checkbox-custom" />
130
+ <span class="option-label">{type}</span>
131
+ </label>
132
+ ))
133
+ }
134
+ </div>
135
+ </div>
136
+ </div>
137
+ </div>
138
+
139
+ <!-- Project Filter -->
140
+ <div class="filter-group">
141
+ <label for="filter-project" class="filter-label">Project</label>
142
+ <div class="select-wrapper">
143
+ <select id="filter-project" class="filter-select" data-filter-project>
144
+ <option value="">All Projects</option>
145
+ {
146
+ projects.map((project) => (
147
+ <option value={project} selected={urlProject === project}>
148
+ {project}
149
+ </option>
150
+ ))
151
+ }
152
+ </select>
153
+ <svg class="select-chevron" width="12" height="12" viewBox="0 0 12 12" fill="none">
154
+ <path
155
+ d="M3 4.5l3 3 3-3"
156
+ stroke="currentColor"
157
+ stroke-width="1.5"
158
+ stroke-linecap="round"
159
+ stroke-linejoin="round"></path>
160
+ </svg>
161
+ </div>
162
+ </div>
163
+
164
+ <!-- Status Filter -->
165
+ <div class="filter-group">
166
+ <label for="filter-status" class="filter-label">Status</label>
167
+ <div class="status-buttons" data-status-filter>
168
+ <button type="button" class={`status-btn ${urlStatus === '' ? 'active' : ''}`} data-status=""
169
+ >All</button
170
+ >
171
+ <button
172
+ type="button"
173
+ class={`status-btn status-ok ${urlStatus === 'ok' ? 'active' : ''}`}
174
+ data-status="ok"
175
+ >
176
+ <span class="status-dot ok"></span>
177
+ OK
178
+ </button>
179
+ <button
180
+ type="button"
181
+ class={`status-btn status-warning ${urlStatus === 'warning' ? 'active' : ''}`}
182
+ data-status="warning"
183
+ >
184
+ <span class="status-dot warning"></span>
185
+ Warning
186
+ </button>
187
+ <button
188
+ type="button"
189
+ class={`status-btn status-critical ${urlStatus === 'critical' ? 'active' : ''}`}
190
+ data-status="critical"
191
+ >
192
+ <span class="status-dot critical"></span>
193
+ Critical
194
+ </button>
195
+ </div>
196
+ </div>
197
+
198
+ <!-- Sort Controls -->
199
+ <div class="filter-group sort-group">
200
+ <label for="filter-sort" class="filter-label">Sort By</label>
201
+ <div class="sort-controls">
202
+ <div class="select-wrapper">
203
+ <select id="filter-sort" class="filter-select" data-filter-sort>
204
+ <option value="cost" selected={urlSort === 'cost'}>Cost</option>
205
+ <option value="name" selected={urlSort === 'name'}>Name</option>
206
+ <option value="usage" selected={urlSort === 'usage'}>Usage</option>
207
+ <option value="requests" selected={urlSort === 'requests'}>Requests</option>
208
+ <option value="status" selected={urlSort === 'status'}>Status</option>
209
+ </select>
210
+ <svg class="select-chevron" width="12" height="12" viewBox="0 0 12 12" fill="none">
211
+ <path
212
+ d="M3 4.5l3 3 3-3"
213
+ stroke="currentColor"
214
+ stroke-width="1.5"
215
+ stroke-linecap="round"
216
+ stroke-linejoin="round"></path>
217
+ </svg>
218
+ </div>
219
+ <button
220
+ type="button"
221
+ class="sort-direction"
222
+ data-sort-direction={urlSortDir}
223
+ aria-label={`Sort ${urlSortDir === 'asc' ? 'ascending' : 'descending'}`}
224
+ >
225
+ <!-- Ascending arrow (shown when sortDir is asc) -->
226
+ <svg
227
+ class="sort-arrow sort-asc"
228
+ width="14"
229
+ height="14"
230
+ viewBox="0 0 14 14"
231
+ fill="none"
232
+ style={urlSortDir === 'asc' ? '' : 'display: none'}
233
+ >
234
+ <path
235
+ d="M7 11V3M7 3l3 3M7 3 4 6"
236
+ stroke="currentColor"
237
+ stroke-width="1.5"
238
+ stroke-linecap="round"
239
+ stroke-linejoin="round"></path>
240
+ </svg>
241
+ <!-- Descending arrow (shown when sortDir is desc) -->
242
+ <svg
243
+ class="sort-arrow sort-desc"
244
+ width="14"
245
+ height="14"
246
+ viewBox="0 0 14 14"
247
+ fill="none"
248
+ style={urlSortDir === 'desc' ? '' : 'display: none'}
249
+ >
250
+ <path
251
+ d="M7 3v8M7 11l3-3M7 11 4 8"
252
+ stroke="currentColor"
253
+ stroke-width="1.5"
254
+ stroke-linecap="round"
255
+ stroke-linejoin="round"></path>
256
+ </svg>
257
+ </button>
258
+ </div>
259
+ </div>
260
+
261
+ <!-- Clear All -->
262
+ <div class="filter-group clear-group">
263
+ <button type="button" class="clear-all-btn" data-clear-all-filters>
264
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
265
+ <path
266
+ d="M10.5 3.5L3.5 10.5M3.5 3.5l7 7"
267
+ stroke="currentColor"
268
+ stroke-width="1.5"
269
+ stroke-linecap="round"></path>
270
+ </svg>
271
+ Clear All
272
+ </button>
273
+ </div>
274
+
275
+ <!-- Active Filters Display -->
276
+ <div class="active-filters" data-active-filters style="display: none">
277
+ <span class="active-filters-label">Active:</span>
278
+ <div class="filter-tags" data-filter-tags></div>
279
+ </div>
280
+ </div>
281
+
282
+ <style>
283
+ .table-filters {
284
+ display: flex;
285
+ flex-wrap: wrap;
286
+ gap: var(--usage-spacing-md, 1rem);
287
+ align-items: flex-end;
288
+ padding: var(--usage-spacing-md, 1rem);
289
+ background: var(--usage-bg-secondary);
290
+ border: 1px solid var(--usage-border-default);
291
+ border-radius: var(--usage-radius-lg, 8px);
292
+ margin-bottom: var(--usage-spacing-lg, 1.5rem);
293
+ }
294
+
295
+ .filter-group {
296
+ display: flex;
297
+ flex-direction: column;
298
+ gap: var(--usage-spacing-xs, 0.25rem);
299
+ }
300
+
301
+ .filter-label {
302
+ font-family: var(--usage-font-sans);
303
+ font-size: 0.75rem;
304
+ font-weight: 500;
305
+ color: var(--usage-text-secondary);
306
+ text-transform: uppercase;
307
+ letter-spacing: 0.05em;
308
+ }
309
+
310
+ /* Search Input */
311
+ .search-group {
312
+ flex: 1;
313
+ min-width: 200px;
314
+ max-width: 320px;
315
+ }
316
+
317
+ .search-input-wrapper {
318
+ position: relative;
319
+ display: flex;
320
+ align-items: center;
321
+ }
322
+
323
+ .search-icon {
324
+ position: absolute;
325
+ left: 0.75rem;
326
+ color: var(--usage-text-muted);
327
+ pointer-events: none;
328
+ }
329
+
330
+ .search-input {
331
+ width: 100%;
332
+ padding: 0.5rem 2rem 0.5rem 2.25rem;
333
+ font-family: var(--usage-font-sans);
334
+ font-size: 0.875rem;
335
+ color: var(--usage-text-primary);
336
+ background: var(--usage-bg-primary);
337
+ border: 1px solid var(--usage-border-default);
338
+ border-radius: var(--usage-radius-md, 6px);
339
+ transition:
340
+ border-color var(--usage-transition-fast),
341
+ box-shadow var(--usage-transition-fast);
342
+ }
343
+
344
+ .search-input:focus {
345
+ outline: none;
346
+ border-color: var(--usage-accent-blue);
347
+ box-shadow: 0 0 0 3px rgba(9, 105, 218, 0.15);
348
+ }
349
+
350
+ .search-input::placeholder {
351
+ color: var(--usage-text-muted);
352
+ }
353
+
354
+ .clear-search {
355
+ position: absolute;
356
+ right: 0.5rem;
357
+ padding: 0.25rem;
358
+ background: none;
359
+ border: none;
360
+ color: var(--usage-text-muted);
361
+ cursor: pointer;
362
+ border-radius: var(--usage-radius-sm, 4px);
363
+ transition:
364
+ color var(--usage-transition-fast),
365
+ background var(--usage-transition-fast);
366
+ }
367
+
368
+ .clear-search:hover {
369
+ color: var(--usage-text-primary);
370
+ background: var(--usage-bg-hover);
371
+ }
372
+
373
+ /* Multi-Select Dropdown */
374
+ .multi-select {
375
+ position: relative;
376
+ }
377
+
378
+ .multi-select-trigger {
379
+ display: flex;
380
+ align-items: center;
381
+ justify-content: space-between;
382
+ gap: 0.5rem;
383
+ min-width: 140px;
384
+ padding: 0.5rem 0.75rem;
385
+ font-family: var(--usage-font-sans);
386
+ font-size: 0.875rem;
387
+ color: var(--usage-text-primary);
388
+ background: var(--usage-bg-primary);
389
+ border: 1px solid var(--usage-border-default);
390
+ border-radius: var(--usage-radius-md, 6px);
391
+ cursor: pointer;
392
+ transition: border-color var(--usage-transition-fast);
393
+ }
394
+
395
+ .multi-select-trigger:hover {
396
+ border-color: var(--usage-border-hover);
397
+ }
398
+
399
+ .multi-select-trigger:focus {
400
+ outline: none;
401
+ border-color: var(--usage-accent-blue);
402
+ box-shadow: 0 0 0 3px rgba(9, 105, 218, 0.15);
403
+ }
404
+
405
+ .dropdown-chevron {
406
+ color: var(--usage-text-muted);
407
+ transition: transform var(--usage-transition-fast);
408
+ }
409
+
410
+ .multi-select.open .dropdown-chevron {
411
+ transform: rotate(180deg);
412
+ }
413
+
414
+ .multi-select-dropdown {
415
+ position: absolute;
416
+ top: calc(100% + 4px);
417
+ left: 0;
418
+ z-index: 100;
419
+ min-width: 200px;
420
+ background: var(--usage-bg-primary);
421
+ border: 1px solid var(--usage-border-default);
422
+ border-radius: var(--usage-radius-md, 6px);
423
+ box-shadow: var(--usage-shadow-lg);
424
+ opacity: 0;
425
+ visibility: hidden;
426
+ transform: translateY(-8px);
427
+ transition:
428
+ opacity var(--usage-transition-fast),
429
+ transform var(--usage-transition-fast),
430
+ visibility var(--usage-transition-fast);
431
+ }
432
+
433
+ .multi-select.open .multi-select-dropdown {
434
+ opacity: 1;
435
+ visibility: visible;
436
+ transform: translateY(0);
437
+ }
438
+
439
+ .dropdown-header {
440
+ display: flex;
441
+ justify-content: space-between;
442
+ padding: 0.5rem 0.75rem;
443
+ border-bottom: 1px solid var(--usage-border-subtle);
444
+ }
445
+
446
+ .select-action {
447
+ font-family: var(--usage-font-sans);
448
+ font-size: 0.75rem;
449
+ color: var(--usage-accent-blue);
450
+ background: none;
451
+ border: none;
452
+ cursor: pointer;
453
+ padding: 0;
454
+ }
455
+
456
+ .select-action:hover {
457
+ text-decoration: underline;
458
+ }
459
+
460
+ .dropdown-options {
461
+ max-height: 240px;
462
+ overflow-y: auto;
463
+ padding: 0.5rem 0;
464
+ }
465
+
466
+ .dropdown-option {
467
+ display: flex;
468
+ align-items: center;
469
+ gap: 0.5rem;
470
+ padding: 0.5rem 0.75rem;
471
+ cursor: pointer;
472
+ transition: background var(--usage-transition-fast);
473
+ }
474
+
475
+ .dropdown-option:hover {
476
+ background: var(--usage-bg-hover);
477
+ }
478
+
479
+ .dropdown-option input[type='checkbox'] {
480
+ position: absolute;
481
+ opacity: 0;
482
+ width: 0;
483
+ height: 0;
484
+ }
485
+
486
+ .checkbox-custom {
487
+ width: 16px;
488
+ height: 16px;
489
+ border: 1.5px solid var(--usage-border-default);
490
+ border-radius: 3px;
491
+ background: var(--usage-bg-primary);
492
+ transition: all var(--usage-transition-fast);
493
+ position: relative;
494
+ }
495
+
496
+ .dropdown-option input:checked + .checkbox-custom {
497
+ background: var(--usage-accent-blue);
498
+ border-color: var(--usage-accent-blue);
499
+ }
500
+
501
+ .dropdown-option input:checked + .checkbox-custom::after {
502
+ content: '';
503
+ position: absolute;
504
+ left: 4px;
505
+ top: 1px;
506
+ width: 5px;
507
+ height: 9px;
508
+ border: solid white;
509
+ border-width: 0 2px 2px 0;
510
+ transform: rotate(45deg);
511
+ }
512
+
513
+ .option-label {
514
+ font-family: var(--usage-font-sans);
515
+ font-size: 0.875rem;
516
+ color: var(--usage-text-primary);
517
+ }
518
+
519
+ /* Select Wrapper */
520
+ .select-wrapper {
521
+ position: relative;
522
+ display: inline-flex;
523
+ align-items: center;
524
+ }
525
+
526
+ .filter-select {
527
+ appearance: none;
528
+ padding: 0.5rem 2rem 0.5rem 0.75rem;
529
+ font-family: var(--usage-font-sans);
530
+ font-size: 0.875rem;
531
+ color: var(--usage-text-primary);
532
+ background: var(--usage-bg-primary);
533
+ border: 1px solid var(--usage-border-default);
534
+ border-radius: var(--usage-radius-md, 6px);
535
+ cursor: pointer;
536
+ transition: border-color var(--usage-transition-fast);
537
+ }
538
+
539
+ .filter-select:hover {
540
+ border-color: var(--usage-border-hover);
541
+ }
542
+
543
+ .filter-select:focus {
544
+ outline: none;
545
+ border-color: var(--usage-accent-blue);
546
+ box-shadow: 0 0 0 3px rgba(9, 105, 218, 0.15);
547
+ }
548
+
549
+ .select-chevron {
550
+ position: absolute;
551
+ right: 0.75rem;
552
+ color: var(--usage-text-muted);
553
+ pointer-events: none;
554
+ }
555
+
556
+ /* Status Buttons */
557
+ .status-buttons {
558
+ display: flex;
559
+ gap: 0.25rem;
560
+ background: var(--usage-bg-tertiary);
561
+ padding: 0.25rem;
562
+ border-radius: var(--usage-radius-md, 6px);
563
+ }
564
+
565
+ .status-btn {
566
+ display: flex;
567
+ align-items: center;
568
+ gap: 0.375rem;
569
+ padding: 0.375rem 0.625rem;
570
+ font-family: var(--usage-font-sans);
571
+ font-size: 0.8125rem;
572
+ color: var(--usage-text-secondary);
573
+ background: transparent;
574
+ border: none;
575
+ border-radius: var(--usage-radius-sm, 4px);
576
+ cursor: pointer;
577
+ transition: all var(--usage-transition-fast);
578
+ }
579
+
580
+ .status-btn:hover {
581
+ color: var(--usage-text-primary);
582
+ background: var(--usage-bg-hover);
583
+ }
584
+
585
+ .status-btn.active {
586
+ color: var(--usage-text-primary);
587
+ background: var(--usage-bg-primary);
588
+ box-shadow: var(--usage-shadow-sm);
589
+ }
590
+
591
+ .status-dot {
592
+ width: 8px;
593
+ height: 8px;
594
+ border-radius: 50%;
595
+ }
596
+
597
+ .status-dot.ok {
598
+ background: var(--usage-status-ok);
599
+ }
600
+
601
+ .status-dot.warning {
602
+ background: var(--usage-status-warning);
603
+ }
604
+
605
+ .status-dot.critical {
606
+ background: var(--usage-status-critical);
607
+ }
608
+
609
+ /* Sort Controls */
610
+ .sort-controls {
611
+ display: flex;
612
+ gap: 0.25rem;
613
+ }
614
+
615
+ .sort-direction {
616
+ display: flex;
617
+ align-items: center;
618
+ justify-content: center;
619
+ width: 34px;
620
+ height: 34px;
621
+ background: var(--usage-bg-primary);
622
+ border: 1px solid var(--usage-border-default);
623
+ border-radius: var(--usage-radius-md, 6px);
624
+ color: var(--usage-text-secondary);
625
+ cursor: pointer;
626
+ transition: all var(--usage-transition-fast);
627
+ }
628
+
629
+ .sort-direction:hover {
630
+ border-color: var(--usage-border-hover);
631
+ color: var(--usage-text-primary);
632
+ }
633
+
634
+ .sort-direction:focus {
635
+ outline: none;
636
+ border-color: var(--usage-accent-blue);
637
+ box-shadow: 0 0 0 3px rgba(9, 105, 218, 0.15);
638
+ }
639
+
640
+ /* Clear All Button */
641
+ .clear-group {
642
+ margin-left: auto;
643
+ }
644
+
645
+ .clear-all-btn {
646
+ display: flex;
647
+ align-items: center;
648
+ gap: 0.375rem;
649
+ padding: 0.5rem 0.75rem;
650
+ font-family: var(--usage-font-sans);
651
+ font-size: 0.875rem;
652
+ color: var(--usage-text-secondary);
653
+ background: transparent;
654
+ border: 1px solid transparent;
655
+ border-radius: var(--usage-radius-md, 6px);
656
+ cursor: pointer;
657
+ transition: all var(--usage-transition-fast);
658
+ }
659
+
660
+ .clear-all-btn:hover {
661
+ color: var(--usage-accent-red);
662
+ background: var(--usage-status-critical-bg);
663
+ border-color: var(--usage-accent-red);
664
+ }
665
+
666
+ /* Active Filters */
667
+ .active-filters {
668
+ width: 100%;
669
+ display: flex;
670
+ align-items: center;
671
+ gap: 0.5rem;
672
+ padding-top: var(--usage-spacing-sm, 0.5rem);
673
+ border-top: 1px solid var(--usage-border-subtle);
674
+ margin-top: var(--usage-spacing-xs, 0.25rem);
675
+ }
676
+
677
+ .active-filters-label {
678
+ font-family: var(--usage-font-sans);
679
+ font-size: 0.75rem;
680
+ color: var(--usage-text-muted);
681
+ }
682
+
683
+ .filter-tags {
684
+ display: flex;
685
+ flex-wrap: wrap;
686
+ gap: 0.375rem;
687
+ }
688
+
689
+ .filter-tag {
690
+ display: inline-flex;
691
+ align-items: center;
692
+ gap: 0.25rem;
693
+ padding: 0.25rem 0.5rem;
694
+ font-family: var(--usage-font-sans);
695
+ font-size: 0.75rem;
696
+ color: var(--usage-accent-blue);
697
+ background: rgba(9, 105, 218, 0.1);
698
+ border-radius: var(--usage-radius-sm, 4px);
699
+ }
700
+
701
+ .filter-tag button {
702
+ display: flex;
703
+ padding: 0;
704
+ background: none;
705
+ border: none;
706
+ color: inherit;
707
+ cursor: pointer;
708
+ opacity: 0.7;
709
+ transition: opacity var(--usage-transition-fast);
710
+ }
711
+
712
+ .filter-tag button:hover {
713
+ opacity: 1;
714
+ }
715
+
716
+ /* Responsive */
717
+ @media (max-width: 768px) {
718
+ .table-filters {
719
+ flex-direction: column;
720
+ align-items: stretch;
721
+ }
722
+
723
+ .filter-group {
724
+ width: 100%;
725
+ }
726
+
727
+ .search-group {
728
+ max-width: none;
729
+ }
730
+
731
+ .clear-group {
732
+ margin-left: 0;
733
+ }
734
+
735
+ .status-buttons {
736
+ flex-wrap: wrap;
737
+ }
738
+ }
739
+ </style>
740
+
741
+ <script>
742
+ // Initialize filters
743
+ const filtersContainer = document.querySelector('[data-filters]') as HTMLElement;
744
+ if (filtersContainer) {
745
+ // State
746
+ let debounceTimer: ReturnType<typeof setTimeout>;
747
+
748
+ // Elements
749
+ const searchInput = filtersContainer.querySelector('[data-filter-search]') as HTMLInputElement;
750
+ const clearSearchBtn = filtersContainer.querySelector(
751
+ '[data-clear-search]'
752
+ ) as HTMLButtonElement;
753
+ const multiSelect = filtersContainer.querySelector(
754
+ '[data-multi-select="types"]'
755
+ ) as HTMLElement;
756
+ const multiSelectTrigger = multiSelect?.querySelector('[data-trigger]') as HTMLButtonElement;
757
+ const multiSelectDropdown = multiSelect?.querySelector('[data-dropdown]') as HTMLElement;
758
+ const multiSelectValue = multiSelect?.querySelector('[data-value]') as HTMLElement;
759
+ const typeCheckboxes = filtersContainer.querySelectorAll(
760
+ '[data-type-checkbox]'
761
+ ) as NodeListOf<HTMLInputElement>;
762
+ const selectAllBtn = multiSelect?.querySelector('[data-select-all]') as HTMLButtonElement;
763
+ const clearAllTypesBtn = multiSelect?.querySelector('[data-clear-all]') as HTMLButtonElement;
764
+ const projectSelect = filtersContainer.querySelector(
765
+ '[data-filter-project]'
766
+ ) as HTMLSelectElement;
767
+ const statusButtons = filtersContainer.querySelectorAll(
768
+ '[data-status]'
769
+ ) as NodeListOf<HTMLButtonElement>;
770
+ const sortSelect = filtersContainer.querySelector('[data-filter-sort]') as HTMLSelectElement;
771
+ const sortDirBtn = filtersContainer.querySelector('[data-sort-direction]') as HTMLButtonElement;
772
+ const clearAllFiltersBtn = filtersContainer.querySelector(
773
+ '[data-clear-all-filters]'
774
+ ) as HTMLButtonElement;
775
+ const activeFiltersContainer = filtersContainer.querySelector(
776
+ '[data-active-filters]'
777
+ ) as HTMLElement;
778
+ const filterTagsContainer = filtersContainer.querySelector('[data-filter-tags]') as HTMLElement;
779
+
780
+ // Get current filter state
781
+ function getFilters() {
782
+ const selectedTypes: string[] = [];
783
+ typeCheckboxes.forEach((cb) => {
784
+ if (cb.checked) selectedTypes.push(cb.value);
785
+ });
786
+
787
+ const activeStatus = filtersContainer.querySelector(
788
+ '.status-btn.active'
789
+ ) as HTMLButtonElement;
790
+
791
+ return {
792
+ search: searchInput?.value || '',
793
+ types: selectedTypes,
794
+ project: projectSelect?.value || '',
795
+ status: activeStatus?.dataset.status || '',
796
+ sort: sortSelect?.value || 'cost',
797
+ sortDir: (sortDirBtn?.dataset.sortDirection || 'desc') as 'asc' | 'desc',
798
+ };
799
+ }
800
+
801
+ // Update URL params
802
+ function updateURL(filters: ReturnType<typeof getFilters>) {
803
+ const url = new URL(window.location.href);
804
+
805
+ if (filters.search) url.searchParams.set('search', filters.search);
806
+ else url.searchParams.delete('search');
807
+
808
+ if (filters.types.length > 0) url.searchParams.set('types', filters.types.join(','));
809
+ else url.searchParams.delete('types');
810
+
811
+ if (filters.project) url.searchParams.set('project', filters.project);
812
+ else url.searchParams.delete('project');
813
+
814
+ if (filters.status) url.searchParams.set('status', filters.status);
815
+ else url.searchParams.delete('status');
816
+
817
+ if (filters.sort !== 'cost') url.searchParams.set('sort', filters.sort);
818
+ else url.searchParams.delete('sort');
819
+
820
+ if (filters.sortDir !== 'desc') url.searchParams.set('sortDir', filters.sortDir);
821
+ else url.searchParams.delete('sortDir');
822
+
823
+ window.history.replaceState({}, '', url.toString());
824
+ }
825
+
826
+ // Dispatch filter change event
827
+ function emitFilterChange() {
828
+ const filters = getFilters();
829
+ updateURL(filters);
830
+ updateActiveFilters(filters);
831
+
832
+ window.dispatchEvent(
833
+ new CustomEvent('usage:filters-changed', {
834
+ detail: filters,
835
+ })
836
+ );
837
+ }
838
+
839
+ // Update active filters display
840
+ function updateActiveFilters(filters: ReturnType<typeof getFilters>) {
841
+ const tags: string[] = [];
842
+
843
+ if (filters.search) tags.push(`Search: "${filters.search}"`);
844
+ if (filters.types.length > 0 && filters.types.length < typeCheckboxes.length) {
845
+ tags.push(`Types: ${filters.types.join(', ')}`);
846
+ }
847
+ if (filters.project) tags.push(`Project: ${filters.project}`);
848
+ if (filters.status) tags.push(`Status: ${filters.status}`);
849
+
850
+ if (tags.length > 0) {
851
+ activeFiltersContainer.style.display = '';
852
+ filterTagsContainer.textContent = ''; // Clear existing
853
+
854
+ tags.forEach((tag) => {
855
+ const tagEl = document.createElement('span');
856
+ tagEl.className = 'filter-tag';
857
+
858
+ const textSpan = document.createElement('span');
859
+ textSpan.textContent = tag;
860
+ tagEl.appendChild(textSpan);
861
+
862
+ const removeBtn = document.createElement('button');
863
+ removeBtn.type = 'button';
864
+ removeBtn.setAttribute('aria-label', `Remove ${tag}`);
865
+
866
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
867
+ svg.setAttribute('width', '10');
868
+ svg.setAttribute('height', '10');
869
+ svg.setAttribute('viewBox', '0 0 10 10');
870
+ svg.setAttribute('fill', 'none');
871
+
872
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
873
+ path.setAttribute('d', 'M7.5 2.5L2.5 7.5M2.5 2.5l5 5');
874
+ path.setAttribute('stroke', 'currentColor');
875
+ path.setAttribute('stroke-width', '1.5');
876
+ path.setAttribute('stroke-linecap', 'round');
877
+
878
+ svg.appendChild(path);
879
+ removeBtn.appendChild(svg);
880
+ tagEl.appendChild(removeBtn);
881
+
882
+ filterTagsContainer.appendChild(tagEl);
883
+ });
884
+ } else {
885
+ activeFiltersContainer.style.display = 'none';
886
+ }
887
+ }
888
+
889
+ // Update multi-select display text
890
+ function updateMultiSelectValue() {
891
+ const selected: string[] = [];
892
+ typeCheckboxes.forEach((cb) => {
893
+ if (cb.checked) selected.push(cb.value);
894
+ });
895
+
896
+ if (selected.length === 0) {
897
+ multiSelectValue.textContent = 'All Types';
898
+ } else if (selected.length === 1) {
899
+ multiSelectValue.textContent = selected[0];
900
+ } else if (selected.length === typeCheckboxes.length) {
901
+ multiSelectValue.textContent = 'All Types';
902
+ } else {
903
+ multiSelectValue.textContent = `${selected.length} selected`;
904
+ }
905
+ }
906
+
907
+ // Search input with debounce
908
+ searchInput?.addEventListener('input', () => {
909
+ clearTimeout(debounceTimer);
910
+ if (clearSearchBtn) {
911
+ clearSearchBtn.style.display = searchInput.value ? '' : 'none';
912
+ }
913
+ debounceTimer = setTimeout(emitFilterChange, 300);
914
+ });
915
+
916
+ clearSearchBtn?.addEventListener('click', () => {
917
+ searchInput.value = '';
918
+ clearSearchBtn.style.display = 'none';
919
+ emitFilterChange();
920
+ });
921
+
922
+ // Multi-select dropdown toggle
923
+ multiSelectTrigger?.addEventListener('click', () => {
924
+ multiSelect.classList.toggle('open');
925
+ });
926
+
927
+ // Close dropdown when clicking outside
928
+ document.addEventListener('click', (e) => {
929
+ if (multiSelect && !multiSelect.contains(e.target as Node)) {
930
+ multiSelect.classList.remove('open');
931
+ }
932
+ });
933
+
934
+ // Type checkboxes
935
+ typeCheckboxes.forEach((cb) => {
936
+ cb.addEventListener('change', () => {
937
+ updateMultiSelectValue();
938
+ emitFilterChange();
939
+ });
940
+ });
941
+
942
+ selectAllBtn?.addEventListener('click', () => {
943
+ typeCheckboxes.forEach((cb) => (cb.checked = true));
944
+ updateMultiSelectValue();
945
+ emitFilterChange();
946
+ });
947
+
948
+ clearAllTypesBtn?.addEventListener('click', () => {
949
+ typeCheckboxes.forEach((cb) => (cb.checked = false));
950
+ updateMultiSelectValue();
951
+ emitFilterChange();
952
+ });
953
+
954
+ // Project select
955
+ projectSelect?.addEventListener('change', emitFilterChange);
956
+
957
+ // Status buttons
958
+ statusButtons.forEach((btn) => {
959
+ btn.addEventListener('click', () => {
960
+ statusButtons.forEach((b) => b.classList.remove('active'));
961
+ btn.classList.add('active');
962
+ emitFilterChange();
963
+ });
964
+ });
965
+
966
+ // Sort select
967
+ sortSelect?.addEventListener('change', emitFilterChange);
968
+
969
+ // Sort direction toggle - using visibility toggle instead of innerHTML
970
+ sortDirBtn?.addEventListener('click', () => {
971
+ const currentDir = sortDirBtn.dataset.sortDirection;
972
+ const newDir = currentDir === 'asc' ? 'desc' : 'asc';
973
+ sortDirBtn.dataset.sortDirection = newDir;
974
+ sortDirBtn.setAttribute(
975
+ 'aria-label',
976
+ `Sort ${newDir === 'asc' ? 'ascending' : 'descending'}`
977
+ );
978
+
979
+ // Toggle visibility of arrow SVGs
980
+ const ascArrow = sortDirBtn.querySelector('.sort-asc') as HTMLElement;
981
+ const descArrow = sortDirBtn.querySelector('.sort-desc') as HTMLElement;
982
+
983
+ if (ascArrow && descArrow) {
984
+ ascArrow.style.display = newDir === 'asc' ? '' : 'none';
985
+ descArrow.style.display = newDir === 'desc' ? '' : 'none';
986
+ }
987
+
988
+ emitFilterChange();
989
+ });
990
+
991
+ // Clear all filters
992
+ clearAllFiltersBtn?.addEventListener('click', () => {
993
+ searchInput.value = '';
994
+ if (clearSearchBtn) clearSearchBtn.style.display = 'none';
995
+ typeCheckboxes.forEach((cb) => (cb.checked = false));
996
+ updateMultiSelectValue();
997
+ projectSelect.value = '';
998
+ statusButtons.forEach((b) => b.classList.remove('active'));
999
+ statusButtons[0]?.classList.add('active');
1000
+ sortSelect.value = 'cost';
1001
+ sortDirBtn.dataset.sortDirection = 'desc';
1002
+
1003
+ // Reset arrow visibility
1004
+ const ascArrow = sortDirBtn.querySelector('.sort-asc') as HTMLElement;
1005
+ const descArrow = sortDirBtn.querySelector('.sort-desc') as HTMLElement;
1006
+ if (ascArrow && descArrow) {
1007
+ ascArrow.style.display = 'none';
1008
+ descArrow.style.display = '';
1009
+ }
1010
+
1011
+ emitFilterChange();
1012
+ });
1013
+
1014
+ // Listen for external filter type events (from StatsHero pills)
1015
+ window.addEventListener('usage:filter-type', ((e: CustomEvent) => {
1016
+ const type = e.detail?.type;
1017
+ if (type) {
1018
+ // Clear all checkboxes first
1019
+ typeCheckboxes.forEach((cb) => (cb.checked = false));
1020
+ // Check only the selected type
1021
+ const targetCheckbox = Array.from(typeCheckboxes).find((cb) => cb.value === type);
1022
+ if (targetCheckbox) {
1023
+ targetCheckbox.checked = true;
1024
+ }
1025
+ updateMultiSelectValue();
1026
+ emitFilterChange();
1027
+ }
1028
+ }) as EventListener);
1029
+
1030
+ // Initial state
1031
+ updateActiveFilters(getFilters());
1032
+ }
1033
+ </script>