Skip to content

Commit 2a8f609

Browse files
authored
Merge pull request #622 from Perdolique/next-iteration
feat(app): add inventory flow and redesign shell
2 parents 2753f61 + 1cb1ea0 commit 2a8f609

117 files changed

Lines changed: 9914 additions & 4170 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/skills/nuxt-app/SKILL.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
---
2+
name: nuxt-app
3+
description: Nuxt application conventions. Use whenever the task touches a Nuxt app — pages, layouts, Nuxt plugins, middleware, app composables, Nitro server routes, `useFetch`, `useAsyncData`, `useRequestFetch`, `$fetch`, `definePageMeta`, runtime config, or Nuxt-specific typing. Make sure to use this skill for any work on Nuxt code, Nitro handlers, or Nuxt-aware data fetching, even when the user only mentions Vue, SSR, typed responses, or `$fetch` without naming Nuxt explicitly.
4+
---
5+
6+
# Nuxt Conventions
7+
8+
This skill collects conventions for building Nuxt applications. It covers Nuxt runtime behavior, Nitro server routes, data fetching, and typing patterns that rely on Nuxt's build-time integration.
9+
10+
Treat it as the home for Nuxt-specific rules. Non-Nuxt Vue component conventions — component structure, props, emits, styling — belong elsewhere.
11+
12+
## Conventions
13+
14+
Each convention below is a self-contained rule. Apply the rules that match the task. When new Nuxt-specific patterns become stable, add them here as additional sections.
15+
16+
### Import Nuxt APIs from `#imports`
17+
18+
When a Nuxt file needs framework APIs, import them explicitly from `#imports`, never from `#app`.
19+
20+
Rules:
21+
22+
- With auto-imports disabled, every Nuxt composable still needs an explicit import.
23+
- Import Nuxt runtime APIs such as `useRoute`, `useFetch`, `navigateTo`, `definePageMeta`, `useState`, and `useCookie` from `#imports`.
24+
- Do not move this guidance into the Vue component skill. It is Nuxt-specific and belongs here.
25+
26+
### Type internal fetch through the Nitro handler
27+
28+
Nuxt infers response types for its own `/api/...` routes from the Nitro handler's return type. Use that inference instead of duplicating generics at the call site.
29+
30+
Rules:
31+
32+
- Annotate every Nitro handler with an explicit return type, usually `Promise<YourResponse>`.
33+
- Call internal routes with `useFetch('/api/...')` without a response generic.
34+
- When the task needs an imperative internal request, obtain request-aware `$fetch` with `const $fetch = useRequestFetch()` and call it without a response generic.
35+
- Do not import `$fetch` from `ofetch` for internal app-to-app requests.
36+
37+
Why it matters:
38+
39+
- A typed handler gives one source of truth for the response shape. A generic at the call site can drift away from the real payload without any compile-time complaint.
40+
- `useRequestFetch()` forwards the current request context, including headers and cookies, into the app's own routes during SSR. A plain `$fetch` from `ofetch` does not carry that context, so protected internal routes can lose session state.
41+
42+
Example:
43+
44+
```ts
45+
const { data } = await useFetch('/api/profile')
46+
47+
const $fetch = useRequestFetch()
48+
49+
await $fetch('/api/profile', {
50+
method: 'PATCH',
51+
body: {
52+
displayName: 'Ada'
53+
}
54+
})
55+
```
56+
57+
```ts
58+
interface ProfileResponse {
59+
displayName: string;
60+
}
61+
62+
export default defineEventHandler(async () : Promise<ProfileResponse> => {
63+
return {
64+
displayName: 'Ada'
65+
}
66+
})
67+
```
68+
69+
Boundary — external HTTP calls:
70+
71+
This convention applies to the app's own internal routes only. External HTTP targets do not participate in Nuxt route inference and do not need `useRequestFetch()` to forward cookies into the app's own API. For them, use the client that fits the integration and type the response at the call site when needed.
72+
73+
```ts
74+
import { $fetch } from 'ofetch'
75+
76+
interface ExternalUserResponse {
77+
id: string;
78+
}
79+
80+
const user = await $fetch<ExternalUserResponse>('https://example.com/api/user')
81+
```

.agents/skills/vue-components/SKILL.md

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: vue-components
3-
description: Vue component conventions and patterns for the Perd project. Use when creating new Vue components, editing existing .vue files, reviewing Vue code, adding styles to components, defining props/emits/slots, working with CSS Modules, writing media queries, or when the user mentions Vue components, component styling, CSS Modules, props, emits, or any .vue file changes. Apply these rules during code generation, code review, and refactoring of Vue components.
3+
description: Vue component conventions and patterns for the Perd project. Use when creating new Vue components, editing existing .vue files, reviewing Vue code, adding styles to components, defining props/emits/slots, working with CSS Modules, writing media queries, or when the user mentions Vue components, component styling, CSS Modules, props, emits, or any .vue file changes. Excludes framework-owned routing, pages/layouts, app composables, and app-level data-fetching patterns.
44
---
55

