Adopt logistics visual system across webapp

This commit is contained in:
Ruslan Bakiev
2026-04-11 08:31:34 +07:00
parent ebe72907a4
commit a74e75049c
28 changed files with 1434 additions and 240 deletions

View File

@@ -1,8 +1,8 @@
<template>
<footer class="bg-base-200 text-base-content/80 py-6">
<div class="w-full px-4 sm:px-6 lg:px-8">
<footer class="px-3 pb-8 pt-2 text-white md:px-4">
<div class="mx-auto max-w-[1440px] rounded-[28px] border border-white/18 bg-[#10223b] px-6 py-6 shadow-[0_22px_54px_rgba(16,34,59,0.24)] [background-image:radial-gradient(circle_at_82%_18%,rgba(244,89,69,0.34),rgba(244,89,69,0)_34%),linear-gradient(130deg,#10223b_0%,#193450_100%)]">
<div class="text-center">
<p class="text-sm">© 2025 Optovia. {{ $t('footer.rights') }}</p>
<p class="text-sm font-medium text-white/82">© 2025 Optovia. {{ $t('footer.rights') }}</p>
</div>
</div>
</footer>

View File

@@ -1,123 +1,38 @@
<script setup lang="ts">
import { profileAvatarSeedFromValue, profileAvatarUrl } from '~/utils/profileAvatars'
const props = withDefaults(defineProps<{
avatarSeed?: string | null
seed?: string | null
label?: string | null
size?: number
}>(), {
avatarSeed: '',
seed: '',
label: '',
size: 48,
})
const normalizedSeed = computed(() => {
const source = props.avatarSeed || props.seed || props.label || 'person'
return profileAvatarSeedFromValue(source)
})
const avatarSrc = computed(() => profileAvatarUrl(normalizedSeed.value))
const avatarStyle = computed(() => ({
width: `${props.size}px`,
height: `${props.size}px`,
}))
</script>
<template>
<div class="flex flex-col items-center space-y-4">
<!-- Avatar -->
<div class="relative">
<div
class="w-24 h-24 rounded-full overflow-hidden border-4 border-base-300 shadow-lg"
:class="{ 'animate-pulse bg-base-200': loading }"
>
<div
v-if="!loading && avatarSvg"
v-html="avatarSvg"
class="w-full h-full"
/>
<div
v-else-if="!loading"
class="w-full h-full bg-gradient-to-br from-primary to-primary/70 flex items-center justify-center text-primary-content text-2xl font-bold"
>
{{ initials }}
</div>
</div>
<!-- Change avatar button -->
<button
@click="regenerateAvatar"
:disabled="loading"
class="absolute -bottom-1 -right-1 w-8 h-8 bg-primary hover:bg-primary/80 disabled:bg-base-300 text-primary-content rounded-full flex items-center justify-center shadow-lg transition-colors"
:title="$t('profile.regenerate_avatar')"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</div>
<!-- User name -->
<div class="text-center">
<p class="font-semibold text-base-content">{{ displayName }}</p>
<p class="text-sm text-base-content/60" v-if="userId">ID: {{ userId }}</p>
</div>
<div
class="relative inline-flex shrink-0 items-center justify-center overflow-hidden rounded-[34%] border border-white/80 bg-white shadow-[0_12px_28px_rgba(47,36,24,0.16)]"
:style="avatarStyle"
:title="label || undefined"
role="img"
:aria-label="label ? `Аватар: ${label}` : 'Аватар'"
>
<img :src="avatarSrc" :alt="label || 'Аватар'" class="h-full w-full object-cover" loading="lazy">
</div>
</template>
<script setup>
const props = defineProps({
userId: String,
firstName: String,
lastName: String,
avatarId: String,
editable: {
type: Boolean,
default: true
}
})
const emit = defineEmits(['avatar-changed'])
const loading = ref(false)
const avatarSvg = ref('')
// Computed properties
const displayName = computed(() => {
const first = props.firstName || ''
const last = props.lastName || ''
return `${first} ${last}`.trim() || 'User'
})
const initials = computed(() => {
const first = props.firstName?.charAt(0) || ''
const last = props.lastName?.charAt(0) || ''
return (first + last).toUpperCase() || '?'
})
// Generate avatar via DiceBear API
const generateAvatar = async (seed) => {
if (!seed) return
try {
loading.value = true
// Use DiceBear API to generate SVG avatar
const response = await fetch(`https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(seed)}&backgroundColor=b6e3f4,c0aede,d1d4f9`)
if (response.ok) {
avatarSvg.value = await response.text()
} else {
console.error('Failed to generate avatar:', response.status)
}
} catch (error) {
console.error('Error generating avatar:', error)
} finally {
loading.value = false
}
}
// Generate new random avatar ID
const regenerateAvatar = async () => {
if (!props.editable || loading.value) return
const newAvatarId = Math.random().toString(36).substring(2, 15)
// Update avatar locally first
await generateAvatar(newAvatarId)
// Notify parent about avatar change
emit('avatar-changed', newAvatarId)
}
// Watch avatarId changes
watch(() => props.avatarId, (newAvatarId) => {
if (newAvatarId) {
generateAvatar(newAvatarId)
}
}, { immediate: true })
// If no avatarId, generate deterministic one based on userId
onMounted(async () => {
if (!props.avatarId && props.userId) {
// Build deterministic ID from userId
const fallbackSeed = props.userId
await generateAvatar(fallbackSeed)
}
})
</script>

