# Code Standards: Vibery Kits Website

## File Naming Conventions

### Components

- **Astro:** PascalCase with .astro extension
  - `Header.astro`, `Hero.astro`, `TemplateCard.astro`
  - Files in `/src/components/`

- **Vue:** PascalCase with .vue extension
  - `SearchBar.vue`, `CartSidebar.vue`, `TemplateModal.vue`
  - Files in `/src/components/vue/`

### Composables

- **Pattern:** `use` prefix + camelCase
  - `useCart.ts`, `useModal.ts`, `useSearch.ts`
  - Files in `/src/composables/`

### Types

- **Pattern:** kebab-case or descriptive name
  - `template.ts`, `cart.ts`
  - Files in `/src/types/`

### Data Files

- **Pattern:** kebab-case
  - `templates.json`, `stacks.json`, `outcomes.json`
  - Files in `/src/data/`

### Pages/Routes

- **Pattern:** kebab-case directories
  - `/pages/index.astro` → `/`
  - `/pages/agents.astro` → `/agents`
  - `/pages/stacks/[id].astro` → `/stacks/123`

### Styles

- **Global:** `global.css` in `/src/styles/`
- **Scoped:** Use `<style scoped>` in Vue components

---

## Component Patterns

### Astro Static Components

**Structure:**

```astro
---
// Props interface
interface Props {
  title: string
  items: Template[]
}

// Component logic
const { title, items } = Astro.props

// Data fetching (build-time)
---

<section class="warm-bg-surface rounded-lg p-6">
  <h2 class="warm-text-primary text-xl">{title}</h2>
  <!-- Template content -->
</section>

<style scoped>
  /* Optional scoped styles */
</style>
```

**Guidelines:**

- Use TypeScript interfaces for props
- Destructure props from `Astro.props`
- Use Warm Terminal classes for styling
- Build-time data fetching only
- No client-side logic (use Vue for interactivity)

### Vue Interactive Components

**Structure:**

```vue
<script setup lang="ts">
import { computed, ref } from "vue";
import { useCart } from "@/composables/useCart";
import type { Template } from "@/types/template";

interface Props {
  template: Template;
}

const props = defineProps<Props>();

const { addItem, hasItem } = useCart();
const isAdded = computed(() => hasItem(props.template.name));
</script>

<template>
  <article
    class="warm-bg-surface border border-warm-text-tertiary rounded-lg p-4"
  >
    <h3 class="warm-text-primary font-semibold">{{ template.name }}</h3>
    <button
      class="btn-primary"
      :class="{ 'bg-green-600': isAdded }"
      @click="addItem(template)"
    >
      {{ isAdded ? "Added ✓" : "Add to Cart" }}
    </button>
  </article>
</template>

<style scoped>
.btn-primary {
  @apply bg-warm-accent text-white px-4 py-2 rounded transition-colors hover:bg-warm-accent-hover;
}
</style>
```

**Guidelines:**

- Use `<script setup>` syntax (Vue 3.3+)
- Type props with TypeScript interfaces
- Use computed() for derived state
- Import composables at top
- Use Warm Terminal classes
- Scoped styles for component-specific CSS
- Emit events with `defineEmits<T>()`

### Composable Pattern

**Structure:**

```typescript
// composables/useXxx.ts
import { ref, computed } from "vue";
import type { CartItem } from "@/types/cart";

// Private state
const items = ref<CartItem[]>([]);

// Public functions
export function useCart() {
  const count = computed(() => items.value.length);

  function addItem(item: CartItem): void {
    if (!hasItem(item.name)) {
      items.value.push(item);
      syncToStorage();
    }
  }

  function removeItem(name: string): void {
    items.value = items.value.filter((i) => i.name !== name);
    syncToStorage();
  }

  function hasItem(name: string): boolean {
    return items.value.some((i) => i.name === name);
  }

  function syncToStorage(): void {
    localStorage.setItem("vibery-cart", JSON.stringify(items.value));
  }

  return {
    items: readonly(items),
    count,
    addItem,
    removeItem,
    hasItem,
  };
}
```

