Ensure playgrounds work + switch to npm workspaces (#2907)

* bump Next in playground

* convert legacy Link after Next.js bump

* update yarn.lock

* switch to npm workspaces

* move `packages/playground-*` to `playgrounds/*`

* use `npm` instead of `yarn`

* sync package-lock.json

* use node 20 for insiders releases
This commit is contained in:
Robin Malfait
2024-01-03 14:26:12 +01:00
committed by GitHub
parent 3b961a690f
commit a73007388f
120 changed files with 10733 additions and 6944 deletions
+20
View File
@@ -0,0 +1,20 @@
import { ComponentProps, forwardRef, ReactNode } from 'react'
function classNames(...classes: (string | false | undefined | null)[]) {
return classes.filter(Boolean).join(' ')
}
export let Button = forwardRef<
HTMLButtonElement,
ComponentProps<'button'> & { children?: ReactNode }
>(({ className, ...props }, ref) => (
<button
ref={ref}
type="button"
className={classNames(
'ui-focus-visible:ring-2 ui-focus-visible:ring-offset-2 flex items-center rounded-md border border-gray-300 bg-white px-2 py-1 ring-gray-500 ring-offset-gray-100 focus:outline-none',
className
)}
{...props}
/>
))
+253
View File
@@ -0,0 +1,253 @@
export let countries = [
'Afghanistan',
'Albania',
'Algeria',
'American Samoa',
'Andorra',
'Angola',
'Anguilla',
'Antarctica',
'Antigua and Barbuda',
'Argentina',
'Armenia',
'Aruba',
'Australia',
'Austria',
'Azerbaijan',
'Bahamas (the)',
'Bahrain',
'Bangladesh',
'Barbados',
'Belarus',
'Belgium',
'Belize',
'Benin',
'Bermuda',
'Bhutan',
'Bolivia (Plurinational State of)',
'Bonaire, Sint Eustatius and Saba',
'Bosnia and Herzegovina',
'Botswana',
'Bouvet Island',
'Brazil',
'British Indian Ocean Territory (the)',
'Brunei Darussalam',
'Bulgaria',
'Burkina Faso',
'Burundi',
'Cabo Verde',
'Cambodia',
'Cameroon',
'Canada',
'Cayman Islands (the)',
'Central African Republic (the)',
'Chad',
'Chile',
'China',
'Christmas Island',
'Cocos (Keeling) Islands (the)',
'Colombia',
'Comoros (the)',
'Congo (the Democratic Republic of the)',
'Congo (the)',
'Cook Islands (the)',
'Costa Rica',
'Croatia',
'Cuba',
'Curaçao',
'Cyprus',
'Czechia',
"Côte d'Ivoire",
'Denmark',
'Djibouti',
'Dominica',
'Dominican Republic (the)',
'Ecuador',
'Egypt',
'El Salvador',
'Equatorial Guinea',
'Eritrea',
'Estonia',
'Eswatini',
'Ethiopia',
'Falkland Islands (the) [Malvinas]',
'Faroe Islands (the)',
'Fiji',
'Finland',
'France',
'French Guiana',
'French Polynesia',
'French Southern Territories (the)',
'Gabon',
'Gambia (the)',
'Georgia',
'Germany',
'Ghana',
'Gibraltar',
'Greece',
'Greenland',
'Grenada',
'Guadeloupe',
'Guam',
'Guatemala',
'Guernsey',
'Guinea',
'Guinea-Bissau',
'Guyana',
'Haiti',
'Heard Island and McDonald Islands',
'Holy See (the)',
'Honduras',
'Hong Kong',
'Hungary',
'Iceland',
'India',
'Indonesia',
'Iran (Islamic Republic of)',
'Iraq',
'Ireland',
'Isle of Man',
'Israel',
'Italy',
'Jamaica',
'Japan',
'Jersey',
'Jordan',
'Kazakhstan',
'Kenya',
'Kiribati',
"Korea (the Democratic People's Republic of)",
'Korea (the Republic of)',
'Kuwait',
'Kyrgyzstan',
"Lao People's Democratic Republic (the)",
'Latvia',
'Lebanon',
'Lesotho',
'Liberia',
'Libya',
'Liechtenstein',
'Lithuania',
'Luxembourg',
'Macao',
'Madagascar',
'Malawi',
'Malaysia',
'Maldives',
'Mali',
'Malta',
'Marshall Islands (the)',
'Martinique',
'Mauritania',
'Mauritius',
'Mayotte',
'Mexico',
'Micronesia (Federated States of)',
'Moldova (the Republic of)',
'Monaco',
'Mongolia',
'Montenegro',
'Montserrat',
'Morocco',
'Mozambique',
'Myanmar',
'Namibia',
'Nauru',
'Nepal',
'Netherlands (the)',
'New Caledonia',
'New Zealand',
'Nicaragua',
'Niger (the)',
'Nigeria',
'Niue',
'Norfolk Island',
'Northern Mariana Islands (the)',
'Norway',
'Oman',
'Pakistan',
'Palau',
'Palestine, State of',
'Panama',
'Papua New Guinea',
'Paraguay',
'Peru',
'Philippines (the)',
'Pitcairn',
'Poland',
'Portugal',
'Puerto Rico',
'Qatar',
'Republic of North Macedonia',
'Romania',
'Russian Federation (the)',
'Rwanda',
'Réunion',
'Saint Barthélemy',
'Saint Helena, Ascension and Tristan da Cunha',
'Saint Kitts and Nevis',
'Saint Lucia',
'Saint Martin (French part)',
'Saint Pierre and Miquelon',
'Saint Vincent and the Grenadines',
'Samoa',
'San Marino',
'Sao Tome and Principe',
'Saudi Arabia',
'Senegal',
'Serbia',
'Seychelles',
'Sierra Leone',
'Singapore',
'Sint Maarten (Dutch part)',
'Slovakia',
'Slovenia',
'Solomon Islands',
'Somalia',
'South Africa',
'South Georgia and the South Sandwich Islands',
'South Sudan',
'Spain',
'Sri Lanka',
'Sudan (the)',
'Suriname',
'Svalbard and Jan Mayen',
'Sweden',
'Switzerland',
'Syrian Arab Republic',
'Taiwan',
'Tajikistan',
'Tanzania, United Republic of',
'Thailand',
'Timor-Leste',
'Togo',
'Tokelau',
'Tonga',
'Trinidad and Tobago',
'Tunisia',
'Turkey',
'Turkmenistan',
'Turks and Caicos Islands (the)',
'Tuvalu',
'Uganda',
'Ukraine',
'United Arab Emirates (the)',
'United Kingdom of Great Britain and Northern Ireland (the)',
'United States Minor Outlying Islands (the)',
'United States of America (the)',
'Uruguay',
'Uzbekistan',
'Vanuatu',
'Venezuela (Bolivarian Republic of)',
'Viet Nam',
'Virgin Islands (British)',
'Virgin Islands (U.S.)',
'Wallis and Futuna',
'Western Sahara',
'Yemen',
'Zambia',
'Zimbabwe',
'Åland Islands',
]
export let timezones: string[] = Intl.supportedValuesOf('timeZone')
+5
View File
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
+7
View File
@@ -0,0 +1,7 @@
/** @type {import('next').NextConfig} */
module.exports = {
reactStrictMode: false,
devIndicators: {
buildActivity: false,
},
}
+40
View File
@@ -0,0 +1,40 @@
{
"name": "playground-react",
"private": true,
"version": "0.0.0",
"scripts": {
"prebuild": "npm run build --workspace=@headlessui/react && npm run build --workspace=@headlessui/tailwindcss",
"predev": "npm run build --workspace=@headlessui/react && npm run build --workspace=@headlessui/tailwindcss",
"dev:tailwindcss": "npm run watch --workspace=@headlessui/tailwindcss",
"dev:headlessui": "npm run watch --workspace=@headlessui/react",
"dev:next": "next dev",
"dev": "npm-run-all -p dev:*",
"build": "next build",
"start": "next start",
"lint-types": "echo",
"clean": "rimraf ./.next"
},
"dependencies": {
"@headlessui/react": "*",
"@headlessui/tailwindcss": "*",
"@heroicons/react": "^1.0.6",
"@popperjs/core": "^2.6.0",
"@tailwindcss/forms": "^0.5.2",
"@tailwindcss/typography": "^0.5.2",
"autoprefixer": "^10.4.7",
"framer-motion": "^6.0.0",
"next": "^14.0.4",
"postcss": "^8.4.14",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-flatpickr": "^3.10.9",
"react-hot-toast": "2.3.0",
"tailwindcss": "^3.3.3"
},
"devDependencies": {
"@floating-ui/react": "^0.24.8"
},
"resolutions": {
"next/@swc/helpers": "0.4.36"
}
}
+214
View File
@@ -0,0 +1,214 @@
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import 'tailwindcss/tailwind.css'
function disposables() {
let disposables: Function[] = []
let api = {
requestAnimationFrame(...args: Parameters<typeof requestAnimationFrame>) {
let raf = requestAnimationFrame(...args)
api.add(() => cancelAnimationFrame(raf))
},
nextFrame(...args: Parameters<typeof requestAnimationFrame>) {
api.requestAnimationFrame(() => {
api.requestAnimationFrame(...args)
})
},
setTimeout(...args: Parameters<typeof setTimeout>) {
let timer = setTimeout(...args)
api.add(() => clearTimeout(timer))
},
add(cb: () => void) {
disposables.push(cb)
},
dispose() {
for (let dispose of disposables.splice(0)) {
dispose()
}
},
}
return api
}
export function useDisposables() {
// Using useState instead of useRef so that we can use the initializer function.
let [d] = useState(disposables)
useEffect(() => () => d.dispose(), [d])
return d
}
enum KeyDisplayMac {
ArrowUp = '↑',
ArrowDown = '↓',
ArrowLeft = '←',
ArrowRight = '→',
Home = '↖',
End = '↘',
Alt = '⌥',
CapsLock = '⇪',
Meta = '⌘',
Shift = '⇧',
Control = '⌃',
Backspace = '⌫',
Delete = '⌦',
Enter = '↵',
Escape = '⎋',
Tab = '↹',
PageUp = '⇞',
PageDown = '⇟',
' ' = '␣',
}
enum KeyDisplayWindows {
ArrowUp = '↑',
ArrowDown = '↓',
ArrowLeft = '←',
ArrowRight = '→',
Meta = 'Win',
Control = 'Ctrl',
Backspace = '⌫',
Delete = 'Del',
Escape = 'Esc',
PageUp = 'PgUp',
PageDown = 'PgDn',
' ' = '␣',
}
function tap<T>(value: T, cb: (value: T) => void) {
cb(value)
return value
}
function useKeyDisplay() {
let [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) return {}
let isMac = navigator.userAgent.indexOf('Mac OS X') !== -1
return isMac ? KeyDisplayMac : KeyDisplayWindows
}
function KeyCaster() {
let [keys, setKeys] = useState<string[]>([])
let d = useDisposables()
let KeyDisplay = useKeyDisplay()
useEffect(() => {
function handler(event: KeyboardEvent) {
setKeys((current) => [
event.shiftKey && event.key !== 'Shift'
? KeyDisplay[`Shift${event.key}`] ?? event.key
: KeyDisplay[event.key] ?? event.key,
...current,
])
d.setTimeout(() => setKeys((current) => tap(current.slice(), (clone) => clone.pop())), 2000)
}
window.addEventListener('keydown', handler, true)
return () => window.removeEventListener('keydown', handler, true)
}, [d, KeyDisplay])
if (keys.length <= 0) return null
return (
<div className="pointer-events-none fixed bottom-4 right-4 z-50 cursor-default select-none overflow-hidden rounded-md bg-blue-800 px-4 py-2 text-2xl tracking-wide text-blue-100 shadow">
{keys.slice().reverse().join(' ')}
</div>
)
}
function MyApp({ Component, pageProps }) {
let router = useRouter()
if (router.query.raw !== undefined) {
return <Component {...pageProps} />
}
return (
<>
<div className="flex h-screen flex-col overflow-hidden bg-gray-700 font-sans text-gray-900 antialiased">
<header className="relative z-10 flex flex-shrink-0 items-center justify-between border-b border-gray-200 bg-gray-700 px-4 py-4 sm:px-6 lg:px-8">
<Link href="/">
<Logo className="h-6" />
</Link>
<span className="font-bold text-white">(React)</span>
</header>
<KeyCaster />
<main className="flex-1 overflow-auto bg-gray-50">
<Component {...pageProps} />
</main>
</div>
</>
)
}
function Logo({ className }) {
return (
<svg className={className} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 243 42">
<path
fill="#fff"
d="M65.74 13.663c-2.62 0-4.702.958-5.974 2.95V6.499h-4.163V33.32h4.163V23.051c0-3.908 2.159-5.518 4.896-5.518 2.62 0 4.317 1.533 4.317 4.445V33.32h4.162V21.557c0-4.982-3.083-7.894-7.4-7.894zM79.936 25.503h15.341c.077-.536.154-1.15.154-1.724 0-5.518-3.931-10.116-9.674-10.116-6.052 0-10.176 4.407-10.176 10.078 0 5.748 4.124 10.078 10.484 10.078 3.778 0 6.668-1.572 8.441-4.177l-3.43-1.993c-.925 1.341-2.66 2.376-4.972 2.376-3.084 0-5.512-1.533-6.168-4.521zm-.038-3.372c.578-2.873 2.698-4.713 5.82-4.713 2.506 0 4.934 1.418 5.512 4.713H79.898zM113.282 14.161v2.72c-1.465-1.992-3.739-3.218-6.746-3.218-5.242 0-9.597 4.368-9.597 10.078 0 5.67 4.355 10.078 9.597 10.078 3.007 0 5.281-1.227 6.746-3.258v2.76h4.162V14.16h-4.162zm-6.09 15.71c-3.469 0-6.091-2.567-6.091-6.13 0-3.564 2.622-6.131 6.091-6.131 3.469 0 6.09 2.567 6.09 6.13 0 3.564-2.621 6.132-6.09 6.132zM136.597 6.498v10.384c-1.465-1.993-3.739-3.219-6.746-3.219-5.242 0-9.597 4.368-9.597 10.078 0 5.67 4.355 10.078 9.597 10.078 3.007 0 5.281-1.227 6.746-3.258v2.76h4.163V6.497h-4.163zm-6.09 23.374c-3.469 0-6.09-2.568-6.09-6.131 0-3.564 2.621-6.131 6.09-6.131s6.09 2.567 6.09 6.13c0 3.564-2.621 6.132-6.09 6.132zM144.648 33.32h4.163V5.348h-4.163V33.32zM155.957 25.503h15.341c.077-.536.154-1.15.154-1.724 0-5.518-3.931-10.116-9.675-10.116-6.051 0-10.176 4.407-10.176 10.078 0 5.748 4.125 10.078 10.485 10.078 3.777 0 6.668-1.572 8.441-4.177l-3.43-1.993c-.926 1.341-2.66 2.376-4.973 2.376-3.083 0-5.512-1.533-6.167-4.521zm-.038-3.372c.578-2.873 2.698-4.713 5.82-4.713 2.505 0 4.934 1.418 5.512 4.713h-11.332zM177.137 19.45c0-1.38 1.311-2.032 2.814-2.032 1.581 0 2.93.69 3.623 2.184l3.508-1.954c-1.349-2.529-3.97-3.985-7.131-3.985-3.931 0-7.053 2.26-7.053 5.863 0 6.859 10.368 4.943 10.368 8.353 0 1.533-1.426 2.146-3.276 2.146-2.12 0-3.662-1.035-4.279-2.759l-3.584 2.07c1.233 2.758 4.008 4.483 7.863 4.483 4.163 0 7.516-2.07 7.516-5.902 0-7.088-10.369-4.98-10.369-8.468zM192.774 19.45c0-1.38 1.31-2.032 2.813-2.032 1.581 0 2.93.69 3.624 2.184l3.507-1.954c-1.349-2.529-3.97-3.985-7.131-3.985-3.931 0-7.053 2.26-7.053 5.863 0 6.859 10.368 4.943 10.368 8.353 0 1.533-1.426 2.146-3.276 2.146-2.12 0-3.662-1.035-4.278-2.759l-3.585 2.07c1.233 2.758 4.009 4.483 7.863 4.483 4.163 0 7.516-2.07 7.516-5.902 0-7.088-10.368-4.98-10.368-8.468zM224.523 28.9c2.889 0 5.027-1.715 5.027-4.53v-8.782h-2.588v8.577c0 1.268-.676 2.219-2.439 2.219s-2.438-.951-2.438-2.22v-8.576h-2.569v8.782c0 2.815 2.138 4.53 5.007 4.53zM232.257 15.588V28.64h2.588V15.588h-2.588z"
/>
<path
fill="#fff"
fillRule="evenodd"
d="M233.817 9.328H220.42c-2.96 0-5.359 2.385-5.359 5.327v13.318c0 2.942 2.399 5.327 5.359 5.327h13.397c2.959 0 5.358-2.385 5.358-5.327V14.655c0-2.942-2.399-5.327-5.358-5.327zM220.42 6.664c-4.439 0-8.038 3.578-8.038 7.99v13.319c0 4.413 3.599 7.99 8.038 7.99h13.397c4.439 0 8.038-3.577 8.038-7.99V14.655c0-4.413-3.599-7.99-8.038-7.99H220.42z"
clipRule="evenodd"
/>
<path
fill="#fff"
fillRule="evenodd"
d="M220.42 9.328h13.397c2.959 0 5.358 2.385 5.358 5.327v13.318c0 2.942-2.399 5.327-5.358 5.327H220.42c-2.96 0-5.359-2.385-5.359-5.327V14.655c0-2.942 2.399-5.327 5.359-5.327zm-8.038 5.327c0-4.413 3.599-7.99 8.038-7.99h13.397c4.439 0 8.038 3.577 8.038 7.99v13.318c0 4.413-3.599 7.99-8.038 7.99H220.42c-4.439 0-8.038-3.577-8.038-7.99V14.655z"
clipRule="evenodd"
/>
<path
fill="url(#prefix__paint0_linear)"
d="M8.577 26.097l25.779-8.556c-.514-3.201-.88-5.342-1.307-6.974-.457-1.756-.821-2.226-.965-2.39a5.026 5.026 0 00-1.81-1.306c-.2-.086-.762-.284-2.583-.175-1.924.116-4.453.507-8.455 1.137-4.003.63-6.529 1.035-8.395 1.516-1.766.456-2.239.817-2.403.96a4.999 4.999 0 00-1.315 1.8c-.085.198-.285.757-.175 2.568.116 1.913.51 4.426 1.143 8.405.178 1.114.337 2.113.486 3.015z"
/>
<path
fill="url(#prefix__paint1_linear)"
fillRule="evenodd"
d="M1.47 24.124C.244 16.427-.37 12.58.96 9.49A11.665 11.665 0 014.027 5.29c2.545-2.21 6.416-2.82 14.16-4.039C25.93.031 29.8-.578 32.907.743a11.729 11.729 0 014.225 3.05c2.223 2.53 2.836 6.38 4.063 14.076 1.226 7.698 1.84 11.546.511 14.636a11.666 11.666 0 01-3.069 4.199c-2.545 2.21-6.416 2.82-14.159 4.039-7.743 1.219-11.614 1.828-14.722.508a11.728 11.728 0 01-4.224-3.05C3.31 35.67 2.697 31.82 1.47 24.123zm13.657 13.668c2.074-.125 4.743-.54 8.697-1.163 3.953-.622 6.62-1.047 8.632-1.566 1.949-.502 2.846-.992 3.426-1.496a7.5 7.5 0 001.973-2.7c.302-.703.494-1.703.372-3.7-.125-2.063-.543-4.716-1.17-8.646-.625-3.93-1.053-6.582-1.574-8.582-.506-1.937-.999-2.83-1.505-3.405a7.54 7.54 0 00-2.716-1.961c-.707-.301-1.713-.492-3.723-.371-2.074.125-4.743.54-8.697 1.163-3.953.622-6.62 1.047-8.632 1.565-1.949.503-2.846.993-3.426 1.497a7.5 7.5 0 00-1.972 2.699c-.303.704-.495 1.704-.373 3.701.125 2.062.543 4.716 1.17 8.646.625 3.93 1.053 6.582 1.574 8.581.506 1.938 1 2.83 1.505 3.406a7.54 7.54 0 002.716 1.961c.707.3 1.713.492 3.723.37z"
clipRule="evenodd"
/>
<defs>
<linearGradient
id="prefix__paint0_linear"
x1="16.759"
x2="23.386"
y1="0"
y2="41.662"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#66E3FF" />
<stop offset="1" stopColor="#7064F9" />
</linearGradient>
<linearGradient
id="prefix__paint1_linear"
x1="16.759"
x2="23.386"
y1="0"
y2="41.662"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#66E3FF" />
<stop offset="1" stopColor="#7064F9" />
</linearGradient>
</defs>
</svg>
)
}
export default MyApp
+31
View File
@@ -0,0 +1,31 @@
import { Head, Html, Main, NextScript } from 'next/document'
export default function Document() {
return (
<Html>
<Head>
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
<link
rel="icon"
type="image/png"
sizes="32x32"
href="https://headlessui.dev/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="https://headlessui.dev/favicon-16x16.png"
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
+53
View File
@@ -0,0 +1,53 @@
import ErrorPage from 'next/error'
import Head from 'next/head'
import Link from 'next/link'
import { ExamplesType, resolveAllExamples } from '../utils/resolve-all-examples'
export async function getStaticProps() {
return {
props: {
examples: await resolveAllExamples('pages'),
},
}
}
export default function Page(props: { examples: false | ExamplesType[] }) {
if (props.examples === false) {
return <ErrorPage statusCode={404} />
}
return (
<>
<Head>
<title>Examples</title>
</Head>
<div className="container mx-auto my-24">
<div className="prose">
<h2>Examples</h2>
<Examples examples={props.examples} />
</div>
</div>
</>
)
}
export function Examples(props: { examples: ExamplesType[] }) {
return (
<ul>
{props.examples.map((example) => (
<li key={example.path}>
{example.children ? (
<h3 className="text-xl capitalize">{example.name}</h3>
) : (
<Link href={example.path} className="capitalize">
{example.name}
</Link>
)}
{example.children && <Examples examples={example.children} />}
</li>
))}
</ul>
)
}
@@ -0,0 +1,357 @@
import { Combobox, Listbox, RadioGroup, Switch } from '@headlessui/react'
import { useState } from 'react'
import { Button } from '../../components/button'
import { classNames } from '../../utils/class-names'
function Section({ title, children }) {
return (
<fieldset className="rounded-lg border bg-gray-200/20 p-3">
<legend className="rounded-md border bg-gray-100 px-2 text-sm uppercase">{title}</legend>
<div className="flex flex-col gap-3">{children}</div>
</fieldset>
)
}
let sizes = ['xs', 'sm', 'md', 'lg', 'xl']
let people = [
{ id: 1, name: { first: 'Alice' } },
{ id: 2, name: { first: 'Bob' } },
{ id: 3, name: { first: 'Charlie' } },
]
let locations = ['New York', 'London', 'Paris', 'Berlin']
export default function App() {
let [result, setResult] = useState(() =>
typeof window === 'undefined' || typeof document === 'undefined' ? [] : new FormData()
)
let [query, setQuery] = useState('')
return (
<div className="py-8">
<form
className="mx-auto flex h-full max-w-4xl flex-col items-start justify-center gap-8 rounded-lg border bg-white p-6"
onSubmit={(event) => {
event.preventDefault()
setResult(new FormData(event.currentTarget))
}}
>
<div className="grid w-full grid-cols-[repeat(auto-fill,minmax(350px,1fr))] items-start gap-3">
<Section title="Switch">
<Section title="Single value">
<Switch.Group as="div" className="flex items-center justify-between space-x-4">
<Switch.Label>Enable notifications</Switch.Label>
<Switch
defaultChecked={true}
name="notifications"
className={({ checked }) =>
classNames(
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2',
checked ? 'bg-blue-600' : 'bg-gray-200'
)
}
>
{({ checked }) => (
<span
className={classNames(
'inline-block h-5 w-5 transform rounded-full bg-white',
checked ? 'translate-x-5' : 'translate-x-0'
)}
/>
)}
</Switch>
</Switch.Group>
</Section>
<Section title="Multiple values">
<Switch.Group as="div" className="flex items-center justify-between space-x-4">
<Switch.Label>Apple</Switch.Label>
<Switch
name="fruit[]"
value="apple"
className={({ checked }) =>
classNames(
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2',
checked ? 'bg-blue-600' : 'bg-gray-200'
)
}
>
{({ checked }) => (
<span
className={classNames(
'inline-block h-5 w-5 transform rounded-full bg-white',
checked ? 'translate-x-5' : 'translate-x-0'
)}
/>
)}
</Switch>
</Switch.Group>
<Switch.Group as="div" className="flex items-center justify-between space-x-4">
<Switch.Label>Banana</Switch.Label>
<Switch
name="fruit[]"
value="banana"
className={({ checked }) =>
classNames(
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2',
checked ? 'bg-blue-600' : 'bg-gray-200'
)
}
>
{({ checked }) => (
<span
className={classNames(
'inline-block h-5 w-5 transform rounded-full bg-white',
checked ? 'translate-x-5' : 'translate-x-0'
)}
/>
)}
</Switch>
</Switch.Group>
</Section>
</Section>
<Section title="Radio Group">
<RadioGroup defaultValue="sm" name="size">
<div className="flex -space-x-px rounded-md bg-white">
{sizes.map((size) => {
return (
<RadioGroup.Option
key={size}
value={size}
className={({ active }) =>
classNames(
'relative flex w-20 border px-2 py-4 first:rounded-l-md last:rounded-r-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2',
active ? 'z-10 border-blue-200 bg-blue-50' : 'border-gray-200'
)
}
>
{({ active, checked }) => (
<div className="flex w-full items-center justify-between">
<div className="ml-3 flex cursor-pointer flex-col">
<span
className={classNames(
'block text-sm font-medium leading-5',
active ? 'text-blue-900' : 'text-gray-900'
)}
>
{size}
</span>
</div>
<div>
{checked && (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
className="h-5 w-5 text-blue-500"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
)}
</div>
</div>
)}
</RadioGroup.Option>
)
})}
</div>
</RadioGroup>
</Section>
<Section title="Listbox">
<div className="w-full space-y-1">
<Listbox name="person" defaultValue={people[1]}>
{({ value }) => (
<>
<div className="relative">
<Listbox.Button as={Button} className="w-full">
<span className="block truncate">{value?.name?.first}</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<svg
className="h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
>
<path
d="M7 7l3-3 3 3m0 6l-3 3-3-3"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
</Listbox.Button>
<div className="absolute z-10 mt-1 w-full rounded-md bg-white shadow-lg">
<Listbox.Options className="shadow-xs max-h-60 overflow-auto rounded-md py-1 text-base leading-6 focus:outline-none sm:text-sm sm:leading-5">
{people.map((person) => (
<Listbox.Option
key={person.id}
value={person}
className={({ active }) => {
return classNames(
'relative cursor-default select-none py-2 pl-3 pr-9 ',
active ? 'bg-blue-600 text-white' : 'text-gray-900'
)
}}
>
{({ active, selected }) => (
<>
<span
className={classNames(
'block truncate',
selected ? 'font-semibold' : 'font-normal'
)}
>
{person.name.first}
</span>
{selected && (
<span
className={classNames(
'absolute inset-y-0 right-0 flex items-center pr-4',
active ? 'text-white' : 'text-blue-600'
)}
>
<svg
className="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</span>
)}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</div>
</div>
</>
)}
</Listbox>
</div>
</Section>
<Section title="Combobox">
<div className="w-full space-y-1">
<Combobox
name="location"
defaultValue={'New York'}
onChange={(location) => {
setQuery('')
}}
>
{({ open, value }) => {
return (
<div className="relative">
<div className="flex w-full flex-col">
<Combobox.Input
onChange={(e) => setQuery(e.target.value)}
className="w-full rounded rounded-md border-gray-300 bg-clip-padding px-3 py-1 shadow-sm focus:border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
placeholder="Search users..."
/>
<div
className={classNames(
'flex border-t',
value && !open ? 'border-transparent' : 'border-gray-200'
)}
>
<div className="absolute z-10 mt-1 w-full rounded-md bg-white shadow-lg">
<Combobox.Options className="shadow-xs max-h-60 overflow-auto rounded-md py-1 text-base leading-6 sm:text-sm sm:leading-5">
{locations
.filter((location) =>
location.toLowerCase().includes(query.toLowerCase())
)
.map((location) => (
<Combobox.Option
key={location}
value={location}
className={({ active }) => {
return classNames(
'relative flex cursor-default select-none space-x-4 py-2 pl-3 pr-9 ',
active ? 'bg-blue-600 text-white' : 'text-gray-900'
)
}}
>
{({ active, selected }) => (
<>
<span
className={classNames(
'block truncate',
selected ? 'font-semibold' : 'font-normal'
)}
>
{location}
</span>
{active && (
<span
className={classNames(
'absolute inset-y-0 right-0 flex items-center pr-4',
active ? 'text-white' : 'text-blue-600'
)}
>
<svg
className="h-5 w-5"
viewBox="0 0 25 24"
fill="none"
>
<path
d="M11.25 8.75L14.75 12L11.25 15.25"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
)}
</>
)}
</Combobox.Option>
))}
</Combobox.Options>
</div>
</div>
</div>
</div>
)
}}
</Combobox>
</div>
</Section>
</div>
<div className="space-x-4">
<button className="rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 sm:text-sm sm:leading-5">
Submit
</button>
<button
type="reset"
className="rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 sm:text-sm sm:leading-5"
>
Reset
</button>
</div>
<div className="w-full border-t py-4">
<span>Form data (entries):</span>
<pre className="text-sm">{JSON.stringify([...result.entries()], null, 2)}</pre>
</div>
</form>
</div>
)
}
@@ -0,0 +1,32 @@
import { Dialog, Tab } from '@headlessui/react'
import { useState } from 'react'
import { Button } from '../../components/button'
export default function App() {
let [open, setOpen] = useState(false)
return (
<div className="p-12">
<Button onClick={() => setOpen(true)}>Open dialog</Button>
<Dialog open={open} onClose={setOpen} className="fixed inset-0 grid place-content-center">
<div className="fixed inset-0 bg-gray-500/70" />
<Dialog.Panel className="inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle">
<div className="bg-white px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
<Tab.Group>
<Tab.List className="flex gap-4 py-4">
<Tab as={Button}>Tab 1</Tab>
<Tab as={Button}>Tab 2</Tab>
<Tab as={Button}>Tab 3</Tab>
</Tab.List>
<Tab.Panels>
<Tab.Panel className="px-3 py-2">Panel 1</Tab.Panel>
<Tab.Panel className="px-3 py-2">Panel 2</Tab.Panel>
<Tab.Panel className="px-3 py-2">Panel 3</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</div>
</Dialog.Panel>
</Dialog>
</div>
)
}
@@ -0,0 +1,126 @@
import { Combobox } from '@headlessui/react'
import { useEffect, useState } from 'react'
import { Button } from '../../components/button'
import { countries as allCountries } from '../../data'
import { classNames } from '../../utils/class-names'
function useDebounce<T>(value: T, delay: number) {
let [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
let timer = setTimeout(() => setDebouncedValue(value), delay)
return () => clearTimeout(timer)
}, [value, delay])
return debouncedValue
}
export default function Home() {
let [query, setQuery] = useState('')
let [activeCountry, setActiveCountry] = useState(allCountries[2])
// Mimic delayed response from an API
let actualQuery = useDebounce(query, 0 /* Change to higher value like 100 for testing purposes */)
// Choose a random person on mount
useEffect(() => {
setActiveCountry(allCountries[Math.floor(Math.random() * allCountries.length)])
}, [])
let countries =
actualQuery === ''
? allCountries
: allCountries.filter((person) => person.toLowerCase().includes(actualQuery.toLowerCase()))
return (
<div className="flex h-full w-screen justify-center bg-gray-50 p-12">
<div className="mx-auto w-full max-w-xs">
<div className="py-8 font-mono text-xs">Selected country: {activeCountry}</div>
<div className="space-y-1">
<Combobox
value={activeCountry}
onChange={(value) => {
setActiveCountry(value)
setQuery('')
}}
as="div"
>
<Combobox.Label className="block text-sm font-medium leading-5 text-gray-700">
Country
</Combobox.Label>
<div className="relative">
<span className="relative inline-flex flex-row overflow-hidden rounded-md border shadow-sm">
<Combobox.Input
onChange={(e) => setQuery(e.target.value)}
className="border-none px-3 py-1 outline-none"
/>
<Combobox.Button as={Button}>
<span className="pointer-events-none flex items-center px-2">
<svg
className="h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
>
<path
d="M7 7l3-3 3 3m0 6l-3 3-3-3"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
</Combobox.Button>
</span>
<div className="absolute mt-1 w-full rounded-md bg-white shadow-lg">
<Combobox.Options className="shadow-xs max-h-60 overflow-auto rounded-md py-1 text-base leading-6 focus:outline-none sm:text-sm sm:leading-5">
{countries.map((country) => (
<Combobox.Option
key={country}
value={country}
className={({ active }) => {
return classNames(
'relative cursor-default select-none py-2 pl-3 pr-9 focus:outline-none',
active ? 'bg-indigo-600 text-white' : 'text-gray-900'
)
}}
>
{({ active, selected }) => (
<>
<span
className={classNames(
'block truncate',
selected ? 'font-semibold' : 'font-normal'
)}
>
{country}
</span>
{selected && (
<span
className={classNames(
'absolute inset-y-0 right-0 flex items-center pr-4',
active ? 'text-white' : 'text-indigo-600'
)}
>
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</span>
)}
</>
)}
</Combobox.Option>
))}
</Combobox.Options>
</div>
</div>
</Combobox>
</div>
</div>
</div>
)
}
@@ -0,0 +1,140 @@
// @ts-nocheck
import { Combobox } from '@headlessui/react'
import { useEffect, useState } from 'react'
import { Button } from '../../components/button'
import { classNames } from '../../utils/class-names'
let everybody = [
'Wade Cooper',
'Arlene Mccoy',
'Devon Webb',
'Tom Cook',
'Tanya Fox',
'Hellen Schmidt',
'Caroline Schultz',
'Mason Heaney',
'Claudie Smitham',
'Emil Schaefer',
]
function useDebounce<T>(value: T, delay: number) {
let [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
let timer = setTimeout(() => setDebouncedValue(value), delay)
return () => clearTimeout(timer)
}, [value, delay])
return debouncedValue
}
export default function Home() {
let [query, setQuery] = useState('')
let [activePerson, setActivePerson] = useState(everybody[2])
// Mimic delayed response from an API
let actualQuery = useDebounce(query, 0 /* Change to higher value like 100 for testing purposes */)
// Choose a random person on mount
useEffect(() => {
setActivePerson(everybody[Math.floor(Math.random() * everybody.length)])
}, [])
let people =
actualQuery === ''
? everybody
: everybody.filter((person) => person.toLowerCase().includes(actualQuery.toLowerCase()))
return (
<div className="flex h-full w-screen justify-center bg-gray-50 p-12">
<div className="mx-auto w-full max-w-xs">
<div className="py-8 font-mono text-xs">Selected person: {activePerson}</div>
<div className="space-y-1">
<Combobox
value={activePerson}
onChange={(value) => {
setActivePerson(value)
setQuery('')
}}
immediate
as="div"
>
<Combobox.Label className="block text-sm font-medium leading-5 text-gray-700">
Assigned to
</Combobox.Label>
<div className="relative">
<span className="relative inline-flex flex-row overflow-hidden rounded-md border shadow-sm">
<Combobox.Input
onChange={(e) => setQuery(e.target.value)}
className="border-none px-3 py-1 outline-none"
/>
<Combobox.Button as={Button}>
<span className="pointer-events-none flex items-center px-2">
<svg
className="h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
>
<path
d="M7 7l3-3 3 3m0 6l-3 3-3-3"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
</Combobox.Button>
</span>
<div className="absolute mt-1 w-full rounded-md bg-white shadow-lg">
<Combobox.Options className="shadow-xs max-h-60 overflow-auto rounded-md py-1 text-base leading-6 focus:outline-none sm:text-sm sm:leading-5">
{people.map((name) => (
<Combobox.Option
key={name}
value={name}
className={({ active }) => {
return classNames(
'relative cursor-default select-none py-2 pl-3 pr-9 focus:outline-none',
active ? 'bg-indigo-600 text-white' : 'text-gray-900'
)
}}
>
{({ active, selected }) => (
<>
<span
className={classNames(
'block truncate',
selected ? 'font-semibold' : 'font-normal'
)}
>
{name}
</span>
{selected && (
<span
className={classNames(
'absolute inset-y-0 right-0 flex items-center pr-4',
active ? 'text-white' : 'text-indigo-600'
)}
>
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</span>
)}
</>
)}
</Combobox.Option>
))}
</Combobox.Options>
</div>
</div>
</Combobox>
</div>
</div>
</div>
)
}
@@ -0,0 +1,196 @@
// @ts-nocheck
import { Combobox } from '@headlessui/react'
import { useRef, useState } from 'react'
import { classNames } from '../../utils/class-names'
import { Button } from '../../components/button'
type Option = {
name: string
disabled: boolean
empty?: boolean
}
export default function Home() {
let [list, setList] = useState<Option[]>(() => [
{ name: 'Alice', disabled: false },
{ name: 'Bob', disabled: false },
{ name: 'Charlie', disabled: false },
{ name: 'David', disabled: false },
{ name: 'Eve', disabled: false },
{ name: 'Fred', disabled: false },
{ name: 'George', disabled: false },
{ name: 'Helen', disabled: false },
{ name: 'Iris', disabled: false },
{ name: 'John', disabled: false },
{ name: 'Kate', disabled: false },
{ name: 'Linda', disabled: false },
{ name: 'Michael', disabled: false },
{ name: 'Nancy', disabled: false },
{ name: 'Oscar', disabled: true },
{ name: 'Peter', disabled: false },
{ name: 'Quentin', disabled: false },
{ name: 'Robert', disabled: false },
{ name: 'Sarah', disabled: false },
{ name: 'Thomas', disabled: false },
{ name: 'Ursula', disabled: false },
{ name: 'Victor', disabled: false },
{ name: 'Wendy', disabled: false },
{ name: 'Xavier', disabled: false },
{ name: 'Yvonne', disabled: false },
{ name: 'Zachary', disabled: false },
])
let emptyOption = useRef({ name: 'No results', disabled: true, empty: true })
let [query, setQuery] = useState('')
let [selectedPerson, setSelectedPerson] = useState<Option | null>(list[0])
let optionsRef = useRef<HTMLUListElement | null>(null)
let filtered =
query === ''
? list
: list.filter((item) => item.name.toLowerCase().includes(query.toLowerCase()))
return (
<div className="mx-auto max-w-fit">
<div className="py-8 font-mono text-xs">Selected person: {selectedPerson?.name ?? 'N/A'}</div>
<Combobox
virtual={{
options: filtered.length > 0 ? filtered : [emptyOption.current],
disabled: (option) => option.disabled || option.empty,
}}
value={selectedPerson}
nullable
onChange={(value) => {
setSelectedPerson(value)
setQuery('')
}}
as="div"
// Don't do this lol — it's not supported
// It's just so we can tab to the "Add" button for the demo
// The combobox doesn't actually support this behavior
onKeyDownCapture={(event: KeyboardEvent) => {
let addButton = document.querySelector('#add_person') as HTMLElement | null
if (event.key === 'Tab' && addButton && filtered.length === 0) {
event.preventDefault()
setTimeout(() => addButton.focus(), 0)
}
}}
>
<Combobox.Label className="block text-sm font-medium leading-5 text-gray-700">
Person
</Combobox.Label>
<div className="relative">
<span className="relative inline-flex flex-row overflow-hidden rounded-md border shadow-sm">
<Combobox.Input
onChange={(e) => setQuery(e.target.value)}
displayValue={(option: Option | null) => option?.name ?? ''}
className="border-none px-3 py-1 outline-none"
/>
<Combobox.Button as={Button}>
<span className="pointer-events-none flex items-center px-2">
<svg
className="h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
>
<path
d="M7 7l3-3 3 3m0 6l-3 3-3-3"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
</Combobox.Button>
</span>
<div className="absolute mt-1 w-full rounded-md bg-white shadow-lg">
<Combobox.Options
// This is a hack to make keep the options list around when it's empty
// It comes with some caveats:
// like the option callback being called with a null option (which is probably a bug)
static={filtered.length === 0}
ref={optionsRef}
className={classNames(
'shadow-xs max-h-60 rounded-md py-1 text-base leading-6 focus:outline-none sm:text-sm sm:leading-5',
filtered.length === 0 ? 'overflow-hidden' : 'overflow-auto'
)}
>
{
// @ts-expect-error TODO: Properly handle this
({ option }: { option: Option }) => {
if (!option || option.empty) {
return (
<Combobox.Option
// TODO: `disabled` being required is a bug
disabled
// Note: Do NOT use `null` for the `value`
value={option ?? emptyOption.current}
className="relative w-full cursor-default select-none px-3 py-2 text-center focus:outline-none"
>
<div className="relative grid h-full grid-cols-1 grid-rows-1">
<div className="absolute inset-0">
<svg
fill="none"
viewBox="0 0 24 24"
strokeWidth={0.5}
stroke="currentColor"
className="-translate-y-1/4 text-gray-500/5"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456zM16.894 20.567L16.5 21.75l-.394-1.183a2.25 2.25 0 00-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 001.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 001.423 1.423l1.183.394-1.183.394a2.25 2.25 0 00-1.423 1.423z"
/>
</svg>
</div>
<div className="z-20 col-span-full col-start-1 row-span-full row-start-1 flex flex-col items-center justify-center p-8">
<h3 className="mx-2 mb-4 text-xl font-semibold text-gray-400">
No people found
</h3>
<button
id="add_person"
type="button"
className="rounded bg-blue-500 px-4 py-2 font-semibold text-white hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
onClick={() => {
let person = { name: query, disabled: false }
setList((list) => [...list, person])
setSelectedPerson(person)
}}
>
Add "{query}"
</button>
</div>
</div>
</Combobox.Option>
)
}
return (
<Combobox.Option
// TODO: `disabled` being required is a bug
disabled={option.disabled}
value={option}
className={({ active }) => {
return classNames(
'relative w-full cursor-default select-none py-2 pl-3 pr-9 focus:outline-none',
active ? 'bg-indigo-600 text-white' : 'text-gray-900'
)
}}
>
<span className="block truncate">{option.name}</span>
</Combobox.Option>
)
}
}
</Combobox.Options>
</div>
</div>
</Combobox>
</div>
)
}
@@ -0,0 +1,209 @@
// @ts-nocheck
import { Combobox } from '@headlessui/react'
import { useMemo, useState } from 'react'
import { Button } from '../../components/button'
import { timezones as _allTimezones } from '../../data'
import { classNames } from '../../utils/class-names'
export default function Home() {
let [count, setCount] = useState(1_000)
let list = useMemo(() => {
console.time('Generating list')
let result = []
while (result.length < count) {
let batch = Math.floor(result.length / _allTimezones.length) + 1
result.push(`${_allTimezones[result.length % _allTimezones.length]} #${batch}`)
}
console.timeEnd('Generating list')
return result
}, [count])
return (
<div className="flex flex-col p-12">
<label className="mx-auto flex w-24 items-center gap-2">
<span>Items:</span>
<select
defaultValue={count}
className="mx-auto"
onChange={(e) => {
setCount(Number(e.target.value))
}}
>
<option value={100}>100</option>
<option value={1_000}>1000</option>
<option value={10_000}>10k</option>
<option value={100_000}>100k</option>
</select>
</label>
<div className="flex">
<Example data={list} virtual={true} initial="Europe/Brussels #1" />
<Example data={list} virtual={false} initial="Europe/Brussels #1" />
</div>
</div>
)
}
let nf = new Intl.NumberFormat('en-US')
function Example({ virtual = true, data, initial }: { virtual?: boolean; data; initial: string }) {
let [query, setQuery] = useState('')
let [activeTimezone, setActiveTimezone] = useState(initial)
let timezones =
query === ''
? data
: data.filter((timezone) => timezone.toLowerCase().includes(query.toLowerCase()))
return (
<div className="flex h-full w-screen justify-center bg-gray-50 p-12">
<div className="mx-auto w-full max-w-xs">
<div className="py-8 font-mono text-xs">Selected timezone: {activeTimezone}</div>
<div className="space-y-1">
<Combobox
virtual={virtual ? { options: timezones } : undefined}
value={activeTimezone}
nullable
onChange={(value) => {
setActiveTimezone(value)
setQuery('')
}}
as="div"
>
<Combobox.Label className="block text-sm font-medium leading-5 text-gray-700">
Timezone{' '}
{virtual
? `(virtual — ${nf.format(timezones.length)} items)`
: `(${nf.format(timezones.length)} items)`}
</Combobox.Label>
<div className="relative">
<span className="relative inline-flex flex-row overflow-hidden rounded-md border shadow-sm">
<Combobox.Input
onChange={(e) => setQuery(e.target.value)}
className="border-none px-3 py-1 outline-none"
/>
<Combobox.Button as={Button}>
<span className="pointer-events-none flex items-center px-2">
<svg
className="h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
>
<path
d="M7 7l3-3 3 3m0 6l-3 3-3-3"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
</Combobox.Button>
</span>
<div className="absolute mt-1 w-full rounded-md bg-white shadow-lg">
{virtual ? (
<Combobox.Options className="shadow-xs max-h-60 overflow-auto rounded-md py-1 text-base leading-6 focus:outline-none sm:text-sm sm:leading-5">
{({ option }) => {
return (
<Combobox.Option
value={option}
className={({ active }) => {
return classNames(
'relative w-full cursor-default select-none py-2 pl-3 pr-9 focus:outline-none',
active ? 'bg-indigo-600 text-white' : 'text-gray-900'
)
}}
>
{({ active, selected }) => (
<>
<span
className={classNames(
'block truncate',
selected ? 'font-semibold' : 'font-normal'
)}
>
{option as any}
</span>
{selected && (
<span
className={classNames(
'absolute inset-y-0 right-0 flex items-center pr-4',
active ? 'text-white' : 'text-indigo-600'
)}
>
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</span>
)}
</>
)}
</Combobox.Option>
)
}}
</Combobox.Options>
) : (
<Combobox.Options className="shadow-xs max-h-60 overflow-auto rounded-md py-1 text-base leading-6 focus:outline-none sm:text-sm sm:leading-5">
{timezones.map((timezone, idx) => {
return (
<Combobox.Option
key={timezone}
order={virtual ? idx : undefined}
value={timezone}
className={({ active }) => {
return classNames(
'relative w-full cursor-default select-none py-2 pl-3 pr-9 focus:outline-none',
active ? 'bg-indigo-600 text-white' : 'text-gray-900'
)
}}
>
{({ active, selected }) => (
<>
<span
className={classNames(
'block truncate',
selected ? 'font-semibold' : 'font-normal'
)}
>
{timezone}
</span>
{selected && (
<span
className={classNames(
'absolute inset-y-0 right-0 flex items-center pr-4',
active ? 'text-white' : 'text-indigo-600'
)}
>
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</span>
)}
</>
)}
</Combobox.Option>
)
})}
</Combobox.Options>
)}
</div>
</div>
</Combobox>
</div>
</div>
</div>
)
}
@@ -0,0 +1,138 @@
import { Combobox } from '@headlessui/react'
import { useEffect, useState } from 'react'
import { Button } from '../../components/button'
import { classNames } from '../../utils/class-names'
let everybody = [
'Wade Cooper',
'Arlene Mccoy',
'Devon Webb',
'Tom Cook',
'Tanya Fox',
'Hellen Schmidt',
'Caroline Schultz',
'Mason Heaney',
'Claudie Smitham',
'Emil Schaefer',
]
function useDebounce<T>(value: T, delay: number) {
let [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
let timer = setTimeout(() => setDebouncedValue(value), delay)
return () => clearTimeout(timer)
}, [value, delay])
return debouncedValue
}
export default function Home() {
let [query, setQuery] = useState('')
let [activePerson, setActivePerson] = useState(everybody[2])
// Mimic delayed response from an API
let actualQuery = useDebounce(query, 0 /* Change to higher value like 100 for testing purposes */)
// Choose a random person on mount
useEffect(() => {
setActivePerson(everybody[Math.floor(Math.random() * everybody.length)])
}, [])
let people =
actualQuery === ''
? everybody
: everybody.filter((person) => person.toLowerCase().includes(actualQuery.toLowerCase()))
return (
<div className="flex h-full w-screen justify-center bg-gray-50 p-12">
<div className="mx-auto w-full max-w-xs">
<div className="py-8 font-mono text-xs">Selected person: {activePerson}</div>
<div className="space-y-1">
<Combobox
value={activePerson}
onChange={(value) => {
setActivePerson(value)
setQuery('')
}}
as="div"
>
<Combobox.Label className="block text-sm font-medium leading-5 text-gray-700">
Assigned to
</Combobox.Label>
<div className="relative">
<span className="relative inline-flex flex-row overflow-hidden rounded-md border shadow-sm">
<Combobox.Input
onChange={(e) => setQuery(e.target.value)}
className="border-none px-3 py-1 outline-none"
/>
<Combobox.Button as={Button}>
<span className="pointer-events-none flex items-center px-2">
<svg
className="h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
>
<path
d="M7 7l3-3 3 3m0 6l-3 3-3-3"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
</Combobox.Button>
</span>
<div className="absolute mt-1 w-full rounded-md bg-white shadow-lg">
<Combobox.Options className="shadow-xs max-h-60 overflow-auto rounded-md py-1 text-base leading-6 focus:outline-none sm:text-sm sm:leading-5">
{people.map((name) => (
<Combobox.Option
key={name}
value={name}
className={({ active }) => {
return classNames(
'relative cursor-default select-none py-2 pl-3 pr-9 focus:outline-none',
active ? 'bg-indigo-600 text-white' : 'text-gray-900'
)
}}
>
{({ active, selected }) => (
<>
<span
className={classNames(
'block truncate',
selected ? 'font-semibold' : 'font-normal'
)}
>
{name}
</span>
{selected && (
<span
className={classNames(
'absolute inset-y-0 right-0 flex items-center pr-4',
active ? 'text-white' : 'text-indigo-600'
)}
>
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</span>
)}
</>
)}
</Combobox.Option>
))}
</Combobox.Options>
</div>
</div>
</Combobox>
</div>
</div>
</div>
)
}
@@ -0,0 +1,150 @@
import { Combobox } from '@headlessui/react'
import { Fragment, useEffect, useState } from 'react'
import { classNames } from '../../utils/class-names'
let everybody = [
{ id: 1, img: 'https://github.com/adamwathan.png', name: 'Adam Wathan' },
{ id: 2, img: 'https://github.com/sschoger.png', name: 'Steve Schoger' },
{ id: 3, img: 'https://github.com/bradlc.png', name: 'Brad Cornes' },
{ id: 4, img: 'https://github.com/simonswiss.png', name: 'Simon Vrachliotis' },
{ id: 5, img: 'https://github.com/robinmalfait.png', name: 'Robin Malfait' },
{
id: 6,
img: 'https://pbs.twimg.com/profile_images/1478879681491394569/eV2PyCnm_400x400.jpg',
name: 'James McDonald',
},
{ id: 7, img: 'https://github.com/reinink.png', name: 'Jonathan Reinink' },
{ id: 8, img: 'https://github.com/thecrypticace.png', name: 'Jordan Pittman' },
]
export default function Home() {
let [query, setQuery] = useState('')
let [activePerson, setActivePerson] = useState(everybody[2])
function setPerson(person) {
setActivePerson(person)
setQuery(person.name ?? '')
}
// Choose a random person on mount
useEffect(() => {
setPerson(everybody[Math.floor(Math.random() * everybody.length)])
}, [])
let people =
query === ''
? everybody
: everybody.filter((person) => person.name.toLowerCase().includes(query.toLowerCase()))
let groups = people.reduce((groups, person) => {
let lastNameLetter = person.name.split(' ')[1][0]
groups.set(lastNameLetter, [...(groups.get(lastNameLetter) || []), person])
return groups
}, new Map())
return (
<div className="flex h-full w-screen justify-center bg-gray-50 p-12">
<div className="mx-auto w-full max-w-lg">
<div className="space-y-1">
<Combobox
as="div"
value={activePerson}
onChange={(person) => setPerson(person)}
className="w-full overflow-hidden rounded border border-black/5 bg-white bg-clip-padding shadow-sm"
>
{({ activeOption }) => {
return (
<div className="flex w-full flex-col">
<Combobox.Input
onChange={(e) => setQuery(e.target.value)}
className="w-full rounded-none border-none bg-none px-3 py-1 outline-none"
placeholder="Search users…"
displayValue={(item: typeof activeOption) => item?.name}
/>
<div className="flex">
<Combobox.Options className="shadow-xs max-h-60 flex-1 overflow-auto text-base leading-6 focus:outline-none sm:text-sm sm:leading-5">
{Array.from(groups.entries())
.sort(([letterA], [letterZ]) => letterA.localeCompare(letterZ))
.map(([letter, people]) => (
<Fragment key={letter}>
<div className="bg-gray-100 px-4 py-2">{letter}</div>
{people.map((person) => (
<Combobox.Option
key={person.id}
value={person}
className={({ active }) => {
return classNames(
'relative flex cursor-default select-none space-x-4 py-2 pl-3 pr-9 focus:outline-none',
active ? 'bg-indigo-600 text-white' : 'text-gray-900'
)
}}
>
{({ active, selected }) => (
<>
<img
src={person.img}
className="h-6 w-6 overflow-hidden rounded-full"
/>
<span
className={classNames(
'block truncate',
selected ? 'font-semibold' : 'font-normal'
)}
>
{person.name}
</span>
{active && (
<span
className={classNames(
'absolute inset-y-0 right-0 flex items-center pr-4',
active ? 'text-white' : 'text-indigo-600'
)}
>
<svg className="h-5 w-5" viewBox="0 0 25 24" fill="none">
<path
d="M11.25 8.75L14.75 12L11.25 15.25"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
)}
</>
)}
</Combobox.Option>
))}
</Fragment>
))}
</Combobox.Options>
{people.length === 0 ? (
<div className="w-full py-4 text-center">No person selected</div>
) : activeOption === null ? null : (
<div className="border-l">
<div className="flex flex-col">
<div className="p-8 text-center">
<img
src={activeOption.img}
className="mb-4 inline-block h-16 w-16 overflow-hidden rounded-full"
/>
<div className="font-bold text-gray-900">{activeOption.name}</div>
<div className="text-gray-700">Obviously cool person</div>
</div>
</div>
</div>
)}
</div>
</div>
)
}}
</Combobox>
</div>
</div>
</div>
)
}
@@ -0,0 +1,135 @@
import { Combobox } from '@headlessui/react'
import { useEffect, useState } from 'react'
import { classNames } from '../../utils/class-names'
let everybody = [
{ id: 1, img: 'https://github.com/adamwathan.png', name: 'Adam Wathan' },
{ id: 2, img: 'https://github.com/sschoger.png', name: 'Steve Schoger' },
{ id: 3, img: 'https://github.com/bradlc.png', name: 'Brad Cornes' },
{ id: 4, img: 'https://github.com/simonswiss.png', name: 'Simon Vrachliotis' },
{ id: 5, img: 'https://github.com/robinmalfait.png', name: 'Robin Malfait' },
{
id: 6,
img: 'https://pbs.twimg.com/profile_images/1478879681491394569/eV2PyCnm_400x400.jpg',
name: 'James McDonald',
},
{ id: 7, img: 'https://github.com/reinink.png', name: 'Jonathan Reinink' },
{ id: 8, img: 'https://github.com/thecrypticace.png', name: 'Jordan Pittman' },
]
export default function Home() {
let [query, setQuery] = useState('')
let [activePerson, setActivePerson] = useState(everybody[2])
// Choose a random person on mount
useEffect(() => {
setActivePerson(everybody[Math.floor(Math.random() * everybody.length)])
}, [])
let people =
query === ''
? everybody
: everybody.filter((person) => person.name.toLowerCase().includes(query.toLowerCase()))
return (
<div className="flex h-full w-screen justify-center bg-gray-50 p-12">
<div className="mx-auto w-full max-w-lg">
<div className="space-y-1">
<Combobox
as="div"
value={activePerson}
onChange={(person) => setActivePerson(person)}
className="w-full overflow-hidden rounded border border-black/5 bg-white bg-clip-padding shadow-sm"
>
{({ activeOption, open }) => {
return (
<div className="flex w-full flex-col">
<Combobox.Input
onChange={(e) => setQuery(e.target.value)}
className="w-full rounded-none border-none px-3 py-1 outline-none"
placeholder="Search users…"
displayValue={(item: typeof activePerson) => item?.name}
/>
<div
className={classNames(
'flex border-t',
activePerson && !open ? 'border-transparent' : 'border-gray-200'
)}
>
<Combobox.Options className="shadow-xs max-h-60 flex-1 overflow-auto py-1 text-base leading-6 focus:outline-none sm:text-sm sm:leading-5">
{people.map((person) => (
<Combobox.Option
key={person.id}
value={person}
className={({ active }) => {
return classNames(
'relative flex cursor-default select-none space-x-4 py-2 pl-3 pr-9 focus:outline-none',
active ? 'bg-indigo-600 text-white' : 'text-gray-900'
)
}}
>
{({ active, selected }) => (
<>
<img
src={person.img}
className="h-6 w-6 overflow-hidden rounded-full"
/>
<span
className={classNames(
'block truncate',
selected ? 'font-semibold' : 'font-normal'
)}
>
{person.name}
</span>
{active && (
<span
className={classNames(
'absolute inset-y-0 right-0 flex items-center pr-4',
active ? 'text-white' : 'text-indigo-600'
)}
>
<svg className="h-5 w-5" viewBox="0 0 25 24" fill="none">
<path
d="M11.25 8.75L14.75 12L11.25 15.25"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
)}
</>
)}
</Combobox.Option>
))}
</Combobox.Options>
{people.length === 0 ? (
<div className="w-full py-4 text-center">No person selected</div>
) : activeOption === null ? null : (
<div className="border-l">
<div className="flex flex-col">
<div className="p-8 text-center">
<img
src={activeOption.img}
className="mb-4 inline-block h-16 w-16 overflow-hidden rounded-full"
/>
<div className="font-bold text-gray-900">{activeOption.name}</div>
<div className="text-gray-700">Obviously cool person</div>
</div>
</div>
</div>
)}
</div>
</div>
)
}}
</Combobox>
</div>
</div>
</div>
)
}
@@ -0,0 +1,164 @@
import { Combobox } from '@headlessui/react'
import { useState } from 'react'
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
let people = [
{ id: 1, name: 'Wade Cooper' },
{ id: 2, name: 'Arlene Mccoy' },
{ id: 3, name: 'Devon Webb' },
{ id: 4, name: 'Tom Cook' },
{ id: 5, name: 'Tanya Fox' },
{ id: 6, name: 'Hellen Schmidt' },
{ id: 7, name: 'Caroline Schultz' },
{ id: 8, name: 'Mason Heaney' },
{ id: 9, name: 'Claudie Smitham' },
{ id: 10, name: 'Emil Schaefer' },
]
export default function Home() {
return (
<div className="flex h-full w-screen justify-center space-x-4 bg-gray-50 p-12">
<MultiPeopleList />
</div>
)
}
function MultiPeopleList() {
let [query, setQuery] = useState('')
let [activePersons, setActivePersons] = useState([people[2], people[3]])
return (
<div className="w-full max-w-4xl">
<div className="space-y-1">
<form
onSubmit={(e) => {
e.preventDefault()
console.log([...new FormData(e.currentTarget).entries()])
}}
>
<Combobox
value={activePersons}
onChange={(people) => setActivePersons(people)}
name="people"
multiple
>
<Combobox.Label className="block text-sm font-medium leading-5 text-gray-700">
Assigned to
</Combobox.Label>
<div className="relative">
<span className="inline-block w-full rounded-md shadow-sm">
<div className="relative w-full cursor-default rounded-md border border-gray-300 bg-white py-2 pl-2 pr-10 text-left transition duration-150 ease-in-out focus-within:border-blue-700 focus-within:outline-none focus-within:ring-1 focus-within:ring-blue-700 sm:text-sm sm:leading-5">
<span className="block flex flex-wrap gap-2">
{activePersons.map((person) => (
<span
key={person.id}
className="flex items-center gap-1 rounded bg-blue-50 px-2 py-0.5"
>
<span>{person.name}</span>
<svg
className="h-4 w-4 cursor-pointer"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
onClick={(e) => {
e.stopPropagation()
e.preventDefault()
setActivePersons((existing) => existing.filter((p) => p !== person))
}}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</span>
))}
<Combobox.Input
onChange={(event) => setQuery(event.target.value)}
onFocus={() => query != '' && setQuery('')}
className="border-none p-0 focus:ring-0"
placeholder="Search..."
/>
</span>
<Combobox.Button className="absolute inset-y-0 right-0 flex items-center pr-2">
<svg
className="h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
>
<path
d="M7 7l3-3 3 3m0 6l-3 3-3-3"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Combobox.Button>
</div>
</span>
<div className="absolute mt-1 w-full rounded-md bg-white shadow-lg">
<Combobox.Options className="shadow-xs max-h-60 overflow-auto rounded-md py-1 text-base leading-6 focus:outline-none sm:text-sm sm:leading-5">
{people
.filter((person) => person.name.toLowerCase().includes(query.toLowerCase()))
.map((person) => (
<Combobox.Option
key={person.id}
value={person}
className={({ active }) => {
return classNames(
'relative cursor-default select-none py-2 pl-3 pr-9 focus:outline-none',
active ? 'bg-indigo-600 text-white' : 'text-gray-900'
)
}}
>
{({ active, selected }) => (
<>
<span
className={classNames(
'block truncate',
selected ? 'font-semibold' : 'font-normal'
)}
>
{person.name}
</span>
{selected && (
<span
className={classNames(
'absolute inset-y-0 right-0 flex items-center pr-4',
active ? 'text-white' : 'text-indigo-600'
)}
>
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</span>
)}
</>
)}
</Combobox.Option>
))}
</Combobox.Options>
</div>
</div>
</Combobox>
<button className="mt-2 inline-flex items-center rounded border border-gray-300 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
Submit
</button>
</form>
</div>
</div>
)
}
@@ -0,0 +1,42 @@
import { Dialog } from '@headlessui/react'
import { useState } from 'react'
import { Button } from '../../components/button'
function Modal(props) {
return (
<Dialog className="relative z-50" {...props}>
<div className="fixed inset-0 bg-green-500 bg-opacity-90 backdrop-blur backdrop-filter" />
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4">
<Dialog.Panel className="relative m-5 flex w-full max-w-3xl gap-4 rounded-lg bg-white p-10 shadow">
<Button>One</Button>
<Button>Two</Button>
</Dialog.Panel>
</div>
</div>
</Dialog>
)
}
export default function DialogFocusIssue() {
let [isOpen, setIsOpen] = useState(false)
return (
<div className="p-10">
<h1 className="py-2 text-3xl font-semibold">Headless UI Focus Jump</h1>
<Button onClick={() => setIsOpen(true)}>Open Dialog</Button>
<div className="bg-white p-20"></div>
<div className="bg-gray-100 p-20"></div>
<div className="bg-gray-200 p-20"></div>
<div className="bg-gray-300 p-20"></div>
<div className="bg-gray-400 p-20"></div>
<div className="bg-gray-500 p-20"></div>
<div className="bg-gray-600 p-20"></div>
<div className="bg-gray-700 p-20"></div>
<div className="bg-gray-800 p-20"></div>
<div className="bg-gray-900 p-20"></div>
<div className="bg-black p-20"></div>
<Modal open={isOpen} onClose={() => setIsOpen(false)} />
</div>
)
}
@@ -0,0 +1,74 @@
import { Dialog, Transition } from '@headlessui/react'
import { Fragment, useState } from 'react'
function MyDialog({ open, close }) {
return (
<>
<Transition show={open} as={Fragment}>
<Dialog onClose={close} className="relative z-50">
<Transition.Child
as={Fragment}
enter="transition duration-500 ease-out"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition duration-500 ease-out"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed bottom-0 left-0 top-0 flex items-center justify-center bg-red-500 p-4">
<Dialog.Panel className="mx-auto w-48 rounded bg-white p-4">
<p className="my-2">Gray area should be scrollable</p>
<p className="h-32 overflow-y-scroll border bg-gray-100">
Are you sure you want to deactivate your account? All of your data will be
permanently removed. This action cannot be undone.
</p>
<p>Colored area on the right should not be scrollable</p>
<a
href="#foo"
onClick={() => {
setTimeout(() => {
close()
}, 2000)
}}
>
Click me to close dialog and scroll to Foo
</a>
</Dialog.Panel>
</div>
</Transition.Child>
</Dialog>
</Transition>
</>
)
}
export default function App() {
let [isOpen, setIsOpen] = useState(false)
return (
<div>
<button onClick={() => setIsOpen((v) => !v)}>Toggle dialog</button>
<MyDialog open={isOpen} close={() => setIsOpen(false)} />
<div className="h-[50vh]" />
<button onClick={() => setIsOpen((v) => !v)}>Toggle dialog</button>
<div className="h-[50vh]" />
<button onClick={() => setIsOpen((v) => !v)}>Toggle dialog</button>
<div className="h-[50vh]" />
<button onClick={() => setIsOpen((v) => !v)}>Toggle dialog</button>
<div className="h-[50vh]" />
<a id="foo" className="block w-full bg-pink-500 p-12">
Hello from Foo!
</a>
<div className="h-[50vh]" />
<button onClick={() => setIsOpen((v) => !v)}>Toggle dialog</button>
<div className="h-[50vh]" />
<button onClick={() => setIsOpen((v) => !v)}>Toggle dialog</button>
<div className="h-[50vh]" />
<button onClick={() => setIsOpen((v) => !v)}>Toggle dialog</button>
<div className="h-[50vh]" />
</div>
)
}
@@ -0,0 +1,79 @@
import { Dialog } from '@headlessui/react'
import { useEffect, useRef, useState } from 'react'
import { Button } from '../../components/button'
if (typeof document !== 'undefined') {
class MyCustomElement extends HTMLElement {
shadow: ShadowRoot
constructor() {
super()
this.shadow = this.attachShadow({ mode: 'closed' })
}
connectedCallback() {
let button = document.createElement('button')
button.textContent = 'Inside shadow root (closed)'
this.shadow.appendChild(button)
}
}
customElements.define('my-custom-element', MyCustomElement)
}
function ShadowChildren({ id }: { id: string }) {
let container = useRef<HTMLDivElement | null>(null)
useEffect(() => {
if (!container.current || container.current.shadowRoot) {
return
}
let shadowRoot = container.current.attachShadow({ mode: 'open' })
let button = document.createElement('button')
button.id = id
button.style.display = 'block'
button.textContent = 'Inside shadow root (open)'
let mce = document.createElement('my-custom-element')
shadowRoot.appendChild(button)
shadowRoot.appendChild(mce)
}, [])
return <div ref={container}></div>
}
export default function App() {
const [open, setOpen] = useState(false)
return (
<div>
<Button onClick={() => setOpen(true)}>open</Button>
<Dialog open={open} onClose={() => setOpen(false)}>
<div className="fixed inset-0 z-50 bg-gray-900/75 backdrop-blur-lg">
<div>
<button
className="m-4 rounded border-0 bg-gray-500 px-3 py-1 font-medium text-white hover:bg-gray-600"
id="btn_outside_light"
>
Outside shadow root
</button>
<ShadowChildren id="btn_outside_shadow" />
</div>
</div>
<Dialog.Panel className="fixed left-16 top-16 z-50 h-64 w-64 rounded-lg border border-black/10 bg-white bg-clip-padding p-12 shadow-lg">
<div>
<button
className="m-4 rounded border-0 bg-gray-500 px-3 py-1 font-medium text-white hover:bg-gray-600"
id="btn_inside_light"
>
Outside shadow root
</button>
<ShadowChildren id="btn_inside_shadow" />
</div>
</Dialog.Panel>
</Dialog>
</div>
)
}
+238
View File
@@ -0,0 +1,238 @@
import { Dialog, Menu, Portal, Transition } from '@headlessui/react'
import { Fragment, useState } from 'react'
import Flatpickr from 'react-flatpickr'
import { Button } from '../../components/button'
import { classNames } from '../../utils/class-names'
import { usePopper } from '../../utils/hooks/use-popper'
import 'flatpickr/dist/themes/light.css'
function resolveClass({ active, disabled }) {
return classNames(
'flex justify-between w-full px-4 py-2 text-sm leading-5 text-left',
active ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
disabled && 'cursor-not-allowed opacity-50'
)
}
function Nested({ onClose, level = 0 }) {
let [showChild, setShowChild] = useState(false)
return (
<>
<Dialog open={true} onClose={onClose} className="fixed inset-0 z-10">
<Dialog.Overlay className="fixed inset-0 bg-gray-500 opacity-25" />
<div
className="fixed left-12 top-24 z-10 w-96 bg-white p-4"
style={{
transform: `translate(calc(50px * ${level}), calc(50px * ${level}))`,
}}
>
<p>Level: {level}</p>
<div className="flex gap-4">
<Button onClick={() => setShowChild(true)}>Open (1)</Button>
<Button onClick={() => setShowChild(true)}>Open (2)</Button>
<Button onClick={() => setShowChild(true)}>Open (3)</Button>
</div>
</div>
{showChild && <Nested onClose={() => setShowChild(false)} level={level + 1} />}
</Dialog>
</>
)
}
export default function Home() {
let [isOpen, setIsOpen] = useState(false)
let [nested, setNested] = useState(false)
let [trigger, container] = usePopper({
placement: 'bottom-end',
strategy: 'fixed',
modifiers: [{ name: 'offset', options: { offset: [0, 10] } }],
})
let [date, setDate] = useState(new Date())
return (
<>
<div className="flex gap-4 p-12">
<Button onClick={() => setIsOpen((v) => !v)}>Toggle!</Button>
<Button onClick={() => setNested(true)}>Show nested</Button>
</div>
{nested && <Nested onClose={() => setNested(false)} />}
<Transition
data-debug="Dialog"
show={isOpen}
as={Fragment}
beforeEnter={() => console.log('[Transition] Before enter')}
afterEnter={() => console.log('[Transition] After enter')}
beforeLeave={() => console.log('[Transition] Before leave')}
afterLeave={() => console.log('[Transition] After leave')}
>
<Dialog
onClose={() => {
console.log('close')
setIsOpen(false)
}}
>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-screen items-end justify-center px-4 pb-20 pt-4 text-center sm:block sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-75"
leave="ease-in duration-200"
leaveFrom="opacity-75"
leaveTo="opacity-0"
entered="opacity-75"
beforeEnter={() => console.log('[Transition.Child] [Overlay] Before enter')}
afterEnter={() => console.log('[Transition.Child] [Overlay] After enter')}
beforeLeave={() => console.log('[Transition.Child] [Overlay] Before leave')}
afterLeave={() => console.log('[Transition.Child] [Overlay] After leave')}
>
<div className="fixed inset-0 bg-gray-500 transition-opacity" />
</Transition.Child>
<Transition.Child
enter="ease-out transform duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in transform duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
beforeEnter={() => console.log('[Transition.Child] [Panel] Before enter')}
afterEnter={() => console.log('[Transition.Child] [Panel] After enter')}
beforeLeave={() => console.log('[Transition.Child] [Panel] Before leave')}
afterLeave={() => console.log('[Transition.Child] [Panel] After leave')}
>
{/* This element is to trick the browser into centering the modal contents. */}
<span
className="hidden sm:inline-block sm:h-screen sm:align-middle"
aria-hidden="true"
>
&#8203;
</span>
<Dialog.Panel className="inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle">
<div className="bg-white px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
{/* Heroicon name: exclamation */}
<svg
className="h-6 w-6 text-red-600"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
</div>
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-900"
>
Deactivate account
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">
Are you sure you want to deactivate your account? All of your data will
be permanently removed. This action cannot be undone.
</p>
<div className="relative mt-10 inline-flex gap-4 text-left">
<Menu>
<Menu.Button as={Button} ref={trigger}>
<span>Choose a reason</span>
<svg
className="-mr-1 ml-2 h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</Menu.Button>
<Transition
enter="transition duration-300 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
>
<Portal>
<Menu.Items
ref={container}
className="z-20 mt-2 w-56 origin-top-right divide-y divide-gray-100 rounded-md border border-gray-200 bg-white shadow-lg outline-none"
>
<div className="px-4 py-3">
<p className="text-sm leading-5">Signed in as</p>
<p className="truncate text-sm font-medium leading-5 text-gray-900">
tom@example.com
</p>
</div>
<div className="py-1">
<Menu.Item
as="a"
href="#account-settings"
className={resolveClass}
>
Account settings
</Menu.Item>
<Menu.Item as="a" href="#support" className={resolveClass}>
Support
</Menu.Item>
<Menu.Item
as="a"
disabled
href="#new-feature"
className={resolveClass}
>
New feature (soon)
</Menu.Item>
<Menu.Item as="a" href="#license" className={resolveClass}>
License
</Menu.Item>
</div>
<div className="py-1">
<Menu.Item as="a" href="#sign-out" className={resolveClass}>
Sign out
</Menu.Item>
</div>
</Menu.Items>
</Portal>
</Transition>
</Menu>
</div>
<Flatpickr value={date} onChange={([date]) => setDate(date)} />
</div>
</div>
</div>
</div>
<div className="flex bg-gray-50 px-4 py-3 sm:flex-row-reverse sm:gap-2">
<Button onClick={() => setIsOpen(false)}>Deactivate</Button>
<Button onClick={() => setIsOpen(false)}>Cancel</Button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
</>
)
}
@@ -0,0 +1,98 @@
import { Dialog, Transition } from '@headlessui/react'
import { ExclamationIcon } from '@heroicons/react/outline'
import { Fragment, useRef, useState } from 'react'
export default function Example() {
const [open, setOpen] = useState(false)
const cancelButtonRef = useRef(null)
return (
<>
<div className="p-12">
<button
type="button"
className="inline-flex items-center rounded-md border border-gray-300 bg-white px-6 py-3 text-base font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
onClick={() => setOpen(true)}
>
Open Dialog
</button>
</div>
<Transition.Root show={open} as={Fragment}>
<Dialog as="div" className="relative z-10" initialFocus={cancelButtonRef} onClose={setOpen}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<ExclamationIcon className="h-6 w-6 text-red-600" aria-hidden="true" />
</div>
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
Deactivate account
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">
Are you sure you want to deactivate your account? All of your data will be
permanently removed from our servers forever. This action cannot be
undone.
</p>
{Array(20)
.fill(null)
.map((_, i) => (
<p key={i} className="text-sm text-gray-500">
Are you sure you want to deactivate your account? All of your data
will be permanently removed from our servers forever. This action
cannot be undone.
</p>
))}
</div>
</div>
</div>
<div className="mt-5 sm:ml-10 sm:mt-4 sm:flex sm:pl-4">
<button
type="button"
className="inline-flex w-full justify-center rounded-md border border-transparent bg-red-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 sm:w-auto sm:text-sm"
onClick={() => setOpen(false)}
>
Deactivate
</button>
<button
type="button"
className="mt-3 inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:ml-3 sm:mt-0 sm:w-auto sm:text-sm"
onClick={() => setOpen(false)}
ref={cancelButtonRef}
>
Cancel
</button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
</>
)
}
@@ -0,0 +1,152 @@
import { Dialog, Transition } from '@headlessui/react'
import { Fragment, useState } from 'react'
export default function Home() {
let [isOpen, setIsOpen] = useState(false)
return (
<>
{Array(5)
.fill(null)
.map((_, i) => (
<p key={i} className="m-4">
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam numquam beatae,
maiores sint est perferendis molestiae deleniti dolorem, illum vel, quam atque facilis!
Necessitatibus nostrum recusandae nemo corrupti, odio eius?
</p>
))}
<button
type="button"
onClick={() => setIsOpen((v) => !v)}
className="focus:shadow-outline-blue m-12 rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5"
>
Toggle!
</button>
{Array(20)
.fill(null)
.map((_, i) => (
<p key={i} className="m-4">
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam numquam beatae,
maiores sint est perferendis molestiae deleniti dolorem, illum vel, quam atque facilis!
Necessitatibus nostrum recusandae nemo corrupti, odio eius?
</p>
))}
<Transition
data-debug="Dialog"
show={isOpen}
as={Fragment}
beforeEnter={() => console.log('[Transition] Before enter')}
afterEnter={() => console.log('[Transition] After enter')}
beforeLeave={() => console.log('[Transition] Before leave')}
afterLeave={() => console.log('[Transition] After leave')}
>
<Dialog
onClose={() => {
console.log('close')
setIsOpen(false)
}}
>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-screen items-end justify-center px-4 pb-20 pt-4 text-center sm:block sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-75"
leave="ease-in duration-200"
leaveFrom="opacity-75"
leaveTo="opacity-0"
entered="opacity-75"
beforeEnter={() => console.log('[Transition.Child] [Overlay] Before enter')}
afterEnter={() => console.log('[Transition.Child] [Overlay] After enter')}
beforeLeave={() => console.log('[Transition.Child] [Overlay] Before leave')}
afterLeave={() => console.log('[Transition.Child] [Overlay] After leave')}
>
<div className="fixed inset-0 bg-gray-500 transition-opacity" />
</Transition.Child>
<Transition.Child
enter="ease-out transform duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in transform duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
beforeEnter={() => console.log('[Transition.Child] [Panel] Before enter')}
afterEnter={() => console.log('[Transition.Child] [Panel] After enter')}
beforeLeave={() => console.log('[Transition.Child] [Panel] Before leave')}
afterLeave={() => console.log('[Transition.Child] [Panel] After leave')}
>
{/* This element is to trick the browser into centering the modal contents. */}
<span
className="hidden sm:inline-block sm:h-screen sm:align-middle"
aria-hidden="true"
>
&#8203;
</span>
<Dialog.Panel className="inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle">
<div className="bg-white px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
{/* Heroicon name: exclamation */}
<svg
className="h-6 w-6 text-red-600"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
</div>
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-900"
>
Deactivate account
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">
Are you sure you want to deactivate your account? All of your data will
be permanently removed. This action cannot be undone.
</p>
</div>
<input type="text" />
</div>
</div>
</div>
<div className="bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6">
<button
type="button"
onClick={() => setIsOpen(false)}
className="focus:shadow-outline-red inline-flex w-full justify-center rounded-md border border-transparent bg-red-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-red-700 focus:outline-none sm:ml-3 sm:w-auto sm:text-sm"
>
Deactivate
</button>
<button
type="button"
onClick={() => setIsOpen(false)}
className="focus:shadow-outline-indigo mt-3 inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium text-gray-700 shadow-sm hover:text-gray-500 focus:outline-none sm:mt-0 sm:w-auto sm:text-sm"
>
Cancel
</button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
</>
)
}
@@ -0,0 +1,24 @@
import { Disclosure, Transition } from '@headlessui/react'
export default function Home() {
return (
<div className="flex h-full w-screen justify-center bg-gray-50 p-12">
<div className="mx-auto w-full max-w-xs">
<Disclosure>
<Disclosure.Button>Trigger</Disclosure.Button>
<Transition
enter="transition duration-1000 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-1000 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
>
<Disclosure.Panel className="mt-4 bg-white p-4">Content</Disclosure.Panel>
</Transition>
</Disclosure>
</div>
</div>
)
}
@@ -0,0 +1,92 @@
import { Listbox } from '@headlessui/react'
import { useEffect, useState } from 'react'
let people = [
'Wade Cooper',
'Arlene Mccoy',
'Devon Webb',
'Tom Cook',
'Tanya Fox',
'Hellen Schmidt',
'Caroline Schultz',
'Mason Heaney',
'Claudie Smitham',
'Emil Schaefer',
]
export default function Home() {
let [active, setActivePerson] = useState(people[2])
// Choose a random person on mount
useEffect(() => {
setActivePerson(people[Math.floor(Math.random() * people.length)])
}, [])
return (
<div className="flex h-full w-screen justify-center bg-gray-50 p-12">
<div className="mx-auto w-full max-w-xs">
<div className="space-y-1">
<Listbox
value={active}
onChange={(value) => {
console.log('value:', value)
setActivePerson(value)
}}
>
<Listbox.Label className="block text-sm font-medium leading-5 text-gray-700">
Assigned to
</Listbox.Label>
<div className="relative">
<span className="inline-block w-full rounded-md shadow-sm">
<Listbox.Button className="focus:shadow-outline-blue relative w-full cursor-default rounded-md border border-gray-300 bg-white py-2 pl-3 pr-10 text-left transition duration-150 ease-in-out focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5">
<span className="block truncate">{active}</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<svg
className="h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
>
<path
d="M7 7l3-3 3 3m0 6l-3 3-3-3"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
</Listbox.Button>
</span>
<div className="absolute mt-1 w-full rounded-md bg-white shadow-lg">
<Listbox.Options className="shadow-xs max-h-60 overflow-auto rounded-md py-1 text-base leading-6 focus:outline-none sm:text-sm sm:leading-5">
{people.map((name) => (
<Listbox.Option
key={name}
value={name}
className="ui-active:bg-indigo-600 ui-active:text-white ui-not-active:text-gray-900 relative cursor-default select-none py-2 pl-3 pr-9 focus:outline-none"
>
<span className="ui-selected:font-semibold ui-not-selected:font-normal block truncate">
{name}
</span>
<span className="ui-not-selected:hidden ui-selected:flex ui-active:text-white ui-not-active:text-indigo-600 absolute inset-y-0 right-0 items-center pr-4">
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</span>
</Listbox.Option>
))}
</Listbox.Options>
</div>
</div>
</Listbox>
</div>
</div>
</div>
)
}
@@ -0,0 +1,154 @@
import { Listbox } from '@headlessui/react'
import { useState } from 'react'
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
let people = [
{ id: 1, name: 'Wade Cooper' },
{ id: 2, name: 'Arlene Mccoy' },
{ id: 3, name: 'Devon Webb' },
{ id: 4, name: 'Tom Cook' },
{ id: 5, name: 'Tanya Fox' },
{ id: 6, name: 'Hellen Schmidt' },
{ id: 7, name: 'Caroline Schultz' },
{ id: 8, name: 'Mason Heaney' },
{ id: 9, name: 'Claudie Smitham' },
{ id: 10, name: 'Emil Schaefer' },
]
export default function Home() {
return (
<div className="flex h-full w-screen justify-center space-x-4 bg-gray-50 p-12">
<MultiPeopleList />
</div>
)
}
function MultiPeopleList() {
let [activePersons, setActivePersons] = useState([people[2], people[3]])
return (
<div className="w-full max-w-4xl">
<div className="space-y-1">
<form
onSubmit={(e) => {
e.preventDefault()
console.log([...new FormData(e.currentTarget).entries()])
}}
>
<Listbox value={activePersons} onChange={setActivePersons} name="people" multiple>
<Listbox.Label className="block text-sm font-medium leading-5 text-gray-700">
Assigned to
</Listbox.Label>
<div className="relative">
<span className="inline-block w-full rounded-md shadow-sm">
<Listbox.Button className="focus:shadow-outline-blue relative w-full cursor-default rounded-md border border-gray-300 bg-white py-2 pl-2 pr-10 text-left transition duration-150 ease-in-out focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5">
<span className="block flex flex-wrap gap-2">
{activePersons.length === 0 ? (
<span className="p-0.5">Empty</span>
) : (
activePersons.map((person) => (
<span
key={person.id}
className="flex items-center gap-1 rounded bg-blue-50 px-2 py-0.5"
>
<span>{person.name}</span>
<svg
className="h-4 w-4 cursor-pointer"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
onClick={(e) => {
e.stopPropagation()
e.preventDefault()
setActivePersons((existing) => existing.filter((p) => p !== person))
}}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</span>
))
)}
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<svg
className="h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
>
<path
d="M7 7l3-3 3 3m0 6l-3 3-3-3"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
</Listbox.Button>
</span>
<div className="absolute mt-1 w-full rounded-md bg-white shadow-lg">
<Listbox.Options className="shadow-xs max-h-60 overflow-auto rounded-md py-1 text-base leading-6 focus:outline-none sm:text-sm sm:leading-5">
{people.map((person) => (
<Listbox.Option
key={person.id}
value={person}
className={({ active }) => {
return classNames(
'relative cursor-default select-none py-2 pl-3 pr-9 focus:outline-none',
active ? 'bg-indigo-600 text-white' : 'text-gray-900'
)
}}
>
{({ active, selected }) => (
<>
<span
className={classNames(
'block truncate',
selected ? 'font-semibold' : 'font-normal'
)}
>
{person.name}
</span>
{selected && (
<span
className={classNames(
'absolute inset-y-0 right-0 flex items-center pr-4',
active ? 'text-white' : 'text-indigo-600'
)}
>
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</span>
)}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</div>
</div>
</Listbox>
<button className="mt-2 inline-flex items-center rounded border border-gray-300 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
Submit
</button>
</form>
</div>
</div>
)
}
@@ -0,0 +1,134 @@
import { Listbox } from '@headlessui/react'
import { useEffect, useState } from 'react'
import { classNames } from '../../utils/class-names'
let people = [
'Wade Cooper',
'Arlene Mccoy',
'Devon Webb',
'Tom Cook',
'Tanya Fox',
'Hellen Schmidt',
'Caroline Schultz',
'Mason Heaney',
'Claudie Smitham',
'Emil Schaefer',
]
export default function Home() {
return (
<div className="flex h-full w-screen justify-center space-x-4 bg-gray-50 p-12">
<PeopleList />
<div>
<label htmlFor="email" className="block text-sm font-medium leading-5 text-gray-700">
Email
</label>
<div className="relative mt-1 rounded-md shadow-sm">
<input
className="form-input block w-full sm:text-sm sm:leading-5"
placeholder="you@example.com"
/>
</div>
</div>
<PeopleList />
</div>
)
}
function PeopleList() {
let [active, setActivePerson] = useState(people[2])
// Choose a random person on mount
useEffect(() => {
setActivePerson(people[Math.floor(Math.random() * people.length)])
}, [])
return (
<div className="w-64">
<div className="space-y-1">
<Listbox
value={active}
onChange={(value) => {
console.log('value:', value)
setActivePerson(value)
}}
>
<Listbox.Label className="block text-sm font-medium leading-5 text-gray-700">
Assigned to
</Listbox.Label>
<div className="relative">
<span className="inline-block w-full rounded-md shadow-sm">
<Listbox.Button className="focus:shadow-outline-blue relative w-full cursor-default rounded-md border border-gray-300 bg-white py-2 pl-3 pr-10 text-left transition duration-150 ease-in-out focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5">
<span className="block truncate">{active}</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<svg
className="h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
>
<path
d="M7 7l3-3 3 3m0 6l-3 3-3-3"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
</Listbox.Button>
</span>
<div className="absolute mt-1 w-full rounded-md bg-white shadow-lg">
<Listbox.Options className="shadow-xs max-h-60 overflow-auto rounded-md py-1 text-base leading-6 focus:outline-none sm:text-sm sm:leading-5">
{people.map((name) => (
<Listbox.Option
key={name}
value={name}
className={({ active }) => {
return classNames(
'relative cursor-default select-none py-2 pl-3 pr-9 focus:outline-none',
active ? 'bg-indigo-600 text-white' : 'text-gray-900'
)
}}
>
{({ active, selected }) => (
<>
<span
className={classNames(
'block truncate',
selected ? 'font-semibold' : 'font-normal'
)}
>
{name}
</span>
{selected && (
<span
className={classNames(
'absolute inset-y-0 right-0 flex items-center pr-4',
active ? 'text-white' : 'text-indigo-600'
)}
>
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</span>
)}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</div>
</div>
</Listbox>
</div>
</div>
)
}
@@ -0,0 +1,94 @@
import { offset, useFloating } from '@floating-ui/react'
import { Menu } from '@headlessui/react'
import { ReactNode, useEffect, useState } from 'react'
import { createPortal } from 'react-dom'
import { Button } from '../../components/button'
import { classNames } from '../../utils/class-names'
export default function Home() {
let { refs, floatingStyles } = useFloating({
placement: 'bottom-end',
strategy: 'fixed',
middleware: [offset(10)],
})
function resolveClass({ active, disabled }) {
return classNames(
'block w-full text-left px-4 py-2 text-sm leading-5 text-gray-700',
active && 'bg-gray-100 text-gray-900',
disabled && 'cursor-not-allowed opacity-50'
)
}
return (
<div className="flex h-full w-screen justify-center bg-gray-50 p-12">
<div className="mt-64 inline-block text-left">
<Menu>
<span className="inline-flex rounded-md shadow-sm">
<Menu.Button ref={refs.setReference} as={Button}>
<span>Options</span>
<svg className="-mr-1 ml-2 h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</Menu.Button>
</span>
<Portal>
<Menu.Items
className="w-56 divide-y divide-gray-100 rounded-md border border-gray-200 bg-white shadow-lg outline-none"
ref={refs.setFloating}
style={floatingStyles}
>
<div className="px-4 py-3">
<p className="text-sm leading-5">Signed in as</p>
<p className="truncate text-sm font-medium leading-5 text-gray-900">
tom@example.com
</p>
</div>
<div className="py-1">
<Menu.Item as="a" href="#account-settings" className={resolveClass}>
Account settings
</Menu.Item>
<Menu.Item>
{(data) => (
<a href="#support" className={resolveClass(data)}>
Support
</a>
)}
</Menu.Item>
<Menu.Item as="a" disabled href="#new-feature" className={resolveClass}>
New feature (soon)
</Menu.Item>
<Menu.Item as="a" href="#license" className={resolveClass}>
License
</Menu.Item>
</div>
<div className="py-1">
<Menu.Item as="a" href="#sign-out" className={resolveClass}>
Sign out
</Menu.Item>
</div>
</Menu.Items>
</Portal>
</Menu>
</div>
</div>
)
}
function Portal(props: { children: ReactNode }) {
let { children } = props
let [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
if (!mounted) return null
return createPortal(children, document.body)
}
@@ -0,0 +1,104 @@
import { Menu, MenuItemProps } from '@headlessui/react'
import { AnimatePresence, motion } from 'framer-motion'
import Link from 'next/link'
import { forwardRef } from 'react'
import { Button } from '../../components/button'
import { classNames } from '../../utils/class-names'
export default function Home() {
return (
<div className="flex h-full w-screen justify-center bg-gray-50 p-12">
<div className="relative inline-block text-left">
<Menu>
{({ open }) => (
<>
<span className="rounded-md shadow-sm">
<Menu.Button as={Button}>
<span>Options</span>
<svg className="-mr-1 ml-2 h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</Menu.Button>
</span>
<AnimatePresence>
{open && (
<Menu.Items
static
as={motion.div}
initial={{ opacity: 0, y: 0 }}
animate={{ opacity: 1, y: '0.5rem' }}
exit={{ opacity: 0, y: 0 }}
className="absolute right-0 w-56 divide-y divide-gray-100 rounded-md border border-gray-200 bg-white opacity-0 shadow-lg outline-none"
>
<div className="px-4 py-3">
<p className="text-sm leading-5">Signed in as</p>
<p className="truncate text-sm font-medium leading-5 text-gray-900">
tom@example.com
</p>
</div>
<div className="py-1">
<Item href="#account-settings">Account settings</Item>
<Item as={Link} href="#support">
Support
</Item>
<Item href="#new-feature" disabled>
New feature (soon)
</Item>
<Item href="#license">License</Item>
</div>
<div className="py-1">
<Item as={SignOutButton} />
</div>
</Menu.Items>
)}
</AnimatePresence>
</>
)}
</Menu>
</div>
</div>
)
}
let SignOutButton = forwardRef<HTMLButtonElement>((props, ref) => {
return (
<form
method="POST"
action="#"
onSubmit={(e) => {
e.preventDefault()
alert('SIGNED OUT')
}}
className="w-full"
>
<button ref={ref} type="submit" {...props}>
Sign out
</button>
</form>
)
})
let Item = forwardRef<HTMLAnchorElement, MenuItemProps<any>>((props, ref) => {
return (
<Menu.Item
ref={ref}
as="a"
className={({ active, disabled }) =>
classNames(
'block w-full px-4 py-2 text-left text-sm leading-5 text-gray-700',
active && 'bg-gray-100 text-gray-900',
disabled && 'cursor-not-allowed opacity-50'
)
}
{...props}
/>
)
})
@@ -0,0 +1,93 @@
import { Menu } from '@headlessui/react'
import { ReactNode, useEffect, useState } from 'react'
import { createPortal } from 'react-dom'
import { Button } from '../../components/button'
import { classNames } from '../../utils/class-names'
import { usePopper } from '../../utils/hooks/use-popper'
export default function Home() {
let [trigger, container] = usePopper({
placement: 'bottom-end',
strategy: 'fixed',
modifiers: [{ name: 'offset', options: { offset: [0, 10] } }],
})
function resolveClass({ active, disabled }) {
return classNames(
'block w-full text-left px-4 py-2 text-sm leading-5 text-gray-700',
active && 'bg-gray-100 text-gray-900',
disabled && 'cursor-not-allowed opacity-50'
)
}
return (
<div className="flex h-full w-screen justify-center bg-gray-50 p-12">
<div className="mt-64 inline-block text-left">
<Menu>
<span className="inline-flex rounded-md shadow-sm">
<Menu.Button ref={trigger} as={Button}>
<span>Options</span>
<svg className="-mr-1 ml-2 h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</Menu.Button>
</span>
<Portal>
<Menu.Items
className="w-56 divide-y divide-gray-100 rounded-md border border-gray-200 bg-white shadow-lg outline-none"
ref={container}
>
<div className="px-4 py-3">
<p className="text-sm leading-5">Signed in as</p>
<p className="truncate text-sm font-medium leading-5 text-gray-900">
tom@example.com
</p>
</div>
<div className="py-1">
<Menu.Item as="a" href="#account-settings" className={resolveClass}>
Account settings
</Menu.Item>
<Menu.Item>
{(data) => (
<a href="#support" className={resolveClass(data)}>
Support
</a>
)}
</Menu.Item>
<Menu.Item as="a" disabled href="#new-feature" className={resolveClass}>
New feature (soon)
</Menu.Item>
<Menu.Item as="a" href="#license" className={resolveClass}>
License
</Menu.Item>
</div>
<div className="py-1">
<Menu.Item as="a" href="#sign-out" className={resolveClass}>
Sign out
</Menu.Item>
</div>
</Menu.Items>
</Portal>
</Menu>
</div>
</div>
)
}
function Portal(props: { children: ReactNode }) {
let { children } = props
let [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
if (!mounted) return null
return createPortal(children, document.body)
}
@@ -0,0 +1,86 @@
import { Menu, Transition } from '@headlessui/react'
import { Button } from '../../components/button'
import { classNames } from '../../utils/class-names'
import { usePopper } from '../../utils/hooks/use-popper'
export default function Home() {
let [trigger, container] = usePopper({
placement: 'bottom-end',
strategy: 'fixed',
modifiers: [{ name: 'offset', options: { offset: [0, 10] } }],
})
function resolveClass({ active, disabled }) {
return classNames(
'flex justify-between w-full px-4 py-2 text-sm leading-5 text-left',
active ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
disabled && 'cursor-not-allowed opacity-50'
)
}
return (
<div className="flex h-full w-screen justify-center bg-gray-50 p-12">
<div className="mt-64 inline-block text-left">
<Menu>
<span className="rounded-md shadow-sm">
<Menu.Button ref={trigger} as={Button}>
<span>Options</span>
<svg className="-mr-1 ml-2 h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</Menu.Button>
</span>
<div ref={container} className="w-56">
<Transition
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
>
<Menu.Items className="divide-y divide-gray-100 rounded-md border border-gray-200 bg-white shadow-lg outline-none">
<div className="px-4 py-3">
<p className="text-sm leading-5">Signed in as</p>
<p className="truncate text-sm font-medium leading-5 text-gray-900">
tom@example.com
</p>
</div>
<div className="py-1">
<Menu.Item as="a" href="#account-settings" className={resolveClass}>
Account settings
</Menu.Item>
<Menu.Item>
{(data) => (
<a href="#support" className={resolveClass(data)}>
Support
</a>
)}
</Menu.Item>
<Menu.Item as="a" disabled href="#new-feature" className={resolveClass}>
New feature (soon)
</Menu.Item>
<Menu.Item as="a" href="#license" className={resolveClass}>
License
</Menu.Item>
</div>
<div className="py-1">
<Menu.Item as="a" href="#sign-out" className={resolveClass}>
Sign out
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</div>
</Menu>
</div>
</div>
)
}
@@ -0,0 +1,77 @@
import { Menu, Transition } from '@headlessui/react'
import { Button } from '../../components/button'
import { classNames } from '../../utils/class-names'
export default function Home() {
function resolveClass({ active, disabled }) {
return classNames(
'flex justify-between w-full px-4 py-2 text-sm leading-5 text-left',
active ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
disabled && 'cursor-not-allowed opacity-50'
)
}
return (
<div className="flex h-full w-screen justify-center bg-gray-50 p-12">
<div className="relative inline-block text-left">
<Menu>
<span className="rounded-md shadow-sm">
<Menu.Button as={Button}>
<span>Options</span>
<svg className="-mr-1 ml-2 h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</Menu.Button>
</span>
<Transition
enter="transition duration-1000 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-1000 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
beforeEnter={() => console.log('Before enter')}
afterEnter={() => console.log('After enter')}
beforeLeave={() => console.log('Before leave')}
afterLeave={() => console.log('After leave')}
>
<Menu.Items className="absolute right-0 mt-2 w-56 origin-top-right divide-y divide-gray-100 rounded-md border border-gray-200 bg-white shadow-lg outline-none">
<div className="px-4 py-3">
<p className="text-sm leading-5">Signed in as</p>
<p className="truncate text-sm font-medium leading-5 text-gray-900">
tom@example.com
</p>
</div>
<div className="py-1">
<Menu.Item as="a" href="#account-settings" className={resolveClass}>
Account settings
</Menu.Item>
<Menu.Item as="a" href="#support" className={resolveClass}>
Support
</Menu.Item>
<Menu.Item as="a" disabled href="#new-feature" className={resolveClass}>
New feature (soon)
</Menu.Item>
<Menu.Item as="a" href="#license" className={resolveClass}>
License
</Menu.Item>
</div>
<div className="py-1">
<Menu.Item as="a" href="#sign-out" className={resolveClass}>
Sign out
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</Menu>
</div>
</div>
)
}
+72
View File
@@ -0,0 +1,72 @@
import { Menu } from '@headlessui/react'
import { Button } from '../../components/button'
import { classNames } from '../../utils/class-names'
export default function Home() {
return (
<div className="flex h-full w-screen justify-center bg-gray-50 p-12">
<div className="relative inline-block text-left">
<Menu>
<span className="rounded-md shadow-sm">
<Menu.Button as={Button}>
<span>Options</span>
<svg
className="-mr-1 ml-2 h-5 w-5 transition-transform duration-150"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</Menu.Button>
</span>
<Menu.Items className="absolute right-0 mt-2 w-56 origin-top-right divide-y divide-gray-100 rounded-md border border-gray-200 bg-white shadow-lg outline-none">
<div className="px-4 py-3">
<p className="text-sm leading-5">Signed in as</p>
<p className="truncate text-sm font-medium leading-5 text-gray-900">
tom@example.com
</p>
</div>
<div className="py-1">
<CustomMenuItem href="#account-settings">Account settings</CustomMenuItem>
<CustomMenuItem href="#support">Support</CustomMenuItem>
<CustomMenuItem disabled href="#new-feature">
New feature (soon)
</CustomMenuItem>
<CustomMenuItem href="#license">License</CustomMenuItem>
</div>
<div className="py-1">
<CustomMenuItem href="#sign-out">Sign out</CustomMenuItem>
</div>
</Menu.Items>
</Menu>
</div>
</div>
)
}
function CustomMenuItem(props) {
return (
<Menu.Item {...props}>
{({ active, disabled }) => (
<a
href={props.href}
className={classNames(
'flex w-full justify-between px-4 py-2 text-left text-sm leading-5',
active ? 'bg-indigo-500 text-white' : 'text-gray-700',
disabled && 'cursor-not-allowed opacity-50'
)}
>
<span className={classNames(active && 'font-bold')}>{props.children}</span>
<kbd className={classNames('font-sans', active && 'text-indigo-50')}>K</kbd>
</a>
)}
</Menu.Item>
)
}
@@ -0,0 +1,83 @@
import { Menu } from '@headlessui/react'
import { Button } from '../../components/button'
import { classNames } from '../../utils/class-names'
export default function Home() {
return (
<div className="flex h-full w-screen justify-center space-x-4 bg-gray-50 p-12">
<Dropdown />
<div>
<div className="relative rounded-md shadow-sm">
<input
className="form-input block w-full sm:text-sm sm:leading-5"
placeholder="you@example.com"
/>
</div>
</div>
<Dropdown />
</div>
)
}
function Dropdown() {
function resolveClass({ active, disabled }) {
return classNames(
'block w-full text-left px-4 py-2 text-sm leading-5 text-gray-700',
active && 'bg-gray-100 text-gray-900',
disabled && 'cursor-not-allowed opacity-50'
)
}
return (
<div className="relative inline-block text-left">
<Menu>
<span className="inline-flex rounded-md shadow-sm">
<Menu.Button as={Button}>
<span>Options</span>
<svg className="-mr-1 ml-2 h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</Menu.Button>
</span>
<Menu.Items className="absolute right-0 mt-2 w-56 origin-top-right divide-y divide-gray-100 rounded-md border border-gray-200 bg-white shadow-lg outline-none">
<div className="px-4 py-3">
<p className="text-sm leading-5">Signed in as</p>
<p className="truncate text-sm font-medium leading-5 text-gray-900">tom@example.com</p>
</div>
<div className="py-1">
<Menu.Item as="a" href="#account-settings" className={resolveClass}>
Account settings
</Menu.Item>
<Menu.Item>
{(data) => (
<a href="#support" className={resolveClass(data)}>
Support
</a>
)}
</Menu.Item>
<Menu.Item as="a" disabled href="#new-feature" className={resolveClass}>
New feature (soon)
</Menu.Item>
<Menu.Item as="a" href="#license" className={resolveClass}>
License
</Menu.Item>
</div>
<div className="py-1">
<Menu.Item as="a" href="#sign-out" className={resolveClass}>
Sign out
</Menu.Item>
</div>
</Menu.Items>
</Menu>
</div>
)
}
+104
View File
@@ -0,0 +1,104 @@
import { Popover, Portal, Transition } from '@headlessui/react'
import React, { Fragment, forwardRef } from 'react'
import { usePopper } from '../../utils/hooks/use-popper'
let Button = forwardRef(
(props: React.ComponentProps<'button'>, ref: React.MutableRefObject<HTMLButtonElement>) => {
return (
<Popover.Button
className="border-2 border-transparent bg-gray-300 px-3 py-2 text-left focus:border-blue-900 focus:outline-none"
{...props}
ref={ref}
/>
)
}
)
export default function Home() {
let options = {
placement: 'bottom-start' as const,
strategy: 'fixed' as const,
modifiers: [],
}
let [reference1, popper1] = usePopper(options)
let [reference2, popper2] = usePopper(options)
let items = ['First', 'Second', 'Third', 'Fourth']
return (
<div className="flex items-center justify-center space-x-12 p-12">
<button>Previous</button>
<Popover.Group as="nav" aria-label="Mythical University" className="flex space-x-3">
<Popover as="div" className="relative">
<Transition
as={Fragment}
enter="transition ease-out duration-300 transform"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition ease-in duration-300 transform"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Popover.Overlay className="fixed inset-0 z-20 bg-gray-500 bg-opacity-75"></Popover.Overlay>
</Transition>
<Popover.Button className="relative z-30 border-2 border-transparent bg-gray-300 px-3 py-2 focus:border-blue-900 focus:outline-none">
Normal
</Popover.Button>
<Popover.Panel className="absolute z-30 flex w-64 flex-col border-2 border-blue-900 bg-gray-100">
{items.map((item, i) => (
<Button key={item} hidden={i === 2}>
Normal - {item}
</Button>
))}
</Popover.Panel>
</Popover>
<Popover as="div" className="relative">
<Button>Focus</Button>
<Popover.Panel
focus
className="absolute flex w-64 flex-col border-2 border-blue-900 bg-gray-100"
>
{items.map((item) => (
<Button key={item}>Focus - {item}</Button>
))}
</Popover.Panel>
</Popover>
<Popover as="div" className="relative">
<Button ref={reference1}>Portal</Button>
<Portal>
<Popover.Panel
ref={popper1}
className="flex w-64 flex-col border-2 border-blue-900 bg-gray-100"
>
{items.map((item) => (
<Button key={item}>Portal - {item}</Button>
))}
</Popover.Panel>
</Portal>
</Popover>
<Popover as="div" className="relative">
<Button ref={reference2}>Focus in Portal</Button>
<Portal>
<Popover.Panel
ref={popper2}
focus
className="flex w-64 flex-col border-2 border-blue-900 bg-gray-100"
>
{items.map((item) => (
<Button key={item}>Focus in Portal - {item}</Button>
))}
</Popover.Panel>
</Portal>
</Popover>
</Popover.Group>
<button>Next</button>
</div>
)
}
@@ -0,0 +1,101 @@
import { RadioGroup } from '@headlessui/react'
import { useState } from 'react'
import { classNames } from '../../utils/class-names'
export default function Home() {
let access = [
{
id: 'access-1',
name: 'Public access',
description: 'This project would be available to anyone who has the link',
},
{
id: 'access-2',
name: 'Private to Project Members',
description: 'Only members of this project would be able to access',
},
{
id: 'access-3',
name: 'Private to you',
description: 'You are the only one able to access this project',
},
]
let [active, setActive] = useState()
return (
<div className="max-w-xl p-12">
<a href="/">Link before</a>
<RadioGroup value={active} onChange={setActive}>
<fieldset className="space-y-4">
<legend>
<h2 className="text-xl">Privacy setting</h2>
</legend>
<div className="-space-y-px rounded-md bg-white">
{access.map(({ id, name, description }, i) => {
return (
<RadioGroup.Option
key={id}
value={id}
className={({ active }) =>
classNames(
// Rounded corners
i === 0 && 'rounded-tl-md rounded-tr-md',
access.length - 1 === i && 'rounded-bl-md rounded-br-md',
// Shared
'relative flex border p-4 focus:outline-none',
active ? 'z-10 border-indigo-200 bg-indigo-50' : 'border-gray-200'
)
}
>
{({ active, checked }) => (
<div className="flex w-full items-center justify-between">
<div className="ml-3 flex cursor-pointer flex-col">
<span
className={classNames(
'block text-sm font-medium leading-5',
active ? 'text-indigo-900' : 'text-gray-900'
)}
>
{name}
</span>
<span
className={classNames(
'block text-sm leading-5',
active ? 'text-indigo-700' : 'text-gray-500'
)}
>
{description}
</span>
</div>
<div>
{checked && (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
className="h-5 w-5 text-indigo-500"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
)}
</div>
</div>
)}
</RadioGroup.Option>
)
})}
</div>
</fieldset>
</RadioGroup>
<a href="/">Link after</a>
</div>
)
}
@@ -0,0 +1,72 @@
'use client'
import { Portal } from '@headlessui/react'
import { lazy, Suspense } from 'react'
function MyComponent({ children }: { children(message: string): JSX.Element }) {
return <>{children('test')}</>
}
let MyComponentLazy = lazy(async () => {
await new Promise((resolve) => setTimeout(resolve, 4000))
return { default: MyComponent }
})
export default function Index() {
return (
<div>
<h1 className="p-8 text-3xl font-bold">Suspense + Portals</h1>
<Portal>
<div className="absolute right-48 top-24 z-10 flex h-32 w-32 flex-col items-center justify-center rounded border border-black/5 bg-white bg-clip-padding p-px shadow">
<div className="w-full rounded-t-sm bg-gray-100 p-1 text-center text-gray-700">
Instant
</div>
<div className="flex w-full flex-1 items-center justify-center text-3xl font-bold text-gray-400">
1
</div>
</div>
</Portal>
<Portal>
<div className="absolute right-8 top-24 z-10 flex h-32 w-32 flex-col items-center justify-center rounded border border-black/5 bg-white bg-clip-padding p-px shadow">
<div className="w-full rounded-t-sm bg-gray-100 p-1 text-center text-gray-700">
Instant
</div>
<div className="flex w-full flex-1 items-center justify-center text-3xl font-bold text-gray-400">
2
</div>
</div>
</Portal>
<Suspense fallback={<span>Loading ...</span>}>
<MyComponentLazy>
{(env) => (
<div>
<Portal>
<div className="absolute right-48 top-64 z-10 flex h-32 w-32 flex-col items-center justify-center rounded border border-black/5 bg-white bg-clip-padding p-px shadow">
<div className="w-full rounded-t-sm bg-gray-100 p-1 text-center text-gray-700">
Suspense
</div>
<div className="flex w-full flex-1 items-center justify-center text-3xl font-bold text-gray-400">
{env} 1
</div>
</div>
</Portal>
<Portal>
<div className="absolute right-8 top-64 z-10 flex h-32 w-32 flex-col items-center justify-center rounded border border-black/5 bg-white bg-clip-padding p-px shadow">
<div className="w-full rounded-t-sm bg-gray-100 p-1 text-center text-gray-700">
Suspense
</div>
<div className="flex w-full flex-1 items-center justify-center text-3xl font-bold text-gray-400">
{env} 2
</div>
</div>
</Portal>
</div>
)}
</MyComponentLazy>
</Suspense>
</div>
)
}
@@ -0,0 +1,39 @@
import { Switch } from '@headlessui/react'
import { useState } from 'react'
import { classNames } from '../../utils/class-names'
export default function Home() {
let [state, setState] = useState(false)
return (
<div className="flex h-full w-screen items-start justify-center bg-gray-50 p-12">
<Switch.Group as="div" className="flex items-center space-x-4">
<Switch.Label>Enable notifications</Switch.Label>
<Switch
as="button"
checked={state}
onChange={setState}
className={({ checked }) =>
classNames(
'focus:shadow-outline relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none',
checked ? 'bg-indigo-600 hover:bg-indigo-800' : 'bg-gray-200 hover:bg-gray-400'
)
}
>
{({ checked }) => (
<>
<span
className={classNames(
'inline-block h-5 w-5 transform rounded-full bg-white transition duration-200 ease-in-out',
checked ? 'translate-x-5' : 'translate-x-0'
)}
/>
</>
)}
</Switch>
</Switch.Group>
</div>
)
}
@@ -0,0 +1,91 @@
import { Switch, Tab } from '@headlessui/react'
import { useState } from 'react'
import { classNames } from '../../utils/class-names'
export default function Home() {
let tabs = [
{ name: 'My Account', content: 'Tab content for my account' },
{ name: 'Company', content: 'Tab content for company', disabled: true },
{ name: 'Team Members', content: 'Tab content for team members' },
{ name: 'Billing', content: 'Tab content for billing' },
]
let [manual, setManual] = useState(false)
return (
<div className="flex h-full w-screen flex-col items-start space-y-12 bg-gray-50 p-12">
<Switch.Group as="div" className="flex items-center space-x-4">
<Switch.Label>Manual keyboard activation</Switch.Label>
<Switch
as="button"
checked={manual}
onChange={setManual}
className={({ checked }) =>
classNames(
'focus:shadow-outline relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none',
checked ? 'bg-indigo-600' : 'bg-gray-200'
)
}
>
{({ checked }) => (
<span
className={classNames(
'inline-block h-5 w-5 transform rounded-full bg-white transition duration-200 ease-in-out',
checked ? 'translate-x-5' : 'translate-x-0'
)}
/>
)}
</Switch>
</Switch.Group>
<Tab.Group
className="flex w-full max-w-3xl flex-col"
as="div"
manual={manual}
defaultIndex={2}
>
<Tab.List className="relative z-0 flex divide-x divide-gray-200 rounded-lg shadow">
{tabs.map((tab, tabIdx) => (
<Tab
key={tab.name}
disabled={tab.disabled}
className={({ selected }) =>
classNames(
selected ? 'text-gray-900' : 'text-gray-500 hover:text-gray-700',
tabIdx === 0 ? 'rounded-l-lg' : '',
tabIdx === tabs.length - 1 ? 'rounded-r-lg' : '',
tab.disabled && 'opacity-50',
'group relative min-w-0 flex-1 overflow-hidden bg-white px-4 py-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10'
)
}
>
{({ selected }) => (
<>
<span>{tab.name}</span>
{tab.disabled && <small className="inline-block px-4 text-xs">(disabled)</small>}
<span
aria-hidden="true"
className={classNames(
selected ? 'bg-indigo-500' : 'bg-transparent',
'absolute inset-x-0 bottom-0 h-0.5'
)}
/>
</>
)}
</Tab>
))}
</Tab.List>
<Tab.Panels className="mt-4">
{tabs.map((tab) => (
<Tab.Panel className="rounded-lg bg-white p-4 shadow" key={tab.name}>
{tab.content}
</Tab.Panel>
))}
</Tab.Panels>
</Tab.Group>
</div>
)
}
@@ -0,0 +1,287 @@
import { Transition } from '@headlessui/react'
import { Fragment, useState } from 'react'
import { Button } from '../../components/button'
export default function AppearExample() {
let [show, setShow] = useState(true)
let [lazy, setLazy] = useState(false)
return (
<div className="space-y-4 p-8">
<div className="flex items-center gap-3">
<Button onClick={() => setShow((v) => !v)}>Toggle show</Button>
<Button onClick={() => setLazy((v) => !v)}>Toggle lazy</Button>
</div>
<div className="flex items-center gap-4">
<div className="rounded-md bg-white p-4 shadow ring-1 ring-black/5">
<span className="mb-2">Initial render</span>
<div className="grid max-w-6xl grid-cols-4 gap-4">
<Transition
show={show}
appear={true}
unmount={true}
enter="duration-1000 transition"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="duration-1000 transition"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
className="aspect-square flex-1 rounded-md bg-blue-200 p-4"
>
Appear: true, unmount: true
</Transition>
<Transition
as={Fragment}
show={show}
appear={true}
unmount={true}
enter="duration-1000 transition"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="duration-1000 transition"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<div className="aspect-square flex-1 rounded-md bg-blue-200 p-4">
Appear: true, as={`Fragment`}, unmount: true
</div>
</Transition>
<Transition
show={show}
appear={false}
unmount={true}
enter="duration-1000 transition"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="duration-1000 transition"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
className="aspect-square flex-1 rounded-md bg-blue-200 p-4"
>
Appear: false, unmount: true
</Transition>
<Transition
as={Fragment}
show={show}
appear={false}
unmount={true}
enter="duration-1000 transition"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="duration-1000 transition"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<div className="aspect-square flex-1 rounded-md bg-blue-200 p-4">
Appear: false, as={`Fragment`}, unmount: true
</div>
</Transition>
<Transition
show={show}
appear={true}
unmount={false}
enter="duration-1000 transition"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="duration-1000 transition"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
className="aspect-square flex-1 rounded-md bg-blue-200 p-4"
>
Appear: true, unmount: false
</Transition>
<Transition
as={Fragment}
show={show}
appear={true}
unmount={false}
enter="duration-1000 transition"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="duration-1000 transition"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<div className="aspect-square flex-1 rounded-md bg-blue-200 p-4">
Appear: true, as={`Fragment`}, unmount: false
</div>
</Transition>
<Transition
show={show}
appear={false}
unmount={false}
enter="duration-1000 transition"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="duration-1000 transition"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
className="aspect-square flex-1 rounded-md bg-blue-200 p-4"
>
Appear: false, unmount: false
</Transition>
<Transition
as={Fragment}
show={show}
appear={false}
unmount={false}
enter="duration-1000 transition"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="duration-1000 transition"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<div className="aspect-square flex-1 rounded-md bg-blue-200 p-4">
Appear: false, as={`Fragment`}, unmount: false
</div>
</Transition>
</div>
</div>
{lazy && (
<div className="rounded-md bg-white p-4 shadow ring-1 ring-black/5">
<span className="mb-2">Not on the initial render</span>
<div className="grid max-w-6xl grid-cols-4 gap-4">
<Transition
show={show}
appear={true}
unmount={true}
enter="duration-1000 transition"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="duration-1000 transition"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
className="aspect-square flex-1 rounded-md bg-blue-200 p-4"
>
Appear: true, unmount: true
</Transition>
<Transition
as={Fragment}
show={show}
appear={true}
unmount={true}
enter="duration-1000 transition"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="duration-1000 transition"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<div className="aspect-square flex-1 rounded-md bg-blue-200 p-4">
Appear: true, as={`Fragment`}, unmount: true
</div>
</Transition>
<Transition
show={show}
appear={false}
unmount={true}
enter="duration-1000 transition"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="duration-1000 transition"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
className="aspect-square flex-1 rounded-md bg-blue-200 p-4"
>
Appear: false, unmount: true
</Transition>
<Transition
as={Fragment}
show={show}
appear={false}
unmount={true}
enter="duration-1000 transition"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="duration-1000 transition"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<div className="aspect-square flex-1 rounded-md bg-blue-200 p-4">
Appear: false, as={`Fragment`}, unmount: true
</div>
</Transition>
<Transition
show={show}
appear={true}
unmount={false}
enter="duration-1000 transition"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="duration-1000 transition"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
className="aspect-square flex-1 rounded-md bg-blue-200 p-4"
>
Appear: true, unmount: false
</Transition>
<Transition
as={Fragment}
show={show}
appear={true}
unmount={false}
enter="duration-1000 transition"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="duration-1000 transition"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<div className="aspect-square flex-1 rounded-md bg-blue-200 p-4">
Appear: true, as={`Fragment`}, unmount: false
</div>
</Transition>
<Transition
show={show}
appear={false}
unmount={false}
enter="duration-1000 transition"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="duration-1000 transition"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
className="aspect-square flex-1 rounded-md bg-blue-200 p-4"
>
Appear: false, unmount: false
</Transition>
<Transition
as={Fragment}
show={show}
appear={false}
unmount={false}
enter="duration-1000 transition"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="duration-1000 transition"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<div className="aspect-square flex-1 rounded-md bg-blue-200 p-4">
Appear: false, as={`Fragment`}, unmount: false
</div>
</Transition>
</div>
</div>
)}
</div>
</div>
)
}
@@ -0,0 +1,98 @@
import { Transition } from '@headlessui/react'
import Head from 'next/head'
import { useState } from 'react'
export default function Home() {
return (
<>
<Head>
<title>Transition Component - Playground</title>
</Head>
<div className="flex h-full w-screen justify-center bg-gray-50 p-12">
<Dropdown />
</div>
</>
)
}
function Dropdown() {
let [isOpen, setIsOpen] = useState(false)
return (
<div className="relative inline-block text-left">
<div>
<span className="rounded-md shadow-sm">
<button
type="button"
className="focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none active:bg-gray-50 active:text-gray-800"
id="options-menu"
aria-haspopup="true"
aria-expanded={isOpen}
onClick={() => setIsOpen((v) => !v)}
>
Options
<svg className="-mr-1 ml-2 h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>
</span>
</div>
<Transition
show={isOpen}
enter="transition ease-out duration-75"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-150"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
className="absolute right-0 mt-2 w-56 origin-top-right rounded-md shadow-lg"
>
<div className="shadow-xs rounded-md bg-white">
<div
className="py-1"
role="menu"
aria-orientation="vertical"
aria-labelledby="options-menu"
>
<a
href="/"
className="block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:bg-gray-100 focus:text-gray-900 focus:outline-none"
role="menuitem"
>
Account settings
</a>
<a
href="/"
className="block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:bg-gray-100 focus:text-gray-900 focus:outline-none"
role="menuitem"
>
Support
</a>
<a
href="/"
className="block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:bg-gray-100 focus:text-gray-900 focus:outline-none"
role="menuitem"
>
License
</a>
<form method="POST" action="#">
<button
type="submit"
className="block w-full px-4 py-2 text-left text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:bg-gray-100 focus:text-gray-900 focus:outline-none"
role="menuitem"
>
Sign out
</button>
</form>
</div>
</div>
</Transition>
</div>
)
}
@@ -0,0 +1,173 @@
import { Transition } from '@headlessui/react'
import { useRef, useState } from 'react'
export default function Home() {
let [isOpen, setIsOpen] = useState(false)
function toggle() {
setIsOpen((v) => !v)
}
let [email, setEmail] = useState('')
let [events, setEvents] = useState([])
let inputRef = useRef(null)
function addEvent(name) {
setEvents((existing) => [...existing, `${new Date().toJSON()} - ${name}`])
}
return (
<div>
<div className="flex space-x-4 p-12">
<div className="inline-block p-12">
<span className="mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto">
<button
onClick={toggle}
type="button"
className="focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5"
>
Show modal
</button>
</span>
</div>
<ul className="bg-gray-200 p-4 text-gray-900">
<h3 className="font-bold">Events:</h3>
{events.map((event, i) => (
<li key={i} className="font-mono text-sm">
{event}
</li>
))}
</ul>
</div>
<Transition
show={isOpen}
className="fixed inset-0 z-10 overflow-y-auto"
beforeEnter={() => {
addEvent('[Root] Before enter')
}}
afterEnter={() => {
inputRef.current?.focus()
addEvent('[Root] After enter')
}}
beforeLeave={() => {
addEvent('[Root] Before leave')
}}
afterLeave={() => {
addEvent('[Root] After leave')
setEmail('')
}}
>
<div className="flex min-h-screen items-end justify-center px-4 pb-20 pt-4 text-center sm:block sm:p-0">
<Transition.Child
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
beforeEnter={() => addEvent('[Overlay] Before enter')}
afterEnter={() => addEvent('[Overlay] After enter')}
beforeLeave={() => addEvent('[Overlay] Before leave')}
afterLeave={() => addEvent('[Overlay] After leave')}
>
<div className="fixed inset-0 transition-opacity">
<div className="absolute inset-0 bg-gray-500 opacity-75"></div>
</div>
</Transition.Child>
{/* This element is to trick the browser into centering the modal contents. */}
<span className="hidden sm:inline-block sm:h-screen sm:align-middle"></span>&#8203;
<Transition.Child
className="inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline"
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
beforeEnter={() => addEvent('[Panel] Before enter')}
afterEnter={() => addEvent('[Panel] After enter')}
beforeLeave={() => addEvent('[Panel] Before leave')}
afterLeave={() => addEvent('[Panel] After leave')}
>
<div className="bg-white px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
{/* Heroicon name: exclamation */}
<svg
className="h-6 w-6 text-red-600"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
</div>
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-headline">
Deactivate account
</h3>
<div className="mt-2">
<p className="text-sm leading-5 text-gray-500">
Are you sure you want to deactivate your account? All of your data will be
permanently removed. This action cannot be undone.
</p>
</div>
<div className="mt-2">
<div>
<label
htmlFor="email"
className="block text-sm font-medium leading-5 text-gray-700"
>
Email address
</label>
<div className="relative mt-1 rounded-md shadow-sm">
<input
ref={inputRef}
value={email}
onChange={(event) => setEmail(event.target.value)}
type="email"
id="email"
className="form-input block w-full px-3 sm:text-sm sm:leading-5"
placeholder="name@example.com"
/>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6">
<span className="flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto">
<button
type="button"
className="focus:shadow-outline-red inline-flex w-full justify-center rounded-md border border-transparent bg-red-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-red-500 focus:border-red-700 focus:outline-none sm:text-sm sm:leading-5"
>
Deactivate
</button>
</span>
<span className="mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto">
<button
onClick={toggle}
type="button"
className="focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5"
>
Cancel
</button>
</span>
</div>
</Transition.Child>
</div>
</Transition>
</div>
)
}
@@ -0,0 +1,60 @@
import { Transition } from '@headlessui/react'
import { ReactNode, useState } from 'react'
export default function Home() {
let [isOpen, setIsOpen] = useState(true)
return (
<>
<div className="flex h-full w-screen justify-center bg-gray-50 p-12">
<div className="w-96 space-y-2">
<span className="inline-flex rounded-md shadow-sm">
<button
type="button"
onClick={() => setIsOpen((v) => !v)}
className="duration-150-out focus:shadow-outline-blue inline-flex items-center rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium leading-4 text-gray-700 transition hover:text-gray-500 focus:border-blue-300 focus:outline-none active:bg-gray-50 active:text-gray-800"
>
{isOpen ? 'Hide' : 'Show'}
</button>
</span>
<Transition show={isOpen} unmount={false}>
<Box>
<Box>
<Box>
<Box />
</Box>
<Box>
<Box>
<Box>
<Box />
</Box>
</Box>
</Box>
</Box>
</Box>
</Transition>
</div>
</div>
</>
)
}
function Box({ children }: { children?: ReactNode }) {
return (
<Transition.Child
unmount={false}
enter="transition translate duration-300"
enterFrom="transform -translate-x-full"
enterTo="transform translate-x-0"
leave="transition translate duration-300"
leaveFrom="transform translate-x-0"
leaveTo="transform translate-x-full"
>
<div className="space-y-2 rounded-md bg-white p-4 text-sm font-semibold uppercase tracking-wide text-gray-700 shadow">
<span>This is a box</span>
{children}
</div>
</Transition.Child>
)
}
@@ -0,0 +1,60 @@
import { Transition } from '@headlessui/react'
import { ReactNode, useState } from 'react'
export default function Home() {
let [isOpen, setIsOpen] = useState(true)
return (
<>
<div className="flex h-full w-screen justify-center bg-gray-50 p-12">
<div className="w-96 space-y-2">
<span className="inline-flex rounded-md shadow-sm">
<button
type="button"
onClick={() => setIsOpen((v) => !v)}
className="duration-150-out focus:shadow-outline-blue inline-flex items-center rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium leading-4 text-gray-700 transition hover:text-gray-500 focus:border-blue-300 focus:outline-none active:bg-gray-50 active:text-gray-800"
>
{isOpen ? 'Hide' : 'Show'}
</button>
</span>
<Transition show={isOpen} unmount={true}>
<Box>
<Box>
<Box>
<Box />
</Box>
<Box>
<Box>
<Box>
<Box />
</Box>
</Box>
</Box>
</Box>
</Box>
</Transition>
</div>
</div>
</>
)
}
function Box({ children }: { children?: ReactNode }) {
return (
<Transition.Child
unmount={true}
enter="transition translate duration-300"
enterFrom="transform -translate-x-full"
enterTo="transform translate-x-0"
leave="transition translate duration-300"
leaveFrom="transform translate-x-0"
leaveTo="transform translate-x-full"
>
<div className="space-y-2 rounded-md bg-white p-4 text-sm font-semibold uppercase tracking-wide text-gray-700 shadow">
<span>This is a box</span>
{children}
</div>
</Transition.Child>
)
}
@@ -0,0 +1,43 @@
import { Transition } from '@headlessui/react'
import { useState } from 'react'
export default function Home() {
let [isOpen, setIsOpen] = useState(true)
return (
<>
<div className="flex h-full w-screen justify-center bg-gray-50 p-12">
<div className="w-96 space-y-2">
<span className="inline-flex rounded-md shadow-sm">
<button
type="button"
onClick={() => setIsOpen((v) => !v)}
className="focus:shadow-outline-blue inline-flex items-center rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium leading-4 text-gray-700 transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none active:bg-gray-50 active:text-gray-800"
>
{isOpen ? 'Hide' : 'Show'}
</button>
</span>
<Transition
show={isOpen}
appear={false}
beforeEnter={() => console.log('beforeEnter')}
afterEnter={() => console.log('afterEnter')}
beforeLeave={() => console.log('beforeLeave')}
afterLeave={() => console.log('afterLeave')}
enter="transition-colors ease-out duration-[5s]"
enterFrom="transform bg-red-500"
enterTo="transform bg-blue-500"
leave="transition-colors ease-in duration-[5s]"
leaveFrom="transform bg-blue-500"
leaveTo="transform bg-red-500"
entered="bg-blue-500"
className="h-64 rounded-md p-4 shadow"
>
Contents to show and hide
</Transition>
</div>
</div>
</>
)
}
@@ -0,0 +1,181 @@
import { Transition } from '@headlessui/react'
import Head from 'next/head'
import { useEffect, useRef, useState } from 'react'
import { classNames } from '../../../utils/class-names'
import { match } from '../../../utils/match'
export default function Shell() {
return (
<>
<Head>
<title>Transition Component - Full Page Transition</title>
</Head>
<div className="h-full bg-gray-50 p-12">
<div className="flex h-full flex-1 flex-col overflow-hidden rounded-lg shadow-lg">
<FullPageTransition />
</div>
</div>
</>
)
}
function usePrevious<T>(value: T) {
let ref = useRef(value)
useEffect(() => {
ref.current = value
}, [value])
return ref.current
}
enum Direction {
Forwards = ' -> ',
Backwards = ' <- ',
}
let pages = ['Dashboard', 'Team', 'Projects', 'Calendar', 'Reports']
let colors = [
'bg-gradient-to-r from-teal-400 to-blue-400',
'bg-gradient-to-r from-blue-400 to-orange-400',
'bg-gradient-to-r from-orange-400 to-purple-400',
'bg-gradient-to-r from-purple-400 to-green-400',
'bg-gradient-to-r from-green-400 to-teal-400',
]
function FullPageTransition() {
let [activePage, setActivePage] = useState(0)
let previousPage = usePrevious(activePage)
let direction = activePage > previousPage ? Direction.Forwards : Direction.Backwards
let transitions = match(direction, {
[Direction.Forwards]: {
enter: 'transition transform ease-in-out duration-500',
enterFrom: 'translate-x-full',
enterTo: 'translate-x-0',
leave: 'transition transform ease-in-out duration-500',
leaveFrom: 'translate-x-0',
leaveTo: '-translate-x-full',
},
[Direction.Backwards]: {
enter: 'transition transform ease-in-out duration-500',
enterFrom: '-translate-x-full',
enterTo: 'translate-x-0',
leave: 'transition transform ease-in-out duration-500',
leaveFrom: 'translate-x-0',
leaveTo: 'translate-x-full',
},
})
return (
<div>
<div className="bg-gray-800 pb-32">
<nav className="bg-gray-800">
<div className="mx-auto max-w-7xl sm:px-6 lg:px-8">
<div className="border-b border-gray-700">
<div className="flex h-16 items-center justify-between px-4 sm:px-0">
<div className="flex items-center">
<div className="flex-shrink-0">
<img
className="h-8 w-8"
src="https://tailwindui.com/img/logos/workflow-mark-on-dark.svg"
alt="Workflow logo"
/>
</div>
<div className="hidden md:block">
<div className="ml-10 flex items-baseline space-x-4">
{pages.map((page, i) => (
<button
key={page}
onClick={() => setActivePage(i)}
className={classNames(
'rounded-md px-3 py-2 text-sm font-medium focus:bg-gray-700 focus:text-white focus:outline-none',
i === activePage
? 'bg-gray-900 text-white'
: 'text-gray-300 hover:bg-gray-700 hover:text-white'
)}
>
{page}
</button>
))}
</div>
</div>
</div>
<div className="hidden md:block">
<div className="ml-4 flex items-center md:ml-6">
<button
className="rounded-full border-2 border-transparent p-1 text-gray-400 hover:text-white focus:bg-gray-700 focus:text-white focus:outline-none"
aria-label="Notifications"
>
<svg
className="h-6 w-6"
stroke="currentColor"
fill="none"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
/>
</svg>
</button>
{/* Profile dropdown */}
<div className="relative ml-3">
<div>
<button
className="focus:shadow-solid flex max-w-xs items-center rounded-full text-sm text-white focus:outline-none"
id="user-menu"
aria-label="User menu"
aria-haspopup="true"
>
<img
className="h-8 w-8 rounded-full"
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
alt=""
/>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</nav>
<header className="py-10">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<h1 className="relative inline-block text-3xl font-bold leading-9 text-white">
{pages[activePage]}
</h1>
</div>
</header>
</div>
<main className="-mt-32">
<div className="mx-auto max-w-7xl px-4 pb-12 sm:px-6 lg:px-8">
<div className="rounded-lg bg-white px-5 py-6 shadow sm:px-6">
<div className="relative h-96 overflow-hidden rounded-lg">
{pages.map((page, i) => (
<Transition
appear={false}
key={page}
show={activePage === i}
className={classNames(
'absolute inset-0 rounded-lg p-8 text-3xl font-bold text-white',
colors[i]
)}
{...transitions}
>
{page} page content
</Transition>
))}
</div>
</div>
</div>
</main>
</div>
)
}
@@ -0,0 +1,170 @@
import { Transition } from '@headlessui/react'
import Head from 'next/head'
import { useEffect, useState } from 'react'
export default function App() {
let [mobileOpen, setMobileOpen] = useState(false)
useEffect(() => {
function handleEscape(event) {
if (!mobileOpen) return
if (event.key === 'Escape') {
setMobileOpen(false)
}
}
document.addEventListener('keyup', handleEscape)
return () => document.removeEventListener('keyup', handleEscape)
}, [mobileOpen])
return (
<>
<Head>
<title>Transition Component - Layout with sidebar</title>
</Head>
<div className="bg-cool-gray-100 flex h-screen overflow-hidden">
{/* Off-canvas menu for mobile */}
<Transition show={mobileOpen} unmount={false} className="fixed inset-0 z-40 flex">
{/* Off-canvas menu overlay, show/hide based on off-canvas menu state. */}
<Transition.Child
unmount={false}
enter="transition-opacity ease-linear duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity ease-linear duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
{() => (
<div className="fixed inset-0">
<div
onClick={() => setMobileOpen(false)}
className="bg-cool-gray-600 absolute inset-0 opacity-75"
/>
</div>
)}
</Transition.Child>
{/* Off-canvas menu, show/hide based on off-canvas menu state. */}
<Transition.Child
unmount={false}
enter="transition ease-in-out duration-300 transform"
enterFrom="-translate-x-full"
enterTo="translate-x-0"
leave="transition ease-in-out duration-300 transform"
leaveFrom="translate-x-0"
leaveTo="-translate-x-full"
className="relative flex w-full max-w-xs flex-1 flex-col bg-teal-600 pb-4 pt-5"
>
<div className="absolute right-0 top-0 -mr-14 p-1">
<Transition.Child
unmount={false}
className="focus:bg-cool-gray-600 flex h-12 w-12 items-center justify-center rounded-full focus:outline-none"
aria-label="Close sidebar"
as="button"
onClick={() => setMobileOpen(false)}
>
<svg
className="h-6 w-6 text-white"
stroke="currentColor"
fill="none"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</Transition.Child>
</div>
<div className="flex flex-shrink-0 items-center px-4">
<img
className="h-8 w-auto"
src="https://tailwindui.com/img/logos/easywire-logo-on-brand.svg"
alt="Easywire logo"
/>
</div>
</Transition.Child>
<div className="w-14 flex-shrink-0">
{/* Dummy element to force sidebar to shrink to fit close icon */}
</div>
</Transition>
{/* Static sidebar for desktop */}
<div className="hidden lg:flex lg:flex-shrink-0">
<div className="flex w-64 flex-col">
{/* Sidebar component, swap this element with another sidebar if you like */}
<div className="flex flex-grow flex-col overflow-y-auto bg-teal-600 pb-4 pt-5">
<div className="flex flex-shrink-0 items-center px-4">
<img
className="h-8 w-auto"
src="https://tailwindui.com/img/logos/easywire-logo-on-brand.svg"
alt="Easywire logo"
/>
</div>
</div>
</div>
</div>
<div className="flex-1 overflow-auto focus:outline-none" tabIndex={0}>
<div className="relative z-10 flex h-16 flex-shrink-0 border-b border-gray-200 bg-white lg:border-none">
<button
className="border-cool-gray-200 text-cool-gray-400 focus:bg-cool-gray-100 focus:text-cool-gray-600 border-r px-4 focus:outline-none lg:hidden"
aria-label="Open sidebar"
onClick={() => setMobileOpen(true)}
>
<svg
className="h-6 w-6 transition duration-150 ease-in-out"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4 6h16M4 12h8m-8 6h16"
/>
</svg>
</button>
{/* Search bar */}
<div className="flex flex-1 justify-between px-4 sm:px-6 lg:mx-auto lg:max-w-6xl lg:px-8">
<div className="flex flex-1">
<form className="flex w-full md:ml-0" action="#" method="GET">
<label htmlFor="search_field" className="sr-only">
Search
</label>
<div className="text-cool-gray-400 focus-within:text-cool-gray-600 relative w-full">
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center">
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
/>
</svg>
</div>
<input
id="search_field"
className="text-cool-gray-900 placeholder-cool-gray-500 focus:placeholder-cool-gray-400 block h-full w-full rounded-md py-2 pl-8 pr-3 focus:outline-none sm:text-sm"
placeholder="Search"
type="search"
/>
</div>
</form>
</div>
</div>
</div>
<main className="relative z-0 flex-1 overflow-y-auto p-8">
{/* Replace with your content */}
<div className="h-96 rounded-lg border-4 border-dashed border-gray-200"></div>
{/* /End replace */}
</main>
</div>
</div>
</>
)
}
@@ -0,0 +1,39 @@
import { Transition } from '@headlessui/react'
import { resolveValue, toast, Toaster, ToastIcon } from 'react-hot-toast'
const TailwindToaster = () => {
return (
<Toaster position="top-right">
{(t) => (
<Transition
appear
show={t.visible}
className="flex transform rounded bg-white p-4 shadow-lg"
enter="transition-all duration-500"
enterFrom="opacity-0 scale-50"
enterTo="opacity-100 scale-100"
leave="transition-all duration-150"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-75"
>
<ToastIcon toast={t} />
<p className="px-2">{resolveValue(t.message, t)}</p>
</Transition>
)}
</Toaster>
)
}
export default function App() {
return (
<div className="m-8">
<button
className="rounded bg-blue-500 p-4 text-white"
onClick={() => toast.success('This is Tailwind CSS')}
>
Create TailwindCSS Toast
</button>
<TailwindToaster />
</div>
)
}
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
+11
View File
@@ -0,0 +1,11 @@
/** @type {import('tailwindcss/types').Config} */
let config = {
content: ['./{pages,components}/**/*.{js,ts,jsx,tsx}'],
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
require('@headlessui/tailwindcss'),
],
}
module.exports = config
+21
View File
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"downlevelIteration": true,
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
+3
View File
@@ -0,0 +1,3 @@
export function classNames(...classes: (false | null | undefined | string)[]): string {
return classes.filter(Boolean).join(' ')
}
@@ -0,0 +1,37 @@
import { createPopper, Options } from '@popperjs/core'
import { RefCallback, useCallback, useMemo, useRef } from 'react'
/**
* Example implementation to use Popper: https://popper.js.org/
*/
export function usePopper(
options?: Partial<Options>
): [RefCallback<Element | null>, RefCallback<HTMLElement | null>] {
let reference = useRef<Element>(null)
let popper = useRef<HTMLElement>(null)
let cleanupCallback = useRef(() => {})
let instantiatePopper = useCallback(() => {
if (!reference.current) return
if (!popper.current) return
if (cleanupCallback.current) cleanupCallback.current()
cleanupCallback.current = createPopper(reference.current, popper.current, options).destroy
}, [reference, popper, cleanupCallback, options])
return useMemo(
() => [
(referenceDomNode) => {
reference.current = referenceDomNode
instantiatePopper()
},
(popperDomNode) => {
popper.current = popperDomNode
instantiatePopper()
},
],
[reference, popper, instantiatePopper]
)
}
+20
View File
@@ -0,0 +1,20 @@
export function match<TValue extends string | number = string, TReturnValue = unknown>(
value: TValue,
lookup: Record<TValue, TReturnValue | ((...args: any[]) => TReturnValue)>,
...args: any[]
): TReturnValue {
if (value in lookup) {
let returnValue = lookup[value]
return typeof returnValue === 'function' ? returnValue(...args) : returnValue
}
let error = new Error(
`Tried to handle "${value}" but there is no handler defined. Only defined handlers are: ${Object.keys(
lookup
)
.map((key) => `"${key}"`)
.join(', ')}.`
)
if (Error.captureStackTrace) Error.captureStackTrace(error, match)
throw error
}
@@ -0,0 +1,50 @@
import fs from 'fs'
import path from 'path'
export type ExamplesType = {
name: string
path: string
children?: ExamplesType[]
}
export async function resolveAllExamples(...paths: string[]) {
let base = path.resolve(process.cwd(), ...paths)
if (!fs.existsSync(base)) {
return false
}
let files = await fs.promises.readdir(base, { withFileTypes: true })
let items: ExamplesType[] = []
for (let file of files) {
if (file.name === '.DS_Store') {
continue
}
// Skip reserved filenames from Next. E.g.: _app.tsx, _error.tsx
if (file.name.startsWith('_')) {
continue
}
let bucket: ExamplesType = {
name: file.name.replace(/-/g, ' ').replace(/\.tsx?/g, ''),
path: [...paths, file.name]
.join('/')
.replace(/^pages/, '')
.replace(/\.tsx?/g, '')
.replace(/\/+/g, '/'),
}
if (file.isDirectory()) {
let children = await resolveAllExamples(...paths, file.name)
if (children) {
bucket.children = children
}
}
items.push(bucket)
}
return items
}
+14
View File
@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Headless UI - Playground</title>
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
</head>
<body class="h-full w-full font-sans text-gray-900 antialiased">
<div class="h-full w-full" id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+36
View File
@@ -0,0 +1,36 @@
{
"name": "playground-vue",
"private": true,
"version": "0.0.0",
"directories": {
"example": "examples"
},
"scripts": {
"prebuild": "npm run build --workspace=@headlessui/vue && npm run build --workspace=@headlessui/tailwindcss",
"predev": "npm run build --workspace=@headlessui/vue && npm run build --workspace=@headlessui/tailwindcss",
"dev:tailwindcss": "npm run watch --workspace=@headlessui/tailwindcss",
"dev:headlessui": "npm run watch --workspace=@headlessui/vue",
"dev:next": "vite serve",
"dev": "npm-run-all -p dev:*",
"build": "NODE_ENV=production vite build",
"lint-types": "echo",
"clean": "rimraf ./dist"
},
"dependencies": {
"@headlessui/vue": "*",
"@heroicons/vue": "^1.0.6",
"@tailwindcss/forms": "^0.5.2",
"@tailwindcss/typography": "^0.5.2",
"autoprefixer": "^10.4.7",
"postcss": "^8.4.14",
"tailwindcss": "^3.3.3",
"vue": "^3.2.27",
"vue-flatpickr-component": "^9.0.5",
"vue-router": "^4.0.0"
},
"devDependencies": {
"@floating-ui/vue": "^1.0.2",
"@vitejs/plugin-vue": "^3.0.3",
"vite": "^3.0.0"
}
}
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

@@ -0,0 +1,2 @@
*
!.gitignore
+31
View File
@@ -0,0 +1,31 @@
<template>
<router-view v-if="layout === 'raw'" />
<Layout v-else>
<router-view />
<KeyCaster />
</Layout>
</template>
<script>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import KeyCaster from './KeyCaster.vue'
import Layout from './Layout.vue'
export default {
name: 'App',
components: {
Layout,
KeyCaster,
},
setup() {
let route = useRoute()
let layout = computed(() => route.query['layout'] ?? 'full')
return {
layout,
}
},
}
</script>
+69
View File
@@ -0,0 +1,69 @@
<template>
<div
class="pointer-events-none fixed bottom-4 right-4 z-50 cursor-default select-none overflow-hidden rounded-md bg-blue-800 px-4 py-2 text-2xl tracking-wide text-blue-100 shadow"
v-if="keys.length > 0"
>
{{ keys.slice().reverse().join(' ') }}
</div>
</template>
<script>
import { defineComponent, ref } from 'vue'
let isMac = navigator.userAgent.indexOf('Mac OS X') !== -1
let KeyDisplay = isMac
? {
ArrowUp: '↑',
ArrowDown: '↓',
ArrowLeft: '←',
ArrowRight: '→',
Home: '↖',
End: '↘',
Alt: '⌥',
CapsLock: '⇪',
Meta: '⌘',
Shift: '⇧',
Control: '⌃',
Backspace: '⌫',
Delete: '⌦',
Enter: '↵',
Escape: '⎋',
Tab: '⇥',
ShiftTab: '⇤',
PageUp: '⇞',
PageDown: '⇟',
' ': '␣',
}
: {
ArrowUp: '↑',
ArrowDown: '↓',
ArrowLeft: '←',
ArrowRight: '→',
Meta: 'Win',
Control: 'Ctrl',
Backspace: '⌫',
Delete: 'Del',
Escape: 'Esc',
PageUp: 'PgUp',
PageDown: 'PgDn',
' ': '␣',
}
export default defineComponent({
setup() {
let keys = ref([])
window.addEventListener('keydown', (event) => {
keys.value.unshift(
event.shiftKey && event.key !== 'Shift'
? KeyDisplay[`Shift${event.key}`] ?? event.key
: KeyDisplay[event.key] ?? event.key
)
setTimeout(() => keys.value.pop(), 2000)
})
return { keys }
},
})
</script>
+80
View File
@@ -0,0 +1,80 @@
<template>
<div
class="flex h-screen flex-col overflow-hidden bg-gray-700 font-sans text-gray-900 antialiased"
>
<header
class="relative z-10 flex flex-shrink-0 items-center justify-between border-b border-gray-200 bg-gray-700 px-4 py-4 sm:px-6 lg:px-8"
>
<router-link to="/">
<svg class="h-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 243 42">
<path
fill="#fff"
d="M65.74 13.663c-2.62 0-4.702.958-5.974 2.95V6.499h-4.163V33.32h4.163V23.051c0-3.908 2.159-5.518 4.896-5.518 2.62 0 4.317 1.533 4.317 4.445V33.32h4.162V21.557c0-4.982-3.083-7.894-7.4-7.894zM79.936 25.503h15.341c.077-.536.154-1.15.154-1.724 0-5.518-3.931-10.116-9.674-10.116-6.052 0-10.176 4.407-10.176 10.078 0 5.748 4.124 10.078 10.484 10.078 3.778 0 6.668-1.572 8.441-4.177l-3.43-1.993c-.925 1.341-2.66 2.376-4.972 2.376-3.084 0-5.512-1.533-6.168-4.521zm-.038-3.372c.578-2.873 2.698-4.713 5.82-4.713 2.506 0 4.934 1.418 5.512 4.713H79.898zM113.282 14.161v2.72c-1.465-1.992-3.739-3.218-6.746-3.218-5.242 0-9.597 4.368-9.597 10.078 0 5.67 4.355 10.078 9.597 10.078 3.007 0 5.281-1.227 6.746-3.258v2.76h4.162V14.16h-4.162zm-6.09 15.71c-3.469 0-6.091-2.567-6.091-6.13 0-3.564 2.622-6.131 6.091-6.131 3.469 0 6.09 2.567 6.09 6.13 0 3.564-2.621 6.132-6.09 6.132zM136.597 6.498v10.384c-1.465-1.993-3.739-3.219-6.746-3.219-5.242 0-9.597 4.368-9.597 10.078 0 5.67 4.355 10.078 9.597 10.078 3.007 0 5.281-1.227 6.746-3.258v2.76h4.163V6.497h-4.163zm-6.09 23.374c-3.469 0-6.09-2.568-6.09-6.131 0-3.564 2.621-6.131 6.09-6.131s6.09 2.567 6.09 6.13c0 3.564-2.621 6.132-6.09 6.132zM144.648 33.32h4.163V5.348h-4.163V33.32zM155.957 25.503h15.341c.077-.536.154-1.15.154-1.724 0-5.518-3.931-10.116-9.675-10.116-6.051 0-10.176 4.407-10.176 10.078 0 5.748 4.125 10.078 10.485 10.078 3.777 0 6.668-1.572 8.441-4.177l-3.43-1.993c-.926 1.341-2.66 2.376-4.973 2.376-3.083 0-5.512-1.533-6.167-4.521zm-.038-3.372c.578-2.873 2.698-4.713 5.82-4.713 2.505 0 4.934 1.418 5.512 4.713h-11.332zM177.137 19.45c0-1.38 1.311-2.032 2.814-2.032 1.581 0 2.93.69 3.623 2.184l3.508-1.954c-1.349-2.529-3.97-3.985-7.131-3.985-3.931 0-7.053 2.26-7.053 5.863 0 6.859 10.368 4.943 10.368 8.353 0 1.533-1.426 2.146-3.276 2.146-2.12 0-3.662-1.035-4.279-2.759l-3.584 2.07c1.233 2.758 4.008 4.483 7.863 4.483 4.163 0 7.516-2.07 7.516-5.902 0-7.088-10.369-4.98-10.369-8.468zM192.774 19.45c0-1.38 1.31-2.032 2.813-2.032 1.581 0 2.93.69 3.624 2.184l3.507-1.954c-1.349-2.529-3.97-3.985-7.131-3.985-3.931 0-7.053 2.26-7.053 5.863 0 6.859 10.368 4.943 10.368 8.353 0 1.533-1.426 2.146-3.276 2.146-2.12 0-3.662-1.035-4.278-2.759l-3.585 2.07c1.233 2.758 4.009 4.483 7.863 4.483 4.163 0 7.516-2.07 7.516-5.902 0-7.088-10.368-4.98-10.368-8.468zM224.523 28.9c2.889 0 5.027-1.715 5.027-4.53v-8.782h-2.588v8.577c0 1.268-.676 2.219-2.439 2.219s-2.438-.951-2.438-2.22v-8.576h-2.569v8.782c0 2.815 2.138 4.53 5.007 4.53zM232.257 15.588V28.64h2.588V15.588h-2.588z"
/>
<path
fill="#fff"
fill-rule="evenodd"
d="M233.817 9.328H220.42c-2.96 0-5.359 2.385-5.359 5.327v13.318c0 2.942 2.399 5.327 5.359 5.327h13.397c2.959 0 5.358-2.385 5.358-5.327V14.655c0-2.942-2.399-5.327-5.358-5.327zM220.42 6.664c-4.439 0-8.038 3.578-8.038 7.99v13.319c0 4.413 3.599 7.99 8.038 7.99h13.397c4.439 0 8.038-3.577 8.038-7.99V14.655c0-4.413-3.599-7.99-8.038-7.99H220.42z"
clip-rule="evenodd"
/>
<path
fill="#fff"
fill-rule="evenodd"
d="M220.42 9.328h13.397c2.959 0 5.358 2.385 5.358 5.327v13.318c0 2.942-2.399 5.327-5.358 5.327H220.42c-2.96 0-5.359-2.385-5.359-5.327V14.655c0-2.942 2.399-5.327 5.359-5.327zm-8.038 5.327c0-4.413 3.599-7.99 8.038-7.99h13.397c4.439 0 8.038 3.577 8.038 7.99v13.318c0 4.413-3.599 7.99-8.038 7.99H220.42c-4.439 0-8.038-3.577-8.038-7.99V14.655z"
clip-rule="evenodd"
/>
<path
fill="url(#prefix__paint0_linear)"
d="M8.577 26.097l25.779-8.556c-.514-3.201-.88-5.342-1.307-6.974-.457-1.756-.821-2.226-.965-2.39a5.026 5.026 0 00-1.81-1.306c-.2-.086-.762-.284-2.583-.175-1.924.116-4.453.507-8.455 1.137-4.003.63-6.529 1.035-8.395 1.516-1.766.456-2.239.817-2.403.96a4.999 4.999 0 00-1.315 1.8c-.085.198-.285.757-.175 2.568.116 1.913.51 4.426 1.143 8.405.178 1.114.337 2.113.486 3.015z"
/>
<path
fill="url(#prefix__paint1_linear)"
fill-rule="evenodd"
d="M1.47 24.124C.244 16.427-.37 12.58.96 9.49A11.665 11.665 0 014.027 5.29c2.545-2.21 6.416-2.82 14.16-4.039C25.93.031 29.8-.578 32.907.743a11.729 11.729 0 014.225 3.05c2.223 2.53 2.836 6.38 4.063 14.076 1.226 7.698 1.84 11.546.511 14.636a11.666 11.666 0 01-3.069 4.199c-2.545 2.21-6.416 2.82-14.159 4.039-7.743 1.219-11.614 1.828-14.722.508a11.728 11.728 0 01-4.224-3.05C3.31 35.67 2.697 31.82 1.47 24.123zm13.657 13.668c2.074-.125 4.743-.54 8.697-1.163 3.953-.622 6.62-1.047 8.632-1.566 1.949-.502 2.846-.992 3.426-1.496a7.5 7.5 0 001.973-2.7c.302-.703.494-1.703.372-3.7-.125-2.063-.543-4.716-1.17-8.646-.625-3.93-1.053-6.582-1.574-8.582-.506-1.937-.999-2.83-1.505-3.405a7.54 7.54 0 00-2.716-1.961c-.707-.301-1.713-.492-3.723-.371-2.074.125-4.743.54-8.697 1.163-3.953.622-6.62 1.047-8.632 1.565-1.949.503-2.846.993-3.426 1.497a7.5 7.5 0 00-1.972 2.699c-.303.704-.495 1.704-.373 3.701.125 2.062.543 4.716 1.17 8.646.625 3.93 1.053 6.582 1.574 8.581.506 1.938 1 2.83 1.505 3.406a7.54 7.54 0 002.716 1.961c.707.3 1.713.492 3.723.37z"
clip-rule="evenodd"
/>
<defs>
<linearGradient
id="prefix__paint0_linear"
x1="16.759"
x2="23.386"
y1="0"
y2="41.662"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#66E3FF" />
<stop offset="1" stop-color="#7064F9" />
</linearGradient>
<linearGradient
id="prefix__paint1_linear"
x1="16.759"
x2="23.386"
y1="0"
y2="41.662"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#66E3FF" />
<stop offset="1" stop-color="#7064F9" />
</linearGradient>
</defs>
</svg>
</router-link>
<span class="font-bold text-white">(Vue)</span>
</header>
<main class="flex-1 overflow-auto bg-gray-50">
<slot></slot>
<KeyCaster />
</main>
</div>
</template>
<script>
import KeyCaster from './KeyCaster.vue'
export default {
name: 'Layout',
components: {
KeyCaster,
},
}
</script>
@@ -0,0 +1,9 @@
<template>
<button
type="button"
class="ui-focus-visible:ring-2 ui-focus-visible:ring-offset-2 flex items-center rounded-md border border-gray-300 bg-white px-2 py-1 ring-gray-500 ring-offset-gray-100 focus:outline-none"
v-bind="$props"
>
<slot></slot>
</button>
</template>
+42
View File
@@ -0,0 +1,42 @@
<script setup>
import { defineComponent } from 'vue'
import { useRouter } from 'vue-router'
let Examples = defineComponent({
props: ['routes'],
setup:
(props, { slots }) =>
() =>
slots.default({ routes: props.routes, slots }),
})
let router = useRouter()
let routes = router
.getRoutes()
.filter((example) => example.path !== '/')
.filter((route) => route.meta.isRoot)
</script>
<template>
<div class="container mx-auto my-24">
<div class="prose">
<h2>Examples</h2>
<Examples :routes="routes" v-slot="{ routes, slots }">
<ul>
<li v-for="{ children, meta, path } in routes">
<template v-if="children.length > 0">
<h3 class="text-xl">{{ meta.name }}</h3>
<!-- This is a bit cursed but it works -->
<component v-for="vnode in slots.default({ routes: children, slots })" :is="vnode" />
</template>
<template v-else>
<router-link :key="path" :to="path">
{{ meta.name }}
</router-link>
</template>
</li>
</ul>
</Examples>
</div>
</div>
</template>
@@ -0,0 +1,290 @@
<template>
<div class="py-8">
<form
class="mx-auto flex h-full max-w-4xl flex-col items-start justify-center gap-8 rounded-lg border bg-white p-6"
@submit.prevent="submitForm"
>
<div class="grid w-full grid-cols-[repeat(auto-fill,minmax(350px,1fr))] items-start gap-3">
<Section title="Switch">
<Section title="Single value">
<SwitchGroup as="div" class="flex items-center justify-between space-x-4">
<SwitchLabel>Enable notifications</SwitchLabel>
<Switch
:defaultChecked="true"
name="notifications"
class="ui-checked:bg-blue-600 ui-not-checked:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
>
<span
class="ui-checked:translate-x-5 ui-not-checked:translate-x-0 inline-block h-5 w-5 transform rounded-full bg-white"
/>
</Switch>
</SwitchGroup>
</Section>
<Section title="Multiple values">
<SwitchGroup as="div" class="flex items-center justify-between space-x-4">
<SwitchLabel>Apple</SwitchLabel>
<Switch
name="fruit[]"
value="apple"
class="ui-checked:bg-blue-600 ui-not-checked:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
>
<span
class="ui-checked:translate-x-5 ui-not-checked:translate-x-0 inline-block h-5 w-5 transform rounded-full bg-white"
/>
</Switch>
</SwitchGroup>
<SwitchGroup as="div" class="flex items-center justify-between space-x-4">
<SwitchLabel>Banana</SwitchLabel>
<Switch
name="fruit[]"
value="banana"
class="ui-checked:bg-blue-600 ui-not-checked:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
>
<span
class="ui-checked:translate-x-5 ui-not-checked:translate-x-0 inline-block h-5 w-5 transform rounded-full bg-white"
/>
</Switch>
</SwitchGroup>
</Section>
</Section>
<Section title="Radio Group">
<RadioGroup defaultValue="sm" name="size">
<div class="flex -space-x-px rounded-md bg-white">
<RadioGroupOption
v-for="size in sizes"
:key="size"
:value="size"
class="ui-active:z-10 ui-active:border-blue-200 ui-active:bg-blue-50 ui-not-active:border-gray-200 relative flex w-20 border px-2 py-4 first:rounded-l-md last:rounded-r-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
>
<div class="flex w-full items-center justify-between">
<div class="ml-3 flex cursor-pointer flex-col">
<span
class="ui-active:text-blue-900 ui-not-active:text-gray-900 block text-sm font-medium leading-5"
>
{{ size }}
</span>
</div>
<div>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
class="ui-checked:block ui-not-checked:hidden h-5 w-5 text-blue-500"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
</div>
</RadioGroupOption>
</div>
</RadioGroup>
</Section>
<Section title="Listbox">
<div class="w-full space-y-1">
<Listbox name="person" :defaultValue="people[1]" v-slot="{ value }">
<div class="relative">
<span class="inline-block w-full rounded-md shadow-sm">
<ListboxButton
class="relative w-full cursor-default rounded-md border border-gray-300 bg-white py-2 pl-3 pr-10 text-left focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 sm:text-sm sm:leading-5"
>
<span class="block truncate">{{ value?.name?.first }}</span>
<span
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
>
<svg
class="h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
>
<path
d="M7 7l3-3 3 3m0 6l-3 3-3-3"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
</ListboxButton>
</span>
<div class="absolute z-10 mt-1 w-full rounded-md bg-white shadow-lg">
<ListboxOptions
class="shadow-xs max-h-60 overflow-auto rounded-md py-1 text-base leading-6 focus:outline-none sm:text-sm sm:leading-5"
>
<ListboxOption
v-for="person in people"
:key="person.id"
:value="person"
class="ui-active:bg-blue-600 ui-active:text-white ui-not-active:text-gray-900 relative cursor-default select-none py-2 pl-3 pr-9"
>
<span
class="ui-selected:font-semibold ui-not-selected:font-normal block truncate"
>
{{ person.name.first }}
</span>
<span
class="ui-selected:block ui-not-selected:hidden ui-active:text-white ui-not-active:text-blue-600 absolute inset-y-0 right-0 flex items-center pr-4"
>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</span>
</ListboxOption>
</ListboxOptions>
</div>
</div>
</Listbox>
</div>
</Section>
<Section title="Combobox">
<div class="w-full space-y-1">
<Combobox
name="location"
defaultValue="New York"
@change="query = ''"
v-slot="{ open, value }"
>
<div class="relative">
<div class="flex w-full flex-col">
<ComboboxInput
@change="query = $event.target.value"
class="w-full rounded-md border-gray-300 bg-clip-padding px-3 py-1 shadow-sm focus:border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
placeholder="Search users..."
/>
<div
class="flex border-t"
:class="[value && !open ? 'border-transparent' : 'border-gray-200']"
>
<div class="absolute z-10 mt-1 w-full rounded-md bg-white shadow-lg">
<ComboboxOptions
class="shadow-xs max-h-60 overflow-auto rounded-md py-1 text-base leading-6 sm:text-sm sm:leading-5"
>
<ComboboxOption
v-for="location in locations.filter((l) =>
l.toLowerCase().includes(query.toLowerCase())
)"
:key="location"
:value="location"
class="ui-active:bg-blue-600 ui-active:text-white ui-not-active:text-gray-900 relative flex cursor-default select-none space-x-4 py-2 pl-3 pr-9"
>
<span
class="ui-selected:font-semibold ui-not-selected:font-normal block truncate"
>
{{ location }}
</span>
<span
class="ui-active:block ui-not-active:hidden ui-active:text-white ui-not-active:text-blue-600 absolute inset-y-0 right-0 flex items-center pr-4"
>
<svg class="h-5 w-5" viewBox="0 0 25 24" fill="none">
<path
d="M11.25 8.75L14.75 12L11.25 15.25"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
</ComboboxOption>
</ComboboxOptions>
</div>
</div>
</div>
</div>
</Combobox>
</div>
</Section>
</div>
<div class="space-x-4">
<button
class="rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 sm:text-sm sm:leading-5"
>
Submit
</button>
<button
type="reset"
class="rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 sm:text-sm sm:leading-5"
>
Reset
</button>
</div>
<div class="w-full border-t py-4">
<span>Form data (entries):</span>
<pre class="text-sm">{{ JSON.stringify([...result.entries()], null, 2) }}</pre>
</div>
</form>
</div>
</template>
<script setup>
import { ref } from 'vue'
import {
Switch,
SwitchLabel,
SwitchGroup,
RadioGroup,
RadioGroupOption,
Listbox,
ListboxButton,
ListboxOptions,
ListboxOption,
ListboxLabel,
Combobox,
ComboboxButton,
ComboboxInput,
ComboboxOptions,
ComboboxOption,
ComboboxLabel,
} from '@headlessui/vue'
let html = String.raw
let Section = {
props: {
title: { type: String, default: '' },
},
template: html`
<fieldset class="rounded-lg border bg-gray-200/20 p-3">
<legend class="rounded-md border bg-gray-100 px-2 text-sm uppercase">{{ title }}</legend>
<div class="flex flex-col gap-3">
<slot />
</div>
</fieldset>
`,
}
function submitForm(event) {
result.value = new FormData(event.currentTarget)
}
let sizes = ref(['xs', 'sm', 'md', 'lg', 'xl'])
let people = ref([
{ id: 1, name: { first: 'Alice' } },
{ id: 2, name: { first: 'Bob' } },
{ id: 3, name: { first: 'Charlie' } },
])
let locations = ref(['New York', 'London', 'Paris', 'Berlin'])
let result = ref(
typeof window === 'undefined' || typeof document === 'undefined' ? [] : new FormData()
)
let query = ref('')
</script>
@@ -0,0 +1,31 @@
<template>
<button @click="open = true">Open dialog</button>
<Dialog :open="open" @close="open = false" class="fixed inset-0 grid place-content-center">
<DialogOverlay class="fixed inset-0 bg-gray-500/70" />
<div
class="inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle"
>
<div class="bg-white px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
<TabGroup>
<TabList>
<Tab class="px-3 py-2">Tab 1</Tab>
<Tab class="px-3 py-2">Tab 2</Tab>
<Tab class="px-3 py-2">Tab 3</Tab>
</TabList>
<TabPanels>
<TabPanel class="px-3 py-2">Panel 1</TabPanel>
<TabPanel class="px-3 py-2">Panel 2</TabPanel>
<TabPanel class="px-3 py-2">Panel 3</TabPanel>
</TabPanels>
</TabGroup>
</div>
</div>
</Dialog>
</template>
<script setup>
import { ref } from 'vue'
import { Dialog, DialogOverlay, TabGroup, TabList, Tab, TabPanels, TabPanel } from '@headlessui/vue'
let open = ref(false)
</script>
@@ -0,0 +1,149 @@
<template>
<div class="flex h-full w-screen justify-center bg-gray-50 p-12">
<div class="mx-auto w-full max-w-xs">
<div class="py-8 font-mono text-xs">Selected timezone: {{ activeTimezone }}</div>
<div class="space-y-1">
<Combobox nullable v-model="activeTimezone" as="div" :virtual="virtual">
<ComboboxLabel class="block text-sm font-medium leading-5 text-gray-700">
Timezone
{{
virtual
? `(virtual — ${nf.format(timezones.length)} items)`
: `(${nf.format(timezones.length)} items)`
}}
</ComboboxLabel>
<div class="relative">
<span class="relative inline-flex flex-row overflow-hidden rounded-md border shadow-sm">
<ComboboxInput
@change="query = $event.target.value"
class="border-none px-3 py-1 outline-none"
/>
<ComboboxButton
class="cursor-default border-l bg-gray-100 px-1 text-indigo-600 focus:outline-none"
>
<span class="pointer-events-none flex items-center px-2">
<svg
class="h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
>
<path
d="M7 7l3-3 3 3m0 6l-3 3-3-3"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
</ComboboxButton>
</span>
<div class="absolute mt-1 w-full rounded-md bg-white shadow-lg">
<ComboboxOptions
v-if="!virtual"
class="shadow-xs max-h-60 overflow-auto rounded-md py-1 text-base leading-6 focus:outline-none sm:text-sm sm:leading-5"
>
<ComboboxOption
v-for="(timezone, idx) in timezones"
:key="timezone"
:value="timezone"
:order="virtual ? idx : undefined"
v-slot="{ active, selected }"
as="template"
>
<li
:class="[
'relative w-full cursor-default select-none py-2 pl-3 pr-9 focus:outline-none',
active ? 'bg-indigo-600 text-white' : 'text-gray-900',
]"
>
<span :class="['block truncate', selected ? 'font-semibold' : 'font-normal']">
{{ timezone }}
</span>
<span
v-if="selected"
:class="[
'absolute inset-y-0 right-0 flex items-center pr-4',
active ? 'text-white' : 'text-indigo-600',
]"
>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</span>
</li>
</ComboboxOption>
</ComboboxOptions>
<ComboboxOptions
v-if="virtual"
class="shadow-xs max-h-60 overflow-auto rounded-md py-1 text-base leading-6 focus:outline-none sm:text-sm sm:leading-5"
v-slot="{ option: timezone }"
>
<ComboboxOption :value="timezone" v-slot="{ active, selected }" as="template">
<li
:class="[
'relative w-full cursor-default select-none py-2 pl-3 pr-9 focus:outline-none',
active ? 'bg-indigo-600 text-white' : 'text-gray-900',
]"
>
<span :class="['block truncate', selected ? 'font-semibold' : 'font-normal']">
{{ timezone }}
</span>
<span
v-if="selected"
:class="[
'absolute inset-y-0 right-0 flex items-center pr-4',
active ? 'text-white' : 'text-indigo-600',
]"
>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</span>
</li>
</ComboboxOption>
</ComboboxOptions>
</div>
</div>
</Combobox>
</div>
</div>
</div>
</template>
<script setup>
let nf = new Intl.NumberFormat('en-US')
import { ref, computed } from 'vue'
import {
Combobox,
ComboboxButton,
ComboboxInput,
ComboboxLabel,
ComboboxOption,
ComboboxOptions,
} from '@headlessui/vue'
let props = defineProps(['data', 'initial', 'virtual'])
let query = ref('')
let activeTimezone = ref(props.initial)
let timezones = computed(() => {
return query.value === ''
? props.data
: props.data.filter((timezone) => timezone.toLowerCase().includes(query.value.toLowerCase()))
})
let virtual = computed(() => {
return props.virtual ? { options: timezones.value } : null
})
</script>
@@ -0,0 +1,133 @@
<script>
import { countries as allCountries } from '../../data'
import { ref, defineComponent, computed, onMounted, watch } from 'vue'
import {
Combobox,
ComboboxButton,
ComboboxInput,
ComboboxLabel,
ComboboxOption,
ComboboxOptions,
} from '@headlessui/vue'
export default defineComponent({
components: {
Combobox,
ComboboxButton,
ComboboxInput,
ComboboxLabel,
ComboboxOption,
ComboboxOptions,
},
setup() {
let query = ref('')
let activeCountry = ref(allCountries[2]) // allCountries[Math.floor(Math.random() * allCountries.length)]
let filteredCountries = computed(() => {
return query.value === ''
? allCountries
: allCountries.filter((country) => {
return country.toLowerCase().includes(query.value.toLowerCase())
})
})
// Choose a random country on mount
onMounted(() => {
activeCountry.value = allCountries[Math.floor(Math.random() * allCountries.length)]
})
watch(activeCountry, () => {
query.value = ''
})
return {
query,
activeCountry,
filteredCountries,
}
},
})
</script>
<template>
<div class="flex h-full w-screen justify-center bg-gray-50 p-12">
<div class="mx-auto w-full max-w-xs">
<div class="py-8 font-mono text-xs">
Selected country: {{ activeCountry?.name ?? 'Nothing yet' }}
</div>
<div class="space-y-1">
<Combobox v-model="activeCountry" as="div">
<ComboboxLabel class="block text-sm font-medium leading-5 text-gray-700">
Assigned to
</ComboboxLabel>
<div class="relative">
<span class="relative inline-flex flex-row overflow-hidden rounded-md border shadow-sm">
<ComboboxInput
@change="query = $event.target.value"
class="border-none px-3 py-1 outline-none"
/>
<ComboboxButton
class="cursor-default border-l bg-gray-100 px-1 text-indigo-600 focus:outline-none"
>
<span class="pointer-events-none flex items-center px-2">
<svg
class="h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
>
<path
d="M7 7l3-3 3 3m0 6l-3 3-3-3"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
</ComboboxButton>
</span>
<div class="absolute mt-1 w-full rounded-md bg-white shadow-lg">
<ComboboxOptions
class="shadow-xs max-h-60 overflow-auto rounded-md py-1 text-base leading-6 focus:outline-none sm:text-sm sm:leading-5"
>
<ComboboxOption
v-for="country in filteredCountries"
:key="country"
:value="country"
v-slot="{ active, selected }"
>
<div
:class="[
'relative cursor-default select-none py-2 pl-3 pr-9 focus:outline-none',
active ? 'bg-indigo-600 text-white' : 'text-gray-900',
]"
>
<span :class="['block truncate', selected ? 'font-semibold' : 'font-normal']">
{{ country }}
</span>
<span
v-if="selected"
:class="[
'absolute inset-y-0 right-0 flex items-center pr-4',
active ? 'text-white' : 'text-indigo-600',
]"
>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</span>
</div>
</ComboboxOption>
</ComboboxOptions>
</div>
</div>
</Combobox>
</div>
</div>
</div>
</template>
@@ -0,0 +1,146 @@
<script>
import { ref, defineComponent, computed, onMounted, watch } from 'vue'
import {
Combobox,
ComboboxButton,
ComboboxInput,
ComboboxLabel,
ComboboxOption,
ComboboxOptions,
} from '@headlessui/vue'
let everybody = [
{ id: 1, name: 'Wade Cooper' },
{ id: 2, name: 'Arlene Mccoy' },
{ id: 3, name: 'Devon Webb' },
{ id: 4, name: 'Tom Cook' },
{ id: 5, name: 'Tanya Fox' },
{ id: 6, name: 'Hellen Schmidt' },
{ id: 7, name: 'Caroline Schultz' },
{ id: 8, name: 'Mason Heaney' },
{ id: 9, name: 'Claudie Smitham' },
{ id: 10, name: 'Emil Schaefer' },
]
export default defineComponent({
components: {
Combobox,
ComboboxButton,
ComboboxInput,
ComboboxLabel,
ComboboxOption,
ComboboxOptions,
},
setup() {
let query = ref('')
let activePerson = ref(everybody[2]) // everybody[Math.floor(Math.random() * everybody.length)]
let filteredPeople = computed(() => {
return query.value === ''
? everybody
: everybody.filter((person) => {
return person.name.toLowerCase().includes(query.value.toLowerCase())
})
})
// Choose a random person on mount
onMounted(() => {
activePerson.value = everybody[Math.floor(Math.random() * everybody.length)]
})
watch(activePerson, () => {
query.value = ''
})
return {
query,
activePerson,
filteredPeople,
}
},
})
</script>
<template>
<div class="flex h-full w-screen justify-center bg-gray-50 p-12">
<div class="mx-auto w-full max-w-xs">
<div class="py-8 font-mono text-xs">
Selected person: {{ activePerson?.name ?? 'Nobody yet' }}
</div>
<div class="space-y-1">
<Combobox v-model="activePerson" as="div" immediate>
<ComboboxLabel class="block text-sm font-medium leading-5 text-gray-700">
Assigned to
</ComboboxLabel>
<div class="relative">
<span class="relative inline-flex flex-row overflow-hidden rounded-md border shadow-sm">
<ComboboxInput
@change="query = $event.target.value"
:displayValue="(person) => person?.name ?? ''"
class="border-none px-3 py-1 outline-none"
/>
<ComboboxButton
class="cursor-default border-l bg-gray-100 px-1 text-indigo-600 focus:outline-none"
>
<span class="pointer-events-none flex items-center px-2">
<svg
class="h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
>
<path
d="M7 7l3-3 3 3m0 6l-3 3-3-3"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
</ComboboxButton>
</span>
<div class="absolute mt-1 w-full rounded-md bg-white shadow-lg">
<ComboboxOptions
class="shadow-xs max-h-60 overflow-auto rounded-md py-1 text-base leading-6 focus:outline-none sm:text-sm sm:leading-5"
>
<ComboboxOption
v-for="person in filteredPeople"
:key="person.id"
:value="person"
v-slot="{ active, selected }"
>
<div
:class="[
'relative cursor-default select-none py-2 pl-3 pr-9 focus:outline-none',
active ? 'bg-indigo-600 text-white' : 'text-gray-900',
]"
>
<span :class="['block truncate', selected ? 'font-semibold' : 'font-normal']">
{{ person.name }}
</span>
<span
v-if="selected"
:class="[
'absolute inset-y-0 right-0 flex items-center pr-4',
active ? 'text-white' : 'text-indigo-600',
]"
>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</span>
</div>
</ComboboxOption>
</ComboboxOptions>
</div>
</div>
</Combobox>
</div>
</div>
</div>
</template>
@@ -0,0 +1,165 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import {
Combobox,
ComboboxLabel,
ComboboxInput,
ComboboxOption,
ComboboxOptions,
ComboboxButton,
} from '@headlessui/vue'
type Option = {
name: string
disabled: boolean
empty?: boolean
}
let list = ref([
{ name: 'Alice', disabled: false },
{ name: 'Bob', disabled: false },
{ name: 'Charlie', disabled: false },
{ name: 'David', disabled: false },
{ name: 'Eve', disabled: false },
{ name: 'Fred', disabled: false },
{ name: 'George', disabled: false },
{ name: 'Helen', disabled: false },
{ name: 'Iris', disabled: false },
{ name: 'John', disabled: false },
{ name: 'Kate', disabled: false },
{ name: 'Linda', disabled: false },
{ name: 'Michael', disabled: false },
{ name: 'Nancy', disabled: false },
{ name: 'Oscar', disabled: true },
{ name: 'Peter', disabled: false },
{ name: 'Quentin', disabled: false },
{ name: 'Robert', disabled: false },
{ name: 'Sarah', disabled: false },
{ name: 'Thomas', disabled: false },
{ name: 'Ursula', disabled: false },
{ name: 'Victor', disabled: false },
{ name: 'Wendy', disabled: false },
{ name: 'Xavier', disabled: false },
{ name: 'Yvonne', disabled: false },
{ name: 'Zachary', disabled: false },
])
let emptyOption = { name: 'No results', disabled: true, empty: true }
let query = ref('')
let selectedPerson = ref<Option | null>(list.value[0])
let optionsRef = ref<HTMLUListElement | null>(null)
let filtered = computed(() => {
return query.value === ''
? list.value
: list.value.filter((item) => item.name.toLowerCase().includes(query.value.toLowerCase()))
})
</script>
<template>
<div class="mx-auto max-w-fit">
<div class="py-8 font-mono text-xs">Selected person: {{ selectedPerson?.name ?? 'N/A' }}</div>
<Combobox
:virtual="{
options: filtered.length > 0 ? filtered : [emptyOption],
disabled: (option) => option.disabled || option.empty,
}"
v-model="selectedPerson"
@update:modelValue="() => (query = '')"
nullable
as="div"
>
<ComboboxLabel class="block text-sm font-medium leading-5 text-gray-700">
Person
</ComboboxLabel>
<div class="relative">
<span class="relative inline-flex flex-row overflow-hidden rounded-md border shadow-sm">
<ComboboxInput
@change="(e) => (query = e.target.value)"
:displayValue="(option: Option | null) => option?.name ?? ''"
class="border-none px-3 py-1 outline-none"
/>
<ComboboxButton as="button">
<span class="pointer-events-none flex items-center px-2">
<svg
class="h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
>
<path
d="M7 7l3-3 3 3m0 6l-3 3-3-3"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
</ComboboxButton>
</span>
<div class="absolute mt-1 w-full rounded-md bg-white shadow-lg">
<ComboboxOptions
:ref="optionsRef"
:class="[
'shadow-xs max-h-60 rounded-md py-1 text-base leading-6 focus:outline-none sm:text-sm sm:leading-5',
filtered.length === 0 ? 'overflow-hidden' : 'overflow-auto',
]"
v-slot="{ option }"
>
<template v-if="option.empty">
<ComboboxOption
:value="option"
class="relative w-full cursor-default select-none px-3 py-2 text-center focus:outline-none"
disabled
>
<div class="relative grid h-full grid-cols-1 grid-rows-1">
<div class="absolute inset-0">
<svg
fill="none"
viewBox="0 0 24 24"
stroke-width="0.5"
stroke="currentColor"
class="-translate-y-1/4 text-gray-500/5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456zM16.894 20.567L16.5 21.75l-.394-1.183a2.25 2.25 0 00-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 001.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 001.423 1.423l1.183.394-1.183.394a2.25 2.25 0 00-1.423 1.423z"
/>
</svg>
</div>
<div
class="z-20 col-span-full col-start-1 row-span-full row-start-1 flex flex-col items-center justify-center p-8"
>
<h3 class="mx-2 mb-4 text-xl font-semibold text-gray-400">No people found</h3>
</div>
</div>
</ComboboxOption>
</template>
<template v-else>
<ComboboxOption
as="template"
v-slot="{ active }"
:disabled="option.disabled"
:value="option"
>
<div
:class="[
'relative w-full cursor-default select-none py-2 pl-3 pr-9 focus:outline-none',
active ? 'bg-indigo-600 text-white' : 'text-gray-900',
]"
>
<span class="block truncate">
{{ option.name }}
</span>
</div>
</ComboboxOption>
</template>
</ComboboxOptions>
</div>
</div>
</Combobox>
</div>
</template>
@@ -0,0 +1,37 @@
<template>
<div className="flex flex-col p-12">
<label class="mx-auto flex w-24 items-center gap-2">
<span>Items:</span>
<select v-model="count" class="mx-auto">
<option :value="100">100</option>
<option :value="1_000">1000</option>
<option :value="10_000">10k</option>
<option :value="100_000">100k</option>
</select>
</label>
<div class="flex">
<Example :data="list" initial="Europe/Brussels #1" :virtual="true" />
<Example :data="list" initial="Europe/Brussels #1" :virtual="false" />
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { timezones as _allTimezones } from '../../data'
import Example from './_virtual-example.vue'
let count = ref(1_000)
let list = computed(() => {
console.time('Generating list')
let result = []
while (result.length < Number(count.value)) {
let batch = Math.floor(result.length / _allTimezones.length) + 1
result.push(`${_allTimezones[result.length % _allTimezones.length]} #${batch}`)
}
console.timeEnd('Generating list')
return result
})
</script>
@@ -0,0 +1,146 @@
<script>
import { ref, defineComponent, computed, onMounted, watch } from 'vue'
import {
Combobox,
ComboboxButton,
ComboboxInput,
ComboboxLabel,
ComboboxOption,
ComboboxOptions,
} from '@headlessui/vue'
let everybody = [
{ id: 1, name: 'Wade Cooper' },
{ id: 2, name: 'Arlene Mccoy' },
{ id: 3, name: 'Devon Webb' },
{ id: 4, name: 'Tom Cook' },
{ id: 5, name: 'Tanya Fox' },
{ id: 6, name: 'Hellen Schmidt' },
{ id: 7, name: 'Caroline Schultz' },
{ id: 8, name: 'Mason Heaney' },
{ id: 9, name: 'Claudie Smitham' },
{ id: 10, name: 'Emil Schaefer' },
]
export default defineComponent({
components: {
Combobox,
ComboboxButton,
ComboboxInput,
ComboboxLabel,
ComboboxOption,
ComboboxOptions,
},
setup() {
let query = ref('')
let activePerson = ref(everybody[2]) // everybody[Math.floor(Math.random() * everybody.length)]
let filteredPeople = computed(() => {
return query.value === ''
? everybody
: everybody.filter((person) => {
return person.name.toLowerCase().includes(query.value.toLowerCase())
})
})
// Choose a random person on mount
onMounted(() => {
activePerson.value = everybody[Math.floor(Math.random() * everybody.length)]
})
watch(activePerson, () => {
query.value = ''
})
return {
query,
activePerson,
filteredPeople,
}
},
})
</script>
<template>
<div class="flex h-full w-screen justify-center bg-gray-50 p-12">
<div class="mx-auto w-full max-w-xs">
<div class="py-8 font-mono text-xs">
Selected person: {{ activePerson?.name ?? 'Nobody yet' }}
</div>
<div class="space-y-1">
<Combobox v-model="activePerson" as="div">
<ComboboxLabel class="block text-sm font-medium leading-5 text-gray-700">
Assigned to
</ComboboxLabel>
<div class="relative">
<span class="relative inline-flex flex-row overflow-hidden rounded-md border shadow-sm">
<ComboboxInput
@change="query = $event.target.value"
:displayValue="(person) => person?.name ?? ''"
class="border-none px-3 py-1 outline-none"
/>
<ComboboxButton
class="cursor-default border-l bg-gray-100 px-1 text-indigo-600 focus:outline-none"
>
<span class="pointer-events-none flex items-center px-2">
<svg
class="h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
>
<path
d="M7 7l3-3 3 3m0 6l-3 3-3-3"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
</ComboboxButton>
</span>
<div class="absolute mt-1 w-full rounded-md bg-white shadow-lg">
<ComboboxOptions
class="shadow-xs max-h-60 overflow-auto rounded-md py-1 text-base leading-6 focus:outline-none sm:text-sm sm:leading-5"
>
<ComboboxOption
v-for="person in filteredPeople"
:key="person.id"
:value="person"
v-slot="{ active, selected }"
>
<div
:class="[
'relative cursor-default select-none py-2 pl-3 pr-9 focus:outline-none',
active ? 'bg-indigo-600 text-white' : 'text-gray-900',
]"
>
<span :class="['block truncate', selected ? 'font-semibold' : 'font-normal']">
{{ person.name }}
</span>
<span
v-if="selected"
:class="[
'absolute inset-y-0 right-0 flex items-center pr-4',
active ? 'text-white' : 'text-indigo-600',
]"
>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</span>
</div>
</ComboboxOption>
</ComboboxOptions>
</div>
</div>
</Combobox>
</div>
</div>
</div>
</template>
@@ -0,0 +1,174 @@
<script>
import { watch, ref, defineComponent, computed } from 'vue'
import {
Combobox,
ComboboxButton,
ComboboxInput,
ComboboxLabel,
ComboboxOption,
ComboboxOptions,
} from '@headlessui/vue'
let everybody = [
{ id: 1, img: 'https://github.com/adamwathan.png', name: 'Adam Wathan' },
{ id: 2, img: 'https://github.com/sschoger.png', name: 'Steve Schoger' },
{ id: 3, img: 'https://github.com/bradlc.png', name: 'Brad Cornes' },
{ id: 4, img: 'https://github.com/simonswiss.png', name: 'Simon Vrachliotis' },
{ id: 5, img: 'https://github.com/robinmalfait.png', name: 'Robin Malfait' },
{
id: 6,
img: 'https://pbs.twimg.com/profile_images/1478879681491394569/eV2PyCnm_400x400.jpg',
name: 'James McDonald',
},
{ id: 7, img: 'https://github.com/reinink.png', name: 'Jonathan Reinink' },
{ id: 8, img: 'https://github.com/thecrypticace.png', name: 'Jordan Pittman' },
]
export default defineComponent({
components: {
Combobox,
ComboboxButton,
ComboboxInput,
ComboboxLabel,
ComboboxOption,
ComboboxOptions,
},
setup() {
let query = ref('')
let activePerson = ref(everybody[2])
// Choose a random person on mount
activePerson.value = everybody[Math.floor(Math.random() * everybody.length)]
watch(
activePerson,
(person) => {
query.value = person?.name ?? ''
},
{ mode: 'sync' }
)
function setPerson(person) {
setActivePerson(person)
setQuery(person.name ?? '')
}
let people = computed(() => {
return query.value === ''
? everybody
: everybody.filter((person) =>
person.name.toLowerCase().includes(query.value.toLowerCase())
)
})
let groups = computed(() => {
return people.value.reduce((groups, person) => {
let lastNameLetter = person.name.split(' ')[1][0]
groups.set(lastNameLetter, [...(groups.get(lastNameLetter) || []), person])
return groups
}, new Map())
})
let sortedGroups = computed(() => {
return Array.from(groups.value.entries()).sort(([letterA], [letterZ]) =>
letterA.localeCompare(letterZ)
)
})
return {
query,
activePerson,
people,
groups,
sortedGroups,
displayValue: (item) => item?.name,
}
},
})
</script>
<template>
<div class="flex h-full w-screen justify-center bg-gray-50 p-12">
<div class="mx-auto w-full max-w-lg">
<div class="space-y-1">
<Combobox
as="div"
v-model="activePerson"
class="w-full overflow-hidden rounded border border-black/5 bg-white bg-clip-padding shadow-sm"
v-slot="{ activeOption }"
>
<div class="flex w-full flex-col">
<ComboboxInput
@change="query = $event.target.value"
class="w-full rounded-none border-none bg-none px-3 py-1 outline-none"
placeholder="Search users…"
:displayValue="displayValue"
/>
<div class="flex">
<ComboboxOptions
class="shadow-xs max-h-60 flex-1 overflow-auto text-base leading-6 focus:outline-none sm:text-sm sm:leading-5"
>
<template v-for="[letter, people] of sortedGroups" :key="letter">
<div class="bg-gray-100 px-4 py-2">{{ letter }}</div>
<ComboboxOption
v-for="person in people"
:key="person.id"
:value="person"
v-slot="{ active, selected }"
>
<div
:class="[
'relative flex cursor-default select-none space-x-4 py-2 pl-3 pr-9 focus:outline-none',
active ? 'bg-indigo-600 text-white' : 'text-gray-900',
]"
>
<img :src="person.img" class="h-6 w-6 overflow-hidden rounded-full" />
<span :class="['block truncate', selected ? 'font-semibold' : 'font-normal']">
{{ person.name }}
</span>
<span
v-if="active"
:class="[
'absolute inset-y-0 right-0 flex items-center pr-4',
active ? 'text-white' : 'text-indigo-600',
]"
>
<svg class="h-5 w-5" viewBox="0 0 25 24" fill="none">
<path
d="M11.25 8.75L14.75 12L11.25 15.25"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
</div>
</ComboboxOption>
</template>
</ComboboxOptions>
<div v-if="people.length === 0" class="w-full py-4 text-center">
No person selected
</div>
<div v-else-if="activeOption" class="border-l">
<div class="flex flex-col">
<div class="p-8 text-center">
<img
:src="activeOption?.img"
class="mb-4 inline-block h-16 w-16 overflow-hidden rounded-full"
/>
<div class="font-bold text-gray-900">{{ activeOption.name }}</div>
<div class="text-gray-700">Obviously cool person</div>
</div>
</div>
</div>
</div>
</div>
</Combobox>
</div>
</div>
</div>
</template>
@@ -0,0 +1,146 @@
<script>
import { ref, defineComponent, computed } from 'vue'
import {
Combobox,
ComboboxButton,
ComboboxInput,
ComboboxLabel,
ComboboxOption,
ComboboxOptions,
} from '@headlessui/vue'
let everybody = [
{ id: 1, img: 'https://github.com/adamwathan.png', name: 'Adam Wathan' },
{ id: 2, img: 'https://github.com/sschoger.png', name: 'Steve Schoger' },
{ id: 3, img: 'https://github.com/bradlc.png', name: 'Brad Cornes' },
{ id: 4, img: 'https://github.com/simonswiss.png', name: 'Simon Vrachliotis' },
{ id: 5, img: 'https://github.com/robinmalfait.png', name: 'Robin Malfait' },
{
id: 6,
img: 'https://pbs.twimg.com/profile_images/1478879681491394569/eV2PyCnm_400x400.jpg',
name: 'James McDonald',
},
{ id: 7, img: 'https://github.com/reinink.png', name: 'Jonathan Reinink' },
{ id: 8, img: 'https://github.com/thecrypticace.png', name: 'Jordan Pittman' },
]
export default defineComponent({
components: {
Combobox,
ComboboxButton,
ComboboxInput,
ComboboxLabel,
ComboboxOption,
ComboboxOptions,
},
setup() {
let query = ref('')
let activePerson = ref(everybody[2])
// Choose a random person on mount
activePerson.value = everybody[Math.floor(Math.random() * everybody.length)]
let people = computed(() => {
return query.value === ''
? everybody
: everybody.filter((person) =>
person.name.toLowerCase().includes(query.value.toLowerCase())
)
})
return {
query,
activePerson,
people,
displayValue: (item) => item?.name,
}
},
})
</script>
<template>
<div class="flex h-full w-screen justify-center bg-gray-50 p-12">
<div class="mx-auto w-full max-w-lg">
<div class="space-y-1">
<Combobox
as="div"
v-model="activePerson"
class="w-full overflow-hidden rounded border border-black/5 bg-white bg-clip-padding shadow-sm"
v-slot="{ activeOption, open }"
>
<div class="flex w-full flex-col">
<ComboboxInput
@change="query = $event.target.value"
class="w-full rounded-none border-none px-3 py-1 outline-none"
placeholder="Search users…"
:displayValue="displayValue"
/>
<div
:class="[
'flex border-t',
activePerson && !open ? 'border-transparent' : 'border-gray-200',
]"
>
<ComboboxOptions
class="shadow-xs max-h-60 flex-1 overflow-auto py-1 text-base leading-6 focus:outline-none sm:text-sm sm:leading-5"
>
<ComboboxOption
v-for="person in people"
:key="person.id"
:value="person"
v-slot="{ active, selected }"
>
<div
:class="[
'relative flex cursor-default select-none space-x-4 py-2 pl-3 pr-9 focus:outline-none',
active ? 'bg-indigo-600 text-white' : 'text-gray-900',
]"
>
<img :src="person.img" class="h-6 w-6 overflow-hidden rounded-full" />
<span :class="['block truncate', selected ? 'font-semibold' : 'font-normal']">
{{ person.name }}
</span>
<span
v-if="active"
:class="[
'absolute inset-y-0 right-0 flex items-center pr-4',
active ? 'text-white' : 'text-indigo-600',
]"
>
<svg class="h-5 w-5" viewBox="0 0 25 24" fill="none">
<path
d="M11.25 8.75L14.75 12L11.25 15.25"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
</div>
</ComboboxOption>
</ComboboxOptions>
<div v-if="people.length === 0" class="w-full py-4 text-center">
No person selected
</div>
<div v-else-if="activeOption" class="border-l">
<div class="flex flex-col">
<div class="p-8 text-center">
<img
:src="activeOption.img"
class="mb-4 inline-block h-16 w-16 overflow-hidden rounded-full"
/>
<div class="font-bold text-gray-900">{{ activeOption.name }}</div>
<div class="text-gray-700">Obviously cool person</div>
</div>
</div>
</div>
</div>
</div>
</Combobox>
</div>
</div>
</div>
</template>
@@ -0,0 +1,158 @@
<template>
<div class="flex h-full w-screen justify-center space-x-4 bg-gray-50 p-12">
<div class="w-full max-w-4xl">
<div class="space-y-1">
<form @submit="onSubmit">
<Combobox v-model="activePersons" name="people" multiple>
<ComboboxLabel class="block text-sm font-medium leading-5 text-gray-700">
Assigned to
</ComboboxLabel>
<div class="relative">
<span class="inline-block w-full rounded-md shadow-sm">
<div
class="relative w-full cursor-default rounded-md border border-gray-300 bg-white py-2 pl-2 pr-10 text-left transition duration-150 ease-in-out focus-within:border-blue-700 focus-within:outline-none focus-within:ring-1 focus-within:ring-blue-700 sm:text-sm sm:leading-5"
>
<span class="block flex flex-wrap gap-2">
<span v-if="activePersons.length === 0" class="p-0.5">Empty</span>
<span
v-for="person in activePersons"
:key="person.id"
class="flex items-center gap-1 rounded bg-blue-50 px-2 py-0.5"
>
<span>{{ person.name }}</span>
<svg
class="h-4 w-4 cursor-pointer"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
@click.stop.prevent="removePerson(person)"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</span>
<ComboboxInput
@change="query = $event.target.value"
@focus="query = ''"
class="border-none p-0 focus:ring-0"
placeholder="Search..."
/>
</span>
<ComboboxButton class="absolute inset-y-0 right-0 flex items-center pr-2">
<svg
class="h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
>
<path
d="M7 7l3-3 3 3m0 6l-3 3-3-3"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</ComboboxButton>
</div>
</span>
<div class="absolute mt-1 w-full rounded-md bg-white shadow-lg">
<ComboboxOptions
class="shadow-xs max-h-60 overflow-auto rounded-md py-1 text-base leading-6 focus:outline-none sm:text-sm sm:leading-5"
>
<ComboboxOption
v-for="person in people.filter((person) =>
person.name.toLowerCase().includes(query.toLowerCase())
)"
:key="person.id"
:value="person"
as="template"
v-slot="{ active, selected }"
>
<li
class="relative cursor-default select-none py-2 pl-3 pr-9 focus:outline-none"
:class="active ? 'bg-indigo-600 text-white' : 'text-gray-900'"
>
<span
class="block truncate"
:class="{ 'font-semibold': selected, 'font-normal': !selected }"
>
{{ person.name }}
</span>
<span
v-if="selected"
class="absolute inset-y-0 right-0 flex items-center pr-4"
:class="{ 'text-white': active, 'text-indigo-600': !active }"
>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</span>
</li>
</ComboboxOption>
</ComboboxOptions>
</div>
</div>
</Combobox>
<button
class="mt-2 inline-flex items-center rounded border border-gray-300 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
Submit
</button>
</form>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import {
Combobox,
ComboboxLabel,
ComboboxInput,
ComboboxButton,
ComboboxOptions,
ComboboxOption,
} from '@headlessui/vue'
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
let query = ref('')
let people = [
{ id: 1, name: 'Wade Cooper' },
{ id: 2, name: 'Arlene Mccoy' },
{ id: 3, name: 'Devon Webb' },
{ id: 4, name: 'Tom Cook' },
{ id: 5, name: 'Tanya Fox' },
{ id: 6, name: 'Hellen Schmidt' },
{ id: 7, name: 'Caroline Schultz' },
{ id: 8, name: 'Mason Heaney' },
{ id: 9, name: 'Claudie Smitham' },
{ id: 10, name: 'Emil Schaefer' },
]
let activePersons = ref([people[2], people[3]])
function onSubmit(e) {
e.preventDefault()
console.log([...new FormData(e.currentTarget).entries()])
}
function removePerson(person) {
activePersons.value = activePersons.value.filter((p) => p !== person)
}
</script>
@@ -0,0 +1,276 @@
<template>
<div className="flex gap-4 p-12">
<Button @click="toggleIsOpen()">Toggle!</Button>
<Button @click="nested = true">Show nested</Button>
</div>
<Nested v-if="nested" @close="nested = false" />
<TransitionRoot :show="isOpen" as="template">
<Dialog @close="setIsOpen">
<div class="fixed inset-0 z-10 overflow-y-auto">
<div
class="flex min-h-screen items-end justify-center px-4 pb-20 pt-4 text-center sm:block sm:p-0"
>
<TransitionChild
as="template"
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-75"
leave="ease-in duration-200"
leaveFrom="opacity-75"
leaveTo="opacity-0"
entered="opacity-75"
>
<div className="fixed inset-0 bg-gray-500 transition-opacity" />
</TransitionChild>
<TransitionChild
enter="ease-out transform duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in transform duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<!-- This element is to trick the browser into centering the modal contents. -->
<span class="hidden sm:inline-block sm:h-screen sm:align-middle" aria-hidden="true">
&#8203;
</span>
<DialogPanel
class="inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle"
>
<div class="bg-white px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div
class="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10"
>
<!-- Heroicon name: exclamation -->
<svg
class="h-6 w-6 text-red-600"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
</div>
<div class="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
<DialogTitle as="h3" class="text-lg font-medium leading-6 text-gray-900">
Deactivate account
</DialogTitle>
<div class="mt-2">
<p class="text-sm text-gray-500">
Are you sure you want to deactivate your account? All of your data will be
permanently removed. This action cannot be undone.
</p>
<div class="relative mt-10 inline-block text-left">
<Menu>
<MenuButton
ref="trigger"
class="ui-focus-visible:ring-2 ui-focus-visible:ring-offset-2 flex items-center rounded-md border border-gray-300 bg-white px-2 py-1 ring-gray-500 ring-offset-gray-100 focus:outline-none"
>
<span>Choose a reason</span>
<svg class="-mr-1 ml-2 h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</MenuButton>
<TransitionRoot
enter="transition duration-300 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
>
<Portal>
<MenuItems
ref="container"
class="z-20 mt-2 w-56 origin-top-right divide-y divide-gray-100 rounded-md border border-gray-200 bg-white shadow-lg outline-none"
>
<div class="px-4 py-3">
<p class="text-sm leading-5">Signed in as</p>
<p class="truncate text-sm font-medium leading-5 text-gray-900">
tom@example.com
</p>
</div>
<div class="py-1">
<MenuItem
as="a"
href="#account-settings"
:className="resolveClass"
>
Account settings
</MenuItem>
<MenuItem as="a" href="#support" :className="resolveClass">
Support
</MenuItem>
<MenuItem
as="a"
disabled
href="#new-feature"
:className="resolveClass"
>
New feature (soon)
</MenuItem>
<MenuItem as="a" href="#license" :className="resolveClass">
License
</MenuItem>
</div>
<div class="py-1">
<MenuItem as="a" href="#sign-out" :className="resolveClass">
Sign out
</MenuItem>
</div>
</MenuItems>
</Portal>
</TransitionRoot>
</Menu>
</div>
<Flatpickr v-model="date" />
</div>
</div>
</div>
</div>
<div class="bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6">
<Button type="button" @click="setIsOpen(false)"> Deactivate </Button>
<Button @click="setIsOpen(false)"> Cancel </Button>
</div>
</DialogPanel>
</TransitionChild>
</div>
</div>
</Dialog>
</TransitionRoot>
</template>
<script>
import { ref, defineComponent, h } from 'vue'
import {
Dialog,
DialogTitle,
DialogOverlay,
DialogPanel,
Menu,
MenuButton,
MenuItems,
MenuItem,
Portal,
TransitionRoot,
TransitionChild,
} from '@headlessui/vue'
import Flatpickr from 'vue-flatpickr-component'
import { usePopper } from '../../playground-utils/hooks/use-popper'
import Button from '../Button.vue'
import 'flatpickr/dist/themes/light.css'
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
function resolveClass({ active, disabled }) {
return classNames(
'flex justify-between w-full px-4 py-2 text-sm leading-5 text-left',
active ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
disabled && 'cursor-not-allowed opacity-50'
)
}
let Nested = defineComponent({
components: { Dialog, DialogOverlay },
emits: ['close'],
props: ['level'],
setup(props, { emit }) {
let showChild = ref(false)
function onClose() {
emit('close', false)
}
return () => {
let level = props.level ?? 0
return h(Dialog, { open: true, onClose, class: 'fixed inset-0 z-10' }, () => [
h(DialogOverlay, { class: 'fixed inset-0 bg-gray-500 opacity-25' }),
h(
'div',
{
class: 'fixed left-12 top-24 z-10 w-96 bg-white p-4',
style: { transform: `translate(calc(50px * ${level}), calc(50px * ${level}))` },
},
[
h('p', `Level: ${level}`),
h('div', { class: 'flex gap-4' }, [
h(Button, { onClick: () => (showChild.value = true) }, () => `Open ${level + 1} a`),
h(Button, { onClick: () => (showChild.value = true) }, () => `Open ${level + 1} b`),
h(Button, { onClick: () => (showChild.value = true) }, () => `Open ${level + 1} c`),
]),
]
),
showChild.value &&
h(Nested, {
onClose: () => (showChild.value = false),
level: level + 1,
}),
])
}
},
})
export default {
components: {
Button,
Nested,
Dialog,
DialogTitle,
DialogOverlay,
DialogPanel,
Menu,
MenuButton,
MenuItems,
MenuItem,
Portal,
Flatpickr,
TransitionRoot,
TransitionChild,
},
setup() {
let isOpen = ref(false)
let [trigger, container] = usePopper({
placement: 'bottom-end',
strategy: 'fixed',
modifiers: [{ name: 'offset', options: { offset: [0, 10] } }],
})
let nested = ref(false)
let date = ref(new Date())
return {
Button,
nested,
date,
isOpen,
trigger,
container,
setIsOpen(value) {
isOpen.value = value
},
toggleIsOpen() {
isOpen.value = !isOpen.value
},
resolveClass,
}
},
}
</script>
@@ -0,0 +1,112 @@
<template>
<TransitionRoot as="template" :show="open">
<Dialog as="div" class="relative z-10" @close="open = false">
<TransitionChild
as="template"
enter="ease-in-out duration-500"
enter-from="opacity-0"
enter-to="opacity-100"
leave="ease-in-out duration-500"
leave-from="opacity-100"
leave-to="opacity-0"
>
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</TransitionChild>
<div class="fixed inset-0 overflow-hidden">
<div class="absolute inset-0 overflow-hidden">
<div class="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10">
<TransitionChild
as="template"
enter="transform transition ease-in-out duration-500 sm:duration-700"
enter-from="translate-x-full"
enter-to="translate-x-0"
leave="transform transition ease-in-out duration-500 sm:duration-700"
leave-from="translate-x-0"
leave-to="translate-x-full"
>
<DialogPanel class="pointer-events-auto w-screen max-w-md">
<div class="flex h-full flex-col overflow-y-scroll bg-white shadow-xl">
<div class="flex-1 overflow-y-auto px-4 py-6 sm:px-6">
<div class="flex items-start justify-between">
<DialogTitle class="text-lg font-medium text-gray-900">Title...</DialogTitle>
<div class="ml-3 flex h-7 items-center">
<button
type="button"
class="-m-2 p-2 text-gray-400 hover:text-gray-500"
@click="open = false"
>
<span class="sr-only">Close panel</span>
<XIcon class="h-6 w-6" aria-hidden="true" />
</button>
</div>
</div>
</div>
</div>
</DialogPanel>
</TransitionChild>
</div>
</div>
</div>
</Dialog>
</TransitionRoot>
</template>
<script>
import { ref, watchEffect } from 'vue'
import { Dialog, DialogPanel, DialogTitle, TransitionChild, TransitionRoot } from '@headlessui/vue'
import { XIcon } from '@heroicons/vue/outline'
const products = [
{
id: 1,
name: 'Throwback Hip Bag',
href: '#',
color: 'Salmon',
price: '$90.00',
quantity: 1,
imageSrc: 'https://tailwindui.com/img/ecommerce-images/shopping-cart-page-04-product-01.jpg',
imageAlt:
'Salmon orange fabric pouch with match zipper, gray zipper pull, and adjustable hip belt.',
},
{
id: 2,
name: 'Medium Stuff Satchel',
href: '#',
color: 'Blue',
price: '$32.00',
quantity: 1,
imageSrc: 'https://tailwindui.com/img/ecommerce-images/shopping-cart-page-04-product-02.jpg',
imageAlt:
'Front of satchel with blue canvas body, black straps and handle, drawstring top, and front zipper pouch.',
},
// More products...
]
export default {
components: {
Dialog,
DialogPanel,
DialogTitle,
TransitionChild,
TransitionRoot,
XIcon,
},
setup() {
const open = ref(true)
watchEffect(() => {
if (open.value === false) {
setTimeout(() => {
open.value = true
}, 1000)
}
})
return {
products,
open,
}
},
}
</script>
@@ -0,0 +1,33 @@
<template>
<div class="flex h-full w-screen justify-center bg-gray-50 p-12">
<div class="mx-auto w-full max-w-xs">
<Disclosure>
<DisclosureButton>Trigger</DisclosureButton>
<TransitionRoot
enter="transition duration-1000 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-1000 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
>
<DisclosurePanel class="mt-4 bg-white p-4">Content</DisclosurePanel>
</TransitionRoot>
</Disclosure>
</div>
</div>
</template>
<script>
import { Disclosure, DisclosureButton, DisclosurePanel, TransitionRoot } from '@headlessui/vue'
export default {
components: {
Disclosure,
DisclosureButton,
DisclosurePanel,
TransitionRoot,
},
}
</script>
@@ -0,0 +1,27 @@
<template>
<div class="flex h-full w-screen justify-center bg-gray-50 p-12">
<div class="mx-auto w-full max-w-xs">
<button>Previous</button>
<FocusTrap>
<button>Trigger</button>
</FocusTrap>
<button>After</button>
</div>
</div>
</template>
<script>
import { defineComponent, h, ref, onMounted, watchEffect, watch } from 'vue'
import { FocusTrap } from '@headlessui/vue'
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
export default {
components: { FocusTrap },
setup(props, context) {
return {}
},
}
</script>
@@ -0,0 +1,109 @@
<template>
<div class="flex h-full w-screen justify-center bg-gray-50 p-12">
<div class="mx-auto w-full max-w-xs">
<div class="space-y-1">
<Listbox v-model="active">
<ListboxLabel class="block text-sm font-medium leading-5 text-gray-700"
>Assigned to</ListboxLabel
>
<div class="relative">
<span class="inline-block w-full rounded-md shadow-sm">
<ListboxButton
class="focus:shadow-outline-blue relative w-full cursor-default rounded-md border border-gray-300 bg-white py-2 pl-3 pr-10 text-left transition duration-150 ease-in-out focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5"
>
<span class="block truncate">{{ active.name }}</span>
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<svg
class="h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
>
<path
d="M7 7l3-3 3 3m0 6l-3 3-3-3"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
</ListboxButton>
</span>
<div class="absolute mt-1 w-full rounded-md bg-white shadow-lg">
<ListboxOptions
class="shadow-xs max-h-60 overflow-auto rounded-md py-1 text-base leading-6 focus:outline-none sm:text-sm sm:leading-5"
>
<ListboxOption
v-for="person in people"
:key="person.id"
:value="person"
as="template"
:disabled="person.disabled"
>
<li
class="ui-active:text-white ui-active:bg-indigo-600 ui-not-active:text-gray-900 ui-disabled:bg-gray-50 ui-disabled:text-gray-300 relative cursor-default select-none py-2 pl-3 pr-9 focus:outline-none"
>
<span
class="ui-selected:font-semibold ui-not-selected:font-normal block truncate"
>
{{ person.name }}
</span>
<span
class="ui-selected:flex ui-not-selected:hidden ui-active:text-white ui-not-active:text-indigo-600 absolute inset-y-0 right-0 items-center pr-4"
>
<svg class="h-5 w-5" viewbox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</span>
</li>
</ListboxOption>
</ListboxOptions>
</div>
</div>
</Listbox>
</div>
</div>
</div>
</template>
<script>
import { ref } from 'vue'
import {
Listbox,
ListboxLabel,
ListboxButton,
ListboxOptions,
ListboxOption,
} from '@headlessui/vue'
export default {
components: { Listbox, ListboxLabel, ListboxButton, ListboxOptions, ListboxOption },
setup(props, context) {
let people = [
{ id: 1, name: 'Wade Cooper' },
{ id: 2, name: 'Arlene Mccoy' },
{ id: 3, name: 'Devon Webb' },
{ id: 4, name: 'Tom Cook' },
{ id: 5, name: 'Tanya Fox', disabled: true },
{ id: 6, name: 'Hellen Schmidt' },
{ id: 7, name: 'Caroline Schultz' },
{ id: 8, name: 'Mason Heaney' },
{ id: 9, name: 'Claudie Smitham' },
{ id: 10, name: 'Emil Schaefer' },
]
let active = ref(people[Math.floor(Math.random() * people.length)])
return {
people,
active,
}
},
}
</script>
@@ -0,0 +1,149 @@
<template>
<div class="flex h-full w-screen justify-center space-x-4 bg-gray-50 p-12">
<div class="w-full max-w-4xl">
<div class="space-y-1">
<form @submit="onSubmit">
<Listbox v-model="activePersons" name="people" multiple>
<ListboxLabel class="block text-sm font-medium leading-5 text-gray-700">
Assigned to
</ListboxLabel>
<div class="relative">
<span class="inline-block w-full rounded-md shadow-sm">
<ListboxButton
class="focus:shadow-outline-blue relative w-full cursor-default rounded-md border border-gray-300 bg-white py-2 pl-2 pr-10 text-left transition duration-150 ease-in-out focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5"
>
<span class="block flex flex-wrap gap-2">
<span v-if="activePersons.length === 0" class="p-0.5">Empty</span>
<span
v-for="person in activePersons"
:key="person.id"
class="flex items-center gap-1 rounded bg-blue-50 px-2 py-0.5"
>
<span>{{ person.name }}</span>
<svg
class="h-4 w-4 cursor-pointer"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
@click.stop.prevent="removePerson(person)"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</span>
</span>
<span
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
>
<svg
class="h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
>
<path
d="M7 7l3-3 3 3m0 6l-3 3-3-3"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</span>
</ListboxButton>
</span>
<div class="absolute mt-1 w-full rounded-md bg-white shadow-lg">
<ListboxOptions
class="shadow-xs max-h-60 overflow-auto rounded-md py-1 text-base leading-6 focus:outline-none sm:text-sm sm:leading-5"
>
<ListboxOption
v-for="person in people"
:key="person.id"
:value="person"
as="template"
v-slot="{ active, selected }"
>
<li
class="relative cursor-default select-none py-2 pl-3 pr-9 focus:outline-none"
:class="active ? 'bg-indigo-600 text-white' : 'text-gray-900'"
>
<span
class="block truncate"
:class="{ 'font-semibold': selected, 'font-normal': !selected }"
>
{{ person.name }}
</span>
<span
v-if="selected"
class="absolute inset-y-0 right-0 flex items-center pr-4"
:class="{ 'text-white': active, 'text-indigo-600': !active }"
>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</span>
</li>
</ListboxOption>
</ListboxOptions>
</div>
</div>
</Listbox>
<button
class="mt-2 inline-flex items-center rounded border border-gray-300 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
Submit
</button>
</form>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import {
Listbox,
ListboxLabel,
ListboxButton,
ListboxOptions,
ListboxOption,
} from '@headlessui/vue'
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
let people = [
{ id: 1, name: 'Wade Cooper' },
{ id: 2, name: 'Arlene Mccoy' },
{ id: 3, name: 'Devon Webb' },
{ id: 4, name: 'Tom Cook' },
{ id: 5, name: 'Tanya Fox' },
{ id: 6, name: 'Hellen Schmidt' },
{ id: 7, name: 'Caroline Schultz' },
{ id: 8, name: 'Mason Heaney' },
{ id: 9, name: 'Claudie Smitham' },
{ id: 10, name: 'Emil Schaefer' },
]
let activePersons = ref([people[2], people[3]])
function onSubmit(e) {
e.preventDefault()
console.log([...new FormData(e.currentTarget).entries()])
}
function removePerson(person) {
activePersons.value = activePersons.value.filter((p) => p !== person)
}
</script>
@@ -0,0 +1,208 @@
<template>
<div class="flex h-full w-screen justify-center space-x-4 bg-gray-50 p-12">
<div class="w-64">
<div class="space-y-1">
<Listbox v-model="active">
<ListboxLabel class="block text-sm font-medium leading-5 text-gray-700"
>Assigned to</ListboxLabel
>
<div class="relative">
<span class="inline-block w-full rounded-md shadow-sm">
<ListboxButton
class="focus:shadow-outline-blue relative w-full cursor-default rounded-md border border-gray-300 bg-white py-2 pl-3 pr-10 text-left transition duration-150 ease-in-out focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5"
>
<span class="block truncate">{{ active.name }}</span>
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<svg
class="h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
>
<path
d="M7 7l3-3 3 3m0 6l-3 3-3-3"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
</ListboxButton>
</span>
<div class="absolute mt-1 w-full rounded-md bg-white shadow-lg">
<ListboxOptions
class="shadow-xs max-h-60 overflow-auto rounded-md py-1 text-base leading-6 focus:outline-none sm:text-sm sm:leading-5"
>
<ListboxOption
v-for="person in people"
:key="person.id"
:value="person"
:className="resolveListboxOptionClassName"
v-slot="{ active, selected }"
>
<span
:class="
classNames('block truncate', selected ? 'font-semibold' : 'font-normal')
"
>
{{ person.name }}
</span>
<span
v-if="selected"
:class="
classNames(
'absolute inset-y-0 right-0 flex items-center pr-4',
active ? 'text-white' : 'text-indigo-600'
)
"
>
<svg class="h-5 w-5" viewbox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</span>
</ListboxOption>
</ListboxOptions>
</div>
</div>
</Listbox>
</div>
</div>
<div>
<label for="email" class="block text-sm font-medium leading-5 text-gray-700"> Email </label>
<div class="relative mt-1 rounded-md shadow-sm">
<input
class="form-input block w-full sm:text-sm sm:leading-5"
placeholder="you@example.com"
/>
</div>
</div>
<div class="w-64">
<div class="space-y-1">
<Listbox v-model="active">
<ListboxLabel class="block text-sm font-medium leading-5 text-gray-700"
>Assigned to</ListboxLabel
>
<div class="relative">
<span class="inline-block w-full rounded-md shadow-sm">
<ListboxButton
class="focus:shadow-outline-blue relative w-full cursor-default rounded-md border border-gray-300 bg-white py-2 pl-3 pr-10 text-left transition duration-150 ease-in-out focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5"
>
<span class="block truncate">{{ active.name }}</span>
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<svg
class="h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
>
<path
d="M7 7l3-3 3 3m0 6l-3 3-3-3"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
</ListboxButton>
</span>
<div class="absolute mt-1 w-full rounded-md bg-white shadow-lg">
<ListboxOptions
class="shadow-xs max-h-60 overflow-auto rounded-md py-1 text-base leading-6 focus:outline-none sm:text-sm sm:leading-5"
>
<ListboxOption
v-for="person in people"
:key="person.id"
:value="person"
:className="resolveListboxOptionClassName"
v-slot="{ active, selected }"
>
<span
:class="
classNames('block truncate', selected ? 'font-semibold' : 'font-normal')
"
>
{{ person.name }}
</span>
<span
v-if="selected"
:class="
classNames(
'absolute inset-y-0 right-0 flex items-center pr-4',
active ? 'text-white' : 'text-indigo-600'
)
"
>
<svg class="h-5 w-5" viewbox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</span>
</ListboxOption>
</ListboxOptions>
</div>
</div>
</Listbox>
</div>
</div>
</div>
</template>
<script>
import { defineComponent, h, ref, onMounted, watchEffect, watch } from 'vue'
import {
Listbox,
ListboxLabel,
ListboxButton,
ListboxOptions,
ListboxOption,
} from '@headlessui/vue'
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
export default {
components: { Listbox, ListboxLabel, ListboxButton, ListboxOptions, ListboxOption },
setup(props, context) {
let people = [
{ id: 1, name: 'Wade Cooper' },
{ id: 2, name: 'Arlene Mccoy' },
{ id: 3, name: 'Devon Webb' },
{ id: 4, name: 'Tom Cook' },
{ id: 5, name: 'Tanya Fox' },
{ id: 6, name: 'Hellen Schmidt' },
{ id: 7, name: 'Caroline Schultz' },
{ id: 8, name: 'Mason Heaney' },
{ id: 9, name: 'Claudie Smitham' },
{ id: 10, name: 'Emil Schaefer' },
]
let active = ref(people[Math.floor(Math.random() * people.length)])
return {
people,
active,
classNames,
resolveListboxOptionClassName({ active }) {
return classNames(
'relative py-2 pl-3 cursor-default select-none pr-9 focus:outline-none',
active ? 'text-white bg-indigo-600' : 'text-gray-900'
)
},
}
},
}
</script>
@@ -0,0 +1,95 @@
<template>
<div class="flex h-full w-screen justify-center bg-gray-50 p-12">
<div class="mt-64 inline-block text-left">
<Menu>
<span class="inline-flex rounded-md shadow-sm">
<MenuButton
ref="reference"
class="focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none active:bg-gray-50 active:text-gray-800"
>
<span>Options</span>
<svg class="-mr-1 ml-2 h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</MenuButton>
</span>
<teleport to="body">
<MenuItems
ref="floating"
:style="floatingStyles"
class="absolute right-0 w-56 origin-top-right divide-y divide-gray-100 rounded-md border border-gray-200 bg-white shadow-lg outline-none"
>
<div class="px-4 py-3">
<p class="text-sm leading-5">Signed in as</p>
<p class="truncate text-sm font-medium leading-5 text-gray-900">tom@example.com</p>
</div>
<div class="py-1">
<MenuItem as="a" href="#account-settings" :className="resolveClass">
Account settings
</MenuItem>
<MenuItem v-slot="data">
<a href="#support" :class="resolveClass(data)">Support</a>
</MenuItem>
<MenuItem as="a" disabled href="#new-feature" :className="resolveClass">
New feature (soon)
</MenuItem>
<MenuItem as="a" href="#license" :className="resolveClass">License</MenuItem>
</div>
<div class="py-1">
<MenuItem as="a" href="#sign-out" :className="resolveClass">Sign out</MenuItem>
</div>
</MenuItems>
</teleport>
</Menu>
</div>
</div>
</template>
<script>
import { defineComponent, h, ref, onMounted, watchEffect, watch, computed } from 'vue'
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
import { useFloating, offset } from '@floating-ui/vue'
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
export default {
components: { Menu, MenuButton, MenuItems, MenuItem },
setup(props, context) {
let reference = ref(null)
let floating = ref(null)
let { floatingStyles } = useFloating(
computed(() => reference.value?.el),
computed(() => floating.value?.el),
{
placement: 'bottom-end',
strategy: 'fixed',
middleware: [offset(10)],
}
)
function resolveClass({ active, disabled }) {
return classNames(
'flex justify-between w-full px-4 py-2 text-sm leading-5 text-left',
active ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
disabled && 'cursor-not-allowed opacity-50'
)
}
return {
reference,
floating,
floatingStyles,
resolveClass,
}
},
}
</script>
@@ -0,0 +1,86 @@
<template>
<div class="flex h-full w-screen justify-center bg-gray-50 p-12">
<div class="mt-64 inline-block text-left">
<Menu>
<span class="inline-flex rounded-md shadow-sm">
<MenuButton
ref="trigger"
class="focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none active:bg-gray-50 active:text-gray-800"
>
<span>Options</span>
<svg class="-mr-1 ml-2 h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</MenuButton>
</span>
<teleport to="body">
<MenuItems
ref="container"
class="absolute right-0 w-56 origin-top-right divide-y divide-gray-100 rounded-md border border-gray-200 bg-white shadow-lg outline-none"
>
<div class="px-4 py-3">
<p class="text-sm leading-5">Signed in as</p>
<p class="truncate text-sm font-medium leading-5 text-gray-900">tom@example.com</p>
</div>
<div class="py-1">
<MenuItem as="a" href="#account-settings" :className="resolveClass">
Account settings
</MenuItem>
<MenuItem v-slot="data">
<a href="#support" :class="resolveClass(data)">Support</a>
</MenuItem>
<MenuItem as="a" disabled href="#new-feature" :className="resolveClass">
New feature (soon)
</MenuItem>
<MenuItem as="a" href="#license" :className="resolveClass">License</MenuItem>
</div>
<div class="py-1">
<MenuItem as="a" href="#sign-out" :className="resolveClass">Sign out</MenuItem>
</div>
</MenuItems>
</teleport>
</Menu>
</div>
</div>
</template>
<script>
import { defineComponent, h, ref, onMounted, watchEffect, watch } from 'vue'
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
import { usePopper } from '../../playground-utils/hooks/use-popper'
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
export default {
components: { Menu, MenuButton, MenuItems, MenuItem },
setup(props, context) {
let [trigger, container] = usePopper({
placement: 'bottom-end',
strategy: 'fixed',
modifiers: [{ name: 'offset', options: { offset: [0, 10] } }],
})
function resolveClass({ active, disabled }) {
return classNames(
'flex justify-between w-full px-4 py-2 text-sm leading-5 text-left',
active ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
disabled && 'cursor-not-allowed opacity-50'
)
}
return {
trigger,
container,
resolveClass,
}
},
}
</script>
@@ -0,0 +1,94 @@
<template>
<div class="flex h-full w-screen justify-center bg-gray-50 p-12">
<div class="relative mt-64 inline-block text-left">
<Menu>
<span class="inline-flex rounded-md shadow-sm">
<MenuButton
ref="trigger"
class="focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none active:bg-gray-50 active:text-gray-800"
>
<span>Options</span>
<svg class="-mr-1 ml-2 h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</MenuButton>
</span>
<div ref="container" class="w-56">
<transition
enter-active-class="transition duration-100 ease-out"
enter-from-class="transform scale-95 opacity-0"
enter-to-class="transform scale-100 opacity-100"
leave-active-class="transition duration-75 ease-out"
leave-from-class="transform scale-100 opacity-100"
leave-to-class="transform scale-95 opacity-0"
>
<MenuItems
class="w-full divide-y divide-gray-100 rounded-md border border-gray-200 bg-white shadow-lg outline-none"
>
<div class="px-4 py-3">
<p class="text-sm leading-5">Signed in as</p>
<p class="truncate text-sm font-medium leading-5 text-gray-900">tom@example.com</p>
</div>
<div class="py-1">
<MenuItem as="a" :className="resolveClass" href="#account-settings">
Account settings
</MenuItem>
<MenuItem v-slot="data">
<a href="#support" :class="resolveClass(data)">Support</a>
</MenuItem>
<MenuItem as="a" :className="resolveClass" disabled href="#new-feature">
New feature (soon)
</MenuItem>
<MenuItem as="a" :className="resolveClass" href="#license">License</MenuItem>
</div>
<div class="py-1">
<MenuItem as="a" :className="resolveClass" href="#sign-out">Sign out</MenuItem>
</div>
</MenuItems>
</transition>
</div>
</Menu>
</div>
</div>
</template>
<script>
import { defineComponent, h, ref, onMounted, watchEffect, watch } from 'vue'
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
import { usePopper } from '../../playground-utils/hooks/use-popper'
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
export default {
components: { Menu, MenuButton, MenuItems, MenuItem },
setup(props, context) {
let [trigger, container] = usePopper({
placement: 'bottom-end',
strategy: 'fixed',
modifiers: [{ name: 'offset', options: { offset: [0, 10] } }],
})
function resolveClass({ active, disabled }) {
return classNames(
'flex justify-between w-full px-4 py-2 text-sm leading-5 text-left',
active ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
disabled && 'cursor-not-allowed opacity-50'
)
}
return {
trigger,
container,
resolveClass,
}
},
}
</script>
@@ -0,0 +1,83 @@
<template>
<div class="flex h-full w-screen justify-center bg-gray-50 p-12">
<div class="relative inline-block text-left">
<Menu>
<span class="rounded-md shadow-sm">
<MenuButton
class="focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none active:bg-gray-50 active:text-gray-800"
>
<span>Options</span>
<svg class="-mr-1 ml-2 h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</MenuButton>
</span>
<transition
enter-active-class="transition duration-100 ease-out"
enter-from-class="transform scale-95 opacity-0"
enter-to-class="transform scale-100 opacity-100"
leave-active-class="transition duration-75 ease-out"
leave-from-class="transform scale-100 opacity-100"
leave-to-class="transform scale-95 opacity-0"
>
<MenuItems
class="absolute right-0 mt-2 w-56 origin-top-right divide-y divide-gray-100 rounded-md border border-gray-200 bg-white shadow-lg outline-none"
>
<div class="px-4 py-3">
<p class="text-sm leading-5">Signed in as</p>
<p class="truncate text-sm font-medium leading-5 text-gray-900">tom@example.com</p>
</div>
<div class="py-1">
<MenuItem as="a" href="#account-settings" :className="resolveClass">
Account settings
</MenuItem>
<MenuItem as="a" href="#support" :className="resolveClass">Support</MenuItem>
<MenuItem as="a" disabled href="#new-feature" :className="resolveClass">
New feature (soon)
</MenuItem>
<MenuItem as="a" href="#license" :className="resolveClass">License</MenuItem>
</div>
<div class="py-1">
<MenuItem as="a" href="#sign-out" :className="resolveClass">Sign out</MenuItem>
</div>
</MenuItems>
</transition>
</Menu>
</div>
</div>
</template>
<script>
import { defineComponent, h } from 'vue'
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
export default {
components: {
Menu,
MenuButton,
MenuItems,
MenuItem,
},
setup() {
return {
resolveClass({ active, disabled }) {
return classNames(
'flex justify-between w-full px-4 py-2 text-sm leading-5 text-left',
active ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
disabled && 'cursor-not-allowed opacity-50'
)
},
}
},
}
</script>
@@ -0,0 +1,84 @@
<template>
<div class="flex h-full w-screen justify-center bg-gray-50 p-12">
<div class="relative inline-block text-left">
<Menu>
<span class="rounded-md shadow-sm">
<MenuButton
class="focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none active:bg-gray-50 active:text-gray-800"
>
<span>Options</span>
<svg class="-mr-1 ml-2 h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</MenuButton>
</span>
<MenuItems
class="absolute right-0 mt-2 w-56 origin-top-right divide-y divide-gray-100 rounded-md border border-gray-200 bg-white shadow-lg outline-none"
>
<div class="px-4 py-3">
<p class="text-sm leading-5">Signed in as</p>
<p class="truncate text-sm font-medium leading-5 text-gray-900">tom@example.com</p>
</div>
<div class="py-1">
<CustomMenuItem href="#account-settings">Account settings</CustomMenuItem>
<CustomMenuItem href="#support">Support</CustomMenuItem>
<CustomMenuItem disabled href="#new-feature">New feature (soon)</CustomMenuItem>
<CustomMenuItem href="#license">License</CustomMenuItem>
</div>
<div class="py-1">
<CustomMenuItem href="#sign-out">Sign out</CustomMenuItem>
</div>
</MenuItems>
</Menu>
</div>
</div>
</template>
<script>
import { defineComponent, h, ref, watchEffect } from 'vue'
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
let CustomMenuItem = defineComponent({
components: { Menu, MenuButton, MenuItems, MenuItem },
setup(props, { slots }) {
return () => {
return h(MenuItem, ({ active, disabled }) => {
return h(
'a',
{
class: classNames(
'flex justify-between w-full text-left px-4 py-2 text-sm leading-5',
active ? 'bg-indigo-500 text-white' : 'text-gray-700',
disabled && 'cursor-not-allowed opacity-50'
),
},
[
h('span', { class: classNames(active && 'font-bold') }, slots.default()),
h('kbd', { class: classNames('font-sans', active && 'text-indigo-50') }, '⌘K'),
]
)
})
}
},
})
export default {
components: {
Menu,
MenuButton,
MenuItems,
MenuItem,
CustomMenuItem,
},
}
</script>
@@ -0,0 +1,125 @@
<template>
<div class="flex h-full w-screen justify-center space-x-4 bg-gray-50 p-12">
<div class="relative inline-block text-left">
<Menu>
<span class="rounded-md shadow-sm">
<MenuButton
class="focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none active:bg-gray-50 active:text-gray-800"
>
<span>Options</span>
<svg class="-mr-1 ml-2 h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</MenuButton>
</span>
<MenuItems
class="absolute right-0 mt-2 w-56 origin-top-right divide-y divide-gray-100 rounded-md border border-gray-200 bg-white shadow-lg outline-none"
>
<div class="px-4 py-3">
<p class="text-sm leading-5">Signed in as</p>
<p class="truncate text-sm font-medium leading-5 text-gray-900">tom@example.com</p>
</div>
<div class="py-1">
<MenuItem as="a" :className="resolveClass" href="#account-settings"
>Account settings</MenuItem
>
<MenuItem as="a" :className="resolveClass" href="#support">Support</MenuItem>
<MenuItem as="a" :className="resolveClass" disabled href="#new-feature"
>New feature (soon)</MenuItem
>
<MenuItem as="a" :className="resolveClass" href="#license">License</MenuItem>
</div>
<div class="py-1">
<MenuItem as="a" :className="resolveClass" href="#sign-out">Sign out</MenuItem>
</div>
</MenuItems>
</Menu>
</div>
<div>
<div class="relative rounded-md shadow-sm">
<input
class="form-input block w-full sm:text-sm sm:leading-5"
placeholder="you@example.com"
/>
</div>
</div>
<div class="relative inline-block text-left">
<Menu>
<span class="rounded-md shadow-sm">
<MenuButton
class="focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none active:bg-gray-50 active:text-gray-800"
>
<span>Options</span>
<svg class="-mr-1 ml-2 h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</MenuButton>
</span>
<MenuItems
class="absolute right-0 mt-2 w-56 origin-top-right divide-y divide-gray-100 rounded-md border border-gray-200 bg-white shadow-lg outline-none"
>
<div class="px-4 py-3">
<p class="text-sm leading-5">Signed in as</p>
<p class="truncate text-sm font-medium leading-5 text-gray-900">tom@example.com</p>
</div>
<div class="py-1">
<MenuItem as="a" :className="resolveClass" href="#account-settings"
>Account settings</MenuItem
>
<MenuItem as="a" :className="resolveClass" href="#support">Support</MenuItem>
<MenuItem as="a" :className="resolveClass" disabled href="#new-feature"
>New feature (soon)</MenuItem
>
<MenuItem as="a" :className="resolveClass" href="#license">License</MenuItem>
</div>
<div class="py-1">
<MenuItem as="a" :className="resolveClass" href="#sign-out">Sign out</MenuItem>
</div>
</MenuItems>
</Menu>
</div>
</div>
</template>
<script>
import { defineComponent, h } from 'vue'
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
export default {
components: {
Menu,
MenuButton,
MenuItems,
MenuItem,
},
setup() {
return {
resolveClass({ active, disabled }) {
return classNames(
'block w-full text-left px-4 py-2 text-sm leading-5 text-gray-700',
active && 'bg-gray-100 text-gray-900',
disabled && 'cursor-not-allowed opacity-50'
)
},
}
},
}
</script>
@@ -0,0 +1,144 @@
<template>
<div class="flex items-center justify-center space-x-12 p-12">
<button>Previous</button>
<PopoverGroup as="nav" ar-label="Mythical University" class="flex space-x-3">
<Popover as="div" class="relative">
<transition
enter-active-class="transition ease-out duration-300 transform"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="transition ease-in duration-300 transform"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<PopoverOverlay class="fixed inset-0 z-20 bg-gray-500 bg-opacity-75"></PopoverOverlay>
</transition>
<PopoverButton
class="relative z-30 border-2 border-transparent bg-gray-300 px-3 py-2 focus:border-blue-900 focus:outline-none"
>Normal</PopoverButton
>
<PopoverPanel class="absolute z-30 flex w-64 flex-col border-2 border-blue-900 bg-gray-100">
<button
v-for="(link, i) of links"
:key="i"
:hidden="i === 2"
class="border-2 border-transparent px-3 py-2 text-left hover:bg-gray-200 focus:border-blue-900 focus:bg-gray-200 focus:outline-none"
>
Normal - {{ link }}
</button>
</PopoverPanel>
</Popover>
<Popover as="div" class="relative">
<PopoverButton
class="border-2 border-transparent bg-gray-300 px-3 py-2 focus:border-blue-900 focus:outline-none"
>Focus</PopoverButton
>
<PopoverPanel
focus
class="absolute flex w-64 flex-col border-2 border-blue-900 bg-gray-100"
>
<button
v-for="(link, i) of links"
:key="i"
class="border-2 border-transparent px-3 py-2 text-left hover:bg-gray-200 focus:border-blue-900 focus:bg-gray-200 focus:outline-none"
>
Focus - {{ link }}
</button>
</PopoverPanel>
</Popover>
<Popover as="div" class="relative">
<PopoverButton
ref="trigger1"
class="border-2 border-transparent bg-gray-300 px-3 py-2 focus:border-blue-900 focus:outline-none"
>Portal</PopoverButton
>
<Portal>
<PopoverPanel
ref="container1"
class="flex w-64 flex-col border-2 border-blue-900 bg-gray-100"
>
<button
v-for="(link, i) of links"
:key="i"
class="border-2 border-transparent px-3 py-2 text-left hover:bg-gray-200 focus:border-blue-900 focus:bg-gray-200 focus:outline-none"
>
Portal - {{ link }}
</button>
</PopoverPanel>
</Portal>
</Popover>
<Popover as="div" class="relative">
<PopoverButton
ref="trigger2"
class="border-2 border-transparent bg-gray-300 px-3 py-2 focus:border-blue-900 focus:outline-none"
>Focus in portal</PopoverButton
>
<Portal>
<PopoverPanel
ref="container2"
focus
class="flex w-64 flex-col border-2 border-blue-900 bg-gray-100"
>
<button
v-for="(link, i) of links"
:key="i"
class="border-2 border-transparent px-3 py-2 text-left hover:bg-gray-200 focus:border-blue-900 focus:bg-gray-200 focus:outline-none"
>
Focus in Portal - {{ link }}
</button>
</PopoverPanel>
</Portal>
</Popover>
</PopoverGroup>
<button>Next</button>
</div>
</template>
<script>
import { defineComponent } from 'vue'
import {
Popover,
PopoverOverlay,
PopoverPanel,
PopoverGroup,
PopoverButton,
Portal,
} from '@headlessui/vue'
import { usePopper } from '../../playground-utils/hooks/use-popper'
function html(templates) {
return templates.join('')
}
export default {
components: {
Popover,
PopoverPanel,
PopoverOverlay,
PopoverGroup,
PopoverButton,
Portal,
},
setup() {
let links = ['First', 'Second', 'Third', 'Fourth']
let [trigger1, container1] = usePopper({
placement: 'bottom-start',
strategy: 'fixed',
})
let [trigger2, container2] = usePopper({
placement: 'bottom-start',
strategy: 'fixed',
})
return { links, trigger1, container1, trigger2, container2 }
},
}
</script>
@@ -0,0 +1,35 @@
<template>
<div class="flex h-full w-screen justify-center bg-gray-50 p-12">
<div class="mx-auto w-full max-w-xs">
<main>
<aside ref="container" id="group-1">A</aside>
<PortalGroup :target="container">
<section id="group-2">
<span>B</span>
</section>
<Portal>Next to A</Portal>
</PortalGroup>
<Portal>I am in the portal root</Portal>
</main>
</div>
</div>
</template>
<script>
import { defineComponent, h, ref, onMounted, watchEffect, watch } from 'vue'
import { Portal, PortalGroup } from '@headlessui/vue'
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
export default {
components: { Portal, PortalGroup },
setup(props, context) {
let container = ref(null)
return { container }
},
}
</script>
@@ -0,0 +1,104 @@
<template>
<div class="max-w-xl p-12">
<a href="/">Link before</a>
<RadioGroup v-model="active">
<fieldset class="space-y-4">
<legend>
<h2 class="text-xl">Privacy setting</h2>
</legend>
<div class="-space-y-px rounded-md bg-white">
<RadioGroupOption
v-for="({ id, name, description }, i) in access"
:key="id"
:value="id"
v-slot="{ active, checked }"
:className="
({ active }) =>
classNames(
// Rounded corners
i === 0 && 'rounded-tl-md rounded-tr-md',
access.length - 1 === i && 'rounded-bl-md rounded-br-md',
// Shared
'relative border p-4 flex focus:outline-none',
active ? 'bg-indigo-50 border-indigo-200 z-10' : 'border-gray-200'
)
"
>
<div class="flex w-full items-center justify-between">
<div class="ml-3 flex cursor-pointer flex-col">
<span
:class="[
'block text-sm font-medium leading-5',
active ? 'text-indigo-900' : 'text-gray-900',
]"
>
{{ name }}
</span>
<span
:class="['block text-sm leading-5', active ? 'text-indigo-700' : 'text-gray-500']"
>
{{ description }}
</span>
</div>
<div>
<svg
v-if="checked"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
class="h-5 w-5 text-indigo-500"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
</div>
</RadioGroupOption>
</div>
</fieldset>
</RadioGroup>
<a href="/">Link after</a>
</div>
</template>
<script>
import { ref } from 'vue'
import { RadioGroup, RadioGroupOption } from '@headlessui/vue'
function classNames(...classes) {
return classes.filter(Boolean)
}
export default {
components: { RadioGroup, RadioGroupOption },
setup() {
let active = ref()
let access = ref([
{
id: 'access-1',
name: 'Public access',
description: 'This project would be available to anyone who has the link',
},
{
id: 'access-2',
name: 'Private to Project Members',
description: 'Only members of this project would be able to access',
},
{
id: 'access-3',
name: 'Private to you',
description: 'You are the only one able to access this project',
},
])
return { active, access, classNames }
},
}
</script>
@@ -0,0 +1,45 @@
<template>
<div class="flex h-full w-screen items-start justify-center bg-gray-50 p-12">
<SwitchGroup as="div" class="flex items-center space-x-4">
<SwitchLabel>Enable notifications</SwitchLabel>
<Switch
as="button"
v-model="state"
:class="resolveSwitchClass({ checked: state })"
v-slot="{ checked }"
>
<span
class="inline-block h-5 w-5 transform rounded-full bg-white transition duration-200 ease-in-out"
:class="{ 'translate-x-5': checked, 'translate-x-0': !checked }"
/>
</Switch>
</SwitchGroup>
</div>
</template>
<script>
import { ref } from 'vue'
import { SwitchGroup, Switch, SwitchLabel } from '@headlessui/vue'
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
export default {
components: { SwitchGroup, Switch, SwitchLabel },
setup() {
let state = ref(false)
return {
state,
resolveSwitchClass({ checked }) {
return classNames(
'focus:shadow-outline relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none',
checked ? 'bg-indigo-600 hover:bg-indigo-800' : 'bg-gray-200 hover:bg-gray-400'
)
},
}
},
}
</script>
@@ -0,0 +1,43 @@
<template>
<div class="flex h-full w-screen flex-col items-start space-y-12 bg-gray-50 p-12">
<TabGroup class="flex w-full max-w-3xl flex-col" as="div">
<TabList class="relative z-0 flex divide-x divide-gray-200 rounded-lg shadow">
<Tab
v-for="tab in tabs"
:key="tab.name"
class="group relative min-w-0 flex-1 overflow-hidden bg-white px-4 py-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10"
v-slot="{ selected }"
>
<span>{{ tab.name }}</span>
<small v-if="tab.disabled" class="inline-block px-4 text-xs">(disabled)</small>
<span
aria-hidden="true"
class="absolute inset-x-0 bottom-0 h-0.5"
:class="{ 'bg-indigo-500': selected, 'bg-transparent': !selected }"
/>
</Tab>
</TabList>
<TabPanels class="mt-4">
<TabPanel v-for="tab in tabs" class="rounded-lg bg-white p-4 shadow" :key="tab.name">
{{ tab.content }}
</TabPanel>
</TabPanels>
</TabGroup>
</div>
</template>
<script setup>
import { TabGroup, TabList, Tab, TabPanels, TabPanel } from '@headlessui/vue'
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
let tabs = [
{ name: 'My Account', content: 'Tab content for my account' },
{ name: 'Company', content: 'Tab content for company' },
{ name: 'Team Members', content: 'Tab content for team members' },
{ name: 'Billing', content: 'Tab content for billing' },
]
</script>
@@ -0,0 +1,90 @@
<template>
<div class="flex h-full w-screen flex-col items-start space-y-12 bg-gray-50 p-12">
<SwitchGroup as="div" class="flex items-center space-x-4">
<SwitchLabel>Manual keyboard activation</SwitchLabel>
<Switch as="button" v-model="manual" :className="resolveSwitchClass" v-slot="{ checked }">
<span
class="inline-block h-5 w-5 transform rounded-full bg-white transition duration-200 ease-in-out"
:class="{ 'translate-x-5': checked, 'translate-x-0': !checked }"
/>
</Switch>
</SwitchGroup>
<TabGroup class="flex w-full max-w-3xl flex-col" as="div" :manual="manual">
<TabList class="relative z-0 flex divide-x divide-gray-200 rounded-lg shadow">
<Tab
v-for="(tab, tabIdx) in tabs"
:key="tab.name"
:disabled="tab.disabled"
class="group relative min-w-0 flex-1 overflow-hidden bg-white px-4 py-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10"
:class="{
'text-gray-900': selected,
'text-gray-500 hover:text-gray-700': !selected,
'rounded-l-lg': tabIdx === 0,
'rounded-r-lg': tabIdx === tabs.length - 1,
'opacity-50': tab.disabled,
}"
v-slot="{ selected }"
>
<span>{{ tab.name }}</span>
<small v-if="tab.disabled" class="inline-block px-4 text-xs">(disabled)</small>
<span
aria-hidden="true"
class="absolute inset-x-0 bottom-0 h-0.5"
:class="{ 'bg-indigo-500': selected, 'bg-transparent': !selected }"
/>
</Tab>
</TabList>
<TabPanels class="mt-4">
<TabPanel v-for="tab in tabs" class="rounded-lg bg-white p-4 shadow" key="tab.name">
{{ tab.content }}
</TabPanel>
</TabPanels>
</TabGroup>
</div>
</template>
<script>
import { defineComponent, h, ref, onMounted, watchEffect, watch } from 'vue'
import {
SwitchGroup,
Switch,
SwitchLabel,
TabGroup,
TabList,
Tab,
TabPanels,
TabPanel,
} from '@headlessui/vue'
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
let tabs = [
{ name: 'My Account', content: 'Tab content for my account' },
{ name: 'Company', content: 'Tab content for company', disabled: true },
{ name: 'Team Members', content: 'Tab content for team members' },
{ name: 'Billing', content: 'Tab content for billing' },
]
export default {
components: { SwitchGroup, Switch, SwitchLabel, TabGroup, TabList, Tab, TabPanels, TabPanel },
setup(props, context) {
let manual = ref(false)
return {
tabs: ref(tabs),
manual,
resolveSwitchClass({ checked }) {
return classNames(
'relative inline-flex flex-shrink-0 h-6 transition-colors duration-200 ease-in-out border-2 border-transparent rounded-full cursor-pointer w-11 focus:outline-none focus:shadow-outline',
checked ? 'bg-indigo-600' : 'bg-gray-200'
)
},
}
},
}
</script>
+253
View File
@@ -0,0 +1,253 @@
export let countries = [
'Afghanistan',
'Albania',
'Algeria',
'American Samoa',
'Andorra',
'Angola',
'Anguilla',
'Antarctica',
'Antigua and Barbuda',
'Argentina',
'Armenia',
'Aruba',
'Australia',
'Austria',
'Azerbaijan',
'Bahamas (the)',
'Bahrain',
'Bangladesh',
'Barbados',
'Belarus',
'Belgium',
'Belize',
'Benin',
'Bermuda',
'Bhutan',
'Bolivia (Plurinational State of)',
'Bonaire, Sint Eustatius and Saba',
'Bosnia and Herzegovina',
'Botswana',
'Bouvet Island',
'Brazil',
'British Indian Ocean Territory (the)',
'Brunei Darussalam',
'Bulgaria',
'Burkina Faso',
'Burundi',
'Cabo Verde',
'Cambodia',
'Cameroon',
'Canada',
'Cayman Islands (the)',
'Central African Republic (the)',
'Chad',
'Chile',
'China',
'Christmas Island',
'Cocos (Keeling) Islands (the)',
'Colombia',
'Comoros (the)',
'Congo (the Democratic Republic of the)',
'Congo (the)',
'Cook Islands (the)',
'Costa Rica',
'Croatia',
'Cuba',
'Curaçao',
'Cyprus',
'Czechia',
"Côte d'Ivoire",
'Denmark',
'Djibouti',
'Dominica',
'Dominican Republic (the)',
'Ecuador',
'Egypt',
'El Salvador',
'Equatorial Guinea',
'Eritrea',
'Estonia',
'Eswatini',
'Ethiopia',
'Falkland Islands (the) [Malvinas]',
'Faroe Islands (the)',
'Fiji',
'Finland',
'France',
'French Guiana',
'French Polynesia',
'French Southern Territories (the)',
'Gabon',
'Gambia (the)',
'Georgia',
'Germany',
'Ghana',
'Gibraltar',
'Greece',
'Greenland',
'Grenada',
'Guadeloupe',
'Guam',
'Guatemala',
'Guernsey',
'Guinea',
'Guinea-Bissau',
'Guyana',
'Haiti',
'Heard Island and McDonald Islands',
'Holy See (the)',
'Honduras',
'Hong Kong',
'Hungary',
'Iceland',
'India',
'Indonesia',
'Iran (Islamic Republic of)',
'Iraq',
'Ireland',
'Isle of Man',
'Israel',
'Italy',
'Jamaica',
'Japan',
'Jersey',
'Jordan',
'Kazakhstan',
'Kenya',
'Kiribati',
"Korea (the Democratic People's Republic of)",
'Korea (the Republic of)',
'Kuwait',
'Kyrgyzstan',
"Lao People's Democratic Republic (the)",
'Latvia',
'Lebanon',
'Lesotho',
'Liberia',
'Libya',
'Liechtenstein',
'Lithuania',
'Luxembourg',
'Macao',
'Madagascar',
'Malawi',
'Malaysia',
'Maldives',
'Mali',
'Malta',
'Marshall Islands (the)',
'Martinique',
'Mauritania',
'Mauritius',
'Mayotte',
'Mexico',
'Micronesia (Federated States of)',
'Moldova (the Republic of)',
'Monaco',
'Mongolia',
'Montenegro',
'Montserrat',
'Morocco',
'Mozambique',
'Myanmar',
'Namibia',
'Nauru',
'Nepal',
'Netherlands (the)',
'New Caledonia',
'New Zealand',
'Nicaragua',
'Niger (the)',
'Nigeria',
'Niue',
'Norfolk Island',
'Northern Mariana Islands (the)',
'Norway',
'Oman',
'Pakistan',
'Palau',
'Palestine, State of',
'Panama',
'Papua New Guinea',
'Paraguay',
'Peru',
'Philippines (the)',
'Pitcairn',
'Poland',
'Portugal',
'Puerto Rico',
'Qatar',
'Republic of North Macedonia',
'Romania',
'Russian Federation (the)',
'Rwanda',
'Réunion',
'Saint Barthélemy',
'Saint Helena, Ascension and Tristan da Cunha',
'Saint Kitts and Nevis',
'Saint Lucia',
'Saint Martin (French part)',
'Saint Pierre and Miquelon',
'Saint Vincent and the Grenadines',
'Samoa',
'San Marino',
'Sao Tome and Principe',
'Saudi Arabia',
'Senegal',
'Serbia',
'Seychelles',
'Sierra Leone',
'Singapore',
'Sint Maarten (Dutch part)',
'Slovakia',
'Slovenia',
'Solomon Islands',
'Somalia',
'South Africa',
'South Georgia and the South Sandwich Islands',
'South Sudan',
'Spain',
'Sri Lanka',
'Sudan (the)',
'Suriname',
'Svalbard and Jan Mayen',
'Sweden',
'Switzerland',
'Syrian Arab Republic',
'Taiwan',
'Tajikistan',
'Tanzania, United Republic of',
'Thailand',
'Timor-Leste',
'Togo',
'Tokelau',
'Tonga',
'Trinidad and Tobago',
'Tunisia',
'Turkey',
'Turkmenistan',
'Turks and Caicos Islands (the)',
'Tuvalu',
'Uganda',
'Ukraine',
'United Arab Emirates (the)',
'United Kingdom of Great Britain and Northern Ireland (the)',
'United States Minor Outlying Islands (the)',
'United States of America (the)',
'Uruguay',
'Uzbekistan',
'Vanuatu',
'Venezuela (Bolivarian Republic of)',
'Viet Nam',
'Virgin Islands (British)',
'Virgin Islands (U.S.)',
'Wallis and Futuna',
'Western Sahara',
'Yemen',
'Zambia',
'Zimbabwe',
'Åland Islands',
]
export let timezones: string[] = Intl.supportedValuesOf('timeZone')
+8
View File
@@ -0,0 +1,8 @@
import { createApp } from 'vue'
// @ts-expect-error TODO: Properly handle this
import App from './App.vue'
import router from './router'
import 'tailwindcss/tailwind.css'
createApp(App).use(router).mount('#app')
@@ -0,0 +1,26 @@
import { createPopper } from '@popperjs/core'
import { onMounted, ref, watchEffect } from 'vue'
export function usePopper(options) {
let reference = ref(null)
let popper = ref(null)
onMounted(() => {
watchEffect((onInvalidate) => {
if (!popper.value) return
if (!reference.value) return
let popperEl = popper.value.el || popper.value
let referenceEl = reference.value.el || reference.value
if (!(referenceEl instanceof HTMLElement)) return
if (!(popperEl instanceof HTMLElement)) return
let { destroy } = createPopper(referenceEl, popperEl, options)
onInvalidate(destroy)
})
})
return [reference, popper]
}

Some files were not shown because too many files have changed in this diff Show More