├── 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 |
2 |
6 |
7 |
8 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
24 |
25 |
26 |
27 |
28 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
56 |
70 |
71 |
72 |
73 |
74 |
75 |
78 |
79 |
80 |
81 |
110 |
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 |
--------------------------------------------------------------------------------