1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Custom terminal fonts support (#5414)

Fixes #5132 #5133 
- bundled 7 new monospaced fonts for terminal
- fix: refresh terminal font after changing in the preferences / keep cluster iframe accessible in DOM while not active/focused
- display terminal's custom font preview with font-name in the select-box + live-preload for current `fontSize`, fix lint

* Fixes for <Input/>:
- remove duplicated error messages for sync validators
- don't propagate invalid values on change (uncontrolled components only)
- more informative error message for numeric input with min/max info
This commit is contained in:
Roman 2022-06-06 18:03:12 +03:00 committed by GitHub
parent fae6bff6fb
commit 55a977554d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 325 additions and 146 deletions

View File

@ -408,7 +408,6 @@
"typed-emitter": "^1.4.0", "typed-emitter": "^1.4.0",
"typedoc": "0.22.17", "typedoc": "0.22.17",
"typedoc-plugin-markdown": "^3.11.12", "typedoc-plugin-markdown": "^3.11.12",
"typeface-roboto": "^1.1.13",
"typescript": "^4.5.5", "typescript": "^4.5.5",
"typescript-plugin-css-modules": "^3.4.0", "typescript-plugin-css-modules": "^3.4.0",
"webpack": "^5.72.0", "webpack": "^5.72.0",

View File

@ -771,6 +771,7 @@ exports[`preferences - navigation to terminal preferences given in preferences,
> >
<input <input
class="input box grow" class="input box grow"
max="50"
min="10" min="10"
spellcheck="false" spellcheck="false"
type="number" type="number"
@ -790,22 +791,78 @@ exports[`preferences - navigation to terminal preferences given in preferences,
</div> </div>
<div <div
class="Input theme round black" class="Select theme-lens css-b62m3t-container"
> >
<label <span
class="input-area flex gaps align-center" class="css-1f43avz-a11yText-A11yText"
id="" id="react-select-2-live-region"
>
<input
class="input box grow"
spellcheck="false"
type="text"
value=""
/>
</label>
<div
class="input-info flex gaps"
/> />
<span
aria-atomic="false"
aria-live="polite"
aria-relevant="additions text"
class="css-1f43avz-a11yText-A11yText"
/>
<div
class="Select__control css-1s2u09g-control"
>
<div
class="Select__value-container css-319lph-ValueContainer"
>
<div
class="Select__placeholder css-14el2xx-placeholder"
id="react-select-2-placeholder"
>
Select...
</div>
<div
class="Select__input-container css-6j8wv5-Input"
data-value=""
>
<input
aria-autocomplete="list"
aria-describedby="react-select-2-placeholder"
aria-expanded="false"
aria-haspopup="true"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
class="Select__input"
id="react-select-2-input"
role="combobox"
spellcheck="false"
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
tabindex="0"
type="text"
value=""
/>
</div>
</div>
<div
class="Select__indicators css-1hb7zxy-IndicatorsContainer"
>
<span
class="Select__indicator-separator css-1okebmr-indicatorSeparator"
/>
<div
aria-hidden="true"
class="Select__indicator Select__dropdown-indicator css-tlfecz-indicatorContainer"
>
<svg
aria-hidden="true"
class="css-tj5bde-Svg"
focusable="false"
height="20"
viewBox="0 0 20 20"
width="20"
>
<path
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
/>
</svg>
</div>
</div>
</div>
</div> </div>
</section> </section>
</section> </section>

View File

@ -66,7 +66,7 @@
.notes { .notes {
white-space: pre-line; white-space: pre-line;
font-family: "RobotoMono", monospace; font-family: var(--font-monospace);
font-size: small; font-size: small;
} }

View File

@ -4,18 +4,20 @@
*/ */
import React from "react"; import React from "react";
import { action } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import type { UserStore } from "../../../common/user-store"; import type { UserStore } from "../../../common/user-store";
import { SubTitle } from "../layout/sub-title"; import { SubTitle } from "../layout/sub-title";
import { Input, InputValidators } from "../input"; import { Input } from "../input";
import { Switch } from "../switch"; import { Switch } from "../switch";
import { Select } from "../select"; import { Select, type SelectOption } from "../select";
import type { ThemeStore } from "../../themes/store"; import type { ThemeStore } from "../../themes/store";
import { Preferences } from "./preferences"; import { Preferences } from "./preferences";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import userStoreInjectable from "../../../common/user-store/user-store.injectable"; import userStoreInjectable from "../../../common/user-store/user-store.injectable";
import themeStoreInjectable from "../../themes/store.injectable"; import themeStoreInjectable from "../../themes/store.injectable";
import defaultShellInjectable from "./default-shell.injectable"; import defaultShellInjectable from "./default-shell.injectable";
import logger from "../../../common/logger";
interface Dependencies { interface Dependencies {
userStore: UserStore; userStore: UserStore;
@ -23,11 +25,12 @@ interface Dependencies {
defaultShell: string; defaultShell: string;
} }
const NonInjectedTerminal = observer(({ const NonInjectedTerminal = observer((
userStore, {
themeStore, userStore,
defaultShell, themeStore,
}: Dependencies) => { defaultShell,
}: Dependencies) => {
const themeOptions = [ const themeOptions = [
{ {
value: "", // TODO: replace with a sentinal value that isn't string (and serialize it differently) value: "", // TODO: replace with a sentinal value that isn't string (and serialize it differently)
@ -39,6 +42,26 @@ const NonInjectedTerminal = observer(({
})), })),
]; ];
// fonts must be declared in `fonts.scss` and at `template.html` (if early-preloading required)
const supportedCustomFonts: SelectOption<string>[] = [
"RobotoMono", "Anonymous Pro", "IBM Plex Mono", "JetBrains Mono", "Red Hat Mono",
"Source Code Pro", "Space Mono", "Ubuntu Mono",
].map(customFont => {
const { fontFamily, fontSize } = userStore.terminalConfig;
return {
label: <span style={{ fontFamily: customFont, fontSize }}>{customFont}</span>,
value: customFont,
isSelected: fontFamily === customFont,
};
});
const onFontFamilyChange = action(({ value: fontFamily }: SelectOption<string>) => {
logger.info(`setting terminal font to ${fontFamily}`);
userStore.terminalConfig.fontFamily = fontFamily; // save to external storage
});
return ( return (
<Preferences data-testid="terminal-preferences-page"> <Preferences data-testid="terminal-preferences-page">
<section> <section>
@ -49,7 +72,7 @@ const NonInjectedTerminal = observer(({
<Input <Input
theme="round-black" theme="round-black"
placeholder={defaultShell} placeholder={defaultShell}
value={userStore.shell} value={userStore.shell ?? ""}
onChange={(value) => userStore.shell = value} onChange={(value) => userStore.shell = value}
/> />
</section> </section>
@ -81,18 +104,19 @@ const NonInjectedTerminal = observer(({
theme="round-black" theme="round-black"
type="number" type="number"
min={10} min={10}
validators={InputValidators.isNumber} max={50}
value={userStore.terminalConfig.fontSize.toString()} defaultValue={userStore.terminalConfig.fontSize.toString()}
onChange={(value) => userStore.terminalConfig.fontSize = Number(value)} onChange={(value) => userStore.terminalConfig.fontSize = Number(value)}
/> />
</section> </section>
<section> <section>
<SubTitle title="Font family" /> <SubTitle title="Font family" />
<Input <Select
theme="round-black" themeName="lens"
type="text" controlShouldRenderValue
value={userStore.terminalConfig.fontFamily} value={userStore.terminalConfig.fontFamily}
onChange={(value) => userStore.terminalConfig.fontFamily = value} options={supportedCustomFonts}
onChange={onFontFamilyChange as any}
/> />
</section> </section>
</section> </section>

View File

@ -12,12 +12,14 @@
@import "./fonts"; @import "./fonts";
:root { :root {
--flex-gap: #{$padding};
--unit: 8px; --unit: 8px;
--padding: var(--unit); --padding: var(--unit);
--margin: var(--unit); --margin: var(--unit);
--border-radius: 3px; --border-radius: 3px;
--font-main: 'Roboto', 'Helvetica', 'Arial', sans-serif; --font-main: 'Roboto', 'Helvetica', 'Arial', sans-serif;
--font-monospace: Lucida Console, Monaco, Consolas, monospace; --font-monospace: Lucida Console, Monaco, Consolas, monospace; // some defaults
--font-terminal: var(--font-monospace); // overridden in terminal.ts, managed by common/user-store.ts
--font-size-small: calc(1.5 * var(--unit)); --font-size-small: calc(1.5 * var(--unit));
--font-size: calc(1.75 * var(--unit)); --font-size: calc(1.75 * var(--unit));
--font-size-big: calc(2 * var(--unit)); --font-size-big: calc(2 * var(--unit));
@ -63,16 +65,13 @@
color: var(--textColorAccent); color: var(--textColorAccent);
} }
html {
font-size: 62.5%; // 1 rem == 10px
color: var(--textColorPrimary);
background-color: var(--mainBackground);
--flex-gap: #{$padding};
}
html, body { html, body {
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
color: var(--textColorPrimary);
background-color: var(--mainBackground);
font-size: var(--font-size);
font-family: var(--font-main);
} }
#terminal-init { #terminal-init {
@ -93,10 +92,6 @@ html, body {
} }
} }
body {
font: $font-size $font-main;
}
fieldset { fieldset {
border: 0; border: 0;
padding: 0; padding: 0;
@ -227,6 +222,22 @@ a {
} }
} }
#fonts-preloading {
> span {
position: absolute;
visibility: hidden;
height: 0;
&:before {
width: 0;
display: block;
overflow: hidden;
content: "text-example"; // some text required to start applying/rendering font in document
font-family: inherit; // font-family must be specified via style="" (see: template.html)
}
}
}
// app's common loading indicator, displaying on the route transitions // app's common loading indicator, displaying on the route transitions
#loading { #loading {
position: absolute; position: absolute;

View File

@ -45,7 +45,6 @@ export class ClusterFrameHandler {
iframe.id = `cluster-frame-${cluster.id}`; iframe.id = `cluster-frame-${cluster.id}`;
iframe.name = cluster.contextName; iframe.name = cluster.contextName;
iframe.style.display = "none";
iframe.setAttribute("src", getClusterFrameUrl(clusterId)); iframe.setAttribute("src", getClusterFrameUrl(clusterId));
iframe.addEventListener("load", action(() => { iframe.addEventListener("load", action(() => {
logger.info(`[LENS-VIEW]: frame for clusterId=${clusterId} has loaded`); logger.info(`[LENS-VIEW]: frame for clusterId=${clusterId} has loaded`);
@ -95,7 +94,7 @@ export class ClusterFrameHandler {
ipcRenderer.send(clusterVisibilityHandler); ipcRenderer.send(clusterVisibilityHandler);
for (const { frame: view } of this.views.values()) { for (const { frame: view } of this.views.values()) {
view.style.display = "none"; view.classList.add("hidden");
} }
const cluster = clusterId const cluster = clusterId
@ -113,9 +112,9 @@ export class ClusterFrameHandler {
return undefined; return undefined;
}, },
(view) => { (view: LensView) => {
logger.info(`[LENS-VIEW]: cluster id=${clusterId} should now be visible`); logger.info(`[LENS-VIEW]: cluster id=${clusterId} should now be visible`);
view.frame.style.display = "flex"; view.frame.classList.remove("hidden");
ipcRenderer.send(clusterVisibilityHandler, clusterId); ipcRenderer.send(clusterVisibilityHandler, clusterId);
}, },
); );

View File

@ -35,7 +35,20 @@
background-color: var(--mainBackground); background-color: var(--mainBackground);
iframe { iframe {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: flex;
flex: 1; flex: 1;
// when updating font settings in the "Preferences -> Terminal" cluster's iframe
// must be accessible in DOM (e.g. elem.getBoundingClientRect() must work)
&.hidden {
opacity: 0;
pointer-events: none;
}
} }
} }
} }

View File

@ -64,16 +64,30 @@ export class Terminal {
} }
} }
constructor(protected readonly dependencies: TerminalDependencies, { tabId, api }: TerminalArguments) { get fontFamily() {
return this.dependencies.terminalConfig.get().fontFamily;
}
get fontSize() {
return this.dependencies.terminalConfig.get().fontSize;
}
get theme(): Record<string/*paramName*/, string/*color*/> {
return this.dependencies.themeStore.xtermColors;
}
constructor(protected readonly dependencies: TerminalDependencies, {
tabId,
api,
}: TerminalArguments) {
this.tabId = tabId; this.tabId = tabId;
this.api = api; this.api = api;
const { fontSize, fontFamily } = this.dependencies.terminalConfig.get();
this.xterm = new XTerm({ this.xterm = new XTerm({
cursorBlink: true, cursorBlink: true,
cursorStyle: "bar", cursorStyle: "bar",
fontSize, fontSize: this.fontSize,
fontFamily, fontFamily: this.fontFamily,
}); });
// enable terminal addons // enable terminal addons
this.xterm.loadAddon(this.fitAddon); this.xterm.loadAddon(this.fitAddon);
@ -95,17 +109,11 @@ export class Terminal {
window.addEventListener("resize", this.onResize); window.addEventListener("resize", this.onResize);
this.disposer.push( this.disposer.push(
reaction(() => this.dependencies.themeStore.xtermColors, colors => { reaction(() => this.theme, colors => this.xterm.setOption("theme", colors), {
this.xterm?.setOption("theme", colors);
}, {
fireImmediately: true,
}),
reaction(() => this.dependencies.terminalConfig.get().fontSize, this.setFontSize, {
fireImmediately: true,
}),
reaction(() => this.dependencies.terminalConfig.get().fontFamily, this.setFontFamily, {
fireImmediately: true, fireImmediately: true,
}), }),
reaction(() => this.fontSize, this.setFontSize, { fireImmediately: true }),
reaction(() => this.fontFamily, this.setFontFamily, { fireImmediately: true }),
() => onDataHandler.dispose(), () => onDataHandler.dispose(),
() => this.fitAddon.dispose(), () => this.fitAddon.dispose(),
() => this.api.removeAllListeners(), () => this.api.removeAllListeners(),
@ -120,15 +128,14 @@ export class Terminal {
} }
fit = () => { fit = () => {
// Since this function is debounced we need to read this value as late as possible
if (!this.xterm) {
return;
}
try { try {
this.fitAddon.fit(); const { cols, rows } = this.fitAddon.proposeDimensions();
const { cols, rows } = this.xterm;
// attempt to resize/fit terminal when it's not visible in DOM will crash with exception
// see: https://github.com/xtermjs/xterm.js/issues/3118
if (isNaN(cols) || isNaN(rows)) return;
this.fitAddon.fit();
this.api.sendTerminalSize(cols, rows); this.api.sendTerminalSize(cols, rows);
} catch (error) { } catch (error) {
// see https://github.com/lensapp/lens/issues/1891 // see https://github.com/lensapp/lens/issues/1891
@ -197,12 +204,21 @@ export class Terminal {
} }
}; };
setFontSize = (size: number) => { setFontSize = (fontSize: number) => {
this.xterm.options.fontSize = size; logger.info(`[TERMINAL]: set fontSize to ${fontSize}`);
this.xterm.options.fontSize = fontSize;
this.fit();
}; };
setFontFamily = (family: string) => { setFontFamily = (fontFamily: string) => {
this.xterm.options.fontFamily = family; logger.info(`[TERMINAL]: set fontFamily to ${fontFamily}`);
this.xterm.options.fontFamily = fontFamily;
this.fit();
// provide css-variable within `:root {}`
document.documentElement.style.setProperty("--font-terminal", fontFamily);
}; };
keyHandler = (evt: KeyboardEvent): boolean => { keyHandler = (evt: KeyboardEvent): boolean => {

View File

@ -3,34 +3,99 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
// Custom fonts // App's main font
@import "~typeface-roboto/index.css"; // Downloaded from: https://fonts.google.com/specimen/Roboto
@font-face {
font-family: "Roboto";
src: url("../fonts/Roboto-Light.ttf") format("truetype");
font-display: swap;
font-weight: 300; // "light"
}
// Material Design Icons, used primarily in icon.tsx @font-face {
// Latest: https://github.com/google/material-design-icons/tree/master/font font-family: "Roboto";
src: url("../fonts/Roboto-LightItalic.ttf") format("truetype");
font-display: swap;
font-weight: 300; // "light" + italic
font-style: italic;
}
@font-face {
font-family: "Roboto";
src: url("../fonts/Roboto-Regular.ttf") format("truetype");
font-display: swap;
font-weight: 400; // "normal"
}
@font-face {
font-family: "Roboto";
src: url("../fonts/Roboto-Bold.ttf") format("truetype");
font-display: swap;
font-weight: 700; // "bold"
}
// Icon fonts, see: `icon.tsx`
// Latest version for manual update: https://github.com/google/material-design-icons/tree/master/font
@font-face { @font-face {
font-family: "Material Icons"; font-family: "Material Icons";
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: block; font-display: block;
src: url("./fonts/MaterialIcons-Regular.ttf") format("truetype"); src: url("../fonts/MaterialIcons-Regular.ttf") format("truetype");
}
// Terminal fonts (monospaced)
// Source: https://fonts.google.com/?category=Monospace
@font-face {
font-family: "Anonymous Pro";
src: local("Anonymous Pro"), url("../fonts/AnonymousPro-Regular.ttf") format("truetype");
font-display: block;
}
@font-face {
font-family: "IBM Plex Mono";
src: local("IBM Plex Mono"), url("../fonts/IBMPlexMono-Regular.ttf") format("truetype");
font-display: block;
}
@font-face {
font-family: "JetBrains Mono";
src: local("JetBrains Mono"), url("../fonts/JetBrainsMono-Regular.ttf") format("truetype");
font-display: block;
}
@font-face {
font-family: "Red Hat Mono";
src: local("Red Hat Mono"), url("../fonts/RedHatMono-Regular.ttf") format("truetype");
font-display: block;
}
@font-face {
font-family: "Source Code Pro";
src: local("Source Code Pro"), url("../fonts/SourceCodePro-Regular.ttf") format("truetype");
font-display: block;
}
@font-face {
font-family: "Space Mono";
src: local("Space Mono"), url("../fonts/SpaceMono-Regular.ttf") format("truetype");
font-display: block;
}
@font-face {
font-family: "Ubuntu Mono";
src: local("Ubuntu Mono"), url("../fonts/UbuntuMono-Regular.ttf") format("truetype");
font-display: block;
} }
// Patched RobotoMono font with icons // Patched RobotoMono font with icons
// RobotoMono Windows Compatible for using in terminal // RobotoMono Windows Compatible for using in terminal
// https://github.com/ryanoasis/nerd-fonts/tree/master/patched-fonts/RobotoMono // https://github.com/ryanoasis/nerd-fonts/tree/master/patched-fonts/RobotoMono
@font-face { @font-face {
font-family: 'RobotoMono'; font-family: "RobotoMono";
src: local("RobotoMono"), url("../fonts/Roboto-Mono-nerd.ttf") format("truetype");
font-display: block; font-display: block;
src: local('RobotoMono'), url('./fonts/roboto-mono-nerd.ttf') format('truetype');
}
#fonts-preloading {
> .icons {
@include font-preload("Material Icons");
}
> .terminal {
@include font-preload("RobotoMono");
}
} }

View File

@ -14,7 +14,6 @@ import { Tooltip } from "../tooltip";
import * as Validators from "./input_validators"; import * as Validators from "./input_validators";
import type { InputValidator } from "./input_validators"; import type { InputValidator } from "./input_validators";
import isFunction from "lodash/isFunction"; import isFunction from "lodash/isFunction";
import isBoolean from "lodash/isBoolean";
import uniqueId from "lodash/uniqueId"; import uniqueId from "lodash/uniqueId";
import { debounce } from "lodash"; import { debounce } from "lodash";
@ -24,7 +23,10 @@ export { InputValidators };
export type { InputValidator }; export type { InputValidator };
type InputElement = HTMLInputElement | HTMLTextAreaElement; type InputElement = HTMLInputElement | HTMLTextAreaElement;
type InputElementProps = InputHTMLAttributes<HTMLInputElement> & TextareaHTMLAttributes<HTMLTextAreaElement> & DOMAttributes<InputElement>; type InputElementProps =
InputHTMLAttributes<HTMLInputElement>
& TextareaHTMLAttributes<HTMLTextAreaElement>
& DOMAttributes<InputElement>;
export interface IconDataFnArg { export interface IconDataFnArg {
isDirty: boolean; isDirty: boolean;
@ -173,22 +175,18 @@ export class Input extends React.Component<InputProps, State> {
error => this.getValidatorError(value, validator) || error, error => this.getValidatorError(value, validator) || error,
), ),
); );
} else {
if (!validator.validate(value, this.props)) {
errors.push(this.getValidatorError(value, validator));
}
} }
const result = validator.validate(value, this.props); const isValid = validator.validate(value, this.props);
if (isBoolean(result) && !result) { if (isValid === false) {
errors.push(this.getValidatorError(value, validator)); errors.push(this.getValidatorError(value, validator));
} else if (result instanceof Promise) { } else if (isValid instanceof Promise) {
if (!validationId) { if (!validationId) {
this.validationId = validationId = uniqueId("validation_id_"); this.validationId = validationId = uniqueId("validation_id_");
} }
asyncValidators.push( asyncValidators.push(
result.then( isValid.then(
() => null, // don't consider any valid result from promise since we interested in errors only () => null, // don't consider any valid result from promise since we interested in errors only
error => this.getValidatorError(value, validator) || error, error => this.getValidatorError(value, validator) || error,
), ),
@ -266,17 +264,25 @@ export class Input extends React.Component<InputProps, State> {
setDirtyOnChange = debounce(() => this.setDirty(), 500); setDirtyOnChange = debounce(() => this.setDirty(), 500);
onChange(evt: React.ChangeEvent<any>) { async onChange(evt: React.ChangeEvent<any>) {
this.props.onChange?.(evt.currentTarget.value, evt); const newValue = evt.currentTarget.value;
this.validate(); const eventCopy = { ...evt };
this.autoFitHeight(); this.autoFitHeight();
this.setDirtyOnChange(); this.setDirtyOnChange();
// re-render component when used as uncontrolled input // Handle uncontrolled components (`props.defaultValue` must be used instead `value`)
// when used @defaultValue instead of @value changing real input.value doesn't call render() if (this.isUncontrolled) {
if (this.isUncontrolled && this.showMaxLenIndicator) { // update DOM since render() is not called on input's changes with uncontrolled inputs
this.forceUpdate(); if (this.showMaxLenIndicator) this.forceUpdate();
// don't propagate changes for invalid values
await this.validate();
if (!this.state.valid) return; // skip
} }
// emit new value update
this.props.onChange?.(newValue, eventCopy);
} }
onKeyDown(evt: React.KeyboardEvent<InputElement>) { onKeyDown(evt: React.KeyboardEvent<InputElement>) {
@ -299,7 +305,7 @@ export class Input extends React.Component<InputProps, State> {
this.setDirty(); this.setDirty();
} }
if(this.props.blurOnEnter){ if (this.props.blurOnEnter) {
//pressing enter indicates that the edit is complete, we can unfocus now //pressing enter indicates that the edit is complete, we can unfocus now
this.blur(); this.blur();
} }
@ -379,7 +385,6 @@ export class Input extends React.Component<InputProps, State> {
multiLine, showValidationLine, validators, theme, maxRows, children, showErrorsAsTooltip, multiLine, showValidationLine, validators, theme, maxRows, children, showErrorsAsTooltip,
maxLength, rows, disabled, autoSelectOnFocus, iconLeft, iconRight, contentRight, id, maxLength, rows, disabled, autoSelectOnFocus, iconLeft, iconRight, contentRight, id,
dirty: _dirty, // excluded from passing to input-element dirty: _dirty, // excluded from passing to input-element
defaultValue,
trim, trim,
blurOnEnter, blurOnEnter,
...inputProps ...inputProps

View File

@ -8,7 +8,8 @@ import type { ReactNode } from "react";
import fse from "fs-extra"; import fse from "fs-extra";
import { TypedRegEx } from "typed-regex"; import { TypedRegEx } from "typed-regex";
export class AsyncInputValidationError extends Error {} export class AsyncInputValidationError extends Error {
}
export type InputValidator<IsAsync extends boolean> = { export type InputValidator<IsAsync extends boolean> = {
/** /**
@ -32,7 +33,7 @@ export type InputValidator<IsAsync extends boolean> = {
message?: undefined; message?: undefined;
debounce: number; debounce: number;
} }
); );
export function inputValidator<IsAsync extends boolean = false>(validator: InputValidator<IsAsync>): InputValidator<IsAsync> { export function inputValidator<IsAsync extends boolean = false>(validator: InputValidator<IsAsync>): InputValidator<IsAsync> {
return validator; return validator;
@ -52,7 +53,14 @@ export const isEmail = inputValidator({
export const isNumber = inputValidator({ export const isNumber = inputValidator({
condition: ({ type }) => type === "number", condition: ({ type }) => type === "number",
message: () => `Invalid number`, message(value, { min, max }) {
const minMax: string = [
typeof min === "number" ? `min: ${min}` : undefined,
typeof max === "number" ? `max: ${max}` : undefined,
].filter(Boolean).join(", ");
return `Invalid number${minMax ? ` (${minMax})` : ""}`;
},
validate: (value, { min, max }) => { validate: (value, { min, max }) => {
const numVal = +value; const numVal = +value;

View File

@ -59,20 +59,3 @@
@content; // css-modules (*.module.scss) @content; // css-modules (*.module.scss)
} }
} }
// Makes custom @font-family available at earlier stages.
// Element must exist in DOM as soon as possible to initiate preloading.
@mixin font-preload($fontFamily) {
position: absolute;
visibility: hidden;
height: 0;
&:before {
width: 0;
display: block;
overflow: hidden;
content: "x"; // some text required to start applying font in document
font-family: $fontFamily; // imported name in @font-face declaration
@content;
}
}

View File

@ -22,7 +22,7 @@ const { Menu } = components;
export interface SelectOption<Value> { export interface SelectOption<Value> {
value: Value; value: Value;
label: string; label: React.ReactNode;
isDisabled?: boolean; isDisabled?: boolean;
isSelected?: boolean; isSelected?: boolean;
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,17 +1,21 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
</head> </head>
<body> <body>
<div id="app"></div>
<div id="app"></div> <div id="fonts-preloading">
<div id="terminal-init"></div> <span style="font-family: 'Material Icons'" />
<span style="font-family: 'RobotoMono'" />
<div id="fonts-preloading"> <span style="font-family: 'Anonymous Pro'" />
<i class="icons"><!--material icons--></i> <span style="font-family: 'IBM Plex Mono'" />
<i class="terminal"><!--roboto mono--></i> <span style="font-family: 'JetBrains Mono'" />
</div> <span style="font-family: 'Red Hat Mono'" />
<span style="font-family: 'Source Code Pro'" />
</body> <span style="font-family: 'Space Mono'" />
<span style="font-family: 'Ubuntu Mono'" />
</div>
<div id="terminal-init"></div>
</body>
</html> </html>

View File

@ -13012,11 +13012,6 @@ typedoc@0.22.17:
minimatch "^5.1.0" minimatch "^5.1.0"
shiki "^0.10.1" shiki "^0.10.1"
typeface-roboto@^1.1.13:
version "1.1.13"
resolved "https://registry.yarnpkg.com/typeface-roboto/-/typeface-roboto-1.1.13.tgz#9c4517cb91e311706c74823e857b4bac9a764ae5"
integrity sha512-YXvbd3a1QTREoD+FJoEkl0VQNJoEjewR2H11IjVv4bp6ahuIcw0yyw/3udC4vJkHw3T3cUh85FTg8eWef3pSaw==
typescript-plugin-css-modules@^3.4.0: typescript-plugin-css-modules@^3.4.0:
version "3.4.0" version "3.4.0"
resolved "https://registry.yarnpkg.com/typescript-plugin-css-modules/-/typescript-plugin-css-modules-3.4.0.tgz#4ff6905d88028684d1608c05c62cb6346e5548cc" resolved "https://registry.yarnpkg.com/typescript-plugin-css-modules/-/typescript-plugin-css-modules-3.4.0.tgz#4ff6905d88028684d1608c05c62cb6346e5548cc"