- added NcModal.vue, NcActions.vue, NcButton.vue, FirstrunWizard.vue, Card.vue, Page0.vue, Page1.vue, Page2.vue, Page3.vue and some icons

- added lime color inside tailwind.config.js
- added some utilities scripts needed for components
- npm updates
- changed postcss.config.js for nesting css styles
- added about function to NavBar.vue
This commit is contained in:
Kaimbacher 2023-12-21 09:30:21 +01:00
parent cefd9081ae
commit 87e9314b00
25 changed files with 3475 additions and 401 deletions

683
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -85,6 +85,7 @@
"clamscan": "^2.1.2", "clamscan": "^2.1.2",
"crypto": "^1.0.1", "crypto": "^1.0.1",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"focus-trap": "^7.5.4",
"http-status-codes": "^2.2.0", "http-status-codes": "^2.2.0",
"leaflet": "^1.9.3", "leaflet": "^1.9.3",
"luxon": "^3.2.1", "luxon": "^3.2.1",
@ -92,7 +93,7 @@
"pg": "^8.9.0", "pg": "^8.9.0",
"proxy-addr": "^2.0.7", "proxy-addr": "^2.0.7",
"redis": "^4.6.10", "redis": "^4.6.10",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.2.1",
"saxon-js": "^2.5.0", "saxon-js": "^2.5.0",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"vuedraggable": "^4.1.0", "vuedraggable": "^4.1.0",

View File

@ -1,5 +1,7 @@
module.exports = { module.exports = {
plugins: { plugins: {
// 'postcss-import': {},
'tailwindcss/nesting': {},
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
}, },

View File

@ -14,6 +14,101 @@
@import '@fontsource/inter/index.css'; @import '@fontsource/inter/index.css';
@import '@fontsource/archivo-black/index.css'; @import '@fontsource/archivo-black/index.css';
:root {
--color-main-background: #ffffff;
--color-main-background-rgb: 255,255,255;
--color-main-background-translucent: rgba(var(--color-main-background-rgb), .97);
--color-main-background-blur: rgba(var(--color-main-background-rgb), .8);
--filter-background-blur: blur(25px);
--gradient-main-background: var(--color-main-background) 0%, var(--color-main-background-translucent) 85%, transparent 100%;
--color-background-hover: #f5f5f5;
/** Can be used e.g. to colorize selected table rows */
--color-background-dark: #ededed;
/** This should only be used for elements, not as a text background! Otherwise it will not work for accessibility. */
--color-background-darker: #dbdbdb;
--color-placeholder-light: #e6e6e6;
--color-placeholder-dark: #cccccc;
--color-main-text: #222222;
--color-text-maxcontrast: #6b6b6b;
--color-text-maxcontrast-default: #6b6b6b;
--color-text-maxcontrast-background-blur: #595959;
/** @deprecated use ` --color-main-text` instead */
--color-text-light: var(--color-main-text);
/** @deprecated use `--color-text-maxcontrast` instead */
--color-text-lighter: var(--color-text-maxcontrast);
--color-scrollbar: rgba(34,34,34, .15);
--color-error: #d91812;
--color-error-rgb: 217,24,18;
--color-error-hover: #dd342f;
--color-error-text: #c61610;
--color-warning: #b88100;
--color-warning-rgb: 184,129,0;
--color-warning-hover: #c69a32;
--color-warning-text: #855d00;
--color-success: #2d7b41;
--color-success-rgb: 45,123,65;
--color-success-hover: #448955;
--color-success-text: #286c39;
--color-info: #0071ad;
--color-info-rgb: 0,113,173;
--color-info-hover: #197fb5;
--color-info-text: #006499;
--color-loading-light: #cccccc;
--color-loading-dark: #444444;
--color-box-shadow-rgb: 77,77,77;
--color-box-shadow: rgba(var(--color-box-shadow-rgb), 0.5);
--color-border: #ededed;
--color-border-dark: #dbdbdb;
--color-border-maxcontrast: #949494;
--font-face: system-ui, -apple-system, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, Ubuntu, 'Helvetica Neue', 'Noto Sans', 'Liberation Sans', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--default-font-size: 15px;
--animation-quick: 100ms;
--animation-slow: 300ms;
--border-radius: 3px;
--border-radius-large: 10px;
--border-radius-rounded: 28px;
--border-radius-pill: 100px;
--default-clickable-area: 44px;
--clickable-area: 44;
--default-line-height: 24px;
--default-grid-baseline: 4px;
--header-height: 50px;
--navigation-width: 300px;
--sidebar-min-width: 300px;
--sidebar-max-width: 500px;
--list-min-width: 200px;
--list-max-width: 300px;
--header-menu-item-height: 44px;
--header-menu-profile-item-height: 66px;
--breakpoint-mobile: 1024px;
--background-invert-if-dark: no;
--background-invert-if-bright: invert(100%);
--background-image-invert-if-bright: no;
--primary-invert-if-bright: no;
--primary-invert-if-dark: invert(100%);
--color-primary: #00679e;
--color-primary-default: #0082c9;
--color-primary-text: #ffffff;
--color-primary-hover: #3285b1;
--color-primary-light: #e5eff5;
--color-primary-light-text: #00293f;
--color-primary-light-hover: #dbe4ea;
/* --color-primary-element: #00679e; */
--color-primary-element: #bfce40;
/* --color-primary-element-hover: #052E37; */
--color-primary-element-hover: rgba(5,46,55,0.7);
--color-primary-element-text: #ffffff;
--color-primary-element-text-dark: #f5f5f5;
--color-primary-element-light: #e5eff5;
--color-primary-element-light-hover: #dbe4ea;
--color-primary-element-light-text: #00293f;
--gradient-primary-background: linear-gradient(40deg, var(--color-primary) 0%, var(--color-primary-hover) 100%);
/* --image-background-default: url('./img/background/kamil-porembinski-clouds.jpg'); */
--color-background-plain: #0082c9;
--radius: 15;
--pi: 3.14159265358979;
}
/* @layer base { /* @layer base {
html, html,
body { body {

View File

@ -0,0 +1,312 @@
<!--
- @copyright Copyright (c) 2023 Arno Kaimbacher <arno.kaimbacher@outlook.at>
-
- @author Arno Kaimbacher <arno.kaimbacher@outlook.at>
-
- @license GNU AGPL version 3 or any later version
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-->
<template>
<NcModal v-if="showModal" id="firstrunwizard" class="first-run-wizard" size="normal" :has-next="hasNext"
:has-previous="hasPrevious" @close="close" @next="goToNextPage" @previous="goToPreviousPage">
<!-- page 0 for entry -->
<Page0 v-if="page === 0" @next="goToNextPage" />
<div v-else class="first-run-wizard__wrapper">
<Transition :name="circleSlideDirection">
<div v-if="page === 1" class="first-run-wizard__background-circle" />
</Transition>
<div class="first-run-wizard__background-bar rounded-t-xl" />
<!-- previous header button -->
<NcButton v-if="page > 1" type="tertiary" class="first-run-wizard__back-button"
aria-label="'Go to previous page'" @click="goToPreviousPage" :icon="mdiArrowLeft" :size="20"
:rounded-full="true">
</NcButton>
<!-- close header button -->
<NcButton :type="page === 1 ? 'primary' : 'tertiary'" class="first-run-wizard__close-button"
aria-label="'Close dialog'" @click="close" :icon="mdiClose" :size="20" :rounded-full="true">
</NcButton>
<!-- show logo on first page -->
<div v-if="page === 1" class="first-run-wizard__logo"> <!-- :style="logoStyle" -->
<JustboilLogo class="w-auto h-16" />
</div>
<div v-if="page === 1" class="first-run-wizard__logo" />
<Transition :name="pageSlideDirection" mode="out-in">
<Page1 v-if="page === 1" />
<Page2 v-else-if="page === 2" />
<Page3 v-else-if="page === 3" />
</Transition>
<!-- last wider button at footer for every page -->
<NcButton type="primary" alignment="center-reverse" :wide="true" @click="handleButtonCLick" :label="buttonText"
:icon="mdiArrowRight" :size="20">
</NcButton>
</div>
</NcModal>
</template>
<script lang="ts">
// import { NcModal, BaseButton } from '@nextcloud/vue';
import NcModal from '@/Components/NcModal.vue';
import BaseButton from '@/Components/BaseButton.vue';
import NcButton from '@/Components/NcButton.vue';
// import { imagePath, generateUrl } from '@nextcloud/router';
// import axios from '@nextcloud/axios';
import JustboilLogo from '@/Components/JustboilLogo.vue';
import Page0 from './components/Page0.vue';
import Page1 from './components/Page1.vue';
import Page2 from './components/Page2.vue';
import Page3 from './components/Page3.vue';
import { mdiClose, mdiArrowRight, mdiArrowLeft } from '@mdi/js';
export default {
name: 'FirstrunWizard',
components: {
NcModal,
Page0,
Page1,
Page2,
BaseButton,
NcButton,
Page3,
JustboilLogo
},
data() {
return {
showModal: false,
page: 1,
// logoURL: '/logo.svg',// imagePath('firstrunwizard', 'nextcloudLogo.svg'),
pageSlideDirection: undefined,
circleSlideDirection: undefined,
mdiClose: mdiClose,
mdiArrowRight: mdiArrowRight,
mdiArrowLeft,
};
},
computed: {
// logoStyle() {
// // return { backgroundImage: 'url(' + this.logoURL + ')' };
// return { backgroundImage: 'url(' + this.logoURL + ')' };
// },
hasPrevious() {
if (window.innerWidth <= 512) {
return false;
} else {
return this.page > 1;
}
},
hasNext() {
if (window.innerWidth <= 512) {
return false;
} else {
return this.page < 3;
}
},
buttonText() {
if (this.page === 1) {
return 'TethysCloud on all your devices';
} else if (this.page === 2) {
return 'More about TethysCloud';
} else if (this.page === 3) {
return 'Get started!';
}
return '';
},
},
methods: {
open() {
this.page = 1;
this.showModal = true;
},
close() {
this.page = 1;
this.showModal = false;
// axios.delete(generateUrl('/apps/firstrunwizard/wizard'));
},
goToNextPage() {
this.pageSlideDirection = 'slide-left';
if (this.page === 1) {
this.circleSlideDirection = 'slide-up';
}
this.$nextTick(() => {
this.page++;
});
},
goToPreviousPage() {
this.pageSlideDirection = 'slide-right';
if (this.page === 2) {
this.circleSlideDirection = 'slide-down';
}
this.$nextTick(() => {
this.page--;
});
},
handleButtonCLick() {
if (this.page < 3) {
this.goToNextPage();
} else {
this.close();
}
},
},
};
</script>
<style lang="css" scoped>
.first-run-wizard {
&__wrapper {
position: relative;
overflow: hidden;
padding: calc(var(--default-grid-baseline) * 5);
display: flex;
flex-direction: column;
justify-content: space-between;
}
&__background-circle {
height: 6000px;
width: 6000px;
border-radius: 3000px;
background-color: var(--color-primary-element);
position: absolute;
top: -5900px;
left: calc(-3000px + 50%);
}
&__background-bar {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 10px;
background-color: var(--color-primary-element);
}
&__back-button {
position: absolute;
top: var(--default-grid-baseline);
left: var(--default-grid-baseline);
}
&__close-button {
position: absolute;
top: var(--default-grid-baseline);
right: var(--default-grid-baseline);
}
&__logo {
pointer-events: none;
position: absolute;
top: 40px;
/* top: 50%; */
left: 50%;
transform: translate(-50%, -50%);
color: var(--color-primary-element-text);
}
}
.modal-wrapper .modal-container {
overflow: hidden;
/* max-width: 90%;
width: 600px;
max-height: min(90%, 100% - 100px); */
}
.modal-wrapper .modal-container__content {
overflow: hidden;
height: 100%;
display: contents;
}
@media only screen and (max-width: 512px) {
:deep(.modal-wrapper .modal-container) {
height: 100dvh;
top: 0;
}
:deep(.modal-header) {
pointer-events: none;
}
}
/* hide close button predefined by NCModal.vue */
:deep(.modal-container__close) {
display: none;
}
.slide-right-enter-active,
.slide-right-leave-active,
.slide-left-enter-active,
.slide-left-leave-active,
.slide-up-enter-active,
.slide-up-leave-active,
.slide-down-enter-active,
.slide-down-leave-active {
transition: all 0.2s;
}
.slide-left-enter {
opacity: 0;
transform: translateX(30%);
}
.slide-left-leave-to {
opacity: 0;
transform: translateX(-30%);
}
.slide-right-enter {
opacity: 0;
transform: translateX(-30%);
}
.slide-right-leave-to {
opacity: 0;
transform: translateX(30%);
}
.slide-up-enter {
top: calc(-5900px);
}
.slide-up-leave-to {
top: calc(-5900px - 80px);
}
.slide-down-enter {
top: calc(-5900px - 80px);
}
.slide-down-leave-to {
top: calc(-5900px);
}
</style>

