Register system font faces for CSS preview, fix picker highlight width
This commit is contained in:
parent
621e38e178
commit
4e03d262c0
1
components.d.ts
vendored
1
components.d.ts
vendored
|
|
@ -20,6 +20,7 @@ declare module 'vue' {
|
|||
ExportSection: typeof import('./src/components/properties/ExportSection.vue')['default']
|
||||
FillPicker: typeof import('./src/components/FillPicker.vue')['default']
|
||||
FillSection: typeof import('./src/components/properties/FillSection.vue')['default']
|
||||
FontPicker: typeof import('./src/components/FontPicker.vue')['default']
|
||||
IconLucideALargeSmall: typeof import('~icons/lucide/a-large-small')['default']
|
||||
IconLucideAlignCenter: typeof import('~icons/lucide/align-center')['default']
|
||||
IconLucideAlignHorizontalJustifyCenter: typeof import('~icons/lucide/align-horizontal-justify-center')['default']
|
||||
|
|
|
|||
104
src/components/FontPicker.vue
Normal file
104
src/components/FontPicker.vue
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import {
|
||||
ListboxContent,
|
||||
ListboxFilter,
|
||||
ListboxItem,
|
||||
ListboxRoot,
|
||||
ListboxVirtualizer,
|
||||
PopoverAnchor,
|
||||
PopoverContent,
|
||||
PopoverRoot,
|
||||
useFilter
|
||||
} from 'reka-ui'
|
||||
|
||||
import { listFamilies } from '@/engine/fonts'
|
||||
|
||||
const modelValue = defineModel<string>({ required: true })
|
||||
const emit = defineEmits<{ select: [family: string] }>()
|
||||
|
||||
const families = ref<string[]>([])
|
||||
const searchTerm = ref('')
|
||||
const open = ref(false)
|
||||
const filterRef = ref<InstanceType<typeof ListboxFilter> | null>(null)
|
||||
|
||||
const { contains } = useFilter({ sensitivity: 'base' })
|
||||
const filtered = computed(() => {
|
||||
if (!searchTerm.value) return families.value
|
||||
return families.value.filter((f) => contains(f, searchTerm.value))
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
families.value = await listFamilies()
|
||||
})
|
||||
|
||||
watch(open, (isOpen) => {
|
||||
if (isOpen) {
|
||||
searchTerm.value = ''
|
||||
nextTick(() => filterRef.value?.$el?.focus())
|
||||
}
|
||||
})
|
||||
|
||||
function onSelect(val: string) {
|
||||
modelValue.value = val
|
||||
emit('select', val)
|
||||
open.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PopoverRoot v-model:open="open">
|
||||
<PopoverAnchor>
|
||||
<button
|
||||
class="flex w-full cursor-pointer items-center justify-between rounded border border-border bg-input px-2 py-1 text-xs text-surface hover:bg-hover"
|
||||
@click="open = !open"
|
||||
>
|
||||
<span class="truncate">{{ modelValue }}</span>
|
||||
<icon-lucide-chevron-down class="size-3 shrink-0 text-muted" />
|
||||
</button>
|
||||
</PopoverAnchor>
|
||||
<PopoverContent
|
||||
:side-offset="2"
|
||||
align="start"
|
||||
class="z-50 flex w-[var(--reka-popper-anchor-width)] flex-col overflow-hidden rounded-md border border-border bg-panel shadow-lg"
|
||||
@open-auto-focus.prevent
|
||||
>
|
||||
<ListboxRoot :model-value="modelValue" @update:model-value="onSelect">
|
||||
<div class="flex items-center gap-1 border-b border-border px-2 py-1">
|
||||
<icon-lucide-search class="size-3 shrink-0 text-muted" />
|
||||
<ListboxFilter
|
||||
ref="filterRef"
|
||||
v-model="searchTerm"
|
||||
class="min-w-0 flex-1 border-none bg-transparent text-xs text-surface outline-none placeholder:text-muted"
|
||||
placeholder="Search fonts…"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
</div>
|
||||
<ListboxContent class="h-72 overflow-y-auto">
|
||||
<ListboxVirtualizer
|
||||
v-slot="{ option }"
|
||||
:options="filtered"
|
||||
:text-content="(f: string) => f"
|
||||
:estimate-size="36"
|
||||
>
|
||||
<ListboxItem
|
||||
:value="option"
|
||||
class="flex w-full cursor-pointer items-center gap-2 px-2 py-2 text-sm text-surface outline-none data-[highlighted]:bg-hover"
|
||||
:style="{ fontFamily: `'${option}', sans-serif` }"
|
||||
>
|
||||
<icon-lucide-check v-if="option === modelValue" class="size-3 shrink-0 text-accent" />
|
||||
<span v-else class="size-3 shrink-0" />
|
||||
<span class="truncate">{{ option }}</span>
|
||||
</ListboxItem>
|
||||
</ListboxVirtualizer>
|
||||
<div v-if="filtered.length === 0" class="px-2 py-3 text-center text-xs text-muted">
|
||||
No fonts found
|
||||
</div>
|
||||
</ListboxContent>
|
||||
</ListboxRoot>
|
||||
</PopoverContent>
|
||||
</PopoverRoot>
|
||||
</template>
|
||||
|
|
@ -1,29 +1,13 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { onClickOutside } from '@vueuse/core'
|
||||
import { computed, onMounted } from 'vue'
|
||||
|
||||
import FontPicker from '@/components/FontPicker.vue'
|
||||
import ScrubInput from '@/components/ScrubInput.vue'
|
||||
import { useNodeProps } from '@/composables/use-node-props'
|
||||
import { listFamilies, loadFont } from '@/engine/fonts'
|
||||
import { loadFont } from '@/engine/fonts'
|
||||
|
||||
const { store, node, updateProp, commitProp } = useNodeProps()
|
||||
|
||||
const fontPickerOpen = ref(false)
|
||||
const fontSearch = ref('')
|
||||
const fontFamilies = ref<string[]>([])
|
||||
const fontPickerRef = ref<HTMLElement | null>(null)
|
||||
const searchInputRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
onClickOutside(fontPickerRef, () => {
|
||||
fontPickerOpen.value = false
|
||||
})
|
||||
|
||||
const filteredFamilies = computed(() => {
|
||||
const q = fontSearch.value.toLowerCase()
|
||||
if (!q) return fontFamilies.value
|
||||
return fontFamilies.value.filter((f) => f.toLowerCase().includes(q))
|
||||
})
|
||||
|
||||
const WEIGHTS = [
|
||||
{ value: 100, label: 'Thin' },
|
||||
{ value: 200, label: 'ExtraLight' },
|
||||
|
|
@ -42,24 +26,10 @@ const currentWeightLabel = computed(
|
|||
|
||||
type TextAlign = 'LEFT' | 'CENTER' | 'RIGHT'
|
||||
|
||||
async function openFontPicker() {
|
||||
fontPickerOpen.value = true
|
||||
fontSearch.value = ''
|
||||
if (fontFamilies.value.length === 0) {
|
||||
fontFamilies.value = await listFamilies()
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
searchInputRef.value?.focus()
|
||||
const active = document.querySelector('[data-active-font]')
|
||||
active?.scrollIntoView({ block: 'center' })
|
||||
})
|
||||
}
|
||||
|
||||
async function selectFamily(family: string) {
|
||||
await loadFont(family, currentWeightLabel.value)
|
||||
store.updateNodeWithUndo(node.value.id, { fontFamily: family }, 'Change font')
|
||||
store.requestRender()
|
||||
fontPickerOpen.value = false
|
||||
}
|
||||
|
||||
async function selectWeight(weight: number) {
|
||||
|
|
@ -69,7 +39,7 @@ async function selectWeight(weight: number) {
|
|||
store.requestRender()
|
||||
}
|
||||
|
||||
function setAlign(align: 'LEFT' | 'CENTER' | 'RIGHT') {
|
||||
function setAlign(align: TextAlign) {
|
||||
store.updateNodeWithUndo(node.value.id, { textAlignHorizontal: align }, 'Change text alignment')
|
||||
store.requestRender()
|
||||
}
|
||||
|
|
@ -83,54 +53,8 @@ onMounted(async () => {
|
|||
<div v-if="node" class="border-b border-border px-3 py-2">
|
||||
<label class="mb-1.5 block text-[11px] text-muted">Typography</label>
|
||||
|
||||
<!-- Font family picker -->
|
||||
<div ref="fontPickerRef" class="relative mb-1.5">
|
||||
<button
|
||||
class="flex w-full cursor-pointer items-center justify-between rounded border border-border bg-input px-2 py-1 text-xs text-surface hover:bg-hover"
|
||||
@click="openFontPicker"
|
||||
>
|
||||
<span class="truncate">{{ node.fontFamily }}</span>
|
||||
<icon-lucide-chevron-down class="size-3 shrink-0 text-muted" />
|
||||
</button>
|
||||
|
||||
<!-- Font picker dropdown -->
|
||||
<div
|
||||
v-if="fontPickerOpen"
|
||||
class="absolute top-full right-0 left-0 z-20 mt-0.5 flex max-h-64 flex-col rounded-md border border-border bg-panel shadow-lg"
|
||||
>
|
||||
<div class="flex items-center gap-1 border-b border-border px-2 py-1">
|
||||
<icon-lucide-search class="size-3 shrink-0 text-muted" />
|
||||
<input
|
||||
ref="searchInputRef"
|
||||
v-model="fontSearch"
|
||||
class="min-w-0 flex-1 border-none bg-transparent text-xs text-surface outline-none placeholder:text-muted"
|
||||
placeholder="Search fonts…"
|
||||
@keydown.escape="fontPickerOpen = false"
|
||||
@keydown.enter="filteredFamilies.length && selectFamily(filteredFamilies[0])"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<button
|
||||
v-for="family in filteredFamilies"
|
||||
:key="family"
|
||||
class="flex w-full cursor-pointer items-center gap-2 border-none bg-transparent px-2 py-1.5 text-left text-xs hover:bg-hover"
|
||||
:class="family === node.fontFamily ? 'text-accent' : 'text-surface'"
|
||||
:data-active-font="family === node.fontFamily ? '' : undefined"
|
||||
:style="{ fontFamily: family }"
|
||||
@click="selectFamily(family)"
|
||||
>
|
||||
<icon-lucide-check v-if="family === node.fontFamily" class="size-3 shrink-0" />
|
||||
<span v-else class="size-3 shrink-0" />
|
||||
<span class="truncate">{{ family }}</span>
|
||||
</button>
|
||||
<div
|
||||
v-if="filteredFamilies.length === 0"
|
||||
class="px-2 py-3 text-center text-xs text-muted"
|
||||
>
|
||||
No fonts found
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-1.5">
|
||||
<FontPicker :model-value="node.fontFamily" @select="selectFamily" />
|
||||
</div>
|
||||
|
||||
<!-- Weight + Size -->
|
||||
|
|
|
|||
|
|
@ -30,7 +30,17 @@ async function getTauriFonts(): Promise<TauriFontFamily[]> {
|
|||
}
|
||||
|
||||
export function preloadFonts(): void {
|
||||
if (isTauri()) getTauriFonts()
|
||||
if (isTauri()) {
|
||||
getTauriFonts().then(registerFontFaces)
|
||||
}
|
||||
}
|
||||
|
||||
function registerFontFaces(fonts: TauriFontFamily[]): void {
|
||||
if (typeof document === 'undefined') return
|
||||
for (const { family } of fonts) {
|
||||
const face = new FontFace(family, `local("${family}")`)
|
||||
document.fonts.add(face)
|
||||
}
|
||||
}
|
||||
|
||||
export async function listFamilies(): Promise<string[]> {
|
||||
|
|
|
|||
Loading…
Reference in a new issue