Explorar o código

Merge pull request #760 from RooVetGit/cte/dropdown-menu

Add shadcn/ui dropdown menu component
Chris Estreich hai 11 meses
pai
achega
6f64975e22

+ 1 - 6
webview-ui/.storybook/main.ts

@@ -2,12 +2,7 @@ import type { StorybookConfig } from "@storybook/react-vite"
 
 const config: StorybookConfig = {
 	stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
-	addons: [
-		"@storybook/addon-onboarding",
-		"@storybook/addon-essentials",
-		"@chromatic-com/storybook",
-		"@storybook/addon-interactions",
-	],
+	addons: ["@storybook/addon-essentials", "@storybook/addon-interactions"],
 	framework: {
 		name: "@storybook/react-vite",
 		options: {},

+ 1 - 1
webview-ui/.storybook/vscode.css

@@ -14,7 +14,7 @@
 	--vscode-button-foreground: #ffffff; /* "button.foreground" */
 	--vscode-button-secondaryBackground: #313131; /* "button.secondaryBackground" */
 	--vscode-button-secondaryForeground: #cccccc; /* "button.secondaryForeground" */
-	--vscode-disabledForeground: red; /* "disabledForeground" */
+	--vscode-disabledForeground: #313131; /* "disabledForeground" */
 	--vscode-descriptionForeground: #9d9d9d; /* "descriptionForeground" */
 	--vscode-focusBorder: #0078d4; /* "focusBorder" */
 	--vscode-errorForeground: #f85149; /* "errorForeground" */

+ 670 - 147
webview-ui/package-lock.json

@@ -8,6 +8,8 @@
 			"name": "webview-ui",
 			"version": "0.1.0",
 			"dependencies": {
+				"@radix-ui/react-dropdown-menu": "^2.1.5",
+				"@radix-ui/react-icons": "^1.3.2",
 				"@radix-ui/react-slot": "^1.1.1",
 				"@tailwindcss/vite": "^4.0.0",
 				"@vscode/webview-ui-toolkit": "^1.4.0",
@@ -31,10 +33,8 @@
 				"vscrui": "^0.2.0"
 			},
 			"devDependencies": {
-				"@chromatic-com/storybook": "^3.2.4",
 				"@storybook/addon-essentials": "^8.5.2",
 				"@storybook/addon-interactions": "^8.5.2",
-				"@storybook/addon-onboarding": "^8.5.2",
 				"@storybook/blocks": "^8.5.2",
 				"@storybook/react": "^8.5.2",
 				"@storybook/react-vite": "^8.5.2",
@@ -601,56 +601,6 @@
 			"dev": true,
 			"license": "MIT"
 		},
-		"node_modules/@chromatic-com/storybook": {
-			"version": "3.2.4",
-			"resolved": "https://registry.npmjs.org/@chromatic-com/storybook/-/storybook-3.2.4.tgz",
-			"integrity": "sha512-5/bOOYxfwZ2BktXeqcCpOVAoR6UCoeART5t9FVy22hoo8F291zOuX4y3SDgm10B1GVU/ZTtJWPT2X9wZFlxYLg==",
-			"dev": true,
-			"license": "MIT",
-			"dependencies": {
-				"chromatic": "^11.15.0",
-				"filesize": "^10.0.12",
-				"jsonfile": "^6.1.0",
-				"react-confetti": "^6.1.0",
-				"strip-ansi": "^7.1.0"
-			},
-			"engines": {
-				"node": ">=16.0.0",
-				"yarn": ">=1.22.18"
-			},
-			"peerDependencies": {
-				"storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0"
-			}
-		},
-		"node_modules/@chromatic-com/storybook/node_modules/ansi-regex": {
-			"version": "6.1.0",
-			"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
-			"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
-			"dev": true,
-			"license": "MIT",
-			"engines": {
-				"node": ">=12"
-			},
-			"funding": {
-				"url": "https://github.com/chalk/ansi-regex?sponsor=1"
-			}
-		},
-		"node_modules/@chromatic-com/storybook/node_modules/strip-ansi": {
-			"version": "7.1.0",
-			"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
-			"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
-			"dev": true,
-			"license": "MIT",
-			"dependencies": {
-				"ansi-regex": "^6.0.1"
-			},
-			"engines": {
-				"node": ">=12"
-			},
-			"funding": {
-				"url": "https://github.com/chalk/strip-ansi?sponsor=1"
-			}
-		},
 		"node_modules/@emotion/is-prop-valid": {
 			"version": "1.2.2",
 			"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz",
@@ -1164,6 +1114,44 @@
 				"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
 			}
 		},
+		"node_modules/@floating-ui/core": {
+			"version": "1.6.9",
+			"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
+			"integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==",
+			"license": "MIT",
+			"dependencies": {
+				"@floating-ui/utils": "^0.2.9"
+			}
+		},
+		"node_modules/@floating-ui/dom": {
+			"version": "1.6.13",
+			"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
+			"integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
+			"license": "MIT",
+			"dependencies": {
+				"@floating-ui/core": "^1.6.0",
+				"@floating-ui/utils": "^0.2.9"
+			}
+		},
+		"node_modules/@floating-ui/react-dom": {
+			"version": "2.1.2",
+			"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
+			"integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==",
+			"license": "MIT",
+			"dependencies": {
+				"@floating-ui/dom": "^1.0.0"
+			},
+			"peerDependencies": {
+				"react": ">=16.8.0",
+				"react-dom": ">=16.8.0"
+			}
+		},
+		"node_modules/@floating-ui/utils": {
+			"version": "0.2.9",
+			"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
+			"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
+			"license": "MIT"
+		},
 		"node_modules/@humanwhocodes/config-array": {
 			"version": "0.13.0",
 			"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
@@ -1800,6 +1788,61 @@
 				"node": ">= 8"
 			}
 		},
