began with settings page and with the input sanity check + hint

This commit is contained in:
Jean Jacques Avril 2022-03-21 21:09:47 +01:00
parent 37f134fd13
commit 3c5354afb9
8 changed files with 215 additions and 128 deletions

View File

@ -1,42 +1,46 @@
const server = 'http://192.168.4.22' const server = 'http://192.168.20.1'
const timeout = 1500;
async function restoreSession(reducer = null) { async function restoreSession(reducer = null) {
let token = localStorage.getItem('token'); let token = localStorage.getItem('token');
let username = localStorage.getItem('username'); let username = localStorage.getItem('username');
console.log({ type: 'start', token, username });
let active = token !== null && username !== null && await checkAuth(token); let active = token !== null && username !== null && await checkAuth(token);
if (active && reducer !== null) { if (active && reducer !== null) {
reducer({ type: 'start', token, username }); reducer({ type: 'start', token, username });
console.log({ type: 'start', token, username });
} }
return (active ? { active: true, token, username } : { active: false }); return (active ? { active: true, token, username } : { active: false });
} }
async function storeSession(username, token) { async function storeSession(result, token) {
token = await token; result.token = await token;
localStorage.setItem('token', token); result.type = 'start';
localStorage.setItem('username', username); console.log(result)
return token if (result.permanent) {
localStorage.setItem('token', result.token);
localStorage.setItem('username', result.username);
}
return result;
} }
async function login(username, password) { async function login(formdata) {
let formData = new FormData(); let formData = new FormData();
formData.append('username', username) formData.append('username', formdata.username)
formData.append('password', password); formData.append('password', formdata.password);
formData.append('action', 'login'); formData.append('action', 'login');
let result = { username: formdata.username, permanent: formdata.permanent }
const controller = new AbortController(); const controller = new AbortController();
const id = setTimeout(() => controller.abort(), 1500); const id = setTimeout(() => controller.abort(), timeout);
const resp = fetch(`${server}/api/auth`, { signal: controller.signal, method: 'POST', mode: 'cors', body: formData }) const resp = await fetch(`${server}/api/auth`, { signal: controller.signal, method: 'POST', mode: 'cors', body: formData })
.then(resp => { .then(resp => {
if (resp.ok) return storeSession(username, resp.text()) if (resp.ok) return storeSession(result, resp.text());
else if(resp.status===401) return 'login_failed'; else if (resp.status === 401)
result.error = 'login_failed';
else throw new Error(resp.error); else throw new Error(resp.error);
return result;
}) })
.catch((error) => { .catch((error) => {
console.log('Error is:', error); result.error = 'network_connection';
return 'network_error';
}) })
; return result;
return resp;
} }
async function logout(token) { async function logout(token) {
let formData = new FormData() let formData = new FormData()
@ -54,10 +58,6 @@ async function checkAuth(token) {
return await resp; return await resp;
} }
async function fetchdb(token = 'azif7eqCl5') { async function fetchdb(token = 'azif7eqCl5') {
//fetch(`${server}/api/userdb`).then()
//let xmlHttp = new XMLHttpRequest();
//xmlHttp.open( "GET", `${server}/api/userdb`, false ); // false for synchronous request
//xmlHttp.send( null );
const resp = await fetch(`${server}/api/userdb`, { method: 'GET', mode: 'cors', headers: { Authentification: token } }) const resp = await fetch(`${server}/api/userdb`, { method: 'GET', mode: 'cors', headers: { Authentification: token } })
.then(resp => resp.text()).then(text => parsedb(text)) .then(resp => resp.text()).then(text => parsedb(text))
return resp; return resp;
@ -100,11 +100,22 @@ function parsedb(raw) {
return users; return users;
} }
async function catchRFID(token){ async function catchRFID(token) {
const resp = await fetch(`${server}/api/rfid`, { method: 'GET', mode: 'cors', headers: { Authentification: token } }) const resp = await fetch(`${server}/api/rfid`, { method: 'GET', mode: 'cors', headers: { Authentification: token } })
.then(resp => resp.json()); .then(resp => resp.json());
return resp; return resp;
} }
const publicfunctions = { login, logout, checkAuth, parsedb, fetchdb, createCsvTable, updateUser, deleteUser, createUser, restoreSession, catchRFID };
async function config(token, payload) {
let reqdata = new FormData();
for (let [k, v] of Object.entries(payload))
if (['number', 'string'].includes(typeof (v)))
reqdata.append(k, v);
const abort = new AbortController();
const aborttimer = setTimeout(() => abort.abort(), timeout);
return fetch(`${server}/api/config`, { signal: abort.signal, method: 'POST', mode: 'cors', body: reqdata, headers:{Authentification: token} }).
then(resp => resp.ok ? resp.json() : { error: resp.status });
}
const publicfunctions = { login, logout, checkAuth, parsedb, fetchdb, createCsvTable, updateUser, deleteUser, createUser, restoreSession, catchRFID, config };
export default { ...publicfunctions } export default { ...publicfunctions }
export { login, logout, checkAuth, parsedb, fetchdb, createCsvTable, updateUser, deleteUser, createUser, restoreSession, catchRFID } export { login, logout, checkAuth, parsedb, fetchdb, createCsvTable, updateUser, deleteUser, createUser, restoreSession, catchRFID, config }

View File

@ -1,53 +1,54 @@
import { h } from "preact"; import { h } from "preact";
import { Router, route } from "preact-router"; import { Router, route } from "preact-router";
import { useEffect, useReducer } from "preact/hooks"; import { useEffect, useReducer, useState } from "preact/hooks";
import { Header, Menu } from "./index.js"; import { Header, Menu } from "./index.js";
import { Home, Users, EditUser, Login, Logout, System } from "../route"; import { Home, Users, EditUser, Login, Logout, System } from "../route";
import { AppStateProvider, UserTableProvider, menuReducer, sessionReducer, userTableReducer } from "../store"; import { AppStateProvider, UserTableProvider, menuReducer, sessionReducer, userTableReducer } from "../store";
import api from "../api/index.js"; import api from "../api/index.js";
function App() {
// useReducer
const menu = useReducer(menuReducer, false);
const session = useReducer(sessionReducer, {});
const [usertable, userreducer] = useReducer(userTableReducer, []);
console.log(session[0]);
if(!session[0]||(session[0]&&session[0].active===undefined))
api.restoreSession(session[1]);
useEffect(()=>{ const menu_items = [
if(session[0]&&session[0].active){
console.log("triggerimport");
api.fetchdb(session[0].token).then(imported => {
let action = { type: 'import', imported };
userreducer(action);
});
}},[session]);
console.log(session);
this.menu_items = [
{ text: "Übersicht", path: "/" }, { text: "Übersicht", path: "/" },
{ text: "Benutzer anlegen", path: "/newuser" }, { text: "Benutzer anlegen", path: "/newuser" },
{ text: "Benutzer verwalten", path: "/users" }, { text: "Benutzer verwalten", path: "/users" },
{ text: "System", path: "/system" }, { text: "System", path: "/system" },
{ text: "Abmelden", path: "/logout" } { text: "Abmelden", path: "/logout" }
] ]
this.handleRoute = async e => {
function App() {
// useReducer
const menu = useReducer(menuReducer, false);
const session = useReducer(sessionReducer, {});
const [usertable, userreducer] = useReducer(userTableReducer, []);
const [lasturl, setlasturl] = useState("/");
if(!session[0]||(session[0]&&session[0].active===undefined))
api.restoreSession(session[1]);
useEffect(()=>{
if(session[0]&&session[0].active){
api.fetchdb(session[0].token).then(imported => {
let action = { type: 'import', imported };
userreducer(action);
});
}},[session]);
const handleRoute = async e => {
let active = session[0].active;
switch (e.url) { switch (e.url) {
default: default:
if (!session[0].active) route('/login', true); if (!active) route('/login', true);
break; break;
} }
}; };
return ( return (
<AppStateProvider value={{ menu, session }} > <AppStateProvider value={{ menu, session }} >
<div id="wrapper"> <div id="wrapper">
<Header title={"Doorlock"} /> <Header title={"Doorlock"} />
<div class="page"> <div class="page">
<Menu items={this.menu_items} /> <Menu items={menu_items} />
{!menu[0] && {!menu[0] &&
<UserTableProvider value={{ usertable, userreducer }} > <UserTableProvider value={{ usertable, userreducer }} >
<Router onChange={this.handleRoute}> <Router onChange={handleRoute}>
<Home path="/" user="me" /> <Home path="/" user="me" />
<Login path="login" /> <Login path="login" />
<System path="/system" /> <System path="/system" />

View File

@ -1,11 +1,26 @@
import { h } from 'preact' import { h, render } from 'preact'
function TextBox({ id, type = 'text', label, disabled, formdata, formchange, maxlength, overridevalue }) { function TextBox({ id, type = 'text', label, disabled, formdata, formchange, maxlength, overridevalue, onChange, onInput, checkInput, allowdChars, showavailable,hinttext }) {
let onChange = (e) => formchange({ ...formdata, [e.target.id]: e.target.value });
return (<div className={'textbox'} > if (typeof (onInput) === 'undefined' && typeof (formchange) === 'function') {
<input placeholder=" " disabled={disabled} type={type} value={overridevalue?overridevalue:formdata[id]?formdata[id]:''} id={id} onInput={onChange} maxlength={maxlength} name={`form-${id}`} /> onInput = (e) => formchange(p => {
if (typeof (checkInput) === 'function' && !checkInput(e.target.value)) {
return ({ ...p, error: { [e.target.id]: 'input_error' } })
}
return ({ ...p, [e.target.id]: e.target.value, error: { [e.target.id]: undefined } })
});
}
const onBlur = (e)=>{
formchange(p =>({ ...p, error: { [e.target.id]: undefined } }));
}
return (<>
<div className={'textbox'} >
<input placeholder=" " disabled={disabled} type={type} value={overridevalue ? overridevalue : formdata[id] ? formdata[id] : ''} id={id} onBlur={onBlur} onInput={onInput} onChange={onChange} maxlength={maxlength} name={`form-${id}`} />
<label for={id}>{label}</label> <label for={id}>{label}</label>
</div>) {formdata&&formdata.error&&formdata.error[id]&&hinttext&&<div className='textbox__hint'>{hinttext}</div>}
</div>
{showavailable && <span class="textbox__info"> {formdata[id] ? formdata[id].length : 0} von {maxlength} Zeichen verwendet.</span>}
</>)
} }
function CheckBox({ id, disabled, label, formdata, formchange }) { function CheckBox({ id, disabled, label, formdata, formchange }) {
let onChange = (e) => formchange({ ...formdata, [e.target.id]: e.target.checked }); let onChange = (e) => formchange({ ...formdata, [e.target.id]: e.target.checked });

View File

@ -82,8 +82,7 @@ function EditUser({ userid }) {
<h3>Eindeutige Identifikationsnummer</h3> <h3>Eindeutige Identifikationsnummer</h3>
<div className='row'> <div className='row'>
<div className='column'> <div className='column'>
<TextBox info={true} formdata={formdata} formchange={formchange} id="uid" label="Benutzer-ID" maxlength={maxlength_uid} /> <TextBox info={true} formdata={formdata} formchange={formchange} id="uid" label="Benutzer-ID" maxlength={maxlength_uid} showavailable />
{<span class="textbox__info"> {formdata["uid"] ? formdata["uid"].length : 0} von {maxlength_uid} Zeichen verwendet.</span>}
</div> </div>
<div className='column'> <div className='column'>
<b>Info</b> <b>Info</b>
@ -94,19 +93,16 @@ function EditUser({ userid }) {
<h3>Persönliche Daten</h3> <h3>Persönliche Daten</h3>
<div className='row'> <div className='row'>
<div className='column'> <div className='column'>
<TextBox formdata={formdata} formchange={formchange} id="first_name" label="Vorname" maxlength={maxlength_name} /> <TextBox formdata={formdata} formchange={formchange} id="first_name" label="Vorname" maxlength={maxlength_name} showavailable />
{<span class="textbox__info"> {formdata["first_name"] ? formdata["first_name"].length : 0} von {maxlength_name} Zeichen verwendet.</span>}
</div> </div>
<div className='column'> <div className='column'>
<TextBox formdata={formdata} formchange={formchange} id="last_name" label="NachName" maxlength={maxlength_name} /> <TextBox formdata={formdata} formchange={formchange} id="last_name" label="NachName" maxlength={maxlength_name} showavailable />
{<span class="textbox__info"> {formdata["last_name"] ? formdata["last_name"].length : 0} von {maxlength_name} Zeichen verwendet.</span>}
</div> </div>
</div> </div>
<h3>Authentifizierung</h3> <h3>Authentifizierung</h3>
<div className='row'> <div className='row'>
<div className='column'> <div className='column'>
<TextBox formdata={formdata} formchange={formchange} id="rfid_uid" label="RFID" maxlength={maxlength_rfid} disabled={rfidscan.active} overridevalue={rfidscan.active&&"Scannen..."}/> <TextBox formdata={formdata} formchange={formchange} id="rfid_uid" label="RFID" maxlength={maxlength_rfid} disabled={rfidscan.active} overridevalue={rfidscan.active&&"Scannen..."} showavailable />
{<span class="textbox__info"> {formdata["rfid_uid"] ? formdata["rfid_uid"].length : 0} von {maxlength_rfid} Zeichen verwendet.</span>}
</div> </div>
<div className='column'> <div className='column'>
<Button onClick={runScan}>{rfidscan.active ? "Abbrechen" : "Scannen"}</Button> <Button onClick={runScan}>{rfidscan.active ? "Abbrechen" : "Scannen"}</Button>
@ -115,8 +111,7 @@ function EditUser({ userid }) {
</div> </div>
<div className='row'> <div className='row'>
<div className='column'> <div className='column'>
<TextBox formdata={formdata} formchange={formchange} id="user_pin" label="Pin" maxlength={maxlength_pin} /> <TextBox formdata={formdata} formchange={formchange} id="user_pin" label="Pin" maxlength={maxlength_pin} showavailable />
{<span class="textbox__info"> {formdata["user_pin"] ? formdata["user_pin"].length : 0} von {maxlength_pin} Zeichen verwendet.</span>}
</div> </div>
<div className='column'> <div className='column'>
<b>Info</b> <b>Info</b>

View File

@ -2,34 +2,23 @@ import { h } from 'preact';
import { route } from 'preact-router'; import { route } from 'preact-router';
import { useContext, useState } from 'preact/hooks'; import { useContext, useState } from 'preact/hooks';
import AppState from '../../store/AppState'; import AppState from '../../store/AppState';
import {Breadcrumbs, Warnbox} from '../../components'; import { Breadcrumbs, Warnbox } from '../../components';
import { CheckBox, Button, TextBox } from '../../components/controls'; import { CheckBox, Button, TextBox } from '../../components/controls';
import api from '../../api' import api from '../../api'
function Login() { function Login() {
let [sessiondata, setsession] = useContext(AppState).session; let [sessiondata, setsession] = useContext(AppState).session;
const [val, set] = useState({ username: '', password: '', error: null }); const [fromdata, setform] = useState({ username: '', password: '', error: null });
const navigation = ["Login"]; const navigation = ["Login"];
if (sessiondata.active) if (sessiondata.active)
route('/', true); route('/', true);
function onSubmit(e) { function onSubmit(e) {
e.preventDefault(); e.preventDefault();
api.login(val.username, val.password).then(result => { api.login(fromdata).then(result => {
if(result == 'login_failed'){ if (result.error !== undefined)
set(prev=>({ ...prev, error: "login_failed",password: '' })); setform(result);
}
else if(result=='network_error')
set(prev=>({ ...prev, error: "network_connection" }));
else { else {
console.log(typeof(result)) setsession(result);
let newsession = {
type: 'start',
username: val.username,
token: result
} }
setsession(newsession);
set({ username: '', password: '' });
}
}) })
} }
@ -40,18 +29,18 @@ function Login() {
<h2>Anmeldung</h2> <h2>Anmeldung</h2>
<p >Bitte melden Sie sich mit ihren Nutzerdaten an.</p> <p >Bitte melden Sie sich mit ihren Nutzerdaten an.</p>
{val.error ==='login_failed' && <Warnbox title='Anmeldefehler'>Ungültige Anmeldedaten.<br />Bitte überprüfen Sie den eingebenen Benutzernamen und das Passwort.</Warnbox>} {fromdata.error === 'login_failed' && <Warnbox title='Anmeldefehler'>Ungültige Anmeldedaten.<br />Bitte überprüfen Sie den eingebenen Benutzernamen und das Passwort.</Warnbox>}
{val.error ==='network_connection' && <Warnbox title='Netwerkfehler'>Die Kommunikation mit dem Gerät ist zurzeit nicht möglich.<br />Bitte überprüfen Sie die Netzwerkverbindung.</Warnbox>} {fromdata.error === 'network_connection' && <Warnbox title='Netwerkfehler'>Die Kommunikation mit dem Gerät ist zurzeit nicht möglich.<br />Bitte überprüfen Sie die Netzwerkverbindung.</Warnbox>}
<form id="login_form" onSubmit={onSubmit} > <form id="login_form" onSubmit={onSubmit} >
<div className='row'> <div className='row'>
<TextBox maxlength={25} formdata={val} formchange={set} id="username" label="Benutzername" /> <TextBox maxlength={25} formdata={fromdata} formchange={setform} id="username" label="Benutzername" />
</div> </div>
<div className='row'> <div className='row'>
<TextBox maxlength={25} formdata={val} formchange={set} id="password" label="Passwort" type='password' /> <TextBox maxlength={25} formdata={fromdata} formchange={setform} id="password" label="Passwort" type='password' />
</div> </div>
<div className='row'> <div className='row'>
<CheckBox id="permanent" formdata={val} formchange={set} label="Angemeldet bleiben?" /> <CheckBox id="permanent" formdata={fromdata} formchange={setform} label="Angemeldet bleiben?" />
</div> </div>
<Button onClick={onSubmit}>Anmelden</Button> <Button onClick={onSubmit}>Anmelden</Button>

View File

@ -1,48 +1,112 @@
import { h } from 'preact'; import { h } from 'preact';
import { Breadcrumbs } from "../../components"; import { Breadcrumbs } from "../../components";
import { useContext, useEffect, useState } from 'preact/hooks';
import { CheckBox, Button, TextBox } from '../../components/controls';
import AppState from '../../store/AppState';
import api from '../../api'
const checkIP = (v)=>{
let parts = v.split('.');
if(parts.length>4) return false;
for(let part of parts)
if(isNaN(part)||part>255||part<0||part.length>3)
return false;
return true;
}
function System() { function System() {
let [session,] = useContext(AppState).session;
let [formdata, setform] = useState({});
useEffect(() => {
api.config(session.token, { action: "get" }).then(r => setform(p => ({ ...p, ...r })));
}, [session])
return ( return (
<div className='container'> <div className='container'>
<Breadcrumbs items={['Systemeinstellungen']} /> <Breadcrumbs items={['Systemeinstellungen']} />
<div className={'contentbox'} > <div className={'contentbox'} >
<h2>System</h2> <h2>System</h2>
<form>
<h3>Geräteeinstellungen</h3>
<div className='row'>
<div className='column'>
<TextBox info={true} formdata={formdata} formchange={setform} id="mode" label="Modus" checkInput={(v)=>!isNaN(v)&&v>=0&&v<=1} maxlength={1} showavailable hinttext="Nur Zahlen 0 bis 1" />
</div>
<div className='column'>
<TextBox info={true} formdata={formdata} formchange={setform} id="fail_timeout" label="Login Timeout" checkInput={(v)=>!isNaN(v)&&v>=0&&v<=255} maxlength={3} showavailable hinttext="Nur Zahlen 0 bis 255" />
</div>
<div className='column'>
<TextBox info={true} formdata={formdata} formchange={setform} id="hold_time" label="Haltezeit" checkInput={(v)=>!isNaN(v)&&v>=0&&v<=255} maxlength={3} showavailable hinttext="Nur Zahlen 0 bis 255"/>
</div>
</div>
<h3>WiFi Setup</h3> <h3>WiFi Setup</h3>
<hr /> <div className='row'>
<form> <div className='column'>
<div className={'textbox'} > <TextBox info={true} formdata={formdata} formchange={setform} id="SSID" label="WiFi Name" maxlength={31} showavailable />
<input type="text" placeholder='Wifi network name' /> </div>
<label>SSID</label> <div className='column'>
<TextBox info={true} formdata={formdata} formchange={setform} id="PASS" label="WiFi Passwort" maxlength={31} showavailable />
</div>
</div>
<div className='row'>
<div className='column'>
<TextBox info={true} formdata={formdata} formchange={setform} id="ip" label="IP Adresse" maxlength={15} showavailable checkInput={checkIP} hinttext="Beispiel: 192.168.20.1" />
</div>
<div className='column'>
<TextBox info={true} formdata={formdata} formchange={setform} id="subnet" label="Netzmaske" maxlength={15} showavailable checkInput={checkIP} hinttext="Beispiel: 255.255.255.0"/>
</div>
<div className='column'>
<TextBox info={true} formdata={formdata} formchange={setform} id="gw" label="Gateway" maxlength={15} showavailable checkInput={checkIP} hinttext="Beispiel: 0.0.0.0"/>
</div>
</div>
<div className='row'>
<div className='column'>
<Button onClick={() => null} >Speichern</Button>
</div>
<div className='column'>
<Button onClick={() => null} >Neustart</Button>
</div>
<div className='column'>
<Button onClick={() => null} >Zurücksetzen</Button>
</div> </div>
<div className={'textbox'} >
<input type="text" placeholder='Wifi network name' />
<label>Password</label>
</div> </div>
</form> </form>
<form>
<h3>Admin User</h3> <h3>Admin User</h3>
<hr /> <div className='row'>
<form> <div className='column'>
<div className={'textbox'} > <TextBox info={true} formdata={formdata} formchange={setform} id="ip" label="Name" maxlength={15} showavailable />
<input type="text" placeholder='Wifi network name' /> </div>
<label>Username</label> </div>
<div className='row'>
<div className='column'>
<TextBox info={true} formdata={formdata} formchange={setform} id="subnet" label="Passwort" maxlength={15} showavailable />
</div>
<div className='column'>
<TextBox info={true} formdata={formdata} formchange={setform} id="subnet" label="Passwort wiederholen" maxlength={15} showavailable />
</div>
</div>
<div className='row'>
<div className='column'>
<Button onClick={() => null} >Speichern</Button>
</div>
<div className='column'>
<Button onClick={() => null} >Zurücksetzen</Button>
</div> </div>
<div className={'textbox'} >
<input type="text" placeholder='Wifi network name' />
<label>Password</label>
</div> </div>
</form> </form>
<h3>Datenbank Backup</h3><hr /> <h3>Datenbank Backup</h3>
<div> <div className='row'>
<div className="column">
<h4>Backup einspielen</h4>
<form> <form>
<h3>Backup einspielen</h3>
<input type={'file'} /> <input type={'file'} />
<input type={'submit'} value={'Hochladen'} /> <input type={'submit'} value={'Hochladen'} />
</form> </form>
<h3>Backup herunterladen</h3> </div>
<div className="column">
<h4>Backup herunterladen</h4>
<button>Download</button> <button>Download</button>
</div> </div>
</div>
</div> </div>
</div>) </div>)
} }

View File

@ -19,12 +19,13 @@
button button
display: block display: block
background: none background: none
min-width: 100% width: 100%
border: none border: none
border-radius: none border-radius: none
font-size: 2em font-size: 2em
transition: all ease-in-out 100ms transition: all ease-in-out 100ms
overflow: hidden
text-overflow: ellipsis
&::before &::before
z-index: -1 z-index: -1
border-radius: .2rem border-radius: .2rem

View File

@ -4,16 +4,17 @@
position: relative position: relative
display: block display: block
margin: 0 margin: 0
overflow: hidden
background: #fafafa background: #fafafa
border-radius: .3em border-radius: .3em
border-bottom: 1px solid #ccc border-bottom: 1px solid #ccc
&:hover &:hover
background: #fff background: #fff
input input
//overflow: hidden
display: block display: block
background: none background: none
min-width: 100% width: 100%
border-radius: none border-radius: none
outline: none outline: none
border: none border: none
@ -28,13 +29,12 @@
font-size: .7em font-size: .7em
cursor: text cursor: text
transition: 250ms all transition: 250ms all
overflow: hidden
text-overflow: ellipsis text-overflow: ellipsis
&::placeholder &::placeholder
color: transparent color: transparent
&:placeholder-shown + label &:placeholder-shown + label
text-overflow: ellipsis
color: grey color: grey
position: absolute position: absolute
padding: .7em padding: .7em
@ -46,6 +46,17 @@
&__info &__info
color: #ccc color: #ccc
font-size: 0.7em font-size: 0.7em
&__hint
position: absolute
z-index: 1
left: 0
right: 0
top: calc(100% - .6em)
border-bottom-left-radius: .3em
border-bottom-right-radius: .3em
color: #fff
font-size: .7em
background: red
@mixin checkbox @mixin checkbox
position: relative position: relative