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:
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user