From 1b1238b341678ab44a62f26d396567ae59ddb053 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?"J.=20Rene=CC=81e=20Beach"?= <splendidnoise@gmail.com>
Date: Mon, 11 Feb 2013 23:55:27 -0500
Subject: [PATCH] Issue #1741498 by jessebeach: Add a mobile preview bar to
 Drupal core
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

commit 49054a990e964495d95da3f28b9dc4c33aa49953
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Mon Feb 11 23:49:34 2013 -0500

    JS is commented and ready to go.

    Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

commit 741cb4fa2f246c73a011b703bc7019045aca3dbf
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Mon Feb 11 20:39:35 2013 -0500

    changed all instances of 'previewer' to 'preview'

    Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

commit 052482592468faad0f73ddb491745b26aa3dddbf
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Mon Feb 11 20:28:30 2013 -0500

    Cleaned up theme css

    Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

commit 17f0fa98d2d5bbd5655ab20cd4ccb03a7d646c3b
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Mon Feb 11 20:17:08 2013 -0500

    Cleaning base.css

    Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

commit 2275918d5e3a95cc27b833dfa68682c81e51baa1
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Mon Feb 11 20:16:46 2013 -0500

    Fixed controls bug.

    Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

commit b23584ab3b844890a6e347a47f55c5a45573d7f0
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Mon Feb 11 00:34:23 2013 -0500

    patch 12

    Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>
---
 core/modules/layout/config/layout.devices.yml     |   26 +
 core/modules/layout/css/layout.preview.base.css   |  158 +++++
 core/modules/layout/css/layout.preview.theme.css  |  178 +++++
 core/modules/layout/images/close.png              |    7 +
 core/modules/layout/images/handle.png             |    4 +
 core/modules/layout/images/icon-layout-active.png |   14 +
 core/modules/layout/images/icon-layout.png        |   14 +
 core/modules/layout/images/ruler.png              |    3 +
 core/modules/layout/js/layout.preview.js          |  767 +++++++++++++++++++++
 core/modules/layout/layout.install                |   41 ++
 core/modules/layout/layout.module                 |  208 ++++++
 11 files changed, 1420 insertions(+)
 create mode 100644 core/modules/layout/config/layout.devices.yml
 create mode 100644 core/modules/layout/css/layout.preview.base.css
 create mode 100644 core/modules/layout/css/layout.preview.theme.css
 create mode 100644 core/modules/layout/images/close.png
 create mode 100644 core/modules/layout/images/handle.png
 create mode 100644 core/modules/layout/images/icon-layout-active.png
 create mode 100644 core/modules/layout/images/icon-layout.png
 create mode 100644 core/modules/layout/images/ruler.png
 create mode 100644 core/modules/layout/js/layout.preview.js
 create mode 100644 core/modules/layout/layout.install

