Upgrade to Nuxt 4 and fix icon issues

- Upgrade from Nuxt 3.6.0 to Nuxt 4.3.1
- Replace nuxt-icon with @nuxt/icon (Nuxt 4 compatible)
- Install missing icon collections (@iconify-json/ion, heroicons, etc.)
- Fix icon colors: use style instead of color prop
- Add AGENTS.md with coding guidelines
- Fix icon alignment in index.vue (items-center)
This commit is contained in:
Michael Winter 2026-02-18 17:34:37 +01:00
parent 72d2d67842
commit 61332c28ef
7 changed files with 162 additions and 25 deletions

131
AGENTS.md Normal file
View file

@ -0,0 +1,131 @@
# AGENTS.md - Agent Coding Guidelines
This document provides guidelines for agents working on this codebase.
## Project Overview
- **Framework**: Nuxt 3 (Vue 3)
- **Styling**: Tailwind CSS
- **State Management**: Pinia
- **UI Components**: Headless UI + Nuxt Icon
- **Image Handling**: @nuxt/image
- **TypeScript**: Enabled (tsconfig extends .nuxt/tsconfig.json)
- **Storybook**: Available for component documentation
## Build Commands
```bash
# Development
npm run dev # Start development server
npm run build # Build for production
npm run generate # Generate static site (SSG)
npm run preview # Preview production build
# No test framework configured
```
## Code Style Guidelines
### General Conventions
- Use Vue 3 Composition API with `<script setup lang="ts">`
- TypeScript is preferred for new files; stores use JavaScript (.js)
- Follow Nuxt 3 auto-import conventions (no explicit imports for composables, components, etc.)
### File Organization
```
/pages/ - Page components (file-based routing)
/components/ - Vue components (auto-imported)
/layouts/ - Layout components
/server/api/ - Server API routes (Nitro)
/stores/ - Pinia stores (.js files)
/assets/ - Static assets
/public/ - Public static files
```
### Naming Conventions
- **Components**: PascalCase (e.g., `Modal.vue`, `IconButton.vue`)
- **Files**: kebab-case for pages, PascalCase for components
- **Stores**: CamelCase (e.g., `ModalStore.js`, `AudioPlayerStore.js`)
- **Props/Emits**: camelCase
### Component Patterns
```vue
<script setup lang="ts">
// Use withDefaults for optional props
const props = withDefaults(
defineProps<{
modelValue?: boolean
persistent?: boolean
}>(),
{
modelValue: false,
persistent: false,
},
)
// Use type-only emits
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
}>()
// Use toRefs for reactive destructuring
const { modelValue } = toRefs(props)
</script>
<template>
<!-- Template content -->
</template>
```
### Tailwind CSS
- Use Tailwind utility classes for all styling
- Common classes used: `flex`, `grid`, `fixed`, `relative`, `z-*`, `p-*`, `m-*`, `text-*`, etc.
### API Routes (Server)
```typescript
// server/api/example.ts
export default defineEventHandler((event) => {
// Handle request and return data
})
```
### Store Patterns (Pinia)
```javascript
// stores/ExampleStore.js
import { defineStore } from "pinia"
export const useExampleStore = defineStore("ExampleStore", {
state: () => ({ count: 0 }),
actions: {
increment() {
this.count++
}
}
})
```
### Error Handling
- Use try/catch in API routes
- Return appropriate HTTP status codes
- Handle undefined/null values gracefully
### Imports
- Vue/composables: Use Nuxt auto-imports (no import needed)
- External modules: Explicit import
- Server-only: Place in `/server/` directory
- Path aliases: `@/` maps to project root
### Additional Notes
- Project uses Storybook (`.stories.ts` files) for component documentation
- Environment variables should use `.env` files (not committed)
- Image domains configured for `unboundedpress.org` in nuxt.config.ts

View file

