@littlebearapps/platform-admin-sdk 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +2 -5
  2. package/dist/templates.d.ts +1 -1
  3. package/dist/templates.js +121 -3
  4. package/package.json +1 -1
  5. package/templates/full/dashboard/src/components/notifications/NotificationDropdown.tsx +130 -0
  6. package/templates/full/dashboard/src/components/notifications/NotificationItem.tsx +264 -0
  7. package/templates/full/dashboard/src/components/patterns/PatternInfoButton.tsx +60 -0
  8. package/templates/full/dashboard/src/components/reports/FeatureUsageReport.tsx +339 -0
  9. package/templates/full/dashboard/src/components/search/SearchResultGroup.tsx +46 -0
  10. package/templates/full/dashboard/src/components/search/SearchResultItem.tsx +212 -0
  11. package/templates/full/dashboard/src/pages/api/patterns/[id]/approve.ts +49 -0
  12. package/templates/full/dashboard/src/pages/api/patterns/[id]/reject.ts +50 -0
  13. package/templates/full/dashboard/src/pages/api/reports/digests/stats.ts +38 -0
  14. package/templates/full/dashboard/src/pages/api/reports/digests.ts +39 -0
  15. package/templates/full/dashboard/src/pages/api/search/reindex/[type].ts +56 -0
  16. package/templates/full/dashboard/src/pages/api/test-reports/[id].ts +102 -0
  17. package/templates/full/dashboard/src/pages/feedback.astro +365 -0
  18. package/templates/full/dashboard/src/pages/kiosk.astro +206 -0
  19. package/templates/full/dashboard/src/pages/map.astro +561 -0
  20. package/templates/full/dashboard/src/pages/revenue.astro +72 -0
  21. package/templates/full/dashboard/src/pages/tests.astro +431 -0
  22. package/templates/full/scripts/ops/audit-cost-anomaly.ts +430 -0
  23. package/templates/full/scripts/ops/verify-account-total.ts +256 -0
  24. package/templates/full/tests/integration/feedback-schema.test.ts +361 -0
  25. package/templates/full/tests/integration/r2-archive.test.ts +108 -0
  26. package/templates/shared/.github/workflows/dependabot-automerge.yml +41 -0
  27. package/templates/shared/.github/workflows/validate-controls.yml +27 -0
  28. package/templates/shared/dashboard/src/components/Breadcrumbs.astro +101 -0
  29. package/templates/shared/dashboard/src/components/EmptyState.astro +46 -0
  30. package/templates/shared/dashboard/src/components/ErrorBoundary.astro +79 -0
  31. package/templates/shared/dashboard/src/components/LoadingSkeleton.astro +105 -0
  32. package/templates/shared/dashboard/src/components/PageShell.astro +72 -0
  33. package/templates/shared/dashboard/src/components/SkipLinks.astro +22 -0
  34. package/templates/shared/dashboard/src/components/Toast.astro +170 -0
  35. package/templates/shared/dashboard/src/components/ToastContainer.astro +156 -0
  36. package/templates/shared/dashboard/src/components/costs/ProviderCostsGrid.tsx +401 -0
  37. package/templates/shared/dashboard/src/components/costs/index.ts +4 -0
  38. package/templates/shared/dashboard/src/components/overview/AlertBanner.tsx +94 -0
  39. package/templates/shared/dashboard/src/components/overview/index.ts +9 -0
  40. package/templates/shared/dashboard/src/components/resources/CostChart.tsx +170 -0
  41. package/templates/shared/dashboard/src/components/resources/ProviderCard.tsx +272 -0
  42. package/templates/shared/dashboard/src/components/resources/ProviderDetail.tsx +293 -0
  43. package/templates/shared/dashboard/src/components/settings/SettingsCard.astro +102 -0
  44. package/templates/shared/dashboard/src/components/usage/AllowanceGauge.astro +170 -0
  45. package/templates/shared/dashboard/src/components/usage/AnomalyAlerts.astro +633 -0
  46. package/templates/shared/dashboard/src/components/usage/BillingCycleCountdown.astro +192 -0
  47. package/templates/shared/dashboard/src/components/usage/BurnRateHero.astro +539 -0
  48. package/templates/shared/dashboard/src/components/usage/CircuitBreakerEventLog.astro +542 -0
  49. package/templates/shared/dashboard/src/components/usage/CircuitBreakerPanel.tsx +292 -0
  50. package/templates/shared/dashboard/src/components/usage/CircuitBreakerStatus.astro +669 -0
  51. package/templates/shared/dashboard/src/components/usage/CompactThresholdBanner.astro +531 -0
  52. package/templates/shared/dashboard/src/components/usage/ComparisonModeSelector.astro +651 -0
  53. package/templates/shared/dashboard/src/components/usage/CostBreakdownChart.astro +381 -0
  54. package/templates/shared/dashboard/src/components/usage/CostBreakdownTable.astro +210 -0
  55. package/templates/shared/dashboard/src/components/usage/CostDataTable.astro +0 -0
  56. package/templates/shared/dashboard/src/components/usage/CostDonutChart.astro +311 -0
  57. package/templates/shared/dashboard/src/components/usage/DailyCostChart.astro +632 -0
  58. package/templates/shared/dashboard/src/components/usage/ExportButton.astro +114 -0
  59. package/templates/shared/dashboard/src/components/usage/FeatureBudgetsTable.astro +872 -0
  60. package/templates/shared/dashboard/src/components/usage/FilterBar.astro +190 -0
  61. package/templates/shared/dashboard/src/components/usage/FilterToggles.astro +175 -0
  62. package/templates/shared/dashboard/src/components/usage/GitHubUsageCard.astro +537 -0
  63. package/templates/shared/dashboard/src/components/usage/OverageCostCard.astro +212 -0
  64. package/templates/shared/dashboard/src/components/usage/PlanUtilizationCard.astro +193 -0
  65. package/templates/shared/dashboard/src/components/usage/ProjectCard.astro +640 -0
  66. package/templates/shared/dashboard/src/components/usage/ProjectCardsGrid.astro +272 -0
  67. package/templates/shared/dashboard/src/components/usage/ResourceSearch.astro +279 -0
  68. package/templates/shared/dashboard/src/components/usage/ServiceUtilizationList.astro +604 -0
  69. package/templates/shared/dashboard/src/components/usage/SparklineCard.astro +399 -0
  70. package/templates/shared/dashboard/src/components/usage/StatsHero.astro +600 -0
  71. package/templates/shared/dashboard/src/components/usage/TableFilters.astro +1033 -0
  72. package/templates/shared/dashboard/src/components/usage/ThresholdAlert.astro +271 -0
  73. package/templates/shared/dashboard/src/components/usage/ThresholdSettings.astro +618 -0
  74. package/templates/shared/dashboard/src/components/usage/TopSpenderCard.astro +170 -0
  75. package/templates/shared/dashboard/src/components/usage/UnifiedResourceTable.astro +1737 -0
  76. package/templates/shared/dashboard/src/components/usage/UsageCard.astro +135 -0
  77. package/templates/shared/dashboard/src/components/usage/UsageHealthBanner.astro +387 -0
  78. package/templates/shared/dashboard/src/components/usage/UtilizationBar.astro +159 -0
  79. package/templates/shared/dashboard/src/components/usage/WorkersBreakdownTable.astro +659 -0
  80. package/templates/shared/dashboard/src/components/usage/daily/CostChart.astro +461 -0
  81. package/templates/shared/dashboard/src/components/usage/daily/CostTable.astro +946 -0
  82. package/templates/shared/dashboard/src/components/usage/daily/DailyOverview.astro +1079 -0
  83. package/templates/shared/dashboard/src/components/usage/design-tokens.ts +187 -0
  84. package/templates/shared/dashboard/src/components/usage/filters/InlineDateRange.astro +285 -0
  85. package/templates/shared/dashboard/src/components/usage/filters/PeriodButtons.astro +157 -0
  86. package/templates/shared/dashboard/src/components/usage/filters/ProjectSelect.astro +284 -0
  87. package/templates/shared/dashboard/src/components/usage/scripts/ai-tab-controller.ts +419 -0
  88. package/templates/shared/dashboard/src/components/usage/scripts/constants.ts +60 -0
  89. package/templates/shared/dashboard/src/components/usage/scripts/formatters.ts +62 -0
  90. package/templates/shared/dashboard/src/components/usage/scripts/overview-controller.ts +1633 -0
  91. package/templates/shared/dashboard/src/components/usage/scripts/resource-table-builder.ts +294 -0
  92. package/templates/shared/dashboard/src/components/usage/scripts/tabs-filters-controller.ts +464 -0
  93. package/templates/shared/dashboard/src/components/usage/state/index.ts +55 -0
  94. package/templates/shared/dashboard/src/components/usage/state/usageActions.ts +439 -0
  95. package/templates/shared/dashboard/src/components/usage/state/usageStore.ts +376 -0
  96. package/templates/shared/dashboard/src/components/usage/types.ts +283 -0
  97. package/templates/shared/dashboard/src/components/usage/usage-colors.ts +292 -0
  98. package/templates/shared/dashboard/src/pages/api/usage/ai-models.ts +235 -0
  99. package/templates/shared/dashboard/src/pages/api/usage/billing-context.ts +296 -0
  100. package/templates/shared/scripts/test-telemetry-flow.ts +464 -0
  101. package/templates/shared/tests/e2e/usage-export.test.ts +784 -0
  102. package/templates/shared/tests/e2e/usage-mobile.test.ts +531 -0
  103. package/templates/standard/dashboard/src/components/errors/PriorityBadge.astro +27 -0
  104. package/templates/standard/dashboard/src/components/infrastructure/HealthchecksStatus.tsx +293 -0
  105. package/templates/standard/dashboard/src/components/infrastructure/InfrastructureTabs.tsx +268 -0
  106. package/templates/standard/dashboard/src/pages/analytics.astro +64 -0
  107. package/templates/standard/dashboard/src/pages/api/infrastructure/alerts.ts +85 -0
  108. package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks/[id]/flips.ts +110 -0
  109. package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks.ts +101 -0
  110. package/templates/standard/dashboard/src/pages/api/infrastructure/uptime/[id]/response-times.ts +121 -0
  111. package/templates/standard/dashboard/src/pages/api/infrastructure/uptime.ts +89 -0
  112. package/templates/standard/dashboard/src/pages/api/test/service-auth.ts +178 -0
  113. package/templates/standard/tests/integration/connectors.test.ts +241 -0
  114. package/templates/standard/tests/integration/github-monitor.test.ts +143 -0
  115. package/templates/standard/tests/integration/ingestion.test.ts +211 -0