+		"node_modules/@radix-ui/primitive": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
+			"integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==",
+			"license": "MIT"
+		},
+		"node_modules/@radix-ui/react-arrow": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz",
+			"integrity": "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/react-primitive": "2.0.1"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"@types/react-dom": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+				"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				},
+				"@types/react-dom": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-collection": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz",
+			"integrity": "sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/react-compose-refs": "1.1.1",
+				"@radix-ui/react-context": "1.1.1",
+				"@radix-ui/react-primitive": "2.0.1",
+				"@radix-ui/react-slot": "1.1.1"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"@types/react-dom": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+				"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				},
+				"@types/react-dom": {
+					"optional": true
+				}
+			}
+		},
 		"node_modules/@radix-ui/react-compose-refs": {
 			"version": "1.1.1",
 			"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
@@ -1815,6 +1858,333 @@
 				}
 			}
 		},
+		"node_modules/@radix-ui/react-context": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz",
+			"integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==",
+			"license": "MIT",
+			"peerDependencies": {
+				"@types/react": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-direction": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
+			"integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==",
+			"license": "MIT",
+			"peerDependencies": {
+				"@types/react": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-dismissable-layer": {
+			"version": "1.1.4",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.4.tgz",
+			"integrity": "sha512-XDUI0IVYVSwjMXxM6P4Dfti7AH+Y4oS/TB+sglZ/EXc7cqLwGAmp1NlMrcUjj7ks6R5WTZuWKv44FBbLpwU3sA==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/primitive": "1.1.1",
+				"@radix-ui/react-compose-refs": "1.1.1",
+				"@radix-ui/react-primitive": "2.0.1",
+				"@radix-ui/react-use-callback-ref": "1.1.0",
+				"@radix-ui/react-use-escape-keydown": "1.1.0"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"@types/react-dom": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+				"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				},
+				"@types/react-dom": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-dropdown-menu": {
+			"version": "2.1.5",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.5.tgz",
+			"integrity": "sha512-50ZmEFL1kOuLalPKHrLWvPFMons2fGx9TqQCWlPwDVpbAnaUJ1g4XNcKqFNMQymYU0kKWR4MDDi+9vUQBGFgcQ==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/primitive": "1.1.1",
+				"@radix-ui/react-compose-refs": "1.1.1",
+				"@radix-ui/react-context": "1.1.1",
+				"@radix-ui/react-id": "1.1.0",
+				"@radix-ui/react-menu": "2.1.5",
+				"@radix-ui/react-primitive": "2.0.1",
+				"@radix-ui/react-use-controllable-state": "1.1.0"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"@types/react-dom": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+				"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				},
+				"@types/react-dom": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-focus-guards": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz",
+			"integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==",
+			"license": "MIT",
+			"peerDependencies": {
+				"@types/react": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-focus-scope": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.1.tgz",
+			"integrity": "sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/react-compose-refs": "1.1.1",
+				"@radix-ui/react-primitive": "2.0.1",
+				"@radix-ui/react-use-callback-ref": "1.1.0"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"@types/react-dom": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+				"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				},
+				"@types/react-dom": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-icons": {
+			"version": "1.3.2",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz",
+			"integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==",
+			"license": "MIT",
+			"peerDependencies": {
+				"react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc"
+			}
+		},
+		"node_modules/@radix-ui/react-id": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz",
+			"integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/react-use-layout-effect": "1.1.0"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-menu": {
+			"version": "2.1.5",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.5.tgz",
+			"integrity": "sha512-uH+3w5heoMJtqVCgYOtYVMECk1TOrkUn0OG0p5MqXC0W2ppcuVeESbou8PTHoqAjbdTEK19AGXBWcEtR5WpEQg==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/primitive": "1.1.1",
+				"@radix-ui/react-collection": "1.1.1",
+				"@radix-ui/react-compose-refs": "1.1.1",
+				"@radix-ui/react-context": "1.1.1",
+				"@radix-ui/react-direction": "1.1.0",
+				"@radix-ui/react-dismissable-layer": "1.1.4",
+				"@radix-ui/react-focus-guards": "1.1.1",
+				"@radix-ui/react-focus-scope": "1.1.1",
+				"@radix-ui/react-id": "1.1.0",
+				"@radix-ui/react-popper": "1.2.1",
+				"@radix-ui/react-portal": "1.1.3",
+				"@radix-ui/react-presence": "1.1.2",
+				"@radix-ui/react-primitive": "2.0.1",
+				"@radix-ui/react-roving-focus": "1.1.1",
+				"@radix-ui/react-slot": "1.1.1",
+				"@radix-ui/react-use-callback-ref": "1.1.0",
+				"aria-hidden": "^1.2.4",
+				"react-remove-scroll": "^2.6.2"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"@types/react-dom": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+				"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				},
+				"@types/react-dom": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-popper": {
+			"version": "1.2.1",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.1.tgz",
+			"integrity": "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==",
+			"license": "MIT",
+			"dependencies": {
+				"@floating-ui/react-dom": "^2.0.0",
+				"@radix-ui/react-arrow": "1.1.1",
+				"@radix-ui/react-compose-refs": "1.1.1",
+				"@radix-ui/react-context": "1.1.1",
+				"@radix-ui/react-primitive": "2.0.1",
+				"@radix-ui/react-use-callback-ref": "1.1.0",
+				"@radix-ui/react-use-layout-effect": "1.1.0",
+				"@radix-ui/react-use-rect": "1.1.0",
+				"@radix-ui/react-use-size": "1.1.0",
+				"@radix-ui/rect": "1.1.0"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"@types/react-dom": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+				"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				},
+				"@types/react-dom": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-portal": {
+			"version": "1.1.3",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.3.tgz",
+			"integrity": "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/react-primitive": "2.0.1",
+				"@radix-ui/react-use-layout-effect": "1.1.0"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"@types/react-dom": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+				"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				},
+				"@types/react-dom": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-presence": {
+			"version": "1.1.2",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz",
+			"integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/react-compose-refs": "1.1.1",
+				"@radix-ui/react-use-layout-effect": "1.1.0"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"@types/react-dom": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+				"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				},
+				"@types/react-dom": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-primitive": {
+			"version": "2.0.1",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz",
+			"integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/react-slot": "1.1.1"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"@types/react-dom": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+				"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				},
+				"@types/react-dom": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-roving-focus": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz",
+			"integrity": "sha512-QE1RoxPGJ/Nm8Qmk0PxP8ojmoaS67i0s7hVssS7KuI2FQoc/uzVlZsqKfQvxPE6D8hICCPHJ4D88zNhT3OOmkw==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/primitive": "1.1.1",
+				"@radix-ui/react-collection": "1.1.1",
+				"@radix-ui/react-compose-refs": "1.1.1",
+				"@radix-ui/react-context": "1.1.1",
+				"@radix-ui/react-direction": "1.1.0",
+				"@radix-ui/react-id": "1.1.0",
+				"@radix-ui/react-primitive": "2.0.1",
+				"@radix-ui/react-use-callback-ref": "1.1.0",
+				"@radix-ui/react-use-controllable-state": "1.1.0"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"@types/react-dom": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+				"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				},
+				"@types/react-dom": {
+					"optional": true
+				}
+			}
+		},
 		"node_modules/@radix-ui/react-slot": {
 			"version": "1.1.1",
 			"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
@@ -1833,6 +2203,114 @@
 				}
 			}
 		},
