dslinter 0.0.6

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 (63) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/LICENSE +201 -0
  3. package/README.md +104 -0
  4. package/bin/dslinter.mjs +29 -0
  5. package/components.json +20 -0
  6. package/package.json +90 -0
  7. package/src/components/InlineCode.tsx +5 -0
  8. package/src/components/icons.tsx +121 -0
  9. package/src/components/ui/badge.tsx +52 -0
  10. package/src/components/ui/button.tsx +57 -0
  11. package/src/components/ui/checkbox.tsx +25 -0
  12. package/src/components/ui/command.tsx +183 -0
  13. package/src/components/ui/dialog.tsx +156 -0
  14. package/src/components/ui/hover-card.tsx +42 -0
  15. package/src/components/ui/input.tsx +22 -0
  16. package/src/components/ui/label.tsx +19 -0
  17. package/src/components/ui/select.tsx +149 -0
  18. package/src/components/ui/table.tsx +118 -0
  19. package/src/components/ui/toggle-group.tsx +83 -0
  20. package/src/components/ui/toggle.tsx +45 -0
  21. package/src/dashboard/ComponentCatalog.tsx +210 -0
  22. package/src/dashboard/ComponentUsageDetails.tsx +109 -0
  23. package/src/dashboard/DashboardBody.tsx +71 -0
  24. package/src/dashboard/FindingsList.tsx +151 -0
  25. package/src/dashboard/ScoreStrip.tsx +28 -0
  26. package/src/dashboard/TokenWall.tsx +241 -0
  27. package/src/dashboard/aggregate.ts +73 -0
  28. package/src/dashboard/paths.ts +10 -0
  29. package/src/dashboard/useWorkspaceReport.ts +136 -0
  30. package/src/index.ts +67 -0
  31. package/src/lib/utils.ts +6 -0
  32. package/src/playground/definePlayground.tsx +99 -0
  33. package/src/playground/enumerateControlCombinations.test.ts +112 -0
  34. package/src/playground/enumerateControlCombinations.ts +74 -0
  35. package/src/report/a11yForModule.ts +35 -0
  36. package/src/report/codeScoreForModule.ts +41 -0
  37. package/src/report/modulePathMatch.ts +27 -0
  38. package/src/report/tokenStyleFindingsForModule.ts +24 -0
  39. package/src/shell/ComponentPlaygroundPane.tsx +438 -0
  40. package/src/shell/DashboardCommandPalette.tsx +134 -0
  41. package/src/shell/DashboardLayout.tsx +230 -0
  42. package/src/shell/EmptyCard.tsx +21 -0
  43. package/src/shell/GovernancePane.tsx +77 -0
  44. package/src/shell/PlaygroundA11yAndCode.tsx +387 -0
  45. package/src/shell/PlaygroundControlField.tsx +213 -0
  46. package/src/shell/PlaygroundControls.tsx +66 -0
  47. package/src/shell/PlaygroundUsageCode.tsx +51 -0
  48. package/src/shell/PlaygroundVariantMatrix.tsx +68 -0
  49. package/src/shell/Section.tsx +34 -0
  50. package/src/shell/Sidebar.tsx +203 -0
  51. package/src/shell/TokensPane.tsx +26 -0
  52. package/src/shell/controlApiTable.ts +53 -0
  53. package/src/shell/hashRoute.ts +49 -0
  54. package/src/shell/playgroundUsageHighlight.ts +53 -0
  55. package/src/shell/playgroundUsageTwoslash.ts +69 -0
  56. package/src/shell/useHashRoute.ts +29 -0
  57. package/src/styles/dashboard-theme.css +188 -0
  58. package/src/types/controls.ts +62 -0
  59. package/src/types/defaultTailwindTypography.ts +55 -0
  60. package/src/types/playground.ts +21 -0
  61. package/src/types/preview.ts +8 -0
  62. package/src/types/report.ts +116 -0
  63. package/src/types/tokenCatalog.ts +54 -0