View File

@@ -0,0 +1,46 @@
<script setup lang="ts">
const props = withDefaults(defineProps<{
shown: number
total?: number | null
canLoadMore?: boolean
loading?: boolean
pageSize?: number
itemLabel?: string
}>(), {
total: null,
canLoadMore: false,
loading: false,
pageSize: 12,
itemLabel: 'элементов',
})
const emit = defineEmits<{
loadMore: []
}>()
const summaryText = computed(() => {
if (typeof props.total === 'number') {
return `Показано ${props.shown} из ${props.total} ${props.itemLabel}`
}
return `Загружено ${props.shown} ${props.itemLabel}`
})
const buttonText = computed(() => props.loading ? 'Загружаем...' : `Загрузить ещё ${props.pageSize}`)
</script>
<template>
<div class="flex flex-col items-center gap-3 rounded-[28px] bg-white/82 px-5 py-5 text-center text-sm text-[#6f6353] shadow-none">
<p>{{ summaryText }}</p>
<button
v-if="canLoadMore"
type="button"
class="btn rounded-full border-0 bg-[#2f2418] px-6 text-white hover:bg-[#493824]"
:disabled="loading"
@click="emit('loadMore')"
>
<span v-if="loading" class="loading loading-spinner loading-sm" />
{{ buttonText }}
</button>
</div>
</template>

View File

