M0: Vue 3 + TypeScript frontend — auth views + authed board shell

- Vite + Vue 3.5 + Pinia + vue-router + Tailwind (brand accent #F5C518),
  dark-mode aware, deterministic package-lock.json for `npm ci`.
- Session store (fetchMe/login/register/logout) over a credentials:'include'
  fetch client; router guards (requiresAuth / guestOnly) with lazy /me resolve.
- BaseButton + BaseInput primitives (focus rings, loading, error states).
- LoginView, RegisterView, and an authed BoardView shell with an empty state
  for the M1 masonry board — all at v1 polish (rule 24).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
This commit is contained in:
2026-07-19 13:17:38 -04:00
co-authored by Claude Opus 4.8
parent 04e3ab20cf
commit bf3d403648
20 changed files with 3173 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
<script setup lang="ts">
withDefaults(
defineProps<{
type?: "button" | "submit";
variant?: "primary" | "ghost";
loading?: boolean;
disabled?: boolean;
}>(),
{ type: "button", variant: "primary", loading: false, disabled: false },
);
</script>
<template>
<button
:type="type"
:disabled="disabled || loading"
class="inline-flex items-center justify-center gap-2 rounded-lg px-4 py-2.5 text-sm font-semibold transition
focus:outline-none focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-2
focus-visible:ring-offset-neutral-50 dark:focus-visible:ring-offset-neutral-950
disabled:cursor-not-allowed disabled:opacity-60"
:class="
variant === 'primary'
? 'bg-brand text-neutral-900 shadow-sm hover:bg-brand-600 active:bg-brand-700'
: 'text-neutral-700 hover:bg-neutral-200/70 dark:text-neutral-200 dark:hover:bg-neutral-800'
"
>
<span
v-if="loading"
class="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent"
aria-hidden="true"
/>
<slot />
</button>
</template>
+35
View File
@@ -0,0 +1,35 @@
<script setup lang="ts">
defineProps<{
id: string;
label: string;
modelValue: string;
type?: string;
autocomplete?: string;
placeholder?: string;
error?: string;
required?: boolean;
}>();
defineEmits<{ (e: "update:modelValue", value: string): void }>();
</script>
<template>
<div class="flex flex-col gap-1.5">
<label :for="id" class="text-sm font-medium text-neutral-700 dark:text-neutral-300">{{ label }}</label>
<input
:id="id"
:type="type ?? 'text'"
:value="modelValue"
:autocomplete="autocomplete"
:placeholder="placeholder"
:required="required"
:aria-invalid="error ? 'true' : undefined"
class="rounded-lg border bg-white px-3 py-2.5 text-sm text-neutral-900 shadow-sm transition
placeholder:text-neutral-400 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand
dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder:text-neutral-500"
:class="error ? 'border-red-400 dark:border-red-500' : 'border-neutral-300 dark:border-neutral-700'"
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
/>
<p v-if="error" class="text-xs text-red-600 dark:text-red-400">{{ error }}</p>
</div>
</template>