diff --git a/packages/ui/.vscode/tsx.code-snippets b/packages/ui/.vscode/tsx.code-snippets new file mode 100644 index 000000000..be2b52bab --- /dev/null +++ b/packages/ui/.vscode/tsx.code-snippets @@ -0,0 +1,18 @@ +{ + // Place your ui workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and + // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope + // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is + // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: + // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. + // Placeholders with the same ids are connected. + // Example: + "Import SCSS styles": { + "scope": "javascriptreact,typescriptreact", + "prefix": "isc", + "body": [ + "import styles from './index.module.scss';", + "$0" + ], + "description": "Import SCSS styles from the same directory." + } +} diff --git a/packages/ui/README.md b/packages/ui/README.md index 6851c29f7..e10f0957c 100644 --- a/packages/ui/README.md +++ b/packages/ui/README.md @@ -1,3 +1,3 @@ # @logto/ui -The core register / sign in experience for end-users. +The core register / sign-in experience for end-users. diff --git a/packages/ui/package.json b/packages/ui/package.json index 9ade5a448..1d077f6a9 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -15,8 +15,11 @@ }, "dependencies": { "classnames": "^2.3.1", + "i18next": "^20.3.3", + "i18next-browser-languagedetector": "^6.1.2", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-i18next": "^11.11.3", "react-router-dom": "^5.2.0" }, "devDependencies": { diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index c8fa9697d..69e972957 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -1,9 +1,12 @@ import React from 'react'; import { Route, Switch } from 'react-router-dom'; import AppContent from './components/AppContent'; +import initI18n from './init/i18n'; import Home from './pages/Home'; import './scss/normalized.scss'; +initI18n(); + const App = () => ( diff --git a/packages/ui/src/components/AppContent/index.module.scss b/packages/ui/src/components/AppContent/index.module.scss index f1dcaa811..3658d5c97 100644 --- a/packages/ui/src/components/AppContent/index.module.scss +++ b/packages/ui/src/components/AppContent/index.module.scss @@ -55,7 +55,7 @@ $font-family: 'PingFang SC', 'SF Pro Text', sans-serif; .mobile { --font-headline: 600 40px/56px #{$font-family}; - --font-heading-1: 600 28px/39.2px #{$font-family}; + --font-heading-1: 600 28px/39px #{$font-family}; --font-heading-2: 600 20px/28px #{$font-family}; --font-heading-3: 600 16px/22.4px #{$font-family}; --font-body: 400 12px/16px #{$font-family}; diff --git a/packages/ui/src/components/Input/index.module.scss b/packages/ui/src/components/Input/index.module.scss new file mode 100644 index 000000000..300ed87f5 --- /dev/null +++ b/packages/ui/src/components/Input/index.module.scss @@ -0,0 +1,19 @@ +@use '/src/scss/underscore' as _; + +.input { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: _.unit(3) _.unit(5); + border-radius: _.unit(2); + background: var(--color-control-background); + color: var(--color-heading); + font: var(--font-heading-3); + border: none; + outline: none; + + &::placeholder { + color: var(--color-placeholder); + } +} diff --git a/packages/ui/src/components/Input/index.tsx b/packages/ui/src/components/Input/index.tsx new file mode 100644 index 000000000..2b1a90c73 --- /dev/null +++ b/packages/ui/src/components/Input/index.tsx @@ -0,0 +1,27 @@ +import classNames from 'classnames'; +import React from 'react'; +import styles from './index.module.scss'; + +export type Props = { + className?: string; + placeholder?: string; + type?: InputType; + value: string; + onChange: (value: string) => void; +}; + +const Input = ({ className, placeholder, type = 'text', value, onChange }: Props) => { + return ( + { + onChange(value); + }} + /> + ); +}; + +export default Input; diff --git a/packages/ui/src/include.d/dom.d.ts b/packages/ui/src/include.d/dom.d.ts new file mode 100644 index 000000000..405265130 --- /dev/null +++ b/packages/ui/src/include.d/dom.d.ts @@ -0,0 +1,23 @@ +type InputType = + | 'button' + | 'checkbox' + | 'color' + | 'date' + | 'datetime-local' + | 'email' + | 'file' + | 'hidden' + | 'image' + | 'month' + | 'number' + | 'password' + | 'radio' + | 'range' + | 'reset' + | 'search' + | 'submit' + | 'tel' + | 'text' + | 'time' + | 'url' + | 'week'; diff --git a/packages/ui/src/include.d/react-i18next.d.ts b/packages/ui/src/include.d/react-i18next.d.ts new file mode 100644 index 000000000..573429fa9 --- /dev/null +++ b/packages/ui/src/include.d/react-i18next.d.ts @@ -0,0 +1,11 @@ +// https://react.i18next.com/latest/typescript#create-a-declaration-file + +// eslint-disable-next-line import/no-unassigned-import +import 'react-i18next'; +import en from '@/locales/en.json'; + +declare module 'react-i18next' { + interface CustomTypeOptions { + resources: typeof en; + } +} diff --git a/packages/ui/src/init/i18n.ts b/packages/ui/src/init/i18n.ts new file mode 100644 index 000000000..b45e62cff --- /dev/null +++ b/packages/ui/src/init/i18n.ts @@ -0,0 +1,23 @@ +import i18n from 'i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; +import { initReactI18next } from 'react-i18next'; +import en from '@/locales/en.json'; +import zhCN from '@/locales/zh-CN.json'; + +const initI18n = () => { + void i18n + .use(initReactI18next) + .use(LanguageDetector) + .init({ + resources: { + en, + 'zh-CN': zhCN, + }, + fallbackLng: 'en', + interpolation: { + escapeValue: false, + }, + }); +}; + +export default initI18n; diff --git a/packages/ui/src/locales/en.json b/packages/ui/src/locales/en.json new file mode 100644 index 000000000..cc2db4b7e --- /dev/null +++ b/packages/ui/src/locales/en.json @@ -0,0 +1,6 @@ +{ + "translation": { + "sign-in.username": "Username", + "sign-in.password": "Password" + } +} diff --git a/packages/ui/src/locales/zh-CN.json b/packages/ui/src/locales/zh-CN.json new file mode 100644 index 000000000..3412ceb52 --- /dev/null +++ b/packages/ui/src/locales/zh-CN.json @@ -0,0 +1,6 @@ +{ + "translation": { + "sign-in.username": "用户名", + "sign-in.password": "密码" + } +} diff --git a/packages/ui/src/pages/Home/index.module.scss b/packages/ui/src/pages/Home/index.module.scss index e3374bca7..d3c5d3d02 100644 --- a/packages/ui/src/pages/Home/index.module.scss +++ b/packages/ui/src/pages/Home/index.module.scss @@ -1,11 +1,18 @@ @use '/src/scss/underscore' as _; .wrapper { - text-align: center; - padding: _.unit(5); -} + padding: _.unit(8); + display: flex; + flex-direction: column; + align-items: center; -.title { - font: var(--font-headline); - color: var(--color-heading); + .title { + font: var(--font-heading-1); + color: var(--color-heading); + } + + > input { + align-self: stretch; + margin: _.unit(1.5) 0; + } } diff --git a/packages/ui/src/pages/Home/index.tsx b/packages/ui/src/pages/Home/index.tsx index baf81688f..fca018672 100644 --- a/packages/ui/src/pages/Home/index.tsx +++ b/packages/ui/src/pages/Home/index.tsx @@ -1,10 +1,23 @@ -import React from 'react'; +import Input from '@/components/Input'; +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; import styles from './index.module.scss'; const Home = () => { + const { t } = useTranslation(); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + return (
-
Logto
+
登录 Logto
+ +
); }; diff --git a/packages/ui/src/scss/normalized.scss b/packages/ui/src/scss/normalized.scss index b4cc7250b..b052e6754 100644 --- a/packages/ui/src/scss/normalized.scss +++ b/packages/ui/src/scss/normalized.scss @@ -3,3 +3,7 @@ body { padding: 0; font-family: sans-serif; } + +* { + box-sizing: border-box; +} diff --git a/packages/ui/yarn.lock b/packages/ui/yarn.lock index cf463b13f..2a85abb56 100644 --- a/packages/ui/yarn.lock +++ b/packages/ui/yarn.lock @@ -955,7 +955,7 @@ "@babel/helper-validator-option" "^7.14.5" "@babel/plugin-transform-typescript" "^7.14.5" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.1.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.14.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.14.6" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.6.tgz#535203bc0892efc7dec60bdc27b2ecf6e409062d" integrity sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg== @@ -5707,6 +5707,13 @@ html-minifier-terser@^5.0.1: relateurl "^0.2.7" terser "^4.6.3" +html-parse-stringify@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2" + integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg== + dependencies: + void-elements "3.1.0" + html-tags@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.1.0.tgz#7b5e6f7e665e9fb41f30007ed9e0d41e97fb2140" @@ -5847,6 +5854,20 @@ husky@^7.0.1: resolved "https://registry.yarnpkg.com/husky/-/husky-7.0.1.tgz#579f4180b5da4520263e8713cc832942b48e1f1c" integrity sha512-gceRaITVZ+cJH9sNHqx5tFwbzlLCVxtVZcusME8JYQ8Edy5mpGDOqD8QBCdMhpyo9a+JXddnujQ4rpY2Ff9SJA== +i18next-browser-languagedetector@^6.1.2: + version "6.1.2" + resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-6.1.2.tgz#68565a28b929cbc98ab6a56826ef2faf0e927ff8" + integrity sha512-YDzIGHhMRvr7M+c8B3EQUKyiMBhfqox4o1qkFvt4QXuu5V2cxf74+NCr+VEkUuU0y+RwcupA238eeolW1Yn80g== + dependencies: + "@babel/runtime" "^7.14.6" + +i18next@^20.3.3: + version "20.3.3" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-20.3.3.tgz#e3fdae045f9f0893e74826ce224715e43c62862b" + integrity sha512-tx9EUhHeaipvZ5pFLTaN9Xdm5Ssal774MpujaTA1Wv/ST/1My5SnoBmliY1lOpyEP5Z51Dq1gXifk/y4Yt3agQ== + dependencies: + "@babel/runtime" "^7.12.0" + iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -9355,6 +9376,14 @@ react-error-overlay@^6.0.7, react-error-overlay@^6.0.9: resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a" integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew== +react-i18next@^11.11.3: + version "11.11.3" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.11.3.tgz#38d083bb079c3e6ee376b3321b0d6e409d798f68" + integrity sha512-upzG5/SpyOlYP5oSF4K8TZBvDWVhnCo38JNV+KnWjrg0+IaJCBltyh6lRGZDO5ovLyA4dU6Ip0bwbUCjb6Yyxw== + dependencies: + "@babel/runtime" "^7.14.5" + html-parse-stringify "^3.0.1" + react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -11503,6 +11532,11 @@ vm-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== +void-elements@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" + integrity sha1-YU9/v42AHwu18GYfWy9XhXUOTwk= + w3c-hr-time@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"