mirror of
https://github.com/fmhy/edit.git
synced 2025-07-28 23:02:16 +10:00
feat: startpage
This commit is contained in:
parent
6418da63ba
commit
8d6c3ace0b
19 changed files with 7804 additions and 6133 deletions
|
@ -10,6 +10,5 @@ services:
|
|||
ports:
|
||||
- '4173:4173'
|
||||
|
||||
|
||||
networks:
|
||||
fmhy:
|
||||
|
|
|
@ -152,8 +152,10 @@ export default defineConfig({
|
|||
search,
|
||||
footer: {
|
||||
message: `${feedback} (rev: ${commitRef})`,
|
||||
copyright: `© ${new Date().getFullYear()}, <a href="https://i.ibb.co/pLVXBSh/image.png">Estd 2018.</a>` + `<br/> This site does not host any files.`
|
||||
},
|
||||
copyright:
|
||||
`© ${new Date().getFullYear()}, <a href="https://i.ibb.co/pLVXBSh/image.png">Estd 2018.</a>` +
|
||||
`<br/> This site does not host any files.`
|
||||
},
|
||||
editLink: {
|
||||
pattern: 'https://github.com/fmhy/edit/edit/main/docs/:path',
|
||||
text: '📝 Edit this page'
|
||||
|
|
|
@ -153,7 +153,10 @@ export const nav: DefaultTheme.NavItem[] = [
|
|||
{ text: '🔖 Bookmarks', link: 'https://github.com/fmhy/bookmarks' },
|
||||
{ text: '✅ SafeGuard', link: 'https://github.com/fmhy/FMHY-SafeGuard' },
|
||||
{ text: '📋 snowbin', link: 'https://pastes.fmhy.net' },
|
||||
{ text: '®️ Redlib', link: 'https://redlib.fmhy.net/r/FREEMEDIAHECKYEAH/wiki/index' },
|
||||
{
|
||||
text: '®️ Redlib',
|
||||
link: 'https://redlib.fmhy.net/r/FREEMEDIAHECKYEAH/wiki/index'
|
||||
},
|
||||
{ text: '🔎 SearXNG', link: 'https://searx.fmhy.net/' },
|
||||
{
|
||||
text: '💡 Site Hunting',
|
||||
|
|
|
@ -266,7 +266,8 @@ const toggleCard = () => (isCardShown.value = !isCardShown.value)
|
|||
placeholder="What a lovely wiki!"
|
||||
/>
|
||||
<p class="desc mb-2">
|
||||
Add your Discord handle if you would like a response, or if we need more information from you, otherwise join our
|
||||
Add your Discord handle if you would like a response, or if we need
|
||||
more information from you, otherwise join our
|
||||
<a
|
||||
class="text-primary text-underline font-semibold"
|
||||
href="https://rentry.co/FMHY-Invite/"
|
||||
|
|
733
docs/.vitepress/theme/components/startpage/Bookmarks.vue
Normal file
733
docs/.vitepress/theme/components/startpage/Bookmarks.vue
Normal file
|
@ -0,0 +1,733 @@
|
|||
<script setup lang="ts">
|
||||
import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogRoot,
|
||||
DialogTitle,
|
||||
DialogTrigger
|
||||
} from 'reka-ui'
|
||||
import { useRouter } from 'vitepress'
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
export type BookmarkType = {
|
||||
name: string
|
||||
chord: string
|
||||
url: string
|
||||
icon?: string // e.g. 'i-logos:github'
|
||||
color?: string
|
||||
isCustom?: boolean // Track if it's a custom bookmark
|
||||
customSvg?: string // Store custom SVG icon
|
||||
}
|
||||
|
||||
const props = defineProps<{ isInputGloballyFocused: boolean }>()
|
||||
|
||||
const currentChordInput = ref('')
|
||||
const activePossibleChords = ref<BookmarkType[]>([])
|
||||
const customBookmarks = ref<BookmarkType[]>([])
|
||||
const allBookmarks = ref<BookmarkType[]>([])
|
||||
|
||||
// Dialog states
|
||||
const isAddDialogOpen = ref(false)
|
||||
const isEditDialogOpen = ref(false)
|
||||
const isDeleteDialogOpen = ref(false)
|
||||
|
||||
// Form states
|
||||
const formData = ref<BookmarkType>({
|
||||
name: '',
|
||||
chord: '',
|
||||
url: '',
|
||||
icon: '',
|
||||
color: '',
|
||||
isCustom: true,
|
||||
customSvg: ''
|
||||
})
|
||||
|
||||
const editingBookmark = ref<BookmarkType | null>(null)
|
||||
const deletingBookmark = ref<BookmarkType | null>(null)
|
||||
|
||||
let chordTimeout: NodeJS.Timeout | null = null
|
||||
|
||||
const initialBookmarksData: BookmarkType[] = [
|
||||
{
|
||||
name: 'Hacker News',
|
||||
chord: 'HN',
|
||||
url: 'https://news.ycombinator.com/',
|
||||
icon: 'i-logos:ycombinator'
|
||||
},
|
||||
{
|
||||
name: 'GitHub',
|
||||
chord: 'GH',
|
||||
url: 'https://github.com/',
|
||||
icon: 'i-simple-icons:github'
|
||||
},
|
||||
{
|
||||
name: 'Reddit',
|
||||
chord: 'RD',
|
||||
url: 'https://reddit.com/',
|
||||
icon: 'i-logos:reddit-icon'
|
||||
},
|
||||
{
|
||||
name: 'Twitter',
|
||||
chord: 'TW',
|
||||
url: 'https://twitter.com/',
|
||||
icon: 'i-logos:twitter'
|
||||
},
|
||||
{
|
||||
name: 'YouTube',
|
||||
chord: 'YT',
|
||||
url: 'https://youtube.com/',
|
||||
icon: 'i-logos:youtube-icon'
|
||||
},
|
||||
{
|
||||
name: 'Wikipedia',
|
||||
chord: 'WK',
|
||||
url: 'https://wikipedia.org/',
|
||||
icon: 'i-simple-icons:wikipedia'
|
||||
},
|
||||
{
|
||||
name: "Beginner's Guide",
|
||||
chord: 'BG',
|
||||
url: '/beginners-guide',
|
||||
icon: 'i-lucide:book-open-text'
|
||||
},
|
||||
{
|
||||
name: 'Wotaku',
|
||||
chord: 'WT',
|
||||
url: 'https://wotaku.wiki/',
|
||||
icon: 'i-twemoji:flag-japan'
|
||||
},
|
||||
{
|
||||
name: 'privateersclub',
|
||||
chord: 'PC',
|
||||
url: 'https://megathread.pages.dev/',
|
||||
icon: 'i-custom:privateersclub'
|
||||
}
|
||||
]
|
||||
|
||||
// Load custom bookmarks from localStorage
|
||||
const loadCustomBookmarks = () => {
|
||||
try {
|
||||
const stored = localStorage.getItem('customBookmarks')
|
||||
if (stored) {
|
||||
customBookmarks.value = JSON.parse(stored)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading custom bookmarks:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Save custom bookmarks to localStorage
|
||||
const saveCustomBookmarks = () => {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
'customBookmarks',
|
||||
JSON.stringify(customBookmarks.value)
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Error saving custom bookmarks:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Update all bookmarks when custom bookmarks change
|
||||
const updateAllBookmarks = () => {
|
||||
allBookmarks.value = [...initialBookmarksData, ...customBookmarks.value]
|
||||
}
|
||||
|
||||
// Watch for changes in custom bookmarks
|
||||
watch(
|
||||
customBookmarks,
|
||||
() => {
|
||||
updateAllBookmarks()
|
||||
saveCustomBookmarks()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
const resetChord = () => {
|
||||
currentChordInput.value = ''
|
||||
activePossibleChords.value = []
|
||||
if (chordTimeout) clearTimeout(chordTimeout)
|
||||
chordTimeout = null
|
||||
}
|
||||
|
||||
const handleBookmarkClick = (bookmark: BookmarkType) => {
|
||||
if (bookmark.url.startsWith('/')) {
|
||||
router.go(bookmark.url)
|
||||
} else {
|
||||
window.open(bookmark.url, '_self')
|
||||
}
|
||||
}
|
||||
|
||||
// Form validation
|
||||
const isFormValid = () => {
|
||||
return (
|
||||
formData.value.name.trim() &&
|
||||
formData.value.chord.trim() &&
|
||||
formData.value.url.trim() &&
|
||||
!isChordTaken(formData.value.chord, editingBookmark.value?.chord)
|
||||
)
|
||||
}
|
||||
|
||||
const isChordTaken = (chord: string, excludeChord?: string) => {
|
||||
return allBookmarks.value.some(
|
||||
(b) =>
|
||||
b.chord.toUpperCase() === chord.toUpperCase() && b.chord !== excludeChord
|
||||
)
|
||||
}
|
||||
|
||||
// Reset form
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
name: '',
|
||||
chord: '',
|
||||
url: '',
|
||||
icon: '',
|
||||
color: '',
|
||||
isCustom: true,
|
||||
customSvg: ''
|
||||
}
|
||||
}
|
||||
|
||||
// Add bookmark
|
||||
const handleAddBookmark = () => {
|
||||
if (!isFormValid()) return
|
||||
|
||||
const newBookmark: BookmarkType = {
|
||||
...formData.value,
|
||||
chord: formData.value.chord.toUpperCase(),
|
||||
isCustom: true
|
||||
}
|
||||
|
||||
// If no icon and no custom SVG, use default website icon
|
||||
if (!newBookmark.icon && !newBookmark.customSvg) {
|
||||
newBookmark.icon = 'i-lucide:globe'
|
||||
}
|
||||
|
||||
customBookmarks.value.push(newBookmark)
|
||||
isAddDialogOpen.value = false
|
||||
resetForm()
|
||||
}
|
||||
|
||||
// Edit bookmark
|
||||
const openEditDialog = (bookmark: BookmarkType) => {
|
||||
editingBookmark.value = bookmark
|
||||
formData.value = { ...bookmark }
|
||||
isEditDialogOpen.value = true
|
||||
}
|
||||
|
||||
const handleEditBookmark = () => {
|
||||
if (!isFormValid() || !editingBookmark.value) return
|
||||
|
||||
const index = customBookmarks.value.findIndex(
|
||||
(b) => b === editingBookmark.value
|
||||
)
|
||||
if (index !== -1) {
|
||||
customBookmarks.value[index] = {
|
||||
...formData.value,
|
||||
chord: formData.value.chord.toUpperCase(),
|
||||
isCustom: true
|
||||
}
|
||||
|
||||
// If no icon and no custom SVG, use default website icon
|
||||
if (
|
||||
!customBookmarks.value[index].icon &&
|
||||
!customBookmarks.value[index].customSvg
|
||||
) {
|
||||
customBookmarks.value[index].icon = 'i-lucide:globe'
|
||||
}
|
||||
}
|
||||
|
||||
isEditDialogOpen.value = false
|
||||
editingBookmark.value = null
|
||||
resetForm()
|
||||
}
|
||||
|
||||
// Delete bookmark
|
||||
const openDeleteDialog = (bookmark: BookmarkType) => {
|
||||
deletingBookmark.value = bookmark
|
||||
isDeleteDialogOpen.value = true
|
||||
}
|
||||
|
||||
const handleDeleteBookmark = () => {
|
||||
if (!deletingBookmark.value) return
|
||||
|
||||
const index = customBookmarks.value.findIndex(
|
||||
(b) => b === deletingBookmark.value
|
||||
)
|
||||
if (index !== -1) {
|
||||
customBookmarks.value.splice(index, 1)
|
||||
}
|
||||
|
||||
isDeleteDialogOpen.value = false
|
||||
deletingBookmark.value = null
|
||||
}
|
||||
|
||||
// Handle SVG input
|
||||
const handleSvgInput = (event: Event) => {
|
||||
const target = event.target as HTMLTextAreaElement
|
||||
formData.value.customSvg = target.value
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (
|
||||
props.isInputGloballyFocused ||
|
||||
e.altKey ||
|
||||
e.metaKey ||
|
||||
e.ctrlKey ||
|
||||
e.shiftKey
|
||||
)
|
||||
return
|
||||
|
||||
const active = document.activeElement as HTMLElement | null
|
||||
if (
|
||||
active?.tagName === 'INPUT' ||
|
||||
active?.tagName === 'TEXTAREA' ||
|
||||
active?.isContentEditable
|
||||
)
|
||||
return
|
||||
|
||||
const key = e.key.toUpperCase()
|
||||
if (chordTimeout) clearTimeout(chordTimeout)
|
||||
|
||||
if (!currentChordInput.value) {
|
||||
const matches = allBookmarks.value.filter((b) => b.chord.startsWith(key))
|
||||
if (matches.length) {
|
||||
e.preventDefault()
|
||||
currentChordInput.value = key
|
||||
activePossibleChords.value = matches
|
||||
chordTimeout = setTimeout(resetChord, 2000)
|
||||
}
|
||||
} else {
|
||||
const next = currentChordInput.value + key
|
||||
const match = activePossibleChords.value.find((b) => b.chord === next)
|
||||
if (match) {
|
||||
if (match.url.startsWith('/')) {
|
||||
router.go(match.url)
|
||||
} else {
|
||||
window.open(match.url, '_self')
|
||||
}
|
||||
resetChord()
|
||||
} else {
|
||||
const filtered = allBookmarks.value.filter((b) =>
|
||||
b.chord.startsWith(next)
|
||||
)
|
||||
if (filtered.length) {
|
||||
currentChordInput.value = next
|
||||
activePossibleChords.value = filtered
|
||||
chordTimeout = setTimeout(resetChord, 2000)
|
||||
} else {
|
||||
resetChord()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadCustomBookmarks()
|
||||
updateAllBookmarks()
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', handleKeyDown)
|
||||
if (chordTimeout) clearTimeout(chordTimeout)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2 text-text-2">
|
||||
<i class="i-lucide:bookmark w-5 h-5" />
|
||||
<h2 class="text-xl">Bookmarks</h2>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
v-if="currentChordInput"
|
||||
class="px-3 py-1 rounded-md text-sm font-medium bg-yellow-200/20 text-yellow-600"
|
||||
>
|
||||
Chord: {{ currentChordInput }}...
|
||||
</div>
|
||||
|
||||
<!-- Add Bookmark Button -->
|
||||
<DialogRoot v-model:open="isAddDialogOpen">
|
||||
<DialogTrigger as-child>
|
||||
<button
|
||||
class="flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium bg-bg-alt text-white hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<i class="i-lucide:plus w-4 h-4" />
|
||||
Add Bookmark
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogPortal>
|
||||
<DialogOverlay class="fixed inset-0 bg-black/50 z-50" />
|
||||
<DialogContent
|
||||
description="Add New Bookmark"
|
||||
class="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-bg border border-div rounded-lg p-6 w-full max-w-md z-50 max-h-[90vh] overflow-y-auto"
|
||||
>
|
||||
<DialogTitle class="text-lg font-semibold text-text mb-4">
|
||||
Add New Bookmark
|
||||
</DialogTitle>
|
||||
<DialogDescription class="text-text-2 mb-6">
|
||||
Add a new bookmark
|
||||
</DialogDescription>
|
||||
<form @submit.prevent="handleAddBookmark" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text-2 mb-1">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
v-model="formData.name"
|
||||
type="text"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-div rounded-md bg-bg-alt text-text focus:border-primary outline-none"
|
||||
placeholder="Bookmark name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text-2 mb-1">
|
||||
Chord (2 letters)
|
||||
</label>
|
||||
<input
|
||||
v-model="formData.chord"
|
||||
type="text"
|
||||
required
|
||||
maxlength="2"
|
||||
class="w-full px-3 py-2 border border-div rounded-md bg-bg-alt text-text focus:border-primary outline-none uppercase"
|
||||
placeholder="AB"
|
||||
@input="
|
||||
(e) =>
|
||||
(formData.chord = (
|
||||
e.target as HTMLInputElement
|
||||
).value.toUpperCase())
|
||||
"
|
||||
/>
|
||||
<p
|
||||
v-if="isChordTaken(formData.chord)"
|
||||
class="text-red-500 text-xs mt-1"
|
||||
>
|
||||
This chord is already taken
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text-2 mb-1">
|
||||
URL
|
||||
</label>
|
||||
<input
|
||||
v-model="formData.url"
|
||||
type="url"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-div rounded-md bg-bg-alt text-text focus:border-primary outline-none"
|
||||
placeholder="https://example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text-2 mb-1">
|
||||
Icon (UnoCSS class)
|
||||
</label>
|
||||
<input
|
||||
v-model="formData.icon"
|
||||
type="text"
|
||||
class="w-full px-3 py-2 border border-div rounded-md bg-bg-alt text-text focus:border-primary outline-none"
|
||||
placeholder="i-lucide:globe (optional)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text-2 mb-1">
|
||||
Custom SVG Icon
|
||||
</label>
|
||||
<textarea
|
||||
v-model="formData.customSvg"
|
||||
class="w-full px-3 py-2 border border-div rounded-md bg-bg-alt text-text focus:border-primary outline-none h-20 resize-none"
|
||||
placeholder="Paste SVG code here (optional)"
|
||||
/>
|
||||
<p class="text-xs text-text-2 mt-1">
|
||||
If provided, this will override the icon class
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text-2 mb-1">
|
||||
Color
|
||||
</label>
|
||||
<input
|
||||
v-model="formData.color"
|
||||
type="text"
|
||||
class="w-full px-3 py-2 border border-div rounded-md bg-bg-alt text-text focus:border-primary outline-none"
|
||||
placeholder="#3B82F6 (optional)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 pt-4">
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="!isFormValid()"
|
||||
class="flex-1 bg-primary text-white py-2 px-4 rounded-md font-medium hover:opacity-80 disabled:opacity-50 disabled:cursor-not-allowed transition-opacity"
|
||||
>
|
||||
Add Bookmark
|
||||
</button>
|
||||
<DialogClose as-child>
|
||||
<button
|
||||
type="button"
|
||||
class="flex-1 bg-bg-alt text-text py-2 px-4 rounded-md font-medium border border-div hover:bg-bg-elv transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</DialogClose>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</DialogRoot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 xl:grid-cols-6 gap-2"
|
||||
>
|
||||
<div
|
||||
v-for="bookmark in allBookmarks"
|
||||
:key="bookmark.name"
|
||||
class="relative group"
|
||||
>
|
||||
<button
|
||||
:class="[
|
||||
'w-full rounded-md border border-div bg-bg-alt px-3 py-2 text-left transition-opacity duration-150',
|
||||
activePossibleChords.some((ab) => ab.chord === bookmark.chord)
|
||||
? bookmark.chord === currentChordInput
|
||||
? 'opacity-100 ring-2 ring-primary ring-offset-2 ring-offset-bg'
|
||||
: 'opacity-75'
|
||||
: currentChordInput
|
||||
? 'opacity-30'
|
||||
: 'opacity-100'
|
||||
]"
|
||||
@click="handleBookmarkClick(bookmark)"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-1 sm:gap-3 w-full"
|
||||
>
|
||||
<div class="flex items-center gap-2 truncate">
|
||||
<!-- Custom SVG Icon -->
|
||||
<div
|
||||
v-if="bookmark.customSvg"
|
||||
class="shrink-0 w-4 h-4"
|
||||
v-html="bookmark.customSvg"
|
||||
/>
|
||||
<!-- Regular Icon -->
|
||||
<i
|
||||
v-else-if="bookmark.icon"
|
||||
:class="`shrink-0 w-4 h-4 ${bookmark.icon}`"
|
||||
:style="bookmark.color ? { color: bookmark.color } : {}"
|
||||
/>
|
||||
<!-- Fallback Icon -->
|
||||
<i v-else class="shrink-0 w-4 h-4 i-lucide:globe" />
|
||||
|
||||
<span class="truncate font-medium">{{ bookmark.name }}</span>
|
||||
</div>
|
||||
<div class="hidden sm:flex text-xs items-center gap-1 text-text-2">
|
||||
<kbd
|
||||
v-for="(char, i) in bookmark.chord.split('')"
|
||||
:key="i"
|
||||
class="bg-bg border border-div px-1 py-0.5 rounded text-sm font-semibold"
|
||||
>
|
||||
{{ char }}
|
||||
</kbd>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<!-- Edit/Delete buttons for custom bookmarks -->
|
||||
<div
|
||||
v-if="bookmark.isCustom"
|
||||
class="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity flex gap-1"
|
||||
>
|
||||
<button
|
||||
@click.stop="openEditDialog(bookmark)"
|
||||
class="p-1 bg-bg-elv border border-div rounded hover:bg-bg-alt transition-colors"
|
||||
title="Edit bookmark"
|
||||
>
|
||||
<i class="i-lucide:edit-2 w-3 h-3 text-text-2" />
|
||||
</button>
|
||||
<button
|
||||
@click.stop="openDeleteDialog(bookmark)"
|
||||
class="p-1 bg-bg-elv border border-div rounded hover:bg-red-100 hover:text-red-600 transition-colors"
|
||||
title="Delete bookmark"
|
||||
>
|
||||
<i class="i-lucide:trash-2 w-3 h-3 text-text-2" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Dialog -->
|
||||
<DialogRoot v-model:open="isEditDialogOpen">
|
||||
<DialogPortal>
|
||||
<DialogOverlay class="fixed inset-0 bg-black/50 z-50" />
|
||||
<DialogContent
|
||||
class="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-bg border border-div rounded-lg p-6 w-full max-w-md z-50 max-h-[90vh] overflow-y-auto"
|
||||
>
|
||||
<DialogTitle class="text-lg font-semibold text-text mb-4">
|
||||
Edit Bookmark
|
||||
</DialogTitle>
|
||||
<DialogDescription class="text-text-2 mb-6">
|
||||
Editing "{{ editingBookmark?.name }}"
|
||||
</DialogDescription>
|
||||
<form @submit.prevent="handleEditBookmark" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text-2 mb-1">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
v-model="formData.name"
|
||||
type="text"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-div rounded-md bg-bg-alt text-text focus:border-primary outline-none"
|
||||
placeholder="Bookmark name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text-2 mb-1">
|
||||
Chord (2 letters)
|
||||
</label>
|
||||
<input
|
||||
v-model="formData.chord"
|
||||
type="text"
|
||||
required
|
||||
maxlength="2"
|
||||
class="w-full px-3 py-2 border border-div rounded-md bg-bg-alt text-text focus:border-primary outline-none uppercase"
|
||||
placeholder="AB"
|
||||
@input="
|
||||
(e) =>
|
||||
(formData.chord = (
|
||||
e.target as HTMLInputElement
|
||||
).value.toUpperCase())
|
||||
"
|
||||
/>
|
||||
<p
|
||||
v-if="isChordTaken(formData.chord, editingBookmark?.chord)"
|
||||
class="text-red-500 text-xs mt-1"
|
||||
>
|
||||
This chord is already taken
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text-2 mb-1">
|
||||
URL
|
||||
</label>
|
||||
<input
|
||||
v-model="formData.url"
|
||||
type="url"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-div rounded-md bg-bg-alt text-text focus:border-primary outline-none"
|
||||
placeholder="https://example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text-2 mb-1">
|
||||
Icon (UnoCSS class)
|
||||
</label>
|
||||
<input
|
||||
v-model="formData.icon"
|
||||
type="text"
|
||||
class="w-full px-3 py-2 border border-div rounded-md bg-bg-alt text-text focus:border-primary outline-none"
|
||||
placeholder="i-lucide:globe (optional)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text-2 mb-1">
|
||||
Custom SVG Icon
|
||||
</label>
|
||||
<textarea
|
||||
v-model="formData.customSvg"
|
||||
class="w-full px-3 py-2 border border-div rounded-md bg-bg-alt text-text focus:border-primary outline-none h-20 resize-none"
|
||||
placeholder="Paste SVG code here (optional)"
|
||||
/>
|
||||
<p class="text-xs text-text-2 mt-1">
|
||||
If provided, this will override the icon class
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text-2 mb-1">
|
||||
Color
|
||||
</label>
|
||||
<input
|
||||
v-model="formData.color"
|
||||
type="text"
|
||||
class="w-full px-3 py-2 border border-div rounded-md bg-bg-alt text-text focus:border-primary outline-none"
|
||||
placeholder="#3B82F6 (optional)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 pt-4">
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="!isFormValid()"
|
||||
class="flex-1 bg-primary text-white py-2 px-4 rounded-md font-medium hover:opacity-80 disabled:opacity-50 disabled:cursor-not-allowed transition-opacity"
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
<DialogClose as-child>
|
||||
<button
|
||||
type="button"
|
||||
class="flex-1 bg-bg-alt text-text py-2 px-4 rounded-md font-medium border border-div hover:bg-bg-elv transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</DialogClose>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</DialogRoot>
|
||||
|
||||
<!-- Delete Confirmation Dialog -->
|
||||
<DialogRoot v-model:open="isDeleteDialogOpen">
|
||||
<DialogPortal>
|
||||
<DialogOverlay class="fixed inset-0 bg-black/50 z-50" />
|
||||
<DialogContent
|
||||
class="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-bg border border-div rounded-lg p-6 w-full max-w-md z-50"
|
||||
>
|
||||
<DialogTitle class="text-lg font-semibold text-text mb-2">
|
||||
Delete Bookmark
|
||||
</DialogTitle>
|
||||
<DialogDescription class="text-text-2 mb-6">
|
||||
Are you sure you want to delete "{{ deletingBookmark?.name }}"? This
|
||||
action cannot be undone.
|
||||
</DialogDescription>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
@click="handleDeleteBookmark"
|
||||
class="flex-1 bg-red-600 text-white py-2 px-4 rounded-md font-medium hover:bg-red-700 transition-colors"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<DialogClose as-child>
|
||||
<button
|
||||
type="button"
|
||||
class="flex-1 bg-bg-alt text-text py-2 px-4 rounded-md font-medium border border-div hover:bg-bg-elv transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</DialogClose>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</DialogRoot>
|
||||
</div>
|
||||
</template>
|
29
docs/.vitepress/theme/components/startpage/Clock.vue
Normal file
29
docs/.vitepress/theme/components/startpage/Clock.vue
Normal file
|
@ -0,0 +1,29 @@
|
|||
<template>
|
||||
<div class="text-6xl font-bold text-text">
|
||||
{{ timeString }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
const time = ref<Date | null>(null)
|
||||
|
||||
function updateTime() {
|
||||
time.value = new Date()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateTime()
|
||||
const interval = setInterval(updateTime, 1000)
|
||||
onUnmounted(() => clearInterval(interval))
|
||||
})
|
||||
|
||||
const timeString = computed(() => {
|
||||
if (!time.value) return '--:--:--'
|
||||
const h = String(time.value.getHours()).padStart(2, '0')
|
||||
const m = String(time.value.getMinutes()).padStart(2, '0')
|
||||
const s = String(time.value.getSeconds()).padStart(2, '0')
|
||||
return `${h}:${m}:${s}`
|
||||
})
|
||||
</script>
|
194
docs/.vitepress/theme/components/startpage/SearchBar.vue
Normal file
194
docs/.vitepress/theme/components/startpage/SearchBar.vue
Normal file
|
@ -0,0 +1,194 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import Clock from './Clock.vue'
|
||||
|
||||
export interface PlatformType {
|
||||
name: string
|
||||
key: string
|
||||
url: string
|
||||
icon?: string
|
||||
color?: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
onFocusChange: (focused: boolean) => void
|
||||
initialQuery?: string
|
||||
}>()
|
||||
|
||||
const platforms: PlatformType[] = [
|
||||
{
|
||||
name: 'SearXNG',
|
||||
key: 'a',
|
||||
url: 'https://searx.fmhy.net/search?q=',
|
||||
icon: 'i-simple-icons:searxng'
|
||||
},
|
||||
{
|
||||
name: 'ChatGPT',
|
||||
key: 's',
|
||||
url: 'https://chat.openai.com/?q=',
|
||||
icon: 'i-simple-icons:openai'
|
||||
},
|
||||
{
|
||||
name: 'Claude',
|
||||
key: 'd',
|
||||
url: 'https://claude.ai/chat/',
|
||||
icon: 'i-logos:claude-icon'
|
||||
},
|
||||
{
|
||||
name: 'Perplexity',
|
||||
key: 'f',
|
||||
url: 'https://www.perplexity.ai/search?q=',
|
||||
icon: 'i-logos:perplexity-icon'
|
||||
}
|
||||
]
|
||||
|
||||
const inputRef = ref<HTMLInputElement | null>(null)
|
||||
const query = ref(props.initialQuery ?? '')
|
||||
const isInputFocused = ref(false)
|
||||
const showShortcuts = ref(false)
|
||||
|
||||
function handleInputFocus() {
|
||||
isInputFocused.value = true
|
||||
props.onFocusChange(true)
|
||||
}
|
||||
|
||||
function handleInputBlur() {
|
||||
isInputFocused.value = false
|
||||
props.onFocusChange(false)
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
if (!query.value.trim()) return
|
||||
const google = platforms.find((p) => p.name === 'SearX') || platforms[0]
|
||||
if (google)
|
||||
window.open(google.url + encodeURIComponent(query.value.trim()), '_self')
|
||||
}
|
||||
|
||||
function handlePlatformClick(platform: PlatformType) {
|
||||
if (!query.value.trim()) return
|
||||
window.open(platform.url + encodeURIComponent(query.value.trim()), '_self')
|
||||
}
|
||||
|
||||
function platformClass() {
|
||||
const base =
|
||||
'widget-card group relative widget-button rounded-md bg-bg-elv p-2 border transition-transform'
|
||||
const disabled = !query.value.trim()
|
||||
const highlight = showShortcuts.value && isInputFocused.value
|
||||
return [
|
||||
base,
|
||||
disabled ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer',
|
||||
highlight ? 'border-2 border-primary scale-105' : 'border-div'
|
||||
].join(' ')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
const active = document.activeElement
|
||||
const isSearchFocused = inputRef.value === active
|
||||
|
||||
if (e.key === '/' && !isSearchFocused) {
|
||||
const typingInInput =
|
||||
active &&
|
||||
(active.tagName === 'INPUT' ||
|
||||
active.tagName === 'TEXTAREA' ||
|
||||
(active instanceof HTMLElement && active.isContentEditable))
|
||||
if (!typingInInput) {
|
||||
e.preventDefault()
|
||||
inputRef.value?.focus()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (isInputFocused.value && e.altKey) {
|
||||
const key = e.key.toLowerCase()
|
||||
let platform = platforms.find((p) => p.key === key)
|
||||
|
||||
if (!platform && e.code.startsWith('Key') && e.code.length === 4) {
|
||||
const codeKey = e.code.slice(3).toLowerCase()
|
||||
platform = platforms.find((p) => p.key === codeKey)
|
||||
}
|
||||
|
||||
if (platform && query.value.trim()) {
|
||||
e.preventDefault()
|
||||
window.open(
|
||||
platform.url + encodeURIComponent(query.value.trim()),
|
||||
'_self'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (e.altKey) showShortcuts.value = true
|
||||
}
|
||||
|
||||
const handleKeyUp = (e: KeyboardEvent) => {
|
||||
if (!e.altKey) showShortcuts.value = false
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
window.addEventListener('keyup', handleKeyUp)
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('keydown', handleKeyDown)
|
||||
window.removeEventListener('keyup', handleKeyUp)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col items-start w-full space-y-4 antialiased">
|
||||
<Clock />
|
||||
|
||||
<form @submit.prevent="handleSubmit" class="relative w-full">
|
||||
<div class="relative">
|
||||
<i
|
||||
class="i-lucide-search absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-text-2"
|
||||
/>
|
||||
<input
|
||||
ref="inputRef"
|
||||
v-model="query"
|
||||
@focus="handleInputFocus"
|
||||
@blur="handleInputBlur"
|
||||
placeholder="What would you like to search for?"
|
||||
class="w-full pl-10 pr-3 py-3 text-lg rounded-md shadow-sm transition-colors bg-bg-elv text-text border-2 outline-none border-div hover:border-primary"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4 w-full">
|
||||
<button
|
||||
v-for="platform in platforms"
|
||||
:key="platform.name"
|
||||
:disabled="!query.trim()"
|
||||
@click="handlePlatformClick(platform)"
|
||||
:class="platformClass()"
|
||||
:style="platform.color ? { borderColor: platform.color } : {}"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<i
|
||||
v-if="platform.icon"
|
||||
:class="`w-5 h-5 ${platform.icon}`"
|
||||
:style="platform.color ? { color: platform.color } : {}"
|
||||
/>
|
||||
<div class="text-left flex-grow">
|
||||
<h3 class="font-semibold truncate">{{ platform.name }}</h3>
|
||||
</div>
|
||||
<div
|
||||
class="hidden sm:flex items-center gap-1 text-xs ml-auto text-white"
|
||||
>
|
||||
<kbd
|
||||
class="bg-bg border border-div px-1 py-0.5 rounded text-sm font-semibold"
|
||||
>
|
||||
Alt
|
||||
</kbd>
|
||||
<span class="text-white font-semibold">+</span>
|
||||
<kbd
|
||||
class="bg-bg border border-div px-1 py-0.5 rounded text-sm font-semibold"
|
||||
>
|
||||
{{ platform.key.toUpperCase() }}
|
||||
</kbd>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
49
docs/.vitepress/theme/components/startpage/Startpage.vue
Normal file
49
docs/.vitepress/theme/components/startpage/Startpage.vue
Normal file
|
@ -0,0 +1,49 @@
|
|||
<script setup lang="ts">
|
||||
import Bookmarks from './Bookmarks.vue'
|
||||
import Searchbar from './SearchBar.vue'
|
||||
|
||||
const isFocused = ref(false)
|
||||
const handleFocusChange = (focused: boolean) => {
|
||||
isFocused.value = focused
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen flex pt-48 justify-center p-4 transition-colors">
|
||||
<div class="w-full max-w-7xl space-y-8">
|
||||
<Searchbar @focus-change="handleFocusChange" />
|
||||
<Bookmarks :is-input-globally-focused="isFocused" />
|
||||
|
||||
<div class="hidden sm:block space-y-2 text-sm text-text-2">
|
||||
<p>
|
||||
Press
|
||||
<kbd class="kbd">/</kbd>
|
||||
anywhere to focus the search box
|
||||
</p>
|
||||
<p>
|
||||
Use
|
||||
<kbd class="kbd">Alt + a/s/d/f</kbd>
|
||||
to search different platforms
|
||||
</p>
|
||||
<p>
|
||||
Type bookmark chords (like
|
||||
<kbd class="kbd px-1.5 py-0.5">H</kbd>
|
||||
<kbd class="kbd px-1.5 py-0.5">N</kbd>
|
||||
for Hacker News) when search is not focused
|
||||
</p>
|
||||
<p>
|
||||
Press
|
||||
<kbd class="kbd">Enter</kbd>
|
||||
to search SearXNG (hosted by us) by default
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
kbd {
|
||||
--uno: px-1.5 py-0.5 rounded-sm font-sans font-semibold text-xs bg-bg-alt
|
||||
text-text-2 border-2 border-div nowrap;
|
||||
}
|
||||
</style>
|
|
@ -318,38 +318,32 @@ const transformLinks = (text: string): string =>
|
|||
{
|
||||
name: 'Windows',
|
||||
find: /(?<=\/ (\/>|[^/\r\n])*)(,\s)?(?<![a-z]\s)Windows(?=,|[ \t]\/|$)/gm,
|
||||
replace:
|
||||
' <div alt="Windows" class="i-qlementine-icons:windows-24" /> '
|
||||
replace: ' <div alt="Windows" class="i-qlementine-icons:windows-24" /> '
|
||||
},
|
||||
{
|
||||
name: 'Mac',
|
||||
find: /(?<=\/ (\/>|[^/\r\n])*)(,\s)?(?<![a-z]\s)Mac(?=,|[ \t]\/|$)/gm,
|
||||
replace:
|
||||
' <div alt="Mac" class="i-qlementine-icons:mac-fill-16" /> '
|
||||
replace: ' <div alt="Mac" class="i-qlementine-icons:mac-fill-16" /> '
|
||||
},
|
||||
{
|
||||
name: 'Linux',
|
||||
find: /(?<=\/ (\/>|[^/\r\n])*)(,\s)?(?<![a-z]\s)Linux(?=,|[ \t]\/|$)/gm,
|
||||
replace:
|
||||
' <div alt="Linux" class="i-fluent-mdl2:linux-logo-32" /> '
|
||||
replace: ' <div alt="Linux" class="i-fluent-mdl2:linux-logo-32" /> '
|
||||
},
|
||||
{
|
||||
name: 'Android',
|
||||
find: /(?<=\/ (\/>|[^/\r\n])*)(,\s)?(?<![a-z]\s)Android(?=,|[ \t]\/|$)/gm,
|
||||
replace:
|
||||
' <div alt="Android" class="i-material-symbols:android" /> '
|
||||
replace: ' <div alt="Android" class="i-material-symbols:android" /> '
|
||||
},
|
||||
{
|
||||
name: 'iOS',
|
||||
find: /(?<=\/ (\/>|[^/\r\n])*)(,\s)?(?<![a-z]\s)iOS(?=,|[ \t]\/|$)/gm,
|
||||
replace:
|
||||
' <div alt="iOS" class="i-simple-icons:ios" /> '
|
||||
replace: ' <div alt="iOS" class="i-simple-icons:ios" /> '
|
||||
},
|
||||
{
|
||||
name: 'Web',
|
||||
find: /(?<=\/ (\/>|[^/\r\n])*)(,\s)?(?<![a-z]\s)Web(?=,|[ \t]\/|$)/gm,
|
||||
replace:
|
||||
' <div alt="Web" class="i-fluent:globe-32-filled" /> '
|
||||
replace: ' <div alt="Web" class="i-fluent:globe-32-filled" /> '
|
||||
}
|
||||
])
|
||||
.getText()
|
||||
|
|
|
@ -22,7 +22,7 @@ interface Header {
|
|||
export const headers: Header = {
|
||||
'adblockvpnguide.md': {
|
||||
title: 'Adblocking / Privacy',
|
||||
description: "Adblocking, Privacy, VPNs, Proxies, Antiviruses"
|
||||
description: 'Adblocking, Privacy, VPNs, Proxies, Antiviruses'
|
||||
},
|
||||
'ai.md': {
|
||||
title: 'Artificial Intelligence',
|
||||
|
@ -139,7 +139,8 @@ export const excluded = [
|
|||
'single-page',
|
||||
'feedback.md',
|
||||
'index.md',
|
||||
'sandbox.md'
|
||||
'sandbox.md',
|
||||
'startpage.md'
|
||||
]
|
||||
|
||||
export function getHeader(id: string) {
|
||||
|
|
11
docs/startpage.md
Normal file
11
docs/startpage.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
layout: false
|
||||
title: Startpage
|
||||
pageClass: startpage-custom-styling
|
||||
---
|
||||
|
||||
<script setup>
|
||||
import StartPage from './.vitepress/theme/components/startpage/Startpage.vue'
|
||||
</script>
|
||||
|
||||
<StartPage />
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@fmhy/website",
|
||||
"packageManager": "pnpm@10.11.0",
|
||||
"packageManager": "pnpm@10.12.2+sha256.07b2396c6c99a93b75b5f9ce22be9285c3b2533c49fec51b349d44798cf56b82",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "21.7.3"
|
||||
|
@ -32,7 +32,7 @@
|
|||
"nitropack": "^2.11.6",
|
||||
"nprogress": "^0.2.0",
|
||||
"pathe": "^2.0.1",
|
||||
"reka-ui": "^2.1.1",
|
||||
"reka-ui": "^2.3.1",
|
||||
"unocss": "66.1.0-beta.3",
|
||||
"vitepress": "^1.6.3",
|
||||
"vue": "^3.5.13",
|
||||
|
@ -47,6 +47,7 @@
|
|||
"@iconify-json/fluent-mdl2": "^1.2.1",
|
||||
"@iconify-json/gravity-ui": "^1.2.5",
|
||||
"@iconify-json/heroicons-solid": "^1.2.0",
|
||||
"@iconify-json/logos": "^1.2.4",
|
||||
"@iconify-json/lucide": "^1.2.10",
|
||||
"@iconify-json/material-symbols": "^1.2.22",
|
||||
"@iconify-json/mdi": "^1.2.1",
|
||||
|
|
2
pests-repellent/.vscode/settings.json
vendored
2
pests-repellent/.vscode/settings.json
vendored
|
@ -2,4 +2,4 @@
|
|||
"files.associations": {
|
||||
"wrangler.json": "jsonc"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,4 +15,4 @@
|
|||
"vitest": "~3.2.0",
|
||||
"wrangler": "^4.20.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,9 +36,7 @@
|
|||
|
||||
/* Skip type checking all .d.ts files. */
|
||||
"skipLibCheck": true,
|
||||
"types": [
|
||||
"./worker-configuration.d.ts"
|
||||
]
|
||||
"types": ["./worker-configuration.d.ts"]
|
||||
},
|
||||
"exclude": ["test"],
|
||||
"include": ["worker-configuration.d.ts", "src/**/*.ts"]
|
||||
|
|
12815
pests-repellent/worker-configuration.d.ts
vendored
12815
pests-repellent/worker-configuration.d.ts
vendored
File diff suppressed because it is too large
Load diff
|
@ -10,9 +10,7 @@
|
|||
"observability": {
|
||||
"enabled": true,
|
||||
},
|
||||
"routes": [
|
||||
"fmhy.net/*"
|
||||
],
|
||||
"routes": ["fmhy.net/*"],
|
||||
/**
|
||||
* Smart Placement
|
||||
* Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
|
||||
|
|
20
pnpm-lock.yaml
generated
20
pnpm-lock.yaml
generated
|
@ -45,8 +45,8 @@ importers:
|
|||
specifier: ^2.0.1
|
||||
version: 2.0.1
|
||||
reka-ui:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2))
|
||||
specifier: ^2.3.1
|
||||
version: 2.3.1(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2))
|
||||
unocss:
|
||||
specifier: 66.1.0-beta.3
|
||||
version: 66.1.0-beta.3(vite@5.4.14(@types/node@20.16.12)(sass@1.85.1)(terser@5.39.0))(vue@3.5.13(typescript@5.8.2))
|
||||
|
@ -84,6 +84,9 @@ importers:
|
|||
'@iconify-json/heroicons-solid':
|
||||
specifier: ^1.2.0
|
||||
version: 1.2.0
|
||||
'@iconify-json/logos':
|
||||
specifier: ^1.2.4
|
||||
version: 1.2.4
|
||||
'@iconify-json/lucide':
|
||||
specifier: ^1.2.10
|
||||
version: 1.2.10
|
||||
|
@ -1048,6 +1051,9 @@ packages:
|
|||
'@iconify-json/heroicons-solid@1.2.0':
|
||||
resolution: {integrity: sha512-o+PjtMXPr4wk0veDS7Eh6H1BnTJT1vD7HcKl+I7ixdYQC8i1P2zdtk0C2v7C9OjJBMsiwJSCxT4qQ3OzONgyjw==}
|
||||
|
||||
'@iconify-json/logos@1.2.4':
|
||||
resolution: {integrity: sha512-XC4If5D/hbaZvUkTV8iaZuGlQCyG6CNOlaAaJaGa13V5QMYwYjgtKk3vPP8wz3wtTVNVEVk3LRx1fOJz+YnSMw==}
|
||||
|
||||
'@iconify-json/lucide@1.2.10':
|
||||
resolution: {integrity: sha512-cR1xpRJ4dnoXlC0ShDjzbrZyu+ICH4OUaYl7S51MhZUO1H040s7asVqv0LsDbofSLDuzWkHCLsBabTTRL0mCUg==}
|
||||
|
||||
|
@ -3415,8 +3421,8 @@ packages:
|
|||
regex@6.0.1:
|
||||
resolution: {integrity: sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==}
|
||||
|
||||
reka-ui@2.1.1:
|
||||
resolution: {integrity: sha512-awvpQ041LPXAvf2uRVFwedsyz9SwsuoWlRql1fg4XimUCxEI2GOfHo6FIdL44dSPb/eG/gWbdGhoGHLlbX5gPA==}
|
||||
reka-ui@2.3.1:
|
||||
resolution: {integrity: sha512-2SjGeybd7jvD8EQUkzjgg7GdOQdf4cTwdVMq/lDNTMqneUFNnryGO43dg8WaM/jaG9QpSCZBvstfBFWlDdb2Zg==}
|
||||
peerDependencies:
|
||||
vue: '>= 3.2.0'
|
||||
|
||||
|
@ -4843,6 +4849,10 @@ snapshots:
|
|||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
|
||||
'@iconify-json/logos@1.2.4':
|
||||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
|
||||
'@iconify-json/lucide@1.2.10':
|
||||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
|
@ -7281,7 +7291,7 @@ snapshots:
|
|||
dependencies:
|
||||
regex-utilities: 2.3.0
|
||||
|
||||
reka-ui@2.1.1(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2)):
|
||||
reka-ui@2.3.1(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2)):
|
||||
dependencies:
|
||||
'@floating-ui/dom': 1.6.13
|
||||
'@floating-ui/vue': 1.1.6(vue@3.5.13(typescript@5.8.2))
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
presetAttributify,
|
||||
presetIcons,
|
||||
presetUno,
|
||||
presetWebFonts,
|
||||
transformerDirectives
|
||||
} from 'unocss'
|
||||
|
||||
|
@ -94,16 +95,44 @@ export default defineConfig({
|
|||
// Color scale utilities
|
||||
...createColorRules('text'),
|
||||
...createColorRules('bg'),
|
||||
...createColorRules('border')
|
||||
...createColorRules('border'),
|
||||
|
||||
[
|
||||
'kbd',
|
||||
{
|
||||
display: 'inline-block',
|
||||
padding: '0.2em 0.4em',
|
||||
fontSize: '0.75em',
|
||||
fontWeight: '500',
|
||||
lineHeight: '1',
|
||||
color: 'var(--vp-c-text-1)',
|
||||
backgroundColor: 'rgb(var(--vp-c-bg-alt))',
|
||||
borderRadius: '4px'
|
||||
}
|
||||
]
|
||||
],
|
||||
presets: [
|
||||
presetUno(),
|
||||
presetAttributify(),
|
||||
presetIcons({
|
||||
autoInstall: true,
|
||||
scale: 1.2,
|
||||
extraProperties: {
|
||||
display: 'inline-block',
|
||||
'vertical-align': 'middle'
|
||||
},
|
||||
collections: {
|
||||
custom: {
|
||||
privateersclub: () =>
|
||||
fetch('https://megathread.pages.dev/favicon.svg').then((r) =>
|
||||
r.text()
|
||||
)
|
||||
}
|
||||
}
|
||||
}),
|
||||
presetWebFonts({
|
||||
fonts: {
|
||||
mono: 'Geist Mono'
|
||||
}
|
||||
})
|
||||
],
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue