feat: register T0
This commit is contained in:
1 parent
a5873f8886
commit
3c3f7c6194
9 files changed
+920
-4
No files matched your search
@@ -60,6 +60,9 @@ const colorModeSelectUi = { trailing: 'hidden' }
|
||||
watch(
|
||||
() => props.title,
|
||||
(title) => {
|
||||
if (!title) {
|
||||
return
|
||||
}
|
||||
document.title = `${title} · RenewCMS`
|
||||
},
|
||||
{ immediate: true },
|
||||
@@ -67,7 +70,10 @@ watch(
|
||||
|
||||
watch(
|
||||
() => props.title,
|
||||
() => {
|
||||
(title) => {
|
||||
if (!title) {
|
||||
return
|
||||
}
|
||||
titleEl.value?.focus()
|
||||
},
|
||||
)
|
||||
@@ -86,7 +92,7 @@ watch(
|
||||
<AppLogo :size="32" />
|
||||
</template>
|
||||
|
||||
<template #title>
|
||||
<template v-if="title" #title>
|
||||
<h1 ref="titleEl" tabindex="-1" class="outline-none">{{ title }}</h1>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface PasswordRule {
|
||||
label: string
|
||||
met: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
rules: PasswordRule[]
|
||||
showErrors: boolean
|
||||
}>()
|
||||
|
||||
function itemClass(rule: PasswordRule) {
|
||||
if (rule.met) {
|
||||
return 'text-success'
|
||||
}
|
||||
return props.showErrors ? 'text-error' : 'text-toned'
|
||||
}
|
||||
|
||||
function itemIcon(rule: PasswordRule) {
|
||||
if (rule.met) {
|
||||
return 'i-lucide-circle-check'
|
||||
}
|
||||
return props.showErrors ? 'i-lucide-circle-alert' : 'i-lucide-circle'
|
||||
}
|
||||
|
||||
const items = computed(() =>
|
||||
props.rules.map((rule) => ({
|
||||
...rule,
|
||||
classes: itemClass(rule),
|
||||
icon: itemIcon(rule),
|
||||
})),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul class="flex flex-col gap-1">
|
||||
<li
|
||||
v-for="item in items"
|
||||
:key="item.label"
|
||||
:class="['flex items-center gap-1.5 text-sm', item.classes]"
|
||||
>
|
||||
<UIcon :name="item.icon" class="shrink-0" />
|
||||
{{ item.label }}
|
||||
<span class="sr-only">{{ item.met ? 'met' : 'not met' }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
@@ -0,0 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
||||
|
||||
interface ResultPanelAction {
|
||||
label: string
|
||||
color?: 'primary' | 'neutral'
|
||||
variant?: 'solid' | 'soft' | 'outline' | 'ghost'
|
||||
to?: string
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
tone: 'success' | 'error' | 'info'
|
||||
icon: string
|
||||
title: string
|
||||
description: string
|
||||
actions: ResultPanelAction[]
|
||||
}>()
|
||||
|
||||
const role = computed(() => (props.tone === 'error' ? 'alert' : 'status'))
|
||||
|
||||
const toneTileClass: Record<'success' | 'error' | 'info', string> = {
|
||||
success: 'bg-success/10 text-success',
|
||||
error: 'bg-error/10 text-error',
|
||||
info: 'bg-info/10 text-info',
|
||||
}
|
||||
|
||||
const tileClass = computed(() => toneTileClass[props.tone])
|
||||
|
||||
const titleEl = ref<HTMLHeadingElement | null>(null)
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
titleEl.value?.focus()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :role="role" class="flex flex-col gap-4">
|
||||
<div :class="['flex h-11 w-11 items-center justify-center', tileClass]">
|
||||
<UIcon :name="icon" class="h-6 w-6" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<h1 ref="titleEl" tabindex="-1" class="text-xl font-semibold leading-7 outline-none">
|
||||
{{ title }}
|
||||
</h1>
|
||||
<p class="text-toned text-sm">{{ description }}</p>
|
||||
</div>
|
||||
<div class="flex w-full flex-col gap-2">
|
||||
<UButton
|
||||
v-for="action in actions"
|
||||
:key="action.label"
|
||||
block
|
||||
size="lg"
|
||||
:color="action.color ?? 'primary'"
|
||||
:variant="action.variant ?? 'solid'"
|
||||
:to="action.to"
|
||||
@click="action.onClick"
|
||||
>
|
||||
{{ action.label }}
|
||||
</UButton>
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -29,6 +29,28 @@ describe('AuthShell renders its title as the page heading and sets the document
|
||||
})
|
||||
})
|
||||
|
||||
describe('AuthShell with an empty title renders no heading and leaves document.title untouched', () => {
|
||||
it('mounts directly with an empty title without rendering an h1 or touching document.title', () => {
|
||||
document.title = 'Existing title · RenewCMS'
|
||||
|
||||
const wrapper = mountShell({ title: '' })
|
||||
|
||||
expect(wrapper.find('h1').exists()).toBe(false)
|
||||
expect(document.title).toBe('Existing title · RenewCMS')
|
||||
})
|
||||
|
||||
it('leaves document.title untouched and does not crash when the title changes to empty', async () => {
|
||||
const wrapper = mountShell({ title: 'Create the owner account' })
|
||||
|
||||
expect(document.title).toBe('Create the owner account · RenewCMS')
|
||||
|
||||
await wrapper.setProps({ title: '' })
|
||||
|
||||
expect(wrapper.find('h1').exists()).toBe(false)
|
||||
expect(document.title).toBe('Create the owner account · RenewCMS')
|
||||
})
|
||||
})
|
||||
|
||||
describe('AuthShell renders its notice and footer slots', () => {
|
||||
it('renders the notice slot as-is inside the card, after the title, and the footer slot outside the card', () => {
|
||||
const wrapper = mountShell(
|
||||
|
||||
Reference in new issue
Block a user