Merge branch 'main' into promotions
This commit is contained in:
@@ -84,7 +84,7 @@ function loaStatus(loa: LOARequest): "Upcoming" | "Active" | "Overdue" | "Closed
|
||||
|
||||
if (now < start) return "Upcoming";
|
||||
if (now >= start && now <= end) return "Active";
|
||||
if (now > end) return "Overdue";
|
||||
if (now > loa.extended_till || end) return "Overdue";
|
||||
|
||||
return "Overdue"; // fallback
|
||||
}
|
||||
@@ -197,7 +197,7 @@ function setPage(pagenum: number) {
|
||||
<TableCell @click.stop="" class="text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger class="cursor-pointer">
|
||||
<Button variant="ghost">
|
||||
<Button variant="ghost" size="icon">
|
||||
<Ellipsis class="size-6"></Ellipsis>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -220,10 +220,11 @@ function setPage(pagenum: number) {
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button v-if="expanded === post.id" @click.stop="expanded = null" variant="ghost">
|
||||
<Button v-if="expanded === post.id" @click.stop="expanded = null" size="icon"
|
||||
variant="ghost">
|
||||
<ChevronUp class="size-6" />
|
||||
</Button>
|
||||
<Button v-else @click.stop="expanded = post.id" variant="ghost">
|
||||
<Button v-else @click.stop="expanded = post.id" size="icon" variant="ghost">
|
||||
<ChevronDown class="size-6" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
@@ -233,18 +234,24 @@ function setPage(pagenum: number) {
|
||||
<TableCell :colspan="8" class="p-0">
|
||||
<div class="w-full p-3 mb-6 space-y-3">
|
||||
<div class="flex justify-between items-start gap-4">
|
||||
<div class="flex-1">
|
||||
<!-- Title -->
|
||||
<p class="text-md font-semibold text-foreground">
|
||||
Reason
|
||||
</p>
|
||||
<div class="space-y-3 w-full">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex items-center gap-2">
|
||||
<h4 class="text-sm font-semibold text-foreground">
|
||||
Reason
|
||||
</h4>
|
||||
<Separator class="flex-1" />
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<p
|
||||
class="mt-1 text-md whitespace-pre-wrap leading-relaxed text-muted-foreground">
|
||||
{{ post.reason }}
|
||||
</p>
|
||||
<div
|
||||
class="rounded-lg border bg-muted/40 px-4 py-3 text-sm leading-relaxed whitespace-pre-wrap text-muted-foreground w-full">
|
||||
{{ post.reason || 'No reason provided.' }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useMemberDirectory } from '@/stores/memberDirectory';
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { Member, type MemberLight } from '@shared/types/member'
|
||||
import { Member, MemberCardDetails, type MemberLight } from '@shared/types/member'
|
||||
import Popover from '../ui/popover/Popover.vue';
|
||||
import PopoverTrigger from '../ui/popover/PopoverTrigger.vue';
|
||||
import PopoverContent from '../ui/popover/PopoverContent.vue';
|
||||
@@ -9,6 +9,7 @@ import { cn } from '@/lib/utils.js'
|
||||
import { watch } from 'vue';
|
||||
import { format } from 'path';
|
||||
import Spinner from '../ui/spinner/Spinner.vue';
|
||||
import { Dot } from 'lucide-vue-next';
|
||||
|
||||
|
||||
// Props
|
||||
@@ -21,7 +22,7 @@ const props = defineProps({
|
||||
|
||||
// Local state
|
||||
const memberLight = ref<MemberLight | null>(null);
|
||||
const memberFull = ref<Member | null>(null)
|
||||
const memberFull = ref<MemberCardDetails | null>(null)
|
||||
const loadingFull = ref(false)
|
||||
const membersStore = useMemberDirectory();
|
||||
|
||||
@@ -63,7 +64,7 @@ const hasFullInfo = computed(() => {
|
||||
if (!memberFull.value) return false
|
||||
|
||||
// check if any field has a value
|
||||
const { rank, unit, status } = memberFull.value
|
||||
const { rank, unit, status } = memberFull.value.member
|
||||
return !!(rank || unit || status)
|
||||
})
|
||||
|
||||
@@ -90,7 +91,7 @@ function formatDate(date: Date): string {
|
||||
{{ displayName }}
|
||||
</p>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-72 p-0 overflow-hidden">
|
||||
<PopoverContent class="w-80 p-0 overflow-hidden">
|
||||
<!-- Loading -->
|
||||
<div v-if="loadingFull" class="p-4 text-sm text-muted-foreground mx-auto flex justify-center my-5">
|
||||
<Spinner></Spinner>
|
||||
@@ -114,26 +115,36 @@ function formatDate(date: Date): string {
|
||||
<div class="p-4 space-y-3 text-sm">
|
||||
<!-- Full info -->
|
||||
<template v-if="hasFullInfo">
|
||||
<div v-if="memberFull.loa_until"
|
||||
<div v-if="memberFull.member.loa_until"
|
||||
class=" rounded-md text-center bg-yellow-500/10 px-2 py-1 text-xs text-yellow-600">
|
||||
On Leave of Absence until {{ formatDate(memberFull.loa_until) }}
|
||||
On Leave of Absence until {{ formatDate(memberFull.member.loa_until) }}
|
||||
</div>
|
||||
|
||||
<div v-if="memberFull.rank" class="flex justify-between">
|
||||
|
||||
<div v-if="memberFull.member.rank" class="flex justify-between">
|
||||
<span class="text-muted-foreground">Rank</span>
|
||||
<span class="font-medium">{{ memberFull.rank }}</span>
|
||||
<span class="font-medium">{{ memberFull.member.rank }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="memberFull.unit" class="flex justify-between">
|
||||
<div v-if="memberFull.member.unit" class="flex justify-between items-center">
|
||||
<span class="text-muted-foreground">Unit</span>
|
||||
<span class="font-medium">{{ memberFull.unit }}</span>
|
||||
<span class="font-medium flex items-center gap-2">
|
||||
<span class="w-2 h-2 rounded-full" :style="{ backgroundColor: textColor }"></span>
|
||||
{{ memberFull.member.unit }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="memberFull.status" class="flex justify-between">
|
||||
<div v-if="memberFull.member.status" class="flex justify-between">
|
||||
<span class="text-muted-foreground">Status</span>
|
||||
<span class="font-medium">{{ memberFull.status }}</span>
|
||||
<span class="font-medium">{{ memberFull.member.status }}</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex gap-2 flex-wrap mt-6">
|
||||
<div v-for="role in memberFull.roles" class="border rounded-full px-3 text-nowrap">
|
||||
{{ role.name }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- No info fallback -->
|
||||
|
||||
103
ui/src/components/roles/addMember.vue
Normal file
103
ui/src/components/roles/addMember.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<script setup lang="ts">
|
||||
import { addMemberToRole, removeMemberFromRole } from '@/api/roles';
|
||||
import { MemberLight } from '@shared/types/member';
|
||||
import { Role } from '@shared/types/roles';
|
||||
import { computed, ref } from 'vue';
|
||||
import Dialog from '../ui/dialog/Dialog.vue'
|
||||
import DialogContent from '../ui/dialog/DialogContent.vue'
|
||||
import DialogHeader from '../ui/dialog/DialogHeader.vue'
|
||||
import DialogTitle from '../ui/dialog/DialogTitle.vue'
|
||||
import DialogDescription from '../ui/dialog/DialogDescription.vue'
|
||||
import DialogFooter from '../ui/dialog/DialogFooter.vue'
|
||||
import Button from '../ui/button/Button.vue';
|
||||
import InputGroup from '../ui/input-group/InputGroup.vue';
|
||||
import InputGroupAddon from '../ui/input-group/InputGroupAddon.vue';
|
||||
import { SearchIcon } from 'lucide-vue-next';
|
||||
|
||||
const props = defineProps<{
|
||||
allMembers: MemberLight[],
|
||||
role: Role
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['submit'])
|
||||
|
||||
const showAddMemberDialog = ref(false)
|
||||
const memberToAdd = ref<MemberLight | null>(null)
|
||||
const searchQuery = ref('')
|
||||
|
||||
const filteredMembers = computed(() => {
|
||||
const q = searchQuery.value.trim().toLowerCase()
|
||||
if (!q) return props.allMembers
|
||||
|
||||
return props.allMembers.filter(m =>
|
||||
m.displayName?.toLowerCase().includes(q) ||
|
||||
m.username?.toLowerCase().includes(q)
|
||||
)
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
openDialog,
|
||||
})
|
||||
|
||||
function openDialog() {
|
||||
showAddMemberDialog.value = true;
|
||||
}
|
||||
|
||||
|
||||
async function handleAddMember() {
|
||||
//guard
|
||||
if (memberToAdd.value == null)
|
||||
return;
|
||||
|
||||
await addMemberToRole(memberToAdd.value.id, props.role.id);
|
||||
emit('submit');
|
||||
showAddMemberDialog.value = false;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model:open="showAddMemberDialog">
|
||||
<DialogContent class="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add member to {{ props.role?.name }}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Search for a member to add to this group.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<!-- Search -->
|
||||
<InputGroup>
|
||||
<InputGroupAddon>
|
||||
<SearchIcon class="h-4 w-4 text-muted-foreground" />
|
||||
</InputGroupAddon>
|
||||
<input v-model="searchQuery" type="text" placeholder="Search members…"
|
||||
class="flex-1 bg-transparent outline-none text-sm" />
|
||||
</InputGroup>
|
||||
|
||||
<!-- Results -->
|
||||
<div class="mt-3 h-64 overflow-y-auto scrollbar-themed rounded-md border">
|
||||
<ul class="divide-y">
|
||||
<li v-for="member in filteredMembers" :key="member.id" @click="memberToAdd = member"
|
||||
class="flex items-center justify-between px-3 py-2 cursor-pointer hover:bg-muted"
|
||||
:class="memberToAdd?.id === member.id && 'bg-muted'">
|
||||
<span>{{ member.displayName || member.username }}</span>
|
||||
|
||||
<span v-if="memberToAdd?.id === member.id" class="text-xs text-muted-foreground">
|
||||
Selected
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="secondary" @click="showAddMemberDialog = false">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button :disabled="!memberToAdd" @click="handleAddMember">
|
||||
Add
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
142
ui/src/components/roles/roleView.vue
Normal file
142
ui/src/components/roles/roleView.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<script setup lang="ts">
|
||||
import { addMemberToRole, getRoleDetails, getRoleMembers, removeMemberFromRole } from '@/api/roles'
|
||||
import type { MemberLight } from '@shared/types/member'
|
||||
import type { Role } from '@shared/types/roles'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import Separator from '@/components/ui/separator/Separator.vue'
|
||||
import { Plus, SearchIcon, X } from 'lucide-vue-next'
|
||||
import MemberCard from '../members/MemberCard.vue'
|
||||
import InputGroup from '../ui/input-group/InputGroup.vue'
|
||||
import InputGroupAddon from '../ui/input-group/InputGroupAddon.vue'
|
||||
|
||||
import AddMember from './addMember.vue'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const roleData = ref<Role | null>(null)
|
||||
const roleMembers = ref<MemberLight[]>([])
|
||||
const loading = ref(true)
|
||||
|
||||
async function loadRole() {
|
||||
const id = Number(route.params.id)
|
||||
roleData.value = await getRoleDetails(id)
|
||||
roleMembers.value = await getRoleMembers(id)
|
||||
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const searchQuery = ref('')
|
||||
const roleMembersFiltered = computed(() => {
|
||||
if (!searchQuery.value) return roleMembers.value
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
return roleMembers.value.filter(member =>
|
||||
member.displayName?.toLowerCase().includes(query) ||
|
||||
member.username?.toLowerCase().includes(query)
|
||||
)
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
allMembers: MemberLight[]
|
||||
}>()
|
||||
|
||||
|
||||
const availableMembers = computed(() =>
|
||||
props.allMembers.filter(
|
||||
m => !roleMembers.value.some(rm => rm.id === m.id)
|
||||
)
|
||||
)
|
||||
|
||||
async function handleRemoveMember(memberId: number) {
|
||||
await removeMemberFromRole(memberId, Number(route.params.id));
|
||||
await loadRole();
|
||||
}
|
||||
|
||||
const addMemberRef = ref<InstanceType<typeof AddMember> | null>(null)
|
||||
|
||||
onMounted(loadRole)
|
||||
watch(() => route.params.id, loadRole)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AddMember ref="addMemberRef" :all-members="availableMembers" :role="roleData" @submit="loadRole"></AddMember>
|
||||
<div class="h-full px-6 py-2">
|
||||
<!-- Loading -->
|
||||
<div v-if="loading" class="text-muted-foreground">
|
||||
Loading group…
|
||||
</div>
|
||||
|
||||
<!-- No role selected -->
|
||||
<div v-else-if="!roleData" class="text-muted-foreground">
|
||||
Select a group to view details
|
||||
</div>
|
||||
|
||||
<!-- Role details -->
|
||||
<div v-else class="space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="h-3 w-3 rounded-full" :style="{ backgroundColor: roleData.color }" />
|
||||
<h2 class="text-2xl font-semibold tracking-tight">
|
||||
{{ roleData.name }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ roleData.description || 'No description provided.' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- <Button variant="ghost" size="sm" class="text-destructive">
|
||||
Delete
|
||||
</Button> -->
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<!-- Members -->
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-medium">
|
||||
Members ({{ roleMembers.length }})
|
||||
</h3>
|
||||
<div class="flex items-center gap-5">
|
||||
<InputGroup class="w-64">
|
||||
<InputGroupAddon>
|
||||
<SearchIcon class="h-4 w-4 text-muted-foreground" />
|
||||
</InputGroupAddon>
|
||||
|
||||
<input v-model="searchQuery" type="text" placeholder="Search members…"
|
||||
class="flex-1 bg-transparent outline-none text-sm" />
|
||||
</InputGroup>
|
||||
<Button variant="secondary" @click="addMemberRef.openDialog()">
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Add Member
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div v-if="roleMembers.length === 0" class="text-sm text-muted-foreground py-6 text-center">
|
||||
No members in this group yet.
|
||||
</div>
|
||||
|
||||
<div class="overflow-y-auto max-h-[55dvh] pr-1 scrollbar-themed">
|
||||
<ul class="space-y-1">
|
||||
<li v-for="member in roleMembersFiltered" :key="member.id"
|
||||
class="flex items-center justify-between rounded-md px-3 py-2 hover:bg-muted/50">
|
||||
<MemberCard :member-id="member.id" />
|
||||
|
||||
<Button variant="ghost" size="icon" class="text-muted-foreground"
|
||||
@click="handleRemoveMember(member.id)">
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
20
ui/src/components/tooltip/Tooltip.vue
Normal file
20
ui/src/components/tooltip/Tooltip.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
open: boolean,
|
||||
message: string
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative inline-flex items-center group w-min">
|
||||
<slot></slot>
|
||||
<div v-if="open" class="pointer-events-none absolute -top-9 left-1/2 -translate-x-1/2
|
||||
whitespace-nowrap rounded-md bg-popover px-2 py-1 text-xs
|
||||
text-popover-foreground shadow-md border border-border
|
||||
opacity-0 translate-y-1
|
||||
group-hover:opacity-100 group-hover:translate-y-0
|
||||
transition-opacity transition-transform duration-150">
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -25,6 +25,7 @@ import Popover from "@/components/ui/popover/Popover.vue";
|
||||
import PopoverTrigger from "@/components/ui/popover/PopoverTrigger.vue";
|
||||
import PopoverContent from "@/components/ui/popover/PopoverContent.vue";
|
||||
import Combobox from '../ui/combobox/Combobox.vue'
|
||||
import Tooltip from '../tooltip/Tooltip.vue'
|
||||
|
||||
|
||||
const { handleSubmit, resetForm, errors, values, setFieldValue } = useForm({
|
||||
@@ -50,7 +51,9 @@ watch(() => values.course_id, (newCourseId, oldCourseId) => {
|
||||
if (!oldCourseId) return;
|
||||
|
||||
values.attendees.forEach((a, index) => {
|
||||
// @ts-ignore
|
||||
setFieldValue(`attendees[${index}].passed_bookwork`, false);
|
||||
// @ts-ignore
|
||||
setFieldValue(`attendees[${index}].passed_qual`, false);
|
||||
});
|
||||
});
|
||||
@@ -326,22 +329,13 @@ const filteredMembers = computed(() => {
|
||||
<VeeField v-slot="{ field }" :name="`attendees[${index}].passed_bookwork`" type="checkbox"
|
||||
:value="false" :unchecked-value="true">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="relative inline-flex items-center group">
|
||||
|
||||
<Tooltip :open="!selectedCourse?.hasBookwork"
|
||||
message="This course does not have bookwork">
|
||||
<Checkbox :disabled="!selectedCourse?.hasBookwork"
|
||||
:name="`attendees[${index}].passed_bookwork`" :model-value="!field.checked"
|
||||
@update:model-value="field['onUpdate:modelValue']">
|
||||
</Checkbox>
|
||||
<!-- Tooltip bubble -->
|
||||
<div v-if="!selectedCourse?.hasBookwork" class="pointer-events-none absolute -top-9 left-1/2 -translate-x-1/2
|
||||
whitespace-nowrap rounded-md bg-popover px-2 py-1 text-xs
|
||||
text-popover-foreground shadow-md border border-border
|
||||
opacity-0 translate-y-1
|
||||
group-hover:opacity-100 group-hover:translate-y-0
|
||||
transition-opacity transition-transform duration-150">
|
||||
This course does not have bookwork
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div class="h-4">
|
||||
</div>
|
||||
</div>
|
||||
@@ -351,20 +345,12 @@ const filteredMembers = computed(() => {
|
||||
<VeeField v-slot="{ field }" :name="`attendees[${index}].passed_qual`" type="checkbox"
|
||||
:value="false" :unchecked-value="true">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="relative inline-flex items-center group">
|
||||
<Tooltip :open="!selectedCourse?.hasQual"
|
||||
message="This course does not have a qualification">
|
||||
<Checkbox :disabled="!selectedCourse?.hasQual"
|
||||
:name="`attendees[${index}].passed_qual`" :model-value="!field.checked"
|
||||
@update:model-value="field['onUpdate:modelValue']"></Checkbox>
|
||||
<!-- Tooltip bubble -->
|
||||
<div v-if="!selectedCourse?.hasQual" class="pointer-events-none absolute -top-9 left-1/2 -translate-x-1/2
|
||||
whitespace-nowrap rounded-md bg-popover px-2 py-1 text-xs
|
||||
text-popover-foreground shadow-md border border-border
|
||||
opacity-0 translate-y-1
|
||||
group-hover:opacity-100 group-hover:translate-y-0
|
||||
transition-opacity transition-transform duration-150">
|
||||
This course does not have a qualification
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div class="h-4">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,10 +2,10 @@ import { cva } from "class-variance-authority";
|
||||
|
||||
export { default as InputGroup } from "./InputGroup.vue";
|
||||
export { default as InputGroupAddon } from "./InputGroupAddon.vue";
|
||||
export { default as InputGroupButton } from "./InputGroupButton.vue";
|
||||
export { default as InputGroupInput } from "./InputGroupInput.vue";
|
||||
export { default as InputGroupText } from "./InputGroupText.vue";
|
||||
export { default as InputGroupTextarea } from "./InputGroupTextarea.vue";
|
||||
// export { default as InputGroupButton } from "./InputGroupButton.vue";
|
||||
// export { default as InputGroupInput } from "./InputGroupInput.vue";
|
||||
// export { default as InputGroupText } from "./InputGroupText.vue";
|
||||
// export { default as InputGroupTextarea } from "./InputGroupTextarea.vue";
|
||||
|
||||
export const inputGroupAddonVariants = cva(
|
||||
"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50",
|
||||
|
||||
@@ -22,7 +22,7 @@ const modelValue = useVModel(props, "modelValue", emits, {
|
||||
data-slot="input"
|
||||
:class="
|
||||
cn(
|
||||
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
||||
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||
props.class,
|
||||
|
||||
Reference in New Issue
Block a user