+		"node_modules/@radix-ui/react-use-callback-ref": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
+			"integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==",
+			"license": "MIT",
+			"peerDependencies": {
+				"@types/react": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-use-controllable-state": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz",
+			"integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/react-use-callback-ref": "1.1.0"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-use-escape-keydown": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
+			"integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/react-use-callback-ref": "1.1.0"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-use-layout-effect": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz",
+			"integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==",
+			"license": "MIT",
+			"peerDependencies": {
+				"@types/react": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-use-rect": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz",
+			"integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/rect": "1.1.0"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-use-size": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz",
+			"integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/react-use-layout-effect": "1.1.0"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/rect": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz",
+			"integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==",
+			"license": "MIT"
+		},
 		"node_modules/@rollup/pluginutils": {
 			"version": "5.1.4",
 			"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
@@ -2300,20 +2778,6 @@
 				"storybook": "^8.5.2"
 			}
 		},
-		"node_modules/@storybook/addon-onboarding": {
-			"version": "8.5.2",
-			"resolved": "https://registry.npmjs.org/@storybook/addon-onboarding/-/addon-onboarding-8.5.2.tgz",
-			"integrity": "sha512-IViKQdBTuF2KSOrhyyq2soT0Je90AZbAAM5SLrVF7Q4H/Pc2lbf1JX8WwAOW2RKH2o7/U2Mvl0SXqNNcwLZC1A==",
-			"dev": true,
-			"license": "MIT",
-			"funding": {
-				"type": "opencollective",
-				"url": "https://opencollective.com/storybook"
-			},
-			"peerDependencies": {
-				"storybook": "^8.5.2"
-			}
-		},
 		"node_modules/@storybook/addon-outline": {
 			"version": "8.5.2",
 			"resolved": "https://registry.npmjs.org/@storybook/addon-outline/-/addon-outline-8.5.2.tgz",
@@ -3344,7 +3808,7 @@
 			"version": "18.3.5",
 			"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz",
 			"integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==",
-			"dev": true,
+			"devOptional": true,
 			"license": "MIT",
 			"peerDependencies": {
 				"@types/react": "^18.0.0"
@@ -3997,6 +4461,18 @@
 			"dev": true,
 			"license": "Python-2.0"
 		},
+		"node_modules/aria-hidden": {
+			"version": "1.2.4",
+			"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz",
+			"integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==",
+			"license": "MIT",
+			"dependencies": {
+				"tslib": "^2.0.0"
+			},
+			"engines": {
+				"node": ">=10"
+			}
+		},
 		"node_modules/aria-query": {
 			"version": "5.3.0",
 			"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
@@ -4623,30 +5099,6 @@
 				"node": ">= 16"
 			}
 		},
-		"node_modules/chromatic": {
-			"version": "11.25.2",
-			"resolved": "https://registry.npmjs.org/chromatic/-/chromatic-11.25.2.tgz",
-			"integrity": "sha512-/9eQWn6BU1iFsop86t8Au21IksTRxwXAl7if8YHD05L2AbuMjClLWZo5cZojqrJHGKDhTqfrC2X2xE4uSm0iKw==",
-			"dev": true,
-			"license": "MIT",
-			"bin": {
-				"chroma": "dist/bin.js",
-				"chromatic": "dist/bin.js",
-				"chromatic-cli": "dist/bin.js"
-			},
-			"peerDependencies": {
-				"@chromatic-com/cypress": "^0.*.* || ^1.0.0",
-				"@chromatic-com/playwright": "^0.*.* || ^1.0.0"
-			},
-			"peerDependenciesMeta": {
-				"@chromatic-com/cypress": {
-					"optional": true
-				},
-				"@chromatic-com/playwright": {
-					"optional": true
-				}
-			}
-		},
 		"node_modules/ci-info": {
 			"version": "3.9.0",
 			"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
@@ -5143,6 +5595,12 @@
 				"node": ">=8"
 			}
 		},
+		"node_modules/detect-node-es": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+			"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
+			"license": "MIT"
+		},
 		"node_modules/devlop": {
 			"version": "1.1.0",
 			"resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
@@ -6204,16 +6662,6 @@
 				"node": "^10.12.0 || >=12.0.0"
 			}
 		},
