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.
- package/CHANGELOG.md +76 -0
- package/LICENSE +201 -0
- package/README.md +104 -0
- package/bin/dslinter.mjs +29 -0
- package/components.json +20 -0
- package/package.json +90 -0
- package/src/components/InlineCode.tsx +5 -0
- package/src/components/icons.tsx +121 -0
- package/src/components/ui/badge.tsx +52 -0
- package/src/components/ui/button.tsx +57 -0
- package/src/components/ui/checkbox.tsx +25 -0
- package/src/components/ui/command.tsx +183 -0
- package/src/components/ui/dialog.tsx +156 -0
- package/src/components/ui/hover-card.tsx +42 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/select.tsx +149 -0
- package/src/components/ui/table.tsx +118 -0
- package/src/components/ui/toggle-group.tsx +83 -0
- package/src/components/ui/toggle.tsx +45 -0
- package/src/dashboard/ComponentCatalog.tsx +210 -0
- package/src/dashboard/ComponentUsageDetails.tsx +109 -0
- package/src/dashboard/DashboardBody.tsx +71 -0
- package/src/dashboard/FindingsList.tsx +151 -0
- package/src/dashboard/ScoreStrip.tsx +28 -0
- package/src/dashboard/TokenWall.tsx +241 -0
- package/src/dashboard/aggregate.ts +73 -0
- package/src/dashboard/paths.ts +10 -0
- package/src/dashboard/useWorkspaceReport.ts +136 -0
- package/src/index.ts +67 -0
- package/src/lib/utils.ts +6 -0
- package/src/playground/definePlayground.tsx +99 -0
- package/src/playground/enumerateControlCombinations.test.ts +112 -0
- package/src/playground/enumerateControlCombinations.ts +74 -0
- package/src/report/a11yForModule.ts +35 -0
- package/src/report/codeScoreForModule.ts +41 -0
- package/src/report/modulePathMatch.ts +27 -0
- package/src/report/tokenStyleFindingsForModule.ts +24 -0
- package/src/shell/ComponentPlaygroundPane.tsx +438 -0
- package/src/shell/DashboardCommandPalette.tsx +134 -0
- package/src/shell/DashboardLayout.tsx +230 -0
- package/src/shell/EmptyCard.tsx +21 -0
- package/src/shell/GovernancePane.tsx +77 -0
- package/src/shell/PlaygroundA11yAndCode.tsx +387 -0
- package/src/shell/PlaygroundControlField.tsx +213 -0
- package/src/shell/PlaygroundControls.tsx +66 -0
- package/src/shell/PlaygroundUsageCode.tsx +51 -0
- package/src/shell/PlaygroundVariantMatrix.tsx +68 -0
- package/src/shell/Section.tsx +34 -0
- package/src/shell/Sidebar.tsx +203 -0
- package/src/shell/TokensPane.tsx +26 -0
- package/src/shell/controlApiTable.ts +53 -0
- package/src/shell/hashRoute.ts +49 -0
- package/src/shell/playgroundUsageHighlight.ts +53 -0
- package/src/shell/playgroundUsageTwoslash.ts +69 -0
- package/src/shell/useHashRoute.ts +29 -0
- package/src/styles/dashboard-theme.css +188 -0
- package/src/types/controls.ts +62 -0
- package/src/types/defaultTailwindTypography.ts +55 -0
- package/src/types/playground.ts +21 -0
- package/src/types/preview.ts +8 -0
- package/src/types/report.ts +116 -0
- 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
|
+
}
|