66
# Vue Component Conventions
@@ -29,20 +29,16 @@ Type exports that other components need go in a separate non-setup script block
2929

3030
Auto-imports are disabled. Every import must be explicit.
3131

32-
- Nuxt composables (`useRoute`, `useFetch`, `navigateTo`, `definePageMeta`, `useState`, `useCookie`, etc.) come from `#imports` — never from `#app`.
33-
- `$fetch` comes from `'ofetch'` — it is not available through `#imports` when auto-imports are disabled.
3432
- Vue APIs (`ref`, `computed`, `onMounted`, etc.) come from `'vue'`.
3533
- VueUse composables come from `'@vueuse/core'`.
3634
- Shared project code comes from `#shared/...`.
37-
- Components use relative `~/components/...` paths.
35+
- Component imports should follow the path style already established by the owning feature instead of introducing alias-specific rules here.
3836

3937
```ts
4038
import { computed, ref } from 'vue'
4139
import { onClickOutside } from '@vueuse/core'
42-
import { $fetch } from 'ofetch'
43-
import { definePageMeta, navigateTo, useRoute } from '#imports'
4440
import { startPagePath } from '#shared/constants'
45-
import PerdButton from '~/components/PerdButton.vue'
41+
import PerdButton from './PerdButton.vue'
4642
```
4743

4844
### Props
@@ -101,25 +97,39 @@ const isOpened = defineModel<boolean>({
10197
})
10298
```
10399

104-
### SSR Safety
100+
### Template Expressions
105101

106-
The project uses SSR (server-side rendering). Never access browser globals (`document`, `window`, `navigator`, etc.) directly in component code — it will crash on the server.
102+
Template bindings should be simple: a prop, a reactive ref, or a computed reference. When a binding requires any expression — `||`, `&&`, a ternary, a negation, or anything beyond a plain identifier — move that logic into a `computed` in the script section and bind the computed instead.
107103

108-
Use VueUse composables for DOM interactions instead. They handle SSR automatically by no-oping on the server:
104+
The reason is that templates are meant to be declarative: they should describe *what* to render, not *how* to compute it. Keeping expressions out of the template makes the logic independently readable, ensures TypeScript can properly infer the bound type, and makes it easy to reason about each binding without tracing expression chains in the markup.
105+
106+
```ts
107+
// In <script setup> — logic lives here
108+
const ariaBusy = computed(() => loading || undefined)
109+
```
110+
111+
```html
112+
<!-- In <template> — binding stays clean -->
113+
:aria-busy="ariaBusy"
114+
```
115+
116+
### Browser APIs
117+
118+
Never access browser globals (`document`, `window`, `navigator`, etc.) directly during component setup. Prefer VueUse composables for DOM interactions because they degrade safely when the DOM is unavailable:
109119

110120
- `useEventListener` — instead of `document.addEventListener` / `element.addEventListener`
111121
- `onClickOutside` — instead of manual pointerdown + contains checks
112122
- Other `@vueuse/core` composables as needed
113123

114124
```ts
115-
// Correct — SSR-safe
125+
// Correct
116126
import { useEventListener } from '@vueuse/core'
117127

118128
useEventListener(dialogRef, 'close', () => {
119129
isOpened.value = false
120130
})
121131

122-
// Wrong — crashes during SSR
132+
// Wrong
123133
document.addEventListener('pointerdown', handler)
124134
```
125135

@@ -154,6 +164,30 @@ Every component uses `<style module>` — never `<style scoped>`. The root class
154164
</style>
155165
```
156166

167+
Use CSS module classes directly in templates with `$style`. Do not import `useCssModule()` just to compose class lists in script. Keep class names in the template and move only the logic into computed values:
168+
169+
```vue
170+
<template>
171+
<button
172+
type="button"
173+
:class="[$style.navigationItem, {
174+
active: isCatalogActive
175+
}]"
176+
>
177+
Catalog
178+
</button>
179+
</template>
180+
181+
<script lang="ts" setup>
182+
import { computed, ref } from 'vue'
183+
184+
const selectedView = ref<'catalog' | 'inventory'>('catalog')
185+
const isCatalogActive = computed(() => selectedView.value === 'catalog')
186+
</script>
187+
```
188+
189+
Avoid inline conditional logic in class bindings. Do not write route comparisons, ternaries, negations, or boolean expressions directly inside template class objects; compute them in script first.
190+
157191
### Modifier Pattern with :global()
158192

