Skip to content

Commit 12e8860

Browse files
committed
This commit makes the sidebar resizable to a specific level as well as adds a collapse button. It also removes the whitespace of the main container to make the inner part using the full width, which gets us way closer to mobile usage friendlyness
Signed-off-by: Kai Wagner <kai.wagner@percona.com>
1 parent 767008f commit 12e8860

File tree

4 files changed

+246
-6
lines changed

4 files changed

+246
-6
lines changed

app/assets/stylesheets/base/root.css

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,76 @@ body {
66
}
77

88
.container {
9-
max-width: var(--max-width-container);
9+
max-width: none;
1010
width: 100%;
11-
margin: 0 auto;
11+
margin: 0;
1212
padding: 0;
1313
background-color: var(--color-bg-container);
1414
min-width: 0;
1515
min-height: calc(100vh - var(--nav-height));
1616
}
1717

1818
.page-layout.with-sidebar {
19+
--sidebar-width: 360px;
20+
--sidebar-resizer-width: 10px;
1921
display: grid;
20-
grid-template-columns: 480px 1fr;
22+
grid-template-columns: var(--sidebar-width) var(--sidebar-resizer-width) minmax(0, 1fr);
23+
grid-template-areas: "sidebar resizer main";
2124
gap: 0;
2225
align-items: start;
26+
overflow: visible;
2327
}
2428

2529
.page-layout.with-sidebar .layout-sidebar {
2630
background: var(--color-bg-card);
27-
border-right: var(--border-width) solid var(--color-border);
2831
min-height: calc(100vh - var(--nav-height));
2932
background-color: var(--color-bg-sidebar);
3033
align-self: stretch;
34+
min-width: 0;
35+
grid-area: sidebar;
36+
}
37+
38+
.page-layout.with-sidebar .layout-sidebar-resizer {
39+
cursor: col-resize;
40+
background: var(--color-bg-page);
41+
border-right: var(--border-width) solid var(--color-border);
42+
min-height: calc(100vh - var(--nav-height));
43+
position: sticky;
44+
top: var(--nav-height);
45+
touch-action: none;
46+
width: var(--sidebar-resizer-width);
47+
grid-area: resizer;
48+
}
49+
50+
.page-layout.with-sidebar .layout-sidebar-resizer:hover,
51+
.page-layout.with-sidebar .layout-sidebar-resizer:active {
52+
background: var(--color-bg-hover);
53+
}
54+
55+
body.sidebar-resizing {
56+
cursor: col-resize;
57+
user-select: none;
58+
}
59+
60+
body.sidebar-collapsed .page-layout.with-sidebar {
61+
grid-template-columns: 0 var(--sidebar-resizer-width) minmax(0, 1fr);
62+
}
63+
64+
body.sidebar-collapsed .layout-sidebar {
65+
visibility: hidden;
66+
pointer-events: none;
67+
}
68+
69+
body.sidebar-collapsed .layout-sidebar-resizer {
70+
border-left: var(--border-width) solid var(--color-border);
71+
justify-self: start;
72+
}
73+
74+
body.sidebar-collapsed .page-layout.with-sidebar > main.container {
75+
max-width: none;
76+
width: 100%;
77+
}
78+
79+
.page-layout.with-sidebar > main.container {
80+
grid-area: main;
3181
}

app/assets/stylesheets/components/sidebar.css

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,32 @@
77
padding-right: var(--spacing-2);
88
}
99