-		"node_modules/filesize": {
-			"version": "10.1.6",
-			"resolved": "https://registry.npmjs.org/filesize/-/filesize-10.1.6.tgz",
-			"integrity": "sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w==",
-			"dev": true,
-			"license": "BSD-3-Clause",
-			"engines": {
-				"node": ">= 10.4.0"
-			}
-		},
 		"node_modules/fill-range": {
 			"version": "7.1.1",
 			"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -6410,6 +6858,15 @@
 				"url": "https://github.com/sponsors/ljharb"
 			}
 		},
+		"node_modules/get-nonce": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+			"integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
+			"license": "MIT",
+			"engines": {
+				"node": ">=6"
+			}
+		},
 		"node_modules/get-package-type": {
 			"version": "0.1.0",
 			"resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
@@ -8412,29 +8869,6 @@
 				"node": ">=6"
 			}
 		},
-		"node_modules/jsonfile": {
-			"version": "6.1.0",
-			"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
-			"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
-			"dev": true,
-			"license": "MIT",
-			"dependencies": {
-				"universalify": "^2.0.0"
-			},
-			"optionalDependencies": {
-				"graceful-fs": "^4.1.6"
-			}
-		},
-		"node_modules/jsonfile/node_modules/universalify": {
-			"version": "2.0.1",
-			"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
-			"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
-			"dev": true,
-			"license": "MIT",
-			"engines": {
-				"node": ">= 10.0.0"
-			}
-		},
 		"node_modules/jsx-ast-utils": {
 			"version": "3.3.5",
 			"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
@@ -9930,22 +10364,6 @@
 				"node": ">=0.10.0"
 			}
 		},
-		"node_modules/react-confetti": {
-			"version": "6.2.2",
-			"resolved": "https://registry.npmjs.org/react-confetti/-/react-confetti-6.2.2.tgz",
-			"integrity": "sha512-K+kTyOPgX+ZujMZ+Rmb7pZdHBvg+DzinG/w4Eh52WOB8/pfO38efnnrtEZNJmjTvLxc16RBYO+tPM68Fg8viBA==",
-			"dev": true,
-			"license": "MIT",
-			"dependencies": {
-				"tween-functions": "^1.2.0"
-			},
-			"engines": {
-				"node": ">=16"
-			},
-			"peerDependencies": {
-				"react": "^16.3.0 || ^17.0.1 || ^18.0.0 || ^19.0.0"
-			}
-		},
 		"node_modules/react-docgen": {
 			"version": "7.1.1",
 			"resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-7.1.1.tgz",
@@ -10046,6 +10464,75 @@
 				"react": ">=16.8"
 			}
 		},
+		"node_modules/react-remove-scroll": {
+			"version": "2.6.3",
+			"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz",
+			"integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==",
+			"license": "MIT",
+			"dependencies": {
+				"react-remove-scroll-bar": "^2.3.7",
+				"react-style-singleton": "^2.2.3",
+				"tslib": "^2.1.0",
+				"use-callback-ref": "^1.3.3",
+				"use-sidecar": "^1.1.3"
+			},
+			"engines": {
+				"node": ">=10"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/react-remove-scroll-bar": {
+			"version": "2.3.8",
+			"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
+			"integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
+			"license": "MIT",
+			"dependencies": {
+				"react-style-singleton": "^2.2.2",
+				"tslib": "^2.0.0"
+			},
+			"engines": {
+				"node": ">=10"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/react-style-singleton": {
+			"version": "2.2.3",
+			"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
+			"integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
+			"license": "MIT",
+			"dependencies": {
+				"get-nonce": "^1.0.0",
+				"tslib": "^2.0.0"
+			},
+			"engines": {
+				"node": ">=10"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				}
+			}
+		},
 		"node_modules/react-textarea-autosize": {
 			"version": "8.5.7",
 			"resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.7.tgz",
@@ -11515,13 +12002,6 @@
 			"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
 			"license": "0BSD"
 		},
-		"node_modules/tween-functions": {
-			"version": "1.2.0",
-			"resolved": "https://registry.npmjs.org/tween-functions/-/tween-functions-1.2.0.tgz",
-			"integrity": "sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA==",
-			"dev": true,
-			"license": "BSD"
-		},
 		"node_modules/type-check": {
 			"version": "0.4.0",
 			"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -11957,6 +12437,27 @@
 				"requires-port": "^1.0.0"
 			}
 		},
+		"node_modules/use-callback-ref": {
+			"version": "1.3.3",
+			"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
+			"integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
+			"license": "MIT",
+			"dependencies": {
+				"tslib": "^2.0.0"
+			},
+			"engines": {
+				"node": ">=10"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				}
+			}
+		},
 		"node_modules/use-composed-ref": {
 			"version": "1.4.0",
 			"resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.4.0.tgz",
@@ -12002,6 +12503,28 @@
 				}
 			}
 		},
