specproof 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.
@@ -0,0 +1,387 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+
5
+ import {
6
+ Accordion,
7
+ AccordionContent,
8
+ AccordionItem,
9
+ AccordionTrigger
10
+ } from '@/components/ui/accordion';
11
+ import {
12
+ Sheet,
13
+ SheetContent,
14
+ SheetDescription,
15
+ SheetHeader,
16
+ SheetTitle
17
+ } from '@/components/ui/sheet';
18
+ import type {
19
+ CoverageReport,
20
+ OperationCoverage,
21
+ StatusCoverage,
22
+ TagCoverage,
23
+ TestSnippet
24
+ } from '@/lib/api-test-coverage';
25
+
26
+ // ============================================================================
27
+ // Helpers
28
+ // ============================================================================
29
+
30
+ function verdictOf(status: StatusCoverage): 'ok' | 'gap' | 'undoc' {
31
+ if (!status.documented) return 'undoc';
32
+ return status.assertions > 0 ? 'ok' : 'gap';
33
+ }
34
+
35
+ /** Render {slug} path segments in a muted tone so parameters read apart */
36
+ function PathInk({ specPath }: { specPath: string }) {
37
+ const parts = specPath.split(/(\{[^}]+\})/g).filter(Boolean);
38
+ return (
39
+ <span className="text-sm tracking-tight">
40
+ {parts.map((part, i) =>
41
+ part.startsWith('{') ? (
42
+ <em key={i} className="not-italic text-muted-foreground">
43
+ {part}
44
+ </em>
45
+ ) : (
46
+ <span key={i}>{part}</span>
47
+ )
48
+ )}
49
+ </span>
50
+ );
51
+ }
52
+
53
+ /** What the code panel is showing: one status of one operation */
54
+ interface Evidence {
55
+ operation: OperationCoverage;
56
+ status: StatusCoverage;
57
+ }
58
+
59
+ // ============================================================================
60
+ // Test-code panel
61
+ // ============================================================================
62
+
63
+ function SnippetBlock({ snippet, code }: { snippet: TestSnippet; code: string }) {
64
+ const hitRe = new RegExp(`\\.status\\)\\.(?:toBe|toEqual)\\(\\s*${code}\\s*\\)`);
65
+ const lines = snippet.source.split('\n');
66
+ return (
67
+ <figure className="flex flex-col gap-2">
68
+ <figcaption className="flex items-baseline gap-3">
69
+ <span className="text-xs font-medium">{snippet.title}</span>
70
+ <span className="sp-leader" aria-hidden />
71
+ <span className="shrink-0 text-[0.65rem] text-muted-foreground">L{snippet.startLine}</span>
72
+ </figcaption>
73
+ <pre className="sp-codeblock py-2">
74
+ {lines.map((line, i) => (
75
+ <div key={i} className="sp-codeline" data-hit={hitRe.test(line) ? '' : undefined}>
76
+ <span className="sp-lineno">{snippet.startLine + i}</span>
77
+ <code>{line || ' '}</code>
78
+ </div>
79
+ ))}
80
+ </pre>
81
+ </figure>
82
+ );
83
+ }
84
+
85
+ function EvidencePanel({
86
+ evidence,
87
+ onClose
88
+ }: {
89
+ evidence: Evidence | null;
90
+ onClose: () => void;
91
+ }) {
92
+ return (
93
+ <Sheet open={evidence !== null} onOpenChange={(open) => !open && onClose()}>
94
+ <SheetContent className="proof-root w-full overflow-y-auto border-l border-[var(--sp-hair-strong)] sm:max-w-2xl">
95
+ {evidence && (
96
+ <>
97
+ <SheetHeader className="text-left">
98
+ <SheetTitle className="flex items-baseline gap-3 font-normal">
99
+ <span
100
+ className="sp-method shrink-0 uppercase"
101
+ data-method={evidence.operation.method}
102
+ >
103
+ {evidence.operation.method}
104
+ </span>
105
+ <span className="font-mono text-sm">{evidence.operation.specPath}</span>
106
+ <span className="sp-code text-lg font-semibold" data-class={evidence.status.code[0]}>
107
+ {evidence.status.code}
108
+ </span>
109
+ </SheetTitle>
110
+ <SheetDescription className="font-mono text-xs">
111
+ {evidence.status.documented ? (
112
+ evidence.status.description
113
+ ) : (
114
+ <span className="text-[var(--sp-undoc)]">
115
+ Asserted in tests but absent from the OpenAPI spec — document it or drop it.
116
+ </span>
117
+ )}
118
+ </SheetDescription>
119
+ </SheetHeader>
120
+
121
+ <div className="mt-6 flex flex-col gap-8">
122
+ {evidence.status.snippets.map((snippet) => (
123
+ <SnippetBlock key={snippet.startLine} snippet={snippet} code={evidence.status.code} />
124
+ ))}
125
+ </div>
126
+
127
+ {evidence.operation.testFile && (
128
+ <p className="mt-8 border-t border-dashed pt-3 text-[0.65rem] tracking-[0.14em] text-muted-foreground">
129
+ SOURCE · {evidence.operation.testFile.toUpperCase()}
130
+ </p>
131
+ )}
132
+ </>
133
+ )}
134
+ </SheetContent>
135
+ </Sheet>
136
+ );
137
+ }
138
+
139
+ // ============================================================================
140
+ // Status list (accordion body)
141
+ // ============================================================================
142
+
143
+ function StatusList({
144
+ operation,
145
+ onShowEvidence
146
+ }: {
147
+ operation: OperationCoverage;
148
+ onShowEvidence: (evidence: Evidence) => void;
149
+ }) {
150
+ return (
151
+ <div className="flex flex-col gap-0 pl-[calc(0.6rem+3px)]">
152
+ {operation.statuses.map((status) => {
153
+ const verdict = verdictOf(status);
154
+ const hasEvidence = status.snippets.length > 0;
155
+ return (
156
+ <div key={status.code} className="flex items-baseline gap-3 py-1.5">
157
+ <span
158
+ className="sp-code w-8 shrink-0 text-sm font-semibold"
159
+ data-class={status.code[0]}
160
+ >
161
+ {status.code}
162
+ </span>
163
+ <span className="text-xs text-muted-foreground">
164
+ {status.documented
165
+ ? status.description
166
+ : 'asserted in tests, but absent from the OpenAPI spec'}
167
+ </span>
168
+ <span className="sp-leader" aria-hidden />
169
+ {verdict === 'ok' && (
170
+ <span className="shrink-0 text-[0.68rem] text-muted-foreground">
171
+ {status.assertions} assertion{status.assertions === 1 ? '' : 's'}
172
+ </span>
173
+ )}
174
+ {hasEvidence ? (
175
+ <button
176
+ type="button"
177
+ className="sp-stamp shrink-0"
178
+ data-verdict={verdict}
179
+ title="Show the test code"
180
+ onClick={() => onShowEvidence({ operation, status })}
181
+ >
182
+ {verdict === 'ok' ? 'VERIFIED' : 'UNDOCUMENTED'} ⌕
183
+ </button>
184
+ ) : (
185
+ <span className="sp-stamp shrink-0" data-verdict={verdict}>
186
+ {verdict === 'ok' ? 'VERIFIED' : verdict === 'gap' ? 'NO TEST' : 'UNDOCUMENTED'}
187
+ </span>
188
+ )}
189
+ </div>
190
+ );
191
+ })}
192
+
193
+ <div className="mt-3 flex items-center gap-2 border-t border-dashed pt-2.5 text-[0.68rem] text-muted-foreground">
194
+ {operation.testFile && operation.testCount > 0 ? (
195
+ <span>
196
+ {operation.testCount} test{operation.testCount === 1 ? '' : 's'} in{' '}
197
+ <span className="text-foreground/70">{operation.testFile}</span>
198
+ </span>
199
+ ) : (
200
+ <span className="sp-stamp" data-verdict="gap">
201
+ NO TEST FILE — THIS OPERATION SHIPS UNTESTED
202
+ </span>
203
+ )}
204
+ </div>
205
+ </div>
206
+ );
207
+ }
208
+
209
+ // ============================================================================
210
+ // Operation row
211
+ // ============================================================================
212
+
213
+ function OperationRow({
214
+ operation,
215
+ onShowEvidence
216
+ }: {
217
+ operation: OperationCoverage;
218
+ onShowEvidence: (evidence: Evidence) => void;
219
+ }) {
220
+ const documented = operation.statuses.filter((s) => s.documented);
221
+ return (
222
+ <AccordionItem
223
+ value={`${operation.method} ${operation.specPath}`}
224
+ className="sp-oprow border-b-0"
225
+ >
226
+ <AccordionTrigger className="gap-3 px-3 py-3.5 hover:no-underline">
227
+ <span className="sp-method w-16 shrink-0 uppercase" data-method={operation.method}>
228
+ {operation.method}
229
+ </span>
230
+ <PathInk specPath={operation.specPath} />
231
+ <span className="sp-leader hidden sm:block" aria-hidden />
232
+ <span className="sp-marks shrink-0" aria-hidden>
233
+ {operation.statuses.map((status) => (
234
+ <span key={status.code} className="sp-mark" data-state={verdictOf(status)} />
235
+ ))}
236
+ </span>
237
+ <span className="w-12 shrink-0 text-right text-xs tabular-nums text-muted-foreground">
238
+ {operation.coveredCount}/{documented.length}
239
+ </span>
240
+ </AccordionTrigger>
241
+ <AccordionContent className="px-3 pb-5">
242
+ {operation.summary && (
243
+ <p className="mb-3 pl-[calc(0.6rem+3px)] text-xs text-muted-foreground">
244
+ {operation.summary}
245
+ </p>
246
+ )}
247
+ <StatusList operation={operation} onShowEvidence={onShowEvidence} />
248
+ </AccordionContent>
249
+ </AccordionItem>
250
+ );
251
+ }
252
+
253
+ // ============================================================================
254
+ // Tag section
255
+ // ============================================================================
256
+
257
+ function TagSection({
258
+ tag,
259
+ index,
260
+ onShowEvidence
261
+ }: {
262
+ tag: TagCoverage;
263
+ index: number;
264
+ onShowEvidence: (evidence: Evidence) => void;
265
+ }) {
266
+ return (
267
+ <section className="sp-rise" style={{ '--sp-stagger': index + 3 } as React.CSSProperties}>
268
+ <header className="mb-1 flex items-baseline gap-4 border-b pb-2">
269
+ <span className="text-[0.65rem] tracking-[0.2em] text-muted-foreground">
270
+ {String(index + 1).padStart(2, '0')}
271
+ </span>
272
+ <h2 className="text-sm font-semibold uppercase tracking-[0.08em]">{tag.tag}</h2>
273
+ <span className="hidden text-xs text-muted-foreground sm:block">{tag.description}</span>
274
+ <span className="sp-leader" aria-hidden />
275
+ <span className="text-sm tabular-nums text-muted-foreground">
276
+ <span className="text-foreground">{tag.coveredCount}</span>/{tag.totalCount} verified
277
+ </span>
278
+ </header>
279
+ <Accordion type="multiple" className="divide-y divide-[var(--sp-hair)]">
280
+ {tag.operations.map((operation) => (
281
+ <OperationRow
282
+ key={`${operation.method} ${operation.specPath}`}
283
+ operation={operation}
284
+ onShowEvidence={onShowEvidence}
285
+ />
286
+ ))}
287
+ </Accordion>
288
+ </section>
289
+ );
290
+ }
291
+
292
+ // ============================================================================
293
+ // Proof
294
+ // ============================================================================
295
+
296
+ export function CoverageProof({
297
+ report,
298
+ compiledAt
299
+ }: {
300
+ report: CoverageReport;
301
+ compiledAt: string;
302
+ }) {
303
+ const [evidence, setEvidence] = useState<Evidence | null>(null);
304
+ const gapCount = report.totalCount - report.coveredCount;
305
+ const facts: Array<[string, string]> = [
306
+ ['operations', String(report.operationCount)],
307
+ ['status pairs verified', `${report.coveredCount}/${report.totalCount}`],
308
+ ['gaps', String(gapCount)],
309
+ ['untested routes', String(report.untestedOperations)]
310
+ ];
311
+
312
+ return (
313
+ <div className="proof-root proof-page min-h-screen">
314
+ <div className="mx-auto flex max-w-4xl flex-col gap-14 px-6 py-16">
315
+ {/* masthead */}
316
+ <header className="sp-rise" style={{ '--sp-stagger': 0 } as React.CSSProperties}>
317
+ <div className="flex flex-wrap items-end justify-between gap-6">
318
+ <h1 className="text-2xl font-semibold tracking-tight">SpecProof</h1>
319
+ <div className="text-right">
320
+ <div className="text-5xl font-semibold tabular-nums leading-none">
321
+ {Math.round((report.coveredCount / Math.max(report.totalCount, 1)) * 100)}
322
+ <span className="text-2xl font-normal text-muted-foreground">%</span>
323
+ </div>
324
+ <div className="mt-2 text-[0.65rem] tracking-[0.14em] text-muted-foreground">
325
+ DOCUMENTED RESPONSES VERIFIED
326
+ </div>
327
+ </div>
328
+ </div>
329
+ </header>
330
+
331
+ {/* summary strip */}
332
+ <dl
333
+ className="sp-rise sp-rule-double flex flex-wrap items-baseline gap-x-10 gap-y-3 border-b py-3"
334
+ style={{ '--sp-stagger': 1 } as React.CSSProperties}
335
+ >
336
+ {facts.map(([label, value]) => (
337
+ <div key={label} className="flex items-baseline gap-2.5">
338
+ <dd className="text-xl tabular-nums">{value}</dd>
339
+ <dt className="text-[0.65rem] tracking-[0.18em] text-muted-foreground">
340
+ {label.toUpperCase()}
341
+ </dt>
342
+ </div>
343
+ ))}
344
+ <div className="ml-auto text-[0.65rem] tracking-[0.18em] text-muted-foreground">
345
+ COMPILED {new Date(compiledAt).toISOString().slice(0, 10)}
346
+ </div>
347
+ </dl>
348
+
349
+ {/* legend */}
350
+ <div
351
+ className="sp-rise -mt-8 flex flex-wrap items-center gap-x-6 gap-y-2 text-[0.65rem] tracking-[0.14em] text-muted-foreground"
352
+ style={{ '--sp-stagger': 2 } as React.CSSProperties}
353
+ >
354
+ <span className="flex items-center gap-2">
355
+ <span className="sp-mark" data-state="ok" /> VERIFIED
356
+ </span>
357
+ <span className="flex items-center gap-2">
358
+ <span className="sp-mark" data-state="gap" /> NO TEST
359
+ </span>
360
+ <span className="flex items-center gap-2">
361
+ <span className="sp-mark" data-state="undoc" /> UNDOCUMENTED
362
+ </span>
363
+ </div>
364
+
365
+ {/* tag sections */}
366
+ {report.operationCount === 0 ? (
367
+ <section
368
+ className="sp-rise border border-dashed px-6 py-10 text-center"
369
+ style={{ '--sp-stagger': 3 } as React.CSSProperties}
370
+ >
371
+ <p className="text-sm font-semibold tracking-[0.14em]">NO API DEFINITION PROVIDED</p>
372
+ <div className="mt-4 flex flex-col gap-1 text-xs text-muted-foreground">
373
+ <code className="text-foreground/70">SPECPROOF_REPO=/path/to/repo</code>
374
+ <code className="text-foreground/70">bun run generate:proof</code>
375
+ </div>
376
+ </section>
377
+ ) : (
378
+ report.tags.map((tag, i) => (
379
+ <TagSection key={tag.tag} tag={tag} index={i} onShowEvidence={setEvidence} />
380
+ ))
381
+ )}
382
+ </div>
383
+
384
+ <EvidencePanel evidence={evidence} onClose={() => setEvidence(null)} />
385
+ </div>
386
+ );
387
+ }
@@ -0,0 +1,57 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as AccordionPrimitive from "@radix-ui/react-accordion"
5
+ import { ChevronDown } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ const Accordion = AccordionPrimitive.Root
10
+
11
+ const AccordionItem = React.forwardRef<
12
+ React.ElementRef<typeof AccordionPrimitive.Item>,
13
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
14
+ >(({ className, ...props }, ref) => (
15
+ <AccordionPrimitive.Item
16
+ ref={ref}
17
+ className={cn("border-b", className)}
18
+ {...props}
19
+ />
20
+ ))
21
+ AccordionItem.displayName = "AccordionItem"
22
+
23
+ const AccordionTrigger = React.forwardRef<
24
+ React.ElementRef<typeof AccordionPrimitive.Trigger>,
25
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
26
+ >(({ className, children, ...props }, ref) => (
27
+ <AccordionPrimitive.Header className="flex">
28
+ <AccordionPrimitive.Trigger
29
+ ref={ref}
30
+ className={cn(
31
+ "flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline text-left [&[data-state=open]>svg]:rotate-180",
32
+ className
33
+ )}
34
+ {...props}
35
+ >
36
+ {children}
37
+ <ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
38
+ </AccordionPrimitive.Trigger>
39
+ </AccordionPrimitive.Header>
40
+ ))
41
+ AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
42
+
43
+ const AccordionContent = React.forwardRef<
44
+ React.ElementRef<typeof AccordionPrimitive.Content>,
45
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
46
+ >(({ className, children, ...props }, ref) => (
47
+ <AccordionPrimitive.Content
48
+ ref={ref}
49
+ className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
50
+ {...props}
51
+ >
52
+ <div className={cn("pb-4 pt-0", className)}>{children}</div>
53
+ </AccordionPrimitive.Content>
54
+ ))
55
+ AccordionContent.displayName = AccordionPrimitive.Content.displayName
56
+
57
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
@@ -0,0 +1,140 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as SheetPrimitive from "@radix-ui/react-dialog"
5
+ import { cva, type VariantProps } from "class-variance-authority"
6
+ import { X } from "lucide-react"
7
+
8
+ import { cn } from "@/lib/utils"
9
+
10
+ const Sheet = SheetPrimitive.Root
11
+
12
+ const SheetTrigger = SheetPrimitive.Trigger
13
+
14
+ const SheetClose = SheetPrimitive.Close
15
+
16
+ const SheetPortal = SheetPrimitive.Portal
17
+
18
+ const SheetOverlay = React.forwardRef<
19
+ React.ElementRef<typeof SheetPrimitive.Overlay>,
20
+ React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
21
+ >(({ className, ...props }, ref) => (
22
+ <SheetPrimitive.Overlay
23
+ className={cn(
24
+ "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
25
+ className
26
+ )}
27
+ {...props}
28
+ ref={ref}
29
+ />
30
+ ))
31
+ SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
32
+
33
+ const sheetVariants = cva(
34
+ "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
35
+ {
36
+ variants: {
37
+ side: {
38
+ top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39
+ bottom:
40
+ "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
41
+ left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
42
+ right:
43
+ "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
44
+ },
45
+ },
46
+ defaultVariants: {
47
+ side: "right",
48
+ },
49
+ }
50
+ )
51
+
52
+ interface SheetContentProps
53
+ extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
54
+ VariantProps<typeof sheetVariants> {}
55
+
56
+ const SheetContent = React.forwardRef<
57
+ React.ElementRef<typeof SheetPrimitive.Content>,
58
+ SheetContentProps
59
+ >(({ side = "right", className, children, ...props }, ref) => (
60
+ <SheetPortal>
61
+ <SheetOverlay />
62
+ <SheetPrimitive.Content
63
+ ref={ref}
64
+ className={cn(sheetVariants({ side }), className)}
65
+ {...props}
66
+ >
67
+ <SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
68
+ <X className="h-4 w-4" />
69
+ <span className="sr-only">Close</span>
70
+ </SheetPrimitive.Close>
71
+ {children}
72
+ </SheetPrimitive.Content>
73
+ </SheetPortal>
74
+ ))
75
+ SheetContent.displayName = SheetPrimitive.Content.displayName
76
+
77
+ const SheetHeader = ({
78
+ className,
79
+ ...props
80
+ }: React.HTMLAttributes<HTMLDivElement>) => (
81
+ <div
82
+ className={cn(
83
+ "flex flex-col space-y-2 text-center sm:text-left",
84
+ className
85
+ )}
86
+ {...props}
87
+ />
88
+ )
89
+ SheetHeader.displayName = "SheetHeader"
90
+
91
+ const SheetFooter = ({
92
+ className,
93
+ ...props
94
+ }: React.HTMLAttributes<HTMLDivElement>) => (
95
+ <div
96
+ className={cn(
97
+ "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
98
+ className
99
+ )}
100
+ {...props}
101
+ />
102
+ )
103
+ SheetFooter.displayName = "SheetFooter"
104
+
105
+ const SheetTitle = React.forwardRef<
106
+ React.ElementRef<typeof SheetPrimitive.Title>,
107
+ React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
108
+ >(({ className, ...props }, ref) => (
109
+ <SheetPrimitive.Title
110
+ ref={ref}
111
+ className={cn("text-lg font-semibold text-foreground", className)}
112
+ {...props}
113
+ />
114
+ ))
115
+ SheetTitle.displayName = SheetPrimitive.Title.displayName
116
+
117
+ const SheetDescription = React.forwardRef<
118
+ React.ElementRef<typeof SheetPrimitive.Description>,
119
+ React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
120
+ >(({ className, ...props }, ref) => (
121
+ <SheetPrimitive.Description
122
+ ref={ref}
123
+ className={cn("text-sm text-muted-foreground", className)}
124
+ {...props}
125
+ />
126
+ ))
127
+ SheetDescription.displayName = SheetPrimitive.Description.displayName
128
+
129
+ export {
130
+ Sheet,
131
+ SheetPortal,
132
+ SheetOverlay,
133
+ SheetTrigger,
134
+ SheetClose,
135
+ SheetContent,
136
+ SheetHeader,
137
+ SheetFooter,
138
+ SheetTitle,
139
+ SheetDescription,
140
+ }
@@ -0,0 +1,43 @@
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+ import tseslint from "typescript-eslint";
4
+ import pluginReact from "eslint-plugin-react";
5
+ import pluginReactHooks from "eslint-plugin-react-hooks";
6
+
7
+ export default [
8
+ {
9
+ ignores: ["**/.next/**", "**/node_modules/**", "**/dist/**", "**/build/**", "**/coverage/**"],
10
+ },
11
+ {
12
+ files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
13
+ languageOptions: {
14
+ globals: {
15
+ ...globals.browser,
16
+ ...globals.node,
17
+ },
18
+ },
19
+ },
20
+ js.configs.recommended,
21
+ ...tseslint.configs.recommended,
22
+ {
23
+ plugins: {
24
+ react: pluginReact,
25
+ "react-hooks": pluginReactHooks,
26
+ },
27
+ rules: {
28
+ ...pluginReact.configs.recommended.rules,
29
+ ...pluginReactHooks.configs.recommended.rules,
30
+ // Disable React JSX scope requirement for Next.js
31
+ "react/react-in-jsx-scope": "off",
32
+ // Disable prop-types for TypeScript projects
33
+ "react/prop-types": "off",
34
+ // Allow unused variables that start with underscore
35
+ "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
36
+ },
37
+ settings: {
38
+ react: {
39
+ version: "detect",
40
+ },
41
+ },
42
+ },
43
+ ];