Added browser app
This commit is contained in:
parent
9eb6368b41
commit
d20c1f6b80
19 changed files with 295 additions and 20 deletions
3
public/config/applications.xml
Normal file
3
public/config/applications.xml
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<options>
|
||||||
|
<startup></startup>
|
||||||
|
</options>
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
<options>
|
<options>
|
||||||
<pins>file-explorer,terminal,settings,media-viewer,text-editor,wordle,balls,minesweeper</pins>
|
<pins>file-explorer,terminal,settings,media-viewer,browser,text-editor,wordle,balls,minesweeper</pins>
|
||||||
</options>
|
</options>
|
||||||
|
|
@ -1,21 +1,19 @@
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { forwardRef, useEffect, useState } from "react";
|
||||||
import styles from "./WebView.module.css";
|
import styles from "./WebView.module.css";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} props
|
* @param {Object} props
|
||||||
* @param {String} props.source
|
* @param {String} props.source
|
||||||
*/
|
*/
|
||||||
export function WebView(props) {
|
export const WebView = forwardRef(({ source, focus, ...props }, ref) => {
|
||||||
const [hovered, setHovered] = useState(false);
|
const [hovered, setHovered] = useState(false);
|
||||||
|
|
||||||
const { source, focus } = props;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.focus();
|
window.focus();
|
||||||
|
|
||||||
const onBlur = (event) => {
|
const onBlur = (event) => {
|
||||||
if (hovered) {
|
if (hovered) {
|
||||||
focus(event);
|
focus?.(event);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -38,10 +36,15 @@ export function WebView(props) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.Container} onMouseOver={onMouseOver} onMouseOut={onMouseOut}>
|
<div className={styles.Container} onMouseOver={onMouseOver} onMouseOut={onMouseOut}>
|
||||||
<iframe
|
<iframe
|
||||||
|
ref={ref}
|
||||||
src={source}
|
src={source}
|
||||||
|
title={props.title ?? "Web view"}
|
||||||
className={styles["Web-view"]}
|
className={styles["Web-view"]}
|
||||||
|
referrerPolicy="no-referrer"
|
||||||
|
sandbox="allow-downloads allow-forms allow-modals allow-pointer-lock allow-popups allow-presentation allow-same-origin allow-scripts"
|
||||||
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
@ -11,4 +11,5 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
|
pointer-events: initial;
|
||||||
}
|
}
|
||||||
113
src/components/applications/browser/Browser.jsx
Normal file
113
src/components/applications/browser/Browser.jsx
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import styles from "./Browser.module.css";
|
||||||
|
import { WebView } from "../.templates/WebView.jsx";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { faCaretLeft, faCaretRight, faHome, faRotateRight } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { HOME_URL, SEARCH_URL } from "../../../constants/applications/browser.js";
|
||||||
|
import { isValidUrl } from "../../../features/utils/browser.js";
|
||||||
|
import { useHistory } from "../../../hooks/utils/history.js";
|
||||||
|
import { TITLE_SEPARATOR } from "../../../constants/windows.js";
|
||||||
|
|
||||||
|
/** @type {import("../../windows/WindowView.jsx").windowProps} */
|
||||||
|
export function Browser({ startUrl, focus }) {
|
||||||
|
const initialUrl = startUrl ?? HOME_URL;
|
||||||
|
|
||||||
|
const [url, setUrl] = useState(initialUrl);
|
||||||
|
const [input, setInput] = useState(initialUrl);
|
||||||
|
const { history, pushState, stateIndex, undo, redo, undoAvailable, redoAvailable } = useHistory(initialUrl);
|
||||||
|
const ref = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (history.length === 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
setUrl(history[stateIndex]);
|
||||||
|
}, [history, stateIndex]);
|
||||||
|
|
||||||
|
const reload = () => {
|
||||||
|
if (ref.current == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ref.current.contentWindow.location.href = url;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateUrl = (newUrl) => {
|
||||||
|
if (url === newUrl) {
|
||||||
|
return reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
setUrl(newUrl);
|
||||||
|
setInput(newUrl);
|
||||||
|
pushState(newUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onInputChange = (event) => {
|
||||||
|
setInput(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeyDown = (event) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
|
||||||
|
if (event.key === "Enter" && value !== "") {
|
||||||
|
if (isValidUrl(value)) {
|
||||||
|
updateUrl(value);
|
||||||
|
} else {
|
||||||
|
updateUrl(`${SEARCH_URL}&q=${value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (<div className={styles.Container}>
|
||||||
|
<div className={styles.Header}>
|
||||||
|
<div className={styles["Nav-bar"]}>
|
||||||
|
<button
|
||||||
|
title="Back"
|
||||||
|
tabIndex={0}
|
||||||
|
className={styles["Icon-button"]}
|
||||||
|
onClick={undo}
|
||||||
|
disabled={!undoAvailable}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faCaretLeft}/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
title="Forward"
|
||||||
|
tabIndex={0}
|
||||||
|
className={styles["Icon-button"]}
|
||||||
|
onClick={redo}
|
||||||
|
disabled={!redoAvailable}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faCaretRight}/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
title="Reload"
|
||||||
|
tabIndex={0}
|
||||||
|
className={styles["Icon-button"]}
|
||||||
|
onClick={reload}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faRotateRight}/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
title="Home"
|
||||||
|
tabIndex={0}
|
||||||
|
className={styles["Icon-button"]}
|
||||||
|
onClick={() => { updateUrl(HOME_URL); }}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faHome}/>
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
value={input}
|
||||||
|
type="text"
|
||||||
|
aria-label="Search bar"
|
||||||
|
className={styles["Search-bar"]}
|
||||||
|
tabIndex={0}
|
||||||
|
onChange={onInputChange}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles["Bookmarks"]}>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<WebView ref={ref} source={url} title="Browser" focus={focus}/>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
92
src/components/applications/browser/Browser.module.css
Normal file
92
src/components/applications/browser/Browser.module.css
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
.Container {
|
||||||
|
--header-height: 3.5rem;
|
||||||
|
--nav-bar-height: 2.25rem;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: var(--header-height);
|
||||||
|
background-color: var(--background-color-a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Nav-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: var(--nav-bar-height);
|
||||||
|
padding: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Icon-button {
|
||||||
|
--color: var(--foreground-color-a);
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 1rem;
|
||||||
|
width: auto;
|
||||||
|
padding: 0;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Icon-button::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(255, 255, 255, 0%);
|
||||||
|
border-radius: 9999px;
|
||||||
|
transform: scale(100%);
|
||||||
|
transform-origin: center;
|
||||||
|
transition: all 200ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Icon-button:hover::after,
|
||||||
|
.Icon-button:focus-visible::after {
|
||||||
|
background-color: rgba(255, 255, 255, 10%);
|
||||||
|
transform: scale(175%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Icon-button:disabled {
|
||||||
|
--color: var(--foreground-color-c);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Icon-button svg {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Icon-button svg path {
|
||||||
|
fill: var(--color);
|
||||||
|
transition: fill 100ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Search-bar {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
background-color: var(--background-color-c);
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
outline: none;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.875em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Bookmarks {
|
||||||
|
height: calc(var(--header-height) - var(--nav-bar-height));
|
||||||
|
}
|
||||||
|
|
@ -154,10 +154,18 @@ export function FileExplorer({ startPath, app, modalsManager }) {
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faCaretRight}/>
|
<FontAwesomeIcon icon={faCaretRight}/>
|
||||||
</button>
|
</button>
|
||||||
<button title="Up" tabIndex={0} className={styles["Icon-button"]} onClick={() => { changeDirectory(".."); }}>
|
<button
|
||||||
|
title="Up"
|
||||||
|
tabIndex={0}
|
||||||
|
className={styles["Icon-button"]}
|
||||||
|
onClick={() => { changeDirectory(".."); }}
|
||||||
|
>
|
||||||
<FontAwesomeIcon icon={faArrowUp}/>
|
<FontAwesomeIcon icon={faArrowUp}/>
|
||||||
</button>
|
</button>
|
||||||
<button title="New" tabIndex={0} className={styles["Icon-button"]}
|
<button
|
||||||
|
title="New"
|
||||||
|
tabIndex={0}
|
||||||
|
className={styles["Icon-button"]}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
openWindowedModal({
|
openWindowedModal({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
|
padding-bottom: calc(var(--taskbar-height) + 0.5rem);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,14 +74,12 @@ export const WindowView = memo(({ id, app, size, position, onInteract, options,
|
||||||
setStartPosition(new Vector2(16, 16));
|
setStartPosition(new Vector2(16, 16));
|
||||||
setMaximized(true);
|
setMaximized(true);
|
||||||
} else {
|
} else {
|
||||||
if (position.x > screenWidth) {
|
if (position.x > screenWidth)
|
||||||
position.x = 0;
|
position.x = 0;
|
||||||
setStartPosition(position);
|
if (position.y > screenHeight)
|
||||||
}
|
|
||||||
if (position.y > screenHeight) {
|
|
||||||
position.y = 0;
|
position.y = 0;
|
||||||
setStartPosition(position);
|
|
||||||
}
|
setStartPosition(position);
|
||||||
}
|
}
|
||||||
}, [position, size, screenHeight, screenWidth]);
|
}, [position, size, screenHeight, screenWidth]);
|
||||||
|
|
||||||
|
|
@ -114,7 +112,7 @@ export const WindowView = memo(({ id, app, size, position, onInteract, options,
|
||||||
key={id}
|
key={id}
|
||||||
axis="both"
|
axis="both"
|
||||||
handle={".Window-handle"}
|
handle={".Window-handle"}
|
||||||
defaultPosition={startPosition}
|
defaultPosition={startPosition.round()}
|
||||||
position={null}
|
position={null}
|
||||||
scale={1}
|
scale={1}
|
||||||
bounds={{
|
bounds={{
|
||||||
|
|
@ -127,6 +125,7 @@ export const WindowView = memo(({ id, app, size, position, onInteract, options,
|
||||||
nodeRef={nodeRef}
|
nodeRef={nodeRef}
|
||||||
disabled={maximized}
|
disabled={maximized}
|
||||||
onStart={focus}
|
onStart={focus}
|
||||||
|
grid={[1, 1]}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={classNames.join(" ")}
|
className={classNames.join(" ")}
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,31 @@ import { useWindows } from "../../hooks/windows/windowsContext.js";
|
||||||
import { useWindowsManager } from "../../hooks/windows/windowsManagerContext.js";
|
import { useWindowsManager } from "../../hooks/windows/windowsManagerContext.js";
|
||||||
import { memo, useEffect, useState } from "react";
|
import { memo, useEffect, useState } from "react";
|
||||||
import { WindowView } from "./WindowView.jsx";
|
import { WindowView } from "./WindowView.jsx";
|
||||||
|
import { useSettingsManager } from "../../hooks/settings/settingsManagerContext.js";
|
||||||
|
import { SettingsManager } from "../../features/settings/settingsManager.js";
|
||||||
|
|
||||||
export const WindowsView = memo(() => {
|
export const WindowsView = memo(() => {
|
||||||
|
const settingsManager = useSettingsManager();
|
||||||
const windows = useWindows();
|
const windows = useWindows();
|
||||||
const windowsManager = useWindowsManager();
|
const windowsManager = useWindowsManager();
|
||||||
const [sortedWindows, setSortedWindows] = useState([]);
|
const [sortedWindows, setSortedWindows] = useState([]);
|
||||||
|
|
||||||
|
// Sort windows
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSortedWindows([...windows].sort((windowA, windowB) =>
|
setSortedWindows([...windows].sort((windowA, windowB) =>
|
||||||
windowA.lastInteraction - windowB.lastInteraction
|
windowA.lastInteraction - windowB.lastInteraction
|
||||||
));
|
));
|
||||||
}, [windows]);
|
}, [windows]);
|
||||||
|
|
||||||
|
// Launch startup apps
|
||||||
|
useEffect(() => {
|
||||||
|
const settings = settingsManager.get(SettingsManager.VIRTUAL_PATHS.applications);
|
||||||
|
settings.get("startup", (value) => {
|
||||||
|
if (value !== "")
|
||||||
|
windowsManager.startup(value?.split(","));
|
||||||
|
});
|
||||||
|
}, [settingsManager, windowsManager]);
|
||||||
|
|
||||||
// TO DO: prevent windows from being rerendered when order is changed
|
// TO DO: prevent windows from being rerendered when order is changed
|
||||||
|
|
||||||
return (<div>
|
return (<div>
|
||||||
|
|
|
||||||
2
src/constants/applications/browser.js
Normal file
2
src/constants/applications/browser.js
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export const HOME_URL = "https://os.prozilla.dev/";
|
||||||
|
export const SEARCH_URL = "https://www.google.com/search?igu=1";
|
||||||
|
|
@ -8,6 +8,7 @@ import { Settings } from "../../components/applications/settings/Settings.jsx";
|
||||||
// import { Calculator } from "../../components/applications/calculator/Calculator.jsx";
|
// import { Calculator } from "../../components/applications/calculator/Calculator.jsx";
|
||||||
import Vector2 from "../math/vector2.js";
|
import Vector2 from "../math/vector2.js";
|
||||||
import { APPS } from "../../constants/applications.js";
|
import { APPS } from "../../constants/applications.js";
|
||||||
|
import { Browser } from "../../components/applications/browser/Browser.jsx";
|
||||||
|
|
||||||
export default class AppsManager {
|
export default class AppsManager {
|
||||||
static APPLICATIONS = [
|
static APPLICATIONS = [
|
||||||
|
|
@ -31,6 +32,9 @@ export default class AppsManager {
|
||||||
source: "https://prozilla.dev/minesweeper",
|
source: "https://prozilla.dev/minesweeper",
|
||||||
size: new Vector2(500, 580)
|
size: new Vector2(500, 580)
|
||||||
}),
|
}),
|
||||||
|
new Application("Browser", "browser", Browser, {
|
||||||
|
size: new Vector2(700, 500)
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -5,4 +5,13 @@ export default class Vector2 {
|
||||||
this.x = x;
|
this.x = x;
|
||||||
this.y = y ?? x;
|
this.y = y ?? x;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Vector2}
|
||||||
|
*/
|
||||||
|
round() {
|
||||||
|
this.x = Math.round(this.x);
|
||||||
|
this.y = Math.round(this.y);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -7,7 +7,8 @@ export class SettingsManager {
|
||||||
*/
|
*/
|
||||||
static VIRTUAL_PATHS = {
|
static VIRTUAL_PATHS = {
|
||||||
desktop: "~/.config/desktop.xml",
|
desktop: "~/.config/desktop.xml",
|
||||||
taskbar: "~/.config/taskbar.xml"
|
taskbar: "~/.config/taskbar.xml",
|
||||||
|
applications: "~/.config/applications.xml",
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -17,3 +17,16 @@ export function reloadViewport(bypassCache = false) {
|
||||||
console.info("Reloading viewport");
|
console.info("Reloading viewport");
|
||||||
window.location.reload(bypassCache);
|
window.location.reload(bypassCache);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} string
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function isValidUrl(string) {
|
||||||
|
try {
|
||||||
|
new URL(string);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -41,6 +41,8 @@ export function loadDefaultData(virtualRoot) {
|
||||||
file.setSource("/config/desktop.xml");
|
file.setSource("/config/desktop.xml");
|
||||||
}).createFile("taskbar", "xml", (file) => {
|
}).createFile("taskbar", "xml", (file) => {
|
||||||
file.setSource("/config/taskbar.xml");
|
file.setSource("/config/taskbar.xml");
|
||||||
|
}).createFile("applications", "xml", (file) => {
|
||||||
|
file.setSource("/config/applications.xml");
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.createFolder("Pictures", (folder) => {
|
.createFolder("Pictures", (folder) => {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ export default class WindowsManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.windows = {};
|
this.windows = {};
|
||||||
this.updateWindows = () => {};
|
this.updateWindows = () => {};
|
||||||
|
this.startupComplete = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -184,6 +185,17 @@ export default class WindowsManager {
|
||||||
this.updateWindows = updateWindows;
|
this.updateWindows = updateWindows;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
startup(appIds) {
|
||||||
|
if (appIds == null || this.startupComplete)
|
||||||
|
return;
|
||||||
|
|
||||||
|
appIds.forEach((appId) => {
|
||||||
|
this.open(appId);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.startupComplete = true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {string[]}
|
* @returns {string[]}
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,6 @@ export function useHistory(initialState) {
|
||||||
return state !== newHistory[index + 1];
|
return state !== newHistory[index + 1];
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(newHistory);
|
|
||||||
|
|
||||||
setHistory(newHistory);
|
setHistory(newHistory);
|
||||||
setStateIndex(0);
|
setStateIndex(0);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,11 @@ body {
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
*, :after, :before {
|
||||||
color: var(--foreground-color-a);
|
color: var(--foreground-color-a);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
text-rendering: optimizelegibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
p, a, button, input, h1, h2, h3, h4, h5, h6 {
|
p, a, button, input, h1, h2, h3, h4, h5, h6 {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue