usePinnedPages
The reader's own shortlist of pages. Pinned from the page header, listed in the sidebar, kept across reloads.
vue
<script setup lang="ts">
import type { PinnedPage } from '@shared/composables/usePinnedPages'
import { usePinnedPages } from '@shared/composables/usePinnedPages'
import { Badge } from '@shared/components/ui/badge'
import { Button } from '@shared/components/ui/button'
import { computed } from 'vue'
const CANDIDATES: PinnedPage[] = [
{ name: 'docs-pin-dashboard', path: '/docs-demo/dashboard', title: 'Dashboard' },
{ name: 'docs-pin-invoices', path: '/docs-demo/invoices', title: 'Invoices' },
{ name: 'docs-pin-customers', path: '/docs-demo/customers', title: 'Customers' },
]
const { pinned, pin, unpin, isPinned, reorder } = usePinnedPages()
const mine = computed(() => pinned.value.filter(p => p.path.startsWith('/docs-demo/')))
function toggle(page: PinnedPage): void {
if (isPinned(page.path))
unpin(page.path)
else
pin(page)
}
function moveUp(index: number): void {
const list = [...pinned.value]
const target = mine.value[index]
const at = list.findIndex(p => p.path === target.path)
const above = list.slice(0, at).reverse().find(p => p.path.startsWith('/docs-demo/'))
if (!above)
return
const aboveAt = list.findIndex(p => p.path === above.path)
list.splice(at, 1)
list.splice(aboveAt, 0, target)
reorder(list)
}
</script>
<template>
<div class="flex flex-col gap-4">
<div class="flex flex-wrap gap-2">
<Button
v-for="page in CANDIDATES"
:key="page.path"
size="sm"
variant="outline"
:aria-pressed="isPinned(page.path)"
@click="toggle(page)"
>
{{ isPinned(page.path) ? 'Unpin' : 'Pin' }} {{ page.title }}
</Button>
</div>
<div>
<p class="sa-text-sm font-semibold">
Pinned, in order
</p>
<ol v-if="mine.length" class="mt-2 flex flex-col gap-2">
<li v-for="(page, index) in mine" :key="page.path" class="flex items-center gap-2">
<Badge variant="outline">
{{ page.title }}
</Badge>
<Button size="sm" variant="ghost" :disabled="index === 0" @click="moveUp(index)">
Move up
</Button>
</li>
</ol>
<p v-else class="mt-1 sa-text-sm text-muted-foreground">
Nothing pinned yet.
</p>
</div>
</div>
</template>Import
ts
import { usePinnedPages } from '@shared/composables/usePinnedPages'Signature
ts
function usePinnedPages(): {
pinned: Readonly<Ref<PinnedPage[]>>
pin: (page: PinnedPage) => void
unpin: (path: string) => void
isPinned: (path: string) => boolean
reorder: (newList: PinnedPage[]) => void
}
interface PinnedPage {
name: string
path: string
title: string
icon?: string
}Returns
| Member | Type | Description |
|---|---|---|
pinned | Readonly<Ref<PinnedPage[]>> | The list, in the reader's own order. |
pin | (page) => void | Appends. Ignored if that path is already pinned. |
unpin | (path) => void | Removes by path. |
isPinned | (path) => boolean | For a toggle's pressed state. |
reorder | (newList) => void | Replaces the whole list, for drag-and-drop. |
Identity is the path, not the name or the route name. Deduping, unpinning and isPinned all match on it.
Example
vue
<script setup lang="ts">
import { usePinnedPages } from '@shared/composables/usePinnedPages'
import { computed } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const { pin, unpin, isPinned } = usePinnedPages()
const pinnedHere = computed(() => isPinned(route.path))
function toggle() {
if (pinnedHere.value) {
unpin(route.path)
}
else {
pin({
name: String(route.name ?? route.path),
path: route.path,
title: String(route.meta.title ?? route.path),
})
}
}
</script>
<template>
<button type="button" :aria-pressed="pinnedHere" @click="toggle()">
{{ pinnedHere ? 'Unpin this page' : 'Pin this page' }}
</button>
</template>Notes
- Persisted under
sa-pinned, as a module-scope singleton, so the sidebar list and the page header's pin are the same state rather than two copies that drift. - Every write replaces the array rather than mutating it, which is what makes the persisted storage notice the change.
pinappends, so a new pin goes to the end. Callreorderto move it.iconis part of the type and is stored, but nothing renders it yet.pinnedis readonly. Usereorderfor a wholesale change.
Related
- Page header — where the pin toggle lives.
- Navigation — where the list is shown.