Skip to content

Commit edb9e08

Browse files
committed
docs(createSelection): add bookmark manager example files
1 parent eb4c9de commit edb9e08

File tree

4 files changed

+310
-0
lines changed

4 files changed

+310
-0
lines changed
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
<script setup lang="ts">
2+
import { computed, shallowRef } from 'vue'
3+
import { mdiPin, mdiPinOutline } from '@mdi/js'
4+
import { Checkbox, useProxyRegistry } from '@vuetify/v0'
5+
import { useBookmarks } from './context'
6+
7+
import type { BookmarkTicket } from './context'
8+
9+
const bookmarks = useBookmarks()
10+
const proxy = useProxyRegistry<BookmarkTicket>(bookmarks)
11+
12+
const title = shallowRef('')
13+
const url = shallowRef('')
14+
const tags = shallowRef('')
15+
const filter = shallowRef<string | null>(null)
16+
17+
const allTags = computed(() => {
18+
const set = new Set<string>()
19+
for (const ticket of proxy.values) {
20+
for (const tag of ticket.tags) {
21+
set.add(tag)
22+
}
23+
}
24+
return Array.from(set).toSorted()
25+
})
26+
27+
const filtered = computed(() => {
28+
if (!filter.value) return proxy.values
29+
return proxy.values.filter(t => t.tags.includes(filter.value!))
30+
})
31+
32+
function onAdd () {
33+
const t = title.value.trim()
34+
const u = url.value.trim()
35+
if (!t || !u) return
36+
bookmarks.add(t, u, tags.value.split(',').map(s => s.trim()).filter(Boolean))
37+
title.value = ''
38+
url.value = ''
39+
tags.value = ''
40+
}
41+
42+
function onSelectAll () {
43+
for (const ticket of filtered.value) {
44+
if (!ticket.disabled) bookmarks.select(ticket.id)
45+
}
46+
}
47+
48+
function onUnselectAll () {
49+
for (const ticket of filtered.value) {
50+
bookmarks.unselect(ticket.id)
51+
}
52+
}
53+
</script>
54+
55+
<template>
56+
<div class="space-y-4">
57+
<!-- Stats -->
58+
<div class="flex items-center gap-4 text-xs text-on-surface-variant">
59+
<span>{{ bookmarks.stats.value.total }} bookmarks</span>
60+
<span class="text-primary">{{ bookmarks.stats.value.selected }} selected</span>
61+
<span v-if="bookmarks.stats.value.pinned > 0" class="text-warning">{{ bookmarks.stats.value.pinned }} pinned</span>
62+
</div>
63+
64+
<!-- Pinned bookmarks -->
65+
<div v-if="bookmarks.pinnedIds.size > 0" class="flex gap-2 flex-wrap">
66+
<span
67+
v-for="id in bookmarks.pinnedIds"
68+
:key="id"
69+
class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs bg-warning/10 text-warning"
70+
>
71+
<svg class="size-3" viewBox="0 0 24 24"><path :d="mdiPin" fill="currentColor" /></svg>
72+
{{ bookmarks.get(id)?.value }}
73+
</span>
74+
</div>
75+
76+
<!-- Add bookmark -->
77+
<div class="flex gap-2 flex-wrap">
78+
<input
79+
v-model="title"
80+
class="flex-1 min-w-30 px-3 py-1.5 text-sm rounded-lg border border-divider bg-surface text-on-surface placeholder:text-on-surface-variant outline-none focus:border-primary"
81+
placeholder="Title"
82+
>
83+
<input
84+
v-model="url"
85+
class="flex-1 min-w-30 px-3 py-1.5 text-sm rounded-lg border border-divider bg-surface text-on-surface placeholder:text-on-surface-variant outline-none focus:border-primary"
86+
placeholder="https://..."
87+
>
88+
<input
89+
v-model="tags"
90+
class="flex-1 min-w-30 px-3 py-1.5 text-sm rounded-lg border border-divider bg-surface text-on-surface placeholder:text-on-surface-variant outline-none focus:border-primary"
91+
placeholder="Tags (comma-separated)"
92+
@keydown.enter="onAdd"
93+
>
94+
<button
95+
class="px-3 py-1.5 text-sm rounded-lg bg-primary text-on-primary hover:bg-primary/90 disabled:opacity-40 transition-colors"
96+
:disabled="!title.trim() || !url.trim()"
97+
@click="onAdd"
98+
>
99+
Add
100+
</button>
101+
</div>
102+
103+
<!-- Filters + bulk actions -->
104+
<div class="flex items-center gap-1.5 flex-wrap">
105+
<button
106+
class="px-2 py-0.5 text-xs rounded-md border transition-all"
107+
:class="filter === null
108+
? 'border-primary bg-primary/10 text-primary font-medium'
109+
: 'border-divider text-on-surface-variant hover:border-primary/50'"
110+
@click="filter = null"
111+
>
112+
All
113+
</button>
114+
<button
115+
v-for="tag in allTags"
116+
:key="tag"
117+
class="px-2 py-0.5 text-xs rounded-md border transition-all"
118+
:class="filter === tag
119+
? 'border-primary bg-primary/10 text-primary font-medium'
120+
: 'border-divider text-on-surface-variant hover:border-primary/50'"
121+
@click="filter = filter === tag ? null : tag"
122+
>
123+
{{ tag }}
124+
</button>
125+
<span class="flex-1" />
126+
<button
127+
class="text-xs text-on-surface-variant hover:text-primary transition-colors"
128+
@click="onSelectAll"
129+
>
130+
Select all
131+
</button>
132+
<span class="text-on-surface-variant/30">|</span>
133+
<button
134+
class="text-xs text-on-surface-variant hover:text-primary transition-colors"
135+
@click="onUnselectAll"
136+
>
137+
Clear
138+
</button>
139+
</div>
140+
141+
<!-- Bookmark list -->
142+
<div class="space-y-1">
143+
<div
144+
v-for="ticket in filtered"
145+
:key="ticket.id"
146+
class="group flex items-center gap-3 px-3 py-2 rounded-lg border transition-all"
147+
:class="[
148+
ticket.disabled
149+
? 'border-divider/50 opacity-40 cursor-not-allowed'
150+
: bookmarks.selected(ticket.id)
151+
? 'border-primary/30 bg-primary/5'
152+
: 'border-divider hover:border-primary/30',
153+
]"
154+
>
155+
<Checkbox.Root
156+
class="size-4.5 rounded border-2 flex items-center justify-center transition-all shrink-0"
157+
:class="bookmarks.selected(ticket.id)
158+
? 'border-primary bg-primary'
159+
: 'border-divider hover:border-primary'"
160+
:disabled="!!ticket.disabled"
161+
:model-value="bookmarks.selected(ticket.id)"
162+
@update:model-value="bookmarks.toggle(ticket.id)"
163+
>
164+
<Checkbox.Indicator class="text-on-primary text-xs">✓</Checkbox.Indicator>
165+
</Checkbox.Root>
166+
167+
<div class="flex-1 min-w-0">
168+
<span
169+
class="text-sm truncate"
170+
:class="bookmarks.selected(ticket.id) ? 'text-primary font-medium' : 'text-on-surface'"
171+
>
172+
{{ ticket.value }}
173+
</span>
174+
<span class="text-[11px] text-on-surface-variant/60 truncate block">{{ ticket.url }}</span>
175+
</div>
176+
177+
<div class="flex items-center gap-1.5">
178+
<span
179+
v-for="tag in ticket.tags"
180+
:key="tag"
181+
class="px-1.5 py-0.5 text-[10px] rounded font-medium bg-surface-variant/50 text-on-surface-variant"
182+
>
183+
{{ tag }}
184+
</span>
185+
</div>
186+
187+
<button
188+
v-if="!ticket.disabled"
189+
class="transition-all"
190+
:class="bookmarks.pinned(ticket.id) ? 'text-warning' : 'text-on-surface-variant/30 hover:text-warning/60'"
191+
@click="bookmarks.pinned(ticket.id) ? bookmarks.unpin(ticket.id) : bookmarks.pin(ticket.id)"
192+
>
193+
<svg class="size-4" viewBox="0 0 24 24">
194+
<path :d="bookmarks.pinned(ticket.id) ? mdiPin : mdiPinOutline" fill="currentColor" />
195+
</svg>
196+
</button>
197+
</div>
198+
199+
<p
200+
v-if="filtered.length === 0"
201+
class="text-center text-sm text-on-surface-variant py-4"
202+
>
203+
{{ filter ? `No bookmarks tagged "${filter}".` : 'No bookmarks yet.' }}
204+
</p>
205+
</div>
206+
207+
<!-- Selection summary -->
208+
<div
209+
v-if="bookmarks.selectedIds.size > 0"
210+
class="flex items-center justify-between rounded-lg bg-primary/5 border border-primary/20 px-3 py-2"
211+
>
212+
<span class="text-xs text-primary font-medium">
213+
{{ bookmarks.selectedIds.size }} bookmark{{ bookmarks.selectedIds.size === 1 ? '' : 's' }} selected
214+
</span>
215+
<button
216+
class="text-xs text-primary hover:underline"
217+
@click="bookmarks.selectedIds.clear()"
218+
>
219+
Deselect all
220+
</button>
221+
</div>
222+
</div>
223+
</template>
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<script setup lang="ts">
2+
import { computed, shallowReactive } from 'vue'
3+
import { useProxyRegistry } from '@vuetify/v0'
4+
5+
import { createBookmarks, provideBookmarks, type BookmarkInput } from './context'
6+
import type { ID } from '@vuetify/v0'
7+
8+
const selection = createBookmarks()
9+
const proxy = useProxyRegistry(selection)
10+
const pinnedIds = shallowReactive(new Set<ID>())
11+
12+
selection.onboard([
13+
{ id: 'vue', value: 'Vue.js', url: 'https://vuejs.org', tags: ['framework'] },
14+
{ id: 'vite', value: 'Vite', url: 'https://vite.dev', tags: ['tooling'] },
15+
{ id: 'pinia', value: 'Pinia', url: 'https://pinia.vuejs.org', tags: ['framework'] },
16+
{ id: 'vitest', value: 'Vitest', url: 'https://vitest.dev', tags: ['tooling'] },
17+
{ id: 'mdn', value: 'MDN Web Docs', url: 'https://developer.mozilla.org', tags: ['reference'] },
18+
{ id: 'caniuse', value: 'Can I Use', url: 'https://caniuse.com', tags: ['reference'] },
19+
{ id: 'legacy', value: 'Legacy API (deprecated)', url: 'https://example.com', tags: ['reference'], disabled: true },
20+
] satisfies BookmarkInput[])
21+
22+
pinnedIds.add('vue')
23+
24+
const stats = computed(() => ({
25+
total: proxy.size,
26+
selected: selection.selectedIds.size,
27+
pinned: pinnedIds.size,
28+
}))
29+
30+
function add (title: string, url: string, tags: string[] = []) {
31+
return selection.register({ value: title, url, tags })
32+
}
33+
34+
function pin (id: ID) {
35+
pinnedIds.add(id)
36+
}
37+
38+
function unpin (id: ID) {
39+
pinnedIds.delete(id)
40+
}
41+
42+
function pinned (id: ID) {
43+
return pinnedIds.has(id)
44+
}
45+
46+
provideBookmarks({ ...selection, pinnedIds, stats, add, pin, unpin, pinned })
47+
</script>
48+
49+
<template>
50+
<slot />
51+
</template>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<script setup lang="ts">
2+
import BookmarkConsumer from './BookmarkConsumer.vue'
3+
import BookmarkProvider from './BookmarkProvider.vue'
4+
</script>
5+
6+
<template>
7+
<BookmarkProvider>
8+
<BookmarkConsumer />
9+
</BookmarkProvider>
10+
</template>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { createContext, createSelection } from '@vuetify/v0'
2+
import type { ID, SelectionContext, SelectionTicket, SelectionTicketInput } from '@vuetify/v0'
3+
import type { ComputedRef, Reactive } from 'vue'
4+
5+
export interface BookmarkInput extends SelectionTicketInput<string> {
6+
url: string
7+
tags: string[]
8+
disabled?: boolean
9+
}
10+
11+
export type BookmarkTicket = SelectionTicket<BookmarkInput> & BookmarkInput
12+
13+
export interface BookmarkContext extends SelectionContext<BookmarkInput, BookmarkTicket> {
14+
pinnedIds: Reactive<Set<ID>>
15+
stats: ComputedRef<{ total: number, selected: number, pinned: number }>
16+
add: (title: string, url: string, tags?: string[]) => BookmarkTicket
17+
pin: (id: ID) => void
18+
unpin: (id: ID) => void
19+
pinned: (id: ID) => boolean
20+
}
21+
22+
export const [useBookmarks, provideBookmarks] = createContext<BookmarkContext>('v0:bookmarks')
23+
24+
export function createBookmarks () {
25+
return createSelection<BookmarkInput, BookmarkTicket>({ multiple: true, events: true })
26+
}

0 commit comments

Comments
 (0)