Added default layout to project

* Added default layout for the site

* App is in light mode by default.

* App is in light mode by default.
This commit is contained in:
Liviu Burcusel 2025-11-12 12:50:56 +01:00 committed by GitHub
parent 9a4dd4b721
commit d1967f718e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 1660 additions and 39 deletions

22
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,22 @@
{
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue"
]
}

View file

@ -1,8 +1,8 @@
<template>
<div>
<NuxtRouteAnnouncer />
<div class="w-full min-h-screen flex justify-center items-top">
<NuxtLayout>
<NuxtPage />
</div>
</NuxtLayout>
</div>
</template>

BIN
app/assets/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -0,0 +1,24 @@
html {
height: 100%;
font-size: 14px;
line-height: 1.2;
}
body {
font-family: Ubuntu, Arial, Helvetica, sans-serif;
color: var(--text-color);
background-color: var(--surface-ground);
margin: 0;
padding: 0;
min-height: 100%;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
text-decoration: none;
}
.layout-wrapper {
min-height: 100vh;
}

View file

@ -0,0 +1,8 @@
.layout-footer {
display: flex;
align-items: center;
justify-content: center;
padding: 1rem 0 1rem 0;
gap: 0.5rem;
border-top: 1px solid var(--surface-border);
}

View file

@ -0,0 +1,14 @@
.layout-main-container {
display: flex;
flex-direction: column;
min-height: 100vh;
justify-content: space-between;
padding: 6rem 2rem 0 2rem;
background-color: var(--surface-ground);
transition: margin-left var(--layout-section-transition-duration);
}
.layout-main {
flex: 1 1 auto;
padding-bottom: 2rem;
}

View file

@ -0,0 +1,160 @@
@use "mixins" as *;
.layout-sidebar {
position: fixed;
width: 20rem;
height: calc(100vh - 8rem);
z-index: 999;
overflow-y: auto;
user-select: none;
top: 6rem;
left: 2rem;
transition:
transform var(--layout-section-transition-duration),
left var(--layout-section-transition-duration);
background-color: var(--surface-overlay);
border-radius: var(--content-border-radius);
padding: 0.5rem 1.5rem;
}
.layout-menu {
margin: 0;
padding: 0;
list-style-type: none;
.layout-root-menuitem {
> .layout-menuitem-root-text {
font-size: 0.857rem;
text-transform: uppercase;
font-weight: 700;
color: var(--text-color);
margin: 0.75rem 0;
}
> a {
display: none;
}
}
a {
user-select: none;
&.active-menuitem {
> .layout-submenu-toggler {
transform: rotate(-180deg);
}
}
}
li.active-menuitem {
> a {
.layout-submenu-toggler {
transform: rotate(-180deg);
}
}
}
ul {
margin: 0;
padding: 0;
list-style-type: none;
a {
display: flex;
align-items: center;
position: relative;
outline: 0 none;
color: var(--text-color);
cursor: pointer;
padding: 0.75rem 1rem;
border-radius: var(--content-border-radius);
transition:
background-color var(--element-transition-duration),
box-shadow var(--element-transition-duration);
.layout-menuitem-icon {
margin-right: 0.5rem;
}
.layout-submenu-toggler {
font-size: 75%;
margin-left: auto;
transition: transform var(--element-transition-duration);
}
&.active-route {
font-weight: 700;
color: var(--primary-color);
}
&:hover {
background-color: var(--surface-hover);
}
&:focus {
@include focused-inset();
}
}
ul {
overflow: hidden;
border-radius: var(--content-border-radius);
li {
a {
margin-left: 1rem;
}
li {
a {
margin-left: 2rem;
}
li {
a {
margin-left: 2.5rem;
}
li {
a {
margin-left: 3rem;
}
li {
a {
margin-left: 3.5rem;
}
li {
a {
margin-left: 4rem;
}
}
}
}
}
}
}
}
}
}
.layout-submenu-enter-from,
.layout-submenu-leave-to {
max-height: 0;
}
.layout-submenu-enter-to,
.layout-submenu-leave-from {
max-height: 1000px;
}
.layout-submenu-leave-active {
overflow: hidden;
transition: max-height 0.45s cubic-bezier(0, 1, 0, 1);
}
.layout-submenu-enter-active {
overflow: hidden;
transition: max-height 1s ease-in-out;
}

View file

@ -0,0 +1,15 @@
@mixin focused() {
outline-width: var(--focus-ring-width);
outline-style: var(--focus-ring-style);
outline-color: var(--focus-ring-color);
outline-offset: var(--focus-ring-offset);
box-shadow: var(--focus-ring-shadow);
transition:
box-shadow var(--transition-duration),
outline-color var(--transition-duration);
}
@mixin focused-inset() {
outline-offset: -1px;
box-shadow: inset var(--focus-ring-shadow);
}

View file

@ -0,0 +1,48 @@
.preloader {
position: fixed;
z-index: 999999;
background: #edf1f5;
width: 100%;
height: 100%;
}
.preloader-content {
border: 0 solid transparent;
border-radius: 50%;
width: 150px;
height: 150px;
position: absolute;
top: calc(50vh - 75px);
left: calc(50vw - 75px);
}
.preloader-content:before,
.preloader-content:after {
content: "";
border: 1em solid var(--primary-color);
border-radius: 50%;
width: inherit;
height: inherit;
position: absolute;
top: 0;
left: 0;
animation: loader 2s linear infinite;
opacity: 0;
}
.preloader-content:before {
animation-delay: 0.5s;
}
@keyframes loader {
0% {
transform: scale(0);
opacity: 0;
}
50% {
opacity: 1;
}
100% {
transform: scale(1);
opacity: 0;
}
}