@@ -0,0 +1,272 @@
1
+ ---
2
+ /**
3
+ * ProjectCardsGrid Component
4
+ *
5
+ * Grid layout for project cards showing:
6
+ * - Scout
7
+ * - Brand Copilot
8
+ * - Australian History MCP (semantic-librarian)
9
+ * - Platform (shared infrastructure)
10
+ *
11
+ * Each card displays MTD cost, utilization, traffic light status, and sparkline.
12
+ */
13
+
14
+ import ProjectCard from './ProjectCard.astro';
15
+ import {
16
+ PROJECT_ALLOWANCES,
17
+ CF_ALLOWANCES,
18
+ type ServiceType,
19
+ } from '../../lib/usage/allowance-config';
20
+
21
+ // Build project configurations from allowance-config.ts
22
+ const projects = PROJECT_ALLOWANCES.map((p) => {
23
+ const primaryResourceLabel =
24
+ CF_ALLOWANCES[p.primaryResource as ServiceType]?.name?.split(' ')[0] ?? p.primaryResource;
25
+ return {
26
+ projectId: p.projectId,
27
+ projectName: p.projectName,
28
+ primaryResource: primaryResourceLabel,
29
+ };
30
+ });
31
+ ---
32
+
33
+ <section class="project-cards-section" role="region" aria-label="Project usage overview">
34
+ <div class="section-header">
35
+ <h2 class="section-title">Project Overview</h2>
36
+ <div class="section-actions">
37
+ <button
38
+ type="button"
39
+ id="refresh-projects-btn"
40
+ class="refresh-btn"
41
+ title="Refresh project data"
42
+ >
43
+ <span class="refresh-icon">↻</span>
44
+ <span class="refresh-text">Refresh</span>
45
+ </button>
46
+ </div>
47
+ </div>
48
+
49
+ <div class="project-cards-grid" id="project-cards-grid">
50
+ {
51
+ projects.map((project) => (
52
+ <ProjectCard
53
+ projectId={project.projectId}
54
+ projectName={project.projectName}
55
+ primaryResource={project.primaryResource}
56
+ />
57
+ ))
58
+ }
59
+ </div>
60
+
61
+ <!-- Loading overlay -->
62
+ <div id="projects-loading" class="projects-loading" style="display: none;">
63
+ <div class="loading-spinner"></div>
64
+ <span class="loading-text">Loading project data...</span>
65
+ </div>
66
+ </section>
67
+
68
+ <style>
69
+ .project-cards-section {
70
+ margin-bottom: 2rem;
71
+ position: relative;
72
+ }
73
+
74
+ .section-header {
75
+ display: flex;
76
+ justify-content: space-between;
77
+ align-items: center;
78
+ margin-bottom: 1rem;
79
+ }
80
+
81
+ .section-title {
82
+ font-size: 1.125rem;
83
+ font-weight: 600;
84
+ color: var(--usage-text-primary);
85
+ margin: 0;
86
+ }
87
+
88
+ .section-actions {
89
+ display: flex;
90
+ gap: 0.5rem;
91
+ }
92
+
93
+ .refresh-btn {
94
+ display: flex;
95
+ align-items: center;
96
+ gap: 0.375rem;
97
+ padding: 0.375rem 0.75rem;
98
+ background: var(--usage-bg-tertiary);
99
+ border: 1px solid var(--usage-border-default);
100
+ border-radius: var(--usage-radius-md);
101
+ color: var(--usage-text-secondary);
102
+ font-size: 0.75rem;
103
+ font-weight: 500;
104
+ cursor: pointer;
105
+ transition: all var(--usage-transition-fast);
106
+ }
107
+
108
+ .refresh-btn:hover {
109
+ background: var(--usage-bg-hover);
110
+ border-color: var(--usage-border-hover);
111
+ color: var(--usage-text-primary);
112
+ }
113
+
114
+ .refresh-btn:active {
115
+ transform: scale(0.98);
116
+ }
117
+
118
+ .refresh-btn.loading .refresh-icon {
119
+ animation: spin 1s linear infinite;
120
+ }
121
+
122
+ @keyframes spin {
123
+ from {
124
+ transform: rotate(0deg);
125
+ }
126
+ to {
127
+ transform: rotate(360deg);
128
+ }
129
+ }
130
+
131
+ .refresh-icon {
132
+ font-size: 0.875rem;
133
+ }
134
+
135
+ .project-cards-grid {
136
+ display: grid;
137
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
138
+ gap: 1.5rem;
139
+ }
140
+
141
+ /* Loading overlay */
142
+ .projects-loading {
143
+ position: absolute;
144
+ top: 0;
145
+ left: 0;
146
+ right: 0;
147
+ bottom: 0;
148
+ background: var(--usage-bg-primary);
149
+ opacity: 0.9;
150
+ display: flex;
151
+ flex-direction: column;
152
+ align-items: center;
153
+ justify-content: center;
154
+ gap: 1rem;
155
+ border-radius: var(--usage-radius-xl);
156
+ z-index: 10;
157
+ }
158
+
159
+ .loading-spinner {
160
+ width: 32px;
161
+ height: 32px;
162
+ border: 3px solid var(--usage-border-default);
163
+ border-top-color: var(--usage-accent-blue);
164
+ border-radius: 50%;
165
+ animation: spin 1s linear infinite;
166
+ }
167
+
168
+ .loading-text {
169
+ font-size: 0.875rem;
170
+ color: var(--usage-text-secondary);
171
+ }
172
+
173
+ /* Responsive */
174
+ @media (max-width: 640px) {
175
+ .project-cards-grid {
176
+ grid-template-columns: 1fr;
177
+ gap: 1rem;
178
+ }
179
+
180
+ .section-header {
181
+ flex-direction: column;
182
+ align-items: flex-start;
183
+ gap: 0.75rem;
184
+ }
185
+ }
186
+ </style>
187
+
188
+ <script>
189
+ /**
190
+ * ProjectCardsGrid JavaScript
191
+ *
192
+ * Handles refresh button and coordinates project card updates.
193
+ */
194
+
195
+ // Initialize refresh button
196
+ function initRefreshButton() {
197
+ const refreshBtn = document.getElementById('refresh-projects-btn');
198
+ if (!refreshBtn) return;
199
+
200
+ refreshBtn.addEventListener('click', async () => {
201
+ refreshBtn.classList.add('loading');
202
+
203
+ // Dispatch event for parent to handle
204
+ window.dispatchEvent(new CustomEvent('usage:refresh-projects'));
205
+
206
+ // Remove loading state after a delay (actual loading handled by parent)
207
+ setTimeout(() => {
208
+ refreshBtn.classList.remove('loading');
209
+ }, 1000);
210
+ });
211
+ }
212
+
213
+ // Show/hide loading overlay
214
+ window.setProjectsLoading = function (loading: boolean) {
215
+ const overlay = document.getElementById('projects-loading');
216
+ if (overlay) {
217
+ overlay.style.display = loading ? 'flex' : 'none';
218
+ }
219
+ };
220
+
221
+ // Update all project cards from API data
222
+ window.updateAllProjectCards = function (
223
+ projects: Array<{
224
+ projectId: string;
225
+ projectName: string;
226
+ primaryResource: string;
227
+ mtdCost: number;
228
+ costDeltaPct?: number;
229
+ utilizationPct: number;
230
+ utilizationCurrent: number;
231
+ utilizationLimit: number;
232
+ utilizationUnit: string;
233
+ status: 'green' | 'yellow' | 'red';
234
+ sparklineData: number[];
235
+ circuitBreakerStatus: 'active' | 'tripped' | 'degraded';
236
+ circuitBreakerLabel: string;
237
+ }>
238
+ ) {
239
+ for (const project of projects) {
240
+ if (window.updateProjectCard) {
241
+ window.updateProjectCard(project);
242
+ }
243
+ }
244
+ };
245
+
246
+ // Type declarations
247
+ declare global {
248
+ interface Window {
249
+ setProjectsLoading: (loading: boolean) => void;
250
+ updateAllProjectCards: (
251
+ projects: Array<{
252
+ projectId: string;
253
+ projectName: string;
254
+ primaryResource: string;
255
+ mtdCost: number;
256
+ costDeltaPct?: number;
257
+ utilizationPct: number;
258
+ utilizationCurrent: number;
259
+ utilizationLimit: number;
260
+ utilizationUnit: string;
261
+ status: 'green' | 'yellow' | 'red';
262
+ sparklineData: number[];
263
+ circuitBreakerStatus: 'active' | 'tripped' | 'degraded';
264
+ circuitBreakerLabel: string;
265
+ }>
266
+ ) => void;
267
+ }
268
+ }
269
+
270
+ // Initialize on DOM ready
271
+ document.addEventListener('DOMContentLoaded', initRefreshButton);
272
+ </script>
@@ -0,0 +1,279 @@
1
+ ---
2
+ /**
3
+ * ResourceSearch Component (task-17.14)
4
+ *
5
+ * Search input to filter resources by name.
6
+ * Features debounced filtering, case-insensitive matching.
7
+ */
8
+
9
+ export interface Props {
10
+ initialQuery?: string;
11
+ placeholder?: string;
12
+ }
13
+
14
+ const { initialQuery = '', placeholder = 'Search resources...' } = Astro.props;
15
+ ---
16
+
17
+ <div class="resource-search" data-component="resource-search">
18
+ <div class="search-input-wrapper">
19
+ <span class="search-icon" aria-hidden="true">
20
+ <svg
21
+ width="16"
22
+ height="16"
23
+ viewBox="0 0 16 16"
24
+ fill="none"
25
+ xmlns="http://www.w3.org/2000/svg"
26
+ >
27
+ <path
28
+ d="M7.33333 12.6667C10.2789 12.6667 12.6667 10.2789 12.6667 7.33333C12.6667 4.38781 10.2789 2 7.33333 2C4.38781 2 2 4.38781 2 7.33333C2 10.2789 4.38781 12.6667 7.33333 12.6667Z"
29
+ stroke="currentColor"
30
+ stroke-width="1.5"
31
+ stroke-linecap="round"
32
+ stroke-linejoin="round"></path>
33
+ <path
34
+ d="M14 14L11.1 11.1"
35
+ stroke="currentColor"
36
+ stroke-width="1.5"
37
+ stroke-linecap="round"
38
+ stroke-linejoin="round"></path>
39
+ </svg>
40
+ </span>
41
+ <input
42
+ type="text"
43
+ id="resource-search-input"
44
+ class="search-input"
45
+ placeholder={placeholder}
46
+ value={initialQuery}
47
+ autocomplete="off"
48
+ spellcheck="false"
49
+ aria-label="Search resources"
50
+ />
51
+ <button
52
+ type="button"
53
+ id="search-clear-button"
54
+ class="clear-button"
55
+ style={initialQuery ? '' : 'display: none;'}
56
+ aria-label="Clear search"
57
+ >
58
+ <svg
59
+ width="14"
60
+ height="14"
61
+ viewBox="0 0 14 14"
62
+ fill="none"
63
+ xmlns="http://www.w3.org/2000/svg"
64
+ >
65
+ <path
66
+ d="M10.5 3.5L3.5 10.5M3.5 3.5L10.5 10.5"
67
+ stroke="currentColor"
68
+ stroke-width="1.5"
69
+ stroke-linecap="round"
70
+ stroke-linejoin="round"></path>
71
+ </svg>
72
+ </button>
73
+ </div>
74
+ <div class="keyboard-hint" title="Press ⌘F or Ctrl+F to focus search">
75
+ <kbd>⌘F</kbd>
76
+ </div>
77
+ </div>
78
+
79
+ <style>
80
+ .resource-search {
81
+ display: flex;
82
+ align-items: center;
83
+ gap: 0.5rem;
84
+ }
85
+
86
+ .search-input-wrapper {
87
+ position: relative;
88
+ display: flex;
89
+ align-items: center;
90
+ flex: 1;
91
+ max-width: 300px;
92
+ }
93
+
94
+ .search-icon {
95
+ position: absolute;
96
+ left: 0.75rem;
97
+ color: #9ca3af;
98
+ pointer-events: none;
99
+ display: flex;
100
+ align-items: center;
101
+ }
102
+
103
+ .search-input {
104
+ width: 100%;
105
+ padding: 0.5rem 2rem 0.5rem 2.25rem;
106
+ border: 1px solid #d1d5db;
107
+ border-radius: 0.375rem;
108
+ background-color: white;
109
+ color: #374151;
110
+ font-size: 0.875rem;
111
+ transition: all 0.15s ease;
112
+ }
113
+
114
+ :global(.dark) .search-input {
115
+ background-color: #1f2937;
116
+ border-color: #374151;
117
+ color: #f9fafb;
118
+ }
119
+
120
+ .search-input::placeholder {
121
+ color: #9ca3af;
122
+ }
123
+
124
+ .search-input:hover {
125
+ border-color: #9ca3af;
126
+ }
127
+
128
+ .search-input:focus {
129
+ outline: none;
130
+ border-color: #3b82f6;
131
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
132
+ }
133
+
134
+ .clear-button {
135
+ position: absolute;
136
+ right: 0.5rem;
137
+ display: flex;
138
+ align-items: center;
139
+ justify-content: center;
140
+ width: 20px;
141
+ height: 20px;
142
+ border: none;
143
+ background-color: #e5e7eb;
144
+ color: #6b7280;
145
+ border-radius: 50%;
146
+ cursor: pointer;
147
+ transition: all 0.15s ease;
148
+ }
149
+
150
+ :global(.dark) .clear-button {
151
+ background-color: #374151;
152
+ color: #9ca3af;
153
+ }
154
+
155
+ .clear-button:hover {
156
+ background-color: #d1d5db;
157
+ color: #374151;
158
+ }
159
+
160
+ :global(.dark) .clear-button:hover {
161
+ background-color: #4b5563;
162
+ color: #f9fafb;
163
+ }
164
+
165
+ .keyboard-hint {
166
+ display: none;
167
+ align-items: center;
168
+ gap: 0.25rem;
169
+ color: #9ca3af;
170
+ font-size: 0.625rem;
171
+ }
172
+
173
+ @media (min-width: 768px) {
174
+ .keyboard-hint {
175
+ display: flex;
176
+ }
177
+ }
178
+
179
+ kbd {
180
+ display: inline-flex;
181
+ align-items: center;
182
+ padding: 0.125rem 0.375rem;
183
+ background-color: #f3f4f6;
184
+ border: 1px solid #d1d5db;
185
+ border-radius: 0.25rem;
186
+ font-family: inherit;
187
+ font-size: 0.625rem;
188
+ font-weight: 500;
189
+ color: #6b7280;
190
+ }
191
+
192
+ :global(.dark) kbd {
193
+ background-color: #374151;
194
+ border-color: #4b5563;
195
+ color: #9ca3af;
196
+ }
197
+
198
+ @media (max-width: 640px) {
199
+ .resource-search {
200
+ width: 100%;
201
+ }
202
+
203
+ .search-input-wrapper {
204
+ max-width: none;
205
+ }
206
+ }
207
+ </style>
208
+
209
+ <script>
210
+ document.addEventListener('DOMContentLoaded', () => {
211
+ const container = document.querySelector('[data-component="resource-search"]');
212
+ if (!container) return;
213
+
214
+ const input = container.querySelector('#resource-search-input') as HTMLInputElement;
215
+ const clearButton = container.querySelector('#search-clear-button') as HTMLButtonElement;
216
+
217
+ let debounceTimer: ReturnType<typeof setTimeout>;
218
+
219
+ // Debounced search handler
220
+ function handleSearch(query: string) {
221
+ // Update URL
222
+ const url = new URL(window.location.href);
223
+ if (query) {
224
+ url.searchParams.set('search', query);
225
+ } else {
226
+ url.searchParams.delete('search');
227
+ }
228
+ window.history.pushState({}, '', url);
229
+
230
+ // Dispatch custom event for parent to handle
231
+ const event = new CustomEvent('resource-search', {
232
+ bubbles: true,
233
+ detail: { query },
234
+ });
235
+ container.dispatchEvent(event);
236
+ }
237
+
238
+ // Input handler with debounce
239
+ input.addEventListener('input', () => {
240
+ const query = input.value.trim();
241
+
242
+ // Show/hide clear button
243
+ clearButton.style.display = query ? 'flex' : 'none';
244
+
245
+ // Debounce the search
246
+ clearTimeout(debounceTimer);
247
+ debounceTimer = setTimeout(() => handleSearch(query), 300);
248
+ });
249
+
250
+ // Clear button handler
251
+ clearButton.addEventListener('click', () => {
252
+ input.value = '';
253
+ clearButton.style.display = 'none';
254
+ handleSearch('');
255
+ input.focus();
256
+ });
257
+
258
+ // Keyboard shortcut: Cmd/Ctrl+F focuses search
259
+ document.addEventListener('keydown', (e) => {
260
+ if ((e.metaKey || e.ctrlKey) && e.key === 'f') {
261
+ // Only prevent default if we're not in an input/textarea
262
+ const target = e.target as HTMLElement;
263
+ if (target.tagName !== 'INPUT' && target.tagName !== 'TEXTAREA') {
264
+ e.preventDefault();
265
+ input.focus();
266
+ input.select();
267
+ }
268
+ }
269
+
270
+ // Escape clears search when focused
271
+ if (e.key === 'Escape' && document.activeElement === input) {
272
+ input.value = '';
273
+ clearButton.style.display = 'none';
274
+ handleSearch('');
275
+ input.blur();
276
+ }
277
+ });
278
+ });
279
+ </script>