From 53bde8c3f9c4668e62dbd2c670aaacc2bd820427 Mon Sep 17 00:00:00 2001
From: Andy Peatling
Date: Fri, 20 Nov 2015 07:54:14 -0800
Subject: [PATCH] Initial commit of wp-calypso
Props to the following people whose hard work has gotten us here.
@aduth
@mtias
@scruffian
@timmyc
@rads
@rralian
@drewblaisdell
@blowery
@stephanethomas
@ebinnion
@gcorne
@lezama
@bluefuton
@retrofox
@ehg
@dzver
@isaackeyet
@enej
@kellychoffman
@johnHackworth
@jonathansadowski
@shaunandrews
@apeatling
@ockham
@breezyskies
@sirbrillig
@mcsf
@gziolo
@seear
@adambbecker
@fabianapsimoes
@roccotripaldi
@melchoyce
@beaulebens
@jancavan
@rase-
@umurkontaci
@mattsherman
@mattm
@jkudish
@dmsnell
@folletto
@yoavf
@johngodley
@janm6k
@jblz
@nb
@fredrocious
@dllh
@omarjackman
@mjangda
@deBhal
@nprasath002
@matthusby
@allendav
@rodrigoi
@akirk
@rickybanister
@kwight
@justinkropp
@mattwiebe
@gravityrail
@drw158
@hewsut
@codebykat
@kateray
@alternatekev
@klimeryk
@jasmussen
@hafizrahman
@TooTallNate
@justinshreve
@lucasartoni
@spncrb
@lancewillett
@peterbutler
@skeltoac
@dbspringer
@mikeshelton1503
@zinigor
@Tsarp
@artpi
@mdawaffe
@scottsweb
@michaeldcain
@gwwar
@jenial
@Tug
@jeremylduvall
@jeffstieler
@hugobaeta
@xyu
@bikedorkjon
@beaucollins
@danhauk
@koke
@bazza
@wahjava
---
.dockerignore | 2 +
.editorconfig | 12 +
.esformatter | 76 +
.eslintignore | 2 +
.eslintrc | 83 +
.gitignore | 35 +
.jsfmtrc | 75 +
.npmrc | 2 +
.rtlcssrc | 10 +
CONTRIBUTING.md | 70 +
CREDITS.md | 1174 ++++++++++
Dockerfile | 39 +
LICENSE.md | 264 +++
Makefile | 152 ++
README.md | 161 ++
Vagrantfile | 11 +
Vagrantfile-boot2docker | 29 +
assets/stylesheets/README.md | 10 +
assets/stylesheets/_components.scss | 301 +++
assets/stylesheets/editor.scss | 4 +
assets/stylesheets/layout/_detail-page.scss | 148 ++
assets/stylesheets/layout/_main.scss | 475 ++++
assets/stylesheets/layout/_masterbar.scss | 335 +++
assets/stylesheets/layout/_overlay.scss | 204 ++
assets/stylesheets/layout/_sidebar.scss | 286 +++
.../sections/_billing-history.scss | 630 ++++++
assets/stylesheets/sections/_checkout.scss | 1077 ++++++++++
assets/stylesheets/sections/_devdocs.scss | 217 ++
.../stylesheets/sections/_domain-search.scss | 290 +++
.../sections/_keyboard-shortcuts.scss | 71 +
assets/stylesheets/sections/_manage.scss | 28 +
assets/stylesheets/sections/_menus.scss | 1034 +++++++++
.../stylesheets/sections/_notifications.scss | 59 +
assets/stylesheets/sections/_nux-welcome.scss | 164 ++
assets/stylesheets/sections/_plugins.scss | 56 +
.../sections/_post-relative-time-status.scss | 40 +
.../stylesheets/sections/_posts-controls.scss | 83 +
assets/stylesheets/sections/_posts.scss | 349 +++
assets/stylesheets/sections/_sharing.scss | 1175 ++++++++++
.../stylesheets/sections/_site-settings.scss | 221 ++
assets/stylesheets/sections/_sites.scss | 276 +++
assets/stylesheets/sections/_stats.scss | 1742 +++++++++++++++
assets/stylesheets/sections/_translator.scss | 102 +
.../sections/_updated-confirmation.scss | 147 ++
assets/stylesheets/sections/_upgrades.scss | 340 +++
assets/stylesheets/shared/_animation.scss | 255 +++
assets/stylesheets/shared/_colors.scss | 40 +
assets/stylesheets/shared/_dropdowns.scss | 124 ++
assets/stylesheets/shared/_extends.scss | 124 ++
assets/stylesheets/shared/_forms.scss | 409 ++++
assets/stylesheets/shared/_functions.scss | 11 +
.../shared/_infinite-scroll-end.scss | 29 +
assets/stylesheets/shared/_livechat.scss | 1019 +++++++++
assets/stylesheets/shared/_reset.scss | 78 +
assets/stylesheets/shared/_toolbar-bulk.scss | 248 +++
assets/stylesheets/shared/_typography.scss | 26 +
assets/stylesheets/shared/_utilities.scss | 26 +
assets/stylesheets/shared/_welcome.scss | 65 +
.../shared/mixins/_breakpoints.scss | 58 +
assets/stylesheets/shared/mixins/_calc.scss | 6 +
.../stylesheets/shared/mixins/_clear-fix.scss | 13 +
.../shared/mixins/_dropdown-menu.scss | 62 +
.../mixins/_hide-content-accessibly.scss | 12 +
.../shared/mixins/_long-content-fade.scss | 61 +
assets/stylesheets/shared/mixins/_mixins.scss | 9 +
.../stylesheets/shared/mixins/_noticon.scss | 27 +
.../shared/mixins/_placeholder.scss | 14 +
.../shared/mixins/_stats-fade-text.scss | 17 +
assets/stylesheets/style.scss | 50 +
bin/bundler | 1 +
bin/generate-devdocs-index | 1 +
bin/get-i18n | 1 +
bin/i18nlint | 1 +
bin/list-assets | 1 +
bin/live-reload | 13 +
bin/pre-commit | 44 +
bin/pre-push | 26 +
bin/record-env | 19 +
bin/run-all-tests | 87 +
bin/run-tests | 10 +
bin/update-dependency | 77 +
client/README.md | 31 +
client/accept-invite/actions.js | 23 +
client/accept-invite/controller.js | 24 +
client/accept-invite/index.js | 16 +
.../invite-form-header/index.jsx | 23 +
.../invite-form-header/style.scss | 16 +
client/accept-invite/invite-header/index.jsx | 66 +
.../accept-invite/invite-header/mock-data.js | 33 +
client/accept-invite/invite-header/style.scss | 36 +
.../accept-invite/logged-in-accept/index.jsx | 107 +
.../accept-invite/logged-in-accept/style.scss | 37 +
.../accept-invite/logged-out-invite/index.jsx | 15 +
.../logged-out-invite/signup-form.jsx | 123 ++
.../logged-out-invite/style.scss | 4 +
client/accept-invite/main.jsx | 96 +
client/accept-invite/style.scss | 8 +
client/analytics/Makefile | 11 +
client/analytics/README.md | 117 +
client/analytics/ad-tracking.js | 189 ++
client/analytics/index.js | 199 ++
client/analytics/super-props.js | 38 +
client/analytics/test/analytics-tests.js | 68 +
client/analytics/test/config.js | 7 +
client/analytics/test/load-script.js | 10 +
client/auth/Makefile | 7 +
client/auth/controller.js | 34 +
client/auth/index.js | 13 +
client/auth/login.jsx | 156 ++
client/auth/style.scss | 161 ++
client/auth/test/login.jsx | 74 +
client/boot/README.md | 21 +
client/boot/index.js | 321 +++
client/components/README.md | 6 +
client/components/accordion/Makefile | 7 +
client/components/accordion/README.md | 32 +
client/components/accordion/docs/example.jsx | 77 +
client/components/accordion/index.jsx | 95 +
client/components/accordion/section.jsx | 15 +
client/components/accordion/style.scss | 133 ++
client/components/accordion/test/index.jsx | 89 +
client/components/add-new-button/README.md | 23 +
.../add-new-button/docs/example.jsx | 34 +
client/components/add-new-button/index.jsx | 57 +
client/components/add-new-button/style.scss | 46 +
client/components/author-selector/README.md | 19 +
client/components/author-selector/index.jsx | 290 +++
client/components/author-selector/style.scss | 79 +
client/components/bulk-select/README.md | 22 +
.../components/bulk-select/docs/example.jsx | 66 +
client/components/bulk-select/index.jsx | 50 +
client/components/bulk-select/style.scss | 28 +
client/components/button-group/README.md | 20 +
.../components/button-group/docs/example.jsx | 69 +
client/components/button-group/index.jsx | 30 +
client/components/button-group/style.scss | 45 +
client/components/button/README.md | 24 +
client/components/button/docs/example.jsx | 105 +
client/components/button/index.jsx | 46 +
client/components/button/style.scss | 216 ++
client/components/chart/README.md | 50 +
client/components/chart/bar-container.jsx | 65 +
client/components/chart/bar.jsx | 133 ++
client/components/chart/index.jsx | 161 ++
client/components/chart/label.jsx | 35 +
client/components/chart/legend.jsx | 86 +
client/components/chart/style.scss | 566 +++++
client/components/chart/tooltip.jsx | 47 +
client/components/chart/x-axis.jsx | 107 +
client/components/comment-button/README.md | 23 +
.../comment-button/docs/example.jsx | 26 +
client/components/comment-button/index.jsx | 47 +
client/components/comment-button/style.scss | 23 +
client/components/count/Makefile | 10 +
client/components/count/README.md | 30 +
client/components/count/docs/example.jsx | 28 +
client/components/count/index.jsx | 21 +
client/components/count/style.scss | 11 +
client/components/count/test/index.jsx | 98 +
.../data/activating-theme/README.md | 8 +
.../components/data/activating-theme/index.js | 49 +
client/components/data/cart/README.md | 28 +
client/components/data/cart/index.jsx | 30 +
.../data/category-list-data/README.md | 46 +
.../data/category-list-data/index.jsx | 120 ++
client/components/data/checkout/README.md | 28 +
client/components/data/checkout/index.jsx | 32 +
.../components/data/current-theme/README.md | 8 +
client/components/data/current-theme/index.js | 65 +
.../data/domain-management/README.md | 67 +
.../data/domain-management/dns/README.md | 48 +
.../data/domain-management/dns/index.jsx | 74 +
.../email-forwarding/README.md | 47 +
.../email-forwarding/index.jsx | 57 +
.../data/domain-management/email/README.md | 58 +
.../data/domain-management/email/index.jsx | 88 +
.../data/domain-management/index.jsx | 69 +
.../domain-management/nameservers/README.md | 48 +
.../domain-management/nameservers/index.jsx | 82 +
.../primary-domain/README.md | 47 +
.../primary-domain/index.jsx | 54 +
.../domain-management/site-redirect/README.md | 47 +
.../domain-management/site-redirect/index.jsx | 48 +
.../data/domain-management/transfer/README.md | 48 +
.../data/domain-management/transfer/index.jsx | 80 +
.../data/domain-management/whois/README.md | 54 +
.../data/domain-management/whois/index.jsx | 88 +
.../data/email-followers-data/README.md | 18 +
.../data/email-followers-data/index.jsx | 110 +
.../components/data/followers-data/README.md | 18 +
.../components/data/followers-data/index.jsx | 109 +
.../media-library-selected-data/README.md | 30 +
.../media-library-selected-data/index.jsx | 50 +
.../components/data/media-list-data/Makefile | 8 +
.../components/data/media-list-data/README.md | 33 +
.../components/data/media-list-data/index.jsx | 85 +
.../data/media-list-data/test/index.js | 3 +
.../data/media-list-data/test/specs/utils.js | 43 +
.../components/data/media-list-data/utils.js | 36 +
.../data/media-validation-data/README.md | 30 +
.../data/media-validation-data/index.jsx | 50 +
.../data/page-templates-data/README.md | 41 +
.../data/page-templates-data/index.jsx | 99 +
.../data/post-counts-data/README.md | 28 +
.../data/post-counts-data/index.jsx | 76 +
.../data/post-formats-data/README.md | 30 +
.../data/post-formats-data/index.jsx | 62 +
.../data/preferences-data/README.md | 30 +
.../data/preferences-data/index.jsx | 45 +
client/components/data/purchases/README.md | 26 +
.../purchases/edit-card-details/index.jsx | 57 +
client/components/data/purchases/index.jsx | 43 +
.../data/purchases/manage-purchase/index.jsx | 60 +
.../data/sharing-connections-data/Makefile | 8 +
.../data/sharing-connections-data/README.md | 40 +
.../data/sharing-connections-data/index.jsx | 70 +
.../sharing-connections-data/test/index.jsx | 132 ++
.../components/data/tag-list-data/README.md | 40 +
.../components/data/tag-list-data/index.jsx | 71 +
client/components/data/viewers-data/README.md | 18 +
client/components/data/viewers-data/index.jsx | 100 +
client/components/date-picker/README.md | 68 +
client/components/date-picker/day.jsx | 121 ++
.../components/date-picker/docs/example.jsx | 75 +
client/components/date-picker/index.jsx | 139 ++
client/components/date-picker/style.scss | 232 ++
client/components/dialog/README.md | 153 ++
client/components/dialog/dialog-base.jsx | 163 ++
client/components/dialog/index.jsx | 91 +
client/components/dialog/style.scss | 83 +
client/components/domains/README.md | 42 +
.../domain-mapping-suggestion/index.jsx | 45 +
.../domain-mapping-suggestion/style.scss | 40 +
.../domains/domain-product-price/index.jsx | 37 +
.../domains/domain-product-price/style.scss | 63 +
.../domain-registration-suggestion/index.jsx | 62 +
.../domains/domain-search-results/index.jsx | 157 ++
.../domains/domain-search-results/style.scss | 24 +
.../domains/domain-suggestion/index.jsx | 68 +
.../domains/domain-suggestion/style.scss | 90 +
.../example-domain-suggestions/index.jsx | 123 ++
.../example-domain-suggestions/style.scss | 84 +
.../domains/map-domain-step/index.jsx | 238 +++
.../domains/map-domain-step/style.scss | 56 +
.../components/domains/map-domain/index.jsx | 89 +
.../domains/register-domain-step/index.jsx | 438 ++++
.../domains/register-domain-step/style.scss | 41 +
client/components/drop-zone/Makefile | 7 +
client/components/drop-zone/README.md | 51 +
client/components/drop-zone/docs/example.jsx | 72 +
client/components/drop-zone/index.jsx | 176 ++
client/components/drop-zone/style.scss | 61 +
client/components/drop-zone/test/index.jsx | 211 ++
.../components/email-verification/README.md | 21 +
.../email-verification-notice.jsx | 163 ++
client/components/email-verification/index.js | 27 +
client/components/emojify/README.md | 38 +
client/components/emojify/index.jsx | 84 +
client/components/emojify/style.scss | 10 +
client/components/external-link/README.md | 35 +
.../components/external-link/docs/example.jsx | 30 +
client/components/external-link/index.jsx | 43 +
client/components/external-link/style.scss | 6 +
client/components/flag/README.md | 19 +
client/components/flag/docs/example.jsx | 38 +
client/components/flag/index.jsx | 29 +
client/components/flag/style.scss | 31 +
client/components/foldable-card/README.md | 43 +
.../components/foldable-card/docs/example.jsx | 58 +
client/components/foldable-card/index.jsx | 144 ++
client/components/foldable-card/style.scss | 165 ++
client/components/follow-button/README.md | 41 +
client/components/follow-button/_style.scss | 95 +
client/components/follow-button/button.jsx | 63 +
.../components/follow-button/docs/example.jsx | 36 +
client/components/follow-button/index.jsx | 66 +
client/components/follow-button/style.scss | 64 +
client/components/forms/README.md | 88 +
.../forms/clipboard-button/README.md | 45 +
.../forms/clipboard-button/docs/example.jsx | 43 +
.../forms/clipboard-button/index.jsx | 58 +
.../forms/counted-textarea/Makefile | 13 +
.../forms/counted-textarea/README.md | 43 +
.../forms/counted-textarea/docs/example.jsx | 41 +
.../forms/counted-textarea/index.jsx | 81 +
.../forms/counted-textarea/style.scss | 24 +
.../forms/counted-textarea/test/index.jsx | 144 ++
client/components/forms/docs/example.jsx | 237 ++
client/components/forms/form-button/index.jsx | 45 +
.../components/forms/form-button/style.scss | 4 +
.../forms/form-buttons-bar/index.jsx | 21 +
.../forms/form-buttons-bar/style.scss | 3 +
.../components/forms/form-checkbox/index.jsx | 19 +
.../forms/form-country-select/index.jsx | 41 +
.../forms/form-country-select/style.scss | 9 +
.../components/forms/form-fieldset/index.jsx | 19 +
.../components/forms/form-fieldset/style.scss | 4 +
.../forms/form-input-validation/index.jsx | 27 +
.../forms/form-input-validation/style.scss | 25 +
client/components/forms/form-label/index.jsx | 19 +
client/components/forms/form-label/style.scss | 11 +
client/components/forms/form-legend/index.jsx | 19 +
.../components/forms/form-legend/style.scss | 8 +
.../forms/form-password-input/index.jsx | 63 +
.../forms/form-password-input/style.scss | 28 +
.../forms/form-phone-input/Makefile | 13 +
.../forms/form-phone-input/index.jsx | 158 ++
.../forms/form-phone-input/test/index.jsx | 76 +
.../test/mock-countries-list-empty.js | 14 +
.../test/mock-countries-list.js | 21 +
client/components/forms/form-radio/index.jsx | 22 +
client/components/forms/form-range/index.jsx | 49 +
client/components/forms/form-range/style.scss | 66 +
.../forms/form-section-heading/index.jsx | 19 +
.../forms/form-section-heading/style.scss | 9 +
client/components/forms/form-select/index.jsx | 19 +
.../components/forms/form-select/style.scss | 9 +
.../forms/form-setting-explanation/index.jsx | 19 +
.../forms/form-setting-explanation/style.scss | 8 +
.../components/forms/form-tel-input/index.jsx | 34 +
.../forms/form-tel-input/style.scss | 13 +
.../form-text-input-with-affixes/README.md | 18 +
.../form-text-input-with-affixes/index.jsx | 41 +
.../form-text-input-with-affixes/style.scss | 86 +
.../forms/form-text-input/index.jsx | 44 +
.../forms/form-text-input/style.scss | 41 +
.../components/forms/form-textarea/index.jsx | 19 +
client/components/forms/form-toggle/Makefile | 7 +
client/components/forms/form-toggle/README.md | 32 +
.../components/forms/form-toggle/compact.jsx | 27 +
client/components/forms/form-toggle/index.jsx | 70 +
.../components/forms/form-toggle/style.scss | 114 +
.../forms/form-toggle/test/index.jsx | 105 +
client/components/forms/language-selector.jsx | 56 +
.../components/forms/multi-checkbox/Makefile | 7 +
.../components/forms/multi-checkbox/README.md | 52 +
.../components/forms/multi-checkbox/index.jsx | 66 +
.../forms/multi-checkbox/test/index.jsx | 89 +
client/components/forms/range/Makefile | 7 +
client/components/forms/range/README.md | 38 +
.../components/forms/range/docs/example.jsx | 44 +
client/components/forms/range/index.jsx | 98 +
client/components/forms/range/style.scss | 65 +
client/components/forms/range/test/index.jsx | 52 +
client/components/forms/select-opt-groups.jsx | 32 +
.../components/forms/sortable-list/README.md | 71 +
.../components/forms/sortable-list/index.jsx | 320 +++
.../components/forms/sortable-list/index.scss | 106 +
client/components/forms/us-state-selector.jsx | 102 +
client/components/gallery-shortcode/README.md | 103 +
client/components/gallery-shortcode/index.jsx | 124 ++
client/components/gauge/README.md | 31 +
client/components/gauge/docs/example.jsx | 26 +
client/components/gauge/index.jsx | 91 +
client/components/gauge/style.scss | 22 +
client/components/gravatar/Makefile | 7 +
client/components/gravatar/README.md | 23 +
client/components/gravatar/index.jsx | 62 +
client/components/gravatar/style.scss | 13 +
client/components/gravatar/test/index.jsx | 73 +
client/components/header-cake/README.md | 17 +
.../components/header-cake/docs/example.jsx | 30 +
client/components/header-cake/index.jsx | 54 +
client/components/header-cake/style.scss | 53 +
client/components/image-preloader/README.md | 41 +
client/components/image-preloader/index.jsx | 106 +
client/components/infinite-list/Makefile | 7 +
client/components/infinite-list/README.md | 88 +
client/components/infinite-list/index.jsx | 394 ++++
.../components/infinite-list/scroll-helper.js | 482 +++++
client/components/infinite-list/style.scss | 3 +
.../infinite-list/test/scroll-helper.js | 626 ++++++
client/components/info-popover/README.md | 27 +
.../components/info-popover/docs/example.jsx | 57 +
client/components/info-popover/index.jsx | 71 +
client/components/info-popover/style.scss | 22 +
client/components/input-chrono/README.md | 40 +
.../components/input-chrono/docs/example.jsx | 60 +
client/components/input-chrono/index.jsx | 99 +
client/components/input-chrono/style.scss | 34 +
client/components/like-button/README.md | 50 +
client/components/like-button/_style.scss | 64 +
client/components/like-button/button.jsx | 99 +
.../components/like-button/docs/example.jsx | 68 +
client/components/like-button/icons.jsx | 39 +
client/components/like-button/index.jsx | 83 +
client/components/main/README.md | 22 +
client/components/main/index.jsx | 17 +
client/components/main/style.scss | 44 +
.../mobile-back-to-sidebar/index.jsx | 31 +
.../mobile-back-to-sidebar/style.scss | 27 +
client/components/notices/docs/example.jsx | 55 +
client/components/olark-chatbox/README.md | 36 +
client/components/olark-chatbox/index.jsx | 122 ++
client/components/olark-chatbox/style.scss | 26 +
client/components/overlay/README.md | 81 +
client/components/overlay/overlay.jsx | 110 +
client/components/overlay/package.json | 6 +
client/components/overlay/toolbar.jsx | 111 +
client/components/payment-logo/index.jsx | 20 +
client/components/payment-logo/style.scss | 31 +
.../components/plans/plan-actions/index.jsx | 305 +++
.../components/plans/plan-actions/style.scss | 168 ++
.../plans/plan-discount-message/index.jsx | 52 +
.../plans/plan-discount-message/style.scss | 59 +
.../plans/plan-feature-cell/index.jsx | 18 +
.../plans/plan-feature-cell/style.scss | 45 +
.../components/plans/plan-features/index.jsx | 73 +
.../components/plans/plan-features/style.scss | 38 +
client/components/plans/plan-header/index.jsx | 24 +
.../components/plans/plan-header/style.scss | 185 ++
client/components/plans/plan-list/index.jsx | 92 +
client/components/plans/plan-list/style.scss | 5 +
client/components/plans/plan-nudge/index.jsx | 94 +
.../components/plans/plan-nudge/preview.jsx | 57 +
client/components/plans/plan-nudge/style.scss | 191 ++
client/components/plans/plan-price/index.jsx | 48 +
client/components/plans/plan-price/style.scss | 84 +
client/components/plans/plan/index.jsx | 211 ++
client/components/plans/plan/style.scss | 156 ++
.../components/plans/plans-compare/index.jsx | 172 ++
.../components/plans/plans-compare/style.scss | 10 +
.../plans/site-specific-plan-details-mixin.js | 25 +
client/components/popover/README.md | 73 +
client/components/popover/docs/example.jsx | 106 +
client/components/popover/index.jsx | 132 ++
client/components/popover/menu-item.jsx | 35 +
client/components/popover/menu.jsx | 153 ++
client/components/popover/style.scss | 196 ++
client/components/post-excerpt/index.jsx | 31 +
client/components/post-excerpt/style.scss | 37 +
client/components/post-list-fetcher/index.jsx | 156 ++
client/components/post-schedule/README.md | 70 +
client/components/post-schedule/clock.jsx | 185 ++
.../components/post-schedule/docs/example.jsx | 263 +++
.../post-schedule/header-controls.jsx | 50 +
client/components/post-schedule/header.jsx | 85 +
client/components/post-schedule/index.jsx | 222 ++
client/components/post-schedule/style.scss | 125 ++
client/components/post-schedule/utils.js | 62 +
client/components/progress-bar/README.md | 29 +
.../components/progress-bar/docs/example.jsx | 28 +
client/components/progress-bar/index.jsx | 43 +
client/components/progress-bar/style.scss | 28 +
.../components/progress-indicator/README.md | 48 +
.../components/progress-indicator/index.jsx | 48 +
.../components/progress-indicator/style.scss | 217 ++
client/components/pulsing-dot/index.jsx | 27 +
client/components/pulsing-dot/style.scss | 27 +
client/components/rating/README.md | 27 +
client/components/rating/docs/example.jsx | 27 +
client/components/rating/index.jsx | 77 +
client/components/rating/style.scss | 9 +
client/components/resizable-iframe/README.md | 46 +
client/components/resizable-iframe/index.jsx | 160 ++
client/components/root-child/Makefile | 11 +
client/components/root-child/README.md | 36 +
client/components/root-child/index.jsx | 46 +
client/components/root-child/test/index.jsx | 102 +
client/components/search-card/README.md | 5 +
client/components/search-card/index.jsx | 43 +
client/components/search-card/style.scss | 7 +
client/components/search/Makefile | 11 +
client/components/search/README.md | 43 +
client/components/search/docs/example.jsx | 42 +
client/components/search/index.jsx | 297 +++
client/components/search/style.scss | 186 ++
client/components/search/test/index.jsx | 63 +
client/components/section-header/README.md | 38 +
client/components/section-header/button.jsx | 34 +
.../section-header/docs/example.jsx | 42 +
client/components/section-header/index.jsx | 38 +
client/components/section-header/style.scss | 49 +
client/components/section-nav/Makefile | 11 +
client/components/section-nav/README.md | 160 ++
.../components/section-nav/docs/example.jsx | 196 ++
client/components/section-nav/index.jsx | 175 ++
client/components/section-nav/item.jsx | 78 +
client/components/section-nav/segmented.jsx | 74 +
client/components/section-nav/style.scss | 377 ++++
client/components/section-nav/tabs.jsx | 175 ++
client/components/section-nav/test/index.jsx | 89 +
client/components/segmented-control/README.md | 177 ++
.../segmented-control/docs/example.jsx | 139 ++
client/components/segmented-control/index.jsx | 226 ++
client/components/segmented-control/item.jsx | 51 +
.../components/segmented-control/style.scss | 85 +
client/components/select-dropdown/README.md | 192 ++
.../select-dropdown/docs/example.jsx | 108 +
client/components/select-dropdown/index.jsx | 339 +++
client/components/select-dropdown/item.jsx | 58 +
.../components/select-dropdown/separator.jsx | 15 +
client/components/select-dropdown/style.scss | 184 ++
client/components/select/docs/example.jsx | 46 +
client/components/shortcode/README.md | 61 +
client/components/shortcode/data.jsx | 43 +
client/components/shortcode/frame.jsx | 136 ++
client/components/shortcode/index.jsx | 83 +
.../components/sidebar-navigation/README.md | 26 +
.../components/sidebar-navigation/index.jsx | 38 +
.../components/sidebar-navigation/style.scss | 13 +
client/components/signup-form/README.md | 37 +
client/components/signup-form/index.jsx | 398 ++++
client/components/signup-form/style.scss | 20 +
.../README.md | 11 +
.../child.js | 116 +
.../index.js | 57 +
client/components/site-icon/README.md | 24 +
client/components/site-icon/package.json | 6 +
client/components/site-icon/site-icon.jsx | 78 +
client/components/site-icon/style.scss | 26 +
.../components/site-selector-modal/README.md | 12 +
.../components/site-selector-modal/index.jsx | 93 +
.../components/site-selector-modal/style.scss | 8 +
client/components/site-selector/README.md | 4 +
client/components/site-selector/index.jsx | 203 ++
client/components/site-selector/style.scss | 165 ++
.../site-stats-sticky-link/README.md | 26 +
.../site-stats-sticky-link/index.jsx | 60 +
.../components/site-users-fetcher/index.jsx | 118 +
.../components/site-users-fetcher/readme.md | 20 +
client/components/sites-popover/README.md | 8 +
client/components/sites-popover/index.jsx | 74 +
client/components/sites-popover/style.scss | 90 +
client/components/spinner/README.md | 45 +
client/components/spinner/docs/example.jsx | 27 +
client/components/spinner/index.jsx | 119 ++
client/components/spinner/style.scss | 128 ++
.../stat-update-indicator/README.md | 34 +
.../stat-update-indicator/index.jsx | 69 +
.../stat-update-indicator/style.scss | 15 +
client/components/sticky-panel/index.jsx | 83 +
client/components/sticky-panel/style.scss | 8 +
client/components/timezone-dropdown/README.md | 36 +
.../timezone-dropdown/docs/example.jsx | 47 +
client/components/timezone-dropdown/index.jsx | 52 +
.../components/timezone-dropdown/style.scss | 4 +
client/components/tinymce/README.md | 170 ++
client/components/tinymce/i18n.js | 78 +
client/components/tinymce/iframe.scss | 162 ++
client/components/tinymce/index.jsx | 454 ++++
.../tinymce/plugins/advanced/plugin.js | 84 +
.../tinymce/plugins/advanced/style.scss | 58 +
.../tinymce/plugins/calypso-alert/alert.jsx | 50 +
.../tinymce/plugins/calypso-alert/plugin.jsx | 54 +
.../tinymce/plugins/calypso-alert/style.scss | 5 +
.../plugins/editor-button-analytics/plugin.js | 99 +
.../tinymce/plugins/media/README.md | 15 +
.../tinymce/plugins/media/drop-zone.jsx | 140 ++
.../tinymce/plugins/media/plugin.jsx | 648 ++++++
.../plugins/media/restrict-size/Makefile | 10 +
.../plugins/media/restrict-size/index.js | 86 +
.../plugins/media/restrict-size/test/index.js | 101 +
.../tinymce/plugins/tabindex/plugin.js | 24 +
.../plugins/touch-scroll-toolbar/plugin.js | 97 +
.../plugins/wpcom-autoresize/plugin.js | 161 ++
.../tinymce/plugins/wpcom-charmap/charmap.jsx | 338 +++
.../tinymce/plugins/wpcom-charmap/plugin.js | 49 +
.../tinymce/plugins/wpcom-charmap/style.scss | 47 +
.../tinymce/plugins/wpcom-help/help-modal.jsx | 130 ++
.../tinymce/plugins/wpcom-help/plugin.js | 50 +
.../tinymce/plugins/wpcom-help/style.scss | 51 +
.../plugins/wpcom-view/gallery-view.jsx | 97 +
.../tinymce/plugins/wpcom-view/plugin.js | 876 ++++++++
.../tinymce/plugins/wpcom-view/views.js | 109 +
.../plugins/wpcom-view/views/embed/index.js | 105 +
.../plugins/wpcom-view/views/embed/view.jsx | 117 +
.../tinymce/plugins/wpcom/plugin.js | 664 ++++++
.../tinymce/plugins/wpeditimage/plugin.js | 688 ++++++
.../tinymce/plugins/wplink/dialog.jsx | 280 +++
.../tinymce/plugins/wplink/plugin.js | 190 ++
.../tinymce/plugins/wplink/style.scss | 23 +
.../tinymce/plugins/wptextpattern/plugin.js | 183 ++
client/components/tinymce/style.scss | 1902 +++++++++++++++++
client/components/token-field/Makefile | 10 +
client/components/token-field/README.md | 47 +
.../components/token-field/docs/example.jsx | 52 +
client/components/token-field/index.jsx | 446 ++++
client/components/token-field/style.scss | 120 ++
.../token-field/suggestions-list.jsx | 129 ++
.../components/token-field/test/fixtures.js | 45 +
client/components/token-field/test/index.jsx | 416 ++++
.../token-field/test/token-field-wrapper.jsx | 42 +
client/components/token-field/token-input.jsx | 53 +
client/components/token-field/token.jsx | 42 +
client/components/tooltip/index.jsx | 45 +
client/components/tooltip/style.scss | 77 +
.../components/track-input-changes/Makefile | 7 +
.../components/track-input-changes/README.md | 28 +
.../components/track-input-changes/index.jsx | 57 +
.../track-input-changes/test/index.jsx | 119 ++
client/components/typography/README.md | 6 +
client/components/typography/docs/example.jsx | 89 +
.../upgrades/credit-card-form/README.md | 58 +
.../upgrades/credit-card-form/index.jsx | 109 +
.../upgrades/credit-card-form/style.scss | 88 +
.../credit-card-number-input/README.md | 24 +
.../credit-card-number-input/index.jsx | 22 +
.../credit-card-number-input/style.scss | 25 +
.../components/upgrades/google-apps/README.md | 41 +
.../upgrades/google-apps/dialog/index.jsx | 260 +++
.../google-apps/dialog/product-details.jsx | 39 +
.../upgrades/google-apps/dialog/users.jsx | 127 ++
.../components/upgrades/google-apps/index.jsx | 78 +
client/components/user/index.jsx | 29 +
client/components/user/style.scss | 21 +
client/components/version/README.md | 39 +
client/components/version/docs/example.jsx | 28 +
client/components/version/index.jsx | 34 +
client/components/version/style.scss | 12 +
client/components/vertical-nav/README.md | 24 +
client/components/vertical-nav/index.jsx | 16 +
client/components/vertical-nav/item/index.jsx | 64 +
.../components/vertical-nav/item/style.scss | 35 +
client/components/vertical-nav/style.scss | 3 +
client/components/web-preview/README.md | 10 +
client/components/web-preview/index.jsx | 176 ++
client/components/web-preview/style.scss | 191 ++
client/components/web-preview/toolbar.jsx | 74 +
client/components/wordpress-logo/index.jsx | 29 +
client/config/.gitignore | 1 +
client/config/README.md | 32 +
client/config/regenerate.js | 60 +
client/devdocs/Makefile | 8 +
client/devdocs/README.md | 23 +
client/devdocs/controller.js | 104 +
client/devdocs/design/index.jsx | 224 ++
client/devdocs/design/style.scss | 72 +
client/devdocs/doc.jsx | 103 +
client/devdocs/form-state-examples/index.jsx | 57 +
client/devdocs/index.js | 19 +
client/devdocs/main.jsx | 170 ++
client/devdocs/service.js | 35 +
client/devdocs/sidebar.jsx | 50 +
client/devdocs/test/doc-test.jsx | 62 +
client/layout/README.md | 10 +
client/layout/community-translator/README.md | 16 +
.../community-translator/invitation-utils.js | 218 ++
.../community-translator/invitation.jsx | 92 +
.../layout/community-translator/launcher.jsx | 96 +
client/layout/community-translator/style.scss | 118 +
client/layout/error.jsx | 55 +
client/layout/index.jsx | 118 +
client/layout/logged-out-oauth.jsx | 19 +
client/layout/logged-out.jsx | 38 +
client/layout/masterbar-logged-out-menu.jsx | 29 +
client/layout/masterbar-new-post.jsx | 109 +
client/layout/masterbar-new-post.scss | 68 +
client/layout/masterbar-sections-menu.jsx | 147 ++
client/layout/masterbar.jsx | 154 ++
client/layout/package.json | 6 +
client/lib/abtest/README.md | 118 +
client/lib/abtest/active-tests.js | 66 +
client/lib/abtest/index.js | 246 +++
client/lib/accept/Makefile | 10 +
client/lib/accept/README.md | 29 +
client/lib/accept/dialog.jsx | 65 +
client/lib/accept/index.js | 35 +
client/lib/accept/style.scss | 4 +
client/lib/accept/test/index.js | 81 +
client/lib/accessible-focus/README.md | 8 +
client/lib/accessible-focus/index.js | 28 +
client/lib/account-password-data/index.js | 115 +
client/lib/ads/Makefile | 11 +
client/lib/ads/README.md | 202 ++
client/lib/ads/actions.js | 111 +
client/lib/ads/earnings-store.js | 81 +
client/lib/ads/settings-store.js | 110 +
client/lib/ads/test/lib/mock-actions.js | 26 +
client/lib/ads/test/lib/mock-earnings.js | 30 +
client/lib/ads/test/lib/mock-settings.js | 17 +
client/lib/ads/test/lib/mock-site.js | 31 +
client/lib/ads/test/test-store.js | 71 +
client/lib/ads/tos-store.js | 98 +
client/lib/ads/utils.js | 20 +
.../lib/application-passwords-data/README.md | 26 +
.../lib/application-passwords-data/index.js | 99 +
client/lib/billing-history-data/README.md | 4 +
client/lib/billing-history-data/index.js | 74 +
client/lib/cart-values/Makefile | 14 +
client/lib/cart-values/cart-items.js | 697 ++++++
client/lib/cart-values/index.js | 119 ++
client/lib/cart-values/schema.json | 54 +
client/lib/cart-values/test/abtest.js | 1 +
.../lib/cart-values/test/lib/user-settings.js | 29 +
client/lib/cart-values/test/lib/user.js | 27 +
client/lib/cart-values/test/test.js | 119 ++
client/lib/cart/store/Makefile | 15 +
client/lib/cart/store/cart-analytics.js | 26 +
client/lib/cart/store/cart-synchronizer.js | 251 +++
client/lib/cart/store/index.js | 138 ++
client/lib/cart/store/test/abtest.js | 1 +
.../cart/store/test/cart-synchronizer-test.js | 67 +
client/lib/cart/store/test/fake-wpcom.js | 50 +
.../lib/cart/store/test/lib/user-settings.js | 29 +
client/lib/cart/store/test/lib/user.js | 27 +
client/lib/comment-like-store/Makefile | 14 +
client/lib/comment-like-store/actions.js | 76 +
.../comment-like-store/comment-like-store.js | 216 ++
client/lib/comment-like-store/constants.js | 9 +
.../test/comment-like-store-test.js | 163 ++
client/lib/comment-like-store/utils.js | 8 +
client/lib/comment-store/Makefile | 14 +
client/lib/comment-store/actions.js | 140 ++
client/lib/comment-store/comment-store.js | 438 ++++
client/lib/comment-store/constants.js | 16 +
.../comment-store/test/comment-store-test.js | 121 ++
client/lib/comment-store/test/lib/wp.js | 14 +
client/lib/comment-store/utils.js | 8 +
.../lib/connected-applications-data/README.md | 20 +
.../lib/connected-applications-data/index.js | 120 ++
client/lib/connections-list/README.md | 48 +
client/lib/connections-list/index.js | 10 +
client/lib/connections-list/list.js | 592 +++++
client/lib/countries-list/README.md | 34 +
client/lib/countries-list/index.js | 188 ++
client/lib/credit-card-details/Makefile | 14 +
client/lib/credit-card-details/README.md | 24 +
client/lib/credit-card-details/index.js | 12 +
client/lib/credit-card-details/masking.js | 81 +
client/lib/credit-card-details/test/test.js | 40 +
client/lib/credit-card-details/validation.js | 191 ++
client/lib/customize/muse.js | 23 +
client/lib/data-poller/README.md | 47 +
client/lib/data-poller/index.js | 70 +
client/lib/data-poller/poller.js | 103 +
client/lib/desktop/README.md | 30 +
client/lib/desktop/index.js | 147 ++
client/lib/desktop/page-notifier.js | 30 +
.../lib/detect-history-navigation/README.md | 19 +
client/lib/detect-history-navigation/index.js | 17 +
client/lib/deterministic-stringify/Makefile | 9 +
client/lib/deterministic-stringify/README.md | 21 +
client/lib/deterministic-stringify/index.js | 32 +
.../lib/deterministic-stringify/test/test.js | 46 +
client/lib/devices/README.md | 8 +
client/lib/devices/index.js | 58 +
client/lib/domains/Makefile | 12 +
client/lib/domains/README.md | 39 +
client/lib/domains/assembler.js | 83 +
client/lib/domains/constants.js | 8 +
client/lib/domains/dns/Makefile | 9 +
client/lib/domains/dns/index.js | 87 +
client/lib/domains/dns/reducer.js | 74 +
client/lib/domains/dns/store.js | 19 +
client/lib/domains/dns/test/index.js | 28 +
client/lib/domains/email-forwarding/Makefile | 12 +
.../lib/domains/email-forwarding/reducer.js | 124 ++
client/lib/domains/email-forwarding/store.js | 15 +
.../email-forwarding/test/store-test.js | 123 ++
.../domains/google-apps-users/assembler.js | 12 +
client/lib/domains/google-apps-users/index.js | 71 +
.../lib/domains/google-apps-users/reducer.js | 46 +
client/lib/domains/google-apps-users/store.js | 19 +
client/lib/domains/index.js | 158 ++
client/lib/domains/nameservers/Makefile | 12 +
client/lib/domains/nameservers/index.js | 37 +
client/lib/domains/nameservers/reducer.js | 71 +
client/lib/domains/nameservers/store.js | 15 +
.../domains/nameservers/test/store-test.js | 95 +
client/lib/domains/reducer.js | 119 ++
client/lib/domains/site-redirect/README.md | 25 +
client/lib/domains/site-redirect/reducer.js | 105 +
client/lib/domains/site-redirect/store.js | 19 +
client/lib/domains/store.js | 11 +
client/lib/domains/test/assembler-test.js | 85 +
.../lib/domains/wapi-domain-info/assembler.js | 8 +
.../lib/domains/wapi-domain-info/reducer.js | 97 +
client/lib/domains/wapi-domain-info/store.js | 17 +
client/lib/domains/whois/Makefile | 12 +
client/lib/domains/whois/README.md | 33 +
client/lib/domains/whois/assembler.js | 29 +
client/lib/domains/whois/reducer.js | 73 +
client/lib/domains/whois/store.js | 15 +
.../lib/domains/whois/test/assembler-test.js | 110 +
client/lib/domains/whois/test/store-test.js | 134 ++
client/lib/dss/README.md | 22 +
client/lib/dss/actions.js | 59 +
client/lib/dss/constants.js | 15 +
client/lib/dss/image-store.js | 48 +
client/lib/dss/preview-store.js | 29 +
client/lib/email-followers/Makefile | 12 +
client/lib/email-followers/README.md | 84 +
client/lib/email-followers/actions.js | 61 +
client/lib/email-followers/store.js | 181 ++
.../email-followers/test/lib/mock-actions.js | 45 +
.../test/lib/mock-email-followers.js | 17 +
.../test/lib/mock-more-email-followers.js | 17 +
.../lib/email-followers/test/lib/mock-site.js | 31 +
client/lib/email-followers/test/test-store.js | 91 +
client/lib/embeds/README.md | 30 +
client/lib/embeds/actions.js | 51 +
client/lib/embeds/list-store.js | 82 +
client/lib/embeds/store.js | 59 +
client/lib/features-list/Makefile | 12 +
client/lib/features-list/README.md | 16 +
client/lib/features-list/index.js | 120 ++
client/lib/features-list/test/data.js | 47 +
client/lib/features-list/test/lib/wp.js | 4 +
client/lib/features-list/test/test.js | 38 +
client/lib/feed-post-store/Makefile | 15 +
client/lib/feed-post-store/README.md | 26 +
client/lib/feed-post-store/actions.js | 89 +
client/lib/feed-post-store/constants.js | 3 +
client/lib/feed-post-store/display-types.js | 18 +
client/lib/feed-post-store/index.js | 286 +++
.../feed-post-store/normalization-rules.js | 108 +
.../lib/feed-post-store/post-batch-fetcher.js | 104 +
.../test/feed-post-store-test.js | 144 ++
.../test/lib/post-normalizer.js | 22 +
client/lib/feed-post-store/test/lib/wp.js | 20 +
client/lib/feed-store/Makefile | 15 +
client/lib/feed-store/actions.js | 36 +
client/lib/feed-store/constants.js | 11 +
client/lib/feed-store/index.js | 104 +
.../lib/feed-store/test/feed-store-tests.js | 116 +
client/lib/feed-store/test/lib/formatting.js | 7 +
client/lib/feed-stream-store/Makefile | 15 +
client/lib/feed-stream-store/actions.js | 174 ++
client/lib/feed-stream-store/constants.js | 13 +
.../feed-stream-store/feed-stream-cache.js | 10 +
client/lib/feed-stream-store/feed-stream.js | 523 +++++
client/lib/feed-stream-store/index.js | 159 ++
.../test/lib/post-normalizer.js | 22 +
client/lib/feed-stream-store/test/lib/wp.js | 20 +
.../test/post-list-store-tests.js | 368 ++++
client/lib/follow-list/Makefile | 12 +
client/lib/follow-list/README.md | 41 +
client/lib/follow-list/index.js | 38 +
client/lib/follow-list/site.js | 61 +
client/lib/follow-list/test/lib/wp.js | 33 +
client/lib/follow-list/test/test.js | 81 +
client/lib/followers/Makefile | 13 +
client/lib/followers/README.md | 84 +
client/lib/followers/actions.js | 60 +
client/lib/followers/store.js | 181 ++
client/lib/followers/test/lib/mock-actions.js | 45 +
client/lib/followers/test/lib/mock-site.js | 31 +
.../test/lib/mock-wpcom-followers1.js | 23 +
.../test/lib/mock-wpcom-followers2.js | 23 +
client/lib/followers/test/test-store.js | 91 +
client/lib/form-state/Makefile | 7 +
client/lib/form-state/README.md | 4 +
.../form-state/examples/async-initialize.jsx | 84 +
.../form-state/examples/sync-initialize.jsx | 72 +
client/lib/form-state/index.js | 366 ++++
.../lib/form-state/store/async-initialize.js | 42 +
client/lib/form-state/store/core.js | 29 +
client/lib/form-state/store/index.js | 78 +
.../lib/form-state/store/sync-initialize.js | 36 +
client/lib/form-state/test/index.js | 160 ++
client/lib/geocoding/Makefile | 10 +
client/lib/geocoding/README.md | 16 +
client/lib/geocoding/index.js | 24 +
client/lib/geocoding/test/index.js | 42 +
client/lib/google-apps/index.js | 12 +
client/lib/happiness-engineers/Makefile | 11 +
client/lib/happiness-engineers/README.md | 76 +
client/lib/happiness-engineers/actions.js | 32 +
client/lib/happiness-engineers/constants.js | 5 +
client/lib/happiness-engineers/store.js | 37 +
.../test/lib/mock-actions.js | 9 +
.../test/lib/mock-happiness-engineers.js | 62 +
.../happiness-engineers/test/test-actions.js | 22 +
.../happiness-engineers/test/test-store.js | 47 +
client/lib/help-search/Makefile | 12 +
client/lib/help-search/README.md | 10 +
client/lib/help-search/actions.js | 33 +
client/lib/help-search/constants.js | 5 +
client/lib/help-search/store.js | 37 +
.../lib/help-search/test/lib/mock-actions.js | 9 +
.../help-search/test/lib/mock-help-links.js | 27 +
client/lib/help-search/test/test-store.js | 39 +
client/lib/highlight/Makefile | 7 +
client/lib/highlight/README.md | 17 +
client/lib/highlight/index.js | 106 +
client/lib/highlight/test/highlight-test.js | 77 +
client/lib/human-date/index.js | 55 +
client/lib/importer/actions.js | 127 ++
client/lib/importer/common.js | 75 +
client/lib/importer/constants.js | 40 +
client/lib/importer/store.js | 112 +
client/lib/infinite-list/actions.js | 48 +
client/lib/infinite-list/positions-store.js | 48 +
client/lib/infinite-list/scroll-store.js | 35 +
client/lib/inflight/index.js | 16 +
client/lib/interpolate-components/Makefile | 7 +
client/lib/interpolate-components/README.md | 40 +
client/lib/interpolate-components/index.js | 130 ++
.../interpolate-components/test/lib/warn.js | 6 +
.../lib/interpolate-components/test/text.jsx | 263 +++
client/lib/interpolate-components/tokenize.js | 33 +
client/lib/invites/Makefile | 11 +
client/lib/invites/actions.js | 52 +
client/lib/invites/constants.js | 16 +
.../invites/reducers/invites-validation.js | 27 +
client/lib/invites/reducers/list-invites.js | 38 +
.../lib/invites/stores/invites-validation.js | 12 +
client/lib/invites/stores/list-invites.js | 11 +
client/lib/invites/test/list-invites-store.js | 76 +
client/lib/keyboard-shortcuts/Makefile | 12 +
client/lib/keyboard-shortcuts/README.md | 68 +
client/lib/keyboard-shortcuts/global.js | 92 +
client/lib/keyboard-shortcuts/index.js | 166 ++
client/lib/keyboard-shortcuts/key-bindings.js | 187 ++
client/lib/keyboard-shortcuts/menu.jsx | 126 ++
.../test/lib/mixins/i18n.js | 16 +
client/lib/keyboard-shortcuts/test/test.js | 58 +
client/lib/layout-focus/README.md | 21 +
client/lib/layout-focus/index.js | 129 ++
client/lib/like-store/Makefile | 15 +
client/lib/like-store/actions.js | 124 ++
client/lib/like-store/like-store.js | 229 ++
client/lib/like-store/test/lib/wp.js | 13 +
client/lib/like-store/test/like-store-test.js | 225 ++
client/lib/like-store/utils.js | 8 +
client/lib/load-script/README.md | 28 +
client/lib/load-script/index.js | 64 +
client/lib/local-list/Makefile | 7 +
client/lib/local-list/README.md | 65 +
client/lib/local-list/index.js | 109 +
client/lib/local-list/test/test.js | 165 ++
client/lib/local-storage/Makefile | 7 +
client/lib/local-storage/README.md | 18 +
client/lib/local-storage/index.js | 53 +
client/lib/local-storage/test/test.js | 43 +
client/lib/locale-suggestions/actions.js | 22 +
client/lib/locale-suggestions/index.js | 40 +
client/lib/media-serialization/Makefile | 12 +
client/lib/media-serialization/constants.js | 15 +
.../create-element-from-string.js | 20 +
.../lib/media-serialization/detect-format.js | 39 +
client/lib/media-serialization/index.js | 18 +
.../lib/media-serialization/strategies/api.js | 41 +
.../lib/media-serialization/strategies/dom.js | 74 +
.../media-serialization/strategies/index.js | 8 +
.../media-serialization/strategies/object.js | 9 +
.../strategies/shortcode.js | 76 +
.../media-serialization/strategies/string.js | 61 +
.../media-serialization/strategies/unknown.js | 17 +
client/lib/media-serialization/test/index.js | 137 ++
client/lib/media/Makefile | 10 +
client/lib/media/README.md | 49 +
client/lib/media/actions.js | 254 +++
client/lib/media/constants.js | 182 ++
client/lib/media/library-selected-store.js | 146 ++
client/lib/media/list-store.js | 270 +++
client/lib/media/store.js | 129 ++
client/lib/media/test/actions.js | 389 ++++
.../lib/media/test/library-selected-store.js | 135 ++
client/lib/media/test/list-store.js | 388 ++++
client/lib/media/test/store.js | 164 ++
client/lib/media/test/utils.js | 463 ++++
client/lib/media/test/validation-store.js | 305 +++
client/lib/media/utils.js | 370 ++++
client/lib/media/validation-store.js | 207 ++
client/lib/menu-data/Makefile | 10 +
client/lib/menu-data/README.md | 10 +
client/lib/menu-data/index.js | 11 +
client/lib/menu-data/menu-data.js | 991 +++++++++
client/lib/menu-data/test/fixtures.js | 52 +
client/lib/menu-data/test/lib/mixins/i18n.js | 12 +
client/lib/menu-data/test/lib/sites-list.js | 21 +
client/lib/menu-data/test/lib/wp.js | 27 +
client/lib/menu-data/test/test-menu-data.js | 572 +++++
client/lib/mixins/analytics/index.js | 755 +++++++
client/lib/mixins/close-on-esc/README.md | 16 +
client/lib/mixins/close-on-esc/index.js | 65 +
client/lib/mixins/data-observe/Makefile | 7 +
client/lib/mixins/data-observe/README.md | 18 +
client/lib/mixins/data-observe/index.js | 51 +
client/lib/mixins/data-observe/test/test.js | 122 ++
client/lib/mixins/i18n/Makefile | 12 +
client/lib/mixins/i18n/README.md | 271 +++
client/lib/mixins/i18n/index.js | 404 ++++
client/lib/mixins/i18n/number-format.js | 36 +
client/lib/mixins/i18n/test/data.js | 280 +++
.../lib/mixins/i18n/test/i18nLocalStrings.js | 2 +
.../lib/mixins/i18n/test/lib/user-settings.js | 29 +
client/lib/mixins/i18n/test/lib/user.js | 27 +
client/lib/mixins/i18n/test/test.jsx | 199 ++
client/lib/mixins/i18n/timezone.js | 14 +
client/lib/mixins/infinite-scroll/README.md | 38 +
client/lib/mixins/infinite-scroll/index.js | 60 +
client/lib/mixins/lock/README.md | 53 +
client/lib/mixins/lock/index.js | 60 +
.../lib/mixins/observe-window-resize/index.js | 30 +
client/lib/mixins/pageable/Makefile | 12 +
client/lib/mixins/pageable/README.md | 30 +
client/lib/mixins/pageable/index.js | 182 ++
client/lib/mixins/pageable/test/test.js | 138 ++
client/lib/mixins/protect-form/README.md | 40 +
client/lib/mixins/protect-form/index.js | 61 +
client/lib/mixins/render-visualizer/README.md | 27 +
client/lib/mixins/render-visualizer/index.js | 246 +++
client/lib/mixins/searchable/Makefile | 7 +
client/lib/mixins/searchable/README.md | 100 +
client/lib/mixins/searchable/index.js | 82 +
client/lib/mixins/searchable/test/test.js | 122 ++
client/lib/mixins/trap-focus/README.md | 16 +
client/lib/mixins/trap-focus/index.js | 89 +
.../lib/mixins/update-post-status/README.md | 33 +
.../lib/mixins/update-post-status/index.jsx | 179 ++
client/lib/mixins/url-search/Makefile | 7 +
client/lib/mixins/url-search/README.md | 67 +
client/lib/mixins/url-search/build-url.js | 26 +
client/lib/mixins/url-search/index.js | 65 +
.../lib/mixins/url-search/test/build-url.js | 47 +
client/lib/network-connection/Makefile | 12 +
client/lib/network-connection/README.md | 61 +
client/lib/network-connection/index.js | 150 ++
.../lib/network-connection/test/index-test.js | 74 +
.../lib/notification-settings-store/Makefile | 12 +
.../notification-settings-store/actions.js | 61 +
.../notification-settings-store/constants.js | 11 +
.../lib/notification-settings-store/index.js | 81 +
.../test/store-test.js | 249 +++
.../toggle-state.js | 37 +
client/lib/oauth-store/Makefile | 7 +
client/lib/oauth-store/README.md | 19 +
client/lib/oauth-store/actions.js | 26 +
client/lib/oauth-store/constants.js | 12 +
client/lib/oauth-store/index.js | 84 +
client/lib/oauth-store/test/oauth-store.js | 125 ++
client/lib/oauth-token/README.md | 14 +
client/lib/oauth-token/index.js | 33 +
client/lib/olark-api/README.md | 4 +
client/lib/olark-api/index.js | 88 +
client/lib/olark-events/Makefile | 12 +
client/lib/olark-events/README.md | 62 +
client/lib/olark-events/index.js | 102 +
.../lib/olark-events/test/lib/mock-olark.js | 18 +
.../olark-events/test/test-onready-event.js | 33 +
client/lib/olark-store/Makefile | 12 +
client/lib/olark-store/README.md | 85 +
client/lib/olark-store/actions.js | 75 +
client/lib/olark-store/constants.js | 16 +
client/lib/olark-store/index.js | 50 +
.../lib/olark-store/test/olark-store-test.js | 19 +
client/lib/olark/README.md | 4 +
client/lib/olark/index.js | 370 ++++
client/lib/page-templates/actions.js | 35 +
client/lib/page-templates/store.js | 55 +
client/lib/paths/Makefile | 7 +
client/lib/paths/index.js | 93 +
client/lib/paths/test/index.js | 129 ++
client/lib/paygate-loader/README.md | 28 +
client/lib/paygate-loader/index.js | 47 +
client/lib/people/Makefile | 12 +
client/lib/people/README.md | 9 +
client/lib/people/actions.js | 17 +
client/lib/people/log-store.js | 145 ++
client/lib/people/test/lib/mock-actions.js | 47 +
client/lib/people/test/lib/mock-site.js | 31 +
client/lib/people/test/test-store.js | 108 +
client/lib/phone-validation/Makefile | 10 +
client/lib/phone-validation/README.md | 5 +
client/lib/phone-validation/index.jsx | 54 +
client/lib/phone-validation/test/test.jsx | 48 +
client/lib/plans-list/Makefile | 12 +
client/lib/plans-list/README.md | 16 +
client/lib/plans-list/index.js | 160 ++
client/lib/plans-list/test/data.js | 110 +
client/lib/plans-list/test/lib/wp.js | 4 +
client/lib/plans-list/test/test.js | 31 +
client/lib/plugins/Makefile | 12 +
client/lib/plugins/README.md | 218 ++
client/lib/plugins/actions.js | 461 ++++
client/lib/plugins/log-store.js | 186 ++
client/lib/plugins/notices.jsx | 674 ++++++
client/lib/plugins/store.js | 455 ++++
client/lib/plugins/test/lib/mixins/i18n.js | 12 +
client/lib/plugins/test/lib/mock-actions.js | 329 +++
.../lib/plugins/test/lib/mock-multi-site.js | 68 +
.../plugins/test/lib/mock-plugins-updated.js | 36 +
client/lib/plugins/test/lib/mock-plugins.js | 62 +
client/lib/plugins/test/lib/mock-site.js | 36 +
.../lib/plugins/test/lib/mock-sites-list.js | 19 +
.../plugins/test/lib/mock-updated-plugin.js | 13 +
client/lib/plugins/test/lib/mock-wpcom.js | 83 +
client/lib/plugins/test/lib/wp.js | 6 +
client/lib/plugins/test/test-actions.js | 90 +
client/lib/plugins/test/test-log-store.js | 22 +
client/lib/plugins/test/test-store.js | 512 +++++
client/lib/plugins/test/test-utils.js | 208 ++
client/lib/plugins/utils.js | 269 +++
client/lib/plugins/wporg-data/Makefile | 11 +
client/lib/plugins/wporg-data/README.md | 133 ++
client/lib/plugins/wporg-data/actions.js | 129 ++
client/lib/plugins/wporg-data/list-store.js | 155 ++
client/lib/plugins/wporg-data/store.js | 99 +
.../wporg-data/test/lib/data-actions.js | 117 +
.../wporg-data/test/lib/mock-actions.js | 18 +
.../wporg-data/test/lib/mock-local-store.js | 25 +
.../plugins/wporg-data/test/lib/mock-store.js | 4 +
.../plugins/wporg-data/test/lib/mock-wporg.js | 29 +
.../wporg-data/test/lib/mocked-actions.js | 9 +
.../plugins/wporg-data/test/test-actions.js | 77 +
.../wporg-data/test/test-list-store.js | 279 +++
.../lib/plugins/wporg-data/test/test-store.js | 69 +
client/lib/popup-monitor/Makefile | 12 +
client/lib/popup-monitor/README.md | 26 +
client/lib/popup-monitor/index.js | 120 ++
client/lib/popup-monitor/test/index.js | 27 +
client/lib/post-formats/Makefile | 12 +
client/lib/post-formats/README.md | 38 +
client/lib/post-formats/actions.js | 34 +
client/lib/post-formats/store.js | 41 +
client/lib/post-formats/test/actions.js | 83 +
client/lib/post-formats/test/store.js | 70 +
client/lib/post-metadata/Makefile | 12 +
client/lib/post-metadata/README.md | 13 +
client/lib/post-metadata/index.js | 121 ++
client/lib/post-metadata/test/index.js | 218 ++
client/lib/post-normalizer/Makefile | 17 +
client/lib/post-normalizer/README.md | 5 +
client/lib/post-normalizer/index.js | 682 ++++++
.../test/lib/safe-image-url.js | 6 +
.../test/post-normalizer-test.js | 774 +++++++
client/lib/post-stats/README.md | 29 +
client/lib/post-stats/actions.js | 65 +
client/lib/post-stats/constants.js | 14 +
client/lib/post-stats/store.js | 123 ++
client/lib/post-types-list/README.md | 39 +
client/lib/post-types-list/index.js | 10 +
client/lib/post-types-list/list.js | 92 +
client/lib/posts/Makefile | 10 +
client/lib/posts/README.md | 42 +
client/lib/posts/actions.js | 537 +++++
client/lib/posts/post-content-images-store.js | 67 +
client/lib/posts/post-counts-store.js | 232 ++
client/lib/posts/post-edit-store.js | 500 +++++
client/lib/posts/post-list-cache-store.js | 130 ++
client/lib/posts/post-list-store.js | 371 ++++
client/lib/posts/posts-store.js | 67 +
client/lib/posts/stats.js | 116 +
client/lib/posts/test/actions.js | 203 ++
client/lib/posts/test/post-edit-store.js | 1004 +++++++++
client/lib/posts/test/utils.js | 110 +
client/lib/posts/utils.js | 234 ++
client/lib/preferences/Makefile | 12 +
client/lib/preferences/README.md | 37 +
client/lib/preferences/actions.js | 97 +
client/lib/preferences/constants.js | 4 +
client/lib/preferences/store.js | 80 +
client/lib/preferences/test/actions.js | 199 ++
client/lib/preferences/test/store.js | 93 +
client/lib/products-list/README.md | 18 +
client/lib/products-list/index.js | 134 ++
client/lib/products-values/index.js | 272 +++
client/lib/products-values/schema.json | 14 +
client/lib/products-values/sort.js | 73 +
client/lib/purchases/Makefile | 12 +
client/lib/purchases/assembler.js | 86 +
client/lib/purchases/index.js | 175 ++
client/lib/purchases/store.js | 87 +
client/lib/purchases/stored-cards/Makefile | 12 +
.../lib/purchases/stored-cards/assembler.js | 19 +
client/lib/purchases/stored-cards/reducer.js | 88 +
client/lib/purchases/stored-cards/store.js | 7 +
.../stored-cards/test/assembler-test.js | 28 +
.../purchases/stored-cards/test/constants.js | 48 +
.../purchases/stored-cards/test/store-test.js | 132 ++
client/lib/purchases/test/assembler-test.js | 43 +
client/lib/purchases/test/store-test.js | 35 +
client/lib/react-pass-to-children/Makefile | 7 +
client/lib/react-pass-to-children/README.md | 21 +
client/lib/react-pass-to-children/index.js | 26 +
.../lib/react-pass-to-children/test/index.jsx | 107 +
client/lib/react-smart-set-state/index.js | 12 +
client/lib/react-test-env-setup/Makefile | 7 +
client/lib/react-test-env-setup/README.md | 27 +
client/lib/react-test-env-setup/index.js | 30 +
client/lib/react-test-env-setup/test/index.js | 59 +
.../Makefile | 15 +
.../actions.js | 70 +
.../constants.js | 19 +
.../index.js | 266 +++
.../comment-email-subscription-store-test.js | 124 ++
client/lib/reader-feed-subscriptions/Makefile | 15 +
.../lib/reader-feed-subscriptions/actions.js | 205 ++
.../reader-feed-subscriptions/constants.js | 22 +
.../lib/reader-feed-subscriptions/helper.js | 10 +
client/lib/reader-feed-subscriptions/index.js | 382 ++++
.../test/feed-subscription-store-test.js | 287 +++
client/lib/reader-lists/README.md | 0
client/lib/reader-lists/actions.js | 138 ++
client/lib/reader-lists/lists.js | 83 +
client/lib/reader-lists/subscriptions.js | 126 ++
.../reader-post-email-subscriptions/Makefile | 15 +
.../actions.js | 97 +
.../constants.js | 22 +
.../reader-post-email-subscriptions/index.js | 316 +++
.../post-email-subscription-store-test.js | 216 ++
client/lib/reader-sidebar/actions.js | 63 +
client/lib/reader-site-blocks/Makefile | 15 +
client/lib/reader-site-blocks/actions.js | 60 +
client/lib/reader-site-blocks/constants.js | 15 +
client/lib/reader-site-blocks/index.js | 155 ++
.../test/site-block-store-test.js | 53 +
client/lib/reader-site-store/Makefile | 15 +
client/lib/reader-site-store/actions.js | 32 +
client/lib/reader-site-store/constants.js | 11 +
client/lib/reader-site-store/index.js | 98 +
.../test/reader-site-store-tests.js | 182 ++
client/lib/reader-tags/README.md | 0
client/lib/reader-tags/actions.js | 106 +
client/lib/reader-tags/subscriptions.js | 128 ++
client/lib/reader-tags/tags.js | 67 +
client/lib/reader-teams/Makefile | 15 +
client/lib/reader-teams/actions.js | 34 +
client/lib/reader-teams/constants.js | 10 +
client/lib/reader-teams/index.js | 85 +
.../lib/reader-teams/test/team-store-test.js | 70 +
client/lib/recommended-sites-store/Makefile | 15 +
client/lib/recommended-sites-store/actions.js | 57 +
.../lib/recommended-sites-store/constants.js | 2 +
client/lib/recommended-sites-store/store.js | 55 +
.../test/action-tests.js | 168 ++
.../test/store-tests.js | 28 +
client/lib/resize-image-url/Makefile | 8 +
client/lib/resize-image-url/README.md | 12 +
client/lib/resize-image-url/index.js | 24 +
client/lib/resize-image-url/test/test.js | 24 +
client/lib/route/Makefile | 10 +
client/lib/route/README.md | 25 +
client/lib/route/index.js | 16 +
client/lib/route/normalize.js | 15 +
client/lib/route/path.js | 137 ++
client/lib/route/redirect.js | 14 +
client/lib/route/test/index.js | 215 ++
client/lib/route/trailingslashit.js | 6 +
client/lib/route/untrailingslashit.js | 8 +
client/lib/safe-image-url/Makefile | 11 +
client/lib/safe-image-url/README.md | 7 +
client/lib/safe-image-url/index.js | 51 +
client/lib/safe-image-url/test/index.js | 68 +
client/lib/safe-protocol-url/Makefile | 8 +
client/lib/safe-protocol-url/README.md | 3 +
client/lib/safe-protocol-url/index.js | 39 +
client/lib/safe-protocol-url/test/test.js | 49 +
client/lib/scroll-to/Makefile | 9 +
client/lib/scroll-to/README.md | 17 +
client/lib/scroll-to/index.js | 54 +
client/lib/scroll-to/test/test.js | 45 +
client/lib/security-checkup/Makefile | 10 +
.../account-recovery-store.js | 245 +++
client/lib/security-checkup/actions.js | 114 +
client/lib/security-checkup/constants.js | 44 +
.../test/account-recovery-store.js | 142 ++
client/lib/security-checkup/test/actions.js | 203 ++
client/lib/security-checkup/test/constants.js | 79 +
client/lib/services-list/README.md | 41 +
client/lib/services-list/index.js | 10 +
client/lib/services-list/list.js | 84 +
client/lib/sharing-buttons-list/README.md | 39 +
client/lib/sharing-buttons-list/index.js | 175 ++
client/lib/shortcode/Makefile | 7 +
client/lib/shortcode/README.md | 47 +
client/lib/shortcode/index.js | 239 +++
client/lib/shortcode/test/index.js | 261 +++
client/lib/shortcodes/README.md | 28 +
client/lib/shortcodes/actions.js | 36 +
client/lib/shortcodes/constants.js | 10 +
client/lib/shortcodes/store.js | 100 +
client/lib/siftscience/README.md | 6 +
client/lib/siftscience/index.js | 39 +
client/lib/signup/Makefile | 12 +
client/lib/signup/README.md | 81 +
client/lib/signup/actions.js | 67 +
client/lib/signup/cart.js | 39 +
client/lib/signup/dependency-store.js | 108 +
client/lib/signup/flow-controller.js | 194 ++
client/lib/signup/progress-store.js | 163 ++
client/lib/signup/step-actions.js | 150 ++
client/lib/signup/test/analytics.js | 7 +
.../lib/signup/test/dependency-store-test.js | 39 +
.../lib/signup/test/flow-controller-test.js | 148 ++
client/lib/signup/test/lib/user/index.js | 11 +
client/lib/signup/test/lib/wp/index.js | 13 +
client/lib/signup/test/progress-store-test.js | 108 +
client/lib/signup/test/signup/config/flows.js | 35 +
client/lib/signup/test/signup/config/steps.js | 64 +
client/lib/site-roles/Makefile | 15 +
client/lib/site-roles/README.md | 4 +
client/lib/site-roles/actions.js | 33 +
client/lib/site-roles/store.js | 68 +
.../lib/site-roles/test/lib/mock-actions.js | 11 +
client/lib/site-roles/test/lib/mock-roles.js | 128 ++
client/lib/site-roles/test/lib/mock-site.js | 31 +
client/lib/site-roles/test/test-store.js | 52 +
.../README.md | 13 +
.../site-specific-plans-details-list/index.js | 147 ++
client/lib/site-stats-sticky-tab/README.md | 25 +
client/lib/site-stats-sticky-tab/actions.js | 22 +
client/lib/site-stats-sticky-tab/constants.js | 8 +
client/lib/site-stats-sticky-tab/store.js | 133 ++
client/lib/site/Makefile | 12 +
client/lib/site/README.md | 12 +
client/lib/site/index.js | 246 +++
client/lib/site/jetpack.js | 409 ++++
client/lib/site/test/lib/wp.js | 4 +
client/lib/site/test/test.js | 101 +
client/lib/site/utils.js | 62 +
client/lib/sites-list/Makefile | 12 +
client/lib/sites-list/README.md | 45 +
client/lib/sites-list/actions.js | 73 +
client/lib/sites-list/delete-site-store.js | 51 +
client/lib/sites-list/docs/example.jsx | 27 +
client/lib/sites-list/index.js | 29 +
client/lib/sites-list/list.js | 580 +++++
client/lib/sites-list/log-store.js | 137 ++
client/lib/sites-list/notices.js | 141 ++
client/lib/sites-list/test/data.js | 322 +++
client/lib/sites-list/test/lib/mixins/i18n.js | 5 +
.../lib/sites-list/test/lib/mock-actions.js | 33 +
client/lib/sites-list/test/lib/mock-site.js | 30 +
client/lib/sites-list/test/lib/user.js | 11 +
client/lib/sites-list/test/lib/wp.js | 4 +
client/lib/sites-list/test/test-log-store.js | 26 +
client/lib/sites-list/test/test.js | 104 +
client/lib/states-list/README.md | 28 +
client/lib/states-list/index.js | 165 ++
client/lib/stats/README.md | 8 +
client/lib/stats/stats-list/Makefile | 12 +
client/lib/stats/stats-list/README.md | 48 +
client/lib/stats/stats-list/index.js | 239 +++
client/lib/stats/stats-list/stats-parser.js | 844 ++++++++
client/lib/stats/stats-list/test/analytics.js | 6 +
client/lib/stats/stats-list/test/data.js | 95 +
.../stats/stats-list/test/lib/mixins/i18n.js | 6 +
client/lib/stats/stats-list/test/lib/user.js | 11 +
client/lib/stats/stats-list/test/lib/wp.js | 72 +
.../stats/stats-list/test/test-stats-list.js | 142 ++
.../stats-list/test/test-stats-parser.js | 65 +
client/lib/stats/summary-list/README.md | 18 +
client/lib/stats/summary-list/index.js | 97 +
client/lib/stats/summary/README.md | 28 +
client/lib/stats/summary/index.js | 171 ++
client/lib/store-transactions/README.md | 65 +
client/lib/store-transactions/index.js | 242 +++
client/lib/store/Makefile | 13 +
client/lib/store/README.md | 22 +
client/lib/store/index.js | 85 +
client/lib/store/test/index-test.js | 127 ++
client/lib/stored-cards/README.md | 16 +
client/lib/stored-cards/index.js | 113 +
client/lib/tags-list/README.md | 25 +
client/lib/tags-list/index.js | 76 +
client/lib/terms/Makefile | 15 +
client/lib/terms/README.md | 41 +
client/lib/terms/actions.js | 162 ++
client/lib/terms/category-store-factory.js | 25 +
client/lib/terms/category-store.js | 279 +++
client/lib/terms/constants.js | 15 +
client/lib/terms/store.js | 72 +
client/lib/terms/tag-store.js | 155 ++
client/lib/terms/test/actions.js | 194 ++
client/lib/terms/test/category-store.js | 313 +++
client/lib/terms/test/common.js | 119 ++
client/lib/terms/test/data.js | 70 +
client/lib/terms/test/store.js | 109 +
client/lib/terms/test/tag-store.js | 142 ++
client/lib/ticker/index.js | 74 +
client/lib/touch-detect/README.md | 12 +
client/lib/touch-detect/index.js | 20 +
client/lib/track-scroll-page/index.js | 10 +
client/lib/transaction/store.js | 112 +
client/lib/translator-jumpstart/README.md | 19 +
client/lib/translator-jumpstart/index.js | 337 +++
client/lib/tree-convert/Makefile | 7 +
client/lib/tree-convert/README.md | 26 +
client/lib/tree-convert/index.js | 93 +
client/lib/tree-convert/test/fixtures.js | 60 +
.../tree-convert/test/test-tree-convert.js | 70 +
client/lib/tree-convert/tree-traverser.js | 176 ++
client/lib/trophies-data/README.md | 4 +
client/lib/trophies-data/index.js | 66 +
client/lib/two-step-authorization/README.md | 8 +
client/lib/two-step-authorization/index.js | 214 ++
client/lib/upgrades/actions/cart.js | 92 +
client/lib/upgrades/actions/checkout.js | 59 +
.../lib/upgrades/actions/domain-management.js | 527 +++++
client/lib/upgrades/actions/domain-search.js | 31 +
client/lib/upgrades/actions/index.js | 5 +
client/lib/upgrades/actions/purchases.js | 150 ++
client/lib/upgrades/constants.js | 74 +
client/lib/user-profile-links/README.md | 4 +
client/lib/user-profile-links/index.js | 134 ++
client/lib/user-settings/Makefile | 12 +
client/lib/user-settings/README.md | 4 +
client/lib/user-settings/index.js | 221 ++
client/lib/user-settings/test/index.js | 31 +
client/lib/user-settings/test/lib/user.js | 5 +
client/lib/user-settings/test/lib/wp.js | 21 +
client/lib/user/Makefile | 12 +
client/lib/user/README.md | 34 +
client/lib/user/index.js | 38 +
client/lib/user/shared-utils.js | 1 +
client/lib/user/test/utils.js | 96 +
client/lib/user/user.js | 244 +++
client/lib/user/utils.js | 49 +
client/lib/username/README.md | 4 +
client/lib/username/index.js | 96 +
client/lib/users/Makefile | 13 +
client/lib/users/README.md | 89 +
client/lib/users/actions.js | 140 ++
client/lib/users/store.js | 212 ++
client/lib/users/test/lib/mock-actions.js | 84 +
.../users/test/lib/mock-deleted-user-data.js | 52 +
.../users/test/lib/mock-more-users-data.js | 28 +
client/lib/users/test/lib/mock-single-user.js | 11 +
client/lib/users/test/lib/mock-site.js | 31 +
.../test/lib/mock-updated-single-user.js | 11 +
client/lib/users/test/lib/mock-users-data.js | 64 +
client/lib/users/test/test-store.js | 198 ++
client/lib/version-compare/README.md | 4 +
client/lib/version-compare/index.js | 121 ++
client/lib/viewers/Makefile | 12 +
client/lib/viewers/README.md | 4 +
client/lib/viewers/actions.js | 61 +
client/lib/viewers/store.js | 135 ++
client/lib/viewers/test/lib/mock-actions.js | 41 +
client/lib/viewers/test/lib/mock-site.js | 31 +
client/lib/viewers/test/lib/mock-viewers-1.js | 25 +
client/lib/viewers/test/lib/mock-viewers-2.js | 25 +
client/lib/viewers/test/test-store.js | 125 ++
client/lib/viewport/README.md | 16 +
client/lib/viewport/index.js | 65 +
client/lib/warn/index.js | 14 +
client/lib/wpcom-xhr-wrapper/Makefile | 7 +
client/lib/wpcom-xhr-wrapper/README.md | 39 +
client/lib/wpcom-xhr-wrapper/index.js | 15 +
.../test/wpcom-xhr-wrapper.js | 94 +
client/lib/wporg/index.js | 77 +
client/lib/wporg/jsonp.js | 90 +
client/mailing-lists/controller.js | 24 +
client/mailing-lists/index.js | 19 +
client/mailing-lists/main.jsx | 193 ++
client/mailing-lists/utils.js | 39 +
client/me/account-password/README.md | 5 +
client/me/account-password/index.jsx | 172 ++
client/me/account-password/style.scss | 20 +
client/me/account/README.md | 4 +
client/me/account/index.jsx | 551 +++++
client/me/account/style.scss | 25 +
client/me/action-remove/README.md | 4 +
client/me/action-remove/index.jsx | 34 +
client/me/action-remove/style.scss | 20 +
client/me/application-password-item/README.md | 4 +
client/me/application-password-item/index.jsx | 66 +
.../me/application-password-item/style.scss | 16 +
client/me/application-passwords/README.md | 4 +
client/me/application-passwords/index.jsx | 207 ++
client/me/application-passwords/style.scss | 41 +
client/me/billing-history/README.md | 11 +
.../billing-history/billing-history-table.jsx | 77 +
client/me/billing-history/index.jsx | 82 +
client/me/billing-history/table-rows.js | 72 +
.../billing-history/transactions-header.jsx | 254 +++
.../me/billing-history/transactions-table.jsx | 159 ++
.../upcoming-charges-table.jsx | 32 +
.../me/billing-history/view-receipt-modal.jsx | 208 ++
.../me/connected-application-icon/README.md | 4 +
.../me/connected-application-icon/index.jsx | 30 +
.../me/connected-application-icon/style.scss | 16 +
.../me/connected-application-item/README.md | 4 +
.../me/connected-application-item/index.jsx | 235 ++
.../me/connected-application-item/style.scss | 160 ++
client/me/connected-applications/README.md | 4 +
client/me/connected-applications/index.jsx | 139 ++
client/me/connected-applications/style.scss | 9 +
client/me/controller.js | 338 +++
client/me/credit-cards/credit-card-delete.jsx | 71 +
.../me/credit-cards/credit-card-delete.scss | 13 +
client/me/credit-cards/credit-cards.scss | 13 +
client/me/credit-cards/index.jsx | 65 +
client/me/event-recorder/index.js | 65 +
client/me/form-base/index.js | 90 +
client/me/help/README.md | 3 +
client/me/help/controller.js | 32 +
.../help/help-contact-confirmation/index.jsx | 32 +
.../help/help-contact-confirmation/style.scss | 62 +
client/me/help/help-contact-form/index.jsx | 172 ++
client/me/help/help-contact-form/style.scss | 54 +
client/me/help/help-contact/index.jsx | 247 +++
client/me/help/help-contact/style.scss | 5 +
.../help/help-happiness-engineers/index.jsx | 52 +
.../help/help-happiness-engineers/style.scss | 36 +
client/me/help/help-results/index.jsx | 40 +
client/me/help/help-results/item.jsx | 37 +
client/me/help/help-results/style.scss | 61 +
client/me/help/help-search/index.jsx | 113 +
client/me/help/help-search/style.scss | 37 +
client/me/help/index.js | 8 +
client/me/help/main.jsx | 57 +
client/me/help/style.scss | 29 +
client/me/index.js | 119 ++
client/me/next-steps/index.jsx | 193 ++
client/me/next-steps/next-steps-box.jsx | 49 +
client/me/next-steps/next-steps-box.scss | 71 +
client/me/next-steps/next-steps.scss | 31 +
client/me/next-steps/steps.js | 134 ++
.../blogs-settings/blog.jsx | 58 +
.../blogs-settings/header.jsx | 89 +
.../blogs-settings/index.jsx | 74 +
.../blogs-settings/placeholder.jsx | 40 +
.../blogs-settings/style.scss | 128 ++
.../comment-settings/index.jsx | 89 +
.../comment-settings/style.scss | 6 +
client/me/notification-settings/index.jsx | 76 +
.../me/notification-settings/navigation.jsx | 56 +
.../reader-subscriptions/index.jsx | 213 ++
.../settings-form/actions.jsx | 36 +
.../settings-form/constants.js | 3 +
.../settings-form/device-selector.jsx | 42 +
.../settings-form/index.jsx | 50 +
.../settings-form/labels-list.jsx | 31 +
.../settings-form/labels.jsx | 31 +
.../settings-form/locales.js | 25 +
.../settings-form/settings.jsx | 104 +
.../settings-form/stream-header.jsx | 34 +
.../settings-form/stream-options.jsx | 47 +
.../settings-form/stream-selector.jsx | 43 +
.../settings-form/stream.jsx | 78 +
.../settings-form/style.scss | 142 ++
.../wpcom-settings/index.jsx | 130 ++
.../wpcom-settings/style.scss | 6 +
client/me/paths.js | 8 +
client/me/profile-gravatar/README.md | 4 +
client/me/profile-gravatar/index.jsx | 51 +
client/me/profile-gravatar/style.scss | 118 +
client/me/profile-link/README.md | 4 +
client/me/profile-link/index.jsx | 73 +
client/me/profile-link/style.scss | 65 +
client/me/profile-links-add-other/README.md | 4 +
client/me/profile-links-add-other/index.jsx | 165 ++
client/me/profile-links-add-other/style.scss | 15 +
.../me/profile-links-add-wordpress/README.md | 4 +
.../me/profile-links-add-wordpress/index.jsx | 227 ++
.../me/profile-links-add-wordpress/style.scss | 26 +
client/me/profile-links/README.md | 4 +
client/me/profile-links/index.jsx | 207 ++
client/me/profile-links/style.scss | 20 +
client/me/profile/README.me | 4 +
client/me/profile/index.jsx | 137 ++
.../cancel-private-registration/index.jsx | 154 ++
.../cancel-private-registration/style.scss | 26 +
.../me/purchases/cancel-purchase/button.jsx | 42 +
client/me/purchases/cancel-purchase/index.jsx | 116 +
.../cancel-purchase/product-information.jsx | 180 ++
.../cancel-purchase/refund-information.jsx | 53 +
.../me/purchases/cancel-purchase/style.scss | 102 +
.../purchases/cancel-purchase/support-box.jsx | 39 +
.../confirm-cancel-purchase/index.jsx | 85 +
.../load-endpoint-form.js | 145 ++
.../confirm-cancel-purchase/style.scss | 56 +
client/me/purchases/controller.js | 209 ++
client/me/purchases/list/header/index.jsx | 42 +
client/me/purchases/list/header/style.scss | 0
client/me/purchases/list/index.jsx | 66 +
client/me/purchases/list/item/index.jsx | 147 ++
client/me/purchases/list/item/style.scss | 82 +
client/me/purchases/list/site/index.jsx | 52 +
client/me/purchases/list/site/style.scss | 21 +
client/me/purchases/manage-purchase/index.jsx | 577 +++++
.../me/purchases/manage-purchase/style.scss | 220 ++
client/me/purchases/paths.js | 42 +
.../payment/edit-card-details/index.jsx | 222 ++
.../payment/edit-card-details/style.scss | 30 +
.../edit-payment-method/credit-card.jsx | 141 ++
.../payment/edit-payment-method/index.jsx | 57 +
.../payment/edit-payment-method/paypal.jsx | 107 +
.../payment/edit-payment-method/style.scss | 57 +
client/me/purchases/purchases-mixin.js | 47 +
client/me/reauth-required/index.jsx | 214 ++
client/me/reauth-required/style.scss | 11 +
.../security-2fa-app-chooser-item/index.jsx | 127 ++
.../security-2fa-app-chooser-item/style.scss | 4 +
.../security-2fa-backup-codes-list/index.jsx | 247 +++
.../security-2fa-backup-codes-list/style.scss | 121 ++
.../index.jsx | 157 ++
.../style.scss | 8 +
client/me/security-2fa-backup-codes/README.md | 15 +
client/me/security-2fa-backup-codes/index.jsx | 124 ++
.../me/security-2fa-backup-codes/style.scss | 24 +
client/me/security-2fa-code-prompt/README.md | 5 +
client/me/security-2fa-code-prompt/index.jsx | 275 +++
client/me/security-2fa-code-prompt/style.scss | 20 +
client/me/security-2fa-disable/README.md | 4 +
client/me/security-2fa-disable/index.jsx | 162 ++
client/me/security-2fa-disable/style.scss | 4 +
client/me/security-2fa-enable/index.jsx | 425 ++++
client/me/security-2fa-enable/style.scss | 49 +
.../me/security-2fa-initial-setup/README.md | 5 +
.../me/security-2fa-initial-setup/index.jsx | 49 +
client/me/security-2fa-progress/README.md | 5 +
client/me/security-2fa-progress/index.jsx | 60 +
.../security-2fa-progress/progress-item.jsx | 43 +
client/me/security-2fa-progress/style.scss | 98 +
.../security-2fa-setup-backup-codes/README.md | 14 +
.../security-2fa-setup-backup-codes/index.jsx | 104 +
client/me/security-2fa-setup/README.md | 4 +
client/me/security-2fa-setup/index.jsx | 120 ++
client/me/security-2fa-sms-settings/README.md | 5 +
client/me/security-2fa-sms-settings/index.jsx | 221 ++
.../me/security-2fa-sms-settings/style.scss | 41 +
client/me/security-2fa-status/README.md | 5 +
client/me/security-2fa-status/index.jsx | 46 +
client/me/security-2fa-status/style.scss | 15 +
client/me/security-checkup/buttons.jsx | 54 +
client/me/security-checkup/edit-email.jsx | 154 ++
client/me/security-checkup/edit-phone.jsx | 128 ++
client/me/security-checkup/index.jsx | 54 +
client/me/security-checkup/manage-contact.jsx | 173 ++
client/me/security-checkup/recovery-email.jsx | 83 +
client/me/security-checkup/recovery-phone.jsx | 95 +
client/me/security-checkup/style.scss | 114 +
client/me/security-section-nav/README.me | 8 +
client/me/security-section-nav/index.jsx | 81 +
client/me/security/password.jsx | 54 +
client/me/select-site.jsx | 37 +
client/me/sidebar-navigation/README.md | 20 +
client/me/sidebar-navigation/package.json | 6 +
.../sidebar-navigation/sidebar-navigation.jsx | 28 +
client/me/sidebar-navigation/style.scss | 11 +
client/me/sidebar/index.jsx | 151 ++
client/me/sidebar/sidebar-item.jsx | 38 +
client/me/sidebar/style.scss | 41 +
client/me/two-step/index.jsx | 166 ++
client/me/two-step/style.scss | 10 +
client/my-sites/README.md | 27 +
client/my-sites/ads/controller.js | 72 +
client/my-sites/ads/form-earnings.jsx | 330 +++
client/my-sites/ads/form-settings.jsx | 397 ++++
client/my-sites/ads/index.js | 19 +
client/my-sites/ads/main.jsx | 95 +
client/my-sites/ads/style.scss | 123 ++
client/my-sites/all-sites-icon/README.md | 22 +
client/my-sites/all-sites-icon/index.jsx | 50 +
client/my-sites/all-sites-icon/package.json | 9 +
client/my-sites/all-sites-icon/style.scss | 105 +
client/my-sites/all-sites/README.md | 23 +
client/my-sites/all-sites/index.jsx | 67 +
client/my-sites/all-sites/style.scss | 14 +
client/my-sites/category-selector/README.md | 40 +
.../category-selector/add-category.jsx | 207 ++
client/my-sites/category-selector/index.jsx | 231 ++
.../my-sites/category-selector/no-results.jsx | 39 +
client/my-sites/category-selector/search.jsx | 25 +
client/my-sites/category-selector/search.scss | 20 +
client/my-sites/category-selector/style.scss | 110 +
client/my-sites/controller.js | 281 +++
client/my-sites/current-site/README.md | 21 +
client/my-sites/current-site/index.jsx | 185 ++
client/my-sites/current-site/style.scss | 144 ++
client/my-sites/customize/README.md | 15 +
client/my-sites/customize/actions.js | 70 +
client/my-sites/customize/controller.js | 38 +
client/my-sites/customize/index.js | 18 +
client/my-sites/customize/loading-panel.jsx | 59 +
client/my-sites/customize/main.jsx | 371 ++++
client/my-sites/customize/package.json | 6 +
client/my-sites/customize/style.scss | 192 ++
client/my-sites/draft/README.md | 4 +
client/my-sites/draft/index.jsx | 284 +++
client/my-sites/draft/style.scss | 287 +++
client/my-sites/drafts/controller.js | 32 +
client/my-sites/drafts/draft-list.jsx | 148 ++
client/my-sites/drafts/index.js | 19 +
client/my-sites/drafts/main.jsx | 43 +
client/my-sites/drafts/style.scss | 15 +
client/my-sites/exporter/advanced-options.jsx | 96 +
client/my-sites/exporter/option-fieldset.jsx | 70 +
client/my-sites/exporter/style.scss | 61 +
client/my-sites/importer/Makefile | 12 +
.../my-sites/importer/author-mapping-item.jsx | 58 +
.../my-sites/importer/author-mapping-pane.jsx | 59 +
client/my-sites/importer/error-pane.jsx | 90 +
client/my-sites/importer/file-importer.jsx | 87 +
client/my-sites/importer/importer-ghost.jsx | 29 +
client/my-sites/importer/importer-header.jsx | 99 +
client/my-sites/importer/importer-icons.jsx | 30 +
client/my-sites/importer/importer-medium.jsx | 29 +
.../importer/importer-squarespace.jsx | 32 +
.../my-sites/importer/importer-wordpress.jsx | 60 +
client/my-sites/importer/importing-pane.jsx | 149 ++
client/my-sites/importer/style.scss | 195 ++
client/my-sites/importer/test/mock-data.js | 388 ++++
client/my-sites/importer/uploading-pane.jsx | 132 ++
client/my-sites/index.js | 16 +
.../jetpack-manage-error-page/README.md | 56 +
.../jetpack-manage-error-page/index.jsx | 74 +
.../jetpack-manage-error-page/package.json | 6 +
client/my-sites/media-library/Makefile | 10 +
client/my-sites/media-library/content.jsx | 199 ++
client/my-sites/media-library/content.scss | 28 +
client/my-sites/media-library/drop-zone.jsx | 69 +
client/my-sites/media-library/filter-bar.jsx | 131 ++
.../media-library/filter-to-mime-prefix.js | 33 +
client/my-sites/media-library/header.jsx | 226 ++
client/my-sites/media-library/index.jsx | 147 ++
.../media-library/list-item-audio.jsx | 17 +
.../media-library/list-item-document.jsx | 17 +
.../media-library/list-item-file-details.jsx | 42 +
.../media-library/list-item-file-details.scss | 49 +
.../media-library/list-item-image.jsx | 102 +
.../media-library/list-item-video.jsx | 55 +
.../media-library/list-item-video.scss | 30 +
client/my-sites/media-library/list-item.jsx | 139 ++
client/my-sites/media-library/list-item.scss | 114 +
.../media-library/list-no-content.jsx | 59 +
.../media-library/list-no-results.jsx | 81 +
client/my-sites/media-library/list.jsx | 227 ++
client/my-sites/media-library/style.scss | 117 +
.../my-sites/media-library/test/fixtures.js | 65 +
client/my-sites/media-library/test/list.jsx | 186 ++
.../my-sites/media-library/upload-button.jsx | 73 +
.../my-sites/media-library/upload-button.scss | 20 +
client/my-sites/media-library/upload-url.jsx | 104 +
client/my-sites/media-library/upload-url.scss | 26 +
client/my-sites/media/controller.js | 46 +
client/my-sites/media/index.js | 20 +
client/my-sites/media/main.jsx | 59 +
client/my-sites/menus/README.md | 44 +
client/my-sites/menus/controller.js | 79 +
client/my-sites/menus/index.js | 18 +
client/my-sites/menus/item-options/Makefile | 10 +
client/my-sites/menus/item-options/README.md | 22 +
.../menus/item-options/category-options.jsx | 87 +
.../menus/item-options/empty-placeholder.jsx | 85 +
.../item-options/loading-placeholder.jsx | 20 +
.../menus/item-options/option-list.jsx | 107 +
.../my-sites/menus/item-options/options.jsx | 60 +
.../my-sites/menus/item-options/post-list.jsx | 45 +
client/my-sites/menus/item-options/posts.jsx | 80 +
.../menus/item-options/taxonomy-list.jsx | 76 +
.../menus/item-options/test/test-posts.jsx | 59 +
client/my-sites/menus/loading-placeholder.jsx | 37 +
client/my-sites/menus/location-picker.jsx | 56 +
client/my-sites/menus/main.jsx | 268 +++
client/my-sites/menus/menu-delete-button.jsx | 79 +
client/my-sites/menus/menu-editable-item.jsx | 386 ++++
.../my-sites/menus/menu-item-drop-target.jsx | 48 +
client/my-sites/menus/menu-item-list.jsx | 444 ++++
client/my-sites/menus/menu-item-types.js | 199 ++
client/my-sites/menus/menu-name.jsx | 115 +
.../my-sites/menus/menu-panel-back-button.jsx | 39 +
client/my-sites/menus/menu-picker.jsx | 100 +
client/my-sites/menus/menu-placeholder.jsx | 36 +
client/my-sites/menus/menu-utils.js | 21 +
client/my-sites/menus/menu.jsx | 250 +++
client/my-sites/menus/menus-save-button.jsx | 61 +
client/my-sites/navigation/navigation.jsx | 69 +
client/my-sites/navigation/package.json | 6 +
client/my-sites/no-results/index.jsx | 25 +
client/my-sites/no-results/style.scss | 15 +
client/my-sites/pages/README.md | 4 +
client/my-sites/pages/controller.js | 65 +
client/my-sites/pages/helpers.js | 25 +
client/my-sites/pages/index.js | 17 +
client/my-sites/pages/main.jsx | 113 +
client/my-sites/pages/page-list.jsx | 230 ++
client/my-sites/pages/page.jsx | 335 +++
client/my-sites/pages/placeholder.jsx | 38 +
client/my-sites/pages/style.scss | 110 +
client/my-sites/people/README.md | 25 +
client/my-sites/people/controller.js | 115 +
client/my-sites/people/delete-user/README.md | 10 +
client/my-sites/people/delete-user/index.jsx | 279 +++
client/my-sites/people/delete-user/style.scss | 21 +
.../people/edit-team-member-form/README.md | 5 +
.../people/edit-team-member-form/index.jsx | 349 +++
.../people/edit-team-member-form/style.scss | 15 +
.../my-sites/people/followers-list/README.md | 4 +
.../my-sites/people/followers-list/index.jsx | 243 +++
client/my-sites/people/index.js | 45 +
client/my-sites/people/main.jsx | 88 +
.../people/people-list-item/README.md | 5 +
.../people/people-list-item/index.jsx | 69 +
.../people/people-list-item/style.scss | 86 +
.../my-sites/people/people-notices/README.md | 4 +
.../my-sites/people/people-notices/index.jsx | 142 ++
.../my-sites/people/people-notices/style.scss | 3 +
.../my-sites/people/people-profile/README.md | 10 +
.../my-sites/people/people-profile/index.jsx | 178 ++
.../my-sites/people/people-profile/style.scss | 129 ++
.../people/people-section-nav/README.md | 4 +
.../people/people-section-nav/index.jsx | 151 ++
client/my-sites/people/role-select/README.md | 4 +
client/my-sites/people/role-select/index.jsx | 93 +
client/my-sites/people/team-list/README.md | 4 +
client/my-sites/people/team-list/index.jsx | 155 ++
client/my-sites/people/viewers-list/README.md | 4 +
client/my-sites/people/viewers-list/index.jsx | 171 ++
client/my-sites/picker/README.md | 4 +
client/my-sites/picker/package.json | 6 +
client/my-sites/picker/picker.jsx | 105 +
client/my-sites/plans/README.md | 19 +
client/my-sites/plans/controller.jsx | 147 ++
client/my-sites/plans/index.js | 53 +
client/my-sites/plans/main.jsx | 83 +
client/my-sites/plans/plans-select.jsx | 64 +
client/my-sites/plans/style.scss | 16 +
client/my-sites/plugins/README.md | 21 +
client/my-sites/plugins/access-control.js | 69 +
client/my-sites/plugins/controller.js | 216 ++
.../disconnect-jetpack-button.jsx | 73 +
.../disconnect-jetpack-dialog.jsx | 100 +
.../plugins/featured-plugins/README.md | 26 +
.../plugins/featured-plugins/index.jsx | 44 +
.../plugins/featured-plugins/style.scss | 29 +
client/my-sites/plugins/index.js | 26 +
client/my-sites/plugins/main.jsx | 780 +++++++
.../my-sites/plugins/plugin-action/Makefile | 7 +
.../my-sites/plugins/plugin-action/README.md | 48 +
.../plugins/plugin-action/plugin-action.jsx | 54 +
.../my-sites/plugins/plugin-action/style.scss | 45 +
.../plugins/plugin-action/test/index.jsx | 66 +
.../plugins/plugin-activate-toggle/Makefile | 10 +
.../plugins/plugin-activate-toggle/README.md | 28 +
.../plugins/plugin-activate-toggle/index.jsx | 80 +
.../plugins/plugin-activate-toggle/style.scss | 7 +
.../plugin-activate-toggle/test/fixtures.js | 13 +
.../plugin-activate-toggle/test/index.jsx | 79 +
.../test/mocks/actions.js | 6 +
.../test/mocks/plugin-action.jsx | 10 +
.../plugins/plugin-autoupdate-toggle/Makefile | 12 +
.../plugin-autoupdate-toggle/README.md | 30 +
.../plugin-autoupdate-toggle/index.jsx | 62 +
.../plugin-autoupdate-toggle/test/fixtures.js | 15 +
.../plugin-autoupdate-toggle/test/index.jsx | 84 +
.../test/mocks/actions.js | 6 +
.../test/mocks/plugin-action.jsx | 10 +
.../plugins/plugin-card-header/README.md | 41 +
.../plugins/plugin-card-header/index.jsx | 34 +
.../plugins/plugin-card-header/style.scss | 10 +
client/my-sites/plugins/plugin-icon/README.md | 22 +
.../plugins/plugin-icon/plugin-icon.jsx | 26 +
.../my-sites/plugins/plugin-icon/style.scss | 43 +
.../plugins/plugin-information/README.md | 9 +
.../plugins/plugin-information/index.jsx | 226 ++
.../plugins/plugin-information/style.scss | 116 +
.../plugins/plugin-install-button/README.md | 28 +
.../plugins/plugin-install-button/index.jsx | 111 +
.../plugins/plugin-install-button/style.scss | 61 +
client/my-sites/plugins/plugin-item/README.md | 32 +
.../plugins/plugin-item/plugin-item.jsx | 251 +++
.../my-sites/plugins/plugin-item/style.scss | 245 +++
client/my-sites/plugins/plugin-meta/README.md | 26 +
client/my-sites/plugins/plugin-meta/index.jsx | 259 +++
.../my-sites/plugins/plugin-meta/style.scss | 176 ++
.../my-sites/plugins/plugin-ratings/README.md | 22 +
.../my-sites/plugins/plugin-ratings/index.jsx | 73 +
.../plugins/plugin-ratings/style.scss | 49 +
.../plugins/plugin-remove-button/README.md | 24 +
.../plugins/plugin-remove-button/index.jsx | 94 +
.../plugins/plugin-remove-button/style.scss | 23 +
.../plugins/plugin-sections/README.md | 22 +
.../plugins/plugin-sections/index.jsx | 186 ++
.../plugins/plugin-sections/style.scss | 97 +
.../plugins/plugin-site-business/README.md | 24 +
.../plugins/plugin-site-business/index.jsx | 36 +
.../plugins/plugin-site-business/style.scss | 16 +
.../plugin-site-disabled-manage/README.md | 24 +
.../plugin-site-disabled-manage/index.jsx | 37 +
.../plugin-site-disabled-manage/style.scss | 42 +
.../plugins/plugin-site-jetpack/README.md | 26 +
.../plugins/plugin-site-jetpack/index.jsx | 94 +
.../plugins/plugin-site-jetpack/style.scss | 59 +
.../plugins/plugin-site-list/README.md | 23 +
.../plugins/plugin-site-list/index.jsx | 65 +
.../plugins/plugin-site-network/README.md | 28 +
.../plugins/plugin-site-network/index.jsx | 148 ++
.../plugins/plugin-site-network/style.scss | 113 +
.../plugin-site-update-indicator/README.md | 28 +
.../plugin-site-update-indicator/index.jsx | 89 +
.../plugin-site-update-indicator/style.scss | 13 +
client/my-sites/plugins/plugin-site/README.md | 27 +
.../plugins/plugin-site/plugin-site.jsx | 32 +
.../my-sites/plugins/plugin-toggle/README.md | 38 +
.../plugins/plugin-toggle/plugin-toggle.jsx | 46 +
.../my-sites/plugins/plugin-version/README.md | 28 +
.../my-sites/plugins/plugin-version/index.jsx | 63 +
.../plugins/plugin-version/style.scss | 22 +
client/my-sites/plugins/plugin.jsx | 309 +++
.../plugins/plugins-browser-item/README.md | 34 +
.../plugins/plugins-browser-item/index.jsx | 84 +
.../plugins/plugins-browser-item/style.scss | 126 ++
.../plugins/plugins-browser-list/README.md | 32 +
.../plugins/plugins-browser-list/index.jsx | 80 +
.../plugins/plugins-browser-list/style.scss | 43 +
.../plugins/plugins-browser/README.md | 32 +
.../plugins/plugins-browser/index.jsx | 220 ++
.../plugins/plugins-browser/style.scss | 15 +
.../my-sites/plugins/plugins-manage-mixin.js | 20 +
client/my-sites/plugins/style.scss | 3 +
.../post-relative-time-status/README.md | 21 +
.../post-relative-time-status/index.jsx | 93 +
client/my-sites/post-selector/README.md | 21 +
client/my-sites/post-selector/index.jsx | 72 +
client/my-sites/post-selector/no-results.jsx | 36 +
client/my-sites/post-selector/search.jsx | 30 +
client/my-sites/post-selector/search.scss | 20 +
client/my-sites/post-selector/selector.jsx | 232 ++
client/my-sites/post-selector/style.scss | 41 +
client/my-sites/post-trends/README.md | 19 +
client/my-sites/post-trends/day.jsx | 120 ++
client/my-sites/post-trends/index.jsx | 159 ++
client/my-sites/post-trends/month.jsx | 51 +
client/my-sites/post-trends/style.scss | 214 ++
client/my-sites/post-trends/week.jsx | 43 +
client/my-sites/post/README.md | 4 +
client/my-sites/post/post-image/index.jsx | 98 +
client/my-sites/post/post-image/style.scss | 38 +
client/my-sites/posts/README.md | 5 +
client/my-sites/posts/controller.js | 96 +
client/my-sites/posts/index.js | 20 +
client/my-sites/posts/main.jsx | 52 +
client/my-sites/posts/post-controls.jsx | 225 ++
client/my-sites/posts/post-header.jsx | 51 +
client/my-sites/posts/post-list.jsx | 265 +++
client/my-sites/posts/post-placeholder.jsx | 45 +
client/my-sites/posts/post-total-views.jsx | 98 +
client/my-sites/posts/post.jsx | 404 ++++
client/my-sites/posts/posts-navigation.jsx | 381 ++++
client/my-sites/sharing/README.md | 12 +
client/my-sites/sharing/buttons/README.md | 38 +
.../my-sites/sharing/buttons/appearance.jsx | 115 +
client/my-sites/sharing/buttons/buttons.jsx | 136 ++
.../my-sites/sharing/buttons/label-editor.jsx | 87 +
client/my-sites/sharing/buttons/options.jsx | 174 ++
.../sharing/buttons/preview-action.jsx | 52 +
.../sharing/buttons/preview-button.jsx | 56 +
.../sharing/buttons/preview-buttons.jsx | 233 ++
.../sharing/buttons/preview-placeholder.jsx | 51 +
.../sharing/buttons/preview-widget.js | 41 +
client/my-sites/sharing/buttons/preview.jsx | 198 ++
client/my-sites/sharing/buttons/style.jsx | 56 +
client/my-sites/sharing/buttons/tray.jsx | 182 ++
client/my-sites/sharing/connections/README.md | 68 +
.../connections/account-dialog-account.jsx | 61 +
.../connections/account-dialog-account.scss | 57 +
.../sharing/connections/account-dialog.jsx | 185 ++
.../sharing/connections/account-dialog.scss | 53 +
.../sharing/connections/connection.jsx | 173 ++
.../sharing/connections/connections.jsx | 140 ++
.../sharing/connections/service-action.jsx | 78 +
.../service-connected-accounts.jsx | 84 +
.../connections/service-connections.js | 359 ++++
.../connections/service-description.jsx | 99 +
.../sharing/connections/service-example.jsx | 37 +
.../sharing/connections/service-examples.jsx | 254 +++
.../connections/service-placeholder.jsx | 22 +
.../sharing/connections/service-tip.jsx | 69 +
.../my-sites/sharing/connections/service.jsx | 247 +++
.../sharing/connections/services-group.jsx | 107 +
.../sharing/connections/services-group.scss | 18 +
.../connections/services/eventbrite.js | 61 +
.../sharing/connections/services/index.js | 3 +
client/my-sites/sharing/controller.js | 108 +
client/my-sites/sharing/index.js | 20 +
client/my-sites/sharing/main.jsx | 77 +
client/my-sites/sidebar-navigation/README.md | 20 +
.../my-sites/sidebar-navigation/package.json | 6 +
.../sidebar-navigation/sidebar-navigation.jsx | 41 +
client/my-sites/sidebar-navigation/style.scss | 56 +
client/my-sites/sidebar/package.json | 6 +
client/my-sites/sidebar/publish-menu.jsx | 214 ++
client/my-sites/sidebar/sidebar-menu-item.jsx | 59 +
client/my-sites/sidebar/sidebar.jsx | 660 ++++++
client/my-sites/site-indicator/README.md | 16 +
client/my-sites/site-indicator/package.json | 6 +
.../site-indicator/site-indicator.jsx | 249 +++
client/my-sites/site-indicator/style.scss | 144 ++
client/my-sites/site-settings/README.md | 53 +
.../site-settings/action-panel/body.jsx | 21 +
.../site-settings/action-panel/figure.jsx | 37 +
.../site-settings/action-panel/footer.jsx | 21 +
.../site-settings/action-panel/index.jsx | 26 +
.../site-settings/action-panel/style.scss | 83 +
.../site-settings/action-panel/title.jsx | 21 +
client/my-sites/site-settings/controller.js | 192 ++
.../delete-site-options/index.jsx | 153 ++
.../delete-site-options/style.scss | 83 +
.../site-settings/delete-site/index.jsx | 207 ++
.../site-settings/delete-site/style.scss | 61 +
.../my-sites/site-settings/form-analytics.jsx | 174 ++
client/my-sites/site-settings/form-base.js | 157 ++
.../site-settings/form-discussion.jsx | 474 ++++
.../my-sites/site-settings/form-general.jsx | 460 ++++
.../site-settings/form-jetpack-monitor.jsx | 165 ++
.../site-settings/form-jetpack-protect.jsx | 151 ++
.../site-settings/form-jetpack-scan.jsx | 485 +++++
.../my-sites/site-settings/form-writing.jsx | 125 ++
client/my-sites/site-settings/index.js | 34 +
client/my-sites/site-settings/main.jsx | 127 ++
.../site-settings/press-this-link.jsx | 97 +
.../site-settings/related-content-preview.jsx | 68 +
.../site-settings/section-analytics.jsx | 29 +
.../site-settings/section-discussion.jsx | 29 +
.../my-sites/site-settings/section-export.jsx | 88 +
.../site-settings/section-general.jsx | 33 +
.../my-sites/site-settings/section-import.jsx | 219 ++
.../site-settings/section-security.jsx | 68 +
.../site-settings/section-writing.jsx | 29 +
.../settings-card-footer/index.jsx | 26 +
.../settings-card-footer/style.scss | 26 +
.../site-settings/start-over/index.jsx | 78 +
client/my-sites/site/README.md | 24 +
client/my-sites/site/index.jsx | 90 +
client/my-sites/site/placeholder.jsx | 27 +
client/my-sites/site/style.scss | 111 +
client/my-sites/sites/README.md | 24 +
client/my-sites/sites/package.json | 6 +
client/my-sites/sites/site-card.jsx | 168 ++
client/my-sites/sites/sites.jsx | 236 ++
client/my-sites/stats/README.md | 199 ++
client/my-sites/stats/action-follow.jsx | 67 +
client/my-sites/stats/action-link.jsx | 32 +
client/my-sites/stats/action-page.jsx | 36 +
client/my-sites/stats/action-spam.jsx | 76 +
client/my-sites/stats/all-time/index.jsx | 120 ++
client/my-sites/stats/all-time/style.scss | 135 ++
client/my-sites/stats/controller.js | 755 +++++++
client/my-sites/stats/download-csv/README.md | 22 +
client/my-sites/stats/download-csv/index.jsx | 78 +
client/my-sites/stats/follows.jsx | 67 +
client/my-sites/stats/geochart/README.md | 20 +
client/my-sites/stats/geochart/index.jsx | 133 ++
client/my-sites/stats/geochart/style.scss | 16 +
client/my-sites/stats/index.js | 49 +
client/my-sites/stats/info-panel.jsx | 199 ++
client/my-sites/stats/insights.jsx | 129 ++
client/my-sites/stats/mixin-skeleton.js | 30 +
client/my-sites/stats/mixin-toggle.js | 91 +
client/my-sites/stats/module-chart-tabs.jsx | 247 +++
client/my-sites/stats/module-comments.jsx | 239 +++
client/my-sites/stats/module-countries.jsx | 164 ++
client/my-sites/stats/module-date-picker.jsx | 75 +
client/my-sites/stats/module-error.jsx | 22 +
.../my-sites/stats/module-followers-page.jsx | 235 ++
client/my-sites/stats/module-followers.jsx | 249 +++
client/my-sites/stats/module-post-months.jsx | 156 ++
client/my-sites/stats/module-post-weeks.jsx | 179 ++
.../module-site-overview-placeholder.jsx | 59 +
.../my-sites/stats/module-summary-chart.jsx | 163 ++
client/my-sites/stats/module-tab.jsx | 59 +
client/my-sites/stats/module-tabs.jsx | 51 +
.../my-sites/stats/module-video-details.jsx | 67 +
client/my-sites/stats/most-popular/index.jsx | 95 +
client/my-sites/stats/most-popular/style.scss | 84 +
client/my-sites/stats/nux/data.js | 62 +
client/my-sites/stats/nux/insights.jsx | 129 ++
client/my-sites/stats/nux/site.jsx | 103 +
client/my-sites/stats/nux/style.scss | 130 ++
client/my-sites/stats/overview/README.md | 22 +
client/my-sites/stats/overview/index.jsx | 111 +
client/my-sites/stats/overview/style.scss | 50 +
client/my-sites/stats/pagination/README.md | 22 +
client/my-sites/stats/pagination/index.jsx | 75 +
.../stats/pagination/pagination-page.jsx | 70 +
client/my-sites/stats/pagination/style.scss | 88 +
.../my-sites/stats/post-performance/README.md | 20 +
.../my-sites/stats/post-performance/index.jsx | 208 ++
client/my-sites/stats/post.jsx | 68 +
client/my-sites/stats/site.jsx | 323 +++
client/my-sites/stats/stats-list-item.jsx | 289 +++
client/my-sites/stats/stats-list.jsx | 72 +
client/my-sites/stats/stats-module.jsx | 144 ++
client/my-sites/stats/stats-navigation.jsx | 53 +
client/my-sites/stats/stats-overview.jsx | 151 ++
client/my-sites/stats/stats-strings.js | 63 +
client/my-sites/stats/summary.jsx | 203 ++
client/my-sites/upgrades/README.md | 6 +
client/my-sites/upgrades/cart/Makefile | 7 +
client/my-sites/upgrades/cart/README.md | 7 +
client/my-sites/upgrades/cart/cart-body.jsx | 50 +
.../my-sites/upgrades/cart/cart-buttons.jsx | 68 +
client/my-sites/upgrades/cart/cart-coupon.jsx | 98 +
client/my-sites/upgrades/cart/cart-empty.jsx | 39 +
client/my-sites/upgrades/cart/cart-item.jsx | 164 ++
client/my-sites/upgrades/cart/cart-items.jsx | 82 +
.../upgrades/cart/cart-messages-mixin.jsx | 69 +
.../my-sites/upgrades/cart/cart-plan-ad.jsx | 45 +
.../upgrades/cart/cart-summary-bar.jsx | 39 +
client/my-sites/upgrades/cart/cart-total.jsx | 56 +
.../my-sites/upgrades/cart/popover-cart.jsx | 143 ++
.../my-sites/upgrades/cart/secondary-cart.jsx | 38 +
client/my-sites/upgrades/cart/style.scss | 331 +++
.../upgrades/cart/test/test-cart-buttons.jsx | 62 +
client/my-sites/upgrades/checkout/README.md | 4 +
.../my-sites/upgrades/checkout/checkout.jsx | 154 ++
.../checkout/credit-card-payment-box.jsx | 72 +
.../checkout/credit-card-selector.jsx | 95 +
.../upgrades/checkout/credits-payment-box.jsx | 64 +
.../upgrades/checkout/domain-details-form.jsx | 318 +++
.../checkout/free-cart-payment-box.jsx | 81 +
.../upgrades/checkout/new-card-form.jsx | 60 +
.../my-sites/upgrades/checkout/package.json | 6 +
.../my-sites/upgrades/checkout/pay-button.jsx | 142 ++
.../upgrades/checkout/payment-box.jsx | 25 +
.../upgrades/checkout/paypal-payment-box.jsx | 182 ++
.../checkout/privacy-protection-dialog.jsx | 86 +
.../checkout/privacy-protection-example.jsx | 86 +
.../upgrades/checkout/privacy-protection.jsx | 128 ++
.../upgrades/checkout/secure-payment-form.jsx | 203 ++
.../upgrades/checkout/stored-card.jsx | 25 +
.../upgrades/checkout/stored-card.scss | 64 +
.../upgrades/checkout/subscription-text.jsx | 27 +
.../upgrades/checkout/subscription-text.scss | 14 +
.../upgrades/checkout/supporting-text.jsx | 88 +
.../upgrades/checkout/terms-of-service.jsx | 31 +
.../my-sites/upgrades/checkout/thank-you.jsx | 640 ++++++
.../checkout/transaction-steps-mixin.jsx | 179 ++
client/my-sites/upgrades/components/README.md | 4 +
.../components/domain-warnings/Makefile | 12 +
.../components/domain-warnings/index.jsx | 195 ++
.../test/test-domain-warnings.jsx | 128 ++
.../components/form/country-select.jsx | 77 +
.../upgrades/components/form/focus-mixin.js | 38 +
.../upgrades/components/form/hidden-input.jsx | 50 +
.../upgrades/components/form/input.jsx | 95 +
.../upgrades/components/form/state-select.jsx | 73 +
client/my-sites/upgrades/controller.jsx | 254 +++
.../upgrades/domain-management/README.md | 21 +
.../add-email-addresses-card.jsx | 305 +++
.../add-google-apps/domains-select.jsx | 47 +
.../add-google-apps/index.jsx | 71 +
.../components/domain/main-placeholder.jsx | 38 +
.../components/domain/primary-flag.jsx | 32 +
.../components/form-footer/index.jsx | 21 +
.../components/header/index.jsx | 32 +
.../components/icann-verification.jsx | 70 +
.../contacts-privacy/card.jsx | 97 +
.../contacts-privacy/contact-display.jsx | 38 +
.../contacts-privacy/index.jsx | 81 +
.../upgrades/domain-management/controller.jsx | 313 +++
.../domain-management/dns/a-record.jsx | 65 +
.../domain-management/dns/cname-record.jsx | 60 +
.../domain-management/dns/dns-add-new.jsx | 187 ++
.../domain-management/dns/dns-details.jsx | 21 +
.../domain-management/dns/dns-list.jsx | 53 +
.../domain-management/dns/dns-record.jsx | 100 +
.../upgrades/domain-management/dns/index.jsx | 77 +
.../domain-management/dns/mx-record.jsx | 73 +
.../domain-management/dns/srv-record.jsx | 117 +
.../domain-management/dns/txt-record.jsx | 69 +
.../domain-management/domain-management.jsx | 15 +
.../edit-contact-info/form-card.jsx | 289 +++
.../edit-contact-info/index.jsx | 73 +
.../privacy-enabled-card.jsx | 31 +
.../edit/card/header/index.jsx | 44 +
.../card/header/primary-domain-button.jsx | 56 +
.../domain-management/edit/card/property.jsx | 21 +
.../edit/card/subscription-settings.jsx | 23 +
.../upgrades/domain-management/edit/index.jsx | 68 +
.../domain-management/edit/mapped-domain.jsx | 112 +
.../edit/registered-domain.jsx | 199 ++
.../domain-management/edit/site-redirect.jsx | 82 +
.../domain-management/edit/wpcom-domain.jsx | 53 +
.../email-forwarding-add-new.jsx | 196 ++
.../email-forwarding-details.jsx | 36 +
.../email-forwarding-item.jsx | 58 +
.../email-forwarding-limit.jsx | 26 +
.../email-forwarding-list.jsx | 38 +
.../email-forwarding/index.jsx | 69 +
.../email/add-google-apps-card.jsx | 176 ++
.../email/google-apps-users-card.jsx | 145 ++
.../domain-management/email/index.jsx | 167 ++
.../upgrades/domain-management/list/index.jsx | 99 +
.../list/item-placeholder.jsx | 26 +
.../upgrades/domain-management/list/item.jsx | 49 +
.../name-servers/custom-nameservers-form.jsx | 178 ++
.../name-servers/custom-nameservers-row.jsx | 60 +
.../name-servers/icann-verification-card.jsx | 45 +
.../domain-management/name-servers/index.jsx | 198 ++
.../name-servers/wpcom-nameservers-toggle.jsx | 83 +
.../upgrades/domain-management/package.json | 6 +
.../primary-domain/index.jsx | 135 ++
.../privacy-protection/card/add-button.jsx | 40 +
.../privacy-protection/card/content.jsx | 69 +
.../privacy-protection/card/header.jsx | 53 +
.../privacy-protection/index.jsx | 102 +
.../domain-management/site-redirect/index.jsx | 153 ++
.../site-redirect/notice.jsx | 42 +
.../upgrades/domain-management/style.scss | 1006 +++++++++
.../transfer/enable-domain-locking-notice.jsx | 88 +
.../transfer/enable-privacy-notice.jsx | 104 +
.../transfer/icann-verification-notice.jsx | 48 +
.../domain-management/transfer/index.jsx | 101 +
.../transfer/pending-transfer-notice.jsx | 135 ++
.../transfer/request-transfer-code.jsx | 245 +++
.../transfer/transfer-prohibited-notice.jsx | 40 +
.../my-sites/upgrades/domain-search/Makefile | 11 +
.../my-sites/upgrades/domain-search/README.md | 10 +
.../upgrades/domain-search/domain-search.jsx | 100 +
.../upgrades/domain-search/package.json | 6 +
.../domain-search/site-redirect-step.jsx | 142 ++
.../upgrades/domain-search/site-redirect.jsx | 63 +
.../test/test-domain-suggestion.jsx | 46 +
client/my-sites/upgrades/index.js | 284 +++
client/my-sites/upgrades/navigation.jsx | 213 ++
client/my-sites/upgrades/paths.js | 112 +
client/my-sites/welcome/README.md | 67 +
client/my-sites/welcome/package.json | 6 +
client/my-sites/welcome/welcome.jsx | 55 +
client/notices/README.md | 46 +
client/notices/arrow-link.jsx | 31 +
client/notices/delete-site-notices.jsx | 85 +
client/notices/index.js | 191 ++
client/notices/notice.jsx | 91 +
client/notices/notices-list.jsx | 95 +
client/notices/simple-notice.jsx | 85 +
client/notices/site-notice.jsx | 27 +
client/notices/style.scss | 293 +++
client/notices/validation-error-list.jsx | 34 +
client/notifications/README.md | 35 +
client/notifications/index.jsx | 222 ++
client/nux-welcome/README.md | 4 +
client/nux-welcome/index.js | 65 +
client/nux-welcome/welcome-message.jsx | 96 +
client/post-editor/Makefile | 10 +
client/post-editor/README.md | 13 +
client/post-editor/controller.js | 171 ++
client/post-editor/drafts-button/index.jsx | 31 +
client/post-editor/drafts-button/style.scss | 10 +
client/post-editor/edit-post-status/index.jsx | 236 ++
.../post-editor/edit-post-status/style.scss | 79 +
.../post-editor/editor-action-bar/index.jsx | 106 +
.../post-editor/editor-action-bar/style.scss | 40 +
client/post-editor/editor-author/index.jsx | 76 +
client/post-editor/editor-author/style.scss | 31 +
.../post-editor/editor-categories/index.jsx | 118 +
.../post-editor/editor-categories/style.scss | 25 +
.../post-editor/editor-delete-post/index.jsx | 97 +
.../post-editor/editor-delete-post/style.scss | 16 +
client/post-editor/editor-discussion/Makefile | 7 +
.../post-editor/editor-discussion/index.jsx | 109 +
.../post-editor/editor-discussion/style.scss | 8 +
.../editor-discussion/test/index.jsx | 179 ++
.../post-editor/editor-drawer-well/index.jsx | 79 +
.../post-editor/editor-drawer-well/style.scss | 62 +
client/post-editor/editor-drawer/index.jsx | 290 +++
client/post-editor/editor-drawer/style.scss | 56 +
.../editor-featured-image/index.jsx | 131 ++
.../preview-container.jsx | 87 +
.../editor-featured-image/preview.jsx | 49 +
.../editor-featured-image/style.scss | 39 +
client/post-editor/editor-fieldset/README.md | 30 +
client/post-editor/editor-fieldset/index.jsx | 31 +
client/post-editor/editor-fieldset/style.scss | 23 +
.../editor-ground-control/Makefile | 10 +
.../editor-ground-control/index.jsx | 424 ++++
.../editor-ground-control/style.scss | 150 ++
.../editor-ground-control/test/index.jsx | 461 ++++
client/post-editor/editor-location/index.jsx | 145 ++
.../editor-location/search-result.jsx | 29 +
client/post-editor/editor-location/search.jsx | 112 +
client/post-editor/editor-location/style.scss | 54 +
.../editor-mobile-navigation/index.jsx | 36 +
.../editor-mobile-navigation/style.scss | 35 +
.../post-editor/editor-more-options/slug.jsx | 49 +
.../editor-more-options/style.scss | 12 +
.../post-editor/editor-page-order/index.jsx | 73 +
.../post-editor/editor-page-order/style.scss | 16 +
.../post-editor/editor-page-parent/index.jsx | 55 +
.../post-editor/editor-page-parent/style.scss | 48 +
client/post-editor/editor-page-slug/index.jsx | 33 +
.../editor-page-templates/index.jsx | 85 +
client/post-editor/editor-permalink/index.jsx | 144 ++
.../post-editor/editor-permalink/style.scss | 48 +
.../editor-post-formats/accordion.jsx | 80 +
.../post-editor/editor-post-formats/index.jsx | 102 +
.../editor-post-formats/style.scss | 29 +
client/post-editor/editor-post-type/index.jsx | 107 +
.../post-editor/editor-post-type/style.scss | 45 +
client/post-editor/editor-preview/index.jsx | 95 +
client/post-editor/editor-revisions/index.jsx | 55 +
.../post-editor/editor-revisions/style.scss | 27 +
client/post-editor/editor-sharing/Makefile | 10 +
.../post-editor/editor-sharing/accordion.jsx | 126 ++
client/post-editor/editor-sharing/index.jsx | 83 +
.../editor-sharing/publicize-connection.jsx | 106 +
.../editor-sharing/publicize-message.jsx | 97 +
.../editor-sharing/publicize-message.scss | 27 +
.../editor-sharing/publicize-options.jsx | 215 ++
.../editor-sharing/publicize-options.scss | 40 +
.../editor-sharing/publicize-services.jsx | 56 +
.../editor-sharing/publicize-services.scss | 12 +
.../editor-sharing/sharing-like-options.jsx | 111 +
client/post-editor/editor-sharing/style.scss | 74 +
.../post-editor/editor-sharing/test/index.js | 3 +
.../test/specs/publicize-connection.jsx | 113 +
client/post-editor/editor-slug/index.jsx | 127 ++
client/post-editor/editor-slug/style.scss | 39 +
client/post-editor/editor-tags/index.jsx | 89 +
client/post-editor/editor-taxonomies/Makefile | 10 +
.../editor-taxonomies/accordion.jsx | 201 ++
.../editor-taxonomies/test/accordion.jsx | 182 ++
client/post-editor/editor-title/container.jsx | 52 +
client/post-editor/editor-title/index.jsx | 110 +
client/post-editor/editor-title/style.scss | 55 +
.../post-editor/editor-visibility/index.jsx | 390 ++++
.../post-editor/editor-visibility/style.scss | 66 +
client/post-editor/index.js | 25 +
client/post-editor/invalid-url-dialog.jsx | 91 +
client/post-editor/media-modal/Makefile | 10 +
.../media-modal/back-to-library.jsx | 17 +
client/post-editor/media-modal/constants.js | 10 +
.../media-modal/detail/_style.scss | 191 ++
.../media-modal/detail/detail-fields.jsx | 140 ++
.../media-modal/detail/detail-file-info.jsx | 106 +
.../media-modal/detail/detail-item.jsx | 131 ++
.../detail/detail-preview-audio.jsx | 26 +
.../detail/detail-preview-document.jsx | 21 +
.../detail/detail-preview-image.jsx | 36 +
.../detail/detail-preview-video.jsx | 31 +
.../detail/detail-preview-videopress.jsx | 47 +
.../media-modal/detail/detail-title.jsx | 98 +
.../post-editor/media-modal/detail/index.jsx | 83 +
client/post-editor/media-modal/fieldset.jsx | 25 +
client/post-editor/media-modal/fieldset.scss | 35 +
.../media-modal/gallery-help-container.jsx | 96 +
.../post-editor/media-modal/gallery-help.jsx | 109 +
.../media-modal/gallery/caption.jsx | 68 +
.../media-modal/gallery/drop-zone.jsx | 52 +
.../media-modal/gallery/edit-item.jsx | 51 +
.../post-editor/media-modal/gallery/edit.jsx | 62 +
.../media-modal/gallery/fields.jsx | 162 ++
.../post-editor/media-modal/gallery/index.jsx | 155 ++
.../media-modal/gallery/preview.jsx | 122 ++
.../media-modal/gallery/remove-button.jsx | 47 +
.../media-modal/gallery/style.scss | 242 +++
client/post-editor/media-modal/index.jsx | 389 ++++
client/post-editor/media-modal/index.scss | 324 +++
client/post-editor/media-modal/markup.js | 215 ++
.../post-editor/media-modal/preload-image.js | 10 +
.../media-modal/secondary-actions.jsx | 166 ++
client/post-editor/media-modal/style.scss | 4 +
client/post-editor/media-modal/test/index.js | 5 +
.../media-modal/test/specs/index.jsx | 169 ++
.../media-modal/test/specs/markup.js | 305 +++
.../media-modal/test/specs/preload-image.js | 52 +
client/post-editor/post-editor.jsx | 838 ++++++++
client/post-editor/restore-post-dialog.jsx | 95 +
client/post-editor/status-label.jsx | 173 ++
client/post-editor/style.scss | 334 +++
client/post-editor/test/post-editor.jsx | 159 ++
client/reader/README.md | 12 +
client/reader/_style.scss | 325 +++
client/reader/comments/README.md | 7 +
client/reader/comments/comment-likes.jsx | 67 +
client/reader/comments/form.jsx | 195 ++
client/reader/comments/helper.jsx | 19 +
client/reader/comments/index.jsx | 337 +++
client/reader/comments/style.scss | 243 +++
client/reader/controller.js | 536 +++++
client/reader/discover/README.md | 3 +
client/reader/discover/_style.scss | 73 +
client/reader/discover/helper.jsx | 45 +
client/reader/discover/post-attribution.jsx | 46 +
client/reader/discover/site-attribution.jsx | 41 +
client/reader/discover/visit-link.jsx | 40 +
client/reader/feed-error/README.md | 7 +
client/reader/feed-error/index.jsx | 30 +
client/reader/feed-header/README.md | 8 +
client/reader/feed-header/index.jsx | 83 +
client/reader/feed-header/style.scss | 168 ++
client/reader/feed-stream/README.md | 9 +
client/reader/feed-stream/empty.jsx | 45 +
client/reader/feed-stream/index.jsx | 150 ++
client/reader/follow-button/README.md | 10 +
client/reader/follow-button/index.jsx | 36 +
client/reader/following-edit/README.md | 9 +
client/reader/following-edit/helper.jsx | 36 +
client/reader/following-edit/index.jsx | 367 ++++
client/reader/following-edit/list-item.jsx | 127 ++
client/reader/following-edit/navigation.jsx | 33 +
.../following-edit/notification-settings.jsx | 175 ++
client/reader/following-edit/placeholder.jsx | 25 +
.../reader/following-edit/sort-controls.jsx | 45 +
client/reader/following-edit/style.scss | 293 +++
.../following-edit/subscribe-form-result.jsx | 44 +
.../reader/following-edit/subscribe-form.jsx | 151 ++
client/reader/following-stream/README.md | 17 +
client/reader/following-stream/_style.scss | 262 +++
client/reader/following-stream/empty.jsx | 45 +
client/reader/following-stream/index.jsx | 440 ++++
.../reader/following-stream/post-blocked.jsx | 33 +
.../following-stream/post-placeholder.jsx | 33 +
.../following-stream/post-unavailable.jsx | 41 +
client/reader/following-stream/post.jsx | 433 ++++
client/reader/following-stream/x-post.jsx | 151 ++
client/reader/full-post/README.md | 13 +
client/reader/full-post/_style.scss | 321 +++
client/reader/full-post/index.jsx | 447 ++++
client/reader/index.js | 81 +
client/reader/like-button/README.md | 7 +
client/reader/like-button/index.jsx | 21 +
client/reader/like-helper.jsx | 18 +
client/reader/liked-stream/README.md | 7 +
client/reader/liked-stream/empty.jsx | 43 +
client/reader/liked-stream/index.jsx | 22 +
client/reader/list-gap/README.md | 9 +
client/reader/list-gap/_style.scss | 48 +
client/reader/list-gap/index.jsx | 48 +
client/reader/list-item/README.md | 17 +
client/reader/list-item/actions.jsx | 11 +
client/reader/list-item/description.jsx | 12 +
client/reader/list-item/icon.jsx | 20 +
client/reader/list-item/index.jsx | 22 +
client/reader/list-item/style.scss | 93 +
client/reader/list-item/title.jsx | 16 +
client/reader/list-management/README.md | 3 +
.../reader/list-management/contents/index.jsx | 29 +
.../description-edit/index.jsx | 29 +
.../list-management/followers/index.jsx | 29 +
.../list-management/navigation/index.jsx | 36 +
client/reader/list-management/style.scss | 0
client/reader/list-stream/README.md | 9 +
client/reader/list-stream/empty.jsx | 43 +
client/reader/list-stream/index.jsx | 115 +
client/reader/post-byline/README.md | 8 +
client/reader/post-byline/_style.scss | 55 +
client/reader/post-byline/index.jsx | 87 +
client/reader/post-errors/README.md | 9 +
client/reader/post-errors/index.jsx | 130 ++
client/reader/post-errors/style.scss | 11 +
client/reader/post-excerpt-link/README.md | 8 +
client/reader/post-excerpt-link/index.jsx | 60 +
client/reader/post-excerpt-link/style.scss | 54 +
client/reader/post-images/README.md | 7 +
client/reader/post-images/_style.scss | 185 ++
client/reader/post-images/index.jsx | 186 ++
client/reader/post-options/README.md | 8 +
client/reader/post-options/_style.scss | 79 +
client/reader/post-options/index.jsx | 170 ++
client/reader/post-permalink/README.md | 8 +
client/reader/post-permalink/index.jsx | 36 +
client/reader/post-permalink/style.scss | 17 +
client/reader/post-time/README.md | 7 +
client/reader/post-time/index.jsx | 43 +
client/reader/reading-time/README.md | 8 +
client/reader/reading-time/index.jsx | 35 +
client/reader/reading-time/style.scss | 8 +
client/reader/recommendations/README.md | 10 +
.../reader/recommendations/for-you/index.jsx | 146 ++
.../recommendations/global-tags/index.jsx | 16 +
.../recommendations/navigation/index.jsx | 29 +
client/reader/recommendations/sites/index.jsx | 16 +
client/reader/recommendations/style.scss | 6 +
client/reader/share/README.md | 9 +
client/reader/share/index.jsx | 154 ++
client/reader/share/style.scss | 49 +
client/reader/sidebar/README.md | 7 +
client/reader/sidebar/_style.scss | 104 +
client/reader/sidebar/package.json | 6 +
client/reader/sidebar/sidebar.jsx | 369 ++++
client/reader/site-and-author-icon/README.md | 9 +
.../reader/site-and-author-icon/_style.scss | 18 +
client/reader/site-and-author-icon/index.jsx | 74 +
client/reader/site-link/README.md | 9 +
client/reader/site-link/index.jsx | 28 +
client/reader/site-stream/README.md | 8 +
client/reader/site-stream/empty.jsx | 45 +
client/reader/site-stream/index.jsx | 98 +
client/reader/stats.js | 60 +
client/reader/stream-header/README.md | 14 +
client/reader/stream-header/index.jsx | 57 +
client/reader/stream-header/style.scss | 161 ++
client/reader/tag-stream/README.md | 8 +
client/reader/tag-stream/empty.jsx | 43 +
client/reader/tag-stream/index.jsx | 91 +
client/reader/update-notice/README.md | 23 +
client/reader/update-notice/_style.scss | 41 +
client/reader/update-notice/index.jsx | 59 +
client/reader/utils.js | 27 +
client/reader/xpost-helper.js | 45 +
client/remove-overlay/index.js | 35 +
client/sections.js | 201 ++
client/signup/Makefile | 12 +
client/signup/README.md | 192 ++
client/signup/config/Makefile | 8 +
client/signup/config/flows.js | 159 ++
client/signup/config/step-components.js | 22 +
client/signup/config/steps.js | 67 +
client/signup/config/test/lib/abtest/index.js | 5 +
.../config/test/lib/signup/step-actions.js | 2 +
client/signup/config/test/lib/user/index.js | 11 +
client/signup/config/test/test.js | 23 +
client/signup/controller.js | 128 ++
.../signup/flow-progress-indicator/index.jsx | 36 +
.../signup/flow-progress-indicator/style.scss | 11 +
client/signup/index.js | 30 +
client/signup/locale-suggestions/index.jsx | 111 +
client/signup/locale-suggestions/style.scss | 15 +
client/signup/log-in-form/index.jsx | 259 +++
client/signup/logged-out-form/index.jsx | 39 +
client/signup/logged-out-form/style.scss | 28 +
client/signup/main.jsx | 344 +++
client/signup/phone-signup-form/index.jsx | 225 ++
client/signup/phone-signup-form/style.scss | 58 +
client/signup/previous-step-button/index.jsx | 55 +
client/signup/previous-step-button/style.scss | 28 +
client/signup/processing-screen/index.jsx | 76 +
client/signup/processing-screen/style.scss | 109 +
client/signup/skip-step-button/index.jsx | 30 +
client/signup/skip-step-button/style.scss | 6 +
client/signup/step-header/index.jsx | 17 +
client/signup/step-header/style.scss | 25 +
client/signup/step-wrapper/index.jsx | 72 +
client/signup/steps/domains/index.jsx | 223 ++
client/signup/steps/domains/style.scss | 4 +
client/signup/steps/dss/index.jsx | 105 +
client/signup/steps/dss/screenshot.jsx | 116 +
client/signup/steps/dss/style.scss | 146 ++
client/signup/steps/dss/theme-thumbnail.jsx | 59 +
.../signup/steps/email-signup-form/index.jsx | 116 +
client/signup/steps/plans/index.jsx | 143 ++
client/signup/steps/plans/style.scss | 23 +
client/signup/steps/site-creation/index.jsx | 288 +++
client/signup/steps/site-creation/style.scss | 11 +
client/signup/steps/test-step/index.jsx | 36 +
client/signup/steps/theme-selection/index.jsx | 59 +
.../signup/steps/theme-selection/style.scss | 44 +
.../steps/theme-selection/theme-thumbnail.jsx | 51 +
client/signup/style.scss | 63 +
client/signup/submit-step-button/index.jsx | 27 +
client/signup/test/flows-test.js | 22 +
client/signup/test/lib/abtest/index.js | 5 +
client/signup/test/lib/user/index.js | 17 +
client/signup/test/signup/config/flows.js | 24 +
client/signup/test/signup/config/steps.js | 5 +
client/signup/test/utils-test.js | 126 ++
client/signup/utils.js | 105 +
client/signup/validation-fieldset/index.jsx | 40 +
client/signup/validation-fieldset/style.scss | 31 +
client/signup/wpcom-login-form/index.jsx | 43 +
client/vip/README.md | 13 +
client/vip/controller.js | 133 ++
client/vip/index.js | 57 +
client/vip/style.scss | 0
client/vip/vip-backups/README.md | 4 +
client/vip/vip-backups/index.jsx | 38 +
client/vip/vip-billing/README.md | 4 +
client/vip/vip-billing/index.jsx | 38 +
client/vip/vip-dashboard/README.md | 4 +
client/vip/vip-dashboard/index.jsx | 38 +
client/vip/vip-deploys/README.md | 4 +
client/vip/vip-deploys/index.jsx | 38 +
client/vip/vip-logs/README.md | 4 +
client/vip/vip-logs/index.jsx | 202 ++
client/vip/vip-logs/logs-table.jsx | 123 ++
client/vip/vip-logs/style.scss | 55 +
client/vip/vip-support/README.md | 4 +
client/vip/vip-support/index.jsx | 38 +
config/README.md | 35 +
config/client.json | 27 +
config/desktop-mac-app-store.json | 289 +++
config/desktop.json | 285 +++
config/development.json | 312 +++
config/empty-secrets.json | 3 +
config/horizon.json | 270 +++
config/production.json | 263 +++
config/stage.json | 267 +++
config/wpcalypso.json | 281 +++
docs/code-reviews.md | 27 +
docs/coding-guidelines.md | 35 +
docs/coding-guidelines/css.md | 279 +++
docs/coding-guidelines/html.md | 81 +
docs/coding-guidelines/javascript.md | 1083 ++++++++++
docs/git-workflow.md | 31 +
docs/guide/0-values.md | 23 +
docs/guide/index.md | 8 +
docs/guide/tech-behind-calypso.md | 93 +
docs/icons.md | 69 +
docs/merge-checklist.md | 21 +
docs/performance.md | 13 +
docs/react-component-unit-testing.md | 145 ++
docs/reactivity.md | 19 +
docs/rtl.md | 11 +
index.js | 27 +
jsconfig.json | 10 +
package.json | 131 ++
public/fonts/wpeditor.eot | Bin 0 -> 6372 bytes
public/fonts/wpeditor.svg | 21 +
public/fonts/wpeditor.ttf | Bin 0 -> 6204 bytes
public/fonts/wpeditor.woff | Bin 0 -> 3596 bytes
public/images/.gitkeep | 0
public/images/authorize/background-1.jpg | Bin 0 -> 181428 bytes
public/images/authorize/background-2.jpg | Bin 0 -> 173484 bytes
public/images/authorize/background-3.jpg | Bin 0 -> 155333 bytes
public/images/authorize/background-4.jpg | Bin 0 -> 173279 bytes
public/images/authorize/background-5.jpg | Bin 0 -> 146161 bytes
public/images/authorize/background-6.jpg | Bin 0 -> 149864 bytes
.../images/comments/illustration_comments.svg | 26 +
public/images/delete-site/export-content.png | Bin 0 -> 12814 bytes
public/images/delete-site/start-over.png | Bin 0 -> 12093 bytes
public/images/devices/iframe-back.png | Bin 0 -> 936 bytes
public/images/drake/drake-404.svg | 36 +
public/images/drake/drake-500.svg | 44 +
public/images/drake/drake-all-done.svg | 32 +
public/images/drake/drake-browser.svg | 43 +
public/images/drake/drake-empty-results.svg | 30 +
public/images/drake/drake-jetpack.svg | 38 +
public/images/drake/drake-new.svg | 30 +
public/images/drake/drake-nomedia.svg | Bin 0 -> 36408 bytes
public/images/drake/drake-nomenus.svg | 40 +
public/images/drake/drake-nosites.svg | 38 +
public/images/drake/drake-ok.svg | 33 +
public/images/drake/drake-whoops.svg | 24 +
.../images/favicons/favicon-development.ico | Bin 0 -> 32956 bytes
public/images/favicons/favicon-horizon.ico | Bin 0 -> 32956 bytes
public/images/favicons/favicon-staging.ico | Bin 0 -> 32956 bytes
public/images/favicons/favicon-wpcalypso.ico | Bin 0 -> 32956 bytes
public/images/loading.gif | Bin 0 -> 17794 bytes
public/images/me/pattern-dark.png | Bin 0 -> 13043 bytes
public/images/pages/illustration-pages.svg | 68 +
public/images/pages/photolia.jpg | Bin 0 -> 166383 bytes
public/images/people/mystery-person.svg | 9 +
public/images/plans/plan-beginner.svg | 20 +
public/images/plans/plan-business.svg | 65 +
public/images/plans/plan-premium.svg | 199 ++
public/images/posts/illustration-posts.svg | 23 +
public/images/related-posts/cat-blog.png | Bin 0 -> 57843 bytes
public/images/related-posts/devices.jpg | Bin 0 -> 26555 bytes
.../images/related-posts/mobile-wedding.jpg | Bin 0 -> 38164 bytes
public/images/sharing/eventbrite-list.png | Bin 0 -> 24012 bytes
public/images/sharing/eventbrite-widget.png | Bin 0 -> 20773 bytes
public/images/sharing/facebook-profile.png | Bin 0 -> 66906 bytes
public/images/sharing/facebook-sharing.png | Bin 0 -> 8260 bytes
public/images/sharing/google-publicize.png | Bin 0 -> 56814 bytes
public/images/sharing/google-sharing.png | Bin 0 -> 8373 bytes
public/images/sharing/instagram-media.png | Bin 0 -> 66863 bytes
public/images/sharing/instagram-widget.png | Bin 0 -> 76542 bytes
public/images/sharing/linkedin-publicize.png | Bin 0 -> 51325 bytes
public/images/sharing/linkedin-sharing.png | Bin 0 -> 8579 bytes
public/images/sharing/path-publicize.png | Bin 0 -> 32362 bytes
public/images/sharing/tumblr-publicize.png | Bin 0 -> 152087 bytes
public/images/sharing/tumblr-sharing.png | Bin 0 -> 8275 bytes
public/images/sharing/twitter-publicize.png | Bin 0 -> 53284 bytes
public/images/sharing/twitter-timeline.png | Bin 0 -> 52498 bytes
public/images/stats/chart-today.png | Bin 0 -> 3778 bytes
public/images/stats/chart.png | Bin 0 -> 6492 bytes
public/images/stats/chart2.png | Bin 0 -> 11051 bytes
public/images/stats/date-picker.png | Bin 0 -> 33247 bytes
.../images/stats/illustration-stats-intro.svg | 88 +
public/images/stats/illustration-stats.svg | 43 +
public/images/stats/left-arrow.svg | 7 +
public/images/stats/map.jpg | Bin 0 -> 29662 bytes
public/images/stats/right-arrow.svg | 7 +
public/images/stats/search-engine.png | Bin 0 -> 404 bytes
public/images/stats/stats-geo-chart.png | Bin 0 -> 335298 bytes
public/images/upgrades/cc-amex-disabled.svg | 47 +
public/images/upgrades/cc-amex.svg | 1 +
.../images/upgrades/cc-discover-disabled.svg | 53 +
public/images/upgrades/cc-discover.svg | 1 +
.../upgrades/cc-mastercard-disabled.svg | 56 +
public/images/upgrades/cc-mastercard.svg | 1 +
public/images/upgrades/cc-placeholder.svg | 14 +
public/images/upgrades/cc-visa-disabled.svg | 28 +
public/images/upgrades/cc-visa.svg | 1 +
public/images/upgrades/google-apps-logo.png | Bin 0 -> 5680 bytes
public/images/upgrades/paypal-disabled.svg | 85 +
public/images/upgrades/paypal.svg | 2 +
public/images/upgrades/plugins/ecwid.png | Bin 0 -> 29286 bytes
public/images/upgrades/plugins/gumroad.png | Bin 0 -> 26229 bytes
.../images/upgrades/plugins/shopify-store.png | Bin 0 -> 34431 bytes
.../tinymce/skins/wordpress/images/audio.png | Bin 0 -> 412 bytes
.../skins/wordpress/images/dashicon-edit.png | Bin 0 -> 368 bytes
.../skins/wordpress/images/dashicon-no.png | Bin 0 -> 339 bytes
.../skins/wordpress/images/embedded.png | Bin 0 -> 8177 bytes
.../skins/wordpress/images/gallery-2x.png | Bin 0 -> 447 bytes
.../skins/wordpress/images/gallery.png | Bin 0 -> 379 bytes
.../skins/wordpress/images/more-2x.png | Bin 0 -> 603 bytes
.../tinymce/skins/wordpress/images/more.png | Bin 0 -> 414 bytes
.../skins/wordpress/images/pagebreak-2x.png | Bin 0 -> 835 bytes
.../skins/wordpress/images/pagebreak.png | Bin 0 -> 1140 bytes
.../skins/wordpress/images/playlist-audio.png | Bin 0 -> 440 bytes
.../skins/wordpress/images/playlist-video.png | Bin 0 -> 290 bytes
.../tinymce/skins/wordpress/images/video.png | Bin 0 -> 363 bytes
public/tinymce/skins/wordpress/wp-content.css | 539 +++++
server/README.md | 14 +
server/api/Makefile | 12 +
server/api/README.md | 8 +
server/api/index.js | 27 +
server/api/oauth.js | 61 +
server/api/test/test.js | 102 +
server/boot/index.js | 77 +
server/build/README.md | 6 +
server/build/index.js | 72 +
server/bundler/README.md | 85 +
server/bundler/assets.js | 13 +
server/bundler/bin/bundler.js | 82 +
server/bundler/bin/list-assets.js | 11 +
server/bundler/hot-reloader.js | 70 +
server/bundler/index.js | 63 +
server/bundler/loader.js | 111 +
server/bundler/plugin.js | 44 +
server/bundler/utils.js | 39 +
server/config/index.js | 63 +
server/devdocs/README.md | 11 +
server/devdocs/bin/generate-devdocs-index | 118 +
server/devdocs/index.js | 202 ++
server/i18n/Makefile | 15 +
server/i18n/README.md | 22 +
server/i18n/bin/i18n-cli.js | 75 +
server/i18n/index.js | 194 ++
server/i18n/preprocess-xgettextjs-match.js | 107 +
server/i18n/test/.gitignore | 2 +
.../i18n-test-example-second-file.jsx | 9 +
.../i18n/test/examples/i18n-test-examples.jsx | 69 +
server/i18n/test/test.js | 101 +
server/i18nlint/Makefile | 7 +
server/i18nlint/README.md | 45 +
server/i18nlint/bin/i18nlint-cli.js | 63 +
server/i18nlint/i18nlint.js | 396 ++++
server/i18nlint/test/test-i18nlint.js | 136 ++
.../testfiles/concatenation-and-quotes.js | 12 +
.../test/testfiles/duplicate-placeholders.js | 20 +
server/i18nlint/test/testfiles/fine.js | 29 +
server/i18nlint/test/testfiles/hashbang.js | 21 +
.../testfiles/missing-singular-placeholder.js | 21 +
.../non-literal-translate-arguments.js | 24 +
server/i18nlint/test/testfiles/testfile.jsx | 39 +
server/pages/404.jade | 27 +
server/pages/500.jade | 23 +
server/pages/README.md | 6 +
server/pages/desktop.jade | 44 +
server/pages/index.jade | 97 +
server/pages/index.js | 387 ++++
server/sanitize/index.js | 53 +
server/user-bootstrap/index.js | 58 +
server/user-bootstrap/shared-utils.js | 71 +
shared/README.md | 48 +
shared/components/card/README.md | 34 +
shared/components/card/compact.jsx | 25 +
shared/components/card/docs/example.jsx | 64 +
shared/components/card/index.jsx | 40 +
shared/components/card/style.scss | 56 +
shared/components/data/screen-title/index.jsx | 30 +
.../data/store-connection/index.jsx | 60 +
.../data/themes-list-fetcher/README.md | 12 +
.../data/themes-list-fetcher/index.jsx | 124 ++
shared/components/empty-content/README.md | 66 +
.../empty-content/empty-content.jsx | 120 ++
.../empty-content/no-sites-message/README.md | 12 +
.../empty-content/no-sites-message/index.jsx | 25 +
shared/components/empty-content/package.json | 6 +
shared/components/empty-content/style.scss | 60 +
shared/components/gridicon/README.md | 20 +
shared/components/gridicon/docs/example.jsx | 165 ++
shared/components/gridicon/index.jsx | 533 +++++
shared/components/gridicon/style.scss | 7 +
shared/components/theme/Makefile | 13 +
shared/components/theme/README.md | 11 +
shared/components/theme/docs/example.jsx | 39 +
shared/components/theme/index.jsx | 150 ++
shared/components/theme/more-button.jsx | 95 +
shared/components/theme/style.scss | 225 ++
shared/components/theme/test/index.jsx | 151 ++
shared/components/themes-list/Makefile | 13 +
shared/components/themes-list/README.md | 13 +
shared/components/themes-list/index.jsx | 105 +
shared/components/themes-list/style.scss | 14 +
shared/components/themes-list/test/index.jsx | 93 +
shared/dispatcher/index.js | 22 +
shared/lib/formatting/Makefile | 8 +
shared/lib/formatting/README.md | 4 +
.../lib/formatting/decode-entities/browser.js | 5 +
shared/lib/formatting/decode-entities/node.js | 1 +
.../formatting/decode-entities/package.json | 10 +
shared/lib/formatting/index.js | 346 +++
shared/lib/formatting/test/test.js | 93 +
shared/lib/i18n-utils/Makefile | 9 +
shared/lib/i18n-utils/README.md | 10 +
shared/lib/i18n-utils/browser.js | 11 +
shared/lib/i18n-utils/node.js | 10 +
shared/lib/i18n-utils/package.json | 7 +
shared/lib/i18n-utils/test/utils-test.js | 27 +
shared/lib/i18n-utils/utils.js | 45 +
shared/lib/mixins/emitter/index.js | 18 +
shared/lib/screen-title/README.md | 5 +
shared/lib/screen-title/actions.js | 36 +
shared/lib/screen-title/constants.js | 11 +
shared/lib/screen-title/store.js | 55 +
shared/lib/screen-title/utils.js | 58 +
shared/lib/themes/Makefile | 13 +
shared/lib/themes/README.md | 28 +
shared/lib/themes/actions.js | 220 ++
shared/lib/themes/constants.js | 31 +
shared/lib/themes/helpers.js | 89 +
shared/lib/themes/reducers/current-theme.js | 39 +
.../lib/themes/reducers/themes-last-event.js | 45 +
.../lib/themes/reducers/themes-last-query.js | 45 +
shared/lib/themes/reducers/themes-list.js | 138 ++
shared/lib/themes/reducers/themes.js | 46 +
shared/lib/themes/stores/current-theme.js | 14 +
shared/lib/themes/stores/themes-last-event.js | 12 +
shared/lib/themes/stores/themes-last-query.js | 18 +
shared/lib/themes/stores/themes-list.js | 15 +
shared/lib/themes/stores/themes.js | 16 +
shared/lib/themes/test/current-theme-store.js | 38 +
shared/lib/themes/test/themes-list-store.js | 173 ++
shared/lib/themes/test/themes-store.js | 74 +
shared/lib/url/index.js | 23 +
shared/lib/wp/README.md | 10 +
shared/lib/wp/browser.js | 36 +
shared/lib/wp/node.js | 6 +
shared/lib/wp/package.json | 7 +
shared/lib/wpcom-undocumented/README.md | 16 +
shared/lib/wpcom-undocumented/index.js | 75 +
shared/lib/wpcom-undocumented/lib/export.js | 74 +
.../wpcom-undocumented/lib/mailing-list.js | 117 +
shared/lib/wpcom-undocumented/lib/me.js | 288 +++
shared/lib/wpcom-undocumented/lib/site.js | 196 ++
.../wpcom-undocumented/lib/undocumented.js | 1760 +++++++++++++++
shared/my-sites/themes/README.md | 28 +
shared/my-sites/themes/controller.js | 60 +
.../my-sites/themes/current-theme/README.md | 10 +
.../my-sites/themes/current-theme/button.jsx | 45 +
.../my-sites/themes/current-theme/index.jsx | 73 +
.../my-sites/themes/current-theme/style.scss | 128 ++
shared/my-sites/themes/index.js | 43 +
.../jetpack-manage-disabled-message.jsx | 45 +
.../themes/jetpack-upgrade-message.jsx | 33 +
shared/my-sites/themes/main.jsx | 154 ++
shared/my-sites/themes/style.scss | 68 +
shared/my-sites/themes/thanks-modal.jsx | 152 ++
shared/my-sites/themes/theme-options.js | 124 ++
.../themes/themes-search-card/index.jsx | 147 ++
.../themes-search-card/select-dropdown.jsx | 56 +
.../themes/themes-search-card/style.scss | 31 +
shared/my-sites/themes/themes-selection.jsx | 119 ++
.../themes/themes-site-selector-modal.jsx | 76 +
webpack.config.js | 116 +
webpack.config.node.js | 80 +
2838 files changed, 245118 insertions(+)
create mode 100644 .dockerignore
create mode 100644 .editorconfig
create mode 100644 .esformatter
create mode 100644 .eslintignore
create mode 100644 .eslintrc
create mode 100644 .gitignore
create mode 100644 .jsfmtrc
create mode 100644 .npmrc
create mode 100644 .rtlcssrc
create mode 100644 CONTRIBUTING.md
create mode 100644 CREDITS.md
create mode 100644 Dockerfile
create mode 100644 LICENSE.md
create mode 100644 Makefile
create mode 100644 README.md
create mode 100644 Vagrantfile
create mode 100644 Vagrantfile-boot2docker
create mode 100644 assets/stylesheets/README.md
create mode 100644 assets/stylesheets/_components.scss
create mode 100644 assets/stylesheets/editor.scss
create mode 100644 assets/stylesheets/layout/_detail-page.scss
create mode 100644 assets/stylesheets/layout/_main.scss
create mode 100644 assets/stylesheets/layout/_masterbar.scss
create mode 100644 assets/stylesheets/layout/_overlay.scss
create mode 100644 assets/stylesheets/layout/_sidebar.scss
create mode 100644 assets/stylesheets/sections/_billing-history.scss
create mode 100644 assets/stylesheets/sections/_checkout.scss
create mode 100644 assets/stylesheets/sections/_devdocs.scss
create mode 100644 assets/stylesheets/sections/_domain-search.scss
create mode 100644 assets/stylesheets/sections/_keyboard-shortcuts.scss
create mode 100644 assets/stylesheets/sections/_manage.scss
create mode 100644 assets/stylesheets/sections/_menus.scss
create mode 100644 assets/stylesheets/sections/_notifications.scss
create mode 100644 assets/stylesheets/sections/_nux-welcome.scss
create mode 100644 assets/stylesheets/sections/_plugins.scss
create mode 100644 assets/stylesheets/sections/_post-relative-time-status.scss
create mode 100644 assets/stylesheets/sections/_posts-controls.scss
create mode 100644 assets/stylesheets/sections/_posts.scss
create mode 100644 assets/stylesheets/sections/_sharing.scss
create mode 100644 assets/stylesheets/sections/_site-settings.scss
create mode 100644 assets/stylesheets/sections/_sites.scss
create mode 100644 assets/stylesheets/sections/_stats.scss
create mode 100644 assets/stylesheets/sections/_translator.scss
create mode 100644 assets/stylesheets/sections/_updated-confirmation.scss
create mode 100644 assets/stylesheets/sections/_upgrades.scss
create mode 100644 assets/stylesheets/shared/_animation.scss
create mode 100644 assets/stylesheets/shared/_colors.scss
create mode 100644 assets/stylesheets/shared/_dropdowns.scss
create mode 100644 assets/stylesheets/shared/_extends.scss
create mode 100644 assets/stylesheets/shared/_forms.scss
create mode 100644 assets/stylesheets/shared/_functions.scss
create mode 100644 assets/stylesheets/shared/_infinite-scroll-end.scss
create mode 100644 assets/stylesheets/shared/_livechat.scss
create mode 100644 assets/stylesheets/shared/_reset.scss
create mode 100644 assets/stylesheets/shared/_toolbar-bulk.scss
create mode 100644 assets/stylesheets/shared/_typography.scss
create mode 100644 assets/stylesheets/shared/_utilities.scss
create mode 100644 assets/stylesheets/shared/_welcome.scss
create mode 100644 assets/stylesheets/shared/mixins/_breakpoints.scss
create mode 100644 assets/stylesheets/shared/mixins/_calc.scss
create mode 100644 assets/stylesheets/shared/mixins/_clear-fix.scss
create mode 100644 assets/stylesheets/shared/mixins/_dropdown-menu.scss
create mode 100644 assets/stylesheets/shared/mixins/_hide-content-accessibly.scss
create mode 100644 assets/stylesheets/shared/mixins/_long-content-fade.scss
create mode 100644 assets/stylesheets/shared/mixins/_mixins.scss
create mode 100644 assets/stylesheets/shared/mixins/_noticon.scss
create mode 100644 assets/stylesheets/shared/mixins/_placeholder.scss
create mode 100644 assets/stylesheets/shared/mixins/_stats-fade-text.scss
create mode 100644 assets/stylesheets/style.scss
create mode 120000 bin/bundler
create mode 120000 bin/generate-devdocs-index
create mode 120000 bin/get-i18n
create mode 120000 bin/i18nlint
create mode 120000 bin/list-assets
create mode 100755 bin/live-reload
create mode 100755 bin/pre-commit
create mode 100755 bin/pre-push
create mode 100755 bin/record-env
create mode 100755 bin/run-all-tests
create mode 100755 bin/run-tests
create mode 100755 bin/update-dependency
create mode 100644 client/README.md
create mode 100644 client/accept-invite/actions.js
create mode 100644 client/accept-invite/controller.js
create mode 100644 client/accept-invite/index.js
create mode 100644 client/accept-invite/invite-form-header/index.jsx
create mode 100644 client/accept-invite/invite-form-header/style.scss
create mode 100644 client/accept-invite/invite-header/index.jsx
create mode 100644 client/accept-invite/invite-header/mock-data.js
create mode 100644 client/accept-invite/invite-header/style.scss
create mode 100644 client/accept-invite/logged-in-accept/index.jsx
create mode 100644 client/accept-invite/logged-in-accept/style.scss
create mode 100644 client/accept-invite/logged-out-invite/index.jsx
create mode 100644 client/accept-invite/logged-out-invite/signup-form.jsx
create mode 100644 client/accept-invite/logged-out-invite/style.scss
create mode 100644 client/accept-invite/main.jsx
create mode 100644 client/accept-invite/style.scss
create mode 100644 client/analytics/Makefile
create mode 100644 client/analytics/README.md
create mode 100644 client/analytics/ad-tracking.js
create mode 100644 client/analytics/index.js
create mode 100644 client/analytics/super-props.js
create mode 100644 client/analytics/test/analytics-tests.js
create mode 100644 client/analytics/test/config.js
create mode 100644 client/analytics/test/load-script.js
create mode 100644 client/auth/Makefile
create mode 100644 client/auth/controller.js
create mode 100644 client/auth/index.js
create mode 100644 client/auth/login.jsx
create mode 100644 client/auth/style.scss
create mode 100644 client/auth/test/login.jsx
create mode 100644 client/boot/README.md
create mode 100644 client/boot/index.js
create mode 100644 client/components/README.md
create mode 100644 client/components/accordion/Makefile
create mode 100644 client/components/accordion/README.md
create mode 100644 client/components/accordion/docs/example.jsx
create mode 100644 client/components/accordion/index.jsx
create mode 100644 client/components/accordion/section.jsx
create mode 100644 client/components/accordion/style.scss
create mode 100644 client/components/accordion/test/index.jsx
create mode 100644 client/components/add-new-button/README.md
create mode 100644 client/components/add-new-button/docs/example.jsx
create mode 100644 client/components/add-new-button/index.jsx
create mode 100644 client/components/add-new-button/style.scss
create mode 100644 client/components/author-selector/README.md
create mode 100644 client/components/author-selector/index.jsx
create mode 100644 client/components/author-selector/style.scss
create mode 100644 client/components/bulk-select/README.md
create mode 100644 client/components/bulk-select/docs/example.jsx
create mode 100644 client/components/bulk-select/index.jsx
create mode 100644 client/components/bulk-select/style.scss
create mode 100644 client/components/button-group/README.md
create mode 100644 client/components/button-group/docs/example.jsx
create mode 100644 client/components/button-group/index.jsx
create mode 100644 client/components/button-group/style.scss
create mode 100644 client/components/button/README.md
create mode 100644 client/components/button/docs/example.jsx
create mode 100644 client/components/button/index.jsx
create mode 100644 client/components/button/style.scss
create mode 100644 client/components/chart/README.md
create mode 100644 client/components/chart/bar-container.jsx
create mode 100644 client/components/chart/bar.jsx
create mode 100644 client/components/chart/index.jsx
create mode 100644 client/components/chart/label.jsx
create mode 100644 client/components/chart/legend.jsx
create mode 100644 client/components/chart/style.scss
create mode 100644 client/components/chart/tooltip.jsx
create mode 100644 client/components/chart/x-axis.jsx
create mode 100644 client/components/comment-button/README.md
create mode 100644 client/components/comment-button/docs/example.jsx
create mode 100644 client/components/comment-button/index.jsx
create mode 100644 client/components/comment-button/style.scss
create mode 100644 client/components/count/Makefile
create mode 100644 client/components/count/README.md
create mode 100644 client/components/count/docs/example.jsx
create mode 100644 client/components/count/index.jsx
create mode 100644 client/components/count/style.scss
create mode 100644 client/components/count/test/index.jsx
create mode 100644 client/components/data/activating-theme/README.md
create mode 100644 client/components/data/activating-theme/index.js
create mode 100644 client/components/data/cart/README.md
create mode 100644 client/components/data/cart/index.jsx
create mode 100644 client/components/data/category-list-data/README.md
create mode 100644 client/components/data/category-list-data/index.jsx
create mode 100644 client/components/data/checkout/README.md
create mode 100644 client/components/data/checkout/index.jsx
create mode 100644 client/components/data/current-theme/README.md
create mode 100644 client/components/data/current-theme/index.js
create mode 100644 client/components/data/domain-management/README.md
create mode 100644 client/components/data/domain-management/dns/README.md
create mode 100644 client/components/data/domain-management/dns/index.jsx
create mode 100644 client/components/data/domain-management/email-forwarding/README.md
create mode 100644 client/components/data/domain-management/email-forwarding/index.jsx
create mode 100644 client/components/data/domain-management/email/README.md
create mode 100644 client/components/data/domain-management/email/index.jsx
create mode 100644 client/components/data/domain-management/index.jsx
create mode 100644 client/components/data/domain-management/nameservers/README.md
create mode 100644 client/components/data/domain-management/nameservers/index.jsx
create mode 100644 client/components/data/domain-management/primary-domain/README.md
create mode 100644 client/components/data/domain-management/primary-domain/index.jsx
create mode 100644 client/components/data/domain-management/site-redirect/README.md
create mode 100644 client/components/data/domain-management/site-redirect/index.jsx
create mode 100644 client/components/data/domain-management/transfer/README.md
create mode 100644 client/components/data/domain-management/transfer/index.jsx
create mode 100644 client/components/data/domain-management/whois/README.md
create mode 100644 client/components/data/domain-management/whois/index.jsx
create mode 100644 client/components/data/email-followers-data/README.md
create mode 100644 client/components/data/email-followers-data/index.jsx
create mode 100644 client/components/data/followers-data/README.md
create mode 100644 client/components/data/followers-data/index.jsx
create mode 100644 client/components/data/media-library-selected-data/README.md
create mode 100644 client/components/data/media-library-selected-data/index.jsx
create mode 100644 client/components/data/media-list-data/Makefile
create mode 100644 client/components/data/media-list-data/README.md
create mode 100644 client/components/data/media-list-data/index.jsx
create mode 100644 client/components/data/media-list-data/test/index.js
create mode 100644 client/components/data/media-list-data/test/specs/utils.js
create mode 100644 client/components/data/media-list-data/utils.js
create mode 100644 client/components/data/media-validation-data/README.md
create mode 100644 client/components/data/media-validation-data/index.jsx
create mode 100644 client/components/data/page-templates-data/README.md
create mode 100644 client/components/data/page-templates-data/index.jsx
create mode 100644 client/components/data/post-counts-data/README.md
create mode 100644 client/components/data/post-counts-data/index.jsx
create mode 100644 client/components/data/post-formats-data/README.md
create mode 100644 client/components/data/post-formats-data/index.jsx
create mode 100644 client/components/data/preferences-data/README.md
create mode 100644 client/components/data/preferences-data/index.jsx
create mode 100644 client/components/data/purchases/README.md
create mode 100644 client/components/data/purchases/edit-card-details/index.jsx
create mode 100644 client/components/data/purchases/index.jsx
create mode 100644 client/components/data/purchases/manage-purchase/index.jsx
create mode 100644 client/components/data/sharing-connections-data/Makefile
create mode 100644 client/components/data/sharing-connections-data/README.md
create mode 100644 client/components/data/sharing-connections-data/index.jsx
create mode 100644 client/components/data/sharing-connections-data/test/index.jsx
create mode 100644 client/components/data/tag-list-data/README.md
create mode 100644 client/components/data/tag-list-data/index.jsx
create mode 100644 client/components/data/viewers-data/README.md
create mode 100644 client/components/data/viewers-data/index.jsx
create mode 100644 client/components/date-picker/README.md
create mode 100644 client/components/date-picker/day.jsx
create mode 100644 client/components/date-picker/docs/example.jsx
create mode 100644 client/components/date-picker/index.jsx
create mode 100644 client/components/date-picker/style.scss
create mode 100644 client/components/dialog/README.md
create mode 100644 client/components/dialog/dialog-base.jsx
create mode 100644 client/components/dialog/index.jsx
create mode 100644 client/components/dialog/style.scss
create mode 100644 client/components/domains/README.md
create mode 100644 client/components/domains/domain-mapping-suggestion/index.jsx
create mode 100644 client/components/domains/domain-mapping-suggestion/style.scss
create mode 100644 client/components/domains/domain-product-price/index.jsx
create mode 100644 client/components/domains/domain-product-price/style.scss
create mode 100644 client/components/domains/domain-registration-suggestion/index.jsx
create mode 100644 client/components/domains/domain-search-results/index.jsx
create mode 100644 client/components/domains/domain-search-results/style.scss
create mode 100644 client/components/domains/domain-suggestion/index.jsx
create mode 100644 client/components/domains/domain-suggestion/style.scss
create mode 100644 client/components/domains/example-domain-suggestions/index.jsx
create mode 100644 client/components/domains/example-domain-suggestions/style.scss
create mode 100644 client/components/domains/map-domain-step/index.jsx
create mode 100644 client/components/domains/map-domain-step/style.scss
create mode 100644 client/components/domains/map-domain/index.jsx
create mode 100644 client/components/domains/register-domain-step/index.jsx
create mode 100644 client/components/domains/register-domain-step/style.scss
create mode 100644 client/components/drop-zone/Makefile
create mode 100644 client/components/drop-zone/README.md
create mode 100644 client/components/drop-zone/docs/example.jsx
create mode 100644 client/components/drop-zone/index.jsx
create mode 100644 client/components/drop-zone/style.scss
create mode 100644 client/components/drop-zone/test/index.jsx
create mode 100644 client/components/email-verification/README.md
create mode 100644 client/components/email-verification/email-verification-notice.jsx
create mode 100644 client/components/email-verification/index.js
create mode 100644 client/components/emojify/README.md
create mode 100644 client/components/emojify/index.jsx
create mode 100644 client/components/emojify/style.scss
create mode 100644 client/components/external-link/README.md
create mode 100644 client/components/external-link/docs/example.jsx
create mode 100644 client/components/external-link/index.jsx
create mode 100644 client/components/external-link/style.scss
create mode 100644 client/components/flag/README.md
create mode 100644 client/components/flag/docs/example.jsx
create mode 100644 client/components/flag/index.jsx
create mode 100644 client/components/flag/style.scss
create mode 100644 client/components/foldable-card/README.md
create mode 100644 client/components/foldable-card/docs/example.jsx
create mode 100644 client/components/foldable-card/index.jsx
create mode 100644 client/components/foldable-card/style.scss
create mode 100644 client/components/follow-button/README.md
create mode 100644 client/components/follow-button/_style.scss
create mode 100644 client/components/follow-button/button.jsx
create mode 100644 client/components/follow-button/docs/example.jsx
create mode 100644 client/components/follow-button/index.jsx
create mode 100644 client/components/follow-button/style.scss
create mode 100644 client/components/forms/README.md
create mode 100644 client/components/forms/clipboard-button/README.md
create mode 100644 client/components/forms/clipboard-button/docs/example.jsx
create mode 100644 client/components/forms/clipboard-button/index.jsx
create mode 100644 client/components/forms/counted-textarea/Makefile
create mode 100644 client/components/forms/counted-textarea/README.md
create mode 100644 client/components/forms/counted-textarea/docs/example.jsx
create mode 100644 client/components/forms/counted-textarea/index.jsx
create mode 100644 client/components/forms/counted-textarea/style.scss
create mode 100644 client/components/forms/counted-textarea/test/index.jsx
create mode 100644 client/components/forms/docs/example.jsx
create mode 100644 client/components/forms/form-button/index.jsx
create mode 100644 client/components/forms/form-button/style.scss
create mode 100644 client/components/forms/form-buttons-bar/index.jsx
create mode 100644 client/components/forms/form-buttons-bar/style.scss
create mode 100644 client/components/forms/form-checkbox/index.jsx
create mode 100644 client/components/forms/form-country-select/index.jsx
create mode 100644 client/components/forms/form-country-select/style.scss
create mode 100644 client/components/forms/form-fieldset/index.jsx
create mode 100644 client/components/forms/form-fieldset/style.scss
create mode 100644 client/components/forms/form-input-validation/index.jsx
create mode 100644 client/components/forms/form-input-validation/style.scss
create mode 100644 client/components/forms/form-label/index.jsx
create mode 100644 client/components/forms/form-label/style.scss
create mode 100644 client/components/forms/form-legend/index.jsx
create mode 100644 client/components/forms/form-legend/style.scss
create mode 100644 client/components/forms/form-password-input/index.jsx
create mode 100644 client/components/forms/form-password-input/style.scss
create mode 100644 client/components/forms/form-phone-input/Makefile
create mode 100644 client/components/forms/form-phone-input/index.jsx
create mode 100644 client/components/forms/form-phone-input/test/index.jsx
create mode 100644 client/components/forms/form-phone-input/test/mock-countries-list-empty.js
create mode 100644 client/components/forms/form-phone-input/test/mock-countries-list.js
create mode 100644 client/components/forms/form-radio/index.jsx
create mode 100644 client/components/forms/form-range/index.jsx
create mode 100644 client/components/forms/form-range/style.scss
create mode 100644 client/components/forms/form-section-heading/index.jsx
create mode 100644 client/components/forms/form-section-heading/style.scss
create mode 100644 client/components/forms/form-select/index.jsx
create mode 100644 client/components/forms/form-select/style.scss
create mode 100644 client/components/forms/form-setting-explanation/index.jsx
create mode 100644 client/components/forms/form-setting-explanation/style.scss
create mode 100644 client/components/forms/form-tel-input/index.jsx
create mode 100644 client/components/forms/form-tel-input/style.scss
create mode 100644 client/components/forms/form-text-input-with-affixes/README.md
create mode 100644 client/components/forms/form-text-input-with-affixes/index.jsx
create mode 100644 client/components/forms/form-text-input-with-affixes/style.scss
create mode 100644 client/components/forms/form-text-input/index.jsx
create mode 100644 client/components/forms/form-text-input/style.scss
create mode 100644 client/components/forms/form-textarea/index.jsx
create mode 100644 client/components/forms/form-toggle/Makefile
create mode 100644 client/components/forms/form-toggle/README.md
create mode 100644 client/components/forms/form-toggle/compact.jsx
create mode 100644 client/components/forms/form-toggle/index.jsx
create mode 100644 client/components/forms/form-toggle/style.scss
create mode 100644 client/components/forms/form-toggle/test/index.jsx
create mode 100644 client/components/forms/language-selector.jsx
create mode 100644 client/components/forms/multi-checkbox/Makefile
create mode 100644 client/components/forms/multi-checkbox/README.md
create mode 100644 client/components/forms/multi-checkbox/index.jsx
create mode 100644 client/components/forms/multi-checkbox/test/index.jsx
create mode 100644 client/components/forms/range/Makefile
create mode 100644 client/components/forms/range/README.md
create mode 100644 client/components/forms/range/docs/example.jsx
create mode 100644 client/components/forms/range/index.jsx
create mode 100644 client/components/forms/range/style.scss
create mode 100644 client/components/forms/range/test/index.jsx
create mode 100644 client/components/forms/select-opt-groups.jsx
create mode 100644 client/components/forms/sortable-list/README.md
create mode 100644 client/components/forms/sortable-list/index.jsx
create mode 100644 client/components/forms/sortable-list/index.scss
create mode 100644 client/components/forms/us-state-selector.jsx
create mode 100644 client/components/gallery-shortcode/README.md
create mode 100644 client/components/gallery-shortcode/index.jsx
create mode 100644 client/components/gauge/README.md
create mode 100644 client/components/gauge/docs/example.jsx
create mode 100644 client/components/gauge/index.jsx
create mode 100644 client/components/gauge/style.scss
create mode 100644 client/components/gravatar/Makefile
create mode 100644 client/components/gravatar/README.md
create mode 100644 client/components/gravatar/index.jsx
create mode 100644 client/components/gravatar/style.scss
create mode 100644 client/components/gravatar/test/index.jsx
create mode 100644 client/components/header-cake/README.md
create mode 100644 client/components/header-cake/docs/example.jsx
create mode 100644 client/components/header-cake/index.jsx
create mode 100644 client/components/header-cake/style.scss
create mode 100644 client/components/image-preloader/README.md
create mode 100644 client/components/image-preloader/index.jsx
create mode 100644 client/components/infinite-list/Makefile
create mode 100644 client/components/infinite-list/README.md
create mode 100644 client/components/infinite-list/index.jsx
create mode 100644 client/components/infinite-list/scroll-helper.js
create mode 100644 client/components/infinite-list/style.scss
create mode 100644 client/components/infinite-list/test/scroll-helper.js
create mode 100644 client/components/info-popover/README.md
create mode 100644 client/components/info-popover/docs/example.jsx
create mode 100644 client/components/info-popover/index.jsx
create mode 100644 client/components/info-popover/style.scss
create mode 100644 client/components/input-chrono/README.md
create mode 100644 client/components/input-chrono/docs/example.jsx
create mode 100644 client/components/input-chrono/index.jsx
create mode 100644 client/components/input-chrono/style.scss
create mode 100644 client/components/like-button/README.md
create mode 100644 client/components/like-button/_style.scss
create mode 100644 client/components/like-button/button.jsx
create mode 100644 client/components/like-button/docs/example.jsx
create mode 100644 client/components/like-button/icons.jsx
create mode 100644 client/components/like-button/index.jsx
create mode 100644 client/components/main/README.md
create mode 100644 client/components/main/index.jsx
create mode 100644 client/components/main/style.scss
create mode 100644 client/components/mobile-back-to-sidebar/index.jsx
create mode 100644 client/components/mobile-back-to-sidebar/style.scss
create mode 100644 client/components/notices/docs/example.jsx
create mode 100644 client/components/olark-chatbox/README.md
create mode 100644 client/components/olark-chatbox/index.jsx
create mode 100644 client/components/olark-chatbox/style.scss
create mode 100644 client/components/overlay/README.md
create mode 100644 client/components/overlay/overlay.jsx
create mode 100644 client/components/overlay/package.json
create mode 100644 client/components/overlay/toolbar.jsx
create mode 100644 client/components/payment-logo/index.jsx
create mode 100644 client/components/payment-logo/style.scss
create mode 100644 client/components/plans/plan-actions/index.jsx
create mode 100644 client/components/plans/plan-actions/style.scss
create mode 100644 client/components/plans/plan-discount-message/index.jsx
create mode 100644 client/components/plans/plan-discount-message/style.scss
create mode 100644 client/components/plans/plan-feature-cell/index.jsx
create mode 100644 client/components/plans/plan-feature-cell/style.scss
create mode 100644 client/components/plans/plan-features/index.jsx
create mode 100644 client/components/plans/plan-features/style.scss
create mode 100644 client/components/plans/plan-header/index.jsx
create mode 100644 client/components/plans/plan-header/style.scss
create mode 100644 client/components/plans/plan-list/index.jsx
create mode 100644 client/components/plans/plan-list/style.scss
create mode 100644 client/components/plans/plan-nudge/index.jsx
create mode 100644 client/components/plans/plan-nudge/preview.jsx
create mode 100644 client/components/plans/plan-nudge/style.scss
create mode 100644 client/components/plans/plan-price/index.jsx
create mode 100644 client/components/plans/plan-price/style.scss
create mode 100644 client/components/plans/plan/index.jsx
create mode 100644 client/components/plans/plan/style.scss
create mode 100644 client/components/plans/plans-compare/index.jsx
create mode 100644 client/components/plans/plans-compare/style.scss
create mode 100644 client/components/plans/site-specific-plan-details-mixin.js
create mode 100644 client/components/popover/README.md
create mode 100644 client/components/popover/docs/example.jsx
create mode 100644 client/components/popover/index.jsx
create mode 100644 client/components/popover/menu-item.jsx
create mode 100644 client/components/popover/menu.jsx
create mode 100644 client/components/popover/style.scss
create mode 100644 client/components/post-excerpt/index.jsx
create mode 100644 client/components/post-excerpt/style.scss
create mode 100644 client/components/post-list-fetcher/index.jsx
create mode 100644 client/components/post-schedule/README.md
create mode 100644 client/components/post-schedule/clock.jsx
create mode 100644 client/components/post-schedule/docs/example.jsx
create mode 100644 client/components/post-schedule/header-controls.jsx
create mode 100644 client/components/post-schedule/header.jsx
create mode 100644 client/components/post-schedule/index.jsx
create mode 100644 client/components/post-schedule/style.scss
create mode 100644 client/components/post-schedule/utils.js
create mode 100644 client/components/progress-bar/README.md
create mode 100644 client/components/progress-bar/docs/example.jsx
create mode 100644 client/components/progress-bar/index.jsx
create mode 100644 client/components/progress-bar/style.scss
create mode 100644 client/components/progress-indicator/README.md
create mode 100644 client/components/progress-indicator/index.jsx
create mode 100644 client/components/progress-indicator/style.scss
create mode 100644 client/components/pulsing-dot/index.jsx
create mode 100644 client/components/pulsing-dot/style.scss
create mode 100644 client/components/rating/README.md
create mode 100644 client/components/rating/docs/example.jsx
create mode 100644 client/components/rating/index.jsx
create mode 100644 client/components/rating/style.scss
create mode 100644 client/components/resizable-iframe/README.md
create mode 100644 client/components/resizable-iframe/index.jsx
create mode 100644 client/components/root-child/Makefile
create mode 100644 client/components/root-child/README.md
create mode 100644 client/components/root-child/index.jsx
create mode 100644 client/components/root-child/test/index.jsx
create mode 100644 client/components/search-card/README.md
create mode 100644 client/components/search-card/index.jsx
create mode 100644 client/components/search-card/style.scss
create mode 100644 client/components/search/Makefile
create mode 100644 client/components/search/README.md
create mode 100644 client/components/search/docs/example.jsx
create mode 100644 client/components/search/index.jsx
create mode 100644 client/components/search/style.scss
create mode 100644 client/components/search/test/index.jsx
create mode 100644 client/components/section-header/README.md
create mode 100644 client/components/section-header/button.jsx
create mode 100644 client/components/section-header/docs/example.jsx
create mode 100644 client/components/section-header/index.jsx
create mode 100644 client/components/section-header/style.scss
create mode 100644 client/components/section-nav/Makefile
create mode 100644 client/components/section-nav/README.md
create mode 100644 client/components/section-nav/docs/example.jsx
create mode 100644 client/components/section-nav/index.jsx
create mode 100644 client/components/section-nav/item.jsx
create mode 100644 client/components/section-nav/segmented.jsx
create mode 100644 client/components/section-nav/style.scss
create mode 100644 client/components/section-nav/tabs.jsx
create mode 100644 client/components/section-nav/test/index.jsx
create mode 100644 client/components/segmented-control/README.md
create mode 100644 client/components/segmented-control/docs/example.jsx
create mode 100644 client/components/segmented-control/index.jsx
create mode 100644 client/components/segmented-control/item.jsx
create mode 100644 client/components/segmented-control/style.scss
create mode 100644 client/components/select-dropdown/README.md
create mode 100644 client/components/select-dropdown/docs/example.jsx
create mode 100644 client/components/select-dropdown/index.jsx
create mode 100644 client/components/select-dropdown/item.jsx
create mode 100644 client/components/select-dropdown/separator.jsx
create mode 100644 client/components/select-dropdown/style.scss
create mode 100644 client/components/select/docs/example.jsx
create mode 100644 client/components/shortcode/README.md
create mode 100644 client/components/shortcode/data.jsx
create mode 100644 client/components/shortcode/frame.jsx
create mode 100644 client/components/shortcode/index.jsx
create mode 100644 client/components/sidebar-navigation/README.md
create mode 100644 client/components/sidebar-navigation/index.jsx
create mode 100644 client/components/sidebar-navigation/style.scss
create mode 100644 client/components/signup-form/README.md
create mode 100644 client/components/signup-form/index.jsx
create mode 100644 client/components/signup-form/style.scss
create mode 100644 client/components/single-child-css-transition-group/README.md
create mode 100644 client/components/single-child-css-transition-group/child.js
create mode 100644 client/components/single-child-css-transition-group/index.js
create mode 100644 client/components/site-icon/README.md
create mode 100644 client/components/site-icon/package.json
create mode 100644 client/components/site-icon/site-icon.jsx
create mode 100644 client/components/site-icon/style.scss
create mode 100644 client/components/site-selector-modal/README.md
create mode 100644 client/components/site-selector-modal/index.jsx
create mode 100644 client/components/site-selector-modal/style.scss
create mode 100644 client/components/site-selector/README.md
create mode 100644 client/components/site-selector/index.jsx
create mode 100644 client/components/site-selector/style.scss
create mode 100644 client/components/site-stats-sticky-link/README.md
create mode 100644 client/components/site-stats-sticky-link/index.jsx
create mode 100644 client/components/site-users-fetcher/index.jsx
create mode 100644 client/components/site-users-fetcher/readme.md
create mode 100644 client/components/sites-popover/README.md
create mode 100644 client/components/sites-popover/index.jsx
create mode 100644 client/components/sites-popover/style.scss
create mode 100644 client/components/spinner/README.md
create mode 100644 client/components/spinner/docs/example.jsx
create mode 100644 client/components/spinner/index.jsx
create mode 100644 client/components/spinner/style.scss
create mode 100644 client/components/stat-update-indicator/README.md
create mode 100644 client/components/stat-update-indicator/index.jsx
create mode 100644 client/components/stat-update-indicator/style.scss
create mode 100644 client/components/sticky-panel/index.jsx
create mode 100644 client/components/sticky-panel/style.scss
create mode 100644 client/components/timezone-dropdown/README.md
create mode 100644 client/components/timezone-dropdown/docs/example.jsx
create mode 100644 client/components/timezone-dropdown/index.jsx
create mode 100644 client/components/timezone-dropdown/style.scss
create mode 100644 client/components/tinymce/README.md
create mode 100644 client/components/tinymce/i18n.js
create mode 100644 client/components/tinymce/iframe.scss
create mode 100644 client/components/tinymce/index.jsx
create mode 100644 client/components/tinymce/plugins/advanced/plugin.js
create mode 100644 client/components/tinymce/plugins/advanced/style.scss
create mode 100644 client/components/tinymce/plugins/calypso-alert/alert.jsx
create mode 100644 client/components/tinymce/plugins/calypso-alert/plugin.jsx
create mode 100644 client/components/tinymce/plugins/calypso-alert/style.scss
create mode 100644 client/components/tinymce/plugins/editor-button-analytics/plugin.js
create mode 100644 client/components/tinymce/plugins/media/README.md
create mode 100644 client/components/tinymce/plugins/media/drop-zone.jsx
create mode 100644 client/components/tinymce/plugins/media/plugin.jsx
create mode 100644 client/components/tinymce/plugins/media/restrict-size/Makefile
create mode 100644 client/components/tinymce/plugins/media/restrict-size/index.js
create mode 100644 client/components/tinymce/plugins/media/restrict-size/test/index.js
create mode 100644 client/components/tinymce/plugins/tabindex/plugin.js
create mode 100644 client/components/tinymce/plugins/touch-scroll-toolbar/plugin.js
create mode 100644 client/components/tinymce/plugins/wpcom-autoresize/plugin.js
create mode 100644 client/components/tinymce/plugins/wpcom-charmap/charmap.jsx
create mode 100644 client/components/tinymce/plugins/wpcom-charmap/plugin.js
create mode 100644 client/components/tinymce/plugins/wpcom-charmap/style.scss
create mode 100644 client/components/tinymce/plugins/wpcom-help/help-modal.jsx
create mode 100644 client/components/tinymce/plugins/wpcom-help/plugin.js
create mode 100644 client/components/tinymce/plugins/wpcom-help/style.scss
create mode 100644 client/components/tinymce/plugins/wpcom-view/gallery-view.jsx
create mode 100644 client/components/tinymce/plugins/wpcom-view/plugin.js
create mode 100644 client/components/tinymce/plugins/wpcom-view/views.js
create mode 100644 client/components/tinymce/plugins/wpcom-view/views/embed/index.js
create mode 100644 client/components/tinymce/plugins/wpcom-view/views/embed/view.jsx
create mode 100644 client/components/tinymce/plugins/wpcom/plugin.js
create mode 100644 client/components/tinymce/plugins/wpeditimage/plugin.js
create mode 100644 client/components/tinymce/plugins/wplink/dialog.jsx
create mode 100644 client/components/tinymce/plugins/wplink/plugin.js
create mode 100644 client/components/tinymce/plugins/wplink/style.scss
create mode 100644 client/components/tinymce/plugins/wptextpattern/plugin.js
create mode 100644 client/components/tinymce/style.scss
create mode 100644 client/components/token-field/Makefile
create mode 100644 client/components/token-field/README.md
create mode 100644 client/components/token-field/docs/example.jsx
create mode 100644 client/components/token-field/index.jsx
create mode 100644 client/components/token-field/style.scss
create mode 100644 client/components/token-field/suggestions-list.jsx
create mode 100644 client/components/token-field/test/fixtures.js
create mode 100644 client/components/token-field/test/index.jsx
create mode 100644 client/components/token-field/test/token-field-wrapper.jsx
create mode 100644 client/components/token-field/token-input.jsx
create mode 100644 client/components/token-field/token.jsx
create mode 100644 client/components/tooltip/index.jsx
create mode 100644 client/components/tooltip/style.scss
create mode 100644 client/components/track-input-changes/Makefile
create mode 100644 client/components/track-input-changes/README.md
create mode 100644 client/components/track-input-changes/index.jsx
create mode 100644 client/components/track-input-changes/test/index.jsx
create mode 100644 client/components/typography/README.md
create mode 100644 client/components/typography/docs/example.jsx
create mode 100644 client/components/upgrades/credit-card-form/README.md
create mode 100644 client/components/upgrades/credit-card-form/index.jsx
create mode 100644 client/components/upgrades/credit-card-form/style.scss
create mode 100644 client/components/upgrades/credit-card-number-input/README.md
create mode 100644 client/components/upgrades/credit-card-number-input/index.jsx
create mode 100644 client/components/upgrades/credit-card-number-input/style.scss
create mode 100644 client/components/upgrades/google-apps/README.md
create mode 100644 client/components/upgrades/google-apps/dialog/index.jsx
create mode 100644 client/components/upgrades/google-apps/dialog/product-details.jsx
create mode 100644 client/components/upgrades/google-apps/dialog/users.jsx
create mode 100644 client/components/upgrades/google-apps/index.jsx
create mode 100644 client/components/user/index.jsx
create mode 100644 client/components/user/style.scss
create mode 100644 client/components/version/README.md
create mode 100644 client/components/version/docs/example.jsx
create mode 100644 client/components/version/index.jsx
create mode 100644 client/components/version/style.scss
create mode 100644 client/components/vertical-nav/README.md
create mode 100644 client/components/vertical-nav/index.jsx
create mode 100644 client/components/vertical-nav/item/index.jsx
create mode 100644 client/components/vertical-nav/item/style.scss
create mode 100644 client/components/vertical-nav/style.scss
create mode 100644 client/components/web-preview/README.md
create mode 100644 client/components/web-preview/index.jsx
create mode 100644 client/components/web-preview/style.scss
create mode 100644 client/components/web-preview/toolbar.jsx
create mode 100644 client/components/wordpress-logo/index.jsx
create mode 100644 client/config/.gitignore
create mode 100644 client/config/README.md
create mode 100755 client/config/regenerate.js
create mode 100644 client/devdocs/Makefile
create mode 100644 client/devdocs/README.md
create mode 100644 client/devdocs/controller.js
create mode 100644 client/devdocs/design/index.jsx
create mode 100644 client/devdocs/design/style.scss
create mode 100644 client/devdocs/doc.jsx
create mode 100644 client/devdocs/form-state-examples/index.jsx
create mode 100644 client/devdocs/index.js
create mode 100644 client/devdocs/main.jsx
create mode 100644 client/devdocs/service.js
create mode 100644 client/devdocs/sidebar.jsx
create mode 100644 client/devdocs/test/doc-test.jsx
create mode 100644 client/layout/README.md
create mode 100644 client/layout/community-translator/README.md
create mode 100644 client/layout/community-translator/invitation-utils.js
create mode 100644 client/layout/community-translator/invitation.jsx
create mode 100644 client/layout/community-translator/launcher.jsx
create mode 100644 client/layout/community-translator/style.scss
create mode 100644 client/layout/error.jsx
create mode 100644 client/layout/index.jsx
create mode 100644 client/layout/logged-out-oauth.jsx
create mode 100644 client/layout/logged-out.jsx
create mode 100644 client/layout/masterbar-logged-out-menu.jsx
create mode 100644 client/layout/masterbar-new-post.jsx
create mode 100644 client/layout/masterbar-new-post.scss
create mode 100644 client/layout/masterbar-sections-menu.jsx
create mode 100644 client/layout/masterbar.jsx
create mode 100644 client/layout/package.json
create mode 100644 client/lib/abtest/README.md
create mode 100644 client/lib/abtest/active-tests.js
create mode 100644 client/lib/abtest/index.js
create mode 100644 client/lib/accept/Makefile
create mode 100644 client/lib/accept/README.md
create mode 100644 client/lib/accept/dialog.jsx
create mode 100644 client/lib/accept/index.js
create mode 100644 client/lib/accept/style.scss
create mode 100644 client/lib/accept/test/index.js
create mode 100644 client/lib/accessible-focus/README.md
create mode 100644 client/lib/accessible-focus/index.js
create mode 100644 client/lib/account-password-data/index.js
create mode 100644 client/lib/ads/Makefile
create mode 100644 client/lib/ads/README.md
create mode 100644 client/lib/ads/actions.js
create mode 100644 client/lib/ads/earnings-store.js
create mode 100644 client/lib/ads/settings-store.js
create mode 100644 client/lib/ads/test/lib/mock-actions.js
create mode 100644 client/lib/ads/test/lib/mock-earnings.js
create mode 100644 client/lib/ads/test/lib/mock-settings.js
create mode 100644 client/lib/ads/test/lib/mock-site.js
create mode 100644 client/lib/ads/test/test-store.js
create mode 100644 client/lib/ads/tos-store.js
create mode 100644 client/lib/ads/utils.js
create mode 100644 client/lib/application-passwords-data/README.md
create mode 100644 client/lib/application-passwords-data/index.js
create mode 100644 client/lib/billing-history-data/README.md
create mode 100644 client/lib/billing-history-data/index.js
create mode 100644 client/lib/cart-values/Makefile
create mode 100644 client/lib/cart-values/cart-items.js
create mode 100644 client/lib/cart-values/index.js
create mode 100644 client/lib/cart-values/schema.json
create mode 100644 client/lib/cart-values/test/abtest.js
create mode 100644 client/lib/cart-values/test/lib/user-settings.js
create mode 100644 client/lib/cart-values/test/lib/user.js
create mode 100644 client/lib/cart-values/test/test.js
create mode 100644 client/lib/cart/store/Makefile
create mode 100644 client/lib/cart/store/cart-analytics.js
create mode 100644 client/lib/cart/store/cart-synchronizer.js
create mode 100644 client/lib/cart/store/index.js
create mode 100644 client/lib/cart/store/test/abtest.js
create mode 100644 client/lib/cart/store/test/cart-synchronizer-test.js
create mode 100644 client/lib/cart/store/test/fake-wpcom.js
create mode 100644 client/lib/cart/store/test/lib/user-settings.js
create mode 100644 client/lib/cart/store/test/lib/user.js
create mode 100644 client/lib/comment-like-store/Makefile
create mode 100644 client/lib/comment-like-store/actions.js
create mode 100644 client/lib/comment-like-store/comment-like-store.js
create mode 100644 client/lib/comment-like-store/constants.js
create mode 100644 client/lib/comment-like-store/test/comment-like-store-test.js
create mode 100644 client/lib/comment-like-store/utils.js
create mode 100644 client/lib/comment-store/Makefile
create mode 100644 client/lib/comment-store/actions.js
create mode 100644 client/lib/comment-store/comment-store.js
create mode 100644 client/lib/comment-store/constants.js
create mode 100644 client/lib/comment-store/test/comment-store-test.js
create mode 100644 client/lib/comment-store/test/lib/wp.js
create mode 100644 client/lib/comment-store/utils.js
create mode 100644 client/lib/connected-applications-data/README.md
create mode 100644 client/lib/connected-applications-data/index.js
create mode 100644 client/lib/connections-list/README.md
create mode 100644 client/lib/connections-list/index.js
create mode 100644 client/lib/connections-list/list.js
create mode 100644 client/lib/countries-list/README.md
create mode 100644 client/lib/countries-list/index.js
create mode 100644 client/lib/credit-card-details/Makefile
create mode 100644 client/lib/credit-card-details/README.md
create mode 100644 client/lib/credit-card-details/index.js
create mode 100644 client/lib/credit-card-details/masking.js
create mode 100644 client/lib/credit-card-details/test/test.js
create mode 100644 client/lib/credit-card-details/validation.js
create mode 100644 client/lib/customize/muse.js
create mode 100644 client/lib/data-poller/README.md
create mode 100644 client/lib/data-poller/index.js
create mode 100644 client/lib/data-poller/poller.js
create mode 100644 client/lib/desktop/README.md
create mode 100644 client/lib/desktop/index.js
create mode 100644 client/lib/desktop/page-notifier.js
create mode 100644 client/lib/detect-history-navigation/README.md
create mode 100644 client/lib/detect-history-navigation/index.js
create mode 100644 client/lib/deterministic-stringify/Makefile
create mode 100644 client/lib/deterministic-stringify/README.md
create mode 100644 client/lib/deterministic-stringify/index.js
create mode 100644 client/lib/deterministic-stringify/test/test.js
create mode 100644 client/lib/devices/README.md
create mode 100644 client/lib/devices/index.js
create mode 100644 client/lib/domains/Makefile
create mode 100644 client/lib/domains/README.md
create mode 100644 client/lib/domains/assembler.js
create mode 100644 client/lib/domains/constants.js
create mode 100644 client/lib/domains/dns/Makefile
create mode 100644 client/lib/domains/dns/index.js
create mode 100644 client/lib/domains/dns/reducer.js
create mode 100644 client/lib/domains/dns/store.js
create mode 100644 client/lib/domains/dns/test/index.js
create mode 100644 client/lib/domains/email-forwarding/Makefile
create mode 100644 client/lib/domains/email-forwarding/reducer.js
create mode 100644 client/lib/domains/email-forwarding/store.js
create mode 100644 client/lib/domains/email-forwarding/test/store-test.js
create mode 100644 client/lib/domains/google-apps-users/assembler.js
create mode 100644 client/lib/domains/google-apps-users/index.js
create mode 100644 client/lib/domains/google-apps-users/reducer.js
create mode 100644 client/lib/domains/google-apps-users/store.js
create mode 100644 client/lib/domains/index.js
create mode 100644 client/lib/domains/nameservers/Makefile
create mode 100644 client/lib/domains/nameservers/index.js
create mode 100644 client/lib/domains/nameservers/reducer.js
create mode 100644 client/lib/domains/nameservers/store.js
create mode 100644 client/lib/domains/nameservers/test/store-test.js
create mode 100644 client/lib/domains/reducer.js
create mode 100644 client/lib/domains/site-redirect/README.md
create mode 100644 client/lib/domains/site-redirect/reducer.js
create mode 100644 client/lib/domains/site-redirect/store.js
create mode 100644 client/lib/domains/store.js
create mode 100644 client/lib/domains/test/assembler-test.js
create mode 100644 client/lib/domains/wapi-domain-info/assembler.js
create mode 100644 client/lib/domains/wapi-domain-info/reducer.js
create mode 100644 client/lib/domains/wapi-domain-info/store.js
create mode 100644 client/lib/domains/whois/Makefile
create mode 100644 client/lib/domains/whois/README.md
create mode 100644 client/lib/domains/whois/assembler.js
create mode 100644 client/lib/domains/whois/reducer.js
create mode 100644 client/lib/domains/whois/store.js
create mode 100644 client/lib/domains/whois/test/assembler-test.js
create mode 100644 client/lib/domains/whois/test/store-test.js
create mode 100644 client/lib/dss/README.md
create mode 100644 client/lib/dss/actions.js
create mode 100644 client/lib/dss/constants.js
create mode 100644 client/lib/dss/image-store.js
create mode 100644 client/lib/dss/preview-store.js
create mode 100644 client/lib/email-followers/Makefile
create mode 100644 client/lib/email-followers/README.md
create mode 100644 client/lib/email-followers/actions.js
create mode 100644 client/lib/email-followers/store.js
create mode 100644 client/lib/email-followers/test/lib/mock-actions.js
create mode 100644 client/lib/email-followers/test/lib/mock-email-followers.js
create mode 100644 client/lib/email-followers/test/lib/mock-more-email-followers.js
create mode 100644 client/lib/email-followers/test/lib/mock-site.js
create mode 100644 client/lib/email-followers/test/test-store.js
create mode 100644 client/lib/embeds/README.md
create mode 100644 client/lib/embeds/actions.js
create mode 100644 client/lib/embeds/list-store.js
create mode 100644 client/lib/embeds/store.js
create mode 100644 client/lib/features-list/Makefile
create mode 100644 client/lib/features-list/README.md
create mode 100644 client/lib/features-list/index.js
create mode 100644 client/lib/features-list/test/data.js
create mode 100644 client/lib/features-list/test/lib/wp.js
create mode 100644 client/lib/features-list/test/test.js
create mode 100644 client/lib/feed-post-store/Makefile
create mode 100644 client/lib/feed-post-store/README.md
create mode 100644 client/lib/feed-post-store/actions.js
create mode 100644 client/lib/feed-post-store/constants.js
create mode 100644 client/lib/feed-post-store/display-types.js
create mode 100644 client/lib/feed-post-store/index.js
create mode 100644 client/lib/feed-post-store/normalization-rules.js
create mode 100644 client/lib/feed-post-store/post-batch-fetcher.js
create mode 100644 client/lib/feed-post-store/test/feed-post-store-test.js
create mode 100644 client/lib/feed-post-store/test/lib/post-normalizer.js
create mode 100644 client/lib/feed-post-store/test/lib/wp.js
create mode 100644 client/lib/feed-store/Makefile
create mode 100644 client/lib/feed-store/actions.js
create mode 100644 client/lib/feed-store/constants.js
create mode 100644 client/lib/feed-store/index.js
create mode 100644 client/lib/feed-store/test/feed-store-tests.js
create mode 100644 client/lib/feed-store/test/lib/formatting.js
create mode 100644 client/lib/feed-stream-store/Makefile
create mode 100644 client/lib/feed-stream-store/actions.js
create mode 100644 client/lib/feed-stream-store/constants.js
create mode 100644 client/lib/feed-stream-store/feed-stream-cache.js
create mode 100644 client/lib/feed-stream-store/feed-stream.js
create mode 100644 client/lib/feed-stream-store/index.js
create mode 100644 client/lib/feed-stream-store/test/lib/post-normalizer.js
create mode 100644 client/lib/feed-stream-store/test/lib/wp.js
create mode 100644 client/lib/feed-stream-store/test/post-list-store-tests.js
create mode 100644 client/lib/follow-list/Makefile
create mode 100644 client/lib/follow-list/README.md
create mode 100644 client/lib/follow-list/index.js
create mode 100644 client/lib/follow-list/site.js
create mode 100644 client/lib/follow-list/test/lib/wp.js
create mode 100644 client/lib/follow-list/test/test.js
create mode 100644 client/lib/followers/Makefile
create mode 100644 client/lib/followers/README.md
create mode 100644 client/lib/followers/actions.js
create mode 100644 client/lib/followers/store.js
create mode 100644 client/lib/followers/test/lib/mock-actions.js
create mode 100644 client/lib/followers/test/lib/mock-site.js
create mode 100644 client/lib/followers/test/lib/mock-wpcom-followers1.js
create mode 100644 client/lib/followers/test/lib/mock-wpcom-followers2.js
create mode 100644 client/lib/followers/test/test-store.js
create mode 100644 client/lib/form-state/Makefile
create mode 100644 client/lib/form-state/README.md
create mode 100644 client/lib/form-state/examples/async-initialize.jsx
create mode 100644 client/lib/form-state/examples/sync-initialize.jsx
create mode 100644 client/lib/form-state/index.js
create mode 100644 client/lib/form-state/store/async-initialize.js
create mode 100644 client/lib/form-state/store/core.js
create mode 100644 client/lib/form-state/store/index.js
create mode 100644 client/lib/form-state/store/sync-initialize.js
create mode 100644 client/lib/form-state/test/index.js
create mode 100644 client/lib/geocoding/Makefile
create mode 100644 client/lib/geocoding/README.md
create mode 100644 client/lib/geocoding/index.js
create mode 100644 client/lib/geocoding/test/index.js
create mode 100644 client/lib/google-apps/index.js
create mode 100644 client/lib/happiness-engineers/Makefile
create mode 100644 client/lib/happiness-engineers/README.md
create mode 100644 client/lib/happiness-engineers/actions.js
create mode 100644 client/lib/happiness-engineers/constants.js
create mode 100644 client/lib/happiness-engineers/store.js
create mode 100644 client/lib/happiness-engineers/test/lib/mock-actions.js
create mode 100644 client/lib/happiness-engineers/test/lib/mock-happiness-engineers.js
create mode 100644 client/lib/happiness-engineers/test/test-actions.js
create mode 100644 client/lib/happiness-engineers/test/test-store.js
create mode 100644 client/lib/help-search/Makefile
create mode 100644 client/lib/help-search/README.md
create mode 100644 client/lib/help-search/actions.js
create mode 100644 client/lib/help-search/constants.js
create mode 100644 client/lib/help-search/store.js
create mode 100644 client/lib/help-search/test/lib/mock-actions.js
create mode 100644 client/lib/help-search/test/lib/mock-help-links.js
create mode 100644 client/lib/help-search/test/test-store.js
create mode 100644 client/lib/highlight/Makefile
create mode 100644 client/lib/highlight/README.md
create mode 100644 client/lib/highlight/index.js
create mode 100644 client/lib/highlight/test/highlight-test.js
create mode 100644 client/lib/human-date/index.js
create mode 100644 client/lib/importer/actions.js
create mode 100644 client/lib/importer/common.js
create mode 100644 client/lib/importer/constants.js
create mode 100644 client/lib/importer/store.js
create mode 100644 client/lib/infinite-list/actions.js
create mode 100644 client/lib/infinite-list/positions-store.js
create mode 100644 client/lib/infinite-list/scroll-store.js
create mode 100644 client/lib/inflight/index.js
create mode 100644 client/lib/interpolate-components/Makefile
create mode 100644 client/lib/interpolate-components/README.md
create mode 100644 client/lib/interpolate-components/index.js
create mode 100644 client/lib/interpolate-components/test/lib/warn.js
create mode 100644 client/lib/interpolate-components/test/text.jsx
create mode 100644 client/lib/interpolate-components/tokenize.js
create mode 100644 client/lib/invites/Makefile
create mode 100644 client/lib/invites/actions.js
create mode 100644 client/lib/invites/constants.js
create mode 100644 client/lib/invites/reducers/invites-validation.js
create mode 100644 client/lib/invites/reducers/list-invites.js
create mode 100644 client/lib/invites/stores/invites-validation.js
create mode 100644 client/lib/invites/stores/list-invites.js
create mode 100644 client/lib/invites/test/list-invites-store.js
create mode 100644 client/lib/keyboard-shortcuts/Makefile
create mode 100644 client/lib/keyboard-shortcuts/README.md
create mode 100644 client/lib/keyboard-shortcuts/global.js
create mode 100644 client/lib/keyboard-shortcuts/index.js
create mode 100644 client/lib/keyboard-shortcuts/key-bindings.js
create mode 100644 client/lib/keyboard-shortcuts/menu.jsx
create mode 100644 client/lib/keyboard-shortcuts/test/lib/mixins/i18n.js
create mode 100644 client/lib/keyboard-shortcuts/test/test.js
create mode 100644 client/lib/layout-focus/README.md
create mode 100644 client/lib/layout-focus/index.js
create mode 100644 client/lib/like-store/Makefile
create mode 100644 client/lib/like-store/actions.js
create mode 100644 client/lib/like-store/like-store.js
create mode 100644 client/lib/like-store/test/lib/wp.js
create mode 100644 client/lib/like-store/test/like-store-test.js
create mode 100644 client/lib/like-store/utils.js
create mode 100644 client/lib/load-script/README.md
create mode 100644 client/lib/load-script/index.js
create mode 100644 client/lib/local-list/Makefile
create mode 100644 client/lib/local-list/README.md
create mode 100644 client/lib/local-list/index.js
create mode 100644 client/lib/local-list/test/test.js
create mode 100644 client/lib/local-storage/Makefile
create mode 100644 client/lib/local-storage/README.md
create mode 100644 client/lib/local-storage/index.js
create mode 100644 client/lib/local-storage/test/test.js
create mode 100644 client/lib/locale-suggestions/actions.js
create mode 100644 client/lib/locale-suggestions/index.js
create mode 100644 client/lib/media-serialization/Makefile
create mode 100644 client/lib/media-serialization/constants.js
create mode 100644 client/lib/media-serialization/create-element-from-string.js
create mode 100644 client/lib/media-serialization/detect-format.js
create mode 100644 client/lib/media-serialization/index.js
create mode 100644 client/lib/media-serialization/strategies/api.js
create mode 100644 client/lib/media-serialization/strategies/dom.js
create mode 100644 client/lib/media-serialization/strategies/index.js
create mode 100644 client/lib/media-serialization/strategies/object.js
create mode 100644 client/lib/media-serialization/strategies/shortcode.js
create mode 100644 client/lib/media-serialization/strategies/string.js
create mode 100644 client/lib/media-serialization/strategies/unknown.js
create mode 100644 client/lib/media-serialization/test/index.js
create mode 100644 client/lib/media/Makefile
create mode 100644 client/lib/media/README.md
create mode 100644 client/lib/media/actions.js
create mode 100644 client/lib/media/constants.js
create mode 100644 client/lib/media/library-selected-store.js
create mode 100644 client/lib/media/list-store.js
create mode 100644 client/lib/media/store.js
create mode 100644 client/lib/media/test/actions.js
create mode 100644 client/lib/media/test/library-selected-store.js
create mode 100644 client/lib/media/test/list-store.js
create mode 100644 client/lib/media/test/store.js
create mode 100644 client/lib/media/test/utils.js
create mode 100644 client/lib/media/test/validation-store.js
create mode 100644 client/lib/media/utils.js
create mode 100644 client/lib/media/validation-store.js
create mode 100644 client/lib/menu-data/Makefile
create mode 100644 client/lib/menu-data/README.md
create mode 100644 client/lib/menu-data/index.js
create mode 100644 client/lib/menu-data/menu-data.js
create mode 100644 client/lib/menu-data/test/fixtures.js
create mode 100644 client/lib/menu-data/test/lib/mixins/i18n.js
create mode 100644 client/lib/menu-data/test/lib/sites-list.js
create mode 100644 client/lib/menu-data/test/lib/wp.js
create mode 100644 client/lib/menu-data/test/test-menu-data.js
create mode 100644 client/lib/mixins/analytics/index.js
create mode 100644 client/lib/mixins/close-on-esc/README.md
create mode 100644 client/lib/mixins/close-on-esc/index.js
create mode 100644 client/lib/mixins/data-observe/Makefile
create mode 100644 client/lib/mixins/data-observe/README.md
create mode 100644 client/lib/mixins/data-observe/index.js
create mode 100644 client/lib/mixins/data-observe/test/test.js
create mode 100644 client/lib/mixins/i18n/Makefile
create mode 100644 client/lib/mixins/i18n/README.md
create mode 100644 client/lib/mixins/i18n/index.js
create mode 100644 client/lib/mixins/i18n/number-format.js
create mode 100644 client/lib/mixins/i18n/test/data.js
create mode 100644 client/lib/mixins/i18n/test/i18nLocalStrings.js
create mode 100644 client/lib/mixins/i18n/test/lib/user-settings.js
create mode 100644 client/lib/mixins/i18n/test/lib/user.js
create mode 100644 client/lib/mixins/i18n/test/test.jsx
create mode 100644 client/lib/mixins/i18n/timezone.js
create mode 100644 client/lib/mixins/infinite-scroll/README.md
create mode 100644 client/lib/mixins/infinite-scroll/index.js
create mode 100644 client/lib/mixins/lock/README.md
create mode 100644 client/lib/mixins/lock/index.js
create mode 100644 client/lib/mixins/observe-window-resize/index.js
create mode 100644 client/lib/mixins/pageable/Makefile
create mode 100644 client/lib/mixins/pageable/README.md
create mode 100644 client/lib/mixins/pageable/index.js
create mode 100644 client/lib/mixins/pageable/test/test.js
create mode 100644 client/lib/mixins/protect-form/README.md
create mode 100644 client/lib/mixins/protect-form/index.js
create mode 100644 client/lib/mixins/render-visualizer/README.md
create mode 100644 client/lib/mixins/render-visualizer/index.js
create mode 100644 client/lib/mixins/searchable/Makefile
create mode 100644 client/lib/mixins/searchable/README.md
create mode 100644 client/lib/mixins/searchable/index.js
create mode 100644 client/lib/mixins/searchable/test/test.js
create mode 100644 client/lib/mixins/trap-focus/README.md
create mode 100644 client/lib/mixins/trap-focus/index.js
create mode 100644 client/lib/mixins/update-post-status/README.md
create mode 100644 client/lib/mixins/update-post-status/index.jsx
create mode 100644 client/lib/mixins/url-search/Makefile
create mode 100644 client/lib/mixins/url-search/README.md
create mode 100644 client/lib/mixins/url-search/build-url.js
create mode 100644 client/lib/mixins/url-search/index.js
create mode 100644 client/lib/mixins/url-search/test/build-url.js
create mode 100644 client/lib/network-connection/Makefile
create mode 100644 client/lib/network-connection/README.md
create mode 100644 client/lib/network-connection/index.js
create mode 100644 client/lib/network-connection/test/index-test.js
create mode 100644 client/lib/notification-settings-store/Makefile
create mode 100644 client/lib/notification-settings-store/actions.js
create mode 100644 client/lib/notification-settings-store/constants.js
create mode 100644 client/lib/notification-settings-store/index.js
create mode 100644 client/lib/notification-settings-store/test/store-test.js
create mode 100644 client/lib/notification-settings-store/toggle-state.js
create mode 100644 client/lib/oauth-store/Makefile
create mode 100644 client/lib/oauth-store/README.md
create mode 100644 client/lib/oauth-store/actions.js
create mode 100644 client/lib/oauth-store/constants.js
create mode 100644 client/lib/oauth-store/index.js
create mode 100644 client/lib/oauth-store/test/oauth-store.js
create mode 100644 client/lib/oauth-token/README.md
create mode 100644 client/lib/oauth-token/index.js
create mode 100644 client/lib/olark-api/README.md
create mode 100644 client/lib/olark-api/index.js
create mode 100644 client/lib/olark-events/Makefile
create mode 100644 client/lib/olark-events/README.md
create mode 100644 client/lib/olark-events/index.js
create mode 100644 client/lib/olark-events/test/lib/mock-olark.js
create mode 100644 client/lib/olark-events/test/test-onready-event.js
create mode 100644 client/lib/olark-store/Makefile
create mode 100644 client/lib/olark-store/README.md
create mode 100644 client/lib/olark-store/actions.js
create mode 100644 client/lib/olark-store/constants.js
create mode 100644 client/lib/olark-store/index.js
create mode 100644 client/lib/olark-store/test/olark-store-test.js
create mode 100644 client/lib/olark/README.md
create mode 100644 client/lib/olark/index.js
create mode 100644 client/lib/page-templates/actions.js
create mode 100644 client/lib/page-templates/store.js
create mode 100644 client/lib/paths/Makefile
create mode 100644 client/lib/paths/index.js
create mode 100644 client/lib/paths/test/index.js
create mode 100644 client/lib/paygate-loader/README.md
create mode 100644 client/lib/paygate-loader/index.js
create mode 100644 client/lib/people/Makefile
create mode 100644 client/lib/people/README.md
create mode 100644 client/lib/people/actions.js
create mode 100644 client/lib/people/log-store.js
create mode 100644 client/lib/people/test/lib/mock-actions.js
create mode 100644 client/lib/people/test/lib/mock-site.js
create mode 100644 client/lib/people/test/test-store.js
create mode 100644 client/lib/phone-validation/Makefile
create mode 100644 client/lib/phone-validation/README.md
create mode 100644 client/lib/phone-validation/index.jsx
create mode 100644 client/lib/phone-validation/test/test.jsx
create mode 100644 client/lib/plans-list/Makefile
create mode 100644 client/lib/plans-list/README.md
create mode 100644 client/lib/plans-list/index.js
create mode 100644 client/lib/plans-list/test/data.js
create mode 100644 client/lib/plans-list/test/lib/wp.js
create mode 100644 client/lib/plans-list/test/test.js
create mode 100644 client/lib/plugins/Makefile
create mode 100644 client/lib/plugins/README.md
create mode 100644 client/lib/plugins/actions.js
create mode 100644 client/lib/plugins/log-store.js
create mode 100644 client/lib/plugins/notices.jsx
create mode 100644 client/lib/plugins/store.js
create mode 100644 client/lib/plugins/test/lib/mixins/i18n.js
create mode 100644 client/lib/plugins/test/lib/mock-actions.js
create mode 100644 client/lib/plugins/test/lib/mock-multi-site.js
create mode 100644 client/lib/plugins/test/lib/mock-plugins-updated.js
create mode 100644 client/lib/plugins/test/lib/mock-plugins.js
create mode 100644 client/lib/plugins/test/lib/mock-site.js
create mode 100644 client/lib/plugins/test/lib/mock-sites-list.js
create mode 100644 client/lib/plugins/test/lib/mock-updated-plugin.js
create mode 100644 client/lib/plugins/test/lib/mock-wpcom.js
create mode 100644 client/lib/plugins/test/lib/wp.js
create mode 100644 client/lib/plugins/test/test-actions.js
create mode 100644 client/lib/plugins/test/test-log-store.js
create mode 100644 client/lib/plugins/test/test-store.js
create mode 100644 client/lib/plugins/test/test-utils.js
create mode 100644 client/lib/plugins/utils.js
create mode 100644 client/lib/plugins/wporg-data/Makefile
create mode 100644 client/lib/plugins/wporg-data/README.md
create mode 100644 client/lib/plugins/wporg-data/actions.js
create mode 100644 client/lib/plugins/wporg-data/list-store.js
create mode 100644 client/lib/plugins/wporg-data/store.js
create mode 100644 client/lib/plugins/wporg-data/test/lib/data-actions.js
create mode 100644 client/lib/plugins/wporg-data/test/lib/mock-actions.js
create mode 100644 client/lib/plugins/wporg-data/test/lib/mock-local-store.js
create mode 100644 client/lib/plugins/wporg-data/test/lib/mock-store.js
create mode 100644 client/lib/plugins/wporg-data/test/lib/mock-wporg.js
create mode 100644 client/lib/plugins/wporg-data/test/lib/mocked-actions.js
create mode 100644 client/lib/plugins/wporg-data/test/test-actions.js
create mode 100644 client/lib/plugins/wporg-data/test/test-list-store.js
create mode 100644 client/lib/plugins/wporg-data/test/test-store.js
create mode 100644 client/lib/popup-monitor/Makefile
create mode 100644 client/lib/popup-monitor/README.md
create mode 100644 client/lib/popup-monitor/index.js
create mode 100644 client/lib/popup-monitor/test/index.js
create mode 100644 client/lib/post-formats/Makefile
create mode 100644 client/lib/post-formats/README.md
create mode 100644 client/lib/post-formats/actions.js
create mode 100644 client/lib/post-formats/store.js
create mode 100644 client/lib/post-formats/test/actions.js
create mode 100644 client/lib/post-formats/test/store.js
create mode 100644 client/lib/post-metadata/Makefile
create mode 100644 client/lib/post-metadata/README.md
create mode 100644 client/lib/post-metadata/index.js
create mode 100644 client/lib/post-metadata/test/index.js
create mode 100644 client/lib/post-normalizer/Makefile
create mode 100644 client/lib/post-normalizer/README.md
create mode 100644 client/lib/post-normalizer/index.js
create mode 100644 client/lib/post-normalizer/test/lib/safe-image-url.js
create mode 100644 client/lib/post-normalizer/test/post-normalizer-test.js
create mode 100644 client/lib/post-stats/README.md
create mode 100644 client/lib/post-stats/actions.js
create mode 100644 client/lib/post-stats/constants.js
create mode 100644 client/lib/post-stats/store.js
create mode 100644 client/lib/post-types-list/README.md
create mode 100644 client/lib/post-types-list/index.js
create mode 100644 client/lib/post-types-list/list.js
create mode 100644 client/lib/posts/Makefile
create mode 100644 client/lib/posts/README.md
create mode 100644 client/lib/posts/actions.js
create mode 100644 client/lib/posts/post-content-images-store.js
create mode 100644 client/lib/posts/post-counts-store.js
create mode 100644 client/lib/posts/post-edit-store.js
create mode 100644 client/lib/posts/post-list-cache-store.js
create mode 100644 client/lib/posts/post-list-store.js
create mode 100644 client/lib/posts/posts-store.js
create mode 100644 client/lib/posts/stats.js
create mode 100644 client/lib/posts/test/actions.js
create mode 100644 client/lib/posts/test/post-edit-store.js
create mode 100644 client/lib/posts/test/utils.js
create mode 100644 client/lib/posts/utils.js
create mode 100644 client/lib/preferences/Makefile
create mode 100644 client/lib/preferences/README.md
create mode 100644 client/lib/preferences/actions.js
create mode 100644 client/lib/preferences/constants.js
create mode 100644 client/lib/preferences/store.js
create mode 100644 client/lib/preferences/test/actions.js
create mode 100644 client/lib/preferences/test/store.js
create mode 100644 client/lib/products-list/README.md
create mode 100644 client/lib/products-list/index.js
create mode 100644 client/lib/products-values/index.js
create mode 100644 client/lib/products-values/schema.json
create mode 100644 client/lib/products-values/sort.js
create mode 100644 client/lib/purchases/Makefile
create mode 100644 client/lib/purchases/assembler.js
create mode 100644 client/lib/purchases/index.js
create mode 100644 client/lib/purchases/store.js
create mode 100644 client/lib/purchases/stored-cards/Makefile
create mode 100644 client/lib/purchases/stored-cards/assembler.js
create mode 100644 client/lib/purchases/stored-cards/reducer.js
create mode 100644 client/lib/purchases/stored-cards/store.js
create mode 100644 client/lib/purchases/stored-cards/test/assembler-test.js
create mode 100644 client/lib/purchases/stored-cards/test/constants.js
create mode 100644 client/lib/purchases/stored-cards/test/store-test.js
create mode 100644 client/lib/purchases/test/assembler-test.js
create mode 100644 client/lib/purchases/test/store-test.js
create mode 100644 client/lib/react-pass-to-children/Makefile
create mode 100644 client/lib/react-pass-to-children/README.md
create mode 100644 client/lib/react-pass-to-children/index.js
create mode 100644 client/lib/react-pass-to-children/test/index.jsx
create mode 100644 client/lib/react-smart-set-state/index.js
create mode 100644 client/lib/react-test-env-setup/Makefile
create mode 100644 client/lib/react-test-env-setup/README.md
create mode 100644 client/lib/react-test-env-setup/index.js
create mode 100644 client/lib/react-test-env-setup/test/index.js
create mode 100644 client/lib/reader-comment-email-subscriptions/Makefile
create mode 100644 client/lib/reader-comment-email-subscriptions/actions.js
create mode 100644 client/lib/reader-comment-email-subscriptions/constants.js
create mode 100644 client/lib/reader-comment-email-subscriptions/index.js
create mode 100644 client/lib/reader-comment-email-subscriptions/test/comment-email-subscription-store-test.js
create mode 100644 client/lib/reader-feed-subscriptions/Makefile
create mode 100644 client/lib/reader-feed-subscriptions/actions.js
create mode 100644 client/lib/reader-feed-subscriptions/constants.js
create mode 100644 client/lib/reader-feed-subscriptions/helper.js
create mode 100644 client/lib/reader-feed-subscriptions/index.js
create mode 100644 client/lib/reader-feed-subscriptions/test/feed-subscription-store-test.js
create mode 100644 client/lib/reader-lists/README.md
create mode 100644 client/lib/reader-lists/actions.js
create mode 100644 client/lib/reader-lists/lists.js
create mode 100644 client/lib/reader-lists/subscriptions.js
create mode 100644 client/lib/reader-post-email-subscriptions/Makefile
create mode 100644 client/lib/reader-post-email-subscriptions/actions.js
create mode 100644 client/lib/reader-post-email-subscriptions/constants.js
create mode 100644 client/lib/reader-post-email-subscriptions/index.js
create mode 100644 client/lib/reader-post-email-subscriptions/test/post-email-subscription-store-test.js
create mode 100644 client/lib/reader-sidebar/actions.js
create mode 100644 client/lib/reader-site-blocks/Makefile
create mode 100644 client/lib/reader-site-blocks/actions.js
create mode 100644 client/lib/reader-site-blocks/constants.js
create mode 100644 client/lib/reader-site-blocks/index.js
create mode 100644 client/lib/reader-site-blocks/test/site-block-store-test.js
create mode 100644 client/lib/reader-site-store/Makefile
create mode 100644 client/lib/reader-site-store/actions.js
create mode 100644 client/lib/reader-site-store/constants.js
create mode 100644 client/lib/reader-site-store/index.js
create mode 100644 client/lib/reader-site-store/test/reader-site-store-tests.js
create mode 100644 client/lib/reader-tags/README.md
create mode 100644 client/lib/reader-tags/actions.js
create mode 100644 client/lib/reader-tags/subscriptions.js
create mode 100644 client/lib/reader-tags/tags.js
create mode 100644 client/lib/reader-teams/Makefile
create mode 100644 client/lib/reader-teams/actions.js
create mode 100644 client/lib/reader-teams/constants.js
create mode 100644 client/lib/reader-teams/index.js
create mode 100644 client/lib/reader-teams/test/team-store-test.js
create mode 100644 client/lib/recommended-sites-store/Makefile
create mode 100644 client/lib/recommended-sites-store/actions.js
create mode 100644 client/lib/recommended-sites-store/constants.js
create mode 100644 client/lib/recommended-sites-store/store.js
create mode 100644 client/lib/recommended-sites-store/test/action-tests.js
create mode 100644 client/lib/recommended-sites-store/test/store-tests.js
create mode 100644 client/lib/resize-image-url/Makefile
create mode 100644 client/lib/resize-image-url/README.md
create mode 100644 client/lib/resize-image-url/index.js
create mode 100644 client/lib/resize-image-url/test/test.js
create mode 100644 client/lib/route/Makefile
create mode 100644 client/lib/route/README.md
create mode 100644 client/lib/route/index.js
create mode 100644 client/lib/route/normalize.js
create mode 100644 client/lib/route/path.js
create mode 100644 client/lib/route/redirect.js
create mode 100644 client/lib/route/test/index.js
create mode 100644 client/lib/route/trailingslashit.js
create mode 100644 client/lib/route/untrailingslashit.js
create mode 100644 client/lib/safe-image-url/Makefile
create mode 100644 client/lib/safe-image-url/README.md
create mode 100644 client/lib/safe-image-url/index.js
create mode 100644 client/lib/safe-image-url/test/index.js
create mode 100644 client/lib/safe-protocol-url/Makefile
create mode 100644 client/lib/safe-protocol-url/README.md
create mode 100644 client/lib/safe-protocol-url/index.js
create mode 100644 client/lib/safe-protocol-url/test/test.js
create mode 100644 client/lib/scroll-to/Makefile
create mode 100644 client/lib/scroll-to/README.md
create mode 100644 client/lib/scroll-to/index.js
create mode 100644 client/lib/scroll-to/test/test.js
create mode 100644 client/lib/security-checkup/Makefile
create mode 100644 client/lib/security-checkup/account-recovery-store.js
create mode 100644 client/lib/security-checkup/actions.js
create mode 100644 client/lib/security-checkup/constants.js
create mode 100644 client/lib/security-checkup/test/account-recovery-store.js
create mode 100644 client/lib/security-checkup/test/actions.js
create mode 100644 client/lib/security-checkup/test/constants.js
create mode 100644 client/lib/services-list/README.md
create mode 100644 client/lib/services-list/index.js
create mode 100644 client/lib/services-list/list.js
create mode 100644 client/lib/sharing-buttons-list/README.md
create mode 100644 client/lib/sharing-buttons-list/index.js
create mode 100644 client/lib/shortcode/Makefile
create mode 100644 client/lib/shortcode/README.md
create mode 100644 client/lib/shortcode/index.js
create mode 100644 client/lib/shortcode/test/index.js
create mode 100644 client/lib/shortcodes/README.md
create mode 100644 client/lib/shortcodes/actions.js
create mode 100644 client/lib/shortcodes/constants.js
create mode 100644 client/lib/shortcodes/store.js
create mode 100644 client/lib/siftscience/README.md
create mode 100644 client/lib/siftscience/index.js
create mode 100644 client/lib/signup/Makefile
create mode 100644 client/lib/signup/README.md
create mode 100644 client/lib/signup/actions.js
create mode 100644 client/lib/signup/cart.js
create mode 100644 client/lib/signup/dependency-store.js
create mode 100644 client/lib/signup/flow-controller.js
create mode 100644 client/lib/signup/progress-store.js
create mode 100644 client/lib/signup/step-actions.js
create mode 100644 client/lib/signup/test/analytics.js
create mode 100644 client/lib/signup/test/dependency-store-test.js
create mode 100644 client/lib/signup/test/flow-controller-test.js
create mode 100644 client/lib/signup/test/lib/user/index.js
create mode 100644 client/lib/signup/test/lib/wp/index.js
create mode 100644 client/lib/signup/test/progress-store-test.js
create mode 100644 client/lib/signup/test/signup/config/flows.js
create mode 100644 client/lib/signup/test/signup/config/steps.js
create mode 100644 client/lib/site-roles/Makefile
create mode 100644 client/lib/site-roles/README.md
create mode 100644 client/lib/site-roles/actions.js
create mode 100644 client/lib/site-roles/store.js
create mode 100644 client/lib/site-roles/test/lib/mock-actions.js
create mode 100644 client/lib/site-roles/test/lib/mock-roles.js
create mode 100644 client/lib/site-roles/test/lib/mock-site.js
create mode 100644 client/lib/site-roles/test/test-store.js
create mode 100644 client/lib/site-specific-plans-details-list/README.md
create mode 100644 client/lib/site-specific-plans-details-list/index.js
create mode 100644 client/lib/site-stats-sticky-tab/README.md
create mode 100644 client/lib/site-stats-sticky-tab/actions.js
create mode 100644 client/lib/site-stats-sticky-tab/constants.js
create mode 100644 client/lib/site-stats-sticky-tab/store.js
create mode 100644 client/lib/site/Makefile
create mode 100644 client/lib/site/README.md
create mode 100644 client/lib/site/index.js
create mode 100644 client/lib/site/jetpack.js
create mode 100644 client/lib/site/test/lib/wp.js
create mode 100644 client/lib/site/test/test.js
create mode 100644 client/lib/site/utils.js
create mode 100644 client/lib/sites-list/Makefile
create mode 100644 client/lib/sites-list/README.md
create mode 100644 client/lib/sites-list/actions.js
create mode 100644 client/lib/sites-list/delete-site-store.js
create mode 100644 client/lib/sites-list/docs/example.jsx
create mode 100644 client/lib/sites-list/index.js
create mode 100644 client/lib/sites-list/list.js
create mode 100644 client/lib/sites-list/log-store.js
create mode 100644 client/lib/sites-list/notices.js
create mode 100644 client/lib/sites-list/test/data.js
create mode 100644 client/lib/sites-list/test/lib/mixins/i18n.js
create mode 100644 client/lib/sites-list/test/lib/mock-actions.js
create mode 100644 client/lib/sites-list/test/lib/mock-site.js
create mode 100644 client/lib/sites-list/test/lib/user.js
create mode 100644 client/lib/sites-list/test/lib/wp.js
create mode 100644 client/lib/sites-list/test/test-log-store.js
create mode 100644 client/lib/sites-list/test/test.js
create mode 100644 client/lib/states-list/README.md
create mode 100644 client/lib/states-list/index.js
create mode 100644 client/lib/stats/README.md
create mode 100644 client/lib/stats/stats-list/Makefile
create mode 100644 client/lib/stats/stats-list/README.md
create mode 100644 client/lib/stats/stats-list/index.js
create mode 100644 client/lib/stats/stats-list/stats-parser.js
create mode 100644 client/lib/stats/stats-list/test/analytics.js
create mode 100644 client/lib/stats/stats-list/test/data.js
create mode 100644 client/lib/stats/stats-list/test/lib/mixins/i18n.js
create mode 100644 client/lib/stats/stats-list/test/lib/user.js
create mode 100644 client/lib/stats/stats-list/test/lib/wp.js
create mode 100644 client/lib/stats/stats-list/test/test-stats-list.js
create mode 100644 client/lib/stats/stats-list/test/test-stats-parser.js
create mode 100644 client/lib/stats/summary-list/README.md
create mode 100644 client/lib/stats/summary-list/index.js
create mode 100644 client/lib/stats/summary/README.md
create mode 100644 client/lib/stats/summary/index.js
create mode 100644 client/lib/store-transactions/README.md
create mode 100644 client/lib/store-transactions/index.js
create mode 100644 client/lib/store/Makefile
create mode 100644 client/lib/store/README.md
create mode 100644 client/lib/store/index.js
create mode 100644 client/lib/store/test/index-test.js
create mode 100644 client/lib/stored-cards/README.md
create mode 100644 client/lib/stored-cards/index.js
create mode 100644 client/lib/tags-list/README.md
create mode 100644 client/lib/tags-list/index.js
create mode 100644 client/lib/terms/Makefile
create mode 100644 client/lib/terms/README.md
create mode 100644 client/lib/terms/actions.js
create mode 100644 client/lib/terms/category-store-factory.js
create mode 100644 client/lib/terms/category-store.js
create mode 100644 client/lib/terms/constants.js
create mode 100644 client/lib/terms/store.js
create mode 100644 client/lib/terms/tag-store.js
create mode 100644 client/lib/terms/test/actions.js
create mode 100644 client/lib/terms/test/category-store.js
create mode 100644 client/lib/terms/test/common.js
create mode 100644 client/lib/terms/test/data.js
create mode 100644 client/lib/terms/test/store.js
create mode 100644 client/lib/terms/test/tag-store.js
create mode 100644 client/lib/ticker/index.js
create mode 100644 client/lib/touch-detect/README.md
create mode 100644 client/lib/touch-detect/index.js
create mode 100644 client/lib/track-scroll-page/index.js
create mode 100644 client/lib/transaction/store.js
create mode 100644 client/lib/translator-jumpstart/README.md
create mode 100644 client/lib/translator-jumpstart/index.js
create mode 100644 client/lib/tree-convert/Makefile
create mode 100644 client/lib/tree-convert/README.md
create mode 100644 client/lib/tree-convert/index.js
create mode 100644 client/lib/tree-convert/test/fixtures.js
create mode 100644 client/lib/tree-convert/test/test-tree-convert.js
create mode 100644 client/lib/tree-convert/tree-traverser.js
create mode 100644 client/lib/trophies-data/README.md
create mode 100644 client/lib/trophies-data/index.js
create mode 100644 client/lib/two-step-authorization/README.md
create mode 100644 client/lib/two-step-authorization/index.js
create mode 100644 client/lib/upgrades/actions/cart.js
create mode 100644 client/lib/upgrades/actions/checkout.js
create mode 100644 client/lib/upgrades/actions/domain-management.js
create mode 100644 client/lib/upgrades/actions/domain-search.js
create mode 100644 client/lib/upgrades/actions/index.js
create mode 100644 client/lib/upgrades/actions/purchases.js
create mode 100644 client/lib/upgrades/constants.js
create mode 100644 client/lib/user-profile-links/README.md
create mode 100644 client/lib/user-profile-links/index.js
create mode 100644 client/lib/user-settings/Makefile
create mode 100644 client/lib/user-settings/README.md
create mode 100644 client/lib/user-settings/index.js
create mode 100644 client/lib/user-settings/test/index.js
create mode 100644 client/lib/user-settings/test/lib/user.js
create mode 100644 client/lib/user-settings/test/lib/wp.js
create mode 100644 client/lib/user/Makefile
create mode 100644 client/lib/user/README.md
create mode 100644 client/lib/user/index.js
create mode 120000 client/lib/user/shared-utils.js
create mode 100644 client/lib/user/test/utils.js
create mode 100644 client/lib/user/user.js
create mode 100644 client/lib/user/utils.js
create mode 100644 client/lib/username/README.md
create mode 100644 client/lib/username/index.js
create mode 100644 client/lib/users/Makefile
create mode 100644 client/lib/users/README.md
create mode 100644 client/lib/users/actions.js
create mode 100644 client/lib/users/store.js
create mode 100644 client/lib/users/test/lib/mock-actions.js
create mode 100644 client/lib/users/test/lib/mock-deleted-user-data.js
create mode 100644 client/lib/users/test/lib/mock-more-users-data.js
create mode 100644 client/lib/users/test/lib/mock-single-user.js
create mode 100644 client/lib/users/test/lib/mock-site.js
create mode 100644 client/lib/users/test/lib/mock-updated-single-user.js
create mode 100644 client/lib/users/test/lib/mock-users-data.js
create mode 100644 client/lib/users/test/test-store.js
create mode 100644 client/lib/version-compare/README.md
create mode 100644 client/lib/version-compare/index.js
create mode 100644 client/lib/viewers/Makefile
create mode 100644 client/lib/viewers/README.md
create mode 100644 client/lib/viewers/actions.js
create mode 100644 client/lib/viewers/store.js
create mode 100644 client/lib/viewers/test/lib/mock-actions.js
create mode 100644 client/lib/viewers/test/lib/mock-site.js
create mode 100644 client/lib/viewers/test/lib/mock-viewers-1.js
create mode 100644 client/lib/viewers/test/lib/mock-viewers-2.js
create mode 100644 client/lib/viewers/test/test-store.js
create mode 100644 client/lib/viewport/README.md
create mode 100644 client/lib/viewport/index.js
create mode 100644 client/lib/warn/index.js
create mode 100644 client/lib/wpcom-xhr-wrapper/Makefile
create mode 100644 client/lib/wpcom-xhr-wrapper/README.md
create mode 100644 client/lib/wpcom-xhr-wrapper/index.js
create mode 100644 client/lib/wpcom-xhr-wrapper/test/wpcom-xhr-wrapper.js
create mode 100644 client/lib/wporg/index.js
create mode 100644 client/lib/wporg/jsonp.js
create mode 100644 client/mailing-lists/controller.js
create mode 100644 client/mailing-lists/index.js
create mode 100644 client/mailing-lists/main.jsx
create mode 100644 client/mailing-lists/utils.js
create mode 100644 client/me/account-password/README.md
create mode 100644 client/me/account-password/index.jsx
create mode 100644 client/me/account-password/style.scss
create mode 100644 client/me/account/README.md
create mode 100644 client/me/account/index.jsx
create mode 100644 client/me/account/style.scss
create mode 100644 client/me/action-remove/README.md
create mode 100644 client/me/action-remove/index.jsx
create mode 100644 client/me/action-remove/style.scss
create mode 100644 client/me/application-password-item/README.md
create mode 100644 client/me/application-password-item/index.jsx
create mode 100644 client/me/application-password-item/style.scss
create mode 100644 client/me/application-passwords/README.md
create mode 100644 client/me/application-passwords/index.jsx
create mode 100644 client/me/application-passwords/style.scss
create mode 100644 client/me/billing-history/README.md
create mode 100644 client/me/billing-history/billing-history-table.jsx
create mode 100644 client/me/billing-history/index.jsx
create mode 100644 client/me/billing-history/table-rows.js
create mode 100644 client/me/billing-history/transactions-header.jsx
create mode 100644 client/me/billing-history/transactions-table.jsx
create mode 100644 client/me/billing-history/upcoming-charges-table.jsx
create mode 100644 client/me/billing-history/view-receipt-modal.jsx
create mode 100644 client/me/connected-application-icon/README.md
create mode 100644 client/me/connected-application-icon/index.jsx
create mode 100644 client/me/connected-application-icon/style.scss
create mode 100644 client/me/connected-application-item/README.md
create mode 100644 client/me/connected-application-item/index.jsx
create mode 100644 client/me/connected-application-item/style.scss
create mode 100644 client/me/connected-applications/README.md
create mode 100644 client/me/connected-applications/index.jsx
create mode 100644 client/me/connected-applications/style.scss
create mode 100644 client/me/controller.js
create mode 100644 client/me/credit-cards/credit-card-delete.jsx
create mode 100644 client/me/credit-cards/credit-card-delete.scss
create mode 100644 client/me/credit-cards/credit-cards.scss
create mode 100644 client/me/credit-cards/index.jsx
create mode 100644 client/me/event-recorder/index.js
create mode 100644 client/me/form-base/index.js
create mode 100644 client/me/help/README.md
create mode 100644 client/me/help/controller.js
create mode 100644 client/me/help/help-contact-confirmation/index.jsx
create mode 100644 client/me/help/help-contact-confirmation/style.scss
create mode 100644 client/me/help/help-contact-form/index.jsx
create mode 100644 client/me/help/help-contact-form/style.scss
create mode 100644 client/me/help/help-contact/index.jsx
create mode 100644 client/me/help/help-contact/style.scss
create mode 100644 client/me/help/help-happiness-engineers/index.jsx
create mode 100644 client/me/help/help-happiness-engineers/style.scss
create mode 100644 client/me/help/help-results/index.jsx
create mode 100644 client/me/help/help-results/item.jsx
create mode 100644 client/me/help/help-results/style.scss
create mode 100644 client/me/help/help-search/index.jsx
create mode 100644 client/me/help/help-search/style.scss
create mode 100644 client/me/help/index.js
create mode 100644 client/me/help/main.jsx
create mode 100644 client/me/help/style.scss
create mode 100644 client/me/index.js
create mode 100644 client/me/next-steps/index.jsx
create mode 100644 client/me/next-steps/next-steps-box.jsx
create mode 100644 client/me/next-steps/next-steps-box.scss
create mode 100644 client/me/next-steps/next-steps.scss
create mode 100644 client/me/next-steps/steps.js
create mode 100644 client/me/notification-settings/blogs-settings/blog.jsx
create mode 100644 client/me/notification-settings/blogs-settings/header.jsx
create mode 100644 client/me/notification-settings/blogs-settings/index.jsx
create mode 100644 client/me/notification-settings/blogs-settings/placeholder.jsx
create mode 100644 client/me/notification-settings/blogs-settings/style.scss
create mode 100644 client/me/notification-settings/comment-settings/index.jsx
create mode 100644 client/me/notification-settings/comment-settings/style.scss
create mode 100644 client/me/notification-settings/index.jsx
create mode 100644 client/me/notification-settings/navigation.jsx
create mode 100644 client/me/notification-settings/reader-subscriptions/index.jsx
create mode 100644 client/me/notification-settings/settings-form/actions.jsx
create mode 100644 client/me/notification-settings/settings-form/constants.js
create mode 100644 client/me/notification-settings/settings-form/device-selector.jsx
create mode 100644 client/me/notification-settings/settings-form/index.jsx
create mode 100644 client/me/notification-settings/settings-form/labels-list.jsx
create mode 100644 client/me/notification-settings/settings-form/labels.jsx
create mode 100644 client/me/notification-settings/settings-form/locales.js
create mode 100644 client/me/notification-settings/settings-form/settings.jsx
create mode 100644 client/me/notification-settings/settings-form/stream-header.jsx
create mode 100644 client/me/notification-settings/settings-form/stream-options.jsx
create mode 100644 client/me/notification-settings/settings-form/stream-selector.jsx
create mode 100644 client/me/notification-settings/settings-form/stream.jsx
create mode 100644 client/me/notification-settings/settings-form/style.scss
create mode 100644 client/me/notification-settings/wpcom-settings/index.jsx
create mode 100644 client/me/notification-settings/wpcom-settings/style.scss
create mode 100644 client/me/paths.js
create mode 100644 client/me/profile-gravatar/README.md
create mode 100644 client/me/profile-gravatar/index.jsx
create mode 100644 client/me/profile-gravatar/style.scss
create mode 100644 client/me/profile-link/README.md
create mode 100644 client/me/profile-link/index.jsx
create mode 100644 client/me/profile-link/style.scss
create mode 100644 client/me/profile-links-add-other/README.md
create mode 100644 client/me/profile-links-add-other/index.jsx
create mode 100644 client/me/profile-links-add-other/style.scss
create mode 100644 client/me/profile-links-add-wordpress/README.md
create mode 100644 client/me/profile-links-add-wordpress/index.jsx
create mode 100644 client/me/profile-links-add-wordpress/style.scss
create mode 100644 client/me/profile-links/README.md
create mode 100644 client/me/profile-links/index.jsx
create mode 100644 client/me/profile-links/style.scss
create mode 100644 client/me/profile/README.me
create mode 100644 client/me/profile/index.jsx
create mode 100644 client/me/purchases/cancel-private-registration/index.jsx
create mode 100644 client/me/purchases/cancel-private-registration/style.scss
create mode 100644 client/me/purchases/cancel-purchase/button.jsx
create mode 100644 client/me/purchases/cancel-purchase/index.jsx
create mode 100644 client/me/purchases/cancel-purchase/product-information.jsx
create mode 100644 client/me/purchases/cancel-purchase/refund-information.jsx
create mode 100644 client/me/purchases/cancel-purchase/style.scss
create mode 100644 client/me/purchases/cancel-purchase/support-box.jsx
create mode 100644 client/me/purchases/confirm-cancel-purchase/index.jsx
create mode 100644 client/me/purchases/confirm-cancel-purchase/load-endpoint-form.js
create mode 100644 client/me/purchases/confirm-cancel-purchase/style.scss
create mode 100644 client/me/purchases/controller.js
create mode 100644 client/me/purchases/list/header/index.jsx
create mode 100644 client/me/purchases/list/header/style.scss
create mode 100644 client/me/purchases/list/index.jsx
create mode 100644 client/me/purchases/list/item/index.jsx
create mode 100644 client/me/purchases/list/item/style.scss
create mode 100644 client/me/purchases/list/site/index.jsx
create mode 100644 client/me/purchases/list/site/style.scss
create mode 100644 client/me/purchases/manage-purchase/index.jsx
create mode 100644 client/me/purchases/manage-purchase/style.scss
create mode 100644 client/me/purchases/paths.js
create mode 100644 client/me/purchases/payment/edit-card-details/index.jsx
create mode 100644 client/me/purchases/payment/edit-card-details/style.scss
create mode 100644 client/me/purchases/payment/edit-payment-method/credit-card.jsx
create mode 100644 client/me/purchases/payment/edit-payment-method/index.jsx
create mode 100644 client/me/purchases/payment/edit-payment-method/paypal.jsx
create mode 100644 client/me/purchases/payment/edit-payment-method/style.scss
create mode 100644 client/me/purchases/purchases-mixin.js
create mode 100644 client/me/reauth-required/index.jsx
create mode 100644 client/me/reauth-required/style.scss
create mode 100644 client/me/security-2fa-app-chooser-item/index.jsx
create mode 100644 client/me/security-2fa-app-chooser-item/style.scss
create mode 100644 client/me/security-2fa-backup-codes-list/index.jsx
create mode 100644 client/me/security-2fa-backup-codes-list/style.scss
create mode 100644 client/me/security-2fa-backup-codes-prompt/index.jsx
create mode 100644 client/me/security-2fa-backup-codes-prompt/style.scss
create mode 100644 client/me/security-2fa-backup-codes/README.md
create mode 100644 client/me/security-2fa-backup-codes/index.jsx
create mode 100644 client/me/security-2fa-backup-codes/style.scss
create mode 100644 client/me/security-2fa-code-prompt/README.md
create mode 100644 client/me/security-2fa-code-prompt/index.jsx
create mode 100644 client/me/security-2fa-code-prompt/style.scss
create mode 100644 client/me/security-2fa-disable/README.md
create mode 100644 client/me/security-2fa-disable/index.jsx
create mode 100644 client/me/security-2fa-disable/style.scss
create mode 100644 client/me/security-2fa-enable/index.jsx
create mode 100644 client/me/security-2fa-enable/style.scss
create mode 100644 client/me/security-2fa-initial-setup/README.md
create mode 100644 client/me/security-2fa-initial-setup/index.jsx
create mode 100644 client/me/security-2fa-progress/README.md
create mode 100644 client/me/security-2fa-progress/index.jsx
create mode 100644 client/me/security-2fa-progress/progress-item.jsx
create mode 100644 client/me/security-2fa-progress/style.scss
create mode 100644 client/me/security-2fa-setup-backup-codes/README.md
create mode 100644 client/me/security-2fa-setup-backup-codes/index.jsx
create mode 100644 client/me/security-2fa-setup/README.md
create mode 100644 client/me/security-2fa-setup/index.jsx
create mode 100644 client/me/security-2fa-sms-settings/README.md
create mode 100644 client/me/security-2fa-sms-settings/index.jsx
create mode 100644 client/me/security-2fa-sms-settings/style.scss
create mode 100644 client/me/security-2fa-status/README.md
create mode 100644 client/me/security-2fa-status/index.jsx
create mode 100644 client/me/security-2fa-status/style.scss
create mode 100644 client/me/security-checkup/buttons.jsx
create mode 100644 client/me/security-checkup/edit-email.jsx
create mode 100644 client/me/security-checkup/edit-phone.jsx
create mode 100644 client/me/security-checkup/index.jsx
create mode 100644 client/me/security-checkup/manage-contact.jsx
create mode 100644 client/me/security-checkup/recovery-email.jsx
create mode 100644 client/me/security-checkup/recovery-phone.jsx
create mode 100644 client/me/security-checkup/style.scss
create mode 100644 client/me/security-section-nav/README.me
create mode 100644 client/me/security-section-nav/index.jsx
create mode 100644 client/me/security/password.jsx
create mode 100644 client/me/select-site.jsx
create mode 100644 client/me/sidebar-navigation/README.md
create mode 100644 client/me/sidebar-navigation/package.json
create mode 100644 client/me/sidebar-navigation/sidebar-navigation.jsx
create mode 100644 client/me/sidebar-navigation/style.scss
create mode 100644 client/me/sidebar/index.jsx
create mode 100644 client/me/sidebar/sidebar-item.jsx
create mode 100644 client/me/sidebar/style.scss
create mode 100644 client/me/two-step/index.jsx
create mode 100644 client/me/two-step/style.scss
create mode 100644 client/my-sites/README.md
create mode 100644 client/my-sites/ads/controller.js
create mode 100644 client/my-sites/ads/form-earnings.jsx
create mode 100644 client/my-sites/ads/form-settings.jsx
create mode 100644 client/my-sites/ads/index.js
create mode 100644 client/my-sites/ads/main.jsx
create mode 100644 client/my-sites/ads/style.scss
create mode 100644 client/my-sites/all-sites-icon/README.md
create mode 100644 client/my-sites/all-sites-icon/index.jsx
create mode 100644 client/my-sites/all-sites-icon/package.json
create mode 100644 client/my-sites/all-sites-icon/style.scss
create mode 100644 client/my-sites/all-sites/README.md
create mode 100644 client/my-sites/all-sites/index.jsx
create mode 100644 client/my-sites/all-sites/style.scss
create mode 100644 client/my-sites/category-selector/README.md
create mode 100644 client/my-sites/category-selector/add-category.jsx
create mode 100644 client/my-sites/category-selector/index.jsx
create mode 100644 client/my-sites/category-selector/no-results.jsx
create mode 100644 client/my-sites/category-selector/search.jsx
create mode 100644 client/my-sites/category-selector/search.scss
create mode 100644 client/my-sites/category-selector/style.scss
create mode 100644 client/my-sites/controller.js
create mode 100644 client/my-sites/current-site/README.md
create mode 100644 client/my-sites/current-site/index.jsx
create mode 100644 client/my-sites/current-site/style.scss
create mode 100644 client/my-sites/customize/README.md
create mode 100644 client/my-sites/customize/actions.js
create mode 100644 client/my-sites/customize/controller.js
create mode 100644 client/my-sites/customize/index.js
create mode 100644 client/my-sites/customize/loading-panel.jsx
create mode 100644 client/my-sites/customize/main.jsx
create mode 100644 client/my-sites/customize/package.json
create mode 100644 client/my-sites/customize/style.scss
create mode 100644 client/my-sites/draft/README.md
create mode 100644 client/my-sites/draft/index.jsx
create mode 100644 client/my-sites/draft/style.scss
create mode 100644 client/my-sites/drafts/controller.js
create mode 100644 client/my-sites/drafts/draft-list.jsx
create mode 100644 client/my-sites/drafts/index.js
create mode 100644 client/my-sites/drafts/main.jsx
create mode 100644 client/my-sites/drafts/style.scss
create mode 100644 client/my-sites/exporter/advanced-options.jsx
create mode 100644 client/my-sites/exporter/option-fieldset.jsx
create mode 100644 client/my-sites/exporter/style.scss
create mode 100644 client/my-sites/importer/Makefile
create mode 100644 client/my-sites/importer/author-mapping-item.jsx
create mode 100644 client/my-sites/importer/author-mapping-pane.jsx
create mode 100644 client/my-sites/importer/error-pane.jsx
create mode 100644 client/my-sites/importer/file-importer.jsx
create mode 100644 client/my-sites/importer/importer-ghost.jsx
create mode 100644 client/my-sites/importer/importer-header.jsx
create mode 100644 client/my-sites/importer/importer-icons.jsx
create mode 100644 client/my-sites/importer/importer-medium.jsx
create mode 100644 client/my-sites/importer/importer-squarespace.jsx
create mode 100644 client/my-sites/importer/importer-wordpress.jsx
create mode 100644 client/my-sites/importer/importing-pane.jsx
create mode 100644 client/my-sites/importer/style.scss
create mode 100644 client/my-sites/importer/test/mock-data.js
create mode 100644 client/my-sites/importer/uploading-pane.jsx
create mode 100644 client/my-sites/index.js
create mode 100644 client/my-sites/jetpack-manage-error-page/README.md
create mode 100644 client/my-sites/jetpack-manage-error-page/index.jsx
create mode 100644 client/my-sites/jetpack-manage-error-page/package.json
create mode 100644 client/my-sites/media-library/Makefile
create mode 100644 client/my-sites/media-library/content.jsx
create mode 100644 client/my-sites/media-library/content.scss
create mode 100644 client/my-sites/media-library/drop-zone.jsx
create mode 100644 client/my-sites/media-library/filter-bar.jsx
create mode 100644 client/my-sites/media-library/filter-to-mime-prefix.js
create mode 100644 client/my-sites/media-library/header.jsx
create mode 100644 client/my-sites/media-library/index.jsx
create mode 100644 client/my-sites/media-library/list-item-audio.jsx
create mode 100644 client/my-sites/media-library/list-item-document.jsx
create mode 100644 client/my-sites/media-library/list-item-file-details.jsx
create mode 100644 client/my-sites/media-library/list-item-file-details.scss
create mode 100644 client/my-sites/media-library/list-item-image.jsx
create mode 100644 client/my-sites/media-library/list-item-video.jsx
create mode 100644 client/my-sites/media-library/list-item-video.scss
create mode 100644 client/my-sites/media-library/list-item.jsx
create mode 100644 client/my-sites/media-library/list-item.scss
create mode 100644 client/my-sites/media-library/list-no-content.jsx
create mode 100644 client/my-sites/media-library/list-no-results.jsx
create mode 100644 client/my-sites/media-library/list.jsx
create mode 100644 client/my-sites/media-library/style.scss
create mode 100644 client/my-sites/media-library/test/fixtures.js
create mode 100644 client/my-sites/media-library/test/list.jsx
create mode 100644 client/my-sites/media-library/upload-button.jsx
create mode 100644 client/my-sites/media-library/upload-button.scss
create mode 100644 client/my-sites/media-library/upload-url.jsx
create mode 100644 client/my-sites/media-library/upload-url.scss
create mode 100644 client/my-sites/media/controller.js
create mode 100644 client/my-sites/media/index.js
create mode 100644 client/my-sites/media/main.jsx
create mode 100644 client/my-sites/menus/README.md
create mode 100644 client/my-sites/menus/controller.js
create mode 100644 client/my-sites/menus/index.js
create mode 100644 client/my-sites/menus/item-options/Makefile
create mode 100644 client/my-sites/menus/item-options/README.md
create mode 100644 client/my-sites/menus/item-options/category-options.jsx
create mode 100644 client/my-sites/menus/item-options/empty-placeholder.jsx
create mode 100644 client/my-sites/menus/item-options/loading-placeholder.jsx
create mode 100644 client/my-sites/menus/item-options/option-list.jsx
create mode 100644 client/my-sites/menus/item-options/options.jsx
create mode 100644 client/my-sites/menus/item-options/post-list.jsx
create mode 100644 client/my-sites/menus/item-options/posts.jsx
create mode 100644 client/my-sites/menus/item-options/taxonomy-list.jsx
create mode 100644 client/my-sites/menus/item-options/test/test-posts.jsx
create mode 100644 client/my-sites/menus/loading-placeholder.jsx
create mode 100644 client/my-sites/menus/location-picker.jsx
create mode 100644 client/my-sites/menus/main.jsx
create mode 100644 client/my-sites/menus/menu-delete-button.jsx
create mode 100644 client/my-sites/menus/menu-editable-item.jsx
create mode 100644 client/my-sites/menus/menu-item-drop-target.jsx
create mode 100644 client/my-sites/menus/menu-item-list.jsx
create mode 100644 client/my-sites/menus/menu-item-types.js
create mode 100644 client/my-sites/menus/menu-name.jsx
create mode 100644 client/my-sites/menus/menu-panel-back-button.jsx
create mode 100644 client/my-sites/menus/menu-picker.jsx
create mode 100644 client/my-sites/menus/menu-placeholder.jsx
create mode 100644 client/my-sites/menus/menu-utils.js
create mode 100644 client/my-sites/menus/menu.jsx
create mode 100644 client/my-sites/menus/menus-save-button.jsx
create mode 100644 client/my-sites/navigation/navigation.jsx
create mode 100644 client/my-sites/navigation/package.json
create mode 100644 client/my-sites/no-results/index.jsx
create mode 100644 client/my-sites/no-results/style.scss
create mode 100644 client/my-sites/pages/README.md
create mode 100644 client/my-sites/pages/controller.js
create mode 100644 client/my-sites/pages/helpers.js
create mode 100644 client/my-sites/pages/index.js
create mode 100644 client/my-sites/pages/main.jsx
create mode 100644 client/my-sites/pages/page-list.jsx
create mode 100644 client/my-sites/pages/page.jsx
create mode 100644 client/my-sites/pages/placeholder.jsx
create mode 100644 client/my-sites/pages/style.scss
create mode 100644 client/my-sites/people/README.md
create mode 100644 client/my-sites/people/controller.js
create mode 100644 client/my-sites/people/delete-user/README.md
create mode 100644 client/my-sites/people/delete-user/index.jsx
create mode 100644 client/my-sites/people/delete-user/style.scss
create mode 100644 client/my-sites/people/edit-team-member-form/README.md
create mode 100644 client/my-sites/people/edit-team-member-form/index.jsx
create mode 100644 client/my-sites/people/edit-team-member-form/style.scss
create mode 100644 client/my-sites/people/followers-list/README.md
create mode 100644 client/my-sites/people/followers-list/index.jsx
create mode 100644 client/my-sites/people/index.js
create mode 100644 client/my-sites/people/main.jsx
create mode 100644 client/my-sites/people/people-list-item/README.md
create mode 100644 client/my-sites/people/people-list-item/index.jsx
create mode 100644 client/my-sites/people/people-list-item/style.scss
create mode 100644 client/my-sites/people/people-notices/README.md
create mode 100644 client/my-sites/people/people-notices/index.jsx
create mode 100644 client/my-sites/people/people-notices/style.scss
create mode 100644 client/my-sites/people/people-profile/README.md
create mode 100644 client/my-sites/people/people-profile/index.jsx
create mode 100644 client/my-sites/people/people-profile/style.scss
create mode 100644 client/my-sites/people/people-section-nav/README.md
create mode 100644 client/my-sites/people/people-section-nav/index.jsx
create mode 100644 client/my-sites/people/role-select/README.md
create mode 100644 client/my-sites/people/role-select/index.jsx
create mode 100644 client/my-sites/people/team-list/README.md
create mode 100644 client/my-sites/people/team-list/index.jsx
create mode 100644 client/my-sites/people/viewers-list/README.md
create mode 100644 client/my-sites/people/viewers-list/index.jsx
create mode 100644 client/my-sites/picker/README.md
create mode 100644 client/my-sites/picker/package.json
create mode 100644 client/my-sites/picker/picker.jsx
create mode 100644 client/my-sites/plans/README.md
create mode 100644 client/my-sites/plans/controller.jsx
create mode 100644 client/my-sites/plans/index.js
create mode 100644 client/my-sites/plans/main.jsx
create mode 100644 client/my-sites/plans/plans-select.jsx
create mode 100644 client/my-sites/plans/style.scss
create mode 100644 client/my-sites/plugins/README.md
create mode 100644 client/my-sites/plugins/access-control.js
create mode 100644 client/my-sites/plugins/controller.js
create mode 100644 client/my-sites/plugins/disconnect-jetpack/disconnect-jetpack-button.jsx
create mode 100644 client/my-sites/plugins/disconnect-jetpack/disconnect-jetpack-dialog.jsx
create mode 100644 client/my-sites/plugins/featured-plugins/README.md
create mode 100644 client/my-sites/plugins/featured-plugins/index.jsx
create mode 100644 client/my-sites/plugins/featured-plugins/style.scss
create mode 100644 client/my-sites/plugins/index.js
create mode 100644 client/my-sites/plugins/main.jsx
create mode 100644 client/my-sites/plugins/plugin-action/Makefile
create mode 100644 client/my-sites/plugins/plugin-action/README.md
create mode 100644 client/my-sites/plugins/plugin-action/plugin-action.jsx
create mode 100644 client/my-sites/plugins/plugin-action/style.scss
create mode 100644 client/my-sites/plugins/plugin-action/test/index.jsx
create mode 100644 client/my-sites/plugins/plugin-activate-toggle/Makefile
create mode 100644 client/my-sites/plugins/plugin-activate-toggle/README.md
create mode 100644 client/my-sites/plugins/plugin-activate-toggle/index.jsx
create mode 100644 client/my-sites/plugins/plugin-activate-toggle/style.scss
create mode 100644 client/my-sites/plugins/plugin-activate-toggle/test/fixtures.js
create mode 100644 client/my-sites/plugins/plugin-activate-toggle/test/index.jsx
create mode 100644 client/my-sites/plugins/plugin-activate-toggle/test/mocks/actions.js
create mode 100644 client/my-sites/plugins/plugin-activate-toggle/test/mocks/plugin-action.jsx
create mode 100644 client/my-sites/plugins/plugin-autoupdate-toggle/Makefile
create mode 100644 client/my-sites/plugins/plugin-autoupdate-toggle/README.md
create mode 100644 client/my-sites/plugins/plugin-autoupdate-toggle/index.jsx
create mode 100644 client/my-sites/plugins/plugin-autoupdate-toggle/test/fixtures.js
create mode 100644 client/my-sites/plugins/plugin-autoupdate-toggle/test/index.jsx
create mode 100644 client/my-sites/plugins/plugin-autoupdate-toggle/test/mocks/actions.js
create mode 100644 client/my-sites/plugins/plugin-autoupdate-toggle/test/mocks/plugin-action.jsx
create mode 100644 client/my-sites/plugins/plugin-card-header/README.md
create mode 100644 client/my-sites/plugins/plugin-card-header/index.jsx
create mode 100644 client/my-sites/plugins/plugin-card-header/style.scss
create mode 100644 client/my-sites/plugins/plugin-icon/README.md
create mode 100644 client/my-sites/plugins/plugin-icon/plugin-icon.jsx
create mode 100644 client/my-sites/plugins/plugin-icon/style.scss
create mode 100644 client/my-sites/plugins/plugin-information/README.md
create mode 100644 client/my-sites/plugins/plugin-information/index.jsx
create mode 100644 client/my-sites/plugins/plugin-information/style.scss
create mode 100644 client/my-sites/plugins/plugin-install-button/README.md
create mode 100644 client/my-sites/plugins/plugin-install-button/index.jsx
create mode 100644 client/my-sites/plugins/plugin-install-button/style.scss
create mode 100644 client/my-sites/plugins/plugin-item/README.md
create mode 100644 client/my-sites/plugins/plugin-item/plugin-item.jsx
create mode 100644 client/my-sites/plugins/plugin-item/style.scss
create mode 100644 client/my-sites/plugins/plugin-meta/README.md
create mode 100644 client/my-sites/plugins/plugin-meta/index.jsx
create mode 100644 client/my-sites/plugins/plugin-meta/style.scss
create mode 100644 client/my-sites/plugins/plugin-ratings/README.md
create mode 100644 client/my-sites/plugins/plugin-ratings/index.jsx
create mode 100644 client/my-sites/plugins/plugin-ratings/style.scss
create mode 100644 client/my-sites/plugins/plugin-remove-button/README.md
create mode 100644 client/my-sites/plugins/plugin-remove-button/index.jsx
create mode 100644 client/my-sites/plugins/plugin-remove-button/style.scss
create mode 100644 client/my-sites/plugins/plugin-sections/README.md
create mode 100644 client/my-sites/plugins/plugin-sections/index.jsx
create mode 100644 client/my-sites/plugins/plugin-sections/style.scss
create mode 100644 client/my-sites/plugins/plugin-site-business/README.md
create mode 100644 client/my-sites/plugins/plugin-site-business/index.jsx
create mode 100644 client/my-sites/plugins/plugin-site-business/style.scss
create mode 100644 client/my-sites/plugins/plugin-site-disabled-manage/README.md
create mode 100644 client/my-sites/plugins/plugin-site-disabled-manage/index.jsx
create mode 100644 client/my-sites/plugins/plugin-site-disabled-manage/style.scss
create mode 100644 client/my-sites/plugins/plugin-site-jetpack/README.md
create mode 100644 client/my-sites/plugins/plugin-site-jetpack/index.jsx
create mode 100644 client/my-sites/plugins/plugin-site-jetpack/style.scss
create mode 100644 client/my-sites/plugins/plugin-site-list/README.md
create mode 100644 client/my-sites/plugins/plugin-site-list/index.jsx
create mode 100644 client/my-sites/plugins/plugin-site-network/README.md
create mode 100644 client/my-sites/plugins/plugin-site-network/index.jsx
create mode 100644 client/my-sites/plugins/plugin-site-network/style.scss
create mode 100644 client/my-sites/plugins/plugin-site-update-indicator/README.md
create mode 100644 client/my-sites/plugins/plugin-site-update-indicator/index.jsx
create mode 100644 client/my-sites/plugins/plugin-site-update-indicator/style.scss
create mode 100644 client/my-sites/plugins/plugin-site/README.md
create mode 100644 client/my-sites/plugins/plugin-site/plugin-site.jsx
create mode 100644 client/my-sites/plugins/plugin-toggle/README.md
create mode 100644 client/my-sites/plugins/plugin-toggle/plugin-toggle.jsx
create mode 100644 client/my-sites/plugins/plugin-version/README.md
create mode 100644 client/my-sites/plugins/plugin-version/index.jsx
create mode 100644 client/my-sites/plugins/plugin-version/style.scss
create mode 100644 client/my-sites/plugins/plugin.jsx
create mode 100644 client/my-sites/plugins/plugins-browser-item/README.md
create mode 100644 client/my-sites/plugins/plugins-browser-item/index.jsx
create mode 100644 client/my-sites/plugins/plugins-browser-item/style.scss
create mode 100644 client/my-sites/plugins/plugins-browser-list/README.md
create mode 100644 client/my-sites/plugins/plugins-browser-list/index.jsx
create mode 100644 client/my-sites/plugins/plugins-browser-list/style.scss
create mode 100644 client/my-sites/plugins/plugins-browser/README.md
create mode 100644 client/my-sites/plugins/plugins-browser/index.jsx
create mode 100644 client/my-sites/plugins/plugins-browser/style.scss
create mode 100644 client/my-sites/plugins/plugins-manage-mixin.js
create mode 100644 client/my-sites/plugins/style.scss
create mode 100644 client/my-sites/post-relative-time-status/README.md
create mode 100644 client/my-sites/post-relative-time-status/index.jsx
create mode 100644 client/my-sites/post-selector/README.md
create mode 100644 client/my-sites/post-selector/index.jsx
create mode 100644 client/my-sites/post-selector/no-results.jsx
create mode 100644 client/my-sites/post-selector/search.jsx
create mode 100644 client/my-sites/post-selector/search.scss
create mode 100644 client/my-sites/post-selector/selector.jsx
create mode 100644 client/my-sites/post-selector/style.scss
create mode 100644 client/my-sites/post-trends/README.md
create mode 100644 client/my-sites/post-trends/day.jsx
create mode 100644 client/my-sites/post-trends/index.jsx
create mode 100644 client/my-sites/post-trends/month.jsx
create mode 100644 client/my-sites/post-trends/style.scss
create mode 100644 client/my-sites/post-trends/week.jsx
create mode 100644 client/my-sites/post/README.md
create mode 100644 client/my-sites/post/post-image/index.jsx
create mode 100644 client/my-sites/post/post-image/style.scss
create mode 100644 client/my-sites/posts/README.md
create mode 100644 client/my-sites/posts/controller.js
create mode 100644 client/my-sites/posts/index.js
create mode 100644 client/my-sites/posts/main.jsx
create mode 100644 client/my-sites/posts/post-controls.jsx
create mode 100644 client/my-sites/posts/post-header.jsx
create mode 100644 client/my-sites/posts/post-list.jsx
create mode 100644 client/my-sites/posts/post-placeholder.jsx
create mode 100644 client/my-sites/posts/post-total-views.jsx
create mode 100644 client/my-sites/posts/post.jsx
create mode 100644 client/my-sites/posts/posts-navigation.jsx
create mode 100644 client/my-sites/sharing/README.md
create mode 100644 client/my-sites/sharing/buttons/README.md
create mode 100644 client/my-sites/sharing/buttons/appearance.jsx
create mode 100644 client/my-sites/sharing/buttons/buttons.jsx
create mode 100644 client/my-sites/sharing/buttons/label-editor.jsx
create mode 100644 client/my-sites/sharing/buttons/options.jsx
create mode 100644 client/my-sites/sharing/buttons/preview-action.jsx
create mode 100644 client/my-sites/sharing/buttons/preview-button.jsx
create mode 100644 client/my-sites/sharing/buttons/preview-buttons.jsx
create mode 100644 client/my-sites/sharing/buttons/preview-placeholder.jsx
create mode 100644 client/my-sites/sharing/buttons/preview-widget.js
create mode 100644 client/my-sites/sharing/buttons/preview.jsx
create mode 100644 client/my-sites/sharing/buttons/style.jsx
create mode 100644 client/my-sites/sharing/buttons/tray.jsx
create mode 100644 client/my-sites/sharing/connections/README.md
create mode 100644 client/my-sites/sharing/connections/account-dialog-account.jsx
create mode 100644 client/my-sites/sharing/connections/account-dialog-account.scss
create mode 100644 client/my-sites/sharing/connections/account-dialog.jsx
create mode 100644 client/my-sites/sharing/connections/account-dialog.scss
create mode 100644 client/my-sites/sharing/connections/connection.jsx
create mode 100644 client/my-sites/sharing/connections/connections.jsx
create mode 100644 client/my-sites/sharing/connections/service-action.jsx
create mode 100644 client/my-sites/sharing/connections/service-connected-accounts.jsx
create mode 100644 client/my-sites/sharing/connections/service-connections.js
create mode 100644 client/my-sites/sharing/connections/service-description.jsx
create mode 100644 client/my-sites/sharing/connections/service-example.jsx
create mode 100644 client/my-sites/sharing/connections/service-examples.jsx
create mode 100644 client/my-sites/sharing/connections/service-placeholder.jsx
create mode 100644 client/my-sites/sharing/connections/service-tip.jsx
create mode 100644 client/my-sites/sharing/connections/service.jsx
create mode 100644 client/my-sites/sharing/connections/services-group.jsx
create mode 100644 client/my-sites/sharing/connections/services-group.scss
create mode 100644 client/my-sites/sharing/connections/services/eventbrite.js
create mode 100644 client/my-sites/sharing/connections/services/index.js
create mode 100644 client/my-sites/sharing/controller.js
create mode 100644 client/my-sites/sharing/index.js
create mode 100644 client/my-sites/sharing/main.jsx
create mode 100644 client/my-sites/sidebar-navigation/README.md
create mode 100644 client/my-sites/sidebar-navigation/package.json
create mode 100644 client/my-sites/sidebar-navigation/sidebar-navigation.jsx
create mode 100644 client/my-sites/sidebar-navigation/style.scss
create mode 100644 client/my-sites/sidebar/package.json
create mode 100644 client/my-sites/sidebar/publish-menu.jsx
create mode 100644 client/my-sites/sidebar/sidebar-menu-item.jsx
create mode 100644 client/my-sites/sidebar/sidebar.jsx
create mode 100644 client/my-sites/site-indicator/README.md
create mode 100644 client/my-sites/site-indicator/package.json
create mode 100644 client/my-sites/site-indicator/site-indicator.jsx
create mode 100644 client/my-sites/site-indicator/style.scss
create mode 100644 client/my-sites/site-settings/README.md
create mode 100644 client/my-sites/site-settings/action-panel/body.jsx
create mode 100644 client/my-sites/site-settings/action-panel/figure.jsx
create mode 100644 client/my-sites/site-settings/action-panel/footer.jsx
create mode 100644 client/my-sites/site-settings/action-panel/index.jsx
create mode 100644 client/my-sites/site-settings/action-panel/style.scss
create mode 100644 client/my-sites/site-settings/action-panel/title.jsx
create mode 100644 client/my-sites/site-settings/controller.js
create mode 100644 client/my-sites/site-settings/delete-site-options/index.jsx
create mode 100644 client/my-sites/site-settings/delete-site-options/style.scss
create mode 100644 client/my-sites/site-settings/delete-site/index.jsx
create mode 100644 client/my-sites/site-settings/delete-site/style.scss
create mode 100644 client/my-sites/site-settings/form-analytics.jsx
create mode 100644 client/my-sites/site-settings/form-base.js
create mode 100644 client/my-sites/site-settings/form-discussion.jsx
create mode 100644 client/my-sites/site-settings/form-general.jsx
create mode 100644 client/my-sites/site-settings/form-jetpack-monitor.jsx
create mode 100644 client/my-sites/site-settings/form-jetpack-protect.jsx
create mode 100644 client/my-sites/site-settings/form-jetpack-scan.jsx
create mode 100644 client/my-sites/site-settings/form-writing.jsx
create mode 100644 client/my-sites/site-settings/index.js
create mode 100644 client/my-sites/site-settings/main.jsx
create mode 100644 client/my-sites/site-settings/press-this-link.jsx
create mode 100644 client/my-sites/site-settings/related-content-preview.jsx
create mode 100644 client/my-sites/site-settings/section-analytics.jsx
create mode 100644 client/my-sites/site-settings/section-discussion.jsx
create mode 100644 client/my-sites/site-settings/section-export.jsx
create mode 100644 client/my-sites/site-settings/section-general.jsx
create mode 100644 client/my-sites/site-settings/section-import.jsx
create mode 100644 client/my-sites/site-settings/section-security.jsx
create mode 100644 client/my-sites/site-settings/section-writing.jsx
create mode 100644 client/my-sites/site-settings/settings-card-footer/index.jsx
create mode 100644 client/my-sites/site-settings/settings-card-footer/style.scss
create mode 100644 client/my-sites/site-settings/start-over/index.jsx
create mode 100644 client/my-sites/site/README.md
create mode 100644 client/my-sites/site/index.jsx
create mode 100644 client/my-sites/site/placeholder.jsx
create mode 100644 client/my-sites/site/style.scss
create mode 100644 client/my-sites/sites/README.md
create mode 100644 client/my-sites/sites/package.json
create mode 100644 client/my-sites/sites/site-card.jsx
create mode 100644 client/my-sites/sites/sites.jsx
create mode 100644 client/my-sites/stats/README.md
create mode 100644 client/my-sites/stats/action-follow.jsx
create mode 100644 client/my-sites/stats/action-link.jsx
create mode 100644 client/my-sites/stats/action-page.jsx
create mode 100644 client/my-sites/stats/action-spam.jsx
create mode 100644 client/my-sites/stats/all-time/index.jsx
create mode 100644 client/my-sites/stats/all-time/style.scss
create mode 100644 client/my-sites/stats/controller.js
create mode 100644 client/my-sites/stats/download-csv/README.md
create mode 100644 client/my-sites/stats/download-csv/index.jsx
create mode 100644 client/my-sites/stats/follows.jsx
create mode 100644 client/my-sites/stats/geochart/README.md
create mode 100644 client/my-sites/stats/geochart/index.jsx
create mode 100644 client/my-sites/stats/geochart/style.scss
create mode 100644 client/my-sites/stats/index.js
create mode 100644 client/my-sites/stats/info-panel.jsx
create mode 100644 client/my-sites/stats/insights.jsx
create mode 100644 client/my-sites/stats/mixin-skeleton.js
create mode 100644 client/my-sites/stats/mixin-toggle.js
create mode 100644 client/my-sites/stats/module-chart-tabs.jsx
create mode 100644 client/my-sites/stats/module-comments.jsx
create mode 100644 client/my-sites/stats/module-countries.jsx
create mode 100644 client/my-sites/stats/module-date-picker.jsx
create mode 100644 client/my-sites/stats/module-error.jsx
create mode 100644 client/my-sites/stats/module-followers-page.jsx
create mode 100644 client/my-sites/stats/module-followers.jsx
create mode 100644 client/my-sites/stats/module-post-months.jsx
create mode 100644 client/my-sites/stats/module-post-weeks.jsx
create mode 100644 client/my-sites/stats/module-site-overview-placeholder.jsx
create mode 100644 client/my-sites/stats/module-summary-chart.jsx
create mode 100644 client/my-sites/stats/module-tab.jsx
create mode 100644 client/my-sites/stats/module-tabs.jsx
create mode 100644 client/my-sites/stats/module-video-details.jsx
create mode 100644 client/my-sites/stats/most-popular/index.jsx
create mode 100644 client/my-sites/stats/most-popular/style.scss
create mode 100644 client/my-sites/stats/nux/data.js
create mode 100644 client/my-sites/stats/nux/insights.jsx
create mode 100644 client/my-sites/stats/nux/site.jsx
create mode 100644 client/my-sites/stats/nux/style.scss
create mode 100644 client/my-sites/stats/overview/README.md
create mode 100644 client/my-sites/stats/overview/index.jsx
create mode 100644 client/my-sites/stats/overview/style.scss
create mode 100644 client/my-sites/stats/pagination/README.md
create mode 100644 client/my-sites/stats/pagination/index.jsx
create mode 100644 client/my-sites/stats/pagination/pagination-page.jsx
create mode 100644 client/my-sites/stats/pagination/style.scss
create mode 100644 client/my-sites/stats/post-performance/README.md
create mode 100644 client/my-sites/stats/post-performance/index.jsx
create mode 100644 client/my-sites/stats/post.jsx
create mode 100644 client/my-sites/stats/site.jsx
create mode 100644 client/my-sites/stats/stats-list-item.jsx
create mode 100644 client/my-sites/stats/stats-list.jsx
create mode 100644 client/my-sites/stats/stats-module.jsx
create mode 100644 client/my-sites/stats/stats-navigation.jsx
create mode 100644 client/my-sites/stats/stats-overview.jsx
create mode 100644 client/my-sites/stats/stats-strings.js
create mode 100644 client/my-sites/stats/summary.jsx
create mode 100644 client/my-sites/upgrades/README.md
create mode 100644 client/my-sites/upgrades/cart/Makefile
create mode 100644 client/my-sites/upgrades/cart/README.md
create mode 100644 client/my-sites/upgrades/cart/cart-body.jsx
create mode 100644 client/my-sites/upgrades/cart/cart-buttons.jsx
create mode 100644 client/my-sites/upgrades/cart/cart-coupon.jsx
create mode 100644 client/my-sites/upgrades/cart/cart-empty.jsx
create mode 100644 client/my-sites/upgrades/cart/cart-item.jsx
create mode 100644 client/my-sites/upgrades/cart/cart-items.jsx
create mode 100644 client/my-sites/upgrades/cart/cart-messages-mixin.jsx
create mode 100644 client/my-sites/upgrades/cart/cart-plan-ad.jsx
create mode 100644 client/my-sites/upgrades/cart/cart-summary-bar.jsx
create mode 100644 client/my-sites/upgrades/cart/cart-total.jsx
create mode 100644 client/my-sites/upgrades/cart/popover-cart.jsx
create mode 100644 client/my-sites/upgrades/cart/secondary-cart.jsx
create mode 100644 client/my-sites/upgrades/cart/style.scss
create mode 100644 client/my-sites/upgrades/cart/test/test-cart-buttons.jsx
create mode 100644 client/my-sites/upgrades/checkout/README.md
create mode 100644 client/my-sites/upgrades/checkout/checkout.jsx
create mode 100644 client/my-sites/upgrades/checkout/credit-card-payment-box.jsx
create mode 100644 client/my-sites/upgrades/checkout/credit-card-selector.jsx
create mode 100644 client/my-sites/upgrades/checkout/credits-payment-box.jsx
create mode 100644 client/my-sites/upgrades/checkout/domain-details-form.jsx
create mode 100644 client/my-sites/upgrades/checkout/free-cart-payment-box.jsx
create mode 100644 client/my-sites/upgrades/checkout/new-card-form.jsx
create mode 100644 client/my-sites/upgrades/checkout/package.json
create mode 100644 client/my-sites/upgrades/checkout/pay-button.jsx
create mode 100644 client/my-sites/upgrades/checkout/payment-box.jsx
create mode 100644 client/my-sites/upgrades/checkout/paypal-payment-box.jsx
create mode 100644 client/my-sites/upgrades/checkout/privacy-protection-dialog.jsx
create mode 100644 client/my-sites/upgrades/checkout/privacy-protection-example.jsx
create mode 100644 client/my-sites/upgrades/checkout/privacy-protection.jsx
create mode 100644 client/my-sites/upgrades/checkout/secure-payment-form.jsx
create mode 100644 client/my-sites/upgrades/checkout/stored-card.jsx
create mode 100644 client/my-sites/upgrades/checkout/stored-card.scss
create mode 100644 client/my-sites/upgrades/checkout/subscription-text.jsx
create mode 100644 client/my-sites/upgrades/checkout/subscription-text.scss
create mode 100644 client/my-sites/upgrades/checkout/supporting-text.jsx
create mode 100644 client/my-sites/upgrades/checkout/terms-of-service.jsx
create mode 100644 client/my-sites/upgrades/checkout/thank-you.jsx
create mode 100644 client/my-sites/upgrades/checkout/transaction-steps-mixin.jsx
create mode 100644 client/my-sites/upgrades/components/README.md
create mode 100644 client/my-sites/upgrades/components/domain-warnings/Makefile
create mode 100644 client/my-sites/upgrades/components/domain-warnings/index.jsx
create mode 100644 client/my-sites/upgrades/components/domain-warnings/test/test-domain-warnings.jsx
create mode 100644 client/my-sites/upgrades/components/form/country-select.jsx
create mode 100644 client/my-sites/upgrades/components/form/focus-mixin.js
create mode 100644 client/my-sites/upgrades/components/form/hidden-input.jsx
create mode 100644 client/my-sites/upgrades/components/form/input.jsx
create mode 100644 client/my-sites/upgrades/components/form/state-select.jsx
create mode 100644 client/my-sites/upgrades/controller.jsx
create mode 100644 client/my-sites/upgrades/domain-management/README.md
create mode 100644 client/my-sites/upgrades/domain-management/add-google-apps/add-email-addresses-card.jsx
create mode 100644 client/my-sites/upgrades/domain-management/add-google-apps/domains-select.jsx
create mode 100644 client/my-sites/upgrades/domain-management/add-google-apps/index.jsx
create mode 100644 client/my-sites/upgrades/domain-management/components/domain/main-placeholder.jsx
create mode 100644 client/my-sites/upgrades/domain-management/components/domain/primary-flag.jsx
create mode 100644 client/my-sites/upgrades/domain-management/components/form-footer/index.jsx
create mode 100644 client/my-sites/upgrades/domain-management/components/header/index.jsx
create mode 100644 client/my-sites/upgrades/domain-management/components/icann-verification.jsx
create mode 100644 client/my-sites/upgrades/domain-management/contacts-privacy/card.jsx
create mode 100644 client/my-sites/upgrades/domain-management/contacts-privacy/contact-display.jsx
create mode 100644 client/my-sites/upgrades/domain-management/contacts-privacy/index.jsx
create mode 100644 client/my-sites/upgrades/domain-management/controller.jsx
create mode 100644 client/my-sites/upgrades/domain-management/dns/a-record.jsx
create mode 100644 client/my-sites/upgrades/domain-management/dns/cname-record.jsx
create mode 100644 client/my-sites/upgrades/domain-management/dns/dns-add-new.jsx
create mode 100644 client/my-sites/upgrades/domain-management/dns/dns-details.jsx
create mode 100644 client/my-sites/upgrades/domain-management/dns/dns-list.jsx
create mode 100644 client/my-sites/upgrades/domain-management/dns/dns-record.jsx
create mode 100644 client/my-sites/upgrades/domain-management/dns/index.jsx
create mode 100644 client/my-sites/upgrades/domain-management/dns/mx-record.jsx
create mode 100644 client/my-sites/upgrades/domain-management/dns/srv-record.jsx
create mode 100644 client/my-sites/upgrades/domain-management/dns/txt-record.jsx
create mode 100644 client/my-sites/upgrades/domain-management/domain-management.jsx
create mode 100644 client/my-sites/upgrades/domain-management/edit-contact-info/form-card.jsx
create mode 100644 client/my-sites/upgrades/domain-management/edit-contact-info/index.jsx
create mode 100644 client/my-sites/upgrades/domain-management/edit-contact-info/privacy-enabled-card.jsx
create mode 100644 client/my-sites/upgrades/domain-management/edit/card/header/index.jsx
create mode 100644 client/my-sites/upgrades/domain-management/edit/card/header/primary-domain-button.jsx
create mode 100644 client/my-sites/upgrades/domain-management/edit/card/property.jsx
create mode 100644 client/my-sites/upgrades/domain-management/edit/card/subscription-settings.jsx
create mode 100644 client/my-sites/upgrades/domain-management/edit/index.jsx
create mode 100644 client/my-sites/upgrades/domain-management/edit/mapped-domain.jsx
create mode 100644 client/my-sites/upgrades/domain-management/edit/registered-domain.jsx
create mode 100644 client/my-sites/upgrades/domain-management/edit/site-redirect.jsx
create mode 100644 client/my-sites/upgrades/domain-management/edit/wpcom-domain.jsx
create mode 100644 client/my-sites/upgrades/domain-management/email-forwarding/email-forwarding-add-new.jsx
create mode 100644 client/my-sites/upgrades/domain-management/email-forwarding/email-forwarding-details.jsx
create mode 100644 client/my-sites/upgrades/domain-management/email-forwarding/email-forwarding-item.jsx
create mode 100644 client/my-sites/upgrades/domain-management/email-forwarding/email-forwarding-limit.jsx
create mode 100644 client/my-sites/upgrades/domain-management/email-forwarding/email-forwarding-list.jsx
create mode 100644 client/my-sites/upgrades/domain-management/email-forwarding/index.jsx
create mode 100644 client/my-sites/upgrades/domain-management/email/add-google-apps-card.jsx
create mode 100644 client/my-sites/upgrades/domain-management/email/google-apps-users-card.jsx
create mode 100644 client/my-sites/upgrades/domain-management/email/index.jsx
create mode 100644 client/my-sites/upgrades/domain-management/list/index.jsx
create mode 100644 client/my-sites/upgrades/domain-management/list/item-placeholder.jsx
create mode 100644 client/my-sites/upgrades/domain-management/list/item.jsx
create mode 100644 client/my-sites/upgrades/domain-management/name-servers/custom-nameservers-form.jsx
create mode 100644 client/my-sites/upgrades/domain-management/name-servers/custom-nameservers-row.jsx
create mode 100644 client/my-sites/upgrades/domain-management/name-servers/icann-verification-card.jsx
create mode 100644 client/my-sites/upgrades/domain-management/name-servers/index.jsx
create mode 100644 client/my-sites/upgrades/domain-management/name-servers/wpcom-nameservers-toggle.jsx
create mode 100644 client/my-sites/upgrades/domain-management/package.json
create mode 100644 client/my-sites/upgrades/domain-management/primary-domain/index.jsx
create mode 100644 client/my-sites/upgrades/domain-management/privacy-protection/card/add-button.jsx
create mode 100644 client/my-sites/upgrades/domain-management/privacy-protection/card/content.jsx
create mode 100644 client/my-sites/upgrades/domain-management/privacy-protection/card/header.jsx
create mode 100644 client/my-sites/upgrades/domain-management/privacy-protection/index.jsx
create mode 100644 client/my-sites/upgrades/domain-management/site-redirect/index.jsx
create mode 100644 client/my-sites/upgrades/domain-management/site-redirect/notice.jsx
create mode 100644 client/my-sites/upgrades/domain-management/style.scss
create mode 100644 client/my-sites/upgrades/domain-management/transfer/enable-domain-locking-notice.jsx
create mode 100644 client/my-sites/upgrades/domain-management/transfer/enable-privacy-notice.jsx
create mode 100644 client/my-sites/upgrades/domain-management/transfer/icann-verification-notice.jsx
create mode 100644 client/my-sites/upgrades/domain-management/transfer/index.jsx
create mode 100644 client/my-sites/upgrades/domain-management/transfer/pending-transfer-notice.jsx
create mode 100644 client/my-sites/upgrades/domain-management/transfer/request-transfer-code.jsx
create mode 100644 client/my-sites/upgrades/domain-management/transfer/transfer-prohibited-notice.jsx
create mode 100644 client/my-sites/upgrades/domain-search/Makefile
create mode 100644 client/my-sites/upgrades/domain-search/README.md
create mode 100644 client/my-sites/upgrades/domain-search/domain-search.jsx
create mode 100644 client/my-sites/upgrades/domain-search/package.json
create mode 100644 client/my-sites/upgrades/domain-search/site-redirect-step.jsx
create mode 100644 client/my-sites/upgrades/domain-search/site-redirect.jsx
create mode 100644 client/my-sites/upgrades/domain-search/test/test-domain-suggestion.jsx
create mode 100644 client/my-sites/upgrades/index.js
create mode 100644 client/my-sites/upgrades/navigation.jsx
create mode 100644 client/my-sites/upgrades/paths.js
create mode 100644 client/my-sites/welcome/README.md
create mode 100644 client/my-sites/welcome/package.json
create mode 100644 client/my-sites/welcome/welcome.jsx
create mode 100644 client/notices/README.md
create mode 100644 client/notices/arrow-link.jsx
create mode 100644 client/notices/delete-site-notices.jsx
create mode 100644 client/notices/index.js
create mode 100644 client/notices/notice.jsx
create mode 100644 client/notices/notices-list.jsx
create mode 100644 client/notices/simple-notice.jsx
create mode 100644 client/notices/site-notice.jsx
create mode 100644 client/notices/style.scss
create mode 100644 client/notices/validation-error-list.jsx
create mode 100644 client/notifications/README.md
create mode 100644 client/notifications/index.jsx
create mode 100644 client/nux-welcome/README.md
create mode 100644 client/nux-welcome/index.js
create mode 100644 client/nux-welcome/welcome-message.jsx
create mode 100644 client/post-editor/Makefile
create mode 100644 client/post-editor/README.md
create mode 100644 client/post-editor/controller.js
create mode 100644 client/post-editor/drafts-button/index.jsx
create mode 100644 client/post-editor/drafts-button/style.scss
create mode 100644 client/post-editor/edit-post-status/index.jsx
create mode 100644 client/post-editor/edit-post-status/style.scss
create mode 100644 client/post-editor/editor-action-bar/index.jsx
create mode 100644 client/post-editor/editor-action-bar/style.scss
create mode 100644 client/post-editor/editor-author/index.jsx
create mode 100644 client/post-editor/editor-author/style.scss
create mode 100644 client/post-editor/editor-categories/index.jsx
create mode 100644 client/post-editor/editor-categories/style.scss
create mode 100644 client/post-editor/editor-delete-post/index.jsx
create mode 100644 client/post-editor/editor-delete-post/style.scss
create mode 100644 client/post-editor/editor-discussion/Makefile
create mode 100644 client/post-editor/editor-discussion/index.jsx
create mode 100644 client/post-editor/editor-discussion/style.scss
create mode 100644 client/post-editor/editor-discussion/test/index.jsx
create mode 100644 client/post-editor/editor-drawer-well/index.jsx
create mode 100644 client/post-editor/editor-drawer-well/style.scss
create mode 100644 client/post-editor/editor-drawer/index.jsx
create mode 100644 client/post-editor/editor-drawer/style.scss
create mode 100644 client/post-editor/editor-featured-image/index.jsx
create mode 100644 client/post-editor/editor-featured-image/preview-container.jsx
create mode 100644 client/post-editor/editor-featured-image/preview.jsx
create mode 100644 client/post-editor/editor-featured-image/style.scss
create mode 100644 client/post-editor/editor-fieldset/README.md
create mode 100644 client/post-editor/editor-fieldset/index.jsx
create mode 100644 client/post-editor/editor-fieldset/style.scss
create mode 100644 client/post-editor/editor-ground-control/Makefile
create mode 100644 client/post-editor/editor-ground-control/index.jsx
create mode 100644 client/post-editor/editor-ground-control/style.scss
create mode 100644 client/post-editor/editor-ground-control/test/index.jsx
create mode 100644 client/post-editor/editor-location/index.jsx
create mode 100644 client/post-editor/editor-location/search-result.jsx
create mode 100644 client/post-editor/editor-location/search.jsx
create mode 100644 client/post-editor/editor-location/style.scss
create mode 100644 client/post-editor/editor-mobile-navigation/index.jsx
create mode 100644 client/post-editor/editor-mobile-navigation/style.scss
create mode 100644 client/post-editor/editor-more-options/slug.jsx
create mode 100644 client/post-editor/editor-more-options/style.scss
create mode 100644 client/post-editor/editor-page-order/index.jsx
create mode 100644 client/post-editor/editor-page-order/style.scss
create mode 100644 client/post-editor/editor-page-parent/index.jsx
create mode 100644 client/post-editor/editor-page-parent/style.scss
create mode 100644 client/post-editor/editor-page-slug/index.jsx
create mode 100644 client/post-editor/editor-page-templates/index.jsx
create mode 100644 client/post-editor/editor-permalink/index.jsx
create mode 100644 client/post-editor/editor-permalink/style.scss
create mode 100644 client/post-editor/editor-post-formats/accordion.jsx
create mode 100644 client/post-editor/editor-post-formats/index.jsx
create mode 100644 client/post-editor/editor-post-formats/style.scss
create mode 100644 client/post-editor/editor-post-type/index.jsx
create mode 100644 client/post-editor/editor-post-type/style.scss
create mode 100644 client/post-editor/editor-preview/index.jsx
create mode 100644 client/post-editor/editor-revisions/index.jsx
create mode 100644 client/post-editor/editor-revisions/style.scss
create mode 100644 client/post-editor/editor-sharing/Makefile
create mode 100644 client/post-editor/editor-sharing/accordion.jsx
create mode 100644 client/post-editor/editor-sharing/index.jsx
create mode 100644 client/post-editor/editor-sharing/publicize-connection.jsx
create mode 100644 client/post-editor/editor-sharing/publicize-message.jsx
create mode 100644 client/post-editor/editor-sharing/publicize-message.scss
create mode 100644 client/post-editor/editor-sharing/publicize-options.jsx
create mode 100644 client/post-editor/editor-sharing/publicize-options.scss
create mode 100644 client/post-editor/editor-sharing/publicize-services.jsx
create mode 100644 client/post-editor/editor-sharing/publicize-services.scss
create mode 100644 client/post-editor/editor-sharing/sharing-like-options.jsx
create mode 100644 client/post-editor/editor-sharing/style.scss
create mode 100644 client/post-editor/editor-sharing/test/index.js
create mode 100644 client/post-editor/editor-sharing/test/specs/publicize-connection.jsx
create mode 100644 client/post-editor/editor-slug/index.jsx
create mode 100644 client/post-editor/editor-slug/style.scss
create mode 100644 client/post-editor/editor-tags/index.jsx
create mode 100644 client/post-editor/editor-taxonomies/Makefile
create mode 100644 client/post-editor/editor-taxonomies/accordion.jsx
create mode 100644 client/post-editor/editor-taxonomies/test/accordion.jsx
create mode 100644 client/post-editor/editor-title/container.jsx
create mode 100644 client/post-editor/editor-title/index.jsx
create mode 100644 client/post-editor/editor-title/style.scss
create mode 100644 client/post-editor/editor-visibility/index.jsx
create mode 100644 client/post-editor/editor-visibility/style.scss
create mode 100644 client/post-editor/index.js
create mode 100644 client/post-editor/invalid-url-dialog.jsx
create mode 100644 client/post-editor/media-modal/Makefile
create mode 100644 client/post-editor/media-modal/back-to-library.jsx
create mode 100644 client/post-editor/media-modal/constants.js
create mode 100644 client/post-editor/media-modal/detail/_style.scss
create mode 100644 client/post-editor/media-modal/detail/detail-fields.jsx
create mode 100644 client/post-editor/media-modal/detail/detail-file-info.jsx
create mode 100644 client/post-editor/media-modal/detail/detail-item.jsx
create mode 100644 client/post-editor/media-modal/detail/detail-preview-audio.jsx
create mode 100644 client/post-editor/media-modal/detail/detail-preview-document.jsx
create mode 100644 client/post-editor/media-modal/detail/detail-preview-image.jsx
create mode 100644 client/post-editor/media-modal/detail/detail-preview-video.jsx
create mode 100644 client/post-editor/media-modal/detail/detail-preview-videopress.jsx
create mode 100644 client/post-editor/media-modal/detail/detail-title.jsx
create mode 100644 client/post-editor/media-modal/detail/index.jsx
create mode 100644 client/post-editor/media-modal/fieldset.jsx
create mode 100644 client/post-editor/media-modal/fieldset.scss
create mode 100644 client/post-editor/media-modal/gallery-help-container.jsx
create mode 100644 client/post-editor/media-modal/gallery-help.jsx
create mode 100644 client/post-editor/media-modal/gallery/caption.jsx
create mode 100644 client/post-editor/media-modal/gallery/drop-zone.jsx
create mode 100644 client/post-editor/media-modal/gallery/edit-item.jsx
create mode 100644 client/post-editor/media-modal/gallery/edit.jsx
create mode 100644 client/post-editor/media-modal/gallery/fields.jsx
create mode 100644 client/post-editor/media-modal/gallery/index.jsx
create mode 100644 client/post-editor/media-modal/gallery/preview.jsx
create mode 100644 client/post-editor/media-modal/gallery/remove-button.jsx
create mode 100644 client/post-editor/media-modal/gallery/style.scss
create mode 100644 client/post-editor/media-modal/index.jsx
create mode 100644 client/post-editor/media-modal/index.scss
create mode 100644 client/post-editor/media-modal/markup.js
create mode 100644 client/post-editor/media-modal/preload-image.js
create mode 100644 client/post-editor/media-modal/secondary-actions.jsx
create mode 100644 client/post-editor/media-modal/style.scss
create mode 100644 client/post-editor/media-modal/test/index.js
create mode 100644 client/post-editor/media-modal/test/specs/index.jsx
create mode 100644 client/post-editor/media-modal/test/specs/markup.js
create mode 100644 client/post-editor/media-modal/test/specs/preload-image.js
create mode 100644 client/post-editor/post-editor.jsx
create mode 100644 client/post-editor/restore-post-dialog.jsx
create mode 100644 client/post-editor/status-label.jsx
create mode 100644 client/post-editor/style.scss
create mode 100644 client/post-editor/test/post-editor.jsx
create mode 100644 client/reader/README.md
create mode 100644 client/reader/_style.scss
create mode 100644 client/reader/comments/README.md
create mode 100644 client/reader/comments/comment-likes.jsx
create mode 100644 client/reader/comments/form.jsx
create mode 100644 client/reader/comments/helper.jsx
create mode 100644 client/reader/comments/index.jsx
create mode 100644 client/reader/comments/style.scss
create mode 100644 client/reader/controller.js
create mode 100644 client/reader/discover/README.md
create mode 100644 client/reader/discover/_style.scss
create mode 100644 client/reader/discover/helper.jsx
create mode 100644 client/reader/discover/post-attribution.jsx
create mode 100644 client/reader/discover/site-attribution.jsx
create mode 100644 client/reader/discover/visit-link.jsx
create mode 100644 client/reader/feed-error/README.md
create mode 100644 client/reader/feed-error/index.jsx
create mode 100644 client/reader/feed-header/README.md
create mode 100644 client/reader/feed-header/index.jsx
create mode 100644 client/reader/feed-header/style.scss
create mode 100644 client/reader/feed-stream/README.md
create mode 100644 client/reader/feed-stream/empty.jsx
create mode 100644 client/reader/feed-stream/index.jsx
create mode 100644 client/reader/follow-button/README.md
create mode 100644 client/reader/follow-button/index.jsx
create mode 100644 client/reader/following-edit/README.md
create mode 100644 client/reader/following-edit/helper.jsx
create mode 100644 client/reader/following-edit/index.jsx
create mode 100644 client/reader/following-edit/list-item.jsx
create mode 100644 client/reader/following-edit/navigation.jsx
create mode 100644 client/reader/following-edit/notification-settings.jsx
create mode 100644 client/reader/following-edit/placeholder.jsx
create mode 100644 client/reader/following-edit/sort-controls.jsx
create mode 100644 client/reader/following-edit/style.scss
create mode 100644 client/reader/following-edit/subscribe-form-result.jsx
create mode 100644 client/reader/following-edit/subscribe-form.jsx
create mode 100644 client/reader/following-stream/README.md
create mode 100644 client/reader/following-stream/_style.scss
create mode 100644 client/reader/following-stream/empty.jsx
create mode 100644 client/reader/following-stream/index.jsx
create mode 100644 client/reader/following-stream/post-blocked.jsx
create mode 100644 client/reader/following-stream/post-placeholder.jsx
create mode 100644 client/reader/following-stream/post-unavailable.jsx
create mode 100644 client/reader/following-stream/post.jsx
create mode 100644 client/reader/following-stream/x-post.jsx
create mode 100644 client/reader/full-post/README.md
create mode 100644 client/reader/full-post/_style.scss
create mode 100644 client/reader/full-post/index.jsx
create mode 100644 client/reader/index.js
create mode 100644 client/reader/like-button/README.md
create mode 100644 client/reader/like-button/index.jsx
create mode 100644 client/reader/like-helper.jsx
create mode 100644 client/reader/liked-stream/README.md
create mode 100644 client/reader/liked-stream/empty.jsx
create mode 100644 client/reader/liked-stream/index.jsx
create mode 100644 client/reader/list-gap/README.md
create mode 100644 client/reader/list-gap/_style.scss
create mode 100644 client/reader/list-gap/index.jsx
create mode 100644 client/reader/list-item/README.md
create mode 100644 client/reader/list-item/actions.jsx
create mode 100644 client/reader/list-item/description.jsx
create mode 100644 client/reader/list-item/icon.jsx
create mode 100644 client/reader/list-item/index.jsx
create mode 100644 client/reader/list-item/style.scss
create mode 100644 client/reader/list-item/title.jsx
create mode 100644 client/reader/list-management/README.md
create mode 100644 client/reader/list-management/contents/index.jsx
create mode 100644 client/reader/list-management/description-edit/index.jsx
create mode 100644 client/reader/list-management/followers/index.jsx
create mode 100644 client/reader/list-management/navigation/index.jsx
create mode 100644 client/reader/list-management/style.scss
create mode 100644 client/reader/list-stream/README.md
create mode 100644 client/reader/list-stream/empty.jsx
create mode 100644 client/reader/list-stream/index.jsx
create mode 100644 client/reader/post-byline/README.md
create mode 100644 client/reader/post-byline/_style.scss
create mode 100644 client/reader/post-byline/index.jsx
create mode 100644 client/reader/post-errors/README.md
create mode 100644 client/reader/post-errors/index.jsx
create mode 100644 client/reader/post-errors/style.scss
create mode 100644 client/reader/post-excerpt-link/README.md
create mode 100644 client/reader/post-excerpt-link/index.jsx
create mode 100644 client/reader/post-excerpt-link/style.scss
create mode 100644 client/reader/post-images/README.md
create mode 100644 client/reader/post-images/_style.scss
create mode 100644 client/reader/post-images/index.jsx
create mode 100644 client/reader/post-options/README.md
create mode 100644 client/reader/post-options/_style.scss
create mode 100644 client/reader/post-options/index.jsx
create mode 100644 client/reader/post-permalink/README.md
create mode 100644 client/reader/post-permalink/index.jsx
create mode 100644 client/reader/post-permalink/style.scss
create mode 100644 client/reader/post-time/README.md
create mode 100644 client/reader/post-time/index.jsx
create mode 100644 client/reader/reading-time/README.md
create mode 100644 client/reader/reading-time/index.jsx
create mode 100644 client/reader/reading-time/style.scss
create mode 100644 client/reader/recommendations/README.md
create mode 100644 client/reader/recommendations/for-you/index.jsx
create mode 100644 client/reader/recommendations/global-tags/index.jsx
create mode 100644 client/reader/recommendations/navigation/index.jsx
create mode 100644 client/reader/recommendations/sites/index.jsx
create mode 100644 client/reader/recommendations/style.scss
create mode 100644 client/reader/share/README.md
create mode 100644 client/reader/share/index.jsx
create mode 100644 client/reader/share/style.scss
create mode 100644 client/reader/sidebar/README.md
create mode 100644 client/reader/sidebar/_style.scss
create mode 100644 client/reader/sidebar/package.json
create mode 100644 client/reader/sidebar/sidebar.jsx
create mode 100644 client/reader/site-and-author-icon/README.md
create mode 100644 client/reader/site-and-author-icon/_style.scss
create mode 100644 client/reader/site-and-author-icon/index.jsx
create mode 100644 client/reader/site-link/README.md
create mode 100644 client/reader/site-link/index.jsx
create mode 100644 client/reader/site-stream/README.md
create mode 100644 client/reader/site-stream/empty.jsx
create mode 100644 client/reader/site-stream/index.jsx
create mode 100644 client/reader/stats.js
create mode 100644 client/reader/stream-header/README.md
create mode 100644 client/reader/stream-header/index.jsx
create mode 100644 client/reader/stream-header/style.scss
create mode 100644 client/reader/tag-stream/README.md
create mode 100644 client/reader/tag-stream/empty.jsx
create mode 100644 client/reader/tag-stream/index.jsx
create mode 100644 client/reader/update-notice/README.md
create mode 100644 client/reader/update-notice/_style.scss
create mode 100644 client/reader/update-notice/index.jsx
create mode 100644 client/reader/utils.js
create mode 100644 client/reader/xpost-helper.js
create mode 100644 client/remove-overlay/index.js
create mode 100644 client/sections.js
create mode 100644 client/signup/Makefile
create mode 100644 client/signup/README.md
create mode 100644 client/signup/config/Makefile
create mode 100644 client/signup/config/flows.js
create mode 100644 client/signup/config/step-components.js
create mode 100644 client/signup/config/steps.js
create mode 100644 client/signup/config/test/lib/abtest/index.js
create mode 100644 client/signup/config/test/lib/signup/step-actions.js
create mode 100644 client/signup/config/test/lib/user/index.js
create mode 100644 client/signup/config/test/test.js
create mode 100644 client/signup/controller.js
create mode 100644 client/signup/flow-progress-indicator/index.jsx
create mode 100644 client/signup/flow-progress-indicator/style.scss
create mode 100644 client/signup/index.js
create mode 100644 client/signup/locale-suggestions/index.jsx
create mode 100644 client/signup/locale-suggestions/style.scss
create mode 100644 client/signup/log-in-form/index.jsx
create mode 100644 client/signup/logged-out-form/index.jsx
create mode 100644 client/signup/logged-out-form/style.scss
create mode 100644 client/signup/main.jsx
create mode 100644 client/signup/phone-signup-form/index.jsx
create mode 100644 client/signup/phone-signup-form/style.scss
create mode 100644 client/signup/previous-step-button/index.jsx
create mode 100644 client/signup/previous-step-button/style.scss
create mode 100644 client/signup/processing-screen/index.jsx
create mode 100644 client/signup/processing-screen/style.scss
create mode 100644 client/signup/skip-step-button/index.jsx
create mode 100644 client/signup/skip-step-button/style.scss
create mode 100644 client/signup/step-header/index.jsx
create mode 100644 client/signup/step-header/style.scss
create mode 100644 client/signup/step-wrapper/index.jsx
create mode 100644 client/signup/steps/domains/index.jsx
create mode 100644 client/signup/steps/domains/style.scss
create mode 100644 client/signup/steps/dss/index.jsx
create mode 100644 client/signup/steps/dss/screenshot.jsx
create mode 100644 client/signup/steps/dss/style.scss
create mode 100644 client/signup/steps/dss/theme-thumbnail.jsx
create mode 100644 client/signup/steps/email-signup-form/index.jsx
create mode 100644 client/signup/steps/plans/index.jsx
create mode 100644 client/signup/steps/plans/style.scss
create mode 100644 client/signup/steps/site-creation/index.jsx
create mode 100644 client/signup/steps/site-creation/style.scss
create mode 100644 client/signup/steps/test-step/index.jsx
create mode 100644 client/signup/steps/theme-selection/index.jsx
create mode 100644 client/signup/steps/theme-selection/style.scss
create mode 100644 client/signup/steps/theme-selection/theme-thumbnail.jsx
create mode 100644 client/signup/style.scss
create mode 100644 client/signup/submit-step-button/index.jsx
create mode 100644 client/signup/test/flows-test.js
create mode 100644 client/signup/test/lib/abtest/index.js
create mode 100644 client/signup/test/lib/user/index.js
create mode 100644 client/signup/test/signup/config/flows.js
create mode 100644 client/signup/test/signup/config/steps.js
create mode 100644 client/signup/test/utils-test.js
create mode 100644 client/signup/utils.js
create mode 100644 client/signup/validation-fieldset/index.jsx
create mode 100644 client/signup/validation-fieldset/style.scss
create mode 100644 client/signup/wpcom-login-form/index.jsx
create mode 100644 client/vip/README.md
create mode 100644 client/vip/controller.js
create mode 100644 client/vip/index.js
create mode 100644 client/vip/style.scss
create mode 100644 client/vip/vip-backups/README.md
create mode 100644 client/vip/vip-backups/index.jsx
create mode 100644 client/vip/vip-billing/README.md
create mode 100644 client/vip/vip-billing/index.jsx
create mode 100644 client/vip/vip-dashboard/README.md
create mode 100644 client/vip/vip-dashboard/index.jsx
create mode 100644 client/vip/vip-deploys/README.md
create mode 100644 client/vip/vip-deploys/index.jsx
create mode 100644 client/vip/vip-logs/README.md
create mode 100644 client/vip/vip-logs/index.jsx
create mode 100644 client/vip/vip-logs/logs-table.jsx
create mode 100644 client/vip/vip-logs/style.scss
create mode 100644 client/vip/vip-support/README.md
create mode 100644 client/vip/vip-support/index.jsx
create mode 100644 config/README.md
create mode 100644 config/client.json
create mode 100644 config/desktop-mac-app-store.json
create mode 100644 config/desktop.json
create mode 100644 config/development.json
create mode 100644 config/empty-secrets.json
create mode 100644 config/horizon.json
create mode 100644 config/production.json
create mode 100644 config/stage.json
create mode 100644 config/wpcalypso.json
create mode 100644 docs/code-reviews.md
create mode 100644 docs/coding-guidelines.md
create mode 100644 docs/coding-guidelines/css.md
create mode 100644 docs/coding-guidelines/html.md
create mode 100644 docs/coding-guidelines/javascript.md
create mode 100644 docs/git-workflow.md
create mode 100644 docs/guide/0-values.md
create mode 100644 docs/guide/index.md
create mode 100644 docs/guide/tech-behind-calypso.md
create mode 100644 docs/icons.md
create mode 100644 docs/merge-checklist.md
create mode 100644 docs/performance.md
create mode 100644 docs/react-component-unit-testing.md
create mode 100644 docs/reactivity.md
create mode 100644 docs/rtl.md
create mode 100644 index.js
create mode 100644 jsconfig.json
create mode 100644 package.json
create mode 100644 public/fonts/wpeditor.eot
create mode 100644 public/fonts/wpeditor.svg
create mode 100644 public/fonts/wpeditor.ttf
create mode 100644 public/fonts/wpeditor.woff
create mode 100644 public/images/.gitkeep
create mode 100644 public/images/authorize/background-1.jpg
create mode 100644 public/images/authorize/background-2.jpg
create mode 100644 public/images/authorize/background-3.jpg
create mode 100644 public/images/authorize/background-4.jpg
create mode 100644 public/images/authorize/background-5.jpg
create mode 100644 public/images/authorize/background-6.jpg
create mode 100644 public/images/comments/illustration_comments.svg
create mode 100644 public/images/delete-site/export-content.png
create mode 100644 public/images/delete-site/start-over.png
create mode 100644 public/images/devices/iframe-back.png
create mode 100644 public/images/drake/drake-404.svg
create mode 100644 public/images/drake/drake-500.svg
create mode 100644 public/images/drake/drake-all-done.svg
create mode 100644 public/images/drake/drake-browser.svg
create mode 100644 public/images/drake/drake-empty-results.svg
create mode 100644 public/images/drake/drake-jetpack.svg
create mode 100644 public/images/drake/drake-new.svg
create mode 100644 public/images/drake/drake-nomedia.svg
create mode 100644 public/images/drake/drake-nomenus.svg
create mode 100644 public/images/drake/drake-nosites.svg
create mode 100644 public/images/drake/drake-ok.svg
create mode 100644 public/images/drake/drake-whoops.svg
create mode 100644 public/images/favicons/favicon-development.ico
create mode 100644 public/images/favicons/favicon-horizon.ico
create mode 100644 public/images/favicons/favicon-staging.ico
create mode 100644 public/images/favicons/favicon-wpcalypso.ico
create mode 100644 public/images/loading.gif
create mode 100644 public/images/me/pattern-dark.png
create mode 100644 public/images/pages/illustration-pages.svg
create mode 100644 public/images/pages/photolia.jpg
create mode 100644 public/images/people/mystery-person.svg
create mode 100644 public/images/plans/plan-beginner.svg
create mode 100644 public/images/plans/plan-business.svg
create mode 100644 public/images/plans/plan-premium.svg
create mode 100644 public/images/posts/illustration-posts.svg
create mode 100644 public/images/related-posts/cat-blog.png
create mode 100644 public/images/related-posts/devices.jpg
create mode 100644 public/images/related-posts/mobile-wedding.jpg
create mode 100644 public/images/sharing/eventbrite-list.png
create mode 100644 public/images/sharing/eventbrite-widget.png
create mode 100644 public/images/sharing/facebook-profile.png
create mode 100644 public/images/sharing/facebook-sharing.png
create mode 100644 public/images/sharing/google-publicize.png
create mode 100644 public/images/sharing/google-sharing.png
create mode 100644 public/images/sharing/instagram-media.png
create mode 100644 public/images/sharing/instagram-widget.png
create mode 100644 public/images/sharing/linkedin-publicize.png
create mode 100644 public/images/sharing/linkedin-sharing.png
create mode 100644 public/images/sharing/path-publicize.png
create mode 100644 public/images/sharing/tumblr-publicize.png
create mode 100644 public/images/sharing/tumblr-sharing.png
create mode 100644 public/images/sharing/twitter-publicize.png
create mode 100644 public/images/sharing/twitter-timeline.png
create mode 100644 public/images/stats/chart-today.png
create mode 100644 public/images/stats/chart.png
create mode 100644 public/images/stats/chart2.png
create mode 100644 public/images/stats/date-picker.png
create mode 100644 public/images/stats/illustration-stats-intro.svg
create mode 100644 public/images/stats/illustration-stats.svg
create mode 100644 public/images/stats/left-arrow.svg
create mode 100644 public/images/stats/map.jpg
create mode 100644 public/images/stats/right-arrow.svg
create mode 100644 public/images/stats/search-engine.png
create mode 100644 public/images/stats/stats-geo-chart.png
create mode 100644 public/images/upgrades/cc-amex-disabled.svg
create mode 100644 public/images/upgrades/cc-amex.svg
create mode 100644 public/images/upgrades/cc-discover-disabled.svg
create mode 100644 public/images/upgrades/cc-discover.svg
create mode 100644 public/images/upgrades/cc-mastercard-disabled.svg
create mode 100644 public/images/upgrades/cc-mastercard.svg
create mode 100644 public/images/upgrades/cc-placeholder.svg
create mode 100644 public/images/upgrades/cc-visa-disabled.svg
create mode 100644 public/images/upgrades/cc-visa.svg
create mode 100644 public/images/upgrades/google-apps-logo.png
create mode 100644 public/images/upgrades/paypal-disabled.svg
create mode 100644 public/images/upgrades/paypal.svg
create mode 100644 public/images/upgrades/plugins/ecwid.png
create mode 100644 public/images/upgrades/plugins/gumroad.png
create mode 100644 public/images/upgrades/plugins/shopify-store.png
create mode 100644 public/tinymce/skins/wordpress/images/audio.png
create mode 100644 public/tinymce/skins/wordpress/images/dashicon-edit.png
create mode 100644 public/tinymce/skins/wordpress/images/dashicon-no.png
create mode 100644 public/tinymce/skins/wordpress/images/embedded.png
create mode 100644 public/tinymce/skins/wordpress/images/gallery-2x.png
create mode 100644 public/tinymce/skins/wordpress/images/gallery.png
create mode 100644 public/tinymce/skins/wordpress/images/more-2x.png
create mode 100644 public/tinymce/skins/wordpress/images/more.png
create mode 100644 public/tinymce/skins/wordpress/images/pagebreak-2x.png
create mode 100644 public/tinymce/skins/wordpress/images/pagebreak.png
create mode 100644 public/tinymce/skins/wordpress/images/playlist-audio.png
create mode 100644 public/tinymce/skins/wordpress/images/playlist-video.png
create mode 100644 public/tinymce/skins/wordpress/images/video.png
create mode 100644 public/tinymce/skins/wordpress/wp-content.css
create mode 100644 server/README.md
create mode 100644 server/api/Makefile
create mode 100644 server/api/README.md
create mode 100644 server/api/index.js
create mode 100644 server/api/oauth.js
create mode 100644 server/api/test/test.js
create mode 100644 server/boot/index.js
create mode 100644 server/build/README.md
create mode 100644 server/build/index.js
create mode 100644 server/bundler/README.md
create mode 100644 server/bundler/assets.js
create mode 100755 server/bundler/bin/bundler.js
create mode 100755 server/bundler/bin/list-assets.js
create mode 100644 server/bundler/hot-reloader.js
create mode 100644 server/bundler/index.js
create mode 100644 server/bundler/loader.js
create mode 100644 server/bundler/plugin.js
create mode 100644 server/bundler/utils.js
create mode 100644 server/config/index.js
create mode 100644 server/devdocs/README.md
create mode 100755 server/devdocs/bin/generate-devdocs-index
create mode 100644 server/devdocs/index.js
create mode 100644 server/i18n/Makefile
create mode 100644 server/i18n/README.md
create mode 100755 server/i18n/bin/i18n-cli.js
create mode 100644 server/i18n/index.js
create mode 100644 server/i18n/preprocess-xgettextjs-match.js
create mode 100644 server/i18n/test/.gitignore
create mode 100644 server/i18n/test/examples/i18n-test-example-second-file.jsx
create mode 100644 server/i18n/test/examples/i18n-test-examples.jsx
create mode 100644 server/i18n/test/test.js
create mode 100644 server/i18nlint/Makefile
create mode 100644 server/i18nlint/README.md
create mode 100755 server/i18nlint/bin/i18nlint-cli.js
create mode 100644 server/i18nlint/i18nlint.js
create mode 100644 server/i18nlint/test/test-i18nlint.js
create mode 100644 server/i18nlint/test/testfiles/concatenation-and-quotes.js
create mode 100644 server/i18nlint/test/testfiles/duplicate-placeholders.js
create mode 100644 server/i18nlint/test/testfiles/fine.js
create mode 100644 server/i18nlint/test/testfiles/hashbang.js
create mode 100644 server/i18nlint/test/testfiles/missing-singular-placeholder.js
create mode 100644 server/i18nlint/test/testfiles/non-literal-translate-arguments.js
create mode 100644 server/i18nlint/test/testfiles/testfile.jsx
create mode 100644 server/pages/404.jade
create mode 100644 server/pages/500.jade
create mode 100644 server/pages/README.md
create mode 100644 server/pages/desktop.jade
create mode 100644 server/pages/index.jade
create mode 100644 server/pages/index.js
create mode 100644 server/sanitize/index.js
create mode 100644 server/user-bootstrap/index.js
create mode 100644 server/user-bootstrap/shared-utils.js
create mode 100644 shared/README.md
create mode 100644 shared/components/card/README.md
create mode 100644 shared/components/card/compact.jsx
create mode 100644 shared/components/card/docs/example.jsx
create mode 100644 shared/components/card/index.jsx
create mode 100644 shared/components/card/style.scss
create mode 100644 shared/components/data/screen-title/index.jsx
create mode 100644 shared/components/data/store-connection/index.jsx
create mode 100644 shared/components/data/themes-list-fetcher/README.md
create mode 100644 shared/components/data/themes-list-fetcher/index.jsx
create mode 100644 shared/components/empty-content/README.md
create mode 100644 shared/components/empty-content/empty-content.jsx
create mode 100644 shared/components/empty-content/no-sites-message/README.md
create mode 100644 shared/components/empty-content/no-sites-message/index.jsx
create mode 100644 shared/components/empty-content/package.json
create mode 100644 shared/components/empty-content/style.scss
create mode 100644 shared/components/gridicon/README.md
create mode 100644 shared/components/gridicon/docs/example.jsx
create mode 100644 shared/components/gridicon/index.jsx
create mode 100644 shared/components/gridicon/style.scss
create mode 100644 shared/components/theme/Makefile
create mode 100644 shared/components/theme/README.md
create mode 100644 shared/components/theme/docs/example.jsx
create mode 100644 shared/components/theme/index.jsx
create mode 100644 shared/components/theme/more-button.jsx
create mode 100644 shared/components/theme/style.scss
create mode 100644 shared/components/theme/test/index.jsx
create mode 100644 shared/components/themes-list/Makefile
create mode 100644 shared/components/themes-list/README.md
create mode 100644 shared/components/themes-list/index.jsx
create mode 100644 shared/components/themes-list/style.scss
create mode 100644 shared/components/themes-list/test/index.jsx
create mode 100644 shared/dispatcher/index.js
create mode 100644 shared/lib/formatting/Makefile
create mode 100644 shared/lib/formatting/README.md
create mode 100644 shared/lib/formatting/decode-entities/browser.js
create mode 100644 shared/lib/formatting/decode-entities/node.js
create mode 100644 shared/lib/formatting/decode-entities/package.json
create mode 100644 shared/lib/formatting/index.js
create mode 100644 shared/lib/formatting/test/test.js
create mode 100644 shared/lib/i18n-utils/Makefile
create mode 100644 shared/lib/i18n-utils/README.md
create mode 100644 shared/lib/i18n-utils/browser.js
create mode 100644 shared/lib/i18n-utils/node.js
create mode 100644 shared/lib/i18n-utils/package.json
create mode 100644 shared/lib/i18n-utils/test/utils-test.js
create mode 100644 shared/lib/i18n-utils/utils.js
create mode 100644 shared/lib/mixins/emitter/index.js
create mode 100644 shared/lib/screen-title/README.md
create mode 100644 shared/lib/screen-title/actions.js
create mode 100644 shared/lib/screen-title/constants.js
create mode 100644 shared/lib/screen-title/store.js
create mode 100644 shared/lib/screen-title/utils.js
create mode 100644 shared/lib/themes/Makefile
create mode 100644 shared/lib/themes/README.md
create mode 100644 shared/lib/themes/actions.js
create mode 100644 shared/lib/themes/constants.js
create mode 100644 shared/lib/themes/helpers.js
create mode 100644 shared/lib/themes/reducers/current-theme.js
create mode 100644 shared/lib/themes/reducers/themes-last-event.js
create mode 100644 shared/lib/themes/reducers/themes-last-query.js
create mode 100644 shared/lib/themes/reducers/themes-list.js
create mode 100644 shared/lib/themes/reducers/themes.js
create mode 100644 shared/lib/themes/stores/current-theme.js
create mode 100644 shared/lib/themes/stores/themes-last-event.js
create mode 100644 shared/lib/themes/stores/themes-last-query.js
create mode 100644 shared/lib/themes/stores/themes-list.js
create mode 100644 shared/lib/themes/stores/themes.js
create mode 100644 shared/lib/themes/test/current-theme-store.js
create mode 100644 shared/lib/themes/test/themes-list-store.js
create mode 100644 shared/lib/themes/test/themes-store.js
create mode 100644 shared/lib/url/index.js
create mode 100644 shared/lib/wp/README.md
create mode 100644 shared/lib/wp/browser.js
create mode 100644 shared/lib/wp/node.js
create mode 100644 shared/lib/wp/package.json
create mode 100644 shared/lib/wpcom-undocumented/README.md
create mode 100644 shared/lib/wpcom-undocumented/index.js
create mode 100644 shared/lib/wpcom-undocumented/lib/export.js
create mode 100644 shared/lib/wpcom-undocumented/lib/mailing-list.js
create mode 100644 shared/lib/wpcom-undocumented/lib/me.js
create mode 100644 shared/lib/wpcom-undocumented/lib/site.js
create mode 100644 shared/lib/wpcom-undocumented/lib/undocumented.js
create mode 100644 shared/my-sites/themes/README.md
create mode 100644 shared/my-sites/themes/controller.js
create mode 100644 shared/my-sites/themes/current-theme/README.md
create mode 100644 shared/my-sites/themes/current-theme/button.jsx
create mode 100644 shared/my-sites/themes/current-theme/index.jsx
create mode 100644 shared/my-sites/themes/current-theme/style.scss
create mode 100644 shared/my-sites/themes/index.js
create mode 100644 shared/my-sites/themes/jetpack-manage-disabled-message.jsx
create mode 100644 shared/my-sites/themes/jetpack-upgrade-message.jsx
create mode 100644 shared/my-sites/themes/main.jsx
create mode 100644 shared/my-sites/themes/style.scss
create mode 100644 shared/my-sites/themes/thanks-modal.jsx
create mode 100644 shared/my-sites/themes/theme-options.js
create mode 100644 shared/my-sites/themes/themes-search-card/index.jsx
create mode 100644 shared/my-sites/themes/themes-search-card/select-dropdown.jsx
create mode 100644 shared/my-sites/themes/themes-search-card/style.scss
create mode 100644 shared/my-sites/themes/themes-selection.jsx
create mode 100644 shared/my-sites/themes/themes-site-selector-modal.jsx
create mode 100644 webpack.config.js
create mode 100644 webpack.config.node.js
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 00000000000000..85dcc16df69a98
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,2 @@
+.git
+node_modules
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000000000..63108ec4c9c4c9
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,12 @@
+# http://editorconfig.org
+root = true
+
+[*]
+indent_style = tab
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/.esformatter b/.esformatter
new file mode 100644
index 00000000000000..36cc5407a042cd
--- /dev/null
+++ b/.esformatter
@@ -0,0 +1,76 @@
+{
+ "root": true,
+
+ "preset": "default",
+ "indent": {
+ "value": "\t",
+ "IfStatementConditional": 2,
+ "SwitchStatement": 1,
+ "TopLevelFunctionBlock": 1
+ },
+ "lineBreak": {
+ "before": {
+ "VariableDeclarationWithoutInit": 0,
+ "ArrayExpressionClosing": 1
+ },
+ "after": {
+ "AssignmentOperator": -1,
+ "ArrayExpressionOpening": 1,
+ "ArrayExpressionComma": 1
+ }
+ },
+ "whiteSpace": {
+ "before": {
+ "ArgumentList": 1,
+ "ArgumentListArrayExpression": 1,
+ "ArgumentListFunctionExpression": 1,
+ "ArgumentListObjectExpression": 1,
+ "ArrayExpressionClosing": 1,
+ "ExpressionClosingParentheses": 1,
+ "ForInStatementExpressionClosing": 1,
+ "ForStatementExpressionClosing": 1,
+ "IfStatementConditionalClosing": 1,
+ "MemberExpressionClosing": 1,
+ "ParameterList": 1,
+ "SwitchDiscriminantClosing": 1,
+ "WhileStatementConditionalClosing": 1,
+ "CallExpression": -1
+ },
+ "after": {
+ "ArgumentList": 1,
+ "ArgumentListArrayExpression": 1,
+ "ArgumentListFunctionExpression": 1,
+ "ArgumentListObjectExpression": 1,
+ "ArrayExpressionOpening": 1,
+ "ExpressionOpeningParentheses": 1,
+ "ForInStatementExpressionOpening": 1,
+ "ForStatementExpressionOpening": 1,
+ "IfStatementConditionalOpening": 1,
+ "MemberExpressionOpening": 1,
+ "ParameterList": 1,
+ "SwitchDiscriminantOpening": 1,
+ "WhileStatementConditionalOpening": 1,
+ "CallExpression": 0
+ }
+ },
+ "collapseObjects": {
+ "ObjectExpression": {
+ "maxLineLength": 120,
+ "maxKeys": 1,
+ "forbidden": [ "FunctionExpression" ]
+ },
+ "ArrayExpression": {
+ "maxLineLength": 120,
+ "maxKeys": 10,
+ "forbidden": [ "FunctionExpression" ]
+ }
+ },
+ "plugins": [
+ "esformatter-quotes",
+ "esformatter-semicolons",
+ "esformatter-braces",
+ "esformatter-dot-notation",
+ "esformatter-special-bangs",
+ "esformatter-collapse-objects-a8c"
+ ]
+}
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 00000000000000..c6136853bc84eb
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,2 @@
+server/devdocs/search-index.js
+client/components/tinymce/plugins/wptextpattern/plugin.js
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 00000000000000..acb3fe6bc903c0
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,83 @@
+{
+ "parser": "babel-eslint",
+ "env": {
+ "browser": true,
+ "es6": true,
+ "mocha": true,
+ "node": true
+ },
+ "ecmaFeatures": {
+ "jsx": true,
+ "modules": true
+ },
+ "plugins": [
+ "eslint-plugin-react"
+ ],
+ "rules": {
+ "brace-style": [ 1, "1tbs" ],
+ // REST API objects include underscores
+ "camelcase": 0,
+ "comma-dangle": 0,
+ "comma-spacing": 1,
+ // Allows returning early as undefined
+ "consistent-return": 0,
+ "dot-notation": 1,
+ "eqeqeq": [ 2, "allow-null" ],
+ "eol-last": 1,
+ "indent": [ 1, "tab", { "SwitchCase": 1 } ],
+ "key-spacing": 1,
+ // Most common is "Emitter", should be improved
+ "new-cap": 1,
+ "no-cond-assign": 2,
+ "no-else-return": 1,
+ "no-empty": 1,
+ // Flux stores use switch case fallthrough
+ "no-fallthrough": 0,
+ "no-lonely-if": 1,
+ "no-mixed-requires": 0,
+ "no-mixed-spaces-and-tabs": 1,
+ "no-multiple-empty-lines": [ 1, { max: 1 } ],
+ "no-multi-spaces": 1,
+ "no-nested-ternary": 1,
+ "no-new": 1,
+ "no-process-exit": 1,
+ "no-shadow": 1,
+ "no-spaced-func": 1,
+ "no-trailing-spaces": 1,
+ "no-undef": 1,
+ "no-underscore-dangle": 0,
+ // Allows Chai `expect` expressions
+ "no-unused-expressions": 0,
+ "no-unused-vars": 1,
+ // Teach eslint about React+JSX
+ "react/jsx-uses-react": 1,
+ "react/jsx-uses-vars": 1,
+ // Allows function use before declaration
+ "no-use-before-define": [ 2, "nofunc" ],
+ // We split external, internal, module variables
+ "one-var": 0,
+ "operator-linebreak": [ 1, "after", { "overrides": {
+ "?": "before",
+ ":": "before"
+ } } ],
+ "padded-blocks": [ 1, "never" ],
+ "quote-props": [ 1, "as-needed" ],
+ "quotes": [ 1, "single", "avoid-escape" ],
+ "semi-spacing": 1,
+ "space-after-keywords": [ 1, "always" ],
+ "space-before-blocks": [ 1, "always" ],
+ "space-before-function-paren": [ 1, "never" ],
+ // Our array literal index exception violates this rule
+ "space-in-brackets": 0,
+ "space-in-parens": [ 1, "always" ],
+ "space-infix-ops": [ 1, { "int32Hint": false } ],
+ // Ideal for "!" but not for "++"
+ "space-unary-ops": 0,
+ // Assumed by default with Babel
+ "strict": [ 2, "never" ],
+ "valid-jsdoc": [ 1, { "requireReturn": false } ],
+ // Common top-of-file requires, expressions between external, interal
+ "vars-on-top": 1,
+ "yoda": 0
+ }
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000000000..e137661def55d4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,35 @@
+.sass-cache
+.vagrant
+.unison
+.env
+.DS_Store
+.idea
+*.iml
+*.iws
+tags
+node_modules
+/npm-debug.log*
+*.sw?
+!*.swf
+
+# script used during docker build that is specific to the environment
+env-config.sh
+
+# added during build
+/config/secrets.json
+
+/build
+
+/public/*.js
+/public/style*.css
+/public/style*.css.map
+/public/editor*.css
+/server/devdocs/search-index.js
+/server/bundler/assets-*.json
+
+*.rdb
+*.db
+
+/calypso-strings.php
+cover.html
+.test.log
diff --git a/.jsfmtrc b/.jsfmtrc
new file mode 100644
index 00000000000000..2b43ca2e0547b5
--- /dev/null
+++ b/.jsfmtrc
@@ -0,0 +1,75 @@
+{
+ "preset": "default",
+ "indent": {
+ "value": "\t",
+ "IfStatementConditional": 2,
+ "SwitchStatement": 1,
+ "TopLevelFunctionBlock": 1
+ },
+ "lineBreak": {
+ "before": {
+ "VariableDeclarationWithoutInit": 0,
+ "ArrayExpressionClosing": 1
+ },
+ "after": {
+ "AssignmentOperator": -1,
+ "ArrayExpressionOpening": 1,
+ "ArrayExpressionComma": 1
+ }
+ },
+ "whiteSpace": {
+ "before": {
+ "ArgumentList": 1,
+ "ArgumentListArrayExpression": 1,
+ "ArgumentListFunctionExpression": 1,
+ "ArgumentListObjectExpression": 1,
+ "ArrayExpressionClosing": 1,
+ "ExpressionClosingParentheses": 1,
+ "ForInStatementExpressionClosing": 1,
+ "ForStatementExpressionClosing": 1,
+ "IfStatementConditionalClosing": 1,
+ "MemberExpressionClosing": 1,
+ "ParameterList": 1,
+ "SwitchDiscriminantClosing": 1,
+ "WhileStatementConditionalClosing": 1,
+ "CallExpression": -1
+ },
+ "after": {
+ "ArgumentList": 1,
+ "ArgumentListArrayExpression": 1,
+ "ArgumentListFunctionExpression": 1,
+ "ArgumentListObjectExpression": 1,
+ "ArrayExpressionOpening": 1,
+ "ExpressionOpeningParentheses": 1,
+ "ForInStatementExpressionOpening": 1,
+ "ForStatementExpressionOpening": 1,
+ "IfStatementConditionalOpening": 1,
+ "MemberExpressionOpening": 1,
+ "ParameterList": 1,
+ "SwitchDiscriminantOpening": 1,
+ "WhileStatementConditionalOpening": 1,
+ "CallExpression": 0
+ }
+ },
+ "collapseObjects": {
+ "ObjectExpression": {
+ "maxLineLength": 120,
+ "maxKeys": 1,
+ "forbidden": [ "FunctionExpression" ]
+ },
+ "ArrayExpression": {
+ "maxLineLength": 120,
+ "maxKeys": 10,
+ "forbidden": [ "FunctionExpression" ]
+ }
+ },
+ "plugins": [
+ "esformatter-quotes",
+ "esformatter-semicolons",
+ "esformatter-braces",
+ "esformatter-dot-notation",
+ "esformatter-special-bangs",
+ "esformatter-jsx-ignore",
+ "esformatter-collapse-objects-a8c"
+ ]
+}
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 00000000000000..7306b7e1427939
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1,2 @@
+save-exact = true
+
diff --git a/.rtlcssrc b/.rtlcssrc
new file mode 100644
index 00000000000000..d1411f1a17ea3e
--- /dev/null
+++ b/.rtlcssrc
@@ -0,0 +1,10 @@
+{
+ "options": {
+ "preserveComments": true,
+ "preserveDirectives": false,
+ "swapLeftRightInUrl": true,
+ "swapLtrRtlInUrl": true,
+ "swapWestEastInUrl": false,
+ "autoRename": false
+ }
+}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000000000..7b531fe75a42d1
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,70 @@
+Contributing to Calypso
+=======================
+
+Calypso is Open Source software licensed under [GNU General Public License v2 (or later)](./LICENSE.md).
+
+--------
+
+We want to help you start off on the right foot when working with Calypso, reading this is a great first step.
+
+**The most important things to know when starting out are:**
+
+1. All code written for Calypso goes through a peer code review process before it is merged to master.
+2. The workflows for Calypso are different than what you're used to at Automattic, it will take time to adjust.
+3. We are all in this together, building a great WordPress powered experience. We're here to help you along the way.
+4. Good judgement trumps all.
+
+If you can keep those in mind during your first couple of months working with Calypso, you're going to do great. Don't stop reading here though, there's some more important details for each of the above points.
+
+The peer code review process
+----------------------------
+
+Code reviews are an important part of the Calypso workflow. They help to keep code quality consistent, and they help every person working on Calypso improve over time. We want to make you the best Calypso contributor you can be.
+
+Every PR should be reviewed and approved by someone other than the author. Fresh eyes can find problems that can hide in the open if you've been working on the code for a while.
+
+This is one of the primary reasons PRs should be kept small and pushed often (more about that below). We can help you with decisions, make suggestions, and help guide your feature *as you're building it.*
+
+*Everyone* is encouraged to review PRs and add feedback and ask questions, even people who are new to calypso. Also, don't just review PRs from your own team. Reading other people's code is a great way to learn new techniques, and seeing code outside of your own feature helps you to see patterns across the project. It's also helpful to see the feedback other contributors are getting on their PRs.
+
+The final thumbs-up and **[Status] Ready to Merge ** should come from a Calypso contributor that has authored and reviewed a number of merged PRs if the change is substantial.
+
+[A positive mindset towards code reviews](https://medium.com/medium-eng/the-code-review-mindset-3280a4af0a89) is really important. We're building something together that is greater than the sum of its parts, everyone should feel ownership of code going into Calypso and want to make it the best it can be.
+
+If you feel yourself waiting for someone to review a PR, don't hesitate to ask for someone to review on Slack or to ping someone directly via a Github mention. _The PR author is responsible for pushing the change through._
+
+The Calypso Workflow
+--------------------
+
+When you're first starting out, your natural instinct when creating a new feature will be to create a local feature branch, and start building away. If you start doing this, *stop*, take your hands off the keyboard, grab a coffee and read on. :)
+
+**It's important to break your feature down into small pieces first**, each piece should become its own pull request. Even if after finishing the first piece your feature isn't functional, that is okay, we love merging unfinished code early and often. You can place your feature behind a [feature-check](config/README.md#feature-flags) to make sure it's not exposed until all pieces are completed. It's also a good idea to read up on [the CSS/SASS coding guidelines](docs/coding-guidelines/css.md), to ensure form and syntax is consistent.
+
+Once you know what the first small piece of your feature will be, follow this general process while working:
+
+1. Create a new branch, use the [proper naming](docs/git-workflow.md#branch-naming-scheme), _e.g._ `add/video-preview` or `fix/1337-language-too-geeky`
+2. Make your first commit: any will do even if empty or trivial, but we need something in order to create the initial pull request. Create the pull request and prefix the name with the section of the product, _e.g._ _Posts: Prepare store for desktop app_.
+ - Write a detailed description of the problem you are solving, the part of Calypso it affects, and how you plan on going about solving it.
+ - Add the **[Status] In Progress ** label. This indicates that the pull request isn't ready for final review and may still be incomplete. On the other hand, it welcomes early feedback and encourages collaboration during the development process.
+3. Start developing and pushing out commits to your new branch.
+ - Push your changes out frequently and try to avoid getting yourself stuck in a long-running branch or a merge nightmare. When your local work diverges enough from the master branch it can be hard to review and hard to reconcile conflicts.
+ - Follow the [merge checklist](docs/merge-checklist.md) before pushing. This ensures that your code follows the style guidelines and doesn't accidentally introduce any errors or regressions.
+ - Note that you can automate some of these tasks by setting up [githooks](docs/coding-guidelines/javascript.md#setting-up-githooks) and they will run whenever you `git commit`.
+ - Don’t be afraid to change, [squash](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html), and rearrange commits or to force push - `git push -f origin fix/something-broken`. Keep in mind, however, that if other people are working on the same branch then you can mess up their history. You are perfectly safe if you are the only one pushing commits to that branch.
+ - Do proactively squash minor commits such as typo fixes or [fixes to previous commits](http://fle.github.io/git-tip-keep-your-branch-clean-with-fixup-and-autosquash.html) in the pull request.
+4. If you end up needing more than a few commits, consider splitting the pull request into separate components. Discuss in the new pull request and in the comments why the branch was broken apart and any changes that may have taken place that necessitated the split. Our goal is to catch early in the review process those pull requests that attempt to do too much.
+5. When you feel that you are ready for a formal review or for merging into `master` make sure you check this list and our [merge checklist](docs/merge-checklist.md).
+ - Make sure your branch merges cleanly and consider rebasing against `master` to keep the branch history short and clean.
+ - If there are visual changes, add before and after screenshots in the pull request comments.
+ - Add unit tests, or at a minimum, provide helpful instructions for the reviewer so he or she can test your changes. This will help speed up the review process.
+ - Ensure that your commit messages are [meaningful](http://robots.thoughtbot.com/5-useful-tips-for-a-better-commit-message).
+6. Remove the **[Status] In Progress ** label from the pull request and add the **[Status] Needs Review ** label - someone will provide feedback on the latest unreviewed changes. The reviewer will also mark the pull request as **[Status] Awaiting Fixes ** if he or she thinks changes are needed.
+7. If you get a , , , , or a LGTM and the status has been changed to **[Status] Ready to Merge **, merge the changes into `master`.
+
+> Reviewing can be a time-consuming process if only handled by a few developers. Why not skim the list of open pull requests while waiting and review someone else's work in the meantime? Everyone is welcome to add comments at any level, from basic programming style to advanced Calypso ways and means. You may not feel comfortable signing off on someone else's work, but they reviewing you are more-fully joining the project and helping to share the load. **We're all in this together**.
+
+
+We're here to help along the way
+--------------------------------
+
+Don't be afraid to ask for help at any point. We want your first experience with Calypso to be a good one, so don't be shy. If you're wondering why something is the way it is, or how a decision was made, you can tag issues with **[Type] Question **.
diff --git a/CREDITS.md b/CREDITS.md
new file mode 100644
index 00000000000000..1622aed96e7064
--- /dev/null
+++ b/CREDITS.md
@@ -0,0 +1,1174 @@
+Credits
+=======
+
+This project makes use of Open Source components. Below is a list of these components included in this project's source code, and their license information. This project also uses NPM packages, released under open licenses by NPM, see [package.json](/package.json). Source code and license information for each of these packages is available at https://npmjs.org. Many thanks to all of the original authors!
+
+### https://github.com/thoughtbot/bourbon
+#### assets/stylesheets/shared/\_functions.scss
+```text
+The MIT License (MIT)
+
+Copyright © 2011–2015 thoughtbot, inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+```
+### https://github.com/facebook/react
+#### client/components/single-child-css-transition-group
+```text
+BSD License
+
+For React software
+
+Copyright (c) 2013-2015, Facebook, Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+ * Neither the name Facebook nor the names of its contributors may be used to
+ endorse or promote products derived from this software without specific
+ prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE
+```
+
+### https://github.com/WordPress/WordPress
+#### client/components/tinymce/plugins/wpcom
+#### client/components/tinymce/plugins/wpcom-autoresize
+#### client/components/tinymce/plugins/wpcom-view
+#### client/components/tinymce/plugins/wpeditimage
+#### client/components/tinymce/plugins/wplink
+#### client/components/tinymce/plugins/wptextpattern
+#### client/lib/media/constants.js
+#### client/post-editor/media-modal/markup.js
+#### shared/lib/formatting/index.js
+```text
+WordPress - Web publishing software
+
+Copyright 2015 by the contributors
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+This program incorporates work covered by the following copyright and
+permission notices:
+
+ b2 is (c) 2001, 2002 Michel Valdrighi - m@tidakada.com -
+ http://tidakada.com
+
+ Wherever third party code has been used, credit has been given in the code's
+ comments.
+
+ b2 is released under the GPL
+
+and
+
+ WordPress - Web publishing software
+
+ Copyright 2003-2010 by the contributors
+
+ WordPress is released under the GPL
+
+=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
+
+WRITTEN OFFER
+
+The source code for any program binaries or compressed scripts that are
+included with WordPress can be freely obtained at the following URL:
+
+ https://wordpress.org/download/source/
+
+### https://github.com/tinymce/tinymce
+#### client/components/tinymce/plugins/wpcom-charmap
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+ When we speak of free software, we are referring to freedom of use,
+not price. Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Libraries
+
+ If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change. You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+ To apply these terms, attach the following notices to the library. It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+ , 1 April 1990
+ Ty Coon, President of Vice
+
+That's all there is to it!
+```
+
+### https://github.com/slevithan/xregexp
+#### client/lib/embeds/list-store.js
+```text
+The MIT License
+
+Copyright (c) 2007-2015 Steven Levithan
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+```
+
+### https://github.com/kvz/phpjs/
+#### client/lib/version-compare
+#### client/lib/mixins/i18n/number-format.js
+```text
+Copyright (c) 2013 Kevin van Zonneveld (http://kvz.io)
+and Contributors (http://phpjs.org/authors)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+### https://github.com/redsunsoft/react-render-visualizer
+#### client/lib/mixins/render-visualizer
+```text
+The MIT License (MIT)
+
+Copyright (c) 2015 Tony
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+### https://github.com/Modernizr/Modernizr
+#### client/lib/touch-detect/index.js
+```text
+
+Modernizr is available under the MIT license:
+
+MIT License
+Copyright © 2009-2015
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+```
+
+### https://github.com/strongloop/express
+#### server/devdocs/bin/generate-devdocs-index
+```text
+
+(The MIT License)
+
+Copyright (c) 2009-2014 TJ Holowaychuk
+Copyright (c) 2013-2014 Roman Shtylman
+Copyright (c) 2014-2015 Douglas Christopher Wilson
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+```
+
+### https://github.com/sindresorhus/escape-string-regexp
+#### server/devdocs/index.js
+```text
+
+The MIT License (MIT)
+
+Copyright (c) Sindre Sorhus (sindresorhus.com)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+```
+
+### https://github.com/alexei/sprintf.js
+#### server/i18nlint/i18nlint.js
+```text
+
+Copyright (c) 2007-2014, Alexandru Marasteanu
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+* Neither the name of this software nor the names of its contributors may be
+ used to endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+### https://github.com/SalesforceEng/secure-filters
+#### server/sanitize/index.js
+```text
+
+Copyright (c) 2013, GoInstant Inc., a salesforce.com company
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+* Neither the name of salesforce.com, nor GoInstant, nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+### Olark
+#### assets/stylesheets/shared/\_livechat.scss
+#### client/lib/olark-api
+```text
+Copyright © 2013 Habla, Inc. Habla Inc. (dba Olark)
+
+Permission kindly granted by Ben Congleton, of Olark.
+```
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000000000..ad985389cbdd83
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,39 @@
+FROM debian:wheezy
+
+MAINTAINER Automattic
+
+WORKDIR /calypso
+
+RUN mkdir -p /tmp
+COPY ./env-config.sh /tmp/
+RUN bash /tmp/env-config.sh
+RUN apt-get -y update && apt-get -y install \
+ wget \
+ git \
+ python \
+ make \
+ build-essential
+RUN wget http://nodejs.org/dist/v0.12.6/node-v0.12.6-linux-x64.tar.gz && \
+ tar -zxf node-v0.12.6-linux-x64.tar.gz -C /usr/local && \
+ ln -sf node-v0.12.6-linux-x64 /usr/local/node && \
+ ln -sf /usr/local/node/bin/npm /usr/local/bin/ && \
+ ln -sf /usr/local/node/bin/node /usr/local/bin/ && \
+ rm node-v0.12.6-linux-x64.tar.gz
+
+ENV NODE_PATH /calypso/server:/calypso/shared
+
+# Install base npm packages to take advantage of the docker cache
+COPY ./package.json /calypso/package.json
+RUN npm install --production
+
+COPY . /calypso
+
+# Build javascript bundles for each environment and change ownership
+RUN CALYPSO_ENV=wpcalypso make build-wpcalypso && \
+ CALYPSO_ENV=horizon make build-horizon && \
+ CALYPSO_ENV=stage make build-stage && \
+ CALYPSO_ENV=production make build-production && \
+ chown -R nobody /calypso
+
+USER nobody
+CMD NODE_ENV=production node build/bundle-$CALYPSO_ENV.js
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 00000000000000..096a2b4f26c5eb
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,264 @@
+The GNU General Public License, Version 2, June 1991 (GPLv2)
+============================================================
+
+> Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+> 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+Everyone is permitted to copy and distribute verbatim copies of this license
+document, but changing it is not allowed.
+
+
+Preamble
+--------
+
+The licenses for most software are designed to take away your freedom to share
+and change it. By contrast, the GNU General Public License is intended to
+guarantee your freedom to share and change free software--to make sure the
+software is free for all its users. This General Public License applies to most
+of the Free Software Foundation's software and to any other program whose
+authors commit to using it. (Some other Free Software Foundation software is
+covered by the GNU Library General Public License instead.) You can apply it to
+your programs, too.
+
+When we speak of free software, we are referring to freedom, not price. Our
+General Public Licenses are designed to make sure that you have the freedom to
+distribute copies of free software (and charge for this service if you wish),
+that you receive source code or can get it if you want it, that you can change
+the software or use pieces of it in new free programs; and that you know you can
+do these things.
+
+To protect your rights, we need to make restrictions that forbid anyone to deny
+you these rights or to ask you to surrender the rights. These restrictions
+translate to certain responsibilities for you if you distribute copies of the
+software, or if you modify it.
+
+For example, if you distribute copies of such a program, whether gratis or for a
+fee, you must give the recipients all the rights that you have. You must make
+sure that they, too, receive or can get the source code. And you must show them
+these terms so they know their rights.
+
+We protect your rights with two steps: (1) copyright the software, and (2) offer
+you this license which gives you legal permission to copy, distribute and/or
+modify the software.
+
+Also, for each author's protection and ours, we want to make certain that
+everyone understands that there is no warranty for this free software. If the
+software is modified by someone else and passed on, we want its recipients to
+know that what they have is not the original, so that any problems introduced by
+others will not reflect on the original authors' reputations.
+
+Finally, any free program is threatened constantly by software patents. We wish
+to avoid the danger that redistributors of a free program will individually
+obtain patent licenses, in effect making the program proprietary. To prevent
+this, we have made it clear that any patent must be licensed for everyone's free
+use or not licensed at all.
+
+The precise terms and conditions for copying, distribution and modification
+follow.
+
+
+Terms And Conditions For Copying, Distribution And Modification
+---------------------------------------------------------------
+
+**0.** This License applies to any program or other work which contains a notice
+placed by the copyright holder saying it may be distributed under the terms of
+this General Public License. The "Program", below, refers to any such program or
+work, and a "work based on the Program" means either the Program or any
+derivative work under copyright law: that is to say, a work containing the
+Program or a portion of it, either verbatim or with modifications and/or
+translated into another language. (Hereinafter, translation is included without
+limitation in the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not covered by
+this License; they are outside its scope. The act of running the Program is not
+restricted, and the output from the Program is covered only if its contents
+constitute a work based on the Program (independent of having been made by
+running the Program). Whether that is true depends on what the Program does.
+
+**1.** You may copy and distribute verbatim copies of the Program's source code
+as you receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice and
+disclaimer of warranty; keep intact all the notices that refer to this License
+and to the absence of any warranty; and give any other recipients of the Program
+a copy of this License along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and you may at
+your option offer warranty protection in exchange for a fee.
+
+**2.** You may modify your copy or copies of the Program or any portion of it,
+thus forming a work based on the Program, and copy and distribute such
+modifications or work under the terms of Section 1 above, provided that you also
+meet all of these conditions:
+
+* **a)** You must cause the modified files to carry prominent notices stating
+ that you changed the files and the date of any change.
+
+* **b)** You must cause any work that you distribute or publish, that in whole
+ or in part contains or is derived from the Program or any part thereof, to
+ be licensed as a whole at no charge to all third parties under the terms of
+ this License.
+
+* **c)** If the modified program normally reads commands interactively when
+ run, you must cause it, when started running for such interactive use in the
+ most ordinary way, to print or display an announcement including an
+ appropriate copyright notice and a notice that there is no warranty (or
+ else, saying that you provide a warranty) and that users may redistribute
+ the program under these conditions, and telling the user how to view a copy
+ of this License. (Exception: if the Program itself is interactive but does
+ not normally print such an announcement, your work based on the Program is
+ not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If identifiable
+sections of that work are not derived from the Program, and can be reasonably
+considered independent and separate works in themselves, then this License, and
+its terms, do not apply to those sections when you distribute them as separate
+works. But when you distribute the same sections as part of a whole which is a
+work based on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the entire whole,
+and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest your
+rights to work written entirely by you; rather, the intent is to exercise the
+right to control the distribution of derivative or collective works based on the
+Program.
+
+In addition, mere aggregation of another work not based on the Program with the
+Program (or with a work based on the Program) on a volume of a storage or
+distribution medium does not bring the other work under the scope of this
+License.
+
+**3.** You may copy and distribute the Program (or a work based on it, under
+Section 2) in object code or executable form under the terms of Sections 1 and 2
+above provided that you also do one of the following:
+
+* **a)** Accompany it with the complete corresponding machine-readable source
+ code, which must be distributed under the terms of Sections 1 and 2 above on
+ a medium customarily used for software interchange; or,
+
+* **b)** Accompany it with a written offer, valid for at least three years, to
+ give any third party, for a charge no more than your cost of physically
+ performing source distribution, a complete machine-readable copy of the
+ corresponding source code, to be distributed under the terms of Sections 1
+ and 2 above on a medium customarily used for software interchange; or,
+
+* **c)** Accompany it with the information you received as to the offer to
+ distribute corresponding source code. (This alternative is allowed only for
+ noncommercial distribution and only if you received the program in object
+ code or executable form with such an offer, in accord with Subsection b
+ above.)
+
+The source code for a work means the preferred form of the work for making
+modifications to it. For an executable work, complete source code means all the
+source code for all modules it contains, plus any associated interface
+definition files, plus the scripts used to control compilation and installation
+of the executable. However, as a special exception, the source code distributed
+need not include anything that is normally distributed (in either source or
+binary form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component itself
+accompanies the executable.
+
+If distribution of executable or object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the source code
+from the same place counts as distribution of the source code, even though third
+parties are not compelled to copy the source along with the object code.
+
+**4.** You may not copy, modify, sublicense, or distribute the Program except as
+expressly provided under this License. Any attempt otherwise to copy, modify,
+sublicense or distribute the Program is void, and will automatically terminate
+your rights under this License. However, parties who have received copies, or
+rights, from you under this License will not have their licenses terminated so
+long as such parties remain in full compliance.
+
+**5.** You are not required to accept this License, since you have not signed
+it. However, nothing else grants you permission to modify or distribute the
+Program or its derivative works. These actions are prohibited by law if you do
+not accept this License. Therefore, by modifying or distributing the Program (or
+any work based on the Program), you indicate your acceptance of this License to
+do so, and all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+**6.** Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the original
+licensor to copy, distribute or modify the Program subject to these terms and
+conditions. You may not impose any further restrictions on the recipients'
+exercise of the rights granted herein. You are not responsible for enforcing
+compliance by third parties to this License.
+
+**7.** If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues), conditions
+are imposed on you (whether by court order, agreement or otherwise) that
+contradict the conditions of this License, they do not excuse you from the
+conditions of this License. If you cannot distribute so as to satisfy
+simultaneously your obligations under this License and any other pertinent
+obligations, then as a consequence you may not distribute the Program at all.
+For example, if a patent license would not permit royalty-free redistribution of
+the Program by all those who receive copies directly or indirectly through you,
+then the only way you could satisfy both it and this License would be to refrain
+entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply and the
+section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any patents or
+other property right claims or to contest validity of any such claims; this
+section has the sole purpose of protecting the integrity of the free software
+distribution system, which is implemented by public license practices. Many
+people have made generous contributions to the wide range of software
+distributed through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing to
+distribute software through any other system and a licensee cannot impose that
+choice.
+
+This section is intended to make thoroughly clear what is believed to be a
+consequence of the rest of this License.
+
+**8.** If the distribution and/or use of the Program is restricted in certain
+countries either by patents or by copyrighted interfaces, the original copyright
+holder who places the Program under this License may add an explicit
+geographical distribution limitation excluding those countries, so that
+distribution is permitted only in or among countries not thus excluded. In such
+case, this License incorporates the limitation as if written in the body of this
+License.
+
+**9.** The Free Software Foundation may publish revised and/or new versions of
+the General Public License from time to time. Such new versions will be similar
+in spirit to the present version, but may differ in detail to address new
+problems or concerns.
+
+Each version is given a distinguishing version number. If the Program specifies
+a version number of this License which applies to it and "any later version",
+you have the option of following the terms and conditions either of that version
+or of any later version published by the Free Software Foundation. If the
+Program does not specify a version number of this License, you may choose any
+version ever published by the Free Software Foundation.
+
+**10.** If you wish to incorporate parts of the Program into other free programs
+whose distribution conditions are different, write to the author to ask for
+permission. For software which is copyrighted by the Free Software Foundation,
+write to the Free Software Foundation; we sometimes make exceptions for this.
+Our decision will be guided by the two goals of preserving the free status of
+all derivatives of our free software and of promoting the sharing and reuse of
+software generally.
+
+
+No Warranty
+-----------
+
+**11.** BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR
+THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE
+STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM
+"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
+BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+**12.** IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR
+INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA
+BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER
+OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
\ No newline at end of file
diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000000000..a35ffe5b316718
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,152 @@
+# get Makefile directory name: http://stackoverflow.com/a/5982798/376773
+THIS_MAKEFILE_PATH:=$(word $(words $(MAKEFILE_LIST)),$(MAKEFILE_LIST))
+THIS_DIR:=$(shell cd $(dir $(THIS_MAKEFILE_PATH));pwd)
+
+empty:=
+space:=$(empty) $(empty)
+THIS_DIR:= $(subst $(space),\$(space),$(THIS_DIR))
+
+# BIN
+BIN := $(THIS_DIR)/bin
+
+# NODE BIN folder
+NODE_BIN := $(THIS_DIR)/node_modules/.bin
+
+# applications
+NODE ?= node
+NPM ?= $(NODE) $(shell which npm)
+BUNDLER ?= $(BIN)/bundler
+SASS ?= $(NODE_BIN)/node-sass --include-path 'client:shared'
+RTLCSS ?= $(NODE_BIN)/rtlcss
+AUTOPREFIXER ?= $(NODE_BIN)/autoprefixer
+RECORD_ENV ?= $(BIN)/record-env
+GET_I18N ?= $(BIN)/get-i18n
+I18NLINT ?= $(BIN)/i18nlint
+LIST_ASSETS ?= $(BIN)/list-assets
+ALL_DEVDOCS_JS ?= $(THIS_DIR)/server/devdocs/bin/generate-devdocs-index
+
+# files used as prereqs
+SASS_FILES := $(shell find client assets -type f -name '*.scss')
+JS_FILES := $(shell find . -type f \( -name '*.js' -or -name '*.jsx' \) -and -not \( -path './node_modules/*' -or -path './public/*' \) )
+MD_FILES := $(shell find . -name '*.md' -and -not -path '*node_modules*' -and -not -path '*.git*' | sed 's/ /\\ /g')
+CLIENT_CONFIG_FILE := client/config/index.js
+
+# variables
+NODE_ENV ?= development
+CALYPSO_ENV ?= $(NODE_ENV)
+
+export NODE_ENV := $(NODE_ENV)
+export CALYPSO_ENV := $(CALYPSO_ENV)
+export NODE_PATH := server:shared:$(THIS_DIR)
+
+.DEFAULT_GOAL := install
+
+install: node_modules
+
+# Simply running `make run` will spawn the Node.js server instance.
+run: install build
+ @$(NODE) build/bundle-$(CALYPSO_ENV).js
+
+# a helper rule to ensure that a specific module is installed,
+# without relying on a generic `npm install` command
+node_modules/%:
+ @$(NPM) install $(notdir $@)
+
+# ensures that the `node_modules` directory is installed and up-to-date with
+# the dependencies listed in the "package.json" file.
+node_modules: package.json
+ @$(NPM) prune
+ @$(NPM) install
+ @touch node_modules
+
+# run `make test` in all discovered Makefiles
+test: build
+ @$(BIN)/run-all-tests
+
+lint: node_modules/eslint node_modules/eslint-plugin-react node_modules/babel-eslint
+ @$(NODE_BIN)/eslint --quiet $(JS_FILES)
+
+eslint: lint
+
+eslint-branch: node_modules/eslint node_modules/eslint-plugin-react node_modules/babel-eslint
+ @git diff --name-only $$(git merge-base $$(git rev-parse --abbrev-ref HEAD) master)..HEAD | grep '\.jsx\?$$' | xargs $(NODE_BIN)/eslint
+
+# Skip test directories (with the sed regex) for i18nlint in lieu of proper
+# ignore functionality
+i18n-lint:
+ @echo "$(JS_FILES)" | sed 's/\([^ ]*\/test\/[^ ]* *\)//g' | xargs -n1 $(I18NLINT)
+
+# keep track of the current CALYPSO_ENV so that it can be used as a
+# prerequisite for other rules
+.env: FORCE
+ @$(RECORD_ENV) $@
+
+# generate the client-side `config` js file
+$(CLIENT_CONFIG_FILE): .env config/$(CALYPSO_ENV).json config/client.json client/config/regenerate.js
+ @$(NODE) client/config/regenerate.js > $@
+
+public/style.css: node_modules $(SASS_FILES)
+ @$(SASS) assets/stylesheets/style.scss $@
+ @$(AUTOPREFIXER) $@
+
+public/style-debug.css: node_modules $(SASS_FILES)
+ @$(SASS) --source-map "$(@D)/style-debug.css.map" assets/stylesheets/style.scss $@
+ @$(AUTOPREFIXER) $@
+
+public/style-rtl.css: public/style.css
+ @$(RTLCSS) $(THIS_DIR)/public/style.css $@
+
+public/editor.css: node_modules $(SASS_FILES)
+ @$(SASS) assets/stylesheets/editor.scss $@
+ @$(AUTOPREFIXER) $@
+
+server/devdocs/search-index.js: $(MD_FILES) $(ALL_DEVDOCS_JS)
+ @$(ALL_DEVDOCS_JS) $(MD_FILES)
+
+build-server: install
+ @mkdir -p build
+ @CALYPSO_ENV=$(CALYPSO_ENV) $(NODE_BIN)/webpack --display-error-details --config webpack.config.node.js
+
+build: install build-$(CALYPSO_ENV)
+
+build-css: public/style.css public/style-rtl.css public/style-debug.css public/editor.css
+
+build-development: build-server $(CLIENT_CONFIG_FILE) server/devdocs/search-index.js build-css
+
+build-wpcalypso: build-server $(CLIENT_CONFIG_FILE) server/devdocs/search-index.js build-css
+ @$(BUNDLER)
+
+build-desktop build-desktop-mac-app-store build-horizon build-stage build-production: build-server $(CLIENT_CONFIG_FILE) build-css
+ @$(BUNDLER)
+
+# the `clean` rule deletes all the files created from `make build`, but not
+# those created by `make install`
+clean:
+ @rm -rf public/style*.css public/style-debug.css.map public/*.js $(CLIENT_CONFIG_FILE) server/devdocs/search-index.js public/editor.css build/* server/bundler/*.json
+
+# the `distclean` rule deletes all the files created from `make install`
+distclean:
+ @rm -rf node_modules
+
+# create list of translations, saved as `./calypso-strings.php`
+translate: node_modules $(CLIENT_CONFIG_FILE)
+ @CALYPSO_ENV=stage $(BUNDLER)
+ @CALYPSO_ENV=stage $(LIST_ASSETS) | xargs $(GET_I18N) ./calypso-strings.php calypso_i18n_strings
+
+# install all git hooks
+githooks: githooks-commit githooks-push
+
+# install git pre-commit hook
+githooks-commit:
+ @if [ ! -e .git/hooks/pre-commit ]; then ln -s ../../bin/pre-commit .git/hooks/pre-commit; fi
+
+# install git pre-push hook
+githooks-push:
+ @if [ ! -e .git/hooks/pre-push ]; then ln -s ../../bin/pre-push .git/hooks/pre-push; fi
+
+# rule that can be used as a prerequisite for other rules to force them to always run
+FORCE:
+
+.PHONY: build build-development build-server
+.PHONY: run install test clean distclean translate route
+.PHONY: githooks githooks-commit githooks-push
diff --git a/README.md b/README.md
new file mode 100644
index 00000000000000..71e37ab8e12605
--- /dev/null
+++ b/README.md
@@ -0,0 +1,161 @@
+Calypso
+=======
+
+Calypso is a web application that allows users to manage all of their WordPress blogs in one place. Calypso is Open Source software licensed under [GNU General Public License v2 (or later)](./LICENSE.md).
+
+Making Changes
+--------------
+
+Our workflow is different — before jumping in and writing thousands of lines of code, you should read the [Contributing to Calypso](CONTRIBUTING.md) document. It describes the process that we're using. Understanding and following that procedure will save you a bunch of pain down the road. :grinning:
+
+Getting Started
+---------------
+
+1. Check the prerequisites are installed (Git, Node, NPM). See below for more details.
+2. Clone this repository locally.
+3. Add `127.0.0.1 calypso.localhost` to your local hosts file.
+4. With the command line open at the repository root, execute `make run`. See below for more details.
+5. Open `calypso.localhost:3000` in your browser.
+
+### Prerequisites
+
+To be able to clone and run the application you need:
+
+- [Node.js](http://nodejs.org/) and [NPM](https://www.npmjs.com/) installed. Here's a [handy installer](https://nodejs.org/dist/latest/) for Windows, Mac, and Linux.
+- [Git](http://git-scm.com/). Try the `git` command from your terminal, if it's not found then use this [installer](http://git-scm.com/download/).
+- The repository also uses `make` to orchestrate compiling the JavaScript, running the server, and several other tasks.
+
+### Installing and Running
+
+Clone this git repo to your machine via the terminal using the `git clone` command and then run `make` from the root calypso directory:
+
+```bash
+$ git clone git@github.com:Automattic/calypso-pre-oss.git
+$ cd calypso-pre-oss
+$ make run
+```
+
+The `make run` command will install any node dependencies and start the application. When changes are made to either the JavaScript files or the Sass stylesheets, the build process will run when making a request to the application via the browser. The build process compiles both the JavaScript and CSS to make sure that you have the latest versions of both.
+
+To run Calypso locally, you'll need to add `127.0.0.1 calypso.localhost` to your hosts file, and load the app at http://calypso.localhost:3000 instead of localhost. This is necessary because only certain origins are allowed to make use of the REST API via the iframe proxy.
+
+Our Approach
+------------
+
+After evaluating several JavaScript libraries and frameworks designed to make creating single-page apps easier, we decided to forego a monolithic framework altogether and build our own system with the help of small open source modules made available via [npm](https://www.npmjs.com/). Significantly we have chosen to use [React](http://facebook.github.io/react/) for the view layer, and have been heavily influenced by the React community.
+
+Coding Guidelines
+-----------------
+
+[Coding Guidelines »](docs/coding-guidelines.md)
+
+I18n Guidelines
+---------------
+
+[I18n Guidelines »](client/lib/mixins/i18n/README.md)
+
+Browser Support
+---------------
+
+We support the latest two versions of all major browsers (see [browse happy](http://browsehappy.com) for current latest versions).
+
+Directory Structure
+-------------------
+
+Since we're using [Node.js](http://nodejs.org) on the server, there is going to be a lot of JavaScript code in the application. This only gets compounded by the fact that both server-side and client-side modules reside on the same `npm` package manager (though that ends up being a net positive due to reusability and discovery).
+
+The structure of the project is as follows:
+
+```
+├── README.md
+├── index.js
+├── package.json
+├── Makefile
+├── assets
+│ └── stylesheets
+├── client
+│ ├── boot
+│ ├── config
+│ └── … more modules
+├── config
+│ ├── client.json
+│ ├── development.json
+│ └── production.json
+├── server
+│ ├── boot
+│ ├── config
+│ └── … more modules
+└── public
+ ├── index.html
+ ├── build.js
+ ├── build.css
+ └── … more static files
+```
+
+Let's explain the main directories individually:
+
+#### `config`
+
+This dir is used to store `.json` config files. At boot-up time, the server decides which config file to use based on the `CALYPSO_ENV` environment variable variable. The default value is `"development"`.
+
+To run calypso use a value other than the default, you can specify the value in the command:
+
+```bash
+CALYPSO_ENV=wpcalypso make run
+```
+
+One of the main uses of the config, is to enable/disable features for different environments. This allows us to merge early and often.
+
+See the main config [README](config/README.md) for more information.
+
+#### `assets`
+
+This directory contains the [Sass](http://sass-lang.com/) stylesheets that are compiled into a single style.css when `make build` runs.
+
+#### `server`
+
+This dir contains all server-side logic, in self-contained modules within this directory. There should be no other files in this directory other than directories of self-contained modules. The server entry point is usually the `boot` module, which should be a function that returns a "http request handler" function (with the `function (req, res) {}` signature), like the one returned from Express.
+
+#### `client`
+
+This dir is similar in structure to the `server` dir, except that this JavaScript code ends up running on the client-side.
+
+Similar to the `server` dir, the client should have a `boot` module that is the entry point to the client-side application. Often times this file uses [page.js](http://visionmedia.github.io/page.js) to set up the client-side routes, which makes for nice symmetry with the server-side.
+
+#### `public`
+
+This dir contains static assets to be served via the sever-side static provider (Express.js' `static()` middleware since we're being particular). The default Makefile setup ends up compiling:
+
+- `build-[environment].[hash].js` - webpack bundle of the client-side application.
+- `build-logged-out-[environment].[hash].js` - webpack bundle of the logged-out client-side application.
+- `[section].[hash].js` - webpack chunk for each section in `sections.js`.
+- `style.css` - the compiled `*.scss` SASS files concat'd into a single CSS file.
+
+Of course, any other static assets may be dropped into this directory as well (web font files, icon packs, the favicon, images, etc.).
+
+Debugging
+---------
+
+Calypso uses the [debug](https://github.com/visionmedia/debug) module to handle debug messaging. To turn on debug mode for all modules, type the following in the browser console:
+
+```js
+localStorage.setItem( 'debug', '*' );
+```
+
+You can also limit the debugging to a particular scope
+
+```js
+localStorage.setItem( 'debug', 'calypso:*' );
+```
+
+The Node server uses the DEBUG environment variable instead of localStorage. `make run` will pass along it's environment, so you can turn on all debug messages with
+
+```bash
+DEBUG=* make run
+```
+
+or limit it as before with
+
+```base
+DEBUG=calypso:* make run
+```
diff --git a/Vagrantfile b/Vagrantfile
new file mode 100644
index 00000000000000..267a8266762f37
--- /dev/null
+++ b/Vagrantfile
@@ -0,0 +1,11 @@
+Vagrant.configure("2") do |config|
+ config.vm.define "app" do |app|
+ app.vm.provider "docker" do |d|
+ d.build_dir = "."
+ d.host_vm_build_dir_options = { rsync__args: ["--verbose", "--archive", "--delete", "-z", "--links"] }
+ d.ports = ["3000:3000"]
+ d.create_args = ["--env", "CALYPSO_ENV=wpcalypso"]
+ d.vagrant_vagrantfile = "./Vagrantfile-boot2docker"
+ end
+ end
+end
diff --git a/Vagrantfile-boot2docker b/Vagrantfile-boot2docker
new file mode 100644
index 00000000000000..b246d9a0250634
--- /dev/null
+++ b/Vagrantfile-boot2docker
@@ -0,0 +1,29 @@
+Vagrant.configure("2") do |config|
+ config.vm.box = "mitchellh/boot2docker"
+ config.vm.network :forwarded_port, guest: 3000, host: 3000
+
+ config.vm.provider "virtualbox" do |v|
+ # On VirtualBox, we don't have guest additions or a functional vboxsf
+ # in TinyCore Linux, so tell Vagrant that so it can be smarter.
+ v.check_guest_additions = false
+ v.functional_vboxsf = false
+ v.memory = 2048
+ end
+
+ ["vmware_fusion", "vmware_workstation"].each do |vmware|
+ config.vm.provider vmware do |v|
+ if v.respond_to?(:functional_hgfs=)
+ v.functional_hgfs = false
+ end
+ v.vmx["memsize"] = "2048"
+ end
+ end
+
+ # b2d doesn't support NFS
+ config.nfs.functional = false
+
+ # b2d doesn't persist filesystem between reboots
+ if config.ssh.respond_to?(:insert_key)
+ config.ssh.insert_key = false
+ end
+end
diff --git a/assets/stylesheets/README.md b/assets/stylesheets/README.md
new file mode 100644
index 00000000000000..bc4df33c3f27a5
--- /dev/null
+++ b/assets/stylesheets/README.md
@@ -0,0 +1,10 @@
+Styles
+======
+
+Calypso uses SASS to build its CSS and compile it to `public/style.css`. Remember that all styles end up in a single file, so all styles will apply to every page, all the time. Make sure you namespace your styles for the page you are working on.
+
+### Adding a new SASS file
+
+If you are adding a new SASS file to `assets/stylesheets` you will need to reference the file in `assets/stylesheets/style.scss` for it to load. We have three directories to organize the respective files: layout, sections, and shared. If you are adding a new file you are likely adding it to `sections`.
+
+Check [our styleguide](https://github.com/Automattic/calypso-pre-oss/blob/master/docs/coding-guidelines/css.md) for more information on how we use SASS.
diff --git a/assets/stylesheets/_components.scss b/assets/stylesheets/_components.scss
new file mode 100644
index 00000000000000..f4ecc907e8c624
--- /dev/null
+++ b/assets/stylesheets/_components.scss
@@ -0,0 +1,301 @@
+// ==========================================================================
+// Components
+//
+// This is an import of all component CSS that is bundled with each component.
+// Please keep these imports sorted and use specificity to ensure that
+// stylesheet order does not matter.
+// ==========================================================================
+
+@import 'accept-invite/invite-form-header/style';
+@import 'accept-invite/invite-header/style';
+@import 'accept-invite/logged-in-accept/style';
+@import 'accept-invite/style';
+@import 'auth/style';
+@import 'accept-invite/logged-out-invite/style';
+@import 'components/accordion/style';
+@import 'components/add-new-button/style';
+@import 'components/author-selector/style';
+@import 'components/bulk-select/style';
+@import 'components/button/style';
+@import 'components/button-group/style';
+@import 'components/card/style';
+@import 'components/chart/style';
+@import 'components/comment-button/style';
+@import 'components/count/style';
+@import 'components/date-picker/style';
+@import 'components/dialog/style';
+@import 'components/domains/domain-mapping-suggestion/style';
+@import 'components/domains/domain-product-price/style';
+@import 'components/domains/domain-search-results/style';
+@import 'components/domains/domain-suggestion/style';
+@import 'components/domains/example-domain-suggestions/style';
+@import 'components/domains/map-domain-step/style';
+@import 'components/domains/register-domain-step/style';
+@import 'components/drop-zone/style';
+@import 'components/emojify/style';
+@import 'components/empty-content/style';
+@import 'components/external-link/style';
+@import 'components/flag/style';
+@import 'components/foldable-card/style';
+@import 'components/follow-button/style';
+@import 'components/gauge/style';
+@import 'components/gravatar/style';
+@import 'components/gridicon/style';
+@import 'components/header-cake/style';
+@import 'components/infinite-list/style';
+@import 'components/info-popover/style';
+@import 'components/input-chrono/style';
+@import 'components/like-button/style';
+@import 'components/main/style';
+@import 'components/mobile-back-to-sidebar/style';
+@import 'components/payment-logo/style';
+@import 'components/progress-bar/style';
+@import 'components/progress-indicator/style';
+@import 'components/popover/style';
+@import 'components/post-excerpt/style';
+@import 'components/pulsing-dot/style';
+@import 'components/rating/style';
+@import 'components/search/style';
+@import 'components/search-card/style';
+@import 'components/section-nav/style';
+@import 'components/segmented-control/style';
+@import 'components/select-dropdown/style';
+@import 'components/site-icon/style';
+@import 'components/site-selector/style';
+@import 'components/site-selector-modal/style';
+@import 'components/sites-popover/style';
+@import 'components/spinner/style';
+@import 'components/stat-update-indicator/style';
+@import 'components/sticky-panel/style';
+@import 'components/theme/style';
+@import 'components/themes-list/style';
+@import 'components/timezone-dropdown/style';
+@import 'components/tinymce/style';
+@import 'components/token-field/style';
+@import 'components/forms/counted-textarea/style';
+@import 'components/forms/form-button/style';
+@import 'components/forms/form-buttons-bar/style';
+@import 'components/forms/form-fieldset/style';
+@import 'components/forms/form-input-validation/style';
+@import 'components/forms/form-label/style';
+@import 'components/forms/form-legend/style';
+@import 'components/forms/form-password-input/style';
+@import 'components/forms/form-range/style';
+@import 'components/forms/form-section-heading/style';
+@import 'components/forms/form-select/style';
+@import 'components/forms/form-setting-explanation/style';
+@import 'components/forms/form-tel-input/style';
+@import 'components/forms/form-text-input/style';
+@import 'components/forms/form-text-input-with-affixes/style';
+@import 'components/forms/form-toggle/style';
+@import 'components/forms/range/style';
+@import 'components/forms/sortable-list/index';
+@import 'components/olark-chatbox/style';
+@import 'components/plans/plan/style';
+@import 'components/plans/plan-actions/style';
+@import 'components/plans/plan-discount-message/style';
+@import 'components/plans/plan-features/style';
+@import 'components/plans/plan-feature-cell/style';
+@import 'components/plans/plan-header/style';
+@import 'components/plans/plan-list/style';
+@import 'components/plans/plan-nudge/style';
+@import 'components/plans/plan-price/style';
+@import 'components/plans/plans-compare/style';
+@import 'components/post-schedule/style';
+@import 'components/section-header/style';
+@import 'components/signup-form/style';
+@import 'components/tooltip/style';
+@import 'components/upgrades/credit-card-form/style';
+@import 'components/upgrades/credit-card-number-input/style';
+@import 'components/user/style';
+@import 'components/web-preview/style';
+@import 'components/version/style';
+@import 'components/vertical-nav/style';
+@import 'components/vertical-nav/item/style';
+@import 'layout/community-translator/style';
+@import 'layout/masterbar-new-post';
+@import 'lib/accept/style';
+@import 'me/account-password/style';
+@import 'me/account/style';
+@import 'me/action-remove/style';
+@import 'me/application-password-item/style';
+@import 'me/application-passwords/style';
+@import 'me/connected-application-item/style';
+@import 'me/connected-applications/style';
+@import 'me/connected-application-icon/style';
+@import 'me/credit-cards/credit-card-delete';
+@import 'me/credit-cards/credit-cards';
+@import 'me/help/help-contact-confirmation/style';
+@import 'me/help/help-contact/style';
+@import 'me/help/help-contact-form/style';
+@import 'me/help/help-happiness-engineers/style';
+@import 'me/help/help-results/style';
+@import 'me/help/help-search/style';
+@import 'me/help/style';
+@import 'me/next-steps/next-steps';
+@import 'me/next-steps/next-steps-box';
+@import 'me/notification-settings/blogs-settings/style';
+@import 'me/notification-settings/comment-settings/style';
+@import 'me/notification-settings/settings-form/style';
+@import 'me/notification-settings/wpcom-settings/style';
+@import 'me/profile-gravatar/style';
+@import 'me/profile-link/style';
+@import 'me/profile-links-add-other/style';
+@import 'me/profile-links-add-wordpress/style';
+@import 'me/profile-links/style';
+@import 'me/purchases/cancel-private-registration/style';
+@import 'me/purchases/cancel-purchase/style';
+@import 'me/purchases/confirm-cancel-purchase/style';
+@import 'me/purchases/list/item/style';
+@import 'me/purchases/list/site/style';
+@import 'me/purchases/manage-purchase/style';
+@import 'me/purchases/payment/edit-card-details/style';
+@import 'me/purchases/payment/edit-payment-method/style';
+@import 'me/reauth-required/style';
+@import 'me/security-2fa-backup-codes-list/style';
+@import 'me/security-2fa-backup-codes-prompt/style';
+@import 'me/security-2fa-backup-codes/style';
+@import 'me/security-2fa-code-prompt/style';
+@import 'me/security-2fa-disable/style';
+@import 'me/security-2fa-enable/style';
+@import 'me/security-2fa-progress/style';
+@import 'me/security-2fa-sms-settings/style';
+@import 'me/security-2fa-status/style';
+@import 'me/security-checkup/style';
+@import 'me/sidebar-navigation/style';
+@import 'me/sidebar/style';
+@import 'me/two-step/style';
+@import 'my-sites/ads/style';
+@import 'my-sites/all-sites/style';
+@import 'my-sites/all-sites-icon/style';
+@import 'my-sites/category-selector/style';
+@import 'my-sites/current-site/style';
+@import 'my-sites/customize/style';
+@import 'my-sites/draft/style';
+@import 'my-sites/drafts/style';
+@import 'my-sites/exporter/style';
+@import 'my-sites/media-library/style';
+@import 'my-sites/no-results/style';
+@import 'my-sites/pages/style';
+@import 'my-sites/people/delete-user/style';
+@import 'my-sites/people/edit-team-member-form/style';
+@import 'my-sites/people/people-list-item/style';
+@import 'my-sites/people/people-profile/style';
+@import 'my-sites/people/people-notices/style';
+@import 'my-sites/plans/style';
+@import 'my-sites/post/post-image/style';
+@import 'my-sites/post-selector/style';
+@import 'my-sites/post-trends/style';
+@import 'my-sites/plugins/featured-plugins/style';
+@import 'my-sites/plugins/style';
+@import 'my-sites/plugins/plugin-action/style';
+@import 'my-sites/plugins/plugin-activate-toggle/style';
+@import 'my-sites/plugins/plugin-card-header/style';
+@import 'my-sites/plugins/plugin-icon/style';
+@import 'my-sites/plugins/plugin-information/style';
+@import 'my-sites/plugins/plugin-item/style';
+@import 'my-sites/plugins/plugin-meta/style';
+@import 'my-sites/plugins/plugin-ratings/style';
+@import 'my-sites/plugins/plugin-sections/style';
+@import 'my-sites/plugins/plugin-site-network/style';
+@import 'my-sites/plugins/plugin-site-disabled-manage/style';
+@import 'my-sites/plugins/plugin-site-jetpack/style';
+@import 'my-sites/plugins/plugin-site-update-indicator/style';
+@import 'my-sites/plugins/plugin-site-business/style';
+@import 'my-sites/plugins/plugin-install-button/style';
+@import 'my-sites/plugins/plugin-remove-button/style';
+@import 'my-sites/plugins/plugins-browser-item/style';
+@import 'my-sites/plugins/plugins-browser-list/style';
+@import 'my-sites/plugins/plugins-browser/style';
+@import 'my-sites/plugins/plugin-version/style';
+@import 'my-sites/sharing/connections/account-dialog';
+@import 'my-sites/sharing/connections/account-dialog-account';
+@import 'my-sites/sharing/connections/services-group';
+@import 'my-sites/sidebar-navigation/style';
+@import 'my-sites/site-indicator/style';
+@import 'my-sites/site-settings/action-panel/style';
+@import 'my-sites/site-settings/delete-site-options/style';
+@import 'my-sites/site-settings/delete-site/style';
+@import 'my-sites/site-settings/settings-card-footer/style';
+@import 'my-sites/importer/style';
+@import 'my-sites/site/style';
+@import 'my-sites/stats/all-time/style';
+@import 'my-sites/stats/geochart/style';
+@import 'my-sites/stats/most-popular/style';
+@import 'my-sites/stats/pagination/style';
+@import 'my-sites/stats/overview/style';
+@import 'my-sites/stats/nux/style';
+@import 'my-sites/themes/current-theme/style';
+@import 'my-sites/themes/style';
+@import 'my-sites/themes/themes-search-card/style';
+@import 'my-sites/upgrades/cart/style';
+@import 'my-sites/upgrades/checkout/stored-card';
+@import 'my-sites/upgrades/checkout/subscription-text';
+@import 'notices/style';
+@import 'my-sites/upgrades/domain-management/style';
+@import 'post-editor/style';
+@import 'post-editor/drafts-button/style';
+@import 'post-editor/edit-post-status/style';
+@import 'post-editor/editor-action-bar/style';
+@import 'post-editor/editor-author/style';
+@import 'post-editor/editor-categories/style';
+@import 'post-editor/editor-delete-post/style';
+@import 'post-editor/editor-discussion/style';
+@import 'post-editor/editor-drawer/style';
+@import 'post-editor/editor-drawer-well/style';
+@import 'post-editor/editor-featured-image/style';
+@import 'post-editor/editor-fieldset/style';
+@import 'post-editor/editor-ground-control/style';
+@import 'post-editor/editor-location/style';
+@import 'post-editor/editor-mobile-navigation/style';
+@import 'post-editor/editor-more-options/style';
+@import 'post-editor/editor-page-parent/style';
+@import 'post-editor/editor-page-order/style';
+@import 'post-editor/editor-permalink/style';
+@import 'post-editor/editor-post-formats/style';
+@import 'post-editor/editor-post-type/style';
+@import 'post-editor/editor-revisions/style';
+@import 'post-editor/editor-sharing/style';
+@import 'post-editor/editor-slug/style';
+@import 'post-editor/editor-title/style';
+@import 'post-editor/editor-visibility/style';
+@import 'post-editor/media-modal/style';
+@import 'reader/comments/style';
+@import 'reader/discover/style';
+@import 'reader/feed-header/style';
+@import 'reader/following-edit/style';
+@import 'reader/following-stream/style';
+@import 'reader/full-post/style';
+@import 'reader/list-gap/style';
+@import 'reader/list-item/style';
+@import 'reader/list-management/style';
+@import 'reader/post-byline/style';
+@import 'reader/post-errors/style';
+@import 'reader/post-excerpt-link/style';
+@import 'reader/post-permalink/style';
+@import 'reader/post-options/style';
+@import 'reader/post-images/style';
+@import 'reader/reading-time/style';
+@import 'reader/recommendations/style';
+@import 'reader/share/style';
+@import 'reader/sidebar/style';
+@import 'reader/site-and-author-icon/style';
+@import 'reader/style';
+@import 'reader/stream-header/style';
+@import 'reader/update-notice/style';
+@import 'signup/style';
+@import 'signup/flow-progress-indicator/style';
+@import 'signup/locale-suggestions/style';
+@import 'signup/logged-out-form/style';
+@import 'signup/phone-signup-form/style';
+@import 'signup/previous-step-button/style';
+@import 'signup/processing-screen/style';
+@import 'signup/skip-step-button/style';
+@import 'signup/step-header/style';
+@import 'signup/steps/domains/style';
+@import 'signup/steps/plans/style';
+@import 'signup/steps/site-creation/style';
+@import 'signup/steps/theme-selection/style';
+@import 'signup/validation-fieldset/style';
+@import 'vip/vip-logs/style';
+@import 'signup/steps/dss/style';
diff --git a/assets/stylesheets/editor.scss b/assets/stylesheets/editor.scss
new file mode 100644
index 00000000000000..610e2438bf0747
--- /dev/null
+++ b/assets/stylesheets/editor.scss
@@ -0,0 +1,4 @@
+@import 'shared/colors';
+@import 'shared/typography';
+
+@import 'components/tinymce/iframe';
diff --git a/assets/stylesheets/layout/_detail-page.scss b/assets/stylesheets/layout/_detail-page.scss
new file mode 100644
index 00000000000000..074f4740bfc824
--- /dev/null
+++ b/assets/stylesheets/layout/_detail-page.scss
@@ -0,0 +1,148 @@
+.detail-page__backdrop {
+ padding: 1px; // this fixes a strange rendering bug at the bottom of the full post card in safari and chrome -shaun
+ z-index: 190; // above the masterbar
+ box-sizing: border-box;
+ margin: 0;
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ background-color: $white;
+
+ // Entrance transitions
+ &.detail-page-enter {
+ opacity: 0;
+ transform: translateX( 200px );
+ }
+
+ &.detail-page-enter-active {
+ opacity: 1;
+ transform: translateX( 0 );
+ transition: all 0.1s ease-out;
+ }
+
+ // Exit transitions
+ &.detail-page-leave {
+ transition: all 0.1s ease-in;
+ }
+
+ &.detail-page-leave-active {
+ opacity: 0;
+ transform: translateX( 200px );
+ }
+
+ .card {
+ margin: 0 auto;
+ min-height: 100vh;
+ background: transparent;
+ padding: 0;
+ box-shadow: none;
+ }
+}
+
+.detail-page__content {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ overflow-y: scroll;
+ overflow-x: hidden;
+ -webkit-overflow-scrolling: touch;
+ -webkit-backface-visibility: hidden;
+}
+
+.detail-page__action-buttons {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ margin: 0;
+ z-index: 200; // above the masterbar
+ list-style: none;
+ text-align: right;
+ padding: 7px 6px;
+ box-sizing: border-box;
+ background: rgba( $white, 0.98 );
+ border-bottom: 1px solid lighten( $gray, 30 );
+
+ // These make our fixed inside fixed stuff work in Chrome
+ -webkit-backface-visibility: hidden;
+ -webkit-transform: translateZ(0);
+
+ .button.action-button {
+ color: lighten( $gray, 10 );
+ padding: 0 8px;
+ line-height: 32px;
+ text-align: center;
+ cursor: pointer;
+ background: transparent;
+ border: none;
+ border-radius: 0;
+
+ span {
+ display: none;
+ font-weight: normal;
+ color: $gray;
+ }
+
+ &:hover {
+ color: $blue-medium;
+ }
+
+ &:before {
+ position: relative;
+ margin-right: 4px;
+ }
+ }
+
+ .detail-page-close {
+ float: left;
+
+ &:before {
+ @include noticon( '\f430', 32px );
+ top: 1px;
+ left: -6px;
+ margin-right: 0;
+ }
+ }
+
+ .like-button, .comment-button, .reader-share {
+ top: 2px;
+ float: right;
+ margin-right: 16px;
+ }
+
+ .post-options {
+ float: right;
+ margin-right: 4px;
+
+ .post-options__trigger .gridicon__ellipsis {
+ padding: 4px 8px;
+ }
+ }
+}
+
+
+// Setup transitions for content
+html.detail-page-active {
+ overflow: hidden; // This prevents the normal page from scrolling -shaun
+
+ .wp-primary {
+ transition: all 0.1s ease-in;
+ }
+}
+
+// Transitions for content
+html.detail-page-open {
+ .reader-update-notice {
+ opacity: 0;
+ }
+
+ .wp-primary {
+ opacity: 0;
+ transform: translateX( -100px );
+ pointer-events: none;
+ }
+}
diff --git a/assets/stylesheets/layout/_main.scss b/assets/stylesheets/layout/_main.scss
new file mode 100644
index 00000000000000..b10fa0d11bc5b5
--- /dev/null
+++ b/assets/stylesheets/layout/_main.scss
@@ -0,0 +1,475 @@
+/**
+ * Layout Views
+ */
+
+.wpcom-site__logo {
+ color: lighten( $gray, 20% );
+ font-size: 12vw;
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate( -50%, -50% );
+
+ @include breakpoint( ">960px" ) {
+ font-size: 120px;
+ }
+}
+
+.layout__loader {
+ background: $blue-wordpress;
+ border-bottom: 1px solid darken( $blue-wordpress, 4% );
+ height: 46px;
+ margin-left: -30px;
+ position: absolute;
+ left: 50%;
+ top: 0;
+ width: 60px;
+ z-index: 200;
+
+ // set a delay threshold for opacity changes
+ // prevents showing loader on fast connections
+ visibility: hidden;
+ opacity: 0;
+ transition: opacity 0.1s linear;
+ transition-delay: 0.4s;
+}
+
+.layout__loader.is-active {
+ visibility: visible;
+ opacity: 1;
+}
+
+/* =Global
+----------------------------------------------- */
+
+@-webkit-viewport { width : device-width; }
+@-moz-viewport { width : device-width; }
+@-ms-viewport { width : device-width; }
+@-o-viewport { width : device-width; }
+@viewport { width : device-width; }
+
+* {
+ -webkit-tap-highlight-color: rgba( 0, 0, 0, 0);
+}
+
+body {
+ background: $gray-light;
+ -ms-overflow-style: scrollbar;
+}
+
+::selection {
+ background: rgba( $blue-light, 0.7 );
+ color: $gray-dark;
+}
+
+body,
+button,
+input,
+select,
+textarea,
+.button,
+#footer,
+#footer a.readmore {
+ font-family: $sans;
+}
+
+/*rtl:ignore*/
+body.rtl,
+.rtl button,
+.rtl input,
+.rtl select,
+.rtl textarea,
+.rtl .button,
+.rtl #footer,
+.rtl #footer a.readmore {
+ font-family: $sans-rtl;
+}
+
+/*rtl:ignore*/
+:lang(he) body.rtl,
+:lang(he) .rtl button,
+:lang(he) .rtl input,
+:lang(he) .rtl select,
+:lang(he) .rtl textarea,
+:lang(he) .rtl .button,
+:lang(he) .rtl #footer,
+:lang(he) .rtl #footer a.readmore {
+ font-family: $sans;
+}
+
+.notifications {
+ display: inherit;
+}
+
+body {
+ color: #404040;
+ font-size: 15px;
+ line-height: 1.5;
+}
+
+/* Headings */
+h1,h2,h3,h4,h5,h6 {
+ clear: both;
+}
+hr {
+ background: #ccc;
+ border: 0;
+ height: 1px;
+ margin-bottom: 1.5em;
+}
+
+/* Text elements */
+p {
+ margin-bottom: 1.5em;
+}
+ul, ol {
+ margin: 0 0 1.5em 3em;
+}
+ul {
+ list-style: disc;
+}
+ol {
+ list-style: decimal;
+}
+ul ul, ol ol, ul ol, ol ul {
+ margin-bottom: 0;
+ margin-left: 1.5em;
+}
+dt {
+ font-weight: 600;
+}
+dd {
+ margin: 0 1.5em 1.5em;
+}
+b, strong {
+ font-weight: 600;
+}
+dfn, cite, em, i {
+ font-style: italic;
+}
+blockquote {
+ margin: 10px 0 0 0;
+ background: #f7f7f7;
+ padding: 10px 10px 1px;
+ margin: 10px 0 0 0;
+ border-radius: 2px;
+}
+address {
+ margin: 0 0 1.5em;
+}
+pre {
+ background: #eee;
+ font-family: $monospace;
+ font-size: 15px;
+ line-height: 1.6;
+ margin-bottom: 1.6em;
+ padding: 1.6em;
+ overflow: auto;
+ max-width: 100%;
+}
+code, kbd, tt, var {
+ font: 15px $code;
+}
+abbr, acronym {
+ border-bottom: 1px dotted #666;
+ cursor: help;
+}
+mark, ins {
+ background: #fff9c0;
+ text-decoration: none;
+}
+small {
+ font-size: 75%;
+}
+big {
+ font-size: 125%;
+}
+figure {
+ margin: 0;
+}
+table {
+ margin: 0 0 1.5em;
+ width: 100%;
+}
+th {
+ font-weight: 600;
+}
+
+.hide, .hidden { display: none; }
+
+/* Links */
+a,
+a:visited {
+ color: $blue-wordpress;
+}
+
+a:hover,
+a:focus,
+a:active {
+ color: $link-highlight;
+}
+
+.link--caution,
+.link--caution:visited,
+.is-link.link--caution,
+.is-link.link--caution:visited {
+
+ &,
+ &:hover,
+ &:focus,
+ &:active {
+ color: $alert-red;
+ }
+}
+
+html.iframed {
+ overflow: hidden;
+}
+
+.noticon:before,
+.noticon:after {
+ @extend %clear-text;
+
+ font-family: Noticons;
+ line-height: 1;
+}
+
+
+/* =General Layout
+----------------------------------------------- */
+.wp-content {
+ @include clear-fix;
+ position: relative;
+ width: 100%;
+ margin: 47px auto 0;
+ box-sizing: border-box;
+ overflow-y: hidden;
+}
+
+.wp-primary {
+ padding: 32px;
+ margin-left: 272px;
+}
+
+// Tablets
+@include breakpoint( "<960px" ) {
+ .wp-primary {
+ margin-left: 224px;
+ }
+}
+
+// Mobile (Full Width)
+@include breakpoint( "<660px" ) {
+ .wp-primary {
+ margin-left: 0;
+ padding: 8px;
+
+ .focus-sidebar & {
+ transform: translateX( 100vw );
+ }
+
+ .focus-sidebar.is-section-post & {
+ transform: translateX( 0 );
+ }
+ }
+}
+
+
+/* =Sidebar Transitions
+----------------------------------------------- */
+.wp-primary {
+ transition: all 0.15s ease-in-out;
+}
+
+.wpcom-sidebar,
+.site-selector,
+.current-site,
+.sidebar-menu {
+ transform: translateX( 0 );
+ transition: all 0.15s cubic-bezier(0.075, 0.820, 0.165, 1.000);
+}
+
+.site-selector {
+ opacity: 0;
+ pointer-events: none;
+}
+
+.focus-sites {
+ .wp-primary {
+ opacity: 0.2;
+ pointer-events: none;
+ }
+
+ .site-selector {
+ opacity: 1;
+ transform: translateX( 272px );
+ pointer-events: auto;
+
+ @include breakpoint( "<660px" ) {
+ transform: translateX( 100vw );
+ }
+ }
+
+ .wpcom-sidebar {
+ pointer-events: none;
+ }
+
+ .current-site,
+ .sidebar-menu {
+ opacity: 0;
+ transform: translateX( 64px );
+ }
+}
+
+.focus-sidebar {
+ overflow: hidden;
+}
+
+
+/* =Content
+----------------------------------------------- */
+
+.wp-content a {
+ text-decoration: none;
+}
+
+/* =Media
+----------------------------------------------- */
+
+img {
+ max-width: 100%; /* Fluid images for posts, comments, and widgets */
+ height: auto;
+}
+
+/* Make sure embeds and iframes fit their containers */
+embed,
+iframe,
+object {
+ max-width: 100%;
+}
+
+/* Netter min-height for the SoundCloud embeds */
+.wpcom-soundcloud-player,
+.embed-soundcloud iframe {
+ min-height: 150px;
+}
+
+
+/* Disabled blocks of content */
+
+.disabled-block {
+ opacity: 0.5;
+}
+
+body.promo {
+ margin-top: 0;
+}
+
+body.newdash div.wordpress-com-extension-promo {
+ display: none !important;
+}
+
+.design-assets {
+ @include breakpoint( "<660px" ) {
+ padding: 0 6px;
+ }
+}
+
+.design-assets h2,
+.design-assets h2 a:first-child {
+ color: $gray-dark;
+ font-family: $serif;
+ font-size: 38px;
+ font-weight: bold;
+ margin: 40px 0 15px;
+}
+.design-assets h3 {
+ font-weight: bold;
+ margin-bottom: 8px;
+}
+.design-assets hr {
+ background: transparent;
+ clear: both;
+ height: 2px;
+ margin: 15px 0;
+}
+
+.environment-badge {
+ position: fixed;
+ bottom: 16px;
+ left: 16px;
+ z-index: 999;
+ backface-visibility: hidden;
+
+ .bug-report {
+ position: relative;
+ display: inline-block;
+ width: 26px;
+ height: 26px;
+ background-color: $white;
+ border: solid 1px $gray-dark;
+ border-radius: 50%;
+ color: $gray-dark;
+ text-decoration: none;
+ text-align: center;
+ z-index: 1000;
+ transition: border-radius 0.2s ease-out;
+ &:before {
+ @include noticon( '\f50a', 14px );
+ vertical-align: middle;
+ }
+ }
+
+ .environment {
+ position: relative;
+ display: inline-block;
+ font-size: 9px;
+ font-weight: 600;
+ line-height: 1;
+ text-transform: uppercase;
+ padding: 4px 7px 4px 6px;
+ vertical-align: middle;
+ &:before {
+ content: '';
+ position: absolute;
+ left: -4px;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ z-index: -1;
+ background-color: $white;
+ border: solid 1px $gray-dark;
+ }
+ &.is-staging {
+ &:before {
+ background-color: $alert-yellow;
+ }
+ }
+ &.is-wpcalypso {
+ &:before {
+ background-color: #B1EED0;
+ }
+ }
+ &.is-horizon,
+ &.is-feedback {
+ &:before {
+ background-color: $blue-light;
+ }
+ }
+ }
+
+ .notouch & {
+ .bug-report {
+ &:hover {
+ border-radius: 4px;
+ }
+ }
+ }
+}
+
+@include breakpoint( "<960px" ) {
+ // Don't show environment badge on smaller screens. It just gets in the way.
+ .environment-badge {
+ display: none;
+ }
+}
diff --git a/assets/stylesheets/layout/_masterbar.scss b/assets/stylesheets/layout/_masterbar.scss
new file mode 100644
index 00000000000000..ff00691aa7dccb
--- /dev/null
+++ b/assets/stylesheets/layout/_masterbar.scss
@@ -0,0 +1,335 @@
+$masterbar-height: 46px;
+$autobar-height: 20px;
+
+/**
+ * The WordPress.com Masterbar
+ */
+.masterbar {
+ background: $blue-wordpress;
+ border-bottom: 1px solid darken( $blue-wordpress, 4% );
+ color: $white;
+ font-size: 0.9em;
+ height: $masterbar-height;
+ margin: 0;
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100%;
+ z-index: 180;
+ -webkit-font-smoothing: subpixel-antialiased;
+
+ @include breakpoint( ">660px" ) {
+ position: fixed;
+ backface-visibility: hidden;
+ }
+
+ .wpcom-navigation {
+ padding: 0 12px 0 0;
+ }
+
+ #wpnt-notes-panel {
+ top: 48px;
+ }
+
+ .noticon {
+ font-size: 28px;
+ height: 28px;
+ width: 28px;
+ vertical-align: middle;
+ }
+
+ .sections-menu {
+ box-sizing: border-box;
+ list-style: none;
+ margin: 0;
+ padding: 0;
+
+ &.menu-right {
+ float: right;
+ }
+ }
+
+ .section-label {
+ margin-left: 8px;
+ }
+
+ li {
+ float: left;
+
+ &.active {
+ a,
+ a:hover,
+ a:focus {
+ background: $blue-dark;
+ }
+ }
+
+ a {
+ box-sizing: border-box;
+ color: $white;
+
+ cursor: pointer;
+ display: block;
+ height: $masterbar-height;
+ line-height: $masterbar-height;
+ margin: 0;
+ padding: 0 12px;
+ text-decoration: none;
+ transition: background 100ms ease-in-out, color 80ms ease-in-out;
+
+ @include breakpoint( ">660px" ) {
+ padding: 0 15px;
+ }
+
+ &:hover,
+ &:focus {
+ background: $blue-medium;
+ background: rgba( 255, 255, 255, 0.15 );
+ color: $white;
+ outline: 0;
+ }
+ }
+ }
+
+ &.collapsible {
+ @include breakpoint( "<660px" ) {
+ .wpcom-navigation {
+ padding: 0;
+ }
+
+ .sections-menu {
+ width: 40%;
+
+ &.menu-right {
+ width: 60%;
+
+ li {
+ width: 33.33%;
+ }
+ }
+ }
+
+ .noticon {
+ display: block;
+ margin: 0 auto;
+ }
+
+ .section-label {
+ display: block !important;
+ font-size: 10px;
+ height: 13px;
+ margin: 0;
+ overflow: hidden;
+ text-align: center;
+ text-overflow: clip;
+ white-space: nowrap;
+ }
+
+ li {
+ width: 50%;
+
+ a {
+ line-height: 1;
+ padding: 3px 0 0 0;
+ }
+ }
+ }
+ }
+
+ .is-button {
+ margin-left: 10px;
+
+ a {
+ background: $white;
+ border-radius: 5px;
+ color: $blue-wordpress;
+ height: 34px;
+ line-height: 34px;
+ margin-top: 6px;
+ overflow: hidden;
+ padding: 0 3px;
+ vertical-align: middle;
+
+ &:hover,
+ &:focus {
+ background: $gray-light;
+ color: $blue-wordpress;
+ }
+ }
+
+ .noticon {
+ @include breakpoint( "<480px" ) {
+ display: none;
+ }
+ }
+
+ .section-label {
+ margin: 0 10px;
+ }
+ }
+
+ /**
+ * Me
+ */
+ .me {
+ .section-label {
+ @include breakpoint( ">660px" ) {
+ clip: rect( 1px, 1px, 1px, 1px );
+ height: 1px;
+ overflow: hidden;
+ position: absolute !important;
+ width: 1px;
+ }
+ }
+ }
+
+ /**
+ * Notifications
+ */
+ .notifications {
+ display: block;
+ position: relative;
+ white-space: nowrap;
+
+ .section-label {
+ @include breakpoint( ">660px" ) {
+ clip: rect( 1px, 1px, 1px, 1px );
+ height: 1px;
+ overflow: hidden;
+ position: absolute !important;
+ width: 1px;
+ }
+ }
+
+ .noticon-bell {
+ font-size: 24px;
+ position: relative;
+ top: 2px;
+
+ @include breakpoint( ">660px" ) {
+ top: 1px;
+ }
+ }
+
+ .notification-bubble {
+ display: none;
+ }
+
+ &.unread {
+ .notification-bubble {
+ background: $orange-jazzy;
+ border: solid 2px $blue-wordpress;
+ border-radius: 50%;
+ display: block;
+ font-size: 8px;
+ height: 8px;
+ letter-spacing: 0;
+ line-height: 12px;
+ margin: 0;
+ padding: 0;
+ position: absolute;
+ top: 2px;
+ left: calc( 50% - 9px );
+ width: 8px;
+ z-index: 99999;
+
+ // Animation
+ transform: translateZ(0);
+ animation: unread-indication .5s linear both;
+
+ @include breakpoint( ">660px" ) {
+ font-size: 9px;
+ height: 9px;
+ left: 18px;
+ top: 7px;
+ width: 9px;
+ }
+ }
+
+ &.initial-load .notification-bubble {
+ animation: none;
+ }
+
+ &.active {
+ .notification-bubble {
+ border-color: $blue-dark;
+ }
+ }
+ }
+ }
+
+ .wpcom-title {
+ .section-label {
+ font-size: 16px;
+ font-weight: 300;
+ letter-spacing: 0.5px;
+ line-height: 46px;
+ line-height: 4.6rem;
+ text-decoration: none;
+ @include breakpoint( "<480px" ) {
+ display: none;
+ }
+ }
+
+ @include breakpoint( ">480px" ) {
+ .noticon {
+ height: 31px; // This was the only way I found to center the noticon vertically
+ }
+ }
+
+ .tld {
+ color: rgba(255, 255, 255, 0.6);
+ }
+ }
+}
+
+@keyframes unread-indication {
+ 30% {
+ transform: scale(1.5);
+ }
+ 60% {
+ transform: scale(.85);
+ }
+ 80% {
+ transform: scale(1.1);
+ }
+}
+
+.masterbar .gravatar {
+ border: 2px solid $white;
+ border-radius: 50%;
+ height: 22px;
+ margin-bottom: 2px;
+ vertical-align: middle;
+ width: 22px;
+
+ @include breakpoint( "<660px" ) {
+ display: block;
+ height: 20px;
+ margin: 2px auto 2px;
+ width: 20px;
+ }
+}
+
+.rtl {
+ .masterbar {
+ .noticon-reader,
+ .new-post,
+ .noticon-bell {
+ filter: fliph; /* IE */
+ transform: rotateY( 180deg );
+ }
+ }
+}
+
+// Horizon replacement logo
+.masterbar .noticon.noticon-horizon {
+ font-size: 59px;
+ margin: -32px 21px 0 -27px;
+ padding-left: 15px;
+
+ @include breakpoint( "<660px" ) {
+ margin: -15px 0px 15px 8px;
+ padding-left: 0;
+ }
+}
diff --git a/assets/stylesheets/layout/_overlay.scss b/assets/stylesheets/layout/_overlay.scss
new file mode 100644
index 00000000000000..c89199f329945a
--- /dev/null
+++ b/assets/stylesheets/layout/_overlay.scss
@@ -0,0 +1,204 @@
+/*
+ * WP.com Overlay
+ * Used for Site specific contexts and the Editor
+ */
+
+// Overlay
+// 1: Indicate that clicking on the background performs an action (closes overlay)
+// 2: Reverse 1)
+
+.wp-overlay {
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 9999;
+ height: 100%;
+ overflow: scroll;
+ -webkit-overflow-scrolling: touch;
+ transform: translate3d( 0, 100%, 0 );
+ background-color: rgba( $gray-dark, 0 );
+ cursor: pointer; // 1
+
+ .overlay-content {
+ cursor: default; // 2
+ }
+
+ // Make overlay cover entire view
+ .overlay-is-front & {
+ transform: translate3d( 0, 0, 0 );
+ }
+
+ // Make overlay visually appear to the user
+ .overlay-open & {
+ background-color: rgba( $gray-dark, 0.9 );
+ }
+
+ .wpcom-masterbar {
+ background: transparent;
+ box-shadow: none;
+ position: absolute;
+ right: 0;
+ left: 0;
+ top: -56px;
+ width: auto;
+
+ .overlay-navigation {
+ background: transparent;
+ padding: 0;
+ overflow: hidden;
+
+ .user-actions ul {
+ list-style-type: none;
+ }
+
+ .user-actions,
+ .user-actions ul,
+ .user-actions ul li {
+ float: none;
+ }
+ }
+ }
+
+ // Close button
+ // 1: //To have the button label be optically centered, moving the icon half an icon size to the left
+ // 2: Optically align label including the x-icon
+ // 2: Needed for now to override settings, should be looked over once the overlay is more widely used
+
+ // Hack for now to make the button appear like a regular primary button
+ // Only used for the overlay Close button in Stats overlays
+ .user-actions ul li .button {
+ padding: 0 16px 0 22px; // 1
+ line-height: 40px;
+ height: 40px; // 2
+ float: right;
+
+ // Need to overrule default masterbar settings on hover :(
+ // These are copied from _buttons.scss
+ &:hover {
+ background: $gray-light;
+ box-shadow: 0 -1px 0 rgba(255,255,255,0.8) inset;
+ }
+ }
+
+ // Include X noticon
+ // Only used for the overlay Close button in Stats overlays
+ // TODO: shouldn't be called .settings-done but currently all secondary buttons are
+ .user-actions ul li a.settings-done::before {
+ @include noticon( '\f405' , 24px );
+ line-height: 40px;
+ margin-left: -12px; // 2
+ }
+
+ // Keep .site-settings here to not break Jetpack and Stats
+ // until those are updated
+ .site-settings .site {
+ float: left;
+ width: 28%;
+
+ @include breakpoint( "<660px" ) {
+ float: none;
+ width: 100%;
+ }
+
+ .site-content {
+ margin-bottom: 0;
+
+ @include breakpoint( "<660px" ) {
+ margin: 6px;
+ }
+ }
+
+ .site-options,
+ .site-more-options {
+ display: none;
+ }
+
+ &.jetpack {
+ border-bottom: 2px solid #8cc258;
+ }
+ }
+
+ // 1: Height of masterbar (46px) + (10px+2) margins top/bottom
+ .wp-content {
+ @include breakpoint( "<660px" ) {
+ background: lighten( $gray, 30% );
+ margin-top: 66px;
+ }
+ }
+
+ .content-header {
+ @include breakpoint( "<660px" ) {
+ margin-top: 10px;
+ }
+ .noticon-menu {
+ display: none;
+ }
+ }
+
+ /**
+ * Iframe within the overlay to contain
+ * elements like the Customizer
+ */
+ iframe {
+ width: 100%;
+ height: 100%;
+ }
+}
+
+.overlay-content {
+ opacity: 0;
+ background: lighten( $gray, 30% );
+ margin: 0 auto 0;
+ max-width: 960px;
+ overflow: visible;
+
+ .overlay-is-front & {
+ opacity: 1;
+ }
+
+ @include breakpoint( ">960px" ) {
+ border-radius: 3px;
+ }
+}
+
+.overlay-open body, .overlay-open {
+ overflow: hidden;
+ overflow-y: hidden;
+}
+
+// 1: Roughly the width of the close button with all its bells and whistles on mobile
+.wp-overlay .current-site {
+ background-color: transparent;
+ box-shadow: none;
+ text-align: left;
+ padding-right: 100px; // 1
+
+ @include breakpoint( "<960px" ) {
+ padding-left: 10px;
+ }
+}
+
+.wp-overlay .actions-menu {
+ position: absolute;
+ top: 6px;
+ right: 0;
+
+ @include breakpoint( "<960px" ) {
+ right: 10px;
+ }
+}
+
+.wp-overlay .current-site .site {
+ color: lighten( $gray, 30% );
+ padding-left: 50px;
+}
+
+.wp-overlay .current-site .site-icon {
+ left: 0;
+}
+
+.wp-overlay .current-site .user-icon {
+ border-radius: 50%;
+}
diff --git a/assets/stylesheets/layout/_sidebar.scss b/assets/stylesheets/layout/_sidebar.scss
new file mode 100644
index 00000000000000..29e2d8bd1ca297
--- /dev/null
+++ b/assets/stylesheets/layout/_sidebar.scss
@@ -0,0 +1,286 @@
+// Setting up the sidebar container
+.sidebar,
+.secondary-cart { // This selects the sidebar on /checkout
+ overflow: auto;
+ padding: 0;
+ background: lighten( $gray, 30% );
+ position: fixed;
+ top: 47px;
+ bottom: 0;
+
+ @include breakpoint( "<960px" ) {
+ border-right: 1px solid lighten( $gray, 25% );
+ width: 224px;
+ }
+
+ @include breakpoint( ">960px" ) {
+ border-right: 1px solid lighten( $gray, 25% );
+ left: 0;
+ width: 272px;
+ }
+
+ @include breakpoint( "<660px" ) {
+ left: -100vw;
+ width: 100vw;
+ max-height: calc(100vh - 47px );
+ pointer-events: none;
+ transform: translateX( 0 );
+ transition: all 0.15s cubic-bezier(0.770, 0.000, 0.175, 1.000);
+
+ .focus-sidebar & {
+ pointer-events: auto;
+ -webkit-overflow-scrolling: touch;
+ transform: translateX( 100vw );
+ }
+
+ .focus-sites & {
+ transform: translateX( 100vw );
+ }
+ }
+}
+
+
+// Clearing out the sidebar list styles
+.wpcom-sidebar {
+ margin: 0;
+
+ ul {
+ list-style: none;
+ margin: 0;
+ }
+
+ li {
+ position: relative;
+ }
+}
+
+
+// Sidebar group headings
+.sidebar-heading {
+ color: darken( $gray, 10% );
+ font-weight: 600;
+ font-size: 12px;
+ margin: 16px 8px 6px 14px;
+}
+
+
+// Menu Links
+.sidebar-menu {
+ display: block;
+
+ @include breakpoint( "<660px" ) {
+ margin-top: 24px;
+ }
+
+ li {
+ display: flex;
+
+ @include breakpoint( "<660px" ) {
+ background-color: $gray-light;
+ border-bottom: 1px solid rgba( lighten( $gray, 20% ), 0.5 );
+
+ &:first-child {
+ border-top: 1px solid lighten( $gray, 20% );
+ }
+
+ &:last-child {
+ border-bottom: 1px solid lighten( $gray, 20% );
+ }
+ }
+ }
+
+ a:first-child {
+ flex: 1 0 auto;
+ width: 0;
+
+ // Fade overlay for longer labels
+ &:after {
+ content: '';
+ text-align: right;
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ width: 15%;
+ background: linear-gradient(
+ to right,
+ rgba( lighten( $gray, 30% ), 0 ),
+ rgba( lighten( $gray, 30% ), 1 ) 50% );
+
+ @include breakpoint( "<660px" ) {
+ background: linear-gradient(
+ to right,
+ rgba( $gray-light, 0 ),
+ rgba( $gray-light, 1 ) 50% );
+ }
+ }
+ }
+
+ a {
+ font-size: 13px;
+ position: relative;
+ padding: 14px 16px 14px 55px;
+ color: $gray-dark;
+ box-sizing: border-box;
+ white-space: nowrap;
+ overflow: hidden;
+ display: flex;
+
+ &:focus {
+ outline: none;
+ }
+
+ @include breakpoint( "<660px" ) {
+ padding: 18px 16px 18px 53px;
+ }
+ }
+
+ a.add-new {
+ padding: 2px 8px 3px 8px;
+ height: 24px;
+ margin: 11px 8px 0 0;
+ line-height: 18px;
+ background-color: $gray-light;
+ color: darken( $gray, 20% );
+ font-size: 11px;
+ font-weight: 600;
+ border-radius: 3px;
+ border: 1px solid lighten( $gray, 20% );
+
+ @include breakpoint( "<660px" ) {
+ font-size: 14px;
+ height: 35px;
+ padding: 8px 16px;
+ margin: 10px 10px 0 0;
+ }
+ }
+
+ a.plan-name {
+ padding: 3px 8px 4px 8px;
+ position: absolute;
+ top: 12px;
+ right: 8px;
+ color: darken( $gray, 20% );
+ font-size: 11px;
+
+ @include breakpoint( "<660px" ) {
+ top: 16px;
+ right: 16px;
+ }
+ }
+
+ .gridicon {
+ position: absolute;
+ top: 11px;
+ left: 20px;
+ fill: $gray;
+ height: 24px;
+ width: 24px;
+
+ @include breakpoint( "<660px" ) {
+ top: 15px;
+ left: 16px;
+ height: 24px;
+ width: 24px;
+ }
+
+ // External indicator for sections that aren't available in Calypso
+ &.gridicons-external {
+ position: absolute;
+ top: 13px;
+ right: 8px;
+ left: auto;
+ z-index: 1;
+ height: 18px;
+
+ @include breakpoint( "<660px" ) {
+ top: 17px;
+ }
+ }
+ }
+
+ .noticon-external {
+ position: absolute;
+ top: 15px;
+ right: 19px;
+ z-index: 1;
+ color: $gray;
+
+ @include breakpoint( "<660px" ) {
+ top: 9px;
+ right: 16px;
+ font-size: 32px;
+ }
+ }
+}
+
+
+// Selected Menu
+@include breakpoint( ">660px" ) {
+ .sidebar-menu .selected {
+ background-color: $gray;
+
+ a {
+ color: $white;
+
+ &:first-child:after {
+ background: linear-gradient(
+ to right,
+ rgba( $gray, 0 ),
+ rgba( $gray, 1 ) 50% );
+ }
+ }
+
+ a.add-new {
+ color: darken( $gray, 30% );
+ border: 1px solid darken( $gray, 10% );
+ }
+
+ .gridicon {
+ fill: $white;
+ }
+
+ &.is-action-button-selected a {
+ &:first-child:after {
+ background: linear-gradient(
+ to right,
+ rgba( $gray-light, 0 ),
+ rgba( $gray-light, 1 ) 50% );
+ }
+ }
+ }
+}
+
+
+// Menu Hover
+.notouch .sidebar-menu li:hover {
+ &:not(.selected) {
+ background-color: $gray-light;
+
+ a {
+ color: $blue-medium;
+
+ &:first-child:after {
+ background: linear-gradient(
+ to right,
+ rgba( $gray-light, 0 ),
+ rgba( $gray-light, 1 ) 50% );
+ }
+
+ &.add-new {
+ background-color: $white;
+ color: $gray-dark;
+ }
+ }
+
+ .gridicon {
+ fill: $blue-medium;
+ }
+ }
+}
+
+.notouch .sidebar-menu li:not(.selected) a {
+ &.add-new:hover {
+ color: $blue-medium;
+ }
+}
diff --git a/assets/stylesheets/sections/_billing-history.scss b/assets/stylesheets/sections/_billing-history.scss
new file mode 100644
index 00000000000000..4159da14bef5c0
--- /dev/null
+++ b/assets/stylesheets/sections/_billing-history.scss
@@ -0,0 +1,630 @@
+.billing-history-page {
+
+ #billing-history-wrapper * {
+ box-sizing: border-box;
+ }
+
+ #billing-history-content,
+ #upcoming-charges {
+ background-color: $white;
+ }
+
+ .billing-history-header p {
+ font-size: 14px;
+ }
+
+ // Transactions Table
+ // =============================================
+
+ .transactions {
+ border-collapse: collapse;
+ }
+
+ .transactions__no-results {
+ display: table-row;
+
+ td {
+ padding: 20px 0;
+ text-align: center;
+ }
+ }
+
+ // Table Header
+ // -----------------------------------
+
+ .transactions thead {
+ background-color: $gray-light;
+ }
+
+ .transactions thead .header-row {
+ vertical-align: top;
+ height: 40px;
+ }
+
+ .transactions thead .header-column {
+ vertical-align: top;
+ padding-top: 4px;
+ }
+
+ .search-form {
+ text-align: right;
+ }
+
+ .search_terms {
+ display: inline-block;
+ font-size: 13px;
+ margin-top: 2px;
+ margin-right: 12px;
+ text-align: left;
+ width: 100px;
+ padding: 3px 9px;
+ background-color: $white;
+ border-radius: 15px;
+ border: 1px solid lighten( $gray, 30% );
+ transition: width 0.25s ease-out;
+ outline: 0;
+
+ &:focus,
+ &:active {
+ width: 150px;
+ }
+ }
+
+ .reset-search {
+ display: none;
+ }
+
+ // Table Body
+ // -----------------------------------
+
+ .transaction {
+ color: $gray-dark;
+ font-size: 14px;
+ height: 66px;
+ border-bottom: 1px solid lighten( $gray, 30% );
+ }
+
+ .transaction td {
+ vertical-align: top;
+ padding: 10px 0;
+ }
+
+ .transactions tbody .date {
+ width: 140px;
+ padding: 12px;
+ }
+
+ .transactions tbody .trans-app {
+ padding-left: 12px;
+ padding-right: 12px;
+ }
+
+ .transactions tbody .amount {
+ text-align: right;
+ padding: 12px;
+ width: 110px;
+ }
+
+ .trans-wrap {
+ @include clear-fix;
+ }
+
+ .service-name {
+ strong {
+ font-weight: normal;
+ }
+
+ small {
+ color: $gray;
+ display: block;
+ font-size: 12px;
+ font-style: italic;
+ }
+ }
+
+ .service-description {
+ float: left;
+ }
+
+ .transaction-links {
+ font-size: 12px;
+ margin: 4px 0 0 0;
+
+ a {
+ padding-right: 8px;
+ }
+ }
+
+ // Filter Popovers
+ // =============================================
+
+ .filter-popover {
+ position: relative;
+ display: inline-block;
+ padding: 4px 12px;
+ vertical-align: top;
+ }
+
+ .filter-popover-content {
+ display: none;
+ position: absolute;
+ top: 35px;
+ z-index: 1;
+ border-radius: 4px;
+ background-color: $white;
+ box-shadow: 0 1px 6px rgba(0,0,0,0.2), 0 0 25px 10px rgba(0,0,0,0.1);
+ outline: 0;
+
+ &.datepicker {
+ right: -115px;
+ }
+
+ &.app {
+ right: -147px;
+ }
+ }
+
+ .popped .filter-popover-content {
+ display: block;
+ }
+
+ .filter-popover-content:before {
+ @extend %clear-text;
+
+ display: block;
+ position: absolute;
+ top: -25px;
+ left: 21px;
+ color: $blue-wordpress;
+ content: '\f500';
+ font-family: Noticons;
+ font-size: 20px;
+ }
+
+ .filter-popover-content .overflow {
+ overflow-x: auto;
+ overflow-y: hidden;
+ }
+
+ .filter-popover-content table {
+ margin-bottom: 0;
+ }
+
+ .filter-popover-content tr:hover td {
+ background: rgba(0,0,0,0.02);
+ cursor: pointer;
+ color: $blue-light;
+ }
+
+ .filter-popover-content tr.selected td {
+ background-color: $gray-dark;
+ color: $white;
+ }
+
+ .filter-popover-content th {
+ @extend %clear-text;
+ padding: 8px 10px;
+ font-size: 12px;
+ font-weight: 600;
+ color: $white;
+ background: $blue-wordpress;
+ text-shadow: 0 -1px 0 rgba(0,0,0,0.2);
+ text-align: left;
+ border-bottom: 1px solid rgba(0,0,0,0.3);
+ white-space: nowrap;
+ }
+
+ .filter-popover-content th.transactions-header__count {
+ text-align: right;
+ }
+
+ .filter-popover-content thead:first-of-type th:first-child {
+ border-top-left-radius: 4px;
+ }
+
+ .filter-popover-content thead:first-of-type th:last-child {
+ border-top-right-radius: 4px;
+ }
+
+ .filter-popover-content td {
+ padding: 8px 10px;
+ font-size: 13px;
+ border-top: 1px dotted rgba(0,0,0,0.1);
+ color: $blue-wordpress;
+ }
+ .filter-popover-content td.descriptor {
+ font-weight: 400;
+ font-size: 13px;
+ }
+ .filter-popover-content td.transactions-header__count {
+ font-weight: 600;
+ font-size: 14px;
+ text-align: right;
+ color: #777;
+ color: rgba(0,0,0,0.8);
+ }
+
+ .filter-popover-toggle {
+ padding: 5px 10px;
+ border-radius: 3px;
+ cursor: pointer;
+ border-radius: 3px;
+ border: 1px solid transparent;
+
+ &:hover {
+ background-color: $white;
+ border-color: rgba(0,0,0,0.05);
+ }
+
+ &:after {
+ display: inline-block;
+ content: '\f431';
+ font-family: Noticons;
+ color: $blue-wordpress;
+ margin-left: 5px;
+ position: relative;
+ top: 2px;
+ }
+ }
+
+ .popped .filter-popover-toggle {
+ background-color: rgba(0,0,0,0.1);
+ border-color: rgba(0,0,0,0.05);
+ box-shadow: 0 1px 0 rgba(255,255,255,0.4);
+ }
+
+ // View Receipt Modal
+ // =============================================
+
+ .wp-overlay {
+ .overlay-content {
+ background: $white;
+ width: 640px;
+ max-width: 100%;
+ }
+
+ .overlay-navigation {
+ background: $gray-dark;
+ padding: 10px;
+ }
+
+ .wp-content {
+ background: $white;
+
+ @include breakpoint( "<960px" ) {
+ padding: 0;
+ }
+ }
+
+ .settings-done {
+ margin-right: 10px;
+ }
+
+ .current-site .site {
+ padding-left: 20px;
+
+ @include breakpoint( "<480px" ) {
+ padding-left: 0;
+ }
+ }
+
+ .site-title,
+ .site-icon {
+ display: none;
+ }
+
+ .site-description {
+ font-size: 20px;
+ padding: 3px;
+ }
+ }
+
+
+ .view-receipt-wrapper {
+ margin: 30px auto 0 auto;
+ padding: 20px 0 0 0;
+
+ .app-overview {
+ min-height: 65px;
+ padding: 10px 40px;
+ position: relative;
+ overflow: auto;
+
+ @include breakpoint( "<480px" ) {
+ padding: 10px 20px;
+ }
+
+ img {
+ max-width: 65px;
+ min-height: 65px;
+ float: left;
+
+ @include breakpoint( "<480px" ) {
+ left: 20px;
+ }
+ }
+
+ h2 {
+ clear: none;
+ float: left;
+ padding: 10px 20px;
+
+ @include breakpoint( "<480px" ) {
+ padding-top: 0;
+ }
+
+ small {
+ display: block;
+ }
+ }
+
+ .transaction-date {
+ color: $gray;
+ font-size: 13px;
+ font-style: italic;
+ padding: 5px 0 0 0;
+
+ @include breakpoint( ">480px" ) {
+ position: absolute;
+ top: 10px;
+ right: 40px;
+ }
+ }
+ }
+
+ ul {
+ background: $gray-light;
+ list-style: none;
+ padding: 20px 40px;
+ margin: 20px 0 0 0;
+ overflow: auto;
+
+ @include breakpoint( "<480px" ) {
+ padding: 20px;
+ }
+
+ li {
+ color: $gray-dark;
+ font-size: 13px;
+ margin: 0 0 15px 0;
+ padding: 0;
+
+ &:last-child {
+ margin: 0;
+ }
+
+ strong {
+ color: darken( $gray, 20% );
+ display: block;
+ font-size: 12px;
+ font-weight: 600;
+ margin: 0 5px 0 0;
+ text-transform: uppercase;
+ }
+ }
+
+ li.billing-details div:hover {
+ border: 2px black dashed;
+ }
+ }
+
+ .resend {
+ padding: 0;
+ margin-left: 0;
+ }
+
+ .receipt {
+ padding: 30px 40px;
+
+ @include breakpoint( "<480px" ) {
+ padding: 30px 20px;
+ }
+
+ h4 {
+ font-size: 20px;
+ }
+
+ .receipt-line-items {
+ margin: 0;
+ padding: 0;
+ width: 100%;
+
+ th {
+ border-bottom: 2px solid lighten( $gray, 25% );
+ color: $gray;
+ font-size: 12px;
+ font-weight: 400;
+ text-transform: uppercase;
+ width: auto;
+ }
+
+ th.receipt-desc {
+ width: 75%;
+ text-align: left;
+ }
+
+ th.receipt-amount {
+ text-align: right;
+ }
+
+ td,
+ th {
+ padding: 10px 0;
+ }
+
+ td.receipt-amount {
+ color: #444;
+ text-align: right;
+ vertical-align: middle;
+ }
+
+ .receipt-item-name {
+ small {
+ color: $gray;
+ font-size: 13px;
+ margin-left: 5px;
+ text-transform: lowercase;
+ }
+ em {
+ color: $gray;
+ display: block;
+ font-size: 13px;
+ }
+ }
+
+ tbody tr {
+ td {
+ border-bottom: 1px solid lighten( $gray, 30% );
+ }
+
+ &:last-child td {
+ border: none;
+ }
+ }
+
+ tfoot {
+ font-weight: 400;
+ text-align: right;
+ vertical-align: bottom;
+
+ td {
+ border-top: 2px solid lighten( $gray, 25% );
+ padding-bottom: 0;
+ }
+ }
+ }
+ }
+
+ .receipt-links {
+ border-top: 1px solid lighten( $gray, 25% );
+ margin: 0;
+ overflow: auto;
+ padding: 30px 40px;
+
+ @include breakpoint( "<480px" ) {
+ padding: 20px;
+ }
+
+ .button {
+ display: block;
+ margin: 10px 0;
+ padding: 15px;
+ text-align: center;
+
+ @include breakpoint( ">480px" ) {
+ float: left;
+ margin: 0 1%;
+ width: 48%;
+ }
+ }
+ }
+ }
+}
+
+@include breakpoint( "<480px" ) {
+ .billing-history-page {
+ thead {
+ display: block;
+ }
+
+ .transactions {
+ display: block;
+ max-width: 100%;
+
+ thead {
+ margin: 0 -16px;
+ }
+
+ .transaction {
+ display: inline-block;
+ height: auto;
+ padding: 1em;
+
+ &:first-of-type {
+ border-top: none;
+ }
+ }
+
+ .header-row {
+ display: block;
+ height: auto !important;
+ padding: .5em 0;
+
+ .date, .trans-app {
+ display: none;
+ }
+
+ .search-field {
+ display: block;
+ padding-top: 0;
+ width: 100%;
+ }
+
+ .search-form {
+ text-align: center;
+
+ .search_terms {
+ margin: 0;
+ width: 80%;
+ }
+ }
+ }
+
+ tbody {
+ td {
+ display: inline-block;
+ width: 100%;
+ padding: 0 !important;
+
+ &.amount {
+ text-align: left;
+ }
+
+ &.date {
+ font-weight: 600;
+ }
+ }
+ }
+ }
+
+ .view-receipt-wrapper {
+ .app-overview {
+ h2 {
+ small {
+ display: block;
+ }
+ }
+ }
+
+ dl {
+ padding: 0 30px;
+
+ dt {
+ text-align: left;
+ width: 50%;
+ }
+
+ dd {
+ width: 50%;
+ }
+ }
+ }
+ }
+}
+
+@media print {
+ #overlay-header,
+ #primary,
+ .receipt-links {
+ display: none;
+ }
+
+ .billing-history-page {
+ .wp-overlay .overlay-content,
+ .wp-content {
+ margin: 0 auto;
+ max-width: none;
+ width: 100%;
+ }
+ }
+}
diff --git a/assets/stylesheets/sections/_checkout.scss b/assets/stylesheets/sections/_checkout.scss
new file mode 100644
index 00000000000000..aa00bc97ad0106
--- /dev/null
+++ b/assets/stylesheets/sections/_checkout.scss
@@ -0,0 +1,1077 @@
+.checkout {
+ position: relative;
+
+ .payment-box {
+ height: 0;
+ margin-bottom: 0;
+ opacity: 0;
+ overflow: hidden;
+ padding: 0;
+ transform: translateZ(0) scale(.8); // Zoom out effect
+ transition: all .3s ease-in-out;
+ visibility: hidden; // To deal with layering issues
+ width: 100%;
+
+ &:not(.domain-details) {
+ @include breakpoint( "<660px" ) {
+ background-color: transparent;
+ box-shadow: none;
+ }
+ }
+
+ &.selected {
+ height: auto;
+ opacity: 1;
+ transform: translateZ(0) scale(1); // Zoom in effect
+ visibility: visible; // To deal with layering issues
+ }
+
+ &.is-empty {
+ .payment-box-section {
+ border: 1px solid lighten( $gray, 30% );
+ margin: 5px 0;
+ display: flex;
+ flex-flow: row wrap;
+ justify-content: space-around;
+ background-color: $white;
+ padding: 10px;
+ }
+
+ .placeholder {
+ animation: pulse-light 0.8s ease-in-out infinite;
+ background: lighten( $gray, 20% );
+ width: 100%;
+ height: 100%;
+ }
+
+ .payment-box__title {
+ @extend .placeholder;
+ height: 22px;
+ width: 130px;
+
+ :after {
+ content: '';
+ }
+
+ }
+
+ .payment-box__header {
+ height: 16px;
+ width: 170px;
+ flex: 0 0 170px;
+ }
+
+ .placeholder-row {
+ height: 40px;
+ flex: 0 0 100%;
+ margin-bottom: 15px;
+ }
+
+ .placeholder-col-narrow {
+ height: 40px;
+ flex: 1 1 auto;
+ margin-bottom: 15px;
+
+ @include breakpoint( ">480px" ) {
+ flex: 2 1 auto;
+ }
+ }
+
+ .placeholder-inline-pad {
+ padding-right: 15px;
+ }
+
+ .placeholder-inline-pad-only-wide {
+ @include breakpoint( ">480px" ) {
+ padding-right: 15px;
+ }
+ }
+
+ .placeholder-col-wide {
+ height: 40px;
+ margin-bottom: 15px;
+ flex: 0 0 100%;
+
+ @include breakpoint( ">480px" ) {
+ flex: 6 3 auto;
+ }
+ }
+
+ .placeholder-button {
+ height: 50px;
+ width: 100%;
+
+ @include breakpoint ( ">480px" ) {
+ width: 80px;
+ height: 40px;
+ }
+ }
+
+ .placeholder-button-container {
+ margin-top: 55px;
+
+ @include breakpoint( ">480px" ) {
+ margin-top: 20px;
+ }
+ }
+
+ .payment-box-hr {
+ margin: 40px 0 20px 0;
+ width: 100%;
+ height: 0;
+ border-bottom: 1px solid lighten( $gray, 30% );
+
+ @include breakpoint( "<480px" ) {
+ display: none;
+ }
+ }
+ }
+ }
+
+ .payment-box__content {
+ min-height: 140px;
+ margin-top: 10px;
+ }
+
+ h5 {
+ color: darken( $gray, 10% );
+ font-size: 15px;
+ font-weight: 600;
+ opacity: 0.7;
+ text-transform: uppercase;
+
+ :after {
+ @include noticon( '\f470', (13 / 12) * 1em );
+ float: right;
+ }
+ }
+
+ .box-padding {
+ padding: 16px 0;
+
+ @include breakpoint( ">660px" ) {
+ padding: 30px 30px 20px 30px;
+ }
+ }
+
+ .domain-details {
+ .box-padding {
+ @include breakpoint( "<660px" ) {
+ padding: 16px;
+ }
+ }
+ }
+
+ form {
+ margin-top: 5px;
+
+ @include breakpoint( ">660px" ) {
+ @include clear-fix;
+ }
+ }
+
+ button[type=submit].button-pay {
+ @include breakpoint( "<660px" ) {
+ width: 100%;
+
+ #wpcom & {
+ min-height: 50px;
+ }
+ }
+
+ @include breakpoint( ">660px" ) {
+ clear: both;
+ float: left;
+ }
+ }
+
+ input[type=number] {
+ &::-webkit-outer-spin-button,
+ &::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+ }
+ }
+
+ // Floating labels
+ // -----------------------------------
+
+ .checkout-field {
+ margin-top: 15px;
+ position: relative;
+
+ select {
+ font-size: 15px;
+ width: 100%;
+ }
+
+ &.invalid {
+ input,
+ select {
+ border-color: $alert-red;
+ }
+ }
+
+ input[ disabled ] {
+ cursor: not-allowed;
+ }
+ }
+
+ // Payment Boxes
+ // =============================================
+
+ .checkout-terms {
+ color: darken( $gray, 10% );
+ font-size: 12px;
+ font-weight: 100;
+ margin: 10px 0;
+ padding: 0 30px;
+ text-align: center;
+
+ @include breakpoint( ">660px" ) {
+ margin-bottom: 20px;
+ padding: 0;
+ text-align: left;
+ }
+ }
+
+ .payment-box-actions {
+ @include breakpoint( ">660px" ) {
+ margin: 20px -30px 0px -30px;
+ padding: 20px 30px 0 30px;
+ border-top: 1px solid lighten( $gray, 30% );
+ @include clear-fix;
+ }
+ }
+
+ .credit-card-payment-box {
+ .payment-box-sections {
+ background-color: $white;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
+
+ @include breakpoint( ">660px" ) {
+ box-shadow: none;
+ }
+ }
+
+ .payment-box-section {
+ cursor: pointer;
+ border-bottom: 1px solid lighten( $gray, 30% );
+
+ &:first-of-type {
+ border-top: 1px solid lighten( $gray, 30% );
+ }
+
+ &.selected {
+ cursor: default;
+ }
+ }
+
+ .payment-box-section-inner {
+ border-left: 1px solid lighten( $gray, 30% );
+ padding-left: 2px;
+ position: relative;
+ border-right: 1px solid lighten( $gray, 30% );
+ min-height: 50px;
+ }
+
+ .payment-box-section.selected .payment-box-section-inner {
+ background-color: #fafdf6;
+ padding-left: 0;
+ }
+
+ .payment-box-section.selected:not( .no-stored-cards ) {
+ .payment-box-section-inner {
+ border-left: 3px solid $alert-green;
+ }
+ .new-card-fields {
+ background-color: #fafdf6;
+ }
+ }
+
+ .no-stored-cards .new-card-fields > .checkout-field:first-child {
+ margin-top: 0;
+ }
+
+ .payment-box-section .new-card-toggle {
+ box-shadow: none;
+ cursor: pointer;
+ font-size: 13px;
+ position: absolute;
+ }
+
+ .payment-box-section .new-card-fields {
+ background-color: $white;
+ max-height: 0;
+ overflow: hidden;
+ padding: 0 15px 0 12px;
+ position: relative;
+ transition: all 0.5s ease-in-out;
+ }
+
+ .payment-box-section.selected .new-card-fields {
+ max-height: 500px;
+ margin-bottom: 0;
+ padding-top: 15px;
+ padding-bottom: 15px;
+ }
+
+ .new-card-toggle {
+ color: $blue-wordpress;
+ padding: 15px 15px 15px 12px;
+ border: 0;
+ background: transparent;
+ }
+
+ .new-card-header {
+ color: $blue-medium;
+ font-weight: 400;
+ }
+
+ .all-fields-required {
+ color: lighten( $gray, 10% );
+ display: block;
+ font-size: 12px;
+ font-style: italic;
+
+ @include breakpoint( ">660px" ) {
+ top: 7px;
+ }
+
+ &.has-saved-cards {
+ top: 18px;
+
+ @include breakpoint( ">660px" ) {
+ position: absolute;
+ right: 18px;
+ }
+ }
+ }
+ }
+
+ // PayPal Payment Box
+ // -----------------------------------
+
+ .paypal-payment-box,
+ .credits-payment-box {
+ .payment-box-section {
+ background-color: $white;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
+
+ @include breakpoint( ">660px" ) {
+ border: 1px solid lighten( $gray, 30% );
+ box-shadow: none;
+ }
+ }
+ }
+
+ .paypal-payment-box {
+ .payment-box-section {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ padding-bottom: 15px;
+ padding-right: 15px;
+ }
+
+ .country,
+ .postal-code {
+ margin-left: 15px;
+ }
+
+ .country {
+ flex-basis: auto;
+ flex-grow: 3;
+ flex-shrink: 0;
+
+ label {
+ display: none;
+ }
+ }
+
+ .postal-code {
+ flex-basis: 8em;
+ flex-grow: 2;
+ flex-shrink: 0;
+ margin-top: 15px;
+
+ label {
+ display: none;
+ }
+ }
+ }
+
+ // Credits Payment Box
+ // -----------------------------------
+ .credits-payment-box {
+ .payment-box-section {
+ box-sizing: border-box;
+ min-height: 91px;
+ padding: 20px 20px 20px 80px;
+ position: relative;
+
+ &::before {
+ color: $blue-medium;
+ left: 10px;
+ position: absolute;
+ top: 15px;
+ @include noticon( '\f205', 60px );
+ }
+
+ > h6 {
+ color: $blue-medium;
+ font-size: 18px;
+ }
+
+ > span {
+ color: darken( $gray, 10% );
+ font-size: 15px;
+ }
+
+ @include breakpoint( ">660px" ) {
+ padding-left: 100px;
+ }
+ }
+ }
+
+ // Supporting Text / Fine Print
+ // -----------------------------------
+ .supporting-text {
+ border-top: 1px solid lighten( $gray, 20% );
+ font-size: 13px;
+ list-style: none;
+ margin: 0;
+ padding: 15px 0;
+ @include clear-fix;
+
+ @include breakpoint( ">660px" ) {
+ border-bottom: 1px solid lighten( $gray, 20% );
+ margin: 30px 0;
+ }
+
+ li {
+ color: lighten( $gray, 10% );
+ text-align: center;
+
+ @include breakpoint( ">660px" ) {
+ float: left;
+ margin: 0 5%;
+ width: 40%;
+ }
+
+ @include breakpoint( "<660px" ) {
+ margin: 0;
+ padding: 15px;
+ }
+
+ h6 {
+ color: darken( $gray, 20% );
+ font-size: 14px;
+ font-weight: 600;
+ }
+
+ p {
+ font-size: 12px;
+ font-weight: 100;
+ margin: 10px 0 0 0;
+ }
+ }
+ }
+
+ .credit-card-supporting-text__refund-link {
+ white-space: nowrap;
+ color: lighten( $gray, 10% );
+ text-decoration: underline;
+ }
+
+ //
+ // Domain Registration Details Page
+ //
+
+ .domain-details {
+ .first-name {
+ margin-top: 0;
+ }
+
+ @include breakpoint( ">660px" ) {
+ .last-name {
+ margin-top: 0;
+ }
+
+ .hidden-input a,
+ .checkout-field {
+ float: left;
+ width: 100%;
+ }
+
+ .last-name,
+ .phone,
+ .postal-code {
+ float: right;
+ }
+
+ .email,
+ .first-name,
+ .last-name,
+ .phone {
+ width: calc( 50% - 7px );
+ }
+
+ .city,
+ .postal-code,
+ .state {
+ width: calc( 33% - 8px );
+ }
+
+ .state {
+ margin-left: 14px;
+
+ label + select {
+ min-width: inherit;
+ }
+ }
+ }
+
+ .hidden-input a {
+ display: block;
+ font-size: 12px;
+ margin-top: 5px;
+ }
+ }
+}
+
+.privacy-protection {
+ background-color: $gray-light;
+ float: left;
+ margin-bottom: 15px;
+ margin-top: 15px;
+ padding: 10px;
+
+ @include breakpoint( ">660px" ) {
+ box-sizing: border-box;
+ padding: 15px;
+ width: 100%;
+
+ section {
+ display: flex;
+ }
+ }
+
+ h6 {
+ font-size: 16px;
+ font-weight: 600;
+ }
+
+ label {
+ background-color: $white;
+ border: 3px solid $white;
+ border-radius: 3px;
+ display: block;
+ margin-top: 10px;
+ padding: 12px;
+ transition: all 0.3s ease-in-out;
+
+ &.selected {
+ border-color: #00AADC;
+ }
+
+ @include breakpoint( ">660px" ) {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ margin-top: 15px;
+ width: 50%;
+
+ &:last-child {
+ margin-left: 15px;
+ }
+ }
+ }
+
+ strong {
+ display: block;
+ font-size: 14px;
+ font-weight: normal;
+ line-height: 130%;
+ }
+
+ p {
+ color: #7096af;
+ font-size: 11px;
+ margin-bottom: 0;
+ margin-top: 5px;
+ }
+
+ input {
+ display: none;
+ }
+
+ button {
+ margin-top: 10px;
+ white-space: normal;
+ width: 100%;
+ }
+}
+
+.privacy-protection-dialog.dialog.card {
+ max-height: 95%;
+ overflow-y: auto;
+
+ .dialog__content {
+ header {
+ text-align: center;
+
+ h1 {
+ font-size: 30px;
+ font-weight: 200;
+ line-height: 130%;
+ height: auto;
+ }
+
+ p {
+ font-size: 13px;
+ }
+
+ .line-break {
+ @include breakpoint( ">660px" ) {
+ display: block;
+ }
+ }
+ }
+ }
+
+ .privacy-features {
+ border-bottom: 1px solid $gray-light;
+ border-top: 1px solid $gray-light;
+ list-style: none;
+ margin: 20px 0;
+ padding: 5px 0;
+
+ li {
+ padding: 5px 10px;
+ text-align: center;
+
+ @include breakpoint( ">660px" ) {
+ display: inline-block;
+ padding: 5px 30px;
+ }
+ }
+
+ h2 {
+ font-size: 14px;
+ font-weight: 600;
+
+ &:before {
+ @include noticon( '\f418', 28px );
+ color: $alert-green;
+ vertical-align: middle;
+ }
+ }
+ }
+
+ .privacy-comparison {
+ list-style: none;
+ margin: 0;
+ padding: 0 0 20px 0;
+
+ @include breakpoint( ">660px" ) {
+ overflow: auto;
+ padding-top: 20px;
+ }
+
+ li {
+ border: 1px solid lighten( $gray, 30% );
+ border-radius: 3px;
+ box-sizing: border-box;
+ margin: 40px 0 0 0;
+ padding: 20px;
+ position: relative;
+
+ @include breakpoint( ">660px" ) {
+ float: left;
+ margin: 0 2%;
+ width: 46%;
+ }
+
+ &:before {
+ border-radius: 50%;
+ color: #FFF;
+ display: block;
+ height: 25px;
+ position: absolute;
+ left: -8px;
+ top: -8px;
+ width: 25px;
+ }
+
+ &.with-privacy:before {
+ @include noticon( '\f418', 25px );
+ background: $alert-green;
+ }
+
+ &.without-privacy:before {
+ background: $alert-yellow;
+ content: '!';
+ font-size: 18px;
+ font-weight: 600;
+ line-height: 25px;
+ text-align: center;
+ }
+
+ h3 {
+ font-size: 16px;
+ font-weight: 600;
+ margin: 0;
+ text-align: center;
+ }
+
+ .privacy-price {
+ color: $gray;
+ font-size: 13px;
+ font-style: italic;
+ font-weight: 400;
+ text-align: center;
+ }
+
+ p {
+ background: $gray-light;
+ font-size: 12px;
+ margin: 20px -20px;
+ min-height: 126px;
+ padding: 20px;
+
+ span {
+ display: block;
+ width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ }
+
+ .button {
+ width: 100%;
+ }
+ }
+ }
+}
+
+.checkout-thank-you {
+ box-sizing: border-box;
+ position: static;
+ text-align: center;
+ width: 100%;
+
+ .thank-you-message {
+ padding-bottom: 20px;
+
+ .receipt-icon {
+ display: inline-block;
+ position: relative;
+
+ &::before {
+ @include noticon( '\f425', 70px );
+ color: lighten( $gray, 20% );
+ }
+
+ &::after {
+ @include noticon( '\f418', 18px );
+ background: #53b66c;
+ border-radius: 18px;
+ color: $white;
+ position: absolute;
+ top: 6px;
+ right: 15px;
+ width: 18px;
+ height: 18px;
+ }
+ }
+
+ > h1 {
+ color: $gray-dark;
+ font-weight: 100;
+ margin: 0;
+ }
+
+ > h2 {
+ color: $gray-dark;
+ font-weight: 200;
+ opacity: 0.6;
+ }
+ }
+
+ .try-out-message {
+ padding-top: 20px;
+
+ > h3 {
+ font-weight: 600;
+ }
+ }
+
+ .purchase-details-list {
+ list-style: none;
+ }
+
+ .purchase-detail {
+ display: inline-block;
+ width: 33.333333%;
+
+ .purchase-detail-text {
+ &::before {
+ color: lighten( $gray, 20% );
+ @include noticon( '\f803' );
+ top: -8px;
+ }
+
+ > h3 {
+ font-weight: 600;
+ }
+
+ > p {
+ font-size: 12px;
+ opacity: 0.7;
+ }
+ }
+
+ &.get-free-domain {
+ .purchase-detail-text::before {
+ @include noticon( '\f475' );
+ }
+ }
+
+ &.customize-fonts-and-colors {
+ .purchase-detail-text::before {
+ @include noticon( '\f103' );
+ }
+ }
+
+ &.upload-to-videopress {
+ .purchase-detail-text::before {
+ @include noticon( '\f104' );
+ }
+ }
+
+ &.ads-have-been-removed {
+ .purchase-detail-text::before {
+ @include noticon( '\f803' );
+ top: -7px;
+ }
+ }
+
+ &.ecommerce {
+ .purchase-detail-text::before {
+ @include noticon( '\f447' );
+ }
+ }
+
+ &.live-chat {
+ .purchase-detail-text::before {
+ @include noticon( '\f108' );
+ }
+ }
+
+ &.unlimited-premium-themes {
+ .purchase-detail-text::before {
+ @include noticon( '\f103' );
+ }
+ }
+
+ &.important {
+ .purchase-detail-text::before {
+ @include noticon( '\f303' );
+ }
+ }
+
+ &.your-primary-domain {
+ .purchase-detail-text::before {
+ @include noticon( '\f475' );
+ }
+ }
+
+ &.upgrade-now {
+ .purchase-detail-text::before {
+ @include noticon( '\f800' );
+ }
+ }
+
+ &.google-apps-details {
+ .purchase-detail-text::before {
+ @include noticon( '\f410' );
+ }
+ }
+
+ &.redirect-now-working {
+ .purchase-detail-text::before {
+ @include noticon( '\f442' );
+ }
+ }
+
+ &.change-redirect-settings {
+ .purchase-detail-text::before {
+ @include noticon( '\f411' );
+ }
+ }
+ }
+
+ .get-support {
+ border-top: 1px solid lighten( $gray, 20% );
+ color: darken($gray, 10%);
+ font-size: 14px;
+
+ > h3 {
+ font-weight: 600;
+ }
+
+ > p {
+ margin-bottom: 0;
+ }
+
+ a {
+ border-bottom: 1px solid $blue-medium;
+ }
+ }
+}
+
+// Mobile styles
+@include breakpoint( "<660px" ) {
+ .checkout-thank-you {
+ background: transparent;
+ padding: 14px;
+
+ .card {
+ background: none;
+ box-shadow: none;
+ padding: 0;
+ }
+
+ .thank-you-message {
+ > h1,
+ > h2 {
+ font-size: 17px;
+ }
+ }
+
+ .purchase-details-list {
+ display: inline-block;
+ margin: 0 0 10px 0;
+ }
+
+ .purchase-detail {
+ background: $white;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
+ box-sizing: border-box;
+ display: inline-block;
+ margin-bottom: 20px;
+ padding: 20px 10px;
+ text-align: left;
+ width: 100%;
+
+ .purchase-detail-text {
+ padding-left: 64px;
+ position: relative;
+
+ &::before {
+ font-size: 50px;
+ position: absolute;
+ left: 0;
+ }
+ }
+
+ > button {
+ height: 42px;
+ line-height: 42px;
+ padding: 0;
+ vertical-align: top;
+ width: 100%;
+ }
+ }
+
+ .get-support {
+ border-bottom: 1px solid lighten( $gray, 20% );
+ padding: 20px 0;
+ }
+ }
+}
+
+// Desktop styles
+@include breakpoint( ">660px" ) {
+ .checkout-thank-you {
+ .thank-you-message {
+ border-bottom: 1px solid lighten( $gray, 30% );
+
+ > h1 {
+ font-size: 25px;
+ }
+ > h2 {
+ font-size: 20px;
+ max-width: 50%;
+ margin: auto;
+ }
+ }
+
+ .purchase-details-list {
+ margin: 0;
+ padding: 30px 0;
+ }
+
+ .purchase-detail {
+ vertical-align: top;
+ }
+
+ .purchase-detail-text {
+ &::before {
+ font-size: 70px;
+ }
+
+ > p {
+ margin: 0 50px 10px 50px;
+ }
+ }
+
+ .get-support {
+ padding: 20px;
+
+ > h3 {
+ display: inline;
+ margin-right: 15px;
+ }
+
+ > p {
+ display: inline;
+ }
+ }
+ }
+}
+
+// If there's no sidebar, we don't show the cart on the checkout page.
+@include breakpoint( "<660px" ) {
+ .secondary-cart {
+ display: none;
+ }
+}
+
+@include breakpoint( ">660px" ) {
+ .pay-button {
+ float: left;
+ }
+}
+
+.credit-card-payment-box__switch-link {
+ color: $link-highlight;
+ font-style: italic;
+ font-weight: 800;
+ line-height: 40px;
+ display: block;
+ clear: both;
+ font-size: 12px;
+
+ @include breakpoint( "<660px" ) {
+ margin: 20px 0 0 0;
+ text-align: center;
+ }
+
+ @include breakpoint( ">960px" ) {
+ float: right;
+ clear: none;
+ }
+}
diff --git a/assets/stylesheets/sections/_devdocs.scss b/assets/stylesheets/sections/_devdocs.scss
new file mode 100644
index 00000000000000..b2627e9b5e60d9
--- /dev/null
+++ b/assets/stylesheets/sections/_devdocs.scss
@@ -0,0 +1,217 @@
+.devdocs,
+.design-assets {
+ font-size: 18px;
+ line-height: 1.618;
+ color: $gray-dark;
+ margin: 0 auto;
+ max-width: 960px;
+ padding: 1.777em 8.184em;
+
+ @include breakpoint( "<960px" ) {
+ padding: 24px;
+ max-width: 100%;
+ }
+}
+
+.is-section-devdocs .wp-content {
+ display: flex;
+ max-width: 100%;
+ margin-top: 47px;
+ margin-left: 0;
+
+ .wp-primary {
+ order: 2;
+ flex: 1;
+ }
+
+ .wp-secondary {
+ background: $white;
+ flex: 0 300px;
+ box-shadow: inset -0.296em 0 0 rgba(0,80,130,0.2);
+
+ @include breakpoint( "<960px" ) {
+ flex: 0 200px;
+ }
+ }
+}
+
+.devdocs__sidebar {
+ margin-top: 48px;
+}
+
+.devdocs__title {
+ color: darken( $gray, 20% );
+ font-weight: 300;
+ font-size: 24px;
+ padding: 24px;
+}
+
+.devdocs__navigation {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+}
+
+.devdocs__navigation-item a {
+ display: block;
+ padding: 16px 24px;
+ box-shadow: inset -0.296em 0 0 rgba(0,80,130,0.2);
+ color: $gray;
+ font-size: 16px;
+
+ &:hover {
+ background: $blue-medium;
+ color: $white;
+ }
+}
+
+.devdocs__result {
+ @extend %container;
+
+ margin: 0.5em 0;
+ padding: 10px 0;
+
+ header {
+ overflow: hidden;
+ padding: 0 18px;
+ h1 {
+ clear: none;
+ float: left;
+ }
+ h1 a {
+ &:hover {
+ color: $blue-medium;
+ }
+ font-size: 18px;
+ color: $gray-dark;
+ }
+ h2 {
+ font-size: ( 12 / 15 ) * 1em;
+ clear: none;
+ color: $gray;
+ float: right;
+ margin: 8px 0 0;
+ }
+ }
+}
+
+.devdocs__result-snippet {
+ p {
+ margin-bottom: 0;
+ }
+ margin: 0;
+ margin-top: 10px;
+ border-top: 1px solid lighten( $gray, 20% );
+ padding: 9px 18px;
+ font-size: ( 13 / 15 ) * 0.8em;
+ background-color: $white;
+}
+
+.devdocs__doc {
+ h1, h2, h3, h4, h5, h6 {
+ font-family: Merriweather, Georgia, "Times New Roman", Times, serif;
+ font-weight: 700;
+ line-height: 1.5em;
+ margin-bottom: 0.6em;
+ }
+
+ h1 {
+ color: $blue-medium;
+ font-size: 3.157em;
+ line-height: 1.333;
+ }
+
+ h2 {
+ font-size: 1.777em;
+ }
+
+ pre {
+ padding: (12 / 15) * 1em;
+ background: $white;
+ background-color: $white;
+ }
+
+ code {
+ font-size: (13 / 15) * 1em;
+ background-color: $white;
+ padding: 0.2em;
+ border-radius: 3px;
+ }
+
+ pre > code {
+ background-color: $transparent;
+ }
+
+ header {
+ @extend %container;
+
+ padding : 1em;
+
+ h2 {
+ font-size : ( 12 / 15 ) * 1em;
+ color : $gray-dark;
+ margin-bottom: 0;
+
+ a {
+ float: right;
+ }
+ }
+ }
+
+ .emoji {
+ height: 18px;
+ }
+
+ .label {
+ $invert: $white;
+ font-weight: bold;
+ font-size: 12px;
+ padding: 1px 5px 2px 5px;
+ border-radius: 2px;
+
+ &.status-awaiting-fixes {
+ background: #ea652d;
+ color: $invert;
+ }
+
+ &.status-needs-review {
+ background: #fbc92f;
+ }
+
+ &.status-ready-to-merge {
+ background: #d6fa82;
+ }
+
+ &.status-in-progress {
+ background: #2880e2;
+ color: $invert;
+ }
+ }
+}
+
+.design-assets__toggle {
+ float: right;
+}
+
+.design-assets__group {
+ color: $gray-dark;
+}
+
+.design-assets__group > .gridicon:hover {
+ fill: $gray;
+ cursor: pointer;
+}
+
+// ==========================================================================
+// Button component styles
+// ==========================================================================
+
+.design-assets__button-row {
+ &:first-child {
+ margin-top: -16px;
+ }
+ .button {
+ margin-top: 16px !important;
+ margin-right: 16px !important;
+ }
+}
diff --git a/assets/stylesheets/sections/_domain-search.scss b/assets/stylesheets/sections/_domain-search.scss
new file mode 100644
index 00000000000000..422d466a1deebf
--- /dev/null
+++ b/assets/stylesheets/sections/_domain-search.scss
@@ -0,0 +1,290 @@
+// domain search
+
+.domain-search__content {
+ overflow: visible;
+ padding: 0 0 20px 0;
+ position: static;
+}
+
+.domain-search-page-wrapper h2 {
+ @extend %heading;
+ margin: 0 0 10px 0;
+}
+
+.domain-search-page-wrapper h3 {
+ color: $gray-dark;
+ font-size: 15px;
+ word-wrap: break-word;
+
+ @include breakpoint( ">660px" ) {
+ font-size: 17px;
+ }
+}
+
+// site redirect step
+
+.site-redirect-step {
+ padding: 0;
+
+ fieldset {
+ clear: left;
+ }
+
+ form.map-domain-step__form {
+ padding: 20px;
+ margin-bottom: 9px;
+ }
+
+ p {
+ color: $gray-dark;
+ font-size: 13px;
+ font-weight: 600;
+ margin-bottom: 0;
+ opacity: 0.7;
+ }
+
+ .domain-product-price {
+ @include breakpoint( ">660px" ) {
+ float: right;
+ margin-top: -5px;
+ }
+ }
+}
+
+.site-redirect-step__domain-description {
+ @include breakpoint( ">660px" ) {
+ float: left;
+ margin-bottom: 20px;
+ }
+}
+
+input.site-redirect-step__external-domain {
+ @include breakpoint( ">660px" ) {
+ float: left;
+ width: calc( 100% - 90px );
+ }
+}
+
+.site-redirect-step__go {
+ margin: 10px 0 0 0;
+ width: 100%;
+
+ @include breakpoint( ">660px" ) {
+ float: right;
+ margin: 0;
+ width: 80px;
+ }
+}
+
+//
+// Google Apps
+//
+
+// The `form` selector adds specificity to override the padding from the
+// `.card` class.
+form.google-apps-dialog {
+ padding: 0;
+
+ .google-apps-dialog__product-details {
+ background: $gray-light;
+ padding: 24px 0;
+ text-align: center;
+ }
+
+ .google-apps-dialog__product-name {
+ color: #888;
+ font-size: 25px;
+ font-weight: 100;
+ margin: 0;
+ }
+
+ .google-apps-dialog__product-logo {
+ background: url('/calypso/images/upgrades/google-apps-logo.png');
+ background-repeat: no-repeat;
+ background-size: contain;
+ display: inline-block;
+ height: 34px;
+ margin: 2px 7px 0 0;
+ text-indent: -999999px;
+ vertical-align: text-top;
+ width: 102px;
+ }
+
+ .google-apps-dialog__header {
+ padding: 18px;
+ text-align: center;
+
+ .google-apps-dialog__title {
+ color: $gray-dark;
+ font-weight: 600;
+ margin: 0;
+ }
+ }
+
+ .google-apps-dialog__no-setup-required {
+ color: lighten( $gray-dark, 20% );
+ }
+
+ .google-apps-dialog__file-storage,
+ .google-apps-dialog__professional-email {
+ color: darken( $gray, 10% );
+ font-size: 13px;
+ font-weight: 600;
+ line-height: 130%;
+ }
+
+ .google-apps-dialog__professional-email {
+ border-bottom: 1px solid darken( $gray-light, 10% );
+ display: inline-block;
+ margin-bottom: 8px;
+ padding-bottom: 12px;
+ }
+
+ .google-apps-dialog__price-per-user {
+ color: $blue-medium;
+ font-size: 18px;
+ font-weight: 600;
+ }
+
+ .google-apps-dialog__billing-period {
+ color: lighten( $gray, 10% );
+ font-size: 12px;
+ text-transform: uppercase;
+ }
+
+ .notice li {
+ list-style: disc;
+ }
+}
+
+.google-apps-dialog__users-enter {
+ max-height: 0;
+ overflow: hidden;
+ transition: max-height 0.2s ease-in-out;
+}
+
+
+.google-apps-dialog__users-enter.google-apps-dialog__users-enter-active {
+ max-height: 300px;
+}
+
+.google-apps-dialog__users {
+ border-bottom: 1px solid darken( $gray-light, 5% );
+ display: block;
+ padding: 0 30px;
+
+ h4 {
+ color: #7799ae;
+ margin-top: 30px;
+ margin-bottom: 4px;
+ }
+
+ .google-apps-dialog__user-fields {
+ animation: google-apps-user-show 0.3s ease-in-out;
+ margin-bottom: 20px;
+ }
+
+ .google-apps-dialog__user-email {
+ margin-bottom: 9px;
+ }
+
+ .google-apps-dialog__user-first-name {
+ margin-bottom: 9px;
+
+ @include breakpoint( ">660px" ) {
+ display: inline-block;
+ margin: 0 4px 0 0;
+ width: calc( 50% - 4px );
+ }
+ }
+
+ .google-apps-dialog__user-last-name {
+ @include breakpoint( ">660px" ) {
+ display: inline-block;
+ margin: 0 0 0 5px;
+ width: calc( 50% - 5px );
+ }
+ }
+
+ .google-apps-dialog__add-another-user-button {
+ border: 2px dashed lighten( $gray, 20% );
+ color: $gray;
+ cursor: pointer;
+ margin: 0 0 30px;
+ padding: 12px 18px 12px 45px;
+ position: relative;
+ text-align: left;
+ width: 100%;
+
+ &:before {
+ position: absolute;
+ left: 15px;
+ top: 6px;
+ @include noticon( '\f8b3', 60px );
+ }
+ }
+}
+
+@keyframes "google-apps-user-show" {
+ 0% {
+ max-height: 0px;
+ }
+
+ 100% {
+ max-height: 150px;
+ }
+}
+
+.google-apps-dialog__footer {
+ padding: 30px;
+ @include clear-fix;
+
+ @include breakpoint( ">660px" ) {
+ padding: 18px;
+ }
+
+ .google-apps-dialog__cancel-link {
+ color: $blue-medium;
+ display: block;
+ font-size: 13px;
+ text-align: center;
+
+ @include breakpoint( ">660px" ) {
+ float: left;
+ line-height: 38px;
+ }
+ }
+
+ .google-apps-dialog__continue-button {
+ @include breakpoint( "<660px" ) {
+ margin-bottom: 18px;
+ width: 100%;
+ }
+
+ @include breakpoint( ">660px" ) {
+ float: right;
+ }
+ }
+
+ .button {
+ width: 100%;
+ margin-top: 10px;
+
+ @include breakpoint( ">660px" ) {
+ width: auto;
+ margin: 0 auto auto 10px;
+ }
+
+ &:first-of-type {
+ margin-left: 0;
+ margin-top: 0;
+ &.is-primary {
+ margin-top: 10px;
+
+ @include breakpoint( ">660px" ) {
+ margin-top: 0;
+ }
+ }
+ }
+ }
+}
diff --git a/assets/stylesheets/sections/_keyboard-shortcuts.scss b/assets/stylesheets/sections/_keyboard-shortcuts.scss
new file mode 100644
index 00000000000000..740671f2d27b5c
--- /dev/null
+++ b/assets/stylesheets/sections/_keyboard-shortcuts.scss
@@ -0,0 +1,71 @@
+/**
+ * The keyboard shortcuts menu
+ */
+
+.dialog.keyboard-shortcuts .keyboard-shortcuts__title {
+ margin: 0;
+ text-align: center;
+ line-height: 1em;
+}
+
+.keyboard-shortcuts__categories {
+ list-style: none;
+ margin: 0;
+ max-width: 620px;
+ color: $gray-dark;
+}
+
+.keyboard-shortcuts__category {
+ display: inline-block;
+ width: 50%;
+ margin-bottom: 15px;
+ float: left;
+}
+
+.keyboard-shortcuts__category-disabled {
+ color: $gray;
+}
+
+.keyboard-shortcuts__site-navigation {
+ float: right;
+}
+
+.keyboard-shortcuts__category h3 {
+ font-weight: bold;
+ margin-bottom: 10px;
+ margin-left: 88px;
+}
+
+.keyboard-shortcuts__list {
+ list-style: none;
+ margin: 0;
+ font-size: 12px;
+}
+
+.keyboard-shortcuts__list li {
+ clear: left;
+ margin-bottom: 8px;
+}
+
+.keyboard-shortcuts__keys {
+ width: 80px;
+ float: left;
+ text-align: right;
+}
+
+.keyboard-shortcuts__key {
+ background-color: $white;
+ border: solid 1px $gray;
+ padding: 0 5px;
+ min-width: 8px;
+ text-align: center;
+ border-radius: 5px;
+ box-shadow: 0 1px 1px $gray;
+ display: inline-block;
+ margin-right: 3px;
+}
+
+.keyboard-shortcuts__description {
+ margin-left: 8px;
+ display: inline-block;
+}
diff --git a/assets/stylesheets/sections/_manage.scss b/assets/stylesheets/sections/_manage.scss
new file mode 100644
index 00000000000000..35d9cf8b0a8d68
--- /dev/null
+++ b/assets/stylesheets/sections/_manage.scss
@@ -0,0 +1,28 @@
+/* Status indicators */
+.corner-status {
+ width: 50px;
+ height: 50px;
+ transform: rotate(45deg);
+
+ position: absolute;
+ top: -25px;
+ right: -25px;
+}
+.corner-status.connected { background: #47b603; }
+.corner-status.disconnected, .corner-status.unapproved { background: #ffbb00; }
+
+.noticon.status-checkmark, .noticon.status-warning {
+ position: absolute;
+ top: 0;
+ right: 0;
+ color: #fff;
+ font-size: 22px;
+ width: auto;
+ height: auto;
+}
+
+.noticon.status-warning {
+ font-size: 13px;
+ top: 4px;
+ right: 3px;
+}
diff --git a/assets/stylesheets/sections/_menus.scss b/assets/stylesheets/sections/_menus.scss
new file mode 100644
index 00000000000000..aaa110cfde5d0b
--- /dev/null
+++ b/assets/stylesheets/sections/_menus.scss
@@ -0,0 +1,1034 @@
+/**
+ * Menus: Mixins
+ */
+@mixin menus-depth-levels( $max-depth, $extra-selector:"" ) {
+ @for $i from 1 through $max-depth {
+ &.depth-#{ $i } #{ $extra-selector } {
+ margin-left: #{ $i * 2 }rem;
+ }
+ }
+}
+@mixin menus-depth-levels-undo( $max-depth, $extra-selector:"" ) {
+ @for $i from 1 through $max-depth {
+ &.depth-#{ $i } #{ $extra-selector } {
+ margin-left: #{ $i * -2 }rem;
+ }
+ }
+}
+
+
+/**
+ * Menus: Empty Content
+ */
+.manage-menus .empty-content {
+ clear: both;
+}
+
+/**
+ * Menus: Placeholders
+ */
+.menus__pickers,
+.menus__menu-header,
+.menus__items {
+ .placeholder-text {
+ color: transparent;
+ background-color: lighten( $gray, 30% );
+ animation: loading-fade 1.6s ease-in-out infinite;
+ }
+}
+
+.menus__picker label .placeholder-text {
+ font-size: 10px;
+}
+
+.menus__picker-select-placeholder {
+ display: block;
+ padding: 23px 40px 10px 52px;
+
+ .placeholder-text {
+ font-size: 12px;
+ }
+}
+
+
+
+
+
+/**
+ * Menus: Pickers
+ */
+.menus__pickers {
+ background-color: lighten( $gray, 30% );
+ background-size: cover;
+ width: 100%;
+ box-sizing: border-box;
+ overflow: hidden;
+
+ padding: 3%;
+ box-shadow: 0 -2px 0 lighten( $gray, 10% ) inset;
+}
+
+.menus__pickers-conjunction {
+ float: left;
+ height: 55px;
+ font-size: 0.8em;
+ line-height: 4.6;
+
+ width: 10%;
+ padding: 0;
+ margin: 0;
+ text-align: center;
+
+ @include breakpoint( "<480px" ) {
+ width: 100%;
+ height: 30px;
+ line-height: 2.3;
+ }
+}
+
+.menus__picker {
+ width: 45%;
+ float: left;
+ position: relative;
+ background: $white; /* required for -moz-appearance below */
+
+ @include breakpoint( "<480px" ) {
+ width: 100%;
+ }
+
+ label,
+ select {
+ display: block;
+ width: 100%;
+ cursor: pointer;
+ }
+
+ label {
+ pointer-events: none; /* click through :D */
+ font-size: 0.8em;
+ color: $gray;
+ position: absolute;
+ top: 10px;
+ left: 10px;
+ padding: 0px 30px 20px 42px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ width: calc(100% - 20px);
+
+
+ &:before {
+ position: absolute;
+ left: 1px;
+ top: 1px;
+ color: $blue-dark;
+ }
+
+ &:after {
+ @include noticon( '\f431', 22px );
+ position: absolute;
+ top: 7px;
+ right: 0;
+ color: $blue-medium;
+ }
+ }
+
+ &.is-location label:before {
+ @include noticon( '\f8a9', 32px );
+ }
+
+ &.is-menu label:before {
+ @include noticon( '\f505', 32px );
+ }
+
+ select {
+ background: $white;
+ height: 55px;
+ border: none;
+ border-radius: 0;
+
+ -webkit-appearance: none;
+ padding: 20px 40px 5px 52px;
+
+ &::-ms-expand {
+ display: none; /* Remove arrow in IE */
+ }
+ }
+}
+
+
+/**
+ * Menu: Header
+ */
+.menus__menu-header {
+ display: flex;
+ flex-direction: row;
+ margin: 20px 0;
+
+ .menus__menu-name {
+ flex: 1 1 auto;
+ width: 0%; /* Firefox 35 and IE 10 fix */
+ }
+
+ .menus__menu-actions {
+ flex: 0 0 auto;
+ margin-left: auto;
+ }
+}
+
+.menus__menu-name {
+ font-size: 1.6em;
+ font-weight: 200;
+ float: left;
+
+ // The rendered field
+ span.is-editable {
+
+ span {
+ display: block;
+ float: left;
+ max-width: 90%; /* should be less to avoid a bug on touch + tag that auto-closes the area, but ellipsis wouldn't work */
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ }
+
+ a {
+ margin-left: 4px;
+ cursor: pointer;
+ &:before {
+ @include noticon( '\f411', 16px );
+ color: darken( $gray, 10% );
+ vertical-align: baseline;
+ }
+ }
+ }
+
+ // The same field, edited
+ input.is-editable {
+ margin: -20px -30px -20px 0;
+ font-size: inherit;
+ font-weight: inherit;
+ width: calc(100% - 40px);
+ margin-right: 40px;
+ }
+}
+
+.menus__menu-actions {
+ float: right;
+
+ .button {
+ margin-left: 0.7em;
+
+ &.noticon:before {
+ line-height: 1.35;
+ color: $gray-dark;
+
+ /* Reset noticon parts overridden by button */
+ font-size: 16px;
+ font-weight: 400;
+ }
+ }
+}
+
+
+/**
+ * Menu: List
+ */
+.menus__items {
+ margin: 0;
+ clear: both;
+ list-style-type: none;
+ background-color: lighten( $gray, 30% );
+
+ ul {
+ padding: 0;
+
+ &.depth-0 {
+ margin-left: 0;
+ }
+ }
+}
+
+.menus__menu-item {
+ @include menus-depth-levels( 7 );
+
+ display: flex;
+ flex-direction: row;
+ color: $blue-dark;
+ background-color: $white;
+ text-decoration: none;
+ border-bottom: 1px solid lighten( $gray, 30% );
+ border-left: 1px solid lighten( $gray, 30% );
+ padding: 17px 20px 16px 16px;
+ font-size: 13px;
+ cursor: default;
+ position: relative;
+ z-index: 2;
+
+ .menu-item-name {
+ flex: 1 1 auto;
+ color: $gray-dark;
+ font-size: 1.1em;
+ line-height: 1.5em;
+ font-family: inherit;
+ text-align: left;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+
+ &:before {
+ font-size: 16px;
+ margin-right: 12px;
+ color: $blue-medium;
+ vertical-align: -2px;
+ }
+
+ &.noticon-placeholder:before {
+ color: transparent;
+ background-color: lighten( $gray, 30% );
+ animation: loading-fade 1.6s ease-in-out infinite;
+
+ vertical-align: -4px;
+ font-size: 19px;
+ }
+ }
+
+ .action-tray {
+ flex: 0 0 auto;
+ float: right;
+ margin-right: -8px; /* compensate buttons padding, more universal */
+ transition: background 200ms ease-in, color 50ms ease-in;
+
+ /* Buttons */
+ button {
+ margin-left: 1em;
+ margin-top: -8px;
+ margin-bottom: -8px;
+ font-size: inherit;
+ line-height: 1.45;
+ }
+
+ .menu-item-action {
+ float: left;
+ color: darken( $gray, 10% );
+ margin: -5px 0 -5px;
+ padding: 8px 6px 8px;
+ text-transform: uppercase;
+ font-size: 0.85em;
+ font-weight: 500;
+ vertical-align: top;
+ cursor: pointer;
+
+ transition: all 200ms ease-out;
+ border-radius: 100%;
+
+ text-indent: -6666em;
+ width: 22px; /* Fix: Firefox won't respect margins with text-indent on otherwise. */
+
+ /* Icon Buttons */
+ &:before {
+ content: "";
+ color: $blue-medium;
+ float: left;
+ font-size: 16px;
+ text-indent: 0;
+ width: 22px;
+ }
+
+ &.edit:before {
+ @include noticon( '\f411', 16px );
+ }
+
+ &.add:before {
+ @include noticon( '\f510', 16px );
+ }
+
+ &.cancel:before {
+ @include noticon( '\f405', 16px );
+ font-size: 19px;
+ margin-bottom: -3px; /* Sigh it's smaller... */
+ }
+
+ &.move {
+ color: $blue-medium;
+ text-indent: 0;
+ width: auto;
+
+ &:hover {
+ color: $blue-light;
+ border-radius: 0;
+ background: transparent;
+ }
+ &:before {
+ content: none;
+ }
+ }
+
+ &:hover,
+ &:focus {
+ color: $white;
+ background: $blue-medium;
+ }
+
+ &:hover:before,
+ &:focus:before {
+ color: $white;
+ }
+ }
+ }
+
+ /**
+ * Menu item is selected
+ */
+ &.is-selected {
+ background: $blue-medium;
+ padding: 6px;
+ border-bottom: 0; /* remove gap */
+
+ .noticon {
+ color: $white;
+ padding: 14px 16px 5px 10px;
+ }
+
+ input {
+ width: calc(100% - 70px);
+ padding: 12px 14px;
+ border: 0;
+ color: $blue-dark;
+ font-size: 14px;
+ }
+ }
+
+ /**
+ * Menu 3-pronged lander areas
+ */
+ &.is-lander {
+ background: lighten( $gray-light, 2% );
+ border-left: 1px solid lighten( $gray, 30% );
+ z-index: 1;
+
+ &:hover {
+ background: $blue-medium;
+ }
+ &:hover span,
+ &:hover span:before {
+ color: $white;
+ }
+
+ span {
+ color: $blue-dark;
+ font-size: 1.1em;
+ line-height: 1.5em;
+ font-family: inherit;
+
+ &:before {
+ font-size: 16px;
+ margin-right: 12px;
+ color: $blue-medium;
+ vertical-align: -2px;
+ }
+ }
+ }
+
+ /**
+ * Item target when dragging with mouse
+ */
+ &.is-dragdrop-target {
+ background: $gray-light;
+ border: 1px dashed lighten( $gray, 20 );
+ margin-top: 10px;
+ margin-bottom: 10px;
+
+ span {
+ visibility: hidden;
+ }
+
+ .add,
+ .edit {
+ display: none;
+ }
+ }
+
+ /**
+ * Hide drag 'ghost' image, because it makes the drop
+ * target hard to see
+ */
+ &:-webkit-drag {
+ visibility: hidden;
+
+ div {
+ display: none;
+ }
+ }
+
+ /**
+ * Menu is Empty, show special add icon
+ */
+ &.is-empty {
+ margin-left: calc(100% - 61px);
+ padding-left: 13px;
+ }
+
+ /**
+ * Item is to be deleted, pending user confirmation
+ */
+ &.is-deleted {
+ background: $gray-light;
+
+ .menu-item-name,
+ .menu-item-name::before {
+ color: $gray;
+ }
+ }
+
+ &.is-corrupt {
+ border-left: 5px solid $alert-yellow;
+ }
+}
+
+
+/**
+ * Menu: add menu item label
+ */
+.menus__add-item-footer-label {
+ float: right;
+ padding: 10px 25px 2px 0;
+
+ color: $gray;
+ font-size: 10px;
+ text-transform: uppercase;
+
+ animation: menus__fade-from-bottom 1.0s ease-in-out;
+
+ &:after {
+ content: "\2191"; /* Up arrow */
+ color: $blue-medium;
+ font-size: 16px;
+ padding: 0 0 0 10px;
+ }
+}
+
+
+/**
+ * Menu: Edit Item & New Item views
+ */
+.menus__menu-item-open-container {
+ background: $gray-light;
+
+ @include breakpoint( "<480px" ) {
+ &.is-panel-left {
+ .menu-item-options {
+ display: none !important;
+ }
+ }
+
+ &.is-panel-right {
+ .menu-item-options {
+ width: auto !important;
+ left: 0 !important;
+ border: 0 !important;
+ }
+ }
+ }
+
+ // If done properly, might be an alternative for mobile.
+ // Seems not working properly with -webkit-perspective turned on.
+ /*@include responsive( mobile ) {
+ z-index: 190; // Higher than masterbar?
+ border: 0;
+ padding: 80px 0 0;
+ background: rgba(244, 248, 250, 0.9);
+
+ position: fixed;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ }*/
+}
+
+.menus__menu-item-open {
+ @include menus-depth-levels( 7 );
+ @include menus-depth-levels-undo( 7, ".editable-item-content" );
+
+ display: block;
+ color: $blue-dark;
+ background-color: $white;
+ text-decoration: none;
+ border-left: 1px solid lighten( $gray, 30% );
+
+ &:before {
+ /* The top arrow */
+ margin-top: -17px;
+ margin-left: 15px;
+ border: solid transparent;
+ content: " ";
+ height: 0;
+ width: 0;
+ position: absolute;
+ pointer-events: none;
+ border-color: rgba(255, 255, 255, 0);
+ border-bottom-color: #ffffff;
+ border-width: 9px;
+ z-index: 2;
+ }
+
+ .editable-item-content {
+ $editable-item-options-width: 75%;
+
+ background: $white;
+ border-top: 1px solid lighten( $gray, 30% );
+ border-bottom: 1px solid lighten( $gray, 30% );
+
+ /**
+ * General
+ */
+ .separated {
+ border-bottom: 1px solid lighten( $gray, 30% );
+ }
+
+ .separated:after {
+ content: " ";
+ display: block;
+ height: 0;
+ clear: both;
+ }
+
+ input:not([type='radio']):not([type='checkbox']) {
+ display: block;
+ font-size: 14px;
+ margin: -1px 0;
+ transition: all 200ms ease-out;
+ }
+
+ /**
+ * Menu Item Name
+ */
+ .menus__menu-item-form-name {
+ display: flex;
+ flex-direction: row;
+
+ label {
+ flex: 0 0 auto;
+ padding: 9px 12px;
+
+ display: block;
+ line-height: 2.4;
+ color: $gray;
+ font-size: 10px;
+ text-transform: uppercase;
+ }
+
+ input {
+ flex: 1 1 auto;
+ width: $editable-item-options-width;
+ background: $gray-light;
+ }
+ }
+
+ /**
+ * Menu Item Types
+ */
+ .menus__menu-item-form-types {
+ list-style: none;
+ margin: 0;
+ position: relative;
+ width: 136px;
+
+ &:hover {
+ border-top-color: $gray-light;
+ border-bottom-color: $gray-light;
+ }
+
+ @include breakpoint( "<480px" ) {
+ width: 100%;
+ }
+
+ li {
+ border: 0;
+ border-top: 1px solid transparent;
+ border-bottom: 1px solid transparent;
+ border-right: 1px solid lighten( $gray, 30% );
+ transition: all 200ms ease-in-out, color 150ms ease-in-out;
+
+ & > label {
+ font-family: inherit;
+ font-size: 14px;
+ padding: 16px 12px;
+ color: $gray-dark;
+ line-height: 1.3;
+ display: block;
+ text-align: left;
+ margin-bottom: -1px;
+ font-weight: 400;
+ transition: all 250ms ease-in-out, color 150ms ease-in-out;
+ cursor: pointer;
+ -webkit-font-smoothing: inherit; /* Fix the noticon change */
+ -moz-osx-font-smoothing: inherit; /* Fix the noticon change */
+
+ &:before {
+ color: lighten( $gray, 20% );
+ font-size: 16px;
+ margin-right: 8px;
+ vertical-align: -3px;
+ transition: all 250ms ease-in-out, color 150ms ease-in-out;
+ }
+
+ &:hover {
+ color: $blue-medium;
+
+ &:before {
+ color: $blue-medium;
+ }
+ }
+ }
+
+ /**
+ * Selected states
+ */
+ &.is-selected {
+ border-top-color: lighten( $gray, 30% );
+ border-bottom-color: lighten( $gray, 30% );
+ border-right-color: $white;
+ color: $gray-dark;
+
+ @include breakpoint( "<480px" ) {
+ border-top: 1px solid transparent;
+ border-bottom: 1px solid transparent;
+ }
+
+ &:first-child {
+ border-top-color: $white;
+ }
+
+ &:last-child {
+ border-bottom-color: $white;
+ }
+
+ & > label {
+ color: $blue-medium;
+
+ &:before {
+ color: $blue-medium;
+ }
+ }
+ }
+ }
+ }
+
+ .menus__types-and-options-container {
+ position: relative;
+
+ .menu-item-options {
+ position: absolute;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ width: $editable-item-options-width;
+
+ background: $white;
+ font-size: 14px;
+ padding: 10px 13px;
+
+ .menu-item-tag-container {
+ margin: 10px;
+
+ .menu-item-tag {
+ display: inline-block;
+ padding: 0 3px;
+ background-color: lighten( $gray, 20% );
+ color: $white;
+ font-size: 10px;
+ border-radius: 3px;
+ margin-left: 5px;
+ text-transform: uppercase;
+ vertical-align: middle;
+ letter-spacing: 0.02em;
+
+ &:first-of-type {
+ margin-left: 0;
+ }
+ }
+ }
+
+
+ .menu-item-back-button {
+ display: none;
+ transition: 200ms all ease-in;
+
+ a {
+ display: block;
+ font-family: inherit;
+ font-size: 14px;
+ color: lighten( $gray-dark, 5% );
+ text-align: left;
+ line-height: 1.3;
+ background: $white;
+ padding: 16px 12px 15px;
+ margin: -10px -13px 10px;
+ border-bottom: 1px solid lighten( $gray, 30% );
+ -webkit-font-smoothing: inherit;
+
+ &:before {
+ color: $blue-light;
+ font-size: 16px;
+ margin-right: 8px;
+ vertical-align: -3px;
+ }
+ }
+
+ @include breakpoint( "<480px" ) {
+ display: block;
+ }
+ }
+
+ .menu-item-form-label {
+ float: none;
+ width: auto;
+ border: 0;
+
+ display: block;
+ padding: 0 0 10px 0;
+ color: $gray;
+ font-size: 11px;
+ font-weight: 600;
+ text-transform: uppercase;
+ }
+
+ .menu-item-form-address {
+ margin-bottom: 1rem;
+ }
+
+ input[type='checkbox'] + label {
+ margin-left: 4px;
+ color: $gray-dark;
+ }
+
+ li {
+ padding: 2px 0;
+
+ input[type=radio] + label {
+ margin-left: 4px;
+ transition: all 200ms ease-out;
+ color: $gray-dark;
+
+ &:hover {
+ color: $blue-medium;
+ }
+ }
+ }
+
+ form > label {
+ cursor: pointer;
+ }
+
+ input:not([type='radio']):not([type='checkbox']) {
+ width: 95%;
+ }
+
+ .is-empty-content {
+ color: $gray;
+
+ a {
+ color: $gray;
+ text-decoration: underline;
+ }
+ }
+
+ .search-container {
+ position: relative;
+
+ .noticon-search {
+ position: absolute;
+ left: 0;
+ padding: 9px 9px;
+ }
+
+ .search-box {
+ right: 0;
+ width: 100%;
+ height: 35px;
+ margin-bottom: 1rem;
+ padding: 5px 5px 5px 30px;
+ background: $white;
+
+ -webkit-appearance: none;
+ }
+ }
+
+ // Scroll
+ overflow: hidden;
+ overflow-y: auto;
+
+ &::-webkit-scrollbar {
+ width: 9px;
+ height: 9px;
+ }
+ &::-webkit-scrollbar-button:start:decrement,
+ &::-webkit-scrollbar-button:end:increment {
+ display: block;
+ height: 0;
+ background-color: transparent;
+ }
+ &::-webkit-scrollbar-track-piece {
+ background-color: transparent;
+ -webkit-border-radius: 0;
+ -webkit-border-bottom-right-radius: 8px;
+ -webkit-border-bottom-left-radius: 8px;
+ }
+ &::-webkit-scrollbar-thumb:vertical,
+ &::-webkit-scrollbar-thumb:horizontal {
+ background-color: lighten( $gray, 20% );
+ -webkit-border-radius: 8px;
+ border: 1px solid $white;
+ }
+ }
+ }
+
+
+ /**
+ * A notice for unsupported item types
+ */
+ .unsupported-notice {
+ padding: 1em;
+
+ h1 {
+ color: $gray-dark;
+ font-size: 16px;
+ margin-bottom: 1em;
+ }
+
+ p, small {
+ color: $gray;
+ }
+
+ p {
+ font-size: 14px;
+ margin-bottom: 0;
+ }
+ }
+ }
+}
+
+
+/**
+ * Menu: Item Action Buttons
+ */
+.menus__menu-item-actions {
+ clear: both;
+ padding: 0.5em;
+ border-top: 1px solid lighten( $gray, 30% );
+ text-align: right;
+
+ .button {
+ margin-left: 0.5em;
+ line-height: 1.3em;
+
+ &.noticon {
+ // Noticon override
+ font-family: inherit;
+ -webkit-font-smoothing: inherit;
+ color: #6f7a88; // Button color?
+
+ &:hover {
+ color: #324155; // Button color?
+ }
+
+ &:before {
+ font-size: 16px;
+ vertical-align: -3px;
+ margin: 0px -3px -1px;
+ }
+ }
+ }
+}
+
+
+/**
+ * Menu: animations, pure CSS fade-ins
+ */
+@keyframes menus__fade-from-bottom {
+ 0% {
+ opacity: 0.0; // Let's add a small delay
+ }
+ 20% {
+ opacity: 0.0;
+ transform: translateY(10px);
+ }
+ 70% {
+ opacity: 1.0;
+ }
+ 100% {
+ opacity: 1.0;
+ transform: none;
+ }
+}
+
+
+/**
+ * Menu: animations, ReactCSSTransitionGroup
+ */
+.menus__droptarget-slidevertical-enter,
+.menus__droptarget-slidevertical-leave {
+ transform: translateZ(0); /* Turns acceleration on if possible */
+
+ &.menus__droptarget-slidevertical-enter-active,
+ &.menus__droptarget-slidevertical-leave-active {
+ /* Fix for Safari / Safari Mobile: the transition doesn't work unless it's on the *-active
+ * Reference:
+ * https://github.com/facebook/react/issues/2227
+ * https://github.com/facebook/react/issues/2104
+ */
+ transition: margin 200ms ease-out, opacity 200ms ease-out;
+ }
+
+ &.is-lander {
+ span {
+ opacity: 0.0;
+ }
+
+ &:hover {
+ background: inherit;
+ color: inherit;
+ }
+ }
+}
+
+.menus__droptarget-slidevertical-enter {
+ margin-top: -55px;
+ opacity: 0.0;
+
+ &.is-position-before {
+ margin-top: 0;
+ margin-bottom: -55px;
+ }
+
+ &.menus__droptarget-slidevertical-enter-active {
+ margin-top: 0;
+ opacity: 1.0;
+
+ &.is-position-before {
+ margin-bottom: 0;
+ }
+ }
+}
+
+.menus__droptarget-slidevertical-leave {
+ margin-top: 0;
+ opacity: 1.0;
+
+ &.is-position-before {
+ margin-bottom: 0;
+ }
+
+ &.menus__droptarget-slidevertical-leave-active {
+ margin-top: -55px;
+ opacity: 0.0;
+
+ &.is-position-before {
+ margin-top: 0;
+ margin-bottom: -55px;
+ }
+ }
+}
diff --git a/assets/stylesheets/sections/_notifications.scss b/assets/stylesheets/sections/_notifications.scss
new file mode 100644
index 00000000000000..c542b6af3a77ec
--- /dev/null
+++ b/assets/stylesheets/sections/_notifications.scss
@@ -0,0 +1,59 @@
+/**
+ * Notifications
+ */
+
+#wpnt-notes-panel2 {
+ position: fixed;
+ top: 47px;
+ right: 0;
+ bottom: 0;
+ min-width: 400px;
+
+ @include breakpoint( "<480px" ) {
+ width: 100%;
+ min-width: 0;
+ }
+
+ &.wpnt-open {
+ opacity: 1;
+ pointer-events: auto;
+ }
+
+ &.wpnt-closed {
+ opacity: 0;
+ pointer-events: none;
+
+ @include breakpoint( "<480px" ) {
+ display: none;
+ }
+ }
+}
+
+#wpnt-notes-panel2.wide {
+ border-left: 0px;
+ box-shadow: none;
+}
+
+html.touch #wpnt-notes-panel2 {
+ overflow-y: scroll;
+ -webkit-overflow-scrolling: touch;
+}
+
+iframe#wpnt-notes-iframe2 {
+ width: 400px;
+ height: 100%;
+
+ @include breakpoint( "<480px" ) {
+ width: 100%;
+ }
+}
+
+iframe#wpnt-notes-iframe2.wide {
+ width: 410px;
+
+ @include breakpoint( ">960px" ) {
+ &.widescreen {
+ width: 810px;
+ }
+ }
+}
diff --git a/assets/stylesheets/sections/_nux-welcome.scss b/assets/stylesheets/sections/_nux-welcome.scss
new file mode 100644
index 00000000000000..b339a2d6121bec
--- /dev/null
+++ b/assets/stylesheets/sections/_nux-welcome.scss
@@ -0,0 +1,164 @@
+.NuxWelcome {
+ background: none;
+ position: relative;
+ padding: 24px 18px 0;
+ margin-bottom: 66px;
+ box-shadow: none;
+ &:before {
+ content: '';
+ position: absolute;
+ top: 100%;
+ left: 0;
+ right: 0;
+ height: 1px;
+ margin-top: 35px;
+ z-index: -1;
+ background: linear-gradient(to right, fade-out(lighten( $gray, 20% ), 1) 0%, lighten( $gray, 20% ) 20%, lighten( $gray, 20% ) 80%, fade-out(lighten( $gray, 20% ), 1) 100%);
+ }
+ &:after {
+ @include noticon( '\f205', 22px );
+ position: absolute;
+ top: 100%;
+ left: 50%;
+ height: 22px;
+ margin-left: -11px;
+ margin-top: 24px;
+ color: $gray;
+ padding: 0 8px;
+ background-color: lighten( $gray, 30% );
+ visibility: visible;
+ }
+ .close-button {
+ padding: 6px;
+ }
+}
+
+.NuxWelcomeMessage__title {
+ color: $gray-dark;
+ font-family: $serif;
+ font-weight: 600;
+ font-size: 24px;
+ line-height: 32px;
+ margin-bottom: 12px;
+ padding-right: 24px;
+}
+
+.NuxWelcomeMessage__primary-content {
+ font-size: 16px;
+ line-height: 24px;
+ // margin-bottom: 20px;
+ p {
+ margin-bottom: 20px;
+ }
+ .button {
+ display: block;
+ width: 100%;
+ padding: 12px 24px;
+ margin-bottom: 8px;
+ text-align: center;
+ }
+ img {
+ display: none;
+ @media only screen and (max-width: 930px) {
+ max-height: 106px;
+ }
+ }
+}
+
+.NuxWelcomeMessage__intro {
+ a {
+ color: $blue-medium;
+ text-shadow: 1px 0 lighten( $gray, 30% ), 2px 0 lighten( $gray, 30% ), -1px 0 lighten( $gray, 30% ), -2px 0 lighten( $gray, 30% );
+ background-image: linear-gradient(to bottom, transparent 50%, $blue-medium 50%);
+ background-repeat: repeat-x;
+ background-size: 2px 2px;
+ background-position: 0 85%;
+ }
+}
+
+.NuxWelcomeMessage__label {
+ color: $gray-dark;
+ font-size: 14px;
+ line-height: 18px;
+ font-weight: 600;
+ margin-bottom: 10px;
+}
+
+.NuxWelcomeMessage__list {
+ list-style-position: inside;
+ margin-left: 0;
+ li {
+ font-size: 14px;
+ line-height: 18px;
+ margin-bottom: 6px;
+ }
+}
+
+@include breakpoint( ">660px" ) {
+
+ .NuxWelcome {
+ padding: 12px 0 0;
+ .close-button {
+ &:before,
+ &:after {
+ content: '';
+ position: absolute;
+ top: 0;
+ right: 0;
+ }
+ &:before {
+ width: 108px;
+ height: 1px;
+ background: linear-gradient(to right, fade-out(lighten( $gray, 20% ), 1) 0%, lighten( $gray, 20% ) 100%);
+ }
+ &:after {
+ width: 1px;
+ height: 108px;
+ background: linear-gradient(to bottom, lighten( $gray, 20% ) 0%, fade-out(lighten( $gray, 20% ), 1) 100%);
+ }
+ }
+ .notouch & {
+ .close-button {
+ &:before,
+ &:after {
+ transition: transform 0.2s ease;
+ }
+ &:before {
+ transform-origin: right;
+ }
+ &:after {
+ transform-origin: top;
+ }
+ &:hover {
+ &:before {
+ transform: scaleX(1.4);
+ }
+ &:after {
+ transform: scaleY(1.4);
+ }
+ }
+ }
+ }
+ }
+
+ .NuxWelcomeMessage__title {
+ clear: none;
+ }
+
+ .NuxWelcomeMessage__primary-content {
+ margin-bottom: 0;
+ .button {
+ display: inline-block;
+ width: auto;
+ padding: 7px 24px;
+ margin-right: 24px;
+ }
+ img {
+ display: block;
+ float: right;
+ width: (212 / 960) * 100%;
+ margin: 6px 48px 24px 12px;
+ }
+ }
+
+}
diff --git a/assets/stylesheets/sections/_plugins.scss b/assets/stylesheets/sections/_plugins.scss
new file mode 100644
index 00000000000000..08da4e7d8076d8
--- /dev/null
+++ b/assets/stylesheets/sections/_plugins.scss
@@ -0,0 +1,56 @@
+// ==========================================================================
+// .plugins
+//
+// section-specific styles
+// ==========================================================================
+
+
+// .toolbar-bulk customizations
+.toolbar-bulk__toggle {
+ .plugins & {
+ color: $blue-wordpress;
+ margin: -49px 40px 0 0;
+ z-index: 21;
+
+ @include breakpoint( '>660px' ) {
+ margin: -39px 84px 0 0;
+ }
+ &:hover {
+ color: $link-highlight;
+ }
+ }
+}
+
+.plugin__page .plugin-icon {
+ margin-bottom: 20px;
+}
+
+.plugin__page.is-wpcom .plugin-icon {
+ margin-bottom: 0;
+}
+
+.section-group-title {
+ margin-bottom: 5px;
+ font-size: 13px;
+ text-transform: uppercase;
+
+ .plugin__information + &,
+ .card.is-compact + & {
+ margin-top: 32px;
+ }
+}
+
+.plugins__list,
+.toolbar-bulk.is-all-sites {
+ margin-bottom: 20px;
+}
+
+.plugins__list-header {
+ background: $white;
+ color: $gray-dark;
+ font-size: 12px;
+ box-shadow: 0 0 0 1px transparentize( lighten( $gray, 20% ), .5 ),
+ 0 1px 2px lighten( $gray, 30% );
+ padding: 14px 24px;
+ text-transform: uppercase;
+}
diff --git a/assets/stylesheets/sections/_post-relative-time-status.scss b/assets/stylesheets/sections/_post-relative-time-status.scss
new file mode 100644
index 00000000000000..54e170e5a45458
--- /dev/null
+++ b/assets/stylesheets/sections/_post-relative-time-status.scss
@@ -0,0 +1,40 @@
+.post-relative-time-status {
+ .time,
+ .status {
+ display: inline-block;
+ margin-right: (11 / 12) * 1em;
+ }
+
+ .time .time-text {
+ display: inline-block;
+ &::first-letter {
+ text-transform: capitalize;
+ }
+ }
+
+ .status .status-text {
+ text-transform: capitalize;
+ }
+
+ .noticon {
+ display: inline-block;
+ font-size: (13 / 12) * 1em;
+ margin-right: (3 / 13) * 1em;
+ }
+
+ // Apply colors on /posts/* cards
+ .posts__list & {
+ .is-sticky {
+ color: $orange-jazzy;
+ }
+ .is-pending {
+ color: $alert-yellow;
+ }
+ .is-scheduled {
+ color: $blue-medium;
+ }
+ .is-trash {
+ color: $alert-red;
+ }
+ }
+}
diff --git a/assets/stylesheets/sections/_posts-controls.scss b/assets/stylesheets/sections/_posts-controls.scss
new file mode 100644
index 00000000000000..4f604125e14a54
--- /dev/null
+++ b/assets/stylesheets/sections/_posts-controls.scss
@@ -0,0 +1,83 @@
+/**
+ * Post Controls
+ */
+.post-controls {
+ box-sizing: border-box;
+ background-color: $gray-light;
+ overflow: hidden;
+ position: relative;
+ width: 100%;
+ height: (43 / 15) * 1em;
+}
+
+.post-controls__pane {
+ // Flex
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: flex-start;
+ align-content: flex-start;
+ align-items: stretch;
+ // Normal
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ list-style: none;
+ font-size: (14 / 15) * 1em;
+ margin: 0;
+ transition: transform 0.2s ease, opacity 0.2s ease;
+ > li {
+ // Flex
+ flex-grow: 1;
+ flex-shrink: 0;
+ flex-basis: auto;
+ // Normal
+ box-sizing: border-box;
+ text-align: center;
+ border-left: solid 1px lighten( $gray, 30% );
+ &:first-child {
+ border-left: none;
+ }
+ a {
+ display: block;
+ box-sizing: border-box;
+ font-size: inherit;
+ padding: (11 / 14) * 1em 0;
+ &:hover {
+ cursor: pointer;
+ }
+ .gridicon{
+ position: relative;
+ top: 3px;
+ margin-right: 6px;
+ }
+ }
+ }
+}
+
+
+
+.post-controls__more-options {
+ transform: scale(0);
+ opacity: 0;
+ pointer-events: none;
+}
+
+// @todo should be moved, together with its logic, to "post-controls" not "post"
+.post {
+ // Show More Options
+ &.show-more-options {
+ .post-controls__main-options {
+ transform: scale(0);
+ opacity: 0;
+ pointer-events: none;
+ }
+ .post-controls__more-options {
+ transform: scale(1);
+ opacity: 1;
+ pointer-events: auto;
+ }
+ }
+}
diff --git a/assets/stylesheets/sections/_posts.scss b/assets/stylesheets/sections/_posts.scss
new file mode 100644
index 00000000000000..26818a6a9569da
--- /dev/null
+++ b/assets/stylesheets/sections/_posts.scss
@@ -0,0 +1,349 @@
+/**
+ * Posts
+ */
+
+.posts__list {
+
+ .post {
+ position: relative;
+ padding: 0;
+ margin-bottom: (24 / 15) * 1em;
+ @include breakpoint( "<660px" ) {
+ font-size: 13px;
+ }
+ }
+
+ .post__body {
+ background-color: $white;
+ }
+
+ .post-attribution,
+ .post__content,
+ .post__info {
+ box-sizing: border-box;
+ padding: (14 / 10) * 1rem; // from _reset.scss: html { font-size: 62.5% } (or 10px), hence base 10
+ @include breakpoint( ">660px" ) {
+ padding: (24 / 10) * 1rem;
+ }
+ }
+
+ .post-attribution {
+ width: 100%;
+ font-size: (14 / 15) * 1em;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ color: $gray;
+ a {
+ color: inherit;
+ }
+ span:first-child {
+ .post-attribution-avatar {
+ margin-left: 0;
+ }
+ }
+ + .post__content {
+ padding-top: 0;
+ }
+ @include breakpoint( ">660px" ) {
+ padding: (16 / 10) * 1rem (24 / 10) * 1rem;
+ }
+ }
+
+ .post-attribution-avatar {
+ width: (24 / 14) * 1em;
+ height: (24 / 14) * 1em;
+ margin: 0 (5 / 14) * 1em 0 (7 / 14) * 1em;
+ vertical-align: middle;
+ display: inline-block;
+ &.is-rounded {
+ border-radius: 50%;
+ }
+ }
+
+ .post__content {
+ padding-top: (16 / 10) * 1rem;
+ padding-bottom: (8 / 10) * 1rem;
+ margin: 0;
+ overflow: hidden;
+ + .post__info {
+ padding-top: 0;
+ }
+ @include breakpoint( ">660px" ) {
+ padding-bottom: (12 / 10) * 1rem;
+ }
+ }
+
+ .post__content-link {
+ display: block;
+ }
+
+ .post__title-link {
+ + .post__excerpt {
+ margin-top: (7 / 15) * 1em;
+ }
+ + .featured-standard {
+ margin-top: (13 / 15) * 1em;
+ + .post__excerpt {
+ margin-top: (7 / 15) * 1em;
+ }
+ }
+ }
+
+ .post__title {
+ color: $gray-dark;
+ font-size: (24 / 15) * 1em;
+ line-height: (32 / 24) * 1em;
+ font-family: $serif;
+ font-weight: 700;
+ }
+
+ .post__excerpt {
+ color: darken( $gray, 20 );
+ font-size: (16 / 15) * 1em;
+ line-height: (24 / 16) * 1em;
+ font-family: $serif;
+ p {
+ margin: 0;
+ }
+ }
+
+ .post__quote {
+ background: none;
+ font-family: $serif;
+ font-style: italic;
+ padding: 0;
+ margin: 0;
+ border-radius: 0;
+ }
+
+ .post__info {
+ width: 100%;
+ font-size: (14 / 15) * 1em;
+ padding-top: (16 / 10) * 1rem;
+ padding-bottom: (16 / 10) * 1rem;
+ color: $gray;
+ overflow: hidden;
+ @include breakpoint( ">660px" ) {
+ padding-top: (20 / 10) * 1rem;
+ padding-bottom: (20 / 10) * 1rem;
+ }
+ }
+
+ .post-relative-time-status {
+ float: left;
+ margin: 0;
+ .noticon {
+ font-size: (16 / 14) * 1em;
+ margin: (3 / 13) * 1em (3 / 13) * 1em 0 0;
+ }
+ a {
+ color: inherit;
+ }
+ small {
+ font-size: 0.9em;
+ color: lighten( $gray, 10% );
+ }
+ }
+
+ .post__meta {
+ float: right;
+ margin: 0;
+ list-style: none;
+ box-sizing: border-box;
+ li {
+ display: inline-block;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ margin-left: (15 / 14) * 1em;
+ color: $gray;
+ vertical-align: top;
+ &:first-child {
+ margin-left: 0;
+ }
+ a {
+ color: inherit;
+ &:before {
+ @extend %noticon;
+ font-size: (21 / 14) * 1em;
+ vertical-align: top;
+ }
+ &.is-empty {
+ .gridicon {
+ color: lighten( $gray, 20% );
+ }
+ }
+ .gridicon{
+ position: relative;
+ top: 6px;
+ }
+ }
+ span {
+ display: inline-block;
+ margin: (1 / 14) * 1em 0 0 (3 / 14) * 1em;
+ }
+ }
+ }
+
+ // ---- Image Elements ----
+ .image-large-format {
+ background-color: $gray-light;
+ // Flex
+ display: flex;
+ flex-wrap: nowrap;
+ align-content: flex-start;
+ .image-item {
+ box-sizing: border-box;
+ max-height: 100%;
+ padding: 0 (6 / 10) * 1rem;
+ flex: 1 0 auto;
+ }
+ .image-item-media {
+ vertical-align: bottom;
+ }
+ }
+
+ .post__header {
+ + .post__content-link {
+ .image-large-format {
+ margin-top: (16 / 10) * 1rem;
+ }
+ }
+ }
+
+ // NOTE: Gallery & Image single are large formats
+ .image-gallery {
+ padding: (6 / 10) * 1rem (8 / 10) * 1rem;
+ // Flex
+ flex-direction: row;
+ justify-content: flex-start;
+ align-items: center;
+ @include breakpoint( ">660px" ) {
+ padding: (6 / 10) * 1rem (18 / 10) * 1rem;
+ }
+ }
+
+ .image-single {
+ height: (226 / 15) * 1em;
+ background-size: cover;
+ background-repeat: no-repeat;
+ background-position: center;
+ @include breakpoint( ">960px" ) {
+ background-size: contain;
+ }
+ }
+
+ // NOTE: Standard is meant to be inline with excerpt on standard posts
+ .featured-standard {
+ float: right;
+ width: (2 / 5) * 100%;
+ max-height: (136 / 15) * 1em;
+ margin: 0 0 (14 / 15) * 1em (14 / 15) * 1em;
+ overflow: hidden;
+ img {
+ display: block;
+ }
+ @include breakpoint( ">660px" ) {
+ margin: 0 0 (24 / 15) * 1em (24 / 15) * 1em;
+ }
+ @include breakpoint( ">960px" ) {
+ margin-bottom: 0;
+ }
+ }
+
+ // ---- Post Variations ----
+ .post {
+ // Protected
+ &.is-protected {
+ .post__title {
+ &:before {
+ @include noticon( '\f470', 1em );
+ color: $gray;
+ margin-right: (4 / 24) * 1em;
+ }
+ }
+ }
+ // Placeholder
+ &.is-placeholder {
+ .post__time {
+ &:before {
+ content: '';
+ margin-right: 0;
+ }
+ }
+ .post-attribution-avatar {
+ display: inline-block;
+ background-color: lighten( $gray, 30% );
+ }
+ }
+ }
+
+ // ---- Placeholder helpers ----
+ .placeholder-text {
+ color: transparent;
+ background-color: lighten( $gray, 30% );
+ animation: loading-fade 1.6s ease-in-out infinite;
+ }
+
+}
+
+.post__header {
+ // Optical alignment of text to base grid
+ padding: 14px 16px 0;
+
+ @include breakpoint( ">660px" ) {
+ padding: 14px 24px 0;
+ }
+}
+.post .site-icon {
+ position: absolute;
+ top: 16px;
+ left: 16px;
+ @include breakpoint( ">660px" ) {
+ left: 24px;
+ }
+}
+.post__site-title {
+ color: darken( $gray, 20 );
+ display: block;
+ font-size: 14px;
+ line-height: 38px;
+ margin-right: 12px;
+ padding-left: 48px;
+
+ a {
+ color: darken( $gray, 20 );
+ }
+}
+.post__header.has-author {
+ // Vertically position the title
+ .post__site-title {
+ line-height: 1.4;
+ }
+}
+.post__author {
+ display: block;
+ color: $gray;
+ font-size: 12px;
+ padding-left: 48px;
+}
+
+
+/* RTL */
+.rtl .posts__list {
+ //smaller font, let's use Tahoma
+ .post__quote {
+ font-family: $sans-rtl;
+ }
+ //we can use the default sans for titles
+ .post__title {
+ font-family: $sans;
+ }
+}
+
+:lang(he) .rtl .posts__list {
+ .post__quote {
+ font-family: $sans;
+ }
+}
diff --git a/assets/stylesheets/sections/_sharing.scss b/assets/stylesheets/sections/_sharing.scss
new file mode 100644
index 00000000000000..5c0460379b6221
--- /dev/null
+++ b/assets/stylesheets/sections/_sharing.scss
@@ -0,0 +1,1175 @@
+$color-facebook: #39579a;
+$color-twitter: #55ACEE;
+$color-gplus: #df4a32;
+$color-tumblr: #35465c;
+$color-linkedin: #0976b4;
+$color-path: #df3b2f;
+$color-instagram: #517fa4;
+$color-eventbrite: #ff8000;
+$color-stumbleupon: #eb4924;
+$color-reddit: #5f99cf;
+$color-pinterest: #cc2127;
+$color-pocket: #ee4256;
+$color-email: #f8f8f8;
+$color-print: #f8f8f8;
+
+.sharing-settings {
+ // labels, checkboxes, radio
+
+ label label {
+ margin: 0;
+ }
+
+ select {
+ font-size: 18px;
+ }
+
+ input[type='number'] {
+ width: 50px;
+ height: 20px;
+ padding: 0 0 1px 2px;
+ }
+
+ h4 {
+ font-size: 18px;
+ margin-bottom: 0.5em;
+ }
+
+ @include breakpoint( "<660px" ) {
+ padding: 0 4em;
+ }
+
+ @include breakpoint( "<480px" ) {
+ padding: 0 0.25em;
+ }
+}
+
+.sharing-settings.sharing-connections {
+ @include breakpoint( "<480px" ) {
+ padding: 16px 0;
+ }
+
+ h2 {
+ font-size: 20px;
+ margin-bottom: 10px;
+ }
+
+ .sharing-link p {
+ margin-bottom: 1em;
+ }
+
+ .button.is-warning {
+ background: darken( $alert-yellow, 3% );
+ border-color: darken( $alert-yellow, 10% );
+ color: $white;
+
+ &:hover {
+ border-color: darken( $alert-yellow, 15% );
+ }
+
+ &:focus {
+ border-color: darken( $alert-yellow, 15% );
+ }
+
+ &[disabled] {
+ background: lighten( $alert-yellow, 12% ) !important;
+ color: $white !important;
+ border-color: lighten( $alert-yellow, 8% ) !important;
+ }
+ }
+
+ .noticon-checkmark,
+ .noticon-warning {
+ vertical-align: middle;
+ margin-right: 0.25em;
+ }
+
+ .noticon-checkmark {
+ font-size: 18px;
+ margin: 0 1px 0 -3px;
+ }
+
+ .noticon-warning {
+ font-size: 12px;
+ margin: -1px 6px 0 0;
+ }
+
+ .sharing-service__content.is-placeholder .sharing-service-examples,
+ .sharing-service__content.is-placeholder .sharing-service-accounts-detail,
+ .sharing-service__content.is-placeholder .sharing-service-tip {
+ display: none;
+ }
+
+ .sharing-service-example {
+ display: inline-block;
+ vertical-align: top;
+ width: 48%;
+
+ @include breakpoint( "<660px" ) {
+ display: block;
+ width: 100%;
+ margin: 20px 0;
+ padding: 0;
+ }
+
+ &:first-child {
+ padding-right: 4%;
+
+ @include breakpoint( "<660px" ) {
+ border-bottom: 1px solid lighten( $gray, 30% );
+ padding-bottom: 10px;
+ }
+
+ @include breakpoint( "<480px" ) {
+ margin-bottom: 16px;
+ padding: 0 0 16px 0;
+ }
+ }
+
+ &.is-single {
+ width: 100%;
+ }
+ }
+
+ .sharing-service-example-screenshot {
+ border: 1px solid lighten( $gray, 30% );
+
+ img {
+ vertical-align: top;
+ }
+ }
+
+ .sharing-service-example-screenshot-label {
+ margin-top: 10px;
+ }
+
+ .sharing-service-tip {
+ margin-top: 16px;
+ font-size: 14px;
+ color: darken( $gray,10 );
+
+ .noticon-info {
+ margin: 3px 3px 0 0;
+ }
+ }
+
+ .sharing-service-tip:empty {
+ display: none;
+ }
+
+ .sharing-service {
+ position: relative;
+ overflow: hidden;
+ background: $white;
+
+ &.not-connected .sharing-service-examples {
+ display: block;
+ }
+
+ &.not-connected .sharing-service-accounts-detail,
+ &.not-connected .sharing-service-tip {
+ display: none;
+ }
+ }
+
+ .sharing-service-examples {
+ display: none;
+ }
+
+ .sharing-service-accounts-detail {
+
+ h2 {
+ font-size: 1.2em;
+ }
+
+ .new-account {
+ padding-bottom: 0.48em;
+ background: $white;
+ #content & {
+ font-size: 0.9em;
+ }
+ }
+ }
+
+ .sharing-service-connected-accounts {
+ margin-left: 0;
+ margin-bottom: 8px;
+ }
+}
+
+.sharing-service {
+ //@extend %container;
+ box-shadow: 0 0 0 1px rgba( lighten( $gray, 20% ), 0.5 ), 0 1px 2px lighten( $gray, 30% );
+ margin-bottom: 16px;
+ padding: 0;
+
+ &:hover {
+ box-shadow: 0 0 0 1px lighten( $gray, 15% );
+ }
+
+ @include breakpoint( "<660px" ) {
+ margin: 0 0 16px 0;
+ }
+
+ &.is-open {
+ box-shadow: 0 0 0 1px lighten( $gray, 10% );
+ }
+}
+
+.sharing-service-action {
+ position: absolute;
+ right: 16px;
+ top: 22px;
+
+ @include breakpoint( "<660px" ) {
+ right: 10px;
+ top: 15px;
+ }
+
+ @include breakpoint( "<480px" ) {
+ top: 11px;
+ }
+
+ &.is-warning {
+ background: darken( $alert-yellow, 3% );
+ border-color: darken( $alert-yellow, 10% );
+ color: $white;
+
+ &:hover {
+ border-color: darken( $alert-yellow, 15% );
+ }
+
+ &:focus {
+ border-color: darken( $alert-yellow, 15% );
+ }
+
+ &[disabled] {
+ background: lighten( $alert-yellow, 12% ) !important;
+ color: $white !important;
+ border-color: lighten( $alert-yellow, 8% ) !important;
+ }
+ }
+}
+
+.sharing-service__content {
+ position: relative;
+ display: none;
+ padding: 15px 20px 20px 20px;
+ background: lighten( $gray, 30% );
+ border-top: 1px solid lighten( $gray, 20% );
+
+ &.is-placeholder {
+ height: 180px;
+ }
+
+ &.is-placeholder::before {
+ content: '';
+ position: absolute;
+ top: 16px;
+ right: 16px;
+ bottom: 16px;
+ left: 16px;
+ background-color: lighten( $gray, 20% );
+ animation: loading-fade 1.6s ease-in-out infinite;
+ }
+}
+
+.sharing-service.is-open .sharing-service__content {
+ display: block;
+}
+
+.sharing-service__overview {
+ padding: 16px;
+ cursor: pointer;
+ user-select: none;
+}
+
+.sharing-service__overview:hover .sharing-service__content-toggle {
+ color: $blue-medium;
+}
+
+.sharing-service__icon {
+ float: left;
+ height: 42px;
+ padding-top: 10px;
+ margin: 0 15px 0 32px;
+ text-align: center;
+ width: 50px;
+ border-radius: 4px;
+
+ @include breakpoint( "<660px" ) {
+ height: 21px;
+ padding-top: 5px;
+ margin: 2px 8px 0 32px;
+ width: 25px;
+ }
+}
+
+.sharing-service__glyph.noticon {
+ color: $white;
+ font-size: 32px;
+ margin: 0;
+ line-height: 1;
+ width: auto;
+ height: auto;
+
+ @include breakpoint( "<660px" ) {
+ font-size: 16px;
+ }
+}
+
+@mixin sharing-service( $name, $color ) {
+ .sharing-service.#{ $name } .sharing-service__icon {
+ background: $color;
+ }
+
+ .sharing-service.#{ $name } .sharing-service__name {
+ color: $color;
+ }
+
+ .sharing-connection__account-avatar.is-fallback.#{ $name } {
+ background-color: $color;
+ }
+}
+
+@include sharing-service( "facebook", $color-facebook );
+@include sharing-service( "twitter", $color-twitter );
+@include sharing-service( "google_plus", $color-gplus );
+@include sharing-service( "tumblr", $color-tumblr );
+@include sharing-service( "linkedin", $color-linkedin );
+@include sharing-service( "path", $color-path );
+@include sharing-service( "instagram", $color-instagram );
+@include sharing-service( "eventbrite", $color-eventbrite );
+
+.sharing-service__name {
+ clear: none;
+ font-size: 20px;
+}
+
+.sharing-service__description {
+ margin: 0;
+ color: darken( $gray, 20% );
+
+ @include breakpoint( "<480px" ) {
+ display: none;
+ }
+
+ @include breakpoint( "<660px" ) {
+ margin-left: 30px;
+ }
+}
+
+.sharing-service.connected .sharing-service__description {
+ font-style: italic;
+}
+
+.sharing-service.reconnect .sharing-service__description {
+ font-weight: bold;
+}
+
+.sharing-service__content-toggle {
+ position: absolute;
+ left: 12px;
+ top: 30px;
+ font-size: 22px;
+ color: #aeb8be;
+
+ @include breakpoint( "<480px" ) {
+ left: 14px;
+ top: 20px;
+ }
+}
+
+.sharing-service.is-placeholder .sharing-service__icon,
+.sharing-service.is-placeholder .sharing-service__name,
+.sharing-service.is-placeholder .sharing-service__description {
+ background-color: lighten( $gray, 30% );
+ animation: loading-fade 1.6s ease-in-out infinite;
+}
+
+.sharing-service.is-placeholder .sharing-service__name,
+.sharing-service.is-placeholder .sharing-service__description {
+ margin-left: 97px;
+
+ @include breakpoint( "<660px" ) {
+ margin-left: 65px;
+ }
+}
+
+.sharing-service.is-placeholder .sharing-service__name {
+ margin-top: 4px;
+ margin-bottom: 4px;
+ height: 22px;
+ width: 20%;
+
+ @include breakpoint( "<660px" ) {
+ width: 30%;
+ }
+}
+
+.sharing-service.is-placeholder .sharing-service__description {
+ margin-top: 8px;
+ margin-bottom: 2px;
+ height: 16px;
+ width: 45%;
+}
+
+.sharing-connection {
+ position: relative;
+ background: transparent;
+ box-shadow: none;
+ border-bottom: 1px solid lighten( $gray, 30% );
+ list-style-type: none;
+ margin-bottom: 0;
+ padding: 8px 0;
+
+ &:first-child {
+ border-top: 1px solid lighten( $gray, 30% );
+ }
+
+ &.disabled {
+ opacity: 0.4;
+ }
+}
+
+.sharing-connection__account-avatar {
+ position: absolute;
+ left: 0;
+ top: 8px;
+ height: 40px;
+ width: 40px;
+ border: 1px solid $gray-light;
+ color: white;
+
+ &.is-fallback::before {
+ @include noticon( '\f304', 40px );
+ }
+}
+
+.sharing-connection__account-status {
+ padding: 10px 90px 10px 48px;
+
+ &.is-shareable {
+ padding-top: 0;
+ padding-bottom: 0;
+ }
+
+ @include breakpoint( "<480px" ) {
+ padding-right: 0;
+ }
+}
+
+.sharing-service.reconnect .sharing-connection__account-status {
+ padding-right: 200px;
+
+ @include breakpoint( "<480px" ) {
+ padding-right: 0;
+ }
+}
+
+.sharing-connection__account-name {
+ font-weight: 600;
+}
+
+.sharing-connection__keyring-user {
+ display: inline-block;
+ margin-left: 8px;
+ font-size: 11px;
+ font-weight: 600;
+ text-transform: uppercase;
+ color: $gray;
+}
+
+.sharing-connection__account-sitewide-connection {
+ display: block;
+}
+
+.sharing-connection__account-sitewide-connection input[type="checkbox"] {
+ margin-top: 3px;
+}
+
+.sharing-connection.disabled .sharing-connection__account-action {
+ cursor: default;
+ cursor: progress;
+}
+
+.sharing-connection__account-actions {
+ position: absolute;
+ right: -10px;
+ top: 50%;
+ margin-top: -16px;
+
+ @include breakpoint( "<480px" ) {
+ position: static;
+ margin-top: 4px;
+ text-align: right;
+ }
+}
+
+.sharing-connection__account-action {
+ display: inline-block;
+ padding: 8px 10px;
+ cursor: pointer;
+ line-height: 16px;
+}
+
+.sharing-connection__account-action.reconnect {
+ color: $alert-yellow;
+
+ &:before {
+ @include noticon( '\f414', 16px );
+ margin-right: 8px;
+ }
+}
+
+.sharing-connection.disabled .sharing-connection__account-action {
+ cursor: default;
+ cursor: progress;
+}
+
+// Sharing Buttons Section
+
+.sharing-settings.sharing-buttons {
+ .sharing-button-styles {
+ box-shadow: 0 -2px 0 lighten( $gray, 30% ) inset;
+ padding-bottom: 0.5em;
+ }
+
+ // Preview (sourced from Sharing plugin)
+
+ .official-preview {
+ vertical-align: top;
+ }
+
+ .add-new-service,
+ .sharing-buttons-add,
+ .sharing-buttons-add-save,
+ .sharing-buttons-add-cancel {
+ cursor: pointer;
+ }
+
+ .sharing-buttons-add {
+ float: right;
+
+ .noticon {
+ font-size: 32px;
+ margin: 7px -18px -2px 0 #{"/*rtl:ignore*/"};
+ }
+
+ &.inactive {
+ opacity: 0.3;
+ }
+ }
+}
+
+.sharing-buttons__submit {
+ float: right;
+}
+
+.sharing-buttons-tray__buttons {
+ @include breakpoint ( "<480px" ) {
+ margin-right: -16px;
+ }
+}
+
+.sharing-buttons-preview-buttons__more {
+ position: absolute;
+ z-index: 1000;
+ max-width: 400px;
+ margin-top: -4px;
+ padding-top: 12px;
+ transform: scaleY( 0 );
+ transform-origin: 50% 6px;
+ transition: transform 0.2s ease-out;
+
+ &.is-visible {
+ transition-timing-function: ease-in;
+ transform: scaleY( 1 );
+ }
+}
+
+.sharing-buttons-preview-buttons__more-inner {
+ min-width: 68px;
+ min-height: 34px;
+ padding: 10px 0 2px 8px;
+ border: 1px solid #ccc;
+ border-radius: 2px;
+ background-color: $white;
+ box-shadow: 0px 5px 20px rgba( 0, 0, 0, .2 );
+}
+
+.sharing-buttons-preview-buttons__more-inner .sharing-buttons-preview-button,
+.sharing-buttons-preview-buttons__more-inner .sharing-buttons-preview-button.style-icon {
+ margin-top: 0;
+ margin-bottom: 8px;
+}
+
+.sharing-buttons-preview-buttons__more::before {
+ content: "";
+ position: absolute;
+ top: 8px;
+ left: 20px #{"/*rtl:ignore*/"};
+ display: block;
+ width: 8px;
+ height: 8px;
+ transform: rotate( 45deg );
+ background-color: $white;
+ border-top: 1px solid #ccc;
+ border-left: 1px solid #ccc #{"/*rtl:ignore*/"};
+}
+
+.sharing-buttons__panel {
+ @include clear-fix;
+ position: relative;
+ margin-bottom: 20px;
+ padding: 20px 24px;
+ background: $white;
+ box-shadow: 0 0 0 1px rgba( lighten( $gray, 20% ), 0.5 ), 0 1px 2px lighten( $gray, 30% );
+}
+
+.sharing-buttons__fieldset-group {
+ @include clear-fix;
+ margin: 0 -3%;
+
+ @include breakpoint( "<660px" ) {
+ margin: 0;
+ }
+}
+
+.sharing-buttons__fieldset {
+ float: left;
+ display: block;
+ width: 50%;
+ padding: 0 3%;
+ margin-bottom: 20px;
+
+ @include breakpoint( "<660px" ) {
+ width: 100%;
+ padding: 0;
+ }
+}
+
+.sharing-buttons__fieldset input[type="radio"],
+.sharing-buttons__fieldset input[type="checkbox"] {
+ margin-right: 8px;
+}
+
+.sharing-buttons__fieldset label {
+ display: block;
+ cursor: pointer;
+}
+
+.sharing-buttons__fieldset-heading {
+ margin-bottom: 5px;
+ font-weight: 600;
+}
+
+.sharing-buttons__fieldset-detail {
+ display: block;
+ margin: 5px 0;
+ font-size: 13px;
+ font-style: italic;
+ color: $gray;
+}
+
+.sharing-buttons-preview {
+ position: relative;
+ margin: 20px 0;
+}
+
+.sharing-buttons-preview__reblog-like {
+ margin: 14px 0;
+}
+
+.sharing-buttons-preview__reblog-like .sharing-buttons-preview-button {
+ margin: 0 1em 3px 0;
+}
+
+.sharing-buttons-preview__reblog,
+.sharing-buttons-preview__like {
+ padding: 1px 8px 0px 5px;
+ line-height: 25px;
+
+ &:before {
+ display: none;
+ }
+
+ &:hover {
+ color: #777;
+ }
+}
+
+.sharing-buttons-preview__reblog .noticon,
+.sharing-buttons-preview__like .noticon {
+ vertical-align: top;
+ margin: 3px 4px 0 0;
+ color: $blue-medium;
+}
+
+.sharing-buttons-preview__like .noticon {
+ margin: 3px 2px 0 -1px;
+}
+
+.sharing-buttons-preview__fake-user {
+ border: 1px solid lighten( $gray, 20% );
+ display: inline-block;
+ height: 24px;
+ width: 24px;
+ line-height: 1;
+ vertical-align: top;
+}
+
+.sharing-buttons-preview__fake-like {
+ color: darken( $gray, 10% );
+ font-size: 11px;
+ font-weight: 300;
+}
+
+.sharing-buttons-preview .sortable-list__navigation {
+ margin-right: 16px;
+}
+
+.sharing-buttons-preview-action {
+ position: relative;
+ overflow: visible;
+ display: none;
+ padding: 8px 8px 8px 26px;
+ cursor: pointer;
+ border: 1px solid lighten( $gray, 20% );
+ border-radius: 4px;
+ box-shadow: 0 0 8px rgba( $gray-dark, 0.04 );
+ background-color: $white;
+ text-align: left;
+ line-height: 1;
+ color: $blue-wordpress;
+
+ @include breakpoint( "<480px") {
+ padding-right: 0;
+ }
+
+ &:hover {
+ color: $blue-medium;
+ }
+
+ &.is-active {
+ display: inline-block;
+ }
+
+ &.is-edit::before {
+ @include noticon( '\f411', 16px );
+ margin-right: 4px;
+ }
+
+ &:disabled {
+ cursor: default;
+ border-color: lighten( $gray, 30% );
+ color: lighten( $gray, 30% );
+ }
+
+ &.is-add::before {
+ @include noticon( '\f510', 16px );
+ margin-right: 4px;
+ }
+
+ &.is-top::after,
+ &.is-bottom::after {
+ content: '';
+ position: absolute;
+ display: block;
+ width: 12px;
+ height: 12px;
+ border: 1px solid lighten( $gray, 20% );
+ background-color: $white;
+ transform: rotate( 45deg );
+ }
+
+ &:disabled.is-top::after,
+ &:disabled.is-bottom::after {
+ border-color: lighten( $gray, 30% );
+ }
+
+ &.is-top {
+ margin-bottom: 14px;
+ }
+
+ &.is-bottom {
+ margin-top: 14px;
+ @include breakpoint( "<480px" ) {
+ margin: 14px 1% 0;
+ }
+ }
+
+ &.is-top::after {
+ bottom: -7px;
+ border-top-width: 0;
+ border-left-width: 0 #{"/*rtl:ignore*/"};
+ }
+
+ &.is-bottom::after {
+ top: -7px;
+ border-right-width: 0 #{"/*rtl:ignore*/"};
+ border-bottom-width: 0;
+ }
+
+ &.is-left::after {
+ left: 30px;
+ }
+
+ &.is-right::after {
+ right: 15px;
+ }
+}
+
+.sharing-buttons-preview-action .noticon {
+ position: absolute;
+ left: 6px;
+ top: 50%;
+ margin-top: -8px;
+ font-size: 16px;
+}
+
+.sharing-buttons-preview__button-tray-actions {
+ @include breakpoint( "<480px" ) {
+ margin: 0 -6%;
+ }
+}
+
+.sharing-buttons-preview-action + .sharing-buttons-preview-action {
+ margin-left: 8px;
+
+ @include breakpoint( "<480px" ) {
+ margin-left: 1%;
+ }
+}
+
+.sharing-buttons-preview-action:last-child {
+ @include breakpoint( "<480px" ) {
+ width: 46%;
+ }
+}
+
+.sharing-buttons-preview-action {
+ @include breakpoint( "<480px" ) {
+ width: 50%;
+ }
+
+ &::before {
+ @include breakpoint( "<480px" ) {
+ float: left;
+ margin: 6px 6px 0 0;
+ }
+ }
+}
+
+.sharing-buttons-preview__display {
+ padding: 10px 20px;
+ border: 1px solid lighten( $gray, 30% );
+ box-shadow: 0 3px 6px -3px rgba( 0, 0, 0, 0.05 );
+}
+
+.sharing-buttons-preview__heading {
+ margin: 0;
+ padding: 8px 0;
+ background-color: $gray-light;
+ border: 1px solid lighten( $gray, 30% );
+ border-bottom: 0;
+ font-size: 11px;
+ line-height: 1;
+ font-weight: bold;
+ text-transform: uppercase;
+ text-align: center;
+ color: $gray;
+}
+
+.sharing-buttons-preview.is-placeholder .sharing-buttons-preview__label,
+.sharing-buttons-preview.is-placeholder .sharing-buttons-preview__buttons {
+ display: block;
+ background-color: lighten( $gray, 30% );
+ animation: loading-fade 1.6s ease-in-out infinite;
+}
+
+.sharing-buttons-preview__label {
+ display: block;
+ margin: 8px 0;
+ font-size: 11px;
+ font-weight: bold;
+ line-height: 1.2;
+ text-transform: uppercase;
+ color: #767676;
+}
+
+.sharing-buttons-preview.is-placeholder .sharing-buttons-preview__label {
+ height: 13px;
+ width: 80px;
+}
+
+.sharing-buttons-preview__buttons {
+ margin-top: 0.25em;
+}
+
+.sharing-buttons-preview.is-placeholder .sharing-buttons-preview__buttons {
+ height: 26px;
+ width: 75%;
+ margin-bottom: 16px;
+}
+
+.sharing-buttons-preview-button {
+ display: inline-block;
+ margin: 8px 8px 0 0;
+ cursor: default;
+ font-size: 12px;
+ border-radius: 3px;
+ color: #777;
+ background: #f8f8f8;
+ border: 1px solid #ccc;
+ box-shadow: 0 1px 0 rgba( 0, 0, 0, .08 );
+ line-height: 23px;
+ padding: 1px 8px 0px 5px;
+
+ &.style-icon {
+ border-radius: 50%;
+ border: 0;
+ box-shadow: none;
+ padding: 8px;
+ position: relative;
+ top: -2px;
+ line-height: 1;
+ width: auto;
+ height: auto;
+ margin-bottom: 0;
+ }
+}
+
+.sharing-buttons-preview__display .sharing-buttons-preview-button {
+ display: none;
+
+ &.is-enabled {
+ display: inline-block;
+ }
+}
+
+&.style-text .sharing-buttons-preview-button__glyph,
+&.style-text .sharing-buttons-preview-button__custom-icon,
+&.style-icon .sharing-buttons-preview-button__service {
+ display: none;
+}
+
+.sharing-buttons-preview-button__service {
+ line-height: 23px;
+ margin-left: 3px;
+}
+
+.sharing-buttons-preview-button.style-icon .sharing-buttons-preview-button__service {
+ line-height: 1;
+}
+
+.sharing-buttons-preview-button__glyph {
+ vertical-align: top;
+ position: relative;
+ top: 3px;
+ text-align: center;
+}
+
+.sharing-buttons-preview-button.style-icon .sharing-buttons-preview-button__glyph {
+ top: 0;
+ color: $white;
+}
+
+.sharing-buttons-preview-button.style-icon.share-email .sharing-buttons-preview-button__glyph,
+.sharing-buttons-preview-button.style-icon.share-print .sharing-buttons-preview-button__glyph {
+ color: #777;
+}
+
+.sharing-buttons-preview-button__custom-icon {
+ display: inline-block;
+ vertical-align: top;
+ width: 16px;
+ height: 16px;
+ background-position: left center;
+ background-repeat: no-repeat;
+ background-size: 100%;
+}
+
+@mixin sharing-button-service( $name, $color ) {
+ .sharing-buttons-preview-button.style-icon.share-#{ $name } {
+ background: $color;
+
+ &:hover {
+ background: rgba( $color, 0.9 );
+ }
+ }
+}
+
+@include sharing-button-service( "facebook", $color-facebook );
+@include sharing-button-service( "twitter", $color-twitter );
+@include sharing-button-service( "press-this", $blue-wordpress );
+@include sharing-button-service( "path", $color-path );
+@include sharing-button-service( "google-plus-1", $color-gplus );
+@include sharing-button-service( "instagram", $color-instagram );
+@include sharing-button-service( "eventbrite", $color-eventbrite );
+@include sharing-button-service( "linkedin", $color-linkedin );
+@include sharing-button-service( "stumbleupon", $color-stumbleupon );
+@include sharing-button-service( "tumblr", $color-tumblr );
+@include sharing-button-service( "reddit", $color-reddit );
+@include sharing-button-service( "pinterest", $color-pinterest );
+@include sharing-button-service( "pocket", $color-pocket );
+@include sharing-button-service( "email", $color-email );
+@include sharing-button-service( "print", $color-email );
+
+.sharing-buttons-preview__panel {
+ position: relative;
+ display: none;
+ background: $white;
+ border: 1px solid lighten( $gray, 20% );
+ border-radius: 4px;
+ box-shadow: 0 0 8px rgba( $gray-dark, 0.04 );
+
+ &::before {
+ content: '';
+ position: absolute;
+ border: 1px solid lighten( $gray, 20% );
+ border-right-width: 0 #{"/*rtl:ignore*/"};
+ border-bottom-width: 0;
+ background: $white;
+ display: block;
+ width: 12px;
+ height: 12px;
+ transform: rotate( 45deg );
+ }
+
+ &.is-top {
+ margin: 0 0 14px;
+ }
+
+ &.is-top::before {
+ bottom: -7px;
+ left: 30px;
+ border-width: 0 1px 1px 0 #{"/*rtl:ignore*/"};
+ }
+
+ &.is-bottom {
+ margin: 14px 0 0;
+ @include breakpoint( "<480px" ) {
+ margin: 15px -8px 0 -8px;
+ }
+ }
+
+ &.is-bottom::before {
+ top: -7px;
+ left: 30px;
+ }
+
+ &.is-active {
+ display: block;
+ }
+
+ &.buttons-hidden::before {
+ left: 208px;
+ }
+}
+
+.sharing-buttons-preview__panel-content {
+ padding: 16px;
+}
+
+.sharing-buttons-preview__panel-heading {
+ font-size: 20px;
+ font-weight: normal;
+ color: $gray-dark;
+}
+
+.sharing-buttons-preview__panel-instructions,
+.sharing-buttons-preview__panel-notice {
+ display: block;
+ color: $gray;
+ margin: 8px 0;
+}
+
+.sharing-buttons-preview__panel-instruction-text {
+ .touch &.is-notouch-context,
+ &.is-touch-reorder-context,
+ &.is-notouch-reorder-context,
+ &.is-touch-context {
+ display: none;
+ }
+
+ .touch &.is-touch-context {
+ display: inline;
+ }
+}
+
+.sharing-buttons-preview__panel.is-reordering .sharing-buttons-preview__panel-instruction-text {
+ &.is-notouch-reorder-context,
+ .touch &.is-touch-reorder-context {
+ display: inline;
+ }
+
+ &.is-notouch-context,
+ &.is-touch-reorder-context,
+ .touch &.is-touch-context,
+ .touch &.is-notouch-reorder-context {
+ display: none;
+ }
+}
+
+.sharing-buttons-preview__panel-content .sharing-buttons-preview-button {
+ margin-top: 8px;
+ cursor: pointer;
+ @include breakpoint( "<480px" ) {
+ margin: 18px 18px 0 0;
+ }
+
+ &.style-icon:hover {
+ border: none;
+ opacity: .6;
+ }
+
+ &.is-enabled {
+ opacity: .3;
+ }
+
+ &.is-enabled.style-icon {
+ opacity: .2;
+ }
+}
+
+.sharing-buttons-preview__panel.is-reordering .sharing-buttons-preview-button.is-enabled {
+ opacity: 1;
+}
+
+.sharing-buttons-preview__panel.is-reordering .sortable-list__item.is-draggable.is-active .sharing-buttons-preview-button {
+ margin: 0;
+}
+
+.sharing-buttons-preview__panel-actions {
+ padding: 10px 20px;
+ text-align: right;
+ border-top: 1px solid lighten( $gray, 30% );
+}
+
+.sharing-buttons-preview__panel-action {
+ margin-left: 10px;
+
+ &.is-left {
+ float: left;
+ margin-left: 0;
+ margin-right: 10px;
+ }
+}
+
+.sharing-buttons-label-editor__input {
+ max-width: 300px;
+}
+
+@include breakpoint( "<660px" ) {
+
+ .right-column {
+ box-sizing: border-box;
+ }
+
+ .sharing-content {
+ .new-account {
+ margin-left: 10px;
+ }
+ }
+}
diff --git a/assets/stylesheets/sections/_site-settings.scss b/assets/stylesheets/sections/_site-settings.scss
new file mode 100644
index 00000000000000..54352762d00bd1
--- /dev/null
+++ b/assets/stylesheets/sections/_site-settings.scss
@@ -0,0 +1,221 @@
+.site-settings {
+ fieldset {
+ clear: both;
+ margin-bottom: 20px;
+ }
+
+ label {
+ display: block;
+ }
+
+ input {
+ display: inline-block;
+ }
+
+ .form-setting-explanation {
+ margin: 5px 0;
+ }
+
+ .short-settings {
+ display: block;
+ min-width: 200px;
+ }
+
+ select + label {
+ margin-top: 24px; // Give labels some margin when they immediately follow a select
+ }
+
+ input[type="number"] {
+ padding: 0;
+ width: 50px;
+ text-align: center;
+ }
+
+ input[type="text"] {
+ -webkit-appearance: none;
+ }
+
+ legend,
+ label {
+ margin-bottom: 5px;
+ font-size: 14px;
+ font-weight: 600;
+ }
+
+ legend + label,
+ label + label,
+ li label,
+ input[type="checkbox"] + label,
+ input[type="radio"] + label,
+ label input[type="checkbox"],
+ label input[type="radio"] {
+ font-weight: normal;
+ }
+
+ .is-primary {
+ float: right;
+
+ &:first-child {
+ margin-bottom: 20px;
+ }
+ }
+
+ .empty-content {
+ .is-primary {
+ float: none;
+ }
+ }
+
+ p.settings-explanation {
+ display: block;
+ margin: 5px 0;
+ font-size: 13px;
+ font-style: italic;
+ font-weight: 400;
+ color: $gray;
+ }
+
+ p.settings-alert {
+ font-weight: 400;
+ color: $alert-red;
+ }
+
+ ul li,
+ ol li {
+ margin-bottom: 0;
+ }
+
+ .analytics-settings {
+ padding-top: 20px;
+ }
+
+ .blogaddress-settings {
+ @include breakpoint( ">660px" ) {
+ display: flex;
+ }
+
+ .button {
+ margin: 8px 0 0 0;
+ text-align: center;
+ width: 100%;
+
+ @include breakpoint( ">660px" ) {
+ margin: 0 0 0 16px;
+ width: 45%;
+ }
+ }
+ }
+}
+
+.writing-settings,
+.general-settings {
+ @include breakpoint( "<660px" ) {
+ select{
+ width: 100%;
+ }
+ }
+}
+
+
+.press-this {
+ /* press this isn't functional on a touch device */
+ .touch & {
+ display: none;
+ }
+
+ p.pressthis {
+ margin: 20px 0;
+
+ a,
+ a:hover,
+ a:focus,
+ a:active {
+ display: inline-block;
+ position: relative;
+ cursor: move;
+ padding: 10px;
+ font-style: normal;
+ line-height: 16px;
+ font-size: 14px;
+ text-decoration: none;
+ color: #333;
+ background: lighten( $gray, 30% );
+ border-radius: 3px;
+ border: 1px solid lighten( $gray, 20% );
+ }
+
+ a span {
+ display: inline-block;
+ margin: 0 10px 0 0;
+ font-family: $sans;
+
+ &:before {
+ color: #777;
+ font-family: Noticons;
+ }
+ }
+ }
+}
+
+.jp-relatedposts {
+ position: relative;
+ margin: 1em 0;
+ padding: 1em 1em .5em;
+ width: 100%;
+ background: $gray-light;
+ box-sizing: border-box;
+
+ &:after {
+ content: '';
+ display: block;
+ clear: both;
+ }
+
+ .jp-relatedposts-headline {
+ margin: 0 0 1em 0;
+ display: inline-block;
+ float: left;
+ font-size: 10px;
+ font-weight: 600;
+ font-family: inherit;
+ }
+
+ .jp-relatedposts-items {
+ clear: left;
+ }
+
+ .jp-relatedposts-post .jp-relatedposts-post-title a {
+ font-weight: normal;
+ text-decoration: none;
+ opacity: 1;
+ }
+
+ .jp-relatedposts-post {
+ float: left;
+ width: 33%;
+ box-sizing: border-box;
+ margin: 0 0 1em;
+ padding: 0 5px;
+
+ @include breakpoint( "<660px" ) {
+ width: 50%;
+ }
+
+ @include breakpoint( "<480px" ) {
+ width: 100%;
+ }
+ }
+
+ .jp-relatedposts-post img,
+ .jp-relatedposts-post span {
+ display: block;
+ max-width: 90%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .jp-relatedposts-post .jp-relatedposts-post-context {
+ opacity: .6;
+ }
+
+}
diff --git a/assets/stylesheets/sections/_sites.scss b/assets/stylesheets/sections/_sites.scss
new file mode 100644
index 00000000000000..3a7c690db94f4c
--- /dev/null
+++ b/assets/stylesheets/sections/_sites.scss
@@ -0,0 +1,276 @@
+/**
+ * Site Grid
+ */
+.sites-grid {
+ width: 100%;
+ float: left;
+ box-sizing: border-box;
+ padding-bottom: 80px;
+
+ @include breakpoint( "<660px" ) {
+ padding-left: .25em;
+ padding-right: .25em;
+ }
+}
+
+.sites__select-heading {
+ clear: both;
+ color: darken( $gray, 30% );
+ display: block;
+ font-family: $serif;
+ font-size: 32px;
+ font-weight: bold;
+ margin-bottom: 20px;
+}
+
+.site-card__add-new {
+ .noticon {
+ font-size: 64px;
+ height: 65px;
+ margin-top: 20px;
+ color: darken( $gray, 10% );
+ transition: all 0.1s ease-in-out;
+ }
+
+ a:hover .noticon {
+ color: $blue-medium;
+ }
+
+ .site-card__content {
+ min-height: 192px;
+ box-sizing: border-box;
+ background: none;
+ box-shadow: none;
+ border: 2px dashed lighten( $gray, 20% );
+ padding-top: 25px;
+
+ .site-card__title {
+ color: darken( $gray, 10% );
+ }
+ }
+
+ &:hover {
+ .site-card__content {
+ box-shadow: none;
+ border: 2px dashed $blue-medium;
+
+ .site-card__title {
+ color: $blue-medium;
+ }
+ }
+ }
+}
+
+// Start SiteCard component
+.site-card {
+ box-sizing: border-box;
+ cursor: pointer;
+ float: left;
+ margin-bottom: 20px;
+ margin-right: 2%;
+ width: 32%;
+
+ @include breakpoint( "<480px" ) {
+ width: 100%;
+ }
+
+ &:nth-child( 3n ) {
+ margin-right: 0;
+
+ @include breakpoint( "<480px" ) {
+ margin: 0;
+ }
+ }
+
+ &.has-update {
+ .site-options li {
+ display: none;
+ }
+ .site-options li:last-child {
+ background: $alert-yellow;
+ display: block;
+ font-size: 12px;
+ width: 100%;
+ letter-spacing: 0;
+ padding: 8px 0;
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ text-transform: none;
+
+ a::before {
+ @include noticon( '\f420', 16px );
+ margin-right: 8px;
+ opacity: 0.6;
+ vertical-align: text-bottom;
+ }
+ }
+ .update-available {
+ color: $white;
+ }
+ }
+
+ &.has-problem {
+ .site-options li {
+ display: none;
+ }
+ .site-options li:last-child {
+ background: $alert-red;
+ display: block;
+ font-size: 12px;
+ width: 100%;
+ letter-spacing: 0;
+ padding: 8px 0;
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ text-transform: none;
+
+ a::before {
+ @include noticon( '\f414', 16px );
+ margin-right: 8px;
+ opacity: 0.6;
+ vertical-align: text-bottom;
+ }
+
+ a {
+ color: $white;
+ }
+ }
+ }
+
+ &:hover {
+ .site-content {
+ box-shadow: 0 1px 4px rgba( $gray-dark, 0.2 );
+ @include breakpoint( "<660px" ) {
+ box-shadow: none;
+ }
+ }
+
+ .site-title, .site-description {
+ color: $blue-medium;
+ @include breakpoint( "<660px" ) {
+ color: $gray-dark;
+ }
+ }
+ .site-description {
+ color: $blue-medium;
+ @include breakpoint( "<660px" ) {
+ color: $gray;
+ }
+ }
+ }
+}
+
+.site-card__content {
+ background-color: $white;
+ box-shadow: 0 1px 2px rgba(0,0,0,0.075);
+ margin-bottom: 0;
+ min-height: 144px;
+ padding: 40px 20px 10px;
+ position: relative;
+ text-align: center;
+
+ // Placeholder boxes for loading sites
+ &.is-empty {
+ min-height: 144px;
+
+ .site-card__title,
+ .site-card__description,
+ .site-icon {
+ animation: pulse-light 0.8s ease-in-out infinite;
+ }
+
+ .site-card__title {
+ background: lighten( $gray, 20% );
+ height: 16px;
+ margin: 4px auto 10px;
+ width: 50%;
+ }
+
+ .site-card__description {
+ background: lighten( $gray, 20% );
+ height: 10px;
+ margin: 4px auto;
+ width: 70%;
+ }
+ }
+
+ // Banner image
+ .site-card__header {
+ background-color: $gray-light;
+ background-size: cover;
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 80px;
+ width: 100%;
+ overflow: hidden;
+
+ img {
+ max-width: 150%;
+ max-height: none;
+ }
+ }
+
+ // Site icon / blavatar
+ .site-icon {
+ display: inline-block;
+ border: 2px solid white;
+ margin-bottom: .4em;
+ box-shadow: 0 1px 1px rgba($gray-dark, 0.2);
+ }
+
+ .site-card__title {
+ white-space: nowrap;
+ font-size: 16px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ /**
+ * Show lock icon on private sites
+ * todo: use @extend noticon
+ */
+ .is-private &::before {
+ font: normal 16px/1 Noticons;
+ display: inline-block;
+ color: $gray;
+ content: "\f470";
+ vertical-align: bottom;
+ height: 20px;
+ line-height: 1.1;
+ margin-right: 2px;
+ }
+ }
+ .site-card__description {
+ font-size: 13px;
+ font-weight: 200;
+ color: $gray;
+ font-style: italic;
+ line-height: 1.4em;
+ margin-bottom: .4em;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ .site-card__options {
+ margin: 0;
+
+ li {
+ font-size: 11px;
+ list-style: none;
+ display: inline-block;
+ color: $gray;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+
+ &:nth-child(2):before {
+ content:'\00a0\2022\00a0';
+ }
+ }
+ }
+}
+// End SiteCard component
diff --git a/assets/stylesheets/sections/_stats.scss b/assets/stylesheets/sections/_stats.scss
new file mode 100644
index 00000000000000..4382cdb3fca155
--- /dev/null
+++ b/assets/stylesheets/sections/_stats.scss
@@ -0,0 +1,1742 @@
+/*
+ Priest Vito Cornelius: You're a monster, Zorg.
+ Zorg: I know.
+*/
+
+// Site sections
+.module-list {
+ @include clear-fix;
+}
+
+.stats-nonperiodic {
+ &.has-no-recent {
+ color: $gray-dark;
+ font-weight: 300;
+
+ p {
+ @include breakpoint( "<660px" ) {
+ margin-left: 24px;
+ margin-right: 24px;
+ }
+
+ @include breakpoint( "<480px" ) {
+ text-align: center;
+ }
+ }
+ }
+}
+
+// Section title
+
+.stats-section-title {
+ @extend %heading;
+
+ @include breakpoint( "<660px" ) {
+ margin-left: 24px;
+ margin-right: 24px;
+ }
+
+ @include breakpoint( "<480px" ) {
+ text-align: center;
+ }
+}
+
+// Module container
+
+@include breakpoint( ">960px" ) {
+ .module-column {
+ float: left;
+ width: 48.8%;
+ width: calc(50% - 0.5em);
+
+ &:last-child {
+ float: right;
+ }
+ }
+}
+
+.old-stats-link .button .gridicon {
+ margin-bottom: -2px;
+ margin-left: 4px;
+}
+
+// Module Placeholder
+
+@keyframes delay-fade-in {
+ 0%, 66% {
+ opacity: 0;
+ }
+
+ 100% {
+ opacity: 1;
+ }
+}
+
+.stats-module.is-loading {
+
+ // Hide elements
+ .module-header .site-icon,
+ .module-header .toggle-info,
+ .chart,
+ .module-content-table,
+ .stats-popular__item {
+ display: none;
+ }
+
+ .module-header-link,
+ .module-header-title {
+ max-width: 120px;
+ white-space: nowrap;
+ overflow: hidden;
+
+ .gridicon {
+ display: none;
+ }
+ }
+
+ // Make text invisible
+ .module-content-list-header .label,
+ .module-content-list-header .value,
+ .module-header-title span,
+ ul.module-content-tabs li:hover .label,
+ ul.module-content-tabs li.module-tab.is-selected:hover .label {
+ color: $transparent;
+ }
+}
+
+.stats-module.is-hidden {
+ display: none;
+}
+
+// 1: ### Showing/hiding should not be handled by CSS
+.module-placeholder {
+ display: none;
+ padding: 0 24px;
+ min-height: 140px;
+
+ // 1
+ &.show,
+ .stats-module.is-loading & {
+ display: block;
+ }
+
+ // Block placeholder, primarily for SVGs and similar
+ &.is-block {
+ background: $gray-light;
+ }
+
+ // Spinner placeholder, to be used in Summary views, where we already have some data
+ &.is-spinner,
+ &.is-void {
+ background: url("images/loading.gif") no-repeat 50% 50%;
+ background-size: 32px 32px;
+ opacity: 1;
+ }
+
+ // 'Void' placeholder, don't stare too long into the void or the void will stare back into you
+ &.is-void {
+ -webkit-animation: delay-fade-in 6s 1; // Animating in this silly way to have the graphic actually be visible once the animation is completed
+ }
+
+ &.is-chart {
+ height: 244px;
+ }
+}
+
+// Module Expand
+// (link that shows only if there are more results than we can show in the overview)
+
+.module-expand {
+ line-height: 40px;
+
+ @include breakpoint( "<480px" ) {
+ line-height: 48px;
+ }
+
+ .stats-module.is-expanded & {
+ display: block;
+ }
+
+ &,
+ .stats-module.is-loading &,
+ .stats-module.has-no-data & {
+ display: none;
+ }
+
+ a {
+ @extend %mobile-link-element;
+ position: relative;
+ display: block;
+ padding: 0 24px;
+ border-top: 1px solid $gray-light;
+ font-size: 14px;
+
+ // Hover state
+ @include breakpoint( ">480px" ) {
+ &:hover {
+ background: $gray-light;
+ border-top-color: $white;
+ }
+ }
+
+ // Focus state
+ &:focus {
+ background: $gray-light;
+ border-top-color: $white;
+ }
+
+ @include breakpoint( ">960px" ) {
+ .module-list & {
+ font-size: 12px;
+ }
+ }
+ }
+
+ .right {
+ color: $gray;
+ position: absolute;
+ right: 24px;
+ top: 0;
+ }
+}
+
+// Module Header
+
+.module-header {
+ position: relative;
+ background: $white;
+ line-height: 40px;
+ height: 40px;
+ padding-left: 24px;
+
+ .stats-module.is-loading & {
+ cursor: default;
+ height: 40px;
+ }
+
+ .stats-module.summary & {
+ cursor: default;
+ }
+}
+
+// Module Header Title
+// 1: If really long titles wrap, hide the excess
+
+.module-header-title {
+ @extend %mobile-interface-element;
+ @extend %placeholder;
+
+ width: auto;
+ font-weight: bold;
+ font-weight: 600;
+ height: 40px; // 1
+ overflow: hidden; // 1
+ color: $gray-dark;
+}
+
+// Module Header Actions
+// 1: To align optically to right line and create bigger touch target
+// 2: So the focus outline isn't covered by the fading pseudo-element
+
+ul.module-header-actions {
+ position: absolute;
+ list-style: none;
+ top: 0;
+ right: 0;
+ height: 40px;
+ margin: 0;
+ background: $white;
+
+ // Fade really long module titles
+ &::before {
+ @include stats-fade-text($white);
+ }
+
+ .module-header-action {
+ display: inline-block;
+
+ &:last-child .module-header-action-link {
+ &,
+ &::after {
+ padding-right: 12px; // 1
+ }
+ }
+
+ .summary &.toggle-services {
+ display: none;
+ }
+ }
+
+ .gridicon {
+ @extend %placeholder-icon;
+ vertical-align: middle;
+ }
+
+ .module-header-action-link {
+ @extend %mobile-interface-element;
+ height: 40px;
+ min-width: 40px;
+ line-height: 40px;
+ display: inline-block;
+ text-align: center;
+ position: relative; // 2
+ z-index: 2; // 2
+ color: $gray;
+
+ .stats-module.is-loading & {
+ cursor: default;
+ }
+ }
+
+ // Hover state
+ @include breakpoint( ">480px" ) {
+ .stats-module:hover & .module-header-action-link {
+ color: $blue-wordpress;
+ }
+
+ .module-header & .module-header-action-link:hover {
+ color: $link-highlight;
+ }
+ }
+
+ // Focus state
+ .module-header & .module-header-action-link:focus {
+ color: $link-highlight;
+ }
+
+ // Info button
+ .toggle-info {
+
+ &,
+ .stats-module.is-loading & {
+ display: none;
+ }
+
+ .stats-module.is-expanded & {
+ display: inline-block;
+ }
+ }
+
+ // Toggle
+ .toggle-services .gridicon {
+ transition: .2s transform ease-out;
+ transform: translate3d(0,0,0);
+ }
+
+ .stats-module.is-expanded & .toggle-services .gridicon {
+ transform: rotate(180deg);
+ }
+}
+
+// Module Content Text
+
+.module-content-text {
+ width: 100%;
+ min-height: 1em;
+ font-size: 13px;
+ color: $gray-dark;
+
+ a:focus {
+ outline: thin dotted;
+ }
+
+ @include breakpoint( ">960px" ) {
+ .module-list & {
+ font-size: 12px;
+ }
+ }
+
+ // Don't show text when loading, even if it's info text and the module is empty
+ .stats-module.has-no-data &,
+ .stats-module.is-loading & {
+ display: none;
+ }
+
+ // Hidden info box (help)
+ // 1: 'inline-block' to get around margin collapse issue, cleaner than 1px top padding
+ // 2: Temporary for the welcome guide, don't show info boxes while modules are loading even if the welcome message is still open
+ .error & {
+ display: block;
+ }
+
+ // Hidden info box
+ &-info {
+ display: none;
+ position: relative;
+ background: $gray-light;
+ color: $gray-dark;
+ box-shadow: inset 0 1px 0 $gray-light;
+
+ .stats-module.is-showing-info & {
+ display: inline-block; // 1
+ }
+
+ .stats-module.is-loading.is-showing-info & {
+ display: none; // 2
+ }
+ }
+
+ // Error messages
+ &.is-error {
+ color: $gray;
+
+ .stats-module.is-showing-error & {
+ display: inline-block;
+ }
+ }
+
+ ul,
+ ol,
+ p {
+ margin: 1em 24px;
+ }
+
+ // Representation of what the published status looks like within a list of posts and pages
+ .legend.published {
+ padding-left: 12px;
+ border-left: 4px solid $orange-jazzy;
+ }
+
+ .legend.achievement {
+ color: $alert-yellow;
+ }
+
+ // Hide legends if there is no data
+ .stats-module.has-no-data & .legend {
+ display: none;
+ }
+
+ // List of associated FAQ items, support pages, tips and tricks, etc
+ .documentation {
+ list-style: none;
+
+ li {
+ line-height: 2em;
+ font-size: 14px;
+
+ @include breakpoint( ">960px" ) {
+ .module-list & {
+ font-size: 12px;
+ }
+ }
+
+ @include breakpoint( "<480px" ) {
+ border-bottom: 1px solid $gray-light;
+
+ &:last-child {
+ border: none;
+ margin-bottom: -12px;
+ }
+ }
+ }
+
+ a {
+ @extend %mobile-link-element;
+ display: block;
+ position: relative;
+ padding: 6px 0 6px 30px;
+
+ @include breakpoint( "<480px" ) {
+ padding-top: 12px;
+ padding-bottom: 12px;
+ }
+ }
+
+ .gridicon {
+ position: absolute;
+ left: 0;
+ vertical-align: middle;
+ }
+ }
+}
+
+// Filter Select
+.select-dropdown__wrapper {
+ margin: 1em 20px;
+
+ .select-dropdown__container {
+ position: relative;
+ }
+
+ .stats-module.is-loading &,
+ .stats-module.has-no-data & {
+ display: none;
+ }
+}
+
+// Tab Content (for comments module)
+
+.tab-content {
+ display: none;
+}
+
+.tab-email-followers .tab-content.email-followers,
+.tab-wpcom-followers .tab-content.wpcom-followers,
+.tab-top-authors .tab-content.top-authors,
+.tab-top-content .tab-content.top-content {
+ display: block;
+}
+
+// Module Tabs
+// (currently only used for the bar chart module at the top)
+
+ul.module-tabs {
+ @include clear-fix;
+ border-top: 1px solid $gray-light;
+ list-style: none;
+ background: $white;
+ margin: 0;
+
+ .module-tab {
+ margin: 0;
+ font-size: 16px;
+ clear: none;
+ border-top: 1px solid $gray-light;
+ box-sizing: border-box;
+
+ &:first-child {
+ border-top: none;
+ }
+
+ @include breakpoint( ">480px" ) {
+ border-top: none;
+ border-left: 1px solid $gray-light;
+ float: left;
+ width: 25%;
+ text-align: center;
+
+ &.is-post-summary {
+ width: 33%;
+ }
+
+ &:first-child {
+ border-left: none;
+ }
+ }
+
+ @include breakpoint( "<480px" ) {
+ width: auto;
+ float: none;
+ text-align: left;
+ }
+
+ a,
+ .no-link {
+ @extend %mobile-interface-element;
+ @include clear-fix;
+ padding: 5px 0 10px;
+ display: block;
+ background: $white;
+ color: $gray-dark;
+
+ @include breakpoint( "<480px" ) {
+ line-height: 24px;
+ padding-top: 10px;
+ -webkit-touch-callout: none;
+ }
+ }
+
+ .label {
+ font-size: 11px;
+ letter-spacing: 0.1em;
+ line-height: inherit;
+ text-transform: uppercase;
+
+ @include breakpoint( "<480px" ) {
+ font-size: 14px;
+ line-height: 24px;
+ float: left;
+ }
+ }
+
+ .gridicon {
+ vertical-align: middle;
+ margin-right: 4px;
+ margin-top: -2px;
+
+ @include breakpoint( "<480px" ) {
+ width: 24px;
+ height: 24px;
+ margin-left: 24px;
+ margin-right: 8px;
+ float: left;
+ }
+ }
+
+ .value {
+ clear: both;
+ display: block;
+ line-height: 24px;
+ color: $blue-wordpress;
+ transition: font-size .3s 0 ease-out;
+
+ @include breakpoint( "<480px" ) {
+ clear: none;
+ float: right;
+ margin-right: 24px;
+ }
+ }
+
+ @include breakpoint( ">480px" ) {
+ // Hover state
+ a:hover,
+ a:hover .value,
+ &.is-low a:hover,
+ &.is-low a:hover .value {
+ color: $link-highlight;
+ }
+
+ a:hover {
+ background: rgba(255,255,255,.5);
+ }
+ }
+
+ // Focus state
+ a:focus,
+ a:focus .value,
+ .stats-module &.is-selected a:focus,
+ .stats-module &.is-selected a:focus .value,
+ &.is-low a:focus,
+ &.is-low a:focus .value {
+ color: $link-highlight;
+ }
+
+ a:focus {
+ background: rgba(255,255,255,.5);
+ }
+
+ // Selected state
+ .stats-module &.is-selected a {
+ background: $white;
+ border-top: 1px solid $white;
+ margin-top: -1px;
+ cursor: default;
+ }
+
+ &.is-selected a,
+ &.is-selected.is-low a {
+ color: $gray-dark;
+ }
+
+ &.is-selected a .value,
+ &.is-selected.is-low a .value,
+ &.is-selected a:hover .value {
+ color: $orange-jazzy;
+ }
+
+ // Low state ('disabled')
+ &.is-low a .value {
+ color: $gray;
+ }
+
+ // Individual tab loading state
+ &.is-loading a,
+ &.is-loading a:hover {
+ cursor: default;
+ }
+
+ .no-link .value {
+ color: $gray-dark;
+
+ &.is-low {
+ color: $gray;
+ }
+ }
+
+ &.is-loading a .value,
+ &.is-loading a:hover .value,
+ &.is-loading .no-link .value {
+ animation: loading-fade 1.6s ease-in-out infinite;
+ cursor: default;
+ font-size: 110%;
+ color: $gray;
+ }
+
+ &.is-loading.is-selected a .value,
+ &.is-loading.is-selected a:hover .value {
+ color: $gray;
+ }
+ }
+
+ // If there's only one tab (used on the Post/Video Details page)
+ li:only-child {
+ width: auto;
+ float: none;
+ text-align: left;
+
+ a {
+ line-height: 24px;
+ padding-top: 10px;
+ }
+
+ .label {
+ font-size: 14px;
+ line-height: 24px;
+ float: left;
+ }
+
+ .gridicon {
+ width: 24px;
+ height: 24px;
+ margin: 0 8px 0 24px;
+ float: left;
+ }
+
+ .value {
+ clear: none;
+ float: right;
+ margin-right: 24px;
+ }
+ }
+
+ &.is-enabled {
+ background: $gray-light;
+
+ &,
+ li {
+ border-color: $gray-light;
+ }
+
+ a {
+ background: $gray-light;
+ }
+ }
+}
+
+// Module Content
+
+.module-content {
+ display: none;
+ position: relative;
+
+ .stats-module.is-expanded & {
+ display: block;
+ }
+}
+
+// Module Content List
+
+.module-content-list {
+ padding: 0;
+ margin: 0 0 .5em;
+ list-style-type: none;
+
+ .stats-module.is-loading &,
+ .stats-module.has-no-data &,
+ .stats-module.is-showing-error & {
+ display: none;
+ }
+
+ .stats-module.is-loading &-legend {
+ display: block;
+ }
+}
+
+// Module Content List Item
+
+.module-content-list-item {
+ // Smaller font-size for narrower, two-column modules
+ font-size: 14px;
+ line-height: 40px;
+
+ // List item height shorter on 2-column modules
+ @include breakpoint( ">960px" ) {
+ .module-list & {
+ font-size: 12px;
+ line-height: 28px;
+ }
+ }
+
+ border-top: 1px solid $transparent;
+
+ // Increase touch targets on mobile
+ @include breakpoint( "<480px" ) {
+ line-height: 48px;
+ border-top: 1px solid $gray-light;
+
+ &:first-child {
+ border-top-color: $transparent;
+ }
+
+ // Darken color for sublists
+ .module-content-list-sublist & {
+ border-top-color: $gray-light;
+ }
+ }
+
+ &.disabled {
+ .module-content-list-item-label,
+ .module-content-list-item-value {
+ opacity: .15;
+ transition: opacity .3s ease .15s;
+ }
+
+ .module-content-list-item-right {
+ &:before {
+ display: none;
+ }
+ }
+
+ .module-content-list-item-actions {
+ cursor: pointer;
+ opacity: 1;
+ transition: opacity .3s ease .15s;
+ position: relative;
+ right: -20px;
+ }
+ }
+}
+
+// Module Content List Item Wrapper
+// (wrapper element, what's actually hovered for each list item)
+
+.module-content-list-item-wrapper {
+ @extend %mobile-interface-element;
+ background: $white; // Default non-active color
+ display: block;
+ line-height: inherit;
+ clear: both; // To make sure no rows overlap no matter the circumstances
+ padding: 0 24px;
+
+ span {
+ font-size: 14px;
+ // Always let child elements inherit line heights
+ line-height: inherit;
+ }
+
+ @include breakpoint( ">960px" ) {
+
+ .module-list & {
+ line-height: 28px;
+
+ span {
+ font-size: 12px;
+ // Always let child elements inherit line heights
+ line-height: inherit;
+ }
+ }
+ }
+
+ // Post was published within the selected period
+ // 1: Move so far out left that only half the icon is showing to reduce footprint
+ .module-content-list-item.published & {
+ box-shadow: inset 4px 0 0 $orange-jazzy;
+ }
+
+}
+
+// Module Content List Item Hover
+
+@include breakpoint( ">480px" ) {
+ .module-content-list-item-link .module-content-list-item-wrapper:hover {
+
+ &,
+ .module-content-list-item-right {
+ background-color: $gray-light;
+ }
+
+ .module-content-list-item-right::before {
+ background-image: linear-gradient(to right, $transparent 0%, $gray-light 90%);
+ }
+
+ // Display hidden actions
+ .module-content-list-item-action-hidden {
+ display: inline-block;
+ }
+ }
+}
+
+// Module Content List Item Focus
+// Active on non-links as well so you can easily go to them and digest information
+
+.module-content-list-item .module-content-list-item-wrapper:focus {
+
+ &,
+ .module-content-list-item-right {
+ background-color: $gray-light;
+ }
+
+ .module-content-list-item-right::before {
+ background-image: linear-gradient(to right, $transparent 0%, $gray-light 90%);
+ }
+
+ // Display hidden actions
+ .module-content-list-item-action-hidden {
+ display: inline-block;
+ }
+}
+
+// Highlight toggle icon if item has a sublist
+
+@include breakpoint( ">480px" ) {
+ .module-content-list > .module-content-list-item-toggle > .module-content-list-item-wrapper:hover .module-content-list-item-label::before {
+ color: $link-highlight;
+ }
+}
+
+.module-content-list > .module-content-list-item-toggle > .module-content-list-item-wrapper:focus .module-content-list-item-label::before {
+ color: $link-highlight;
+}
+
+// Module Content List Item Label
+// 1: To create the illusion that text is fading out (break up even long chunks of text)
+// 2: ### always has to be the line-height -- could we sync up with a variable?
+
+.module-content-list-item-label {
+ display: block;
+ overflow: hidden;
+ word-break: break-all; // 1
+ height: 40px; // 2
+
+ // List item height shorter on 2-column modules
+ @include breakpoint( ">960px" ) {
+ .module-list & {
+ height: 28px;
+ }
+ }
+
+ @include breakpoint( "<480px" ) {
+ height: 48px; // ### see ^
+ }
+
+ // Icons
+ .icon,
+ .gridicon {
+ margin-right: 8px;
+ }
+
+ .icon {
+ $icon-size: 24px;
+
+ position: relative;
+ display: inline-block;
+ width: $icon-size;
+ height: $icon-size;
+ overflow: hidden;
+ vertical-align: middle;
+ min-width: 24px;
+ line-height: inherit;
+
+ img {
+ display: block;
+ background: $white;
+ position: relative;
+ }
+
+ // Hide for user avatars
+ .followers &,
+ .top-authors &,
+ .authorviews & {
+ background: none;
+
+ &::before {
+ content: none;
+ }
+ }
+ }
+
+ .gridicon {
+ vertical-align: middle;
+ }
+
+ // Icons smaller on 2col
+ @include breakpoint( ">960px" ) {
+ .module-list & .icon {
+ font-size: 20px;
+ line-height: 1.3;
+ }
+
+ .module-list & .gridicon {
+ width: 18px;
+ height: 18px;
+ }
+ }
+
+ .avatar {
+ width: 24px;
+ height: 24px;
+ }
+
+ .avatar-user {
+ border-radius: 12px
+ }
+
+ .user-selectable & {
+ -webkit-user-select: text;
+ -khtml-user-select: text;
+ -moz-user-select: text;
+ -ms-user-select: text;
+ user-select: text;
+ }
+
+ // Label sections: For when multiple data points are displayed in one expandable list item
+ .module-content-list-item-label-section {
+ margin-right: 11px;
+ padding-right: 12px;
+ border-right: 1px solid $gray-light;
+
+ &:last-child {
+ margin: 0;
+ padding: 0;
+ border: none;
+ }
+ }
+}
+
+// Module Content List Item Right column
+
+.module-content-list-item-right {
+ position: relative;
+ float: right;
+ background: $white;
+ margin-left: -48px; // ### keep? experimental: to force labels to go longer than they normally would to make sure the fade out shows
+
+
+ @include breakpoint( ">960px" ) {
+ .module-list & {
+ height: 28px;
+ }
+ }
+
+ @include breakpoint( "<480px" ) {
+ height: 48px;
+ }
+
+ // Fade out value if long
+ &::before {
+ @include stats-fade-text($white);
+ }
+}
+
+// Module Content List Item Value
+// 1: Makes secondary actions aligned up to values of '99,999' or a string like '99 hours'
+.module-content-list-item-value {
+ display: inline-block;
+ text-align: right;
+ min-width: 44px; // 1
+
+ .followers & {
+ min-width: 60px; // 1
+ }
+}
+
+// Module Content List Item Actions (in right column)
+// Actions list
+ul.module-content-list-item-actions {
+ display: inline-block;
+ margin: 0 .5em 0 0;
+
+ // ### guess we need to abstract this to a mixin since its repeated now
+ // ### this should be fixed but refraining since we're going to use
+ // the popover and select UI patterns more closely in the future
+ &.collapsed {
+ @include dropdown-menu;
+
+ background: $gray-light;
+ display: none;
+ z-index: 3;
+ margin: 0;
+ top: 30px;
+ right: auto;
+ left: -172px; // module-content-list-item-right is relative, so this is min-width 220px - action width 48px = 172
+
+ .module-content-list-item-right.is-expanded & {
+ display: inline-block;
+ }
+
+ &::after {
+ border-bottom-color: $gray-light;
+ right: 18px; // Logically this should be 24px but thanks to the borders (?) 18px is actually centered
+ left: auto;
+ }
+
+ .module-content-list-item-action-wrapper,
+ ul.module-content-list-item-action-submenu {
+ display: block;
+ text-align: left;
+ }
+
+ // If displayed in a sublist or otherwise expanded item, swap background color
+ .module-content-list-item-toggle.is-expanded & {
+ background-color: $white;
+
+ &::after {
+ border-bottom-color: $white;
+ }
+ }
+ }
+
+ @include breakpoint( "<480px" ) {
+ @include dropdown-menu;
+
+ background: $gray-light;
+ display: none;
+ z-index: 2;
+ margin: 0;
+ top: 46px;
+ right: auto;
+ left: -172px; // module-content-list-item-right is relative, so this is min-width 220px - action width 48px = 172
+
+ .module-content-list-item-right.is-expanded & {
+ display: inline-block;
+ }
+
+ &::after {
+ border-bottom-color: $gray-light;
+ right: 18px; // Logically this should be 24px but thanks to the borders (?) 18px is actually centered
+ left: auto;
+ }
+
+ .module-content-list-item-action-wrapper,
+ ul.module-content-list-item-action-submenu {
+ display: block;
+ text-align: left;
+ }
+
+ // If displayed in a sublist or otherwise expanded item, swap background color
+ .module-content-list-item-toggle.is-expanded & {
+ background-color: $white;
+
+ &::after {
+ border-bottom-color: $white;
+ }
+ }
+ }
+}
+
+// Actions toggle
+// (a toggle that's only shown on smaller screen sizes)
+// 1: ### Showing/hiding should not be handled by CSS
+
+.module-content-list-item-actions-toggle {
+ display: none;
+ min-width: 24px;
+ padding: 0 12px;
+ height: 40px;
+ line-height: inherit;
+
+ @include breakpoint( ">960px" ) {
+ .module-list & {
+ height: 28px;
+ }
+ }
+
+ .gridicon {
+ vertical-align: middle;
+ }
+
+ @include breakpoint( "<480px" ) {
+ display: inline-block;
+ height: 48px;
+ }
+
+ // 1
+ &.show {
+ display: inline-block;
+ height: 30px;
+ }
+}
+
+// Actions Menu
+// (Used for links that should be hidden even on desktop)
+ul.module-content-list-item-action-submenu {
+ display: inline-block;
+ list-style: none;
+ margin: 0;
+
+ @include breakpoint( ">480px" ) {
+
+ @include dropdown-menu;
+
+ display: none;
+ z-index: 2;
+ margin: 0;
+ top: 32px;
+ right: -20px;
+
+ .module-content-list-item-action.hidden-action.is-expanded & {
+ display: inline-block;
+ }
+
+ &::after {
+ right: 24px;
+ left: auto;
+ }
+
+ .module-content-list-item-action-wrapper {
+ display: block;
+ text-align: left;
+ }
+ }
+}
+
+// Action
+.module-content-list-item-action {
+ display: inline-block;
+ margin: 0 1em 0 0;
+
+ // So that 'View' label is moved more to the right since icon has been dropped
+ @include breakpoint( ">960px" ) {
+ .module-list & {
+ margin: 0;
+ }
+ }
+
+ @include breakpoint( "<480px" ) {
+ margin-right: 0;
+ }
+
+ // Action wrapper, what's actually selected
+ .module-content-list-item-action-wrapper {
+ @extend %mobile-interface-element;
+ display: inline-block;
+ text-align: center;
+ margin: 0;
+ line-height: inherit;
+
+ @include breakpoint( "<480px" ) {
+ min-width: 24px;
+ padding: 0 12px;
+
+ &.toggle {
+ display: none;
+ }
+ }
+
+ .module-content-list-item-action-label.unfollow {
+ display: none;
+ }
+
+ // Hide 'View' label next to icon on 2-column modules
+ @include breakpoint( ">960px" ) {
+ .module-list & span.module-content-list-item-action-label-view {
+ display: none;
+ }
+ }
+ }
+
+ // Color follow action when already following
+ .module-content-list-item-action-wrapper.following .module-content-list-item-action-label {
+ color: $alert-green;
+ }
+
+ // Display hidden label and change icon for Unfollow action
+ .module-content-list-item-action-wrapper.following:focus,
+ .module-content-list-item-action-wrapper.following:hover {
+
+ .module-content-list-item-action-label {
+ display: none;
+ color: $link-highlight;
+ }
+
+ .module-content-list-item-action-label.unfollow {
+ display: inline-block;
+ }
+ }
+
+ // Icon settings
+ // 1: Optically align a bit better
+ .gridicon {
+ vertical-align: middle;
+ margin-right: 4px;
+ margin-top: -2px; // 1
+ }
+
+ // Color spam action red
+ .module-content-list-item-action-wrapper.spam {
+ color: $alert-red;
+
+ // Hover state
+ @include breakpoint( ">480px" ) {
+ &:hover {
+ color: tint($alert-red, 30%);
+ }
+ }
+
+ // Focus state
+ &:focus {
+ color: tint($alert-red, 30%);
+ }
+ }
+}
+
+
+// Module Content List style: Legend
+// (a legend for the data displayed)
+
+ul.module-content-list-legend {
+ margin-top: .5em;
+ margin-bottom: 0;
+}
+
+.module-content-list-legend .module-content-list-item .module-content-list-item-value,
+.module-content-list-legend .module-content-list-item .module-content-list-item-label {
+ @extend %placeholder;
+
+ color: $gray;
+ font-weight: bold;
+
+ // Limit width when loading for placeholders to take less visual space
+ .stats-module.is-loading & {
+ max-width: 60px;
+ }
+}
+
+// Display full action labels in header to use them as legends for the list's actions
+.module-content-list-legend .module-content-list-item .module-content-list-item-action .module-content-list-item-action-label {
+ @include breakpoint( "<480px" ) {
+ display: inline;
+ }
+}
+
+// Module Content List Item style: Disabled
+// (there's absolutely no data or bad error)
+// ### Do :empty for value to input N/A? Or placeholder content in general?
+
+.module-content-list > .module-content-list-item-disabled {
+ cursor: default;
+
+ .module-content-list-item-value,
+ .module-content-list-item-label {
+ color: $gray;
+ }
+}
+
+
+// Module Content List Item style: Large
+// (a larger display of a list item, currently only used for the Authors module)
+
+.module-content-list > .module-content-list-item-large {
+
+ > .module-content-list-item-wrapper {
+ line-height: 48px;
+ @include breakpoint( ">960px" ) {
+ .module-list & {
+ line-height: 28px;
+ }
+ }
+ }
+
+ > .module-content-list-item-wrapper .module-content-list-item-label {
+ height: 48px;
+ @include breakpoint( ">960px" ) {
+ .module-list & {
+ height: 28px;
+ }
+ }
+ }
+
+ > .module-content-list-item-wrapper .module-content-list-item-label .avatar {
+ font-size: 32px;
+ margin-right: 10px;
+ }
+
+ > .module-content-list-item-wrapper .module-content-list-item-label .icon {
+ font-size: 32px;
+ line-height: 32px;
+ width: 32px;
+ height: 32px;
+ @include breakpoint( ">960px" ) {
+ .module-list & {
+ width: 24px;
+ height: 24px;
+ }
+ }
+ }
+
+ @include breakpoint( ">960px" ) {
+ .module-list & > .module-content-list-item-wrapper .module-content-list-item-label .icon {
+ margin-top: -2px; // Really couldn't figure out a better way to correctly position the avatar
+ }
+ }
+
+ > .module-content-list-item-wrapper .module-content-list-item-label .avatar {
+ width: 32px;
+ height: 32px;
+ @include breakpoint( ">960px" ) {
+ .module-list & {
+ width: 24px;
+ height: 24px;
+ }
+ }
+ }
+
+ > .module-content-list-item-wrapper .module-content-list-item-label .avatar-user {
+ border-radius: 16px;
+ }
+}
+
+// Module Content List Item style: Link
+// (this item has a main action or links somewhere)
+
+.module-content-list > .module-content-list-item-link {
+ cursor: pointer;
+
+ &.disabled {
+ cursor: default;
+ }
+
+ // Change colors to highlight label when row is highlighted
+ .module-content-list-item-label {
+ color: $blue-wordpress;
+ }
+
+ // Highlight main action (usually what's indicated in the label)
+
+ @include breakpoint( ">480px" ) {
+ .module-content-list-item-wrapper:hover .module-content-list-item-label {
+ color: $link-highlight;
+ }
+ }
+
+ .module-content-list-item-wrapper:focus .module-content-list-item-label {
+ color: $link-highlight;
+ }
+}
+
+// Module Content List Item style: Toggle
+// (this item's main action is to show an enclosed sublist or something else)
+
+.module-content-list > .module-content-list-item-toggle {
+ position: relative;
+
+ // Toggle icon
+ > .module-content-list-item-wrapper .module-content-list-item-label .gridicons-chevron-down {
+ vertical-align: middle;
+ transition: .2s transform ease-out;
+ transform: translate3d(0,0,0);
+ }
+}
+
+// Active (sublist is showing)
+.module-content-list-item-toggle.is-expanded {
+ border-top-color: $gray-light;
+
+ > .module-content-list {
+ display: block;
+ }
+
+ // Lock in hover states to show that the list item is now selected (active)
+ &,
+ > .module-content-list-item-wrapper,
+ > .module-content-list-item-wrapper .module-content-list-item-right {
+ background: $gray-light;
+ }
+
+ > .module-content-list-item-wrapper .module-content-list-item-value {
+ color: $gray;
+ }
+
+ > .module-content-list-item-wrapper .module-content-list-item-right::before {
+ background-image: linear-gradient(to right, $transparent 0%, $gray-light 90%);
+ }
+
+ // Rotate toggle icon
+ > .module-content-list-item-wrapper .module-content-list-item-label .gridicons-chevron-down {
+ transform: rotate(180deg);
+ }
+
+ // Hide the top border of an active sub-list
+ > .module-content-list-item {
+ border-top-color: $transparent;
+ }
+
+ // Hover changes
+ @include breakpoint( ">480px" ) {
+ > .module-content-list-item-wrapper:hover {
+
+ // Change background and gradient color
+ &,
+ .module-content-list-item-right {
+ background-color: $white;
+ }
+
+ span.module-content-list-item-right::before {
+ background-image: linear-gradient(to right, $transparent 0%, $white 90%);
+ }
+ }
+ }
+}
+
+// Module Content List: Sublist
+// (modified content list fit for sublists to be displayed inside other lists)
+
+.module-content-list-sublist {
+ display: none;
+ padding: 4px 0;
+
+ // Add more padding for third level nested lists
+ .module-content-list-sublist .module-content-list-item-wrapper {
+ padding-left: 56px;
+ }
+}
+
+.module-content-list-sublist .module-content-list-item {
+
+ // Change background and gradient color
+ > .module-content-list-item-wrapper .module-content-list-item-right,
+ > .module-content-list-item-wrapper {
+ background: $gray-light; // Default non-active color
+ }
+
+ .module-content-list-item-right::before {
+ background-image: linear-gradient(to right, $transparent 0%, $gray-light 90%);
+ }
+
+ // Hover changes
+ @include breakpoint( ">480px" ) {
+ &-link .module-content-list-item-wrapper:hover,
+ &-normal .module-content-list-item-wrapper:hover {
+
+ // Change background and gradient color
+ &,
+ .module-content-list-item-right {
+ background-color: $white;
+ }
+
+ span.module-content-list-item-right::before {
+ background-image: linear-gradient(to right, $transparent 0%, $white 90%);
+ }
+ }
+ }
+}
+
+// Module Content Table
+
+.module-content-table {
+ position: relative;
+
+ .module-content-table-scroll {
+ overflow: auto;
+ overflow-x: auto;
+ overflow-y: visible;
+ min-height: 210px;
+ }
+
+ // Table cells
+ // 1: Make sure table cells are always only on one line, otherwise the sticky left tds don't have the right size
+ // 2: Make right padding much greater to accommodate for increased gradient
+ td,
+ th {
+ font-size: 14px;
+ padding: 8px 12px;
+ border-bottom: 1px solid $gray-light;
+ border-right: 1px solid $gray-light;
+ white-space: nowrap; // 1
+
+ &:first-child {
+ padding-left: 24px;
+ }
+
+ &:last-child {
+ padding-right: 60px; // 2
+ border-right: none;
+ }
+ }
+
+ tbody tr:last-child th,
+ tbody tr:last-child td {
+ border-bottom: none;
+ }
+
+ tbody th:first-child {
+ background: $white;
+ position: absolute;
+ z-index: 2;
+
+ // Disable fixed legend for mobile
+ @include breakpoint( "<480px" ) {
+ position: static;
+ }
+ }
+
+ // Left this modifier as-is because these tables are likely going to change
+ // a lot or otherwise be removed, and at least it's directly dependent on
+ // being associated with a td in this structure
+ @include breakpoint( ">480px" ) {
+ .stats-module & td.has-no-data:hover, // 1
+ tbody tr:hover td,
+ tbody tr:hover th {
+ background: $gray-light;
+ }
+
+ .stats-module & td.highest-count:hover,
+ tbody tr td:hover {
+ background: $gray-light;
+ color: $gray-dark;
+ }
+ }
+
+ .stats-module & td.highest-count {
+ position: relative;
+ color: $alert-yellow;
+ }
+
+ .spacer {
+ color: transparent;
+ }
+
+ .date,
+ .value {
+ white-space: nowrap;
+ }
+
+ .value {
+ display: inline-block;
+ }
+
+ .value-rising,
+ .value-falling {
+ color: $alert-green;
+
+ .gridicon {
+ margin-right: 4px;
+ margin-bottom: -2px;
+ }
+ }
+
+ .value-falling {
+ color: $alert-red;
+ }
+
+ thead th,
+ .date {
+ color: $gray;
+ }
+
+ .date {
+ display: block;
+ font-size: 11px;
+ text-transform: uppercase;
+ letter-spacing: .1em;
+ }
+
+ // Fade out sides of tables to hint at horizontal scrolling
+ // 1: 16px should cover for any scrollbar
+ // 2: Much wider to show horizontal scrolling better (since the window loads scrolled to the left)
+ &::before,
+ &::after {
+ content: "";
+ display: block;
+ position: absolute;
+ top: 0;
+ bottom: 16px; // 1
+ left: 0;
+ z-index: 1;
+ width: 20px;
+ background-image: linear-gradient(to left, $transparent 0%, $white 90%);
+ }
+
+ &::after {
+ right: 0;
+ left: auto;
+ width: 60px; // 2
+ background-image: linear-gradient(to right, $transparent 0%, $white 90%);
+ }
+}
+
+// Module specific tweaks
+
+// Countries
+.countryviews {
+
+ &.is-block {
+ display: none;
+ }
+
+ .stats-module.is-loading & .summary .module-placeholder.is-block {
+ display: block;
+ }
+
+ .module-content-list-item-label .icon img {
+ background-color: $gray-light;
+ }
+}
+
+// Show block placeholder on the summary page only
+.stats-module.is-loading.summary .module-placeholder.is-block {
+ display: block;
+}
+
+// --- Module specific tweaks, avoid adding things here
+
+// Overlay
+
+.main-column.stats-centered {
+ float: none;
+ margin: 0 auto;
+
+ @include breakpoint( ">960px" ) {
+ min-width: 640px;
+ }
+
+ @include breakpoint( "<660px" ) {
+ .stats-module {
+ margin-left: .25em;
+ margin-right: .25em;
+ }
+ }
+
+ @include breakpoint( ">480px" ) {
+ .stats-section-title {
+ text-align: left;
+ }
+ }
+}
+
+// Welcome Stats message
+// -- extends .welcome-message /assets/stylesheets/shared/welcome.scss
+
+.stats-message {
+
+ // Container for 'Visit the old stats layout' link
+ @include breakpoint( "<480px" ) {
+ &.switch-stats {
+ text-align: center;
+ }
+ }
+}
+
+// Insights poll
+
+.stats-poll {
+ color: $gray-dark;
+ text-align: center;
+
+ &.is-gone {
+ animation: fade-out-once .7s forwards; // Poll disappears forever after voting
+ animation-delay: 2s;
+ }
+}
+
+@keyframes fade-out-once {
+ from {
+ opacity: 1;
+ }
+ to {
+ opacity: 0;
+ }
+}
+
+.stats-poll__message {
+
+ @include breakpoint( "<480px" ) {
+ display: block;
+ }
+}
+
+.stats-poll__button {
+ display: inline-block;
+ margin: -9px 0 -9px 7px;
+
+ @include breakpoint( "<480px" ) {
+ width: 40%;
+ margin: 9px 0 9px 7px;
+ }
+}
+
+.card {
+ &.stats-module {
+ padding: 0;
+ }
+}
diff --git a/assets/stylesheets/sections/_translator.scss b/assets/stylesheets/sections/_translator.scss
new file mode 100644
index 00000000000000..3c76a1c88ee364
--- /dev/null
+++ b/assets/stylesheets/sections/_translator.scss
@@ -0,0 +1,102 @@
+#translator-launcher {
+ position: fixed;
+ bottom: 45px;
+ right: 20px;
+
+ border-radius: 27px;
+ background: $blue-wordpress;
+ padding: 4px;
+ font-size: 16px;
+ z-index: 99;
+
+ a {
+ color: $white;
+ text-decoration: none;
+ outline: 0;
+
+ .noticon {
+ font-size: 32px;
+ width: 32px;
+ }
+
+ .text {
+ float: right;
+ width: 0;
+ overflow: hidden;
+ height: 32px;
+ line-height: 32px;
+ white-space: nowrap;
+ }
+
+ &:hover {
+ .text {
+ transition: all 0.25s ease-in-out;
+ width: auto;
+ margin-right: 6px;
+ padding: 0 2px;
+ }
+ }
+ }
+
+ &.active {
+ background: $white;
+ a {
+ color: $blue-wordpress;
+ }
+ }
+}
+
+//Overwriting the popup defaults
+body {
+ .webui-popover {
+ border-radius: 2px;
+ padding: 0;
+ text-align: inherit;
+ border-color: lighten( $gray, 20% );
+
+ .webui-popover-title {
+ background-color: lighten( $gray, 20% );
+ border-color: lighten( $gray, 30% );
+ border-radius: 1px 1px 0 0;
+ }
+
+ &.top,
+ &.top-right,
+ &.top-left {
+ .arrow {
+ border-top-color: lighten( $gray, 20% );
+ }
+ }
+
+ &.right,
+ &.right-top,
+ &.right-bottom {
+ .arrow {
+ border-right-color: lighten( $gray, 20% );
+ }
+ }
+
+ &.left,
+ &.left-top,
+ &.left-bottom {
+ .arrow {
+ border-left-color: lighten( $gray, 20% );
+ }
+ }
+
+ &.bottom,
+ &.bottom-right,
+ &.bottom-left {
+ .arrow {
+ border-bottom-color: lighten( $gray, 20% );
+ &:after {
+ border-bottom-color: lighten( $gray, 20% );
+ }
+ }
+ }
+ }
+}
+
+.translator-modal {
+ max-width: 400px;
+}
diff --git a/assets/stylesheets/sections/_updated-confirmation.scss b/assets/stylesheets/sections/_updated-confirmation.scss
new file mode 100644
index 00000000000000..e1478e54a661b6
--- /dev/null
+++ b/assets/stylesheets/sections/_updated-confirmation.scss
@@ -0,0 +1,147 @@
+
+/**
+ * Updated-confirmation (for post and page changes)
+ */
+
+.updated-confirmation {
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ z-index: 2;
+ background-color: rgba(255, 255, 255, 0.8);
+ .conf-alert {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%) scale(1);
+ width: (166 / 16) * 1em;
+ font-size: (16 / 15) * 1em;
+ padding: (12 / 16) * 1em;
+ text-align: center;
+ background-color: $white;
+ border-radius: 3px;
+ border: solid 1px $alert-green;
+ color: $alert-green;
+ transition: border-color 0.3s ease, color 0.3s ease;
+ .conf-alert_title {
+ &:after {
+ @extend %noticon;
+ content: '\f418';
+ font-size: (24 / 16) * 1em;
+ vertical-align: top;
+ }
+ }
+ hr {
+ background: #eee;
+ margin: (10 / 16) * 1em 0 0;
+ }
+ .undo {
+ display: block;
+ font-size: (12 / 16) * 1em;
+ color: $gray;
+ padding: (10 / 12) * 1em 0 0;
+ &:hover {
+ cursor: pointer;
+ }
+ .pages-list & {
+ display: inline;
+ }
+ span {
+ display: inline-block;
+ border-bottom: solid 1px lighten( $gray, 20% );
+ }
+ }
+ .pages-list & {
+ width: auto;
+ white-space: nowrap;
+ padding: (4 / 16) * 1em (12 / 16) * 1em;
+ }
+ &.conf-alert--trashed,
+ &.conf-alert--deleted,
+ &.conf-alert--error {
+ border-color: $alert-red;
+ color: $alert-red;
+ }
+ &.conf-alert--trashing,
+ &.conf-alert--updating {
+ border-color: $blue-wordpress;
+ color: $blue-wordpress;
+ .loading-dot {
+ animation-name: loading-dot-pulse;
+ animation-duration: 0.5s;
+ animation-fill-mode: both;
+ animation-iteration-count: infinite;
+ &:nth-child(2) {
+ animation-delay: 0.15s;
+ }
+ &:last-child {
+ animation-delay: 0.3s;
+ }
+ }
+ .conf-alert_title {
+ &:after {
+ content: '';
+ }
+ }
+ }
+ &.conf-alert--trashed {
+ .conf-alert_title {
+ &:after {
+ content: '\f407';
+ }
+ }
+ }
+ &.conf-alert--deleted,
+ &.conf-alert--error {
+ .conf-alert_title {
+ &:after {
+ content: '';
+ }
+ }
+ }
+ }
+}
+
+.updated-trans-enter {
+ transition: opacity 0.3s ease;
+ opacity: 0;
+ .updated-confirmation {
+ pointer-events: none;
+ }
+ .conf-alert {
+ transition: transform 0.3s cubic-bezier(0.160, 0.390, 0.220, 1.275), border-color 0.3s ease, color 0.3s ease;
+ transform: translate(-50%, -50%) scale(0.5);
+ }
+ &.updated-trans-enter-active {
+ opacity: 1;
+ .updated-confirmation {
+ pointer-events: auto;
+ }
+ .conf-alert {
+ transform: translate(-50%, -50%) scale(1);
+ }
+ }
+}
+
+.updated-trans-leave {
+ transition: opacity 0.3s ease;
+ opacity: 1;
+ .updated-confirmation {
+ pointer-events: none;
+ }
+ .conf-alert {
+ transition: transform 0.3s cubic-bezier(0.160, 0.390, 0.220, 1.275), border-color 0.3s ease, color 0.3s ease;
+ transform: translate(-50%, -50%) scale(1);
+ }
+ &.updated-trans-leave-active {
+ opacity: 0;
+ .updated-confirmation {
+ pointer-events: none;
+ }
+ .conf-alert {
+ transform: translate(-50%, -50%) scale(0.5);
+ }
+ }
+}
diff --git a/assets/stylesheets/sections/_upgrades.scss b/assets/stylesheets/sections/_upgrades.scss
new file mode 100644
index 00000000000000..205d26cadf0513
--- /dev/null
+++ b/assets/stylesheets/sections/_upgrades.scss
@@ -0,0 +1,340 @@
+.upgrades {
+ .right-column {
+ overflow: hidden;
+ }
+}
+
+// Purchased Products
+.purchased {
+
+ hr {
+ float: left;
+ width: 100%;
+ display: block;
+ height: 2px;
+ background: lighten( $gray, 20% );
+ }
+
+ .product {
+ position: relative;
+ float: left;
+ width: 100%;
+ margin: 0 0 15px 0;
+ padding: 15px;
+ background: $white;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
+ box-sizing: border-box;
+
+ .name {
+ width: 42%;
+ float: left;
+ margin: 0 10px 0 0;
+
+ h3 {
+ margin: 0 0 10px;
+ font-size: 15px;
+ line-height: 1;
+ }
+
+ p {
+ margin: 0;
+ padding: 0;
+ font-size: 13px;
+ color: darken( $gray, 10% );
+ }
+ }
+
+ .status {
+ float: left;
+ width: auto;
+
+ h4 {
+ margin: 2px 0 10px;
+ font-size: 13px;
+ line-height: 1;
+ }
+
+ p {
+ margin: 0;
+ padding: 0;
+ font-size: 13px;
+ color: $gray;
+
+ }
+ }
+
+ .actions {
+ float: right;
+ width: auto;
+ margin: 6px 0 0 0;
+
+ .toggle {
+ float: right;
+ display: inline-block;
+ width: 32px;
+ height: 32px;
+ margin: 0 0 0 5px;
+ color: $gray;
+
+ .noticon {
+ width: 32px;
+ text-align: center;
+ line-height: 32px;
+ }
+ }
+
+ .button {
+ float: right;
+ }
+ }
+ }
+}
+
+// Expiring Products
+.expiring {
+ .product {
+ position: relative;
+ float: left;
+ width: 100%;
+ margin: 0 0 15px 0;
+ padding: 15px;
+ background: $white;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
+ box-sizing: border-box;
+
+ .name {
+ width: 42%;
+ float: left;
+ margin: 0 10px 0 0;
+
+ h3 {
+ margin: 0 0 10px;
+ font-size: 15px;
+ line-height: 1;
+ }
+
+ p {
+ margin: 0;
+ padding: 0;
+ font-size: 13px;
+ color: $gray;
+ }
+ }
+
+ .status {
+ float: left;
+ width: auto;
+
+ h4 {
+ margin: 2px 0 10px;
+ font-size: 13px;
+ line-height: 1;
+ }
+
+ p {
+ margin: 0;
+ padding: 0;
+ font-size: 13px;
+ color: $alert-red;
+
+ }
+ }
+
+ .actions {
+ float: right;
+ width: auto;
+ margin: 6px 0 0 0;
+
+ .toggle {
+ float: right;
+ display: inline-block;
+ width: 32px;
+ height: 32px;
+ margin: 0 0 0 5px;
+ color: $gray;
+
+ .noticon {
+ width: 32px;
+ text-align: center;
+ line-height: 32px;
+ }
+ }
+
+ .button {
+ float: right;
+ }
+ }
+ }
+}
+
+@media screen and (max-width: 770px) {
+ body {
+ }
+}
+
+@media screen and (max-width: 450px) {
+
+ .plan {
+ float: left;
+ width: 100%;
+ margin: 0 0 15px 0;
+
+ .plan-header {
+ float: left;
+ width: 100%;
+ height: 50px;
+ box-sizing: border-box;
+
+ .badge {
+ display: none;
+ }
+
+ h4 {
+ float: left;
+ display: inline-block;
+ margin: 0;
+ padding: 0;
+ text-align: left;
+ font-size: 13px;
+ line-height: 20px;
+ }
+
+ h3 {
+ display: inline-block;
+ margin: 0;
+ padding: 0;
+ font-size: 20px;
+ line-height: 20px;
+ text-align: right;
+ }
+
+ p {
+ display: inline-block;
+ text-align: right;
+ }
+ }
+
+ .plan-details {
+ float: left;
+ width: 100%;
+ box-sizing: border-box;
+
+ p {
+ margin: 0;
+ padding: 0;
+ }
+
+ .illustration {
+ display: none;
+ }
+ }
+
+ .actions {
+ float: left;
+ width: 100%;
+ padding: 15px;
+ box-sizing: border-box;
+ border-top: 1px solid lighten( $gray, 30% );
+
+ p {
+ margin: 0;
+ padding: 0;
+
+ &.current-plan {
+ margin: 0;
+ padding: 0;
+ color: $alert-green;
+ line-height: 1;
+ border: none;
+
+ &:before {
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+
+ content: '\f418';
+ display: inline-block;
+ margin: -3px 5px 0 0;
+ font: normal 16px/1 Noticons;
+ vertical-align: middle;
+
+ }
+ }
+ }
+ }
+ }
+
+ // Purchased Upgrades
+
+ .purchased {
+
+ .product {
+ padding: 0;
+ background: $gray-light;
+
+ .name {
+ float: left;
+ width: 100%;
+ margin: 0;
+ padding: 15px;
+ border-bottom: 2px solid lighten( $gray, 30% );
+ box-sizing: border-box;
+ background: $white;
+
+ h3 {
+ display: inline;
+ float: left;
+ margin: 0;
+ padding: 0;
+ font-size: 13px;
+ font-weight: 600;
+ }
+
+ p {
+ display: inline;
+ float: right;
+ margin: 0;
+ padding: 0;
+ font-size: 13px;
+ line-height: 1;
+ }
+ }
+
+ .status {
+ float: left;
+ width: auto;
+ margin: 2px 0 0 0;
+ padding: 0 0 0 15px;
+
+ h4 {
+ display: inline;
+ float: left;
+ margin: 0 5px 0 0;
+ padding: 0;
+ font-size: 13px;
+ line-height: 46px;
+ }
+
+ p {
+ display: inline;
+ float: left;
+ margin: 0;
+ padding: 0;
+ font-size: 13px;
+ line-height: 46px;
+ }
+ }
+
+ .actions {
+ float: right;
+ width: auto;
+ margin: 0;
+ padding: 10px 15px 10px 0;
+
+ a {
+ font-size: 13px;
+ }
+ }
+ }
+ }
+
+}
diff --git a/assets/stylesheets/shared/_animation.scss b/assets/stylesheets/shared/_animation.scss
new file mode 100644
index 00000000000000..656be52911c31e
--- /dev/null
+++ b/assets/stylesheets/shared/_animation.scss
@@ -0,0 +1,255 @@
+/*
+ * Animations
+ *
+ * Defines `slide-in-up` `slide-out-up` `scale-fade`
+ * Used for section overlays
+ */
+
+$sidebar-width: 269px;
+
+body,
+html {
+ // Setup default styles for some animation classes
+ .slide-in-up {
+ transform: translate3d( 0, 100%, 0 );
+ }
+ .scale-fade {
+ opacity: 1;
+ }
+ .fade {
+ opacity: 0;
+ }
+ .slide-in-left {
+ transform: translate3d( -100%, 0, 0 );
+ }
+ .show-in {
+ opacity: 0;
+
+ &:nth-child(10n + 2) {
+ animation-delay: .05s !important;
+ }
+ &:nth-child(10n + 3) {
+ animation-delay: .1s !important;
+ }
+ &:nth-child(10n + 4) {
+ animation-delay: .15s !important;
+ }
+ &:nth-child(10n + 5) {
+ animation-delay: .2s !important;
+ }
+ &:nth-child(10n + 6) {
+ animation-delay: .25s !important;
+ }
+ &:nth-child(10n + 7) {
+ animation-delay: .3s !important;
+ }
+ &:nth-child(10n + 8) {
+ animation-delay: .35s !important;
+ }
+ &:nth-child(10n + 9) {
+ animation-delay: .4s !important;
+ }
+ &:nth-child(10n + 10) {
+ animation-delay: .45s !important;
+ }
+ }
+
+ // Setup transition parameter on `animate`
+ // Includes timings and animation curves
+ &.animate {
+ .slide-out-up {
+ transition: transform .2s ease-out;
+ transform: translate3d( 0, 0, 0 ) ;
+ }
+ .slide-in-up {
+ transition: transform .4s cubic-bezier( .215, .61, .355, 1 );
+ }
+ .slide-in-left {
+ transition: transform .3s cubic-bezier( .215, .61, .355, 1 ), opacity .3s cubic-bezier( .19, 1, .22, 1 );
+ }
+ .slide-out-right {
+ transition: transform .5s cubic-bezier( .215, .61, .355, 1 ), opacity .5s cubic-bezier( .19, 1, .22, 1 );
+ }
+ .scale-fade {
+ transition: transform .5s, opacity .5s;
+ transform-origin: 50% 60px;
+ }
+ .fade {
+ transition: opacity .5s cubic-bezier( .4, 1, .4, 1 );
+ }
+ .fade-background {
+ transition: background-color .2s cubic-bezier( .4, 1, .4, 1 );
+ }
+ }
+
+ // Transformations for overlay class animation
+ &.overlay-open {
+ .slide-out-up {
+ transform: translate3d( 0, -46px, 0 );
+ }
+ .slide-in-up {
+ transform: translate3d( 0, 0, 0 );
+ }
+ .scale-fade {
+ transform: scale( .95 );
+ opacity: .8;
+ }
+ .fade {
+ opacity: 1;
+ }
+ }
+
+ &.customizer-section {
+ .slide-in-left {
+ transform: translate3d( -40%, 0, 0 );
+ }
+ }
+
+ &.themes-section {
+ .slide-out-right {
+ transform: translate3d( 100%, 0, 0 );
+ }
+ .slide-in-left {
+ transform: translate3d( 0, 0, 0 );
+ }
+ }
+}
+
+// Content sliding left and right to show sidebar
+
+@keyframes slideContentRight {
+ 0% {
+ transform: translate3d( 0, 0, 0 );
+ transform: translate3d( 0, 0, 0 );
+ }
+
+ 70% {
+ transform: translate3d( $sidebar-width + 20, 0, 0 );
+ }
+
+ 100% {
+ transform: translate3d( $sidebar-width, 0, 0 );
+ }
+}
+
+@keyframes slideContentLeft {
+ 0% {
+ transform: translate3d( 0, 0, 0 );
+ }
+
+ 30% {
+ transform: translate3d( 20px, 0, 0 );
+ }
+
+ 100% {
+ transform: translate3d( -$sidebar-width, 0, 0 );
+ }
+}
+
+// Sliding responsive filter and nav menus up and down
+
+@keyframes slideMenuDown {
+ 0% {
+ opacity: 0;
+ transform: translate3d( 0, 0, 0 );
+ }
+
+ 80% {
+ opacity: 1;
+ transform: translate3d( 0, 22px, 0 );
+ }
+
+ 100% {
+ opacity: 1;
+ transform: translate3d( 0, 17px, 0 );
+ }
+}
+
+@keyframes slideMenuUp {
+ 0% {
+ opacity: 1;
+ transform: translate3d( 0, 0, 0 );
+ }
+
+ 20% {
+ opacity: 1;
+ transform: translate3d( 0, 5px, 0 );
+ }
+
+ 100% {
+ opacity: 0;
+ transform: translate3d( 0, -20px, 0 );
+ }
+}
+
+// Rotating chevrons when expanding and collapsing sections
+
+@keyframes rotateOpen {
+ 0% {
+ transform: rotate(0);
+ }
+
+ 50% {
+ transform: rotate(200deg)
+ }
+
+ 75% {
+ transform: rotate(175deg);
+ }
+
+ 90% {
+ transform: rotate(185deg)
+ }
+
+ 100% {
+ transform: rotate(180deg)
+ }
+}
+
+@keyframes rotateClosed {
+ 0% {
+ transform: rotate(180deg)
+ }
+
+ 50% {
+ transform: rotate(-20deg)
+ }
+
+ 75% {
+ transform: rotate(5deg);
+ }
+
+ 90% {
+ transform: rotate(-5deg);
+ }
+
+ 100% {
+ transform: rotate(0);
+ }
+}
+
+// Simple animation to make elements appear
+@keyframes "appear" {
+ 0% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
+
+@keyframes "pulse-light" {
+ 50% { background-color: lighten( $gray, 30% ); }
+}
+
+@keyframes "loading-dot-pulse" {
+ 0% { opacity: 0; }
+ 50% { opacity: 1; }
+ 100% { opacity: 0; }
+}
+
+@keyframes "loading-fade" {
+ 0% { opacity: .5; }
+ 50% { opacity: 1; }
+ 100% { opacity: .5; }
+}
diff --git a/assets/stylesheets/shared/_colors.scss b/assets/stylesheets/shared/_colors.scss
new file mode 100644
index 00000000000000..10f056f211c107
--- /dev/null
+++ b/assets/stylesheets/shared/_colors.scss
@@ -0,0 +1,40 @@
+// Blues
+$blue-wordpress: #0087be;
+$blue-light: #78dcfa;
+$blue-medium: #00aadc;
+$blue-dark: #005082;
+
+// Grays
+$gray: #87a6bc;
+
+// $gray color functions:
+//
+// lighten( $gray, 10% )
+// lighten( $gray, 20% )
+// lighten( $gray, 30% )
+// darken( $gray, 10% )
+// darken( $gray, 20% )
+// darken( $gray, 30% )
+//
+// See wordpress.com/design-handbook/colors/ for more info.
+
+$gray-light: lighten( $gray, 33% ); //#f3f6f8
+$gray-dark: darken( $gray, 38% ); //#2e4453
+
+// Oranges
+$orange-jazzy: #f0821e;
+$orange-fire: #d54e21;
+
+// Alerts
+$alert-yellow: #f0b849;
+$alert-red: #d94f4f;
+$alert-green: #4ab866;
+
+// Link hovers
+$link-highlight: tint($blue-medium, 20%);
+
+// Essentials
+$white: rgba(255,255,255,1);
+$transparent: rgba(255,255,255,0);
+
+$border-ultra-light-gray: #e8f0f5;
diff --git a/assets/stylesheets/shared/_dropdowns.scss b/assets/stylesheets/shared/_dropdowns.scss
new file mode 100644
index 00000000000000..bcadb24c2fbcb3
--- /dev/null
+++ b/assets/stylesheets/shared/_dropdowns.scss
@@ -0,0 +1,124 @@
+/**
+ * Dropdown menu for extra site options
+ */
+.dropdown-menu {
+ background-color: $white;
+ box-shadow: 0 1px 4px rgba($gray-dark, 0.2);
+ position: absolute;
+ right: 0;
+ top: 130%;
+ z-index: 9999;
+ display: none;
+ float: left;
+ margin: 2px 0 0;
+ padding: 6px 0;
+
+ .pointer {
+ position: absolute;
+ right: 8px;
+ top: -7px;
+ width: 12px;
+ height: 10px;
+ float: left;
+ overflow: hidden;
+
+ .pointer-outer,
+ .pointer-inner {
+ position: absolute;
+ top: 0;
+ left: 0;
+ display: inline-block;
+ margin-left: -1px;
+ }
+
+ .pointer-outer {
+ left: 0;
+ top: 0;
+ border-bottom: 7px solid rgba(0,0,0,0.06);
+ border-left: 7px solid transparent;
+ border-right: 7px solid transparent;
+ }
+
+ .pointer-inner {
+ top: 1px;
+ left: 1px;
+ border-left: 6px solid transparent;
+ border-right: 6px solid transparent;
+ border-bottom: 6px solid $white;
+ }
+ }
+
+ ul {
+ margin: 0;
+ padding: 0;
+ list-style-type: none;
+ }
+
+ li {
+ box-sizing: border-box;
+ display: block;
+ float: none;
+ white-space: nowrap;
+
+ a {
+ background: transparent;
+ display: block;
+ font-size: 12px;
+ padding: 5px 20px 5px 18px;
+ text-align: left;
+
+ &:hover {
+ background: $gray-light;
+ }
+ }
+ }
+}
+
+/**
+ * Gear toggle button
+ */
+.gear-dropdown {
+ cursor: pointer;
+ position: absolute;
+ right: 10px;
+ top: 8px;
+ display: block;
+ height: 30px;
+ width: 30px;
+
+ &:after {
+ background: $gray-light;
+ border: 1px solid lighten( $gray, 20% );
+ border-radius: 3px;
+ display: inline-block;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ font: normal 18px/1 Noticons;
+ content: '\f445';
+ color: darken( $gray, 10% );
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 28px;
+ width: 28px;
+ line-height: 28px;
+ text-align: center;
+ z-index: 0;
+ box-shadow: 0 1px 1px rgba($gray-dark,0.1);
+ }
+
+ &:hover:after {
+ color: darken( $gray, 10% );
+ }
+
+ &.gear-open {
+ .dropdown-menu {
+ display: block;
+ }
+ &:after {
+ border-color: rgba( $blue-wordpress, 0.8 );
+ background: rgba( $blue-wordpress, 0.8 );
+ color: $white;
+ }
+ }
+}
diff --git a/assets/stylesheets/shared/_extends.scss b/assets/stylesheets/shared/_extends.scss
new file mode 100644
index 00000000000000..6fe475147c5a71
--- /dev/null
+++ b/assets/stylesheets/shared/_extends.scss
@@ -0,0 +1,124 @@
+%clear-text {
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+%noticon {
+ @extend %clear-text;
+ display: inline-block;
+ font: normal 16px/1 Noticons;
+ vertical-align: middle;
+}
+
+%container {
+ position: relative;
+ margin-bottom: 6%;
+ padding: 0 20px 12px;
+ background: $white;
+ box-shadow: 0 1px 2px rgba(0,0,0,0.075);
+
+ @include clear-fix;
+
+ ul,
+ ol {
+ font-size: 14px;
+ margin-bottom: 4px;
+ }
+
+ @include breakpoint( "<480px" ) {
+ margin-left: .25em;
+ margin-right: .25em;
+ }
+}
+
+%container-header {
+ background: $gray-light;
+ padding: 13px 20px 10px;
+ margin: 0 -20px;
+ min-height: 37px;
+ font-size: 14px;
+ line-height: 135%;
+ border-bottom: 1px solid #EBF0F2;
+}
+
+%status {
+ padding: 3px 5px;
+ font-size: 11px;
+ letter-spacing: 1px;
+ font-weight: 300;
+ text-transform: uppercase;
+ color: $white;
+ border-radius: 3px;
+}
+
+%status-draft,
+%status-pending {
+ background: $alert-yellow;
+ box-shadow: inset 0px 1px 0 darken($alert-yellow, 5%);
+}
+
+%status-scheduled {
+ background: $alert-green;
+ box-shadow: inset 0px 1px 0 darken($alert-green, 5%);
+}
+
+%placeholder {
+
+ .stats-module.is-loading & * {
+ color: transparent;
+ }
+
+ .stats-module.is-loading & {
+ animation: loading-fade 1.6s ease-in-out infinite;
+ position: relative;
+ color: transparent;
+ cursor: default;
+ }
+
+ .stats-module.is-loading &::after {
+ content: "";
+ display: block;
+ position: absolute;
+ background: $gray-light;
+ top: 35%;
+ bottom: 35%;
+ left: 0;
+ right: 0;
+ z-index: 2;
+ }
+}
+
+%placeholder-icon {
+
+ .stats-module.is-loading & {
+ animation: loading-fade 1.6s ease-in-out infinite;
+ }
+
+ .stats-module.is-loading &:hover::before,
+ .stats-module.is-loading &::before,
+ .stats-module.is-loading &:hover,
+ .stats-module.is-loading & {
+ color: $gray-light;
+ fill: $gray-light;
+ }
+}
+
+%heading {
+ color: darken( $gray, 20% );
+ font-size: 2rem;
+ font-weight: 300;
+ margin: 1em 0;
+}
+
+%mobile-link-element {
+ -webkit-tap-highlight-color: rgba($white, .4); // Until we capture ontouch events in JS this is better than :active
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+%mobile-interface-element {
+ @extend %mobile-link-element;
+ -webkit-touch-callout: none;
+}
diff --git a/assets/stylesheets/shared/_forms.scss b/assets/stylesheets/shared/_forms.scss
new file mode 100644
index 00000000000000..49e3bc1dfb1e55
--- /dev/null
+++ b/assets/stylesheets/shared/_forms.scss
@@ -0,0 +1,409 @@
+// Below, you can choose from either using global form styles or class-driven
+// form styles. By default, the global styles are on.
+
+%form {
+ ul {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ }
+}
+
+%form-field {
+ margin: 0;
+ padding: 7px 14px;
+ width: 100%;
+ color: $gray-dark;
+ font-size: 16px;
+ line-height: 1.5;
+ border: 1px solid lighten( $gray, 20% );
+ background-color: $white;
+ transition: all .15s ease-in-out;
+ box-sizing: border-box;
+
+ &::placeholder {
+ color: $gray;
+ }
+
+ &:hover {
+ border-color: lighten( $gray, 10% );
+ }
+
+ &:focus {
+ border-color: $blue-wordpress;
+ outline: none;
+ box-shadow: 0 0 0 2px $blue-light;
+
+ &::-ms-clear {
+ display: none;
+ }
+ }
+
+ &:disabled {
+ background: $gray-light;
+ border-color: lighten( $gray, 30% );
+ color: lighten( $gray, 10% );
+
+ &:hover {
+ cursor: default;
+ }
+
+ &::placeholder {
+ color: lighten( $gray, 10% );
+ }
+ }
+}
+
+%textarea {
+ min-height: 92px;
+}
+
+
+// ==========================================================================
+// Global form elements
+// ==========================================================================
+
+form {
+ @extend %form;
+}
+input[type="text"],
+input[type="search"],
+input[type="email"],
+input[type="number"],
+input[type="password"],
+input[type=checkbox],
+input[type=radio],
+input[type="tel"],
+input[type="url"],
+textarea {
+ @extend %form-field;
+}
+textarea {
+ @extend %textarea;
+}
+
+fieldset,
+input[type="text"],
+input[type="search"],
+input[type="email"],
+input[type="number"],
+input[type="password"],
+input[type="tel"],
+input[type="url"],
+textarea,
+select,
+label {
+ box-sizing: border-box;
+}
+
+/*Checkbooms*/
+
+input[type=checkbox],
+input[type=radio] {
+ clear: none;
+ cursor: pointer;
+ display: inline-block;
+ line-height: 0;
+ height: 16px;
+ margin: 4px 0 0;
+ float: left;
+ outline: 0;
+ padding: 0;
+ text-align: center;
+ vertical-align: middle;
+ width: 16px;
+ min-width: 16px;
+ appearance: none;
+}
+
+input[type=checkbox] + span,
+input[type=radio] + span {
+ display: block;
+ margin-left: 24px;
+}
+
+input[type=checkbox] {
+ &:checked:before {
+ @extend %clear-text;
+
+ content: '\f418';
+ margin: -4px 0 0 -5px;
+ float: left;
+ display: inline-block;
+ vertical-align: middle;
+ width: 16px;
+ font: 400 23px/1 Noticons;
+ speak: none;
+ color: $blue-medium;
+ }
+ &:disabled:checked:before {
+ color: lighten( $gray, 10% );
+ }
+}
+
+input[type=radio] {
+ border-radius: 50%;
+ margin-right: 4px;
+ line-height: 10px;
+
+ &:checked:before {
+ float: left;
+ display: inline-block;
+ content: '\2022';
+ margin: 3px;
+ width: 8px;
+ height: 8px;
+ text-indent: -9999px;
+ background: $blue-medium;
+ vertical-align: middle;
+ border-radius: 50%;
+ animation: grow .2s ease-in-out;
+ }
+
+ &:disabled:checked:before {
+ background: lighten( $gray, 30% );
+ }
+}
+
+@keyframes grow {
+ 0% {
+ transform: scale(0.3);
+ }
+
+ 60% {
+ transform: scale(1.15);
+ }
+
+ 100% {
+ transform: scale(1);
+ }
+}
+
+@keyframes grow {
+ 0% {
+ transform: scale(0.3);
+ }
+
+ 60% {
+ transform: scale(1.15);
+ }
+
+ 100% {
+ transform: scale(1);
+ }
+}
+
+/* end checkbooms */
+
+
+// ==========================================================================
+// Custom form elements
+// ==========================================================================
+
+// Tristate checkbox for bulk selection options
+
+// Example:
+//
+// Check all
+//
+
+.checkbox-tristate {
+ @extend input[type=checkbox];
+ position: relative;
+ margin: 20px 0 19px 20px;
+
+ &:before {
+ position: absolute;
+ color: $blue-medium;
+ font-family: Noticons;
+ }
+
+ .some-selected & {
+ &:before {
+ content: '\f421'; // .noticon-minimize
+ top: 7px;
+ left: 0;
+ }
+ }
+ .all-selected & {
+ &:before {
+ content: url("data:image/svg+xml;utf8, ");
+ height: 100%;
+ width: 100%;
+ top: 0;
+ }
+ }
+}
+
+
+// Toggle switches
+.toggle[type="checkbox"] {
+ display: none;
+}
+
+.toggle {
+ + .toggle-label {
+ position: relative;
+ display: inline-block;
+ border-radius: 12px;
+ padding: 2px;
+ width: 40px;
+ height: 24px;
+ background: lighten( $gray, 10% );
+ vertical-align: middle;
+ outline: 0;
+ cursor: pointer;
+ transition: all .4s ease;
+
+ &:after, &:before{
+ position: relative;
+ display: block;
+ content: "";
+ width: 20px;
+ height: 20px;
+ }
+ &:after{
+ left: 0;
+ border-radius: 50%;
+ background: #fff;
+ transition: all .2s ease;
+ }
+ &:before{
+ display: none;
+ }
+ &:hover{
+ background: lighten( $gray, 20% );
+ }
+ }
+ &:focus{
+ + .toggle-label{
+ box-shadow: 0 0 0 2px $blue-medium;
+ }
+ &:checked + .toggle-label{
+ box-shadow: 0 0 0 2px $blue-light;
+ }
+ }
+ &:checked{
+ + .toggle-label{
+ background: $blue-medium;
+
+ &:after{
+ left: 16px;
+ }
+ }
+ }
+ &:checked:hover{
+ + .toggle-label{
+ background: $blue-light;
+ }
+ }
+
+ &:disabled,
+ &:disabled:hover {
+ + .toggle-label{
+ background: lighten( $gray, 30% );
+ }
+ }
+}
+
+// Classes for toggle state before action is complete (updating plugin or something)
+.toggle.is-toggling {
+ + .toggle-label {
+ background: $blue-medium;
+ }
+ &:checked {
+ + .toggle-label {
+ background: lighten( $gray, 20% );
+ }
+ }
+}
+
+select {
+ background: $white url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiAgICAgICAgPHRpdGxlPmFycm93LWRvd248L3RpdGxlPiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4gICAgPGRlZnM+PC9kZWZzPiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4gICAgICAgIDxnIGlkPSJhcnJvdy1kb3duIiBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIiBmaWxsPSIjQzhEN0UxIj4gICAgICAgICAgICA8cGF0aCBkPSJNMTUuNSw2IEwxNyw3LjUgTDEwLjI1LDE0LjI1IEwzLjUsNy41IEw1LDYgTDEwLjI1LDExLjI1IEwxNS41LDYgWiIgaWQ9IkRvd24tQXJyb3ciIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4gICAgICAgIDwvZz4gICAgPC9nPjwvc3ZnPg==) no-repeat right 10px center;
+ border-color: lighten( $gray, 20% );
+ border-style: solid;
+ border-radius: 4px;
+ border-width: 1px 1px 2px;
+ color: $gray-dark;
+ cursor: pointer;
+ display: inline-block;
+ margin: 0;
+ outline: 0;
+ overflow: hidden;
+ font-size: 14px;
+ line-height: 21px;
+ font-weight: 600;
+ text-overflow: ellipsis;
+ text-decoration: none;
+ vertical-align: top;
+ white-space: nowrap;
+ box-sizing: border-box;
+ padding: 7px 32px 9px 14px; // Aligns the text to the 8px baseline grid and adds padding on right to allow for the arrow.
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+
+ &:hover {
+ background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiAgICAgICAgPHRpdGxlPmFycm93LWRvd248L3RpdGxlPiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4gICAgPGRlZnM+PC9kZWZzPiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4gICAgICAgIDxnIGlkPSJhcnJvdy1kb3duIiBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIiBmaWxsPSIjYThiZWNlIj4gICAgICAgICAgICA8cGF0aCBkPSJNMTUuNSw2IEwxNyw3LjUgTDEwLjI1LDE0LjI1IEwzLjUsNy41IEw1LDYgTDEwLjI1LDExLjI1IEwxNS41LDYgWiIgaWQ9IkRvd24tQXJyb3ciIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4gICAgICAgIDwvZz4gICAgPC9nPjwvc3ZnPg==);
+ }
+
+ &:focus {
+ background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiA8dGl0bGU+YXJyb3ctZG93bjwvdGl0bGU+IDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPiA8ZGVmcz48L2RlZnM+IDxnIGlkPSJQYWdlLTEiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHNrZXRjaDp0eXBlPSJNU1BhZ2UiPiA8ZyBpZD0iYXJyb3ctZG93biIgc2tldGNoOnR5cGU9Ik1TQXJ0Ym9hcmRHcm91cCIgZmlsbD0iIzJlNDQ1MyI+IDxwYXRoIGQ9Ik0xNS41LDYgTDE3LDcuNSBMMTAuMjUsMTQuMjUgTDMuNSw3LjUgTDUsNiBMMTAuMjUsMTEuMjUgTDE1LjUsNiBaIiBpZD0iRG93bi1BcnJvdyIgc2tldGNoOnR5cGU9Ik1TU2hhcGVHcm91cCI+PC9wYXRoPiA8L2c+IDwvZz48L3N2Zz4=);
+ border-color: $blue-medium;
+ box-shadow: 0 0 0 2px $blue-light;
+ outline: 0;
+ -moz-outline:none;
+ -moz-user-focus:ignore;
+ }
+
+ &:disabled,
+ &:hover:disabled {
+ background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPiAgICAgICAgPHRpdGxlPmFycm93LWRvd248L3RpdGxlPiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4gICAgPGRlZnM+PC9kZWZzPiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4gICAgICAgIDxnIGlkPSJhcnJvdy1kb3duIiBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIiBmaWxsPSIjZTllZmYzIj4gICAgICAgICAgICA8cGF0aCBkPSJNMTUuNSw2IEwxNyw3LjUgTDEwLjI1LDE0LjI1IEwzLjUsNy41IEw1LDYgTDEwLjI1LDExLjI1IEwxNS41LDYgWiIgaWQ9IkRvd24tQXJyb3ciIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4gICAgICAgIDwvZz4gICAgPC9nPjwvc3ZnPg==) no-repeat right 10px center;;
+ }
+
+ // A smaller variant that works well when presented inline with text
+ &.is-compact {
+ min-width: 0;
+ padding: 0 20px 2px 6px;
+ margin: 0 4px;
+ background-position: right 5px center;
+ background-size: 12px 12px;
+ }
+
+ // Make it display:block when it follows a label
+ label &,
+ label + & {
+ display: block;
+ min-width: 200px;
+
+ &.is-compact {
+ display: inline-block;
+ min-width: 0;
+ }
+ }
+
+ // IE: Remove the default arrow
+ &::-ms-expand {
+ display: none;
+ }
+
+ // IE: Remove default background and color styles on focus
+ &::-ms-value {
+ background: none;
+ color: $gray-dark;
+ }
+
+ // Firefox: Remove the focus outline, see http://stackoverflow.com/questions/3773430/remove-outline-from-select-box-in-ff/18853002#18853002
+ &:-moz-focusring {
+ color: transparent;
+ text-shadow: 0 0 0 $gray-dark;
+ }
+}
+
+/*Search Inputs*/
+input[type="search"]::-webkit-search-decoration {
+ // We don't use the native results="" UI
+ // Ensures the input text is flush to the start of the element, as in regular text inputs
+ // See, for example, http://geek.michaelgrace.org/2011/06/webkit-search-input-styling/
+ display: none;
+}
diff --git a/assets/stylesheets/shared/_functions.scss b/assets/stylesheets/shared/_functions.scss
new file mode 100644
index 00000000000000..32dddb923ad52c
--- /dev/null
+++ b/assets/stylesheets/shared/_functions.scss
@@ -0,0 +1,11 @@
+// Add percentage of white to a color
+// Copyright © 2011–2015 thoughtbot. See CREDITS.md#L3
+@function tint($color, $percent){
+ @return mix(white, $color, $percent);
+}
+
+// Add percentage of black to a color
+// Copyright © 2011–2015 thoughtbot. See CREDITS.md#L3
+@function shade($color, $percent){
+ @return mix(black, $color, $percent);
+}
diff --git a/assets/stylesheets/shared/_infinite-scroll-end.scss b/assets/stylesheets/shared/_infinite-scroll-end.scss
new file mode 100644
index 00000000000000..a8c0b300700527
--- /dev/null
+++ b/assets/stylesheets/shared/_infinite-scroll-end.scss
@@ -0,0 +1,29 @@
+/**
+* Infinite Scroll End:
+* ------
+* Marker that gets appended to the end of a list (e.g. posts) once
+* infinite scroll has reached the last page.
+*/
+
+.infinite-scroll-end {
+ position: relative;
+ width: 100%;
+ margin-top: (15 / 15) * 1em;
+ text-align: center;
+ &:before {
+ content: '';
+ position: absolute;
+ top: 50%;
+ left: 0;
+ right: 0;
+ z-index: -1;
+ border-bottom: solid 1px lighten( $gray, 25% );
+ }
+ &:after {
+ @include noticon( '\f205', (24 / 15) * 1em );
+ color: $gray;
+ font-size: (22 / 15) * 1em;
+ padding: 0 (8 / 22) * 1em;
+ background-color: $gray-light;
+ }
+}
diff --git a/assets/stylesheets/shared/_livechat.scss b/assets/stylesheets/shared/_livechat.scss
new file mode 100644
index 00000000000000..15b9b606ac27c3
--- /dev/null
+++ b/assets/stylesheets/shared/_livechat.scss
@@ -0,0 +1,1019 @@
+// © Copyright Olark 2015
+#habla_window_div,
+#habla_window_div a,
+#habla_window_div abbr,
+#habla_window_div acronym,
+#habla_window_div address,
+#habla_window_div applet,
+#habla_window_div article,
+#habla_window_div aside,
+#habla_window_div audio,
+#habla_window_div b,
+#habla_window_div big,
+#habla_window_div blockquote,
+#habla_window_div caption,
+#habla_window_div cite,
+#habla_window_div code,
+#habla_window_div dd,
+#habla_window_div del,
+#habla_window_div dfn,
+#habla_window_div dialog,
+#habla_window_div div,
+#habla_window_div dl,
+#habla_window_div dt,
+#habla_window_div em,
+#habla_window_div fieldset,
+#habla_window_div figure,
+#habla_window_div font,
+#habla_window_div footer,
+#habla_window_div form,
+#habla_window_div h1,
+#habla_window_div h2,
+#habla_window_div h3,
+#habla_window_div h4,
+#habla_window_div h5,
+#habla_window_div h6,
+#habla_window_div header,
+#habla_window_div hgroup,
+#habla_window_div hr,
+#habla_window_div i,
+#habla_window_div iframe,
+#habla_window_div img,
+#habla_window_div input,
+#habla_window_div ins,
+#habla_window_div kbd,
+#habla_window_div label,
+#habla_window_div legend,
+#habla_window_div li,
+#habla_window_div mark,
+#habla_window_div menu,
+#habla_window_div nav,
+#habla_window_div object,
+#habla_window_div ol,
+#habla_window_div option,
+#habla_window_div p,
+#habla_window_div pre,
+#habla_window_div q,
+#habla_window_div s,
+#habla_window_div samp,
+#habla_window_div section,
+#habla_window_div select,
+#habla_window_div small,
+#habla_window_div span,
+#habla_window_div strike,
+#habla_window_div strong,
+#habla_window_div sub,
+#habla_window_div sup,
+#habla_window_div table,
+#habla_window_div tbody,
+#habla_window_div td,
+#habla_window_div textarea,
+#habla_window_div tfoot,
+#habla_window_div th,
+#habla_window_div thead,
+#habla_window_div time,
+#habla_window_div tr,
+#habla_window_div tt,
+#habla_window_div ul,
+#habla_window_div var,
+#habla_window_div video {
+ background-attachment:scroll;
+ background-color:transparent;
+ background-image:none;
+ background-position:0 0;
+ background-repeat:repeat;
+ border-color:#000;
+ border-style:none;
+ border-width:medium;
+ clear:none;
+ clip:auto;
+ color:inherit;
+ counter-increment:none;
+ counter-reset:none;
+ cursor:auto;
+ direction:inherit;
+ display:inline;
+ float:none;
+ font-family:inherit;
+ font-size:inherit;
+ font-style:inherit;
+ font-variant:normal;
+ font-weight:inherit;
+ height:auto;
+ letter-spacing:normal;
+ line-height:inherit;
+ list-style:inside;
+ list-style-type:inherit;
+ margin:0;
+ max-height:none;
+ max-width:none;
+ outline:none;
+ overflow:visible;
+ padding:0;
+ position:static;
+ quotes:"" "";
+ table-layout:auto;
+ text-align:inherit;
+ text-decoration:inherit;
+ text-transform:none;
+ unicode-bidi:normal;
+ vertical-align:baseline;
+ visibility:visible;
+ white-space:normal;
+ width:auto;
+ word-spacing:normal;
+ z-index:auto;
+ border-radius:0;
+ -moz-border-radius:0;
+ -webkit-border-radius:0;
+ opacity:1;
+}
+
+#habla_window_div,
+#habla_window_div address,
+#habla_window_div article,
+#habla_window_div aside,
+#habla_window_div blockquote,
+#habla_window_div caption,
+#habla_window_div dd,
+#habla_window_div dialog,
+#habla_window_div div,
+#habla_window_div dl,
+#habla_window_div dt,
+#habla_window_div fieldset,
+#habla_window_div figure,
+#habla_window_div footer,
+#habla_window_div form,
+#habla_window_div h1,
+#habla_window_div h2,
+#habla_window_div h3,
+#habla_window_div h4,
+#habla_window_div h5,
+#habla_window_div h6,
+#habla_window_div header,
+#habla_window_div hgroup,
+#habla_window_div hr,
+#habla_window_div menu,
+#habla_window_div nav,
+#habla_window_div ol,
+#habla_window_div option,
+#habla_window_div p,
+#habla_window_div pre,
+#habla_window_div section,
+#habla_window_div select,
+#habla_window_div table,
+#habla_window_div tbody,
+#habla_window_div td,
+#habla_window_div textarea,
+#habla_window_div tfoot,
+#habla_window_div th,
+#habla_window_div thead,
+#habla_window_div tr,
+#habla_window_div ul {
+ display:block;
+}
+
+#habla_window_div nav ol,
+#habla_window_div nav ul {
+ list-style-type:none;
+}
+
+#habla_window_div menu,
+#habla_window_div ul {
+ list-style-type:disc;
+}
+
+#habla_window_div ol {
+ list-style-type:decimal;
+}
+
+#habla_window_div menu menu,
+#habla_window_div menu ul,
+#habla_window_div ol menu,
+#habla_window_div ol ul,
+#habla_window_div ul menu,
+#habla_window_div ul ul {
+ list-style-type:circle;
+}
+
+#habla_window_div menu menu menu,
+#habla_window_div menu menu ul,
+#habla_window_div menu ol menu,
+#habla_window_div menu ol ul,
+#habla_window_div menu ul menu,
+#habla_window_div menu ul ul,
+#habla_window_div ol menu menu,
+#habla_window_div ol menu ul,
+#habla_window_div ol ol menu,
+#habla_window_div ol ol ul,
+#habla_window_div ol ul menu,
+#habla_window_div ol ul ul,
+#habla_window_div ul menu menu,
+#habla_window_div ul menu ul,
+#habla_window_div ul ol menu,
+#habla_window_div ul ol ul,
+#habla_window_div ul ul menu,
+#habla_window_div ul ul ul {
+ list-style-type:square;
+}
+
+#habla_window_div li {
+ display:list-item;
+ min-height:auto;
+ min-width:auto;
+}
+
+#habla_window_div strong {
+ font-weight:700;
+}
+
+#habla_window_div em {
+ font-style:italic;
+}
+
+#habla_window_div code,
+#habla_window_div kbd,
+#habla_window_div samp {
+ font-family:monospace;
+}
+
+#habla_window_div a,
+#habla_window_div a *,
+#habla_window_div input[type=checkbox],
+#habla_window_div input[type=radio],
+#habla_window_div input[type=submit],
+#habla_window_div select {
+ cursor:pointer;
+}
+
+#habla_window_div a:hover {
+ text-decoration:underline;
+}
+
+#habla_window_div button,
+#habla_window_div input[type=submit] {
+ text-align:center;
+}
+
+#habla_window_div input[type=hidden] {
+ display:none;
+}
+
+#habla_window_div abbr[title],
+#habla_window_div acronym[title],
+#habla_window_div dfn[title] {
+ cursor:help;
+ border-bottom-width:1px;
+ border-bottom-style:dotted;
+}
+
+#habla_window_div ins {
+ background-color:#ff9;
+ color:#000;
+}
+
+#habla_window_div del {
+ text-decoration:line-through;
+}
+
+#habla_window_div blockquote,
+#habla_window_div q {
+ quotes:none;
+}
+
+#habla_window_div blockquote:after,
+#habla_window_div blockquote:before,
+#habla_window_div li:after,
+#habla_window_div li:before,
+#habla_window_div q:after,
+#habla_window_div q:before {
+ content:"";
+ content:none;
+}
+
+#habla_window_div input,
+#habla_window_div select { vertical-align:middle;
+}
+
+#habla_window_div input,
+#habla_window_div select,
+#habla_window_div textarea {
+ border:1px solid #ccc;
+}
+
+#habla_window_div table {
+ border-collapse:collapse;
+ border-spacing:0;
+}
+
+#habla_window_div hr {
+ display:block;
+ height:1px;
+ border:0;
+ border-top:1px solid #ccc;
+ margin:1em 0;
+}
+
+#habla_window_div [dir=rtl] {
+ direction:rtl;
+}
+
+#habla_window_div mark {
+ background-color:#ff9;
+ color:#000;
+ font-style:italic;
+ font-weight:700;
+}
+
+// End of Reset sorta stuff
+//////////////////////////////////////////////////
+
+#habla_window_div {
+ line-height:1;
+ direction:ltr;
+ text-align:left;
+ color:#000;
+ font-style:normal;
+ font-weight:400;
+ text-decoration:none;
+}
+
+#habla_window_div.habla_window_div_base {
+ display:block!important;
+ z-index:99999999;
+}
+
+#habla_window_div #olark-callout-bubble,
+#habla_window_div #olark-callout-bubble-offline,
+#habla_window_div #olark-callout-bubble-online {
+ position:relative!important;
+}
+
+#habla_window_div #habla_panel_div {
+ overflow:hidden;
+}
+
+#habla_window_div #habla_middle_div {
+ padding:6px 10px 3px;
+ background: $white;
+}
+
+:first-child+html #habla_window_div #habla_middle_div {
+ padding:6px 0 0;
+}
+
+#habla_window_div textarea {
+ max-width:100%;
+ width:100%;
+}
+
+:first-child+html #habla_window_div textarea {
+ width:97%;
+}
+
+#habla_window_div #habla_input_div {
+
+}
+
+:first-child+html #habla_window_div #habla_input_div {
+ margin-left:0;
+ margin-right:0;
+ width:95%;
+}
+
+#habla_window_div #habla_chatform_form {
+ margin-top: 5px;
+}
+
+#habla_window_div #habla_conversation_div {
+ padding:6px 10px 0;
+ margin:-6px -10px 0;
+}
+
+:first-child+html #habla_window_div #habla_conversation_div,
+:first-child+html #habla_window_div #habla_offline_message_div,
+:first-child+html #habla_window_div #habla_pre_chat_div {
+ width:97%;
+}
+
+#habla_window_div #habla_name_input,
+#habla_window_div #habla_offline_body_input,
+#habla_window_div #habla_offline_email_input,
+#habla_window_div #habla_pre_chat_email_input,
+#habla_window_div #habla_pre_chat_name_input {
+ // overflow:hidden;
+}
+
+#habla_window_div #habla_offline_message_div,
+#habla_window_div #habla_offline_message_sent_div,
+#habla_window_div #habla_pre_chat_div {
+
+}
+
+:first-child+html #habla_window_div #habla_offline_message_div,
+:first-child+html #habla_window_div #habla_offline_message_sent_div,
+:first-child+html #habla_window_div #habla_pre_chat_div {
+ padding:5px;
+ margin-left:0;
+}
+
+#habla_middle_div {
+ line-height:1.5em;
+}
+
+#habla_window_div #habla_expanded_div {
+ // border-left:1px solid lighten( $gray, 30% );
+ // border-right:1px solid lighten( $gray, 30% );
+ border-left: 1px solid lighten( $gray, 20% );
+ border-right: 1px solid lighten( $gray, 20% );
+ box-shadow: -3px 1px 10px -2px rgba( $gray-dark, 0.075 );
+}
+
+#habla_window_div.habla_window_div_position_inline .habla_panel_border {
+ border-bottom:1px solid lighten( $gray, 30% );
+}
+
+#habla_window_div.olrk-fixed-bottom #habla_topbar_div,
+#habla_window_div.olrk-fixed-bottom .habla_panel_border {
+ // -moz-border-radius-topleft:5px;
+ // -moz-border-radius-topright:5px;
+ // border-top-left-radius:5px;
+ // border-top-right-radius:5px;
+}
+
+#habla_window_div.olrk-fixed-top .habla_panel_border {
+ // -moz-border-radius-bottomleft:5px;
+ // -moz-border-radius-bottomright:5px;
+ // border-bottom-left-radius:5px;
+ // border-bottom-right-radius:5px;
+}
+
+#habla_window_div.olrk-fixed-top #habla_expanded_div {
+ border-bottom:1px solid lighten( $gray, 30% );
+}
+
+#habla_window_div .habla_conversation_div {
+ background:0 0;
+ border-bottom:1px solid lighten( $gray, 30% );
+ line-height:1.5em;
+ overflow:auto;
+ color:#000;
+ width:100%;
+}
+
+#habla_window_div #habla_wcsend_input {
+ background:0 0;
+ overflow:auto;
+ padding:5px;
+ vertical-align:text-top;
+ line-height:1.5em;
+ min-height: 0 !important;
+ height: auto !important;
+}
+
+#habla_window_div .habla_wcsend_input_normal {
+ border:1px solid #b6b6b6;
+ color:#000;
+}
+
+#habla_window_div .habla_wcsend_input_pre {
+ color: $gray-dark;
+}
+
+#habla_window_div .habla_wcsend_input_highlighted {
+ border-color: $gray-dark !important;
+ color:#000;
+}
+
+#habla_window_div .habla_conversation_p_item {
+ background:0 0;
+ color:#000;
+ padding:0;
+ margin:0 0 0 20px;
+ text-indent:-20px;
+ overflow:visible;
+}
+
+#habla_window_div .habla_conversation_person1 {
+ color: lighten( $gray, 20% );
+ padding-right:5px;
+ display:inline;
+}
+
+#habla_window_div .habla_conversation_person2 {
+ color: $blue-wordpress;
+ padding-right:5px;
+}
+
+#habla_window_div .olrk_avatar {
+ float:right;
+ border:1px solid #d3d3d3;
+ margin-left:5px;
+ margin-bottom:5px;
+}
+
+#habla_window_div #habla_offline_message_span,
+#habla_window_div #habla_pre_chat_span {
+ margin-bottom:5px;
+ display:block;
+}
+
+#habla_window_div #habla_offline_message_div,
+#habla_window_div #habla_pre_chat_div {
+ line-height:1.5em;
+}
+
+#habla_window_div #habla_offline_message_span {
+ margin-bottom:5px;
+ display:block;
+}
+
+#habla_window_div #habla_name_input,
+#habla_window_div #habla_offline_body_input,
+#habla_window_div #habla_offline_email_input,
+#habla_window_div #habla_pre_chat_email_input,
+#habla_window_div #habla_pre_chat_name_input {
+ padding: 5px;
+ margin-bottom: 5px;
+ min-height: 0 !important;
+ height: auto!important;
+}
+
+#habla_window_div .habla_offline_submit_input {
+ border-radius: 3px;
+ border-style: solid;
+ border-width: 1px 1px 2px;
+ cursor: pointer;
+ display: inline-block;
+ font-size: 13px;
+ margin: 0;
+ outline: 0;
+ overflow: hidden;
+ padding: .4em .9em;
+ text-overflow: ellipsis;
+ text-decoration: none;
+ vertical-align: top;
+ white-space: nowrap;
+ appearance: none;
+ box-shadow: 0 -1px 0 rgba(255,255,255,0.8) inset;
+ box-sizing: border-box;
+ background: $blue-medium;
+ border-color: shade( $blue-medium, 15% );
+ border-top-color: shade( $blue-medium, 10% );
+ border-bottom-color: shade( $blue-medium, 20% );
+ color: $white;
+ box-shadow: 0 -1px 0 rgba(255,255,255,0.15) inset;
+
+ &:hover,
+ &:focus {
+ background: $link-highlight;
+ border-color: shade($link-highlight, 15%);
+ border-bottom-color: shade($link-highlight, 20%)
+ }
+
+ &:focus {
+ border-color: $gray-dark; // MT / check this color
+ box-shadow: inset 0 1px 0 rgba(120, 200, 230, .6), 1px 1px 2px rgba(0, 0, 0, .4);
+ }
+}
+
+#habla_window_div #habla_pre_chat_error_span,
+#habla_window_div .habla_offline_error_span {
+ margin-top:-20px;
+ float:left;
+ padding-bottom:10px;
+ font-style:italic;
+ line-height:1.5em;
+}
+
+#habla_window_div #habla_topbar_div {
+ background: $blue-medium;
+ color: $white;
+ padding:10px;
+ cursor:pointer;
+}
+
+#habla_window_div #habla_oplink_a {
+ color:#fff;
+ text-decoration:none;
+}
+
+#habla_window_div #habla_oplink_a.habla_oplink_a_hover {
+ text-decoration:underline;
+}
+
+#habla_window_div .clear_style {
+ clear:both;
+}
+
+#habla_window_div .habla_button {
+ float:right;
+ margin-top:-1px;
+ margin-left:4px;
+ padding:0;
+ width:16px;
+ height:16px;
+ cursor:pointer!important;
+ overflow:hidden;
+}
+
+#habla_window_div .habla_button:hover {
+ background-color: rgba(255, 255, 255, 0.15);
+}
+
+#habla_window_div #habla_sizebutton_a {
+
+ &:before {
+ display: inline-block;
+ width: 16px;
+ height: 16px;
+ font: 16px/1 Noticons;
+ content: '\f108';
+ }
+}
+
+.olrk-state-expanded #habla_window_div #habla_sizebutton_a {
+
+ &:before {
+ display: inline-block;
+ width: 16px;
+ height: 16px;
+ font: 16px/1 Noticons;
+ content: '\f431';
+ }
+}
+
+#habla_window_div #habla_sizebutton_a:hover {
+ background-color: rgba(255, 255, 255, 0.15);
+}
+
+#habla_window_div #habla_closebutton_a {
+
+ &:before {
+ display: inline-block;
+ width: 16px;
+ height: 16px;
+ font: 16px/1 Noticons;
+ content: '\f405';
+ }
+}
+
+#habla_window_div #habla_closebutton_a:hover {
+ background-color: rgba(255, 255, 255, 0.15);
+}
+
+#habla_window_div #habla_popout_a {
+
+ &:before {
+ width: 16px;
+ height: 16px;
+ font: 16px/1 Noticons;
+ content: '\f442';
+ }
+}
+
+#habla_window_div #habla_popout_a:hover {
+ background-color: rgba(255, 255, 255, 0.15);
+}
+
+#habla_window_div #habla_panel_div #habla_conversation_div a,
+#habla_window_div #habla_panel_div #habla_conversation_div a:active,
+#habla_window_div #habla_panel_div #habla_conversation_div a:visited,
+#habla_window_div #habla_panel_div #habla_expanded_div a,
+#habla_window_div #habla_panel_div #habla_expanded_div a:active,
+#habla_window_div #habla_panel_div #habla_expanded_div a:visited {
+ color: $blue-wordpress;
+}
+
+#habla_conversation_div .habla_conversation_p_item {
+ word-break: break-word;
+ word-wrap: break-word;
+}
+
+#habla_conversation_div .olark-feedback-wrapper {
+ background-color:#fff!important;
+ border:1px solid #e6e6e6;
+ border-radius:0;
+ -webkit-border-radius:0;
+ -moz-border-radius:0;
+ color:$gray-dark;
+ padding:10px 10px 12px!important;
+ filter:none!important;
+}
+
+#habla_conversation_div p .olark-feedback-wrapper {
+ color:$gray-dark;
+ color:rgba(0,0,0,.7);
+ line-height:1.2em;
+ font-size:13px;
+}
+
+#habla_conversation_div p.olark-feedback-question {
+ color:$gray-dark;
+ color:rgba(0,0,0,.7);
+ font-weight:700;
+}
+
+#habla_conversation_div .olark-feedback-error {
+ background-color:#FF9581;
+ border-radius:2px;
+ -webkit-border-radius:2px;
+ -moz-border-radius:2px;
+ color:#fff;
+ margin-bottom:3px;
+ padding:5px;
+}
+
+#habla_conversation_div .olark-feedback-hidden {
+ display:none;
+}
+
+#habla_conversation_div .olark-feedback-high-answer,
+#habla_conversation_div .olark-feedback-low-answer {
+ background:#E4E089;
+ border-radius:2px;
+ -webkit-border-radius:2px;
+ -moz-border-radius:2px;
+ color:$gray-dark;
+ display:inline-block;
+ font-size:12px;
+ line-height:1em;
+ margin:8px 0 15px;
+ padding:5px;
+ position:relative;
+ text-align:center;
+ max-width:42%;
+}
+
+#habla_conversation_div .olark-feedback-high-answer {
+ float:right;
+}
+
+#habla_conversation_div .olark-feedback-high-answer:after,
+#habla_conversation_div .olark-feedback-low-answer:after,
+#habla_conversation_div p .olark-feedback-high-answer:before,
+#habla_conversation_div p .olark-feedback-low-answer:before {
+ content:"";
+ position:absolute;
+ width:0;
+ height:0;
+}
+
+#habla_conversation_div .olark-feedback-low-answer:after,
+#habla_conversation_div p .olark-feedback-low-answer:before {
+ left:5px;
+ bottom:-5px;
+ border:4px solid;
+ border-color:#E4E089 transparent transparent #E4E089;
+}
+
+#habla_conversation_div .olark-feedback-high-answer:after,
+#habla_conversation_div p .olark-feedback-high-answer:before {
+ right:5px;
+ bottom:-5px;
+ border:4px solid;
+ border-color:#E4E089 #E4E089 transparent transparent;
+}
+
+#habla_conversation_div .olark-feedback-choices-wrap {
+ border-top:2px solid #CACACA;
+ clear:both;
+ height:15px;
+ text-align:justify;
+}
+
+#habla_conversation_div .olark-feedback-radio {
+ -webkit-appearance:radio;
+}
+
+#habla_conversation_div .olark-feedback-input {
+ border:0;
+ display:inline-block;
+ margin-top:-20px;
+}
+
+#habla_conversation_div .olark-feedback-choices-wrap:after {
+ content:'';
+ width:100%;
+ display:inline-block;
+}
+
+#habla_conversation_div .olark-feedback-question-number {
+ color:$gray-dark;
+ display:inline-block;
+ font-size:13px;
+ padding:6px 0;
+ margin-right:5px;
+}
+
+#habla_conversation_div .olark-feedback-text {
+ box-sizing:border-box;
+ color:$gray-dark;
+ font-size:14px;
+ min-height:100px;
+ margin-bottom:5px;
+ padding:3px;
+}
+
+#habla_conversation_div .olark-feedback-placeholder {
+ color:#AAA;
+}
+
+#habla_conversation_div .olark-feedback-submit {
+ background:#1eaedb;
+ border:0;
+ border-radius:2px;
+ -webkit-border-radius:2px;
+ -moz-border-radius:2px;
+ color:#fff;
+ display:inline-block;
+ font-family: "Helvetica Neue", Arial, Helvetica, Geneva, sans-serif;
+ font-size:13px;
+ font-weight:700;
+ line-height:1em;
+ padding:5px;
+}
+
+#habla_conversation_div .olark-feedback-submit:disabled {
+ background:#DDD;
+ color:$gray-dark;
+}
+
+#habla_conversation_div .habla_conversation_notification.olark-feedback-wrapper {
+ color:$gray-dark!important;
+}
+
+#habla_conversation_div .habla_conversation_notification {
+ float: left;
+ color: $gray;
+ font-style: italic;
+ margin: 1em 0;
+}
+
+@-webkit-keyframes pulse {
+ 50% {
+ background-color:rgba(255,0,0,.7)
+ }
+}
+
+@-webkit-keyframes tab_in_bottom{
+ 0% {
+ margin-bottom:-50px;
+ padding-bottom:10px;
+ }
+ 50% {
+ margin-bottom:0;
+ padding-bottom:15px;
+ }
+ 100% {
+ padding-bottom:10px;
+ }
+}
+
+@-webkit-keyframes tab_in_top {
+ 0% {
+ margin-top:-50px;
+ padding-top:10px;
+ }
+ 50% {
+ margin-top:0;
+ padding-top:15px;
+ }
+ 100% {
+ padding-top:10px;
+ }
+}
+
+.olrk-state-compressed .olrk-fixed-bottom #habla_topbar_div {
+ -webkit-animation-name:tab_in_bottom;
+ -webkit-animation-duration:1s;
+ -webkit-animation-iteration-count:1;
+ -webkit-animation-direction:alternate;
+ -webkit-animation-timing-function:ease-in-out;
+}
+
+.olrk-state-compressed .olrk-fixed-top #habla_topbar_div {
+ -webkit-animation-name:tab_in_top;
+ -webkit-animation-duration:1s;
+ -webkit-animation-iteration-count:1;
+ -webkit-animation-direction:alternate;
+ -webkit-animation-timing-function:ease-in-out;
+}
+
+#habla_window_div .habla_topbar_div_highlighted {
+ background:#d05c34;
+ color:#FFF;
+ -webkit-animation-name:pulse;
+ -webkit-animation-duration:3s;
+ -webkit-animation-iteration-count:2;
+ -webkit-animation-direction:alternate;
+ -webkit-animation-timing-function:ease-in-out;
+}
+
+a.hbl_pal_title_fg {
+ color:!important;
+}
+
+.hbl_pal_main_bg {
+ background-color:#fff!important;
+}
+
+.hbl_pal_local_fg,
+.hbl_pal_title_fg {
+ color:!important;
+}
+
+.hbl_pal_title_bg {
+ background-color:!important;
+}
+
+.hbl_pal_offline_submit_fg,
+.hbl_pal_remote_fg {
+ color:!important;
+}
+
+.hbl_pal_offline_submit_bg {
+ background-color:!important;
+}
+
+div.hbl_pal_main_height {
+ // height:150px!important;
+}
+
+div.hbl_pal_main_width {
+ width: 267px!important;
+}
+
+.olrk-fixed-top {
+ position:fixed;
+ bottom:auto;
+ top:0;
+}
+
+.olrk-fixed-bottom {
+ position:fixed;
+ bottom:0;
+ top:auto;
+}
+
+.olrk-fixed-left {
+ position:fixed;
+ right:auto;
+ left:0;
+}
+
+.olrk-fixed-right {
+ position:fixed;
+ right:0;
+ left:auto;
+}
+
+.habla_window_div_position {
+ bottom:0;
+ position:fixed;
+ right:0;
+ margin-right:10px;
+ margin-bottom:10px;
+}
+
+.habla_window_div_position_floating {
+ bottom:0;
+ position:fixed;
+ right:0;
+ margin-right:10px;
+ margin-bottom:10px;
+}
+
+.habla_window_div_position_floating_ie {
+ bottom:0;
+ position:absolute;
+ right:0;
+ margin-right:10px;
+ margin-bottom:10px;
+}
+
+.olrk-state-compressed #habla_window_div #habla_topbar_div {
+ margin-right: -10px;
+ background: transparent;
+}
+
+.olrk-state-compressed #habla_panel_div {
+ background: transparent !important;
+}
+
+.olrk-state-compressed #habla_window_div #habla_sizebutton_a {
+ background-color: $blue-medium;
+ padding: 5px 5px 3px 5px;
+ text-decoration: none;
+}
+
+.olrk-state-compressed #habla_window_div #habla_closebutton_a {
+ display: none;
+}
+.olrk-state-compressed #habla_oplink_a {
+ display: none;
+}
+
+.olrk-state-compressed div.hbl_pal_main_width {
+ width: 28px !important;
+}
diff --git a/assets/stylesheets/shared/_reset.scss b/assets/stylesheets/shared/_reset.scss
new file mode 100644
index 00000000000000..db5621ff9f6949
--- /dev/null
+++ b/assets/stylesheets/shared/_reset.scss
@@ -0,0 +1,78 @@
+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, font, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td {
+ border: 0;
+ font-family: inherit;
+ font-size: 100%;
+ font-style: inherit;
+ font-weight: inherit;
+ margin: 0;
+ outline: 0;
+ padding: 0;
+ vertical-align: baseline;
+}
+html {
+ font-size: 62.5%; /* Corrects text resizing oddly in IE6/7 when body font-size is set using em units http://clagnut.com/blog/348/#c790 */
+ overflow-y: scroll; /* Keeps page centred in all browsers regardless of content height */
+ -webkit-text-size-adjust: 100%; /* Prevents iOS text size adjust after orientation change, without disabling user zoom */
+ -ms-text-size-adjust: 100%; /* www.456bereastreet.com/archive/201012/controlling_text_size_in_safari_for_ios_without_disabling_user_zoom/ */
+}
+body {
+ background: #fff;
+}
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+nav,
+section {
+ display: block;
+}
+ol, ul {
+ list-style: none;
+}
+table { /* tables still need 'cellspacing="0"' in the markup */
+ border-collapse: separate;
+ border-spacing: 0;
+}
+caption, th, td {
+ font-weight: normal;
+ text-align: left;
+}
+blockquote:before, blockquote:after,
+q:before, q:after {
+ content: "";
+}
+blockquote, q {
+ quotes: "" "";
+}
+a:focus {
+ outline: thin dotted;
+}
+a:hover,
+a:active { /* Improves readability when focused and also mouse hovered in all browsers people.opera.com/patrickl/experiments/keyboard/test */
+ outline: 0;
+}
+a img {
+ border: 0;
+}
+
+// Reset the default styling on form inputs in Webkit/iOS
+input,
+textarea {
+ border-radius: 0;
+ appearance: none;
+}
+input[type="radio"],
+input[type="checkbox"] {
+ -webkit-appearance: none;
+}
\ No newline at end of file
diff --git a/assets/stylesheets/shared/_toolbar-bulk.scss b/assets/stylesheets/shared/_toolbar-bulk.scss
new file mode 100644
index 00000000000000..e93ab7003c193c
--- /dev/null
+++ b/assets/stylesheets/shared/_toolbar-bulk.scss
@@ -0,0 +1,248 @@
+// ==========================================================================
+// .toolbar-bulk
+//
+// The toolbar used for bulk actions including bulk selecting and deselecting.
+// ==========================================================================
+
+
+.toolbar-bulk {
+ display: none;
+ position: relative;
+ margin-bottom: 1px;
+ min-height: 55px;
+ background: $gray-light;
+ box-shadow: 0 0 0 1px transparentize( lighten( $gray, 20% ), .5 ),
+ 0 1px 2px lighten( $gray, 30 );
+ transition: margin .15s ease-in-out;
+ z-index: 2; // so menus appear above items
+
+ a {
+ cursor: pointer;
+ }
+
+ &.is-bulk-editing {
+ display: block;
+ background: #fff;
+
+ .toolbar-bulk__toggle {
+ background: lighten( $gray, 30% );
+ background: $gray-light;
+ }
+ }
+}
+
+.toolbar-bulk__toggle {
+ position: relative;
+ float:right;
+ display: block;
+ padding: 4px 10px;
+ background:#fff;
+ border-radius: 2px;
+ font-size: 10px;
+ color: $gray;
+ line-height: 1.5;
+ cursor: pointer;
+ z-index: 30;
+}
+
+// Bulk options
+.toolbar-bulk__actions {
+ @include clear-fix;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity .15s ease-in-out;
+
+ .is-bulk-editing & {
+ opacity: 1;
+ pointer-events: auto;
+ }
+}
+
+// Includes tristate checkbox and dropdown for partial selections
+.toolbar-bulk__check-all {
+ float: left;
+ position: relative;
+ border-right: 1px solid transparentize( lighten( $gray, 20% ), .5 );
+ z-index: 1;
+ display: flex;
+ align-items: center;
+
+ .checkbox-tristate {
+ margin: 0 0 0 24px;
+ }
+
+}
+
+.toolbar-bulk__selection-options-toggle[type="checkbox"] {
+ display: none;
+}
+
+.toolbar-bulk__selection-options-label.noticon { // TODO: remove .noticon
+ height: 55px;
+ padding: 0 8px;
+ color: $blue-wordpress;
+ line-height: 55px;
+ cursor: pointer;
+}
+
+.toolbar-bulk__selection-options {
+ display: none;
+ position: absolute;
+ top: 100%;
+ left: 0;
+ z-index: 1;
+ border: 1px solid transparentize( lighten( $gray, 20% ), .5 );
+ border-left: 0;
+ background: #fff;
+ line-height: 1.5;
+ z-index: 1;
+
+ .toolbar-bulk__selection-options-toggle:checked ~ & {
+ display: block;
+ }
+}
+
+.toolbar-bulk__selection-item {
+ display: block;
+ padding: 12px 20px;
+ font-size: 14px;
+ border-bottom: 1px solid transparentize( lighten( $gray, 20% ), .5 );
+ white-space: pre;
+
+ &:last-child {
+ border: 0;
+ }
+}
+
+
+// Bulk actions dropdown styles
+.toolbar-bulk__action-options-toggle[type="checkbox"] {
+ display: none;
+}
+
+// For mobile menu dropdown
+.toolbar-bulk__action-options-label {
+ padding-left: 15px;
+ color: lighten( $gray, 30% );
+ font-size: 14px;
+ line-height: 55px;
+ cursor: pointer;
+ pointer-events: none;
+
+ .some-selected &,
+ .all-selected & {
+ color: $blue-wordpress;
+ pointer-events: auto;
+ }
+ .noticon {
+ margin-left: 4px;
+ line-height: 55px;
+ }
+ @include breakpoint( ">660px" ) {
+ display: none;
+ }
+}
+
+// wrap everything in this
+.toolbar-bulk__action-options {
+ @include clear-fix;
+ display: none;
+ clear: both;
+ border-top: 1px solid transparentize( lighten( $gray, 20% ), .5 );
+
+ a {
+ display: block;
+ padding: 12px 20px;
+ border-bottom: 1px solid transparentize( lighten( $gray, 20% ), .5 );
+ font-size: 14px;
+ white-space: pre;
+ }
+ .toolbar-bulk__action-options-toggle:checked ~ & {
+ display: block;
+ }
+ @include breakpoint( ">660px" ) {
+ display: block;
+ border: 0;
+ opacity: 0;
+ clear: none;
+
+ .some-selected &,
+ .all-selected & {
+ opacity: 1;
+ }
+ a {
+ display: inline-block;
+ padding: 0 10px;
+ line-height: 56px;
+ border: 0;
+ }
+ }
+}
+
+.toolbar-bulk__action-group {
+ &:last-child {
+ a:last-child {
+ border: 0;
+ }
+ }
+ .noticon {
+ display: none;
+ }
+ @include breakpoint( ">660px" ) {
+ position: relative;
+ display: block;
+ float: left;
+ border-right: 1px solid transparentize( lighten( $gray, 20% ), .5 );
+
+ .noticon {
+ display: inline-block;
+ }
+ .toolbar-bulk__more-actions-toggle {
+ margin-left: -10px;
+ }
+ .noticon-trash {
+ font-size: 20px;
+ + a {
+ display: none;
+ }
+ }
+ }
+}
+
+// Allows for dropdowns with more actions
+.toolbar-bulk__more-actions {
+ @include breakpoint( ">660px" ) {
+ display: none;
+ position: absolute;
+ top: 100%;
+ left: 0;
+ margin: 0 0 0 -1px;
+ min-width: 200px;
+ width: 100%;
+ background: #fff;
+ border: 1px solid transparentize( lighten( $gray, 20% ), .5 );
+ z-index: 1;
+
+ &:before {
+ content: '';
+ position: absolute;
+ top: -2px; // keeps hover state active despite 2px gap caused by borders
+ left: 0;
+ width: 100%;
+ height: 2px;
+ }
+ a {
+ display: block;
+ line-height: 44px;
+ border-bottom: 1px solid transparentize( lighten( $gray, 20% ), .5 );
+
+ &:last-child {
+ border: 0;
+ }
+ }
+ .toolbar-bulk__action-group:hover > &,
+ &:hover {
+ display: block;
+ }
+ }
+}
diff --git a/assets/stylesheets/shared/_typography.scss b/assets/stylesheets/shared/_typography.scss
new file mode 100644
index 00000000000000..da33747201b66c
--- /dev/null
+++ b/assets/stylesheets/shared/_typography.scss
@@ -0,0 +1,26 @@
+// Typeface Variables
+
+$monospace: "Courier 10 Pitch", Courier, monospace;
+$code: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", $monospace;
+
+$serif-fallback: Georgia, "Times New Roman", Times, serif;
+$serif: Merriweather, $serif-fallback;
+
+$sans-fallback: Helvetica, Arial, sans-serif;
+$sans: "Open Sans", $sans-fallback; // 400 400i 600 700 700i
+
+$sans-rtl: Tahoma, $sans-fallback;
+
+%content-font {
+ font-family: $serif;
+ font-weight: 400;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ color: darken( $gray, 30 );
+}
+
+
+// NOTE:
+// If there are exceptions to these stacks,
+// please mark them with a //typography-exception comment
+// so we can easily search for them later.
diff --git a/assets/stylesheets/shared/_utilities.scss b/assets/stylesheets/shared/_utilities.scss
new file mode 100644
index 00000000000000..71fc30b6df334e
--- /dev/null
+++ b/assets/stylesheets/shared/_utilities.scss
@@ -0,0 +1,26 @@
+// Text meant only for screen readers
+.screen-reader-text { // TODO add _accessibility.scss
+ clip: rect(1px, 1px, 1px, 1px);
+ position: absolute !important;
+
+ &:hover,
+ &:active,
+ &:focus {
+ background-color: #f1f1f1;
+ border-radius: 3px;
+ box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.6);
+ clip: auto !important;
+ color: #21759b;
+ display: block;
+ font-size: 14px;
+ font-weight: bold;
+ height: auto;
+ left: 5px;
+ line-height: normal;
+ padding: 15px 23px 14px;
+ text-decoration: none;
+ top: 5px;
+ width: auto;
+ z-index: 100000; // Above WP toolbar
+ }
+}
diff --git a/assets/stylesheets/shared/_welcome.scss b/assets/stylesheets/shared/_welcome.scss
new file mode 100644
index 00000000000000..530d0b99317a2c
--- /dev/null
+++ b/assets/stylesheets/shared/_welcome.scss
@@ -0,0 +1,65 @@
+/**
+* Welcome Messages (currently on stats and post pages)
+*/
+
+.welcome-message {
+ @extend %container;
+ position: relative;
+ background: $gray-light;
+ margin-bottom: 6%;
+ padding: 16px;
+
+ .close-button {
+ position: absolute;
+ top: 0;
+ right: 0;
+ display: block;
+ padding: 14px 8px;
+
+ .noticon {
+ font-size: 32px;
+ line-height: 32px;
+ }
+
+ // Focus state
+ &:focus {
+ outline: none;
+ color: $link-highlight;
+ }
+ }
+
+ .welcome-section-title {
+ @extend %heading;
+
+ font-size: 21px;
+ line-height: 24px;
+ color: $gray;
+
+ margin-top: 0;
+ margin-bottom: 20px;
+ padding-right: 20px;
+ }
+
+ p:last-child {
+ margin-bottom: 0;
+ }
+
+ .welcome-intro-illustration {
+ width: 160px;
+ height: 160px;
+ float: right;
+ margin: 0 0 1em 1em;
+
+ @include breakpoint( "<480px" ) {
+ width: 120px;
+ height: 120px;
+ }
+ }
+
+ @include breakpoint( ">660px" ) {
+ padding: 24px;
+ .close-button {
+ padding: 8px;
+ }
+ }
+}
diff --git a/assets/stylesheets/shared/mixins/_breakpoints.scss b/assets/stylesheets/shared/mixins/_breakpoints.scss
new file mode 100644
index 00000000000000..035bf65054cc70
--- /dev/null
+++ b/assets/stylesheets/shared/mixins/_breakpoints.scss
@@ -0,0 +1,58 @@
+// ==========================================================================
+// Breakpoint Mixin
+// See https://wpcalypso.wordpress.com/devdocs/docs/coding-guidelines/css.md#media-queries
+// ==========================================================================
+
+$breakpoints: 480px, 660px, 960px, 1040px; // Think very carefully before adding a new breakpoint
+
+@mixin breakpoint( $size ){
+ @if type-of($size) == string {
+ $approved-value: 0;
+ @each $breakpoint in $breakpoints {
+ $and-larger: ">" + $breakpoint;
+ $and-smaller: "<" + $breakpoint;
+
+ @if $size == $and-smaller {
+ $approved-value: 1;
+ @media ( max-width: $breakpoint ) {
+ @content;
+ }
+ }
+ @else {
+ @if $size == $and-larger {
+ $approved-value: 2;
+ @media ( min-width: $breakpoint + 1 ) {
+ @content;
+ }
+ }
+ @else {
+ @each $breakpoint-end in $breakpoints {
+ $range: $breakpoint + "-" + $breakpoint-end;
+ @if $size == $range {
+ $approved-value: 3;
+ @media ( min-width: $breakpoint + 1 ) and ( max-width: $breakpoint-end ) {
+ @content;
+ }
+ }
+ }
+ }
+ }
+ }
+ @if $approved-value == 0 {
+ $sizes: "";
+ @each $breakpoint in $breakpoints {
+ $sizes: $sizes + " " + $breakpoint;
+ }
+ // TODO - change this to use @error, when it is supported by node-sass
+ @warn "ERROR in breakpoint( #{ $size } ): You can only use these sizes[ #{$sizes} ] using the following syntax [ <#{ nth( $breakpoints, 1 ) } >#{ nth( $breakpoints, 1 ) } #{ nth( $breakpoints, 1 ) }-#{ nth( $breakpoints, 2 ) } ]";
+ }
+ }
+ @else {
+ $sizes: "";
+ @each $breakpoint in $breakpoints {
+ $sizes: $sizes + " " + $breakpoint;
+ }
+ // TODO - change this to use @error, when it is supported by node-sass
+ @warn "ERROR in breakpoint( #{ $size } ): Please wrap the breakpoint $size in parenthesis. You can use these sizes[ #{$sizes} ] using the following syntax [ <#{ nth( $breakpoints, 1 ) } >#{ nth( $breakpoints, 1 ) } #{ nth( $breakpoints, 1 ) }-#{ nth( $breakpoints, 2 ) } ]";
+ }
+}
diff --git a/assets/stylesheets/shared/mixins/_calc.scss b/assets/stylesheets/shared/mixins/_calc.scss
new file mode 100644
index 00000000000000..343a891b51fc9c
--- /dev/null
+++ b/assets/stylesheets/shared/mixins/_calc.scss
@@ -0,0 +1,6 @@
+@mixin calc($property, $expression) {
+ #{$property}: -moz-calc(#{$expression});
+ #{$property}: -o-calc(#{$expression});
+ #{$property}: -webkit-calc(#{$expression});
+ #{$property}: calc(#{$expression});
+}
diff --git a/assets/stylesheets/shared/mixins/_clear-fix.scss b/assets/stylesheets/shared/mixins/_clear-fix.scss
new file mode 100644
index 00000000000000..5a69a1ef735868
--- /dev/null
+++ b/assets/stylesheets/shared/mixins/_clear-fix.scss
@@ -0,0 +1,13 @@
+// ==========================================================================
+// Clear fix mixin (clearfix)
+// ==========================================================================
+
+@mixin clear-fix {
+ &:after {
+ content: ".";
+ display: block;
+ height: 0;
+ clear: both;
+ visibility: hidden;
+ }
+}
diff --git a/assets/stylesheets/shared/mixins/_dropdown-menu.scss b/assets/stylesheets/shared/mixins/_dropdown-menu.scss
new file mode 100644
index 00000000000000..ffaf038f226d5a
--- /dev/null
+++ b/assets/stylesheets/shared/mixins/_dropdown-menu.scss
@@ -0,0 +1,62 @@
+// ==========================================================================
+// Dropdown menu mixin
+// Turn a list into a dropdown menu
+// ==========================================================================
+
+@mixin dropdown-menu {
+ display: none;
+ background: $white;
+ float: none;
+ line-height: 46px;
+ min-width: 220px;
+ overflow: visible;
+ padding: 0;
+ position: absolute;
+ width: auto;
+ z-index: 1;
+ box-sizing: border-box;
+ box-shadow: 0 0 2px rgba(0,0,0,0.15), 0 3px 8px rgba(0,0,0,0.1);
+
+ &:after {
+ border: 6px solid transparent;
+ border-bottom-color: $white;
+ content: ' ';
+ height: 0;
+ position: absolute;
+ top: -12px;
+ left: 73px;
+ width: 0;
+ }
+
+ li {
+ display: block;
+ float: none;
+
+ a,
+ a.selected {
+ border-bottom: 1px solid rgba(0,0,0,0.1);
+ color: $blue-wordpress;
+ display: block;
+ float: none;
+ height: auto;
+ margin: 0;
+ padding: 0 14px;
+ text-align: left;
+
+ &:hover {
+ border-bottom: 1px solid rgba(0,0,0,0.1);
+ background: none; // Remove inherited background color
+ color: $link-highlight;
+ box-shadow: none; // Remove inherited box shadow
+ }
+ }
+
+ a.selected {
+ color: $gray-dark;
+ }
+
+ &:last-child a {
+ border-bottom: none; // Last child in the dropdown doesn't need a bottom border
+ }
+ }
+}
diff --git a/assets/stylesheets/shared/mixins/_hide-content-accessibly.scss b/assets/stylesheets/shared/mixins/_hide-content-accessibly.scss
new file mode 100644
index 00000000000000..69b3155edf4110
--- /dev/null
+++ b/assets/stylesheets/shared/mixins/_hide-content-accessibly.scss
@@ -0,0 +1,12 @@
+// ==========================================================================
+// Hide content accessibly mixin
+// Hides content in a way that makes it still accessible to screen readers
+// ==========================================================================
+
+@mixin hide-content-accessibly {
+ clip: rect( 1px, 1px, 1px, 1px );
+ height: 1px;
+ overflow: hidden;
+ position: absolute;
+ width: 1px;
+}
diff --git a/assets/stylesheets/shared/mixins/_long-content-fade.scss b/assets/stylesheets/shared/mixins/_long-content-fade.scss
new file mode 100644
index 00000000000000..f6b5919bdbd786
--- /dev/null
+++ b/assets/stylesheets/shared/mixins/_long-content-fade.scss
@@ -0,0 +1,61 @@
+// ==========================================================================
+// Long content fade mixin
+//
+// Creates a fading overlay to signify that the content is longer
+// than the space allows.
+// ==========================================================================
+
+@mixin long-content-fade( $direction: right, $size: 20%, $color: #fff, $edge: 0px, $z-index: false) {
+ content: '';
+ display: block;
+ position: absolute;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ pointer-events: none;
+
+ @if $z-index {
+ z-index: $z-index;
+ }
+
+ @if $direction == 'bottom' {
+ background: linear-gradient( to top, rgba( $color, 0 ), $color 90% );
+ left: $edge;
+ right: $edge;
+ top: $edge;
+ bottom: calc(100% - $size);
+ width: auto;
+ }
+
+ @if $direction == 'top' {
+ background: linear-gradient( to bottom, rgba( $color, 0 ), $color 90% );
+ top: calc(100% - $size);
+ left: $edge;
+ right: $edge;
+ bottom: $edge;
+ width: auto;
+ }
+
+ @if $direction == 'left' {
+ background: linear-gradient( to left, rgba( $color, 0 ), $color 90% );
+ top: $edge;
+ left: $edge;
+ bottom: $edge;
+ right: auto;
+ width: $size;
+ height: auto;
+ }
+
+ @if $direction == 'right' {
+ background: linear-gradient( to right, rgba( $color, 0 ), $color 90% );
+ top: $edge;
+ bottom: $edge;
+ right: $edge;
+ left: auto;
+ width: $size;
+ height: auto;
+ }
+}
diff --git a/assets/stylesheets/shared/mixins/_mixins.scss b/assets/stylesheets/shared/mixins/_mixins.scss
new file mode 100644
index 00000000000000..501d3774e2c9ae
--- /dev/null
+++ b/assets/stylesheets/shared/mixins/_mixins.scss
@@ -0,0 +1,9 @@
+@import 'breakpoints';
+@import 'calc';
+@import 'clear-fix';
+@import 'dropdown-menu';
+@import 'hide-content-accessibly';
+@import 'long-content-fade';
+@import 'noticon';
+@import 'placeholder';
+@import 'stats-fade-text';
diff --git a/assets/stylesheets/shared/mixins/_noticon.scss b/assets/stylesheets/shared/mixins/_noticon.scss
new file mode 100644
index 00000000000000..aa47adf9e81049
--- /dev/null
+++ b/assets/stylesheets/shared/mixins/_noticon.scss
@@ -0,0 +1,27 @@
+// ==========================================================================
+// Noticon mixin (clearfix)
+// See http://calypso.localhost:3000/devdocs/docs/coding-guidelines/css.md
+// ==========================================================================
+
+@mixin noticon($char, $size: null) {
+ // This isn't very clean, but... we'll see ;)
+ @if $size != 0 {
+ font-size: $size;
+ }
+ content: $char;
+
+ // Copied verbatim
+ vertical-align: top;
+ text-align: center;
+ display: inline-block;
+ font-family: Noticons;
+ font-style: normal;
+ font-weight: normal;
+ font-variant: normal;
+ line-height: 1;
+ text-decoration: inherit;
+ text-transform: none;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-font-smoothing: antialiased;
+ speak: none;
+}
diff --git a/assets/stylesheets/shared/mixins/_placeholder.scss b/assets/stylesheets/shared/mixins/_placeholder.scss
new file mode 100644
index 00000000000000..93328c884de075
--- /dev/null
+++ b/assets/stylesheets/shared/mixins/_placeholder.scss
@@ -0,0 +1,14 @@
+// ==========================================================================
+// Placeholder mixin
+// Adds animation to placeholder section
+// ==========================================================================
+
+@mixin placeholder( $lighten-percentage: 30% ) {
+ animation: loading-fade 1.6s ease-in-out infinite;
+ background-color: lighten( $gray, $lighten-percentage );
+ color: transparent;
+
+ &:after {
+ content: '\00a0';
+ }
+}
diff --git a/assets/stylesheets/shared/mixins/_stats-fade-text.scss b/assets/stylesheets/shared/mixins/_stats-fade-text.scss
new file mode 100644
index 00000000000000..38245855b8083e
--- /dev/null
+++ b/assets/stylesheets/shared/mixins/_stats-fade-text.scss
@@ -0,0 +1,17 @@
+// ==========================================================================
+// Stats fade text mixin
+//
+// TODO: rename or bundle with stats
+// ==========================================================================
+
+@mixin stats-fade-text($toColor) {
+ background-image: linear-gradient(to right, $transparent 0%, $toColor 90%);
+ position: absolute;
+ z-index: 1;
+ left: -48px;
+ top: 0;
+ bottom: 0;
+ content: "";
+ display: block;
+ width: 48px;
+}
diff --git a/assets/stylesheets/style.scss b/assets/stylesheets/style.scss
new file mode 100644
index 00000000000000..26d04b48567876
--- /dev/null
+++ b/assets/stylesheets/style.scss
@@ -0,0 +1,50 @@
+// Shared
+@import 'shared/reset'; // css reset before the rest of the styles are defined
+@import 'shared/functions'; // functions that we've used from Compass, ported over
+@import 'shared/colors'; // import all of our wpcom colors
+@import 'shared/utilities'; // Helper classes
+@import 'shared/typography'; // all the typographic rules, variables, etc.
+@import 'shared/mixins/mixins'; // sass mixins for gradients, bordius radii, etc.
+@import 'shared/extends'; // sass extends for commonly used styles
+@import 'shared/animation'; // all UI animation
+@import 'shared/forms'; // form styling
+@import 'shared/dropdowns'; // dropdown styling
+@import 'shared/toolbar-bulk'; // The toolbar used for bulk actions including bulk selecting and deselecting
+@import 'shared/livechat'; // styles for the popup livechat box
+@import 'shared/welcome'; // welcome messages
+@import 'shared/infinite-scroll-end'; // Last page marker once infinite scroll has reached end
+
+// Layouts
+@import 'layout/main'; // global layout and responsive styles
+@import 'layout/masterbar'; // masterbar styles
+@import 'layout/sidebar'; // sidebar styles, shared across most pages
+@import 'layout/overlay'; // overlay UX styling
+@import 'layout/detail-page'; // detail page styles
+
+// Sections
+@import 'sections/sites'; // sites section styles
+@import 'sections/manage'; // manage section styles
+@import 'sections/posts'; // posts section styles
+@import 'sections/updated-confirmation'; // confirmation boxes for posts/pages
+@import 'sections/posts-controls'; // posts controls styles
+@import 'sections/post-relative-time-status'; // post relative time styles
+@import 'sections/stats'; // stats page styles
+@import 'sections/upgrades'; // upgrades page styles
+@import 'sections/sharing'; // sharing page styles
+@import 'sections/site-settings'; // blog setting styles
+@import 'sections/notifications'; // notifications styles
+@import 'sections/checkout'; // Checkout styles
+@import 'sections/billing-history'; // Billing History styles
+@import 'sections/domain-search'; // Domain Search styles
+@import 'sections/menus'; // menus page styles
+@import 'sections/keyboard-shortcuts'; // keyboard shortcuts menu
+@import 'sections/plugins';
+@import 'sections/translator';
+@import 'sections/devdocs'; // developer documentation at /devdocs
+@import 'sections/nux-welcome'; // persistent welcome for new users
+
+// Components
+@import 'components';
+
+// Devdocs
+@import 'devdocs/design/style';
diff --git a/bin/bundler b/bin/bundler
new file mode 120000
index 00000000000000..fd6c51b76dddfd
--- /dev/null
+++ b/bin/bundler
@@ -0,0 +1 @@
+../server/bundler/bin/bundler.js
\ No newline at end of file
diff --git a/bin/generate-devdocs-index b/bin/generate-devdocs-index
new file mode 120000
index 00000000000000..c1a548db21d116
--- /dev/null
+++ b/bin/generate-devdocs-index
@@ -0,0 +1 @@
+../server/devdocs/bin/generate-devdocs-index
\ No newline at end of file
diff --git a/bin/get-i18n b/bin/get-i18n
new file mode 120000
index 00000000000000..e3af0c70808e54
--- /dev/null
+++ b/bin/get-i18n
@@ -0,0 +1 @@
+../server/i18n/bin/i18n-cli.js
\ No newline at end of file
diff --git a/bin/i18nlint b/bin/i18nlint
new file mode 120000
index 00000000000000..b75bd16f9f4da1
--- /dev/null
+++ b/bin/i18nlint
@@ -0,0 +1 @@
+../server/i18nlint/bin/i18nlint-cli.js
\ No newline at end of file
diff --git a/bin/list-assets b/bin/list-assets
new file mode 120000
index 00000000000000..ccb50f06dbf462
--- /dev/null
+++ b/bin/list-assets
@@ -0,0 +1 @@
+../server/bundler/bin/list-assets.js
\ No newline at end of file
diff --git a/bin/live-reload b/bin/live-reload
new file mode 100755
index 00000000000000..9009eda5cbad5a
--- /dev/null
+++ b/bin/live-reload
@@ -0,0 +1,13 @@
+#!/usr/bin/env node
+
+var livereload = require('livereload'),
+ server = livereload.createServer({
+ exts: [ 'html', 'css', 'scss', 'js', 'jsx', 'png', 'gif', 'jpg', 'svg' ],
+ applyJSLive: false,
+ applyCSSLive: false,
+ exclusions: [ '.git', 'node_modules' ],
+ originalPath: 'http://calypso.localhost:3000',
+ debug: true
+ });
+
+server.watch( __dirname + '/..' );
diff --git a/bin/pre-commit b/bin/pre-commit
new file mode 100755
index 00000000000000..5bdab6c53deb39
--- /dev/null
+++ b/bin/pre-commit
@@ -0,0 +1,44 @@
+#!/bin/sh
+
+echo "\nBy contributing to this project, you license the materials you contribute under the GNU General Public License v2 (or later).\n\n"
+
+files=$(git diff --cached --name-only --diff-filter=ACM | grep ".jsx*$")
+if [ "$files" = "" ]; then
+ exit 0
+fi
+
+pass=true
+
+# i18nlinting. Just have i18nlint issue warnings for now without failing
+echo "\nValidating translatable strings:\n"
+for file in ${files}; do
+ result=$(./bin/i18nlint --color ${file})
+ if [ $? -ne 0 ]; then
+ echo "\033[31mi18nlint Failed: \033[0m$result"
+ echo "\n -----\n"
+ pass=false
+ else
+ echo "\033[32mi18nlint Passed: ${file}\033[0m"
+ fi
+done
+
+echo "\nValidating .jsx and .js:\n"
+
+for file in ${files}; do
+ ./node_modules/.bin/eslint ${file}
+ if [ $? -ne 0 ]; then
+ echo "\033[31meslint Failed: ${file}\033[0m"
+ pass=false
+ else
+ echo "\033[32meslint Passed: ${file}\033[0m"
+ fi
+done
+
+echo "\neslint validation complete\n"
+
+if ! $pass; then
+ echo "\033[41mCOMMIT FAILED:\033[0m Your commit contains files that should pass validation tests but do not. Please fix the errors and try again.\n"
+ exit 1
+else
+ echo "\033[42mCOMMIT SUCCEEDED\033[0m\n"
+fi
diff --git a/bin/pre-push b/bin/pre-push
new file mode 100755
index 00000000000000..8690ffb9365b88
--- /dev/null
+++ b/bin/pre-push
@@ -0,0 +1,26 @@
+#!/bin/sh
+
+echo "\nBy contributing to this project, you license the materials you contribute under the GNU General Public License v2 (or later).\n\n"
+echo "\nRunning tests ...\n"
+make test
+if [ $? -ne 0 ]; then
+ echo "\n\n\033[41mPUSH NOT ALLOWED:\033[0m The tests are broken! Please fix them and try again.\n"
+ echo "(If you want to push anyway, you can repeat the command using --no-verify to skip this check)\n"
+ exit 1
+fi
+
+echo "\n\n\033[42mPUSH READY TO GO\033[0m\n"
+
+current_branch=$(git symbolic-ref HEAD | sed -e 's,.*/\(.*\),\1,')
+if [ 'master' = $current_branch ]
+then
+ read -p "You're about to push ¡¡¡[ master ]!!!, is that what you intended? [y|n] " -r < /dev/tty
+ echo
+ if echo $REPLY | grep -E '^[Yy]$' > /dev/null
+ then
+ exit 0 # push will execute
+ fi
+ exit 1 # push will not execute
+fi
+exit 0
+
diff --git a/bin/record-env b/bin/record-env
new file mode 100755
index 00000000000000..60790671a83233
--- /dev/null
+++ b/bin/record-env
@@ -0,0 +1,19 @@
+#!/bin/sh
+
+FILE=$1
+
+CURRENT_ENV=$CALYPSO_ENV
+if [ "$ENABLE_FEATURES" ]; then
+ CURRENT_ENV="${CURRENT_ENV} with=\"${ENABLE_FEATURES}\""
+fi
+if [ "$DISABLE_FEATURES" ]; then
+ CURRENT_ENV="${CURRENT_ENV} without=\"${DISABLE_FEATURES}\""
+fi
+
+if [ -e $FILE ]; then
+ PREVIOUS_ENV=`cat $FILE`
+fi
+
+if [ ! -e $FILE ] || [ "$PREVIOUS_ENV" != "$CURRENT_ENV" ]; then
+ echo $CURRENT_ENV > $FILE
+fi
diff --git a/bin/run-all-tests b/bin/run-all-tests
new file mode 100755
index 00000000000000..75b8b63bb8aab2
--- /dev/null
+++ b/bin/run-all-tests
@@ -0,0 +1,87 @@
+#!/bin/bash
+
+BASEDIR=$(dirname $0)
+FAILED=0
+FAILED_ROUTES=()
+RED='\033[0;31m'
+BLUE='\033[0;34m'
+NO_COLOR='\033[0m'
+
+if [ "${MULTICORE}" = 1 ]; then
+ CORES=${CORES:-}
+else
+ CORES=1
+fi
+
+
+# print as many equal signs as the terminal is wide
+print_horizontal_rule() {
+ printf "%$(tput cols)s\n" | tr " " "="
+}
+
+
+__count_core_items() {
+ echo $(cat /proc/cpuinfo | grep "$1" | sort -u | wc -l)
+}
+
+get_cores__linux() {
+ local cpus=$(__count_core_items "physical id")
+ local cores=$(__count_core_items "core id")
+ echo $((cpus * cores))
+}
+
+get_core_count() {
+ if [ -z "${CORES}" ]; then
+ local cores=1
+ # OSX
+ which sysctl &>/dev/null && cores=$(sysctl -n hw.physicalcpu)
+ # Unix
+ test -f /proc/cpuinfo && cores=$(get_cores__linux)
+ echo $cores
+ else
+ echo ${CORES}
+ fi
+}
+
+__print_usage() {
+ printf "$BLUE"
+ echo "
+*----------------------------------------------------------------------------------------*
+| Experimental parallel testing available |
+| Usage: |
+| export MULTICORE=1 to enable experimental parallel testing. |
+| export CORES=4 to control the number of cores used. Leave unset to utilize all cores. |
+*----------------------------------------------------------------------------------------*
+"
+ printf "$NO_COLOR"
+}
+
+run_test_fast() {
+ echo > .test.log
+ local cores=$(get_core_count)
+ echo "Using $cores cores" 1>&2
+ __print_usage
+ xargs -P"${cores}" -I % /bin/bash -c '"$1"/run-tests "$2" || (echo "$2" >> .test.log && false)' -- "${BASEDIR}" %
+ local exitcode=$?
+ if [ "$exitcode" -ne 0 ]; then
+ printf "$RED"
+ echo "These tests have failed:"
+ cat .test.log | xargs -I % dirname % | xargs /bin/bash -c 'echo $(cd $(dirname "$1") && pwd)/$(basename "$1")' --
+ printf "$NO_COLOR"
+ echo
+ else
+ echo "ALL SYSTEMS GO"
+ fi
+ rm -f .test.log
+ exit "$exitcode"
+}
+
+__cleanup() {
+ echo Cleaning up
+ rm -f .test.log
+}
+
+trap __cleanup SIGINT SIGTERM
+
+# Run all tests
+find {$BASEDIR/../client,$BASEDIR/../server} -name Makefile | run_test_fast
diff --git a/bin/run-tests b/bin/run-tests
new file mode 100755
index 00000000000000..711a048dbb9c74
--- /dev/null
+++ b/bin/run-tests
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+NODE_ENV="test"
+
+for makefile in "$@"; do
+ cat "$makefile" | grep test: > /dev/null
+ if [[ $? -eq 0 ]]; then
+ make -C "$(dirname "$makefile")" --no-print-directory test 2>&1
+ fi
+done
diff --git a/bin/update-dependency b/bin/update-dependency
new file mode 100755
index 00000000000000..b87894c5ed0140
--- /dev/null
+++ b/bin/update-dependency
@@ -0,0 +1,77 @@
+#!/usr/bin/env node
+
+/**
+ * Module dependencies.
+ */
+
+var path = require('path');
+var fs = require('fs');
+var join = path.join;
+var read = fs.readFileSync;
+var write = fs.writeFileSync;
+var readdir = fs.readdirSync;
+
+var dep = process.argv[2];
+var version = process.argv[3];
+if (!dep || !version) {
+ console.log('usage:');
+ console.log(' ' + process.argv.slice(0, 2).join(' ') + ' ');
+ return process.exit(2);
+}
+
+console.log();
+console.log(' updating %s to %s', dep, version);
+var dirs = [
+ 'server',
+ 'client'
+];
+
+function visitPackageFiles(dir, callback) {
+ fs.readdir( dir, function( err, list ) {
+ if (err) {
+ callback( err );
+ }
+
+ list.forEach( function(file) {
+
+ file = join( dir, file );
+
+ if ( /package\.json$/.test( file ) ) {
+ callback( null, file );
+ }
+
+ fs.stat(file, function( err, stat ) {
+ if ( stat && stat.isDirectory() ) {
+ visitPackageFiles( file, callback );
+ }
+ });
+ });
+ });
+}
+
+dirs.forEach( function ( dir ) {
+ visitPackageFiles( dir, function( err, path ) {
+ if ( err ) { console.log( err ); }
+ var conf;
+ try {
+ conf = JSON.parse( read( path, 'utf8' ) );
+ } catch (e) {
+ //console.error('skipping %j: %s', name, e);
+ return;
+ }
+ [ 'dependencies', 'devDependencies' ].forEach(function (dependencies) {
+ if (! conf[dependencies] ) {
+ return;
+ }
+ if (! conf[dependencies][dep] ) {
+ return;
+ }
+ var curr = conf[dependencies][dep];
+ console.log(' updating %j %j %s v%s -> v%s', path, dependencies, dep, curr, version);
+ conf[dependencies][dep] = version;
+ });
+ var json = JSON.stringify(conf, null, 2);
+ write(path, json + '\n');
+ });
+});
+console.log('');
diff --git a/client/README.md b/client/README.md
new file mode 100644
index 00000000000000..bf44ff8d8bacad
--- /dev/null
+++ b/client/README.md
@@ -0,0 +1,31 @@
+Client
+======
+
+This is the heart of Calypso, the client side application. It's pieced together with webpack from different components — both external and internal. ([List of technologies used.](../docs/guide/tech-behind-calypso.md)) It only requires an HTML shell with a body to work with.
+
+### Main modules
+
+These are some of the key modules of the application, kept in `client`'s root for clarity:
+
+* `boot` - the booting file that sets up the application and requires the main sections.
+* `config` - generated configuration settings.
+* `layout` - handles the main React layout, including the masterbar. Notably, it sets #primary and #secondary used to render the different sections.
+* `sections.js` - defines section groups, paths, and main modules. (Used by webpack to generate separate chunks.)
+
+### Components
+
+The `/components` folder holds all the internal React components used to build the Calypso UI across sections. Most of these are rendered in `/devdocs/design` for reference.
+
+### Lib
+
+The `/lib` folder holds internal modules and utilities that power Calypso.
+
+### Core Sections
+
+These represent the top section in the masterbar and handle the controllers for the entire app. Most React components live within their specific section.
+
+* `my-sites`: the site related admin functionality. Akin to wp-admin.
+* `reader`: the home of all Reader sections.
+* `notifications`: the notifications panel.
+* `me`: the sections under the `/me` route.
+
diff --git a/client/accept-invite/actions.js b/client/accept-invite/actions.js
new file mode 100644
index 00000000000000..ee7db65b72fec7
--- /dev/null
+++ b/client/accept-invite/actions.js
@@ -0,0 +1,23 @@
+/**
+ * Internal dependencies
+ */
+import wpcom from 'lib/wp' ;
+
+export function createAccount( userData, callback ) {
+ return wpcom.undocumented().usersNew(
+ Object.assign( {}, userData, { validate: false } ),
+ ( error, response ) => {
+ const bearerToken = response && response.bearer_token;
+ callback( error, bearerToken );
+ }
+ );
+}
+
+export function acceptInvite( invite, bearerToken, callback ) {
+ wpcom.loadToken( bearerToken );
+ return wpcom.undocumented().acceptInvite(
+ invite.blog_id,
+ invite.invite_slug,
+ callback
+ );
+}
diff --git a/client/accept-invite/controller.js b/client/accept-invite/controller.js
new file mode 100644
index 00000000000000..8cb4c1f839cd7d
--- /dev/null
+++ b/client/accept-invite/controller.js
@@ -0,0 +1,24 @@
+/**
+ * External Dependencies
+ */
+import React from 'react';
+
+/**
+ * Internal Dependencies
+ */
+import i18n from 'lib/mixins/i18n';
+import titleActions from 'lib/screen-title/actions';
+import Main from './main';
+
+export default {
+ acceptInvite( context ) {
+ titleActions.setTitle( i18n.translate( 'Accept Invite', { textOnly: true } ) );
+
+ React.unmountComponentAtNode( document.getElementById( 'secondary' ) );
+
+ React.render(
+ React.createElement( Main, context.params ),
+ document.getElementById( 'primary' )
+ );
+ }
+};
diff --git a/client/accept-invite/index.js b/client/accept-invite/index.js
new file mode 100644
index 00000000000000..8984073e6c54e2
--- /dev/null
+++ b/client/accept-invite/index.js
@@ -0,0 +1,16 @@
+/**
+ * External dependencies
+ */
+import page from 'page';
+
+/**
+ * Internal dependencies
+ */
+import controller from './controller';
+
+export default () => {
+ page(
+ '/accept-invite/:site_id/:invitation_key',
+ controller.acceptInvite
+ );
+};
diff --git a/client/accept-invite/invite-form-header/index.jsx b/client/accept-invite/invite-form-header/index.jsx
new file mode 100644
index 00000000000000..0522d212286cda
--- /dev/null
+++ b/client/accept-invite/invite-form-header/index.jsx
@@ -0,0 +1,23 @@
+/**
+ * External dependencies
+ */
+import React from 'react'
+
+export default React.createClass( {
+ displayName: 'InviteFormHeader',
+
+ render() {
+ return (
+
+
+ { this.props.title }
+
+ { this.props.explanation &&
+
+ { this.props.explanation }
+
+ }
+
+ )
+ }
+} );
diff --git a/client/accept-invite/invite-form-header/style.scss b/client/accept-invite/invite-form-header/style.scss
new file mode 100644
index 00000000000000..c2b0bb0d459d28
--- /dev/null
+++ b/client/accept-invite/invite-form-header/style.scss
@@ -0,0 +1,16 @@
+.invite-form-header__title {
+ color: darken( $gray, 30% );
+ font-family: $sans;
+ font-size: 21px;
+ font-weight: 300;
+ line-height: 24px;
+ margin-bottom: 16px;
+}
+
+.invite-form-header__explanation {
+ color: $gray-dark;
+ font-family: $sans;
+ font-size: 14px;
+ line-height: 21px;
+ margin-bottom: 16px;
+}
diff --git a/client/accept-invite/invite-header/index.jsx b/client/accept-invite/invite-header/index.jsx
new file mode 100644
index 00000000000000..6a931b25da3cb6
--- /dev/null
+++ b/client/accept-invite/invite-header/index.jsx
@@ -0,0 +1,66 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+import classNames from 'classnames';
+import { get } from 'lodash';
+
+/**
+ * Internal dependencies
+ */
+import CompactCard from 'components/card/compact';
+import Site from 'my-sites/site';
+import SitePlaceholder from 'my-sites/site/placeholder';
+import Gravatar from 'components/gravatar';
+
+export default React.createClass( {
+ displayName: 'InviteHeader',
+
+ getInvitationVerb() {
+ switch ( get( this.props, 'invite.meta' ) ) {
+ case 'viewer':
+ case 'follower':
+ return this.translate( 'view' );
+ break;
+ default:
+ return this.translate( 'contribute to' );
+ }
+ },
+
+ render() {
+ let classes = classNames( 'invite-header', { 'is-placeholder': ! this.props.invite } );
+ return(
+
+
+
+
+
+ {
+ this.translate( '{{strong}}%(user)s{{/strong}} invited you to %(verb)s:', {
+ args: {
+ user: get(
+ this.props,
+ 'inviter.name',
+ this.translate( 'User', { context: 'Placeholder text while loading an invitation.' } )
+ ),
+ verb: this.getInvitationVerb()
+ },
+ components: {
+ strong:
+ }
+ } )
+ }
+
+
+
+
+ {
+ this.props.blog_details
+ ?
+ :
+ }
+
+
+ );
+ }
+} );
diff --git a/client/accept-invite/invite-header/mock-data.js b/client/accept-invite/invite-header/mock-data.js
new file mode 100644
index 00000000000000..2660dd973cd274
--- /dev/null
+++ b/client/accept-invite/invite-header/mock-data.js
@@ -0,0 +1,33 @@
+module.exports = {
+ invite: {
+ invite_slug: 'asdf2345',
+ blog_id: 1234,
+ user_id: 1234,
+ invited_id: 5678,
+ signed_up: '0000-00-00 00:00:00',
+ invite_date: '2015-11-03 16:45:37',
+ meta: {
+ role: 'editor'
+ }
+ },
+ inviter: {
+ ID: 1234,
+ login: 'testuser',
+ email: false,
+ name: 'Test User',
+ first_name: 'Test',
+ last_name: 'User',
+ URL: 'https://example.com',
+ avatar_URL: 'https://1.gravatar.com/avatar',
+ profile_URL: 'http://en.gravatar.com',
+ site_ID: 1234
+ },
+ blog_details: {
+ domain: 'example.com',
+ title: 'Example WordPress website',
+ icon: {
+ img: 'https://secure.gravatar.com/blavatar',
+ ico: 'https://secure.gravatar.com/blavatar'
+ }
+ }
+}
diff --git a/client/accept-invite/invite-header/style.scss b/client/accept-invite/invite-header/style.scss
new file mode 100644
index 00000000000000..1db8861df80215
--- /dev/null
+++ b/client/accept-invite/invite-header/style.scss
@@ -0,0 +1,36 @@
+.invite-header {
+ .card {
+ &.is-compact {
+ padding: 0px;
+
+ &.invite-header__site {
+ margin-bottom: 24px;
+ }
+ }
+ }
+
+ .invite-header__inviter-info {
+ padding: 16px;
+ display: flex;
+ align-items: center;
+
+ .gravatar {
+ width: 32px;
+ height: 32px;
+ margin-right: 12px;
+ }
+
+ p {
+ margin-bottom: 0;
+ }
+ }
+}
+
+.invite-header.is-placeholder {
+ .invite-header__invited-you-text {
+ @include placeholder();
+
+ // Overriding here to match site placeholder
+ animation: pulse-light 0.8s ease-in-out infinite;
+ }
+}
diff --git a/client/accept-invite/logged-in-accept/index.jsx b/client/accept-invite/logged-in-accept/index.jsx
new file mode 100644
index 00000000000000..e3097b4ef98e1f
--- /dev/null
+++ b/client/accept-invite/logged-in-accept/index.jsx
@@ -0,0 +1,107 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+import classNames from 'classnames';
+
+/**
+ * Internal dependencies
+ */
+import Card from 'components/card';
+import Gravatar from 'components/gravatar';
+import Button from 'components/button';
+import config from 'config';
+import userModule from 'lib/user';
+import InviteFormHeader from '../invite-form-header';
+
+const user = userModule();
+
+const mockData = {
+ invite: {
+ invite_slug: 'asdf2345',
+ blog_id: 1234,
+ user_id: 1234,
+ invited_id: 5678,
+ signed_up: '0000-00-00 00:00:00',
+ invite_date: '2015-11-03 16:45:37',
+ meta: {
+ role: 'editor'
+ }
+ },
+ blog_details: {
+ domain: 'example.com',
+ title: 'Example WordPress website',
+ icon: {
+ img: 'https://secure.gravatar.com/blavatar',
+ ico: 'https://secure.gravatar.com/blavatar'
+ }
+ }
+};
+
+export default React.createClass( {
+
+ displayName: 'LoggedInAccept',
+
+ getInviteRole() {
+ let meta = mockData.invite && mockData.invite.meta ? mockData.invite.meta : false;
+ return meta && meta.role ? meta.role : false;
+ },
+
+ render() {
+ let userObject = user.get(),
+ signInLink = config( 'login_url' ) + '?redirect_to=' + encodeURIComponent( window.location.href );
+
+ return (
+
+
+
+ }
+ } )
+ }
+ explanation={
+ this.translate(
+ 'As an %(siteRole)s you will be able to publish and edit your own posts as well as upload media.', {
+ args: {
+ siteRole: this.getInviteRole()
+ }
+ }
+ )
+ }
+ />
+
+
+ {
+ this.translate( 'Join as {{usernameWrap}}%(username)s{{/usernameWrap}}', {
+ components: {
+ usernameWrap:
+ },
+ args: {
+ username: userObject && userObject.display_name
+ }
+ } )
+ }
+
+
+
+ { this.translate( 'Decline', { context: 'button' } ) }
+
+
+ { this.translate( 'Join', { context: 'button' } ) }
+
+
+
+
+ { this.translate( 'Sign in as a different user' ) }
+
+
+ );
+ }
+} );
diff --git a/client/accept-invite/logged-in-accept/style.scss b/client/accept-invite/logged-in-accept/style.scss
new file mode 100644
index 00000000000000..3117c759026c83
--- /dev/null
+++ b/client/accept-invite/logged-in-accept/style.scss
@@ -0,0 +1,37 @@
+.logged-in-accept__join-as {
+ color: darken( $gray, 30% );
+ font-family: $sans;
+ font-size: 21px;
+ font-weight: 300;
+ line-height: 24px;
+ margin-bottom: 16px;
+ text-align: center;
+}
+
+.logged-in-accept__join-as-username {
+ font-family: $serif;
+ font-weight: 600;
+}
+
+.logged-in-accept__join-as .gravatar {
+ display: block;
+ margin: 0 auto 8px auto;
+}
+
+.logged-in-accept__button-bar {
+ display: flex;
+}
+
+.logged-in-accept__button-bar .button {
+ flex-basis: 0;
+ flex-grow: 1;
+ margin: 0 4px;
+
+ &:first-child {
+ margin-left: 0;
+ }
+
+ &:last-child {
+ margin-right: 0;
+ }
+}
diff --git a/client/accept-invite/logged-out-invite/index.jsx b/client/accept-invite/logged-out-invite/index.jsx
new file mode 100644
index 00000000000000..41d3b049e05e48
--- /dev/null
+++ b/client/accept-invite/logged-out-invite/index.jsx
@@ -0,0 +1,15 @@
+/**
+ * External dependencies
+ */
+import React from 'react'
+
+/**
+ * Internal dependencies
+ */
+import SignupForm from './signup-form'
+
+export default class LoggedOutInvite extends React.Component {
+ render() {
+ return
+ }
+}
diff --git a/client/accept-invite/logged-out-invite/signup-form.jsx b/client/accept-invite/logged-out-invite/signup-form.jsx
new file mode 100644
index 00000000000000..0c5a7630b567ab
--- /dev/null
+++ b/client/accept-invite/logged-out-invite/signup-form.jsx
@@ -0,0 +1,123 @@
+/**
+ * External dependencies
+ */
+import React from 'react'
+import debugModule from 'debug';
+
+/**
+ * Internal dependencies
+ */
+import SignupForm from 'components/signup-form'
+import InviteFormHeader from '../invite-form-header'
+import { createAccount, acceptInvite } from '../actions'
+import WpcomLoginForm from 'signup/wpcom-login-form'
+
+/**
+ * Module variables
+ */
+const debug = debugModule( 'calypso:accept-invite:logged-out' );
+
+export default React.createClass( {
+
+ displayName: 'LoggedOutInviteSignupForm',
+
+ getInitialState() {
+ return { error: false, bearerToken: false, userData: false, submitting: false };
+ },
+
+ getRedirectToAfterLoginUrl() {
+ return '/accept-invite';
+ },
+
+ submitButtonText() {
+ return this.translate( 'Sign Up & Join' );
+ },
+
+ submitForm( form, userData ) {
+ this.setState( { submitting: true } );
+ createAccount(
+ userData,
+ ( error, bearerToken ) =>
+ bearerToken &&
+ acceptInvite(
+ this.props.invite,
+ bearerToken,
+ ( error, response ) => this.setState( { error, userData, bearerToken } )
+ )
+ );
+ },
+
+ getInviteRole() {
+ let meta = this.props.invite && this.props.invite.meta ? this.props.invite.meta : false;
+ return meta && meta.role ? meta.role : false;
+ },
+
+ getFormHeader() {
+ return (
+
+ }
+ } )
+ }
+ explanation={
+ this.translate(
+ 'As an %(siteRole)s you will be able to publish and edit your own posts as well as upload media.', {
+ args: {
+ siteRole: this.getInviteRole()
+ }
+ }
+ )
+ }
+ />
+ );
+ },
+
+ getRedirectTo() {
+ const redirectTo = window.location.origin,
+ { invite } = this.props;
+ switch ( invite.meta.role ) {
+ case 'viewer':
+ case 'follower':
+ return redirectTo;
+ break;
+ default:
+ return redirectTo + '/posts/' + invite.blog_id ;
+ }
+ },
+
+ loginUser() {
+ const { userData, bearerToken } = this.state;
+ return (
+
+ )
+ },
+
+ render() {
+ return (
+
+
+ { this.state.userData && this.loginUser() }
+
+ )
+ }
+
+} );
diff --git a/client/accept-invite/logged-out-invite/style.scss b/client/accept-invite/logged-out-invite/style.scss
new file mode 100644
index 00000000000000..2fb39a8b13c0b8
--- /dev/null
+++ b/client/accept-invite/logged-out-invite/style.scss
@@ -0,0 +1,4 @@
+.logged-out-invite {
+ margin: 0 auto;
+ max-width: 400px;
+}
diff --git a/client/accept-invite/main.jsx b/client/accept-invite/main.jsx
new file mode 100644
index 00000000000000..bbd08ab857c8ba
--- /dev/null
+++ b/client/accept-invite/main.jsx
@@ -0,0 +1,96 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+import Debug from 'debug';
+import classNames from 'classnames';
+
+/**
+ * Internal Dependencies
+ */
+import InviteHeader from './invite-header';
+import LoggedInAccept from './logged-in-accept';
+import LoggedOutInvite from './logged-out-invite';
+import userModule from 'lib/user';
+import InvitesActions from 'lib/invites/actions';
+import InvitesStore from 'lib/invites/stores/invites-validation';
+import EmptyContent from 'components/empty-content';
+
+/**
+ * Module variables
+ */
+const debug = new Debug( 'calypso:accept-invite' );
+const user = userModule();
+
+export default React.createClass( {
+
+ displayName: 'AcceptInvite',
+
+ getInitialState() {
+ return {
+ invite: false,
+ error: false
+ }
+ },
+
+ componentWillMount() {
+ InvitesActions.fetchInvite( this.props.site_id, this.props.invitation_key );
+ InvitesStore.on( 'change', this.refreshInvite );
+ },
+
+ componentWillUnmount() {
+ InvitesStore.off( 'change', this.refreshInvite );
+ },
+
+ refreshInvite() {
+ const invite = InvitesStore.getInvite( this.props.site_id, this.props.invitation_key );
+ const error = InvitesStore.getInviteError( this.props.site_id, this.props.invitation_key );
+ this.setState( { invite, error } );
+ },
+
+ getErrorTitle() {
+ return this.translate(
+ 'Oops, your invite is not valid',
+ { context: 'Title that is display to users when attempting to accept an invalid invite.' }
+ );
+ },
+
+ getErrorMessage() {
+ return this.translate(
+ "We weren't able to verify that invitation.",
+ { context: 'Message that is displayed to users when an invitation is invalid.' }
+ );
+ },
+
+ renderForm() {
+ if ( ! this.state.invite ) {
+ debug( 'Not rendering form - Invite not set' );
+ return null;
+ }
+ debug( 'Rendering invite' );
+ return user.get()
+ ?
+ : ;
+ },
+
+ renderError() {
+ debug( 'Rendering error: ' + JSON.stringify( this.state.error ) );
+ return (
+
+ );
+ },
+
+ render() {
+ let classes = classNames( 'accept-invite', { 'is-error': !! this.state.error } );
+ return (
+
+ { ! this.state.error && }
+ { this.state.error ? this.renderError() : this.renderForm() }
+
+ );
+ }
+} );
diff --git a/client/accept-invite/style.scss b/client/accept-invite/style.scss
new file mode 100644
index 00000000000000..a828490324c1c9
--- /dev/null
+++ b/client/accept-invite/style.scss
@@ -0,0 +1,8 @@
+.accept-invite {
+ margin: 0 auto;
+ max-width: 400px;
+
+ &.is-error {
+ max-width: none;
+ }
+}
diff --git a/client/analytics/Makefile b/client/analytics/Makefile
new file mode 100644
index 00000000000000..69f8ac679ed978
--- /dev/null
+++ b/client/analytics/Makefile
@@ -0,0 +1,11 @@
+REPORTER ?= spec
+MOCHA ?= ../../node_modules/.bin/mocha
+
+# In order to simply stub modules, add test to the NODE_PATH
+test:
+ @NODE_ENV=test NODE_PATH=test:../../client $(MOCHA) --reporter $(REPORTER)
+
+test-w:
+ @NODE_ENV=test NODE_PATH=test:../../client $(MOCHA) --reporter min --watch --growl -b
+
+.PHONY: test
diff --git a/client/analytics/README.md b/client/analytics/README.md
new file mode 100644
index 00000000000000..475ef3f032c8c3
--- /dev/null
+++ b/client/analytics/README.md
@@ -0,0 +1,117 @@
+Analytics
+=========
+
+This module includes functionality for interacting with analytics packages.
+
+Turn on debugging in the JavaScript developer console to view calls being made with the analytics module:
+
+`localStorage.setItem('debug', 'calypso:analytics');`
+
+
+## Which analytics tool should I use?
+
+*Page Views* (`analytics.pageView.record`) should be used to record all page views (when the main content body completely changes). This will automatically record the pageview to both Google Analytics and Tracks.
+
+*Google Analytics* should be used to record all events the user performs on a page that *do not* trigger a page view (this will allow us to determine bounce rate on pages).
+
+We are using GA to monitor user flows through the user interface of Calypso in order learn where they succeed and fail, as well as determine usage of different sections. *Please do not ship anything that does not have GA tracking in place*, otherwise we will create big gaps in our understanding.
+
+Automatticians may refer to internal documentation for more information about MC/Tracks.
+
+# Usage
+
+```js
+// require the module
+var analytics = require( 'analytics' );
+
+```
+
+## PageView Wrapper
+
+### analytics#pageView#record( pageURL, pageTitle )
+
+Record a pageview:
+
+```js
+analytics.pageView.record( '/posts/draft', 'Posts > Drafts' );
+```
+
+
+## Google Analytics API
+
+### analytics#ga#recordPageView( pageURL, pageTitle )
+
+Record a virtual pageview:
+
+```js
+analytics.ga.recordPageView( '/posts/draft', 'Posts > Drafts' );
+```
+
+*Note: Unless you have a strong reason to directly record a pageview to GA, you should use `analytics.pageView.record` instead*
+
+### analytics#ga#recordEvent( eventCategory, eventAction [, optionLabel, optionValue ] )
+
+Record an event:
+
+```js
+analytics.ga.recordEvent( 'Reader', 'Clicked Like' );
+analytics.ga.recordEvent( 'Reader', 'Loaded Next Page', 'page', 2 );
+```
+
+For more information and examples about how and when to provide the optional `optionLabel` and `optionValue` parameters, refer to the [Google Analytics Event Tracking documentation](https://developers.google.com/analytics/devguides/collection/analyticsjs/events#overview).
+
+## Tracks API
+
+### analytics#tracks#recordEvent( eventName, eventProperties )
+
+Record an event with optional properties:
+
+```js
+analytics.tracks.recordEvent( 'calypso_checkout_coupon_apply', {
+ 'coupon_code': 'abc123'
+} );
+```
+
+## MC API
+
+### analytics#mc#bumpStat( group, name )
+
+Bump a single WP.com stat.
+
+```js
+analytics.mc.bumpStat( 'newdash_visits', 'sites' );
+```
+
+### analytics#mc#bumpStat( obj )
+
+Bump multiple WP.com stats:
+
+```js
+analytics.mc.bumpStat( {
+ 'stat_name1': 'stat_value1',
+ 'statname2': 'stat_value2'
+} );
+```
+
+## Google Analytics Naming Conventions
+
+For page view tracking, you should never pass a dynamic URL, or anything including a specific domain e.g. (/posts/apeatling.wordpress.com). Always pass a placeholder in these instances (/posts/:site). If you do not do this, we cannot accurately track views for a specific page. Titles should always use a ` > ` to break up the hierarchy of the page title. Examples are `Posts > My > Drafts`, `Reader > Following > Edit`, `Sharing > Connections`.
+
+Events should be categorized by the section they are in. Examples are `Posts`, `Pages`, `Reader`, `Sharing`. Event actions should be written in readable form, and action centric. Good examples are `Clicked Save Button`, `Clicked Like`, `Activated Theme`.
+
+## Tracks Naming Conventions
+
+Event names should be prefixed by `calypso_` to make it easy to easy to identify when analyzing the data with our various analytics tools.
+
+Each token in the event and property names should be separated by an underscore (`_`), not spaces or dashes.
+
+In order to keep similar events grouped together when output in an alphabetized list (as is typical with ananlytics tools), put the verb at _the end_ of the event name:
+
+* `calypso_cart_product_add`
+* `calypso_cart_product_remove`
+
+If we had instead used `calypso_add_cart_product` and `calypso_remove_cart_product` for example, then they'd likely be separated in a list of all the event names.
+
+Finally, for consistency, the verb at the end should not be in the form `add`, `remove`, `view`, `click`, etc and _not_ `adds`, `added`, `adding`, etc.
+
+With the exception of separating tokens with underscores, these rules do not apply to property names. `coupon_code` is perfectly fine.
diff --git a/client/analytics/ad-tracking.js b/client/analytics/ad-tracking.js
new file mode 100644
index 00000000000000..5158284d5a5ea7
--- /dev/null
+++ b/client/analytics/ad-tracking.js
@@ -0,0 +1,189 @@
+/**
+ * External dependencies
+ */
+const async = require( 'async' ),
+ noop = require( 'lodash/utility/noop' ),
+ map = require( 'lodash/collection/map' ),
+ some = require( 'lodash/collection/some' ),
+ debug = require( 'debug' )( 'calypso:ad-tracking' );
+
+/**
+ * Internal dependencies
+ */
+const loadScript = require( 'lib/load-script' ),
+ config = require( 'config' ),
+ isPremium = require( 'lib/products-values' ).isPremium,
+ isBusiness = require( 'lib/products-values' ).isBusiness;
+
+/**
+ * Module variables
+ */
+let hasLoadedScripts = false,
+ retargetingInitialized = false;
+
+/**
+ * Constants
+ */
+const FACEBOOK_TRACKING_SCRIPT_URL = 'https://connect.facebook.net/en_US/fbds.js',
+ GOOGLE_TRACKING_SCRIPT_URL = 'https://www.googleadservices.com/pagead/conversion_async.js',
+ BING_TRACKING_SCRIPT_URL = 'https://bat.bing.com/bat.js',
+ GOOGLE_CONVERSION_ID = config( 'google_adwords_conversion_id' ),
+ TRACKING_IDS = {
+ freeSignup: {
+ facebook: '6024523283021',
+ google: 'd-fNCIe7m1wQ1uXz_AM'
+ },
+
+ premiumTrial: {
+ facebook: '6028365445821',
+ google: '_q3ECJ--m1wQ1uXz_AM'
+ },
+
+ premiumSignup: {
+ facebook: '6028365447021',
+ google: 'UMSeCIyYmFwQ1uXz_AM',
+ bing: '4074038'
+ },
+
+ businessTrial: {
+ facebook: '6028365448821',
+ google: 'm9zRCNO8m1wQ1uXz_AM'
+ },
+
+ businessSignup: {
+ facebook: '6028365461821',
+ google: 'JxKBCKK-m1wQ1uXz_AM',
+ bing: '4074039'
+ },
+
+ retargeting: '823166884443641'
+ };
+
+/**
+ * Globals
+ */
+if ( ! window._fbq ) {
+ window._fbq = []; // Facebook global
+}
+
+if ( ! window.uetq ) {
+ window.uetq = []; // Bing global
+}
+
+function loadTrackingScripts( callback ) {
+ async.parallel( [
+ function( onComplete ) {
+ loadScript.loadScript( FACEBOOK_TRACKING_SCRIPT_URL, onComplete );
+ },
+ function( onComplete ) {
+ loadScript.loadScript( GOOGLE_TRACKING_SCRIPT_URL, onComplete );
+ },
+ function( onComplete ) {
+ loadScript.loadScript( BING_TRACKING_SCRIPT_URL, onComplete );
+ }
+ ], function( errors ) {
+ if ( ! some( errors ) ) {
+ hasLoadedScripts = true;
+
+ // update Facebook's tracking global
+ window._fbq.loaded = true;
+ window._fbq.push( [ 'addPixelId', TRACKING_IDS.retargeting ] );
+
+ if ( typeof callback === 'function' ) {
+ callback();
+ }
+ } else {
+ debug( 'Some scripts failed to load: ', errors );
+ }
+ } );
+}
+
+function retarget() {
+ if ( ! hasLoadedScripts ) {
+ return loadTrackingScripts( retarget );
+ }
+
+ if ( ! retargetingInitialized ) {
+ debug( 'Retargeting initialized' );
+
+ window._fbq.push( [ 'track', 'PixelInitialized', {} ] );
+ retargetingInitialized = true;
+ }
+}
+
+function recordPurchase( product ) {
+ let type;
+
+ if ( ! hasLoadedScripts ) {
+ return loadTrackingScripts( function() {
+ recordPurchase( type );
+ } );
+ }
+
+ if ( isPremium( product ) ) {
+ if ( 0 === product.cost ) {
+ type = 'premiumTrial';
+ } else {
+ type = 'premiumSignup';
+ }
+ }
+
+ if ( isBusiness( product ) ) {
+ if ( 0 === product.cost ) {
+ type = 'businessTrial';
+ } else {
+ type = 'businessSignup';
+ }
+ }
+
+ if ( ! type ) {
+ return;
+ }
+
+ debug( 'Recorded purchase', type );
+
+ // record the purchase w/ Facebook
+ window._fbq.push( [
+ 'track',
+ TRACKING_IDS[ type ].facebook,
+ {
+ value: '0.00',
+ currency: 'USD'
+ }
+ ] );
+
+ // record the purchase w/ Google
+ window.google_trackConversion( {
+ google_conversion_id: GOOGLE_CONVERSION_ID,
+ google_conversion_label: TRACKING_IDS[ type ].google,
+ google_remarketing_only: false
+ } );
+
+ // record the purchase w/ Bing if a tracking ID is present
+ if ( TRACKING_IDS[ type ].bing ) {
+ window.uetq = new UET( {
+ ti: TRACKING_IDS[ type ].bing,
+ o: window.uetq
+ } );
+ window.uetq.push( 'pageLoad' );
+ window.uetq.push( { ec: 'conversion' } );
+ }
+}
+
+module.exports = {
+ retarget: function( context, next ) {
+ const nextFunction = typeof next === 'function' ? next : noop;
+
+ if ( config.isEnabled( 'ad-tracking' ) ) {
+ retarget();
+ }
+
+ nextFunction();
+ },
+
+ recordPurchases: function( products ) {
+ if ( config.isEnabled( 'ad-tracking' ) ) {
+ map( products, recordPurchase );
+ }
+ }
+};
diff --git a/client/analytics/index.js b/client/analytics/index.js
new file mode 100644
index 00000000000000..9b14aeec4665ca
--- /dev/null
+++ b/client/analytics/index.js
@@ -0,0 +1,199 @@
+/**
+ * External dependencies
+ */
+var debug = require( 'debug' )( 'calypso:analytics' ),
+ assign = require( 'lodash/object/assign' );
+
+/**
+ * Internal dependencies
+ */
+var config = require( 'config' ),
+ loadScript = require( 'lib/load-script' ).loadScript,
+ _superProps,
+ _user;
+
+// Load tracking scripts
+window._tkq = window._tkq || [];
+window.ga = window.ga || function() {
+ ( window.ga.q = window.ga.q || [] ).push( arguments );
+ };
+window.ga.l = +new Date();
+
+loadScript( '//stats.wp.com/w.js?48' );
+loadScript( '//www.google-analytics.com/analytics.js' );
+
+function buildQuerystring( group, name ) {
+ var uriComponent = '';
+
+ if ( 'object' === typeof group ) {
+ for ( var key in group ) {
+ uriComponent += '&x_' + encodeURIComponent( key ) + '=' + encodeURIComponent( group[ key ] );
+ }
+ debug( 'Bumping stats %o', group );
+ } else {
+ uriComponent = '&x_' + encodeURIComponent( group ) + '=' + encodeURIComponent( name );
+ debug( 'Bumping stat "%s" in group "%s"', name, group );
+ }
+
+ return uriComponent;
+}
+
+function buildQuerystringNoPrefix( group, name ) {
+ var uriComponent = '';
+
+ if ( 'object' === typeof group ) {
+ for ( var key in group ) {
+ uriComponent += '&' + encodeURIComponent( key ) + '=' + encodeURIComponent( group[ key ] );
+ }
+ debug( 'Built stats %o', group );
+ } else {
+ uriComponent = '&' + encodeURIComponent( group ) + '=' + encodeURIComponent( name );
+ debug( 'Built stat "%s" in group "%s"', name, group );
+ }
+
+ return uriComponent;
+}
+
+var analytics = {
+
+ initialize: function( user, superProps ) {
+ analytics.setUser( user );
+ analytics.setSuperProps( superProps );
+ analytics.identifyUser();
+ },
+
+ setUser: function( user ) {
+ _user = user;
+ },
+
+ setSuperProps: function( superProps ) {
+ _superProps = superProps;
+ },
+
+ mc: {
+ bumpStat: function( group, name ) {
+ var uriComponent = buildQuerystring( group, name ); // prints debug info
+ if ( config( 'mc_analytics_enabled' ) ) {
+ new Image().src = document.location.protocol + '//pixel.wp.com/g.gif?v=wpcom-no-pv' + uriComponent + '&t=' + Math.random();
+ }
+ },
+
+ bumpStatWithPageView: function( group, name ) {
+ // this function is fairly dangerous, as it bumps page views for wpcom and should only be called in very specific cases.
+ var uriComponent = buildQuerystringNoPrefix( group, name ); // prints debug info
+ if ( config( 'mc_analytics_enabled' ) ) {
+ new Image().src = document.location.protocol + '//pixel.wp.com/g.gif?v=wpcom' + uriComponent + '&t=' + Math.random();
+ }
+ }
+ },
+
+ // pageView is a wrapper for pageview events across Tracks and GA
+ pageView: {
+ record: function( urlPath, pageTitle ) {
+ analytics.tracks.recordPageView( urlPath );
+ analytics.ga.recordPageView( urlPath, pageTitle );
+ }
+ },
+
+ tracks: {
+ recordEvent: function( eventName, eventProperties ) {
+ var superProperties;
+
+ eventProperties = eventProperties || {};
+
+ debug( 'Record event "%s" called with props %s', eventName, JSON.stringify( eventProperties ) );
+
+ if ( eventName.indexOf( 'calypso_' ) !== 0 ) {
+ debug( '- Event name must be prefixed by "calypso_"' );
+ return;
+ }
+
+ if ( _superProps ) {
+ superProperties = _superProps.getAll();
+ debug( '- Super Props: %o', superProperties );
+ eventProperties = assign( eventProperties, superProperties );
+ }
+
+ window._tkq.push( [ 'recordEvent', eventName, eventProperties ] );
+ },
+
+ recordPageView: function( urlPath ) {
+ analytics.tracks.recordEvent( 'calypso_page_view', {
+ 'path': urlPath
+ } );
+ }
+ },
+
+ // Google Analytics usage and event stat tracking
+ ga: {
+
+ initialized: false,
+
+ initialize: function() {
+ var parameters = {};
+ if ( ! analytics.ga.initialized ) {
+ if ( _user && _user.get() ) {
+ parameters = {
+ 'userId': 'u-' + _user.get().ID
+ };
+ }
+ window.ga( 'create', config( 'google_analytics_key' ), 'auto', parameters );
+ analytics.ga.initialized = true;
+ }
+ },
+
+ recordPageView: function( urlPath, pageTitle ) {
+ analytics.ga.initialize();
+
+ debug( 'Recording Page View ~ [URL: ' + urlPath + '] [Title: ' + pageTitle + ']' );
+
+ if ( config( 'google_analytics_enabled' ) ) {
+ // Set the current page so all GA events are attached to it.
+ window.ga( 'set', 'page', urlPath );
+
+ window.ga( 'send', {
+ 'hitType': 'pageview',
+ 'page': urlPath,
+ 'title': pageTitle
+ } );
+ }
+ },
+
+ recordEvent: function( category, action, label, value ) {
+ analytics.ga.initialize();
+
+ var debugText = 'Recording Event ~ [Category: ' + category + '] [Action: ' + action + ']';
+
+ if ( 'undefined' !== typeof label ) {
+ debugText += ' [Option Label: ' + label + ']';
+ }
+
+ if ( 'undefined' !== typeof value ) {
+ debugText += ' [Option Value: ' + value + ']';
+ }
+
+ debug( debugText );
+
+ if ( config( 'google_analytics_enabled' ) ) {
+ window.ga( 'send', 'event', category, action, label, value );
+ }
+ }
+ },
+
+ identifyUser: function() {
+ // Don't identify the user if we don't have one
+ if ( _user && _user.initialized ) {
+ window._tkq.push( [ 'identifyUser', _user.get().ID, _user.get().username ] );
+ }
+ },
+
+ setProperties: function( properties ) {
+ window._tkq.push( [ 'setProperties', properties ] );
+ },
+
+ clearedIdentity: function() {
+ window._tkq.push( [ 'clearIdentity' ] );
+ }
+};
+
+module.exports = analytics;
diff --git a/client/analytics/super-props.js b/client/analytics/super-props.js
new file mode 100644
index 00000000000000..eb45b4999a5806
--- /dev/null
+++ b/client/analytics/super-props.js
@@ -0,0 +1,38 @@
+/**
+ * External dependencies
+ */
+var config = require( 'config' ),
+ assign = require( 'lodash/object/assign' );
+/**
+ * Internal dependencies
+ */
+var sites = require( 'lib/sites-list' )();
+
+module.exports = {
+ getAll: function() {
+ var selectedSite = sites.getSelectedSite(),
+ siteProps = {},
+ defaultProps = {
+ environment: config( 'env' ),
+ site_count: sites.data.length,
+ site_id_label: 'wpcom',
+ client: config( 'tracks_client_prop' )
+ };
+
+ if ( selectedSite ) {
+ siteProps = {
+
+ // Tracks expects a blog_id property to identify the blog which is
+ // why we use it here instead of calling the property site_id
+ blog_id: selectedSite.ID,
+
+ site_id_label: selectedSite.jetpack ? 'jetpack' : 'wpcom',
+ site_language: selectedSite.lang,
+ site_plan_id: selectedSite.plan ? selectedSite.plan.product_id : null,
+ site_post_count: selectedSite.post_count
+ };
+ }
+
+ return assign( defaultProps, siteProps );
+ }
+};
diff --git a/client/analytics/test/analytics-tests.js b/client/analytics/test/analytics-tests.js
new file mode 100644
index 00000000000000..8b23baf282893b
--- /dev/null
+++ b/client/analytics/test/analytics-tests.js
@@ -0,0 +1,68 @@
+var expect = require( 'chai' ).expect,
+ url = require( 'url' );
+
+require( 'lib/react-test-env-setup' )();
+
+var imagesLoaded = [];
+global.Image = function() {
+ this._src = '';
+};
+
+Object.defineProperty( global.Image.prototype, 'src', {
+ get: function() {
+ return this._src;
+ },
+ set: function( value ) {
+ this._src = value;
+ imagesLoaded.push( url.parse( value, true, true ) );
+ }
+} );
+
+var analytics = require( '../' );
+
+describe( 'Analytics', function() {
+
+ beforeEach( function() {
+ imagesLoaded = [];
+ } );
+
+ describe( 'mc', function() {
+ it( 'bumpStat with group and stat', function() {
+ analytics.mc.bumpStat( 'go', 'time' );
+ expect( imagesLoaded[ 0 ].query.v ).to.eql( 'wpcom-no-pv' );
+ expect( imagesLoaded[ 0 ].query.x_go ).to.eql( 'time' );
+ expect( imagesLoaded[ 0 ].query.t ).to.be.ok;
+ } );
+
+ it( 'bumpStat with value object', function() {
+ analytics.mc.bumpStat( {
+ go: 'time',
+ another: 'one'
+ } );
+ expect( imagesLoaded[ 0 ].query.v ).to.eql( 'wpcom-no-pv' );
+ expect( imagesLoaded[ 0 ].query.x_go ).to.eql( 'time' );
+ expect( imagesLoaded[ 0 ].query.x_another ).to.eql( 'one' );
+ expect( imagesLoaded[ 0 ].query.t ).to.be.ok;
+ } );
+
+ it( 'bumpStatWithPageView with group and stat', function() {
+ analytics.mc.bumpStatWithPageView( 'go', 'time' );
+ expect( imagesLoaded[ 0 ].query.v ).to.eql( 'wpcom' );
+ expect( imagesLoaded[ 0 ].query.go ).to.eql( 'time' );
+ expect( imagesLoaded[ 0 ].query.t ).to.be.ok;
+ } );
+
+ it( 'bumpStatWithPageView with value object', function() {
+ analytics.mc.bumpStatWithPageView( {
+ go: 'time',
+ another: 'one'
+ } );
+ expect( imagesLoaded[ 0 ].query.v ).to.eql( 'wpcom' );
+ expect( imagesLoaded[ 0 ].query.go ).to.eql( 'time' );
+ expect( imagesLoaded[ 0 ].query.another ).to.eql( 'one' );
+ expect( imagesLoaded[ 0 ].query.t ).to.be.ok;
+ } );
+
+ } );
+
+} );
diff --git a/client/analytics/test/config.js b/client/analytics/test/config.js
new file mode 100644
index 00000000000000..9243cace95851b
--- /dev/null
+++ b/client/analytics/test/config.js
@@ -0,0 +1,7 @@
+module.exports = function( key ) {
+ if ( key === 'mc_analytics_enabled' ) {
+ return true;
+ }
+
+ throw new Error( 'key ' + key + ' not expected to be needed' );
+};
diff --git a/client/analytics/test/load-script.js b/client/analytics/test/load-script.js
new file mode 100644
index 00000000000000..1e7402b9400821
--- /dev/null
+++ b/client/analytics/test/load-script.js
@@ -0,0 +1,10 @@
+function fakeLoader( url, callback ) {
+ fakeLoader.urlsLoaded.push( url );
+ if ( callback ) {
+ setTimeout( function() { callback(); }, 0 );
+ }
+}
+
+fakeLoader.urlsLoaded = [];
+
+module.exports = { loadScript: fakeLoader };
diff --git a/client/auth/Makefile b/client/auth/Makefile
new file mode 100644
index 00000000000000..dc5bb4623e3f5c
--- /dev/null
+++ b/client/auth/Makefile
@@ -0,0 +1,7 @@
+REPORTER ?= spec
+MOCHA ?= ../../node_modules/.bin/mocha
+
+test:
+ @NODE_ENV=test NODE_PATH=test:../../client:../../shared $(MOCHA) --compilers jsx:babel/register --reporter $(REPORTER)
+
+.PHONY: test
diff --git a/client/auth/controller.js b/client/auth/controller.js
new file mode 100644
index 00000000000000..bc585935fe85b6
--- /dev/null
+++ b/client/auth/controller.js
@@ -0,0 +1,34 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+import startsWith from 'lodash/string/startsWith';
+import page from 'page';
+
+/**
+ * Internal dependencies
+ */
+import LoginComponent from './login';
+import * as OAuthToken from 'lib/oauth-token';
+
+module.exports = {
+ login: function() {
+ if ( OAuthToken.getToken() ) {
+ page( '/' );
+ } else {
+ React.render(
+ React.createElement( LoginComponent, {} ),
+ document.getElementById( 'primary' )
+ );
+ }
+ },
+
+ checkToken: function( context, next ) {
+ // Check we have an OAuth token, otherwise redirect to login page
+ if ( OAuthToken.getToken() === false && ! startsWith( context.path, '/login' ) && ! startsWith( context.path, '/oauth' ) ) {
+ page( '/login' );
+ } else {
+ next();
+ }
+ }
+};
diff --git a/client/auth/index.js b/client/auth/index.js
new file mode 100644
index 00000000000000..40e202b11e3ba5
--- /dev/null
+++ b/client/auth/index.js
@@ -0,0 +1,13 @@
+/**
+ * External dependencies
+ */
+import page from 'page';
+
+/**
+ * Internal dependencies
+ */
+import controller from './controller';
+
+module.exports = function() {
+ page( '/login', controller.login );
+};
diff --git a/client/auth/login.jsx b/client/auth/login.jsx
new file mode 100644
index 00000000000000..9f76e3d1ec6d23
--- /dev/null
+++ b/client/auth/login.jsx
@@ -0,0 +1,156 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+
+/**
+ * Internal dependencies
+ */
+import Main from 'components/main';
+import FormTextInput from 'components/forms/form-text-input';
+import FormPasswordInput from 'components/forms/form-password-input';
+import FormFieldset from 'components/forms/form-fieldset';
+import FormButton from 'components/forms/form-button';
+import FormButtonsBar from 'components/forms/form-buttons-bar';
+import Notice from 'notices/notice';
+import AuthStore from 'lib/oauth-store';
+import * as AuthActions from 'lib/oauth-store/actions';
+import eventRecorder from 'me/event-recorder';
+import Gridicon from 'components/gridicon';
+import WordPressLogo from 'components/wordpress-logo';
+
+const LostPassword = React.createClass( {
+ render: function() {
+ return (
+
+
+ { this.translate( 'Lost your password?' ) }
+
+
+ );
+ }
+} );
+
+module.exports = React.createClass( {
+ displayName: 'Auth',
+
+ mixins: [ React.addons.LinkedStateMixin, eventRecorder ],
+
+ componentDidMount: function() {
+ AuthStore.on( 'change', this.refreshData );
+ },
+
+ componentWillUnmount: function() {
+ AuthStore.off( 'change', this.refreshData );
+ },
+
+ refreshData: function() {
+ this.setState( AuthStore.get() );
+ },
+
+ componentDidUpdate() {
+ if ( this.state.requires2fa && this.state.inProgress === false ) {
+ this.refs.auth_code.getDOMNode().focus();
+ }
+ },
+
+ getInitialState: function() {
+ return Object.assign( {
+ login: '',
+ password: '',
+ auth_code: ''
+ }, AuthStore.get() );
+ },
+
+ submitForm: function( event ) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ AuthActions.login( this.state.login, this.state.password, this.state.auth_code );
+ },
+
+ hasLoginDetails: function() {
+ if ( this.state.login === '' || this.state.password === '' ) {
+ return false;
+ }
+
+ return true;
+ },
+
+ canSubmitForm: function() {
+ // No submission until the ajax has finished
+ if ( this.state.inProgress ) {
+ return false;
+ }
+
+ // If we have 2fa set then don't allow submission until a code is entered
+ if ( this.state.requires2fa ) {
+ return parseInt( this.state.auth_code, 10 ) > 0;
+ }
+
+ // Don't allow submission until username+password is entered
+ return this.hasLoginDetails();
+ },
+
+ render: function() {
+ const { requires2fa, inProgress, errorMessage, errorLevel } = this.state;
+
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+} );
diff --git a/client/auth/style.scss b/client/auth/style.scss
new file mode 100644
index 00000000000000..69da7939404984
--- /dev/null
+++ b/client/auth/style.scss
@@ -0,0 +1,161 @@
+/**
+ * Oauth login page, used in WordPress Desktop App
+ */
+
+html.is-desktop {
+ .logged-out-auth {
+ background: $blue-wordpress;
+
+ .wp-content {
+ margin: 0 auto;
+ overflow: hidden;
+ max-width: none;
+ padding-left: 0;
+ padding-right: 0;
+ }
+
+ .wp-content:after {
+ content: none;
+ }
+ }
+}
+
+.auth.main {
+ float: none;
+ height: 100%;
+ margin: 0 auto;
+ text-align: center;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-content: center;
+ background: none;
+
+ .notice {
+ width: calc( 100% - 20px );
+ margin: 20px auto 0 auto;
+ }
+
+ .notice.is-info {
+ color: $gray-light;
+ padding: 0;
+ margin: 20px 0 0 0;
+ }
+
+ .notice.is-info .notice__text {
+ padding: 0;
+ }
+
+ .notice.is-info:before {
+ content: none;
+ }
+}
+
+.auth__form {
+ width: 320px;
+ margin: 0 auto;
+
+ .form-fieldset input {
+ position: relative;
+ z-index: 1;
+ }
+
+ .form-fieldset input[type="text"],
+ .form-fieldset input[type="password"] {
+ padding-left: 36px;
+ }
+
+ .form-fieldset input:focus {
+ z-index: 2;
+ }
+
+ .form-password-input {
+ margin-top: -1px;
+ }
+
+ .form-buttons-bar button {
+ float: none;
+ margin: 0;
+ width: calc( 100% - 20px );
+ }
+
+ .form-buttons-bar button,
+ .form-buttons-bar .button.is-primary[disabled] {
+ background: #00a8db;
+ border: #00a8db;
+ }
+
+ .form-buttons-bar .button.is-primary[disabled],
+ .form-fieldset input[type="password"]:disabled,
+ .form-fieldset input[type="text"]:disabled {
+ opacity: .5;
+ }
+
+ .form-fieldset input[type="text"]:disabled {
+ margin-bottom: 1px;
+ }
+
+ .form-fieldset input[type="number"] {
+ text-align: center;
+ }
+
+ .form-password-input__toggle-visibility {
+ z-index: 4;
+ .gridicon {
+ position: static;
+ }
+ }
+
+ input[type="number"]::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+ }
+}
+
+.auth__input-wrapper {
+ position: relative;
+
+ .gridicon {
+ position: absolute;
+ z-index: 3;
+ left: 8px;
+ top: 7px;
+ fill: $gray;
+ }
+}
+
+.auth__lost-password {
+ margin-top: 1.5em;
+ text-align: center;
+}
+
+.auth__lost-password a {
+ color: lighten( $gray, 20% );
+}
+
+// should be a new component
+.wordpress-logo {
+ fill: $white;
+ margin: 0 auto 20px auto;
+}
+
+.auth__help {
+ position: absolute;
+ top: 16px;
+ right: 16px;
+ color: $white;
+}
+
+.auth__links {
+ position: absolute;
+ bottom: 40px;
+ left: 0;
+ right: 0;
+}
+
+.auth__links a {
+ text-decoration: none;
+ color: $white;
+ margin: 0 16px;
+ padding: 10px 0;
+}
diff --git a/client/auth/test/login.jsx b/client/auth/test/login.jsx
new file mode 100644
index 00000000000000..ed46a791e697a7
--- /dev/null
+++ b/client/auth/test/login.jsx
@@ -0,0 +1,74 @@
+/* eslint-disable vars-on-top */
+require( 'lib/react-test-env-setup' )();
+
+/**
+ * External dependencies
+ */
+
+const React = require( 'react/addons' ),
+ i18n = require( 'lib/mixins/i18n' ),
+ expect = require( 'chai' ).expect,
+ sinon = require( 'sinon' ),
+ ReactInjection = require( 'react/lib/ReactInjection' ),
+ TestUtils = React.addons.TestUtils;
+
+/**
+ * Internal dependencies
+ */
+const AuthActions = require( 'lib/oauth-store/actions' );
+
+// Handle initialization here instead of in `before()` to avoid timeouts. See client/post-editor/test/post-editor.jsx
+i18n.initialize();
+ReactInjection.Class.injectMixin( i18n.mixin );
+
+let Login = require( '../login.jsx' );
+const page = React.render( , document.body );
+
+describe( 'LoginTest', function() {
+ it( 'OTP is not present on first render', function( done ) {
+ page.setState( { requires2fa: false }, function() {
+ expect( page.refs.auth_code ).to.be.undefined;
+ done();
+ } );
+ } );
+
+ it( 'cannot submit until login details entered', function( done ) {
+ var submit = TestUtils.findRenderedDOMComponentWithTag( page, 'button' );
+
+ page.setState( { login: 'test', password: 'test', inProgress: false }, function() {
+ expect( submit.props.disabled ).to.be.false;
+ done();
+ } );
+ } );
+
+ it( 'shows OTP box with valid login', function( done ) {
+ page.setState( { login: 'test', password: 'test', requires2fa: true }, function() {
+ expect( page.refs.auth_code ).to.not.be.undefined;
+ done();
+ } );
+ } );
+
+ it( 'prevents change of login when asking for OTP', function( done ) {
+ page.setState( { login: 'test', password: 'test', requires2fa: true }, function() {
+ expect( page.refs.login.props.disabled ).to.be.true;
+ expect( page.refs.password.props.disabled ).to.be.true;
+ done();
+ } );
+ } );
+
+ it( 'submits login form', function( done ) {
+ var submit = TestUtils.findRenderedDOMComponentWithTag( page, 'form' );
+
+ sinon.stub( AuthActions, 'login' );
+
+ page.setState( { login: 'user', password: 'pass', auth_code: 'otp' }, function() {
+ TestUtils.Simulate.submit( submit );
+
+ expect( AuthActions.login ).to.have.been.calledOnce;
+ expect( AuthActions.login.calledWith( 'user', 'pass', 'otp' ) ).to.be.true;
+
+ AuthActions.login.restore();
+ done();
+ } );
+ } );
+} );
diff --git a/client/boot/README.md b/client/boot/README.md
new file mode 100644
index 00000000000000..5ad46c5f32495e
--- /dev/null
+++ b/client/boot/README.md
@@ -0,0 +1,21 @@
+Boot
+======
+
+This module is where all the client-side magic starts. The core set of application modules that contain the client-side application routes, any additional application bootstrapping occurs, and the page.js router is started triggering the initial route handler.
+
+Boot is responsible for handling a lot of features we want to happen on every page. The major ones are (in roughly chronological order):
+
+- Initializing i18n
+- Injecting the i18n mixin to React components
+- Adding touch events
+- Adding accessibility focus features
+- Setting the document title
+- Passing layout to all page handlers
+- Passing the query and hash objects into the page.js context
+- Boot strapping translation strings
+- Setting up analytics
+- Rendering the main layout template
+- Focussing parts of the layout based on the query string
+- Handling query strings
+- Loading various helpers (olark, keyboard shortcuts, network connection)
+- Starting page.js
diff --git a/client/boot/index.js b/client/boot/index.js
new file mode 100644
index 00000000000000..426325eccad02c
--- /dev/null
+++ b/client/boot/index.js
@@ -0,0 +1,321 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ store = require( 'store' ),
+ ReactInjection = require( 'react/lib/ReactInjection' ),
+ some = require( 'lodash/collection/some' ),
+ startsWith = require( 'lodash/string/startsWith' ),
+ classes = require( 'component-classes' ),
+ debug = require( 'debug' )( 'calypso' ),
+ page = require( 'page' ),
+ url = require( 'url' ),
+ qs = require( 'querystring' ),
+ injectTapEventPlugin = require( 'react-tap-event-plugin' );
+
+/**
+ * Internal dependencies
+ */
+// lib/local-storage must be run before lib/user
+var config = require( 'config' ),
+ localStoragePolyfill = require( 'lib/local-storage' )(), //eslint-disable-line
+ analytics = require( 'analytics' ),
+ route = require( 'lib/route' ),
+ user = require( 'lib/user' )(),
+ sites = require( 'lib/sites-list' )(),
+ superProps = require( 'analytics/super-props' ),
+ config = require( 'config' ),
+ i18n = require( 'lib/mixins/i18n' ),
+ translatorJumpstart = require( 'lib/translator-jumpstart' ),
+ translatorInvitation = require( 'layout/community-translator/invitation-utils' ),
+ layoutFocus = require( 'lib/layout-focus' ),
+ nuxWelcome = require( 'nux-welcome' ),
+ emailVerification = require( 'components/email-verification' ),
+ viewport = require( 'lib/viewport' ),
+ detectHistoryNavigation = require( 'lib/detect-history-navigation' ),
+ sections = require( 'sections' ),
+ touchDetect = require( 'lib/touch-detect' ),
+ accessibleFocus = require( 'lib/accessible-focus' ),
+ TitleStore = require( 'lib/screen-title/store' ),
+ // The following mixins require i18n content, so must be required after i18n is initialized
+ Layout,
+ LoggedOutLayout;
+
+function init() {
+ var i18nLocaleStringsObject = null;
+
+ debug( 'Starting Calypso. Let\'s do this.' );
+
+ // Initialize i18n
+ if ( window.i18nLocaleStrings ) {
+ i18nLocaleStringsObject = JSON.parse( window.i18nLocaleStrings );
+ }
+ i18n.initialize( i18nLocaleStringsObject );
+
+ ReactInjection.Class.injectMixin( i18n.mixin );
+
+ // Infer touch screen by checking if device supports touch events
+ // See touch-detect/README.md
+ if ( touchDetect.hasTouch() ) {
+ classes( document.documentElement ).add( 'touch' );
+ } else {
+ classes( document.documentElement ).add( 'notouch' );
+ }
+
+ // Initialize touch
+ injectTapEventPlugin();
+
+ // Add accessible-focus listener
+ accessibleFocus();
+
+ // Set document title
+ TitleStore.on( 'change', function() {
+ var title = TitleStore.getState().formattedTitle;
+ if ( title && title !== document.title ) {
+ document.title = title;
+ }
+ } );
+}
+
+function setUpContext( layout ) {
+ // Pass the layout so that it is available to all page handlers
+ // and add query and hash objects onto context object
+ page( '*', function( context, next ) {
+ var parsed = url.parse( location.href, true );
+
+ context.layout = layout;
+
+ // Break routing and do full page load for logout link in /me
+ if ( context.pathname === '/wp-login.php' ) {
+ window.location.href = context.path;
+ return;
+ }
+
+ // set `context.query`
+ // debugger
+ const querystringStart = context.canonicalPath.indexOf( '?' );
+ if ( querystringStart !== -1 ) {
+ context.query = qs.parse( context.canonicalPath.substring( querystringStart + 1 ) );
+ } else {
+ context.query = {};
+ }
+ context.prevPath = parsed.path === context.path ? false : parsed.path;
+
+ // set `context.hash` (we have to parse manually)
+ if ( parsed.hash && parsed.hash.length > 1 ) {
+ try {
+ context.hash = qs.parse( parsed.hash.substring( 1 ) );
+ } catch ( e ) {
+ debug( 'failed to query-string parse `location.hash`', e );
+ context.hash = {};
+ }
+ } else {
+ context.hash = {};
+ }
+ next();
+ } );
+}
+
+function loadDevModulesAndBoot() {
+ if ( config.isEnabled( 'render-visualizer' ) ) {
+ // Use Webpack's code splitting feature to put the render visualizer in a separate fragment.
+ // This way it won't get downloaded unless this feature is enabled.
+ // Since loading this fragment is asynchronous and we need to inject this mixin into all React classes,
+ // we have to wait for it to load before proceeding with the application's startup.
+ require.ensure( [], function() {
+ ReactInjection.Class.injectMixin( require( 'lib/mixins/render-visualizer' ) );
+ boot();
+ }, 'devmodules' );
+
+ return;
+ }
+
+ boot();
+}
+
+function boot() {
+ var layoutSection, layout, validSections = [];
+
+ init();
+
+ // When the user is bootstrapped, we also bootstrap the
+ // locale strings
+ if ( ! config( 'wpcom_user_bootstrap' ) ) {
+ i18n.setLocaleSlug( user.get().localeSlug );
+ }
+ // Set the locale for the current user
+ user.on( 'change', function() {
+ i18n.setLocaleSlug( user.get().localeSlug );
+ } );
+
+ translatorJumpstart.init();
+
+ if ( user.get() ) {
+ // When logged in the analytics module requires user and superProps objects
+ // Inject these here
+ analytics.initialize( user, superProps );
+
+ // Create layout instance with current user prop
+ Layout = require( 'layout' );
+ layout = React.render( React.createElement( Layout, {
+ user: user,
+ sites: sites,
+ focus: layoutFocus,
+ nuxWelcome: nuxWelcome,
+ translatorInvitation: translatorInvitation
+ } ), document.getElementById( 'wpcom' ) );
+ } else {
+ if ( config.isEnabled( 'oauth' ) ) {
+ LoggedOutLayout = require( 'layout/logged-out-oauth' );
+ } else {
+ LoggedOutLayout = require( 'layout/logged-out' );
+ }
+
+ layout = React.render(
+ React.createElement( LoggedOutLayout ),
+ document.getElementById( 'wpcom' )
+ );
+ }
+
+ debug( 'Main layout rendered.' );
+
+ // If `?sb` or `?sp` are present on the path set the focus of layout
+ // This needs to be done before the page.js router is started and can be removed when the legacy version is retired
+ if ( window && [ '?sb', '?sp' ].indexOf( window.location.search ) !== -1 ) {
+ layoutSection = ( window.location.search === '?sb' ) ? 'sidebar' : 'sites';
+ layoutFocus.set( layoutSection );
+ window.history.replaceState( null, document.title, window.location.pathname );
+ }
+
+ setUpContext( layout );
+
+ page( '*', require( 'lib/route/normalize' ) );
+
+ // warn against navigating from changed, unsaved forms
+ page( '*', require( 'lib/mixins/protect-form' ).checkFormHandler );
+
+ page( '*', function( context, next ) {
+ var path = context.pathname;
+
+ // Bypass this global handler for legacy routes
+ // to avoid bumping stats and changing focus to the content
+ if ( /.php$/.test( path ) ||
+ /^\/?$/.test( path ) && ! config.isEnabled( 'reader' ) ||
+ /^\/my-stats/.test( path ) ||
+ /^\/(post\b|page\b)/.test( path ) && ! config.isEnabled( 'post-editor' ) ||
+ /^\/notifications/.test( path ) ||
+ /^\/themes/.test( path ) ||
+ /^\/manage/.test( path ) ||
+ /^\/plans/.test( path ) && ! config.isEnabled( 'manage/plans' ) ||
+ /^\/me/.test( path ) && ! /^\/me\/billing/.test( path ) &&
+ ! /^\/me\/next/.test( path ) && ! config.isEnabled( 'me/my-profile' ) ) {
+ return next();
+ }
+
+ // Focus UI on the content on page navigation
+ if ( ! config.isEnabled( 'code-splitting' ) ) {
+ layoutFocus.next();
+ }
+
+ // If `?welcome` is present show the welcome message
+ if ( context.querystring === 'welcome' && context.pathname.indexOf( '/me/next' ) === -1 ) {
+ // show welcome message, persistent for full sized screens
+ nuxWelcome.setWelcome( viewport.isDesktop() );
+ } else {
+ nuxWelcome.clearTempWelcome();
+ }
+
+ // Bump general stat tracking overall Newdash usage
+ analytics.mc.bumpStat( { newdash_pageviews: 'route' } );
+
+ next();
+ } );
+
+ page( '*', function( context, next ) {
+ if ( '/me/account' !== context.path && user.get().phone_account ) {
+ page( '/me/account' );
+ }
+
+ next();
+ } );
+
+ page( '*', function( context, next ) {
+ emailVerification.renderNotice( context );
+ next();
+ } );
+
+ // clear notices
+ page( '*', require( 'notices' ).clearNoticesOnNavigation );
+
+ if ( config.isEnabled( 'oauth' ) ) {
+ // Forces OAuth users to the /login page if no token is present
+ page( '*', require( 'auth/controller' ).checkToken );
+ }
+
+ // Load the application modules for the various sections and features
+ sections.load();
+
+ // delete any lingering local storage data from signup
+ if ( ! startsWith( window.location.pathname, '/start' ) ) {
+ [ 'signupProgress', 'signupDependencies' ].forEach( store.remove );
+ }
+
+ validSections = sections.get().reduce( function( acc, section ) {
+ return section.enableLoggedOut ? acc.concat( section.paths ) : acc;
+ }, [] );
+
+ if ( ! user.get() ) {
+ // Dead-end the sections the user can't access when logged out
+ page( '*', function( context, next ) {
+ var isValidSection = some( validSections, function( validPath ) {
+ return startsWith( context.path, validPath );
+ } );
+
+ if ( isValidSection ) {
+ next();
+ }
+ } );
+ }
+
+ page( '*', function( context, next ) {
+ // Reset the selected site before each route is executed. This needs to
+ // occur after the sections routes execute to avoid a brief flash where
+ // sites are reset but the next section is waiting to be loaded.
+ if ( ! route.getSiteFragment( context.path ) && sites.getSelectedSite() ) {
+ sites.resetSelectedSite();
+ }
+
+ next();
+ } );
+
+ require( 'my-sites' )();
+
+ if ( config.isEnabled( 'olark' ) ) {
+ require( 'lib/olark' );
+ }
+
+ if ( config.isEnabled( 'keyboard-shortcuts' ) ) {
+ require( 'lib/keyboard-shortcuts/global' )( sites );
+ }
+
+ if ( config.isEnabled( 'network-connection' ) ) {
+ require( 'lib/network-connection' ).init();
+ }
+
+ if ( config.isEnabled( 'desktop' ) ) {
+ require( 'lib/desktop' ).init();
+ }
+
+ detectHistoryNavigation.start();
+ page.start();
+}
+
+window.AppBoot = function() {
+ if ( user.initialized ) {
+ loadDevModulesAndBoot();
+ } else {
+ user.once( 'change', function() {
+ loadDevModulesAndBoot();
+ } );
+ }
+};
diff --git a/client/components/README.md b/client/components/README.md
new file mode 100644
index 00000000000000..030d055205c844
--- /dev/null
+++ b/client/components/README.md
@@ -0,0 +1,6 @@
+Components
+==========
+
+This place harbors shared React components used for composing the UI of Calypso. Components come with their own styles defined [according to our guidelines](https://github.com/Automattic/calypso-pre-oss/blob/master/docs/coding-guidelines/css.md), and manually loaded from the [styles assets folder](https://github.com/Automattic/calypso-pre-oss/blob/master/assets/stylesheets/_components.scss). Structuring the user interface with these building blocks has several benefits — like allowing to quickly construct a view that is visually consistent with the rest of Calypso, and easier to iterate on.
+
+Some of these components can be seen in action in our [DevDocs: Design](https://wpcalypso.wordpress.com/devdocs/design) section.
\ No newline at end of file
diff --git a/client/components/accordion/Makefile b/client/components/accordion/Makefile
new file mode 100644
index 00000000000000..5fb42bb5b03d57
--- /dev/null
+++ b/client/components/accordion/Makefile
@@ -0,0 +1,7 @@
+REPORTER ?= spec
+MOCHA ?= ../../../node_modules/.bin/mocha
+
+test:
+ @NODE_ENV=test NODE_PATH=test:../../ $(MOCHA) --compilers jsx:babel/register --reporter $(REPORTER)
+
+.PHONY: test
diff --git a/client/components/accordion/README.md b/client/components/accordion/README.md
new file mode 100644
index 00000000000000..3ac8c53f17f5df
--- /dev/null
+++ b/client/components/accordion/README.md
@@ -0,0 +1,32 @@
+Accordion
+=========
+
+Accordion is a React component to display collapsible content panels.
+
+## Usage
+
+At a minimum, you must provide a `title` for your Accordion, and a child or set of children to be shown in the panel.
+
+```jsx
+var Accordion = require( 'components/accordion' );
+
+module.exports = React.createClass( {
+ render: function() {
+ return (
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris fermentum eget libero at pretium. Morbi hendrerit arcu mauris, laoreet dapibus est maximus nec. Sed volutpat, lorem semper porta efficitur, dui augue tempor ante, eget faucibus quam erat vitae velit.
+
+ );
+ }
+} );
+```
+
+## Props
+
+The following props are available to customize the accordion:
+
+- `initialExpanded`: Boolean indicating whether the panel should default to expanded with the content visible
+- `onToggle`: Function handler to invoke when the user toggles the accordion. The function will be passed a boolean indicating the expanded state after the toggle.
+- `title`: Main heading shown in the always-visible toggle button
+- `subtitle`: Subheading shown in the always-visible toggle button
+- `icon`: String or React element to be shown as an icon adjacent to the headings in the always-visible toggle button. A string will be assumed to be used as the Noticon class suffix for the icon element.
diff --git a/client/components/accordion/docs/example.jsx b/client/components/accordion/docs/example.jsx
new file mode 100644
index 00000000000000..34dd01ae469f43
--- /dev/null
+++ b/client/components/accordion/docs/example.jsx
@@ -0,0 +1,77 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var Accordion = require( 'components/accordion' ),
+ Gridicon = require( 'components/gridicon' );
+
+module.exports = React.createClass( {
+ displayName: 'Accordions',
+
+ mixins: [ React.addons.PureRenderMixin ],
+
+ getInitialState: function() {
+ return {
+ showSubtitles: true
+ };
+ },
+
+ _toggleShowSubtitles: function() {
+ this.setState( {
+ showSubtitles: ! this.state.showSubtitles
+ } );
+ },
+
+ render: function() {
+ return (
+
+
+
+
+
+
+ Show subtitles
+
+
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris fermentum eget libero at pretium. Morbi hendrerit arcu mauris, laoreet dapibus est maximus nec. Sed volutpat, lorem semper porta efficitur, dui augue tempor ante, eget faucibus quam erat vitae velit.
+
+
}>
+ In tempor orci sapien, non tempor risus suscipit ut. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Mauris vitae volutpat nunc. Nunc at congue arcu. Proin non leo augue. Nulla dapibus laoreet ligula, nec varius sit amet.
+
+
+ Suspendisse pellentesque diam in nisi pulvinar maximus. Integer feugiat feugiat justo ac vehicula. Curabitur iaculis, risus suscipit sodales auctor, nisl urna elementum sem, non vestibulum mauris ante et purus. Duis iaculis nisl neque, eget rutrum erat imperdiet non.
+
+
+ Drumstick ham tongue flank doner pork chop picanha. Cow short ribs tail kevin capicola ball tip. Leberkas shankle landjaeger tenderloin, chuck cupim pastrami cow frankfurter. Kielbasa bacon capicola shoulder porchetta, frankfurter rump short loin pig cupim. Tri-tip spare ribs porchetta flank jerky bresaola bacon kevin shank cupim meatball ground round ham sirloin ball tip. Tail bresaola shank, beef ribs turkey tenderloin meatloaf frankfurter.
+
+
}
+ >
+ Etiam dictum odio elit, id faucibus urna elementum ac. Mauris in est nec tortor luctus auctor ut a velit. Suspendisse vulputate lectus arcu, sed condimentum risus rutrum vitae. Nullam sagittis ultricies nisl. Duis accumsan libero vel arcu sodales venenatis.
+
+
+
+ );
+ }
+} );
diff --git a/client/components/accordion/index.jsx b/client/components/accordion/index.jsx
new file mode 100644
index 00000000000000..4947efcbb47417
--- /dev/null
+++ b/client/components/accordion/index.jsx
@@ -0,0 +1,95 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react/addons' ),
+ noop = require( 'lodash/utility/noop' ),
+ classNames = require( 'classnames' );
+
+module.exports = React.createClass( {
+ displayName: 'Accordion',
+
+ propTypes: {
+ initialExpanded: React.PropTypes.bool,
+ onToggle: React.PropTypes.func,
+ title: React.PropTypes.string.isRequired,
+ subtitle: React.PropTypes.string,
+ icon: React.PropTypes.oneOfType( [
+ React.PropTypes.string,
+ React.PropTypes.element
+ ] )
+ },
+
+ getInitialState: function() {
+ return {
+ isExpanded: this.props.initialExpanded
+ };
+ },
+
+ getDefaultProps: function() {
+ return {
+ onToggle: noop
+ };
+ },
+
+ toggleExpanded: function() {
+ var isExpanded = ! this.state.isExpanded;
+
+ this.setState( {
+ isExpanded: isExpanded
+ } );
+
+ this.props.onToggle( isExpanded );
+ },
+
+ renderIcon: function() {
+ if ( ! this.props.icon ) {
+ return;
+ }
+
+ if ( 'string' === typeof this.props.icon ) {
+ return ;
+ }
+
+ return { this.props.icon } ;
+ },
+
+ renderSubtitle: function() {
+ if ( this.props.subtitle ) {
+ return { this.props.subtitle } ;
+ }
+ },
+
+ renderHeader: function() {
+ var classes = classNames( 'accordion__header', {
+ 'has-icon': !! this.props.icon,
+ 'has-subtitle': !! this.props.subtitle
+ } );
+
+ return (
+
+
+ { this.renderIcon() }
+ { this.props.title }
+ { this.renderSubtitle() }
+
+
+ );
+ },
+
+ render: function() {
+ var classes = classNames( 'accordion', this.props.className, {
+ 'is-expanded': this.state.isExpanded
+ } );
+
+ return (
+
+ { this.renderHeader() }
+
+
+ { this.props.children }
+
+
+
+ );
+ }
+} );
diff --git a/client/components/accordion/section.jsx b/client/components/accordion/section.jsx
new file mode 100644
index 00000000000000..bf04b036917eba
--- /dev/null
+++ b/client/components/accordion/section.jsx
@@ -0,0 +1,15 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+import classNames from 'classnames';
+
+export default React.createClass( {
+ render() {
+ return (
+
+ { this.props.children }
+
+ );
+ },
+} );
diff --git a/client/components/accordion/style.scss b/client/components/accordion/style.scss
new file mode 100644
index 00000000000000..778a959bc3fc15
--- /dev/null
+++ b/client/components/accordion/style.scss
@@ -0,0 +1,133 @@
+$accordion-padding: 16px;
+$accordion-subtitle-height: 16px;
+$accordion-background-collapsed: $gray-light; // same as body
+$accordion-background-hover: lighten( $gray, 30% );
+$accordion-background-expanded: $white;
+
+.accordion {
+ box-shadow: 0 -1px 0 lighten( $gray, 26% ),
+ 0 1px 0 lighten( $gray, 26% );
+ margin-top: 1px;
+ position: relative;
+}
+
+.accordion__toggle {
+ display: block;
+ width: 100%;
+ cursor: pointer;
+ position: relative;
+ margin: 0;
+ padding: $accordion-padding;
+ padding-right: ( $accordion-padding + 22px );
+ color: $gray-dark;
+ background-color: $accordion-background-collapsed;
+ text-align: left;
+
+ &:hover {
+ background-color: $accordion-background-hover;
+ }
+
+ &::after {
+ @include noticon( '\f431', 22px );
+ position: absolute;
+ top: 50%;
+ right: $accordion-padding;
+ transform: translateY( -50% );
+ color: lighten( $gray, 10% );
+ }
+}
+
+.accordion.is-expanded {
+ box-shadow: 0 0 0 1px lighten( $gray, 20% );
+
+ .accordion__toggle {
+ background-color: $accordion-background-expanded;
+ box-shadow: 0 0 0 1px lighten( $gray, 20% );
+
+ &::after {
+ content: '\f432';
+ }
+ }
+
+ .accordion__content {
+ overflow: visible;
+ }
+}
+
+.accordion__header.has-subtitle .accordion__toggle {
+ padding-top: ( $accordion-padding - $accordion-subtitle-height / 2 );
+ padding-bottom: ( $accordion-padding - $accordion-subtitle-height / 2 );
+}
+
+.accordion__icon {
+ position: absolute;
+ left: $accordion-padding;
+ top: 50%;
+ transform: translateY( -50% );
+ margin-top: 3px;
+
+ .gridicon {
+ width: 20px;
+ height: 20px;
+ }
+}
+
+.accordion__title,
+.accordion__subtitle {
+ display: block;
+}
+
+.accordion__header.has-icon .accordion__title,
+.accordion__header.has-icon .accordion__subtitle {
+ padding-left: 28px;
+}
+
+.accordion__subtitle {
+ font-size: 0.8em;
+ font-style: italic;
+ color: $gray;
+ white-space: nowrap;
+ overflow-x: hidden;
+ position: relative;
+ height: $accordion-subtitle-height;
+}
+
+.accordion:not( .is-expanded ) .accordion__toggle .accordion__subtitle {
+ &::after {
+ @include long-content-fade( $color: $accordion-background-collapsed );
+ }
+}
+
+.accordion:not( .is-expanded ) .accordion__toggle:hover .accordion__subtitle {
+ &::after {
+ @include long-content-fade( $color: $accordion-background-hover );
+ }
+}
+
+.accordion.is-expanded .accordion__subtitle {
+ &::after {
+ @include long-content-fade( $color: $accordion-background-expanded );
+ }
+}
+
+.accordion__content {
+ overflow: hidden;
+ height: 0;
+ background-color: $accordion-background-expanded;
+}
+
+.accordion.is-expanded .accordion__content {
+ height: auto;
+}
+
+.accordion__content-wrap {
+ padding: $accordion-padding;
+}
+
+.accordion__section {
+ margin-bottom: 24px;
+}
+
+.accordion__section:last-child {
+ margin-bottom: 0;
+}
diff --git a/client/components/accordion/test/index.jsx b/client/components/accordion/test/index.jsx
new file mode 100644
index 00000000000000..63add168d4f1d2
--- /dev/null
+++ b/client/components/accordion/test/index.jsx
@@ -0,0 +1,89 @@
+/* eslint-disable vars-on-top */
+require( 'lib/react-test-env-setup' )();
+
+/**
+ * External dependencies
+ */
+var expect = require( 'chai' ).expect,
+ React = require( 'react/addons' ),
+ TestUtils = React.addons.TestUtils;
+
+require( 'react-tap-event-plugin' )();
+
+/**
+ * Internal dependencies
+ */
+var Accordion = require( '../' );
+
+describe( 'Accordion', function() {
+ afterEach( function() {
+ React.unmountComponentAtNode( document.body );
+ } );
+
+ it( 'should render as expected with a title and content', function() {
+ var tree = TestUtils.renderIntoDocument( Content ),
+ node = React.findDOMNode( tree );
+
+ expect( node.className ).to.equal( 'accordion' );
+ expect( tree.state.isExpanded ).to.not.be.ok;
+ expect( node.querySelector( '.accordion__header:not( .has-icon ):not( .has-subtitle )' ) ).to.be.an.instanceof( window.Element );
+ expect( node.querySelector( '.accordion__icon' ) ).to.be.null;
+ expect( node.querySelector( '.accordion__title' ).textContent ).to.equal( 'Section' );
+ expect( node.querySelector( '.accordion__subtitle' ) ).to.be.null;
+ expect( React.findDOMNode( tree.refs.content ).textContent ).to.equal( 'Content' );
+ } );
+
+ it( 'should accept an icon prop to be rendered as a noticon', function() {
+ var tree = TestUtils.renderIntoDocument( Content ),
+ node = React.findDOMNode( tree );
+
+ expect( node.querySelector( '.accordion__header.has-icon:not( .has-subtitle )' ) ).to.be.an.instanceof( window.Element );
+ expect( node.querySelector( '.accordion__icon' ) ).to.be.an.instanceof( window.Element );
+ } );
+
+ it( 'should accept a subtitle prop to be rendered aside the title', function() {
+ var tree = TestUtils.renderIntoDocument( Content ),
+ node = React.findDOMNode( tree );
+
+ expect( node.querySelector( '.accordion__header.has-subtitle:not( .has-icon )' ) ).to.be.an.instanceof( window.Element );
+ expect( node.querySelector( '.accordion__subtitle' ).textContent ).to.equal( 'Subtitle' );
+ } );
+
+ it( 'should toggle when clicked', function() {
+ var tree = TestUtils.renderIntoDocument( Content );
+
+ TestUtils.Simulate.touchTap( React.findDOMNode( TestUtils.findRenderedDOMComponentWithClass( tree, 'accordion__toggle' ) ) );
+
+ expect( tree.state.isExpanded ).to.be.ok;
+ } );
+
+ it( 'should accept an onToggle function handler to be invoked when toggled', function( done ) {
+ var tree = TestUtils.renderIntoDocument( Content );
+
+ TestUtils.Simulate.touchTap( React.findDOMNode( TestUtils.findRenderedDOMComponentWithClass( tree, 'accordion__toggle' ) ) );
+
+ function finishTest( isExpanded ) {
+ expect( isExpanded ).to.be.ok;
+
+ process.nextTick( function() {
+ expect( tree.state.isExpanded ).to.be.ok;
+ done();
+ } );
+ }
+ } );
+
+ it( 'should always use the initialExpanded prop, if specified', function( done ) {
+ var tree = TestUtils.renderIntoDocument( Content );
+
+ TestUtils.Simulate.touchTap( React.findDOMNode( TestUtils.findRenderedDOMComponentWithClass( tree, 'accordion__toggle' ) ) );
+
+ function finishTest( isExpanded ) {
+ expect( isExpanded ).to.not.be.ok;
+
+ process.nextTick( function() {
+ expect( tree.state.isExpanded ).to.not.be.ok;
+ done();
+ } );
+ }
+ } );
+} );
diff --git a/client/components/add-new-button/README.md b/client/components/add-new-button/README.md
new file mode 100644
index 00000000000000..600079b684a68a
--- /dev/null
+++ b/client/components/add-new-button/README.md
@@ -0,0 +1,23 @@
+Add new button
+===========
+
+This component implements a button styled to be used in 'add' actions.
+
+## Usage
+
+```jsx
+import AddNewButton from 'components/add-new-button';
+
+export default React.createClass( {
+ render: () => Link
+} );
+
+```
+
+## Props
+
+- `href`: String with the destination url.
+- `onClick`: Function to be executed when the button is clicked
+- `isCompact`: Boolean indicating whether the button is in compact mode. Defaults to false.
+- `outline`: Boolean indicating whether the button has an outline. Defaults to false.
+- `icon`: String indicating which Gridicon we are going to use for the button. Defaults to `add` or `add-outline`, depending on `outline` value
\ No newline at end of file
diff --git a/client/components/add-new-button/docs/example.jsx b/client/components/add-new-button/docs/example.jsx
new file mode 100644
index 00000000000000..17c94242f6241e
--- /dev/null
+++ b/client/components/add-new-button/docs/example.jsx
@@ -0,0 +1,34 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var AddNewButton = require( 'components/add-new-button' );
+
+var AddNewButtons = React.createClass( {
+ displayName: 'AddNewButton',
+
+ mixins: [ React.addons.PureRenderMixin ],
+
+ render: function() {
+ return (
+
+
+
Unstyled Add New Button
+
Link-styled with outline
+
+
Install Button
+
Link
+
Link
+
Link
+
+ );
+ }
+} );
+
+module.exports = AddNewButtons;
diff --git a/client/components/add-new-button/index.jsx b/client/components/add-new-button/index.jsx
new file mode 100644
index 00000000000000..ceee91c899fd43
--- /dev/null
+++ b/client/components/add-new-button/index.jsx
@@ -0,0 +1,57 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+import classnames from 'classnames';
+import assign from 'lodash/object/assign';
+import noop from 'lodash/utility/noop';
+
+/**
+ * Internal dependencies
+ */
+import Gridicon from 'components/gridicon';
+
+export default React.createClass( {
+
+ displayName: 'AddNewButton',
+
+ getDefaultProps() {
+ return {
+ isCompact: false,
+ onClick: noop,
+ outline: false,
+ icon: null,
+ };
+ },
+
+ propTypes: {
+ className: React.PropTypes.string,
+ href: React.PropTypes.string,
+ icon: React.PropTypes.string,
+ isCompact: React.PropTypes.bool,
+ onClick: React.PropTypes.func,
+ outline: React.PropTypes.bool
+ },
+
+ render() {
+ // this component creates an anchor or a button
+ // depending on which props are passed
+ const element = this.props.href ? 'a' : 'button';
+
+ const classes = classnames( 'add-new-button', this.props.className, {
+ 'is-compact': this.props.isCompact,
+ 'has-icon': !! this.props.icon,
+ } );
+
+ const defaultIcon = ( this.props.outline ? 'add-outline' : 'add' );
+ const icon = this.props.icon ? this.props.icon : defaultIcon;
+ const firstIcon = this.props.icon ? : null;
+
+ return React.createElement(
+ element,
+ assign( {}, this.props, { className: classes } ),
+ [ firstIcon, ],
+ { this.props.children }
+ );
+ }
+} );
diff --git a/client/components/add-new-button/style.scss b/client/components/add-new-button/style.scss
new file mode 100644
index 00000000000000..33f430f3bd2680
--- /dev/null
+++ b/client/components/add-new-button/style.scss
@@ -0,0 +1,46 @@
+// Add new category
+a.add-new-button,
+button.add-new-button {
+ cursor: pointer;
+ color: darken( $gray, 20% );
+ margin: 0 0 8px;
+ padding: 8px 0;
+ font-size: 14px;
+ display: block;
+ text-align: left;
+
+ &:hover {
+ color: $gray-dark;
+ }
+
+ .gridicon {
+ float: left;
+ margin-top: 1px;
+ }
+ &.has-icon {
+ position: relative;
+
+ &:hover {
+ color: $gray-dark;
+ }
+
+ .gridicon {
+ margin-left: 8px;
+ }
+
+ .gridicons-plus-small {
+ position: absolute;
+ top: 5px;
+ left: 0;
+ margin-left: 0;
+ }
+ }
+}
+
+.add-new-button__text {
+ padding-left: 8px;
+ font-size: 11px;
+ font-weight: 400;
+ text-transform: uppercase;
+ line-height: 18px;
+}
diff --git a/client/components/author-selector/README.md b/client/components/author-selector/README.md
new file mode 100644
index 00000000000000..c4a1d8dcec4957
--- /dev/null
+++ b/client/components/author-selector/README.md
@@ -0,0 +1,19 @@
+Author Selector
+======================
+
+This component allows an administrator with sufficient privileges to edit the author of a post. Use it by wrapping the element that you will use to toggle the menu open/closed.
+
+```js
+
+ by William Shakespeare
+
+```
+
+The component will retrieve site users and render the child span as a clickable element to expand the `author-selector` UX. If selecting other authors is not appropriate (i.e., only one available author, Users not loaded, or insufficient permission), it will simply display the span.
+
+### Props
+* siteId - siteId for site from which to fetch authors
+* onSelect - function to call when user is selected, selected `author` passed as parameter
+* exclude - Optional array of users IDs to be excluded from the author selector
+* allowSingleUser - Optional boolean for whether or not to display the author selector when there is only one user
diff --git a/client/components/author-selector/index.jsx b/client/components/author-selector/index.jsx
new file mode 100644
index 00000000000000..37f95f1bc292de
--- /dev/null
+++ b/client/components/author-selector/index.jsx
@@ -0,0 +1,290 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ debug = require( 'debug' )( 'calypso:author-selector' ),
+ trim = require( 'lodash/string/trim' );
+
+/**
+ * Internal dependencies
+ */
+var Popover = require( 'components/popover' ),
+ PopoverMenuItem = require( 'components/popover/menu-item' ),
+ SiteUsersFetcher = require( 'components/site-users-fetcher' ),
+ UserItem = require( 'components/user' ),
+ Gridicon = require( 'components/gridicon' ),
+ InfiniteList = require( 'components/infinite-list' ),
+ UsersActions = require( 'lib/users/actions' ),
+ Search = require( 'components/search' ),
+ hasTouch = require( 'lib/touch-detect' ).hasTouch;
+
+/**
+ * Module variables
+ */
+var instance = 0, SwitcherShell;
+
+SwitcherShell = React.createClass( {
+ displayName: 'AuthorSwitcherShell',
+ propTypes: {
+ users: React.PropTypes.array,
+ fetchingUsers: React.PropTypes.bool,
+ numUsersFetched: React.PropTypes.number,
+ totalUsers: React.PropTypes.number,
+ usersCurrentOffset: React.PropTypes.number,
+ allowSingleUser: React.PropTypes.bool,
+ popoverPosition: React.PropTypes.string
+ },
+
+ getInitialState: function() {
+ return {
+ showAuthorMenu: false
+ };
+ },
+
+ componentWillMount: function() {
+ this.instance = instance;
+ instance++;
+ },
+
+ componentWillReceiveProps: function( nextProps ) {
+ if ( ! nextProps.fetchOptions.siteId || nextProps.fetchOptions.siteId !== this.props.fetchOptions.siteId ) {
+ this.props.updateSearch( false );
+ }
+ },
+
+ componentDidUpdate: function( prevProps, prevState ) {
+ if ( ! this.state.showAuthorMenu ) {
+ return;
+ }
+
+ if ( ! prevState.showAuthorMenu && this.props.users.length > 10 && ! hasTouch() ) {
+ setTimeout( () => this.refs.authorSelectorSearch.focus(), 0 );
+ }
+ },
+
+ render: function() {
+ var users = this.props.users,
+ infiniteListKey = this.props.fetchNameSpace + this.instance;
+
+ if ( ! this._userCanSelectAuthor() ) {
+ return { this.props.children } ;
+ }
+
+ return (
+
+
+ { this.props.children }
+
+
+
+ { this.props.fetchOptions.search || users.length > 10 ?
+ :
+ null }
+ { this.props.fetchInitialized && ! users.length && this.props.fetchOptions.search && ! this.props.fetchingUsers ?
+ this._noUsersFound() :
+
+
+ }
+
+
+ );
+ },
+
+ _isLastPage: function() {
+ var usersLength = this.props.users.length;
+ if ( this.props.exclude ) {
+ usersLength += this.props.excludedUsers.length;
+ }
+
+ return this.props.totalUsers <= usersLength;
+ },
+
+ _setListContext: function( infiniteListInstance ) {
+ this.setState( {
+ listContext: React.findDOMNode( infiniteListInstance )
+ } );
+ },
+
+ _userCanSelectAuthor: function() {
+ var users = this.props.users;
+
+ if ( this.props.fetchOptions.search ) {
+ return true;
+ }
+
+ // no user choice
+ if ( ! users || ! users.length || ( ! this.props.allowSingleUser && users.length === 1 ) ) {
+ return false;
+ }
+
+ return true;
+ },
+
+ _toggleShowAuthor: function() {
+ this.setState( {
+ showAuthorMenu: ! this.state.showAuthorMenu
+ } );
+ },
+
+ _onClose: function( event ) {
+ var toggleElement = React.findDOMNode( this.refs[ 'author-selector-toggle' ] );
+
+ if ( event && toggleElement.contains( event.target ) ) {
+ // let _toggleShowAuthor() handle this case
+ return;
+ }
+ this.setState( {
+ showAuthorMenu: false
+ } );
+ this.props.updateSearch( false );
+ },
+
+ _renderAuthor: function( author ) {
+ var authorGUID = this._getAuthorItemGUID( author );
+ return (
+
+
+
+ );
+ },
+
+ _noUsersFound: function() {
+ return (
+
+ { this.translate( 'No matching users found.' ) }
+
+ );
+ },
+
+ _selectAuthor: function( author ) {
+ debug( 'assign author:', author );
+ if ( this.props.onSelect ) {
+ this.props.onSelect( author );
+ }
+ this.setState( {
+ showAuthorMenu: false
+ } );
+ this.props.updateSearch( false );
+ },
+
+ _fetchNextPage: function() {
+ var fetchOptions = Object.assign( {}, this.props.fetchOptions, { offset: this.props.users.length } );
+ debug( 'fetching next batch of authors' );
+ UsersActions.fetchUsers( fetchOptions );
+ },
+
+ _getAuthorItemGUID: function( author ) {
+ return 'author-item-' + author.ID;
+ },
+
+ _renderLoadingAuthors: function() {
+ return (
+
+
+
+ );
+ },
+
+ _onSearch: function( searchTerm ) {
+ this.props.updateSearch( searchTerm );
+ }
+} );
+
+module.exports = React.createClass( {
+ displayName: 'AuthorSelector',
+ propTypes: {
+ siteId: React.PropTypes.number.isRequired,
+ onSelect: React.PropTypes.func,
+ exclude: React.PropTypes.arrayOf( React.PropTypes.number ),
+ allowSingleUser: React.PropTypes.bool,
+ popoverPosition: React.PropTypes.string
+ },
+
+ getInitialState: function() {
+ return {
+ search: ''
+ };
+ },
+
+ getDefaultProps: function() {
+ return {
+ showAuthorMenu: false,
+ onClose: function() {},
+ allowSingleUser: false,
+ popoverPosition: 'bottom left'
+ };
+ },
+
+ componentDidMount: function() {
+ debug( 'AuthorSelector mounted' );
+ },
+
+ render: function() {
+ var searchString = this.state.search || '',
+ fetchOptions;
+
+ searchString = trim( searchString );
+
+ fetchOptions = {
+ siteId: this.props.siteId,
+ order: 'ASC',
+ order_by: 'display_name',
+ number: 50
+ };
+
+ if ( searchString ) {
+ fetchOptions.number = 20; // make search a little faster
+ fetchOptions.search = searchString;
+ fetchOptions.search_columns = [ 'user_login', 'display_name' ];
+ }
+
+ Object.freeze( fetchOptions );
+ return (
+
+
+
+ );
+ },
+
+ _updateSearch: function( searchTerm ) {
+ searchTerm = searchTerm ? '*' + searchTerm + '*' : '';
+ this.setState( {
+ search: searchTerm
+ } );
+ }
+} );
diff --git a/client/components/author-selector/style.scss b/client/components/author-selector/style.scss
new file mode 100644
index 00000000000000..e9e2dddcbeeb3a
--- /dev/null
+++ b/client/components/author-selector/style.scss
@@ -0,0 +1,79 @@
+.author-selector__author-toggle {
+ cursor: pointer;
+
+ .gridicon {
+ display: inline;
+ vertical-align: middle;
+ color: darken( $gray, 10% );
+ }
+
+ &.is-open .gridicon {
+ transform: rotate( 180deg );
+ }
+
+ &:hover,
+ &.is-open {
+ .gridicon,
+ .editor-author__name {
+ color: darken( $gray, 30% );
+ }
+ }
+}
+
+.author-selector__author-toggle .editor-author__name {
+ margin: 0 3px 0 8px;
+}
+
+.author-selector__infinite-list {
+ max-height: 280px;
+ overflow-y: auto;
+ padding: 4px 0;
+ width: 200px;
+ max-width: 200px;
+ white-space: nowrap;
+}
+
+.author-selector__menu-item {
+ width: 100%;
+
+ .user {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+}
+
+.author-selector__popover.popover {
+ z-index: 100;
+}
+
+.author-selector__popover {
+ .search {
+ border-bottom: 1px solid lighten( $gray, 30 );
+ // .search should be cleaned up to not force height
+ height: 43px;
+ margin-bottom: 0;
+
+ .is-open .noticon-search {
+ color: $gray;
+ }
+
+ .search__input[type="search"] {
+ border-radius: 5px;
+ font-size: 14px;
+ height: 43px;
+ padding: 0 50px 0;
+ }
+
+ & + .author-selector__infinite-list {
+ padding-top: 0;
+ }
+ }
+}
+.author-selector__no-users {
+ padding: 8px 16px;
+ line-height: 26px;
+ width: 168px;
+ font-size: 14px;
+ font-style: italic;
+ color: $gray;
+}
diff --git a/client/components/bulk-select/README.md b/client/components/bulk-select/README.md
new file mode 100644
index 00000000000000..c0acb96d017776
--- /dev/null
+++ b/client/components/bulk-select/README.md
@@ -0,0 +1,22 @@
+Bulk Select
+=========
+
+This component is used to implement a checkbox which you can use to bulk select a list of elements
+
+#### How to use:
+
+```js
+var BulkSelect = require( 'components/bulk-select' );
+
+render: function() {
+ return (
+
+ );
+}
+```
+
+#### Props
+
+* `selectedElements`: (number) The number of elements currently selected
+* `totalElements`: (number) The number of all the elements that can be selected
+* `onToggle`: (function) callback to be executed when the checkbox state is change. The callback receives a boolean indicating the state of the 'select all' checkbox, and normally you would want to apply it to the list of elements here.
diff --git a/client/components/bulk-select/docs/example.jsx b/client/components/bulk-select/docs/example.jsx
new file mode 100644
index 00000000000000..7a9e7f494cad6f
--- /dev/null
+++ b/client/components/bulk-select/docs/example.jsx
@@ -0,0 +1,66 @@
+/**
+* External dependencies
+*/
+import React from 'react';
+
+/**
+ * Internal dependencies
+ */
+import Card from 'components/card';
+import BulkSelect from 'components/bulk-select';
+
+module.exports = React.createClass( {
+ displayName: 'BulkSelects',
+
+ handleToggleAll( checkedState ) {
+ let newElements = [];
+ this.state.elements.forEach( element => {
+ if ( typeof checkedState !== 'undefined' ) {
+ element.selected = checkedState;
+ } else {
+ element.selected = ! element.selected;
+ }
+ newElements.push( element );
+ } );
+ this.setState( { elements: newElements } );
+ },
+
+ getInitialState() {
+ return { elements: [ { title: 'Apples', selected: true }, { title: 'Oranges', selected: false } ] };
+ },
+
+ getSelectedElementsNumber: function() {
+ return this.state.elements.filter( function( element ) {
+ return element.selected;
+ } ).length;
+ },
+
+ renderElements() {
+ return this.state.elements.map( element => {
+ const onClick = function() {
+ element.selected = ! element.selected;
+ this.forceUpdate();
+ }.bind( this );
+ return (
+
+
+ { element.title }
+
+ );
+ } );
+ },
+
+ render() {
+ return (
+
+
+
+
+ { this.renderElements() }
+
+
+ );
+ }
+} );
diff --git a/client/components/bulk-select/index.jsx b/client/components/bulk-select/index.jsx
new file mode 100644
index 00000000000000..25a8164376c453
--- /dev/null
+++ b/client/components/bulk-select/index.jsx
@@ -0,0 +1,50 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+
+/**
+ * Internal dependencies
+ */
+import Count from 'components/count';
+import Gridicon from 'components/gridicon';
+
+export default React.createClass( {
+
+ displayName: 'BulkSelect',
+
+ propTypes: {
+ totalElements: React.PropTypes.number.isRequired,
+ selectedElements: React.PropTypes.number.isRequired,
+ onToggle: React.PropTypes.func.isRequired
+ },
+
+ getStateIcon() {
+ if ( this.hasSomeElementsSelected() ) {
+ return ;
+ }
+ },
+
+ hasAllElementsSelected() {
+ return this.props.selectedElements && this.props.selectedElements === this.props.totalElements;
+ },
+
+ hasSomeElementsSelected() {
+ return this.props.selectedElements && this.props.selectedElements < this.props.totalElements;
+ },
+
+ handleToggleAll() {
+ const newCheckedState = ! ( this.hasSomeElementsSelected() || this.hasAllElementsSelected() );
+ this.props.onToggle( newCheckedState );
+ },
+
+ render() {
+ return (
+
+
+
+ { this.getStateIcon() }
+
+ );
+ }
+} );
diff --git a/client/components/bulk-select/style.scss b/client/components/bulk-select/style.scss
new file mode 100644
index 00000000000000..021d685d76c71d
--- /dev/null
+++ b/client/components/bulk-select/style.scss
@@ -0,0 +1,28 @@
+.bulk-select {
+ position: relative;
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+
+ .gridicon {
+ width: 16px;
+ height: 16px;
+ color: $blue-medium;
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ }
+
+ > .count {
+ margin-left: 8px;
+ }
+}
+
+input[type=checkbox].bulk-select__box {
+ height: 16px;
+ margin: 0;
+ padding: 0;
+ width: 16px;
+ min-width: 16px;
+ appearance: none;
+}
diff --git a/client/components/button-group/README.md b/client/components/button-group/README.md
new file mode 100644
index 00000000000000..ff31ca28c0227f
--- /dev/null
+++ b/client/components/button-group/README.md
@@ -0,0 +1,20 @@
+Button Group
+=========
+
+This component is used to group several semantically linked buttons under the same group
+
+#### How to use:
+
+```js
+const ButtonGroup = require( 'components/button-group' ),
+ Button = require( 'components/button' );
+
+render: function() {
+ return (
+
+ Save
+ Cancel
+
+ );
+}
+```
diff --git a/client/components/button-group/docs/example.jsx b/client/components/button-group/docs/example.jsx
new file mode 100644
index 00000000000000..f9834c3b14f021
--- /dev/null
+++ b/client/components/button-group/docs/example.jsx
@@ -0,0 +1,69 @@
+/**
+* External dependencies
+*/
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var ButtonGroup = require( 'components/button-group' ),
+ Button = require( 'components/button' ),
+ Card = require( 'components/card' ),
+ Gridicon = require( 'components/gridicon' );
+
+var Buttons = React.createClass( {
+ displayName: 'ButtonGroup',
+
+ mixins: [ React.addons.PureRenderMixin ],
+
+ render: function() {
+ return (
+
+
+
+
+
+ One button
+
+
+
+
+ Do things
+ Cancel
+
+
+
+
+ Button one
+ Button two
+ Button three
+
+
+
+
+ Draft
+ Save
+ Delete
+
+
+
+
+ Do bigger things
+ Cancel
+
+
+
+
+ Publish
+
+
+
+
+
+ );
+ },
+} );
+
+module.exports = Buttons;
diff --git a/client/components/button-group/index.jsx b/client/components/button-group/index.jsx
new file mode 100644
index 00000000000000..c7f026f3d7202d
--- /dev/null
+++ b/client/components/button-group/index.jsx
@@ -0,0 +1,30 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+import classNames from 'classnames';
+
+export default React.createClass( {
+
+ displayName: 'ButtonGroup',
+
+ propTypes: {
+ children( props ) {
+ let error = null;
+ React.Children.forEach( props.children, ( child ) => {
+ if ( ! child.props || child.props.type !== 'button' ) {
+ error = new Error( 'All children elements should be a Button.' );
+ }
+ } );
+ return error;
+ }
+ },
+
+ render() {
+ const buttonGroupClasses = classNames( 'button-group', this.props.className );
+
+ return (
+ { this.props.children }
+ );
+ }
+} );
diff --git a/client/components/button-group/style.scss b/client/components/button-group/style.scss
new file mode 100644
index 00000000000000..4af79b1444da93
--- /dev/null
+++ b/client/components/button-group/style.scss
@@ -0,0 +1,45 @@
+.button-group {
+
+ .button {
+ border-left-width: 0;
+ border-radius: 0;
+
+ &:focus {
+ // fixes focus styles in stacking context
+ position: relative;
+ z-index: 1;
+ box-shadow: inset 1px 0 0 $blue-medium, 0 0 0 2px $blue-light;
+ }
+ &.is-primary:focus {
+ box-shadow: inset 1px 0 0 $blue-dark, 0 0 0 2px $blue-light;
+ }
+ &.is-scary:focus {
+ box-shadow: inset 1px 0 0 $alert-red, 0 0 0 2px lighten( $alert-red, 20% );
+ }
+ &.is-primary.is-scary:focus {
+ box-shadow: inset 1px 0 0 darken( $alert-red, 30% ), 0 0 0 2px lighten( $alert-red, 20% );
+ }
+ &:first-child:focus {
+ box-shadow: 0 0 0 2px $blue-light;
+ }
+ &.is-scary:first-child:focus {
+ box-shadow: 0 0 0 2px lighten( $alert-red, 20% );
+ }
+ }
+
+ .button:first-child {
+ border-left-width: 1px;
+ border-top-left-radius: 4px;
+ border-bottom-left-radius: 4px;
+ }
+
+ .button:last-child {
+ border-top-right-radius: 4px;
+ border-bottom-right-radius: 4px;
+ }
+
+ &.example {
+ margin-bottom: 16px;
+ display: inline-block;
+ }
+}
diff --git a/client/components/button/README.md b/client/components/button/README.md
new file mode 100644
index 00000000000000..9919060de8ea7f
--- /dev/null
+++ b/client/components/button/README.md
@@ -0,0 +1,24 @@
+Button
+=========
+
+This component is used to implement dang sweet buttons.
+
+#### How to use:
+
+```js
+var Button = require( 'components/button' );
+
+render: function() {
+ return (
+ You rock
+ );
+}
+```
+
+#### Props
+
+* `compact`: (bool) whether the button is compact or not.
+* `primary`: (bool) whether the button is styled as a primary button.
+* `scary`: (bool) whether the button has modified styling to warn users (delete, remove, etc).
+* `href`: (string) if this property is added, it will use an `a` rather than a `button` element.
+* `disabled`: (bool) whether the button should be in the disabled state.
diff --git a/client/components/button/docs/example.jsx b/client/components/button/docs/example.jsx
new file mode 100644
index 00000000000000..f56eacf7d77dc1
--- /dev/null
+++ b/client/components/button/docs/example.jsx
@@ -0,0 +1,105 @@
+/**
+* External dependencies
+*/
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var Button = require( 'components/button' ),
+ Gridicon = require( 'components/gridicon' ),
+ Card = require( 'components/card' );
+
+var Buttons = React.createClass( {
+ displayName: 'Buttons',
+
+ mixins: [ React.addons.PureRenderMixin ],
+
+ getInitialState: function() {
+ return {
+ compactButtons: false
+ };
+ },
+
+ render: function() {
+ var toggleButtonsText = this.state.compactButtons ? 'Normal Buttons' : 'Compact Buttons';
+
+ return (
+
+
+ { this.renderButtons() }
+
+ );
+ },
+
+ renderButtons: function() {
+ if ( ! this.state.compactButtons ) {
+ return (
+
+
+ Button
+ Icon button
+
+ Disabled button
+
+
+ Scary button
+ Scary icon button
+
+ Scary disabled button
+
+
+ Primary button
+ Primary icon button
+
+ Primary disabled button
+
+
+ Primary scary button
+ Primary scary icon button
+
+ Primary scary disabled button
+
+
+ );
+ } else {
+ return (
+
+
+ Compact button
+ Compact icon button
+
+ Compact disabled button
+
+
+ Compact scary button
+ Compact scary icon button
+
+ Compact scary disabled button
+
+
+ Compact primary button
+ Compact primary icon button
+
+ Compact primary disabled button
+
+
+ Compact primary scary button
+ Compact primary scary icon button
+
+ Compact primary scary disabled button
+
+
+ );
+ }
+ },
+
+ toggleButtons: function() {
+ this.setState( { compactButtons: ! this.state.compactButtons } );
+ }
+} );
+
+module.exports = Buttons;
diff --git a/client/components/button/index.jsx b/client/components/button/index.jsx
new file mode 100644
index 00000000000000..4d8352dd3643bd
--- /dev/null
+++ b/client/components/button/index.jsx
@@ -0,0 +1,46 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+import assign from 'lodash/object/assign';
+import classNames from 'classnames';
+import noop from 'lodash/utility/noop';
+
+export default React.createClass( {
+
+ displayName: 'Button',
+
+ propTypes: {
+ disabled: React.PropTypes.bool,
+ compact: React.PropTypes.bool,
+ primary: React.PropTypes.bool,
+ scary: React.PropTypes.bool,
+ type: React.PropTypes.string,
+ href: React.PropTypes.string,
+ onClick: React.PropTypes.func
+ },
+
+ getDefaultProps() {
+ return {
+ disabled: false,
+ type: 'button',
+ onClick: noop
+ };
+ },
+
+ render() {
+ const element = this.props.href ? 'a' : 'button';
+ const buttonClasses = classNames( {
+ button: true,
+ 'is-compact': this.props.compact,
+ 'is-primary': this.props.primary,
+ 'is-scary': this.props.scary
+ } );
+
+ const props = assign( {}, this.props, {
+ className: classNames( this.props.className, buttonClasses )
+ } );
+
+ return React.createElement( element, props, this.props.children );
+ }
+} );
diff --git a/client/components/button/style.scss b/client/components/button/style.scss
new file mode 100644
index 00000000000000..0ca09bfdeb3ca6
--- /dev/null
+++ b/client/components/button/style.scss
@@ -0,0 +1,216 @@
+// ==========================================================================
+// Buttons
+// ==========================================================================
+
+// resets button styles
+button {
+ background: transparent;
+ border: none;
+ outline: 0;
+ padding: 0;
+ font-size: 14px;
+ -webkit-appearance: none;
+ appearance: none;
+ vertical-align: baseline;
+}
+
+.button {
+ background: $white;
+ border-color: lighten( $gray, 20% );
+ border-style: solid;
+ border-width: 1px 1px 2px;
+ color: $gray-dark;
+ cursor: pointer;
+ display: inline-block;
+ margin: 0;
+ outline: 0;
+ overflow: hidden;
+ font-weight: 600;
+ text-overflow: ellipsis;
+ text-decoration: none;
+ vertical-align: top;
+ box-sizing: border-box;
+ font-size: 14px;
+ line-height: 21px;
+ border-radius: 4px;
+ padding: 7px 14px 9px;
+ -webkit-appearance: none;
+ appearance: none;
+
+ &:hover {
+ border-color: lighten( $gray, 10% );
+ color: $gray-dark;
+ }
+ &:active {
+ border-width: 2px 1px 1px;
+ }
+ &:visited {
+ color: $gray-dark;
+ }
+ &[disabled],
+ &:disabled {
+ color: lighten( $gray, 30% );
+ background: $white;
+ border-color: lighten( $gray, 30% );
+ cursor: default;
+
+ &:active {
+ border-width: 1px 1px 2px;
+ }
+ }
+ &:focus {
+ border-color: $blue-medium;
+ box-shadow: 0 0 0 2px $blue-light;
+ }
+ &.is-compact {
+ padding: 7px;
+ color: darken( $gray, 10% );
+ font-size: 11px;
+ line-height: 1;
+ text-transform: uppercase;
+
+ &:disabled {
+ color: lighten( $gray, 30% );
+ }
+ .gridicon {
+ top: 4px;
+ margin-top: -8px;
+ }
+ }
+ &.hidden {
+ display: none;
+ }
+ .gridicon {
+ position: relative;
+ top: 4px;
+ margin-top: -2px;
+ width: 18px;
+ height: 18px;
+ }
+}
+
+// Primary buttons
+.button.is-primary {
+ background: $blue-medium;
+ border-color: $blue-wordpress;
+ color: $white;
+
+ &:hover,
+ &:focus {
+ border-color: $blue-dark;
+ color: $white;
+ }
+ &[disabled] {
+ background: tint( $blue-light, 50% );
+ border-color: tint( $blue-wordpress, 55% );
+ color: $white;
+ }
+ &.is-compact {
+ color: $white;
+ }
+}
+
+// Scary buttons
+.button.is-scary,
+.button.is-dangerous {
+ color: $alert-red;
+
+ &[disabled] {
+ color: lighten( $alert-red, 30% );
+ }
+ &:hover,
+ &:focus {
+ border-color: $alert-red;
+ }
+ &:focus {
+ box-shadow: 0 0 0 2px lighten( $alert-red, 20% );
+ }
+}
+
+.button.is-primary.is-scary,
+.button.is-destructive {
+ background: $alert-red;
+ border-color: darken( $alert-red, 20% );
+ color: $white;
+
+ &:hover,
+ &:focus {
+ border-color: darken( $alert-red, 30% );
+ }
+ &[disabled] {
+ background: lighten( $alert-red, 20% );
+ border-color: tint( $alert-red, 30% );
+ }
+}
+
+// ==========================================================================
+// Deprecated styles
+//
+// `button.is-destructive` and ``.button.is-dangerous` are also deprecated
+// ==========================================================================
+
+.button.is-link {
+ background: transparent;
+ border: none;
+ border-radius: 0;
+ padding: 0;
+ color: $blue-medium;
+ font-weight: 400;
+ font-size: inherit;
+ line-height: 1.65;
+
+ &:hover,
+ &:focus,
+ &:active {
+ color: $link-highlight;
+ box-shadow: none;
+ }
+}
+
+// Positions and resets font styles of noticons applied to buttons
+.button.noticon {
+ line-height: inherit;
+
+ &:before {
+ display: inline-block;
+ vertical-align: middle;
+ margin-top: -2px;
+ font-size: 16px;
+ font-style: normal;
+ font-weight: normal;
+ }
+}
+
+// ==========================================================================
+// Resets
+// ==========================================================================
+
+// Turn Reset 'buttons' into regular text links
+.wp-content input {
+ &[type=reset],
+ &[type=reset]:hover,
+ &[type=reset]:active,
+ &[type=reset]:focus {
+ background: 0 0;
+ border: 0;
+ padding: 0 2px 1px;
+ width: auto;
+ box-shadow: none;
+ }
+}
+
+// Buttons within sentences sit on the text baseline.
+.wp-content p .button {
+ vertical-align: baseline
+}
+
+// Firefox Junk
+.wp-content button::-moz-focus-inner,
+.wp-content input[type=reset]::-moz-focus-inner,
+.wp-content input[type=button]::-moz-focus-inner,
+.wp-content input[type=submit]::-moz-focus-inner {
+ border-width: 1px 0;
+ border-style: solid none;
+ border-color: transparent;
+ padding: 0
+}
diff --git a/client/components/chart/README.md b/client/components/chart/README.md
new file mode 100644
index 00000000000000..013006c94f86bf
--- /dev/null
+++ b/client/components/chart/README.md
@@ -0,0 +1,50 @@
+Chart
+=====
+
+This module renders a dataset as an HTML-based chart representing the data.
+
+## Usage
+
+```js
+
+// require the component
+var ElementChart = require( 'my-sites/chart' );
+
+// And use it inline inside the render method of another component
+render: function() {
+ return(
+ } data={ } barClick={ } />
+ );
+}
+
+// Example Data Array
+
+[ {
+ 'label': , // x-axis label
+ 'value': , // bar value
+ 'nestedValue': , // nested bar value or null if no nested bar
+ 'className': , // classname(s) applied to bar container
+ 'data': , // any data that you want to have access to in the barClick callback
+ 'tooltipData': [
+ {
+ label: ,
+ value: ,
+ link: ,
+ icon: ,
+ className:
+ }
+ ]
+} ]
+
+
+```
+
+## Required Props
+
+* loading — Any truthy value indicates the chart is loading
+* data — An array of data objects using the format outlined above
+
+## Optional Props
+* minTouchBarWidth — _default: 42_ The minimum bar width on touch devices
+* minBarWidth — _default: 15_ The minimum bar width on non-touch devices
+* barClick - The function to be called when a bar is clicked on the chart, it is passed the entire data object of the bar
\ No newline at end of file
diff --git a/client/components/chart/bar-container.jsx b/client/components/chart/bar-container.jsx
new file mode 100644
index 00000000000000..6cd6db5f2381c7
--- /dev/null
+++ b/client/components/chart/bar-container.jsx
@@ -0,0 +1,65 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var Bar = require( './bar' ),
+ XAxis = require( './x-axis' ),
+ user = require( 'lib/user' )();
+
+module.exports = React.createClass( {
+ displayName: 'ModuleChartBarContainer',
+
+ propTypes: {
+ isTouch: React.PropTypes.bool,
+ data: React.PropTypes.array,
+ yAxisMax: React.PropTypes.number,
+ width: React.PropTypes.number,
+ barClick: React.PropTypes.func
+ },
+
+ buildBars: function( max ) {
+ var bars,
+ numberBars = this.props.data.length,
+ tooltipPosition = user.isRTL() ? 'bottom left' : 'bottom right',
+ width = this.props.chartWidth,
+ barWidth = ( width / numberBars );
+
+ bars = this.props.data.map( function ( item, index ) {
+ var barOffset = barWidth * ( index + 1 );
+
+ if (
+ ( ( barOffset + 230 ) > width ) &&
+ ( ( ( barOffset + barWidth ) - 230 ) > 0 )
+ ) {
+ tooltipPosition = user.isRTL() ? 'bottom right' : 'bottom left';
+ }
+
+ return ;
+ }, this );
+
+ return bars;
+ },
+
+ render: function() {
+ return (
+
+
+ { this.buildBars( this.props.yAxisMax ) }
+
+
+
+ );
+ }
+} );
\ No newline at end of file
diff --git a/client/components/chart/bar.jsx b/client/components/chart/bar.jsx
new file mode 100644
index 00000000000000..48c228964640b6
--- /dev/null
+++ b/client/components/chart/bar.jsx
@@ -0,0 +1,133 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ classNames = require( 'classnames' ),
+ noop = require( 'lodash/utility/noop' ),
+ debug = require( 'debug' )( 'calypso:module-chart:bar' );
+
+/**
+ * Internal dependencies
+ */
+var Popover = require( 'components/popover' ),
+ Tooltip = require( 'components/chart/tooltip' );
+
+module.exports = React.createClass( {
+ displayName: 'ModuleChartBar',
+
+ propTypes: {
+ isTouch: React.PropTypes.bool,
+ tooltipPosition: React.PropTypes.string,
+ className: React.PropTypes.string,
+ clickHandler: React.PropTypes.func,
+ data: React.PropTypes.object.isRequired,
+ max: React.PropTypes.number,
+ count: React.PropTypes.number
+ },
+
+ getInitialState: function() {
+ return { showPopover: false };
+ },
+
+ buildSections: function() {
+ var value = this.props.data.value,
+ max = this.props.max,
+ percentage = max ? Math.ceil( ( value / max ) * 10000 ) / 100 : 0,
+ remain = 100 - percentage,
+ remainFloor = Math.max( 1, Math.floor( remain ) ),
+ sections = [],
+ remainStyle,
+ valueStyle,
+ nestedValue = this.props.data.nestedValue,
+ nestedBar,
+ nestedPercentage,
+ nestedStyle,
+ spacerClassOptions = {
+ 'chart__bar-section': true,
+ 'is-spacer': true,
+ 'is-ghost': ( 100 === remain ) && ! this.props.active
+ };
+
+ remainStyle = {
+ height: remainFloor + '%'
+ };
+
+ sections.push(
);
+
+ valueStyle = {
+ top: remainFloor + '%'
+ };
+
+ if ( nestedValue ) {
+ nestedPercentage = value ? Math.ceil( ( nestedValue / value ) * 10000 ) / 100 : 0;
+
+ nestedStyle = { height: nestedPercentage + '%' };
+
+ nestedBar = (
);
+ }
+
+ sections.push( { nestedBar }
);
+
+ sections.push( { this.props.label }
);
+
+ return sections;
+ },
+
+ clickHandler: function(){
+ if ( 'function' === typeof( this.props.clickHandler ) ) {
+ this.props.clickHandler( this.props.data );
+ }
+ },
+
+
+ mouseEnter: function(){
+ this.setState( { showPopover: true } );
+ },
+
+ mouseLeave: function() {
+ this.setState( { showPopover: false } );
+ },
+
+ render: function() {
+ debug( 'Rendering bar', this.state );
+
+ var barStyle,
+ barClass,
+ count = this.props.count || 1,
+ tooltip;
+
+ barClass = { chart__bar: true };
+
+ if ( this.props.className ){
+ barClass[ this.props.className ] = true;
+ }
+
+ barStyle = {
+ width: ( ( 1 / count ) * 100 ) + '%'
+ };
+
+ if ( this.props.data.tooltipData && this.props.data.tooltipData.length && ! this.props.isTouch ) {
+ tooltip =
+
+ ;
+ }
+
+ return (
+
+ { this.buildSections() }
+
+
+
+ { tooltip }
+
+ );
+ }
+} );
\ No newline at end of file
diff --git a/client/components/chart/index.jsx b/client/components/chart/index.jsx
new file mode 100644
index 00000000000000..96f3ccca4c4c5a
--- /dev/null
+++ b/client/components/chart/index.jsx
@@ -0,0 +1,161 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ debug = require( 'debug' )( 'calypso:chart' ),
+ noop = require( 'lodash/utility/noop' ),
+ throttle = require( 'lodash/function/throttle' );
+
+/**
+ * Internal dependencies
+ */
+var BarContainer = require( './bar-container' ),
+ observe = require( 'lib/mixins/data-observe' ),
+ touchDetect = require( 'lib/touch-detect' );
+
+module.exports = React.createClass( {
+ displayName: 'ModuleChart',
+
+ mixins: [ observe( 'dataList' ) ],
+
+ propTypes: {
+ loading: React.PropTypes.bool,
+ data: React.PropTypes.array,
+ minTouchBarWidth: React.PropTypes.number,
+ minBarWidth: React.PropTypes.number,
+ barClick: React.PropTypes.func
+ },
+
+ getInitialState: function() {
+ return {
+ maxBars: 100, // arbitrarily high number. This will be calculated by resize method
+ width: 650
+ };
+ },
+
+ getDefaultProps: function() {
+ return {
+ minTouchBarWidth: 42,
+ minBarWidth: 15,
+ barClick: noop
+ };
+ },
+
+ // Add listener for window resize
+ componentDidMount: function() {
+ this.resize = throttle( this.resize, 400 );
+ window.addEventListener( 'resize', this.resize );
+ this.resize();
+ },
+
+ // Remove listener
+ componentWillUnmount: function() {
+ window.removeEventListener( 'resize', this.resize );
+ },
+
+ componentWillReceiveProps: function( nextProps ) {
+ if ( this.props.loading && ! nextProps.loading ) {
+ this.resize();
+ }
+ },
+
+ resize: function() {
+ if ( this.isMounted() ) {
+ var node = this.getDOMNode(),
+ width = node.clientWidth - 82,
+ maxBars;
+
+ if ( touchDetect.hasTouch() ) {
+ width = ( width <= 0 ) ? 350 : width; // mobile safari bug with zero width
+ maxBars = Math.floor( width / this.props.minTouchBarWidth );
+ } else {
+ maxBars = Math.floor( width / this.props.minBarWidth );
+ }
+
+ this.setState( {
+ maxBars: maxBars,
+ width: width
+ } );
+ }
+ },
+
+ getYAxisMax: function( values ) {
+ var max = Math.max.apply( null, values ),
+ operand = Math.pow( 10, ( max.toString().length - 1 ) ),
+ rounded = ( Math.ceil( ( max + 1 ) / operand ) * operand );
+
+ if ( rounded < 10 ) {
+ rounded = 10;
+ }
+
+ return rounded;
+ },
+
+ getData: function() {
+ var data = this.props.data;
+
+ data = data.slice( 0 - this.state.maxBars );
+
+ return data;
+ },
+
+ getValues: function() {
+ var data = this.getData();
+
+ data = data.map( function ( item ) {
+ return item.value;
+ }, this );
+
+ return data;
+ },
+
+ isEmptyChart: function( values ) {
+ values = values.filter( function( value ) {
+ return value > 0;
+ }, this );
+
+ return values.length === 0;
+ },
+
+ render: function() {
+ debug( 'Rendering chart with props: ', this.props );
+
+ var values = this.getValues(),
+ yAxisMax = this.getYAxisMax( values ),
+ data = this.getData(),
+ emptyChart;
+
+ // If we have an empty chart, show a message
+ // @todo this message needs to either use a or make a custom "chart__notice" class
+ if ( values.length && this.isEmptyChart( values ) ) {
+ emptyChart = (
+
+
+ { this.translate( 'No activity this period', {
+ context: 'Message on empty bar chart in Stats',
+ comment: 'Should be limited to 32 characters to prevent wrapping'
+ } ) }
+
+
+ );
+ }
+
+ return (
+
+
+
+
{ this.numberFormat( 100000 ) }
+
{ this.numberFormat( yAxisMax ) }
+
{ this.numberFormat( yAxisMax / 2 ) }
+
{ this.numberFormat( 0 ) }
+
+
+ { emptyChart }
+
+ );
+ }
+} );
diff --git a/client/components/chart/label.jsx b/client/components/chart/label.jsx
new file mode 100644
index 00000000000000..9502fa6d13cf86
--- /dev/null
+++ b/client/components/chart/label.jsx
@@ -0,0 +1,35 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ debug = require( 'debug' )( 'calypso:module-chart:label' );
+
+/**
+ * Internal dependencies
+ */
+var user = require( 'lib/user' )();
+
+module.exports = React.createClass( {
+ displayName: 'ModuleChartLabel',
+
+ propTypes: {
+ width: React.PropTypes.number.isRequired,
+ x: React.PropTypes.number.isRequired,
+ label: React.PropTypes.string.isRequired
+ },
+
+ render: function() {
+ debug( 'Rendering label' );
+
+ var labelStyle,
+ dir = user.isRTL() ? 'right' : 'left';
+
+ labelStyle = {
+ width: this.props.width + 'px'
+ };
+
+ labelStyle[ dir ] = this.props.x + 'px';
+
+ return { this.props.label }
;
+ }
+} );
\ No newline at end of file
diff --git a/client/components/chart/legend.jsx b/client/components/chart/legend.jsx
new file mode 100644
index 00000000000000..2f8000601095b5
--- /dev/null
+++ b/client/components/chart/legend.jsx
@@ -0,0 +1,86 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ PureRenderMixin = React.addons.PureRenderMixin,
+ debug = require( 'debug' )( 'calypso:module-chart:legend' );
+
+/**
+ * Internal dependencies
+ */
+
+var LegendItem = React.createClass( {
+ displayName: 'ModuleChartLegendItem',
+
+ mixins: [ PureRenderMixin ],
+
+ propTypes: {
+ checked: React.PropTypes.bool.isRequired,
+ label: React.PropTypes.oneOfType( [ React.PropTypes.object, React.PropTypes.string ] ),
+ attr: React.PropTypes.string.isRequired,
+ changeHandler: React.PropTypes.func.isRequired
+ },
+
+ clickHandler: function() {
+ this.props.changeHandler( this.props.attr );
+ },
+
+ render: function() {
+ return (
+
+
+
+ { this.props.label }
+
+
+ );
+ }
+
+} );
+
+var Legend = React.createClass( {
+ displayName: 'ModuleChartLegend',
+
+ propTypes: {
+ activeTab: React.PropTypes.object.isRequired,
+ tabs: React.PropTypes.array.isRequired,
+ activeCharts: React.PropTypes.array.isRequired,
+ availableCharts: React.PropTypes.array.isRequired,
+ clickHandler: React.PropTypes.func.isRequired
+ },
+
+ onFilterChange: function( chartItem ) {
+ this.props.clickHandler( chartItem );
+ },
+
+ render: function() {
+ debug( 'Rendering legend', this.props );
+ var legendColors = [ 'chart__legend-color is-dark-blue' ],
+ tab = this.props.activeTab,
+ legendItems;
+
+ legendItems = this.props.availableCharts.map( function( legendItem, index ) {
+ var colorClass = legendColors[ index ],
+ checked = ( -1 !== this.props.activeCharts.indexOf( legendItem ) ),
+ tab;
+
+ tab = this.props.tabs.filter( function( tab ) {
+ return tab.attr === legendItem;
+ } ).shift();
+
+ return ;
+ }, this );
+
+
+ return (
+
+
+ { tab.label }
+ { legendItems }
+
+
+ );
+ }
+} );
+
+module.exports = Legend;
\ No newline at end of file
diff --git a/client/components/chart/style.scss b/client/components/chart/style.scss
new file mode 100644
index 00000000000000..bdf4174b121749
--- /dev/null
+++ b/client/components/chart/style.scss
@@ -0,0 +1,566 @@
+// Chart
+// Life is a statistical anomaly
+
+.chart {
+ position: relative;
+ box-sizing: border-box;
+ background-color: $white;
+ padding: 8px 0 8px 20px;
+}
+
+// Y-axis
+
+// Y-axis markers (lines)
+// 1: Corresponds to padding of chart
+
+.chart .chart__y-axis-markers {
+ position: absolute;
+ top: 8px; // 1
+ left: 0;
+ right: 0;
+ height: 200px;
+}
+
+.chart .chart__y-axis-marker {
+ position: absolute;
+ top: 0;
+ width: 100%;
+ height: 1px;
+ border-top: 1px solid lighten( $gray, 30% );
+}
+
+// Y-axis marker lines inside each chart__bar
+// (This is needed so that bars overlap correctly)
+.chart__bar-marker {
+ z-index: 1;
+ position: absolute;
+ top: 0;
+ width: 100%;
+ height: 1px;
+ border-top: 1px solid rgba( lighten( $gray, 30% ), .1 );
+}
+
+.chart__bar-marker,
+.chart__y-axis-label,
+.chart .chart__y-axis-marker {
+ &.is-fifty {
+ top: 50%;
+ }
+
+ &.is-zero {
+ top: 100%;
+ }
+}
+
+// Y-axis labels
+// 1: matches Y-axis padding
+
+.chart__y-axis {
+ position: relative;
+ float: right;
+ height: 200px;
+ padding: 0 20px 0 10px;
+ font-size: 11px;
+ color: darken( $gray, 10% );
+ margin-bottom: 30px;
+}
+
+.chart__y-axis-label {
+ position: absolute;
+ top: 0;
+ right: 20px; // 1
+ text-align: right;
+}
+
+// For forcing the width of y-axis to the width of the label
+.chart__y-axis-width-fix {
+ color: $transparent;
+}
+
+// X-axis
+// 1: hides spaces between elements
+
+.chart__x-axis {
+ position: relative;
+ font-size: 0; // 1
+ padding: 5px 0;
+ min-height: 18px;
+ color: darken( $gray, 30% );
+}
+
+.chart__x-axis-label {
+ position: absolute;
+ display: inline-block;
+ vertical-align: top;
+ font-size: 11px;
+ text-align: center;
+}
+
+// X-axis label indicator
+// (vertical thin grey bar)
+
+.chart__x-axis-label::before {
+ content: '';
+ display: block;
+ position: absolute;
+ top: -4px;
+ left: 50%;
+ margin-left: -.5px;
+ width: 1px;
+ height: 5px;
+ background: $gray-light;
+ background-image: linear-gradient(to bottom, $gray-light 0%, lighten( $gray, 20% ) 100%);
+}
+
+// Bar wrapper
+// 1: hides spaces between elements
+
+.chart__bars {
+ position: relative;
+ font-size: 0; // 1
+ height: 200px;
+ text-align: center;
+ overflow: hidden;
+ display: -ms-flex;
+ display: flex;
+}
+
+// Individual bar
+// 1: Needs to be relative so that the contained graphic bar has boundaries
+
+.chart__bar {
+ text-align: center;
+ display: inline-block;
+ position: relative; // 1
+ height: 200px;
+ -ms-flex-grow: 1;
+ flex-grow: 1;
+ -ms-flex-shrink: 1;
+ flex-shrink: 1;
+
+
+ &.is-weekend {
+ background-color: rgba( lighten( $gray, 30% ), .5 );
+ }
+
+ &:hover {
+ cursor: pointer;
+ background-color: rgba( lighten( $gray, 30% ), .3 );
+ }
+
+ &.is-selected {
+ cursor: default;
+ background-color: rgba( $orange-jazzy, .1 );
+ }
+}
+
+// Individual bar wrapper & misc
+// 1: Positions the bar in the space as defined by .bar
+// 2: Default value for top so bars grow when they get a new value
+// (doesn't function correctly for period switching right now, not sure why)
+// (also doesn't work because updating the DOM is so heavy it stalls all animations)
+
+.chart__bar-section {
+ display: inline-block;
+ background-color: $blue-wordpress;
+ position: absolute;
+ top: 0; // 2
+ right: 16%; // 1
+ bottom: 0; // 1
+ left: 16%; // 1
+ z-index: 2;
+ transition: top .3s ease-out; // 2
+
+ .chart__bar:hover &.is-bar {
+ background-color: $blue-medium;
+ }
+
+ .chart__bar.is-selected &.is-bar {
+ background-color: $orange-jazzy;
+ }
+
+ &.is-spacer {
+ z-index: 0;
+ background-color: $transparent;
+ }
+
+ &.is-ghost::after {
+ content: "";
+ display: block;
+ position: absolute;
+ top: 160px;
+ bottom: 0;
+ left: 0;
+ z-index: 1;
+ width: 100%;
+ height: 40px;
+ background-image: linear-gradient(to bottom, $transparent, rgba( lighten( $gray, 30% ), .5 ) ); // TODO: needs to use default color for gradient
+
+ .chart__bar:hover & {
+ display: none;
+ }
+ }
+}
+
+.chart__bar-section-inner {
+ background: darken( $blue-dark, 5% );
+ position: absolute;
+ right: 23.33%;
+ bottom: 0;
+ left: 23.33%;
+
+ .chart__bar.is-selected & {
+ background-color: $orange-fire;
+ }
+}
+
+// Chart legend (wrapper)
+// 1: L/R matches padding of y-axis labels in chart
+
+.chart__legend {
+ @include clear-fix;
+ margin-bottom: -8px;
+}
+
+// Chart legend options (list)
+
+.chart__legend .chart__legend-options {
+ float: right;
+ color: lighten( $gray-dark, 20% );
+ list-style-type: none;
+ margin: 0;
+ font-size: 11px;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+
+ @include breakpoint( "<480px" ) {
+ width: 100%;
+ }
+
+}
+
+// Chart legend option (list item)
+
+.chart__legend-option {
+ display: inline;
+ text-align: left;
+
+ // Expand labels to create bigger touch targets
+ @include breakpoint( "<480px") {
+ width: 50%;
+ display: inline-block;
+ }
+}
+
+// Chart legend label
+// 1: 19/10px instead of 20/12px because it aligns better optically
+
+.chart__legend-label {
+ display: inline-block;
+ padding: 12px 19px 10px 20px; // 1
+
+ &.is-selectable {
+ cursor: pointer;
+
+ &:focus,
+ &:hover {
+ color: $link-highlight;
+ }
+ }
+
+ @include breakpoint( "<480px" ) {
+ display: block;
+ }
+}
+
+// Chart legend color
+// 1: Needed to overvwrite form styles in main stylesheets
+// 2: Make leftmost legend fit snugly up against the leftmost bars
+
+.chart__legend-option .chart__legend-color {
+ width: 10px;
+ height: 10px;
+ background: $blue-wordpress;
+ display: inline-block;
+ border-radius: 1px;
+ vertical-align: top;
+ margin: 3px 5px 3px 8px; // 1
+}
+
+@include breakpoint( "<480px" ) {
+ .chart__legend-option:first-child .chart__legend-color {
+ margin-left: 2px; // 2
+ }
+}
+
+.chart__legend-color.is-dark-blue {
+ background: darken( $blue-dark, 5% );
+}
+
+// Chart legend checkbox
+
+.chart__legend-option .chart__legend-checkbox {
+ margin: 0;
+ float: none;
+ vertical-align: top;
+}
+
+// Chart empty (message)
+// A message displayed when there's absolutely no data to chart
+
+.chart__empty {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ text-align: center;
+ font-size: 14px;
+ line-height: 24px;
+ clear: both;
+ z-index: 1;
+}
+
+.chart__empty_notice {
+ position: relative;
+ top: 97px;
+ padding: 11px 24px;
+ margin-bottom: 24px;
+ border-radius: 1px;
+ background: #fff;
+ box-sizing: border-box;
+ font-size: 14px;
+ line-height: 1.4285;
+ animation: appear .3s ease-in-out;
+ box-shadow: 0 0 0 1px transparentize( lighten( $gray, 20% ), .5 ),
+ 0 1px 2px lighten( $gray, 30% );
+
+ @include breakpoint( ">660px" ) {
+ padding: 13px 48px;
+ font-size: inherit;
+
+ &::before {
+ @extend %noticon;
+ content: '\f456';
+ position: absolute;
+ top: 23px;
+ left: 20px;
+ margin: -12px 0px 0 -8px;
+ font-size: 24px;
+ line-height: 1;
+ }
+ }
+}
+
+// Chart tooltip
+
+.chart__tooltip {
+ z-index: 1000;
+ position: absolute;
+ /* default offset for edge-cases: https://github.com/component/tip/pull/12 */
+ top: 0;
+ left: 0 #{"/*rtl:ignore*/"};
+ right: auto #{"/*rtl:ignore*/"};
+
+ &.tip-bottom-left {
+ .tip-arrow {
+ border-color: $transparent;
+ position: absolute;
+
+ &::before {
+ content: '';
+ border: 4px solid $transparent;
+ border-bottom-color: darken( $gray, 30% );
+ position: absolute;
+ left: 231px #{"/*rtl:ignore*/"};
+ top: -3px;
+
+ .rtl & {
+ left: -18px #{"/*rtl:ignore*/"};
+ top: -3px;
+ }
+ }
+ }
+ }
+
+ &.tip-bottom-right,
+ &.tip-bottom {
+ .tip-arrow {
+ border-color: $transparent;
+ position: absolute;
+
+ &::before {
+ content: '';
+ border: 4px solid $transparent;
+ border-bottom-color: darken( $gray, 30% );
+ position: absolute;
+ left: 11px #{"/*rtl:ignore*/"};
+ top: -3px;
+
+ .rtl & {
+ left: -238px #{"/*rtl:ignore*/"};
+ top: -3px;
+ }
+ }
+ }
+ }
+
+ &.tip-top {
+ .tip-arrow {
+ border-color: $transparent;
+ position: absolute;
+
+ &::before {
+ content: '';
+ border: 4px solid $transparent;
+ border-top-color: darken( $gray, 30% );
+ position: absolute;
+ left: 118px #{"/*rtl:ignore*"};
+ bottom: -40px;
+ }
+ }
+ }
+
+ &.tip-bottom {
+ .tip-arrow {
+ &::before {
+ left: 86px #{"/*rtl:ignore*/"};
+ top: -4px;
+
+ .rtl & {
+ left: -93px #{"/*rtl:ignore*/"};
+ }
+ }
+ }
+ }
+
+ .tip-inner {
+ border: 0px;
+ box-shadow: none;
+ border-radius: 2px;
+ color: $white;
+ background: darken( $gray, 30% );
+ font-size: 11px;
+ padding: 6px 10px;
+ margin-top: 5px;
+ width: 230px;
+ text-align: left;
+
+ ul {
+ @include clear-fix;
+ list-style: none;
+ margin: 0;
+ padding: 0;
+
+ li {
+ font-size: 11px;
+ text-transform: uppercase;
+ font-weight: 100;
+ height: 24px;
+ letter-spacing: 0.1em;
+ border: 0;
+
+ .wrapper {
+ display: block;
+ line-height: inherit;
+ line-height: 24px;
+ clear: both;
+ }
+
+ .value {
+ text-align: right;
+ float: right;
+ min-width: 22px;
+ color: lighten( $gray, 20% );
+ }
+
+ .label {
+ display: block;
+ overflow: hidden;
+ word-break: break-all;
+ vertical-align: baseline;
+ }
+
+ .gridicon {
+ vertical-align: middle;
+ margin-right: 6px;
+ margin-top: -3px;
+ }
+ }
+ }
+ }
+
+ &.is-streak {
+ .tip-arrow {
+ &::before {
+ left: 87px;
+ }
+ }
+
+ .tip-inner {
+ width: 160px;
+ position: relative;
+ top: -10px;
+
+ li {
+ height: 14px;
+
+ .label {
+ width: 100%;
+ float: left;
+ text-align: center;
+
+ .rtl & {
+ font-size: 11px;
+ }
+
+ .post-count {
+ font-weight: bold;
+ }
+ }
+ .value {
+ float: none;
+ }
+ }
+ }
+ }
+}
+
+.chart__tooltip .module-content-list-item {
+
+ &.is-date-label {
+ font-size: 11px;
+ margin-bottom: 2px;
+ text-transform: uppercase;
+ font-weight: bold;
+ border-bottom: 1px solid darken( $gray, 27% );
+ padding-bottom: 2px;
+ }
+
+ &.is-published-item {
+ height: 19px;
+
+ .label {
+ text-transform: none;
+ color: lighten( $gray, 20% );
+ overflow: hidden;
+ letter-spacing: 0;
+ height: 19px;
+ }
+
+ .value {
+ width: 0;
+ min-width: 0;
+
+ &::before {
+ content: '';
+ position: relative;
+ background-image: linear-gradient(to right, rgba(61, 89, 109, 0) 0%, rgba(61, 89, 109, 0.5), rgba(61, 89, 109, 1));
+ left: -30px;
+ width: 30px;
+ height: 24px;
+ display: block;
+ }
+ }
+ }
+}
diff --git a/client/components/chart/tooltip.jsx b/client/components/chart/tooltip.jsx
new file mode 100644
index 00000000000000..f9ac4761d2fec2
--- /dev/null
+++ b/client/components/chart/tooltip.jsx
@@ -0,0 +1,47 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var Gridicon = require( 'components/gridicon' );
+
+module.exports = React.createClass( {
+ displayName: 'ChartTooltip',
+
+ propTypes: {
+ data: React.PropTypes.array.isRequired
+ },
+
+ render: function() {
+ var listItemElements;
+
+ listItemElements = this.props.data.map( function( options, i ) {
+ var wrapperClasses = [ 'module-content-list-item' ],
+ gridiconSpan;
+
+ if ( options.icon ) {
+ gridiconSpan = ( );
+ }
+
+ wrapperClasses.push( options.className );
+
+ return (
+
+
+ { options.value }
+ { gridiconSpan }{ options.label }
+
+
+ );
+ } );
+
+ return (
+
+ );
+ }
+} );
diff --git a/client/components/chart/x-axis.jsx b/client/components/chart/x-axis.jsx
new file mode 100644
index 00000000000000..96f41f6b7b27b3
--- /dev/null
+++ b/client/components/chart/x-axis.jsx
@@ -0,0 +1,107 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ debug = require( 'debug' )( 'calypso:module-chart:x-axis' ),
+ throttle = require( 'lodash/function/throttle' );
+
+/**
+ * Internal dependencies
+ */
+var Label = require( './label' );
+
+module.exports = React.createClass( {
+ displayName: 'ModuleChartXAxis',
+
+ propTypes: {
+ labelWidth: React.PropTypes.number.isRequired,
+ data: React.PropTypes.array.isRequired
+ },
+
+ getInitialState: function() {
+ return {
+ divisor: 1,
+ spacing: this.props.labelWidth
+ };
+ },
+
+ // Add listener for window resize
+ componentDidMount: function() {
+ this.resizeThrottled = throttle( this.resize, 400 );
+ window.addEventListener( 'resize', this.resizeThrottled );
+ this.resize();
+ },
+
+ // Remove listener
+ componentWillUnmount: function() {
+ if( this.resizeThrottled.cancel ) {
+ this.resizeThrottled.cancel();
+ }
+ window.removeEventListener( 'resize', this.resizeThrottled );
+ },
+
+ componentWillReceiveProps: function( nextProps ) {
+ this.resize( nextProps );
+ },
+
+ resize: function( nextProps ) {
+ if ( this.isMounted() ) {
+ var node,
+ props = this.props,
+ width,
+ dataCount,
+ spacing,
+ labelWidth,
+ divisor;
+
+ node = this.getDOMNode();
+
+ if ( nextProps && ! ( nextProps instanceof Event ) ) {
+ props = nextProps;
+ }
+
+ /**
+ * Overflow needs to be hidden to calculate the desired width,
+ * but visible to display each labels' overflow :/
+ */
+
+ node.style.overflow = 'hidden';
+ width = node.clientWidth;
+ node.style.overflow = 'visible';
+
+ dataCount = props.data.length || 1;
+ spacing = width / dataCount;
+ labelWidth = props.labelWidth;
+ divisor = Math.ceil( labelWidth / spacing );
+
+ this.setState( {
+ divisor: divisor,
+ spacing: spacing
+ } );
+ }
+ },
+
+ render: function() {
+ debug( 'Rendering chart x-axis', this.props.data );
+
+ var labels,
+ data = this.props.data;
+
+ labels = data.map( function ( item, index ) {
+ var x = ( index * this.state.spacing ) + ( ( this.state.spacing - this.props.labelWidth ) / 2 ),
+ rightIndex = data.length - index - 1,
+ label;
+
+ if ( rightIndex % this.state.divisor === 0 ) {
+ label = ;
+ }
+
+ return label;
+ }, this );
+
+ return (
+ { labels }
+ );
+ }
+} );
+
diff --git a/client/components/comment-button/README.md b/client/components/comment-button/README.md
new file mode 100644
index 00000000000000..7627a3f9224d7f
--- /dev/null
+++ b/client/components/comment-button/README.md
@@ -0,0 +1,23 @@
+Comment Button
+=========
+
+This component is used to display a button with an embedded number indicator
+
+#### How to use:
+
+```js
+const CommentButton = require( 'components/comment-button' );
+
+render: function() {
+ return (
+
+ );
+}
+```
+
+#### Props
+
+* `commentCount`: Number indicating the number of comments to be displayed next to the button.
+* `onClick`: Function to be executed when the user clicks the button.
+* `tagName`: String with the HTML tag we are going to use to render the component. Defaults to 'li'.
+* `size`: Number with the size of the comments icon to be displayed. Defaults to 24.
\ No newline at end of file
diff --git a/client/components/comment-button/docs/example.jsx b/client/components/comment-button/docs/example.jsx
new file mode 100644
index 00000000000000..bfcdd5de647b12
--- /dev/null
+++ b/client/components/comment-button/docs/example.jsx
@@ -0,0 +1,26 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var CommentButton = require( 'components/comment-button' );
+
+var AddNewButtons = React.createClass( {
+ displayName: 'CommentButton',
+
+ render: function() {
+ return (
+
+ );
+ }
+} );
+
+module.exports = AddNewButtons;
diff --git a/client/components/comment-button/index.jsx b/client/components/comment-button/index.jsx
new file mode 100644
index 00000000000000..69574e329b240b
--- /dev/null
+++ b/client/components/comment-button/index.jsx
@@ -0,0 +1,47 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ noop = require( 'lodash/utility/noop' );
+/**
+ * Internal dependencies
+ */
+var Gridicon = require( 'components/gridicon' );
+
+var CommentButton = React.createClass( {
+
+ propTypes: {
+ onClick: React.PropTypes.func,
+ tagName: React.PropTypes.string,
+ commentCount: React.PropTypes.number.isRequired
+ },
+
+ getDefaultProps: function() {
+ return {
+ onClick: noop,
+ tagName: 'li',
+ size: 24
+ };
+ },
+
+ onClick: function( event ) {
+ event.preventDefault();
+ this.props.onClick();
+ },
+
+ render: function() {
+ var containerTag = this.props.tagName;
+
+ var labelElement = ( { this.props.commentCount } );
+
+ return React.createElement(
+ containerTag, {
+ className: 'comment-button',
+ onTouchTap: this.onClick
+ },
+ , labelElement
+ );
+ }
+} );
+
+module.exports = CommentButton;
diff --git a/client/components/comment-button/style.scss b/client/components/comment-button/style.scss
new file mode 100644
index 00000000000000..344de77d4cd9a9
--- /dev/null
+++ b/client/components/comment-button/style.scss
@@ -0,0 +1,23 @@
+.comment-button {
+ color: lighten( $gray, 10 );
+ list-style-type: none;
+ padding: 4px 4px 4px 27px;
+ position: relative;
+ text-transform: uppercase;
+
+ &:hover {
+ color: $blue-light;
+ cursor: pointer;
+ }
+}
+
+.comment-button__icon {
+ fill: lighten( $gray, 10 );
+ position: absolute;
+ left: 0;
+ top: 2px;
+
+ &:hover {
+ fill: $blue-light;
+ }
+}
diff --git a/client/components/count/Makefile b/client/components/count/Makefile
new file mode 100644
index 00000000000000..a6096d682d8084
--- /dev/null
+++ b/client/components/count/Makefile
@@ -0,0 +1,10 @@
+REPORTER ?= spec
+NODE_BIN := $(shell npm bin)
+MOCHA ?= ../../../node_modules/.bin/mocha
+BASE_DIR := $(NODE_BIN)/../..
+NODE_PATH := test:$(BASE_DIR)/client:$(BASE_DIR)/shared
+
+test:
+ @NODE_ENV=test NODE_PATH=$(NODE_PATH) $(MOCHA) --compilers jsx:babel/register --reporter $(REPORTER)
+
+.PHONY: test
diff --git a/client/components/count/README.md b/client/components/count/README.md
new file mode 100644
index 00000000000000..84e094465537f3
--- /dev/null
+++ b/client/components/count/README.md
@@ -0,0 +1,30 @@
+Count
+=========
+
+Count is a React component that shows positive and negative integer numbers, by default with rounded corners. The component internationalizes the passed number. For example, it's used to show post and draft counts as well as the number of people on a team.
+
+
+
+## Usage
+
+If you want to display a count of some sort -- number of posts, drafts, team members, etc. -- use this component to keep the style consistent across components and to not worry about i18n.
+
+```jsx
+render: function() {
+ return (
+
+ Posts
+
+ );
+}
+```
+
+## Props
+
+### `count`
+
+The number to be displayed. Make sure it's a number, not a string containing a number.
+
+## Custom Styling
+
+In some cases, it may be necessary to increase the font size or remove the border. In your component's style file, specify rules for the `.count` within your component's selector. For an example, see the `select-dropdown` component's style file.
diff --git a/client/components/count/docs/example.jsx b/client/components/count/docs/example.jsx
new file mode 100644
index 00000000000000..9ed926a1b4a565
--- /dev/null
+++ b/client/components/count/docs/example.jsx
@@ -0,0 +1,28 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var Count = require( 'components/count' );
+
+module.exports = React.createClass( {
+ displayName: 'Count',
+
+ mixins: [ React.addons.PureRenderMixin ],
+
+ render: function() {
+ return (
+
+ );
+ }
+} );
diff --git a/client/components/count/index.jsx b/client/components/count/index.jsx
new file mode 100644
index 00000000000000..766f86b1b0fc0c
--- /dev/null
+++ b/client/components/count/index.jsx
@@ -0,0 +1,21 @@
+/**
+ * External dependencies
+ */
+import React from 'react/addons';
+
+export default React.createClass( {
+
+ displayName: 'Count',
+
+ mixins: [ React.addons.PureRenderMixin ],
+
+ propTypes: {
+ count: React.PropTypes.number.isRequired,
+ },
+
+ render() {
+ return (
+ { this.numberFormat( this.props.count ) }
+ );
+ }
+} );
diff --git a/client/components/count/style.scss b/client/components/count/style.scss
new file mode 100644
index 00000000000000..066265d98942e8
--- /dev/null
+++ b/client/components/count/style.scss
@@ -0,0 +1,11 @@
+.count {
+ display: inline-block;
+ padding: 1px 6px;
+ border: solid 1px $gray;
+ border-radius: 12px;
+ font-size: 11px;
+ font-weight: 600;
+ line-height: 14px;
+ color: $gray;
+ text-align: center;
+}
diff --git a/client/components/count/test/index.jsx b/client/components/count/test/index.jsx
new file mode 100644
index 00000000000000..daeb9937d2da4c
--- /dev/null
+++ b/client/components/count/test/index.jsx
@@ -0,0 +1,98 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react/addons' ),
+ ReactInjection = require( 'react/lib/ReactInjection' ),
+ TestUtils = React.addons.TestUtils,
+ expect = require( 'chai' ).expect,
+ sinon = require( 'sinon' );
+
+/**
+ * Internal dependencies
+ */
+var i18n = require( 'lib/mixins/i18n' );
+
+describe( 'Count', function() {
+ var Count, renderer;
+
+ before( function() {
+ i18n.initialize();
+ ReactInjection.Class.injectMixin( i18n.mixin );
+ Count = require( '../' );
+ } );
+
+ beforeEach( function() {
+ renderer = TestUtils.createRenderer();
+ } );
+
+ it( 'should render the passed count', function() {
+ var result;
+
+ renderer.render( );
+ result = renderer.getRenderOutput();
+
+ expect( result.props.className ).to.equal( 'count' );
+ expect( result.props.children ).to.equal( '23' );
+ } );
+
+ it( 'should use the correct class name', function() {
+ var result;
+
+ renderer.render( );
+ result = renderer.getRenderOutput();
+
+ expect( result.props.className ).to.equal( 'count' );
+ } );
+
+ it( 'should internationalize the passed count', function() {
+ var result;
+
+ renderer.render( );
+ result = renderer.getRenderOutput();
+
+ expect( result.props.children ).to.equal( '2,317' );
+ } );
+
+ it( 'should render zero', function() {
+ var result;
+
+ renderer.render( );
+ result = renderer.getRenderOutput();
+
+ expect( result.props.children ).to.equal( '0' );
+ } );
+
+ it( 'should render negative numbers', function() {
+ var result;
+
+ renderer.render( );
+ result = renderer.getRenderOutput();
+
+ expect( result.props.children ).to.equal( '-1,000' );
+ } );
+
+ it( 'should cut off floating point numbers', function() {
+ var result;
+
+ renderer.render( );
+ result = renderer.getRenderOutput();
+
+ expect( result.props.children ).to.equal( '3' );
+ } );
+
+ it( 'should warn when passing something that is not a number', function() {
+ var result, oldWarn;
+
+ // replace console.warn so the warning isn't shown when running the test
+ oldWarn = console.warn;
+ console.warn = function() {};
+
+ sinon.spy( console, 'warn' );
+ renderer.render( );
+ expect( console.warn ).to.have.been.called;
+
+ // put back the old console.warn
+ console.warn = oldWarn;
+ } );
+
+} );
diff --git a/client/components/data/activating-theme/README.md b/client/components/data/activating-theme/README.md
new file mode 100644
index 00000000000000..d5e8889e993e2a
--- /dev/null
+++ b/client/components/data/activating-theme/README.md
@@ -0,0 +1,8 @@
+ActivatingTheme
+================
+
+Determines whether we're activating a theme
+
+## Usage
+
+Wrap a child component with ` `. The child component will be passed the props `isActivating`, `hasActivated` and `currentTheme`
diff --git a/client/components/data/activating-theme/index.js b/client/components/data/activating-theme/index.js
new file mode 100644
index 00000000000000..fac44b6fd0a3c2
--- /dev/null
+++ b/client/components/data/activating-theme/index.js
@@ -0,0 +1,49 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var CurrentThemeStore = require( 'lib/themes/stores/current-theme' );
+
+function getState( props ) {
+ return {
+ isActivating: CurrentThemeStore.isActivating(),
+ hasActivated: CurrentThemeStore.hasActivated(),
+ currentTheme: CurrentThemeStore.getCurrentTheme( props.siteId )
+ };
+}
+
+/**
+ * Passes the activating state of themes to the supplied child component.
+ */
+var ActivatingThemeData = React.createClass( {
+
+ propTypes: {
+ children: React.PropTypes.element.isRequired
+ },
+
+ getInitialState: function() {
+ return getState( this.props );
+ },
+
+ componentWillMount: function() {
+ CurrentThemeStore.on( 'change', this.onActivatingTheme );
+ },
+
+ componentWillUnmount: function() {
+ CurrentThemeStore.off( 'change', this.onActivatingTheme );
+ },
+
+ onActivatingTheme: function() {
+ this.setState( getState( this.props ) );
+ },
+
+ render: function() {
+ return React.cloneElement( this.props.children, this.state );
+ }
+} );
+
+module.exports = ActivatingThemeData;
diff --git a/client/components/data/cart/README.md b/client/components/data/cart/README.md
new file mode 100644
index 00000000000000..ad2100455399c0
--- /dev/null
+++ b/client/components/data/cart/README.md
@@ -0,0 +1,28 @@
+CartData
+========
+
+`CartData` is a React component intended to be used as a controller-view to simplify binding and interacting with the [cart Flux module](../../../lib/cart/).
+
+## Usage
+
+Wrap a child component with ` `. [As a controller-view](https://facebook.github.io/flux/docs/overview.html#views-and-controller-views), CartData does not render any content of its own; instead, it simply renders the child component. When mounted, the component will automatically trigger a network request for data if data hasn't yet been retrieved for the site.
+
+```jsx
+var React = require( 'react' ),
+ CartData = require( 'components/data/cart-data' ),
+ MyChildComponent = require( './my-child-component' );
+
+module.exports = React.createClass( {
+ displayName: 'MyComponent',
+
+ render: function() {
+ return (
+
+
+
+ );
+ }
+} );
+```
+
+The child component should expect to receive any props defined during the render.
diff --git a/client/components/data/cart/index.jsx b/client/components/data/cart/index.jsx
new file mode 100644
index 00000000000000..7d1f2517cf597f
--- /dev/null
+++ b/client/components/data/cart/index.jsx
@@ -0,0 +1,30 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var StoreConnection = require( 'components/data/store-connection' ),
+ CartStore = require( 'lib/cart/store' );
+
+var stores = [ CartStore ];
+
+function getStateFromStores() {
+ return {
+ cart: CartStore.get()
+ };
+}
+
+var CartData = React.createClass( {
+ render: function() {
+ return (
+
+ { this.props.children }
+
+ );
+ }
+} );
+
+module.exports = CartData;
diff --git a/client/components/data/category-list-data/README.md b/client/components/data/category-list-data/README.md
new file mode 100644
index 00000000000000..1863b621535da7
--- /dev/null
+++ b/client/components/data/category-list-data/README.md
@@ -0,0 +1,46 @@
+CategoryListData
+================
+
+CategoryListData is a component which aims to ease interactions with the
+[terms flux store and actions](../../../lib/terms) related to post categories.
+
+## Usage
+
+Use ` ` to wrap a child component that will do the actual
+rendering of the view.
+
+```jsx
+var React = require( 'react' ),
+ CategoryListData = require( 'components/data/category-list-data' ),
+ MyChildComponent = require( './my-child-component' );
+
+module.exports = React.createClass( {
+ displayName: 'MyComponent',
+
+ render: function() {
+ return (
+
+
+
+ );
+ }
+} );
+```
+
+### Required Props
+
+- `siteId`: The site you would like to get category data for
+
+### Optional Props
+
+- `search`: A search string to filter the category store by
+
+## Results
+
+The child component will receive the props outlined above, along with the following:
+
+- `categories`: An ordered array of known categories for the site, or `undefined` if currently fetching data
+- `categoriesFound`: The `found` figure returned by the API which represents the total number of categories
+- `categoriesHasNextPage`: if another page of category data can be fetched
+- `categoriesFetchingNextPage`: if another page is currently being fetched
+- `categoriesOnFetchNextPage`: a function to call when more category items are needed
diff --git a/client/components/data/category-list-data/index.jsx b/client/components/data/category-list-data/index.jsx
new file mode 100644
index 00000000000000..5fe2e8db5fb8bd
--- /dev/null
+++ b/client/components/data/category-list-data/index.jsx
@@ -0,0 +1,120 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ assign = require( 'lodash/object/assign' ),
+ isEqual = require( 'lodash/lang/isEqual' ),
+ debug = require( 'debug' )( 'calypso:components:data:category-list' );
+
+/**
+ * Internal dependencies
+ */
+var TermActions = require( 'lib/terms/actions' ),
+ passToChildren = require( 'lib/react-pass-to-children' ),
+ CategoryStoreFactory = require( 'lib/terms/category-store-factory' );
+
+function getStateData( categoryStoreId, siteId ) {
+ var categoryStore = CategoryStoreFactory( categoryStoreId );
+ return {
+ categories: categoryStore.all( siteId ),
+ categoriesFound: categoryStore.found( siteId ),
+ categoriesHasNextPage: categoryStore.hasNextPage( siteId ),
+ categoriesFetchingNextPage: categoryStore.isFetchingPage( siteId )
+ };
+}
+
+module.exports = React.createClass( {
+ displayName: 'CategoryList',
+
+ propTypes: {
+ siteId: React.PropTypes.number.isRequired,
+ search: React.PropTypes.string,
+ categoryStoreId: React.PropTypes.string
+ },
+
+ getDefaultProps: function() {
+ return {
+ categoryStoreId: 'default'
+ };
+ },
+
+ getInitialState: function() {
+ return getStateData( this.props.categoryStoreId, this.props.siteId );
+ },
+
+ getCategoryStore: function() {
+ return CategoryStoreFactory( this.props.categoryStoreId );
+ },
+
+ componentDidMount: function() {
+ var siteId, query, categoryStoreId, categoryStore;
+
+ categoryStoreId = this.props.categoryStoreId;
+ categoryStore = this.getCategoryStore();
+ siteId = this.props.siteId;
+ query = this.getQuery();
+
+ setTimeout( function() {
+ TermActions.setCategoryQuery( siteId, query, categoryStoreId );
+ }, 0 );
+ categoryStore.on( 'change', this.updateState );
+ this.maybeFetchData( siteId );
+ },
+
+ componentWillUnmount: function() {
+ var categoryStore = this.getCategoryStore();
+ categoryStore.removeListener( 'change', this.updateState );
+ },
+
+ componentWillReceiveProps: function( nextProps ) {
+ var nextQuery = this.getQuery( nextProps );
+ if ( ! isEqual( this.getQuery(), nextQuery ) || nextProps.siteId !== this.props.siteId ) {
+ debug( 'updating state and possibly fetching data', nextQuery );
+ TermActions.setCategoryQuery( nextProps.siteId, nextQuery, this.props.categoryStoreId );
+ this.updateState( nextProps.siteId );
+ this.maybeFetchData( nextProps.siteId );
+ }
+ },
+
+ fetchData: function() {
+ debug( 'fetchData called', this.props.siteId );
+ TermActions.fetchNextCategoryPage( this.props.siteId, this.props.categoryStoreId );
+ },
+
+ getQuery: function( props ) {
+ var query = {};
+
+ props = props || this.props;
+
+ if ( props.search ) {
+ query.search = props.search;
+ }
+
+ return query;
+ },
+
+ maybeFetchData: function( siteId ) {
+ var categoryStore = this.getCategoryStore(),
+ categoryStoreId = this.props.categoryStoreId;
+
+ if ( categoryStore.all( siteId ) ) {
+ return;
+ }
+
+ debug( 'calling fetchNextCategoryPage', siteId );
+ setTimeout( function() {
+ TermActions.fetchNextCategoryPage( siteId, categoryStoreId );
+ }, 0 );
+ },
+
+ updateState: function( siteId ) {
+ this.setState( getStateData( this.props.categoryStoreId, siteId || this.props.siteId ) );
+ },
+
+ render: function() {
+ debug( 'rendering category data for site ' + this.props.siteId + ' and categoryStore: ' + this.props.categoryStoreId, this.state );
+ return passToChildren( this, assign( {}, this.state, {
+ categoriesFetchNextPage: this.fetchData
+ } ) );
+ }
+} );
diff --git a/client/components/data/checkout/README.md b/client/components/data/checkout/README.md
new file mode 100644
index 00000000000000..060940a27387b8
--- /dev/null
+++ b/client/components/data/checkout/README.md
@@ -0,0 +1,28 @@
+CheckoutData
+============
+
+`CheckoutData` is a React component intended to be used as a controller-view to simplify binding and interacting with the Flux modules required for the [checkout components](../../../my-sites/upgrades/checkout/).
+
+## Usage
+
+Wrap a child component with ` `. [As a controller-view](https://facebook.github.io/flux/docs/overview.html#views-and-controller-views), `CheckoutData` does not render any content of its own; instead, it simply renders the child component. When mounted, the component will automatically trigger a network request for data if data hasn't yet been retrieved for the site.
+
+```jsx
+var React = require( 'react' ),
+ CheckoutData = require( 'components/data/checkout' ),
+ MyChildComponent = require( './my-child-component' );
+
+module.exports = React.createClass( {
+ displayName: 'MyComponent',
+
+ render: function() {
+ return (
+
+
+
+ );
+ }
+} );
+```
+
+The child component should expect to receive any props defined during the render.
diff --git a/client/components/data/checkout/index.jsx b/client/components/data/checkout/index.jsx
new file mode 100644
index 00000000000000..dd5ef8e089cd3a
--- /dev/null
+++ b/client/components/data/checkout/index.jsx
@@ -0,0 +1,32 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var StoreConnection = require( 'components/data/store-connection' ),
+ CartStore = require( 'lib/cart/store' ),
+ TransactionStore = require( 'lib/transaction/store' );
+
+var stores = [ TransactionStore, CartStore ];
+
+function getStateFromStores() {
+ return {
+ transaction: TransactionStore.get(),
+ cart: CartStore.get()
+ };
+}
+
+var CheckoutData = React.createClass( {
+ render: function() {
+ return (
+
+ { this.props.children }
+
+ );
+ }
+} );
+
+module.exports = CheckoutData;
diff --git a/client/components/data/current-theme/README.md b/client/components/data/current-theme/README.md
new file mode 100644
index 00000000000000..5faa41e07acd8c
--- /dev/null
+++ b/client/components/data/current-theme/README.md
@@ -0,0 +1,8 @@
+CurrentThemeData
+================
+
+A component to decouple the fetching of a site's current theme from any rendering.
+
+## Usage
+
+A child component wrapped with ` ` will be passed the prop `currentTheme`, a theme object.
diff --git a/client/components/data/current-theme/index.js b/client/components/data/current-theme/index.js
new file mode 100644
index 00000000000000..332e3b62a55342
--- /dev/null
+++ b/client/components/data/current-theme/index.js
@@ -0,0 +1,65 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var CurrentThemeStore = require( 'lib/themes/stores/current-theme' ),
+ Actions = require( 'lib/themes/actions' );
+
+/**
+ * Fetches the currently active theme of the supplied site
+ * and passes it to the supplied child component.
+ */
+var CurrentThemeData = React.createClass( {
+
+ propTypes: {
+ site: React.PropTypes.oneOfType( [
+ React.PropTypes.object,
+ React.PropTypes.bool
+ ] ).isRequired,
+ children: React.PropTypes.element.isRequired
+ },
+
+ getInitialState: function() {
+ return {
+ currentTheme: CurrentThemeStore.getCurrentTheme( this.props.site.ID )
+ };
+ },
+
+ componentWillMount: function() {
+ CurrentThemeStore.on( 'change', this.onCurrentThemeChange );
+
+ if ( ! this.state.currentTheme && this.props.site ) {
+ Actions.fetchCurrentTheme( this.props.site );
+ }
+ },
+
+ componentWillReceiveProps: function( nextProps ) {
+ if ( this.state.currentTheme ) {
+ return;
+ }
+
+ if ( nextProps.site && nextProps.site !== this.props.site ) {
+ Actions.fetchCurrentTheme( nextProps.site );
+ }
+ },
+
+ componentWillUnmount: function() {
+ CurrentThemeStore.off( 'change', this.onCurrentThemeChange );
+ },
+
+ onCurrentThemeChange: function() {
+ this.setState( {
+ currentTheme: CurrentThemeStore.getCurrentTheme( this.props.site.ID )
+ } );
+ },
+
+ render: function() {
+ return React.cloneElement( this.props.children, this.state );
+ }
+} );
+
+module.exports = CurrentThemeData;
diff --git a/client/components/data/domain-management/README.md b/client/components/data/domain-management/README.md
new file mode 100644
index 00000000000000..fba7aa3bf80abf
--- /dev/null
+++ b/client/components/data/domain-management/README.md
@@ -0,0 +1,67 @@
+DomainManagementData
+====================
+
+## Components list
+
+This folder contains the following components:
+
+* `DomainManagementData`
+* `DnsData`
+* `EmailData`
+* `EmailForwardingData`
+* `NameserversData`
+* `PrimaryDomainData`
+* `SiteRedirectData`
+* `TransferData`
+* `WhoisData`
+
+## Main component
+
+`DomainManagementData` - a component, located directly in this folder, that fetches a site's domains and cart content, then passes them to its children.
+
+## Usage
+
+Pass a component as a child of ` `. `DomainManagementData` will pass data to the given component, which is mounted as a child.
+It will handle the data itself thus helping us to decouple concerns: i.e. fetching and displaying data. This pattern is also called [container components](https://medium.com/@learnreact/container-components-c0e67432e005).
+
+```js
+import React from 'react';
+import DomainManagementData from 'components/data/domain-management';
+import MyChildComponent from 'components/my-child-component';
+
+// initialize rest of the variables
+
+const MyComponent = React.createClass( {
+ render() {
+ return (
+
+ { MyChildComponent }
+
+ );
+ }
+} );
+
+export default MyComponent;
+```
+
+The component expects to receive all listed props:
+
+* `context` - a request context
+* `productsList` - a collection of all the products users can have on WordPress.com
+* `sites` - a list of user sites
+
+The child component should receive processed props defined during the render:
+
+* `context` - a request context
+* `products` - a collection of all the products users can have on WordPress.com
+* `selectedSite` - the site currently selected
+
+As well as:
+
+* `cart` - products added to the cart, it's the result of a call to `CartStore.get`
+* `domains` - a list of domains, it's the result of a call to `DomainsStore.getForSite` for the current site
+
+It's updated whenever CartStore`, `DomainStore`, `productsList` or `sites` changes.
diff --git a/client/components/data/domain-management/dns/README.md b/client/components/data/domain-management/dns/README.md
new file mode 100644
index 00000000000000..5bc87c287a5e9d
--- /dev/null
+++ b/client/components/data/domain-management/dns/README.md
@@ -0,0 +1,48 @@
+DnsData
+=======
+
+A component that fetches a domain's DNS related data and passes it to its children.
+
+## Usage
+
+Pass a component through the `component` prop of ` `. `DnsData` will pass data to the given `component` prop, which is mounted as a child.
+It will handle the data itself thus helping us to decouple concerns: i.e. fetching and displaying data. This pattern is also called [container components](https://medium.com/@learnreact/container-components-c0e67432e005).
+
+```js
+import React from 'react';
+import DnsData from 'components/data/domain-management/dns';
+import MyChildComponent from 'components/my-child-component';
+
+// initialize rest of the variables
+
+const MyComponent = React.createClass( {
+ render() {
+ return (
+
+ );
+ }
+} );
+
+export default MyComponent;
+```
+
+The component expects to receive all listed props:
+
+* `component` - mentioned above
+* `selectedDomainName` - the domain name currently selected
+* `sites` - a list of user sites
+
+The child component should receive processed props defined during the render:
+
+* `selectedDomainName` - the domain name currently selected
+* `selectedSite` - the site currently selected
+
+As well as:
+
+* `domains` - a list of domains, it's the result of a call to `DomainsStore.getForSite` for the current site
+* `dns` - DNS records, it's the result of a call to `DnsStore.getByDomainName` for the current domain
+
+It's updated whenever `DomainsStore`, `DnsStore` or `sites` changes.
diff --git a/client/components/data/domain-management/dns/index.jsx b/client/components/data/domain-management/dns/index.jsx
new file mode 100644
index 00000000000000..815e64e7aa0203
--- /dev/null
+++ b/client/components/data/domain-management/dns/index.jsx
@@ -0,0 +1,74 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var StoreConnection = require( 'components/data/store-connection' ),
+ DnsStore = require( 'lib/domains/dns/store' ),
+ DomainsStore = require( 'lib/domains/store' ),
+ observe = require( 'lib/mixins/data-observe' ),
+ upgradesActions = require( 'lib/upgrades/actions' );
+
+const stores = [
+ DomainsStore,
+ DnsStore
+];
+
+function getStateFromStores( props ) {
+ let domains;
+
+ if ( props.selectedSite ) {
+ domains = DomainsStore.getForSite( props.selectedSite.ID );
+ }
+
+ return {
+ domains,
+ dns: DnsStore.getByDomainName( props.selectedDomainName ),
+ selectedDomainName: props.selectedDomainName,
+ selectedSite: props.selectedSite
+ };
+}
+
+module.exports = React.createClass( {
+ displayName: 'DnsData',
+
+ propTypes: {
+ component: React.PropTypes.func.isRequired,
+ selectedDomainName: React.PropTypes.string.isRequired,
+ sites: React.PropTypes.object.isRequired
+ },
+
+ mixins: [ observe( 'sites' ) ],
+
+ componentWillMount() {
+ this.loadDns();
+ },
+
+ componentWillUpdate() {
+ this.loadDns();
+ },
+
+ loadDns() {
+ const dns = DnsStore.getByDomainName( this.props.selectedDomainName );
+
+ if ( ! dns ) {
+ upgradesActions.fetchDns( this.props.selectedDomainName );
+ }
+ },
+
+ render() {
+ return (
+
+ { this.props.children }
+
+ );
+ }
+} );
diff --git a/client/components/data/domain-management/email-forwarding/README.md b/client/components/data/domain-management/email-forwarding/README.md
new file mode 100644
index 00000000000000..1179fbf52eeaaf
--- /dev/null
+++ b/client/components/data/domain-management/email-forwarding/README.md
@@ -0,0 +1,47 @@
+EmailForwardingData
+===================
+
+A component that fetches a domain's email forwarding related data and passes it to its children.
+
+## Usage
+
+Pass a component through the `component` prop of ` `. `EmailForwardingData` will pass data to the given `component` prop, which is mounted as a child.
+It will handle the data itself thus helping us to decouple concerns: i.e. fetching and displaying data. This pattern is also called [container components](https://medium.com/@learnreact/container-components-c0e67432e005).
+
+```js
+import React from 'react';
+import EmailForwardingData from 'components/data/domain-management/email-forwarding';
+import MyChildComponent from 'components/my-child-component';
+
+// initialize rest of the variables
+
+const MyComponent = React.createClass( {
+ render() {
+ return (
+
+ );
+ }
+} );
+
+export default MyComponent;
+```
+
+The component expects to receive all listed props:
+
+* `component` - mentioned above
+* `selectedDomainName` - the domain name currently selected
+* `sites` - a list of user sites
+
+The child component should receive processed props defined during the render:
+
+* `selectedDomainName` - the domain name currently selected
+* `selectedSite` - the site currently selected
+
+As well as:
+
+* `emailForwarding` - email forwarding data, it's the result of a call to `EmailForwardingStore.getByDomainName` for the current domain
+
+It's updated whenever `EmailForwardingStore` or `sites` changes.
diff --git a/client/components/data/domain-management/email-forwarding/index.jsx b/client/components/data/domain-management/email-forwarding/index.jsx
new file mode 100644
index 00000000000000..db6eb2a537f35c
--- /dev/null
+++ b/client/components/data/domain-management/email-forwarding/index.jsx
@@ -0,0 +1,57 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+
+/**
+ * Internal dependencies
+ */
+import EmailForwardingStore from 'lib/domains/email-forwarding/store';
+import StoreConnection from 'components/data/store-connection';
+import observe from 'lib/mixins/data-observe';
+import * as upgradesActions from 'lib/upgrades/actions';
+
+function getStateFromStores( props ) {
+ return {
+ emailForwarding: EmailForwardingStore.getByDomainName( props.selectedDomainName ),
+ selectedDomainName: props.selectedDomainName,
+ selectedSite: props.selectedSite
+ };
+}
+
+const EmailForwardingData = React.createClass( {
+ propTypes: {
+ component: React.PropTypes.func.isRequired,
+ selectedDomainName: React.PropTypes.string.isRequired,
+ sites: React.PropTypes.object.isRequired
+ },
+
+ mixins: [ observe( 'sites' ) ],
+
+ componentWillMount() {
+ this.loadEmailForwarding();
+ },
+
+ componentWillUpdate() {
+ this.loadEmailForwarding();
+ },
+
+ loadEmailForwarding() {
+ upgradesActions.fetchEmailForwarding( this.props.selectedDomainName );
+ },
+
+ render() {
+ return (
+
+ { this.props.children }
+
+ );
+ }
+} );
+
+export default EmailForwardingData;
diff --git a/client/components/data/domain-management/email/README.md b/client/components/data/domain-management/email/README.md
new file mode 100644
index 00000000000000..6570d2ba106299
--- /dev/null
+++ b/client/components/data/domain-management/email/README.md
@@ -0,0 +1,58 @@
+EmailData
+============
+
+A component that fetches a domain's email related data and passes it to its children.
+
+## Usage
+
+Pass a component through the `component` prop of ` `. `EmailData` will pass data to the given `component` prop, which is mounted as a child.
+It will handle the data itself thus helping us to decouple concerns: i.e. fetching and displaying data. This pattern is also called [container components](https://medium.com/@learnreact/container-components-c0e67432e005).
+
+```js
+import React from 'react';
+import EmailData from 'components/data/domain-management/email';
+import MyChildComponent from 'components/my-child-component';
+
+// initialize rest of the variables
+
+const MyComponent = React.createClass( {
+ render() {
+ return (
+
+ );
+ }
+} );
+
+export default MyComponent;
+```
+
+The component expects to receive all listed props:
+
+* `component` - mentioned above
+* `context` - a request context
+* `productsList` - a collection of all the products users can have on WordPress.com
+* `selectedDomainName` - the domain name currently selected
+* `sites` - a list of user sites
+* `user` - a current user object
+
+The child component should receive processed props defined during the render:
+
+* `context` - a request context
+* `products` - a collection of all the products users can have on WordPress.com
+* `selectedDomainName` - the domain name currently selected
+* `selectedSite` - the site currently selected
+* `user` - a current user object
+
+As well as:
+
+* `cart` - products added to the cart, it's the result of a call to `CartStore.get`
+* `domains` - a list of domains, it's the result of a call to `DomainsStore.getForSite` for the current site
+* `googleAppsUsers` - Google Apps users, it's the result of a call to `GoogleAppsUsersStore.getByDomainName` for the current domain
+
+It's updated whenever `CartStore`, `DomainsStore`, `GoogleAppsUsersStore`, `productsList` or `sites` changes.
diff --git a/client/components/data/domain-management/email/index.jsx b/client/components/data/domain-management/email/index.jsx
new file mode 100644
index 00000000000000..64c19c17353e39
--- /dev/null
+++ b/client/components/data/domain-management/email/index.jsx
@@ -0,0 +1,88 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var StoreConnection = require( 'components/data/store-connection' ),
+ DomainsStore = require( 'lib/domains/store' ),
+ GoogleAppsUsersStore = require( 'lib/domains/google-apps-users/store' ),
+ CartStore = require( 'lib/cart/store' ),
+ observe = require( 'lib/mixins/data-observe' ),
+ upgradesActions = require( 'lib/upgrades/actions' );
+
+var stores = [
+ DomainsStore,
+ GoogleAppsUsersStore,
+ CartStore
+];
+
+function getStateFromStores( props ) {
+ let domains;
+
+ if ( props.selectedSite ) {
+ domains = DomainsStore.getForSite( props.selectedSite.ID );
+ }
+
+ return {
+ domains,
+ cart: CartStore.get(),
+ context: props.context,
+ products: props.products,
+ googleAppsUsers: GoogleAppsUsersStore.getByDomainName( props.selectedDomainName ),
+ selectedDomainName: props.selectedDomainName,
+ selectedSite: props.selectedSite,
+ user: props.user
+ };
+}
+
+module.exports = React.createClass( {
+ displayName: 'EmailData',
+
+ propTypes: {
+ component: React.PropTypes.func.isRequired,
+ context: React.PropTypes.object.isRequired,
+ productsList: React.PropTypes.object.isRequired,
+ selectedDomainName: React.PropTypes.string,
+ sites: React.PropTypes.object.isRequired,
+ user: React.PropTypes.object.isRequired
+ },
+
+ mixins: [ observe( 'productsList', 'sites' ) ],
+
+ componentWillMount() {
+ this.loadDomains();
+ },
+
+ componentWillUpdate() {
+ this.loadDomains();
+ },
+
+ loadDomains() {
+ const selectedSite = this.props.sites.getSelectedSite();
+
+ if ( this.prevSelectedSite !== selectedSite ) {
+ upgradesActions.fetchDomains( selectedSite.ID );
+
+ this.prevSelectedSite = selectedSite;
+ }
+ },
+
+ render() {
+ return (
+
+ { this.props.children }
+
+ );
+ }
+} );
diff --git a/client/components/data/domain-management/index.jsx b/client/components/data/domain-management/index.jsx
new file mode 100644
index 00000000000000..6a473756630cc3
--- /dev/null
+++ b/client/components/data/domain-management/index.jsx
@@ -0,0 +1,69 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var StoreConnection = require( 'components/data/store-connection' ),
+ DomainsStore = require( 'lib/domains/store' ),
+ CartStore = require( 'lib/cart/store' ),
+ observe = require( 'lib/mixins/data-observe' ),
+ upgradesActions = require( 'lib/upgrades/actions' );
+
+var stores = [
+ DomainsStore,
+ CartStore
+];
+
+function getStateFromStores( props ) {
+ return {
+ cart: CartStore.get(),
+ context: props.context,
+ domains: ( props.selectedSite ? DomainsStore.getForSite( props.selectedSite.ID ) : null ),
+ products: props.products,
+ selectedSite: props.selectedSite
+ };
+}
+
+module.exports = React.createClass( {
+ displayName: 'DomainManagementData',
+
+ propTypes: {
+ context: React.PropTypes.object.isRequired,
+ productsList: React.PropTypes.object.isRequired,
+ sites: React.PropTypes.object.isRequired
+ },
+
+ mixins: [ observe( 'productsList', 'sites' ) ],
+
+ componentWillMount: function() {
+ if ( this.props.sites.getSelectedSite() ) {
+ upgradesActions.fetchDomains( this.props.sites.getSelectedSite().ID );
+ }
+
+ this.prevSelectedSite = this.props.sites.getSelectedSite();
+ },
+
+ componentWillUpdate: function() {
+ if ( this.prevSelectedSite !== this.props.sites.getSelectedSite() ) {
+ upgradesActions.fetchDomains( this.props.sites.getSelectedSite().ID );
+ }
+
+ this.prevSelectedSite = this.props.sites.getSelectedSite();
+ },
+
+ render: function() {
+ return (
+
+ { this.props.children }
+
+ );
+ }
+} );
diff --git a/client/components/data/domain-management/nameservers/README.md b/client/components/data/domain-management/nameservers/README.md
new file mode 100644
index 00000000000000..34d8ccdee59b18
--- /dev/null
+++ b/client/components/data/domain-management/nameservers/README.md
@@ -0,0 +1,48 @@
+NameserversData
+===============
+
+A component that fetches a domain's nameservers related data and passes it to its children.
+
+## Usage
+
+Pass a component through the `component` prop of ` `. `NameserversData` will pass data to the given `component` prop, which is mounted as a child.
+It will handle the data itself thus helping us to decouple concerns: i.e. fetching and displaying data. This pattern is also called [container components](https://medium.com/@learnreact/container-components-c0e67432e005).
+
+```js
+import React from 'react';
+import NameserversData from 'components/data/domain-management/nameservers';
+import MyChildComponent from 'components/my-child-component';
+
+// initialize rest of the variables
+
+const MyComponent = React.createClass( {
+ render() {
+ return (
+
+ );
+ }
+} );
+
+export default MyComponent;
+```
+
+The component expects to receive all listed props:
+
+* `component` - mentioned above
+* `selectedDomainName` - the domain name currently selected
+* `sites` - a list of user sites
+
+The child component should receive processed props defined during the render:
+
+* `selectedDomainName` - the domain name currently selected
+* `selectedSite` - the site currently selected
+
+As well as:
+
+* `domains` - a list of domains, it's the result of a call to `DomainsStore.getForSite` for the current site
+* `nameservers` - nameservers data, it's the result of a call to `NameserversStore.getByDomainName` for the current domain
+
+It's updated whenever `DomainsStore`, `NameserversStore` or `sites` changes.
diff --git a/client/components/data/domain-management/nameservers/index.jsx b/client/components/data/domain-management/nameservers/index.jsx
new file mode 100644
index 00000000000000..0b49656d3a0ba9
--- /dev/null
+++ b/client/components/data/domain-management/nameservers/index.jsx
@@ -0,0 +1,82 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+
+/**
+ * Internal dependencies
+ */
+import DomainsStore from 'lib/domains/store';
+import NameserversStore from 'lib/domains/nameservers/store';
+import observe from 'lib/mixins/data-observe';
+import StoreConnection from 'components/data/store-connection';
+import * as upgradesActions from 'lib/upgrades/actions';
+
+const stores = [
+ DomainsStore,
+ NameserversStore
+];
+
+function getStateFromStores( props ) {
+ let domains;
+
+ if ( props.selectedSite ) {
+ domains = DomainsStore.getForSite( props.selectedSite.ID );
+ }
+
+ return {
+ domains,
+ nameservers: NameserversStore.getByDomainName( props.selectedDomainName ),
+ selectedDomainName: props.selectedDomainName,
+ selectedSite: props.selectedSite
+ };
+}
+
+const NameserversData = React.createClass( {
+ propTypes: {
+ component: React.PropTypes.func.isRequired,
+ selectedDomainName: React.PropTypes.string.isRequired,
+ sites: React.PropTypes.object.isRequired
+ },
+
+ mixins: [ observe( 'sites' ) ],
+
+ componentWillMount() {
+ this.loadDomains();
+ this.loadNameservers();
+ },
+
+ componentWillUpdate() {
+ this.loadDomains();
+ this.loadNameservers();
+ },
+
+ loadDomains() {
+ const selectedSite = this.props.sites.getSelectedSite();
+
+ if ( this.prevSelectedSite !== selectedSite ) {
+ upgradesActions.fetchDomains( selectedSite.ID );
+
+ this.prevSelectedSite = selectedSite;
+ }
+ },
+
+ loadNameservers() {
+ upgradesActions.fetchNameservers( this.props.selectedDomainName );
+ },
+
+ render() {
+ return (
+
+ { this.props.children }
+
+ );
+ }
+} );
+
+export default NameserversData;
diff --git a/client/components/data/domain-management/primary-domain/README.md b/client/components/data/domain-management/primary-domain/README.md
new file mode 100644
index 00000000000000..21c9ee822ca9a7
--- /dev/null
+++ b/client/components/data/domain-management/primary-domain/README.md
@@ -0,0 +1,47 @@
+PrimaryDomainData
+=================
+
+A component that fetches a domain's primary domain related data and passes it to its children.
+
+## Usage
+
+Pass a component through the `component` prop of ` `. `PrimaryDomainData` will pass data to the given `component` prop, which is mounted as a child.
+It will handle the data itself thus helping us to decouple concerns: i.e. fetching and displaying data. This pattern is also called [container components](https://medium.com/@learnreact/container-components-c0e67432e005).
+
+```js
+import React from 'react';
+import PrimaryDomainData from 'components/data/domain-management/primary-domain';
+import MyChildComponent from 'components/my-child-component';
+
+// initialize rest of the variables
+
+const MyComponent = React.createClass( {
+ render() {
+ return (
+
+ );
+ }
+} );
+
+export default MyComponent;
+```
+
+The component expects to receive all listed props:
+
+* `component` - mentioned above
+* `selectedDomainName` - the domain name currently selected
+* `sites` - a list of user sites
+
+The child component should receive processed props defined during the render:
+
+* `selectedDomainName` - the domain name currently selected
+* `selectedSite` - the site currently selected
+
+As well as:
+
+* `domains` - a list of domains, it's the result of a call to `DomainsStore.getForSite` for the current site
+
+It's updated whenever `DomainsStore` or `sites` changes.
diff --git a/client/components/data/domain-management/primary-domain/index.jsx b/client/components/data/domain-management/primary-domain/index.jsx
new file mode 100644
index 00000000000000..9ddcb418ebf36e
--- /dev/null
+++ b/client/components/data/domain-management/primary-domain/index.jsx
@@ -0,0 +1,54 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+
+/**
+ * Internal dependencies
+ */
+import StoreConnection from 'components/data/store-connection';
+import DomainsStore from 'lib/domains/store';
+import observe from 'lib/mixins/data-observe';
+
+const stores = [
+ DomainsStore
+];
+
+function getStateFromStores( props ) {
+ let domains;
+
+ if ( props.selectedSite ) {
+ domains = DomainsStore.getForSite( props.selectedSite.ID );
+ }
+
+ return {
+ domains,
+ selectedDomainName: props.selectedDomainName,
+ selectedSite: props.selectedSite
+ };
+}
+
+export default React.createClass( {
+ displayName: 'PrimaryDomainData',
+
+ propTypes: {
+ component: React.PropTypes.func.isRequired,
+ selectedDomainName: React.PropTypes.string.isRequired,
+ sites: React.PropTypes.object.isRequired
+ },
+
+ mixins: [ observe( 'sites' ) ],
+
+ render() {
+ return (
+
+ { this.props.children }
+
+ );
+ }
+} );
diff --git a/client/components/data/domain-management/site-redirect/README.md b/client/components/data/domain-management/site-redirect/README.md
new file mode 100644
index 00000000000000..a6849b77b39655
--- /dev/null
+++ b/client/components/data/domain-management/site-redirect/README.md
@@ -0,0 +1,47 @@
+SiteRedirectData
+================
+
+A component that fetches a site redirect's related data and passes it to its children.
+
+## Usage
+
+Pass a component through the `component` prop of ` `. `SiteRedirectData` will pass data to the given `component` prop, which is mounted as a child.
+It will handle the data itself thus helping us to decouple concerns: i.e. fetching and displaying data. This pattern is also called [container components](https://medium.com/@learnreact/container-components-c0e67432e005).
+
+```js
+import React from 'react';
+import SiteRedirectData from 'components/data/domain-management/site-redirect';
+import MyChildComponent from 'components/my-child-component';
+
+// initialize rest of the variables
+
+const MyComponent = React.createClass( {
+ render() {
+ return (
+
+ );
+ }
+} );
+
+export default MyComponent;
+```
+
+The component expects to receive all listed props:
+
+* `component` - mentioned above
+* `selectedDomainName` - the domain name currently selected
+* `sites` - a list of user sites
+
+The child component should receive processed props defined during the render:
+
+* `selectedDomainName` - the domain name currently selected
+* `selectedSite` - the site currently selected
+
+As well as:
+
+* `location` - a site redirect's location, it's the result of a call to `SiteRedirectStore.getBySite` for the current site
+
+It's updated whenever `SiteRedirectStore` or `sites` changes.
diff --git a/client/components/data/domain-management/site-redirect/index.jsx b/client/components/data/domain-management/site-redirect/index.jsx
new file mode 100644
index 00000000000000..dc109c9667fbb8
--- /dev/null
+++ b/client/components/data/domain-management/site-redirect/index.jsx
@@ -0,0 +1,48 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var StoreConnection = require( 'components/data/store-connection' ),
+ SiteRedirectStore = require( 'lib/domains/site-redirect/store' ),
+ observe = require( 'lib/mixins/data-observe' );
+
+var stores = [
+ SiteRedirectStore
+];
+
+function getStateFromStores( props ) {
+ return {
+ location: SiteRedirectStore.getBySite( props.selectedSite.domain ),
+ selectedDomainName: props.selectedDomainName,
+ selectedSite: props.selectedSite
+ };
+}
+
+module.exports = React.createClass( {
+ displayName: 'SiteRedirectData',
+
+ propTypes: {
+ component: React.PropTypes.func.isRequired,
+ selectedDomainName: React.PropTypes.string.isRequired,
+ sites: React.PropTypes.object.isRequired
+ },
+
+ mixins: [ observe( 'sites' ) ],
+
+ render() {
+ return (
+
+ { this.props.children }
+
+ );
+ }
+} );
diff --git a/client/components/data/domain-management/transfer/README.md b/client/components/data/domain-management/transfer/README.md
new file mode 100644
index 00000000000000..39194a86b999c4
--- /dev/null
+++ b/client/components/data/domain-management/transfer/README.md
@@ -0,0 +1,48 @@
+TransferData
+============
+
+A component that fetches a domain's transfer domain related data (Wapi info) and passes it to its children.
+
+## Usage
+
+Pass a component through the `component` prop of ` `. `TransferData` will pass data to the given `component` prop, which is mounted as a child.
+It will handle the data itself thus helping us to decouple concerns: i.e. fetching and displaying data. This pattern is also called [container components](https://medium.com/@learnreact/container-components-c0e67432e005).
+
+```js
+import React from 'react';
+import TransferData from 'components/data/domain-management/transfer';
+import MyChildComponent from 'components/my-child-component';
+
+// initialize rest of the variables
+
+const MyComponent = React.createClass( {
+ render() {
+ return (
+
+ );
+ }
+} );
+
+export default MyComponent;
+```
+
+The component expects to receive all listed props:
+
+* `component` - mentioned above
+* `selectedDomainName` - the domain name currently selected
+* `sites` - a list of user sites
+
+The child component should receive processed props defined during the render:
+
+* `selectedDomainName` - the domain name currently selected
+* `selectedSite` - the site currently selected
+
+As well as:
+
+* `domains` - a list of domains, it's the result of a call to `DomainsStore.getForSite` for the current site
+* `wapiDomainInfo` - Wapi domain info, it's the result of a call to `WapiDomainInfoStore.getForSite` for the current site
+
+It's updated whenever `DomainsStore`, `WapiDomainInfoStore` or `sites` changes.
diff --git a/client/components/data/domain-management/transfer/index.jsx b/client/components/data/domain-management/transfer/index.jsx
new file mode 100644
index 00000000000000..5a858d15b65e93
--- /dev/null
+++ b/client/components/data/domain-management/transfer/index.jsx
@@ -0,0 +1,80 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+
+/**
+ * Internal dependencies
+ */
+import DomainsStore from 'lib/domains/store';
+import observe from 'lib/mixins/data-observe';
+import StoreConnection from 'components/data/store-connection';
+import WapiDomainInfoStore from 'lib/domains/wapi-domain-info/store';
+import { fetchDomains, fetchWapiDomainInfo } from 'lib/upgrades/actions';
+
+const stores = [
+ DomainsStore,
+ WapiDomainInfoStore
+];
+
+function getStateFromStores( props ) {
+ let domains;
+
+ if ( props.selectedSite ) {
+ domains = DomainsStore.getForSite( props.selectedSite.ID );
+ }
+
+ return {
+ domains,
+ selectedDomainName: props.selectedDomainName,
+ selectedSite: props.selectedSite,
+ wapiDomainInfo: WapiDomainInfoStore.getByDomainName( props.selectedDomainName )
+ };
+}
+
+const TransferData = React.createClass( {
+ propTypes: {
+ component: React.PropTypes.func.isRequired,
+ selectedDomainName: React.PropTypes.string.isRequired,
+ sites: React.PropTypes.object.isRequired
+ },
+
+ mixins: [ observe( 'sites' ) ],
+
+ componentWillMount() {
+ this.loadDomains();
+ this.loadWapiDomainInfo();
+ },
+
+ componentWillUpdate() {
+ this.loadDomains();
+ this.loadWapiDomainInfo();
+ },
+
+ loadDomains() {
+ const selectedSite = this.props.sites.getSelectedSite();
+
+ if ( this.prevSelectedSite !== selectedSite ) {
+ fetchDomains( selectedSite.ID );
+
+ this.prevSelectedSite = selectedSite;
+ }
+ },
+
+ loadWapiDomainInfo() {
+ fetchWapiDomainInfo( this.props.selectedDomainName );
+ },
+
+ render() {
+ return (
+
+ );
+ }
+} );
+
+export default TransferData;
diff --git a/client/components/data/domain-management/whois/README.md b/client/components/data/domain-management/whois/README.md
new file mode 100644
index 00000000000000..36ec946a4e426e
--- /dev/null
+++ b/client/components/data/domain-management/whois/README.md
@@ -0,0 +1,54 @@
+WhoisData
+=========
+
+A component that fetches a domain's WHOIS related data and passes it to its children.
+
+## Usage
+
+Pass a component through the `component` prop of ` `. `WhoisData` will pass data to the given `component` prop, which is mounted as a child.
+It will handle the data itself thus helping us to decouple concerns: i.e. fetching and displaying data. This pattern is also called [container components](https://medium.com/@learnreact/container-components-c0e67432e005).
+
+```js
+import React from 'react';
+import WhoisData from 'components/data/domain-management/whois';
+import MyChildComponent from 'components/my-child-component';
+
+// initialize rest of the variables
+
+const MyComponent = React.createClass( {
+ render() {
+ return (
+
+ );
+ }
+} );
+
+export default MyComponent;
+```
+
+The component expects to receive all listed props:
+
+* `component` - mentioned above
+* `context` - a request context
+* `productsList` - a collection of all the products users can have on WordPress.com
+* `selectedDomainName` - the domain name currently selected
+* `sites` - a list of user sites
+
+The child component should receive processed props defined during the render:
+
+* `context` - a request context
+* `products` - a collection of all the products users can have on WordPress.com
+* `selectedDomainName` - the domain name currently selected
+* `selectedSite` - the site currently selected
+
+As well as:
+
+* `domains` - a list of domains, it's the result of a call to `DomainsStore.getForSite` for the current site
+* `whois` - WHOIS contact information, it's the result of a call to `WhoisStore.getByDomainName` for the current domain
+
+It's updated whenever `DomainsStore`, `WhoisStore`, `productsList` or `sites` changes.
diff --git a/client/components/data/domain-management/whois/index.jsx b/client/components/data/domain-management/whois/index.jsx
new file mode 100644
index 00000000000000..150eeee7dd87b0
--- /dev/null
+++ b/client/components/data/domain-management/whois/index.jsx
@@ -0,0 +1,88 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var StoreConnection = require( 'components/data/store-connection' ),
+ DomainsStore = require( 'lib/domains/store' ),
+ WhoisStore = require( 'lib/domains/whois/store' ),
+ observe = require( 'lib/mixins/data-observe' ),
+ upgradesActions = require( 'lib/upgrades/actions' );
+
+var stores = [
+ DomainsStore,
+ WhoisStore
+];
+
+function getStateFromStores( props ) {
+ let domains;
+
+ if ( props.selectedSite ) {
+ domains = DomainsStore.getForSite( props.selectedSite.ID );
+ }
+
+ return {
+ domains,
+ whois: WhoisStore.getByDomainName( props.selectedDomainName ),
+ products: props.products,
+ selectedDomainName: props.selectedDomainName,
+ selectedSite: props.selectedSite,
+ context: props.context
+ };
+}
+
+module.exports = React.createClass( {
+ displayName: 'WhoisData',
+
+ propTypes: {
+ component: React.PropTypes.func.isRequired,
+ context: React.PropTypes.object.isRequired,
+ productsList: React.PropTypes.object.isRequired,
+ selectedDomainName: React.PropTypes.string.isRequired,
+ sites: React.PropTypes.object.isRequired
+ },
+
+ mixins: [ observe( 'productsList', 'sites' ) ],
+
+ componentWillMount() {
+ this.loadDomains();
+ this.loadWhois();
+ },
+
+ componentWillUpdate() {
+ this.loadDomains();
+ this.loadWhois();
+ },
+
+ loadDomains() {
+ const selectedSite = this.props.sites.getSelectedSite();
+
+ if ( this.prevSelectedSite !== selectedSite ) {
+ upgradesActions.fetchDomains( selectedSite.ID );
+
+ this.prevSelectedSite = selectedSite;
+ }
+ },
+
+ loadWhois() {
+ upgradesActions.fetchWhois( this.props.selectedDomainName );
+ },
+
+ render() {
+ return (
+
+ { this.props.children }
+
+ );
+ }
+} );
diff --git a/client/components/data/email-followers-data/README.md b/client/components/data/email-followers-data/README.md
new file mode 100644
index 00000000000000..e5b3154bd98254
--- /dev/null
+++ b/client/components/data/email-followers-data/README.md
@@ -0,0 +1,18 @@
+EmailFollowersData
+==================
+
+A component that fetches a site's email followers and passes them to its children.
+
+## Props
+
+` ` should be given a `fetchOptions` object which will be used as parameters for the API call /sites/$site/stats/followers
+
+## Usage
+
+A component wrapped with ` ` will receive the following props:
+
+- followers: An array of follower objects
+- totalFollowers: The total number of followers found for the site
+- currentPage: The last page that was fetched from the API
+- fetching: A boolean that is true if the fetch is in progress
+- fetchInitialized: A boolean that states if the fetch has been initialized yet
\ No newline at end of file
diff --git a/client/components/data/email-followers-data/index.jsx b/client/components/data/email-followers-data/index.jsx
new file mode 100644
index 00000000000000..5e416f0e2da07d
--- /dev/null
+++ b/client/components/data/email-followers-data/index.jsx
@@ -0,0 +1,110 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+import isEqual from 'lodash/lang/isEqual';
+import debugModule from 'debug';
+
+/**
+ * Internal dependencies
+ */
+import EmailFollowersStore from 'lib/email-followers/store';
+import EmailFollowersActions from 'lib/email-followers/actions';
+import passToChildren from 'lib/react-pass-to-children';
+
+/**
+ * Module variables
+ */
+const debug = debugModule( 'calypso:email-followers-data' );
+
+export default React.createClass( {
+ displayName: 'EmailFollowersData',
+
+ propTypes: {
+ fetchOptions: React.PropTypes.object.isRequired
+ },
+
+ getInitialState() {
+ return {
+ followers: false,
+ totalFollowers: false,
+ currentPage: false,
+ fetchInitialized: false
+ };
+ },
+
+ componentDidMount() {
+ EmailFollowersStore.on( 'change', this.refreshFollowers );
+ this.fetchIfEmpty( this.props.fetchOptions );
+ },
+
+ componentWillReceiveProps( nextProps ) {
+ if ( ! nextProps.fetchOptions ) {
+ return;
+ }
+ if ( ! isEqual( this.props.fetchOptions, nextProps.fetchOptions ) ) {
+ this.setState( this.getInitialState() );
+ this.fetchIfEmpty( nextProps.fetchOptions );
+ }
+ },
+
+ componentWillUnmount() {
+ EmailFollowersStore.removeListener( 'change', this.refreshFollowers );
+ },
+
+ fetchIfEmpty( fetchOptions ) {
+ fetchOptions = fetchOptions || this.props.fetchOptions;
+ if ( ! fetchOptions || ! fetchOptions.siteId ) {
+ return;
+ }
+ if ( EmailFollowersStore.getFollowers( fetchOptions ).length ) {
+ this.refreshFollowers( fetchOptions );
+ return;
+ }
+
+ // defer fetch requests to avoid dispatcher conflicts
+ let defer = function() {
+ var paginationData = EmailFollowersStore.getPaginationData( fetchOptions );
+ if ( paginationData.fetchingFollowers ) {
+ return;
+ }
+ EmailFollowersActions.fetchFollowers( fetchOptions );
+ this.setState( { fetchInitialized: true } );
+ }.bind( this );
+ setTimeout( defer, 0 );
+ },
+
+ isFetching: function() {
+ let fetchOptions = this.props.fetchOptions;
+ if ( ! fetchOptions.siteId ) {
+ debug( 'Is fetching because siteId is falsey' );
+ return true;
+ }
+ if ( ! this.state.followers ) {
+ debug( 'Is fetching because not followers' );
+ return true;
+ }
+
+ let followersPaginationData = EmailFollowersStore.getPaginationData( fetchOptions );
+ debug( 'Followers pagination data: ' + JSON.stringify( followersPaginationData ) );
+
+ if ( followersPaginationData.fetchingFollowers ) {
+ return true;
+ }
+ return false;
+ },
+
+ refreshFollowers( fetchOptions ) {
+ fetchOptions = fetchOptions || this.props.fetchOptions;
+ debug( 'Refreshing followers: ' + JSON.stringify( fetchOptions ) );
+ this.setState( {
+ followers: EmailFollowersStore.getFollowers( fetchOptions ),
+ totalFollowers: EmailFollowersStore.getPaginationData( fetchOptions ).totalFollowers,
+ currentPage: EmailFollowersStore.getPaginationData( fetchOptions ).followersCurrentPage
+ } );
+ },
+
+ render() {
+ return passToChildren( this, Object.assign( {}, this.state, { fetching: this.isFetching() } ) );
+ }
+} );
diff --git a/client/components/data/followers-data/README.md b/client/components/data/followers-data/README.md
new file mode 100644
index 00000000000000..8e031139a8a762
--- /dev/null
+++ b/client/components/data/followers-data/README.md
@@ -0,0 +1,18 @@
+FollowersData
+=============
+
+A component that fetches a site's wpcom followers and passes them to its children.
+
+## Props
+
+` ` should be given a `fetchOptions` object which will be used as parameters for the API call /sites/$site/stats/followers
+
+## Usage
+
+A component wrapped with ` ` will receive the following props:
+
+- followers: An array of follower objects
+- totalFollowers: The total number of followers found for the site
+- currentPage: The last page that was fetched from the API
+- fetching: A boolean that is true if the fetch is in progress
+- fetchInitialized: A boolean that states if the fetch has been initialized yet
\ No newline at end of file
diff --git a/client/components/data/followers-data/index.jsx b/client/components/data/followers-data/index.jsx
new file mode 100644
index 00000000000000..2aa719ba8034fc
--- /dev/null
+++ b/client/components/data/followers-data/index.jsx
@@ -0,0 +1,109 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+import isEqual from 'lodash/lang/isEqual';
+import debugModule from 'debug';
+
+/**
+ * Internal dependencies
+ */
+import FollowersStore from 'lib/followers/store';
+import FollowersActions from 'lib/followers/actions';
+import passToChildren from 'lib/react-pass-to-children';
+
+/**
+ * Module variables
+ */
+const debug = debugModule( 'calypso:followers-data' );
+
+export default React.createClass( {
+ displayName: 'FollowersData',
+
+ propTypes: {
+ fetchOptions: React.PropTypes.object.isRequired
+ },
+
+ getInitialState() {
+ return {
+ followers: false,
+ totalFollowers: false,
+ currentPage: false,
+ fetchInitialized: false
+ };
+ },
+
+ componentDidMount() {
+ FollowersStore.on( 'change', this.refreshFollowers );
+ this.fetchIfEmpty( this.props.fetchOptions );
+ },
+
+ componentWillReceiveProps( nextProps ) {
+ if ( ! nextProps.fetchOptions ) {
+ return;
+ }
+ if ( ! isEqual( this.props.fetchOptions, nextProps.fetchOptions ) ) {
+ this.setState( this.getInitialState() );
+ this.fetchIfEmpty( nextProps.fetchOptions );
+ }
+ },
+
+ componentWillUnmount() {
+ FollowersStore.removeListener( 'change', this.refreshFollowers );
+ },
+
+ fetchIfEmpty( fetchOptions ) {
+ fetchOptions = fetchOptions || this.props.fetchOptions;
+ if ( ! fetchOptions || ! fetchOptions.siteId ) {
+ return;
+ }
+ if ( FollowersStore.getFollowers( fetchOptions ).length ) {
+ this.refreshFollowers( fetchOptions );
+ return;
+ }
+ // defer fetch requests to avoid dispatcher conflicts
+ let defer = function() {
+ var paginationData = FollowersStore.getPaginationData( fetchOptions );
+ if ( paginationData.fetchingFollowers ) {
+ return;
+ }
+ FollowersActions.fetchFollowers( fetchOptions );
+ this.setState( { fetchInitialized: true } );
+ }.bind( this );
+ setTimeout( defer, 0 );
+ },
+
+ isFetching: function() {
+ let fetchOptions = this.props.fetchOptions;
+ if ( ! fetchOptions.siteId ) {
+ debug( 'Is fetching because siteId is falsey' );
+ return true;
+ }
+ if ( ! this.state.followers ) {
+ debug( 'Is fetching because not followers' );
+ return true;
+ }
+
+ let followersPaginationData = FollowersStore.getPaginationData( fetchOptions );
+ debug( 'Followers pagination data: ' + JSON.stringify( followersPaginationData ) );
+
+ if ( followersPaginationData.fetchingFollowers ) {
+ return true;
+ }
+ return false;
+ },
+
+ refreshFollowers( fetchOptions ) {
+ fetchOptions = fetchOptions || this.props.fetchOptions;
+ debug( 'Refreshing followers: ' + JSON.stringify( fetchOptions ) );
+ this.setState( {
+ followers: FollowersStore.getFollowers( fetchOptions ),
+ totalFollowers: FollowersStore.getPaginationData( fetchOptions ).totalFollowers,
+ currentPage: FollowersStore.getPaginationData( fetchOptions ).followersCurrentPage
+ } );
+ },
+
+ render() {
+ return passToChildren( this, Object.assign( {}, this.state, { fetching: this.isFetching() } ) );
+ }
+} );
diff --git a/client/components/data/media-library-selected-data/README.md b/client/components/data/media-library-selected-data/README.md
new file mode 100644
index 00000000000000..4af5b398630272
--- /dev/null
+++ b/client/components/data/media-library-selected-data/README.md
@@ -0,0 +1,30 @@
+MediaLibrarySelectedData
+========================
+
+MediaLibrarySelectedData is a React component intended to be used as a controller-view to simplify binding and interacting with the [media library selected store](../../../lib/media/library-selected-store.js).
+
+## Usage
+
+Wrap a child component with ` `, passing a `siteId`. [As a controller-view](https://facebook.github.io/flux/docs/overview.html#views-and-controller-views), MediaLibrarySelectedData does not render any content of its own; instead, it simply renders the child component.
+
+```jsx
+var React = require( 'react' ),
+ MediaLibrarySelectedData = require( 'components/data/media-library-selected-data' ),
+ MyChildComponent = require( './my-child-component' );
+
+module.exports = React.createClass( {
+ displayName: 'MyComponent',
+
+ render: function() {
+ return (
+
+
+
+ );
+ }
+} );
+```
+
+The child component should expect to receive any props defined during the render, as well as the following additional props:
+
+- `mediaLibrarySelectedItems`: An array of media items which are selected in the media library.
diff --git a/client/components/data/media-library-selected-data/index.jsx b/client/components/data/media-library-selected-data/index.jsx
new file mode 100644
index 00000000000000..0a65f7ed7d8149
--- /dev/null
+++ b/client/components/data/media-library-selected-data/index.jsx
@@ -0,0 +1,50 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var MediaLibrarySelectedStore = require( 'lib/media/library-selected-store' ),
+ passToChildren = require( 'lib/react-pass-to-children' );
+
+function getStateData( siteId ) {
+ return {
+ mediaLibrarySelectedItems: MediaLibrarySelectedStore.getAll( siteId )
+ };
+}
+
+module.exports = React.createClass( {
+ displayName: 'MediaLibrarySelectedData',
+
+ propTypes: {
+ siteId: React.PropTypes.number.isRequired
+ },
+
+ getInitialState: function() {
+ return getStateData( this.props.siteId );
+ },
+
+ componentDidMount: function() {
+ MediaLibrarySelectedStore.on( 'change', this.updateState );
+ },
+
+ componentWillUnmount: function() {
+ MediaLibrarySelectedStore.off( 'change', this.updateState );
+ },
+
+ componentWillReceiveProps: function( nextProps ) {
+ if ( this.props.siteId !== nextProps.siteId ) {
+ this.setState( getStateData( nextProps.siteId ) );
+ }
+ },
+
+ updateState: function() {
+ this.setState( getStateData( this.props.siteId ) );
+ },
+
+ render: function() {
+ return passToChildren( this, this.state );
+ }
+} );
diff --git a/client/components/data/media-list-data/Makefile b/client/components/data/media-list-data/Makefile
new file mode 100644
index 00000000000000..a2fc5e4007dd36
--- /dev/null
+++ b/client/components/data/media-list-data/Makefile
@@ -0,0 +1,8 @@
+REPORTER ?= spec
+MOCHA ?= ../../../../node_modules/.bin/mocha
+
+# In order to simply stub modules, add test to the NODE_PATH
+test:
+ @NODE_ENV=test NODE_PATH=test:../../../../client $(MOCHA) --reporter $(REPORTER)
+
+.PHONY: test
diff --git a/client/components/data/media-list-data/README.md b/client/components/data/media-list-data/README.md
new file mode 100644
index 00000000000000..3824ad9028816f
--- /dev/null
+++ b/client/components/data/media-list-data/README.md
@@ -0,0 +1,33 @@
+MediaListData
+=============
+
+MediaListData is a React component intended to be used as a controller-view to simplify binding and interacting with the [media stores and actions](../../../lib/media/).
+
+## Usage
+
+Wrap a child component with ` `, passing a `siteId` and optional `filter` and `search` values. [As a controller-view](https://facebook.github.io/flux/docs/overview.html#views-and-controller-views), MediaListData does not render any content of its own; instead, it simply renders the child component.
+
+```jsx
+var React = require( 'react' ),
+ MediaListData = require( 'components/data/media-list-data' ),
+ MyChildComponent = require( './my-child-component' );
+
+module.exports = React.createClass( {
+ displayName: 'MyComponent',
+
+ render: function() {
+ return (
+
+
+
+ );
+ }
+} );
+```
+
+The child component should expect to receive any props defined during the render, as well as the following additional props:
+
+- `media`: An ordered array of known media items for the site, or `undefined` if currently fetching data
+- `mediaHasNextPage`: A boolean indicating whether more media items exist for the site
+- `mediaFetchingNextPage`: A boolean indicating whether the next page of media items is being fetched
+- `mediaOnFetchNextPage`: A function to invoke when more media items are desired
diff --git a/client/components/data/media-list-data/index.jsx b/client/components/data/media-list-data/index.jsx
new file mode 100644
index 00000000000000..9f12fb716db258
--- /dev/null
+++ b/client/components/data/media-list-data/index.jsx
@@ -0,0 +1,85 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ assign = require( 'lodash/object/assign' ),
+ isEqual = require( 'lodash/lang/isEqual' );
+
+/**
+ * Internal dependencies
+ */
+var MediaActions = require( 'lib/media/actions' ),
+ MediaListStore = require( 'lib/media/list-store' ),
+ passToChildren = require( 'lib/react-pass-to-children' ),
+ utils = require( './utils' );
+
+function getStateData( siteId ) {
+ return {
+ media: MediaListStore.getAll( siteId ),
+ mediaHasNextPage: MediaListStore.hasNextPage( siteId ),
+ mediaFetchingNextPage: MediaListStore.isFetchingNextPage( siteId )
+ };
+}
+
+module.exports = React.createClass( {
+ displayName: 'MediaListData',
+
+ propTypes: {
+ siteId: React.PropTypes.number.isRequired,
+ filter: React.PropTypes.string,
+ search: React.PropTypes.string
+ },
+
+ getInitialState: function() {
+ return getStateData( this.props.siteId );
+ },
+
+ componentWillMount: function() {
+ MediaActions.setQuery( this.props.siteId, this.getQuery() );
+ MediaListStore.on( 'change', this.updateStateData );
+ this.updateStateData();
+ },
+
+ componentWillUnmount: function() {
+ MediaListStore.off( 'change', this.updateStateData );
+ },
+
+ componentWillReceiveProps: function( nextProps ) {
+ var nextQuery = this.getQuery( nextProps );
+
+ if ( this.props.siteId !== nextProps.siteId || ! isEqual( nextQuery, this.getQuery() ) ) {
+ MediaActions.setQuery( nextProps.siteId, nextQuery );
+ this.setState( getStateData( nextProps.siteId ) );
+ }
+ },
+
+ getQuery: function( props ) {
+ var query = {};
+
+ props = props || this.props;
+
+ if ( props.search ) {
+ query.search = props.search;
+ }
+
+ if ( props.filter ) {
+ query.mime_type = utils.getMimeBaseTypeFromFilter( props.filter );
+ }
+
+ return query;
+ },
+
+ fetchData: function() {
+ MediaActions.fetchNextPage( this.props.siteId );
+ },
+
+ updateStateData: function() {
+ this.setState( getStateData( this.props.siteId ) );
+ },
+
+ render: function() {
+ return passToChildren( this, assign( {}, this.state, {
+ mediaOnFetchNextPage: this.fetchData
+ } ) );
+ }
+} );
diff --git a/client/components/data/media-list-data/test/index.js b/client/components/data/media-list-data/test/index.js
new file mode 100644
index 00000000000000..6233b44e874a78
--- /dev/null
+++ b/client/components/data/media-list-data/test/index.js
@@ -0,0 +1,3 @@
+describe( 'media-list', function() {
+ require( './specs/utils' );
+} );
diff --git a/client/components/data/media-list-data/test/specs/utils.js b/client/components/data/media-list-data/test/specs/utils.js
new file mode 100644
index 00000000000000..3727caaa2687bb
--- /dev/null
+++ b/client/components/data/media-list-data/test/specs/utils.js
@@ -0,0 +1,43 @@
+/**
+ * External dependencies
+ */
+var expect = require( 'chai' ).expect;
+
+/**
+ * Internal dependencies
+ */
+var utils = require( '../../utils' );
+
+describe( 'utils', function() {
+ describe( '#getMimeBaseTypeFromFilter()', function() {
+ it( 'should return an empty string for an unknown filter', function() {
+ var baseType = utils.getMimeBaseTypeFromFilter( 'unknown' );
+
+ expect( baseType ).to.equal( '' );
+ } );
+
+ it( 'should return "image/" for "images"', function() {
+ var baseType = utils.getMimeBaseTypeFromFilter( 'images' );
+
+ expect( baseType ).to.equal( 'image/' );
+ } );
+
+ it( 'should return "audio/" for "audio"', function() {
+ var baseType = utils.getMimeBaseTypeFromFilter( 'audio' );
+
+ expect( baseType ).to.equal( 'audio/' );
+ } );
+
+ it( 'should return "video/" for "videos"', function() {
+ var baseType = utils.getMimeBaseTypeFromFilter( 'videos' );
+
+ expect( baseType ).to.equal( 'video/' );
+ } );
+
+ it( 'should return "application/" for "documents"', function() {
+ var baseType = utils.getMimeBaseTypeFromFilter( 'documents' );
+
+ expect( baseType ).to.equal( 'application/' );
+ } );
+ } );
+} );
diff --git a/client/components/data/media-list-data/utils.js b/client/components/data/media-list-data/utils.js
new file mode 100644
index 00000000000000..b0b6e00a17da08
--- /dev/null
+++ b/client/components/data/media-list-data/utils.js
@@ -0,0 +1,36 @@
+module.exports = {
+ /**
+ * Given a media filter, returns a partial mime type that can be used to
+ * find only media of a certain type. Returns a blank mime if no filter,
+ * or an unrecognized filter, is provided.
+ *
+ * @param {string} filter - The filter to get a mime from
+ */
+ getMimeBaseTypeFromFilter: function( filter ) {
+ var mime;
+
+ switch ( filter ) {
+ case 'images':
+ mime = 'image/';
+ break;
+ case 'audio':
+ mime = 'audio/';
+ break;
+ case 'videos':
+ mime = 'video/';
+ break;
+ case 'documents':
+ // All document formats allowed by WordPress are prefixed by
+ // application/. Despite its name, no other type allowed by WP
+ // is using the prefix so this is easier then listing all doc
+ // types separately.
+ mime = 'application/';
+ break;
+ default:
+ mime = '';
+ break;
+ }
+
+ return mime;
+ }
+};
diff --git a/client/components/data/media-validation-data/README.md b/client/components/data/media-validation-data/README.md
new file mode 100644
index 00000000000000..87cb9fdc7782b2
--- /dev/null
+++ b/client/components/data/media-validation-data/README.md
@@ -0,0 +1,30 @@
+MediaValidationData
+===================
+
+MediaValidationData is a React component intended to be used as a controller-view to simplify binding and interacting with the [media validation store](../../../lib/media/validation-store.js).
+
+## Usage
+
+Wrap a child component with ` `, passing a `siteId`. [As a controller-view](https://facebook.github.io/flux/docs/overview.html#views-and-controller-views), MediaValidationData does not render any content of its own; instead, it simply renders the child component.
+
+```jsx
+var React = require( 'react' ),
+ MediaValidationData = require( 'components/data/media-validation-data' ),
+ MyChildComponent = require( './my-child-component' );
+
+module.exports = React.createClass( {
+ displayName: 'MyComponent',
+
+ render: function() {
+ return (
+
+
+
+ );
+ }
+} );
+```
+
+The child component should expect to receive any props defined during the render, as well as the following additional props:
+
+- `mediaValidationErrors`: An object of key value pairs, where the key is an item ID and the value is an array of errors for that media item.
diff --git a/client/components/data/media-validation-data/index.jsx b/client/components/data/media-validation-data/index.jsx
new file mode 100644
index 00000000000000..133b097cd7ed65
--- /dev/null
+++ b/client/components/data/media-validation-data/index.jsx
@@ -0,0 +1,50 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+/**
+ * Interanl dependencies
+ */
+var MediaValidationStore = require( 'lib/media/validation-store' ),
+ passToChildren = require( 'lib/react-pass-to-children' );
+
+function getStateData( siteId ) {
+ return {
+ mediaValidationErrors: MediaValidationStore.getAllErrors( siteId )
+ };
+}
+
+module.exports = React.createClass( {
+ displayName: 'MediaValidationData',
+
+ propTypes: {
+ siteId: React.PropTypes.number.isRequired
+ },
+
+ getInitialState: function() {
+ return getStateData( this.props.siteId );
+ },
+
+ componentDidMount: function() {
+ MediaValidationStore.on( 'change', this.updateState );
+ },
+
+ componentWillUnmount: function() {
+ MediaValidationStore.off( 'change', this.updateState );
+ },
+
+ componentWillReceiveProps: function( nextProps ) {
+ if ( this.props.siteId !== nextProps.siteId ) {
+ this.setState( getStateData( nextProps.siteId ) );
+ }
+ },
+
+ updateState: function() {
+ this.setState( getStateData( this.props.siteId ) );
+ },
+
+ render: function() {
+ return passToChildren( this, this.state );
+ }
+} );
diff --git a/client/components/data/page-templates-data/README.md b/client/components/data/page-templates-data/README.md
new file mode 100644
index 00000000000000..9d7aaf215fb125
--- /dev/null
+++ b/client/components/data/page-templates-data/README.md
@@ -0,0 +1,41 @@
+PageTemplatesData
+=================
+
+PageTemplatesData is a data component that fetches Page Templates for a given site and passes the data into a child component.
+
+Usage
+-----
+
+Wrap a child component with ` `, passing a `siteId`. [As a controller-view](https://facebook.github.io/flux/docs/overview.html#views-and-controller-views), and PageTemplatesData will pass data into its child component as props.
+
+```jsx
+import React from 'react';
+import PageTemplatesData from 'components/data/page-templates-data';
+import MyChildComponent from './my-child-component';
+
+export default React.createClass( {
+ displayName: 'MyComponent',
+
+ render() {
+ return (
+
+
+
+ );
+ }
+} );
+```
+
+The child component should expect to receive any props defined during the render, as well as the following additional props:
+
+- `pageTemplates`: An array of Page Templates data as returned from the API. Each template is represented as an object with this shape:
+
+```js
+{
+ file: "example-file.php",
+ label: "Template Example"
+}
+```
+
+- `isFetchingPageTemplates`: A boolean of whether the component is actively fetching data.
+- `isInitializedPageTemplates`: A boolean of whether the component has fetched data for this given instance.
diff --git a/client/components/data/page-templates-data/index.jsx b/client/components/data/page-templates-data/index.jsx
new file mode 100644
index 00000000000000..c38031ffdeaf6f
--- /dev/null
+++ b/client/components/data/page-templates-data/index.jsx
@@ -0,0 +1,99 @@
+/**
+ * External dependencies
+ */
+import React, { PropTypes } from 'react';
+import debugModule from 'debug';
+
+/**
+ * Internal dependencies
+ */
+import PageTemplatesStore from 'lib/page-templates/store';
+import PageTemplatesActions from 'lib/page-templates/actions';
+import sitesList from 'lib/sites-list';
+import passToChildren from 'lib/react-pass-to-children';
+
+/**
+ * Module variables
+ */
+const sites = sitesList();
+const debug = debugModule( 'calypso:page-templates-data' );
+
+export default React.createClass( {
+ displayName: 'PageTempaltesData',
+
+ propTypes: {
+ siteId: PropTypes.number.isRequired
+ },
+
+ getInitialState() {
+ return {
+ pageTemplates: []
+ };
+ },
+
+ componentWillMount() {
+ let site = sites.getSite( this.props.siteId );
+ this.activeTheme = site.options ? site.options.theme_slug : null;
+ PageTemplatesStore.on( 'change', this._updateState );
+ site.on( 'change', this._fetchTemplatesIfThemeChanges );
+ this._updateState();
+ // defer initial fetch to avoid dispatcher conflict
+ setTimeout( () => this._fetchIfUnfetched(), 0 );
+ },
+
+ componentWillUnmount() {
+ let site = sites.getSite( this.props.siteId );
+ PageTemplatesStore.off( 'change', this._updateState );
+ site.off( 'change', this._fetchTemplatesIfThemeChanges );
+ },
+
+ componentWillReceiveProps( nextProps ) {
+ if ( nextProps.siteId === this.props.siteId ) {
+ return;
+ }
+ this._fetchIfUnfetched( nextProps.siteId );
+ this._updateState( nextProps.siteId );
+ let oldSite = sites.getSite( this.props.siteId );
+ let newSite = sites.getSite( nextProps.siteId );
+ this.activeTheme = newSite.options ? newSite.options.theme_slug : null;
+ oldSite.off( 'change', this._fetchTemplatesIfThemeChanges );
+ newSite.on( 'change', this._fetchTemplatesIfThemeChanges );
+ },
+
+ render() {
+ debug( 'rendering page templates data for site ' + this.props.siteId, this.state );
+ return passToChildren( this, this.state );
+ },
+
+ _fetchIfUnfetched( siteId ) {
+ siteId = siteId || this.props.siteId;
+ if ( PageTemplatesStore.isInitialized( siteId ) ) {
+ return;
+ }
+ PageTemplatesActions.fetchPageTemplates( siteId );
+ },
+
+ _fetchTemplatesIfThemeChanges() {
+ let site = sites.getSite( this.props.siteId );
+ if ( site.options && site.options.theme_slug === this.activeTheme ) {
+ return;
+ }
+ this.activeTheme = site.options ? site.options.theme_slug : null;
+ PageTemplatesActions.fetchPageTemplates( this.props.siteId );
+ },
+
+ _getUpdatedState( siteId ) {
+ let defaultTemplate = { label: this.translate( 'Default Template' ), file: '' };
+ siteId = siteId || this.props.siteId;
+ return {
+ pageTemplates: [ defaultTemplate ].concat( PageTemplatesStore.getPageTemplates( siteId ) ),
+ isFetchingPageTemplates: PageTemplatesStore.isFetchingPageTemplates( siteId ),
+ isInitializedPageTemplates: PageTemplatesStore.isInitialized( siteId ),
+ };
+ },
+
+ _updateState( siteId ) {
+ siteId = siteId || this.props.siteId;
+ this.setState( this._getUpdatedState( siteId ) );
+ }
+} );
diff --git a/client/components/data/post-counts-data/README.md b/client/components/data/post-counts-data/README.md
new file mode 100644
index 00000000000000..1cdd48344c62e0
--- /dev/null
+++ b/client/components/data/post-counts-data/README.md
@@ -0,0 +1,28 @@
+Post Counts Data
+================
+
+A React data component that fetches Post Counts data for a given site and passes it into a child component as props.
+
+## Usage
+
+Wrap a child component with ` `, passing a `siteId` and — optionally — a post status. It'll pass a `count` property to the child with the data.
+
+```jsx
+import React from 'react';
+import PostCountsData from 'components/data/post-counts-data';
+
+export default React.createClass( {
+ render() {
+ return (
+
+
+
+ )
+ }
+} );
+```
+
+## Props
+
+- `siteId`: (required) Site id.
+- `status`: The post status you want counts for. (i.e. `[ 'publish', 'private', 'draft', 'pending', 'future', 'trash' ]` .)
diff --git a/client/components/data/post-counts-data/index.jsx b/client/components/data/post-counts-data/index.jsx
new file mode 100644
index 00000000000000..042b9482b3a0e4
--- /dev/null
+++ b/client/components/data/post-counts-data/index.jsx
@@ -0,0 +1,76 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+
+/**
+ * Internal dependencies
+ */
+import actions from 'lib/posts/actions';
+import PostCountsStore from 'lib/posts/post-counts-store';
+
+function getState( siteId, status ) {
+ const counts = PostCountsStore.get( siteId ) || {};
+ let count;
+
+ if ( counts && status ) {
+ count = counts[ status ];
+
+ // include `pending` in `draft` count
+ if ( status === 'draft' && counts.pending ) {
+ count += counts.pending;
+ }
+ }
+
+ return {
+ count: count
+ };
+}
+
+export default React.createClass( {
+ displayName: 'PostCountsData',
+
+ propTypes: {
+ siteId: React.PropTypes.number.isRequired,
+ status: React.PropTypes.string
+ },
+
+ getInitialState() {
+ return getState( this.props.siteId, this.props.status );
+ },
+
+ componentDidMount() {
+ PostCountsStore.on( 'change', this.updateState );
+ this.fetchCounts( this.props.siteId );
+ },
+
+ componentWillUnmount() {
+ PostCountsStore.off( 'change', this.updateState );
+ },
+
+ componentWillReceiveProps( nextProps ) {
+ if ( nextProps.siteId !== this.props.siteId ) {
+ this.updateState( nextProps.siteId );
+ this.fetchCounts( nextProps.siteId );
+ }
+ },
+
+ updateState( siteId ) {
+ siteId = siteId || this.props.siteId;
+ return this.setState( getState( siteId, this.props.status ) );
+ },
+
+ fetchCounts( siteId ) {
+ if ( PostCountsStore.get( siteId ) ) {
+ return;
+ }
+
+ setTimeout( function() {
+ actions.fetchCounts( siteId );
+ }, 0 );
+ },
+
+ render() {
+ return React.cloneElement( this.props.children, this.state );
+ }
+} );
diff --git a/client/components/data/post-formats-data/README.md b/client/components/data/post-formats-data/README.md
new file mode 100644
index 00000000000000..1e0dd6a25beda4
--- /dev/null
+++ b/client/components/data/post-formats-data/README.md
@@ -0,0 +1,30 @@
+PostFormatsData
+===============
+
+PostFormatsData is a React component intended to be used as a controller-view to simplify binding and interacting with the [post formats Flux module](../../../lib/post-formats/).
+
+## Usage
+
+Wrap a child component with ` `, passing a `siteId`. [As a controller-view](https://facebook.github.io/flux/docs/overview.html#views-and-controller-views), PostFormatsData does not render any content of its own; instead, it simply renders the child component. When mounted, the component will automatically trigger a network request for data if data hasn't yet been retrieved for the site.
+
+```jsx
+var React = require( 'react' ),
+ PostFormatsData = require( 'components/data/post-formats-data' ),
+ MyChildComponent = require( './my-child-component' );
+
+module.exports = React.createClass( {
+ displayName: 'MyComponent',
+
+ render: function() {
+ return (
+
+
+
+ );
+ }
+} );
+```
+
+The child component should expect to receive any props defined during the render, as well as the following additional props:
+
+- `postFormats`: An array of known post formats for the site, or `undefined` if data has not yet been fetched. Each post format is an object containing a `slug` and `label` value.
diff --git a/client/components/data/post-formats-data/index.jsx b/client/components/data/post-formats-data/index.jsx
new file mode 100644
index 00000000000000..defb9cb42d528a
--- /dev/null
+++ b/client/components/data/post-formats-data/index.jsx
@@ -0,0 +1,62 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var PostFormatsActions = require( 'lib/post-formats/actions' ),
+ PostFormatsStore = require( 'lib/post-formats/store' );
+
+function getStateData( siteId ) {
+ return {
+ postFormats: PostFormatsStore.get( siteId )
+ };
+}
+
+module.exports = React.createClass( {
+ displayName: 'PostFormatsData',
+
+ propTypes: {
+ siteId: React.PropTypes.number.isRequired
+ },
+
+ getInitialState: function() {
+ return getStateData( this.props.siteId );
+ },
+
+ componentDidMount: function() {
+ PostFormatsStore.on( 'change', this.updateState );
+ this.maybeFetchData( this.props.siteId );
+ },
+
+ componentWillUnmount: function() {
+ PostFormatsStore.off( 'change', this.updateState );
+ },
+
+ componentWillReceiveProps: function( nextProps ) {
+ if ( nextProps.siteId !== this.props.siteId ) {
+ this.updateState( nextProps.siteId );
+ this.maybeFetchData( nextProps.siteId );
+ }
+ },
+
+ maybeFetchData: function( siteId ) {
+ if ( PostFormatsStore.get( siteId ) ) {
+ return;
+ }
+
+ setTimeout( function() {
+ PostFormatsActions.fetch( siteId );
+ }, 0 );
+ },
+
+ updateState: function( siteId ) {
+ this.setState( getStateData( siteId || this.props.siteId ) );
+ },
+
+ render: function() {
+ return React.cloneElement( this.props.children, this.state );
+ }
+} );
diff --git a/client/components/data/preferences-data/README.md b/client/components/data/preferences-data/README.md
new file mode 100644
index 00000000000000..ecdfa79d6e7ca3
--- /dev/null
+++ b/client/components/data/preferences-data/README.md
@@ -0,0 +1,30 @@
+PreferencesData
+===============
+
+PreferencesData is a React component intended to be used as a controller-view to simplify binding and interacting with the [preferences Flux module](../../../lib/preferences/).
+
+## Usage
+
+Wrap a child component with ` `. [As a controller-view](https://facebook.github.io/flux/docs/overview.html#views-and-controller-views), PreferencesData does not render any content of its own; instead, it simply renders the child component. When mounted, the component will automatically trigger a network request for data if data hasn't yet been retrieved.
+
+```jsx
+var React = require( 'react' ),
+ PreferencesData = require( 'components/data/preferences-data' ),
+ MyChildComponent = require( './my-child-component' );
+
+module.exports = React.createClass( {
+ displayName: 'MyComponent',
+
+ render: function() {
+ return (
+
+
+
+ );
+ }
+} );
+```
+
+The child component should expect to receive any props defined during the render, as well as the following additional props:
+
+- `preferences`: An object of known preferences, or `undefined` if data has not yet been fetched.
diff --git a/client/components/data/preferences-data/index.jsx b/client/components/data/preferences-data/index.jsx
new file mode 100644
index 00000000000000..638e80df0d054c
--- /dev/null
+++ b/client/components/data/preferences-data/index.jsx
@@ -0,0 +1,45 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var PreferencesActions = require( 'lib/preferences/actions' ),
+ PreferencesStore = require( 'lib/preferences/store' ),
+ passToChildren = require( 'lib/react-pass-to-children' );
+
+function getStateData() {
+ return {
+ preferences: PreferencesStore.getAll()
+ };
+}
+
+module.exports = React.createClass( {
+ displayName: 'PreferencesData',
+
+ getInitialState: function() {
+ return getStateData();
+ },
+
+ componentDidMount: function() {
+ PreferencesStore.on( 'change', this.updateState );
+
+ if ( undefined === this.state.preferences ) {
+ PreferencesActions.fetch();
+ }
+ },
+
+ componentWillUnmount: function() {
+ PreferencesStore.off( 'change', this.updateState );
+ },
+
+ updateState: function() {
+ this.setState( getStateData() );
+ },
+
+ render: function() {
+ return passToChildren( this, this.state );
+ }
+} );
diff --git a/client/components/data/purchases/README.md b/client/components/data/purchases/README.md
new file mode 100644
index 00000000000000..6cd748184d5310
--- /dev/null
+++ b/client/components/data/purchases/README.md
@@ -0,0 +1,26 @@
+PurchasesData
+=============
+
+PurchasesData is a controller-view React component that fetches purchases for the current user and passes the data into a child component.
+
+## Usage
+
+Pass a component through the `component` prop of ` `. `PurchasesData` will pass data to the given `component` prop, which is mounted as a child.
+
+```js
+import React from 'react';
+import PurchasesData from 'components/data/purchases';
+import MyChildComponent from 'components/my-child-component';
+
+const MyComponent = React.createClass( {
+ render() {
+ return (
+
+ );
+ }
+} );
+
+export default MyComponent;
+```
+
+The child component should expect to receive any props defined during the render, as well as the `purchases` prop, which is the result of a call to `PurchasesStore.getByUser` for the current user, and is updated whenever `PurchasesStore` changes.
diff --git a/client/components/data/purchases/edit-card-details/index.jsx b/client/components/data/purchases/edit-card-details/index.jsx
new file mode 100644
index 00000000000000..2277108a182f9d
--- /dev/null
+++ b/client/components/data/purchases/edit-card-details/index.jsx
@@ -0,0 +1,57 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+
+/**
+ * Internal dependencies
+ */
+import CountriesList from 'lib/countries-list';
+import { fetchUserPurchases } from 'lib/upgrades/actions';
+import observe from 'lib/mixins/data-observe';
+import PurchasesStore from 'lib/purchases/store';
+import StoreConnection from 'components/data/store-connection';
+
+/**
+ * Module variables
+ */
+const stores = [
+ PurchasesStore
+];
+
+function getStateFromStores( props ) {
+ return {
+ countriesList: CountriesList.forPayments(),
+ selectedPurchase: PurchasesStore.getByPurchaseId( parseInt( props.purchaseId, 10 ) ),
+ selectedSite: props.selectedSite
+ };
+}
+
+const EditCardDetailsData = React.createClass( {
+ propTypes: {
+ cardId: React.PropTypes.string.isRequired,
+ component: React.PropTypes.func.isRequired,
+ purchaseId: React.PropTypes.string.isRequired,
+ sites: React.PropTypes.object.isRequired
+ },
+
+ mixins: [ observe( 'sites' ) ],
+
+ componentWillMount() {
+ fetchUserPurchases();
+ },
+
+ render() {
+ return (
+
+ );
+ }
+} );
+
+export default EditCardDetailsData;
diff --git a/client/components/data/purchases/index.jsx b/client/components/data/purchases/index.jsx
new file mode 100644
index 00000000000000..e619cb0bebd504
--- /dev/null
+++ b/client/components/data/purchases/index.jsx
@@ -0,0 +1,43 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+
+/**
+ * Internal dependencies
+ */
+import { fetchUserPurchases } from 'lib/upgrades/actions';
+import PurchasesStore from 'lib/purchases/store';
+import StoreConnection from 'components/data/store-connection';
+import userFactory from 'lib/user';
+
+/**
+ * Module variables
+ */
+const stores = [ PurchasesStore ],
+ user = userFactory();
+
+function getStateFromStores() {
+ return { purchases: PurchasesStore.getByUser( user.get().ID ) };
+}
+
+const PurchasesData = React.createClass( {
+ propTypes: {
+ component: React.PropTypes.func.isRequired
+ },
+
+ componentDidMount() {
+ fetchUserPurchases();
+ },
+
+ render() {
+ return (
+
+ );
+ }
+} );
+
+export default PurchasesData;
diff --git a/client/components/data/purchases/manage-purchase/index.jsx b/client/components/data/purchases/manage-purchase/index.jsx
new file mode 100644
index 00000000000000..d489d62642d891
--- /dev/null
+++ b/client/components/data/purchases/manage-purchase/index.jsx
@@ -0,0 +1,60 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+
+/**
+ * Internal dependencies
+ */
+import CartStore from 'lib/cart/store';
+import { fetchUserPurchases } from 'lib/upgrades/actions';
+import observe from 'lib/mixins/data-observe';
+import PurchasesStore from 'lib/purchases/store';
+import StoreConnection from 'components/data/store-connection';
+
+/**
+ * Module variables
+ */
+const stores = [
+ CartStore,
+ PurchasesStore
+];
+
+function getStateFromStores( props ) {
+ return {
+ cart: CartStore.get(),
+ purchaseId: props.purchaseId,
+ selectedPurchase: PurchasesStore.getByPurchaseId( parseInt( props.purchaseId, 10 ) ),
+ selectedSite: props.selectedSite,
+ destinationType: props.destinationType
+ };
+}
+
+const ManagePurchaseData = React.createClass( {
+ propTypes: {
+ component: React.PropTypes.func.isRequired,
+ purchaseId: React.PropTypes.string.isRequired,
+ sites: React.PropTypes.object.isRequired,
+ destinationType: React.PropTypes.string
+ },
+
+ mixins: [ observe( 'sites' ) ],
+
+ componentWillMount() {
+ fetchUserPurchases();
+ },
+
+ render() {
+ return (
+
+ );
+ }
+} );
+
+export default ManagePurchaseData;
diff --git a/client/components/data/sharing-connections-data/Makefile b/client/components/data/sharing-connections-data/Makefile
new file mode 100644
index 00000000000000..a0d967167c0395
--- /dev/null
+++ b/client/components/data/sharing-connections-data/Makefile
@@ -0,0 +1,8 @@
+REPORTER ?= spec
+MOCHA ?= ../../../../node_modules/.bin/mocha
+
+# In order to simply stub modules, add test to the NODE_PATH
+test:
+ @NODE_ENV=test NODE_PATH=test:../../../../client $(MOCHA) --compilers jsx:babel/register --reporter $(REPORTER)
+
+.PHONY: test
diff --git a/client/components/data/sharing-connections-data/README.md b/client/components/data/sharing-connections-data/README.md
new file mode 100644
index 00000000000000..28a158f7eec7c2
--- /dev/null
+++ b/client/components/data/sharing-connections-data/README.md
@@ -0,0 +1,40 @@
+SharingConnectionsData
+======================
+
+SharingConnectionsData is a React component intended to be used as a controller-view to simplify binding and interacting with the [`connections-list` module](../../../lib/connections-list/).
+
+## Usage
+
+Wrap a child component with ` `, passing a `siteId` and optional `userId`. [As a controller-view](https://facebook.github.io/flux/docs/overview.html#views-and-controller-views), SharingConnectionsData does not render any content of its own; instead, it simply renders the child component. When mounted, the component will automatically trigger a network request for data if data hasn't yet been retrieved for the site.
+
+```jsx
+var React = require( 'react' ),
+ SharingConnectionsData = require( 'components/data/sharing-connections-data' ),
+ MyChildComponent = require( './my-child-component' );
+
+module.exports = React.createClass( {
+ displayName: 'MyComponent',
+
+ render: function() {
+ return (
+
+
+
+ );
+ }
+} );
+```
+
+The child component should expect to receive any props defined during the render, as well as the following additional props:
+
+- `connections`: An array of known connections for the site, or `undefined` if data has not yet been fetched
+
+## Props
+
+### `siteId`
+
+Required. The site ID for which the connections should be retrieved.
+
+### `userId`
+
+Optional. If omitted, connections will be filtered to those available for the current user. Otherwise, you can pass an explicit user ID to limit connections to those available to a specific user, or pass `null` to include all connections available for a site.
diff --git a/client/components/data/sharing-connections-data/index.jsx b/client/components/data/sharing-connections-data/index.jsx
new file mode 100644
index 00000000000000..cc2241e1c8224b
--- /dev/null
+++ b/client/components/data/sharing-connections-data/index.jsx
@@ -0,0 +1,70 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var connections = require( 'lib/connections-list' )(),
+ user = require( 'lib/user' )(),
+ passToChildren = require( 'lib/react-pass-to-children' );
+
+function getConnectionsByUserId( siteId, userId ) {
+ var userConnections;
+
+ if ( undefined === userId ) {
+ userId = ( user.get() || {} ).ID;
+ }
+
+ userConnections = connections.get( siteId, {
+ userId: userId
+ } );
+
+ if ( ! connections.initialized ) {
+ return;
+ }
+
+ return userConnections;
+}
+
+function getStateData( siteId, userId ) {
+ return {
+ connections: getConnectionsByUserId( siteId, userId )
+ };
+}
+
+module.exports = React.createClass( {
+ displayName: 'SharingConnectionsData',
+
+ propTypes: {
+ siteId: React.PropTypes.number.isRequired,
+ userId: React.PropTypes.number
+ },
+
+ getInitialState: function() {
+ return getStateData( this.props.siteId, this.props.userId );
+ },
+
+ componentDidMount: function() {
+ connections.on( 'change', this.updateState );
+ },
+
+ componentWillUnmount: function() {
+ connections.off( 'change', this.updateState );
+ },
+
+ componentWillReceiveProps: function( nextProps ) {
+ if ( this.props.siteId !== nextProps.siteId ) {
+ this.setState( getStateData( nextProps.siteId, nextProps.userId ) );
+ }
+ },
+
+ updateState: function() {
+ this.setState( getStateData( this.props.siteId, this.props.userId ) );
+ },
+
+ render: function() {
+ return passToChildren( this, this.state );
+ }
+} );
diff --git a/client/components/data/sharing-connections-data/test/index.jsx b/client/components/data/sharing-connections-data/test/index.jsx
new file mode 100644
index 00000000000000..4d94419ea0fe52
--- /dev/null
+++ b/client/components/data/sharing-connections-data/test/index.jsx
@@ -0,0 +1,132 @@
+/* eslint-disable vars-on-top */
+
+/**
+ * External dependencies
+ */
+var React = require( 'react/addons' ),
+ TestUtils = React.addons.TestUtils,
+ mockery = require( 'mockery' ),
+ sinon = require( 'sinon' ),
+ chai = require( 'chai' ),
+ sinonChai = require( 'sinon-chai' ),
+ expect = chai.expect;
+
+chai.use( sinonChai );
+
+/**
+ * Module variables
+ */
+var DUMMY_SITE_ID = 77203074,
+ DUMMY_CURRENT_USER_ID = 73705554,
+ DUMMY_SECOND_USER_ID = 73705672,
+ DUMMY_CURRENT_USER_CONNECTION = { ID: 1, keyring_connection_ID: 1, keyring_connection_user_ID: DUMMY_CURRENT_USER_ID },
+ DUMMY_SECOND_USER_CONNECTION = { ID: 2, keyring_connection_ID: 2, keyring_connection_user_ID: DUMMY_SECOND_USER_ID },
+ DUMMY_CONNECTIONS = [ DUMMY_CURRENT_USER_CONNECTION, DUMMY_SECOND_USER_CONNECTION ];
+
+describe( 'SharingConnectionsData', function() {
+ var getConnections, SharingConnectionsData, renderer;
+
+ getConnections = sinon.stub();
+ getConnections.withArgs( DUMMY_SITE_ID, { userId: DUMMY_CURRENT_USER_ID } ).returns( [ DUMMY_CURRENT_USER_CONNECTION ] );
+ getConnections.withArgs( DUMMY_SITE_ID, { userId: DUMMY_SECOND_USER_ID } ).returns( [ DUMMY_SECOND_USER_CONNECTION ] );
+ getConnections.withArgs( DUMMY_SITE_ID ).returns( DUMMY_CONNECTIONS );
+
+ beforeEach( function() {
+ mockery.enable( {
+ warnOnReplace: false,
+ warnOnUnregistered: false
+ } );
+ mockery.registerMock( 'lib/user', function() {
+ return {
+ get: function() {
+ return { ID: DUMMY_CURRENT_USER_ID };
+ }
+ };
+ } );
+ mockery.registerMock( 'lib/connections-list', function() {
+ return {
+ get: getConnections,
+ initialized: true
+ };
+ } );
+
+ SharingConnectionsData = require( '../' );
+ getConnections.reset();
+ renderer = TestUtils.createRenderer();
+ } );
+
+ after( function() {
+ mockery.deregisterAll();
+ mockery.disable();
+ } );
+
+ it( 'should assume an undefined userId should include connections available to the current user', function() {
+ var result;
+
+ renderer.render(
+
+
+
+ );
+ result = renderer.getRenderOutput();
+
+ expect( getConnections ).to.have.been.calledWith( DUMMY_SITE_ID, { userId: DUMMY_CURRENT_USER_ID } );
+ expect( result.props.connections ).to.eql( [ DUMMY_CURRENT_USER_CONNECTION ] );
+ } );
+
+ it( 'should assume an explicit userId should include connections available to that user', function() {
+ var result;
+
+ renderer.render(
+
+
+
+ );
+ result = renderer.getRenderOutput();
+
+ expect( getConnections ).to.have.been.calledWith( DUMMY_SITE_ID, { userId: DUMMY_SECOND_USER_ID } );
+ expect( result.props.connections ).to.eql( [ DUMMY_SECOND_USER_CONNECTION ] );
+ } );
+
+ it( 'should assume a `null` userId should include all connections for the site', function() {
+ var result;
+
+ renderer.render(
+
+
+
+ );
+ result = renderer.getRenderOutput();
+
+ expect( getConnections ).to.have.been.calledWith( DUMMY_SITE_ID );
+ expect( result.props.connections ).to.eql( DUMMY_CONNECTIONS );
+ } );
+
+ context( 'uninitialized connections-list', function() {
+ beforeEach( function() {
+ mockery.registerMock( 'lib/connections-list', function() {
+ return {
+ get: getConnections,
+ initialized: false
+ };
+ } );
+
+ delete require.cache[ require.resolve( '../' ) ];
+ SharingConnectionsData = require( '../' );
+ } );
+
+ it( 'should pass `undefined` `connections` while the connections are being fetched', function() {
+ var result;
+
+ renderer.render(
+
+
+
+ );
+ result = renderer.getRenderOutput();
+
+ expect( result.props.connections ).to.be.undefined;
+ } );
+ } );
+} );
+
diff --git a/client/components/data/tag-list-data/README.md b/client/components/data/tag-list-data/README.md
new file mode 100644
index 00000000000000..79039fbf7fb30c
--- /dev/null
+++ b/client/components/data/tag-list-data/README.md
@@ -0,0 +1,40 @@
+TagListData
+================
+
+TagListData is a component which aims to ease interactions with the
+[terms flux store and actions](../../../lib/terms) related to post tags.
+
+## Usage
+
+Use ` ` to wrap a child component that will do the actual
+rendering of the view.
+
+```jsx
+var React = require( 'react' ),
+ TagListData = require( 'components/data/tag-list-data' ),
+ MyChildComponent = require( './my-child-component' );
+
+module.exports = React.createClass( {
+ displayName: 'MyComponent',
+
+ render: function() {
+ return (
+
+
+
+ );
+ }
+} );
+```
+
+### Required Props
+
+- `siteId`: The site you would like to get tag data for
+
+## Results
+
+The child component will receive the props outlined above, along with the following:
+
+- `tags`: An ordered array of known tags for the site, or `undefined` if currently fetching data
+- `tagsHasNextPage`: if another page of tag data can be fetched
+- `tagsFetchingNextPage`: if another page is currently being fetched
diff --git a/client/components/data/tag-list-data/index.jsx b/client/components/data/tag-list-data/index.jsx
new file mode 100644
index 00000000000000..df10502adeb3cc
--- /dev/null
+++ b/client/components/data/tag-list-data/index.jsx
@@ -0,0 +1,71 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ debug = require( 'debug' )( 'calypso:components:data:tag-list' );
+
+/**
+ * Internal dependencies
+ */
+var TermActions = require( 'lib/terms/actions' ),
+ passToChildren = require( 'lib/react-pass-to-children' ),
+ TagStore = require( 'lib/terms/tag-store' );
+
+function getStateData( siteId ) {
+ return {
+ tags: TagStore.all( siteId ),
+ tagsHasNextPage: TagStore.hasNextPage( siteId ),
+ tagsFetchingNextPage: TagStore.isFetchingPage( siteId )
+ };
+}
+
+module.exports = React.createClass( {
+ displayName: 'TagListData',
+
+ propTypes: {
+ siteId: React.PropTypes.number.isRequired
+ },
+
+ getInitialState: function() {
+ return getStateData( this.props.siteId );
+ },
+
+ componentDidMount: function() {
+ TagStore.on( 'change', this.updateState );
+ this.maybeFetchData( this.props.siteId );
+ },
+
+ componentWillUnmount: function() {
+ TagStore.off( 'change', this.updateState );
+ },
+
+ componentWillReceiveProps: function( nextProps ) {
+ if ( nextProps.siteId !== this.props.siteId ) {
+ this.updateState( nextProps.siteId );
+ this.maybeFetchData( nextProps.siteId );
+ }
+ },
+
+ fetchData: function() {
+ TermActions.fetchNextTagPage( this.props.siteId );
+ },
+
+ maybeFetchData: function( siteId ) {
+ if ( TagStore.all( siteId ) ) {
+ return;
+ }
+
+ setTimeout( function() {
+ TermActions.fetchNextTagPage( siteId );
+ }, 0 );
+ },
+
+ updateState: function( siteId ) {
+ this.setState( getStateData( siteId || this.props.siteId ) );
+ },
+
+ render: function() {
+ debug( 'rendering tag data for site ' + this.props.siteId, this.state );
+ return passToChildren( this, this.state );
+ }
+} );
diff --git a/client/components/data/viewers-data/README.md b/client/components/data/viewers-data/README.md
new file mode 100644
index 00000000000000..b89ed787cb1209
--- /dev/null
+++ b/client/components/data/viewers-data/README.md
@@ -0,0 +1,18 @@
+ViewersData
+===========
+
+A component that fetches a private wpcom site's viewers and passes them to its children.
+
+## Props
+
+` ` should be given a `siteId` which will be used in the path for the API call to /sites/$site/viewers
+
+## Usage
+
+A component wrapped with ` ` will receive the following props:
+
+- viewers: An array of viewer objects
+- totalViewers: The total number of viewers found for the site
+- currentPage: The last page that was fetched from the API
+- fetching: A boolean that is true if the fetch is in progress
+- fetchInitialized: A boolean that states if the fetch has been initialized yet
\ No newline at end of file
diff --git a/client/components/data/viewers-data/index.jsx b/client/components/data/viewers-data/index.jsx
new file mode 100644
index 00000000000000..f0bf34e37fcb00
--- /dev/null
+++ b/client/components/data/viewers-data/index.jsx
@@ -0,0 +1,100 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+
+/**
+ * Internal dependencies
+ */
+import ViewersStore from 'lib/viewers/store';
+import ViewersActions from 'lib/viewers/actions';
+import passToChildren from 'lib/react-pass-to-children';
+
+export default React.createClass( {
+ displayName: 'ViewersData',
+
+ propTypes: {
+ siteId: React.PropTypes.number.isRequired
+ },
+
+ getInitialState() {
+ return {
+ viewers: false,
+ totalViewers: false,
+ currentPage: false,
+ fetchInitialized: false
+ };
+ },
+
+ componentDidMount() {
+ ViewersStore.on( 'change', this.refreshViewers );
+ this.fetchIfEmpty( this.props.siteId );
+ },
+
+ componentWillReceiveProps( nextProps ) {
+ if ( ! nextProps.siteId ) {
+ return;
+ }
+ if ( this.props.siteId !== nextProps.siteId ) {
+ this.setState( this.getInitialState() );
+ this.fetchIfEmpty( nextProps.siteId );
+ }
+ },
+
+ componentWillUnmount() {
+ ViewersStore.removeListener( 'change', this.refreshViewers );
+ },
+
+ fetchIfEmpty( siteId ) {
+ siteId = siteId || this.props.siteId;
+ if ( ! siteId ) {
+ return;
+ }
+ if ( ViewersStore.getViewers( siteId ).length ) {
+ this.refreshViewers( siteId );
+ return;
+ }
+
+ // defer fetch requests to avoid dispatcher conflicts
+ let defer = function() {
+ var paginationData = ViewersStore.getPaginationData( siteId );
+ if ( paginationData.fetchingViewers ) {
+ return;
+ }
+ ViewersActions.fetch( siteId );
+ this.setState( { fetchInitialized: true } );
+ }.bind( this );
+ setTimeout( defer, 0 );
+ },
+
+ isFetching: function() {
+ let siteId = this.props.siteId;
+ if ( ! siteId ) {
+ return true;
+ }
+
+ if ( ! this.state.viewers ) {
+ return true;
+ }
+
+ let paginationData = ViewersStore.getPaginationData( siteId );
+
+ if ( paginationData.fetchingViewers ) {
+ return true;
+ }
+ return false;
+ },
+
+ refreshViewers( siteId ) {
+ siteId = siteId || this.props.siteId;
+ this.setState( {
+ viewers: ViewersStore.getViewers( siteId ),
+ totalViewers: ViewersStore.getPaginationData( siteId ).totalViewers,
+ currentPage: ViewersStore.getPaginationData( siteId ).currentViewersPage
+ } );
+ },
+
+ render() {
+ return passToChildren( this, Object.assign( {}, this.state, { fetching: this.isFetching() } ) );
+ }
+} );
diff --git a/client/components/date-picker/README.md b/client/components/date-picker/README.md
new file mode 100644
index 00000000000000..4dc31292c87d99
--- /dev/null
+++ b/client/components/date-picker/README.md
@@ -0,0 +1,68 @@
+DatePicker
+==========
+
+React component used to display a Date Picker.
+
+---
+
+## Example Usage
+
+```js
+var DatePicker = require( 'components/date-picker' );
+
+module.exports = React.createClass( {
+
+ // ...
+
+ this.onSelect: function( date ) {
+ this.setState( { date: date } );
+ },
+
+ render: function() {
+ var events = [
+ {
+ title: '1 other post scheduled',
+ date: new Date( '2015-10-15 10:30' ),
+ type: 'schedulled'
+ },
+ {
+ title: 'Happy birthday Damian!',
+ date: new Date( '2015-07-18 15:00' )
+ }
+ ];
+
+ return (
+
+
+ );
+ }
+
+} );
+```
+
+---
+
+## DatePicker
+
+#### Props
+
+`initialMonth` - **optional** Date object that defines the month of the calendar. Default is `now`.
+
+`selectedDay` - **optional** Moment instance to select the current day.
+
+`timeReference` - **optional** Moment instance used to adjust the time when a day
+is selected.
+
+`events` - **optional** Array of events.
+
+`className` - **optional** Add a custom class property.
+
+`onSelect` - **optional**
+
+`onMonthChange` - **optional**
+
+------------
diff --git a/client/components/date-picker/day.jsx b/client/components/date-picker/day.jsx
new file mode 100644
index 00000000000000..854449f238d51b
--- /dev/null
+++ b/client/components/date-picker/day.jsx
@@ -0,0 +1,121 @@
+/**
+ * External Dependencies
+ */
+var React = require( 'react' ),
+ noop = require( 'lodash/utility/noop' ),
+ classNames = require( 'classnames' );
+
+/**
+ * Internal dependencies
+ */
+var Tooltip = require( 'components/tooltip' );
+
+module.exports = React.createClass( {
+ displayName: 'DatePickerDay',
+
+ propTypes: {
+ date: React.PropTypes.object.isRequired,
+ events: React.PropTypes.array
+ },
+
+ getInitialState: function() {
+ return {
+ showTooltip: false
+ };
+ },
+
+ isPastDay: function( date ) {
+ var today = this.moment().set( {
+ hour: 0,
+ minute: 0,
+ second: 0,
+ millisecond: 0
+ } );
+
+ date = date || this.props.date;
+
+ return ( +today - 1 ) >= +date;
+ },
+
+ handleTooltip: function( show ) {
+ var showTooltip = ! ! this.props.events.length && show;
+ this.setState( { showTooltip: showTooltip } );
+ },
+
+ renderTooltip: function() {
+ var label;
+
+ if ( ! this.state.showTooltip ) {
+ return;
+ }
+
+ label = this.translate(
+ '%(posts)d post',
+ '%(posts)d posts', {
+ count: this.props.events.length,
+ args: {
+ posts: this.props.events.length
+ }
+ }
+ );
+
+ return (
+
+ { label }
+
+
+ {
+ this.props.events.map( function( event ) {
+ return { event.title } ;
+ } )
+ }
+
+
+ );
+ },
+
+ render: function() {
+ var classes = { 'date-picker__day': true },
+ i = 0,
+ dayEvent;
+
+ classes[ 'is-selected' ] = this.props.selected === true;
+ classes[ 'past-day' ] = this.isPastDay() === true;
+
+ if ( this.props.events.length ) {
+ classes[ 'date-picker__day_event' ] = true;
+
+ for ( i; i < this.props.events.length; i++ ) {
+ dayEvent = this.props.events[ i ];
+
+ if ( dayEvent.type &&
+ ( ! classes[ 'date-picker__day_event_' + dayEvent.type ] ) ) {
+ classes[ 'date-picker__day_event_' + dayEvent.type ] = true;
+ }
+ }
+ }
+
+ return (
+
+
+
+
+ { this.props.date.getDate() }
+
+
+ { this.renderTooltip() }
+
+ );
+ }
+} );
diff --git a/client/components/date-picker/docs/example.jsx b/client/components/date-picker/docs/example.jsx
new file mode 100644
index 00000000000000..0010e58fa5247f
--- /dev/null
+++ b/client/components/date-picker/docs/example.jsx
@@ -0,0 +1,75 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var Card = require( 'components/card' ),
+ DatePicker = require( 'components/date-picker' );
+
+/**
+ * Date Picker Demo
+ */
+var datePicker = React.createClass( {
+ mixins: [ React.addons.PureRenderMixin ],
+
+ getInitialState: function() {
+ var date = new Date();
+ date.setDate( date.getDate() + 3 );
+ date.setMilliseconds( 0 );
+ date.setSeconds( 0 );
+ date.setMinutes( 0 );
+ date.setHours( 0 );
+
+ return {
+ events: [
+ {
+ title: '1 other post scheduled',
+ date: new Date( '2015-07-15 10:30' ),
+ type: 'scheduled'
+ },
+ {
+ title: 'Happy birthday Damian',
+ date: new Date( '2015-07-18 15:00' ),
+ type: 'birthday'
+ },
+ {
+ title: 'Do not rest',
+ date: new Date( '2015-07-18 8:00' )
+ }
+ ],
+ selectedDay: this.moment( date )
+ };
+ },
+
+ selectDay: function( date, modifiers ) {
+ this.setState( { selectedDay: date } );
+
+ if ( date ) {
+ console.log( date.toDate(), modifiers );
+ }
+ },
+
+ render: function() {
+ return (
+
+ );
+ }
+} );
+
+module.exports = datePicker;
diff --git a/client/components/date-picker/index.jsx b/client/components/date-picker/index.jsx
new file mode 100644
index 00000000000000..b6433ebd8cb006
--- /dev/null
+++ b/client/components/date-picker/index.jsx
@@ -0,0 +1,139 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ DayPicker = require( 'react-day-picker' ),
+ merge = require( 'lodash/object/merge' ),
+ noop = require( 'lodash/utility/noop' );
+
+/**
+ * Internal dependencies
+ */
+var DayItem = require( 'components/date-picker/day' );
+
+/* Internal dependencies
+ */
+module.exports = React.createClass( {
+ displayName: 'DatePicker',
+
+ propTypes: {
+ calendarViewDate: React.PropTypes.object,
+ enableOutsideDays: React.PropTypes.bool,
+ events: React.PropTypes.array,
+ locale: React.PropTypes.object,
+
+ selectedDay: React.PropTypes.object,
+ timeReference: React.PropTypes.object,
+
+ onMonthChange: React.PropTypes.func,
+ onSelectDay: React.PropTypes.func
+ },
+
+ getDefaultProps: function() {
+ return {
+ enableOutsideDays: true,
+ calendarViewDate: new Date(),
+ selectedDay: null,
+ onMonthChange: noop,
+ onSelectDay: noop
+ };
+ },
+
+ isSameDay: function( d0, d1 ) {
+ d0 = this.moment( d0 );
+ d1 = this.moment( d1 );
+ return d0.isSame( d1, 'day' );
+ },
+
+ filterEventsByDay: function( day ) {
+ var i, event, eventsInDay = [];
+
+ if ( ! this.props.events ) {
+ return [];
+ }
+
+ for ( i = 0; i < this.props.events.length; i++ ) {
+ event = this.props.events[ i ];
+
+ if ( this.isSameDay( event.date, day ) ) {
+ eventsInDay.push( event );
+ }
+ }
+
+ return eventsInDay;
+ },
+
+ locale: function() {
+ var moment = this.moment,
+ localeData = moment().localeData(),
+ locale = {
+ formatMonthTitle: function( date ) {
+ return moment( date ).format( 'MMMM YYYY' );
+ },
+
+ formatWeekdayShort: function( day ) {
+ return moment().weekday( day ).format( 'dd' )[ 0 ];
+ },
+
+ formatWeekdayLong: function( day ) {
+ return moment().weekday( day ).format( 'dddd' );
+ },
+
+ getFirstDayOfWeek: function() {
+ return localeData.firstDayOfWeek();
+ }
+ };
+
+ return merge( locale, this.props.locale );
+ },
+
+ setCalendarDay: function( event, clickedDay ) {
+ clickedDay = this.moment( clickedDay );
+
+ let modifiers = {
+ year: clickedDay.year(),
+ month: clickedDay.month(),
+ date: clickedDay.date()
+ };
+
+ let date = ( this.props.timeReference || clickedDay ).set( modifiers );
+
+ this.props.onSelectDay( date, modifiers );
+ },
+
+ handleCaptionClick: function() {
+ var daypicker = this.refs.daypicker;
+ daypicker.showMonth( new Date() );
+ },
+
+ renderDay: function( day ) {
+ var isSelected = this.props.selectedDay &&
+ this.isSameDay( this.props.selectedDay, day );
+
+ return (
+
+ );
+ },
+
+ render: function() {
+ return (
+
+
+
+
+ );
+ }
+} );
diff --git a/client/components/date-picker/style.scss b/client/components/date-picker/style.scss
new file mode 100644
index 00000000000000..8a8ac6bcd7a621
--- /dev/null
+++ b/client/components/date-picker/style.scss
@@ -0,0 +1,232 @@
+$date-picker_caption_height: 50px;
+$date-picker_nav_button_size: 20px;
+
+.date-picker {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ position: relative;
+ padding: 0;
+ user-select: none;
+ width: 100%;
+}
+
+.date-picker__day {
+ position: relative;
+ border-radius: 50%;
+ border: 1px solid $transparent;
+ width: 24px;
+ height: 24px;
+ color: inherit;
+ font-weight: inherit;
+ text-decoration: inherit;
+ line-height: 24px;
+ text-align: center;
+ margin: 0 auto;
+
+ &.is-selected {
+ color: $white;
+ &:hover {
+ color: $white;
+ }
+ }
+
+ &:hover {
+ color: lighten( $gray, 10% );
+ }
+}
+
+.date-picker__day_event {
+ border-color: lighten( $gray, 20 );
+}
+
+.date-picker__day-text {
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ color: inherit;
+ font-weight: inherit;
+ text-decoration: inherit;
+ transition: color 90ms ease;
+}
+
+.date-picker__day-selected {
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ transition: transform 125ms cubic-bezier( 0.215, 0.610, 0.355, 1.000 ), opacity 125ms ease-out;
+ transform: scale( 0 );
+ opacity: 0;
+ border-radius: 50%;
+ background-color: $blue-medium;
+}
+
+.date-picker__day.is-selected .date-picker__day-selected {
+ transition: transform 125ms cubic-bezier( 0.105, 1.075, 0.940, 1.080 ) , opacity 125ms ease-out;
+ transform: scale( 1 );
+ opacity: 1;
+}
+
+/**
+ * The follow class names are coming from react-day-picker component
+ * and they aren't possible change them without change its code base.
+ */
+
+.DayPicker-Month {
+ display: table;
+ width: 100%;
+ border-collapse: collapse;
+ border-spacing: 0;
+ user-select: none;
+ margin: 0;
+}
+
+.DayPicker-NavBar {
+ position: absolute;
+ left: 0;
+ right: 0;
+ height: $date-picker_caption_height;
+}
+
+.DayPicker-NavButton {
+ position: absolute;
+ width: $date-picker_nav_button_size;
+ height: $date-picker_nav_button_size;
+ line-height: $date-picker_nav_button_size;
+ top: ( $date-picker_caption_height - $date-picker_nav_button_size ) / 2;
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: contain;
+ cursor: pointer;
+ font-size: 18px;
+
+ &:before {
+ height: $date-picker_nav_button_size;
+ }
+}
+
+.DayPicker-NavButton--prev {
+ color: $gray;
+ left: 0;
+
+ &:before {
+ @include noticon( '\f431' );
+ transform: rotate( 90deg );
+ }
+}
+
+.DayPicker-NavButton--next {
+ color: $gray;
+ right: 0;
+
+ &:before {
+ @include noticon( '\f432' );
+ transform: rotate( 90deg );
+ }
+}
+
+.DayPicker-Caption {
+ display: table-caption;
+ color: $gray;
+ text-align: center;
+ height: $date-picker_caption_height;
+ line-height: $date-picker_caption_height;
+ font-size: 18px;
+ font-weight: 300;
+ margin: 0 $date-picker_nav_button_size * 1.5;
+ position: relative;
+ cursor: pointer;
+
+ &:first-letter {
+ text-transform: uppercase;
+ }
+}
+
+.DayPicker-Weekdays {
+ margin-top: 10px;
+ border-top: 1px solid lighten( $gray, 25% );
+ display: table-header-group;
+}
+
+.DayPicker-Weekday {
+ display: table-cell;
+ padding: 15px 0 10px;
+ font-size: 11px;
+ text-align: center;
+ font-weight: 600;
+ color: lighten( $gray, 20% );
+ text-transform: uppercase;
+}
+
+.DayPicker-Body {
+ display: table-row-group;
+}
+
+.DayPicker-Week {
+ display: table-row;
+}
+
+.DayPicker-Day {
+ display: table-cell;
+ position: relative;
+ height: 34px;
+ line-height: 34px;
+ vertical-align: middle;
+ text-align: center;
+ cursor: pointer;
+ font-size: 11px;
+ font-weight: 600;
+ color: $gray;
+}
+
+.DayPicker--interactionDisabled .DayPicker-Day {
+ cursor: default;
+}
+
+// Modifiers
+
+.DayPicker-Day--today {
+ &::before {
+ content: '';
+ box-sizing: border-box;
+ width: 24px;
+ height: 24px;
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ margin: -12px 0 0 -12px;
+ border: solid 1px $orange-jazzy;
+ border-radius: 50%;
+ }
+}
+
+.DayPicker-Day--disabled {
+ color: lighten( $gray, 20% );
+ cursor: default;
+}
+
+.DayPicker-Day--sunday {
+ color: $gray-light;
+}
+
+.DayPicker-Day--outside {
+ cursor: pointer;
+ font-weight: normal;
+
+ .date-picker__day {
+ color: lighten( $gray, 20% );
+ }
+}
+
+.DayPicker-Day--selected:not(.DayPicker-Day--disabled):not(.DayPicker-Day--outside) {
+ color: $white;
+ background-color: $gray-light;
+}
+
+.DayPicker--ar {
+ direction: rtl;
+}
diff --git a/client/components/dialog/README.md b/client/components/dialog/README.md
new file mode 100644
index 00000000000000..3c8914586dfcd6
--- /dev/null
+++ b/client/components/dialog/README.md
@@ -0,0 +1,153 @@
+Dialog
+======
+
+A React component that provides support for modal dialogs.
+
+Place the `Dialog` JSX element anywhere -- when it is rendered, the resulting DOM nodes for the dialog will be
+appending to the `body` of the document.
+
+The `onClose` property must be set, and should modify the parent's state such that the dialog's `isVisible` property
+will be false when `render` is called.
+
+By controlling the dialog's visibility through the `isVisible` property, the dialog component itself is responsible for
+providing any CSS transitions to animate the opening/closing of the dialog. This also keeps the parent's code clean and
+readable, with a minimal amount of boilerplate code required to show a dialog.
+
+### Basic Usage
+
+```js
+var MyComponent = React.createClass( {
+ getInitialState: function() {
+ return {
+ showDialog: false
+ };
+ },
+
+ render: function() {
+ var buttons = [
+ { action: 'cancel', label: this.translate( 'Cancel' ) },
+ { action: 'delete', label: this.translate( 'Delete Everything' ), isPrimary: true },
+
+ ];
+
+ return (
+
+
Show Dialog
+
+
+ { this.translate( 'Confirmation' ) }
+ { this.translate( 'Do you want to delete everything?' ) }
+
+
+ );
+ },
+
+ _onShowDialog: function() {
+ this.setState( { showDialog: true } );
+ },
+
+ _onCloseDialog: function( action ) {
+ // action is the `action` property of the button clicked to close the dialog. If the dialog is closed
+ // by pressing ESC or clicking outside of the dialog, action will be `undefined`
+
+ this.setState( { showDialog: false } );
+ }
+} );
+
+React.render(
+ ,
+ document.getElementById( 'content' )
+);
+```
+
+### `onClick` handlers for buttons
+
+You can attach `onClick` handlers for dialog buttons. The `onClick` handler will be passed a function that when
+called will close the dialog the dialog button is a member of.
+
+```js
+ render: function() {
+ buttons = [
+ { action: 'more-options', label: this.translate( 'More Options…' ), onClick: this._onMoreOptions },
+ { action: 'cancel', label: this.translate( 'Cancel' ) },
+ { action: 'save', label: this.translate( 'Save' ), isPrimary: true }
+ ];
+
+ return (
+
+ { this.translate( 'Dialog Title' ) }
+ { this.translate( 'Dialog content' ) }
+
+ );
+ },
+
+ _onMoreOptions: function( closeDialog ) {
+ // call the passed in `closeDialog` function to close the dialog the dialog button is
+ // a member of
+ }
+```
+
+### Custom Buttons
+
+If you need more than can be provided by passing button props, you can also pass a ReactElement in the place of
+the button spec. The ReactElement cannot close the dialog directly, but you could close the dialog by routing back
+through the Dialog's host.
+
+```js
+ render: function() {
+ buttons = [
+
+ ];
+
+ return (
+
+ { this.translate( 'Dialog Title' ) }
+ { this.translate( 'Dialog content' ) }
+
+ );
+ },
+
+ _onCustomButtonAction: function() {
+ this.setState( { showDialog: false } );
+ }
+
+```
+
+### Providing custom styling for a dialog
+
+The dialog component renders the following DOM tree (simplified to only show structure and classes):
+
+```html
+
+```
+
+You can provide custom styling for a dialog by making use of the following properties:
+
+- `baseClassName`: if you specify this, you are responsible for providing all the following classes for the dialog (you
+can `@extend` the base `dialog` SCSS classes if you just want to tweak things a bit):
+ - _baseClassName_
+ - _baseClassName___backdrop
+ - _baseClassName___content
+ - _baseClassName___action-buttons
+- `additionalClassNames`: if you specify this, these additional class names will be applied to the dialog element
+(not the backdrop)
+
+```js
+ render: function() {
+ return (
+
+ { this.translate( 'Dialog Title' ) }
+ { this.translate( 'Dialog content' ) }
+
+ );
+ }
+```
diff --git a/client/components/dialog/dialog-base.jsx b/client/components/dialog/dialog-base.jsx
new file mode 100644
index 00000000000000..66651844c6c9aa
--- /dev/null
+++ b/client/components/dialog/dialog-base.jsx
@@ -0,0 +1,163 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ clickOutside = require( 'click-outside' ),
+ closest = require( 'component-closest' ),
+ noop = require( 'lodash/utility/noop' ),
+ joinClasses = require( 'react/lib/joinClasses' );
+
+/**
+ * Internal dependencies
+ */
+var closeOnEsc = require( 'lib/mixins/close-on-esc' ),
+ Card = require( 'components/card' ),
+ trapFocus = require( 'lib/mixins/trap-focus' );
+
+var DialogBase = React.createClass( {
+ mixins: [ closeOnEsc( '_close' ), trapFocus ],
+
+ displayName: 'DialogBase',
+
+ getDefaultProps: function() {
+ return {
+ baseClassName: 'dialog',
+ isFullScreen: true,
+ autoFocus: true,
+ onClickOutside: noop
+ };
+ },
+
+ componentDidMount: function() {
+ // set focus after a short timeout in order to avoid
+ // interrupting any CSS transitions (Chrome issue)
+ this._focusTimeout = setTimeout( function() {
+ this._focusTimeout = false;
+ if ( this.props.autoFocus ) {
+ React.findDOMNode( this.refs.content ).focus();
+ }
+
+ this._unbindClickHandler = clickOutside( React.findDOMNode( this.refs.dialog ), this._onBackgroundClick );
+ }.bind( this ), 10 );
+ },
+
+ componentWillUnmount: function() {
+ if ( this._focusTimeout ) {
+ clearTimeout( this._focusTimeout );
+ this._focusTimeout = false;
+ }
+
+ if ( this._unbindClickHandler ) {
+ this._unbindClickHandler();
+ this._unbindClickHandler = null;
+ }
+ },
+
+ render: function() {
+ var baseClassName = this.props.baseClassName,
+ backdropClassName = baseClassName + '__backdrop',
+ dialogClassName = baseClassName,
+ contentClassName = baseClassName + '__content';
+
+ if ( this.props.additionalClassNames ) {
+ dialogClassName = joinClasses( this.props.additionalClassNames, dialogClassName );
+ }
+
+ if ( this.props.isFullScreen ) {
+ backdropClassName = joinClasses( 'is-full-screen', backdropClassName );
+ }
+
+ return (
+
+
+
+ { this.props.children }
+
+ { this._renderButtonsBar() }
+
+
+ );
+ },
+
+ _renderButtonsBar: function() {
+ var baseClassName = this.props.baseClassName,
+ buttonsClassName = baseClassName + '__action-buttons';
+
+ if ( ! this.props.buttons ) {
+ return null;
+ }
+
+ return (
+
+ { this.props.buttons.map( this._renderButton, this ) }
+
+ );
+ },
+
+ _renderButton: function( button, index ) {
+ if ( React.isValidElement( button ) ) {
+ return React.cloneElement( button, { key: 'dialog-button-' + index } );
+ }
+
+ let classes = this._getButtonClasses( button ),
+ clickHandler = this._onButtonClick.bind( this, button );
+
+ return (
+
+ { button.label }
+
+ );
+ },
+
+ _getButtonClasses: function( button ) {
+ var classes = button.className || 'button';
+
+ if ( button.isPrimary || this.props.buttons.length === 1 ) {
+ classes += ' is-primary';
+ }
+
+ if ( button.additionalClassNames ) {
+ classes += ' ' + button.additionalClassNames;
+ }
+
+ return classes;
+ },
+
+ _onBackgroundClick: function( event ) {
+ // In cases of Dialogception (Dialog inside a Dialog), we want to
+ // prevent a click outside the currently visible Dialog from closing
+ // any dialogs below.
+ var isBackdropOrLowerStackingContext = (
+ ! this.refs ||
+ React.findDOMNode( this.refs.backdrop ).contains( event.target ) || // Clicked on this dialog's backdrop
+ ! closest( event.target, '.dialog__backdrop', true ) // Clicked offscreen, but not from another dialog
+ );
+
+ if ( ! isBackdropOrLowerStackingContext ) {
+ return;
+ }
+
+ this.props.onClickOutside( event );
+
+ if ( ! event.defaultPrevented ) {
+ this._close();
+ }
+ },
+
+ _onButtonClick: function( button ) {
+ if ( button.onClick ) {
+ button.onClick( this._close.bind( this, button.action ) );
+ return;
+ }
+
+ this._close( button.action );
+ },
+
+ _close: function( action ) {
+ if ( this.props.onDialogClose ) {
+ this.props.onDialogClose( action );
+ }
+ }
+} );
+
+module.exports = DialogBase;
diff --git a/client/components/dialog/index.jsx b/client/components/dialog/index.jsx
new file mode 100644
index 00000000000000..95085407e81e80
--- /dev/null
+++ b/client/components/dialog/index.jsx
@@ -0,0 +1,91 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react/addons' ),
+ noop = require( 'lodash/utility/noop' ),
+ debug = require( 'debug' )( 'calypso:dialog' );
+
+/**
+ * Internal dependencies
+ */
+var SingleChildCSSTransitionGroup = require( 'components/single-child-css-transition-group' ),
+ DialogBase = require( './dialog-base' );
+
+var Dialog = React.createClass( {
+ propTypes: {
+ isVisible: React.PropTypes.bool,
+ baseClassName: React.PropTypes.string,
+ enterTimeout: React.PropTypes.number,
+ leaveTimeout: React.PropTypes.number,
+ onClosed: React.PropTypes.func
+ },
+
+ getDefaultProps: function() {
+ return {
+ isVisible: false,
+ enterTimeout: 200,
+ leaveTimeout: 200,
+ transitionLeave: true,
+ onClosed: noop,
+ onClickOutside: noop
+ };
+ },
+
+ componentDidMount: function() {
+ debug( 'mounting' );
+ this._container = document.createElement( 'div' );
+ document.body.appendChild( this._container );
+
+ this._renderDialogBase();
+ },
+
+ componentDidUpdate: function() {
+ debug( 'updating' );
+ this._renderDialogBase();
+ },
+
+ componentWillUnmount: function() {
+ debug( 'unmounting' );
+ if ( this._container ) {
+ React.unmountComponentAtNode( this._container );
+ this._container.parentNode.removeChild( this._container );
+ this._container = null;
+ }
+ },
+
+ _renderDialogBase: function() {
+ var dialogComponent = this.props.isVisible ? : null,
+ transitionName = this.props.baseClassName || 'dialog';
+
+ React.render(
+
+ { dialogComponent }
+ ,
+ this._container
+ );
+ },
+
+ render: function() {
+ return null;
+ },
+
+ onDialogDidLeave: function() {
+ if ( this.props.onClosed ) {
+ process.nextTick( this.props.onClosed );
+ }
+ },
+
+ onDialogClose: function( action ) {
+ if ( this.props.onClose ) {
+ this.props.onClose( action );
+ }
+ }
+} );
+
+module.exports = Dialog;
diff --git a/client/components/dialog/style.scss b/client/components/dialog/style.scss
new file mode 100644
index 00000000000000..4bb5dc23f24fac
--- /dev/null
+++ b/client/components/dialog/style.scss
@@ -0,0 +1,83 @@
+.dialog__backdrop {
+ align-items: center;
+ bottom: 0;
+ left: 0;
+ display: flex;
+ justify-content: center;
+ position: fixed;
+ right: 0;
+ top: 46px;
+ transition: background-color .2s ease-in;
+ z-index: 100200; // try to ensure that dialogs are on top of everything else
+
+ &.dialog-enter,
+ &.dialog-leave.dialog-leave-active {
+ background-color: rgba( lighten( $gray, 30% ), 0 );
+ }
+
+ &,
+ &.dialog-enter.dialog-enter-active,
+ &.dialog-leave {
+ background-color: rgba( lighten( $gray, 30% ), 0.8 );
+ }
+
+ // covers the masterbar as well
+ &.is-full-screen {
+ top: 0;
+ }
+}
+
+.dialog.card {
+ max-width: 90%;
+ opacity: 1;
+ position: relative;
+ transition: opacity .2s ease-in;
+ // IE needs a horizontal margin values to properly center flex item
+ margin: auto 0;
+
+ .dialog-enter &,
+ .dialog-leave.dialog-leave-active & {
+ opacity: 0;
+ }
+
+ &,
+ .dialog-enter.dialog-enter-active &,
+ .dialog-leave & {
+ opacity: 1;
+ }
+}
+
+.dialog__content {
+ color: darken( $gray, 20% );
+
+ h1 {
+ color: $gray-dark;
+ font-size: 1.375em;
+ font-weight: 600;
+ height: 2em;
+ line-height: 2em;
+ margin-bottom: .5em;
+ }
+}
+
+.dialog__action-buttons {
+ overflow: hidden;
+ border-top: 1px solid lighten( $gray, 30% );
+ padding: 16px;
+ margin: 0 -24px -24px;
+ text-align: right;
+}
+
+.dialog__action-buttons .button {
+ margin-left: 10px;
+ min-width: 80px;
+
+ .is-left-aligned {
+ margin-left: 0;
+ margin-right: 10px;
+ }
+}
+
+.dialog__action-buttons .is-left-aligned {
+ float: left;
+}
diff --git a/client/components/domains/README.md b/client/components/domains/README.md
new file mode 100644
index 00000000000000..48a2c60c68a41b
--- /dev/null
+++ b/client/components/domains/README.md
@@ -0,0 +1,42 @@
+Domains Components
+==================
+
+This directory contains React components that are used for purchasing domains at both /domains and /start.
+
+It contains the following components:
+
+domain-mapping-suggestion
+-------------------------
+Suggests that you may want to map a domain you searched for
+
+domain-product-price
+--------------------
+Shows the price of a domain product
+
+domain-registration-suggestion
+------------------------------
+Suggests domains available to register
+
+domain-search-results
+---------------------
+The results of domain search - offers mapping or registration
+
+domain-suggestion
+-----------------
+A single registration suggestion
+
+example-domain-suggestions
+--------------------------
+Gives examples of what domains are available
+
+map-domain
+----------
+A wrapper for map domain step
+
+map-domain-step
+---------------
+Enables users to select a domain to map
+
+register-domain-step
+--------------------
+Enables users to select a domain to register
diff --git a/client/components/domains/domain-mapping-suggestion/index.jsx b/client/components/domains/domain-mapping-suggestion/index.jsx
new file mode 100644
index 00000000000000..f26d457b18a7f1
--- /dev/null
+++ b/client/components/domains/domain-mapping-suggestion/index.jsx
@@ -0,0 +1,45 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var DomainSuggestion = require( 'components/domains/domain-suggestion' );
+
+var DomainMappingSuggestion = React.createClass( {
+ render: function() {
+ var buttonLabel = this.translate( 'Map it', {
+ context: 'Go to the flow to add a domain mapping'
+ } );
+
+ return (
+
+
+
+ { this.translate( 'Already own a domain?', {
+ context: 'Upgrades: Register domain header',
+ comment: 'Asks if you want to own a new domain (not if you want to map an existing domain).'
+ } ) }
+
+
+ { this.translate( 'Map this domain to use it as your site\'s address.', {
+ context: 'Upgrades: Register domain description',
+ comment: 'Explains how you could use a new domain name for your site\'s address.'
+ } ) }
+
+
+
+ );
+ }
+} );
+
+module.exports = DomainMappingSuggestion;
diff --git a/client/components/domains/domain-mapping-suggestion/style.scss b/client/components/domains/domain-mapping-suggestion/style.scss
new file mode 100644
index 00000000000000..3c4835390fc2c3
--- /dev/null
+++ b/client/components/domains/domain-mapping-suggestion/style.scss
@@ -0,0 +1,40 @@
+.domain-mapping-suggestion {
+ display: flex;
+
+ .domain-product-price {
+ @include breakpoint( ">660px" ) {
+ margin-top: 5px;
+ }
+
+ &.is-free-domain {
+ @include breakpoint( ">660px" ) {
+ margin-top: 5px;
+ }
+ }
+ }
+ .domain-suggestion__action {
+ margin-top: 20px;
+
+ @include breakpoint( ">660px" ) {
+ margin-top: 10px;
+ }
+ }
+}
+
+.domain-mapping-suggestion__domain-description {
+ @include breakpoint( ">660px" ) {
+ width: 75%;
+ }
+
+ > p {
+ color: $gray-dark;
+ font-size: 12px;
+ font-weight: 600;
+ margin-bottom: 0;
+ opacity: 0.7;
+
+ @include breakpoint( ">960px" ) {
+ margin-bottom: 8px;
+ }
+ }
+}
diff --git a/client/components/domains/domain-product-price/index.jsx b/client/components/domains/domain-product-price/index.jsx
new file mode 100644
index 00000000000000..d217c8a8a6a1e4
--- /dev/null
+++ b/client/components/domains/domain-product-price/index.jsx
@@ -0,0 +1,37 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ classNames = require( 'classnames' );
+
+var DomainProductPrice = React.createClass( {
+ render: function() {
+ var freeWithPlan = this.props.cart && this.props.cart.hasLoadedFromServer && this.props.cart.next_domain_is_free && ! this.props.isFinalPrice,
+ classes = classNames( 'domain-product-price', { 'is-free-domain': freeWithPlan }, {
+ 'is-placeholder': this.props.isLoading
+ } );
+
+ if ( this.props.isLoading ) {
+ return { this.translate( 'Loading…' ) }
;
+ }
+
+ return (
+
+
+ {
+ this.props.price ?
+ this.translate( '%(cost)s {{small}}/year{{/small}}', {
+ args: { cost: this.props.price },
+ components: { small: }
+ } ) :
+ this.translate( 'Free' )
+ }
+
+
+ { freeWithPlan ? { this.translate( 'Free with your plan' ) } : null }
+
+ );
+ }
+} );
+
+module.exports = DomainProductPrice;
diff --git a/client/components/domains/domain-product-price/style.scss b/client/components/domains/domain-product-price/style.scss
new file mode 100644
index 00000000000000..fbafc5a1df8ef7
--- /dev/null
+++ b/client/components/domains/domain-product-price/style.scss
@@ -0,0 +1,63 @@
+.domain-product-price {
+ color: darken( $gray, 10% );
+ display: inline-block;
+ font-size: 17px;
+ font-weight: 600;
+
+ @include breakpoint( ">660px" ) {
+ min-width: 125px
+ }
+
+ @include breakpoint( "<660px" ) {
+ font-size: 14px;
+ }
+
+ .map-domain-step & {
+ @include breakpoint( "<660px" ) {
+ padding-bottom: 5px;
+ }
+ }
+
+ small {
+ font-weight: 400;
+ opacity: .6;
+ }
+
+ .domain-product-price__free-text {
+ color: $alert-green;
+ display: block;
+ font-size: 11px;
+ text-transform: uppercase;
+ }
+
+ &.is-free-domain {
+ display: block;
+ font-size: 13px;
+
+ @include breakpoint( ">960px" ) {
+ margin-top: -6px;
+ }
+
+ small {
+ font-size: 100%;
+ opacity: 1;
+ }
+
+ .domain-product-price__price {
+ opacity: .6;
+ text-decoration: line-through;
+ }
+ }
+
+ .is-placeholder & {
+ @include breakpoint( ">660px" ) {
+ display: none;
+ }
+
+ @include breakpoint( "<660px" ) {
+ animation: loading-fade 1.6s ease-in-out infinite;
+ background-color: lighten( $gray, 30% );
+ color: transparent;
+ }
+ }
+}
diff --git a/client/components/domains/domain-registration-suggestion/index.jsx b/client/components/domains/domain-registration-suggestion/index.jsx
new file mode 100644
index 00000000000000..eace6aaf04a09f
--- /dev/null
+++ b/client/components/domains/domain-registration-suggestion/index.jsx
@@ -0,0 +1,62 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ isEmpty = require( 'lodash/lang/isEmpty' );
+
+/**
+ * Internal dependencies
+ */
+var DomainSuggestion = require( 'components/domains/domain-suggestion' ),
+ cartItems = require( 'lib/cart-values/cart-items' );
+
+var DomainRegistrationSuggestion = React.createClass( {
+ propTypes: {
+ cart: React.PropTypes.object,
+ suggestion: React.PropTypes.object,
+ onButtonClick: React.PropTypes.func
+ },
+
+ buttonLabel: function( isAdded ) {
+ if ( this.props.buttonLabel ) {
+ return this.props.buttonLabel;
+ }
+
+ if ( isAdded ) {
+ return null;
+ }
+
+ return this.translate( 'Add', {
+ context: 'Add a domain registration to the shopping cart'
+ } );
+ },
+
+ render: function() {
+ var suggestion = this.props.suggestion ? this.props.suggestion : {},
+ domainName = suggestion.domain_name ? suggestion.domain_name : this.translate( 'Loading\u2026' ),
+ isAdded = !! ( this.props.cart && cartItems.hasDomainInCart( this.props.cart, suggestion.domain_name ) ),
+ buttonClasses;
+
+ if ( isAdded ) {
+ buttonClasses = 'added';
+ } else {
+ buttonClasses = 'add is-primary';
+ }
+
+ return (
+
+ { domainName }
+
+ );
+ }
+} );
+
+module.exports = DomainRegistrationSuggestion;
diff --git a/client/components/domains/domain-search-results/index.jsx b/client/components/domains/domain-search-results/index.jsx
new file mode 100644
index 00000000000000..88959099b33173
--- /dev/null
+++ b/client/components/domains/domain-search-results/index.jsx
@@ -0,0 +1,157 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ classNames = require( 'classnames' ),
+ page = require( 'page' ),
+ times = require( 'lodash/utility/times' ),
+ contains = require( 'lodash/collection/contains' ),
+ SimpleNotice = require( 'notices/simple-notice' );
+
+/**
+ * Internal dependencies
+ */
+var DomainRegistrationSuggestion = require( 'components/domains/domain-registration-suggestion' ),
+ DomainMappingSuggestion = require( 'components/domains/domain-mapping-suggestion' ),
+ cartItems = require( 'lib/cart-values' ).cartItems,
+ upgradesActions = require( 'lib/upgrades/actions' );
+
+var DomainSearchResults = React.createClass( {
+ isDomainUnavailable: function() {
+ return this.props.lastDomainError &&
+ contains( [ 'not_available', 'not_available_but_mappable' ], this.props.lastDomainError.code );
+ },
+
+ domainAvailability: function() {
+ var availableDomain = this.props.availableDomain,
+ availabilityElementClasses = classNames( {
+ 'domain-search-results__domain-is-available': availableDomain,
+ 'domain-search-results__domain-not-available': ! availableDomain
+ } ),
+ lastDomainSearched = this.props.lastDomainSearched,
+ availabilityElement,
+ domainSuggestionElement,
+ mappingOffer;
+
+ if ( availableDomain ) {
+ // should use real notice component or custom class
+ availabilityElement = (
+
+ {
+ this.translate(
+ '%(domain)s is available!',
+ { args: { domain: lastDomainSearched } }
+ )
+ }
+
+ );
+
+ domainSuggestionElement = (
+
+ );
+ } else if ( this.props.suggestions && this.props.suggestions.length !== 0 && this.isDomainUnavailable() ) {
+ if ( this.props.products.domain_map && this.props.lastDomainError.code === 'not_available_but_mappable' ) {
+ mappingOffer = this.translate( 'Is it yours? {{a}}Map it{{/a}} for %(cost)s.', {
+ args: { cost: this.props.products.domain_map.cost_display },
+ components: { a: }
+ } );
+ }
+
+ const domainUnavailableMessage = this.translate( 'Aww \u2014 %(domain)s is not available.', {
+ args: { domain: lastDomainSearched }
+ } );
+
+ if ( this.props.offerMappingOption ) {
+ availabilityElement = (
+
+ { domainUnavailableMessage} { mappingOffer }
+
+ );
+ }
+ }
+
+ return (
+
+
+ { availabilityElement }
+ { domainSuggestionElement }
+
+
+ );
+ },
+
+ addMappingAndRedirect: function( event ) {
+ event.preventDefault();
+
+ if ( this.props.onAddMapping ) {
+ return this.props.onAddMapping( this.props.lastDomainSearched );
+ }
+
+ upgradesActions.addItem( cartItems.domainMapping( { domain: this.props.lastDomainSearched } ) );
+
+ page( '/checkout/' + this.props.selectedSite.slug );
+ },
+
+ onClickResult: function( suggestion ) {
+ this.props.onClickResult( suggestion );
+ },
+
+ placeholders: function() {
+ return times( this.props.placeholderQuantity, function( n ) {
+ return ;
+ } );
+ },
+
+ suggestions: function() {
+ var suggestionElements,
+ mappingOffer;
+
+ if ( this.props.suggestions.length ) {
+ suggestionElements = this.props.suggestions.map( function( suggestion ) {
+ return (
+
+ );
+ }, this );
+
+ if ( this.props.offerMappingOption ) {
+ mappingOffer = ;
+ }
+ } else {
+ suggestionElements = this.placeholders();
+ }
+
+ return (
+
+ { suggestionElements }
+ { mappingOffer }
+
+ );
+ },
+
+ render: function() {
+ return (
+
+ { this.domainAvailability() }
+ { this.suggestions() }
+
+ );
+ }
+} );
+
+module.exports = DomainSearchResults;
diff --git a/client/components/domains/domain-search-results/style.scss b/client/components/domains/domain-search-results/style.scss
new file mode 100644
index 00000000000000..3fef00ab48b69a
--- /dev/null
+++ b/client/components/domains/domain-search-results/style.scss
@@ -0,0 +1,24 @@
+.domain-search-results__domain-availability {
+ .notice.is-success {
+ margin: 0;
+ }
+
+ .domain-suggestion.card {
+ border: solid 2px $alert-green;
+ box-shadow: none;
+ opacity: 1;
+ position: static;
+ top: auto;
+ margin-bottom: 20px;
+
+ @include breakpoint( ">660px" ) {
+ margin-bottom: 30px;
+ }
+ }
+}
+
+.map-domain-step {
+ .domain-search-results__domain-availability {
+ margin-top: 30px;
+ }
+}
diff --git a/client/components/domains/domain-suggestion/index.jsx b/client/components/domains/domain-suggestion/index.jsx
new file mode 100644
index 00000000000000..5666ae919bc031
--- /dev/null
+++ b/client/components/domains/domain-suggestion/index.jsx
@@ -0,0 +1,68 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ classNames = require( 'classnames' );
+
+/**
+ * Internal dependencies
+ */
+var DomainProductPrice = require( 'components/domains/domain-product-price' ),
+ Gridicon = require( 'components/gridicon' );
+
+var DomainSuggestion = React.createClass( {
+
+ propTypes: {
+ buttonLabel: React.PropTypes.string,
+ buttonClasses: React.PropTypes.string,
+ extraClasses: React.PropTypes.string,
+ onButtonClick: React.PropTypes.func,
+ price: React.PropTypes.string,
+ cart: React.PropTypes.object,
+ isAdded: React.PropTypes.bool.isRequired
+ },
+
+ formatPrice: function( price ) {
+ // remove trailing zeroes from the price
+ price = price ? price.replace( /\.00$/, '' ) : price;
+ return price;
+ },
+
+ renderButton: function() {
+ var buttonContent;
+ if ( this.props.isAdded ) {
+ buttonContent = ;
+ } else {
+ buttonContent = this.props.buttonLabel;
+ }
+ return (
+
+ { buttonContent }
+
+ );
+ },
+
+ render: function() {
+ var classes = classNames( 'domain-suggestion', 'card', 'is-compact', {
+ 'is-placeholder': this.props.isLoading,
+ 'is-added': this.props.isAdded
+ }, this.props.extraClasses );
+
+ return (
+
+
+ { this.props.children }
+
+
+
+ { this.renderButton() }
+
+
+ );
+ }
+} );
+
+module.exports = DomainSuggestion;
diff --git a/client/components/domains/domain-suggestion/style.scss b/client/components/domains/domain-suggestion/style.scss
new file mode 100644
index 00000000000000..9d2a97d4a72b20
--- /dev/null
+++ b/client/components/domains/domain-suggestion/style.scss
@@ -0,0 +1,90 @@
+.domain-suggestion {
+ box-sizing: border-box;
+ display: flex;
+
+ @include clear-fix;
+
+ @include breakpoint( ">660px" ) {
+ padding: 15px 20px;
+
+ .domain-product-price {
+ margin-left: 5%;
+ }
+ }
+
+ &.is-added {
+ background-color: lighten( $gray, 35% );
+
+ .domain-suggestion__content {
+ h3, .domain-product-price {
+ color: $gray;
+ }
+ }
+ }
+}
+
+.domain-suggestion__content {
+ width: 100%;
+
+ @include breakpoint( ">660px" ) {
+ display: flex;
+ margin: 8px 0 0 0;
+ }
+
+ .is-placeholder & {
+ animation: loading-fade 1.6s ease-in-out infinite;
+ background-color: lighten( $gray, 30% );
+ color: transparent;
+ }
+
+ > h3 {
+ word-break: break-all;
+
+ @include breakpoint( ">660px" ) {
+ width: 75%;
+ }
+
+ .is-placeholder & {
+ color: transparent;
+ }
+ }
+}
+
+.domain-suggestion__action {
+ margin: 1px 0 0 5%;
+ min-width: 100px;
+
+ @include breakpoint( ">660px" ) {
+ margin-top: 3px;
+ }
+
+ @include breakpoint( ">960px" ) {
+ margin-top: 0;
+ }
+
+ .is-placeholder & {
+ animation: loading-fade 1.6s ease-in-out infinite;
+ background-color: lighten( $gray, 30% );
+ border: none;
+ border-radius: 0;
+ color: transparent;
+
+ .button {
+ opacity: 0;
+ pointer-events: none;
+ }
+ }
+
+ .button {
+ width: 100%;
+
+ &.added {
+ background-color: $gray-light;
+
+ .noticon {
+ font-size: 25px;
+ line-height: 16px;
+ }
+ }
+ }
+}
diff --git a/client/components/domains/example-domain-suggestions/index.jsx b/client/components/domains/example-domain-suggestions/index.jsx
new file mode 100644
index 00000000000000..a5aae33ab80ef0
--- /dev/null
+++ b/client/components/domains/example-domain-suggestions/index.jsx
@@ -0,0 +1,123 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+import isEmpty from 'lodash/lang/isEmpty';
+import classNames from 'classnames';
+
+/**
+ * Internal dependencies
+ */
+import Card from 'components/card';
+import CompactCard from 'components/card/compact';
+import analytics from 'analytics';
+import wpcom from 'lib/wp';
+import { abtest } from 'lib/abtest';
+
+module.exports = React.createClass( {
+ displayName: 'ExampleDomainSuggestions',
+
+ getInitialState: function() {
+ return {
+ exampleDomainSuggestionsData: [
+ {
+ domain_name: 'example.wordpress.com',
+ cost: 0
+ },
+ {
+ domain_name: 'example.com',
+ }
+ ]
+ };
+ },
+
+ componentWillMount: function() {
+ wpcom.undocumented().exampleDomainSuggestions( function( errors, response ) {
+ if ( response ) {
+ this.setState( {
+ exampleDomainSuggestionsData: response
+ } );
+ }
+ }.bind( this ) );
+ },
+
+ costDisplay: function( cost ) {
+ if ( cost === undefined ) {
+ return this.translate( 'Loading…' );
+ }
+
+ if ( cost === 0 ) {
+ return this.translate( 'Free' );
+ }
+
+ return this.translate( 'Starting at %(cost)s {{small}}/ year{{/small}}', {
+ args: {
+ cost: cost
+ },
+ components: {
+ small:
+ }
+ } );
+ },
+
+ getExampleSuggestions: function() {
+ return this.state.exampleDomainSuggestionsData.map( example => {
+ const classes = classNames( 'example-domain-suggestions__price', {
+ 'is-free': 0 === example.cost
+ } );
+
+ return (
+
+ { example.domain_name }
+ { this.costDisplay( example.cost ) }
+
+ );
+ } );
+ },
+
+ handleClickMappingLink: function() {
+ analytics.tracks.recordEvent( 'calypso_example_domain_suggestions_mapping_link_click' );
+ },
+
+ render: function() {
+ let examples, mappingInformation;
+ const mappingUrl = this.props.path + '/mapping';
+
+ if ( ! isEmpty( this.props.products ) ) {
+ mappingInformation = this.translate(
+ '{{strong}}Already own a domain?{{/strong}} ' +
+ '{{mappingLink}}Map it{{/mappingLink}} for %(mappingCost)s.', {
+ args: {
+ mappingCost: this.props.products.domain_map.cost_display
+ },
+
+ components: {
+ mappingLink: ,
+ strong:
+ }
+ }
+ );
+ } else {
+ mappingInformation = this.translate( 'Loading…' );
+ }
+
+ examples = (
+
+
+
+
+ { this.translate( 'What are my options?' ) }
+
+
+ { this.getExampleSuggestions() }
+
+
+ { mappingInformation }
+
+
+
+ );
+
+ return examples;
+ }
+} );
diff --git a/client/components/domains/example-domain-suggestions/style.scss b/client/components/domains/example-domain-suggestions/style.scss
new file mode 100644
index 00000000000000..f823034778ea0a
--- /dev/null
+++ b/client/components/domains/example-domain-suggestions/style.scss
@@ -0,0 +1,84 @@
+.example-domain-suggestions {
+ background-color: $gray-light;
+ padding: 0;
+}
+
+.example-domain-suggestions__illustration {
+ background: url( '/calypso/images/drake/drake-nomedia.svg' ) center no-repeat;
+ background-size: 175%;
+ float: left;
+ height: 100%;
+ position: absolute;
+ top: 50%;
+ transform: translateY( -50% );
+ width: 25%;
+
+ @include breakpoint( "<660px" ) {
+ display: none;
+ }
+}
+
+.example-domain-suggestions__information {
+ background-color: $white;
+ width: 100%;
+
+ @include breakpoint( "<660px" ) {
+ padding: 5px 0;
+ }
+
+ @include breakpoint( ">660px" ) {
+ float: right;
+ width: 75%;
+ }
+}
+
+.example-domain-suggestions__header {
+ color: darken( $gray, 20% );
+ font-size: 22px;
+ font-weight: 200;
+ margin: 15px 20px;
+}
+
+.example-domain-suggestions__list {
+ list-style: none;
+ margin: 0 20px;
+
+ li {
+ background-color: $gray-light;
+ padding: 15px;
+ margin: 2px 0;
+
+ @include clear-fix;
+
+ @include breakpoint( ">480px" ) {
+ display: flex;
+ }
+ }
+}
+
+.example-domain-suggestions__price {
+ color: $gray;
+ font-size: 14px;
+
+ @include breakpoint( ">660px" ) {
+ font-size: 16px;
+ }
+
+ @include breakpoint( ">480px" ) {
+ text-align: right;
+ width: 100%;
+ }
+
+ &.is-free {
+ text-transform: uppercase;
+ }
+}
+
+.example-domain-suggestions__mapping-information {
+ color: darken( $gray, 20% );
+ margin: 15px 20px;
+
+ a {
+ text-decoration: underline;
+ }
+}
diff --git a/client/components/domains/map-domain-step/index.jsx b/client/components/domains/map-domain-step/index.jsx
new file mode 100644
index 00000000000000..72c9563bd9e507
--- /dev/null
+++ b/client/components/domains/map-domain-step/index.jsx
@@ -0,0 +1,238 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ page = require( 'page' );
+
+/**
+ * Internal dependencies
+ */
+var cartItems = require( 'lib/cart-values' ).cartItems,
+ Notice = require( 'notices/notice' ),
+ { getFixedDomainSearch, canMap, canRegister } = require( 'lib/domains' ),
+ DomainRegistrationSuggestion = require( 'components/domains/domain-registration-suggestion' ),
+ DomainProductPrice = require( 'components/domains/domain-product-price' ),
+ analyticsMixin = require( 'lib/mixins/analytics' ),
+ upgradesActions = require( 'lib/upgrades/actions' );
+
+var MapDomainStep = React.createClass( {
+ mixins: [ analyticsMixin( 'mapDomain' ) ],
+
+ propTypes: {
+ products: React.PropTypes.object.isRequired,
+ analyticsSection: React.PropTypes.string.isRequired
+ },
+
+ getInitialState: function() {
+ return { searchQuery: '' };
+ },
+
+ componentWillMount: function() {
+ if ( this.props.initialState ) {
+ this.setState( this.props.initialState );
+ }
+ },
+
+ componentWillUnmount: function() {
+ this.save();
+ },
+
+ notice: function() {
+ if ( this.state.notice ) {
+ return ;
+ }
+ },
+
+ save: function() {
+ if ( this.props.onSave ) {
+ this.props.onSave( this.state );
+ }
+ },
+
+ render: function() {
+ var price = this.props.products.domain_map ? this.props.products.domain_map.cost_display : null;
+
+ return (
+
+ );
+ },
+
+ domainRegistrationUpsell: function() {
+ var suggestion = this.state.suggestion;
+ if ( ! suggestion ) {
+ return;
+ }
+
+ return (
+
+
+ {
+ this.translate(
+ '%(domain)s is available!',
+ { args: { domain: suggestion.domain_name } }
+ )
+ }
+
+
+
+ );
+ },
+
+ registerSuggestedDomain: function( event ) {
+ event.preventDefault();
+
+ this.recordEvent( 'addDomainButtonClick', this.state.suggestion.domain_name, this.props.analyticsSection );
+
+ if ( this.props.onAddDomain ) {
+ return this.props.onAddDomain( this.state.suggestion );
+ }
+
+ upgradesActions.registerDomain( this.state.suggestion );
+ },
+
+ recordInputFocus: function() {
+ this.recordEvent( 'inputFocus', this.state.searchQuery );
+ },
+
+ recordGoButtonClick: function() {
+ this.recordEvent( 'goButtonClick', this.state.searchQuery, this.props.analyticsSection );
+ },
+
+ setSearchQuery: function( event ) {
+ this.setState( { searchQuery: event.target.value } );
+ },
+
+ handleFormSubmit: function( event ) {
+ var domain = getFixedDomainSearch( this.state.searchQuery );
+
+ event.preventDefault();
+ this.recordEvent( 'formSubmit', this.state.searchQuery );
+ this.setState( { suggestion: null, notice: null } );
+
+ canMap( domain, function( error ) {
+ if ( error ) {
+ this.handleValidationErrorMessage( domain, error );
+ return;
+ }
+
+ if ( this.props.onAddMapping ) {
+ return this.props.onAddMapping( domain, this.state );
+ }
+
+ this.addMappingToCart( domain );
+ }.bind( this ) );
+
+ canRegister( domain, function( error, result ) {
+ if ( error ) {
+ return;
+ }
+
+ result.domain_name = domain;
+ this.setState( { suggestion: result } );
+ }.bind( this ) );
+ },
+
+ addMappingToCart: function( domain ) {
+ upgradesActions.addItem( cartItems.domainMapping( { domain: domain } ) );
+
+ if ( this.isMounted() ) {
+ page( '/checkout/' + this.props.selectedSite.slug );
+ }
+ },
+
+ handleValidationErrorMessage: function( domain, error ) {
+ let message;
+
+ switch ( error.code ) {
+ case 'not_mappable':
+ message = this.translate( 'Sorry but %(domain)s has not been registered yet therefore cannot be mapped.', {
+ args: { domain: domain }
+ } );
+ break;
+
+ case 'invalid_domain':
+ message = this.translate( 'Sorry but %(domain)s does not appear to be a valid domain name.', {
+ args: { domain: domain }
+ } );
+ break;
+
+ case 'mapped_domain':
+ message = this.translate( 'Sorry but %(domain)s is already mapped to a WordPress.com blog.', {
+ args: { domain: domain }
+ } );
+ break;
+
+ case 'restricted_domain':
+ message = this.translate( 'Sorry but WordPress.com domains cannot be mapped to a WordPress.com blog.' );
+
+ case 'blacklisted_domain':
+ message = this.translate( 'Sorry but %(domain)s cannot be mapped to a WordPress.com blog.', {
+ args: { domain: domain }
+ } );
+ break;
+
+ case 'forbidden_domain':
+ message = this.translate( 'Sorry but you do not have the correct permissions to map %(domain)s.', {
+ args: { domain: domain }
+ } );
+ break;
+
+ case 'invalid_tld':
+ message = this.translate( 'Sorry but %(domain)s does not end with a valid domain extension.', {
+ args: { domain: domain }
+ } );
+ break;
+
+ case 'empty_query':
+ message = this.translate( 'Please enter a domain name or keyword.' );
+ break;
+
+ default:
+ throw new Error( 'Unrecognized error code: ' + error.code );
+ }
+
+ if ( message ) {
+ this.setState( { notice: message } );
+ }
+ }
+} );
+
+module.exports = MapDomainStep;
diff --git a/client/components/domains/map-domain-step/style.scss b/client/components/domains/map-domain-step/style.scss
new file mode 100644
index 00000000000000..c58c844893faff
--- /dev/null
+++ b/client/components/domains/map-domain-step/style.scss
@@ -0,0 +1,56 @@
+.map-domain-step {
+ padding: 0;
+
+ fieldset {
+ clear: left;
+ }
+
+ form.map-domain-step__form {
+ padding: 20px;
+ margin-bottom: 9px;
+ }
+
+ p {
+ color: $gray-dark;
+ font-size: 13px;
+ font-weight: 600;
+ margin-bottom: 0;
+ opacity: 0.7;
+ }
+
+ .domain-product-price {
+ @include breakpoint( ">660px" ) {
+ float: right;
+ margin-top: -5px;
+ }
+ }
+
+ .notice {
+ margin-top: 25px;
+ }
+}
+
+.map-domain-step__domain-description {
+ @include breakpoint( ">660px" ) {
+ float: left;
+ margin-bottom: 20px;
+ }
+}
+
+input.map-domain-step__external-domain {
+ @include breakpoint( ">660px" ) {
+ float: left;
+ width: calc( 100% - 90px );
+ }
+}
+
+.map-domain-step__go {
+ margin: 10px 0 0 0;
+ width: 100%;
+
+ @include breakpoint( ">660px" ) {
+ float: right;
+ margin: 0;
+ width: 80px;
+ }
+}
diff --git a/client/components/domains/map-domain/index.jsx b/client/components/domains/map-domain/index.jsx
new file mode 100644
index 00000000000000..b296e07aa4edad
--- /dev/null
+++ b/client/components/domains/map-domain/index.jsx
@@ -0,0 +1,89 @@
+/**
+ * External dependencies
+ */
+var page = require( 'page' ),
+ React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var HeaderCake = require( 'components/header-cake' ),
+ MapDomainStep = require( 'components/domains/map-domain-step' ),
+ observe = require( 'lib/mixins/data-observe' );
+
+var MapDomain = React.createClass( {
+ mixins: [ observe( 'productsList', 'sites' ) ],
+
+ propTypes: {
+ analyticsSection: React.PropTypes.string,
+ productsList: React.PropTypes.object.isRequired
+ },
+
+ getDefaultProps: function() {
+ return { analyticsSection: 'domains' };
+ },
+
+ componentWillMount: function() {
+ this.checkSiteIsUpgradeable();
+ },
+
+ componentDidMount: function() {
+ if ( this.props.sites ) {
+ this.props.sites.on( 'change', this.checkSiteIsUpgradeable );
+ }
+ },
+
+ componentWillUnmount: function() {
+ if ( this.props.sites ) {
+ this.props.sites.off( 'change', this.checkSiteIsUpgradeable );
+ }
+ },
+
+ checkSiteIsUpgradeable: function( ) {
+ if ( ! this.props.sites ) {
+ return;
+ }
+
+ const selectedSite = this.props.sites.getSelectedSite();
+
+ if ( selectedSite && ! selectedSite.isUpgradeable() ) {
+ page.redirect( '/domains/add/mapping' );
+ }
+ },
+
+ goBack: function() {
+ if ( ! this.props.sites ) {
+ return page( this.props.path.replace( '/mapping', '' ) );
+ }
+
+ page( '/domains/add/' + this.props.sites.getSelectedSite().slug );
+ },
+
+ render: function() {
+ let selectedSite;
+
+ if ( this.props.sites ) {
+ selectedSite = this.props.sites.getSelectedSite();
+ }
+
+ return (
+
+
+ { this.translate( 'Map a Domain' ) }
+
+
+
+
+ );
+ },
+} );
+
+module.exports = MapDomain;
diff --git a/client/components/domains/register-domain-step/index.jsx b/client/components/domains/register-domain-step/index.jsx
new file mode 100644
index 00000000000000..6c406b21183753
--- /dev/null
+++ b/client/components/domains/register-domain-step/index.jsx
@@ -0,0 +1,438 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ extend = require( 'lodash/object/extend' ),
+ async = require( 'async' ),
+ flatten = require( 'lodash/array/flatten' ),
+ reject = require( 'lodash/collection/reject' ),
+ find = require( 'lodash/collection/find' ),
+ uniq = require( 'lodash/array/uniq' ),
+ times = require( 'lodash/utility/times' ),
+ compact = require( 'lodash/array/compact' ),
+ noop = require( 'lodash/utility/noop' ),
+ page = require( 'page' );
+
+/**
+ * Internal dependencies
+ */
+var wpcom = require( 'lib/wp' ).undocumented(),
+ Notice = require( 'notices/notice' ),
+ { getFixedDomainSearch, canRegister } = require( 'lib/domains' ),
+ SearchCard = require( 'components/search-card' ),
+ DomainRegistrationSuggestion = require( 'components/domains/domain-registration-suggestion' ),
+ DomainMappingSuggestion = require( 'components/domains/domain-mapping-suggestion' ),
+ DomainSearchResults = require( 'components/domains/domain-search-results' ),
+ ExampleDomainSuggestions = require( 'components/domains/example-domain-suggestions' ),
+ analyticsMixin = require( 'lib/mixins/analytics' ),
+ upgradesActions = require( 'lib/upgrades/actions' ),
+ cartItems = require( 'lib/cart-values/cart-items' ),
+ abtest = require( 'lib/abtest' ).abtest;
+// max amount of domain suggestions we should fetch/display
+var SUGGESTION_QUANTITY = 4,
+ INITIAL_SUGGESTION_QUANTITY = 2;
+
+var RegisterDomainStep = React.createClass( {
+ mixins: [ analyticsMixin( 'registerDomain' ) ],
+
+ propTypes: {
+ cart: React.PropTypes.object,
+ onDomainsAvailabilityChange: React.PropTypes.func,
+ products: React.PropTypes.object.isRequired,
+ selectedSite: React.PropTypes.oneOfType( [ React.PropTypes.object, React.PropTypes.bool ] ),
+ basePath: React.PropTypes.string.isRequired
+ },
+
+ getDefaultProps: function() {
+ return {
+ onDomainsAvailabilityChange: noop,
+ analyticsSection: 'domains'
+ };
+ },
+
+ getInitialState: function() {
+ return {
+ clickedExampleSuggestion: false,
+ lastQuery: null,
+ searchResults: null,
+ defaultSuggestions: null,
+ lastDomainSearched: null,
+ lastDomainError: null,
+ loadingResults: false,
+ notice: null
+ };
+ },
+
+ componentWillMount: function() {
+ if ( this.props.selectedSite ) {
+ this.fetchDefaultSuggestions();
+ }
+
+ if ( this.props.initialState ) {
+ this.setState( this.props.initialState );
+ }
+ },
+
+ componentDidMount: function() {
+ if ( this.state.lastQuery ) {
+ this.onSearch( this.state.lastQuery );
+ }
+ },
+
+ componentDidUpdate: function( prevProps ) {
+ if ( this.props.selectedSite && this.props.selectedSite.domain !== prevProps.selectedSite.domain ) {
+ this.setState( this.getInitialState() );
+ this.focusSearchCard();
+ this.fetchDefaultSuggestions();
+ }
+ },
+
+ focusSearchCard: function() {
+ this.refs.searchCard.focus();
+ },
+
+ isLoadingSuggestions: function() {
+ return this.state.defaultSuggestions === null;
+ },
+
+ fetchDefaultSuggestions: function() {
+ var initialQuery;
+
+ if ( ! this.props.selectedSite || ! this.props.selectedSite.domain ) {
+ return;
+ }
+
+ initialQuery = this.props.selectedSite.domain.split( '.' )[ 0 ];
+
+ wpcom.fetchDomainSuggestions( initialQuery, { quantity: SUGGESTION_QUANTITY }, function( error, suggestions ) {
+ if ( ! this.isMounted() ) {
+ return;
+ }
+ if ( error && error.statusCode === 503 ) {
+ return this.props.onDomainsAvailabilityChange( false );
+ } else if ( error ) {
+ throw error;
+ }
+
+ this.props.onDomainsAvailabilityChange( true );
+
+ suggestions = suggestions.map( function( suggestion ) {
+ return extend( suggestion, { isVisible: true } );
+ } );
+
+ this.setState( { defaultSuggestions: suggestions } );
+ }.bind( this ) );
+ },
+
+ render: function() {
+ return (
+
+ { this.searchForm() }
+ { this.notices() }
+ { this.content() }
+
+ );
+ },
+
+ notices: function() {
+ if ( this.state.notice ) {
+ return ;
+ }
+ },
+
+ handleClickExampleSuggestion: function() {
+ this.focusSearchCard();
+
+ this.setState( { clickedExampleSuggestion: true } );
+ },
+
+ content: function() {
+ if ( Array.isArray( this.state.searchResults ) || this.state.loadingResults ) {
+ return this.allSearchResults();
+ }
+
+ if ( this.props.showExampleSuggestions ) {
+ return ;
+ }
+
+ return this.initialSuggestions();
+ },
+
+ searchForm: function() {
+ return (
+
+
+
+ );
+ },
+
+ save: function() {
+ if ( this.props.onSave ) {
+ this.props.onSave( this.state );
+ }
+ },
+
+ onSearchChange: function( searchQuery ) {
+ this.setState( {
+ lastQuery: searchQuery,
+ lastDomainSearched: null,
+ loadingResults: Boolean( getFixedDomainSearch( searchQuery ) ),
+ notice: null,
+ searchResults: null
+ } );
+ },
+
+ onSearch: function( searchQuery ) {
+ var suggestions = [],
+ domain = getFixedDomainSearch( searchQuery );
+
+ this.setState( { lastQuery: searchQuery }, this.save );
+
+ if ( ! domain || ! this.state.loadingResults ) {
+ // the search was cleared or the domain contained only spaces
+ return;
+ }
+
+ this.recordEvent( 'searchFormSubmit', searchQuery, this.props.analyticsSection );
+
+ this.setState( {
+ lastDomainSearched: domain,
+ searchResults: [],
+ lastDomainError: null
+ } );
+
+ async.parallel(
+ [
+ callback => {
+ if ( domain.indexOf( '.' ) < 0 ) {
+ return callback();
+ }
+
+ canRegister( domain, ( error, result ) => {
+ if ( error && error.code === 'domain_registration_unavailable' ) {
+ return this.props.onDomainsAvailabilityChange( false );
+ } else if ( error ) {
+ this.showValidationErrorMessage( domain, error );
+ this.setState( { lastDomainError: error } );
+ } else if ( result ) {
+ result.domain_name = domain;
+ }
+
+ if ( ( error && ( error.code === 'not_available' || error.code === 'not_available_but_mappable' ) ) ||
+ ! error ) {
+ this.setState( { notice: null } );
+ }
+
+ this.props.onDomainsAvailabilityChange( true );
+
+ callback( null, result );
+ } );
+ },
+ callback => {
+ const params = {
+ quantity: SUGGESTION_QUANTITY,
+ includeWordPressDotCom: this.props.includeWordPressDotCom
+ };
+
+ wpcom.fetchDomainSuggestions( domain, params, ( error, domainSuggestions ) => {
+ if ( error && error.statusCode === 503 ) {
+ return this.props.onDomainsAvailabilityChange( false );
+ } else if ( error && error.error ) {
+ error.code = error.error;
+ this.showValidationErrorMessage( domain, error );
+ }
+
+ this.props.onDomainsAvailabilityChange( true );
+
+ callback( error, domainSuggestions );
+ } );
+ }
+ ], ( error, result ) => {
+ if ( ! this.state.loadingResults || domain !== this.state.lastDomainSearched ) {
+ // this callback is irrelevant now, a newer search has been made or the results were cleared
+ return;
+ }
+
+ suggestions = uniq( flatten( compact( result ) ), function( suggestion ) {
+ return suggestion.domain_name;
+ } );
+
+ this.setState( {
+ searchResults: suggestions,
+ loadingResults: false
+ }, this.save );
+ }
+ );
+ },
+
+ initialSuggestions: function() {
+ var domainRegistrationSuggestions,
+ domainMappingSuggestion,
+ suggestions;
+
+ if ( this.isLoadingSuggestions() ) {
+ domainRegistrationSuggestions = times( INITIAL_SUGGESTION_QUANTITY + 1, function( n ) {
+ return ;
+ } );
+ } else {
+ // only display two suggestions initially
+ suggestions = this.state.defaultSuggestions.slice( 0, INITIAL_SUGGESTION_QUANTITY );
+
+ domainRegistrationSuggestions = suggestions.map( function( suggestion ) {
+ return (
+
+ );
+ }, this );
+
+ domainMappingSuggestion = (
+
+ );
+ }
+
+ return (
+
+ { domainRegistrationSuggestions }
+ { domainMappingSuggestion }
+
+ );
+ },
+
+ allSearchResults: function() {
+ var lastDomainSearched = this.state.lastDomainSearched,
+ isSearchedDomain = function( suggestion ) {
+ return suggestion.domain_name === lastDomainSearched;
+ },
+ suggestions = reject( this.state.searchResults, isSearchedDomain ),
+ availableDomain = find( this.state.searchResults, isSearchedDomain ),
+ onAddMapping = this.props.onAddMapping ?
+ domain => {
+ return this.props.onAddMapping( domain, this.state );
+ } :
+ undefined;
+
+ if ( suggestions.length === 0 && ! this.state.loadingResults ) {
+ // the search returned no results
+
+ if ( this.props.showExampleSuggestions ) {
+ return ;
+ }
+
+ suggestions = this.state.defaultSuggestions;
+ }
+
+ return (
+
+ );
+ },
+
+ goToMapDomainStep: function( event ) {
+ event.preventDefault();
+
+ this.recordEvent( 'mapDomainButtonClick', this.props.analyticsSection );
+
+ let mapDomainPath = this.props.selectedSite ?
+ this.props.basePath + '/mapping/' + this.props.selectedSite.slug :
+ this.props.basePath + '/mapping';
+
+ page( mapDomainPath );
+ },
+
+ addRemoveDomainToCart: function( suggestion, event ) {
+ event.preventDefault();
+
+ this.recordEvent( 'addDomainButtonClick', suggestion.domain_name, this.props.analyticsSection );
+
+ if ( this.props.onAddDomain ) {
+ return this.props.onAddDomain( suggestion, this.state );
+ }
+
+ if ( ! cartItems.hasDomainInCart( this.props.cart, suggestion.domain_name ) ) {
+ upgradesActions.addDomainToCart( suggestion );
+
+ if ( abtest( 'multiDomainRegistrationV1' ) === 'popupCart' ) {
+ upgradesActions.openCartPopup( { showKeepSearching: true } );
+ } else { // keep searching in gapps or singlePurchaseFlow
+ upgradesActions.goToDomainCheckout( suggestion );
+ }
+ } else {
+ this.recordEvent( 'removeDomainButtonClick', suggestion.domain_name );
+ upgradesActions.removeDomainFromCart( suggestion );
+ }
+ },
+
+ showValidationErrorMessage: function( domain, error ) {
+ var message;
+
+ switch ( error.code ) {
+ case 'not_registrable':
+ if ( domain.indexOf( '.' ) ) {
+ message = this.translate( 'Sorry but %(domain)s cannot be registered on WordPress.com.', {
+ args: { domain: domain }
+ } );
+ }
+ break;
+ case 'not_available':
+ case 'not_available_but_mappable':
+ // unavailable domains are displayed in the search results, not as a notice
+ break;
+
+ case 'empty_query':
+ message = this.translate( 'Please enter a domain name or keyword.' );
+ break;
+
+ case 'invalid_query':
+ message = this.translate( 'Sorry but %(domain)s does not appear to be a valid domain name.', {
+ args: { domain: domain }
+ } );
+ break;
+
+ case 'server_error':
+ message = this.translate( 'Sorry but there was a problem processing your request. Please try again in a few minutes.' );
+ break;
+
+ default:
+ throw new Error( 'Unrecognized error code: ' + error.code );
+ }
+
+ if ( message ) {
+ this.setState( { notice: message } );
+ }
+ }
+} );
+
+module.exports = RegisterDomainStep;
diff --git a/client/components/domains/register-domain-step/style.scss b/client/components/domains/register-domain-step/style.scss
new file mode 100644
index 00000000000000..87bb87c1fde918
--- /dev/null
+++ b/client/components/domains/register-domain-step/style.scss
@@ -0,0 +1,41 @@
+.register-domain-step__search {
+ padding-bottom: 20px;
+
+ .search {
+ margin-bottom: 0;
+
+ &.is-refocused {
+ animation: shake .5s both;
+ box-shadow: 0 0 0 1px $gray,
+ 0 2px 4px lighten( $gray, 20 );
+ }
+ }
+
+ @include breakpoint( ">660px" ) {
+ padding-bottom: 30px;
+ }
+
+ &.disabled {
+ border-bottom: none; // so that bottom border is not there during google app dialog animation
+ opacity: 0.7;
+ transition: opacity, 0.3s, ease-in-out;
+ }
+
+ .search-card {
+ margin-bottom: 0;
+ }
+}
+
+@keyframes shake {
+ 0%, 100% {
+ transform: translate3d( 0, 0, 0 );
+ }
+
+ 10%, 60% {
+ transform: translate3d( -5px, 0, 0 );
+ }
+
+ 30% {
+ transform: translate3d( 5px, 0, 0 );
+ }
+}
\ No newline at end of file
diff --git a/client/components/drop-zone/Makefile b/client/components/drop-zone/Makefile
new file mode 100644
index 00000000000000..5fb42bb5b03d57
--- /dev/null
+++ b/client/components/drop-zone/Makefile
@@ -0,0 +1,7 @@
+REPORTER ?= spec
+MOCHA ?= ../../../node_modules/.bin/mocha
+
+test:
+ @NODE_ENV=test NODE_PATH=test:../../ $(MOCHA) --compilers jsx:babel/register --reporter $(REPORTER)
+
+.PHONY: test
diff --git a/client/components/drop-zone/README.md b/client/components/drop-zone/README.md
new file mode 100644
index 00000000000000..ef577bccfda931
--- /dev/null
+++ b/client/components/drop-zone/README.md
@@ -0,0 +1,51 @@
+Drop Zone
+=========
+
+Drop Zone is a React component which can be used to illustrate areas on the page upon which a user can drop files.
+
+
+
+## Usage
+
+Render the component in the context of a parent element which is assigned a `relative` position style, or specify the `fullScreen` to occupy the entire page.
+
+```jsx
+var React = require( 'react' ),
+ DropZone = require( 'components/drop-zone' );
+
+module.exports = React.createClass( {
+ displayName: 'MyComponent',
+
+ onFilesDrop: function( files ) {
+ console.log( 'You dropped some files: %s', files.map( function( file ) {
+ return file.name;
+ }.join( ', ' ) );
+ },
+
+ render: function() {
+ return (
+
+
+
+ );
+ }
+} );
+```
+
+## Props
+
+### `onDrop`
+
+A function to be invoked when a drop event occurs within the rendered Drop Zone element. This provides raw access to the drop event.
+
+### `onVerifyValidTransfer`
+
+A function to be invoked when files are dragged over or dropped on the rendered Drop Zone element. Passed a [`DataTransfer` object](https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer), this function should return true if the contents of the transfer are valid for the Drop Zone.
+
+### `onFilesDrop`
+
+A function to be invoked when a user drops a file into the rendered Drop Zone element. This does not handle file uploading, nor does it affect any rendered list of items associated with the droppable area.
+
+### `fullScreen`
+
+Pass true to have the droppable area occupy the entire screen, regardless of whether the component is rendered in the context of a `relative` positioned element. Defaults to `false`.
diff --git a/client/components/drop-zone/docs/example.jsx b/client/components/drop-zone/docs/example.jsx
new file mode 100644
index 00000000000000..17ff51e6f0b2d8
--- /dev/null
+++ b/client/components/drop-zone/docs/example.jsx
@@ -0,0 +1,72 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var Card = require( 'components/card' ),
+ DropZone = require( 'components/drop-zone' );
+
+module.exports = React.createClass( {
+ displayName: 'DropZones',
+
+ mixins: [ React.addons.PureRenderMixin ],
+
+ getInitialState: function() {
+ return {};
+ },
+
+ onFilesDrop: function( files ) {
+ this.setState( {
+ lastDroppedFiles: files
+ } );
+ },
+
+ renderContainerContent: function() {
+ var style = {
+ lineHeight: '100px',
+ textAlign: 'center'
+ }, fileNames;
+
+ if ( this.state.lastDroppedFiles ) {
+ fileNames = this.state.lastDroppedFiles.map( function( file ) {
+ return file.name;
+ } ).join( ', ' );
+ }
+
+ return (
+
+ { fileNames ?
+ this.translate( 'You dropped: %s', { args: fileNames } ) :
+ this.translate( 'This is a droppable area' ) }
+
+ );
+ },
+
+ renderContainer: function() {
+ var style = {
+ position: 'relative',
+ height: '150px'
+ };
+
+ return (
+
+ { this.renderContainerContent() }
+
+
+ );
+ },
+
+ render: function() {
+ return (
+
+
+ { this.renderContainer() }
+
+ );
+ }
+} );
diff --git a/client/components/drop-zone/index.jsx b/client/components/drop-zone/index.jsx
new file mode 100644
index 00000000000000..ec0f6eaa969202
--- /dev/null
+++ b/client/components/drop-zone/index.jsx
@@ -0,0 +1,176 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react/addons' ),
+ classNames = require( 'classnames' ),
+ noop = require( 'lodash/utility/noop' );
+
+/**
+ * Internal dependencies
+ */
+var RootChild = require( 'components/root-child' );
+
+module.exports = React.createClass( {
+ displayName: 'DropZone',
+
+ propTypes: {
+ onDrop: React.PropTypes.func,
+ onVerifyValidTransfer: React.PropTypes.func,
+ onFilesDrop: React.PropTypes.func,
+ fullScreen: React.PropTypes.bool,
+ icon: React.PropTypes.string
+ },
+
+ getInitialState: function() {
+ return {
+ isDraggingOverDocument: false,
+ isDraggingOverElement: false
+ };
+ },
+
+ getDefaultProps: function() {
+ return {
+ onDrop: noop,
+ onVerifyValidTransfer: () => true,
+ onFilesDrop: noop,
+ fullScreen: false,
+ icon: 'dashicons dashicons-admin-media'
+ };
+ },
+
+ componentDidMount: function() {
+ this.dragEnteredCounter = 0;
+
+ window.addEventListener( 'dragover', this.preventDefault );
+ window.addEventListener( 'drop', this.onDrop );
+ window.addEventListener( 'dragenter', this.toggleDraggingOverDocument );
+ window.addEventListener( 'dragleave', this.toggleDraggingOverDocument );
+ },
+
+ componentWillUnmount: function() {
+ delete this.dragEnteredCounter;
+
+ window.removeEventListener( 'dragover', this.preventDefault );
+ window.removeEventListener( 'drop', this.onDrop );
+ window.removeEventListener( 'dragenter', this.toggleDraggingOverDocument );
+ window.removeEventListener( 'dragleave', this.toggleDraggingOverDocument );
+ },
+
+ toggleDraggingOverDocument: function( event ) {
+ var isDraggingOverDocument, detail, isValidDrag;
+
+ switch ( event.type ) {
+ case 'dragenter': this.dragEnteredCounter++; break;
+ case 'dragleave': this.dragEnteredCounter--; break;
+ }
+
+ // In some contexts, it may be necessary to capture and redirect the
+ // drag event (e.g. atop an `iframe`). To accommodate this, you can
+ // create an instance of CustomEvent with the original event specified
+ // as the `detail` property.
+ //
+ // See: https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events
+ detail = window.CustomEvent && event instanceof window.CustomEvent ? event.detail : event;
+
+ isValidDrag = this.props.onVerifyValidTransfer( detail.dataTransfer );
+ isDraggingOverDocument = isValidDrag && 0 !== this.dragEnteredCounter;
+
+ this.setState( {
+ isDraggingOverDocument: isDraggingOverDocument,
+ isDraggingOverElement: isDraggingOverDocument && ( this.props.fullScreen ||
+ this.isWithinZoneBounds( detail.clientX, detail.clientY ) )
+ } );
+
+ if ( window.CustomEvent && event instanceof window.CustomEvent ) {
+ // For redirected CustomEvent instances, immediately decrement the
+ // counter following the state update, since another "real" event
+ // will be triggered on the `window` object immediately following.
+ this.dragEnteredCounter--;
+ }
+ },
+
+ preventDefault: function( event ) {
+ event.preventDefault();
+ },
+
+ isWithinZoneBounds: function( x, y ) {
+ var rect;
+
+ if ( ! this.refs.zone ) {
+ return false;
+ }
+
+ rect = this.refs.zone.getDOMNode().getBoundingClientRect();
+
+ return x >= rect.left && x <= rect.right &&
+ y >= rect.top && y <= rect.bottom;
+ },
+
+ onDrop: function( event ) {
+ // This seemingly useless line has been shown to resolve a Safari issue
+ // where files dragged directly from the dock are not recognized
+ event.dataTransfer && event.dataTransfer.files.length;
+
+ this.setState( {
+ isDraggingOverDocument: false,
+ isDraggingOverElement: false
+ } );
+
+ if ( ! this.props.fullScreen && ! React.findDOMNode( this.refs.zone ).contains( event.target ) ) {
+ return;
+ }
+
+ this.props.onDrop( event );
+
+ if ( ! this.props.onVerifyValidTransfer( event.dataTransfer ) ) {
+ return;
+ }
+
+ if ( event.dataTransfer ) {
+ this.props.onFilesDrop( Array.prototype.slice.call( event.dataTransfer.files ) );
+ }
+
+ event.stopPropagation();
+ event.preventDefault();
+ },
+
+ renderContent: function() {
+ var content;
+
+ if ( this.props.children ) {
+ content = this.props.children;
+ } else {
+ content = React.addons.createFragment( {
+ icon: ,
+ text: (
+
+ { this.translate( 'Drop files to upload' ) }
+
+ )
+ } );
+ }
+
+ return { content }
;
+ },
+
+ render: function() {
+ var classes = classNames( 'drop-zone', {
+ 'is-active': this.state.isDraggingOverDocument || this.state.isDraggingOverElement,
+ 'is-dragging-over-document': this.state.isDraggingOverDocument,
+ 'is-dragging-over-element': this.state.isDraggingOverElement,
+ 'is-full-screen': this.props.fullScreen
+ } ), element;
+
+ element = (
+
+ { this.renderContent() }
+
+ );
+
+ if ( this.props.fullScreen ) {
+ return { element } ;
+ } else {
+ return element;
+ }
+ }
+} );
diff --git a/client/components/drop-zone/style.scss b/client/components/drop-zone/style.scss
new file mode 100644
index 00000000000000..4564c60e5677f6
--- /dev/null
+++ b/client/components/drop-zone/style.scss
@@ -0,0 +1,61 @@
+.drop-zone {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 1000;
+ visibility: hidden;
+ opacity: 0;
+ transition: 0.3s opacity, 0.3s background-color, 0s visibility 0.3s;
+ border: 2px dashed $blue-dark;
+ background-color: rgba( $blue-wordpress, 0.8 );
+
+ &.is-active {
+ opacity: 1;
+ visibility: visible;
+ transition: 0.3s opacity, 0.3s background-color;
+ }
+
+ &.is-dragging-over-element {
+ background-color: rgba( $blue-medium, 0.8 );
+ }
+
+ &.is-full-screen {
+ position: fixed;
+ }
+}
+
+.drop-zone__content {
+ position: absolute;
+ top: 50%;
+ left: 0;
+ right: 0;
+ z-index: 1010;
+ transform: translateY( -50% );
+ width: 100%;
+ font-size: 24px;
+ text-align: center;
+ color: $white;
+ transition: transform 0.3s ease-in-out;
+}
+
+.drop-zone.is-dragging-over-element .drop-zone__content {
+ transform: translateY( -50% ) scale( 1.05 );
+}
+
+.drop-zone__content-icon {
+ display: block;
+ margin-bottom: 6px;
+ width: auto;
+ height: auto;
+ font-size: 60px;
+}
+
+.drop-zone.is-full-screen .drop-zone__content {
+ font-size: 30px;
+}
+
+.drop-zone.is-full-screen .drop-zone__content-icon {
+ font-size: 70px;
+}
diff --git a/client/components/drop-zone/test/index.jsx b/client/components/drop-zone/test/index.jsx
new file mode 100644
index 00000000000000..814a7ed2fcde07
--- /dev/null
+++ b/client/components/drop-zone/test/index.jsx
@@ -0,0 +1,211 @@
+/* eslint-disable vars-on-top */
+require( 'lib/react-test-env-setup' )( '
' );
+
+/**
+ * External dependencies
+ */
+var expect = require( 'chai' ).expect,
+ React = require( 'react/addons' ),
+ TestUtils = React.addons.TestUtils,
+ sinon = require( 'sinon' );
+
+/**
+ * Internal dependencies
+ */
+var DropZone = require( '../' );
+
+describe( 'DropZone', function() {
+ var container, sandbox;
+
+ before( function() {
+ DropZone.type.prototype.__reactAutoBindMap.translate = sinon.stub().returnsArg( 0 );
+ container = document.getElementById( 'container' );
+ } );
+
+ after( function() {
+ delete DropZone.type.prototype.__reactAutoBindMap.translate;
+ } );
+
+ beforeEach( function() {
+ sandbox = sinon.sandbox.create();
+ } );
+
+ afterEach( function() {
+ sandbox.restore();
+ React.unmountComponentAtNode( container );
+ } );
+
+ it( 'should render as a child of its container by default', function() {
+ var tree = React.render( React.createElement( DropZone ), container );
+
+ expect( tree.refs.zone.getDOMNode().parentNode.id ).to.equal( 'container' );
+ } );
+
+ it( 'should accept a fullScreen prop to be rendered at the root', function() {
+ var tree = React.render( React.createElement( DropZone, {
+ fullScreen: true
+ } ), container );
+
+ expect( tree.refs.zone.getDOMNode().parentNode.id ).to.not.equal( 'container' );
+ expect( tree.refs.zone.getDOMNode().parentNode.parentNode ).to.eql( document.body );
+ } );
+
+ it( 'should render default content if none is provided', function() {
+ var tree = React.render( React.createElement( DropZone ), container ),
+ content = TestUtils.findRenderedDOMComponentWithClass( tree, 'drop-zone__content' );
+
+ TestUtils.findRenderedDOMComponentWithClass( tree, 'drop-zone__content-icon' );
+ TestUtils.findRenderedDOMComponentWithClass( tree, 'drop-zone__content-text' );
+ expect( content.getDOMNode().textContent ).to.equal( 'Drop files to upload' );
+ } );
+
+ it( 'should accept children to override the default content', function() {
+ var tree = React.render( React.createElement( DropZone, null, 'Hello World' ), container ),
+ content = TestUtils.findRenderedDOMComponentWithClass( tree, 'drop-zone__content' );
+
+ expect( content.getDOMNode().textContent ).to.equal( 'Hello World' );
+ } );
+
+ it( 'should accept an icon to override the default icon', function() {
+ var tree = React.render( React.createElement( DropZone, {
+ icon: 'hello-world'
+ } ), container ), icon;
+
+ icon = TestUtils.findRenderedDOMComponentWithClass( tree, 'drop-zone__content-icon' );
+
+ expect( icon.getDOMNode().className ).to.contain( 'hello-world' );
+ } );
+
+ it( 'should highlight the drop zone when dragging over the body', function() {
+ var tree = React.render( React.createElement( DropZone ), container ),
+ dragEnterEvent = new window.MouseEvent();
+
+ dragEnterEvent.initMouseEvent( 'dragenter', true, true );
+ window.dispatchEvent( dragEnterEvent );
+
+ expect( tree.state.isDraggingOverDocument ).to.be.ok;
+ expect( tree.state.isDraggingOverElement ).to.not.be.ok;
+ } );
+
+ it( 'should not highlight if onVerifyValidTransfer returns false', function() {
+ var dragEnterEvent = new window.MouseEvent(),
+ tree;
+
+ tree = React.render( React.createElement( DropZone, {
+ onVerifyValidTransfer: function() {
+ return false;
+ }
+ } ), container );
+
+ dragEnterEvent.initMouseEvent( 'dragenter', true, true );
+ window.dispatchEvent( dragEnterEvent );
+
+ expect( tree.state.isDraggingOverDocument ).to.not.be.ok;
+ expect( tree.state.isDraggingOverElement ).to.not.be.ok;
+ } );
+
+ it( 'should further highlight the drop zone when dragging over the element', function() {
+ var tree, dragEnterEvent;
+
+ sandbox.stub( DropZone.type.prototype.__reactAutoBindMap, 'isWithinZoneBounds' ).returns( true );
+
+ tree = React.render( React.createElement( DropZone ), container );
+
+ dragEnterEvent = new window.MouseEvent();
+ dragEnterEvent.initMouseEvent( 'dragenter', true, true );
+ window.dispatchEvent( dragEnterEvent );
+
+ expect( tree.state.isDraggingOverDocument ).to.be.ok;
+ expect( tree.state.isDraggingOverElement ).to.be.ok;
+ } );
+
+ it( 'should further highlight the drop zone when dragging over the body if fullScreen', function() {
+ var tree = React.render( React.createElement( DropZone, {
+ fullScreen: true
+ } ), container ), dragEnterEvent;
+
+ dragEnterEvent = new window.MouseEvent();
+ dragEnterEvent.initMouseEvent( 'dragenter', true, true );
+ window.dispatchEvent( dragEnterEvent );
+
+ expect( tree.state.isDraggingOverDocument ).to.be.ok;
+ expect( tree.state.isDraggingOverElement ).to.be.ok;
+ } );
+
+ it( 'should call onDrop with the raw event data when a drop occurs', function() {
+ var tree, dropEvent,
+ spyDrop = sandbox.spy();
+
+ sandbox.stub( window.HTMLElement.prototype, 'contains' ).returns( true );
+
+ tree = React.render( React.createElement( DropZone, {
+ onDrop: spyDrop
+ } ), container );
+
+ dropEvent = new window.MouseEvent();
+ dropEvent.initMouseEvent( 'drop', true, true );
+ window.dispatchEvent( dropEvent );
+
+ expect( spyDrop.calledOnce ).to.be.ok;
+ expect( spyDrop.getCall( 0 ).args[0] ).to.eql( dropEvent );
+ } );
+
+ it( 'should call onFilesDrop with the files array when a drop occurs', function() {
+ var tree, dropEvent,
+ spyDrop = sandbox.spy();
+
+ sandbox.stub( window.HTMLElement.prototype, 'contains' ).returns( true );
+ tree = React.render( React.createElement( DropZone, {
+ onFilesDrop: spyDrop
+ } ), container );
+
+ dropEvent = new window.MouseEvent();
+ dropEvent.initMouseEvent( 'drop', true, true );
+ dropEvent.dataTransfer = { files: [ 1, 2, 3 ] };
+ window.dispatchEvent( dropEvent );
+
+ expect( spyDrop.calledOnce ).to.be.ok;
+ expect( spyDrop.getCall( 0 ).args[0] ).to.eql( [ 1, 2, 3 ] );
+ } );
+
+ it( 'should not call onFilesDrop if onVerifyValidTransfer returns false', function() {
+ var spyDrop = sandbox.spy(),
+ dropEvent = new window.MouseEvent();
+
+ React.render( React.createElement( DropZone, {
+ onFilesDrop: spyDrop,
+ onVerifyValidTransfer: function() {
+ return false;
+ }
+ } ), container );
+
+ dropEvent.initMouseEvent( 'drop', true, true );
+ dropEvent.dataTransfer = { files: [ 1, 2, 3 ] };
+ window.dispatchEvent( dropEvent );
+
+ expect( spyDrop.called ).to.not.be.ok;
+ } );
+
+ it( 'should allow more than one rendered DropZone on a page', function() {
+ var tree = React.render(
+ React.DOM.div(
+ null,
+ React.createElement( DropZone ),
+ React.createElement( DropZone )
+ ),
+ container
+ ), dragEnterEvent, rendered;
+
+ rendered = TestUtils.scryRenderedComponentsWithType( tree, DropZone );
+
+ dragEnterEvent = new window.MouseEvent();
+ dragEnterEvent.initMouseEvent( 'dragenter', true, true );
+ window.dispatchEvent( dragEnterEvent );
+
+ expect( rendered ).to.have.length.of( 2 );
+ rendered.forEach( function( zone ) {
+ expect( zone.state.isDraggingOverDocument ).to.be.ok;
+ expect( zone.state.isDraggingOverElement ).to.not.be.ok;
+ } );
+ } );
+} );
diff --git a/client/components/email-verification/README.md b/client/components/email-verification/README.md
new file mode 100644
index 00000000000000..24f1aa8283555a
--- /dev/null
+++ b/client/components/email-verification/README.md
@@ -0,0 +1,21 @@
+Email Verification
+==================
+
+`EmailVerificationNotice` displays a notice to when the current user has not verified their account, prompting them to click the link in the verification email and allowing them to resend the email.
+
+
+# Usage
+```js
+import EmailVerificationNotice from 'components/email-verification';
+import userFactory from 'lib/user';
+const user = userFactory();
+
+render() {
+ return (
+
+ );
+}
+```
+
+#### Props
+* `user`: An object containing the user info.
diff --git a/client/components/email-verification/email-verification-notice.jsx b/client/components/email-verification/email-verification-notice.jsx
new file mode 100644
index 00000000000000..a0ecc74a7876af
--- /dev/null
+++ b/client/components/email-verification/email-verification-notice.jsx
@@ -0,0 +1,163 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ page = require( 'page' ),
+ isEmpty = require( 'lodash/lang/isEmpty' );
+
+/**
+ * Internal dependencies
+ */
+var notices = require( 'notices' ),
+ sites = require( 'lib/sites-list' )(),
+ Notice = require( 'notices/notice' ),
+ SimpleNotice = require( 'notices/simple-notice' ),
+ emailVerification = require( 'components/email-verification' );
+
+module.exports = React.createClass( {
+ displayName: 'EmailVerification',
+
+ getInitialState: function() {
+ return {
+ activeNotice: undefined,
+ dismissed: false,
+ emailSent: false,
+ pendingRequest: false,
+ error: null
+ };
+ },
+
+ componentWillMount: function() {
+ emailVerification.on( 'change', this.setActiveNotice );
+ this.props.user.on( 'change', this.setActiveNotice );
+ },
+
+ componentWillUnmount: function() {
+ this.unsubscribeFromStores();
+ },
+
+ unsubscribeFromStores: function() {
+ emailVerification.off( 'change', this.setActiveNotice );
+ this.props.user.off( 'change', this.setActiveNotice );
+ },
+
+ setActiveNotice: function() {
+ var user = this.props.user;
+
+ if ( user.fetching ) {
+ // wait until the user is fetched to set a notice
+ return;
+ }
+
+ if ( emailVerification.showUnverifiedNotice && user.get() && ! user.get().email_verified ) {
+ return this.setState( { activeNotice: 'UNVERIFIED' } );
+ }
+
+ if ( emailVerification.showVerifiedNotice ) {
+ return this.setState( { activeNotice: 'VERIFIED' } );
+ }
+
+ if ( user.get().email_verified ) {
+ this.unsubscribeFromStores();
+ return this.setState( { activeNotice: undefined } );
+ }
+ },
+
+ sendVerificationEmail: function() {
+ if ( this.state.pendingRequest ) {
+ return;
+ }
+
+ this.setState( { pendingRequest: true } );
+
+ this.props.user.sendVerificationEmail( function( error, response ) {
+ this.setState( {
+ emailSent: response && response.success,
+ error: error,
+ pendingRequest: false
+ }, this.showEmailSentSuccessMessage );
+ }.bind( this ) );
+ },
+
+ showEmailSentSuccessMessage: function() {
+ var user, noticeText;
+ if ( this.state.emailSent ) {
+ user = this.props.user.get();
+ noticeText = this.translate(
+ 'We sent another confirmation email to {{email /}}',
+ { components: { email: user.email } }
+ );
+ notices.success( noticeText );
+ }
+ },
+
+ handleChangeEmail: function() {
+ page( '/me/account' );
+ },
+
+ dismissNotice: function() {
+ this.setState( { dismissed: true } );
+ },
+
+ unverifiedNotice: function() {
+ var user = this.props.user.get(),
+ noticeText,
+ noticeStatus;
+
+ if ( this.state.error ) {
+ noticeText = this.state.error.message;
+ noticeStatus = 'is-error';
+ } else {
+ noticeText = (
+
+ { this.translate( 'Please verify your email address' ) }
+
+
+ { this.translate(
+ 'To post and keep using WordPress.com you need to validate your email address. ' +
+ 'Please click the link in the email we sent at %(email)s.',
+ { args: { email: user.email } }
+ ) }
+
+
+ { this.translate(
+ '{{requestButton}}Re-send your activation email{{/requestButton}} ' +
+ 'or {{changeButton}}change the email address on your account{{/changeButton}}.', {
+ components: {
+ requestButton: ,
+ changeButton:
+ } }
+ ) }
+
+
);
+ }
+
+ return ;
+ },
+
+ verifiedNotice: function() {
+ var noticeText = isEmpty( sites.get() ) ?
+ this.translate( "You've successfully verified your email address." ) :
+ this.translate( "Email verified! Now that you've confirmed your email address you can publish posts on your blog." );
+
+ return { noticeText } ;
+ },
+
+ render: function() {
+ var user = this.props.user.get();
+
+ if ( ! user || this.state.emailSent || this.state.dismissed || ! this.state.activeNotice ) {
+ return null;
+ }
+
+ if ( 'UNVERIFIED' === this.state.activeNotice && ! user.email_verified ) {
+ return this.unverifiedNotice();
+ }
+
+ if ( 'VERIFIED' === this.state.activeNotice ) {
+ return this.verifiedNotice();
+ }
+
+ return null;
+ }
+} );
diff --git a/client/components/email-verification/index.js b/client/components/email-verification/index.js
new file mode 100644
index 00000000000000..6977ecc80ba040
--- /dev/null
+++ b/client/components/email-verification/index.js
@@ -0,0 +1,27 @@
+/**
+ * External dependencies
+ */
+var startsWith = require( 'lodash/string/startsWith' );
+
+/**
+ * Internal dependencies
+ */
+var emitter = require( 'lib/mixins/emitter' );
+
+var emailVerification = {
+ renderNotice: function( context ) {
+ this.showUnverifiedNotice = ! startsWith( context.path, '/checkout' ) && ! startsWith( context.path, '/plans' );
+ this.showVerifiedNotice = '1' === context.query.verified;
+ this.emit( 'change' );
+ }
+};
+
+/**
+ * Mixins
+ */
+emitter( emailVerification );
+
+/**
+ * Expose `emailVerification` singleton
+ */
+module.exports = emailVerification;
diff --git a/client/components/emojify/README.md b/client/components/emojify/README.md
new file mode 100644
index 00000000000000..e3bce592f6ff04
--- /dev/null
+++ b/client/components/emojify/README.md
@@ -0,0 +1,38 @@
+Emojify
+=========
+
+This module includes functionality for converting strings that could possibly contain UTF emoji codes into a graphical web representation.
+
+This is desirable since implementations are inconsistent or non-existant.
+
+# Usage
+```js
+// require the module
+var Emojify = require( 'components/emojify' ),
+ textToEmojify = 'This will be converted 🙈🙉🙊';
+
+React.render( This text will be unaffected
{ textToEmojify } );
+
+```
+
+CSS
+```css
+// Emoji!!
+.emojified__emoji {
+ height: 18px;
+ width: 18px;
+ vertical-align: middle;
+}
+
+```
+
+* The `` component requires exactly one child and it must be a text node.
+* The `size` property is optional:
+ * It defaults to `'36x36'`
+ * Available options are determined by your CDN
+
+# Requires
+### [punycode](https://github.com/bestiejs/punycode.js/) -- built into node since v0.6.2
+
+# Attributions
+The parsing code was adapted from [this gist](https://gist.github.com/thomasboyt/b5ef9ed8606ce6d93982)
diff --git a/client/components/emojify/index.jsx b/client/components/emojify/index.jsx
new file mode 100644
index 00000000000000..bff0dd11f074bc
--- /dev/null
+++ b/client/components/emojify/index.jsx
@@ -0,0 +1,84 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react/addons' ),
+ PureRenderMixin = React.addons.PureRenderMixin,
+ punycode = require( 'punycode' );
+
+/**
+ * Internal dependencies
+ */
+var baseCDNUrl = '//s0.wp.com/wp-content/mu-plugins/emoji/twemoji/';
+
+module.exports = React.createClass( {
+ mixins: [ PureRenderMixin ],
+ propTypes: {
+ children: React.PropTypes.string.isRequired,
+
+ // Optional. These are dependent on your CDN.
+ size: React.PropTypes.oneOf( [
+ '16x16', '36x36', '72x72'
+ ] )
+ },
+
+ getDefaultProps: function() {
+ return {
+ size: '36x36'
+ };
+ },
+
+ isEmojiCode: function( charCode ) {
+ return (
+ ( charCode >= 0x1F300 && charCode <= 0x1F5FF ) ||
+ ( charCode >= 0x1F600 && charCode <= 0x1F64F ) ||
+ ( charCode >= 0x1F680 && charCode <= 0x1F6FF ) ||
+ ( charCode >= 0x2600 && charCode <= 0x26FF )
+ );
+ },
+
+ emojiTransform: function( text ) {
+ var decoded,
+ entries = [];
+
+ if ( null === text ) {
+ return null;
+ }
+
+ decoded = punycode.ucs2.decode( text );
+
+ decoded.forEach( function( charCode, idx ) {
+ var hexCode,
+ lastIdx,
+ src,
+ char;
+
+ if ( this.isEmojiCode( charCode ) ) {
+
+ // emoji char encountered, insert img tag
+ hexCode = charCode.toString( 16 );
+ src = encodeURI( baseCDNUrl + this.props.size + '/' + hexCode + '.png' );
+
+ entries.push(
+
+ );
+ } else {
+ char = punycode.ucs2.encode( [ charCode ] );
+ lastIdx = entries.length - 1;
+
+ if ( typeof entries[ lastIdx ] === 'string' ) {
+ entries[ lastIdx ] += char;
+ } else {
+ entries.push( char );
+ }
+ }
+ }, this );
+
+ return entries;
+ },
+
+ render: function() {
+ return (
+ { this.emojiTransform( this.props.children ) }
+ );
+ }
+} );
diff --git a/client/components/emojify/style.scss b/client/components/emojify/style.scss
new file mode 100644
index 00000000000000..a6212a75f41878
--- /dev/null
+++ b/client/components/emojify/style.scss
@@ -0,0 +1,10 @@
+/**
+ * Emojify
+ */
+
+// Emoji!!
+.emojified__emoji {
+ height: 18px;
+ width: 18px;
+ vertical-align: middle;
+}
diff --git a/client/components/external-link/README.md b/client/components/external-link/README.md
new file mode 100644
index 00000000000000..f4313419f99b4e
--- /dev/null
+++ b/client/components/external-link/README.md
@@ -0,0 +1,35 @@
+External Link
+=======
+
+External Link is a React component for rendering an external link.
+
+## Usage
+
+```jsx
+
+import React from 'react';
+import ExternalLink from 'components/external-link';
+
+React.createClass( {
+ render() {
+ return WordPress.org ;
+ }
+} );
+```
+
+## Props
+The following props can be passed to the External Link component:
+
+### `icon`
+
+
+ Type Bool
+ Required No
+
+
+Set to true if you want to render a nice external Gridicon at the end of link.
+
+
+## Other Props
+Any other props that you pass into the `a` tag will be rendered as expected.
+For example `onClick` and `href`.
diff --git a/client/components/external-link/docs/example.jsx b/client/components/external-link/docs/example.jsx
new file mode 100644
index 00000000000000..42c91c1b452352
--- /dev/null
+++ b/client/components/external-link/docs/example.jsx
@@ -0,0 +1,30 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+
+/**
+ * Internal dependencies
+ */
+import ExternalLink from 'components/external-link';
+import Card from 'components/card';
+
+export default React.createClass( {
+
+ displayName: 'ExternalLink',
+
+ render() {
+ return (
+
+
+
+ WordPress.org
+ WordPress.org
+
+
+
+ );
+ }
+} );
diff --git a/client/components/external-link/index.jsx b/client/components/external-link/index.jsx
new file mode 100644
index 00000000000000..0a24a60f8d97fa
--- /dev/null
+++ b/client/components/external-link/index.jsx
@@ -0,0 +1,43 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+import classnames from 'classnames';
+import assign from 'lodash/object/assign';
+
+/**
+ * Internal dependencies
+ */
+import Gridicon from 'components/gridicon';
+
+export default React.createClass( {
+
+ displayName: 'ExternalLink',
+
+ mixins: [ React.addons.PureRenderMixin ],
+
+ propTypes: {
+ className: React.PropTypes.string,
+ href: React.PropTypes.string,
+ onClick: React.PropTypes.func,
+ icon: React.PropTypes.bool
+ },
+
+ render() {
+ let children = [];
+ children.push( this.props.children );
+ if ( this.props.icon ) {
+ children.push( );
+ }
+
+ const classes = classnames( 'external-link', this.props.className, {
+ 'has-icon': !! this.props.icon,
+ } );
+
+ const props = assign( {}, this.props, {
+ className: classes,
+ rel: 'external'
+ } );
+ return React.createElement( 'a', props, children );
+ }
+} );
diff --git a/client/components/external-link/style.scss b/client/components/external-link/style.scss
new file mode 100644
index 00000000000000..9e7176ebf10ec7
--- /dev/null
+++ b/client/components/external-link/style.scss
@@ -0,0 +1,6 @@
+.external-link .gridicons-external {
+ color: currentColor;
+ margin-left: 8px;
+ top: 2px;
+ position: relative;
+}
diff --git a/client/components/flag/README.md b/client/components/flag/README.md
new file mode 100644
index 00000000000000..f73e93519008dc
--- /dev/null
+++ b/client/components/flag/README.md
@@ -0,0 +1,19 @@
+Flag
+====
+
+## Usage
+
+```js
+import Flag from 'components/flag';
+
+This is a flag
+
+```
+
+## Required props
+
+* `type` – String that determines which type of flag is displayed. Currently accepts:
+ * is-success
+ * is-warning
+ * is-error
+* `icon` – String for the desired the noticon class (i.e. "noticon-lock").
diff --git a/client/components/flag/docs/example.jsx b/client/components/flag/docs/example.jsx
new file mode 100644
index 00000000000000..7be725d88e1c5d
--- /dev/null
+++ b/client/components/flag/docs/example.jsx
@@ -0,0 +1,38 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+
+/**
+ * Internal dependencies
+ */
+import Flag from '../index';
+
+const FlagExamples = React.createClass( {
+ mixins: [ React.addons.PureRenderMixin ],
+
+ render() {
+ return (
+
+
Flag
+
+
+ Success flag with a lock icon.
+
+
+
+
+ Warning flag with a warning icon.
+
+
+
+
+ Error flag with a warning icon.
+
+
+
+ );
+ }
+} );
+
+module.exports = FlagExamples;
diff --git a/client/components/flag/index.jsx b/client/components/flag/index.jsx
new file mode 100644
index 00000000000000..7c72c2e69907e1
--- /dev/null
+++ b/client/components/flag/index.jsx
@@ -0,0 +1,29 @@
+/**
+ * External dependencies
+ */
+import classNames from 'classnames';
+import omit from 'lodash/object/omit';
+import React from 'react';
+
+const Flag = React.createClass( {
+ propTypes: {
+ icon: React.PropTypes.string.isRequired,
+ type: React.PropTypes.string.isRequired,
+ className: React.PropTypes.string
+ },
+
+ render() {
+ const props = omit( this.props, [ 'icon', 'type', 'className' ] );
+
+ return (
+
+
+ { this.props.children }
+
+ );
+ }
+} );
+
+module.exports = Flag;
diff --git a/client/components/flag/style.scss b/client/components/flag/style.scss
new file mode 100644
index 00000000000000..531f4eb5f5d0cc
--- /dev/null
+++ b/client/components/flag/style.scss
@@ -0,0 +1,31 @@
+.flag {
+ border-radius: 2px;
+ color: $white;
+ display: inline-block;
+ font-size: 10px;
+ padding: 2px 6px 2px 6px;
+ text-decoration: none;
+ text-transform: uppercase;
+
+ .noticon {
+ margin-right: 2px;
+ vertical-align: middle;
+ }
+
+ .noticon-warning {
+ font-size: 12px;
+ margin-right: 5px;
+ }
+
+ &.is-success {
+ background: $alert-green;
+ }
+
+ &.is-warning {
+ background: $alert-yellow;
+ }
+
+ &.is-error {
+ background: $alert-red;
+ }
+}
diff --git a/client/components/foldable-card/README.md b/client/components/foldable-card/README.md
new file mode 100644
index 00000000000000..5c78aafc2b44b0
--- /dev/null
+++ b/client/components/foldable-card/README.md
@@ -0,0 +1,43 @@
+Foldable card
+==============
+
+This component is used to display a box that can be clicked to expand a hidden section with its contents.
+
+#### How to use:
+
+```js
+var FoldableCard = require( 'components/foldable-card' );
+
+render: function() {
+ return (
+
+
+ { content }
+
+
+ );
+}
+```
+
+#### Props
+
+* `header`: a string, HTML or component to show in the default header view of the box
+
+#### Children
+* `content`: a string, HTML or component to show in the expandable section of the box when it's expanded
+
+##### Optional props
+* `actionButton`: a component to substitute the regular expand button
+* `actionButtonExpanded`: a component to substitute the regular expand button when the card is expanded. If not provided, we use `actionButton`
+* `cardKey`: a unique identifier for the card that can be used to help track its state outside the component (for example, to record which cards are open).
+* `compact`: a boolean indicating if the foldable card is compact
+* `disabled`: boolean indicating if the component it's not interactive
+* `expandedSummary`: string or component to show next to the action button when expanded
+* `expanded`: boolean indicating if the component is expanded on initial render
+* `onClick`: function to be executed in addition to the expand action when the header is clicked
+* `onClose`: function to be executed in addition to the expand action when the card is closed
+* `onOpen`: function to be executed in addition to the expand action when the card is opened
+* `summary`: string or component to show next to the action button when closed
+* `clickableHeader`: boolean indicating if the whole header can be clicked to open the card
\ No newline at end of file
diff --git a/client/components/foldable-card/docs/example.jsx b/client/components/foldable-card/docs/example.jsx
new file mode 100644
index 00000000000000..578328480546fd
--- /dev/null
+++ b/client/components/foldable-card/docs/example.jsx
@@ -0,0 +1,58 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var FoldableCard = require( 'components/foldable-card' );
+
+module.exports = React.createClass( {
+ displayName: 'FoldableCard',
+
+ mixins: [ React.addons.PureRenderMixin ],
+
+ render: function() {
+ return (
+
+
+
+
+ These are its contents
+
+
+
+
+
+ I'm tiny! :D
+
+
+
+
+ You can't see me!
+
+
+
+
+ This is the main content of the card.
+
+
+
+ This is a multiline foldable card
with a summary component & a expanded summary component
}
+ summary={ Update }
+ expandedSummary={ Update }>
+ Nothing to see here. Keep walking!
+
+
+
+ );
+ }
+} );
diff --git a/client/components/foldable-card/index.jsx b/client/components/foldable-card/index.jsx
new file mode 100644
index 00000000000000..13eb6590d57d8a
--- /dev/null
+++ b/client/components/foldable-card/index.jsx
@@ -0,0 +1,144 @@
+/**
+ * External Dependencies
+ */
+var React = require( 'react' ),
+ classNames = require( 'classnames' ),
+ noop = require( 'lodash/utility/noop' );
+
+/**
+ * Internal Dependencies
+ */
+var Card = require( 'components/card' ),
+ CompactCard = require( 'components/card/compact' ),
+ Gridicon = require( 'components/gridicon' );
+
+var FoldableCard = React.createClass( {
+
+ propTypes: {
+ actionButton: React.PropTypes.element,
+ actionButtonExpanded: React.PropTypes.element,
+ cardKey: React.PropTypes.string,
+ compact: React.PropTypes.bool,
+ disabled: React.PropTypes.bool,
+ expandedSummary: React.PropTypes.oneOfType( [ React.PropTypes.string, React.PropTypes.element ] ),
+ expanded: React.PropTypes.bool,
+ onClick: React.PropTypes.func,
+ onClose: React.PropTypes.func,
+ onOpen: React.PropTypes.func,
+ summary: React.PropTypes.oneOfType( [ React.PropTypes.string, React.PropTypes.element ] )
+ },
+
+ getInitialState: function() {
+ return {
+ expanded: this.props.expanded
+ };
+ },
+
+ getDefaultProps: function() {
+ return {
+ onOpen: noop,
+ onClose: noop,
+ cardKey: '',
+ isExpanded: false
+ };
+ },
+
+ onClick: function() {
+ if ( this.props.children ) {
+ this.setState( { expanded: ! this.state.expanded } );
+ }
+
+ if ( this.props.onClick ) {
+ this.props.onClick();
+ }
+
+ if ( this.state.expanded ) {
+ this.props.onClose( this.props.cardKey );
+ } else {
+ this.props.onOpen( this.props.cardKey );
+ }
+ },
+
+ getClickAction: function() {
+ if ( this.props.disabled ) {
+ return;
+ }
+ return this.onClick;
+ },
+
+ getActionButton: function() {
+ if ( this.state.expanded ) {
+ return this.props.actionButtonExpanded || this.props.actionButton;
+ }
+ return this.props.actionButton;
+ },
+
+ renderActionButton: function() {
+ const clickAction = ! this.props.clickableHeader ? this.getClickAction() : null;
+ if ( this.props.actionButton ) {
+ return (
+
+ { this.getActionButton() }
+
+ );
+ }
+ if ( this.props.children ) {
+ let iconSize = 24;
+ return (
+
+ { this.translate( 'More' ) }
+
+
+ );
+ }
+ },
+
+ renderContent: function() {
+ return (
+
+ { this.props.children }
+
+ );
+ },
+
+ renderHeader: function() {
+ var summary = this.props.summary ? { this.props.summary } : null,
+ expandedSummary = this.props.expandedSummary ? { this.props.expandedSummary } : null,
+ headerClickAction = this.props.clickableHeader ? this.getClickAction() : null,
+ headerClasses = classNames( 'foldable-card__header', {
+ 'is-clickable': !! this.props.clickableHeader
+ } );
+ return (
+
+ { this.props.header }
+
+ { summary }
+ { expandedSummary }
+ { this.renderActionButton() }
+
+
+ );
+ },
+
+ render: function() {
+ var Container = this.props.compact ? CompactCard : Card,
+ itemSiteClasses = classNames(
+ 'foldable-card',
+ this.props.className,
+ {
+ 'is-disabled': !! this.props.disabled,
+ 'is-expanded': !! this.state.expanded,
+ 'has-expanded-summary': !! this.props.expandedSummary
+ }
+ );
+
+ return (
+
+ { this.renderHeader() }
+ { this.renderContent() }
+
+ );
+ }
+} );
+
+module.exports = FoldableCard;
diff --git a/client/components/foldable-card/style.scss b/client/components/foldable-card/style.scss
new file mode 100644
index 00000000000000..d9e6e47b401c3c
--- /dev/null
+++ b/client/components/foldable-card/style.scss
@@ -0,0 +1,165 @@
+// Multisite
+.foldable-card.card {
+ @include clear-fix;
+ position: relative;
+ transition: margin .15s linear;
+ padding: 0;
+
+ &.is-expanded {
+ margin-bottom: 8px;
+ }
+
+}
+
+.foldable-card__header {
+ min-height: 64px;
+ width: 100%;
+ padding: 16px;
+ box-sizing: border-box;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ position: relative;
+
+ &.is-clickable {
+ cursor: pointer;
+ }
+
+ .foldable-card.is-compact & {
+ padding: 8px 16px;
+ min-height: 40px;
+ }
+
+ .foldable-card.is-expanded & {
+ margin-bottom: 0px;
+ height: inherit;
+ min-height: 64px;
+ }
+
+ .foldable-card.is-expanded.is-compact & {
+ min-height: 40px;
+ }
+
+ .foldable-card.is-disabled & {
+ opacity: 0.2;
+ }
+}
+
+.foldable-card__action {
+ position: absolute;
+ top: 0;
+ right: 0;
+ height: 100%;
+
+ .foldable-card.is-expanded & {
+ height: 100%;
+ }
+
+ .foldable-card.is-disabled & {
+ cursor: default;
+ }
+
+ .accessible-focus &:focus {
+ outline: thin dotted;
+ }
+}
+
+button.foldable-card__action {
+ cursor: pointer;
+}
+
+.foldable-card__main {
+ max-width: calc( 100% - 36px );
+ display: flex;
+ align-items: center;
+ flex: 2 1;
+ margin-right: 5px;
+}
+
+.foldable-card__secondary {
+ display: flex;
+ align-items: center;
+ flex: 1 1;
+ justify-content: flex-end;
+}
+
+.foldable-card__expand {
+ width: 48px;
+
+ .gridicon {
+ fill: $gray;
+ display: flex;
+ align-items: center;
+ width: 100%;
+ vertical-align: middle;
+
+ transition: transform .15s cubic-bezier(0.175, .885, .32, 1.275), color .20s ease-in;
+
+ .foldable-card.is-expanded & {
+ transform: rotate( 180deg );
+ }
+ }
+
+ .gridicon:hover {
+ fill: $gray;
+ }
+
+ &:hover .gridicon {
+ fill: $blue-medium;
+ }
+}
+
+.foldable-card__content {
+ display: none;
+
+ .foldable-card.is-expanded & {
+ display: block;
+ padding: 16px;
+ border-top: 1px solid $gray-light;
+
+
+ .foldable-card.is-compact & {
+ padding: 8px;
+ }
+ }
+}
+
+.foldable-card__summary,
+.foldable-card__summary_expanded {
+ margin-right: 40px;
+ color: $gray;
+ font-size: 12px;
+ transition: opacity 0.2s linear;
+ display: inline-block;
+
+ .foldable-card.has-expanded-summary & {
+ transition: none;
+ flex: 2;
+ text-align: right;
+ }
+
+ @include breakpoint( "<480px" ) {
+ display: none;
+ }
+}
+
+
+.foldable-card__summary {
+ opacity: 1;
+ display: inline-block;
+
+ .foldable-card.is-expanded & {
+ display: none;
+ .has-expanded-summary & {
+ display: none;
+ }
+ }
+}
+
+.foldable-card__summary_expanded {
+ display: none;
+
+ .foldable-card.is-expanded & {
+ display: inline-block;
+ }
+}
diff --git a/client/components/follow-button/README.md b/client/components/follow-button/README.md
new file mode 100644
index 00000000000000..cb0df53575af14
--- /dev/null
+++ b/client/components/follow-button/README.md
@@ -0,0 +1,41 @@
+Follow Button
+=========
+
+This component is used to display a follow/unfollow button.
+It has two parts, the actual button and a container that works with the FeedSubscriptionStore.
+For most uses, the container is the easiest route.
+
+#### How to use the container:
+
+```js
+var FollowButtonContainer = require( 'components/follow-button' );
+
+render: function() {
+ return (
+
+
+
+ );
+}
+```
+
+#### Props
+
+* `siteUrl`: string, a site URL to follow or unfollow
+
+#### How to use the button directly:
+```js
+var FollowButton = require( 'components/follow-button/button' );
+
+render: function() {
+ return (
+
+
+
+ );
+}
+```
+
+#### Props
+
+* `following`: (default: false ) a boolean indicating if the current user is currently following the site URL
\ No newline at end of file
diff --git a/client/components/follow-button/_style.scss b/client/components/follow-button/_style.scss
new file mode 100644
index 00000000000000..98308e3855d2e4
--- /dev/null
+++ b/client/components/follow-button/_style.scss
@@ -0,0 +1,95 @@
+.follow-button {
+ position: relative;
+ background: inherit;
+ border: none;
+ border-radius: 0;
+ color: darken( $gray, 10% );
+ cursor: pointer;
+ display: block;
+ font-size: 14px;
+ font-weight: 400;
+ margin: 0;
+ padding: 8px 16px;
+ text-align: left;
+
+ &:first-child {
+ margin-top: 5px;
+ }
+
+ &:hover,
+ &:focus {
+ border: 0;
+ box-shadow: none;
+ }
+
+ &:last-child {
+ margin-bottom: 5px;
+ }
+
+ &::-moz-focus-inner {
+ border: 0;
+ }
+
+ // Menu Items with Icons
+ &.has-icon {
+ padding-left: 42px;
+ }
+
+ .gridicon__follow {
+ opacity: 1;
+ }
+
+ .gridicon__following {
+ opacity: 0;
+ pointer-events: none;
+ transform: rotate( 180deg ) scale( 3 );
+ }
+
+ .gridicon__unfollow {
+ display: none;
+ }
+
+ &.is-following {
+ color: $alert-green;
+
+ .gridicon {
+ fill: $alert-green;
+ }
+
+ .gridicon__follow {
+ opacity: 0;
+ pointer-events: none;
+ transform: rotate( -180deg ) scale( 0.5 );
+ }
+
+ .gridicon__following {
+ opacity: 1;
+ pointer-events: auto;
+ transform: rotate( 0 ) scale( 1 );
+ }
+ }
+
+ &:hover {
+ color: $alert-green;
+ .gridicon {
+ fill: $alert-green;
+ }
+
+ &.is-following {
+ .gridicon {
+ fill: lighten( $gray, 10 );
+ }
+
+ color: lighten( $gray, 10 );
+
+ }
+ }
+
+ .gridicon {
+ position: absolute;
+ top: 8px;
+ left: 13px;
+ fill: lighten( $gray, 10 );
+ transition: all 0.15s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+ }
+}
diff --git a/client/components/follow-button/button.jsx b/client/components/follow-button/button.jsx
new file mode 100644
index 00000000000000..b4c6830688db19
--- /dev/null
+++ b/client/components/follow-button/button.jsx
@@ -0,0 +1,63 @@
+// External dependencies
+var React = require( 'react' ),
+ noop = require( 'lodash/utility/noop' );
+
+var FollowButton = React.createClass( {
+
+ propTypes: {
+ following: React.PropTypes.bool.isRequired,
+ onFollowToggle: React.PropTypes.func
+ },
+
+ getDefaultProps: function() {
+ return {
+ following: false,
+ onFollowToggle: noop,
+ iconSize: 20,
+ tagName: 'button'
+ };
+ },
+
+ componentWillMount: function() {
+ this.strings = {
+ FOLLOW: this.translate( 'Follow' ),
+ FOLLOWING: this.translate( 'Following' )
+ };
+ },
+
+ toggleFollow: function( event ) {
+ if ( event ) {
+ event.preventDefault();
+ }
+
+ if ( this.props.onFollowToggle ) {
+ this.props.onFollowToggle( ! this.props.following );
+ }
+ },
+
+ render: function() {
+ var menuClasses = [ 'follow-button', 'has-icon' ],
+ label = this.strings.FOLLOW,
+ iconSize = this.props.iconSize;
+
+ if ( this.props.following ) {
+ menuClasses.push( 'is-following' );
+ label = this.strings.FOLLOWING;
+ }
+
+ menuClasses = menuClasses.join( ' ' );
+
+ var followingIcon = ( ),
+ followIcon = ( ),
+ followLabel = ( { label } );
+
+ return React.createElement( this.props.tagName, {
+ onClick: this.toggleFollow,
+ className: menuClasses,
+ title: label
+ }, [ followingIcon, followIcon, followLabel ] );
+ }
+
+} );
+
+module.exports = FollowButton;
diff --git a/client/components/follow-button/docs/example.jsx b/client/components/follow-button/docs/example.jsx
new file mode 100644
index 00000000000000..4f1321571c4005
--- /dev/null
+++ b/client/components/follow-button/docs/example.jsx
@@ -0,0 +1,36 @@
+/**
+* External dependencies
+*/
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var FollowButton = require( 'components/follow-button/button' ),
+ Card = require( 'components/card/compact' );
+
+var FollowButtons = React.createClass( {
+ displayName: 'FollowButtons',
+
+ mixins: [ React.addons.PureRenderMixin ],
+
+ render: function() {
+ return (
+
+
+
+ Default:
+
+
+
+ Following:
+
+
+
+ );
+ }
+} );
+
+module.exports = FollowButtons;
diff --git a/client/components/follow-button/index.jsx b/client/components/follow-button/index.jsx
new file mode 100644
index 00000000000000..3b3e6d04acecf9
--- /dev/null
+++ b/client/components/follow-button/index.jsx
@@ -0,0 +1,66 @@
+/**
+ * External Dependencies
+ */
+var React = require( 'react' ),
+ noop = require( 'lodash/utility/noop' );
+
+/**
+ * Internal Dependencies
+ */
+var FollowButton = require( './button' ),
+ FeedSubscriptionActions = require( 'lib/reader-feed-subscriptions/actions' ),
+ FeedSubscriptionStore = require( 'lib/reader-feed-subscriptions' );
+
+var FollowButtonContainer = React.createClass( {
+ propTypes: {
+ siteUrl: React.PropTypes.string.isRequired,
+ iconSize: React.PropTypes.number,
+ onFollowToggle: React.PropTypes.func
+ },
+
+ getDefaultProps: function() {
+ return {
+ onFollowToggle: noop
+ };
+ },
+
+ getInitialState: function() {
+ return this.getStateFromStores();
+ },
+
+ getStateFromStores: function() {
+ return { following: FeedSubscriptionStore.getIsFollowingBySiteUrl( this.props.siteUrl ) };
+ },
+
+ componentDidMount: function() {
+ FeedSubscriptionStore.on( 'change', this.onStoreChange );
+ },
+
+ componentWillUnmount: function() {
+ FeedSubscriptionStore.off( 'change', this.onStoreChange );
+ },
+
+ onStoreChange: function() {
+ var newState = this.getStateFromStores();
+ if ( newState.following !== this.state.following ) {
+ this.setState( newState );
+ }
+ },
+
+ handleFollowToggle: function( following ) {
+ FeedSubscriptionActions[ following ? 'follow' : 'unfollow' ]( this.props.siteUrl );
+ this.props.onFollowToggle( following );
+ },
+
+ render: function() {
+ return (
+
+ );
+ }
+} );
+
+module.exports = FollowButtonContainer;
diff --git a/client/components/follow-button/style.scss b/client/components/follow-button/style.scss
new file mode 100644
index 00000000000000..56760a2857f322
--- /dev/null
+++ b/client/components/follow-button/style.scss
@@ -0,0 +1,64 @@
+// FollowButton Component
+.follow-button {
+ position: relative;
+ cursor: pointer;
+
+ @include breakpoint( "<480px" ) {
+ padding-left: 16px;
+ }
+
+ .gridicon {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 24px;
+ width: 24px;
+ }
+
+ .gridicon-follow {
+ fill: $gray;
+ opacity: 1;
+ transform: rotate( 0 );
+ transition: all 0.1s ease-in-out;
+ }
+
+ .gridicon-following {
+ fill: $alert-green;
+ opacity: 0;
+ transform: rotate( -90deg );
+ transition: all 0.3s cubic-bezier(0.175, 0.885, 0.320, 1.275);
+ }
+
+ &.is-mini {
+ .follow-button__label {
+ font-size: 13px;
+ }
+ .gridicon {
+ height: 16px;
+ width: 16px;
+ }
+ }
+
+ &.is-followed {
+ .gridicon-follow {
+ opacity: 0;
+ transform: rotate( -30deg );
+ }
+
+ .gridicon-following {
+ opacity: 1;
+ transform: rotate( 0 );
+ }
+ }
+}
+
+.follow-button__label {
+ display: inline-block;
+ padding-left: 30px;
+ color: $gray;
+ transition: all 0.1s linear;
+
+ @include breakpoint( "<480px" ) {
+ display: none;
+ }
+}
diff --git a/client/components/forms/README.md b/client/components/forms/README.md
new file mode 100644
index 00000000000000..04918af16cbceb
--- /dev/null
+++ b/client/components/forms/README.md
@@ -0,0 +1,88 @@
+Form Components
+===============
+
+This is a directory of shared form components.
+
+### Settings Form Fields
+The following form components were created as an effort to minimize duplication between site settings and me settings.
+
+- form-button
+- form-buttons-bar
+- form-checkbox
+- form-fieldset
+- form-label
+- form-legend
+- form-radio
+- form-select
+- form-setting-explanation
+- form-text-input
+- form-textarea
+
+The component jsx files are wrappers that ensure our classes are added to each form field. Each form field component also contains a `style.scss` file in its directory for styling. These stylesheets are included in `/assets/stylesheets/_components.scss`.
+
+### FormSectionHeading
+The `FormSectionHeading` component allows you to add a section header to your settings form.
+
+### FormInputValidation
+The `FormInputValidation` component is used to display a validation notice to the user. You can use it like this:
+
+
+
+
+### MultiCheckbox
+
+[See README.md for MultiCheckbox](multi-checkbox/README.md)
+
+### SelectOptGroups
+`SelectOptGroups` allows you to pass structured data to render a select element with `` elements nested inside ` ` separators. You can use it like this:
+
+```
+var options = [
+ {
+ label: 'Group 1',
+ options: [
+ {
+ label: 'Option 1',
+ value: 1
+ },
+ {
+ label: 'Option 2',
+ value: 2
+ }
+ ]
+ },
+ {
+ label: 'Group 2',
+ options: [
+ {
+ label: 'Option 3',
+ value: 3
+ },
+ {
+ label: 'Option 4',
+ value: 4
+ }
+ ]
+ }
+],
+initialSelected = 3;
+
+
+```
+
+And this would render:
+
+```
+
+
+ Option 1
+ Option 2
+
+
+ Option 3
+ Option 4
+
+
+```
+
+Any valid jsx attributes that are passed to `` will also get passed to the rendered `` element, so you can also pass in attributes like `className`, `onChange`, etc.
diff --git a/client/components/forms/clipboard-button/README.md b/client/components/forms/clipboard-button/README.md
new file mode 100644
index 00000000000000..95e90d4062cfba
--- /dev/null
+++ b/client/components/forms/clipboard-button/README.md
@@ -0,0 +1,45 @@
+Clipboard Button
+================
+
+Clipboard Button is a React component to facilitate creating a "click-to-copy" button. Under the hood, the component uses [Clipboard.js](https://github.com/zenorocha/clipboard.js), which is a more recent library leveraging newer browser features enabling access to the user's clipboard. Browser support is [fairly good](https://github.com/zenorocha/clipboard.js#browser-support), with Safari being the notable exception. In browsers where clipboard access is not granted, the user will be presented with a prompt window after pressing the button, from which they can copy the text via system copy.
+
+## Usage
+
+```jsx
+var React = require( 'react' ),
+ ClipboardButton = require( 'components/forms/clipboard-button' );
+
+React.createClass( {
+ render: function() {
+ return (
+
+ Button Text
+
+ );
+ }
+} );
+```
+
+## Props
+
+Below is a list of supported props. With the exception of `className`, any other props passed will be transferred to the rendered ` ` component.
+
+### `text`
+
+
+ Type String
+ Required No
+ Default undefined
+
+
+The text to copy to the user's clipboard upon clicking the button.
+
+### `onCopy`
+
+
+ Type Function
+ Required No
+ Default lodash/utility/noop
+
+
+Function to invoke after the text has been copied to the user's clipboard. This will not be triggered if the a prompt is shown in place of direct clipboard copy, in cases where the browser does not grant clipboard access.
diff --git a/client/components/forms/clipboard-button/docs/example.jsx b/client/components/forms/clipboard-button/docs/example.jsx
new file mode 100644
index 00000000000000..3ca22960b2c0c8
--- /dev/null
+++ b/client/components/forms/clipboard-button/docs/example.jsx
@@ -0,0 +1,43 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var ClipboardButton = require( '../' );
+
+module.exports = React.createClass( {
+ displayName: 'ClipboardButtons',
+
+ mixins: [ React.addons.PureRenderMixin ],
+
+ getInitialState: function() {
+ return {
+ isCopied: false
+ };
+ },
+
+ onCopy: function() {
+ this.setState( {
+ isCopied: true
+ } );
+ },
+
+ render: function() {
+ return (
+
+
+
+ { this.state.isCopied ? 'Copied!' : 'Copy to clipboard' }
+
+
+ );
+ }
+} );
diff --git a/client/components/forms/clipboard-button/index.jsx b/client/components/forms/clipboard-button/index.jsx
new file mode 100644
index 00000000000000..8181489ed6fcc1
--- /dev/null
+++ b/client/components/forms/clipboard-button/index.jsx
@@ -0,0 +1,58 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ Clipboard = require( 'clipboard' ),
+ omit = require( 'lodash/object/omit' ),
+ noop = require( 'lodash/utility/noop' ),
+ classNames = require( 'classnames' );
+
+/**
+ * Internal dependencies
+ */
+var Button = require( 'components/button' );
+
+module.exports = React.createClass( {
+ displayName: 'ClipboardButton',
+
+ propTypes: {
+ className: React.PropTypes.string,
+ text: React.PropTypes.string,
+ onCopy: React.PropTypes.func
+ },
+
+ getDefaultProps: function() {
+ return {
+ onCopy: noop
+ };
+ },
+
+ componentDidMount: function() {
+ var button = React.findDOMNode( this.refs.button );
+ this.clipboard = new Clipboard( button, {
+ text: () => this.props.text
+ } );
+ this.clipboard.on( 'success', this.props.onCopy );
+ this.clipboard.on( 'error', this.displayPrompt );
+ },
+
+ componentWillUnmount: function() {
+ this.clipboard.destroy();
+ delete this.clipboard;
+ },
+
+ displayPrompt: function() {
+ window.prompt( this.translate( 'Highlight and copy the following text to your clipboard:' ), this.props.text );
+ },
+
+ render: function() {
+ var classes = classNames( 'clipboard-button', this.props.className );
+
+ return (
+
+ );
+ }
+} );
diff --git a/client/components/forms/counted-textarea/Makefile b/client/components/forms/counted-textarea/Makefile
new file mode 100644
index 00000000000000..8692e1e3dc5580
--- /dev/null
+++ b/client/components/forms/counted-textarea/Makefile
@@ -0,0 +1,13 @@
+UI ?= bdd
+REPORTER ?= spec
+COMPILERS ?= jsx:babel/register
+NODE_BIN := $(shell npm bin)
+MOCHA ?= $(NODE_BIN)/mocha
+BASE_DIR := $(NODE_BIN)/../..
+NODE_PATH := test:$(BASE_DIR)/client:$(BASE_DIR)/shared
+
+# In order to simply stub modules, add test to the NODE_PATH
+test:
+ @NODE_ENV=test NODE_PATH=$(NODE_PATH) $(MOCHA) --compilers $(COMPILERS) --reporter $(REPORTER) --ui $(UI)
+
+.PHONY: test
diff --git a/client/components/forms/counted-textarea/README.md b/client/components/forms/counted-textarea/README.md
new file mode 100644
index 00000000000000..d9f0cbb0b76e41
--- /dev/null
+++ b/client/components/forms/counted-textarea/README.md
@@ -0,0 +1,43 @@
+Counted Textarea
+===================
+
+Counted Textarea is a React form component which renders a `` accompanied by a character count display.
+
+
+
+## Usage
+
+` ` expects to be rendered as a [controlled component](https://facebook.github.io/react/docs/forms.html#controlled-components), meaning that it will only behave as expected if passed a `value` prop. The textarea will automatically keep the character count in sync as the value prop is changed. With the exception of `className`, all props will be transferred to the child ``. If a `className` prop is passed, it will be applied to the wrapper node.
+
+```jsx
+var React = require( 'react' ),
+ CountedTextarea = require( 'components/forms/counted-textarea' );
+
+module.exports = React.createClass( {
+ displayName: 'MyComponent',
+
+ getInitialState: function() {
+ return { value: '' };
+ },
+
+ onChange: function( event ) {
+ this.setState( { value: event.target.value } );
+ },
+
+ render: function() {
+ return
+ }
+} );
+```
+
+## Props
+
+The following props are made available:
+
+### `className`
+
+If a `className` prop is passed, it will be applied to the wrapper node.
+
+### `acceptableLength`
+
+If passed and the value of the input exceeds the acceptable character count length, warning styles will be applied to the rendered output. If a maximum length is desired, use the browser default [`maxLength` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textareaattr-maxlength).
diff --git a/client/components/forms/counted-textarea/docs/example.jsx b/client/components/forms/counted-textarea/docs/example.jsx
new file mode 100644
index 00000000000000..3729ae7184c6e5
--- /dev/null
+++ b/client/components/forms/counted-textarea/docs/example.jsx
@@ -0,0 +1,41 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var CountedTextarea = require( 'components/forms/counted-textarea' );
+
+module.exports = React.createClass( {
+ displayName: 'CountedTextareas',
+
+ mixins: [ React.addons.PureRenderMixin ],
+
+ getInitialState: function() {
+ return {
+ value: 'Hello World!'
+ };
+ },
+
+ onChange: function( event ) {
+ this.setState( {
+ value: event.target.value
+ } );
+ },
+
+ render: function() {
+ return (
+
+ );
+ }
+} );
diff --git a/client/components/forms/counted-textarea/index.jsx b/client/components/forms/counted-textarea/index.jsx
new file mode 100644
index 00000000000000..34572f3599cbe1
--- /dev/null
+++ b/client/components/forms/counted-textarea/index.jsx
@@ -0,0 +1,81 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ classNames = require( 'classnames' ),
+ omit = require( 'lodash/object/omit' ),
+ noop = require( 'lodash/utility/noop' );
+
+/**
+ * Internal dependencies
+ */
+var FormTextarea = require( 'components/forms/form-textarea' );
+
+module.exports = React.createClass( {
+ displayName: 'CountedTextarea',
+
+ propTypes: {
+ value: React.PropTypes.string,
+ placeholder: React.PropTypes.string,
+ countPlaceholderLength: React.PropTypes.bool,
+ onChange: React.PropTypes.func,
+ acceptableLength: React.PropTypes.number,
+ showRemainingCharacters: React.PropTypes.bool
+ },
+
+ getDefaultProps: function() {
+ return {
+ value: '',
+ placeholder: '',
+ countPlaceholderLength: false,
+ onChange: noop,
+ showRemainingCharacters: false,
+ };
+ },
+
+ renderCountPanel: function() {
+ var length = this.props.value.length;
+
+ if ( ! length && this.props.countPlaceholderLength ) {
+ length = this.props.placeholder.length;
+ }
+
+ if ( this.props.showRemainingCharacters && this.props.acceptableLength ) {
+ return (
+ { this.translate( '%d character remaining', '%d characters remaining', {
+ context: 'Input length',
+ args: [ this.props.acceptableLength - length ],
+ count: this.props.acceptableLength - length
+ } ) }
+ { this.props.children }
+
+ );
+ } else {
+ return (
+
+ { this.translate( '%d character', '%d characters', {
+ context: 'Input length',
+ args: [ length ],
+ count: length
+ } ) }
+ { this.props.children }
+
+ );
+ }
+ },
+
+ render: function() {
+ var classes = classNames( 'counted-textarea', this.props.className, {
+ 'is-exceeding-acceptable-length': this.props.acceptableLength && this.props.value.length > this.props.acceptableLength
+ } );
+
+ return (
+
+
+ { this.renderCountPanel() }
+
+ );
+ }
+} );
diff --git a/client/components/forms/counted-textarea/style.scss b/client/components/forms/counted-textarea/style.scss
new file mode 100644
index 00000000000000..93e1ab6ab1411d
--- /dev/null
+++ b/client/components/forms/counted-textarea/style.scss
@@ -0,0 +1,24 @@
+.counted-textarea {
+ border: 1px solid lighten( $gray, 20% );
+ background-color: lighten( $gray, 30% );
+
+ &.is-exceeding-acceptable-length {
+ background: $alert-red;
+ .counted-textarea__count-panel {
+ color: $white;
+ }
+ }
+}
+
+.counted-textarea__input {
+ display: block;
+ resize: vertical;
+ border: none;
+ padding: 8px;
+}
+
+.counted-textarea__count-panel {
+ padding: 8px;
+ font-size: 12px;
+ color: darken( $gray, 10% );
+}
diff --git a/client/components/forms/counted-textarea/test/index.jsx b/client/components/forms/counted-textarea/test/index.jsx
new file mode 100644
index 00000000000000..db0754435af670
--- /dev/null
+++ b/client/components/forms/counted-textarea/test/index.jsx
@@ -0,0 +1,144 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react/addons' ),
+ ReactInjection = require( 'react/lib/ReactInjection' ),
+ TestUtils = React.addons.TestUtils,
+ expect = require( 'chai' ).expect;
+
+/**
+ * Internal dependencies
+ */
+var i18n = require( 'lib/mixins/i18n' );
+
+describe( 'CountedTextarea', function() {
+ var CountedTextarea, renderer;
+
+ before( function() {
+ i18n.initialize();
+ ReactInjection.Class.injectMixin( i18n.mixin );
+ CountedTextarea = require( '../' );
+ } );
+
+ beforeEach( function() {
+ renderer = TestUtils.createRenderer();
+ } );
+
+ it( 'should render the character count of the passed value', function() {
+ var result;
+
+ renderer.render( );
+ result = renderer.getRenderOutput();
+
+ expect( result.props.className ).to.equal( 'counted-textarea' );
+ expect( result.props.children ).to.have.length( 2 );
+ expect( result.props.children[1].props.children[0] ).to.equal( '12 characters' );
+ } );
+
+ it( 'should render warning styles when the acceptable length is exceeded', function() {
+ var result;
+
+ renderer.render( );
+ result = renderer.getRenderOutput();
+
+ expect( result.props.className ).to.equal( 'counted-textarea is-exceeding-acceptable-length' );
+ } );
+
+ it( 'should apply className to the wrapper element', function() {
+ var result;
+
+ renderer.render( );
+ result = renderer.getRenderOutput();
+
+ expect( result.props.className ).to.equal( 'counted-textarea custom-class' );
+ } );
+
+ it( 'should pass props to the child textarea', function() {
+ var value = 'Hello World!',
+ placeholder = 'placeholder test',
+ result;
+
+ renderer.render( );
+ result = renderer.getRenderOutput();
+
+ expect( result.props.children ).to.have.length( 2 );
+ expect( result.props.children[0].props.value ).to.equal( value );
+ expect( result.props.children[0].props.placeholder ).to.equal( placeholder );
+ expect( result.props.children[0].props.className ).to.equal( 'counted-textarea__input' );
+ } );
+
+ it( 'should not use the placeholder as the counted item if value is empty and countPlaceholderLength is not set', function() {
+ var value = '',
+ placeholder = 'placeholder test',
+ result;
+
+ renderer.render( );
+ result = renderer.getRenderOutput();
+
+ expect( result.props.children[1].props.children[0] ).to.equal( '0 characters' );
+ } );
+
+ it( 'should use the placeholder as the counted item if value is empty and countPlaceholderLength is true', function() {
+ var value = '',
+ placeholder = 'placeholder test',
+ result;
+
+ renderer.render( );
+ result = renderer.getRenderOutput();
+
+ expect( result.props.children[1].props.children[0] ).to.equal( '16 characters' );
+ } );
+
+ it( 'should use the value as the counted item if value is set', function() {
+ var value = 'Hello World!',
+ placeholder = 'placeholder test',
+ result;
+
+ renderer.render( );
+ result = renderer.getRenderOutput();
+
+ expect( result.props.children[1].props.children[0] ).to.equal( '12 characters' );
+ } );
+
+ it( 'should not pass acceptableLength prop to the child textarea', function() {
+ var value = 'Hello World!',
+ acceptableLength = 140,
+ result;
+
+ renderer.render( );
+ result = renderer.getRenderOutput();
+
+ expect( result.props.children ).to.have.length( 2 );
+ expect( result.props.children[0].props.value ).to.equal( value );
+ expect( result.props.children[0].props.acceptableLength ).to.be.undefined;
+ expect( result.props.children[0].props.className ).to.equal( 'counted-textarea__input' );
+ } );
+
+ it( 'should render a reversed count when set to showRemainingCount', function() {
+ var value = 'Hello World!',
+ acceptableLength = 140,
+ result;
+
+ renderer.render( );
+ result = renderer.getRenderOutput();
+
+ expect( result.props.className ).to.equal( 'counted-textarea' );
+ expect( result.props.children ).to.have.length( 2 );
+ expect( result.props.children[1].props.children[0] ).to.equal( '128 characters remaining' );
+ } );
+
+ it( 'should render additional panel content when set', function() {
+ var value = 'Hello World!',
+ acceptableLength = 140,
+ additionalPanelContent = 'Extra stuff',
+ result;
+
+ renderer.render( { additionalPanelContent } );
+ result = renderer.getRenderOutput();
+
+ expect( result.props.className ).to.equal( 'counted-textarea' );
+ expect( result.props.children ).to.have.length( 2 );
+ expect( result.props.children[1].props.children[0] ).to.equal( '128 characters remaining' );
+ expect( result.props.children[1].props.children[1][1] ).to.equal( 'Extra stuff' );
+ } );
+} );
diff --git a/client/components/forms/docs/example.jsx b/client/components/forms/docs/example.jsx
new file mode 100644
index 00000000000000..76a795f4ef365e
--- /dev/null
+++ b/client/components/forms/docs/example.jsx
@@ -0,0 +1,237 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var countriesList = require( 'lib/countries-list' ).forSms(),
+ Card = require( 'components/card' ),
+ FormButton = require( 'components/forms/form-button' ),
+ FormButtonsBar = require( 'components/forms/form-buttons-bar' ),
+ FormCheckbox = require( 'components/forms/form-checkbox' ),
+ FormCountrySelect = require( 'components/forms/form-country-select' ),
+ FormFieldset = require( 'components/forms/form-fieldset' ),
+ FormInputValidation = require( 'components/forms/form-input-validation' ),
+ FormLabel = require( 'components/forms/form-label' ),
+ FormLegend = require( 'components/forms/form-legend' ),
+ FormPasswordInput = require( 'components/forms/form-password-input' ),
+ FormPhoneInput = require( 'components/forms/form-phone-input' ),
+ FormRadio = require( 'components/forms/form-radio' ),
+ FormSectionHeading = require( 'components/forms/form-section-heading' ),
+ FormSelect = require( 'components/forms/form-select' ),
+ FormSettingExplanation = require( 'components/forms/form-setting-explanation' ),
+ FormStateSelector = require( 'components/forms/us-state-selector' ),
+ FormTelInput = require( 'components/forms/form-tel-input' ),
+ FormTextarea = require( 'components/forms/form-textarea' ),
+ FormTextInput = require( 'components/forms/form-text-input' ),
+ FormTextInputWithAffixes = require( 'components/forms/form-text-input-with-affixes' ),
+ FormToggle = require( 'components/forms/form-toggle' ),
+ CompactFormToggle = require( 'components/forms/form-toggle/compact' );
+
+var FormFields = React.createClass( {
+ displayName: 'FormFields',
+
+ mixins: [ React.addons.PureRenderMixin ],
+
+ getInitialState: function() {
+ return {
+ checkedRadio: 'first',
+ toggled: false,
+ compactToggled: false
+ };
+ },
+
+ handleRadioChange: function( event ) {
+ this.setState( { checkedRadio: event.currentTarget.value } );
+ },
+
+ handleToggle: function() {
+ this.setState( { toggled: ! this.state.toggled } );
+ },
+
+ handleCompactToggle: function() {
+ this.setState( { compactToggled: ! this.state.compactToggled } );
+ },
+
+ render: function() {
+ return (
+
+
+
+
+ The form fields components act as wrapper components to aid in componentizing CSS.
+ Here is an example of all of the form fields components and their expected markup.
+
+
+
+ The following form fields components are wrapped in Card components to demonstrate the FormSectionHeading component.
+
+
+
+ Form Section Heading
+
+
+ Form Checkbox
+
+
+ Email me when someone Likes one of my comments.
+
+
+
+
+ Disabled Form Text Input
+
+
+
+
+ Form Text Input
+
+ This is an explanation of FormTextInput.
+
+
+
+ Form Text Input
+
+
+
+
+
+ Form Text Input
+
+
+
+
+
+ Form Text Input With Affixes
+
+
+
+
+ Form Select
+
+ 1
+ 2
+ 3
+ 4
+
+
+
+
+ { this.translate( 'Form Password Input' ) }
+
+
+
+ Form Toggle
+
+
+
+
+
+
+
+
+
+ Form Button
+
+
+
+
+ Form Section Heading
+
+
+ Form Country Select
+
+
+
+
+ Form US State Select
+
+
+
+
+ Form Radios
+
+
+ First radio
+
+
+
+
+ Second radio
+
+
+
+
+ Form Tel Input
+
+
+
+
+ Form Phone Input
+
+
+
+
+ Form Textarea
+
+
+
+
+ Form Button
+ Secondary Form Button
+
+
+
+ );
+ }
+} );
+
+module.exports = FormFields;
diff --git a/client/components/forms/form-button/index.jsx b/client/components/forms/form-button/index.jsx
new file mode 100644
index 00000000000000..43971c081f3581
--- /dev/null
+++ b/client/components/forms/form-button/index.jsx
@@ -0,0 +1,45 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ joinClasses = require( 'react/lib/joinClasses' ),
+ classNames = require( 'classnames' ),
+ omit = require( 'lodash/object/omit' ),
+ isEmpty = require( 'lodash/lang/isEmpty' );
+
+/**
+ * Internal dependencies
+ */
+var Button = require( 'components/button' );
+
+module.exports = React.createClass( {
+
+ displayName: 'FormsButton',
+
+ getDefaultProps: function() {
+ return {
+ isSubmitting: false,
+ isPrimary: true,
+ type: 'submit'
+ };
+ },
+
+ getDefaultButtonAction: function() {
+ return this.props.isSubmitting ? this.translate( 'Saving…' ) : this.translate( 'Save Settings' );
+ },
+
+ render: function() {
+ var buttonClasses = classNames( {
+ 'form-button': true
+ } );
+
+ return (
+
+ { isEmpty( this.props.children ) ? this.getDefaultButtonAction() : this.props.children }
+
+ );
+ }
+} );
diff --git a/client/components/forms/form-button/style.scss b/client/components/forms/form-button/style.scss
new file mode 100644
index 00000000000000..e500e0dd8ba38a
--- /dev/null
+++ b/client/components/forms/form-button/style.scss
@@ -0,0 +1,4 @@
+.form-button {
+ float: right;
+ margin-left: 10px;
+}
diff --git a/client/components/forms/form-buttons-bar/index.jsx b/client/components/forms/form-buttons-bar/index.jsx
new file mode 100644
index 00000000000000..d4cf8fb2ddd8f3
--- /dev/null
+++ b/client/components/forms/form-buttons-bar/index.jsx
@@ -0,0 +1,21 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react/addons' ),
+ joinClasses = require( 'react/lib/joinClasses' ),
+ omit = require( 'lodash/object/omit' );
+
+module.exports = React.createClass( {
+
+ displayName: 'FormButtonsBar',
+
+ render: function() {
+ return (
+
+ { this.props.children }
+
+ );
+ }
+} );
diff --git a/client/components/forms/form-buttons-bar/style.scss b/client/components/forms/form-buttons-bar/style.scss
new file mode 100644
index 00000000000000..df044797f82f01
--- /dev/null
+++ b/client/components/forms/form-buttons-bar/style.scss
@@ -0,0 +1,3 @@
+.form-buttons-bar {
+ @include clear-fix;
+}
diff --git a/client/components/forms/form-checkbox/index.jsx b/client/components/forms/form-checkbox/index.jsx
new file mode 100644
index 00000000000000..9726f31b7b7423
--- /dev/null
+++ b/client/components/forms/form-checkbox/index.jsx
@@ -0,0 +1,19 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react/addons' ),
+ joinClasses = require( 'react/lib/joinClasses' ),
+ omit = require( 'lodash/object/omit' );
+
+module.exports = React.createClass( {
+
+ displayName: 'FormInputCheckbox',
+
+ render: function() {
+ var otherProps = omit( this.props, [ 'className', 'type' ] );
+
+ return (
+
+ );
+ }
+} );
diff --git a/client/components/forms/form-country-select/index.jsx b/client/components/forms/form-country-select/index.jsx
new file mode 100644
index 00000000000000..93a8e11f7bc4a3
--- /dev/null
+++ b/client/components/forms/form-country-select/index.jsx
@@ -0,0 +1,41 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react/addons' ),
+ isEmpty = require( 'lodash/lang/isEmpty' ),
+ joinClasses = require( 'react/lib/joinClasses' ),
+ observe = require( 'lib/mixins/data-observe' ),
+ omit = require( 'lodash/object/omit' );
+
+module.exports = React.createClass( {
+
+ displayName: 'FormCountrySelect',
+
+ mixins: [ observe( 'countriesList' ) ],
+
+ render: function() {
+ var countriesList = this.props.countriesList.get(),
+ options = [];
+
+ if ( isEmpty( countriesList ) ) {
+ options.push( { key: '', label: this.translate( 'Loading…' ), disabled: 'disabled' } );
+ } else {
+ options = options.concat( countriesList.map( function( country ) {
+ return { key: country.code, label: country.name };
+ }
+ ) );
+ }
+
+ return (
+
+ { options.map( function( option ) {
+ return { option.label } ;
+ } ) }
+
+ );
+ }
+} );
diff --git a/client/components/forms/form-country-select/style.scss b/client/components/forms/form-country-select/style.scss
new file mode 100644
index 00000000000000..ecf695501dd237
--- /dev/null
+++ b/client/components/forms/form-country-select/style.scss
@@ -0,0 +1,9 @@
+.form-country-select {
+ margin-bottom: 1em;
+}
+
+// According to CSS tricks, browser support is IE9+
+.form-country-select:only-of-type,
+.form-country-select:last-of-type {
+ margin-bottom: 0;
+}
diff --git a/client/components/forms/form-fieldset/index.jsx b/client/components/forms/form-fieldset/index.jsx
new file mode 100644
index 00000000000000..45c9bf66b4768a
--- /dev/null
+++ b/client/components/forms/form-fieldset/index.jsx
@@ -0,0 +1,19 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react/addons' ),
+ joinClasses = require( 'react/lib/joinClasses' ),
+ omit = require( 'lodash/object/omit' );
+
+module.exports = React.createClass( {
+
+ displayName: 'FormFieldset',
+
+ render: function() {
+ return (
+
+ { this.props.children }
+
+ );
+ }
+} );
diff --git a/client/components/forms/form-fieldset/style.scss b/client/components/forms/form-fieldset/style.scss
new file mode 100644
index 00000000000000..46a16f3c976d56
--- /dev/null
+++ b/client/components/forms/form-fieldset/style.scss
@@ -0,0 +1,4 @@
+.form-fieldset {
+ clear: both;
+ margin-bottom: 20px;
+}
diff --git a/client/components/forms/form-input-validation/index.jsx b/client/components/forms/form-input-validation/index.jsx
new file mode 100644
index 00000000000000..4b4cb8c88178b2
--- /dev/null
+++ b/client/components/forms/form-input-validation/index.jsx
@@ -0,0 +1,27 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react/addons' ),
+ classNames = require( 'classnames' );
+
+module.exports = React.createClass( {
+
+ displayName: 'FormInputValidation',
+
+ getDefaultProps: function() {
+ return { isError: false };
+ },
+
+ render: function() {
+ var classes = classNames( {
+ 'form-input-validation': true,
+ 'is-error': this.props.isError
+ } );
+
+ return (
+
+ { this.props.text }
+
+ );
+ }
+} );
diff --git a/client/components/forms/form-input-validation/style.scss b/client/components/forms/form-input-validation/style.scss
new file mode 100644
index 00000000000000..3becb41d6044eb
--- /dev/null
+++ b/client/components/forms/form-input-validation/style.scss
@@ -0,0 +1,25 @@
+.form-input-validation {
+ color: $alert-green;
+ position: relative;
+ padding: 6px 24px 11px 28px;
+ border-radius: 1px;
+ box-sizing: border-box;
+ font-size: 14px;
+ animation: appear .3s ease-in-out;
+
+ &:before {
+ @include noticon( '\f418', 16px );
+ position: absolute;
+ left: 0;
+ font-size: 24px;
+ line-height: 1;
+ }
+
+ &.is-error {
+ color: $alert-red;
+
+ &:before {
+ content: "\f424"
+ }
+ }
+}
diff --git a/client/components/forms/form-label/index.jsx b/client/components/forms/form-label/index.jsx
new file mode 100644
index 00000000000000..2f7120428a1ce6
--- /dev/null
+++ b/client/components/forms/form-label/index.jsx
@@ -0,0 +1,19 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react/addons' ),
+ joinClasses = require( 'react/lib/joinClasses' ),
+ omit = require( 'lodash/object/omit' );
+
+module.exports = React.createClass( {
+
+ displayName: 'FormLabel',
+
+ render: function() {
+ return (
+
+ { this.props.children }
+
+ );
+ }
+} );
diff --git a/client/components/forms/form-label/style.scss b/client/components/forms/form-label/style.scss
new file mode 100644
index 00000000000000..b918e757134153
--- /dev/null
+++ b/client/components/forms/form-label/style.scss
@@ -0,0 +1,11 @@
+.form-label {
+ display: block;
+ font-size: 14px;
+ font-weight: 600;
+ margin-bottom: 5px;
+}
+
+.form-label input[type="checkbox"] + span,
+.form-label input[type="radio"] + span {
+ font-weight: normal;
+}
diff --git a/client/components/forms/form-legend/index.jsx b/client/components/forms/form-legend/index.jsx
new file mode 100644
index 00000000000000..1eafc6669d2beb
--- /dev/null
+++ b/client/components/forms/form-legend/index.jsx
@@ -0,0 +1,19 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react/addons' ),
+ joinClasses = require( 'react/lib/joinClasses' ),
+ omit = require( 'lodash/object/omit' );
+
+module.exports = React.createClass( {
+
+ displayName: 'FormLegend',
+
+ render: function() {
+ return (
+
+ { this.props.children }
+
+ );
+ }
+} );
diff --git a/client/components/forms/form-legend/style.scss b/client/components/forms/form-legend/style.scss
new file mode 100644
index 00000000000000..be3e7ed86a1ce1
--- /dev/null
+++ b/client/components/forms/form-legend/style.scss
@@ -0,0 +1,8 @@
+.form-legend {
+ font-size: 14px;
+ font-weight: 600;
+ margin-bottom: 5px;
+}
+li .form-legend {
+ margin-top: 4px;
+}
diff --git a/client/components/forms/form-password-input/index.jsx b/client/components/forms/form-password-input/index.jsx
new file mode 100644
index 00000000000000..5cd7e7895ba04b
--- /dev/null
+++ b/client/components/forms/form-password-input/index.jsx
@@ -0,0 +1,63 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react/addons' ),
+ Gridicon = require( 'components/gridicon' ),
+ classNames = require( 'classnames' ),
+ omit = require( 'lodash/object/omit' );
+
+/**
+ * Internal dependencies
+ */
+var FormTextInput = require( 'components/forms/form-text-input' ),
+ viewport = require( 'lib/viewport' );
+
+module.exports = React.createClass( {
+
+ displayName: 'FormPasswordInput',
+
+ getInitialState: function() {
+ var isMobile = viewport.isMobile();
+
+ if ( isMobile ) {
+ return { hidePassword: false };
+ } else {
+ return { hidePassword: true };
+ }
+ },
+
+ togglePasswordVisibility: function() {
+ this.setState( { hidePassword: ! this.state.hidePassword } );
+ },
+
+ hidden: function() {
+ if ( this.props.hideToggle ) {
+ return true;
+ }
+ return this.props.submitting || this.state.hidePassword;
+ },
+
+ render: function() {
+
+ var toggleVisibilityClasses = classNames( {
+ 'form-password-input__toggle': true,
+ 'form-password-input__toggle-visibility': ! this.props.hideToggle
+ } );
+
+ return (
+
+
+
+
+ { this.hidden() ?
+
+ :
+
+ }
+
+
+ );
+ }
+} );
diff --git a/client/components/forms/form-password-input/style.scss b/client/components/forms/form-password-input/style.scss
new file mode 100644
index 00000000000000..1a866998924ac3
--- /dev/null
+++ b/client/components/forms/form-password-input/style.scss
@@ -0,0 +1,28 @@
+.form-password-input {
+ position: relative;
+
+ .form-text-input {
+ padding-right: 32px;
+ }
+
+ .form-password-input__toggle {
+ display: none;
+ }
+
+ .form-password-input__toggle-visibility {
+ display: block;
+ cursor: pointer;
+ position: absolute;
+ right: 8px #{"/*rtl:ignore*/"};
+ top: 8px;
+ user-select: none;
+
+ .gridicon {
+ fill: lighten( $gray, 10% );
+
+ &:hover {
+ fill: darken( $gray, 10% );
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/client/components/forms/form-phone-input/Makefile b/client/components/forms/form-phone-input/Makefile
new file mode 100644
index 00000000000000..8692e1e3dc5580
--- /dev/null
+++ b/client/components/forms/form-phone-input/Makefile
@@ -0,0 +1,13 @@
+UI ?= bdd
+REPORTER ?= spec
+COMPILERS ?= jsx:babel/register
+NODE_BIN := $(shell npm bin)
+MOCHA ?= $(NODE_BIN)/mocha
+BASE_DIR := $(NODE_BIN)/../..
+NODE_PATH := test:$(BASE_DIR)/client:$(BASE_DIR)/shared
+
+# In order to simply stub modules, add test to the NODE_PATH
+test:
+ @NODE_ENV=test NODE_PATH=$(NODE_PATH) $(MOCHA) --compilers $(COMPILERS) --reporter $(REPORTER) --ui $(UI)
+
+.PHONY: test
diff --git a/client/components/forms/form-phone-input/index.jsx b/client/components/forms/form-phone-input/index.jsx
new file mode 100644
index 00000000000000..596c0757e9010a
--- /dev/null
+++ b/client/components/forms/form-phone-input/index.jsx
@@ -0,0 +1,158 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ noop = require( 'lodash/utility/noop' ),
+ first = require( 'lodash/array/first' ),
+ where = require( 'lodash/collection/where' );
+
+/**
+ * Internal dependencies
+ */
+var FormLabel = require( 'components/forms/form-label' ),
+ FormTelInput = require( 'components/forms/form-tel-input' ),
+ FormFieldset = require( 'components/forms/form-fieldset' ),
+ CountrySelect = require( 'components/forms/form-country-select' ),
+ joinClasses = require( 'react/lib/joinClasses' ),
+ phoneValidation = require( 'lib/phone-validation' );
+
+var CLEAN_REGEX = /^0|[\s.\-()]+/g;
+
+module.exports = React.createClass( {
+ displayName: 'FormPhoneInput',
+
+ mixins: [ React.addons.LinkedStateMixin ],
+
+ propTypes: {
+ initialCountryCode: React.PropTypes.string,
+ initialPhoneNumber: React.PropTypes.string,
+ countriesList: React.PropTypes.object.isRequired,
+ isDisabled: React.PropTypes.bool,
+ countrySelectProps: React.PropTypes.object,
+ phoneInputProps: React.PropTypes.object,
+ onChange: React.PropTypes.func
+ },
+
+ getDefaultProps: function() {
+ return {
+ isDisabled: false,
+ countrySelectProps: {},
+ phoneInputProps: {},
+ onChange: noop
+ };
+ },
+
+ getInitialState: function() {
+ return {
+ countryCode: this.props.initialCountryCode || '',
+ phoneNumber: this.props.initialPhoneNumber || ''
+ };
+ },
+
+ componentWillMount: function() {
+ this._maybeSetCountryStateFromList();
+ },
+
+ componentDidUpdate: function() {
+ this._maybeSetCountryStateFromList();
+ },
+
+ render: function() {
+ var countryValueLink = {
+ value: this.state.countryCode,
+ requestChange: this._handleCountryChange
+ },
+ phoneValueLink = {
+ value: this.state.phoneNumber,
+ requestChange: this._handlePhoneChange
+ };
+
+ return (
+
+
+ { this.translate( 'Country Code', { context: 'The country code for the phone for the user.' } ) }
+
+
+
+
+ { this.translate( 'Phone Number' ) }
+
+
+
+ );
+ },
+
+ _getCountryData: function() {
+ // TODO: move this to country-list or CountrySelect
+ return first( where( this.props.countriesList.get(), {
+ code: this.state.countryCode
+ } ) );
+ },
+
+ _handleCountryChange: function( newValue ) {
+ this.setState( { countryCode: newValue }, this._triggerOnChange );
+ },
+
+ _handlePhoneChange: function( newValue ) {
+ this.setState( { phoneNumber: newValue }, this._triggerOnChange );
+ },
+
+ _triggerOnChange: function() {
+ this.props.onChange( this.getValue() );
+ },
+
+ _cleanNumber: function( number ) {
+ return number.replace( CLEAN_REGEX, '' );
+ },
+
+ // Set the default state of the country code selector, if not already set
+ _maybeSetCountryStateFromList: function() {
+ var countries;
+
+ if ( this.state.countryCode ) {
+ return;
+ }
+
+ countries = this.props.countriesList.get();
+ if ( ! countries.length ) {
+ return;
+ }
+
+ this.setState( {
+ countryCode: countries[ 0 ].code
+ } );
+ },
+
+ _validate: function( number ) {
+ return phoneValidation( number );
+ },
+
+ getValue: function() {
+ var countryData = this._getCountryData(),
+ numberClean = this._cleanNumber( this.state.phoneNumber ),
+ countryNumericCode = countryData ? countryData.numeric_code : '',
+ numberFull = countryNumericCode + numberClean,
+ isValid = this._validate( numberFull );
+
+ return {
+ isValid: ! isValid.error,
+ validation: isValid,
+ countryData: countryData,
+ phoneNumber: numberClean,
+ phoneNumberFull: numberFull
+ };
+ }
+} );
diff --git a/client/components/forms/form-phone-input/test/index.jsx b/client/components/forms/form-phone-input/test/index.jsx
new file mode 100644
index 00000000000000..3734d92456d28b
--- /dev/null
+++ b/client/components/forms/form-phone-input/test/index.jsx
@@ -0,0 +1,76 @@
+/* eslint-disable vars-on-top */
+
+require( 'lib/react-test-env-setup' )();
+
+/**
+ * External dependencies
+ */
+var React = require( 'react/addons' ),
+ ReactInjection = require( 'react/lib/ReactInjection' ),
+ TestUtils = React.addons.TestUtils,
+ expect = require( 'chai' ).expect;
+
+/**
+ * Internal dependencies
+ */
+var i18n = require( 'lib/mixins/i18n' ),
+ mockCountriesList = require( './mock-countries-list' ),
+ mockCountriesListEmpty = require( './mock-countries-list-empty' );
+
+var countries = mockCountriesList.get();
+
+describe( 'FormPhoneInput', function() {
+ var FormPhoneInput;
+
+ before( function() {
+ i18n.initialize();
+ ReactInjection.Class.injectMixin( i18n.mixin );
+ FormPhoneInput = require( 'components/forms/form-phone-input' );
+ } );
+
+ afterEach( function() {
+ React.unmountComponentAtNode( document.body );
+ } );
+
+ describe( 'getValue()', function() {
+ it( 'should set country from props', function() {
+ var phoneComponent = React.render( , document.body );
+
+ expect( phoneComponent.getValue().countryData ).to.deep.equal( countries[ 1 ] );
+ } );
+
+ it( 'should set country to first element when not specified', function() {
+ var phoneComponent = React.render( , document.body );
+
+ expect( phoneComponent.getValue().countryData ).to.deep.equal( countries[ 0 ] );
+ } );
+
+ it( 'should update country on change', function() {
+ var phoneComponent = React.render( , document.body ),
+ select = TestUtils.findRenderedDOMComponentWithTag( phoneComponent, 'select' );
+
+ TestUtils.Simulate.change( select.getDOMNode(), {
+ target: {
+ value: countries[ 1 ].code
+ }
+ } );
+
+ expect( phoneComponent.getValue().countryData ).to.deep.equal( countries[ 1 ] );
+ } );
+
+ it( 'should have no country with empty countryList', function() {
+ var phoneComponent = React.render( , document.body );
+
+ expect( phoneComponent.getValue().countryData ).to.equal( undefined );
+ } );
+
+ it( 'should update country on countryList change', function() {
+ var phoneComponent = React.render( , document.body );
+
+ // Render again with filled country list
+ phoneComponent = React.render( , document.body );
+
+ expect( phoneComponent.getValue().countryData ).to.deep.equal( countries[ 0 ] );
+ } );
+ } );
+} );
diff --git a/client/components/forms/form-phone-input/test/mock-countries-list-empty.js b/client/components/forms/form-phone-input/test/mock-countries-list-empty.js
new file mode 100644
index 00000000000000..ab76bc28b14fdc
--- /dev/null
+++ b/client/components/forms/form-phone-input/test/mock-countries-list-empty.js
@@ -0,0 +1,14 @@
+/**
+ * External dependencies
+ */
+var emitter = require( 'lib/mixins/emitter' );
+
+var countriesList = {
+ get: function() {
+ return {};
+ }
+};
+
+emitter( countriesList );
+
+module.exports = countriesList;
diff --git a/client/components/forms/form-phone-input/test/mock-countries-list.js b/client/components/forms/form-phone-input/test/mock-countries-list.js
new file mode 100644
index 00000000000000..94c326fd0139ed
--- /dev/null
+++ b/client/components/forms/form-phone-input/test/mock-countries-list.js
@@ -0,0 +1,21 @@
+/**
+ * External dependencies
+ */
+var emitter = require( 'lib/mixins/emitter' );
+
+var countriesList = {
+ get: function() {
+ return [
+ {
+ code: 'US', name: 'United States (+1)', numeric_code: '+1', country_name: 'United States'
+ },
+ {
+ code: 'AR', name: 'Argentina (+54)', numeric_code: '+54', country_name: 'Argentina'
+ }
+ ];
+ }
+};
+
+emitter( countriesList );
+
+module.exports = countriesList;
diff --git a/client/components/forms/form-radio/index.jsx b/client/components/forms/form-radio/index.jsx
new file mode 100644
index 00000000000000..72d50c388e6418
--- /dev/null
+++ b/client/components/forms/form-radio/index.jsx
@@ -0,0 +1,22 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react/addons' ),
+ joinClasses = require( 'react/lib/joinClasses' ),
+ omit = require( 'lodash/object/omit' );
+
+module.exports = React.createClass( {
+
+ displayName: 'FormRadio',
+
+ render: function() {
+ var otherProps = omit( this.props, [ 'className', 'type' ] );
+
+ return (
+
+ );
+ }
+} );
diff --git a/client/components/forms/form-range/index.jsx b/client/components/forms/form-range/index.jsx
new file mode 100644
index 00000000000000..6c7521431e988c
--- /dev/null
+++ b/client/components/forms/form-range/index.jsx
@@ -0,0 +1,49 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ omit = require( 'lodash/object/omit' ),
+ classnames = require( 'classnames' );
+
+module.exports = React.createClass( {
+ displayName: 'FormRange',
+
+ propTypes: {
+ onChange: React.PropTypes.func
+ },
+
+ getDefaultProps: function() {
+ return {
+ onChange: function() {}
+ };
+ },
+
+ componentDidMount: function() {
+ if ( this.shouldNormalizeChange() ) {
+ this.refs.range.getDOMNode().addEventListener( 'change', this.onChange );
+ }
+ },
+
+ componentWillUnmount: function() {
+ this.refs.range.getDOMNode().removeEventListener( 'change', this.onChange );
+ },
+
+ shouldNormalizeChange: function() {
+ var ua = window.navigator.userAgent;
+
+ // Internet Explorer doesn't trigger the normal "input" event as the
+ // user drags the thumb. Instead, it emits the equivalent event on
+ // "change", so we watch the change event and emit a simulated event.
+ return -1 !== ua.indexOf( 'MSIE' ) || -1 !== ua.indexOf( 'Trident/' );
+ },
+
+ onChange: function( event ) {
+ this.props.onChange( event );
+ },
+
+ render: function() {
+ var classes = classnames( this.props.className, 'form-range' );
+
+ return ;
+ }
+} );
diff --git a/client/components/forms/form-range/style.scss b/client/components/forms/form-range/style.scss
new file mode 100644
index 00000000000000..dbee8ae0a621ae
--- /dev/null
+++ b/client/components/forms/form-range/style.scss
@@ -0,0 +1,66 @@
+.form-range {
+ -webkit-appearance: none;
+ display: block;
+ width: 100%;
+ height: 18px;
+ margin: 0;
+ padding: 0;
+ background: lighten( $gray, 20% );
+ background: linear-gradient(
+ to bottom,
+ transparent,
+ transparent 8px,
+ lighten( $gray, 20% ) 8px,
+ lighten( $gray, 20% ) 10px,
+ transparent 10px
+ );
+}
+
+.form-range:focus {
+ outline: none;
+}
+
+@mixin form-range-thumb() {
+ height: 26px;
+ width: 26px;
+ border: none;
+ background: radial-gradient(
+ $blue-medium,
+ $blue-medium 6px,
+ $blue-dark 7px,
+ transparent 8px,
+ transparent
+ );
+ cursor: pointer;
+}
+
+.form-range::-webkit-slider-thumb {
+ @include form-range-thumb();
+ -webkit-appearance: none;
+}
+
+.form-range::-moz-range-track {
+ background: transparent;
+ border: none;
+}
+
+.form-range::-moz-range-thumb {
+ @include form-range-thumb();
+}
+
+.form-range::-ms-track {
+ width: 100%;
+ cursor: pointer;
+ background: transparent;
+ border-color: transparent;
+ color: transparent;
+}
+
+.form-range::-ms-fill-lower,
+.form-range::-ms-fill-upper {
+ background: transparent;
+}
+
+.form-range::-ms-thumb {
+ @include form-range-thumb();
+}
diff --git a/client/components/forms/form-section-heading/index.jsx b/client/components/forms/form-section-heading/index.jsx
new file mode 100644
index 00000000000000..f427b6f7cd1cd6
--- /dev/null
+++ b/client/components/forms/form-section-heading/index.jsx
@@ -0,0 +1,19 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ joinClasses = require( 'react/lib/joinClasses' ),
+ omit = require( 'lodash/object/omit' );
+
+module.exports = React.createClass( {
+
+ displayName: 'FormSectionHeading',
+
+ render: function() {
+ return (
+
+ { this.props.children }
+
+ );
+ }
+} );
diff --git a/client/components/forms/form-section-heading/style.scss b/client/components/forms/form-section-heading/style.scss
new file mode 100644
index 00000000000000..5e56b91b4471c2
--- /dev/null
+++ b/client/components/forms/form-section-heading/style.scss
@@ -0,0 +1,9 @@
+.form-section-heading {
+ font-size: 24px;
+ font-weight: 300;
+ margin: 30px 0 20px 0;
+}
+
+.form-section-heading:first-child {
+ margin-top: 0;
+}
diff --git a/client/components/forms/form-select/index.jsx b/client/components/forms/form-select/index.jsx
new file mode 100644
index 00000000000000..4a8a563b75679b
--- /dev/null
+++ b/client/components/forms/form-select/index.jsx
@@ -0,0 +1,19 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react/addons' ),
+ joinClasses = require( 'react/lib/joinClasses' ),
+ omit = require( 'lodash/object/omit' );
+
+module.exports = React.createClass( {
+
+ displayName: 'FormSelect',
+
+ render: function() {
+ return (
+
+ { this.props.children }
+
+ );
+ }
+} );
diff --git a/client/components/forms/form-select/style.scss b/client/components/forms/form-select/style.scss
new file mode 100644
index 00000000000000..4ba31be590a01c
--- /dev/null
+++ b/client/components/forms/form-select/style.scss
@@ -0,0 +1,9 @@
+.form-select {
+ margin-bottom: 1em;
+}
+
+// According to CSS tricks, browser support is IE9+
+.form-select:only-of-type,
+.form-select:last-of-type {
+ margin-bottom: 0;
+}
diff --git a/client/components/forms/form-setting-explanation/index.jsx b/client/components/forms/form-setting-explanation/index.jsx
new file mode 100644
index 00000000000000..9a57cbd61fa4dd
--- /dev/null
+++ b/client/components/forms/form-setting-explanation/index.jsx
@@ -0,0 +1,19 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react/addons' ),
+ joinClasses = require( 'react/lib/joinClasses' ),
+ omit = require( 'lodash/object/omit' );
+
+module.exports = React.createClass( {
+
+ displayName: 'FormSettingExplanation',
+
+ render: function() {
+ return (
+
+ { this.props.children }
+
+ );
+ }
+} );
diff --git a/client/components/forms/form-setting-explanation/style.scss b/client/components/forms/form-setting-explanation/style.scss
new file mode 100644
index 00000000000000..cd1690e49fd8c8
--- /dev/null
+++ b/client/components/forms/form-setting-explanation/style.scss
@@ -0,0 +1,8 @@
+.form-setting-explanation {
+ color: $gray;
+ display: block;
+ font-size: 13px;
+ font-style: italic;
+ font-weight: 400;
+ margin: 5px 0 0 0;
+}
diff --git a/client/components/forms/form-tel-input/index.jsx b/client/components/forms/form-tel-input/index.jsx
new file mode 100644
index 00000000000000..52bc5088a53055
--- /dev/null
+++ b/client/components/forms/form-tel-input/index.jsx
@@ -0,0 +1,34 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react/addons' ),
+ joinClasses = require( 'react/lib/joinClasses' ),
+ omit = require( 'lodash/object/omit' ),
+ classNames = require( 'classnames' );
+
+module.exports = React.createClass( {
+
+ displayName: 'FormTelInput',
+
+ getDefaultProps: function() {
+ return {
+ isError: false
+ };
+ },
+
+ render: function() {
+ var otherProps = omit( this.props, [ 'className', 'type' ] ),
+ classes = classNames( {
+ 'form-tel-input': true,
+ 'is-error': this.props.isError
+ } );
+
+ return (
+
+ );
+ }
+} );
diff --git a/client/components/forms/form-tel-input/style.scss b/client/components/forms/form-tel-input/style.scss
new file mode 100644
index 00000000000000..24de0d0c71c65c
--- /dev/null
+++ b/client/components/forms/form-tel-input/style.scss
@@ -0,0 +1,13 @@
+.form-tel-input {
+ -webkit-appearance: none;
+
+ &:not( :focus ) {
+ &.is-error {
+ border-color: $alert-red;
+ }
+
+ &.is-error:hover {
+ border-color: darken( $alert-red, 10 );
+ }
+ }
+}
diff --git a/client/components/forms/form-text-input-with-affixes/README.md b/client/components/forms/form-text-input-with-affixes/README.md
new file mode 100644
index 00000000000000..c694e87fc2d1eb
--- /dev/null
+++ b/client/components/forms/form-text-input-with-affixes/README.md
@@ -0,0 +1,18 @@
+Form Text Input With Affixes
+============================
+
+This component is a wrapper around the default form text input that adds support for affixes, i.e. the ability to display a fixed part either at the beginning or at the end of the text input.
+
+## Props
+
+### `prefix`
+
+A text to be inserted at the beginning of the input.
+
+### `suffix`
+
+A text to be appended at the end of the input.
+
+### `noWrap`
+
+A flag that prevents the prefix and suffix from wrapping when the component is displayed on small viewports. This basically disables the corresponding breakpoint.
diff --git a/client/components/forms/form-text-input-with-affixes/index.jsx b/client/components/forms/form-text-input-with-affixes/index.jsx
new file mode 100644
index 00000000000000..7e040446517a20
--- /dev/null
+++ b/client/components/forms/form-text-input-with-affixes/index.jsx
@@ -0,0 +1,41 @@
+/**
+ * External dependencies
+ */
+import React from 'react/addons';
+import classNames from 'classnames';
+import omit from 'lodash/object/omit';
+
+/**
+ * Internal dependencies
+ */
+import FormTextInput from 'components/forms/form-text-input';
+
+export default React.createClass( {
+ displayName: 'FormTextInputWithAffixes',
+
+ propTypes: {
+ noWrap: React.PropTypes.bool,
+ prefix: React.PropTypes.string,
+ suffix: React.PropTypes.string
+ },
+
+ render() {
+ return (
+
+ { this.props.prefix && (
+
+ { this.props.prefix }
+
+ ) }
+
+
+
+ { this.props.suffix && (
+
+ { this.props.suffix }
+
+ ) }
+
+ );
+ }
+} );
diff --git a/client/components/forms/form-text-input-with-affixes/style.scss b/client/components/forms/form-text-input-with-affixes/style.scss
new file mode 100644
index 00000000000000..69904f84891e03
--- /dev/null
+++ b/client/components/forms/form-text-input-with-affixes/style.scss
@@ -0,0 +1,86 @@
+.form-text-input-with-affixes {
+ display: inline-flex;
+ flex-direction: column;
+ width: 100%;
+
+ &.no-wrap {
+ flex-direction: row;
+ }
+
+ @include breakpoint( ">480px" ) {
+ flex-direction: row;
+ }
+
+ input[type="email"],
+ input[type="password"],
+ input[type="url"],
+ input[type="text"] {
+ flex-grow: 1;
+
+ &:focus {
+ // Fixes the right border of the box shadow displayed when this input element is focused which appears as
+ // cut off when this input has a suffix, or is stuck to another element that has a higher stacking order
+ // (fix found at http://stackoverflow.com/a/24728957)
+ transform: scale( 1 );
+ }
+ }
+}
+
+@mixin no-prefix-wrap() {
+ border-bottom-left-radius: 2px;
+ border-right: none;
+ border-top-right-radius: 0;
+}
+
+@mixin no-suffix-wrap() {
+ border-bottom-left-radius: 0;
+ border-left: none;
+ border-top-right-radius: 2px;
+}
+
+.form-text-input-with-affixes__prefix,
+.form-text-input-with-affixes__suffix {
+ background: lighten( $gray, 30% );
+ border: 1px solid lighten( $gray, 20% );
+ color: $gray-dark;
+ padding: 8px 14px;
+ white-space: nowrap;
+}
+
+.form-text-input-with-affixes__prefix {
+ border-top-left-radius: 2px;
+ border-top-right-radius: 2px;
+
+ @include breakpoint( "<480px" ) {
+ :not( .no-wrap ) > & {
+ border-bottom: none;
+ }
+ }
+
+ .no-wrap > & {
+ @include no-prefix-wrap();
+ }
+
+ @include breakpoint( ">480px" ) {
+ @include no-prefix-wrap();
+ }
+}
+
+.form-text-input-with-affixes__suffix {
+ border-bottom-left-radius: 2px;
+ border-bottom-right-radius: 2px;
+
+ @include breakpoint( "<480px" ) {
+ :not( .no-wrap ) > & {
+ border-top: none;
+ }
+ }
+
+ .no-wrap > & {
+ @include no-suffix-wrap();
+ }
+
+ @include breakpoint( ">480px" ) {
+ @include no-suffix-wrap();
+ }
+}
diff --git a/client/components/forms/form-text-input/index.jsx b/client/components/forms/form-text-input/index.jsx
new file mode 100644
index 00000000000000..1777e3fb7f36de
--- /dev/null
+++ b/client/components/forms/form-text-input/index.jsx
@@ -0,0 +1,44 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react/addons' ),
+ joinClasses = require( 'react/lib/joinClasses' ),
+ omit = require( 'lodash/object/omit' ),
+ classNames = require( 'classnames' );
+
+module.exports = React.createClass( {
+
+ displayName: 'FormTextInput',
+
+ getDefaultProps: function() {
+ return {
+ isError: false,
+ isValid: false,
+ selectOnFocus: false,
+ type: 'text'
+ };
+ },
+
+ render: function() {
+ var otherProps = omit( this.props, [ 'className', 'type' ] ),
+ classes = classNames( {
+ 'form-text-input': true,
+ 'is-error': this.props.isError,
+ 'is-valid': this.props.isValid
+ } );
+
+ return (
+
+ );
+ },
+
+ selectOnFocus: function( event ) {
+ event.target.select();
+ }
+
+} );
diff --git a/client/components/forms/form-text-input/style.scss b/client/components/forms/form-text-input/style.scss
new file mode 100644
index 00000000000000..e0d5a088be79b9
--- /dev/null
+++ b/client/components/forms/form-text-input/style.scss
@@ -0,0 +1,41 @@
+input[type="email"].form-text-input,
+input[type="password"].form-text-input,
+input[type="url"].form-text-input,
+input[type="text"].form-text-input { // input[type="text"] is needed to override the default styles
+ -webkit-appearance: none;
+
+ &.is-valid {
+ border-color: $alert-green;
+ }
+
+ &.is-valid:hover {
+ border-color: darken( $alert-green, 10 );
+ }
+
+ &.is-error {
+ border-color: $alert-red;
+ }
+
+ &.is-error:hover {
+ border-color: darken( $alert-red, 10 );
+ }
+
+ &:focus {
+ &.is-valid {
+ box-shadow: 0 0 0 2px lighten( $alert-green, 35 );
+ }
+
+ &.is-valid:hover {
+ box-shadow: 0 0 0 2px lighten( $alert-green, 25 );
+ }
+
+ &.is-error {
+ box-shadow: 0 0 0 2px lighten( $alert-red, 35 );
+
+ }
+
+ &.is-error:hover {
+ box-shadow: 0 0 0 2px lighten( $alert-red, 25 );
+ }
+ }
+}
diff --git a/client/components/forms/form-textarea/index.jsx b/client/components/forms/form-textarea/index.jsx
new file mode 100644
index 00000000000000..a7e10328b0b82f
--- /dev/null
+++ b/client/components/forms/form-textarea/index.jsx
@@ -0,0 +1,19 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react/addons' ),
+ joinClasses = require( 'react/lib/joinClasses' ),
+ omit = require( 'lodash/object/omit' );
+
+module.exports = React.createClass( {
+
+ displayName: 'FormTextarea',
+
+ render: function() {
+ return (
+
+ );
+ }
+} );
diff --git a/client/components/forms/form-toggle/Makefile b/client/components/forms/form-toggle/Makefile
new file mode 100644
index 00000000000000..bc38f1cff4a73f
--- /dev/null
+++ b/client/components/forms/form-toggle/Makefile
@@ -0,0 +1,7 @@
+REPORTER ?= spec
+MOCHA ?= ../../../../node_modules/.bin/mocha
+
+test:
+ @NODE_ENV=test NODE_PATH=test:../../../ $(MOCHA) --compilers jsx:babel/register --reporter $(REPORTER)
+
+.PHONY: test
diff --git a/client/components/forms/form-toggle/README.md b/client/components/forms/form-toggle/README.md
new file mode 100644
index 00000000000000..c6b473b81d7288
--- /dev/null
+++ b/client/components/forms/form-toggle/README.md
@@ -0,0 +1,32 @@
+Toggle
+=======
+
+This component is used to implement toggle switches.
+
+#### How to use:
+
+```js
+var FormToggle = require( 'components/forms/form-toggle' );
+
+render: function() {
+ return (
+
+
+
+ );
+}
+```
+
+#### Props
+
+* `checked`: (bool) the current status of the toggle.
+* `toggling`: (bool) whether the toggle is in the middle of being performed.
+* `disabled`: (bool) whether the toggle should be in the disabled state.
+* `onChange`: (callback) what should be executed once the user clicks the toggle.
+* `id`: (string) the id of the checkbox and the for attribute of the label, should be unique.
diff --git a/client/components/forms/form-toggle/compact.jsx b/client/components/forms/form-toggle/compact.jsx
new file mode 100644
index 00000000000000..5be5ba2215ace2
--- /dev/null
+++ b/client/components/forms/form-toggle/compact.jsx
@@ -0,0 +1,27 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ classNames = require( 'classnames' ),
+ omit = require( 'lodash/object/omit' );
+
+/**
+ * Internal dependencies
+ */
+var Toggle = require( 'components/forms/form-toggle' );
+
+module.exports = React.createClass( {
+
+ displayName: 'CompactFormToggle',
+
+ render: function() {
+ return (
+
+ { this.props.children }
+
+ );
+ }
+} );
diff --git a/client/components/forms/form-toggle/index.jsx b/client/components/forms/form-toggle/index.jsx
new file mode 100644
index 00000000000000..7e5969f3b33c9d
--- /dev/null
+++ b/client/components/forms/form-toggle/index.jsx
@@ -0,0 +1,70 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react/addons' ),
+ classNames = require( 'classnames' );
+
+var idNum = 0;
+
+module.exports = React.createClass( {
+
+ displayName: 'FormToggle',
+
+ propTypes: {
+ onChange: React.PropTypes.func,
+ checked: React.PropTypes.bool,
+ disabled: React.PropTypes.bool,
+ id: React.PropTypes.string
+ },
+
+ getDefaultProps: function() {
+ return {
+ checked: false,
+ disabled: false
+ };
+ },
+ _onKeyDown: function( event ) {
+ if ( ! this.props.disabled ) {
+ if ( event.key === 'Enter' || event.key === ' ' ) {
+ event.preventDefault();
+ this.props.onChange();
+ }
+ }
+ if ( this.props.onKeyDown ) {
+ this.props.onKeyDown( event );
+ }
+ },
+
+ render: function() {
+ var id = this.props.id || 'toggle-' + idNum++,
+ toggleClasses = classNames( {
+ 'form-toggle': true,
+ 'is-toggling': this.props.toggling
+ } );
+
+ return (
+
+
+
+
+ { this.props.children }
+
+
+ );
+ }
+} );
diff --git a/client/components/forms/form-toggle/style.scss b/client/components/forms/form-toggle/style.scss
new file mode 100644
index 00000000000000..04832a62caf04b
--- /dev/null
+++ b/client/components/forms/form-toggle/style.scss
@@ -0,0 +1,114 @@
+// ==========================================================================
+// FormToggle
+// ==========================================================================
+
+.form-toggle[type="checkbox"] {
+ display: none;
+}
+
+.form-toggle__switch {
+ position: relative;
+ display: inline-block;
+ border-radius: 12px;
+ box-sizing: border-box;
+ padding: 2px;
+ width: 40px;
+ height: 24px;
+ background: lighten( $gray, 10% );
+ vertical-align: middle;
+ outline: 0;
+ cursor: pointer;
+ transition: all .4s ease, box-shadow 0s;
+
+ &:before,
+ &:after {
+ position: relative;
+ display: block;
+ content: "";
+ width: 20px;
+ height: 20px;
+ }
+ &:after {
+ left: 0;
+ border-radius: 50%;
+ background: $white;
+ transition: all .2s ease;
+ }
+ &:before {
+ display: none;
+ }
+ &:hover {
+ background: lighten( $gray, 20% );
+ }
+ .accessible-focus &:focus{
+ box-shadow: 0 0 0 2px $blue-medium;
+ }
+}
+
+.form-toggle__label {
+ cursor: pointer;
+}
+
+.form-toggle {
+ .accessible-focus &:focus {
+ + .form-toggle__label .form-toggle__switch {
+ box-shadow: 0 0 0 2px $blue-medium;
+ }
+ &:checked + .form-toggle__label .form-toggle__switch {
+ box-shadow: 0 0 0 2px $blue-light;
+ }
+ }
+ &:checked{
+ + .form-toggle__label .form-toggle__switch {
+ background: $blue-medium;
+
+ &:after {
+ left: 16px;
+ }
+ }
+ }
+ &:checked {
+ + .form-toggle__label .form-toggle__switch:hover {
+ background: $blue-light;
+ }
+ }
+ &:disabled,
+ &:disabled:hover {
+ + .form-toggle__label .form-toggle__switch {
+ background: lighten( $gray, 30% );
+ }
+ }
+}
+
+// Classes for toggle state before action is complete (updating plugin or something)
+.form-toggle.is-toggling {
+ + .form-toggle__label .form-toggle__switch {
+ background: $blue-medium;
+ }
+ &:checked {
+ + .form-toggle__label .form-toggle__switch {
+ background: lighten( $gray, 20% );
+ }
+ }
+}
+
+.form-toggle.is-compact {
+ + .form-toggle__label .form-toggle__switch {
+ border-radius: 8px;
+ width: 24px;
+ height: 16px;
+
+ &:before,
+ &:after {
+ width: 12px;
+ height: 12px;
+ }
+ }
+ &:checked {
+ + .form-toggle__label .form-toggle__switch {
+ &:after{
+ left: 8px;
+ }
+ }
+ }
+}
diff --git a/client/components/forms/form-toggle/test/index.jsx b/client/components/forms/form-toggle/test/index.jsx
new file mode 100644
index 00000000000000..8ebc4ea49eba67
--- /dev/null
+++ b/client/components/forms/form-toggle/test/index.jsx
@@ -0,0 +1,105 @@
+/**
+ * External dependencies
+ */
+var assert = require( 'assert' ),
+ React = require( 'react/addons' ),
+ TestUtils = React.addons.TestUtils,
+ unique = require( 'lodash/array/uniq' );
+
+/**
+ * Internal dependencies
+ */
+var FormToggle = require( 'components/forms/form-toggle' ),
+ CompactFormToggle = require( 'components/forms/form-toggle/compact' );
+
+require( 'lib/react-test-env-setup' )();
+
+describe( 'CompactFormToggle', function() {
+ describe( 'rendering', function() {
+ it( 'should have is-compact class', function() {
+ var toggle = TestUtils.renderIntoDocument( ),
+ toggleInput = TestUtils.scryRenderedDOMComponentsWithClass( toggle, 'form-toggle' );
+
+ assert( 0 < toggleInput.length, 'a form toggle was rendered' );
+ assert( toggleInput[ 0 ].getDOMNode().className.indexOf( 'is-compact' ) >= 0, 'is-compact class exists' );
+ } );
+ } );
+} );
+
+describe( 'FormToggle', function() {
+ afterEach( function() {
+ React.unmountComponentAtNode( document.body );
+ } );
+
+ describe( 'rendering', function() {
+ it( 'should have form-toggle class', function() {
+ var toggle = TestUtils.renderIntoDocument( ),
+ toggleInput = TestUtils.scryRenderedDOMComponentsWithClass( toggle, 'form-toggle' );
+
+ assert( 0 < toggleInput.length, 'a form toggle was rendered' );
+ } );
+
+ it( 'should not have is-compact class', function() {
+ var toggle = TestUtils.renderIntoDocument( ),
+ toggleInput = TestUtils.scryRenderedDOMComponentsWithClass( toggle, 'is-compact' );
+
+ assert( 0 === toggleInput.length, 'no form toggle with is-compact class' );
+ } );
+
+ it( 'should be checked when checked is true', function() {
+ [ true, false ].forEach( function( bool ) {
+ var toggle = TestUtils.renderIntoDocument(
+ ),
+ toggleInput = TestUtils.scryRenderedDOMComponentsWithClass( toggle, 'form-toggle' );
+
+ assert( 0 < toggleInput.length, 'a form toggle was rendered' );
+ assert( bool === toggleInput[ 0 ].getDOMNode().checked, 'form toggle checked equals boolean' );
+ } );
+ } );
+
+ it( 'should not be disabled when disabled is false', function() {
+ var toggle = TestUtils.renderIntoDocument( ),
+ toggleInput = TestUtils.scryRenderedDOMComponentsWithClass( toggle, 'form-toggle' );
+
+ assert( 0 < toggleInput.length, 'a form toggle was rendered' );
+ assert( false === toggleInput[ 0 ].getDOMNode().disabled, 'form toggle disabled equals boolean' );
+ } );
+
+ it( 'should be disabled when disabled is true', function() {
+ var toggle = TestUtils.renderIntoDocument( ),
+ toggleInput = TestUtils.scryRenderedDOMComponentsWithClass( toggle, 'form-toggle' );
+
+ assert( 0 < toggleInput.length, 'a form toggle was rendered' );
+ assert( true === toggleInput[ 0 ].getDOMNode().disabled, 'form toggle disabled equals boolean' );
+ } );
+
+ it( 'should have a label whose htmlFor matches the checkbox id', function() {
+ var toggle = TestUtils.renderIntoDocument( ),
+ toggleInput = TestUtils.scryRenderedDOMComponentsWithClass( toggle, 'form-toggle__switch' ),
+ toggleLabel = TestUtils.scryRenderedDOMComponentsWithTag( toggle, 'label' );
+
+ assert( toggleInput[ 0 ].getDOMNode().id === toggleLabel[ 0 ].getDOMNode().htmlFor );
+ } );
+
+ it( 'should create unique ids for each toggle', function() {
+ var toggles = TestUtils.renderIntoDocument(
+
+
+
+
+
+ ),
+ toggleInputs = TestUtils.scryRenderedDOMComponentsWithClass( toggles, 'form-toggle' ),
+ ids = toggleInputs.map( function( input ) {
+ return input.getDOMNode().id;
+ } );
+
+ return ids.length === unique( ids ).length;
+ } );
+ } );
+} );
diff --git a/client/components/forms/language-selector.jsx b/client/components/forms/language-selector.jsx
new file mode 100644
index 00000000000000..04e7385dd3d01d
--- /dev/null
+++ b/client/components/forms/language-selector.jsx
@@ -0,0 +1,56 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ debug = require( 'debug' )( 'calypso:forms:language-selector' );
+/**
+ * Internal dependencies
+ */
+var SelectOptGroups = require( 'components/forms/select-opt-groups' );
+
+function coerceToOptions( data, valueKey ) {
+ valueKey = 'undefined' === typeof valueKey ? 'value' : valueKey;
+
+ return data.map( function( language ){
+ return { value: language[ valueKey ], label: language.name };
+ } );
+}
+
+var LanguageSelector = React.createClass( {
+
+ displayName: 'LanguageSelector',
+
+ componentWillMount: function() {
+ debug( 'Mounting LanguageSelector React component.' );
+ },
+
+ languageOptGroups: function() {
+ var allLanguages, popularLanguages;
+
+ allLanguages = coerceToOptions( this.props.languages, this.props.valueKey );
+
+ popularLanguages = this.props.languages.filter( function( language ) { return language.popular; } );
+ popularLanguages.sort( function( a, b ) { return a.popular - b.popular; } );
+ popularLanguages = coerceToOptions( popularLanguages, this.props.valueKey );
+
+ return [
+ {
+ label: this.translate( 'Popular languages', { textOnly: true } ),
+ options: popularLanguages
+ },
+ {
+ label: this.translate( 'All languages', { textOnly: true } ),
+ options: allLanguages
+ }
+ ];
+
+ },
+
+ render: function() {
+ return (
+
+ );
+ }
+});
+
+module.exports = LanguageSelector;
diff --git a/client/components/forms/multi-checkbox/Makefile b/client/components/forms/multi-checkbox/Makefile
new file mode 100644
index 00000000000000..bc38f1cff4a73f
--- /dev/null
+++ b/client/components/forms/multi-checkbox/Makefile
@@ -0,0 +1,7 @@
+REPORTER ?= spec
+MOCHA ?= ../../../../node_modules/.bin/mocha
+
+test:
+ @NODE_ENV=test NODE_PATH=test:../../../ $(MOCHA) --compilers jsx:babel/register --reporter $(REPORTER)
+
+.PHONY: test
diff --git a/client/components/forms/multi-checkbox/README.md b/client/components/forms/multi-checkbox/README.md
new file mode 100644
index 00000000000000..8347ee6c598ab4
--- /dev/null
+++ b/client/components/forms/multi-checkbox/README.md
@@ -0,0 +1,52 @@
+MultiCheckbox
+=============
+
+MultiCheckbox is a React component that can be used in forms to simplify the creation of checkbox inputs where multiple values are possible.
+
+## Example
+
+Below is an example use for the MultiCheckbox component:
+
+```
+var options = [
+ { value: 1, label: 'One' },
+ { value: 2, label: 'Two' }
+];
+
+
+```
+
+This code snippet will generate the following output:
+
+```html
+
+ One
+ Two
+
+```
+
+## Props
+
+### `name`
+
+A name to be used as the name field for each checkbox generated. You do not need to suffix the name with "[]".
+
+### `options`
+
+An array of options, of which each is an object containing a `value` and `label` string to be displayed alongside the checkbox.
+
+### `checked`
+
+An array of option values to be checked in the rendered set of checkboxes.
+
+### `defaultChecked`
+
+If any values should be checked by default, pass these as an array using the `defaultChecked` prop.
+
+### `onChange`
+
+Behaves similarly to the equivalent function handler for standard input elements. This function is invoked when the set of selected checkboxes changes, and is passed a single object argument containing `value` as an array of the newly selected checkbox values.
+
+### `disabled`
+
+Pass `true` to set each of the rendered checkboxes as disabled.
diff --git a/client/components/forms/multi-checkbox/index.jsx b/client/components/forms/multi-checkbox/index.jsx
new file mode 100644
index 00000000000000..e4bfc67c21680b
--- /dev/null
+++ b/client/components/forms/multi-checkbox/index.jsx
@@ -0,0 +1,66 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ omit = require( 'lodash/object/omit' ),
+ debug = require( 'debug' )( 'calypso:forms:multi-checkbox' );
+
+var MultiCheckbox = module.exports = React.createClass({
+ displayName: 'MultiCheckbox',
+
+ propTypes: {
+ defaultChecked: React.PropTypes.array,
+ onChange: React.PropTypes.func,
+ disabled: React.PropTypes.bool
+ },
+
+ getInitialState: function() {
+ return { initialChecked: this.props.defaultChecked };
+ },
+
+ getDefaultProps: function() {
+ return {
+ defaultChecked: Object.freeze( [] ),
+ onChange: function() {},
+ disabled: false
+ };
+ },
+
+ componentWillMount: function() {
+ debug( 'Mounting ' + this.constructor.displayName + ' React component.' );
+ },
+
+ handleChange: function( event ) {
+ var target = event.target,
+ checked = this.props.checked || this.state.initialChecked;
+
+ checked = checked.concat( [ target.value ] ).filter( function( currentValue ) {
+ return currentValue !== target.value || target.checked;
+ } );
+
+ this.props.onChange( {
+ value: checked
+ } );
+
+ event.stopPropagation();
+ },
+
+ getCheckboxElements: function() {
+ var checked = this.props.checked || this.state.initialChecked;
+
+ return this.props.options.map( function( option ) {
+ var isChecked = checked.indexOf( option.value ) !== -1;
+
+ return (
+
+
+ { option.label }
+
+ );
+ }, this );
+ },
+
+ render: function() {
+ return { this.getCheckboxElements() }
;
+ }
+});
diff --git a/client/components/forms/multi-checkbox/test/index.jsx b/client/components/forms/multi-checkbox/test/index.jsx
new file mode 100644
index 00000000000000..09d678d1691321
--- /dev/null
+++ b/client/components/forms/multi-checkbox/test/index.jsx
@@ -0,0 +1,89 @@
+require( 'lib/react-test-env-setup' )();
+
+/**
+ * External dependencies
+ */
+var assert = require( 'assert' ),
+ React = require( 'react/addons' ),
+ TestUtils = React.addons.TestUtils;
+
+/**
+ * Internal dependencies
+ */
+var MultiCheckbox = require( '../' );
+
+describe( 'MultiCheckbox', function() {
+ var options = [
+ { value: 1, label: 'One' },
+ { value: 2, label: 'Two' }
+ ];
+
+ afterEach( function() {
+ React.unmountComponentAtNode( document.body );
+ } );
+
+ describe( 'rendering', function() {
+ it( 'should render a set of checkboxes', function() {
+ var checkboxes = TestUtils.renderIntoDocument( ),
+ labels = TestUtils.scryRenderedDOMComponentsWithTag( checkboxes, 'label' );
+
+ assert.equal( options.length, labels.length );
+ labels.forEach( function( label, i ) {
+ var labelNode = label.getDOMNode(),
+ inputNode = labelNode.querySelector( 'input' );
+ assert.equal( 'favorite_colors[]', inputNode.name );
+ assert.equal( options[ i ].value, inputNode.value );
+ assert.equal( options[ i ].label, labelNode.textContent );
+ } );
+ } );
+
+ it( 'should accept an array of checked values', function() {
+ var checkboxes = TestUtils.renderIntoDocument( ),
+ labels = TestUtils.scryRenderedDOMComponentsWithTag( checkboxes, 'label' );
+
+ assert.equal( true, labels[0].getDOMNode().querySelector( 'input' ).checked );
+ assert.equal( false, labels[1].getDOMNode().querySelector( 'input' ).checked );
+ } );
+
+ it( 'should accept an array of defaultChecked', function() {
+ var checkboxes = TestUtils.renderIntoDocument( ),
+ labels = TestUtils.scryRenderedDOMComponentsWithTag( checkboxes, 'label' );
+
+ assert.equal( true, labels[0].getDOMNode().querySelector( 'input' ).checked );
+ assert.equal( false, labels[1].getDOMNode().querySelector( 'input' ).checked );
+ } );
+
+ it( 'should accept an onChange event handler', function( done ) {
+ var checkboxes = TestUtils.renderIntoDocument( ),
+ labels = TestUtils.scryRenderedDOMComponentsWithTag( checkboxes, 'label' );
+
+ TestUtils.Simulate.change( labels[0].getDOMNode().querySelector( 'input' ), {
+ target: {
+ value: options[0].value,
+ checked: true
+ }
+ } );
+
+ function finishTest( event ) {
+ assert.deepEqual( [ options[0].value ], event.value );
+ done();
+ }
+ } );
+
+ it( 'should accept a disabled boolean', function() {
+ var checkboxes = TestUtils.renderIntoDocument( ),
+ labels = TestUtils.scryRenderedDOMComponentsWithTag( checkboxes, 'label' );
+
+ assert.ok( labels[0].getDOMNode().querySelector( 'input' ).disabled );
+ assert.ok( labels[1].getDOMNode().querySelector( 'input' ).disabled );
+ } );
+
+ it( 'should transfer props to the rendered element', function() {
+ var className = 'transferred-class',
+ checkboxes = TestUtils.renderIntoDocument( ),
+ div = TestUtils.findRenderedDOMComponentWithTag( checkboxes, 'div' );
+
+ assert.notEqual( -1, div.getDOMNode().className.indexOf( className ) );
+ } );
+ } );
+} );
diff --git a/client/components/forms/range/Makefile b/client/components/forms/range/Makefile
new file mode 100644
index 00000000000000..bc38f1cff4a73f
--- /dev/null
+++ b/client/components/forms/range/Makefile
@@ -0,0 +1,7 @@
+REPORTER ?= spec
+MOCHA ?= ../../../../node_modules/.bin/mocha
+
+test:
+ @NODE_ENV=test NODE_PATH=test:../../../ $(MOCHA) --compilers jsx:babel/register --reporter $(REPORTER)
+
+.PHONY: test
diff --git a/client/components/forms/range/README.md b/client/components/forms/range/README.md
new file mode 100644
index 00000000000000..0a6c542d98225c
--- /dev/null
+++ b/client/components/forms/range/README.md
@@ -0,0 +1,38 @@
+Range
+=====
+
+Range is a React component used to render a range input field. It is essentially an enhanced version of ` `, enabling support for a value tooltip and content to be shown at the ends of the range field.
+
+
+
+## Usage
+
+Refer to the following code snippet for a typical usage example:
+
+```jsx
+ }
+ maxContent={ }
+ max="100"
+ value={ this.state.rangeValue }
+ onChange={ this.onChange }
+ showValueLabel={ true } />
+```
+
+The Range component does not track its own value state, much like any other form input in React. Refer to the React Forms documentation for more guidance on tracking form value state.
+
+## Props
+
+Props not listed below will be passed automatically to the rendered range input element.
+
+### `minContent` (`string` or `Element`)
+
+Content to be shown preceding the range input.
+
+### `maxContent` (`string` or `Element`)
+
+Content to be shown following the range input.
+
+### `showValueLabel` (`boolean`)
+
+A boolean indicating whether a tooltip is to be shown with the current range value.
diff --git a/client/components/forms/range/docs/example.jsx b/client/components/forms/range/docs/example.jsx
new file mode 100644
index 00000000000000..6099cddded7b16
--- /dev/null
+++ b/client/components/forms/range/docs/example.jsx
@@ -0,0 +1,44 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var FormRange = require( 'components/forms/range' );
+
+module.exports = React.createClass( {
+ displayName: 'Ranges',
+
+ mixins: [ React.addons.PureRenderMixin ],
+
+ getInitialState: function() {
+ return {
+ rangeValue: 24
+ };
+ },
+
+ onChange: function( event ) {
+ this.setState( {
+ rangeValue: event.target.value
+ } );
+ },
+
+ render: function() {
+ return (
+
+
+
}
+ maxContent={
}
+ max="100"
+ value={ this.state.rangeValue }
+ onChange={ this.onChange }
+ showValueLabel={ true } />
+
+ );
+ }
+} );
diff --git a/client/components/forms/range/index.jsx b/client/components/forms/range/index.jsx
new file mode 100644
index 00000000000000..1a936727e934bd
--- /dev/null
+++ b/client/components/forms/range/index.jsx
@@ -0,0 +1,98 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ omit = require( 'lodash/object/omit' ),
+ classnames = require( 'classnames' ),
+ uniqueId = require( 'lodash/utility/uniqueId' );
+
+/**
+ * External dependencies
+ */
+var FormRange = require( 'components/forms/form-range' );
+
+module.exports = React.createClass( {
+ displayName: 'Range',
+
+ propTypes: {
+ minContent: React.PropTypes.oneOfType( [ React.PropTypes.element, React.PropTypes.string ] ),
+ maxContent: React.PropTypes.oneOfType( [ React.PropTypes.element, React.PropTypes.string ] ),
+ min: React.PropTypes.oneOfType( [ React.PropTypes.string, React.PropTypes.number ] ),
+ max: React.PropTypes.oneOfType( [ React.PropTypes.string, React.PropTypes.number ] ),
+ value: React.PropTypes.oneOfType( [ React.PropTypes.string, React.PropTypes.number ] ),
+ showValueLabel: React.PropTypes.bool
+ },
+
+ getInitialState: function() {
+ return {
+ id: uniqueId( 'range' )
+ };
+ },
+
+ getDefaultProps: function() {
+ return {
+ min: 0,
+ max: 10,
+ value: 0,
+ showValueLabel: false
+ };
+ },
+
+ getMinContentElement: function() {
+ if ( this.props.minContent ) {
+ return { this.props.minContent } ;
+ }
+ },
+
+ getMaxContentElement: function() {
+ if ( this.props.maxContent ) {
+ return { this.props.maxContent } ;
+ }
+ },
+
+ getValueLabelElement: function() {
+ var left, offset;
+
+ if ( this.props.showValueLabel ) {
+ left = 100 * ( this.props.value - this.props.min ) / ( this.props.max - this.props.min );
+
+ // The center of the slider thumb is not aligned to the same
+ // percentage stops as an absolute positioned element will be.
+ // Therefore, we adjust based on the thumb's position relative to
+ // its own size. Ideally, we would use `getComputedStyle` here,
+ // but this method doesn't support the thumb pseudo-element in all
+ // browsers. The multiplier is equal to half of the thumb's width.
+ //
+ // Normal:
+ // v v v
+ // |( )----( )----( )|
+ //
+ // Adjusted:
+ // v v v
+ // |( )----( )----( )|
+ offset = Math.floor( 13 * ( ( 50 - left ) / 50 ) ); // 26px / 2 = 13px
+
+ return (
+
+ { this.props.value }
+
+ );
+ }
+ },
+
+ render: function() {
+ var classes = classnames( this.props.className, 'range', {
+ 'has-min-content': !! this.props.minContent,
+ 'has-max-content': !! this.props.maxContent
+ } );
+
+ return (
+
+ { this.getMinContentElement() }
+
+ { this.getMaxContentElement() }
+ { this.getValueLabelElement() }
+
+ );
+ }
+} );
diff --git a/client/components/forms/range/style.scss b/client/components/forms/range/style.scss
new file mode 100644
index 00000000000000..d63584e4301df4
--- /dev/null
+++ b/client/components/forms/range/style.scss
@@ -0,0 +1,65 @@
+$range-content-padding: 24px;
+
+.range {
+ position: relative;
+
+ &.has-min-content {
+ margin-left: $range-content-padding;
+ }
+
+ &.has-max-content {
+ margin-right: $range-content-padding;
+ }
+}
+
+.range__content {
+ position: absolute;
+ top: 50%;
+ transform: translateY( -50% );
+ color: $gray;
+
+ &.is-min {
+ left: ( -1 * $range-content-padding );
+ }
+
+ &.is-max {
+ right: ( -1 * $range-content-padding );
+ }
+}
+
+.range__content > * {
+ display: block;
+}
+
+.range__label {
+ position: absolute;
+ bottom: 100%;
+ z-index: 10;
+ transform: translateX( -50% );
+ padding-bottom: 5px;
+ pointer-events: none;
+
+ &::before {
+ content: "";
+ position: absolute;
+ bottom: 1px;
+ left: 50%;
+ display: block;
+ width: 8px;
+ height: 8px;
+ margin-left: -4px;
+ transform: rotate( 45deg );
+ background-color: $white;
+ border-right: 1px solid lighten( $gray,20 ) #{"/*rtl:ignore*/"};
+ border-bottom: 1px solid lighten( $gray,20 ) #{"/*rtl:ignore*/"};
+ }
+}
+
+.range__label-inner {
+ display: block;
+ padding: 8px 12px;
+ border: 1px solid lighten( $gray,20 );
+ border-radius: 2px;
+ background-color: $white;
+ box-shadow: 0px 5px 20px rgba( 0, 0, 0, .2 );
+}
diff --git a/client/components/forms/range/test/index.jsx b/client/components/forms/range/test/index.jsx
new file mode 100644
index 00000000000000..598d68dfee3985
--- /dev/null
+++ b/client/components/forms/range/test/index.jsx
@@ -0,0 +1,52 @@
+require( 'lib/react-test-env-setup' )();
+
+/**
+ * External dependencies
+ */
+var expect = require( 'chai' ).expect,
+ React = require( 'react/addons' ),
+ TestUtils = React.addons.TestUtils;
+
+/**
+ * Internal dependencies
+ */
+var FormRange = require( '../' );
+
+describe( 'Range', function() {
+ afterEach( function() {
+ React.unmountComponentAtNode( document.body );
+ } );
+
+ it( 'should render beginning content if passed a `minContent` prop', function() {
+ var range = TestUtils.renderIntoDocument( } /> );
+ TestUtils.findRenderedDOMComponentWithClass( range, 'noticon-minus' );
+ } );
+
+ it( 'should not render ending content if not passed a `maxContent` prop', function() {
+ var range = TestUtils.renderIntoDocument( } /> ),
+ content = TestUtils.scryRenderedDOMComponentsWithClass( range, 'range__content' );
+
+ expect( content ).to.have.length( 1 );
+ expect( content[0].props.className ).to.contain( 'is-min' );
+ } );
+
+ it( 'should render ending content if passed a `maxContent` prop', function() {
+ var range = TestUtils.renderIntoDocument( } /> );
+ TestUtils.findRenderedDOMComponentWithClass( range, 'noticon-plus' );
+ } );
+
+ it( 'should not render beginning content if not passed a `minContent` prop', function() {
+ var range = TestUtils.renderIntoDocument( } /> ),
+ content = TestUtils.scryRenderedDOMComponentsWithClass( range, 'range__content' );
+
+ expect( content ).to.have.length( 1 );
+ expect( content[0].props.className ).to.contain( 'is-max' );
+ } );
+
+ it( 'should render a value label if passed a truthy `showValueLabel` prop', function() {
+ var range = TestUtils.renderIntoDocument( ),
+ label = TestUtils.findRenderedDOMComponentWithClass( range, 'range__label' );
+
+ expect( label.getDOMNode().textContent ).to.equal( '8' );
+ } );
+} );
diff --git a/client/components/forms/select-opt-groups.jsx b/client/components/forms/select-opt-groups.jsx
new file mode 100644
index 00000000000000..3ea154a706766c
--- /dev/null
+++ b/client/components/forms/select-opt-groups.jsx
@@ -0,0 +1,32 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ debug = require( 'debug' )( 'calypso:forms:select-opt-groups' );
+
+var SelectOptGroups = React.createClass( {
+
+ displayName: 'SelectOptGroups',
+
+ componentWillMount: function() {
+ debug( 'Mounting SelectOptGroups React component.' );
+ },
+
+ render: function() {
+ return (
+
+ { this.props.optGroups.map( function( optGroup ) {
+ return (
+
+ { optGroup.options.map( function( option ) {
+ return { option.label } ;
+ })}
+
+ );
+ } ) }
+
+ );
+ }
+});
+
+module.exports = SelectOptGroups;
diff --git a/client/components/forms/sortable-list/README.md b/client/components/forms/sortable-list/README.md
new file mode 100644
index 00000000000000..bccca6a56a81ad
--- /dev/null
+++ b/client/components/forms/sortable-list/README.md
@@ -0,0 +1,71 @@
+SortableList
+===============
+
+SortableList is a React component to enable device-friendly item rearranging. For non-touch devices, child elements of SortableList can be rearranged by drag-and-drop. On touch devices, the user must tap an item to activate it before rearranging via one of two directional button controls.
+
+*Desktop*
+
+
+
+*Touch*
+
+
+
+## Usage
+
+Below is example usage for rendering an SortableList:
+
+```jsx
+
+ First
+ Second
+
+```
+
+In traditional React fashion, a SortableList does not track its own state, but instead expects you as the developer to track changes through an `onChange` handler, re-rendering the component with the updated element ordering. Refer to the following example:
+
+```jsx
+var SortableList = require( 'components/forms/sortable-list' );
+
+module.exports = React.createClass( {
+ getInitialState: function() {
+ return {
+ items: [ 'First', 'Second', 'Third' ];
+ };
+ },
+
+ onChange: function( order ) {
+ var items = [];
+
+ this.state.items.forEach( function( item, i ) {
+ items[ order[ i ] ] = item;
+ }, this );
+
+ this.setState( {
+ items: items
+ } );
+ },
+
+ render: function() {
+ var items = this.items.map( function( item ) {
+ return { item } ;
+ } );
+
+ return { items } ;
+ }
+} );
+```
+
+## Props
+
+### `direction`
+
+Accepts either "horizontal" (default) or "vertical". A horizontal SortableList is rendered from left to right and can wrap. A vertical SortableList is rendered from top to bottom.
+
+### `allowDrag`
+
+If dragging is not desired in any device context, pass an `allowDrag` value of `false`. Defaults to `true`.
+
+### `onChange`
+
+A change handler to invoke when the user has modified the ordering of elements. This function is passed a single array argument. Each index of the array aligns the original element ordering, and the value represents the element's new position on a zero-based index. Using the example above as a reference, if a user were to move "First" to be the second element in the list, you could expect an `onChange` argument of `[ 1, 0, 2 ]`.
diff --git a/client/components/forms/sortable-list/index.jsx b/client/components/forms/sortable-list/index.jsx
new file mode 100644
index 00000000000000..56aa8abd06076e
--- /dev/null
+++ b/client/components/forms/sortable-list/index.jsx
@@ -0,0 +1,320 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ zipObject = require( 'lodash/array/zipObject' ),
+ findIndex = require( 'lodash/array/findIndex' ),
+ assign = require( 'lodash/object/assign' ),
+ debug = require( 'debug' )( 'calypso:forms:sortable-list' ),
+ classNames = require( 'classnames' );
+
+/**
+ * Internal dependencies
+ */
+var touchDetect = require( 'lib/touch-detect' );
+
+module.exports = React.createClass( {
+ displayName: 'SortableList',
+
+ propTypes: {
+ direction: React.PropTypes.oneOf( [ 'horizontal', 'vertical' ] ),
+ allowDrag: React.PropTypes.bool,
+ onChange: React.PropTypes.func
+ },
+
+ getInitialState: function() {
+ return {
+ activeIndex: null,
+ activeOrder: null,
+ position: null
+ };
+ },
+
+ getDefaultProps: function() {
+ return {
+ direction: 'horizontal',
+ allowDrag: true,
+ onChange: function() {}
+ };
+ },
+
+ componentWillMount: function() {
+ debug( 'Mounting ' + this.constructor.displayName + ' React component.' );
+ },
+
+ componentDidMount: function() {
+ document.addEventListener( 'mousemove', this.onMouseMove );
+ },
+
+ componentWillUnmount: function() {
+ document.removeEventListener( 'mousemove', this.onMouseMove );
+ },
+
+ getPositionForCursorElement: function( element, event ) {
+ return {
+ top: event.clientY - ( element.clientHeight / 2 ),
+ left: event.clientX - ( element.clientWidth / 2 )
+ };
+ },
+
+ compareCursorVerticalToElement: function( element, event ) {
+ var rect = element.getBoundingClientRect();
+
+ if ( event.clientY < rect.top ) {
+ return -1;
+ } else if ( event.clientY > rect.bottom ) {
+ return 1;
+ } else {
+ return 0;
+ }
+ },
+
+ isCursorBeyondElementThreshold: function( element, direction, permittedVertical, event ) {
+ var rect = element.getBoundingClientRect();
+
+ // We check for Y bounds on right and left and not X bounds for top
+ // and bottom because horizontal lists can have line breaks, so we
+ // should be careful to consider vertical position in those cases
+ switch ( direction ) {
+ case 'top':
+ return event.clientY <= rect.top + ( rect.height / 2 );
+ case 'right':
+ return event.clientX >= rect.left + ( rect.width / 2 ) &&
+ ( 'top' === permittedVertical || event.clientY >= rect.top ) &&
+ ( 'bottom' === permittedVertical || event.clientY <= rect.bottom );
+ case 'bottom':
+ return event.clientY >= rect.top + ( rect.height / 2 );
+ case 'left':
+ return event.clientX <= rect.left + ( rect.width / 2 ) &&
+ ( 'top' === permittedVertical || event.clientY >= rect.top ) &&
+ ( 'bottom' === permittedVertical || event.clientY <= rect.bottom );
+ default:
+ return false;
+ }
+ },
+
+ getAdjustedElementIndex: function( index ) {
+ // The active order array is used as an array where each index matches
+ // the original prop children indices, but the values correspond to
+ // their visible position index
+ if ( this.state.activeOrder ) {
+ return this.state.activeOrder[ index ];
+ } else {
+ return index;
+ }
+ },
+
+ getCursorElementIndex: function( event ) {
+ var cursorCompare = this.compareCursorVerticalToElement( this.refs.list.getDOMNode(), event ),
+ adjustedActiveIndex = this.getAdjustedElementIndex( this.state.activeIndex ),
+ shadowRect = this.refs[ 'wrap-shadow-' + this.state.activeIndex ].getDOMNode().getBoundingClientRect(),
+ index;
+
+ index = findIndex( this.props.children, function( child, i ) {
+ var isBeyond, adjustedElementIndex, permittedVertical;
+
+ // Avoid self-comparisons for the active item
+ if ( i === this.state.activeIndex ) {
+ return false;
+ }
+
+ // Since elements are now shifted around, we want to find their
+ // visible position to make accurate comparisons
+ adjustedElementIndex = this.getAdjustedElementIndex( i );
+
+ // When rearranging on a horizontal plane, permit breaking of
+ // vertical if the cursor is outside the list element on the
+ // same vertical, and only if the element is on the same line as
+ // the active item's shadow element
+ if ( 'horizontal' === this.props.direction ) {
+ if ( 1 === cursorCompare && this.refs[ 'wrap-' + i ].getDOMNode().getBoundingClientRect().top >= shadowRect.top ) {
+ permittedVertical = 'bottom';
+ } else if ( -1 === cursorCompare && this.refs[ 'wrap-' + i ].getDOMNode().getBoundingClientRect().bottom <= shadowRect.bottom ) {
+ permittedVertical = 'top';
+ }
+ }
+
+ if ( adjustedElementIndex < adjustedActiveIndex ) {
+ // If the item which is currently before the active item is
+ // suddenly after, return this item's index
+ isBeyond = this.isCursorBeyondElementThreshold(
+ this.refs[ 'wrap-' + i ].getDOMNode(),
+ 'horizontal' === this.props.direction ? 'left' : 'top',
+ permittedVertical,
+ event
+ );
+ } else if ( adjustedElementIndex > adjustedActiveIndex ) {
+ // If the item which is currently after the active item is
+ // suddenly before, return this item's index
+ isBeyond = isBeyond || this.isCursorBeyondElementThreshold(
+ this.refs[ 'wrap-' + i ].getDOMNode(),
+ 'horizontal' === this.props.direction ? 'right' : 'bottom',
+ permittedVertical,
+ event
+ );
+ }
+
+ return isBeyond;
+ }.bind( this ) );
+
+ return this.getAdjustedElementIndex( index );
+ },
+
+ moveItem: function( direction ) {
+ var increment = 'previous' === direction ? -1 : 1,
+ activeOrder = Object.keys( this.props.children ).map( Number );
+
+ activeOrder[ this.state.activeIndex + increment ] = this.state.activeIndex;
+ activeOrder[ this.state.activeIndex ] = this.state.activeIndex + increment;
+
+ this.props.onChange( activeOrder );
+
+ this.setState( {
+ activeIndex: activeOrder[ this.state.activeIndex ]
+ } );
+ },
+
+ onMouseDown: function( index, event ) {
+ this.setState( {
+ activeIndex: index,
+ position: this.getPositionForCursorElement( event.currentTarget.firstChild, event )
+ } );
+ },
+
+ onMouseMove: function( event ) {
+ var activeOrder, newIndex;
+ if ( null === this.state.activeIndex || ! this.props.allowDrag || touchDetect.hasTouch() ) {
+ return;
+ }
+
+ activeOrder = this.state.activeOrder;
+
+ // Find the new cursor location
+ newIndex = this.getCursorElementIndex( event );
+ if ( newIndex >= 0 ) {
+ if ( this.state.activeIndex === newIndex ) {
+ // If we're changing the index back to the active item's
+ // original position, we can shortcut this by simply
+ // setting the order back to default
+ activeOrder = null;
+ } else {
+ // Create an ordered array of items using the index from
+ // the child props array
+ activeOrder = Object.keys( this.props.children ).map( Number );
+
+ for ( var i = 0, il = activeOrder.length; i < il; i++ ) {
+ if ( i >= newIndex && i < this.state.activeIndex ) {
+ // Bump up any item below the active index and
+ // above the new index
+ activeOrder[ i ] = i + 1;
+ } else if ( i <= newIndex && i > this.state.activeIndex ) {
+ // Bump down any item above the active index
+ // and below the new index
+ activeOrder[ i ] = i - 1;
+ }
+ }
+
+ // Set the new index for the active item
+ activeOrder[ this.state.activeIndex ] = newIndex;
+ }
+ }
+
+ this.setState( {
+ position: this.getPositionForCursorElement( this.refs[ 'wrap-' + this.state.activeIndex ].getDOMNode().firstChild, event ),
+ activeOrder: activeOrder
+ } );
+ },
+
+ onMouseUp: function() {
+ if ( this.state.activeOrder ) {
+ this.props.onChange( this.state.activeOrder );
+ }
+
+ this.setState( {
+ activeIndex: null,
+ activeOrder: null,
+ position: null
+ } );
+ },
+
+ onClick: function( index ) {
+ this.setState( {
+ activeIndex: index
+ } );
+ },
+
+ getOrderedListItemElements: function() {
+ return React.Children.map( this.props.children, function( child, index ) {
+ var isActive = this.state.activeIndex === index,
+ isDraggable = this.props.allowDrag && ! touchDetect.hasTouch(),
+ events = isDraggable ? [ 'onMouseDown', 'onMouseUp' ] : [ 'onClick' ],
+ style = { order: this.getAdjustedElementIndex( index ) },
+ classes = classNames( {
+ 'sortable-list__item': true,
+ 'is-active': isActive,
+ 'is-draggable': isDraggable
+ } ), item;
+
+ events = zipObject( events.map( function( event ) {
+ return [ event, this[ event ].bind( null, index ) ];
+ }, this ) );
+
+ if ( isActive ) {
+ assign( style, this.state.position );
+ }
+
+ item = { child } ;
+
+ if ( isActive && isDraggable ) {
+ return [
+ { child } ,
+ item
+ ];
+ } else {
+ return item;
+ }
+ }, this );
+ },
+
+ getNavigationElement: function() {
+ if ( this.props.allowDrag && ! touchDetect.hasTouch() ) {
+ return;
+ }
+
+ return (
+
+
+ { this.translate( 'Move previous' ) }
+
+
+
+ { this.translate( 'Move next' ) }
+
+
+
+ );
+ },
+
+ render: function() {
+ var classes = classNames( {
+ 'sortable-list': true,
+ 'is-horizontal': 'horizontal' === this.props.direction,
+ 'is-vertical': 'vertical' === this.props.direction
+ } );
+
+ return (
+
+
{ this.getOrderedListItemElements() }
+ { this.getNavigationElement() }
+
+ );
+ }
+} );
diff --git a/client/components/forms/sortable-list/index.scss b/client/components/forms/sortable-list/index.scss
new file mode 100644
index 00000000000000..e3d43e4bbad207
--- /dev/null
+++ b/client/components/forms/sortable-list/index.scss
@@ -0,0 +1,106 @@
+.sortable-list__list {
+ display: flex;
+ margin: 0;
+ user-select: none;
+}
+
+.sortable-list.is-horizontal .sortable-list__list {
+ flex-direction: row;
+ flex-wrap: wrap;
+}
+
+.sortable-list.is-vertical .sortable-list__list {
+ flex-direction: column;
+}
+
+.sortable-list__item {
+ display: inline-block;
+
+ &.is-active > * {
+ box-shadow: 0 0 0 2px white, 0 0 0 4px $blue-medium;
+ }
+
+ &.is-draggable.is-active {
+ position: fixed;
+ z-index: 1000;
+ }
+
+ &.is-shadow {
+ filter: url( "data:image/svg+xml;utf8, #grayscale" );
+ filter: grayscale( 100% );
+ opacity: 0.5;
+ }
+
+ &.is-draggable > * {
+ cursor: move;
+ box-shadow: none;
+ }
+}
+
+.sortable-list__navigation {
+ margin-top: 18px;
+ text-align: right;
+}
+
+.sortable-list__navigation-button {
+ padding: 8px;
+ background-color: lighten( $gray, 33% );
+ border: 1px solid lighten( $gray, 20% );
+ color: $gray;
+
+ &:not( :disabled ):hover {
+ cursor: pointer;
+ color: $blue-medium;
+ }
+
+ &:disabled {
+ cursor: not-allowed;
+ opacity: 0.4;
+ }
+}
+
+.sortable-list.is-horizontal .sortable-list__navigation-button {
+ display: inline-block;
+
+ &.is-previous {
+ padding-left: 12px;
+ border-top-left-radius: 50%;
+ border-bottom-left-radius: 50%;
+ }
+
+ &.is-next {
+ margin-left: -1px;
+ padding-right: 12px;
+ border-top-right-radius: 50%;
+ border-bottom-right-radius: 50%;
+ }
+}
+
+.sortable-list.is-horizontal .sortable-list__navigation-button .noticon {
+ transform: rotate( 90deg ) translateX( 1px );
+}
+
+.sortable-list.is-vertical .sortable-list__navigation-button {
+ display: block;
+ margin-left: auto;
+
+ &.is-previous {
+ border-top-right-radius: 50%;
+ border-top-left-radius: 50%;
+ }
+
+ &.is-next {
+ margin-top: -1px;
+ border-bottom-right-radius: 50%;
+ border-bottom-left-radius: 50%;
+ }
+}
+
+.sortable-list.is-vertical .sortable-list__navigation-button .noticon {
+ transform: rotate( 180deg ) translateX( 1px );
+}
+
+.sortable-list__navigation-button .noticon {
+ font-size: 24px;
+ font-weight: bold;
+}
diff --git a/client/components/forms/us-state-selector.jsx b/client/components/forms/us-state-selector.jsx
new file mode 100644
index 00000000000000..4a50a24c75e4a0
--- /dev/null
+++ b/client/components/forms/us-state-selector.jsx
@@ -0,0 +1,102 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ debug = require( 'debug' )( 'calypso:forms:state-selector' );
+
+/**
+ * Internal dependencies
+ */
+var SelectOptGroups = require( 'components/forms/select-opt-groups' );
+
+var USStateSelector = React.createClass( {
+
+ displayName: 'USStateSelector',
+
+ componentWillMount: function() {
+ debug( 'Mounting USStateSelector React component.' );
+ },
+
+ render: function() {
+ var states, territories, stateGroup;
+
+ states = [
+ { value: 'AL', label: this.translate( 'Alabama' ) },
+ { value: 'AK', label: this.translate( 'Alaska' ) },
+ { value: 'AZ', label: this.translate( 'Arizona' ) },
+ { value: 'AR', label: this.translate( 'Arkansas' ) },
+ { value: 'CA', label: this.translate( 'California' ) },
+ { value: 'CO', label: this.translate( 'Colorado' ) },
+ { value: 'CT', label: this.translate( 'Connecticut' ) },
+ { value: 'DE', label: this.translate( 'Delaware' ) },
+ { value: 'DC', label: this.translate( 'District of Columbia' ) },
+ { value: 'FL', label: this.translate( 'Florida' ) },
+ { value: 'GA', label: this.translate( 'Georgia' ) },
+ { value: 'HI', label: this.translate( 'Hawaii' ) },
+ { value: 'ID', label: this.translate( 'Idaho' ) },
+ { value: 'IL', label: this.translate( 'Illinois' ) },
+ { value: 'IN', label: this.translate( 'Indiana' ) },
+ { value: 'IA', label: this.translate( 'Iowa' ) },
+ { value: 'KS', label: this.translate( 'Kansas' ) },
+ { value: 'KY', label: this.translate( 'Kentucky' ) },
+ { value: 'LA', label: this.translate( 'Louisiana' ) },
+ { value: 'ME', label: this.translate( 'Maine' ) },
+ { value: 'MD', label: this.translate( 'Maryland' ) },
+ { value: 'MA', label: this.translate( 'Massachusetts' ) },
+ { value: 'MI', label: this.translate( 'Michigan' ) },
+ { value: 'MN', label: this.translate( 'Minnesota' ) },
+ { value: 'MS', label: this.translate( 'Mississippi' ) },
+ { value: 'MO', label: this.translate( 'Missouri' ) },
+ { value: 'MT', label: this.translate( 'Montana' ) },
+ { value: 'NE', label: this.translate( 'Nebraska' ) },
+ { value: 'NV', label: this.translate( 'Nevada' ) },
+ { value: 'NH', label: this.translate( 'New Hampshire' ) },
+ { value: 'NJ', label: this.translate( 'New Jersey' ) },
+ { value: 'NM', label: this.translate( 'New Mexico' ) },
+ { value: 'NY', label: this.translate( 'New York' ) },
+ { value: 'NC', label: this.translate( 'North Carolina' ) },
+ { value: 'ND', label: this.translate( 'North Dakota' ) },
+ { value: 'OH', label: this.translate( 'Ohio' ) },
+ { value: 'OK', label: this.translate( 'Oklahoma' ) },
+ { value: 'OR', label: this.translate( 'Oregon' ) },
+ { value: 'PA', label: this.translate( 'Pennsylvania' ) },
+ { value: 'RI', label: this.translate( 'Rhode Island' ) },
+ { value: 'SC', label: this.translate( 'South Carolina' ) },
+ { value: 'SD', label: this.translate( 'South Dakota' ) },
+ { value: 'TN', label: this.translate( 'Tennessee' ) },
+ { value: 'TX', label: this.translate( 'Texas' ) },
+ { value: 'UT', label: this.translate( 'Utah' ) },
+ { value: 'VT', label: this.translate( 'Vermont' ) },
+ { value: 'VA', label: this.translate( 'Virginia' ) },
+ { value: 'WA', label: this.translate( 'Washington' ) },
+ { value: 'WV', label: this.translate( 'West Virginia' ) },
+ { value: 'WI', label: this.translate( 'Wisconsin' ) },
+ { value: 'WY', label: this.translate( 'Wyoming' ) }
+ ];
+
+ territories = [
+ { value: 'AA', label: this.translate( 'Armed Forces Americas' ) },
+ { value: 'AE', label: this.translate( 'Armed Forces Europe, Middle East, & Canada' ) },
+ { value: 'AP', label: this.translate( 'Armed Forces Pacific' ) },
+ { value: 'AS', label: this.translate( 'American Samoa' ) },
+ { value: 'FM', label: this.translate( 'Federated States of Micronesia' ) },
+ { value: 'GU', label: this.translate( 'Guam' ) },
+ { value: 'MH', label: this.translate( 'Marshall Islands' ) },
+ { value: 'MP', label: this.translate( 'Northern Mariana Islands' ) },
+ { value: 'PW', label: this.translate( 'Palau' ) },
+ { value: 'PR', label: this.translate( 'Puerto Rico' ) },
+ { value: 'VI', label: this.translate( 'Virgin Islands' ) }
+ ];
+
+ stateGroup = [
+ { label: this.translate( 'States' ), options: states },
+ { label: this.translate( 'Territories' ), options: territories }
+ ];
+
+ return (
+
+ );
+ }
+});
+
+module.exports = USStateSelector;
diff --git a/client/components/gallery-shortcode/README.md b/client/components/gallery-shortcode/README.md
new file mode 100644
index 00000000000000..51ef80d0806461
--- /dev/null
+++ b/client/components/gallery-shortcode/README.md
@@ -0,0 +1,103 @@
+Gallery Shortcode
+=================
+
+Gallery Shortcode is a React component used in displaying galleries. It makes use of the [Shortcode component](../shortcode), rendering an `` element containing the exact output as would be rendered to the site.
+
+## Usage
+
+Simply pass a site ID and an array of media items.
+
+```jsx
+import React from 'react';
+import GalleryShortcode from 'components/gallery-shortcode';
+
+export default React.createClass( {
+ displayName: 'MyComponent',
+
+ render() {
+ return (
+
+ );
+ }
+} );
+```
+
+## Props
+
+The following props can be passed to the GalleryShortcode component. If a `className` is passed, it will be added to the rendered `.gallery-shortcode` element.
+
+### `siteId`
+
+
+ Type Number
+ Required Yes
+
+
+The site ID for which to render the shortcode.
+
+### `items`
+
+
+ Type Array<Media>
+ Required No
+ Default []
+
+
+The media items to include in the rendered gallery.
+
+### `type`
+
+
+ Type String
+ Required No
+ Default "default"
+
+
+The rendered style of the gallery. Available options include `default` (Thumbnail Grid), `rectangle` (Tiled Mosaic), `square` (Square Tiles), `circle` (Circles), `columns` (Tiled Columns), and `slideshow` (Slideshow). Defaults to `default`.
+
+### `columns`
+
+
+ Type Number
+ Required No
+ Default 3
+
+
+The number of columns. The gallery will include a break tag at the end of each row, and calculate the column width as appropriate.
+
+### `orderBy`
+
+
+ Type String
+ Required No
+ Default "menu_order"
+
+
+The rendered order of the gallery. Available options include `menu_order` (order specified by the media modal), `title` (order by the title of the image in the Media Library), `post_date` (sort by date/time uploaded), `rand` (order randomly) and `ID` (order by media item ID). Defaults to `menu_order`.
+
+### `link`
+
+
+ Type String
+ Required No
+ Default ''
+
+
+The type of link that each image will link to. Available options include `''` (Empty string which specifies that the link goes to the image's attachment page), `file` (Link to the image file), `none` (No link). Defaults to `''` (attachment page).
+
+### `size`
+
+
+ Type String
+ Required No
+ Default thumbnail
+
+
+The image size to use for the gallery thumbnail display. Available options include `thumbnail`, `medium`, `large`, `full` and any other additional image size that was registered with on the site. Defaults to `thumbnail`.
+
+## Resources
+
+More information about the gallery shortcode can be found [at the WordPress Codex](https://codex.wordpress.org/Gallery_Shortcode).
diff --git a/client/components/gallery-shortcode/index.jsx b/client/components/gallery-shortcode/index.jsx
new file mode 100644
index 00000000000000..36424d3660f5f8
--- /dev/null
+++ b/client/components/gallery-shortcode/index.jsx
@@ -0,0 +1,124 @@
+/**
+ * External dependencies
+ */
+import React, { PropTypes } from 'react';
+import classNames from 'classnames';
+import debugModule from 'debug';
+import assign from 'lodash/object/assign';
+import pick from 'lodash/object/pick';
+import omit from 'lodash/object/omit';
+
+/**
+ * Internal dependencies
+ */
+import Shortcode from 'components/shortcode';
+import { parse as parseShortcode } from 'lib/shortcode';
+import MediaUtils from 'lib/media/utils';
+import { GalleryDefaultAttrs } from 'lib/media/constants';
+
+/**
+ * Module variables
+ */
+const debug = debugModule( 'calypso:gallery-shortcode' );
+
+export default React.createClass( {
+ displayName: 'GalleryShortcode',
+
+ propTypes: {
+ siteId: PropTypes.number.isRequired,
+ children: PropTypes.string,
+ items: PropTypes.array,
+ type: PropTypes.string,
+ columns: PropTypes.number,
+ orderBy: PropTypes.string,
+ link: PropTypes.string,
+ size: PropTypes.string,
+ className: PropTypes.string
+ },
+
+ getDefaultProps() {
+ return GalleryDefaultAttrs;
+ },
+
+ filterRenderResult( rendered ) {
+ if ( ! rendered.body && ! rendered.scripts && ! rendered.styles ) {
+ return rendered;
+ }
+
+ const filtered = assign( {}, rendered, {
+ scripts: {
+ 'tiled-gallery': {
+ src: 'https://s0.wp.com/wp-content/mu-plugins/tiled-gallery/tiled-gallery.js'
+ }
+ },
+ styles: {
+ 'tiled-gallery': {
+ src: 'https://s0.wp.com/wp-content/mu-plugins/tiled-gallery/tiled-gallery.css'
+ },
+ 'gallery-styles': {
+ src: 'https://widgets.wp.com/gallery-preview/style.css'
+ }
+ }
+ } );
+
+ if ( 'slideshow' === this.getAttributes().type ) {
+ assign( filtered, {
+ scripts: {
+ 'jquery-cycle': {
+ src: 'https://s0.wp.com/wp-content/mu-plugins/shortcodes/js/jquery.cycle.js'
+ },
+ 'jetpack-slideshow': {
+ src: 'https://s0.wp.com/wp-content/mu-plugins/shortcodes/js/slideshow-shortcode.js',
+ extra: 'var jetpackSlideshowSettings = { "spinner": "https://s0.wp.com/wp-content/mu-plugins/shortcodes/img/slideshow-loader.gif" };'
+ }
+ },
+ styles: {
+ 'jetpack-slideshow': {
+ src: 'https://s0.wp.com/wp-content/mu-plugins/shortcodes/css/slideshow-shortcode.css'
+ }
+ }
+ } );
+ }
+
+ return filtered;
+ },
+
+ getAttributes() {
+ let attributes = pick( this.props, 'items', 'type', 'columns', 'orderBy', 'link', 'size' );
+
+ if ( this.props.children ) {
+ assign( attributes, parseShortcode( this.props.children ).attrs.named );
+ }
+
+ return attributes;
+ },
+
+ getShortcode() {
+ if ( this.props.children ) {
+ return this.props.children;
+ }
+
+ return MediaUtils.generateGalleryShortcode( this.getAttributes() );
+ },
+
+ render() {
+ const shortcode = this.getShortcode();
+ if ( ! shortcode ) {
+ return null;
+ }
+
+ const classes = classNames( 'gallery-shortcode', this.props.className );
+
+ debug( shortcode );
+
+ return (
+
+ { shortcode }
+
+ );
+ }
+} );
diff --git a/client/components/gauge/README.md b/client/components/gauge/README.md
new file mode 100644
index 00000000000000..0553ec2626cff1
--- /dev/null
+++ b/client/components/gauge/README.md
@@ -0,0 +1,31 @@
+Gauge
+======
+
+This component renders a simple gauge using a ` ` element that shows a percentage visually.
+
+#### How to use:
+
+```js
+var Gauge = require( 'components/gauge' );
+
+render: function() {
+ return (
+
+ );
+}
+```
+
+#### Required Props
+
+* `percentage`: a numeric percentage between 0-100.
+
+#### Optional Props
+
+The following props may also be used. Default values are shown inside [].
+
+* `width`: [ 100 ] numeric width of canvas
+* `height`: [ 100 ] numeric height of canvas
+* `lineWidth`: [ 14 ] numeric width of arc stroke in the canvas
+* `labelSize`: [ 20 ] numeric size used for `px` of the label
+* `colors`: [ '#c8d7e1', '#004069' ] array of colors used for the arcs. First value is the background color, second value is used to show percentage
+* `metric`: text label for the numerical value above it
diff --git a/client/components/gauge/docs/example.jsx b/client/components/gauge/docs/example.jsx
new file mode 100644
index 00000000000000..657ead5ef946e6
--- /dev/null
+++ b/client/components/gauge/docs/example.jsx
@@ -0,0 +1,26 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var Gauge = require( 'components/gauge' );
+
+module.exports = React.createClass( {
+ displayName: 'Gauge',
+
+ mixins: [ React.addons.PureRenderMixin ],
+
+ render: function() {
+ return (
+
+ );
+ }
+} );
diff --git a/client/components/gauge/index.jsx b/client/components/gauge/index.jsx
new file mode 100644
index 00000000000000..c87a49b55e375b
--- /dev/null
+++ b/client/components/gauge/index.jsx
@@ -0,0 +1,91 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react/addons' ),
+ PureRenderMixin = React.addons.PureRenderMixin;
+
+module.exports = React.createClass( {
+ displayName: 'Gauge',
+
+ mixins: [ PureRenderMixin ],
+
+ propTypes: {
+ percentage: React.PropTypes.number.isRequired,
+ width: React.PropTypes.number,
+ height: React.PropTypes.number,
+ colors: React.PropTypes.array,
+ lineWidth: React.PropTypes.number,
+ metric: React.PropTypes.string.isRequired
+ },
+
+ getDefaultProps: function() {
+ return {
+ width: 118,
+ height: 118,
+ lineWidth: 9,
+ labelSize: 32,
+ colors: [ '#e9eff3', '#00aadc' ]
+ };
+ },
+
+ componentDidUpdate: function() {
+ var canvas = this.refs.canvas.getDOMNode(),
+ ctx = canvas.getContext( '2d' );
+
+ ctx.clearRect( 0, 0, this.props.width, this.props.height );
+ this.drawArcs();
+ },
+
+ componentDidMount: function() {
+ this.drawArcs();
+ },
+
+ drawArcs: function() {
+ var canvas = this.refs.canvas.getDOMNode(),
+ x = ( this.props.width / 2 ),
+ y = ( this.props.height / 2 ),
+ ctx = canvas.getContext( '2d' ),
+ startAngle = ( 0.8 * Math.PI ),
+ endAngle = ( 2.2 * Math.PI ),
+ valueEndAngle = ( 0.8 + ( 1.4 * ( this.props.percentage / 100 ) ) ) * Math.PI,
+ radius = x - ( this.props.lineWidth / 2 ),
+ angleData = [ endAngle, valueEndAngle ];
+
+ angleData.forEach( function( angle, idx ) {
+ ctx.beginPath();
+ ctx.arc( x, y, radius, startAngle, angle, false );
+ ctx.lineWidth = this.props.lineWidth;
+ ctx.strokeStyle = this.props.colors[ idx ];
+ ctx.lineCap = 'round';
+ ctx.stroke();
+ }, this );
+ },
+
+ render: function() {
+ var wrapperStyles = {
+ width: this.props.width,
+ height: this.props.height
+ },
+ labelStyles = {
+ color: this.props.colors[ 1 ],
+ fontSize: this.props.labelSize + 'px'
+ },
+ labelTop,
+ label = this.props.percentage + '%';
+
+ // style the label
+ labelStyles.color = this.props.colors[ 1 ];
+ labelTop = ( this.props.height / 2 ) + this.props.labelSize;
+ labelStyles.top = '-' + labelTop + 'px';
+
+ return (
+
+
+
+ { label }
+ { this.props.metric }
+
+
+ );
+ }
+} );
diff --git a/client/components/gauge/style.scss b/client/components/gauge/style.scss
new file mode 100644
index 00000000000000..ceedb3fb211710
--- /dev/null
+++ b/client/components/gauge/style.scss
@@ -0,0 +1,22 @@
+.gauge__label {
+ display: block;
+ position: relative;
+ text-align: center;
+
+ .gauge__number {
+ color: darken( $gray, 30% );
+ display: block;
+ margin-top: -5px;
+ }
+
+ .gauge__metric {
+ display: block;
+ color: darken( $gray, 20% );
+ text-transform: uppercase;
+ font-size: 10px;
+ letter-spacing: 0.1em;
+ font-weight: bold;
+ line-height: .7;
+ }
+
+}
\ No newline at end of file
diff --git a/client/components/gravatar/Makefile b/client/components/gravatar/Makefile
new file mode 100644
index 00000000000000..b68d291769d5c1
--- /dev/null
+++ b/client/components/gravatar/Makefile
@@ -0,0 +1,7 @@
+REPORTER ?= spec
+MOCHA ?= ../../../node_modules/.bin/mocha
+
+test:
+ @NODE_ENV=test NODE_PATH=test:../../../client $(MOCHA) --compilers jsx:babel/register --reporter $(REPORTER)
+
+.PHONY: test
diff --git a/client/components/gravatar/README.md b/client/components/gravatar/README.md
new file mode 100644
index 00000000000000..5ee8724445319a
--- /dev/null
+++ b/client/components/gravatar/README.md
@@ -0,0 +1,23 @@
+Gravatar
+======
+
+This component is used to display the [Gravatar](https://gravatar.com/) for a user. It takes a User object as a prop and read the images from user.avatar_URL. The images size is set at 96px, used at smaller sizes for retina display. Using one size allows us to only request one image and cache it on the browser. Even if you are displaying it at smaller sizes you should not change the source image.
+
+#### How to use:
+
+```js
+var Gravatar = require( 'components/gravatar' );
+
+render: function() {
+ return (
+
+ );
+}
+```
+
+#### Props
+
+* `user`: a User object. Not passing a user puts the component in "placeholder" mode.
+* `alt`: (default: User's display_name) By default the alt text will be User's name, but this can be overridden.
+* `size`: (default: 32) change the requested icon size.
+* `imgSize`: (default: 96) change the source image size. This should not be changed unless there's a valid requirement, as 96 is most commonly cached.
diff --git a/client/components/gravatar/index.jsx b/client/components/gravatar/index.jsx
new file mode 100644
index 00000000000000..1e0565c67f0057
--- /dev/null
+++ b/client/components/gravatar/index.jsx
@@ -0,0 +1,62 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ url = require( 'url' ),
+ qs = require( 'querystring' );
+
+/**
+ * Internal dependencies
+ */
+var safeImageURL = require( 'lib/safe-image-url' );
+
+module.exports = React.createClass( {
+ displayName: 'Gravatar',
+
+ propTypes: {
+ user: React.PropTypes.object,
+ size: React.PropTypes.number,
+ imgSize: React.PropTypes.number
+ },
+
+ getDefaultProps: function() {
+ // The REST-API returns s=96 by default, so that is most likely to be cached
+ return {
+ imgSize: 96,
+ size: 32
+ };
+ },
+
+ _getResizedImageURL: function( imageURL ) {
+ var parsedURL, query;
+
+ imageURL = imageURL || 'https://www.gravatar.com/avatar/0';
+ parsedURL = url.parse( imageURL );
+ query = qs.parse( parsedURL.query );
+ if ( /^([-a-zA-Z0-9_]+\.)*(gravatar.com)$/.test( parsedURL.hostname ) ) {
+ query.s = this.props.imgSize;
+ query.d = 'mm';
+ } else {
+ // assume photon
+ query.resize = this.props.imgSize + ',' + this.props.imgSize;
+ }
+ parsedURL.search = qs.stringify( query );
+ return url.format( parsedURL );
+ },
+
+ render: function() {
+ const size = this.props.size;
+
+ if ( ! this.props.user ) {
+ return ;
+ }
+
+ const alt = this.props.alt || this.props.user.display_name;
+ const avatarURL = this._getResizedImageURL( safeImageURL( this.props.user.avatar_URL ) );
+
+ return (
+
+ );
+ }
+
+} );
diff --git a/client/components/gravatar/style.scss b/client/components/gravatar/style.scss
new file mode 100644
index 00000000000000..d84ab0bfd2c0a8
--- /dev/null
+++ b/client/components/gravatar/style.scss
@@ -0,0 +1,13 @@
+/**
+ * @component Gravatar
+ */
+
+.gravatar {
+ border-radius: 50%;
+
+ &.is-placeholder {
+ background: lighten( $gray, 20% );
+ display: inline-block;
+ animation: pulse-light 0.8s ease-in-out infinite;
+ }
+}
diff --git a/client/components/gravatar/test/index.jsx b/client/components/gravatar/test/index.jsx
new file mode 100644
index 00000000000000..da728eeed1f4c2
--- /dev/null
+++ b/client/components/gravatar/test/index.jsx
@@ -0,0 +1,73 @@
+/**
+ * Pass in a react-generated html string to remove react-specific attributes
+ * to make it easier to compare to expected html structure
+ */
+function stripReactAttributes( string ) {
+ return string.replace( /\sdata\-(reactid|react\-checksum)\=\"[^\"]+\"/g, '' );
+}
+
+/**
+ * External dependencies
+ */
+var assert = require( 'assert' ),
+ React = require( 'react/addons' );
+
+/**
+ * Internal dependencies
+ */
+var Gravatar = require( '../' );
+
+describe( 'Gravatar', function() {
+
+ var bobTester = {
+ avatar_URL: "https://0.gravatar.com/avatar/cf55adb1a5146c0a11a808bce7842f7b?s=96&d=identicon",
+ display_name: "Bob The Tester"
+ };
+
+ describe( 'rendering', function() {
+ it( 'should render an image given a user with valid avatar_URL, with default width and height 32', function() {
+ var gravatar = ,
+ expectedResultString = ' ';
+
+ assert.equal( expectedResultString, stripReactAttributes( React.renderToString( gravatar ) ) );
+ } );
+
+ it( 'should update the width and height when given a size attribute', function() {
+ var gravatar = ,
+ expectedResultString = ' ';
+
+ assert.equal( expectedResultString, stripReactAttributes( React.renderToString( gravatar ) ) );
+ } );
+
+ it( 'should update source image when given imgSize attribute', function() {
+ var gravatar = ,
+ expectedResultString = ' ';
+
+ assert.equal( expectedResultString, stripReactAttributes( React.renderToString( gravatar ) ) );
+ } );
+
+ it( 'should serve a default image if no avatar_URL available', function() {
+ var noImageTester = { display_name: "Bob The Tester" },
+ gravatar = ,
+ expectedResultString = ' ';
+
+ assert.equal( expectedResultString, stripReactAttributes( React.renderToString( gravatar ) ) );
+ } );
+
+ it( 'should allow overriding the alt attribute', function() {
+ var gravatar = ,
+ expectedResultString = ' ';
+
+ assert.equal( expectedResultString, stripReactAttributes( React.renderToString( gravatar ) ) );
+ } );
+
+ // I believe jetpack sites could have custom avatars, so can't assume it's always a gravatar
+ it( 'should promote non-secure avatar urls to secure', function() {
+ var nonSecureTester = { avatar_URL: "http://www.example.com/avatar" },
+ gravatar = ,
+ expectedResultString = ' ';
+
+ assert.equal( expectedResultString, stripReactAttributes( React.renderToString( gravatar ) ) );
+ } );
+ } );
+} );
diff --git a/client/components/header-cake/README.md b/client/components/header-cake/README.md
new file mode 100644
index 00000000000000..562b72bdb131cc
--- /dev/null
+++ b/client/components/header-cake/README.md
@@ -0,0 +1,17 @@
+Back Button aka Header Cake
+===========================
+
+## Usage
+
+```
+ var HeaderCake = require( 'components/header-cake' );
+
+ Button Text
+```
+
+## Props
+
+* `onClick` - Function to trigger when the back text is clicked
+* `onTitleClick` - Function to trigger when the title is clicked
+* `backText` - React Element or string to use in place of default "Back" text
+* `isCompact` - Optional variant of a more visually compact header cake
diff --git a/client/components/header-cake/docs/example.jsx b/client/components/header-cake/docs/example.jsx
new file mode 100644
index 00000000000000..db6bb7b830796c
--- /dev/null
+++ b/client/components/header-cake/docs/example.jsx
@@ -0,0 +1,30 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var HeaderCake = require( 'components/header-cake' );
+
+module.exports = React.createClass( {
+
+ displayName: 'Headers',
+
+ mixins: [ React.addons.PureRenderMixin ],
+
+ render: function() {
+ return (
+
+
+
+ Subsection Header aka Header Cake
+
+
Clicking header cake returns to previous section.
+
+ );
+ }
+} );
diff --git a/client/components/header-cake/index.jsx b/client/components/header-cake/index.jsx
new file mode 100644
index 00000000000000..e62362743fc0bb
--- /dev/null
+++ b/client/components/header-cake/index.jsx
@@ -0,0 +1,54 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ classNames = require( 'classnames' );
+
+/**
+ * Internal dependencies
+ */
+var Card = require( 'components/card' );
+
+module.exports = React.createClass( {
+ displayName: 'HeaderCake',
+
+ propTypes: {
+ onClick: React.PropTypes.func.isRequired,
+ onTitleClick: React.PropTypes.func,
+ backText: React.PropTypes.oneOfType( [
+ React.PropTypes.element,
+ React.PropTypes.string
+ ] )
+ },
+
+ getDefaultProps: function() {
+ return {
+ isCompact: false
+ };
+ },
+
+ render: function() {
+ var classes = classNames(
+ 'header-cake',
+ this.props.className,
+ {
+ 'is-compact': this.props.isCompact
+ }
+ );
+
+ return (
+
+
+
+ { this.props.children }
+
+
+
+ );
+ }
+} );
diff --git a/client/components/header-cake/style.scss b/client/components/header-cake/style.scss
new file mode 100644
index 00000000000000..05dc206d48753e
--- /dev/null
+++ b/client/components/header-cake/style.scss
@@ -0,0 +1,53 @@
+// ==========================================================================
+// .header-cake
+//
+// The nav bar used as a tertiary content-level nav that consists of a back
+// button and a view title.
+// ==========================================================================
+
+.header-cake.card {
+ display: flex;
+ align-items: stretch;
+ justify-content: center;
+ font-size: 14px;
+ line-height: 18px;
+ padding: 16px;
+
+ @include breakpoint( "<660px" ) {
+ margin-top: 10px;
+ }
+}
+
+.header-cake__title {
+ color: darken( $gray, 20% );
+ text-align: center;
+ word-break: break-word;
+ flex: 2 1;
+}
+
+.header-cake__corner {
+ display: flex;
+ flex: 1 0;
+}
+
+.header-cake__back {
+ flex: 1 0;
+ display: block;
+ margin: -16px;
+ margin-right: 0;
+ padding: 15px 16px 16px;
+ color: darken( $gray, 20% );
+ cursor: pointer;
+
+ .noticon {
+ margin-top: 1px;
+ opacity: 0.6;
+ transform: rotate( -90deg );
+ }
+}
+
+.header-cake__back-text {
+ font-size: 11px;
+ font-weight: 600;
+ text-transform: uppercase;
+}
diff --git a/client/components/image-preloader/README.md b/client/components/image-preloader/README.md
new file mode 100644
index 00000000000000..361a6866652ecc
--- /dev/null
+++ b/client/components/image-preloader/README.md
@@ -0,0 +1,41 @@
+Image Preloader
+===============
+
+Image Preloader is a React component to display a placeholder element until the network request to retrieve the image has completed. This is particularly useful when changing the `src` attribute of an image, which can have a noticeable delay due to how React applies the minimal DOM diff. This is illustrated by the following CodePen, where progressing between images maintains the current image until the next is finished loading:
+
+http://codepen.io/aduth/pen/doqovP?editors=001
+
+## Usage
+
+```jsx
+var React = require( 'react' ),
+ ImagePreloader = require( 'components/image-preloader' );
+
+React.createClass( {
+ render: function() {
+ return (
+ Loading... }
+ src="http://lorempixel.com/200/200" />
+ );
+ }
+} );
+```
+
+## Props
+
+All props will be transferred to the rendered ` ` element, with the exception of `placeholder`.
+
+### `placeholder`
+
+- __Type:__ React element
+- __Required:__ Yes
+
+A React element to render while the image `src` is being loaded.
+
+### `children`
+
+- __Type:__ React node
+- __Required:__ No
+
+If a child is passed, it will be used as substitute content in the case that the image fails to load.
diff --git a/client/components/image-preloader/index.jsx b/client/components/image-preloader/index.jsx
new file mode 100644
index 00000000000000..177f2ec8083145
--- /dev/null
+++ b/client/components/image-preloader/index.jsx
@@ -0,0 +1,106 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ omit = require( 'lodash/object/omit' ),
+ noop = require( 'lodash/utility/noop' );
+
+/**
+ * Module variables
+ */
+var LoadStatus = {
+ PENDING: 'PENDING',
+ LOADING: 'LOADING',
+ LOADED: 'LOADED',
+ FAILED: 'FAILED'
+};
+
+module.exports = React.createClass( {
+ displayName: 'ImagePreloader',
+
+ propTypes: {
+ src: React.PropTypes.string.isRequired,
+ placeholder: React.PropTypes.element.isRequired,
+ children: React.PropTypes.node
+ },
+
+ getInitialState: function() {
+ return {
+ status: LoadStatus.PENDING
+ };
+ },
+
+ componentWillMount: function() {
+ this.createLoader();
+ },
+
+ componentWillReceiveProps: function( nextProps ) {
+ if ( nextProps.src !== this.props.src ) {
+ this.createLoader( nextProps );
+ }
+ },
+
+ componentWillUnmount: function() {
+ this.destroyLoader();
+ },
+
+ createLoader: function( nextProps ) {
+ var src = ( nextProps || this.props ).src;
+
+ this.destroyLoader();
+
+ this.image = new Image();
+ this.image.src = src;
+ this.image.onload = this.onLoadComplete;
+ this.image.onerror = this.onLoadComplete;
+
+ this.setState( {
+ status: LoadStatus.LOADING
+ } );
+ },
+
+ destroyLoader: function() {
+ if ( ! this.image ) {
+ return;
+ }
+
+ this.image.onload = noop;
+ this.image.onerror = noop;
+ delete this.image;
+ },
+
+ onLoadComplete: function( event ) {
+ this.destroyLoader();
+
+ this.setState( {
+ status: 'load' === event.type ? LoadStatus.LOADED : LoadStatus.FAILED
+ } );
+ },
+
+ render: function() {
+ var children, imageProps;
+
+ switch ( this.state.status ) {
+ case LoadStatus.LOADING:
+ children = this.props.placeholder;
+ break;
+
+ case LoadStatus.LOADED:
+ imageProps = omit( this.props, Object.keys( this.constructor.propTypes ) );
+ children = ;
+ break;
+
+ case LoadStatus.FAILED:
+ children = this.props.children;
+ break;
+
+ default: break;
+ }
+
+ return (
+
+ { children }
+
+ );
+ }
+} );
diff --git a/client/components/infinite-list/Makefile b/client/components/infinite-list/Makefile
new file mode 100644
index 00000000000000..b68d291769d5c1
--- /dev/null
+++ b/client/components/infinite-list/Makefile
@@ -0,0 +1,7 @@
+REPORTER ?= spec
+MOCHA ?= ../../../node_modules/.bin/mocha
+
+test:
+ @NODE_ENV=test NODE_PATH=test:../../../client $(MOCHA) --compilers jsx:babel/register --reporter $(REPORTER)
+
+.PHONY: test
diff --git a/client/components/infinite-list/README.md b/client/components/infinite-list/README.md
new file mode 100644
index 00000000000000..9349fa569d9ae1
--- /dev/null
+++ b/client/components/infinite-list/README.md
@@ -0,0 +1,88 @@
+Infinite Scroll List
+====================
+
+An infinitely scrollable list, not rendering invisible items above and below viewport to reduce memory usage.
+Important mainly for low end mobile devices.
+
+## When to use
+
+There is simpler implementation of infinite scroll - the [infinite scroll](../mixins/infinite-scroll/) mixin. Use `InfiniteList` when:
+
+* items contain images or other media
+* you expect that user will scroll a lot
+
+## Props
+
+* `items` Array of all items in list
+* `lastPage` Boolean, if last page of list was fetched
+* `fechingNextPage` Boolean, if we are currently fetching more items
+* `guessedItemHeight` Number, height to be used when real rendered height is unknown
+* `itemsPerRow` Number (default: `1`), number of items per row if rendered as rows of items
+* `fetchNextPage` Function, called to trigger loading of next page, takes `options` as argument, described below
+* `getItemRef` Function, for given item returns string usable as React `key` and `ref`
+* `renderItem` Function, for given item gets its React component. Render *must* sets `ref` and `key` using `getItemRef`.
+* `renderLoadingPlaceholders` Function, returning array of react components to be appended to list to indicate loading state.
+* `renderTrailingItems` Function, returning markup to be rendered after the content items. Optional, useful for padding flexbox grid with invisible elements.
+* `context` Object, DOM node in which the content is to be monitored for scroll state (optional, defaults to window if omitted or set to `false`)
+
+All other props will be passed to the `div` which holds the list. This allows to set e.g. `className` for it.
+
+## Analytics tracking
+
+If you need to track when user scrolls to another page, do it in `fetchNextPage` method. The method accepts an object with one key, `triggeredByScroll`, which signals if fetching page is a response to user action, or the mixin trying to automatically fill available space. This is to prevent tracking analytics event for cases where more than one page fits to screen.
+
+## Example Usage
+
+```
+
+var Listing = React.createClass( {
+ ...
+ fetchNextPage: funciton ( options ) {
+ if ( options.triggeredByScroll ) {
+ // track analytics events
+ }
+ actions.fetchNextPage();
+ },
+
+ getItemRef: function( item ) {
+ return 'item-' + item.id;
+ },
+
+ renderItem: function( item ) {
+ var itemKey = this.getItemRef( item );
+ return (
+
+ );
+ },
+
+ renderLoadingPlaceholders: function() {
+ var count = this.props.list.get().length ? 2 : this.props.list.perPage,
+ placeholders = [];
+ times( count, function( i ) {
+ placeholders.push( );
+ });
+
+ return placeholders;
+ },
+
+ render: function() {
+ return (
+
+ );
+ }
+
+} );
+
+```
+
+If you need reset scroll state of `InfiniteList` component, e.g. because the using component received list with different content, assign it different `key`, so that React creates new instance for it.
diff --git a/client/components/infinite-list/index.jsx b/client/components/infinite-list/index.jsx
new file mode 100644
index 00000000000000..f1ebd3ac5a5c6a
--- /dev/null
+++ b/client/components/infinite-list/index.jsx
@@ -0,0 +1,394 @@
+/**
+ * External dependencies
+ */
+var debug = require( 'debug' )( 'calypso:infinite-list' ),
+ omit = require( 'lodash/object/omit' ),
+ raf = require( 'raf' ),
+ React = require( 'react' ),
+ page = require( 'page' );
+
+/**
+ * Internal dependencies
+ */
+var ScrollHelper = require( './scroll-helper' ),
+ InfiniteListPositionsStore = require( 'lib/infinite-list/positions-store' ),
+ InfiniteListScrollStore = require( 'lib/infinite-list/scroll-store' ),
+ InfiniteListActions = require( 'lib/infinite-list/actions' ),
+ detectHistoryNavigation = require( 'lib/detect-history-navigation' ),
+ scrollTo = require( 'lib/scroll-to' ),
+ smartSetState = require( 'lib/react-smart-set-state' );
+
+module.exports = React.createClass( {
+ displayName: 'InfiniteList',
+
+ lastScrollTop: -1,
+ scrollRAFHandle: false,
+ scrollHelper: null,
+ isScrolling: false,
+
+ propTypes: {
+ items: React.PropTypes.array.isRequired,
+ fetchingNextPage: React.PropTypes.bool.isRequired,
+ lastPage: React.PropTypes.bool.isRequired,
+ guessedItemHeight: React.PropTypes.number.isRequired,
+ itemsPerRow: React.PropTypes.number,
+
+ fetchNextPage: React.PropTypes.func.isRequired,
+ getItemRef: React.PropTypes.func.isRequired,
+ renderItem: React.PropTypes.func.isRequired,
+ renderLoadingPlaceholders: React.PropTypes.func.isRequired,
+ renderTrailingItems: React.PropTypes.func,
+ context: React.PropTypes.oneOfType( [
+ React.PropTypes.object,
+ React.PropTypes.bool,
+ ] ),
+ },
+
+ getDefaultProps: function() {
+ return {
+ itemsPerRow: 1,
+ renderTrailingItems: () => {}
+ };
+ },
+
+ smartSetState: smartSetState,
+
+ componentWillMount: function() {
+ var url = page.current,
+ newState, scrollPosition;
+
+ if ( detectHistoryNavigation.loadedViaHistory() ) {
+ newState = InfiniteListPositionsStore.get( url );
+ scrollPosition = InfiniteListScrollStore.get( url );
+ }
+
+ if ( newState && scrollPosition ) {
+ debug( 'overriding scrollTop:', scrollPosition );
+ newState.scrollTop = scrollPosition;
+ }
+
+ this.scrollHelper = new ScrollHelper( this.boundsForRef );
+ this.scrollHelper.props = this.props;
+ if ( this._contextLoaded() ) {
+ this._scrollContainer = this.props.context || window;
+ this.scrollHelper.updateContextHeight( this.getCurrentContextHeight() );
+ }
+
+ this.isScrolling = false;
+
+ if ( newState ) {
+ debug( 'infinite-list positions loaded from store' );
+ } else {
+ debug( 'infinite-list positions reset for new list' );
+ newState = {
+ firstRenderedIndex: 0,
+ topPlaceholderHeight: 0,
+ lastRenderedIndex: this.scrollHelper.initialLastRenderedIndex(),
+ bottomPlaceholderHeight: 0,
+ scrollTop: 0
+ };
+ }
+ debug( 'infinite list mounting', newState );
+ this.setState( newState );
+ },
+
+ componentDidMount: function() {
+ if ( this._contextLoaded() ) {
+ this._setContainerY( this.state.scrollTop );
+ }
+
+ // only override browser history scroll if navigated via history
+ if ( detectHistoryNavigation.loadedViaHistory() ) {
+ this._overrideHistoryScroll();
+ }
+ debug( 'setting scrollTop:', this.state.scrollTop );
+ this.updateScroll( {
+ triggeredByScroll: false
+ } );
+ if ( this._contextLoaded() ) {
+ this._scrollContainer.addEventListener( 'scroll', this.onScroll );
+ }
+ },
+
+ componentWillReceiveProps: function( newProps ) {
+ this.scrollHelper.props = newProps;
+
+ // New item may have arrived, should we change the rendered range?
+ if ( ! this.isScrolling ) {
+ this.cancelAnimationFrame();
+ this.updateScroll( {
+ triggeredByScroll: false
+ } );
+ }
+
+ // if the context changes, remove our scroll listener
+ if ( newProps.context === this.props.context ) {
+ return;
+ }
+ if ( this._contextLoaded() ) {
+ this._scrollContainer.removeEventListener( 'scroll', this._resetScroll );
+ }
+ },
+
+ componentDidUpdate: function( prevProps ) {
+ if ( ! this._contextLoaded() ) {
+ return;
+ }
+
+ if ( this.props.context !== prevProps.context ) {
+ // remove old listener
+ if ( this._scrollContainer ) {
+ this._scrollContainer.removeEventListener( 'scroll', this.onScroll );
+ }
+
+ // add new listeners
+ this._scrollContainer = this.props.context || window;
+ this._scrollContainer.addEventListener( 'scroll', this.onScroll );
+
+ // only override browser history scroll if navigated via history
+ if ( detectHistoryNavigation.loadedViaHistory() ) {
+ this._overrideHistoryScroll();
+ }
+ }
+
+ // we may have guessed item heights wrong - now we have real heights
+ if ( ! this.isScrolling ) {
+ this.cancelAnimationFrame();
+ this.updateScroll( {
+ triggeredByScroll: false
+ } );
+ }
+ },
+
+ componentWillUnmount: function() {
+ this._scrollContainer.removeEventListener( 'scroll', this.onScroll );
+ this._scrollContainer.removeEventListener( 'scroll', this._resetScroll );
+ this.cancelAnimationFrame();
+ },
+
+ cancelAnimationFrame: function() {
+ if ( this.scrollRAFHandle ) {
+ raf.cancel( this.scrollRAFHandle );
+ this.scrollRAFHandle = null;
+ }
+ this.lastScrollTop = -1;
+ },
+
+ onScroll: function() {
+ if ( this.isScrolling ) {
+ return;
+ }
+ if ( ! this.scrollRAFHandle && this.getCurrentScrollTop() !== this.lastScrollTop ) {
+ this.scrollRAFHandle = raf( this.scrollChecks );
+ }
+ },
+
+ getCurrentContextHeight: function() {
+ var context = this.props.context || window.document.documentElement;
+ return context.clientHeight;
+ },
+
+ getCurrentScrollTop: function() {
+ if ( this.props.context ) {
+ debug( 'getting scrollTop from context' );
+ return this.props.context.scrollTop;
+ }
+ return window.pageYOffset;
+ },
+
+ scrollChecks: function() {
+ // isMounted is necessary to prevent running this before it is mounted,
+ // which could be triggered by data-observe mixin.
+ if ( ! this.isMounted() || this.getCurrentScrollTop() === this.lastScrollTop ) {
+ this.scrollRAFHandle = null;
+ return;
+ }
+ this.updateScroll( {
+ triggeredByScroll: true
+ } );
+ },
+
+ scrollToTop: function() {
+ this.cancelAnimationFrame();
+ this.isScrolling = true;
+ if ( this.props.context && this.props.context !== window ) {
+ this.props.context.scrollTop = 0;
+ this.updateScroll( { triggeredByScroll: false } );
+ this.isScrolling = false;
+ } else {
+ scrollTo( {
+ x: 0,
+ y: 0,
+ duration: 250,
+ onComplete: () => {
+ if ( this.isMounted() ) {
+ this.updateScroll( { triggeredByScroll: false } );
+ }
+ this.isScrolling = false;
+ }
+ } );
+ }
+ },
+
+ updateScroll: function( options ) {
+ var url = page.current,
+ newState;
+
+ if ( ! this._contextLoaded() ) {
+ return;
+ }
+
+ this.lastScrollTop = this.getCurrentScrollTop();
+ InfiniteListActions.storeScroll( url, this.lastScrollTop );
+ this.scrollHelper.updateContextHeight( this.getCurrentContextHeight() );
+ this.scrollHelper.scrollPosition = this.lastScrollTop;
+ this.scrollHelper.triggeredByScroll = options.triggeredByScroll;
+ this.scrollHelper.updatePlaceholderDimensions();
+
+ this.scrollHelper.scrollChecks( this.state );
+
+ if ( this.scrollHelper.stateUpdated ) {
+ newState = {
+ firstRenderedIndex: this.scrollHelper.firstRenderedIndex,
+ topPlaceholderHeight: this.scrollHelper.topPlaceholderHeight,
+ lastRenderedIndex: this.scrollHelper.lastRenderedIndex,
+ bottomPlaceholderHeight: this.scrollHelper.bottomPlaceholderHeight,
+ scrollTop: this.lastScrollTop
+ };
+
+ // Force one more check on next animation frame,
+ // item heights may have been guessed wrong.
+ this.lastScrollTop = -1;
+
+ debug( 'new scroll positions', newState, this.state );
+ this.smartSetState( newState );
+ InfiniteListActions.storePositions( url, newState );
+ }
+
+ this.scrollRAFHandle = raf( this.scrollChecks );
+ },
+
+ boundsForRef: function( ref ) {
+ if ( ref in this.refs ) {
+ return this.refs[ ref ].getDOMNode().getBoundingClientRect();
+ }
+ return null;
+ },
+
+ /**
+ * Returns a list of visible item indexes. This includes any items that are
+ * partially visible in the viewport.
+ * @param options.offsetTop - in pixels, 0 if unspecified
+ * @param options.offsetBottom - in pixels, 0 if unspecified
+ * @returns {Array}
+ */
+ getVisibleItemIndexes: function( options ) {
+ var container = React.findDOMNode( this ),
+ visibleItemIndexes = [],
+ firstIndex = this.state.firstRenderedIndex,
+ lastIndex = this.state.lastRenderedIndex,
+ offsetTop = options && options.offsetTop ? options.offsetTop : 0,
+ offsetBottom = options && options.offsetBottom ? options.offsetBottom : 0,
+ windowHeight,
+ rect,
+ children,
+ i;
+ offsetBottom = offsetBottom || 0;
+ if ( lastIndex > -1 ) {
+ // stored item heights are not reliable at all in scroll helper,
+ // for this first pass, do bounds checks on children
+ children = container.children;
+ // skip over first and last child since these are spacers.
+ for ( i = 1; i < children.length - 1; i++ ) {
+ rect = container.children[ i ].getBoundingClientRect();
+ windowHeight = window.innerHeight || document.documentElement.clientHeight;
+ if (
+ ( rect.top < 0 && Math.abs( rect.top ) < rect.height - offsetTop ) ||
+ ( rect.top > 0 && rect.top < windowHeight - offsetBottom ) ) {
+ visibleItemIndexes.push( {
+ index: firstIndex + i - 1,
+ bounds: rect
+ } );
+ }
+ }
+ }
+ return visibleItemIndexes;
+ },
+
+ render: function() {
+ var lastRenderedIndex = this.state.lastRenderedIndex,
+ itemsToRender = [],
+ propsToTransfer = omit( this.props, Object.keys( this.constructor.propTypes ) ),
+ spacerClassName = 'infinite-list__spacer',
+ i;
+
+ if ( lastRenderedIndex === -1 || lastRenderedIndex > this.props.items.length - 1 ) {
+ debug( 'resetting lastRenderedIndex, currently at %s, %d items', lastRenderedIndex, this.props.items.length );
+ lastRenderedIndex = Math.min( this.state.firstRenderedIndex + this.scrollHelper.initialLastRenderedIndex(), this.props.items.length - 1 );
+ debug( 'reset lastRenderedIndex to %s', lastRenderedIndex );
+ }
+
+ debug( 'rendering %d to %d', this.state.firstRenderedIndex, lastRenderedIndex );
+
+ for ( i = this.state.firstRenderedIndex; i <= lastRenderedIndex; i++ ) {
+ itemsToRender.push( this.props.renderItem( this.props.items[ i ], i ) );
+ }
+
+ if ( this.props.fetchingNextPage ) {
+ itemsToRender = itemsToRender.concat( this.props.renderLoadingPlaceholders() );
+ }
+
+ return (
+
+
+ { itemsToRender }
+ { this.props.renderTrailingItems() }
+
+
+ );
+ },
+
+ _setContainerY: function( position ) {
+ if ( this.props.context && this.props.context !== window ) {
+ this.props.context.scrollTop = position;
+ return;
+ }
+ window.scrollTo( 0, position );
+ },
+
+ /**
+ * We are manually setting the scroll position to the last remembered one, so we
+ * want to override the scroll position that would otherwise get applied from
+ * HTML5 history.
+ */
+ _overrideHistoryScroll: function() {
+ if ( ! this._contextLoaded() ) {
+ return;
+ }
+ this._scrollContainer.addEventListener( 'scroll', this._resetScroll );
+ },
+
+ _resetScroll: function( event ) {
+ var position = this.state.scrollTop;
+ if ( ! this._contextLoaded() ) {
+ return;
+ }
+ debug( 'history setting scroll position:', event );
+ this._setContainerY( position );
+ this._scrollContainer.removeEventListener( 'scroll', this._resetScroll );
+ debug( 'override scroll position from HTML5 history popstate:', position );
+ },
+
+ /**
+ * Determine whether context is available or still being rendered.
+ * @return {bool} whether context is available
+ */
+ _contextLoaded: function() {
+ return this.props.context || this.props.context === false || ! ( 'context' in this.props );
+ }
+
+} );
diff --git a/client/components/infinite-list/scroll-helper.js b/client/components/infinite-list/scroll-helper.js
new file mode 100644
index 00000000000000..f891d81f92029c
--- /dev/null
+++ b/client/components/infinite-list/scroll-helper.js
@@ -0,0 +1,482 @@
+
+var assign = require( 'lodash/object/assign' ),
+ debug = require( 'debug' )( 'calypso:infinite-list:helper' );
+
+/**
+ * Scrolling algorithm extracted as separate object
+ *
+ * The purpose of extracting it is to make it testable and help the methods
+ * to be shorter and readable.
+ */
+function ScrollHelper( boundsForRef ) {
+ this.boundsForRef = boundsForRef;
+ this.itemHeights = {};
+
+ // Hide levels and context height
+ this.contextHeight = null;
+ this.topHideLevelHard = null;
+ this.topHideLevelSoft = null;
+ this.bottomHideLevelHard = null;
+ this.bottomHideLevelSoft = null;
+ this.bottomHideLevelUltraSoft = null;
+
+ // set by component
+ this.props = null;
+ this.scrollPosition = null;
+
+ // queried directly from placeholder rects
+ this.containerTop = null;
+ this.topPlaceholderHeight = null;
+ this.bottomPlaceholderHeight = null;
+ this.containerBottom = null;
+
+ this.stateUpdated = null;
+ this.firstRenderedIndex = null;
+ this.lastRenderedIndex = null;
+}
+
+assign ( ScrollHelper.prototype, {
+
+ storedItemHeight: function( itemKey ) {
+ var height = this.props.guessedItemHeight;
+
+ if ( itemKey in this.itemHeights ) {
+ height = this.itemHeights[ itemKey ];
+ }
+
+ return height;
+ },
+
+ forEachInRow: function( index, callback, context ) {
+ var firstIndexInRow, lastIndexInRow;
+
+ if ( 'function' !== typeof callback ) {
+ return;
+ }
+
+ if ( context ) {
+ callback = callback.bind( context );
+ }
+
+ firstIndexInRow = index - ( index % this.props.itemsPerRow );
+ lastIndexInRow = Math.min( firstIndexInRow + this.props.itemsPerRow, this.props.items.length ) - 1;
+ for ( var i = firstIndexInRow; i <= lastIndexInRow; i++ ) {
+ callback( this.props.items[ i ], i );
+ }
+ },
+
+ storeRowItemHeights: function( fromDirection, index ) {
+ this.forEachInRow( index, function( item ) {
+ var itemKey = this.props.getItemRef( item ),
+ itemBounds = this.boundsForRef( itemKey ),
+ height;
+
+ if ( itemBounds ) {
+ if ( 'bottom' === fromDirection ) {
+ height = this.containerBottom - this.bottomPlaceholderHeight - itemBounds.top;
+ } else {
+ height = itemBounds.bottom - ( this.containerTop + this.topPlaceholderHeight );
+ }
+ } else {
+ height = this.props.guessedItemHeight;
+ }
+
+ this.itemHeights[ itemKey ] = height;
+ }, this );
+ },
+
+ deleteRowItemHeights: function( index ) {
+ this.forEachInRow( index, function( item ) {
+ var itemKey = this.props.getItemRef( item );
+ delete this.itemHeights[ itemKey ];
+ }, this );
+ },
+
+ getRowHeight: function( index ) {
+ var maxHeight = 0;
+
+ this.forEachInRow( index, function( item ) {
+ var itemKey = this.props.getItemRef( item ),
+ height = this.storedItemHeight( itemKey );
+
+ maxHeight = Math.max( maxHeight, height );
+ }, this );
+
+ return maxHeight;
+ },
+
+ updateContextHeight: function( contextHeight ) {
+
+ if ( this.contextHeight === contextHeight ) {
+ return;
+ }
+
+ this.contextHeight = contextHeight;
+
+ this.topHideLevelHard = Math.min(
+ -1 * contextHeight,
+ -5 * this.props.guessedItemHeight
+ );
+
+ this.topHideLevelSoft = Math.min(
+ -2 * contextHeight,
+ -10 * this.props.guessedItemHeight
+ );
+
+ this.bottomHideLevelHard = contextHeight + Math.max(
+ contextHeight,
+ 5 * this.props.guessedItemHeight
+ );
+
+ this.bottomHideLevelSoft = contextHeight + Math.max(
+ 2 * contextHeight,
+ 10 * this.props.guessedItemHeight
+ );
+
+ this.bottomHideLevelUltraSoft = contextHeight + Math.max(
+ 3 * contextHeight,
+ 15 * this.props.guessedItemHeight
+ );
+ },
+
+ initialLastRenderedIndex: function() {
+ return Math.min(
+ this.props.items.length - 1,
+ Math.floor( this.bottomHideLevelSoft / this.props.guessedItemHeight ) - 1
+ );
+ },
+
+
+ updatePlaceholderDimensions: function() {
+ var topPlaceholderRect = this.boundsForRef( 'topPlaceholder' ),
+ bottomPlaceholderRect = this.boundsForRef( 'bottomPlaceholder' );
+
+ this.topPlaceholderHeight = topPlaceholderRect.height;
+ this.containerTop = topPlaceholderRect.top;
+
+ this.bottomPlaceholderHeight = bottomPlaceholderRect.height;
+ // It is important to use placeholder to get container bottom.
+ // Container node is reported longer than it should be in mobile Safari 7.0
+ this.containerBottom = bottomPlaceholderRect.bottom;
+ },
+
+ scrollChecks: function( state ) {
+ this.reset( state );
+
+ this.adjustLastRenderedIndex();
+
+ if ( this.shouldHideItemsAbove() ) {
+ this.hideItemsAbove();
+ } else if ( this.shouldShowItemsAbove() ) {
+ this.showItemsAbove();
+ }
+
+ if ( this.shouldHideItemsBelow() ) {
+ this.hideItemsBelow();
+ } else if ( this.shouldShowItemsBelow() ) {
+ this.showItemsBelow();
+ }
+
+ if ( this.shouldLoadNextPage() ) {
+ this.loadNextPage();
+ }
+ },
+
+ reset: function( state ) {
+ this.stateUpdated = false;
+ this.firstRenderedIndex = state.firstRenderedIndex;
+ this.lastRenderedIndex = state.lastRenderedIndex;
+ },
+
+ adjustLastRenderedIndex: function() {
+ // last index -1 means everything is rendered - it can happen when
+ // item count is not known when component is mounted
+ const offset = this.initialLastRenderedIndex(),
+ lastIndex = this.lastRenderedIndex || 0, // fixes NaN
+ firstIndex = this.firstRenderedIndex || 0, // fixes NaN
+ itemCount = this.props.items.length;
+ let newIndex = lastIndex;
+
+ if ( itemCount === 0 ) {
+ newIndex = -1;
+ }
+
+ if ( lastIndex >= itemCount ) {
+ newIndex = Math.min( firstIndex + offset, itemCount - 1 );
+ }
+
+ if ( newIndex === -1 && itemCount > 0 && firstIndex === 0 ) {
+ newIndex = offset;
+ }
+
+ if ( newIndex !== this.lastRenderedIndex ) {
+ this.stateUpdated = true;
+ this.lastRenderedIndex = newIndex;
+ }
+ },
+
+ shouldHideItemsAbove: function() {
+ //
+ // Hiding Item Above Chart
+ //
+ // +---- container top relative to context - value below zero
+ // |
+ // | placeholder
+ // |
+ // +---- placeholder bottom edge (before) = container top + placeholder height
+ // |
+ // | item to be hidden
+ // |
+ // +----
+ // |
+ // -----|- soft hide limit = - 2 * context height
+ // |
+ // | item to be hidden
+ // |
+ // +----
+ // |
+ // | last item to be hidden
+ // |
+ // +---- new placeholder bottom edge
+ // |
+ // -----|- hard hide limit = -1 * context height
+ // |
+ // | this item will stay
+ // |
+ // +----
+ // |
+ // |
+ // |
+ // |
+ // -----|- context top = 0
+ // |
+ //
+ return this.containerTop + this.topPlaceholderHeight < this.topHideLevelSoft;
+ },
+
+ hideItemsAbove: function() {
+ var rowHeight, rowBottom;
+
+ while ( this.firstRenderedIndex < this.props.items.length ) {
+ this.storeRowItemHeights( 'top', this.firstRenderedIndex );
+ rowHeight = this.getRowHeight( this.firstRenderedIndex );
+ rowBottom = this.containerTop + this.topPlaceholderHeight + rowHeight;
+
+ if ( rowBottom > this.topHideLevelHard ) {
+ this.deleteRowItemHeights( this.firstRenderedIndex );
+ break;
+ }
+
+ this.topPlaceholderHeight += rowHeight;
+ this.firstRenderedIndex += this.props.itemsPerRow;
+ this.stateUpdated = true;
+ debug( 'hiding top item', rowHeight, this.topPlaceholderHeight );
+ }
+ },
+
+
+ shouldShowItemsAbove: function() {
+ //
+ // Showing Item Above Chart
+ //
+ // +---- container top relative to context - value below zero
+ // |
+ // |
+ // |
+ // |
+ // -----|- soft hide limit = - 2 * context height
+ // |
+ // +---- new placeholder bottom
+ // |
+ // | Last item to be shown
+ // |
+ // +----
+ // |
+ // -----|- hard hide limit = -1 * context height
+ // |
+ // +----
+ // |
+ // | Item to be shown
+ // |
+ // +---- placeholder bottom when check started
+ // |
+ // -----|- context top = 0
+ // |
+ //
+ return this.containerTop + this.topPlaceholderHeight > this.topHideLevelHard;
+ },
+
+ showItemsAbove: function() {
+ var rowHeight, newPlaceholderBottom;
+
+ while ( this.firstRenderedIndex > 0 ) {
+ rowHeight = this.getRowHeight( this.firstRenderedIndex - this.props.itemsPerRow );
+ newPlaceholderBottom = this.containerTop + this.topPlaceholderHeight - rowHeight;
+
+ if ( newPlaceholderBottom < this.topHideLevelSoft ) {
+ break;
+ }
+
+ this.deleteRowItemHeights( this.firstRenderedIndex - this.props.itemsPerRow );
+ this.firstRenderedIndex -= this.props.itemsPerRow;
+ this.firstRenderedIndex = Math.max( 0, this.firstRenderedIndex );
+ if ( this.firstRenderedIndex <= 0 ) {
+ // never allow top placeholder when everything is shown
+ this.topPlaceholderHeight = 0;
+ } else {
+ this.topPlaceholderHeight = Math.max( 0, this.topPlaceholderHeight - rowHeight );
+ }
+
+ this.stateUpdated = true;
+ debug( 'showing top item', rowHeight, this.topPlaceholderHeight );
+ }
+
+ },
+
+
+ shouldHideItemsBelow: function() {
+ //
+ // Hiding Items Below Chart
+ //
+ // |
+ // -----|- context bottom = 1 * context height, e.g. 1000
+ // |
+ // +----
+ // |
+ // | Item
+ // |
+ // +----
+ // |
+ // -----|- hard hide limit, e.g. 2000
+ // |
+ // +---- new placeholder top
+ // |
+ // | Last item to be hidden
+ // |
+ // +----
+ // |
+ // -----|- soft hide limit, e.g. 3000
+ // |
+ // +----
+ // |
+ // | Item to be hidden
+ // |
+ // +----
+ // |
+ // -----|- 3rd hide limit, e.g. 4000
+ // |
+ // | Item to be hidden
+ // |
+ // +---- placeholder top when check started
+ // |
+ // |
+ // | Bottom placeholder
+ // |
+ // +---- container bottom relative to context, e.g. 5000
+ //
+ var placeholderTop = this.containerBottom - this.bottomPlaceholderHeight;
+ return placeholderTop > this.bottomHideLevelUltraSoft;
+ },
+
+ hideItemsBelow: function() {
+ var rowTop, rowHeight;
+
+ while ( this.lastRenderedIndex >= 0 ) {
+ this.storeRowItemHeights( 'bottom', this.lastRenderedIndex );
+ rowHeight = this.getRowHeight( this.lastRenderedIndex );
+ rowTop = this.containerBottom - this.bottomPlaceholderHeight - rowHeight;
+
+ if ( rowTop < this.bottomHideLevelHard ) {
+ this.deleteRowItemHeights( this.lastRenderedIndex );
+ break;
+ }
+
+ this.bottomPlaceholderHeight += rowHeight;
+ this.lastRenderedIndex -= this.props.itemsPerRow;
+ this.stateUpdated = true;
+ debug( 'hiding bottom item', rowHeight, this.bottomPlaceholderHeight );
+ }
+ },
+
+
+ shouldShowItemsBelow: function() {
+ var placeholderTop = this.containerBottom - this.bottomPlaceholderHeight;
+ return placeholderTop < this.bottomHideLevelHard;
+ },
+
+ showItemsBelow: function() {
+ var rowHeight, itemTop, placeholderTop;
+
+ while ( this.lastRenderedIndex < this.props.items.length - 1 ) {
+
+ rowHeight = this.getRowHeight( this.lastRenderedIndex + this.props.itemsPerRow );
+ placeholderTop = this.containerBottom - this.bottomPlaceholderHeight;
+ itemTop = placeholderTop + rowHeight;
+
+ if ( itemTop > this.bottomHideLevelSoft &&
+ // always show at least one item when placholder top is above hard limit
+ placeholderTop > this.bottomHideLevelHard
+ ) {
+ break;
+ }
+
+ this.deleteRowItemHeights( this.lastRenderedIndex + this.props.itemsPerRow );
+ if ( this.bottomPlaceholderHeight - rowHeight < 0 ) {
+ this.containerBottom += rowHeight - this.bottomPlaceholderHeight;
+ this.bottomPlaceholderHeight = 0;
+ } else {
+ this.bottomPlaceholderHeight -= rowHeight;
+ }
+ this.lastRenderedIndex += this.props.itemsPerRow;
+ this.lastRenderedIndex = Math.min( this.lastRenderedIndex, this.props.items.length - 1 );
+
+ // if everything is shown, then there should be no placeholder
+ if ( this.lastRenderedIndex >= this.props.items.length - 1 ) {
+ this.bottomPlaceholderHeight = 0;
+ }
+
+ this.stateUpdated = true;
+ debug( 'showing bottom item', rowHeight, this.bottomPlaceholderHeight );
+ }
+ },
+
+
+ shouldLoadNextPage: function() {
+ if ( this.props.fetchingNextPage || this.props.lastPage ) {
+ return false;
+ }
+
+ return this.bottomPlaceholderHeight === 0 &&
+ this.containerBottom < this.bottomHideLevelHard;
+ },
+
+ loadNextPage: function() {
+ var triggeredByScroll = this.triggeredByScroll;
+
+ debug(
+ 'fetching next page',
+ this.containerBottom,
+ this.bottomPlaceholderHeight
+ );
+
+ // Consider all page fetches once user starts scrolling as triggered by scroll
+ // Same condition check is in lib/mixins/infinite-scroll loadNextPage
+ if ( this.scrollPosition > this.contextHeight ) {
+ triggeredByScroll = true;
+ }
+
+ // scroll check may be triggered while dispatching an action,
+ // we cannot create new action while dispatching old one
+ setTimeout( function() {
+ // checking these values again because we shifted the fetch to the next stack
+ if ( this.props.fetchingNextPage || this.props.lastPage ) {
+ return false;
+ }
+ this.props.fetchNextPage( {
+ triggeredByScroll: triggeredByScroll
+ } );
+ }.bind( this ), 0 );
+ }
+} );
+
+module.exports = ScrollHelper;
diff --git a/client/components/infinite-list/style.scss b/client/components/infinite-list/style.scss
new file mode 100644
index 00000000000000..69a276b71f48dd
--- /dev/null
+++ b/client/components/infinite-list/style.scss
@@ -0,0 +1,3 @@
+.infinite-list__spacer {
+ width: 100%;
+}
diff --git a/client/components/infinite-list/test/scroll-helper.js b/client/components/infinite-list/test/scroll-helper.js
new file mode 100644
index 00000000000000..e1cdf3c1686f11
--- /dev/null
+++ b/client/components/infinite-list/test/scroll-helper.js
@@ -0,0 +1,626 @@
+/**
+ * External dependencies
+ */
+var assert = require( 'chai' ).assert,
+ range = require( 'lodash/utility/range' );
+
+/**
+ * Internal dependencies
+ */
+var ScrollHelper = require( '../scroll-helper' );
+
+function getItemRef( item ) {
+ return 'i' + item;
+}
+
+describe( 'Infinite Scroll Helper', function() {
+
+ describe( 'Hide Levels', function() {
+
+ describe( 'Context Higher than 5 items', function() {
+ var helper = new ScrollHelper();
+ helper.props = {
+ guessedItemHeight: 100,
+ itemsPerRow: 1
+ };
+ helper.updateContextHeight( 1000 );
+
+ it( 'top hard hide levels is 1 vh above context', function() {
+ assert.equal( helper.topHideLevelHard, -1000 );
+ } );
+ it( 'top soft hide level is 2 vh above context', function() {
+ assert.equal( helper.topHideLevelSoft, -2000 );
+ } );
+ it( 'bottom hard hide level is 1 vh below context', function() {
+ assert.equal( helper.bottomHideLevelHard, 2000 );
+ } );
+ it( 'bottom soft hide level is 2 vh below context', function() {
+ assert.equal( helper.bottomHideLevelSoft, 3000 );
+ } );
+ it( 'bottom 3rd hide level is 3 vh below context', function() {
+ assert.equal( helper.bottomHideLevelUltraSoft, 4000 );
+ } );
+ } );
+
+ describe( 'Context Shorter than 5 items', function() {
+ var helper = new ScrollHelper();
+ helper.props = {
+ guessedItemHeight: 100,
+ itemsPerRow: 1
+ };
+ helper.updateContextHeight( 200 );
+
+ it( 'top hard hide levels is 5 items above context', function() {
+ assert.equal( helper.topHideLevelHard, -500 );
+ } );
+ it( 'top soft hide level is 10 items above context', function() {
+ assert.equal( helper.topHideLevelSoft, -1000 );
+ } );
+ it( 'bottom hard hide level is 5 items below context', function() {
+ assert.equal( helper.bottomHideLevelHard, 700 );
+ } );
+ it( 'bottom soft hide level is 10 items below context', function() {
+ assert.equal( helper.bottomHideLevelSoft, 1200 );
+ } );
+ it( 'bottom 3rd hide level is 15 items below context', function() {
+ assert.equal( helper.bottomHideLevelUltraSoft, 1700 );
+ } );
+ } );
+
+ } );
+
+ describe( 'Container and placeholder positioning', function() {
+ var preparedBounds = {
+ topPlaceholder: {
+ top: -2000,
+ height: 1000
+ },
+ bottomPlaceholder: {
+ bottom: 4000,
+ height: 2000
+ }
+ },
+ helper = new ScrollHelper( function( ref ) {
+ return preparedBounds[ ref ];
+ }
+ );
+
+ helper.updatePlaceholderDimensions();
+
+ it( 'Placeholders height determined using their bounds ', function() {
+ assert.equal( helper.topPlaceholderHeight, 1000 );
+ assert.equal( helper.bottomPlaceholderHeight, 2000 );
+ } );
+
+ it( 'Container top determined using top placeholder bounds', function() {
+ assert.equal( helper.containerTop, -2000 );
+ } );
+
+ it( 'Container bottom determined using bottom placeholder bounds', function() {
+ assert.equal( helper.containerBottom, 4000 );
+ } );
+
+ } );
+
+ describe( 'Initial last rendered index', function() {
+ var helper;
+ beforeEach( function() {
+ helper = new ScrollHelper();
+ helper.props = {
+ guessedItemHeight: 200,
+ itemsPerRow: 1
+ };
+ helper.updateContextHeight( 1000 );
+ } );
+
+ it( 'renders only up to bottom soft hide level', function() {
+ helper.props.items = range( 100 );
+ assert.equal( helper.initialLastRenderedIndex(), 14 ); // 3000 / 200 - 1
+ } );
+
+ it( 'renders everything if it should fit', function() {
+ helper.props.items = range( 10 );
+ assert.equal( helper.initialLastRenderedIndex(), 9 );
+ } );
+ } );
+
+ describe( 'Items Above', function() {
+
+ it( 'Starts hiding when placeholder bottom edge is above soft level', function() {
+ var helper = new ScrollHelper();
+ helper.containerTop = -3000;
+ helper.topPlaceholderHeight = 500;
+ helper.topHideLevelSoft = -2000;
+
+ assert.ok( helper.shouldHideItemsAbove() );
+
+ helper.topPlaceholderHeight = 1500;
+ assert.notOk( helper.shouldHideItemsAbove() );
+ } );
+
+ describe( 'Hiding batch of items', function() {
+ var preparedBounds = {
+ i0: {
+ bottom: -1850
+ },
+ i1: {
+ bottom: -1500
+ },
+ // i2: { bottom: -1200 }, Let it use guessedItemHeight for this one
+ i3: {
+ bottom: -900
+ }
+ },
+ helper = new ScrollHelper( function( ref ) {
+ return preparedBounds[ ref ];
+ }
+ );
+ helper.props = {
+ guessedItemHeight: 300,
+ items: range( 4 ),
+ itemsPerRow: 1,
+ getItemRef: getItemRef,
+ };
+ helper.reset( {
+ firstRenderedIndex: 0,
+ } );
+ helper.containerTop = -2100;
+ helper.topHideLevelHard = -1000;
+ helper.topPlaceholderHeight = 0;
+
+ helper.hideItemsAbove();
+
+ it( 'updated state', function() {
+ assert( helper.stateUpdated );
+ } );
+
+ it( 'created placeholder for 3 items', function() {
+ assert.equal( 900, helper.topPlaceholderHeight );
+ } );
+
+ it( 'hid 3 items', function() {
+ assert.equal( 3, helper.firstRenderedIndex );
+ } );
+
+ it( 'stored hidden items height', function() {
+ assert.deepEqual( {
+ i0: 250,
+ i1: 350,
+ i2: 300
+ }, helper.itemHeights );
+ } );
+ } );
+
+ describe( 'Completely above context', function() {
+ var preparedBounds = {
+ i0: {
+ bottom: -1850
+ },
+ i1: {
+ bottom: -1500
+ },
+ },
+ helper = new ScrollHelper( function( ref ) {
+ return preparedBounds[ ref ];
+ }
+ );
+
+ helper.props = {
+ guessedItemHeight: 300,
+ items: range( 2 ),
+ itemsPerRow: 1,
+ getItemRef: getItemRef,
+ };
+ helper.reset( {
+ firstRenderedIndex: 0,
+ } );
+
+ helper.containerTop = -2100;
+ helper.topHideLevelHard = -1000;
+ helper.topPlaceholderHeight = 0;
+
+ helper.hideItemsAbove();
+
+ it( 'created placeholder for 2 items', function() {
+ assert.equal( 600, helper.topPlaceholderHeight );
+ } );
+
+ it( 'hid 2 items', function() {
+ assert.equal( 2, helper.firstRenderedIndex );
+ } );
+ } );
+
+ it( 'Starts showing when placeholder bottom edge is below hard level', function() {
+ var helper = new ScrollHelper();
+ helper.containerTop = -3000;
+ helper.topPlaceholderHeight = 2500;
+ helper.topHideLevelHard = -1000;
+
+ assert.ok( helper.shouldShowItemsAbove() );
+
+ helper.topPlaceholderHeight = 1500;
+ assert.notOk( helper.shouldShowItemsAbove() );
+ } );
+
+ describe( 'Showing batch of items', function() {
+ var helper = new ScrollHelper();
+ helper.props = {
+ items: range( 6 ),
+ guessedItemHeight: 300,
+ itemsPerRow: 1,
+ getItemRef: getItemRef,
+ };
+ helper.reset( {
+ firstRenderedIndex: 5,
+ } );
+ helper.containerTop = -2100;
+ helper.topHideLevelSoft = -2000;
+ helper.topPlaceholderHeight = 1500;
+ helper.firstRenderedIndex = 5;
+ helper.itemHeights = {
+ i0: 250,
+ i1: 350,
+ i3: 300,
+ i4: 300
+ }; // i2 left to default
+
+ helper.showItemsAbove();
+
+ it( 'updated state', function() {
+ assert( helper.stateUpdated );
+ } );
+
+ it( 'reduced placeholder height', function() {
+ assert.equal( 250, helper.topPlaceholderHeight );
+ } );
+
+ it( 'shown 4 items', function() {
+ assert.equal( 1, helper.firstRenderedIndex );
+ } );
+
+ it( 'removed shown items height', function() {
+ assert.deepEqual( {
+ i0: 250
+ }, helper.itemHeights );
+ } );
+ } );
+
+ describe( 'Show items when their real height is higher than stored', function() {
+ var helper = new ScrollHelper();
+ helper.props = {
+ items: range( 3 ),
+ guessedItemHeight: 300,
+ itemsPerRow: 1,
+ getItemRef: getItemRef,
+ };
+ helper.reset( {
+ firstRenderedIndex: 2,
+ } );
+ helper.containerTop = 0;
+ helper.topHideLevelSoft = -2000;
+ // extrame case - no top placeholder, but still items to be shown
+ helper.topPlaceholderHeight = 0;
+ // item heights left to default
+
+ helper.showItemsAbove();
+
+ it( 'placeholder height is never negative', function() {
+ assert.equal( 0, helper.topPlaceholderHeight );
+ } );
+
+ it( 'shown all items', function() {
+ assert.equal( 0, helper.firstRenderedIndex );
+ } );
+ } );
+
+ it( 'removes placeholder if everything is shown', function() {
+ var helper = new ScrollHelper();
+ helper.props = {
+ items: range( 3 ),
+ guessedItemHeight: 300,
+ itemsPerRow: 1,
+ getItemRef: getItemRef,
+ };
+ helper.reset( {
+ firstRenderedIndex: 2,
+ } );
+ helper.containerTop = 0;
+ helper.topHideLevelSoft = -2000;
+ helper.topPlaceholderHeight = 700; // more than 2 * 300
+ // item heights left to default
+
+ helper.showItemsAbove();
+
+ assert.equal( 0, helper.topPlaceholderHeight );
+ } );
+
+ } );
+
+ describe( 'Items Below', function() {
+
+ it( 'Starts hiding when placholder top edge is below 3rd hide limit', function() {
+ var helper = new ScrollHelper();
+ helper.containerBottom = 5000;
+ helper.bottomPlaceholderHeight = 500;
+ helper.bottomHideLevelUltraSoft = 4000;
+
+ assert.ok( helper.shouldHideItemsBelow() );
+
+ helper.bottomPlaceholderHeight = 1500;
+ assert.notOk( helper.shouldHideItemsBelow() );
+ } );
+
+ describe( 'Hiding batch of items', function() {
+ var preparedBounds = {
+ i5: {
+ top: 1900
+ },
+ i6: {
+ top: 2200
+ },
+ i7: {
+ top: 2500
+ },
+ // i8: { top: 3400 }, Let it use guessedItemHeight for this one
+ i9: {
+ top: 3700
+ }
+ },
+ helper = new ScrollHelper( function( ref ) {
+ return preparedBounds[ ref ];
+ }
+ );
+ helper.props = {
+ items: range( 10 ),
+ guessedItemHeight: 300,
+ itemsPerRow: 1,
+ getItemRef: getItemRef,
+ };
+ helper.reset( {
+ lastRenderedIndex: 9,
+ } );
+
+ helper.containerBottom = 5000;
+ helper.bottomHideLevelHard = 2000;
+ helper.bottomPlaceholderHeight = 800;
+
+ helper.hideItemsBelow();
+
+ it( 'updated state', function() {
+ assert.ok( helper.stateUpdated );
+ } );
+
+ it( 'created placeholder for 3 items', function() {
+ assert.equal( 2800, helper.bottomPlaceholderHeight );
+ } );
+
+ it( 'hid 4 items', function() {
+ assert.equal( 5, helper.lastRenderedIndex );
+ } );
+
+ it( 'stored hidden items height', function() {
+ assert.deepEqual( {
+ i6: 300,
+ i7: 900,
+ i8: 300,
+ i9: 500
+ }, helper.itemHeights );
+ } );
+ } );
+
+ describe( 'Completely below context', function() {
+ var helper = new ScrollHelper( function() {
+ return null;
+ } // let it use guessed height
+ );
+ helper.props = {
+ items: range( 2 ),
+ guessedItemHeight: 300,
+ itemsPerRow: 1,
+ getItemRef: getItemRef,
+ };
+ helper.reset( {
+ lastRenderedIndex: 1,
+ } );
+
+ helper.containerBottom = 5000;
+ helper.bottomHideLevelHard = 2000;
+ helper.bottomPlaceholderHeight = 800;
+
+ helper.hideItemsBelow();
+
+ it( 'created placeholder for 2 items', function() {
+ assert.equal( 1400, helper.bottomPlaceholderHeight );
+ } );
+
+ it( 'hid all items', function() {
+ assert.equal( -1, helper.lastRenderedIndex );
+ } );
+ } );
+
+
+ it( 'Starts showing when placeholder top edge is above first hide limit', function() {
+ var helper = new ScrollHelper();
+ helper.containerBottom = 5000;
+ helper.bottomPlaceholderHeight = 3500;
+ helper.bottomHideLevelHard = 2000;
+
+ assert.ok( helper.shouldShowItemsBelow() );
+
+ helper.bottomPlaceholderHeight = 2500;
+ assert.notOk( helper.shouldShowItemsBelow() );
+ } );
+
+ describe( 'Showing batch of items', function() {
+ var helper = new ScrollHelper();
+ helper.props = {
+ items: range( 8 ),
+ guessedItemHeight: 300,
+ itemsPerRow: 1,
+ getItemRef: getItemRef,
+ };
+ helper.reset( {
+ lastRenderedIndex: 4,
+ } );
+
+ helper.containerBottom = 5000;
+ helper.bottomHideLevelHard = 2000;
+ helper.bottomHideLevelSoft = 3000;
+ helper.bottomPlaceholderHeight = 3100;
+ helper.itemHeights = {
+ i5: 300,
+ i6: 300,
+ i7: 900
+ };
+ // corresponding itemTops: 1900, 2200, 2500, 3400
+
+ helper.showItemsBelow();
+
+ it( 'updated state', function() {
+ assert( helper.stateUpdated );
+ } );
+
+ it( 'reduced placeholder height', function() {
+ assert.equal( 2500, helper.bottomPlaceholderHeight );
+ } );
+
+ it( 'shown 2 items', function() {
+ assert.equal( 6, helper.lastRenderedIndex );
+ } );
+
+ it( 'removed shown items height', function() {
+ assert.deepEqual( {
+ i7: 900
+ }, helper.itemHeights );
+ } );
+ } );
+
+ describe( 'Show item longer than context', function() {
+ var helper = new ScrollHelper();
+ helper.props = {
+ items: range( 8 ),
+ guessedItemHeight: 300,
+ itemsPerRow: 1,
+ getItemRef: getItemRef,
+ };
+ helper.reset( {
+ lastRenderedIndex: 4,
+ } );
+
+ helper.containerBottom = 5000;
+ helper.bottomHideLevelHard = 2000;
+ helper.bottomHideLevelSoft = 3000;
+ helper.bottomPlaceholderHeight = 3100;
+ helper.itemHeights = {
+ i5: 1200,
+ i6: 300
+ };
+ // corresponding itemTops: 3100, 3400
+
+ helper.showItemsBelow();
+
+ it( 'reduced placeholder height', function() {
+ assert.equal( 1900, helper.bottomPlaceholderHeight );
+ } );
+
+ it( 'shown 2 items', function() {
+ assert.equal( 5, helper.lastRenderedIndex );
+ } );
+ } );
+
+ describe( 'Show new items', function() {
+ var helper = new ScrollHelper();
+ helper.props = {
+ items: range( 8 ),
+ guessedItemHeight: 300,
+ itemsPerRow: 1,
+ getItemRef: getItemRef,
+ };
+ helper.reset( {
+ lastRenderedIndex: 4,
+ } );
+ helper.containerBottom = 900;
+ helper.bottomHideLevelHard = 2000;
+ helper.bottomHideLevelSoft = 3000;
+ helper.bottomPlaceholderHeight = 200;
+ // stored item heighs left to default
+
+ helper.showItemsBelow();
+
+ it( 'placeholder height is never negative', function() {
+ assert.equal( 0, helper.bottomPlaceholderHeight );
+ } );
+
+ it( 'container bottom is increased', function() {
+ assert.equal( 1600, helper.containerBottom );
+ } );
+
+ it( 'shown 3 items', function() {
+ assert.equal( 7, helper.lastRenderedIndex );
+ } );
+ } );
+
+ it( 'Placeholder height is always zero if everything shown', function() {
+ var helper = new ScrollHelper();
+ helper.props = {
+ items: range( 8 ),
+ guessedItemHeight: 300,
+ itemsPerRow: 1,
+ getItemRef: getItemRef,
+ };
+ helper.reset( {
+ lastRenderedIndex: 4,
+ } );
+
+ helper.containerBottom = 5000;
+ helper.bottomHideLevelHard = 2000;
+ helper.bottomHideLevelSoft = 3000;
+ helper.bottomPlaceholderHeight = 4200;
+ // stored item heighs left to default
+
+ helper.showItemsBelow();
+
+ assert.equal( 0, helper.bottomPlaceholderHeight );
+ assert.equal( 7, helper.lastRenderedIndex );
+ } );
+ } );
+
+
+ describe( 'Next page', function() {
+ var helper;
+
+ beforeEach( function() {
+ helper = new ScrollHelper();
+ helper.props = {
+ fetchingNextPage: false,
+ lastPage: false
+ };
+ helper.updateContextHeight( 1000 );
+ helper.bottomPlaceholderHeight = 0;
+ helper.containerBottom = 1900;
+ helper.bottomHideLevelHard = 2000;
+ } );
+
+ it( 'loaded when container bottom above hard limit', function() {
+ assert.ok( helper.shouldLoadNextPage() );
+ } );
+
+ it( 'not loaded when loading previous', function() {
+ helper.props.fetchingNextPage = true;
+ assert.notOk( helper.shouldLoadNextPage() );
+ } );
+
+ it( 'not loaded on last page', function() {
+ helper.props.lastPage = true;
+ assert.notOk( helper.shouldLoadNextPage() );
+ } );
+
+ it( 'not loaded if some items hidden', function() {
+ helper.bottomPlaceholderHeight = 100;
+ assert.notOk( helper.shouldLoadNextPage() );
+ } );
+
+ } );
+
+} );
+
diff --git a/client/components/info-popover/README.md b/client/components/info-popover/README.md
new file mode 100644
index 00000000000000..8762ab332c2e0a
--- /dev/null
+++ b/client/components/info-popover/README.md
@@ -0,0 +1,27 @@
+InfoPopover
+===========
+
+`InfoPopover` is a component based on `Popover` used to show a popover as a tooltip to a `Gridicon`.
+
+### `InfoPopover` Properties
+
+#### `position`
+
+The `position` property can be one of the following values:
+
+- `top`
+- `top left`
+- `top right`
+- `bottom`
+- `bottom left`
+- `bottom right`
+- `left`
+- `right`
+
+### `InfoPopover` Usage
+
+```js
+
+ This is some informational text
+
+```
diff --git a/client/components/info-popover/docs/example.jsx b/client/components/info-popover/docs/example.jsx
new file mode 100644
index 00000000000000..62f1e5d527c733
--- /dev/null
+++ b/client/components/info-popover/docs/example.jsx
@@ -0,0 +1,57 @@
+/**
+* External dependencies
+*/
+var React = require( 'react' );
+
+/**
+* Internal dependencies
+*/
+var InfoPopover = require( 'components/info-popover' );
+
+var InfoPopoverExample = React.createClass( {
+ displayName: 'InfoPopover',
+
+ mixins: [ React.addons.PureRenderMixin ],
+
+ getInitialState: function() {
+ return {
+ popoverPosition: 'bottom left',
+ };
+ },
+
+ render: function() {
+ return (
+
+
+
Position
+
+ top
+ top left
+ top right
+ left
+ right
+ bottom
+ bottom left
+ bottom right
+
+
+
+
+
+
+ Some informational text.
+
+
+
+ );
+ },
+
+ _changePopoverPosition: function( event ) {
+ this.setState( { popoverPosition: event.target.value } );
+ }
+
+} );
+
+module.exports = InfoPopoverExample;
diff --git a/client/components/info-popover/index.jsx b/client/components/info-popover/index.jsx
new file mode 100644
index 00000000000000..49a6659827d613
--- /dev/null
+++ b/client/components/info-popover/index.jsx
@@ -0,0 +1,71 @@
+/**
+* External dependencies
+*/
+import React from 'react';
+
+/**
+* Internal dependencies
+*/
+import Popover from 'components/popover';
+import Gridicon from 'components/gridicon';
+import classNames from 'classnames';
+import analytics from 'analytics';
+
+export default React.createClass( {
+
+ displayName: 'InfoPopover',
+
+ propTypes: {
+ position: React.PropTypes.string,
+ className: React.PropTypes.string,
+ gaEventCategory: React.PropTypes.string,
+ popoverName: React.PropTypes.string
+ },
+
+ getDefaultProps() {
+ return {
+ position: 'bottom'
+ };
+ },
+
+ getInitialState() {
+ return {
+ showPopover: false
+ };
+ },
+
+ render() {
+ return (
+
+
+
+ { this.props.children }
+
+
+ );
+ },
+
+ _onClick( event ) {
+ event.preventDefault();
+ this.setState( { showPopover: ! this.state.showPopover }, this._recordStats );
+ },
+
+ _onClose() {
+ this.setState( { showPopover: false }, this._recordStats );
+ },
+
+ _recordStats() {
+ const { gaEventCategory, popoverName } = this.props;
+
+ if ( gaEventCategory && popoverName ) {
+ const dialogState = this.state.showPopover ? ' Opened' : ' Closed';
+ analytics.ga.recordEvent( gaEventCategory, 'InfoPopover: ' + popoverName + dialogState );
+ }
+ }
+
+} );
diff --git a/client/components/info-popover/style.scss b/client/components/info-popover/style.scss
new file mode 100644
index 00000000000000..f6bd596b1acedd
--- /dev/null
+++ b/client/components/info-popover/style.scss
@@ -0,0 +1,22 @@
+.info-popover .gridicon {
+ cursor: pointer;
+ color: lighten( $gray, 15% );
+
+ &:hover {
+ color: $gray-dark;
+ }
+}
+
+.info-popover.is_active .gridicon {
+ color: $gray-dark;
+}
+
+.popover.info-popover__tooltip {
+ .tip-inner {
+ color: darken( $gray, 20% );
+ font-size: 13px;
+ max-width: 220px;
+ padding: 16px;
+ text-align: left;
+ }
+}
diff --git a/client/components/input-chrono/README.md b/client/components/input-chrono/README.md
new file mode 100644
index 00000000000000..5c6c51a01d9744
--- /dev/null
+++ b/client/components/input-chrono/README.md
@@ -0,0 +1,40 @@
+InputChrono
+============
+
+React component that creates a Date object from a user-entered textual date description.
+
+## Example Usage
+
+```js
+import InputChrono from 'components/input-chrono';
+
+export default React.createClass( {
+
+ // ...
+
+ onSet( date ) {
+ console.log( `date %s`, date );
+ },
+
+ render() {
+ return (
+
+ );
+ }
+
+} );
+```
+
+---
+
+## InputChrono
+
+#### Props
+
+`value` - **optional** initial input value
+
+`onSet` - **optional** Bound function called when a Date object is created.
+It's checked when the input loses the focus or when an enter key down is
+detected.
diff --git a/client/components/input-chrono/docs/example.jsx b/client/components/input-chrono/docs/example.jsx
new file mode 100644
index 00000000000000..bd482f14d69bde
--- /dev/null
+++ b/client/components/input-chrono/docs/example.jsx
@@ -0,0 +1,60 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+
+/**
+ * Internal dependencies
+ */
+import InputChrono from 'components/input-chrono';
+import Card from 'components/card';
+
+/**
+ * Date Picker Demo
+ */
+export default React.createClass( {
+ displayName: 'InputChrono',
+
+ mixins: [ React.addons.PureRenderMixin ],
+
+ getInitialState() {
+ return {
+ date: this.moment()
+ };
+ },
+
+ componentWillMount() {
+ var self = this;
+ this.interval = setInterval( function() {
+ var date = self.moment( self.state.date );
+ date.hours( date.hours() + 1 );
+ self.setState( { date: date } );
+ }, 1000 );
+ },
+
+ componentWillUnmount() {
+ clearInterval( this.interval );
+ },
+
+ onSet( date ) {
+ console.log( `date: %s`, date.toDate() );
+ this.setState( { date: date } );
+ },
+
+ render() {
+ return (
+
+ );
+ }
+} );
diff --git a/client/components/input-chrono/index.jsx b/client/components/input-chrono/index.jsx
new file mode 100644
index 00000000000000..601232b6dd1106
--- /dev/null
+++ b/client/components/input-chrono/index.jsx
@@ -0,0 +1,99 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+import chrono from 'chrono-node';
+
+/**
+ * Supported languages
+ */
+const supportedLanguages = [ 'en', 'jp' ];
+
+export default React.createClass( {
+ displayName: 'InputChrono',
+
+ focused: false,
+
+ propTypes: {
+ value: React.PropTypes.string,
+ lang: React.PropTypes.string,
+ onSet: React.PropTypes.func,
+ placeholder: React.PropTypes.string
+ },
+
+ getDefaultProps() {
+ return {
+ value: '',
+ lang: '',
+ placeholder: '',
+ onSet: () => {}
+ };
+ },
+
+ getInitialState() {
+ return {
+ value: this.props.value
+ };
+ },
+
+ componentWillReceiveProps( nextProps ) {
+ if ( ! this.focused && this.props.value !== nextProps.value ) {
+ this.setState( { value: nextProps.value } );
+ }
+ },
+
+ handleChange( event ) {
+ this.setState( { value: event.target.value } );
+ },
+
+ handleBlur( event ) {
+ this.setDateText( event );
+ this.focused = false;
+ },
+
+ handleFocus() {
+ this.focused = true;
+ },
+
+ onKeyDown( event ) {
+ if ( 13 !== event.keyCode ) {
+ return;
+ }
+
+ this.setDateText( event );
+ },
+
+ setDateText( event ) {
+ var date = chrono.parseDate( event.target.value );
+
+ if ( date ) {
+ this.setState( { value: this.moment( date ).calendar() } );
+ this.props.onSet( this.moment( date ) );
+ }
+ },
+
+ isLangSupported( lang ) {
+ return supportedLanguages.indexOf( lang ) >= 0;
+ },
+
+ render() {
+ return (
+
+ { this.isLangSupported( this.props.lang ) ?
+
:
+
+ { this.state.value }
+
+ }
+
+ );
+ }
+} );
+
diff --git a/client/components/input-chrono/style.scss b/client/components/input-chrono/style.scss
new file mode 100644
index 00000000000000..7a6081cbab5e19
--- /dev/null
+++ b/client/components/input-chrono/style.scss
@@ -0,0 +1,34 @@
+.input-chrono__container {
+ position: relative;
+ margin: 6px auto;
+
+ .gridicons-calendar {
+ color: lighten( $gray, 20% );
+ z-index: 0;
+ font-size: 8px;
+ width: 20px;
+ position: absolute;
+ left: 5px;
+ top: 5px;
+ }
+}
+
+.text-chrono,
+input.input-chrono {
+ width: 100%;
+ height: 36px;
+ line-height: 36px;
+ padding: 0 10px;
+ border: 1px solid lighten( $gray, 30% );
+ box-sizing: border-box;
+ background-color: $transparent;
+ color: $gray;
+ font-size: 12px;
+ text-align: left;
+ position: relative;
+ z-index: 1;
+}
+
+.text-chrono {
+ border: 0;
+}
diff --git a/client/components/like-button/README.md b/client/components/like-button/README.md
new file mode 100644
index 00000000000000..2c38c33929e42b
--- /dev/null
+++ b/client/components/like-button/README.md
@@ -0,0 +1,50 @@
+Like Button
+=========
+
+This component is used to display a like button.
+It has two parts, the actual button and a container that works with the LikeStore.
+For most usage, the container is the easiest route.
+
+#### How to use the container:
+
+```js
+var LikeButtonContainer = require( 'components/like-button' );
+
+render: function() {
+ return (
+
+
+
+ );
+}
+```
+
+#### Props
+
+* `siteId`: number, a site ID to fetch likes for
+* `postId`: number, a post ID to fetch likes for
+
+
+#### How to use the button directly:
+```js
+var LikeButton = require( 'components/like-button/button' );
+
+render: function() {
+ return (
+
+
+
+ );
+},
+
+handleLikeToggle: function( newState ) {
+ // save the state somehow
+}
+
+#### Props
+
+* `likeCount`: a string
+* `showCount`: (default: false) a boolean that replaces default "Like" label with the likeCount. By default, the likeCount is not displayed.
+* `liked`: (default: false ) a boolean indicating if the current user has liked whatever is being liked
+* `tagName`: (default: 'li' ) string, the tag to use for the button.
+* `onLikeToggle`: a callback that is invoked when the like button toggles. It is called with the new state.
diff --git a/client/components/like-button/_style.scss b/client/components/like-button/_style.scss
new file mode 100644
index 00000000000000..44c727cd5b1470
--- /dev/null
+++ b/client/components/like-button/_style.scss
@@ -0,0 +1,64 @@
+.like-button {
+ display: inline-block;
+ padding: 4px 4px 4px 27px;
+ color: lighten( $gray, 10 );;
+ position: relative;
+ box-sizing: border-box;
+ transition: color 0.15s linear;
+
+ .gridicon {
+ position: absolute;
+ top: 2px;
+ left: 0;
+ }
+
+ .gridicon__liked {
+ opacity: 0;
+ pointer-events: none;
+ fill: $orange-jazzy;
+ transform: scale( 4 ) rotate( 90deg );
+ }
+
+ &.is-animated .gridicon__liked {
+ transition: all 0.3s cubic-bezier(0.175, 0.885, 0.320, 1.275);
+ }
+
+ .gridicon__like-empty {
+ fill: lighten( $gray, 20 );
+ }
+
+ &.is-animated .gridicon__like-empty {
+ transition: all 0.2s cubic-bezier(0.175, 0.885, 0.320, 1.275);
+ }
+
+ &:hover {
+ cursor: pointer;
+ color: $blue-light;
+
+ .gridicon__like-empty {
+ fill: $blue-light;
+ }
+ }
+
+ &.is-liked {
+ color: lighten( $gray, 10 );;
+
+ .gridicon__liked {
+ opacity: 1;
+ fill: $orange-jazzy;
+ pointer-events: auto;
+ transform: scale( 1 ) rotate( 0 );
+ }
+
+ .gridicon__like-empty {
+ opacity: 0;
+ pointer-events: none;
+ fill: $orange-jazzy;
+ transform: translateX( -10px ) rotate( 1deg ) scale( 0.3 );
+ }
+
+ .like-button__label {
+ color: $orange-jazzy;
+ }
+ }
+}
diff --git a/client/components/like-button/button.jsx b/client/components/like-button/button.jsx
new file mode 100644
index 00000000000000..245b5a5b84b82a
--- /dev/null
+++ b/client/components/like-button/button.jsx
@@ -0,0 +1,99 @@
+/**
+ * Exeternal Dependencies
+ */
+var React = require( 'react/addons' ),
+ classnames = require( 'classnames' );
+
+/**
+ * Internal Dependencies
+ */
+var LikeIcons = require( './icons' );
+
+var LikeButton = React.createClass( {
+
+ mixins: [ React.addons.PureRenderMixin ],
+
+ propTypes: {
+ liked: React.PropTypes.bool,
+ showCount: React.PropTypes.bool,
+ likeCount: React.PropTypes.number,
+ tagName: React.PropTypes.string,
+ onLikeToggle: React.PropTypes.func,
+ likedLabel: React.PropTypes.string,
+ isMini: React.PropTypes.bool,
+ animateLike: React.PropTypes.bool
+ },
+
+ getDefaultProps: function() {
+ return {
+ liked: false,
+ showCount: false,
+ likeCount: 0,
+ isMini: false,
+ animateLike: true
+ };
+ },
+
+ toggleLiked: function( event ) {
+ if ( event ) {
+ event.preventDefault();
+ }
+ if ( this.props.onLikeToggle ) {
+ this.props.onLikeToggle( ! this.props.liked );
+ }
+ },
+
+ render: function() {
+ var containerClasses = {
+ 'like-button': true,
+ 'ignore-click': true,
+ 'is-mini': this.props.isMini,
+ 'is-animated': this.props.animateLike
+ },
+ likeLabel = this.translate( 'Like', { comment: 'Label for a button to "like" a post.'} ),
+ likeCount = this.props.likeCount,
+ containerTag = this.props.tagName || 'li',
+ labelElement,
+ iconSize = 24;
+
+ if ( this.props.liked ) {
+ containerClasses[ 'is-liked' ] = true;
+
+ if ( this.props.likedLabel ) {
+ likeLabel = this.props.likedLabel;
+ } else {
+ likeLabel = this.translate( 'Liked', { comment: 'Displayed when a person "likes" a post.' } );
+ }
+ }
+
+ // Override the label with a counter
+ if ( likeCount > 0 || this.props.showCount ) {
+ likeLabel = this.translate( '%d Like', '%d Likes', {
+ args: [ likeCount ],
+ count: likeCount,
+ comment: 'Displayed when a person "likes" a post.'
+ } );
+ }
+
+ if ( this.props.isMini ) {
+ iconSize = 18;
+ }
+
+ containerClasses = classnames( containerClasses );
+
+ labelElement = ( { likeLabel } );
+
+ return (
+ React.createElement(
+ containerTag,
+ {
+ className: containerClasses,
+ onTouchTap: this.toggleLiked
+ },
+ , labelElement
+ )
+ );
+ }
+} );
+
+module.exports = LikeButton;
diff --git a/client/components/like-button/docs/example.jsx b/client/components/like-button/docs/example.jsx
new file mode 100644
index 00000000000000..39c4111fc3e17b
--- /dev/null
+++ b/client/components/like-button/docs/example.jsx
@@ -0,0 +1,68 @@
+/**
+* External dependencies
+*/
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var LikeButton = require( 'components/like-button/button' ),
+ Card = require( 'components/card/compact' );
+
+var SimpleLikeButtonContainer = React.createClass( {
+
+ mixins: [ React.addons.PureRenderMixin ],
+
+ getInitialState: function() {
+ return {
+ liked: !! this.props.liked,
+ count: this.props.likeCount || 0
+ };
+ },
+
+ render: function() {
+ return (
+ );
+ },
+
+ handleLikeToggle: function( newState ) {
+ this.setState( {
+ liked: newState,
+ count: this.state.count += ( newState ? 1 : -1 )
+ } );
+ }
+} );
+
+var LikeButtons = React.createClass( {
+ displayName: 'LikeButton',
+
+ mixins: [ React.addons.PureRenderMixin ],
+
+ render: function() {
+ return (
+
+
+
+ Default:
+
+
+
+ Counter shown by default:
+
+
+
+ Liked button:
+
+
+
+ );
+ }
+} );
+
+module.exports = LikeButtons;
diff --git a/client/components/like-button/icons.jsx b/client/components/like-button/icons.jsx
new file mode 100644
index 00000000000000..504418b5c623b1
--- /dev/null
+++ b/client/components/like-button/icons.jsx
@@ -0,0 +1,39 @@
+/**
+ * External Dependencies
+ */
+var React = require( 'react' );
+
+var LikeIcons = React.createClass( {
+
+ propTypes: { size: React.PropTypes.number, },
+
+ getDefaultProps: function() {
+ return { size: 24 };
+ },
+
+ render: function() {
+
+ var size = this.props.size;
+
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+} );
+
+module.exports = LikeIcons;
diff --git a/client/components/like-button/index.jsx b/client/components/like-button/index.jsx
new file mode 100644
index 00000000000000..011ec1dceb6e2a
--- /dev/null
+++ b/client/components/like-button/index.jsx
@@ -0,0 +1,83 @@
+/**
+ * External Dependencies
+ */
+var React = require( 'react/addons' ),
+ omit = require( 'lodash/object/omit' ),
+ noop = require( 'lodash/utility/noop' );
+
+/**
+ * Internal Dependencies
+ */
+var LikeActions = require( 'lib/like-store/actions' ),
+ LikeButton = require( './button' ),
+ LikeStore = require( 'lib/like-store/like-store' );
+
+var LikeButtonContainer = React.createClass( {
+ propTypes: {
+ siteId: React.PropTypes.number.isRequired,
+ postId: React.PropTypes.number.isRequired,
+ showCount: React.PropTypes.bool,
+ tagName: React.PropTypes.string,
+ onLikeToggle: React.PropTypes.func
+ },
+
+ mixins: [ React.addons.PureRenderMixin ],
+
+ getDefaultProps: function() {
+ return {
+ onLikeToggle: noop
+ };
+ },
+
+ getInitialState: function() {
+ return this.getStateFromStores();
+ },
+
+ getStateFromStores: function( props = this.props, animateLike = true ) {
+ return {
+ likeCount: LikeStore.getLikeCountForPost( props.siteId, props.postId ),
+ iLike: LikeStore.isPostLikedByCurrentUser( props.siteId, props.postId ),
+ animateLike: animateLike
+ };
+ },
+
+ componentWillReceiveProps: function( nextProps ) {
+ if ( this.props.siteId !== nextProps.siteId ||
+ this.props.postId !== nextProps.postId ) {
+ const newState = this.getStateFromStores( nextProps, false );
+ this.setState( newState );
+ }
+ },
+
+ componentDidMount: function() {
+ LikeStore.on( 'change', this.onStoreChange );
+ },
+
+ componentWillUnmount: function() {
+ LikeStore.off( 'change', this.onStoreChange );
+ },
+
+ onStoreChange: function() {
+ var newState = this.getStateFromStores();
+ if ( newState.likeCount !== this.state.likeCount ||
+ newState.iLike !== this.state.iLike ) {
+ this.setState( newState );
+ }
+ },
+
+ handleLikeToggle: function( liked ) {
+ LikeActions[ liked ? 'likePost' : 'unlikePost' ]( this.props.siteId, this.props.postId );
+ this.props.onLikeToggle( liked );
+ },
+
+ render: function() {
+ var props = omit( this.props, [ 'siteId', 'postId' ] );
+ return ;
+ }
+} );
+
+module.exports = LikeButtonContainer;
diff --git a/client/components/main/README.md b/client/components/main/README.md
new file mode 100644
index 00000000000000..425bc70b48b707
--- /dev/null
+++ b/client/components/main/README.md
@@ -0,0 +1,22 @@
+Main (jsx)
+==========
+
+Component used to declare the main area of any given section — it's the main wrapper that gets render first inside `#primary`.
+
+#### How to use:
+
+```js
+var Main = require( 'components/main' );
+
+render: function() {
+ return (
+
+ Your section content...
+
+ );
+}
+```
+
+#### Props
+
+* `className`: Add your own class to the wrapper.
diff --git a/client/components/main/index.jsx b/client/components/main/index.jsx
new file mode 100644
index 00000000000000..f4382490ce201b
--- /dev/null
+++ b/client/components/main/index.jsx
@@ -0,0 +1,17 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ joinClasses = require( 'react/lib/joinClasses' );
+
+module.exports = React.createClass( {
+ displayName: 'Main',
+
+ render: function() {
+ return (
+
+ { this.props.children }
+
+ );
+ }
+} );
diff --git a/client/components/main/style.scss b/client/components/main/style.scss
new file mode 100644
index 00000000000000..5dc0ade516620c
--- /dev/null
+++ b/client/components/main/style.scss
@@ -0,0 +1,44 @@
+.main {
+ margin: auto;
+ max-width: 720px;
+ z-index: 20;
+
+ // Some screens (like /sites) don't have a sidebar.
+ // @todo: This kinda sucks. I think the full-width class
+ // should be moved to .wp-primary, and we can just remove
+ // the margin, instead of overriding it here. -shaun
+ &.is-full {
+ margin-left: -272px;
+ max-width: calc( 100% + 272px );
+
+ // Tablets
+ @include breakpoint( "<960px" ) {
+ margin-left: -224px;
+ }
+
+ @include breakpoint( "<660px" ) {
+ margin-left: 0;
+ max-width: 100%;
+ }
+ }
+
+ // Themes is a great example of using all this new space ;)
+ &.themes {
+ max-width: 100%;
+ }
+
+ // The customizer is full-width
+ &.customize {
+ max-width: 100%;
+ }
+
+ @include breakpoint( "<660px" ) {
+ backface-visibility: hidden;
+ perspective: 1000;
+ }
+}
+
+// Used on views (ex posts & pages ) where the empty-content is already inside a padded div
+.main .empty-content {
+ margin-top: 0;
+}
diff --git a/client/components/mobile-back-to-sidebar/index.jsx b/client/components/mobile-back-to-sidebar/index.jsx
new file mode 100644
index 00000000000000..c4dbd1219a7a3f
--- /dev/null
+++ b/client/components/mobile-back-to-sidebar/index.jsx
@@ -0,0 +1,31 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+/**
+ * Internal Dependencies
+ */
+var layoutFocus = require( 'lib/layout-focus' );
+
+var MobileBackToSidebar = React.createClass( {
+
+ toggleSidebar: function( event ) {
+ event.preventDefault();
+ layoutFocus.set( 'sidebar' );
+ },
+
+ render: function() {
+
+ return (
+
+
+
+ { this.props.children }
+
+
+ );
+ }
+} );
+
+module.exports = MobileBackToSidebar;
diff --git a/client/components/mobile-back-to-sidebar/style.scss b/client/components/mobile-back-to-sidebar/style.scss
new file mode 100644
index 00000000000000..d01a89b8dccd93
--- /dev/null
+++ b/client/components/mobile-back-to-sidebar/style.scss
@@ -0,0 +1,27 @@
+// Mobile Back to Sidebar
+.mobile-back-to-sidebar {
+ margin: -8px -8px 8px -8px;
+ padding: 15px 0 15px 36px;
+ position: relative;
+ background: $white;
+ border-bottom: 1px solid lighten( $gray, 20% );
+
+ @include breakpoint( ">660px" ) {
+ display: none;
+ }
+}
+
+.mobile-back-to-sidebar__icon {
+ position: absolute;
+ top: 16px;
+ left: 10px;
+ fill: $gray;
+ height: 20px;
+ width: 20px;
+ transform: rotate( 180deg );
+}
+
+.mobile-back-to-sidebar__content {
+ font-size: 15px;
+ color: $gray-dark;
+}
diff --git a/client/components/notices/docs/example.jsx b/client/components/notices/docs/example.jsx
new file mode 100644
index 00000000000000..b5b4d738d5046c
--- /dev/null
+++ b/client/components/notices/docs/example.jsx
@@ -0,0 +1,55 @@
+/**
+* External dependencies
+*/
+var React = require( 'react' );
+
+/**
+* Internal dependencies
+*/
+var NoticeArrowLink = require( 'notices/arrow-link' ),
+ SimpleNotice = require( 'notices/simple-notice' ),
+ Notice = require( 'notices/notice' );
+
+var Notices = React.createClass( {
+ mixins: [ React.addons.PureRenderMixin ],
+
+ getInitialState: function() {
+ return {
+ compactNotices: false
+ };
+ },
+
+ render: function() {
+ var toggleNoticesText = this.state.compactNotices ? 'Normal Notices' : 'Compact Notices';
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ { "Preview" }
+
+
+
+ );
+ },
+
+ toggleNotices: function() {
+ this.setState( { compactNotices: ! this.state.compactNotices } );
+ }
+} );
+
+module.exports = Notices;
diff --git a/client/components/olark-chatbox/README.md b/client/components/olark-chatbox/README.md
new file mode 100644
index 00000000000000..23a433a97e5a22
--- /dev/null
+++ b/client/components/olark-chatbox/README.md
@@ -0,0 +1,36 @@
+Olark chatbox
+=============
+
+***Note this is still a work in progress and should not be used in production yet.
+It lacks responsivness among other things***
+
+This component allows you to render the olark chat widget inline on any page.
+
+#### Why we need this
+Olarks api doesn't seem like it was built with our use case in mind (A single load javascript app). Their API doesn't offer the ability for its chat widget to go from inline -> floating or vice versa without performing a page refresh. Their API also doesn't offer any tear down or uninitialization methods that would allow us to pull off the floating -> inline -> floating switching we need.
+
+#### How it works
+This component takes control of the floating olark chat widget and inlines it within our DOM node. We then apply some CSS to the widget once it's inlined so that it looks like a standalone inlined chat component.
+
+
+*Note that you can only have a single chatbox on a page since we only have one olark widget to take control of.*
+
+
+#### How to use it:
+
+```js
+var React = require( 'react' ),
+ Main = require( 'components/main' ),
+ OlarkChatbox = require( 'components/olark-chatbox' );
+
+module.exports = React.createClass( {
+ render: function() {
+ return (
+
+
+
+ );
+ }
+} );
+
+```
\ No newline at end of file
diff --git a/client/components/olark-chatbox/index.jsx b/client/components/olark-chatbox/index.jsx
new file mode 100644
index 00000000000000..3b96e90923c212
--- /dev/null
+++ b/client/components/olark-chatbox/index.jsx
@@ -0,0 +1,122 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ debug = require( 'debug' )( 'calypso:olark:chatbox' );
+
+/**
+ * Internal dependencies
+ */
+var OlarkEvents = require( 'lib/olark-events' );
+
+module.exports = React.createClass( {
+ /**
+ * Initialize our component by binding to all of the necessary olark events.
+ */
+ componentDidMount: function() {
+ debug( 'mounted' );
+
+ // Bind to the onReady event so we know when we can grab and bind the olark widget to our component
+ OlarkEvents.on( 'api.chat.onReady', this.bindOlarkWidget );
+
+ // Lets bind to the onHide event so we can make sure that our chatbox is always visible by re-showing it
+ OlarkEvents.on( 'api.box.onHide', this.showChatbox );
+
+ // Lets bind to the onShrink event so we can make sure that our chatbox is always visible by re-expanding it
+ OlarkEvents.on( 'api.box.onShrink', this.expandChatbox );
+ },
+
+ /**
+ * Handle the shutdown of our component by unbinding from all of the events we listened to and return the chat
+ * widget to its original DOM parent.
+ */
+ componentWillUnmount: function() {
+ OlarkEvents.off( 'api.chat.onReady', this.bindOlarkWidget );
+ OlarkEvents.off( 'api.box.onHide', this.showChatbox );
+ OlarkEvents.off( 'api.box.onShrink', this.expandChatbox );
+
+ // Release the olark widget so that its nolonger inlined
+ this.releaseOlarkWidget();
+
+ debug( 'unmounted' );
+ },
+
+ /**
+ * Use the Olark API to show the chatbox
+ */
+ showChatbox: function() {
+ var olarkApi = window.olark;
+
+ olarkApi( 'api.box.show' );
+ },
+
+ /**
+ * Use the Olark API to expand the chatbox
+ */
+ expandChatbox: function() {
+ var olarkApi = window.olark;
+
+ olarkApi( 'api.box.expand' );
+ },
+
+ /**
+ * Take control of the olark widget by removing it from its DOM parent and adding it to our DOM node so that we can make it look inlined.
+ * This is also a callback for the api.chat.onReady event
+ */
+ bindOlarkWidget: function() {
+ var olarkWidget, dom = window.document;
+
+ // Check if our component is still mounted
+ if ( ! this.isMounted() ) {
+ // If this component was unmounted before the api.chat.onReady event is fired then don't try to bind it to our component.
+ // I'm unsure if removing the event listener before it is fired will make this unnecessary but this is double insurance
+ return;
+ }
+
+ // Search for the floating olark widget in the document
+ olarkWidget = dom.querySelector( '#habla_beta_container_do_not_rely_on_div_classes_or_names' );
+
+ // Check for the widget in the document
+ if ( ! olarkWidget ) {
+ //If we couldn't find the widget for some reason then bail
+ return;
+ }
+
+ // Save the parent of the widget so that we can return it when this component is unmounted
+ this.originalDOMParent = olarkWidget.parentElement;
+
+ // Expand/show the widget since we are inlining it
+ this.showChatbox();
+ this.expandChatbox();
+
+ // Change the parent of the widget to our DOM node and save a refrence to it
+ this.olarkDOMNode = React.findDOMNode( this ).appendChild( olarkWidget );
+
+ debug( 'bind the olark chat widget' );
+ },
+
+ /**
+ * Change the olark widgets parent back to the body element.
+ */
+ releaseOlarkWidget: function() {
+ // If we don't find the widget in our node then it was never added and we have no need to go any further.
+ if ( ! this.olarkDOMNode ) {
+ return;
+ }
+
+ // Return the olark widget to its original DOM node
+ this.originalDOMParent.appendChild( this.olarkDOMNode );
+
+ debug( 'release the olark chat widget' );
+ },
+
+ /**
+ * Render our chatbox container div
+ * @return {object} jsx object
+ */
+ render: function() {
+ return (
+
+ );
+ }
+} );
diff --git a/client/components/olark-chatbox/style.scss b/client/components/olark-chatbox/style.scss
new file mode 100644
index 00000000000000..06cc31a995d781
--- /dev/null
+++ b/client/components/olark-chatbox/style.scss
@@ -0,0 +1,26 @@
+.olark-chatbox__container {
+ #habla_topbar_div {
+ // Hide the top bar because it contains buttons that hide/shrink the chat widget
+ display: none;
+ }
+
+ #habla_window_div {
+ // Override the inline styles added by olark so we can make the chatbox inlined instead of absolute positioned
+ margin: auto !important;
+ bottom: auto !important;
+ right: auto !important;
+ display: block !important;
+ position: inherit !important;
+
+ #habla_expanded_div {
+ // Add a border to the top and bottom that matches the side borders olark adds
+ border-top: 1px solid #c8d7e1;
+ border-bottom: 1px solid #c8d7e1;
+ }
+ }
+
+ .hbl_pal_main_width {
+ // Make the olark chat stretch to the width of the component
+ width: auto !important;
+ }
+}
diff --git a/client/components/overlay/README.md b/client/components/overlay/README.md
new file mode 100644
index 00000000000000..f94829dc7fd99a
--- /dev/null
+++ b/client/components/overlay/README.md
@@ -0,0 +1,81 @@
+Overlay
+=======
+
+### This module is deprecated. Please don't use for new code
+
+
+
+
+
+
+
+Renders the overlay masterbar and their actions.
+
+#### How to use?
+
+Make use of it as a React component. It handles its own unmounting and body-classes wrangling for showing and animating the overlay and removing the overlay.
+
+```javascript
+var Overlay = require( 'components/overlay' );
+
+var Component = React.createClass({
+ render: function() {
+ return (
+
+
+ My Neat Component Logic Here
+
+ );
+ }
+});
+```
+
+### Customize Props
+
+You can also customize pass in the following props to the component to customize it further:
+
+* 'context': A page.js context object that is used to automatically figure out what url to use for the secondary action.
+* 'primary': `primary` is useful as a call-to-action. You can customize the primary action by passing in an object with the following attributes - or omit to have no primary button shown.
+ * 'title': The text for the button.
+ * 'action': The onClick action when the button is pressed.
+* 'secondary': `secondary` is useful as a cancel or done button and can be used without a `primary` action. Omitting this prop will result in a `Done` button with the default options. To remove the `secondary` button, pass a falsey value. You can pass in a configruation object with the following nodes:
+ * 'title': (optional) The text for the button. Default is 'Done'
+ * 'defaultBack': (optional) The default `back` location to return to if there is no previous page in the browser history. Default is `/sites`.
+ * 'action': (optional) An onClick action to take when the secondary button is clicked.
+
+```javascript
+var Overlay = require( 'components/overlay' ),
+ primary = { action: this.myClickHandler, title: 'Title Of Button' };
+
+var Component = React.createClass({
+ render: function() {
+ return (
+
+
+ My Neat Component Logic Here
+
+ );
+ }
+});
+```
+
+The following would not show a primary button, and the secondary button would be shown with the title of _Donezo_:
+
+```javascript
+var Overlay = require( 'components/overlay' ),
+ secondary = { title: 'Donezo' };
+ primary = null;
+
+var Component = React.createClass({
+ render: function() {
+ return (
+
+
+ My Neat Component Logic Here
+
+ );
+ }
+});
+
+* 'sectionID': Used as the class for the wrapping `` element.
+```
diff --git a/client/components/overlay/overlay.jsx b/client/components/overlay/overlay.jsx
new file mode 100644
index 00000000000000..d428302d6d2c6d
--- /dev/null
+++ b/client/components/overlay/overlay.jsx
@@ -0,0 +1,110 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react/addons' ),
+ debug = require( 'debug' )( 'calypso:overlay' ),
+ classes = require( 'component-classes' );
+
+/**
+ * Internal dependencies
+ */
+var Toolbar = require( './toolbar' ),
+ NoticesList = require( 'notices/notices-list' ),
+ notices = require( 'notices' ),
+ page = require( 'page' ),
+ TitleData = require( 'components/data/screen-title' );
+
+module.exports = React.createClass({
+ displayName: 'Overlay',
+
+ getDefaultProps: function() {
+ return {
+ closeOnESC: true,
+ // `sectionID` is used to namespace the overlay wrapper classes
+ sectionID: ''
+ };
+ },
+
+ /**
+ * When overlay will be mounted add `overlay-open` class
+ * to the document html element to display it
+ */
+ componentWillMount: function() {
+ debug( 'Mounting overlay component.' );
+ },
+
+ componentDidMount: function() {
+ setTimeout( function() {
+ classes( document.documentElement ).add( 'overlay-open' ).add( 'animate' ).add( 'overlay-is-front' );
+ }, 10 );
+
+ // Register listeners for `keydown` and `click` within #tertiary to close the overlay
+ if ( this.props.closeOnESC ) {
+ window.addEventListener( 'keydown', this.handleKeyPress );
+ }
+
+ document.getElementById( 'tertiary' ).addEventListener( 'click', this.handleClickWithinOverlay );
+ },
+
+ /**
+ * When overlay is going to be unmounted remove the `overlay-open`
+ * class from the document html element to animate it out
+ * and remove `overlay-is-front` when the animation has completed
+ */
+ componentWillUnmount: function() {
+ debug( 'Unmounting overlay component.' );
+ classes( document.documentElement ).remove( 'overlay-open' ).remove( 'animate' );
+ window.removeEventListener( 'keydown', this.handleKeyPress );
+ document.getElementById( 'tertiary' ).removeEventListener( 'click', this.handleClickWithinOverlay );
+ },
+
+ closeOverlay: function() {
+ page( this.getDefaultBack() );
+ },
+
+ getDefaultBack: function() {
+ var context = this.props.context || {},
+ defaultBack = this.props.secondary.defaultBack || '/sites';
+
+ if ( context.prevPath && context.prevPath !== context.path ) {
+ defaultBack = context.prevPath;
+ }
+ return defaultBack;
+ },
+
+ handleKeyPress: function( event ) {
+ // 'esc' key closes the overlay
+ if ( event.keyCode === 27 ) {
+ this.closeOverlay();
+ }
+ },
+
+ handleClickWithinOverlay: function( event ) {
+ if ( ! this.refs.overlayInnerContent.getDOMNode().contains( event.target ) ) {
+ this.closeOverlay();
+ }
+ },
+
+ render: function() {
+ var overlayClass = 'overlay-content slide-in-up ' + this.props.sectionID,
+ secondary = ( typeof this.props.secondary !== 'undefined' ) ? this.props.secondary : { title: this.translate( 'Done' ) };
+
+ return (
+
+
+
+
+
+
+
+ { this.props.children }
+
+
+ );
+ }
+
+});
diff --git a/client/components/overlay/package.json b/client/components/overlay/package.json
new file mode 100644
index 00000000000000..9208a9a3edbd70
--- /dev/null
+++ b/client/components/overlay/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "overlay",
+ "version": "0.0.0",
+ "private": true,
+ "main": "overlay.jsx"
+}
diff --git a/client/components/overlay/toolbar.jsx b/client/components/overlay/toolbar.jsx
new file mode 100644
index 00000000000000..cc620162028bfd
--- /dev/null
+++ b/client/components/overlay/toolbar.jsx
@@ -0,0 +1,111 @@
+/**
+ * External Dependencies
+ */
+var React = require( 'react' ),
+ debug = require( 'debug' )( 'calypso:overlay:toolbar' );
+
+/**
+ * Internal Dependencies
+ */
+var sites = require( 'lib/sites-list' )(),
+ SiteIcon = require( 'components/site-icon' );
+
+module.exports = React.createClass({
+ displayName: 'OverlayToolbar',
+
+ componentDidMount: function() {
+ debug( 'The OverlayToolbar component is mounted.' );
+ },
+
+ renderSiteContext: function() {
+ var site = sites.getSelectedSite(),
+ allSites;
+
+ if ( ! sites.initialized ) {
+ return;
+ }
+
+ if ( sites.selected ) {
+ site = sites.getSelectedSite();
+ } else {
+ site = sites.getPrimary();
+ }
+
+ allSites = (
+
+
+ { this.translate( 'All My Sites' ) }
+ { this.translate( 'Manage all my sites' ) }
+
+ );
+
+ return (
+
+ );
+ },
+
+ renderUserContext: function() {
+ // This should go in dependencies
+ var user = require( 'lib/user' )();
+
+ return (
+
+ );
+ },
+
+ renderContext: function() {
+ // If there is no site context, let's use user context.
+ if ( undefined === this.props.context ) {
+ return this.renderUserContext();
+ }
+
+ return this.renderSiteContext();
+ },
+
+ render: function() {
+ var primary = '',
+ secondary = '';
+
+ /**
+ * The primary and secondary buttons with their actions
+ */
+ if ( this.props.primary ) {
+ primary = ( { this.props.primary.title } );
+ }
+
+ if ( this.props.secondary ) {
+ secondary = ( { this.props.secondary.title } );
+ }
+
+ return (
+
+ );
+ }
+
+});
diff --git a/client/components/payment-logo/index.jsx b/client/components/payment-logo/index.jsx
new file mode 100644
index 00000000000000..9d5587cabb471d
--- /dev/null
+++ b/client/components/payment-logo/index.jsx
@@ -0,0 +1,20 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+
+const PaymentLogo = React.createClass( {
+ propTypes: {
+ type: React.PropTypes.string.isRequired
+ },
+
+ render: function() {
+ const classes = `payment-logo is-${ this.props.type }`;
+
+ return (
+
+ );
+ }
+} );
+
+module.exports = PaymentLogo;
diff --git a/client/components/payment-logo/style.scss b/client/components/payment-logo/style.scss
new file mode 100644
index 00000000000000..0af39fa01062c2
--- /dev/null
+++ b/client/components/payment-logo/style.scss
@@ -0,0 +1,31 @@
+.payment-logo {
+ background-position: 0 center;
+ background-repeat: no-repeat;
+ background-size: 35px auto;
+ display: inline-block;
+ height: 20px;
+ vertical-align: middle;
+ width: 35px;
+
+ &.is-amex {
+ background-image: url( '/calypso/images/upgrades/cc-amex.svg' );
+ }
+
+ &.is-discover {
+ background-image: url( '/calypso/images/upgrades/cc-discover.svg' );
+ }
+
+ &.is-mastercard {
+ background-image: url( '/calypso/images/upgrades/cc-mastercard.svg' );
+ }
+
+ &.is-visa {
+ background-image: url( '/calypso/images/upgrades/cc-visa.svg' );
+ }
+
+ &.is-paypal {
+ background-image: url( '/calypso/images/upgrades/paypal.svg' );
+ background-size: 70px;
+ width: 70px;
+ }
+}
diff --git a/client/components/plans/plan-actions/index.jsx b/client/components/plans/plan-actions/index.jsx
new file mode 100644
index 00000000000000..6750c61ac78c01
--- /dev/null
+++ b/client/components/plans/plan-actions/index.jsx
@@ -0,0 +1,305 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ classNames = require( 'classnames' );
+
+/**
+ * Internal dependencies
+ */
+var analytics = require( 'analytics' ),
+ config = require( 'config' ),
+ productsValues = require( 'lib/products-values' ),
+ isFreePlan = productsValues.isFreePlan,
+ isBusiness = productsValues.isBusiness,
+ isEnterprise = productsValues.isEnterprise,
+ cartItems = require( 'lib/cart-values' ).cartItems;
+
+module.exports = React.createClass( {
+ displayName: 'PlanActions',
+
+ propTypes: { plan: React.PropTypes.object },
+
+ getButtons: function() {
+ if ( this.props.isImageButton ) {
+ return this.getImageButton();
+ }
+
+ if ( this.props.isInSignup ) {
+ return this.newPlanActions();
+ }
+
+ if ( this.siteHasThisPlan() ) {
+ return (
+
+
+ { this.managePlanButton() }
+ { this.getCurrentPlanHint() }
+
+
+ );
+ }
+
+ if ( this.canSelectPlan( this.props.plan ) ) {
+ if ( config.isEnabled( 'upgrades/checkout' ) ) {
+ return this.getInternalButtons();
+ }
+ return this.getAtlasButtons();
+ }
+ },
+
+ getAtlasButtons: function() {
+ var checkoutURL = 'https://wordpress.com/checkout/' + this.props.site.ID + '/' + this.props.plan.product_id;
+
+ return (
+
+ );
+ },
+
+ getInternalButtons: function() {
+ if ( ! this.props.cart.hasLoadedFromServer || ! this.props.site ) {
+ return null;
+ }
+
+ const canStartTrial = this.props.siteSpecificPlansDetails.can_start_trial;
+
+ return canStartTrial ? this.newPlanActions() : this.upgradeActions();
+ },
+
+ upgradeActions: function() {
+ return (
+
+
+ { this.translate( 'Upgrade Now' ) }
+
+
+ );
+ },
+
+ recordStartFreeTrialClick: function( cartItem ) {
+ analytics.ga.recordEvent( 'Upgrades', 'Clicked Start Free Trial Button', 'Product ID', cartItem.product_id );
+ },
+
+ recordUpgradeNowClick: function( cartItem ) {
+ analytics.ga.recordEvent( 'Upgrades', 'Clicked Upgrade Now Link', 'Product ID', cartItem.product_id );
+ },
+
+ recordUpgradeNowButton: function( cartItem ) {
+ analytics.ga.recordEvent( 'Upgrades', 'Clicked Upgrade Now Button', 'Product ID', cartItem.product_id );
+ },
+
+ recordUpgradeTrialNowClick: function( cartItem ) {
+ analytics.ga.recordEvent( 'Upgrades', 'Clicked Upgrade Now Link For Trial', 'Product ID', cartItem.product_id );
+ },
+
+ handleAddToCart: function( cartItem, actionType, event ) {
+ if ( event ) {
+ event.preventDefault();
+ }
+
+ if ( cartItem && actionType ) {
+ if ( actionType === 'button' ) {
+ if ( cartItem.free_trial ) {
+ this.recordStartFreeTrialClick( cartItem );
+ }
+ if ( ! cartItem.free_trial ) {
+ this.recordUpgradeNowButton( cartItem );
+ }
+ }
+
+ if ( actionType === 'link' ) {
+ if ( cartItem.free_trial ) {
+ this.recordUpgradeTrialNowClick( cartItem );
+ }
+ if ( ! cartItem.free_trial ) {
+ this.recordUpgradeNowClick( cartItem );
+ }
+ }
+ }
+
+ if ( this.props.onSelectPlan ) {
+ this.props.onSelectPlan( cartItem );
+ }
+ },
+
+ canSelectPlan: function() {
+ if ( this.props.site ) {
+ if ( this.siteHasThisPlan() ) {
+ return false;
+ }
+
+ if ( this.planHasCost() && ! isBusiness( this.props.site.plan ) ) {
+ return true;
+ }
+ return false;
+ }
+ return true;
+ },
+
+ getImageButton: function() {
+ const classes = classNames( 'plan-actions__illustration', this.props.plan.product_slug );
+
+ if ( ! this.canSelectPlan( this.props.plan ) ) {
+ return (
+
+ );
+ }
+
+ if ( isFreePlan( this.props.plan ) ) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+ },
+
+ newPlanActions: function() {
+ if ( isFreePlan( this.props.plan ) ) {
+ return
+
+ { this.translate( 'Select Free Plan' ) }
+
+
;
+ }
+
+ return (
+
+
+ { this.translate( 'Upgrade Now', { context: 'Store action' } ) }
+
+
+ );
+ },
+
+ cartItem: function( properties ) {
+ return cartItems.getItemForPlan( this.props.plan, properties );
+ },
+
+ downgradeMessage: function() {
+ return (
+ { this.translate( 'Contact support to downgrade your plan.' ) }
+ );
+ },
+
+ siteHasThisPlan: function() {
+ return ! this.props.isInSignup && this.props.site && this.props.site.plan.product_id === this.props.plan.product_id;
+ },
+
+ managePlanButton: function() {
+ if ( this.planHasCost() ) {
+ return (
+ { this.translate( 'Manage Plan', { context: 'Link to current plan from /plans/' } ) }
+ );
+ }
+ },
+
+ freePlanExpiration: function() {
+ if ( ! this.planHasCost() ) {
+ return (
+ { this.translate( 'Never expires', { context: 'Expiration info for free plan in /plans/' } ) }
+ );
+ }
+ },
+
+ recordCurrentPlanClick: function() {
+ analytics.ga.recordEvent( 'Upgrades', 'Clicked Current Plan' );
+ },
+
+ getTrialPlanHint: function() {
+ var remainingDays = this.moment(
+ this.props.siteSpecificPlansDetails.expiry
+ ).diff( this.moment(), 'days' ),
+ translationComponents = {
+ strong: ,
+ link:
+ },
+ hint;
+
+ if ( remainingDays === 0 ) {
+ hint = this.translate(
+ '{{strong}}Your trial ends today.{{/strong}} Like what you see? {{link}}Upgrade Now{{/link}}',
+ { components: translationComponents }
+ );
+ } else {
+ hint = this.translate(
+ '{{strong}}Your trial ends in %(days)d day.{{/strong}} Like what you see? {{link}}Upgrade Now{{/link}}',
+ '{{strong}}Your trial ends in %(days)d days.{{/strong}} Like what you see? {{link}}Upgrade Now{{/link}}',
+ {
+ args: { days: remainingDays },
+ count: remainingDays,
+ components: translationComponents
+ }
+ );
+ }
+
+ return (
+ { hint }
+ );
+ },
+
+ getCurrentPlanHint: function() {
+ if ( ! this.props.siteSpecificPlansDetails ) {
+ return;
+ }
+
+ if ( this.isPlanOnTrial() ) {
+ return this.getTrialPlanHint();
+ }
+
+ return (
+
+
+ { this.translate( 'Your current plan', { context: 'Informing the user of their current plan on /plans/' } ) }
+
+ );
+ },
+
+ planHasCost: function() {
+ return this.props.plan.cost > 0;
+ },
+
+ isPlanOnTrial: function() {
+ return this.props.siteSpecificPlansDetails.free_trial;
+ },
+
+ placeholder: function() {
+ return ;
+ },
+
+ getContent: function() {
+ if ( this.props.isPlaceholder ) {
+ return this.placeholder();
+ }
+
+ if ( ! this.props.isInSignup && this.props.site && isEnterprise( this.props.site.plan ) ) {
+ return this.downgradeMessage();
+ }
+
+ return this.getButtons();
+ },
+
+ render: function() {
+ var classes = classNames( {
+ 'plan-actions': true,
+ 'is-placeholder': this.props.isPlaceholder,
+ 'is-image-button': this.props.isImageButton
+ } );
+
+ return (
+
+ { this.getContent() }
+
+ );
+ }
+
+} );
diff --git a/client/components/plans/plan-actions/style.scss b/client/components/plans/plan-actions/style.scss
new file mode 100644
index 00000000000000..0827fb58e5a5ce
--- /dev/null
+++ b/client/components/plans/plan-actions/style.scss
@@ -0,0 +1,168 @@
+.plan { // This nesting is necessary to prevent these styles bleeding onto the /plans/compare screen
+ .plan-actions {
+ float: left;
+ width: 100%;
+ padding: 0 16px 16px 16px;
+ box-sizing: border-box;
+
+ &.is-image-button {
+ padding: 0;
+ }
+ }
+
+ .plan-actions__action-details {
+ clear: both;
+ font-size: 12px;
+ color: darken( $gray, 10% );
+ @include clear-fix;
+ text-align: center;
+ }
+}
+
+.plan-actions__upgrade-button {
+ display: block;
+ text-align: center;
+ width: 100%;
+}
+
+.plan-actions__trial-period {
+ text-align: center;
+ line-height: 16px;
+ display: block;
+ padding: 5px 10px;
+ color: $gray-dark;
+ opacity: .8;
+}
+
+.plan-actions.is-placeholder {
+ .plan-actions__upgrade-button {
+ @include placeholder( 23% );
+ border: none;
+ border-radius: 0;
+ margin-bottom: 0.5em;
+ pointer-events: none;
+ }
+
+ .plan-actions__trial-period {
+ height: 3em;
+ }
+}
+
+.plan-actions__current {
+ .plan-actions__upgrade-button {
+ background: $white;
+ margin: 0 0 8px 0;
+ }
+
+ .plan-actions__plan-expiration {
+ color: $gray-dark;
+ display: block;
+ font-size: 11px;
+ opacity: .8;
+ padding: 7px 0 0 0;
+ }
+}
+
+.plan-actions__current-plan-label {
+ border: 1px solid lighten( $gray, 20% );
+ border-radius: 4px;
+ color: $gray-dark;
+ display: block;
+ font-size: 14px;
+ opacity: .6;
+ padding: 0.5em 1.2em 0.62em;
+ text-align: center;
+
+ &:before {
+ @extend %clear-text;
+ @include noticon( '\f418', 18px );
+
+ color: $alert-green;
+ margin-left: -10px;
+ padding-right: 5px;
+ text-align: center;
+ vertical-align: middle;
+ }
+}
+
+.plan-actions__trial-hint {
+ font-size: 12px;
+ line-height: 18px;
+}
+
+.plan-actions__trial-upgrade-now {
+ white-space: nowrap;
+}
+
+.plans-compare {
+ .plan-actions {
+ font-size: 16px;
+ padding: 10px;
+ }
+
+ .plan-actions__upgrade-button {
+ overflow: visible;
+ padding-left: 5px;
+ padding-right: 5px;
+ white-space: normal;
+ }
+
+ .plan-actions__current-plan-label {
+ font-size: 12px;
+ padding: 10px 5px;
+
+ &:before {
+ margin-left: -5px;
+ padding-right: 1px;
+ }
+ }
+}
+
+@mixin plans-collapsed {
+ .plans-compare {
+ .plan-actions {
+ display: none;
+ }
+ }
+}
+
+.plans.has-sidebar {
+ @include breakpoint( "<960px" ) {
+ @include plans-collapsed();
+ }
+
+ @include breakpoint( ">960px" ) {
+ @include plans-in-three-columns();
+ }
+}
+
+.plans.has-no-sidebar {
+ @include breakpoint( "<660px" ) {
+ @include plans-collapsed();
+ }
+
+ @include breakpoint( ">660px" ) {
+ @include plans-in-three-columns();
+ }
+}
+.plan-actions__illustration {
+ width: 100px;
+ height: 100px;
+ margin: 5px auto 10px auto;
+ background-size: 100%;
+ border-radius: 50%;
+ border: 5px solid lighten( $gray, 30% );
+}
+
+.free_plan.plan-actions__illustration,
+.jetpack_free.plan-actions__illustration {
+ background-image: url('/calypso/images/plans/plan-beginner.svg');
+}
+.value_bundle.plan-actions__illustration,
+.jetpack_premium.plan-actions__illustration {
+ background-image: url('/calypso/images/plans/plan-premium.svg');
+}
+.business-bundle.plan-actions__illustration,
+.jetpack_business.plan-actions__illustration {
+ background-image: url('/calypso/images/plans/plan-business.svg');
+}
diff --git a/client/components/plans/plan-discount-message/index.jsx b/client/components/plans/plan-discount-message/index.jsx
new file mode 100644
index 00000000000000..2d9c67358ff841
--- /dev/null
+++ b/client/components/plans/plan-discount-message/index.jsx
@@ -0,0 +1,52 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+ var productsValues = require( 'lib/products-values' );
+
+module.exports = React.createClass( {
+ displayName: 'PlanDiscountMessage',
+
+ showMostPopularMessage: function() {
+ return (
+ this.props.showMostPopularMessage &&
+ productsValues.isPremium( this.props.plan ) &&
+ this.props.plan.product_id !== ( this.props.site && this.props.site.plan.product_id )
+ );
+ },
+
+ mostPopularPlan: function() {
+ var hasBusiness = this.props.site && productsValues.isBusiness( this.props.site.plan );
+
+ return (
+ hasBusiness ? null : { this.translate( 'Our most popular plan' ) }
+ );
+ },
+
+ planHasDiscount: function() {
+ return this.props.siteSpecificPlansDetails && this.props.siteSpecificPlansDetails.raw_discount > 0;
+ },
+
+ planDiscountMessage: function() {
+ var message = this.translate( 'Get %(discount)s off your first year', {
+ args: { discount: this.props.siteSpecificPlansDetails.formatted_discount }
+ } );
+
+ return (
+ { message }
+ );
+ },
+
+ render: function() {
+ if ( this.planHasDiscount() ) {
+ return this.planDiscountMessage();
+ } else if ( this.showMostPopularMessage() ) {
+ return this.mostPopularPlan();
+ }
+ return false;
+ }
+} );
diff --git a/client/components/plans/plan-discount-message/style.scss b/client/components/plans/plan-discount-message/style.scss
new file mode 100644
index 00000000000000..3a276cf83d710d
--- /dev/null
+++ b/client/components/plans/plan-discount-message/style.scss
@@ -0,0 +1,59 @@
+.plan-discount-message {
+ display: block;
+ padding: 2px 10px;
+ box-sizing: border-box;
+ background: $gray-dark;
+ color: $white;
+ font-size: 10px;
+ text-transform: uppercase;
+}
+
+.plan-discount-message {
+ display: block;
+ width: 100%;
+}
+
+@mixin plans-in-three-columns() {
+ .plan-discount-message {
+ text-align: center;
+ }
+
+ .plan-discount-message {
+ position: absolute;
+ top: -19px;
+ z-index: 1;
+ }
+
+ .plans-compare {
+ .plan-discount-message {
+ margin: 0 10px;
+ padding: 5px 10px;
+ position: relative;
+ top: auto;
+ width: calc( 100% - 20px );
+
+ &:after {
+ content: "";
+ position: absolute;
+ border-width: 10px;
+ border-style: solid;
+ border-color: transparent transparent $gray-dark transparent;
+ top: -20px;
+ left: 42%;
+ }
+ }
+ }
+}
+
+.plans.has-sidebar {
+ @include breakpoint( ">960px" ) {
+ @include plans-in-three-columns();
+ }
+}
+
+.plans.has-no-sidebar {
+ @include breakpoint( ">660px" ) {
+ @include plans-in-three-columns();
+ }
+}
+
diff --git a/client/components/plans/plan-feature-cell/index.jsx b/client/components/plans/plan-feature-cell/index.jsx
new file mode 100644
index 00000000000000..14743391dc16f7
--- /dev/null
+++ b/client/components/plans/plan-feature-cell/index.jsx
@@ -0,0 +1,18 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+module.exports = React.createClass( {
+ displayName: 'PlanFeatureCell',
+
+ render: function() {
+ return (
+
+
+ { this.props.children }
+
+
+ );
+ }
+} );
diff --git a/client/components/plans/plan-feature-cell/style.scss b/client/components/plans/plan-feature-cell/style.scss
new file mode 100644
index 00000000000000..7e288df9dad776
--- /dev/null
+++ b/client/components/plans/plan-feature-cell/style.scss
@@ -0,0 +1,45 @@
+.plan-feature-cell {
+ border-bottom: 1px solid lighten( $gray, 20% );
+ display: table;
+ height: 54px;
+ vertical-align: middle;
+ width: 100%;
+}
+
+.plan-feature-cell__feature-text {
+ display: table-cell;
+ vertical-align: middle;
+ font-size: 12px;
+}
+
+.feature-list.is-placeholder {
+ .plan-feature-cell__feature-text {
+ span {
+ @include placeholder( 23% );
+ display: block;
+ width: 65%;
+ }
+ }
+}
+
+.is-placeholder .plan-feature-cell__feature-text {
+ span {
+ @include placeholder( 25% );
+ display: block;
+ margin: 0 auto;
+ width: 15%;
+ }
+}
+
+.plan-features .plan-feature-cell__feature-text {
+ color: darken( $gray, 10% );
+ opacity: .7;
+}
+
+.plan-features:not(:nth-child(2)) {
+ .plan-feature-cell__feature-text {
+ color: $gray-dark;
+ opacity: .9;
+ font-weight: bold;
+ }
+}
diff --git a/client/components/plans/plan-features/index.jsx b/client/components/plans/plan-features/index.jsx
new file mode 100644
index 00000000000000..e580ec1ecfbd57
--- /dev/null
+++ b/client/components/plans/plan-features/index.jsx
@@ -0,0 +1,73 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ classNames = require( 'classnames' );
+
+/**
+ * Internal dependencies
+ */
+var PlanHeader = require( 'components/plans/plan-header' ),
+ PlanFeatureCell = require( 'components/plans/plan-feature-cell' ),
+ PlanActions = require( 'components/plans/plan-actions' ),
+ PlanPrice = require( 'components/plans/plan-price' ),
+ PlanDiscountMessage = require( 'components/plans/plan-discount-message' );
+
+module.exports = React.createClass( {
+ displayName: 'PlanFeatures',
+
+ featureIncludedString: function( feature ) {
+ var included = feature[ this.props.plan.product_id ];
+ if ( 'boolean' === typeof included && included ) {
+ return ( );
+ }
+ if ( 'string' === typeof included ) {
+ return ( { included } );
+ }
+ return ( );
+ },
+
+ headerText: function() {
+ return { this.props.plan.product_name } ;
+ },
+
+ render: function() {
+ var features, classes,
+ siteSpecificPlansDetails = this.props.siteSpecificPlansDetailsList ?
+ this.props.siteSpecificPlansDetailsList.get( this.props.site.domain, this.props.plan.product_id ) :
+ undefined;
+
+ features = this.props.features.map( function( feature ) {
+ return (
+
+ { this.featureIncludedString( feature ) }
+
+ );
+ }, this );
+
+ classes = classNames( 'plan-feature-column', 'plan-features', this.props.plan.product_slug );
+
+ return (
+
+ );
+ }
+
+} );
diff --git a/client/components/plans/plan-features/style.scss b/client/components/plans/plan-features/style.scss
new file mode 100644
index 00000000000000..a0741ac4b1345d
--- /dev/null
+++ b/client/components/plans/plan-features/style.scss
@@ -0,0 +1,38 @@
+.plan-features {
+ text-align: center;
+
+ &.is-placeholder {
+ .plan-header {
+ .header-text {
+ @include placeholder( 23% );
+ display: block;
+
+ &:after {
+ content: '\A\00a0';
+ white-space: pre;
+ }
+ }
+ }
+ }
+}
+
+.plan-features__included {
+ color: $gray-dark;
+}
+
+.plan-features__included.no-text {
+ &:before {
+ color: $alert-green;
+
+ @include noticon( '\f418', 24px );
+ }
+}
+
+.plan-features__not-included.no-text {
+ &:before {
+ color: darken( $gray, 10% );
+
+ @include noticon( '-', 24px );
+ vertical-align: inherit;
+ }
+}
diff --git a/client/components/plans/plan-header/index.jsx b/client/components/plans/plan-header/index.jsx
new file mode 100644
index 00000000000000..ee14f6a50b7473
--- /dev/null
+++ b/client/components/plans/plan-header/index.jsx
@@ -0,0 +1,24 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ classNames = require( 'classnames' );
+
+module.exports = React.createClass( {
+ displayName: 'PlanHeader',
+
+ render: function() {
+ var classes = classNames( {
+ 'plan-header': true,
+ 'is-placeholder': this.props.isPlaceholder
+ } );
+
+ return (
+
+
{ this.props.text }
+
+ { this.props.children }
+
+ );
+ }
+} );
diff --git a/client/components/plans/plan-header/style.scss b/client/components/plans/plan-header/style.scss
new file mode 100644
index 00000000000000..c802e32e764d95
--- /dev/null
+++ b/client/components/plans/plan-header/style.scss
@@ -0,0 +1,185 @@
+.plan, .plan-feature-column {
+ .plan-header__title {
+ &:before {
+ background-size: 100%;
+ content: '';
+ display: block;
+ margin: 0 auto;
+ width: 35px;
+ height: 35px;
+ }
+ }
+
+ &.free_plan,
+ &.jetpack_free {
+ .plan-header__title:before {
+ background-image: url( '/calypso/images/plans/plan-beginner.svg' );
+ }
+ }
+ &.value_bundle,
+ &.jetpack_premium {
+ .plan-header__title:before {
+ background-image: url( '/calypso/images/plans/plan-premium.svg' );
+ }
+ }
+ &.business-bundle,
+ &.jetpack_business {
+ .plan-header__title:before {
+ background-image: url( '/calypso/images/plans/plan-business.svg' );
+ }
+ }
+}
+
+.plan {
+ .plan-header {
+ background: $white;
+ width: 100%;
+ padding: 24px 16px;
+ position: relative;
+ box-sizing: border-box;
+ cursor: pointer;
+
+ &:after {
+ @include noticon( '\f431', 16px );
+ color: $blue-medium;
+ font-size: 30px;
+ position: absolute;
+ right: 10px;
+ top: 20px;
+ transition: all .3s ease-in-out;
+ }
+
+ .plan-header__title {
+ color: $blue-wordpress;
+ display: block;
+ font-size: 17px;
+ line-height: 16px;
+
+ &:before {
+ left: 10px;
+ top: 16px;
+ position: absolute;
+ }
+ }
+ }
+
+ &.is-active {
+ .plan-header:after {
+ transform: rotate(180deg);
+ }
+ }
+}
+
+.plan-header.is-placeholder {
+ .plan-header__title {
+ @include placeholder( 23% );
+ margin: 0 auto 5px;
+ width: 60%;
+
+ &:before {
+ display: none;
+ }
+ }
+}
+
+.plans-compare {
+ .plan-header {
+ border-bottom: 2px solid lighten( $gray, 20% );
+ height: 3em;
+ padding: 0 15px 10px 15px;
+
+ .plan-header__title {
+ text-align: center;
+ color: $gray-dark;
+ font-size: 14px;
+ font-weight: 600;
+ line-height: 18px;
+
+ .header-text {
+ &:first-line {
+ color: $white;
+ font-size: 13px;
+ color: darken( $gray, 10% );
+ }
+
+ }
+ }
+ }
+}
+
+@mixin plans-in-three-columns() {
+ .plan, .plan-feature-column {
+ .plan-header {
+ .plan-header__title {
+ &:before {
+ display: none;
+ }
+ }
+ }
+ }
+
+ .plan.card {
+ .plan-header {
+ border-bottom: 2px solid lighten( $gray, 20% );
+ min-height: 230px;
+ padding: 16px;
+
+ &:after {
+ display: none;
+ }
+
+ .plan-header__title {
+ line-height: 20px;
+ text-align: center;
+ }
+ }
+ }
+}
+
+@mixin plans-collapsed() {
+ .plan.card {
+ .plan-header {
+ float: left;
+ height: 70px;
+
+ .plan-header__title {
+ margin: -8px 0 0 0;
+ padding: 0 0 0 40px;
+ text-align: left;
+ }
+ .plan-actions {
+ display: none;
+ }
+ }
+ }
+
+ .plans-compare {
+ .plan-header {
+ .plan-header__title {
+ .header-text {
+ display: none;
+ }
+ }
+ }
+ }
+}
+
+.plans.has-sidebar {
+ @include breakpoint( "<960px" ) {
+ @include plans-collapsed();
+ }
+
+ @include breakpoint( ">960px" ) {
+ @include plans-in-three-columns();
+ }
+}
+
+.plans.has-no-sidebar {
+ @include breakpoint( "<660px" ) {
+ @include plans-collapsed();
+ }
+
+ @include breakpoint( ">660px" ) {
+ @include plans-in-three-columns();
+ }
+}
diff --git a/client/components/plans/plan-list/index.jsx b/client/components/plans/plan-list/index.jsx
new file mode 100644
index 00000000000000..a01c9f7bf4d7a7
--- /dev/null
+++ b/client/components/plans/plan-list/index.jsx
@@ -0,0 +1,92 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react/addons' ),
+ times = require( 'lodash/utility/times' );
+
+/**
+ * Internal dependencies
+ */
+var Plan = require( 'components/plans/plan' ),
+ Card = require( 'components/card' ),
+ abtest = require( 'lib/abtest' ).abtest;
+
+module.exports = React.createClass( {
+ displayName: 'PlanList',
+
+ getInitialState: function() {
+ return { openPlan: '' };
+ },
+
+ openPlan: function( planId ) {
+ this.setState( { openPlan: planId === this.state.openPlan ? '' : planId } );
+ },
+
+ render: function() {
+ var plans = this.props.plans,
+ showJetpackPlans = false,
+ site,
+ plansList;
+
+ if ( this.props.sites ) {
+ site = this.props.sites.getSelectedSite();
+ showJetpackPlans = site && site.jetpack;
+ }
+
+ // check if this site was registered via the JPPHP "Jetpack Start" program
+ // if so, we want to display a message that this plan is managed via the hosting partner
+
+ if ( this.props.siteSpecificPlansDetailsList && this.props.siteSpecificPlansDetailsList.hasJpphpBundle( site.domain ) ) {
+ return (
+
+
+ {
+ this.translate( 'This plan is managed by your web host. ' +
+ 'Please log into your host\'s control panel to manage subscription ' +
+ 'and billing information.' )
+ }
+
+
+ );
+ }
+
+ if ( plans.length > 0 ) {
+ plans = plans.filter( function( plan ) {
+ return ( showJetpackPlans === ( 'jetpack' === plan.product_type ) );
+ } );
+
+ plansList = plans.map( function( plan ) {
+ return (
+
+ );
+ }, this );
+ } else {
+ plansList = times( 3, function( n ) {
+ return (
+
+ );
+ }, this );
+ }
+
+ var aaMarkup;
+ if ( abtest( 'plansPageBusinessAATest' ) === 'originalA' ) {
+ aaMarkup = plansList;
+ } else {
+ aaMarkup = plansList;
+ }
+
+ return { aaMarkup }
;
+ }
+} );
diff --git a/client/components/plans/plan-list/style.scss b/client/components/plans/plan-list/style.scss
new file mode 100644
index 00000000000000..460039806589e8
--- /dev/null
+++ b/client/components/plans/plan-list/style.scss
@@ -0,0 +1,5 @@
+.plan-list {
+ padding-top: 19px;
+ text-align: left;
+ @include clear-fix;
+}
diff --git a/client/components/plans/plan-nudge/index.jsx b/client/components/plans/plan-nudge/index.jsx
new file mode 100644
index 00000000000000..17500e36122980
--- /dev/null
+++ b/client/components/plans/plan-nudge/index.jsx
@@ -0,0 +1,94 @@
+/**
+* External dependencies
+*/
+import React from 'react';
+import defer from 'lodash/function/defer';
+import page from 'page';
+
+/**
+ * Internal dependencies
+ */
+import SectionHeader from 'components/section-header';
+import CompactCard from 'components/card/compact';
+import PlanPreview from './preview';
+import plansList from 'lib/plans-list';
+import * as upgradesActions from 'lib/upgrades/actions';
+import Gridicon from 'components/gridicon';
+import { handlePlanSelect } from 'my-sites/plans/controller';
+
+/**
+ * Module variables
+ */
+const plans = plansList();
+
+export default React.createClass( {
+ displayName: 'PlanNudge',
+
+ propTypes: {
+ currentProductId: React.PropTypes.number.isRequired,
+ selectedSiteSlug: React.PropTypes.string.isRequired,
+ },
+
+ componentDidMount: function() {
+ plans.on( 'change', this.updatePlans );
+
+ this.updatePlans();
+ },
+
+ componentWillUnmount: function() {
+ plans.off( 'change', this.updatePlans );
+ },
+
+ updatePlans: function() {
+ this.setState( {
+ currentPlan: plans.get().find( plan => plan.product_id === this.props.currentProductId ),
+ businessPlan: plans.getPlanFromPath( 'business' )
+ } );
+ },
+
+ handleNewPlan: function() {
+ handlePlanSelect( this.state.businessPlan, this.props.selectedSiteSlug );
+ },
+
+ render: function() {
+ if ( ! this.state || ! this.state.currentPlan || ! this.state.businessPlan ) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ { this.translate( 'Want to add a store to your site?' ) }
+
+
+ { this.translate( 'Upgrade to WordPress.com Business to connect your Shopify, Ecwid, or Gumroad store to your site.' ) }
+
+
+
+
{ this.translate( 'Current Plan' ) }
+
+
+
+
{ this.translate( 'New Plan' ) }
+
+
+
+
+
+
+
+ { this.translate( "Why can't I add plugins to my site? " ) }
+
+
+
+
+ );
+ }
+} );
diff --git a/client/components/plans/plan-nudge/preview.jsx b/client/components/plans/plan-nudge/preview.jsx
new file mode 100644
index 00000000000000..561c77a8c8bb75
--- /dev/null
+++ b/client/components/plans/plan-nudge/preview.jsx
@@ -0,0 +1,57 @@
+/**
+* External dependencies
+*/
+import React from 'react';
+import classNames from 'classnames';
+
+/**
+ * Internal dependencies
+ */
+
+export default React.createClass( {
+ displayName: 'PlanPreview',
+
+ propTypes: {
+ plan: React.PropTypes.object.isRequired,
+ action: React.PropTypes.func,
+ actionLabel: React.PropTypes.string,
+ },
+
+ renderAction: function() {
+ if ( ! this.props.action ) {
+ return null;
+ }
+
+ return (
+
+
+ { this.props.actionLabel }
+
+
+ );
+ },
+
+ render: function() {
+ var planClasses = classNames( 'plan-preview', this.props.plan.product_slug );
+
+ return (
+
+
+
+ { this.props.plan.product_name_short }
+
+
+ { this.props.plan.price }
+
+ { this.props.plan.bill_period_label }
+
+
+
+
+ { this.props.plan.shortdesc }
+
+ { this.renderAction() }
+
+ );
+ }
+} );
diff --git a/client/components/plans/plan-nudge/style.scss b/client/components/plans/plan-nudge/style.scss
new file mode 100644
index 00000000000000..fbf5daf6961ba0
--- /dev/null
+++ b/client/components/plans/plan-nudge/style.scss
@@ -0,0 +1,191 @@
+.plan-nudge__selection {
+ color: $gray-dark;
+}
+
+.plan-nudge__title {
+ font-size: 21px;
+ font-weight: 700;
+ line-height: 28px;
+ margin-top: 7%;
+ margin-bottom: 10px;
+}
+
+.plan-nudge__plan-container {
+ display: flex;
+}
+
+.plan-nudge__plan {
+ display: flex;
+ flex-flow: row wrap;
+ margin: 35px 0;
+
+ @include breakpoint( ">480px" ) {
+ padding-top: 30px;
+ position: relative;
+ width: 50%;
+ }
+
+ &.current-plan {
+ @include breakpoint( ">960px" ) {
+ margin-left: 8%;
+ }
+ }
+
+ &.new-plan {
+ @include breakpoint( ">480px" ) {
+ margin-left: 0;
+ }
+
+ @include breakpoint( ">960px" ) {
+ margin-right: 8%;
+ }
+ }
+
+ &.current-plan {
+ display: none;
+
+ @include breakpoint( ">480px" ) {
+ display: flex;
+ }
+ }
+}
+
+.plan__status {
+ display: none;
+ margin-bottom: 10px;
+ position: absolute;
+ top: 0;
+ text-align: center;
+ width: 100%;
+
+ @include breakpoint( ">480px" ) {
+ display: block;
+ }
+
+ .new-plan & {
+ font-weight: 700;
+ }
+}
+
+.plan-nudge__footer {
+ &.card.is-compact {
+ padding: 30px 16px;
+
+ @include breakpoint( ">480px" ) {
+ padding-left: 65px;
+ }
+ }
+
+ .gridicons-info-outline {
+ position: absolute;
+ left: 27px;
+
+ @include breakpoint( "<480px" ) {
+ display: none;
+ }
+ }
+
+ .footer__title {
+ font-weight: 700;
+ margin-bottom: 5px;
+ }
+}
+
+.plan-preview {
+ background: $white;
+ box-shadow: 0 0 0 1px transparentize( lighten( $gray, 20% ), .5 ),
+ 0 1px 2px lighten( $gray, 30% );
+ display: flex;
+ flex-direction: column;
+ padding: 28px 20px;
+
+ .current-plan & {
+ margin: 7px 0;
+ padding-top: 23px;
+ }
+
+ .price,
+ .bill-period {
+ &.free_plan,
+ &.jetpack_free {
+ display: none;
+ }
+ }
+
+ &.business-bundle,
+ &.jetpack_business {
+ background: $blue-medium;
+ color: white;
+
+ .plan-preview__title {
+ color: white;
+ }
+ }
+}
+
+.plan-preview__header {
+ background-repeat: no-repeat;
+ background-size: 45px;
+ min-height: 50px;
+
+ &::before {
+ background: lighten($gray, 20%) no-repeat center;
+ background-size: 52px 52px;
+ border: 3px solid #e9eff3;
+ border-radius: 50%;
+ content: "";
+ display: block;
+ height: 50px;
+ position: absolute;
+ width: 50px;
+
+ .free_plan &,
+ .jetpack_free & {
+ background-image: url( '/calypso/images/plans/plan-beginner.svg' );
+ }
+
+ .value_bundle &,
+ .jetpack_premium & {
+ background-image: url( '/calypso/images/plans/plan-premium.svg' );
+ }
+
+ .business-bundle &,
+ .jetpack_business & {
+ background-image: url( '/calypso/images/plans/plan-business.svg' );
+ }
+ }
+
+ .plan-preview__title {
+ color: $blue-wordpress;
+ font-size: 21px;
+ line-height: 1em;
+ margin-top: 10px;
+ padding-left: 65px;
+ }
+
+ .price {
+ padding-left: 65px;
+ }
+
+ .bill-period {
+ display: inline-block;
+ font-size: 10px;
+ font-style: italic;
+ padding-left: 3px;
+ }
+}
+
+.plan-preview__description {
+ font-size: 14px;
+ font-weight: 200;
+ margin: 15px 0;
+}
+
+.plan-preview__action {
+ text-align: center;
+ width: 100%;
+
+ .button {
+ width: 100%;
+ }
+}
diff --git a/client/components/plans/plan-price/index.jsx b/client/components/plans/plan-price/index.jsx
new file mode 100644
index 00000000000000..9ddfc02561f6a3
--- /dev/null
+++ b/client/components/plans/plan-price/index.jsx
@@ -0,0 +1,48 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+module.exports = React.createClass( {
+ displayName: 'PlanPrice',
+
+ getFormattedPrice: function( planDetails ) {
+ if ( planDetails ) {
+ if ( planDetails.raw_price === 0 ) {
+ return this.translate( 'Free', { context: 'Zero cost product price' } );
+ }
+
+ return planDetails.formatted_price;
+ }
+ return this.translate( 'Loading' );
+ },
+
+ getPrice: function() {
+ var standardPrice = this.getFormattedPrice( this.props.plan ),
+ discountedPrice = this.getFormattedPrice( this.props.siteSpecificPlansDetails );
+
+ if ( this.props.siteSpecificPlansDetails && this.props.siteSpecificPlansDetails.raw_discount > 0 ) {
+ return ( { standardPrice } { discountedPrice } );
+ }
+
+ return ( { standardPrice } );
+ },
+
+ render: function() {
+ const { plan, siteSpecificPlansDetails: details } = this.props;
+ const hasDiscount = details && details.raw_discount > 0;
+
+ if ( this.props.isPlaceholder ) {
+ return
;
+ }
+
+ return (
+
+ { this.getPrice() }
+
+ { hasDiscount ? this.translate( 'for first year' ) : plan.bill_period_label }
+
+
+ );
+ }
+} );
diff --git a/client/components/plans/plan-price/style.scss b/client/components/plans/plan-price/style.scss
new file mode 100644
index 00000000000000..f9a65710c9c682
--- /dev/null
+++ b/client/components/plans/plan-price/style.scss
@@ -0,0 +1,84 @@
+.plan-header .plan-price {
+ color: $gray-dark;
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 20px;
+ text-align: right;
+}
+
+.plan-price__billing-period {
+ font-size: 12px;
+ font-style: italic;
+ color: darken( $gray, 10% );
+}
+
+.plan-price.is-placeholder {
+ @include placeholder( 23% );
+}
+
+.plan-price__discounted {
+ color: $gray;
+ text-decoration: line-through;
+}
+
+.plans-compare {
+ .plan-price {
+ font-size: 10px;
+ padding: 1em 0;
+
+ @include breakpoint( ">480px" ) {
+ font-size: 12px;
+ }
+
+ @include breakpoint( ">660px" ) {
+ font-size: 16px;
+ }
+ }
+
+ .plan-price__billing-period {
+ display: block;
+ opacity: .7;
+ }
+}
+
+@mixin plans-collapsed() {
+ .plan-header .plan-price {
+ display: inline-block;
+ padding: 0 0 0 40px;
+ }
+
+ .plan-price__billing-period {
+ margin-left: 3px;
+ }
+}
+
+@mixin plans-in-three-columns() {
+ .plan-header .plan-price {
+ text-align: center;
+ font-size: 18px;
+ }
+
+ .plan-price__billing-period {
+ display: block;
+ }
+}
+
+.plans.has-sidebar {
+ @include breakpoint( "<960px" ) {
+ @include plans-collapsed();
+ }
+
+ @include breakpoint( ">960px" ) {
+ @include plans-in-three-columns();
+ }
+}
+
+.plans.has-no-sidebar {
+ @include breakpoint( "<660px" ) {
+ @include plans-collapsed();
+ }
+
+ @include breakpoint( ">660px" ) {
+ @include plans-in-three-columns();
+ }
+}
diff --git a/client/components/plans/plan/index.jsx b/client/components/plans/plan/index.jsx
new file mode 100644
index 00000000000000..9c2702f62fa18b
--- /dev/null
+++ b/client/components/plans/plan/index.jsx
@@ -0,0 +1,211 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ classNames = require( 'classnames' );
+
+/**
+ * Internal dependencies
+ */
+var analytics = require( 'analytics' ),
+ Gridicon = require( 'components/gridicon' ),
+ PlanActions = require( 'components/plans/plan-actions' ),
+ PlanHeader = require( 'components/plans/plan-header' ),
+ PlanPrice = require( 'components/plans/plan-price' ),
+ PlanDiscountMessage = require( 'components/plans/plan-discount-message' ),
+ Card = require( 'components/card' );
+
+module.exports = React.createClass( {
+ displayName: 'Plan',
+
+ handleLearnMoreClick: function() {
+ window.scrollTo( 0, 0 );
+ this.recordLearnMoreClick();
+ },
+
+ recordLearnMoreClick: function() {
+ analytics.ga.recordEvent( 'Upgrades', 'Clicked Learn More Link', 'Product ID', this.props.plan.product_id );
+
+ if ( this.props.isInSignup ) {
+ analytics.tracks.recordEvent( 'calypso_signup_compare_plans_click', {
+ location: 'Learn more link',
+ product_slug: this.props.plan.product_slug
+ } );
+ }
+ },
+
+ getDescription: function() {
+ var comparePlansUrl, siteSuffix;
+
+ if ( this.isPlaceholder() ) {
+ return (
+
+ );
+ }
+
+ siteSuffix = this.props.site ? this.props.site.slug : '';
+ comparePlansUrl = this.props.comparePlansUrl ? this.props.comparePlansUrl : '/plans/compare/' + siteSuffix;
+
+ return (
+
+ );
+ },
+
+ showDetails: function() {
+ if ( 'function' === typeof ( this.props.onOpen ) ) {
+ this.props.onOpen( this.props.plan.product_id );
+ }
+ },
+
+ selectedSiteHasPlan: function() {
+ return this.props.site && this.props.site.plan.product_id === this.props.plan.product_id;
+ },
+
+ isPlaceholder: function() {
+ return this.props.placeholder;
+ },
+
+ getProductId: function() {
+ if ( this.isPlaceholder() ) {
+ return;
+ }
+
+ return this.props.plan.product_id;
+ },
+
+ getClassNames: function() {
+ var classObject = {
+ plan: true,
+ 'is-active': this.props.open,
+ 'is-current-plan': this.selectedSiteHasPlan()
+ };
+
+ if ( this.isPlaceholder() ) {
+ classObject[ 'is-placeholder' ] = true;
+ } else {
+ classObject[ this.props.plan.product_slug ] = true;
+ }
+
+ return classNames( classObject );
+ },
+
+ getSiteSpecificPlanDetails: function() {
+ if ( this.isPlaceholder() || ! this.props.site ) {
+ return;
+ }
+
+ return this.props.siteSpecificPlansDetailsList.get( this.props.site.domain, this.getProductId() );
+ },
+
+ getPlanDiscountMessage: function() {
+ if ( this.isPlaceholder() ) {
+ return;
+ }
+
+ return (
+
+ );
+ },
+
+ getBadge: function() {
+ if ( this.props.site ) {
+ if ( this.props.site.plan.product_id === this.getProductId() ) {
+ return (
+
+ );
+ }
+ }
+ },
+
+ getProductName: function() {
+ if ( this.isPlaceholder() ) {
+ return;
+ }
+
+ return this.props.plan.product_name_short;
+ },
+
+ getPlanTagline: function() {
+ if ( this.isPlaceholder() ) {
+ return;
+ }
+
+ return this.props.plan.tagline;
+ },
+
+ getPlanPrice: function() {
+ var isAllMySites = ! this.props.site && ! this.props.isInSignup;
+ if ( isAllMySites ) {
+ return;
+ }
+
+ return (
+
+ );
+ },
+
+ getPlanActions: function() {
+ return (
+
+ );
+ },
+
+ getImagePlanAction: function() {
+ return (
+
+ );
+ },
+
+ render: function() {
+ return (
+
+ { this.getPlanDiscountMessage() }
+
+ { this.getBadge() }
+
+ { this.getPlanTagline() }
+
+ { this.getImagePlanAction() }
+ { this.getPlanPrice() }
+
+
+
+ { this.getDescription() }
+
+ { this.getPlanActions() }
+
+
+ );
+ }
+} );
diff --git a/client/components/plans/plan/style.scss b/client/components/plans/plan/style.scss
new file mode 100644
index 00000000000000..7daeb1e18f9738
--- /dev/null
+++ b/client/components/plans/plan/style.scss
@@ -0,0 +1,156 @@
+.plan {
+ background: $gray-light;
+ box-sizing: border-box;
+ position: relative;
+ float: left;
+ width: 100%;
+ margin: 0 0 15px 0;
+ padding: 0;
+
+ &.is-placeholder {
+ .plan__plan-tagline {
+ @include placeholder( 23% );
+ }
+
+ .plan__illustration {
+ display: none;
+ }
+
+ .plan__plan-details {
+ p {
+ @include placeholder( 25% );
+ margin-bottom: 5px;
+ }
+
+ p:nth-child(2) {
+ width: 70%;
+ }
+ }
+ }
+
+ .gridicons-checkmark-circle {
+ fill: $alert-green;
+ position: absolute;
+ z-index: 1;
+ left: 30px;
+ top: 6px;
+ }
+}
+
+.plan__plan-expand {
+ overflow: hidden;
+ transition: all 0.4s ease-in-out;
+ width: 100%;
+}
+
+.plan__plan-details {
+ box-sizing: border-box;
+ color: $gray-dark;
+ padding: 16px;
+ width: 100%;
+ font-size: 13px;
+ opacity: 0.8;
+
+ ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ font-size: 12px;
+
+ li {
+ font-size: 13px;
+ padding: 3px 0;
+ opacity: .8;
+
+ &:before {
+ @include noticon( '\f418', 16px );
+ vertical-align: middle;
+ }
+ }
+ }
+
+ p {
+ margin: 0;
+ }
+
+ .plan__learn-more {
+ font-size: 13px;
+ display: block;
+ margin: 8px 0 0 0;
+ }
+}
+
+@mixin plans-collapsed() {
+ .plan.is-active {
+ .plan__plan-expand {
+ max-height: 500px;
+ }
+ }
+
+ .plan__plan-expand {
+ max-height: 0;
+ }
+
+ .plan__plan-tagline {
+ display: none;
+ }
+
+ .plan__illustration {
+ display: none;
+ }
+
+ .plan__plan-details {
+ border-top: 2px solid lighten( $gray, 20% );
+ float: left;
+ }
+}
+
+@mixin plans-in-three-columns() {
+ .plan {
+ width: 32%;
+ margin: 0 2% 15px 0;
+
+ &:last-child {
+ margin-right: 0;
+ }
+
+ .gridicons-checkmark-circle {
+ left: 50%;
+ top: 64px;
+ margin-left: 30px;
+ }
+ }
+
+ .plan__plan-tagline {
+ color: $gray;
+ font-size: 13px;
+ font-weight: bold;
+ margin: 0 0 5px 0;
+ padding: 0;
+ text-align: center;
+ }
+
+ .plan__plan-details {
+ min-height: 155px;
+ }
+}
+
+.plans.has-sidebar {
+ @include breakpoint( "<960px" ) {
+ @include plans-collapsed();
+ }
+
+ @include breakpoint( ">960px" ) {
+ @include plans-in-three-columns();
+ }
+}
+
+.plans.has-no-sidebar {
+ @include breakpoint( "<660px" ) {
+ @include plans-collapsed();
+ }
+
+ @include breakpoint( ">660px" ) {
+ @include plans-in-three-columns();
+ }
+}
diff --git a/client/components/plans/plans-compare/index.jsx b/client/components/plans/plans-compare/index.jsx
new file mode 100644
index 00000000000000..593e89e732e4ba
--- /dev/null
+++ b/client/components/plans/plans-compare/index.jsx
@@ -0,0 +1,172 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ page = require( 'page' ),
+ classNames = require( 'classnames' ),
+ times = require( 'lodash/utility/times' );
+
+/**
+ * Internal dependencies
+ */
+var observe = require( 'lib/mixins/data-observe' ),
+ SidebarNavigation = require( 'my-sites/sidebar-navigation' ),
+ PlanFeatures = require( 'components/plans/plan-features' ),
+ PlanHeader = require( 'components/plans/plan-header' ),
+ PlanFeatureCell = require( 'components/plans/plan-feature-cell' ),
+ siteSpecificPlansDetailsMixin = require( 'components/plans/site-specific-plan-details-mixin' ),
+ analytics = require( 'analytics' ),
+ HeaderCake = require( 'components/header-cake' ),
+ Card = require( 'components/card' );
+
+module.exports = React.createClass( {
+ displayName: 'PlansCompare',
+
+ mixins: [
+ siteSpecificPlansDetailsMixin,
+ observe( 'sites', 'siteSpecificPlansDetailsList', 'features', 'plans' )
+ ],
+
+ getDefaultProps: function() {
+ return {
+ isInSignup: false
+ };
+ },
+
+ componentDidMount: function() {
+ analytics.tracks.recordEvent( 'calypso_plans_compare', {
+ isInSignup: this.props.isInSignup
+ } );
+ },
+
+ recordViewAllPlansClick: function() {
+ analytics.ga.recordEvent( 'Upgrades', 'Clicked View All Plans' );
+ },
+
+ goBack: function() {
+ var selectedSite = this.props.sites ? this.props.sites.getSelectedSite() : undefined,
+ plansLink = '/plans';
+
+ if ( this.props.backUrl ) {
+ return page( this.props.backUrl );
+ }
+
+ if ( selectedSite ) {
+ plansLink += '/' + selectedSite.slug;
+ }
+
+ this.recordViewAllPlansClick();
+ page( plansLink );
+ },
+
+ featureNames: function( featuresList ) {
+ return featuresList.map( function( feature ) {
+ return (
+
+ { feature.title }
+
+ );
+ } );
+ },
+
+ featureColumns: function( site, plans, featuresList ) {
+ return plans.map( function( plan ) {
+ return (
+
+ );
+ }, this );
+ },
+
+ comparisonTable: function() {
+ var plansColumns,
+ featuresList = this.props.features.get(),
+ plans = this.props.plans.get(),
+ site = this.props.sites ? this.props.sites.getSelectedSite() : undefined,
+ showJetpackPlans = site ? site.jetpack : false;
+
+ plans = plans.filter( function( plan ) {
+ return ( showJetpackPlans === ( 'jetpack' === plan.product_type ) );
+ } );
+
+ if ( this.props.features.hasLoadedFromServer() ) {
+ // Remove features not supported by any plan
+ featuresList = featuresList.filter( function( feature ) {
+ var keepFeature = false;
+ plans.forEach( function( plan ) {
+ if ( plan.product_id in feature ) {
+ keepFeature = true;
+ }
+ } );
+ return keepFeature;
+ } );
+
+ return (
+
+
+
+ { this.featureNames( featuresList ) }
+
+ { this.featureColumns( site, plans, featuresList ) }
+
+ );
+ }
+
+ plansColumns = times( 4, function( i ) {
+ var planFeatures,
+ classes = {
+ 'plan-feature-column': true,
+ 'is-placeholder': true,
+ 'feature-list': i === 0,
+ 'plan-features': i > 0
+ };
+
+ planFeatures = times( 5, function( j ) {
+ return (
+
+
+
+ );
+ } );
+
+ return (
+
+ );
+ } );
+
+ return (
+
+ { plansColumns }
+
+ );
+ },
+
+ render: function() {
+ return (
+
+ {
+ this.props.isInSignup ?
+ null :
+
+ }
+
+ { this.translate( 'Compare Plans' ) }
+
+
+ { this.comparisonTable() }
+
+
+ );
+ }
+
+} );
diff --git a/client/components/plans/plans-compare/style.scss b/client/components/plans/plans-compare/style.scss
new file mode 100644
index 00000000000000..d336fab8ea3da6
--- /dev/null
+++ b/client/components/plans/plans-compare/style.scss
@@ -0,0 +1,10 @@
+.plan-feature-column {
+ display: inline-block;
+ font-size: .9em;
+ vertical-align: top;
+ width: 25%;
+
+ .feature-list {
+ text-align: left;
+ }
+}
diff --git a/client/components/plans/site-specific-plan-details-mixin.js b/client/components/plans/site-specific-plan-details-mixin.js
new file mode 100644
index 00000000000000..f1960626a7474f
--- /dev/null
+++ b/client/components/plans/site-specific-plan-details-mixin.js
@@ -0,0 +1,25 @@
+/**
+ * External dependencies
+ */
+var debug = require( 'debug' )( 'calypso:my-sites:upgrades:plans:site-specific-plan-details-mixin' );
+
+module.exports = {
+ componentWillMount: function() {
+ this.getLatestSiteSpecificPlanDetails();
+ },
+
+ componentWillReceiveProps: function() {
+ this.getLatestSiteSpecificPlanDetails();
+ },
+
+ getLatestSiteSpecificPlanDetails: function() {
+ var site;
+ if ( ! this.props.sites ) {
+ return;
+ }
+
+ site = this.props.sites.getSelectedSite();
+ this.props.siteSpecificPlansDetailsList.fetch( site.domain );
+ debug( 'get latest plan details' );
+ },
+};
diff --git a/client/components/popover/README.md b/client/components/popover/README.md
new file mode 100644
index 00000000000000..5a6227df346d09
--- /dev/null
+++ b/client/components/popover/README.md
@@ -0,0 +1,73 @@
+Popover
+=======
+
+React components that provide support for modal popovers.
+
+- `Popover` is a component that can be used to show any content
+in a popover.
+- `PopoverMenu` is a component based on `Popover` used to show a menu of
+actions in a popover. It is fully keyboard accessible.
+
+### Common `Popover`/`PopoverMenu` Properties
+
+#### `onClose`
+
+The popover's `onClose` property must be set, and should modify the parent's
+state such that the popover's `isVisible` property will be false when `render`
+is called.
+
+#### `isVisible`
+
+By controlling the popover's visibility through the `isVisible` property, the
+popover itself is responsible for providing any CSS transitions to
+animate the opening/closing of the popover. This also keeps the parent's code
+clean and readable, with a minimal amount of boilerplate code required to show
+a popover.
+
+#### `context`
+
+The `context` property must be set to a React ref to the element the popover
+should be attached to (point to).
+
+#### `position`
+
+The `position` property can be one of the following values:
+
+- `top`
+- `top left`
+- `top right`
+- `bottom`
+- `bottom left`
+- `bottom right`
+- `left`
+- `right`
+
+### `Popover` Usage
+
+```js
+Show Popover
+
+ Lorem ipsum dolor sit amet.
+
+```
+
+### `PopoverMenu` Usage
+
+```js
+Show Popover Menu
+
+ Item A
+ Item B
+ Item C
+
+```
diff --git a/client/components/popover/docs/example.jsx b/client/components/popover/docs/example.jsx
new file mode 100644
index 00000000000000..5f285248dcf207
--- /dev/null
+++ b/client/components/popover/docs/example.jsx
@@ -0,0 +1,106 @@
+/**
+* External dependencies
+*/
+var React = require( 'react' );
+
+/**
+* Internal dependencies
+*/
+var Popover = require( 'components/popover' ),
+ PopoverMenu = require( 'components/popover/menu' ),
+ PopoverMenuItem = require( 'components/popover/menu-item' );
+
+var Popovers = React.createClass( {
+ mixins: [ React.addons.PureRenderMixin ],
+
+ getInitialState: function() {
+ return {
+ popoverPosition: 'top',
+ showPopover: false,
+ showPopoverMenu: false
+ };
+ },
+
+ render: function() {
+ return (
+
+
+
Position
+
+ top
+ top left
+ top right
+ left
+ right
+ bottom
+ bottom left
+ bottom right
+
+
+
+
+
+
Show Popover
+
+ Lorem ipsum dolor sit amet.
+
+
+
+
+
Show Popover Menu
+
+ Item A
+ Item B
+ Item C
+
+
+ );
+ },
+
+ _changePopoverPosition: function( event ) {
+ this.setState( { popoverPosition: event.target.value } );
+ },
+
+ _showPopover: function() {
+ this.setState( {
+ showPopover: ! this.state.showPopover,
+ showPopoverMenu: false
+ } );
+ },
+
+ _closePopover: function() {
+ this.setState( { showPopover: false } );
+ },
+
+ _showPopoverMenu: function() {
+ this.setState( {
+ showPopover: false,
+ showPopoverMenu: ! this.state.showPopoverMenu
+ } );
+ },
+
+ _closePopoverMenu: function( action ) {
+ this.setState( { showPopoverMenu: false } );
+
+ if ( action ) {
+ setTimeout( function() {
+ console.log( 'PopoverMenu action: ' + action );
+ }, 0 );
+ }
+ },
+
+ _onPopoverMenuItemBClick: function( closePopover ) {
+ console.log( 'Custom onClick handler' );
+ closePopover();
+ }
+} );
+
+module.exports = Popovers;
diff --git a/client/components/popover/index.jsx b/client/components/popover/index.jsx
new file mode 100644
index 00000000000000..39e1c1cfc745b4
--- /dev/null
+++ b/client/components/popover/index.jsx
@@ -0,0 +1,132 @@
+/**
+ * External dependencies
+ */
+var clickOutside = require( 'click-outside' ),
+ React = require( 'react' ),
+ omit = require( 'lodash/object/omit' ),
+ Tip = require( 'component-tip' );
+
+/**
+ * Internal dependencies
+ */
+var closeOnEsc = require( 'lib/mixins/close-on-esc' ),
+ warn = require( 'lib/warn' );
+
+var Content = React.createClass( {
+ mixins: [ closeOnEsc( '_close' ) ],
+
+ render: function() {
+ return (
+
+ );
+ },
+
+ _close: function() {
+ this.props.onClose();
+ }
+} );
+
+var Popover = React.createClass( {
+ propTypes: {
+ isVisible: React.PropTypes.bool.isRequired,
+ onClose: React.PropTypes.func.isRequired,
+ position: React.PropTypes.string
+ },
+
+ getDefaultProps: function() {
+ return {
+ position: 'top',
+ className: 'popover'
+ };
+ },
+
+ componentDidMount: function() {
+ this._showOrHideTip( {} );
+ },
+
+ componentDidUpdate: function( prevProps ) {
+ this._showOrHideTip( prevProps );
+ },
+
+ componentWillUnmount: function() {
+ clearTimeout( this._clickOutsideTimeout );
+ if ( this._unbindClickOutside ) {
+ this._unbindClickOutside();
+ this._unbindClickOutside = null;
+ }
+ this._tip.remove();
+ React.unmountComponentAtNode( this._container );
+ this._container = null;
+ },
+
+ render: function() {
+ return null;
+ },
+
+ _showOrHideTip: function( prevProps ) {
+ if ( ! this._tip ) {
+ this._tip = new Tip( '' );
+ this._tip.classname = this.props.className;
+ this._container = this._tip.inner;
+ }
+
+ if ( this.props.isVisible && this.props.context ) {
+ React.render(
+ ,
+ this._container
+ );
+
+ if ( ! prevProps.isVisible ) {
+ const contextNode = React.findDOMNode( this.props.context );
+ if ( contextNode.nodeType !== Node.ELEMENT_NODE || contextNode.nodeName.toLowerCase() === 'svg' ) {
+ warn(
+ 'Popover is attached to a %s element (nodeType %d). '
+ + 'This causes problems in IE11 - see 12168-gh-calypso-pre-oss.',
+ contextNode.nodeName,
+ contextNode.nodeType
+ );
+ }
+
+ this._tip.position( this.props.position, { auto: false } );
+ this._tip.show( contextNode );
+
+ if ( this.props.onShow ) {
+ this.props.onShow();
+ }
+
+ this._setupClickOutside();
+ }
+ } else {
+ React.unmountComponentAtNode( this._container );
+ this._tip.hide();
+
+ if ( this._unbindClickOutside ) {
+ this._unbindClickOutside();
+ this._unbindClickOutside = null;
+ }
+ }
+ },
+
+ _setupClickOutside: function() {
+ if ( this._unbindClickOutside ) {
+ this._unbindClickOutside();
+ }
+
+ // have to setup clickOutside after a short delay, otherwise it counts the current
+ // click to show the tip and the tip will never be shown
+ this._clickOutsideTimeout = setTimeout( function() {
+ this._unbindClickOutside = clickOutside( this._container, function( event ) {
+ const contextNode = React.findDOMNode( this.props.context );
+ if ( contextNode && contextNode.contains && ! contextNode.contains( event.target ) ) {
+ this._close( event );
+ }
+ }.bind( this ) );
+ }.bind( this ), 10 );
+ },
+
+ _close: function( event ) {
+ this.props.onClose( event );
+ }
+} );
+
+module.exports = Popover;
diff --git a/client/components/popover/menu-item.jsx b/client/components/popover/menu-item.jsx
new file mode 100644
index 00000000000000..e09685c720fb1b
--- /dev/null
+++ b/client/components/popover/menu-item.jsx
@@ -0,0 +1,35 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ joinClasses = require( 'react/lib/joinClasses' );
+
+var MenuItem = React.createClass( {
+ getDefaultProps: function() {
+ return {
+ isVisible: false,
+ className: '',
+ focusOnHover: true
+ };
+ },
+
+ render: function() {
+ var onMouseOver = this.props.focusOnHover ? this._onMouseOver : null;
+ return (
+
+ { this.props.children }
+
+ );
+ },
+
+ _onMouseOver: function() {
+ this.getDOMNode().focus();
+ }
+} );
+
+module.exports = MenuItem;
diff --git a/client/components/popover/menu.jsx b/client/components/popover/menu.jsx
new file mode 100644
index 00000000000000..1eb7850e81b3ef
--- /dev/null
+++ b/client/components/popover/menu.jsx
@@ -0,0 +1,153 @@
+/**
+* External dependencies
+*/
+var React = require( 'react' );
+
+/**
+* Internal dependencies
+*/
+var Popover = require( 'components/popover' );
+
+var PopoverMenu = React.createClass( {
+ propTypes: {
+ isVisible: React.PropTypes.bool.isRequired,
+ onClose: React.PropTypes.func.isRequired,
+ position: React.PropTypes.string,
+ className: React.PropTypes.string
+ },
+
+ getDefaultProps: function() {
+ return {
+ position: 'top'
+ };
+ },
+
+ componentWillUnmount: function() {
+ // Make sure we don't hold on to reference to the DOM reference
+ this._previouslyFocusedElement = null;
+ },
+
+ render: function() {
+ var children = React.Children.map( this.props.children, this._setPropsOnChild, this );
+
+ return (
+
+
+ { children }
+
+
+ );
+ },
+
+ _setPropsOnChild: function( child ) {
+ if ( child == null ) {
+ return child;
+ }
+
+ let boundOnClose = this._onClose.bind( this, child.props.action ),
+ onClick = boundOnClose;
+
+ if ( child.props.onClick ) {
+ onClick = child.props.onClick.bind( null, boundOnClose );
+ }
+
+ return React.addons.cloneWithProps( child, {
+ onClick: onClick
+ } );
+ },
+
+ _onShow: function() {
+ var elementToFocus = React.findDOMNode( this.refs.menu );
+
+ this._previouslyFocusedElement = document.activeElement;
+
+ if ( elementToFocus ) {
+ elementToFocus.focus();
+ }
+ },
+
+ _isInvalidTarget: function( target ) {
+ return target.tagName === 'HR';
+ },
+
+ /*
+ * Warning:
+ *
+ * This doesn't cover crazy things like a separator at the very top or
+ * bottom.
+ */
+ _getClosestSibling: function( target, isDownwardMotion = true ) {
+ const menu = React.findDOMNode( this.refs.menu );
+
+ let first = menu.firstChild,
+ last = menu.lastChild;
+
+ if ( ! isDownwardMotion ) {
+ first = menu.lastChild;
+ last = menu.firstChild;
+ }
+
+ if ( target === menu ) {
+ return first;
+ }
+
+ const closest = target[ isDownwardMotion ?
+ 'nextSibling' : 'previousSibling' ];
+
+ const sibling = closest || last;
+
+ return this._isInvalidTarget( sibling ) ?
+ this._getClosestSibling( sibling, isDownwardMotion ) :
+ sibling;
+ },
+
+ _onKeyDown: function( event ) {
+ var handled = false,
+ target = event.target,
+ elementToFocus;
+
+ switch ( event.keyCode ) {
+ case 9: // tab
+ this.props.onClose();
+ handled = true;
+ break;
+ case 38: // up arrow
+ elementToFocus = this._getClosestSibling( target, false );
+ handled = true;
+ break;
+ case 40: // down arrow
+ elementToFocus = this._getClosestSibling( target, true );
+ handled = true;
+ break;
+ default:
+ break; // do nothing
+ }
+
+ if ( elementToFocus ) {
+ elementToFocus.focus();
+ }
+
+ if ( handled ) {
+ event.preventDefault();
+ }
+ },
+
+ _onClose: function( action ) {
+ if ( this._previouslyFocusedElement ) {
+ this._previouslyFocusedElement.focus();
+ this._previouslyFocusedElement = null;
+ }
+
+ if ( this.props.onClose ) {
+ this.props.onClose( action );
+ }
+ }
+} );
+
+module.exports = PopoverMenu;
diff --git a/client/components/popover/style.scss b/client/components/popover/style.scss
new file mode 100644
index 00000000000000..d6b00fa5a8e470
--- /dev/null
+++ b/client/components/popover/style.scss
@@ -0,0 +1,196 @@
+/**
+ * "popover" theme for `component/tip`.
+ */
+
+// not sure if we need this
+.tip-hide {
+ opacity: 0;
+}
+
+.popover {
+ font-size: 11px;
+ padding: 10px;
+ z-index: 1000;
+ position: absolute;
+ /* default offset for edge-cases: https://github.com/component/tip/pull/12 */
+ top: 0;
+ left: 0 #{"/*rtl:ignore*/"};
+ right: auto #{"/*rtl:ignore*/"};
+
+ // class coming from `component/tip`
+ .tip-inner {
+ background-color: $white;
+ border: 1px solid lighten( $gray, 20% );
+ border-radius: 4px;
+ box-shadow: 0 2px 5px rgba( 0, 0, 0, 0.1 ),
+ 0 0 56px rgba( 0, 0, 0, 0.075 );
+ text-align: center;
+ }
+
+ // class coming from `component/tip`
+ .tip-arrow {
+ border: 10px dashed lighten( $gray, 20% );
+ height: 0;
+ line-height: 0;
+ position: absolute;
+ width: 0;
+ z-index: 1;
+ }
+
+ &.fade {
+ transition: opacity 100ms;
+ }
+
+ @mixin tip-arrow( $side ) {
+ @if $side == "top" {
+ $opposite-side: "bottom";
+ $cross-side: "left";
+ $cross-opposite-side: "right";
+ } @else if $side == "bottom" {
+ $opposite-side: "top";
+ $cross-side: "left";
+ $cross-opposite-side: "right";
+ } @else if $side == "left" {
+ $opposite-side: "right";
+ $cross-side: "top";
+ $cross-opposite-side: "bottom";
+ } @else if $side == "right" {
+ $opposite-side: "left";
+ $cross-side: "top";
+ $cross-opposite-side: "bottom";
+ }
+
+ &.tip-#{$side} .tip-arrow,
+ &.tip-#{$side}-#{$cross-side} .tip-arrow,
+ &.tip-#{$side}-#{$cross-opposite-side} .tip-arrow {
+ @mixin shared-between-base-and-before {
+ #{$cross-side}: 50% #{"/*rtl:ignore*/"};
+ margin-#{$cross-side}: -10px#{"/*rtl:ignore*/"};
+ border-#{$side}-style: solid#{"/*rtl:ignore*/"};
+ border-#{$opposite-side}: none#{"/*rtl:ignore*/"};
+ border-#{$cross-side}-color: transparent#{"/*rtl:ignore*/"};
+ border-#{$cross-opposite-side}-color: transparent#{"/*rtl:ignore*/"};
+ }
+
+ #{$opposite-side}: 0 #{"/*rtl:ignore*/"};
+ @include shared-between-base-and-before;
+
+ &::before {
+ #{$opposite-side}: 2px #{"/*rtl:ignore*/"};
+ border: 10px solid $white;
+ content: " ";
+ position: absolute;
+ @include shared-between-base-and-before;
+ }
+ }
+ }
+
+ @include tip-arrow( "top" );
+ @include tip-arrow( "bottom" );
+ @include tip-arrow( "left" );
+ @include tip-arrow( "right" );
+
+ &.tip-top-left,
+ &.tip-bottom-left,
+ &.tip-top-right,
+ &.tip-bottom-right {
+ padding-right: 0;
+ padding-left: 0;
+ }
+
+ &.tip-top-left .tip-arrow,
+ &.tip-bottom-left .tip-arrow {
+ left: auto #{"/*rtl:ignore*/"};
+ right: 5px #{"/*rtl:ignore*/"};
+ }
+
+ &.tip-top-right .tip-arrow,
+ &.tip-bottom-right .tip-arrow {
+ left: 15px #{"/*rtl:ignore*/"};
+ }
+
+ &.tip-top-left .tip-inner,
+ &.tip-bottom-left .tip-inner {
+ position: relative;
+ left: 10px #{"/*rtl:ignore*/"};
+ }
+
+ &.tip-top-right .tip-inner,
+ &.tip-bottom-right .tip-inner {
+ position: relative;
+ left: -10px #{"/*rtl:ignore*/"};
+ }
+
+ &.is-dialog-visible {
+ z-index: 100300; /* Above .dialog */
+ }
+}
+
+.popover__menu {
+ display: flex;
+ flex-direction: column;
+ min-width: 200px;
+}
+
+.popover__menu-item {
+ position: relative;
+ background: inherit;
+ border: none;
+ border-radius: 0;
+ color: $gray-dark;
+ cursor: pointer;
+ display: block;
+ font-size: 14px;
+ font-weight: 400;
+ margin: 0;
+ padding: 8px 16px;
+ text-align: left;
+ transition: all 0.05s ease-in-out;
+
+ &:first-child {
+ margin-top: 5px;
+ }
+
+ &:hover,
+ &:focus {
+ background-color: $blue-medium;
+ border: 0;
+ box-shadow: none;
+ color: white;
+
+ .gridicon {
+ color: $white;
+ }
+ }
+
+ &[ disabled ]:hover,
+ &[ disabled ]:focus {
+ background: transparent;
+ cursor: default;
+ }
+
+ &:last-child {
+ margin-bottom: 5px;
+ }
+
+ &::-moz-focus-inner {
+ border: 0;
+ }
+
+ // Menu Items with Icons
+ &.has-icon {
+ padding-left: 42px;
+ }
+
+ // with gridicons
+ .gridicon {
+ color: lighten( $gray, 10 );
+ vertical-align: bottom;
+ margin-right: 8px;
+ }
+}
+
+.popover__hr {
+ margin: 8px 0;
+ background: lighten( $gray, 30 );
+}
diff --git a/client/components/post-excerpt/index.jsx b/client/components/post-excerpt/index.jsx
new file mode 100644
index 00000000000000..b54e4c4a843b0d
--- /dev/null
+++ b/client/components/post-excerpt/index.jsx
@@ -0,0 +1,31 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ joinClasses = require( 'react/lib/joinClasses' );
+
+var PostExcerpt = React.createClass( {
+
+ render: function() {
+ var text = this.props.text,
+ textClass = [ 'post-excerpt__text' ];
+
+ if ( ! text ) {
+ return null;
+ }
+
+ if ( text.length > 80 ) {
+ textClass.push( 'is-long' );
+ }
+
+ textClass = textClass.join( ' ' );
+
+ return (
+
+ );
+ }
+} );
+
+module.exports = PostExcerpt;
diff --git a/client/components/post-excerpt/style.scss b/client/components/post-excerpt/style.scss
new file mode 100644
index 00000000000000..2d589de1e0ef71
--- /dev/null
+++ b/client/components/post-excerpt/style.scss
@@ -0,0 +1,37 @@
+.post-excerpt {
+ .post-excerpt__text {
+ @extend %content-font;
+ margin: 0;
+ position: relative;
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ -webkit-line-clamp: 3;
+ font-size: 16px;
+ line-height: 1.618;
+ max-height: 24px * 3;
+
+ @include breakpoint( "<480px" ) {
+ font-size: 15px;
+ line-height: 22px;
+ max-height: 22px * 3;
+ }
+
+ &.is-long {
+ &:after {
+ content: '';
+ text-align: right;
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ width: 50%;
+ height: 24px;
+ background: linear-gradient(to right, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1) 50%);
+
+ @include breakpoint( "<480px" ) {
+ height: 22px;
+ }
+ }
+ }
+ }
+}
diff --git a/client/components/post-list-fetcher/index.jsx b/client/components/post-list-fetcher/index.jsx
new file mode 100644
index 00000000000000..7d28bba340a97a
--- /dev/null
+++ b/client/components/post-list-fetcher/index.jsx
@@ -0,0 +1,156 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var PostListStore = require( 'lib/posts/post-list-store' ),
+ PostContentImagesStore = require( 'lib/posts/post-content-images-store' ),
+ Dispatcher = require( 'dispatcher' ),
+ actions = require( 'lib/posts/actions' ),
+ pollers = require( 'lib/data-poller' );
+
+var PostListFetcher;
+
+function dispatchQueryActions( query ) {
+ actions.queryPosts( query );
+
+ if ( PostListStore.getPage() === 0 ) {
+ actions.fetchNextPage();
+ }
+}
+
+function queryPosts( props ) {
+ var query = {
+ type: props.type || 'post',
+ siteID: props.siteID,
+ status: props.status,
+ author: props.author,
+ search: props.search,
+ exclude_tree: props.excludeTree,
+ orderBy: props.orderBy,
+ order: props.order,
+ number: props.number,
+ before: props.before,
+ after: props.after
+ };
+
+ if ( props.withCounts ) {
+ query.meta = 'counts';
+ }
+
+ // This is to avoid dispatching during a dispatch.
+ // Not ideal nor a best practice
+ if ( Dispatcher.isDispatching() ) {
+ setTimeout( function() {
+ dispatchQueryActions( query );
+ }, 0 );
+ } else {
+ dispatchQueryActions( query );
+ }
+}
+
+function getPostsState() {
+ return {
+ listId: PostListStore.getID(),
+ posts: PostListStore.getAll(),
+ postImages: PostContentImagesStore.getAll(),
+ page: PostListStore.getPage(),
+ lastPage: PostListStore.isLastPage(),
+ loading: PostListStore.isFetchingNextPage()
+ };
+}
+
+function shouldQueryPosts( props, nextProps ) {
+ // evaluates props that are used to build the post-list query,
+ // withImages is excluded because it is only used client-side
+
+ return props.type !== nextProps.type ||
+ props.status !== nextProps.status ||
+ props.author !== nextProps.author ||
+ props.search !== nextProps.search ||
+ props.excludeTree !== nextProps.excludeTree ||
+ props.withCounts !== nextProps.withCounts ||
+ props.orderBy !== nextProps.orderBy ||
+ props.order !== nextProps.order ||
+ props.number !== nextProps.number ||
+ props.before !== nextProps.before ||
+ props.after !== nextProps.after ||
+ props.siteID !== nextProps.siteID;
+}
+
+PostListFetcher = React.createClass( {
+
+ propTypes: {
+ children: React.PropTypes.element.isRequired,
+ type: React.PropTypes.string,
+ status: React.PropTypes.string,
+ author: React.PropTypes.number,
+ search: React.PropTypes.string,
+ siteID: React.PropTypes.any,
+ withImages: React.PropTypes.bool,
+ withCounts: React.PropTypes.bool,
+ excludeTree: React.PropTypes.number,
+ orderBy: React.PropTypes.oneOf(
+ [ 'title', 'date', 'modified', 'comment_count', 'ID' ]
+ ),
+ order: React.PropTypes.oneOf( [ 'ASC', 'DESC' ] ),
+ number: React.PropTypes.number,
+ before: React.PropTypes.string,
+ after: React.PropTypes.string
+ },
+
+ getDefaultProps: function() {
+ return {
+ orderBy: 'date',
+ order: 'DESC'
+ };
+ },
+
+ componentWillMount: function() {
+ PostListStore.on( 'change', this.onPostsChange );
+ if ( this.props.withImages ) {
+ PostContentImagesStore.on( 'change', this.onPostsChange );
+ }
+ queryPosts( this.props );
+ },
+
+ componentDidMount: function() {
+ this._poller = pollers.add( PostListStore, actions.fetchUpdated, { interval: 60000, leading: false } );
+ },
+
+ componentWillUnmount: function() {
+ pollers.remove( this._poller );
+ PostListStore.off( 'change', this.onPostsChange );
+ if ( this.props.withImages ) {
+ PostContentImagesStore.off( 'change', this.onPostsChange );
+ }
+ },
+
+ componentWillReceiveProps: function( nextProps ) {
+ var listenerChange;
+
+ if ( shouldQueryPosts( this.props, nextProps ) ) {
+ queryPosts( nextProps );
+ }
+
+ if ( nextProps.withImages !== this.props.withImages ) {
+ listenerChange = ( nextProps.withImages ) ? 'on' : 'off';
+ PostContentImagesStore[ listenerChange ]( 'change', this.onPostsChange );
+ }
+ },
+
+ onPostsChange: function() {
+ this.setState( getPostsState( this.props ) );
+ },
+
+ render: function() {
+ // Clone the child element along and pass along state (containing data from the stores)
+ return React.cloneElement( this.props.children, this.state );
+ }
+
+} );
+
+module.exports = PostListFetcher;
diff --git a/client/components/post-schedule/README.md b/client/components/post-schedule/README.md
new file mode 100644
index 00000000000000..fd9ca42c7df26f
--- /dev/null
+++ b/client/components/post-schedule/README.md
@@ -0,0 +1,70 @@
+PostSchedule
+============
+
+This React component implements a small calendar (shown by month) which allows us to select a date through its interface. PostSchedule can be localized to display days/events in a given timezone by passing in a `timezone` or `gmtOffset` properties.
+---
+
+## Example Usage
+
+```js
+var PostSchedule = require( 'components/post-schedule' );
+
+module.exports = React.createClass( {
+
+ // ...
+
+ onDateChange: function( date ) {
+ console.log( 'current date: ', date );
+ },
+
+ render: function() {
+ var events = [
+ {
+ id: 1,
+ title: 'My daily post',
+ date: new Date( '2015-10-15 10:30' ),
+ type: 'personal'
+ },
+ {
+ id: 2,
+ title: 'Happy Birthday!',
+ date: new Date( '2015-07-18 15:00' )
+ }
+ ];
+
+ return (
+
+ );
+ }
+
+ // ...
+
+} );
+```
+
+---
+
+## PostSchedule
+
+#### Props
+
+`events` - **optional** Array - Events array to print into the calendar.
+
+`selectedDay` - **optional** Moment - Takes instance of Date object to set the selected day.
+
+`timezone` - **optional** String (e.g., 'America/Los_Angeles') - Applies offset
+correction when the given timezone is different from the user's timezone. `timezone` takes priority over `gmtOffset`.
+
+`gmtOffset` - **optional** Number - Like as timezone-like manner, an offset correction will be applied if there is difference between the given gmtOffset and the user's gmtOffset. Ignored if `timezone` also passed.
+
+`onDateChange` - **optional** Called when user selects a new date on the calendar. Passed a moment Date object.
+
+`onMonthChange` - **optional** Called when the user selects a new month on the calendar. Passed a moment Date object representing the view date for the calendar, which can be used to determine the currently-showing month and year.
+
+Note: Changing the view month does not change the selected date.
+
+---
diff --git a/client/components/post-schedule/clock.jsx b/client/components/post-schedule/clock.jsx
new file mode 100644
index 00000000000000..c596efa9dc957b
--- /dev/null
+++ b/client/components/post-schedule/clock.jsx
@@ -0,0 +1,185 @@
+/**
+ * External Dependencies
+ */
+import React, { Component, PropTypes } from 'react';
+
+/**
+ * Internal dependencies
+ */
+import InfoPopover from 'components/info-popover';
+import viewport from 'lib/viewport';
+import i18n from 'lib/mixins/i18n';
+
+/**
+ * Local dependencies
+ */
+import utils from './utils';
+
+/**
+ * Globals
+ */
+const noop = () => {};
+
+/**
+ * Check if the given value is useful to use in time format
+ * @param {String} value - time value to check
+ * @return {String} checked value
+ */
+function checkTimeValue( value ) {
+ if ( value !== '0' && value !== '00' && ( value[0] === '0' || Number( value ) > 99 ) ) {
+ value = value.substr( 1 );
+ }
+
+ if ( ! ( isNaN( Number( value ) ) || Number( value ) < 0 || value.length > 2 ) ) {
+ return value;
+ }
+
+ return false;
+}
+
+class PostScheduleClock extends Component {
+
+ handleKeyDown( field, event ) {
+ var operation = event.keyCode - 39,
+ value = Number( event.target.value ),
+ modifiers = this.getTimeValues();
+
+ if ( ! ( -1 === operation || 1 === operation ) ) {
+ return null;
+ }
+
+ value -= operation;
+
+ if ( 'hour' === field ) {
+ value = value > 23 ? 0 : value;
+ value = value < 0 ? 23 : value;
+ } else {
+ value = value > 59 ? 0 : value;
+ value = value < 0 ? 59 : value;
+ }
+
+ modifiers[ field ] = value;
+
+ this.setTime( modifiers );
+ }
+
+ getTimeValues() {
+ var modifiers = {},
+ hour = checkTimeValue( this.refs.timeHourRef.getDOMNode().value ),
+ minute = checkTimeValue( this.refs.timeMinuteRef.getDOMNode().value );
+
+ if ( false !== hour && hour <= 24 ) {
+ modifiers.hour = Number( hour );
+ }
+
+ if ( false !== minute && minute <= 59 ) {
+ modifiers.minute = Number( minute );
+ }
+
+ return modifiers;
+ }
+
+ setTime( modifiers ) {
+ let date = i18n.moment( this.props.date ).set( modifiers );
+ this.props.onChange( date, modifiers );
+ }
+
+ render() {
+ return (
+
+ {
+ this.setTime( this.getTimeValues() );
+ } }
+ onKeyDown={ this.handleKeyDown.bind( this, 'hour' ) }
+ type="text" />
+
+ :
+
+ {
+ this.setTime( this.getTimeValues() );
+ } }
+ onKeyDown={ this.handleKeyDown.bind( this, 'minute' ) }
+ type="text" />
+
+ { this.renderTimezoneBox() }
+
+ );
+ }
+
+ renderTimezoneBox() {
+ if ( ! ( this.props.timezone || utils.isValidGMTOffset( this.props.gmtOffset ) ) ) {
+ return;
+ }
+
+ let diffInHours, formatZ;
+
+ if ( this.props.timezone ) {
+ let tzDate = this.props.date.clone().tz( this.props.timezone );
+ diffInHours = tzDate.utcOffset() - i18n.moment().utcOffset();
+ formatZ = tzDate.format( ' Z ' );
+ } else if ( utils.isValidGMTOffset( this.props.gmtOffset ) ) {
+ let utcDate = this.props.date.clone().utcOffset( this.props.gmtOffset );
+ diffInHours = utcDate.utcOffset() - i18n.moment().utcOffset();
+ formatZ = utcDate.format( ' Z ' );
+ }
+
+ if ( ! diffInHours ) {
+ return;
+ }
+
+ diffInHours = diffInHours / 60;
+ diffInHours = Math.round( diffInHours * 100 ) / 100;
+ diffInHours = ( diffInHours > 0 ? '+' : '' ) + diffInHours + 'h';
+
+ const popoverPosition = viewport.isMobile() ? 'top' : 'right';
+
+ return (
+
+
+ {
+ i18n.translate( 'Site %(diff)s from you', {
+ args: { diff: diffInHours }
+ } )
+ }
+
+
+ { this.props.timezone
+ ? this.props.timezone.replace( /(\/)/ig, ' $1 ' )
+ : 'UTC'
+ }
+ { formatZ }
+
+
+
+ );
+ }
+};
+
+/**
+ * Statics
+ */
+PostScheduleClock.propTypes = {
+ date: PropTypes.object.isRequired,
+ timezone: PropTypes.string,
+ gmtOffset: PropTypes.number,
+ onChange: PropTypes.func
+};
+
+PostScheduleClock.defaultProps = {
+ onChange: noop
+};
+
+export default PostScheduleClock;
diff --git a/client/components/post-schedule/docs/example.jsx b/client/components/post-schedule/docs/example.jsx
new file mode 100644
index 00000000000000..95057684f31c68
--- /dev/null
+++ b/client/components/post-schedule/docs/example.jsx
@@ -0,0 +1,263 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+
+/**
+ * Internal dependencies
+ */
+import PostSchedule from 'components/post-schedule';
+import TimezoneDropdown from 'components/timezone-dropdown';
+import Gridicon from 'components/gridicon';
+import Card from 'components/card';
+
+/**
+ * Date Picker Demo
+ */
+export default React.createClass( {
+ displayName: 'PostSchedule',
+
+ mixins: [ React.addons.PureRenderMixin ],
+
+ getInitialState() {
+ var date = new Date(),
+ tz = 'America/Los_Angeles',
+ tomorrow = ( new Date() ).setDate( date.getDate() + 1 );
+
+ date.setDate( date.getDate() + 3 );
+ date.setMilliseconds( 0 );
+ date.setSeconds( 0 );
+
+ date = this.moment( date ).tz( tz );
+ date.set( { hour: 11, minute: 20 } );
+
+ return {
+ events: [
+ {
+ id: 1,
+ title: 'Happy 30th birthday',
+ date: new Date( '2015-07-18T15:00:00' ),
+ type: 'birthday'
+ },
+ {
+ id: 2,
+ title: 'Tomorrow is tomorrow',
+ date: tomorrow
+ }
+ ],
+ gmtOffset: 1,
+ timezone: tz,
+ date: date
+ };
+ },
+
+ componentWillMount: function() {
+ this.setState( {
+ isFuture: true
+ } );
+ },
+
+ setDate( date ) {
+ console.log( `date: %s`, date.format() );
+
+ this.setState( {
+ isFuture: +new Date() < +new Date( date ),
+ date: date
+ } );
+ },
+
+ setMonth( date ) {
+ console.log( `month: %s`, date.format() );
+ this.setState( { month: date } );
+ },
+
+ setGMTOffset( event ) {
+ if ( 'undefined' === typeof event.target.value ) {
+ return;
+ }
+
+ this.setState( { gmtOffset: Number( event.target.value ) } );
+ },
+
+ setTimezone( zone ) {
+ this.setState( { timezone: zone } );
+ },
+
+ clearState( state, event ) {
+ event.preventDefault();
+ this.setState( { [ state ]: null } );
+ },
+
+ render() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ owner
+
+ ownee
+
+
+
+
state.timezone
+
+ { this.state.timezone || 'not defined' }
+
+
+
+
+
+ clean timezone
+
+
+
+
+
+
+
state.date
+
+ {
+ this.state.date
+ ? this.state.date.format()
+ : 'not defined'
+ }
+
+
+
+ clean selectedDay
+
+
+
+
+
+
+
+ owner
+
+ ownee
+
+
+
+
prop.onDateChange( date )
+
+ {
+ this.state.date
+ ? this.state.date.format()
+ : 'not defined'
+ }
+
+
+
+
+
prop.onMonthChange( date )
+
+ {
+ this.state.month
+ ? this.state.month.format()
+ : 'not defined'
+ }
+
+
+
+
+
+ chronologically:
+ { this.renderDateReference() }
+
+
+
+
+ );
+ },
+
+ renderDateReference() {
+ if ( ! this.state.date ) {
+ return;
+ }
+
+ return (
+
+ {
+ this.state.isFuture
+ ? 'FUTURE'
+ : 'PRESENT or PAST'
+ }
+
+ );
+ }
+} );
diff --git a/client/components/post-schedule/header-controls.jsx b/client/components/post-schedule/header-controls.jsx
new file mode 100644
index 00000000000000..7aa88c46a1c15b
--- /dev/null
+++ b/client/components/post-schedule/header-controls.jsx
@@ -0,0 +1,50 @@
+/**
+ * External Dependencies
+ */
+import React from 'react';
+
+/**
+ * Internal dependencies
+ */
+import Gridicon from 'components/gridicon';
+
+/**
+ * Globals
+ */
+var noop = () => {};
+
+export default React.createClass( {
+ propTypes: {
+ onYearChange: React.PropTypes.func
+ },
+
+ getDefaultProps() {
+ return {
+ onYearChange: noop
+ };
+ },
+
+ render() {
+ return (
+
+ {
+ this.props.onYearChange( 1 );
+ } }
+ >
+
+
+
+ {
+ this.props.onYearChange( -1 );
+ } }
+ >
+
+
+
+ );
+ }
+} );
diff --git a/client/components/post-schedule/header.jsx b/client/components/post-schedule/header.jsx
new file mode 100644
index 00000000000000..12211f80c6298c
--- /dev/null
+++ b/client/components/post-schedule/header.jsx
@@ -0,0 +1,85 @@
+/**
+ * External Dependencies
+ */
+import React from 'react';
+
+/**
+ * Local dependencies
+ */
+import HeaderControl from './header-controls';
+
+/**
+ * Globals
+ */
+var noop = () => {};
+
+export default React.createClass( {
+ propTypes: {
+ date: React.PropTypes.object,
+ onDateChange: React.PropTypes.func,
+ },
+
+ getDefaultProps() {
+ return { onDateChange: noop };
+ },
+
+ getInitialState() {
+ return {
+ showYearControls: false
+ }
+ },
+
+ setToCurrentMonth() {
+ var month = this.moment().month();
+ this.props.onDateChange( this.props.date.month( month ) );
+ },
+
+ setToCurrentYear() {
+ var year = this.moment().year();
+ this.props.onDateChange( this.props.date.year( year ) );
+ },
+
+ setYear( modifier ) {
+ var date = this.moment( this.props.date );
+ date.year( date.year() + modifier );
+
+ if ( 0 > date.year() || date.year() > 9999 ) {
+ return null;
+ }
+
+ this.props.onDateChange( date );
+ },
+
+ render() {
+ return (
+
+
+ { this.props.date.format( 'MMMM' ) }
+
+
+
{
+ this.setState( { showYearControls: true } );
+ } }
+
+ onMouseLeave={ () => {
+ this.setState( { showYearControls: false } );
+ } }
+ >
+
+ { this.props.date.format( 'YYYY' ) }
+
+
+ {
+ this.state.showYearControls &&
+
+ }
+
+
+ );
+ }
+} );
diff --git a/client/components/post-schedule/index.jsx b/client/components/post-schedule/index.jsx
new file mode 100644
index 00000000000000..c49b19cb56c471
--- /dev/null
+++ b/client/components/post-schedule/index.jsx
@@ -0,0 +1,222 @@
+/**
+ * External dependencies
+ */
+import React, { PropTypes, Component } from 'react';
+import i18n from 'lib/mixins/i18n';
+
+/**
+ * Internal dependencies
+ */
+import InputChrono from 'components/input-chrono';
+import DatePicker from 'components/date-picker';
+import User from 'lib/user';
+
+/**
+ * Local dependencies
+ */
+import Clock from './clock';
+import Header from './header';
+import utils from './utils';
+
+var user = new User(),
+ noop = () => {};
+
+class PostSchedule extends Component {
+ constructor( props ) {
+ super( props );
+
+ this.state = {
+ calendarViewDate: i18n.moment(
+ this.props.selectedDay
+ ? this.props.selectedDay
+ : new Date()
+ )
+ };
+ }
+
+ componentWillMount() {
+ if ( ! this.props.selectedDay ) {
+ return this.setState( { localizedDate: null } );
+ }
+
+ this.setState( {
+ localizedDate: this.getDateToUserLocation( this.props.selectedDay )
+ } );
+ }
+
+ componentWillReceiveProps( nextProps ) {
+ if ( this.props.selectedDay === nextProps.selectedDay ) {
+ return;
+ }
+
+ if ( ! nextProps.selectedDay ) {
+ return this.setState( { localizedDate: null } );
+ }
+
+ this.setState( {
+ localizedDate: this.getDateToUserLocation( nextProps.selectedDay )
+ } );
+ }
+
+ locale() {
+ return {
+ formatMonthTitle: function() {
+ return;
+ }
+ };
+ }
+
+ events() {
+ return this.props.events.concat(
+ this.getEventsFromPosts( this.props.posts )
+ );
+ }
+
+ getEventsFromPosts( postsList = [] ) {
+ return postsList.map( post => {
+ let localDate = this.getDateToUserLocation( post.date );
+
+ return {
+ id: post.ID,
+ title: post.title,
+ date: localDate.toDate()
+ }
+ } );
+ }
+
+ getDateToUserLocation( date ) {
+ return utils.convertDateToUserLocation(
+ date || new Date(),
+ this.props.timezone,
+ this.props.gmtOffset
+ );
+ }
+
+ setCurrentMonth( date ) {
+ date = i18n.moment( date );
+ this.props.onMonthChange( date );
+ this.setState( { calendarViewDate: date } );
+ }
+
+ setViewDate( date ) {
+ this.setState( { calendarViewDate: i18n.moment( date ) } );
+ }
+
+ getCurrentDate() {
+ return i18n.moment( this.state.localizedDate || this.getDateToUserLocation() );
+ }
+
+ updateDate( date ) {
+ this.setState( { calendarViewDate: date } );
+
+ this.props.onDateChange( utils.convertDateToGivenOffset(
+ date,
+ this.props.timezone,
+ this.props.gmtOffset
+ ) );
+ }
+
+ /** Renders **/
+
+ renderInputChrono() {
+ var lang = user.getLanguage(),
+ date = this.getCurrentDate(),
+ chronoText;
+
+ if ( this.state.localizedDate ) {
+ let today = i18n.moment().startOf( 'day' ),
+ selected = i18n.moment( date ).startOf( 'day' ),
+ diffInMinutes = selected.diff( today, 'days' );
+
+ if ( -7 <= diffInMinutes && diffInMinutes <= 6 ) {
+ chronoText = date.calendar();
+ } else {
+ chronoText = date.format( 'L LT' );
+ }
+ }
+
+ return (
+
+
+
+
+
+ );
+ }
+
+ renderClock() {
+ let date = this.state.localizedDate;
+
+ if ( ! date ) {
+ date = this.getDateToUserLocation( new Date() );
+ }
+
+ return (
+
+ );
+ }
+
+ render() {
+ return (
+
+
+
+ { this.renderInputChrono() }
+
+
+
+ { this.renderClock() }
+
+ );
+ }
+};
+
+/**
+ * Statics
+ */
+PostSchedule.displayName = 'PostSchedule';
+
+PostSchedule.propTypes = {
+ events: PropTypes.array,
+ posts: PropTypes.array,
+ timezone: PropTypes.string,
+ gmtOffset: PropTypes.number,
+
+ onDateChange: PropTypes.func,
+ onMonthChange: PropTypes.func
+};
+
+PostSchedule.defaultProps = {
+ posts: [],
+ events: [],
+ onDateChange: noop,
+ onMonthChange: noop
+};
+
+export default PostSchedule;
diff --git a/client/components/post-schedule/style.scss b/client/components/post-schedule/style.scss
new file mode 100644
index 00000000000000..98fb2091fb2213
--- /dev/null
+++ b/client/components/post-schedule/style.scss
@@ -0,0 +1,125 @@
+.post-schedule {
+ position: relative;
+}
+
+.post-schedule__header {
+ height: 26px;
+ text-align: center;
+ position: absolute;
+ z-index: 1;
+ top: 47px;
+ color: $gray;
+ font-size: 18px;
+ font-weight: 300;
+ width: 80%;
+ margin: 0 10%;
+
+ &:first-letter {
+ text-transform: uppercase;
+ }
+}
+
+.post-schedule .input-chrono__container {
+ margin: 0 -16px;
+
+ input.input-chrono {
+ padding-left: 16px;
+ padding-right: 16px;
+ border: 0;
+ }
+}
+
+.post-schedule__header-month {
+ cursor: pointer;
+}
+
+.post-schedule__header-year {
+ display: inline-block;
+ position: relative;
+ text-align: center;
+ font-size: 12px;
+ color: lighten( $gray, 10% );
+ height: 20;
+ line-height: 20px;
+ padding: 0 5px;
+ margin-left: 10px;
+ cursor: pointer;
+}
+
+.post-schedule__year-control-up,
+.post-schedule__year-control-down {
+ position: absolute;
+ width: 100%;
+ left: 0;
+ height: 16px;
+ cursor: pointer;
+ color: lighten( $gray, 10% );
+
+ &:hover {
+ color: $gray;
+ }
+}
+
+.post-schedule__year-control-up {
+ top: -12px;
+}
+
+.post-schedule__year-control-down {
+ bottom: -12px;
+}
+
+.post-schedule__clock {
+ text-align: center;
+ margin: 15px auto 10px;
+ color: $gray;
+ font-size: 12px;
+}
+
+hr.post-schedule__hr {
+ height: 1px;
+ background: lighten( $gray, 30% );
+ margin: 0 -16px;
+}
+
+input.post-schedule__clock_time {
+ height: 26px;
+ line-height: 26px;
+ display: inline-block;
+ border: 1px solid lighten( $gray, 30% );
+ width: 42px;
+ text-align: center;
+ padding: 0;
+ color: lighten( $gray, 10% );
+ font-size: 12px;
+}
+
+.post-schedule__clock-divisor {
+ margin: 0 6px;
+}
+
+.info-popover__tooltip.post-schedule__timezone-info,
+.post-schedule__clock-timezone {
+ color: $gray;
+ line-height: 20px;
+}
+
+.post-schedule__clock-timezone {
+ margin-top: 10px;
+ padding: 10px 0 0;
+ border-top: 1px solid lighten( $gray, 25% );
+}
+
+.post-schedule__timezone-info {
+ display: inline-block;
+ vertical-align: middle;
+ line-height: 10px;
+ margin-left: 5px;
+}
+
+.info-popover__tooltip.post-schedule__timezone-info {
+ margin-left: 0;
+
+ .tip-inner {
+ font-size: 12px;
+ }
+}
diff --git a/client/components/post-schedule/utils.js b/client/components/post-schedule/utils.js
new file mode 100644
index 00000000000000..6a1dc2fb1e550d
--- /dev/null
+++ b/client/components/post-schedule/utils.js
@@ -0,0 +1,62 @@
+/**
+ * Internal dependencies
+ */
+import i18n from 'lib/mixins/i18n';
+
+export default {
+ isValidGMTOffset( gmtOffset ) {
+ return 'number' === typeof gmtOffset;
+ },
+
+ /**
+ * Return localized date depending of given timezone and gmtOffset
+ * parameters.
+ *
+ * @param {Moment} date - date instance
+ * @param {String} tz - timezone
+ * @param {Number} gmt - gmt offset
+ * @return {Moment} localized date
+ */
+ getLocalizedDate( date, tz, gmt ) {
+ date = i18n.moment( date );
+
+ if ( tz ) {
+ date.tz( tz );
+ } else if ( this.isValidGMTOffset( gmt ) ) {
+ date.utcOffset( gmt );
+ }
+
+ return date;
+ },
+
+ convertDateToUserLocation( date, tz, gmt ) {
+ if ( ! ( tz || this.isValidGMTOffset( gmt ) ) ) {
+ return i18n.moment( date );
+ }
+
+ return this.getDateInLocalUTC( date )
+ .subtract( this.getTimeOffset( date, tz, gmt ), 'minute' );
+ },
+
+ convertDateToGivenOffset( date, tz, gmt ) {
+ date = this.getLocalizedDate( date, tz, gmt )
+ .add( this.getTimeOffset( date, tz, gmt ), 'minute' );
+
+ if ( ! tz && this.isValidGMTOffset( gmt ) ) {
+ date.utcOffset( gmt );
+ };
+
+ return date;
+ },
+
+ getTimeOffset( date, tz, gmt ) {
+ const userLocalDate = this.getDateInLocalUTC( date ),
+ localizedDate = this.getLocalizedDate( date, tz, gmt );
+
+ return userLocalDate.utcOffset() - localizedDate.utcOffset();
+ },
+
+ getDateInLocalUTC( date ) {
+ return i18n.moment( date.format ? date.format() : date )
+ }
+}
diff --git a/client/components/progress-bar/README.md b/client/components/progress-bar/README.md
new file mode 100644
index 00000000000000..8e25623c799e7d
--- /dev/null
+++ b/client/components/progress-bar/README.md
@@ -0,0 +1,29 @@
+Progress Bar
+==============
+
+This component is used to display a single bar in a background color,
+and another bar on top of that filled, in a different color,
+with a % of the size of the background.
+
+#### How to use:
+
+```js
+var ProgressBar = require( 'components/progress-bar' );
+
+render: function() {
+ return ;
+}
+```
+
+#### Props
+
+* `value`: a number representing the amount over the total to be represented in the bar (required).
+* `total`: a number representing the value corresponding to the 100% of the bar (optional, default == 100).
+* `color`: a string of a css color (optional).
+* `title`: a string for the title attribute (optional).
+* `className`: You can add classes to either (optional).
diff --git a/client/components/progress-bar/docs/example.jsx b/client/components/progress-bar/docs/example.jsx
new file mode 100644
index 00000000000000..5c62b87064fded
--- /dev/null
+++ b/client/components/progress-bar/docs/example.jsx
@@ -0,0 +1,28 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var ProgressBar = require( 'components/progress-bar' );
+
+module.exports = React.createClass( {
+ displayName: 'ProgressBar',
+
+ mixins: [ React.addons.PureRenderMixin ],
+
+ render: function() {
+ return (
+
+ );
+ }
+} );
diff --git a/client/components/progress-bar/index.jsx b/client/components/progress-bar/index.jsx
new file mode 100644
index 00000000000000..06969a2875193e
--- /dev/null
+++ b/client/components/progress-bar/index.jsx
@@ -0,0 +1,43 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ joinClasses = require( 'react/lib/joinClasses' );
+
+module.exports = React.createClass( {
+
+ displayName: 'ProgressBar',
+
+ getDefaultProps: function() {
+ return { total: 100 };
+ },
+
+ propTypes: {
+ value: React.PropTypes.number.isRequired,
+ total: React.PropTypes.number,
+ color: React.PropTypes.string,
+ title: React.PropTypes.string,
+ className: React.PropTypes.string
+ },
+
+ renderBar: function() {
+ var styles = { width: Math.ceil( this.props.value / this.props.total * 100 ) + '%' },
+ title = this.props.title
+ ? { this.props.title }
+ : null;
+
+ if ( this.props.color ) {
+ styles.backgroundColor = this.props.color;
+ }
+
+ return { title }
;
+ },
+
+ render: function() {
+ return (
+
+ { this.renderBar() }
+
+ );
+ }
+} );
diff --git a/client/components/progress-bar/style.scss b/client/components/progress-bar/style.scss
new file mode 100644
index 00000000000000..c74c854649f383
--- /dev/null
+++ b/client/components/progress-bar/style.scss
@@ -0,0 +1,28 @@
+.progress-bar {
+ width: 100%;
+ display: inline-block;
+ position: relative;
+ height: 9px;
+ background-color: lighten( $gray, 20% );
+ border-radius: 4.5px;
+}
+
+.progress-bar__progress {
+ display: inline-block;
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100%;
+ background-color: lighten( $blue-dark, 10% );
+ border-radius: 4.5px;
+}
+
+/* Percentage bar */
+.percentage-bar {
+ border-radius: 0;
+ height: 8px;
+ width: 150px;
+ .progress-bar__progress{
+ border-radius: 0;
+ }
+}
diff --git a/client/components/progress-indicator/README.md b/client/components/progress-indicator/README.md
new file mode 100644
index 00000000000000..c1cebc7e363c6d
--- /dev/null
+++ b/client/components/progress-indicator/README.md
@@ -0,0 +1,48 @@
+Progress Indicator
+=========
+
+This component is used to display a progress indicator to the user.
+The progress indicator lets the user know when an action is in progress,
+has completed or has failed.
+
+#### How to use:
+
+```js
+var ProgressIndicator = require( 'components/progress-indicator' );
+
+render: function() {
+ return (
+
+ );
+}
+```
+
+#### Props
+
+* `status`: Status can be one of the following: 'failed', 'success', 'complete', 'in-progress', 'processing', 'inactive'
+
+ - `failed` = Diplays a failed state to the user.
+ - `success` = Display the success action to the user.
+ - `complete` = Displays that the action is completed.
+ - `in-progress` = Dispays that the action requested by the user is in progress. The progress indicator goes to 90% completion.
+ - `processing` = Display the progress indicatror as spinning.
+ - `inactive` = Hides the progress indicatior.
+
+* `className`: Add your own class to the progress indicator for easier styling.
+
+
+#### More examples
+```js
+ /* status should be hidden */
+
+
+
+
+
+
+
+
+
+/* later somewhere */
+this.setState( { autoupdatingStatus: 'success' } );
+```
diff --git a/client/components/progress-indicator/index.jsx b/client/components/progress-indicator/index.jsx
new file mode 100644
index 00000000000000..77fa92db7ccba5
--- /dev/null
+++ b/client/components/progress-indicator/index.jsx
@@ -0,0 +1,48 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ joinClasses = require( 'react/lib/joinClasses' ),
+ classNames = require( 'classnames' );
+
+module.exports = React.createClass( {
+ displayName: 'ProgressIndicator',
+
+ getDefaultProps: function() {
+ return {
+ status: 'inactive'
+ };
+ },
+
+ propTypes: {
+ status: React.PropTypes.string
+ },
+
+ render: function() {
+ var last = null,
+ status = this.props.status,
+ classes;
+
+ if ( 'failed' === status ) {
+ last = (
);
+ } else if ( 'success' === status ) {
+ last = (
);
+ }
+
+ classes = classNames( {
+ 'progress-indicator': true,
+ 'is-in-progress': 'in-progress' === status,
+ 'is-processing': 'processing' === status,
+ 'is-complete': 'success' === status || 'complete' === status,
+ 'is-inactive': 'inactive' === status
+ } );
+
+ return (
+
+ );
+ }
+} );
diff --git a/client/components/progress-indicator/style.scss b/client/components/progress-indicator/style.scss
new file mode 100644
index 00000000000000..8409b804a93f20
--- /dev/null
+++ b/client/components/progress-indicator/style.scss
@@ -0,0 +1,217 @@
+// ==========================================================================
+// .progress-indicator
+//
+// Includs a spinner and the ability to have specific progress-indicator__percentages using the
+// same elements.
+//
+// Check out this demo to understand how this works:
+// http://codepen.io/MichaelArestad/pen/2740d550eaa89cef6c753316595298b7
+// ==========================================================================
+
+
+// Does progress-indicator__percentage math for the spinner
+@function progress-indicator__percent($progress-indicator__percent) {
+ @return ( 360 * $progress-indicator__percent / 200 );
+}
+
+
+// Global spinner settings
+$progress-indicator__size: 20px;
+$progress-indicator__transition-speed: .4s;
+
+
+// Progress-indicator container
+.progress-indicator {
+ position: relative;
+ width: $progress-indicator__size;
+ height: $progress-indicator__size;
+ border-radius: 50%;
+ box-shadow: inset 0 0 0 1px darken( $gray, 10% );
+ box-sizing: border-box;
+}
+
+// Progress indicator guts and magic
+.progress-indicator__half {
+ position: absolute;
+ top: 0;
+ left: 50%;
+ width: 50%;
+ height: 100%;
+ overflow: hidden;
+ transform-origin: 0% 50%;
+ transform: translateZ(0);
+ transition: all $progress-indicator__transition-speed ease-in-out;
+
+ &:before {
+ content: '';
+ position: absolute;
+ top: 0;
+ right: 100%;
+ width: 100%;
+ height: 100%;
+ border: 3px solid $blue-medium;
+ border-right: 0;
+ border-top-left-radius: 500px;
+ border-bottom-left-radius: 500px;
+ box-sizing: border-box;
+ transform-origin: 100% 50%;
+ transition: all $progress-indicator__transition-speed ease-in-out;
+ }
+}
+
+
+// Example:
+
+// .progress-indicator__half:before,
+// .is-latter {
+// [data-progress-indicator__percent="75"] & {
+// transform: rotate(#{progress-indicator__percent(45)}deg);
+// }
+// }
+
+.progress-indicator.is-inactive {
+ visibility: hidden;
+}
+
+.progress-indicator.is-inactive {
+ .progress-indicator__half,
+ .progress-indicator__half:before {
+ transition: none;
+ }
+}
+
+.progress-indicator.is-in-progress {
+ .progress-indicator__half:before,
+ .is-latter {
+ // transition: none;
+ transform: rotate(#{progress-indicator__percent(90)}deg);
+ }
+ .progress-indicator__half,
+ .progress-indicator__half:before {
+ transition-duration: 5s;
+ transition-timing-function: ease-out;
+ }
+}
+
+// is complete
+.progress-indicator.is-complete {
+ .progress-indicator__half:before,
+ .is-latter {
+ transform: rotate(#{progress-indicator__percent(100)}deg);
+ }
+}
+
+
+// Animations for infinite spinner
+
+// spins pieces of the circle
+@keyframes progress-indicator__spin {
+ 100% {
+ transform: rotate( 360deg );
+ }
+}
+
+// spins the whole thing
+
+@keyframes progress-indicator__fancy-spin {
+ 50% {
+ transform: rotate( 360deg );
+ }
+ 50.01% {
+ transform: rotate( 540deg );
+ }
+ 100% {
+ transform: rotate( 900deg );
+ }
+}
+
+
+$progress-indicator--spin-time: 3.4s;
+
+.progress-indicator.is-processing {
+ animation: progress-indicator__fancy-spin $progress-indicator--spin-time ease-in-out infinite;
+
+
+ .progress-indicator__half,
+ .progress-indicator__half:before {
+ transform: none;
+ }
+ .progress-indicator__half:before,
+ .progress-indicator__half.is-latter {
+ animation: progress-indicator__spin $progress-indicator--spin-time ease-in-out infinite;
+ }
+}
+
+
+// x styling
+.progress-indicator__cancel.noticon:before { // TODO remove noticon
+ display: block;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ color: darken( $gray, 10% );
+ text-align: center;
+ line-height: $progress-indicator__size;
+ text-decoration: none;
+ z-index: 1;
+}
+
+
+// smile styling
+.progress-indicator .is-success,
+.progress-indicator .is-problem {
+ position: absolute;
+ top: $progress-indicator__size/2 + 1;
+ left: $progress-indicator__size/4;
+ width: $progress-indicator__size/2;
+ height: $progress-indicator__size/4;
+ border-bottom-right-radius: 500px;
+ border-bottom-left-radius: 500px;
+ background: $blue-medium;
+ z-index: 2;
+
+ &:before,
+ &:after {
+ content: '';
+ position: absolute;
+ top: -6px;
+ left: 0;
+ width: 4px;
+ height: 4px;
+ border-radius: 50%;
+ background: $blue-medium;
+ }
+ &:after {
+ left: auto;
+ right: 0;
+ }
+}
+
+.progress-indicator {
+ .is-success {
+ animation: progress-indicator__appear .6s ease-in-out;
+ }
+}
+
+@keyframes progress-indicator__appear {
+ 0% {
+ opacity: 0;
+ }
+ 66% {
+ opacity: 0;
+ }
+}
+
+.progress-indicator .is-problem {
+ background: darken( $gray, 10% );
+ border: 0;
+ border-top-right-radius: 500px;
+ border-top-left-radius: 500px;
+
+ &:before,
+ &:after {
+ background: darken( $gray, 10% );
+ }
+}
diff --git a/client/components/pulsing-dot/index.jsx b/client/components/pulsing-dot/index.jsx
new file mode 100644
index 00000000000000..f92f41fc27b6ee
--- /dev/null
+++ b/client/components/pulsing-dot/index.jsx
@@ -0,0 +1,27 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' ),
+ classnames = require( 'classnames' );
+
+var PulsingDot = React.createClass( {
+
+ getDefaultProps: function() {
+ return {
+ active: false
+ };
+ },
+
+ render: function() {
+ var className = classnames( {
+ 'pulsing-dot': true,
+ 'is-active': this.props.active
+ } );
+ return (
+
+ );
+ }
+
+} );
+
+module.exports = PulsingDot;
diff --git a/client/components/pulsing-dot/style.scss b/client/components/pulsing-dot/style.scss
new file mode 100644
index 00000000000000..b380d7784f10e3
--- /dev/null
+++ b/client/components/pulsing-dot/style.scss
@@ -0,0 +1,27 @@
+.pulsing-dot {
+ background: $gray-light;
+ transform: translate3d( 0, 0, 0 );
+ position: absolute;
+ top: 45%;
+ left: 50%;
+ margin-left: -20px 0 0 -20px;
+ display: block;
+ width: 6px;
+ height: 6px;
+ border: none;
+ box-shadow: 0 0 0 0 rgba( 168, 190, 206, 0.7 );
+ border-radius: 100%;
+ v-align: middle;
+ animation: dot-pulse 1.25s infinite cubic-bezier( 0.66, 0, 0, 1 );
+ animation-play-state: paused;
+
+ &.is-active {
+ animation-play-state: running;
+ }
+}
+
+@keyframes dot-pulse {
+ to {
+ box-shadow: 0 0 0 15px rgba( 90, 153, 220, 0 );
+ }
+}
diff --git a/client/components/rating/README.md b/client/components/rating/README.md
new file mode 100644
index 00000000000000..309e74daf8d536
--- /dev/null
+++ b/client/components/rating/README.md
@@ -0,0 +1,27 @@
+Rating
+======
+
+This component is used to display a set of 5 stars, full colored, empty or half-colored,
+that represents a rating in a scale between 0 and 5.
+
+#### How to use:
+
+```js
+var Rating = require( 'components/rating' );
+
+render: function() {
+ return (
+
+ );
+}
+```
+
+#### Props
+
+* `rating`: Number - A number with the 0-100 rating to render
+
+* `size`: **optional** Number - `font-size` value in pixels. If it isn't
+ defined font-size will be set to `inherit`
diff --git a/client/components/rating/docs/example.jsx b/client/components/rating/docs/example.jsx
new file mode 100644
index 00000000000000..404e5504efa142
--- /dev/null
+++ b/client/components/rating/docs/example.jsx
@@ -0,0 +1,27 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+/**
+ * Internal dependencies
+ */
+var Rating = require( 'components/rating' );
+
+module.exports = React.createClass( {
+ displayName: 'Rating',
+
+ mixins: [ React.addons.PureRenderMixin ],
+
+ render: function() {
+ return (
+
+ );
+ }
+} );
diff --git a/client/components/rating/index.jsx b/client/components/rating/index.jsx
new file mode 100644
index 00000000000000..ea0f06d65fc7d7
--- /dev/null
+++ b/client/components/rating/index.jsx
@@ -0,0 +1,77 @@
+/**
+ * External dependencies
+ */
+var React = require( 'react' );
+
+module.exports = React.createClass( {
+
+ displayName: 'Rating',
+
+ getDefaultProps: function() {
+ return { rating: 0 };
+ },
+
+ propTypes: {
+ rating: React.PropTypes.number,
+ size: React.PropTypes.number
+ },
+
+ getStars: function() {
+ var i,
+ stars = [],
+ ratingOverTen = Math.ceil( this.props.rating / 10 ),
+ numberOfStars = Math.floor( ratingOverTen / 2 ),
+ hasHalfStar = ( ( ratingOverTen / 2 ) % 1 >= 0.5 ),
+ starStyles = {
+ fontSize: this.props.size
+ ? this.props.size + 'px'
+ : 'inherit'
+ };
+
+ for ( i = 0; i < numberOfStars; i++ ) {
+ stars.push(
+
+ );
+ }
+
+ if ( hasHalfStar ) {
+ stars.push(
+
+ );
+ }
+
+ while ( stars.length < 5 ) {
+ stars.push(
+
+ );
+ }
+
+ return stars;
+ },
+
+ render: function() {
+ var ratingStyles = {
+ width: this.props.size
+ ? ( this.props.size * 5 ) + 'px'
+ : '100%'
+ };
+
+ return (
+
+ { this.getStars() }
+
+ );
+ }
+} );
diff --git a/client/components/rating/style.scss b/client/components/rating/style.scss
new file mode 100644
index 00000000000000..65132868c90a03
--- /dev/null
+++ b/client/components/rating/style.scss
@@ -0,0 +1,9 @@
+.rating {
+ width: 80px;
+ color: $blue-medium;
+ line-height: 1;
+
+ .noticon-rating-empty {
+ color: lighten( $gray, 20% );
+ }
+}
diff --git a/client/components/resizable-iframe/README.md b/client/components/resizable-iframe/README.md
new file mode 100644
index 00000000000000..b6423601d7a566
--- /dev/null
+++ b/client/components/resizable-iframe/README.md
@@ -0,0 +1,46 @@
+Resizable Iframe
+================
+
+Resizable Iframe is a React component for rendering an `