A Pen by Tomáš Reichmann on CodePen.
Last active
January 4, 2017 16:15
-
-
Save tomasreichmann/3b4d5a15f0d6b87979518cb424cd6063 to your computer and use it in GitHub Desktop.
Fate - Character sheet
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div id="wrapper"></div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var ref; | |
var data = { | |
//view: "editCharacter", //list | |
view: "list", //list | |
viewData: { | |
//id: "-KQPRrTpqWgyuPwBesFq" | |
}, | |
lastCharacterId: 1, | |
characters: [] | |
}; | |
var config = { | |
skillLevels: 5, | |
skillsPerLevel: 5, | |
skillList: [ "Atletika", "Zlodějství", "Konexe", "Řemeslo", "Klamání", "Pilotování", "Vcítění", "Provokace", "Boj", "Vyšetřování", "Věda", "Medicína", "Technologie", "Všímavost", "Fyzická zdatnost", "Diplomacie", "Zdroje", "Střelba", "Kradmost", "Vůle", "Artilérie" ], | |
stress: [ | |
{ label: "PHYSICAL STRESS", key: "physical", count: 4, def: 2, skill: "Fyzická zdatnost" }, | |
{ label: "MENTAL STRESS", key: "mental", count: 4, def: 2, skill: "Vůle" } | |
], | |
consequences: { | |
defaultCount: 2, | |
bonusSkillLevel: 5, | |
skills: ["Fyzická zdatnost", "Vůle"], | |
list: [{ val: 2, label: "Minor", key: "consequences.minor" }, { val: 4, label: "Moderate", key: "consequences.moderate" }, { val: 6, label: "Severe", key: "consequences.severe" }] | |
}, | |
challengeLevels: [ "Average", "Fair", "Good", "Great", "Superb" ], | |
texts: { | |
clear: "-vymazat-", | |
confirm: "OK", | |
cancel: "zrušit", | |
save: "uložit", | |
edit: "upravit", | |
new: "nový" | |
} | |
} | |
function seq(count){ | |
return Array(count).fill(null).map( (item, index)=>( index ) ); | |
} | |
var Input = React.createClass({ | |
getInitialState: function() { | |
return {text: this.props.val || data.text || ''}; | |
}, | |
getInitialProps: function() { | |
return { handleChange: ()=>(null) } | |
}, | |
handleChange: function(event) { | |
this.props.handleChange(event.target.value) | |
this.setState({text: event.target.value}); | |
}, | |
componentWillReceiveProps: function(nextProps) { | |
nextProps.hasOwnProperty("val") && this.setState({text: nextProps.val}); | |
}, | |
render: function() { | |
var text = this.state.text; | |
return <input value={text} onChange={this.handleChange} { ...this.props } />; | |
} | |
}); | |
function editCharacter(e){ | |
const id = this; | |
console.log("editCharacter", id); | |
update({ | |
...data, | |
view: "editCharacter", | |
viewData: { id } | |
}) | |
}; | |
function deleteCharacter(e){ | |
const id = this; | |
console.log("deleteCharacter", id); | |
ref.child(id).remove(); | |
}; | |
function addCharacter(data, character){ | |
return { | |
...data, | |
characters: [ | |
...data.characters, | |
{ | |
...character | |
} | |
] | |
} | |
}; | |
function removeCharacter(data, id){ | |
let index = data.characters.findIndex( (item)=>( | |
item.id === id | |
) ); | |
return { | |
...data, | |
characters: [ | |
...data.characters.slice(0, index), | |
...data.characters.slice(index+1) | |
] | |
} | |
}; | |
function replaceCharacter(data, character){ | |
let index = data.characters.findIndex( (item)=>( | |
item.id === character.id | |
) ); | |
return { | |
...data, | |
characters: [ | |
...data.characters.slice(0, index), | |
character, | |
...data.characters.slice(index+1) | |
] | |
} | |
}; | |
function updateCharacter(data, character, id){ | |
let index = data.characters.findIndex( (item)=>( | |
item.id === id | |
) ); | |
return { | |
...data, | |
characters: [ | |
...data.characters.slice(0, index-1), | |
{ | |
...character | |
}, | |
...data.characters.slice(index+1), | |
] | |
} | |
}; | |
function saveCharacter(){ | |
console.log("saveCharacter", data.viewData.id); | |
const character = { ...collectData( document.querySelector(".editView .cs") ), id: data.viewData.id || -1 }; | |
console.log("saveCharacter", character); | |
// handle update | |
let doUpdate = (data.viewData.id !== undefined); | |
update({ | |
...data, | |
view: "list", | |
viewData: {} | |
}); | |
console.log("doUpdate", doUpdate, character.id, character); | |
if( doUpdate ){ | |
ref.child(character.id).set(character); | |
} else { | |
ref.push(character); | |
} | |
}; | |
function cancelCharacterEdit(){ | |
console.log("cancelCharacterEdit"); | |
update({ | |
...data, | |
view: "list" | |
}) | |
}; | |
function fillSkill(){ | |
let { level, column, skill, character } = this; | |
console.log("fillSkill", level, column, skill, !!character ); | |
let unsaved = Immutable.fromJS(data.viewData.unsaved || character).setIn(['skills', level, column], skill).toJS(); | |
update({ | |
...data, | |
viewData: { | |
id: character.id, | |
unsaved: unsaved | |
} | |
}); | |
} | |
const inputTemplates = { | |
text: (path, val, handleChange)=>( <Input type="text" key={path} data-model={path} val={val} handleChange={handleChange} /> ), | |
textarea: (path, val, handleChange)=>( <textarea data-model={path} key={path} defaultValue={val} handleChange={handleChange} ></textarea> ), | |
checkbox: (path, val, handleChange)=>( <input type="checkbox" key={path} data-model={path} defaultChecked={ !!val } handleChange={handleChange} /> ) | |
} | |
function getIn(obj, path){ | |
return path.split(".").reduce( (ref, key)=>( | |
typeof ref === "object" && key in ref ? ref[key] : undefined | |
), obj); | |
} | |
function getInput(type, path, data, def, handleChange){ | |
def = def || type === "checkbox" ? false : ""; | |
let val = getIn(data, path) || def; | |
return inputTemplates[type](path, val, handleChange); | |
} | |
function addToObject(data, path, val){ | |
let ref = data; | |
path.split(".").forEach( (key, index, arr) => { | |
if(index + 1 == arr.length){ | |
ref[key] = val; | |
} else { | |
ref[key] = ref[key] || {}; | |
ref = ref[key]; | |
} | |
}); | |
return data; | |
} | |
function getSkillLevels(character){ | |
let skills = Immutable.Map(); | |
character.skills && Immutable.fromJS(character.skills).toArray().forEach( (level, levelIndex)=>( | |
level.toArray().forEach( (skill)=>( | |
!!skill && (skills = skills.set(skill, levelIndex+1)) | |
) ) | |
) ); | |
return skills.toJS(); | |
} | |
function collectData(el){ | |
var model = {}; | |
el.querySelectorAll("[data-model]").forEach( (el)=>{ | |
let val = el.getAttribute("type") === "checkbox" ? el.checked : el.value; | |
let path = el.getAttribute("data-model"); | |
model = addToObject(model, path, val); | |
} ); | |
console.log("collectData", model); | |
return model; | |
} | |
function update(newData){ | |
console.log("update", newData); | |
data = newData; | |
ReactDOM.render(views[data.view](data), document.querySelector("#wrapper")); | |
}; | |
function connectDB(){ | |
var config = { | |
apiKey: "AIzaSyDIKfY8EvvjFBeF_uEW0ajV6jhVtsw9qs0", | |
authDomain: "fir-demom.firebaseapp.com", | |
databaseURL: "https://fir-demom.firebaseio.com", | |
storageBucket: "", | |
}; | |
var db = firebase.initializeApp(config).database(); | |
ref = db.ref("fate"); | |
ref.on("child_added", (snapshot)=>{ | |
let character = snapshot.val(); | |
character.id = snapshot.key; | |
console.log("child_added", character ); | |
update( addCharacter(data, character ) ) | |
}); | |
ref.on("child_removed", (snapshot)=>{ | |
console.log("child_removed", snapshot.key ); | |
update( removeCharacter(data, snapshot.key ) ) | |
}); | |
ref.on("child_changed", (snapshot)=>{ | |
console.log("child_changed", snapshot.key, snapshot.val() ); | |
update( replaceCharacter(data, snapshot.val() ) ) | |
}); | |
}; | |
function getUnsavedCharacterInput(type, path, dataItem, def){ | |
return getInput(type, path, dataItem, def, (newVal)=>{ | |
let character = data.viewData.id !== undefined && data.characters.find( (item) => ( item.id === data.viewData.id ) ) || {}; | |
return update({ | |
...data, | |
viewData: { | |
...data.viewData, | |
unsaved: Immutable.fromJS(data.viewData.unsaved || character).setIn(path.split("."), newVal).toJS() | |
} | |
}) | |
}); | |
}; | |
function closeModal(){ | |
update({ | |
...data, | |
viewData: { | |
...data.viewData, | |
modal: undefined | |
} | |
}); | |
} | |
function displayDeleteCharacterConfirmation(id, data){ | |
let character = data.characters.find( (item) => ( item.id === id ) ) || {}; | |
update({ | |
...data, | |
viewData: { | |
...data.viewData, | |
modal: { | |
cls: "modal-danger", | |
type: "confirm", | |
text: "Opravdu si přejete smazat charakter " + character.name + "?", | |
confirm: ()=>{ | |
closeModal(); | |
deleteCharacter.bind(character.id)(); | |
}, | |
cancel: closeModal | |
} | |
} | |
}); | |
} | |
const views = { | |
modal: (modalData) => { | |
return <div className={ "modal " + (modalData.type ? "modal-" + modalData.type : "" ) + " " + (modalData.cls || "") } > | |
<div className="modal-content" > | |
<div className="modal-body" >{modalData.text}</div> | |
<div className="modal-controls" > | |
{ modalData.cancel && <button onClick={modalData.cancel} className="btn modal-cancel">{config.texts.cancel}</button> || null } | |
{ modalData.confirm && <button onClick={modalData.confirm} className="btn btn-primary modal-confirm" >{config.texts.confirm}</button> || null } | |
</div> | |
</div> | |
</div> | |
}, | |
list: (data) => { | |
let modal = !data.viewData.modal ? undefined : views["modal"](data.viewData.modal); | |
return <div className="list" > | |
{ modal } | |
<h2>Characters</h2> | |
<div className="character-list" > | |
{ data.characters.map( (item)=>( <div className="list-item" ><div className="item-title" >{item.name}</div><div className="actions" ><button className="edit btn" onClick={ editCharacter.bind(item.id) } >{config.texts.edit}</button> <button className="delete btn btn-danger" onClick={ displayDeleteCharacterConfirmation.bind(this, item.id, data) } >x</button></div></div> ) ) } | |
</div> | |
<p><button onClick={editCharacter} className="btn btn-primary">+ {config.texts.new}</button></p> | |
</div> | |
}, editCharacter: (data) => { | |
let character = data.viewData.id !== undefined && data.characters.find( (item) => ( item.id === data.viewData.id ) ) || {}; | |
character = Immutable.fromJS(character).mergeDeep( data.viewData.unsaved ).toJS(); | |
console.log("character", character); | |
let skills = getSkillLevels(character); | |
let maxConsequences = config.consequences.defaultCount; | |
if( config.consequences.skills.reduce( (maxLevel, skill)=>( Math.max( maxLevel, skills[skill] || 0 ) ), 0 ) >= config.consequences.bonusSkillLevel ){ | |
maxConsequences += 1; | |
} | |
return <div className="editView" > | |
<div className="cs"> | |
<div className="row mb-sm"> | |
<div className="col-xs-12 col-sm-8"> | |
<div className="row mb-sm"> | |
<div className="col-xs-12 col-sm-12"><h2>ID</h2></div> | |
<div className="col-xs-12 col-sm-9 name-column"> | |
<label className="input-wrap name mb-sm"><span>Name</span>{getUnsavedCharacterInput("text", "name", character)}</label> | |
<div className="description mb-sm" ><label className="textarea-wrap"><span>Description</span>{getUnsavedCharacterInput("textarea", "description", character)}</label></div> | |
</div> | |
<div className="col-xs-12 col-sm-3 refresh mb-sm"> | |
<label className="textarea-wrap"><span>Refresh</span>{getUnsavedCharacterInput("textarea", "refresh", character)}</label> | |
</div> | |
</div> | |
</div> | |
<div className="col-xs-12 col-sm-4 logo mb-sm"> | |
<img src="http://3.bp.blogspot.com/-bJjDZ-FtpHQ/ULRW6uKF3JI/AAAAAAAAAh8/PRrdYkH2fi8/s1600/Fate%2BCore%2BCover.png" alt="FATE core system" /> | |
</div> | |
</div> | |
<div className="row mb"> | |
<div className="col-xs-12 col-sm-5 aspects"> | |
<h2>Aspects</h2> | |
<label className="input-wrap"><span>Main aspect</span>{getUnsavedCharacterInput("text", "aspects.main", character)}</label> | |
<label className="input-wrap"><span>Trouble</span>{getUnsavedCharacterInput("text", "aspects.trouble", character)}</label> | |
<label className="input-wrap">{getUnsavedCharacterInput("text", "aspects.3", character)}</label> | |
<label className="input-wrap">{getUnsavedCharacterInput("text", "aspects.4", character)}</label> | |
<label className="input-wrap">{getUnsavedCharacterInput("text", "aspects.5", character)}</label> | |
</div> | |
<div className="col-xs-12 col-sm-7 skills"> | |
<h2>Skills</h2> | |
{ seq(config.skillLevels).reverse().map( (level) =>{ | |
return <div className="row"> | |
<div className="col-xs-12 col-sm-2">{config.challengeLevels[level]} (+{level+1})</div> | |
{ seq(config.skillsPerLevel).map( (column)=>{ | |
let currentPath = "skills.level"+(level+1)+".column"+(column+1); | |
let val = getIn(character, currentPath); | |
let inputState = "skill-valid"; | |
let title; | |
if( level !== 0 ){ | |
let lowerSkillCount = Immutable.fromJS( getIn(character, "skills.level"+(level) ) || {} ).toArray().filter( (val)=>( !!val ) ).length; | |
if( column >= lowerSkillCount) { | |
if(!!val){ | |
inputState = "skill-invalid"; | |
title = "This exceeds maximum number of skills on this level"; | |
} else { | |
inputState = "skill-locked"; | |
title = "Maximum number of skills on this level reached"; | |
} | |
} | |
} | |
return <div className="col-xs-12 col-sm-2"><label className={"input-wrap " + inputState} title={title} >{getUnsavedCharacterInput("text", currentPath, character)}<div className="skill-list" > {[config.texts.clearText].concat(config.skillList).map( (skill)=>( <button onClick={ fillSkill.bind({ level: "level"+(level+1), column: "column"+(column+1), skill: (skill === config.texts.clearText ? "" : skill), character}) } >{skill}</button> ) )}</div></label></div> | |
} ) } | |
</div> } ) } | |
</div> | |
</div> | |
<div className="row mb"> | |
<div className="col-xs-12 col-sm-6"> | |
<h2>Extras</h2> | |
<div className="extras" >{getUnsavedCharacterInput("textarea", "extras", character)}</div> | |
</div> | |
<div className="col-xs-12 col-sm-6"> | |
<h2>Stunts</h2> | |
<div className="stunts" >{getUnsavedCharacterInput("textarea", "stunts", character)}</div> | |
</div> | |
</div> | |
<div className="row"> | |
<div className="col-xs-12 col-sm-4"> | |
{ config.stress.map( (stress)=>{ | |
let maxStress = stress.def; | |
if(stress.skill in skills){ | |
maxStress = maxStress + (skills[stress.skill] >= 3 ? 2 : 1); | |
} | |
return <div className="stress-lane" ><h2>{stress.label} <span className="note">({stress.skill})</span></h2> | |
<div className="stress"> | |
{ seq(stress.count).map( (stressBox, index)=>( | |
<label className={ "checkbox" + ((index >= maxStress) ? " disabled" : "") } ><span><i className="superscript">{index+1}</i></span>{getUnsavedCharacterInput("checkbox", "stress."+stress.key+"."+(index+1), character)}<s></s></label> | |
) ) } | |
</div> | |
</div> | |
} ) } | |
</div> | |
<div className="col-xs-12 col-sm-8 consequences"> | |
<h2>Consequences</h2> | |
<div>{config.consequences.list.map( (consequence, index)=>( | |
<label key={consequence.key} className={"input-wrap" + (index >= maxConsequences ? " disabled" : "")}><i className="superscript" >{consequence.val}</i><span>{consequence.label}</span>{getUnsavedCharacterInput("text",consequence.key, character)}</label> | |
) )}</div> | |
</div> | |
</div> | |
</div> | |
<div className="character-edit-controls" ><hr /><button onClick={cancelCharacterEdit} className="btn" >{config.texts.cancel}</button><button onClick={saveCharacter} className="btn btn-success" >{config.texts.save}</button></div> | |
</div>; | |
} | |
} | |
connectDB(); | |
update(data); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script src="https://npmcdn.com/react@15.3.0/dist/react.min.js"></script> | |
<script src="https://npmcdn.com/react-dom@15.3.0/dist/react-dom.min.js"></script> | |
<script src="http://www.gstatic.com/firebasejs/live/3.0/firebase.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.1/immutable.js"></script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
*, *:after, *:before { | |
box-sizing: border-box; | |
} | |
#wrapper { | |
padding: 20px; | |
min-width: 300px; | |
overflow: auto; | |
} | |
h2 { | |
position: relative; | |
background-color: #000; | |
color: #fff; | |
text-transform: uppercase; | |
margin: 0 0 10px 6px; | |
font-size: 14px; | |
line-height: 18px; | |
.note { | |
display: inline-block; | |
font-size: 10px; | |
line-height: inherit; | |
} | |
&:after { | |
content: ""; | |
position: absolute; | |
left: -6px; | |
top: 0; | |
width: 0; | |
height: 100%; | |
border-top: 6px solid transparent; | |
border-right: 6px solid #000; | |
} | |
} | |
.btn { | |
position: relative; | |
border: 2px solid #000; | |
background-color: transparent; | |
font-size: 12px; | |
text-transform: uppercase; | |
background-color: #000; | |
color: #fff; | |
font-weight: bold; | |
padding: 0 5px 0 0; | |
margin-left: 6px; | |
&:before { | |
content: ""; | |
position: absolute; | |
left: -8px; | |
top: -2px; | |
width: 0; | |
bottom: -2px; | |
border-top: 6px solid transparent; | |
border-right: 6px solid #000; | |
} | |
&:hover { | |
color: #000; | |
background-color: #fff; | |
&:after { | |
content: ""; | |
position: absolute; | |
left: -6px; | |
top: 0px; | |
width: 0; | |
bottom: 0px; | |
border-top: 6px solid transparent; | |
border-right: 6px solid #fff; | |
} | |
} | |
&-success { | |
background-color: #0a0; | |
border-color: #0a0; | |
&:hover { | |
color: #0a0; | |
} | |
&:before { | |
border-color: transparent #0a0; | |
} | |
} | |
&-danger { | |
background-color: #a00; | |
border-color: #a00; | |
&:hover { | |
color: #a00; | |
} | |
&:before { | |
border-color: transparent #a00; | |
} | |
} | |
&-primary { | |
background-color: #00a; | |
border-color: #00a; | |
&:hover { | |
color: #00a; | |
} | |
&:before { | |
border-color: transparent #00a; | |
} | |
} | |
} | |
.modal { | |
position: fixed; | |
left: 0; | |
top: 0; | |
width: 100vw; | |
height: 100vh; | |
background-color: rgba(0,0,0,0.9); | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
z-index: 1000; | |
overflow: auto; | |
&-content { | |
position: relative; | |
background-color: #fff; | |
padding: 20px; | |
max-width: 400px; | |
min-width: 200px; | |
clip-path: polygon(20px 0, 100% 0, 100% 100%, 0 100%, 0 20px); | |
} | |
&-controls { | |
margin-top: 10px; | |
padding-top: 10px; | |
border-top: 2px solid #000; | |
text-align: right; | |
button + button { | |
margin-left: 10px; | |
} | |
} | |
&-danger &-content { | |
border: 10px solid #a00; | |
&:after { | |
content: ""; | |
position: absolute; | |
left: 0; | |
top: 0; | |
border-left: 15px solid #a00; | |
border-bottom: 15px solid transparent | |
} | |
} | |
} | |
hr { | |
background: #000; | |
height: 2px; | |
} | |
.character-edit-controls { | |
text-align: right; | |
margin-top: 20px; | |
button + button { | |
margin-left: 10px; | |
} | |
} | |
.character-list { | |
.list-item { | |
padding: 4px 0; | |
border-bottom: 2px solid #000; | |
display: flex; | |
&:hover { | |
background-color: #f2f2f2; | |
} | |
} | |
.item-title { | |
flex: 1 0 auto; | |
font-weight: bold; | |
} | |
} | |
.cs { | |
position: relative; | |
background-color: #fff; | |
font-size: 10px; | |
&:after { | |
position: absolute; | |
content: ""; | |
left: 0; | |
top: 0; | |
width: 100%; | |
height: 100%; | |
opacity: 0.5; | |
z-index: -1; | |
//background: transparent url('http://rpgknights.com/wp-content/uploads/2013/10/Fate-Core-Character-Sheet-Draft1.jpg') 0 0 no-repeat; | |
background-size: contain; | |
} | |
$inputBorder: 2px solid #000; | |
.label { | |
cursor: text; | |
border: $inputBorder; | |
background-color: #fff; | |
line-height: 1.5; | |
padding: 0 4px; | |
color: #888; | |
white-space: nowrap; | |
text-overflow: ellipsis; | |
overflow: hidden; | |
text-transform: uppercase; | |
} | |
.logo img { | |
display: block; | |
width: 100%; | |
} | |
.input-wrap { | |
position: relative; | |
display: flex; | |
min-width: 64px; | |
&.disabled, &.skill-locked { | |
input, span, i { | |
border-color: #888; | |
color: #888; | |
pointer-events: none; | |
} | |
} | |
&.skill-invalid { | |
input, span i { | |
border-color: #a00; | |
color: #a00; | |
pointer-events: none; | |
} | |
} | |
span { | |
@extend .label; | |
flex: 0 1 auto; | |
border-right: 0; | |
min-width: 32px; | |
&+input { | |
border-left: 0; | |
} | |
} | |
input { | |
line-height: 1.5; | |
flex: 1 0 0; | |
width: 0; | |
border: $inputBorder; | |
padding: 0 5px; | |
min-width: 32px; | |
&:focus + .skill-list { | |
visibility: visible; | |
opacity: 1; | |
} | |
} | |
.skill-list { | |
position: absolute; | |
opacity: 0; | |
visibility: hidden; | |
top: 100%; | |
right: 0; | |
padding: 10px; | |
background-color: rgba(255,255,255,0.9); | |
z-index: 1; | |
min-width: 300px; | |
border: 2px solid #888; | |
transition: all 0.5s; | |
button { | |
padding: 0; | |
border: 0; | |
color: #44a; | |
background: transparent none; | |
cursor: pointer; | |
display: inline; | |
margin: 0 5px 5px 5px; | |
} | |
} | |
} | |
.textarea-wrap { | |
position: relative; | |
height: 100%; | |
display: flex; | |
flex-direction: column; | |
&.disabled span, &.disabled textarea, &.disabled .superscript { | |
border-color: #888; | |
color: #888; | |
pointer-events: none; | |
} | |
span { | |
@extend .label; | |
border-bottom: 0; | |
display: block; | |
flex: 0; | |
} | |
textarea { | |
flex: 1 1 40px; | |
line-height: 1.5; | |
width: 100%; | |
border: $inputBorder; | |
border-top: 0; | |
resize: vertical; | |
} | |
} | |
.superscript { | |
position: absolute; | |
font-size: 1.8em; | |
font-weight: bold; | |
left: 0; | |
top: 0; | |
font-style: normal; | |
z-index: 1; | |
text-shadow: 2px 2px 0 #fff, -2px -2px 0 #fff, -2px 2px 0 #fff, 2px -2px 0 #fff; | |
transform: translate(-50%, -50%); | |
} | |
.checkbox { | |
position: relative; | |
display: inline-block; | |
cursor: pointer; | |
input { | |
position: absolute; | |
clip: rect(0,0,0,0); | |
&:checked + s:after { | |
content: ""; | |
position: absolute; | |
left: 2px; | |
top: 2px; | |
right: 2px; | |
bottom: 2px; | |
background-color: #000; | |
} | |
} | |
s { | |
user-select: none; | |
position: relative; | |
display: inline-block; | |
width: 20px; | |
height: 20px; | |
border: $inputBorder; | |
} | |
&.disabled, &.disabled input, &.disabled s, &.disabled span { | |
pointer-events: none; | |
cursor: auto; | |
border-color: #888; | |
color: #888 | |
} | |
} | |
.mb { | |
margin-bottom: 10px; | |
} | |
.mb-sm { | |
margin-bottom: 5px; | |
} | |
.name-column { | |
display: flex; | |
flex-direction: column; | |
.description { | |
flex-grow: 1; | |
display: flex; | |
flex-direction: column; | |
position: relative; | |
.textarea-wrap { | |
display: flex; | |
flex-direction: column; | |
flex: 1; | |
} | |
} | |
} | |
.aspects label { | |
margin-bottom: 5px; | |
} | |
.skills .row > div { | |
margin-bottom: 5px; | |
} | |
.extras textarea, .stunts textarea { | |
width: 100%; | |
height: 80px; | |
border: $inputBorder; | |
resize: vertical; | |
} | |
.consequences label + label { | |
margin-top: 5px; | |
} | |
.stress { | |
margin: 0 -5px 10px -5px; | |
display: flex; | |
flex-wrap: wrap; | |
label { | |
flex: 1; | |
margin: 0 5px 10px 5px; | |
} | |
} | |
.refresh textarea { | |
text-align: center; | |
font-size: 60px; | |
line-height: 60px; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<link href="//cdnjs.cloudflare.com/ajax/libs/flexboxgrid/6.3.1/flexboxgrid.min.css" rel="stylesheet" /> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment