mirror of
https://github.com/ancsemi/Haven.git
synced 2026-10-26 00:29:14 +00:00
File themes that set the page background (Braid, Braid Light, Compact) are treated as full palettes: turning one on in Settings selects it in the picker instead of layering its colours on top of Matrix or another built-in theme. The theme popup gains a Layout section with Braid and Compact switches so the chrome can be changed without touching the picker colours. Picking a built-in theme now saves first and then reapplies only the real CSS tweaks, so the Settings toggles and the picker agree.
325 lines
10 KiB
JavaScript
325 lines
10 KiB
JavaScript
/**
|
|
* @name Compact Layout
|
|
* @description Reversible desktop layout that folds the server rail into the navigation sidebar and docks account and voice controls in its footer. Preserves Haven's tablet, mobile, and message-density behavior.
|
|
* @author bernardokcosta
|
|
* @version 1.0.0
|
|
*/
|
|
class CompactLayout {
|
|
start() {
|
|
if (this._started) return;
|
|
this._started = true;
|
|
this._engaged = false;
|
|
this._blocked = false;
|
|
this._suspended = document.documentElement.hasAttribute('data-haven-layout-editing');
|
|
this._listeners = [];
|
|
this._placements = [];
|
|
this._inlineRestores = [];
|
|
this._media = window.matchMedia('(min-width: 901px)');
|
|
|
|
try {
|
|
HavenApi.DOM.addStyle('CompactLayout', CompactLayout.CSS);
|
|
this._buildControl();
|
|
this._listen(document, 'keydown', (event) => {
|
|
if (!event.ctrlKey || !event.altKey || event.key.toLowerCase() !== 'c') return;
|
|
event.preventDefault();
|
|
this._toggle();
|
|
}, true);
|
|
this._listen(document, 'haven:layout-editing', (event) => {
|
|
this._suspended = event.detail?.active === true;
|
|
if (!this._suspended && event.detail?.owner && event.detail.owner !== 'CompactLayout') {
|
|
this._restoreDesktop();
|
|
return;
|
|
}
|
|
this._syncDesktop();
|
|
});
|
|
this._listen(document, 'haven:layout-owner-change', (event) => {
|
|
if (event.detail?.owner !== 'CompactLayout') this._syncDesktop();
|
|
});
|
|
this._listen(this._media, 'change', () => this._syncDesktop());
|
|
this._listen(document, 'haven:layout-effect', (event) => {
|
|
if (typeof event.detail?.compact !== 'boolean') return;
|
|
if (event.detail.compact) this._engage();
|
|
else this._disengage();
|
|
});
|
|
|
|
if (HavenApi.Data.load('CompactLayout', 'layoutOn', '1') !== '0') this._engage(false);
|
|
else this._renderControl();
|
|
} catch (error) {
|
|
this.stop();
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
stop() {
|
|
if (!this._started) return;
|
|
this._disengage(false);
|
|
for (const [target, type, listener, options] of this._listeners) {
|
|
target.removeEventListener(type, listener, options);
|
|
}
|
|
this._listeners = [];
|
|
this._control?.remove();
|
|
this._control = null;
|
|
this._media = null;
|
|
HavenApi.DOM.removeStyle('CompactLayout');
|
|
this._started = false;
|
|
}
|
|
|
|
_listen(target, type, listener, options) {
|
|
target.addEventListener(type, listener, options);
|
|
this._listeners.push([target, type, listener, options]);
|
|
}
|
|
|
|
_region(name) {
|
|
return HavenApi.DOM.query(`[data-haven-region="${CompactLayout.REGIONS[name]}"]`);
|
|
}
|
|
|
|
_buildControl() {
|
|
const actions = this._region('sidebarActions');
|
|
if (!actions || this._control) return;
|
|
const control = document.createElement('button');
|
|
control.type = 'button';
|
|
control.textContent = 'C';
|
|
control.title = 'Compact layout (Ctrl+Alt+C)';
|
|
control.setAttribute('aria-label', 'Switch to compact layout');
|
|
control.setAttribute('aria-pressed', 'false');
|
|
control.setAttribute('data-compact-layout-control', '');
|
|
control.addEventListener('click', () => this._toggle());
|
|
actions.prepend(control);
|
|
this._control = control;
|
|
}
|
|
|
|
_toggle() {
|
|
this._engaged ? this._disengage() : this._engage();
|
|
}
|
|
|
|
_engage(persist = true) {
|
|
if (this._engaged) return;
|
|
this._engaged = true;
|
|
document.documentElement.setAttribute('data-compact-layout', '1');
|
|
try {
|
|
this._syncDesktop();
|
|
if (persist) HavenApi.Data.save('CompactLayout', 'layoutOn', '1');
|
|
this._renderControl();
|
|
document.dispatchEvent(new CustomEvent('haven:compact-layout', { detail: { on: true } }));
|
|
} catch (error) {
|
|
this._engaged = false;
|
|
try { this._restoreDesktop(); }
|
|
finally {
|
|
document.documentElement.removeAttribute('data-compact-layout');
|
|
this._renderControl();
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
_disengage(persist = true) {
|
|
if (!this._engaged) return;
|
|
this._engaged = false;
|
|
this._restoreDesktop();
|
|
document.documentElement.removeAttribute('data-compact-layout');
|
|
if (persist) HavenApi.Data.save('CompactLayout', 'layoutOn', '0');
|
|
this._renderControl();
|
|
document.dispatchEvent(new CustomEvent('haven:compact-layout', { detail: { on: false } }));
|
|
}
|
|
|
|
_renderControl() {
|
|
if (!this._control) return;
|
|
this._control.setAttribute('aria-pressed', this._engaged ? 'true' : 'false');
|
|
const blocked = this._engaged && this._blocked;
|
|
this._control.title = blocked
|
|
? 'Compact layout is waiting for another layout plugin'
|
|
: 'Compact layout (Ctrl+Alt+C)';
|
|
this._control.setAttribute(
|
|
'aria-label',
|
|
blocked
|
|
? 'Compact layout waiting for another layout plugin'
|
|
: (this._engaged ? 'Switch to classic layout' : 'Switch to compact layout')
|
|
);
|
|
}
|
|
|
|
_syncDesktop() {
|
|
const editing = document.documentElement.hasAttribute('data-haven-layout-editing');
|
|
if (this._engaged && !this._suspended && !editing && this._media?.matches) this._applyDesktop();
|
|
else this._restoreDesktop();
|
|
}
|
|
|
|
_applyDesktop() {
|
|
if (this._placements.length) return;
|
|
const serverRail = this._region('serverRail');
|
|
const navigation = this._region('navigation');
|
|
const account = this._region('account');
|
|
const footer = this._region('footer');
|
|
const sidebarActions = this._region('sidebarActions');
|
|
const voiceSettings = this._region('voiceSettings');
|
|
const voiceControls = this._region('voiceControls');
|
|
if (!serverRail || !navigation || !account || !footer || !sidebarActions
|
|
|| !voiceSettings || !voiceControls) {
|
|
console.warn('[CompactLayout] Required public layout regions are unavailable');
|
|
return;
|
|
}
|
|
if (HavenApi.Layout && !HavenApi.Layout.acquire('CompactLayout')) {
|
|
this._blocked = true;
|
|
this._renderControl();
|
|
return;
|
|
}
|
|
|
|
this._blocked = false;
|
|
this._move(serverRail, navigation, navigation.firstChild);
|
|
this._move(account, footer, sidebarActions);
|
|
this._move(voiceSettings, footer, account);
|
|
this._move(voiceControls, footer, account);
|
|
this._dockVoiceControls(voiceControls);
|
|
document.documentElement.setAttribute('data-compact-layout-desktop', '1');
|
|
this._renderControl();
|
|
}
|
|
|
|
_move(element, parent, before) {
|
|
this._placements.push([element, element.parentNode, element.nextSibling]);
|
|
parent.insertBefore(element, before || null);
|
|
}
|
|
|
|
_dockVoiceControls(element) {
|
|
const overrides = {
|
|
position: 'static',
|
|
top: 'auto',
|
|
right: 'auto',
|
|
bottom: 'auto',
|
|
left: 'auto',
|
|
width: 'auto',
|
|
'max-width': 'none',
|
|
transform: 'none',
|
|
'z-index': 'auto',
|
|
border: '0',
|
|
'border-radius': '0',
|
|
'box-shadow': 'none',
|
|
overflow: 'auto hidden',
|
|
resize: 'none'
|
|
};
|
|
for (const [property, value] of Object.entries(overrides)) {
|
|
this._inlineRestores.push([
|
|
element,
|
|
property,
|
|
element.style.getPropertyValue(property),
|
|
element.style.getPropertyPriority(property)
|
|
]);
|
|
element.style.setProperty(property, value, 'important');
|
|
}
|
|
}
|
|
|
|
_restoreDesktop() {
|
|
document.documentElement.removeAttribute('data-compact-layout-desktop');
|
|
for (let index = this._inlineRestores.length - 1; index >= 0; index--) {
|
|
const [element, property, value, priority] = this._inlineRestores[index];
|
|
if (value) element.style.setProperty(property, value, priority);
|
|
else element.style.removeProperty(property);
|
|
}
|
|
this._inlineRestores = [];
|
|
for (let index = this._placements.length - 1; index >= 0; index--) {
|
|
const [element, parent, next] = this._placements[index];
|
|
parent.insertBefore(element, next?.parentNode === parent ? next : null);
|
|
}
|
|
this._placements = [];
|
|
HavenApi.Layout?.release('CompactLayout');
|
|
this._blocked = Boolean(
|
|
this._engaged && HavenApi.Layout?.owner && HavenApi.Layout.owner !== 'CompactLayout'
|
|
);
|
|
this._renderControl();
|
|
}
|
|
}
|
|
|
|
CompactLayout.REGIONS = Object.freeze({
|
|
serverRail: 'server-rail',
|
|
navigation: 'navigation-sidebar',
|
|
account: 'account',
|
|
footer: 'sidebar-footer',
|
|
sidebarActions: 'sidebar-actions',
|
|
voiceSettings: 'voice-settings',
|
|
voiceControls: 'voice-controls'
|
|
});
|
|
|
|
CompactLayout.CSS = `
|
|
html[data-compact-layout-desktop="1"] :where([data-haven-region="server-rail"]) {
|
|
order: -1 !important;
|
|
width: 100%;
|
|
min-width: 0;
|
|
height: 3.25rem;
|
|
min-height: 3.25rem;
|
|
box-sizing: border-box;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
padding: 0.375rem 0.5rem;
|
|
border-inline: 0 !important;
|
|
border-block-end: 1px solid var(--border);
|
|
overflow-x: auto;
|
|
overflow-y: hidden;
|
|
scrollbar-width: thin;
|
|
}
|
|
|
|
html[data-compact-layout-desktop="1"] :where([data-haven-region="server-rail"]) > * {
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
html[data-compact-layout-desktop="1"] :where([data-haven-region="server-list"]) {
|
|
display: flex;
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
overflow-x: auto;
|
|
overflow-y: hidden;
|
|
scrollbar-width: thin;
|
|
}
|
|
|
|
html[data-compact-layout-desktop="1"] :where([data-haven-region="sidebar-footer"]) {
|
|
background: var(--bg-tertiary);
|
|
}
|
|
|
|
html[data-compact-layout-desktop="1"] :where([data-haven-region="account"]) {
|
|
margin: 0.375rem 0.5rem;
|
|
border: 1px solid var(--border);
|
|
}
|
|
|
|
html[data-compact-layout-desktop="1"] :where([data-haven-region="voice-settings"]) {
|
|
max-height: 45vh;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
html[data-compact-layout-desktop="1"] :where([data-haven-region="voice-controls"]) {
|
|
justify-content: flex-start;
|
|
overflow-x: auto;
|
|
padding: 0.375rem 0.5rem;
|
|
}
|
|
|
|
html[data-haven-theme-api="1"] :where([data-compact-layout-control]) {
|
|
width: 2.25rem;
|
|
height: 2.25rem;
|
|
padding: 0;
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
background: var(--bg-tertiary);
|
|
color: var(--text-secondary);
|
|
font: 700 0.75rem var(--font-mono);
|
|
cursor: pointer;
|
|
}
|
|
|
|
html[data-haven-theme-api="1"] :where([data-compact-layout-control]):hover {
|
|
border-color: var(--accent);
|
|
background: var(--bg-hover);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
html[data-haven-theme-api="1"] :where([data-compact-layout-control]):focus-visible {
|
|
outline: 2px solid var(--accent);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
html[data-haven-theme-api="1"] :where([data-compact-layout-control][aria-pressed="true"]) {
|
|
border-color: var(--accent);
|
|
background: var(--accent);
|
|
color: var(--accent-text);
|
|
}
|
|
`;
|
|
|
|
if (typeof module !== 'undefined') module.exports = CompactLayout;
|
|
if (typeof _win !== 'undefined') _win.CompactLayout = CompactLayout;
|