+		"node_modules/use-sidecar": {
+			"version": "1.1.3",
+			"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
+			"integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
+			"license": "MIT",
+			"dependencies": {
+				"detect-node-es": "^1.1.0",
+				"tslib": "^2.0.0"
+			},
+			"engines": {
+				"node": ">=10"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				}
+			}
+		},
 		"node_modules/util": {
 			"version": "0.12.5",
 			"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",

+ 2 - 2
webview-ui/package.json

@@ -13,6 +13,8 @@
 		"build-storybook": "storybook build"
 	},
 	"dependencies": {
+		"@radix-ui/react-dropdown-menu": "^2.1.5",
+		"@radix-ui/react-icons": "^1.3.2",
 		"@radix-ui/react-slot": "^1.1.1",
 		"@tailwindcss/vite": "^4.0.0",
 		"@vscode/webview-ui-toolkit": "^1.4.0",
@@ -36,10 +38,8 @@
 		"vscrui": "^0.2.0"
 	},
 	"devDependencies": {
-		"@chromatic-com/storybook": "^3.2.4",
 		"@storybook/addon-essentials": "^8.5.2",
 		"@storybook/addon-interactions": "^8.5.2",
-		"@storybook/addon-onboarding": "^8.5.2",
 		"@storybook/blocks": "^8.5.2",
 		"@storybook/react": "^8.5.2",
 		"@storybook/react-vite": "^8.5.2",

+ 1 - 1
webview-ui/src/components/ui/button.tsx

@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"
 import { cn } from "@/lib/utils"
 
 const buttonVariants = cva(
-	"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+	"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
 	{
 		variants: {
 			variant: {

+ 177 - 0
webview-ui/src/components/ui/dropdown-menu.tsx

@@ -0,0 +1,177 @@
+import * as React from "react"
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
+import { CheckIcon, ChevronRightIcon, DotFilledIcon } from "@radix-ui/react-icons"
+
+import { cn } from "@/lib/utils"
+
+const DropdownMenu = DropdownMenuPrimitive.Root
+
+const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
+
+const DropdownMenuGroup = DropdownMenuPrimitive.Group
+
+const DropdownMenuPortal = DropdownMenuPrimitive.Portal
+
+const DropdownMenuSub = DropdownMenuPrimitive.Sub
+
+const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
+
+const DropdownMenuSubTrigger = React.forwardRef<
+	React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
+	React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
+		inset?: boolean
+	}
+>(({ className, inset, children, ...props }, ref) => (
+	<DropdownMenuPrimitive.SubTrigger
+		ref={ref}
+		className={cn(
+			"flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+			inset && "pl-8",
+			className,
+		)}
+		{...props}>
+		{children}
+		<ChevronRightIcon className="ml-auto" />
+	</DropdownMenuPrimitive.SubTrigger>
+))
+DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
+
+const DropdownMenuSubContent = React.forwardRef<
+	React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
+	React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
+>(({ className, ...props }, ref) => (
+	<DropdownMenuPrimitive.SubContent
+		ref={ref}
+		className={cn(
+			"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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",
+			className,
+		)}
+		{...props}
+	/>
+))
+DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
+
+const DropdownMenuContent = React.forwardRef<
+	React.ElementRef<typeof DropdownMenuPrimitive.Content>,
+	React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
+>(({ className, sideOffset = 4, ...props }, ref) => (
+	<DropdownMenuPrimitive.Portal>
+		<DropdownMenuPrimitive.Content
+			ref={ref}
+			sideOffset={sideOffset}
+			className={cn(
+				"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 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",
+				className,
+			)}
+			{...props}
+		/>
+	</DropdownMenuPrimitive.Portal>
+))
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
+
+const DropdownMenuItem = React.forwardRef<
+	React.ElementRef<typeof DropdownMenuPrimitive.Item>,
+	React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
+		inset?: boolean
+	}
+>(({ className, inset, ...props }, ref) => (
+	<DropdownMenuPrimitive.Item
+		ref={ref}
+		className={cn(
+			"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
+			inset && "pl-8",
+			className,
+		)}
+		{...props}
+	/>
+))
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
+
+const DropdownMenuCheckboxItem = React.forwardRef<
+	React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
+	React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
+>(({ className, children, checked, ...props }, ref) => (
+	<DropdownMenuPrimitive.CheckboxItem
+		ref={ref}
+		className={cn(
+			"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
+			className,
+		)}
+		checked={checked}
+		{...props}>
+		<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
+			<DropdownMenuPrimitive.ItemIndicator>
+				<CheckIcon className="h-4 w-4" />
+			</DropdownMenuPrimitive.ItemIndicator>
+		</span>
+		{children}
+	</DropdownMenuPrimitive.CheckboxItem>
+))
+DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
+
+const DropdownMenuRadioItem = React.forwardRef<
+	React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
+	React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
+>(({ className, children, ...props }, ref) => (
+	<DropdownMenuPrimitive.RadioItem
+		ref={ref}
+		className={cn(
+			"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
+			className,
+		)}
+		{...props}>
+		<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
+			<DropdownMenuPrimitive.ItemIndicator>
+				<DotFilledIcon className="h-2 w-2 fill-current" />
+			</DropdownMenuPrimitive.ItemIndicator>
+		</span>
+		{children}
+	</DropdownMenuPrimitive.RadioItem>
+))
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
+
+const DropdownMenuLabel = React.forwardRef<
+	React.ElementRef<typeof DropdownMenuPrimitive.Label>,
+	React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
+		inset?: boolean
+	}
+>(({ className, inset, ...props }, ref) => (
+	<DropdownMenuPrimitive.Label
+		ref={ref}
+		className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
+		{...props}
+	/>
+))
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
+
+const DropdownMenuSeparator = React.forwardRef<
+	React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
+	React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
+>(({ className, ...props }, ref) => (
+	<DropdownMenuPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
+))
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
+
+const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
+	return <span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />
+}
+DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
+
+export {
+	DropdownMenu,
+	DropdownMenuTrigger,
+	DropdownMenuContent,
+	DropdownMenuItem,
+	DropdownMenuCheckboxItem,
+	DropdownMenuRadioItem,
+	DropdownMenuLabel,
+	DropdownMenuSeparator,
+	DropdownMenuShortcut,
+	DropdownMenuGroup,
+	DropdownMenuPortal,
+	DropdownMenuSub,
+	DropdownMenuSubContent,
+	DropdownMenuSubTrigger,
+	DropdownMenuRadioGroup,
+}

+ 2 - 0
webview-ui/src/components/ui/index.ts

@@ -0,0 +1,2 @@
+export * from "./button"
+export * from "./dropdown-menu"

+ 15 - 3
webview-ui/src/index.css

@@ -1,10 +1,22 @@
-/* @import "tailwindcss"; */
+/**
+ * Normally we'd import tailwind with the following:
+ *
+ * @import "tailwindcss";
+ *
+ * However, we need to customize the preflight styles since the extension's
+ * current UI assumes there's no CSS resetting or normalization.
+ *
+ * We're excluding tailwind's default preflight and importing our own, which
+ * is based on the original:
+ * https://github.com/tailwindlabs/tailwindcss/blob/main/packages/tailwindcss/preflight.css
+ *
+ * Reference: https://tailwindcss.com/docs/preflight
+ */
 
 @layer theme, base, components, utilities;
 
 @import "tailwindcss/theme.css" layer(theme);
-/* https://tailwindcss.com/docs/preflight */
-/* @import "tailwindcss/preflight.css" layer(base); */
+@import "./preflight.css" layer(base);
 @import "tailwindcss/utilities.css" layer(utilities);
 
 @plugin "tailwindcss-animate";

+ 383 - 0
webview-ui/src/preflight.css

@@ -0,0 +1,383 @@
+/*
+  1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
+  2. Remove default margins and padding
+  3. Reset all borders.
+*/
+
+*,
+::after,
+::before,
+::backdrop,
+::file-selector-button {
+	box-sizing: border-box; /* 1 */
+	/* margin: 0; */ /* 2 */
+	padding: 0; /* 2 */
+	border: 0 solid; /* 3 */
+}
+
+/*
+  1. Use a consistent sensible line-height in all browsers.
+  2. Prevent adjustments of font size after orientation changes in iOS.
+  3. Use a more readable tab size.
+  4. Use the user's configured `sans` font-family by default.
+  5. Use the user's configured `sans` font-feature-settings by default.
+  6. Use the user's configured `sans` font-variation-settings by default.
+  7. Disable tap highlights on iOS.
+*/
+
+html,
+:host {
+	line-height: 1.5; /* 1 */
+	-webkit-text-size-adjust: 100%; /* 2 */
+	tab-size: 4; /* 3 */
+	font-family: var(
+		--default-font-family,
+		ui-sans-serif,
+		system-ui,
+		sans-serif,
+		"Apple Color Emoji",
+		"Segoe UI Emoji",
+		"Segoe UI Symbol",
+		"Noto Color Emoji"
+	); /* 4 */
+	font-feature-settings: var(--default-font-feature-settings, normal); /* 5 */
+	font-variation-settings: var(--default-font-variation-settings, normal); /* 6 */
+	-webkit-tap-highlight-color: transparent; /* 7 */
+}
+
+/*
+  Inherit line-height from `html` so users can set them as a class directly on the `html` element.
+*/
+
+body {
+	line-height: inherit;
+}
+
+/*
+  1. Add the correct height in Firefox.
+  2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
+  3. Reset the default border style to a 1px solid border.
+*/
+
+hr {
+	height: 0; /* 1 */
+	color: inherit; /* 2 */
+	border-top-width: 1px; /* 3 */
+}
+
+/*
+  Add the correct text decoration in Chrome, Edge, and Safari.
+*/
+
+abbr:where([title]) {
+	-webkit-text-decoration: underline dotted;
+	text-decoration: underline dotted;
+}
+
+/*
+  Remove the default font size and weight for headings.
+*/
+
+/* h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+  font-size: inherit;
+  font-weight: inherit;
+} */
+
+/*
+  Reset links to optimize for opt-in styling instead of opt-out.
+*/
+
+a {
+	color: inherit;
+	-webkit-text-decoration: inherit;
+	text-decoration: inherit;
+}
+
+/*
+  Add the correct font weight in Edge and Safari.
+*/
+
+b,
+strong {
+	font-weight: bolder;
+}
+
+/*
+  1. Use the user's configured `mono` font-family by default.
+  2. Use the user's configured `mono` font-feature-settings by default.
+  3. Use the user's configured `mono` font-variation-settings by default.
+  4. Correct the odd `em` font sizing in all browsers.
+*/
+
+code,
+kbd,
+samp,
+pre {
+	font-family: var(
+		--default-mono-font-family,
+		ui-monospace,
+		SFMono-Regular,
+		Menlo,
+		Monaco,
+		Consolas,
+		"Liberation Mono",
+		"Courier New",
+		monospace
+	); /* 4 */
+	font-feature-settings: var(--default-mono-font-feature-settings, normal); /* 5 */
+	font-variation-settings: var(--default-mono-font-variation-settings, normal); /* 6 */
+	font-size: 1em; /* 4 */
+}
+
+/*
+  Add the correct font size in all browsers.
+*/
+
+small {
+	font-size: 80%;
+}
+
+/*
+  Prevent `sub` and `sup` elements from affecting the line height in all browsers.
+*/
+
+sub,
+sup {
+	font-size: 75%;
+	line-height: 0;
+	position: relative;
+	vertical-align: baseline;
+}
+
+sub {
+	bottom: -0.25em;
+}
+
+sup {
+	top: -0.5em;
+}
+
+/*
+  1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
+  2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
+  3. Remove gaps between table borders by default.
+*/
+
+table {
+	text-indent: 0; /* 1 */
+	border-color: inherit; /* 2 */
+	border-collapse: collapse; /* 3 */
+}
+
+/*
+  Use the modern Firefox focus style for all focusable elements.
+*/
+
+:-moz-focusring {
+	outline: auto;
+}
+
+/*
+  Add the correct vertical alignment in Chrome and Firefox.
+*/
+
+progress {
+	vertical-align: baseline;
+}
+
+/*
+  Add the correct display in Chrome and Safari.
+*/
+
+summary {
+	display: list-item;
+}
+
+/*
+  Make lists unstyled by default.
+*/
+
+ol,
+ul,
+menu {
+	list-style: none;
+}
+
+/*
+  1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
+  2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
+      This can trigger a poorly considered lint error in some tools but is included by design.
+*/
+
+img,
+svg,
+video,
+canvas,
+audio,
+iframe,
+embed,
+object {
+	display: block; /* 1 */
+	vertical-align: middle; /* 2 */
+}
+
+/*
+  Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
+*/
+
+img,
+video {
+	max-width: 100%;
+	height: auto;
+}
+
+/*
+  1. Inherit font styles in all browsers.
+  2. Remove border radius in all browsers.
+  3. Remove background color in all browsers.
+  4. Ensure consistent opacity for disabled states in all browsers.
+*/
+
+button,
+input,
+select,
+optgroup,
+textarea,
+::file-selector-button {
+	font: inherit; /* 1 */
+	font-feature-settings: inherit; /* 1 */
+	font-variation-settings: inherit; /* 1 */
+	letter-spacing: inherit; /* 1 */
+	color: inherit; /* 1 */
+	border-radius: 0; /* 2 */
+	background-color: transparent; /* 3 */
+	opacity: 1; /* 4 */
+}
+
+/*
+  Restore default font weight.
+*/
+
+:where(select:is([multiple], [size])) optgroup {
+	font-weight: bolder;
+}
+
+/*
+  Restore indentation.
+*/
+
+:where(select:is([multiple], [size])) optgroup option {
+	padding-inline-start: 20px;
+}
+
+/*
+  Restore space after button.
+*/
+
+::file-selector-button {
+	margin-inline-end: 4px;
+}
+
+/*
+  1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
+  2. Set the default placeholder color to a semi-transparent version of the current text color.
+*/
+
+::placeholder {
+	opacity: 1; /* 1 */
+	color: color-mix(in oklab, currentColor 50%, transparent); /* 2 */
+}
+
+/*
+  Prevent resizing textareas horizontally by default.
+*/
+
+textarea {
+	resize: vertical;
+}
+
+/*
+  Remove the inner padding in Chrome and Safari on macOS.
+*/
+
+::-webkit-search-decoration {
+	-webkit-appearance: none;
+}
+
+/*
+  1. Ensure date/time inputs have the same height when empty in iOS Safari.
+  2. Ensure text alignment can be changed on date/time inputs in iOS Safari.
+*/
+
+::-webkit-date-and-time-value {
+	min-height: 1lh; /* 1 */
+	text-align: inherit; /* 2 */
+}
+
+/*
+  Prevent height from changing on date/time inputs in macOS Safari when the input is set to `display: block`.
+*/
+
+::-webkit-datetime-edit {
+	display: inline-flex;
+}
+
+/*
+  Remove excess padding from pseudo-elements in date/time inputs to ensure consistent height across browsers.
+*/
+
+::-webkit-datetime-edit-fields-wrapper {
+	padding: 0;
+}
+
+::-webkit-datetime-edit,
+::-webkit-datetime-edit-year-field,
+::-webkit-datetime-edit-month-field,
+::-webkit-datetime-edit-day-field,
+::-webkit-datetime-edit-hour-field,
+::-webkit-datetime-edit-minute-field,
+::-webkit-datetime-edit-second-field,
+::-webkit-datetime-edit-millisecond-field,
+::-webkit-datetime-edit-meridiem-field {
+	padding-block: 0;
+}
+
+/*
+  Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
+*/
+
+:-moz-ui-invalid {
+	box-shadow: none;
+}
+
+/*
+  Correct the inability to style the border radius in iOS Safari.
+*/
+
+button,
+input:where([type="button"], [type="reset"], [type="submit"]),
+::file-selector-button {
+	appearance: button;
+}
+
+/*
+  Correct the cursor style of increment and decrement buttons in Safari.
+*/
+
+::-webkit-inner-spin-button,
+::-webkit-outer-spin-button {
+	height: auto;
+}
+
+/*
+  Make elements with the HTML hidden attribute stay hidden by default.
+*/
+
+[hidden]:where(:not([hidden="until-found"])) {
+	display: none !important;
+}

+ 38 - 38
webview-ui/src/stories/Button.stories.ts

@@ -1,15 +1,47 @@
 import type { Meta, StoryObj } from "@storybook/react"
-import { fn } from "@storybook/test"
 
-import { Button } from "@/components/ui/button"
+import { Button } from "@/components/ui"
 
 const meta = {
-	title: "Example/Button",
+	title: "@shadcn/Button",
 	component: Button,
 	parameters: { layout: "centered" },
 	tags: ["autodocs"],
-	argTypes: {},
-	args: { onClick: fn(), children: "Button" },
+	argTypes: {
+		variant: {
+			control: "select",
+			options: ["default", "secondary", "outline", "ghost", "link", "destructive"],
+			type: "string",
+			table: {
+				defaultValue: {
+					summary: "default",
+				},
+			},
+		},
+		size: {
+			control: "select",
+			options: ["default", "sm", "lg", "icon"],
+			type: "string",
+			table: {
+				defaultValue: {
+					summary: "default",
+				},
+			},
+		},
+		children: {
+			table: {
+				disable: true,
+			},
+		},
+		asChild: {
+			table: {
+				disable: true,
+			},
+		},
+	},
+	args: {
+		children: "Button",
+	},
 } satisfies Meta<typeof Button>
 
 export default meta
@@ -17,37 +49,5 @@ export default meta
 type Story = StoryObj<typeof meta>
 
 export const Default: Story = {
-	args: {
-		variant: "default",
-	},
-}
-
-export const Secondary: Story = {
-	args: {
-		variant: "secondary",
-	},
-}
-
-export const Outline: Story = {
-	args: {
-		variant: "outline",
-	},
-}
-
-export const Ghost: Story = {
-	args: {
-		variant: "ghost",
-	},
-}
-
-export const Link: Story = {
-	args: {
-		variant: "link",
-	},
-}
-
-export const Destructive: Story = {
-	args: {
-		variant: "destructive",
-	},
+	name: "Button",
 }

+ 134 - 0
webview-ui/src/stories/DropdownMenu.stories.tsx

@@ -0,0 +1,134 @@
+import type { Meta, StoryObj } from "@storybook/react"
+import {
+	HamburgerMenuIcon,
+	BorderLeftIcon,
+	BorderRightIcon,
+	BorderBottomIcon,
+	BorderTopIcon,
+	TextAlignLeftIcon,
+	TextAlignCenterIcon,
+	TextAlignRightIcon,
+} from "@radix-ui/react-icons"
+
+import {
+	Button,
+	DropdownMenu,
+	DropdownMenuContent,
+	DropdownMenuGroup,
+	DropdownMenuItem,
+	DropdownMenuLabel,
+	DropdownMenuPortal,
+	DropdownMenuSeparator,
+	DropdownMenuShortcut,
+	DropdownMenuSub,
+	DropdownMenuSubContent,
+	DropdownMenuSubTrigger,
+	DropdownMenuTrigger,
+} from "@/components/ui"
+
+const meta = {
+	title: "@shadcn/DropdownMenu",
+	component: DropdownMenu,
+	parameters: { layout: "centered" },
+	tags: ["autodocs"],
+} satisfies Meta<typeof DropdownMenu>
+
+export default meta
+
+type Story = StoryObj<typeof meta>
+
+export const Default: Story = {
+	name: "DropdownMenu",
+	render: () => (
+		<DropdownMenu>
+			<DropdownMenuTrigger asChild>
+				<Button variant="ghost" size="icon">
+					<HamburgerMenuIcon />
+				</Button>
+			</DropdownMenuTrigger>
+			<DropdownMenuContent>
+				<DropdownMenuLabel>Label</DropdownMenuLabel>
+				<DropdownMenuSeparator />
+				<DropdownMenuGroup>
+					<DropdownMenuItem>Item 1</DropdownMenuItem>
+					<DropdownMenuItem>
+						Item 2<DropdownMenuShortcut>⌘2</DropdownMenuShortcut>
+					</DropdownMenuItem>
+				</DropdownMenuGroup>
+				<DropdownMenuSeparator />
+				<DropdownMenuGroup>
+					<DropdownMenuSub>
+						<DropdownMenuSubTrigger>Submenu</DropdownMenuSubTrigger>
+						<DropdownMenuPortal>
+							<DropdownMenuSubContent>
+								<DropdownMenuItem>Foo</DropdownMenuItem>
+								<DropdownMenuItem>
+									Bar
+									<DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
+								</DropdownMenuItem>
+								<DropdownMenuSeparator />
+								<DropdownMenuItem>Baz</DropdownMenuItem>
+							</DropdownMenuSubContent>
+						</DropdownMenuPortal>
+					</DropdownMenuSub>
+				</DropdownMenuGroup>
+			</DropdownMenuContent>
+		</DropdownMenu>
+	),
+}
+
+type DropdownMenuVariantProps = {
+	side?: "top" | "bottom" | "left" | "right"
+	align?: "start" | "center" | "end"
+	children?: React.ReactNode
+}
+
+const DropdownMenuVariant = ({ side = "bottom", align = "center", children }: DropdownMenuVariantProps) => (
+	<DropdownMenu>
+		<DropdownMenuTrigger asChild>
+			<Button variant="ghost" size="icon">
+				{children}
+			</Button>
+		</DropdownMenuTrigger>
+		<DropdownMenuContent side={side} align={align}>
+			<DropdownMenuItem>Foo</DropdownMenuItem>
+			<DropdownMenuItem>Bar</DropdownMenuItem>
+			<DropdownMenuItem>Baz</DropdownMenuItem>
+		</DropdownMenuContent>
+	</DropdownMenu>
+)
+
+export const Placements: Story = {
+	render: () => (
+		<div className="flex gap-2">
+			<DropdownMenuVariant side="top">
+				<BorderTopIcon />
+			</DropdownMenuVariant>
+			<DropdownMenuVariant side="bottom">
+				<BorderBottomIcon />
+			</DropdownMenuVariant>
+			<DropdownMenuVariant side="left">
+				<BorderLeftIcon />
+			</DropdownMenuVariant>
+			<DropdownMenuVariant side="right">
+				<BorderRightIcon />
+			</DropdownMenuVariant>
+		</div>
+	),
+}
+
+export const Alignments: Story = {
+	render: () => (
+		<div className="flex gap-2">
+			<DropdownMenuVariant align="center">
+				<TextAlignCenterIcon />
+			</DropdownMenuVariant>
+			<DropdownMenuVariant align="end">
+				<TextAlignRightIcon />
+			</DropdownMenuVariant>
+			<DropdownMenuVariant align="start">
+				<TextAlignLeftIcon />
+			</DropdownMenuVariant>
+		</div>
+	),
+}

+ 47 - 0
webview-ui/src/stories/vscrui/Dropdown.stories.tsx

@@ -0,0 +1,47 @@
+import type { Meta, StoryObj } from "@storybook/react"
+
+import { Dropdown } from "vscrui"
+
+const meta = {
+	title: "@vscrui/Dropdown",
+	component: () => (
+		<Dropdown
+			value="foo"
+			role="combobox"
+			options={[
+				{ value: "foo", label: "Foo" },
+				{ value: "bar", label: "Bar" },
+				{ value: "baz", label: "Baz" },
+			]}
+		/>
+	),
+	parameters: { layout: "centered" },
+	tags: ["autodocs"],
+	argTypes: {},
+	args: {},
+} satisfies Meta<typeof Dropdown>
+
+export default meta
+
+type Story = StoryObj<typeof meta>
+
+export const Default: Story = {
+	args: {},
+	parameters: {
+		docs: {
+			source: {
+				code: `
+<Dropdown
+    value="foo"
+    role="combobox"
+    options={[
+        { value: "foo", label: "Foo" },
+        { value: "bar", label: "Bar" },
+        { value: "baz", label: "Baz" }
+    ]}
+/>`,
+				language: "tsx",
+			},
+		},
+	},
+}