├── src ├── vite-env.d.ts ├── index.ts ├── types │ ├── util.ts │ ├── fonts.ts │ ├── fontpicker.ts │ └── translations.ts ├── data │ ├── fonts.ts │ ├── fonts │ │ └── _systemFonts.txt │ └── translations.ts ├── helpers │ ├── FontFamily.ts │ ├── Font.ts │ └── FontLoader.ts ├── util │ ├── FPB.ts │ ├── sortUtil.ts │ └── DOMUtil.ts ├── css │ ├── fontpicker.css │ └── fpb.css ├── templates │ └── dialogContent.html └── core │ ├── FontPicker.ts │ └── PickerDialog.ts ├── .prettierrc ├── screenshots ├── button-dark.png ├── dialog-dark.png ├── button-light.png └── dialog-light.png ├── dev ├── demo.css ├── index.html └── demo.ts ├── .gitignore ├── package.json ├── plugins └── viteHTML.ts ├── tsconfig.json ├── vite.config.ts ├── LICENSE ├── dist ├── fontpicker.d.ts ├── fontpicker.min.css └── fontpicker.css ├── README.md ├── DOCUMENTATION.md └── bun.lock /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false, 4 | "printWidth": 100 5 | } 6 | -------------------------------------------------------------------------------- /screenshots/button-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wipeautcrafter/jsfontpicker/HEAD/screenshots/button-dark.png -------------------------------------------------------------------------------- /screenshots/dialog-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wipeautcrafter/jsfontpicker/HEAD/screenshots/dialog-dark.png -------------------------------------------------------------------------------- /screenshots/button-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wipeautcrafter/jsfontpicker/HEAD/screenshots/button-light.png -------------------------------------------------------------------------------- /screenshots/dialog-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wipeautcrafter/jsfontpicker/HEAD/screenshots/dialog-light.png -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import './css/fpb.css' 2 | import './css/fontpicker.css' 3 | import { FontPicker } from './core/FontPicker' 4 | 5 | export default FontPicker 6 | -------------------------------------------------------------------------------- /dev/demo.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | color: var(--fp-body-color); 4 | background-color: var(--fp-body-bg); 5 | } 6 | 7 | .form-label { 8 | display: block; 9 | margin-bottom: 0.5rem; 10 | font-weight: 600; 11 | } 12 | -------------------------------------------------------------------------------- /src/types/util.ts: -------------------------------------------------------------------------------- 1 | import type { Category, Metric, Subset } from '../types/translations' 2 | 3 | export type Filters = { 4 | name: string 5 | subset: Subset 6 | categories: Category[] 7 | width: Metric 8 | thickness: Metric 9 | complexity: Metric 10 | curvature: Metric 11 | } 12 | -------------------------------------------------------------------------------- /src/data/fonts.ts: -------------------------------------------------------------------------------- 1 | import { FontFamily } from '../helpers/FontFamily' 2 | import _googleFonts from './fonts/_googleFonts.txt?raw' 3 | import _systemFonts from './fonts/_systemFonts.txt?raw' 4 | 5 | export const googleFonts = _googleFonts.split('|').map(FontFamily.parse) 6 | export const systemFonts = _systemFonts.split('|').map(FontFamily.parse) 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | # dist 12 | # dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | testkees/ -------------------------------------------------------------------------------- /src/types/fonts.ts: -------------------------------------------------------------------------------- 1 | import type { Category, Subset } from './translations' 2 | 3 | export interface FamilyProps { 4 | name: string 5 | variants: string[] 6 | category?: Category 7 | subsets?: Subset[] 8 | popularity?: number 9 | metrics?: { 10 | width: number 11 | thickness: number 12 | complexity: number 13 | curvature: number 14 | } 15 | url?: string 16 | } 17 | 18 | export type FontWeight = 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fontpicker", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite --host", 8 | "tsc": "tsc --pretty", 9 | "build": "vite build", 10 | "preview": "vite preview", 11 | "format": "prettier -w src" 12 | }, 13 | "devDependencies": { 14 | "@types/bun": "latest", 15 | "esbuild": "^0.25.5", 16 | "events": "^3.3.0", 17 | "leven": "^4.0.0", 18 | "prettier": "^3.3.3", 19 | "typescript": "^5.5.3", 20 | "vite": "^6.3.5", 21 | "vite-plugin-dts": "^4.2.3" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /plugins/viteHTML.ts: -------------------------------------------------------------------------------- 1 | export default function viteHTML() { 2 | return { 3 | name: 'minify-html', 4 | transform(code: string, id: string) { 5 | if (/\.html.raw?$/.test(id)) { 6 | return { 7 | code: code 8 | .replace(//gs, '') // comments 9 | .replace(/\\n/g, '') // newlines 10 | .replace(/\s\s+/g, ' ') // multiple spaces 11 | .replace(/\s*<\s*/g, '<') // space between elements 12 | .replace(/\s*>\s*/g, '>') // space between elements 13 | .trim(), 14 | map: null, 15 | } 16 | } 17 | }, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "strictPropertyInitialization": false, 22 | "verbatimModuleSyntax": true 23 | }, 24 | "include": ["src"] 25 | } 26 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { defineConfig } from 'vite' 3 | 4 | import dts from 'vite-plugin-dts' 5 | import viteHTML from './plugins/viteHTML' 6 | 7 | export default defineConfig({ 8 | server: { 9 | open: '/dev/', 10 | allowedHosts: ['cure'] 11 | }, 12 | build: { 13 | lib: { 14 | entry: resolve(import.meta.dirname, 'src/index.ts'), 15 | name: 'FontPicker', 16 | fileName: 'fontpicker', 17 | formats: ['iife', 'es'], 18 | }, 19 | rollupOptions: { 20 | external: ['bootstrap'], 21 | output: { 22 | globals: { 23 | bootstrap: 'bootstrap', 24 | }, 25 | assetFileNames: 'fontpicker.[ext]', 26 | }, 27 | }, 28 | minify: false, 29 | cssMinify: false, 30 | }, 31 | plugins: [dts({ rollupTypes: true }), viteHTML()], 32 | }) 33 | -------------------------------------------------------------------------------- /src/types/fontpicker.ts: -------------------------------------------------------------------------------- 1 | import type { Language, Category, Criterion, Metric, Subset } from '../types/translations' 2 | import type { FamilyProps } from '../types/fonts' 3 | 4 | export interface PickerConfig { 5 | language: Language 6 | container: HTMLElement 7 | previewText: string | null 8 | 9 | font: string | null 10 | verbose: boolean 11 | variants: boolean 12 | 13 | favourites: string[] 14 | saveFavourites: boolean 15 | storageKey: string 16 | stateKey: string 17 | 18 | defaultSearch: string 19 | defaultSubset: Subset 20 | defaultCategories: Category[] 21 | defaultWidth: Metric 22 | defaultThickness: Metric 23 | defaultComplexity: Metric 24 | defaultCurvature: Metric 25 | 26 | sortBy: Criterion 27 | sortReverse: boolean 28 | 29 | googleFonts: string[] | null 30 | systemFonts: string[] | null 31 | 32 | extraFonts: FamilyProps[] 33 | 34 | showCancelButton: boolean 35 | showClearButton: boolean 36 | } 37 | -------------------------------------------------------------------------------- /src/data/fonts/_systemFonts.txt: -------------------------------------------------------------------------------- 1 | Arial/sans-serif/400,400i,700,700i/cyrillic,cyrillic-ext,greek,greek-ext,hebrew,latin,latin-ext,vietnamese|Comic Sans MS/sans-serif/400,400i,700,700i/cyrillic,cyrillic-ext,greek,greek-ext,latin,latin-ext|Courier New/sans-serif/400,400i,700,700i/cyrillic,cyrillic-ext,greek,greek-ext,hebrew,latin,latin-ext,vietnamese|Georgia/serif/400,400i,700,700i/cyrillic,cyrillic-ext,greek,greek-ext,hebrew,latin,latin-ext|Helvetica/sans-serif/400,400i,700,700i/cyrillic,cyrillic-ext,greek,greek-ext,hebrew,latin,latin-ext,vietnamese|Impact/sans-serif/400/cyrillic,cyrillic-ext,greek,greek-ext,hebrew,latin,latin-ext|Tahoma/sans-serif/400,700/cyrillic,cyrillic-ext,greek,greek-ext,hebrew,latin,latin-ext,vietnamese|Times New Roman/serif/400,400i,700,700i/cyrillic,cyrillic-ext,greek,greek-ext,hebrew,latin,latin-ext,vietnamese|Trebuchet MS/sans-serif/400,400i,700,700i/cyrillic,cyrillic-ext,greek,greek-ext,latin,latin-ext|Verdana/sans-serif/400,400i,700,700i/cyrillic,cyrillic-ext,greek,greek-ext,latin,latin-ext,vietnamese 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024-2025 Zygomatic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/helpers/FontFamily.ts: -------------------------------------------------------------------------------- 1 | import type { FamilyProps } from '../types/fonts' 2 | import type { Category, Subset } from '../types/translations' 3 | 4 | export interface FontFamily extends FamilyProps {} 5 | 6 | export class FontFamily { 7 | constructor(family: FamilyProps) { 8 | Object.assign(this, family) 9 | } 10 | 11 | toString() { 12 | return this.name 13 | } 14 | 15 | getDefaultVariant() { 16 | const weights = Array.from(new Set(this.variants)) 17 | return weights.toSorted((a, b) => { 18 | return Math.abs(parseInt(a) - 400) - Math.abs(parseInt(b) - 400) 19 | })[0] 20 | } 21 | 22 | // parse font family from compressed format 23 | static parse(raw: string) { 24 | const [name, cate, vari, subs, popu, thic, widt, comp, curv] = raw.split('/') 25 | 26 | const family = new FontFamily({ 27 | name, 28 | category: cate as Category, 29 | variants: vari.split(','), 30 | subsets: subs.split(',') as Subset[], 31 | }) 32 | 33 | if (popu) family.popularity = parseInt(popu) 34 | if (thic && widt && comp && curv) 35 | family.metrics = { 36 | thickness: parseFloat(thic), 37 | width: parseFloat(widt), 38 | complexity: parseFloat(comp), 39 | curvature: parseFloat(curv), 40 | } 41 | 42 | return family 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/types/translations.ts: -------------------------------------------------------------------------------- 1 | export type Language = 'en' | 'nl' | 'de' | 'es' | 'fr' 2 | 3 | export type Subset = 4 | | 'all' 5 | | 'arabic' 6 | | 'bengali' 7 | | 'chinese-hongkong' 8 | | 'chinese-simplified' 9 | | 'chinese-traditional' 10 | | 'cyrillic' 11 | | 'cyrillic-ext' 12 | | 'devanagari' 13 | | 'greek' 14 | | 'greek-ext' 15 | | 'gujarati' 16 | | 'gurmukhi' 17 | | 'hebrew' 18 | | 'japanese' 19 | | 'kannada' 20 | | 'khmer' 21 | | 'korean' 22 | | 'latin' 23 | | 'latin-ext' 24 | | 'malayalam' 25 | | 'myanmar' 26 | | 'oriya' 27 | | 'sinhala' 28 | | 'tamil' 29 | | 'telugu' 30 | | 'thai' 31 | | 'tibetan' 32 | | 'vietnamese' 33 | 34 | export type Category = 'serif' | 'sans-serif' | 'display' | 'handwriting' | 'monospace' 35 | export type Metric = 'all' | '0!' | '1!' | '2!' | '3!' | '4!' 36 | export type Criterion = 'name' | 'popularity' | 'width' | 'thickness' | 'complexity' | 'curvature' 37 | 38 | type Subsets = { [subset in Subset]: string } 39 | type Categories = { [category in Category]: string } 40 | type Metrics = { [metric in Metric]: string } 41 | type Criteria = { [criterion in Criterion]: string } 42 | 43 | export interface Translation { 44 | selectFont: string 45 | sampleText: string 46 | pickHint: string 47 | 48 | filters: string 49 | search: string 50 | 51 | subsets: Subsets 52 | categories: Categories 53 | 54 | metrics: string 55 | widths: Metrics 56 | thicknesses: Metrics 57 | complexities: Metrics 58 | curvatures: Metrics 59 | 60 | sort: string 61 | sorts: Criteria 62 | 63 | clearFilters: string 64 | 65 | clear: string 66 | cancel: string 67 | select: string 68 | } 69 | 70 | export type Translations = { [lang in Language]: Translation } 71 | -------------------------------------------------------------------------------- /src/helpers/Font.ts: -------------------------------------------------------------------------------- 1 | import type { FontFamily } from './FontFamily' 2 | import type { FontWeight } from '../types/fonts' 3 | 4 | export class Font { 5 | static weightNames: { [weight in FontWeight]: string } = { 6 | 100: 'Thin', 7 | 200: 'Extra Light', 8 | 300: 'Light', 9 | 400: 'Normal', 10 | 500: 'Medium', 11 | 600: 'Semi Bold', 12 | 700: 'Bold', 13 | 800: 'Extra Bold', 14 | 900: 'Black', 15 | } 16 | 17 | readonly family: FontFamily 18 | readonly weight: FontWeight 19 | readonly italic: boolean 20 | 21 | constructor(family: FontFamily, weight: FontWeight, italic: boolean) { 22 | this.family = family 23 | this.weight = weight 24 | this.italic = italic 25 | } 26 | 27 | get style() { 28 | return this.italic ? 'italic' : 'normal' 29 | } 30 | 31 | get variant() { 32 | return this.weight + (this.italic ? 'i' : '') 33 | } 34 | 35 | toId() { 36 | return `${this.family}:${this.variant}` 37 | } 38 | 39 | toConcise() { 40 | if (this.family.getDefaultVariant() === this.variant) return this.family.name 41 | return this.toId() 42 | } 43 | 44 | toString() { 45 | // if this is the default variant, return that 46 | if (this.family.getDefaultVariant() === this.variant) return this.family.name 47 | 48 | const entries = [this.family.name] 49 | 50 | entries.push(Font.weightNames[this.weight]) 51 | if (this.italic) entries.push('Italic') 52 | entries.push(`(${this.variant})`) 53 | 54 | return entries.join(' ') 55 | } 56 | 57 | static parse(family: FontFamily, variant = family.getDefaultVariant()) { 58 | const weight = parseInt(variant) as FontWeight 59 | const italic = variant.endsWith('i') 60 | return new Font(family, weight, italic) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /dev/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | FontPicker Demo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

JSFontPicker Demo

16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |

The Quick Brown Fox Jumps Over The Lazy Dog

