Skip to content

Commit a418df1

Browse files
authored
feat: add custom width configuration for sidebar (#108)
Introduce a custom width setting for the sidebar, enhancing layout flexibility and responsiveness.
1 parent 8b88048 commit a418df1

5 files changed

Lines changed: 83 additions & 11 deletions

File tree

src/components/editor/forms/header-schema.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,13 @@ export const BASE_APPEARANCE_SCHEMA = memoizeOne((data: SidebarAppearanceConfig)
106106
},
107107
},
108108
},
109+
{
110+
name: 'width',
111+
label: 'Custom Width',
112+
helper:
113+
'Set a custom width for the sidebar, allows values with css units (e.g., 300px or 20%), or a number (which will be treated as pixels)',
114+
type: 'string',
115+
},
109116
] as const,
110117
},
111118
],

src/sidebar-css.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export const DIVIDER_ADDED_STYLE = css`
6060
padding: 0;
6161
box-sizing: border-box;
6262
margin: var(--divider-margin-radius);
63-
width: 248px;
63+
width: calc(100% - var(--ha-space-2));
6464
}
6565
:host(:not([expanded])) .divider[added] {
6666
margin: 0 !important;
@@ -75,6 +75,13 @@ export const DIVIDER_ADDED_STYLE = css`
7575
--mdc-icon-size: 20px !important;
7676
}
7777
78+
:host([expanded]) .menu {
79+
width: 100% !important;
80+
}
81+
:host([expanded]) ha-md-list-item {
82+
width: calc(100% - var(--ha-space-2)) !important;
83+
}
84+
7885
:host([expanded]) .grid-container > ha-md-list-item[grid-item] > ha-icon.badge,
7986
:host([expanded]) .grid-container > ha-md-list-item[grid-item] > span.badge {
8087
position: absolute;
@@ -124,16 +131,19 @@ export const DIVIDER_ADDED_STYLE = css`
124131
}
125132
:host([expanded]) .grid-container {
126133
display: grid;
127-
grid-template-columns: repeat(auto-fill, calc(25% - 0px));
134+
/* Use flexible minmax columns so grid items reflow with the available drawer width,
135+
* which keeps the layout responsive when --custom-sidebar-width is changed. */
136+
grid-template-columns: repeat(auto-fill, minmax(48px, 1fr));
137+
grid-gap: 4px 4px;
138+
width: calc(100% - var(--ha-space-2));
128139
padding: 0;
129140
margin: 0;
130141
overflow: clip;
131142
/* max-height: fit-content; */
132-
justify-content: center;
133-
/* grid-gap: 4px 4px; */
143+
/* justify-content: flex-start; */
134144
}
135145
:host([expanded]) .grid-container > ha-md-list-item[grid-item] {
136-
width: 48px;
146+
width: 48px !important;
137147
height: 48px;
138148
/* justify-content: center;
139149
align-items: center; */
@@ -296,6 +306,12 @@ export const DRAWER_STYLE = css`
296306
}
297307
`;
298308

309+
export const HA_MAIN_CUSTOM_WIDTH_STYLE = css`
310+
:host([expanded]:not([modal])) {
311+
--mdc-drawer-width: var(--custom-sidebar-width, calc(256px + var(--safe-area-inset-left, 0px)));
312+
}
313+
`;
314+
299315
export const HUI_ROOT_STYLE = css`
300316
:host .header::before {
301317
content: '';

src/sidebar-organizer.ts

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ import { HAElement, HAQuerySelector, HAQuerySelectorEvent, OnListenDetail } from
5353
import { HomeAssistantStylesManager } from 'home-assistant-styles-manager';
5454

5555
import { SoGroupDivider } from './components/so-group-divider';
56-
import { DIVIDER_ADDED_STYLE, DRAWER_STYLE, HUI_ROOT_STYLE } from './sidebar-css';
56+
import { DIVIDER_ADDED_STYLE, DRAWER_STYLE, HA_MAIN_CUSTOM_WIDTH_STYLE, HUI_ROOT_STYLE } from './sidebar-css';
5757

5858
export class SidebarOrganizer {
5959
constructor() {
@@ -245,9 +245,8 @@ export class SidebarOrganizer {
245245
let ticking = false;
246246
let lastScrollY = window.scrollY;
247247
let currentOffset = 0;
248-
249-
// NEW
250248
let accumulatedDelta = 0;
249+
251250
const threshold = maxHide; // Minimum scroll delta before reacting, this helps to prevent jitter on small scrolls. Defaults to header height;
252251

253252
const updateHeaderVisibility = () => {
@@ -543,6 +542,9 @@ export class SidebarOrganizer {
543542
this._pinnedGroups = normalizePinnedGroups(pinned_groups || {});
544543
// Initialize collapsed groups based on config, this will be used to set initial state of groups and manage collapse/expand functionality
545544
this.collapsedItems = getCollapsedItems(custom_groups, default_collapsed);
545+
546+
// Setup custom sidebar width
547+
this._addCustomWidthStyle();
546548
// Setup additional styles based on color config
547549
this._addAdditionalStyles(color_config);
548550

@@ -574,14 +576,14 @@ export class SidebarOrganizer {
574576
beforeSpacerContainer.appendChild(newItemEl);
575577
}
576578
});
577-
console.log('New items added to sidebar:', new_items);
579+
console.debug('New items added to sidebar:', new_items);
578580
}
579581

580582
if (move_settings_from_fixed === true) {
581583
const settingsItem = sidebarShadowRoot.querySelector(SELECTOR.SETTINGS_ITEM) as SidebarPanelItem;
582584
if (settingsItem) {
583585
beforeSpacerContainer.appendChild(settingsItem);
584-
console.log('Settings item moved from fixed:', move_settings_from_fixed);
586+
console.debug('Settings item moved from fixed:', move_settings_from_fixed);
585587
} else {
586588
console.log(
587589
'%cSIDEBAR-ORGANIZER:%c ❌ Settings item not found',
@@ -734,7 +736,11 @@ export class SidebarOrganizer {
734736
this._panelsList.insertBefore(haMdList, spacer.nextElementSibling);
735737
}
736738
//success
737-
console.log('%cSIDEBAR-ORGANIZER:%c ✅ Bottom items added to sidebar', 'color: #bada55;', 'color: #40c057;');
739+
console.debug(
740+
'%cSIDEBAR-ORGANIZER:%c ✅ Bottom items added to sidebar',
741+
'color: #bada55;',
742+
'color: #40c057;'
743+
);
738744
}
739745
console.groupEnd();
740746
});
@@ -1118,6 +1124,23 @@ export class SidebarOrganizer {
11181124
});
11191125
}
11201126

1127+
private _addCustomWidthStyle(): void {
1128+
if (!this._config.width) return;
1129+
Promise.all([this._haMain.element as Promise<HTMLElement>]).then((elements) => {
1130+
const [haMain] = elements;
1131+
const { width } = this._config!;
1132+
const customWidth = this._store._utils.CONFIG._computeWidth(width);
1133+
if (!customWidth) {
1134+
console.debug('❌ Invalid custom width, skipping applying custom width style:', width);
1135+
return;
1136+
}
1137+
haMain.style.setProperty('--custom-sidebar-width', customWidth);
1138+
console.debug('Custom sidebar width applied:', customWidth);
1139+
1140+
this._styleManager.addStyle([HA_MAIN_CUSTOM_WIDTH_STYLE.toString()], haMain.shadowRoot!);
1141+
});
1142+
}
1143+
11211144
private _addAdditionalStyles(color_config: SidebarConfig['color_config'], mode?: string) {
11221145
mode = mode ? mode : this.darkMode ? 'dark' : 'light';
11231146
const customTheme = color_config?.custom_theme?.theme || undefined;

src/types/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ export interface SidebarAppearanceConfig {
124124
text_transformation?: TextTransformation;
125125
move_settings_from_fixed?: boolean;
126126
force_transparent_background?: boolean;
127+
width?: number | string;
127128
}
128129
export const AppearanceConfigKeys = [
129130
'header_title',
@@ -133,6 +134,7 @@ export const AppearanceConfigKeys = [
133134
'text_transformation',
134135
'move_settings_from_fixed',
135136
'force_transparent_background',
137+
'width',
136138
] as const;
137139

138140
export interface SidebarColorConfig {

src/utilities/configs/validators.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,3 +215,27 @@ export const _changeStorageConfig = (config: SidebarConfig): void => {
215215
setStorage(STORAGE.UI_CONFIG, config);
216216
}
217217
};
218+
219+
const ALLOWED_UNITS = ['%', 'em', 'ex', 'px', 'rem', 'vh', 'vmax', 'vmin', 'vw'];
220+
const WIDTH_REGEX = new RegExp(`^\\s*(\\d+(\\.\\d+)?)(\\s*(${ALLOWED_UNITS.join('|')})?)\\s*$`);
221+
222+
export const _computeWidth = (width?: number | string): string | undefined => {
223+
if (width == null) return undefined;
224+
// check if width has units, if not parse it as number and add 'px' unit, if it has units, validate the unit and return the value with unit
225+
226+
const normalizedWidth = typeof width === 'string' ? width.trim() : String(width);
227+
const match = WIDTH_REGEX.exec(normalizedWidth);
228+
if (!match) {
229+
console.warn(
230+
`%cVALIDATORS:%c Invalid width value "${width}". Please provide a valid CSS size (e.g. "250px", "20%", "15em").`,
231+
'color: #ff9800;',
232+
'color: #ff5722;'
233+
);
234+
return undefined;
235+
}
236+
237+
const value = match[1];
238+
const unit = match[4] || 'px'; // default to 'px' if no unit is provided
239+
240+
return `${value}${unit}`;
241+
};

0 commit comments

Comments
 (0)