index.vue 20 KB

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