View File

@ -1,8 +1,7 @@
<!-- <!--
- @copyright Copyright (c) 2023 Marco Ambrosini <marcoambrosini@proton.me> - @copyright Copyright (c) 2023 Arno Kaimbacher <arno.kaimbacher@outlook.at>
- -
- @author Simon Lindner <szaimen@e.mail.de> - @author Arno Kaimbacher <arno.kaimbacher@outlook.at>
- @author Marco Ambrosini <marcoambrosini@proton.me>
- -
- @license GNU AGPL version 3 or any later version - @license GNU AGPL version 3 or any later version
- -
@ -21,18 +20,18 @@
- -
--> -->
<template> <template>
<element :is="isLink ? 'a' : 'div'" :href="href || undefined" class="card" :class="{ 'card--link': isLink }" <component :is="isLink ? 'a' : 'div'" :href="href || undefined" class="card" :class="{ 'card--link': isLink }"
:target="!isLink ? undefined : '_blank'" :rel="!isLink ? undefined : 'noreferrer'"> :target="!isLink ? undefined : '_blank'" :rel="!isLink ? undefined : 'noreferrer'">
<div v-if="!isLink" class="card__icon"> <div v-if="!isLink" class="card__icon">
<slot /> <slot />
</div> </div>
<div class="card__text"> <div class="card__text">
<h3 class="card__heading"> <h3 class="text-base text-ellipsis card__heading">
{{ title }} {{ title }}
</h3> </h3>
<p>{{ subtitle }}</p> <p>{{ subtitle }}</p>
</div> </div>
</element> </component>
</template> </template>
<script> <script>

View File

@ -0,0 +1,85 @@
<template>
<!--<div class="video-wrapper">
<video ref="video" playsinline autoplay muted @ended="handleEnded">
<source :src="videoWebm" type="video/webm" />
<source :src="videoMp4" type="video/mp4" />
{{ videoFallbackText }}
</video>
</div>-->
<div id="page__wrapper" class="flex flex-col justify-between min-h-520">
<div class="page__scroller">
<h2 id="page__heading" class="text-xl font-bold leading-tight dark:text-slate-400 text-center">
{{ 'Seamless integration with your devices.' }}
</h2>
<!-- <p id="page__subtitle" class="max-w-md m-auto text-center">
{{ subtitleText }}
</p> -->
<div class="page__content">
Discover the power of Tethys, the cutting-edge web backend solution that revolutionizes the way you handle
research
data. At the heart of Tethys lies our meticulously developed research data repository, which leverages
state-of-the-art
CI/CD techniques to deliver a seamless and efficient experience.
</div>
</div>
</div>
</template>
<script lang="ts">
// import { generateFilePath } from '@nextcloud/router';
import CardBox from '@/Components/CardBox.vue';
export default {
name: 'Page0',
components: {
CardBox
},
data() {
return {
// videoMp4: generateFilePath('firstrunwizard', 'img', 'Nextcloud.mp4'),
// videoWebm: generateFilePath('firstrunwizard', 'img', 'Nextcloud.webm'),
};
},
computed: {
// videoFallbackText() {
// return 'Welcome to {cloudName}!', { cloudName: "test" };
// },
},
methods: {
handleEnded() {
this.$emit('next');
},
},
};
</script>
<style scoped lang="css">
/* video {
width: 100%;
height: 100%;
object-fit: cover;
} */
.video-wrapper {
background-color: var(--color-primary-element);
}
.page {
&__scroller {
/* overflow-y: scroll; */
margin-top: calc(var(--default-grid-baseline) * 8);
}
&__content {
display: flex;
flex-wrap: wrap;
gap: calc(var(--default-grid-baseline) * 6);
justify-content: center;
margin: calc(var(--default-grid-baseline) * 10) 0;
}
}
</style>

View File

