├── .gitignore ├── HISTORY.md ├── README.md ├── examples ├── mobile │ ├── .gitignore │ ├── .meteor │ │ ├── .finished-upgraders │ │ ├── .gitignore │ │ ├── .id │ │ ├── packages │ │ ├── platforms │ │ ├── release │ │ └── versions │ └── client │ │ ├── main.coffee │ │ ├── main.html │ │ └── style │ │ ├── 0global.coffee │ │ ├── buttons.coffee │ │ ├── normalize.css │ │ └── text.coffee └── reactive │ ├── .gitignore │ ├── .meteor │ ├── .finished-upgraders │ ├── .gitignore │ ├── .id │ ├── packages │ ├── platforms │ ├── release │ └── versions │ └── client │ ├── main.coffee │ └── main.html ├── lib ├── css.coffee ├── helpers.coffee └── mixins.coffee └── package.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | .versions -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # 1.0.4 2 | 1.0.2 broke css::nested 3 | 4 | # 1.0.2 5 | 6 | Added support for `css.prototype.stop` to top autorun computations associated with that object. This should help with template styles. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reactive CSS 2 | 3 | This package allows you to define all your CSS rules in a Javascript or Coffeescript and with reactive bindings to Tracker-aware functions. 4 | 5 | Check out the [live demo](http://reactive-css.meteor.com). It demonstrates: 6 | 7 | - reactive-responsive deisgn: changes layout when window resizes 8 | - reactively updating color scheme 9 | - platform-specific css for Android vs iOS 10 | 11 | ## Getting Started 12 | 13 | meteor add ccorcos:reactive-css 14 | 15 | The API is highly flexible so choose whatever syntax works best for you. 16 | 17 | ### Basics 18 | 19 | The easiest way to get started is by defining nested rules they way you are probably comfortable with in whatever CSS preprocessing language you use. 20 | 21 | css 22 | '.page': 23 | '.nav': 24 | 'height': '90px' 25 | 'width': '100%' 26 | '.content': 27 | 'paddingTop': '90px' 28 | 'paddingBottom': '50px' 29 | 'color': 'blue' 30 | '&:hover': 31 | 'color': 'red' 32 | '.toolbar': 33 | 'height': '50px' 34 | 35 | 36 | The other way is more object oriented. 37 | 38 | page = css('.page') 39 | 40 | nav = page.child('.nav').height('90px').width('100%') 41 | 42 | content = page.child('.content') 43 | content.paddingTop('90px') 44 | content.paddingBottom('20px') 45 | content.color('blue') 46 | 47 | hoveredContent = content.also(':hover') 48 | hoveredContent.color('red') 49 | 50 | toolbar = page.child('.toolbar').height('50px') 51 | 52 | Every function returns `this` so you can chain them, or not. 53 | 54 | ### Units 55 | 56 | Units are handled in a few ways. For nested objects, it is sometimes convenient to leave everything as numbers so you can add and subtract them. Thus you can specify the unit by the postfix 2 letters in the CSS rule. For example: 57 | 58 | navHeightpx = 90 59 | toolbarHeightpx = 50 60 | css 61 | '.page': 62 | '.nav': 63 | 'heightpx': navHeightpx 64 | 'widthpc': 100 65 | '.content': 66 | 'paddingToppx': navHeightpx 67 | 'paddingBottompx': toolbarHeightpx 68 | 'color': 'blue' 69 | '&:hover': 70 | 'color': 'red' 71 | '.toolbar': 72 | 'heightpx': toolbarHeightpx 73 | 74 | Valid postfixes are 'px', 'pc', 'vh', 'vw', and 'em'. 75 | 76 | The object oriented way is to pass a second string for the units. 77 | 78 | page = css('.page') 79 | 80 | nav = page.child('.nav').height(navHeightpx, 'px').width(100, 'pc') 81 | 82 | content = page.child('.content') 83 | content.paddingTop(navHeightpx) 84 | content.paddingBottom(toolbarHeightpx) 85 | content.color('blue') 86 | 87 | hoveredContent = content.also(':hover') 88 | hoveredContent.color('red') 89 | 90 | toolbar = page.child('.toolbar').height(toolbarHeightpx) 91 | 92 | The defualt unit is 'px' so you don't necessarily have to specify it. 93 | 94 | ### Reactivity 95 | 96 | This package is "Tracker-aware". So if you pass a function, it will evaluate the function with `Tracker.autorun`. This allows you to reactively update CSS rules! Suppose you parameterize your whole app within a reactive dictionary: 97 | 98 | styles = new ReactiveDict() 99 | styles.set('primary', 'blue') 100 | styles.set('background', 'white') 101 | styles.set('navHeightpx', 90) 102 | styles.set('toolbarHeightpx', 50) 103 | 104 | Then for the nested object you could use: 105 | 106 | css 107 | '.page': 108 | '.nav': 109 | 'backgroundColor': -> styles.get('primary') 110 | 'heightpx': -> styles.get('navHeightpx') 111 | '.content': 112 | 'paddingToppx': -> styles.get('navHeightpx') 113 | 'paddingBottompx': -> styles.get('toolbarHeightpx') 114 | 'backgroundColor': -> styles.get('background') 115 | '.toolbar': 116 | 'backgroundColor': -> styles.get('primary') 117 | 'heightpx': -> styles.get('toolbarHeightpx') 118 | 119 | The object oriented way is the same idea, only the units will be the first arguement as opposed to the second. This makes your coffeescript a lot nicer :) 120 | 121 | page = css('.page') 122 | 123 | nav = page 124 | .child '.nav' 125 | .height 'px', -> styles.get('navHeightpx') 126 | .backgroundColor -> styles.get('primary') 127 | 128 | content = page 129 | .child '.content' 130 | .paddingTop 'px', -> styles.get('navHeightpx') 131 | .paddingBottom 'px', -> styles.get('toolbarHeightpx') 132 | .backgroundColor -> styles.get('background') 133 | 134 | toolbar = page 135 | .child '.toolbar' 136 | .height 'px', -> styles.get('toolbarHeightpx') 137 | .backgroundColor -> styles.get('primary') 138 | 139 | ### Mixins 140 | 141 | This library is clearly incomplete and it would be concenient if you could extend it nicely. Mixins attach to the css object AND the css prototype. Here's how you define one: 142 | 143 | css.mixin 'fullPage', -> 144 | position: 'absolute' 145 | top: 0 146 | bottom: 0 147 | left: 0 148 | right: 0 149 | 150 | css.mixin 'boxSizing', (args...) -> 151 | obj = {} 152 | value = args.join(' ') 153 | obj['boxSizing'] = value 154 | obj["Webkit"+capitalize('boxSizing')] = value 155 | obj["Moz"+capitalize('boxSizing')] = value 156 | obj["Ms"+capitalize('boxSizing')] = value 157 | return obj 158 | 159 | css.mixin 'borderRadius', (args...) -> 160 | obj = {} 161 | # args could be [10, 'em'] 162 | value = args.join(' ') 163 | obj['borderRadius'] = value 164 | obj["Webkit"+capitalize('borderRadius')] = value 165 | obj["Moz"+capitalize('borderRadius')] = value 166 | obj["Ms"+capitalize('borderRadius')] = value 167 | return obj 168 | 169 | This allows you to add vendor prefixes as you like as well as create convenient helpers which you could use in a few ways: 170 | 171 | css '*': css.boxSizing('border-box') 172 | 173 | css 174 | '.page': _.extend css.fullPage(), 175 | '.nav': 176 | 'backgroundColor': -> styles.get('primary') 177 | 'heightpx': -> styles.get('navHeightpx') 178 | 179 | page = css('.page').fullPage() 180 | 181 | Now, all that typing can be a pain, especially 'backgroundColor'. So there's an alias function to alias mixins. 182 | 183 | css.alias('backgroundColor', 'bg') 184 | 185 | Ahh... Much better. No help me expand this package! Or build a responsive framework using [reactive window size](https://github.com/gadicc/meteor-reactive-window)! Create different styles easily whether on Android or iOS. Or, as in the demo, create a rotating color scheme for your app! 186 | 187 | ## Pros and Cons 188 | 189 | There is obviously going to be a tradeoff here. So lets enumerate them. 190 | 191 | Pros 192 | 193 | - Create your CSS styles using a turing complete language you are familiar with. 194 | - Reactively update your CSS styles. 195 | 196 | Cons 197 | 198 | - Browsers cannot cache your CSS stylesheets. 199 | - `Tracker.autorun` everywhere, but wait, you don't have to use it. 200 | 201 | I think its worth it. I hate "battling the framework" when it comes to CSS. 202 | 203 | ## To Do 204 | - tests 205 | - template-specific css using `Template.name.css(...)`. 206 | - can we set inline styles using @find on created? 207 | - we also need to stop all the associated autorun methods -------------------------------------------------------------------------------- /examples/mobile/.gitignore: -------------------------------------------------------------------------------- 1 | packages/ 2 | -------------------------------------------------------------------------------- /examples/mobile/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | -------------------------------------------------------------------------------- /examples/mobile/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /examples/mobile/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 7qheeahbcy04m3kr1g 8 | -------------------------------------------------------------------------------- /examples/mobile/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | meteor-platform 8 | autopublish 9 | insecure 10 | coffeescript 11 | ccorcos:reactive-css 12 | pagebakers:ionicons 13 | d3js:d3 14 | -------------------------------------------------------------------------------- /examples/mobile/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /examples/mobile/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.0.3.1 2 | -------------------------------------------------------------------------------- /examples/mobile/.meteor/versions: -------------------------------------------------------------------------------- 1 | application-configuration@1.0.4 2 | autopublish@1.0.2 3 | autoupdate@1.1.5 4 | base64@1.0.2 5 | binary-heap@1.0.2 6 | blaze@2.0.4 7 | blaze-tools@1.0.2 8 | boilerplate-generator@1.0.2 9 | callback-hook@1.0.2 10 | ccorcos:reactive-css@1.0.4 11 | check@1.0.4 12 | coffeescript@1.0.5 13 | d3js:d3@3.5.5 14 | ddp@1.0.14 15 | deps@1.0.6 16 | ejson@1.0.5 17 | fastclick@1.0.2 18 | follower-livedata@1.0.3 19 | geojson-utils@1.0.2 20 | html-tools@1.0.3 21 | htmljs@1.0.3 22 | http@1.0.10 23 | id-map@1.0.2 24 | insecure@1.0.2 25 | jquery@1.11.3 26 | json@1.0.2 27 | launch-screen@1.0.1 28 | livedata@1.0.12 29 | logging@1.0.6 30 | meteor@1.1.4 31 | meteor-platform@1.2.1 32 | minifiers@1.1.3 33 | minimongo@1.0.6 34 | mobile-status-bar@1.0.2 35 | mongo@1.0.11 36 | observe-sequence@1.0.4 37 | ordered-dict@1.0.2 38 | pagebakers:ionicons@2.0.0 39 | random@1.0.2 40 | reactive-dict@1.0.5 41 | reactive-var@1.0.4 42 | reload@1.1.2 43 | retry@1.0.2 44 | routepolicy@1.0.4 45 | session@1.0.5 46 | spacebars@1.0.5 47 | spacebars-compiler@1.0.4 48 | templating@1.0.11 49 | tracker@1.0.5 50 | ui@1.0.5 51 | underscore@1.0.2 52 | url@1.0.3 53 | webapp@1.1.6 54 | webapp-hashing@1.0.2 55 | -------------------------------------------------------------------------------- /examples/mobile/client/main.coffee: -------------------------------------------------------------------------------- 1 | 2 | page = css('.page') 3 | .position('absolute') 4 | .top(0) 5 | .bottom(0) 6 | .left(0) 7 | .right(0) 8 | 9 | nav = page.child('.nav') 10 | .position('absolute') 11 | .top(0).left(0).right(0) 12 | .textOverflow('ellipsis') 13 | .height(style.navHeight) 14 | .lineHeight(style.navHeight-style.statusBarHeight) 15 | .pt(style.statusBarHeight) 16 | .fontSize(style.navFontSize) 17 | .textAlign('center') 18 | .color(colors.primaryText) 19 | .bg(colors.primary) 20 | 21 | left = nav.child('.left') 22 | .position('absolute') 23 | .top(style.statusBarHeight) 24 | .left(0) 25 | .height(style.navHeight-style.statusBarHeight) 26 | .pl(style.contentPadding) 27 | 28 | right = nav.child('.right') 29 | .position('absolute') 30 | .top(style.statusBarHeight) 31 | .right(0) 32 | .height(style.navHeight-style.statusBarHeight) 33 | .pr(style.contentPadding) 34 | 35 | statusBar = nav.child('.statusbar') 36 | .position('absolute') 37 | .top(0) 38 | .right(0) 39 | .left(0) 40 | .height(style.statusBarHeight) 41 | .bg('black') 42 | 43 | content = page.child('.content') 44 | .position('absolute') 45 | .top(style.navHeight) 46 | .bottom(0) 47 | .left(0) 48 | .right(0) 49 | .color(colors.backgroundText) 50 | .bg(colors.background) 51 | .padding(style.contentPadding) -------------------------------------------------------------------------------- /examples/mobile/client/main.html: -------------------------------------------------------------------------------- 1 | 2 | {{>main}} 3 | 4 | 5 | -------------------------------------------------------------------------------- /examples/mobile/client/style/0global.coffee: -------------------------------------------------------------------------------- 1 | # darker to lighter color scheme using HCL 2 | scheme = ['#21313E', '#20575F', '#268073', '#53A976', '#98CF6F', '#EFEE69'] 3 | # scheme = d3.scale.linear() 4 | # .domain([0,1]) 5 | # .range(["#21313E", "#EFEE69"]) 6 | # .interpolate(d3.interpolateHcl) 7 | 8 | @colors = 9 | primary: scheme[0] 10 | primaryText: scheme[3] 11 | secondary: scheme[1] 12 | secondaryText: scheme[4] 13 | tertiary: scheme[2] 14 | tertiaryText: scheme[5] 15 | background: '#dddddd' 16 | backgroundText: '#222222' 17 | 18 | @style = 19 | statusBarHeight: 10 20 | navHeight: 49 21 | navFontSize: 24 22 | contentPadding: 10 -------------------------------------------------------------------------------- /examples/mobile/client/style/buttons.coffee: -------------------------------------------------------------------------------- 1 | button = css('button') 2 | .margin(0) 3 | .pl(12).pr(12) 4 | .minWidth(52) 5 | .minHeight(47) 6 | .borderWidth(1) 7 | .borderStyle('solid') 8 | .borderColor('#555555') 9 | .borderRadius(2) 10 | .textAlign('center') 11 | .textOverflow('ellipsis') 12 | .fontSize(16) 13 | .lineHeight(42) 14 | .cursor('pointer') 15 | 16 | 17 | button.also(':hover').textDecoration('none') 18 | button.also('.block').w(100, 'pc') 19 | 20 | button.also('.bar') 21 | .w("calc(100% + #{style.contentPadding*2}px)") 22 | .margin("10px -#{style.contentPadding}px") 23 | .borderRadius(0) 24 | .borderLeft('none') 25 | .borderRight('none') -------------------------------------------------------------------------------- /examples/mobile/client/style/normalize.css: -------------------------------------------------------------------------------- 1 | html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, i, u, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, fieldset, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { 2 | margin: 0; 3 | padding: 0; 4 | border: 0; 5 | vertical-align: baseline; 6 | font: inherit; 7 | font-size: 100%; } 8 | 9 | /** 10 | * 1. Set default font family to sans-serif. 11 | * 2. Prevent iOS text size adjust after orientation change, without disabling 12 | * user zoom. 13 | */ 14 | html { 15 | -webkit-user-select: none; 16 | -moz-user-select: none; 17 | -ms-user-select: none; 18 | user-select: none; 19 | font-family: sans-serif; 20 | /* 1 */ 21 | -webkit-text-size-adjust: 100%; 22 | -ms-text-size-adjust: 100%; 23 | /* 2 */ 24 | -webkit-text-size-adjust: 100%; 25 | /* 2 */ } 26 | 27 | /** 28 | * Remove default margin. 29 | */ 30 | body { 31 | margin: 0; 32 | line-height: 1; } 33 | 34 | /** 35 | * Remove default outlines. 36 | */ 37 | a, button, :focus, a:focus, button:focus, a:active, a:hover { 38 | outline: 0; 39 | text-decoration: none;} 40 | 41 | /* * 42 | * Remove tap highlight color 43 | */ 44 | a { 45 | -webkit-user-drag: none; 46 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 47 | -webkit-tap-highlight-color: transparent; } 48 | a[href]:hover { 49 | cursor: pointer; } 50 | 51 | 52 | /** 53 | * 1. Correct font family not being inherited in all browsers. 54 | * 2. Correct font size not being inherited in all browsers. 55 | * 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome. 56 | * 4. Remove any default :focus styles 57 | * 5. Make sure webkit font smoothing is being inherited 58 | * 6. Remove default gradient in Android Firefox / FirefoxOS 59 | */ 60 | button, input, select, textarea { 61 | margin: 0; 62 | /* 3 */ 63 | font-size: 100%; 64 | /* 2 */ 65 | font-family: inherit; 66 | /* 1 */ 67 | outline-offset: 0; 68 | /* 4 */ 69 | outline-style: none; 70 | /* 4 */ 71 | outline-width: 0; 72 | /* 4 */ 73 | -webkit-font-smoothing: inherit; 74 | /* 5 */ 75 | background-image: none; 76 | /* 6 */ } 77 | 78 | /** 79 | * Address Firefox 4+ setting `line-height` on `input` using `importnt` in 80 | * the UA stylesheet. 81 | */ 82 | button, input { 83 | line-height: normal; } 84 | 85 | /** 86 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 87 | * All other form control elements do not inherit `text-transform` values. 88 | * Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+. 89 | * Correct `select` style inheritance in Firefox 4+ and Opera. 90 | */ 91 | button, select { 92 | text-transform: none; } 93 | 94 | /** 95 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 96 | * and `video` controls. 97 | * 2. Correct inability to style clickable `input` types in iOS. 98 | * 3. Improve usability and consistency of cursor style between image-type 99 | * `input` and others. 100 | */ 101 | button, html input[type="button"], input[type="reset"], input[type="submit"] { 102 | cursor: pointer; 103 | /* 3 */ 104 | -webkit-appearance: button; 105 | /* 2 */ } 106 | 107 | /** 108 | * Re-set default cursor for disabled elements. 109 | */ 110 | button[disabled], html input[disabled] { 111 | cursor: default; } 112 | 113 | /** 114 | * 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. 115 | * 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome 116 | * (include `-moz` to future-proof). 117 | */ 118 | input[type="search"] { 119 | -webkit-box-sizing: content-box; 120 | /* 2 */ 121 | -moz-box-sizing: content-box; 122 | box-sizing: content-box; 123 | -webkit-appearance: textfield; 124 | /* 1 */ } 125 | 126 | /** 127 | * Remove inner padding and search cancel button in Safari 5 and Chrome 128 | * on OS X. 129 | */ 130 | input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration { 131 | -webkit-appearance: none; } 132 | 133 | /** 134 | * Remove inner padding and border in Firefox 4+. 135 | */ 136 | button::-moz-focus-inner, input::-moz-focus-inner { 137 | padding: 0; 138 | border: 0; } 139 | 140 | /** 141 | * 1. Remove default vertical scrollbar in IE 8/9. 142 | * 2. Improve readability and alignment in all browsers. 143 | */ 144 | textarea { 145 | overflow: auto; 146 | /* 1 */ 147 | vertical-align: top; 148 | /* 2 */ } 149 | 150 | img { 151 | -webkit-user-drag: none; } 152 | 153 | 154 | *, *:before, *:after { 155 | -webkit-box-sizing: border-box; 156 | -moz-box-sizing: border-box; 157 | box-sizing: border-box; } 158 | 159 | html { 160 | overflow: hidden; 161 | -ms-touch-action: pan-y; 162 | touch-action: pan-y; } 163 | 164 | body, .ionic-body { 165 | -webkit-touch-callout: none; 166 | -webkit-font-smoothing: antialiased; 167 | font-smoothing: antialiased; 168 | -webkit-text-size-adjust: none; 169 | -moz-text-size-adjust: none; 170 | text-size-adjust: none; 171 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 172 | -webkit-tap-highlight-color: transparent; 173 | -webkit-user-select: none; 174 | -moz-user-select: none; 175 | -ms-user-select: none; 176 | user-select: none; 177 | top: 0; 178 | right: 0; 179 | bottom: 0; 180 | left: 0; 181 | overflow: hidden; 182 | margin: 0; 183 | padding: 0; 184 | color: #000; 185 | word-wrap: break-word; 186 | font-size: 14px; 187 | font-family: "Helvetica Neue", "Roboto", sans-serif; 188 | line-height: 20px; 189 | text-rendering: optimizeLegibility; 190 | -webkit-backface-visibility: hidden; 191 | -webkit-user-drag: none; } 192 | 193 | 194 | -------------------------------------------------------------------------------- /examples/mobile/client/style/text.coffee: -------------------------------------------------------------------------------- 1 | 2 | css('h1, h2, h3, h4, h5, h6') 3 | .fontWeight(500) 4 | .fontFamily('sans-serif') 5 | 6 | css('h1, h2, h3').mt(20).mb(10) 7 | css('h4, h5, h6').mt(10).mb(10) 8 | 9 | css('h1').fontSize(36) 10 | css('h2').fontSize(30) 11 | css('h3').fontSize(24) 12 | css('h4').fontSize(18) 13 | css('h5').fontSize(14) 14 | css('h6').fontSize(12) 15 | 16 | css('p').mb(10) -------------------------------------------------------------------------------- /examples/reactive/.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | packages/ -------------------------------------------------------------------------------- /examples/reactive/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | -------------------------------------------------------------------------------- /examples/reactive/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /examples/reactive/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 6b8ws2bzaslg1akqwvx 8 | -------------------------------------------------------------------------------- /examples/reactive/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | meteor-platform 8 | autopublish 9 | insecure 10 | coffeescript 11 | reactive-dict 12 | reactive-var 13 | rcy:nouislider 14 | d3js:d3 15 | ccorcos:reactive-css 16 | -------------------------------------------------------------------------------- /examples/reactive/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /examples/reactive/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.0.3.1 2 | -------------------------------------------------------------------------------- /examples/reactive/.meteor/versions: -------------------------------------------------------------------------------- 1 | application-configuration@1.0.4 2 | autopublish@1.0.2 3 | autoupdate@1.1.5 4 | base64@1.0.2 5 | binary-heap@1.0.2 6 | blaze@2.0.4 7 | blaze-tools@1.0.2 8 | boilerplate-generator@1.0.2 9 | callback-hook@1.0.2 10 | ccorcos:reactive-css@1.0.4 11 | check@1.0.4 12 | coffeescript@1.0.5 13 | d3js:d3@3.5.3 14 | ddp@1.0.14 15 | deps@1.0.6 16 | ejson@1.0.5 17 | fastclick@1.0.2 18 | follower-livedata@1.0.3 19 | geojson-utils@1.0.2 20 | html-tools@1.0.3 21 | htmljs@1.0.3 22 | http@1.0.10 23 | id-map@1.0.2 24 | insecure@1.0.2 25 | jquery@1.11.3 26 | json@1.0.2 27 | launch-screen@1.0.1 28 | livedata@1.0.12 29 | logging@1.0.6 30 | meteor@1.1.4 31 | meteor-platform@1.2.1 32 | minifiers@1.1.3 33 | minimongo@1.0.6 34 | mobile-status-bar@1.0.2 35 | mongo@1.0.11 36 | observe-sequence@1.0.4 37 | ordered-dict@1.0.2 38 | random@1.0.2 39 | rcy:nouislider@7.0.7_2 40 | reactive-dict@1.0.5 41 | reactive-var@1.0.4 42 | reload@1.1.2 43 | retry@1.0.2 44 | routepolicy@1.0.4 45 | session@1.0.5 46 | spacebars@1.0.5 47 | spacebars-compiler@1.0.4 48 | templating@1.0.11 49 | tracker@1.0.5 50 | ui@1.0.5 51 | underscore@1.0.2 52 | url@1.0.3 53 | webapp@1.1.6 54 | webapp-hashing@1.0.2 55 | -------------------------------------------------------------------------------- /examples/reactive/client/main.coffee: -------------------------------------------------------------------------------- 1 | # Define some styles 2 | @styles = new ReactiveDict() 3 | styles.set 'navHeightpx', 90 4 | styles.set 'toolbarHeightpx', 50 5 | styles.set 'contentMaxWidthem', 40 6 | 7 | # reactive sizing 8 | adjustSize = -> 9 | width = window.innerWidth 10 | height = window.innerHeight 11 | styles.set 'size.width', width 12 | styles.set 'size.height', height 13 | if width < 1200 14 | styles.set 'size', 'mobile' 15 | else 16 | styles.set 'size', 'desktop' 17 | 18 | adjustSize() 19 | window.addEventListener "resize", adjustSize 20 | 21 | # React to Andoid vs iOS 22 | ios = !!navigator.userAgent.match(/iPad/i) or !!navigator.userAgent.match(/iPhone/i) or !!navigator.userAgent.match(/iPod/i) 23 | android = navigator.userAgent.indexOf('Android') > 0 24 | 25 | styles.set 'platform', do -> 26 | if ios then return 'ios' 27 | if android then return 'android' 28 | return 'desktop' 29 | 30 | # Using D3 to define an HCL color interpolation 31 | @color = d3.scale.linear() 32 | .domain([0,1]) 33 | .range(["#F3F983", "#373A49"]) 34 | .interpolate(d3.interpolateHcl); 35 | 36 | # @color = d3.scale.linear() 37 | # .domain([0,1]) 38 | # .range(["#4D261F","#573649","#395267","#206B5D","#5E793B","#A97839"]) 39 | # .interpolate(d3.interpolateHcl); 40 | 41 | # Rotate the color scheme with a reactive var and a setInterval 42 | @rotate = new ReactiveVar(0.0) 43 | 44 | desaturate = (x) -> 45 | c = d3.hcl(x) 46 | c.c *= 0.7 47 | return c.toString() 48 | 49 | lighten = (x) -> 50 | c = d3.hcl(x) 51 | c.l /= 0.7 52 | return c.toString() 53 | 54 | darken = (x) -> 55 | c = d3.hcl(x) 56 | c.l *= 0.7 57 | return c.toString() 58 | 59 | Tracker.autorun -> 60 | r = rotate.get() 61 | styles.set 'primary', color(2/6 + r) 62 | styles.set 'secondary', color(1/6 + r) 63 | styles.set 'tertiary', color(0/6 + r) 64 | styles.set 'primary.text', lighten(desaturate(color(0/6+r))) 65 | styles.set 'secondary.text', lighten(desaturate(color(0/6+r))) 66 | styles.set 'tertiary.text', darken(desaturate(color(2/6+r))) 67 | 68 | inc = 1/6/10 69 | rotateColors = -> 70 | r = rotate.get() 71 | if r+inc+2/6 >= 1.0 72 | inc *= -1 73 | else if r+inc <= 0 74 | inc *= -1 75 | r += inc 76 | rotate.set(r) 77 | 78 | Meteor.setInterval(rotateColors, '100') 79 | 80 | 81 | # Define all the styles 82 | css '*': css.boxSizing('border-box') 83 | 84 | page = css('.page') 85 | .fp() 86 | .bg -> styles.get('tertiary') 87 | .c -> styles.get('tertiary.text') 88 | 89 | nav = css('.nav') 90 | .w 100, 'pc' 91 | .h 'px', -> styles.get('navHeightpx') 92 | .bg -> styles.get('primary') 93 | .c -> styles.get('primary.text') 94 | 95 | 96 | title = nav.child('.title') 97 | .w 100, 'pc' 98 | .h styles.get('navHeightpx') 99 | .lineHeight styles.get('navHeightpx') 100 | .fontSize 2, 'em' 101 | .c -> styles.get('primary.text') 102 | 103 | # Make this whole chunk reactive 104 | # Android titles pull left, while iOS is centered 105 | Tracker.autorun -> 106 | if styles.equals('platform', 'android') 107 | title 108 | .pl 10, 'px' 109 | .margin 'auto' 110 | .textAlign 'left' 111 | else 112 | title 113 | .margin '0 auto' 114 | .pl 0 115 | .textAlign 'center' 116 | 117 | content = css('.content') 118 | .position 'absolute' 119 | .top -> styles.get('navHeightpx') 120 | .pt 10, 'px' 121 | .bottom 'px', -> 122 | if styles.equals('size','mobile') 123 | return styles.get('toolbarHeightpx') 124 | else 125 | return 0 126 | .pb 10, 'px' 127 | .pl 10 128 | .pr 10 129 | .left 0 130 | .right 0 131 | .overflowY 'scroll' 132 | .maxWidth 'em', -> styles.get('contentMaxWidthem') 133 | .margin '0 auto' 134 | 135 | toolbar = css('.toolbar') 136 | .position('absolute') 137 | .bg -> styles.get('secondary') 138 | .c -> styles.get('secondary.text') 139 | 140 | # On a larger layout, pull the toolbar to the left 141 | Tracker.autorun -> 142 | device = styles.get('size') 143 | if device is 'mobile' 144 | toolbar 145 | .bottom 0 146 | .height styles.get('toolbarHeightpx') 147 | .left 0 148 | .right 0 149 | .top 'auto' 150 | .w 100, 'pc' 151 | else 152 | toolbar 153 | .height 'auto' 154 | .left "calc(50% - #{styles.get('contentMaxWidthem')/2}em - 210px)" 155 | .right 'auto' 156 | .width 200 157 | .top styles.get('navHeightpx') + 10 158 | .bottom styles.get('toolbarHeightpx') 159 | -------------------------------------------------------------------------------- /examples/reactive/client/main.html: -------------------------------------------------------------------------------- 1 | 2 | {{> main}} 3 | 4 | 5 | 12 | 13 | 20 | 21 | 28 | 29 | -------------------------------------------------------------------------------- /lib/css.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | # Note 3 | a = -> 4 | if not (this instanceof a) then return new a() 5 | @something = 10 6 | return this 7 | 8 | a.f = -> 21 9 | a::f = -> @something * 10 10 | 11 | console.log a.f() # => 21 12 | console.log a().f() # => 100 13 | ### 14 | 15 | # Some helper functions 16 | isFunction = (value) -> (typeof value is 'function') 17 | isPlainObject = (value) -> (typeof value is 'object') and not Array.isArray(value) 18 | isArray = (value) -> Array.isArray(value) 19 | isString = (value) -> (typeof value == 'string') 20 | isNumber = (value) -> (typeof value == 'number') 21 | capitalize = (string) -> string and (string.charAt(0).toUpperCase() + string.slice(1)) 22 | 23 | # Create the Stylesheet in the DOM 24 | Stylesheet = -> 25 | if not (this instanceof Stylesheet) then return new Stylesheet() 26 | @styles = {} 27 | style = document.createElement('style') 28 | style.setAttribute('media', 'screen') 29 | style.appendChild(document.createTextNode('')) 30 | document.head.appendChild(style) 31 | @sheet = style.sheet 32 | return this 33 | 34 | # Add a CSS rule to the stylesheet 35 | Stylesheet::rule = (selector, key, value) -> 36 | # check if the selector is already in the stylesheet 37 | idx = _.map(@sheet.cssRules, (rule) -> rule.selectorText).indexOf(selector) 38 | # else make a new rule 39 | unless idx >= 0 40 | idx = @sheet.insertRule("#{selector} {}", 0) 41 | 42 | unless selector of @styles 43 | @styles[selector] = {} 44 | 45 | # check if the rule ends with px, pc, or em for translating values 46 | ending = key[-2..-1] 47 | if ending is 'px' 48 | key = key[0...-2] 49 | value = "#{value}px" 50 | else if ending is 'pc' 51 | key = key[0...-2] 52 | value = "#{value}%" 53 | else if ending is 'em' 54 | key = key[0...-2] 55 | value = "#{value}em" 56 | else if ending is 'vh' 57 | key = key[0...-2] 58 | value = "#{value}vh" 59 | else if ending is 'vw' 60 | key = key[0...-2] 61 | value = "#{value}vw" 62 | 63 | # write the rule 64 | # console.log "#{key}: #{value}" 65 | @styles[selector][key] = value 66 | @sheet.cssRules[idx].style[key] = value 67 | 68 | # EXAMPLE: Make a style sheet and add rules to it: 69 | # stylesheet().rule('.page', 'widthpc', 100) 70 | 71 | # Create a stylesheet for everything else to use 72 | @stylesheet = Stylesheet() 73 | rule = (args...) -> stylesheet.rule.apply(stylesheet, args) 74 | 75 | # if input is a... 76 | # object: then parse out the nested CSS rules 77 | # string: set the selector and the prototype function create rules on that selector 78 | @css = (input) -> 79 | # safeguard so you dont have to use new 80 | if not (this instanceof css) then return new css(input) 81 | @autoruns = [] 82 | if isString(input) 83 | @selector = input 84 | return this 85 | else if isPlainObject(input) 86 | @nested(input) 87 | return 88 | else 89 | console.log "WARNING CoffeeCSS: calling css() doesn't do anything.", input 90 | return 91 | 92 | # Recursively parse a nested object of CSS rules 93 | css::nested = (obj, selector="") -> 94 | self = this 95 | for key, value of obj 96 | # if its a string, then its a CSS rule 97 | # if its an object, then recursively update the selector 98 | # if its a function, run reactively 99 | if isString(value) or isNumber(value) 100 | if selector is "" 101 | console.log("WARNING CoffeeCSS: No selector in nested CSS object?!") 102 | return 103 | rule(selector, key, value) 104 | 105 | else if isPlainObject(value) 106 | nextSelector = "" 107 | if selector is "" 108 | nextSelector = key 109 | else if key[0] is '&' 110 | nextSelector = "#{selector}#{key}" 111 | else 112 | nextSelector = "#{selector} #{key}" 113 | @nested(value, nextSelector) 114 | 115 | else if isFunction(value) 116 | do (key, value) -> 117 | self.autoruns.push Tracker.autorun -> 118 | rule(selector, key, value()) 119 | 120 | else 121 | console.log "WARNING CoffeeCSS: Not a valid CSS rule: #{selector} {#{key}: #{value}};" 122 | 123 | # return a new css instance with the selector appended 124 | css::child = (string) -> 125 | return css(@selector + " #{string}") 126 | 127 | # return a new css instance with the selector appended 128 | css::also = (string) -> 129 | return css(@selector + "#{string}") 130 | 131 | # manual specification 132 | # css('.page').rule('overflowX', 'scroll') 133 | # => writes the rule, .page {overflow-x: scroll} 134 | css::rule = (name, value) -> 135 | if isFunction(value) 136 | self = this 137 | # run the function in a reactive context 138 | @autoruns.push Tracker.autorun -> 139 | rule(self.selector, name, value()) 140 | else 141 | rule(@selector, name, value) 142 | return this 143 | 144 | css::rules = (obj) -> 145 | @nested(obj, @selector) 146 | 147 | css::stop = -> 148 | for autorun in @autoruns 149 | autorun.stop() 150 | return 151 | 152 | # define a method for creating mixins. 153 | css.mixin = (name, func) -> 154 | css[name] = func 155 | css::[name] = (args...) -> 156 | last = args.length-1 157 | if isFunction(args[last]) 158 | self = this 159 | # your first input can be a unit, with the second being the function 160 | # but we need to reverse them into the mixin function because we 161 | # typically just join the args. 162 | if args.length > 1 163 | args.unshift(args[last]) 164 | args.pop() 165 | # run the function in a reactive context 166 | f = args[0] 167 | @autoruns.push Tracker.autorun -> 168 | args[0] = f() 169 | obj = func.apply(self, args) 170 | for k, v of obj 171 | rule(self.selector, k, v) 172 | else 173 | obj = func.apply(@, args) 174 | for k, v of obj 175 | rule(@selector, k, v) 176 | 177 | return this 178 | 179 | css.alias = (name, aliases...) -> 180 | for alias in aliases 181 | css[alias] = css[name] 182 | css::[alias] = css::[name] -------------------------------------------------------------------------------- /lib/helpers.coffee: -------------------------------------------------------------------------------- 1 | # Helpers 2 | css.mixin 'fullPage', -> 3 | position: 'absolute' 4 | top: 0 5 | bottom: 0 6 | left: 0 7 | right: 0 8 | 9 | css.mixin 'noPadding', -> 10 | paddingTop: 0 11 | paddingBottom: 0 12 | paddingLeft: 0 13 | paddingRight: 0 14 | 15 | css.mixin 'noMargin', -> 16 | marginTop: 0 17 | marginBottom: 0 18 | marginLeft: 0 19 | marginRight: 0 20 | 21 | 22 | css.mixin 'fullWidth', -> {width: '100%'} 23 | css.mixin 'fullHeight', -> {height: '100%'} 24 | 25 | css.alias('fullPage', 'fp') 26 | css.alias('fullWidth', 'fw') 27 | css.alias('fullHeight', 'fh') 28 | css.alias('fullPage', 'fp') 29 | css.alias('noMargin', 'nm') 30 | css.alias('noPadding', 'np') 31 | 32 | -------------------------------------------------------------------------------- /lib/mixins.coffee: -------------------------------------------------------------------------------- 1 | 2 | isString = (value) -> (typeof value == 'string') 3 | capitalize = (string) -> string and (string.charAt(0).toUpperCase() + string.slice(1)) 4 | 5 | 6 | # parse curried arguments for single units if necessary 7 | # default px 8 | argsToUnits = (args...) -> 9 | if args.length is 1 10 | value = args[0] 11 | if isString(value) 12 | return value 13 | else 14 | return "#{value}px" 15 | else if args.length is 2 16 | if args[1] is 'pc' then args[1] = '%' 17 | return "#{args[0]}#{args[1]}" 18 | 19 | # simple meaning no units or anything to parse 20 | simpleMixins = [ 21 | 'color' 22 | 'backgroundColor' 23 | 'position' 24 | 'overflow' 25 | 'overflowX' 26 | 'overflowY' 27 | 'margin' 28 | 'zIndex' 29 | 'fontWeight' 30 | 'fontFamily' 31 | 'textOverflow' 32 | 'borderStyle' 33 | 'cursor' 34 | 'textDecoration' 35 | 'borderColor' 36 | 'borderLeft' 37 | 'borderRight' 38 | ] 39 | 40 | simplePrefixMixins = [ 41 | 'boxSizing' 42 | 'userSelect' 43 | 'opacity' 44 | 'textAlign' 45 | 'transform' 46 | ] 47 | 48 | # one unit meaning theres only one posisble value input 49 | # but it could be px, em, %, etc. 50 | oneUnitMixins = [ 51 | 'height' 52 | 'maxHeight' 53 | 'minHeight' 54 | 'width' 55 | 'maxWidth' 56 | 'minWidth' 57 | 'paddingTop' 58 | 'paddingBottom' 59 | 'paddingRight' 60 | 'paddingLeft' 61 | 'marginTop' 62 | 'marginBottom' 63 | 'marginRight' 64 | 'marginLeft' 65 | 'top' 66 | 'bottom' 67 | 'left' 68 | 'right' 69 | 'lineHeight' 70 | 'fontSize' 71 | 'padding' 72 | 'borderWidth' 73 | ] 74 | 75 | oneUnitPrefixMixins = [ 76 | 'borderRadius' 77 | ] 78 | 79 | for name in simpleMixins 80 | do (name) -> 81 | css.mixin name, (args...) -> 82 | obj = {} 83 | value = args.join(' ') 84 | obj[name] = value 85 | return obj 86 | 87 | for name in simplePrefixMixins 88 | do (name) -> 89 | css.mixin name, (args...) -> 90 | obj = {} 91 | value = args.join(' ') 92 | obj[name] = value 93 | obj["Webkit"+capitalize(name)] = value 94 | obj["Moz"+capitalize(name)] = value 95 | obj["Ms"+capitalize(name)] = value 96 | return obj 97 | 98 | for name in oneUnitMixins 99 | do (name) -> 100 | css.mixin name, (args...) -> 101 | obj = {} 102 | obj[name] = argsToUnits.apply({}, args) 103 | return obj 104 | 105 | for name in oneUnitPrefixMixins 106 | do (name) -> 107 | css.mixin name, (args...) -> 108 | obj = {} 109 | unit = argsToUnits.apply({}, args) 110 | obj[name] = unit 111 | obj["Webkit"+capitalize(name)] = unit 112 | obj["Moz"+capitalize(name)] = unit 113 | obj["Ms"+capitalize(name)] = unit 114 | return obj 115 | 116 | # Create some aliases of the mixins :) 117 | css.alias('color', 'c') 118 | css.alias('backgroundColor', 'bg') 119 | css.alias('height', 'h') 120 | css.alias('width', 'w') 121 | css.alias('paddingTop', 'pt') 122 | css.alias('paddingBottom', 'pb') 123 | css.alias('paddingLeft', 'pl') 124 | css.alias('paddingRight', 'pr') 125 | css.alias('marginTop', 'mt') 126 | css.alias('marginBottom', 'mb') 127 | css.alias('marginLeft', 'ml') 128 | css.alias('marginRight', 'mr') 129 | 130 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'ccorcos:reactive-css', 3 | summary: 'Define reactive CSS rules in Javascript or (preferably ;) Coffeescript.', 4 | version: '1.0.5', 5 | git: 'https://github.com/ccorcos/meteor-reactive-css' 6 | }); 7 | 8 | Package.onUse(function(api) { 9 | api.versionsFrom('METEOR@1'); 10 | 11 | api.use([ 12 | 'coffeescript', 13 | 'underscore' 14 | ], 'client'); 15 | 16 | api.addFiles([ 17 | 'lib/css.coffee', 18 | 'lib/mixins.coffee', 19 | 'lib/helpers.coffee', 20 | ], 'client'); 21 | 22 | }); --------------------------------------------------------------------------------