New component: Menu

It's time to do this menu thing the right way instead of hacky CSS
This commit is contained in:
Lim Chee Aun 2023-01-24 20:56:43 +08:00
parent 19ee95d188
commit 28281bb752
7 changed files with 166 additions and 105 deletions

83
package-lock.json generated
View file

@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"@github/text-expander-element": "~2.3.0", "@github/text-expander-element": "~2.3.0",
"@iconify-icons/mingcute": "~1.2.3", "@iconify-icons/mingcute": "~1.2.3",
"@szhsin/react-menu": "~3.3.1",
"dayjs": "~1.11.7", "dayjs": "~1.11.7",
"dayjs-twitter": "~0.5.0", "dayjs-twitter": "~0.5.0",
"fast-blurhash": "~1.1.2", "fast-blurhash": "~1.1.2",
@ -2364,6 +2365,19 @@
"string.prototype.matchall": "^4.0.6" "string.prototype.matchall": "^4.0.6"
} }
}, },
"node_modules/@szhsin/react-menu": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@szhsin/react-menu/-/react-menu-3.3.1.tgz",
"integrity": "sha512-e8vK+N1YWwTdYXElvRRf5GIImtcDecqTCzpAa0DkGAknKwfQwtQtUnBn+DECodwsWi5H5ONKTU+kn0qJ70hEYQ==",
"dependencies": {
"prop-types": "^15.7.2",
"react-transition-state": "^1.1.5"
},
"peerDependencies": {
"react": ">=16.14.0",
"react-dom": ">=16.14.0"
}
},
"node_modules/@trivago/prettier-plugin-sort-imports": { "node_modules/@trivago/prettier-plugin-sort-imports": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.0.0.tgz", "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.0.0.tgz",
@ -4130,7 +4144,6 @@
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"peer": true,
"dependencies": { "dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0" "js-tokens": "^3.0.0 || ^4.0.0"
}, },
@ -4370,6 +4383,14 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-inspect": { "node_modules/object-inspect": {
"version": "1.12.2", "version": "1.12.2",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",
@ -4565,6 +4586,16 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"node_modules/proxy-compare": { "node_modules/proxy-compare": {
"version": "2.4.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.4.0.tgz", "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.4.0.tgz",
@ -4664,6 +4695,11 @@
"react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
} }
}, },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/react-router": { "node_modules/react-router": {
"version": "6.6.2", "version": "6.6.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.6.2.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.6.2.tgz",
@ -4694,6 +4730,15 @@
"react-dom": ">=16.8" "react-dom": ">=16.8"
} }
}, },
"node_modules/react-transition-state": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/react-transition-state/-/react-transition-state-1.1.5.tgz",
"integrity": "sha512-ITY2mZqc2dWG2eitJkYNdcSFW8aKeOlkL2A/vowRrLL8GH3J6Re/SpD/BLvQzrVOTqjsP0b5S9N10vgNNzwMUQ==",
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/regenerate": { "node_modules/regenerate": {
"version": "1.4.2", "version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
@ -7477,6 +7522,15 @@
"string.prototype.matchall": "^4.0.6" "string.prototype.matchall": "^4.0.6"
} }
}, },
"@szhsin/react-menu": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@szhsin/react-menu/-/react-menu-3.3.1.tgz",
"integrity": "sha512-e8vK+N1YWwTdYXElvRRf5GIImtcDecqTCzpAa0DkGAknKwfQwtQtUnBn+DECodwsWi5H5ONKTU+kn0qJ70hEYQ==",
"requires": {
"prop-types": "^15.7.2",
"react-transition-state": "^1.1.5"
}
},
"@trivago/prettier-plugin-sort-imports": { "@trivago/prettier-plugin-sort-imports": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.0.0.tgz", "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.0.0.tgz",
@ -8819,7 +8873,6 @@
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"peer": true,
"requires": { "requires": {
"js-tokens": "^3.0.0 || ^4.0.0" "js-tokens": "^3.0.0 || ^4.0.0"
} }
@ -9001,6 +9054,11 @@
"integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
"dev": true "dev": true
}, },
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="
},
"object-inspect": { "object-inspect": {
"version": "1.12.2", "version": "1.12.2",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",
@ -9131,6 +9189,16 @@
"integrity": "sha512-6UqkYefdogmzqAZWzJ7laYeJnaXDy2/J+ZqiiMtS7t7OfpXWTlaeGMwX8U6EFvPV/YWWEKRkS8hKS4k60WHTOg==", "integrity": "sha512-6UqkYefdogmzqAZWzJ7laYeJnaXDy2/J+ZqiiMtS7t7OfpXWTlaeGMwX8U6EFvPV/YWWEKRkS8hKS4k60WHTOg==",
"dev": true "dev": true
}, },
"prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"requires": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"proxy-compare": { "proxy-compare": {
"version": "2.4.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.4.0.tgz", "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.4.0.tgz",
@ -9196,6 +9264,11 @@
"integrity": "sha512-IXpIsPe6BleFOEHKzKh5UjwRUaz/JYS0lT/HPsupWEQou2hDqjhLMStc5zyE3eQVT4Fk3FufM8Fw33qW1uyeiw==", "integrity": "sha512-IXpIsPe6BleFOEHKzKh5UjwRUaz/JYS0lT/HPsupWEQou2hDqjhLMStc5zyE3eQVT4Fk3FufM8Fw33qW1uyeiw==",
"requires": {} "requires": {}
}, },
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"react-router": { "react-router": {
"version": "6.6.2", "version": "6.6.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.6.2.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.6.2.tgz",
@ -9213,6 +9286,12 @@
"react-router": "6.6.2" "react-router": "6.6.2"
} }
}, },
"react-transition-state": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/react-transition-state/-/react-transition-state-1.1.5.tgz",
"integrity": "sha512-ITY2mZqc2dWG2eitJkYNdcSFW8aKeOlkL2A/vowRrLL8GH3J6Re/SpD/BLvQzrVOTqjsP0b5S9N10vgNNzwMUQ==",
"requires": {}
},
"regenerate": { "regenerate": {
"version": "1.4.2", "version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",

View file

@ -12,6 +12,7 @@
"dependencies": { "dependencies": {
"@github/text-expander-element": "~2.3.0", "@github/text-expander-element": "~2.3.0",
"@iconify-icons/mingcute": "~1.2.3", "@iconify-icons/mingcute": "~1.2.3",
"@szhsin/react-menu": "~3.3.1",
"dayjs": "~1.11.7", "dayjs": "~1.11.7",
"dayjs-twitter": "~0.5.0", "dayjs-twitter": "~0.5.0",
"fast-blurhash": "~1.1.2", "fast-blurhash": "~1.1.2",

View file

@ -805,54 +805,27 @@ button.carousel-dot:is(.active, [disabled].active) {
/* MENU POPUP */ /* MENU POPUP */
.menu-container { .szh-menu {
position: relative; padding: 8px 0 !important;
}
.menu-container button {
color: inherit !important;
}
.menu-container button:is(:hover, :active, :focus) {
background-color: var(--button-plain-bg-hover-color);
}
.menu-container menu {
position: absolute;
right: 0;
top: 0;
transform: translateY(-100%);
opacity: 0;
pointer-events: none;
padding: 8px 0;
margin: 0; margin: 0;
font-size: 16px; font-size: 16px;
background-color: var(--bg-color); background-color: var(--bg-color);
width: 10em;
list-style: none;
z-index: 100;
border: 1px solid var(--outline-color); border: 1px solid var(--outline-color);
border-radius: 8px; border-radius: 8px;
transition: all 0.2s ease-in-out;
box-shadow: 0 0 8px var(--bg-faded-color), 0 4px 8px var(--bg-faded-color), box-shadow: 0 0 8px var(--bg-faded-color), 0 4px 8px var(--bg-faded-color),
0 2px 4px var(--bg-faded-color); 0 2px 4px var(--bg-faded-color);
}
.menu-container menu li {
margin: 0;
padding: 0;
list-style: none;
}
.menu-container > button:is(:hover, :active, :focus) + menu,
.menu-container menu:is(:hover, :active) {
opacity: 1;
pointer-events: auto;
}
.menu-container menu button {
width: 100%;
text-align: left; text-align: left;
color: var(--text-color) !important;
border-radius: 0;
} }
.menu-container menu button:is(:hover, :focus) { .szh-menu .szh-menu__item {
color: var(--bg-color) !important; padding: 8px 16px !important;
background-color: var(--link-color); }
.szh-menu
.szh-menu__item:not(.szh-menu__item--disabled, .szh-menu__item--hover) {
color: var(--text-color);
}
.szh-menu .szh-menu__item--hover {
color: var(--button-text-color);
background-color: var(--button-bg-color);
} }
/* DONUT METER */ /* DONUT METER */

View file

@ -1,5 +1,6 @@
import './status.css'; import './status.css';
import { Menu, MenuItem } from '@szhsin/react-menu';
import { getBlurHashAverageColor } from 'fast-blurhash'; import { getBlurHashAverageColor } from 'fast-blurhash';
import mem from 'mem'; import mem from 'mem';
import { memo } from 'preact/compat'; import { memo } from 'preact/compat';
@ -587,30 +588,32 @@ function Status({
/> />
</div> </div>
{isSelf && ( {isSelf && (
<span class="menu-container"> <Menu
<button type="button" title="More" class="plain more-button"> align="end"
<Icon icon="more" size="l" alt="More" /> menuButton={
</button> <div class="action">
<menu> <button
{isSelf && ( type="button"
<li> title="More"
<button class="plain more-button"
type="button" >
class="plain" <Icon icon="more" size="l" alt="More" />
onClick={(e) => { </button>
e.preventDefault(); </div>
e.stopPropagation(); }
states.showCompose = { >
editStatus: status, {isSelf && (
}; <MenuItem
}} onClick={() => {
> states.showCompose = {
Edit&hellip; editStatus: status,
</button> };
</li> }}
)} >
</menu> Edit&hellip;
</span> </MenuItem>
)}
</Menu>
)} )}
</div> </div>
</> </>

View file

@ -1,5 +1,7 @@
import './index.css'; import './index.css';
import '@szhsin/react-menu/dist/core.css';
import { render } from 'preact'; import { render } from 'preact';
import { HashRouter } from 'react-router-dom'; import { HashRouter } from 'react-router-dom';

View file

@ -29,7 +29,7 @@
padding: 0; padding: 0;
list-style: none; list-style: none;
} }
#settings-container ul li { #settings-container ul:not([role='menu']) > li {
padding: 8px 0 16px; padding: 8px 0 16px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -37,26 +37,26 @@
flex-wrap: wrap; flex-wrap: wrap;
border-bottom: var(--hairline-width) solid var(--outline-color); border-bottom: var(--hairline-width) solid var(--outline-color);
} }
#settings-container ul li .current { #settings-container ul:not([role='menu']) > li .current {
margin-right: 8px; margin-right: 8px;
color: var(--green-color); color: var(--green-color);
opacity: 0.1; opacity: 0.1;
} }
#settings-container ul li .current.is-current { #settings-container ul:not([role='menu']) > li .current.is-current {
opacity: 1; opacity: 1;
} }
#settings-container ul li .current.is-current + .avatar { #settings-container ul:not([role='menu']) > li .current.is-current + .avatar {
box-shadow: 0 0 0 1.5px var(--green-color), 0 0 8px var(--green-color); box-shadow: 0 0 0 1.5px var(--green-color), 0 0 8px var(--green-color);
} }
#settings-container ul li > div { #settings-container ul:not([role='menu']) > li > div {
flex-grow: 1; flex-grow: 1;
max-width: 100%; max-width: 100%;
} }
#settings-container ul li > div.actions { #settings-container ul:not([role='menu']) > li > div.actions {
flex-basis: fit-content; flex-basis: fit-content;
margin-top: 8px; margin-top: 8px;
} }
#settings-container ul li > div:last-child { #settings-container ul:not([role='menu']) > li > div:last-child {
text-align: right; text-align: right;
} }
#settings-container div, #settings-container div,

