index.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886
  1. <script setup lang="ts">
  2. import {
  3. defineAsyncComponent,
  4. ref,
  5. watch,
  6. onMounted,
  7. onBeforeUnmount
  8. } from "vue";
  9. import { useRoute, useRouter } from "vue-router";
  10. import { useWebsocketsStore } from "@/stores/websockets";
  11. import { useUserAuthStore } from "@/stores/userAuth";
  12. import keyboardShortcuts from "@/keyboardShortcuts";
  13. const FloatingBox = defineAsyncComponent(
  14. () => import("@/components/FloatingBox.vue")
  15. );
  16. const route = useRoute();
  17. const router = useRouter();
  18. const { socket } = useWebsocketsStore();
  19. const { hasPermission } = useUserAuthStore();
  20. const currentTab = ref("");
  21. const siteSettings = ref({
  22. logo: "",
  23. sitename: ""
  24. });
  25. const sidebarActive = ref(true);
  26. const sidebarPadding = ref(0);
  27. const keyboardShortcutsHelper = ref();
  28. const childrenActive = ref({
  29. songs: false,
  30. users: false,
  31. youtube: false
  32. });
  33. const toggleChildren = payload => {
  34. if (typeof payload.force === "undefined")
  35. childrenActive.value[payload.child] =
  36. !childrenActive.value[payload.child];
  37. else childrenActive.value[payload.child] = payload.force;
  38. };
  39. const getTabFromPath = (path?) => {
  40. const localPath = path || route.path;
  41. return localPath.substr(0, 7) === "/admin/"
  42. ? localPath.substr(7, localPath.length)
  43. : null;
  44. };
  45. const onRouteChange = () => {
  46. if (currentTab.value.startsWith("songs")) {
  47. toggleChildren({ child: "songs", force: false });
  48. } else if (currentTab.value.startsWith("users")) {
  49. toggleChildren({ child: "users", force: false });
  50. } else if (currentTab.value.startsWith("youtube")) {
  51. toggleChildren({ child: "youtube", force: false });
  52. }
  53. currentTab.value = getTabFromPath();
  54. // if (this.$refs[`${currentTab.value}-tab`])
  55. // this.$refs[`${currentTab.value}-tab`].scrollIntoView({
  56. // inline: "center",
  57. // block: "nearest"
  58. // });
  59. localStorage.setItem("lastAdminPage", currentTab.value);
  60. if (currentTab.value.startsWith("songs"))
  61. toggleChildren({ child: "songs", force: true });
  62. else if (currentTab.value.startsWith("users"))
  63. toggleChildren({ child: "users", force: true });
  64. else if (currentTab.value.startsWith("youtube"))
  65. toggleChildren({ child: "youtube", force: true });
  66. };
  67. const toggleKeyboardShortcutsHelper = () => {
  68. keyboardShortcutsHelper.value.toggleBox();
  69. };
  70. const resetKeyboardShortcutsHelper = () => {
  71. keyboardShortcutsHelper.value.resetBox();
  72. };
  73. const toggleSidebar = () => {
  74. sidebarActive.value = !sidebarActive.value;
  75. localStorage.setItem("admin-sidebar-active", sidebarActive.value);
  76. };
  77. const calculateSidebarPadding = () => {
  78. const scrollTop =
  79. document.documentElement.scrollTop || document.scrollTop || 0;
  80. if (scrollTop <= 64) sidebarPadding.value = 64 - scrollTop;
  81. else sidebarPadding.value = 0;
  82. };
  83. watch(
  84. () => route.path,
  85. path => {
  86. if (getTabFromPath(path)) onRouteChange();
  87. }
  88. );
  89. onMounted(async () => {
  90. if (getTabFromPath()) {
  91. onRouteChange();
  92. } else if (localStorage.getItem("lastAdminPage")) {
  93. router.push(`/admin/${localStorage.getItem("lastAdminPage")}`);
  94. } else {
  95. router.push(`/admin/songs`);
  96. }
  97. siteSettings.value = await lofig.get("siteSettings");
  98. sidebarActive.value = JSON.parse(
  99. localStorage.getItem("admin-sidebar-active")
  100. );
  101. if (sidebarActive.value === null)
  102. sidebarActive.value = !(document.body.clientWidth <= 768);
  103. calculateSidebarPadding();
  104. window.addEventListener("scroll", calculateSidebarPadding);
  105. keyboardShortcuts.registerShortcut("admin.toggleKeyboardShortcutsHelper", {
  106. keyCode: 191, // '/' key
  107. ctrl: true,
  108. preventDefault: true,
  109. handler: () => {
  110. toggleKeyboardShortcutsHelper();
  111. }
  112. });
  113. keyboardShortcuts.registerShortcut("admin.resetKeyboardShortcutsHelper", {
  114. keyCode: 191, // '/' key
  115. ctrl: true,
  116. shift: true,
  117. preventDefault: true,
  118. handler: () => {
  119. resetKeyboardShortcutsHelper();
  120. }
  121. });
  122. });
  123. onBeforeUnmount(() => {
  124. socket.dispatch("apis.leaveRooms");
  125. window.removeEventListener("scroll", calculateSidebarPadding);
  126. const shortcutNames = [
  127. "admin.toggleKeyboardShortcutsHelper",
  128. "admin.resetKeyboardShortcutsHelper"
  129. ];
  130. shortcutNames.forEach(shortcutName => {
  131. keyboardShortcuts.unregisterShortcut(shortcutName);
  132. });
  133. });
  134. </script>
  135. <template>
  136. <div class="app">
  137. <div class="admin-area">
  138. <main-header :class="{ 'admin-sidebar-active': sidebarActive }" />
  139. <div class="admin-content">
  140. <div
  141. class="admin-sidebar"
  142. :class="{ minimised: !sidebarActive }"
  143. >
  144. <div class="inner">
  145. <div
  146. class="bottom"
  147. :style="`padding-bottom: ${sidebarPadding}px`"
  148. >
  149. <div
  150. class="sidebar-item toggle-sidebar"
  151. @click="toggleSidebar()"
  152. content="Expand"
  153. v-tippy="{ onShow: () => !sidebarActive }"
  154. >
  155. <i class="material-icons">menu_open</i>
  156. <span>Minimise</span>
  157. </div>
  158. <div
  159. v-if="
  160. hasPermission('admin.view.songs') &&
  161. sidebarActive
  162. "
  163. class="sidebar-item with-children"
  164. :class="{ 'is-active': childrenActive.songs }"
  165. >
  166. <span>
  167. <router-link to="/admin/songs">
  168. <i class="material-icons">music_note</i>
  169. <span>Songs</span>
  170. </router-link>
  171. <i
  172. class="material-icons toggle-sidebar-children"
  173. @click="
  174. toggleChildren({ child: 'songs' })
  175. "
  176. >
  177. {{
  178. childrenActive.songs
  179. ? "expand_less"
  180. : "expand_more"
  181. }}
  182. </i>
  183. </span>
  184. <div class="sidebar-item-children">
  185. <router-link
  186. class="sidebar-item-child"
  187. to="/admin/songs"
  188. >
  189. Songs
  190. </router-link>
  191. <router-link
  192. v-if="
  193. hasPermission('admin.view.import')
  194. "
  195. class="sidebar-item-child"
  196. to="/admin/songs/import"
  197. >
  198. Import
  199. </router-link>
  200. </div>
  201. </div>
  202. <router-link
  203. v-else-if="
  204. hasPermission('admin.view.users') &&
  205. !sidebarActive
  206. "
  207. class="sidebar-item songs"
  208. to="/admin/songs"
  209. content="Songs"
  210. v-tippy="{
  211. theme: 'info',
  212. onShow: () => !sidebarActive
  213. }"
  214. >
  215. <i class="material-icons">music_note</i>
  216. <span>Songs</span>
  217. </router-link>
  218. <router-link
  219. v-if="hasPermission('admin.view.reports')"
  220. class="sidebar-item reports"
  221. to="/admin/reports"
  222. content="Reports"
  223. v-tippy="{
  224. theme: 'info',
  225. onShow: () => !sidebarActive
  226. }"
  227. >
  228. <i class="material-icons">flag</i>
  229. <span>Reports</span>
  230. </router-link>
  231. <router-link
  232. v-if="hasPermission('admin.view.stations')"
  233. class="sidebar-item stations"
  234. to="/admin/stations"
  235. content="Stations"
  236. v-tippy="{
  237. theme: 'info',
  238. onShow: () => !sidebarActive
  239. }"
  240. >
  241. <i class="material-icons">radio</i>
  242. <span>Stations</span>
  243. </router-link>
  244. <router-link
  245. v-if="hasPermission('admin.view.playlists')"
  246. class="sidebar-item playlists"
  247. to="/admin/playlists"
  248. content="Playlists"
  249. v-tippy="{
  250. theme: 'info',
  251. onShow: () => !sidebarActive
  252. }"
  253. >
  254. <i class="material-icons">library_music</i>
  255. <span>Playlists</span>
  256. </router-link>
  257. <div
  258. v-if="
  259. hasPermission('admin.view.users') &&
  260. sidebarActive
  261. "
  262. class="sidebar-item with-children"
  263. :class="{ 'is-active': childrenActive.users }"
  264. >
  265. <span>
  266. <router-link to="/admin/users">
  267. <i class="material-icons">people</i>
  268. <span>Users</span>
  269. </router-link>
  270. <i
  271. class="material-icons toggle-sidebar-children"
  272. @click="
  273. toggleChildren({ child: 'users' })
  274. "
  275. >
  276. {{
  277. childrenActive.users
  278. ? "expand_less"
  279. : "expand_more"
  280. }}
  281. </i>
  282. </span>
  283. <div class="sidebar-item-children">
  284. <router-link
  285. class="sidebar-item-child"
  286. to="/admin/users"
  287. >
  288. Users
  289. </router-link>
  290. <router-link
  291. class="sidebar-item-child"
  292. to="/admin/users/data-requests"
  293. >
  294. Data Requests
  295. </router-link>
  296. <router-link
  297. class="sidebar-item-child"
  298. to="/admin/users/punishments"
  299. >
  300. Punishments
  301. </router-link>
  302. </div>
  303. </div>
  304. <router-link
  305. v-else-if="
  306. hasPermission('admin.view.users') &&
  307. !sidebarActive
  308. "
  309. class="sidebar-item users"
  310. to="/admin/users"
  311. content="Users"
  312. v-tippy="{
  313. theme: 'info',
  314. onShow: () => !sidebarActive
  315. }"
  316. >
  317. <i class="material-icons">people</i>
  318. <span>Users</span>
  319. </router-link>
  320. <router-link
  321. v-if="hasPermission('admin.view.news')"
  322. class="sidebar-item news"
  323. to="/admin/news"
  324. content="News"
  325. v-tippy="{
  326. theme: 'info',
  327. onShow: () => !sidebarActive
  328. }"
  329. >
  330. <i class="material-icons">chrome_reader_mode</i>
  331. <span>News</span>
  332. </router-link>
  333. <router-link
  334. v-if="hasPermission('admin.view.statistics')"
  335. class="sidebar-item statistics"
  336. to="/admin/statistics"
  337. content="Statistics"
  338. v-tippy="{
  339. theme: 'info',
  340. onShow: () => !sidebarActive
  341. }"
  342. >
  343. <i class="material-icons">show_chart</i>
  344. <span>Statistics</span>
  345. </router-link>
  346. <div
  347. v-if="
  348. (hasPermission('admin.view.youtube') ||
  349. hasPermission(
  350. 'admin.view.youtubeVideos'
  351. )) &&
  352. sidebarActive
  353. "
  354. class="sidebar-item with-children"
  355. :class="{ 'is-active': childrenActive.youtube }"
  356. >
  357. <span>
  358. <router-link
  359. :to="`/admin/youtube${
  360. hasPermission('admin.view.youtube')
  361. ? ''
  362. : '/videos'
  363. }`"
  364. >
  365. <i class="material-icons"
  366. >smart_display</i
  367. >
  368. <span>YouTube</span>
  369. </router-link>
  370. <i
  371. class="material-icons toggle-sidebar-children"
  372. @click="
  373. toggleChildren({ child: 'youtube' })
  374. "
  375. >
  376. {{
  377. childrenActive.youtube
  378. ? "expand_less"
  379. : "expand_more"
  380. }}
  381. </i>
  382. </span>
  383. <div class="sidebar-item-children">
  384. <router-link
  385. v-if="
  386. hasPermission('admin.view.youtube')
  387. "
  388. class="sidebar-item-child"
  389. to="/admin/youtube"
  390. >
  391. YouTube
  392. </router-link>
  393. <router-link
  394. v-if="
  395. hasPermission(
  396. 'admin.view.youtubeVideos'
  397. )
  398. "
  399. class="sidebar-item-child"
  400. to="/admin/youtube/videos"
  401. >
  402. Videos
  403. </router-link>
  404. </div>
  405. </div>
  406. <router-link
  407. v-else-if="
  408. (hasPermission('admin.view.youtube') ||
  409. hasPermission(
  410. 'admin.view.youtubeVideos'
  411. )) &&
  412. !sidebarActive
  413. "
  414. class="sidebar-item youtube"
  415. :to="`/admin/youtube${
  416. hasPermission('admin.view.youtube')
  417. ? ''
  418. : '/videos'
  419. }`"
  420. content="YouTube"
  421. v-tippy="{
  422. theme: 'info',
  423. onShow: () => !sidebarActive
  424. }"
  425. >
  426. <i class="material-icons">smart_display</i>
  427. <span>YouTube</span>
  428. </router-link>
  429. </div>
  430. </div>
  431. </div>
  432. <div class="admin-container">
  433. <div class="admin-tab-container">
  434. <router-view></router-view>
  435. </div>
  436. <main-footer />
  437. </div>
  438. </div>
  439. </div>
  440. <floating-box
  441. id="keyboardShortcutsHelper"
  442. ref="keyboardShortcutsHelper"
  443. title="Admin Keyboard Shortcuts"
  444. >
  445. <template #body>
  446. <div>
  447. <div>
  448. <span class="biggest"
  449. ><b>Keyboard shortcuts helper</b></span
  450. >
  451. <span
  452. ><b>Ctrl + /</b> - Toggles this keyboard shortcuts
  453. helper</span
  454. >
  455. <span
  456. ><b>Ctrl + Shift + /</b> - Resets the position of
  457. this keyboard shortcuts helper</span
  458. >
  459. <hr />
  460. </div>
  461. <div>
  462. <span class="biggest"><b>Table</b></span>
  463. <span class="bigger"><b>Navigation</b></span>
  464. <span
  465. ><b>Up / Down arrow keys</b> - Move between
  466. rows</span
  467. >
  468. <hr />
  469. </div>
  470. <div>
  471. <span class="bigger"><b>Page navigation</b></span>
  472. <span
  473. ><b>Ctrl + Left/Right arrow keys</b> - Previous/next
  474. page</span
  475. >
  476. <span
  477. ><b>Ctrl + Shift + Left/Right arrow keys</b> -
  478. First/last page</span
  479. >
  480. <hr />
  481. </div>
  482. <div>
  483. <span class="bigger"><b>Reset localStorage</b></span>
  484. <span><b>Ctrl + F5</b> - Resets localStorage</span>
  485. <hr />
  486. </div>
  487. <div>
  488. <span class="bigger"><b>Selecting</b></span>
  489. <span><b>Space</b> - Selects/unselects a row</span>
  490. <span><b>Ctrl + A</b> - Selects all rows</span>
  491. <span
  492. ><b>Shift + Up/Down arrow keys</b> - Selects all
  493. rows in between</span
  494. >
  495. <span
  496. ><b>Ctrl + Up/Down arrow keys</b> - Unselects all
  497. rows in between</span
  498. >
  499. <hr />
  500. </div>
  501. <div>
  502. <span class="bigger"><b>Popup actions</b></span>
  503. <span><b>Ctrl + 1-9</b> - Execute action 1-9</span>
  504. <span><b>Ctrl + 0</b> - Select action 1</span>
  505. <hr />
  506. </div>
  507. </div>
  508. </template>
  509. </floating-box>
  510. </div>
  511. </template>
  512. <style lang="less" scoped>
  513. .night-mode {
  514. .main-container .admin-area {
  515. .admin-sidebar .inner {
  516. .top {
  517. background-color: var(--dark-grey-3);
  518. }
  519. .bottom {
  520. background-color: var(--dark-grey-2);
  521. .sidebar-item {
  522. background-color: var(--dark-grey-2);
  523. border-color: var(--dark-grey-3);
  524. &,
  525. &.with-children .sidebar-item-child,
  526. &.with-children > span > a {
  527. color: var(--white);
  528. }
  529. }
  530. }
  531. }
  532. :deep(.admin-content .admin-container .admin-tab-container) {
  533. .admin-tab {
  534. .card {
  535. background-color: var(--dark-grey-3);
  536. p {
  537. color: var(--light-grey-2);
  538. }
  539. }
  540. }
  541. }
  542. }
  543. }
  544. .main-container {
  545. height: auto;
  546. .admin-area {
  547. display: flex;
  548. flex-direction: column;
  549. min-height: 100vh;
  550. :deep(.nav) {
  551. .nav-menu.is-active {
  552. left: 45px;
  553. }
  554. &.admin-sidebar-active .nav-menu.is-active {
  555. left: 200px;
  556. }
  557. }
  558. .admin-sidebar {
  559. display: flex;
  560. min-width: 200px;
  561. width: 200px;
  562. @media screen and (max-width: 768px) {
  563. min-width: 45px;
  564. width: 45px;
  565. }
  566. .inner {
  567. display: flex;
  568. flex-direction: column;
  569. max-height: 100vh;
  570. width: 200px;
  571. position: sticky;
  572. top: 0;
  573. bottom: 0;
  574. left: 0;
  575. z-index: 5;
  576. box-shadow: @box-shadow;
  577. .bottom {
  578. overflow-y: auto;
  579. height: 100%;
  580. max-height: 100%;
  581. display: flex;
  582. flex-direction: column;
  583. flex: 1 0 auto;
  584. background-color: var(--white);
  585. .sidebar-item {
  586. display: flex;
  587. padding: 0 20px;
  588. line-height: 40px;
  589. font-size: 16px;
  590. font-weight: 600;
  591. color: var(--primary-color);
  592. background-color: var(--white);
  593. border-bottom: 1px solid var(--light-grey-2);
  594. transition: filter 0.2s ease-in-out;
  595. & > .material-icons {
  596. line-height: 40px;
  597. margin-right: 5px;
  598. }
  599. &:hover,
  600. &:focus,
  601. &.router-link-active,
  602. &.is-active {
  603. filter: brightness(95%);
  604. }
  605. &.toggle-sidebar {
  606. cursor: pointer;
  607. font-weight: 400;
  608. }
  609. &.with-children {
  610. flex-direction: column;
  611. & > span {
  612. display: flex;
  613. line-height: 40px;
  614. cursor: pointer;
  615. & > a {
  616. display: flex;
  617. }
  618. & > .material-icons,
  619. & > a > .material-icons {
  620. line-height: 40px;
  621. margin-right: 5px;
  622. }
  623. }
  624. .toggle-sidebar-children {
  625. margin-left: auto;
  626. }
  627. .sidebar-item-children {
  628. display: none;
  629. }
  630. &.is-active .sidebar-item-children {
  631. display: flex;
  632. flex-direction: column;
  633. .sidebar-item-child {
  634. display: flex;
  635. flex-direction: column;
  636. margin-left: 30px;
  637. font-size: 14px;
  638. line-height: 30px;
  639. position: relative;
  640. &::before {
  641. content: "";
  642. position: absolute;
  643. width: 1px;
  644. height: 30px;
  645. top: 0;
  646. left: -20px;
  647. background-color: var(--light-grey-3);
  648. }
  649. &:last-child::before {
  650. height: 16px;
  651. }
  652. &::after {
  653. content: "";
  654. position: absolute;
  655. width: 15px;
  656. height: 1px;
  657. top: 15px;
  658. left: -20px;
  659. background-color: var(--light-grey-3);
  660. }
  661. &.router-link-active {
  662. filter: brightness(95%);
  663. }
  664. }
  665. }
  666. }
  667. }
  668. }
  669. }
  670. &.minimised {
  671. min-width: 45px;
  672. width: 45px;
  673. .inner {
  674. max-width: 45px;
  675. .top {
  676. justify-content: center;
  677. .full-logo {
  678. display: none;
  679. }
  680. .minimised-logo {
  681. display: flex;
  682. }
  683. }
  684. .sidebar-item {
  685. justify-content: center;
  686. padding: 0;
  687. & > span {
  688. display: none;
  689. }
  690. }
  691. }
  692. }
  693. }
  694. .admin-content {
  695. display: flex;
  696. flex-direction: row;
  697. flex-grow: 1;
  698. .admin-container {
  699. display: flex;
  700. flex-direction: column;
  701. flex-grow: 1;
  702. overflow: hidden;
  703. :deep(.admin-tab-container) {
  704. display: flex;
  705. flex-direction: column;
  706. flex: 1 0 auto;
  707. padding: 10px 10px 20px 10px;
  708. .admin-tab {
  709. display: flex;
  710. flex-direction: column;
  711. width: 100%;
  712. max-width: 1900px;
  713. margin: 0 auto;
  714. padding: 0 10px;
  715. .card {
  716. display: flex;
  717. flex-grow: 1;
  718. flex-direction: column;
  719. padding: 20px;
  720. margin: 10px 0;
  721. border-radius: @border-radius;
  722. background-color: var(--white);
  723. color: var(--dark-grey);
  724. box-shadow: @box-shadow;
  725. h1 {
  726. font-size: 36px;
  727. margin: 0 0 5px 0;
  728. }
  729. h4 {
  730. font-size: 22px;
  731. margin: 0;
  732. }
  733. h5 {
  734. font-size: 18px;
  735. margin: 0;
  736. }
  737. hr {
  738. margin: 10px 0;
  739. }
  740. &.tab-info {
  741. flex-direction: row;
  742. flex-wrap: wrap;
  743. .info-row {
  744. display: flex;
  745. flex-grow: 1;
  746. flex-direction: column;
  747. }
  748. .button-row {
  749. display: flex;
  750. flex-direction: row;
  751. flex-wrap: wrap;
  752. justify-content: center;
  753. margin: auto 0;
  754. padding: 5px 0;
  755. & > .button,
  756. & > span {
  757. margin: auto 0;
  758. &:not(:first-child) {
  759. margin-left: 5px;
  760. }
  761. }
  762. & > span > .control.has-addons {
  763. margin-bottom: 0 !important;
  764. }
  765. }
  766. }
  767. }
  768. @media screen and (min-width: 980px) {
  769. &.container {
  770. margin: 0 auto;
  771. max-width: 960px;
  772. }
  773. }
  774. @media screen and (min-width: 1180px) {
  775. &.container {
  776. max-width: 1200px;
  777. }
  778. }
  779. }
  780. }
  781. }
  782. }
  783. }
  784. }
  785. :deep(.box) {
  786. box-shadow: @box-shadow;
  787. display: block;
  788. &:not(:last-child) {
  789. margin-bottom: 20px;
  790. }
  791. }
  792. #keyboardShortcutsHelper {
  793. .box-body {
  794. .biggest {
  795. font-size: 18px;
  796. }
  797. .bigger {
  798. font-size: 16px;
  799. }
  800. span {
  801. display: block;
  802. }
  803. }
  804. }
  805. </style>