Files
kycnotme/web/src/components/InputCardGroup.astro
2025-05-26 14:45:22 +00:00

128 lines
4.3 KiB
Plaintext

---
import { Icon } from 'astro-icon/components'
import { Markdown } from 'astro-remote'
import { cn } from '../lib/cn'
import InputWrapper from './InputWrapper.astro'
import type { MarkdownString } from '../lib/markdown'
import type { ComponentProps } from 'astro/types'
type Props<Multiple extends boolean = false> = Omit<
ComponentProps<typeof InputWrapper>,
'children' | 'inputId'
> & {
options: {
label: string
value: string
icon?: string
iconClass?: string
description?: MarkdownString
disabled?: boolean
noTransitionPersist?: boolean
}[]
disabled?: boolean
selectedValue?: Multiple extends true ? string[] : string
cardSize?: 'lg' | 'md' | 'sm'
iconSize?: 'md' | 'sm'
multiple?: Multiple
}
const {
options,
disabled,
selectedValue = undefined as string[] | string | undefined,
cardSize = 'sm',
iconSize = 'sm',
class: className,
multiple = false as boolean,
...wrapperProps
} = Astro.props
const hasError = !!wrapperProps.error && wrapperProps.error.length > 0
---
{/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */}
<InputWrapper class={cn('@container', className)} {...wrapperProps}>
<div
class={cn(
'grid grid-cols-[repeat(auto-fill,minmax(var(--card-min-size),1fr))] gap-2 rounded-lg',
!multiple &&
'has-focus-visible:ring-offset-night-900 has-focus-visible:ring-day-200 has-focus-visible:bg-night-900 has-focus-visible:ring-2 has-focus-visible:ring-offset-3',
{
'[--card-min-size:12rem] @max-[12rem]:grid-cols-1': cardSize === 'sm',
'[--card-min-size:16rem] @max-[16rem]:grid-cols-1': cardSize === 'md',
'[--card-min-size:32rem] @max-[32rem]:grid-cols-1': cardSize === 'lg',
},
hasError && 'border border-red-700 p-2'
)}
>
{
options.map((option) => (
<label
class={cn(
'group border-night-400 bg-night-600 hover:bg-night-500 relative cursor-pointer items-start gap-3 rounded-lg border p-3 transition-all',
'has-checked:border-green-700 has-checked:bg-green-700/20 has-checked:ring-1 has-checked:ring-green-700',
multiple &&
'has-focus-visible:border-day-300 has-focus-visible:ring-2 has-focus-visible:ring-green-700 has-focus-visible:ring-offset-1',
'has-[input:disabled]:cursor-not-allowed has-[input:disabled]:opacity-50'
)}
>
<input
transition:persist={option.noTransitionPersist || !multiple ? undefined : true}
type={multiple ? 'checkbox' : 'radio'}
name={wrapperProps.name}
value={option.value}
checked={
Array.isArray(selectedValue)
? selectedValue.includes(option.value)
: selectedValue === option.value
}
class="peer sr-only"
disabled={disabled || option.disabled}
/>
<div class="flex items-center gap-1.5">
{option.icon && (
<Icon
name={option.icon}
class={cn(
'text-day-200 group-peer-checked:text-day-300 size-8',
{
'size-4': iconSize === 'sm',
'size-8': iconSize === 'md',
},
option.iconClass
)}
/>
)}
<p class="text-day-200 group-peer-checked:text-day-300 flex-1 text-sm leading-none font-medium text-pretty">
{option.label}
</p>
<div class="self-stretch">
<div
class={cn(
'border-day-600 flex size-5 items-center justify-center border-2',
'group-has-checked:border-green-600 group-has-checked:bg-green-600',
multiple ? 'rounded-md' : 'rounded-full',
!!option.description && '-m-1'
)}
>
<Icon
name="ri:check-line"
class="text-day-100 size-3 opacity-0 group-has-checked:opacity-100"
/>
</div>
</div>
</div>
{option.description && (
<div class="prose prose-sm prose-invert text-day-400 mt-1 text-xs text-pretty">
<Markdown content={option.description} />
</div>
)}
</label>
))
}
</div>
</InputWrapper>