This commit is contained in:
Leonardo
2026-03-06 06:37:13 -03:00
parent d58dc21297
commit f733db8436
146 changed files with 43436 additions and 12779 deletions
File diff suppressed because it is too large Load Diff
@@ -1,12 +1,15 @@
<!-- src/features/agenda/components/AgendaClinicMosaic.vue -->
<script setup>
import { computed, ref, watch, nextTick } from 'vue'
import FullCalendar from '@fullcalendar/vue3'
import timeGridPlugin from '@fullcalendar/timegrid'
import dayGridPlugin from '@fullcalendar/daygrid'
import interactionPlugin from '@fullcalendar/interaction'
import ptBrLocale from '@fullcalendar/core/locales/pt-br'
const props = defineProps({
view: { type: String, default: 'day' }, // 'day' | 'week'
view: { type: String, default: 'day' }, // 'day' | 'week' | 'month'
mode: { type: String, default: 'work_hours' }, // 'full_24h' | 'work_hours'
timezone: { type: String, default: 'America/Sao_Paulo' },
@@ -22,33 +25,63 @@ const props = defineProps({
loading: { type: Boolean, default: false },
// controla quantas colunas "visíveis" por vez (resto vai por scroll horizontal)
minColWidth: { type: Number, default: 360 }
// largura mínima de cada coluna (terapeutas)
minColWidth: { type: Number, default: 360 },
// ✅ coluna da clínica
showClinicColumn: { type: Boolean, default: true },
clinicId: { type: String, default: '' },
clinicTitle: { type: String, default: 'Clínica' },
clinicSubtitle: { type: String, default: 'Agenda da clínica' },
// subtitle terapeutas
staffSubtitle: { type: String, default: 'Visão diária operacional' }
})
// ✅ rangeChange = mudança de range (carregar eventos)
// ✅ slotSelect = seleção de intervalo em uma coluna específica (criar evento)
// ✅ eventClick/Drop/Resize = ações em evento
const emit = defineEmits(['rangeChange', 'slotSelect', 'eventClick', 'eventDrop', 'eventResize'])
const emit = defineEmits([
'rangeChange',
'slotSelect',
'eventClick',
'eventDrop',
'eventResize',
// ✅ debug
'debugColumn'
])
const calendarRefs = ref([])
function setCalendarRef (el, idx) {
if (!el) return
calendarRefs.value[idx] = el
}
const initialView = computed(() => (props.view === 'week' ? 'timeGridWeek' : 'timeGridDay'))
const initialView = computed(() => {
if (props.view === 'week') return 'timeGridWeek'
if (props.view === 'month') return 'dayGridMonth'
return 'timeGridDay'
})
const computedSlotMinTime = computed(() => (props.mode === 'full_24h' ? '00:00:00' : props.slotMinTime))
// ✅ 23:59:59 para evitar edge-case de 24:00:00
const computedSlotMaxTime = computed(() => (props.mode === 'full_24h' ? '23:59:59' : props.slotMaxTime))
// ✅ coluna fixa (clínica)
const clinicColumn = computed(() => {
if (!props.showClinicColumn) return null
const id = String(props.clinicId || '').trim()
if (!id) return null
return { id, title: props.clinicTitle || 'Clínica', __kind: 'clinic' }
})
const staffColumns = computed(() => {
const base = Array.isArray(props.staff) ? props.staff : []
return base
.filter(s => s?.id)
.map(s => ({ ...s, __kind: 'staff' }))
})
function apiAt (idx) {
const fc = calendarRefs.value[idx]
return fc?.getApi?.()
}
function forEachApi (fn) {
for (let i = 0; i < calendarRefs.value.length; i++) {
const api = apiAt(i)
@@ -59,18 +92,24 @@ function forEachApi (fn) {
function goToday () { forEachApi(api => api.today()) }
function prev () { forEachApi(api => api.prev()) }
function next () { forEachApi(api => api.next()) }
function setView (v) {
const target = v === 'week' ? 'timeGridWeek' : 'timeGridDay'
forEachApi(api => api.changeView(target))
function gotoDate (date) {
if (!date) return
const dt = (date instanceof Date) ? new Date(date) : new Date(date)
dt.setHours(12, 0, 0, 0) // anti “voltar dia”
forEachApi(api => api.gotoDate(dt))
}
defineExpose({ goToday, prev, next, setView })
function setView (v) {
const target = v === 'week' ? 'timeGridWeek' : (v === 'month' ? 'dayGridMonth' : 'timeGridDay')
forEachApi(api => api.changeView(target))
}
function setMode () {}
defineExpose({ goToday, prev, next, gotoDate, setView, setMode })
// Eventos por profissional (owner)
function eventsFor (ownerId) {
const list = props.events || []
return list.filter(e => e?.extendedProps?.owner_id === ownerId)
return list.filter(e => String(e?.extendedProps?.owner_id || '') === String(ownerId || ''))
}
// ---- range sync ----
@@ -82,35 +121,93 @@ function onDatesSet (arg) {
if (key === lastRangeKey) return
lastRangeKey = key
// dispara carregamento no pai
emit('rangeChange', {
start: arg.start,
end: arg.end,
startStr: arg.startStr,
endStr: arg.endStr,
viewType: arg.view.type
viewType: arg.view.type,
currentDate: arg.view?.currentStart || arg.start
})
// mantém todos os calendários na mesma data
if (suppressSync) return
suppressSync = true
const masterDate = arg.start
const masterDate = arg.view?.currentStart || arg.start
forEachApi((api) => {
const cur = api.view?.currentStart
if (!cur) return
if (!cur || !masterDate) return
if (cur.getTime() !== masterDate.getTime()) api.gotoDate(masterDate)
})
// libera no próximo tick (evita loops)
Promise.resolve().then(() => { suppressSync = false })
}
// Se trocar view, garante que todos estão no mesmo
watch(() => props.view, async () => {
await nextTick()
setView(props.view)
})
// ---------- helpers UI ----------
function colSubtitle (p) {
return p?.__kind === 'clinic' ? props.clinicSubtitle : props.staffSubtitle
}
// ✅ debug emitter (cabeçalho clicável)
function emitDebug (col) {
emit('debugColumn', {
staffCol: col,
staffUserId: col?.id || null,
staffTitle: col?.title || null,
kind: col?.__kind || null,
at: new Date().toISOString()
})
}
function buildFcOptions (ownerId) {
const base = {
plugins: [timeGridPlugin, dayGridPlugin, interactionPlugin],
locale: ptBrLocale,
timeZone: props.timezone,
headerToolbar: false,
initialView: initialView.value,
nowIndicator: true,
editable: true,
selectable: true,
selectMirror: true,
slotDuration: props.slotDuration,
slotMinTime: computedSlotMinTime.value,
slotMaxTime: computedSlotMaxTime.value,
height: 'auto',
expandRows: true,
allDaySlot: false,
events: eventsFor(ownerId),
datesSet: onDatesSet,
eventClick: (info) => emit('eventClick', info),
eventDrop: (info) => emit('eventDrop', info),
eventResize: (info) => emit('eventResize', info)
}
base.select = (selection) => {
emit('slotSelect', {
ownerId,
start: selection.start,
end: selection.end,
startStr: selection.startStr,
endStr: selection.endStr,
jsEvent: selection.jsEvent || null,
viewType: selection.view?.type || initialView.value
})
}
return base
}
</script>
<template>
@@ -119,74 +216,105 @@ watch(() => props.view, async () => {
Carregando agenda da clínica
</div>
<!-- Mosaic -->
<div
class="p-2 md:p-3 overflow-x-auto"
:style="{ display: 'grid', gridAutoFlow: 'column', gridAutoColumns: `minmax(${minColWidth}px, 1fr)`, gap: '12px' }"
>
<div
v-for="(p, idx) in staff"
:key="p.id"
class="rounded-[1.25rem] border border-[var(--surface-border)] bg-[color-mix(in_srgb,var(--surface-card),transparent_12%)] overflow-hidden"
>
<!-- Header da coluna -->
<div class="p-3 border-b border-[var(--surface-border)] flex items-center justify-between gap-2">
<div class="min-w-0">
<div class="font-semibold truncate">{{ p.title }}</div>
<div class="text-xs opacity-70 truncate">Visão diária operacional</div>
<div class="mosaic-shell">
<!-- Coluna fixa: Clínica -->
<div v-if="clinicColumn" class="mosaic-fixed">
<div class="mosaic-col">
<div class="mosaic-col-head cursor-pointer" @click="emitDebug(clinicColumn)" title="Debug desta coluna">
<div class="min-w-0">
<div class="font-semibold truncate">{{ clinicColumn.title }}</div>
<div class="text-xs opacity-70 truncate">{{ colSubtitle(clinicColumn) }}</div>
</div>
<div class="text-xs opacity-70 whitespace-nowrap">
{{ mode === 'full_24h' ? '24h' : 'Horário' }}
</div>
</div>
<div class="text-xs opacity-70 whitespace-nowrap">
{{ mode === 'full_24h' ? '24h' : 'Horário' }}
<div class="p-2">
<FullCalendar
:ref="(el) => setCalendarRef(el, 0)"
:options="buildFcOptions(clinicColumn.id)"
/>
</div>
</div>
</div>
<div class="p-2">
<FullCalendar
:ref="(el) => setCalendarRef(el, idx)"
:options="{
plugins: [timeGridPlugin, interactionPlugin],
initialView: initialView,
timeZone: timezone,
<!-- Área rolável: Terapeutas -->
<div class="mosaic-scroll">
<div
class="mosaic-grid"
:style="{ gridAutoColumns: `minmax(${minColWidth}px, 1fr)` }"
>
<div
v-for="(p, sIdx) in staffColumns"
:key="p.id"
class="mosaic-col"
>
<div class="mosaic-col-head cursor-pointer" @click="emitDebug(p)" title="Debug desta coluna">
<div class="min-w-0">
<div class="font-semibold truncate">{{ p.title }}</div>
<div class="text-xs opacity-70 truncate">{{ colSubtitle(p) }}</div>
</div>
<div class="text-xs opacity-70 whitespace-nowrap">
{{ mode === 'full_24h' ? '24h' : 'Horário' }}
</div>
</div>
headerToolbar: false,
nowIndicator: true,
editable: true,
// ✅ seleção para criar evento (por coluna)
selectable: true,
selectMirror: true,
select: (selection) => {
emit('slotSelect', {
ownerId: p.id,
start: selection.start,
end: selection.end,
startStr: selection.startStr,
endStr: selection.endStr,
jsEvent: selection.jsEvent || null,
viewType: selection.view?.type || initialView
})
},
slotDuration: slotDuration,
slotMinTime: computedSlotMinTime,
slotMaxTime: computedSlotMaxTime,
height: 'auto',
expandRows: true,
allDaySlot: false,
events: eventsFor(p.id),
datesSet: onDatesSet,
eventClick: (info) => emit('eventClick', info),
eventDrop: (info) => emit('eventDrop', info),
eventResize: (info) => emit('eventResize', info)
}"
/>
<div class="p-2">
<FullCalendar
:ref="(el) => setCalendarRef(el, (clinicColumn ? (sIdx + 1) : sIdx))"
:options="buildFcOptions(p.id)"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
</template>
<style scoped>
.mosaic-shell{
display:flex;
gap:12px;
padding: 8px;
}
@media (min-width: 768px){
.mosaic-shell{ padding: 12px; }
}
.mosaic-fixed{
flex: 0 0 auto;
width: 420px;
min-width: 320px;
max-width: 460px;
}
.mosaic-scroll{
flex: 1 1 auto;
min-width: 0;
overflow-x: auto;
}
.mosaic-grid{
display:grid;
grid-auto-flow: column;
gap:12px;
}
.mosaic-col{
border-radius: 1.25rem;
border: 1px solid var(--surface-border);
background: color-mix(in_srgb, var(--surface-card), transparent 12%);
overflow:hidden;
}
.mosaic-col-head{
padding: 12px;
border-bottom: 1px solid var(--surface-border);
display:flex;
align-items:center;
justify-content: space-between;
gap: 8px;
}
</style>
File diff suppressed because it is too large Load Diff
@@ -1,8 +1,5 @@
<!-- src/features/agenda/components/AgendaRightPanel.vue -->
<script setup>
import Card from 'primevue/card'
import Divider from 'primevue/divider'
import Button from 'primevue/button'
const props = defineProps({
title: { type: String, default: 'Painel' },
@@ -1,13 +1,7 @@
<script setup>
import { computed, ref, watch } from 'vue'
import Button from 'primevue/button'
import SelectButton from 'primevue/selectbutton'
import ToggleButton from 'primevue/togglebutton'
import FloatLabel from 'primevue/floatlabel'
import IconField from 'primevue/iconfield'
import InputIcon from 'primevue/inputicon'
import InputText from 'primevue/inputtext'
const props = defineProps({
title: { type: String, default: 'Agenda' },
@@ -0,0 +1,523 @@
<template>
<Dialog
v-model:visible="visible"
modal
:draggable="false"
:closable="!saving"
:dismissableMask="!saving"
class="dc-dialog w-[96vw] max-w-2xl"
:pt="{ content: { class: 'p-0' }, header: { class: 'pb-0' } }"
>
<template #header>
<div class="flex w-full items-center justify-between gap-3 px-1">
<div class="flex items-center gap-3 min-w-0">
<!-- Dot de cor -->
<span
class="dc-header-dot shrink-0"
:style="{ backgroundColor: previewBgColor }"
/>
<div class="min-w-0">
<div class="text-base font-semibold truncate">
{{ form.name || (mode === 'create' ? 'Novo compromisso' : 'Editar compromisso') }}
</div>
<div class="text-xs opacity-50">
{{ mode === 'create' ? 'Novo tipo de compromisso' : 'Editando tipo de compromisso' }}
</div>
</div>
</div>
<div class="flex items-center gap-2 shrink-0">
<Button
v-if="mode === 'edit' && canDelete"
icon="pi pi-trash"
severity="danger"
text
rounded
:disabled="saving"
v-tooltip.top="'Excluir'"
@click="emitDelete"
/>
<Button
label="Cancelar"
severity="secondary"
outlined
class="rounded-full"
:disabled="saving"
@click="close"
/>
<Button
label="Salvar"
icon="pi pi-check"
class="rounded-full"
:loading="saving"
:disabled="!canSubmit"
@click="submit"
/>
</div>
</div>
</template>
<!-- Banner de preview -->
<div
class="dc-banner"
:style="{ backgroundColor: previewBgColor }"
>
<span
class="dc-banner__pill"
:style="{ color: form.text_color || '#ffffff' }"
>
{{ form.name || 'Nome do compromisso' }}
</span>
</div>
<!-- Corpo -->
<div class="flex flex-col gap-4 p-4">
<!-- Nome + Ativo -->
<div class="flex items-center gap-3">
<div class="flex-1">
<FloatLabel variant="on">
<IconField>
<InputIcon class="pi pi-tag" />
<InputText
id="cr-nome"
v-model="form.name"
class="w-full"
variant="filled"
:disabled="saving || isEditLocked"
@keydown.enter.prevent="submit"
/>
</IconField>
<label for="cr-nome">Nome *</label>
</FloatLabel>
</div>
<div class="flex items-center gap-2 shrink-0 pt-1">
<span class="text-sm font-medium">Ativo</span>
<InputSwitch v-model="form.active" :disabled="saving || isActiveLocked" />
</div>
</div>
<!-- Seção Cor -->
<div class="dc-section">
<div class="dc-section__label">Cor</div>
<!-- Paleta predefinida -->
<div class="dc-palette">
<button
v-for="p in presetColors"
:key="p.bg"
class="dc-swatch"
:class="{ 'dc-swatch--active': form.bg_color === p.bg }"
:style="{ backgroundColor: `#${p.bg}` }"
:title="p.name"
:disabled="saving || isEditLocked"
@click="applyPreset(p)"
>
<i v-if="form.bg_color === p.bg" class="pi pi-check dc-swatch__check" />
</button>
<!-- Custom ColorPicker -->
<div class="dc-swatch dc-swatch--custom" title="Cor personalizada">
<ColorPicker
v-model="form.bg_color"
format="hex"
:disabled="saving || isEditLocked"
/>
</div>
</div>
<!-- Texto -->
<div class="flex items-center gap-3 mt-2">
<span class="text-xs font-medium opacity-60 uppercase tracking-wide">Texto</span>
<div class="flex gap-1">
<button
class="dc-text-opt"
:class="{ 'dc-text-opt--active': form.text_color === '#ffffff' }"
:disabled="saving || isEditLocked"
@click="form.text_color = '#ffffff'"
>
<span class="dc-text-opt__dot" style="background:#ffffff; border: 1px solid #ccc;" />
Branco
</button>
<button
class="dc-text-opt"
:class="{ 'dc-text-opt--active': form.text_color === '#000000' }"
:disabled="saving || isEditLocked"
@click="form.text_color = '#000000'"
>
<span class="dc-text-opt__dot" style="background:#000000;" />
Preto
</button>
</div>
</div>
</div>
<!-- Descrição -->
<FloatLabel variant="on">
<Textarea
id="cr-descricao"
v-model="form.description"
autoResize
rows="2"
class="w-full"
variant="filled"
:disabled="saving || isEditLocked"
/>
<label for="cr-descricao">Descrição</label>
</FloatLabel>
<!-- Campos adicionais -->
<div class="dc-section">
<div class="flex items-center justify-between gap-2 mb-3">
<div class="dc-section__label mb-0">Campos adicionais</div>
<Button
label="Adicionar campo"
icon="pi pi-plus"
severity="secondary"
outlined
size="small"
class="rounded-full"
:disabled="saving || isFieldsLocked"
@click="addField"
/>
</div>
<div v-if="!form.fields.length" class="py-3 text-center text-sm opacity-50">
Nenhum campo adicional configurado.
</div>
<div v-else class="flex flex-col gap-2">
<div
v-for="(f, idx) in form.fields"
:key="f.key"
class="grid grid-cols-1 gap-2 rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-0)] p-3 md:grid-cols-12"
>
<div class="md:col-span-6">
<FloatLabel variant="on">
<InputText
:id="`cr-field-label-${idx}`"
v-model="f.label"
class="w-full"
variant="filled"
:disabled="saving || isFieldsLocked"
@keydown.enter.prevent="submit"
@blur="syncKey(f)"
/>
<label :for="`cr-field-label-${idx}`">Nome do campo</label>
</FloatLabel>
</div>
<div class="md:col-span-4">
<FloatLabel variant="on">
<Dropdown
:id="`cr-field-type-${idx}`"
v-model="f.type"
:options="fieldTypeOptions"
optionLabel="label"
optionValue="value"
class="w-full"
variant="filled"
:disabled="saving || isFieldsLocked"
/>
<label :for="`cr-field-type-${idx}`">Tipo</label>
</FloatLabel>
</div>
<div class="md:col-span-2 flex items-center justify-end">
<Button
icon="pi pi-trash"
severity="danger"
text
rounded
:disabled="saving || isFieldsLocked"
@click="removeField(idx)"
/>
</div>
<div class="md:col-span-12 text-xs opacity-40 font-mono">
key: {{ f.key }}
</div>
</div>
</div>
</div>
</div>
</Dialog>
</template>
<script setup>
import { computed, reactive, watch } from 'vue'
import Textarea from 'primevue/textarea'
import Dropdown from 'primevue/dropdown'
import InputSwitch from 'primevue/inputswitch'
import ColorPicker from 'primevue/colorpicker'
const props = defineProps({
modelValue: { type: Boolean, default: false },
mode: { type: String, default: 'create' }, // 'create' | 'edit'
saving: { type: Boolean, default: false },
commitment: { type: Object, default: null } // quando edit
})
const emit = defineEmits(['update:modelValue', 'save', 'delete'])
const fieldTypeOptions = [
{ label: 'Texto', value: 'text' },
{ label: 'Texto longo', value: 'textarea' }
]
const presetColors = [
{ bg: '6366f1', text: '#ffffff', name: 'Índigo' },
{ bg: '8b5cf6', text: '#ffffff', name: 'Violeta' },
{ bg: 'ec4899', text: '#ffffff', name: 'Rosa' },
{ bg: 'ef4444', text: '#ffffff', name: 'Vermelho' },
{ bg: 'f97316', text: '#ffffff', name: 'Laranja' },
{ bg: 'eab308', text: '#000000', name: 'Amarelo' },
{ bg: '22c55e', text: '#ffffff', name: 'Verde' },
{ bg: '14b8a6', text: '#ffffff', name: 'Teal' },
{ bg: '3b82f6', text: '#ffffff', name: 'Azul' },
{ bg: '06b6d4', text: '#ffffff', name: 'Ciano' },
{ bg: '64748b', text: '#ffffff', name: 'Ardósia' },
{ bg: '292524', text: '#ffffff', name: 'Escuro' },
]
function applyPreset (p) {
if (props.saving) return
form.bg_color = p.bg
form.text_color = p.text
}
const visible = computed({
get: () => props.modelValue,
set: (v) => emit('update:modelValue', v)
})
const form = reactive({
id: null,
name: '',
description: '',
native: false,
locked: false,
active: true,
bg_color: '6366f1',
text_color: '#ffffff',
fields: []
})
const previewBgColor = computed(() => {
if (!form.bg_color) return '#6366f1'
return form.bg_color.startsWith('#') ? form.bg_color : `#${form.bg_color}`
})
watch(
() => props.modelValue,
(open) => {
if (!open) return
hydrate()
}
)
watch(
() => props.commitment,
() => {
if (!props.modelValue) return
hydrate()
}
)
function hydrate () {
const c = props.commitment
if (props.mode === 'edit' && c) {
form.id = c.id
form.name = c.name || ''
form.description = c.description || ''
form.native = !!c.native
form.locked = !!c.locked
form.active = !!c.active
form.bg_color = c.bg_color || '6366f1'
form.text_color = c.text_color || '#ffffff'
form.fields = Array.isArray(c.fields) ? JSON.parse(JSON.stringify(c.fields)) : []
} else {
form.id = null
form.name = ''
form.description = ''
form.native = false
form.locked = false
form.active = true
form.bg_color = '6366f1'
form.text_color = '#ffffff'
form.fields = []
}
}
const isActiveLocked = computed(() => !!form.locked) // nativo+locked → sempre ativo, nunca pode desativar
const isEditLocked = computed(() => false) // edição sempre permitida
const isFieldsLocked = computed(() => false) // campos sempre editáveis
const canDelete = computed(() => !form.native)
const canSubmit = computed(() => {
if (props.saving) return false
if (!String(form.name || '').trim()) return false
return true
})
function close () {
if (props.saving) return
visible.value = false
}
function submit () {
if (!canSubmit.value) return
const payload = {
id: form.id,
name: String(form.name || '').trim(),
description: String(form.description || '').trim(),
active: form.locked ? true : !!form.active,
bg_color: form.bg_color || null,
text_color: form.text_color || null,
fields: (form.fields || []).map(f => ({
key: f.key,
label: String(f.label || '').trim() || 'Campo',
type: f.type === 'textarea' ? 'textarea' : 'text',
required: !!f.required
}))
}
emit('save', payload)
}
function emitDelete () {
if (props.saving) return
emit('delete', { id: form.id })
visible.value = false
}
function addField () {
const base = `campo-${form.fields.length + 1}`
form.fields.push({
key: makeKey(base),
label: 'Observação',
type: 'textarea',
required: false
})
}
function removeField (idx) {
form.fields.splice(idx, 1)
}
function syncKey (field) {
// se o user renomear, a key acompanha (sem quebrar: simples por enquanto)
const next = makeKey(field.label)
field.key = next
}
function makeKey (label) {
const k = String(label || '')
.trim()
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/[^a-z0-9]+/g, '_')
.replace(/(^_|_$)/g, '') || `field_${Math.random().toString(16).slice(2, 8)}`
return k
}
</script>
<style scoped>
/* ── Header ─────────────────────────────── */
.dc-header-dot {
width: 14px; height: 14px;
border-radius: 50%;
border: 2px solid rgba(255,255,255,0.3);
box-shadow: 0 0 0 3px rgba(0,0,0,0.08);
transition: background-color 0.2s ease;
}
/* ── Banner de preview ───────────────────── */
.dc-banner {
height: 72px;
display: flex; align-items: center; justify-content: center;
transition: background-color 0.25s ease;
}
.dc-banner__pill {
font-size: 1rem; font-weight: 700; letter-spacing: -0.02em;
padding: 0.35rem 1.1rem;
background: rgba(0,0,0,0.15);
border-radius: 999px;
backdrop-filter: blur(4px);
transition: color 0.2s ease;
}
/* ── Section ─────────────────────────────── */
.dc-section {
border: 1px solid var(--surface-border);
border-radius: 1.25rem;
background: var(--surface-card);
padding: 1rem;
}
.dc-section__label {
font-size: 0.7rem; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.06em;
opacity: 0.45; margin-bottom: 0.75rem;
}
/* ── Paleta ──────────────────────────────── */
.dc-palette {
display: flex; flex-wrap: wrap; gap: 0.45rem;
}
.dc-swatch {
width: 28px; height: 28px;
border-radius: 50%;
border: 2px solid transparent;
display: grid; place-items: center;
cursor: pointer;
transition: transform 0.12s ease, box-shadow 0.12s ease, border-color 0.12s ease;
position: relative;
}
.dc-swatch:hover:not(:disabled) {
transform: scale(1.18);
box-shadow: 0 3px 10px rgba(0,0,0,0.2);
}
.dc-swatch--active {
border-color: var(--surface-0, #fff);
box-shadow: 0 0 0 2px var(--text-color);
}
.dc-swatch__check {
font-size: 0.6rem; color: #fff; font-weight: 900;
}
.dc-swatch--custom {
background: conic-gradient(red, yellow, lime, cyan, blue, magenta, red);
overflow: hidden;
}
.dc-swatch--custom :deep(.p-colorpicker-preview) {
width: 100%; height: 100%;
border: none; border-radius: 50%;
opacity: 0;
}
/* ── Texto toggle ────────────────────────── */
.dc-text-opt {
display: inline-flex; align-items: center; gap: 0.4rem;
padding: 0.25rem 0.75rem;
border-radius: 999px;
border: 1px solid var(--surface-border);
font-size: 0.8rem; font-weight: 500;
cursor: pointer;
color: var(--text-color);
background: transparent;
transition: background 0.12s, border-color 0.12s;
}
.dc-text-opt:hover:not(:disabled) { background: var(--surface-hover); }
.dc-text-opt--active {
background: var(--surface-section, var(--surface-100));
border-color: var(--primary-color);
color: var(--primary-color);
font-weight: 700;
}
.dc-text-opt__dot {
width: 10px; height: 10px; border-radius: 50%; display: inline-block;
}
</style>
@@ -2,13 +2,6 @@
<script setup>
import { computed, ref, watch } from 'vue'
import Card from 'primevue/card'
import Button from 'primevue/button'
import Divider from 'primevue/divider'
import SelectButton from 'primevue/selectbutton'
import InputText from 'primevue/inputtext'
import FloatLabel from 'primevue/floatlabel'
import Tag from 'primevue/tag'
const props = defineProps({
title: { type: String, default: 'Agenda' },
@@ -2,10 +2,6 @@
<script setup>
import { computed } from 'vue'
import Card from 'primevue/card'
import Button from 'primevue/button'
import Divider from 'primevue/divider'
import Tag from 'primevue/tag'
const props = defineProps({
stats: { type: Object, default: () => ({}) }