From d1967f718e1dc9b04a8fcbc2a05ca9ef680a82f8 Mon Sep 17 00:00:00 2001 From: Liviu Burcusel Date: Wed, 12 Nov 2025 12:50:56 +0100 Subject: [PATCH] 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. --- .vscode/settings.json | 22 + app/app.vue | 4 +- app/assets/images/logo.png | Bin 0 -> 20312 bytes app/assets/scss/layout/_core.scss | 24 + app/assets/scss/layout/_footer.scss | 8 + app/assets/scss/layout/_main.scss | 14 + app/assets/scss/layout/_menu.scss | 160 +++++++ app/assets/scss/layout/_mixins.scss | 15 + app/assets/scss/layout/_preloading.scss | 48 ++ app/assets/scss/layout/_responsive.scss | 110 +++++ app/assets/scss/layout/_topbar.scss | 201 ++++++++ app/assets/scss/layout/_typography.scss | 68 +++ app/assets/scss/layout/_utils.scss | 25 + app/assets/scss/layout/layout.scss | 11 + app/assets/scss/layout/variables/_common.scss | 26 + app/assets/scss/styles.scss | 3 + app/layouts/Default.vue | 21 + app/layouts/default/Footer.vue | 9 + app/layouts/default/Sidebar.vue | 35 ++ app/layouts/default/Topbar.vue | 56 +++ app/pages/config/Account.vue | 5 + app/pages/config/Settings.vue | 5 + app/pages/index.vue | 22 +- eslint.config.mjs | 4 + nuxt.config.ts | 84 +++- package-lock.json | 448 +++++++++++++++++- package.json | 2 + tests/layouts/Default.test.ts | 11 + tests/layouts/default/Footer.test.ts | 55 +++ tests/layouts/default/Sidebar.test.ts | 11 + tests/layouts/default/Topbar.test.ts | 155 ++++++ tests/pages/config/Account.test.ts | 11 + tests/pages/config/Settings.test.ts | 11 + tests/pages/index.test.ts | 2 +- tests/setup.ts | 9 +- vitest.config.ts | 4 +- 36 files changed, 1660 insertions(+), 39 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 app/assets/images/logo.png create mode 100644 app/assets/scss/layout/_core.scss create mode 100644 app/assets/scss/layout/_footer.scss create mode 100644 app/assets/scss/layout/_main.scss create mode 100644 app/assets/scss/layout/_menu.scss create mode 100644 app/assets/scss/layout/_mixins.scss create mode 100644 app/assets/scss/layout/_preloading.scss create mode 100644 app/assets/scss/layout/_responsive.scss create mode 100644 app/assets/scss/layout/_topbar.scss create mode 100644 app/assets/scss/layout/_typography.scss create mode 100644 app/assets/scss/layout/_utils.scss create mode 100644 app/assets/scss/layout/layout.scss create mode 100644 app/assets/scss/layout/variables/_common.scss create mode 100644 app/assets/scss/styles.scss create mode 100644 app/layouts/Default.vue create mode 100644 app/layouts/default/Footer.vue create mode 100644 app/layouts/default/Sidebar.vue create mode 100644 app/layouts/default/Topbar.vue create mode 100644 app/pages/config/Account.vue create mode 100644 app/pages/config/Settings.vue create mode 100644 tests/layouts/Default.test.ts create mode 100644 tests/layouts/default/Footer.test.ts create mode 100644 tests/layouts/default/Sidebar.test.ts create mode 100644 tests/layouts/default/Topbar.test.ts create mode 100644 tests/pages/config/Account.test.ts create mode 100644 tests/pages/config/Settings.test.ts 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 0000000000000000000000000000000000000000..52f1bff67879dfe73df1efbf33a4e7748e3b019c GIT binary patch literal 20312 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 literal 0 HcmV?d00001 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 @@ + + + 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 @@ + 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 @@ + 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 @@ 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)), }, }, });