feat: add Popover docs

This commit is contained in:
p-sw 2024-06-05 21:31:50 +09:00
parent a51d7eead1
commit fdb4d0d70f
5 changed files with 372 additions and 0 deletions

View File

@ -0,0 +1,117 @@
import { TabProvider, TabTrigger, TabContent, TabList } from "@components/Tabs";
import { Story } from "@/components/Story";
import { LoadedCode, GITHUB } from "@/components/LoadedCode";
import { PopoverDemo } from "./PopoverBlocks/Preview";
import Examples from "./PopoverBlocks/Examples";
# Popover
Displays rich content in a portal, triggered by a button.
<TabProvider defaultName="preview">
<TabList>
<TabTrigger name="preview">Preview</TabTrigger>
<TabTrigger name="code">Code</TabTrigger>
</TabList>
<TabContent name="preview">
<Story layout="centered">
<PopoverDemo />
</Story>
</TabContent>
<TabContent name="code">
<LoadedCode from={`${GITHUB}/packages/react/src/docs/components/PopoverBlocks/Preview.tsx`} />
</TabContent>
</TabProvider>
## Installation
1. Create a new file `Popover.tsx` in your component folder.
2. Copy and paste the following code into the file.
<LoadedCode from={`${GITHUB}/packages/react/components/Popover.tsx`} />
## Usage
```tsx
import { Popover, PopoverTrigger, PopoverContent } from "@components/popover"
```
```html
<Popover>
<PopoverTrigger>
<Button />
</PopoverTrigger>
<PopoverContent>
{/* content of popover */}
</PopoverContent>
</Popover
```
> Note:
>
> PopoverTrigger will merge its onClick event handler to its children.
> Also, there is no default element for those.
> So you always have to provide the clickable children for PopoverTrigger.
>
> It is easier to understand if you think of this component as always having the `asChild` prop applied to it.
## Props
### Popover
#### Special
| Prop | Type | Default | Description |
|:----------|:----------|:--------|:------------------------------------------------------------------|
| `opened` | `boolean` | `false` | Initial open state |
| `asChild` | `boolean` | `false` | Whether the root of popover is rendered as a child of a component |
### PopoverContent
#### Variants
| Prop | Type | Default | Description |
|:---------|:-------------------------------------------------------------------------|:-----------------|:--------------------------------|
| `anchor` | `` `${"top" \| "middle" \| "bottom"}${"Left" \| "Center" \| "Right"}` `` | `"bottomCenter"` | Position of Popover content |
| `offset` | `"sm" \| "md" \| "lg"` | `"md"` | Gap between trigger and popover |
#### Special
| Prop | Type | Default | Description |
|:----------|:----------|:--------|:--------------------------------------------------------------------------------|
| `asChild` | `boolean` | `false` | `Whether the container of popover content is rendered as a child of a component |
## Examples
### Theme Selector
<TabProvider defaultName={"preview"}>
<TabList>
<TabTrigger name={"preview"}>Preview</TabTrigger>
<TabTrigger name={"code"}>Code</TabTrigger>
</TabList>
<TabContent name={"preview"}>
<Story layout={"centered"}>
<Examples.ThemeSelector />
</Story>
</TabContent>
<TabContent name={"code"}>
<LoadedCode from={`${GITHUB}/packages/react/src/docs/components/PopoverBlocks/Examples/ThemeSelector.tsx`} />
</TabContent>
</TabProvider>
### User Control
<TabProvider defaultName={"preview"}>
<TabList>
<TabTrigger name={"preview"}>Preview</TabTrigger>
<TabTrigger name={"code"}>Code</TabTrigger>
</TabList>
<TabContent name={"preview"}>
<Story layout={"centered"}>
<Examples.UserControl />
</Story>
</TabContent>
<TabContent name={"code"}>
<LoadedCode from={`${GITHUB}/packages/react/src/docs/components/PopoverBlocks/Examples/UserControl.tsx`} />
</TabContent>
</TabProvider>

View File

@ -0,0 +1,43 @@
import { Popover, PopoverTrigger, PopoverContent } from "@components/Popover.tsx";
import { Button } from "@components/Button.tsx";
import { useState } from "react";
const DarkIcon = () => {
// ic:baseline-dark-mode
return <svg xmlns="http://www.w3.org/2000/svg" width="1.2em" height="1.2em" viewBox="0 0 24 24">
<path fill="currentColor"
d="M12 3a9 9 0 1 0 9 9c0-.46-.04-.92-.1-1.36a5.389 5.389 0 0 1-4.4 2.26a5.403 5.403 0 0 1-3.14-9.8c-.44-.06-.9-.1-1.36-.1"/>
</svg>
}
const LightIcon = () => {
// ic:baseline-light-mode
return <svg xmlns="http://www.w3.org/2000/svg" width="1.2em" height="1.2em" viewBox="0 0 24 24">
<path fill="currentColor"
d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5s5-2.24 5-5s-2.24-5-5-5M2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1m18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1M11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1m0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1M5.99 4.58a.996.996 0 0 0-1.41 0a.996.996 0 0 0 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0s.39-1.03 0-1.41zm12.37 12.37a.996.996 0 0 0-1.41 0a.996.996 0 0 0 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0a.996.996 0 0 0 0-1.41zm1.06-10.96a.996.996 0 0 0 0-1.41a.996.996 0 0 0-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0zM7.05 18.36a.996.996 0 0 0 0-1.41a.996.996 0 0 0-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0z"/>
</svg>
}
export const ThemeSelector = () => {
const [theme, setTheme] = useState<"light" | "dark">("dark");
return <Popover>
<PopoverTrigger>
<Button preset={"default"} size={"icon"}>
{
theme === "light" ? <LightIcon /> : <DarkIcon />
}
</Button>
</PopoverTrigger>
<PopoverContent anchor={"bottomCenter"}>
<Button onClick={() => setTheme("dark")} preset={"ghost"} className={"gap-2"}>
<DarkIcon />
<span className={"whitespace-nowrap"}>Dark Mode</span>
</Button>
<Button onClick={() => setTheme("light")} preset={"ghost"} className={"gap-2"}>
<LightIcon />
<span className={"whitespace-nowrap"}>Light Mode</span>
</Button>
</PopoverContent>
</Popover>
}

View File

@ -0,0 +1,151 @@
import {
Popover,
PopoverTrigger,
PopoverContent,
} from "@components/Popover.tsx";
import { Button } from "@components/Button.tsx";
import { useToast } from "@components/Toast.tsx";
import {
createContext,
Dispatch,
SetStateAction,
SVGProps,
useContext,
useState,
useTransition,
} from "react";
import { Label } from "@components/Label.tsx";
import { Input } from "@components/Input.tsx";
interface UserControlState {
signIn: boolean;
}
const initialState: UserControlState = {
signIn: false,
};
const UserControlContext = createContext<
[UserControlState, Dispatch<SetStateAction<UserControlState>>]
>([initialState, () => {}]);
const logInServerAction = async () => {
return new Promise((r) => setTimeout(r, 2000));
};
const logOutServerAction = async () => {
return new Promise((r) => setTimeout(r, 1000));
};
function MdiLoading(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1.2em"
height="1.2em"
viewBox="0 0 24 24"
{...props}
>
<path
fill="currentColor"
d="M12 4V2A10 10 0 0 0 2 12h2a8 8 0 0 1 8-8"
></path>
</svg>
);
}
const SignInForm = () => {
const [isSigningIn, setIsSigningIn] = useState(false);
const transition = useTransition();
const [_, setState] = useContext(UserControlContext);
const { toast } = useToast();
function startSignIn() {
transition[1](() => {
setIsSigningIn(true);
const toasted = toast({
title: "Logging In...",
description: "Please wait until server responses",
status: "loading",
});
logInServerAction().then(() => {
toasted.update({
title: "Log In Success",
description: "Successfully logged in!",
status: "success",
});
setIsSigningIn(false);
setState((prev) => ({ ...prev, signIn: true }));
});
});
}
return (
<PopoverContent anchor={"bottomLeft"} className={"p-4 space-y-3"}>
<Label>
<span>Username</span>
<Input type={"text"} />
</Label>
<Label>
<span>Password</span>
<Input type={"password"} />
</Label>
<div className={"flex flex-row justify-end"}>
<Button preset={"success"} onClick={startSignIn}>
{isSigningIn ? <MdiLoading className={"animate-spin"} /> : "Sign In"}
</Button>
</div>
</PopoverContent>
);
};
const UserControlContent = () => {
const [isSigningOut, setIsSigningOut] = useState(false);
const transition = useTransition();
const [_, setState] = useContext(UserControlContext);
const { toast } = useToast();
function startSignOut() {
transition[1](() => {
setIsSigningOut(true);
const toasted = toast({
title: "Logging Out",
description: "Please wait until server responses",
status: "loading",
});
logOutServerAction().then(() => {
toasted.update({
title: "Log Out Success",
description: "Successfully logged out!",
status: "success",
});
setIsSigningOut(false);
setState((prev) => ({ ...prev, signIn: false }));
});
});
}
return (
<PopoverContent anchor={"bottomLeft"}>
<Button preset={"ghost"}>Dashboard</Button>
<Button preset={"ghost"} onClick={startSignOut}>
{isSigningOut ? <MdiLoading className={"animate-spin"} /> : "Sign Out"}
</Button>
</PopoverContent>
);
};
export const UserControl = () => {
const [state, setState] = useState<UserControlState>({
signIn: false,
});
return (
<Popover>
<PopoverTrigger>
<Button>{state.signIn ? "Log Out" : "Log In"}</Button>
</PopoverTrigger>
<UserControlContext.Provider value={[state, setState]}>
{state.signIn ? <UserControlContent /> : <SignInForm />}
</UserControlContext.Provider>
</Popover>
);
};

View File

@ -0,0 +1,7 @@
import { ThemeSelector } from "./ThemeSelector";
import { UserControl } from "./UserControl";
export default {
ThemeSelector,
UserControl,
}

View File

@ -0,0 +1,54 @@
import { Button } from "@components/Button";
import { Popover, PopoverContent, PopoverTrigger } from "@components/Popover";
export function PopoverDemo() {
return (
<Popover>
<PopoverTrigger>
<Button size="icon">
<svg
xmlns="http://www.w3.org/2000/svg"
width="1.2em"
height="1.2em"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 4a4 4 0 0 1 4 4a4 4 0 0 1-4 4a4 4 0 0 1-4-4a4 4 0 0 1 4-4m0 10c4.42 0 8 1.79 8 4v2H4v-2c0-2.21 3.58-4 8-4"
/>
</svg>
</Button>
</PopoverTrigger>
<PopoverContent>
<Button preset="ghost" className="gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
width="1.2em"
height="1.2em"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"
/>
</svg>
<span className="flex-grow text-left">Dashboard</span>
</Button>
<Button preset="ghost" className="gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
width="1.2em"
height="1.2em"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="m17 7l-1.41 1.41L18.17 11H8v2h10.17l-2.58 2.58L17 17l5-5M4 5h8V3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8v-2H4z"
/>
</svg>
<span className="flex-grow text-left">Log out</span>
</Button>
</PopoverContent>
</Popover>
);
}