diff --git a/app/assets/javascripts/discourse/app/components/sidebar.js b/app/assets/javascripts/discourse/app/components/sidebar.js
new file mode 100644
index 00000000000..ea096319112
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/sidebar.js
@@ -0,0 +1,3 @@
+import GlimmerComponent from "discourse/components/glimmer";
+
+export default class Sidebar extends GlimmerComponent {}
diff --git a/app/assets/javascripts/discourse/app/controllers/application.js b/app/assets/javascripts/discourse/app/controllers/application.js
index b494f56e54d..c07ca32adc9 100644
--- a/app/assets/javascripts/discourse/app/controllers/application.js
+++ b/app/assets/javascripts/discourse/app/controllers/application.js
@@ -6,6 +6,12 @@ export default Controller.extend({
showTop: true,
showFooter: false,
router: service(),
+ showSidebar: true,
+
+ @discourseComputed("showSidebar", "currentUser.experimental_sidebar_enabled")
+ mainOutletWrapperClasses(showSidebar, experimentalSidebarEnabled) {
+ return showSidebar && experimentalSidebarEnabled ? "has-sidebar" : "";
+ },
@discourseComputed
canSignUp() {
diff --git a/app/assets/javascripts/discourse/app/routes/application.js b/app/assets/javascripts/discourse/app/routes/application.js
index dbffdf2de99..ea6da0a5c76 100644
--- a/app/assets/javascripts/discourse/app/routes/application.js
+++ b/app/assets/javascripts/discourse/app/routes/application.js
@@ -37,6 +37,10 @@ const ApplicationRoute = DiscourseRoute.extend(OpenComposer, {
});
},
+ toggleSidebar() {
+ this.controllerFor("application").toggleProperty("showSidebar");
+ },
+
toggleMobileView() {
mobile.toggleMobileView();
},
diff --git a/app/assets/javascripts/discourse/app/templates/application.hbs b/app/assets/javascripts/discourse/app/templates/application.hbs
index 6c0944f9059..7e16b0f9106 100644
--- a/app/assets/javascripts/discourse/app/templates/application.hbs
+++ b/app/assets/javascripts/discourse/app/templates/application.hbs
@@ -8,26 +8,34 @@
showKeyboard=(route-action "showKeyboardShortcutsHelp")
toggleMobileView=(route-action "toggleMobileView")
toggleAnonymous=(route-action "toggleAnonymous")
- logout=(route-action "logout")}}
+ logout=(route-action "logout")
+ toggleSidebar=(route-action "toggleSidebar")
+ }}
{{software-update-prompt}}
{{plugin-outlet name="below-site-header" connectorTagName="div" args=(hash currentPath=router._router.currentPath)}}
-
- {{plugin-outlet name="above-main-container" connectorTagName="div"}}
-
- {{#if showTop}}
- {{custom-html name="top"}}
- {{/if}}
- {{notification-consent-banner}}
- {{pwa-install-banner}}
- {{global-notice}}
- {{create-topics-notice}}
- {{plugin-outlet name="top-notices" connectorTagName="div" args=(hash currentPath=router._router.currentPath)}}
-
+
+ {{#if currentUser.experimental_sidebar_enabled}}
+
+ {{/if}}
- {{outlet}}
- {{outlet "user-card"}}
+
+ {{plugin-outlet name="above-main-container" connectorTagName="div"}}
+
+ {{#if showTop}}
+ {{custom-html name="top"}}
+ {{/if}}
+ {{notification-consent-banner}}
+ {{pwa-install-banner}}
+ {{global-notice}}
+ {{create-topics-notice}}
+ {{plugin-outlet name="top-notices" connectorTagName="div" args=(hash currentPath=router._router.currentPath)}}
+
+
+ {{outlet}}
+ {{outlet "user-card"}}
+
{{plugin-outlet name="above-footer" connectorTagName="div" args=(hash showFooter=showFooter)}}
diff --git a/app/assets/javascripts/discourse/app/templates/components/sidebar.hbs b/app/assets/javascripts/discourse/app/templates/components/sidebar.hbs
new file mode 100644
index 00000000000..91657f9f4c6
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/templates/components/sidebar.hbs
@@ -0,0 +1,6 @@
+{{#if @shouldDisplay}}
+
+{{/if}}
diff --git a/app/assets/javascripts/discourse/app/widgets/header-contents.js b/app/assets/javascripts/discourse/app/widgets/header-contents.js
index 5ce69e3b2d5..7f13995929f 100644
--- a/app/assets/javascripts/discourse/app/widgets/header-contents.js
+++ b/app/assets/javascripts/discourse/app/widgets/header-contents.js
@@ -4,6 +4,9 @@ import hbs from "discourse/widgets/hbs-compiler";
createWidget("header-contents", {
tagName: "div.contents.clearfix",
template: hbs`
+ {{#if attrs.sidebarEnabled}}
+ {{sidebar-toggle attrs=attrs}}
+ {{/if}}
{{home-logo attrs=attrs}}
{{#if attrs.topic}}
{{header-topic-info attrs=attrs}}
diff --git a/app/assets/javascripts/discourse/app/widgets/header.js b/app/assets/javascripts/discourse/app/widgets/header.js
index 4885bbf1981..9d289ae005d 100644
--- a/app/assets/javascripts/discourse/app/widgets/header.js
+++ b/app/assets/javascripts/discourse/app/widgets/header.js
@@ -400,7 +400,12 @@ export default createWidget("header", {
return panels;
};
- let contentsAttrs = { contents, minimized: !!attrs.topic };
+ let contentsAttrs = {
+ contents,
+ minimized: !!attrs.topic,
+ sidebarEnabled: this.currentUser?.experimental_sidebar_enabled,
+ };
+
return h(
"div.wrap",
this.attach("header-contents", Object.assign({}, attrs, contentsAttrs))
diff --git a/app/assets/javascripts/discourse/app/widgets/sidebar-toggle.js b/app/assets/javascripts/discourse/app/widgets/sidebar-toggle.js
new file mode 100644
index 00000000000..b39163c0f32
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/widgets/sidebar-toggle.js
@@ -0,0 +1,16 @@
+import { createWidget } from "discourse/widgets/widget";
+
+export default createWidget("sidebar-toggle", {
+ tagName: "span.header-sidebar-toggle",
+
+ html() {
+ return [
+ this.attach("button", {
+ title: "",
+ icon: "bars",
+ action: "toggleSidebar",
+ className: "btn btn-flat",
+ }),
+ ];
+ },
+});
diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-test.js
new file mode 100644
index 00000000000..c173b7e4d17
--- /dev/null
+++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-test.js
@@ -0,0 +1,65 @@
+import { click, visit } from "@ember/test-helpers";
+import {
+ acceptance,
+ conditionalTest,
+ exists,
+} from "discourse/tests/helpers/qunit-helpers";
+import { test } from "qunit";
+import { isLegacyEmber } from "discourse-common/config/environment";
+
+acceptance("Sidebar - Anon User", function () {
+ // Don't show sidebar for anon user until we know what we want to display
+ test("sidebar is not displayed", async function (assert) {
+ await visit("/");
+
+ assert.ok(!exists("#main-outlet-wrapper.has-sidebar"));
+ assert.ok(!exists(".sidebar-wrapper"));
+ });
+});
+
+acceptance("Sidebar - User with sidebar disabled", function (needs) {
+ needs.user({ experimental_sidebar_enabled: false });
+
+ conditionalTest(
+ "sidebar is not displayed",
+ !isLegacyEmber(),
+ async function (assert) {
+ await visit("/");
+
+ assert.ok(!exists("#main-outlet-wrapper.has-sidebar"));
+ assert.ok(!exists(".sidebar-wrapper"));
+ }
+ );
+});
+
+acceptance("Sidebar - User with sidebar enabled", function (needs) {
+ needs.user({ experimental_sidebar_enabled: true });
+
+ conditionalTest(
+ "hiding and displaying sidebar",
+ !isLegacyEmber(),
+ async function (assert) {
+ await visit("/");
+
+ assert.ok(
+ exists("#main-outlet-wrapper.has-sidebar"),
+ "adds sidebar utility class on main outlet wrapper"
+ );
+
+ assert.ok(exists(".sidebar-wrapper"), "displays the sidebar by default");
+
+ await click(".header-sidebar-toggle .btn");
+
+ assert.ok(
+ !exists("#main-outlet-wrapper.has-sidebar"),
+ "removes sidebar utility class from main outlet wrapper"
+ );
+
+ assert.ok(!exists(".sidebar-wrapper"), "hides the sidebar");
+
+ await click(".header-sidebar-toggle .btn");
+
+ assert.ok(exists(".sidebar-wrapper"), "displays the sidebar");
+ }
+ );
+});
diff --git a/app/assets/stylesheets/common/base/_index.scss b/app/assets/stylesheets/common/base/_index.scss
index f6b8e49d0a0..6741fdc6bbe 100644
--- a/app/assets/stylesheets/common/base/_index.scss
+++ b/app/assets/stylesheets/common/base/_index.scss
@@ -44,6 +44,7 @@
@import "search";
@import "share_link";
@import "shared-drafts";
+@import "sidebar";
@import "tagging";
@import "tooltip";
@import "topic-admin-menu";
diff --git a/app/assets/stylesheets/common/base/discourse.scss b/app/assets/stylesheets/common/base/discourse.scss
index 0126ae6cc55..b9cdd3cfb34 100644
--- a/app/assets/stylesheets/common/base/discourse.scss
+++ b/app/assets/stylesheets/common/base/discourse.scss
@@ -651,6 +651,7 @@ table {
#main-outlet {
padding-top: 2.5em;
+ grid-area: content;
}
#main {
diff --git a/app/assets/stylesheets/common/base/header.scss b/app/assets/stylesheets/common/base/header.scss
index 07cffbaf119..0cc0a38922e 100644
--- a/app/assets/stylesheets/common/base/header.scss
+++ b/app/assets/stylesheets/common/base/header.scss
@@ -14,7 +14,6 @@
backface-visibility: hidden; /** do magic for scrolling performance **/
> .wrap {
- box-sizing: border-box;
width: 100%;
height: 100%;
diff --git a/app/assets/stylesheets/common/base/sidebar.scss b/app/assets/stylesheets/common/base/sidebar.scss
new file mode 100644
index 00000000000..d1c87b6a639
--- /dev/null
+++ b/app/assets/stylesheets/common/base/sidebar.scss
@@ -0,0 +1,40 @@
+.header-sidebar-toggle {
+ margin-right: 1em;
+ margin-left: -10px;
+
+ button {
+ position: relative;
+ font-size: var(--font-up-2);
+
+ .discourse-no-touch & {
+ &:hover {
+ background: var(--primary-low);
+ .d-icon {
+ color: var(--primary-medium);
+ }
+ }
+ }
+ }
+}
+
+.sidebar-wrapper {
+ grid-area: sidebar;
+ position: sticky;
+ top: var(--header-offset);
+ height: calc(100vh - var(--header-offset));
+ align-self: start;
+ overflow-y: auto;
+ background-color: var(--primary-very-low);
+}
+
+.sidebar-container {
+ box-sizing: border-box;
+ height: 100%;
+ width: 240px;
+ padding: 1em;
+}
+
+.sidebar-toggle {
+ display: flex;
+ justify-content: flex-end;
+}
diff --git a/app/assets/stylesheets/desktop/discourse.scss b/app/assets/stylesheets/desktop/discourse.scss
index 8fb5a9dd553..18893c7aae1 100644
--- a/app/assets/stylesheets/desktop/discourse.scss
+++ b/app/assets/stylesheets/desktop/discourse.scss
@@ -182,3 +182,16 @@ input {
min-width: 0;
}
}
+
+#main-outlet-wrapper {
+ display: grid;
+ grid-template-areas: "content";
+ grid-template-columns: 1fr;
+ gap: 0;
+
+ &.has-sidebar {
+ grid-template-areas: "sidebar content";
+ grid-template-columns: 240px 1fr;
+ gap: 0 2em;
+ }
+}