Full seamless login support

This commit is contained in:
Eduard Kuzmenko 2023-01-09 17:44:17 +04:00
parent 7097d0c5af
commit 1f1cb9c489
16 changed files with 403 additions and 127 deletions

View File

@ -3982,92 +3982,15 @@ export default class ChatBubbles {
const {mid} = message;
const {url, button_id} = button;
const openWindow = (url: string) => {
window.open(url, '_blank');
};
const onUrlAuthResultAccepted = (urlAuthResult: UrlAuthResult.urlAuthResultAccepted) => {
openWindow(urlAuthResult.url);
};
const onUrlAuthResult = async(urlAuthResult: UrlAuthResult): Promise<void> => {
if(urlAuthResult._ === 'urlAuthResultRequest') {
const b = document.createElement('b');
b.append(urlAuthResult.domain);
const peerTitle = await wrapPeerTitle({peerId: rootScope.myId});
const botPeerTitle = await wrapPeerTitle({peerId: urlAuthResult.bot.id.toPeerId()});
const logInCheckbox: PopupPeerCheckboxOptions = {
text: 'OpenUrlOption1',
textArgs: [b.cloneNode(true), peerTitle],
checked: true
};
const allowMessagesCheckbox: PopupPeerCheckboxOptions = urlAuthResult.pFlags.request_write_access ? {
text: 'OpenUrlOption2',
textArgs: [botPeerTitle],
checked: true
} : undefined;
const checkboxes: PopupPeerCheckboxOptions[] = [
logInCheckbox,
allowMessagesCheckbox
];
const confirmationPromise = confirmationPopup({
titleLangKey: 'OpenUrlTitle',
button: {
langKey: 'Open'
},
descriptionLangKey: 'OpenUrlAlert2',
descriptionLangArgs: [b],
checkboxes: checkboxes.filter(Boolean)
});
if(allowMessagesCheckbox) {
logInCheckbox.checkboxField.input.addEventListener('change', () => {
const disabled = !logInCheckbox.checkboxField.checked;
allowMessagesCheckbox.checkboxField.toggleDisability(disabled);
if(disabled) {
allowMessagesCheckbox.checkboxField.checked = false;
}
});
}
const [logInChecked, allowMessagesChecked] = await confirmationPromise;
if(!logInChecked) {
openWindow(url);
return;
}
const result = await this.managers.appMessagesManager.acceptUrlAuth(
peerId,
mid,
url,
button_id,
allowMessagesChecked
);
return onUrlAuthResult(result);
} else if(urlAuthResult._ === 'urlAuthResultAccepted') {
onUrlAuthResultAccepted(urlAuthResult);
} else {
openWindow(url);
}
};
attachClickEvent(buttonEl, () => {
const toggle = toggleDisability([buttonEl], true);
this.managers.appMessagesManager.requestUrlAuth(
this.chat.appImManager.handleUrlAuth({
peerId,
mid,
url,
button_id
).then((urlAuthResult) => {
buttonId: button_id
}).then(() => {
toggle();
onUrlAuthResult(urlAuthResult);
});
});
break;

View File

@ -167,10 +167,7 @@ export default class AppActiveSessionsTab extends SliderSuperTabEventable {
}
onCloseAfterTimeout() {
if(this.menuElement) {
this.menuElement.remove();
}
this.menuElement?.remove();
return super.onCloseAfterTimeout();
}
}

View File

@ -0,0 +1,114 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import {formatDateAccordingToTodayNew} from '../../../helpers/date';
import {attachClickEvent} from '../../../helpers/dom/clickEvent';
import findUpClassName from '../../../helpers/dom/findUpClassName';
import toggleDisability from '../../../helpers/dom/toggleDisability';
import {WebAuthorization} from '../../../layer';
import AvatarElement from '../../avatar';
import Button from '../../button';
import confirmationPopup from '../../confirmationPopup';
import Row from '../../row';
import SettingSection from '../../settingSection';
import {SliderSuperTabEventable} from '../../sliderTab';
import wrapPeerTitle from '../../wrappers/peerTitle';
export default class AppActiveWebSessionsTab extends SliderSuperTabEventable {
public async init(sessions: WebAuthorization[]) {
this.container.classList.add('active-sessions-container');
this.setTitle('WebSessionsTitle');
const Session = async(auth: WebAuthorization) => {
const peerId = auth.bot_id.toPeerId();
const row = new Row({
title: await wrapPeerTitle({peerId}),
subtitle: [auth.ip, auth.region].join(' - '),
clickable: true,
titleRight: formatDateAccordingToTodayNew(new Date(Math.max(auth.date_active, auth.date_created) * 1000))
});
const media = row.createMedia('big');
const avatar = new AvatarElement();
avatar.classList.add('avatar-48');
await avatar.updateWithOptions({peerId});
media.append(avatar);
row.container.dataset.hash = '' + auth.hash;
row.container.dataset.peerId = '' + peerId;
row.midtitle.textContent = [auth.domain, auth.browser, auth.platform].filter(Boolean).join(', ');
return row;
};
{
const section = new SettingSection({
caption: 'ClearOtherWebSessionsHelp'
});
const btnTerminate = Button('btn-primary btn-transparent danger', {icon: 'stop', text: 'TerminateAllWebSessions'});
attachClickEvent(btnTerminate, async() => {
await confirmationPopup({
descriptionLangKey: 'AreYouSureWebSessions',
button: {
langKey: 'Disconnect',
isDanger: true
}
});
const toggle = toggleDisability([btnTerminate], true);
this.managers.appSeamlessLoginManager.resetWebAuthorizations().then(() => {
this.close();
});
}, {listenerSetter: this.listenerSetter});
section.content.append(btnTerminate);
this.scrollable.append(section.container);
}
{
const section = new SettingSection({
name: 'OtherWebSessions',
caption: 'TerminateWebSessionInfo'
});
const rows = await Promise.all(sessions.map(Session));
section.content.append(...rows.map((row) => row.container));
let leftLength = rows.length;
attachClickEvent(section.content, async(e) => {
const row = findUpClassName(e.target, 'row');
if(!row) {
return;
}
await confirmationPopup({
descriptionLangKey: 'TerminateWebSessionText',
descriptionLangArgs: [await wrapPeerTitle({peerId: row.dataset.peerId.toPeerId()})],
button: {
langKey: 'Disconnect',
isDanger: true
}
});
const hash = row.dataset.hash;
row.classList.add('is-disabled');
this.managers.appSeamlessLoginManager.resetWebAuthorization(hash).then(() => {
if(!--leftLength) {
this.close();
} else {
row.remove();
}
});
}, {listenerSetter: this.listenerSetter});
this.scrollable.append(section.container);
}
}
}

View File

@ -6,7 +6,7 @@
import SliderSuperTab, {SliderSuperTabEventable} from '../../sliderTab';
import Row from '../../row';
import {AccountPassword, Authorization, InputPrivacyKey, Updates} from '../../../layer';
import {AccountPassword, Authorization, InputPrivacyKey, Updates, WebAuthorization} from '../../../layer';
import AppPrivacyPhoneNumberTab from './privacy/phoneNumber';
import AppTwoStepVerificationTab from './2fa';
import AppTwoStepVerificationEnterPasswordTab from './2fa/enterPassword';
@ -33,16 +33,21 @@ import noop from '../../../helpers/noop';
import {toastNew} from '../../toast';
import AppPrivacyVoicesTab from './privacy/voices';
import SettingSection from '../../settingSection';
import AppActiveWebSessionsTab from './activeWebSessions';
export default class AppPrivacyAndSecurityTab extends SliderSuperTabEventable {
private activeSessionsRow: Row;
private authorizations: Authorization.authorization[];
private websitesRow: Row;
private websites: WebAuthorization[];
public static getInitArgs(fromTab: SliderSuperTab) {
return {
appConfig: fromTab.managers.apiManager.getAppConfig(),
globalPrivacy: fromTab.managers.appPrivacyManager.getGlobalPrivacySettings(),
contentSettings: fromTab.managers.apiManager.invokeApi('account.getContentSettings')
contentSettings: fromTab.managers.apiManager.invokeApi('account.getContentSettings'),
webAuthorizations: fromTab.managers.appSeamlessLoginManager.getWebAuthorizations()
};
}
@ -51,6 +56,7 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTabEventable {
this.setTitle('PrivacySettings');
const SUBTITLE: LangPackKey = 'Loading';
const promises: Promise<any>[] = [];
{
const section = new SettingSection({noDelimiter: true, caption: 'SessionsInfo'});
@ -113,7 +119,22 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTabEventable {
});
activeSessionsRow.freezed = true;
section.content.append(blockedUsersRow.container, twoFactorRow.container, activeSessionsRow.container);
const websitesRow = this.websitesRow = new Row({
icon: 'mention',
titleLangKey: 'OtherWebSessions',
subtitleLangKey: SUBTITLE,
clickable: () => {
const tab = this.slider.createTab(AppActiveWebSessionsTab);
tab.eventListener.addEventListener('destroy', () => {
this.updateActiveWebsites();
});
tab.open(this.websites);
},
listenerSetter: this.listenerSetter
});
websitesRow.freezed = true;
section.content.append(blockedUsersRow.container, twoFactorRow.container, activeSessionsRow.container, websitesRow.container);
this.scrollable.append(section.container);
const setBlockedCount = (count: number) => {
@ -152,6 +173,7 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTabEventable {
});
this.updateActiveSessions();
promises.push(this.updateActiveWebsites(p.webAuthorizations));
}
{
@ -271,8 +293,6 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTabEventable {
});
}
const promises: Promise<any>[] = [];
{
const section = new SettingSection({name: 'NewChatsFromNonContacts', caption: 'ArchiveAndMuteInfo'});
@ -441,15 +461,35 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTabEventable {
this.scrollable.append(section.container);
}
// {
// const section = new SettingSection({
// name: 'OtherWebSessions'
// });
// const row = new Row({
// });
// this.scrollable.append(section.container);
// }
return Promise.all(promises);
}
public updateActiveSessions() {
this.managers.apiManager.invokeApi('account.getAuthorizations').then((auths) => {
return this.managers.apiManager.invokeApi('account.getAuthorizations').then((auths) => {
this.activeSessionsRow.freezed = false;
this.authorizations = auths.authorizations;
_i18n(this.activeSessionsRow.subtitle, 'Privacy.Devices', [this.authorizations.length]);
// console.log('auths', auths);
});
}
public updateActiveWebsites(promise = this.managers.appSeamlessLoginManager.getWebAuthorizations()) {
return promise.then((authorizations) => {
this.websitesRow.freezed = false;
this.websites = authorizations;
_i18n(this.websitesRow.subtitle, 'Privacy.Websites', [this.websites.length]);
this.websitesRow.container.classList.toggle('hide', !this.websites.length);
});
}
}

View File

@ -21,7 +21,7 @@ const App = {
version: process.env.VERSION,
versionFull: process.env.VERSION_FULL,
build: +process.env.BUILD,
langPackVersion: '0.7.5',
langPackVersion: '0.7.6',
langPack: 'webk',
langPackCode: 'en',
domains: MAIN_DOMAINS,

View File

@ -14,10 +14,11 @@ export default function addAnchorListener<Params extends {pathnameParams?: any,
protocol?: 'tg',
callback: (params: Params, element?: HTMLAnchorElement) => boolean | any,
noPathnameParams?: boolean,
noUriParams?: boolean
noUriParams?: boolean,
noCancelEvent?: boolean
}) {
(window as any)[(options.protocol ? options.protocol + '_' : '') + options.name] = (element?: HTMLAnchorElement/* , e: Event */) => {
cancelEvent(null);
!options.noCancelEvent && cancelEvent(null);
let href = element.href;
let pathnameParams: any[];

View File

@ -9,8 +9,8 @@
* https://github.com/zhukov/webogram/blob/master/LICENSE
*/
export default function cancelEvent(event: Event) {
event = event || window.event;
export default function cancelEvent(event?: Event) {
event ||= window.event;
if(event) {
// @ts-ignore
event = event.originalEvent || event;

View File

@ -98,6 +98,10 @@ const lang = {
'one_value': '%1$d device',
'other_value': '%1$d devices'
},
'Privacy.Websites': {
'one_value': '%1$d website',
'other_value': '%1$d websites'
},
'Privacy.SensitiveContent': 'Sensitive Content',
'PrivacyModal.Search.Placeholder': 'Add Users or Groups...',
'Permissions.NoExceptions': 'No exceptions',
@ -855,6 +859,14 @@ const lang = {
'EditTopicHideInfo': 'If the \'General\' topic is hidden, group members can pull down in the topic list to view it.',
'OpenUrlOption1': 'Log in to %1$s as **%2$s**',
'OpenUrlOption2': 'Allow **%1$s** to send me messages',
'OtherWebSessions': 'Connected websites',
'WebSessionsTitle': 'Logged In with Telegram',
'TerminateWebSessionText': 'Are you sure you want to disconnect %1$s?',
'Disconnect': 'Disconnect',
'TerminateAllWebSessions': 'Disconnect All Websites',
'AreYouSureWebSessions': 'Are you sure you want to disconnect all websites where you logged in with Telegram?',
'ClearOtherWebSessionsHelp': 'You can log in on websites that support signing in with Telegram.',
'TerminateWebSessionInfo': 'Tap to disconnect from your Telegram account.',
// * macos
'AccountSettings.Filters': 'Chat Folders',

View File

@ -27,9 +27,9 @@ import {MOUNT_CLASS_TO} from '../../config/debug';
import appNavigationController from '../../components/appNavigationController';
import AppPrivateSearchTab from '../../components/sidebarRight/tabs/search';
import I18n, {i18n, join, LangPackKey} from '../langPack';
import {ChatFull, ChatInvite, ChatParticipant, ChatParticipants, Message, MessageAction, MessageMedia, SendMessageAction, User, Chat as MTChat} from '../../layer';
import {ChatFull, ChatInvite, ChatParticipant, ChatParticipants, Message, MessageAction, MessageMedia, SendMessageAction, User, Chat as MTChat, UrlAuthResult} from '../../layer';
import PeerTitle from '../../components/peerTitle';
import PopupPeer from '../../components/popups/peer';
import PopupPeer, {PopupPeerCheckboxOptions} from '../../components/popups/peer';
import blurActiveElement from '../../helpers/dom/blurActiveElement';
import cancelEvent from '../../helpers/dom/cancelEvent';
import disableTransition from '../../helpers/dom/disableTransition';
@ -98,6 +98,8 @@ import {MiddleEllipsisElement} from '../../components/middleEllipsis';
import addAnchorListener from '../../helpers/addAnchorListener';
import parseUriParams from '../../helpers/string/parseUriParams';
import getMessageThreadId from './utils/messages/getMessageThreadId';
import findUpTag from '../../helpers/dom/findUpTag';
import {MTAppConfig} from '../mtproto/appConfig';
export type ChatSavedPosition = {
mids: number[],
@ -783,6 +785,149 @@ export class AppImManager extends EventListenerBase<{
this.onHashChange(true);
this.attachKeydownListener();
this.handleAutologinDomains();
}
public handleUrlAuth(options: {
peerId?: PeerId,
mid?: number,
buttonId?: number,
url: string
}) {
const {peerId, mid, buttonId, url} = options;
const openWindow = (url: string) => {
window.open(url, '_blank');
};
const onUrlAuthResultAccepted = (urlAuthResult: UrlAuthResult.urlAuthResultAccepted) => {
openWindow(urlAuthResult.url);
};
const onUrlAuthResult = async(urlAuthResult: UrlAuthResult): Promise<void> => {
if(urlAuthResult._ === 'urlAuthResultRequest') {
const b = document.createElement('b');
b.append(urlAuthResult.domain);
const peerTitle = await wrapPeerTitle({peerId: rootScope.myId});
const botPeerTitle = await wrapPeerTitle({peerId: urlAuthResult.bot.id.toPeerId()});
const logInCheckbox: PopupPeerCheckboxOptions = {
text: 'OpenUrlOption1',
textArgs: [b.cloneNode(true), peerTitle],
checked: true
};
const allowMessagesCheckbox: PopupPeerCheckboxOptions = urlAuthResult.pFlags.request_write_access ? {
text: 'OpenUrlOption2',
textArgs: [botPeerTitle],
checked: true
} : undefined;
const checkboxes: PopupPeerCheckboxOptions[] = [
logInCheckbox,
allowMessagesCheckbox
];
const confirmationPromise = confirmationPopup({
titleLangKey: 'OpenUrlTitle',
button: {
langKey: 'Open'
},
descriptionLangKey: 'OpenUrlAlert2',
descriptionLangArgs: [b],
checkboxes: checkboxes.filter(Boolean)
});
if(allowMessagesCheckbox) {
logInCheckbox.checkboxField.input.addEventListener('change', () => {
const disabled = !logInCheckbox.checkboxField.checked;
allowMessagesCheckbox.checkboxField.toggleDisability(disabled);
if(disabled) {
allowMessagesCheckbox.checkboxField.checked = false;
}
});
}
const [logInChecked, allowMessagesChecked] = await confirmationPromise;
if(!logInChecked) {
openWindow(url);
return;
}
const result = await this.managers.appSeamlessLoginManager.acceptUrlAuth(
url,
peerId,
mid,
buttonId,
allowMessagesChecked
);
return onUrlAuthResult(result);
} else if(urlAuthResult._ === 'urlAuthResultAccepted') {
onUrlAuthResultAccepted(urlAuthResult);
} else {
openWindow(url);
}
};
return this.managers.appSeamlessLoginManager.requestUrlAuth(
url,
peerId,
mid,
buttonId
).then((urlAuthResult) => {
onUrlAuthResult(urlAuthResult);
});
}
private handleAutologinDomains() {
let appConfig: MTAppConfig;
rootScope.addEventListener('app_config', (_appConfig) => {
appConfig = _appConfig;
});
const onAnchorClick = (element: HTMLAnchorElement) => {
const url = new URL(element.href);
if(appConfig.url_auth_domains.includes(url.hostname)) {
this.handleUrlAuth({url: element.href});
cancelEvent();
return;
}
if(!appConfig.autologin_token || !appConfig.autologin_domains) {
return;
}
const originalUrl = element.dataset.originalUrl ??= element.href;
if(appConfig.autologin_domains.includes(url.hostname)) {
url.searchParams.set('autologin_token', appConfig.autologin_token);
element.href = url.toString();
setTimeout(() => {
element.href = originalUrl;
delete element.dataset.originalUrl;
}, 0);
}
};
document.addEventListener('click', (e) => {
const anchor = findUpTag(e.target as HTMLElement, 'A') as HTMLAnchorElement;
if(anchor?.href) {
onAnchorClick(anchor);
}
});
// addAnchorListener({
// name: 'handleUrlClick',
// callback: (_, element) => {
// onAnchorClick(element);
// },
// noCancelEvent: true,
// noPathnameParams: true,
// noUriParams: true
// });
}
private getStackFromElement(element: HTMLElement) {

View File

@ -6399,31 +6399,4 @@ export class AppMessagesManager extends AppManager {
send_as: this.appPeersManager.getInputPeerById(sendAsPeerId)
});
}
public requestUrlAuth(peerId: PeerId, mid: number, url: string, buttonId: number) {
return this.apiManager.invokeApi('messages.requestUrlAuth', {
button_id: buttonId,
msg_id: getServerMessageId(mid),
peer: this.appPeersManager.getInputPeerById(peerId),
url
}).then((urlAuthResult) => {
if(urlAuthResult._ === 'urlAuthResultRequest') {
this.appUsersManager.saveApiUser(urlAuthResult.bot);
}
return urlAuthResult;
});
}
public acceptUrlAuth(peerId: PeerId, mid: number, url: string, buttonId: number, writeAllowed?: boolean) {
return this.apiManager.invokeApi('messages.acceptUrlAuth', {
button_id: buttonId,
msg_id: getServerMessageId(mid),
peer: this.appPeersManager.getInputPeerById(peerId),
url,
write_allowed: writeAllowed
}).then((urlAuthResult) => {
return urlAuthResult as Exclude<UrlAuthResult, UrlAuthResult.urlAuthResultRequest>;
});
}
}

View File

@ -0,0 +1,55 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import {UrlAuthResult} from '../../layer';
import {AppManager} from './manager';
import getServerMessageId from './utils/messageId/getServerMessageId';
export default class AppSeamlessLoginManager extends AppManager {
public requestUrlAuth(url: string, peerId?: PeerId, mid?: number, buttonId?: number) {
return this.apiManager.invokeApi('messages.requestUrlAuth', {
button_id: buttonId,
msg_id: mid ? getServerMessageId(mid) : undefined,
peer: peerId ? this.appPeersManager.getInputPeerById(peerId) : undefined,
url
}).then((urlAuthResult) => {
if(urlAuthResult._ === 'urlAuthResultRequest') {
this.appUsersManager.saveApiUser(urlAuthResult.bot);
}
return urlAuthResult;
});
}
public acceptUrlAuth(url: string, peerId?: PeerId, mid?: number, buttonId?: number, writeAllowed?: boolean) {
return this.apiManager.invokeApi('messages.acceptUrlAuth', {
button_id: buttonId,
msg_id: mid ? getServerMessageId(mid) : undefined,
peer: peerId ? this.appPeersManager.getInputPeerById(peerId) : undefined,
url,
write_allowed: writeAllowed
}).then((urlAuthResult) => {
return urlAuthResult as Exclude<UrlAuthResult, UrlAuthResult.urlAuthResultRequest>;
});
}
public getWebAuthorizations() {
return this.apiManager.invokeApi('account.getWebAuthorizations').then((webAuthorizations) => {
this.appUsersManager.saveApiUsers(webAuthorizations.users);
return webAuthorizations.authorizations;
});
}
public resetWebAuthorization(hash: Long) {
return this.apiManager.invokeApi('account.resetWebAuthorization', {
hash
});
}
public resetWebAuthorizations() {
return this.apiManager.invokeApi('account.resetWebAuthorizations');
}
}

View File

@ -46,6 +46,7 @@ import filterUnique from '../../helpers/array/filterUnique';
import AppWebDocsManager from './appWebDocsManager';
import AppPaymentsManager from './appPaymentsManager';
import AppAttachMenuBotsManager from './appAttachMenuBotsManager';
import AppSeamlessLoginManager from './appSeamlessLoginManager';
export default function createManagers(appStoragesManager: AppStoragesManager, userId: UserId) {
const managers = {
@ -88,7 +89,8 @@ export default function createManagers(appStoragesManager: AppStoragesManager, u
appStateManager: appStateManager,
appWebDocsManager: new AppWebDocsManager,
appPaymentsManager: new AppPaymentsManager,
appAttachMenuBotsManager: new AppAttachMenuBotsManager
appAttachMenuBotsManager: new AppAttachMenuBotsManager,
appSeamlessLoginManager: new AppSeamlessLoginManager
};
type T = typeof managers;

View File

@ -44,6 +44,7 @@ import type {AppUsersManager} from './appUsersManager';
import type AppWebDocsManager from './appWebDocsManager';
import type {AppWebPagesManager} from './appWebPagesManager';
import type AppAttachMenuBotsManager from './appAttachMenuBotsManager';
import type AppSeamlessLoginManager from './appSeamlessLoginManager';
import type {AppManagers} from './managers';
export class AppManager {
@ -88,6 +89,7 @@ export class AppManager {
protected appWebDocsManager: AppWebDocsManager;
protected appPaymentsManager: AppPaymentsManager;
protected appAttachMenuBotsManager: AppAttachMenuBotsManager;
protected appSeamlessLoginManager: AppSeamlessLoginManager;
public clear: (init?: boolean) => void;

View File

@ -98,6 +98,13 @@ export class ApiManager extends ApiManagerMethods {
this.changeTransportType(transportType);
});
// #endif
// * Make sure that the used autologin_token is no more than 10000 seconds old
// * https://core.telegram.org/api/url-authorization
const REFRESH_APP_CONFIG_INTERVAL = (10000 - 30) * 1000;
setInterval(() => {
this.getAppConfig(true);
}, REFRESH_APP_CONFIG_INTERVAL);
}
protected after() {

View File

@ -225,6 +225,11 @@ avatar-element {
--multiplier: 1.35;
}
&.avatar-36 {
--size: 36px;
--multiplier: 1.5;
}
&.avatar-35 {
--size: 35px;
--multiplier: 1.542857;

View File

@ -114,7 +114,7 @@ $btn-menu-z-index: 4;
font-size: 1rem;
backdrop-filter: var(--menu-backdrop-filter);
min-width: 11.25rem;
max-width: 16.25rem;
// max-width: 16.25rem;
&-old {
padding: .5rem 0;