WordPress.org

Make WordPress Core

Ticket #37661: 37661.diff

File 37661.diff, 69.6 KB (added by celloexpressions, 3 years ago)

First pass, see completed and pending to-do lists above.

Line 
1Index: src/wp-admin/css/customize-controls.css
2===================================================================
3--- src/wp-admin/css/customize-controls.css     (revision 38260)
4+++ src/wp-admin/css/customize-controls.css     (working copy)
5@@ -975,29 +975,25 @@
6        animation: customize-reload .75s;
7 }
8 
9-.control-section-themes .accordion-section-title {
10+.control-panel-themes .accordion-section-title {
11        cursor: default;
12 }
13 
14-#customize-theme-controls .control-section-themes .accordion-section-title:hover,
15-#customize-theme-controls .control-section-themes .accordion-section-title:focus {
16+#customize-theme-controls .control-panel-themes > .accordion-section-title:hover,
17+#customize-theme-controls .control-panel-themes > .accordion-section-title:focus {
18        color: #555;
19        background-color: #fff;
20 }
21 
22-.control-section-themes .accordion-section-title {
23+#customize-theme-controls .control-panel-themes > .accordion-section-title {
24        margin: 15px 0;
25-}
26-
27-.customize-themes-panel .accordion-section-title {
28-       margin: 15px -8px;
29-}
30-
31-.control-section-themes .accordion-section-title {
32        padding-right: 100px; /* Space for the button */
33+       border-top: 1px solid #ddd;
34+       border-bottom: 1px solid #ddd;
35+       border-right: 0;
36 }
37 
38-.control-section-themes .accordion-section-title span.customize-action,
39+.control-panel-themes .accordion-section-title span.customize-action,
40 #customize-controls .customize-section-title span.customize-action {
41        font-size: 13px;
42        display: block;
43@@ -1004,8 +1000,7 @@
44        font-weight: 400;
45 }
46 
47-.control-section-themes .accordion-section-title .change-theme,
48-.control-section-themes .accordion-section-title .customize-theme {
49+.control-panel-themes .accordion-section-title .change-theme {
50        position: absolute;
51        right: 10px;
52        top: 50%;
53@@ -1013,45 +1008,191 @@
54        font-weight: 400;
55 }
56 
57-.control-section-themes .accordion-section-title:before {
58+#customize-theme-controls .control-panel-themes > .accordion-section-title:after {
59        display: none;
60 }
61 
62-.customize-themes-panel {
63+.control-panel-themes .control-panel-content {
64        display: none;
65-       padding: 0 8px;
66-       background: #f1f1f1;
67-       -webkit-box-sizing: border-box;
68-       -moz-box-sizing: border-box;
69-       box-sizing: border-box;
70+       position: fixed;
71+       width: 100%;
72+       height: 100%;
73+       top: 0;
74+       left: -100%;
75+       background: #eee;
76+       z-index: 10;
77+       transition: .2s left ease-in-out;
78+       margin: 0;
79+       padding: 0;
80+       overflow-y: auto;
81 }
82 
83-.customize-themes-panel .accordion-section-title:first-child {
84-       margin-top: 0;
85+.in-themes-panel .control-panel-themes .control-panel-content {
86+       left: 0;
87 }
88 
89-#customize-controls .customize-themes-panel .accordion-section-title:nth-child(2) {
90-       font-size: 14px;
91-       font-weight: 600;
92+#customize-controls,
93+#customize-footer-actions,
94+#customize-footer-actions .collapse-sidebar {
95+       left: 0;
96+       transition: .2s left ease-in-out;
97 }
98 
99+.in-themes-panel #customize-controls,
100+.in-themes-panel #customize-preview,
101+.in-themes-panel #customize-footer-actions,
102+.in-themes-panel #customize-footer-actions .collapse-sidebar {
103+       left: 100%;
104+}
105+
106 .customize-themes-panel > h2 {
107        padding: 15px 8px 0 8px;
108 }
109 
110-.control-section.open .customize-themes-panel {
111-       display: block;
112+.control-panel-themes .panel-meta .current-theme {
113+       position: absolute;
114+       right: 0;
115+       top: 0;
116+       background: #fff;
117+       border-left: 1px solid #ddd;
118+       padding: 15px 125px 15px 25px;
119 }
120 
121-#customize-theme-controls .customize-themes-panel .accordion-section-content {
122+.control-panel-themes .panel-meta .customize-theme {
123+       position: absolute;
124+       bottom: 15px;
125+       right: 25px;
126+}
127+
128+.control-panel-themes .panel-meta h2 {
129+       font-size: 32px;
130+       font-weight: 200;
131+       margin: 0 25px;
132+       padding: 15px 0;
133+       line-height: 50px;
134+       float: left;
135+}
136+
137+.control-panel-themes .panel-meta {
138+       min-height: 80px;
139+       overflow: hidden;
140+}
141+
142+.control-panel-themes .panel-meta .page-title-action {
143+       margin-top: 28px;
144+}
145+
146+/* Mobile header */
147+@media screen and (max-width:700px) {
148+       .control-panel-themes .panel-meta .current-theme {
149+               width: calc(100% - 150px);
150+               position: relative;
151+               border-bottom: 1px solid #ddd;
152+       }
153+}
154+
155+.control-panel-themes .customize-themes-notifications .notice {
156+       margin: 0 25px 15px 25px;
157+}
158+
159+#customize-theme-section-navigation {
160+       margin: 0 25px 15px 25px;
161+       width: calc(100% - 50px);
162+}
163+
164+.control-panel-themes .wp-filter .customize-help-toggle {
165+       float: right;
166+       padding: 10px;
167+       height: 40px;
168+       width: 40px;
169+       margin: 5px;
170        background: transparent;
171-       display: block;
172+       border: none;
173+       box-shadow: none;
174+       cursor: pointer;
175+       color: #555;
176 }
177 
178-.customize-control.customize-control-theme {
179-       margin-bottom: 8px;
180+.control-panel-themes .wp-filter .customize-help-toggle:hover {
181+       color: #0073aa;
182 }
183 
184+.control-panel-themes .wp-filter .customize-help-toggle:focus,
185+.control-panel-themes .wp-filter .customize-help-toggle.toggled {
186+       color: #0073aa;
187+       border-radius: 100%;
188+       box-shadow: 0 0 0 1px #5b9dd9,
189+                   0 0 2px 1px rgba(30, 140, 190, .8);
190+       outline: 0;
191+}
192+
193+.control-panel-themes .customize-panel-description {
194+       position: absolute;
195+       right: -1px;
196+       z-index: 5;
197+       background: #fff;
198+       font-size: 14px;
199+       width: 300px;
200+       border: 1px solid #ddd;
201+       border-top: none;
202+       padding: 5px 25px 15px 10px;
203+       text-align: right;
204+       display: none;
205+}
206+
207+.control-panel-themes .theme-section {
208+       width: calc(100% - 50px);
209+       padding: 0 25px 25px 25px;
210+       margin: 0;
211+       display: none !important; /* There is unknown JS that perpetually tries to show all theme sections when more items are added. */
212+       overflow: hidden;
213+}
214+
215+.control-panel-themes .theme-section.current-section {
216+       display: list-item !important; /* There is unknown JS that perpetually tries to show all theme sections when more items are added. */
217+}
218+
219+#customize-theme-section-navigation .customize-themes-section-title {
220+       display: inline-block;
221+       margin: 0 10px;
222+       padding: 15px 0;
223+       background: #fff;
224+       border: 0;
225+       border-bottom: 4px solid #fff;
226+       color: #555;
227+       cursor: pointer;
228+}
229+
230+.filter-links .themes-section-feature_filter_themes:before {
231+       content: "\f111";
232+       font: 16px/1 dashicons;
233+       position: relative;
234+       top: 2px;
235+       margin-right: 4px;
236+}
237+
238+#customize-theme-section-navigation .customize-themes-section-title.themes-section-search_themes {
239+       border-bottom: none;
240+       padding: 10px 0;
241+}
242+
243+#customize-theme-section-navigation .customize-themes-section-title.selected {
244+       border-bottom: 4px solid #555;
245+}
246+
247+#customize-theme-section-navigation .customize-themes-section-title:hover,
248+#customize-theme-section-navigation .customize-themes-section-title:focus {
249+       color: #0073aa;
250+       border-bottom: 4px solid #0073aa;
251+       box-shadow: none;
252+       outline: none;
253+}
254+
255+#customize-theme-section-navigation .customize-themes-section-title.themes-section-search_themes.selected,
256+#customize-theme-section-navigation .customize-themes-section-title.themes-section-search_themes:hover {
257+       border-bottom: none;
258+}
259+
260 #customize-theme-controls .themes.accordion-section-content {
261        position: relative;
262        left: 0;
263@@ -1059,15 +1200,112 @@
264        width: 100%;
265 }
266 
267-.wp-customizer .theme-browser .themes {
268-       padding-bottom: 8px;
269+.theme-section.loading .spinner {
270+       display: block;
271+       visibility: visible;
272+       position: relative;
273+       clear: both;
274+       width: 20px;
275+       height: 20px;
276+       left: calc(50% - 10px);
277+       float: none;
278+       margin-top: 50px;
279 }
280 
281-.wp-customizer .theme-browser .theme {
282+.customize-themes-section .filter-drawer {
283+       border-top: none;
284+       display: block;
285+       background: transparent;
286+       padding-top: 5px;
287+}
288+
289+.customize-themes-section .clear-filters {
290+       margin-left: 8px;
291+       display: none;
292+}
293+
294+#accordion-section-feature_filter_themes .theme-browser {
295+       display: none; /* Shown with JS once filters are applied */
296+}
297+
298+.customize-themes-section .no-themes {
299+       display: none;
300+}
301+
302+#accordion-section-installed_themes .theme .notice-success {
303+       display: none;
304+}
305+
306+.control-panel-themes .theme-browser .theme .theme-actions .button-primary {
307+       margin: 0 0 0 8px;
308+}
309+
310+.customize-control-theme .theme {
311+       width: 100%;
312        margin: 0;
313-       width: 100%;
314 }
315 
316+.customize-control.customize-control-theme { /* override most properties on .customize-control */
317+       box-sizing: border-box;
318+       width: 18.4%;
319+       margin: 0 2% 2% 0;
320+       padding: 0;
321+       clear: none;
322+}
323+
324+/* 5 columns above 2100px */
325+@media screen and (min-width: 2101px) {
326+       .customize-control.customize-control-theme:nth-child(5n) {
327+               margin-right: 0;
328+       }
329+}
330+
331+/* 4 columns up to 2100px */
332+@media screen and (min-width: 1601px) and (max-width: 2100px) {
333+       .customize-control.customize-control-theme {
334+               width: 23.5%;
335+       }
336+
337+       .customize-control.customize-control-theme:nth-child(4n) {
338+               margin-right: 0;
339+       }
340+}
341+
342+/* 3 columns up to 1600px */
343+@media screen and (min-width: 1101px) and (max-width: 1600px) {
344+       .customize-control.customize-control-theme {
345+               width: 32%;
346+       }
347+
348+       .customize-control.customize-control-theme:nth-child(3n) {
349+               margin-right: 0;
350+       }
351+}
352+
353+/* 2 columns up to 1100px */
354+@media screen and (min-width: 501px) and (max-width: 1100px) {
355+       .customize-control.customize-control-theme {
356+               width: 49%;
357+       }
358+
359+       .customize-control.customize-control-theme:nth-child(even) {
360+               margin-right: 0;
361+       }
362+}
363+
364+/* 1 column up to 500 px */
365+@media screen and (max-width: 500px) {
366+       .customize-control.customize-control-theme {
367+               width: 100%;
368+               margin: 0 0 3% 0;
369+       }
370+}
371+
372+
373+.wp-customizer .theme-browser .themes {
374+       padding-bottom: 8px;
375+}
376+
377 .wp-customizer .theme-browser .theme .theme-actions {
378        -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";
379        opacity: 1;
380@@ -1088,27 +1326,15 @@
381        width: 100%;
382 }
383 
384-#accordion-section-themes .accordion-section-title:after {
385+#accordion-panel-themes .accordion-section-title:after {
386        display: none;
387 }
388 
389-#customize-theme-controls .control-section-themes.current-panel > h3.accordion-section-title {
390+#customize-theme-controls .control-panel-themes.current-panel > h3.accordion-section-title {
391        left: 0;
392 }
393 
394-.customize-themes-panel.control-panel-content {
395-       position: absolute;
396-       left: -100%;
397-       top: 0;
398-       width: 100%;
399-       border-top: 1px solid #ddd;
400-}
401 
402-.in-themes-panel #customize-info,
403-.in-themes-panel #customize-theme-controls > ul > .accordion-section {
404-       left: 100%;
405-}
406-
407 /* Details View */
408 .wp-customizer .theme-overlay {
409        display: none;
410@@ -1129,6 +1355,15 @@
411        z-index: 110;
412 }
413 
414+.wp-customizer .theme-overlay .star-rating {
415+       float: left;
416+       margin-right: 8px;
417+}
418+
419+.wp-customizer .theme-rating .num-ratings {
420+       line-height: 20px;
421+}
422+
423 .wp-customizer .theme-overlay .theme-wrap {
424        left: 90px;
425        right: 90px;
426@@ -1139,9 +1374,14 @@
427 }
428 
429 .wp-customizer .theme-overlay .theme-actions {
430-       text-align: right; /* Because there's only one action, match the pattern of media modals and right-align the action. */
431+       text-align: right; /* Because there're only one or two actions, match the UI pattern of media modals and right-align the action. */
432+    padding: 10px 15px;
433 }
434 
435+.wp-customizer .theme-overlay .theme-actions .theme-install.preview {
436+       margin-left: 8px;
437+}
438+
439 .modal-open .in-themes-panel #customize-controls .wp-full-overlay-sidebar-content {
440        overflow: visible; /* Prevent the top-level Customizer controls from becoming visible when elements on the right of the details modal are focused. */
441 }
442Index: src/wp-admin/includes/theme.php
443===================================================================
444--- src/wp-admin/includes/theme.php     (revision 38260)
445+++ src/wp-admin/includes/theme.php     (working copy)
446@@ -606,7 +606,9 @@
447  * @since 4.2.0
448  */
449 function customize_themes_print_templates() {
450-       $preview_url = esc_url( add_query_arg( 'theme', '__THEME__' ) ); // Token because esc_url() strips curly braces.
451+       $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
452+       $active_url  = esc_url( remove_query_arg( 'theme', $current_url ) );
453+       $preview_url = esc_url( add_query_arg( 'theme', '__THEME__', $current_url ) ); // Token because esc_url() strips curly braces.
454        $preview_url = str_replace( '__THEME__', '{{ data.id }}', $preview_url );
455        ?>
456        <script type="text/html" id="tmpl-customize-themes-details-view">
457@@ -619,7 +621,7 @@
458                        </div>
459                        <div class="theme-about wp-clearfix">
460                                <div class="theme-screenshots">
461-                               <# if ( data.screenshot[0] ) { #>
462+                               <# if ( data.screenshot && data.screenshot[0] ) { #>
463                                        <div class="screenshot"><img src="{{ data.screenshot[0] }}" alt="" /></div>
464                                <# } else { #>
465                                        <div class="screenshot blank"></div>
466@@ -632,29 +634,46 @@
467                                        <# } #>
468                                        <h2 class="theme-name">{{{ data.name }}}<span class="theme-version"><?php printf( __( 'Version: %s' ), '{{ data.version }}' ); ?></span></h2>
469                                        <h3 class="theme-author"><?php printf( __( 'By %s' ), '{{{ data.authorAndUri }}}' ); ?></h3>
470+
471+                                       <# if ( data.stars && 0 != data.num_ratings ) { #>
472+                                               <div class="theme-rating">
473+                                                       {{{ data.stars }}}
474+                                                       <span class="num-ratings"><?php echo sprintf( __( '(%s ratings)' ), '{{ data.num_ratings }}' ); ?></span>
475+                                               </div>
476+                                       <# } #>
477+
478+                                       <# if ( false == false && data.hasUpdate ) { // @todo patch updates.js to work in the customizer #>
479+                                               <div class="notice notice-warning notice-alt notice-large">
480+                                                       <h3 class="notice-title"><?php _e( 'Update Available' ); ?></h3>
481+                                                       {{{ data.update }}}
482+                                               </div>
483+                                       <# } #>
484+
485                                        <p class="theme-description">{{{ data.description }}}</p>
486 
487                                        <# if ( data.parent ) { #>
488                                                <p class="parent-theme"><?php printf( __( 'This is a child theme of %s.' ), '<strong>{{{ data.parent }}}</strong>' ); ?></p>
489                                        <# } #>
490-
491                                        <# if ( data.tags ) { #>
492-                                               <p class="theme-tags"><span><?php _e( 'Tags:' ); ?></span> {{ data.tags }}</p>
493+                                               <p class="theme-tags"><span><?php _e( 'Tags:' ); ?></span> {{{ data.tags }}}</p>
494                                        <# } #>
495                                </div>
496                        </div>
497 
498-                       <# if ( ! data.active ) { #>
499-                               <div class="theme-actions">
500-                                       <div class="inactive-theme">
501-                                               <?php
502-                                               /* translators: %s: Theme name */
503-                                               $aria_label = sprintf( __( 'Preview %s' ), '{{ data.name }}' );
504-                                               ?>
505-                                               <a href="<?php echo $preview_url; ?>" target="_top" class="button button-primary" aria-label="<?php echo esc_attr( $aria_label ); ?>"><?php _e( 'Live Preview' ); ?></a>
506-                                       </div>
507-                               </div>
508-                       <# } #>
509+                       <div class="theme-actions">
510+                               <# if ( data.active ) { #>
511+                                       <?php
512+                                       /* translators: %s: Theme name */
513+                                       $aria_label = sprintf( __( 'Preview %s' ), '{{ data.name }}' );
514+                                       ?>
515+                                       <button type="button" class="button button-primary customize-theme" aria-label="<?php echo esc_attr( $aria_label ); ?>"><?php _e( 'Customize' ); ?></a>
516+                               <# } else if ( 'installed' === data.type ) { #>
517+                                       <button type="button" class="button button-primary preview-theme" data-preview-url="<?php echo esc_attr( $preview_url ); ?>" aria-label="<?php echo esc_attr( $aria_label ); ?>"><?php _e( 'Live Preview' ); ?></span>
518+                               <# } else { #>
519+                                       <button type="button" class="button theme-install" data-slug="{{ data.id }}"><?php _e( 'Install' ); ?></button>
520+                                       <button type="button" class="button button-primary theme-install preview" data-slug="{{ data.id }}" data-previewurl="<?php echo esc_attr( $preview_url ); ?>"><?php _e( 'Install & Preview' ); ?></button>
521+                               <# } #>
522+                       </div>
523                </div>
524        </script>
525        <?php
526Index: src/wp-admin/js/customize-controls.js
527===================================================================
528--- src/wp-admin/js/customize-controls.js       (revision 38260)
529+++ src/wp-admin/js/customize-controls.js       (working copy)
530@@ -774,8 +775,8 @@
531        /**
532         * wp.customize.ThemesSection
533         *
534-        * Custom section for themes that functions similarly to a backwards panel,
535-        * and also handles the theme-details view rendering and navigation.
536+        * Custom section for themes that loads themes by category, and also
537+        * handles the theme-details view rendering and navigation.
538         *
539         * @constructor
540         * @augments wp.customize.Section
541@@ -787,18 +788,14 @@
542                template: '',
543                screenshotQueue: null,
544                $window: $( window ),
545+               loaded: 0,
546+               loading: false,
547+               fullyLoaded: false,
548+               term: '',
549 
550                /**
551                 * @since 4.2.0
552                 */
553-               initialize: function () {
554-                       this.$customizeSidebar = $( '.wp-full-overlay-sidebar-content:first' );
555-                       return api.Section.prototype.initialize.apply( this, arguments );
556-               },
557-
558-               /**
559-                * @since 4.2.0
560-                */
561                ready: function () {
562                        var section = this;
563                        section.overlay = section.container.find( '.theme-overlay' );
564@@ -826,7 +823,7 @@
565                                }
566                        });
567 
568-                       _.bindAll( this, 'renderScreenshots' );
569+                       _.bindAll( this, 'renderScreenshots', 'loadMore', 'checkTerm', 'filtersChecked', 'clearFilters' );
570                },
571 
572                /**
573@@ -833,8 +830,8 @@
574                 * Override Section.isContextuallyActive method.
575                 *
576                 * Ignore the active states' of the contained theme controls, and just
577-                * use the section's own active state instead. This ensures empty search
578-                * results for themes to cause the section to become inactive.
579+                * use the section's own active state instead. This prevents empty search
580+                * results for theme sections from causing the section to become inactive.
581                 *
582                 * @since 4.2.0
583                 *
584@@ -850,20 +847,22 @@
585                attachEvents: function () {
586                        var section = this;
587 
588-                       // Expand/Collapse section/panel.
589-                       section.container.find( '.change-theme, .customize-theme' ).on( 'click keydown', function( event ) {
590-                               if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
591-                                       return;
592-                               }
593-                               event.preventDefault(); // Keep this AFTER the key filter above
594-
595-                               if ( section.expanded() ) {
596-                                       section.collapse();
597-                               } else {
598+                       // Expand section/panel. Only collapse when opening another section.
599+                       $( '#customize-theme-section-navigation' ).on( 'click', '.themes-section-' + section.id, function( event ) {
600+                               if ( ! section.expanded() ) {
601                                        section.expand();
602                                }
603                        });
604 
605+                       // Preview installed themes.
606+                       section.container.on( 'click', '.theme-actions .preview-theme', function() {
607+                               var previewUrl = $( this ).data( 'previewUrl' );
608+
609+                               $( '.wp-full-overlay' ).addClass( 'customize-loading' );
610+
611+                               window.parent.location = previewUrl;
612+                       });
613+       
614                        // Theme navigation in details view.
615                        section.container.on( 'click keydown', '.left', function( event ) {
616                                if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
617@@ -909,18 +908,46 @@
618 
619                                // Update theme count.
620                                count = section.container.find( 'li.customize-control:visible' ).length;
621-                               section.container.find( '.theme-count' ).text( count );
622+                               $( '.control-panel-themes' ).find( '.theme-count' ).text( count );
623                        });
624 
625-                       // Pre-load the first 3 theme screenshots.
626-                       api.bind( 'ready', function () {
627-                               _.each( section.controls().slice( 0, 3 ), function ( control ) {
628-                                       var img, src = control.params.theme.screenshot[0];
629-                                       if ( src ) {
630-                                               img = new Image();
631-                                               img.src = src;
632+                       // Event listeners for queries with user-entered terms.
633+                       if ( 'search' === section.params.action ) {
634+                               var debounced = _.debounce( section.checkTerm, 800 ); // Wait until there is no input for 800 miliseconds to initiate a search.
635+                               $( '#customize-theme-section-navigation' ).on( 'keydown', '#wp-filter-search-input', function() {
636+                                       debounced( section );
637+                               });
638+                       } else if ( 'favorites' === section.params.action ) {
639+                               section.container.on( 'click', '.favorites-form-submit', function() {
640+                                       section.checkTerm( section );
641+                               });
642+                               section.container.on( 'keydown', '#wporg-username-input', function( e ) {
643+                                       if ( api.utils.isKeydownButNotEnterEvent( e ) ) {
644+                                               return;
645                                        }
646+                                       section.checkTerm( section );
647                                });
648+                       } else if ( 'feature_filter' === section.params.action ) {
649+                               section.container.on( 'click', '.filter-group input', function() {
650+                                       section.filtersChecked();
651+                               });
652+                               section.container.on( 'click', '.clear-filters', function() {
653+                                       section.clearFilters();
654+                               });
655+                               section.container.on( 'click', '.apply-filters', function() {
656+                                       section.checkTerm( section );
657+                               });
658+                               section.container.on( 'click', '.filtered-by .tags, .filtered-by button', function() {
659+                                       section.container.find( '.filter-group' ).show();
660+                                       section.container.find( '.buttons' ).show();
661+                                       section.container.find( '.filtered-by' ).hide();
662+                                       section.container.find( '.theme-browser' ).hide();
663+                               });
664+                       }
665+
666+                       // Move section-expansion buttons to consolidated menu bar in themes panel.
667+                       api.bind( 'ready', function () {
668+                               section.container.find( '.customize-themes-section-title' ).appendTo( $( '#customize-theme-section-navigation .filter-links' ) );
669                        });
670                },
671 
672@@ -945,70 +972,292 @@
673                        }
674 
675                        // Note: there is a second argument 'args' passed
676-                       var position, scroll,
677-                               panel = this,
678-                               section = panel.container.closest( '.accordion-section' ),
679-                               overlay = section.closest( '.wp-full-overlay' ),
680-                               container = section.closest( '.wp-full-overlay-sidebar-content' ),
681-                               siblings = container.find( '.open' ),
682-                               customizeBtn = section.find( '.customize-theme' ),
683-                               changeBtn = section.find( '.change-theme' ),
684-                               content = section.find( '.control-panel-content' );
685+                       var section = this,
686+                               overlay = section.container.closest( '.wp-full-overlay-sidebar-content' ),
687+                               container = section.container.closest( '.control-panel-content' );
688 
689                        if ( expanded ) {
690 
691+                               // Load controls if none are loaded yet.
692+                               if ( 0 === section.loaded ) {
693+                                       section.loadControls();
694+                               }
695+
696                                // Collapse any sibling sections/panels
697                                api.section.each( function ( otherSection ) {
698-                                       if ( otherSection !== panel ) {
699+                                       if ( otherSection !== section ) {
700                                                otherSection.collapse( { duration: args.duration } );
701                                        }
702                                });
703-                               api.panel.each( function ( otherPanel ) {
704-                                       otherPanel.collapse( { duration: 0 } );
705-                               });
706 
707-                               content.show( 0, function() {
708-                                       position = content.offset().top;
709-                                       scroll = container.scrollTop();
710-                                       content.css( 'margin-top', ( $( '#customize-header-actions' ).height() - position - scroll ) );
711-                                       section.addClass( 'current-panel' );
712-                                       overlay.addClass( 'in-themes-panel' );
713-                                       container.scrollTop( 0 );
714-                                       _.delay( panel.renderScreenshots, 10 ); // Wait for the controls
715-                                       panel.$customizeSidebar.on( 'scroll.customize-themes-section', _.throttle( panel.renderScreenshots, 300 ) );
716-                                       if ( args.completeCallback ) {
717-                                               args.completeCallback();
718-                                       }
719-                               } );
720-                               customizeBtn.focus();
721+                               section.container.addClass( 'current-section' );
722+                               $( '#customize-theme-section-navigation .themes-section-' + section.id ).addClass( 'selected' );
723+                               container.on( 'scroll', _.throttle( section.renderScreenshots, 300 ) );
724+                               container.on( 'scroll', _.throttle( section.loadMore, 300 ) );
725+                               if ( args.completeCallback ) {
726+                                       args.completeCallback();
727+                               }
728+                               section.updateCount(); // Show this section's count.
729                        } else {
730-                               siblings.removeClass( 'open' );
731-                               section.removeClass( 'current-panel' );
732-                               overlay.removeClass( 'in-themes-panel' );
733-                               panel.$customizeSidebar.off( 'scroll.customize-themes-section' );
734-                               content.delay( 180 ).hide( 0, function() {
735-                                       content.css( 'margin-top', 'inherit' ); // Reset
736-                                       if ( args.completeCallback ) {
737-                                               args.completeCallback();
738-                                       }
739-                               } );
740-                               customizeBtn.attr( 'tabindex', '0' );
741-                               changeBtn.focus();
742-                               container.scrollTop( 0 );
743+                               section.container.removeClass( 'current-section' );
744+                               $( '#customize-theme-section-navigation .themes-section-' + section.id ).removeClass( 'selected' );
745+                               container.off( 'scroll' );
746+                               if ( args.completeCallback ) {
747+                                       args.completeCallback();
748+                               }
749                        }
750                },
751 
752                /**
753-                * Recalculate the top margin.
754+                * Don't recalculate the top margin.
755                 *
756-                * @since 4.4.0
757+                * @since 4.7.0
758                 * @private
759                 */
760-               _recalculateTopMargin: function() {
761-                       api.Panel.prototype._recalculateTopMargin.call( this );
762+               _recalculateTopMargin: function() {},
763+
764+               /**
765+                * Load theme data via ajax and add themes to the section as controls.
766+                *
767+                * @since 4.7.0
768+                */
769+               loadControls: function() {
770+                       var section = this, params, page, request, search;
771+
772+                       if ( section.loading ) {
773+                               return; // We're already loading a batch of themes.
774+                       }
775+
776+                       // Parameters for every API query. Additional params are set in PHP.
777+                       page = Math.ceil( section.loaded / 100 ) + 1;
778+                       params = {
779+                               'customize-themes-nonce': api.settings.nonce['customize-themes'],
780+                               'wp_customize': 'on',
781+                               'theme_action': section.params.action,
782+                               'customized_theme': api.settings.theme.stylesheet,
783+                               'page': page
784+                       }
785+
786+                       // Add fields for special request actions.
787+                       if ( 'search' === section.params.action ) {
788+                               if ( '' === section.term ) {
789+                                       return;
790+                               } else {
791+                                       params.search = section.term;
792+                               }
793+                       } else if ( 'favorites' === section.params.action ) {
794+                               if ( '' === section.term ) {
795+                                       return;
796+                               } else {
797+                                       params.user = section.term;
798+                               }
799+                       } else if ( 'feature_filter' === section.params.action ) {
800+                               if ( '' === section.term ) {
801+                                       return;
802+                               } else {
803+                                       params.tags = section.term;
804+                               }
805+                       }
806+
807+                       // Load themes.
808+                       section.container.addClass( 'loading' );
809+                       section.loading = true;
810+                       section.container.find( '.no-themes' ).hide();
811+                       request = wp.ajax.post( 'customize-load-themes', params );
812+                       request.done(function( data ) {
813+                               var themes = data.themes,
814+                                   themeControl, newThemeControls;
815+                               if ( 0 !== themes.length ) {
816+                                       newThemeControls = new Array();
817+                                       // Add controls for each theme.
818+                                       _.each( themes, function ( theme ) {
819+                                               customizeId = section.params.action + '_theme_' + theme.id;
820+                                               themeControl = new api.controlConstructor.theme( customizeId, {
821+                                                       params: {
822+                                                               type: 'theme',
823+                                                               content: '<li id="customize-control-theme-' + section.params.action + '_' + theme.id + '" class="customize-control customize-control-theme"></li>',
824+                                                               section: section.params.id,
825+                                                               active: true,
826+                                                               theme: theme,
827+                                                               priority: section.loaded + 1
828+                                                       },
829+                                                       previewer: api.previewer
830+                                               } );
831+
832+                                               api.control.add( customizeId, themeControl );
833+                                               newThemeControls.push( themeControl );
834+                                               section.loaded = section.loaded + 1;
835+                                               return;
836+                                       });
837+                               
838+                                       if ( 1 === page ) {
839+                                               // Pre-load the first 3 theme screenshots.
840+                                               _.each( section.controls().slice( 0, 3 ), function ( control ) {
841+                                                       var img, src = control.params.theme.screenshot[0];
842+                                                       if ( src ) {
843+                                                               img = new Image();
844+                                                               img.src = src;
845+                                                       }
846+                                               });
847+                                       } else {
848+                                               Array.prototype.push.apply( section.screenshotQueue, newThemeControls ); // Add new themes to the screenshot queue.
849+                                       }
850+                                       _.delay( section.renderScreenshots, 100 ); // Wait for the controls to become visible.
851+
852+                                       if ( 'installed' === section.action || 100 > themes.length ) { // If we have less than the requested 100 themes, it's the end of the list.
853+                                               section.fullyLoaded = true;
854+                                       }
855+                               } else {
856+                                       if ( 0 === section.loaded ) {
857+                                               section.container.find( '.no-themes' ).show();
858+                                       } else {
859+                                               section.fullyLoaded = true;
860+                                       }
861+                               }
862+                               if ( 'installed' === section.params.action ) {
863+                                       section.updateCount();
864+                               } else {
865+                                       section.updateCount( data.info.results );
866+                               }
867+                               section.container.find( '.unexpected-error' ).hide(); // Hide error notice in case it was previously shown.
868+
869+                               // This cannot run on request.always, as section.loading may turn false before the new controls load in the success case.
870+                               section.container.removeClass( 'loading' );
871+                               section.loading = false;
872+                       });
873+                       request.fail(function( data ) {
874+                               if ( 'undefined' === typeof data ) {
875+                                       section.container.find( '.unexpected-error' ).show();
876+                               } else if ( typeof console !== 'undefined' && console.error ) {
877+                                       console.error( data );
878+                               }
879+                               
880+                               // This cannot run on request.always, as section.loading may turn false before the new controls load in the success case.
881+                               section.container.removeClass( 'loading' );
882+                               section.loading = false;
883+                       });
884                },
885 
886                /**
887+                * Determines whether more themes should be loaded, and loads them.
888+                *
889+                * @since 4.7.0
890+                */
891+               loadMore: function() {
892+                       var section = this, container, bottom, threshold, page;
893+                       if ( ! section.fullyLoaded && ! section.loading ) {
894+                               container = section.container.closest( '.control-panel-content' );
895+
896+                               bottom = container.scrollTop() + container.height();
897+                               threshold = container.prop( 'scrollHeight' ) - 3000; // Use a fixed distance to the bottom of loaded results to avoid unnecessarily loading results sooner when using a percentage of scroll distance.
898+
899+                               if ( bottom > threshold ) {
900+                                       section.loadControls();
901+                               }
902+                       }
903+               },
904+
905+               /**
906+                * Event handler for search, feature filter, and favorites input that determines if the term has changed and loads new controls as needed.
907+                *
908+                * @since 4.7.0
909+                *
910+                * @param api.ThemesSection section The current theme section, passed through the debouncer.
911+                */
912+               checkTerm: function( section ) {
913+                       var newTerm, filteringBy;
914+
915+                       // Find term.
916+                       if ( 'search' === section.params.action ) {
917+                               newTerm = $( '#wp-filter-search-input' ).val();
918+                       } else if ( 'favorites' === section.params.action ) {
919+                               newTerm = $( '#wporg-username-input' ).val();
920+                       } else if ( 'feature_filter' === section.params.action ) {
921+                               newTerm = section.term; // Set separately by filtersChecked(), as they're changed.
922+                               if ( '' === newTerm ) {
923+                                       return;
924+                               }
925+
926+                               section.container.find( '.filter-group' ).hide();
927+                               section.container.find( '.buttons' ).hide();
928+                               section.container.find( '.filtered-by' ).show();
929+                               section.container.find( '.theme-browser' ).show();
930+                               filteringBy = section.container.find( '.filtered-by .tags' );
931+                               filteringBy.empty();
932+
933+                               _.each( newTerm, function( tag ) {
934+                                       name = $( 'label[for="filter-id-' + tag + '"]' ).text();
935+                                       filteringBy.append( '<span class="tag">' + name + '</span>' );
936+                               });
937+                       } else {
938+                               return;
939+                       }
940+
941+                       if ( section.term === newTerm && 'feature_filter' !== section.params.action ) {
942+                               return;
943+                       }
944+                       // Clear the controls in the section.
945+                       _.each( section.controls(), function( control ) {
946+                               control.container.remove();
947+                               api.control.remove( control.id );
948+                       });
949+                       section.loaded = 0;
950+                       section.fullyLoaded = false;
951+                       section.screenshotQueue = null;
952+
953+                       if ( '' !== newTerm ) { // Empty term should not show any results.
954+                               // Run a new query, with loadControls handling paging, etc.
955+                               section.term = newTerm;
956+                               section.loadControls();
957+                       }
958+               },
959+               
960+               /**
961+                * Check for filters checked in the feature filter list.
962+                *
963+                * @since 4.7.0
964+                */
965+               filtersChecked: function() {
966+                       var section = this,
967+                           items = section.container.find( '.filter-group' ).find( ':checkbox' ),
968+                           tags = [];
969+
970+                       if ( 'feature_filter' !== section.params.action ) {
971+                               return false;
972+                       }
973+
974+                       _.each( items.filter( ':checked' ), function( item ) {
975+                               tags.push( $( item ).prop( 'value' ) );
976+                       });
977+
978+                       // When no filters are checked, restore initial state and return
979+                       if ( tags.length === 0 ) {
980+                               section.container.find( '.filter-drawer .apply-filters' ).find( 'span' ).text( '' );
981+                               section.container.find( '.filter-drawer .clear-filters' ).hide();
982+                               section.term = '';
983+                       } else {
984+                               section.container.find( '.filter-drawer .apply-filters' ).find( 'span' ).text( tags.length );
985+                               section.container.find( '.filter-drawer .clear-filters' ).css( 'display', 'inline-block' ); // $.show() manually.
986+                               section.term = tags;
987+                       }
988+               },
989+
990+               /**
991+                * Clear filters from the feature filter list.
992+                *
993+                * @since 4.7.0
994+                */
995+               clearFilters: function() {
996+                       var section = this,
997+                           items = section.container.find( '.filter-group' ).find( ':checkbox' );
998+
999+                       _.each( items.filter( ':checked' ), function( item ) {
1000+                               $( item ).prop( 'checked', false );
1001+                       });
1002+                       section.filtersChecked();
1003+               },
1004+
1005+               /**
1006                 * Render control's screenshot if the control comes into view.
1007                 *
1008                 * @since 4.2.0
1009@@ -1016,12 +1265,15 @@
1010                renderScreenshots: function( ) {
1011                        var section = this;
1012 
1013-                       // Fill queue initially.
1014-                       if ( section.screenshotQueue === null ) {
1015-                               section.screenshotQueue = section.controls();
1016+                       // Fill queue initially, or check for more if empty.
1017+                       if ( section.screenshotQueue === null || 0 === section.screenshotQueue.length ) {
1018+                               // Add controls that haven't had their screenshots rendered.
1019+                               section.screenshotQueue = _.filter( section.controls(), function( control ) {
1020+                                       return ! control.screenshotRendered;
1021+                               });
1022                        }
1023 
1024-                       // Are all screenshots rendered?
1025+                       // Are all screenshots rendered (for now)?
1026                        if ( ! section.screenshotQueue.length ) {
1027                                return;
1028                        }
1029@@ -1057,6 +1309,18 @@
1030                },
1031 
1032                /**
1033+                * Update the number of themes in the section.
1034+                *
1035+                * @since 4.7.0
1036+                */
1037+               updateCount: function ( count ) {
1038+                       if ( ! count ) {
1039+                               count = this.loaded;
1040+                       }
1041+                       this.container.closest( '.control-panel-content' ).find( '.theme-count' ).text( count );
1042+               },
1043+
1044+               /**
1045                 * Advance the modal to the next theme.
1046                 *
1047                 * @since 4.2.0
1048@@ -1076,13 +1340,13 @@
1049                 * @since 4.2.0
1050                 */
1051                getNextTheme: function () {
1052-                       var control, next;
1053-                       control = api.control( 'theme_' + this.currentTheme );
1054+                       var section = this, control, next;
1055+                       control = api.control( section.params.action + '_theme_' + this.currentTheme );
1056                        next = control.container.next( 'li.customize-control-theme' );
1057                        if ( ! next.length ) {
1058                                return false;
1059                        }
1060-                       next = next[0].id.replace( 'customize-control-', '' );
1061+                       next = next[0].id.replace( 'customize-control-theme-' + section.params.action, section.params.action + '_theme' );
1062                        control = api.control( next );
1063 
1064                        return control.params.theme;
1065@@ -1108,13 +1372,13 @@
1066                 * @since 4.2.0
1067                 */
1068                getPreviousTheme: function () {
1069-                       var control, previous;
1070-                       control = api.control( 'theme_' + this.currentTheme );
1071+                       var section = this, control, previous;
1072+                       control = api.control( section.params.action + '_theme_' + this.currentTheme );
1073                        previous = control.container.prev( 'li.customize-control-theme' );
1074                        if ( ! previous.length ) {
1075                                return false;
1076                        }
1077-                       previous = previous[0].id.replace( 'customize-control-', '' );
1078+                       previous = previous[0].id.replace( 'customize-control-theme-' + section.params.action, section.params.action + '_theme' );
1079                        control = api.control( previous );
1080 
1081                        return control.params.theme;
1082@@ -1162,7 +1426,7 @@
1083                closeDetails: function () {
1084                        $( 'body' ).removeClass( 'modal-open' );
1085                        this.overlay.fadeOut( 'fast' );
1086-                       api.control( 'theme_' + this.currentTheme ).focus();
1087+                       api.control( this.params.action + '_theme_' + this.currentTheme ).container.find( '.theme' ).focus();
1088                },
1089 
1090                /**
1091@@ -1452,7 +1716,252 @@
1092                }
1093        });
1094 
1095+
1096        /**
1097+        * wp.customize.ThemesPanel
1098+        *
1099+        * Custom section for themes that displays without the customize preview.
1100+        *
1101+        * @constructor
1102+        * @augments wp.customize.Panel
1103+        * @augments wp.customize.Container
1104+        */
1105+       api.ThemesPanel = api.Panel.extend({
1106+               installingThemes: [],
1107+
1108+               /**
1109+                * @since 4.7.0
1110+                */
1111+               attachEvents: function () {
1112+                       var panel = this;
1113+
1114+                       // Expand/Collapse panel.
1115+                       panel.container.on( 'click', '.change-theme, .customize-theme', function( event ) {
1116+                               if ( panel.expanded() ) {
1117+                                       panel.collapse();
1118+                               } else {
1119+                                       panel.expand();
1120+                               }
1121+                       });
1122+
1123+                       // Toggle help display.
1124+                       panel.container.on( 'click', '.customize-help-toggle', function() {
1125+                               var button = $( this ),
1126+                                   content = button.next( '.customize-panel-description' );
1127+                               button.toggleClass( 'toggled' );
1128+                               content.toggle();
1129+                               if ( button.hasClass( 'toggled' ) ) {
1130+                                       button.attr( 'aria-expanded', 'true' );
1131+                               } else {
1132+                                       button.attr( 'aria-expanded', 'false' );
1133+                               }
1134+                       });
1135+
1136+                       api.bind( 'saved', function() {
1137+                               panel.container.find( '.customize-themes-unsaved-changes' ).hide();
1138+                       });
1139+                       
1140+                       // Save & publish customizer changes.
1141+                       panel.container.on( 'click', '#customize-themes-save', function() {
1142+                               $( '#save' ).click(); // Trigger customizer save.
1143+                               panel.container.find( '.customize-themes-unsaved-changes' ).hide();
1144+                               api.section( 'installed_themes' ).focus();
1145+                       });
1146+                       
1147+                       // Toggle theme upload view.
1148+                       panel.container.on( 'click', '.upload-toggle', function() {
1149+                               var button = $( this );
1150+                               panel.container.find( '.upload-theme' ).toggle();
1151+                               if ( 'true' === button.attr( 'aria-expanded' ) ) {
1152+                                       button.attr( 'aria-expanded', 'false' );
1153+                               } else {
1154+                                       button.attr( 'aria-expanded', 'true' );
1155+                               }
1156+                       });
1157+
1158+                       // Install theme.
1159+                       panel.container.on( 'click', '.theme-install', function( event ) {
1160+                               panel.installTheme( event );
1161+                       });
1162+
1163+                       // Update theme.
1164+                       panel.container.on( 'click', '.update-theme', function( event ) {
1165+                               panel.updateTheme( event );
1166+                       });
1167+
1168+                       _.bindAll( this, 'installTheme', 'updateTheme' );
1169+               },
1170+
1171+               /**
1172+                * Update UI to reflect expanded state
1173+                *
1174+                * @since 4.7.0
1175+                *
1176+                * @param {Boolean}  expanded
1177+                * @param {Object}   args
1178+                * @param {Boolean}  args.unchanged
1179+                * @param {Callback} args.completeCallback
1180+                */
1181+               onChangeExpanded: function ( expanded, args ) {
1182+
1183+                       // Immediately call the complete callback if there were no changes
1184+                       if ( args.unchanged ) {
1185+                               if ( args.completeCallback ) {
1186+                                       args.completeCallback();
1187+                               }
1188+                               return;
1189+                       }
1190+
1191+                       // Note: there is a second argument 'args' passed
1192+                       var panel = this,
1193+                               section = panel.container.closest( '.accordion-section' ),
1194+                               overlay = section.closest( '.wp-full-overlay' ),
1195+                               customizeBtn = section.find( '.customize-theme' ),
1196+                               changeBtn = section.find( '.change-theme' ),
1197+                               content = section.find( '.control-panel-content' );
1198+
1199+                       if ( expanded ) {
1200+                               content.show( 0, function() {
1201+                                       overlay.addClass( 'in-themes-panel' );
1202+                                       section.addClass( 'current-panel' );
1203+                                       customizeBtn.attr( 'tabindex', '0' );
1204+                                       customizeBtn.focus();
1205+                                       if ( false === api.state( 'saved' ).get() ) {
1206+                                               panel.container.find( '.customize-themes-unsaved-changes' ).show();
1207+                                       }
1208+                               });
1209+                               // Automatically open the installed themes section.
1210+                               api.section( 'installed_themes' ).expand();
1211+                       } else {
1212+                               section.removeClass( 'current-panel' );
1213+                               overlay.removeClass( 'in-themes-panel' );
1214+                               content.delay( 200 ).hide( 0, function() {
1215+                                       if ( args.completeCallback ) {
1216+                                               args.completeCallback();
1217+                                       }
1218+                               } );
1219+                               customizeBtn.attr( 'tabindex', '-1' );
1220+                               changeBtn.focus();
1221+                       }
1222+               },
1223+
1224+               /**
1225+                * Don't recalculate the top margin.
1226+                *
1227+                * @since 4.7.0
1228+                * @private
1229+                */
1230+               _recalculateTopMargin: function() {},
1231+
1232+               /**
1233+                * Install a theme via wp.updates.
1234+                *
1235+                * @since 4.7.0
1236+                */
1237+               installTheme: function( event ) {
1238+                       var preview = false, previewUrl,
1239+                           slug = $( event.target ).data( 'slug' );
1240+
1241+                       if ( -1 !== $.inArray( this.installingThemes, slug ) ) {
1242+                               return; // Theme is already being installed.
1243+                       }
1244+
1245+                       wp.updates.maybeRequestFilesystemCredentials( event );
1246+
1247+                       $( document ).on( 'wp-theme-install-success', function( event, response ) {
1248+                               var theme = false, customizeId, themeControl;
1249+                               if ( preview ) {
1250+                                       window.parent.location = previewUrl;
1251+                               } else {
1252+                                       api.control.each( function( control ) {
1253+                                               if ( 'theme' === control.params.type && control.params.theme.id === response.slug ) {
1254+                                                       theme = control.params.theme; // Used below to add theme control.
1255+                                                       control.rerenderAsInstalled();
1256+                                               }
1257+                                       });
1258+
1259+                                       // Don't add the same theme more than once.
1260+                                       if ( ! theme || 'undefined' !== typeof api.control( 'installed_theme_' + theme.id ) ) {
1261+                                               return;
1262+                                       }
1263+
1264+                                       // Add theme control to installed section.
1265+                                       theme.type = 'installed';
1266+                                       customizeId = 'installed_theme_' + theme.id;
1267+                                       themeControl = new api.controlConstructor.theme( customizeId, {
1268+                                               params: {
1269+                                                       type: 'theme',
1270+                                                       content: '<li id="customize-control-theme-installed_' + theme.id + '" class="customize-control customize-control-theme"></li>',
1271+                                                       section: 'installed_themes',
1272+                                                       active: true,
1273+                                                       theme: theme,
1274+                                                       priority: 0 // Add all newly-installed themes to the top.
1275+                                               },
1276+                                               previewer: api.previewer
1277+                                       } );
1278+
1279+                                       api.control.add( customizeId, themeControl );
1280+                                       api.control( customizeId ).container.trigger( 'render-screenshot' );
1281+
1282+                                       // Close the details modal if it's open to the installed theme.
1283+                                       api.section.each( function( section ) {
1284+                                               if ( 'themes' !== section.params.type ) {
1285+                                                       if ( theme.id === section.currentTheme ) { // Don't close the modal if the user has navigated elsewhere.
1286+                                                               section.closeDetails();
1287+                                                       }
1288+                                               }
1289+                                       });
1290+                               }
1291+                       } );
1292+
1293+                       $( document ).on( 'wp-theme-install-failure', function( event, response ) {
1294+                               if ( preview ) {
1295+                                       $( '.wp-full-overlay' ).removeClass( 'customize-loading' );
1296+                               }
1297+                       });
1298+
1299+                       this.installingThemes.push( $( event.target ).data( 'slug' ) ); // Note: we don't remove elements from installingThemes, since they shouldn't be installed again.
1300+                       wp.updates.installTheme( {
1301+                               slug: $( event.target ).data( 'slug' )
1302+                       } );
1303+                       
1304+                       // Also preview the theme as the event is triggered on Install & Preview.
1305+                       if ( $( event.target ).hasClass( 'preview' ) ) {
1306+                               preview = true;
1307+                               $( '.wp-full-overlay' ).addClass( 'customize-loading' );
1308+                               previewUrl = $( event.target ).data( 'previewurl' );
1309+                       }
1310+               },
1311+
1312+               /**
1313+                * Update a theme via wp.updates.
1314+                *
1315+                * @since 4.7.0
1316+                */
1317+               updateTheme: function( event ) {
1318+                       var _this = this;
1319+
1320+                       wp.updates.maybeRequestFilesystemCredentials( event );
1321+
1322+                       $( document ).on( 'wp-theme-update-success', function( event, response ) {
1323+                               // Rerender the control to reflect the update.
1324+                               api.control.each( function( control ) {
1325+                                       if ( 'theme' === control.params.type && control.params.theme.id === response.slug ) {
1326+                                               control.params.hasUpdate = false;
1327+                                               control.rerenderAsInstalled();
1328+                                       }
1329+                               });
1330+                       } );
1331+
1332+                       wp.updates.updateTheme( {
1333+                               slug: $( event.target ).closest( '.notice' ).data( 'slug' )
1334+                       } );
1335+               }
1336+       });
1337+
1338+
1339+       /**
1340         * A Customizer Control.
1341         *
1342         * A control provides a UI element that allows a user to modify a Customizer Setting.
1343@@ -2770,35 +3279,11 @@
1344        api.ThemeControl = api.Control.extend({
1345 
1346                touchDrag: false,
1347-               isRendered: false,
1348+               screenshotRendered: false,
1349 
1350                /**
1351-                * Defer rendering the theme control until the section is displayed.
1352-                *
1353                 * @since 4.2.0
1354                 */
1355-               renderContent: function () {
1356-                       var control = this,
1357-                               renderContentArgs = arguments;
1358-
1359-                       api.section( control.section(), function( section ) {
1360-                               if ( section.expanded() ) {
1361-                                       api.Control.prototype.renderContent.apply( control, renderContentArgs );
1362-                                       control.isRendered = true;
1363-                               } else {
1364-                                       section.expanded.bind( function( expanded ) {
1365-                                               if ( expanded && ! control.isRendered ) {
1366-                                                       api.Control.prototype.renderContent.apply( control, renderContentArgs );
1367-                                                       control.isRendered = true;
1368-                                               }
1369-                                       } );
1370-                               }
1371-                       } );
1372-               },
1373-
1374-               /**
1375-                * @since 4.2.0
1376-                */
1377                ready: function() {
1378                        var control = this;
1379 
1380@@ -2822,20 +3307,6 @@
1381                                        return;
1382                                }
1383 
1384-                               var previewUrl = $( this ).data( 'previewUrl' );
1385-
1386-                               $( '.wp-full-overlay' ).addClass( 'customize-loading' );
1387-
1388-                               window.parent.location = previewUrl;
1389-                       });
1390-
1391-                       control.container.on( 'click keydown', '.theme-actions .theme-details', function( event ) {
1392-                               if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1393-                                       return;
1394-                               }
1395-
1396-                               event.preventDefault(); // Keep this AFTER the key filter above
1397-
1398                                api.section( control.section() ).showDetails( control.params.theme );
1399                        });
1400 
1401@@ -2846,6 +3317,7 @@
1402                                if ( source ) {
1403                                        $screenshot.attr( 'src', source );
1404                                }
1405+                               control.screenshotRendered = true;
1406                        });
1407                },
1408 
1409@@ -2866,6 +3338,18 @@
1410                        } else {
1411                                control.deactivate();
1412                        }
1413+               },
1414+
1415+               /**
1416+                * Rerender the theme from its JS template with the installed type.
1417+                *
1418+                * @since 4.2.0
1419+                */
1420+               rerenderAsInstalled: function() {
1421+                       var control = this;
1422+                       control.params.theme.type = 'installed';
1423+                       control.renderContent(); // replaces existing content
1424+                       control.container.trigger( 'render-screenshot' );
1425                }
1426        });
1427 
1428@@ -3414,7 +3898,9 @@
1429                background:    api.BackgroundControl,
1430                theme:         api.ThemeControl
1431        };
1432-       api.panelConstructor = {};
1433+       api.panelConstructor = {
1434+               themes: api.ThemesPanel
1435+       };
1436        api.sectionConstructor = {
1437                themes: api.ThemesSection
1438        };
1439Index: src/wp-includes/class-wp-customize-manager.php
1440===================================================================
1441--- src/wp-includes/class-wp-customize-manager.php      (revision 38260)
1442+++ src/wp-includes/class-wp-customize-manager.php      (working copy)
1443@@ -230,6 +230,7 @@
1444 
1445                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menus-panel.php' );
1446 
1447+               require_once( ABSPATH . WPINC . '/customize/class-wp-customize-themes-panel.php' );
1448                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-themes-section.php' );
1449                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-sidebar-section.php' );
1450                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-section.php' );
1451@@ -289,6 +290,7 @@
1452 
1453                add_action( 'wp_ajax_customize_save',           array( $this, 'save' ) );
1454                add_action( 'wp_ajax_customize_refresh_nonces', array( $this, 'refresh_nonces' ) );
1455+               add_action( 'wp_ajax_customize-load-themes',    array( $this, 'load_themes_ajax' ) );
1456 
1457                add_action( 'customize_register',                 array( $this, 'register_controls' ) );
1458                add_action( 'customize_register',                 array( $this, 'register_dynamic_settings' ), 11 ); // allow code to create settings first
1459@@ -1634,6 +1636,9 @@
1460                foreach ( $this->controls as $control ) {
1461                        $control->enqueue();
1462                }
1463+               if ( ! is_multisite() && current_user_can( 'install_themes') ) {
1464+                       wp_enqueue_script( 'updates' );
1465+               }
1466        }
1467 
1468        /**
1469@@ -1787,6 +1792,7 @@
1470                $nonces = array(
1471                        'save' => wp_create_nonce( 'save-customize_' . $this->get_stylesheet() ),
1472                        'preview' => wp_create_nonce( 'preview-customize_' . $this->get_stylesheet() ),
1473+                       'customize-themes' => wp_create_nonce( 'customize-themes' ),
1474                );
1475 
1476                /**
1477@@ -1970,8 +1976,10 @@
1478 
1479                /* Panel, Section, and Control Types */
1480                $this->register_panel_type( 'WP_Customize_Panel' );
1481+               $this->register_panel_type( 'WP_Customize_Themes_Panel' );
1482                $this->register_section_type( 'WP_Customize_Section' );
1483                $this->register_section_type( 'WP_Customize_Sidebar_Section' );
1484+               $this->register_section_type( 'WP_Customize_Themes_Section' );
1485                $this->register_control_type( 'WP_Customize_Color_Control' );
1486                $this->register_control_type( 'WP_Customize_Media_Control' );
1487                $this->register_control_type( 'WP_Customize_Upload_Control' );
1488@@ -1981,49 +1989,76 @@
1489                $this->register_control_type( 'WP_Customize_Site_Icon_Control' );
1490                $this->register_control_type( 'WP_Customize_Theme_Control' );
1491 
1492-               /* Themes */
1493+               /* Themes (controls are loaded via ajax) */
1494 
1495-               $this->add_section( new WP_Customize_Themes_Section( $this, 'themes', array(
1496+               $this->add_panel( new WP_Customize_Themes_Panel( $this, 'themes', array(
1497                        'title'      => $this->theme()->display( 'Name' ),
1498+                       'description' => __( 'Once themes are installed, you can live-preview them on your site, customize them, and publish them. Browse available themes in the categories in this menu or upload a theme from a <code>.zip</code> file.' ),
1499                        'capability' => 'switch_themes',
1500                        'priority'   => 0,
1501                ) ) );
1502 
1503-               // Themes Setting (unused - the theme is considerably more fundamental to the Customizer experience).
1504-               $this->add_setting( new WP_Customize_Filter_Setting( $this, 'active_theme', array(
1505+               $this->add_section( new WP_Customize_Themes_Section( $this, 'installed_themes', array(
1506+                       'title'      => __( 'Installed' ),
1507+                       'action'     => 'installed',
1508                        'capability' => 'switch_themes',
1509+                       'panel'      => 'themes',
1510+                       'priority'   => 0,
1511                ) ) );
1512 
1513-               require_once( ABSPATH . 'wp-admin/includes/theme.php' );
1514+               $this->add_section( new WP_Customize_Themes_Section( $this, 'featured_themes', array(
1515+                       'title'      => __( 'Featured' ),
1516+                       'action'     => 'featured',
1517+                       'capability' => 'install_themes',
1518+                       'panel'      => 'themes',
1519+                       'priority'   => 5,
1520+               ) ) );
1521 
1522-               // Theme Controls.
1523+               $this->add_section( new WP_Customize_Themes_Section( $this, 'popular_themes', array(
1524+                       'title'      => __( 'Popular' ),
1525+                       'action'     => 'popular',
1526+                       'capability' => 'install_themes',
1527+                       'panel'      => 'themes',
1528+                       'priority'   => 10,
1529+               ) ) );
1530 
1531-               // Add a control for the active/original theme.
1532-               if ( ! $this->is_theme_active() ) {
1533-                       $themes = wp_prepare_themes_for_js( array( wp_get_theme( $this->original_stylesheet ) ) );
1534-                       $active_theme = current( $themes );
1535-                       $active_theme['isActiveTheme'] = true;
1536-                       $this->add_control( new WP_Customize_Theme_Control( $this, $active_theme['id'], array(
1537-                               'theme'    => $active_theme,
1538-                               'section'  => 'themes',
1539-                               'settings' => 'active_theme',
1540-                       ) ) );
1541-               }
1542+               $this->add_section( new WP_Customize_Themes_Section( $this, 'latest_themes', array(
1543+                       'title'      => __( 'Latest' ),
1544+                       'action'     => 'latest',
1545+                       'capability' => 'install_themes',
1546+                       'panel'      => 'themes',
1547+                       'priority'   => 15,
1548+               ) ) );
1549 
1550-               $themes = wp_prepare_themes_for_js();
1551-               foreach ( $themes as $theme ) {
1552-                       if ( $theme['active'] || $theme['id'] === $this->original_stylesheet ) {
1553-                               continue;
1554-                       }
1555+               $this->add_section( new WP_Customize_Themes_Section( $this, 'favorites_themes', array(
1556+                       'title'      => __( 'Favorites' ),
1557+                       'action'     => 'favorites',
1558+                       'capability' => 'install_themes',
1559+                       'panel'      => 'themes',
1560+                       'priority'   => 20,
1561+               ) ) );
1562+               
1563+               $this->add_section( new WP_Customize_Themes_Section( $this, 'feature_filter_themes', array(
1564+                       'title'      => __( 'Feature Filter' ),
1565+                       'action'     => 'feature_filter',
1566+                       'capability' => 'install_themes',
1567+                       'panel'      => 'themes',
1568+                       'priority'   => 25,
1569+               ) ) );
1570+               
1571+               $this->add_section( new WP_Customize_Themes_Section( $this, 'search_themes', array(
1572+                       'title'      => __( 'Search themes &hellip;' ),
1573+                       'action'     => 'search',
1574+                       'capability' => 'install_themes',
1575+                       'panel'      => 'themes',
1576+                       'priority'   => 30,
1577+               ) ) );
1578+               
1579+               // Themes Setting (unused - the theme is considerably more fundamental to the Customizer experience).
1580+               $this->add_setting( new WP_Customize_Filter_Setting( $this, 'active_theme', array(
1581+                       'capability' => 'switch_themes',
1582+               ) ) );
1583 
1584-                       $theme_id = 'theme_' . $theme['id'];
1585-                       $theme['isActiveTheme'] = false;
1586-                       $this->add_control( new WP_Customize_Theme_Control( $this, $theme_id, array(
1587-                               'theme'    => $theme,
1588-                               'section'  => 'themes',
1589-                               'settings' => 'active_theme',
1590-                       ) ) );
1591-               }
1592 
1593                /* Site Identity */
1594 
1595@@ -2330,6 +2365,141 @@
1596        }
1597 
1598        /**
1599+        * Load themes into the theme browsing/installation UI.
1600+        *
1601+        * @since 4.7.0
1602+        * @access public
1603+        */
1604+       public function load_themes_ajax() {
1605+               check_ajax_referer( 'customize-themes', 'customize-themes-nonce' );
1606+
1607+               if ( ! current_user_can( 'switch_themes' ) ) {
1608+                       wp_die( -1 );
1609+               }
1610+
1611+               if ( empty( $_POST['theme_action'] ) ) {
1612+                       wp_send_json_error( 'missing_theme_action' );
1613+               }
1614+
1615+               if ( 'search' === $_POST['theme_action'] && ! array_key_exists( 'search', $_POST ) ) {
1616+                       wp_send_json_error( 'empty_search' );
1617+               } elseif ( 'favorites' === $_POST['theme_action'] && ! array_key_exists( 'user', $_POST ) ) {
1618+                       wp_send_json_error( 'empty_user' );
1619+               } elseif ( 'feature_filter' === $_POST['theme_action'] && ! array_key_exists( 'tags', $_POST ) ) {
1620+                       wp_send_json_error( 'no_features' );
1621+               }
1622+
1623+               require_once( ABSPATH . 'wp-admin/includes/theme.php' );
1624+               if ( 'installed' === $_POST['theme_action'] ) {
1625+                       $themes = array( 'themes' => wp_prepare_themes_for_js() );
1626+                       foreach ( $themes['themes'] as &$theme ) {
1627+                               $theme['type'] = 'installed';
1628+                               // Set active based on customized theme.
1629+                               if ( $_POST['customized_theme'] === $theme['id'] ) {
1630+                                       $theme['active'] = true;
1631+                               } else {
1632+                                       $theme['active'] = false;
1633+                               }
1634+                       }
1635+               } else {
1636+                       if ( ! current_user_can( 'install_themes' ) ) {
1637+                               wp_die( -1 );
1638+                       }
1639+
1640+                       // Arguments for all queries.
1641+                       $args = array(
1642+                               'per_page' => 100,
1643+                               'page' => absint( $_POST['page'] ),
1644+                               'fields' => array(
1645+                                       'slug' => true,
1646+                                       'screenshot' => true,
1647+                                       'description' => true,
1648+                                       'requires' => true,
1649+                                       'rating' => true,
1650+                                       'downloaded' => true,
1651+                                       'downloadLink' => true,
1652+                                       'last_updated' => true,
1653+                                       'homepage' => true,
1654+                                       'num_ratings' => true,
1655+                                       'tags' => true,
1656+                               )
1657+                       );
1658+
1659+                       // Specialized handling for each query.
1660+                       switch ( $_POST['theme_action'] ) {
1661+                               case 'search':
1662+                                       $args['search'] = wp_unslash( $_POST['search'] );
1663+                                       break;
1664+                               case 'favorites':
1665+                                       $args['user'] = wp_unslash(  $_POST['user'] );
1666+                               case 'featured':
1667+                               case 'popular':
1668+                                       $args['browse'] = $_POST['theme_action'];
1669+                                       break;
1670+                               case 'latest':
1671+                                       $args['browse'] = 'new';
1672+                                       break;
1673+                               case 'feature_filter':
1674+                                       $args['tag'] = wp_unslash( $_POST['tags'] );
1675+                                       break;
1676+                       }
1677+
1678+                       // Load themes from the .org API.
1679+                       $themes = themes_api( 'query_themes', $args );
1680+                       if ( is_wp_error( $themes ) ) {
1681+                               wp_send_json_error();
1682+                       }
1683+
1684+                       // Prepare a list of installed themes to check against before the loop.
1685+                       $installed_themes = array();
1686+                       $wp_themes = wp_get_themes();
1687+                       foreach ( $wp_themes as $theme ) {
1688+                               $installed_themes[] = $theme->get_stylesheet();
1689+                       }
1690+                       $update_php = network_admin_url( 'update.php?action=install-theme' );
1691+                       foreach ( $themes->themes as &$theme ) {
1692+                               $theme->install_url = add_query_arg( array(
1693+                                       'theme'    => $theme->slug,
1694+                                       '_wpnonce' => wp_create_nonce( 'install-theme_' . $theme->slug )
1695+                               ), $update_php );
1696+
1697+                               $theme->name        = wp_kses( $theme->name, $themes_allowedtags );
1698+                               $theme->author      = wp_kses( $theme->author, $themes_allowedtags );
1699+                               $theme->version     = wp_kses( $theme->version, $themes_allowedtags );
1700+                               $theme->description = wp_kses( $theme->description, $themes_allowedtags );
1701+                               $theme->tags        = implode( ', ', $theme->tags );
1702+                               $theme->stars       = wp_star_rating( array( 'rating' => $theme->rating, 'type' => 'percent', 'number' => $theme->num_ratings, 'echo' => false ) );
1703+                               $theme->num_ratings = number_format_i18n( $theme->num_ratings );
1704+                               $theme->preview_url = set_url_scheme( $theme->preview_url );
1705+
1706+                               // Handle themes that are already installed as installed themes.
1707+                               if ( in_array( $theme->slug, $installed_themes ) ) {
1708+                                       $theme->type = 'installed';
1709+                               } else {
1710+                                       $theme->type = $_POST['theme_action'];
1711+                               }
1712+
1713+                               // Set active based on customized theme.
1714+                               if ( $_POST['customized_theme'] === $theme->slug ) {
1715+                                       $theme->active = true;
1716+                               } else {
1717+                                       $theme->active = false;
1718+                               }
1719+
1720+                               // Map available theme properties to installed theme properties.
1721+                               $theme->id           = $theme->slug;
1722+                               $theme->screenshot   = array( $theme->screenshot_url );
1723+                               $theme->authorAndUri = $theme->author;
1724+                               unset( $theme->slug );
1725+                               unset( $theme->screenshot_url );
1726+                               unset( $theme->author );
1727+                       }
1728+               }
1729+               wp_send_json_success( $themes );
1730+       }
1731+
1732+
1733+       /**
1734         * Callback for validating the header_textcolor value.
1735         *
1736         * Accepts 'blank', and otherwise uses sanitize_hex_color_no_hash().
1737Index: src/wp-includes/customize/class-wp-customize-theme-control.php
1738===================================================================
1739--- src/wp-includes/customize/class-wp-customize-theme-control.php      (revision 38260)
1740+++ src/wp-includes/customize/class-wp-customize-theme-control.php      (working copy)
1741@@ -67,13 +67,13 @@
1742                $preview_url = esc_url( add_query_arg( 'theme', '__THEME__', $current_url ) ); // Token because esc_url() strips curly braces.
1743                $preview_url = str_replace( '__THEME__', '{{ data.theme.id }}', $preview_url );
1744                ?>
1745-               <# if ( data.theme.isActiveTheme ) { #>
1746-                       <div class="theme active" tabindex="0" data-preview-url="<?php echo esc_attr( $active_url ); ?>" aria-describedby="{{ data.theme.id }}-action {{ data.theme.id }}-name">
1747+               <# if ( data.theme.active ) { #>
1748+                       <div class="theme active" tabindex="0" aria-describedby="{{ data.theme.id }}-action {{ data.theme.id }}-name">
1749                <# } else { #>
1750-                       <div class="theme" tabindex="0" data-preview-url="<?php echo esc_attr( $preview_url ); ?>" aria-describedby="{{ data.theme.id }}-action {{ data.theme.id }}-name">
1751+                       <div class="theme" tabindex="0" aria-describedby="{{ data.theme.id }}-action {{ data.theme.id }}-name">
1752                <# } #>
1753 
1754-                       <# if ( data.theme.screenshot[0] ) { #>
1755+                       <# if ( data.theme.screenshot && data.theme.screenshot[0] ) { #>
1756                                <div class="theme-screenshot">
1757                                        <img data-src="{{ data.theme.screenshot[0] }}" alt="" />
1758                                </div>
1759@@ -81,27 +81,35 @@
1760                                <div class="theme-screenshot blank"></div>
1761                        <# } #>
1762 
1763-                       <# if ( data.theme.isActiveTheme ) { #>
1764-                               <span class="more-details" id="{{ data.theme.id }}-action"><?php _e( 'Customize' ); ?></span>
1765-                       <# } else { #>
1766-                               <span class="more-details" id="{{ data.theme.id }}-action"><?php _e( 'Live Preview' ); ?></span>
1767+                       <span class="more-details theme-details" id="{{ data.theme.id }}-action"><?php _e( 'Theme Details' ); ?></span>
1768+
1769+
1770+                       <# if ( false == false && 'installed' === data.theme.type && data.theme.hasUpdate ) { // @todo patch updates.js to update themes in the customizer. #>
1771+                               <div class="update-message notice inline notice-warning notice-alt" data-slug="{{ data.theme.id }}"><p><?php _e( 'New version available. <button class="button-link update-theme" type="button">Update now</button>' ); ?></p></div>
1772                        <# } #>
1773 
1774-                       <div class="theme-author"><?php printf( __( 'By %s' ), '{{ data.theme.author }}' ); ?></div>
1775-
1776-                       <# if ( data.theme.isActiveTheme ) { #>
1777+                       <# if ( data.theme.active ) { #>
1778                                <h3 class="theme-name" id="{{ data.theme.id }}-name">
1779                                        <?php
1780                                        /* translators: %s: theme name */
1781-                                       printf( __( '<span>Active:</span> %s' ), '{{{ data.theme.name }}}' );
1782+                                       printf( __( '<span>Current:</span> %s' ), '{{ data.theme.name }}' );
1783                                        ?>
1784                                </h3>
1785+                               <div class="theme-actions">
1786+                                       <button type="button" class="button button-primary customize-theme"><?php _e( 'Customize' ); ?></span>
1787+                       <# } else if ( 'installed' === data.theme.type ) { #>
1788+                               <h3 class="theme-name" id="{{ data.theme.id }}-name">{{ data.theme.name }}</h3>
1789+                               <div class="theme-actions">
1790+                                       <button type="button" class="button button-primary preview-theme" data-preview-url="<?php echo esc_attr( $preview_url ); ?>"><?php _e( 'Live Preview' ); ?></span>
1791+                               </div>
1792+                               <div class="notice notice-success notice-alt"><p><?php _e( 'Installed' ); ?></p></div>
1793                        <# } else { #>
1794-                               <h3 class="theme-name" id="{{ data.theme.id }}-name">{{{ data.theme.name }}}</h3>
1795+                               <h3 class="theme-name" id="{{ data.theme.id }}-name">{{ data.theme.name }}</h3>
1796                                <div class="theme-actions">
1797-                                       <button type="button" class="button theme-details"><?php _e( 'Theme Details' ); ?></button>
1798+                                       <button type="button" class="button theme-install" data-slug="{{ data.theme.id }}"><?php _e( 'Install' ); ?></button>
1799+                                       <button type="button" class="button button-primary theme-install preview" data-slug="{{ data.theme.id }}" data-previewurl="<?php echo esc_attr( $preview_url ); ?>"><?php _e( 'Install & Preview' ); ?></button>
1800                                </div>
1801-                       <# } #>
1802+                       <# } #>                 
1803                </div>
1804        <?php
1805        }
1806Index: src/wp-includes/customize/class-wp-customize-themes-panel.php
1807===================================================================
1808--- src/wp-includes/customize/class-wp-customize-themes-panel.php       (revision 0)
1809+++ src/wp-includes/customize/class-wp-customize-themes-panel.php       (working copy)
1810@@ -0,0 +1,119 @@
1811+<?php
1812+/**
1813+ * Customize API: WP_Customize_Themes_Panel class
1814+ *
1815+ * @package WordPress
1816+ * @subpackage Customize
1817+ * @since 4.7.0
1818+ */
1819+
1820+/**
1821+ * Customize Themes Panel Class
1822+ *
1823+ * @since 4.7.0
1824+ *
1825+ * @see WP_Customize_Panel
1826+ */
1827+class WP_Customize_Themes_Panel extends WP_Customize_Panel {
1828+
1829+       /**
1830+        * Panel type.
1831+        *
1832+        * @since 4.7.0
1833+        * @access public
1834+        * @var string
1835+        */
1836+       public $type = 'themes';
1837+
1838+       /**
1839+        * An Underscore (JS) template for rendering this panel's container.
1840+        *
1841+        * The themes panel renders a custom panel heading with the current theme and a switch themes button.
1842+        *
1843+        * @see WP_Customize_Panel::print_template()
1844+        *
1845+        * @since 4.7.0
1846+        * @access protected
1847+        */
1848+       protected function render_template() {
1849+               ?>
1850+               <li id="accordion-section-{{ data.id }}" class="accordion-section control-panel-themes">
1851+                       <h3 class="accordion-section-title">
1852+                               <?php
1853+                               if ( $this->manager->is_theme_active() ) {
1854+                                       echo '<span class="customize-action">' . __( 'Active theme' ) . '</span> {{ data.title }}';
1855+                               } else {
1856+                                       echo '<span class="customize-action">' . __( 'Previewing theme' ) . '</span> {{ data.title }}';
1857+                               }
1858+                               ?>
1859+
1860+                               <?php
1861+                               if ( current_user_can( 'switch_themes' ) ) : ?>
1862+                                       <button type="button" class="button change-theme"><?php _ex( 'Change', 'theme' ); ?></button>
1863+                               <?php endif; ?>
1864+                       </h3>
1865+                       <ul class="accordion-sub-container control-panel-content"></ul>
1866+               </li>
1867+               <?php
1868+       }
1869+
1870+       /**
1871+        * An Underscore (JS) template for this panel's content (but not its container).
1872+        *
1873+        * Class variables for this panel class are available in the `data` JS object;
1874+        * export custom variables by overriding WP_Customize_Panel::json().
1875+        *
1876+        * @since 4.7.0
1877+        * @access protected
1878+        *
1879+        * @see WP_Customize_Panel::print_template()
1880+        */
1881+       protected function content_template() {
1882+               ?>
1883+               <li class="panel-meta accordion-section customize-info">
1884+                       <div class="current-theme">
1885+                               <span class="preview-notice">
1886+                                       <?php
1887+                                       /* translators: %s: the site/panel title in the Customizer */
1888+                                       printf( __( 'Current Theme %s' ), '<strong class="panel-title">{{ data.title }}</strong>' );
1889+                                       ?>
1890+                               </span>
1891+                               <button type="button" class="button customize-theme" tabindex="-1"><?php _e( 'Customize' ); ?></button>
1892+                       </div>
1893+                       <h2><?php _e( 'Themes' ); ?></h2>
1894+                       <?php if ( current_user_can( 'install_themes' ) ) : ?>
1895+                               <button type="button" class="upload-toggle page-title-action" aria-expanded="false"><?php _e( 'Upload Theme' ); ?></button>
1896+                       <?php endif; ?>
1897+               </li>
1898+               <li class="upload-theme">
1899+                       <?php if ( current_user_can( 'install_themes' ) ) : ?>
1900+                               <p class="install-help"><?php _e( 'If you have a theme in a <code>.zip</code> format, you may install it by uploading it here.' ); ?></p>
1901+                               <div class="wp-upload-form">
1902+                                       <input type="file" id="themezip" name="themezip"><label class="screen-reader-text" for="themezip"><?php _e( 'Theme zip file' ); ?></label>
1903+                                       <button type="button" id="upload-theme-submit" class="button"><?php _e( 'Install Now' ); ?></button>
1904+                               </div>
1905+                       <?php endif; ?>
1906+                       </div>
1907+               </li>
1908+               <li id="customize-container"></li><?php // Used as a full-screen overlay transition after clicking to preview a theme. ?>
1909+               <li class="customize-themes-notifications">
1910+                       <div class="notice notice-warning customize-themes-unsaved-changes" style="display: none;"><p>
1911+                               <?php _e( 'You have unsaved changes that will be lost if you preview a new theme.' ); ?>
1912+                               <button type="button" id="customize-themes-save" class="button" ><?php _e( 'Save & Publish' ); ?></button>
1913+                       </p></div>
1914+               </li>
1915+               <li id="customize-theme-section-navigation" class="wp-filter">
1916+                       <div class="filter-count"><span class="count theme-count">0</span></div>
1917+                       <div class="filter-links"></div>
1918+                       <# if ( data.description ) { #>
1919+                               <button class="customize-help-toggle dashicons dashicons-editor-help" type="button" aria-expanded="false"><span class="screen-reader-text"><?php _e( 'Help' ); ?></span></button>
1920+                               <div class="description customize-panel-description">
1921+                                       {{{ data.description }}}
1922+                               </div>
1923+                       <# } #>
1924+               </li>
1925+       <?php
1926+       }
1927+}
1928Index: src/wp-includes/customize/class-wp-customize-themes-section.php
1929===================================================================
1930--- src/wp-includes/customize/class-wp-customize-themes-section.php     (revision 38260)
1931+++ src/wp-includes/customize/class-wp-customize-themes-section.php     (working copy)
1932@@ -10,7 +10,7 @@
1933 /**
1934  * Customize Themes Section class.
1935  *
1936- * A UI container for theme controls, which behaves like a backwards Panel.
1937+ * A UI container for theme controls, which are displayed in tabbed sections.
1938  *
1939  * @since 4.2.0
1940  *
1941@@ -28,57 +28,96 @@
1942        public $type = 'themes';
1943 
1944        /**
1945-        * Render the themes section, which behaves like a panel.
1946+        * Theme section action.
1947         *
1948-        * @since 4.2.0
1949+        * Defines the type of themes to load (installed, featured, latest, etc.).
1950+        *
1951+        * @since 4.7.0
1952+        * @access public
1953+        * @var string
1954+        */
1955+       public $action = '';
1956+
1957+       /**
1958+        * Get section parameters for JS.
1959+        *
1960+        * @since 4.7.0
1961+        * @access public
1962+        * @return array Exported parameters.
1963+        */
1964+       public function json() {
1965+               $exported = parent::json();
1966+               $exported['action'] = $this->action;
1967+
1968+               return $exported;
1969+       }
1970+
1971+       /**
1972+        * Render a themes section as a JS template.
1973+        *
1974+        * The template is only rendered by PHP once, so all actions are prepared at once on the server side.
1975+        *
1976+        * @since 4.7.0
1977         * @access protected
1978         */
1979-       protected function render() {
1980-               $classes = 'accordion-section control-section control-section-' . $this->type;
1981+       protected function render_template() {
1982                ?>
1983-               <li id="accordion-section-<?php echo esc_attr( $this->id ); ?>" class="<?php echo esc_attr( $classes ); ?>">
1984-                       <h3 class="accordion-section-title">
1985-                               <?php
1986-                               if ( $this->manager->is_theme_active() ) {
1987-                                       echo '<span class="customize-action">' . __( 'Active theme' ) . '</span> ' . $this->title;
1988-                               } else {
1989-                                       echo '<span class="customize-action">' . __( 'Previewing theme' ) . '</span> ' . $this->title;
1990-                               }
1991-                               ?>
1992-
1993-                               <?php if ( count( $this->controls ) > 0 ) : ?>
1994-                                       <button type="button" class="button change-theme" tabindex="0"><?php _ex( 'Change', 'theme' ); ?></button>
1995-                               <?php endif; ?>
1996-                       </h3>
1997-                       <div class="customize-themes-panel control-panel-content themes-php">
1998-                               <h3 class="accordion-section-title customize-section-title">
1999-                                       <span class="customize-action"><?php _e( 'Customizing' ); ?></span>
2000-                                       <?php _e( 'Themes' ); ?>
2001-                                       <span class="title-count theme-count"><?php echo count( $this->controls ) + 1 /* Active theme */; ?></span>
2002-                               </h3>
2003-                               <h3 class="accordion-section-title customize-section-title">
2004+               <li id="accordion-section-{{ data.id }}" class="theme-section">
2005+                       <# if ( 'search' === data.action ) { #>
2006+                               <div class="search-form customize-themes-section-title themes-section-search_themes">
2007+                                       <label class="screen-reader-text" for="wp-filter-search-input">{{ data.title }}</label>
2008+                                       <input placeholder="{{ data.title }}" type="search" aria-describedby="live-search-desc" id="wp-filter-search-input" class="wp-filter-search">
2009+                                       <span id="live-search-desc" class="screen-reader-text"><?php _e( 'The search results will be updated as you type.' ); ?></span>
2010+                               </div>
2011+                       <# } else { #>
2012+                               <button type="button" class="customize-themes-section-title themes-section-{{ data.id }}">{{ data.title }}</button>
2013+                       <# } #>
2014+                       <div class="customize-themes-section control-section-content themes-php">
2015+                               <# if ( 'favorites' === data.action ) { #>
2016+                                       <div class="favorites-form">
2017+                                               <p class="install-help"><?php _e( 'If you have marked themes as favorites on WordPress.org, you can browse them here.' ); ?></p>
2018+                                               <p>
2019+                                                       <label for="wporg-username-input"><?php _e( 'Your WordPress.org username:' ); ?></label>
2020+                                                       <input type="search" id="wporg-username-input" value="">
2021+                                                       <button type="button" class="button button-secondary favorites-form-submit"><?php _e( 'Get Favorites' ); ?></button>
2022+                                               </p>
2023+                                       </div>
2024+                               <# } else if ( 'feature_filter' === data.action ) { #>
2025+                                       <div class="filter-drawer">
2026+                                               <div class="buttons">
2027+                                                       <button type="button" class="apply-filters button button-secondary"><?php _e( 'Apply Filters' ); ?><span></span></a>
2028+                                                       <button type="button" class="clear-filters button button-secondary"><?php _e( 'Clear' ); ?></a>
2029+                                               </div>
2030                                        <?php
2031-                                       if ( $this->manager->is_theme_active() ) {
2032-                                               echo '<span class="customize-action">' . __( 'Active theme' ) . '</span> ' . $this->title;
2033-                                       } else {
2034-                                               echo '<span class="customize-action">' . __( 'Previewing theme' ) . '</span> ' . $this->title;
2035+                                       $feature_list = get_theme_feature_list();
2036+                                       foreach ( $feature_list as $feature_name => $features ) {
2037+                                               echo '<fieldset class="filter-group">';
2038+                                               $feature_name = esc_html( $feature_name );
2039+                                               echo '<legend>' . $feature_name . '</legend>';
2040+                                               echo '<div class="filter-group-feature">';
2041+                                               foreach ( $features as $feature => $feature_name ) {
2042+                                                       $feature = esc_attr( $feature );
2043+                                                       echo '<input type="checkbox" id="filter-id-' . $feature . '" value="' . $feature . '" /> ';
2044+                                                       echo '<label for="filter-id-' . $feature . '">' . $feature_name . '</label><br>';
2045+                                               }
2046+                                               echo '</div>';
2047+                                               echo '</fieldset>';
2048                                        }
2049                                        ?>
2050-                                       <button type="button" class="button customize-theme"><?php _e( 'Customize' ); ?></button>
2051-                               </h3>
2052-
2053+                                               <div class="filtered-by">
2054+                                                       <span><?php _e( 'Filtering by:' ); ?></span>
2055+                                                       <div class="tags"></div>
2056+                                                       <button type="button" class="button-link"><?php _e( 'Edit' ); ?></a>
2057+                                               </div>
2058+                                       </div>
2059+                               <# } #>
2060                                <div class="theme-overlay" tabindex="0" role="dialog" aria-label="<?php esc_attr_e( 'Theme Details' ); ?>"></div>
2061-
2062-                               <div id="customize-container"></div>
2063-                               <?php if ( count( $this->controls ) > 4 ) : ?>
2064-                                       <p><label for="themes-filter">
2065-                                               <span class="screen-reader-text"><?php _e( 'Search installed themes&hellip;' ); ?></span>
2066-                                               <input type="text" id="themes-filter" placeholder="<?php esc_attr_e( 'Search installed themes&hellip;' ); ?>" />
2067-                                       </label></p>
2068-                               <?php endif; ?>
2069                                <div class="theme-browser rendered">
2070-                                       <ul class="themes accordion-section-content">
2071+                                       <div class="error unexpected-error" style="display: none; "><p><?php _e( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server&#8217;s configuration. If you continue to have problems, please try the <a href="https://wordpress.org/support/">support forums</a>.' ); ?></p></div>
2072+                                       <ul class="themes">
2073                                        </ul>
2074+                                       <p class="no-themes"><?php _e( 'No themes found. Try a different search.' ); ?></p>
2075+                                       <p class="spinner"></p>
2076                                </div>
2077                        </div>
2078                </li>