├── demo ├── entry.js ├── Ad.jsx ├── TweetButton.jsx ├── base.css └── Demo.jsx ├── webpack.config.js ├── README.md ├── index.html ├── package.json ├── src └── FitterHappierText.jsx └── FitterHappierText.js /demo/entry.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import Demo from './Demo.jsx' 4 | import css from './base.css' 5 | 6 | React.render(React.createElement(Demo), document.querySelector('#demo')) 7 | 8 | -------------------------------------------------------------------------------- /demo/Ad.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | 4 | class Ad extends React.Component { 5 | 6 | render () { 7 | return ( 8 |
9 | 10 |
11 | ) 12 | } 13 | 14 | } 15 | 16 | export default Ad 17 | 18 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | entry: './demo/entry.js', 4 | 5 | output: { 6 | filename: 'bundle.js', 7 | path: __dirname 8 | }, 9 | 10 | module: { 11 | loaders: [ 12 | { test: /(\.js$|\.jsx$)/, exclude: /node_modules/, loader: 'babel-loader' }, 13 | { test: /\.css$/, loader: 'style-loader!css-loader!cssnext-loader' } 14 | ] 15 | }, 16 | 17 | cssnext: { 18 | compress: true, 19 | features: { 20 | } 21 | } 22 | 23 | } 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-fitter-happier-text 2 | 3 | React component for fully fluid headings 4 | 5 | *Note:* Requires `react >= 0.14.0` 6 | 7 | ## Demo 8 | 9 | http://jxnblk.com/react-fitter-happier-text 10 | 11 | ## Usage 12 | 13 | ```bash 14 | npm i react-fitter-happier-text 15 | ``` 16 | 17 | ```js 18 | var React = require('react') 19 | var FitterHappierText = require('react-fitter-happier-text') 20 | 21 | React.render(, document.querySelector('#hello-world')) 22 | ``` 23 | 24 | Note: this component relies on `element.offsetWidth` and only works in client-side contexts. 25 | 26 | ## Related 27 | 28 | http://jxnblk.com/fitter-happier-text 29 | 30 | MIT License 31 | 32 | -------------------------------------------------------------------------------- /demo/TweetButton.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | 4 | class TweetButton extends React.Component { 5 | 6 | render () { 7 | var script = { 8 | __html: '!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?"http":"https";if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+"://platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document, "script", "twitter-wjs");' 9 | } 10 | var text = this.props.text 11 | return ( 12 |
13 | 18 | Tweet 19 | 20 | 13 | 14 |
15 | 16 | 17 |
18 | 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-fitter-happier-text", 3 | "version": "1.0.2", 4 | "description": "React component for fully fluid headings", 5 | "main": "FitterHappierText.js", 6 | "scripts": { 7 | "babel": "babel src --out-dir .", 8 | "babel:watch": "babel -w src --out-dir .", 9 | "webpack": "webpack --progress --colors", 10 | "dev": "webpack-dev-server --progress --colors", 11 | "prod": "webpack -p --progress --colors", 12 | "start": "npm run babel:watch & npm run dev" 13 | }, 14 | "keywords": [ 15 | "react", 16 | "react-component", 17 | "svg", 18 | "headings" 19 | ], 20 | "author": "Brent Jackson", 21 | "license": "MIT", 22 | "devDependencies": { 23 | "babel": "^5.5.8", 24 | "babel-core": "^5.5.8", 25 | "babel-loader": "^5.1.4", 26 | "blk": "^3.0.4", 27 | "css-loader": "^0.15.1", 28 | "cssnext-loader": "^1.0.1", 29 | "flex-object": "^1.1.3", 30 | "gravitons": "^1.0.0", 31 | "node-libs-browser": "^0.5.2", 32 | "react": "^0.13.3", 33 | "style-loader": "^0.12.3", 34 | "webpack": "^1.9.11", 35 | "webpack-dev-server": "^1.9.0" 36 | }, 37 | "dependencies": { 38 | "lodash": "^4.6.1", 39 | "react": "^0.14.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/FitterHappierText.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import { debounce } from 'lodash' 4 | 5 | class FitterHappierText extends React.Component { 6 | 7 | constructor () { 8 | super () 9 | this.resize = debounce(this.resize.bind(this)) 10 | this.state = { 11 | width: 256, 12 | height: 24 13 | } 14 | } 15 | 16 | resize () { 17 | let el = this.refs.text; 18 | let state = this.state 19 | let width = el.offsetWidth || el.getComputedTextLength() 20 | let height = el.offsetHeight || 24 21 | if (state.width !== width || state.height !== height) { 22 | this.setState({ 23 | width: width, 24 | height: height 25 | }) 26 | } 27 | } 28 | 29 | componentDidMount () { 30 | this.resize() 31 | } 32 | 33 | componentWillReceiveProps () { 34 | this.resize() 35 | } 36 | 37 | render () { 38 | let styles = { 39 | svg: { 40 | width: '100%', 41 | maxHeight: '100%', 42 | fill: 'currentcolor', 43 | overflow: 'visible' 44 | }, 45 | text: { 46 | fontFamily: 'inherit', 47 | fontSize: '1rem', 48 | fontWeight: 'inherit', 49 | textAnchor: 'middle' 50 | } 51 | } 52 | let viewBox = [ 53 | 0, 0, 54 | this.state.width, 55 | this.state.height 56 | ].join(' ') 57 | 58 | return ( 59 | 62 | 67 | {this.props.text} 68 | 69 | 70 | ) 71 | } 72 | 73 | } 74 | 75 | FitterHappierText.defaultProps = { 76 | baseline: 16, 77 | paddingY: 0, 78 | } 79 | 80 | FitterHappierText.propTypes = { 81 | text: React.PropTypes.string, 82 | baseline: React.PropTypes.number, 83 | paddingY: React.PropTypes.number, 84 | } 85 | 86 | export default FitterHappierText 87 | 88 | -------------------------------------------------------------------------------- /demo/base.css: -------------------------------------------------------------------------------- 1 | 2 | @import 'blk'; 3 | 4 | body { 5 | margin: 0; 6 | padding: 1em 1.5em; 7 | min-height: 100vh; 8 | color: #fff; 9 | background-color: var(--blue); 10 | } 11 | 12 | .avenir { 13 | font-family: 'Avenir Next', 'Helvetica Neue', Helvetica, sans-serif; 14 | font-weight: 600; 15 | line-height: 1; 16 | } 17 | 18 | 19 | .input { 20 | box-sizing: border-box; 21 | height: 2em; 22 | display: inline-block; 23 | font-family: inherit; 24 | font-size: inherit; 25 | line-height: 1; 26 | margin: 0; 27 | padding: .5em .5em; 28 | vertical-align: middle; 29 | color: inherit; 30 | border: 1px solid transparent; 31 | border-radius: 3px; 32 | -webkit-appearance: none; 33 | } 34 | 35 | .input-light { 36 | background-color: color(#fff a(.25)); 37 | /* 38 | border-color: color(#000 a(.25)); 39 | */ 40 | } 41 | 42 | .input-light:focus { 43 | outline: 5px solid var(--darken-2); 44 | } 45 | 46 | .btn-dark { 47 | background-color: color(#000 a(.25)); 48 | border-color: color(#000 a(.5)); 49 | } 50 | 51 | 52 | #carbonads { 53 | font-size: 14px; 54 | font-weight: normal; 55 | line-height: 1.25; 56 | text-align: left; 57 | text-transform: none; 58 | 59 | padding: .5em; 60 | background-color: color(#fff a(.9)); 61 | } 62 | 63 | #tweet-button { 64 | margin-top: 1em; 65 | } 66 | 67 | @media (min-width: 32em) { 68 | #carbonads { 69 | position: fixed; 70 | right: 0; 71 | bottom: 0; 72 | max-width: 320px; 73 | margin: .5em; 74 | } 75 | #tweet-button { 76 | position: fixed; 77 | left: 0; 78 | bottom: 0; 79 | padding: .5em; 80 | } 81 | } 82 | 83 | #carbonads a, 84 | #carbonads a:hover { 85 | color: #222; 86 | text-decoration: none; 87 | background: none; 88 | } 89 | #carbonads span { display: block } 90 | #carbonads > span::before, 91 | #carbonads > span::after { 92 | content: ''; 93 | display: table; 94 | } 95 | #carbonads > span::after { 96 | clear: both; 97 | } 98 | .carbon-img { 99 | float: left; 100 | margin-right: .5em; 101 | } 102 | .carbon-img > img { display: block } 103 | .carbon-text { overflow: hidden } 104 | .carbon-poweredby { 105 | float: left; 106 | margin-top: .25em; 107 | opacity: 0.75; 108 | } 109 | 110 | 111 | /* 112 | @import 'gravitons'; 113 | @import 'flex-object'; 114 | */ 115 | 116 | :root { 117 | } 118 | 119 | -------------------------------------------------------------------------------- /demo/Demo.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import FitterHappierText from '../FitterHappierText' 4 | import { MinimalHeader, Header, Footer } from 'blk' 5 | 6 | class Demo extends React.Component { 7 | 8 | constructor () { 9 | super () 10 | this.state = { 11 | text: 'Live Demo', 12 | caps: true 13 | } 14 | this.handleChange = this.handleChange.bind(this) 15 | this.toggleCaps = this.toggleCaps.bind(this) 16 | } 17 | 18 | handleChange (e) { 19 | this.setState({ text: e.target.value }) 20 | } 21 | 22 | toggleCaps () { 23 | var caps = !this.state.caps 24 | this.setState({ caps: caps }) 25 | } 26 | 27 | render () { 28 | var classNames = { 29 | demo: (this.state.caps ? 'caps' : '') + ' avenir' 30 | } 31 | return ( 32 |
33 | 35 |
36 |
37 | 38 | 39 | 40 |
41 |
42 |
43 | 47 | 52 |
53 |
54 | 57 |
58 |
59 |
60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 |

79 | Source: 80 | 83 | HTML Design Principles 84 | 85 |

86 |
87 |
88 |

Usage

89 |
npm i react-fitter-happier-text
90 |
 91 | {`var FitterHappierText = require('react-fitter-happier-text')
 92 | React.render(, document.querySelector('#hello-world'))`}
 93 |           
94 |

Note: this component relies on element.offsetWidth and only works in client-side contexts.

95 |
96 |