159193
State-based class modifiers use plain string classes in the template combined with `&:global(.modifier)` in CSS. This is the project's established convention for CSS Modules — it is intentional and correct:
@@ -178,6 +212,12 @@ State-based class modifiers use plain string classes in the template combined wi
178212

179213
The modifier string (e.g., `visible`, `active`, `small`) is a plain class name — not a `$style` reference. The `&:global(.modifier)` selector escapes the CSS Module hashing so it matches the plain class.
180214

215+
### Naming And Selectors
216+
217+
Use full, readable names for component prop values and CSS/state variants. Prefer `medium`, `small`, `icon-only`, and `icon-small` over abbreviations like `md`, `sm`, or `icon-sm`.
218+
219+
Style owned markup through explicit classes on the element being styled. Avoid nested tag selectors such as `& em`, `& span`, or `& strong` when the element can be given its own class.
220+
181221
### CSS Features (Baseline 2025)
182222

183223
The project targets modern browsers only. Use these freely:

.oxlintrc.jsonc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
"./node_modules/eslint-config-greenpie/configs/oxlintrc.jsonc"
66
],
77

8+
"ignorePatterns": [
9+
"new-design-assets/"
10+
],
11+
812
"options": {
913
"typeAware": true,
1014
"typeCheck": true,

AGENTS.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,24 @@
1818
- Public reference-data read routes use `slug`; admin mutations use stable `id` params.
1919
- All tables live in `server/database/schema.ts`, organized by Auth, Equipment catalog, and User data sections.
2020

21+
## Visual design prototype
22+
23+
The designer's visual prototype of the final application lives in `new-design-assets/`. It is the source of truth for UI layout, component appearance, color tokens, typography, spacing, and interaction patterns.
24+
25+
When creating or updating Vue components, page layouts, or any visual styles, consult `new-design-assets/` first:
26+
27+
- `new-design-assets/styles/tokens.css` — design tokens (colors, spacing, typography scale).
28+
- `new-design-assets/styles/base.css` and `components.css` — global resets and component-level styles.
29+
- `new-design-assets/scripts/` — JSX view files that show per-screen layout and component structure (`view-dashboard.jsx`, `view-catalog.jsx`, `view-detail.jsx`, etc.).
30+
- `new-design-assets/index.html` — runnable prototype; open in a browser to inspect the full interactive design.
31+
32+
Do not invent visual decisions (colors, spacing values, component shapes) that contradict the prototype. If the prototype does not cover a case, match the nearest existing pattern from it.
33+
2134
## Local skills
2235

2336
Before any task, check whether a local skill matches the domain and follow it.
2437

38+
- `nuxt-app` for Nuxt pages, layouts, app composables, internal API fetch typing, request-aware `useRequestFetch()`, and Nitro handler return types that drive internal route inference.
2539
- `vue-components` for `.vue` component structure, styling, props/emits, and SSR-safe frontend patterns.
2640
- `equipment-backend` for equipment/catalog backend work in `server/api`, validation schemas, Drizzle write paths, schema changes, and equipment API tests.
2741
- `planning-docs` for roadmap files in `plan/`, completed work notes, architecture docs, and iteration planning tasks.
@@ -30,6 +44,14 @@ Before any task, check whether a local skill matches the domain and follow it.
3044

3145
- Files imported by standalone Node or `tsx` scripts, including `tools/*.ts`, migrations, seeds, and their transitive dependencies, must not rely on Nuxt-only aliases like `~/` or `@@/`. If they use `#shared/*` or `#server/*`, keep those aliases backed by `package.json#imports`.
3246
- Do not implement fixes, features, compatibility branches, or refactors unless they address a reproducible problem, an explicitly requested behavior, or a currently supported project scenario. Treat hypothetical improvements and broader compatibility ideas as follow-up work, not as justification for changing the current behavior.
47+
- Async action areas must remain structurally stable while state is loading or mutating. Keep the same interactive control mounted and change its state instead of swapping it out for unrelated text blocks.
48+
- Related navigation and actions must be grouped by user meaning. Do not tuck inventory actions or similar workflow controls into unrelated content sections such as property lists.
49+
- Plan each "next iteration" as a sequence of minimal self-contained tasks. Every task must state its intended result, scope boundaries, and required verification.
50+
- When one iteration spans multiple tasks, each task must be safe to complete, commit, and validate independently without hidden follow-up decisions.
51+
- New Playwright scenarios should be run against their individual spec file before the full `pnpm run test:e2e:ci` suite.
52+
- Playwright `context.route(...)` matchers must account for query strings whenever the real endpoint is requested with query parameters.
53+
- E2E flows that rely on mocked auth should not use `page.goto()` after login unless the test also establishes a real server-side session cookie.
54+
- If browser tests run through `wrangler dev`, the E2E preview path must keep Wrangler state, logs, and config under a writable temp directory instead of the user's default `~/.config`.
3355
- After any architecture, route-convention, data-model, or test-strategy change, update the relevant docs in the same change. This can include `AGENTS.md`, `plan/PLAN.md`, `plan/completed.md`, or the detailed roadmap file that changed.
3456

3557
## Verification matrix

README.md

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,56 @@
1-
# Equipment list
1+
# Perd
22

33
<https://perd.perd.workers.dev/>
44

5-
A web app for those who love hanging out in the forest but always forget to bring something important. With this app, you can easily create a checklist of things you must take on your adventure.
5+
Perd is an outdoor equipment companion for people who plan trips, hikes, and other outdoor outings and do not want to forget important gear.
66

7-
## Similar applications
7+
The product direction is built around one practical loop:
88

9-
- [lighterpack.com](https://lighterpack.com) (ugly)
10-
- [hikepack.app](https://www.hikepack.app/list/84a8ea0c-006b-4cff-bb2b-7bcf52183b0b) (simple TODO list)
11-
- [packstack.io](https://www.packstack.io/) (Bad UX)
12-
- [Don't Forget the Spoon](https://play.google.com/store/apps/details?id=com.dontforgetthespoon.dont_forget_the_spoon&hl=en_US&pli=1) (Android, featureless)
13-
- ~~packflare.com~~ (dead)
14-
- ~~geargrams.com~~ (dead)
15-
- ~~trailhawk.io~~ (dead)
16-
- ~~baseweight.co~~ (dead)
17-
- ~~packlist.io~~ (dead)
18-
- ~~outdoormojo.com~~ (dead)
19-
- ... (suggestions are welcome)
9+
1. browse a public equipment catalog;
10+
2. keep a personal list of gear you own;
11+
3. build packing lists for a specific trip or activity.
2012

21-
## Stack
13+
Perd is not meant to stop at a read-only catalog. The goal is a useful packing workflow that still works even when the catalog is incomplete.
2214

23-
### Details
15+
## What you can do now
2416

25-
[Wiki](https://github.com/Perdolique/perd/wiki)
17+
- sign in;
18+
- open the catalog;
19+
- browse the current list of equipment;
20+
- open item detail pages;
21+
- keep a personal inventory with "I have this" actions.
22+
23+
This is still an early product state. The full user workflow is not shipped yet.
2624

27-
## MVP Release
25+
## What is planned next
26+
27+
The next user-facing slices are:
28+
29+
- packing lists for specific trips or activities;
30+
- custom checklist entries for things that are not in the catalog yet.
31+
32+
The priority is the user workflow, not internal admin tooling.
33+
34+
## Similar applications
35+
36+
- [lighterpack.com](https://lighterpack.com)
37+
- [hikepack.app](https://www.hikepack.app/list/84a8ea0c-006b-4cff-bb2b-7bcf52183b0b)
38+
- [packstack.io](https://www.packstack.io/)
39+
- [Don't Forget the Spoon](https://play.google.com/store/apps/details?id=com.dontforgetthespoon.dont_forget_the_spoon&hl=en_US&pli=1)
40+
- ~~packflare.com~~
41+
- ~~geargrams.com~~
42+
- ~~trailhawk.io~~
43+
- ~~baseweight.co~~
44+
- ~~packlist.io~~
45+
- ~~outdoormojo.com~~
46+
47+
## Stack
2848

2949
- [Nuxt](https://nuxt.com/)
3050
- [Cloudflare Workers](https://developers.cloudflare.com/workers/)
31-
- [Neon database](https://neon.tech)
51+
- [Neon](https://neon.tech/)
52+
- [Drizzle ORM](https://orm.drizzle.team/)
53+
54+
## Details
55+
56+
[Wiki](https://github.com/Perdolique/perd/wiki)

app/assets/styles/base.css

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,30 @@
1212
font-family: "Inter", sans-serif;
1313
}
1414

15+
html,
1516
body {
16-
background-color: var(--color-background);
17-
color: var(--color-text);
17+
min-height: 100%;
1818
}
1919

20-
.perd-root {
21-
height: 100dvh;
20+
body {
21+
background-color: var(--color-background-base);
22+
color: var(--color-text-primary);
23+
line-height: var(--line-height-body);
24+
font-size: var(--font-size-16);
25+
text-rendering: optimizeLegibility;
26+
-webkit-font-smoothing: antialiased;
27+
-moz-osx-font-smoothing: grayscale;
28+
}
29+
30+
a,
31+
button,
32+
input,
33+
textarea,
34+
select {
35+
font: inherit;
36+
}
37+
38+
:focus-visible {
39+
outline: 2px solid var(--color-accent-ring);
40+
outline-offset: 3px;
2241
}

0 commit comments

Comments
 (0)