@ -75,7 +75,7 @@ const toggle = () => {
<Icon <Icon
name="heroicons:chevron-down" name="heroicons:chevron-down"
:class="isOpen ? 'transform rotate-180' : ''" :class="isOpen ? 'transform rotate-180' : ''"
class="w-5 h-5" class="w-5 h-5 text-black"
/> />
<slot name="title"></slot> <slot name="title"></slot>
</div> </div>

View file

@ -1,7 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue' import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue'
import { ref, toRefs, watch } from 'vue' import { ref, toRefs, watch } from 'vue'
import Icon from '../Icon/index.vue'
import Collapsible from './Collapsible.vue' import Collapsible from './Collapsible.vue'
interface CollapsibleItem { interface CollapsibleItem {

View file

@ -3,35 +3,35 @@
<div v-show="visible" class="bg-black rounded-full text-xs inline-flex" > <div v-show="visible" class="bg-black rounded-full text-xs inline-flex" >
<NuxtLink v-if="type === 'score'" :to="link" class="inline-flex p-1"> <NuxtLink v-if="type === 'score'" :to="link" class="inline-flex p-1">
<Icon name="ion:book-sharp" color="white" /> <Icon name="ion:book-sharp" style="color: white" />
</NuxtLink> </NuxtLink>
<NuxtLink v-else-if="type === 'document'" class="inline-flex p-1" :to="link" :target="newTab ? '_blank' : undefined"> <NuxtLink v-else-if="type === 'document'" class="inline-flex p-1" :to="link" :target="newTab ? '_blank' : undefined">
<Icon name="ion:book-sharp" color="white" /> <Icon name="ion:book-sharp" style="color: white" />
</NuxtLink> </NuxtLink>
<NuxtLink v-else-if="type === 'buy'" class="inline-flex p-1" :to="link"> <NuxtLink v-else-if="type === 'buy'" class="inline-flex p-1" :to="link">
<Icon name="bxs:purchase-tag" color="white" /> <Icon name="bxs:purchase-tag" style="color: white" />
</NuxtLink> </NuxtLink>
<NuxtLink v-else-if="type === 'email'" class="inline-flex p-1" :to="link"> <NuxtLink v-else-if="type === 'email'" class="inline-flex p-1" :to="link">
<Icon name="ic:baseline-email" color="white" /> <Icon name="ic:baseline-email" style="color: white" />
</NuxtLink> </NuxtLink>
<NuxtLink v-else-if="type === 'discogs'" class="inline-flex p-1" :to="link"> <NuxtLink v-else-if="type === 'discogs'" class="inline-flex p-1" :to="link">
<Icon name="simple-icons:discogs" color="white" /> <Icon name="simple-icons:discogs" style="color: white" />
</NuxtLink> </NuxtLink>
<button @click="audioPlayerStore.setSoundCloudTrackID(work.soundcloud_trackid)" v-else-if="type === 'audio'" class="inline-flex p-1"> <button @click="audioPlayerStore.setSoundCloudTrackID(work.soundcloud_trackid)" v-else-if="type === 'audio'" class="inline-flex p-1">
<Icon name="wpf:speaker" color="white" /> <Icon name="wpf:speaker" style="color: white" />
</button> </button>
<button @click="modalStore.setModalProps('video', 'aspect-video', true, '', '', work.vimeo_trackid)" v-else-if="type === 'video'" class="inline-flex p-1"> <button @click="modalStore.setModalProps('video', 'aspect-video', true, '', '', work.vimeo_trackid)" v-else-if="type === 'video'" class="inline-flex p-1">
<Icon name="fluent:video-48-filled" color="white" /> <Icon name="fluent:video-48-filled" style="color: white" />
</button> </button>
<button @click="modalStore.setModalProps('image', 'aspect-auto', true, 'images', work.gallery, '')" v-else="type === 'image'" class="inline-flex p-1"> <button @click="modalStore.setModalProps('image', 'aspect-auto', true, 'images', work.gallery, '')" v-else="type === 'image'" class="inline-flex p-1">
<Icon name="mdi:camera" color="white" /> <Icon name="mdi:camera" style="color: white" />
</button> </button>
</div> </div>

View file

@ -2,8 +2,7 @@
// https://nuxt.com/docs/api/configuration/nuxt-config // https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({ export default defineNuxtConfig({
modules: ['@nuxtjs/tailwindcss', '@nuxt/image', 'nuxt-icon', '@pinia/nuxt', 'nuxt-headlessui', 'nuxt-swiper'], modules: ['@nuxtjs/tailwindcss', '@nuxt/image', '@nuxt/icon', '@pinia/nuxt', 'nuxt-headlessui', 'nuxt-swiper', 'nuxt-umami'],
extends: ['nuxt-umami'],
image: { image: {
domains: ['unboundedpress.org'] domains: ['unboundedpress.org']
}, },

View file

@ -9,19 +9,27 @@
"postinstall": "nuxt prepare" "postinstall": "nuxt prepare"
}, },
"devDependencies": { "devDependencies": {
"@nuxt/image": "^1.0.0-rc.1", "@iconify-json/bxs": "^1.2.2",
"@nuxtjs/tailwindcss": "^6.7.0", "@iconify-json/fluent": "^1.2.39",
"@types/node": "^18", "@iconify-json/heroicons": "^1.2.3",
"nuxt-headlessui": "^1.1.4", "@iconify-json/ion": "^1.2.6",
"nuxt-icon": "^0.4.1" "@iconify-json/mdi": "^1.2.3",
"@iconify-json/simple-icons": "^1.2.71",
"@iconify-json/wpf": "^1.2.0",
"@nuxt/icon": "^2.2.1",
"@nuxt/image": "^2.0.0",
"@nuxtjs/tailwindcss": "^6.14.0",
"@types/node": "^25.2.3",
"nuxt-headlessui": "^1.2.2",
"nuxt-icon": "^1.0.0-beta.7"
}, },
"dependencies": { "dependencies": {
"@pinia/nuxt": "^0.4.11", "@pinia/nuxt": "^0.11.3",
"mongodb": "^7.1.0", "mongodb": "^7.1.0",
"nuxt": "^3.6.0", "nuxt": "^4.3.1",
"nuxt-swiper": "^1.1.0", "nuxt-swiper": "^2.0.1",
"nuxt-umami": "^2.4.2", "nuxt-umami": "^3.2.1",
"pinia": "^2.1.3", "pinia": "^3.0.4",
"sharp": "^0.32.1" "sharp": "^0.34.5"
} }
} }

