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:
@@ -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}
|
||||
/>
|
||||
))
|
||||
@@ -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')
|
||||
Vendored
+5
@@ -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.
|
||||
@@ -0,0 +1,7 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
module.exports = {
|
||||
reactStrictMode: false,
|
||||
devIndicators: {
|
||||
buildActivity: false,
|
||||
},
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
>
|
||||
​
|
||||
</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"
|
||||
>
|
||||
​
|
||||
</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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>​
|
||||
<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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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]
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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">
|
||||
​
|
||||
</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>
|
||||
@@ -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')
|
||||
@@ -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
Reference in New Issue
Block a user