@@ -369,7 +369,7 @@ const props = withDefaults(defineProps<{
userAvatarSvg?: string
userName?: string
userInitials?: string
theme?: 'cupcake' | 'night'
theme?: 'silk' | 'night'
userData?: {
id?: string
activeTeam?: { name?: string; teamType?: string }

View File

@@ -1,12 +1,12 @@
<template>
<nav v-if="items.length > 0" class="bg-base-100 shadow-sm">
<div class="flex items-center gap-1 py-2 px-4 lg:px-6 overflow-x-auto">
<nav v-if="items.length > 0" class="mx-auto mt-2 w-full max-w-[2200px] px-3 md:px-4">
<div class="flex items-center gap-1 overflow-x-auto rounded-[24px] border border-[#e2d8ca] bg-[#efe6d8]/92 px-3 py-2 shadow-[0_14px_34px_rgba(47,36,24,0.08)] backdrop-blur">
<NuxtLink
v-for="item in items"
:key="item.path"
:to="localePath(item.path)"
class="px-4 py-2 rounded-full text-sm font-medium transition-colors whitespace-nowrap text-base-content/70 hover:text-base-content hover:bg-base-200"
:class="{ 'text-primary bg-primary/10': isActive(item.path) }"
class="rounded-full px-4 py-2 text-sm font-bold whitespace-nowrap transition-colors text-[#5f4b33] hover:bg-[#f8f3ec]"
:class="{ 'bg-[#2f2418] text-white shadow-[0_10px_24px_rgba(47,36,24,0.16)]': isActive(item.path) }"
>
{{ item.label }}
</NuxtLink>
@@ -49,4 +49,3 @@ const isActive = (path: string) => {
return route.path === localePath(path) || route.path.startsWith(localePath(path) + '/')
}
</script>

View File

@@ -0,0 +1,267 @@
<script setup lang="ts">
type CalendarCheckpoint = {
code: string
name: string
plannedDate?: string | null
actualDate?: string | null
completed: boolean
current: boolean
}
type CalendarOrder = {
id: string
status: string
quotationId?: string | null
totalAmount: number
currency: string
createdAt: string
pickupDate?: string | null
fromAddress: {
city: string
country: string
}
toAddress: {
city: string
country: string
}
currentCheckpoint?: {
code: string
name: string
plannedDate?: string | null
completed: boolean
current: boolean
} | null
checkpoints: CalendarCheckpoint[]
}
const props = defineProps<{
orders: CalendarOrder[]
}>()
const emit = defineEmits<{
select: [orderId: string]
}>()
const weekdayLabels = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']
function startOfMonthUtc(value: Date) {
return new Date(Date.UTC(value.getUTCFullYear(), value.getUTCMonth(), 1))
}
function addMonthsUtc(value: Date, diff: number) {
return new Date(Date.UTC(value.getUTCFullYear(), value.getUTCMonth() + diff, 1))
}
function formatDateKey(value: Date) {
return value.toISOString().slice(0, 10)
}
function toDateOnlyDate(value?: string | null) {
if (!value) return null
const normalized = String(value).trim()
if (!normalized) return null
const date = new Date(`${normalized}T00:00:00.000Z`)
if (Number.isNaN(date.getTime())) return null
return date
}
function orderAnchorCheckpoint(order: CalendarOrder) {
const plannedCurrent = order.checkpoints.find(checkpoint => checkpoint.current && checkpoint.plannedDate)
if (plannedCurrent) return plannedCurrent
const firstPending = order.checkpoints.find(checkpoint => !checkpoint.completed && checkpoint.plannedDate)
if (firstPending) return firstPending
const lastPlanned = [...order.checkpoints].reverse().find(checkpoint => checkpoint.plannedDate)
if (lastPlanned) return lastPlanned
return order.currentCheckpoint || null
}
function orderCalendarDate(order: CalendarOrder) {
const checkpointDate = orderAnchorCheckpoint(order)?.plannedDate
if (checkpointDate) return checkpointDate
if (order.pickupDate) return order.pickupDate
return order.createdAt.slice(0, 10)
}
function orderCalendarLabel(order: CalendarOrder) {
const checkpoint = orderAnchorCheckpoint(order)
if (checkpoint?.name) return checkpoint.name
return 'Дата уточняется'
}
function orderPersonLabel(order: CalendarOrder) {
if (order.quotationId) {
return `Клиент ${order.quotationId.slice(-6).toUpperCase()}`
}
return `Клиент ${order.id.slice(-6).toUpperCase()}`
}
function orderAvatarSeed(order: CalendarOrder) {
return order.quotationId || order.id
}
const initialMonth = computed(() => {
const firstOrderDate = props.orders
.map(order => toDateOnlyDate(orderCalendarDate(order)))
.find(Boolean)
return startOfMonthUtc(firstOrderDate || new Date())
})
const visibleMonth = ref(startOfMonthUtc(initialMonth.value))
watch(initialMonth, (nextValue) => {
visibleMonth.value = startOfMonthUtc(nextValue)
})
const monthLabel = computed(() => {
return new Intl.DateTimeFormat('ru-RU', {
month: 'long',
year: 'numeric',
timeZone: 'UTC',
}).format(visibleMonth.value)
})
const calendarOrdersByDay = computed(() => {
return props.orders.reduce<Record<string, CalendarOrder[]>>((acc, order) => {
const dateKey = orderCalendarDate(order)
if (!dateKey) return acc
if (!acc[dateKey]) acc[dateKey] = []
acc[dateKey].push(order)
return acc
}, {})
})
const monthCells = computed(() => {
const firstDay = visibleMonth.value
const firstWeekday = (firstDay.getUTCDay() + 6) % 7
const gridStart = new Date(firstDay.getTime())
gridStart.setUTCDate(gridStart.getUTCDate() - firstWeekday)
return Array.from({ length: 42 }, (_, index) => {
const date = new Date(gridStart.getTime())
date.setUTCDate(gridStart.getUTCDate() + index)
const dateKey = formatDateKey(date)
return {
key: dateKey,
date,
dateKey,
inCurrentMonth: date.getUTCMonth() === visibleMonth.value.getUTCMonth(),
isToday: dateKey === formatDateKey(new Date()),
orders: calendarOrdersByDay.value[dateKey] || [],
}
})
})
function previousMonth() {
visibleMonth.value = addMonthsUtc(visibleMonth.value, -1)
}
function nextMonth() {
visibleMonth.value = addMonthsUtc(visibleMonth.value, 1)
}
function openOrder(orderId: string) {
emit('select', orderId)
}
</script>
<template>
<section class="rounded-[28px] bg-white p-4 md:p-5">
<div class="flex items-center justify-between gap-3">
<div>
<p class="text-xs font-bold uppercase tracking-[0.14em] text-[#8c7b67]">Orders calendar</p>
<p class="mt-1 text-lg font-black capitalize text-[#2f2418]">{{ monthLabel }}</p>
</div>
<div class="inline-flex items-center rounded-full bg-[#f6f1ea] p-1">
<button
type="button"
class="flex h-9 w-9 items-center justify-center rounded-full text-[#5f4b33] transition hover:bg-white"
aria-label="Previous month"
@click="previousMonth"
>
<svg viewBox="0 0 24 24" fill="none" class="h-4 w-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m15 18-6-6 6-6" />
</svg>
</button>
<button
type="button"
class="flex h-9 w-9 items-center justify-center rounded-full text-[#5f4b33] transition hover:bg-white"
aria-label="Next month"
@click="nextMonth"
>
<svg viewBox="0 0 24 24" fill="none" class="h-4 w-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m9 6 6 6-6 6" />
</svg>
</button>
</div>
</div>
<div class="mt-4 grid grid-cols-7 gap-2">
<div
v-for="weekday in weekdayLabels"
:key="weekday"
class="px-1 text-center text-[11px] font-bold uppercase tracking-[0.12em] text-[#8c7b67]"
>
{{ weekday }}
</div>
<article
v-for="cell in monthCells"
:key="cell.key"
class="flex min-h-[132px] flex-col rounded-[22px] border border-[#e6ddd1] bg-[#fbf8f4] p-2.5"
:class="[
cell.inCurrentMonth ? 'opacity-100' : 'opacity-45',
cell.isToday ? 'ring-2 ring-[#8bc7f2]/70' : '',
]"
>
<div class="mb-2 flex items-center justify-between gap-2">
<span class="text-sm font-black text-[#2f2418]">{{ cell.date.getUTCDate() }}</span>
<span v-if="cell.orders.length" class="rounded-full bg-white px-2 py-0.5 text-[10px] font-bold uppercase tracking-[0.1em] text-[#5f4b33]">
{{ cell.orders.length }}
</span>
</div>
<div class="flex flex-1 flex-col gap-1.5">
<button
v-for="order in cell.orders.slice(0, 3)"
:key="order.id"
type="button"
class="rounded-[16px] bg-white px-2.5 py-2 text-left transition hover:shadow-[0_12px_30px_rgba(38,29,18,0.14)]"
@click="openOrder(order.id)"
>
<div class="flex items-start gap-2">
<UserAvatar
:seed="orderAvatarSeed(order)"
:label="orderPersonLabel(order)"
:size="26"
/>
<div class="min-w-0">
<p class="truncate text-[11px] font-black text-[#2f2418]">
{{ orderPersonLabel(order) }}
</p>
<p class="truncate text-xs font-black text-[#2f2418]">
{{ order.fromAddress.city }} {{ order.toAddress.city }}
</p>
<p class="mt-0.5 truncate text-[11px] text-[#7c6d5d]">
{{ orderCalendarLabel(order) }}
</p>
</div>
</div>
</button>
<div
v-if="cell.orders.length > 3"
class="rounded-[16px] bg-white/70 px-2.5 py-2 text-[11px] font-semibold text-[#7c6d5d]"
>
Ещё {{ cell.orders.length - 3 }}
</div>
</div>
</article>
</div>
</section>
</template>

View File

@@ -17,22 +17,22 @@ const props = defineProps({
})
const variantMap: Record<string, string> = {
default: 'badge-neutral',
success: 'badge-success',
warning: 'badge-warning',
error: 'badge-error',
muted: 'badge-ghost',
primary: 'badge-primary',
default: 'bg-[#f6f1ea] text-[#5f4b33]',
success: 'bg-emerald-100 text-emerald-700',
warning: 'bg-amber-100 text-amber-700',
error: 'bg-rose-100 text-rose-700',
muted: 'bg-[#efe7da] text-[#8a7761]',
primary: 'bg-[#2f2418] text-white',
}
const sizeMap: Record<string, string> = {
xs: 'badge-xs',
sm: 'badge-sm',
md: 'badge-md',
xs: 'px-2 py-1 text-[10px]',
sm: 'px-3 py-1 text-xs',
md: 'px-3.5 py-1.5 text-sm',
}
const badgeClass = computed(() => {
const base = 'badge'
const base = 'inline-flex items-center rounded-full font-semibold'
const variantClass = variantMap[props.variant] || variantMap.default
const sizeClass = sizeMap[props.size] || sizeMap.sm
return [base, variantClass, sizeClass].join(' ')

View File

@@ -2,7 +2,7 @@
<component
:is="componentTag"
:type="componentType"
:class="['btn', variantClass, fullWidth ? 'w-full' : '']"
:class="[baseClass, variantClass, fullWidth ? 'w-full' : '']"
v-bind="$attrs"
>
<slot />
@@ -36,10 +36,11 @@ const componentTag = computed(() => {
return props.as || 'button'
})
const componentType = computed(() => (props.as === 'button' ? props.type : undefined))
const baseClass = 'inline-flex items-center justify-center gap-2 rounded-full border-0 px-5 py-3 text-sm font-bold transition duration-200'
const variantClass = computed(() => {
if (props.variant === 'outline') return 'btn-outline btn-primary'
if (props.variant === 'ghost') return 'btn-ghost'
return 'btn-primary'
if (props.variant === 'outline') return 'bg-transparent text-[#2f2418] ring-1 ring-[#cbbca6] hover:bg-[#f6f1ea]'
if (props.variant === 'ghost') return 'bg-[#f6f1ea] text-[#5f4b33] hover:bg-[#ece2d3]'
return 'bg-[#2f2418] text-white shadow-[0_12px_28px_rgba(47,36,24,0.16)] hover:bg-[#493824]'
})
</script>

View File

@@ -27,18 +27,18 @@ const paddingMap: Record<string, string> = {
}
const toneMap: Record<string, string> = {
default: 'bg-base-100',
muted: 'bg-base-200',
primary: 'bg-primary/10',
default: 'bg-white',
muted: 'bg-[#fbf8f4]',
primary: 'bg-[#f6f1ea]',
}
const cardClass = computed(() => {
const paddingClass = paddingMap[props.padding] || paddingMap.medium
const toneClass = toneMap[props.tone] || toneMap.default
const interactiveClass = props.interactive
? 'cursor-pointer hover:shadow-lg transition-shadow duration-200'
? 'cursor-pointer transition-[transform,box-shadow] duration-200 hover:-translate-y-0.5 hover:shadow-[0_18px_34px_rgba(62,47,26,0.12)]'
: ''
const baseClass = 'card'
const baseClass = 'rounded-[28px] border border-[#eadfce] text-[#2f2418] shadow-none'
return [baseClass, paddingClass, toneClass, interactiveClass].filter(Boolean).join(' ')
})
</script>

View File

@@ -1,9 +1,9 @@
<template>
<label v-if="label" class="w-full space-y-1">
<span class="text-base font-semibold text-base-content">{{ label }}</span>
<label v-if="label" class="w-full space-y-2">
<span class="text-sm font-bold uppercase tracking-[0.12em] text-[#8a7761]">{{ label }}</span>
<input
v-bind="$attrs"
class="input input-bordered w-full bg-base-100 text-base-content placeholder-base-content/60"
class="h-12 w-full rounded-full border-0 bg-[#f6f1ea] px-5 text-[#2f2418] shadow-none outline-none placeholder:text-[#9b8d79]"
:value="modelValue"
@input="onInput"
/>
@@ -11,7 +11,7 @@
<input
v-else
v-bind="$attrs"
class="input input-bordered w-full bg-base-100 text-base-content placeholder-base-content/60"
class="h-12 w-full rounded-full border-0 bg-[#f6f1ea] px-5 text-[#2f2418] shadow-none outline-none placeholder:text-[#9b8d79]"
:value="modelValue"
@input="onInput"
/>

View File

@@ -1,8 +1,9 @@
<template>
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div class="space-y-1">
<h1 class="text-2xl lg:text-3xl font-bold text-base-content">{{ title }}</h1>
<p v-if="description" class="text-base-content/70">{{ description }}</p>
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div class="space-y-2">
<p class="text-xs font-bold uppercase tracking-[0.16em] text-[#8c7b67]">Workspace</p>
<h1 class="text-2xl font-black text-[#2f2418] lg:text-3xl">{{ title }}</h1>
<p v-if="description" class="max-w-[720px] text-sm leading-6 text-[#6f6353]">{{ description }}</p>
</div>
<div v-if="$slots.actions || actions?.length" class="flex items-center gap-2 flex-shrink-0">
<slot name="actions">

View File

@@ -2,7 +2,7 @@
<component
:is="to ? NuxtLink : 'button'"
:to="to"
class="btn btn-sm btn-ghost gap-2"
class="inline-flex items-center gap-2 rounded-full bg-[#f6f1ea] px-4 py-2 text-sm font-bold text-[#5f4b33] transition hover:bg-[#ece2d3]"
@click="!to && $emit('click')"
>
<Icon v-if="icon" :name="icon" size="16" />

View File

@@ -21,20 +21,20 @@ const props = defineProps({
})
const variantMap: Record<string, string> = {
neutral: 'badge-neutral',
primary: 'badge-primary',
outline: 'badge-outline',
inverse: 'badge-ghost bg-white/10 text-white border border-white/40',
neutral: 'bg-[#f6f1ea] text-[#5f4b33]',
primary: 'bg-[#2f2418] text-white',
outline: 'bg-transparent text-[#2f2418] ring-1 ring-[#cbbca6]',
inverse: 'bg-white/10 text-white border border-white/40',
}
const toneMap: Record<string, string> = {
default: '',
success: 'badge-success',
warning: 'badge-warning',
success: 'bg-emerald-100 text-emerald-700',
warning: 'bg-amber-100 text-amber-700',
}
const pillClass = computed(() => {
const base = ['badge', props.size === 'sm' ? 'badge-sm' : 'badge-md']
const base = ['inline-flex items-center rounded-full font-semibold', props.size === 'sm' ? 'px-3 py-1 text-xs' : 'px-3.5 py-1.5 text-sm']
const variantClass = variantMap[props.variant] || variantMap.neutral
const toneClass = toneMap[props.tone] || ''
return [base, variantClass, toneClass].flat().filter(Boolean).join(' ')

View File

@@ -17,8 +17,8 @@ const props = defineProps({
})
const variantMap: Record<string, string> = {
default: 'bg-base-200 text-base-content',
hero: 'bg-primary text-primary-content rounded-box overflow-hidden px-6',
default: 'rounded-[28px] bg-white px-6 text-[#2f2418] shadow-none',
hero: 'overflow-hidden rounded-[34px] bg-[#10223b] px-6 text-white shadow-[0_22px_54px_rgba(16,34,59,0.24)]',
plain: '',
}

View File

@@ -1,5 +1,5 @@
<template>
<select v-bind="$attrs" class="select select-bordered w-full">
<select v-bind="$attrs" class="h-12 w-full rounded-full border-0 bg-[#f6f1ea] px-5 text-[#2f2418] shadow-none outline-none">
<slot />
</select>
</template>

View File

@@ -1,6 +1,6 @@
<template>
<label v-if="label" class="w-full space-y-1">
<span class="text-base font-semibold text-base-content">{{ label }}</span>
<label v-if="label" class="w-full space-y-2">
<span class="text-sm font-bold uppercase tracking-[0.12em] text-[#8a7761]">{{ label }}</span>
<textarea
v-bind="$attrs"
:class="fieldClass"
@@ -39,6 +39,6 @@ const onInput = (event: Event) => {
}
const fieldClass = computed(() =>
['textarea textarea-bordered w-full min-h-[120px]', props.mono ? 'font-mono' : ''].join(' ')
['w-full min-h-[120px] rounded-[24px] border-0 bg-[#f6f1ea] px-5 py-4 text-[#2f2418] shadow-none outline-none placeholder:text-[#9b8d79]', props.mono ? 'font-mono' : ''].join(' ')
)
</script>