View file

@ -0,0 +1,110 @@
@media screen and (min-width: 1960px) {
.layout-main,
.landing-wrapper {
width: 1504px;
margin-left: auto !important;
margin-right: auto !important;
}
}
@media (min-width: 992px) {
.layout-wrapper {
&.layout-overlay {
.layout-main-container {
margin-left: 0;
padding-left: 2rem;
}
.layout-sidebar {
transform: translateX(-100%);
left: 0;
top: 0;
height: 100vh;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-right: 1px solid var(--surface-border);
transition:
transform 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99),
left 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99);
box-shadow:
0px 3px 5px rgba(0, 0, 0, 0.02),
0px 0px 2px rgba(0, 0, 0, 0.05),
0px 1px 4px rgba(0, 0, 0, 0.08);
}
&.layout-overlay-active {
.layout-sidebar {
transform: translateX(0);
}
}
}
&.layout-static {
.layout-main-container {
margin-left: 22rem;
}
&.layout-static-inactive {
.layout-sidebar {
transform: translateX(-100%);
left: 0;
}
.layout-main-container {
margin-left: 0;
padding-left: 2rem;
}
}
}
.layout-mask {
display: none;
}
}
}
@media (max-width: 991px) {
.blocked-scroll {
overflow: hidden;
}
.layout-wrapper {
.layout-main-container {
margin-left: 0;
padding-left: 2rem;
}
.layout-sidebar {
transform: translateX(-100%);
left: 0;
top: 0;
height: 100vh;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
transition:
transform 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99),
left 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99);
}
.layout-mask {
display: none;
position: fixed;
top: 0;
left: 0;
z-index: 998;
width: 100%;
height: 100%;
background-color: var(--maskbg);
}
&.layout-mobile-active {
.layout-sidebar {
transform: translateX(0);
}
.layout-mask {
display: block;
}
}
}
}

View file