10+
.layout-sidebar-resizer {
11+
display: flex;
12+
align-items: center;
13+
justify-content: center;
14+
}
15+
16+
.sidebar-collapse-button {
17+
border: none;
18+
background: var(--color-bg-card);
19+
color: var(--color-text-secondary);
20+
width: 28px;
21+
height: 28px;
22+
border-radius: 999px;
23+
box-shadow: var(--shadow-sm);
24+
cursor: pointer;
25+
display: inline-flex;
26+
align-items: center;
27+
justify-content: center;
28+
transition: transform var(--transition-fast), background-color var(--transition-fast);
29+
}
30+
31+
.sidebar-collapse-button:hover {
32+
background: var(--color-bg-hover);
33+
transform: scale(1.05);
34+
}
35+
1036
.sidebar-section {
1137
margin-bottom: var(--spacing-8);
1238
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { Controller } from "@hotwired/stimulus"
2+
3+
const STORAGE_WIDTH_KEY = "hackorum-sidebar-width"
4+
const STORAGE_COLLAPSED_KEY = "hackorum-sidebar-collapsed"
5+
const DEFAULT_WIDTH = 360
6+
const MIN_WIDTH = 260
7+
const MAX_WIDTH = 960
8+
const MAX_WIDTH_RATIO = 0.75
9+
10+
export default class extends Controller {
11+
static targets = ["layout", "sidebar", "resizer", "toggleButton", "toggleIcon"]
12+
13+
connect() {
14+
if (!this.hasLayoutTarget || !this.hasSidebarTarget) {
15+
return
16+
}
17+
18+
this.handleWindowResize = this.handleWindowResize.bind(this)
19+
window.addEventListener("resize", this.handleWindowResize)
20+
21+
this.applyStoredState()
22+
}
23+
24+
disconnect() {
25+
window.removeEventListener("resize", this.handleWindowResize)
26+
this.stopResize()
27+
}
28+
29+
toggle() {
30+
if (this.isCollapsed()) {
31+
this.expand()
32+
} else {
33+
this.collapse()
34+
}
35+
}
36+
37+
startResize(event) {
38+
if (this.isCollapsed()) {
39+
return
40+
}
41+
42+
event.preventDefault()
43+
this.isResizing = true
44+
this.startX = this.clientXFrom(event)
45+
this.startWidth = this.sidebarTarget.getBoundingClientRect().width
46+
47+
this.boundHandleResize = this.handleResize.bind(this)
48+
this.boundStopResize = this.stopResize.bind(this)
49+
50+
document.addEventListener("mousemove", this.boundHandleResize)
51+
document.addEventListener("mouseup", this.boundStopResize)
52+
document.addEventListener("touchmove", this.boundHandleResize, { passive: false })
53+
document.addEventListener("touchend", this.boundStopResize)
54+
55+
document.body.classList.add("sidebar-resizing")
56+
}
57+
58+
handleResize(event) {
59+
if (!this.isResizing) {
60+
return
61+
}
62+
63+
event.preventDefault()
64+
const delta = this.clientXFrom(event) - this.startX
65+
const nextWidth = this.clampWidth(this.startWidth + delta)
66+
this.setSidebarWidth(nextWidth)
67+
}
68+
69+
stopResize() {
70+
if (!this.isResizing) {
71+
return
72+
}
73+
74+
this.isResizing = false
75+
document.body.classList.remove("sidebar-resizing")
76+
77+
document.removeEventListener("mousemove", this.boundHandleResize)
78+
document.removeEventListener("mouseup", this.boundStopResize)
79+
document.removeEventListener("touchmove", this.boundHandleResize)
80+
document.removeEventListener("touchend", this.boundStopResize)
81+
82+
window.localStorage.setItem(STORAGE_WIDTH_KEY, String(this.currentWidth || this.startWidth || DEFAULT_WIDTH))
83+
}
84+
85+
handleWindowResize() {
86+
if (this.isCollapsed()) {
87+
return
88+
}
89+
90+
const stored = this.readWidth()
91+
this.setSidebarWidth(this.clampWidth(stored))
92+
}
93+
94+
applyStoredState() {
95+
const collapsed = window.localStorage.getItem(STORAGE_COLLAPSED_KEY) === "true"
96+
if (collapsed) {
97+
this.collapse(false)
98+
return
99+
}
100+
101+
this.expand(false)
102+
this.setSidebarWidth(this.clampWidth(this.readWidth()))
103+
}
104+
105+
collapse(shouldStore = true) {
106+
document.body.classList.add("sidebar-collapsed")
107+
if (shouldStore) {
108+
window.localStorage.setItem(STORAGE_COLLAPSED_KEY, "true")
109+
}
110+
this.updateToggleIcon()
111+
}
112+
113+
expand(shouldStore = true) {
114+
document.body.classList.remove("sidebar-collapsed")
115+
if (shouldStore) {
116+
window.localStorage.setItem(STORAGE_COLLAPSED_KEY, "false")
117+
}
118+
this.setSidebarWidth(this.clampWidth(this.readWidth()))
119+
this.updateToggleIcon()
120+
}
121+
122+
updateToggleIcon() {
123+
if (!this.hasToggleIconTarget) {
124+
return
125+
}
126+
127+
this.toggleIconTarget.textContent = this.isCollapsed() ? "▶" : "◀"
128+
}
129+
130+
isCollapsed() {
131+
return document.body.classList.contains("sidebar-collapsed")
132+
}
133+
134+
readWidth() {
135+
const stored = Number.parseFloat(window.localStorage.getItem(STORAGE_WIDTH_KEY))
136+
if (Number.isFinite(stored) && stored > 0) {
137+
return stored
138+
}
139+
return DEFAULT_WIDTH
140+
}
141+
142+
setSidebarWidth(width) {
143+
this.currentWidth = width
144+
this.layoutTarget.style.setProperty("--sidebar-width", `${width}px`)
145+
}
146+
147+
clampWidth(width) {
148+
const maxWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, Math.round(window.innerWidth * MAX_WIDTH_RATIO)))
149+
return Math.min(Math.max(width, MIN_WIDTH), maxWidth)
150+
}
151+
152+
clientXFrom(event) {
153+
if (event.touches && event.touches.length > 0) {
154+
return event.touches[0].clientX
155+
}
156+
if (event.changedTouches && event.changedTouches.length > 0) {
157+
return event.changedTouches[0].clientX
158+
}
159+
return event.clientX
160+
}
161+
}

app/views/layouts/application.html.slim

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,12 @@ html data-theme="light"
6565
= link_to "Register", new_registration_path, class: "nav-link"
6666

6767
- if content_for?(:sidebar)
68-
.page-layout.with-sidebar
69-
.layout-sidebar
68+
.page-layout.with-sidebar data-controller="sidebar" data-sidebar-target="layout"
69+
.layout-sidebar#layout-sidebar data-sidebar-target="sidebar"
7070
= yield :sidebar
71+
.layout-sidebar-resizer data-sidebar-target="resizer" role="separator" aria-orientation="vertical" aria-label="Resize sidebar" data-action="mousedown->sidebar#startResize touchstart->sidebar#startResize"
72+
button.sidebar-collapse-button type="button" aria-label="Toggle sidebar" data-action="click->sidebar#toggle" data-sidebar-target="toggleButton"
73+
span data-sidebar-target="toggleIcon"
7174
main.container
7275
- flash.each do |type, message|
7376
.flash class=type

0 commit comments

Comments
 (0)