summaryrefslogtreecommitdiffstats
path: root/packages/gitbook-core/src/components
diff options
context:
space:
mode:
authorSamy Pessé <samypesse@gmail.com>2016-12-22 10:18:38 +0100
committerGitHub <noreply@github.com>2016-12-22 10:18:38 +0100
commit194ebc3da9641ff96f083f9d8ab43c2d27944f9a (patch)
treec50988f32ccf18df93ae7ab40be78e9459642818 /packages/gitbook-core/src/components
parent64ccb6b00b4b63fa0e516d4e35351275b34f8c07 (diff)
parent16af264360e48e8a833e9efa9ab8d194574dbc70 (diff)
downloadgitbook-194ebc3da9641ff96f083f9d8ab43c2d27944f9a.zip
gitbook-194ebc3da9641ff96f083f9d8ab43c2d27944f9a.tar.gz
gitbook-194ebc3da9641ff96f083f9d8ab43c2d27944f9a.tar.bz2
Merge pull request #1543 from GitbookIO/dream
React for rendering website with plugins
Diffstat (limited to 'packages/gitbook-core/src/components')
-rw-r--r--packages/gitbook-core/src/components/Backdrop.js56
-rw-r--r--packages/gitbook-core/src/components/Button.js22
-rw-r--r--packages/gitbook-core/src/components/ButtonGroup.js23
-rw-r--r--packages/gitbook-core/src/components/ContextProvider.js34
-rw-r--r--packages/gitbook-core/src/components/Dropdown.js126
-rw-r--r--packages/gitbook-core/src/components/HTMLContent.js77
-rw-r--r--packages/gitbook-core/src/components/HotKeys.js59
-rw-r--r--packages/gitbook-core/src/components/I18nProvider.js28
-rw-r--r--packages/gitbook-core/src/components/Icon.js28
-rw-r--r--packages/gitbook-core/src/components/Import.js48
-rw-r--r--packages/gitbook-core/src/components/InjectedComponent.js117
-rw-r--r--packages/gitbook-core/src/components/Link.js37
-rw-r--r--packages/gitbook-core/src/components/PJAXWrapper.js102
-rw-r--r--packages/gitbook-core/src/components/Panel.js22
-rw-r--r--packages/gitbook-core/src/components/Tooltipped.js44
15 files changed, 823 insertions, 0 deletions
diff --git a/packages/gitbook-core/src/components/Backdrop.js b/packages/gitbook-core/src/components/Backdrop.js
new file mode 100644
index 0000000..7b34b0d
--- /dev/null
+++ b/packages/gitbook-core/src/components/Backdrop.js
@@ -0,0 +1,56 @@
+const React = require('react');
+const HotKeys = require('./HotKeys');
+
+/**
+ * Backdrop for modals, dropdown, etc. that covers the whole screen
+ * and handles click and pressing escape.
+ *
+ * <Backdrop onClose={onCloseModal} />
+ */
+const Backdrop = React.createClass({
+ propTypes: {
+ // Callback when backdrop is closed
+ onClose: React.PropTypes.func.isRequired,
+ // Z-index for the backdrop
+ zIndex: React.PropTypes.number,
+ children: React.PropTypes.node
+ },
+
+ getDefaultProps() {
+ return {
+ zIndex: 200
+ };
+ },
+
+ onClick(event) {
+ const { onClose } = this.props;
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ onClose();
+ },
+
+ render() {
+ const { zIndex, children, onClose } = this.props;
+ const style = {
+ zIndex,
+ position: 'fixed',
+ top: 0,
+ right: 0,
+ width: '100%',
+ height: '100%'
+ };
+ const keyMap = {
+ 'escape': onClose
+ };
+
+ return (
+ <HotKeys keyMap={keyMap}>
+ <div style={style} onClick={this.onClick}>{children}</div>
+ </HotKeys>
+ );
+ }
+});
+
+module.exports = Backdrop;
diff --git a/packages/gitbook-core/src/components/Button.js b/packages/gitbook-core/src/components/Button.js
new file mode 100644
index 0000000..4d929b8
--- /dev/null
+++ b/packages/gitbook-core/src/components/Button.js
@@ -0,0 +1,22 @@
+const React = require('react');
+const classNames = require('classnames');
+
+const Button = React.createClass({
+ propTypes: {
+ active: React.PropTypes.bool,
+ className: React.PropTypes.string,
+ children: React.PropTypes.node,
+ onClick: React.PropTypes.func
+ },
+
+ render() {
+ const { children, active, onClick } = this.props;
+ const className = classNames('GitBook-Button', this.props.className, {
+ active
+ });
+
+ return <button className={className} onClick={onClick}>{children}</button>;
+ }
+});
+
+module.exports = Button;
diff --git a/packages/gitbook-core/src/components/ButtonGroup.js b/packages/gitbook-core/src/components/ButtonGroup.js
new file mode 100644
index 0000000..4c20b68
--- /dev/null
+++ b/packages/gitbook-core/src/components/ButtonGroup.js
@@ -0,0 +1,23 @@
+const React = require('react');
+const classNames = require('classnames');
+
+const ButtonGroup = React.createClass({
+ propTypes: {
+ className: React.PropTypes.string,
+ children: React.PropTypes.node,
+ onClick: React.PropTypes.func
+ },
+
+ render() {
+ let { className, children } = this.props;
+
+ className = classNames(
+ 'GitBook-ButtonGroup',
+ className
+ );
+
+ return <div className={className}>{children}</div>;
+ }
+});
+
+module.exports = ButtonGroup;
diff --git a/packages/gitbook-core/src/components/ContextProvider.js b/packages/gitbook-core/src/components/ContextProvider.js
new file mode 100644
index 0000000..96a44e3
--- /dev/null
+++ b/packages/gitbook-core/src/components/ContextProvider.js
@@ -0,0 +1,34 @@
+const React = require('react');
+const { Provider } = require('react-redux');
+
+const ContextShape = require('../propTypes/Context');
+
+/**
+ * React component to provide a GitBook context to children components.
+ */
+
+const ContextProvider = React.createClass({
+ propTypes: {
+ context: ContextShape.isRequired,
+ children: React.PropTypes.node
+ },
+
+ childContextTypes: {
+ gitbook: ContextShape
+ },
+
+ getChildContext() {
+ const { context } = this.props;
+
+ return {
+ gitbook: context
+ };
+ },
+
+ render() {
+ const { context, children } = this.props;
+ return <Provider store={context.store}>{children}</Provider>;
+ }
+});
+
+module.exports = ContextProvider;
diff --git a/packages/gitbook-core/src/components/Dropdown.js b/packages/gitbook-core/src/components/Dropdown.js
new file mode 100644
index 0000000..83a377f
--- /dev/null
+++ b/packages/gitbook-core/src/components/Dropdown.js
@@ -0,0 +1,126 @@
+const React = require('react');
+const classNames = require('classnames');
+
+/**
+ * Dropdown to display a menu
+ *
+ * <Dropdown.Container>
+ *
+ * <Button />
+ *
+ * <Dropdown.Menu>
+ * <Dropdown.Item href={...}> ... </Dropdown.Item>
+ * <Dropdown.Item onClick={...}> ... </Dropdown.Item>
+ * </Dropdown.Menu>
+ * </Dropdown.Container>
+ */
+
+const DropdownContainer = React.createClass({
+ propTypes: {
+ className: React.PropTypes.string,
+ children: React.PropTypes.node
+ },
+
+ render() {
+ let { className, children } = this.props;
+
+ className = classNames(
+ 'GitBook-Dropdown',
+ className
+ );
+
+ return (
+ <div className={className}>
+ {children}
+ </div>
+ );
+ }
+});
+
+/**
+ * A dropdown item which can contains informations.
+ */
+const DropdownItem = React.createClass({
+ propTypes: {
+ children: React.PropTypes.node
+ },
+
+ render() {
+ const { children } = this.props;
+
+ return (
+ <div className="GitBook-DropdownItem">
+ {children}
+ </div>
+ );
+ }
+});
+
+
+/**
+ * A dropdown item, which is always a link.
+ */
+const DropdownItemLink = React.createClass({
+ propTypes: {
+ children: React.PropTypes.node,
+ onClick: React.PropTypes.func,
+ href: React.PropTypes.string
+ },
+
+ onClick(event) {
+ const { onClick, href } = this.props;
+
+ if (href) {
+ return;
+ }
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ if (onClick) {
+ onClick();
+ }
+ },
+
+ render() {
+ const { children, href, ...otherProps } = this.props;
+
+ return (
+ <a {...otherProps} className="GitBook-DropdownItemLink" href={href || '#'} onClick={this.onClick} >
+ {children}
+ </a>
+ );
+ }
+});
+
+
+/**
+ * A DropdownMenu to display DropdownItems. Must be inside a
+ * DropdownContainer.
+ */
+const DropdownMenu = React.createClass({
+ propTypes: {
+ className: React.PropTypes.string,
+ children: React.PropTypes.node
+ },
+
+ render() {
+ let { className, children } = this.props;
+ className = classNames('GitBook-DropdownMenu', className);
+
+ return (
+ <div className={className}>
+ {children}
+ </div>
+ );
+ }
+});
+
+const Dropdown = {
+ Item: DropdownItem,
+ ItemLink: DropdownItemLink,
+ Menu: DropdownMenu,
+ Container: DropdownContainer
+};
+
+module.exports = Dropdown;
diff --git a/packages/gitbook-core/src/components/HTMLContent.js b/packages/gitbook-core/src/components/HTMLContent.js
new file mode 100644
index 0000000..9d15398
--- /dev/null
+++ b/packages/gitbook-core/src/components/HTMLContent.js
@@ -0,0 +1,77 @@
+const React = require('react');
+const ReactSafeHtml = require('react-safe-html');
+const { DOMProperty } = require('react-dom/lib/ReactInjection');
+const htmlTags = require('html-tags');
+const entities = require('entities');
+
+const { InjectedComponent } = require('./InjectedComponent');
+
+DOMProperty.injectDOMPropertyConfig({
+ Properties: {
+ align: DOMProperty.MUST_USE_ATTRIBUTE
+ },
+ isCustomAttribute: (attributeName) => {
+ return attributeName === 'align';
+ }
+});
+
+/*
+ HTMLContent is a container for the page HTML that parse the content and
+ render the right block.
+ All html elements can be extended using the injected component.
+ */
+
+function inject(injectedProps, Component) {
+ return (props) => {
+ const cleanProps = {
+ ...props,
+ className: props['class']
+ };
+ delete cleanProps['class'];
+
+ return (
+ <InjectedComponent {...injectedProps(cleanProps)}>
+ <Component {...cleanProps} />
+ </InjectedComponent>
+ );
+ };
+}
+
+const COMPONENTS = {
+ // Templating blocks are exported as <xblock name="youtube" props="{}" />
+ 'xblock': inject(
+ ({name, props}) => {
+ props = entities.decodeHTML(props);
+ return {
+ matching: { role: `block:${name}` },
+ props: JSON.parse(props)
+ };
+ },
+ props => <div {...props} />
+ )
+};
+
+htmlTags.forEach(tag => {
+ COMPONENTS[tag] = inject(
+ props => {
+ return {
+ matching: { role: `html:${tag}` },
+ props
+ };
+ },
+ props => React.createElement(tag, props)
+ );
+});
+
+const HTMLContent = React.createClass({
+ propTypes: {
+ html: React.PropTypes.string.isRequired
+ },
+
+ render() {
+ const { html } = this.props;
+ return <ReactSafeHtml html={html} components={COMPONENTS} />;
+ }
+});
+
+module.exports = HTMLContent;
diff --git a/packages/gitbook-core/src/components/HotKeys.js b/packages/gitbook-core/src/components/HotKeys.js
new file mode 100644
index 0000000..e2a8154
--- /dev/null
+++ b/packages/gitbook-core/src/components/HotKeys.js
@@ -0,0 +1,59 @@
+const React = require('react');
+const Mousetrap = require('mousetrap');
+const { Map } = require('immutable');
+
+/**
+ * Defines hotkeys globally when this component is mounted.
+ *
+ * keyMap = {
+ * 'escape': (e) => quit()
+ * 'mod+s': (e) => save()
+ * }
+ *
+ * <HotKeys keyMap={keyMap}>
+ * < ... />
+ * </HotKeys>
+ */
+
+const HotKeys = React.createClass({
+ propTypes: {
+ children: React.PropTypes.node.isRequired,
+ keyMap: React.PropTypes.objectOf(React.PropTypes.func)
+ },
+
+ getDefaultProps() {
+ return { keyMap: {} };
+ },
+
+ updateBindings(keyMap) {
+ Map(keyMap).forEach((handler, key) => {
+ Mousetrap.bind(key, handler);
+ });
+ },
+
+ clearBindings(keyMap) {
+ Map(keyMap).forEach((handler, key) => {
+ Mousetrap.unbind(key, handler);
+ });
+ },
+
+ componentDidMount() {
+ this.updateBindings(this.props.keyMap);
+ },
+
+ componentDidUpdate(prevProps) {
+ this.clearBindings(prevProps.keyMap);
+ this.updateBindings(this.props.keyMap);
+ },
+
+ componentWillUnmount() {
+ this.clearBindings(this.props.keyMap);
+ },
+
+ render() {
+ // Simply render the only child
+ return React.Children.only(this.props.children);
+ }
+});
+
+module.exports = HotKeys;
diff --git a/packages/gitbook-core/src/components/I18nProvider.js b/packages/gitbook-core/src/components/I18nProvider.js
new file mode 100644
index 0000000..b6b2d0f
--- /dev/null
+++ b/packages/gitbook-core/src/components/I18nProvider.js
@@ -0,0 +1,28 @@
+const { Map } = require('immutable');
+const React = require('react');
+const intl = require('react-intl');
+const ReactRedux = require('react-redux');
+
+const I18nProvider = React.createClass({
+ propTypes: {
+ children: React.PropTypes.node,
+ messages: React.PropTypes.object
+ },
+
+ render() {
+ let { messages } = this.props;
+ messages = messages.get('en', Map()).toJS();
+
+ return (
+ <intl.IntlProvider locale={'en'} messages={messages}>
+ {this.props.children}
+ </intl.IntlProvider>
+ );
+ }
+});
+
+const mapStateToProps = state => {
+ return { messages: state.i18n.messages };
+};
+
+module.exports = ReactRedux.connect(mapStateToProps)(I18nProvider);
diff --git a/packages/gitbook-core/src/components/Icon.js b/packages/gitbook-core/src/components/Icon.js
new file mode 100644
index 0000000..5f2c751
--- /dev/null
+++ b/packages/gitbook-core/src/components/Icon.js
@@ -0,0 +1,28 @@
+const React = require('react');
+
+const Icon = React.createClass({
+ propTypes: {
+ id: React.PropTypes.string,
+ type: React.PropTypes.string,
+ className: React.PropTypes.string
+ },
+
+ getDefaultProps() {
+ return {
+ type: 'fa'
+ };
+ },
+
+ render() {
+ const { id, type } = this.props;
+ let { className } = this.props;
+
+ if (id) {
+ className = 'GitBook-Icon ' + type + ' ' + type + '-' + id;
+ }
+
+ return <i className={className}/>;
+ }
+});
+
+module.exports = Icon;
diff --git a/packages/gitbook-core/src/components/Import.js b/packages/gitbook-core/src/components/Import.js
new file mode 100644
index 0000000..68318b9
--- /dev/null
+++ b/packages/gitbook-core/src/components/Import.js
@@ -0,0 +1,48 @@
+const React = require('react');
+const Head = require('react-helmet');
+const ReactRedux = require('react-redux');
+
+/**
+ * Resolve a file url to a relative url in current state
+ * @param {String} href
+ * @param {State} state
+ * @return {String}
+ */
+function resolveForCurrentFile(href, state) {
+ const { file } = state;
+ return file.relative(href);
+}
+
+const ImportLink = ReactRedux.connect((state, {rel, href}) => {
+ href = resolveForCurrentFile(href, state);
+
+ return {
+ link: [
+ {
+ rel,
+ href
+ }
+ ]
+ };
+})(Head);
+
+const ImportScript = ReactRedux.connect((state, {type, src}) => {
+ src = resolveForCurrentFile(src, state);
+
+ return {
+ script: [
+ {
+ type,
+ src
+ }
+ ]
+ };
+})(Head);
+
+const ImportCSS = props => <ImportLink rel="stylesheet" {...props} />;
+
+module.exports = {
+ ImportLink,
+ ImportScript,
+ ImportCSS
+};
diff --git a/packages/gitbook-core/src/components/InjectedComponent.js b/packages/gitbook-core/src/components/InjectedComponent.js
new file mode 100644
index 0000000..097edaf
--- /dev/null
+++ b/packages/gitbook-core/src/components/InjectedComponent.js
@@ -0,0 +1,117 @@
+const React = require('react');
+const ReactRedux = require('react-redux');
+const { List } = require('immutable');
+
+const { findMatchingComponents } = require('../actions/components');
+
+/*
+ Public: InjectedComponent makes it easy to include a set of dynamically registered
+ components inside of your React render method. Rather than explicitly render
+ an array of buttons, for example, you can use InjectedComponentSet:
+
+ ```js
+ <InjectedComponentSet className="message-actions"
+ matching={{role: 'ThreadActionButton'}}
+ props={{ a: 1 }}>
+ ```
+
+ InjectedComponentSet will look up components registered for the location you provide,
+ render them inside a {Flexbox} and pass them `exposedProps`. By default, all injected
+ children are rendered inside {UnsafeComponent} wrappers to prevent third-party code
+ from throwing exceptions that break React renders.
+
+ InjectedComponentSet monitors the ComponentStore for changes. If a new component
+ is registered into the location you provide, InjectedComponentSet will re-render.
+ If no matching components is found, the InjectedComponent renders an empty span.
+ */
+
+const Injection = React.createClass({
+ propTypes: {
+ component: React.PropTypes.func,
+ props: React.PropTypes.object,
+ children: React.PropTypes.node
+ },
+
+ render() {
+ const Comp = this.props.component;
+ const { props, children } = this.props;
+
+ // TODO: try to render with an error handling for unsafe component
+ return <Comp {...props}>{children}</Comp>;
+ }
+});
+
+const InjectedComponentSet = React.createClass({
+ propTypes: {
+ components: React.PropTypes.oneOfType([
+ React.PropTypes.arrayOf(React.PropTypes.func),
+ React.PropTypes.instanceOf(List)
+ ]).isRequired,
+ props: React.PropTypes.object,
+ children: React.PropTypes.node
+ },
+
+ render() {
+ const { components, props, children, ...divProps } = this.props;
+
+ const inner = components.map((Comp, i) => <Injection key={i} component={Comp} props={props} />);
+
+ return (
+ <div {...divProps}>
+ {children}
+ {inner}
+ </div>
+ );
+ }
+});
+
+/**
+ * Render only the first component matching
+ */
+const InjectedComponent = React.createClass({
+ propTypes: {
+ components: React.PropTypes.oneOfType([
+ React.PropTypes.arrayOf(React.PropTypes.func),
+ React.PropTypes.instanceOf(List)
+ ]).isRequired,
+ props: React.PropTypes.object,
+ children: React.PropTypes.node
+ },
+
+ render() {
+ let { components, props, children } = this.props;
+
+ if (!children) {
+ children = null;
+ } else {
+ children = React.Children.only(children);
+ }
+
+ return components.reduce((inner, Comp) => {
+ return (
+ <Injection component={Comp} props={props}>
+ {inner}
+ </Injection>
+ );
+ }, children);
+ }
+});
+
+/**
+ * Map Redux state to InjectedComponentSet's props
+ */
+function mapStateToProps(state, props) {
+ const { components } = state;
+ const { matching } = props;
+
+ return {
+ components: findMatchingComponents(components, matching)
+ };
+}
+
+const connect = ReactRedux.connect(mapStateToProps);
+
+module.exports = {
+ InjectedComponent: connect(InjectedComponent),
+ InjectedComponentSet: connect(InjectedComponentSet)
+};
diff --git a/packages/gitbook-core/src/components/Link.js b/packages/gitbook-core/src/components/Link.js
new file mode 100644
index 0000000..ab364bb
--- /dev/null
+++ b/packages/gitbook-core/src/components/Link.js
@@ -0,0 +1,37 @@
+const React = require('react');
+const ReactRedux = require('react-redux');
+
+const File = require('../models/File');
+const SummaryArticle = require('../models/SummaryArticle');
+const SummaryArticleShape = require('../propTypes/SummaryArticle');
+const FileShape = require('../propTypes/File');
+
+const Link = React.createClass({
+ propTypes: {
+ currentFile: FileShape,
+ children: React.PropTypes.node,
+
+ // Destination of the link
+ to: React.PropTypes.oneOfType([
+ React.PropTypes.string,
+ SummaryArticleShape,
+ FileShape
+ ])
+ },
+
+ render() {
+ const { currentFile, to, children, ...props } = this.props;
+ let href = to;
+
+ if (SummaryArticle.is(to) || File.is(to)) {
+ href = to.url;
+ }
+
+ href = currentFile.relative(href);
+ return <a href={href} {...props}>{children}</a>;
+ }
+});
+
+module.exports = ReactRedux.connect(state => {
+ return { currentFile: state.file };
+})(Link);
diff --git a/packages/gitbook-core/src/components/PJAXWrapper.js b/packages/gitbook-core/src/components/PJAXWrapper.js
new file mode 100644
index 0000000..6ed0697
--- /dev/null
+++ b/packages/gitbook-core/src/components/PJAXWrapper.js
@@ -0,0 +1,102 @@
+const React = require('react');
+const ReactRedux = require('react-redux');
+const History = require('../actions/history');
+
+/**
+ * Check if an element is inside a link
+ * @param {DOMElement} el
+ * @param {String} name
+ * @return {DOMElement|undefined
+ */
+function findParentByTagName(el, name) {
+ while (el && el !== document) {
+ if (el.tagName && el.tagName.toUpperCase() === name) {
+ return el;
+ }
+ el = el.parentNode;
+ }
+
+ return false;
+}
+
+/**
+ * Internal: Return the `href` component of given URL object with the hash
+ * portion removed.
+ *
+ * @param {Location|HTMLAnchorElement} location
+ * @return {String}
+ */
+function stripHash(location) {
+ return location.href.replace(/#.*/, '');
+}
+
+/**
+ * Test if a click event should be handled,
+ * return the new url if it's a normal lcick
+ */
+function getHrefForEvent(event) {
+ const link = findParentByTagName(event.target, 'A');
+
+ if (!link)
+ return;
+
+ // Middle click, cmd click, and ctrl click should open
+ // links in a new tab as normal.
+ if (event.which > 1 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
+ return;
+
+ // Ignore cross origin links
+ if (location.protocol !== link.protocol || location.hostname !== link.hostname)
+ return;
+
+ // Ignore case when a hash is being tacked on the current URL
+ if (link.href.indexOf('#') > -1 && stripHash(link) == stripHash(location))
+ return;
+
+ // Ignore event with default prevented
+ if (event.defaultPrevented)
+ return;
+
+ // Explicitly ignored
+ if (link.getAttribute('data-nopjax'))
+ return;
+
+ return link.pathname;
+}
+
+/*
+ Wrapper to bind all navigation events to fetch pages.
+ */
+
+const PJAXWrapper = React.createClass({
+ propTypes: {
+ children: React.PropTypes.node,
+ dispatch: React.PropTypes.func
+ },
+
+ onClick(event) {
+ const { dispatch } = this.props;
+ const href = getHrefForEvent(event);
+
+ if (!href) {
+ return;
+ }
+
+ event.preventDefault();
+ dispatch(History.push(href));
+ },
+
+ componentDidMount() {
+ document.addEventListener('click', this.onClick, false);
+ },
+
+ componentWillUnmount() {
+ document.removeEventListener('click', this.onClick, false);
+ },
+
+ render() {
+ return React.Children.only(this.props.children);
+ }
+});
+
+module.exports = ReactRedux.connect()(PJAXWrapper);
diff --git a/packages/gitbook-core/src/components/Panel.js b/packages/gitbook-core/src/components/Panel.js
new file mode 100644
index 0000000..694cc29
--- /dev/null
+++ b/packages/gitbook-core/src/components/Panel.js
@@ -0,0 +1,22 @@
+const React = require('react');
+const classNames = require('classnames');
+
+const Panel = React.createClass({
+ propTypes: {
+ className: React.PropTypes.string,
+ children: React.PropTypes.node
+ },
+
+ render() {
+ let { className, children } = this.props;
+ className = classNames('GitBook-Panel', className);
+
+ return (
+ <div className={className}>
+ {children}
+ </div>
+ );
+ }
+});
+
+module.exports = Panel;
diff --git a/packages/gitbook-core/src/components/Tooltipped.js b/packages/gitbook-core/src/components/Tooltipped.js
new file mode 100644
index 0000000..4d297fd
--- /dev/null
+++ b/packages/gitbook-core/src/components/Tooltipped.js
@@ -0,0 +1,44 @@
+const React = require('react');
+const classNames = require('classnames');
+
+const POSITIONS = {
+ BOTTOM_RIGHT: 'e',
+ BOTTOM_LEFT: 'w',
+ TOP_LEFT: 'nw',
+ TOP_RIGHT: 'ne',
+ BOTTOM: '',
+ TOP: 'n'
+};
+
+const Tooltipped = React.createClass({
+ propTypes: {
+ title: React.PropTypes.string.isRequired,
+ position: React.PropTypes.string,
+ open: React.PropTypes.bool,
+ children: React.PropTypes.node
+ },
+
+ statics: {
+ POSITIONS
+ },
+
+ render() {
+ const { title, position, open, children } = this.props;
+
+ const className = classNames(
+ 'GitBook-Tooltipped',
+ position ? 'Tooltipped-' + position : '',
+ {
+ 'Tooltipped-o': open
+ }
+ );
+
+ return (
+ <div className={className} aria-label={title}>
+ {children}
+ </div>
+ );
+ }
+});
+
+module.exports = Tooltipped;