lint: fix linting and build

This commit is contained in:
Yehoshua Sandler 2025-04-28 21:18:15 -05:00
parent f291efcc18
commit ebe686b47e
22 changed files with 137 additions and 490 deletions

View File

@ -13,6 +13,7 @@ const eslintConfig = [
...compat.extends('next/core-web-vitals', 'next/typescript'), ...compat.extends('next/core-web-vitals', 'next/typescript'),
{ {
rules: { rules: {
'@next/next/no-img-element': 'off',
'@typescript-eslint/ban-ts-comment': 'warn', '@typescript-eslint/ban-ts-comment': 'warn',
'@typescript-eslint/no-empty-object-type': 'warn', '@typescript-eslint/no-empty-object-type': 'warn',
'@typescript-eslint/no-explicit-any': 'warn', '@typescript-eslint/no-explicit-any': 'warn',

View File

@ -5,8 +5,7 @@ import { redirect } from 'next/navigation'
import { LoginForm } from '@/components/login-form' import { LoginForm } from '@/components/login-form'
import Image from 'next/image' import Image from 'next/image'
type Props = {} const LoginPage = async () => {
const LoginPage = async (props: Props) => {
const payload = await getPayload({ config: configPromise }) const payload = await getPayload({ config: configPromise })
const headers = await nextHeaders() const headers = await nextHeaders()
const userResult = await payload.auth({ headers }) const userResult = await payload.auth({ headers })

View File

@ -4,7 +4,7 @@ import config from '@/payload.config'
import React from 'react' import React from 'react'
import UserFeed from '@/components/Feed/UserFeed' import UserFeed from '@/components/Feed/UserFeed'
import { Book, HoldRequest, Repository } from '@/payload-types' import { Book, Repository } from '@/payload-types'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { TextShimmer } from '@/components/ui/text-shimmer' import { TextShimmer } from '@/components/ui/text-shimmer'
import { LoginForm } from '@/components/login-form' import { LoginForm } from '@/components/login-form'

View File

@ -1,4 +1,4 @@
import type { Book, Copy, HoldRequest, Repository, User } from '@/payload-types' import type { Book, HoldRequest, Repository, User } from '@/payload-types'
import { getPayload, PaginatedDocs } from 'payload' import { getPayload, PaginatedDocs } from 'payload'
import config from '@/payload.config' import config from '@/payload.config'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '../ui/card' import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '../ui/card'

View File

@ -13,7 +13,7 @@ const HoldRequestNotifications = (props: Props) => {
const holdRequestsByRepoElements = repos?.docs.map((r) => { const holdRequestsByRepoElements = repos?.docs.map((r) => {
return ( return (
<ul role="list" className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3"> <ul key={r.id} role="list" className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{r.holdRequests?.docs?.map((h) => { {r.holdRequests?.docs?.map((h) => {
const hold = h as HoldRequest const hold = h as HoldRequest
const book = hold.book as Book const book = hold.book as Book

View File

@ -51,7 +51,7 @@ const SearchBooksInlineForm = (props: Props) => {
try { try {
const searchResults = await searchBooks(values) const searchResults = await searchBooks(values)
if (searchResults && onSearchResult) onSearchResult(searchResults) if (searchResults && onSearchResult) onSearchResult(searchResults)
} catch (err) { } catch (_) {
toast('There was an issue with that search') toast('There was an issue with that search')
} finally { } finally {
setIsSearching(false) setIsSearching(false)

View File

@ -58,7 +58,7 @@ export default function SiteNavigation(props: { children: React.ReactNode }) {
setUser(userRequest.user) setUser(userRequest.user)
console.log(userRequest.user) console.log(userRequest.user)
}) })
}, [user?.id]) }, [user, setUser])
const profilePicture = user?.profilePicture as Media | undefined const profilePicture = user?.profilePicture as Media | undefined
const initials = user?.firstName const initials = user?.firstName

View File

@ -30,7 +30,9 @@ export function Avatar({
'inline-grid shrink-0 align-middle [--avatar-radius:20%] *:col-start-1 *:row-start-1', 'inline-grid shrink-0 align-middle [--avatar-radius:20%] *:col-start-1 *:row-start-1',
'outline -outline-offset-1 outline-black/10 dark:outline-white/10', 'outline -outline-offset-1 outline-black/10 dark:outline-white/10',
// Border radius // Border radius
square ? 'rounded-(--avatar-radius) *:rounded-(--avatar-radius)' : 'rounded-full *:rounded-full' square
? 'rounded-(--avatar-radius) *:rounded-(--avatar-radius)'
: 'rounded-full *:rounded-full',
)} )}
> >
{initials && ( {initials && (
@ -40,7 +42,14 @@ export function Avatar({
aria-hidden={alt ? undefined : 'true'} aria-hidden={alt ? undefined : 'true'}
> >
{alt && <title>{alt}</title>} {alt && <title>{alt}</title>}
<text x="50%" y="50%" alignmentBaseline="middle" dominantBaseline="middle" textAnchor="middle" dy=".125em"> <text
x="50%"
y="50%"
alignmentBaseline="middle"
dominantBaseline="middle"
textAnchor="middle"
dy=".125em"
>
{initials} {initials}
</text> </text>
</svg> </svg>
@ -59,13 +68,16 @@ export const AvatarButton = forwardRef(function AvatarButton(
className, className,
...props ...props
}: AvatarProps & }: AvatarProps &
(Omit<Headless.ButtonProps, 'as' | 'className'> | Omit<React.ComponentPropsWithoutRef<typeof Link>, 'className'>), (
ref: React.ForwardedRef<HTMLElement> | Omit<Headless.ButtonProps, 'as' | 'className'>
| Omit<React.ComponentPropsWithoutRef<typeof Link>, 'className'>
),
ref: React.ForwardedRef<HTMLElement>,
) { ) {
let classes = clsx( const classes = clsx(
className, className,
square ? 'rounded-[20%]' : 'rounded-full', square ? 'rounded-[20%]' : 'rounded-full',
'relative inline-grid focus:outline-hidden data-focus:outline-2 data-focus:outline-offset-2 data-focus:outline-blue-500' 'relative inline-grid focus:outline-hidden data-focus:outline-2 data-focus:outline-offset-2 data-focus:outline-blue-500',
) )
return 'href' in props ? ( return 'href' in props ? (

View File

@ -36,14 +36,18 @@ const colors = {
type BadgeProps = { color?: keyof typeof colors } type BadgeProps = { color?: keyof typeof colors }
export function Badge({ color = 'zinc', className, ...props }: BadgeProps & React.ComponentPropsWithoutRef<'span'>) { export function Badge({
color = 'zinc',
className,
...props
}: BadgeProps & React.ComponentPropsWithoutRef<'span'>) {
return ( return (
<span <span
{...props} {...props}
className={clsx( className={clsx(
className, className,
'inline-flex items-center gap-x-1.5 rounded-md px-1.5 py-0.5 text-sm/5 font-medium sm:text-xs/5 forced-colors:outline', 'inline-flex items-center gap-x-1.5 rounded-md px-1.5 py-0.5 text-sm/5 font-medium sm:text-xs/5 forced-colors:outline',
colors[color] colors[color],
)} )}
/> />
) )
@ -59,11 +63,11 @@ export const BadgeButton = forwardRef(function BadgeButton(
| Omit<Headless.ButtonProps, 'as' | 'className'> | Omit<Headless.ButtonProps, 'as' | 'className'>
| Omit<React.ComponentPropsWithoutRef<typeof Link>, 'className'> | Omit<React.ComponentPropsWithoutRef<typeof Link>, 'className'>
), ),
ref: React.ForwardedRef<HTMLElement> ref: React.ForwardedRef<HTMLElement>,
) { ) {
let classes = clsx( const classes = clsx(
className, className,
'group relative inline-flex rounded-md focus:outline-hidden data-focus:outline-2 data-focus:outline-offset-2 data-focus:outline-blue-500' 'group relative inline-flex rounded-md focus:outline-hidden data-focus:outline-2 data-focus:outline-offset-2 data-focus:outline-blue-500',
) )
return 'href' in props ? ( return 'href' in props ? (

View File

@ -169,12 +169,16 @@ type ButtonProps = (
export const Button = forwardRef(function Button( export const Button = forwardRef(function Button(
{ color, outline, plain, className, children, ...props }: ButtonProps, { color, outline, plain, className, children, ...props }: ButtonProps,
ref: React.ForwardedRef<HTMLElement> ref: React.ForwardedRef<HTMLElement>,
) { ) {
let classes = clsx( const classes = clsx(
className, className,
styles.base, styles.base,
outline ? styles.outline : plain ? styles.plain : clsx(styles.solid, styles.colors[color ?? 'dark/zinc']) outline
? styles.outline
: plain
? styles.plain
: clsx(styles.solid, styles.colors[color ?? 'dark/zinc']),
) )
return 'href' in props ? ( return 'href' in props ? (

View File

@ -24,18 +24,27 @@ export function Combobox<T>({
autoFocus?: boolean autoFocus?: boolean
'aria-label'?: string 'aria-label'?: string
children: (value: NonNullable<T>) => React.ReactElement children: (value: NonNullable<T>) => React.ReactElement
} & Omit<Headless.ComboboxProps<T, false>, 'as' | 'multiple' | 'children'> & { anchor?: 'top' | 'bottom' }) { } & Omit<Headless.ComboboxProps<T, false>, 'as' | 'multiple' | 'children'> & {
anchor?: 'top' | 'bottom'
}) {
const [query, setQuery] = useState('') const [query, setQuery] = useState('')
const filteredOptions = const filteredOptions =
query === '' query === ''
? options ? options
: options.filter((option) => : options.filter((option) =>
filter ? filter(option, query) : displayValue(option)?.toLowerCase().includes(query.toLowerCase()) filter
? filter(option, query)
: displayValue(option)?.toLowerCase().includes(query.toLowerCase()),
) )
return ( return (
<Headless.Combobox {...props} multiple={false} virtual={{ options: filteredOptions }} onClose={() => setQuery('')}> <Headless.Combobox
{...props}
multiple={false}
virtual={{ options: filteredOptions }}
onClose={() => setQuery('')}
>
<span <span
data-slot="control" data-slot="control"
className={clsx([ className={clsx([
@ -90,8 +99,18 @@ export function Combobox<T>({
aria-hidden="true" aria-hidden="true"
fill="none" fill="none"
> >
<path d="M5.75 10.75L8 13L10.25 10.75" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" /> <path
<path d="M10.25 5.25L8 3L5.75 5.25" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" /> d="M5.75 10.75L8 13L10.25 10.75"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M10.25 5.25L8 3L5.75 5.25"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg> </svg>
</Headless.ComboboxButton> </Headless.ComboboxButton>
</span> </span>
@ -112,7 +131,7 @@ export function Combobox<T>({
// Shadows // Shadows
'shadow-lg ring-1 ring-zinc-950/10 dark:ring-white/10 dark:ring-inset', 'shadow-lg ring-1 ring-zinc-950/10 dark:ring-white/10 dark:ring-inset',
// Transitions // Transitions
'transition-opacity duration-100 ease-in data-closed:data-leave:opacity-0 data-transition:pointer-events-none' 'transition-opacity duration-100 ease-in data-closed:data-leave:opacity-0 data-transition:pointer-events-none',
)} )}
> >
{({ option }) => children(option)} {({ option }) => children(option)}
@ -129,7 +148,7 @@ export function ComboboxOption<T>({
Headless.ComboboxOptionProps<'div', T>, Headless.ComboboxOptionProps<'div', T>,
'as' | 'className' 'as' | 'className'
>) { >) {
let sharedClasses = clsx( const sharedClasses = clsx(
// Base // Base
'flex min-w-0 items-center', 'flex min-w-0 items-center',
// Icons // Icons
@ -137,7 +156,7 @@ export function ComboboxOption<T>({
'*:data-[slot=icon]:text-zinc-500 group-data-focus/option:*:data-[slot=icon]:text-white dark:*:data-[slot=icon]:text-zinc-400', '*:data-[slot=icon]:text-zinc-500 group-data-focus/option:*:data-[slot=icon]:text-white dark:*:data-[slot=icon]:text-zinc-400',
'forced-colors:*:data-[slot=icon]:text-[CanvasText] forced-colors:group-data-focus/option:*:data-[slot=icon]:text-[Canvas]', 'forced-colors:*:data-[slot=icon]:text-[CanvasText] forced-colors:group-data-focus/option:*:data-[slot=icon]:text-[Canvas]',
// Avatars // Avatars
'*:data-[slot=avatar]:-mx-0.5 *:data-[slot=avatar]:size-6 sm:*:data-[slot=avatar]:size-5' '*:data-[slot=avatar]:-mx-0.5 *:data-[slot=avatar]:size-6 sm:*:data-[slot=avatar]:size-5',
) )
return ( return (
@ -153,7 +172,7 @@ export function ComboboxOption<T>({
// Forced colors mode // Forced colors mode
'forced-color-adjust-none forced-colors:data-focus:bg-[Highlight] forced-colors:data-focus:text-[HighlightText]', 'forced-color-adjust-none forced-colors:data-focus:bg-[Highlight] forced-colors:data-focus:text-[HighlightText]',
// Disabled // Disabled
'data-disabled:opacity-50' 'data-disabled:opacity-50',
)} )}
> >
<span className={clsx(className, sharedClasses)}>{children}</span> <span className={clsx(className, sharedClasses)}>{children}</span>
@ -170,16 +189,25 @@ export function ComboboxOption<T>({
} }
export function ComboboxLabel({ className, ...props }: React.ComponentPropsWithoutRef<'span'>) { export function ComboboxLabel({ className, ...props }: React.ComponentPropsWithoutRef<'span'>) {
return <span {...props} className={clsx(className, 'ml-2.5 truncate first:ml-0 sm:ml-2 sm:first:ml-0')} /> return (
<span
{...props}
className={clsx(className, 'ml-2.5 truncate first:ml-0 sm:ml-2 sm:first:ml-0')}
/>
)
} }
export function ComboboxDescription({ className, children, ...props }: React.ComponentPropsWithoutRef<'span'>) { export function ComboboxDescription({
className,
children,
...props
}: React.ComponentPropsWithoutRef<'span'>) {
return ( return (
<span <span
{...props} {...props}
className={clsx( className={clsx(
className, className,
'flex flex-1 overflow-hidden text-zinc-500 group-data-focus/option:text-white before:w-2 before:min-w-0 before:shrink dark:text-zinc-400' 'flex flex-1 overflow-hidden text-zinc-500 group-data-focus/option:text-white before:w-2 before:min-w-0 before:shrink dark:text-zinc-400',
)} )}
> >
<span className="flex-1 truncate">{children}</span> <span className="flex-1 truncate">{children}</span>

View File

@ -44,7 +44,7 @@ export function DropdownMenu({
// Define grid at the menu level if subgrid is supported // Define grid at the menu level if subgrid is supported
'supports-[grid-template-columns:subgrid]:grid supports-[grid-template-columns:subgrid]:grid-cols-[auto_1fr_1.5rem_0.5rem_auto]', 'supports-[grid-template-columns:subgrid]:grid supports-[grid-template-columns:subgrid]:grid-cols-[auto_1fr_1.5rem_0.5rem_auto]',
// Transitions // Transitions
'transition data-leave:duration-100 data-leave:ease-in data-closed:data-leave:opacity-0' 'transition data-leave:duration-100 data-leave:ease-in data-closed:data-leave:opacity-0',
)} )}
/> />
) )
@ -57,7 +57,7 @@ export function DropdownItem({
| Omit<Headless.MenuItemProps<'button'>, 'as' | 'className'> | Omit<Headless.MenuItemProps<'button'>, 'as' | 'className'>
| Omit<Headless.MenuItemProps<typeof Link>, 'as' | 'className'> | Omit<Headless.MenuItemProps<typeof Link>, 'as' | 'className'>
)) { )) {
let classes = clsx( const classes = clsx(
className, className,
// Base styles // Base styles
'group cursor-default rounded-lg px-3.5 py-2.5 focus:outline-hidden sm:px-3 sm:py-1.5', 'group cursor-default rounded-lg px-3.5 py-2.5 focus:outline-hidden sm:px-3 sm:py-1.5',
@ -75,7 +75,7 @@ export function DropdownItem({
'*:data-[slot=icon]:col-start-1 *:data-[slot=icon]:row-start-1 *:data-[slot=icon]:mr-2.5 *:data-[slot=icon]:-ml-0.5 *:data-[slot=icon]:size-5 sm:*:data-[slot=icon]:mr-2 sm:*:data-[slot=icon]:size-4', '*:data-[slot=icon]:col-start-1 *:data-[slot=icon]:row-start-1 *:data-[slot=icon]:mr-2.5 *:data-[slot=icon]:-ml-0.5 *:data-[slot=icon]:size-5 sm:*:data-[slot=icon]:mr-2 sm:*:data-[slot=icon]:size-4',
'*:data-[slot=icon]:text-zinc-500 data-focus:*:data-[slot=icon]:text-white dark:*:data-[slot=icon]:text-zinc-400 dark:data-focus:*:data-[slot=icon]:text-white', '*:data-[slot=icon]:text-zinc-500 data-focus:*:data-[slot=icon]:text-white dark:*:data-[slot=icon]:text-zinc-400 dark:data-focus:*:data-[slot=icon]:text-white',
// Avatar // Avatar
'*:data-[slot=avatar]:mr-2.5 *:data-[slot=avatar]:-ml-1 *:data-[slot=avatar]:size-6 sm:*:data-[slot=avatar]:mr-2 sm:*:data-[slot=avatar]:size-5' '*:data-[slot=avatar]:mr-2.5 *:data-[slot=avatar]:-ml-1 *:data-[slot=avatar]:size-6 sm:*:data-[slot=avatar]:mr-2 sm:*:data-[slot=avatar]:size-5',
) )
return 'href' in props ? ( return 'href' in props ? (
@ -99,7 +99,7 @@ export function DropdownSection({
className={clsx( className={clsx(
className, className,
// Define grid at the section level instead of the item level if subgrid is supported // Define grid at the section level instead of the item level if subgrid is supported
'col-span-full supports-[grid-template-columns:subgrid]:grid supports-[grid-template-columns:subgrid]:grid-cols-[auto_1fr_1.5rem_0.5rem_auto]' 'col-span-full supports-[grid-template-columns:subgrid]:grid supports-[grid-template-columns:subgrid]:grid-cols-[auto_1fr_1.5rem_0.5rem_auto]',
)} )}
/> />
) )
@ -114,7 +114,7 @@ export function DropdownHeading({
{...props} {...props}
className={clsx( className={clsx(
className, className,
'col-span-full grid grid-cols-[1fr_auto] gap-x-12 px-3.5 pt-2 pb-1 text-sm/5 font-medium text-zinc-500 sm:px-3 sm:text-xs/5 dark:text-zinc-400' 'col-span-full grid grid-cols-[1fr_auto] gap-x-12 px-3.5 pt-2 pb-1 text-sm/5 font-medium text-zinc-500 sm:px-3 sm:text-xs/5 dark:text-zinc-400',
)} )}
/> />
) )
@ -129,7 +129,7 @@ export function DropdownDivider({
{...props} {...props}
className={clsx( className={clsx(
className, className,
'col-span-full mx-3.5 my-1 h-px border-0 bg-zinc-950/5 sm:mx-3 dark:bg-white/10 forced-colors:bg-[CanvasText]' 'col-span-full mx-3.5 my-1 h-px border-0 bg-zinc-950/5 sm:mx-3 dark:bg-white/10 forced-colors:bg-[CanvasText]',
)} )}
/> />
) )
@ -140,7 +140,12 @@ export function DropdownLabel({
...props ...props
}: { className?: string } & Omit<Headless.LabelProps, 'as' | 'className'>) { }: { className?: string } & Omit<Headless.LabelProps, 'as' | 'className'>) {
return ( return (
<Headless.Label {...props} data-slot="label" className={clsx(className, 'col-start-2 row-start-1')} {...props} /> <Headless.Label
{...props}
data-slot="label"
className={clsx(className, 'col-start-2 row-start-1')}
{...props}
/>
) )
} }
@ -154,7 +159,7 @@ export function DropdownDescription({
{...props} {...props}
className={clsx( className={clsx(
className, className,
'col-span-2 col-start-2 row-start-2 text-sm/5 text-zinc-500 group-data-focus:text-white sm:text-xs/5 dark:text-zinc-400 forced-colors:group-data-focus:text-[HighlightText]' 'col-span-2 col-start-2 row-start-2 text-sm/5 text-zinc-500 group-data-focus:text-white sm:text-xs/5 dark:text-zinc-400 forced-colors:group-data-focus:text-[HighlightText]',
)} )}
/> />
) )
@ -164,7 +169,10 @@ export function DropdownShortcut({
keys, keys,
className, className,
...props ...props
}: { keys: string | string[]; className?: string } & Omit<Headless.DescriptionProps<'kbd'>, 'as' | 'className'>) { }: { keys: string | string[]; className?: string } & Omit<
Headless.DescriptionProps<'kbd'>,
'as' | 'className'
>) {
return ( return (
<Headless.Description <Headless.Description
as="kbd" as="kbd"

View File

@ -1,27 +0,0 @@
import clsx from 'clsx'
type HeadingProps = { level?: 1 | 2 | 3 | 4 | 5 | 6 } & React.ComponentPropsWithoutRef<
'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
>
export function Heading({ className, level = 1, ...props }: HeadingProps) {
let Element: `h${typeof level}` = `h${level}`
return (
<Element
{...props}
className={clsx(className, 'text-2xl/8 font-semibold text-zinc-950 sm:text-xl/8 dark:text-white')}
/>
)
}
export function Subheading({ className, level = 2, ...props }: HeadingProps) {
let Element: `h${typeof level}` = `h${level}`
return (
<Element
{...props}
className={clsx(className, 'text-base/7 font-semibold text-zinc-950 sm:text-sm/6 dark:text-white')}
/>
)
}

View File

@ -1,177 +0,0 @@
'use client'
import * as Headless from '@headlessui/react'
import clsx from 'clsx'
import { Fragment } from 'react'
export function Listbox<T>({
className,
placeholder,
autoFocus,
'aria-label': ariaLabel,
children: options,
...props
}: {
className?: string
placeholder?: React.ReactNode
autoFocus?: boolean
'aria-label'?: string
children?: React.ReactNode
} & Omit<Headless.ListboxProps<typeof Fragment, T>, 'as' | 'multiple'>) {
return (
<Headless.Listbox {...props} multiple={false}>
<Headless.ListboxButton
autoFocus={autoFocus}
data-slot="control"
aria-label={ariaLabel}
className={clsx([
className,
// Basic layout
'group relative block w-full',
// Background color + shadow applied to inset pseudo element, so shadow blends with border in light mode
'before:absolute before:inset-px before:rounded-[calc(var(--radius-lg)-1px)] before:bg-white before:shadow-sm',
// Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo
'dark:before:hidden',
// Hide default focus styles
'focus:outline-hidden',
// Focus ring
'after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:ring-transparent after:ring-inset data-focus:after:ring-2 data-focus:after:ring-blue-500',
// Disabled state
'data-disabled:opacity-50 data-disabled:before:bg-zinc-950/5 data-disabled:before:shadow-none',
])}
>
<Headless.ListboxSelectedOption
as="span"
options={options}
placeholder={placeholder && <span className="block truncate text-zinc-500">{placeholder}</span>}
className={clsx([
// Basic layout
'relative block w-full appearance-none rounded-lg py-[calc(--spacing(2.5)-1px)] sm:py-[calc(--spacing(1.5)-1px)]',
// Set minimum height for when no value is selected
'min-h-11 sm:min-h-9',
// Horizontal padding
'pr-[calc(--spacing(7)-1px)] pl-[calc(--spacing(3.5)-1px)] sm:pl-[calc(--spacing(3)-1px)]',
// Typography
'text-left text-base/6 text-zinc-950 placeholder:text-zinc-500 sm:text-sm/6 dark:text-white forced-colors:text-[CanvasText]',
// Border
'border border-zinc-950/10 group-data-active:border-zinc-950/20 group-data-hover:border-zinc-950/20 dark:border-white/10 dark:group-data-active:border-white/20 dark:group-data-hover:border-white/20',
// Background color
'bg-transparent dark:bg-white/5',
// Invalid state
'group-data-invalid:border-red-500 group-data-hover:group-data-invalid:border-red-500 dark:group-data-invalid:border-red-600 dark:data-hover:group-data-invalid:border-red-600',
// Disabled state
'group-data-disabled:border-zinc-950/20 group-data-disabled:opacity-100 dark:group-data-disabled:border-white/15 dark:group-data-disabled:bg-white/[2.5%] dark:group-data-disabled:data-hover:border-white/15',
])}
/>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<svg
className="size-5 stroke-zinc-500 group-data-disabled:stroke-zinc-600 sm:size-4 dark:stroke-zinc-400 forced-colors:stroke-[CanvasText]"
viewBox="0 0 16 16"
aria-hidden="true"
fill="none"
>
<path d="M5.75 10.75L8 13L10.25 10.75" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" />
<path d="M10.25 5.25L8 3L5.75 5.25" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" />
</svg>
</span>
</Headless.ListboxButton>
<Headless.ListboxOptions
transition
anchor="selection start"
className={clsx(
// Anchor positioning
'[--anchor-offset:-1.625rem] [--anchor-padding:--spacing(4)] sm:[--anchor-offset:-1.375rem]',
// Base styles
'isolate w-max min-w-[calc(var(--button-width)+1.75rem)] scroll-py-1 rounded-xl p-1 select-none',
// Invisible border that is only visible in `forced-colors` mode for accessibility purposes
'outline outline-transparent focus:outline-hidden',
// Handle scrolling when menu won't fit in viewport
'overflow-y-scroll overscroll-contain',
// Popover background
'bg-white/75 backdrop-blur-xl dark:bg-zinc-800/75',
// Shadows
'shadow-lg ring-1 ring-zinc-950/10 dark:ring-white/10 dark:ring-inset',
// Transitions
'transition-opacity duration-100 ease-in data-closed:data-leave:opacity-0 data-transition:pointer-events-none'
)}
>
{options}
</Headless.ListboxOptions>
</Headless.Listbox>
)
}
export function ListboxOption<T>({
children,
className,
...props
}: { className?: string; children?: React.ReactNode } & Omit<
Headless.ListboxOptionProps<'div', T>,
'as' | 'className'
>) {
let sharedClasses = clsx(
// Base
'flex min-w-0 items-center',
// Icons
'*:data-[slot=icon]:size-5 *:data-[slot=icon]:shrink-0 sm:*:data-[slot=icon]:size-4',
'*:data-[slot=icon]:text-zinc-500 group-data-focus/option:*:data-[slot=icon]:text-white dark:*:data-[slot=icon]:text-zinc-400',
'forced-colors:*:data-[slot=icon]:text-[CanvasText] forced-colors:group-data-focus/option:*:data-[slot=icon]:text-[Canvas]',
// Avatars
'*:data-[slot=avatar]:-mx-0.5 *:data-[slot=avatar]:size-6 sm:*:data-[slot=avatar]:size-5'
)
return (
<Headless.ListboxOption as={Fragment} {...props}>
{({ selectedOption }) => {
if (selectedOption) {
return <div className={clsx(className, sharedClasses)}>{children}</div>
}
return (
<div
className={clsx(
// Basic layout
'group/option grid cursor-default grid-cols-[--spacing(5)_1fr] items-baseline gap-x-2 rounded-lg py-2.5 pr-3.5 pl-2 sm:grid-cols-[--spacing(4)_1fr] sm:py-1.5 sm:pr-3 sm:pl-1.5',
// Typography
'text-base/6 text-zinc-950 sm:text-sm/6 dark:text-white forced-colors:text-[CanvasText]',
// Focus
'outline-hidden data-focus:bg-blue-500 data-focus:text-white',
// Forced colors mode
'forced-color-adjust-none forced-colors:data-focus:bg-[Highlight] forced-colors:data-focus:text-[HighlightText]',
// Disabled
'data-disabled:opacity-50'
)}
>
<svg
className="relative hidden size-5 self-center stroke-current group-data-selected/option:inline sm:size-4"
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
>
<path d="M4 8.5l3 3L12 4" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" />
</svg>
<span className={clsx(className, sharedClasses, 'col-start-2')}>{children}</span>
</div>
)
}}
</Headless.ListboxOption>
)
}
export function ListboxLabel({ className, ...props }: React.ComponentPropsWithoutRef<'span'>) {
return <span {...props} className={clsx(className, 'ml-2.5 truncate first:ml-0 sm:ml-2 sm:first:ml-0')} />
}
export function ListboxDescription({ className, children, ...props }: React.ComponentPropsWithoutRef<'span'>) {
return (
<span
{...props}
className={clsx(
className,
'flex flex-1 overflow-hidden text-zinc-500 group-data-focus/option:text-white before:w-2 before:min-w-0 before:shrink dark:text-zinc-400'
)}
>
<span className="flex-1 truncate">{children}</span>
</span>
)
}

View File

@ -12,11 +12,17 @@ export function Navbar({ className, ...props }: React.ComponentPropsWithoutRef<'
} }
export function NavbarDivider({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) { export function NavbarDivider({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
return <div aria-hidden="true" {...props} className={clsx(className, 'h-6 w-px bg-zinc-950/10 dark:bg-white/10')} /> return (
<div
aria-hidden="true"
{...props}
className={clsx(className, 'h-6 w-px bg-zinc-950/10 dark:bg-white/10')}
/>
)
} }
export function NavbarSection({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) { export function NavbarSection({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
let id = useId() const id = useId()
return ( return (
<LayoutGroup id={id}> <LayoutGroup id={id}>
@ -39,9 +45,9 @@ export const NavbarItem = forwardRef(function NavbarItem(
| Omit<Headless.ButtonProps, 'as' | 'className'> | Omit<Headless.ButtonProps, 'as' | 'className'>
| Omit<React.ComponentPropsWithoutRef<typeof Link>, 'className'> | Omit<React.ComponentPropsWithoutRef<typeof Link>, 'className'>
), ),
ref: React.ForwardedRef<HTMLAnchorElement | HTMLButtonElement> ref: React.ForwardedRef<HTMLAnchorElement | HTMLButtonElement>,
) { ) {
let classes = clsx( const classes = clsx(
// Base // Base
'relative flex min-w-0 items-center gap-3 rounded-lg p-2 text-left text-base/6 font-medium text-zinc-950 sm:text-sm/5', 'relative flex min-w-0 items-center gap-3 rounded-lg p-2 text-left text-base/6 font-medium text-zinc-950 sm:text-sm/5',
// Leading icon/icon-only // Leading icon/icon-only
@ -57,7 +63,7 @@ export const NavbarItem = forwardRef(function NavbarItem(
// Dark mode // Dark mode
'dark:text-white dark:*:data-[slot=icon]:fill-zinc-400', 'dark:text-white dark:*:data-[slot=icon]:fill-zinc-400',
'dark:data-hover:bg-white/5 dark:data-hover:*:data-[slot=icon]:fill-white', 'dark:data-hover:bg-white/5 dark:data-hover:*:data-[slot=icon]:fill-white',
'dark:data-active:bg-white/5 dark:data-active:*:data-[slot=icon]:fill-white' 'dark:data-active:bg-white/5 dark:data-active:*:data-[slot=icon]:fill-white',
) )
return ( return (

View File

@ -1,101 +0,0 @@
import clsx from 'clsx'
import type React from 'react'
import { Button } from './button'
export function Pagination({
'aria-label': ariaLabel = 'Page navigation',
className,
...props
}: React.ComponentPropsWithoutRef<'nav'>) {
return <nav aria-label={ariaLabel} {...props} className={clsx(className, 'flex gap-x-2')} />
}
export function PaginationPrevious({
href = null,
className,
children = 'Previous',
}: React.PropsWithChildren<{ href?: string | null; className?: string }>) {
return (
<span className={clsx(className, 'grow basis-0')}>
<Button {...(href === null ? { disabled: true } : { href })} plain aria-label="Previous page">
<svg className="stroke-current" data-slot="icon" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path
d="M2.75 8H13.25M2.75 8L5.25 5.5M2.75 8L5.25 10.5"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
{children}
</Button>
</span>
)
}
export function PaginationNext({
href = null,
className,
children = 'Next',
}: React.PropsWithChildren<{ href?: string | null; className?: string }>) {
return (
<span className={clsx(className, 'flex grow basis-0 justify-end')}>
<Button {...(href === null ? { disabled: true } : { href })} plain aria-label="Next page">
{children}
<svg className="stroke-current" data-slot="icon" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path
d="M13.25 8L2.75 8M13.25 8L10.75 10.5M13.25 8L10.75 5.5"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Button>
</span>
)
}
export function PaginationList({ className, ...props }: React.ComponentPropsWithoutRef<'span'>) {
return <span {...props} className={clsx(className, 'hidden items-baseline gap-x-2 sm:flex')} />
}
export function PaginationPage({
href,
className,
current = false,
children,
}: React.PropsWithChildren<{ href: string; className?: string; current?: boolean }>) {
return (
<Button
href={href}
plain
aria-label={`Page ${children}`}
aria-current={current ? 'page' : undefined}
className={clsx(
className,
'min-w-[2.25rem] before:absolute before:-inset-px before:rounded-lg',
current && 'before:bg-zinc-950/5 dark:before:bg-white/10'
)}
>
<span className="-mx-0.5">{children}</span>
</Button>
)
}
export function PaginationGap({
className,
children = <>&hellip;</>,
...props
}: React.ComponentPropsWithoutRef<'span'>) {
return (
<span
aria-hidden="true"
{...props}
className={clsx(
className,
'w-[2.25rem] text-center text-sm/6 font-semibold text-zinc-950 select-none dark:text-white'
)}
>
{children}
</span>
)
}

View File

@ -20,7 +20,11 @@ function CloseMenuIcon() {
) )
} }
function MobileSidebar({ open, close, children }: React.PropsWithChildren<{ open: boolean; close: () => void }>) { function MobileSidebar({
open,
close,
children,
}: React.PropsWithChildren<{ open: boolean; close: () => void }>) {
return ( return (
<Headless.Dialog open={open} onClose={close} className="lg:hidden"> <Headless.Dialog open={open} onClose={close} className="lg:hidden">
<Headless.DialogBackdrop <Headless.DialogBackdrop
@ -49,7 +53,7 @@ export function SidebarLayout({
sidebar, sidebar,
children, children,
}: React.PropsWithChildren<{ navbar: React.ReactNode; sidebar: React.ReactNode }>) { }: React.PropsWithChildren<{ navbar: React.ReactNode; sidebar: React.ReactNode }>) {
let [showSidebar, setShowSidebar] = useState(false) const [showSidebar, setShowSidebar] = useState(false)
return ( return (
<div className="relative isolate flex min-h-svh w-full bg-white max-lg:flex-col lg:bg-zinc-100 dark:bg-zinc-900 dark:lg:bg-zinc-950"> <div className="relative isolate flex min-h-svh w-full bg-white max-lg:flex-col lg:bg-zinc-100 dark:bg-zinc-900 dark:lg:bg-zinc-950">

View File

@ -17,7 +17,7 @@ export function SidebarHeader({ className, ...props }: React.ComponentPropsWitho
{...props} {...props}
className={clsx( className={clsx(
className, className,
'flex flex-col border-b border-zinc-950/5 p-4 dark:border-white/5 [&>[data-slot=section]+[data-slot=section]]:mt-2.5' 'flex flex-col border-b border-zinc-950/5 p-4 dark:border-white/5 [&>[data-slot=section]+[data-slot=section]]:mt-2.5',
)} )}
/> />
) )
@ -29,7 +29,7 @@ export function SidebarBody({ className, ...props }: React.ComponentPropsWithout
{...props} {...props}
className={clsx( className={clsx(
className, className,
'flex flex-1 flex-col overflow-y-auto p-4 [&>[data-slot=section]+[data-slot=section]]:mt-8' 'flex flex-1 flex-col overflow-y-auto p-4 [&>[data-slot=section]+[data-slot=section]]:mt-8',
)} )}
/> />
) )
@ -41,14 +41,14 @@ export function SidebarFooter({ className, ...props }: React.ComponentPropsWitho
{...props} {...props}
className={clsx( className={clsx(
className, className,
'flex flex-col border-t border-zinc-950/5 p-4 dark:border-white/5 [&>[data-slot=section]+[data-slot=section]]:mt-2.5' 'flex flex-col border-t border-zinc-950/5 p-4 dark:border-white/5 [&>[data-slot=section]+[data-slot=section]]:mt-2.5',
)} )}
/> />
) )
} }
export function SidebarSection({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) { export function SidebarSection({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
let id = useId() const id = useId()
return ( return (
<LayoutGroup id={id}> <LayoutGroup id={id}>
@ -58,7 +58,12 @@ export function SidebarSection({ className, ...props }: React.ComponentPropsWith
} }
export function SidebarDivider({ className, ...props }: React.ComponentPropsWithoutRef<'hr'>) { export function SidebarDivider({ className, ...props }: React.ComponentPropsWithoutRef<'hr'>) {
return <hr {...props} className={clsx(className, 'my-4 border-t border-zinc-950/5 lg:-mx-4 dark:border-white/5')} /> return (
<hr
{...props}
className={clsx(className, 'my-4 border-t border-zinc-950/5 lg:-mx-4 dark:border-white/5')}
/>
)
} }
export function SidebarSpacer({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) { export function SidebarSpacer({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
@ -67,7 +72,13 @@ export function SidebarSpacer({ className, ...props }: React.ComponentPropsWitho
export function SidebarHeading({ className, ...props }: React.ComponentPropsWithoutRef<'h3'>) { export function SidebarHeading({ className, ...props }: React.ComponentPropsWithoutRef<'h3'>) {
return ( return (
<h3 {...props} className={clsx(className, 'mb-1 px-2 text-xs/6 font-medium text-zinc-500 dark:text-zinc-400')} /> <h3
{...props}
className={clsx(
className,
'mb-1 px-2 text-xs/6 font-medium text-zinc-500 dark:text-zinc-400',
)}
/>
) )
} }
@ -81,9 +92,9 @@ export const SidebarItem = forwardRef(function SidebarItem(
| Omit<Headless.ButtonProps, 'as' | 'className'> | Omit<Headless.ButtonProps, 'as' | 'className'>
| Omit<Headless.ButtonProps<typeof Link>, 'as' | 'className'> | Omit<Headless.ButtonProps<typeof Link>, 'as' | 'className'>
), ),
ref: React.ForwardedRef<HTMLAnchorElement | HTMLButtonElement> ref: React.ForwardedRef<HTMLAnchorElement | HTMLButtonElement>,
) { ) {
let classes = clsx( const classes = clsx(
// Base // Base
'flex w-full items-center gap-3 rounded-lg px-2 py-2.5 text-left text-base/6 font-medium text-zinc-950 sm:py-2 sm:text-sm/5', 'flex w-full items-center gap-3 rounded-lg px-2 py-2.5 text-left text-base/6 font-medium text-zinc-950 sm:py-2 sm:text-sm/5',
// Leading icon/icon-only // Leading icon/icon-only
@ -102,7 +113,7 @@ export const SidebarItem = forwardRef(function SidebarItem(
'dark:text-white dark:*:data-[slot=icon]:fill-zinc-400', 'dark:text-white dark:*:data-[slot=icon]:fill-zinc-400',
'dark:data-hover:bg-white/5 dark:data-hover:*:data-[slot=icon]:fill-white', 'dark:data-hover:bg-white/5 dark:data-hover:*:data-[slot=icon]:fill-white',
'dark:data-active:bg-white/5 dark:data-active:*:data-[slot=icon]:fill-white', 'dark:data-active:bg-white/5 dark:data-active:*:data-[slot=icon]:fill-white',
'dark:data-current:*:data-[slot=icon]:fill-white' 'dark:data-current:*:data-[slot=icon]:fill-white',
) )
return ( return (

View File

@ -53,7 +53,7 @@ export function StackedLayout({
sidebar, sidebar,
children, children,
}: React.PropsWithChildren<{ navbar: React.ReactNode; sidebar: React.ReactNode }>) { }: React.PropsWithChildren<{ navbar: React.ReactNode; sidebar: React.ReactNode }>) {
let [showSidebar, setShowSidebar] = useState(false) const [showSidebar, setShowSidebar] = useState(false)
return ( return (
<div className="relative isolate flex min-h-svh w-full flex-col lg:bg-zinc-100 dark:bg-zinc-900 dark:lg:bg-zinc-950"> <div className="relative isolate flex min-h-svh w-full flex-col lg:bg-zinc-100 dark:bg-zinc-900 dark:lg:bg-zinc-950">

View File

@ -1,124 +0,0 @@
'use client'
import clsx from 'clsx'
import type React from 'react'
import { createContext, useContext, useState } from 'react'
import { Link } from './link'
const TableContext = createContext<{ bleed: boolean; dense: boolean; grid: boolean; striped: boolean }>({
bleed: false,
dense: false,
grid: false,
striped: false,
})
export function Table({
bleed = false,
dense = false,
grid = false,
striped = false,
className,
children,
...props
}: { bleed?: boolean; dense?: boolean; grid?: boolean; striped?: boolean } & React.ComponentPropsWithoutRef<'div'>) {
return (
<TableContext.Provider value={{ bleed, dense, grid, striped } as React.ContextType<typeof TableContext>}>
<div className="flow-root">
<div {...props} className={clsx(className, '-mx-(--gutter) overflow-x-auto whitespace-nowrap')}>
<div className={clsx('inline-block min-w-full align-middle', !bleed && 'sm:px-(--gutter)')}>
<table className="min-w-full text-left text-sm/6 text-zinc-950 dark:text-white">{children}</table>
</div>
</div>
</div>
</TableContext.Provider>
)
}
export function TableHead({ className, ...props }: React.ComponentPropsWithoutRef<'thead'>) {
return <thead {...props} className={clsx(className, 'text-zinc-500 dark:text-zinc-400')} />
}
export function TableBody(props: React.ComponentPropsWithoutRef<'tbody'>) {
return <tbody {...props} />
}
const TableRowContext = createContext<{ href?: string; target?: string; title?: string }>({
href: undefined,
target: undefined,
title: undefined,
})
export function TableRow({
href,
target,
title,
className,
...props
}: { href?: string; target?: string; title?: string } & React.ComponentPropsWithoutRef<'tr'>) {
let { striped } = useContext(TableContext)
return (
<TableRowContext.Provider value={{ href, target, title } as React.ContextType<typeof TableRowContext>}>
<tr
{...props}
className={clsx(
className,
href &&
'has-[[data-row-link][data-focus]]:outline-2 has-[[data-row-link][data-focus]]:-outline-offset-2 has-[[data-row-link][data-focus]]:outline-blue-500 dark:focus-within:bg-white/[2.5%]',
striped && 'even:bg-zinc-950/[2.5%] dark:even:bg-white/[2.5%]',
href && striped && 'hover:bg-zinc-950/5 dark:hover:bg-white/5',
href && !striped && 'hover:bg-zinc-950/[2.5%] dark:hover:bg-white/[2.5%]'
)}
/>
</TableRowContext.Provider>
)
}
export function TableHeader({ className, ...props }: React.ComponentPropsWithoutRef<'th'>) {
let { bleed, grid } = useContext(TableContext)
return (
<th
{...props}
className={clsx(
className,
'border-b border-b-zinc-950/10 px-4 py-2 font-medium first:pl-(--gutter,--spacing(2)) last:pr-(--gutter,--spacing(2)) dark:border-b-white/10',
grid && 'border-l border-l-zinc-950/5 first:border-l-0 dark:border-l-white/5',
!bleed && 'sm:first:pl-1 sm:last:pr-1'
)}
/>
)
}
export function TableCell({ className, children, ...props }: React.ComponentPropsWithoutRef<'td'>) {
let { bleed, dense, grid, striped } = useContext(TableContext)
let { href, target, title } = useContext(TableRowContext)
let [cellRef, setCellRef] = useState<HTMLElement | null>(null)
return (
<td
ref={href ? setCellRef : undefined}
{...props}
className={clsx(
className,
'relative px-4 first:pl-(--gutter,--spacing(2)) last:pr-(--gutter,--spacing(2))',
!striped && 'border-b border-zinc-950/5 dark:border-white/5',
grid && 'border-l border-l-zinc-950/5 first:border-l-0 dark:border-l-white/5',
dense ? 'py-2.5' : 'py-4',
!bleed && 'sm:first:pl-1 sm:last:pr-1'
)}
>
{href && (
<Link
data-row-link
href={href}
target={target}
aria-label={title}
tabIndex={cellRef?.previousElementSibling === null ? 0 : -1}
className="absolute inset-0 focus:outline-hidden"
/>
)}
{children}
</td>
)
}

View File

@ -1,4 +1,3 @@
import * as jeact from 'react'
import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from 'lucide-react' import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from 'lucide-react'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'

View File

@ -13,7 +13,7 @@ type GlobalState = {
const defaultState = { const defaultState = {
user: undefined, user: undefined,
setUser: (user?: User) => {}, setUser: (_?: User) => {},
} }
const GlobalContext = createContext<GlobalState>(defaultState) const GlobalContext = createContext<GlobalState>(defaultState)