@geenius/tools 0.1.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 (160) hide show
  1. package/.changeset/config.json +11 -0
  2. package/.env.example +2 -0
  3. package/.github/CODEOWNERS +1 -0
  4. package/.github/ISSUE_TEMPLATE/bug_report.md +16 -0
  5. package/.github/ISSUE_TEMPLATE/feature_request.md +11 -0
  6. package/.github/PULL_REQUEST_TEMPLATE.md +10 -0
  7. package/.github/dependabot.yml +11 -0
  8. package/.github/workflows/ci.yml +23 -0
  9. package/.github/workflows/release.yml +29 -0
  10. package/.node-version +1 -0
  11. package/.nvmrc +1 -0
  12. package/.prettierrc +7 -0
  13. package/.project/ACCOUNT.yaml +4 -0
  14. package/.project/IDEAS.yaml +7 -0
  15. package/.project/PROJECT.yaml +11 -0
  16. package/.project/ROADMAP.yaml +15 -0
  17. package/CHANGELOG.md +16 -0
  18. package/CODE_OF_CONDUCT.md +26 -0
  19. package/CONTRIBUTING.md +69 -0
  20. package/LICENSE +21 -0
  21. package/README.md +1 -0
  22. package/SECURITY.md +18 -0
  23. package/SUPPORT.md +14 -0
  24. package/package.json +75 -0
  25. package/packages/convex/shared/README.md +1 -0
  26. package/packages/convex/shared/package.json +42 -0
  27. package/packages/convex/shared/src/audit/index.ts +5 -0
  28. package/packages/convex/shared/src/audit/presets.ts +165 -0
  29. package/packages/convex/shared/src/audit/schema.ts +85 -0
  30. package/packages/convex/shared/src/audit/write.ts +102 -0
  31. package/packages/convex/shared/src/extract.ts +75 -0
  32. package/packages/convex/shared/src/index.ts +41 -0
  33. package/packages/convex/shared/src/messages.ts +45 -0
  34. package/packages/convex/shared/src/security.ts +112 -0
  35. package/packages/convex/shared/src/throw.ts +184 -0
  36. package/packages/convex/shared/src/types.ts +57 -0
  37. package/packages/convex/shared/src/utils.ts +58 -0
  38. package/packages/convex/shared/tsconfig.json +28 -0
  39. package/packages/convex/shared/tsup.config.ts +12 -0
  40. package/packages/devtools/package.json +27 -0
  41. package/packages/devtools/react/README.md +1 -0
  42. package/packages/devtools/react/package.json +53 -0
  43. package/packages/devtools/react/src/components/DesignPreview.tsx +59 -0
  44. package/packages/devtools/react/src/components/DesignSwitcherDropdown.tsx +99 -0
  45. package/packages/devtools/react/src/components/DevSidebar.tsx +247 -0
  46. package/packages/devtools/react/src/components/DevToolbar.tsx +242 -0
  47. package/packages/devtools/react/src/components/GitHubIssueDialog.tsx +402 -0
  48. package/packages/devtools/react/src/components/InspectorOverlay.tsx +312 -0
  49. package/packages/devtools/react/src/components/PageLoadWaterfall.tsx +144 -0
  50. package/packages/devtools/react/src/components/PerformancePanel.tsx +330 -0
  51. package/packages/devtools/react/src/context/DevModeContext.tsx +226 -0
  52. package/packages/devtools/react/src/context/PerformanceContext.tsx +143 -0
  53. package/packages/devtools/react/src/data/designs.ts +13 -0
  54. package/packages/devtools/react/src/hooks/useGitHubLabels.ts +47 -0
  55. package/packages/devtools/react/src/hooks/useVirtualList.ts +124 -0
  56. package/packages/devtools/react/src/index.ts +77 -0
  57. package/packages/devtools/react/src/panels/ConvexSpy.tsx +130 -0
  58. package/packages/devtools/react/src/panels/DatabaseSeeder.tsx +116 -0
  59. package/packages/devtools/react/src/panels/DevModePhase2.tsx +191 -0
  60. package/packages/devtools/react/src/panels/DevModePhase3.tsx +234 -0
  61. package/packages/devtools/react/src/panels/FeatureFlagsToggle.tsx +104 -0
  62. package/packages/devtools/react/src/panels/QuickRouteJump.tsx +152 -0
  63. package/packages/devtools/react/src/services/github-service.ts +247 -0
  64. package/packages/devtools/react/tsconfig.json +31 -0
  65. package/packages/devtools/react/tsup.config.ts +18 -0
  66. package/packages/devtools/solidjs/README.md +1 -0
  67. package/packages/devtools/solidjs/package.json +49 -0
  68. package/packages/devtools/solidjs/src/components/DesignPreview.tsx +51 -0
  69. package/packages/devtools/solidjs/src/components/DesignSwitcherDropdown.tsx +95 -0
  70. package/packages/devtools/solidjs/src/components/DevSidebar.tsx +247 -0
  71. package/packages/devtools/solidjs/src/components/DevToolbar.tsx +242 -0
  72. package/packages/devtools/solidjs/src/components/GitHubIssueDialog.tsx +400 -0
  73. package/packages/devtools/solidjs/src/components/InspectorOverlay.tsx +311 -0
  74. package/packages/devtools/solidjs/src/components/PageLoadWaterfall.tsx +144 -0
  75. package/packages/devtools/solidjs/src/components/PerformancePanel.tsx +330 -0
  76. package/packages/devtools/solidjs/src/context/DevModeContext.tsx +216 -0
  77. package/packages/devtools/solidjs/src/context/PerformanceContext.tsx +135 -0
  78. package/packages/devtools/solidjs/src/data/designs.ts +13 -0
  79. package/packages/devtools/solidjs/src/hooks/createGitHubLabels.ts +47 -0
  80. package/packages/devtools/solidjs/src/index.ts +64 -0
  81. package/packages/devtools/solidjs/src/services/github-service.ts +247 -0
  82. package/packages/devtools/solidjs/tsconfig.json +21 -0
  83. package/packages/devtools/src/index.ts +377 -0
  84. package/packages/devtools/tsup.config.ts +12 -0
  85. package/packages/env/package.json +30 -0
  86. package/packages/env/src/index.ts +264 -0
  87. package/packages/env/tsup.config.ts +12 -0
  88. package/packages/errors/package.json +27 -0
  89. package/packages/errors/react/README.md +1 -0
  90. package/packages/errors/react/package.json +72 -0
  91. package/packages/errors/react/src/analytics.ts +16 -0
  92. package/packages/errors/react/src/components/ErrorBoundary.tsx +248 -0
  93. package/packages/errors/react/src/components/ErrorDisplay.tsx +328 -0
  94. package/packages/errors/react/src/components/ValidationErrors.tsx +102 -0
  95. package/packages/errors/react/src/config.ts +199 -0
  96. package/packages/errors/react/src/constants.ts +74 -0
  97. package/packages/errors/react/src/hooks/useErrorBoundary.ts +92 -0
  98. package/packages/errors/react/src/hooks/useErrorHandler.ts +87 -0
  99. package/packages/errors/react/src/index.ts +96 -0
  100. package/packages/errors/react/src/types.ts +102 -0
  101. package/packages/errors/react/src/utils/errorMessages.ts +35 -0
  102. package/packages/errors/react/src/utils/errorPolicy.ts +139 -0
  103. package/packages/errors/react/src/utils/extractAppError.ts +174 -0
  104. package/packages/errors/react/src/utils/formatError.ts +112 -0
  105. package/packages/errors/react/tsconfig.json +25 -0
  106. package/packages/errors/react/tsup.config.ts +24 -0
  107. package/packages/errors/solidjs/README.md +1 -0
  108. package/packages/errors/solidjs/package.json +46 -0
  109. package/packages/errors/solidjs/src/components/ErrorDisplay.tsx +179 -0
  110. package/packages/errors/solidjs/src/config.ts +98 -0
  111. package/packages/errors/solidjs/src/hooks/createErrorHandler.ts +107 -0
  112. package/packages/errors/solidjs/src/index.ts +61 -0
  113. package/packages/errors/solidjs/src/types.ts +34 -0
  114. package/packages/errors/solidjs/src/utils/errorPolicy.ts +56 -0
  115. package/packages/errors/solidjs/src/utils/extractAppError.ts +94 -0
  116. package/packages/errors/solidjs/src/utils/formatError.ts +33 -0
  117. package/packages/errors/solidjs/tsconfig.json +26 -0
  118. package/packages/errors/solidjs/tsup.config.ts +21 -0
  119. package/packages/errors/src/index.ts +320 -0
  120. package/packages/errors/tsup.config.ts +12 -0
  121. package/packages/logger/package.json +27 -0
  122. package/packages/logger/react/README.md +1 -0
  123. package/packages/logger/react/package.json +46 -0
  124. package/packages/logger/react/src/index.ts +4 -0
  125. package/packages/logger/react/src/useMetrics.ts +42 -0
  126. package/packages/logger/react/src/usePerformanceLog.ts +61 -0
  127. package/packages/logger/react/tsconfig.json +31 -0
  128. package/packages/logger/react/tsup.config.ts +12 -0
  129. package/packages/logger/solidjs/README.md +1 -0
  130. package/packages/logger/solidjs/package.json +45 -0
  131. package/packages/logger/solidjs/src/createMetrics.ts +37 -0
  132. package/packages/logger/solidjs/src/createPerformanceLog.ts +58 -0
  133. package/packages/logger/solidjs/src/index.ts +4 -0
  134. package/packages/logger/solidjs/tsconfig.json +32 -0
  135. package/packages/logger/solidjs/tsup.config.ts +12 -0
  136. package/packages/logger/src/index.ts +363 -0
  137. package/packages/logger/tsup.config.ts +12 -0
  138. package/packages/perf/package.json +27 -0
  139. package/packages/perf/react/README.md +1 -0
  140. package/packages/perf/react/package.json +59 -0
  141. package/packages/perf/react/src/components/PerformanceDashboard.tsx +257 -0
  142. package/packages/perf/react/src/hooks/useMonitoredQuery.ts +89 -0
  143. package/packages/perf/react/src/hooks/usePerformanceMetrics.ts +78 -0
  144. package/packages/perf/react/src/index.ts +33 -0
  145. package/packages/perf/react/src/services/PerformanceMonitor.ts +313 -0
  146. package/packages/perf/react/src/types.ts +77 -0
  147. package/packages/perf/react/tsconfig.json +25 -0
  148. package/packages/perf/react/tsup.config.ts +19 -0
  149. package/packages/perf/solidjs/README.md +1 -0
  150. package/packages/perf/solidjs/package.json +41 -0
  151. package/packages/perf/solidjs/src/components/PerformanceDashboard.tsx +207 -0
  152. package/packages/perf/solidjs/src/hooks/createPerformanceMetrics.ts +73 -0
  153. package/packages/perf/solidjs/src/index.ts +31 -0
  154. package/packages/perf/solidjs/src/services/PerformanceMonitor.ts +134 -0
  155. package/packages/perf/solidjs/src/types.ts +78 -0
  156. package/packages/perf/solidjs/tsconfig.json +26 -0
  157. package/packages/perf/solidjs/tsup.config.ts +14 -0
  158. package/packages/perf/src/index.ts +410 -0
  159. package/packages/perf/tsup.config.ts +12 -0
  160. package/pnpm-workspace.yaml +2 -0