@ -0,0 +1,201 @@
@use "mixins" as *;
.layout-topbar {
position: fixed;
height: 4rem;
z-index: 997;
left: 0;
top: 0;
width: 100%;
padding: 0 2rem;
background-color: var(--surface-card);
transition: left var(--layout-section-transition-duration);
display: flex;
align-items: center;
.layout-topbar-logo-container {
width: 20rem;
display: flex;
align-items: center;
}
.layout-topbar-logo {
display: inline-flex;
align-items: center;
font-size: 1.5rem;
border-radius: var(--content-border-radius);
color: var(--text-color);
font-weight: 500;
gap: 0.5rem;
img {
width: 3rem;
}
&:focus-visible {
@include focused();
}
}
.layout-topbar-action {
display: inline-flex;
justify-content: center;
align-items: center;
border-radius: 50%;
width: 2.5rem;
height: 2.5rem;
color: var(--text-color);
transition: background-color var(--element-transition-duration);
cursor: pointer;
&:hover {
background-color: var(--surface-hover);
}
&:focus-visible {
@include focused();
}
i {
font-size: 1.25rem;
}
span {
font-size: 1rem;
display: none;
}
&.layout-topbar-action-highlight {
background-color: var(--primary-color);
color: var(--primary-contrast-color);
}
}
.layout-menu-button {
margin-right: 0.5rem;
}
.layout-topbar-menu-button {
display: none;
}
.layout-topbar-actions {
margin-left: auto;
display: flex;
gap: 1rem;
}
.layout-topbar-menu-content {
display: flex;
gap: 1rem;
}
.layout-config-menu {
display: flex;
gap: 1rem;
}
}
@media (max-width: 991px) {
.layout-topbar {
padding: 0 2rem;
.layout-topbar-logo-container {
width: auto;
}
.layout-menu-button {
margin-left: 0;
margin-right: 0.5rem;
}
.layout-topbar-menu-button {
display: inline-flex;
}
.layout-topbar-menu {
position: absolute;
background-color: var(--surface-overlay);
transform-origin: top;
box-shadow:
0px 3px 5px rgba(0, 0, 0, 0.02),
0px 0px 2px rgba(0, 0, 0, 0.05),
0px 1px 4px rgba(0, 0, 0, 0.08);
border-radius: var(--content-border-radius);
padding: 1rem;
right: 2rem;
top: 4rem;
min-width: 15rem;
border: 1px solid var(--surface-border);
.layout-topbar-menu-content {
gap: 0.5rem;
}
.layout-topbar-action {
display: flex;
width: 100%;
height: auto;
justify-content: flex-start;
border-radius: var(--content-border-radius);
padding: 0.5rem 1rem;
i {
font-size: 1rem;
margin-right: 0.5rem;
}
span {
font-weight: medium;
display: block;
}
}
}
.layout-topbar-menu-content {
flex-direction: column;
}
}
}
.config-panel {
.config-panel-label {
font-size: 0.875rem;
color: var(--text-secondary-color);
font-weight: 600;
line-height: 1;
}
.config-panel-colors {
> div {
padding-top: 0.5rem;
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
justify-content: space-between;
button {
border: none;
width: 1.25rem;
height: 1.25rem;
border-radius: 50%;
padding: 0;
cursor: pointer;
outline-color: transparent;
outline-width: 2px;
outline-style: solid;
outline-offset: 1px;
&.active-color {
outline-color: var(--primary-color);
}
}
}
}
.config-panel-settings {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
}

View file

@ -0,0 +1,68 @@
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 1.5rem 0 1rem 0;
font-family: inherit;
font-weight: 700;
line-height: 1.5;
color: var(--text-color);
&:first-child {
margin-top: 0;
}
}
h1 {
font-size: 2.5rem;
}
h2 {
font-size: 2rem;
}
h3 {
font-size: 1.75rem;
}
h4 {
font-size: 1.5rem;
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
mark {
background: #fff8e1;
padding: 0.25rem 0.4rem;
border-radius: var(--content-border-radius);
font-family: monospace;
}
blockquote {
margin: 1rem 0;
padding: 0 2rem;
border-left: 4px solid #90a4ae;
}
hr {
border-top: solid var(--surface-border);
border-width: 1px 0 0 0;
margin: 1rem 0;
}
p {
margin: 0 0 1rem 0;
line-height: 1.5;
&:last-child {
margin-bottom: 0;
}
}

View file

@ -0,0 +1,25 @@
/* Utils */
.clearfix:after {
content: " ";
display: block;
clear: both;
}
.card {
background: var(--surface-card);
padding: 2rem;
margin-bottom: 2rem;
border-radius: var(--content-border-radius);
&:last-child {
margin-bottom: 0;
}
}
.p-toast {
&.p-toast-top-right,
&.p-toast-top-left,
&.p-toast-top-center {
top: 100px;
}
}

View file

@ -0,0 +1,11 @@
@use "./variables/_common";
@use "./_mixins";
@use "./_preloading";
@use "./_core";
@use "./_main";
@use "./_topbar";
@use "./_menu";
@use "./_footer";
@use "./_responsive";
@use "./_utils";
@use "./_typography";

View file

@ -0,0 +1,26 @@
:root {
--primary-color: var(--p-primary-color);
--primary-contrast-color: var(--p-primary-contrast-color);
--text-color: var(--p-text-color);
--text-color-secondary: var(--p-text-muted-color);
--surface-ground: var(--p-surface-100);
--surface-border: var(--p-content-border-color);
--surface-card: var(--p-content-background);
--surface-hover: var(--p-content-hover-background);
--surface-overlay: var(--p-overlay-popover-background);
--transition-duration: var(--p-transition-duration);
--maskbg: var(--p-mask-background);
--content-border-radius: var(--p-content-border-radius);
--layout-section-transition-duration: 0.2s;
--element-transition-duration: var(--p-transition-duration);
--focus-ring-width: var(--p-focus-ring-width);
--focus-ring-style: var(--p-focus-ring-style);
--focus-ring-color: var(--p-focus-ring-color);
--focus-ring-offset: var(--p-focus-ring-offset);
--focus-ring-shadow: var(--p-focus-ring-shadow);
}
:root[class="app-dark"] {
--p-text-color: #ffffff;
--surface-ground: var(--p-surface-950);
}

View file

@ -0,0 +1,3 @@
@use "primeicons/primeicons.css";
@use "~/assets/css/tailwind.css";
@use "~/assets/scss/layout/layout.scss";

21
app/layouts/Default.vue Normal file
View file

@ -0,0 +1,21 @@
<template>
<div class="layout-wrapper layout-static">
<default-topbar />
<default-sidebar />
<div class="layout-main-container">
<div class="layout-main">
<slot />
</div>
<default-footer />
</div>
<div class="layout-mask animate-fadein"></div>
</div>
<Toast />
</template>
<script setup lang="ts">
import Toast from "primevue/toast";
import DefaultTopbar from "~/layouts/default/Topbar.vue";
import DefaultSidebar from "~/layouts/default/Sidebar.vue";
import DefaultFooter from "~/layouts/default/Footer.vue";
</script>

View file

@ -0,0 +1,9 @@
<script setup lang="ts">
const currentYear = new Date().getFullYear();
</script>
<template>
<footer class="layout-footer font-bold">
<div v-if="currentYear === 2025">Glowing Fiesta 2025</div>
<div v-else>Glowing Fiesta 2025 - {{ currentYear }}</div>
</footer>
</template>

View file

@ -0,0 +1,35 @@
<template>
<div class="layout-sidebar">
<ul class="layout-menu">
<li class="layout-root-menuitem">
<div class="layout-menuitem-root-text">Home</div>
<ul class="layout-submenu">
<li>
<nuxt-link to="/">
<i class="pi pi-home layout-menuitem-icon"></i>
<span class="layout-menuitem-text">Dashboard</span>
</nuxt-link>
</li>
</ul>
</li>
<li class="layout-root-menuitem">
<div class="layout-menuitem-root-text">Configuration</div>
<ul class="layout-submenu">
<li>
<nuxt-link to="/config/account">
<i class="pi pi-user layout-menuitem-icon"></i>
<span class="layout-menuitem-text">Account</span>
</nuxt-link>
</li>
<li>
<nuxt-link to="/config/settings">
<i class="pi pi-cog layout-menuitem-icon"></i>
<span class="layout-menuitem-text">Settings</span>
</nuxt-link>
</li>
</ul>
</li>
</ul>
</div>
</template>

View file

@ -0,0 +1,56 @@
<script setup lang="ts">
import { ref } from "vue";
const isDarkTheme = ref(false);
const toggleDarkMode = () => {
isDarkTheme.value = !isDarkTheme.value;
document.documentElement.classList.toggle("app-dark");
};
const toggleMenu = () => {
document.getElementsByClassName("layout-wrapper")[0]?.classList.toggle("layout-static-inactive");
};
</script>
<template>
<nav class="layout-topbar">
<div class="layout-topbar-logo-container">
<button class="layout-menu-button layout-topbar-action" @click="toggleMenu">
<i class="pi pi-bars"></i>
</button>
<nuxt-link to="/" class="layout-topbar-logo">
<img src="@/assets/images/logo.png" alt="Two stick silhouettes admiring fireworks in the sky" />
<span>Glowing Fiesta</span>
</nuxt-link>
</div>
<div class="layout-topbar-actions">
<div class="layout-config-menu">
<button type="button" class="layout-topbar-action layout-topbar-action-highlight" @click="toggleDarkMode">
<i :class="['pi', { 'pi-moon': isDarkTheme, 'pi-sun': !isDarkTheme }]"></i>
</button>
</div>
<Divider layout="vertical" />
<div class="layout-topbar-menu hidden lg:block">
<div class="layout-topbar-menu-content">
<button type="button" class="layout-topbar-action">
<i class="pi pi-calendar"></i>
<span>Calendar</span>
</button>
<button type="button" class="layout-topbar-action">
<i class="pi pi-inbox"></i>
<span>Messages</span>
</button>
<button type="button" class="layout-topbar-action">
<i class="pi pi-user"></i>
<span>Profile</span>
</button>
</div>
</div>
</div>
</nav>
</template>

View file

@ -0,0 +1,5 @@
<template>
<div class="card">
<h1>Account</h1>
</div>
</template>

View file

@ -0,0 +1,5 @@
<template>
<div class="card">
<h1>Settings</h1>
</div>
</template>

View file

@ -1,21 +1,5 @@
<template>
<article class="w-3/4 flex flex-col gap-4 m-8 text-center">
<header class="mb-4">
<h1 class="w-full text-4xl font-bold">Homepage</h1>
<p class="w-full">This is your homepage</p>
</header>
<div class="article-content w-full">
<h2 class="w-full text-xl font-bold">First paragraph</h2>
<p class="w-full text-justify mb-4">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam eu ornare velit. Curabitur urna felis, malesuada et sapien
eu, tincidunt consectetur sem. Cras eu tortor nec lorem vehicula sollicitudin. Sed et pellentesque dolor, et faucibus
magna nulla.
</p>
</div>
<footer>
<Button label="Verify PrimeVue" />
</footer>
</article>
<div class="card">
<h1>Dashboard</h1>
</div>
</template>

View file

@ -5,6 +5,10 @@ import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"
export default withNuxt(eslintPluginPrettierRecommended, {
files: ["**/*.{ts,js,vue}"],
rules: {
// Vue rules
"vue/no-multiple-template-root": "off",
// Typescript rules
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-require-imports": "off",
"@typescript-eslint/no-unused-vars": "warn",

View file

@ -1,17 +1,77 @@
import { definePreset } from "@primeuix/themes";
import Aura from "@primeuix/themes/aura";
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: "2025-07-15",
devtools: { enabled: true },
modules: ["@nuxt/eslint", "@nuxtjs/tailwindcss", "@primevue/nuxt-module"],
primevue: {
options: {
ripple: true,
theme: {
preset: Aura,
options: {
darkModeSelector: "system",
const defaultPreset = definePreset(Aura, {
semantic: {
primary: {
50: "{teal.50}",
100: "{teal.100}",
200: "{teal.200}",
300: "{teal.300}",
400: "{teal.400}",
500: "{teal.500}",
600: "{teal.600}",
700: "{teal.700}",
800: "{teal.800}",
900: "{teal.900}",
950: "{teal.950}",
},
colorScheme: {
light: {
surface: {
0: "#ffffff",
50: "{slate.50}",
100: "{slate.100}",
200: "{slate.200}",
300: "{slate.300}",
400: "{slate.400}",
500: "{slate.500}",
600: "{slate.600}",
700: "{slate.700}",
800: "{slate.800}",
900: "{slate.900}",
950: "{slate.950}",
},
},
dark: {
surface: {
0: "#000000",
50: "{slate.50}",
100: "{slate.100}",
200: "{slate.200}",
300: "{slate.300}",
400: "{slate.400}",
500: "{slate.500}",
600: "{slate.600}",
700: "{slate.700}",
800: "{slate.800}",
900: "{slate.900}",
950: "{slate.950}",
},
},
},
},
});
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: "2025-07-15",
devtools: { enabled: true },
router: {
options: {
linkActiveClass: "active-route",
linkExactActiveClass: "exact-active-route",
},
},
modules: ["@nuxt/eslint", "@nuxtjs/tailwindcss", "@primevue/nuxt-module"],
css: ["~/assets/css/tailwind.css", "~/assets/scss/styles.scss"],
primevue: {
options: {
ripple: true,
theme: {
preset: defaultPreset,
options: {
darkModeSelector: ".app-dark",
},
},
},

448
package-lock.json generated
View file

@ -10,6 +10,7 @@
"@nuxtjs/tailwindcss": "^7.0.0-beta.1",
"@primeuix/themes": "^1.2.5",
"nuxt": "^4.1.3",
"primeicons": "^7.0.0",
"primevue": "^4.4.1",
"tailwindcss": "^4.1.16",
"vue": "^3.5.22",
@ -26,6 +27,7 @@
"eslint-plugin-prettier": "^5.5.4",
"happy-dom": "^20.0.8",
"prettier": "^3.6.2",
"sass-embedded": "^1.93.3",
"typescript": "^5.9.3",
"vitest": "^4.0.1"
}
@ -482,6 +484,13 @@
"node": ">=18"
}
},
"node_modules/@bufbuild/protobuf": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.10.0.tgz",
"integrity": "sha512-fdRs9PSrBF7QUntpZpq6BTw58fhgGJojgg39m9oFOJGZT+nip9b0so5cYY1oWl5pvemDLr0cPPsH46vwThEbpQ==",
"devOptional": true,
"license": "(Apache-2.0 AND BSD-3-Clause)"
},
"node_modules/@clack/core": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@clack/core/-/core-0.5.0.tgz",
@ -5839,6 +5848,13 @@
"ieee754": "^1.2.1"
}
},
"node_modules/buffer-builder": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/buffer-builder/-/buffer-builder-0.2.0.tgz",
"integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==",
"devOptional": true,
"license": "MIT/X11"
},
"node_modules/buffer-crc32": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz",
@ -6365,6 +6381,13 @@
"integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==",
"license": "MIT"
},
"node_modules/colorjs.io": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz",
"integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==",
"devOptional": true,
"license": "MIT"
},
"node_modules/commander": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
@ -8619,6 +8642,13 @@
"integrity": "sha512-3MOLanc3sb3LNGWQl1RlQlNWURE5g32aUphrDyFeCsxBTk08iE3VNe4CwsUZ0Qs1X+EfX0+r29Sxdpza4B+yRA==",
"license": "MIT"
},
"node_modules/immutable": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
"integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
"devOptional": true,
"license": "MIT"
},
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@ -11403,6 +11433,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/primeicons": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz",
"integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw==",
"license": "MIT"
},
"node_modules/primevue": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/primevue/-/primevue-4.4.1.tgz",
@ -11843,6 +11879,16 @@
"queue-microtask": "^1.2.2"
}
},
"node_modules/rxjs": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@ -11863,6 +11909,374 @@
],
"license": "MIT"
},
"node_modules/sass": {
"version": "1.93.3",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.93.3.tgz",
"integrity": "sha512-elOcIZRTM76dvxNAjqYrucTSI0teAF/L2Lv0s6f6b7FOwcwIuA357bIE871580AjHJuSvLIRUosgV+lIWx6Rgg==",
"license": "MIT",
"optional": true,
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^5.0.2",
"source-map-js": ">=0.6.2 <2.0.0"
},
"bin": {
"sass": "sass.js"
},
"engines": {
"node": ">=14.0.0"
},
"optionalDependencies": {
"@parcel/watcher": "^2.4.1"
}
},
"node_modules/sass-embedded": {
"version": "1.93.3",
"resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.93.3.tgz",
"integrity": "sha512-+VUy01yfDqNmIVMd/LLKl2TTtY0ovZN0rTonh+FhKr65mFwIYgU9WzgIZKS7U9/SPCQvWTsTGx9jyt+qRm/XFw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@bufbuild/protobuf": "^2.5.0",
"buffer-builder": "^0.2.0",
"colorjs.io": "^0.5.0",
"immutable": "^5.0.2",
"rxjs": "^7.4.0",
"supports-color": "^8.1.1",
"sync-child-process": "^1.0.2",
"varint": "^6.0.0"
},
"bin": {
"sass": "dist/bin/sass.js"
},
"engines": {
"node": ">=16.0.0"
},
"optionalDependencies": {
"sass-embedded-all-unknown": "1.93.3",
"sass-embedded-android-arm": "1.93.3",
"sass-embedded-android-arm64": "1.93.3",
"sass-embedded-android-riscv64": "1.93.3",
"sass-embedded-android-x64": "1.93.3",
"sass-embedded-darwin-arm64": "1.93.3",
"sass-embedded-darwin-x64": "1.93.3",
"sass-embedded-linux-arm": "1.93.3",
"sass-embedded-linux-arm64": "1.93.3",
"sass-embedded-linux-musl-arm": "1.93.3",
"sass-embedded-linux-musl-arm64": "1.93.3",
"sass-embedded-linux-musl-riscv64": "1.93.3",
"sass-embedded-linux-musl-x64": "1.93.3",
"sass-embedded-linux-riscv64": "1.93.3",
"sass-embedded-linux-x64": "1.93.3",
"sass-embedded-unknown-all": "1.93.3",
"sass-embedded-win32-arm64": "1.93.3",
"sass-embedded-win32-x64": "1.93.3"
}
},
"node_modules/sass-embedded-all-unknown": {
"version": "1.93.3",
"resolved": "https://registry.npmjs.org/sass-embedded-all-unknown/-/sass-embedded-all-unknown-1.93.3.tgz",
"integrity": "sha512-3okGgnE41eg+CPLtAPletu6nQ4N0ij7AeW+Sl5Km4j29XcmqZQeFwYjHe1AlKTEgLi/UAONk1O8i8/lupeKMbw==",
"cpu": [
"!arm",
"!arm64",
"!riscv64",
"!x64"
],
"license": "MIT",
"optional": true,
"dependencies": {
"sass": "1.93.3"
}
},
"node_modules/sass-embedded-android-arm": {
"version": "1.93.3",
"resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.93.3.tgz",
"integrity": "sha512-8xOw9bywfOD6Wv24BgCmgjkk6tMrsOTTHcb28KDxeJtFtoxiUyMbxo0vChpPAfp2Hyg2tFFKS60s0s4JYk+Raw==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-android-arm64": {
"version": "1.93.3",
"resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.93.3.tgz",
"integrity": "sha512-uqUl3Kt1IqdGVAcAdbmC+NwuUJy8tM+2ZnB7/zrt6WxWVShVCRdFnWR9LT8HJr7eJN7AU8kSXxaVX/gedanPsg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-android-riscv64": {
"version": "1.93.3",
"resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.93.3.tgz",
"integrity": "sha512-2jNJDmo+3qLocjWqYbXiBDnfgwrUeZgZFHJIwAefU7Fn66Ot7rsXl+XPwlokaCbTpj7eMFIqsRAZ/uDueXNCJg==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-android-x64": {
"version": "1.93.3",
"resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.93.3.tgz",
"integrity": "sha512-y0RoAU6ZenQFcjM9PjQd3cRqRTjqwSbtWLL/p68y2oFyh0QGN0+LQ826fc0ZvU/AbqCsAizkqjzOn6cRZJxTTQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-darwin-arm64": {
"version": "1.93.3",
"resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.93.3.tgz",
"integrity": "sha512-7zb/hpdMOdKteK17BOyyypemglVURd1Hdz6QGsggy60aUFfptTLQftLRg8r/xh1RbQAUKWFbYTNaM47J9yPxYg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-darwin-x64": {
"version": "1.93.3",
"resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.93.3.tgz",
"integrity": "sha512-Ek1Vp8ZDQEe327Lz0b7h3hjvWH3u9XjJiQzveq74RPpJQ2q6d9LfWpjiRRohM4qK6o4XOHw1X10OMWPXJtdtWg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-arm": {
"version": "1.93.3",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.93.3.tgz",
"integrity": "sha512-yeiv2y+dp8B4wNpd3+JsHYD0mvpXSfov7IGyQ1tMIR40qv+ROkRqYiqQvAOXf76Qwh4Y9OaYZtLpnsPjfeq6mA==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-arm64": {
"version": "1.93.3",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.93.3.tgz",
"integrity": "sha512-RBrHWgfd8Dd8w4fbmdRVXRrhh8oBAPyeWDTKAWw8ZEmuXfVl4ytjDuyxaVilh6rR1xTRTNpbaA/YWApBlLrrNw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-musl-arm": {
"version": "1.93.3",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.93.3.tgz",
"integrity": "sha512-fU0fwAwbp7sBE3h5DVU5UPzvaLg7a4yONfFWkkcCp6ZrOiPuGRHXXYriWQ0TUnWy4wE+svsVuWhwWgvlb/tkKg==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-musl-arm64": {
"version": "1.93.3",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.93.3.tgz",
"integrity": "sha512-PS829l+eUng+9W4PFclXGb4uA2+965NHV3/Sa5U7qTywjeeUUYTZg70dJHSqvhrBEfCc2XJABeW3adLJbyQYkw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-musl-riscv64": {
"version": "1.93.3",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.93.3.tgz",
"integrity": "sha512-cK1oBY+FWQquaIGEeQ5H74KTO8cWsSWwXb/WaildOO9U6wmUypTgUYKQ0o5o/29nZbWWlM1PHuwVYTSnT23Jjg==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-musl-x64": {
"version": "1.93.3",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.93.3.tgz",
"integrity": "sha512-A7wkrsHu2/I4Zpa0NMuPGkWDVV7QGGytxGyUq3opSXgAexHo/vBPlGoDXoRlSdex0cV+aTMRPjoGIfdmNlHwyg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-riscv64": {
"version": "1.93.3",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.93.3.tgz",
"integrity": "sha512-vWkW1+HTF5qcaHa6hO80gx/QfB6GGjJUP0xLbnAoY4pwEnw5ulGv6RM8qYr8IDhWfVt/KH+lhJ2ZFxnJareisQ==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-x64": {
"version": "1.93.3",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.93.3.tgz",
"integrity": "sha512-k6uFxs+e5jSuk1Y0niCwuq42F9ZC5UEP7P+RIOurIm8w/5QFa0+YqeW+BPWEW5M1FqVOsNZH3qGn4ahqvAEjPA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-unknown-all": {
"version": "1.93.3",
"resolved": "https://registry.npmjs.org/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.93.3.tgz",
"integrity": "sha512-o5wj2rLpXH0C+GJKt/VpWp6AnMsCCbfFmnMAttcrsa+U3yrs/guhZ3x55KAqqUsE8F47e3frbsDL+1OuQM5DAA==",
"license": "MIT",
"optional": true,
"os": [
"!android",
"!darwin",
"!linux",
"!win32"
],
"dependencies": {
"sass": "1.93.3"
}
},
"node_modules/sass-embedded-win32-arm64": {
"version": "1.93.3",
"resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.93.3.tgz",
"integrity": "sha512-0dOfT9moy9YmBolodwYYXtLwNr4jL4HQC9rBfv6mVrD7ud8ue2kDbn+GVzj1hEJxvEexVSmDCf7MHUTLcGs9xQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-win32-x64": {
"version": "1.93.3",
"resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.93.3.tgz",
"integrity": "sha512-wHFVfxiS9hU/sNk7KReD+lJWRp3R0SLQEX4zfOnRP2zlvI2X4IQR5aZr9GNcuMP6TmNpX0nQPZTegS8+h9RrEg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded/node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/sax": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
@ -12444,6 +12858,29 @@
"url": "https://opencollective.com/svgo"
}
},
"node_modules/sync-child-process": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz",
"integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"sync-message-port": "^1.0.0"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/sync-message-port": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.1.3.tgz",
"integrity": "sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==",
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/synckit": {
"version": "0.11.11",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz",
@ -12670,8 +13107,8 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD",
"optional": true
"devOptional": true,
"license": "0BSD"
},
"node_modules/type-check": {
"version": "0.4.0",
@ -13148,6 +13585,13 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/varint": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",
"integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==",
"devOptional": true,
"license": "MIT"
},
"node_modules/vite": {
"version": "7.1.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz",

View file

@ -18,6 +18,7 @@
"@nuxtjs/tailwindcss": "^7.0.0-beta.1",
"@primeuix/themes": "^1.2.5",
"nuxt": "^4.1.3",
"primeicons": "^7.0.0",
"primevue": "^4.4.1",
"tailwindcss": "^4.1.16",
"vue": "^3.5.22",
@ -34,6 +35,7 @@
"eslint-plugin-prettier": "^5.5.4",
"happy-dom": "^20.0.8",
"prettier": "^3.6.2",
"sass-embedded": "^1.93.3",
"typescript": "^5.9.3",
"vitest": "^4.0.1"
}

View file

@ -0,0 +1,11 @@
import { describe, expect, it } from "vitest";
import { mount, type VueWrapper } from "@vue/test-utils";
import DefaultLayout from "~/layouts/Default.vue";
describe("Default.vue", () => {
const wrapper: VueWrapper = mount(DefaultLayout, {});
it("loads without crashing", () => {
expect(wrapper.exists()).toBe(true);
});
});

View file

@ -0,0 +1,55 @@
import { afterEach, describe, expect, it } from "vitest";
import { mount, type VueWrapper } from "@vue/test-utils";
import Footer from "~/layouts/default/Footer.vue";
describe("Footer.vue", () => {
const RealDate = Date;
const mockDate = (year: number) => {
globalThis.Date = class extends RealDate {
constructor() {
super();
return new RealDate(`${year}-01-01`);
}
static now() {
return new RealDate(`${year}-01-01`).getTime();
}
};
};
afterEach(() => {
globalThis.Date = RealDate;
});
it("loads without crashing", () => {
const wrapper: VueWrapper = mount(Footer, {});
expect(wrapper.exists()).toBe(true);
expect(wrapper.find(".layout-footer").exists()).toBe(true);
expect(wrapper.find(".layout-footer").classes()).toContain("font-bold");
});
it("displays only 'Glowing Fiesta 2025' when current year is 2025", () => {
mockDate(2025);
const wrapper = mount(Footer);
expect(wrapper.text()).toBe("Glowing Fiesta 2025");
expect(wrapper.text()).not.toContain(" - ");
});
it("displays 'Glowing Fiesta 2025 - 2034' when current year is 2034", () => {
mockDate(2034);
const wrapper = mount(Footer);
expect(wrapper.text()).toBe("Glowing Fiesta 2025 - 2034");
});
it("has proper structure / content", () => {
const wrapper = mount(Footer);
const footer = wrapper.find("footer");
expect(footer.exists()).toBe(true);
expect(footer.element.tagName).toBe("FOOTER");
expect(wrapper.findAll("div")).toHaveLength(1);
});
});

View file

@ -0,0 +1,11 @@
import { describe, expect, it } from "vitest";
import { mount, type VueWrapper } from "@vue/test-utils";
import Sidebar from "~/layouts/default/Sidebar.vue";
describe("Sidebar.vue", () => {
it("loads without crashing", () => {
const wrapper: VueWrapper = mount(Sidebar, {});
expect(wrapper.exists()).toBe(true);
expect(wrapper.find(".layout-sidebar").exists()).toBe(true);
});
});

View file

@ -0,0 +1,155 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { mount, type VueWrapper } from "@vue/test-utils";
import Topbar from "~/layouts/default/Topbar.vue";
describe("Topbar.vue", () => {
let wrapper: VueWrapper;
beforeEach(() => {
document.documentElement.className = "";
document.body.innerHTML = '<div class="layout-wrapper"></div>';
wrapper = mount(Topbar);
});
afterEach(() => {
if (wrapper) {
wrapper.unmount();
}
});
describe("Component Rendering", () => {
it("loads without crashing", () => {
expect(wrapper.exists()).toBe(true);
expect(wrapper.find(".layout-topbar").exists()).toBe(true);
});
it("renders the logo image and text", () => {
const logo = wrapper.find(".layout-topbar-logo");
expect(logo.exists()).toBe(true);
expect(logo.find("img").attributes("alt")).toBe("Two stick silhouettes admiring fireworks in the sky");
expect(logo.text()).toContain("Glowing Fiesta");
});
it("renders the menu toggle button", () => {
const menuButton = wrapper.find(".layout-menu-button");
expect(menuButton.exists()).toBe(true);
expect(menuButton.find(".pi-bars").exists()).toBe(true);
});
it("renders the dark mode toggle button", () => {
const darkModeButton = wrapper.find(".layout-topbar-action-highlight");
expect(darkModeButton.exists()).toBe(true);
});
it("renders the topbar menu items", () => {
const menuButtons = wrapper.findAll(".layout-topbar-menu button");
expect(menuButtons).toHaveLength(3);
expect(menuButtons[0].text()).toContain("Calendar");
expect(menuButtons[1].text()).toContain("Messages");
expect(menuButtons[2].text()).toContain("Profile");
});
});
describe("Dark Mode Toggle", () => {
it("starts with sun icon (light mode)", () => {
const darkModeButton = wrapper.find(".layout-topbar-action-highlight");
expect(darkModeButton.find(".pi-sun").exists()).toBe(true);
expect(darkModeButton.find(".pi-moon").exists()).toBe(false);
});
it("toggles to moon icon when clicked", async () => {
const darkModeButton = wrapper.find(".layout-topbar-action-highlight");
await darkModeButton.trigger("click");
expect(darkModeButton.find(".pi-moon").exists()).toBe(true);
expect(darkModeButton.find(".pi-sun").exists()).toBe(false);
});
it("removes app-dark class when toggled off", async () => {
expect(document.documentElement.classList.contains("app-dark")).toBe(false);
const darkModeButton = wrapper.find(".layout-topbar-action-highlight");
// Toggle on
await darkModeButton.trigger("click");
expect(document.documentElement.classList.contains("app-dark")).toBe(true);
// Toggle off
await darkModeButton.trigger("click");
expect(document.documentElement.classList.contains("app-dark")).toBe(false);
});
it("updates isDarkTheme ref when toggled", async () => {
const darkModeButton = wrapper.find(".layout-topbar-action-highlight");
expect(wrapper.vm.isDarkTheme).toBe(false);
await darkModeButton.trigger("click");
expect(wrapper.vm.isDarkTheme).toBe(true);
await darkModeButton.trigger("click");
expect(wrapper.vm.isDarkTheme).toBe(false);
});
});
describe("Menu Toggle", () => {
it("toggles layout-static-inactive class on layout wrapper", async () => {
const menuButton = wrapper.find(".layout-menu-button");
const layoutWrapper = document.querySelector(".layout-wrapper");
expect(layoutWrapper?.classList.contains("layout-static-inactive")).toBe(false);
await menuButton.trigger("click");
expect(layoutWrapper?.classList.contains("layout-static-inactive")).toBe(true);
await menuButton.trigger("click");
expect(layoutWrapper?.classList.contains("layout-static-inactive")).toBe(false);
});
it("handles missing layout wrapper gracefully", async () => {
document.body.innerHTML = "";
const menuButton = wrapper.find(".layout-menu-button");
// Should not throw error
expect(() => menuButton.trigger("click")).not.toThrow();
});
});
describe("Logo Link", () => {
it("links to home page", () => {
const logoLink = wrapper.find(".layout-topbar-logo");
expect(logoLink.exists()).toBe(true);
});
it("has correct image source", () => {
const img = wrapper.find(".layout-topbar-logo img");
expect(img.attributes("src")).toContain("logo.png");
});
});
describe("Topbar Actions", () => {
it("renders Calendar action button with icon", () => {
const buttons = wrapper.findAll(".layout-topbar-menu button");
const calendarButton = buttons[0];
expect(calendarButton.find(".pi-calendar").exists()).toBe(true);
expect(calendarButton.text()).toContain("Calendar");
});
it("renders Messages action button with icon", () => {
const buttons = wrapper.findAll(".layout-topbar-menu button");
const messagesButton = buttons[1];
expect(messagesButton.find(".pi-inbox").exists()).toBe(true);
expect(messagesButton.text()).toContain("Messages");
});
it("renders Profile action button with icon", () => {
const buttons = wrapper.findAll(".layout-topbar-menu button");
const profileButton = buttons[2];
expect(profileButton.find(".pi-user").exists()).toBe(true);
expect(profileButton.text()).toContain("Profile");
});
});
});

View file

@ -0,0 +1,11 @@
import { describe, expect, it } from "vitest";
import { mount, type VueWrapper } from "@vue/test-utils";
import AccountPage from "~/pages/config/Account.vue";
describe("Account.vue", () => {
const wrapper: VueWrapper = mount(AccountPage, {});
it("loads without crashing", () => {
expect(wrapper.exists()).toBe(true);
});
});

View file

@ -0,0 +1,11 @@
import { describe, expect, it } from "vitest";
import { mount, type VueWrapper } from "@vue/test-utils";
import SettingsPage from "~/pages/config/Settings.vue";
describe("Settings.vue", () => {
const wrapper: VueWrapper = mount(SettingsPage, {});
it("loads without crashing", () => {
expect(wrapper.exists()).toBe(true);
});
});

View file

@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { mount, type VueWrapper } from "@vue/test-utils";
import IndexPage from "~/app/pages/index.vue";
import IndexPage from "~/pages/index.vue";
describe("pages/index.vue", () => {
const wrapper: VueWrapper = mount(IndexPage, {});

View file

@ -2,6 +2,7 @@ import { vi } from "vitest";
import { config } from "@vue/test-utils";
import PrimeVue from "primevue/config";
import Button from "primevue/button";
import Ripple from "primevue/ripple";
Object.defineProperty(global, "import", {
value: {
@ -13,5 +14,11 @@ Object.defineProperty(global, "import", {
});
config.global.plugins = [PrimeVue];
config.global.stubs = { NuxtPage: true };
config.global.stubs = {
NuxtLayout: true,
NuxtPage: true,
Divider: true,
NuxtLink: { template: "<a><slot /></a>" },
};
config.global.components = { Button };
config.global.directives = { Ripple };

View file

@ -38,8 +38,8 @@ export default defineConfig({
},
resolve: {
alias: {
"~": fileURLToPath(new URL("./", import.meta.url)),
"@": fileURLToPath(new URL("./", import.meta.url)),
"~": fileURLToPath(new URL("./app", import.meta.url)),
"@": fileURLToPath(new URL("./app", import.meta.url)),
},
},
});