Replace popup with React

This commit is contained in:
sienori 2019-02-23 00:21:55 +09:00
parent 4028acda0c
commit a73359094a
17 changed files with 541 additions and 529 deletions

6
src/common/openUrl.js Normal file
View file

@ -0,0 +1,6 @@
import browser from "webextension-polyfill";
export default async url => {
const activeTab = (await browser.tabs.query({ currentWindow: true, active: true }))[0];
browser.tabs.create({ url: url, index: activeTab.index + 1 });
};

View file

@ -65,6 +65,15 @@ const formatResult = result => {
export default async (sourceWord, sourceLang = "auto", targetLang) => {
sourceWord = sourceWord.trim();
if (sourceWord === "")
return {
resultText: "",
candidateText: "",
sourceLanguage: "en",
percentage: 0,
statusText: "OK"
};
const history = getHistory(sourceWord, sourceLang, targetLang);
if (history) return history.result;

View file

@ -0,0 +1,50 @@
import React, { Component } from "react";
import browser from "webextension-polyfill";
import genelateLangOptions from "src/common/genelateLangOptions";
import openUrl from "src/common/openUrl";
import "../styles/Footer.scss";
export default class Footer extends Component {
constructor(props) {
super(props);
this.langList = genelateLangOptions();
}
handleLinkClick = async () => {
const { tabUrl, targetLang } = this.props;
const encodedUrl = encodeURIComponent(tabUrl);
const translateUrl = `https://translate.google.com/translate?hl=${targetLang}&sl=auto&u=${encodedUrl}`;
openUrl(translateUrl);
};
handleChange = e => {
const lang = e.target.value;
this.props.handleLangChange(lang);
};
render() {
const { tabUrl, targetLang } = this.props;
return (
<div id="footer">
<div className="translateLink">
{tabUrl && <a onClick={this.handleLinkClick}>{browser.i18n.getMessage("showLink")}</a>}
</div>
<div className="selectWrap">
<select
id="langList"
value={targetLang}
onChange={this.handleChange}
title={browser.i18n.getMessage("targetLangLabel")}
>
{this.langList.map(option => (
<option value={option.value} key={option.value}>
{option.name}
</option>
))}
</select>
</div>
</div>
);
}
}

View file

@ -0,0 +1,42 @@
import React from "react";
import browser from "webextension-polyfill";
import browserInfo from "browser-info";
import openUrl from "src/common/openUrl";
import HeartIcon from "../icons/heart.svg";
import SettingsIcon from "../icons/settings.svg";
import "../styles/header.scss";
//TODO: 次のタブで開く
const openPayPal = () => {
const isChrome = browserInfo().name === "Chrome";
const url = `https://www.paypal.com/cgi-bin/webscr?cmd=_xclick&no_shipping=1&business=sienori.firefox@gmail.com&item_name=SimpleTranslate ${
isChrome ? "for Chrome " : ""
}- Donation`;
openUrl(url);
};
const openSettings = () => {
const url = "../options/index.html#settings";
openUrl(url);
};
export default () => (
<div id="header">
<div className="title">Simple Translate</div>
<div className="rightButtons">
<button
className="heartButton"
onClick={openPayPal}
title={browser.i18n.getMessage("donateWithPaypalLabel")}
>
<HeartIcon />
</button>
<button
className={"settingsButton"}
onClick={openSettings}
title={browser.i18n.getMessage("settingsLabel")}
>
<SettingsIcon />
</button>
</div>
</div>
);

View file

@ -0,0 +1,33 @@
import React, { Component } from "react";
import ReactDOM from "react-dom";
import browser from "webextension-polyfill";
import "../styles/inputArea.scss";
export default class InputArea extends Component {
resizeTextArea = () => {
const textarea = ReactDOM.findDOMNode(this.refs.textarea);
textarea.style.height = "1px";
textarea.style.height = `${textarea.scrollHeight + 2}px`;
};
handleInputText = e => {
const inputText = e.target.value;
this.props.handleInputText(inputText);
this.resizeTextArea();
};
render() {
return (
<div id="inputArea">
<textarea
value={this.props.inputText}
ref="textarea"
placeholder={browser.i18n.getMessage("initialTextArea")}
onChange={this.handleInputText}
autoFocus
spellCheck={false}
/>
</div>
);
}
}

View file

@ -0,0 +1,123 @@
import React, { Component } from "react";
import browser from "webextension-polyfill";
import { initSettings, getSettings } from "src/settings/settings";
import translate from "src/common/translate";
import Header from "./Header";
import InputArea from "./InputArea";
import ResultArea from "./ResultArea";
import Footer from "./Footer";
import "../styles/PopupPage.scss";
const getTabInfo = async () => {
try {
const tab = (await browser.tabs.query({ currentWindow: true, active: true }))[0];
const tabInfo = await browser.tabs.sendMessage(tab.id, { message: "getTabInfo" });
return tabInfo;
} catch (e) {
return { url: "", selectedText: "" };
}
};
export default class PopupPage extends Component {
constructor(props) {
super(props);
this.state = {
targetLang: "",
inputText: "",
resultText: "",
candidateText: "",
statusText: "OK",
tabUrl: ""
};
this.isSwitchedSecondLang = false;
this.init();
}
init = async () => {
await initSettings();
const targetLang = getSettings("targetLang");
this.setState({
targetLang: targetLang
});
const tabInfo = await getTabInfo();
this.setState({
inputText: tabInfo.selectedText,
tabUrl: tabInfo.url
});
if (tabInfo.selectedText !== "") this.translateText(tabInfo.selectedText, targetLang);
};
handleInputText = inputText => {
this.setState({ inputText: inputText });
const waitTime = getSettings("waitTime");
clearTimeout(this.inputTimer);
this.inputTimer = setTimeout(
() => this.translateText(inputText, this.state.targetLang),
waitTime
);
};
handleLangChange = lang => {
this.setState({ targetLang: lang });
const inputText = this.state.inputText;
if (inputText !== "") this.translateText(inputText, lang);
};
translateText = async (text, targetLang) => {
const result = await translate(text, "auto", targetLang);
this.setState({
resultText: result.resultText,
candidateText: result.candidateText,
statusText: result.statusText
});
this.switchSecondLang(result);
};
switchSecondLang = result => {
if (!getSettings("ifChangeSecondLang")) return;
const defaultTargetLang = getSettings("targetLang");
const secondLang = getSettings("secondTargetLang");
if (defaultTargetLang === secondLang) return;
const equalsSourceAndTarget =
result.sourceLanguage === this.state.targetLang && result.percentage > 0;
const equalsSourceAndDefault =
result.sourceLanguage === defaultTargetLang && result.percentage > 0;
if (!this.isSwitchedSecondLang) {
if (equalsSourceAndTarget && equalsSourceAndDefault) {
this.handleLangChange(secondLang);
this.isSwitchedSecondLang = true;
}
} else {
if (!equalsSourceAndDefault) {
this.handleLangChange(defaultTargetLang);
this.isSwitchedSecondLang = false;
}
}
};
render() {
return (
<div>
<Header />
<InputArea inputText={this.state.inputText} handleInputText={this.handleInputText} />
<hr />
<ResultArea
resultText={this.state.resultText}
candidateText={this.state.candidateText}
statusText={this.state.statusText}
/>
<Footer
tabUrl={this.state.tabUrl}
targetLang={this.state.targetLang}
handleLangChange={this.handleLangChange}
/>
</div>
);
}
}

View file

@ -0,0 +1,38 @@
import React from "react";
import browser from "webextension-polyfill";
import "../styles/ResultArea.scss";
const getErrorMessage = statusText => {
let errorMessage = "";
switch (statusText) {
case "":
errorMessage = browser.i18n.getMessage("networkError");
break;
case "Service Unavailable":
errorMessage = browser.i18n.getMessage("unavailableError");
break;
default:
errorMessage = `${browser.i18n.getMessage("unknownError")} [${statusText}]`;
break;
}
return errorMessage;
};
const splitLine = text => {
const regex = /(\n)/g;
return text.split(regex).map((line, i) => (line.match(regex) ? <br key={i} /> : line));
};
export default props => {
const { resultText, candidateText, statusText } = props;
const isError = statusText !== "OK";
return (
<div id="resultArea">
<p className="resultText">{splitLine(resultText)}</p>
<p className="candidateText">
{isError ? getErrorMessage(statusText) : splitLine(candidateText)}
</p>
</div>
);
};

View file

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 492.719 492.719">
<path d="M492.719,166.008c0-73.486-59.573-133.056-133.059-133.056c-47.985,0-89.891,25.484-113.302,63.569
c-23.408-38.085-65.332-63.569-113.316-63.569C59.556,32.952,0,92.522,0,166.008c0,40.009,17.729,75.803,45.671,100.178
l188.545,188.553c3.22,3.22,7.587,5.029,12.142,5.029c4.555,0,8.922-1.809,12.142-5.029l188.545-188.553
C474.988,241.811,492.719,206.017,492.719,166.008z" />
</svg>

After

Width:  |  Height:  |  Size: 467 B

View file

@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path class="st0" d="M499.453,210.004l-55.851-2.58c-5.102-0.23-9.608-3.395-11.546-8.103l-11.508-27.695
c-1.937-4.728-0.997-10.145,2.455-13.914l37.668-41.332c4.718-5.188,4.546-13.205-0.421-18.182l-46.434-46.443
c-4.986-4.967-13.003-5.159-18.2-0.412l-41.312,37.668c-3.778,3.443-9.206,4.402-13.924,2.436l-27.694-11.488
c-4.718-1.946-7.864-6.454-8.094-11.565l-2.589-55.831C301.675,5.534,295.883,0,288.864,0h-65.708
c-7.02,0-12.831,5.534-13.156,12.562l-2.571,55.831c-0.23,5.111-3.376,9.618-8.094,11.565L171.64,91.447
c-4.737,1.966-10.165,1.007-13.924-2.436l-41.331-37.668c-5.198-4.746-13.215-4.564-18.201,0.412L51.769,98.198
c-4.986,4.977-5.158,12.994-0.422,18.182l37.668,41.332c3.452,3.769,4.373,9.186,2.416,13.914l-11.469,27.695
c-1.956,4.708-6.444,7.873-11.564,8.103l-55.832,2.58c-7.019,0.316-12.562,6.118-12.562,13.147v65.699
c0,7.019,5.543,12.83,12.562,13.148l55.832,2.579c5.12,0.229,9.608,3.394,11.564,8.103l11.469,27.694
c1.957,4.728,1.036,10.146-2.416,13.914l-37.668,41.313c-4.756,5.217-4.564,13.224,0.403,18.201l46.471,46.443
c4.967,4.977,12.965,5.15,18.182,0.422l41.312-37.677c3.759-3.443,9.207-4.392,13.924-2.435l27.694,11.478
c4.719,1.956,7.864,6.464,8.094,11.575l2.571,55.831c0.325,7.02,6.136,12.562,13.156,12.562h65.708
c7.02,0,12.812-5.542,13.138-12.562l2.589-55.831c0.23-5.111,3.376-9.619,8.094-11.575l27.694-11.478
c4.718-1.957,10.146-1.008,13.924,2.435l41.312,37.677c5.198,4.728,13.215,4.555,18.2-0.422l46.434-46.443
c4.967-4.977,5.139-12.984,0.421-18.201l-37.668-41.313c-3.452-3.768-4.412-9.186-2.455-13.914l11.508-27.694
c1.937-4.709,6.444-7.874,11.546-8.103l55.851-2.579c7.019-0.318,12.542-6.129,12.542-13.148v-65.699
C511.995,216.122,506.472,210.32,499.453,210.004z M256.01,339.618c-46.164,0-83.622-37.438-83.622-83.612
c0-46.184,37.458-83.622,83.622-83.622s83.602,37.438,83.602,83.622C339.612,302.179,302.174,339.618,256.01,339.618z" />
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -1,81 +1,12 @@
<!DOCTYPE html>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="popup.css">
<meta charset="UTF-8">
</head>
<body>
<svg class=hidden>
<defs>
<symbol id="settingSvg" viewBox="0 0 512 512">
<path class="st0" d="M499.453,210.004l-55.851-2.58c-5.102-0.23-9.608-3.395-11.546-8.103l-11.508-27.695
c-1.937-4.728-0.997-10.145,2.455-13.914l37.668-41.332c4.718-5.188,4.546-13.205-0.421-18.182l-46.434-46.443
c-4.986-4.967-13.003-5.159-18.2-0.412l-41.312,37.668c-3.778,3.443-9.206,4.402-13.924,2.436l-27.694-11.488
c-4.718-1.946-7.864-6.454-8.094-11.565l-2.589-55.831C301.675,5.534,295.883,0,288.864,0h-65.708
c-7.02,0-12.831,5.534-13.156,12.562l-2.571,55.831c-0.23,5.111-3.376,9.618-8.094,11.565L171.64,91.447
c-4.737,1.966-10.165,1.007-13.924-2.436l-41.331-37.668c-5.198-4.746-13.215-4.564-18.201,0.412L51.769,98.198
c-4.986,4.977-5.158,12.994-0.422,18.182l37.668,41.332c3.452,3.769,4.373,9.186,2.416,13.914l-11.469,27.695
c-1.956,4.708-6.444,7.873-11.564,8.103l-55.832,2.58c-7.019,0.316-12.562,6.118-12.562,13.147v65.699
c0,7.019,5.543,12.83,12.562,13.148l55.832,2.579c5.12,0.229,9.608,3.394,11.564,8.103l11.469,27.694
c1.957,4.728,1.036,10.146-2.416,13.914l-37.668,41.313c-4.756,5.217-4.564,13.224,0.403,18.201l46.471,46.443
c4.967,4.977,12.965,5.15,18.182,0.422l41.312-37.677c3.759-3.443,9.207-4.392,13.924-2.435l27.694,11.478
c4.719,1.956,7.864,6.464,8.094,11.575l2.571,55.831c0.325,7.02,6.136,12.562,13.156,12.562h65.708
c7.02,0,12.812-5.542,13.138-12.562l2.589-55.831c0.23-5.111,3.376-9.619,8.094-11.575l27.694-11.478
c4.718-1.957,10.146-1.008,13.924,2.435l41.312,37.677c5.198,4.728,13.215,4.555,18.2-0.422l46.434-46.443
c4.967-4.977,5.139-12.984,0.421-18.201l-37.668-41.313c-3.452-3.768-4.412-9.186-2.455-13.914l11.508-27.694
c1.937-4.709,6.444-7.874,11.546-8.103l55.851-2.579c7.019-0.318,12.542-6.129,12.542-13.148v-65.699
C511.995,216.122,506.472,210.32,499.453,210.004z M256.01,339.618c-46.164,0-83.622-37.438-83.622-83.612
c0-46.184,37.458-83.622,83.622-83.622s83.602,37.438,83.602,83.622C339.612,302.179,302.174,339.618,256.01,339.618z"></path>
</symbol>
<symbol id="heartSvg" viewBox="0 0 492.719 492.719">
<path d="M492.719,166.008c0-73.486-59.573-133.056-133.059-133.056c-47.985,0-89.891,25.484-113.302,63.569
c-23.408-38.085-65.332-63.569-113.316-63.569C59.556,32.952,0,92.522,0,166.008c0,40.009,17.729,75.803,45.671,100.178
l188.545,188.553c3.22,3.22,7.587,5.029,12.142,5.029c4.555,0,8.922-1.809,12.142-5.029l188.545-188.553
C474.988,241.811,492.719,206.017,492.719,166.008z"></path>
</symbol>
</defs>
</svg>
<div id=header>
<div id=title>Simple Translate</div>
<div class="rightButtons">
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_xclick&no_shipping=1&business=sienori.firefox@gmail.com&item_name=SimpleTranslate - Donation"
target="_blank">
<div id="donate" title="Donate with PayPal">
<svg>
<use xlink:href="#heartSvg"></use>
</svg>
</div>
</a>
<a href="../options/options.html" target="_blank">
<div id="setting" title="Setting">
<svg>
<use xlink:href="#settingSvg"></use>
</svg>
</div>
</a>
</div>
</div>
<div id=main>
<textarea id=textarea spellcheck=false contenteditable=true autofocus></textarea>
<hr>
<div id=target>
<p class=result></p>
<p class=candidate></p>
</div>
</div>
<div id=footer>
<div id=link></div>
<div class=selectWrap>
<select id="langList" title="Target language"></select>
</div>
</div>
<script src="../Settings.js"></script>
<script src="../translate.js"></script>
<script src="popup.js"></script>
<div id=root />
</body>
</html>

View file

@ -1,231 +1,5 @@
/* Copyright (c) 2017-2018 Sienori All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import React from "react";
import ReactDOM from "react-dom";
import PopupPage from "./components/PopupPage";
const S = new settingsObj();
const T = new Translate();
//設定を読み出し
S.init().then(function(value) {
defaultTargetLang = value.targetLang;
secondTargetLang = value.secondTargetLang;
langList.value = value.targetLang; //リスト初期値をセット
langList.addEventListener("change", changeLang);
document.body.style.fontSize = value.fontSize;
});
let target = document.getElementById("target");
let langList = document.getElementById("langList");
let textarea = document.getElementById("textarea");
const initialText = browser.i18n.getMessage("initialTextArea");
textarea.placeholder = initialText;
let secondTargetLang;
let defaultTargetLang;
let sourceWord = "";
setLangList();
function setLangList() {
let langListStr = browser.i18n.getMessage("langList");
langListStr = langListStr.split(", ");
for (let i in langListStr) {
langListStr[i] = langListStr[i].split(":");
}
langListStr = langListStr.sort(alphabeticallySort);
let langListHtml = "";
for (let i of langListStr) {
langListHtml += `<option value=${i[0]}>${i[1]}</option>`;
}
langList.innerHTML = langListHtml;
}
setTitles();
function setTitles() {
document.getElementById("donate").title = browser.i18n.getMessage("donateWithPaypalLabel");
document.getElementById("setting").title = browser.i18n.getMessage("settingsLabel");
document.getElementById("langList").title = browser.i18n.getMessage("targetLangLabel");
}
function alphabeticallySort(a, b) {
if (a[1].toString() > b[1].toString()) {
return 1;
} else {
return -1;
}
}
//翻訳先言語変更時に更新
async function changeLang() {
if (typeof url != "undefined") showLink();
if (sourceWord !== "") {
const resultData = await T.translate(sourceWord, undefined, langList.value);
showResult(resultData.resultText, resultData.candidateText, resultData.statusText);
}
}
//アクティブなタブを取得して渡す
browser.tabs
.query({
currentWindow: true,
active: true
})
.then(function(tabs) {
getSelectionWord(tabs);
});
//アクティブタブから選択文字列とurlを取得
function getSelectionWord(tabs) {
for (let tab of tabs) {
browser.tabs
.sendMessage(tab.id, {
message: "fromPopup"
})
.then(response => {
sourceWord = response.word || "";
url = response.url;
refleshSource();
showLink();
})
.catch(() => {});
}
}
//ページ翻訳へのリンクを表示
function showLink() {
document.getElementById("link").innerHTML =
"<a href=https://translate.google.com/translate?hl=" +
langList.value +
"&sl=auto&u=" +
encodeURIComponent(url) +
">" +
browser.i18n.getMessage("showLink") +
"</a>";
}
//翻訳元テキストを表示
function refleshSource() {
if (sourceWord !== "") {
textarea.innerHTML = sourceWord;
resize();
inputText();
}
}
textarea.addEventListener("paste", () => {
resize();
inputText();
});
textarea.addEventListener("keydown", resize);
textarea.addEventListener("keyup", function(event) {
if (sourceWord == textarea.value) return;
resize();
inputText();
});
//テキストボックスをリサイズ
function resize() {
setTimeout(function() {
textarea.style.height = "0px";
textarea.style.height = parseInt(textarea.scrollHeight) + "px";
}, 0);
}
textarea.addEventListener("click", textAreaClick, {
once: true
});
//テキストエリアクリック時の処理
function textAreaClick() {
textarea.select();
}
let inputTimer;
//文字入力時の処理
function inputText() {
sourceWord = textarea.value;
const waitTime = S.get().waitTime;
clearTimeout(inputTimer);
inputTimer = setTimeout(() => {
runTranslation();
}, waitTime);
}
async function runTranslation() {
if (sourceWord == "") {
showResult("", "");
return;
}
const resultData = await T.translate(sourceWord, "auto", langList.value);
changeSecondLang(defaultTargetLang, resultData.sourceLanguage, resultData.percentage);
showResult(resultData.resultText, resultData.candidateText, resultData.statusText);
}
function showResult(resultText, candidateText, statusText = "OK") {
const resultArea = target.getElementsByClassName("result")[0];
const candidateArea = target.getElementsByClassName("candidate")[0];
resultArea.innerText = resultText;
if (S.get().ifShowCandidate) candidateArea.innerText = candidateText;
if (statusText != "OK") showError(statusText);
}
function showError(statusText) {
let errorMessage = "";
switch (statusText) {
case "":
errorMessage = browser.i18n.getMessage("networkError");
break;
case "Service Unavailable":
errorMessage = browser.i18n.getMessage("unavailableError");
break;
default:
errorMessage = `${browser.i18n.getMessage("unknownError")} [${statusText}]`;
break;
}
const candidateArea = target.getElementsByClassName("candidate")[0];
candidateArea.innerText = errorMessage;
}
let changeLangFlag = false;
function changeSecondLang(defaultTargetLang, sourceLang, percentage) {
if (!S.get().ifChangeSecondLang) return;
//検出された翻訳元言語がターゲット言語と一致
const equalsSourceAndTarget = sourceLang == langList.value && percentage > 0;
//検出された翻訳元言語がデフォルト言語と一致
const equalsSourceAndDefault = sourceLang == defaultTargetLang && percentage > 0;
if (!changeLangFlag) {
//通常時
if (equalsSourceAndTarget && equalsSourceAndDefault) {
//ソースとターゲットとデフォルトが一致する場合
//ターゲットを第2言語に変更
changeLangFlag = true;
langList.value = secondTargetLang;
changeLang();
}
} else {
//第2言語に切替した後
if (!equalsSourceAndDefault) {
//ソースとデフォルトが異なる場合
//ターゲットをデフォルトに戻す
changeLangFlag = false;
langList.value = defaultTargetLang;
changeLang();
}
}
}
ReactDOM.render(<PopupPage />, document.getElementById("root"));

View file

@ -1,227 +0,0 @@
:root {
--main-text: #0c0c0d;
--sub-text: #737373;
--line: #ededf0;
--button: #d7d7db;
--highlight: #5595ff;
--main-bg: #ffffff;
--confirm: #ff4f4f;
}
body {
font-family: "Segoe UI", "San Francisco", "Ubuntu", "Fira Sans", "Roboto", "Arial", "Helvetica",
sans-serif;
text-align: left;
font-size: 13px;
width: 348px;
overflow: hidden;
background-color: var(--main-bg);
padding: 0px;
margin: 0px;
display: flex;
flex-direction: column;
}
.hidden {
display: none;
}
svg {
pointer-events: none;
}
#header {
padding: 10px;
background-color: var(--line);
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
-moz-user-select: none;
}
#title {
font-size: 15px;
font-weight: 400;
color: #666;
cursor: default;
}
#header .rightButtons {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
}
#donate {
display: flex;
align-items: center;
cursor: pointer;
margin-right: 8px;
}
#donate svg {
height: 18px;
width: 18px;
fill: var(--sub-text);
transition: all 100ms;
}
#donate:hover svg {
fill: var(--confirm);
}
#setting {
display: flex;
align-items: center;
cursor: pointer;
}
#setting svg {
flex-shrink: 0;
height: 18px;
width: 18px;
fill: var(--sub-text);
transform: rotate(180deg);
transition: fill 100ms, transform 300ms ease;
}
#setting:hover svg {
fill: var(--highlight);
transform: rotate(270deg);
}
#main {
padding: 10px;
}
textarea {
font: inherit;
resize: none;
overflow: auto;
background-color: var(--main-bg);
max-height: 215px;
height: 37px;
/* 100% - padding*2 - border*2 */
width: calc(100% - 22px);
padding: 10px;
border: solid 1px var(--button);
border-radius: 2px;
transition: border-color 100ms ease-out;
}
textarea:hover,
textarea:focus {
border-color: var(--highlight);
}
hr {
border: none;
border-top: solid 1px var(--button);
height: 1px;
margin: 10px 0px;
}
#target {
max-height: 215px;
min-height: 30px;
overflow-y: auto;
word-wrap: break-word;
padding: 0px 5px 0px;
background-color: var(--main-bg);
}
#target p {
margin: 0;
background-color: var(--main-bg);
}
#target .result {
background-color: var(--main-bg);
}
#target .candidate {
color: var(--sub-text);
margin-top: 1em;
background-color: var(--main-bg);
}
#target .candidate:empty {
margin-top: 0;
}
#footer {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 0px 10px 10px;
}
#link {
flex-shrink: 0;
}
#link a {
font-style: normal;
text-decoration: none;
color: var(--highlight);
}
#link a:hover {
text-decoration: underline;
}
select {
-moz-appearance: none;
text-overflow: ellipsis;
border: var(--button) solid 1px;
border-radius: 2px;
padding: 3px 5px;
padding-right: 20px;
width: 100%;
transition: border-color 100ms ease-out;
}
select:hover {
border: var(--highlight) solid 1px;
}
.selectWrap {
position: relative;
margin-left: 5px;
}
.selectWrap:before {
pointer-events: none;
content: "";
z-index: 1;
position: absolute;
top: 40%;
right: 7px;
width: 5px;
height: 5px;
transform: rotate(45deg);
border-bottom: 2px solid var(--sub-text);
border-right: 2px solid var(--sub-text);
transition: border-color 100ms ease-out;
}
.selectWrap:hover::before {
border-bottom: 2px solid var(--highlight);
border-right: 2px solid var(--highlight);
}
::-moz-selection {
background: var(--line);
}