26 |
27 | 28 |
29 | 30 |
31 | 32 | 38 |
39 | 40 | 41 | 42 |
43 | 44 |
45 | 46 | 47 | -------------------------------------------------------------------------------- /src/helpers/FontLoader.ts: -------------------------------------------------------------------------------- 1 | import { googleFonts, systemFonts } from '../data/fonts' 2 | import { FontFamily } from './FontFamily' 3 | export class FontLoader { 4 | static #cache = new Map>() 5 | 6 | static loaded(name: string) { 7 | return this.#cache.has(name) 8 | } 9 | 10 | static async #appendStylesheet(url: string) { 11 | const $link = document.createElement('link') 12 | $link.href = url 13 | $link.rel = 'stylesheet' 14 | $link.type = 'text/css' 15 | document.head.append($link) 16 | } 17 | 18 | static async #loadGoogleFont(font: FontFamily) { 19 | const url = new URL('https://fonts.googleapis.com/css') 20 | const name = font.name + ':' + font.variants.join(',') 21 | url.searchParams.set('family', name) 22 | url.searchParams.set('display', 'swap') 23 | this.#appendStylesheet(url.toString()) 24 | await document.fonts.load(`1em "${font.name}"`) 25 | } 26 | 27 | static async #loadExtraFont(font: FontFamily) { 28 | const fontFace = new FontFace(font.name, `url(${font.url})`) 29 | 30 | document.fonts.add(await fontFace.load()) 31 | await document.fonts.load(`1em "${font.name}"`) 32 | } 33 | 34 | static async load(font: string | FontFamily) { 35 | const family = font instanceof FontFamily ? font : null 36 | const name = font instanceof FontFamily ? font.name : font 37 | 38 | let promise = this.#cache.get(name) 39 | 40 | if (!promise) { 41 | const systemFont = systemFonts.find((sf) => sf.name === name) 42 | const googleFont = googleFonts.find((gf) => gf.name === name) 43 | 44 | if (family && family.url) { 45 | promise = this.#loadExtraFont(family) 46 | } else if (systemFont) { 47 | promise = Promise.resolve() 48 | } else if (googleFont) { 49 | promise = this.#loadGoogleFont(googleFont) 50 | } else { 51 | console.error(`Could not load font ${name}!`) 52 | promise = Promise.resolve() 53 | } 54 | 55 | this.#cache.set(name, promise) 56 | } 57 | 58 | await promise 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/util/FPB.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events' 2 | 3 | export class Modal extends EventEmitter<{ 4 | opening: [] 5 | opened: [] 6 | closing: [] 7 | closed: [] 8 | }> { 9 | $el: HTMLElement 10 | 11 | constructor($el: HTMLElement) { 12 | super() 13 | this.$el = $el 14 | } 15 | 16 | get isOpen() { 17 | return this.$el.classList.contains('fpb__open') 18 | } 19 | 20 | toggle(force?: boolean) { 21 | const open = this.$el.classList.toggle('fpb__open', force) 22 | this.emit(open ? 'opening' : 'closing') 23 | setTimeout(() => this.emit(open ? 'opened' : 'closed'), 500) 24 | } 25 | 26 | open() { 27 | if (!this.isOpen) this.toggle(true) 28 | } 29 | 30 | close() { 31 | if (this.isOpen) this.toggle(false) 32 | } 33 | } 34 | 35 | export class Accordion { 36 | $el: HTMLElement 37 | 38 | constructor($el: HTMLElement) { 39 | this.$el = $el 40 | 41 | this.$el.addEventListener('click', (event) => { 42 | const $target = event.target as HTMLElement 43 | 44 | const $accordionToggle = $target.closest('.fpb__accordion-toggle') 45 | if ($accordionToggle) this.toggleItem($accordionToggle.parentElement!) 46 | }) 47 | } 48 | 49 | private getItems() { 50 | return this.$el.querySelectorAll('.fpb__accordion-item') 51 | } 52 | 53 | private _toggle($item: HTMLElement, force?: boolean) { 54 | const $content = $item.querySelector('.fpb__accordion-content')! 55 | const height = $content.children[0].clientHeight + 'px' 56 | 57 | $content.style.setProperty('--fpb-height', height) 58 | setTimeout(() => { 59 | const open = $item.classList.toggle('fpb__open', force) 60 | setTimeout(() => $content.style.removeProperty('--fpb-height'), open ? 500 : 0) 61 | }, 1) 62 | 63 | return open 64 | } 65 | 66 | private toggleItem($item: HTMLElement) { 67 | const open = this._toggle($item) 68 | if (!open) return 69 | this.getItems().forEach(($otherItem) => { 70 | if ($otherItem !== $item) this._toggle($otherItem, false) 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/util/sortUtil.ts: -------------------------------------------------------------------------------- 1 | import leven from 'leven' 2 | 3 | import type { FontFamily } from '../helpers/FontFamily' 4 | import type { Criterion, Metric } from '../types/translations' 5 | import type { Filters } from '../types/util' 6 | 7 | export const familySort = (a: FontFamily, b: FontFamily, key: Criterion) => { 8 | // direct properties 9 | if (key === 'name') return a.name.localeCompare(b.name) 10 | if (key === 'popularity') { 11 | if (a.popularity === undefined && b.popularity === undefined) return 0 12 | if (a.popularity === undefined) return Infinity 13 | if (b.popularity === undefined) return -Infinity 14 | return a.popularity - b.popularity 15 | } 16 | 17 | // metrics properties 18 | if (a.metrics === undefined && b.metrics === undefined) return 0 19 | if (a.metrics === undefined) return Infinity 20 | if (b.metrics === undefined) return -Infinity 21 | 22 | if (key === 'complexity') return b.metrics.complexity - a.metrics.complexity 23 | if (key === 'curvature') return b.metrics.curvature - a.metrics.curvature 24 | if (key === 'thickness') return b.metrics.thickness - a.metrics.thickness 25 | if (key === 'width') return b.metrics.width - a.metrics.width 26 | 27 | // fallback 28 | return 0 29 | } 30 | 31 | const compareMetric = (value: number | undefined, target: Metric) => { 32 | if (target === 'all') return true 33 | if (value === undefined) return false 34 | return value === parseFloat(target) 35 | } 36 | 37 | export const familyFilter = (a: FontFamily, filters: Filters) => { 38 | // direct properties 39 | if (filters.name) { 40 | const difference = leven(a.name.toLowerCase(), filters.name.toLowerCase()) 41 | const threshold = [...a.name].length - [...filters.name].length 42 | if (difference > threshold) return false 43 | } 44 | 45 | if (a.subsets && filters.subset !== 'all' && !a.subsets.includes(filters.subset)) return false 46 | if (a.category && !filters.categories.includes(a.category)) return false 47 | 48 | // metrics properties 49 | if (!compareMetric(a.metrics?.width, filters.width)) return false 50 | if (!compareMetric(a.metrics?.complexity, filters.complexity)) return false 51 | if (!compareMetric(a.metrics?.curvature, filters.curvature)) return false 52 | if (!compareMetric(a.metrics?.thickness, filters.thickness)) return false 53 | 54 | return true 55 | } 56 | -------------------------------------------------------------------------------- /src/css/fontpicker.css: -------------------------------------------------------------------------------- 1 | /* Font picker button element */ 2 | .font-picker { 3 | text-align: start; 4 | overflow: hidden; 5 | white-space: nowrap; 6 | text-overflow: ellipsis; 7 | vertical-align: middle; 8 | } 9 | input.font-picker { 10 | caret-color: transparent; 11 | } 12 | 13 | /* Filters */ 14 | .fp__changed::after { 15 | content: '*'; 16 | color: var(--fp-secondary); 17 | } 18 | 19 | /* Font list */ 20 | #fp__fonts { 21 | display: flex; 22 | flex-direction: column; 23 | flex-grow: 1; 24 | overflow-y: scroll; 25 | 26 | padding: 0.25rem; 27 | margin-top: -1px; 28 | 29 | border-block: 1px solid var(--fp-border-color); 30 | } 31 | #fp__fonts:focus-visible { 32 | outline: 0; 33 | box-shadow: var(--fp-ring-shadow); 34 | border-color: var(--fp-ring-color); 35 | transition: 36 | border-color 0.15s, 37 | box-shadow 0.15s; 38 | } 39 | 40 | /* Font item */ 41 | .fp__font-item { 42 | display: flex; 43 | justify-content: space-between; 44 | align-items: center; 45 | 46 | padding: 0.25rem 1rem; 47 | min-height: 2rem; 48 | user-select: none; 49 | border-radius: 9999px; 50 | } 51 | .fp__font-item:hover { 52 | background: var(--fp-border-color); 53 | } 54 | .fp__font-item.fp__selected { 55 | color: var(--fp-light); 56 | background: var(--fp-primary); 57 | } 58 | 59 | .fp__font-family { 60 | font-size: 1rem; 61 | pointer-events: none; 62 | } 63 | 64 | /* Font heart */ 65 | .fp__heart { 66 | height: 1em; 67 | } 68 | .fp__heart svg { 69 | height: 1em; 70 | pointer-events: none; 71 | vertical-align: baseline; 72 | 73 | --fp-heart-color: var(--fp-border-color-rgb); 74 | 75 | fill: rgba(var(--fp-heart-color), 0.5); 76 | stroke: rgb(var(--fp-heart-color)); 77 | } 78 | .fp__heart:hover svg { 79 | fill: rgb(var(--fp-heart-color)); 80 | } 81 | 82 | .fp__font-item:hover .fp__heart svg, 83 | .fp__font-item.fp__selected .fp__heart svg { 84 | --fp-heart-color: var(--fp-body-bg-rgb); 85 | } 86 | .fp__font-item.fp__fav .fp__heart svg { 87 | --fp-heart-color: var(--fp-danger-rgb); 88 | } 89 | .fp__font-item.fp__fav.fp__selected .fp__heart svg { 90 | filter: drop-shadow(0px 0px 2px var(--fp-dark)); 91 | } 92 | 93 | /* Preview */ 94 | .fp__preview-container { 95 | padding: 0.25rem; 96 | } 97 | #fp__preview { 98 | white-space: nowrap; 99 | text-overflow: ellipsis; 100 | overflow: hidden; 101 | text-align: center; 102 | font-size: 1.25rem; 103 | line-height: 1.5; 104 | 105 | padding-inline: 0.75rem; 106 | border: 1px solid transparent; 107 | 108 | transition: 109 | border-color 0.15s ease-in-out, 110 | box-shadow 0.15s ease-in-out; 111 | } 112 | #fp__preview:focus { 113 | outline: 0; 114 | border-color: var(--fp-ring-color); 115 | box-shadow: var(--fp-ring-shadow); 116 | } 117 | 118 | /* Variants */ 119 | #fp__variants { 120 | display: flex; 121 | flex-wrap: wrap; 122 | justify-content: center; 123 | gap: 0.5rem; 124 | padding: 0.5rem 1rem; 125 | border-top: 1px solid var(--fp-border-color); 126 | } 127 | #fp__variants:has(#fp__italic:checked) { 128 | font-style: italic !important; 129 | } 130 | #fp__variants:empty { 131 | display: none; 132 | } 133 | -------------------------------------------------------------------------------- /dev/demo.ts: -------------------------------------------------------------------------------- 1 | import FontPicker from '../src/index' 2 | 3 | document.addEventListener('DOMContentLoaded', () => { 4 | const canvas = document.querySelector('#sampleCanvas')! 5 | const ctx = canvas.getContext('2d')! 6 | 7 | document.querySelector('#getFont1Btn')!.onclick = () => { 8 | console.log(picker4button.font) 9 | } 10 | 11 | document.querySelector('#getFont2Btn')!.onclick = () => { 12 | console.log(picker4input.font) 13 | } 14 | 15 | document.querySelector('#setFont1Btn')!.onclick = () => { 16 | picker4button.setFont('Quicksand', true) 17 | } 18 | 19 | document.querySelector('#clearFont1Btn')!.onclick = () => { 20 | picker4button.clear() 21 | } 22 | 23 | document.querySelector('#setFont2Btn')!.onclick = () => { 24 | const picker = document.querySelector('#pickerInput')! 25 | picker.value = 'Quicksand' 26 | picker.dispatchEvent(new Event('change')) 27 | } 28 | 29 | document.querySelector('#destroy1Btn')!.onclick = () => { 30 | picker4button.destroy() 31 | } 32 | 33 | document.querySelector('#destroy2Btn')!.onclick = () => { 34 | picker4input.destroy() 35 | } 36 | 37 | const picker4button = new FontPicker('#pickerButton', { 38 | variants: true, 39 | verbose: true, 40 | googleFonts: ['Pacifico', 'Open Sans', 'Poppins'], 41 | }) 42 | .on('pick', (font) => { 43 | console.log(font) 44 | const $sample = document.querySelector('#sampleText')! 45 | if (!font) return 46 | 47 | $sample.style.fontFamily = font.family.name 48 | $sample.style.fontWeight = font.weight.toString() 49 | $sample.style.fontStyle = font.italic ? 'italic' : 'normal' 50 | }) 51 | .on('open', () => { 52 | console.log('open') 53 | }) 54 | .on('opened', () => { 55 | console.log('opened') 56 | }) 57 | .on('close', () => { 58 | console.log('close') 59 | }) 60 | .on('closed', () => { 61 | console.log('closed') 62 | }) 63 | .on('clear', () => { 64 | console.log('clear') 65 | }) 66 | 67 | const picker4input = new FontPicker('#pickerInput', { 68 | //font: 'Open Sans', 69 | //defaultSubset: 'latin', 70 | //defaultCategories: ['sans-serif', 'display', 'handwriting'], 71 | language: 'nl', 72 | verbose: true, 73 | variants: true, 74 | favourites: ['Open Sans'], 75 | }) 76 | .on('open', () => console.log('Picker open')) 77 | .on('pick', async (font) => { 78 | if (!font) { 79 | console.log('font clear') 80 | return 81 | } 82 | 83 | console.log('Picker pick', font, font.toId(), font.toString()) 84 | const fontName = font.toString() 85 | 86 | ctx.clearRect(0, 0, canvas.width, canvas.height) 87 | ctx.textBaseline = 'top' 88 | ctx.fillStyle = '#000' 89 | ctx.font = `2em ${fontName}` 90 | ctx.fillText(fontName + ' normal', 10, 0) 91 | 92 | await document.fonts.load(`700 1em "${fontName}"`) 93 | ctx.font = `700 2em ${fontName}` 94 | ctx.fillText(fontName + ' bold', 10, 50) 95 | 96 | await document.fonts.load(`900 italic 1em "${fontName}"`) 97 | ctx.font = `900 italic 2em ${fontName}` 98 | ctx.fillText(fontName + ' extrabold italic ', 10, 100) 99 | }) 100 | .on('cancel', () => console.log('Picker cancel')) 101 | .on('close', () => console.log('Picker close')) 102 | }) 103 | -------------------------------------------------------------------------------- /src/util/DOMUtil.ts: -------------------------------------------------------------------------------- 1 | import type { FontFamily } from '../helpers/FontFamily' 2 | 3 | // Font list items 4 | const heartSVG = `
` 5 | 6 | export const createLazyFont = (font: FontFamily) => { 7 | const $item = document.createElement('div') 8 | $item.className = 'fp__font-item' 9 | $item.role = 'button' 10 | 11 | $item.dataset.family = font.name 12 | 13 | return $item 14 | } 15 | 16 | export const hydrateFont = ($item: HTMLElement, font: FontFamily) => { 17 | const $family = document.createElement('span') 18 | $family.className = 'fp__font-family' 19 | $family.textContent = font.name 20 | $family.style.fontFamily = `"${font.name}"` 21 | 22 | $item.append($family) 23 | $item.insertAdjacentHTML('beforeend', heartSVG) 24 | } 25 | 26 | // Toggle buttons 27 | const createRadioToggle = ({ 28 | id, 29 | name, 30 | value, 31 | label, 32 | classes, 33 | checked, 34 | }: { 35 | id: string 36 | name: string 37 | value: string 38 | label: string 39 | classes?: string[] 40 | checked?: boolean 41 | }) => { 42 | const $input = document.createElement('input') 43 | $input.className = 'fpb__hidden-input' 44 | $input.type = 'radio' 45 | $input.name = name 46 | $input.id = id 47 | $input.value = value 48 | $input.autocomplete = 'off' 49 | if (checked) $input.checked = true 50 | 51 | const $label = document.createElement('label') 52 | $label.className = 'fpb__btn fpb__btn-pill fpb__btn-small fpb__btn-toggle' 53 | $label.htmlFor = id 54 | $label.textContent = label 55 | if (classes) $label.classList.add(...classes) 56 | 57 | return [$input, $label] 58 | } 59 | 60 | const createCheckToggle = ({ 61 | id, 62 | value, 63 | label, 64 | classes, 65 | checked, 66 | }: { 67 | id: string 68 | label: string 69 | value?: string 70 | classes?: string[] 71 | checked?: boolean 72 | }) => { 73 | const $input = document.createElement('input') 74 | $input.className = 'fpb__hidden-input' 75 | $input.type = 'checkbox' 76 | $input.id = id 77 | $input.autocomplete = 'off' 78 | if (value) $input.value = value 79 | if (checked) $input.checked = true 80 | 81 | const $label = document.createElement('label') 82 | $label.className = 'fpb__btn fpb__btn-pill fpb__btn-small fpb__btn-toggle' 83 | $label.htmlFor = id 84 | $label.textContent = label 85 | if (classes) $label.classList.add(...classes) 86 | 87 | return [$input, $label] 88 | } 89 | 90 | // Toggle applications 91 | export const createVariants = (variants: string[]) => { 92 | // convert variants to numbers (strips i suffix), then convert to set and parse as array 93 | const weights = Array.from(new Set(variants.map((v) => parseInt(v)))) 94 | 95 | return [ 96 | ...weights.flatMap((weight) => 97 | createRadioToggle({ 98 | id: `fp__weight-${weight}`, 99 | name: 'fp__weight', 100 | label: weight.toString(), 101 | value: weight.toString(), 102 | }), 103 | ), 104 | ...createCheckToggle({ id: 'fp__italic', label: 'Italic', classes: ['fpb__btn-secondary'] }), 105 | ] 106 | } 107 | 108 | export const createBadges = (badges: { [key: string]: string }) => { 109 | return Object.entries(badges).flatMap(([value, label]) => 110 | createCheckToggle({ 111 | id: `fp__category-${value}`, 112 | value, 113 | label, 114 | }), 115 | ) 116 | } 117 | 118 | export const setActiveBadges = ($parent: HTMLElement, values: string[]) => { 119 | const $inputs = $parent.querySelectorAll('.fpb__hidden-input') 120 | for (const $input of $inputs) { 121 | $input.checked = values.includes($input.value) 122 | } 123 | } 124 | 125 | export const getActiveBadges = ($parent: HTMLElement) => { 126 | const $inputs = $parent.querySelectorAll('.fpb__hidden-input:checked') 127 | return [...$inputs].map(($input) => $input.value) 128 | } 129 | 130 | // Select options 131 | const createOption = (key: string, label: string) => { 132 | const $option = document.createElement('option') 133 | $option.value = key 134 | $option.textContent = label 135 | return $option 136 | } 137 | 138 | export const createOptions = (options: { [key: string]: string }) => { 139 | return Object.entries(options).map(([key, label]) => createOption(key, label)) 140 | } 141 | -------------------------------------------------------------------------------- /dist/fontpicker.d.ts: -------------------------------------------------------------------------------- 1 | import { default as default_2 } from 'events'; 2 | 3 | declare type Category = 'serif' | 'sans-serif' | 'display' | 'handwriting' | 'monospace'; 4 | 5 | declare type Criterion = 'name' | 'popularity' | 'width' | 'thickness' | 'complexity' | 'curvature'; 6 | 7 | declare interface FamilyProps { 8 | name: string; 9 | variants: string[]; 10 | category?: Category; 11 | subsets?: Subset[]; 12 | popularity?: number; 13 | metrics?: { 14 | width: number; 15 | thickness: number; 16 | complexity: number; 17 | curvature: number; 18 | }; 19 | url?: string; 20 | } 21 | 22 | declare class Font { 23 | static weightNames: { 24 | [weight in FontWeight]: string; 25 | }; 26 | readonly family: FontFamily; 27 | readonly weight: FontWeight; 28 | readonly italic: boolean; 29 | constructor(family: FontFamily, weight: FontWeight, italic: boolean); 30 | get style(): "italic" | "normal"; 31 | get variant(): string; 32 | toId(): string; 33 | toConcise(): string; 34 | toString(): string; 35 | static parse(family: FontFamily, variant?: string): Font; 36 | } 37 | 38 | declare interface FontFamily extends FamilyProps { 39 | } 40 | 41 | declare class FontFamily { 42 | constructor(family: FamilyProps); 43 | toString(): string; 44 | getDefaultVariant(): string; 45 | static parse(raw: string): FontFamily; 46 | } 47 | 48 | declare class FontLoader { 49 | #private; 50 | static loaded(name: string): boolean; 51 | static load(font: string | FontFamily): Promise; 52 | } 53 | 54 | declare class FontPicker extends default_2<{ 55 | open: []; 56 | opened: []; 57 | pick: [font: Font | null]; 58 | clear: []; 59 | cancel: []; 60 | close: []; 61 | closed: []; 62 | }> { 63 | static FontLoader: typeof FontLoader; 64 | private $el; 65 | private $inputEl; 66 | private orgInputType; 67 | private _font; 68 | get font(): Font | null; 69 | private _families; 70 | get families(): Map; 71 | private _favourites; 72 | get favourites(): Set; 73 | private _config; 74 | getConfig(): PickerConfig; 75 | private clickHandler?; 76 | private changeHandler?; 77 | constructor(el: HTMLButtonElement | HTMLInputElement | string, config?: Partial); 78 | configure(options: Partial): void; 79 | private initialize; 80 | private updateFamilies; 81 | getFamily(name: string): FontFamily; 82 | setFont(font: Font | FontFamily | string | null, emit?: boolean): void; 83 | clear(emit?: boolean): void; 84 | markFavourite(family: FontFamily, value?: boolean): boolean; 85 | open(): Promise; 86 | close(): Promise; 87 | destroy(): void; 88 | } 89 | export default FontPicker; 90 | 91 | declare type FontWeight = 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900; 92 | 93 | declare type Language = 'en' | 'nl' | 'de' | 'es' | 'fr'; 94 | 95 | declare type Metric = 'all' | '0!' | '1!' | '2!' | '3!' | '4!'; 96 | 97 | declare interface PickerConfig { 98 | language: Language; 99 | container: HTMLElement; 100 | previewText: string | null; 101 | font: string | null; 102 | verbose: boolean; 103 | variants: boolean; 104 | favourites: string[]; 105 | saveFavourites: boolean; 106 | storageKey: string; 107 | stateKey: string; 108 | defaultSearch: string; 109 | defaultSubset: Subset; 110 | defaultCategories: Category[]; 111 | defaultWidth: Metric; 112 | defaultThickness: Metric; 113 | defaultComplexity: Metric; 114 | defaultCurvature: Metric; 115 | sortBy: Criterion; 116 | sortReverse: boolean; 117 | googleFonts: string[] | null; 118 | systemFonts: string[] | null; 119 | extraFonts: FamilyProps[]; 120 | showCancelButton: boolean; 121 | showClearButton: boolean; 122 | } 123 | 124 | declare type Subset = 'all' | 'arabic' | 'bengali' | 'chinese-hongkong' | 'chinese-simplified' | 'chinese-traditional' | 'cyrillic' | 'cyrillic-ext' | 'devanagari' | 'greek' | 'greek-ext' | 'gujarati' | 'gurmukhi' | 'hebrew' | 'japanese' | 'kannada' | 'khmer' | 'korean' | 'latin' | 'latin-ext' | 'malayalam' | 'myanmar' | 'oriya' | 'sinhala' | 'tamil' | 'telugu' | 'thai' | 'tibetan' | 'vietnamese'; 125 | 126 | export { } 127 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Javascript Font Picker 2 | 3 | An open source, free (as in beer), versatile, flexible and lightweight Javascript Font Picker Component for System fonts, Google fonts and custom (woff/ttf) fonts. Features dynamic font loading, favourites, keyboard navigation, fuzzy search, advanced metrics filters, property sorting and much more. Available in multiple languages. 4 | 5 | ## Please visit [jsfontpicker.com](https://www.jsfontpicker.com/) for more detailed documentation and extensive demo's.

6 | 7 | ## Table of Contents 8 | 9 | - [Javascript Font Picker](#javascript-font-picker) 10 | - [Table of Contents](#table-of-contents) 11 | - [Features](#features) 12 | - [Live Demo](#live-demo) 13 | - [Screenshots](#screenshots) 14 | - [Installation](#installation) 15 | - [IIFE Bundle](#iife-bundle) 16 | - [ESM Bundle](#esm-bundle) 17 | - [Getting started](#getting-started) 18 | - [Create](#create) 19 | - [Configure](#configure) 20 | - [Interact](#interact) 21 | - [Documentation](#documentation) 22 | - [Developing](#developing) 23 | 24 | ## Features 25 | 26 | - ❤️ Favourites 27 | - ⌨️ Keyboard shortcuts 28 | - ⚡ Dynamic font loading 29 | - 🔤 Custom font support 30 | - 🔎 Fuzzy search 31 | - 📐 Advanced metrics filters 32 | - 📶 Property sorting 33 | - 🇳🇱 Translations for English, Dutch, German, Spanish and French 34 | - 💪 No JQuery, just pure ES6 35 | 36 | ## [Live Demo](https://jsfontpicker.com) 37 | 38 | 👆 Try it out now! 39 | 40 | ## Screenshots 41 | 42 | | | Light | Dark | 43 | | :--------- | :----------------------------------------------------: | :---------------------------------------------------: | 44 | | **Button** | | | 45 | | **Dialog** | | | 46 | 47 | ## Installation 48 | 49 | The FontPicker requires a small stylesheet. 50 | Please include the it like this: 51 | 52 | ```html 53 | 54 | ``` 55 | 56 | It is also **highly recommended** to include a preconnect to Google fonts: 57 | 58 | ```html 59 | 60 | 61 | ``` 62 | 63 | Now, depending on your environment, choose one of the following: 64 | 65 | - [IIFE Bundle](#iife-bundle) → When using vanilla JavaScript, without ES modules 66 | - [ESM Bundle](#esm-bundle) → When using ES modules or a bundler 67 | 68 | ### IIFE Bundle 69 | 70 | Please import the IIFE script using a `script` tag in your HTML: 71 | 72 | ```html 73 | 74 | ``` 75 | 76 | This exposes `FontPicker` and `FontPicker.FontLoader` globally (on window). 77 | 78 | ### ESM Bundle 79 | 80 | Please import the ESM bundle using the `import` directive in your script: 81 | 82 | ```js 83 | import FontPicker from 'fontpicker.js' 84 | ``` 85 | 86 | This allows you to use `FontPicker` and `FontPicker.FontLoader` directly. 87 | 88 | ## Getting started 89 | 90 | ### Create 91 | 92 | To create a font picker, first create a button or input element: 93 | 94 | ```html 95 | 96 | ``` 97 | 98 | Next instantiate the FontPicker, passing the element and an (optional) configuration: 99 | 100 | ```js 101 | const picker = new FontPicker('#picker', { 102 | language: 'en', 103 | font: 'Open Sans', 104 | defaultSubset: 'latin', 105 | }) 106 | ``` 107 | 108 | ### Configure 109 | 110 | The picker's configuration can be changed after initialization. This is done by calling `.configure({...})` on the element: 111 | 112 | ```js 113 | picker.configure({ 114 | language: 'nl', 115 | }) 116 | ``` 117 | 118 | ### Interact 119 | 120 | The picker's various methods and properties can also be accessed directly on the element: 121 | 122 | ```js 123 | // Set the current font 124 | picker.setFont('Roboto:800') 125 | 126 | // Handle events 127 | picker.on('pick', (font) => { ... }) 128 | 129 | // Open the FontPicker, which returns a promise! 130 | const font = await picker.open() 131 | ``` 132 | 133 | ## Documentation 134 | 135 | For all methods and properties, please view the [documentation](DOCUMENTATION.md). 136 | 137 | ## Developing 138 | 139 | To install dependencies: 140 | 141 | ```bash 142 | bun|deno|npm|pnpm|yarn install 143 | ``` 144 | 145 | To run: 146 | 147 | ```bash 148 | bun|deno|npm|pnpm|yarn run dev 149 | ``` 150 | 151 | ## License 152 | 153 | This component is released under the MIT license. It is simple and easy to understand and places almost no restrictions on what you can do with the code. 154 | [More Information](http://en.wikipedia.org/wiki/MIT_License) 155 | 156 | The development of this component was funded by [Zygomatic](https://www.zygomatic.nl/). 157 | -------------------------------------------------------------------------------- /src/templates/dialogContent.html: -------------------------------------------------------------------------------- 1 | 111 |
112 | -------------------------------------------------------------------------------- /dist/fontpicker.min.css: -------------------------------------------------------------------------------- 1 | [data-bs-theme=dark],[data-fp-theme=dark]{--fp-select-toggle-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e") !important;--fp-body-bg: #212529 !important;--fp-body-color: #dee2e6 !important;--fp-body-bg-rgb: 33, 37, 41 !important;--fp-border-color: #495057 !important;--fp-border-color-rgb: 73, 80, 87 !important;--fp-border-color-translucent: rgba(255, 255, 255, .15) !important;--fp-tertiary-color: rgba(222, 226, 230, .5) !important}:root,[data-bs-theme=light],[data-fp-theme=light]{--fp-select-toggle-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");--fp-body-bg: #fff;--fp-body-color: #212529;--fp-body-bg-rgb: 255, 255, 255;--fp-border-color: #dee2e6;--fp-border-color-rgb: 222, 226, 230;--fp-border-color-translucent: rgba(0, 0, 0, .175);--fp-tertiary-color: rgba(33, 37, 41, .5)}:root{--fp-dark: #212529;--fp-light: #fff;--fp-primary: #0d6efd;--fp-secondary: #ff8239;--fp-hover-color: #0b5ed7;--fp-ring-color: #86b7fe;--fp-ring-shadow: rgba(13, 110, 253, .25) 0 0 0 .25rem;--fp-danger-rgb: 220, 53, 69;--fp-border-radius-sm: .25rem;--fp-border-radius: .375rem;--fp-border-radius-lg: .5rem;--fp-box-shadow: 0 .5rem 1rem rgba(0, 0, 0, .15);--fp-box-shadow-sm: 0 .125rem .25rem rgba(0, 0, 0, .075)}.fpb__input,.fpb__input *,.fpb__input :before,.fpb__input :after,.fpb__modal,.fpb__modal *,.fpb__modal :before,.fpb__modal :after{box-sizing:border-box}.fpb__input,.fpb__modal{color:var(--fp-body-color);font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Noto Sans,Liberation Sans,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}.fpb__input{display:block;font-size:1rem;font-weight:400;line-height:1.5;color:var(--fp-body-color);appearance:none;background-color:var(--fp-body-bg);padding:.375rem .75rem;border:1px solid var(--fp-border-color);border-radius:var(--fp-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.fpb__input:hover{background-color:var(--fp-body-bg)!important;border:1px solid var(--fp-border-color)!important}.fpb__input:focus{outline:0;border-color:var(--fp-ring-color);box-shadow:var(--fp-ring-shadow)}.fpb__input:disabled{pointer-events:none;cursor:not-allowed;opacity:.75}.fpb__dropdown{margin:unset;padding:.375rem 2.25rem .375rem .75rem;background-image:var(--fp-select-toggle-img);background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px}.fpb__modal{position:fixed;max-width:32rem;width:calc(100vw - 1rem);height:calc(100vh - 3rem);top:50%;left:50%;border:1px solid var(--fp-border-color-translucent);border-radius:var(--fp-border-radius-lg);background-color:var(--fp-body-bg);box-shadow:var(--fp-box-shadow);display:flex;flex-direction:column;overflow:hidden;opacity:0;pointer-events:none;transform:translate(-50%,-51%);transition:opacity .5s,transform .5s;z-index:1055}.fpb__modal.fpb__open{opacity:1;pointer-events:all;transform:translate(-50%,-50%)}.fpb__backdrop{position:fixed;width:100vw;height:100vh;inset:0;z-index:1054;background-color:#000;opacity:0;pointer-events:none;transition:opacity .5s}.fpb__modal.fpb__open+.fpb__backdrop{opacity:.5;pointer-events:all}.fpb__modal-header{display:flex;justify-content:space-between;align-items:center;padding:1rem}.fp__modal-title{margin:0;font-size:1.25rem;font-weight:700;font-family:inherit}.fpb__modal button:hover{background-color:transparent!important}.fpb__modal-footer{display:flex;align-items:center;padding:1rem;gap:1rem;border-top:1px solid var(--fp-border-color)}.fpb__modal .fpb__input{width:100%}.fpb__btn-close{display:grid;place-items:center;color:var(--fp-tertiary-color);font-size:2rem;width:.75em;height:.75em;padding:0;line-height:0;appearance:none;background:none;border:none;border-radius:var(--fp-border-radius)}.fpb__btn-close:hover{color:var(--fp-body-color)}.fpb__btn-close:focus-visible{outline:0;box-shadow:var(--fp-ring-shadow)}.fpb__accordion-item{margin-top:-1px}.fpb__accordion-toggle{--fp-accordion-toggle-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");display:flex;width:100%;align-items:center;color:var(--fp-body-color);font-size:1rem;text-align:start;appearance:none;background:none;border:none;border-radius:0;border-block:1px solid var(--fp-border-color)!important;padding:.5rem 1rem;margin:0;transition:border-color .15s,box-shadow .15s}.fpb__accordion-toggle:after{content:"";margin-left:auto;width:1.25rem;height:1.25rem;background-image:var(--fp-accordion-toggle-img);background-repeat:no-repeat;background-size:100% 100%;transition:transform .2s}.fpb__accordion-item.fpb__open>.fpb__accordion-toggle:after{transform:rotate(-180deg)}.fpb__accordion-content{--fpb-height: 0;max-height:0;overflow:hidden;visibility:hidden;transition:max-height .2s}.fpb__accordion-content>*{padding:.5rem 1rem}.fpb__accordion-item.fpb__open .fpb__accordion-content{--fpb-height: fit-content;max-height:var(--fpb-height);visibility:visible}.fpb__accordion-toggle:focus{outline:0;box-shadow:var(--fp-ring-shadow);border-color:var(--fp-ring-color)}[role=button],button{cursor:pointer}.fpb__btn{--fpb-variant: var(--fp-primary);cursor:pointer;font-size:1rem;padding:.375rem .75rem;line-height:1.5;color:var(--fp-light);text-align:center;user-select:none;border:1px solid var(--fpb-variant);background-color:var(--fpb-variant);border-radius:var(--fp-border-radius);transition:color .15s,background-color .15s,border-color .15s,box-shadow .15s,opacity .15s}.fpb__btn:hover{opacity:.8}.fpb__btn-small{font-size:.75rem;padding:.1rem .5rem}.fpb__btn-pill{border-radius:9999px}button.fpb__btn-pill:hover{background-color:var(--fpb-variant)!important;border-color:var(--fpb-variant)!important}.fpb__btn-secondary{--fpb-variant: var(--fp-secondary)}.fpb__btn-link{color:var(--fpb-variant);background:none;border:none}.fpb__btn-link:after{content:"";width:0px;height:1px;display:block;background:currentColor;opacity:0;transition:width .2s,opacity .2s}.fpb__btn-link:hover{background:inherit!important}.fpb__btn-link:hover:after{width:100%;opacity:1}.fpb__hidden-input{position:absolute!important;clip:rect(0,0,0,0);pointer-events:none}.fpb__btn-toggle{white-space:nowrap;color:var(--fpb-variant);background-color:transparent}.fpb__btn-flip,.fpb__btn-toggle.fpb__active,input:checked+.fpb__btn-toggle{background-color:var(--fpb-variant);border-color:var(--fpb-variant);color:var(--fp-light)}.fpb__btn-flip>*{transition:transform .25s ease-in-out}input:checked+.fpb__btn-flip>*,.fpb__btn-flip.fpb__active>*{transform:scaleY(-1)}.fpb__btn:focus-visible,input:focus-visible+.fpb__btn-toggle{outline:0;opacity:.8;box-shadow:var(--fp-ring-shadow)}.fpb__btn:disabled,input:disabled+.fpb__btn-toggle{opacity:.5;cursor:not-allowed}.fpb__input-group{display:flex}.fpb__input-group>*:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.fpb__input-group>*:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.fpb__grid-2{display:grid;grid-template-columns:1fr 1fr;gap:.5rem}.fpb__span-2{grid-column:span 2}.fpb__grow{flex-grow:1}.fpb__hlist{display:flex;overflow-x:auto;gap:.5rem}.fpb__hidden{display:none!important}.fpb__has-icon,.fpb__has-icon *{vertical-align:middle}.fpb__has-icon svg{margin-right:.25rem}.fpb__primary{color:var(--fp-primary)}@media (max-width: 576px){.fpb__modal{height:calc(100vh - 1rem)}.fpb__grid-2{display:flex;flex-direction:column}}.font-picker{text-align:start;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;vertical-align:middle}input.font-picker{caret-color:transparent}.fp__changed:after{content:"*";color:var(--fp-secondary)}#fp__fonts{display:flex;flex-direction:column;flex-grow:1;overflow-y:scroll;padding:.25rem;margin-top:-1px;border-block:1px solid var(--fp-border-color)}#fp__fonts:focus-visible{outline:0;box-shadow:var(--fp-ring-shadow);border-color:var(--fp-ring-color);transition:border-color .15s,box-shadow .15s}.fp__font-item{display:flex;justify-content:space-between;align-items:center;padding:.25rem 1rem;min-height:2rem;user-select:none;border-radius:9999px}.fp__font-item:hover{background:var(--fp-border-color)}.fp__font-item.fp__selected{color:var(--fp-light);background:var(--fp-primary)}.fp__font-family{font-size:1rem;pointer-events:none}.fp__heart{height:1em}.fp__heart svg{height:1em;pointer-events:none;vertical-align:baseline;--fp-heart-color: var(--fp-border-color-rgb);fill:rgba(var(--fp-heart-color),.5);stroke:rgb(var(--fp-heart-color))}.fp__heart:hover svg{fill:rgb(var(--fp-heart-color))}.fp__font-item:hover .fp__heart svg,.fp__font-item.fp__selected .fp__heart svg{--fp-heart-color: var(--fp-body-bg-rgb)}.fp__font-item.fp__fav .fp__heart svg{--fp-heart-color: var(--fp-danger-rgb)}.fp__font-item.fp__fav.fp__selected .fp__heart svg{filter:drop-shadow(0px 0px 2px var(--fp-dark))}.fp__preview-container{padding:.25rem}#fp__preview{white-space:nowrap;text-overflow:ellipsis;overflow:hidden;text-align:center;font-size:1.25rem;line-height:1.5;padding-inline:.75rem;border:1px solid transparent;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}#fp__preview:focus{outline:0;border-color:var(--fp-ring-color);box-shadow:var(--fp-ring-shadow)}#fp__variants{display:flex;flex-wrap:wrap;justify-content:center;gap:.5rem;padding:.5rem 1rem;border-top:1px solid var(--fp-border-color)}#fp__variants:has(#fp__italic:checked){font-style:italic!important}#fp__variants:empty{display:none} 2 | -------------------------------------------------------------------------------- /src/core/FontPicker.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events' 2 | import { PickerDialog } from './PickerDialog' 3 | import { Font } from '../helpers/Font' 4 | import { FontLoader } from '../helpers/FontLoader' 5 | import { FontFamily } from '../helpers/FontFamily' 6 | import { translations } from '../data/translations' 7 | import { googleFonts, systemFonts } from '../data/fonts' 8 | 9 | import type { PickerConfig } from '../types/fontpicker' 10 | 11 | let pickerDialog: PickerDialog | null = null 12 | 13 | export class FontPicker extends EventEmitter<{ 14 | open: [] 15 | opened: [] 16 | pick: [font: Font | null] 17 | clear: [] 18 | cancel: [] 19 | close: [] 20 | closed: [] 21 | }> { 22 | static FontLoader = FontLoader 23 | 24 | private $el: HTMLButtonElement | HTMLInputElement | HTMLDivElement 25 | private $inputEl: HTMLInputElement 26 | private orgInputType: string 27 | 28 | private _font: Font | null 29 | get font() { 30 | return this._font 31 | } 32 | 33 | private _families: Map 34 | get families() { 35 | return this._families 36 | } 37 | 38 | private _favourites: Set 39 | get favourites() { 40 | return this._favourites 41 | } 42 | 43 | private _config: PickerConfig = { 44 | language: 'en', 45 | container: document.body, 46 | previewText: null, 47 | 48 | font: null, 49 | verbose: false, 50 | variants: true, 51 | 52 | favourites: [], 53 | saveFavourites: true, 54 | storageKey: 'fp__favourites', 55 | stateKey: 'default', 56 | 57 | defaultSearch: '', 58 | defaultSubset: 'all', 59 | defaultCategories: ['display', 'handwriting', 'monospace', 'sans-serif', 'serif'], 60 | defaultWidth: 'all', 61 | defaultThickness: 'all', 62 | defaultComplexity: 'all', 63 | defaultCurvature: 'all', 64 | 65 | sortBy: 'popularity', 66 | sortReverse: false, 67 | 68 | googleFonts: null, 69 | systemFonts: null, 70 | 71 | extraFonts: [], 72 | 73 | showCancelButton: true, 74 | showClearButton: false, 75 | } 76 | 77 | getConfig(): PickerConfig { 78 | return { ...this._config } 79 | } 80 | 81 | private clickHandler?: () => void 82 | private changeHandler?: () => void 83 | 84 | constructor( 85 | el: HTMLButtonElement | HTMLInputElement | string, 86 | config: Partial = {}, 87 | ) { 88 | super() 89 | this.$el = typeof el === 'string' ? document.querySelector(el)! : el 90 | 91 | if (this.$el instanceof HTMLInputElement) { 92 | // This is an element. Wrap inside
. 93 | this.orgInputType = this.$el.type 94 | if (this.$el.value) { 95 | config.font = this.$el.value 96 | } 97 | const $wrap = document.createElement('button') 98 | $wrap.setAttribute('type', 'button'); 99 | this.$el.after($wrap) 100 | 101 | this.$inputEl = this.$el 102 | this.$inputEl.type = 'hidden' 103 | 104 | this.$el = $wrap 105 | 106 | this.changeHandler = () => this.setFont(this.$inputEl.value) 107 | this.$inputEl.addEventListener('change', this.changeHandler) 108 | } else if (this.$el.dataset.font) { 109 | config.font = this.$el.dataset.font 110 | } 111 | 112 | this.$el.classList.add('font-picker', 'fpb__input', 'fpb__dropdown') 113 | this.clickHandler = this.open.bind(this) 114 | this.$el.addEventListener('click', this.clickHandler) 115 | 116 | this.configure(config) 117 | this.initialize() 118 | } 119 | 120 | configure(options: Partial) { 121 | if ( 122 | 'container' in options && 123 | options.container && 124 | !(options.container instanceof HTMLElement) 125 | ) { 126 | // container can be a DOM element or a string 127 | options.container = document.querySelector(options.container) ?? undefined 128 | } 129 | 130 | Object.assign(this._config, options) 131 | 132 | const keys = Object.keys(options) 133 | 134 | // when family list hasn't been assigned, or a new one has been passed 135 | if ( 136 | !this.families || 137 | keys.includes('googleFonts') || 138 | keys.includes('systemFonts') || 139 | keys.includes('extraFonts') 140 | ) { 141 | this.updateFamilies() 142 | } 143 | 144 | // when font hasn't been assigned, or a new one has been passed 145 | if (!this.font || keys.includes('font')) { 146 | this.setFont(this._config.font) 147 | } 148 | } 149 | 150 | private initialize() { 151 | // load favourites 152 | const favourites: string[] = this._config.favourites.slice() 153 | 154 | if (this._config.saveFavourites) { 155 | const names = localStorage.getItem(this._config.storageKey) 156 | if (names) favourites.push(...JSON.parse(names)) 157 | } 158 | 159 | this._favourites = new Set() 160 | 161 | for (const name of favourites) { 162 | try { 163 | const family = this.getFamily(name) 164 | this._favourites.add(family) 165 | } catch (error) { 166 | console.warn(`Font from favourites is not available: '${name}'!`) 167 | } 168 | } 169 | } 170 | 171 | private updateFamilies() { 172 | const families = [ 173 | ...googleFonts.filter((font) => this._config.googleFonts?.includes(font.name) ?? true), 174 | ...systemFonts.filter((font) => this._config.systemFonts?.includes(font.name) ?? true), 175 | ...this._config.extraFonts.map((font) => new FontFamily(font)), 176 | ] 177 | 178 | this._families = new Map() 179 | families.forEach((family) => this.families.set(family.name, family)) 180 | } 181 | 182 | getFamily(name: string) { 183 | const family = this.families.get(name) 184 | if (!family) throw new Error(`Could not find font family '${name}'!`) 185 | return family 186 | } 187 | 188 | setFont(font: Font | FontFamily | string | null, emit: boolean = false) { 189 | if (!font) { 190 | this._font = null 191 | } else if (font instanceof Font) { 192 | // directly set font 193 | this._font = font 194 | } else if (typeof font === 'string') { 195 | // set font parsed from name 196 | const [name, variant] = font.split(':') 197 | const family = this.getFamily(name) 198 | this._font = Font.parse(family, variant) 199 | } else { 200 | // set font from font family 201 | this._font = Font.parse(font) 202 | } 203 | 204 | if (this.font) { 205 | // check if font variant is supported by font family 206 | if (!this.font.family.variants.includes(this.font.variant)) { 207 | const variant = this.font.family.getDefaultVariant() 208 | console.warn( 209 | `Variant ${this.font.variant} not supported by '${this.font.family.name}', falling back to ${variant}.`, 210 | ) 211 | this._font = Font.parse(this.font.family, variant) 212 | } 213 | 214 | const text = this._config.verbose ? this.font.toString() : this.font.toConcise() 215 | this.$el.textContent = text 216 | this.$el.dataset.font = this.font.toId() 217 | if (this.$inputEl) { 218 | this.$inputEl.value = this.font.toId() 219 | } 220 | 221 | this.$el.style.fontFamily = `"${this.font.family}"` 222 | this.$el.style.fontWeight = this.font.weight.toString() 223 | this.$el.style.fontStyle = this.font.style 224 | 225 | FontLoader.load(this.font.family) 226 | } else { 227 | this.$el.textContent = translations[this._config.language].pickHint 228 | this.$el.dataset.font = '' 229 | if (this.$inputEl) { 230 | this.$inputEl.value = '' 231 | } 232 | 233 | this.$el.style.removeProperty('font-family') 234 | this.$el.style.removeProperty('font-weight') 235 | this.$el.style.removeProperty('font-style') 236 | } 237 | 238 | if (emit) { 239 | this.emit('pick', this.font) 240 | if (this.$inputEl) { 241 | this.$inputEl.dispatchEvent(new Event('change')) 242 | } 243 | } 244 | } 245 | 246 | clear(emit?: boolean) { 247 | this.setFont(null, emit) 248 | if (emit) this.emit('clear') 249 | } 250 | 251 | markFavourite(family: FontFamily, value?: boolean) { 252 | if (value === undefined) value = !this.favourites.has(family) 253 | 254 | if (value) { 255 | this.favourites.add(family) 256 | } else { 257 | this.favourites.delete(family) 258 | } 259 | 260 | // save to storage 261 | if (this._config.saveFavourites) { 262 | const data = Array.from(this.favourites).map((font) => font.name) 263 | localStorage.setItem(this._config.storageKey, JSON.stringify(data)) 264 | } 265 | 266 | return value 267 | } 268 | 269 | async open() { 270 | // close existing fontpicker 271 | this.close() 272 | 273 | pickerDialog = new PickerDialog(this._config.container) 274 | await pickerDialog.open(this) 275 | pickerDialog = null 276 | 277 | return this.font 278 | } 279 | 280 | async close() { 281 | pickerDialog?.close() 282 | } 283 | 284 | destroy() { 285 | this.close() 286 | pickerDialog?.destroy() 287 | 288 | if (this.changeHandler) this.$el.removeEventListener('change', this.changeHandler) 289 | if (this.clickHandler) this.$el.removeEventListener('click', this.clickHandler) 290 | 291 | this.$el.classList.remove('font-picker', 'fpb__input', 'fpb__dropdown') 292 | this.$el.removeAttribute('data-font') 293 | this.$el.style.removeProperty('font-family') 294 | this.$el.style.removeProperty('font-weight') 295 | this.$el.style.removeProperty('font-style') 296 | 297 | if (this.$inputEl) { 298 | this.$inputEl.type = this.orgInputType 299 | this.$el.remove() 300 | } 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /src/css/fpb.css: -------------------------------------------------------------------------------- 1 | /* Theming */ 2 | [data-bs-theme='dark'], 3 | [data-fp-theme='dark'] { 4 | --fp-select-toggle-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e") !important; 5 | 6 | --fp-body-bg: #212529 !important; 7 | --fp-body-color: #dee2e6 !important; 8 | --fp-body-bg-rgb: 33, 37, 41 !important; 9 | 10 | --fp-border-color: #495057 !important; 11 | --fp-border-color-rgb: 73, 80, 87 !important; 12 | --fp-border-color-translucent: rgba(255, 255, 255, 0.15) !important; 13 | --fp-tertiary-color: rgba(222, 226, 230, 0.5) !important; 14 | } 15 | 16 | :root, 17 | [data-bs-theme='light'], 18 | [data-fp-theme='light'] { 19 | --fp-select-toggle-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e"); 20 | 21 | --fp-body-bg: #fff; 22 | --fp-body-color: #212529; 23 | --fp-body-bg-rgb: 255, 255, 255; 24 | 25 | --fp-border-color: #dee2e6; 26 | --fp-border-color-rgb: 222, 226, 230; 27 | --fp-border-color-translucent: rgba(0, 0, 0, 0.175); 28 | --fp-tertiary-color: rgba(33, 37, 41, 0.5); 29 | } 30 | 31 | :root { 32 | --fp-dark: #212529; 33 | --fp-light: #fff; 34 | 35 | --fp-primary: #0d6efd; 36 | --fp-secondary: #ff8239; 37 | --fp-hover-color: #0b5ed7; 38 | --fp-ring-color: #86b7fe; 39 | --fp-ring-shadow: rgba(13, 110, 253, 0.25) 0 0 0 0.25rem; 40 | --fp-danger-rgb: 220, 53, 69; 41 | 42 | --fp-border-radius-sm: 0.25rem; 43 | --fp-border-radius: 0.375rem; 44 | --fp-border-radius-lg: 0.5rem; 45 | 46 | --fp-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); 47 | --fp-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); 48 | } 49 | 50 | /* Global */ 51 | .fpb__input, 52 | .fpb__input *, 53 | .fpb__input ::before, 54 | .fpb__input ::after, 55 | .fpb__modal, 56 | .fpb__modal *, 57 | .fpb__modal ::before, 58 | .fpb__modal ::after { 59 | box-sizing: border-box; 60 | } 61 | 62 | /* Default component styling */ 63 | .fpb__input, 64 | .fpb__modal { 65 | color: var(--fp-body-color); 66 | font-family: 67 | system-ui, 68 | -apple-system, 69 | 'Segoe UI', 70 | Roboto, 71 | 'Helvetica Neue', 72 | 'Noto Sans', 73 | 'Liberation Sans', 74 | Arial, 75 | sans-serif, 76 | 'Apple Color Emoji', 77 | 'Segoe UI Emoji', 78 | 'Segoe UI Symbol', 79 | 'Noto Color Emoji'; 80 | } 81 | 82 | /* Inputs */ 83 | .fpb__input { 84 | display: block; 85 | 86 | font-size: 1rem; 87 | font-weight: 400; 88 | line-height: 1.5; 89 | color: var(--fp-body-color); 90 | 91 | appearance: none; 92 | background-color: var(--fp-body-bg); 93 | 94 | padding: 0.375rem 0.75rem; 95 | border: 1px solid var(--fp-border-color); 96 | border-radius: var(--fp-border-radius); 97 | 98 | transition: 99 | border-color 0.15s ease-in-out, 100 | box-shadow 0.15s ease-in-out; 101 | } 102 | .fpb__input:hover { 103 | background-color: var(--fp-body-bg) !important; 104 | border: 1px solid var(--fp-border-color) !important; 105 | } 106 | .fpb__input:focus { 107 | outline: 0; 108 | border-color: var(--fp-ring-color); 109 | box-shadow: var(--fp-ring-shadow); 110 | } 111 | .fpb__input:disabled { 112 | pointer-events: none; 113 | cursor: not-allowed; 114 | opacity: 0.75; 115 | } 116 | .fpb__dropdown { 117 | margin: unset; /* CSS reset */ 118 | padding: 0.375rem 2.25rem 0.375rem 0.75rem; 119 | background-image: var(--fp-select-toggle-img); 120 | background-repeat: no-repeat; 121 | background-position: right 0.75rem center; 122 | background-size: 16px 12px; 123 | } 124 | 125 | /* Modals */ 126 | .fpb__modal { 127 | position: fixed; 128 | max-width: 32rem; 129 | width: calc(100vw - 1rem); 130 | height: calc(100vh - 3rem); 131 | top: 50%; 132 | left: 50%; 133 | 134 | border: 1px solid var(--fp-border-color-translucent); 135 | border-radius: var(--fp-border-radius-lg); 136 | background-color: var(--fp-body-bg); 137 | box-shadow: var(--fp-box-shadow); 138 | 139 | display: flex; 140 | flex-direction: column; 141 | overflow: hidden; 142 | 143 | opacity: 0; 144 | pointer-events: none; 145 | transform: translate(-50%, -51%); 146 | 147 | transition: 148 | opacity 0.5s, 149 | transform 0.5s; 150 | 151 | z-index: 1055; 152 | } 153 | .fpb__modal.fpb__open { 154 | opacity: 1; 155 | pointer-events: all; 156 | transform: translate(-50%, -50%); 157 | } 158 | .fpb__backdrop { 159 | position: fixed; 160 | width: 100vw; 161 | height: 100vh; 162 | inset: 0; 163 | z-index: 1054; 164 | 165 | background-color: #000; 166 | opacity: 0; 167 | pointer-events: none; 168 | 169 | transition: opacity 0.5s; 170 | } 171 | .fpb__modal.fpb__open + .fpb__backdrop { 172 | opacity: 0.5; 173 | pointer-events: all; 174 | } 175 | 176 | .fpb__modal-header { 177 | display: flex; 178 | justify-content: space-between; 179 | align-items: center; 180 | padding: 1rem; 181 | } 182 | .fp__modal-title { 183 | margin: 0; 184 | font-size: 1.25rem; 185 | font-weight: 700; 186 | font-family: inherit; 187 | } 188 | 189 | .fpb__modal button:hover { 190 | background-color: transparent !important; /* CSS reset */ 191 | } 192 | 193 | .fpb__modal-footer { 194 | display: flex; 195 | align-items: center; 196 | padding: 1rem; 197 | gap: 1rem; 198 | border-top: 1px solid var(--fp-border-color); 199 | } 200 | .fpb__modal .fpb__input { 201 | width: 100%; 202 | } 203 | 204 | .fpb__btn-close { 205 | display: grid; 206 | place-items: center; 207 | 208 | color: var(--fp-tertiary-color); 209 | font-size: 2rem; 210 | width: 0.75em; 211 | height: 0.75em; 212 | padding: 0; 213 | 214 | line-height: 0; 215 | appearance: none; 216 | background: none; 217 | border: none; 218 | border-radius: var(--fp-border-radius); 219 | } 220 | .fpb__btn-close:hover { 221 | color: var(--fp-body-color); 222 | } 223 | .fpb__btn-close:focus-visible { 224 | outline: 0; 225 | box-shadow: var(--fp-ring-shadow); 226 | } 227 | 228 | /* Accordion */ 229 | .fpb__accordion-item { 230 | margin-top: -1px; 231 | } 232 | .fpb__accordion-toggle { 233 | --fp-accordion-toggle-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); 234 | 235 | display: flex; 236 | width: 100%; 237 | align-items: center; 238 | 239 | color: var(--fp-body-color); 240 | font-size: 1rem; 241 | text-align: start; 242 | 243 | appearance: none; 244 | background: none; 245 | border: none; 246 | border-radius: 0; 247 | border-block: 1px solid var(--fp-border-color) !important; 248 | 249 | padding: 0.5rem 1rem; 250 | margin: 0; 251 | 252 | transition: 253 | border-color 0.15s, 254 | box-shadow 0.15s; 255 | } 256 | 257 | .fpb__accordion-toggle::after { 258 | content: ''; 259 | margin-left: auto; 260 | width: 1.25rem; 261 | height: 1.25rem; 262 | background-image: var(--fp-accordion-toggle-img); 263 | background-repeat: no-repeat; 264 | background-size: 100% 100%; 265 | transition: transform 0.2s; 266 | } 267 | .fpb__accordion-item.fpb__open > .fpb__accordion-toggle::after { 268 | transform: rotate(-180deg); 269 | } 270 | .fpb__accordion-content { 271 | --fpb-height: 0; 272 | max-height: 0; 273 | overflow: hidden; 274 | visibility: hidden; 275 | transition: max-height 0.2s; 276 | } 277 | .fpb__accordion-content > * { 278 | padding: 0.5rem 1rem; 279 | } 280 | .fpb__accordion-item.fpb__open .fpb__accordion-content { 281 | --fpb-height: fit-content; 282 | max-height: var(--fpb-height); 283 | visibility: visible; 284 | } 285 | .fpb__accordion-toggle:focus { 286 | outline: 0; 287 | box-shadow: var(--fp-ring-shadow); 288 | border-color: var(--fp-ring-color); 289 | } 290 | 291 | /* Button */ 292 | [role='button'], 293 | button { 294 | cursor: pointer; 295 | } 296 | 297 | .fpb__btn { 298 | --fpb-variant: var(--fp-primary); 299 | 300 | cursor: pointer; 301 | font-size: 1rem; 302 | padding: 0.375rem 0.75rem; 303 | line-height: 1.5; 304 | color: var(--fp-light); 305 | text-align: center; 306 | user-select: none; 307 | border: 1px solid var(--fpb-variant); 308 | background-color: var(--fpb-variant); 309 | border-radius: var(--fp-border-radius); 310 | 311 | transition: 312 | color 0.15s, 313 | background-color 0.15s, 314 | border-color 0.15s, 315 | box-shadow 0.15s, 316 | opacity 0.15s; 317 | } 318 | .fpb__btn:hover { 319 | opacity: 0.8; 320 | } 321 | .fpb__btn-small { 322 | font-size: 0.75rem; 323 | padding: 0.1rem 0.5rem; 324 | } 325 | .fpb__btn-pill { 326 | border-radius: 9999px; 327 | } 328 | button.fpb__btn-pill:hover { 329 | /* CSS reset */ 330 | background-color: var(--fpb-variant) !important; 331 | border-color: var(--fpb-variant) !important; 332 | } 333 | .fpb__btn-secondary { 334 | --fpb-variant: var(--fp-secondary); 335 | } 336 | .fpb__btn-link { 337 | color: var(--fpb-variant); 338 | background: none; 339 | border: none; 340 | } 341 | .fpb__btn-link::after { 342 | content: ''; 343 | width: 0px; 344 | height: 1px; 345 | display: block; 346 | background: currentColor; 347 | opacity: 0; 348 | transition: 349 | width 0.2s, 350 | opacity 0.2s; 351 | } 352 | .fpb__btn-link:hover { 353 | /* CSS reset */ 354 | background: inherit !important; 355 | } 356 | .fpb__btn-link:hover::after { 357 | width: 100%; 358 | opacity: 1; 359 | } 360 | 361 | .fpb__hidden-input { 362 | position: absolute !important; 363 | clip: rect(0, 0, 0, 0); 364 | pointer-events: none; 365 | } 366 | .fpb__btn-toggle { 367 | white-space: nowrap; 368 | color: var(--fpb-variant); 369 | background-color: transparent; 370 | } 371 | .fpb__btn-flip, 372 | .fpb__btn-toggle.fpb__active, 373 | input:checked + .fpb__btn-toggle { 374 | background-color: var(--fpb-variant); 375 | border-color: var(--fpb-variant); 376 | color: var(--fp-light); 377 | } 378 | .fpb__btn-flip > * { 379 | transition: transform 0.25s ease-in-out; 380 | } 381 | input:checked + .fpb__btn-flip > *, 382 | .fpb__btn-flip.fpb__active > * { 383 | transform: scaleY(-1); 384 | } 385 | 386 | .fpb__btn:focus-visible, 387 | input:focus-visible + .fpb__btn-toggle { 388 | outline: 0; 389 | opacity: 0.8; 390 | box-shadow: var(--fp-ring-shadow); 391 | } 392 | 393 | .fpb__btn:disabled, 394 | input:disabled + .fpb__btn-toggle { 395 | opacity: 0.5; 396 | cursor: not-allowed; 397 | } 398 | 399 | /* Input groups */ 400 | .fpb__input-group { 401 | display: flex; 402 | } 403 | .fpb__input-group > *:first-child { 404 | border-top-right-radius: 0; 405 | border-bottom-right-radius: 0; 406 | } 407 | .fpb__input-group > *:last-child { 408 | border-top-left-radius: 0; 409 | border-bottom-left-radius: 0; 410 | } 411 | 412 | /* Grid utility */ 413 | .fpb__grid-2 { 414 | display: grid; 415 | grid-template-columns: 1fr 1fr; 416 | gap: 0.5rem; 417 | } 418 | .fpb__span-2 { 419 | grid-column: span 2; 420 | } 421 | 422 | .fpb__grow { 423 | flex-grow: 1; 424 | } 425 | 426 | .fpb__hlist { 427 | display: flex; 428 | overflow-x: auto; 429 | gap: 0.5rem; 430 | } 431 | 432 | .fpb__hidden { 433 | display: none !important; 434 | } 435 | 436 | /* Text utility */ 437 | .fpb__has-icon, 438 | .fpb__has-icon * { 439 | vertical-align: middle; 440 | } 441 | .fpb__has-icon svg { 442 | margin-right: 0.25rem; 443 | } 444 | 445 | .fpb__primary { 446 | color: var(--fp-primary); 447 | } 448 | 449 | @media (max-width: 576px) { 450 | .fpb__modal { 451 | height: calc(100vh - 1rem); 452 | } 453 | .fpb__grid-2 { 454 | display: flex; 455 | flex-direction: column; 456 | } 457 | } 458 | -------------------------------------------------------------------------------- /DOCUMENTATION.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | - [Documentation](#documentation) 4 | - [(class) FontPicker](#class-fontpicker) 5 | - [Constructor](#constructor) 6 | - [Methods](#methods) 7 | - [getConfig](#getconfig) 8 | - [configure](#configure) 9 | - [setFont](#setfont) 10 | - [clear](#clear) 11 | - [open](#open) 12 | - [close](#close) 13 | - [destroy](#destroy) 14 | - [Properties](#properties) 15 | - [font](#font) 16 | - [families](#families) 17 | - [favourites](#favourites) 18 | - [Events](#events) 19 | - [open](#open-1) 20 | - [opened](#opened) 21 | - [close](#close-1) 22 | - [closed](#closed) 23 | - [pick](#pick) 24 | - [clear](#clear-1) 25 | - [cancel](#cancel) 26 | - [(class) Font](#class-font) 27 | - [Methods](#methods-1) 28 | - [toId](#toid) 29 | - [toConcise](#toconcise) 30 | - [toString](#tostring) 31 | - [Properties](#properties-1) 32 | - [family](#family) 33 | - [weight](#weight) 34 | - [italic](#italic) 35 | - [style](#style) 36 | - [variant](#variant) 37 | - [(class) FontFamily](#class-fontfamily) 38 | - [Methods](#methods-2) 39 | - [toString](#tostring-1) 40 | - [getDefaultVariant](#getdefaultvariant) 41 | - [Types](#types) 42 | - [(interface) PickerConfig](#interface-pickerconfig) 43 | - [(interface) FamilyProps](#interface-familyprops) 44 | - [(type) Language](#type-language) 45 | - [(type) Subset](#type-subset) 46 | - [(type) Category](#type-category) 47 | - [(type) Metric](#type-metric) 48 | - [(type) Criterion](#type-criterion) 49 | - [(type) FontWeight](#type-fontweight) 50 | 51 | ## (class) FontPicker 52 | 53 | ### Constructor 54 | 55 | Creates a new FontPicker instance. 56 | 57 | ```js 58 | new FontPicker(element, config) 59 | ``` 60 | 61 | **Arguments** 62 | 63 | > **element** 64 | > The (query for a) button or input element to bind to 65 | > **Type:** [`HTMLButtonElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLButtonElement) | [`HTMLInputElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement) | `string` 66 | 67 | > **config** _(partial, optional)_ 68 | > Picker configuration options. 69 | > **Type:** [`PickerConfig`](#interface-pickerconfig) 70 | 71 | ### Methods 72 | 73 | #### getConfig 74 | 75 | Gets the current picker configuration. 76 | 77 | ```js 78 | .getConfig() 79 | ``` 80 | 81 | **Returns** 82 | 83 | > The current configuration object. 84 | > **Type:** [`PickerConfig`](#interface-pickerconfig) 85 | 86 | #### configure 87 | 88 | Sets the picker configuration. 89 | 90 | ```js 91 | .configure(config) 92 | ``` 93 | 94 | **Arguments** 95 | 96 | > **config** _(partial)_ 97 | > Picker configuration options. 98 | > **Type:** [`PickerConfig`](#interface-pickerconfig) 99 | 100 | #### setFont 101 | 102 | Sets the currently selected font. 103 | 104 | ```js 105 | .setFont(font, [emit]) 106 | ``` 107 | 108 | **Arguments** 109 | 110 | > **font** 111 | > Font family name, optionally with variant. 112 | > **Type:** [`Font`](#class-font) | [`FontFamily`](#class-fontfamily) | `string` | `null` 113 | 114 | > **emit** 115 | > Emit an event? 116 | > **Type:** `boolean` 117 | > **Default:** `false` 118 | 119 | #### clear 120 | 121 | Clears the picker, so no font is currently selected. 122 | 123 | ```js 124 | .clear([emit]) 125 | ``` 126 | 127 | **Arguments** 128 | 129 | > **emit** 130 | > Emit an event? 131 | > **Type:** `boolean` 132 | > **Default:** `false` 133 | 134 | #### open 135 | 136 | Opens the font picker dialog. 137 | 138 | ```js 139 | .open() 140 | ``` 141 | 142 | **Returns** 143 | 144 | > Promise with the picked font. 145 | > **Type:** [`Promise`](#class-font) 146 | 147 | #### close 148 | 149 | Closes the font picker dialog. 150 | 151 | ```js 152 | .close() 153 | ``` 154 | 155 | **Returns** 156 | 157 | > Empty promise. 158 | > **Type:** `Promise` 159 | 160 | #### destroy 161 | 162 | Destroys the font picker dialog. 163 | 164 | ```js 165 | .destroy() 166 | ``` 167 | 168 | ### Properties 169 | 170 | #### font 171 | 172 | > _(readonly)_ 173 | > The currently selected font. 174 | > **Type:** [`Font`](#class-font) | `null` 175 | 176 | #### families 177 | 178 | > _(readonly)_ 179 | > A list of available font families. 180 | > **Type:** [`Map`](#class-fontfamily) 181 | 182 | #### favourites 183 | 184 | > _(readonly)_ 185 | > A list of favourited font families. 186 | > **Type:** [`Set`](#class-fontfamily) 187 | 188 | ### Events 189 | 190 | #### open 191 | 192 | Fires when the font picker dialog starts opening. 193 | 194 | ``` 195 | .on('open', () => ...) 196 | ``` 197 | 198 | #### opened 199 | 200 | Fires when the font picker dialog has finished opening. 201 | 202 | ``` 203 | .on('opened', () => ...) 204 | ``` 205 | 206 | #### close 207 | 208 | Fires when the font picker dialog starts closing. 209 | 210 | ``` 211 | .on('close', () => ...) 212 | ``` 213 | 214 | #### closed 215 | 216 | Fires when the font picker dialog has finished closing. 217 | 218 | ``` 219 | .on('closed', () => ...) 220 | ``` 221 | 222 | #### pick 223 | 224 | Fires when a font is succesfully picked. 225 | 226 | ``` 227 | .on('pick', (font) => ...) 228 | ``` 229 | 230 | **Arguments** 231 | 232 | > **font** 233 | > The picked font. 234 | > **Type:** [`Font`](#class-font) | `null` 235 | 236 | #### clear 237 | 238 | Fires when font is cleared. 239 | 240 | ``` 241 | .on('clear', () => ...) 242 | ``` 243 | 244 | #### cancel 245 | 246 | Fires when font selection is cancelled. 247 | 248 | ``` 249 | .on('cancel', () => ...) 250 | ``` 251 | 252 | ## (class) Font 253 | 254 | ### Methods 255 | 256 | #### toId 257 | 258 | Get the font's ID string. 259 | 260 | ```js 261 | .toId() 262 | ``` 263 | 264 | **Returns** 265 | 266 | > The font's ID string. 267 | > **Type:** `string` 268 | 269 | #### toConcise 270 | 271 | Get the font's concise name string. 272 | 273 | ```js 274 | .toConcise() 275 | ``` 276 | 277 | **Returns** 278 | 279 | > The font's concise name string. 280 | > **Type:** `string` 281 | 282 | #### toString 283 | 284 | Get the font's verbose name string. 285 | 286 | ```js 287 | .toString() 288 | ``` 289 | 290 | **Returns** 291 | 292 | > The font's verbose name string. 293 | > **Type:** `string` 294 | 295 | ### Properties 296 | 297 | #### family 298 | 299 | > _(readonly)_ 300 | > The font family. 301 | > **Type:** [`FontFamily`](#class-fontfamily) 302 | 303 | #### weight 304 | 305 | > _(readonly)_ 306 | > The font weight. 307 | > **Type:** [`FontWeight`](#type-fontweight) 308 | 309 | #### italic 310 | 311 | > _(readonly)_ 312 | > Whether the font is italic. 313 | > **Type:** `boolean` 314 | 315 | #### style 316 | 317 | > _(readonly)_ 318 | > The font style. 319 | > **Type:** `'normal' | 'italic'` 320 | 321 | #### variant 322 | 323 | > _(readonly)_ 324 | > The font variant. 325 | > **Type:** `string` 326 | 327 | ## (class) FontFamily 328 | 329 | ### Methods 330 | 331 | #### toString 332 | 333 | Get the font family's name. 334 | 335 | ```js 336 | .toString() 337 | ``` 338 | 339 | **Returns** 340 | 341 | > The font family's name. 342 | > **Type:** `string` 343 | 344 | #### getDefaultVariant 345 | 346 | Gets the default variant for the font family. 347 | 348 | ```js 349 | .getDefaultVariant() 350 | ``` 351 | 352 | **Returns** 353 | 354 | > The font family's default variant. 355 | > **Type:** `string` 356 | 357 | # Types 358 | 359 | ## (interface) PickerConfig 360 | 361 | A FontPicker configuration object. 362 | 363 | > **language** 364 | > Language to use for the interface. 365 | > **Type:** [`Language`](#type-language) 366 | 367 | > **container** 368 | > Container to place the modal in. 369 | > **Type:** [`HTMLElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement) 370 | 371 | > **previewText** 372 | > Override for the font preview translation. 373 | > **Type:** `string | null` 374 | 375 | > **font** 376 | > Default font family and variant. 377 | > **Type:** `string | null` 378 | 379 | > **verbose** 380 | > Show the full variant name on the picker button? 381 | > **Type:** `boolean` 382 | 383 | > **variants** 384 | > Allow the user to choose font variants? 385 | > **Type:** `boolean` 386 | 387 | > **favourites** 388 | > Names of default favourite font families. 389 | > **Type:** `string[]` 390 | 391 | > **saveFavourites** 392 | > Save favourites to localStorage? 393 | > **Type:** `boolean` 394 | 395 | > **storageKey** 396 | > Key to use for accessing localStorage. 397 | > **Type:** `string` 398 | 399 | > **stateKey** 400 | > Key to use when saving the state of applied filters. 401 | > Filters will persist between pickers with the same _stateKey_! 402 | > **Type:** `string` 403 | 404 | > **defaultSubset** 405 | > Default subset filter. 406 | > **Type:** [`Subset`](#type-subset) 407 | 408 | > **defaultCategories** 409 | > Default category filters. 410 | > **Type:** [`Category[]`](#type-category) 411 | 412 | > **defaultWidth** 413 | > Default metric filter width. 414 | > **Type:** [`Metric`](#type-metric) 415 | 416 | > **defaultThickness** 417 | > Default metric filter thickness. 418 | > **Type:** [`Metric`](#type-metric) 419 | 420 | > **defaultComplexity** 421 | > Default metric filter complexity. 422 | > **Type:** [`Metric`](#type-metric) 423 | 424 | > **defaultCurvature** 425 | > Default metric filter curvature. 426 | > **Type:** [`Metric`](#type-metric) 427 | 428 | > **sortBy** 429 | > Default sorting criterion. 430 | > **Type:** [`Criterion`](#type-criterion) 431 | 432 | > **sortReverse** 433 | > Reverse search order? 434 | > **Type:** `boolean` 435 | 436 | > **googleFonts** 437 | > Whitelist for Google Fonts. 438 | > **Type:** `string[] | null` 439 | 440 | > **systemFonts** 441 | > Whitelist for System Fonts. 442 | > **Type:** `string[] | null` 443 | 444 | > **extraFonts** 445 | > Extra fonts to also include in the picker. 446 | > **Type:** [`FamilyProps[]`](#interface-familyprops) 447 | 448 | > **showCancelButton** 449 | > Show the cancel button? 450 | > **Type:** `boolean` 451 | 452 | > **showClearButton** 453 | > Show the clear button? 454 | > **Type:** `boolean` 455 | 456 | ## (interface) FamilyProps 457 | 458 | An object representing a Font Family. 459 | 460 | > **name** 461 | > Font family name. 462 | > **Type:** `string` 463 | 464 | > **variants** 465 | > Variants supported by the font family. 466 | > **Type:** `string[]` 467 | 468 | > **category** _(optional)_ 469 | > Font family category. 470 | > **Type:** [`Category`](#category) 471 | 472 | > **subsets** _(optional)_ 473 | > Subsets supported by the font family. 474 | > **Type:** [`Subset[]`](#subset) 475 | 476 | > **popularity** _(optional)_ 477 | > Font family popularity index. 478 | > **Type:** `number` 479 | 480 | > **metrics** _(optional)_ 481 | > Font family metrics properties. 482 | > **Type:** `Object` 483 | > 484 | > > **width** 485 | > > Font family width metric value. 486 | > > **Type:** `number` 487 | > 488 | > > **thickness** 489 | > > Font family thickness metric value. 490 | > > **Type:** `number` 491 | > 492 | > > **complexity** 493 | > > Font family complexity metric value. 494 | > > **Type:** `number` 495 | > 496 | > > **curvature** 497 | > > Font family curvature metric value. 498 | > > **Type:** `number` 499 | 500 | > **url** _(optional)_ 501 | > URL to load the font family from. 502 | > **Type:** `string` 503 | 504 | ## (type) Language 505 | 506 | A translation language key. 507 | 508 | `'en' | 'nl' | 'de' | 'es' | 'fr'` 509 | 510 | ## (type) Subset 511 | 512 | A subset to filter fonts by. 513 | 514 | `'all' | 'arabic' | 'bengali' | 'chinese-hongkong' | 'chinese-simplified' | 'chinese-traditional' | 'cyrillic' | 'cyrillic-ext' | 'devanagari' | 'greek' | 'greek-ext' | 'gujarati' | 'gurmukhi' | 'hebrew' | 'japanese' | 'kannada' | 'khmer' | 'korean' | 'latin' | 'latin-ext' | 'malayalam' | 'myanmar' | 'oriya' | 'sinhala' | 'tamil' | 'telugu' | 'thai' | 'tibetan' | 'vietnamese'` 515 | 516 | ## (type) Category 517 | 518 | A category to filter fonts by. 519 | 520 | `'serif' | 'sans-serif' | 'display' | 'handwriting' | 'monospace'` 521 | 522 | ## (type) Metric 523 | 524 | A metrics options key to filter fonts by. 525 | 526 | `'all' | '0!' | '1!' | '2!' | '3!' | '4!'` 527 | 528 | ## (type) Criterion 529 | 530 | A criterion to sort fonts by. 531 | 532 | `'name' | 'popularity' | 'width' | 'thickness' | 'complexity' | 'curvature'` 533 | 534 | ## (type) FontWeight 535 | 536 | A font weight. 537 | 538 | `100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900` 539 | -------------------------------------------------------------------------------- /src/data/translations.ts: -------------------------------------------------------------------------------- 1 | import type { Translations } from '../types/translations' 2 | 3 | export const translations: Translations = { 4 | en: { 5 | selectFont: 'Select a font', 6 | sampleText: 'The quick brown fox jumps over the lazy dog.', 7 | pickHint: 'Pick a font...', 8 | 9 | filters: 'Filters', 10 | search: 'Search', 11 | 12 | subsets: { 13 | all: '(All Subsets)', 14 | arabic: 'Arabic', 15 | bengali: 'Bengali', 16 | 'chinese-hongkong': 'Chinese (Hong Kong)', 17 | 'chinese-simplified': 'Chinese (Simplified)', 18 | 'chinese-traditional': 'Chinese (Traditional)', 19 | cyrillic: 'Cyrillic', 20 | 'cyrillic-ext': 'Cyrillic Extended', 21 | devanagari: 'Devanagari', 22 | greek: 'Greek', 23 | 'greek-ext': 'Greek Extended', 24 | gujarati: 'Gujarati', 25 | gurmukhi: 'Gurmukhi', 26 | hebrew: 'Hebrew', 27 | japanese: 'Japanese', 28 | kannada: 'Kannada', 29 | khmer: 'Khmer', 30 | korean: 'Korean', 31 | latin: 'Latin', 32 | 'latin-ext': 'Latin Extended', 33 | malayalam: 'Malayalam', 34 | myanmar: 'Myanmar', 35 | oriya: 'Oriya', 36 | sinhala: 'Sinhala', 37 | tamil: 'Tamil', 38 | telugu: 'Telugu', 39 | thai: 'Thai', 40 | tibetan: 'Tibetan', 41 | vietnamese: 'Vietnamese', 42 | }, 43 | 44 | categories: { 45 | serif: 'Serif', 46 | 'sans-serif': 'Sans-serif', 47 | display: 'Display', 48 | handwriting: 'Handwriting', 49 | monospace: 'Monospace', 50 | }, 51 | 52 | metrics: 'Metrics', 53 | widths: { 54 | all: '(All Widths)', 55 | '0!': 'Very narrow', 56 | '1!': 'Narrow', 57 | '2!': 'Medium width', 58 | '3!': 'Wide', 59 | '4!': 'Very wide', 60 | }, 61 | thicknesses: { 62 | all: '(All Thicknesses)', 63 | '0!': 'Very thin', 64 | '1!': 'Thin', 65 | '2!': 'Medium thickness', 66 | '3!': 'Thick', 67 | '4!': 'Very thick', 68 | }, 69 | complexities: { 70 | all: '(All Complexities)', 71 | '0!': 'Very Simple', 72 | '1!': 'Simple', 73 | '2!': 'Medium complexity', 74 | '3!': 'Complex', 75 | '4!': 'Very complex', 76 | }, 77 | curvatures: { 78 | all: '(All Curvatures)', 79 | '0!': 'Very straight', 80 | '1!': 'Straight', 81 | '2!': 'Medium curvature', 82 | '3!': 'Curvy', 83 | '4!': 'Very Curvy', 84 | }, 85 | 86 | sort: 'Sort', 87 | sorts: { 88 | name: 'Sort by Name', 89 | popularity: 'Sort by Popularity', 90 | width: 'Sort by Width', 91 | thickness: 'Sort by Thickness', 92 | complexity: 'Sort by Complexity', 93 | curvature: 'Sort by Curvature', 94 | }, 95 | 96 | clearFilters: 'Clear filters', 97 | 98 | clear: 'Clear', 99 | cancel: 'Cancel', 100 | select: 'Select', 101 | }, 102 | nl: { 103 | selectFont: 'Selecteer een lettertype', 104 | sampleText: 'Wazig tv-filmpje rond chique skybox.', 105 | pickHint: 'Kies een lettertype...', 106 | 107 | filters: 'Filters', 108 | search: 'Zoeken', 109 | 110 | subsets: { 111 | all: '(Alle subsets)', 112 | arabic: 'Arabisch', 113 | bengali: 'Bengaals', 114 | 'chinese-hongkong': 'Chinees (Hongkong)', 115 | 'chinese-simplified': 'Chinees (Vereenvoudigd)', 116 | 'chinese-traditional': 'Chinees (Traditioneel)', 117 | cyrillic: 'Cyrillisch', 118 | 'cyrillic-ext': 'Cyrillisch Uitgebreid', 119 | devanagari: 'Devanagari', 120 | greek: 'Grieks', 121 | 'greek-ext': 'Grieks Uitgebreid', 122 | gujarati: 'Gujarati', 123 | gurmukhi: 'Gurmukhi', 124 | hebrew: 'Hebreeuws', 125 | japanese: 'Japans', 126 | kannada: 'Kannada', 127 | khmer: 'Khmer', 128 | korean: 'Koreaans', 129 | latin: 'Latijn', 130 | 'latin-ext': 'Latijn Uitgebreid', 131 | malayalam: 'Malayalam', 132 | myanmar: 'Myanmar', 133 | oriya: 'Oriya', 134 | sinhala: 'Sinhala', 135 | tamil: 'Tamil', 136 | telugu: 'Telugu', 137 | thai: 'Thai', 138 | tibetan: 'Tibetaans', 139 | vietnamese: 'Vietnamees', 140 | }, 141 | 142 | categories: { 143 | serif: 'Schreef', 144 | 'sans-serif': 'Schreefloos', 145 | display: 'Display', 146 | handwriting: 'Handschrift', 147 | monospace: 'Monospace', 148 | }, 149 | 150 | metrics: 'Metriek', 151 | widths: { 152 | all: '(Alle breedtes)', 153 | '0!': 'Zeer smal', 154 | '1!': 'Smal', 155 | '2!': 'Normale breedte', 156 | '3!': 'Breed', 157 | '4!': 'Zeer breed', 158 | }, 159 | thicknesses: { 160 | all: '(Alle diktes)', 161 | '0!': 'Zeer dun', 162 | '1!': 'Dun', 163 | '2!': 'Normale dikte', 164 | '3!': 'Dik', 165 | '4!': 'Zeer dik', 166 | }, 167 | complexities: { 168 | all: '(Alle complexiteiten)', 169 | '0!': 'Zeer eenvoudig', 170 | '1!': 'Eenvoudig', 171 | '2!': 'Normale complexiteit', 172 | '3!': 'Complex', 173 | '4!': 'Zeer complex', 174 | }, 175 | curvatures: { 176 | all: '(Alle krommingen)', 177 | '0!': 'Zeer recht', 178 | '1!': 'Recht', 179 | '2!': 'Normale kromming', 180 | '3!': 'Gebogen', 181 | '4!': 'Zeer gebogen', 182 | }, 183 | 184 | sort: 'Sorteren', 185 | sorts: { 186 | name: 'Sorteer op naam', 187 | popularity: 'Sorteer op populariteit', 188 | width: 'Sorteer op breedte', 189 | thickness: 'Sorteer op dikte', 190 | complexity: 'Sorteer op complexiteit', 191 | curvature: 'Sorteer op kromming', 192 | }, 193 | 194 | clearFilters: 'Filters wissen', 195 | 196 | clear: 'Wissen', 197 | cancel: 'Annuleren', 198 | select: 'Selecteren', 199 | }, 200 | de: { 201 | selectFont: 'Schriftart auswählen', 202 | sampleText: 'Falsches Üben von Xylophonmusik quält jeden größeren Zwerg.', 203 | pickHint: 'Wähle eine Schriftart...', 204 | 205 | filters: 'Filter', 206 | search: 'Suche', 207 | 208 | subsets: { 209 | all: '(Alle Untergruppen)', 210 | arabic: 'Arabisch', 211 | bengali: 'Bengalisch', 212 | 'chinese-hongkong': 'Chinesisch (Hongkong)', 213 | 'chinese-simplified': 'Chinesisch (Vereinfacht)', 214 | 'chinese-traditional': 'Chinesisch (Traditionell)', 215 | cyrillic: 'Kyrillisch', 216 | 'cyrillic-ext': 'Kyrillisch Erweitert', 217 | devanagari: 'Devanagari', 218 | greek: 'Griechisch', 219 | 'greek-ext': 'Griechisch Erweitert', 220 | gujarati: 'Gujarati', 221 | gurmukhi: 'Gurmukhi', 222 | hebrew: 'Hebräisch', 223 | japanese: 'Japanisch', 224 | kannada: 'Kannada', 225 | khmer: 'Khmer', 226 | korean: 'Koreanisch', 227 | latin: 'Lateinisch', 228 | 'latin-ext': 'Lateinisch Erweitert', 229 | malayalam: 'Malayalam', 230 | myanmar: 'Myanmar', 231 | oriya: 'Oriya', 232 | sinhala: 'Singhalesisch', 233 | tamil: 'Tamil', 234 | telugu: 'Telugu', 235 | thai: 'Thailändisch', 236 | tibetan: 'Tibetisch', 237 | vietnamese: 'Vietnamesisch', 238 | }, 239 | 240 | categories: { 241 | serif: 'Serifen', 242 | 'sans-serif': 'Serifenlos', 243 | display: 'Display', 244 | handwriting: 'Handschrift', 245 | monospace: 'Monospace', 246 | }, 247 | 248 | metrics: 'Metriken', 249 | widths: { 250 | all: '(Alle Breiten)', 251 | '0!': 'Sehr schmal', 252 | '1!': 'Schmal', 253 | '2!': 'Mittlere Breite', 254 | '3!': 'Breit', 255 | '4!': 'Sehr breit', 256 | }, 257 | thicknesses: { 258 | all: '(Alle Strichstärken)', 259 | '0!': 'Sehr dünn', 260 | '1!': 'Dünn', 261 | '2!': 'Mittlere Stärke', 262 | '3!': 'Dick', 263 | '4!': 'Sehr dick', 264 | }, 265 | complexities: { 266 | all: '(Alle Komplexitäten)', 267 | '0!': 'Sehr einfach', 268 | '1!': 'Einfach', 269 | '2!': 'Mittlere Komplexität', 270 | '3!': 'Komplex', 271 | '4!': 'Sehr komplex', 272 | }, 273 | curvatures: { 274 | all: '(Alle Krümmungen)', 275 | '0!': 'Sehr gerade', 276 | '1!': 'Gerade', 277 | '2!': 'Mittlere Krümmung', 278 | '3!': 'Geschwungen', 279 | '4!': 'Sehr geschwungen', 280 | }, 281 | 282 | sort: 'Sortieren', 283 | sorts: { 284 | name: 'Nach Name sortieren', 285 | popularity: 'Nach Beliebtheit sortieren', 286 | width: 'Nach Breite sortieren', 287 | thickness: 'Nach Strichstärke sortieren', 288 | complexity: 'Nach Komplexität sortieren', 289 | curvature: 'Nach Krümmung sortieren', 290 | }, 291 | 292 | clearFilters: 'Filter löschen', 293 | 294 | clear: 'Löschen', 295 | cancel: 'Abbrechen', 296 | select: 'Auswählen', 297 | }, 298 | es: { 299 | selectFont: 'Selecciona una fuente', 300 | sampleText: 'El veloz murciélago hindú comía feliz cardillo y kiwi.', 301 | pickHint: 'Elige una fuente...', 302 | 303 | filters: 'Filtros', 304 | search: 'Buscar', 305 | 306 | subsets: { 307 | all: '(Todos los subconjuntos)', 308 | arabic: 'Árabe', 309 | bengali: 'Bengalí', 310 | 'chinese-hongkong': 'Chino (Hong Kong)', 311 | 'chinese-simplified': 'Chino (Simplificado)', 312 | 'chinese-traditional': 'Chino (Tradicional)', 313 | cyrillic: 'Cirílico', 314 | 'cyrillic-ext': 'Cirílico extendido', 315 | devanagari: 'Devanagari', 316 | greek: 'Griego', 317 | 'greek-ext': 'Griego extendido', 318 | gujarati: 'Gujarati', 319 | gurmukhi: 'Gurmukhi', 320 | hebrew: 'Hebreo', 321 | japanese: 'Japonés', 322 | kannada: 'Canarés', 323 | khmer: 'Jemer', 324 | korean: 'Coreano', 325 | latin: 'Latín', 326 | 'latin-ext': 'Latín extendido', 327 | malayalam: 'Malayalam', 328 | myanmar: 'Birmano', 329 | oriya: 'Oriya', 330 | sinhala: 'Cingalés', 331 | tamil: 'Tamil', 332 | telugu: 'Telugu', 333 | thai: 'Tailandés', 334 | tibetan: 'Tibetano', 335 | vietnamese: 'Vietnamita', 336 | }, 337 | 338 | categories: { 339 | serif: 'Serifa', 340 | 'sans-serif': 'Sans serif', 341 | display: 'Decorativo', 342 | handwriting: 'Manuscrita', 343 | monospace: 'Monoespaciada', 344 | }, 345 | 346 | metrics: 'Métricas', 347 | widths: { 348 | all: '(Todas las anchuras)', 349 | '0!': 'Muy estrecha', 350 | '1!': 'Estrecha', 351 | '2!': 'Anchura media', 352 | '3!': 'Ancha', 353 | '4!': 'Muy ancha', 354 | }, 355 | thicknesses: { 356 | all: '(Todos los grosores)', 357 | '0!': 'Muy delgada', 358 | '1!': 'Delgada', 359 | '2!': 'Grosor medio', 360 | '3!': 'Gruesa', 361 | '4!': 'Muy gruesa', 362 | }, 363 | complexities: { 364 | all: '(Todas las complejidades)', 365 | '0!': 'Muy simple', 366 | '1!': 'Simple', 367 | '2!': 'Complejidad media', 368 | '3!': 'Compleja', 369 | '4!': 'Muy compleja', 370 | }, 371 | curvatures: { 372 | all: '(Todas las curvaturas)', 373 | '0!': 'Muy recta', 374 | '1!': 'Recta', 375 | '2!': 'Curvatura media', 376 | '3!': 'Curvada', 377 | '4!': 'Muy curvada', 378 | }, 379 | 380 | sort: 'Ordenar', 381 | sorts: { 382 | name: 'Ordenar por nombre', 383 | popularity: 'Ordenar por popularidad', 384 | width: 'Ordenar por anchura', 385 | thickness: 'Ordenar por grosor', 386 | complexity: 'Ordenar por complejidad', 387 | curvature: 'Ordenar por curvatura', 388 | }, 389 | 390 | clearFilters: 'Borrar filtros', 391 | 392 | clear: 'Borrar', 393 | cancel: 'Cancelar', 394 | select: 'Seleccionar', 395 | }, 396 | fr: { 397 | selectFont: 'Sélectionnez une police', 398 | sampleText: 'Portez ce vieux whisky au juge blond qui fume.', 399 | pickHint: 'Choisissez une police...', 400 | 401 | filters: 'Filtres', 402 | search: 'Rechercher', 403 | 404 | subsets: { 405 | all: '(Tous les sous-ensembles)', 406 | arabic: 'Arabe', 407 | bengali: 'Bengali', 408 | 'chinese-hongkong': 'Chinois (Hong Kong)', 409 | 'chinese-simplified': 'Chinois (simplifié)', 410 | 'chinese-traditional': 'Chinois (traditionnel)', 411 | cyrillic: 'Cyrillique', 412 | 'cyrillic-ext': 'Cyrillique étendu', 413 | devanagari: 'Devanagari', 414 | greek: 'Grec', 415 | 'greek-ext': 'Grec étendu', 416 | gujarati: 'Gujarati', 417 | gurmukhi: 'Gurmukhi', 418 | hebrew: 'Hébreu', 419 | japanese: 'Japonais', 420 | kannada: 'Kannada', 421 | khmer: 'Khmer', 422 | korean: 'Coréen', 423 | latin: 'Latin', 424 | 'latin-ext': 'Latin étendu', 425 | malayalam: 'Malayalam', 426 | myanmar: 'Myanmar', 427 | oriya: 'Oriya', 428 | sinhala: 'Singhalais', 429 | tamil: 'Tamoul', 430 | telugu: 'Telugu', 431 | thai: 'Thaï', 432 | tibetan: 'Tibétain', 433 | vietnamese: 'Vietnamien', 434 | }, 435 | 436 | categories: { 437 | serif: 'Empattement', 438 | 'sans-serif': 'Sans empattement', 439 | display: 'Décoratives', 440 | handwriting: 'Écriture manuscrite', 441 | monospace: 'Monospace', 442 | }, 443 | 444 | metrics: 'Métriques', 445 | widths: { 446 | all: '(Toutes les largeurs)', 447 | '0!': 'Très étroit', 448 | '1!': 'Étroit', 449 | '2!': 'Largeur moyenne', 450 | '3!': 'Large', 451 | '4!': 'Très large', 452 | }, 453 | thicknesses: { 454 | all: '(Toutes les épaisseurs)', 455 | '0!': 'Très fin', 456 | '1!': 'Fin', 457 | '2!': 'Épaisseur moyenne', 458 | '3!': 'Épais', 459 | '4!': 'Très épais', 460 | }, 461 | complexities: { 462 | all: '(Toutes les complexités)', 463 | '0!': 'Très simple', 464 | '1!': 'Simple', 465 | '2!': 'Complexité moyenne', 466 | '3!': 'Complexe', 467 | '4!': 'Très complexe', 468 | }, 469 | curvatures: { 470 | all: '(Toutes les courbures)', 471 | '0!': 'Très droit', 472 | '1!': 'Droit', 473 | '2!': 'Courbure moyenne', 474 | '3!': 'Courbé', 475 | '4!': 'Très courbé', 476 | }, 477 | 478 | sort: 'Trier', 479 | sorts: { 480 | name: 'Trier par nom', 481 | popularity: 'Trier par popularité', 482 | width: 'Trier par largeur', 483 | thickness: 'Trier par épaisseur', 484 | complexity: 'Trier par complexité', 485 | curvature: 'Trier par courbure', 486 | }, 487 | 488 | clearFilters: 'Effacer filtres', 489 | 490 | clear: 'Effacer', 491 | cancel: 'Annuler', 492 | select: 'Sélectionner', 493 | }, 494 | } 495 | -------------------------------------------------------------------------------- /dist/fontpicker.css: -------------------------------------------------------------------------------- 1 | /* Theming */ 2 | [data-bs-theme='dark'], 3 | [data-fp-theme='dark'] { 4 | --fp-select-toggle-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e") !important; 5 | 6 | --fp-body-bg: #212529 !important; 7 | --fp-body-color: #dee2e6 !important; 8 | --fp-body-bg-rgb: 33, 37, 41 !important; 9 | 10 | --fp-border-color: #495057 !important; 11 | --fp-border-color-rgb: 73, 80, 87 !important; 12 | --fp-border-color-translucent: rgba(255, 255, 255, 0.15) !important; 13 | --fp-tertiary-color: rgba(222, 226, 230, 0.5) !important; 14 | } 15 | 16 | :root, 17 | [data-bs-theme='light'], 18 | [data-fp-theme='light'] { 19 | --fp-select-toggle-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e"); 20 | 21 | --fp-body-bg: #fff; 22 | --fp-body-color: #212529; 23 | --fp-body-bg-rgb: 255, 255, 255; 24 | 25 | --fp-border-color: #dee2e6; 26 | --fp-border-color-rgb: 222, 226, 230; 27 | --fp-border-color-translucent: rgba(0, 0, 0, 0.175); 28 | --fp-tertiary-color: rgba(33, 37, 41, 0.5); 29 | } 30 | 31 | :root { 32 | --fp-dark: #212529; 33 | --fp-light: #fff; 34 | 35 | --fp-primary: #0d6efd; 36 | --fp-secondary: #ff8239; 37 | --fp-hover-color: #0b5ed7; 38 | --fp-ring-color: #86b7fe; 39 | --fp-ring-shadow: rgba(13, 110, 253, 0.25) 0 0 0 0.25rem; 40 | --fp-danger-rgb: 220, 53, 69; 41 | 42 | --fp-border-radius-sm: 0.25rem; 43 | --fp-border-radius: 0.375rem; 44 | --fp-border-radius-lg: 0.5rem; 45 | 46 | --fp-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); 47 | --fp-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); 48 | } 49 | 50 | /* Global */ 51 | .fpb__input, 52 | .fpb__input *, 53 | .fpb__input ::before, 54 | .fpb__input ::after, 55 | .fpb__modal, 56 | .fpb__modal *, 57 | .fpb__modal ::before, 58 | .fpb__modal ::after { 59 | box-sizing: border-box; 60 | } 61 | 62 | /* Default component styling */ 63 | .fpb__input, 64 | .fpb__modal { 65 | color: var(--fp-body-color); 66 | font-family: 67 | system-ui, 68 | -apple-system, 69 | 'Segoe UI', 70 | Roboto, 71 | 'Helvetica Neue', 72 | 'Noto Sans', 73 | 'Liberation Sans', 74 | Arial, 75 | sans-serif, 76 | 'Apple Color Emoji', 77 | 'Segoe UI Emoji', 78 | 'Segoe UI Symbol', 79 | 'Noto Color Emoji'; 80 | } 81 | 82 | /* Inputs */ 83 | .fpb__input { 84 | display: block; 85 | 86 | font-size: 1rem; 87 | font-weight: 400; 88 | line-height: 1.5; 89 | color: var(--fp-body-color); 90 | 91 | appearance: none; 92 | background-color: var(--fp-body-bg); 93 | 94 | padding: 0.375rem 0.75rem; 95 | border: 1px solid var(--fp-border-color); 96 | border-radius: var(--fp-border-radius); 97 | 98 | transition: 99 | border-color 0.15s ease-in-out, 100 | box-shadow 0.15s ease-in-out; 101 | } 102 | .fpb__input:hover { 103 | background-color: var(--fp-body-bg) !important; 104 | border: 1px solid var(--fp-border-color) !important; 105 | } 106 | .fpb__input:focus { 107 | outline: 0; 108 | border-color: var(--fp-ring-color); 109 | box-shadow: var(--fp-ring-shadow); 110 | } 111 | .fpb__input:disabled { 112 | pointer-events: none; 113 | cursor: not-allowed; 114 | opacity: 0.75; 115 | } 116 | .fpb__dropdown { 117 | margin: unset; /* CSS reset */ 118 | padding: 0.375rem 2.25rem 0.375rem 0.75rem; 119 | background-image: var(--fp-select-toggle-img); 120 | background-repeat: no-repeat; 121 | background-position: right 0.75rem center; 122 | background-size: 16px 12px; 123 | } 124 | 125 | /* Modals */ 126 | .fpb__modal { 127 | position: fixed; 128 | max-width: 32rem; 129 | width: calc(100vw - 1rem); 130 | height: calc(100vh - 3rem); 131 | top: 50%; 132 | left: 50%; 133 | 134 | border: 1px solid var(--fp-border-color-translucent); 135 | border-radius: var(--fp-border-radius-lg); 136 | background-color: var(--fp-body-bg); 137 | box-shadow: var(--fp-box-shadow); 138 | 139 | display: flex; 140 | flex-direction: column; 141 | overflow: hidden; 142 | 143 | opacity: 0; 144 | pointer-events: none; 145 | transform: translate(-50%, -51%); 146 | 147 | transition: 148 | opacity 0.5s, 149 | transform 0.5s; 150 | 151 | z-index: 1055; 152 | } 153 | .fpb__modal.fpb__open { 154 | opacity: 1; 155 | pointer-events: all; 156 | transform: translate(-50%, -50%); 157 | } 158 | .fpb__backdrop { 159 | position: fixed; 160 | width: 100vw; 161 | height: 100vh; 162 | inset: 0; 163 | z-index: 1054; 164 | 165 | background-color: #000; 166 | opacity: 0; 167 | pointer-events: none; 168 | 169 | transition: opacity 0.5s; 170 | } 171 | .fpb__modal.fpb__open + .fpb__backdrop { 172 | opacity: 0.5; 173 | pointer-events: all; 174 | } 175 | 176 | .fpb__modal-header { 177 | display: flex; 178 | justify-content: space-between; 179 | align-items: center; 180 | padding: 1rem; 181 | } 182 | .fp__modal-title { 183 | margin: 0; 184 | font-size: 1.25rem; 185 | font-weight: 700; 186 | font-family: inherit; 187 | } 188 | 189 | .fpb__modal button:hover { 190 | background-color: transparent !important; /* CSS reset */ 191 | } 192 | 193 | .fpb__modal-footer { 194 | display: flex; 195 | align-items: center; 196 | padding: 1rem; 197 | gap: 1rem; 198 | border-top: 1px solid var(--fp-border-color); 199 | } 200 | .fpb__modal .fpb__input { 201 | width: 100%; 202 | } 203 | 204 | .fpb__btn-close { 205 | display: grid; 206 | place-items: center; 207 | 208 | color: var(--fp-tertiary-color); 209 | font-size: 2rem; 210 | width: 0.75em; 211 | height: 0.75em; 212 | padding: 0; 213 | 214 | line-height: 0; 215 | appearance: none; 216 | background: none; 217 | border: none; 218 | border-radius: var(--fp-border-radius); 219 | } 220 | .fpb__btn-close:hover { 221 | color: var(--fp-body-color); 222 | } 223 | .fpb__btn-close:focus-visible { 224 | outline: 0; 225 | box-shadow: var(--fp-ring-shadow); 226 | } 227 | 228 | /* Accordion */ 229 | .fpb__accordion-item { 230 | margin-top: -1px; 231 | } 232 | .fpb__accordion-toggle { 233 | --fp-accordion-toggle-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); 234 | 235 | display: flex; 236 | width: 100%; 237 | align-items: center; 238 | 239 | color: var(--fp-body-color); 240 | font-size: 1rem; 241 | text-align: start; 242 | 243 | appearance: none; 244 | background: none; 245 | border: none; 246 | border-radius: 0; 247 | border-block: 1px solid var(--fp-border-color) !important; 248 | 249 | padding: 0.5rem 1rem; 250 | margin: 0; 251 | 252 | transition: 253 | border-color 0.15s, 254 | box-shadow 0.15s; 255 | } 256 | 257 | .fpb__accordion-toggle::after { 258 | content: ''; 259 | margin-left: auto; 260 | width: 1.25rem; 261 | height: 1.25rem; 262 | background-image: var(--fp-accordion-toggle-img); 263 | background-repeat: no-repeat; 264 | background-size: 100% 100%; 265 | transition: transform 0.2s; 266 | } 267 | .fpb__accordion-item.fpb__open > .fpb__accordion-toggle::after { 268 | transform: rotate(-180deg); 269 | } 270 | .fpb__accordion-content { 271 | --fpb-height: 0; 272 | max-height: 0; 273 | overflow: hidden; 274 | visibility: hidden; 275 | transition: max-height 0.2s; 276 | } 277 | .fpb__accordion-content > * { 278 | padding: 0.5rem 1rem; 279 | } 280 | .fpb__accordion-item.fpb__open .fpb__accordion-content { 281 | --fpb-height: fit-content; 282 | max-height: var(--fpb-height); 283 | visibility: visible; 284 | } 285 | .fpb__accordion-toggle:focus { 286 | outline: 0; 287 | box-shadow: var(--fp-ring-shadow); 288 | border-color: var(--fp-ring-color); 289 | } 290 | 291 | /* Button */ 292 | [role='button'], 293 | button { 294 | cursor: pointer; 295 | } 296 | 297 | .fpb__btn { 298 | --fpb-variant: var(--fp-primary); 299 | 300 | cursor: pointer; 301 | font-size: 1rem; 302 | padding: 0.375rem 0.75rem; 303 | line-height: 1.5; 304 | color: var(--fp-light); 305 | text-align: center; 306 | user-select: none; 307 | border: 1px solid var(--fpb-variant); 308 | background-color: var(--fpb-variant); 309 | border-radius: var(--fp-border-radius); 310 | 311 | transition: 312 | color 0.15s, 313 | background-color 0.15s, 314 | border-color 0.15s, 315 | box-shadow 0.15s, 316 | opacity 0.15s; 317 | } 318 | .fpb__btn:hover { 319 | opacity: 0.8; 320 | } 321 | .fpb__btn-small { 322 | font-size: 0.75rem; 323 | padding: 0.1rem 0.5rem; 324 | } 325 | .fpb__btn-pill { 326 | border-radius: 9999px; 327 | } 328 | button.fpb__btn-pill:hover { 329 | /* CSS reset */ 330 | background-color: var(--fpb-variant) !important; 331 | border-color: var(--fpb-variant) !important; 332 | } 333 | .fpb__btn-secondary { 334 | --fpb-variant: var(--fp-secondary); 335 | } 336 | .fpb__btn-link { 337 | color: var(--fpb-variant); 338 | background: none; 339 | border: none; 340 | } 341 | .fpb__btn-link::after { 342 | content: ''; 343 | width: 0px; 344 | height: 1px; 345 | display: block; 346 | background: currentColor; 347 | opacity: 0; 348 | transition: 349 | width 0.2s, 350 | opacity 0.2s; 351 | } 352 | .fpb__btn-link:hover { 353 | /* CSS reset */ 354 | background: inherit !important; 355 | } 356 | .fpb__btn-link:hover::after { 357 | width: 100%; 358 | opacity: 1; 359 | } 360 | 361 | .fpb__hidden-input { 362 | position: absolute !important; 363 | clip: rect(0, 0, 0, 0); 364 | pointer-events: none; 365 | } 366 | .fpb__btn-toggle { 367 | white-space: nowrap; 368 | color: var(--fpb-variant); 369 | background-color: transparent; 370 | } 371 | .fpb__btn-flip, 372 | .fpb__btn-toggle.fpb__active, 373 | input:checked + .fpb__btn-toggle { 374 | background-color: var(--fpb-variant); 375 | border-color: var(--fpb-variant); 376 | color: var(--fp-light); 377 | } 378 | .fpb__btn-flip > * { 379 | transition: transform 0.25s ease-in-out; 380 | } 381 | input:checked + .fpb__btn-flip > *, 382 | .fpb__btn-flip.fpb__active > * { 383 | transform: scaleY(-1); 384 | } 385 | 386 | .fpb__btn:focus-visible, 387 | input:focus-visible + .fpb__btn-toggle { 388 | outline: 0; 389 | opacity: 0.8; 390 | box-shadow: var(--fp-ring-shadow); 391 | } 392 | 393 | .fpb__btn:disabled, 394 | input:disabled + .fpb__btn-toggle { 395 | opacity: 0.5; 396 | cursor: not-allowed; 397 | } 398 | 399 | /* Input groups */ 400 | .fpb__input-group { 401 | display: flex; 402 | } 403 | .fpb__input-group > *:first-child { 404 | border-top-right-radius: 0; 405 | border-bottom-right-radius: 0; 406 | } 407 | .fpb__input-group > *:last-child { 408 | border-top-left-radius: 0; 409 | border-bottom-left-radius: 0; 410 | } 411 | 412 | /* Grid utility */ 413 | .fpb__grid-2 { 414 | display: grid; 415 | grid-template-columns: 1fr 1fr; 416 | gap: 0.5rem; 417 | } 418 | .fpb__span-2 { 419 | grid-column: span 2; 420 | } 421 | 422 | .fpb__grow { 423 | flex-grow: 1; 424 | } 425 | 426 | .fpb__hlist { 427 | display: flex; 428 | overflow-x: auto; 429 | gap: 0.5rem; 430 | } 431 | 432 | .fpb__hidden { 433 | display: none !important; 434 | } 435 | 436 | /* Text utility */ 437 | .fpb__has-icon, 438 | .fpb__has-icon * { 439 | vertical-align: middle; 440 | } 441 | .fpb__has-icon svg { 442 | margin-right: 0.25rem; 443 | } 444 | 445 | .fpb__primary { 446 | color: var(--fp-primary); 447 | } 448 | 449 | @media (max-width: 576px) { 450 | .fpb__modal { 451 | height: calc(100vh - 1rem); 452 | } 453 | .fpb__grid-2 { 454 | display: flex; 455 | flex-direction: column; 456 | } 457 | } 458 | /* Font picker button element */ 459 | .font-picker { 460 | text-align: start; 461 | overflow: hidden; 462 | white-space: nowrap; 463 | text-overflow: ellipsis; 464 | vertical-align: middle; 465 | } 466 | input.font-picker { 467 | caret-color: transparent; 468 | } 469 | 470 | /* Filters */ 471 | .fp__changed::after { 472 | content: '*'; 473 | color: var(--fp-secondary); 474 | } 475 | 476 | /* Font list */ 477 | #fp__fonts { 478 | display: flex; 479 | flex-direction: column; 480 | flex-grow: 1; 481 | overflow-y: scroll; 482 | 483 | padding: 0.25rem; 484 | margin-top: -1px; 485 | 486 | border-block: 1px solid var(--fp-border-color); 487 | } 488 | #fp__fonts:focus-visible { 489 | outline: 0; 490 | box-shadow: var(--fp-ring-shadow); 491 | border-color: var(--fp-ring-color); 492 | transition: 493 | border-color 0.15s, 494 | box-shadow 0.15s; 495 | } 496 | 497 | /* Font item */ 498 | .fp__font-item { 499 | display: flex; 500 | justify-content: space-between; 501 | align-items: center; 502 | 503 | padding: 0.25rem 1rem; 504 | min-height: 2rem; 505 | user-select: none; 506 | border-radius: 9999px; 507 | } 508 | .fp__font-item:hover { 509 | background: var(--fp-border-color); 510 | } 511 | .fp__font-item.fp__selected { 512 | color: var(--fp-light); 513 | background: var(--fp-primary); 514 | } 515 | 516 | .fp__font-family { 517 | font-size: 1rem; 518 | pointer-events: none; 519 | } 520 | 521 | /* Font heart */ 522 | .fp__heart { 523 | height: 1em; 524 | } 525 | .fp__heart svg { 526 | height: 1em; 527 | pointer-events: none; 528 | vertical-align: baseline; 529 | 530 | --fp-heart-color: var(--fp-border-color-rgb); 531 | 532 | fill: rgba(var(--fp-heart-color), 0.5); 533 | stroke: rgb(var(--fp-heart-color)); 534 | } 535 | .fp__heart:hover svg { 536 | fill: rgb(var(--fp-heart-color)); 537 | } 538 | 539 | .fp__font-item:hover .fp__heart svg, 540 | .fp__font-item.fp__selected .fp__heart svg { 541 | --fp-heart-color: var(--fp-body-bg-rgb); 542 | } 543 | .fp__font-item.fp__fav .fp__heart svg { 544 | --fp-heart-color: var(--fp-danger-rgb); 545 | } 546 | .fp__font-item.fp__fav.fp__selected .fp__heart svg { 547 | filter: drop-shadow(0px 0px 2px var(--fp-dark)); 548 | } 549 | 550 | /* Preview */ 551 | .fp__preview-container { 552 | padding: 0.25rem; 553 | } 554 | #fp__preview { 555 | white-space: nowrap; 556 | text-overflow: ellipsis; 557 | overflow: hidden; 558 | text-align: center; 559 | font-size: 1.25rem; 560 | line-height: 1.5; 561 | 562 | padding-inline: 0.75rem; 563 | border: 1px solid transparent; 564 | 565 | transition: 566 | border-color 0.15s ease-in-out, 567 | box-shadow 0.15s ease-in-out; 568 | } 569 | #fp__preview:focus { 570 | outline: 0; 571 | border-color: var(--fp-ring-color); 572 | box-shadow: var(--fp-ring-shadow); 573 | } 574 | 575 | /* Variants */ 576 | #fp__variants { 577 | display: flex; 578 | flex-wrap: wrap; 579 | justify-content: center; 580 | gap: 0.5rem; 581 | padding: 0.5rem 1rem; 582 | border-top: 1px solid var(--fp-border-color); 583 | } 584 | #fp__variants:has(#fp__italic:checked) { 585 | font-style: italic !important; 586 | } 587 | #fp__variants:empty { 588 | display: none; 589 | } 590 | -------------------------------------------------------------------------------- /src/core/PickerDialog.ts: -------------------------------------------------------------------------------- 1 | import dialogContent from '../templates/dialogContent.html?raw' 2 | 3 | import * as DOM from '../util/DOMUtil' 4 | import { Modal, Accordion } from '../util/FPB' 5 | 6 | import { familyFilter, familySort } from '../util/sortUtil' 7 | import { Font } from '../helpers/Font' 8 | import { FontLoader } from '../helpers/FontLoader' 9 | import { translations } from '../data/translations' 10 | 11 | import type { FontFamily } from '../helpers/FontFamily' 12 | import type { FontPicker } from './FontPicker' 13 | import type { Filters } from '../types/util' 14 | import type { FontWeight } from '../types/fonts' 15 | import type { Category, Criterion, Metric, Subset } from '../types/translations' 16 | import type { PickerConfig } from '../types/fontpicker' 17 | 18 | const configOverrides = new Map>() 19 | 20 | export class PickerDialog { 21 | private opened = false 22 | private picker: FontPicker 23 | private config: PickerConfig 24 | private override: Partial 25 | 26 | private observer: IntersectionObserver 27 | 28 | private selected: Font | null 29 | private hovered: Font | null = null 30 | 31 | private modal: Modal 32 | 33 | private $modal: HTMLDivElement 34 | private $modalBackdrop: HTMLButtonElement 35 | private $closeBtn: HTMLButtonElement 36 | 37 | private $search: HTMLInputElement 38 | private $subset: HTMLSelectElement 39 | private $categories: HTMLDivElement 40 | private $width: HTMLSelectElement 41 | private $thickness: HTMLSelectElement 42 | private $complexity: HTMLSelectElement 43 | private $curvature: HTMLSelectElement 44 | private $sort: HTMLSelectElement 45 | private $sortOrder: HTMLInputElement 46 | private $preview: HTMLDivElement 47 | private $fonts: HTMLDivElement 48 | private $variants: HTMLDivElement 49 | 50 | private $filtersText: HTMLSpanElement 51 | private $metricsText: HTMLSpanElement 52 | private $sortText: HTMLSpanElement 53 | 54 | private $clearFiltersBtn: HTMLButtonElement 55 | 56 | private $cancelBtn: HTMLButtonElement 57 | private $clearBtn: HTMLButtonElement 58 | private $pickBtn: HTMLButtonElement 59 | 60 | constructor(parent: HTMLElement) { 61 | this.createLayout(parent) 62 | 63 | this.observer = new IntersectionObserver((entries) => { 64 | for (const entry of entries) { 65 | const $target = entry.target as HTMLDivElement 66 | 67 | if (entry.isIntersecting && !$target.childElementCount) { 68 | const family = this.getFamilyFor($target) 69 | if (!family) continue 70 | 71 | DOM.hydrateFont($target, family) 72 | FontLoader.load(family) 73 | } else if (!entry.isIntersecting && $target.childElementCount) { 74 | $target.textContent = '' 75 | } 76 | } 77 | }) 78 | } 79 | 80 | private createLayout(parent: HTMLElement) { 81 | parent.insertAdjacentHTML('afterbegin', dialogContent) 82 | 83 | this.$modal = document.querySelector('#fp__modal')! 84 | this.$modalBackdrop = document.querySelector('#fp__backdrop')! 85 | this.$closeBtn = this.$modal.querySelector('#fp__close')! 86 | 87 | this.$search = this.$modal.querySelector('#fp__search')! 88 | this.$subset = this.$modal.querySelector('#fp__subsets')! 89 | this.$categories = this.$modal.querySelector('#fp__categories')! 90 | 91 | this.$width = this.$modal.querySelector('#fp__width')! 92 | this.$thickness = this.$modal.querySelector('#fp__thickness')! 93 | this.$complexity = this.$modal.querySelector('#fp__complexity')! 94 | this.$curvature = this.$modal.querySelector('#fp__curvature')! 95 | 96 | this.$sort = this.$modal.querySelector('#fp__sort')! 97 | this.$sortOrder = this.$modal.querySelector('#fp__sort-order')! 98 | 99 | this.$preview = this.$modal.querySelector('#fp__preview')! 100 | this.$fonts = this.$modal.querySelector('#fp__fonts')! 101 | this.$variants = this.$modal.querySelector('#fp__variants')! 102 | 103 | this.$clearFiltersBtn = this.$modal.querySelector('#fp__clear-filters')! 104 | 105 | this.$cancelBtn = this.$modal.querySelector('#fp__cancel')! 106 | this.$clearBtn = this.$modal.querySelector('#fp__clear')! 107 | this.$pickBtn = this.$modal.querySelector('#fp__pick')! 108 | 109 | this.$filtersText = this.$modal.querySelector('#fp__t-filters')! 110 | this.$metricsText = this.$modal.querySelector('#fp__t-metrics')! 111 | this.$sortText = this.$modal.querySelector('#fp__t-sort')! 112 | 113 | this.modal = new Modal(this.$modal) 114 | new Accordion(this.$modal.querySelector('.fpb__accordion')!) 115 | } 116 | 117 | private getElementFor(family: FontFamily) { 118 | const $font = this.$fonts.querySelector(`[data-family="${family.name}"]`) 119 | if (!$font) throw new Error(`Could not find element for '${family.name}'!`) 120 | return $font as HTMLElement 121 | } 122 | 123 | private getFamilyFor($element: Element | EventTarget) { 124 | const name = ($element as HTMLElement).dataset.family 125 | if (!name) return null 126 | return this.picker.getFamily(name) 127 | } 128 | 129 | private getFamilies() { 130 | return Array.from(this.picker.families.values()) 131 | } 132 | 133 | private sortFamilies(orderBy: Criterion, reverse = false) { 134 | const families = this.getFamilies() 135 | 136 | const sorted = families.sort((a, b) => familySort(a, b, orderBy)) 137 | if (reverse) sorted.reverse() 138 | 139 | for (const family of sorted) { 140 | this.$fonts.append(this.getElementFor(family)) 141 | } 142 | 143 | // put selected and favourites at the top 144 | for (const favourite of this.picker.favourites) { 145 | const $favourite = this.getElementFor(favourite) 146 | this.$fonts.prepend($favourite) 147 | } 148 | 149 | if (this.selected) { 150 | const $selected = this.getElementFor(this.selected.family) 151 | this.$fonts.prepend($selected) 152 | } 153 | 154 | this.$fonts.scrollTop = 0 155 | } 156 | 157 | private filterFamilies(filters: Filters) { 158 | const families = this.getFamilies() 159 | 160 | const filtered = families.filter((a) => familyFilter(a, filters)) 161 | const familyNames = filtered.map((filtered) => filtered.name) 162 | 163 | for (const $font of this.$fonts.children) { 164 | const name = ($font as HTMLElement).dataset.family! 165 | const hidden = !familyNames.includes(name) 166 | $font.classList.toggle('fpb__hidden', hidden) 167 | } 168 | } 169 | 170 | private updateSort() { 171 | const orderBy = this.$sort.value as Criterion 172 | const reverse = this.$sortOrder.checked 173 | this.sortFamilies(orderBy, reverse) 174 | } 175 | 176 | private updateFilter() { 177 | this.filterFamilies({ 178 | name: this.$search.value, 179 | subset: this.$subset.value as Subset, 180 | categories: DOM.getActiveBadges(this.$categories) as Category[], 181 | complexity: this.$complexity.value as Metric, 182 | curvature: this.$curvature.value as Metric, 183 | thickness: this.$thickness.value as Metric, 184 | width: this.$width.value as Metric, 185 | }) 186 | } 187 | 188 | private updatePreview() { 189 | // only use selected variants for selected font, otherwise use defaults 190 | const font = this.hovered ?? this.selected 191 | 192 | if (font) { 193 | this.$preview.style.fontFamily = `"${font.family}"` 194 | this.$preview.style.fontWeight = font.weight.toString() 195 | this.$preview.style.fontStyle = font.style 196 | } else { 197 | this.$preview.style.removeProperty('font-family') 198 | this.$preview.style.removeProperty('font-weight') 199 | this.$preview.style.removeProperty('font-style') 200 | } 201 | } 202 | 203 | private selectFont(font: Font | null) { 204 | // deselect previously selected fonts 205 | for (const $font of this.$fonts.querySelectorAll('.fp__selected')) { 206 | $font.classList.remove('fp__selected') 207 | } 208 | 209 | // set selected font 210 | this.selected = font 211 | if (!font) return 212 | 213 | this.getElementFor(font.family).classList.add('fp__selected') 214 | if (!this.config.variants) return 215 | 216 | // create variants 217 | this.$variants.textContent = '' 218 | this.$variants.append(...DOM.createVariants(font.family.variants)) 219 | 220 | // set current variant 221 | const $weight = this.$variants.querySelector(`#fp__weight-${font.weight}`) 222 | const $italic = this.$variants.querySelector('#fp__italic') 223 | if (!$weight) throw new Error('Could not find weight button for selected font.') 224 | if (!$italic) throw new Error('Could not find italic button for selected font.') 225 | 226 | $weight.checked = true 227 | $italic.checked = font.italic 228 | 229 | this.updateVariant() 230 | } 231 | 232 | private favouriteFont(font: Font) { 233 | const $family = this.getElementFor(font.family) 234 | const value = $family.classList.toggle('fp__fav') 235 | this.picker.markFavourite(font.family, value) 236 | } 237 | 238 | private updateVariant() { 239 | if (!this.config.variants) return 240 | if (!this.selected) return 241 | 242 | const $weight = this.$variants.querySelector('[name=fp__weight]:checked') 243 | const $italic = this.$variants.querySelector('#fp__italic') 244 | 245 | if (!$weight) throw new Error('Could not find weight button for selected font.') 246 | if (!$italic) throw new Error('Could not find italic button for selected font.') 247 | 248 | let weight = parseInt($weight.value) as FontWeight 249 | let italic = $italic.checked 250 | 251 | // check if font doesn't have italic/regular variants for current font 252 | const hasRegular = this.selected.family.variants.includes(`${weight}`) 253 | const hasItalic = this.selected.family.variants.includes(`${weight}i`) 254 | 255 | $italic.disabled = !hasRegular || !hasItalic 256 | if (!hasRegular) italic = true 257 | if (!hasItalic) italic = false 258 | $italic.checked = italic 259 | 260 | this.selected = new Font(this.selected.family, weight, italic) 261 | this.updatePreview() 262 | } 263 | 264 | private createLazyFontList() { 265 | for (const font of this.getFamilies()) { 266 | const $item = DOM.createLazyFont(font) 267 | this.$fonts.append($item) 268 | this.observer.observe($item) 269 | } 270 | } 271 | 272 | private applyTranslations() { 273 | const dict = translations[this.config.language] 274 | 275 | this.$search.placeholder = dict.search 276 | this.$modal.querySelector('#fp__title')!.textContent = dict.selectFont 277 | 278 | this.$subset.append(...DOM.createOptions(dict.subsets)) 279 | this.$categories.append(...DOM.createBadges(dict.categories)) 280 | 281 | this.$width.append(...DOM.createOptions(dict.widths)) 282 | this.$thickness.append(...DOM.createOptions(dict.thicknesses)) 283 | this.$complexity.append(...DOM.createOptions(dict.complexities)) 284 | this.$curvature.append(...DOM.createOptions(dict.curvatures)) 285 | this.$sort.append(...DOM.createOptions(dict.sorts)) 286 | 287 | this.$preview.textContent = this.config.previewText ?? dict.sampleText 288 | 289 | this.$filtersText.textContent = dict.filters 290 | this.$metricsText.textContent = dict.metrics 291 | this.$sortText.textContent = dict.sort 292 | 293 | this.$modal.querySelector('#fp__t-clear-filters')!.textContent = dict.clearFilters 294 | 295 | this.$modal.querySelector('#fp__t-cancel')!.textContent = dict.cancel 296 | this.$modal.querySelector('#fp__t-clear')!.textContent = dict.clear 297 | this.$modal.querySelector('#fp__t-pick')!.textContent = dict.select 298 | } 299 | 300 | private onFontHover(event: MouseEvent) { 301 | const family = this.getFamilyFor(event.target!) 302 | if (!family) return 303 | 304 | // to prevent different variant for selected font being previewed 305 | if (family === this.selected?.family) { 306 | this.hovered = null 307 | } else { 308 | this.hovered = Font.parse(family) 309 | } 310 | 311 | this.updatePreview() 312 | } 313 | 314 | private onFontUnhover(event: MouseEvent) { 315 | if (!this.getFamilyFor(event.target!)) return 316 | 317 | this.hovered = null 318 | this.updatePreview() 319 | } 320 | 321 | private onFontClick(event: MouseEvent) { 322 | const $target = event.target as HTMLElement 323 | 324 | // when favourite is clicked 325 | if ($target.classList.contains('fp__heart')) { 326 | const family = this.getFamilyFor($target.parentElement!) 327 | if (!family) return 328 | const font = Font.parse(family) 329 | this.selectFont(font) 330 | this.favouriteFont(font) 331 | return 332 | } 333 | 334 | // when font is clicked 335 | const family = this.getFamilyFor($target) 336 | if (!family || family === this.selected?.family) return 337 | this.selectFont(Font.parse(family)) 338 | } 339 | 340 | private onFontDoubleClick(event: MouseEvent) { 341 | if (!this.getFamilyFor(event.target!)) return 342 | this.submit() 343 | } 344 | 345 | private selectClosestFont(excluded: boolean, reverse: boolean, $from?: Element | null) { 346 | if (!this.selected) { 347 | ;(this.$fonts.firstElementChild as HTMLButtonElement).click() 348 | return 349 | } 350 | 351 | let $target = $from ? ($from as HTMLElement) : this.getElementFor(this.selected.family) 352 | 353 | while (excluded || $target.classList.contains('fpb__hidden')) { 354 | excluded = false 355 | 356 | const $next = reverse ? $target.previousElementSibling : $target.nextElementSibling 357 | if (!$next) return 358 | 359 | $target = $next as HTMLElement 360 | } 361 | 362 | this.hovered = null 363 | 364 | $target.click() 365 | $target.scrollIntoView({ 366 | behavior: 'instant', 367 | block: 'center', 368 | }) 369 | } 370 | 371 | private selectClosestVariant(reverse: boolean) { 372 | const $origin = this.$variants.querySelector('[name=fp__weight]:checked') 373 | const $next = reverse 374 | ? $origin?.previousElementSibling?.previousElementSibling 375 | : $origin?.nextElementSibling?.nextElementSibling 376 | 377 | if (!$next) return 378 | 379 | const $target = $next as HTMLInputElement 380 | $target.checked = !$target.checked 381 | 382 | this.updateVariant() 383 | } 384 | 385 | private toggleVariantItalic() { 386 | const $target = this.$variants.querySelector('#fp__italic') 387 | if (!$target) return 388 | $target.checked = !$target.checked 389 | this.updateVariant() 390 | } 391 | 392 | private onKeyPressed(event: KeyboardEvent) { 393 | if (!this.opened) return 394 | 395 | const $target = event.target as HTMLElement | null 396 | if ($target && $target !== this.$modal && !this.$fonts.contains($target)) { 397 | return // an element is focused that requires keyboard input, don't handle. 398 | } 399 | 400 | let handled = true 401 | 402 | if (event.key === 'Escape') { 403 | // cancel font picker modal 404 | this.cancel() 405 | } else if (event.key === 'f') { 406 | // toggle favourite for selected font 407 | if (this.selected) this.favouriteFont(this.selected) 408 | } else if (event.key === 'PageUp') { 409 | // select first font 410 | this.selectClosestFont(false, false, this.$fonts.firstElementChild) 411 | } else if (event.key === 'PageDown') { 412 | // select last font 413 | this.selectClosestFont(false, true, this.$fonts.lastElementChild) 414 | } else if (event.key === 'ArrowUp') { 415 | // select previous font 416 | this.selectClosestFont(true, true, null) 417 | } else if (event.key === 'ArrowDown') { 418 | // select next font 419 | this.selectClosestFont(true, false, null) 420 | } else if (event.key === 'ArrowLeft') { 421 | // select previous font variant 422 | this.selectClosestVariant(true) 423 | } else if (event.key === 'ArrowRight') { 424 | // select next font variant 425 | this.selectClosestVariant(false) 426 | } else if (event.key === 'i') { 427 | // toggle font italic 428 | this.toggleVariantItalic() 429 | } else if (event.key === '/') { 430 | // focus search input 431 | this.$search.focus() 432 | } else if (event.key === 'Enter') { 433 | // submit font picker modal 434 | this.submit() 435 | } else { 436 | handled = false 437 | } 438 | 439 | if (handled) event.preventDefault() 440 | } 441 | 442 | private bindEvents() { 443 | const filtersCallback = () => { 444 | this.filtersChanged(this.$filtersText) 445 | this.updateFilter() 446 | } 447 | 448 | this.$categories.addEventListener('input', filtersCallback) 449 | this.$search.addEventListener('input', filtersCallback) 450 | this.$subset.addEventListener('input', filtersCallback) 451 | 452 | const metricsCallback = () => { 453 | this.filtersChanged(this.$metricsText) 454 | this.updateFilter() 455 | } 456 | 457 | this.$width.addEventListener('input', metricsCallback) 458 | this.$thickness.addEventListener('input', metricsCallback) 459 | this.$complexity.addEventListener('input', metricsCallback) 460 | this.$curvature.addEventListener('input', metricsCallback) 461 | 462 | const sortCallback = () => { 463 | this.filtersChanged(this.$sortText) 464 | this.updateSort() 465 | } 466 | 467 | this.$sort.addEventListener('input', sortCallback) 468 | this.$sortOrder.addEventListener('input', sortCallback) 469 | 470 | this.$fonts.addEventListener('mouseover', (event) => this.onFontHover(event)) 471 | this.$fonts.addEventListener('mouseout', (event) => this.onFontUnhover(event)) 472 | this.$fonts.addEventListener('click', (event) => this.onFontClick(event)) 473 | this.$fonts.addEventListener('dblclick', (event) => this.onFontDoubleClick(event)) 474 | 475 | this.$variants.addEventListener('input', () => this.updateVariant()) 476 | 477 | this.$clearFiltersBtn.addEventListener('click', () => { 478 | this.override = {} 479 | this.assignDefaults() 480 | }) 481 | 482 | this.$pickBtn.addEventListener('click', () => this.submit()) 483 | 484 | this.$clearBtn?.addEventListener('click', () => this.clear()) 485 | this.$cancelBtn?.addEventListener('click', () => this.cancel()) 486 | 487 | this.$modalBackdrop.addEventListener('click', () => this.cancel()) 488 | this.$closeBtn.addEventListener('click', () => this.cancel()) 489 | 490 | this.$modal.addEventListener('keydown', (event) => this.onKeyPressed(event)) 491 | } 492 | 493 | private applyConfiguration() { 494 | // set favourites 495 | this.picker.favourites.forEach((family) => this.getElementFor(family).classList.add('fp__fav')) 496 | 497 | // hide variants 498 | this.$variants.classList.toggle('fpb__hidden', !this.config.variants) 499 | 500 | // hide buttons 501 | if (!this.config.showClearButton) this.$clearBtn.remove() 502 | if (!this.config.showCancelButton) this.$cancelBtn.remove() 503 | } 504 | 505 | private filtersChanged($target: HTMLElement | null) { 506 | if ($target) { 507 | $target.classList.add('fp__changed') 508 | } else { 509 | this.$filtersText.classList.remove('fp__changed') 510 | this.$metricsText.classList.remove('fp__changed') 511 | this.$sortText.classList.remove('fp__changed') 512 | } 513 | 514 | this.$clearFiltersBtn.classList.toggle('fpb__hidden', !$target) 515 | } 516 | 517 | private assignDefaults() { 518 | const config = { ...this.config, ...this.override } 519 | 520 | this.$search.value = config.defaultSearch 521 | 522 | DOM.setActiveBadges(this.$categories, config.defaultCategories) 523 | this.$subset.value = config.defaultSubset 524 | this.$width.value = config.defaultWidth 525 | this.$thickness.value = config.defaultThickness 526 | this.$complexity.value = config.defaultComplexity 527 | this.$curvature.value = config.defaultCurvature 528 | this.$sort.value = config.sortBy 529 | this.$sortOrder.checked = config.sortReverse 530 | 531 | // Apply changed defaults 532 | this.updateSort() 533 | this.updateFilter() 534 | 535 | // If no overrides were passed, don't check 536 | this.filtersChanged(null) 537 | if (!Object.values(this.override).length) return 538 | 539 | // Update filters clear button 540 | if ( 541 | this.override.defaultSearch !== undefined || 542 | this.override.defaultCategories !== undefined || 543 | this.override.defaultSubset !== undefined 544 | ) 545 | this.filtersChanged(this.$filtersText) 546 | 547 | if ( 548 | this.override.defaultWidth !== undefined || 549 | this.override.defaultThickness !== undefined || 550 | this.override.defaultComplexity !== undefined || 551 | this.override.defaultCurvature !== undefined 552 | ) 553 | this.filtersChanged(this.$metricsText) 554 | 555 | if (this.override.sortBy !== undefined || this.override.sortReverse !== undefined) 556 | this.filtersChanged(this.$sortText) 557 | } 558 | 559 | private storeDefaults() { 560 | const override: Partial = {} 561 | 562 | const defaultSearch = this.$search.value 563 | if (defaultSearch !== this.config.defaultSearch) override.defaultSearch = defaultSearch 564 | 565 | const defaultCategories = DOM.getActiveBadges(this.$categories) as Category[] 566 | if ( 567 | JSON.stringify(defaultCategories.toSorted()) !== 568 | JSON.stringify(this.config.defaultCategories?.toSorted()) 569 | ) 570 | override.defaultCategories = defaultCategories 571 | 572 | const defaultSubset = this.$subset.value as Subset 573 | if (defaultSubset !== this.config.defaultSubset) override.defaultSubset = defaultSubset 574 | 575 | const defaultWidth = this.$width.value as Metric 576 | if (defaultWidth !== this.config.defaultWidth) override.defaultWidth = defaultWidth 577 | 578 | const defaultThickness = this.$thickness.value as Metric 579 | if (defaultThickness !== this.config.defaultThickness) 580 | override.defaultThickness = defaultThickness 581 | 582 | const defaultComplexity = this.$complexity.value as Metric 583 | if (defaultComplexity !== this.config.defaultComplexity) 584 | override.defaultComplexity = defaultComplexity 585 | 586 | const defaultCurvature = this.$curvature.value as Metric 587 | if (defaultCurvature !== this.config.defaultCurvature) 588 | override.defaultCurvature = defaultCurvature 589 | 590 | const sortBy = this.$sort.value as Criterion 591 | if (sortBy !== this.config.sortBy) override.sortBy = sortBy 592 | 593 | const sortReverse = this.$sortOrder.checked 594 | if (sortReverse !== this.config.sortReverse) override.sortReverse = sortReverse 595 | 596 | configOverrides.set(this.config.stateKey, override) 597 | } 598 | 599 | async open(picker: FontPicker) { 600 | if (this.opened) return 601 | 602 | this.opened = true 603 | this.picker = picker 604 | 605 | this.config = this.picker.getConfig() 606 | this.override = configOverrides.get(this.config.stateKey) ?? {} 607 | 608 | this.applyTranslations() 609 | this.bindEvents() 610 | 611 | this.createLazyFontList() 612 | this.selectFont(picker.font) 613 | 614 | this.applyConfiguration() 615 | this.assignDefaults() 616 | 617 | requestAnimationFrame(() => { 618 | this.modal.open() 619 | this.picker.emit('open') 620 | this.modal.once('opened', () => { 621 | this.picker.emit('opened') 622 | this.$fonts.focus() 623 | }) 624 | }) 625 | 626 | // Destroy elements without blocking 627 | this.modal.once('closed', () => { 628 | this.picker.emit('close') 629 | this.$modal.remove() 630 | this.$modalBackdrop.remove() 631 | }) 632 | 633 | // Resolve when modal starts closing 634 | await new Promise((resolve) => { 635 | this.modal.once('closing', () => resolve()) 636 | }) 637 | 638 | this.storeDefaults() 639 | } 640 | 641 | submit() { 642 | this.picker.setFont(this.selected, true /* Emit events */) 643 | //this.picker.emit('pick', this.selected) 644 | this.close() 645 | } 646 | 647 | clear() { 648 | this.picker.clear(true /* Emit events */) 649 | this.close() 650 | } 651 | 652 | cancel() { 653 | this.picker.emit('cancel') 654 | this.close() 655 | } 656 | 657 | close() { 658 | this.opened = false 659 | this.modal.close() 660 | } 661 | 662 | destroy() { 663 | this.$modal.remove() 664 | } 665 | } 666 | -------------------------------------------------------------------------------- /bun.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1, 3 | "workspaces": { 4 | "": { 5 | "name": "fontpicker", 6 | "devDependencies": { 7 | "@types/bun": "latest", 8 | "esbuild": "^0.25.5", 9 | "events": "^3.3.0", 10 | "leven": "^4.0.0", 11 | "prettier": "^3.3.3", 12 | "typescript": "^5.5.3", 13 | "vite": "^6.3.5", 14 | "vite-plugin-dts": "^4.2.3", 15 | }, 16 | }, 17 | }, 18 | "packages": { 19 | "@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.7", "", {}, "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g=="], 20 | 21 | "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.7", "", {}, "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg=="], 22 | 23 | "@babel/parser": ["@babel/parser@7.25.7", "", { "dependencies": { "@babel/types": "^7.25.7" }, "bin": "./bin/babel-parser.js" }, "sha512-aZn7ETtQsjjGG5HruveUK06cU3Hljuhd9Iojm4M8WWv3wLE6OkE5PWbDUkItmMgegmccaITudyuW5RPYrYlgWw=="], 24 | 25 | "@babel/types": ["@babel/types@7.25.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.7", "@babel/helper-validator-identifier": "^7.25.7", "to-fast-properties": "^2.0.0" } }, "sha512-vwIVdXG+j+FOpkwqHRcBgHLYNL7XMkufrlaFvL9o6Ai9sJn9+PdyIL5qa0XzTZw084c+u9LOls53eoZWP/W5WQ=="], 26 | 27 | "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="], 28 | 29 | "@esbuild/android-arm": ["@esbuild/android-arm@0.25.5", "", { "os": "android", "cpu": "arm" }, "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA=="], 30 | 31 | "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.5", "", { "os": "android", "cpu": "arm64" }, "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg=="], 32 | 33 | "@esbuild/android-x64": ["@esbuild/android-x64@0.25.5", "", { "os": "android", "cpu": "x64" }, "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw=="], 34 | 35 | "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ=="], 36 | 37 | "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ=="], 38 | 39 | "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw=="], 40 | 41 | "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw=="], 42 | 43 | "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.5", "", { "os": "linux", "cpu": "arm" }, "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw=="], 44 | 45 | "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg=="], 46 | 47 | "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA=="], 48 | 49 | "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg=="], 50 | 51 | "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg=="], 52 | 53 | "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ=="], 54 | 55 | "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA=="], 56 | 57 | "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ=="], 58 | 59 | "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.5", "", { "os": "linux", "cpu": "x64" }, "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw=="], 60 | 61 | "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.5", "", { "os": "none", "cpu": "arm64" }, "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw=="], 62 | 63 | "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.5", "", { "os": "none", "cpu": "x64" }, "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ=="], 64 | 65 | "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.5", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw=="], 66 | 67 | "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg=="], 68 | 69 | "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA=="], 70 | 71 | "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw=="], 72 | 73 | "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ=="], 74 | 75 | "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g=="], 76 | 77 | "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], 78 | 79 | "@microsoft/api-extractor": ["@microsoft/api-extractor@7.47.7", "", { "dependencies": { "@microsoft/api-extractor-model": "7.29.6", "@microsoft/tsdoc": "~0.15.0", "@microsoft/tsdoc-config": "~0.17.0", "@rushstack/node-core-library": "5.7.0", "@rushstack/rig-package": "0.5.3", "@rushstack/terminal": "0.14.0", "@rushstack/ts-command-line": "4.22.6", "lodash": "~4.17.15", "minimatch": "~3.0.3", "resolve": "~1.22.1", "semver": "~7.5.4", "source-map": "~0.6.1", "typescript": "5.4.2" }, "bin": { "api-extractor": "bin/api-extractor" } }, "sha512-fNiD3G55ZJGhPOBPMKD/enozj8yxJSYyVJWxRWdcUtw842rvthDHJgUWq9gXQTensFlMHv2wGuCjjivPv53j0A=="], 80 | 81 | "@microsoft/api-extractor-model": ["@microsoft/api-extractor-model@7.29.6", "", { "dependencies": { "@microsoft/tsdoc": "~0.15.0", "@microsoft/tsdoc-config": "~0.17.0", "@rushstack/node-core-library": "5.7.0" } }, "sha512-gC0KGtrZvxzf/Rt9oMYD2dHvtN/1KPEYsrQPyMKhLHnlVuO/f4AFN3E4toqZzD2pt4LhkKoYmL2H9tX3yCOyRw=="], 82 | 83 | "@microsoft/tsdoc": ["@microsoft/tsdoc@0.15.0", "", {}, "sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA=="], 84 | 85 | "@microsoft/tsdoc-config": ["@microsoft/tsdoc-config@0.17.0", "", { "dependencies": { "@microsoft/tsdoc": "0.15.0", "ajv": "~8.12.0", "jju": "~1.4.0", "resolve": "~1.22.2" } }, "sha512-v/EYRXnCAIHxOHW+Plb6OWuUoMotxTN0GLatnpOb1xq0KuTNw/WI3pamJx/UbsoJP5k9MCw1QxvvhPcF9pH3Zg=="], 86 | 87 | "@rollup/pluginutils": ["@rollup/pluginutils@5.1.2", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^2.3.1" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-/FIdS3PyZ39bjZlwqFnWqCOVnW7o963LtKMwQOD0NhQqw22gSr2YY1afu3FxRip4ZCZNsD5jq6Aaz6QV3D/Njw=="], 88 | 89 | "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.43.0", "", { "os": "android", "cpu": "arm" }, "sha512-Krjy9awJl6rKbruhQDgivNbD1WuLb8xAclM4IR4cN5pHGAs2oIMMQJEiC3IC/9TZJ+QZkmZhlMO/6MBGxPidpw=="], 90 | 91 | "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.43.0", "", { "os": "android", "cpu": "arm64" }, "sha512-ss4YJwRt5I63454Rpj+mXCXicakdFmKnUNxr1dLK+5rv5FJgAxnN7s31a5VchRYxCFWdmnDWKd0wbAdTr0J5EA=="], 92 | 93 | "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.43.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eKoL8ykZ7zz8MjgBenEF2OoTNFAPFz1/lyJ5UmmFSz5jW+7XbH1+MAgCVHy72aG59rbuQLcJeiMrP8qP5d/N0A=="], 94 | 95 | "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.43.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-SYwXJgaBYW33Wi/q4ubN+ldWC4DzQY62S4Ll2dgfr/dbPoF50dlQwEaEHSKrQdSjC6oIe1WgzosoaNoHCdNuMg=="], 96 | 97 | "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.43.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-SV+U5sSo0yujrjzBF7/YidieK2iF6E7MdF6EbYxNz94lA+R0wKl3SiixGyG/9Klab6uNBIqsN7j4Y/Fya7wAjQ=="], 98 | 99 | "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.43.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J7uCsiV13L/VOeHJBo5SjasKiGxJ0g+nQTrBkAsmQBIdil3KhPnSE9GnRon4ejX1XDdsmK/l30IYLiAaQEO0Cg=="], 100 | 101 | "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.43.0", "", { "os": "linux", "cpu": "arm" }, "sha512-gTJ/JnnjCMc15uwB10TTATBEhK9meBIY+gXP4s0sHD1zHOaIh4Dmy1X9wup18IiY9tTNk5gJc4yx9ctj/fjrIw=="], 102 | 103 | "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.43.0", "", { "os": "linux", "cpu": "arm" }, "sha512-ZJ3gZynL1LDSIvRfz0qXtTNs56n5DI2Mq+WACWZ7yGHFUEirHBRt7fyIk0NsCKhmRhn7WAcjgSkSVVxKlPNFFw=="], 104 | 105 | "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.43.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-8FnkipasmOOSSlfucGYEu58U8cxEdhziKjPD2FIa0ONVMxvl/hmONtX/7y4vGjdUhjcTHlKlDhw3H9t98fPvyA=="], 106 | 107 | "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.43.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-KPPyAdlcIZ6S9C3S2cndXDkV0Bb1OSMsX0Eelr2Bay4EsF9yi9u9uzc9RniK3mcUGCLhWY9oLr6er80P5DE6XA=="], 108 | 109 | "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.43.0", "", { "os": "linux", "cpu": "none" }, "sha512-HPGDIH0/ZzAZjvtlXj6g+KDQ9ZMHfSP553za7o2Odegb/BEfwJcR0Sw0RLNpQ9nC6Gy8s+3mSS9xjZ0n3rhcYg=="], 110 | 111 | "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.43.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gEmwbOws4U4GLAJDhhtSPWPXUzDfMRedT3hFMyRAvM9Mrnj+dJIFIeL7otsv2WF3D7GrV0GIewW0y28dOYWkmw=="], 112 | 113 | "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.43.0", "", { "os": "linux", "cpu": "none" }, "sha512-XXKvo2e+wFtXZF/9xoWohHg+MuRnvO29TI5Hqe9xwN5uN8NKUYy7tXUG3EZAlfchufNCTHNGjEx7uN78KsBo0g=="], 114 | 115 | "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.43.0", "", { "os": "linux", "cpu": "none" }, "sha512-ruf3hPWhjw6uDFsOAzmbNIvlXFXlBQ4nk57Sec8E8rUxs/AI4HD6xmiiasOOx/3QxS2f5eQMKTAwk7KHwpzr/Q=="], 116 | 117 | "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.43.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-QmNIAqDiEMEvFV15rsSnjoSmO0+eJLoKRD9EAa9rrYNwO/XRCtOGM3A5A0X+wmG+XRrw9Fxdsw+LnyYiZWWcVw=="], 118 | 119 | "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.43.0", "", { "os": "linux", "cpu": "x64" }, "sha512-jAHr/S0iiBtFyzjhOkAics/2SrXE092qyqEg96e90L3t9Op8OTzS6+IX0Fy5wCt2+KqeHAkti+eitV0wvblEoQ=="], 120 | 121 | "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.43.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3yATWgdeXyuHtBhrLt98w+5fKurdqvs8B53LaoKD7P7H7FKOONLsBVMNl9ghPQZQuYcceV5CDyPfyfGpMWD9mQ=="], 122 | 123 | "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.43.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-wVzXp2qDSCOpcBCT5WRWLmpJRIzv23valvcTwMHEobkjippNf+C3ys/+wf07poPkeNix0paTNemB2XrHr2TnGw=="], 124 | 125 | "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.43.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-fYCTEyzf8d+7diCw8b+asvWDCLMjsCEA8alvtAutqJOJp/wL5hs1rWSqJ1vkjgW0L2NB4bsYJrpKkiIPRR9dvw=="], 126 | 127 | "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.43.0", "", { "os": "win32", "cpu": "x64" }, "sha512-SnGhLiE5rlK0ofq8kzuDkM0g7FN1s5VYY+YSMTibP7CqShxCQvqtNxTARS4xX4PFJfHjG0ZQYX9iGzI3FQh5Aw=="], 128 | 129 | "@rushstack/node-core-library": ["@rushstack/node-core-library@5.7.0", "", { "dependencies": { "ajv": "~8.13.0", "ajv-draft-04": "~1.0.0", "ajv-formats": "~3.0.1", "fs-extra": "~7.0.1", "import-lazy": "~4.0.0", "jju": "~1.4.0", "resolve": "~1.22.1", "semver": "~7.5.4" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-Ff9Cz/YlWu9ce4dmqNBZpA45AEya04XaBFIjV7xTVeEf+y/kTjEasmozqFELXlNG4ROdevss75JrrZ5WgufDkQ=="], 130 | 131 | "@rushstack/rig-package": ["@rushstack/rig-package@0.5.3", "", { "dependencies": { "resolve": "~1.22.1", "strip-json-comments": "~3.1.1" } }, "sha512-olzSSjYrvCNxUFZowevC3uz8gvKr3WTpHQ7BkpjtRpA3wK+T0ybep/SRUMfr195gBzJm5gaXw0ZMgjIyHqJUow=="], 132 | 133 | "@rushstack/terminal": ["@rushstack/terminal@0.14.0", "", { "dependencies": { "@rushstack/node-core-library": "5.7.0", "supports-color": "~8.1.1" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-juTKMAMpTIJKudeFkG5slD8Z/LHwNwGZLtU441l/u82XdTBfsP+LbGKJLCNwP5se+DMCT55GB8x9p6+C4UL7jw=="], 134 | 135 | "@rushstack/ts-command-line": ["@rushstack/ts-command-line@4.22.6", "", { "dependencies": { "@rushstack/terminal": "0.14.0", "@types/argparse": "1.0.38", "argparse": "~1.0.9", "string-argv": "~0.3.1" } }, "sha512-QSRqHT/IfoC5nk9zn6+fgyqOPXHME0BfchII9EUPR19pocsNp/xSbeBCbD3PIR2Lg+Q5qk7OFqk1VhWPMdKHJg=="], 136 | 137 | "@types/argparse": ["@types/argparse@1.0.38", "", {}, "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA=="], 138 | 139 | "@types/bun": ["@types/bun@1.1.9", "", { "dependencies": { "bun-types": "1.1.27" } }, "sha512-SXJRejXpmAc3qxyN/YS4/JGWEzLf4dDBa5fLtRDipQXHqNccuMU4EUYCooXNTsylG0DmwFQsGgEDHxZF+3DqRw=="], 140 | 141 | "@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], 142 | 143 | "@types/node": ["@types/node@20.12.14", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg=="], 144 | 145 | "@types/ws": ["@types/ws@8.5.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ=="], 146 | 147 | "@volar/language-core": ["@volar/language-core@2.4.6", "", { "dependencies": { "@volar/source-map": "2.4.6" } }, "sha512-FxUfxaB8sCqvY46YjyAAV6c3mMIq/NWQMVvJ+uS4yxr1KzOvyg61gAuOnNvgCvO4TZ7HcLExBEsWcDu4+K4E8A=="], 148 | 149 | "@volar/source-map": ["@volar/source-map@2.4.6", "", {}, "sha512-Nsh7UW2ruK+uURIPzjJgF0YRGP5CX9nQHypA2OMqdM2FKy7rh+uv3XgPnWPw30JADbKvZ5HuBzG4gSbVDYVtiw=="], 150 | 151 | "@volar/typescript": ["@volar/typescript@2.4.6", "", { "dependencies": { "@volar/language-core": "2.4.6", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-NMIrA7y5OOqddL9VtngPWYmdQU03htNKFtAYidbYfWA0TOhyGVd9tfcP4TsLWQ+RBWDZCbBqsr8xzU0ZOxYTCQ=="], 152 | 153 | "@vue/compiler-core": ["@vue/compiler-core@3.5.11", "", { "dependencies": { "@babel/parser": "^7.25.3", "@vue/shared": "3.5.11", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.0" } }, "sha512-PwAdxs7/9Hc3ieBO12tXzmTD+Ln4qhT/56S+8DvrrZ4kLDn4Z/AMUr8tXJD0axiJBS0RKIoNaR0yMuQB9v9Udg=="], 154 | 155 | "@vue/compiler-dom": ["@vue/compiler-dom@3.5.11", "", { "dependencies": { "@vue/compiler-core": "3.5.11", "@vue/shared": "3.5.11" } }, "sha512-pyGf8zdbDDRkBrEzf8p7BQlMKNNF5Fk/Cf/fQ6PiUz9at4OaUfyXW0dGJTo2Vl1f5U9jSLCNf0EZJEogLXoeew=="], 156 | 157 | "@vue/compiler-vue2": ["@vue/compiler-vue2@2.7.16", "", { "dependencies": { "de-indent": "^1.0.2", "he": "^1.2.0" } }, "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A=="], 158 | 159 | "@vue/language-core": ["@vue/language-core@2.1.6", "", { "dependencies": { "@volar/language-core": "~2.4.1", "@vue/compiler-dom": "^3.4.0", "@vue/compiler-vue2": "^2.7.16", "@vue/shared": "^3.4.0", "computeds": "^0.0.1", "minimatch": "^9.0.3", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-MW569cSky9R/ooKMh6xa2g1D0AtRKbL56k83dzus/bx//RDJk24RHWkMzbAlXjMdDNyxAaagKPRquBIxkxlCkg=="], 160 | 161 | "@vue/shared": ["@vue/shared@3.5.11", "", {}, "sha512-W8GgysJVnFo81FthhzurdRAWP/byq3q2qIw70e0JWblzVhjgOMiC2GyovXrZTFQJnFVryYaKGP3Tc9vYzYm6PQ=="], 162 | 163 | "acorn": ["acorn@8.12.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg=="], 164 | 165 | "ajv": ["ajv@8.12.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA=="], 166 | 167 | "ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="], 168 | 169 | "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], 170 | 171 | "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], 172 | 173 | "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], 174 | 175 | "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], 176 | 177 | "bun-types": ["bun-types@1.1.27", "", { "dependencies": { "@types/node": "~20.12.8", "@types/ws": "~8.5.10" } }, "sha512-rHXAiIDefeMS/fleNM1rRDYqolJGNRdch3+AuCRwcZWaqTa1vjGBNsahH/HVV7Y82frllYhJomCVSEiHzLzkgg=="], 178 | 179 | "compare-versions": ["compare-versions@6.1.1", "", {}, "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg=="], 180 | 181 | "computeds": ["computeds@0.0.1", "", {}, "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q=="], 182 | 183 | "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], 184 | 185 | "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], 186 | 187 | "de-indent": ["de-indent@1.0.2", "", {}, "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg=="], 188 | 189 | "debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], 190 | 191 | "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], 192 | 193 | "esbuild": ["esbuild@0.25.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.5", "@esbuild/android-arm": "0.25.5", "@esbuild/android-arm64": "0.25.5", "@esbuild/android-x64": "0.25.5", "@esbuild/darwin-arm64": "0.25.5", "@esbuild/darwin-x64": "0.25.5", "@esbuild/freebsd-arm64": "0.25.5", "@esbuild/freebsd-x64": "0.25.5", "@esbuild/linux-arm": "0.25.5", "@esbuild/linux-arm64": "0.25.5", "@esbuild/linux-ia32": "0.25.5", "@esbuild/linux-loong64": "0.25.5", "@esbuild/linux-mips64el": "0.25.5", "@esbuild/linux-ppc64": "0.25.5", "@esbuild/linux-riscv64": "0.25.5", "@esbuild/linux-s390x": "0.25.5", "@esbuild/linux-x64": "0.25.5", "@esbuild/netbsd-arm64": "0.25.5", "@esbuild/netbsd-x64": "0.25.5", "@esbuild/openbsd-arm64": "0.25.5", "@esbuild/openbsd-x64": "0.25.5", "@esbuild/sunos-x64": "0.25.5", "@esbuild/win32-arm64": "0.25.5", "@esbuild/win32-ia32": "0.25.5", "@esbuild/win32-x64": "0.25.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ=="], 194 | 195 | "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], 196 | 197 | "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], 198 | 199 | "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], 200 | 201 | "fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="], 202 | 203 | "fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], 204 | 205 | "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], 206 | 207 | "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], 208 | 209 | "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], 210 | 211 | "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], 212 | 213 | "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], 214 | 215 | "he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], 216 | 217 | "import-lazy": ["import-lazy@4.0.0", "", {}, "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw=="], 218 | 219 | "is-core-module": ["is-core-module@2.15.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ=="], 220 | 221 | "jju": ["jju@1.4.0", "", {}, "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA=="], 222 | 223 | "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], 224 | 225 | "jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], 226 | 227 | "kolorist": ["kolorist@1.8.0", "", {}, "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="], 228 | 229 | "leven": ["leven@4.0.0", "", {}, "sha512-puehA3YKku3osqPlNuzGDUHq8WpwXupUg1V6NXdV38G+gr+gkBwFC8g1b/+YcIvp8gnqVIus+eJCH/eGsRmJNw=="], 230 | 231 | "local-pkg": ["local-pkg@0.5.0", "", { "dependencies": { "mlly": "^1.4.2", "pkg-types": "^1.0.3" } }, "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg=="], 232 | 233 | "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], 234 | 235 | "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], 236 | 237 | "magic-string": ["magic-string@0.30.11", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A=="], 238 | 239 | "minimatch": ["minimatch@3.0.8", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q=="], 240 | 241 | "mlly": ["mlly@1.7.2", "", { "dependencies": { "acorn": "^8.12.1", "pathe": "^1.1.2", "pkg-types": "^1.2.0", "ufo": "^1.5.4" } }, "sha512-tN3dvVHYVz4DhSXinXIk7u9syPYaJvio118uomkovAtWBT+RdbP6Lfh/5Lvo519YMmwBafwlh20IPTXIStscpA=="], 242 | 243 | "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 244 | 245 | "muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="], 246 | 247 | "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], 248 | 249 | "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], 250 | 251 | "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], 252 | 253 | "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], 254 | 255 | "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], 256 | 257 | "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], 258 | 259 | "pkg-types": ["pkg-types@1.2.0", "", { "dependencies": { "confbox": "^0.1.7", "mlly": "^1.7.1", "pathe": "^1.1.2" } }, "sha512-+ifYuSSqOQ8CqP4MbZA5hDpb97n3E8SVWdJe+Wms9kj745lmd3b7EZJiqvmLwAlmRfjrI7Hi5z3kdBJ93lFNPA=="], 260 | 261 | "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], 262 | 263 | "prettier": ["prettier@3.3.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew=="], 264 | 265 | "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], 266 | 267 | "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], 268 | 269 | "resolve": ["resolve@1.22.8", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw=="], 270 | 271 | "rollup": ["rollup@4.43.0", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.43.0", "@rollup/rollup-android-arm64": "4.43.0", "@rollup/rollup-darwin-arm64": "4.43.0", "@rollup/rollup-darwin-x64": "4.43.0", "@rollup/rollup-freebsd-arm64": "4.43.0", "@rollup/rollup-freebsd-x64": "4.43.0", "@rollup/rollup-linux-arm-gnueabihf": "4.43.0", "@rollup/rollup-linux-arm-musleabihf": "4.43.0", "@rollup/rollup-linux-arm64-gnu": "4.43.0", "@rollup/rollup-linux-arm64-musl": "4.43.0", "@rollup/rollup-linux-loongarch64-gnu": "4.43.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.43.0", "@rollup/rollup-linux-riscv64-gnu": "4.43.0", "@rollup/rollup-linux-riscv64-musl": "4.43.0", "@rollup/rollup-linux-s390x-gnu": "4.43.0", "@rollup/rollup-linux-x64-gnu": "4.43.0", "@rollup/rollup-linux-x64-musl": "4.43.0", "@rollup/rollup-win32-arm64-msvc": "4.43.0", "@rollup/rollup-win32-ia32-msvc": "4.43.0", "@rollup/rollup-win32-x64-msvc": "4.43.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-wdN2Kd3Twh8MAEOEJZsuxuLKCsBEo4PVNLK6tQWAn10VhsVewQLzcucMgLolRlhFybGxfclbPeEYBaP6RvUFGg=="], 272 | 273 | "semver": ["semver@7.5.4", "", { "dependencies": { "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" } }, "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA=="], 274 | 275 | "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], 276 | 277 | "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], 278 | 279 | "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], 280 | 281 | "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], 282 | 283 | "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], 284 | 285 | "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], 286 | 287 | "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], 288 | 289 | "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], 290 | 291 | "to-fast-properties": ["to-fast-properties@2.0.0", "", {}, "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog=="], 292 | 293 | "typescript": ["typescript@5.6.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw=="], 294 | 295 | "ufo": ["ufo@1.5.4", "", {}, "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ=="], 296 | 297 | "undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], 298 | 299 | "universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], 300 | 301 | "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], 302 | 303 | "vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="], 304 | 305 | "vite-plugin-dts": ["vite-plugin-dts@4.2.3", "", { "dependencies": { "@microsoft/api-extractor": "7.47.7", "@rollup/pluginutils": "^5.1.0", "@volar/typescript": "^2.4.4", "@vue/language-core": "2.1.6", "compare-versions": "^6.1.1", "debug": "^4.3.6", "kolorist": "^1.8.0", "local-pkg": "^0.5.0", "magic-string": "^0.30.11" }, "peerDependencies": { "typescript": "*", "vite": "*" }, "optionalPeers": ["vite"] }, "sha512-O5NalzHANQRwVw1xj8KQun3Bv8OSDAlNJXrnqoAz10BOuW8FVvY5g4ygj+DlJZL5mtSPuMu9vd3OfrdW5d4k6w=="], 306 | 307 | "vscode-uri": ["vscode-uri@3.0.8", "", {}, "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw=="], 308 | 309 | "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], 310 | 311 | "@microsoft/api-extractor/typescript": ["typescript@5.4.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ=="], 312 | 313 | "@rollup/pluginutils/@types/estree": ["@types/estree@1.0.5", "", {}, "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="], 314 | 315 | "@rollup/pluginutils/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], 316 | 317 | "@rushstack/node-core-library/ajv": ["ajv@8.13.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.4.1" } }, "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA=="], 318 | 319 | "@vue/language-core/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], 320 | 321 | "ajv-formats/ajv": ["ajv@8.13.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.4.1" } }, "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA=="], 322 | 323 | "vite/esbuild": ["esbuild@0.25.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.1", "@esbuild/android-arm": "0.25.1", "@esbuild/android-arm64": "0.25.1", "@esbuild/android-x64": "0.25.1", "@esbuild/darwin-arm64": "0.25.1", "@esbuild/darwin-x64": "0.25.1", "@esbuild/freebsd-arm64": "0.25.1", "@esbuild/freebsd-x64": "0.25.1", "@esbuild/linux-arm": "0.25.1", "@esbuild/linux-arm64": "0.25.1", "@esbuild/linux-ia32": "0.25.1", "@esbuild/linux-loong64": "0.25.1", "@esbuild/linux-mips64el": "0.25.1", "@esbuild/linux-ppc64": "0.25.1", "@esbuild/linux-riscv64": "0.25.1", "@esbuild/linux-s390x": "0.25.1", "@esbuild/linux-x64": "0.25.1", "@esbuild/netbsd-arm64": "0.25.1", "@esbuild/netbsd-x64": "0.25.1", "@esbuild/openbsd-arm64": "0.25.1", "@esbuild/openbsd-x64": "0.25.1", "@esbuild/sunos-x64": "0.25.1", "@esbuild/win32-arm64": "0.25.1", "@esbuild/win32-ia32": "0.25.1", "@esbuild/win32-x64": "0.25.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ=="], 324 | 325 | "@vue/language-core/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], 326 | 327 | "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ=="], 328 | 329 | "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.1", "", { "os": "android", "cpu": "arm" }, "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q=="], 330 | 331 | "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.1", "", { "os": "android", "cpu": "arm64" }, "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA=="], 332 | 333 | "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.1", "", { "os": "android", "cpu": "x64" }, "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw=="], 334 | 335 | "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ=="], 336 | 337 | "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA=="], 338 | 339 | "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A=="], 340 | 341 | "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww=="], 342 | 343 | "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.1", "", { "os": "linux", "cpu": "arm" }, "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ=="], 344 | 345 | "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ=="], 346 | 347 | "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ=="], 348 | 349 | "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg=="], 350 | 351 | "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg=="], 352 | 353 | "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg=="], 354 | 355 | "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ=="], 356 | 357 | "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ=="], 358 | 359 | "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA=="], 360 | 361 | "vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.1", "", { "os": "none", "cpu": "arm64" }, "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g=="], 362 | 363 | "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.1", "", { "os": "none", "cpu": "x64" }, "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA=="], 364 | 365 | "vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg=="], 366 | 367 | "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw=="], 368 | 369 | "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg=="], 370 | 371 | "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ=="], 372 | 373 | "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A=="], 374 | 375 | "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.1", "", { "os": "win32", "cpu": "x64" }, "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg=="], 376 | } 377 | } 378 | --------------------------------------------------------------------------------