**Guidelines:**

- Keep internal state private (prefix with `_` if exposed)
- Export typed functions
- Use `computed()` for derived values
- Use `watch()` for side effects
- Return readonly versions of state refs
- Document public API

### Search Composable Pattern (useSearch)

**Structure:**

```typescript
// composables/useSearch.ts
import MiniSearch from "minisearch";
import { ref, shallowRef } from "vue";

// Module-level singleton state
let searchIndex: MiniSearch | null = null;
let isIndexed = false;

export type SearchableItem = /* item with id + itemType + tags */;
export interface SearchResult {
  item: SearchableItem;
  score: number;
  match: Record<string, string[]>;
}

const query = ref("");
const results = shallowRef<SearchResult[]>([]);
const isSearching = ref(false);

function initializeIndex(templates: Template[], stacks: Stack[]) {
  if (isIndexed) return;
  searchIndex = new MiniSearch({
    fields: ["name", "description", "tags", "category", "type"],
    searchOptions: {
      boost: { name: 3, tags: 2, category: 1.5, description: 1 },
      fuzzy: 0.2,
      prefix: true,
    },
  });
  // Combine items, add index
  searchIndex.addAll(searchableItems);
  isIndexed = true;
}

export function useSearch() {
  function performSearch(q: string, templates: Template[], stacks: Stack[] = []) {
    query.value = q;
    isSearching.value = true;
    try {
      if (!isIndexed) initializeIndex(templates, stacks);
      results.value = search(q);
    } finally {
      isSearching.value = false;
    }
  }

  function clearSearch() {
    query.value = "";
    results.value = [];
  }

  return { query, results, isSearching, performSearch, clearSearch };
}
```

**Key Points:**

- **Singleton pattern:** Module-level state persists across component lifecycles
- **Lazy initialization:** Index created on first search
- **shallowRef for results:** Optimizes reactivity tracking
- **Fuzzy tolerance:** Dynamic based on query length
- **Field boosting:** Configurable weights for relevance
- **Result capping:** Max 8 items returned
- **Error handling:** Try/finally ensures isSearching clears

**See also:** `/docs/search-api.md` for complete API reference

---

## Styling & Design System

### Warm Terminal Classes

**Structure:** `warm-{type}-{shade}`

**Usage:**

```html
<!-- Backgrounds -->
<div class="warm-bg-deep">...</div>
<!-- Main bg #0c0a09 -->
<div class="warm-bg-surface">...</div>
<!-- Cards #1c1917 -->
<div class="warm-bg-elevated">...</div>
<!-- Hover #292524 -->
<div class="warm-bg-hover">...</div>
<!-- Active #44403c -->

<!-- Text -->
<p class="warm-text-primary">...</p>
<!-- #fafaf9 -->
<p class="warm-text-secondary">...</p>
<!-- #a8a29e -->
<p class="warm-text-tertiary">...</p>
<!-- #78716c -->
<p class="warm-text-muted">...</p>
<!-- #57534e -->

<!-- Accent -->
<button class="warm-accent">...</button>
<!-- #e07256 -->
<button class="warm-accent-hover">...</button>
<!-- #c85a42 -->
```

**Tailwind Config Reference:**

```javascript
// tailwind.config.mjs
colors: {
  warm: {
    bg: {
      deep: '#0c0a09',
      surface: '#1c1917',
      elevated: '#292524',
      hover: '#44403c',
    },
    text: {
      primary: '#fafaf9',
      secondary: '#a8a29e',
      tertiary: '#78716c',
      muted: '#57534e',
    },
    accent: '#e07256',
    'accent-hover': '#c85a42',
  },
}
```

### Responsive Design

**Breakpoints:** Tailwind defaults

```css
sm: 640px
md: 768px
lg: 1024px
xl: 1280px
2xl: 1536px
```

**Example:**

```html
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
  <!-- Auto-responsive grid -->
</div>
```

### Typography

**Fonts:**

- **Sans:** Satoshi (headings, UI)
- **Mono:** IBM Plex Mono (code blocks)

**Hierarchy:**