@@ -0,0 +1,234 @@
1
+ import { useState, useEffect, useCallback, useMemo } from 'react'
2
+
3
+ // ─── Accessibility Auditor ───────────────────────────────────
4
+
5
+ interface A11yIssue {
6
+ id: string
7
+ impact: 'critical' | 'serious' | 'moderate' | 'minor'
8
+ description: string
9
+ element: string
10
+ help: string
11
+ helpUrl?: string
12
+ }
13
+
14
+ /**
15
+ * Accessibility Auditor Panel β€” Runtime a11y checking.
16
+ * Uses DOM inspection to detect common accessibility issues.
17
+ */
18
+ export function AccessibilityAuditor() {
19
+ const [issues, setIssues] = useState<A11yIssue[]>([])
20
+ const [isScanning, setIsScanning] = useState(false)
21
+ const [filter, setFilter] = useState<string>('all')
22
+
23
+ const runAudit = useCallback(() => {
24
+ setIsScanning(true)
25
+ const found: A11yIssue[] = []
26
+ let id = 0
27
+
28
+ // Check images without alt
29
+ document.querySelectorAll('img:not([alt])').forEach(el => {
30
+ found.push({
31
+ id: `a11y_${id++}`, impact: 'critical',
32
+ description: 'Image missing alt attribute',
33
+ element: `<img src="${el.getAttribute('src')?.slice(0, 40)}">`,
34
+ help: 'Add descriptive alt text to all images',
35
+ })
36
+ })
37
+
38
+ // Check buttons without accessible names
39
+ document.querySelectorAll('button').forEach(el => {
40
+ if (!el.textContent?.trim() && !el.getAttribute('aria-label') && !el.getAttribute('title')) {
41
+ found.push({
42
+ id: `a11y_${id++}`, impact: 'critical',
43
+ description: 'Button without accessible name',
44
+ element: el.outerHTML.slice(0, 80),
45
+ help: 'Add text content, aria-label, or title to buttons',
46
+ })
47
+ }
48
+ })
49
+
50
+ // Check form inputs without labels
51
+ document.querySelectorAll('input, select, textarea').forEach(el => {
52
+ const input = el as HTMLInputElement
53
+ const hasLabel = input.id && document.querySelector(`label[for="${input.id}"]`)
54
+ const hasAria = input.getAttribute('aria-label') || input.getAttribute('aria-labelledby')
55
+ if (!hasLabel && !hasAria && input.type !== 'hidden') {
56
+ found.push({
57
+ id: `a11y_${id++}`, impact: 'serious',
58
+ description: 'Form input without label',
59
+ element: `<${el.tagName.toLowerCase()} type="${input.type}">`,
60
+ help: 'Associate a <label> element or add aria-label',
61
+ })
62
+ }
63
+ })
64
+
65
+ // Check color contrast (simplified β€” check for very light text)
66
+ document.querySelectorAll('*').forEach(el => {
67
+ const style = getComputedStyle(el)
68
+ const color = style.color
69
+ const bg = style.backgroundColor
70
+ if (color === 'rgb(255, 255, 255)' && bg === 'rgb(255, 255, 255)') {
71
+ found.push({
72
+ id: `a11y_${id++}`, impact: 'serious',
73
+ description: 'Possible invisible text (white on white)',
74
+ element: el.tagName.toLowerCase(),
75
+ help: 'Ensure sufficient color contrast ratio (4.5:1 for normal text)',
76
+ })
77
+ }
78
+ })
79
+
80
+ // Check heading hierarchy
81
+ const headings = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6'))
82
+ let prevLevel = 0
83
+ for (const h of headings) {
84
+ const level = parseInt(h.tagName[1])
85
+ if (level > prevLevel + 1 && prevLevel > 0) {
86
+ found.push({
87
+ id: `a11y_${id++}`, impact: 'moderate',
88
+ description: `Heading level skipped (h${prevLevel} β†’ h${level})`,
89
+ element: `<${h.tagName.toLowerCase()}>${h.textContent?.slice(0, 30)}</${h.tagName.toLowerCase()}>`,
90
+ help: 'Maintain proper heading hierarchy without skipping levels',
91
+ })
92
+ }
93
+ prevLevel = level
94
+ }
95
+
96
+ // Check for multiple h1
97
+ const h1s = document.querySelectorAll('h1')
98
+ if (h1s.length > 1) {
99
+ found.push({
100
+ id: `a11y_${id++}`, impact: 'moderate',
101
+ description: `Multiple h1 elements found (${h1s.length})`,
102
+ element: '<h1> Γ— ' + h1s.length,
103
+ help: 'Use only one h1 per page',
104
+ })
105
+ }
106
+
107
+ // Check links without href
108
+ document.querySelectorAll('a:not([href])').forEach(el => {
109
+ found.push({
110
+ id: `a11y_${id++}`, impact: 'minor',
111
+ description: 'Link without href attribute',
112
+ element: el.outerHTML.slice(0, 60),
113
+ help: 'Add href or use a <button> instead',
114
+ })
115
+ })
116
+
117
+ setIssues(found)
118
+ setIsScanning(false)
119
+ }, [])
120
+
121
+ const filtered = useMemo(() =>
122
+ filter === 'all' ? issues : issues.filter(i => i.impact === filter)
123
+ , [issues, filter])
124
+
125
+ const impactColors: Record<string, string> = {
126
+ critical: '#ef4444', serious: '#f59e0b', moderate: '#3b82f6', minor: '#64748b',
127
+ }
128
+
129
+ return (
130
+ <div style={{ fontFamily: 'system-ui', fontSize: 13, color: '#e2e8f0', background: '#0f172a', borderRadius: 8, overflow: 'hidden' }}>
131
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px 12px', background: '#1e293b', borderBottom: '1px solid #334155' }}>
132
+ <span style={{ fontWeight: 700, fontSize: 13 }}>β™Ώ Accessibility Auditor</span>
133
+ <div style={{ display: 'flex', gap: 6 }}>
134
+ <select value={filter} onChange={e => setFilter(e.target.value)} style={{ padding: '4px 6px', borderRadius: 4, border: '1px solid #334155', background: '#0f172a', color: '#e2e8f0', fontSize: 11 }}>
135
+ <option value="all">All</option>
136
+ <option value="critical">Critical</option>
137
+ <option value="serious">Serious</option>
138
+ <option value="moderate">Moderate</option>
139
+ <option value="minor">Minor</option>
140
+ </select>
141
+ <button onClick={runAudit} disabled={isScanning} style={{ padding: '4px 10px', borderRadius: 4, border: '1px solid #334155', background: isScanning ? '#334155' : '#0f172a', color: '#e2e8f0', cursor: 'pointer', fontSize: 11, fontWeight: 600 }}>
142
+ {isScanning ? 'Scanning...' : 'πŸ” Scan'}
143
+ </button>
144
+ </div>
145
+ </div>
146
+ <div style={{ maxHeight: 350, overflow: 'auto', padding: 4 }}>
147
+ {issues.length === 0 ? (
148
+ <div style={{ padding: 20, textAlign: 'center', color: '#64748b' }}>
149
+ {isScanning ? 'Scanning DOM...' : 'Click Scan to run accessibility audit'}
150
+ </div>
151
+ ) : filtered.map(issue => (
152
+ <div key={issue.id} style={{ padding: '6px 10px', borderBottom: '1px solid #1e293b' }}>
153
+ <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
154
+ <span style={{ padding: '1px 6px', borderRadius: 4, fontSize: 9, fontWeight: 700, background: impactColors[issue.impact], color: 'white', textTransform: 'uppercase' }}>{issue.impact}</span>
155
+ <span style={{ fontWeight: 600 }}>{issue.description}</span>
156
+ </div>
157
+ <div style={{ fontSize: 11, color: '#64748b', marginTop: 2 }}>
158
+ <code style={{ color: '#94a3b8' }}>{issue.element}</code>
159
+ </div>
160
+ <div style={{ fontSize: 11, color: '#94a3b8', marginTop: 2 }}>πŸ’‘ {issue.help}</div>
161
+ </div>
162
+ ))}
163
+ </div>
164
+ <div style={{ padding: '6px 12px', borderTop: '1px solid #334155', color: '#64748b', fontSize: 10 }}>
165
+ {issues.length} issues: {issues.filter(i => i.impact === 'critical').length} critical, {issues.filter(i => i.impact === 'serious').length} serious
166
+ </div>
167
+ </div>
168
+ )
169
+ }
170
+
171
+ // ─── Responsive Layout Tester ────────────────────────────────
172
+
173
+ interface Viewport { name: string; width: number; height: number; icon: string }
174
+
175
+ const VIEWPORTS: Viewport[] = [
176
+ { name: 'Mobile S', width: 320, height: 568, icon: 'πŸ“±' },
177
+ { name: 'Mobile M', width: 375, height: 812, icon: 'πŸ“±' },
178
+ { name: 'Mobile L', width: 425, height: 896, icon: 'πŸ“±' },
179
+ { name: 'Tablet', width: 768, height: 1024, icon: 'πŸ“‹' },
180
+ { name: 'Laptop', width: 1024, height: 768, icon: 'πŸ’»' },
181
+ { name: 'Desktop', width: 1440, height: 900, icon: 'πŸ–₯' },
182
+ { name: '4K', width: 2560, height: 1440, icon: 'πŸ–₯' },
183
+ ]
184
+
185
+ /**
186
+ * Responsive Layout Tester β€” Multi-viewport preview.
187
+ */
188
+ export function ResponsiveTester() {
189
+ const [selectedViewport, setSelectedViewport] = useState<Viewport | null>(null)
190
+ const [originalSize, setOriginalSize] = useState<{ width: number; height: number } | null>(null)
191
+
192
+ const applyViewport = useCallback((vp: Viewport | null) => {
193
+ if (vp) {
194
+ if (!originalSize) {
195
+ setOriginalSize({ width: window.innerWidth, height: window.innerHeight })
196
+ }
197
+ // Can't resize window from JS, but we can add visual indicators
198
+ document.documentElement.style.setProperty('--viewport-width', `${vp.width}px`)
199
+ setSelectedViewport(vp)
200
+ } else {
201
+ document.documentElement.style.removeProperty('--viewport-width')
202
+ setSelectedViewport(null)
203
+ }
204
+ }, [originalSize])
205
+
206
+ return (
207
+ <div style={{ fontFamily: 'system-ui', fontSize: 13, color: '#e2e8f0', background: '#0f172a', borderRadius: 8, overflow: 'hidden' }}>
208
+ <div style={{ padding: '8px 12px', background: '#1e293b', borderBottom: '1px solid #334155', fontWeight: 700, fontSize: 13 }}>
209
+ πŸ“ Responsive Tester
210
+ </div>
211
+ <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', padding: 10 }}>
212
+ {VIEWPORTS.map(vp => (
213
+ <button
214
+ key={vp.name}
215
+ onClick={() => applyViewport(selectedViewport?.name === vp.name ? null : vp)}
216
+ style={{
217
+ padding: '6px 10px', borderRadius: 6, fontSize: 11, cursor: 'pointer', fontWeight: 600,
218
+ border: selectedViewport?.name === vp.name ? '2px solid #6366f1' : '1px solid #334155',
219
+ background: selectedViewport?.name === vp.name ? '#1e1b4b' : '#1e293b',
220
+ color: '#e2e8f0',
221
+ }}
222
+ >
223
+ {vp.icon} {vp.name} <span style={{ fontSize: 9, color: '#64748b' }}>{vp.width}Γ—{vp.height}</span>
224
+ </button>
225
+ ))}
226
+ </div>
227
+ {selectedViewport && (
228
+ <div style={{ padding: '6px 12px', borderTop: '1px solid #334155', color: '#64748b', fontSize: 10 }}>
229
+ Active: {selectedViewport.name} ({selectedViewport.width}Γ—{selectedViewport.height})
230
+ </div>
231
+ )}
232
+ </div>
233
+ )
234
+ }
@@ -0,0 +1,104 @@
1
+ import { useState, useCallback} from 'react'
2
+
3
+ // ─── Types ────────────────────────────────────────────────────
4
+
5
+ interface FeatureFlag {
6
+ key: string
7
+ label: string
8
+ description?: string
9
+ enabled: boolean
10
+ category?: string
11
+ }
12
+
13
+ interface FeatureFlagsToggleProps {
14
+ flags: FeatureFlag[]
15
+ onToggle: (key: string, enabled: boolean) => void | Promise<void>
16
+ }
17
+
18
+ // ─── Component ────────────────────────────────────────────────
19
+
20
+ /**
21
+ * Feature Flags Toggle Panel β€” Instant flag switching for dev mode.
22
+ * Shows all flags with category grouping and instant toggle.
23
+ */
24
+ export function FeatureFlagsToggle({ flags, onToggle }: FeatureFlagsToggleProps) {
25
+ const [filter, setFilter] = useState('')
26
+ const [togglingKey, setTogglingKey] = useState<string | null>(null)
27
+
28
+ const handleToggle = useCallback(async (flag: FeatureFlag) => {
29
+ setTogglingKey(flag.key)
30
+ try {
31
+ await onToggle(flag.key, !flag.enabled)
32
+ } finally {
33
+ setTogglingKey(null)
34
+ }
35
+ }, [onToggle])
36
+
37
+ const filtered = filter
38
+ ? flags.filter(f => f.key.toLowerCase().includes(filter.toLowerCase()) || f.label.toLowerCase().includes(filter.toLowerCase()))
39
+ : flags
40
+
41
+ // Group by category
42
+ const groups = new Map<string, FeatureFlag[]>()
43
+ for (const f of filtered) {
44
+ const cat = f.category ?? 'General'
45
+ if (!groups.has(cat)) groups.set(cat, [])
46
+ groups.get(cat)!.push(f)
47
+ }
48
+
49
+ return (
50
+ <div style={{ fontFamily: 'system-ui', fontSize: 13, color: '#e2e8f0', background: '#0f172a', borderRadius: 8, overflow: 'hidden' }}>
51
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px 12px', background: '#1e293b', borderBottom: '1px solid #334155' }}>
52
+ <span style={{ fontWeight: 700, fontSize: 13 }}>🚩 Feature Flags</span>
53
+ <input
54
+ placeholder="Filter flags..."
55
+ value={filter}
56
+ onChange={e => setFilter(e.target.value)}
57
+ style={{ padding: '4px 8px', borderRadius: 4, border: '1px solid #334155', background: '#0f172a', color: '#e2e8f0', fontSize: 11, width: 140 }}
58
+ />
59
+ </div>
60
+ <div style={{ maxHeight: 350, overflow: 'auto', padding: 8 }}>
61
+ {[...groups.entries()].map(([category, categoryFlags]) => (
62
+ <div key={category}>
63
+ <div style={{ padding: '6px 8px', color: '#64748b', fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: 1 }}>
64
+ {category}
65
+ </div>
66
+ {categoryFlags.map(flag => (
67
+ <div
68
+ key={flag.key}
69
+ style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '6px 8px', borderBottom: '1px solid #1e293b' }}
70
+ >
71
+ <div>
72
+ <div style={{ fontWeight: 600 }}>{flag.label}</div>
73
+ <div style={{ fontSize: 11, color: '#64748b' }}>
74
+ <code style={{ color: '#94a3b8' }}>{flag.key}</code>
75
+ {flag.description && ` β€” ${flag.description}`}
76
+ </div>
77
+ </div>
78
+ <button
79
+ onClick={() => handleToggle(flag)}
80
+ disabled={togglingKey === flag.key}
81
+ style={{
82
+ width: 44, height: 24, borderRadius: 12, border: 'none', cursor: 'pointer',
83
+ background: flag.enabled ? '#22c55e' : '#334155',
84
+ position: 'relative', transition: 'background 0.2s',
85
+ }}
86
+ >
87
+ <div style={{
88
+ width: 18, height: 18, borderRadius: '50%', background: 'white',
89
+ position: 'absolute', top: 3,
90
+ left: flag.enabled ? 23 : 3,
91
+ transition: 'left 0.2s',
92
+ }} />
93
+ </button>
94
+ </div>
95
+ ))}
96
+ </div>
97
+ ))}
98
+ </div>
99
+ <div style={{ padding: '6px 12px', borderTop: '1px solid #334155', color: '#64748b', fontSize: 10 }}>
100
+ {flags.filter(f => f.enabled).length}/{flags.length} enabled
101
+ </div>
102
+ </div>
103
+ )
104
+ }
@@ -0,0 +1,152 @@
1
+ import { useState, useEffect, useCallback, useRef } from 'react'
2
+
3
+ // ─── Types ────────────────────────────────────────────────────
4
+
5
+ interface Route {
6
+ path: string
7
+ label: string
8
+ icon?: string
9
+ group?: string
10
+ }
11
+
12
+ interface QuickRouteJumpProps {
13
+ routes: Route[]
14
+ onNavigate: (path: string) => void
15
+ hotkey?: string // Default: 'Mod+K'
16
+ }
17
+
18
+ // ─── Component ────────────────────────────────────────────────
19
+
20
+ /**
21
+ * Quick Route Jump β€” Command palette for instant navigation.
22
+ * Opens with Cmd/Ctrl+K, search routes, arrow key navigation.
23
+ */
24
+ export function QuickRouteJump({ routes, onNavigate, hotkey = 'Mod+K' }: QuickRouteJumpProps) {
25
+ const [isOpen, setIsOpen] = useState(false)
26
+ const [query, setQuery] = useState('')
27
+ const [selectedIndex, setSelectedIndex] = useState(0)
28
+ const inputRef = useRef<HTMLInputElement>(null)
29
+
30
+ // Keyboard shortcut to open
31
+ useEffect(() => {
32
+ const handler = (e: KeyboardEvent) => {
33
+ const isMod = e.metaKey || e.ctrlKey
34
+ if (isMod && e.key.toLowerCase() === 'k') {
35
+ e.preventDefault()
36
+ setIsOpen(prev => !prev)
37
+ setQuery('')
38
+ setSelectedIndex(0)
39
+ }
40
+ if (e.key === 'Escape') setIsOpen(false)
41
+ }
42
+ window.addEventListener('keydown', handler)
43
+ return () => window.removeEventListener('keydown', handler)
44
+ }, [])
45
+
46
+ // Focus input when opened
47
+ useEffect(() => {
48
+ if (isOpen) setTimeout(() => inputRef.current?.focus(), 50)
49
+ }, [isOpen])
50
+
51
+ const filtered = query
52
+ ? routes.filter(r =>
53
+ r.path.toLowerCase().includes(query.toLowerCase()) ||
54
+ r.label.toLowerCase().includes(query.toLowerCase())
55
+ )
56
+ : routes
57
+
58
+ const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
59
+ if (e.key === 'ArrowDown') {
60
+ e.preventDefault()
61
+ setSelectedIndex(i => Math.min(i + 1, filtered.length - 1))
62
+ } else if (e.key === 'ArrowUp') {
63
+ e.preventDefault()
64
+ setSelectedIndex(i => Math.max(i - 1, 0))
65
+ } else if (e.key === 'Enter' && filtered[selectedIndex]) {
66
+ e.preventDefault()
67
+ onNavigate(filtered[selectedIndex].path)
68
+ setIsOpen(false)
69
+ }
70
+ }, [filtered, selectedIndex, onNavigate])
71
+
72
+ if (!isOpen) return null
73
+
74
+ // Group routes
75
+ const groups = new Map<string, Route[]>()
76
+ for (const r of filtered) {
77
+ const g = r.group ?? 'Routes'
78
+ if (!groups.has(g)) groups.set(g, [])
79
+ groups.get(g)!.push(r)
80
+ }
81
+
82
+ let globalIdx = -1
83
+
84
+ return (
85
+ <>
86
+ {/* Backdrop */}
87
+ <div onClick={() => setIsOpen(false)} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.6)', zIndex: 9998 }} />
88
+ {/* Panel */}
89
+ <div style={{
90
+ position: 'fixed', top: '20%', left: '50%', transform: 'translateX(-50%)',
91
+ width: '100%', maxWidth: 520, background: '#0f172a', border: '1px solid #334155',
92
+ borderRadius: 12, overflow: 'hidden', zIndex: 9999,
93
+ boxShadow: '0 25px 50px rgba(0,0,0,0.4)',
94
+ }}>
95
+ <div style={{ padding: 12, borderBottom: '1px solid #334155' }}>
96
+ <input
97
+ ref={inputRef}
98
+ value={query}
99
+ onChange={e => { setQuery(e.target.value); setSelectedIndex(0) }}
100
+ onKeyDown={handleKeyDown}
101
+ placeholder="Jump to route..."
102
+ style={{
103
+ width: '100%', padding: '10px 12px', borderRadius: 8, border: '1px solid #334155',
104
+ background: '#1e293b', color: '#e2e8f0', fontSize: 14, outline: 'none',
105
+ }}
106
+ />
107
+ </div>
108
+ <div style={{ maxHeight: 350, overflow: 'auto', padding: 4 }}>
109
+ {filtered.length === 0 ? (
110
+ <div style={{ padding: 24, textAlign: 'center', color: '#64748b' }}>No routes match "{query}"</div>
111
+ ) : [...groups.entries()].map(([group, groupRoutes]) => (
112
+ <div key={group}>
113
+ <div style={{ padding: '8px 12px', color: '#64748b', fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: 1 }}>
114
+ {group}
115
+ </div>
116
+ {groupRoutes.map(route => {
117
+ globalIdx++
118
+ const idx = globalIdx
119
+ return (
120
+ <div
121
+ key={route.path}
122
+ onClick={() => { onNavigate(route.path); setIsOpen(false) }}
123
+ style={{
124
+ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 12px',
125
+ borderRadius: 6, cursor: 'pointer',
126
+ background: idx === selectedIndex ? '#1e293b' : 'transparent',
127
+ }}
128
+ onMouseEnter={() => setSelectedIndex(idx)}
129
+ >
130
+ <span style={{ fontSize: 16 }}>{route.icon ?? 'πŸ“„'}</span>
131
+ <div style={{ flex: 1 }}>
132
+ <div style={{ fontWeight: 600, color: '#e2e8f0', fontSize: 13 }}>{route.label}</div>
133
+ <div style={{ fontSize: 11, color: '#64748b' }}>{route.path}</div>
134
+ </div>
135
+ {idx === selectedIndex && (
136
+ <span style={{ fontSize: 10, color: '#64748b', border: '1px solid #334155', padding: '2px 6px', borderRadius: 4 }}>↡</span>
137
+ )}
138
+ </div>
139
+ )
140
+ })}
141
+ </div>
142
+ ))}
143
+ </div>
144
+ <div style={{ padding: '8px 12px', borderTop: '1px solid #334155', display: 'flex', gap: 12, color: '#64748b', fontSize: 10 }}>
145
+ <span>↑↓ Navigate</span>
146
+ <span>↡ Open</span>
147
+ <span>Esc Close</span>
148
+ </div>
149
+ </div>
150
+ </>
151
+ )
152
+ }