@ -1,24 +1,38 @@
<template> <template>
<div class="page__wrapper"> <div id="page__wrapper" class="flex flex-col justify-between min-h-520">
<div class="page__scroller first-page"> <div class="page__scroller first-page">
<h2 class="page__heading"> <h2 id="page__heading" class="text-xl font-bold leading-tight dark:text-slate-400 text-center">
{{ t('firstrunwizard', 'A collaboration platform that puts you in control') }} {{ 'A researchdata platform that puts you in control' }}
</h2> </h2>
<div class="page__content"> <div class="page__content">
<Card :title="t('firstrunwizard', 'Privacy')" <!-- <Card :title="'Privacy'"
:subtitle="t('firstrunwizard', 'Host your data and files where you decide.')"> :subtitle="'Host your data and files where you decide.'">
<Lock :size="20" /> <Lock :size="20" />
</Card> </Card>
<Card :title="t('firstrunwizard', 'Productivity')" <Card :title="'Productivity'"
:subtitle="t('firstrunwizard', 'Collaborate and communicate across any platform.')"> :subtitle="'Collaborate and communicate across any platform.'">
<BriefcaseCheck :size="20" /> <BriefcaseCheck :size="20" />
</Card> </Card> -->
<Card :title="t('firstrunwizard', 'Interoperability')" <div class="max-w-60 h-fit box-border flex">
:subtitle="t('firstrunwizard', 'Import and export anything you want with open standards.')">
<!-- <div class="card__icon">
<BriefcaseCheck :size="20" />
</div> -->
<div>
Discover the power of TethysCloud, the cutting-edge web backend solution that revolutionizes the way you
handle
research
data. At the heart of Tethys lies our meticulously developed research data repository, which
leverages
state-of-the-art
CI/CD techniques to deliver a seamless and efficient experience.
</div>
</div>
<Card :title="'Interoperability'" :subtitle="'Import and export anything you want with open standards.'">
<SwapHorizontal :size="20" /> <SwapHorizontal :size="20" />
</Card> </Card>
<Card :title="t('firstrunwizard', 'Community')" <Card :title="'Community'" :subtitle="'Enjoy constant improvements from a thriving open-source community.'">
:subtitle="t('firstrunwizard', 'Enjoy constant improvements from a thriving open-source community.')">
<AccountGroup :size="20" /> <AccountGroup :size="20" />
</Card> </Card>
</div> </div>
@ -26,21 +40,18 @@
</div> </div>
</template> </template>
<script> <script lang="ts">
import Card from './Card.vue' import Card from './Card.vue'
import Lock from 'vue-material-design-icons/Lock.vue' import SwapHorizontal from '@/Components/Icons/SwapHorizontal.vue';
import BriefcaseCheck from 'vue-material-design-icons/BriefcaseCheck.vue' import AccountGroup from '@/Components/Icons/AccountGroup.vue'
import SwapHorizontal from 'vue-material-design-icons/SwapHorizontal.vue'
import AccountGroup from 'vue-material-design-icons/AccountGroup.vue'
export default { export default {
name: 'Page1', name: 'Page1',
components: { components: {
Card, Card,
Lock,
BriefcaseCheck,
SwapHorizontal, SwapHorizontal,
AccountGroup, AccountGroup,
}, },
@ -48,7 +59,25 @@ export default {
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
/* @import "pageStyles"; */ .page {
&__scroller {
/* overflow-y: scroll; */
margin-top: calc(var(--default-grid-baseline) * 8);
}
&__content {
display: flex;
flex-wrap: wrap;
gap: calc(var(--default-grid-baseline) * 6);
justify-content: center;
margin: calc(var(--default-grid-baseline) * 10) 0;
}
}
.card__icon {
display: flex;
flex: 0 0 44px;
align-items: center;
}
.first-page { .first-page {
margin-top: 100px; margin-top: 100px;

View File

@ -0,0 +1,88 @@
<!--
- @copyright Copyright (c) 2023 Arno Kaimbacher <arno.kaimbacher@outlook.at>
-
- @author Marco Ambrosini <arno.kaimbacher@outlook.at>
-
- @license GNU AGPL version 3 or any later version
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-->
<template>
<div id="page__wrapper" class="flex flex-col justify-between min-h-520">
<div class="page__scroller">
<h2 id="page__heading" class="text-xl font-bold leading-tight dark:text-slate-400 text-center">
{{ 'Seamless integration with your devices.' }}
</h2>
<p id="page__subtitle" class="max-w-md m-auto text-center">
{{ subtitleText }}
</p>
<div class="page__content">
<!-- <AppStoreBadge type="android" />
<AppStoreBadge type="ios" /> -->
<Card :href="desktop" :title="'Desktop app ↗'" :subtitle="'Download For Windows, Mac OS and Linux.'" />
<!-- <Card :href="syncClientsUrl" :title="'Calendar and contacts ↗'"
:subtitle="'Connect your calendar and contacts with your devices.'" /> -->
</div>
</div>
</div>
</template>
<script lang="ts">
import Card from './Card.vue';
// import AppStoreBadge from './AppStoreBadge.vue';
// import { generateUrl } from '@nextcloud/router';
import { loadState } from '@/utils/initialState';
const desktop = loadState('firstrunwizard', 'desktop');
export default {
name: 'Page2',
components: {
Card,
// AppStoreBadge,
},
data() {
return {
subtitleText: 'Sync your files across your devices with the desktop and mobile apps, and connect your calendar and contacts.',
// syncClientsUrl: generateUrl('settings/user/sync-clients'),
desktop,
};
},
};
</script>
<style lang="css" scoped>
/* @import 'pageStyles'; */
.page {
&__scroller {
/* overflow-y: scroll; */
margin-top: calc(var(--default-grid-baseline) * 8);
}
&__content {
display: flex;
flex-wrap: wrap;
gap: calc(var(--default-grid-baseline) * 6);
justify-content: center;
margin: calc(var(--default-grid-baseline) * 10) 0;
}
}
</style>

View File

@ -0,0 +1,68 @@
<template>
<div id="page__wrapper" class="flex flex-col justify-between min-h-520">
<div class="page__scroller">
<h2 id="page__heading" class="text-xl font-bold leading-tight dark:text-slate-400 text-center">
{{ 'More about TethysCloud' }}
</h2>
<div id="page__content" class="flex flex-wrap justify-center page__content">
<Card href="https://gitea.geologie.ac.at/geolba" :title="'Explore more apps ↗'"
:subtitle="'Extend the functionality of TethysCloud.'" />
<Card href="https://gitea.geologie.ac.at/geolba" :title="'Get involved! ↗'"
:subtitle="'Be a part of the community that helps build, design, translate and promote TethysCloud!'" />
<Card href="https://gitea.geologie.ac.at/geolba" :title="'Need help? ↗'"
:subtitle="'Find out more about your TethysCloud setup with the admin, user or developer documentation.'" />
<Card href="https://gitea.geologie.ac.at/geolba" :title="'For large organisations ↗'"
:subtitle="'Get TethysCloud Enterprise for mission critical environments where advanced security and compliance are important.'" />
</div>
<p class="version-number">
{{ versionNumber }}
</p>
</div>
</div>
</template>
<script>
import Card from './Card.vue'
export default {
name: 'Page3',
components: {
Card,
},
computed: {
versionNumber() {
return 'This TethysCloud is on version 2.0.0'; // + OC.config.versionstring
},
},
}
</script>
<style lang="css" scoped>
.page {
&__scroller {
/* overflow-y: scroll; */
margin-top: calc(var(--default-grid-baseline) * 8);
}
/* &__content {
gap: calc(var(--default-grid-baseline) * 6);
margin: calc(var(--default-grid-baseline) * 10) 0;
} */
}
.page__content {
gap: calc(var(--default-grid-baseline) * 6);
margin: calc(var(--default-grid-baseline) * 6) 0 calc(var(--default-grid-baseline) * 4) 0;
}
.version-number {
margin: 0px 0 calc(var(--default-grid-baseline) * 4) 0;
color: var(--color-text-maxcontrast);
text-align: center;
}
</style>

View File

@ -0,0 +1,31 @@
<template>
<span v-bind="$attrs" :aria-hidden="!title" :aria-label="title" class="material-design-icon dots-horizontal-icon"
role="img" @click="$emit('click', $event)">
<svg :fill="fillColor" class="material-design-icon__svg" :width="size" :height="size" viewBox="0 0 24 24">
<path
d="M16,12A2,2 0 0,1 18,10A2,2 0 0,1 20,12A2,2 0 0,1 18,14A2,2 0 0,1 16,12M10,12A2,2 0 0,1 12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12M4,12A2,2 0 0,1 6,10A2,2 0 0,1 8,12A2,2 0 0,1 6,14A2,2 0 0,1 4,12Z">
<title v-if="title">{{ title }}</title>
</path>
</svg>
</span>
</template>
<script>
export default {
name: "DotsHorizontalIcon",
emits: ['click'],
props: {
title: {
type: String,
},
fillColor: {
type: String,
default: "currentColor"
},
size: {
type: Number,
default: 24
}
}
}
</script>

View File

@ -0,0 +1,30 @@
<template>
<span v-bind="$attrs" :aria-hidden="!title" :aria-label="title" class="material-design-icon pause-icon" role="img"
@click="$emit('click', $event)">
<svg :fill="fillColor" class="material-design-icon__svg" :width="size" :height="size" viewBox="0 0 24 24">
<path d="M14,19H18V5H14M6,19H10V5H6V19Z">
<title v-if="title">{{ title }}</title>
</path>
</svg>
</span>
</template>
<script>
export default {
name: "PauseIcon",
emits: ['click'],
props: {
title: {
type: String,
},
fillColor: {
type: String,
default: "currentColor"
},
size: {
type: Number,
default: 24
}
}
}
</script>

View File

@ -0,0 +1,30 @@
<template>
<span v-bind="$attrs" :aria-hidden="!title" :aria-label="title" class="material-design-icon play-icon" role="img"
@click="$emit('click', $event)">
<svg :fill="fillColor" class="material-design-icon__svg" :width="size" :height="size" viewBox="0 0 24 24">
<path d="M8,5.14V19.14L19,12.14L8,5.14Z">
<title v-if="title">{{ title }}</title>
</path>
</svg>
</span>
</template>
<script>
export default {
name: "PlayIcon",
emits: ['click'],
props: {
title: {
type: String,
},
fillColor: {
type: String,
default: "currentColor"
},
size: {
type: Number,
default: 24
}
}
}
</script>

View File

@ -27,7 +27,8 @@ import {
mdiGithub, mdiGithub,
mdiThemeLightDark, mdiThemeLightDark,
mdiViewDashboard, mdiViewDashboard,
mdiMapSearch mdiMapSearch,
mdiInformationVariant,
} from '@mdi/js'; } from '@mdi/js';
import NavBarItem from '@/Components/NavBarItem.vue'; import NavBarItem from '@/Components/NavBarItem.vue';
import NavBarItemLabel from '@/Components/NavBarItemLabel.vue'; import NavBarItemLabel from '@/Components/NavBarItemLabel.vue';
@ -38,8 +39,8 @@ import BaseIcon from '@/Components/BaseIcon.vue';
// import NavBarSearch from '@/Components/NavBarSearch.vue'; // import NavBarSearch from '@/Components/NavBarSearch.vue';
import { stardust } from '@eidellev/adonis-stardust/client'; import { stardust } from '@eidellev/adonis-stardust/client';
import type { User } from '@/Dataset'; import type { User } from '@/Dataset';
// import FirstrunWizard from '@/Components/FirstrunWizard/FirstrunWizard.vue' import FirstrunWizard from '@/Components/FirstrunWizard/FirstrunWizard.vue'
import Lock from 'vue-material-design-icons/Lock.vue' // import Lock from 'vue-material-design-icons/Lock.vue'
// import BriefcaseCheck from 'vue-material-design-icons/BriefcaseCheck.vue' // import BriefcaseCheck from 'vue-material-design-icons/BriefcaseCheck.vue'
// import SwapHorizontal from 'vue-material-design-icons/SwapHorizontal.vue' // import SwapHorizontal from 'vue-material-design-icons/SwapHorizontal.vue'
// import AccountGroup from 'vue-material-design-icons/AccountGroup.vue' // import AccountGroup from 'vue-material-design-icons/AccountGroup.vue'
@ -89,11 +90,20 @@ const logout = async () => {
await router.post(stardust.route('logout')); await router.post(stardust.route('logout'));
}; };
const about = ref();
const showAbout = async () => {
// router.post(route('logout'));
about.value.open();
};
</script> </script>
<template> <template>
<nav class="text-base top-0 left-0 right-0 fixed bg-gray-50 h-14 z-40 w-screen transition-position lg:w-auto dark:bg-slate-800" <nav class="text-base top-0 left-0 right-0 fixed bg-gray-50 h-14 z-40 w-screen transition-position lg:w-auto dark:bg-slate-800"
:class="{ 'xl:pl-60': props.showBurger == true }"> :class="{ 'xl:pl-60': props.showBurger == true }">
<FirstrunWizard ref="about"></FirstrunWizard>
<div class="flex lg:items-stretch" :class="containerMaxW"> <div class="flex lg:items-stretch" :class="containerMaxW">
<div class="flex-1 items-stretch flex h-14"> <div class="flex-1 items-stretch flex h-14">
<NavBarItem type="flex lg:hidden" @click.prevent="layoutStore.asideMobileToggle()" v-if="props.showBurger"> <NavBarItem type="flex lg:hidden" @click.prevent="layoutStore.asideMobileToggle()" v-if="props.showBurger">
@ -165,6 +175,9 @@ const logout = async () => {
<!-- <NavBarItem> <!-- <NavBarItem>
<NavBarItemLabel :icon="mdiEmail" label="Messages" /> <NavBarItemLabel :icon="mdiEmail" label="Messages" />
</NavBarItem> --> </NavBarItem> -->
<NavBarItem @click="showAbout">
<NavBarItemLabel :icon="mdiInformationVariant" label="About" />
</NavBarItem>
<BaseDivider nav-bar /> <BaseDivider nav-bar />
<NavBarItem @click="logout"> <NavBarItem @click="logout">
<NavBarItemLabel :icon="mdiLogout" label="Log Out" /> <NavBarItemLabel :icon="mdiLogout" label="Log Out" />
@ -178,9 +191,12 @@ const logout = async () => {
<NavBarItem href="https://gitea.geologie.ac.at/geolba/tethys" target="_blank" is-desktop-icon-only> <NavBarItem href="https://gitea.geologie.ac.at/geolba/tethys" target="_blank" is-desktop-icon-only>
<NavBarItemLabel v-bind:icon="mdiGithub" label="GitHub" is-desktop-icon-only /> <NavBarItemLabel v-bind:icon="mdiGithub" label="GitHub" is-desktop-icon-only />
</NavBarItem> </NavBarItem>
<NavBarItem is-desktop-icon-only @click="showAbout">
<NavBarItemLabel v-bind:icon="mdiInformationVariant" label="About" is-desktop-icon-only />
</NavBarItem>
<NavBarItem is-desktop-icon-only @click="logout"> <NavBarItem is-desktop-icon-only @click="logout">
<NavBarItemLabel v-bind:icon="mdiLogout" label="Log out" is-desktop-icon-only /> <NavBarItemLabel v-bind:icon="mdiLogout" label="Log out" is-desktop-icon-only />
</NavBarItem> </NavBarItem>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,774 @@
<script lang="ts">
// import NcButton from '../NcButton/index.js'
import BaseButton from '@/Components/BaseButton.vue';
// import NcPopover from '../NcPopover/index.js'
import GenRandomId from '../utils/GenRandomId';
// import { t } from '../../l10n.js'
import { computed } from 'vue';
import DotsHorizontal from '@/Components/Icons/DotsHorizontal.vue';
const focusableSelector = '.focusable';
/**
* The Actions component can be used to display one ore more actions.
* If only a single action is provided, it will be rendered as an inline icon.
* For more, a menu indicator will be shown and a popovermenu containing the
* actions will be opened on click.
*
* @since 0.10.0
*/
export default {
name: 'NcActions',
components: {
// NcButton,
BaseButton,
DotsHorizontal,
// NcPopover,
},
provide() {
return {
/**
* NcActions can be used as:
* - Application menu (has menu role)
* - Navigation (has no specific role, should be used an element with navigation role)
* - Popover with plain text or text inputs (has no specific role)
* Depending on the usage (used items), the menu and its items should have different roles for a11y.
* Provide the role for NcAction* components in the NcActions content.
* @type {import('vue').ComputedRef<boolean>}
*/
'NcActions:isSemanticMenu': computed(() => this.isSemanticMenu),
};
},
props: {
/**
* Specify the open state of the popover menu
*/
open: {
type: Boolean,
default: false,
},
/**
* This disables the internal open management,
* so the actions menu only respects the `open` prop.
* This is e.g. necessary for the NcAvatar component
* to only open the actions menu after loading it's entries has finished.
*/
manualOpen: {
type: Boolean,
default: false,
},
/**
* Force the actions to display in a three dot menu
*/
forceMenu: {
type: Boolean,
default: false,
},
/**
* Force the name to show for single actions
*/
forceName: {
type: Boolean,
default: false,
},
/**
* Specify the menu name
*/
menuName: {
type: String,
default: null,
},
/**
* Apply primary styling for this menu
*/
primary: {
type: Boolean,
default: false,
},
/**
* Specifies the button type used for trigger and single actions buttons
* Accepted values: primary, secondary, tertiary, tertiary-no-background, tertiary-on-primary, error, warning, success. If left empty,
* the default button style will be applied.
*/
type: {
type: String,
validator(value: string) {
return (
[
'primary',
'secondary',
'tertiary',
'tertiary-no-background',
'tertiary-on-primary',
'error',
'warning',
'success',
].indexOf(value) !== -1
);
},
default: null,
},
/**
* Icon to show for the toggle menu button
* when more than one action is inside the actions component.
* Only replace the default three-dot icon if really necessary.
*/
defaultIcon: {
type: String,
default: '',
},
/**
* Aria label for the actions menu.
*
* If `menuName` is defined this will not be used to prevent
* any accessible name conflicts. This ensures that the
* element can be activated via voice input.
*/
ariaLabel: {
type: String,
default: 'Actions',
},
/**
* @deprecated To be removed in @nextcloud/vue 9. Migration guide: remove ariaHidden prop from NcAction* components.
* @todo Add a check in @nextcloud/vue 9 that this prop is not provided,
* otherwise root element will inherit incorrect aria-hidden.
*/
ariaHidden: {
type: Boolean,
default: null,
},
/**
* Wanted direction of the menu
*/
placement: {
type: String,
default: 'bottom',
},
/**
* DOM element for the actions' popover boundaries
*/
boundariesElement: {
type: Element,
default: () => document.querySelector('body'),
},
/**
* Selector for the actions' popover container
*/
container: {
type: [String, Object, Element, Boolean],
default: 'body',
},
/**
* Disabled state of the main button (single action or menu toggle)
*/
disabled: {
type: Boolean,
default: false,
},
/**
* Display x items inline out of the dropdown menu
* Will be ignored if `forceMenu` is set
*/
inline: {
type: Number,
default: 0,
},
},
emits: ['open', 'update:open', 'close', 'focus', 'blur'],
data() {
return {
opened: this.open,
focusIndex: 0,
randomId: `menu-${GenRandomId()}`,
isSemanticMenu: false,
isSemanticNavigation: false,
isSemanticPopoverLike: false,
};
},
computed: {
triggerBtnType() {
// If requested, we use a primary button
return (
this.type ||
(this.primary
? 'primary'
: // If it has a name, we use a secondary button
this.menuName
? 'secondary'
: 'tertiary')
);
},
},
watch: {
// Watch parent prop
open(state) {
if (state === this.opened) {
return;
}
this.opened = state;
},
},
methods: {
/**
* Do we have exactly one Action and
* is it allowed as a standalone element?
*
* @param {Array} action The action to check
* @return {boolean}
*/
isValidSingleAction(action) {
const componentName = action?.componentOptions?.Ctor?.extendOptions?.name ?? action?.componentOptions?.tag;
return ['NcActionButton', 'NcActionLink', 'NcActionRouter'].includes(componentName);
},
// MENU STATE MANAGEMENT
openMenu() {
if (this.opened) {
return;
}
this.opened = true;
/**
* Event emitted when the popover menu open state is changed
*
* @type {boolean}
*/
this.$emit('update:open', true);
/**
* Event emitted when the popover menu is opened
*/
this.$emit('open');
},
closeMenu(returnFocus = true) {
if (!this.opened) {
return;
}
this.opened = false;
this.$refs.popover.clearFocusTrap({ returnFocus });
/**
* Event emitted when the popover menu open state is changed
*
* @type {boolean}
*/
this.$emit('update:open', false);
/**
* Event emitted when the popover menu is closed
*/
this.$emit('close');
// close everything
this.focusIndex = 0;
// focus back the menu button
this.$refs.menuButton.$el.focus();
},
onOpen(event) {
this.$nextTick(() => {
this.focusFirstAction(event);
});
},
// MENU KEYS & FOCUS MANAGEMENT
// focus nearest focusable item on mouse move
// DO NOT change the focus if the target is already focused
// this will prevent issues with input being unfocused
// on mouse move
onMouseFocusAction(event) {
if (document.activeElement === event.target) {
return;
}
const menuItem = event.target.closest('li');
if (menuItem && this.$refs.menu.contains(menuItem)) {
const focusableItem = menuItem.querySelector(focusableSelector);
if (focusableItem) {
const focusList = this.$refs.menu.querySelectorAll(focusableSelector);
const focusIndex = [...focusList].indexOf(focusableItem);
if (focusIndex > -1) {
this.focusIndex = focusIndex;
this.focusAction();
}
}
}
},
/**
* Dispatches the keydown listener to different handlers
*
* @param {object} event The keydown event
*/
onKeydown(event) {
if (event.key === 'Tab' && !this.isSemanticPopoverLike) {
this.closeMenu(false);
}
if (event.key === 'ArrowUp') {
this.focusPreviousAction(event);
}
if (event.key === 'ArrowDown') {
this.focusNextAction(event);
}
if (event.key === 'PageUp') {
this.focusFirstAction(event);
}
if (event.key === 'PageDown') {
this.focusLastAction(event);
}
if (event.key === 'Escape') {
this.closeMenu();
event.preventDefault();
}
},
removeCurrentActive() {
const currentActiveElement = this.$refs.menu.querySelector('li.active');
if (currentActiveElement) {
currentActiveElement.classList.remove('active');
}
},
focusAction() {
// TODO: have a global disabled state for non input elements
const focusElement = this.$refs.menu.querySelectorAll(focusableSelector)[this.focusIndex];
if (focusElement) {
this.removeCurrentActive();
const liMenuParent = focusElement.closest('li.action');
focusElement.focus();
if (liMenuParent) {
liMenuParent.classList.add('active');
}
}
},
focusPreviousAction(event) {
if (this.opened) {
if (this.focusIndex === 0) {
this.focusLastAction(event);
} else {
this.preventIfEvent(event);
this.focusIndex = this.focusIndex - 1;
}
this.focusAction();
}
},
focusNextAction(event) {
if (this.opened) {
const indexLength = this.$refs.menu.querySelectorAll(focusableSelector).length - 1;
if (this.focusIndex === indexLength) {
this.focusFirstAction(event);
} else {
this.preventIfEvent(event);
this.focusIndex = this.focusIndex + 1;
}
this.focusAction();
}
},
focusFirstAction(event) {
if (this.opened) {
this.preventIfEvent(event);
// In case a button is considered aria-selected we will use this one as a initial focus
const firstSelectedIndex = [...this.$refs.menu.querySelectorAll(focusableSelector)].findIndex((button) => {
return button.parentElement.getAttribute('aria-selected');
});
this.focusIndex = firstSelectedIndex > -1 ? firstSelectedIndex : 0;
this.focusAction();
}
},
focusLastAction(event) {
if (this.opened) {
this.preventIfEvent(event);
this.focusIndex = this.$refs.menu.querySelectorAll(focusableSelector).length - 1;
this.focusAction();
}
},
preventIfEvent(event) {
if (event) {
event.preventDefault();
event.stopPropagation();
}
},
onFocus(event) {
this.$emit('focus', event);
},
onBlur(event) {
this.$emit('blur', event);
},
},
/**
* The render function to display the component
*
* @param {Function} h The function to create VNodes
* @return {object|undefined} The created VNode
*/
render(h) {
/**
* Filter the Actions, so that we only get allowed components.
* This also ensure that we don't get 'text' elements, which would
* become problematic later on.
*/
const actions = ([this.$slots.default] || []).filter(
(action) => action?.componentOptions?.tag || action?.componentOptions?.Ctor?.extendOptions?.name,
);
const getActionName = (action) => action?.componentOptions?.Ctor?.extendOptions?.name ?? action?.componentOptions?.tag;
const menuItemsActions = ['NcActionButton', 'NcActionButtonGroup', 'NcActionCheckbox', 'NcActionRadio'];
const textInputActions = ['NcActionInput', 'NcActionTextEditable'];
const linkActions = ['NcActionLink', 'NcActionRouter'];
const hasTextInputAction = actions.some((action) => textInputActions.includes(getActionName(action)));
const hasMenuItemAction = actions.some((action) => menuItemsActions.includes(getActionName(action)));
const hasLinkAction = actions.some((action) => linkActions.includes(getActionName(action)));
// We consider the NcActions to have role="menu" if it consists some button-like action and not text inputs
this.isSemanticMenu = hasMenuItemAction && !hasTextInputAction;
// We consider the NcActions to be navigation if it consists some link-like action
this.isSemanticNavigation = hasLinkAction && !hasMenuItemAction && !hasTextInputAction;
// If it is no a manu and not a navigation, it is a popover with items: a form or just a text
this.isSemanticPopoverLike = !this.isSemanticMenu && !this.isSemanticNavigation;
/**
* Filter and list actions that are allowed to be displayed inline
*/
let inlineActions = actions.filter(this.isValidSingleAction);
if (this.forceMenu && inlineActions.length > 0 && this.inline > 0) {
// Vue.util.warn('Specifying forceMenu will ignore any inline actions rendering.');
inlineActions = [];
}
// Check that we have at least one action
if (actions.length === 0) {
return;
}
/**
* Render the provided action
*
* @param {import('vue').VNode} action the action to render
* @return {Function} the vue render function
*/
const renderInlineAction = (action) => {
const icon =
action?.data?.scopedSlots?.icon()?.[0] || h('span', { class: ['icon', action?.componentOptions?.propsData?.icon] });
const attrs = action?.data?.attrs || {};
const clickListener = action?.componentOptions?.listeners?.click;
const text = action?.componentOptions?.children?.[0]?.text?.trim?.();
const ariaLabel = action?.componentOptions?.propsData?.ariaLabel || text;
const buttonText = this.forceName ? text : '';
let title = action?.componentOptions?.propsData?.title;
// Show a default title for single actions if none is present
if (!(this.forceName || title)) {
title = text;
}
return h(
'BaseButton',
{
class: ['action-item action-item--single', action?.data?.staticClass, action?.data?.class],
attrs: {
...attrs,
'aria-label': ariaLabel,
title,
},
ref: action?.data?.ref,
props: {
// If it has a menuName, we use a secondary button
type: this.type || (buttonText ? 'secondary' : 'tertiary'),
disabled: this.disabled || action?.componentOptions?.propsData?.disabled,
...action?.componentOptions?.propsData,
},
on: {
focus: this.onFocus,
blur: this.onBlur,
// If we have a click listener,
// we bind it to execute on click and forward the click event
...(!!clickListener && {
click: (event) => {
if (clickListener) {
clickListener(event);
}
},
}),
},
},
[h('template', { slot: 'icon' }, [icon]), buttonText],
);
};
/**
* Render the actions popover
*
* @param {Array} actions the actions to render within
* @return {Function} the vue render function
*/
const renderActionsPopover = (actions) => {
const triggerIcon =
this.$slots.icon?.[0] ||
(this.defaultIcon
? h('span', { class: ['icon', this.defaultIcon] })
: h('DotsHorizontal', {
props: {
size: 20,
},
}));
return h(
'NcPopover',
{
ref: 'popover',
props: {
delay: 0,
handleResize: true,
shown: this.opened,
placement: this.placement,
boundary: this.boundariesElement,
container: this.container,
popoverBaseClass: 'action-item__popper',
// Menu and navigation should not have focus trap
// Tab should close the menu and move focus to the next UI element
setReturnFocus: !this.isSemanticPopoverLike ? null : this.$refs.menuButton?.$el,
focusTrap: this.isSemanticPopoverLike,
},
// For some reason the popover component
// does not react to props given under the 'props' key,
// so we use both 'attrs' and 'props'
attrs: {
delay: 0,
handleResize: true,
shown: this.opened,
placement: this.placement,
boundary: this.boundariesElement,
container: this.container,
...(this.manualOpen && { triggers: [] }),
},
on: {
'show': this.openMenu,
'after-show': this.onOpen,
'hide': this.closeMenu,
},
},
[
h(
'BaseButton',
{
class: 'action-item__menutoggle',
props: {
type: this.triggerBtnType,
disabled: this.disabled,
},
slot: 'trigger',
ref: 'menuButton',
attrs: {
'aria-haspopup': this.isSemanticMenu ? 'menu' : null,
'aria-label': this.menuName ? null : this.ariaLabel,
'aria-controls': this.opened ? this.randomId : null,
'aria-expanded': this.opened ? 'true' : 'false',
},
on: {
focus: this.onFocus,
blur: this.onBlur,
},
},
[h('template', { slot: 'icon' }, [triggerIcon]), this.menuName],
),
h(
'div',
{
class: {
open: this.opened,
},
attrs: {
tabindex: '-1',
},
on: {
keydown: this.onKeydown,
mousemove: this.onMouseFocusAction,
},
ref: 'menu',
},
[
h(
'ul',
{
attrs: {
id: this.randomId,
tabindex: '-1',
role: this.isSemanticMenu ? 'menu' : undefined,
},
},
[actions],
),
],
),
],
);
};
/**
* If we have a single action only and didn't force a menu,
* we render the action as a standalone button
*/
if (actions.length === 1 && inlineActions.length === 1 && !this.forceMenu) {
return renderInlineAction(inlineActions[0]);
}
// If we completely re-render the children
// we need to focus the first action again
// Mostly used when clicking a menu item
this.$nextTick(() => {
if (this.opened && this.$refs.menu) {
const isAnyActive = this.$refs.menu.querySelector('li.active') || [];
if (isAnyActive.length === 0) {
this.focusFirstAction();
}
}
});
/**
* If we some inline actions to render, render them, then the menu
*/
if (inlineActions.length > 0 && this.inline > 0) {
const renderedInlineActions = inlineActions.slice(0, this.inline);
// Filter already rendered actions
const menuActions = actions.filter((action) => !renderedInlineActions.includes(action));
return h(
'div',
{
class: ['action-items', `action-item--${this.triggerBtnType}`],
},
[
// Render inline actions
...renderedInlineActions.map(renderInlineAction),
// render the rest within the popover menu
menuActions.length > 0
? h(
'div',
{
class: [
'action-item',
{
'action-item--open': this.opened,
},
],
},
[renderActionsPopover(menuActions)],
)
: null,
],
);
}
/**
* Otherwise, we render the actions in a popover
*/
return h(
'div',
{
class: [
'action-item action-item--default-popover',
`action-item--${this.triggerBtnType}`,
{
'action-item--open': this.opened,
},
],
},
[renderActionsPopover(actions)],
);
},
};
</script>
<style lang="css" scoped>
/* Inline buttons */
.action-items {
display: flex;
align-items: center;
/* Spacing between buttons */
/* &>button {
margin-right: math.div(28, 2);
} */
}
.action-item {
/* --open-background-color: var(--color-background-hover, $action-background-hover); */
position: relative;
display: inline-block;
&.action-item--primary {
--open-background-color: var(--color-primary-element-hover);
}
&.action-item--secondary {
--open-background-color: var(--color-primary-element-light-hover);
}
&.action-item--error {
--open-background-color: var(--color-error-hover);
}
&.action-item--warning {
--open-background-color: var(--color-warning-hover);
}
&.action-item--success {
--open-background-color: var(--color-success-hover);
}
&.action-item--tertiary-no-background {
--open-background-color: transparent;
}
&.action-item--open .action-item__menutoggle {
background-color: var(--open-background-color);
}
}
</style>
<style lang="css">
/* // We overwrote the popover base class, so we can style
// the popover__inner for actions only. */
.v-popper--theme-dropdown.v-popper__popper.action-item__popper .v-popper__wrapper {
border-radius: var(--border-radius-large);
overflow: hidden;
.v-popper__inner {
border-radius: var(--border-radius-large);
padding: 4px;
max-height: calc(50vh - 16px);
overflow: auto;
}
}
</style>

View File

@ -0,0 +1,544 @@
<template>
<component :is="is" :class="componentClass" :target="target" href="href" aria-label="ariaLabel" aria-pressed="pressed"
:disabled="disabled" :type="type" v-on:click="click">
<span class="button-vue__wrapper">
<!-- <span v-if="hasIcon" class="button-vue__icon" aria-hidden="true">
{{ this.$slots.icon }}-->
<BaseIcon v-if="icon" :path="icon" class="button-vue__icon" aria-hidden="true" :size="size" />
<!-- <span v-if="hasText" class="button-vue__text">{{ this.$slots.default }}</span> -->
<span v-if="label" class="font-bold whitespace-nowrap text-ellipsis overflow-hidden" :class="labelClass">{{
label }}</span>
</span>
</component>
</template>
<script lang="ts">
import BaseIcon from '@/Components/BaseIcon.vue';
// import { h } from 'vue';
export default {
name: 'NcButton',
components: {
BaseIcon
},
props: {
size: {
type: [String, Number],
default: 16,
},
small: Boolean,
roundedFull: {
type: Boolean,
default: false,
},
label: {
type: [String, Number],
default: null,
},
icon: {
type: String,
default: null,
},
/**
* Set the text and icon alignment
*
* @default 'center'
*/
alignment: {
type: String,
default: 'center',
validator: (alignment: string) => ['start', 'start-reverse', 'center', 'center-reverse', 'end', 'end-reverse'].includes(alignment),
},
/**
* Toggles the disabled state of the button on and off.
*/
disabled: {
type: Boolean,
default: false,
},
/**
* Specifies the button type
* Accepted values: primary, secondary, tertiary, tertiary-no-background, tertiary-on-primary, error, warning, success. If left empty,
* the default button style will be applied.
*/
type: {
type: String,
validator(value: string) {
return (
[
'primary',
'secondary',
'tertiary',
'tertiary-no-background',
'tertiary-on-primary',
'error',
'warning',
'success',
].indexOf(value) !== -1
);
},
default: 'secondary',
},
/**
* Specifies the button native type
* Accepted values: submit, reset, button. If left empty,
* the default "button" type will be used.
*/
nativeType: {
type: String,
validator(value: string) {
return ['submit', 'reset', 'button'].indexOf(value) !== -1;
},
default: 'button',
},
/**
* Specifies whether the button should span all the available width.
* By default, buttons span the whole width of the container.
*/
wide: {
type: Boolean,
default: false,
},
/**
* Always try to provide an aria-label to your button. Make it more
* specific than the button's name by provide some more context. E.g. if
* the name of the button is "send" in the Mail app, the aria label could
* be "Send email".
*/
ariaLabel: {
type: String,
default: null,
},
/**
* Providing the href attribute turns the button component into an `a`
* element.
*/
href: {
type: String,
default: null,
},
/**
* Providing the download attribute with href downloads file when clicking.
*/
download: {
type: String,
default: null,
},
/**
* Providing the to attribute turns the button component into a `router-link`
* element. Takes precedence over the href attribute.
*/
to: {
type: [String, Object],
default: null,
},
/**
* Pass in `true` if you want the matching behaviour of `router-link` to
* be non-inclusive: https://router.vuejs.org/api/#exact
*/
exact: {
type: Boolean,
default: false,
},
/**
* @deprecated To be removed in @nextcloud/vue 9. Migration guide: remove ariaHidden prop from NcAction* components.
* @todo Add a check in @nextcloud/vue 9 that this prop is not provided,
* otherwise root element will inherit incorrect aria-hidden.
*/
ariaHidden: {
type: Boolean,
default: null,
},
/**
* The pressed state of the button if it has a checked state
* This will add the `aria-pressed` attribute and for the button to have the primary style in checked state.
*/
pressed: {
type: Boolean,
default: null,
},
},
emits: ['update:pressed', 'click'],
computed: {
labelClass() {
return (this.small && this.icon) ? 'px-1' : 'px-2';
},
is() {
if (this.href) {
return 'a';
}
return 'button';
},
hasText() {
// return !!this.$slots.default;
return this.label != undefined;
},
hasIcon() {
// return this.$slots?.icon;
return this.icon != undefined;
},
// type() {
// return this.href ? null : this.nativeType;
// },
componentClass() {
const base = [
'button-vue',
this.roundedFull ? 'rounded-full' : 'rounded',
'inline-flex',
'cursor-pointer',
'justify-center',
'items-center',
'whitespace-nowrap',
'focus:outline-none',
'transition-colors',
// 'focus:ring-2',
{
'button-vue--icon-only': this.hasIcon && !this.hasText,
'button-vue--text-only': this.hasText && !this.hasIcon,
'button-vue--icon-and-text': this.hasIcon && this.hasText,
[`button-vue--vue-${this.realType}`]: this.realType,
'button-vue--wide': this.wide,
[`button-vue--${this.flexAlignment}`]: this.flexAlignment !== 'center',
'button-vue--reverse': this.isReverseAligned,
// Add other classes based on conditions as needed
},
];
if (this.small) {
base.push('text-sm', this.roundedFull ? 'px-3 py-1' : 'p-1');
} else {
base.push('py-2', this.roundedFull ? 'px-6' : 'px-3');
}
return base;
},
target() {
return (!this.to && this.href) ? '_self' : null;
},
/**
* The real type to be used for the button, enforces `primary` for pressed state and, if stateful button, any other type for not pressed state
* Otherwise the type property is used.
*/
realType() {
// Force *primary* when pressed
if (this.pressed) {
return 'primary';
}
// If not pressed but button is configured as stateful button then the type must not be primary
if (this.pressed === false && this.type === 'primary') {
return 'secondary';
}
return this.type;
},
/**
* The flexbox alignment of the button content
*/
flexAlignment() {
return this.alignment.split('-')[0];
},
/**
* If the button content should be reversed (icon on the end)
*/
isReverseAligned() {
return this.alignment.includes('-');
},
},
methods: {
click($event) {
// Update pressed prop on click if it is set
if (typeof this.pressed === 'boolean') {
/**
* Update the current pressed state of the button (if the `pressed` property was configured)
*
* @property {boolean} newValue The new `pressed`-state
*/
this.$emit('update:pressed', !this.pressed)
}
// We have to both navigate and emit the click event
this.$emit('click', $event)
// navigate?.($event)
},
},
};
</script>
<style lang="css" scoped>
.button-vue {
position: relative;
/* width: fit-content;
overflow: hidden;
border: 0; */
padding: 0;
/* font-size: var(--default-font-size);
font-weight: bold; */
min-height: 44px;
min-width: 44px;
display: flex;
align-items: center;
/* justify-content: center; */
/* Cursor pointer on element and all children */
cursor: pointer;
& *,
span {
cursor: pointer;
}
/* border-radius: math.div($clickable-area, 2); */
/* transition-property: color, */
/* border-color,
background-color;
transition-duration: 0.1s;
transition-timing-function: linear; */
/* border-radius: math.div($clickable-area, 2); */
/* border-radius: calc(50% - var(--clickable-area) / 2);
transition-property: color, border-color, background-color;
transition-duration: 0.1s;
transition-timing-function: linear; */
/* No outline feedback for focus. Handled with a toggled class in js (see data) */
&:focus {
outline: none;
}
&:disabled {
cursor: default;
& * {
cursor: default;
}
opacity: 0.5;
/* // Gives a wash out effect */
filter: saturate(0.7l);
}
/* // Default button type */
color: var(--color-primary-element-light-text);
background-color: var(--color-primary-element-light);
&:hover:not(:disabled) {
background-color: var(--color-primary-element-light-hover);
}
/* Back to the default color for this button when active */
/* TODO: add ripple effect */
&:active {
background-color: var(--color-primary-element-light);
}
&__wrapper {
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
}
&--end &__wrapper {
justify-content: end;
}
&--start &__wrapper {
justify-content: start;
}
&--reverse &__wrapper {
flex-direction: row-reverse;
}
&--reverse &--icon-and-text {
padding-inline: calc(var(--default-grid-baseline) * 4) var(--default-grid-baseline);
}
&__icon {
height: 44px;
width: 44px;
min-height: 44px;
min-width: 44px;
display: flex;
justify-content: center;
align-items: center;
}
/* &__text {
font-weight: bold;
margin-bottom: 1px;
padding: 2px 0;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
} */
/* Icon-only button */
&--icon-only {
width: 44px !important;
}
/* Text-only button */
&--text-only {
padding: 0 12px;
& .button-vue__text {
margin-left: 4px;
margin-right: 4px;
}
}
/* Icon and text button */
&--icon-and-text {
padding-block: 0;
padding-inline: var(--default-grid-baseline) calc(var(--default-grid-baseline) * 4);
}
/* Wide button spans the whole width of the container */
&--wide {
width: 100%;
}
&:focus-visible {
outline: 2px solid var(--color-main-text) !important;
box-shadow: 0 0 0 4px var(--color-main-background) !important;
&.button-vue--vue-tertiary-on-primary {
outline: 2px solid var(--color-primary-element-text);
border-radius: var(--border-radius);
background-color: transparent;
}
}
/* Button types */
/* // Primary */
&--vue-primary {
background-color: var(--color-primary-element);
color: var(--color-primary-element-text);
&:hover:not(:disabled) {
background-color: var(--color-primary-element-hover);
}
/* Back to the default color for this button when active
TODO: add ripple effect */
&:active {
background-color: var(--color-primary-element);
}
}
/* Secondary */
&--vue-secondary {
color: var(--color-primary-element-light-text);
background-color: var(--color-primary-element-light);
&:hover:not(:disabled) {
color: var(--color-primary-element-light-text);
background-color: var(--color-primary-element-light-hover);
}
}
/* Tertiary */
&--vue-tertiary {
color: var(--color-main-text);
background-color: transparent;
&:hover:not(:disabled) {
background-color: var(--color-background-hover);
}
}
/* Tertiary, no background */
&--vue-tertiary-no-background {
color: var(--color-main-text);
background-color: transparent;
&:hover:not(:disabled) {
background-color: transparent;
}
}
/* Tertiary on primary color (like the header) */
&--vue-tertiary-on-primary {
color: var(--color-primary-element-text);
background-color: transparent;
&:hover:not(:disabled) {
background-color: transparent;
}
}
/* Success */
&--vue-success {
background-color: var(--color-success);
color: white;
&:hover:not(:disabled) {
background-color: var(--color-success-hover);
}
/* Back to the default color for this button when active
: add ripple effect */
&:active {
background-color: var(--color-success);
}
}
/* Warning */
&--vue-warning {
background-color: var(--color-warning);
color: white;
&:hover:not(:disabled) {
background-color: var(--color-warning-hover);
}
/* Back to the default color for this button when active
TODO: add ripple effect */
&:active {
background-color: var(--color-warning);
}
}
/* Error */
&--vue-error {
background-color: var(--color-error);
color: white;
&:hover:not(:disabled) {
background-color: var(--color-error-hover);
}
/* Back to the default color for this button when active
TODO: add ripple effect */
&:active {
background-color: var(--color-error);
}
}
}
</style>

View File

@ -0,0 +1,901 @@
<template>
<transition name="fade" appear @after-enter="useFocusTrap" @before-leave="clearFocusTrap">
<div v-show="showModal" ref="mask" class="modal-mask"
:class="{ 'modal-mask--dark': dark || !closeButtonContained || hasPrevious || hasNext }" :style="cssVariables"
role="dialog" aria-modal="true" :aria-labelledby="'modal-name-' + randId"
:aria-describedby="'modal-description-' + randId" tabindex="-1">
<!-- Header -->
<transition name="fade-visibility" appear>
<div class="modal-header">
<h2 v-if="name.trim() !== ''" :id="'modal-name-' + randId" class="modal-name">
{{ name }}
</h2>
<div class="icons-menu">
<!-- Play-pause toggle -->
<button v-if="hasNext && enableSlideshow" :class="{ 'play-pause-icons--paused': slideshowPaused }"
class="play-pause-icons" type="button" @click="togglePlayPause">
<!-- Play/pause icons -->
<Play v-if="!playing" :size="iconSize" class="play-pause-icons__play" />
<Pause v-else :size="iconSize" class="play-pause-icons__pause" />
<span class="hidden-visually">
{{ playPauseName }}
</span>
<!-- Progress circle, css animated -->
<svg v-if="playing" class="progress-ring" height="50" width="50">
<circle class="progress-ring__circle" stroke="white" stroke-width="2" fill="transparent"
r="15" cx="25" cy="25" />
</svg>
</button>
<!-- Actions menu -->
<!-- @slot List of actions to show -->
<NcActions class="header-actions" :inline="inlineActions">
<slot name="actions" />
</NcActions>
<!-- Close modal -->
<NcButton v-if="canClose && !closeButtonContained" :aria-label="closeButtonAriaLabel"
class="header-close" type="tertiary" @click="close" :icon="mdiClose">
<!-- <template #icon>
<Close :size="iconSize" />
</template> -->
</NcButton>
<!-- <BaseButton v-if="canClose && !closeButtonContained" :aria-label="closeButtonAriaLabel" :color="button" outline @click="cancel" >
</BaseButton> -->
</div>
</div>
</transition>
<!-- modal-wrapper -->
<transition :name="modalTransitionName" appear>
<div v-show="showModal"
:class="[`modal-wrapper--${size}`, { 'modal-wrapper--spread-navigation': spreadNavigation }]"
class="modal-wrapper" @mousedown.self="handleClickModalWrapper">
<!-- Navigation button left -->
<transition name="fade-visibility" appear>
<NcButton v-show="hasPrevious" type="tertiary-no-background" class="prev"
:aria-label="prevButtonAriaLabel" @click="previous" :icon="mdiChevronLeft" :size="40">
</NcButton>
</transition>
<!-- modal-container: Content with close button -->
<div :id="'modal-description-' + randId" class="modal-container">
<!-- Close modal -default hidden by FirstrunWizard.vue -->
<NcButton v-if="canClose && closeButtonContained" type="tertiary" class="modal-container__close"
:aria-label="'closeButtonAriaLabel'" @click="close" :icon="mdiClose" :size="20"
:rounded-full="true">
</NcButton>
<div class="modal-container__content">
<!-- @slot Modal content to render -->
<slot />
</div>
</div>
<!-- Navigation button right -->
<transition name="fade-visibility" appear>
<NcButton v-show="hasNext" type="tertiary-no-background" class="next"
:aria-label="nextButtonAriaLabel" @click="next" :icon="mdiChevronRight" :size="40">
</NcButton>
</transition>
</div>
</transition>
</div>
</transition>
</template>
<script lang="ts">
import { getTrapStack } from '../utils/focusTrap';
// import { t } from '../../l10n.js';
import GenRandomId from '../utils/GenRandomId';
// import l10n from '../../mixins/l10n.js';
import NcActions from '@/Components/NcActions.vue';
import NcButton from '@/Components/NcButton.vue';
import Timer from '../utils/Timer.js';
// import Tooltip from '../../directives/Tooltip/index.js';
import Pause from '@/Components/Icons/Pause.vue';
import Play from '@/Components/Icons/Play.vue';
import { mdiClose, mdiChevronLeft, mdiChevronRight } from '@mdi/js';
import { createFocusTrap } from 'focus-trap';
// import { useSwipe } from '@vueuse/core';
export default {
name: 'NcModal',
components: {
NcActions,
Pause,
Play,
NcButton,
},
// directives: {
// tooltip: Tooltip,
// },
// mixins: [l10n],
props: {
/**
* Name to be shown with the modal
*/
name: {
type: String,
default: '',
},
/**
* Declare if a previous slide is available
*/
hasPrevious: {
type: Boolean,
default: false,
},
/**
* Declare if a next slide is available
*/
hasNext: {
type: Boolean,
default: false,
},
/**
* Declare if hiding the modal should be animated
*/
outTransition: {
type: Boolean,
default: false,
},
/**
* Declare if the slideshow functionality should be enabled
*/
enableSlideshow: {
type: Boolean,
default: false,
},
/**
* Declare the slide interval
*/
slideshowDelay: {
type: Number,
default: 5000,
},
/**
* Allow to pause an ongoing slideshow
*/
slideshowPaused: {
type: Boolean,
default: false,
},
/**
* Enable swipe between slides
*/
enableSwipe: {
type: Boolean,
default: true,
},
spreadNavigation: {
type: Boolean,
default: false,
},
/**
* Defines the modal size.
* Default is 'normal'.
* Available are 'small', 'normal', 'large' and 'full'.
* All sizes except 'small' change automatically to full-screen on mobile.
*/
size: {
type: String,
default: 'normal',
validator: (size: string) => {
return ['small', 'normal', 'large', 'full'].includes(size);
},
},
/**
* Declare if the modal can be closed
*/
canClose: {
type: Boolean,
default: true,
},
/**
* Close the modal if the user clicked outside of the modal
* Only relevant if `canClose` is set to true.
*/
closeOnClickOutside: {
type: Boolean,
default: false,
},
/**
* Makes the modal backdrop black if true
* Will be overwritten if some buttons are shown outside
*/
dark: {
type: Boolean,
default: false,
},
/**
* Selector for the modal container, pass `null` to prevent automatic container mounting
*/
// container: {
// type: [String, null],
// default: 'body',
// },
/**
* Pass in false if you want the modal 'close' button to be displayed
* outside the modal boundaries, in the top right corner of the window
*/
closeButtonContained: {
type: Boolean,
default: true,
},
/**
* Additional elements to add to the focus trap
*/
additionalTrapElements: {
type: Array,
default: () => [],
},
/**
* Display x items inline
*
* @see Actions component usage
*/
inlineActions: {
type: Number,
default: 0,
},
show: {
type: Boolean,
default: undefined,
},
},
emits: ['previous', 'next', 'close', 'update:show'],
data() {
return {
mc: null,
playing: false,
slideshowTimeout: null,
iconSize: 24,
focusTrap: null,
randId: GenRandomId(),
internalShow: true,
mdiClose,
mdiChevronLeft,
mdiChevronRight,
};
},
computed: {
showModal() {
return this.show === undefined ? this.internalShow : this.show;
},
modalTransitionName() {
return `modal-${this.outTransition ? 'out' : 'in'}`;
},
playPauseName() {
return this.playing ? 'Pause slideshow' : 'Start slideshow';
},
cssVariables() {
return {
'--slideshow-duration': this.slideshowDelay + 'ms',
'--icon-size': this.iconSize + 'px',
};
},
closeButtonAriaLabel() {
return 'Close';
},
prevButtonAriaLabel() {
return 'Previous';
},
nextButtonAriaLabel() {
return 'Next';
},
},
watch: {
/**
* Handle play/pause of an ongoing slideshow
*
* @param {boolean} paused is the player paused
*/
slideshowPaused(paused) {
if (this.slideshowTimeout) {
if (paused) {
this.slideshowTimeout.pause();
} else {
this.slideshowTimeout.start();
}
}
},
additionalTrapElements(elements) {
if (this.focusTrap) {
const contentContainer = this.$refs.mask;
this.focusTrap.updateContainerElements([contentContainer, ...elements]);
}
},
},
beforeMount() {
window.addEventListener('keydown', this.handleKeydown);
},
beforeDestroy() {
window.removeEventListener('keydown', this.handleKeydown);
this.mc.stop();
},
mounted() {
// init clear view
this.useFocusTrap();
// this.mc = useSwipe(this.$refs.mask, {
// onSwipeEnd: this.handleSwipe,
// });
if (this.container) {
if (this.container === 'body') {
// force mount the component to body
document.body.insertBefore(this.$el, document.body.lastChild);
} else {
const container = document.querySelector(this.container);
container.appendChild(this.$el);
}
}
},
destroyed() {
this.clearFocusTrap();
this.$el.remove();
},
methods: {
// Events emitters
previous(event) {
// do not send the event if nothing is available
if (this.hasPrevious) {
// if data is set, then it's a user mouse event
// and not the slideshow handler, therefore
// we reset the timer
if (event) {
this.resetSlideshow();
}
this.$emit('previous', event);
}
},
next(event) {
// do not send the event if nothing is available
if (this.hasNext) {
// if data is set, then it's a mouse event
// and not the slideshow handler, therefore
// we reset the timer
if (event) {
this.resetSlideshow();
}
this.$emit('next', event);
}
},
close(data) {
// do not fire event if forbidden
if (this.canClose) {
// We set internalShow here, so the out transitions properly run before the component is destroyed
this.internalShow = false;
this.$emit('update:show', false);
// delay closing for animation
setTimeout(() => {
/**
* Emitted when the closing animation is finished
*/
this.$emit('close', data);
}, 300);
}
},
/**
* Handle click on modal wrapper
* If `closeOnClickOutside` is set the modal will be closed
*
* @param {MouseEvent} event The click event
*/
handleClickModalWrapper(event) {
if (this.closeOnClickOutside) {
this.close(event);
}
},
/**
* @param {KeyboardEvent} event - keyboard event
*/
handleKeydown(event) {
if (event.key === 'Escape') {
const trapStack = getTrapStack();
// Only close the most recent focus trap modal
if (trapStack.length > 0 && trapStack[trapStack.length - 1] !== this.focusTrap) {
return;
}
return this.close(event);
}
const arrowHandlers = {
ArrowLeft: this.previous,
ArrowRight: this.next,
};
if (arrowHandlers[event.key]) {
// Ignore arrow navigation, if there is a current focus outside the modal.
// For example, when the focus is in Sidebar or NcActions's items,
// arrow navigation should not be intercept by modal slider
if (document.activeElement && !this.$el.contains(document.activeElement)) {
return;
}
return arrowHandlers[event.key](event);
}
},
/**
* handle the swipe event
*
* @param {TouchEvent} e The touch event
* @param {import('@vueuse/core').SwipeDirection} direction Swipe direction
*/
handleSwipe(e, direction) {
if (this.enableSwipe) {
if (direction === 'left') {
// swiping to left to go to the next item
this.next(e);
} else if (direction === 'right') {
// swiping to right to go back to the previous item
this.previous(e);
}
}
},
/**
* Toggle the slideshow state
*/
togglePlayPause() {
this.playing = !this.playing;
if (this.playing) {
this.handleSlideshow();
} else {
this.clearSlideshowTimeout();
}
},
/**
* Reset the slideshow timer and keep going if it was on
*/
resetSlideshow() {
this.playing = !this.playing;
this.clearSlideshowTimeout();
this.$nextTick(function () {
this.togglePlayPause();
});
},
/**
* Handle the slideshow timer and next event
*/
handleSlideshow() {
this.playing = true;
if (this.hasNext) {
this.slideshowTimeout = new Timer(() => {
this.next();
this.handleSlideshow();
}, this.slideshowDelay);
} else {
this.playing = false;
this.clearSlideshowTimeout();
}
},
/**
* Clear slideshowTimeout if ongoing
*/
clearSlideshowTimeout() {
if (this.slideshowTimeout) {
this.slideshowTimeout.clear();
}
},
/**
* Add focus trap for accessibility.
*/
async useFocusTrap() {
// Don't do anything if the modal is hidden,
// or we have a focus trap already
if (!this.showModal || this.focusTrap) {
return;
}
const contentContainer = this.$refs.mask;
// wait until all children are mounted and available in the DOM before focusTrap can be added
await this.$nextTick();
const options = {
allowOutsideClick: true,
fallbackFocus: contentContainer,
trapStack: getTrapStack(),
// Esc can be used without stop in content or additionalTrapElements where it should not deacxtivate modal's focus trap.
// Focus trap is deactivated on modal close anyway.
escapeDeactivates: false,
};
// Init focus trap
this.focusTrap = createFocusTrap([contentContainer, ...this.additionalTrapElements], options);
this.focusTrap.activate();
},
clearFocusTrap() {
if (!this.focusTrap) {
return;
}
this.focusTrap?.deactivate();
this.focusTrap = null;
},
},
};
</script>
<style lang="css" scoped>
.modal-mask {
position: fixed;
z-index: 9998;
top: 0;
left: 0;
display: block;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
&--dark {
background-color: rgba(0, 0, 0, 0.92);
}
}
.modal-header {
position: absolute;
z-index: 10001;
top: 0;
right: 0;
left: 0;
/* prevent vue show to use display:none and reseting */
/* the circle animation loop */
display: flex !important;
align-items: center;
justify-content: center;
width: 100%;
height: var(--header-height);
overflow: hidden;
transition:
opacity 250ms,
visibility 250ms;
.modal-name {
overflow-x: hidden;
box-sizing: border-box;
width: 100%;
/* maximum actions is 3 */
padding: 0 var(--clickable-area * 3) 0 12px;
transition: padding ease 100ms;
white-space: nowrap;
text-overflow: ellipsis;
color: #fff;
/* font-size: var(--icon-margin); */
margin-bottom: 0;
}
/* On wider screens the name can be centered
@media only screen and (min-width: $breakpoint-mobile) {
.modal-name {
padding-left: #{$clickable-area * 3}; // maximum actions is 3
text-align: center;
}
} */
.icons-menu {
position: absolute;
right: 0;
display: flex;
align-items: center;
justify-content: flex-end;
.header-close {
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
/* margin: math.div($header-height - $clickable-area, 2); */
padding: 0;
}
.play-pause-icons {
position: relative;
width: var(--header-height);
height: var(header-height);
margin: 0;
padding: 0;
cursor: pointer;
border: none;
background-color: transparent;
&:hover,
&:focus {
.play-pause-icons__play,
.play-pause-icons__pause {
opacity: var(--opacity_full);
/* border-radius: math.div($clickable-area, 2); */
background-color: var(--icon-focus-bg);
}
}
&__play,
&__pause {
box-sizing: border-box;
width: var(--default-clickable-area);
height: var(--default-clickable-area);
margin: var(--header-height) - var(--default-clickable-area);
cursor: pointer;
opacity: var(--opacity_normal);
}
}
.header-actions {
color: white;
}
&:deep() .action-item {
/* margin: math.div($header-height - $clickable-area, 2); */
&--single {
box-sizing: border-box;
/* width: $clickable-area;
height: $clickable-area; */
cursor: pointer;
background-position: center;
background-size: 22px;
}
}
:deep(button) {
/* force white instead of default main text */
color: #fff;
}
/* Force the Actions menu icon to be the same size as other icons */
&:deep(.action-item__menutoggle) {
padding: 0;
span,
svg {
width: var(--icon-size);
height: var(--icon-size);
}
}
}
}
.modal-wrapper {
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: 100%;
height: 100%;
/* Navigation buttons */
.prev,
.next {
z-index: 10000;
height: 35vh;
min-height: 300px;
position: absolute;
transition: opacity 250ms;
/* // hover the mask */
color: white;
&:focus-visible {
/* Override NcButton focus styles */
box-shadow: 0 0 0 2px var(--color-primary-element-text);
background-color: var(--color-box-shadow);
}
}
.prev {
left: 2px;
}
.next {
right: 2px;
}
/* Content */
.modal-container {
position: relative;
display: flex;
padding: 0;
transition: transform 300ms ease;
border-radius: var(--border-radius-large);
background-color: var(--color-main-background);
color: var(--color-main-text);
box-shadow: 0 0 40px rgba(0, 0, 0, 0.2);
&__close {
/* // Ensure the close button is always ontop of the content */
z-index: 1;
position: absolute;
top: 4px;
right: 4px;
}
&__content {
width: 100%;
/* At least the close button shall fit in */
min-height: 52px;
/* avoids unecessary hacks if the content should be bigger than the modal */
overflow: auto;
}
}
/* We allow 90% max-height, but we need to ensure the header does not overlap the modal
as the modal is centered, we need the space on top and bottom */
/* $max-modal-height: min(90%, calc(100% - 2 * $header-height)); */
/* // Sizing */
&--small {
.modal-container {
width: 400px;
max-width: 90%;
/* max-height: $max-modal-height; */
}
}
&--normal {
.modal-container {
max-width: 90%;
width: 600px;
/* max-height: $max-modal-height; */
}
}
&--large {
.modal-container {
max-width: 90%;
width: 900px;
/* max-height: $max-modal-height; */
}
}
&--full {
.modal-container {
width: 100%;
height: calc(100% - var(--header-height));
position: absolute;
/* top: $header-height; */
border-radius: 0;
}
}
/* // Make modal full screen on mobile */
@media only screen and (max-width: 640px) {
.modal-container {
max-width: initial;
width: 100%;
max-height: initial;
height: calc(100% - var(--header-height));
position: absolute;
border-radius: 0;
}
}
}
/* TRANSITIONS */
.fade-enter-active,
.fade-leave-active {
transition: opacity 250ms;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
.fade-visibility-enter,
.fade-visibility-leave-to {
visibility: hidden;
opacity: 0;
}
.modal-in-enter-active,
.modal-in-leave-active,
.modal-out-enter-active,
.modal-out-leave-active {
transition: opacity 250ms;
}
.modal-in-enter,
.modal-in-leave-to,
.modal-out-enter,
.modal-out-leave-to {
opacity: 0;
}
.modal-in-enter .modal-container,
.modal-in-leave-to .modal-container {
transform: scale(0.9);
}
.modal-out-enter .modal-container,
.modal-out-leave-to .modal-container {
transform: scale(1.1);
}
/* // animated circle */
/* $radius: 15;
$pi: 3.14159265358979; */
.modal-mask .play-pause-icons {
.progress-ring {
position: absolute;
top: 0;
left: 0;
transform: rotate(-90deg);
.progress-ring__circle {
transition: 100ms stroke-dashoffset;
/* // axis compensation */
transform-origin: 50% 50%;
animation: progressring linear var(--slideshow-duration) infinite;
stroke-linecap: round;
/* radius * 2 * PI */
stroke-dashoffset: 15 * 2 * 3.14159265358979;
/* radius * 2 * PI */
stroke-dasharray: 15 * 2 * 3.14159265358979;
}
}
&--paused {
.icon-pause {
animation: breath 2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
.progress-ring__circle {
animation-play-state: paused !important;
}
}
}
/* keyframes get scoped too and break the animation name, we need them unscoped */
@keyframes progressring {
from {
/* radius * 2 * PI */
stroke-dashoffset: 15 * 2 * 3.14159265358979;
}
to {
stroke-dashoffset: 0;
}
}
@keyframes breath {
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
</style>

View File

@ -146,7 +146,7 @@ const datasets = computed(() => mainService.datasets);
<TableSampleClients /> <TableSampleClients />
</CardBox> </CardBox>
<CardBox> <!-- <CardBox>
<p class="mb-3 text-gray-500 dark:text-gray-400"> <p class="mb-3 text-gray-500 dark:text-gray-400">
Discover the power of Tethys, the cutting-edge web backend solution that revolutionizes the way you handle research Discover the power of Tethys, the cutting-edge web backend solution that revolutionizes the way you handle research
data. At the heart of Tethys lies our meticulously developed research data repository, which leverages state-of-the-art data. At the heart of Tethys lies our meticulously developed research data repository, which leverages state-of-the-art
@ -163,7 +163,7 @@ const datasets = computed(() => mainService.datasets);
eliminates errors and minimizes downtime. Our CI/CD pipeline automatically verifies each code commit, runs comprehensive eliminates errors and minimizes downtime. Our CI/CD pipeline automatically verifies each code commit, runs comprehensive
tests, and deploys the repository seamlessly, ensuring that your research data is always up-to-date and accessible. tests, and deploys the repository seamlessly, ensuring that your research data is always up-to-date and accessible.
</p> </p>
</CardBox> </CardBox> -->
</SectionMain> </SectionMain>
<!-- </section> --> <!-- </section> -->
</LayoutAuthenticated> </LayoutAuthenticated>

View File

@ -6,8 +6,10 @@ export const basic = {
asideMenuItemActive: 'font-bold text-cyan-300', asideMenuItemActive: 'font-bold text-cyan-300',
asideMenuDropdown: 'bg-gray-700/50', asideMenuDropdown: 'bg-gray-700/50',
navBarItemLabel: 'text-black', navBarItemLabel: 'text-black',
navBarItemLabelHover: 'hover:text-blue-500', // navBarItemLabelHover: 'hover:text-blue-500',
navBarItemLabelActiveColor: 'text-blue-600', navBarItemLabelHover: 'hover:text-lime-dark',
// navBarItemLabelActiveColor: 'text-blue-600',
navBarItemLabelActiveColor: 'text-lime-dark',
overlay: 'from-gray-700 via-gray-900 to-gray-700', overlay: 'from-gray-700 via-gray-900 to-gray-700',
}; };

View File

@ -0,0 +1,8 @@
const GenRandomId = (length) => {
return Math.random()
.toString(36)
.replace(/[^a-z]+/g, '')
.slice(0, length || 5)
}
export default GenRandomId

View File

@ -0,0 +1,43 @@
/**
* @param {Function} callback The function to call
* @param {number} delay The time to wait
*/
export default function timer(callback, delay) {
let id;
let started;
let remaining = delay;
let running;
this.start = function () {
running = true;
started = new Date();
id = setTimeout(callback, remaining);
};
this.pause = function () {
running = false;
clearTimeout(id);
remaining -= new Date() - started;
};
this.clear = function () {
running = false;
clearTimeout(id);
remaining = 0;
};
this.getTimeLeft = function () {
if (running) {
this.pause();
this.start();
}
return remaining;
};
this.getStateRunning = function () {
return running;
};
this.start();
}

View File

@ -0,0 +1,11 @@
/**
* Return the default global focus trap stack
*
* @return {import('focus-trap').FocusTrap[]}
*/
export const getTrapStack = function() {
// Create global stack if undefined
Object.assign(window, { _nc_focus_trap: window._nc_focus_trap || [] })
return window._nc_focus_trap
}

View File

@ -0,0 +1,16 @@
export function loadState<T>(app: string, key: string, fallback?: T): T {
const elem = <HTMLInputElement>document.querySelector(`#initial-state-${app}-${key}`);
if (elem === null) {
if (fallback !== undefined) {
return fallback;
}
throw new Error(`Could not find initial state ${key} of ${app}`);
}
try {
const value = atob(elem.value);
return JSON.parse(value);
} catch (e) {
throw new Error(`Could not parse initial state ${key} of ${app}`);
}
}

View File

@ -1,20 +1,27 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
{{-- <link rel="icon" type="image/png" href="/favicon.ico"> --}} {{--
<link rel="icon" type="image/png" href="/favicon.png"> <link rel="icon" type="image/png" href="/favicon.ico"> --}}
{{-- <link rel="icon" href="/apps/theming/favicon/settings?v=ad28c447"> --}} <link rel="icon" type="image/png" href="/favicon.png">
{{--
<link rel="icon" href="/apps/theming/favicon/settings?v=ad28c447"> --}}
<input type="hidden" id="initial-state-firstrunwizard-desktop"
value="Imh0dHBzOi8vZ2l0ZWEuZ2VvbG9naWUuYWMuYXQvZ2VvbGJhL3RldGh5cy5iYWNrZW5kIg==">
@routes @routes
@entryPointStyles('app') @entryPointStyles('app')
@entryPointScripts('app') @entryPointScripts('app')
{{-- <title>myapp</title> --}} {{-- <title>myapp</title> --}}
</head> </head>
<body> <body>
@inertia() @inertia()
</body> </body>
</html>
</html>

View File

@ -1,6 +1,7 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
const plugin = require('tailwindcss/plugin'); const plugin = require('tailwindcss/plugin');
const defaultTheme = require('tailwindcss/defaultTheme'); const defaultTheme = require('tailwindcss/defaultTheme');
const { registerables } = require('chart.js');
module.exports = { module.exports = {
content: ['./resources/**/*.{edge,js,ts,jsx,tsx,vue}'], content: ['./resources/**/*.{edge,js,ts,jsx,tsx,vue}'],
@ -13,7 +14,21 @@ module.exports = {
extend: { extend: {
colors: { colors: {
'primary': '#22C55E', 'primary': '#22C55E',
'primary-dark': '#DCFCE7', 'primary-dark': '#DCFCE7',
'lime': {
DEFAULT: '#BFCE40',
dark: 'rgba(5,46,55,0.7)',
50: '#FBFCF7',
100: '#F8FBE1',
200: '#EEF69E',
300: '#DCEC53',
400: '#A8D619',
500: '#65DC21',
600: '#429E04',
700: '#357C06',
800: '#295B09',
900: '#20450A',
},
}, },
fontFamily: { fontFamily: {
sans: ['Inter', ...defaultTheme.fontFamily.sans], sans: ['Inter', ...defaultTheme.fontFamily.sans],