diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..4816602
--- /dev/null
+++ b/.vscode/settings.json
@@ -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"
+ ]
+}
diff --git a/app/app.vue b/app/app.vue
index bc45a20..ed9e94c 100644
--- a/app/app.vue
+++ b/app/app.vue
@@ -1,8 +1,8 @@
diff --git a/app/assets/images/logo.png b/app/assets/images/logo.png
new file mode 100644
index 0000000..52f1bff
Binary files /dev/null and b/app/assets/images/logo.png differ
diff --git a/app/assets/scss/layout/_core.scss b/app/assets/scss/layout/_core.scss
new file mode 100644
index 0000000..e040f53
--- /dev/null
+++ b/app/assets/scss/layout/_core.scss
@@ -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;
+}
diff --git a/app/assets/scss/layout/_footer.scss b/app/assets/scss/layout/_footer.scss
new file mode 100644
index 0000000..8f96833
--- /dev/null
+++ b/app/assets/scss/layout/_footer.scss
@@ -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);
+}
diff --git a/app/assets/scss/layout/_main.scss b/app/assets/scss/layout/_main.scss
new file mode 100644
index 0000000..459bbdd
--- /dev/null
+++ b/app/assets/scss/layout/_main.scss
@@ -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;
+}
diff --git a/app/assets/scss/layout/_menu.scss b/app/assets/scss/layout/_menu.scss
new file mode 100644
index 0000000..a4a9642
--- /dev/null
+++ b/app/assets/scss/layout/_menu.scss
@@ -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;
+}
diff --git a/app/assets/scss/layout/_mixins.scss b/app/assets/scss/layout/_mixins.scss
new file mode 100644
index 0000000..6256ad9
--- /dev/null
+++ b/app/assets/scss/layout/_mixins.scss
@@ -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);
+}
diff --git a/app/assets/scss/layout/_preloading.scss b/app/assets/scss/layout/_preloading.scss
new file mode 100644
index 0000000..fd3c0d1
--- /dev/null
+++ b/app/assets/scss/layout/_preloading.scss
@@ -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;
+ }
+}
diff --git a/app/assets/scss/layout/_responsive.scss b/app/assets/scss/layout/_responsive.scss
new file mode 100644
index 0000000..6b14bcb
--- /dev/null
+++ b/app/assets/scss/layout/_responsive.scss
@@ -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;
+ }
+ }
+ }
+}
diff --git a/app/assets/scss/layout/_topbar.scss b/app/assets/scss/layout/_topbar.scss
new file mode 100644
index 0000000..06cbe76
--- /dev/null
+++ b/app/assets/scss/layout/_topbar.scss
@@ -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;
+ }
+}
diff --git a/app/assets/scss/layout/_typography.scss b/app/assets/scss/layout/_typography.scss
new file mode 100644
index 0000000..be58cec
--- /dev/null
+++ b/app/assets/scss/layout/_typography.scss
@@ -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;
+ }
+}
diff --git a/app/assets/scss/layout/_utils.scss b/app/assets/scss/layout/_utils.scss
new file mode 100644
index 0000000..c5221d3
--- /dev/null
+++ b/app/assets/scss/layout/_utils.scss
@@ -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;
+ }
+}
diff --git a/app/assets/scss/layout/layout.scss b/app/assets/scss/layout/layout.scss
new file mode 100644
index 0000000..faf39c3
--- /dev/null
+++ b/app/assets/scss/layout/layout.scss
@@ -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";
diff --git a/app/assets/scss/layout/variables/_common.scss b/app/assets/scss/layout/variables/_common.scss
new file mode 100644
index 0000000..5af2c5c
--- /dev/null
+++ b/app/assets/scss/layout/variables/_common.scss
@@ -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);
+}
diff --git a/app/assets/scss/styles.scss b/app/assets/scss/styles.scss
new file mode 100644
index 0000000..b434e58
--- /dev/null
+++ b/app/assets/scss/styles.scss
@@ -0,0 +1,3 @@
+@use "primeicons/primeicons.css";
+@use "~/assets/css/tailwind.css";
+@use "~/assets/scss/layout/layout.scss";
diff --git a/app/layouts/Default.vue b/app/layouts/Default.vue
new file mode 100644
index 0000000..26b8245
--- /dev/null
+++ b/app/layouts/Default.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+
diff --git a/app/layouts/default/Footer.vue b/app/layouts/default/Footer.vue
new file mode 100644
index 0000000..e14780f
--- /dev/null
+++ b/app/layouts/default/Footer.vue
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/app/layouts/default/Sidebar.vue b/app/layouts/default/Sidebar.vue
new file mode 100644
index 0000000..7f17150
--- /dev/null
+++ b/app/layouts/default/Sidebar.vue
@@ -0,0 +1,35 @@
+
+
+
diff --git a/app/layouts/default/Topbar.vue b/app/layouts/default/Topbar.vue
new file mode 100644
index 0000000..f7c1c0d
--- /dev/null
+++ b/app/layouts/default/Topbar.vue
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+ Glowing Fiesta
+
+
+
+
+
+
diff --git a/app/pages/config/Account.vue b/app/pages/config/Account.vue
new file mode 100644
index 0000000..556b876
--- /dev/null
+++ b/app/pages/config/Account.vue
@@ -0,0 +1,5 @@
+
+
+
Account
+
+
diff --git a/app/pages/config/Settings.vue b/app/pages/config/Settings.vue
new file mode 100644
index 0000000..46d12c9
--- /dev/null
+++ b/app/pages/config/Settings.vue
@@ -0,0 +1,5 @@
+
+
+
Settings
+
+
diff --git a/app/pages/index.vue b/app/pages/index.vue
index 77130e0..b23f2a5 100644
--- a/app/pages/index.vue
+++ b/app/pages/index.vue
@@ -1,21 +1,5 @@
-
-
- Homepage
- This is your homepage
-
-
-
-
First paragraph
-
- 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.
-
-
-
-
-
+
+
Dashboard
+
diff --git a/eslint.config.mjs b/eslint.config.mjs
index bfbdce7..b801e08 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -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",
diff --git a/nuxt.config.ts b/nuxt.config.ts
index 49bf60e..aa9f230 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -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",
},
},
},
diff --git a/package-lock.json b/package-lock.json
index d2562e7..5d959e5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index e9b64ae..4b88ba2 100644
--- a/package.json
+++ b/package.json
@@ -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"
}
diff --git a/tests/layouts/Default.test.ts b/tests/layouts/Default.test.ts
new file mode 100644
index 0000000..0b5f4f3
--- /dev/null
+++ b/tests/layouts/Default.test.ts
@@ -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);
+ });
+});
diff --git a/tests/layouts/default/Footer.test.ts b/tests/layouts/default/Footer.test.ts
new file mode 100644
index 0000000..5186608
--- /dev/null
+++ b/tests/layouts/default/Footer.test.ts
@@ -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);
+ });
+});
diff --git a/tests/layouts/default/Sidebar.test.ts b/tests/layouts/default/Sidebar.test.ts
new file mode 100644
index 0000000..5b33d9a
--- /dev/null
+++ b/tests/layouts/default/Sidebar.test.ts
@@ -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);
+ });
+});
diff --git a/tests/layouts/default/Topbar.test.ts b/tests/layouts/default/Topbar.test.ts
new file mode 100644
index 0000000..4e851f6
--- /dev/null
+++ b/tests/layouts/default/Topbar.test.ts
@@ -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 = '
';
+ 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");
+ });
+ });
+});
diff --git a/tests/pages/config/Account.test.ts b/tests/pages/config/Account.test.ts
new file mode 100644
index 0000000..96ed16d
--- /dev/null
+++ b/tests/pages/config/Account.test.ts
@@ -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);
+ });
+});
diff --git a/tests/pages/config/Settings.test.ts b/tests/pages/config/Settings.test.ts
new file mode 100644
index 0000000..acebecd
--- /dev/null
+++ b/tests/pages/config/Settings.test.ts
@@ -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);
+ });
+});
diff --git a/tests/pages/index.test.ts b/tests/pages/index.test.ts
index 9ad7de2..efc1a96 100644
--- a/tests/pages/index.test.ts
+++ b/tests/pages/index.test.ts
@@ -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, {});
diff --git a/tests/setup.ts b/tests/setup.ts
index d9f8d40..76db9b5 100644
--- a/tests/setup.ts
+++ b/tests/setup.ts
@@ -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: " " },
+};
config.global.components = { Button };
+config.global.directives = { Ripple };
diff --git a/vitest.config.ts b/vitest.config.ts
index a787fb4..07f384e 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -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)),
},
},
});