Compare commits

..

2 Commits

Author SHA1 Message Date
bumi a75bcc6a59 Add configurable network name
This is required for the infura provider
2018-04-22 20:25:58 +02:00
bumi f5dc8f86cf Use infura hosted provider if web3 is not available
If the user does not have metamask or similar (no web3 provider is
availale) we will load data from the infura node.

No signer is available for the infura node but the ethers provider would
throw an error if we try to access it - so this moves the decision of
choosing the provider to the ethProvider function.
2018-04-22 18:41:51 +02:00
260 changed files with 21208 additions and 40410 deletions
+1
View File
@@ -4,6 +4,7 @@
root = true root = true
[*] [*]
end_of_line = lf end_of_line = lf
charset = utf-8 charset = utf-8
-21
View File
@@ -1,21 +0,0 @@
# unconventional js
/blueprints/*/files/
/vendor/
# compiled output
/dist/
/tmp/
/release/
# dependencies
/bower_components/
/node_modules/
# misc
/coverage/
!.*
# ember-try
/.node_modules.ember-try/
/bower.json.ember-try
/package.json.ember-try
+7 -27
View File
@@ -1,14 +1,8 @@
'use strict';
module.exports = { module.exports = {
root: true, root: true,
parser: 'babel-eslint',
parserOptions: { parserOptions: {
ecmaVersion: 2018, ecmaVersion: 2017,
sourceType: 'module', sourceType: 'module'
ecmaFeatures: {
legacyDecorators: true
}
}, },
globals: { globals: {
console: true console: true
@@ -25,39 +19,25 @@ module.exports = {
}, },
rules: { rules: {
'ember/avoid-leaking-state-in-ember-objects': 'warn', 'ember/avoid-leaking-state-in-ember-objects': 'warn',
'no-console': 'off', 'no-console': 'off'
'ember/no-jquery': 'error',
'ember/require-computed-property-dependencies': 'warn',
'ember/no-observers': 'warn'
}, },
overrides: [ overrides: [
// node files // node files
{ {
files: [ files: [
'.eslintrc.js',
'.template-lintrc.js',
'ember-cli-build.js', 'ember-cli-build.js',
'testem.js', 'testem.js',
'blueprints/*/index.js',
'config/**/*.js', 'config/**/*.js',
'lib/*/index.js', 'lib/*/index.js'
'server/**/*.js'
], ],
parserOptions: { parserOptions: {
sourceType: 'script' sourceType: 'script',
ecmaVersion: 2015
}, },
env: { env: {
browser: false, browser: false,
node: true node: true
}, }
plugins: ['node'],
rules: Object.assign({}, require('eslint-plugin-node').configs.recommended.rules, {
// add your custom rules and overrides for node files here
// this can be removed once the following is fixed
// https://github.com/mysticatea/eslint-plugin-node/issues/77
'node/no-unpublished-require': 'off'
})
} }
] ]
}; };
-4
View File
@@ -1,4 +0,0 @@
template: |
## Changes
$CHANGES
+11 -13
View File
@@ -1,26 +1,24 @@
# See https://help.github.com/ignore-files/ for more about ignoring files. # See https://help.github.com/ignore-files/ for more about ignoring files.
# compiled output # compiled output
/dist/ /dist
/tmp/ /tmp
# dependencies # dependencies
/bower_components/ /node_modules
/node_modules/ /bower_components
# misc # misc
/.env*
/.pnp*
/.sass-cache /.sass-cache
/connect.lock /connect.lock
/coverage/ /coverage/*
/libpeerconnection.log /libpeerconnection.log
/npm-debug.log* npm-debug.log*
/testem.log yarn-error.log
/yarn-error.log testem.log
.tm_properties .tm_properties
# ember-try # ember-try
/.node_modules.ember-try/ .node_modules.ember-try/
/bower.json.ember-try bower.json.ember-try
/package.json.ember-try package.json.ember-try
-93
View File
@@ -1,93 +0,0 @@
'use strict';
module.exports = {
extends: 'octane',
rules: {
'simple-unless': false,
'no-nested-interactive': false
},
ignore: [
'kredits-web/templates/components/**',
'app/templates/components/**'
],
pending: [
{
"moduleId": "app/templates/dashboard",
"only": [
"no-action"
]
},
{
"moduleId": "app/components/add-contribution/template",
"only": [
"no-action",
"no-curly-component-invocation"
]
},
{
"moduleId": "app/components/add-contributor/template",
"only": [
"no-action",
"no-curly-component-invocation"
]
},
{
"moduleId": "app/components/contribution-list/template",
"only": [
"no-action",
"no-curly-component-invocation"
]
},
{
"moduleId": "app/components/contributor-list/template",
"only": [
"no-action"
]
},
{
"moduleId": "app/components/topbar-account-panel/template",
"only": [
"no-action"
]
},
{
"moduleId": "app/templates/contributions/new",
"only": [
"no-action"
]
},
{
"moduleId": "app/templates/contributions/resubmit",
"only": [
"no-action"
]
},
{
"moduleId": "app/templates/contributors/edit",
"only": [
"no-action"
]
},
{
"moduleId": "app/templates/contributors/new",
"only": [
"no-action"
]
},
{
"moduleId": "app/templates/signup/eth-account",
"only": [
"no-action"
]
},
{
"moduleId": "app/templates/signup/index",
"only": [
"no-action"
]
}
]
};
+5 -10
View File
@@ -1,15 +1,10 @@
--- ---
language: node_js language: node_js
node_js: node_js:
- "10" - "6"
sudo: false sudo: false
dist: xenial dist: trusty
notifications:
email:
on_success: never
on_failure: always
addons: addons:
chrome: stable chrome: stable
@@ -23,9 +18,9 @@ env:
# See https://git.io/vdao3 for details. # See https://git.io/vdao3 for details.
- JOBS=1 - JOBS=1
branches: before_install:
only: - npm config set spin false
- master
script: script:
- npm run lint:js
- npm test - npm test
+15 -28
View File
@@ -22,8 +22,7 @@ You will need the following things properly installed on your computer.
## Running / Development ## Running / Development
* `ember serve`
* `ember serve` - by default kredits-web connects to the Ethreum Kovan network
* Visit your app at [http://localhost:4200](http://localhost:4200). * Visit your app at [http://localhost:4200](http://localhost:4200).
* Visit your tests at [http://localhost:4200/tests](http://localhost:4200/tests). * Visit your tests at [http://localhost:4200/tests](http://localhost:4200/tests).
@@ -40,7 +39,6 @@ Make use of the many generators for code, try `ember help generate` for more det
### Linting ### Linting
* `npm run lint:hbs`
* `npm run lint:js` * `npm run lint:js`
* `npm run lint:js -- --fix` * `npm run lint:js -- --fix`
@@ -58,12 +56,13 @@ _(You need collaborator permissions on the 5apps Deploy project.)_
## Working with locally deployed contracts ## Working with locally deployed contracts
The smart contracts and their JavaScript wrapper library are developed in the The smart contracts and their JavaScript wrapper library are developed in the
[kredits-contracts](https://github.com/67P/kredits-contracts) repo/package. [truffle-kredits](https://github.com/67P/truffle-kredits) repo/package.
You can run `kredits-web` on your machine, against a local, simulated Ethereum You can run `kredits-web` on your machine, against a local, simulated Ethereum
network, provided e.g. by [ganache](http://truffleframework.com/ganache/). network, provided e.g. by [ganache](http://truffleframework.com/ganache/) or
[ganache-cli](https://github.com/trufflesuite/ganache-cli).
[kredits-contracts](https://github.com/67P/kredits-contracts) holds all the tools [truffle-kredits](https://github.com/67P/truffle-kredits) holds all the tools
to start and set up such a simulated network, as well as to deploy smart to start and set up such a simulated network, as well as to deploy smart
contracts to it. contracts to it.
@@ -71,38 +70,27 @@ These are the basic steps to get up and running:
#### 1. IPFS #### 1. IPFS
Run a local IPFS deamon. Run a local IPFS deamon in offline mode.
* Make sure CORS headers are configured. See [IPFS](#ipfs) for more info. * Make sure CORS headers are configured. See [IPFS](#ipfs) for more info.
* `ipfs daemon` * `ipfs daemon --offline`
#### 2. kredits-contracts #### 2. truffle-kredits
Get your local Ethereum development node running. (See [kredits-contracts README](https://github.com/67P/kredits-contracts) Get your local Ethereum development node running.
for details.
* Clone [kredits-contracts](https://github.com/67P/kredits-contracts) * Clone [truffle-kredits](https://github.com/67P/truffle-kredits)
* `npm install` * `npm install`
* `npm run devchain` - runs a local development chain * `npm run ganache` - which is basically: `ganache-cli -p 7545 -i 100` (we use the non-default port for local networks and a fixed network id)
* `npm run bootstrap` - bootstrap runs deploys all the contracts * `npm run bootstrap` - bootstrap runs fresh migrations, adds some seed data and writes the address/abi information to JSON that will be used by kredits-web
* `npm link` - make the `kredits-contracts` module linkable as `kredits-contracts` on your machine (currently broken :( ) * `npm link` - make the `truffle-kredits` module linkable as `kredits-contracts` on your machine
#### 3. kredits-web #### 3. kredits-web
With IPFS and Ethereum/ganache running, you can now start this Ember app. With IPFS and Ethereum/ganache running, you can now start this Ember app.
* `npm link kredits-contracts` - link the local `kredits-contracts` package * `npm link kredits-contracts` - link the local `truffle-kredits` package (it will become `kredits-contracts` soon)
* `npm run start:local` - WEB3_PROVIDER_URL=http://localhost:7545 must be set for local settings * `npm start`
If you restart the devchain your network ID changes. So kredits-contracts does not find the correct DAO address.
If that is the case the DAO address must be provided as `KREDITS_DAO_ADDRESS` environment variable.
* `npm run dao:address` in the kredits-contracts repo to find your address
* `KREDITS_DAO_ADDRESS=<0xYOURADDRESS> npm run start:local`
#### 4. Metamask network
Switch the network in Metamask to "Custom RPC" with the RPC URL `http://localhost:7545`.
#### IPFS #### IPFS
@@ -112,7 +100,6 @@ Install IPFS with your favorite package manager and run
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["localhost:4200"]' ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["localhost:4200"]'
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods '["PUT", "GET", "POST"]' ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods '["PUT", "GET", "POST"]'
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Credentials '["true"]' ipfs config --json API.HTTPHeaders.Access-Control-Allow-Credentials '["true"]'
ipfs config Addresses.Gateway /ip4/127.0.0.1/tcp/8080
## Further Reading / Useful Links ## Further Reading / Useful Links
+8 -6
View File
@@ -1,12 +1,14 @@
import Application from '@ember/application'; import Application from '@ember/application';
import Resolver from 'ember-resolver'; import Resolver from './resolver';
import loadInitializers from 'ember-load-initializers'; import loadInitializers from 'ember-load-initializers';
import config from './config/environment'; import config from './config/environment';
export default class App extends Application { const App = Application.extend({
modulePrefix = config.modulePrefix; modulePrefix: config.modulePrefix,
podModulePrefix = config.podModulePrefix; podModulePrefix: config.podModulePrefix,
Resolver = Resolver; Resolver
} });
loadInitializers(App, config.modulePrefix); loadInitializers(App, config.modulePrefix);
export default App;
@@ -1,78 +0,0 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import { and, notEmpty } from '@ember/object/computed';
import { assign } from '@ember/polyfills';
import moment from 'moment';
export default Component.extend({
attributes: null,
contributors: Object.freeze([]),
isValidContributor: notEmpty('contributorId'),
isValidKind: notEmpty('kind'),
isValidAmount: computed('amount', function() {
return parseInt(this.amount, 10) > 0;
}),
isValidDescription: notEmpty('description'),
isValidUrl: notEmpty('url'),
isValid: and('isValidContributor',
'isValidKind',
'isValidAmount',
'isValidDescription'),
init () {
this._super(...arguments);
this.set('defaultDate', moment().startOf('hour').toDate());
this.set('defaultAttr', {
contributorId: null,
kind: null,
date: this.defaultDate,
amount: null,
description: null,
url: null,
details: null
});
this.set('attributes', assign({}, this.defaultAttr, this.attributes));
this.reset();
},
reset () {
this.setProperties(this.attributes);
},
actions: {
submit (evt) {
evt.preventDefault();
if (!this.isValid) {
alert('Invalid data. Please review and try again.');
return;
}
const attributes = this.getProperties(Object.keys(this.attributes));
let dateInput = (attributes.date instanceof Array) ?
attributes.date[0] : attributes.date;
const [ date, time ] = dateInput.toISOString().split('T');
[ attributes.date, attributes.time ] = [ date, time ];
this.set('inProgress', true);
this.save(attributes)
.then((/*contribution*/) => {
this.reset();
}, err => {
console.warn(err);
window.alert('Something went wrong. Check the browser console for details.');
})
.finally(() => this.set('inProgress', false));
}
}
});
@@ -1,83 +0,0 @@
<form onsubmit={{action "submit"}}>
<label>
<p class="label">Contributor:</p>
<p>
<select required onchange={{action (mut this.contributorId) value="target.value"}}>
<option value="" selected disabled hidden></option>
{{#each @contributors as |contributor|}}
<option value={{contributor.id}} selected={{eq this.contributorId contributor.id}}>{{contributor.name}}</option>
{{/each}}
</select>
</p>
</label>
<label>
<p class="label">Kind:</p>
<p>
<select required onchange={{action (mut this.kind) value="target.value"}}>
<option value="" selected disabled hidden></option>
<option value="community" selected={{eq this.kind "community"}}>Community</option>
<option value="design" selected={{eq this.kind "design"}}>Design</option>
<option value="dev" selected={{eq this.kind "dev"}}>Development</option>
<option value="docs" selected={{eq this.kind "docs"}}>Documentation</option>
<option value="ops" selected={{eq this.kind "ops"}}>IT Operations</option>
<option value="special" selected={{eq this.kind "special"}}>Special</option>
</select>
</p>
</label>
<label>
<p class="label">Date:</p>
<p>
{{ember-flatpickr
date=this.date
defaultDate=this.defaultDate
maxDate=this.defaultDate
enableTime=true
time_24hr=true
onChange=(action (mut this.date))
}}
</p>
</label>
<label>
<p class="label">Amount:</p>
<p>
{{input type="text"
placeholder="500"
value=this.amount
class=(if this.isValidAmount "valid" "")}}
</p>
</label>
<label>
<p class="label">Description:</p>
<p>
{{input type="text"
value=this.description
class=(if this.isValidDescription "valid" "")}}
</p>
</label>
<label>
<p class="label">URL (optional):</p>
<p>
{{input type="text"
value=this.url
class=(if this.isValidUrl "valid" "")}}
</p>
</label>
{{#if this.details}}
<label>
<p class="label">Details:</p>
<p>
<pre>
{{this.details}}
</pre>
</p>
</label>
{{/if}}
<p class="actions">
{{input type="submit"
disabled=this.inProgress
value=(if this.inProgress "Processing" "Save")}}
</p>
</form>
+30 -59
View File
@@ -1,93 +1,64 @@
import Component from '@ember/component'; import Component from '@ember/component';
import { computed } from '@ember/object';
import { and, notEmpty } from '@ember/object/computed'; import { and, notEmpty } from '@ember/object/computed';
import { inject as service } from '@ember/service'; import { inject as injectService } from '@ember/service';
import { isPresent } from '@ember/utils';
import { isAddress } from 'web3-utils';
export default Component.extend({ export default Component.extend({
kredits: injectService(),
router: service(), // Default attributes used by reset
kredits: service(), attributes: {
account: null,
name: null,
kind: 'person',
url: null,
github_username: null,
github_uid: null,
wiki_username: null,
isCore: false,
},
attributes: null, didInsertElement() {
this._super(...arguments);
isValidAccount: computed('account', function() { this.reset();
return isAddress(this.account); },
}),
// TODO: add proper address validation
isValidAccount: notEmpty('account'),
isValidName: notEmpty('name'), isValidName: notEmpty('name'),
isValidURL: notEmpty('url'), isValidURL: notEmpty('url'),
isValidGithubUID: notEmpty('github_uid'), isValidGithubUID: notEmpty('github_uid'),
isValidGithubUsername: notEmpty('github_username'), isValidGithubUsername: notEmpty('github_username'),
isValidGiteaUsername: notEmpty('gitea_username'),
isValidWikiUsername: notEmpty('wiki_username'), isValidWikiUsername: notEmpty('wiki_username'),
isValidZoomDisplayName: notEmpty('zoom_display_name'),
isValid: and( isValid: and(
'isValidAccount', 'isValidAccount',
'isValidName', 'isValidName',
'isValidGithubUID' 'isValidGithubUID'
), ),
inProgress: false,
init () {
this._super(...arguments);
this.setDefaultAttributes();
this.reset();
},
setDefaultAttributes () {
if (isPresent(this.attributes)) { return; }
this.set('attributes', {
account: null,
name: null,
kind: 'person',
url: null,
github_username: null,
github_uid: null,
gitea_username: null,
wiki_username: null,
zoom_display_name: null
});
},
reset: function() { reset: function() {
this.setProperties(this.attributes); this.setProperties(this.attributes);
}, },
actions: { actions: {
submit() {
submit (evt) {
evt.preventDefault();
if (!this.kredits.currentUserIsCore) {
window.alert('Only core team members can edit profiles. Please ask someone to set you up.');
return;
}
if (!this.isValid) { if (!this.isValid) {
window.alert('Invalid data. Please review and try again.'); alert('Invalid data. Please review and try again.');
return; return;
} }
const attributes = Object.keys(this.attributes); let attributes = Object.keys(this.attributes);
const contributor = this.getProperties(attributes); let contributor = this.getProperties(attributes);
let saved = this.save(contributor);
this.set('inProgress', true); // The promise handles inProgress
this.set('inProgress', saved);
this.save(contributor).then(() => { saved.then(() => {
this.reset(); this.reset();
}).catch(err => { window.scroll(0,0);
console.warn(err); window.alert('Contributor added.');
window.alert('Something went wrong. Please check the browser console.');
}).finally(() => {
this.set('inProgress', false);
this.router.transitionTo('dashboard');
}); });
} }
} }
}); });
+46 -38
View File
@@ -1,56 +1,64 @@
<form onsubmit={{action "submit"}}> <form {{action "submit" on="submit"}}>
<p> <p>
<label for="c-account">Ethereum account</label> {{input name="is-core"
<Input @type="text" type="checkbox"
@value={{this.account}} id="is-core"
name="account" id="c-account" checked=isCore}}
placeholder="0xF18E631Ea191aE4ebE70046Fcb01a436554421BA4" <label for="is-core" class="checkbox">
class={{if this.isValidAccount "valid" ""}} /> Core team member (can add contributors)
</label>
</p> </p>
<p> <p>
<label for="c-kind">Kind</label> {{input name="account"
<select required onchange={{action (mut this.kind) value="target.value"}} id="c-kind"> type="text"
<option value="person" selected={{eq this.kind "person"}}>Person</option> placeholder="0xF18E631Ea191aE4ebE70046Fcb01a436554421BA4"
<option value="organization" selected={{eq this.kind "organization"}}>Organization</option> value=account
class=(if isValidAccount 'valid' '')}}
</p>
<p>
<select required onchange={{action (mut kind) value="target.value"}}>
<option value="person" selected={{eq kind "person"}}>Person</option>
<option value="organization" selected={{eq kind "organization"}}>Organization</option>
</select> </select>
</p> </p>
<p> <p>
<label for="c-name">Name</label> {{input name="name"
{{input name="name" type="text" value=this.name placeholder="Zero Cool" type="text"
class=(if this.isValidName "valid" "") id="c-name"}} placeholder="Name"
value=name
class=(if isValidName 'valid' '')}}
</p> </p>
<p> <p>
<label for="c-url">URL</label> {{input name="url"
{{input name="url" type="text" value=this.url placeholder="http://zerocool.bit" type="text"
class=(if this.isValidURL "valid" "") id="c-url"}} placeholder="URL"
value=url
class=(if isValidURL 'valid' '')}}
</p> </p>
<p> <p>
<label for="c-github-uid">GitHub UID</label> {{input name="github_uid"
{{input name="github_uid" type="text" value=this.github_uid placeholder="2342" type="text"
class=(if this.isValidGithubUID "valid" "") id="c-github-uid"}} placeholder="GitHub UID (123)"
value=github_uid
class=(if isValidGithubUID 'valid' '')}}
</p> </p>
<p> <p>
<label for="c-github-username">GitHub username</label> {{input name="github_username"
{{input name="github_username" type="text" value=this.github_username placeholder="zerocool" type="text"
class=(if this.isValidGithubUsername "valid" "") id="c-github-username"}} placeholder="GitHub username"
value=github_username
class=(if isValidGithubUsername 'valid' '')}}
</p> </p>
<p> <p>
<label for="c-gitea-username">Gitea username</label> {{input name="wiki_username"
{{input name="gitea_username" type="text" value=this.gitea_username placeholder="zerocool" type="text"
class=(if this.isValidGiteaUsername "valid" "") id="c-gitea-username"}} placeholder="Wiki Username"
</p> value=wiki_username
<p> class=(if isValidWikiUsername 'valid' '')}}
<label for="c-wiki-username">Wiki username</label>
{{input name="wiki_username" type="text" value=this.wiki_username placeholder="ZeroCool"
class=(if this.isValidWikiUsername "valid" "") id="c-wiki-username"}}
</p>
<p>
<label for="c-zoom-display-name">Zoom display name</label>
{{input name="zoom_display_name" type="text" value=this.zoom_display_name placeholder="Zero Cool"
class=(if this.isValidZoomDisplayName "valid" "") id="c-zoom-display-name"}}
</p> </p>
<p class="actions"> <p class="actions">
{{input type="submit" disabled=this.inProgress {{input type="submit"
value=(if this.inProgress "Processing" "Save")}} disabled=(is-pending inProgress)
value=(if (is-pending inProgress) 'Processing' 'Save')}}
</p> </p>
</form> </form>
+57
View File
@@ -0,0 +1,57 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import { and, notEmpty } from '@ember/object/computed';
export default Component.extend({
// Default attributes used by reset
attributes: {
contributorId: null,
kind: 'community',
amount: null,
description: null,
url: null,
},
didInsertElement() {
this._super(...arguments);
this.reset();
},
contributors: [],
isValidContributor: notEmpty('contributorId'),
isValidAmount: computed('amount', function() {
return parseInt(this.amount, 10) > 0;
}),
isValidDescription: notEmpty('description'),
isValidUrl: notEmpty('url'),
isValid: and('isValidContributor',
'isValidAmount',
'isValidDescription'),
reset: function() {
this.setProperties(this.attributes);
},
actions: {
submit() {
if (!this.isValid) {
alert('Invalid data. Please review and try again.');
return;
}
let attributes = Object.keys(this.attributes);
let proposal = this.getProperties(attributes);
let saved = this.save(proposal);
// The promise handles inProgress
this.set('inProgress', saved);
saved.then(() => {
this.reset();
window.scroll(0,0);
window.alert('Proposal added.');
});
}
}
});
+44
View File
@@ -0,0 +1,44 @@
<form {{action "submit" on="submit"}}>
<p>
<select required onchange={{action (mut contributorId) value="target.value"}}>
<option value="" selected disabled hidden>Contributor</option>
{{#each contributors as |contributor|}}
<option value={{contributor.id}} selected={{eq contributorId contributor.id}}>{{contributor.github_username}}</option>
{{/each}}
</select>
</p>
<p>
<select required onchange={{action (mut kind) value="target.value"}}>
<option value="community" selected={{eq kind "community"}}>Community</option>
<option value="design" selected={{eq kind "design"}}>Design</option>
<option value="dev" selected={{eq kind "dev"}}>Development</option>
<option value="docs" selected={{eq kind "docs"}}>Documentation</option>
<option value="ops" selected={{eq kind "ops"}}>IT Operations</option>
</select>
</p>
<p>
{{input type="text"
placeholder="100"
value=amount
class=(if isValidAmount 'valid' '')}}
</p>
<p>
{{input type="text"
placeholder="Description"
value=description
class=(if isValidDescription 'valid' '')}}
</p>
<p>
{{input type="text"
placeholder="URL (optional)"
value=url
class=(if isValidUrl 'valid' '')}}
</p>
<p class="actions">
{{input type="submit"
disabled=(is-pending inProgress)
value=(if (is-pending inProgress) 'Processing' 'Save')}}
{{#link-to 'index'}}Back{{/link-to}}
</p>
</form>
@@ -1,64 +0,0 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
let categoryColors = {
community: "#fb6868",
design: "#fbe468",
dev: "#e068fb",
docs: "#97fb68",
ops: "#8f68fb",
}
export default Component.extend({
contributions: null,
chartOptions: Object.freeze({
legend: {
display: false
}
}),
chartData: computed('contributions', function() {
let kredits = this.contributions
.filterBy('vetoed', false)
.map(c => {
return { kind: c.kind, amount: c.amount }
})
.reduce(function (kinds, c) {
if (c.kind in kinds) {
kinds[c.kind] = kinds[c.kind] + c.amount
} else {
kinds[c.kind] = c.amount;
}
return kinds;
}, {});
return {
datasets: [{
data: [
kredits['community'],
kredits['design'],
kredits['dev'],
kredits['ops'],
kredits['docs'],
],
borderColor: [
categoryColors.community,
categoryColors.design,
categoryColors.dev,
categoryColors.ops,
categoryColors.docs,
],
borderWidth: 1
}],
labels: [
'Community',
'Design',
'Development',
'Operations & Infrastructure',
'Documentation'
],
}
})
});
@@ -1,6 +0,0 @@
<div class="chart">
<EmberChart @type="doughnut"
@data={{this.chartData}}
@options={{this.chartOptions}}
@width="200" @height="200" />
</div>
@@ -1,70 +0,0 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import { sort } from '@ember/object/computed';
import { isPresent } from '@ember/utils';
import { inject as service } from '@ember/service';
export default Component.extend({
router: service(),
tagName: 'div',
classNames: ['contributions'],
selectedContribution: null,
showQuickFilter: false,
hideSmallContributions: false,
contributorId: null,
contributionKind: null,
kredits: service(),
contributorsSorting: Object.freeze(['name:asc']),
contributors: sort('kredits.contributors', 'contributorsSorting'),
contributorsActive: computed('contributors.[]', 'contributions', function() {
const activeIds = new Set(this.contributions.mapBy('contributorId')
.map(id => id.toString()));
return this.contributors.filter(c => activeIds.has(c.id.toString()));
}),
contributionKinds: computed('contributions.[]', function() {
return this.contributions.mapBy('kind').uniq();
}),
contributionsFiltered: computed('contributions.[]', 'hideSmallContributions', 'contributorId', 'contributionKind', function() {
return this.contributions.filter(c => {
let included = true;
if (this.hideSmallContributions &&
c.amount <= 500) { included = false; }
if (isPresent(this.contributorId) &&
c.contributorId.toString() !== this.contributorId.toString()) { included = false; }
if (isPresent(this.contributionKind) &&
c.kind !== this.contributionKind) { included = false; }
return included;
});
}),
actions: {
veto (contributionId) {
if (this.contractInteractionEnabled) {
this.vetoContribution(contributionId);
} else {
window.alert('Only members can veto contributions. Please ask someone to set you up.');
}
},
openContributionDetails(contribution) {
this.router.transitionTo('dashboard.contributions.show', contribution);
}
}
});
@@ -1,56 +0,0 @@
{{#if @showQuickFilter}}
<div class="quick-filter">
<p>
<label class="filter-contributor">
Contributor:
<select onchange={{action (mut this.contributorId) value="target.value"}}>
<option value="" selected>all</option>
{{#each this.contributorsActive as |contributor|}}
<option value={{contributor.id}} selected={{eq this.contributorId contributor.id}}>{{contributor.name}}</option>
{{/each}}
</select>
</label>
<label class="filter-contribution-kind">
Kind:
<select onchange={{action (mut this.contributionKind) value="target.value"}}>
<option value="" selected>all</option>
{{#each this.contributionKinds as |kind|}}
<option value={{kind}} selected={{eq this.contributionKind kind}}>{{capitalize-string kind}}</option>
{{/each}}
</select>
</label>
<label class="filter-contribution-size">
<Input @type="checkbox" @checked={{this.hideSmallContributions}} />
Hide small contributions
</label>
</p>
</div>
{{/if}}
<ul class="contribution-list">
{{#each this.contributionsFiltered as |contribution|}}
<li role="button" data-contribution-id={{contribution.id}}
{{action "openContributionDetails" contribution}}
class="{{contribution-status contribution}}{{if (eq contribution.id @selectedContributionId) " selected"}}">
<p class="meta">
<span class="recipient"><UserAvatar @contributor={{contribution.contributor}} /></span>
<span class="category {{contribution.kind}}">({{contribution.kind}})</span>
<span class="title">{{contribution.description}}</span>
</p>
<p class="kredits-amount">
<span class="amount">{{contribution.amount}}</span><span class="symbol">₭S</span>
</p>
{{#unless contribution.vetoed}}
{{#unless (is-confirmed-contribution contribution)}}
<p class="voting">
{{input type="button" class="button small danger" value="veto"
click=(action "veto" contribution.id)
disabled=contribution.hasPendingChanges}}
</p>
{{/unless}}
{{/unless}}
</li>
{{/each}}
</ul>
+8 -7
View File
@@ -1,19 +1,20 @@
import Component from '@ember/component'; import Component from '@ember/component';
import { inject as service } from '@ember/service';
export default Component.extend({ export default Component.extend({
router: service(),
tagName: 'table', tagName: 'table',
classNames: 'contributor-list', classNames: 'contributor-list',
selectedContributor: null,
selectedContributorId: null,
actions: { actions: {
openContributorDetails(contributor) { toggleContributorInfo(contributor) {
this.router.transitionTo('dashboard.contributors.show', contributor); if (contributor.get('showMetadata')) {
contributor.set('showMetadata', false);
} else {
this.contributors.setEach('showMetadata', false);
contributor.set('showMetadata', true);
}
} }
} }
+18 -12
View File
@@ -1,21 +1,27 @@
<tbody> <tbody>
{{#each @contributorList as |c|}} {{#each contributors as |contributor|}}
<tr role="button" <tr class="{{if contributor.isCurrentUser 'current-user'}}" {{action "toggleContributorInfo" contributor}}>
onclick={{action "openContributorDetails" c.contributor}}
class="{{if (is-current-user c.contributor) "current-user"}} {{if (eq c.contributor.id @selectedContributorId) "selected"}}">
<td class="person"> <td class="person">
<UserAvatar @contributor={{c.contributor}} /> {{c.contributor.name}} <img class="avatar" src={{contributor.avatarURL}}>
{{contributor.name}}
</td> </td>
<td class="kredits"> <td class="kredits">
<span class="amount"> <span class="amount">{{contributor.balance}}</span>
{{#if @showUnconfirmedKredits}}
{{c.amountTotal}}
{{else}}
{{c.amountConfirmed}}
{{/if}}
</span>
<span class="symbol">₭S</span> <span class="symbol">₭S</span>
</td> </td>
</tr> </tr>
<tr class="metadata {{if contributor.isCurrentUser 'current-user'}} {{if contributor.showMetadata 'visible'}}">
<td colspan="2">
<ul>
<li><a href="https://testnet.etherscan.io/address/{{contributor.account}}">Inspect Ethereum transactions</a></li>
{{#if contributor.ipfsHash}}
<li><a href="https://ipfs.io/ipfs/{{contributor.ipfsHash}}">Inspect IPFS profile</a></li>
{{/if}}
</ul>
{{#if contributor.showMetadata}}
<pre>{{contributor.ipfsData}}</pre>
{{/if}}
</td>
</tr>
{{/each}} {{/each}}
</tbody> </tbody>
@@ -1,15 +0,0 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
export default Component.extend({
tagName: "",
account: null,
iconComponentName: computed('account.site', function() {
if (this.account.site.match(/github|gitea|wiki|zoom/)) {
return 'icon-account-' + this.account.site.replace(/\./g, '-');
} else {
return 'icon-web-globe';
}
})
});
@@ -1,5 +0,0 @@
<a href={{this.account.url}} target="_blank" rel="noopener noreferrer"
title="{{this.account.username}} on {{this.account.site}}">
<span class="site">{{this.account.site}}</span>
{{component this.iconComponentName}}
</a>
+2 -2
View File
@@ -2,7 +2,7 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1" x="0px" y="0px" viewBox="0 0 132 100" version="1.1" x="0px" y="0px" viewBox="0 0 132 100"
enable-background="new 0 0 100 100" xml:space="preserve"> enable-background="new 0 0 100 100" xml:space="preserve">
<path id="path-comet" <path id="path-comet"
d="M79.062,24.173L79.062,24.173l-0.021-0.011c-0.068-0.03-0.137-0.062-0.204-0.093L42.392,7.337 d="M79.062,24.173L79.062,24.173l-0.021-0.011c-0.068-0.03-0.137-0.062-0.204-0.093L42.392,7.337
c0,0,3.065,14.104,4.458,18.019l0,0c0,0-41.754-12.481-46.85-13.67c1.67,6.52,30.607,62.492,30.607,62.492 c0,0,3.065,14.104,4.458,18.019l0,0c0,0-41.754-12.481-46.85-13.67c1.67,6.52,30.607,62.492,30.607,62.492
c5.848,11.873,19.394,18.485,33.522,18.485c19.811,0,35.87-16.059,35.87-35.869C100,42.313,91.418,29.837,79.062,24.173z"> c5.848,11.873,19.394,18.485,33.522,18.485c19.811,0,35.87-16.059,35.87-35.869C100,42.313,91.418,29.837,79.062,24.173z">
@@ -10,5 +10,5 @@
</svg> </svg>
</p> </p>
<p> <p>
Loading data... Loading data from Ethereum...
</p> </p>
+20
View File
@@ -0,0 +1,20 @@
import Component from '@ember/component';
export default Component.extend({
tagName: 'ul',
classNames: ['proposal-list'],
actions: {
confirm(proposalId) {
if (this.contractInteractionEnabled) {
this.confirmProposal(proposalId);
} else {
window.alert('Only members can vote on proposals. Please ask someone to set you up.');
}
}
}
});
+12
View File
@@ -0,0 +1,12 @@
{{#each proposals as |proposal|}}
<li data-proposal-id={{proposal.id}} title="({{proposal.kind}}) {{proposal.description}}">
<span class="category {{proposal.kind}}">♥</span>
<span class="amount">{{proposal.amount}}</span><span class="symbol">₭S</span>
for <span class="recipient">{{proposal.contributor.name}}</span>
<span class="votes">({{proposal.votesCount}}/{{proposal.votesNeeded}} votes)</span>
{{#unless proposal.isExecuted}}
<button {{action "confirm" proposal.id}}>+1</button>
{{/unless}}
</li>
{{/each}}
@@ -1,46 +0,0 @@
import Component from '@ember/component';
import { inject as service } from '@ember/service';
import { computed } from '@ember/object';
import { isPresent } from '@ember/utils';
export default Component.extend({
tagName: '',
kredits: service(),
router: service(),
setupInProgress: false,
userHasEthereumWallet: computed(function() {
return isPresent(window.ethereum);
}),
showConnectButton: computed('userHasEthereumWallet',
'kredits.hasAccounts', function() {
return this.userHasEthereumWallet &&
!this.kredits.hasAccounts;
}),
actions: {
signup() {
this.router.transitionTo('signup');
},
async connectAccount() {
try {
await window.ethereum.enable();
this.set('setupInProgress', true);
await this.kredits.setup();
this.set('setupInProgress', false);
this.router.transitionTo('dashboard');
} catch (error) {
this.set('setupInProgress', false);
console.log('Opening Ethereum wallet failed:', error);
}
}
}
});
@@ -1,18 +0,0 @@
<section id="user-account">
{{#if this.setupInProgress}}
Connecting account...
{{else}}
{{#if (and this.kredits.hasAccounts this.kredits.currentUser)}}
{{this.kredits.currentUser.name}}
{{#if this.kredits.currentUserIsCore}}
<span class="core-flag">(core)</span>
{{/if}}
{{else}}
<span class="anonymous">Anonymous</span>
<button onclick={{action "signup"}} class="small" type="button">Sign up</button>
{{#if this.showConnectButton}}
<button onclick={{action "connectAccount"}} class="small green" type="button">Connect account</button>
{{/if}}
{{/if}}
{{/if}}
</section>
-33
View File
@@ -1,33 +0,0 @@
import Component from '@ember/component';
import { alias } from '@ember/object/computed';
import { computed } from '@ember/object';
const SIZES = {
'small': '128', // pixels
'medium': '256',
'large': '512'
}
export default Component.extend({
contributor: null,
tagName: 'img',
classNames: ['avatar'],
classNameBindings: ['size'],
attributeBindings: ['src', 'title'],
size: 'small',
src: alias('avatarURL'),
title: alias('contributor.name'),
avatarURL: computed('contributor.github_uid', 'size', function() {
const github_uid = this.contributor.github_uid;
if (github_uid) {
return `https://avatars2.githubusercontent.com/u/${github_uid}?v=3&s=${SIZES[this.size]}`;
} else {
// TODO use custom avatar
return '';
}
})
});
-2
View File
@@ -1,6 +1,4 @@
import Controller from '@ember/controller'; import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
export default Controller.extend({ export default Controller.extend({
kredits: service(),
}); });
-32
View File
@@ -1,32 +0,0 @@
import Controller from '@ember/controller';
import { alias, filterBy, sort } from '@ember/object/computed';
import { inject as service } from '@ember/service';
export default Controller.extend({
kredits: service(),
queryParams: ['contributorId', 'kind', 'amount'],
contributors: alias('kredits.contributors'),
minedContributors: filterBy('contributors', 'id'),
contributorsSorting: Object.freeze(['name:asc']),
sortedContributors: sort('minedContributors', 'contributorsSorting'),
actions: {
save (contribution) {
const contributor = this.contributors.findBy('id', contribution.contributorId);
contribution.contributorIpfsHash = contributor.ipfsHash;
return this.kredits.addContribution(contribution)
.then(contribution => {
this.transitionToRoute('index');
return contribution;
});
}
}
});
@@ -1,7 +0,0 @@
import ContributionsNewController from 'kredits-web/controllers/contributions/new';
export default ContributionsNewController.extend({
attributes: null,
});
-16
View File
@@ -1,16 +0,0 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
export default Controller.extend({
kredits: service(),
actions: {
save (attributes) {
return this.kredits.updateContributor(this.model.id, attributes);
}
}
});
-16
View File
@@ -1,16 +0,0 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
export default Controller.extend({
kredits: service(),
actions: {
save (contributor) {
return this.kredits.addContributor(contributor);
}
}
});
-57
View File
@@ -1,57 +0,0 @@
import Controller from '@ember/controller';
import { computed } from '@ember/object';
import { alias, not, sort } from '@ember/object/computed';
import { inject as service } from '@ember/service';
export default Controller.extend({
kredits: service(),
showDetailsPane: false,
selectedContributorId: null,
selectedContributionId: null,
currentBlock: alias('kredits.currentBlock'),
contributions: alias('kredits.contributions'),
contributionsConfirmed: alias('kredits.contributionsConfirmed'),
contributionsUnconfirmed: alias('kredits.contributionsUnconfirmed'),
contributionsSorting: Object.freeze(['date:desc', 'time:desc', 'id:desc']),
contributionsUnconfirmedSorted: sort('contributionsUnconfirmed', 'contributionsSorting'),
contributionsConfirmedSorted: sort('contributionsConfirmed', 'contributionsSorting'),
kreditsByContributor: alias('kredits.kreditsByContributor'),
kreditsToplistSorting: computed('showUnconfirmedKredits', function(){
return this.showUnconfirmedKredits ? ['amountTotal:desc'] : ['amountConfirmed:desc'];
}),
kreditsToplist: sort('kreditsByContributor', 'kreditsToplistSorting'),
showUnconfirmedKredits: true,
hideUnconfirmedKredits: not('showUnconfirmedKredits'),
showQuickFilterUnconfirmed: false,
showQuickFilterConfirmed: false,
showIntroText: computed('kredits.{hasAccounts,currentUser}', function(){
return (!this.kredits.hasAccounts || !this.kredits.currentUser);
}),
actions: {
vetoContribution (contributionId) {
this.kredits.veto(contributionId).then(transaction => {
console.debug('[controllers:index] Veto submitted to Ethereum blockhain: '+transaction.hash);
});
},
toggleQuickFilterUnconfirmed () {
this.toggleProperty('showQuickFilterUnconfirmed');
},
toggleQuickFilterConfirmed () {
this.toggleProperty('showQuickFilterConfirmed');
}
}
});
@@ -1,12 +0,0 @@
import Controller from '@ember/controller';
import { computed } from '@ember/object';
import config from 'kredits-web/config/environment';
export default Controller.extend({
ipfsGatewayUrl: computed(function() {
return config.ipfs.gatewayUrl;
})
});
@@ -1,17 +0,0 @@
import Controller from '@ember/controller';
import { computed } from '@ember/object';
import config from 'kredits-web/config/environment';
export default Controller.extend({
roleName: computed('model.isCore', 'totalKreditsEarned', function() {
if (this.model.isCore) return 'Core Contributor';
if (this.model.totalKreditsEarned <= 5000) return 'Newcomer';
return 'Contributor';
}),
ipfsGatewayUrl: computed(function() {
return config.ipfs.gatewayUrl;
})
});
+37
View File
@@ -0,0 +1,37 @@
import Controller from '@ember/controller';
import { alias, filter, filterBy, sort } from '@ember/object/computed';
import { inject as injectService } from '@ember/service';
export default Controller.extend({
kredits: injectService(),
contributors: alias('kredits.contributors'),
contributorsWithKredits: filter('contributors', function(contributor) {
return contributor.get('balance') !== 0;
}),
contributorsSorting: ['balance:desc'],
contributorsSorted: sort('contributorsWithKredits', 'contributorsSorting'),
proposals: alias('kredits.proposals'),
proposalsOpen: filterBy('proposals', 'isExecuted', false),
proposalsClosed: filterBy('proposals', 'isExecuted', true),
proposalsSorting: ['id:desc'],
proposalsClosedSorted: sort('proposalsClosed', 'proposalsSorting'),
proposalsOpenSorted: sort('proposalsOpen', 'proposalsSorting'),
actions: {
confirmProposal(proposalId) {
this.kredits.vote(proposalId).then(transaction => {
window.confirm('Vote submitted to Ethereum blockhain: '+transaction.hash);
});
},
save(contributor) {
return this.kredits.addContributor(contributor)
.then((contributor) => {
this.contributors.pushObject(contributor);
return contributor;
});
}
}
});
+24
View File
@@ -0,0 +1,24 @@
import Controller from '@ember/controller';
import { alias, filterBy } from '@ember/object/computed';
import { inject as injectService } from '@ember/service';
export default Controller.extend({
kredits: injectService(),
contributors: alias('kredits.contributors'),
minedContributors: filterBy('contributors', 'id'),
actions: {
save(proposal) {
// contributorIpfsHash is needed for the proposal ipfs data. I'm not happy to do this here but I think to load all the contributors in addProposal again is a bit too much. I hope we can refactor it later.
let contributor = this.contributors.findBy('id', proposal.contributorId);
proposal.contributorIpfsHash = contributor.get('ipfsHash');
return this.kredits.addProposal(proposal)
.then((proposal) => {
this.transitionToRoute('index');
return proposal;
});
}
}
});
-49
View File
@@ -1,49 +0,0 @@
import Controller from '@ember/controller';
import { computed } from '@ember/object';
import { alias, not } from '@ember/object/computed';
import { isAddress } from 'web3-utils';
import { inject as service } from '@ember/service';
import config from 'kredits-web/config/environment';
export default Controller.extend({
kredits: service(),
ethAddress: null,
githubAccessToken: alias('kredits.githubAccessToken'),
isValidEthAccount: computed('ethAddress', function() {
return isAddress(this.ethAddress);
}),
signupButtonDisabled: not('isValidEthAccount'),
actions: {
completeSignup () {
const payload = {
accessToken: this.githubAccessToken,
account: this.ethAddress
}
fetch(config.githubSignupUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(res => res.json())
.then(data => {
console.log('Created contributor:', data);
this.setProperties({
githubAccessToken: null,
ethAddress: null
});
this.transitionToRoute('signup.complete');
});
}
}
});
-14
View File
@@ -1,14 +0,0 @@
import Controller from '@ember/controller';
import config from 'kredits-web/config/environment';
export default Controller.extend({
actions: {
connectGithub () {
window.location = config.githubConnectUrl;
}
}
});
-7
View File
@@ -1,7 +0,0 @@
import Helper from '@ember/component/helper';
export default Helper.extend({
compute([string]) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
});
-56
View File
@@ -1,56 +0,0 @@
import Helper from '@ember/component/helper';
import { inject as service } from '@ember/service';
import { alias } from '@ember/object/computed';
import { once } from '@ember/runloop';
export default Helper.extend({
kredits: service(),
currentBlock: alias('kredits.currentBlock'),
compute([contribution]) {
this.setupRecompute(contribution);
let status = [];
if (contribution.vetoed) {
status.push('vetoed');
} else if (contribution.confirmedAt > this.currentBlock) {
status.push('unconfirmed');
} else {
status.push('confirmed');
}
if (contribution.hasPendingChanges) {
status.push('pending');
}
return status.join(' ');
},
destroy () {
if (this.teardown) this.teardown();
this._super(...arguments);
},
setupRecompute (contribution) {
if (this.teardown) this.teardown();
contribution.addObserver('vetoed' , this, this.triggerRecompute);
contribution.addObserver('confirmedAt' , this, this.triggerRecompute);
contribution.addObserver('currentBlock' , this, this.triggerRecompute);
contribution.addObserver('hasPendingChanges' , this, this.triggerRecompute);
this.teardown = () => {
contribution.removeObserver('vetoed', this, this.triggerRecompute);
contribution.removeObserver('confirmedAt', this, this.triggerRecompute);
contribution.removeObserver('currentBlock', this, this.triggerRecompute);
contribution.removeObserver('hasPendingChanges', this, this.triggerRecompute);
};
},
triggerRecompute () {
once(this, this.recompute);
}
});
-15
View File
@@ -1,15 +0,0 @@
import Helper from '@ember/component/helper';
import { inject as service } from '@ember/service';
import { alias } from '@ember/object/computed';
export default Helper.extend({
kredits: service(),
currentBlock: alias('kredits.currentBlock'),
compute([contribution]) {
return !contribution.vetoed &&
(contribution.confirmedAt <= this.currentBlock);
}
});
-16
View File
@@ -1,16 +0,0 @@
import Helper from '@ember/component/helper';
import { inject as service } from '@ember/service';
import { alias } from '@ember/object/computed';
import { isPresent } from '@ember/utils';
export default Helper.extend({
kredits: service(),
currentUser: alias('kredits.currentUser'),
compute([contributor]) {
return isPresent(contributor) && isPresent(this.currentUser) &&
contributor.account === this.currentUser.account;
}
});
-46
View File
@@ -1,46 +0,0 @@
import EmberObject, { computed } from '@ember/object';
import { isEmpty, isPresent } from '@ember/utils';
import bignumber from 'kredits-web/utils/cps/bignumber';
import moment from 'moment';
export default EmberObject.extend({
// Contract
id: null,
contributorId: null,
amount: null,
confirmedAt: bignumber('confirmedAtBlock', 'toNumber'),
vetoed: null,
ipfsHash: null,
creatorAccount: null,
// IPFS
kind: null,
description: null,
details: null,
url: null,
date: null,
time: null,
ipfsData: '',
pendingTx: null,
init () {
this._super(...arguments);
if (isEmpty(this.details)) this.set('details', {});
},
iso8601Date: computed('date', 'time', function() {
return this.time ? `${this.date}T${this.time}` : this.date;
}),
jsDate: computed('iso8601Date', function() {
return moment(this.iso8601Date).toDate();
}),
hasPendingChanges: computed('pendingTx', function() {
return isPresent(this.pendingTx);
})
});
+12 -6
View File
@@ -1,14 +1,12 @@
import { computed } from '@ember/object';
import EmberObject from '@ember/object'; import EmberObject from '@ember/object';
import bignumber from 'kredits-web/utils/cps/bignumber'; import bignumber from 'kredits-web/utils/cps/bignumber';
import kreditsValue from 'kredits-web/utils/cps/kredits';
export default EmberObject.extend({ export default EmberObject.extend({
// Contract // Contract
id: bignumber('idRaw', 'toString'), id: bignumber('idRaw', 'toString'),
account: null, account: null,
balance: kreditsValue('balanceRaw'), balance: bignumber('balanceRaw', 'toNumber'),
totalKreditsEarned: bignumber('totalKreditsEarnedRaw', 'toNumber'),
contributionsCount: bignumber('contributionsCountRaw', 'toNumber'),
isCore: false, isCore: false,
ipfsHash: null, ipfsHash: null,
@@ -19,7 +17,15 @@ export default EmberObject.extend({
github_username: null, github_username: null,
github_uid: null, github_uid: null,
wiki_username: null, wiki_username: null,
zoom_display_name: null, ipfsData: '',
ipfsData: ''
// Deprecated
isCurrentUser: false,
avatarURL: computed('github_uid', function() {
let github_uid = this.github_uid;
if (github_uid) {
return `https://avatars2.githubusercontent.com/u/${github_uid}?v=3&s=128`;
}
}),
}); });
+25
View File
@@ -0,0 +1,25 @@
import EmberObject from '@ember/object';
import { alias } from '@ember/object/computed';
import bignumber from 'kredits-web/utils/cps/bignumber';
export default EmberObject.extend({
// Contract
id: bignumber('idRaw', 'toString'),
creatorAccount: null,
contributorId: bignumber('contributorIdRaw', 'toString'),
amount: bignumber('amountRaw', 'toNumber'),
votesCount: bignumber('votesCountRaw', 'toNumber'),
votesNeeded: bignumber('votesNeededRaw', 'toNumber'),
executed: null,
ipfsHash: null,
// Shortcuts
isExecuted: alias('executed'),
// IPFS
kind: null,
description: null,
details: {},
url: null,
ipfsData: '',
});
+3
View File
@@ -0,0 +1,3 @@
import Resolver from 'ember-resolver';
export default Resolver;
+8 -24
View File
@@ -1,32 +1,16 @@
import EmberRouter from '@ember/routing/router'; import EmberRouter from '@ember/routing/router';
import config from './config/environment'; import config from './config/environment';
export default class Router extends EmberRouter { const Router = EmberRouter.extend({
location = config.locationType; location: config.locationType,
rootURL = config.rootURL; rootURL: config.rootURL
} });
Router.map(function() { Router.map(function() {
this.route('dashboard', function() { this.route('spinner');
this.route('contributors', function() { this.route('proposals', function() {
this.route('show', { path: ':id' });
});
this.route('contributions', function() {
this.route('show', { path: ':id' });
});
});
this.route('contributions', function() {
this.route('new', { queryParams: ['contributorId', 'kind', 'amount'] });
this.route('resubmit', { path: ':id/resubmit' });
});
this.route('contributors', function() {
this.route('new'); this.route('new');
this.route('edit', { path: ':id/edit' });
});
this.route('signup', function() {
this.route('github');
this.route('eth-account');
this.route('complete');
}); });
}); });
export default Router;
+9 -8
View File
@@ -1,24 +1,25 @@
import { inject as service } from '@ember/service'; import { inject as injectService } from '@ember/service';
import Route from '@ember/routing/route'; import Route from '@ember/routing/route';
export default Route.extend({ export default Route.extend({
kredits: service(), kredits: injectService(),
beforeModel(/* transition */) { beforeModel(transition) {
const kredits = this.kredits; const kredits = this.kredits;
return kredits.setup().then(() => { return kredits.setup().then(() => {
kredits.get('kredits').preflightChecks().catch((error) => { if (kredits.get('accountNeedsUnlock')) {
console.error('Kredits preflight check failed!'); if (confirm('It looks like you have an Ethereum wallet available. Please unlock your account.')) {
console.error(error); transition.retry();
}); }
}
}).catch((error) => { }).catch((error) => {
console.log('Error initializing Kredits', error); console.log('Error initializing Kredits', error);
}); });
}, },
afterModel() { afterModel() {
return this.kredits.loadInitialData() return this.kredits.loadContributorsAndProposals()
.then(() => { .then(() => {
this.kredits.addContractEventHandlers(); this.kredits.addContractEventHandlers();
}); });
-9
View File
@@ -1,9 +0,0 @@
import Route from '@ember/routing/route';
export default Route.extend({
model (params) {
return { params };
}
});
-25
View File
@@ -1,25 +0,0 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default Route.extend({
kredits: service(),
model(params) {
const contribution = this.kredits.contributions.findBy('id', parseInt(params.id));
contribution.contributorId = contribution.contributorId.toString();
return contribution;
},
setupController (controller, model) {
this._super(controller, model);
controller.set('attributes', model.getProperties([
'kind', 'amount', 'description', 'url', 'details'
]));
controller.set('attributes.contributorId', model.contributorId.toString());
controller.set('attributes.date', model.jsDate);
}
});
-30
View File
@@ -1,30 +0,0 @@
import { inject as service } from '@ember/service';
import Route from '@ember/routing/route';
import { alias } from '@ember/object/computed';
export default Route.extend({
kredits: service(),
contributors: alias('kredits.contributors'),
model(params) {
return this.kredits.contributors.findBy('id', params.id);
},
setupController (controller, model) {
this._super(controller, model);
controller.set('attributes', {
account: model.account,
name: model.name,
kind: model.kind,
url: model.url,
github_username: model.github_username,
github_uid: model.github_uid,
gitea_username: model.gitea_username,
wiki_username: model.wiki_username,
zoom_display_name: model.zoom_display_name
});
}
});
@@ -1,32 +0,0 @@
import { inject as service } from '@ember/service';
import Route from '@ember/routing/route';
import { alias } from '@ember/object/computed';
export default Route.extend({
kredits: service(),
contributions: alias('kredits.contributions'),
model (params) {
return this.contributions.findBy('id', parseInt(params.id));
},
setupController (controller, model) {
this._super(controller, model);
this.controllerFor('dashboard')
.setProperties({
showDetailsPane: true,
selectedContributionId: model.id
});
},
deactivate () {
this.controllerFor('dashboard')
.setProperties({
showDetailsPane: false,
selectedContributionId: null
});
}
});
-32
View File
@@ -1,32 +0,0 @@
import { inject as service } from '@ember/service';
import Route from '@ember/routing/route';
import { alias } from '@ember/object/computed';
export default Route.extend({
kredits: service(),
contributors: alias('kredits.contributors'),
model (params) {
return this.contributors.findBy('id', params.id);
},
setupController (controller, model) {
this._super(controller, model);
this.controllerFor('dashboard')
.setProperties({
showDetailsPane: true,
selectedContributorId: model.id
});
},
deactivate () {
this.controllerFor('dashboard')
.setProperties({
showDetailsPane: false,
selectedContributorId: null
});
}
});
-9
View File
@@ -1,9 +0,0 @@
import Route from '@ember/routing/route';
export default Route.extend({
redirect () {
this.transitionTo('dashboard');
}
});
-4
View File
@@ -1,4 +0,0 @@
import Route from '@ember/routing/route';
export default Route.extend({
});
-17
View File
@@ -1,17 +0,0 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { isEmpty } from '@ember/utils';
export default Route.extend({
kredits: service(),
redirect () {
this._super(...arguments);
if (isEmpty(this.kredits.githubAccessToken)) {
this.transitionTo('signup.index');
}
}
});
-29
View File
@@ -1,29 +0,0 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { isEmpty } from '@ember/utils';
export default Route.extend({
kredits: service(),
redirect () {
this._super(...arguments);
let accessToken;
try {
accessToken = window.location.hash.match(/access_token=(.+)/)[1];
} catch (error) { /* ignore */ }
if (isEmpty(accessToken) || accessToken === 'undefined') {
console.error('No GitHub access token found.');
this.transitionTo('signup');
return;
}
this.kredits.set('githubAccessToken', accessToken);
this.transitionTo('signup.eth-account');
}
});
+105 -225
View File
@@ -1,263 +1,147 @@
import ethers from 'ethers'; import ethers from 'npm:ethers';
import Kredits from 'kredits-contracts'; import Kredits from 'npm:kredits-contracts';
import RSVP from 'rsvp'; import RSVP from 'rsvp';
import Service from '@ember/service'; import Service from '@ember/service';
import EmberObject from '@ember/object';
import { computed } from '@ember/object'; import { computed } from '@ember/object';
import { alias, notEmpty } from '@ember/object/computed'; import { alias, notEmpty } from '@ember/object/computed';
import { isEmpty, isPresent } from '@ember/utils'; import { isEmpty } from '@ember/utils';
import groupBy from 'kredits-web/utils/group-by';
import formatKredits from 'kredits-web/utils/format-kredits';
import config from 'kredits-web/config/environment'; import config from 'kredits-web/config/environment';
import Contributor from 'kredits-web/models/contributor' import Contributor from 'kredits-web/models/contributor'
import Contribution from 'kredits-web/models/contribution' import Proposal from 'kredits-web/models/proposal'
export default Service.extend({ export default Service.extend({
currentBlock: null,
currentUserAccounts: null, // default to not having an account. this is the wen web3 is loaded. currentUserAccounts: null, // default to not having an account. this is the wen web3 is loaded.
currentUser: null, currentUser: null,
contributors: null,
contributions: null,
githubAccessToken: null,
currentUserIsContributor: notEmpty('currentUser'), currentUserIsContributor: notEmpty('currentUser'),
currentUserIsCore: alias('currentUser.isCore'), currentUserIsCore: alias('currentUser.isCore'),
hasAccounts: notEmpty('currentUserAccounts'), hasAccounts: notEmpty('currentUserAccounts'),
accountNeedsUnlock: computed('currentUserAccounts', function() {
contributionsUnconfirmed: computed('contributions.[]', 'currentBlock', function() { return this.currentUserAccounts && isEmpty(this.currentUserAccounts);
return this.contributions.filter(contribution => {
return contribution.confirmedAt > this.currentBlock;
});
}), }),
contributionsConfirmed: computed('contributions.[]', 'currentBlock', function() { // this is called in the routes beforeModel(). So it is initialized before everything else
return this.contributions // and we can rely on the ethProvider and the potential currentUserAccounts to be available
.filterBy('vetoed', false) getEthProvider: function() {
.filter(contribution => { return new RSVP.Promise((resolve) => {
return contribution.confirmedAt <= this.currentBlock; let ethProvider;
}); let networkId;
}), let networkName;
if (typeof window.web3 !== 'undefined') {
kreditsByContributor: computed('contributionsUnconfirmed.@each.vetoed', 'contributors.[]', function() {
const contributionsUnconfirmed = this.contributionsUnconfirmed.filterBy('vetoed', false);
const contributionsGrouped = groupBy(contributionsUnconfirmed, 'contributorId');
const contributorsWithUnconfirmed = contributionsGrouped.map(c => c.value.toString());
const contributorsWithOnlyConfirmed = this.contributors.reject(c => contributorsWithUnconfirmed.includes(c.id))
const kreditsByContributor = contributionsGrouped.map(c => {
const amountUnconfirmed = c.items.mapBy('amount').reduce((a, b) => a + b);
const contributor = this.contributors.findBy('id', c.value.toString());
return EmberObject.create({
contributor: contributor,
amountUnconfirmed: amountUnconfirmed,
amountConfirmed: contributor.totalKreditsEarned,
amountTotal: contributor.totalKreditsEarned + amountUnconfirmed
})
});
contributorsWithOnlyConfirmed.forEach(c => {
kreditsByContributor.push(EmberObject.create({
contributor: c,
amountUnconfirmed: 0,
amountConfirmed: c.totalKreditsEarned,
amountTotal: c.totalKreditsEarned
}));
})
return kreditsByContributor;
}),
init () {
this._super(...arguments);
this.set('contributors', []);
this.set('contributions', []);
},
// This is called in the application route's beforeModel(). So it is
// initialized before everything else, and we can rely on the ethProvider and
// the potential currentUserAccounts to be available
getEthProvider () {
let ethProvider;
return new RSVP.Promise(async (resolve) => {
function instantiateWithoutAccount () {
console.debug('[kredits] Creating new instance from npm module class');
console.debug(`[kredits] providerURL: ${config.web3ProviderUrl}`);
ethProvider = new ethers.providers.JsonRpcProvider(config.web3ProviderUrl);
resolve({
ethProvider: ethProvider,
ethSigner: null
});
}
async function instantiateWithAccount (web3Provider, context) {
console.debug('[kredits] Using user-provided instance, e.g. from Mist browser or Metamask'); console.debug('[kredits] Using user-provided instance, e.g. from Mist browser or Metamask');
ethProvider = new ethers.providers.Web3Provider(web3Provider); networkId = parseInt(window.web3.version.network);
ethProvider = new ethers.providers.Web3Provider(
const network = await ethProvider.getNetwork(); window.web3.currentProvider,
if (isPresent(config.web3RequiredNetwork) && { chainId: networkId }
network.name !== config.web3RequiredNetwork) { );
window.alert(`Please switch your Ethereum wallet to the "${config.web3RequiredNetwork}" network before connecting your account.`); ethProvider.listAccounts().then((accounts) => {
return instantiateWithoutAccount(); this.set('currentUserAccounts', accounts);
} resolve(ethProvider, ethProvider.getSigner());
ethProvider.listAccounts().then(accounts => {
context.set('currentUserAccounts', accounts);
const ethSigner = accounts.length === 0 ? null : ethProvider.getSigner();
resolve({
ethProvider,
ethSigner
});
}); });
} } else {
console.debug('[kredits] Creating new instance from npm module class');
if (window.ethereum) { networkId = parseInt(config.contractMetadata.networkId);
if (window.ethereum.isConnected()) { networkName = config.contractMetadata.networkName;
instantiateWithAccount(window.ethereum, this); console.log(networkName);
} else { ethProvider = new ethers.providers.InfuraProvider({ chainId: networkId, name: networkName });
instantiateWithoutAccount(); resolve(ethProvider, null);
}
}
// Legacy dapp browsers...
else if (window.web3) {
instantiateWithAccount(window.web3.currentProvider, this);
}
// Non-dapp browsers...
else {
instantiateWithoutAccount();
} }
}); });
}, },
setup () { setup() {
return this.getEthProvider().then((providerAndSigner) => { return this.getEthProvider().then((ethProvider, ethSigner) => {
let kredits = new Kredits(providerAndSigner.ethProvider, providerAndSigner.ethSigner, { let kredits = new Kredits(ethProvider, ethSigner);
addresses: { Kernel: config.kreditsKernelAddress },
apm: config.kreditsApmDomain,
ipfsConfig: config.ipfs
});
return kredits return kredits
.init() .init()
.then(async (kredits) => { .then((kredits) => {
this.set('kredits', kredits); this.set('kredits', kredits);
this.set('currentBlock', await kredits.provider.getBlockNumber());
if (this.currentUserAccounts && this.currentUserAccounts.length > 0) { if (this.currentUserAccounts && this.currentUserAccounts.length > 0) {
this.getCurrentUser.then((contributorData) => { this.getCurrentUser.then((contributorData) => {
this.set('currentUser', contributorData); this.set('currentUser', contributorData);
}); });
} }
return kredits; return kredits;
}); });
}); });
}, },
totalSupply: computed(function() { totalSupply: computed(function() {
return this.kredits.Token.functions.totalSupply().then(total => { return this.kredits.Token.functions.totalSupply();
return formatKredits(total);
})
}), }),
totalKreditsEarned: computed(function() { contributors: [],
return this.kredits.Contribution.functions.totalKreditsEarned(true) proposals: [],
.then(total => total.toNumber());
}),
loadContributorsAndProposals() {
loadInitialData () {
return this.getContributors() return this.getContributors()
.then(contributors => this.contributors.pushObjects(contributors)) .then(contributors => this.contributors.pushObjects(contributors))
.then(() => this.getContributions()) .then(() => this.getProposals())
.then(contributions => this.contributions.pushObjects(contributions)) .then(proposals => this.proposals.pushObjects(proposals))
}, },
addContributor (attributes) { addContributor(attributes) {
if (attributes.github_uid) {
const uidInt = parseInt(attributes.github_uid);
attributes.github_uid = uidInt;
}
console.debug('[kredits] add contributor', attributes); console.debug('[kredits] add contributor', attributes);
return this.kredits.Contributor.add(attributes, { gasLimit: 350000 }) return this.kredits.Contributor.add(attributes)
.then(data => { .then((data) => {
console.debug('[kredits] add contributor response', data); console.debug('[kredits] add contributor response', data);
return Contributor.create(attributes);
}); });
}, },
updateContributor (id, attributes) { getContributors() {
if (attributes.github_uid) {
const uidInt = parseInt(attributes.github_uid);
attributes.github_uid = uidInt;
}
console.debug('[kredits] update contributor', attributes);
return this.kredits.Contributor.updateProfile(id, attributes, { gasLimit: 350000 })
.then(data => {
console.debug('[kredits] updateProfile response', data);
});
},
getContributors () {
return this.kredits.Contributor.all() return this.kredits.Contributor.all()
.then(contributors => { .then((contributors) => {
return contributors.map(contributor => { return contributors.map((contributor) => {
return Contributor.create(contributor); return Contributor.create(contributor);
}); });
}); });
}, },
addContribution (attributes) { addProposal(attributes) {
console.debug('[kredits] add contribution', attributes); console.debug('[kredits] add proposal', attributes);
return this.kredits.Contribution.addContribution(attributes, { gasLimit: 300000 }) return this.kredits.Operator.addProposal(attributes)
.then(data => { .then((data) => {
console.debug('[kredits] add contribution response', data); console.debug('[kredits] add proposal response', data);
attributes.contributor = this.contributors.findBy('id', attributes.contributorId); attributes.contributor = this.contributors.findBy('id', attributes.contributorId);
const contribution = Contribution.create(attributes); return Proposal.create(attributes);
contribution.set('pendingTx', data);
contribution.set('confirmedAtBlock', data.blockNumber + 40320);
this.contributions.pushObject(contribution);
return contribution;
}); });
}, },
getContributions () { getProposals() {
return this.kredits.Contribution.all({page: {size: 200}}) return this.kredits.Operator.all()
.then(contributions => { .then((proposals) => {
return contributions.map(contribution => { return proposals.map((proposal) => {
contribution.contributor = this.contributors.findBy('id', contribution.contributorId.toString()); proposal.contributor = this.contributors.findBy('id', proposal.contributorId.toString());
return Contribution.create(contribution); return Proposal.create(proposal);
}); });
}); });
}, },
veto (contributionId) { vote(proposalId) {
console.debug('[kredits] veto against', contributionId); console.debug('[kredits] vote for', proposalId);
const contribution = this.contributions.findBy('id', contributionId);
return this.kredits.Contribution.functions.veto(contributionId, { gasLimit: 300000 }) return this.kredits.Operator.functions.vote(proposalId)
.then(data => { .then((data) => {
console.debug('[kredits] veto response', data); console.debug('[kredits] vote response', data);
contribution.set('pendingTx', data);
return data; return data;
}); });
}, },
getCurrentUser: computed('kredits.provider', 'currentUserAccounts.[]', function() { getCurrentUser: computed('kredits.provider', function() {
if (isEmpty(this.currentUserAccounts)) { if (isEmpty(this.currentUserAccounts)) {
return RSVP.resolve(); return RSVP.resolve();
} }
return this.kredits.Contributor return this.kredits.Contributor
.functions.getContributorIdByAddress(this.currentUserAccounts.firstObject) .functions.getContributorIdByAddress(this.get('currentUserAccounts.firstObject'))
.then((id) => { .then((id) => {
id = id.toNumber();
// check if the user is a contributor or not // check if the user is a contributor or not
if (id === 0) { if (id === 0) {
return RSVP.resolve(); return RSVP.resolve();
@@ -267,67 +151,63 @@ export default Service.extend({
}); });
}), }),
findProposalById(proposalId) {
return this.proposals.findBy('id', proposalId.toString());
},
// Contract events // Contract events
addContractEventHandlers () { addContractEventHandlers() {
this.kredits.Contributor // Operator events
.on('ContributorProfileUpdated', this.handleContributorChange.bind(this)) this.kredits.Operator
.on('ContributorAccountUpdated', this.handleContributorChange.bind(this)) .on('ProposalCreated', this.handleProposalCreated.bind(this))
.on('ContributorAdded', this.handleContributorChange.bind(this)) .on('ProposalVoted', this.handleProposalVoted.bind(this))
.on('ProposalExecuted', this.handleProposalExecuted.bind(this));
this.kredits.Contribution
.on('ContributionAdded', this.handleContributionAdded.bind(this))
.on('ContributionVetoed', this.handleContributionVetoed.bind(this))
// Token events
this.kredits.Token this.kredits.Token
.on('Transfer', this.handleTransfer.bind(this)); .on('Transfer', this.handleTransfer.bind(this));
}, },
async handleContributorChange (contributorId, ...args) { handleProposalCreated(proposalId) {
console.debug('[kredits] Contributor add/update event received for ID', contributorId); let proposal = this.findProposalById(proposalId);
console.debug('[kredits] Event data:', args);
const contributorData = await this.kredits.Contributor.getById(contributorId);
const newContributor = Contributor.create(contributorData);
const oldContributor = this.contributors.findBy('id', contributorId.toString()); if (proposal) {
if (oldContributor) { console.debug('[events] proposal exists, not adding from event');
console.debug('[kredits] old contributor', oldContributor); return;
this.contributors.removeObject(oldContributor);
} }
console.debug('[kredits] new contributor', newContributor); this.kredits.Operator.getById(proposalId)
this.contributors.pushObject(newContributor); .then((proposal) => {
proposal.contributor = this.contributors.findBy('id', proposal.contributorId.toString());
this.proposals.pushObject(Proposal.create(proposal));
});
}, },
async handleContributionAdded (id, contributorId, amount) { // TODO: We may want to reload that proposal to show the voter as voted
console.debug('[kredits] ContributionAdded event received', { id, contributorId, amount }); handleProposalVoted(proposalId, voterId, totalVotes) {
let proposal = this.findProposalById(proposalId);
const pendingContribution = this.contributions.find(c => { if (proposal) {
return (c.id === null) && proposal.set('votesCount', totalVotes);
(c.contributorId.toString() === contributorId.toString()) &&
(c.amount.toString() === amount.toString());
});
if (pendingContribution) {
const attributes = await this.kredits.Contribution.getById(id);
attributes.contributor = this.contributors.findBy('id', attributes.contributorId.toString());
const newContribution = Contribution.create(attributes);
this.contributions.addObject(newContribution);
this.contributions.removeObject(pendingContribution);
} }
}, },
handleContributionVetoed (contributionId) { handleProposalExecuted(proposalId, contributorId, amount) {
console.debug('[kredits] ContributionVetoed event received for ', contributionId); let proposal = this.findProposalById(proposalId);
const contribution = this.contributions.findBy('id', contributionId);
console.debug('[kredits] contribution', contribution);
if (contribution) { if (proposal.get('isExecuted')) {
contribution.set('vetoed', true); console.debug('[events] proposal already executed, not adding from event');
contribution.set('pendingTx', null); return;
} }
proposal.set('executed', true);
this.contributors
.findBy('id', contributorId.toString())
.incrementProperty('balance', amount.toNumber());
}, },
handleTransfer (from, to, value) { handleTransfer(from, to, value) {
value = value.toNumber(); value = value.toNumber();
this.contributors this.contributors
-74
View File
@@ -1,74 +0,0 @@
button, input[type=submit], .button {
display: inline-block;
padding: 0.6rem 2rem;
background-color: rgba(22, 21, 40, 0.6);
border: 1px solid rgba(22, 21, 40, 1);
border-radius: 3px;
color: $primary-color;
font-weight: 500;
text-decoration: none;
text-transform: uppercase;
cursor: pointer;
letter-spacing: 0.1em;
-webkit-appearance: none;
&+button, &+input[type=submit], &+.button {
margin-left: 0.5rem;
}
&:hover {
background-color: rgba(22, 21, 40, 0.8);
}
&:active, &.active {
border-color: $primary-color;
}
&[disabled] {
color: rgba(255,255,255,0.5);
}
&.small {
font-size: 0.86rem;
padding: 0.2rem 0.8rem;
}
&.danger:not(:disabled) {
color: $red;
background-color: rgba(40, 21, 21, 0.6);
border-color: rgba(40, 21, 21, 1);
&:hover {
background-color: rgba(40, 21, 21, 0.8);
}
&:active, &.active {
border-color: $red;
}
}
&.green:not(:disabled) {
color: $green;
background-color: rgba(21, 40, 21, 0.6);
border-color: rgba(21, 40, 21, 1);
&:hover {
background-color: rgba(21, 40, 21, 0.8);
}
&:active, &.active {
border-color: $green;
}
}
&.icon {
svg {
width: 2rem;
height: 2rem;
vertical-align: middle;
margin-right: 1rem;
.fg {
fill: $primary-color;
}
}
}
}
+1 -3
View File
@@ -5,6 +5,4 @@ $green: #97fb68;
$yellow: #fbe468; $yellow: #fbe468;
$red: #fb6868; $red: #fb6868;
$primary-color: $blue; $primaryColor: $blue;
$body-text-color: #fff;
+18 -141
View File
@@ -1,154 +1,31 @@
main { section {
padding: 1rem 2rem; @include outer-container;
margin-bottom: 8rem;
@include media-max(small) {
padding: 1rem;
}
&#dashboard {
width: 100%;
display: grid;
grid-row-gap: 2rem;
grid-template-areas:
"stats"
"contributions";
}
&.center-column {
display: flex;
flex-direction: column;
align-items: center;
section {
width: 600px;
max-width: 100%;
header {
text-align: center;
}
}
}
section {
.content {
a {
font-size: inherit;
}
&.text-lg {
p {
font-size: 1.2rem;
margin-bottom: 1em;
line-height: 1.5em;
}
}
&.text-center {
text-align: center;
}
p {
&.mg-bottom-md {
margin-bottom: 2rem;
}
&.actions {
text-align: center;
padding-top: 2rem;
}
}
}
}
}
#intro {
padding: 2rem;
background-color: rgba(0,0,0,.2);
font-size: 1.6rem;
@include media-max(small) {
padding: 2rem 1rem;
font-size: 1.4rem;
}
h2 {
font-size: inherit;
margin-bottom: 1em;
}
p {
margin-bottom: 1em;
line-height: 1.5em;
font-size: 1.2rem;
&:last-child {
margin-bottom: 0;
}
}
a {
text-decoration: none;
&:hover, &:active {
text-decoration: underline;
}
}
}
@media (min-width: 550px) {
main {
&#dashboard {
grid-column-gap: 4rem;
grid-row-gap: 2rem;
grid-template-columns: 2fr 4fr;
grid-template-areas:
"stats contributions";
&.with-details {
grid-column-gap: 3rem;
grid-template-columns: 2fr 4fr 2fr;
grid-template-areas:
"stats contributions details";
}
}
}
}
main section {
margin-bottom: 5rem;
&:first-of-type { &:first-of-type {
margin-top: 2rem; margin-top: 6rem;
} }
@include media-max(small) { @include media($mobile) {
margin-bottom: 5rem; margin-bottom: 5rem;
&:first-of-type {
margin-top: 2rem;
}
} }
header { header {
margin-bottom: 3rem; @include span-columns(12);
padding-bottom: 3rem;
text-align: center;
// background-color: purple;
&.with-nav { @include media($mobile) {
display: grid; @include span-columns(10);
grid-template-columns: auto 5rem; @include shift(1);
grid-template-areas: padding-bottom: 2rem;
"title" "actions";
} }
h2 { div.content {
display: inline-block; @include span-columns(12);
// padding-left: 1.2rem;
}
nav {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
}
@include media-max(small) {
margin-bottom: 2rem;
} }
} }
} }
-33
View File
@@ -1,33 +0,0 @@
$breakpoints-max: (
small: 600px,
medium: 960px,
large: 1280px
);
$breakpoints-min: (
small: 601px,
medium: 961px,
large: 1281px
);
@mixin media-max($screen-size) {
@if map-has-key($breakpoints-max, $screen-size) {
@media (max-width: map-get($breakpoints-max, $screen-size)) {
@content;
}
} @else {
// Debugging
@warn "'#{$screen-size}' has not been declared as a breakpoint."
}
}
@mixin media-min($screen-size) {
@if map-has-key($breakpoints-min, $screen-size) {
@media (min-width: map-get($breakpoints-min, $screen-size)) {
@content;
}
} @else {
// Debugging
@warn "'#{$screen-size}' has not been declared as a breakpoint."
}
}
+46 -40
View File
@@ -10,23 +10,23 @@
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
} }
@import "mediaqueries"; @import "bourbon/bourbon";
@import "settings/neat";
@import "neat/neat";
@import "settings/breakpoints";
@import "layout"; @import "layout";
@import "colors"; @import "colors";
$font-family-sans: 'Open Sans', sans-serif; $font-family-sans: 'Open Sans', sans-serif;
html, body {
font-family: $font-family-sans;
font-weight: 300;
font-size: 12px;
color: $body-text-color;
}
body { body {
background-image: linear-gradient(to bottom, rgba(22, 21, 40, .4), rgba(0, 0, 0, .75)), url('/img/bg.jpg'); background-image: url('/img/bg.jpg');
background-repeat: none; background-repeat: none;
background-attachment: fixed; background-attachment: fixed;
font-family: $font-family-sans;
font-size: 14px;
font-weight: 300;
color: #fff;
} }
@media screen and (min-width: 900px) { @media screen and (min-width: 900px) {
@@ -41,60 +41,66 @@ h1, h2, h3, h4, h5, input, button {
font-weight: 300; font-weight: 300;
} }
a {
color: $primary-color;
}
section { section {
h2 { h2 {
font-size: 1.4rem; font-size: 2.8rem;
color: $primary-color; color: $primaryColor;
@include media($mobile) {
font-size: 2rem;
}
} }
&#people { &#contributors {
.content { .content {
p.stats { p.stats {
margin-bottom: 1rem; padding-top: 3rem;
font-size: 1rem; font-size: 1rem;
color: white; color: white;
text-align: center; text-align: center;
span.number { span.number {
font-weight: 600; font-weight: 600;
} }
@include media($mobile) {
padding-top: 2rem;
}
} }
} }
} }
&#contributions-by-type { &#proposals-open, &#proposals-closed {
.chart { .actions {
width: 50%; padding-top: 3rem;
margin-left: auto; font-size: 1rem;
margin-right: auto; color: $primaryColor;
text-align: center;
@include media($mobile) {
padding-top: 2rem;
}
@include media-max(small) { a {
width: 75%; color: $primaryColor;
} }
} }
} }
} }
@media (min-width: 550px) { button, input[type=submit] {
section { display: inline-block;
h2 { border: 1px solid rgba(22, 21, 40, 1);
font-size: 2rem; background-color: rgba(22, 21, 40, 0.6);
} color: $primaryColor;
border-radius: 3px;
font-weight: 500;
text-transform: uppercase;
cursor: pointer;
letter-spacing: 0.1em;
&:hover {
background-color: rgba(22, 21, 40, 0.8);
} }
} }
@import "buttons";
@import "forms";
@import "components/contribution-list";
@import "components/contribution-details";
@import "components/contributor-list";
@import "components/contributor-profile";
@import "components/external-account-link";
@import "components/loading-spinner"; @import "components/loading-spinner";
@import "components/topbar"; @import "components/contributor-list";
@import "components/topbar-account-panel"; @import "components/add-contributor";
@import "components/user-avatar"; @import "components/proposal-list";
+19
View File
@@ -0,0 +1,19 @@
@charset "UTF-8";
/// Throws Sass warnings to announce library deprecations. You can disable them
/// by setting the `$output-bourbon-deprecation-warnings` variable to `false`.
///
/// @access private
@mixin _bourbon-deprecate($feature, $message: null) {
@if $output-bourbon-deprecation-warnings == true {
@warn "[Bourbon] [Deprecation] `#{$feature}` is deprecated and will be " +
"removed in 5.0.0. #{$message}";
@content;
}
}
@mixin _bourbon-deprecate-for-prefixing($feature) {
@include _bourbon-deprecate($feature, "We suggest using an automated " +
"prefixing tool, like Autoprefixer.");
}
+425
View File
@@ -0,0 +1,425 @@
// The following features have been deprecated and will be removed in the next MAJOR version release
@mixin inline-block {
@include _bourbon-deprecate("inline-block");
display: inline-block;
}
@mixin button ($style: simple, $base-color: #4294f0, $text-size: inherit, $padding: 7px 18px) {
@include _bourbon-deprecate("button");
@if type-of($style) == string and type-of($base-color) == color {
@include buttonstyle($style, $base-color, $text-size, $padding);
}
@if type-of($style) == string and type-of($base-color) == number {
$padding: $text-size;
$text-size: $base-color;
$base-color: #4294f0;
@if $padding == inherit {
$padding: 7px 18px;
}
@include buttonstyle($style, $base-color, $text-size, $padding);
}
@if type-of($style) == color and type-of($base-color) == color {
$base-color: $style;
$style: simple;
@include buttonstyle($style, $base-color, $text-size, $padding);
}
@if type-of($style) == color and type-of($base-color) == number {
$padding: $text-size;
$text-size: $base-color;
$base-color: $style;
$style: simple;
@if $padding == inherit {
$padding: 7px 18px;
}
@include buttonstyle($style, $base-color, $text-size, $padding);
}
@if type-of($style) == number {
$padding: $base-color;
$text-size: $style;
$base-color: #4294f0;
$style: simple;
@if $padding == #4294f0 {
$padding: 7px 18px;
}
@include buttonstyle($style, $base-color, $text-size, $padding);
}
&:disabled {
cursor: not-allowed;
opacity: 0.5;
}
}
// Selector Style Button
@mixin buttonstyle($type, $b-color, $t-size, $pad) {
// Grayscale button
@if $type == simple and $b-color == grayscale($b-color) {
@include simple($b-color, true, $t-size, $pad);
}
@if $type == shiny and $b-color == grayscale($b-color) {
@include shiny($b-color, true, $t-size, $pad);
}
@if $type == pill and $b-color == grayscale($b-color) {
@include pill($b-color, true, $t-size, $pad);
}
@if $type == flat and $b-color == grayscale($b-color) {
@include flat($b-color, true, $t-size, $pad);
}
// Colored button
@if $type == simple {
@include simple($b-color, false, $t-size, $pad);
}
@else if $type == shiny {
@include shiny($b-color, false, $t-size, $pad);
}
@else if $type == pill {
@include pill($b-color, false, $t-size, $pad);
}
@else if $type == flat {
@include flat($b-color, false, $t-size, $pad);
}
}
// Simple Button
@mixin simple($base-color, $grayscale: false, $textsize: inherit, $padding: 7px 18px) {
$color: hsl(0, 0, 100%);
$border: adjust-color($base-color, $saturation: 9%, $lightness: -14%);
$inset-shadow: adjust-color($base-color, $saturation: -8%, $lightness: 15%);
$stop-gradient: adjust-color($base-color, $saturation: 9%, $lightness: -11%);
$text-shadow: adjust-color($base-color, $saturation: 15%, $lightness: -18%);
@if is-light($base-color) {
$color: hsl(0, 0, 20%);
$text-shadow: adjust-color($base-color, $saturation: 10%, $lightness: 4%);
}
@if $grayscale == true {
$border: grayscale($border);
$inset-shadow: grayscale($inset-shadow);
$stop-gradient: grayscale($stop-gradient);
$text-shadow: grayscale($text-shadow);
}
border: 1px solid $border;
border-radius: 3px;
box-shadow: inset 0 1px 0 0 $inset-shadow;
color: $color;
display: inline-block;
font-size: $textsize;
font-weight: bold;
@include linear-gradient ($base-color, $stop-gradient);
padding: $padding;
text-decoration: none;
text-shadow: 0 1px 0 $text-shadow;
background-clip: padding-box;
&:hover:not(:disabled) {
$base-color-hover: adjust-color($base-color, $saturation: -4%, $lightness: -5%);
$inset-shadow-hover: adjust-color($base-color, $saturation: -7%, $lightness: 5%);
$stop-gradient-hover: adjust-color($base-color, $saturation: 8%, $lightness: -14%);
@if $grayscale == true {
$base-color-hover: grayscale($base-color-hover);
$inset-shadow-hover: grayscale($inset-shadow-hover);
$stop-gradient-hover: grayscale($stop-gradient-hover);
}
@include linear-gradient ($base-color-hover, $stop-gradient-hover);
box-shadow: inset 0 1px 0 0 $inset-shadow-hover;
cursor: pointer;
}
&:active:not(:disabled),
&:focus:not(:disabled) {
$border-active: adjust-color($base-color, $saturation: 9%, $lightness: -14%);
$inset-shadow-active: adjust-color($base-color, $saturation: 7%, $lightness: -17%);
@if $grayscale == true {
$border-active: grayscale($border-active);
$inset-shadow-active: grayscale($inset-shadow-active);
}
border: 1px solid $border-active;
box-shadow: inset 0 0 8px 4px $inset-shadow-active, inset 0 0 8px 4px $inset-shadow-active;
}
}
// Shiny Button
@mixin shiny($base-color, $grayscale: false, $textsize: inherit, $padding: 7px 18px) {
$color: hsl(0, 0, 100%);
$border: adjust-color($base-color, $red: -117, $green: -111, $blue: -81);
$border-bottom: adjust-color($base-color, $red: -126, $green: -127, $blue: -122);
$fourth-stop: adjust-color($base-color, $red: -79, $green: -70, $blue: -46);
$inset-shadow: adjust-color($base-color, $red: 37, $green: 29, $blue: 12);
$second-stop: adjust-color($base-color, $red: -56, $green: -50, $blue: -33);
$text-shadow: adjust-color($base-color, $red: -140, $green: -141, $blue: -114);
$third-stop: adjust-color($base-color, $red: -86, $green: -75, $blue: -48);
@if is-light($base-color) {
$color: hsl(0, 0, 20%);
$text-shadow: adjust-color($base-color, $saturation: 10%, $lightness: 4%);
}
@if $grayscale == true {
$border: grayscale($border);
$border-bottom: grayscale($border-bottom);
$fourth-stop: grayscale($fourth-stop);
$inset-shadow: grayscale($inset-shadow);
$second-stop: grayscale($second-stop);
$text-shadow: grayscale($text-shadow);
$third-stop: grayscale($third-stop);
}
@include linear-gradient(top, $base-color 0%, $second-stop 50%, $third-stop 50%, $fourth-stop 100%);
border: 1px solid $border;
border-bottom: 1px solid $border-bottom;
border-radius: 5px;
box-shadow: inset 0 1px 0 0 $inset-shadow;
color: $color;
display: inline-block;
font-size: $textsize;
font-weight: bold;
padding: $padding;
text-align: center;
text-decoration: none;
text-shadow: 0 -1px 1px $text-shadow;
&:hover:not(:disabled) {
$first-stop-hover: adjust-color($base-color, $red: -13, $green: -15, $blue: -18);
$second-stop-hover: adjust-color($base-color, $red: -66, $green: -62, $blue: -51);
$third-stop-hover: adjust-color($base-color, $red: -93, $green: -85, $blue: -66);
$fourth-stop-hover: adjust-color($base-color, $red: -86, $green: -80, $blue: -63);
@if $grayscale == true {
$first-stop-hover: grayscale($first-stop-hover);
$second-stop-hover: grayscale($second-stop-hover);
$third-stop-hover: grayscale($third-stop-hover);
$fourth-stop-hover: grayscale($fourth-stop-hover);
}
@include linear-gradient(top, $first-stop-hover 0%,
$second-stop-hover 50%,
$third-stop-hover 50%,
$fourth-stop-hover 100%);
cursor: pointer;
}
&:active:not(:disabled),
&:focus:not(:disabled) {
$inset-shadow-active: adjust-color($base-color, $red: -111, $green: -116, $blue: -122);
@if $grayscale == true {
$inset-shadow-active: grayscale($inset-shadow-active);
}
box-shadow: inset 0 0 20px 0 $inset-shadow-active;
}
}
// Pill Button
@mixin pill($base-color, $grayscale: false, $textsize: inherit, $padding: 7px 18px) {
$color: hsl(0, 0, 100%);
$border-bottom: adjust-color($base-color, $hue: 8, $saturation: -11%, $lightness: -26%);
$border-sides: adjust-color($base-color, $hue: 4, $saturation: -21%, $lightness: -21%);
$border-top: adjust-color($base-color, $hue: -1, $saturation: -30%, $lightness: -15%);
$inset-shadow: adjust-color($base-color, $hue: -1, $saturation: -1%, $lightness: 7%);
$stop-gradient: adjust-color($base-color, $hue: 8, $saturation: 14%, $lightness: -10%);
$text-shadow: adjust-color($base-color, $hue: 5, $saturation: -19%, $lightness: -15%);
@if is-light($base-color) {
$color: hsl(0, 0, 20%);
$text-shadow: adjust-color($base-color, $saturation: 10%, $lightness: 4%);
}
@if $grayscale == true {
$border-bottom: grayscale($border-bottom);
$border-sides: grayscale($border-sides);
$border-top: grayscale($border-top);
$inset-shadow: grayscale($inset-shadow);
$stop-gradient: grayscale($stop-gradient);
$text-shadow: grayscale($text-shadow);
}
border: 1px solid $border-top;
border-color: $border-top $border-sides $border-bottom;
border-radius: 16px;
box-shadow: inset 0 1px 0 0 $inset-shadow;
color: $color;
display: inline-block;
font-size: $textsize;
font-weight: normal;
line-height: 1;
@include linear-gradient ($base-color, $stop-gradient);
padding: $padding;
text-align: center;
text-decoration: none;
text-shadow: 0 -1px 1px $text-shadow;
background-clip: padding-box;
&:hover:not(:disabled) {
$base-color-hover: adjust-color($base-color, $lightness: -4.5%);
$border-bottom: adjust-color($base-color, $hue: 8, $saturation: 13.5%, $lightness: -32%);
$border-sides: adjust-color($base-color, $hue: 4, $saturation: -2%, $lightness: -27%);
$border-top: adjust-color($base-color, $hue: -1, $saturation: -17%, $lightness: -21%);
$inset-shadow-hover: adjust-color($base-color, $saturation: -1%, $lightness: 3%);
$stop-gradient-hover: adjust-color($base-color, $hue: 8, $saturation: -4%, $lightness: -15.5%);
$text-shadow-hover: adjust-color($base-color, $hue: 5, $saturation: -5%, $lightness: -22%);
@if $grayscale == true {
$base-color-hover: grayscale($base-color-hover);
$border-bottom: grayscale($border-bottom);
$border-sides: grayscale($border-sides);
$border-top: grayscale($border-top);
$inset-shadow-hover: grayscale($inset-shadow-hover);
$stop-gradient-hover: grayscale($stop-gradient-hover);
$text-shadow-hover: grayscale($text-shadow-hover);
}
@include linear-gradient ($base-color-hover, $stop-gradient-hover);
background-clip: padding-box;
border: 1px solid $border-top;
border-color: $border-top $border-sides $border-bottom;
box-shadow: inset 0 1px 0 0 $inset-shadow-hover;
cursor: pointer;
text-shadow: 0 -1px 1px $text-shadow-hover;
}
&:active:not(:disabled),
&:focus:not(:disabled) {
$active-color: adjust-color($base-color, $hue: 4, $saturation: -12%, $lightness: -10%);
$border-active: adjust-color($base-color, $hue: 6, $saturation: -2.5%, $lightness: -30%);
$border-bottom-active: adjust-color($base-color, $hue: 11, $saturation: 6%, $lightness: -31%);
$inset-shadow-active: adjust-color($base-color, $hue: 9, $saturation: 2%, $lightness: -21.5%);
$text-shadow-active: adjust-color($base-color, $hue: 5, $saturation: -12%, $lightness: -21.5%);
@if $grayscale == true {
$active-color: grayscale($active-color);
$border-active: grayscale($border-active);
$border-bottom-active: grayscale($border-bottom-active);
$inset-shadow-active: grayscale($inset-shadow-active);
$text-shadow-active: grayscale($text-shadow-active);
}
background: $active-color;
border: 1px solid $border-active;
border-bottom: 1px solid $border-bottom-active;
box-shadow: inset 0 0 6px 3px $inset-shadow-active;
text-shadow: 0 -1px 1px $text-shadow-active;
}
}
// Flat Button
@mixin flat($base-color, $grayscale: false, $textsize: inherit, $padding: 7px 18px) {
$color: hsl(0, 0, 100%);
@if is-light($base-color) {
$color: hsl(0, 0, 20%);
}
background-color: $base-color;
border-radius: 3px;
border: 0;
color: $color;
display: inline-block;
font-size: $textsize;
font-weight: bold;
padding: $padding;
text-decoration: none;
background-clip: padding-box;
&:hover:not(:disabled){
$base-color-hover: adjust-color($base-color, $saturation: 4%, $lightness: 5%);
@if $grayscale == true {
$base-color-hover: grayscale($base-color-hover);
}
background-color: $base-color-hover;
cursor: pointer;
}
&:active:not(:disabled),
&:focus:not(:disabled) {
$base-color-active: adjust-color($base-color, $saturation: -4%, $lightness: -5%);
@if $grayscale == true {
$base-color-active: grayscale($base-color-active);
}
background-color: $base-color-active;
cursor: pointer;
}
}
// Flexible grid
@function flex-grid($columns, $container-columns: $fg-max-columns) {
@if $output-bourbon-deprecation-warnings == true {
@warn "[Bourbon] [Deprecation] `flex-grid` is deprecated and will be " +
"removed in 5.0.0. For grid functions, check out Bourbon's sister library" +
"Neat.";
}
$width: $columns * $fg-column + ($columns - 1) * $fg-gutter;
$container-width: $container-columns * $fg-column + ($container-columns - 1) * $fg-gutter;
@return percentage($width / $container-width);
}
// Flexible gutter
@function flex-gutter($container-columns: $fg-max-columns, $gutter: $fg-gutter) {
@if $output-bourbon-deprecation-warnings == true {
@warn "[Bourbon] [Deprecation] `flex-gutter` is deprecated and will be " +
"removed in 5.0.0. For grid functions, check out Bourbon's sister library" +
"Neat.";
}
$container-width: $container-columns * $fg-column + ($container-columns - 1) * $fg-gutter;
@return percentage($gutter / $container-width);
}
@function grid-width($n) {
@if $output-bourbon-deprecation-warnings == true {
@warn "[Bourbon] [Deprecation] `grid-width` is deprecated and will be " +
"removed in 5.0.0. For grid functions, check out Bourbon's sister library" +
"Neat.";
}
@return $n * $gw-column + ($n - 1) * $gw-gutter;
}
@function golden-ratio($value, $increment) {
@if $output-bourbon-deprecation-warnings == true {
@warn "[Bourbon] [Deprecation] `golden-ratio` is deprecated and will be " +
"removed in 5.0.0. You can use the `modular-scale` function instead.";
}
@return modular-scale($increment, $value, $ratio: $golden);
}
@mixin box-sizing($box) {
@include _bourbon-deprecate-for-prefixing("box-sizing");
@include prefixer(box-sizing, $box, webkit moz spec);
}
+90
View File
@@ -0,0 +1,90 @@
// Bourbon 4.3.0
// http://bourbon.io
// Copyright 2011-2017 thoughtbot, inc.
// MIT License
@import "settings/deprecation-warnings";
@import "settings/prefixer";
@import "settings/px-to-em";
@import "settings/asset-pipeline";
@import "bourbon-deprecate";
@import "functions/assign-inputs";
@import "functions/contains";
@import "functions/contains-falsy";
@import "functions/is-length";
@import "functions/is-light";
@import "functions/is-number";
@import "functions/is-size";
@import "functions/px-to-em";
@import "functions/px-to-rem";
@import "functions/shade";
@import "functions/strip-units";
@import "functions/tint";
@import "functions/transition-property-name";
@import "functions/unpack";
@import "functions/modular-scale";
@import "helpers/convert-units";
@import "helpers/directional-values";
@import "helpers/font-source-declaration";
@import "helpers/gradient-positions-parser";
@import "helpers/linear-angle-parser";
@import "helpers/linear-gradient-parser";
@import "helpers/linear-positions-parser";
@import "helpers/linear-side-corner-parser";
@import "helpers/radial-arg-parser";
@import "helpers/radial-positions-parser";
@import "helpers/radial-gradient-parser";
@import "helpers/render-gradients";
@import "helpers/shape-size-stripper";
@import "helpers/str-to-num";
@import "css3/animation";
@import "css3/appearance";
@import "css3/backface-visibility";
@import "css3/background";
@import "css3/background-image";
@import "css3/border-image";
@import "css3/calc";
@import "css3/columns";
@import "css3/filter";
@import "css3/flex-box";
@import "css3/font-face";
@import "css3/font-feature-settings";
@import "css3/hidpi-media-query";
@import "css3/hyphens";
@import "css3/image-rendering";
@import "css3/keyframes";
@import "css3/linear-gradient";
@import "css3/perspective";
@import "css3/placeholder";
@import "css3/radial-gradient";
@import "css3/selection";
@import "css3/text-decoration";
@import "css3/transform";
@import "css3/transition";
@import "css3/user-select";
@import "addons/border-color";
@import "addons/border-radius";
@import "addons/border-style";
@import "addons/border-width";
@import "addons/buttons";
@import "addons/clearfix";
@import "addons/ellipsis";
@import "addons/font-stacks";
@import "addons/hide-text";
@import "addons/margin";
@import "addons/padding";
@import "addons/position";
@import "addons/prefixer";
@import "addons/retina-image";
@import "addons/size";
@import "addons/text-inputs";
@import "addons/timing-functions";
@import "addons/triangle";
@import "addons/word-wrap";
@import "bourbon-deprecated-upcoming";
+26
View File
@@ -0,0 +1,26 @@
@charset "UTF-8";
/// Provides a quick method for targeting `border-color` on specific sides of a box. Use a `null` value to “skip” a side.
///
/// @param {Arglist} $vals
/// List of arguments
///
/// @example scss - Usage
/// .element {
/// @include border-color(#a60b55 #76cd9c null #e8ae1a);
/// }
///
/// @example css - CSS Output
/// .element {
/// border-left-color: #e8ae1a;
/// border-right-color: #76cd9c;
/// border-top-color: #a60b55;
/// }
///
/// @require {mixin} directional-property
///
/// @output `border-color`
@mixin border-color($vals...) {
@include directional-property(border, color, $vals...);
}
+48
View File
@@ -0,0 +1,48 @@
@charset "UTF-8";
/// Provides a quick method for targeting `border-radius` on both corners on the side of a box.
///
/// @param {Number} $radii
/// List of arguments
///
/// @example scss - Usage
/// .element-one {
/// @include border-top-radius(5px);
/// }
///
/// .element-two {
/// @include border-left-radius(3px);
/// }
///
/// @example css - CSS Output
/// .element-one {
/// border-top-left-radius: 5px;
/// border-top-right-radius: 5px;
/// }
///
/// .element-two {
/// border-bottom-left-radius: 3px;
/// border-top-left-radius: 3px;
/// }
///
/// @output `border-radius`
@mixin border-top-radius($radii) {
border-top-left-radius: $radii;
border-top-right-radius: $radii;
}
@mixin border-right-radius($radii) {
border-bottom-right-radius: $radii;
border-top-right-radius: $radii;
}
@mixin border-bottom-radius($radii) {
border-bottom-left-radius: $radii;
border-bottom-right-radius: $radii;
}
@mixin border-left-radius($radii) {
border-bottom-left-radius: $radii;
border-top-left-radius: $radii;
}
+25
View File
@@ -0,0 +1,25 @@
@charset "UTF-8";
/// Provides a quick method for targeting `border-style` on specific sides of a box. Use a `null` value to “skip” a side.
///
/// @param {Arglist} $vals
/// List of arguments
///
/// @example scss - Usage
/// .element {
/// @include border-style(dashed null solid);
/// }
///
/// @example css - CSS Output
/// .element {
/// border-bottom-style: solid;
/// border-top-style: dashed;
/// }
///
/// @require {mixin} directional-property
///
/// @output `border-style`
@mixin border-style($vals...) {
@include directional-property(border, style, $vals...);
}
+25
View File
@@ -0,0 +1,25 @@
@charset "UTF-8";
/// Provides a quick method for targeting `border-width` on specific sides of a box. Use a `null` value to “skip” a side.
///
/// @param {Arglist} $vals
/// List of arguments
///
/// @example scss - Usage
/// .element {
/// @include border-width(1em null 20px);
/// }
///
/// @example css - CSS Output
/// .element {
/// border-bottom-width: 20px;
/// border-top-width: 1em;
/// }
///
/// @require {mixin} directional-property
///
/// @output `border-width`
@mixin border-width($vals...) {
@include directional-property(border, width, $vals...);
}
+64
View File
@@ -0,0 +1,64 @@
@charset "UTF-8";
/// Generates variables for all buttons. Please note that you must use interpolation on the variable: `#{$all-buttons}`.
///
/// @example scss - Usage
/// #{$all-buttons} {
/// background-color: #f00;
/// }
///
/// #{$all-buttons-focus},
/// #{$all-buttons-hover} {
/// background-color: #0f0;
/// }
///
/// #{$all-buttons-active} {
/// background-color: #00f;
/// }
///
/// @example css - CSS Output
/// button,
/// input[type="button"],
/// input[type="reset"],
/// input[type="submit"] {
/// background-color: #f00;
/// }
///
/// button:focus,
/// input[type="button"]:focus,
/// input[type="reset"]:focus,
/// input[type="submit"]:focus,
/// button:hover,
/// input[type="button"]:hover,
/// input[type="reset"]:hover,
/// input[type="submit"]:hover {
/// background-color: #0f0;
/// }
///
/// button:active,
/// input[type="button"]:active,
/// input[type="reset"]:active,
/// input[type="submit"]:active {
/// background-color: #00f;
/// }
///
/// @require assign-inputs
///
/// @type List
///
/// @todo Remove double assigned variables (Lines 5962) in v5.0.0
$buttons-list: 'button',
'input[type="button"]',
'input[type="reset"]',
'input[type="submit"]';
$all-buttons: assign-inputs($buttons-list);
$all-buttons-active: assign-inputs($buttons-list, active);
$all-buttons-focus: assign-inputs($buttons-list, focus);
$all-buttons-hover: assign-inputs($buttons-list, hover);
$all-button-inputs: $all-buttons;
$all-button-inputs-active: $all-buttons-active;
$all-button-inputs-focus: $all-buttons-focus;
$all-button-inputs-hover: $all-buttons-hover;
+25
View File
@@ -0,0 +1,25 @@
@charset "UTF-8";
/// Provides an easy way to include a clearfix for containing floats.
///
/// @link http://cssmojo.com/latest_new_clearfix_so_far/
///
/// @example scss - Usage
/// .element {
/// @include clearfix;
/// }
///
/// @example css - CSS Output
/// .element::after {
/// clear: both;
/// content: "";
/// display: table;
/// }
@mixin clearfix {
&::after {
clear: both;
content: "";
display: table;
}
}
+30
View File
@@ -0,0 +1,30 @@
@charset "UTF-8";
/// Truncates text and adds an ellipsis to represent overflow.
///
/// @param {Number} $width [100%]
/// Max-width for the string to respect before being truncated
///
/// @example scss - Usage
/// .element {
/// @include ellipsis;
/// }
///
/// @example css - CSS Output
/// .element {
/// display: inline-block;
/// max-width: 100%;
/// overflow: hidden;
/// text-overflow: ellipsis;
/// white-space: nowrap;
/// word-wrap: normal;
/// }
@mixin ellipsis($width: 100%) {
display: inline-block;
max-width: $width;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
word-wrap: normal;
}
+31
View File
@@ -0,0 +1,31 @@
@charset "UTF-8";
/// Georgia font stack.
///
/// @type List
$georgia: "Georgia", "Cambria", "Times New Roman", "Times", serif;
/// Helvetica font stack.
///
/// @type List
$helvetica: "Helvetica Neue", "Helvetica", "Roboto", "Arial", sans-serif;
/// Lucida Grande font stack.
///
/// @type List
$lucida-grande: "Lucida Grande", "Tahoma", "Verdana", "Arial", sans-serif;
/// Monospace font stack.
///
/// @type List
$monospace: "Bitstream Vera Sans Mono", "Consolas", "Courier", monospace;
/// Verdana font stack.
///
/// @type List
$verdana: "Verdana", "Geneva", sans-serif;
+27
View File
@@ -0,0 +1,27 @@
/// Hides the text in an element, commonly used to show an image. Some elements will need block-level styles applied.
///
/// @link http://zeldman.com/2012/03/01/replacing-the-9999px-hack-new-image-replacement
///
/// @example scss - Usage
/// .element {
/// @include hide-text;
/// }
///
/// @example css - CSS Output
/// .element {
/// overflow: hidden;
/// text-indent: 101%;
/// white-space: nowrap;
/// }
///
/// @todo Remove height argument in v5.0.0
@mixin hide-text($height: null) {
overflow: hidden;
text-indent: 101%;
white-space: nowrap;
@if $height {
@warn "The `hide-text` mixin has changed and no longer requires a height. The height argument will no longer be accepted in v5.0.0";
}
}
+26
View File
@@ -0,0 +1,26 @@
@charset "UTF-8";
/// Provides a quick method for targeting `margin` on specific sides of a box. Use a `null` value to “skip” a side.
///
/// @param {Arglist} $vals
/// List of arguments
///
/// @example scss - Usage
/// .element {
/// @include margin(null 10px 3em 20vh);
/// }
///
/// @example css - CSS Output
/// .element {
/// margin-bottom: 3em;
/// margin-left: 20vh;
/// margin-right: 10px;
/// }
///
/// @require {mixin} directional-property
///
/// @output `margin`
@mixin margin($vals...) {
@include directional-property(margin, false, $vals...);
}
+26
View File
@@ -0,0 +1,26 @@
@charset "UTF-8";
/// Provides a quick method for targeting `padding` on specific sides of a box. Use a `null` value to “skip” a side.
///
/// @param {Arglist} $vals
/// List of arguments
///
/// @example scss - Usage
/// .element {
/// @include padding(12vh null 10px 5%);
/// }
///
/// @example css - CSS Output
/// .element {
/// padding-bottom: 10px;
/// padding-left: 5%;
/// padding-top: 12vh;
/// }
///
/// @require {mixin} directional-property
///
/// @output `padding`
@mixin padding($vals...) {
@include directional-property(padding, false, $vals...);
}
+48
View File
@@ -0,0 +1,48 @@
@charset "UTF-8";
/// Provides a quick method for setting an elements position. Use a `null` value to “skip” a side.
///
/// @param {Position} $position [relative]
/// A CSS position value
///
/// @param {Arglist} $coordinates [null null null null]
/// List of values that correspond to the 4-value syntax for the edges of a box
///
/// @example scss - Usage
/// .element {
/// @include position(absolute, 0 null null 10em);
/// }
///
/// @example css - CSS Output
/// .element {
/// left: 10em;
/// position: absolute;
/// top: 0;
/// }
///
/// @require {function} is-length
/// @require {function} unpack
@mixin position($position: relative, $coordinates: null null null null) {
@if type-of($position) == list {
$coordinates: $position;
$position: relative;
}
$coordinates: unpack($coordinates);
$offsets: (
top: nth($coordinates, 1),
right: nth($coordinates, 2),
bottom: nth($coordinates, 3),
left: nth($coordinates, 4)
);
position: $position;
@each $offset, $value in $offsets {
@if is-length($value) {
#{$offset}: $value;
}
}
}
+66
View File
@@ -0,0 +1,66 @@
@charset "UTF-8";
/// A mixin for generating vendor prefixes on non-standardized properties.
///
/// @param {String} $property
/// Property to prefix
///
/// @param {*} $value
/// Value to use
///
/// @param {List} $prefixes
/// Prefixes to define
///
/// @example scss - Usage
/// .element {
/// @include prefixer(border-radius, 10px, webkit ms spec);
/// }
///
/// @example css - CSS Output
/// .element {
/// -webkit-border-radius: 10px;
/// -moz-border-radius: 10px;
/// border-radius: 10px;
/// }
///
/// @require {variable} $prefix-for-webkit
/// @require {variable} $prefix-for-mozilla
/// @require {variable} $prefix-for-microsoft
/// @require {variable} $prefix-for-opera
/// @require {variable} $prefix-for-spec
@mixin prefixer($property, $value, $prefixes) {
@each $prefix in $prefixes {
@if $prefix == webkit {
@if $prefix-for-webkit {
-webkit-#{$property}: $value;
}
} @else if $prefix == moz {
@if $prefix-for-mozilla {
-moz-#{$property}: $value;
}
} @else if $prefix == ms {
@if $prefix-for-microsoft {
-ms-#{$property}: $value;
}
} @else if $prefix == o {
@if $prefix-for-opera {
-o-#{$property}: $value;
}
} @else if $prefix == spec {
@if $prefix-for-spec {
#{$property}: $value;
}
} @else {
@warn "Unrecognized prefix: #{$prefix}";
}
}
}
@mixin disable-prefix-for-all() {
$prefix-for-webkit: false !global;
$prefix-for-mozilla: false !global;
$prefix-for-microsoft: false !global;
$prefix-for-opera: false !global;
$prefix-for-spec: false !global;
}
+27
View File
@@ -0,0 +1,27 @@
@mixin retina-image($filename, $background-size, $extension: png, $retina-filename: null, $retina-suffix: _2x, $asset-pipeline: $asset-pipeline) {
@include _bourbon-deprecate("retina-image");
@if $asset-pipeline {
background-image: image-url("#{$filename}.#{$extension}");
} @else {
background-image: url("#{$filename}.#{$extension}");
}
@include hidpi {
@if $asset-pipeline {
@if $retina-filename {
background-image: image-url("#{$retina-filename}.#{$extension}");
} @else {
background-image: image-url("#{$filename}#{$retina-suffix}.#{$extension}");
}
} @else {
@if $retina-filename {
background-image: url("#{$retina-filename}.#{$extension}");
} @else {
background-image: url("#{$filename}#{$retina-suffix}.#{$extension}");
}
}
background-size: $background-size;
}
}
+51
View File
@@ -0,0 +1,51 @@
@charset "UTF-8";
/// Sets the `width` and `height` of the element.
///
/// @param {List} $size
/// A list of at most 2 size values.
///
/// If there is only a single value in `$size` it is used for both width and height. All units are supported.
///
/// @example scss - Usage
/// .first-element {
/// @include size(2em);
/// }
///
/// .second-element {
/// @include size(auto 10em);
/// }
///
/// @example css - CSS Output
/// .first-element {
/// width: 2em;
/// height: 2em;
/// }
///
/// .second-element {
/// width: auto;
/// height: 10em;
/// }
///
/// @todo Refactor in 5.0.0 to use a comma-separated argument
@mixin size($value) {
$width: nth($value, 1);
$height: $width;
@if length($value) > 1 {
$height: nth($value, 2);
}
@if is-size($height) {
height: $height;
} @else {
@warn "`#{$height}` is not a valid length for the `$height` parameter in the `size` mixin.";
}
@if is-size($width) {
width: $width;
} @else {
@warn "`#{$width}` is not a valid length for the `$width` parameter in the `size` mixin.";
}
}
+113
View File
@@ -0,0 +1,113 @@
@charset "UTF-8";
/// Generates variables for all text-based inputs. Please note that you must use interpolation on the variable: `#{$all-text-inputs}`.
///
/// @example scss - Usage
/// #{$all-text-inputs} {
/// border: 1px solid #f00;
/// }
///
/// #{$all-text-inputs-focus},
/// #{$all-text-inputs-hover} {
/// border: 1px solid #0f0;
/// }
///
/// #{$all-text-inputs-active} {
/// border: 1px solid #00f;
/// }
///
/// @example css - CSS Output
/// input[type="color"],
/// input[type="date"],
/// input[type="datetime"],
/// input[type="datetime-local"],
/// input[type="email"],
/// input[type="month"],
/// input[type="number"],
/// input[type="password"],
/// input[type="search"],
/// input[type="tel"],
/// input[type="text"],
/// input[type="time"],
/// input[type="url"],
/// input[type="week"],
/// textarea {
/// border: 1px solid #f00;
/// }
///
/// input[type="color"]:focus,
/// input[type="date"]:focus,
/// input[type="datetime"]:focus,
/// input[type="datetime-local"]:focus,
/// input[type="email"]:focus,
/// input[type="month"]:focus,
/// input[type="number"]:focus,
/// input[type="password"]:focus,
/// input[type="search"]:focus,
/// input[type="tel"]:focus,
/// input[type="text"]:focus,
/// input[type="time"]:focus,
/// input[type="url"]:focus,
/// input[type="week"]:focus,
/// textarea:focus,
/// input[type="color"]:hover,
/// input[type="date"]:hover,
/// input[type="datetime"]:hover,
/// input[type="datetime-local"]:hover,
/// input[type="email"]:hover,
/// input[type="month"]:hover,
/// input[type="number"]:hover,
/// input[type="password"]:hover,
/// input[type="search"]:hover,
/// input[type="tel"]:hover,
/// input[type="text"]:hover,
/// input[type="time"]:hover,
/// input[type="url"]:hover,
/// input[type="week"]:hover,
/// textarea:hover {
/// border: 1px solid #0f0;
/// }
///
/// input[type="color"]:active,
/// input[type="date"]:active,
/// input[type="datetime"]:active,
/// input[type="datetime-local"]:active,
/// input[type="email"]:active,
/// input[type="month"]:active,
/// input[type="number"]:active,
/// input[type="password"]:active,
/// input[type="search"]:active,
/// input[type="tel"]:active,
/// input[type="text"]:active,
/// input[type="time"]:active,
/// input[type="url"]:active,
/// input[type="week"]:active,
/// textarea:active {
/// border: 1px solid #00f;
/// }
///
/// @require assign-inputs
///
/// @type List
$text-inputs-list: 'input[type="color"]',
'input[type="date"]',
'input[type="datetime"]',
'input[type="datetime-local"]',
'input[type="email"]',
'input[type="month"]',
'input[type="number"]',
'input[type="password"]',
'input[type="search"]',
'input[type="tel"]',
'input[type="text"]',
'input[type="time"]',
'input[type="url"]',
'input[type="week"]',
'input:not([type])',
'textarea';
$all-text-inputs: assign-inputs($text-inputs-list);
$all-text-inputs-active: assign-inputs($text-inputs-list, active);
$all-text-inputs-focus: assign-inputs($text-inputs-list, focus);
$all-text-inputs-hover: assign-inputs($text-inputs-list, hover);
+34
View File
@@ -0,0 +1,34 @@
@charset "UTF-8";
/// CSS cubic-bezier timing functions. Timing functions courtesy of jquery.easie (github.com/jaukia/easie)
///
/// Timing functions are the same as demoed here: http://jqueryui.com/resources/demos/effect/easing.html
///
/// @type cubic-bezier
$ease-in-quad: cubic-bezier(0.550, 0.085, 0.680, 0.530);
$ease-in-cubic: cubic-bezier(0.550, 0.055, 0.675, 0.190);
$ease-in-quart: cubic-bezier(0.895, 0.030, 0.685, 0.220);
$ease-in-quint: cubic-bezier(0.755, 0.050, 0.855, 0.060);
$ease-in-sine: cubic-bezier(0.470, 0.000, 0.745, 0.715);
$ease-in-expo: cubic-bezier(0.950, 0.050, 0.795, 0.035);
$ease-in-circ: cubic-bezier(0.600, 0.040, 0.980, 0.335);
$ease-in-back: cubic-bezier(0.600, -0.280, 0.735, 0.045);
$ease-out-quad: cubic-bezier(0.250, 0.460, 0.450, 0.940);
$ease-out-cubic: cubic-bezier(0.215, 0.610, 0.355, 1.000);
$ease-out-quart: cubic-bezier(0.165, 0.840, 0.440, 1.000);
$ease-out-quint: cubic-bezier(0.230, 1.000, 0.320, 1.000);
$ease-out-sine: cubic-bezier(0.390, 0.575, 0.565, 1.000);
$ease-out-expo: cubic-bezier(0.190, 1.000, 0.220, 1.000);
$ease-out-circ: cubic-bezier(0.075, 0.820, 0.165, 1.000);
$ease-out-back: cubic-bezier(0.175, 0.885, 0.320, 1.275);
$ease-in-out-quad: cubic-bezier(0.455, 0.030, 0.515, 0.955);
$ease-in-out-cubic: cubic-bezier(0.645, 0.045, 0.355, 1.000);
$ease-in-out-quart: cubic-bezier(0.770, 0.000, 0.175, 1.000);
$ease-in-out-quint: cubic-bezier(0.860, 0.000, 0.070, 1.000);
$ease-in-out-sine: cubic-bezier(0.445, 0.050, 0.550, 0.950);
$ease-in-out-expo: cubic-bezier(1.000, 0.000, 0.000, 1.000);
$ease-in-out-circ: cubic-bezier(0.785, 0.135, 0.150, 0.860);
$ease-in-out-back: cubic-bezier(0.680, -0.550, 0.265, 1.550);
+63
View File
@@ -0,0 +1,63 @@
@mixin triangle($size, $color, $direction) {
$width: nth($size, 1);
$height: nth($size, length($size));
$foreground-color: nth($color, 1);
$background-color: if(length($color) == 2, nth($color, 2), transparent);
height: 0;
width: 0;
@if ($direction == up) or ($direction == down) or ($direction == right) or ($direction == left) {
$width: $width / 2;
$height: if(length($size) > 1, $height, $height/2);
@if $direction == up {
border-bottom: $height solid $foreground-color;
border-left: $width solid $background-color;
border-right: $width solid $background-color;
} @else if $direction == right {
border-bottom: $width solid $background-color;
border-left: $height solid $foreground-color;
border-top: $width solid $background-color;
} @else if $direction == down {
border-left: $width solid $background-color;
border-right: $width solid $background-color;
border-top: $height solid $foreground-color;
} @else if $direction == left {
border-bottom: $width solid $background-color;
border-right: $height solid $foreground-color;
border-top: $width solid $background-color;
}
} @else if ($direction == up-right) or ($direction == up-left) {
border-top: $height solid $foreground-color;
@if $direction == up-right {
border-left: $width solid $background-color;
} @else if $direction == up-left {
border-right: $width solid $background-color;
}
} @else if ($direction == down-right) or ($direction == down-left) {
border-bottom: $height solid $foreground-color;
@if $direction == down-right {
border-left: $width solid $background-color;
} @else if $direction == down-left {
border-right: $width solid $background-color;
}
} @else if ($direction == inset-up) {
border-color: $background-color $background-color $foreground-color;
border-style: solid;
border-width: $height $width;
} @else if ($direction == inset-down) {
border-color: $foreground-color $background-color $background-color;
border-style: solid;
border-width: $height $width;
} @else if ($direction == inset-right) {
border-color: $background-color $background-color $background-color $foreground-color;
border-style: solid;
border-width: $width $height;
} @else if ($direction == inset-left) {
border-color: $background-color $foreground-color $background-color $background-color;
border-style: solid;
border-width: $width $height;
}
}
+29
View File
@@ -0,0 +1,29 @@
@charset "UTF-8";
/// Provides an easy way to change the `word-wrap` property.
///
/// @param {String} $wrap [break-word]
/// Value for the `word-break` property.
///
/// @example scss - Usage
/// .wrapper {
/// @include word-wrap(break-word);
/// }
///
/// @example css - CSS Output
/// .wrapper {
/// overflow-wrap: break-word;
/// word-break: break-all;
/// word-wrap: break-word;
/// }
@mixin word-wrap($wrap: break-word) {
overflow-wrap: $wrap;
word-wrap: $wrap;
@if $wrap == break-word {
word-break: break-all;
} @else {
word-break: $wrap;
}
}
+61
View File
@@ -0,0 +1,61 @@
// http://www.w3.org/TR/css3-animations/#the-animation-name-property-
// Each of these mixins support comma separated lists of values, which allows different transitions for individual properties to be described in a single style rule. Each value in the list corresponds to the value at that same position in the other properties.
@mixin animation($animations...) {
@include _bourbon-deprecate-for-prefixing("animation");
@include prefixer(animation, $animations, webkit moz spec);
}
@mixin animation-name($names...) {
@include _bourbon-deprecate-for-prefixing("animation-name");
@include prefixer(animation-name, $names, webkit moz spec);
}
@mixin animation-duration($times...) {
@include _bourbon-deprecate-for-prefixing("animation-duration");
@include prefixer(animation-duration, $times, webkit moz spec);
}
@mixin animation-timing-function($motions...) {
@include _bourbon-deprecate-for-prefixing("animation-timing-function");
// ease | linear | ease-in | ease-out | ease-in-out
@include prefixer(animation-timing-function, $motions, webkit moz spec);
}
@mixin animation-iteration-count($values...) {
@include _bourbon-deprecate-for-prefixing("animation-iteration-count");
// infinite | <number>
@include prefixer(animation-iteration-count, $values, webkit moz spec);
}
@mixin animation-direction($directions...) {
@include _bourbon-deprecate-for-prefixing("animation-direction");
// normal | alternate
@include prefixer(animation-direction, $directions, webkit moz spec);
}
@mixin animation-play-state($states...) {
@include _bourbon-deprecate-for-prefixing("animation-play-state");
// running | paused
@include prefixer(animation-play-state, $states, webkit moz spec);
}
@mixin animation-delay($times...) {
@include _bourbon-deprecate-for-prefixing("animation-delay");
@include prefixer(animation-delay, $times, webkit moz spec);
}
@mixin animation-fill-mode($modes...) {
@include _bourbon-deprecate-for-prefixing("animation-fill-mode");
// none | forwards | backwards | both
@include prefixer(animation-fill-mode, $modes, webkit moz spec);
}
+5
View File
@@ -0,0 +1,5 @@
@mixin appearance($value) {
@include _bourbon-deprecate-for-prefixing("appearance");
@include prefixer(appearance, $value, webkit moz ms o spec);
}
+5
View File
@@ -0,0 +1,5 @@
@mixin backface-visibility($visibility) {
@include _bourbon-deprecate-for-prefixing("backface-visibility");
@include prefixer(backface-visibility, $visibility, webkit spec);
}
+44
View File
@@ -0,0 +1,44 @@
//************************************************************************//
// Background-image property for adding multiple background images with
// gradients, or for stringing multiple gradients together.
//************************************************************************//
@mixin background-image($images...) {
@include _bourbon-deprecate-for-prefixing("background-image");
$webkit-images: ();
$spec-images: ();
@each $image in $images {
$webkit-image: ();
$spec-image: ();
@if (type-of($image) == string) {
$url-str: str-slice($image, 1, 3);
$gradient-type: str-slice($image, 1, 6);
@if $url-str == "url" {
$webkit-image: $image;
$spec-image: $image;
}
@else if $gradient-type == "linear" {
$gradients: _linear-gradient-parser($image);
$webkit-image: map-get($gradients, webkit-image);
$spec-image: map-get($gradients, spec-image);
}
@else if $gradient-type == "radial" {
$gradients: _radial-gradient-parser($image);
$webkit-image: map-get($gradients, webkit-image);
$spec-image: map-get($gradients, spec-image);
}
}
$webkit-images: append($webkit-images, $webkit-image, comma);
$spec-images: append($spec-images, $spec-image, comma);
}
background-image: $webkit-images;
background-image: $spec-images;
}
+57
View File
@@ -0,0 +1,57 @@
//************************************************************************//
// Background property for adding multiple backgrounds using shorthand
// notation.
//************************************************************************//
@mixin background($backgrounds...) {
@include _bourbon-deprecate-for-prefixing("background");
$webkit-backgrounds: ();
$spec-backgrounds: ();
@each $background in $backgrounds {
$webkit-background: ();
$spec-background: ();
$background-type: type-of($background);
@if $background-type == string or $background-type == list {
$background-str: if($background-type == list, nth($background, 1), $background);
$url-str: str-slice($background-str, 1, 3);
$gradient-type: str-slice($background-str, 1, 6);
@if $url-str == "url" {
$webkit-background: $background;
$spec-background: $background;
}
@else if $gradient-type == "linear" {
$gradients: _linear-gradient-parser("#{$background}");
$webkit-background: map-get($gradients, webkit-image);
$spec-background: map-get($gradients, spec-image);
}
@else if $gradient-type == "radial" {
$gradients: _radial-gradient-parser("#{$background}");
$webkit-background: map-get($gradients, webkit-image);
$spec-background: map-get($gradients, spec-image);
}
@else {
$webkit-background: $background;
$spec-background: $background;
}
}
@else {
$webkit-background: $background;
$spec-background: $background;
}
$webkit-backgrounds: append($webkit-backgrounds, $webkit-background, comma);
$spec-backgrounds: append($spec-backgrounds, $spec-background, comma);
}
background: $webkit-backgrounds;
background: $spec-backgrounds;
}
+61
View File
@@ -0,0 +1,61 @@
@mixin border-image($borders...) {
@include _bourbon-deprecate-for-prefixing("border-image");
$webkit-borders: ();
$spec-borders: ();
@each $border in $borders {
$webkit-border: ();
$spec-border: ();
$border-type: type-of($border);
@if $border-type == string or list {
$border-str: if($border-type == list, nth($border, 1), $border);
$url-str: str-slice($border-str, 1, 3);
$gradient-type: str-slice($border-str, 1, 6);
@if $url-str == "url" {
$webkit-border: $border;
$spec-border: $border;
}
@else if $gradient-type == "linear" {
$gradients: _linear-gradient-parser("#{$border}");
$webkit-border: map-get($gradients, webkit-image);
$spec-border: map-get($gradients, spec-image);
}
@else if $gradient-type == "radial" {
$gradients: _radial-gradient-parser("#{$border}");
$webkit-border: map-get($gradients, webkit-image);
$spec-border: map-get($gradients, spec-image);
}
@else {
$webkit-border: $border;
$spec-border: $border;
}
}
@else {
$webkit-border: $border;
$spec-border: $border;
}
$webkit-borders: append($webkit-borders, $webkit-border, comma);
$spec-borders: append($spec-borders, $spec-border, comma);
}
-webkit-border-image: $webkit-borders;
border-image: $spec-borders;
border-style: solid;
}
//Examples:
// @include border-image(url("image.png"));
// @include border-image(url("image.png") 20 stretch);
// @include border-image(linear-gradient(45deg, orange, yellow));
// @include border-image(linear-gradient(45deg, orange, yellow) stretch);
// @include border-image(linear-gradient(45deg, orange, yellow) 20 30 40 50 stretch round);
// @include border-image(radial-gradient(top, cover, orange, yellow, orange));
+6
View File
@@ -0,0 +1,6 @@
@mixin calc($property, $value) {
@include _bourbon-deprecate-for-prefixing("calc");
#{$property}: -webkit-calc(#{$value});
#{$property}: calc(#{$value});
}
+67
View File
@@ -0,0 +1,67 @@
@mixin columns($arg: auto) {
@include _bourbon-deprecate-for-prefixing("columns");
// <column-count> || <column-width>
@include prefixer(columns, $arg, webkit moz spec);
}
@mixin column-count($int: auto) {
@include _bourbon-deprecate-for-prefixing("column-count");
// auto || integer
@include prefixer(column-count, $int, webkit moz spec);
}
@mixin column-gap($length: normal) {
@include _bourbon-deprecate-for-prefixing("column-gap");
// normal || length
@include prefixer(column-gap, $length, webkit moz spec);
}
@mixin column-fill($arg: auto) {
@include _bourbon-deprecate-for-prefixing("column-fill");
// auto || length
@include prefixer(column-fill, $arg, webkit moz spec);
}
@mixin column-rule($arg) {
@include _bourbon-deprecate-for-prefixing("column-rule");
// <border-width> || <border-style> || <color>
@include prefixer(column-rule, $arg, webkit moz spec);
}
@mixin column-rule-color($color) {
@include _bourbon-deprecate-for-prefixing("column-rule-color");
@include prefixer(column-rule-color, $color, webkit moz spec);
}
@mixin column-rule-style($style: none) {
@include _bourbon-deprecate-for-prefixing("column-rule-style");
// none | hidden | dashed | dotted | double | groove | inset | inset | outset | ridge | solid
@include prefixer(column-rule-style, $style, webkit moz spec);
}
@mixin column-rule-width($width: none) {
@include _bourbon-deprecate-for-prefixing("column-rule-width");
@include prefixer(column-rule-width, $width, webkit moz spec);
}
@mixin column-span($arg: none) {
@include _bourbon-deprecate-for-prefixing("column-span");
// none || all
@include prefixer(column-span, $arg, webkit moz spec);
}
@mixin column-width($length: auto) {
@include _bourbon-deprecate-for-prefixing("column-width");
// auto || length
@include prefixer(column-width, $length, webkit moz spec);
}
+6
View File
@@ -0,0 +1,6 @@
@mixin filter($function: none) {
@include _bourbon-deprecate-for-prefixing("filter");
// <filter-function> [<filter-function]* | none
@include prefixer(filter, $function, webkit spec);
}
+327
View File
@@ -0,0 +1,327 @@
// CSS3 Flexible Box Model and property defaults
// Custom shorthand notation for flexbox
@mixin box($orient: inline-axis, $pack: start, $align: stretch) {
@include _bourbon-deprecate-for-prefixing("box");
@include display-box;
@include box-orient($orient);
@include box-pack($pack);
@include box-align($align);
}
@mixin display-box {
@include _bourbon-deprecate-for-prefixing("display-box");
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox; // IE 10
display: box;
}
@mixin box-orient($orient: inline-axis) {
@include _bourbon-deprecate-for-prefixing("box-orient");
// horizontal|vertical|inline-axis|block-axis|inherit
@include prefixer(box-orient, $orient, webkit moz spec);
}
@mixin box-pack($pack: start) {
@include _bourbon-deprecate-for-prefixing("box-pack");
// start|end|center|justify
@include prefixer(box-pack, $pack, webkit moz spec);
-ms-flex-pack: $pack; // IE 10
}
@mixin box-align($align: stretch) {
@include _bourbon-deprecate-for-prefixing("box-align");
// start|end|center|baseline|stretch
@include prefixer(box-align, $align, webkit moz spec);
-ms-flex-align: $align; // IE 10
}
@mixin box-direction($direction: normal) {
@include _bourbon-deprecate-for-prefixing("box-direction");
// normal|reverse|inherit
@include prefixer(box-direction, $direction, webkit moz spec);
-ms-flex-direction: $direction; // IE 10
}
@mixin box-lines($lines: single) {
@include _bourbon-deprecate-for-prefixing("box-lines");
// single|multiple
@include prefixer(box-lines, $lines, webkit moz spec);
}
@mixin box-ordinal-group($int: 1) {
@include _bourbon-deprecate-for-prefixing("box-ordinal-group");
@include prefixer(box-ordinal-group, $int, webkit moz spec);
-ms-flex-order: $int; // IE 10
}
@mixin box-flex($value: 0) {
@include _bourbon-deprecate-for-prefixing("box-flex");
@include prefixer(box-flex, $value, webkit moz spec);
-ms-flex: $value; // IE 10
}
@mixin box-flex-group($int: 1) {
@include _bourbon-deprecate-for-prefixing("box-flex-group");
@include prefixer(box-flex-group, $int, webkit moz spec);
}
// CSS3 Flexible Box Model and property defaults
// Unified attributes for 2009, 2011, and 2012 flavours.
// 2009 - display (box | inline-box)
// 2011 - display (flexbox | inline-flexbox)
// 2012 - display (flex | inline-flex)
@mixin display($value) {
@include _bourbon-deprecate-for-prefixing("display");
// flex | inline-flex
@if $value == "flex" {
// 2009
display: -webkit-box;
display: -moz-box;
display: box;
// 2012
display: -webkit-flex;
display: -moz-flex;
display: -ms-flexbox; // 2011 (IE 10)
display: flex;
} @else if $value == "inline-flex" {
display: -webkit-inline-box;
display: -moz-inline-box;
display: inline-box;
display: -webkit-inline-flex;
display: -moz-inline-flex;
display: -ms-inline-flexbox;
display: inline-flex;
} @else {
display: $value;
}
}
// 2009 - box-flex (integer)
// 2011 - flex (decimal | width decimal)
// 2012 - flex (integer integer width)
@mixin flex($value) {
@include _bourbon-deprecate-for-prefixing("flex");
// Grab flex-grow for older browsers.
$flex-grow: nth($value, 1);
// 2009
@include prefixer(box-flex, $flex-grow, webkit moz spec);
// 2011 (IE 10), 2012
@include prefixer(flex, $value, webkit moz ms spec);
}
// 2009 - box-orient ( horizontal | vertical | inline-axis | block-axis)
// - box-direction (normal | reverse)
// 2011 - flex-direction (row | row-reverse | column | column-reverse)
// 2012 - flex-direction (row | row-reverse | column | column-reverse)
@mixin flex-direction($value: row) {
@include _bourbon-deprecate-for-prefixing("flex-direction");
// Alt values.
$value-2009: $value;
$value-2011: $value;
$direction: normal;
@if $value == row {
$value-2009: horizontal;
} @else if $value == "row-reverse" {
$value-2009: horizontal;
$direction: reverse;
} @else if $value == column {
$value-2009: vertical;
} @else if $value == "column-reverse" {
$value-2009: vertical;
$direction: reverse;
}
// 2009
@include prefixer(box-orient, $value-2009, webkit moz spec);
@include prefixer(box-direction, $direction, webkit moz spec);
// 2012
@include prefixer(flex-direction, $value, webkit moz spec);
// 2011 (IE 10)
-ms-flex-direction: $value;
}
// 2009 - box-lines (single | multiple)
// 2011 - flex-wrap (nowrap | wrap | wrap-reverse)
// 2012 - flex-wrap (nowrap | wrap | wrap-reverse)
@mixin flex-wrap($value: nowrap) {
@include _bourbon-deprecate-for-prefixing("flex-wrap");
// Alt values
$alt-value: $value;
@if $value == nowrap {
$alt-value: single;
} @else if $value == wrap {
$alt-value: multiple;
} @else if $value == "wrap-reverse" {
$alt-value: multiple;
}
@include prefixer(box-lines, $alt-value, webkit moz spec);
@include prefixer(flex-wrap, $value, webkit moz ms spec);
}
// 2009 - TODO: parse values into flex-direction/flex-wrap
// 2011 - TODO: parse values into flex-direction/flex-wrap
// 2012 - flex-flow (flex-direction || flex-wrap)
@mixin flex-flow($value) {
@include _bourbon-deprecate-for-prefixing("flex-flow");
@include prefixer(flex-flow, $value, webkit moz spec);
}
// 2009 - box-ordinal-group (integer)
// 2011 - flex-order (integer)
// 2012 - order (integer)
@mixin order($int: 0) {
@include _bourbon-deprecate-for-prefixing("order");
// 2009
@include prefixer(box-ordinal-group, $int, webkit moz spec);
// 2012
@include prefixer(order, $int, webkit moz spec);
// 2011 (IE 10)
-ms-flex-order: $int;
}
// 2012 - flex-grow (number)
@mixin flex-grow($number: 0) {
@include _bourbon-deprecate-for-prefixing("flex-grow");
@include prefixer(flex-grow, $number, webkit moz spec);
-ms-flex-positive: $number;
}
// 2012 - flex-shrink (number)
@mixin flex-shrink($number: 1) {
@include _bourbon-deprecate-for-prefixing("flex-shrink");
@include prefixer(flex-shrink, $number, webkit moz spec);
-ms-flex-negative: $number;
}
// 2012 - flex-basis (number)
@mixin flex-basis($width: auto) {
@include _bourbon-deprecate-for-prefixing("flex-basis");
@include prefixer(flex-basis, $width, webkit moz spec);
-ms-flex-preferred-size: $width;
}
// 2009 - box-pack (start | end | center | justify)
// 2011 - flex-pack (start | end | center | justify)
// 2012 - justify-content (flex-start | flex-end | center | space-between | space-around)
@mixin justify-content($value: flex-start) {
@include _bourbon-deprecate-for-prefixing("justify-content");
// Alt values.
$alt-value: $value;
@if $value == "flex-start" {
$alt-value: start;
} @else if $value == "flex-end" {
$alt-value: end;
} @else if $value == "space-between" {
$alt-value: justify;
} @else if $value == "space-around" {
$alt-value: distribute;
}
// 2009
@include prefixer(box-pack, $alt-value, webkit moz spec);
// 2012
@include prefixer(justify-content, $value, webkit moz ms o spec);
// 2011 (IE 10)
-ms-flex-pack: $alt-value;
}
// 2009 - box-align (start | end | center | baseline | stretch)
// 2011 - flex-align (start | end | center | baseline | stretch)
// 2012 - align-items (flex-start | flex-end | center | baseline | stretch)
@mixin align-items($value: stretch) {
@include _bourbon-deprecate-for-prefixing("align-items");
$alt-value: $value;
@if $value == "flex-start" {
$alt-value: start;
} @else if $value == "flex-end" {
$alt-value: end;
}
// 2009
@include prefixer(box-align, $alt-value, webkit moz spec);
// 2012
@include prefixer(align-items, $value, webkit moz ms o spec);
// 2011 (IE 10)
-ms-flex-align: $alt-value;
}
// 2011 - flex-item-align (auto | start | end | center | baseline | stretch)
// 2012 - align-self (auto | flex-start | flex-end | center | baseline | stretch)
@mixin align-self($value: auto) {
@include _bourbon-deprecate-for-prefixing("align-self");
$value-2011: $value;
@if $value == "flex-start" {
$value-2011: start;
} @else if $value == "flex-end" {
$value-2011: end;
}
// 2012
@include prefixer(align-self, $value, webkit moz spec);
// 2011 (IE 10)
-ms-flex-item-align: $value-2011;
}
// 2011 - flex-line-pack (start | end | center | justify | distribute | stretch)
// 2012 - align-content (flex-start | flex-end | center | space-between | space-around | stretch)
@mixin align-content($value: stretch) {
@include _bourbon-deprecate-for-prefixing("align-content");
$value-2011: $value;
@if $value == "flex-start" {
$value-2011: start;
} @else if $value == "flex-end" {
$value-2011: end;
} @else if $value == "space-between" {
$value-2011: justify;
} @else if $value == "space-around" {
$value-2011: distribute;
}
// 2012
@include prefixer(align-content, $value, webkit moz spec);
// 2011 (IE 10)
-ms-flex-line-pack: $value-2011;
}
+24
View File
@@ -0,0 +1,24 @@
@mixin font-face(
$font-family,
$file-path,
$weight: normal,
$style: normal,
$asset-pipeline: $asset-pipeline,
$file-formats: eot woff2 woff ttf svg) {
$font-url-prefix: font-url-prefixer($asset-pipeline);
@font-face {
font-family: $font-family;
font-style: $style;
font-weight: $weight;
src: font-source-declaration(
$font-family,
$file-path,
$asset-pipeline,
$file-formats,
$font-url-prefix
);
}
}

Some files were not shown because too many files have changed in this diff Show More