View file

@ -0,0 +1,57 @@
#footer {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 0px 10px 10px;
.translateLink {
flex-shrink: 0;
a {
font-style: normal;
text-decoration: none;
color: var(--highlight);
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
}
.selectWrap {
position: relative;
margin-left: 5px;
&:before {
pointer-events: none;
content: "";
z-index: 1;
position: absolute;
top: 40%;
right: 7px;
width: 5px;
height: 5px;
transform: rotate(45deg);
border-bottom: 2px solid var(--sub-text);
border-right: 2px solid var(--sub-text);
transition: border-color 100ms ease-out;
}
&:hover::before {
border-bottom: 2px solid var(--highlight);
border-right: 2px solid var(--highlight);
}
select {
-moz-appearance: none;
text-overflow: ellipsis;
background-color: var(--main-bg);
border: var(--button) solid 1px;
border-radius: 2px;
padding: 3px 5px;
padding-right: 20px;
width: 100%;
transition: border-color 100ms ease-out;
}
}
}

View file

@ -0,0 +1,61 @@
#header {
padding: 10px;
background-color: var(--line);
display: flex;
flex-direction: row;
flex-shrink: 0;
align-items: center;
justify-content: space-between;
-moz-user-select: none;
-webkit-user-select: none;
.title {
font-size: 15px;
font-weight: 400;
color: #666;
cursor: default;
flex-shrink: 0;
}
.rightButtons {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
height: 18px;
button {
display: block;
background-color: transparent;
border: none;
cursor: pointer;
outline: none;
padding: 0;
}
.heartButton {
display: flex;
align-items: center;
margin-right: 10px;
&:hover svg {
fill: var(--confirm);
}
}
.settingsButton {
display: flex;
align-items: center;
&:hover svg {
fill: var(--highlight);
transform: rotate(90deg);
}
}
svg {
height: 18px;
width: 18px;
fill: var(--sub-text);
transition: fill 100ms, transform 300ms ease;
}
}
}

View file

@ -0,0 +1,26 @@
#inputArea {
margin: 10px;
textarea {
font: inherit;
resize: none;
overflow: auto;
background-color: var(--main-bg);
box-sizing: border-box;
width: 100%;
height: 60px;
max-height: 240px;
min-height: 60px;
margin: 0;
padding: 10px;
border: solid 1px var(--button);
border-radius: 2px;
transition: border-color 100ms ease-out;
}
textarea:hover,
textarea:focus {
border-color: var(--highlight);
}
}

View file

@ -0,0 +1,38 @@
body {
margin: 0;
font-family: "Segoe UI", "San Francisco", "Ubuntu", "Fira Sans", "Roboto", "Arial", "Helvetica",
sans-serif;
font-size: 13px;
width: 348px;
overflow: hidden;
background-color: var(--main-bg);
#root {
height: 100%;
}
hr {
border: none;
border-top: solid 1px var(--button);
height: 1px;
margin: 0px 10px;
}
::-moz-selection {
background: var(--line);
}
}
:root {
--main-text: #0c0c0d;
--sub-text: #737373;
--line: #ededf0;
--button: #d7d7db;
--highlight: #5595ff;
--main-bg: #ffffff;
--confirm: #ff4f4f;
--error: #d70022;
--warn: #ff8f00;
--success: #058b00;
--info: #0a84ff;
}

View file

@ -0,0 +1,25 @@
#resultArea {
max-height: 240px;
min-height: 30px;
overflow-y: auto;
word-wrap: break-word;
background-color: var(--main-bg);
margin: 10px;
p {
margin: 0;
padding: 0px 5px;
background-color: var(--main-bg);
&.resultText {
color: var(--main-text);
}
&.candidateText {
color: var(--sub-text);
margin-top: 1em;
&:empty {
margin-top: 0;
}
}
}
}