Register system font faces for CSS preview, fix picker highlight width

This commit is contained in:
Danila Poyarkov 2026-03-01 03:25:50 +03:00
parent 621e38e178
commit 4e03d262c0
4 changed files with 122 additions and 83 deletions

1
components.d.ts vendored
View file

@ -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']

View 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>

View file

@ -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 -->

View file

@ -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[]> {