25 |
26 | ## How to open
27 |
28 | Auto open:
29 |
30 |
...
31 |
32 | Open via JavaScript:
33 |
34 |
...
35 | $('.pony').trigger('open.modal.tamia');
36 |
37 | Open via link:
38 |
39 |
Open
40 |
41 |
42 | ## Events
43 |
44 | ### open.modal.tamia
45 |
46 | Fire this event on `.modal` DOM node to open popup.
47 |
48 | ### close.modal.tamia
49 |
50 | Fire this event on `.modal` DOM node to close popup.
51 |
52 | ### commit.modal.tamia
53 |
54 | Fires when user clicks Save button.
55 |
56 | You can prevent popup from closing:
57 |
58 | $('.js-popup').on('commit.modal.tamia', function() {
59 | return false;
60 | });
61 |
62 | ### dismiss.modal.tamia
63 |
64 | Fires when user click Cancel button
65 |
66 |
67 | ## Skin
68 |
69 | Set `modal_default_skin` or `modules_default_skin` to `true` to enable default skin.
70 |
71 |
72 | ## Configuration
73 |
74 | ### modal_shade_color
75 |
76 | Type: CSS color value.
77 |
78 | Color of popup shade layer.
79 |
--------------------------------------------------------------------------------
/tamia/modules/spinner/index.styl:
--------------------------------------------------------------------------------
1 | // Tâmia © 2013 Artem Sapegin http://sapegin.me
2 | // Spinner
3 | // Based on http://codepen.io/beben-koben/pen/JcGnK
4 |
5 | spinner_fallback_gif ?= false
6 | loader_shade_color ?= white(.8)
7 |
8 |
9 | @keyframes spinner-rotate
10 | 0%
11 | transform: rotate(0deg)
12 | 100%
13 | transform: rotate(360deg)
14 |
15 |
16 | // Regular spinner
17 | .spinner,
18 | .loader
19 | display: inline-block
20 | width: .25em
21 | height: .25em
22 | margin: .5em .2em
23 | opacity: .7
24 | border-radius: 50%
25 | font-size: 16px
26 | box-shadow: 0 -.4em 0 0 rgba(0,0,0,1), -.28em -.28em 0 0 rgba(0,0,0,.75), -.4em 0 0 0 rgba(0,0,0,.5), -.28em .28em 0 0 rgba(0,0,0,.25)
27 |
28 | &_big
29 | font-size: 32px
30 |
31 |
32 | // Loader
33 | .loader
34 | visibility: hidden
35 | opacity: 0
36 | transition: visibility 0s .3s, opacity .3s ease-out
37 |
38 | .is-loading &
39 | visibility: visible
40 | opacity: 1
41 | transition: opacity .5s ease-out
42 |
43 |
44 | // Enable animation only when spinner is visible to increase page performance
45 | .spinner,
46 | .is-loading .loader
47 | animation: .85s spinner-rotate steps(9) infinite
48 |
49 | .spinner.is-hidden,
50 | .loader.is-hidden
51 | animation: none
52 |
53 |
54 | // Loader shade
55 | .loader-wrapper
56 | position: relative
57 |
58 | .loader-shade
59 | position: absolute
60 | top: 0
61 | left: 0
62 | width: 100%
63 | height: 100%
64 | opacity: 0
65 | background: loader_shade_color
66 | border-radius: inherit
67 | z-index: 99999
68 | transition: opacity .15s ease-out
69 |
70 | .is-loading &
71 | opacity: 1
72 |
73 |
74 | // Fallback GIF image for browsers that don’t support CSS animations
75 | if spinner_fallback_gif
76 | .no-cssanimations .spinner
77 | width: 16px
78 | height: 16px
79 | margin: 0
80 | opacity: 1
81 | background: url(spinner_fallback_gif)
82 |
--------------------------------------------------------------------------------
/app/components/select.js:
--------------------------------------------------------------------------------
1 | // Author: Artem Sapegin http://sapegin.me, 2015
2 |
3 | 'use strict';
4 |
5 | import Block from 'bem-cn';
6 |
7 | let b = new Block('select');
8 |
9 | export default React.createClass({
10 | displayName: 'Select',
11 | propTypes: {
12 | items: React.PropTypes.object,
13 | value: React.PropTypes.any
14 | },
15 |
16 | getDefaultProps: function() {
17 | return {
18 | items: {},
19 | value: ''
20 | };
21 | },
22 |
23 | componentDidMount: function() {
24 | this._handleChange();
25 | },
26 |
27 | _handleChange: function() {
28 | this.refs.box.getDOMNode().innerHTML = this._getNameByValue(this.refs.select.getDOMNode().value);
29 | },
30 |
31 | _handleFocus: function() {
32 | this.refs.container.getDOMNode().classList.add('is-focused');
33 | },
34 |
35 | _handleBlur: function() {
36 | this.refs.container.getDOMNode().classList.remove('is-focused');
37 | },
38 |
39 | _getNameByValue: function(needleValue) {
40 | let items = this.props.items;
41 | for (let name in items) {
42 | let value = items[name];
43 | if (String(value) === String(needleValue)) {
44 | return name;
45 | }
46 | }
47 | },
48 |
49 | getValue: function() {
50 | return this.refs.select.getDOMNode().value;
51 | },
52 |
53 | render: function() {
54 | let items = this.props.items;
55 | let options = Object.keys(items).map(function(name) {
56 | let value = items[name];
57 | return (
58 |
59 | );
60 | });
61 |
62 | return (
63 |
64 |
{this.props.value}
65 |
72 |
73 | );
74 | }
75 | });
76 |
--------------------------------------------------------------------------------
/tamia/modules/code/tomorrow.styl:
--------------------------------------------------------------------------------
1 | // Tâmia © 2015 Artem Sapegin http://sapegin.me
2 | // Tomorrow theme
3 | // Theme from: https://github.com/isagalaev/highlight.js/blob/master/src/styles/tomorrow.css
4 |
5 | _prefix = code_class_prefix
6 |
7 | // Comment
8 | .{_prefix}comment
9 | color: #8e908c
10 | @media print
11 | font-style: italic
12 |
13 | // Red
14 | .{_prefix}variable,
15 | .{_prefix}attribute,
16 | .{_prefix}tag,
17 | .{_prefix}regexp,
18 | .ruby .{_prefix}constant,
19 | .xml .{_prefix}tag .{_prefix}title,
20 | .xml .{_prefix}pi,
21 | .xml .{_prefix}doctype,
22 | .html .{_prefix}doctype,
23 | .css .{_prefix}id,
24 | .css .{_prefix}class,
25 | .css .{_prefix}pseudo
26 | color: #c82829
27 | @media print
28 | font-weight: bold
29 |
30 | // Orange
31 | .{_prefix}number,
32 | .{_prefix}preprocessor,
33 | .{_prefix}pragma,
34 | .{_prefix}built_in,
35 | .{_prefix}literal,
36 | .{_prefix}params,
37 | .{_prefix}constant
38 | color: #f5871f
39 |
40 | // Yellow
41 | .ruby .{_prefix}class .{_prefix}title,
42 | .css .{_prefix}rules .{_prefix}attribute
43 | color: #eab700
44 |
45 | // Green
46 | .{_prefix}string,
47 | .{_prefix}value,
48 | .{_prefix}inheritance,
49 | .{_prefix}header,
50 | .ruby .{_prefix}symbol,
51 | .xml .{_prefix}cdata
52 | color: #718c00
53 |
54 | // Aqua
55 | .{_prefix}title,
56 | .css .{_prefix}hexcolor
57 | color: #3e999f
58 |
59 | // Blue
60 | .{_prefix}function,
61 | .python .{_prefix}decorator,
62 | .python .{_prefix}title,
63 | .ruby .{_prefix}function .{_prefix}title,
64 | .ruby .{_prefix}title .{_prefix}keyword,
65 | .perl .{_prefix}sub,
66 | .javascript .{_prefix}title,
67 | .coffeescript .{_prefix}title
68 | color: #4271ae
69 |
70 | // Purple
71 | .{_prefix}keyword,
72 | .javascript .{_prefix}function
73 | color: #8959a8
74 |
75 | .coffeescript .javascript,
76 | .javascript .xml,
77 | .tex .hljs-formula,
78 | .xml .javascript,
79 | .xml .vbscript,
80 | .xml .css,
81 | .xml .hljs-cdata
82 | opacity: 0.5
83 |
--------------------------------------------------------------------------------
/tamia/modules/print/index.styl:
--------------------------------------------------------------------------------
1 | // Tâmia © 2013 Artem Sapegin http://sapegin.me
2 | // Print stylesheet
3 |
4 | // Site domain (for example, `example.com`) that will be printed.
5 | site_domain ?= false
6 |
7 | @media print
8 |
9 | @page
10 | margin: .5cm
11 |
12 | nav,
13 | .social-likes,
14 | .no-print
15 | display: none
16 |
17 | .print
18 | display: block
19 |
20 | *
21 | font-family: Cambria, Georgia, serif
22 | color: #000 !important
23 | background: transparent !important
24 | float: none !important
25 | width: auto !important
26 | margin-side: 0 !important
27 | padding-side: 0 !important
28 | text-shadow: none !important
29 | box-shadow: none !important
30 |
31 | body
32 | padding-bottom: 0
33 |
34 | a
35 | border: none !important
36 |
37 | h1,
38 | h2,
39 | h3,
40 | h4,
41 | h5,
42 | h6
43 | font-family: Corbel, 'Helvetica Neue', Arial, sans-serif
44 | page-break-inside: avoid
45 | page-break-after: avoid
46 |
47 | a[href^="http"]:link:after,
48 | a[href^="http"]:visited:after
49 | content: " (" attr(href) ")"
50 | font-size: .9em
51 |
52 | if site_domain
53 | a[href^="/"]:link:after,
54 | a[href^="/"]:visited:after
55 | content: (" (http://" + site_domain) attr(href) ")"
56 | font-size: .9em
57 |
58 | .header
59 | margin-bottom: 2*spacer
60 | border-bottom: 1pt solid #000
61 | height: auto
62 |
63 | .footer
64 | position: static
65 | margin-top: 2*spacer
66 | border-top: 1pt solid #000
67 | height: auto
68 | a
69 | text-decoration: none
70 |
71 | if site_domain
72 | .logo:before
73 | content: url(/favicon.ico)
74 | padding-right: 2px
75 | vertical-align: middle
76 |
77 | .logo a:link:after,
78 | .logo a:visited:after
79 | content: " — " + site_domain
80 |
81 | p,
82 | blockquote,
83 | ul,
84 | ol,
85 | dl,
86 | tr,
87 | img
88 | page-break-inside: avoid
89 |
90 | p,
91 | h2,
92 | h3
93 | orphans: 3
94 | widows: 3
95 |
96 | ul
97 | margin-left: 1.2em !important
98 |
--------------------------------------------------------------------------------
/tamia/modules/switcher/index.styl:
--------------------------------------------------------------------------------
1 | // Tâmia © 2013 Artem Sapegin http://sapegin.me
2 | // Switcher
3 | // Dependencies: form
4 |
5 | // Bones
6 |
7 | .switcher
8 | clearfix()
9 | no-select()
10 |
11 | &__label,
12 | &__input
13 | // For prehistoric browsers
14 | vertical-align: text-bottom
15 | padding-right: .5em
16 | font-size: 1em
17 |
18 | &__input:checked,
19 | &__input:not(:checked)
20 | position: absolute
21 | opacity: 0
22 |
23 | &__input:checked + &__label,
24 | &__input:not(:checked) + &__label
25 | position: relative
26 | float: left
27 | box-sizing: border-box
28 | vertical-align: top
29 | cursor: pointer
30 | z-index: 1
31 | margin-left: -1px
32 | user-select: none
33 |
34 | &__label:nth-of-type(1)
35 | margin-left: 0
36 |
37 | &__input:checked + &__label
38 | cursor: default
39 | z-index: 2
40 |
41 | &.is-disabled
42 | opacity: .4
43 |
44 |
45 | // Default skin
46 |
47 | modules_default_skin ?= true
48 | switcher_default_skin ?= false
49 |
50 | if modules_default_skin or switcher_default_skin
51 | .switcher
52 | &__input:checked + &__label,
53 | &__input:not(:checked) + &__label
54 | height: 2em
55 | padding: .4em 1em .6em
56 | background: #f4f4f4
57 | background: linear-gradient(to bottom, #fefefe, #f4f4f4)
58 | border: 1px solid #bbb
59 | line-height: 1
60 | color: #555
61 | transition: border-color .1s ease-in-out, box-shadow .1s ease-in-out
62 |
63 | &__label:nth-of-type(1)
64 | border-radius: form_border_radius 0 0 form_border_radius
65 | &__label:nth-last-of-type(1)
66 | border-radius: 0 form_border_radius form_border_radius 0
67 |
68 | &__input:checked + &__label
69 | background: #ccc
70 | background: linear-gradient(to bottom, #bbb, #eee)
71 | border-color: #999
72 | box-shadow: inset 0 .1em .2em black(.2)
73 | text-shadow: 0 1px 0 black(.1)
74 |
75 | &__input:focus + &__label
76 | border-color: form_focus_color
77 | box-shadow: 0 0 .4em rgba(form_focus_color, .75)
78 |
--------------------------------------------------------------------------------
/icons/sw-09.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
27 |
--------------------------------------------------------------------------------
/tamia/modules/select/index.styl:
--------------------------------------------------------------------------------
1 | // Tâmia © 2013 Artem Sapegin http://sapegin.me
2 | // Select with custom design
3 | // Dependencies: form
4 |
5 | // Bones
6 |
7 | .select
8 | no-select()
9 | position: relative
10 | display: block
11 | overflow: hidden
12 | font-size: 1em
13 | height: 2em;
14 |
15 | &_inline
16 | display: inline-block
17 |
18 | &__select
19 | position: absolute
20 | bottom: 0
21 | left: 0
22 | width: 100%
23 | opacity: 0
24 | z-index: 2
25 | cursor: default
26 | transform: scaleY(2) // Select height could be smaller than box height
27 | transform-origin: 0 100% // Fixes dropdown position on Windows
28 |
29 | &__box
30 | display: block
31 | vertical-align: middle
32 | line-height: 1
33 | white-space: nowrap
34 | overflow: hidden
35 | text-overflow: ellipsis
36 |
37 | &:after
38 | content: ""
39 | position: absolute
40 | top: 0
41 | bottom: 0
42 | right: 0
43 | z-index: 1
44 |
45 | &.is-disabled
46 | opacity: .4
47 |
48 |
49 | // Default skin
50 |
51 | modules_default_skin ?= true
52 | select_default_skin ?= false
53 |
54 | if modules_default_skin or select_default_skin
55 | .select
56 | border: 1px solid #bbb
57 | border-bottom-color: #aaa
58 | border-radius: form_border_radius
59 | transition: border-color .1s ease-in-out, box-shadow .1s ease-in-out
60 |
61 | &__box
62 | height: 2em
63 | padding: .45em 1.5em .5em .5em
64 | background: #f4f4f4
65 | background: linear-gradient(to bottom, #fefefe, #f4f4f4)
66 | line-height: 1
67 | color: #555
68 |
69 | &:after
70 | top: 50%
71 | right: .3em
72 | width: .8em
73 | height: .7em
74 | margin-top: -.3em
75 | background: embedurl("arrow.svg") no-repeat
76 | background-size: 100% 100%
77 | .no-svg &:after
78 | content: "▼"
79 | padding-right: .2em
80 | text-align: center
81 | vertical-align: middle
82 | font-size: 1em
83 | line-height: 2em
84 |
85 | &.is-focused
86 | border-color: form_focus_color
87 | box-shadow: 0 0 .4em rgba(form_focus_color, .75)
88 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | # React Weather App
2 |
3 | [](https://travis-ci.org/sapegin/react-weather)
4 |
5 | My [coding assignment](https://gist.github.com/TimBeyer/2565bd2b085fdb38000f) for [Delivery Hero](http://www.deliveryhero.com/).
6 |
7 | Try the [deployed app](http://sapegin.github.io/react-weather/) or read below hot to run it locally.
8 |
9 |
10 | ## How to run locally
11 |
12 | First checkout the repository (`$ git clone https://github.com/sapegin/react-weather.git`) or download it as a [ZIP file](https://github.com/sapegin/react-weather/archive/master.zip).
13 |
14 | Then install dependencies:
15 |
16 | ```
17 | $ npm install
18 | ```
19 |
20 | And run development server:
21 |
22 | ```
23 | $ npm run server
24 | ```
25 |
26 | It’ll open the app in your browser.
27 |
28 |
29 | ## What’s inside
30 |
31 | ### App
32 |
33 | * [React](http://facebook.github.io/react/).
34 | * [alt](https://github.com/goatslacker/alt) — a simple Flux implementation: stores, actions, etc.
35 | * [window.fetch polyfill](https://github.com/github/fetch).
36 | * [Moment.js](http://momentjs.com/).
37 | * [bem-cn](https://github.com/albburtsev/bem-cn) — BEM-style class name generator.
38 |
39 | ### Styles
40 |
41 | * [Stylus](http://learnboost.github.io/stylus/).
42 | * [Autoprefixer](https://github.com/postcss/autoprefixer) (via [Stylobuild](https://github.com/kizu/stylobuild) Stylus plugin).
43 | * [Tâmia](http://tamiadev.github.io/tamia/) — my front-end framework.
44 |
45 | ### Build
46 |
47 | * [Gulp](http://gulpjs.com/).
48 | * [Bower](http://bower.io/).
49 | * [Webpack](http://webpack.github.io/).
50 | * [Babel](http://babeljs.io/) (as a [Webpack loader](https://github.com/babel/babel-loader)) — ES6/JSX trasformation.
51 |
52 | ### Debugging
53 |
54 | * [BrowserSync](http://www.browsersync.io/).
55 | * [eslint](http://eslint.org/).
56 |
57 |
58 | ## Author
59 |
60 | * [Artem Sapegin](http://sapegin.me/)
61 |
62 |
63 | ---
64 |
65 | ## License
66 |
67 | The MIT License, see the included [License.md](License.md) file.
68 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true
5 | },
6 | "ecmaFeatures": {
7 | "modules": true,
8 | "jsx": true
9 | },
10 | "rules": {
11 | "no-alert": 2,
12 | "no-array-constructor": 2,
13 | "no-caller": 2,
14 | "no-bitwise": 0,
15 | "no-catch-shadow": 2,
16 | "no-console": 2,
17 | "no-comma-dangle": 2,
18 | "no-control-regex": 2,
19 | "no-debugger": 2,
20 | "no-div-regex": 2,
21 | "no-dupe-keys": 2,
22 | "no-else-return": 0,
23 | "no-empty": 2,
24 | "no-empty-class": 2,
25 | "no-eq-null": 2,
26 | "no-eval": 2,
27 | "no-ex-assign": 2,
28 | "no-func-assign": 0,
29 | "no-floating-decimal": 2,
30 | "no-implied-eval": 2,
31 | "no-with": 2,
32 | "no-fallthrough": 2,
33 | "no-unreachable": 2,
34 | "no-undef": 2,
35 | "no-undef-init": 2,
36 | "no-unused-expressions": 2,
37 | "no-octal": 2,
38 | "no-octal-escape": 2,
39 | "no-obj-calls": 2,
40 | "no-multi-str": 2,
41 | "no-new-wrappers": 2,
42 | "no-new": 2,
43 | "no-new-func": 2,
44 | "no-native-reassign": 2,
45 | "no-plusplus": 0,
46 | "no-delete-var": 2,
47 | "no-return-assign": 2,
48 | "no-new-object": 2,
49 | "no-label-var": 2,
50 | "no-ternary": 0,
51 | "no-self-compare": 2,
52 | "no-sync": 2,
53 | "no-underscore-dangle": 0,
54 | "no-loop-func": 2,
55 | "no-empty-label": 2,
56 | "no-unused-vars": 1,
57 | "no-script-url": 2,
58 | "no-proto": 2,
59 | "no-iterator": 2,
60 | "no-mixed-requires": [0, false],
61 | "no-wrap-func": 2,
62 | "no-shadow": 2,
63 | "no-use-before-define": 0,
64 | "no-redeclare": 2,
65 | "no-regex-spaces": 2,
66 | "brace-style": 0,
67 | "block-scoped-var": 0,
68 | "camelcase": 2,
69 | "complexity": [0, 11],
70 | "consistent-this": [0, "that"],
71 | "curly": 2,
72 | "dot-notation": [0, {"allowKeywords": false}],
73 | "eqeqeq": 2,
74 | "guard-for-in": 0,
75 | "max-depth": [0, 4],
76 | "max-len": [0, 80, 4],
77 | "max-params": [0, 3],
78 | "max-statements": [0, 10],
79 | "new-cap": 2,
80 | "new-parens": 2,
81 | "one-var": 0,
82 | "quotes": [2, "single"],
83 | "quote-props": 0,
84 | "radix": 0,
85 | "semi": 2,
86 | "strict": 2,
87 | "unnecessary-strict": 0,
88 | "global-strict": 0,
89 | "use-isnan": 2,
90 | "valid-typeof": 0,
91 | "wrap-iife": 2,
92 | "wrap-regex": 0,
93 | "no-irregular-whitespace": 0
94 | },
95 | "globals": {
96 | "React": false
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/icons/sw-01.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
29 |
--------------------------------------------------------------------------------
/icons/sw-02.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
29 |
--------------------------------------------------------------------------------
/tamia/modules/richtypo/index.styl:
--------------------------------------------------------------------------------
1 | // Tâmia © 2015 Artem Sapegin http://sapegin.me
2 | // Rich typograhy
3 | //
4 | // Syntax for OpenType features in CSS:
5 | // http://help.typekit.com/customer/portal/articles/1789736
6 | // Death to typewriters by Medium. Technical supplement:
7 | // https://medium.com/@mwichary/death-to-typewriters-technical-supplement-8f3c754626f2
8 |
9 | // Enables ligatures on body (just headers otherwise).
10 | richtypo_global_ligatures ?= true
11 |
12 | // Extra OpenType features, eg. "ss01", "ss03", "salt" (richtypo_global_ligatures should be enabled).
13 | richtypo_extra_features ?= false
14 |
15 | // Figures type: proportional, oldstyle (richtypo_global_ligatures should be enabled).
16 | richtypo_figures ?= "proportional"
17 |
18 | // Enables small caps in abbreviations.
19 | richtypo_proper_abbr ?= true
20 |
21 | _ligatures = common-ligatures discretionary-ligatures contextual historical-ligatures
22 | _base_features = "kern", "liga", "dlig", "hlig", "clig"
23 | if richtypo_extra_features
24 | _base_features = _base_features, richtypo_extra_features
25 | if richtypo_figures == "proportional"
26 | _base_features = _base_features, "pnum"
27 | _figures_variant = "proportional-nums"
28 | else if richtypo_figures == "oldstyle"
29 | _base_features = _base_features, "onum"
30 | _figures_variant = "oldstyle-nums"
31 |
32 | // Global font settings.
33 | if richtypo_global_ligatures
34 | body
35 | font-kerning: normal
36 | font-variant-numeric: _figures_variant
37 | font-variant-ligatures: _ligatures
38 | font-feature-settings: _base_features
39 |
40 | .table,
41 | code,
42 | pre
43 | font-variant-numeric: tabular-nums
44 | font-variant-ligatures: none
45 | font-feature-settings: "thum"
46 |
47 | // Abbreviations with spacing.
48 | .abbr,
49 | .text abbr
50 | letter-spacing: 0.1em
51 | margin-right: -0.1em
52 | if richtypo_proper_abbr
53 | font-variant-caps: all-small-caps
54 | font-feature-settings: _base_features, "c2sc", "smcp"
55 |
56 | // The best ampersand.
57 | .amp
58 | font-family: Baskerville, Constantia, Palatino, "Palatino Linotype", "Book Antiqua", serif
59 | font-style: italic
60 |
61 | // Hanging punctuation.
62 | .sbrace
63 | margin-right: 0.3em
64 | .hbrace
65 | margin-left: -0.3em
66 | .slaquo
67 | margin-right: 0.42em
68 | .hlaquo
69 | margin-left: -0.42em
70 |
71 | // Headings with ligatures.
72 | if not richtypo_global_ligatures
73 | .text h1, .alpha,
74 | .text h2, .beta,
75 | .text h3, .gamma,
76 | .text h4, .delta,
77 | .text h5, .epsilon,
78 | .text h6, .zeta
79 | font-variant-ligatures: _ligatures
80 | font-feature-settings: _base_features
81 |
--------------------------------------------------------------------------------
/tamia/modules/password/script.js:
--------------------------------------------------------------------------------
1 | // Tâmia © 2014 Artem Sapegin http://sapegin.me
2 | // Password field with toggle to show characters
3 |
4 | /*global tamia:false*/
5 | ;(function(window, $, undefined) {
6 | 'use strict';
7 |
8 | var _unlockedState = 'unlocked';
9 | var _disabledState = 'disabled';
10 | var _inputSyncEvent = 'input.sync.password';
11 |
12 | tamia.Password = tamia.extend(tamia.Component, {
13 | displayName: 'tamia.Password',
14 | binded: 'toggle focus',
15 | template: {
16 | block: 'password',
17 | node: 'root',
18 | content: [
19 | {
20 | block: 'password',
21 | elem: 'toggle',
22 | link: 'toggleElem'
23 | },
24 | {
25 | block: 'password',
26 | elem: 'field',
27 | mix: {
28 | block: 'field'
29 | },
30 | node: '.js-field',
31 | link: 'fieldElem',
32 | attrs: {
33 | autocapitalize: 'off',
34 | autocomplete: 'off',
35 | autocorrect: 'off',
36 | spellcheck: 'false'
37 | }
38 | }
39 | ]
40 | },
41 |
42 | init: function() {
43 | // Mousedown instead of click to catch focused field
44 | this.toggleElem.on('mousedown', this.toggle_);
45 |
46 | if (this.elem.hasState(_disabledState)) {
47 | this.fieldElem.prop(_disabledState, true);
48 | }
49 | },
50 |
51 | toggle: function() {
52 | var focused = document.activeElement === this.fieldElem[0];
53 | var locked = !this.isLocked();
54 |
55 | this.elem.toggleState(_unlockedState);
56 |
57 | // Create hidden input[type=password] element to invoke password saving in browser
58 | if (!locked) {
59 | this.cloneElem = this.fieldElem.clone();
60 | this.cloneElem.hide();
61 | this.fieldElem.after(this.cloneElem);
62 | this.fieldElem.name = '';
63 | this.fieldElem.on(_inputSyncEvent, this.syncWith.bind(this, this.cloneElem));
64 | this.cloneElem.on(_inputSyncEvent, this.syncWith.bind(this, this.fieldElem));
65 | }
66 | else if (this.cloneElem) {
67 | this.fieldElem.off(_inputSyncEvent);
68 | this.fieldElem.name = this.cloneElem.name;
69 | this.cloneElem.remove();
70 | }
71 |
72 | this.fieldElem.attr('type', locked ? 'password' : 'text');
73 |
74 | if (focused) {
75 | setTimeout(this.focus_, 0);
76 | }
77 | },
78 |
79 | focus: function() {
80 | this.fieldElem.focus();
81 | },
82 |
83 | isLocked: function() {
84 | return !this.elem.hasState(_unlockedState);
85 | },
86 |
87 | syncWith: function(receiver, event) {
88 | receiver.val(event.target.value);
89 | }
90 | });
91 |
92 | tamia.initComponents({password: tamia.Password});
93 |
94 | }(window, jQuery));
95 |
--------------------------------------------------------------------------------
/tamia/modules/checkbox/index.styl:
--------------------------------------------------------------------------------
1 | // Tâmia © 2013 Artem Sapegin http://sapegin.me
2 | // Checkbox and radio button
3 | // Requires Modernizr.svg
4 | // Dependencies: form
5 |
6 | // Bones
7 |
8 | .checkbox,
9 | .radio
10 | white-space: nowrap
11 |
12 | &__button
13 | // For prehistoric browsers
14 | display: none
15 |
16 | &__text
17 | display: inline-block
18 | white-space: normal
19 |
20 | &__button,
21 | &__text
22 | vertical-align: middle
23 |
24 |
25 | &__input:checked,
26 | &__input:not(:checked)
27 | // Input should be hidden but focusable
28 | // display:none and width=height=0 don’t work here
29 | position: absolute
30 | opacity: 0
31 | overflow: hidden
32 | height: 1px
33 | width: 1px
34 | padding: 0
35 | border: 0
36 |
37 | &__input:checked + &__label &__button,
38 | &__input:not(:checked) + &__label &__button
39 | display: inline-block
40 |
41 | &__input:checked + &__label &__button
42 | position: relative
43 | &:before
44 | content: ""
45 | position: absolute
46 | line-height: 1
47 |
48 | &.is-disabled &__input + &__label
49 | opacity: .4
50 |
51 |
52 | // Default skin
53 |
54 | modules_default_skin ?= true
55 | checkbox_default_skin ?= false
56 |
57 | if modules_default_skin or checkbox_default_skin
58 |
59 | .checkbox,
60 | .radio
61 | &__button,
62 | &__text
63 | line-height: 1.8
64 | font-size: 1em
65 |
66 | &__input:checked + &__label &__button,
67 | &__input:not(:checked) + &__label &__button
68 | width: size = 1em
69 | height: size
70 | margin-top: -.15em
71 | margin-right: .15em
72 | border: 1px solid #bbb
73 | border-radius: form_border_radius
74 | box-shadow: inset 0 .1em .2em black(.1)
75 |
76 | &__input:checked + &__label &__button
77 | &:before
78 | background-size: 100% 100%
79 | background-repeat: no-repeat
80 |
81 | &__input:enabled:active + &__label &__button,
82 | &__input:focus + &__label &__button
83 | background: #f4f4f4
84 |
85 | .checkbox
86 | &__input:checked + &__label &__button
87 | &:before
88 | center(.8em, .65em)
89 | background-image: embedurl("check.svg")
90 | .no-svg &:before
91 | margin-top:-.5em;
92 | font-size: 0.8em
93 | font-weight:bold;
94 | color: #444
95 | content: "✓"
96 |
97 | .radio
98 | &__input:checked + &__label &__button,
99 | &__input:not(:checked) + &__label &__button
100 | border-radius: 50%
101 |
102 | &__input:checked + &__label &__button
103 | &:before
104 | center(.5em)
105 | background-image: embedurl("radio.svg")
106 | .no-svg &:before
107 | background: #444
108 | border-radius: 50%
109 |
--------------------------------------------------------------------------------
/icons/sw-50.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
34 |
--------------------------------------------------------------------------------
/app/utils/weatherdata.js:
--------------------------------------------------------------------------------
1 | // Author: Artem Sapegin, http://sapegin.me, 2015
2 |
3 | 'use strict';
4 |
5 | import 'fetch';
6 |
7 | const UNICODE_MINUS = '\u2212';
8 | const RETRY_INTERVAL = 5000;
9 | const APPID = 'b1b15e88fa797225412429c1c50c122a';
10 |
11 | class WeatherData {
12 | constructor() {
13 | this._cities = [];
14 | }
15 |
16 | subscribe(name, interval, onchange) {
17 | let cityId = this._cities.length;
18 | let city = {
19 | id: cityId,
20 | name: name,
21 | interval: interval,
22 | onchange: onchange
23 | };
24 | this._cities.push(city);
25 | this._fetch(cityId);
26 | }
27 |
28 | unsubscribe(name) {
29 | let cityId = this._getIdByName(name);
30 | let city = this._getCityById(cityId);
31 | if (city.timer) {
32 | clearTimeout(city.timer);
33 | }
34 | this._cities[city.id] = null;
35 | }
36 |
37 | _getCityById(cityId) {
38 | return this._cities[cityId];
39 | }
40 |
41 | _getIdByName(name) {
42 | for (let city of this._cities) {
43 | if (city && city.name === name) {
44 | return city.id;
45 | }
46 | }
47 | }
48 |
49 | _fetch(cityId) {
50 | let city = this._getCityById(cityId);
51 | fetch(this._getApiUrl(city.name))
52 | .then((res) => {
53 | return res.json();
54 | })
55 | .then((json) => {
56 | let response = this._parseResponse(json);
57 | this._emitChange(cityId, response);
58 | if (!response.error) {
59 | this._schedule(cityId);
60 | }
61 | })
62 | .catch(() => {
63 | this._emitError(cityId, 'Data cannot be loaded');
64 | this._schedule(cityId, RETRY_INTERVAL);
65 | });
66 | }
67 |
68 | _schedule(cityId, interval) {
69 | let city = this._getCityById(cityId);
70 | city.timer = setTimeout(this._fetch.bind(this, cityId), interval || city.interval);
71 | }
72 |
73 | _getApiUrl(name) {
74 | name = encodeURIComponent(name);
75 | return `http://api.openweathermap.org/data/2.5/weather?units=metric&q=${name}&appid=${APPID}`;
76 | }
77 |
78 | _parseResponse(json) {
79 | if (json.cod === '404') {
80 | return {error: 'City not found.'};
81 | }
82 |
83 | return {
84 | temp: this._formatNumber(json.main.temp),
85 | humidity: json.main.humidity,
86 | sunrise: json.sys.sunrise,
87 | sunset: json.sys.sunset,
88 | windSpeed: json.wind.speed,
89 | conditions: json.weather[0].main,
90 | icon: json.weather[0].icon.replace(/[dn]$/, '')
91 | };
92 | }
93 |
94 | _formatNumber(number) {
95 | return String(Math.round(number)).replace(/^\-/, UNICODE_MINUS);
96 | }
97 |
98 | _emitChange(cityId, data) {
99 | let city = this._getCityById(cityId);
100 | if (city && city.onchange) {
101 | city.onchange(data);
102 | }
103 | }
104 |
105 | _emitError(cityId, message) {
106 | this._emitChange(cityId, {error: message});
107 | }
108 | }
109 |
110 | export default new WeatherData();
111 |
--------------------------------------------------------------------------------
/tamia/tamia/images.styl:
--------------------------------------------------------------------------------
1 | // Tâmia © 2015 Artem Sapegin http://sapegin.me
2 | // Images, sprites, Retina support
3 |
4 | //
5 | // Configuration
6 | //
7 |
8 | // Enable Retina support (default: false).
9 | retinafy ?= false
10 |
11 | // Path to calculate file system image path based on CSS path (default: .).
12 | images_relative_root ?= '.'
13 |
14 | // Sprite fingerprint, string that will be appended to URL to flush browser cache.
15 | sprite_fingerprint ?= null
16 |
17 | // Default sprite image file (default: ../build/sprite.png).
18 | // Sprite should be generated by [grunt-tamia-sprite](https://github.com/tamiadev/grunt-tamia-sprite).
19 | sprite_image ?= '../build/sprite.png'
20 | sprite_image = sprite_image + '?' + sprite_fingerprint if sprite_fingerprint
21 |
22 |
23 | //
24 | // Functions
25 | //
26 |
27 | // background-image with Retina variant.
28 | //
29 | // path - Image file path.
30 | // x - background-position x (default: 0).
31 | // y - background-position y (default: 0).
32 | //
33 | // Retina image file should be named path@2x.png.
34 | image(path, x=0, y=0)
35 | if retinafy
36 | background: url(path) x y
37 | +retina()
38 | ext = extname(path)
39 | hdpath = dirname(path) + '/' + basename(path, ext) + '@2x' + ext
40 | background: url(hdpath) x y
41 | background-size: image-size(pathjoin(images_relative_root, path))
42 | else
43 | background: url(path) x y
44 |
45 |
46 | // Sprite (to use in pseudo element).
47 | //
48 | // img - Sprite image variable.
49 | //
50 | // Example:
51 | //
52 | // .elem
53 | // sprite(sprite_pony);
54 | sprite(img)
55 | if not @content and match(":(before|after)", selector())
56 | content: ""
57 | if not @display
58 | display: inline-block
59 | image(sprite_image, img[0], img[1])
60 | width: img[2]
61 | height: img[3]
62 |
63 | // background-position for sprite.
64 | //
65 | // img - Sprite image variable.
66 | //
67 | // Example:
68 | //
69 | // .elem
70 | // sprite-bg()
71 | // sprite-pos(sprite_pony-hover)
72 | sprite-pos(img)
73 | background-position: img[0] img[1]
74 |
75 | // background-image for sprite.
76 | sprite-bg()
77 | image(sprite_image)
78 |
79 | // Element width for sprite.
80 | //
81 | // img - Sprite image variable.
82 | //
83 | // Returns pixels
84 | sprite-width(img)
85 | img[2]
86 |
87 | // Element height for sprite.
88 | //
89 | // img - Sprite image variable.
90 | //
91 | // Returns pixels
92 | sprite-height(img)
93 | img[3]
94 |
95 | // Element width/height for sprite.
96 | //
97 | // img - Sprite image variable.
98 | //
99 | // Example:
100 | //
101 | // .elem
102 | // sprite-size(sprite_pony);
103 | sprite-size(img)
104 | width: sprite-width(img)
105 | height: sprite-height(img)
106 |
107 | // Shifts element half height to top.
108 | //
109 | // img - Sprite image variable.
110 | sprite-shift-top(img)
111 | margin-top: (-(round(sprite-height(img)/2)))
112 |
--------------------------------------------------------------------------------
/tamia/modules/toggle/Readme.md:
--------------------------------------------------------------------------------
1 | # Toggle ß
2 |
3 | Magic content toggler.
4 |
5 |
6 | ## Markup
7 |
8 |
28 |
29 |
30 | ## States
31 |
32 | There are two kind of states:
33 |
34 | * Wrapper states: define different look of your UI. You can switch wrapper sates with togglers (see below).
35 | * Element states: define look of particular elements (hidden, disabled, etc.) as a reaction for wrapper state switching.
36 |
37 | For every wrapper state you could define which element states should be added or removed.
38 |
39 | For example this element will be hidden at `restore` state:
40 |
41 |
Log in
42 |
43 | But this one will be visible:
44 |
45 |
Restore
46 |
47 | You could react to or change any number of states:
48 |
49 |
50 |
51 | You should set initial states for wrapper (`
`) and elements (`
Restore
`) in your markup.
52 |
53 | You can use wrapper state classes (`.state-restore`) to add custom CSS.
54 |
55 |
56 | ### Elements States
57 |
58 | Basically element state is an `.is-statename` CSS class. But some states imply additional magic:
59 |
60 | * `hidden` will also trigger `appear.tamia`/`disappear.tamia` events.
61 | * `disabled` will also add/remove `disabled` attribute.
62 |
63 |
64 | ## Togglers
65 |
66 | You can toggle state with a special link:
67 |
68 |
Toggle
69 |
70 | Or several states at once:
71 |
72 |
Toggle
73 |
74 |
75 | ## Transitions
76 |
77 | By default element states change with fade transition (crossfade when necessary) but you can use different transition:
78 |
79 |
80 |
81 |
82 | ### Available Transitions
83 |
84 | * `toggle-transition-slide`: kind of slide transition (unfinished).
85 |
86 |
87 | ## Configuration
88 |
89 | ### toggle_transition_time
90 |
91 | Type: CSS duration value.
92 |
93 | Duration of appearing/disappearing animation.
94 |
--------------------------------------------------------------------------------
/app/components/weatherwidget.js:
--------------------------------------------------------------------------------
1 | // Author: Artem Sapegin, http://sapegin.me, 2015
2 |
3 | 'use strict';
4 |
5 | import moment from 'moment';
6 | import Block from 'bem-cn';
7 | import LocationActions from '../actions/locationactions';
8 | import WeatherData from '../utils/weatherdata';
9 | import WeatherIcon from './weathericon';
10 |
11 | let b = new Block('weather-widget');
12 |
13 | export default React.createClass({
14 | displayName: 'WeatherWidget',
15 | propTypes: {
16 | location: React.PropTypes.object.isRequired
17 | },
18 |
19 | getInitialState: function() {
20 | return {
21 | loading: true
22 | };
23 | },
24 |
25 | componentDidMount: function() {
26 | WeatherData.subscribe(this.props.location.name, this.props.location.interval, this._onChange);
27 | },
28 |
29 | componentWillUnmount: function() {
30 | WeatherData.unsubscribe(this.props.location.name);
31 | },
32 |
33 | _onChange: function(state) {
34 | this.setState({
35 | loading: false,
36 | weather: state
37 | });
38 | },
39 |
40 | _handleRemove: function() {
41 | LocationActions.remove(this.props.location.id);
42 | },
43 |
44 | _splitLocation: function(location) {
45 | location = location.split(/,\s*/);
46 | return {
47 | city: location[0],
48 | country: location[1] || ''
49 | };
50 | },
51 |
52 | _formatTime: function(timestamp) {
53 | return moment.unix(timestamp).format('H:mm');
54 | },
55 |
56 | render: function() {
57 | let { city, country } = this._splitLocation(this.props.location.name);
58 | return (
59 |
60 |
61 |
62 |
{city}
63 |
{country}
64 |
65 | {this.renderContents()}
66 |
67 | );
68 | },
69 |
70 | renderContents: function() {
71 | if (this.state.loading) {
72 | return this.renderSpinner();
73 | }
74 | if (this.state.weather.error) {
75 | return this.renderError();
76 | }
77 | else {
78 | return this.renderWeather();
79 | }
80 | },
81 |
82 | renderSpinner: function() {
83 | return (
84 |
87 | );
88 | },
89 |
90 | renderError: function() {
91 | return (
92 |
{this.state.weather.error}
93 | );
94 | },
95 |
96 | renderWeather: function() {
97 | let weather = this.state.weather;
98 | return (
99 |
100 |
{weather.temp}˚
101 |
102 |
103 |
104 |
105 |
Humidity: {weather.humidity}%
106 |
Wind: {weather.windSpeed} mps
107 |
Sunrise: {this._formatTime(weather.sunrise)}
108 |
Sunset: {this._formatTime(weather.sunset)}
109 |
110 |
111 | );
112 | }
113 | });
114 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp');
2 | var gutil = require('gulp-util');
3 | var plumber = require('gulp-plumber');
4 | var stylus = require('gulp-stylus');
5 | var copy = require('gulp-copy');
6 | var ghPages = require('gulp-gh-pages');
7 | var webpack = require('webpack');
8 | var notifier = require('node-notifier');
9 | var browserSync = require('browser-sync');
10 | var stylobuild = require('stylobuild');
11 |
12 | var isDevelopment = process.env.NODE_ENV !== 'production';
13 | var isWatch = false;
14 |
15 | function reload() {
16 | if (isDevelopment && isWatch) {
17 | return browserSync.reload.apply(browserSync, arguments);
18 | }
19 | else {
20 | return gutil.noop();
21 | }
22 | }
23 |
24 | function notifyError(plugin, message, filepath, filename) {
25 | console.log(gutil.colors.red.bold('\nError in ' + (filename || filepath) + '\n'));
26 | console.log(message);
27 | if (isWatch) {
28 | notifier.notify({
29 | title: '\uD83D\uDEA8 ' + plugin + ' error',
30 | message: gutil.colors.stripColor(message).replace(/ +/g, ' '),
31 | open: 'file://' + filepath
32 | });
33 | }
34 | else {
35 | process.exit(1);
36 | }
37 | }
38 |
39 | gulp.task('webpack', function() {
40 | webpack(require('./webpack.config.js'), function(err, stats) {
41 | var error, message;
42 | if (stats.hasErrors()) {
43 | error = stats.compilation.errors[0];
44 | message = error.error;
45 | }
46 | else if (stats.hasWarnings()) {
47 | error = stats.compilation.warnings[0];
48 | message = error.warning;
49 | }
50 | if (error) {
51 | notifyError('Webpack', message.toString(), error.module.resource, error.module.rawRequest);
52 | }
53 | else {
54 | reload();
55 | }
56 | });
57 | });
58 |
59 | gulp.task('styles', function() {
60 | return gulp.src('styles/index.styl')
61 | .pipe(plumber({errorHandler: function(error) {
62 | notifyError('Stylus', error.message.replace(/.*\n/, ''), error.filename);
63 | this.emit('end');
64 | }}))
65 | .pipe(stylus({
66 | url: {
67 | name: 'embedurl'
68 | },
69 | define: {
70 | DEBUG: isDevelopment
71 | },
72 | paths: [
73 | 'tamia'
74 | ],
75 | use: [
76 | stylobuild({
77 | autoprefixer: {
78 | browsers: 'last 2 versions, ie 9'
79 | },
80 | minifier: isDevelopment ? false : 'cleancss',
81 | pixrem: false
82 | })
83 | ]
84 | }))
85 | .pipe(gulp.dest('build/'))
86 | .pipe(reload({stream: true}));
87 | });
88 |
89 | gulp.task('watch', ['webpack', 'styles'], function() {
90 | isWatch = true;
91 | if (isDevelopment) {
92 | browserSync({
93 | notify: false,
94 | online: false,
95 | server: '.'
96 | });
97 | }
98 | gulp.watch(['app/**/*.{js,jsx}'], ['webpack']);
99 | gulp.watch(['styles/**/*.styl'], ['styles']);
100 | });
101 |
102 | gulp.task('make-dist', function() {
103 | return gulp.src([
104 | 'build/**/*',
105 | 'icons/*.svg',
106 | 'index.html'
107 | ])
108 | .pipe(copy('dist'));
109 | });
110 |
111 | gulp.task('gh-deploy', function() {
112 | return gulp.src('dist/**/*')
113 | .pipe(ghPages());
114 | });
115 |
116 | gulp.task('default', ['webpack', 'styles']);
117 | gulp.task('deploy', ['webpack', 'styles', 'make-dist', 'gh-deploy']);
118 |
--------------------------------------------------------------------------------
/tamia/modules/form/Readme.md:
--------------------------------------------------------------------------------
1 | # Form
2 |
3 | Basic form controls: inputs, textareas, buttons, form layouts and helpers.
4 |
5 |
6 | ## Markup
7 |
8 | Controls:
9 |
10 |
11 |
12 |
13 |
14 | Block controls:
15 |
16 |
17 |
18 |
19 | Field with unit:
20 |
21 |
22 |
23 |
24 |
25 |
26 | Layout:
27 |
28 |
43 |
44 | Messages:
45 |
46 |
Thank you!
47 |
Are you sure?
48 |
Not enough cheese
49 |
50 |
51 | ## States
52 |
53 | ### .is-disabled
54 |
55 | Disabled state of a control. Should be combined with `disabled` attribute where appropriate.
56 |
57 | ### .is-success / .is-error
58 |
59 | Shows/hides success/error messages:
60 |
61 |
65 |
66 |
67 | ## Events
68 |
69 | ### enable.form.tamia / disable.form.tamia
70 |
71 | Enables / disables all descendant form elements (they should have classes `.field`, `.button` or `.disablable`).
72 |
73 | ### lock.form.tamia / unlock.form.tamia
74 |
75 | Disables / enables submit button of a form.
76 |
77 |
78 | ## Ajax forms
79 |
80 | You can convert any form to Ajax form:
81 |
82 |
87 |
88 | You can subscribe to events `send.form.tamia`, `success.form.tamia` and `success.form.tamia`:
89 |
90 | form.on({
91 | 'send.form.tamia': function(event, fields) {
92 | event.preventDefault(); // Cancel form sending
93 | },
94 | 'success.form.tamia': function(event, data) {
95 | // Server should return data.result = 'success'
96 | return 'Error message.';
97 | },
98 | 'error.form.tamia': function(event, data) {
99 | return 'Success message.';
100 | }
101 | });
102 |
103 | Or use states `.is-success`, `.is-error` and `.is-sending` to check form status.
104 |
105 |
106 | ## Auto disable submit button
107 |
108 | Disable submit button on form submit:
109 |
110 |
34 | */
35 | function Component(elem) {
36 | if (!elem || elem.nodeType !== 1) throw new ReferenceError('No DOM node passed to Component constructor.');
37 |
38 | // Bind methods to `this`
39 | if (this.binded) {
40 | if (typeof this.binded === 'string') this.binded = this.binded.split(' ');
41 | this.bindAll.apply(this, this.binded);
42 | }
43 |
44 | this.elemNode = elem;
45 | this.elem = $(elem);
46 | this.initializable = this.isInitializable();
47 | if (!this.initializable) return;
48 |
49 | if (this.isSupported()) {
50 | this.handlers = {};
51 | this.initHtml();
52 | this.init();
53 | this.elem.addState('ok');
54 | }
55 | else {
56 | this.fallback();
57 | this.elem.addState('unsupported');
58 | }
59 | }
60 |
61 | Component.prototype = {
62 | __tamia_cmpnt__: true,
63 | displayName: 'tamia.Component',
64 |
65 | /**
66 | * List of methods that should be binded to `this` (see `bindAll` method).
67 | *
68 | * @type {String|Array}
69 | */
70 | binded: null,
71 |
72 | /**
73 | * Component’s OPORJSON template (see `initHtml` method).
74 | */
75 | template: null,
76 |
77 | /**
78 | * Put all your initialization code in this method.
79 | */
80 | init: function() {
81 | // Should be implemented
82 | },
83 |
84 | /**
85 | * You can implement this method to do destroy component.
86 | */
87 | destroy: function() {
88 | // Could be implemented
89 | },
90 |
91 | /**
92 | * Implement this method if you want to check whether browser is good for your component or not.
93 | *
94 | * @return {Boolean}
95 | */
96 | isSupported: function() {
97 | return true;
98 | },
99 |
100 | /**
101 | * Implement this method if you want to check whether component could be initialized.
102 | *
103 | * Example:
104 | *
105 | * isInitializable: function() {
106 | * // Do not initialize component if it's not visible
107 | * return this.isVisible();
108 | * }
109 | *
110 | * @return {Boolean}
111 | */
112 | isInitializable: function() {
113 | return true;
114 | },
115 |
116 | /**
117 | * Implement this method to do some fallbacks. It will be called if isSupported() returns false.
118 | */
119 | fallback: function() {
120 | // Could be implemented
121 | },
122 |
123 | /**
124 | * Initialize HTML using OPORJSON stored in `this.template`.
125 | */
126 | initHtml: function() {
127 | if (!this.template) return;
128 | if (DEBUG && !tamia.oporNode) throw new tamia.Error('Component.initHtml: Tâmia OPOR API not found. Please include tamia/tamia/opor.js.');
129 | var opor = tamia.oporNode(this.template, {
130 | root: this.elem
131 | });
132 | $.extend(this, opor.data('links'));
133 | },
134 |
135 | /**
136 | * Binds all specified methods to this. Binded method names have `_` at the end.
137 | *
138 | * @param {String} method1... Method names
139 | *
140 | * Example:
141 | *
142 | * this.bindAll('toggle');
143 | * this.elem.on('click', this.toggle_);
144 | */
145 | bindAll: function() {
146 | if (arguments.length === 0) throw new tamia.Error('Component.bindAll: no method names passed.');
147 | for (var funcIdx = 0; funcIdx < arguments.length; funcIdx++) {
148 | var func = arguments[funcIdx];
149 | if (DEBUG && !this[func] || !$.isFunction(this[func])) throw new tamia.Error('Component.bindAll: method ' + func + ' not exists or not a function.');
150 | this[func + '_'] = this[func].bind(this);
151 | }
152 | },
153 |
154 | /**
155 | * Returns component visibility.
156 | *
157 | * @return {Boolean}
158 | */
159 | isVisible: function() {
160 | return !!(this.elemNode.offsetWidth || this.elemNode.offsetHeight);
161 | }
162 | };
163 |
164 | tamia.Component = Component;
165 |
166 | }(window, jQuery));
167 |
--------------------------------------------------------------------------------
/tamia/modules/form/index.styl:
--------------------------------------------------------------------------------
1 | // Tâmia © 2013 Artem Sapegin http://sapegin.me
2 | // Basic form controls: inputs, textareas, buttons
3 |
4 | // Disabled form element
5 | .is-disabled
6 | cursor: default
7 | pointer-events: none
8 | text-shadow: none
9 |
10 |
11 | // Bones
12 |
13 | .field,
14 | .button
15 | display: inline-block
16 | vertical-align: middle
17 | font-size: 1em
18 | line-height: 1
19 | outline: 0
20 | transition: opacity .25 ease-out
21 |
22 | &_block
23 | display: block
24 | width: 100%
25 |
26 | &.is-disabled
27 | opacity: .4
28 |
29 | .field
30 | // Hide IE10 clear button
31 | &::-ms-clear
32 | size: 0 // Not display:none because: http://bit.ly/1h3UlAH
33 |
34 | .field_area
35 | resize: vertical // Vertical resizing for textareas
36 |
37 | .button
38 | no-select()
39 | position: relative // Fixes strange bugs in webkit
40 | text-decoration: none
41 | white-space: nowrap
42 | cursor: pointer
43 |
44 | // Fixing Mozilla's inner paddings
45 | // https://github.com/nanoblocks/nanoblocks/blob/gh-pages/blocks/button/button.styl
46 | &::-moz-focus-inner
47 | padding: 0
48 | border: none
49 |
50 | .button + .button,
51 | .field + .field,
52 | .field + .button,
53 | .button + .field
54 | margin-left: spacer
55 |
56 | .form
57 | &_block .field,
58 | &_block .button,
59 | &_block .select, // TODO: Move to select
60 | &_block .password // TODO: Move to password
61 | display: block
62 | width: 100%
63 |
64 | &__group
65 | space(2)
66 |
67 | &__row,
68 | &__row.is-transit
69 | display: flex
70 | space()
71 | &:last-child
72 | space(0)
73 |
74 | &__label
75 | width: 20%
76 | min-width: 6em
77 | max-width: 20em
78 | padding-right: spacer
79 | padding-top: .2em
80 | text-align: right
81 |
82 | &__widget
83 | flex: 1
84 | padding-left: spacer
85 | text-align: left
86 |
87 | .form__success,
88 | .form__error
89 | display: none
90 | &.is-success .form__success
91 | display: block
92 | &.is-error .form__error
93 | display: block
94 |
95 | .field-with-unit
96 | position: relative
97 | font-size: 1em
98 | line-height: 1.4
99 |
100 | &__field
101 | width: 100%
102 |
103 | &__unit
104 | position: absolute
105 | top: 0
106 | right: 0
107 |
108 | &_left &__unit
109 | right: auto
110 | left: 0
111 |
112 |
113 | // Close button
114 | .close
115 | line-height: 1
116 | outline: 0
117 |
118 | // Fix