```html
<h1 class="text-4xl font-bold warm-text-primary">...</h1>
<h2 class="text-2xl font-bold warm-text-primary">...</h2>
<h3 class="text-xl font-semibold warm-text-primary">...</h3>
<p class="text-base warm-text-secondary">...</p>
<small class="text-sm warm-text-tertiary">...</small>
```

---

## TypeScript Conventions

### Type Definitions

**Location:** `/src/types/`

**Template Type Example:**

```typescript
// types/template.ts
export type TemplateType =
  | "agent"
  | "command"
  | "mcp"
  | "setting"
  | "hook"
  | "skill";

export interface Template {
  id: string;
  name: string;
  type: TemplateType;
  description: string;
  icon: string;
  category: string;
  tags: string[];
  installs: number;
  updated: string;
  source?: string;
  author?: string;
}

export interface Stack {
  id: string;
  name: string;
  description: string;
  icon: string;
  category: string;
  templates: StackTemplate[];
  tags: string[];
  credits?: string;
}

interface StackTemplate {
  type: TemplateType;
  name: string;
}
```

### Strict Mode

**tsconfig.json:**

```json
{
  "compilerOptions": {
    "strict": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "noImplicitAny": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true
  }
}
```

### Props & Emits

**Vue Props:**

```typescript
interface Props {
  template: Template;
  showIcon?: boolean;
  onSelect?: (id: string) => void;
}

const props = withDefaults(defineProps<Props>(), {
  showIcon: true,
});
```

**Vue Emits:**

```typescript
const emit = defineEmits<{
  add: [template: Template];
  remove: [id: string];
}>();

emit("add", template);
```

---

## State Management

### Composables as Stores

**Pattern:** Singleton composables with shared state

**useCart Example:**

```typescript
import { ref, readonly } from "vue";

const _items = ref<CartItem[]>([]);

export function useCart() {
  function addItem(item: CartItem) {
    if (!hasItem(item.name)) _items.value.push(item);
  }

  function getItems() {
    return readonly(_items);
  }

  return { addItem, getItems };
}
```

**Usage in Components:**

```vue
<script setup>
const { addItem, getItems } = useCart();

const items = getItems(); // Reactive, readonly
</script>
```

### Persistence

**localStorage:**

```typescript
function syncToStorage() {
  localStorage.setItem("vibery-cart", JSON.stringify(items.value));
}

function loadFromStorage() {
  const stored = localStorage.getItem("vibery-cart");
  if (stored) items.value = JSON.parse(stored);
}
```

**Guidelines:**

- Use `JSON.stringify` / `JSON.parse`
- Handle errors gracefully
- Clear on logout (future)

---

## API & Data Patterns

### JSON Data Files

**Location:** `/src/data/`

**Structure:** Flat array of objects

```json
{
  "templates": [
    {
      "id": "prompt-engineer",
      "name": "Prompt Engineer",
      "type": "agent",
      "description": "...",
      "icon": "ph-book",
      "category": "productivity",
      "tags": ["prompting", "ai"],
      "installs": 1250,
      "updated": "2024-12-21"
    }
  ]
}
```

### Data Access

**Static Build-Time:**

```astro
---
import { templates } from '../data/templates.json'

const agents = templates.filter(t => t.type === 'agent')
---
```

**Client-Side (Vue):**

```vue
<script setup>
import templates from "@/data/templates.json";

const searchResults = computed(() => {
  return templates.filter((t) =>
    t.name.toLowerCase().includes(query.value.toLowerCase()),
  );
});
</script>
```

---

## Testing Conventions

### Unit Tests (Vue Components)

**Tool:** Vitest (future setup)

```typescript
import { describe, it, expect } from "vitest";
import { mount } from "@vue/test-utils";
import CartSidebar from "@/components/vue/CartSidebar.vue";

describe("CartSidebar", () => {
  it("adds item to cart", () => {
    const wrapper = mount(CartSidebar);
    // Test assertions
  });
});
```

### Testing Composables

```typescript
import { useCart } from "@/composables/useCart";

describe("useCart", () => {
  it("adds item to cart", () => {
    const { addItem, items } = useCart();
    addItem({ name: "test", type: "agent" });
    expect(items.value).toHaveLength(1);
  });
});
```

---

## Performance Best Practices

### Bundle Optimization

- **Vue Islands:** Only hydrate interactive components
- **Code splitting:** Lazy-load modals on demand
- **Image optimization:** Use Phosphor icons (SVG)
- **CSS:** Tailwind purges unused classes

### Build-Time

- **Static generation:** Astro pre-renders all pages
- **SSG:** Dynamic routes use `getStaticPaths()`
- **Sitemap:** Auto-generated by @astrojs/sitemap

### Runtime

- **Computed properties:** Use for derived state
- **Memoization:** Cache filter results
- **Lazy loading:** Modals load on click

---

## Security Best Practices

### Input Validation

```typescript
// SearchBar.vue
const query = ref("");

watch(query, (newQuery) => {
  // Sanitize query (Tailwind Safe mode)
  const sanitized = newQuery.replace(/[<>]/g, "");
  // Filter templates
});
```

### XSS Prevention

- Vue auto-escapes templates
- Use `v-text` instead of `v-html` when possible
- No `dangerouslySetInnerHTML`

### CSP Headers

**Cloudflare:** Set via security.txt or worker

```
Content-Security-Policy: default-src 'self'; script-src 'self'
```

---

## Error Handling

### Component Error Boundary (Future)

```vue
<script setup>
import { ref, onErrorCaptured } from "vue";

const error = ref(null);

onErrorCaptured((err) => {
  error.value = err.message;
  return false; // Prevent propagation
});
</script>

<template>
  <div v-if="error" class="error-banner">{{ error }}</div>
  <slot v-else />
</template>
```

### Composable Error Handling

```typescript
export function useSearch() {
  const error = ref<string | null>(null);

  function search(query: string) {
    try {
      // Filter logic
    } catch (err) {
      error.value = (err as Error).message;
    }
  }

  return { search, error };
}
```

---

## Documentation Standards

### JSDoc Comments

```typescript
/**
 * Adds a template to the cart.
 * @param item - The template to add
 * @returns true if added, false if already exists
 */
function addItem(item: CartItem): boolean {
  // Implementation
}
```

### Component Docstrings

```vue
<!--
CartSidebar
Displays cart contents with item count and install command.

Props:
  - (none, uses useCart composable)

Events:
  - @install: User clicks install button
-->
```

---

## Migration Guidelines (Astro → Vue Islands)

### Phase 1: Identify Candidates

- [ ] Modal.astro (→ TemplateModal.vue)
- [ ] Cart.astro (→ CartSidebar.vue)
- [ ] FilterBar.astro (→ FilterBar.vue)
- [ ] ClientScripts.astro (→ Composables)

### Phase 2: Create Vue Component

```vue
<script setup lang="ts">
// Migrate logic from Astro frontend matter
</script>

<template>
  <!-- Migrate HTML template -->
</template>
```

### Phase 3: Create Composable (if state)

```typescript
// Extract reactive logic to composable
export function useXxx() {}
```

### Phase 4: Integration

```astro
---
// Import Vue component
import { CartSidebar } from '@/components/vue/CartSidebar.vue'
---

<CartSidebar client:load />
```

### Phase 5: Cleanup

- Remove old Astro component
- Remove duplicate logic from ClientScripts.astro
- Update imports

---

## Code Review Checklist

- [ ] Uses TypeScript (no `any`)
- [ ] Props are typed
- [ ] Composables are documented
- [ ] Warm Terminal classes used
- [ ] No neon-\* classes
- [ ] Responsive design (@sm, @md, @lg)
- [ ] Error handling included
- [ ] Performance optimized
- [ ] Security reviewed
- [ ] Tests added/updated

---

## Related Files

- `tailwind.config.mjs` - Warm Terminal theme config
- `astro.config.mjs` - Build configuration
- `tsconfig.json` - TypeScript configuration
- `/src/types/` - Type definitions
- `/src/composables/` - State management
