merchant-backoffice

ZZZ: Inactive/Deprecated
Log | Files | Refs | Submodules | README

commit 329acdc5035845cf06352c158a6c6479bdf38fd3
parent 35af67df3cda2c79c5ca9de78498f21f7d559db2
Author: ms <ms@taler.net>
Date:   Wed, 23 Mar 2022 18:51:25 +0100

copying pybank's look.  WIP

Diffstat:
Apackages/bank/src/assets/logo-white.svg | 45+++++++++++++++++++++++++++++++++++++++++++++
Mpackages/bank/src/components/app.tsx | 2--
Mpackages/bank/src/components/menu/LangSelector.tsx | 35++++++++++++++++++++++++++++++++++-
Mpackages/bank/src/pages/home/index.tsx | 561+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Apackages/bank/src/scss/bank.scss | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/bank/src/scss/colors-bank.scss | 24++++++++++++++++++++++++
Apackages/bank/src/scss/demo.scss | 138+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/bank/src/scss/main.scss | 238++-----------------------------------------------------------------------------
Apackages/bank/src/scss/pure.scss | 1328+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
9 files changed, 2031 insertions(+), 420 deletions(-)

diff --git a/packages/bank/src/assets/logo-white.svg b/packages/bank/src/assets/logo-white.svg @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + width="670" + height="300" + viewBox="0 0 201 90" + version="1.1" + id="svg8"> + <g + id="logo"> + <g + id="circles" + style="fill:#FFF;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.327943"> + <path + d="m 86.662153,1.1211936 c 15.589697,0 29.129227,9.4011664 35.961027,23.2018054 h -5.81736 C 110.4866,13.623304 99.349002,6.5180852 86.662153,6.5180852 c -19.690571,0 -35.652876,17.1120008 -35.652876,38.2205688 0,10.331797 3.825597,19.704678 10.03957,26.582945 -1.342357,1.120912 -2.771532,2.127905 -4.275488,3.006754 C 50.071485,66.553412 45.974857,56.15992 45.974857,44.738654 c 0,-24.089211 18.216325,-43.6174604 40.687296,-43.6174604 z M 122.51416,65.375898 c -6.86645,13.680134 -20.34561,22.980218 -35.852007,22.980218 -1.052702,0 -2.096093,-0.04291 -3.128683,-0.127026 3.052192,-1.561167 5.913582,-3.480387 8.538307,-5.707305 10.320963,-1.684389 19.185983,-8.113638 24.601813,-17.145887 z" + id="path2350" /> + <path + d="m 64.212372,1.1211936 c 1.052607,0 2.095998,0.042919 3.128684,0.1270583 C 64.288864,2.8094199 61.427378,4.728606 58.802653,6.9555572 41.679542,9.7498571 28.559494,25.601563 28.559494,44.738654 c 0,14.264563 7.29059,26.702023 18.093843,33.268925 -1.593656,0.26719 -3.226966,0.406948 -4.890748,0.406948 -1.239545,0 -2.46151,-0.07952 -3.663522,-0.229364 C 29.191129,70.184015 23.525076,58.171633 23.525076,44.738654 23.525076,20.649443 41.7414,1.1211936 64.212372,1.1211936 Z M 69.62209,82.521785 C 79.943207,80.837396 88.808164,74.407841 94.224059,65.375422 h 5.840511 c -6.866354,13.680305 -20.345548,22.980694 -35.852198,22.980694 -1.052703,0 -2.095999,-0.04291 -3.128684,-0.127026 3.052002,-1.561371 5.913836,-3.480218 8.538402,-5.707305 z M 94.355885,24.322999 c -3.13939,-5.314721 -7.467551,-9.74275 -12.584511,-12.853269 1.593656,-0.26719 3.226904,-0.406948 4.890779,-0.406948 1.239451,0 2.461512,0.07952 3.663524,0.229364 4.016018,3.607242 7.373195,8.030111 9.849053,13.030853 z" + id="path2352" /> + <path + d="m 41.762589,1.1211936 c 1.064296,0 2.118804,0.044379 3.162607,0.1302161 -3.046523,1.558961 -5.903162,3.4745139 -8.52358,5.6968133 C 19.254624,9.7205882 6.1097128,25.583465 6.1097128,44.738654 c 0,21.108568 15.9624012,38.22057 35.6528762,38.22057 12.599746,0 23.672446,-7.007056 30.013748,-17.583802 h 5.838515 C 70.748498,79.055727 57.26924,88.356116 41.762589,88.356116 c -22.470907,0 -40.6871998,-19.52825 -40.6871998,-43.617462 0,-24.089211 18.2162928,-43.6174604 40.6871998,-43.6174604 z M 71.905375,24.322999 c -1.31192,-2.220567 -2.830984,-4.287049 -4.528877,-6.166508 1.342452,-1.120945 2.771374,-2.128381 4.275139,-3.00723 2.372984,2.753011 4.418875,5.834636 6.072489,9.173738 z" + id="path2354" /> + </g> + <g + id="letters" + style="fill:#FFF"> + <path + d="m 76.135411,34.409066 h 9.161042 V 29.36588 H 61.857537 v 5.043186 h 9.161137 v 25.92317 h 5.116737 z" + id="path2346" /> + <path + d="m 92.647571,52.856334 h 13.659009 l 2.93009,7.476072 h 5.36461 L 101.89122,29.144903 H 97.187186 L 84.477089,60.332406 h 5.199533 z m 11.802109,-4.822276 h -9.944771 l 4.951718,-12.386462 z" + id="path2362" /> + <path + d="m 123.80641,29.366084 h -4.58038 v 30.966322 h 20.54728 v -4.910253 c -5.32227,0 -10.64463,0 -15.9669,0 z" + id="path2356" /> + <path + d="m 166.4722,29.366084 h -21.37564 v 30.966322 h 21.58203 v -4.910253 h -16.54771 v -8.27275 h 14.48439 V 42.23925 h -14.48439 v -7.962811 h 16.34132 z" + id="path2360" /> + <path + d="m 191.19035,39.474593 c 0,1.59947 -0.53646,2.87535 -1.61628,3.818883 -1.07281,0.95124 -2.52409,1.422837 -4.34678,1.422837 h -7.44851 V 34.276439 h 7.4073 c 1.9051,0 3.38376,0.435027 4.42939,1.312178 1.05226,0.870258 1.57488,2.167734 1.57488,3.885976 z m 6.06602,20.857813 -7.79911,-11.723191 c 1.01771,-0.294794 1.94631,-0.714813 2.78553,-1.260566 0.83885,-0.545619 1.56122,-1.209263 2.16629,-1.990627 0.60541,-0.781738 1.07981,-1.681096 1.42369,-2.698345 0.34378,-1.017553 0.51561,-2.175238 0.51561,-3.472883 0,-1.50409 -0.24743,-2.867948 -0.74267,-4.092048 -0.49515,-1.223794 -1.20344,-2.256186 -2.12499,-3.096734 -0.92173,-0.840446 -2.04957,-1.489252 -3.38375,-1.946452 -1.33447,-0.457267 -2.82692,-0.685476 -4.4774,-0.685476 h -12.87512 v 30.966322 h 5.03433 V 49.538522 h 6.37569 l 7.11829,10.793884 z" + id="path2358" /> + </g> + </g> +</svg> diff --git a/packages/bank/src/components/app.tsx b/packages/bank/src/components/app.tsx @@ -2,12 +2,10 @@ import { FunctionalComponent, h } from "preact"; import { TranslationProvider } from "../context/translation"; import { BankHome } from "../pages/home/index"; import { Menu } from "./menu"; -import { LangSelector } from "./menu/LangSelector"; const App: FunctionalComponent = () => { return ( <TranslationProvider> - <LangSelector /> <BankHome /> </TranslationProvider> ); diff --git a/packages/bank/src/components/menu/LangSelector.tsx b/packages/bank/src/components/menu/LangSelector.tsx @@ -19,7 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { h, VNode } from "preact"; +import { h, VNode, Fragment } from "preact"; import { useState } from "preact/hooks"; import langIcon from "../../assets/icons/languageicon.svg"; import { useTranslationContext } from "../../context/translation"; @@ -43,6 +43,39 @@ function getLangName(s: keyof LangsNames | string): string { return String(s); } +export function LangSelectorLikePy(): VNode { + const [updatingLang, setUpdatingLang] = useState(false); + const { lang, changeLanguage } = useTranslationContext(); + + return ( + <Fragment> + <span>{getLangName(lang)}</span> + <div style="position: relative; overflow: visible;"> + <div + class="nav" + style="position: absolute; background: #0042b2; max-height: 60vh; overflow-y: scroll"> + <br></br> + {Object.keys(messages) + .filter((l) => l !== lang) + .map((l) => ( + <a + key={l} + class="navbtn" + value={l} + onClick={() => { + changeLanguage(l); + setUpdatingLang(false); + }}> + {getLangName(l)} + </a> + ))} + <br></br> + </div> + </div> + </Fragment> + ); +} + export function LangSelector(): VNode { const [updatingLang, setUpdatingLang] = useState(false); const { lang, changeLanguage } = useTranslationContext(); diff --git a/packages/bank/src/pages/home/index.tsx b/packages/bank/src/pages/home/index.tsx @@ -6,6 +6,8 @@ import { useTranslator, Translate } from "../../i18n"; import { QR } from "../../components/QR"; import { useNotNullLocalStorage, useLocalStorage } from "../../hooks"; import "../../scss/main.scss"; +import talerLogo from "../../assets/logo-white.svg"; +import { LangSelectorLikePy as LangSelector} from "../../components/menu/LangSelector"; /** * FIXME: @@ -28,11 +30,11 @@ import "../../scss/main.scss"; * Globals * **********/ - /************ * Contexts * ***********/ var CurrencyContext = createContext<any>(null); +var PageContext = createContext<any>(null); /********************************************** * Type definitions for states and API calls. * @@ -106,6 +108,13 @@ interface AccountStateType { ***********/ /** + * Bring the state to show the public accounts page. + */ +function goPublicAccounts(pageStateSetter: StateUpdater<PageStateType>) { + return () => pageStateSetter((prevState) => ({...prevState, showPublicHistories: true})) +} + +/** * Validate (the number part of) an amount. If needed, * replace comma with a dot. Returns 'false' whenever * the input is invalid, the valid amount otherwise. @@ -277,6 +286,7 @@ function usePageState( ): [PageStateType, StateUpdater<PageStateType>] { const ret = useNotNullLocalStorage("page-state", JSON.stringify(state)); const retObj: PageStateType = JSON.parse(ret[0]); + console.log("Current page state", retObj); const retSetter: StateUpdater<PageStateType> = function(val) { const newVal = val instanceof Function ? JSON.stringify(val(retObj)) : JSON.stringify(val) console.log("Setting new page state", newVal) @@ -624,8 +634,10 @@ async function registrationCall( headers: headers }); } catch (error) { - console.log("Could not POST new registration to the bank", error); - pageStateSetter((prevState) => ({ ...prevState, hasError: true })); + console.log(`Could not POST new registration to the bank (${url.href})`, error); + pageStateSetter((prevState) => ({ + ...prevState, hasError: true, error: "Registration failed, please report." + })); return; } if (!res.ok) { @@ -656,6 +668,74 @@ async function registrationCall( * Functional components. * *************************/ +function ErrorBanner(Props: any): VNode { + const [pageState, pageStateSetter] = Props.pageState; + const i18n = useTranslator(); + if (!pageState.hasError) return; + return ( + <p class="informational informational-fail">{pageState.error} + &nbsp;&nbsp;<a href="#" onClick={() => { + pageStateSetter((prevState: PageStateType) =>({...prevState, hasError: false}))}}> + {i18n`Clear`} + </a> + </p>); +} + +function BankFrame(Props: any): VNode { + const i18n = useTranslator(); + const [pageState, pageStateSetter] = useContext(PageContext); + console.log("BankFrame state", pageState); + return ( + <Fragment> + <header class="demobar" style="display: flex; flex-direction: row; justify-content: space-between;"> + <div style="max-width: 50em; margin-left: 2em;"> + <h1> + <span class="it"> + <a href="/">{i18n`Demo Bank`}</a> + </span> + </h1> + <p><Translate> + This part of the demo shows how a bank that supports + Taler directly would work. In addition to using your own + bank account, you can also see the transaction history of + some <a href="#" onclick={goPublicAccounts(pageStateSetter)}>Public Accounts</a>. + </Translate></p> + </div> + <a href="https://taler.net/"> + <img + src={talerLogo} + height="100" + width="224" + style="margin: 2em 2em"> + </img> + </a> + </header> + <div style="display:flex; flex-direction: column;" class="navcontainer"> + <nav class="demolist"> + <a href="#">DEMO SITE 0</a> + <a href="#">DEMO SITE 1</a> + <a href="#">DEMO SITE 2</a> + <a href="#">DEMO SITE 3</a> + <a href="#">DEMO SITE 4</a> + <a href="#">DEMO SITE 5</a> + <div class="right"> + <LangSelector /> + </div> + </nav> + </div> + <section id="main" class="content"> + <ErrorBanner pageState={[pageState, pageStateSetter]}/> + {Props.children} + <hr></hr> + <div> + <p>You can learn more about GNU Taler on our <a href="https://taler.net">main website</a>.</p> + </div> + <div style="flex-grow:1"></div> + <p>Copyright &copy; 2014&mdash;2022 Taler Systems SA</p> + </section> + </Fragment>); +} + function PaytoWireTransfer(Props: any): VNode { const {backendState, pageStateSetter} = Props; const currency = useContext(CurrencyContext); @@ -730,18 +810,30 @@ function TalerWithdrawal(Props: any): VNode { )}}>{i18n`Charge Taler wallet`} </button>; - return <div> - <input - type="text" - placeholder="amount" - required - pattern={amountRegex} - onInput={(e): void => { - submitAmount = e.currentTarget.value - }} /> - <label>{currency}</label> - { submitButton } - </div> + return (<article> + <div> + <h2>{i18n`Withdraw Money into a Taler wallet`}</h2> + <div id="reserve-form" + class="pure-form" + method="post" + name="tform"> + {i18n`Amount to withdraw`} + <select id="reserve-amount" name="withdraw-amount" class="amount" autofocus> + <option value="5.00">5.00</option> + <option value="10.00">10.00</option> + <option value="15.00">15.00</option> + <option value="20.00">20.00</option> + </select> + <input + type="text" + readonly + class="currency-indicator" + size={balance.currency.length} + tabindex="-1" value="{{ currency }}" /> + {submitButton} + </div> + </div> + </article>); } /** @@ -751,82 +843,107 @@ function LoginForm(Props: any): VNode { const {backendStateSetter, pageStateSetter} = Props; var submitData: CredentialsRequestType; const i18n = useTranslator(); - return <Fragment> - <input - type="text" - placeholder="username" - required - onInput={(e): void => { - submitData = { - ...submitData, - username: e.currentTarget.value, - };}} /> - <input - type="text" - placeholder="password" - required - onInput={(e): void => { - submitData = { - ...submitData, - password: e.currentTarget.value, - };}} /> - <button - onClick={() => { - loginCall( - submitData, - backendStateSetter, - pageStateSetter - ); - }}>{i18n`Login`}</button> - </Fragment> + // FIXME: try removing the outer Fragment. + return <div class="login-form"> + <h2>{i18n`Please login!`}</h2> + <div class="pure-form"> + <input + type="text" + placeholder="username" + required + onInput={(e): void => { + submitData = { + ...submitData, + username: e.currentTarget.value, + };}} /> + <input + type="password" + placeholder="password" + required + onInput={(e): void => { + submitData = { + ...submitData, + password: e.currentTarget.value, + };}} /> + <button + type="submit" + class="pure-button pure-button-primary" + onClick={() => { + if (submitData.password.length > 0 && submitData.username.length > 0) + loginCall( + submitData, + backendStateSetter, + pageStateSetter + ); + }}>{i18n`Login`}</button> + </div> + </div> } /** * Collect and submit registration data. */ function RegistrationForm(Props: any): VNode { - var {pageState, pageStateSetter} = Props; - var submitData: CredentialsRequestType; + const [pageState, pageStateSetter] = useContext(PageContext); + var submitData: CredentialsRequestType = {}; const i18n = useTranslator(); - return (<Fragment> - <h2>Register!</h2> - <input - type="text" - placeholder="username" - required - onInput={(e): void => { - submitData = { - ...submitData, - username: e.currentTarget.value, - };}} /> - <input - type="text" - placeholder="password" - required - onInput={(e): void => { - submitData = { - ...submitData, - password: e.currentTarget.value, - };}} /> - <button - onClick={() => { - registrationCall( - submitData, - Props.backendStateSetter, - pageStateSetter - );}}>{i18n`Register`}</button> - <a onClick={() => { - pageStateSetter((prevState: PageStateType) => - ({...prevState, tryRegister: false}))}}>Go back</a> - </Fragment>); + + return ( + <BankFrame> + <h1 class="nav">{i18n`Register to the $currency bank!`}</h1> + <aside class="sidebar" id="left"></aside> + <article> + <a href="#" onClick={() => { + pageStateSetter((prevState: PageStateType) =>({...prevState, tryRegister: false}))}}> + {i18n`Go back`} + </a> + </article> + <article> + <div class="register-form"> + <h1>{i18n`Registration form`}</h1> + <div class="pure-form"> + <input + type="text" + placeholder="username" + required + autofocus + onInput={(e): void => { + submitData = { + ...submitData, + username: e.currentTarget.value, + };}} /> + <input + type="password" + placeholder="password" + required + onInput={(e): void => { + submitData = { + ...submitData, + password: e.currentTarget.value, + };}} /> + <button + class="pure-button pure-button-primary" + onClick={() => { + if (!("password" in submitData) || !("username" in submitData)) return; + if (submitData.password.length === 0 || submitData.username.length === 0) return; + registrationCall( + submitData, + Props.backendStateSetter, // will store BE URL, if OK. + pageStateSetter + );}}>{i18n`Register`}</button> + </div> + </div> + </article> + </BankFrame> + ) } /** * Show one page of transactions. */ function Transactions(Props: any): VNode { - const { pageNumber, accountLabel } = Props; + const i18n = useTranslator(); const { data, error } = useSWR( `access-api/accounts/${accountLabel}/transactions?page=${pageNumber}` ); @@ -849,19 +966,37 @@ function Transactions(Props: any): VNode { return <p>"Transactions page loading..."</p>; } console.log(`History data of ${accountLabel}`, data); - return <ul>{ - data.transactions.map(function(item: any) { - const sign = item.direction == "DBIT" ? "-" : ""; - const counterpart = item.direction == "DBIT" ? item.creditorIban : item.debtorIban; - // Pattern: - // - // DD/MM YYYY subject -5 EUR - // DD/MM YYYY subject 5 EUR - const dateRegex = /^([0-9]{4})-([0-9]{2})-([0-9]{1,2})/ - const dateParse = dateRegex.exec(item.date) - const date = dateParse !== null ? `${dateParse[3]}/${dateParse[2]} ${dateParse[1]}` : "date not found" - return <li>{date} {item.subject} {sign}{item.amount} {item.currency}</li> - })}</ul> + return (<div class="results"> + <table class="pure-table pure-table-striped"> + <thead> + <tr> + <th>{i18n`Date`}</th> + <th>{i18n`Amount`}</th> + <th>{i18n`Counterpart`}</th> + <th>{i18n`Subject`}</th> + </tr> + </thead> + <tbody> + {data.transactions.map(function(item: any) { + const sign = item.direction == "DBIT" ? "-" : ""; + const counterpart = item.direction == "DBIT" ? item.creditorIban : item.debtorIban; + // Pattern: + // + // DD/MM YYYY subject -5 EUR + // DD/MM YYYY subject 5 EUR + const dateRegex = /^([0-9]{4})-([0-9]{2})-([0-9]{1,2})/ + const dateParse = dateRegex.exec(item.date) + const date = dateParse !== null ? `${dateParse[3]}/${dateParse[2]} ${dateParse[1]}` : "date not found" + return (<tr> + <td>{date}</td> + <td>{sign}{item.amount} {item.currency}</td> + <td>{counterpart}</td> + <td>{item.subject}</td> + </tr>); + })} + </tbody> + </table> + </div>); } /** @@ -873,6 +1008,7 @@ function Account(Props: any): VNode { transferOutcome, talerWithdrawUri, accountLabel } = Props; + const pageState = useContext(PageContext); /** * This part shows a list of transactions: with 5 elements by * default and offers a "load more" button. @@ -880,7 +1016,7 @@ function Account(Props: any): VNode { var [txPageNumber, setTxPageNumber] = useTransactionPageNumber() var txsPages = [] for (let i = 0; i <= txPageNumber; i++) { - txsPages.push(<Transactions accountLabel={Props.accountLabel} pageNumber={i} />) + txsPages.push(<Transactions accountLabel={accountLabel} pageNumber={i} />) } const i18n = useTranslator(); /** @@ -889,18 +1025,41 @@ function Account(Props: any): VNode { const { data, error } = useSWR(`access-api/accounts/${accountLabel}`); if (typeof error !== "undefined") { console.log("account error", error); + + /** + * FIXME: try only one invocation of pageStateSetter, + * after having decided the error message in the case-branch. + */ switch(error.status) { case 404: { - return <p>Username or account label not found</p> + pageState[1]((prevState) => ({ + ...prevState, + hasError: true, + isLoggedIn: false, + error: i18n`Username or account label not found.` + })); + return <p>Profile not found...</p>; } case 401: { - return <p>Wrong credentials given</p> + pageState[1]((prevState) => ({ + ...prevState, + hasError: true, + isLoggedIn: false, + error: i18n`Wrong credentials given.` + })); + return <p>Wrong credentials...</p>; } default: { - return <p>Account information could not be retrieved.</p> + pageState[1]((prevState) => ({ + ...prevState, + hasError: true, + isLoggedIn: false, + error: i18n`Account information could not be retrieved.` + })); + return <p>Unknown problem...</p>; + } } } - } if (!data) return <p>Retrieving the profile page...</p>; /** @@ -909,20 +1068,20 @@ function Account(Props: any): VNode { * have been reported earlier. */ if (transferOutcome) { - return <Fragment> + return <BankFrame> <p>{transferOutcome}</p> {Props.children} - </Fragment> + </BankFrame> } /** * Withdrawal reached a final state: show it. */ if (withdrawalOutcome) { - return <Fragment> + return <BankFrame> <p>{withdrawalOutcome}</p> {Props.children} - </Fragment> + </BankFrame> } /** @@ -948,17 +1107,29 @@ function Account(Props: any): VNode { </Fragment>); } const balance = parseAmount(data.balance.amount) - return (<Fragment> - <p>Hello {accountLabel}, {getIbanFromPayto(data.paytoUri)}.</p> - <p>Your balance is {`${balance.value} ${balance.currency}`}.</p> + return (<BankFrame> <div> - <span>{i18n`Last transactions:`}</span> { txsPages } - <button onClick={() => setTxPageNumber(txPageNumber + 1)}>{i18n`Load more transactions`}</button> + <h1 class="nav"> + <Translate>Welcome, {accountLabel} ({getIbanFromPayto(data.paytoUri)})!</Translate> + </h1> + <a + href="#" + class="pure-button logout-button" + onClick={() => { + setTxPageNumber(0); + pageStateSetter((prevState) => { + const { + talerWithdrawUri, + withdrawalOutcome, + withdrawalId, ...rest } = prevState; + return {...rest, isLoggedIn: false, withdrawalInProgress: false}; + }) + }}>[{i18n`Logout`}]</a><br /> </div> - <CurrencyContext.Provider value={balance.currency}> - {Props.children} - </CurrencyContext.Provider> - </Fragment>); + <section id="menu"> + <p>{i18n`Bank account balance:`} <br /> <b>{`${balance.value} ${balance.currency}`}</b></p> + </section> + </BankFrame>); } /** @@ -1013,14 +1184,30 @@ function SWRWithoutCredentials(Props: any): VNode { function PublicHistories(Props: any): VNode { const [showAccount, setShowAccount] = useState<string | undefined>(); const { data, error } = useSWR("access-api/public-accounts") + const i18n = useTranslator(); + if (typeof error !== "undefined") { console.log("account error", error); switch(error.status) { case 404: { - return <p>List of public accounts was not found</p> + console.log("public accounts: 404", error); + Props.pageStateSetter((prevState) => ({ + ...prevState, + hasError: true, + showPublicHistories: false, + error: i18n`List of public accounts was not found.` + })); + return; } default: { - return <p>List of public accounts could not be retrieved.</p> + console.log("public accounts: non-404 error", error); + Props.pageStateSetter((prevState) => ({ + ...prevState, + hasError: true, + showPublicHistories: false, + error: i18n`List of public accounts could not be retrieved.` + })); + return; } } } @@ -1028,17 +1215,6 @@ function PublicHistories(Props: any): VNode { var txs: any = {}; var accountsBar = []; - // Ask first story of all the public accounts. - for (const account of data.publicAccounts) { - console.log("Asking transactions for", account.accountLabel) - accountsBar.push( - <li><a onClick={() => setShowAccount(account.accountLabel)}>{account.accountLabel}</a></li> - ); - txs[account.accountLabel] = - <div>{account.accountLabel} latest transactions: - <Transactions accountLabel={account.accountLabel} pageNumber={0} /> - </div> - } /** * Show the account specified in the props, or just one * from the list if that's not given. @@ -1046,10 +1222,29 @@ function PublicHistories(Props: any): VNode { if (typeof showAccount === "undefined" && Object.keys(txs).length > 0) setShowAccount(Object.keys(txs).pop()); console.log(`Public history tab: ${showAccount}`); + + // Ask first story of all the public accounts. + for (const account of data.publicAccounts) { + console.log("Asking transactions for", account.accountLabel) + accountsBar.push( + <li> + <a + class={(account.accountLabel == showAccount) ? "pure-menu-item pure-menu" : "pure-menu-item pure-menu-selected"} + onClick={() => setShowAccount(account.accountLabel)}>{account.accountLabel}</a> + </li> + ); + txs[account.accountLabel] = <Transactions accountLabel={account.accountLabel} pageNumber={0} /> + } + return <Fragment> - <ul>{accountsBar}</ul> - {typeof showAccount !== "undefined" ? txs[showAccount] : <p>No public transactions found.</p>} - {Props.children} + <h1 class="nav">{i18n`History of public accounts`}</h1> + <section id="main"> + <div name="accountMenu" class="pure-menu pure-menu-horizontal"> + <ul class="pure-menu-list">{accountsBar}</ul> + {typeof showAccount !== "undefined" ? txs[showAccount] : <p>No public transactions found.</p>} + {Props.children} + </div> + </section> </Fragment>; } @@ -1064,18 +1259,9 @@ export function BankHome(): VNode { const setTxPageNumber = useTransactionPageNumber()[1]; var i18n = useTranslator(); - if (pageState.hasError) { - return <Fragment> - <p>{i18n`Page has a problem:`} {pageState.error}</p> - <a onClick={() => { - pageStateSetter((prevState) => ({...prevState, hasError: false})) - }}>{i18n`Go back`}</a> - </Fragment>; - } - if (pageState.showPublicHistories) { return (<SWRWithoutCredentials baseUrl={getRootPath()}> - <PublicHistories> + <PublicHistories pageStateSetter={pageStateSetter}> <a onClick={() => { pageStateSetter((prevState: PageStateType) => ({...prevState, showPublicHistories: false}))}}>Go back</a> @@ -1084,10 +1270,11 @@ export function BankHome(): VNode { } if (pageState.tryRegister) { - return (<RegistrationForm - pageState={pageState} - pageStateSetter={pageStateSetter} - backendStateSetter={backendStateSetter} />); + return ( + <PageContext.Provider value={[pageState, pageStateSetter]}> + <RegistrationForm backendStateSetter={backendStateSetter} /> + </PageContext.Provider> + ); } /** * Credentials were correct, now render the bank account page, @@ -1099,32 +1286,29 @@ export function BankHome(): VNode { pageStateSetter((prevState) => ({ ...prevState, hasError: true, + isLoggedIn: false, error: i18n`Page has a problem: logged in but backend state is lost.` })); return <p>Error: waiting for details...</p>; } - + console.log("Showing the profile page.."); return ( <SWRWithCredentials - username={backendState.username} - password={backendState.password} - backendUrl={backendState.url}> - <Fragment> - <Account - withdrawalOutcome={pageState.withdrawalOutcome} - talerWithdrawUri={pageState.talerWithdrawUri} - transferOutcome={pageState.transferOutcome} - accountLabel={backendState.username}> + username={backendState.username} + password={backendState.password} + backendUrl={backendState.url}> + <PageContext.Provider value={[pageState, pageStateSetter]}> + <Account> { /** * No action is currently being performed (page is 'pristine'): - * offer the Taler withdrawal button. + * offer the Taler withdrawal button. */ !pageState.withdrawalInProgress && !pageState.transferOutcome && <TalerWithdrawal - backendState={backendState} - pageStateSetter={pageStateSetter} /> + backendState={backendState} + pageStateSetter={pageStateSetter} /> } - + { /** * Wire transfer reached a persisten state: offer to * return back to the pristine profile page. @@ -1143,10 +1327,10 @@ export function BankHome(): VNode { pageStateSetter((prevState) => { const { withdrawalOutcome, withdrawalId, ...rest } = prevState; return { - ...rest, - withdrawalInProgress: false - };} - )}}>{i18n`Close Taler withdrawal`}</button> + ...rest, + withdrawalInProgress: false + };} + )}}>{i18n`Close Taler withdrawal`}</button> } { /** @@ -1164,36 +1348,49 @@ export function BankHome(): VNode { pageState.withdrawalId, pageStateSetter);}}>{i18n`Abort withdrawal`}</button> </div> - } - - { /** - * Profile page is pristine: offer the wire transfer form. - */ + } + + { /** + * Profile page is pristine: offer the wire transfer form. + */ !pageState.withdrawalInProgress && - !pageState.transferOutcome && - <PaytoWireTransfer pageStateSetter={pageStateSetter} - backendState={backendState} /> - } + !pageState.transferOutcome && + <PaytoWireTransfer pageStateSetter={pageStateSetter} + backendState={backendState} /> + } </Account> - { /* The user is logged in: offer to log out. */ } - <button onClick={() => { - setTxPageNumber(0); - pageStateSetter((prevState) => { - const { - talerWithdrawUri, - withdrawalOutcome, - withdrawalId, ...rest } = prevState; - return {...rest, isLoggedIn: false, withdrawalInProgress: false}; - }) - }}>Sign out</button> - </Fragment> + </PageContext.Provider> </SWRWithCredentials> ); } // end of logged-in state. + /** + * Currency only known _after_ a user logs in / registers. Thus not + * mentioning the currency right at the home page (as instead the Python + * bank did.) FIXME: currency needed at startup too. + */ return ( - <Fragment> - <p>{i18n`Welcome to euFin bank: Taler+IBAN now possible!`}</p> - <SWRWithoutCredentials baseUrl={getRootPath()}> + <PageContext.Provider value={[pageState, pageStateSetter]}> + <BankFrame> + <h1 class="nav">{i18n`Welcome to the $currency bank!`}</h1> + <LoginForm + pageStateSetter={pageStateSetter} + backendStateSetter={backendStateSetter} /> + <p><Translate> + If you are a new customer please <a href="#" onClick={() => { + pageStateSetter((prevState) => ({...prevState, tryRegister: true}))}}>register!</a> + &nbsp;&nbsp; Registration is fast and free, and it gives you a registration bonus + of 100 $currency + </Translate></p> + <p><Translate>To view transactions of public accounts, please <a href="#" + onClick={goPublicAccounts(pageStateSetter)}>click here</a>. + </Translate></p> + </BankFrame> + </PageContext.Provider> + ); +} + +/* +<SWRWithoutCredentials baseUrl={getRootPath()}> <LoginForm pageStateSetter={pageStateSetter} backendStateSetter={backendStateSetter} /> @@ -1203,6 +1400,4 @@ export function BankHome(): VNode { pageStateSetter((prevState) => ( {...prevState, showPublicHistories: true}))}}>transactions</a></p> </SWRWithoutCredentials> - </Fragment> - ); -} +*/ diff --git a/packages/bank/src/scss/bank.scss b/packages/bank/src/scss/bank.scss @@ -0,0 +1,80 @@ +.abort-button { + margin-left: 2px; + border: 2px solid rgb(0, 120, 231); + color: rgb(0, 120, 231); + font-size: 87%; + margin-top: 1px; + background: white; +} + +div.pages-list { + margin-top: 15px; +} + +a.page-number { + color: blue; +} + +a.current-page-number { + color: inherit; +} + +.cancelled { + text-decoration: line-through; +} + +input[type="number"]::-webkit-outer-spin-button, +input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +input[type="number"] { + -moz-appearance: textfield; +} + +#transfer-fields { + display: flex; + flex-wrap: wrap; +} + +#id_amount { + width: 6em; + display: inline-block; + border-radius: 4px 0px 0px 4px; +} + +/** + * Amount without the currency, + * placed left to a .currency-indicator. + */ +#main .amount { + width: 6em; + display: inline-block; + border-radius: 4px 0px 0px 4px; +} + +/* + * Currency indicator to the right of input fields, + * with non-rounded corners to the left. + */ +#main .currency-indicator { + color: black; + display: inline-block; + border-radius: 0px 4px 4px 0px; +} + +#main .fieldlabel { + display: block; + padding-bottom: 0.5em; +} + +#main .fieldbox { + margin-right: 1em; + margin-bottom: 0.5em; +} + +#logout-button { + display: block; + width: fit-content; +} diff --git a/packages/bank/src/scss/colors-bank.scss b/packages/bank/src/scss/colors-bank.scss @@ -0,0 +1,24 @@ +nav, +nav a, +nav span, +.navcontainer, +.demobar, +.navbtn { + color: white; + background: #c00000; +} + +nav a.active, +nav span.active, +.navbtn.active { + background-color: #7a0606; +} + +nav a.active:hover, +nav span.active:hover, +.navbtn.active:hover, +nav a:hover, +nav span:hover, +.navbtn:hover { + background: #df3d3d; +} diff --git a/packages/bank/src/scss/demo.scss b/packages/bank/src/scss/demo.scss @@ -0,0 +1,138 @@ +@charset "UTF-8"; +/* +Style common to all demo pages. + +Colors: +- #1e2739 (dark blue) +- #0042b2 (default blue) +- #3daee9 (highlight blue) +*/ + +.demobar h1 { + text-align: center; +} + +.demobar > p { + padding: 0.5em; +} + +.demobar a, +.demobar a:visited { + color: inherit; +} + +.tt { + font-family: "Lucida Console", Monaco, monospace; +} + +.informational-ok { + background: lightgreen; + border-radius: 1em; + padding: 0.5em; +} + +.informational-fail { + background: lightpink; + border-radius: 1em; + padding: 0.5em; +} + +.content { + margin-left: 2em; + overflow-x: auto; +} + +.demobar { + overflow-x: auto; + background-color: #0042b2; + color: white; +} + +body { + overflow-x: hidden; + overflow-y: auto; +} + +.navcontainer { + background: #0042b2; + margin-bottom: 50px; + width: 100%; + color: white; + position: -webkit-sticky; + position: sticky; + top: 0px; + width: 100vw; + backdrop-filter: blur(10px); + opacity: 1; + z-index: 10000; +} + +nav { + left: 1vw; + position: relative; + background: #0042b2; + z-index: 10000; +} + +nav a, +nav span, +.navbtn { + border: none; + color: white; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 16px; + background: #0042b2; + height: inherit; +} + +nav a, +nav span, +.navbtn { + padding: 15px 32px; +} + +nav a:hover, +nav span:hover, +.navbtn:hover { + background: #3daee9; +} + +nav a.active, +nav span.active, +.navbtn.active { + background-color: #1e2739; +} + +nav a.active:hover, +nav span.active:hover, +.navbtn.active:hover { + background: #3daee9; +} + +nav a, +nav span, +.navbtn { + cursor: pointer; +} + +nav .right { + float: right; + margin-right: 5vw; +} +nav .right div.nav { + display: none; +} +nav .right div.nav:hover { + display: block; +} + +nav .right:hover div.nav { + display: block; +} + +.langbtn { + width: 100%; + text-align: left; +} diff --git a/packages/bank/src/scss/main.scss b/packages/bank/src/scss/main.scss @@ -1,234 +1,4 @@ -/* - This file is part of GNU Taler - (C) 2021 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -/* Theme style (colors & sizes) */ -@import "theme-default"; - -/* Core Libs & Lib configs */ -@import "libs/all"; - -/* Mixins */ -@import "mixins"; - -/* Theme components */ -@import "nav-bar"; -@import "aside"; -@import "title-bar"; -@import "hero-bar"; -@import "card"; -@import "table"; -@import "tiles"; -@import "form"; -@import "main-section"; -@import "modal"; -@import "footer"; -@import "misc"; -@import "custom-calendar"; -@import "loading"; - -@import "fonts/nunito.css"; -@import "icons/materialdesignicons-4.9.95.min.css"; - -$tooltip-color: red; - -@import "../../node_modules/@creativebulma/bulma-tooltip/dist/bulma-tooltip.min.css"; -// @import "../../node_modules/bulma-timeline/dist/css/bulma-timeline.min.css"; - -.notification { - background-color: transparent; -} - -.timeline .timeline-item .timeline-content { - padding-top: 0; -} - -.timeline .timeline-item:last-child::before { - display: none; -} - -.timeline .timeline-item .timeline-marker { - top: 0; -} - -.toast { - position: absolute; - width: 60%; - margin-left: 10%; - margin-right: 10%; - z-index: 999; - - display: flex; - flex-direction: column; - padding: 15px; - text-align: center; - pointer-events: none; -} - -.toast > .message { - white-space: pre-wrap; - opacity: 80%; -} - -div { - &.is-loading { - position: relative; - pointer-events: none; - opacity: 0.5; - &:after { - // @include loader; - position: absolute; - top: calc(50% - 2.5em); - left: calc(50% - 2.5em); - width: 5em; - height: 5em; - border-width: 0.25em; - } - } -} - -input[type="checkbox"]:indeterminate + .check { - background: red !important; -} - -.right-sticky { - position: sticky; - right: 0px; - background-color: $white; -} - -.right-sticky .buttons { - flex-wrap: nowrap; -} - -.table.is-striped tbody tr:not(.is-selected):nth-child(even) .right-sticky { - background-color: #fafafa; -} - -tr:hover .right-sticky { - background-color: hsl(0, 0%, 80%); -} -.table.is-striped tbody tr:nth-child(even):hover .right-sticky { - background-color: hsl(0, 0%, 95%); -} - -.content-full-size { - height: calc(100% - 3rem); - position: absolute; - width: calc(100% - 14rem); - display: flex; -} - -.content-full-size .column .card { - min-width: 200px; -} - -@include touch { - .content-full-size { - height: 100%; - position: absolute; - width: 100%; - } -} - -.column.is-half { - flex: none; - width: 50%; -} - -input:read-only { - cursor: initial; -} - -[data-tooltip]:before { - max-width: 15rem; - width: max-content; - text-align: left; - transition: opacity 0.1s linear 1s; - // transform: inherit !important; - white-space: pre-wrap !important; - font-weight: normal; - // position: relative; -} - -.icon[data-tooltip]:before { - transition: none; - z-index: 5; -} - -span[data-tooltip] { - border-bottom: none; -} - -div[data-tooltip]::before { - position: absolute; -} - -.modal-card-body > p { - padding: 1em; -} - -.modal-card-body > p.warning { - background-color: #fffbdd; - border: solid 1px #f2e9bf; -} - -.home { - padding: 1em 1em; - min-height: 100%; - width: 100%; - // max-width: 40em; -} - -// .home div { -// margin-top: 0.5em; -// margin-bottom: 0.5em; -// } - -.policy { - padding: 0.5em; - border: 1px solid black; - border-radius: 0.5em; - border-radius: 0.5em; -} - -.home > #error { - padding: 0.5em; - border: 1px solid black; - background-color: rgb(228, 189, 197); - border-radius: 0.5em; -} - -.profile { - padding: 56px 20px; - min-height: 100%; - width: 100%; -} - -.notfound { - padding: 0 5%; - margin: 100px 0; -} - -h1 { - font-size: 1.5em; - margin-top: 0.8em; - margin-bottom: 0.8em; -} +@import "pure"; +@import "bank"; +@import "demo"; +@import "colors-bank"; diff --git a/packages/bank/src/scss/pure.scss b/packages/bank/src/scss/pure.scss @@ -0,0 +1,1328 @@ +/*! +Pure v0.6.2 +Copyright 2013 Yahoo! +Licensed under the BSD License. +https://github.com/yahoo/pure/blob/master/LICENSE.md +*/ +/*! +normalize.css v^3.0 | MIT License | git.io/normalize +Copyright (c) Nicolas Gallagher and Jonathan Neal +*/ +/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ +/** + * 1. Set default font family to sans-serif. + * 2. Prevent iOS and IE text size adjust after device orientation change, + * without disabling user zoom. + */ +html { + font-family: sans-serif; + /* 1 */ + -ms-text-size-adjust: 100%; + /* 2 */ + -webkit-text-size-adjust: 100%; + /* 2 */ } + +/** + * Remove default margin. + */ +body { + margin: 0; } + +/* HTML5 display definitions + ========================================================================== */ +/** + * Correct `block` display not defined for any HTML5 element in IE 8/9. + * Correct `block` display not defined for `details` or `summary` in IE 10/11 + * and Firefox. + * Correct `block` display not defined for `main` in IE 11. + */ +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +menu, +nav, +section, +summary { + display: block; } + +/** + * 1. Correct `inline-block` display not defined in IE 8/9. + * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. + */ +audio, +canvas, +progress, +video { + display: inline-block; + /* 1 */ + vertical-align: baseline; + /* 2 */ } + +/** + * Prevent modern browsers from displaying `audio` without controls. + * Remove excess height in iOS 5 devices. + */ +audio:not([controls]) { + display: none; + height: 0; } + +/** + * Address `[hidden]` styling not present in IE 8/9/10. + * Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22. + */ +[hidden], +template { + display: none; } + +/* Links + ========================================================================== */ +/** + * Remove the gray background color from active links in IE 10. + */ +a { + background-color: transparent; } + +/** + * Improve readability of focused elements when they are also in an + * active/hover state. + */ +a:active, +a:hover { + outline: 0; } + +/* Text-level semantics + ========================================================================== */ +/** + * Address styling not present in IE 8/9/10/11, Safari, and Chrome. + */ +abbr[title] { + border-bottom: 1px dotted; } + +/** + * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. + */ +b, +strong { + font-weight: bold; } + +/** + * Address styling not present in Safari and Chrome. + */ +dfn { + font-style: italic; } + +/** + * Address variable `h1` font-size and margin within `section` and `article` + * contexts in Firefox 4+, Safari, and Chrome. + */ +h1 { + font-size: 2em; + margin: 0.67em 0; } + +/** + * Address styling not present in IE 8/9. + */ +mark { + background: #ff0; + color: #000; } + +/** + * Address inconsistent and variable font size in all browsers. + */ +small { + font-size: 80%; } + +/** + * Prevent `sub` and `sup` affecting `line-height` in all browsers. + */ +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; } + +sup { + top: -0.5em; } + +sub { + bottom: -0.25em; } + +/* Embedded content + ========================================================================== */ +/** + * Remove border when inside `a` element in IE 8/9/10. + */ +img { + border: 0; } + +/** + * Correct overflow not hidden in IE 9/10/11. + */ +svg:not(:root) { + overflow: hidden; } + +/* Grouping content + ========================================================================== */ +/** + * Address margin not present in IE 8/9 and Safari. + */ +figure { + margin: 1em 40px; } + +/** + * Address differences between Firefox and other browsers. + */ +hr { + box-sizing: content-box; + height: 0; } + +/** + * Contain overflow in all browsers. + */ +pre { + overflow: auto; } + +/** + * Address odd `em`-unit font size rendering in all browsers. + */ +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; } + +/* Forms + ========================================================================== */ +/** + * Known limitation: by default, Chrome and Safari on OS X allow very limited + * styling of `select`, unless a `border` property is set. + */ +/** + * 1. Correct color not being inherited. + * Known issue: affects color of disabled elements. + * 2. Correct font properties not being inherited. + * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. + */ +button, +input, +optgroup, +select, +textarea { + color: inherit; + /* 1 */ + font: inherit; + /* 2 */ + margin: 0; + /* 3 */ } + +/** + * Address `overflow` set to `hidden` in IE 8/9/10/11. + */ +button { + overflow: visible; } + +/** + * Address inconsistent `text-transform` inheritance for `button` and `select`. + * All other form control elements do not inherit `text-transform` values. + * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. + * Correct `select` style inheritance in Firefox. + */ +button, +select { + text-transform: none; } + +/** + * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` + * and `video` controls. + * 2. Correct inability to style clickable `input` types in iOS. + * 3. Improve usability and consistency of cursor style between image-type + * `input` and others. + */ +button, +html input[type="button"], +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; + /* 2 */ + cursor: pointer; + /* 3 */ } + +/** + * Re-set default cursor for disabled elements. + */ +button[disabled], +html input[disabled] { + cursor: default; } + +/** + * Remove inner padding and border in Firefox 4+. + */ +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; } + +/** + * Address Firefox 4+ setting `line-height` on `input` using `!important` in + * the UA stylesheet. + */ +input { + line-height: normal; } + +/** + * It's recommended that you don't attempt to style these elements. + * Firefox's implementation doesn't respect box-sizing, padding, or width. + * + * 1. Address box sizing set to `content-box` in IE 8/9/10. + * 2. Remove excess padding in IE 8/9/10. + */ +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; + /* 1 */ + padding: 0; + /* 2 */ } + +/** + * Fix the cursor style for Chrome's increment/decrement buttons. For certain + * `font-size` values of the `input`, it causes the cursor style of the + * decrement button to change from `default` to `text`. + */ +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; } + +/** + * 1. Address `appearance` set to `searchfield` in Safari and Chrome. + * 2. Address `box-sizing` set to `border-box` in Safari and Chrome. + */ +input[type="search"] { + -webkit-appearance: textfield; + /* 1 */ + box-sizing: content-box; + /* 2 */ } + +/** + * Remove inner padding and search cancel button in Safari and Chrome on OS X. + * Safari (but not Chrome) clips the cancel button when the search input has + * padding (and `textfield` appearance). + */ +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; } + +/** + * Define consistent border, margin, and padding. + */ +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; } + +/** + * 1. Correct `color` not being inherited in IE 8/9/10/11. + * 2. Remove padding so people aren't caught out if they zero out fieldsets. + */ +legend { + border: 0; + /* 1 */ + padding: 0; + /* 2 */ } + +/** + * Remove default vertical scrollbar in IE 8/9/10/11. + */ +textarea { + overflow: auto; } + +/** + * Don't inherit the `font-weight` (applied by a rule above). + * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. + */ +optgroup { + font-weight: bold; } + +/* Tables + ========================================================================== */ +/** + * Remove most spacing between table cells. + */ +table { + border-collapse: collapse; + border-spacing: 0; } + +td, +th { + padding: 0; } + +/*csslint important:false*/ +/* ========================================================================== + Pure Base Extras + ========================================================================== */ +/** + * Extra rules that Pure adds on top of Normalize.css + */ +/** + * Always hide an element when it has the `hidden` HTML attribute. + */ +.hidden, +[hidden] { + display: none !important; } + +/** + * Add this class to an image to make it fit within it's fluid parent wrapper while maintaining + * aspect ratio. + */ +.pure-img { + max-width: 100%; + height: auto; + display: block; } + +/*csslint regex-selectors:false, known-properties:false, duplicate-properties:false*/ +.pure-g { + letter-spacing: -0.31em; + /* Webkit: collapse white-space between units */ + *letter-spacing: normal; + /* reset IE < 8 */ + *word-spacing: -0.43em; + /* IE < 8: collapse white-space between units */ + text-rendering: optimizespeed; + /* Webkit: fixes text-rendering: optimizeLegibility */ + /* + Sets the font stack to fonts known to work properly with the above letter + and word spacings. See: https://github.com/yahoo/pure/issues/41/ + + The following font stack makes Pure Grids work on all known environments. + + * FreeSans: Ships with many Linux distros, including Ubuntu + + * Arimo: Ships with Chrome OS. Arimo has to be defined before Helvetica and + Arial to get picked up by the browser, even though neither is available + in Chrome OS. + + * Droid Sans: Ships with all versions of Android. + + * Helvetica, Arial, sans-serif: Common font stack on OS X and Windows. + */ + font-family: FreeSans, Arimo, "Droid Sans", Helvetica, Arial, sans-serif; + /* Use flexbox when possible to avoid `letter-spacing` side-effects. */ + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-flow: row wrap; + -ms-flex-flow: row wrap; + flex-flow: row wrap; + /* Prevents distributing space between rows */ + -webkit-align-content: flex-start; + -ms-flex-line-pack: start; + align-content: flex-start; } + +/* IE10 display: -ms-flexbox (and display: flex in IE 11) does not work inside a table; fall back to block and rely on font hack */ +@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) { + table .pure-g { + display: block; } } +/* Opera as of 12 on Windows needs word-spacing. + The ".opera-only" selector is used to prevent actual prefocus styling + and is not required in markup. +*/ +.opera-only :-o-prefocus, +.pure-g { + word-spacing: -0.43em; } + +.pure-u { + display: inline-block; + *display: inline; + /* IE < 8: fake inline-block */ + zoom: 1; + letter-spacing: normal; + word-spacing: normal; + vertical-align: top; + text-rendering: auto; } + +/* +Resets the font family back to the OS/browser's default sans-serif font, +this the same font stack that Normalize.css sets for the `body`. +*/ +.pure-g [class*="pure-u"] { + font-family: sans-serif; } + +.pure-u-1, +.pure-u-1-1, +.pure-u-1-2, +.pure-u-1-3, +.pure-u-2-3, +.pure-u-1-4, +.pure-u-3-4, +.pure-u-1-5, +.pure-u-2-5, +.pure-u-3-5, +.pure-u-4-5, +.pure-u-5-5, +.pure-u-1-6, +.pure-u-5-6, +.pure-u-1-8, +.pure-u-3-8, +.pure-u-5-8, +.pure-u-7-8, +.pure-u-1-12, +.pure-u-5-12, +.pure-u-7-12, +.pure-u-11-12, +.pure-u-1-24, +.pure-u-2-24, +.pure-u-3-24, +.pure-u-4-24, +.pure-u-5-24, +.pure-u-6-24, +.pure-u-7-24, +.pure-u-8-24, +.pure-u-9-24, +.pure-u-10-24, +.pure-u-11-24, +.pure-u-12-24, +.pure-u-13-24, +.pure-u-14-24, +.pure-u-15-24, +.pure-u-16-24, +.pure-u-17-24, +.pure-u-18-24, +.pure-u-19-24, +.pure-u-20-24, +.pure-u-21-24, +.pure-u-22-24, +.pure-u-23-24, +.pure-u-24-24 { + display: inline-block; + *display: inline; + zoom: 1; + letter-spacing: normal; + word-spacing: normal; + vertical-align: top; + text-rendering: auto; } + +.pure-u-1-24 { + width: 4.1667%; + *width: 4.1357%; } + +.pure-u-1-12, +.pure-u-2-24 { + width: 8.3333%; + *width: 8.3023%; } + +.pure-u-1-8, +.pure-u-3-24 { + width: 12.5000%; + *width: 12.4690%; } + +.pure-u-1-6, +.pure-u-4-24 { + width: 16.6667%; + *width: 16.6357%; } + +.pure-u-1-5 { + width: 20%; + *width: 19.9690%; } + +.pure-u-5-24 { + width: 20.8333%; + *width: 20.8023%; } + +.pure-u-1-4, +.pure-u-6-24 { + width: 25%; + *width: 24.9690%; } + +.pure-u-7-24 { + width: 29.1667%; + *width: 29.1357%; } + +.pure-u-1-3, +.pure-u-8-24 { + width: 33.3333%; + *width: 33.3023%; } + +.pure-u-3-8, +.pure-u-9-24 { + width: 37.5000%; + *width: 37.4690%; } + +.pure-u-2-5 { + width: 40%; + *width: 39.9690%; } + +.pure-u-5-12, +.pure-u-10-24 { + width: 41.6667%; + *width: 41.6357%; } + +.pure-u-11-24 { + width: 45.8333%; + *width: 45.8023%; } + +.pure-u-1-2, +.pure-u-12-24 { + width: 50%; + *width: 49.9690%; } + +.pure-u-13-24 { + width: 54.1667%; + *width: 54.1357%; } + +.pure-u-7-12, +.pure-u-14-24 { + width: 58.3333%; + *width: 58.3023%; } + +.pure-u-3-5 { + width: 60%; + *width: 59.9690%; } + +.pure-u-5-8, +.pure-u-15-24 { + width: 62.5000%; + *width: 62.4690%; } + +.pure-u-2-3, +.pure-u-16-24 { + width: 66.6667%; + *width: 66.6357%; } + +.pure-u-17-24 { + width: 70.8333%; + *width: 70.8023%; } + +.pure-u-3-4, +.pure-u-18-24 { + width: 75%; + *width: 74.9690%; } + +.pure-u-19-24 { + width: 79.1667%; + *width: 79.1357%; } + +.pure-u-4-5 { + width: 80%; + *width: 79.9690%; } + +.pure-u-5-6, +.pure-u-20-24 { + width: 83.3333%; + *width: 83.3023%; } + +.pure-u-7-8, +.pure-u-21-24 { + width: 87.5000%; + *width: 87.4690%; } + +.pure-u-11-12, +.pure-u-22-24 { + width: 91.6667%; + *width: 91.6357%; } + +.pure-u-23-24 { + width: 95.8333%; + *width: 95.8023%; } + +.pure-u-1, +.pure-u-1-1, +.pure-u-5-5, +.pure-u-24-24 { + width: 100%; } + +.pure-button { + /* Structure */ + display: inline-block; + zoom: 1; + line-height: normal; + white-space: nowrap; + vertical-align: middle; + text-align: center; + cursor: pointer; + -webkit-user-drag: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + box-sizing: border-box; } + +/* Firefox: Get rid of the inner focus border */ +.pure-button::-moz-focus-inner { + padding: 0; + border: 0; } + +/* Inherit .pure-g styles */ +.pure-button-group { + letter-spacing: -0.31em; + /* Webkit: collapse white-space between units */ + *letter-spacing: normal; + /* reset IE < 8 */ + *word-spacing: -0.43em; + /* IE < 8: collapse white-space between units */ + text-rendering: optimizespeed; + /* Webkit: fixes text-rendering: optimizeLegibility */ } + +.opera-only :-o-prefocus, +.pure-button-group { + word-spacing: -0.43em; } + +.pure-button-group .pure-button { + letter-spacing: normal; + word-spacing: normal; + vertical-align: top; + text-rendering: auto; } + +/*csslint outline-none:false*/ +.pure-button { + font-family: inherit; + font-size: 100%; + padding: 0.5em 1em; + color: #444; + /* rgba not supported (IE 8) */ + color: rgba(0, 0, 0, 0.8); + /* rgba supported */ + border: 1px solid #999; + /*IE 6/7/8*/ + border: none rgba(0, 0, 0, 0); + /*IE9 + everything else*/ + background-color: #E6E6E6; + text-decoration: none; + border-radius: 2px; } + +.pure-button-hover, +.pure-button:hover, +.pure-button:focus { + /* csslint ignore:start */ + filter: alpha(opacity=90); + /* csslint ignore:end */ + background-image: -webkit-linear-gradient(transparent, rgba(0, 0, 0, 0.05) 40%, rgba(0, 0, 0, 0.1)); + background-image: linear-gradient(transparent, rgba(0, 0, 0, 0.05) 40%, rgba(0, 0, 0, 0.1)); } + +.pure-button:focus { + outline: 0; } + +.pure-button-active, +.pure-button:active { + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.15) inset, 0 0 6px rgba(0, 0, 0, 0.2) inset; + border-color: #000; } + +.pure-button[disabled], +.pure-button-disabled, +.pure-button-disabled:hover, +.pure-button-disabled:focus, +.pure-button-disabled:active { + border: none; + background-image: none; + /* csslint ignore:start */ + filter: alpha(opacity=40); + /* csslint ignore:end */ + opacity: 0.40; + cursor: not-allowed; + box-shadow: none; + pointer-events: none; } + +.pure-button-hidden { + display: none; } + +.pure-button-primary, +.pure-button-selected, +a.pure-button-primary, +a.pure-button-selected { + background-color: #0078e7; + color: #fff; } + +/* Button Groups */ +.pure-button-group .pure-button { + margin: 0; + border-radius: 0; + border-right: 1px solid #111; + /* fallback color for rgba() for IE7/8 */ + border-right: 1px solid rgba(0, 0, 0, 0.2); } + +.pure-button-group .pure-button:first-child { + border-top-left-radius: 2px; + border-bottom-left-radius: 2px; } + +.pure-button-group .pure-button:last-child { + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + border-right: none; } + +/*csslint box-model:false*/ +/* +Box-model set to false because we're setting a height on select elements, which +also have border and padding. This is done because some browsers don't render +the padding. We explicitly set the box-model for select elements to border-box, +so we can ignore the csslint warning. +*/ +.pure-form input[type="text"], +.pure-form input[type="password"], +.pure-form input[type="email"], +.pure-form input[type="url"], +.pure-form input[type="date"], +.pure-form input[type="month"], +.pure-form input[type="time"], +.pure-form input[type="datetime"], +.pure-form input[type="datetime-local"], +.pure-form input[type="week"], +.pure-form input[type="number"], +.pure-form input[type="search"], +.pure-form input[type="tel"], +.pure-form input[type="color"], +.pure-form select, +.pure-form textarea { + padding: 0.5em 0.6em; + display: inline-block; + border: 1px solid #ccc; + box-shadow: inset 0 1px 3px #ddd; + border-radius: 4px; + vertical-align: middle; + box-sizing: border-box; } + +/* +Need to separate out the :not() selector from the rest of the CSS 2.1 selectors +since IE8 won't execute CSS that contains a CSS3 selector. +*/ +.pure-form input:not([type]) { + padding: 0.5em 0.6em; + display: inline-block; + border: 1px solid #ccc; + box-shadow: inset 0 1px 3px #ddd; + border-radius: 4px; + box-sizing: border-box; } + +/* Chrome (as of v.32/34 on OS X) needs additional room for color to display. */ +/* May be able to remove this tweak as color inputs become more standardized across browsers. */ +.pure-form input[type="color"] { + padding: 0.2em 0.5em; } + +.pure-form input[type="text"]:focus, +.pure-form input[type="password"]:focus, +.pure-form input[type="email"]:focus, +.pure-form input[type="url"]:focus, +.pure-form input[type="date"]:focus, +.pure-form input[type="month"]:focus, +.pure-form input[type="time"]:focus, +.pure-form input[type="datetime"]:focus, +.pure-form input[type="datetime-local"]:focus, +.pure-form input[type="week"]:focus, +.pure-form input[type="number"]:focus, +.pure-form input[type="search"]:focus, +.pure-form input[type="tel"]:focus, +.pure-form input[type="color"]:focus, +.pure-form select:focus, +.pure-form textarea:focus { + outline: 0; + border-color: #129FEA; } + +/* +Need to separate out the :not() selector from the rest of the CSS 2.1 selectors +since IE8 won't execute CSS that contains a CSS3 selector. +*/ +.pure-form input:not([type]):focus { + outline: 0; + border-color: #129FEA; } + +.pure-form input[type="file"]:focus, +.pure-form input[type="radio"]:focus, +.pure-form input[type="checkbox"]:focus { + outline: thin solid #129FEA; + outline: 1px auto #129FEA; } + +.pure-form .pure-checkbox, +.pure-form .pure-radio { + margin: 0.5em 0; + display: block; } + +.pure-form input[type="text"][disabled], +.pure-form input[type="password"][disabled], +.pure-form input[type="email"][disabled], +.pure-form input[type="url"][disabled], +.pure-form input[type="date"][disabled], +.pure-form input[type="month"][disabled], +.pure-form input[type="time"][disabled], +.pure-form input[type="datetime"][disabled], +.pure-form input[type="datetime-local"][disabled], +.pure-form input[type="week"][disabled], +.pure-form input[type="number"][disabled], +.pure-form input[type="search"][disabled], +.pure-form input[type="tel"][disabled], +.pure-form input[type="color"][disabled], +.pure-form select[disabled], +.pure-form textarea[disabled] { + cursor: not-allowed; + background-color: #eaeded; + color: #cad2d3; } + +/* +Need to separate out the :not() selector from the rest of the CSS 2.1 selectors +since IE8 won't execute CSS that contains a CSS3 selector. +*/ +.pure-form input:not([type])[disabled] { + cursor: not-allowed; + background-color: #eaeded; + color: #cad2d3; } + +.pure-form input[readonly], +.pure-form select[readonly], +.pure-form textarea[readonly] { + background-color: #eee; + /* menu hover bg color */ + color: #777; + /* menu text color */ + border-color: #ccc; } + +.pure-form input:focus:invalid, +.pure-form textarea:focus:invalid, +.pure-form select:focus:invalid { + color: #b94a48; + border-color: #e9322d; } + +.pure-form input[type="file"]:focus:invalid:focus, +.pure-form input[type="radio"]:focus:invalid:focus, +.pure-form input[type="checkbox"]:focus:invalid:focus { + outline-color: #e9322d; } + +.pure-form select { + /* Normalizes the height; padding is not sufficient. */ + height: 2.25em; + border: 1px solid #ccc; + background-color: white; } + +.pure-form select[multiple] { + height: auto; } + +.pure-form label { + margin: 0.5em 0 0.2em; } + +.pure-form fieldset { + margin: 0; + padding: 0.35em 0 0.75em; + border: 0; } + +.pure-form legend { + display: block; + width: 100%; + padding: 0.3em 0; + margin-bottom: 0.3em; + color: #333; + border-bottom: 1px solid #e5e5e5; } + +.pure-form-stacked input[type="text"], +.pure-form-stacked input[type="password"], +.pure-form-stacked input[type="email"], +.pure-form-stacked input[type="url"], +.pure-form-stacked input[type="date"], +.pure-form-stacked input[type="month"], +.pure-form-stacked input[type="time"], +.pure-form-stacked input[type="datetime"], +.pure-form-stacked input[type="datetime-local"], +.pure-form-stacked input[type="week"], +.pure-form-stacked input[type="number"], +.pure-form-stacked input[type="search"], +.pure-form-stacked input[type="tel"], +.pure-form-stacked input[type="color"], +.pure-form-stacked input[type="file"], +.pure-form-stacked select, +.pure-form-stacked label, +.pure-form-stacked textarea { + display: block; + margin: 0.25em 0; } + +/* +Need to separate out the :not() selector from the rest of the CSS 2.1 selectors +since IE8 won't execute CSS that contains a CSS3 selector. +*/ +.pure-form-stacked input:not([type]) { + display: block; + margin: 0.25em 0; } + +.pure-form-aligned input, +.pure-form-aligned textarea, +.pure-form-aligned select, +.pure-form-aligned .pure-help-inline, +.pure-form-message-inline { + display: inline-block; + *display: inline; + *zoom: 1; + vertical-align: middle; } + +.pure-form-aligned textarea { + vertical-align: top; } + +/* Aligned Forms */ +.pure-form-aligned .pure-control-group { + margin-bottom: 0.5em; } + +.pure-form-aligned .pure-control-group label { + text-align: right; + display: inline-block; + vertical-align: middle; + width: 10em; + margin: 0 1em 0 0; } + +.pure-form-aligned .pure-controls { + margin: 1.5em 0 0 11em; } + +/* Rounded Inputs */ +.pure-form input.pure-input-rounded, +.pure-form .pure-input-rounded { + border-radius: 2em; + padding: 0.5em 1em; } + +/* Grouped Inputs */ +.pure-form .pure-group fieldset { + margin-bottom: 10px; } + +.pure-form .pure-group input, +.pure-form .pure-group textarea { + display: block; + padding: 10px; + margin: 0 0 -1px; + border-radius: 0; + position: relative; + top: -1px; } + +.pure-form .pure-group input:focus, +.pure-form .pure-group textarea:focus { + z-index: 3; } + +.pure-form .pure-group input:first-child, +.pure-form .pure-group textarea:first-child { + top: 1px; + border-radius: 4px 4px 0 0; + margin: 0; } + +.pure-form .pure-group input:first-child:last-child, +.pure-form .pure-group textarea:first-child:last-child { + top: 1px; + border-radius: 4px; + margin: 0; } + +.pure-form .pure-group input:last-child, +.pure-form .pure-group textarea:last-child { + top: -2px; + border-radius: 0 0 4px 4px; + margin: 0; } + +.pure-form .pure-group button { + margin: 0.35em 0; } + +.pure-form .pure-input-1 { + width: 100%; } + +.pure-form .pure-input-3-4 { + width: 75%; } + +.pure-form .pure-input-2-3 { + width: 66%; } + +.pure-form .pure-input-1-2 { + width: 50%; } + +.pure-form .pure-input-1-3 { + width: 33%; } + +.pure-form .pure-input-1-4 { + width: 25%; } + +/* Inline help for forms */ +/* NOTE: pure-help-inline is deprecated. Use .pure-form-message-inline instead. */ +.pure-form .pure-help-inline, +.pure-form-message-inline { + display: inline-block; + padding-left: 0.3em; + color: #666; + vertical-align: middle; + font-size: 0.875em; } + +/* Block help for forms */ +.pure-form-message { + display: block; + color: #666; + font-size: 0.875em; } + +@media only screen and (max-width: 480px) { + .pure-form button[type="submit"] { + margin: 0.7em 0 0; } + + .pure-form input:not([type]), + .pure-form input[type="text"], + .pure-form input[type="password"], + .pure-form input[type="email"], + .pure-form input[type="url"], + .pure-form input[type="date"], + .pure-form input[type="month"], + .pure-form input[type="time"], + .pure-form input[type="datetime"], + .pure-form input[type="datetime-local"], + .pure-form input[type="week"], + .pure-form input[type="number"], + .pure-form input[type="search"], + .pure-form input[type="tel"], + .pure-form input[type="color"], + .pure-form label { + margin-bottom: 0.3em; + display: block; } + + .pure-group input:not([type]), + .pure-group input[type="text"], + .pure-group input[type="password"], + .pure-group input[type="email"], + .pure-group input[type="url"], + .pure-group input[type="date"], + .pure-group input[type="month"], + .pure-group input[type="time"], + .pure-group input[type="datetime"], + .pure-group input[type="datetime-local"], + .pure-group input[type="week"], + .pure-group input[type="number"], + .pure-group input[type="search"], + .pure-group input[type="tel"], + .pure-group input[type="color"] { + margin-bottom: 0; } + + .pure-form-aligned .pure-control-group label { + margin-bottom: 0.3em; + text-align: left; + display: block; + width: 100%; } + + .pure-form-aligned .pure-controls { + margin: 1.5em 0 0 0; } + + /* NOTE: pure-help-inline is deprecated. Use .pure-form-message-inline instead. */ + .pure-form .pure-help-inline, + .pure-form-message-inline, + .pure-form-message { + display: block; + font-size: 0.75em; + /* Increased bottom padding to make it group with its related input element. */ + padding: 0.2em 0 0.8em; } } +/*csslint adjoining-classes: false, box-model:false*/ +.pure-menu { + box-sizing: border-box; } + +.pure-menu-fixed { + position: fixed; + left: 0; + top: 0; + z-index: 3; } + +.pure-menu-list, +.pure-menu-item { + position: relative; } + +.pure-menu-list { + list-style: none; + margin: 0; + padding: 0; } + +.pure-menu-item { + padding: 0; + margin: 0; + height: 100%; } + +.pure-menu-link, +.pure-menu-heading { + display: block; + text-decoration: none; + white-space: nowrap; } + +/* HORIZONTAL MENU */ +.pure-menu-horizontal { + width: 100%; + white-space: nowrap; } + +.pure-menu-horizontal .pure-menu-list { + display: inline-block; } + +/* Initial menus should be inline-block so that they are horizontal */ +.pure-menu-horizontal .pure-menu-item, +.pure-menu-horizontal .pure-menu-heading, +.pure-menu-horizontal .pure-menu-separator { + display: inline-block; + *display: inline; + zoom: 1; + vertical-align: middle; } + +/* Submenus should still be display: block; */ +.pure-menu-item .pure-menu-item { + display: block; } + +.pure-menu-children { + display: none; + position: absolute; + left: 100%; + top: 0; + margin: 0; + padding: 0; + z-index: 3; } + +.pure-menu-horizontal .pure-menu-children { + left: 0; + top: auto; + width: inherit; } + +.pure-menu-allow-hover:hover > .pure-menu-children, +.pure-menu-active > .pure-menu-children { + display: block; + position: absolute; } + +/* Vertical Menus - show the dropdown arrow */ +.pure-menu-has-children > .pure-menu-link:after { + padding-left: 0.5em; + content: "\25B8"; + font-size: small; } + +/* Horizontal Menus - show the dropdown arrow */ +.pure-menu-horizontal .pure-menu-has-children > .pure-menu-link:after { + content: "\25BE"; } + +/* scrollable menus */ +.pure-menu-scrollable { + overflow-y: scroll; + overflow-x: hidden; } + +.pure-menu-scrollable .pure-menu-list { + display: block; } + +.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list { + display: inline-block; } + +.pure-menu-horizontal.pure-menu-scrollable { + white-space: nowrap; + overflow-y: hidden; + overflow-x: auto; + -ms-overflow-style: none; + -webkit-overflow-scrolling: touch; + /* a little extra padding for this style to allow for scrollbars */ + padding: .5em 0; } + +.pure-menu-horizontal.pure-menu-scrollable::-webkit-scrollbar { + display: none; } + +/* misc default styling */ +.pure-menu-separator, +.pure-menu-horizontal .pure-menu-children .pure-menu-separator { + background-color: #ccc; + height: 1px; + margin: .3em 0; } + +.pure-menu-horizontal .pure-menu-separator { + width: 1px; + height: 1.3em; + margin: 0 0.3em; } + +/* Need to reset the separator since submenu is vertical */ +.pure-menu-horizontal .pure-menu-children .pure-menu-separator { + display: block; + width: auto; } + +.pure-menu-heading { + text-transform: uppercase; + color: #565d64; } + +.pure-menu-link { + color: #777; } + +.pure-menu-children { + background-color: #fff; } + +.pure-menu-link, +.pure-menu-disabled, +.pure-menu-heading { + padding: .5em 1em; } + +.pure-menu-disabled { + opacity: .5; } + +.pure-menu-disabled .pure-menu-link:hover { + background-color: transparent; } + +.pure-menu-active > .pure-menu-link, +.pure-menu-link:hover, +.pure-menu-link:focus { + background-color: #eee; } + +.pure-menu-selected .pure-menu-link, +.pure-menu-selected .pure-menu-link:visited { + color: #000; } + +.pure-table { + /* Remove spacing between table cells (from Normalize.css) */ + border-collapse: collapse; + border-spacing: 0; + empty-cells: show; + border: 1px solid #cbcbcb; } + +.pure-table caption { + color: #000; + font: italic 85%/1 arial, sans-serif; + padding: 1em 0; + text-align: center; } + +.pure-table td, +.pure-table th { + border-left: 1px solid #cbcbcb; + /* inner column border */ + border-width: 0 0 0 1px; + font-size: inherit; + margin: 0; + overflow: visible; + /*to make ths where the title is really long work*/ + padding: 0.5em 1em; + /* cell padding */ } + +/* Consider removing this next declaration block, as it causes problems when +there's a rowspan on the first cell. Case added to the tests. issue#432 */ +.pure-table td:first-child, +.pure-table th:first-child { + border-left-width: 0; } + +.pure-table thead { + background-color: #e0e0e0; + color: #000; + text-align: left; + vertical-align: bottom; } + +/* +striping: + even - #fff (white) + odd - #f2f2f2 (light gray) +*/ +.pure-table td { + background-color: transparent; } + +.pure-table-odd td { + background-color: #f2f2f2; } + +/* nth-child selector for modern browsers */ +.pure-table-striped tr:nth-child(2n-1) td { + background-color: #f2f2f2; } + +/* BORDERED TABLES */ +.pure-table-bordered td { + border-bottom: 1px solid #cbcbcb; } + +.pure-table-bordered tbody > tr:last-child > td { + border-bottom-width: 0; } + +/* HORIZONTAL BORDERED TABLES */ +.pure-table-horizontal td, +.pure-table-horizontal th { + border-width: 0 0 1px 0; + border-bottom: 1px solid #cbcbcb; } + +.pure-table-horizontal tbody > tr:last-child > td { + border-bottom-width: 0; } + +/*# sourceMappingURL=pure.css.map */