View file

@ -1,5 +1,6 @@
import './settings.css'; import './settings.css';
import { Menu, MenuItem } from '@szhsin/react-menu';
import { useReducer, useRef, useState } from 'preact/hooks'; import { useReducer, useRef, useState } from 'preact/hooks';
import { useSnapshot } from 'valtio'; import { useSnapshot } from 'valtio';
@ -92,43 +93,45 @@ function Settings({ onClose }) {
<Icon icon="transfer" /> Switch <Icon icon="transfer" /> Switch
</button> </button>
)} )}
<span> <Menu
{!isDefault && moreThanOneAccount && ( align="end"
menuButton={
<button <button
type="button" type="button"
class="plain small" title="More"
onClick={() => { class="plain more-button"
// Move account to the top of the list
accounts.splice(i, 1);
accounts.unshift(account);
store.local.setJSON('accounts', accounts);
setCurrentDefault(i);
}}
> >
Set as default <Icon icon="more" size="l" alt="More" />
</button> </button>
)} }
{isCurrent && ( >
<> <MenuItem
{' '} disabled={isDefault || !moreThanOneAccount}
<button onClick={() => {
type="button" // Move account to the top of the list
class="plain small" accounts.splice(i, 1);
onClick={() => { accounts.unshift(account);
const yes = confirm( store.local.setJSON('accounts', accounts);
'Are you sure you want to log out?', setCurrentDefault(i);
); }}
if (!yes) return; >
accounts.splice(i, 1); Set as default
store.local.setJSON('accounts', accounts); </MenuItem>
location.reload(); <MenuItem
}} disabled={!isCurrent}
> onClick={() => {
Log out const yes = confirm(
</button> 'Are you sure you want to log out?',
</> );
)} if (!yes) return;
</span> accounts.splice(i, 1);
store.local.setJSON('accounts', accounts);
location.reload();
}}
>
Log out
</MenuItem>
</Menu>
</div> </div>
</li> </li>
); );