fix: web component modals

This commit is contained in:
Zack Spear
2023-06-06 18:41:19 -07:00
parent f4b4271c91
commit a9c859f022
18 changed files with 338 additions and 172 deletions

View File

@@ -46,8 +46,8 @@ const serverState = {
expireTime,
lanIp: '192.168.0.1',
locale: 'en',
pluginInstalled: true,
registered: true,
pluginInstalled: false,
registered: false,
site: 'http://localhost:4321',
state,
uptime,

View File

@@ -10,11 +10,12 @@ $myservers = file_exists($myservers_flash_cfg_path) ? @parse_ini_file($myservers
// extract web component JS file from manifest
$jsonManifest = file_get_contents('/usr/local/emhttp/plugins/dynamix.my.servers/webComponents/manifest.json');
$jsonManifestData = json_decode($jsonManifest, true);
$webComponentFile = $jsonManifestData["connect-components.client.mjs"]["file"];
$webComponentJsFile = $jsonManifestData["connect-components.client.mjs"]["file"];
// web component
$localSource = '/plugins/dynamix.my.servers/webComponents/' . $webComponentFile;
$localSourceBasePath = '/plugins/dynamix.my.servers/webComponents/';
$localSourceJs = $localSourceBasePath . $webComponentJsFile;
// add the web component source to the DOM
echo '<script id="unraid-webcomponents" defer src="' . $localSource . '"></script>';
echo '<script id="unraid-webcomponents" defer src="' . $localSourceJs . '"></script>';
/**
* Build vars for user profile prop

57
app.vue
View File

@@ -1,6 +1,4 @@
<script lang="ts" setup>
import serverState from './_data/serverState';
const nuxtApp = useNuxtApp();
onBeforeMount(() => {
nuxtApp.$customElements.registerEntry('ConnectComponents');
@@ -8,56 +6,7 @@ onBeforeMount(() => {
</script>
<template>
<div class="max-w-5xl mx-auto bg-gray-200">
<client-only>
<div class="flex flex-col gap-6 p-6">
<h2>Vue Components</h2>
<h3>UserProfileCe</h3>
<UserProfileCe :server="serverState" />
<hr />
<h3>DownloadApiLogsCe</h3>
<DownloadApiLogsCe />
<hr />
<h3>AuthCe</h3>
<AuthCe />
<hr />
<h3>KeyActionsCe</h3>
<KeyActionsCe />
<hr />
<h3>WanIpCheckCe</h3>
<WanIpCheckCe />
</div>
<div class="flex flex-col gap-6 p-6">
<h2>Web Components</h2>
<h3>UserProfileCe</h3>
<connect-user-profile :server="JSON.stringify(serverState)"></connect-user-profile>
<hr />
<h3>DownloadApiLogsCe</h3>
<connect-download-api-logs></connect-download-api-logs>
<hr />
<h3>AuthCe</h3>
<connect-auth></connect-auth>
<hr />
<h3>KeyActionsCe</h3>
<connect-key-actions></connect-key-actions>
<hr />
<h3>WanIpCheckCe</h3>
<connect-wan-ip-check></connect-wan-ip-check>
</div>
</client-only>
</div>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
<style lang="postcss" scoped>
h2 {
@apply text-xl font-semibold font-mono;
}
h3 {
@apply text-lg font-semibold font-mono;
}
hr {
@apply border-black;
}
</style>

View File

@@ -40,4 +40,52 @@ body {
border-bottom: 11px solid var(--color-alpha);
border-left: 11px solid transparent;
}
}
.BrandLoading_2,
.BrandLoading_4 {
animation: mark_2 1.5s ease infinite;
}
.BrandLoading_3 {
animation: mark_3 1.5s ease infinite;
}
.BrandLoading_6,
.BrandLoading_8 {
animation: mark_6 1.5s ease infinite;
}
.BrandLoading_7 {
animation: mark_7 1.5s ease infinite;
}
@keyframes mark_2 {
50% {
transform: translateY(-40px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_3 {
50% {
transform: translateY(-62px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_6 {
50% {
transform: translateY(40px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_7 {
50% {
transform: translateY(62px);
}
100% {
transform: translateY(0);
}
}

View File

@@ -1,4 +1,7 @@
<script setup lang="ts">
import 'tailwindcss/tailwind.css';
import '~/assets/main.css';
export interface Props {
gradientStart?: string;
gradientStop?: string;
@@ -12,7 +15,6 @@ withDefaults(defineProps<Props>(), {
height: 64,
title: 'Loading',
});
</script>
<template>
@@ -20,7 +22,6 @@ withDefaults(defineProps<Props>(), {
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 133.52 76.97"
class="unraid_sc_loader"
:class="`h-[${height}px]`"
role="img"
>
@@ -42,97 +43,47 @@ withDefaults(defineProps<Props>(), {
<path
d="m70,19.24zm57,0l6.54,0l0,38.49l-6.54,0l0,-38.49z"
fill="url(#unraidLoadingGradient)"
class="unraid_sc_loader_9"
class="BrandLoading_9"
/>
<path
d="m70,19.24zm47.65,11.9l-6.55,0l0,-23.79l6.55,0l0,23.79z"
fill="url(#unraidLoadingGradient)"
class="unraid_sc_loader_8"
class="BrandLoading_8"
/>
<path
d="m70,19.24zm31.77,-4.54l-6.54,0l0,-14.7l6.54,0l0,14.7z"
fill="url(#unraidLoadingGradient)"
class="unraid_sc_loader_7"
class="BrandLoading_7"
/>
<path
d="m70,19.24zm15.9,11.9l-6.54,0l0,-23.79l6.54,0l0,23.79z"
fill="url(#unraidLoadingGradient)"
class="unraid_sc_loader_6"
class="BrandLoading_6"
/>
<path
d="m63.49,19.24l6.51,0l0,38.49l-6.51,0l0,-38.49z"
fill="url(#unraidLoadingGradient)"
class="unraid_sc_loader_5"
class="BrandLoading_5"
/>
<path
d="m70,19.24zm-22.38,26.6l6.54,0l0,23.78l-6.54,0l0,-23.78z"
fill="url(#unraidLoadingGradient)"
class="unraid_sc_loader_4"
class="BrandLoading_4"
/>
<path
d="m70,19.24zm-38.26,43.03l6.55,0l0,14.73l-6.55,0l0,-14.73z"
fill="url(#unraidLoadingGradient)"
class="unraid_sc_loader_3"
class="BrandLoading_3"
/>
<path
d="m70,19.24zm-54.13,26.6l6.54,0l0,23.78l-6.54,0l0,-23.78z"
fill="url(#unraidLoadingGradient)"
class="unraid_sc_loader_2"
class="BrandLoading_2"
/>
<path
d="m70,19.24zm-63.46,38.49l-6.54,0l0,-38.49l6.54,0l0,38.49z"
fill="url(#unraidLoadingGradient)"
class="unraid_sc_loader_1"
class="BrandLoading_1"
/>
</svg>
</template>
<style lang="postcss" scoped>
.unraid_sc_loader_2,
.unraid_sc_loader_4 {
animation: mark_2 1.5s ease infinite;
}
.unraid_sc_loader_3 {
animation: mark_3 1.5s ease infinite;
}
.unraid_sc_loader_6,
.unraid_sc_loader_8 {
animation: mark_6 1.5s ease infinite;
}
.unraid_sc_loader_7 {
animation: mark_7 1.5s ease infinite;
}
@keyframes mark_2 {
50% {
transform: translateY(-40px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_3 {
50% {
transform: translateY(-62px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_6 {
50% {
transform: translateY(40px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_7 {
50% {
transform: translateY(62px);
}
100% {
transform: translateY(0);
}
}
</style>

View File

@@ -1,9 +1,7 @@
<script setup lang="ts">
/**
* @fix Modal closes when clicking inside the modal
*/
import { Dialog, DialogPanel, DialogTitle, DialogDescription, TransitionChild, TransitionRoot } from '@headlessui/vue';
import { CheckIcon, XMarkIcon } from '@heroicons/vue/24/outline';
import { TransitionChild, TransitionRoot } from '@headlessui/vue';
import { XMarkIcon } from '@heroicons/vue/24/outline';
import useFocusTrap from '~/composables/useFocusTrap';
export interface Props {
description?: string;
@@ -13,39 +11,49 @@ export interface Props {
title?: string;
}
withDefaults(defineProps<Props>(), {
const props = withDefaults(defineProps<Props>(), {
maxWidth: 'sm:max-w-lg',
open: false,
showCloseX: false,
});
const emit = defineEmits(['close']);
const closeModal = () => {
console.debug('[closeModal]');
emit('close');
};
const { trapRef } = useFocusTrap();
const ariaLablledById = computed((): string|undefined => props.title ? `ModalTitle-${Math.random()}`.replace('0.', '') : undefined);
onMounted(() => {
console.debug('[onMounted]');
document.body.style.setProperty('overflow', 'hidden');
});
onBeforeUnmount(() => {
document.removeEventListener('keyup', () => {});
document.body.style.removeProperty('overflow');
});
</script>
<template>
<TransitionRoot as="template" :show="open">
<Dialog as="div" class="relative z-[99999]">
<TransitionChild as="template" enter="ease-out duration-300" enter-from="opacity-0" enter-to="opacity-100" leave="ease-in duration-200" leave-from="opacity-100" leave-to="opacity-0">
<div class="fixed inset-0 z-0 bg-black bg-opacity-50 transition-opacity" />
</TransitionChild>
<div class="fixed inset-0 z-10 overflow-y-auto">
<div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<TransitionChild as="template" enter="ease-out duration-300" enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" enter-to="opacity-100 translate-y-0 sm:scale-100" leave="ease-in duration-200" leave-from="opacity-100 translate-y-0 sm:scale-100" leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
<DialogPanel :class="maxWidth" class="text-beta bg-alpha relative transform overflow-hidden rounded-lg px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:p-6">
<DialogTitle v-if="title">{{ title }}</DialogTitle>
<DialogDescription v-if="description">{{ description }}</DialogDescription>
<div v-if="showCloseX" class="absolute z-20 right-0 top-0 hidden pt-2 pr-2 sm:block">
<button type="button" class="rounded-md bg-alpha text-gray-400 p-2 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" @click="$emit('close')">
<span class="sr-only">Close</span>
<XMarkIcon class="h-6 w-6" aria-hidden="true" />
</button>
</div>
<slot />
</DialogPanel>
</TransitionChild>
<div v-if="open" @keyup.esc="closeModal" ref="trapRef" class="fixed inset-0 z-10 overflow-y-auto" role="dialog" aria-dialog="true" :aria-labelledby="ariaLablledById" tabindex="-1">
<div @click="closeModal" class="fixed inset-0 z-0 bg-black bg-opacity-50 transition-opacity" />
<div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<div :class="maxWidth" class="text-beta bg-alpha relative transform overflow-hidden rounded-lg px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:p-6">
<div v-if="showCloseX" class="absolute z-20 right-0 top-0 hidden pt-2 pr-2 sm:block">
<button @click="closeModal" type="button" class="rounded-md bg-alpha text-gray-400 p-2 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
<span class="sr-only">Close</span>
<XMarkIcon class="h-6 w-6" aria-hidden="true" />
</button>
</div>
<h1 v-if="title" :id="ariaLablledById">{{ title }}</h1>
<h2 v-if="description">{{ description }}</h2>
<slot />
</div>
</Dialog>
</TransitionRoot>
</div>
</div>
</template>

26
components/Modals.ce.vue Normal file
View File

@@ -0,0 +1,26 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import 'tailwindcss/tailwind.css';
import '~/assets/main.css';
import { useCallbackStore } from '~/store/callback';
import { usePromoStore } from '~/store/promo';
const callbackStore = useCallbackStore();
const promoStore = usePromoStore();
const { callbackFeedbackVisible } = storeToRefs(callbackStore);
const { promoVisible } = storeToRefs(promoStore);
</script>
<template>
<div class="relative z-[99999]">
<UpcCallbackFeedback v-if="callbackFeedbackVisible" :open="callbackFeedbackVisible" />
<UpcPromo v-if="promoVisible" :open="promoVisible" />
</div>
</template>
<style lang="postcss">
@tailwind base;
@tailwind components;
@tailwind utilities;
</style>

View File

@@ -81,9 +81,6 @@ onBeforeMount(() => {
</script>
<template>
<UpcCallbackFeedback />
<UpcPromo />
<div id="UserProfile" class="text-alpha relative z-20 flex flex-col h-full gap-y-4px pl-40px rounded">
<div class="text-gamma text-12px text-right font-semibold leading-normal flex flex-row items-baseline justify-end gap-x-12px">
<UpcUptimeExpire />

View File

@@ -2,19 +2,23 @@
import { storeToRefs } from 'pinia';
import 'tailwindcss/tailwind.css';
import '~/assets/main.css';
import { useCallbackStore } from '~/store/callback';
const callbackStore = useCallbackStore();
const { callbackFeedbackVisible, decryptedData } = storeToRefs(callbackStore);
onBeforeMount(() => {
callbackStore.watcher();
export interface Props {
open?: boolean;
}
withDefaults(defineProps<Props>(), {
open: false,
});
const callbackStore = useCallbackStore();
const { decryptedData } = storeToRefs(callbackStore);
</script>
<template>
<Modal
:open="callbackFeedbackVisible"
:open="open"
@close="callbackStore.hide()"
max-width="max-w-800px"
>

View File

@@ -3,15 +3,20 @@
* @todo future idea turn this into a carousel. each feature could have a short video if we ever them
*/
import { Switch, SwitchGroup, SwitchLabel } from '@headlessui/vue';
import { storeToRefs } from 'pinia';
import { usePromoStore } from '~/store/promo';
import type { UserProfilePromoFeature } from '~/types/userProfile';
import 'tailwindcss/tailwind.css';
import '~/assets/main.css';
export interface Props {
open?: boolean;
}
withDefaults(defineProps<Props>(), {
open: false,
});
const promoStore = usePromoStore();
const { promoVisible } = storeToRefs(promoStore);
const features = ref<UserProfilePromoFeature[]>([
{
@@ -58,8 +63,9 @@ const installButtonClasses = 'text-white text-14px text-center w-full flex flex-
<template>
<Modal
:open="promoVisible"
:open="open"
@close="promoStore.promoHide()"
:show-close-x="true"
max-width="max-w-800px"
>
<div class="text-center relative w-full p-24px">

View File

@@ -0,0 +1,42 @@
/**
* @see https://www.telerik.com/blogs/how-to-trap-focus-modal-vue-3
*/
import { customRef } from "vue";
import { createFocusTrap } from "focus-trap";
const useFocusTrap = focusTrapArgs => {
const trapRef = customRef((track, trigger) => {
let $trapEl = null;
return {
get() {
track();
return $trapEl;
},
set(value) {
$trapEl = value;
value ? initFocusTrap(focusTrapArgs) : clearFocusTrap();
trigger();
},
};
});
let trap = null;
const initFocusTrap = focusTrapArgs => {
if (!trapRef.value) return;
trap = createFocusTrap(trapRef.value, focusTrapArgs);
trap.activate();
};
const clearFocusTrap = () => {
trap?.deactivate();
trap = null;
};
return {
trapRef,
initFocusTrap,
clearFocusTrap,
};
};
export default useFocusTrap;

9
layouts/default.vue Normal file
View File

@@ -0,0 +1,9 @@
<template>
<client-only>
<div class="flex flex-row items-center justify-center gap-6 p-6">
<NuxtLink to="/" class="underline hover:no-underline focus:no-underline">Test Vue Components</NuxtLink>
<NuxtLink to="/webComponents" class="underline hover:no-underline focus:no-underline">Test Web Components</NuxtLink>
</div>
<slot />
</client-only>
</template>

View File

@@ -37,6 +37,10 @@ export default defineNuxtConfig({
name: 'ConnectKeyActions',
path: '@/components/KeyActions.ce',
},
{
name: 'ConnectModals',
path: '@/components/Modals.ce',
},
{
name: 'ConnectUserProfile',
path: '@/components/UserProfile.ce',

13
package-lock.json generated
View File

@@ -2940,6 +2940,14 @@
"resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
"integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ=="
},
"focus-trap": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.4.3.tgz",
"integrity": "sha512-BgSSbK4GPnS2VbtZ50VtOv1Sti6DIkj3+LkVjiWMNjLeAp1SH1UlLx3ULu/DCu4vq5R4/uvTm+zrvsMsuYmGLg==",
"requires": {
"tabbable": "^6.1.2"
}
},
"follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
@@ -6341,6 +6349,11 @@
}
}
},
"tabbable": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.1.2.tgz",
"integrity": "sha512-qCN98uP7i9z0fIS4amQ5zbGBOq+OSigYeGvPy7NDk8Y9yncqDZ9pRPgfsc2PJIVM9RrJj7GIfuRgmjoUU9zTHQ=="
},
"tailwind-config-viewer": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/tailwind-config-viewer/-/tailwind-config-viewer-1.7.2.tgz",

View File

@@ -27,7 +27,8 @@
"@pinia/nuxt": "^0.4.11",
"@vueuse/components": "^10.1.2",
"crypto-js": "^4.1.1",
"dayjs": "^1.11.7"
"dayjs": "^1.11.7",
"focus-trap": "^7.4.3"
},
"overrides": {
"vue": "latest"

49
pages/index.vue Normal file
View File

@@ -0,0 +1,49 @@
<script lang="ts" setup>
import serverState from '../_data/serverState';
const nuxtApp = useNuxtApp();
onBeforeMount(() => {
nuxtApp.$customElements.registerEntry('ConnectComponents');
});
</script>
<template>
<div class="max-w-5xl mx-auto bg-gray-200">
<client-only>
<div class="flex flex-col gap-6 p-6">
<h2>Vue Components</h2>
<h3>UserProfileCe</h3>
<UserProfileCe :server="serverState" />
<hr />
<h3>DownloadApiLogsCe</h3>
<DownloadApiLogsCe />
<hr />
<h3>AuthCe</h3>
<AuthCe />
<hr />
<h3>KeyActionsCe</h3>
<KeyActionsCe />
<hr />
<h3>WanIpCheckCe</h3>
<WanIpCheckCe />
<hr />
<h3>ModalsCe</h3>
<ModalsCe />
</div>
</client-only>
</div>
</template>
<style lang="postcss" scoped>
h2 {
@apply text-xl font-semibold font-mono;
}
h3 {
@apply text-lg font-semibold font-mono;
}
hr {
@apply border-black;
}
</style>

49
pages/webComponents.vue Normal file
View File

@@ -0,0 +1,49 @@
<script lang="ts" setup>
import serverState from '../_data/serverState';
const nuxtApp = useNuxtApp();
onBeforeMount(() => {
nuxtApp.$customElements.registerEntry('ConnectComponents');
});
</script>
<template>
<client-only>
<div class="max-w-5xl mx-auto bg-gray-200">
<div class="flex flex-col gap-6 p-6">
<h2>Web Components</h2>
<h3>UserProfileCe</h3>
<connect-user-profile :server="JSON.stringify(serverState)"></connect-user-profile>
<hr />
<h3>DownloadApiLogsCe</h3>
<connect-download-api-logs></connect-download-api-logs>
<hr />
<h3>AuthCe</h3>
<connect-auth></connect-auth>
<hr />
<h3>KeyActionsCe</h3>
<connect-key-actions></connect-key-actions>
<hr />
<h3>WanIpCheckCe</h3>
<connect-wan-ip-check></connect-wan-ip-check>
<hr />
<h3>ModalsCe</h3>
<connect-modals></connect-modals>
</div>
</div>
</client-only>
</template>
<style lang="postcss" scoped>
h2 {
@apply text-xl font-semibold font-mono;
}
h3 {
@apply text-lg font-semibold font-mono;
}
hr {
@apply border-black;
}
</style>

View File

@@ -18,6 +18,7 @@ export const useCallbackStore = defineStore('callback', () => {
// const encryptKey = config.public.callbackKey;
const encryptKey = 'Uyv2o8e*FiQe8VeLekTqyX6Z*8XonB';
// state
const currentUrl = ref();
const callbackFeedbackVisible = ref<boolean>(false);
const decryptedData = ref();
const encryptedMessage = ref('');
@@ -40,8 +41,8 @@ export const useCallbackStore = defineStore('callback', () => {
const watcher = () => {
console.debug('[watcher]');
const currentUrl = new URL(window.location);
console.debug('[watcher]', currentUrl);
const callbackValue = currentUrl.searchParams.get('data');
console.debug('[watcher]', { callbackValue });
if (!callbackValue) {
return console.debug('[watcher] no callback to handle');
}
@@ -66,15 +67,23 @@ export const useCallbackStore = defineStore('callback', () => {
}
};
const hide = () => callbackFeedbackVisible.value = false;
const show = () => callbackFeedbackVisible.value = true;
const hide = () => {
console.debug('[hide]');
callbackFeedbackVisible.value = false;
};
const show = () => {
console.debug('[show]');
callbackFeedbackVisible.value = true;
}
const toggle = useToggle(callbackFeedbackVisible);
/**
* @todo consider removing query string once actions are done
*/
watch(callbackFeedbackVisible, (newVal, _oldVal) => {
console.debug('[callbackFeedbackVisible]', newVal, _oldVal);
console.debug('[callbackFeedbackVisible]', newVal);
// removing query string once actions are done so users can't refresh the page and go through the same actions
if (newVal === false) {
console.debug('[callbackFeedbackVisible] push history w/o query');
window.history.pushState(null, '', window.location.pathname);
}
});
return {