test
This commit is contained in:
parent
efd21a7297
commit
441676be27
97 changed files with 10764 additions and 223 deletions
49
frontend/src/components/ui/AppAlert.vue
Normal file
49
frontend/src/components/ui/AppAlert.vue
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
import AppButton from "@/components/ui/AppButton.vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
tone?: "danger" | "warning" | "info" | "success";
|
||||
dismissible?: boolean;
|
||||
}>(),
|
||||
{
|
||||
tone: "info",
|
||||
dismissible: false,
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{ dismiss: [] }>();
|
||||
|
||||
const toneClass = computed(() => {
|
||||
if (props.tone === "danger") {
|
||||
return "border-rose-300 bg-rose-50 text-rose-900 dark:border-rose-800 dark:bg-rose-950/45 dark:text-rose-200";
|
||||
}
|
||||
if (props.tone === "warning") {
|
||||
return "border-amber-300 bg-amber-50 text-amber-900 dark:border-amber-800 dark:bg-amber-950/45 dark:text-amber-200";
|
||||
}
|
||||
if (props.tone === "success") {
|
||||
return "border-emerald-300 bg-emerald-50 text-emerald-900 dark:border-emerald-800 dark:bg-emerald-950/45 dark:text-emerald-200";
|
||||
}
|
||||
return "border-brand-300 bg-brand-50 text-brand-900 dark:border-brand-800 dark:bg-brand-950/45 dark:text-brand-200";
|
||||
});
|
||||
|
||||
const alertRole = computed(() => (props.tone === "danger" || props.tone === "warning" ? "alert" : "status"));
|
||||
const alertLive = computed(() => (props.tone === "danger" || props.tone === "warning" ? "assertive" : "polite"));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-start justify-between gap-3 rounded-xl border px-4 py-3 text-sm"
|
||||
:class="toneClass"
|
||||
:role="alertRole"
|
||||
:aria-live="alertLive"
|
||||
>
|
||||
<div class="space-y-1">
|
||||
<strong v-if="$slots.title" class="block font-semibold"><slot name="title" /></strong>
|
||||
<slot />
|
||||
</div>
|
||||
<AppButton v-if="props.dismissible" variant="ghost" @click="emit('dismiss')">Dismiss</AppButton>
|
||||
</div>
|
||||
</template>
|
||||
33
frontend/src/components/ui/AppBadge.vue
Normal file
33
frontend/src/components/ui/AppBadge.vue
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
tone?: "neutral" | "success" | "warning" | "danger" | "info";
|
||||
}>();
|
||||
|
||||
const toneClass = computed(() => {
|
||||
if (props.tone === "success") {
|
||||
return "border-emerald-300 bg-emerald-50 text-emerald-700 dark:border-emerald-800 dark:bg-emerald-950/50 dark:text-emerald-300";
|
||||
}
|
||||
|
||||
if (props.tone === "warning") {
|
||||
return "border-amber-300 bg-amber-50 text-amber-800 dark:border-amber-800 dark:bg-amber-950/50 dark:text-amber-300";
|
||||
}
|
||||
|
||||
if (props.tone === "danger") {
|
||||
return "border-rose-300 bg-rose-50 text-rose-700 dark:border-rose-800 dark:bg-rose-950/50 dark:text-rose-300";
|
||||
}
|
||||
|
||||
if (props.tone === "info") {
|
||||
return "border-brand-300 bg-brand-50 text-brand-700 dark:border-brand-800 dark:bg-brand-950/50 dark:text-brand-300";
|
||||
}
|
||||
|
||||
return "border-zinc-300 bg-zinc-100 text-zinc-700 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300";
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium" :class="toneClass">
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
43
frontend/src/components/ui/AppButton.vue
Normal file
43
frontend/src/components/ui/AppButton.vue
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
variant?: "primary" | "secondary" | "ghost" | "danger";
|
||||
type?: "button" | "submit" | "reset";
|
||||
disabled?: boolean;
|
||||
}>(),
|
||||
{
|
||||
variant: "primary",
|
||||
type: "button",
|
||||
disabled: false,
|
||||
},
|
||||
);
|
||||
|
||||
const variantClass = computed(() => {
|
||||
if (props.variant === "secondary") {
|
||||
return "border-zinc-300 bg-zinc-100 text-zinc-900 hover:bg-zinc-200 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700";
|
||||
}
|
||||
|
||||
if (props.variant === "ghost") {
|
||||
return "border-zinc-300 bg-transparent text-zinc-700 hover:bg-zinc-100 dark:border-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-800";
|
||||
}
|
||||
|
||||
if (props.variant === "danger") {
|
||||
return "border-rose-300 bg-rose-100 text-rose-800 hover:bg-rose-200 dark:border-rose-800 dark:bg-rose-950/60 dark:text-rose-200 dark:hover:bg-rose-900/70";
|
||||
}
|
||||
|
||||
return "border-brand-700 bg-brand-700 text-white hover:bg-brand-600 dark:border-brand-400 dark:bg-brand-400 dark:text-zinc-950 dark:hover:bg-brand-300";
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="inline-flex min-h-10 items-center justify-center rounded-lg border px-3 py-2 text-sm font-semibold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-100 disabled:cursor-not-allowed disabled:opacity-60 dark:focus-visible:ring-brand-400 dark:focus-visible:ring-offset-zinc-950"
|
||||
:class="variantClass"
|
||||
:type="props.type"
|
||||
:disabled="props.disabled"
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
5
frontend/src/components/ui/AppCard.vue
Normal file
5
frontend/src/components/ui/AppCard.vue
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<article class="min-w-0 rounded-2xl border border-zinc-200 bg-white p-5 shadow-panel dark:border-zinc-800 dark:bg-zinc-900">
|
||||
<slot />
|
||||
</article>
|
||||
</template>
|
||||
22
frontend/src/components/ui/AppCheckbox.vue
Normal file
22
frontend/src/components/ui/AppCheckbox.vue
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<script setup lang="ts">
|
||||
const model = defineModel<boolean>({ default: false });
|
||||
|
||||
defineProps<{
|
||||
id?: string;
|
||||
disabled?: boolean;
|
||||
label?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<label class="inline-flex min-h-10 items-center gap-2 text-sm text-zinc-800 dark:text-zinc-200" :for="id">
|
||||
<input
|
||||
:id="id"
|
||||
v-model="model"
|
||||
class="h-4 w-4 rounded border-zinc-400 text-brand-600 focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-100 dark:border-zinc-600 dark:bg-zinc-900 dark:focus-visible:ring-brand-400 dark:focus-visible:ring-offset-zinc-950"
|
||||
type="checkbox"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
<span><slot>{{ label }}</slot></span>
|
||||
</label>
|
||||
</template>
|
||||
16
frontend/src/components/ui/AppEmptyState.vue
Normal file
16
frontend/src/components/ui/AppEmptyState.vue
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
title: string;
|
||||
body: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="rounded-xl border border-dashed border-zinc-300 bg-zinc-50 p-5 dark:border-zinc-700 dark:bg-zinc-900/60">
|
||||
<h3 class="font-display text-lg font-semibold text-zinc-900 dark:text-zinc-100">{{ title }}</h3>
|
||||
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400">{{ body }}</p>
|
||||
<div v-if="$slots.default" class="mt-4">
|
||||
<slot />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
21
frontend/src/components/ui/AppInput.vue
Normal file
21
frontend/src/components/ui/AppInput.vue
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<script setup lang="ts">
|
||||
const model = defineModel<string>({ default: "" });
|
||||
|
||||
defineProps<{
|
||||
id?: string;
|
||||
type?: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
v-model="model"
|
||||
class="w-full rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-900 outline-none ring-brand-300 transition placeholder:text-zinc-400 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-100 disabled:cursor-not-allowed disabled:opacity-60 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-100 dark:ring-brand-400 dark:focus-visible:ring-offset-zinc-950 dark:placeholder:text-zinc-500"
|
||||
:id="id"
|
||||
:type="type || 'text'"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
</template>
|
||||
17
frontend/src/components/ui/AppModal.vue
Normal file
17
frontend/src/components/ui/AppModal.vue
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
open: boolean;
|
||||
title: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{ close: [] }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="props.open" class="fixed inset-0 z-50 grid place-items-center bg-zinc-950/60 p-4" @click.self="emit('close')">
|
||||
<div class="w-full max-w-lg rounded-2xl border border-zinc-200 bg-white p-6 shadow-panel dark:border-zinc-800 dark:bg-zinc-900">
|
||||
<h3 class="mb-4 font-display text-xl font-semibold text-zinc-900 dark:text-zinc-100">{{ props.title }}</h3>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
19
frontend/src/components/ui/AppSelect.vue
Normal file
19
frontend/src/components/ui/AppSelect.vue
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<script setup lang="ts">
|
||||
const model = defineModel<string>({ default: "" });
|
||||
|
||||
defineProps<{
|
||||
id?: string;
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<select
|
||||
v-model="model"
|
||||
class="w-full rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-900 outline-none ring-brand-300 transition focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-100 disabled:cursor-not-allowed disabled:opacity-60 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-100 dark:ring-brand-400 dark:focus-visible:ring-offset-zinc-950"
|
||||
:id="id"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<slot />
|
||||
</select>
|
||||
</template>
|
||||
15
frontend/src/components/ui/AppSkeleton.vue
Normal file
15
frontend/src/components/ui/AppSkeleton.vue
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
lines?: number;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-2" role="status" aria-live="polite">
|
||||
<span
|
||||
v-for="line in lines || 3"
|
||||
:key="line"
|
||||
class="block h-3 animate-pulse rounded-full bg-zinc-200 dark:bg-zinc-800"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
31
frontend/src/components/ui/AppTable.vue
Normal file
31
frontend/src/components/ui/AppTable.vue
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
label?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="max-w-full overflow-x-auto rounded-xl border border-zinc-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-100 dark:border-zinc-800 dark:focus-visible:ring-brand-400 dark:focus-visible:ring-offset-zinc-950"
|
||||
tabindex="0"
|
||||
>
|
||||
<table class="min-w-full border-collapse bg-white text-sm dark:bg-zinc-900" :aria-label="label || 'Data table'">
|
||||
<slot />
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(th),
|
||||
:deep(td) {
|
||||
@apply border-b border-zinc-200 px-3 py-2 text-left align-top dark:border-zinc-800;
|
||||
}
|
||||
|
||||
:deep(th) {
|
||||
@apply sticky top-0 z-10 bg-zinc-100 font-semibold text-zinc-800 dark:bg-zinc-900 dark:text-zinc-200;
|
||||
}
|
||||
|
||||
:deep(td) {
|
||||
@apply text-zinc-700 dark:text-zinc-300;
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue