Introdução
O componente Calendário é responsável pela visualização
centralizada das avaliações de todas as disciplinas de um semestre. Este
componente foi desenhado para oferecer uma visão global, filtrável e
interativa das avaliações, integrando dados dinâmicos provenientes de
ficheiros ucs.json e mantendo consistência visual com o
resto da interface do utilizador.
- Visualização mensal de avaliações com navegação entre meses.
- Vista alternativa em lista cronológica de todas as avaliações.
- Vista heatmap semestral, representando intensidade de carga académica.
- Filtros por disciplina com seleção múltipla e legenda de cores.
- Integração direta com dados JSON de unidades curriculares.
- Comportamento responsivo e acessível, com foco em desempenho.
Instalação
Instalar dependências
npm install date-fns tailwindcss lucide-vue-nextImportar e registar o componente
import CalendarComponent from "@/components/Calendar.vue";
O componente foi desenvolvido para Vue 3 com suporte a
Composition API. Não é compatível com Vue 2 sem alterações.
Certifica-te de que o teu projeto utiliza vue@^3.3+
e
vite ou
nuxt 3.
Utilização Base
O exemplo seguinte demonstra a utilização mínima do componente de
calendário. Por omissão, este apresenta-se em modo de visualização
calendar (mensal), carregando internamente os dados das
disciplinas e avaliações.
<template>
<div class="p-6">
<Calendar />
</div>
</template>
<script setup lang="ts">
import Calendar from '@/components/ui/calendar'
</script>
O componente não requer propriedades obrigatórias. Caso não
sejam fornecidos dados externos, ele inicializa-se com o estado
interno e os valores padrão definidos na sua configuração. É, no
entanto, possível passar explicitamente listas de disciplinas e
avaliações através das
props descritas na secção Propriedades e Eventos.
Propriedades e Eventos
A API do componente foi desenhada para ser flexível, podes utilizar o calendário com dados internos (padrão) ou passar dados e controladores externos para integração avançada. Abaixo está a lista exaustiva de propriedades (props), eventos (emits) e métodos expostos.
- Props:
viewMode,disciplinasData,initialDate,locale,confige mais. - Emits / Callbacks:
day-click,month-change,filter-change. - Métodos: possibilidade de aceder a funções
reativas via
ref(ex.:goToDate,refreshData).
Assinatura TypeScript
// Tipos principais usados pelo componente
type Avaliacao = {
data: string; // ISO date 'YYYY-MM-DD'
descricao: string;
disciplina?: string;
slug?: string;
};
type Disciplina = {
slug: string;
nome: string;
avaliacoes: Avaliacao[];
};
interface CalendarConfig {
firstWeekDay?: number; // 0 = Domingo, 1 = Segunda (padrão usado: 1)
semesterRange?: { startMonth: number; endMonth: number }; // ex: { startMonth: 8, endMonth: 1 } para set-fev
heatmapBucketCount?: number; // número de níveis de cor
maxHeatmapWeeks?: number;
dateAdapter?: 'native' | 'date-fns' | 'dayjs';
}
// Props expostas
interface CalendarProps {
viewMode?: 'calendar' | 'list' | 'heatmap';
disciplinasData?: Disciplina[];
initialDate?: string | Date;
locale?: string; // ex: 'pt-PT'
config?: CalendarConfig;
showLegend?: boolean;
compact?: boolean;
// Callbacks
onDayClick?: (date: string, avaliacoes: Avaliacao[]) => void;
onViewChange?: (view: 'calendar' | 'list' | 'heatmap') => void;
onFilterChange?: (selectedSlugs: string[]) => void;
}
// Métodos expostos via ref (opcional)
interface CalendarHandle {
goToDate: (date: string | Date) => void;
nextMonth: () => void;
previousMonth: () => void;
refreshData: () => Promise<void>;
}Props detalhadas
| Prop | Tipo | Default | Descrição |
|---|---|---|---|
| viewMode | 'calendar' | 'list' | 'heatmap' | 'calendar' | Modo de visualização inicial. Pode ser controlado externamente para implementar layouts específicos. |
| disciplinasData | Array<Disciplina> | undefined |
Fonte dos dados, se não fornecida, o componente
carrega ucs.json por defeito. Estrutura:
{ slug, nome, avaliacoes }.
|
| initialDate | string | Date | new Date() | Data inicial exibida no calendário. Aceita ISO string ('YYYY-MM-DD') ou objeto Date. |
| locale | string | 'pt-PT' |
Localização para formatação de datas. Usado em toLocaleDateString e títulos.
|
| config | CalendarConfig | Parâmetros avançados: definição do primeiro dia da semana, intervalo do semestre, comportamento do heatmap, e adaptador de datas. | |
| showLegend | boolean | true | Controla a visibilidade da legenda de cores das disciplinas. |
| compact | boolean | false | Modo compacto para inserção em sidebars ou painéis menores (reduz padding e textos). |
Eventos / Callbacks
- day-click — disparado quando o utilizador clica
num dia com avaliações. Callback:
(date: string, avaliacoes: Avaliacao[]) => void. - month-change — disparado quando o mês alterou (navegação
por anterior/próximo). Callback:
(year: number, month: number) => void. - filter-change — disparado sempre que as disciplinas
selecionadas são alteradas. Callback:
(selectedSlugs: string[]) => void. - view-change — disparado quando o utilizador muda
o modo de visualização (calendar/list/heatmap). Callback:
(view: 'calendar' | 'list' | 'heatmap') => void.
Exemplo de utilização controlada (props + emits)
<template>
<Calendar
:viewMode="view"
:disciplinasData="disciplinas"
:initialDate="initialDate"
@day-click="onDayClick"
@filter-change="onFilterChange"
@view-change="onViewChange"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import CalendarComponent from "@/components/Calendar.vue";
import disciplinas from '@/data/ucs.json'
const view = ref<'calendar'|'list'|'heatmap'>('calendar')
const initialDate = ref(new Date())
function onDayClick(date, avaliacoes) {
// abre um modal personalizado com informações complementares
console.log('Dia clicado', date, avaliacoes)
}
function onFilterChange(selected) {
console.log('Disciplinas selecionadas', selected)
}
function onViewChange(v) {
view.value = v
}
</script>
Se passares disciplinasData como prop, considera fornecer
uma cópia imutável ou utilizar um mecanismo de refresh no componente
para evitar que alterações externas desalinhem estados internos (por
exemplo: seleção de disciplinas). O componente aceita actualizações
reativas, mas algumas optimizações (memoização de mapas por slug)
podem exigir uma chamada a refreshData().
Componentes Internos
O componente principal é composto por vários subcomponentes e unidades lógicas que suportam a experiência: Painel de filtros, Vista de Calendário (grid de dias), Vista de Lista, Heatmap Semestral, Diálogo de detalhe e utilitários auxiliares. Abaixo descrevemos cada um deles, com responsabilidades, API interna e pontos de atenção.
Filtros de Disciplinas
O painel de filtros permite selecionar quais as disciplinas que aparecem nas vistas. É composto por um componente de selecção (Checkbox), um botão para seleccionar/desseleccionar todas e uma área scrollable com a lista de disciplinas.
Responsabilidades
- Manter o estado de seleção por slug.
- Expor um estado "todasSelecionadas" (boolean) e "algumasSelecionadas" (indeterminado).
-
Emitir
filter-changecom a lista de slugs seleccionados. - Mostrar um indicador numérico de avaliações por disciplina e por mês (estatísticas).
Props internas
-
disciplinas: Disciplina[]— lista completa. -
selectedMap: Record<string, boolean>— mapa de selecção por slug. -
onToggle(slug: string)— callback para alternar seleção.
Exemplo: implementação minimal (pseudo)
<template>
<div class="filters">
<Checkbox v-model="allSelected" :indeterminate="someSelected" />
<div class="list">
<div v-for="d in disciplinas" :key="d.slug" class="item">
<Checkbox :id="d.slug" v-model="selectedMap[d.slug]" />
<label :for="d.slug">{{ d.nome }} ({{ d.avaliacoes.length }})</label>
</div>
</div>
</div>
</template>
<script setup lang="ts">
defineProps({
disciplinas: Array,
selectedMap: Object,
})
// lógica: allSelected, someSelected, watchers para emitir filter-change
</script>
As checkboxes devem usar atributos aria-checked
e labels associados com for. Em dispositivos
móveis, o painel de filtros abre como Sheet para
ocupar ecrã completo.
Vista: Calendário (Grid Mensal)
A vista de calendário é responsável por:
- Gerar a grelha de dias do mês com placeholders para os dias anteriores ao primeiro dia do mês.
- Exibir micro-blocos que representam avaliações por dia, com limite de itens visíveis e indicador "+N".
- Destacar o dia actual e aplicar estilos de foco/hover para interacção.
- Expor clique no dia para abrir o diálogo de detalhes.
Algoritmos importantes
- getFirstDayOfMonth(date): calcula deslocamento para alinhar segunda-feira como primeiro dia da semana (consistente com PT).
- calendarDays: constrói o array de (Date|null) com placeholders.
- getAvaliacoesForDate(date): lookup por string ISO, optimizado com um Map<dateStr, Avaliacao[]> para O(1) por dia.
Estrutura do DOM e classes utilitárias
Cada célula do calendário é um botão semânticamente interativo (ou div com role="button") com:
-
aria-labelcom data formatada e número de avaliações. - Indicadores visuais por disciplina (bolinhas ou badges).
- Classes responsivas para versão mobile/desktop.
// Optimização sugerida (pseudo)
const avaliacoesMap = computed(() => {
const map = new Map()
avaliacoes.forEach(a => {
map.set(a.data, (map.get(a.data) || []).concat(a))
})
return map
})
function getAvaliacoesForDate(date) {
const key = date.toISOString().split('T')[0]
return avaliacoesMap.value.get(key) || []
}
Para meses com elevada densidade de avaliações (ex.:
centenas por mês), evita renderizações desnecessárias: usa v-for com :key estável, memoiza lookups e limita o número
de items renderizados por célula.
Vista: Lista Cronológica
A vista em lista é destinada a leitura sequencial, útil para estudantes que querem ver as próximas avaliações ordenadas por data e agrupadas por mês.
Comportamento
- Ordena por data ascendente (mais próximo primeiro).
- Agrupa por mês (ex.: "Setembro 2025").
- Cada item apresenta: dia, mês abreviado, nome da disciplina, descrição e badge opcional.
- Suporta infinite-scroll ou altura fixa com ScrollArea para usabilidade em listas longas.
// Agrupar por mês (já implementado no componente)
const agruparPorMes = (lista) => {
const grupos = {}
lista.forEach(a => {
const data = new Date(a.data + 'T00:00:00')
const mesAno = data.toLocaleDateString('pt-PT', { month: 'long', year: 'numeric' })
if (!grupos[mesAno]) grupos[mesAno] = []
grupos[mesAno].push(a)
})
return grupos
}Vista: Heatmap Semestral
O heatmap apresenta semanas como linhas (ou colunas, dependendo da implementação) e dias como células, colore cada célula segundo o número de avaliações desse dia. É orientado para identificar picos de carga.
Construção do semestre
O heatmap é gerado com base no período de um semestre académico
definido por startMonth e endMonth. A
implementação calcula todas as semanas entre as datas de início
e fim e preenche dias nulos com placeholders.
Intensidade e buckets
A cor de cada célula é determinada por um valor normalizado: intensity = count / maxCount. O componente fornece utilitários para mapear essa intensidade
para classes de CSS (ex: bg-chart-1/30).
function getHeatmapIntensity(count: number, max: number) {
if (count === 0) return 'bg-muted'
const intensity = count / max
if (intensity <= 0.25) return 'bg-chart-1/30'
if (intensity <= 0.5) return 'bg-chart-1/50'
if (intensity <= 0.75) return 'bg-chart-1/70'
return 'bg-chart-1'
}Para manter a legibilidade do heatmap em ecrãs pequenos, mostra apenas marcadores numéricos ao hover/tap e fornece a legenda de intensidades mais em baixo.
Dialog / Details
O pop-up de detalhe é invocado ao clicar num dia com avaliações.
É implementado com o componente Dialog
e apresenta a lista de avaliações com os campos: disciplina, descrição
e data formatada.
Requisitos de acessibilidade
- Foco inicial no título do pop-up.
-
Fecho com
Escapee botão explícito de fechar. -
Leitura linear por leitores de ecrã; elementos com
role="dialog"earia-modal="true".
<Dialog v-model:open="dialogOpen">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ selectedDateFormatted }}</DialogTitle>
</DialogHeader>
<div v-if="avaliacoes.length">
<div v-for="a in avaliacoes" :key="a.descricao" class="p-3 border rounded">
<h4 class="font-semibold">{{ a.disciplina }}</h4>
<p class="text-sm">{{ a.descricao }}</p>
<p class="text-xs text-muted-foreground">{{ formatarData(a.data) }}</p>
</div>
</div>
</DialogContent>
</Dialog>Utilitários e Helpers
O componente inclui vários helpers pequenas funções que encapsulam lógica de datas, formatação e mapear disciplinas para cores.
Principais funções
-
formatarData(dateStr)— formata para 'dd de mês de aaaa' em pt-PT. -
formatarDataRelativa(dateStr)— devolve 'hoje', 'amanhã', 'há X dias', 'daqui a X semanas', etc. -
getCorDisciplina(slug)— devolve uma classe CSS colorida com mapeamento estável por índice. -
agruparPorMes(lista)— agrupa avaliações por label 'Mês Ano'.
// Exemplo: formatarData
function formatarData(dataStr: string) {
const data = new Date(dataStr + 'T00:00:00')
return data.toLocaleDateString('pt-PT', {
day: '2-digit',
month: 'long',
year: 'numeric',
})
}Estados e Lógica Interna
O componente Calendário é altamente reativo. Toda a
sua arquitetura interna é baseada em
ref, computed e watch da Composition
API. O estado principal é mínimo e derivável, permitindo uma renderização
previsível e consistente, mesmo sob carga intensa (ex.: dezenas de disciplinas
e centenas de avaliações).
- currentDate: data atual exibida (mês/ano corrente).
- viewMode: modo de visualização ativo — 'calendar', 'list' ou 'heatmap'.
- selectedSlugs: disciplinas atualmente visíveis no filtro.
- avaliacoesFiltradas: avaliações já filtradas, usadas nas vistas.
- avaliacoesMap: cache (Map) de avaliações por data.
- dialogState: estado reativo de abertura/fecho e data selecionada.
- config: configuração normalizada, combinando defaults com props.
Estrutura geral do script setup
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import { parseISO, format, addMonths, subMonths } from 'date-fns'
import { pt } from 'date-fns/locale'
import type { Disciplina, Avaliacao, CalendarConfig } from './types'
const props = defineProps<CalendarProps>()
const emit = defineEmits(['day-click', 'month-change', 'filter-change', 'view-change'])
// Estado principal
const currentDate = ref(props.initialDate ? new Date(props.initialDate) : new Date())
const viewMode = ref(props.viewMode || 'calendar')
const selectedSlugs = ref<string[]>([])
const dialogOpen = ref(false)
const selectedDay = ref<string | null>(null)
// Computed: configuração normalizada
const config = computed<CalendarConfig>(() => ({
firstWeekDay: props.config?.firstWeekDay ?? 1,
semesterRange: props.config?.semesterRange ?? { startMonth: 8, endMonth: 1 },
dateAdapter: props.config?.dateAdapter ?? 'date-fns',
heatmapBucketCount: props.config?.heatmapBucketCount ?? 5,
maxHeatmapWeeks: props.config?.maxHeatmapWeeks ?? 24,
}))
// Computed: disciplinas ativas
const disciplinasAtivas = computed(() => {
if (!props.disciplinasData) return []
if (selectedSlugs.value.length === 0) return props.disciplinasData
return props.disciplinasData.filter((d) => selectedSlugs.value.includes(d.slug))
})
// Computed: avaliações filtradas
const avaliacoesFiltradas = computed<Avaliacao[]>(() => {
return disciplinasAtivas.value.flatMap((d) => d.avaliacoes)
})
// Cache: mapa de avaliações por data (ISO)
const avaliacoesMap = computed(() => {
const map = new Map<string, Avaliacao[]>()
avaliacoesFiltradas.value.forEach((a) => {
if (!map.has(a.data)) map.set(a.data, [])
map.get(a.data)!.push(a)
})
return map
})
// Watchers
watch(viewMode, (v) => emit('view-change', v))
watch(selectedSlugs, (s) => emit('filter-change', s))
function goToNextMonth() {
currentDate.value = addMonths(currentDate.value, 1)
emit('month-change', currentDate.value.getFullYear(), currentDate.value.getMonth() + 1)
}
function goToPreviousMonth() {
currentDate.value = subMonths(currentDate.value, 1)
emit('month-change', currentDate.value.getFullYear(), currentDate.value.getMonth() + 1)
}
function onDayClick(dateStr: string) {
const avaliacoes = avaliacoesMap.value.get(dateStr) || []
emit('day-click', dateStr, avaliacoes)
selectedDay.value = dateStr
dialogOpen.value = true
}
</script>Gestão de estado derivado
Nenhum dado é duplicado. O estado derivado é obtido dinamicamente, minimizando inconsistências:
-
avaliacoesFiltradasdepende dedisciplinasAtivas. -
avaliacoesMapdepende deavaliacoesFiltradas. -
heatmapDatadepende deavaliacoesMapeconfig.
Esta hierarquia reativa assegura que o calendário nunca precisa
de deep watch em listas complexas, todas as dependências
são computadas a partir de fontes únicas, o que evita loops ou actualizações
desnecessárias.
Decisões de Design
O desenho do componente foi orientado por princípios de consistência visual, acessibilidade, desempenho e manutenção. As decisões abaixo documentam os principais trade-offs técnicos e UX considerados.
1. Consistência visual
Foi seguida a linha de design dos componentes shadcn/ui, priorizando legibilidade e hierarquia visual: cores neutras, foco
claro e interacções suaves. O calendário herda as variáveis do tema
definido.
2. Acessibilidade (A11Y)
-
Todos os elementos interativos possuem
roleearia-label. - Navegação por teclado: setas movem o foco entre dias, Enter ativa o diálogo.
- Dialog implementado com foco cíclico e retorno automático após fecho.
3. Desempenho
-
Pré-cálculo de mapas de avaliações (
Map<string, Avaliacao[]>) para lookup O(1). -
Uso intensivo de
computedem vez de watchers manuais. - Lazy rendering em heatmap e lista (v-if condicional fora da viewport).
4. UX e microinterações
A navegação mensal é suave: o calendário realiza uma transição animada entre meses, evitando o salto abrupto. As células de dia são clicáveis, mas também focáveis, garantindo que o utilizador pode navegar só com teclado.
5. Modularidade
Todas as vistas (CalendarView, ListView, HeatmapView) são componentes independentes. Isto permite manter responsabilidades isoladas e facilita futuras extensões (ex.: "WeekView" ou "AgendaView").
Evita inserir lógica de dados (fetch, transformação) diretamente
na UI. O calendário deve receber dados prontos ou delegar o
carregamento ao módulo de integração (ucs.json).
Integração com o Projeto
O componente está desenhado para integrar-se diretamente com os
dados definidos em
ucs.json, um ficheiro que contém a estrutura de todas
as unidades curriculares, incluindo avaliações, datas e metadados
adicionais.
Estrutura do ficheiro ucs.json
[
{
"slug": "matematica-i",
"nome": "Matemática I",
"avaliacoes": [
{ "data": "2025-09-24", "descricao": "Teste 1" },
{ "data": "2025-11-02", "descricao": "Exame final" }
]
},
{
"slug": "fisica-i",
"nome": "Física I",
"avaliacoes": [
{ "data": "2025-10-13", "descricao": "Mini-teste" }
]
}
]
O formato é simples, mas consistente. Cada disciplina é
identificada por um
slug único, usado internamente no calendário para gerar
cores e filtros estáveis.
Importação e validação
O ficheiro é importado diretamente via módulo estático, o que permite bundling e tipagem. Contudo, pode também ser carregado dinamicamente (fetch). O calendário valida a estrutura básica dos dados antes de processá-los.
import ucsData from '@/data/ucs.json'
function validarUCS(data: any): data is Disciplina[] {
if (!Array.isArray(data)) return false
return data.every((uc) => uc.slug && uc.nome && Array.isArray(uc.avaliacoes))
}
if (!validarUCS(ucsData)) {
console.error('Estrutura de ucs.json inválida')
} else {
console.log('Dados validados:', ucsData.length, 'disciplinas')
}Não é necessário validar profundamente cada campo, mas garantir que os campos estruturais principais estão presentes evita erros de parsing e datas inválidas.
Exemplo de integração direta
<template>
<div class="p-6">
<Calendar :disciplinasData="ucsData" @day-click="abrirDetalhes" />
</div>
</template>
<script setup lang="ts">
import CalendarComponent from "@/components/Calendar.vue";
import ucsData from '@/data/ucs.json'
function abrirDetalhes(date, avaliacoes) {
console.log('Avaliações nesse dia:', date, avaliacoes)
}
</script>
Este exemplo mostra a integração típica no projeto, o componente
é reativo e funciona imediatamente ao receber a estrutura ucs.json.
Boas Práticas e Recomendações
O componente foi projetado para ser reutilizável, extensível e fácil de manter. As boas práticas abaixo ajudam a garantir consistência e longevidade no projeto.
1. Tipagem e Segurança
Define sempre os tipos Disciplina, Avaliacao e CalendarConfig explicitamente. Isto evita erros silenciosos
e melhora a autocompletação no editor.
2. Comunicação de Eventos
Usa defineEmits com eventos semanticamente claros. Evita
enviar objetos excessivamente aninhados ou dependentes de estado interno.
3. Acessibilidade
- Evita dependências de mouse-only, testa com teclado.
- Usa labels descritivos e feedback auditivo (screen readers).
- Verifica contraste de cores e tamanhos de fonte.
4. Desempenho e Lazy Loading
Carrega apenas a vista necessária, v-if sobre o modo ativo.
O heatmap e lista não devem ser montados até o utilizador os ativar.
5. Persistência e Preferências
Podes armazenar as preferências de filtro e modo de visualização no localStorage (embora para já não seja implementado nativamente):
watch(viewMode, (v) => localStorage.setItem('calendar-view', v))
onMounted(() => {
const saved = localStorage.getItem('calendar-view')
if (saved) viewMode.value = saved
})
À medida que o número de disciplinas cresce, o calendário
mantém-se estável porque usa computed e Map para operações de O(1). O heatmap, em particular, escala para mais
de 500 avaliações sem perda de fluidez perceptível.
7. Deploy e Internacionalização
Usa date-fns com locale configurável. Para novos idiomas,
basta importar e ajustar o locale:
import { es } from 'date-fns/locale'
const config = { ...defaultConfig, locale: es }
Todos os subcomponentes do calendário aceitam class
e style props para override. Assim, é possível adaptá-los
ao tema da instituição sem alterar a lógica base.
Extensões Futuras / Roadmap
O Calendário foi concebido como base sólida de visualização e gestão de avaliações. No entanto, existem diversas áreas de expansão já mapeadas para versões futuras, alinhadas com a evolução do ecossistema Vue e das necessidades.
1. Vistas adicionais
- WeekView: visão semanal compacta, ideal para planeamento detalhado.
- AgendaView: listagem contínua de avaliações futuras, agrupadas por disciplina.
- TimelineView: linha temporal horizontal com marcações por tipo de avaliação.
2. Sincronização externa
-
Exportação para
iCalendar (.ics),Google Calendare sincronização bidirecional. - Integração com APIs institucionais (ex.: SIGARRA, Moodle, Google Classroom).
- Webhook para notificações automáticas de alterações de datas.
3. Colaboração
Permitir que professores e estudantes adicionem anotações ou lembretes pessoais diretamente no calendário, sem afetar os dados oficiais.
4. Inteligência Adaptativa
Planeia-se a introdução de um módulo preditivo, capaz de sugerir datas de estudo ideais com base na densidade de avaliações e desempenho histórico do aluno.
5. Extensão para dispositivos móveis
- Gestos de swipe para navegar entre meses.
- Modo compacto adaptativo para ecrãs pequenos.
- Cache local e modo offline (PWA-ready).
6. Observabilidade e Telemetria
Implementação futura de métricas de uso anónimas, recolhendo eventos como mudança de vista, número médio de avaliações carregadas e frequência de acessos. Estas métricas ajudarão a priorizar otimizações reais, baseadas em uso.
Cada extensão relacionada deve ser lançada sob um sistema de
versionamento semântico:
major.minor.patch. As alterações de API pública
futuramente serão documentadas em changelogs no diretório docs/changelogs/.
7. Interoperabilidade futura
Uma das metas de médio prazo é converter o núcleo do calendário num
componente web independente (<academic-calendar>), permitindo a integração com projetos não-Vue, como Astro puro,
React ou Svelte.
Versão do documento: 1.0.0 • Última atualização: Outubro 2025 — Infoloom