View file

@ -7,7 +7,7 @@
<div class="py-2 ml-3" v-for="item in works"> <div class="py-2 ml-3" v-for="item in works">
<p class="font-thin">{{ item.year }}</p> <p class="font-thin">{{ item.year }}</p>
<div class="leading-tight py-1 ml-3" v-for="work in item.works"> <div class="leading-tight py-1 ml-3" v-for="work in item.works">
<div class="grid grid-cols-[65%,30%] gap-1 font-thin"> <div class="grid grid-cols-[65%,30%] gap-1 font-thin items-center">
<div class="italic text-sm">{{ work.title }}</div> <div class="italic text-sm">{{ work.title }}</div>
<div class="inline-flex"> <div class="inline-flex">
@ -37,7 +37,7 @@
<p class="text-lg">writings</p> <p class="text-lg">writings</p>
<div class="leading-tight py-2 ml-3 text-sm" v-for="item in pubs"> <div class="leading-tight py-2 ml-3 text-sm" v-for="item in pubs">
<div class="grid grid-cols-[95%,5%] gap-1"> <div class="grid grid-cols-[95%,5%] gap-1 items-center">
<div> <div>
<span v-html="item.entryTags.title"></span> <span v-html="item.entryTags.title"></span>
<div class="ml-4 text-[#7F7F7F]"> <div class="ml-4 text-[#7F7F7F]">