start component tree + implem navbar

This commit is contained in:
TD
2026-08-28 20:46:44 +02:00
parent 147a270e02
commit 7d98af7817
17 changed files with 1807 additions and 3 deletions
+1617
View File
File diff suppressed because it is too large Load Diff
+9 -1
View File
@@ -8,9 +8,13 @@
"build": "vue-tsc -b && vite build", "build": "vue-tsc -b && vite build",
"preview": "vite preview", "preview": "vite preview",
"lint": "eslint . --fix", "lint": "eslint . --fix",
"format": "prettier --write ." "format": "prettier --write .",
"test": "vitest",
"test:ui": "vitest --ui"
}, },
"dependencies": { "dependencies": {
"@headlessui/vue": "^1.7.23",
"@heroicons/vue": "^2.2.0",
"@supabase/supabase-js": "^2.112.3", "@supabase/supabase-js": "^2.112.3",
"@tailwindcss/vite": "^4.3.3", "@tailwindcss/vite": "^4.3.3",
"pinia": "^4.0.3", "pinia": "^4.0.3",
@@ -22,16 +26,20 @@
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@types/node": "^24.13.3", "@types/node": "^24.13.3",
"@vitejs/plugin-vue": "^6.0.8", "@vitejs/plugin-vue": "^6.0.8",
"@vitest/ui": "^4.1.11",
"@vue/eslint-config-typescript": "^14.9.0", "@vue/eslint-config-typescript": "^14.9.0",
"@vue/test-utils": "^2.4.11",
"@vue/tsconfig": "^0.9.1", "@vue/tsconfig": "^0.9.1",
"eslint": "^10.8.1", "eslint": "^10.8.1",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.6", "eslint-plugin-prettier": "^5.5.6",
"eslint-plugin-vue": "^10.10.0", "eslint-plugin-vue": "^10.10.0",
"jsdom": "^30.0.1",
"prettier": "^3.9.6", "prettier": "^3.9.6",
"typescript": "~6.0.2", "typescript": "~6.0.2",
"typescript-eslint": "^8.67.0", "typescript-eslint": "^8.67.0",
"vite": "^8.2.0", "vite": "^8.2.0",
"vitest": "^4.1.11",
"vue-tsc": "^3.3.8" "vue-tsc": "^3.3.8"
} }
} }
@@ -0,0 +1,28 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import EntryItem from './EntryItem.vue'
describe('EntryItem', () => {
it('renders the label and formatted amount', () => {
const wrapper = mount(EntryItem, {
props: { label: 'Rent', amount: 850 },
})
expect(wrapper.text()).toContain('Rent')
expect(wrapper.text()).toContain('850')
})
it('emits "remove" with the entry id when delete is clicked', async () => {
const wrapper = mount(EntryItem, {
props: { id: 'random_id', label: 'Rent', amount: 850 },
})
await wrapper.find('[data-test="delete-btn"]').trigger('click')
expect(wrapper.emitted('remove')).toEqual([['random_id']])
})
it('matches snapshot', () => {
const wrapper = mount(EntryItem, {
props: { label: 'Rent', amount: 850 },
})
expect(wrapper.html()).toMatchSnapshot()
})
})
+2
View File
@@ -0,0 +1,2 @@
<template></template>
<script setup lang="ts"></script>
+74
View File
@@ -0,0 +1,74 @@
<template>
<Disclosure as="nav" class="rounded bg-tints-0 dark:bg-shades-100" v-slot="{ open }">
<div class="flex h-16 items-center justify-between px-4">
<div class="flex items-center gap-6">
<span class="text-lg font-semibold text-shades-200 dark:text-shades-900">Budget</span>
<div class="hidden sm:flex sm:gap-2">
<a
v-for="item in navigation"
:key="item.name"
:href="item.href"
:class="[
item.current
? 'bg-shades-500 text-tints-0 dark:bg-tints-1000 dark:text-shades-100'
: 'text-shades-300 hover:bg-shades-800 dark:text-shades-700 dark:hover:bg-shades-300/20',
'rounded-md px-3 py-2 text-sm font-medium',
]"
:aria-current="item.current ? 'page' : undefined"
>{{ item.name }}</a
>
</div>
</div>
<div class="flex items-center gap-1">
<button
type="button"
class="rounded-md p-2 text-shades-300 hover:bg-shades-800 dark:text-shades-700 dark:hover:bg-shades-300/20"
@click="toggle"
>
<span class="sr-only">Toggle dark mode</span>
<SunIcon v-if="isDark" class="size-5" aria-hidden="true" />
<MoonIcon v-else class="size-5" aria-hidden="true" />
</button>
<DisclosureButton
class="rounded-md p-2 text-shades-300 hover:bg-shades-800 sm:hidden dark:text-shades-700 dark:hover:bg-shades-300/20"
>
<span class="sr-only">Open main menu</span>
<Bars3Icon v-if="!open" class="size-6" aria-hidden="true" />
<XMarkIcon v-else class="size-6" aria-hidden="true" />
</DisclosureButton>
</div>
</div>
<DisclosurePanel class="space-y-1 px-4 pb-3 sm:hidden">
<DisclosureButton
v-for="item in navigation"
:key="item.name"
as="a"
:href="item.href"
:class="[
item.current
? 'bg-shades-500 text-tints-0 dark:bg-tints-1000 dark:text-shades-100'
: 'text-shades-300 hover:bg-shades-800 dark:text-shades-700 dark:hover:bg-shades-300/20',
'block rounded-md px-3 py-2 text-base font-medium',
]"
:aria-current="item.current ? 'page' : undefined"
>{{ item.name }}</DisclosureButton
>
</DisclosurePanel>
</Disclosure>
</template>
<script setup lang="ts">
import { useDarkMode } from '@/composables/useDarkMode'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue'
import { Bars3Icon, MoonIcon, SunIcon, XMarkIcon } from '@heroicons/vue/24/outline'
const { isDark, toggle } = useDarkMode()
const navigation = [
{ name: 'Dashboard', href: '#', current: true },
{ name: 'History', href: '#', current: false },
]
</script>
@@ -0,0 +1,5 @@
<template>
<div class="h-16 flex items-center justify-start bg-tints-0 dark:bg-shades-100">
<span>Amount</span>
</div>
</template>
@@ -0,0 +1 @@
<template>Spend Columns</template>
+1
View File
@@ -0,0 +1 @@
<template>Spend Input</template>
+25
View File
@@ -0,0 +1,25 @@
import { ref } from 'vue'
const isDark = ref(false)
function apply() {
document.documentElement.classList.toggle('dark', isDark.value)
}
function init() {
const stored = localStorage.getItem('theme')
isDark.value = stored ? stored === 'dark' : window.matchMedia('(prefers-color-scheme: dark)').matches
apply()
}
function toggle() {
isDark.value = !isDark.value
localStorage.setItem('theme', isDark.value ? 'dark' : 'light')
apply()
}
init()
export function useDarkMode() {
return { isDark, toggle }
}
+15
View File
@@ -0,0 +1,15 @@
<script setup lang="ts">
import Navbar from '@/components/Navbar/Navbar.vue'
import RemainingAmount from '@/components/RemainingAmount/RemainingAmount.vue'
import SpendColumns from '@/components/SpendColumns/SpendColumns.vue'
import SpendInput from '@/components/SpendInput/SpendInput.vue'
</script>
<template>
<div class="container flex flex-col space-x-1 space-y-2 p-2">
<div class="w-full rounded"><Navbar /></div>
<div class="h-12 w-full rounded"><RemainingAmount /></div>
<div class="h-128 w-full rounded"><SpendColumns /></div>
<div class="h-16 w-full rounded"><SpendInput /></div>
</div>
</template>
+1
View File
@@ -0,0 +1 @@
<template></template>
+24 -1
View File
@@ -1 +1,24 @@
@import "tailwindcss"; @import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--color-shades-900: #f8ead3;
--color-shades-800: #f2d6a6;
--color-shades-700: #edc278;
--color-shades-600: #eaaf48;
--color-shades-500: #e89b17;
--color-shades-400: #bc7d10;
--color-shades-300: #8f5e0a;
--color-shades-200: #613f05;
--color-shades-100: #312002;
--color-tints-1000: #f59f0a;
--color-tints-880: #f2a92c;
--color-tints-750: #f0b248;
--color-tints-630: #efbd67;
--color-tints-500: #efc885;
--color-tints-380: #f0d29e;
--color-tints-250: #f3deba;
--color-tints-130: #f5e8d1;
--color-tints-0: #faf4ea;
}
+5 -1
View File
@@ -1,11 +1,15 @@
import tailwindcss from '@tailwindcss/vite' import tailwindcss from '@tailwindcss/vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import { fileURLToPath, URL } from 'node:url' import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite' import { defineConfig } from 'vitest/config'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [vue(), tailwindcss()], plugins: [vue(), tailwindcss()],
test: {
environment: 'jsdom',
globals: true,
},
resolve: { resolve: {
alias: { alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)), '@': fileURLToPath(new URL('./src', import.meta.url)),