diff --git a/core/modules/layout/config/layout.devices.yml b/core/modules/layout/config/layout.devices.yml
new file mode 100644
index 0000000..b39722c
--- /dev/null
+++ b/core/modules/layout/config/layout.devices.yml
@@ -0,0 +1,26 @@
+devices:
+  iphone:
+    label: iPhone
+    dimensions:
+      width: 320
+      height: 480
+  android:
+    label: Android
+    dimensions:
+      width: 540
+      height: 960
+  ipad:
+    label: iPad
+    dimensions:
+      width: 768
+      height: 1024
+  desktop:
+    label: desktop
+    dimensions:
+      width: 1366
+      height: 768
+  bigscreen:
+    label: big screen
+    dimensions:
+      width: 1600
+      height: 800
diff --git a/core/modules/layout/css/layout.preview.base.css b/core/modules/layout/css/layout.preview.base.css
new file mode 100644
index 0000000..0d91502
--- /dev/null
+++ b/core/modules/layout/css/layout.preview.base.css
@@ -0,0 +1,158 @@
+/**
+ * Constrain the window height to the client height when the preview is active.
+ */
+.layout-preview-active {
+  height: 100%;
+  overflow: hidden;
+}
+
+/**
+ * Toolbar tab.
+ */
+.layout-preview-toolbar-tab {
+  display: none;
+}
+/* At narrow screen widths, float the tab to the left so it falls in line with
+ * the rest of the toolbar tabs. */
+.js .toolbar .bar .layout-preview-toolbar-tab.tab {
+  display: block;
+  float: left; /* LTR */
+}
+/* At wide widths, float the tab to the right. */
+@media only screen and (min-width: 36em) {
+  .js .toolbar .bar .layout-preview-toolbar-tab.tab {
+    float: right; /* LTR */
+  }
+}
+.layout-preview-toolbar-tab .layout-preview-options {
+  display: none;
+}
+.layout-preview-toolbar-tab.open .layout-preview-options {
+  display: block;
+}
+
+/**
+ * Preview container.
+ *
+ * The container is kept offscreen after it is built and has been disabled.
+ */
+#layout-preview-container {
+  display: none;
+  height: 100%;
+  left: -200%; /* LTR */
+  position: absolute;
+  width: 100%;
+  z-index: 1050;
+}
+#layout-preview-container.active {
+  display: block;
+  left: 0; /* LTR */
+}
+#layout-preview-close {
+  position: absolute;
+  z-index: 75;
+}
+.layout-modal-background {
+  bottom: 0;
+  height: 100%;
+  left: 0;
+  position: fixed;
+  right: 0;
+  top: 3em;
+  width: 100%;
+  z-index: 1;
+}
+
+/**
+ * Developer ruler
+ */
+.layout-slider-background {
+  height: 2em;
+  position: absolute;
+  top: -1px;
+  width: 100%;
+  z-index: 2;
+}
+#frame-slider {
+  z-index: 50;
+}
+
+/**
+ * Breakpoints
+ */
+#layout-bp-view {
+  height: 2em;
+  position: absolute;
+  top: 2px;
+  width: 100%;
+  z-index: 5;
+}
+.layout-bp {
+  height: 2em;
+  position: absolute;
+  left: auto;
+  right: auto;
+  top: 0;
+  width: 100%;
+}
+.layout-bp div {
+  box-sizing: border-box;
+  height: 2em;
+  margin: 0 auto;
+  width: 100%;
+}
+.layout-bp-min {
+  position: relative;
+}
+.layout-bp .label {
+  box-sizing: border-box;
+  display: block;
+  left: -100%; /* LTR */
+  position: absolute;
+  top: 0;
+  white-space: nowrap;
+  width: 100%;
+}
+
+/**
+ * Preview iframe.
+ */
+#layout-preview-container iframe {
+  height: 100%;
+  position: relative;
+  width: 100%;
+  z-index: 100;
+}
+
+/**
+ * Developer controls.
+ */
+#layout-preview-controls {
+  display: none;
+  position: relative;
+  z-index: 25;
+}
+#layout-preview-controls.active {
+  display: block;
+}
+#layout-size-input {
+  position: absolute;
+  top: 1em;
+  z-index: 50;
+}
+#layout-size-input * {
+  box-sizing: border-box;
+  display: inline-block;
+  float: left; /* LTR */
+}
+
+/**
+ * Override Toolbar styling in the preview iframe.
+ */
+.layout-preview-frame #toolbar-administration {
+  display: none !important;
+}
+body.toolbar-tray-open.layout-preview-frame {
+  margin-left: 0 !important;
+  margin-right: 0 !important;
+}
diff --git a/core/modules/layout/css/layout.preview.theme.css b/core/modules/layout/css/layout.preview.theme.css
new file mode 100644
index 0000000..66072cb
--- /dev/null
+++ b/core/modules/layout/css/layout.preview.theme.css
@@ -0,0 +1,178 @@
+/**
+ * Toolbar tab.
+ */
+.layout-preview-toolbar-tab .layout-preview-options {
+  background-color: #0f0f0f;
+}
+/* Toolbar icon. */
+.toolbar .bar .icon.icon-layout {
+  margin-left: 0;
+  margin-right: 0;
+  padding-left: 0;
+  padding-right: 0;
+  text-indent: -9999px; /* LTR */
+  width: 5em;
+}
+.icon-layout:before {
+  background-image: url("../images/icon-layout.png");
+}
+.toolbar .bar .layout-preview-toolbar-tab .icon-layout:before {
+  left: 1em; /* LTR */
+}
+.layout-preview-toolbar-tab .open .icon-layout:before,
+.layout-preview-toolbar-tab .icon-layout.active:before {
+  background-image: url("../images/icon-layout-active.png");
+}
+@media only screen and (min-width: 16.5em) {
+  .toolbar .layout-preview-toolbar-tab.tab .icon-layout:before {
+    width: 20px;
+  }
+}
+/* Device preview options. */
+.layout-preview-toolbar-tab .layout-preview-options {
+  border-right: none; /* LTR */
+  box-shadow: 0 0 2em 0 rgba(0, 0, 0, 0.75);
+  position: absolute;
+}
+.layout-preview-toolbar-tab .layout-preview-options li {
+  background-color: white;
+  border-top: 1px solid #cfcfcf;
+}
+.layout-preview-toolbar-tab .trigger,
+.layout-preview-toolbar-tab .layout-preview-options a {
+  padding-bottom: 1em;
+  padding-top: 1em;
+}
+.toolbar .layout-preview-toolbar-tab.tab .layout-preview-options a {
+  color: black;
+}
+/* Toolbar tab triangle toggle. */
+.layout-preview-toolbar-tab .trigger:after {
+  border-bottom-color: transparent;
+  border-left-color: transparent;
+  border-right-color: transparent;
+  border-style: solid;
+  border-width: 0.4545em 0.4em 0;
+  color: #a0a0a0;
+  content: ' ';
+  display: block;
+  height: 0;
+  line-height: 0;
+  position: absolute;
+  right: 1.2em; /* LTR */
+  top: 50%;
+  margin-top: -0.1666em;
+  width: 0;
+  overflow: hidden;
+}
+.layout-preview-toolbar-tab.open .trigger:after {
+  border-bottom: 0.4545em solid;
+  border-top-color: transparent;
+  top: 1.25em;
+}
+
+/**
+ * Preview container.
+ */
+#layout-preview-container {
+  box-shadow: 0 0 10px 0 black;
+}
+#layout-preview-close {
+  background-attachment: scroll;
+  background-color: #a0a0a0;
+  background-image: url("../images/close.png");
+  background-image: url("../images/close.png"), -webkit-linear-gradient(transparent, #787878 150%);
+  background-image: url("../images/close.png"), linear-gradient(transparent, #787878 150%);
+  background-position: center center;
+  background-repeat: no-repeat;
+  border: none;
+  border-radius: 3px;
+  cursor: pointer;
+  font-size: 1em;
+  height: 2.333em;
+  left: 0; /* LTR */
+  margin-left: 10px;
+  margin-top: 9px;
+  text-indent: -9999em; /* LTR */
+  width: 2.333em;
+}
+#layout-preview-close:hover {
+  background-image: url("../images/close.png");
+}
+.layout-modal-background {
+  background-color: black;
+  background-color: rgba(0,0,0,0.92);
+}
+
+/**
+ * Developer ruler.
+ */
+.layout-slider-background {
+  background-color: white;
+  border: 1px solid white;
+}
+#frame-slider {
+  background-attachment: scroll;
+  background-color: transparent;
+  background-image: url("../images/ruler.png");
+  background-position: center bottom;
+  background-repeat: repeat-x;
+  border-color: #ccc;
+  border-style: solid;
+  border-radius: 0;
+  border-width: 1px 0 0;
+  height: 2em;
+}
+#frame-slider .ui-slider-range {
+  background: none;
+  background-color: rgba(90,90,90,0.2);
+}
+#frame-slider.ui-slider-horizontal .ui-slider-handle {
+  background-attachment: scroll;
+  background-color: transparent;
+  background-image: url("../images/handle.png");
+  background-position: center 1px;
+  background-repeat: no-repeat;
+  border: 0;
+  border-left: 1px solid #b0b0b0; /* LTR */
+  border-radius: 0;
+  height: 100%;
+  margin-left: 0; /* LTR */
+  top: 0;
+}
+#frame-slider.ui-slider-horizontal .ui-slider-handle + .ui-slider-handle {
+  border-left: 0 none; /* LTR */
+  border-right: 1px solid #b0b0b0; /* LTR */
+  margin-left: -1.2em; /* LTR */
+}
+
+/**
+ * Breakpoints.
+ */
+#layout-bp-view {
+  background-color: rgba(150,150,150,0.6667);
+}
+.layout-bp-min {
+  background-color: rgba(255,255,255,0.75);
+  border-color: white;
+  border-style: solid;
+  border-width: 0 1px;
+}
+.layout-bp .label {
+  font-size: 0.7em;
+  height: 1em;
+  line-height: 1;
+  padding-right: 6px; /* LTR */
+  text-align: right;
+}
+
+/**
+ * Developer controls.
+ */
+#layout-size-input {
+  font-size: 0.75em;
+}
+#layout-size-input input {
+  padding: 0;
+  width: 10em;
+}
diff --git a/core/modules/layout/images/close.png b/core/modules/layout/images/close.png
new file mode 100644
index 0000000..2203cbb
--- /dev/null
+++ b/core/modules/layout/images/close.png
@@ -0,0 +1,7 @@
+PNG
+
+   IHDR         a   tEXtSoftware Adobe ImageReadyqe<  IDATx|R=LZa}!T%MvpWt $kͩZ;Cn&j*1&jæE14`C=!{߹~]DLDwVWWMcccJrJcLA.Oe5uIkJ>hXq7F"Z}^
+N~vR䜘l6|nZݲ,[4M#x</douo9L#$4Ęө{A\䨘 011!5VVVLe*[KQkvvvF!XAXg șf+(8
+&=T]8ۡPxcc-;V^AXݧƶp 2Pj3ܦ/f@W'DcL9rEChԎ(LODrQ+
+KKKee%8I4AV_$WՑɟo?\Q;TEyonn
+ Z8Vl455@pPHaǿBLlB{{{9[j(цN:T@F0Nq}}mrupߨ5` Ye     IENDB`
\ No newline at end of file
diff --git a/core/modules/layout/images/handle.png b/core/modules/layout/images/handle.png
new file mode 100644
index 0000000..656b884
--- /dev/null
+++ b/core/modules/layout/images/handle.png
@@ -0,0 +1,4 @@
+PNG
+
+   IHDR         &(ۙ   tEXtSoftware Adobe ImageReadyqe<  IDATxڤ9Aa?@B !$8	82bZ$q_fke[\AS~˛/@z"hEI'ėXFov.1"2(=ܻ/9LX_USB.uXsA{VX`˅xc0Еdl6q8D"i< k^)8뽿%&x5#0#LN'%r9jQy8G;>a.K1R9L:|Bp'vz"F-|Hv4ᕛTnQXr;C9VT.-# GܨT*r@Q^Fd9sR)lZV(ك	nZ4>H=n^KaPW)UFWCRD?wFȃS:(pjRkUV.JCb^+5Ul60ը&()n*{T/iZLcͩGĮ:ő
+e>:T-{*zVmи=l|!&JIJcTŭ^lTnRT. >]C`ci(et 2(M^L͕ɒ{R@ҤAcӎwG1:$? *M=b    IENDB`
\ No newline at end of file
diff --git a/core/modules/layout/images/icon-layout-active.png b/core/modules/layout/images/icon-layout-active.png
new file mode 100644
index 0000000..ff89708
--- /dev/null
+++ b/core/modules/layout/images/icon-layout-active.png
@@ -0,0 +1,14 @@
+PNG
+
+   IHDR         $   	pHYs    ~  
+OiCCPPhotoshop ICC profile  xڝSgTS=BKKoR RB&*!	J!QEEȠQ,
+!{kּ>H3Q5B.@
+$p d!s# ~<<+" x M0B\t8K @zB @F&S  `cb P- `' { [!  eD h; VE X0 fK9 - 0IWfH    0Q) { `##x  FW<+*  x<$9E[-qWW.(I+6aa@.y24  x6_-"bbϫp@  t~,/;m%h^uf@ Wp~<<EJB[aW}g_Wl~<$2]GLϒ	bG"IbX*QqD2"B)%d,>5 j>{-]cK'Xt  o(hw?G% fIq  ^D$.Tʳ?  D*A,`6B$BB
+dr`)B(Ͱ*`/@4Qhp.U=pa(	Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F; 2G1Q=C7Fdt1r=6Ыhڏ>C03l0.B8,	c˱"VcϱwE	6wB aAHXLXNH $4	7	Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![
+b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGwǈg(gwLӋT071oUX**|
+J&*/TުUUT^S}FU3S	ԖUPSSg;goT?~YYLOCQ_ cx,!ku5&|v*=9C3J3WRf?qtN	(~))4L1e\kXHQG6EYAJ'\'GgSSݧ
+M=:.kDwn^Loy}/TmGX$<5qo</QC]@Caaᄑ<FFi\$mmƣ&&!&KMMRM);L;L֙͢5=12כ߷`ZxZ,eIZYnZ9YXUZ]F%ֻNNgðɶۮm}agbgŮ}}=Z~sr:V:ޚΜ?}/gX3)iSGggs󈋉K.>.ȽJtq]zۯ6iܟ4)Y3sCQ?0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz %gA[z|!?:eAAA!h쐭!ΑiP~aa~'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl{/]py.,:@LN8A*%w%
+yg"/6шC\*NH*Mz쑼5y$3,幄'LLݛ:v m2=:1qB!Mggfvˬen/kY-
+BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9<qy
++V<*mOW~&zMk^ʂkU
+}]OX/Yߵa>(xoʿܔĹdff-[nڴVE/(ۻC<e;?TTTT6ݵan{4[>ɾUUMfeI?m]Nmq#׹=TR+Gw-6U#pDy	:v{vg/jBFS[b[O>zG4<YyJTiӓgό}~.`ۢ{cjotE;;\tWW:_mt<Oǻ\kz{f7y՞9=ݽzo~r'˻w'O_@AC݇?[jwGCˆ8>99?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3-    cHRM  z%        u0  `  :  o_F   kIDATx쓱0$1(Rd) 4 7)'Y./-$1TܝÓR7 wohkSM;L!FgZk	'x|<!<> S댞|    IENDB`
\ No newline at end of file
diff --git a/core/modules/layout/images/icon-layout.png b/core/modules/layout/images/icon-layout.png
new file mode 100644
index 0000000..d1d6aef
--- /dev/null
+++ b/core/modules/layout/images/icon-layout.png
@@ -0,0 +1,14 @@
+PNG
+
+   IHDR         $   	pHYs    ~  
+OiCCPPhotoshop ICC profile  xڝSgTS=BKKoR RB&*!	J!QEEȠQ,
+!{kּ>H3Q5B.@
+$p d!s# ~<<+" x M0B\t8K @zB @F&S  `cb P- `' { [!  eD h; VE X0 fK9 - 0IWfH    0Q) { `##x  FW<+*  x<$9E[-qWW.(I+6aa@.y24  x6_-"bbϫp@  t~,/;m%h^uf@ Wp~<<EJB[aW}g_Wl~<$2]GLϒ	bG"IbX*QqD2"B)%d,>5 j>{-]cK'Xt  o(hw?G% fIq  ^D$.Tʳ?  D*A,`6B$BB
+dr`)B(Ͱ*`/@4Qhp.U=pa(	Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F; 2G1Q=C7Fdt1r=6Ыhڏ>C03l0.B8,	c˱"VcϱwE	6wB aAHXLXNH $4	7	Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![
+b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGwǈg(gwLӋT071oUX**|
+J&*/TުUUT^S}FU3S	ԖUPSSg;goT?~YYLOCQ_ cx,!ku5&|v*=9C3J3WRf?qtN	(~))4L1e\kXHQG6EYAJ'\'GgSSݧ
+M=:.kDwn^Loy}/TmGX$<5qo</QC]@Caaᄑ<FFi\$mmƣ&&!&KMMRM);L;L֙͢5=12כ߷`ZxZ,eIZYnZ9YXUZ]F%ֻNNgðɶۮm}agbgŮ}}=Z~sr:V:ޚΜ?}/gX3)iSGggs󈋉K.>.ȽJtq]zۯ6iܟ4)Y3sCQ?0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz %gA[z|!?:eAAA!h쐭!ΑiP~aa~'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl{/]py.,:@LN8A*%w%
+yg"/6шC\*NH*Mz쑼5y$3,幄'LLݛ:v m2=:1qB!Mggfvˬen/kY-
+BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9<qy
++V<*mOW~&zMk^ʂkU
+}]OX/Yߵa>(xoʿܔĹdff-[nڴVE/(ۻC<e;?TTTT6ݵan{4[>ɾUUMfeI?m]Nmq#׹=TR+Gw-6U#pDy	:v{vg/jBFS[b[O>zG4<YyJTiӓgό}~.`ۢ{cjotE;;\tWW:_mt<Oǻ\kz{f7y՞9=ݽzo~r'˻w'O_@AC݇?[jwGCˆ8>99?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3-    cHRM  z%        u0  `  :  o_F   IDATx >RK*pB%<h  v1Q z%fąZX% K@J)|K)xI#l=,uN'oK=!O6ioDI`Oe 43    IENDB`
\ No newline at end of file
diff --git a/core/modules/layout/images/ruler.png b/core/modules/layout/images/ruler.png
new file mode 100644
index 0000000..a600619
--- /dev/null
+++ b/core/modules/layout/images/ruler.png
@@ -0,0 +1,3 @@
+PNG
+
+   IHDR   Z      )#yM   tEXtSoftware Adobe ImageReadyqe<   CIDATxױ   AuXWXTB®NfF _thC;"]  V    IENDB`
\ No newline at end of file
diff --git a/core/modules/layout/js/layout.preview.js b/core/modules/layout/js/layout.preview.js
new file mode 100644
index 0000000..a560239
--- /dev/null
+++ b/core/modules/layout/js/layout.preview.js
@@ -0,0 +1,767 @@
+/**
+ * @file layout,preview.js
+ *
+ * Builds a component that previews the a page in various device dimensions.
+ */
+(function (Drupal, $) {
+
+  Drupal.layout = Drupal.layout || {};
+
+  var $toolbarTab = $();
+  var size; // The width of the preview container.
+  var handles = []; // The values of the jQuery UI Slider handles.
+  var leftOffset; // The left value of the iframe containing the previewed page.
+  var $frame; // The iframe that contains the previewed page.
+  var iframeDocument; // The document of the iframe that contains the preview.
+  var $slider; // The jQuery UI Slider widget that adjusts the iframed preview.
+  var $container; // The container of the page preview component.
+  var $controls; // Developer controls container.
+  var $sizeInput; // The input element that display the width of the preview.
+  var breakpoints = {}; // A list of breakpoints, keyed by configuration string.
+  var $breakpointView; // The container of the breakpoint views.
+  var edgeTolerance = 60;
+
+  Drupal.behaviors.layout = {
+    attach: function (context, settings) {
+      var $body = $(window.top.document.body).once('layout-preview');
+
+      if ($body.length) {
+        // Append the selector to the preview container.
+        $toolbarTab = $('.layout-preview-toolbar-tab')
+          .on('click.layout', '#layout-preview', toggleConfigurationOptions)
+          .on('mouseleave.layout', '.layout-preview-options', {open: false}, toggleConfigurationOptions)
+          .on('click.layout', '.layout-preview-options .layout-preview-device', {open: false}, toggleConfigurationOptions)
+          .on('click.layout', '.layout-preview-device', loadDevicePreview)
+          .on('click.layout', '#layout-preview-ruler-trigger', toggleControls);
+        // Register a handler on window resize to reposition the tab dropdown.
+        $(window.top)
+          .on('resize.layout.tab', handleWindowToolbarResize)
+          .trigger('resize.layout.tab');
+      }
+      // Remove administrative elements in the document inside the iframe.
+      if (window.top !== window.self) {
+        var $frameBody = $(window.self.document.body).once('layout-preview');
+        if ($frameBody.length > 0) {
+          $frameBody.get(0).className += ' layout-preview-frame';
+        }
+      }
+    }
+  };
+
+  /**
+   * Toggles the list of devices available to preview from the toolbar tab.
+   *
+   * @param {Object} event
+   *   - jQuery Event object.
+   */
+  var toggleConfigurationOptions = function (event) {
+    event.preventDefault();
+    var open = (event.data && typeof event.data.open === 'boolean') ? event.data.open : undefined;
+    $(event.delegateTarget)
+      // Set an open class on the tab wrapper.
+      .toggleClass('open', open)
+      .find('.layout-preview-options')
+      // The list of options will most likely render outside the window. Correct
+      // this.
+      .drupalLayout('correctEdgeCollisions');
+  };
+
+  /**
+   * Toggles the developer controls above the preview iframe.
+   *
+   * @param {Object} event
+   *   - jQuery Event object.
+   */
+  var toggleControls = function (event) {
+    event.preventDefault();
+    var $this = $(this);
+    if ($controls) {
+      $controls.toggleClass('active');
+      if ($controls.hasClass('active')) {
+        $this.text(Drupal.t('Hide developer ruler'));
+      }
+      else {
+        $this.text(Drupal.t('Show developer ruler'));
+      }
+    }
+  }
+
+  /**
+   * Toggles the layout preview component on or off.
+   *
+   * When first toggled on, the layout preview component is built. All
+   * subsequent toggles hide or show the built component.
+   *
+   * @param {Object} event
+   *   - jQuery Event object.
+   *
+   * @param {Boolean} activate
+   *   - A boolean that forces the preview to show (true) or to hide (false).
+   */
+  var toggleLayoutPreview = function (event, activate) {
+    event.preventDefault();
+    // Build the preview if it doesn't exist.
+    if (!$container) {
+      // Initialize the handle positions.
+      handles = (handles.length) ? handles : [0, document.documentElement.clientWidth];
+      buildpreview();
+      // Size is the width of the iframe.
+      updateDimensions({width: (size || window.top.document.documentElement.clientWidth)});
+    }
+    $container
+      .toggleClass('active', activate)
+    $('body')
+      .toggleClass('layout-preview-active', activate);
+  };
+
+  /**
+   * Assembles a layout preview.
+   */
+  var buildpreview = function () {
+    $(window.top.document.body).once('layout-preview-container', function (index, element) {
+      $container = $(Drupal.theme('layoutContainer'));
+      // Build the preview controls.
+      $controls = $(Drupal.theme('layoutControls'));
+      // Theme and render a jQuery UI slider.
+      $slider = $(Drupal.theme('layoutSlider'))
+        .filter('#frame-slider')
+        .slider({
+          'animate': 'fast',
+          'range': true,
+          'max': document.documentElement.clientWidth,
+          'min': 0,
+          'values': handles,
+          'slide': handleSlide
+        })
+        .appendTo($controls);
+
+      // Load the breakpoints for the current theme.
+      if ('breakpoints' in Drupal.settings.layout.routes) {
+        $.ajax(Drupal.settings.layout.routes.breakpoints)
+          .success(breakpointsCallback);
+      }
+      // Displays and allows a user to edit the width of the preview.
+      $sizeInput = $(Drupal.theme('layoutSizeInput'))
+        .appendTo($controls);
+
+      // Attach the controls.
+      $container.append($controls);
+
+      // Add a close button.
+      $container
+        .append(Drupal.theme('layoutClose'));
+
+      // Attach the iframe that will hold the preview.
+      $frame = $(Drupal.theme('layoutFrame'))
+        .css({
+          'width': size
+        })
+        .appendTo($container);
+
+      // Append the container to the window.
+      $container.appendTo(window.top.document.body);
+      // Displace the top of the container.
+      $container
+        .css({
+          top: getDisplacement('top'),
+        })
+        .attr('data-offset-top', getDisplacement('top'));
+
+      // The contentDocument property is not supported in IE until IE8.
+      iframeDocument = $frame[0].contentDocument || $frame[0].contentWindow.document;
+
+      $container
+        .on('click.layout', '#layout-preview-close', {activate: false}, toggleLayoutPreview)
+        .on('keypress.layout', '#layout-size-input', {pattern: /^[0-9\.]$/, callback: handleSizeInputChange}, keyManager)
+        .on('sizeUpdate.layout', refreshPreviewSizing);
+
+      // Trigger a resize to kick off some initial placements.
+      $(window.top)
+        .on('resize.layout', handleWindowResize)
+        .trigger('resize.layout');
+
+      // Load the current page URI into the preview iframe.
+      // @todo, are there any security implications to loading a page like this?
+      iframeDocument.location.href = Drupal.settings.basePath + Drupal.settings.currentPath;
+    });
+  };
+
+  /**
+   * Responds to a jQuery UI Slider slide event.
+   *
+   * @param {Object} event
+   *   - jQuery Event object.
+   *
+   * @param {Object} ui
+   *   - jQuery Slider widget state information resulting from a slide event.
+   */
+  var handleSlide = function (event, ui) {
+    // Layout will control the placement of the handles.
+    event.preventDefault();
+    var delta = 0;
+    var vals = [];
+    var handle, split, width;
+    // Get the delta of the original value of the handles and the new value.
+    for (var i = ui.values.length - 1; i >= 0; i--) {
+      // Get the original handle value.
+      var handle = handles[i];
+      // The new handle value.
+      var value = ui.values[i];
+      // If the original and the new values are not equal, adjust the handles.
+      if (handle !== value) {
+        var max = $slider.slider('option', 'max');
+        var otherHandle = (i === 0) ? 1 : 0;
+        vals[i] = value;
+        // The value of the other handle is the inverse of the percentage of the
+        // active handle.
+        vals[otherHandle] = max * (1 - (value / max));
+        // Get the updated width of the viewport.
+        width = Math.abs(vals[i] - vals[otherHandle]);
+        // Update the dimension variables in the closure.
+        updateDimensions({width: width});
+        // Only one handle moves at a time, so if a handle-move was processed,
+        // then break;
+        break;
+      }
+    }
+  };
+
+  /**
+   * Responds to keypress events from the frame size input.
+   *
+   * @param {Object} event
+   *   - jQuery Event object.
+   */
+  var handleSizeInputChange = function (event) {
+    var newSize;
+    if (event.isDefaultPrevented()) {
+      return false;
+    }
+    if (event.key) {
+      newSize = $sizeInput.find('input').val();
+      // If the key is a '.' and the value already contains one then
+      // prevent default.
+      if (event.key == '.' && newSize.indexOf('.') > -1) {
+        event.preventDefault();
+      }
+    }
+    // Process the press of the enter key.
+    if (event.keyCode === 13) {
+      var newSize = parseFloat($sizeInput.find('input').val());
+      if (newSize > 0) {
+        // Update the dimensions variables in the closure.
+        updateDimensions({width: newSize}, 250);
+      }
+    }
+  };
+
+  /**
+   * Updates the dimension variables of the preview components.
+   *
+   * @param {Object} dimensions
+   *   - An object with the following properties:
+   *     - {Number} width: The width the preview should be set to.
+   *     - {Number} height (optional): The height the preview should be set to.
+   *     Currently this is not used.
+   *
+   * @param {Number} speed
+   *   - A number representing time in milliseconds or a jQuery speed keyword.
+   *   Determines the speed at which animations between changes dimension values
+   *   should take place. Defaults to zero.
+   */
+  var updateDimensions = function (dimensions, speed) {
+    var width = dimensions.width || NaN;
+    var height = dimensions.height || NaN;
+    // Calculate the handle placements.
+    var max = $slider.slider('option', 'max');
+    var values = [];
+    var gutterPercent = (1 - (width / max)) / 2;
+    values[0] = gutterPercent * max;
+    // The gutters must be at least the width of the edgeTolerance
+    values[0] = (values[0] < edgeTolerance) ? edgeTolerance : values[0];
+    // The frame width must fit within the different of the gutters and the page
+    // width.
+    width = (max - (values[0] * 2) < width) ? max - (values[0] * 2) : width;
+    values[1] = values[0] + width;
+    // Store the new values of the handles.
+    handles = [values[0], values[1]];
+    // Set the new size of the frame.
+    size = Math.round((max * ((values[1] - values[0]) / max)) * 10) / 10;
+    // Set the left offset of the frame.
+    leftOffset = max * (values[0] / max);
+    // Trigger a dimension change.
+    $container.trigger('sizeUpdate.layout', speed);
+  };
+
+  /**
+   * Handles refreshing the layout toolbar tab positioning.
+   *
+   * @param {Object} event
+   *   - jQuery Event object.
+   */
+  var handleWindowToolbarResize = function (event) {
+    var options = $toolbarTab
+      .find('.layout-preview-options')
+      // Move the list back onto the screen.
+      .drupalLayout('correctEdgeCollisions')
+      .find('.layout-preview-device')
+      // Hide layout options that are wider than the current screen
+      .drupalLayout('prunePreviewChoices', edgeTolerance)
+      // The lis will be toggled. Assign them to options.
+      .parent('li');
+
+    $toolbarTab.toggle(options.not('.element-hidden').length > 0);
+  };
+
+  /**
+   * Responds to window resize events.
+   *
+   * @param {Object} event
+   *   - jQuery Event object.
+   */
+  var handleWindowResize = function (event) {
+    var doc = this.document.documentElement;
+    var docWidth = doc.clientWidth;
+    var framePercent = size / docWidth;
+    var gutterPercent = (1 - framePercent) / 2;
+    // Adjust the parameters of the slider.
+    $slider
+      // The new max of the slider is the width of the document.
+      .slider('option', 'max', docWidth)
+      // Update the position values of the slider handles.
+      .slider('values', [(gutterPercent * docWidth), ((gutterPercent + framePercent) * docWidth)]);
+    // If the window has been reduced below the width of the frame, reduce the
+    // width of the frame.
+    updateDimensions({width: ((docWidth < size) ? docWidth : size)});
+    // Adjust the parameters of the frame.
+    $frame.css({
+      'left': gutterPercent * docWidth,
+      'width': size
+    });
+    // Adjust the position of the size input.
+    var inputPercent = $sizeInput.width() / docWidth;
+    gutterPercent = (1 - inputPercent) / 2;
+    $container.find('#layout-size-input').css({
+      'left': gutterPercent * docWidth
+    });
+    // Update the size input value.
+    $sizeInput.find('input').val(size);
+  };
+
+  /**
+   * A jQuery plugin that contains element manipulation utilities.
+   *
+   * @return {Function}
+   *   - The method to invoke this plugin.
+   */
+  $.fn.drupalLayout = (function () {
+
+    /**
+     * Corrects element window edge collisions.
+     *
+     * Elements are moved back into the window if part of the element is
+     * rendered outside the visible window.
+     */
+    function correct () {
+      // Clear any previous corrections.
+      clear.apply(this);
+      // Go through each element and correct edge collisions.
+      return this.each(function (index, element) {
+        var $this = $(this);
+        var width = $this.width();
+        var height = $this.height();
+        var clientW = document.documentElement.clientWidth;
+        var clientH = document.documentElement.clientHeight;
+        var collisions = {
+          'top': null,
+          'right': null,
+          'bottom': null,
+          'left': null
+        };
+        // Determine if the element is too big for the document. Resize to fit.
+        if (width > clientW) {
+          $this.width(clientW);
+          // If the element is too wide, it will collide on both left and right.
+          collisions.left = true;
+          collisions.right = true;
+        }
+        if (height > clientH) {
+          $this.height(clientH);
+          // If the element is too high, it will collide on both top and bottom.
+          collisions.top = true;
+          collisions.bottom = true;
+        }
+        // Check each edge for a collision.
+        if (!collisions.top && $this.offset().top < 0) {
+          collisions.top = true;
+        }
+        if (!collisions.right && (($this.offset().left + width) > clientW)) {
+          collisions.right = true;
+        }
+        if (!collisions.bottom && (($this.offset().top + height) > clientH)) {
+          collisions.bottom = true;
+        }
+        if (!collisions.left && $this.offset().left < 0) {
+          collisions.left = true;
+        }
+        // Set the offset to zero for any collision on an edge.
+        for (var edge in collisions) {
+          if (collisions.hasOwnProperty(edge)) {
+            if (collisions[edge]) {
+              $this.css(edge, 0);
+            }
+          }
+        }
+      });
+    }
+
+    /**
+     * Clears any previous edge correction styling.
+     */
+    function clear () {
+      var edges = ['top', 'right', 'bottom', 'left'];
+      return this.each(function (index, element) {
+        for (var i = 0; i < edges.length; i++) {
+          this.style[edges[i]] = "";
+        }
+      });
+    }
+
+    /**
+     * Hides device prevview options that are too wide for the current window.
+     *
+     * @param {Number} tolerance
+     *   - The distance from the edge of the window that a device cannot exceed
+     *   or it will be pruned from the list.
+     */
+    function prune (tolerance) {
+      var docWidth = document.documentElement.clientWidth;
+      tolerance = (typeof tolerance === 'number' && tolerance > 0) ? tolerance : 0;
+      return this.each(function () {
+        var $this = $(this);
+        var width = parseInt($this.data('layout-width'));
+        var fits = ((width + (tolerance * 2)) < docWidth);
+        $this.parent('li').toggleClass('element-hidden', !fits);
+      });
+    }
+
+    /**
+     * Methods that this plugin exposes.
+     */
+    var methods = {
+      'correctEdgeCollisions': correct,
+      'prunePreviewChoices': prune
+    };
+
+    return function (method) {
+      if (methods[method]) {
+        return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
+      }
+      else {
+        $.error(Drupal.t('Method @method does not exist in this plugin.', {'@method': method}));
+      }
+    };
+
+  }());
+
+  /**
+   * Renders breakpoint configuration to an HTML view.
+   *
+   * @param {Object} data
+   *   - Breakpoint configuration data. The keys of the object correspond to the
+   *   keys of theme-configured breakpoints. The value of each key is a string
+   *   that represents a media query.
+   *
+   * @param {String} textStatus
+   *   - The status of the AJAX request.
+   *
+   * @param {Object} jqXHR
+   *   - A jQuery XMLHttpRequest object.
+   */
+  var breakpointsCallback = function (data, textStatus, jqXHR) {
+    $breakpointView = $(Drupal.theme('layoutBreakpointView'));
+    var $item, options;
+    for (var bp in data) {
+      if (data.hasOwnProperty(bp)) {
+        breakpoints[bp] = parseMediaQuery(data[bp]);
+        // Append a representation of each breakpoint to the frame slider.
+        if (breakpoints[bp].hasquery) {
+          options = breakpoints[bp];
+          options.id = 'layout-bp-' + bp.replace(/\./g, '-');
+          options.label = bp;
+          $item = $(Drupal.theme('layoutBreakpointItemView', options));
+          $breakpointView.prepend($item);
+        }
+      }
+    }
+    if ($breakpointView.children().length) {
+      // @todo, These breakpoints should be sorted by min width eventually.
+      // For now we assume they are listed from small to largest.
+      $container.find('#layout-preview-controls').append($breakpointView);
+    }
+  };
+
+  /**
+   * Resizes the preview iframe to the configured dimensions of a device.
+   *
+   * @param {Object} event
+   *   - A jQuery event object.
+   */
+  function loadDevicePreview (event) {
+    event.preventDefault();
+    toggleLayoutPreview(event, true);
+    var $link = $(event.target);
+    var width = $link.data('layout-width')
+    updateDimensions({width: width}, 250);
+  }
+
+  /**
+   * Redraws the layout preview component based on the stored dimensions.
+   *
+   * @param {Object} event
+   *   - A jQuery event object.
+   *
+   * @param {Number/String} speed
+   *   - A number representing time in milliseconds or a jQuery speed keyword.
+   *   Determines the speed at which animations between changes dimension values
+   *   should take place. Defaults to zero.
+   */
+  var refreshPreviewSizing = function (event, speed) {
+    speed = speed || 0;
+    // Adjust the frame.
+    $slider.slider('option', 'values', [handles[0], handles[1]]);
+    $frame.animate({
+      left: leftOffset,
+      width: size
+    }, speed);
+    // Update the size input value.
+    $sizeInput.find('input').val(size);
+    // Reposition the close button.
+    $('#layout-preview-close')
+      .css('left', (leftOffset + size));
+  };
+
+  /**
+   * Get the total displacement of given region.
+   *
+   * @param String region
+   *   Region name. Either "top" or "bottom".
+   *
+   * @return
+   *   The total displacement of given region in pixels.
+   */
+  var getDisplacement = function (region) {
+    var displacement = 0;
+    var lastDisplaced = $('[data-offset-' + region + ']');
+    if (lastDisplaced.length) {
+      displacement = parseInt(lastDisplaced.attr('data-offset-' + region));
+    }
+    return displacement;
+  };
+
+  /**
+   * Registers theme templates with Drupal.theme().
+   */
+  $.extend(Drupal.theme, {
+    /**
+     * Returns the preview container element.
+     */
+    layoutContainer: function () {
+      return '<div id="layout-preview-container"><div class="layout-modal-background"></div></div>';
+    },
+
+    /**
+     * Returns the close button for the preview container.
+     */
+    layoutClose: function () {
+      return '<button id="layout-preview-close" role="button" aria-pressed="false">' + Drupal.t('Close') + '</button>';
+    },
+
+    /**
+     * Returns an overlay iframe element.
+     */
+    layoutFrame: function (url) {
+      return '<iframe id="layout-preview" frameborder="0" scrolling="auto" allowtransparency="true"></iframe>';
+    },
+
+    /**
+     * Returns the HTML for the jQuery UI Slider attachment.
+     */
+    layoutSlider: function () {
+      return '<div class="layout-slider-background"></div><div id="frame-slider"></div>';
+    },
+
+    /**
+     * Returns a container for the preview developer controls.
+     */
+    layoutControls: function () {
+      return '<div id="layout-preview-controls" />';
+    },
+
+    /**
+     * Returns the input element for changing the preview width.
+     */
+    layoutSizeInput: function () {
+      return '<div id="layout-size-input"><span class="label">' + Drupal.t('Width') + '</span><input name="layout_width" type="text" /><span class="unit">px</span></div>';
+    },
+
+    /**
+     * Returns the wrapper for breakpoint item views.
+     */
+    layoutBreakpointView: function () {
+      return '<div id="layout-bp-view"></div>'
+    },
+
+    /**
+     * Returns individual breakpoint configuration views.
+     */
+    layoutBreakpointItemView: function (options) {
+      var markup = '';
+      options = options || {};
+
+      markup += '<div id="' + (options.id || '')  + '" class="layout-bp">';
+      markup += '<div class="layout-bp-max" style="width:' + ((options.maxw) ? options.maxw : ((options.minw) ? options.minw : 'auto')) + ';">';
+      markup += '<div class="layout-bp-min" style="width:' + ((options.minw) ? options.minw : 'auto') + ';"><span class="label">' + options.label  + '</span></div>';
+      markup += '</div>';
+      markup += '</div>';
+
+      return markup;
+    }
+  });
+
+  /**
+   * Handles key input.
+   *
+   * Fires a callback function with either return the key that was pressed in
+   * the event.key property, or, if the key is a control key or does not match
+   * a supplied pattern, then null as the event.key value.
+   *
+   * @param {Regex} event.data.pattern
+   *   - A regular expression that filters allowed key input. Only keys matching
+   *   the expression will be returned. All other keys return null.
+   *
+   * @param {Function} event.data.callback
+   *   - A callback function to be invoked after a key is processed. Any
+   *   variadic parameters supplied to keyManager are passed through as well.
+   *
+   * @param {Array} event.data.controls
+   *   - An Array of char codes that should be ignored as control keys.
+   */
+  var keyManager = function (event) {
+    event.data = event.data || {};
+    var pattern = event.data.pattern || undefined;
+    var callback = event.data.callback || undefined;
+    var controls = event.data.controls || [
+      8, // Delete.
+      13, // Enter.
+      37, // Left.
+      38, // Up.
+      39, // Right.
+      40 // Down.
+    ];
+    // Get the key from its keyCode.
+    var key = mapKeyToChar(event.shiftKey, event.keyCode);
+    // Prevent default if:
+    // (1) mapKeyToChar did not produce a key and the key is not a control key.
+    // (2) mapKeyToChar produced a key and the pattern does not match.
+    if ((!key && $.inArray(event.keyCode, controls) === -1) ||
+        (key && (pattern && typeof pattern === 'object' && 'exec' in pattern && !pattern.exec(key)))) {
+      event.preventDefault();
+    }
+    // Provide the key as the mapped character in the event object.
+    event.key = (key) ? key : null;
+    // The callback function should check for isDefaultPrevented() to know if
+    // the keyManager validated this key.
+    if (callback && typeof callback === 'function') {
+      callback.apply(this, arguments);
+    }
+  }
+
+  /**
+   * Maps keyCode to Strings, taking the shift key state into account.
+   *
+   * @param {Boolean} isShifted
+   *   - A Boolean representing if the shift key was pressed (true) or not
+   *   (false)
+   *
+   * @param {Number} keyCode
+   *   - The numeric code of the key that was pressed.
+   *
+   * @return {String}
+   *   - A single character corresponding to the keyCode or null if no
+   *   correspondence is found.
+   */
+  var mapKeyToChar = function (isShifted, keyCode) {
+    if (keyCode === 27
+      || keyCode === 8
+      || keyCode === 9
+      || keyCode === 20
+      || keyCode === 16
+      || keyCode === 17
+      || keyCode === 91
+      || keyCode === 13
+      || keyCode === 92
+      || keyCode === 18) {
+      return false;
+    }
+    if (typeof isShifted != "boolean" || typeof keyCode != "number") {
+      return false;
+    }
+    var charMap = [];
+    charMap[192] = "~";
+    charMap[49] = "!";
+    charMap[50] = "@";
+    charMap[51] = "#";
+    charMap[52] = "$";
+    charMap[53] = "%";
+    charMap[54] = "^";
+    charMap[55] = "&";
+    charMap[56] = "*";
+    charMap[57] = "(";
+    charMap[48] = ")";
+    charMap[109] = "_";
+    charMap[107] = "+";
+    charMap[219] = "{";
+    charMap[221] = "}";
+    charMap[220] = "|";
+    charMap[59] = ":";
+    charMap[222] = "\"";
+    charMap[188] = "<";
+    charMap[190] = ">";
+    charMap[191] = "?";
+    charMap[32] = " ";
+    var character = "";
+    if (isShifted) {
+      if (keyCode >= 65 && keyCode <= 90) {
+        character = String.fromCharCode(keyCode);
+      }
+      else {
+        character = charMap[keyCode];
+      }
+    }
+    else {
+      if (keyCode >= 65 && keyCode <= 90) {
+        character = String.fromCharCode(keyCode).toLowerCase();
+      }
+      else {
+        character = String.fromCharCode(keyCode);
+      }
+    }
+    return character;
+  }
+
+  /**
+   * Parses a String representing a media query into usable values.
+   *
+   * @param {String} mq
+   *   - A String representing a media query e.g. 'all and (min-width: 800px)'
+   */
+  var parseMediaQuery = function (mq) {
+    return {
+      media : mq.split("(")[0].match(/(only\s+)?([a-zA-Z]+)\s?/) && RegExp.$2 || "all",
+      hasquery: mq.indexOf("(") > -1,
+      minw  : mq.match(/\(min\-width:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/) && parseFloat(RegExp.$1) + (RegExp.$2 || ""),
+      maxw  : mq.match(/\(max\-width:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/) && parseFloat(RegExp.$1) + (RegExp.$2 || "")
+    }
+  }
+}(Drupal, jQuery));
diff --git a/core/modules/layout/layout.install b/core/modules/layout/layout.install
new file mode 100644
index 0000000..af17f07
--- /dev/null
+++ b/core/modules/layout/layout.install
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * @file
+ * Install, update and uninstall functions for the layout module.
+ */
+
+/**
+ *
+ */
+function layout_enable() {
+  /* Set the preview to active on bartik if the setting does not exist. */
+  $theme_var = 'theme_bartik_settings';
+  $settings = variable_get($theme_var, array());
+  if (!isset($settings['layout_preview_tab'])) {
+    $settings['layout_preview_tab'] = 1;
+    variable_set($theme_var, $settings);
+  }
+}
+
+/**
+ * Implements hook_uninstall().
+ */
+function layout_uninstall() {
+  // Start with the global theme_settings.
+  $theme_settings_list = array('theme_settings');
+  // Get a list of themes for custom overrides.
+  $themes = list_themes(true);
+  foreach ($themes as $machine_name => $info) {
+    array_push($theme_settings_list, 'theme_' . $machine_name . '_settings');
+  }
+  // Remove the 'layout_preview' configuration value from each theme and the
+  // global theme settings.
+  foreach ($theme_settings_list as $theme_var) {
+    $settings = variable_get($theme_var, array());
+    if (isset($settings['layout_preview_tab'])) {
+      unset($settings['layout_preview_tab']);
+      variable_set($theme_var, $settings);
+    }
+  }
+}
diff --git a/core/modules/layout/layout.module b/core/modules/layout/layout.module
index c6ed7ae..16b8611 100644
--- a/core/modules/layout/layout.module
+++ b/core/modules/layout/layout.module
@@ -5,6 +5,8 @@
  * Manages page layouts for content presentation.
  */
 
+use Symfony\Component\HttpFoundation\JsonResponse;
+
 /**
  * Implements hook_menu().
  */
@@ -25,6 +27,13 @@ function layout_menu() {
     'access arguments' => array(4),
     'file' => 'layout.admin.inc',
   );
+  // Fetch breakpoints for a theme.
+  $items['layout/breakpoints'] = array(
+    'page callback' => 'layout_retrieve_theme_breakpoints_jsonp',
+    'access callback' => 'user_access',
+    'access arguments' => array('administer layouts'),
+    'type' => MENU_CALLBACK,
+  );
   return $items;
 }
 
@@ -78,5 +87,204 @@ function layout_theme($existing, $type, $theme, $path) {
       'template' => $layout['template'],
     );
   }
+
   return $items;
 }
+
+/**
+ *
+ */
+function layout_preview_toolbar_controls() {
+
+  $links = array();
+
+  $links += layout_get_devices_list();
+
+  $links['ruler'] = array(
+    'title' => t('Show developer ruler'),
+    'href' => '',
+    'fragment' => '!',
+    'exteranl' => TRUE,
+    'attributes' => array(
+      'id' => 'layout-preview-ruler-trigger',
+      'title' => t('Show the developer ruler above the page preview.'),
+    ),
+    'weight' => 100,
+  );
+
+  return $links;
+}
+
+/**
+ * Page callback: Returns the breakpoints of the current active theme.
+ *
+ * @see layout_menu().
+ */
+function layout_get_devices_list() {
+  $devices = config('layout.devices')->get('devices');
+
+  $links = array();
+
+  foreach($devices as $name => $info) {
+    $links[$name] = array(
+      'title' => $info['label'],
+      'href' => '',
+      'fragment' => '!',
+      'exteranl' => TRUE,
+      'options' => array(
+        'fragment' => '!',
+        'exteranl' => TRUE,
+      ),
+      'attributes' => array(
+        'class' => array('layout-preview-device'),
+        'data-layout-width' => ($info['dimensions']['width']) ? $info['dimensions']['width'] : '',
+        'data-layout-height' => ($info['dimensions']['height']) ? $info['dimensions']['height'] : '',
+      ),
+    );
+  }
+  return $links;
+}
+
+/**
+ * Page callback: Returns the breakpoints of the current active theme.
+ *
+ * @see layout_menu().
+ */
+function layout_retrieve_theme_breakpoints_jsonp() {
+  global $theme_key;
+
+  // Get the configured breakpoint to switch from vertical to horizontal
+  // toolbar presentation.
+  $media_queries = array();
+  $breakpoints = entity_load('breakpoint_group', 'theme.' . $theme_key . '.' . $theme_key);
+  if (!empty($breakpoints)) {
+    $media_queries = array_map(
+      function($object) {
+        return $object->mediaQuery;
+      },
+      $breakpoints->breakpoints
+    );
+  }
+
+  $response = new JsonResponse($media_queries);
+  return $response;
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function layout_form_system_theme_settings_alter(&$form, &$form_state) {
+  // Get the theme for the current settings form.
+  $theme = (!empty($form_state['build_info']['args'][0])) ? $form_state['build_info']['args'][0] : NULL;
+  $form['layout_preview'] = array(
+    '#type' => 'details',
+    '#title' => t('Device preview'),
+    '#attributes' => array(
+      'class' => array(
+        'theme-settings-bottom'
+      )
+    ),
+  );
+  $form['layout_preview']['layout_preview_tab'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Allow theme preview from the toolbar'),
+    '#default_value' => theme_get_setting('layout_preview_tab', $theme),
+    '#tree' => FALSE,
+  );
+}
+
+function layout_preview_access() {
+  global $custom_theme, $theme;
+  if (!empty($custom_theme)) {
+    $current_theme = $custom_theme;
+  }
+  else {
+    $current_theme = $theme ? $theme : variable_get('theme_default', 'bartik');
+  }
+  return (bool) theme_get_setting('layout_preview_tab', $current_theme);
+}
+
+/**
+ * Implements hook_toolbar().
+ */
+function layout_toolbar() {
+
+  $items['layout_preview'] = array(
+    '#type' => 'toolbar_item',
+    'tab' => array(
+      'trigger' => array(
+        '#theme' => 'html_tag',
+        '#tag' => 'button',
+        '#value' => t('Layout preview'),
+        '#attributes' => array(
+          'id' => 'layout-preview',
+          'title' => "Preview page layout",
+          'class' => array('icon', 'icon-layout', 'trigger'),
+        ),
+      ),
+      'device_options' => array(
+        '#theme' => 'links',
+        '#links' => layout_preview_toolbar_controls(),
+        '#attributes' => array(
+          'class' => array('layout-preview-options'),
+        ),
+      ),
+    ),
+    '#wrapper_attributes' => array(
+      'class' => array('layout-preview-toolbar-tab'),
+    ),
+    '#attached' => array(
+      'library' => array(
+        array('layout', 'layout.preview'),
+      ),
+    ),
+    '#weight' => 200,
+    '#access' => layout_preview_access(),
+  );
+
+  return $items;
+}
+
+/**
+ * Implements hook_library().
+ */
+function layout_library_info() {
+  $libraries = array();
+  $path = drupal_get_path('module', 'layout');
+  $options = array(
+    'scope' => 'footer',
+    'attributes' => array('defer' => TRUE),
+  );
+
+  $libraries['layout.preview'] = array(
+    'title' => 'Preview layouts',
+    'website' => 'http://drupal.org/project/layout',
+    'version' => VERSION,
+    'css' => array(
+      $path . '/css/layout.preview.base.css',
+      $path . '/css/layout.preview.theme.css',
+    ),
+    'js' => array(
+      // Core.
+      $path . '/js/layout.preview.js' => $options,
+      array(
+        'data' => array(
+          'layout' => array(
+            'routes' => array(
+              'breakpoints' => 'layout/breakpoints'
+            ),
+          ),
+        ),
+        'type' => 'setting',
+      ),
+    ),
+    'dependencies' => array(
+      array('system', 'jquery'),
+      array('system', 'drupal.ajax'),
+      array('system', 'drupalSettings'),
+      array('system', 'jquery.ui.slider'),
+    ),
+  );
+
+  return $libraries;
+}
-- 
1.7.10.4

