feat(demo): add HintChip component with per-view localStorage dismiss
This commit is contained in:
parent
0697f119f6
commit
51b7dbb29b
2 changed files with 91 additions and 0 deletions
63
web/src/components/HintChip.vue
Normal file
63
web/src/components/HintChip.vue
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<template>
|
||||
<div v-if="!dismissed" class="hint-chip" role="status">
|
||||
<span aria-hidden="true" class="hint-chip__icon">💡</span>
|
||||
<span class="hint-chip__message">{{ message }}</span>
|
||||
<button
|
||||
class="hint-chip__dismiss"
|
||||
@click="dismiss"
|
||||
:aria-label="`Dismiss hint for ${viewKey}`"
|
||||
>✕</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
viewKey: string // used for localStorage key — e.g. 'home', 'review'
|
||||
message: string
|
||||
}>()
|
||||
|
||||
const LS_KEY = `peregrine_hint_${props.viewKey}`
|
||||
const dismissed = ref(!!localStorage.getItem(LS_KEY))
|
||||
|
||||
function dismiss(): void {
|
||||
localStorage.setItem(LS_KEY, '1')
|
||||
dismissed.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.hint-chip {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-2, 8px);
|
||||
background: var(--color-surface, #0d1829);
|
||||
border: 1px solid var(--app-primary, #2B6CB0);
|
||||
border-radius: var(--radius-md, 8px);
|
||||
padding: var(--space-2, 8px) var(--space-3, 12px);
|
||||
margin-bottom: var(--space-3, 12px);
|
||||
}
|
||||
|
||||
.hint-chip__icon { flex-shrink: 0; font-size: 0.9rem; }
|
||||
|
||||
.hint-chip__message {
|
||||
flex: 1;
|
||||
font-size: 0.85rem;
|
||||
color: var(--app-primary-light, #68A8D8);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.hint-chip__dismiss {
|
||||
flex-shrink: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-text-muted, #8898aa);
|
||||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
padding: 0 2px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.hint-chip__dismiss:hover { color: var(--color-text, #eaeff8); }
|
||||
</style>
|
||||
28
web/src/components/__tests__/HintChip.test.ts
Normal file
28
web/src/components/__tests__/HintChip.test.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import HintChip from '../HintChip.vue'
|
||||
|
||||
beforeEach(() => { localStorage.clear() })
|
||||
|
||||
const factory = (viewKey = 'home', message = 'Test hint') =>
|
||||
mount(HintChip, { props: { viewKey, message } })
|
||||
|
||||
describe('HintChip', () => {
|
||||
it('renders the message', () => {
|
||||
const w = factory()
|
||||
expect(w.text()).toContain('Test hint')
|
||||
})
|
||||
|
||||
it('is hidden when localStorage key is already set', () => {
|
||||
localStorage.setItem('peregrine_hint_home', '1')
|
||||
const w = factory()
|
||||
expect(w.find('.hint-chip').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('hides and sets localStorage when dismiss button is clicked', async () => {
|
||||
const w = factory()
|
||||
await w.find('.hint-chip__dismiss').trigger('click')
|
||||
expect(w.find('.hint-chip').exists()).toBe(false)
|
||||
expect(localStorage.getItem('peregrine_hint_home')).toBe('1')
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue