FontPicker
A drop-down of fonts, each option drawn in itself, with a preview line in the one chosen.
Demo
Inter
Each option is drawn in its font where the browser allows.
The quick brown fox jumps over the lazy dog
Headings in Georgia.
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/font_picker.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/font_picker.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/font_picker.qwe
component FontPickerDemoLive() { state font = "Georgia" <div.demo-stack> <FontPicker name="heading_font" label="Headings" value={font} field="font-picker-demo-live" preview="The quick brown fox jumps over the lazy dog" on_change={(f) => { font = f }}/> <p.demo-note>Headings in {font}.</p> </div>}view FontPickerDemo() = <div.demo-stack.demo-narrow> <FontPicker name="font" label="Body font" value="Inter" field="font-picker-demo-all" preview="" hint="Each option is drawn in its font where the browser allows." required={true}/> <FontPickerDemoLive/> <div.demo-row> <FontPicker name="code_font" label="Code" size="sm" placeholder="Choose a font" field="font-picker-demo-small" fonts={[{ label: "JetBrains Mono", value: "jetbrains", stack: "'JetBrains Mono', monospace" }, "Menlo", "Courier New"]}/> <FontPicker name="locked" label="Disabled" size="sm" value="Arial" fonts={["Arial"]} disabled={true}/> </div> </div>Source
ui/font_picker.qwe
// FontPicker: a drop-down of fonts, each option shown in the font it names. It is// the browser's own <select>, so a plain <form> sends the chosen value as `name`// (and the system's own popup may show every option in one font: the preview// line under the select always shows the font chosen).//// <FontPicker name="font" label="Font" value="Georgia"/>// <FontPicker name="heading_font" label="Headings" preview="The quick brown fox" on_change={(f) => { font = f }}// fonts={[{ label: "Inter", value: "inter", stack: "Inter, sans-serif" }, "Georgia", "Courier New"]}/>//// fonts: strings (the font's name is the value and the stack), or maps of// { label, value, stack? } (stack: the CSS font-family it is shown in); the// built-in list is fourteen fonts most systems have (`font_picker_fonts`).// value: the one chosen to start with. preview: a line shown under the select in// the chosen font (none by default; "" shows the font's own name). placeholder: a// first line that cannot be chosen. hint, error, required, disabled, field (the// id, default "font-picker-" + name) and size ("sm" or "md") as in Select.// on_change(value): inside one of your components, given the value chosen. The// preview follows `value`, so a component that keeps it in state keeps it right.import ui/theme.qwe use ui_clslet font_picker_css = css`.ui-font-picker { display: grid; gap: 6px; min-width: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-font-picker-label { font-size: 13px; font-weight: 500; color: var(--ui-fg); }.ui-font-picker-req { margin-left: 2px; color: var(--ui-danger); }.ui-font-picker-box { position: relative; display: block; }.ui-font-picker-box::after { content: ""; position: absolute; right: 14px; top: 50%; width: 7px; height: 7px; margin-top: -6px; border: solid var(--ui-muted); border-width: 0 1.5px 1.5px 0; transform: rotate(45deg); pointer-events: none;}.ui-font-picker select { appearance: none; display: block; width: 100%; height: 38px; padding: 0 36px 0 12px; background: var(--ui-surface); color: var(--ui-fg); font: 14px var(--ui-font); border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); cursor: pointer; text-overflow: ellipsis; transition: border-color .15s, box-shadow .15s;}.ui-font-picker select:hover { border-color: var(--ui-strong); }.ui-font-picker select:focus-visible { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-font-picker select:has(option[value=""]:checked) { color: var(--ui-muted); }.ui-font-picker option { color: var(--ui-fg); background: var(--ui-surface); font-size: 15px; }.ui-font-picker-preview { margin: 0; padding: 10px 12px; border: 1px dashed var(--ui-border); border-radius: var(--ui-radius-sm); font-size: 20px; line-height: 1.3; overflow-wrap: anywhere;}.ui-font-picker-sm select { height: 30px; padding: 0 30px 0 10px; font-size: 13px; }.ui-font-picker-sm .ui-font-picker-box::after { right: 11px; width: 6px; height: 6px; margin-top: -5px; }.ui-font-picker-sm .ui-font-picker-preview { font-size: 16px; padding: 6px 10px; }.ui-font-picker-invalid select, .ui-font-picker-invalid select:hover { border-color: var(--ui-danger); }.ui-font-picker-hint, .ui-font-picker-error { margin: 0; font-size: 13px; color: var(--ui-muted); }.ui-font-picker-error { color: var(--ui-danger); }.ui-font-picker-disabled { opacity: .55; }.ui-font-picker-disabled select { cursor: not-allowed; }@media (prefers-reduced-motion: reduce) { .ui-font-picker select { transition: none; } }`let font_picker_fonts = [ { label: "Arial", value: "Arial", stack: "Arial, Helvetica, sans-serif" }, { label: "Comic Sans MS", value: "Comic Sans MS", stack: "'Comic Sans MS', 'Comic Sans', cursive" }, { label: "Courier New", value: "Courier New", stack: "'Courier New', Courier, monospace" }, { label: "Garamond", value: "Garamond", stack: "Garamond, 'EB Garamond', serif" }, { label: "Georgia", value: "Georgia", stack: "Georgia, 'Times New Roman', serif" }, { label: "Helvetica", value: "Helvetica", stack: "Helvetica, Arial, sans-serif" }, { label: "Impact", value: "Impact", stack: "Impact, 'Arial Black', sans-serif" }, { label: "Inter", value: "Inter", stack: "Inter, system-ui, sans-serif" }, { label: "Menlo", value: "Menlo", stack: "Menlo, Consolas, monospace" }, { label: "Palatino", value: "Palatino", stack: "Palatino, 'Palatino Linotype', 'Book Antiqua', serif" }, { label: "System UI", value: "system-ui", stack: "system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif" }, { label: "Times New Roman", value: "Times New Roman", stack: "'Times New Roman', Times, serif" }, { label: "Trebuchet MS", value: "Trebuchet MS", stack: "'Trebuchet MS', Tahoma, sans-serif" }, { label: "Verdana", value: "Verdana", stack: "Verdana, Geneva, sans-serif" },]// A font as { label, value, stack }, from a string or a map.fn font_picker_font(f) = type_of(f) == "map" ? { label: f.label ?? str(f.value), value: str(f.value ?? f.label), stack: f.stack ?? str(f.value ?? f.label) } : { label: str(f), value: str(f), stack: str(f) }// A font stack only made of what one can be: names, quotes, commas, dashes.fn font_picker_safe(stack: str) -> str = re.test(stack, "^[A-Za-z0-9 ,'_-]{1,120}$") ? stack : "inherit"view FontPicker(name: str? = null, label: str? = null, value: str? = null, fonts: list? = null, preview: str? = null, placeholder: str? = null, hint: str? = null, error: str? = null, required: bool = false, disabled: bool = false, field: str? = null, size: str = "md", on_change = null, class: str? = null) { style global font_picker_css let id = field ?? `font-picker-${name ?? "font"}` let items = (fonts ?? font_picker_fonts) | map(font_picker_font) let chosen = value != null ? find(items, (f) => f.value == value) : null let described = ui_cls([hint != null && `${id}-hint`, error != null && `${id}-error`, preview != null && `${id}-preview`]) <div class={ui_cls(["ui-font-picker", size == "sm" && "ui-font-picker-sm", error != null && "ui-font-picker-invalid", disabled && "ui-font-picker-disabled", class])}> {label != null && <label.ui-font-picker-label for={id}> {label}{required && <span.ui-font-picker-req aria-hidden="true">*</span>} </label>} <span.ui-font-picker-box> <select id={id} name={name} required={required} disabled={disabled} style={chosen != null ? `font-family: ${font_picker_safe(chosen.stack)}` : null} aria-invalid={error != null ? "true" : null} aria-describedby={described != "" ? described : null} @change={(e) => { on_change && on_change(e.value) }}> {placeholder != null && <option value="" disabled selected={chosen == null}>{placeholder}</option>} {for f in items => <option value={f.value} selected={chosen != null && f.value == chosen.value} style={`font-family: ${font_picker_safe(f.stack)}`}>{f.label}</option>} </select> </span> {preview != null && <p.ui-font-picker-preview id={`${id}-preview`} aria-live="polite" style={chosen != null ? `font-family: ${font_picker_safe(chosen.stack)}` : null}> {preview != "" ? preview : chosen != null ? chosen.label : "No font chosen"} </p>} {hint != null && <p.ui-font-picker-hint id={`${id}-hint`}>{hint}</p>} {error != null && <p.ui-font-picker-error id={`${id}-error`} role="alert">{error}</p>} </div>}Related
BirthdayInputDay, month and year drop-downs that give one date in a hidden input, the days following the month and the years the age allowed.5 views · 7 downloadsCountryPickerA drop-down of every country with its flag and ISO code, favourites first, sending the two-letter code with a form.5 views · 7 downloadsLanguagePickerA drop-down of languages each written in its own name, sending the ISO 639-1 code with a form.5 views · 5 downloadsMultiSelectSeveral choices from a searchable drop-down of checkboxes, shown as chips that come off with ×.5 views · 8 downloadsTimezonePickerA drop-down of time zones grouped by region, each with its UTC offset for today, sending the IANA name with a form.5 views · 5 downloadsCheckboxCardsAny number of choices as cards with a title, a line of detail and a check, each a real checkbox a form sends.5 views · 6 downloads
Thread
Thread 0 posts
On FontPicker: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.