mirror of
https://github.com/cline/cline.git
synced 2026-09-13 09:50:12 +08:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ae6db5620 | ||
|
|
83fe701b9c | ||
|
|
d47b054757 | ||
|
|
05d816848d | ||
|
|
e0489bb6f4 |
@@ -60,6 +60,8 @@ const mockVSCodeDarkTheme = {
|
||||
"--vscode-widget-shadow": "#00000050",
|
||||
"--vscode-panel-border": "#454545",
|
||||
"--vscode-icon-foreground": "#CCCCCC",
|
||||
"--popover-foreground": "#D4D4D4",
|
||||
"--vscode-problemsErrorIcon-foreground": "#be1100",
|
||||
}
|
||||
|
||||
const mockVSCodeLightTheme = {
|
||||
@@ -121,6 +123,7 @@ const mockVSCodeLightTheme = {
|
||||
"--vscode-widget-shadow": "#00000033",
|
||||
"--vscode-panel-border": "#D4D4D4",
|
||||
"--vscode-icon-foreground": "#3E3E42",
|
||||
"--popover-foreground": "#D4D4D4",
|
||||
}
|
||||
|
||||
// Mock VSCode theme variables for Storybook
|
||||
|
||||
Generated
+238
-4
@@ -12,12 +12,15 @@
|
||||
"@fontsource/azeret-mono": "^5.2.9",
|
||||
"@heroui/react": "^2.8.0-beta.2",
|
||||
"@paper-design/shaders-react": "^0.0.46",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-slider": "^1.3.6",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-toggle": "^1.1.10",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@vscode/webview-ui-toolkit": "^1.4.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
@@ -3753,6 +3756,12 @@
|
||||
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@radix-ui/number": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
|
||||
"integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
||||
@@ -3782,6 +3791,50 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collection": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
|
||||
"integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"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/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"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-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
@@ -3812,6 +3865,75 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog": {
|
||||
"version": "1.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
|
||||
"integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.11",
|
||||
"@radix-ui/react-focus-guards": "1.1.3",
|
||||
"@radix-ui/react-focus-scope": "1.1.7",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-portal": "1.1.9",
|
||||
"@radix-ui/react-presence": "1.1.5",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-slot": "1.2.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||
"aria-hidden": "^1.2.4",
|
||||
"react-remove-scroll": "^2.6.3"
|
||||
},
|
||||
"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-dialog/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"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.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
|
||||
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
|
||||
"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.11",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
|
||||
@@ -3965,6 +4087,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"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-popper": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
|
||||
@@ -4068,6 +4208,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"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-progress": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz",
|
||||
@@ -4115,10 +4273,43 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slider": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz",
|
||||
"integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/number": "1.1.1",
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-collection": "1.1.7",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-direction": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1",
|
||||
"@radix-ui/react-use-previous": "1.1.1",
|
||||
"@radix-ui/react-use-size": "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-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz",
|
||||
"integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
@@ -4162,6 +4353,31 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-toggle": {
|
||||
"version": "1.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz",
|
||||
"integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.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-tooltip": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
|
||||
@@ -4196,6 +4412,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"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-callback-ref": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||
|
||||
@@ -20,12 +20,15 @@
|
||||
"@fontsource/azeret-mono": "^5.2.9",
|
||||
"@heroui/react": "^2.8.0-beta.2",
|
||||
"@paper-design/shaders-react": "^0.0.46",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-slider": "^1.3.6",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-toggle": "^1.1.10",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@vscode/webview-ui-toolkit": "^1.4.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { Meta } from "@storybook/react-vite"
|
||||
import ClineLogoWhite from "@/assets/ClineLogoWhite"
|
||||
import { Alert, AlertDescription } from "./alert"
|
||||
|
||||
const meta: Meta<typeof Alert> = {
|
||||
title: "Ui/Alert",
|
||||
component: Alert,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Displays alert messages with different severity levels (default, warning, danger). Includes optional dismiss functionality and supports title and description content.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
export const Default = () => (
|
||||
<div className="w-screen flex justify-center items-center">
|
||||
<div className="flex flex-col gap-4 w-full max-w-md px-4">
|
||||
<Alert title="Default" variant="default">
|
||||
<AlertDescription>This is a default alert message.</AlertDescription>
|
||||
</Alert>
|
||||
<Alert title="Warning" variant="warning">
|
||||
<AlertDescription>This is a warning alert.</AlertDescription>
|
||||
</Alert>
|
||||
<Alert title="Error" variant="danger">
|
||||
<AlertDescription>An error has occurred.</AlertDescription>
|
||||
</Alert>
|
||||
<Alert isDismissible={false} title="Non Dismissible" variant="default">
|
||||
<AlertDescription>This alert cannot be dismissed.</AlertDescription>
|
||||
</Alert>
|
||||
<Alert icon={<ClineLogoWhite className="size-2" />} title="Brand" variant="cline">
|
||||
<AlertDescription>
|
||||
<p className="my-1">How can I help you?</p>
|
||||
<ul className="list-inside list-disc text-sm">
|
||||
<li>Coding</li>
|
||||
<li>Debugging</li>
|
||||
<li>Write Tests</li>
|
||||
</ul>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -11,7 +11,8 @@ const alertVariants = cva(
|
||||
variant: {
|
||||
default: "bg-banner-background text-banner-foreground border-foreground/20",
|
||||
warning: "bg-warning/50 border-foreground/20 [&>svg]:text-warning-foreground",
|
||||
danger: "bg-input-error-background text-input-error-foreground border-foreground/20 [&>svg]:text-input-error-foreground",
|
||||
danger: "bg-input-error-background text-foreground border-foreground/20 [&>svg]:text-input-error-foreground",
|
||||
cline: "bg-cline/20 text-fg-cline border-cline [&>svg]:text-fg-cline",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
@@ -22,8 +23,9 @@ const alertVariants = cva(
|
||||
|
||||
const Alert = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants> & { isDismissible?: boolean; title?: string }
|
||||
>(({ className, variant, children, isDismissible = true, title, ...props }, ref) => {
|
||||
React.HTMLAttributes<HTMLDivElement> &
|
||||
VariantProps<typeof alertVariants> & { isDismissible?: boolean; title?: string; icon?: React.ReactNode }
|
||||
>(({ className, variant, children, isDismissible = true, title, icon, ...props }, ref) => {
|
||||
const [dismissed, setDismissed] = React.useState(false)
|
||||
if (dismissed) {
|
||||
return null
|
||||
@@ -33,7 +35,7 @@ const Alert = React.forwardRef<
|
||||
<div className={cn(alertVariants({ variant }), className)} ref={ref} role="alert" {...props}>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<AlertTitle className="flex gap-1 w-full">
|
||||
<AlertTriangleIcon className="shrink-0 size-2 mr-1" />
|
||||
<span className="mr-1 shrink-0">{icon ?? <AlertTriangleIcon className="size-2" />}</span>
|
||||
{title}
|
||||
</AlertTitle>
|
||||
{isDismissible && (
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { Meta } from "@storybook/react-vite"
|
||||
import { ClockIcon, XIcon } from "lucide-react"
|
||||
import { Badge } from "./badge"
|
||||
|
||||
const meta: Meta<typeof Badge> = {
|
||||
title: "Ui/Badge",
|
||||
component: Badge,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Displays a badge with different variants (default, info, danger, outline, brand, neutral, gray, success, warning) and types (default, round, icon). Used to highlight status, categories, or counts.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
export const Default = () => {
|
||||
const variants = ["default", "info", "danger", "outline", "cline", "success", "warning"] as const
|
||||
|
||||
const types = [
|
||||
{ value: "default", label: "Default" },
|
||||
{ value: "icon", label: "With Icon" },
|
||||
{ value: "round", label: "Icon Only" },
|
||||
] as const
|
||||
|
||||
return (
|
||||
<div className="w-screen flex justify-center p-8">
|
||||
<div className="flex flex-col gap-8 w-full max-w-4xl">
|
||||
{types.map((type) => (
|
||||
<div className="flex flex-col gap-4" key={type.value}>
|
||||
<h2 className="text-lg font-semibold">{type.label}</h2>
|
||||
<div className="flex flex-wrap gap-3 items-center">
|
||||
{variants.map((variant) => (
|
||||
<Badge key={variant} type={type.value} variant={variant}>
|
||||
{type.value === "icon" && <ClockIcon />}
|
||||
{type.value === "round" ? <ClockIcon /> : variant.charAt(0).toUpperCase() + variant.slice(1)}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Dismissible = () => {
|
||||
const variants = ["cline", "danger", "success", "warning"] as const
|
||||
|
||||
return (
|
||||
<div className="w-screen flex justify-center p-8">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{variants.map((variant) => (
|
||||
<Badge className="gap-1 pe-0.5 ps-1.5" key={variant} type="icon" variant={variant}>
|
||||
<span>{variant.charAt(0).toUpperCase() + variant.slice(1)}</span>
|
||||
<button
|
||||
aria-label="Remove"
|
||||
className="inline-flex items-center p-0.5 text-sm bg-transparent rounded-xs hover:opacity-80"
|
||||
type="button">
|
||||
<XIcon className="w-3 h-3" />
|
||||
<span className="sr-only">Remove badge</span>
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -4,18 +4,22 @@ import type * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center border text-xs transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 [&_svg]:size-2",
|
||||
"inline-flex items-center border text-xs font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 [&_svg]:size-2 text-badge-foreground shadow-xs",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border-transparent bg-badge-background text-badge-foreground shadow hover:bg-badge-background/80",
|
||||
info: "border-transparent bg-button-background/80 text-button-foreground hover:bg-button-hover",
|
||||
danger: "border-transparent bg-error text-error-foreground shadow hover:bg-error/80",
|
||||
outline: "text-foreground",
|
||||
default: "border-transparent bg-badge-background",
|
||||
info: "border-transparent bg-button-background/80",
|
||||
danger: "border-transparent bg-error-icon",
|
||||
outline: "text-foreground border border-accent/20",
|
||||
cline: "bg-cline border-cline",
|
||||
success: "bg-success/80 border-success",
|
||||
warning: "bg-warning/80 border-warning/50",
|
||||
},
|
||||
type: {
|
||||
default: "rounded-xs px-1 font-normal",
|
||||
round: "rounded-full h-5 w-auto",
|
||||
default: "rounded px-1.5 py-0.5",
|
||||
round: "rounded-full h-5 w-5 justify-center p-0 border-none",
|
||||
icon: "rounded px-1.5 py-0.5 gap-1 ring-0",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { Meta } from "@storybook/react-vite"
|
||||
import { SearchIcon } from "lucide-react"
|
||||
import { Button } from "./button"
|
||||
|
||||
const meta: Meta<typeof Button> = {
|
||||
title: "Ui/Button",
|
||||
component: Button,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Displays different types of error messages in the chat interface, including API errors, credit limit errors, diff errors, and clineignore errors. Handles special error parsing for Cline provider errors and provides appropriate user actions.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
// Default showcase all variants
|
||||
export const Overview = () => {
|
||||
const sizes = [
|
||||
{ value: "sm", label: "Small" },
|
||||
{ value: "default", label: "Default" },
|
||||
{ value: "lg", label: "Large" },
|
||||
] as const
|
||||
|
||||
const variants = ["default", "secondary", "danger", "outline", "ghost", "link", "text", "icon"] as const
|
||||
|
||||
return (
|
||||
<div className="w-screen flex justify-center gap-4">
|
||||
{sizes.map((size) => (
|
||||
<div className="flex flex-col gap-4 w-md px-auto" key={size.value}>
|
||||
<h1>{size.label}</h1>
|
||||
{variants.map((variant) => (
|
||||
<Button key={variant} size={size.value === "default" ? undefined : size.value} variant={variant}>
|
||||
{variant === "icon" ? (
|
||||
<>
|
||||
<SearchIcon />
|
||||
{size.value === "default" && " Icon with Text"}
|
||||
</>
|
||||
) : (
|
||||
variant.charAt(0).toUpperCase() + variant.slice(1)
|
||||
)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -8,16 +8,16 @@ const buttonVariants = cva(
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-button-background text-primary-foreground hover:bg-button-hover shadow-sm shadow-button-background/50",
|
||||
default: "bg-button-background text-primary-foreground hover:bg-button-hover",
|
||||
secondary:
|
||||
"bg-button-secondary-background text-button-secondary-foreground hover:bg-button-secondary-background-hover shadow-sm shadow-button-secondary-background/50",
|
||||
danger: "bg-error text-background hover:bg-error/90 shadow-sm shadow-error/50",
|
||||
outline: "hover:bg-accent/10 border border-accent/20 shadow-sm shadow-accent/50",
|
||||
"bg-button-secondary-background text-button-secondary-foreground hover:bg-button-secondary-background-hover",
|
||||
danger: "bg-error text-background hover:bg-error/90",
|
||||
outline: "hover:bg-accent/10 border border-accent/20",
|
||||
ghost: "hover:bg-accent/10",
|
||||
link: "text-link underline-offset-4 hover:underline p-0 m-0",
|
||||
text: "text-foreground",
|
||||
icon: "p-0 m-0 border-0 cursor-pointer hover:shadow-none focus:ring-0 focus:ring-offset-0",
|
||||
link: "text-link underline-offset-4 hover:underline p-0 m-0 cursor-text select-text",
|
||||
text: "text-foreground cursor-text select-text p-0 m-0",
|
||||
icon: "hover:opacity-80 p-0 m-0 border-0 cursor-pointer hover:shadow-none focus:ring-0 focus:ring-offset-0",
|
||||
cline: "bg-cline border-foreground/20 text-cline-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "py-1.5 px-4 [&_svg]:size-3",
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
import type { Meta } from "@storybook/react-vite"
|
||||
import { ImageIcon } from "lucide-react"
|
||||
import { Button } from "./button"
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./card"
|
||||
import { InputGroup, InputGroupInput, InputGroupTextarea } from "./input-group"
|
||||
|
||||
const meta: Meta<typeof Card> = {
|
||||
title: "Ui/Card",
|
||||
component: Card,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"A versatile card component for displaying content in a contained format. Includes CardHeader, CardTitle, CardDescription, CardContent, and CardFooter subcomponents for structured layouts.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
// Comprehensive showcase with all card examples
|
||||
export const Overview = () => {
|
||||
return (
|
||||
<div className="w-screen p-8">
|
||||
<div className="flex flex-col gap-8">
|
||||
{/* Basic Cards */}
|
||||
<div>
|
||||
<h1 className="mb-4">Basic Cards</h1>
|
||||
<div className="flex gap-6">
|
||||
{/* Simple card */}
|
||||
<Card className="w-[350px]">
|
||||
<CardHeader>
|
||||
<CardTitle>Simple Card</CardTitle>
|
||||
<CardDescription>This is a basic card with header and description</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm">Card content goes here. You can add any content you want.</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Card with footer */}
|
||||
<Card className="w-[350px]">
|
||||
<CardHeader>
|
||||
<CardTitle>Card with Footer</CardTitle>
|
||||
<CardDescription>This card includes action buttons in the footer</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm">This demonstrates a card with buttons in the footer section.</p>
|
||||
</CardContent>
|
||||
<CardFooter className="gap-2">
|
||||
<Button size="sm" variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm" variant="default">
|
||||
Confirm
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cards with Images */}
|
||||
<div>
|
||||
<h1 className="mb-4">Cards with Images</h1>
|
||||
<div className="flex gap-6">
|
||||
{/* Card with image placeholder */}
|
||||
<Card className="w-[350px]">
|
||||
<CardHeader className="p-0">
|
||||
<div className="aspect-video bg-muted rounded-t-xl flex items-center justify-center">
|
||||
<ImageIcon className="w-12 h-12 text-muted-foreground" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-6">
|
||||
<CardTitle className="mb-2">Image Card Title</CardTitle>
|
||||
<CardDescription className="mb-4">
|
||||
This card includes an image at the top with content below.
|
||||
</CardDescription>
|
||||
<p className="text-sm">
|
||||
Perfect for blog posts, product displays, or any content that benefits from visual context.
|
||||
</p>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full" size="sm" variant="default">
|
||||
View Details
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
{/* Another image card variant */}
|
||||
<Card className="w-[350px]">
|
||||
<CardHeader className="p-0">
|
||||
<div className="aspect-video bg-gradient-to-br from-blue-500/20 to-purple-500/20 rounded-t-xl flex items-center justify-center">
|
||||
<ImageIcon className="w-12 h-12 text-blue-500" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-6">
|
||||
<CardTitle className="mb-2">Featured Content</CardTitle>
|
||||
<CardDescription className="mb-4">
|
||||
Cards can be customized with different image styles and gradients.
|
||||
</CardDescription>
|
||||
<p className="text-sm">This example uses a gradient background instead of a solid color.</p>
|
||||
</CardContent>
|
||||
<CardFooter className="gap-2">
|
||||
<Button className="flex-1" size="sm" variant="outline">
|
||||
Share
|
||||
</Button>
|
||||
<Button className="flex-1" size="sm" variant="default">
|
||||
Save
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cards with Forms */}
|
||||
<div>
|
||||
<h1 className="mb-4">Cards with Forms</h1>
|
||||
<div className="flex gap-6">
|
||||
{/* Login form card */}
|
||||
<Card className="w-[400px]">
|
||||
<CardHeader>
|
||||
<CardTitle>Login</CardTitle>
|
||||
<CardDescription>Enter your credentials to access your account</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium" htmlFor="email">
|
||||
Email
|
||||
</label>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="email" placeholder="name@example.com" type="email" />
|
||||
</InputGroup>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium" htmlFor="password">
|
||||
Password
|
||||
</label>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="password" placeholder="Enter your password" type="password" />
|
||||
</InputGroup>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input className="rounded border-input" id="remember" type="checkbox" />
|
||||
<label className="text-sm" htmlFor="remember">
|
||||
Remember me
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-2">
|
||||
<Button className="w-full" variant="default">
|
||||
Sign In
|
||||
</Button>
|
||||
<Button className="w-full" size="sm" variant="ghost">
|
||||
Forgot password?
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
{/* Settings form card */}
|
||||
<Card className="w-[400px]">
|
||||
<CardHeader>
|
||||
<CardTitle>Profile Settings</CardTitle>
|
||||
<CardDescription>Update your profile information</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium" htmlFor="name">
|
||||
Display Name
|
||||
</label>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="name" placeholder="John Doe" type="text" />
|
||||
</InputGroup>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium" htmlFor="bio">
|
||||
Bio
|
||||
</label>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea id="bio" placeholder="Tell us about yourself" rows={3} />
|
||||
</InputGroup>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium" htmlFor="notifications">
|
||||
Notification Preferences
|
||||
</label>
|
||||
<select
|
||||
className="w-full h-9 px-3 border border-input rounded-md bg-background text-sm focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
id="notifications">
|
||||
<option>All notifications</option>
|
||||
<option>Important only</option>
|
||||
<option>None</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter className="gap-2">
|
||||
<Button className="flex-1" variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button className="flex-1" variant="default">
|
||||
Save Changes
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
className={cn("rounded-xs border border-accent/20 bg-sidebar-background text-foreground shadow", className)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
|
||||
<div className={cn("flex flex-col space-y-1.5 p-6", className)} ref={ref} {...props} />
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
|
||||
<div className={cn("font-semibold leading-none tracking-tight", className)} ref={ref} {...props} />
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
|
||||
<div className={cn("text-sm text-muted-foreground", className)} ref={ref} {...props} />
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
|
||||
<div className={cn("p-6 pt-0", className)} ref={ref} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
|
||||
<div className={cn("flex items-center p-6 pt-0", className)} ref={ref} {...props} />
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
@@ -0,0 +1,215 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { Button } from "./button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "./dialog"
|
||||
|
||||
const meta: Meta = {
|
||||
title: "Ui/Dialog",
|
||||
component: Dialog,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"A modal dialog component built on Radix UI. Displays content in a layer above the main application with an overlay backdrop. Includes header, footer, title, description, and close button components for composing dialog layouts.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type StoryProps = {
|
||||
dialogTitle: string
|
||||
dialogDescription: string
|
||||
dialogContent: string
|
||||
showFooter: boolean
|
||||
cancelText: string
|
||||
confirmText: string
|
||||
triggerVariant: "default" | "secondary" | "danger" | "outline" | "ghost" | "link"
|
||||
confirmVariant: "default" | "secondary" | "danger" | "outline" | "ghost"
|
||||
}
|
||||
|
||||
// Interactive story with controls
|
||||
export const Interactive: StoryObj<StoryProps> = {
|
||||
args: {
|
||||
dialogTitle: "Dialog Title",
|
||||
dialogDescription: "This is a description of what this dialog is about.",
|
||||
dialogContent: "This is the main content area of the dialog.",
|
||||
showFooter: true,
|
||||
cancelText: "Cancel",
|
||||
confirmText: "Confirm",
|
||||
triggerVariant: "default",
|
||||
confirmVariant: "default",
|
||||
},
|
||||
argTypes: {
|
||||
dialogTitle: {
|
||||
control: "text",
|
||||
description: "Dialog title text",
|
||||
},
|
||||
dialogDescription: {
|
||||
control: "text",
|
||||
description: "Dialog description text",
|
||||
},
|
||||
dialogContent: {
|
||||
control: "text",
|
||||
description: "Main content of the dialog",
|
||||
},
|
||||
showFooter: {
|
||||
control: "boolean",
|
||||
description: "Show or hide the footer with action buttons",
|
||||
},
|
||||
cancelText: {
|
||||
control: "text",
|
||||
description: "Cancel button text",
|
||||
},
|
||||
confirmText: {
|
||||
control: "text",
|
||||
description: "Confirm button text",
|
||||
},
|
||||
triggerVariant: {
|
||||
control: "select",
|
||||
options: ["default", "secondary", "danger", "outline", "ghost", "link"],
|
||||
description: "Trigger button variant",
|
||||
},
|
||||
confirmVariant: {
|
||||
control: "select",
|
||||
options: ["default", "secondary", "danger", "outline", "ghost"],
|
||||
description: "Confirm button variant",
|
||||
},
|
||||
},
|
||||
render: (args) => (
|
||||
<div className="flex justify-center items-center min-h-screen">
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant={args.triggerVariant}>Open Dialog</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{args.dialogTitle}</DialogTitle>
|
||||
<DialogDescription>{args.dialogDescription}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<p className="text-sm">{args.dialogContent}</p>
|
||||
{args.showFooter && (
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="ghost">{args.cancelText}</Button>
|
||||
</DialogClose>
|
||||
<Button variant={args.confirmVariant}>{args.confirmText}</Button>
|
||||
</DialogFooter>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
// Showcase all dialog variants
|
||||
export const Overview = () => {
|
||||
const variants = [
|
||||
{
|
||||
label: "Complete",
|
||||
triggerVariant: "default" as const,
|
||||
title: "Dialog Title",
|
||||
description:
|
||||
"This is a description of what this dialog is about. It provides context to the user about the action they're taking.",
|
||||
content:
|
||||
"This is the main content area of the dialog. You can put any content here, such as forms, information, or other interactive elements.",
|
||||
hasFooter: true,
|
||||
cancelVariant: "ghost" as const,
|
||||
confirmVariant: "default" as const,
|
||||
confirmText: "Confirm",
|
||||
},
|
||||
{
|
||||
label: "Simple",
|
||||
triggerVariant: "secondary" as const,
|
||||
title: "Simple Dialog",
|
||||
description: "This dialog has no footer, just content.",
|
||||
content: "This is a simpler dialog without action buttons in the footer.",
|
||||
hasFooter: false,
|
||||
},
|
||||
{
|
||||
label: "Confirmation",
|
||||
triggerVariant: "danger" as const,
|
||||
title: "Are you sure?",
|
||||
description: "This action cannot be undone. This will permanently delete the item.",
|
||||
content: null,
|
||||
hasFooter: true,
|
||||
cancelVariant: "secondary" as const,
|
||||
confirmVariant: "danger" as const,
|
||||
confirmText: "Delete",
|
||||
},
|
||||
{
|
||||
label: "With Form",
|
||||
triggerVariant: "outline" as const,
|
||||
title: "Edit Profile",
|
||||
description: "Make changes to your profile here.",
|
||||
content: (
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<label className="text-sm font-medium" htmlFor="name">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
defaultValue="John Doe"
|
||||
id="name"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<label className="text-sm font-medium" htmlFor="email">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
defaultValue="john@example.com"
|
||||
id="email"
|
||||
type="email"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
hasFooter: true,
|
||||
cancelVariant: "secondary" as const,
|
||||
confirmVariant: "default" as const,
|
||||
confirmText: "Save Changes",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="w-screen flex justify-center gap-8 p-8">
|
||||
{variants.map((variant) => (
|
||||
<div className="flex flex-col gap-4 items-center" key={variant.label}>
|
||||
<h2 className="text-lg font-semibold">{variant.label}</h2>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant={variant.triggerVariant}>Open {variant.label}</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{variant.title}</DialogTitle>
|
||||
<DialogDescription>{variant.description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
{typeof variant.content === "string" ? <p className="text-sm">{variant.content}</p> : variant.content}
|
||||
{variant.hasFooter && (
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant={variant.cancelVariant}>Cancel</Button>
|
||||
</DialogClose>
|
||||
<Button variant={variant.confirmVariant}>{variant.confirmText}</Button>
|
||||
</DialogFooter>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-input-placeholder/20 bg-background p-6 shadow-lg duration-200 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 sm:rounded-lg",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<XIcon className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
|
||||
)
|
||||
DialogHeader.displayName = "DialogHeader"
|
||||
|
||||
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
|
||||
)
|
||||
DialogFooter.displayName = "DialogFooter"
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title className={cn("text-lg font-semibold leading-none tracking-tight", className)} ref={ref} {...props} />
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description className={cn("text-sm text-description", className)} ref={ref} {...props} />
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { Meta } from "@storybook/react-vite"
|
||||
import { Button } from "./button"
|
||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from "./hover-card"
|
||||
|
||||
const meta: Meta<typeof HoverCard> = {
|
||||
title: "Ui/HoverCard",
|
||||
component: HoverCard,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Displays additional content in a floating card when hovering over a trigger element. Built on Radix UI with customizable alignment and side offset.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
export const Default = () => (
|
||||
<div className="w-screen flex justify-center items-center min-h-[400px]">
|
||||
<div className="flex flex-col gap-8 w-full max-w-md px-4">
|
||||
<HoverCard>
|
||||
<HoverCardTrigger asChild>
|
||||
<Button>Hover over me</Button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent>
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-semibold">Hover Card Title</h4>
|
||||
<p className="text-sm">This is the content that appears when you hover over the trigger element.</p>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
|
||||
<HoverCard>
|
||||
<HoverCardTrigger asChild>
|
||||
<span className="text-sm underline cursor-pointer">Hover for more info</span>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent align="start">
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-semibold">Additional Information</h4>
|
||||
<p className="text-sm">
|
||||
Hover cards can contain any content including text, images, or other components.
|
||||
</p>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -0,0 +1,146 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"group/input-group border-border-panel dark:bg-input/30 shadow-xs relative flex w-full items-center rounded-md border outline-none transition-[color,box-shadow]",
|
||||
"h-9 has-[>textarea]:h-auto",
|
||||
|
||||
// Variants based on alignment.
|
||||
"has-[>[data-align=inline-start]]:[&>input]:pl-2",
|
||||
"has-[>[data-align=inline-end]]:[&>input]:pr-2",
|
||||
"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
|
||||
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
|
||||
|
||||
// Focus state.
|
||||
"has-[[data-slot=input-group-control]:focus-visible]:ring-ring has-[[data-slot=input-group-control]:focus-visible]:ring-1",
|
||||
|
||||
// Error state.
|
||||
"has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
|
||||
|
||||
className,
|
||||
)}
|
||||
data-slot="input-group"
|
||||
role="group"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const inputGroupAddonVariants = cva(
|
||||
"text-muted-foreground flex h-auto cursor-text select-none items-center justify-center gap-2 py-1.5 text-sm font-medium group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
variants: {
|
||||
align: {
|
||||
"inline-start": "order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
|
||||
"inline-end": "order-last pr-3 has-[>button]:mr-[-0.4rem] has-[>kbd]:mr-[-0.35rem]",
|
||||
"block-start":
|
||||
"[.border-b]:pb-3 order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5",
|
||||
"block-end": "[.border-t]:pt-3 order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
align: "inline-start",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
function InputGroupAddon({
|
||||
className,
|
||||
align = "inline-start",
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
|
||||
return (
|
||||
<div
|
||||
className={cn(inputGroupAddonVariants({ align }), className)}
|
||||
data-align={align}
|
||||
data-slot="input-group-addon"
|
||||
onClick={(e) => {
|
||||
if ((e.target as HTMLElement).closest("button")) {
|
||||
return
|
||||
}
|
||||
e.currentTarget.parentElement?.querySelector("input")?.focus()
|
||||
}}
|
||||
role="group"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const inputGroupButtonVariants = cva("flex items-center gap-2 text-sm shadow-none", {
|
||||
variants: {
|
||||
size: {
|
||||
xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5",
|
||||
sm: "h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5",
|
||||
"icon-xs": "size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
|
||||
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "xs",
|
||||
},
|
||||
})
|
||||
|
||||
function InputGroupButton({
|
||||
className,
|
||||
type = "button",
|
||||
variant = "ghost",
|
||||
size = "xs",
|
||||
...props
|
||||
}: Omit<React.ComponentProps<typeof Button>, "size"> & VariantProps<typeof inputGroupButtonVariants>) {
|
||||
return (
|
||||
<Button
|
||||
className={cn(inputGroupButtonVariants({ size }), className)}
|
||||
data-size={size}
|
||||
type={type}
|
||||
variant={variant}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"text-muted-foreground flex items-center gap-2 text-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function InputGroupInput({ className, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<Input
|
||||
className={cn(
|
||||
"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
|
||||
className,
|
||||
)}
|
||||
data-slot="input-group-control"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function InputGroupTextarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<Textarea
|
||||
className={cn(
|
||||
"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
|
||||
className,
|
||||
)}
|
||||
data-slot="input-group-control"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { InputGroup, InputGroupAddon, InputGroupButton, InputGroupText, InputGroupInput, InputGroupTextarea }
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { Meta } from "@storybook/react-vite"
|
||||
import { Button } from "./button"
|
||||
import { Input } from "./input"
|
||||
|
||||
const meta: Meta<typeof Input> = {
|
||||
title: "Ui/Input",
|
||||
component: Input,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"A styled text input component that supports all standard HTML input types. Includes focus states, placeholder text, and disabled state styling.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
export const Default = () => (
|
||||
<div className="w-screen flex justify-center items-center">
|
||||
<div className="flex flex-col gap-4 w-full max-w-md px-4">
|
||||
<Input placeholder="Default text input" type="text" />
|
||||
<Input placeholder="Email input" type="email" />
|
||||
<Input placeholder="Password input" type="password" />
|
||||
<Input placeholder="Number input" type="number" />
|
||||
<Input disabled placeholder="Disabled input" type="text" />
|
||||
<Input defaultValue="Input with value" type="text" />
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<Input placeholder="Email" type="email" />
|
||||
<Button type="submit">Subscribe</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -0,0 +1,109 @@
|
||||
import type { Meta } from "@storybook/react-vite"
|
||||
import { BellIcon, SettingsIcon, UserIcon } from "lucide-react"
|
||||
import { Badge } from "./badge"
|
||||
import { Button } from "./button"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemFooter,
|
||||
ItemGroup,
|
||||
ItemHeader,
|
||||
ItemMedia,
|
||||
ItemSeparator,
|
||||
ItemTitle,
|
||||
} from "./item"
|
||||
|
||||
const meta: Meta<typeof Item> = {
|
||||
title: "Ui/Item",
|
||||
component: Item,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"A flexible item component for building lists with media, content, actions, and separators. Supports multiple variants (default, outline, select, muted) and sizes.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
export const Default = () => (
|
||||
<div className="w-screen flex justify-center items-center">
|
||||
<div className="flex flex-col gap-5 w-full max-w-md px-4">
|
||||
<ItemGroup>
|
||||
<Item variant="default">
|
||||
<ItemMedia variant="icon">
|
||||
<UserIcon />
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>User Profile</ItemTitle>
|
||||
<ItemDescription>View and edit your profile information</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button size="sm" variant="ghost">
|
||||
Edit
|
||||
</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<ItemSeparator />
|
||||
<Item variant="default">
|
||||
<ItemMedia variant="icon">
|
||||
<SettingsIcon />
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Settings</ItemTitle>
|
||||
<ItemDescription>Manage your account settings and preferences</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
<ItemSeparator />
|
||||
<Item variant="default">
|
||||
<ItemMedia variant="icon">
|
||||
<BellIcon />
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Notifications</ItemTitle>
|
||||
<ItemDescription>Configure notification preferences</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Badge type="round" variant="info">
|
||||
3
|
||||
</Badge>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
</ItemGroup>
|
||||
|
||||
<Item variant="outline">
|
||||
<ItemContent>
|
||||
<ItemHeader>
|
||||
<ItemTitle>Item with Header and Footer</ItemTitle>
|
||||
<Badge variant="default">New</Badge>
|
||||
</ItemHeader>
|
||||
<ItemDescription>This item has a header and footer layout</ItemDescription>
|
||||
<ItemFooter>
|
||||
<span className="text-xs text-muted-foreground">2 hours ago</span>
|
||||
<Button size="sm" variant="link">
|
||||
View More
|
||||
</Button>
|
||||
</ItemFooter>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
|
||||
<Item size="sm" variant="select">
|
||||
<ItemContent>
|
||||
<ItemTitle>Select Variant</ItemTitle>
|
||||
<ItemDescription>This item uses the select variant with small size</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
|
||||
<Item variant="muted">
|
||||
<ItemContent>
|
||||
<ItemTitle>Muted Variant</ItemTitle>
|
||||
<ItemDescription>This item has a subtle muted background</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -0,0 +1,84 @@
|
||||
import type { Meta } from "@storybook/react-vite"
|
||||
import { Button } from "./button"
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "./popover"
|
||||
|
||||
const meta: Meta<typeof Popover> = {
|
||||
title: "Ui/Popover",
|
||||
component: Popover,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Displays rich content in a floating panel when clicking a trigger element. Built on Radix UI with customizable alignment, side positioning, and an arrow indicator.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
export const Default = () => (
|
||||
<div className="w-screen flex justify-center items-center min-h-[400px]">
|
||||
<div className="flex flex-col gap-8 w-full max-w-md px-4">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="cline">Open Popover</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium leading-none">Popover Title</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This is the content inside the popover. It can contain any React components.
|
||||
</p>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<div className="flex gap-4 justify-center">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="secondary">Left Align</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="start">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm">This popover is aligned to the start of the trigger.</p>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="secondary">Right Align</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm">This popover is aligned to the end of the trigger.</p>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="default">With Actions</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium leading-none">Confirm Action</h4>
|
||||
<p className="text-sm text-muted-foreground">Are you sure you want to proceed?</p>
|
||||
</div>
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button size="sm" variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm" variant="default">
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { Meta } from "@storybook/react-vite"
|
||||
import { Progress } from "./progress"
|
||||
|
||||
const meta: Meta<typeof Progress> = {
|
||||
title: "Ui/Progress",
|
||||
component: Progress,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Displays a horizontal progress bar with a smooth animated indicator. Built on Radix UI and accepts a value between 0-100 to show completion percentage.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
export const Default = () => (
|
||||
<div className="w-screen flex justify-center items-center">
|
||||
<div className="flex flex-col gap-6 w-full max-w-md px-4">
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm text-muted-foreground">0% Complete</div>
|
||||
<Progress value={0} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm text-muted-foreground">25% Complete</div>
|
||||
<Progress value={25} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm text-muted-foreground">50% Complete</div>
|
||||
<Progress value={50} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm text-muted-foreground">75% Complete</div>
|
||||
<Progress value={75} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm text-muted-foreground">100% Complete</div>
|
||||
<Progress value={100} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { Meta } from "@storybook/react-vite"
|
||||
import { Separator } from "./separator"
|
||||
|
||||
const meta: Meta<typeof Separator> = {
|
||||
title: "Ui/Separator",
|
||||
component: Separator,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Visually separates content with a horizontal or vertical line. Built on Radix UI and supports both orientations for flexible layout separation.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
export const Default = () => (
|
||||
<div className="w-screen flex justify-center items-center">
|
||||
<div className="flex flex-col gap-8 w-full max-w-md px-4">
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-sm font-medium">Horizontal Separator</h4>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm">Content above the separator</p>
|
||||
</div>
|
||||
<Separator orientation="horizontal" />
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm">Content below the separator</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-sm font-medium">Vertical Separator</h4>
|
||||
<div className="flex h-20 items-center space-x-4">
|
||||
<div className="text-sm">Left content</div>
|
||||
<Separator orientation="vertical" />
|
||||
<div className="text-sm">Middle content</div>
|
||||
<Separator orientation="vertical" />
|
||||
<div className="text-sm">Right content</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-sm font-medium">In a Menu Layout</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm p-2 hover:bg-accent rounded-sm cursor-pointer">Menu Item 1</div>
|
||||
<div className="text-sm p-2 hover:bg-accent rounded-sm cursor-pointer">Menu Item 2</div>
|
||||
<Separator />
|
||||
<div className="text-sm p-2 hover:bg-accent rounded-sm cursor-pointer">Menu Item 3</div>
|
||||
<div className="text-sm p-2 hover:bg-accent rounded-sm cursor-pointer">Menu Item 4</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { Meta } from "@storybook/react-vite"
|
||||
import { Slider } from "./slider"
|
||||
|
||||
const meta: Meta<typeof Slider> = {
|
||||
title: "Ui/Slider",
|
||||
component: Slider,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"An interactive slider control for selecting a value from a range. Built on Radix UI with a draggable thumb and visual track indicator. Supports accessibility features like keyboard navigation.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
export const Default = () => (
|
||||
<div className="w-screen flex justify-center items-center">
|
||||
<div className="flex flex-col gap-6 w-full max-w-md px-4">
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm text-muted-foreground">Default (0-100)</div>
|
||||
<Slider defaultValue={[50]} max={100} step={1} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm text-muted-foreground">At minimum (0)</div>
|
||||
<Slider defaultValue={[0]} max={100} step={1} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm text-muted-foreground">At maximum (100)</div>
|
||||
<Slider defaultValue={[100]} max={100} step={1} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm text-muted-foreground">Custom range (0-10, step 0.5)</div>
|
||||
<Slider defaultValue={[5]} max={10} step={0.5} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm text-muted-foreground">Disabled state</div>
|
||||
<Slider defaultValue={[50]} disabled max={100} step={1} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider"
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Slider = React.forwardRef<
|
||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SliderPrimitive.Root
|
||||
className={cn("relative flex w-full touch-none select-none items-center", className)}
|
||||
ref={ref}
|
||||
{...props}>
|
||||
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-accent/20">
|
||||
<SliderPrimitive.Range className="absolute h-full bg-button-background" />
|
||||
</SliderPrimitive.Track>
|
||||
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border border-button-background/50 bg-foreground shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
|
||||
</SliderPrimitive.Root>
|
||||
))
|
||||
Slider.displayName = SliderPrimitive.Root.displayName
|
||||
|
||||
export { Slider }
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { Meta } from "@storybook/react-vite"
|
||||
import { Switch } from "./switch"
|
||||
|
||||
const meta: Meta<typeof Switch> = {
|
||||
title: "Ui/Switch",
|
||||
component: Switch,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"A toggle switch component for binary on/off states. Built on Radix UI with smooth animations and keyboard accessibility support.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
export const Default = () => (
|
||||
<div className="w-screen flex justify-center items-center">
|
||||
<div className="flex flex-col gap-6 w-full max-w-md px-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium cursor-pointer" htmlFor="switch-1">
|
||||
Default switch (unchecked)
|
||||
</label>
|
||||
<Switch id="switch-1" />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium cursor-pointer" htmlFor="switch-2">
|
||||
Checked switch
|
||||
</label>
|
||||
<Switch defaultChecked id="switch-2" />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium cursor-not-allowed opacity-50" htmlFor="switch-3">
|
||||
Disabled switch
|
||||
</label>
|
||||
<Switch disabled id="switch-3" />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium cursor-not-allowed opacity-50" htmlFor="switch-4">
|
||||
Disabled checked switch
|
||||
</label>
|
||||
<Switch defaultChecked disabled id="switch-4" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 p-4 bg-accent/20 rounded-sm">
|
||||
<h4 className="text-sm font-medium">Setting Example</h4>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<label className="text-sm font-medium cursor-pointer" htmlFor="notifications">
|
||||
Enable notifications
|
||||
</label>
|
||||
<div className="text-xs text-muted-foreground">Receive updates about your account activity</div>
|
||||
</div>
|
||||
<Switch id="notifications" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -0,0 +1,101 @@
|
||||
import type { Meta } from "@storybook/react-vite"
|
||||
import { Table, TableBody, TableCaption, TableCell, TableEmpty, TableFooter, TableHead, TableHeader, TableRow } from "./table"
|
||||
|
||||
const meta: Meta<typeof Table> = {
|
||||
title: "Ui/Table",
|
||||
component: Table,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"A flexible table component built on semantic HTML. Includes styled subcomponents for header, body, footer, rows, cells, and captions with hover effects and proper border styling.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
export const PaymentHistory = () => (
|
||||
<div className="w-screen flex justify-center items-center">
|
||||
<div className="flex flex-col gap-6 w-full max-w-3xl px-4">
|
||||
<Table>
|
||||
<TableCaption>Your recent payment history</TableCaption>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[100px]">Date</TableHead>
|
||||
<TableHead>Description</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Method</TableHead>
|
||||
<TableHead className="text-right">Amount</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="font-medium">Jan 15</TableCell>
|
||||
<TableCell>Team Plan</TableCell>
|
||||
<TableCell>Paid</TableCell>
|
||||
<TableCell>Credit Card</TableCell>
|
||||
<TableCell className="text-right">$29.99</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="font-medium">Dec 15</TableCell>
|
||||
<TableCell>Team Plan</TableCell>
|
||||
<TableCell>Paid</TableCell>
|
||||
<TableCell>Credit Card</TableCell>
|
||||
<TableCell className="text-right">$29.99</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="font-medium">Nov 15</TableCell>
|
||||
<TableCell>Team Plan</TableCell>
|
||||
<TableCell>Paid</TableCell>
|
||||
<TableCell>PayPal</TableCell>
|
||||
<TableCell className="text-right">$29.99</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="font-medium">Oct 15</TableCell>
|
||||
<TableCell>Team Plan</TableCell>
|
||||
<TableCell>Paid</TableCell>
|
||||
<TableCell>Credit Card</TableCell>
|
||||
<TableCell className="text-right">$29.99</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="font-medium">Sep 15</TableCell>
|
||||
<TableCell>Team Plan</TableCell>
|
||||
<TableCell>Paid</TableCell>
|
||||
<TableCell>Credit Card</TableCell>
|
||||
<TableCell className="text-right">$29.99</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
<TableCell colSpan={4}>Total (Last 5 months)</TableCell>
|
||||
<TableCell className="text-right">$149.95</TableCell>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
export const EmptyState = () => (
|
||||
<div className="w-screen flex justify-center items-center">
|
||||
<div className="flex flex-col gap-6 w-full max-w-3xl px-4">
|
||||
<Table>
|
||||
<TableCaption>Your payment history.</TableCaption>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[100px]">Date</TableHead>
|
||||
<TableHead>Description</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Method</TableHead>
|
||||
<TableHead className="text-right">Amount</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableEmpty>No payment history yet.</TableEmpty>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -0,0 +1,83 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table className={cn("w-full caption-bottom text-sm", className)} ref={ref} {...props} />
|
||||
</div>
|
||||
))
|
||||
Table.displayName = "Table"
|
||||
|
||||
const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
|
||||
({ className, ...props }, ref) => <thead className={cn("[&_tr]:border-b", className)} ref={ref} {...props} />,
|
||||
)
|
||||
TableHeader.displayName = "TableHeader"
|
||||
|
||||
const TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
|
||||
({ className, ...props }, ref) => <tbody className={cn("[&_tr:last-child]:border-0", className)} ref={ref} {...props} />,
|
||||
)
|
||||
TableBody.displayName = "TableBody"
|
||||
|
||||
const TableEmpty = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
|
||||
({ className, children, ...props }, ref) => (
|
||||
<TableRow className={cn("hover:bg-transparent", className)} ref={ref} {...props}>
|
||||
<TableCell className="h-24 text-center text-muted-foreground" colSpan={100}>
|
||||
{children || "No results."}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
),
|
||||
)
|
||||
TableEmpty.displayName = "TableEmpty"
|
||||
|
||||
const TableFooter = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<tfoot className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)} ref={ref} {...props} />
|
||||
),
|
||||
)
|
||||
TableFooter.displayName = "TableFooter"
|
||||
|
||||
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<tr
|
||||
className={cn("border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", className)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
)
|
||||
TableRow.displayName = "TableRow"
|
||||
|
||||
const TableHead = React.forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<HTMLTableCellElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<th
|
||||
className={cn(
|
||||
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
)
|
||||
TableHead.displayName = "TableHead"
|
||||
|
||||
const TableCell = React.forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<td
|
||||
className={cn("p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", className)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
)
|
||||
TableCell.displayName = "TableCell"
|
||||
|
||||
const TableCaption = React.forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<caption className={cn("mt-4 text-sm text-muted-foreground", className)} ref={ref} {...props} />
|
||||
),
|
||||
)
|
||||
TableCaption.displayName = "TableCaption"
|
||||
|
||||
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption, TableEmpty }
|
||||
@@ -0,0 +1,18 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className,
|
||||
)}
|
||||
data-slot="textarea"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Textarea }
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { Meta } from "@storybook/react-vite"
|
||||
import { Bold, Italic, Underline } from "lucide-react"
|
||||
import { Toggle } from "./toggle"
|
||||
|
||||
const meta: Meta<typeof Toggle> = {
|
||||
title: "Ui/Toggle",
|
||||
component: Toggle,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"A two-state button that can be either on or off. Toggle components are used for binary state changes, such as enabling/disabling features or applying formatting options.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
export const Overview = () => {
|
||||
const variants = ["default", "outline"] as const
|
||||
|
||||
const states = [
|
||||
{ value: "icon-only", label: "Icon Only" },
|
||||
{ value: "with-text", label: "With Text" },
|
||||
{ value: "disabled", label: "Disabled" },
|
||||
{ value: "pressed", label: "Default Pressed" },
|
||||
] as const
|
||||
|
||||
const sizes = [
|
||||
{ value: "sm", label: "Small" },
|
||||
{ value: "default", label: "Default" },
|
||||
{ value: "lg", label: "Large" },
|
||||
] as const
|
||||
|
||||
return (
|
||||
<div className="w-screen flex justify-center p-8">
|
||||
<div className="flex flex-col gap-8 w-full max-w-4xl">
|
||||
{states.map((state) => (
|
||||
<div className="flex flex-col gap-4" key={state.value}>
|
||||
<h2 className="text-lg font-semibold">{state.label}</h2>
|
||||
<div className="flex gap-8">
|
||||
{sizes.map((size) => (
|
||||
<div className="flex flex-col gap-3" key={size.value}>
|
||||
<h3 className="text-sm font-medium">{size.label}</h3>
|
||||
<div className="flex gap-2">
|
||||
{variants.map((variant) => {
|
||||
const sizeValue = size.value === "default" ? undefined : size.value
|
||||
const commonProps = {
|
||||
key: variant,
|
||||
variant,
|
||||
size: sizeValue,
|
||||
}
|
||||
|
||||
if (state.value === "icon-only") {
|
||||
return (
|
||||
<Toggle {...commonProps} aria-label="Toggle bold">
|
||||
<Bold />
|
||||
</Toggle>
|
||||
)
|
||||
}
|
||||
|
||||
if (state.value === "with-text") {
|
||||
return (
|
||||
<Toggle {...commonProps}>
|
||||
<Bold />
|
||||
Bold
|
||||
</Toggle>
|
||||
)
|
||||
}
|
||||
|
||||
if (state.value === "disabled") {
|
||||
return (
|
||||
<Toggle {...commonProps} disabled>
|
||||
<Italic />
|
||||
</Toggle>
|
||||
)
|
||||
}
|
||||
|
||||
if (state.value === "pressed") {
|
||||
return (
|
||||
<Toggle {...commonProps} defaultPressed>
|
||||
<Underline />
|
||||
</Toggle>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import * as TogglePrimitive from "@radix-ui/react-toggle"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline: "border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-2 min-w-9 [&_svg]:size-3",
|
||||
sm: "h-8 px-1.5 min-w-8 [&_svg]:size-2",
|
||||
lg: "h-10 px-2.5 min-w-10 [&_svg]:size-4",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
const Toggle = React.forwardRef<
|
||||
React.ElementRef<typeof TogglePrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> & VariantProps<typeof toggleVariants>
|
||||
>(({ className, variant, size, ...props }, ref) => (
|
||||
<TogglePrimitive.Root className={cn(toggleVariants({ variant, size, className }))} ref={ref} {...props} />
|
||||
))
|
||||
|
||||
Toggle.displayName = TogglePrimitive.Root.displayName
|
||||
|
||||
export { Toggle, toggleVariants }
|
||||
@@ -0,0 +1,101 @@
|
||||
import type { Meta } from "@storybook/react-vite"
|
||||
import { InfoIcon } from "lucide-react"
|
||||
import { Button } from "./button"
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip"
|
||||
|
||||
const meta: Meta<typeof Tooltip> = {
|
||||
title: "Ui/Tooltip",
|
||||
component: Tooltip,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Displays helpful text when hovering over an element. Built on Radix UI with customizable positioning, optional arrow indicator, and smooth animations.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
export const Default = () => (
|
||||
<div className="w-screen flex justify-center items-center min-h-[400px]">
|
||||
<div className="flex flex-col gap-8 w-full max-w-md px-4 items-center">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button>Hover for tooltip</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>This is a helpful tooltip</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="secondary">Top</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
<p>Tooltip on top</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="secondary">Bottom</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<p>Tooltip on bottom</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="secondary">Left</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left">
|
||||
<p>Tooltip on left</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="secondary">Right</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>Tooltip on right</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button size="icon" variant="icon">
|
||||
<InfoIcon />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Click for more information</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button>No arrow</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent showArrow={false}>
|
||||
<p>This tooltip has no arrow</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="text-sm underline cursor-help">Hover me</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Tooltips can wrap any element, not just buttons</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -1,5 +1,6 @@
|
||||
/* Editor theme mapping */
|
||||
@theme {
|
||||
--color-cline: #9663f1;
|
||||
--color-background: var(--vscode-sideBar-background);
|
||||
--color-border: var(--vscode-focusBorder);
|
||||
--color-border-panel: var(--vscode-panel-border);
|
||||
@@ -46,6 +47,7 @@
|
||||
--color-toolbar-default: var(--vscode-toolbar-background);
|
||||
--color-toolbar-hover: var(--vscode-toolbar-hoverBackground);
|
||||
--color-error: var(--vscode-errorForeground);
|
||||
--color-error-icon: var(--vscode-problemsErrorIcon-foreground);
|
||||
--color-description: var(--vscode-descriptionForeground);
|
||||
--color-success: var(--vscode-charts-green);
|
||||
--color-warning: var(--vscode-charts-yellow);
|
||||
@@ -188,4 +190,18 @@
|
||||
clip: auto;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
@keyframes flash {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-flash {
|
||||
animation: flash 1.5s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user