@@ -0,0 +1,149 @@
1
+ import * as React from "react";
2
+ import * as SelectPrimitive from "@radix-ui/react-select";
3
+ import { cn } from "../../lib/utils";
4
+ import { IconCheck, IconChevronDown, IconChevronUp } from "../icons";
5
+
6
+ const Select = SelectPrimitive.Root;
7
+
8
+ const SelectGroup = SelectPrimitive.Group;
9
+
10
+ const SelectValue = SelectPrimitive.Value;
11
+
12
+ const SelectTrigger = React.forwardRef<
13
+ React.ElementRef<typeof SelectPrimitive.Trigger>,
14
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
15
+ >(({ className, children, ...props }, ref) => (
16
+ <SelectPrimitive.Trigger
17
+ ref={ref}
18
+ className={cn(
19
+ "flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
20
+ className,
21
+ )}
22
+ {...props}
23
+ >
24
+ {children}
25
+ <SelectPrimitive.Icon asChild>
26
+ <IconChevronDown className="size-4 opacity-50" />
27
+ </SelectPrimitive.Icon>
28
+ </SelectPrimitive.Trigger>
29
+ ));
30
+ SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
31
+
32
+ const SelectScrollUpButton = React.forwardRef<
33
+ React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
34
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
35
+ >(({ className, ...props }, ref) => (
36
+ <SelectPrimitive.ScrollUpButton
37
+ ref={ref}
38
+ className={cn("flex cursor-default items-center justify-center py-1", className)}
39
+ {...props}
40
+ >
41
+ <IconChevronUp className="size-4" />
42
+ </SelectPrimitive.ScrollUpButton>
43
+ ));
44
+ SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
45
+
46
+ const SelectScrollDownButton = React.forwardRef<
47
+ React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
48
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
49
+ >(({ className, ...props }, ref) => (
50
+ <SelectPrimitive.ScrollDownButton
51
+ ref={ref}
52
+ className={cn("flex cursor-default items-center justify-center py-1", className)}
53
+ {...props}
54
+ >
55
+ <IconChevronDown className="size-4" />
56
+ </SelectPrimitive.ScrollDownButton>
57
+ ));
58
+ SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
59
+
60
+ const SelectContent = React.forwardRef<
61
+ React.ElementRef<typeof SelectPrimitive.Content>,
62
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
63
+ >(({ className, children, position = "popper", ...props }, ref) => (
64
+ <SelectPrimitive.Portal>
65
+ <SelectPrimitive.Content
66
+ ref={ref}
67
+ className={cn(
68
+ "relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]",
69
+ position === "popper" &&
70
+ "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
71
+ className,
72
+ )}
73
+ position={position}
74
+ {...props}
75
+ >
76
+ <SelectScrollUpButton />
77
+ <SelectPrimitive.Viewport
78
+ className={cn(
79
+ "p-1",
80
+ position === "popper" &&
81
+ "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
82
+ )}
83
+ >
84
+ {children}
85
+ </SelectPrimitive.Viewport>
86
+ <SelectScrollDownButton />
87
+ </SelectPrimitive.Content>
88
+ </SelectPrimitive.Portal>
89
+ ));
90
+ SelectContent.displayName = SelectPrimitive.Content.displayName;
91
+
92
+ const SelectLabel = React.forwardRef<
93
+ React.ElementRef<typeof SelectPrimitive.Label>,
94
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
95
+ >(({ className, ...props }, ref) => (
96
+ <SelectPrimitive.Label
97
+ ref={ref}
98
+ className={cn("px-2 py-1.5 text-sm font-semibold", className)}
99
+ {...props}
100
+ />
101
+ ));
102
+ SelectLabel.displayName = SelectPrimitive.Label.displayName;
103
+
104
+ const SelectItem = React.forwardRef<
105
+ React.ElementRef<typeof SelectPrimitive.Item>,
106
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
107
+ >(({ className, children, ...props }, ref) => (
108
+ <SelectPrimitive.Item
109
+ ref={ref}
110
+ className={cn(
111
+ "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
112
+ className,
113
+ )}
114
+ {...props}
115
+ >
116
+ <span className="absolute right-2 flex size-3.5 items-center justify-center">
117
+ <SelectPrimitive.ItemIndicator>
118
+ <IconCheck className="size-4" />
119
+ </SelectPrimitive.ItemIndicator>
120
+ </span>
121
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
122
+ </SelectPrimitive.Item>
123
+ ));
124
+ SelectItem.displayName = SelectPrimitive.Item.displayName;
125
+
126
+ const SelectSeparator = React.forwardRef<
127
+ React.ElementRef<typeof SelectPrimitive.Separator>,
128
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
129
+ >(({ className, ...props }, ref) => (
130
+ <SelectPrimitive.Separator
131
+ ref={ref}
132
+ className={cn("-mx-1 my-1 h-px bg-muted", className)}
133
+ {...props}
134
+ />
135
+ ));
136
+ SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
137
+
138
+ export {
139
+ Select,
140
+ SelectGroup,
141
+ SelectValue,
142
+ SelectTrigger,
143
+ SelectContent,
144
+ SelectLabel,
145
+ SelectItem,
146
+ SelectSeparator,
147
+ SelectScrollUpButton,
148
+ SelectScrollDownButton,
149
+ };
@@ -0,0 +1,118 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "../../lib/utils";
4
+
5
+ function Table({ className, ...props }: React.ComponentProps<"table">) {
6
+ return (
7
+ <div
8
+ data-slot="table-container"
9
+ className={cn(
10
+ "relative w-full overflow-x-auto rounded-lg border border-border bg-card shadow-xs",
11
+ className,
12
+ )}
13
+ >
14
+ <table
15
+ data-slot="table"
16
+ className="w-full caption-bottom relative text-sm"
17
+ {...props}
18
+ />
19
+ </div>
20
+ );
21
+ }
22
+
23
+ function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
24
+ return (
25
+ <thead
26
+ data-slot="table-header"
27
+ className={cn("[&_tr]:border-b", className)}
28
+ {...props}
29
+ />
30
+ );
31
+ }
32
+
33
+ function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
34
+ return (
35
+ <tbody
36
+ data-slot="table-body"
37
+ className={cn("[&_tr:last-child]:border-0", className)}
38
+ {...props}
39
+ />
40
+ );
41
+ }
42
+
43
+ function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
44
+ return (
45
+ <tfoot
46
+ data-slot="table-footer"
47
+ className={cn(
48
+ "border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
49
+ className,
50
+ )}
51
+ {...props}
52
+ />
53
+ );
54
+ }
55
+
56
+ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
57
+ return (
58
+ <tr
59
+ data-slot="table-row"
60
+ className={cn(
61
+ "border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted",
62
+ className,
63
+ )}
64
+ {...props}
65
+ />
66
+ );
67
+ }
68
+
69
+ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
70
+ return (
71
+ <th
72
+ data-slot="table-head"
73
+ className={cn(
74
+ "h-8 text-xs py-1 px-4 text-left align-middle font-medium whitespace-nowrap text-foreground has-[[role=checkbox]]:pr-0 has-[[role=checkbox]]:translate-y-0.5",
75
+ "sticky top-0 z-30 bg-muted/50",
76
+ className,
77
+ )}
78
+ {...props}
79
+ />
80
+ );
81
+ }
82
+
83
+ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
84
+ return (
85
+ <td
86
+ data-slot="table-cell"
87
+ className={cn(
88
+ "px-4 py-2 align-middle whitespace-nowrap has-[[role=checkbox]]:pr-0 has-[[role=checkbox]]:translate-y-0.5",
89
+ className,
90
+ )}
91
+ {...props}
92
+ />
93
+ );
94
+ }
95
+
96
+ function TableCaption({
97
+ className,
98
+ ...props
99
+ }: React.ComponentProps<"caption">) {
100
+ return (
101
+ <caption
102
+ data-slot="table-caption"
103
+ className={cn("mt-4 text-sm text-muted-foreground", className)}
104
+ {...props}
105
+ />
106
+ );
107
+ }
108
+
109
+ export {
110
+ Table,
111
+ TableHeader,
112
+ TableBody,
113
+ TableFooter,
114
+ TableHead,
115
+ TableRow,
116
+ TableCell,
117
+ TableCaption,
118
+ };
@@ -0,0 +1,83 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { type VariantProps } from "class-variance-authority"
5
+ import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui"
6
+
7
+ import { cn } from "../../lib/utils"
8
+ import { toggleVariants } from "./toggle"
9
+
10
+ const ToggleGroupContext = React.createContext<
11
+ VariantProps<typeof toggleVariants> & {
12
+ spacing?: number
13
+ }
14
+ >({
15
+ size: "default",
16
+ variant: "default",
17
+ spacing: 0,
18
+ })
19
+
20
+ function ToggleGroup({
21
+ className,
22
+ variant,
23
+ size,
24
+ spacing = 0,
25
+ children,
26
+ ...props
27
+ }: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
28
+ VariantProps<typeof toggleVariants> & {
29
+ spacing?: number
30
+ }) {
31
+ return (
32
+ <ToggleGroupPrimitive.Root
33
+ data-slot="toggle-group"
34
+ data-variant={variant}
35
+ data-size={size}
36
+ data-spacing={spacing}
37
+ style={{ "--gap": spacing } as React.CSSProperties}
38
+ className={cn(
39
+ "group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs",
40
+ className
41
+ )}
42
+ {...props}
43
+ >
44
+ <ToggleGroupContext.Provider value={{ variant, size, spacing }}>
45
+ {children}
46
+ </ToggleGroupContext.Provider>
47
+ </ToggleGroupPrimitive.Root>
48
+ )
49
+ }
50
+
51
+ function ToggleGroupItem({
52
+ className,
53
+ children,
54
+ variant,
55
+ size,
56
+ ...props
57
+ }: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
58
+ VariantProps<typeof toggleVariants>) {
59
+ const context = React.useContext(ToggleGroupContext)
60
+
61
+ return (
62
+ <ToggleGroupPrimitive.Item
63
+ data-slot="toggle-group-item"
64
+ data-variant={context.variant || variant}
65
+ data-size={context.size || size}
66
+ data-spacing={context.spacing}
67
+ className={cn(
68
+ toggleVariants({
69
+ variant: context.variant || variant,
70
+ size: context.size || size,
71
+ }),
72
+ "w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10",
73
+ "data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l",
74
+ className
75
+ )}
76
+ {...props}
77
+ >
78
+ {children}
79
+ </ToggleGroupPrimitive.Item>
80
+ )
81
+ }
82
+
83
+ export { ToggleGroup, ToggleGroupItem }
@@ -0,0 +1,45 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { Toggle as TogglePrimitive } from "radix-ui"
4
+
5
+ import { cn } from "../../lib/utils"
6
+
7
+ const toggleVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none hover:bg-muted hover:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-transparent",
13
+ outline:
14
+ "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
15
+ },
16
+ size: {
17
+ default: "h-9 min-w-9 px-2",
18
+ sm: "h-8 min-w-8 px-1.5",
19
+ lg: "h-10 min-w-10 px-2.5",
20
+ },
21
+ },
22
+ defaultVariants: {
23
+ variant: "default",
24
+ size: "default",
25
+ },
26
+ }
27
+ )
28
+
29
+ function Toggle({
30
+ className,
31
+ variant,
32
+ size,
33
+ ...props
34
+ }: React.ComponentProps<typeof TogglePrimitive.Root> &
35
+ VariantProps<typeof toggleVariants>) {
36
+ return (
37
+ <TogglePrimitive.Root
38
+ data-slot="toggle"
39
+ className={cn(toggleVariants({ variant, size, className }))}
40
+ {...props}
41
+ />
42
+ )
43
+ }
44
+
45
+ export { Toggle, toggleVariants }
@@ -0,0 +1,210 @@
1
+ import { useMemo } from "react";
2
+ import {
3
+ HoverCard,
4
+ HoverCardContent,
5
+ HoverCardTrigger,
6
+ } from "@/components/ui/hover-card";
7
+ import {
8
+ Table,
9
+ TableBody,
10
+ TableCell,
11
+ TableHead,
12
+ TableHeader,
13
+ TableRow,
14
+ } from "@/components/ui/table";
15
+ import {
16
+ aggregateDeclaredProps,
17
+ aggregateDefinitions,
18
+ catalogComponentNames,
19
+ usageMap,
20
+ } from "./aggregate";
21
+ import { shortPath } from "./paths";
22
+ import type { WorkspaceReport } from "../types/report";
23
+ import { pluralize } from "usemods";
24
+
25
+ /** Set of `"ComponentName/propName"` keys for every declared prop with no recorded usage. */
26
+ function buildUnusedPropSet(report: WorkspaceReport): Set<string> {
27
+ const s = new Set<string>();
28
+ const usageByComponent = new Map(
29
+ (report.usage_by_component ?? []).map((usage) => [
30
+ usage.component,
31
+ usage.prop_frequencies ?? {},
32
+ ]),
33
+ );
34
+
35
+ for (const file of report.files ?? []) {
36
+ for (const definition of file.definitions ?? []) {
37
+ const componentName = definition.name;
38
+ const propFrequencies = usageByComponent.get(componentName) ?? {};
39
+
40
+ for (const propName of definition.declared_props ?? []) {
41
+ if ((propFrequencies[propName] ?? 0) === 0) {
42
+ s.add(`${componentName}/${propName}`);
43
+ }
44
+ }
45
+ }
46
+ }
47
+
48
+ return s;
49
+ }
50
+
51
+ const catalogHoverTriggerClass =
52
+ "cursor-default text-xs underline decoration-dotted underline-offset-2 hover:text-foreground";
53
+
54
+ function CatalogPropUsageHover({
55
+ component,
56
+ declared,
57
+ unusedProps,
58
+ usedPropCount,
59
+ }: {
60
+ component: string;
61
+ declared: string[];
62
+ unusedProps: Set<string>;
63
+ usedPropCount: number;
64
+ }) {
65
+ const used = declared.filter(
66
+ (prop) => !unusedProps.has(`${component}/${prop}`),
67
+ );
68
+ const unused = declared.filter((prop) =>
69
+ unusedProps.has(`${component}/${prop}`),
70
+ );
71
+
72
+ return (
73
+ <HoverCard openDelay={200} closeDelay={100}>
74
+ <HoverCardTrigger asChild>
75
+ <button type="button" className={catalogHoverTriggerClass}>
76
+ <span className="text-muted-foreground">
77
+ {usedPropCount}/{declared.length} {pluralize("prop", usedPropCount)}{" "}
78
+ used
79
+ </span>
80
+ </button>
81
+ </HoverCardTrigger>
82
+ <HoverCardContent align="start" className="w-56 p-3">
83
+ <p className="text-xs font-medium text-foreground">Props</p>
84
+ {used.length > 0 ? (
85
+ <div className="mt-2">
86
+ <p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
87
+ Used
88
+ </p>
89
+ <ul className="mt-1 space-y-0.5 font-mono text-xs text-foreground">
90
+ {used.map((prop) => (
91
+ <li key={prop}>{prop}</li>
92
+ ))}
93
+ </ul>
94
+ </div>
95
+ ) : null}
96
+ {unused.length > 0 ? (
97
+ <div className={used.length > 0 ? "mt-3" : "mt-2"}>
98
+ <p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
99
+ Never passed
100
+ </p>
101
+ <ul className="mt-1 space-y-0.5 font-mono text-xs text-muted-foreground/70">
102
+ {unused.map((prop) => (
103
+ <li key={prop} className="line-through">
104
+ {prop}
105
+ </li>
106
+ ))}
107
+ </ul>
108
+ </div>
109
+ ) : null}
110
+ </HoverCardContent>
111
+ </HoverCard>
112
+ );
113
+ }
114
+
115
+ function CatalogAppUsageHover({
116
+ root,
117
+ fileCount,
118
+ files,
119
+ }: {
120
+ root: string;
121
+ fileCount: number;
122
+ files: string[];
123
+ }) {
124
+ const sortedFiles = [...files].sort((a, b) => a.localeCompare(b));
125
+
126
+ return (
127
+ <HoverCard openDelay={200} closeDelay={100}>
128
+ <HoverCardTrigger asChild>
129
+ <button type="button" className={catalogHoverTriggerClass}>
130
+ {fileCount} {pluralize("file", fileCount)}
131
+ </button>
132
+ </HoverCardTrigger>
133
+ <HoverCardContent align="start" className="w-72 p-3">
134
+ <p className="text-xs font-medium text-foreground">
135
+ {fileCount} {pluralize("file", fileCount)}
136
+ </p>
137
+ <ul className="mt-2 max-h-48 space-y-0.5 overflow-y-auto font-mono text-xs text-muted-foreground">
138
+ {sortedFiles.map((file) => (
139
+ <li key={file} className="truncate" title={shortPath(root, file)}>
140
+ {shortPath(root, file)}
141
+ </li>
142
+ ))}
143
+ </ul>
144
+ </HoverCardContent>
145
+ </HoverCard>
146
+ );
147
+ }
148
+
149
+ export function ComponentCatalog({ report }: { report: WorkspaceReport }) {
150
+ const defs = aggregateDefinitions(report);
151
+ const usages = usageMap(report);
152
+ const names = catalogComponentNames(defs, usages);
153
+ const unusedProps = useMemo(() => buildUnusedPropSet(report), [report]);
154
+ const declaredByName = useMemo(
155
+ () => aggregateDeclaredProps(report),
156
+ [report],
157
+ );
158
+
159
+ return (
160
+ <Table>
161
+ <TableHeader>
162
+ <TableRow>
163
+ <TableHead scope="col">Component</TableHead>
164
+ <TableHead scope="col">Prop Usage</TableHead>
165
+ <TableHead scope="col">App Usage</TableHead>
166
+ </TableRow>
167
+ </TableHeader>
168
+ <TableBody>
169
+ {names.map((name) => {
170
+ const use = usages.get(name);
171
+ const declared = declaredByName.get(name) ?? [];
172
+ const usedPropCount = declared.filter(
173
+ (prop) => !unusedProps.has(`${name}/${prop}`),
174
+ ).length;
175
+
176
+ return (
177
+ <TableRow key={name}>
178
+ <TableCell>{name}</TableCell>
179
+
180
+ <TableCell>
181
+ {declared.length > 0 ? (
182
+ <CatalogPropUsageHover
183
+ component={name}
184
+ declared={declared}
185
+ unusedProps={unusedProps}
186
+ usedPropCount={usedPropCount}
187
+ />
188
+ ) : (
189
+ <span className="text-muted-foreground/70">—</span>
190
+ )}
191
+ </TableCell>
192
+
193
+ <TableCell>
194
+ {use && use.files.length > 0 ? (
195
+ <CatalogAppUsageHover
196
+ root={report.root}
197
+ fileCount={use.file_count}
198
+ files={use.files}
199
+ />
200
+ ) : (
201
+ <span className="text-muted-foreground/70">—</span>
202
+ )}
203
+ </TableCell>
204
+ </TableRow>
205
+ );
206
+ })}
207
+ </TableBody>
208
+ </Table>
209
+ );
210
+ }