Add experiments folder. Add usage.md and SKILL.md. Fix Sheet interactions and add dynamic sheet mode.
This commit is contained in:
44
apps/demo/src/compatibility-lab.ts
Normal file
44
apps/demo/src/compatibility-lab.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { reactive, type InjectionKey, type Ref } from "vue";
|
||||
|
||||
export interface CompatibilityLabEvent {
|
||||
id: number;
|
||||
timestamp: string;
|
||||
source: string;
|
||||
hook: string;
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
export interface CompatibilityLabContext {
|
||||
source: string;
|
||||
routeLabel: Readonly<Ref<string>>;
|
||||
}
|
||||
|
||||
export const demoAppValueKey: InjectionKey<string> = Symbol("demo-app-value");
|
||||
export const compatibilityLabContextKey: InjectionKey<CompatibilityLabContext> =
|
||||
Symbol("compatibility-lab-context");
|
||||
|
||||
let eventSequence = 0;
|
||||
const startedAt = performance.now();
|
||||
|
||||
export const compatibilityLab = reactive({
|
||||
events: [] as CompatibilityLabEvent[],
|
||||
});
|
||||
|
||||
export function recordCompatibilityEvent(
|
||||
source: string,
|
||||
hook: string,
|
||||
detail?: string,
|
||||
) {
|
||||
compatibilityLab.events.unshift({
|
||||
id: ++eventSequence,
|
||||
timestamp: `${(performance.now() - startedAt).toFixed(0)} ms`,
|
||||
source,
|
||||
hook,
|
||||
detail,
|
||||
});
|
||||
if (compatibilityLab.events.length > 120) compatibilityLab.events.splice(120);
|
||||
}
|
||||
|
||||
export function clearCompatibilityEvents() {
|
||||
compatibilityLab.events.splice(0);
|
||||
}
|
||||
39
apps/demo/src/components/CompatibilityAsyncProbe.vue
Normal file
39
apps/demo/src/components/CompatibilityAsyncProbe.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import { inject } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import {
|
||||
compatibilityLabContextKey,
|
||||
demoAppValueKey,
|
||||
recordCompatibilityEvent,
|
||||
} from "../compatibility-lab";
|
||||
|
||||
const props = defineProps<{ requestId: number }>();
|
||||
const route = useRoute();
|
||||
const appValue = inject(demoAppValueKey, "missing app injection");
|
||||
const labContext = inject(compatibilityLabContextKey);
|
||||
|
||||
recordCompatibilityEvent(
|
||||
"Suspense",
|
||||
"async setup started",
|
||||
`request ${props.requestId}`,
|
||||
);
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 750));
|
||||
recordCompatibilityEvent(
|
||||
"Suspense",
|
||||
"async setup resolved",
|
||||
`request ${props.requestId}`,
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article
|
||||
class="compat-probe compat-probe--resolved"
|
||||
data-testid="compat-suspense-ready"
|
||||
>
|
||||
<span class="compat-probe__badge">Suspense resolved</span>
|
||||
<strong>Async request {{ requestId }} complete</strong>
|
||||
<p>{{ appValue }}</p>
|
||||
<p>{{ labContext?.routeLabel.value }}</p>
|
||||
<p>{{ route.fullPath }}</p>
|
||||
</article>
|
||||
</template>
|
||||
41
apps/demo/src/components/CompatibilityCompositionProbe.vue
Normal file
41
apps/demo/src/components/CompatibilityCompositionProbe.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
onBeforeMount,
|
||||
onBeforeUnmount,
|
||||
onBeforeUpdate,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
onUpdated,
|
||||
ref,
|
||||
} from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { recordCompatibilityEvent } from "../compatibility-lab";
|
||||
|
||||
const props = defineProps<{
|
||||
instanceName: string;
|
||||
revision: number;
|
||||
}>();
|
||||
const route = useRoute();
|
||||
const localCount = ref(0);
|
||||
const record = (hook: string) =>
|
||||
recordCompatibilityEvent(props.instanceName, hook, route.fullPath);
|
||||
|
||||
onBeforeMount(() => record("onBeforeMount"));
|
||||
onMounted(() => record("onMounted"));
|
||||
onBeforeUpdate(() => record("onBeforeUpdate"));
|
||||
onUpdated(() => record("onUpdated"));
|
||||
onBeforeUnmount(() => record("onBeforeUnmount"));
|
||||
onUnmounted(() => record("onUnmounted"));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article class="compat-probe" data-testid="composition-lifecycle-probe">
|
||||
<span class="compat-probe__badge">Composition API</span>
|
||||
<strong>{{ instanceName }}</strong>
|
||||
<p><code>useRoute()</code>: {{ route.fullPath }}</p>
|
||||
<p>Revision {{ revision }} · local count {{ localCount }}</p>
|
||||
<button type="button" @click="localCount += 1">
|
||||
Increment local state
|
||||
</button>
|
||||
</article>
|
||||
</template>
|
||||
29
apps/demo/src/components/CompatibilityInjectProbe.vue
Normal file
29
apps/demo/src/components/CompatibilityInjectProbe.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import {
|
||||
compatibilityLabContextKey,
|
||||
demoAppValueKey,
|
||||
} from "../compatibility-lab";
|
||||
|
||||
defineProps<{ location: "route tree" | "teleport" }>();
|
||||
|
||||
const route = useRoute();
|
||||
const appValue = inject(demoAppValueKey, "missing app injection");
|
||||
const labContext = inject(compatibilityLabContextKey);
|
||||
const labValue = computed(
|
||||
() => labContext?.routeLabel.value ?? "missing page injection",
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article
|
||||
class="compat-probe"
|
||||
:data-testid="`inject-probe-${location.replace(' ', '-')}`"
|
||||
>
|
||||
<span class="compat-probe__badge">provide / inject · {{ location }}</span>
|
||||
<strong>{{ appValue }}</strong>
|
||||
<p>Page injection: {{ labValue }}</p>
|
||||
<p>Scoped route: {{ route.fullPath }}</p>
|
||||
</article>
|
||||
</template>
|
||||
36
apps/demo/src/components/CompatibilityKeepAliveProbe.vue
Normal file
36
apps/demo/src/components/CompatibilityKeepAliveProbe.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
onActivated,
|
||||
onBeforeMount,
|
||||
onBeforeUnmount,
|
||||
onDeactivated,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
ref,
|
||||
} from "vue";
|
||||
import { recordCompatibilityEvent } from "../compatibility-lab";
|
||||
|
||||
const props = defineProps<{ name: string }>();
|
||||
const count = ref(0);
|
||||
const record = (hook: string) =>
|
||||
recordCompatibilityEvent(`KeepAlive ${props.name}`, hook);
|
||||
|
||||
onBeforeMount(() => record("onBeforeMount"));
|
||||
onMounted(() => record("onMounted"));
|
||||
onActivated(() => record("onActivated"));
|
||||
onDeactivated(() => record("onDeactivated"));
|
||||
onBeforeUnmount(() => record("onBeforeUnmount"));
|
||||
onUnmounted(() => record("onUnmounted"));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article
|
||||
class="compat-probe compat-probe--keepalive"
|
||||
:data-testid="`keep-alive-${name}`"
|
||||
>
|
||||
<span class="compat-probe__badge">Kept instance {{ name }}</span>
|
||||
<strong>Counter: {{ count }}</strong>
|
||||
<p>Increment, switch instances, then return to verify preserved state.</p>
|
||||
<button type="button" @click="count += 1">Increment {{ name }}</button>
|
||||
</article>
|
||||
</template>
|
||||
56
apps/demo/src/components/CompatibilityOptionsProbe.vue
Normal file
56
apps/demo/src/components/CompatibilityOptionsProbe.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import type { RouteLocationNormalizedLoaded } from "vue-router";
|
||||
import { recordCompatibilityEvent } from "../compatibility-lab";
|
||||
|
||||
export default defineComponent({
|
||||
name: "CompatibilityOptionsProbe",
|
||||
props: {
|
||||
instanceName: { type: String, required: true },
|
||||
revision: { type: Number, required: true },
|
||||
},
|
||||
data: () => ({ localCount: 0 }),
|
||||
beforeCreate() {
|
||||
recordCompatibilityEvent(this.instanceName, "beforeCreate");
|
||||
},
|
||||
created() {
|
||||
recordCompatibilityEvent(this.instanceName, "created", this.routePath());
|
||||
},
|
||||
beforeMount() {
|
||||
recordCompatibilityEvent(this.instanceName, "beforeMount");
|
||||
},
|
||||
mounted() {
|
||||
recordCompatibilityEvent(this.instanceName, "mounted");
|
||||
},
|
||||
beforeUpdate() {
|
||||
recordCompatibilityEvent(this.instanceName, "beforeUpdate");
|
||||
},
|
||||
updated() {
|
||||
recordCompatibilityEvent(this.instanceName, "updated");
|
||||
},
|
||||
beforeUnmount() {
|
||||
recordCompatibilityEvent(this.instanceName, "beforeUnmount");
|
||||
},
|
||||
unmounted() {
|
||||
recordCompatibilityEvent(this.instanceName, "unmounted");
|
||||
},
|
||||
methods: {
|
||||
routePath() {
|
||||
return (this as unknown as { $route: RouteLocationNormalizedLoaded })
|
||||
.$route.fullPath;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article class="compat-probe" data-testid="options-lifecycle-probe">
|
||||
<span class="compat-probe__badge">Options API</span>
|
||||
<strong>{{ instanceName }}</strong>
|
||||
<p><code>$route</code>: {{ routePath() }}</p>
|
||||
<p>Revision {{ revision }} · local count {{ localCount }}</p>
|
||||
<button type="button" @click="localCount += 1">
|
||||
Increment local state
|
||||
</button>
|
||||
</article>
|
||||
</template>
|
||||
@@ -3,6 +3,7 @@ import { createNativeRouter } from "@native-vue-router/core";
|
||||
import { createCapacitorAdapter } from "@native-vue-router/capacitor";
|
||||
import { createElectronRendererAdapter } from "@native-vue-router/electron";
|
||||
import App from "./App.vue";
|
||||
import { demoAppValueKey } from "./compatibility-lab";
|
||||
import { createPwaAdapter } from "./pwa";
|
||||
import { router } from "./router";
|
||||
import "./style.css";
|
||||
@@ -27,6 +28,7 @@ const nativeRouter = createNativeRouter({
|
||||
const app = createApp(App);
|
||||
app.use(router);
|
||||
app.use(nativeRouter);
|
||||
app.provide(demoAppValueKey, "Injected from the demo application root");
|
||||
|
||||
await router.isReady();
|
||||
app.mount("#app");
|
||||
|
||||
@@ -104,6 +104,31 @@ const routes: RouteRecordRaw[] = [
|
||||
native: { presentation: "push", parent: "/profile", gesture: "edge" },
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/profile/vue-lab/:sample",
|
||||
name: "vue-compatibility",
|
||||
component: () => import("./views/VueCompatibilityView.vue"),
|
||||
meta: {
|
||||
native: { presentation: "push", parent: "/profile", gesture: "edge" },
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/profile/vue-lab/:sample/away",
|
||||
name: "vue-compatibility-away",
|
||||
component: () => import("./views/VueCompatibilityAwayView.vue"),
|
||||
meta: {
|
||||
native: {
|
||||
presentation: "push",
|
||||
parent: (route) => ({
|
||||
name: "vue-compatibility",
|
||||
params: { sample: route.params.sample },
|
||||
query: route.query,
|
||||
hash: route.hash,
|
||||
}),
|
||||
gesture: "edge",
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const router = createRouter({
|
||||
|
||||
@@ -28,7 +28,8 @@ html,
|
||||
body,
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* Commenting this out for now. Its causing a black bar at the bottom of the screen on iOS */
|
||||
/* height: 100%; */
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
overscroll-behavior: none;
|
||||
@@ -65,7 +66,7 @@ html[data-pwa-edge-guard="active"] body {
|
||||
.app-frame {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100dvh;
|
||||
height: 100vh;
|
||||
max-width: 740px;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
@@ -662,18 +663,19 @@ html[data-pwa-edge-guard="active"] body {
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
.sheet-screen {
|
||||
border-radius: 22px 22px 0 0;
|
||||
.compose-sheet {
|
||||
--nvr-sheet-background: #0b0d12;
|
||||
--nvr-sheet-backdrop: rgba(0, 0, 0, 0.42);
|
||||
}
|
||||
.sheet-handle {
|
||||
position: sticky;
|
||||
z-index: 12;
|
||||
top: 8px;
|
||||
width: 38px;
|
||||
height: 5px;
|
||||
margin: 8px auto 0;
|
||||
border-radius: 4px;
|
||||
background: #4e5360;
|
||||
.compose-sheet-content {
|
||||
min-height: 0;
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at 88% 4%,
|
||||
rgba(124, 92, 255, 0.09),
|
||||
transparent 24%
|
||||
),
|
||||
#0b0d12;
|
||||
}
|
||||
.sheet-header {
|
||||
display: grid;
|
||||
@@ -685,12 +687,23 @@ html[data-pwa-edge-guard="active"] body {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
.sheet-header small {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
}
|
||||
.sheet-header button {
|
||||
justify-self: start;
|
||||
border: 0;
|
||||
color: #9b88ff;
|
||||
background: transparent;
|
||||
}
|
||||
.sheet-header button:last-child {
|
||||
justify-self: end;
|
||||
font-size: 11px;
|
||||
}
|
||||
.compose-search {
|
||||
margin: 0 16px 14px;
|
||||
}
|
||||
@@ -884,6 +897,226 @@ html[data-pwa-edge-guard="active"] body {
|
||||
background: #ff6c76;
|
||||
box-shadow: 0 0 9px #ff6c76;
|
||||
}
|
||||
|
||||
.compatibility-screen {
|
||||
padding-bottom: calc(34px + env(safe-area-inset-bottom));
|
||||
}
|
||||
.compat-header-state {
|
||||
padding: 5px 8px;
|
||||
border: 1px solid rgba(255, 194, 92, 0.28);
|
||||
border-radius: 999px;
|
||||
color: #ffc86c;
|
||||
background: rgba(255, 194, 92, 0.08);
|
||||
font-size: 9px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.compat-header-state.active {
|
||||
border-color: rgba(61, 217, 170, 0.3);
|
||||
color: #6ce5bf;
|
||||
background: rgba(61, 217, 170, 0.08);
|
||||
}
|
||||
.compat-intro > span {
|
||||
width: 64px;
|
||||
font-size: 15px;
|
||||
}
|
||||
.compat-route-state code {
|
||||
max-width: 52%;
|
||||
overflow-wrap: anywhere;
|
||||
color: #b9adff;
|
||||
font-size: 11px;
|
||||
text-align: right;
|
||||
}
|
||||
.compat-controls {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
margin: 0 16px 18px;
|
||||
}
|
||||
.compat-controls--three {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
.compat-controls button,
|
||||
.compat-inline-actions button,
|
||||
.compat-probe button,
|
||||
.compat-event-heading button {
|
||||
min-height: 42px;
|
||||
border: 1px solid rgba(124, 92, 255, 0.25);
|
||||
border-radius: 12px;
|
||||
color: #c7beff;
|
||||
background: rgba(124, 92, 255, 0.1);
|
||||
font-size: 11px;
|
||||
font-weight: 750;
|
||||
}
|
||||
.settings-group > .compat-inline-actions {
|
||||
min-height: auto;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.compat-inline-actions button {
|
||||
flex: 1;
|
||||
}
|
||||
.settings-group > .compat-probe-grid {
|
||||
display: grid;
|
||||
min-height: 0;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
}
|
||||
.compat-probe {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
margin: 10px 12px;
|
||||
padding: 13px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 15px;
|
||||
background: rgba(255, 255, 255, 0.025);
|
||||
}
|
||||
.compat-probe-grid .compat-probe {
|
||||
margin: 0;
|
||||
}
|
||||
.compat-probe__badge {
|
||||
color: #a998ff;
|
||||
font-size: 9px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.compat-probe strong {
|
||||
font-size: 13px;
|
||||
}
|
||||
.compat-probe p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 10px;
|
||||
line-height: 1.45;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.compat-probe code {
|
||||
color: #b9adff;
|
||||
}
|
||||
.compat-probe button {
|
||||
width: 100%;
|
||||
margin-top: auto;
|
||||
}
|
||||
.compat-probe--keepalive {
|
||||
border-color: rgba(61, 217, 170, 0.16);
|
||||
background: rgba(61, 217, 170, 0.045);
|
||||
}
|
||||
.compat-probe--resolved {
|
||||
border-color: rgba(61, 217, 170, 0.2);
|
||||
}
|
||||
.compat-probe--loading {
|
||||
min-height: 96px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.compat-transition-card {
|
||||
margin: 10px 12px;
|
||||
padding: 22px 14px;
|
||||
border: 1px solid rgba(255, 194, 92, 0.22);
|
||||
border-radius: 15px;
|
||||
color: #ffd28a;
|
||||
background: rgba(255, 194, 92, 0.07);
|
||||
font-size: 12px;
|
||||
font-weight: 750;
|
||||
text-align: center;
|
||||
}
|
||||
.compat-fade-enter-active,
|
||||
.compat-fade-leave-active {
|
||||
transition:
|
||||
opacity 220ms ease,
|
||||
transform 220ms ease;
|
||||
}
|
||||
.compat-fade-enter-from,
|
||||
.compat-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(8px) scale(0.98);
|
||||
}
|
||||
.compat-teleport {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 20px;
|
||||
background: rgba(2, 3, 6, 0.74);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
.compat-teleport > section {
|
||||
position: relative;
|
||||
width: min(440px, 100%);
|
||||
padding: 20px;
|
||||
border: 1px solid rgba(169, 152, 255, 0.32);
|
||||
border-radius: 22px;
|
||||
background: #141720;
|
||||
box-shadow: 0 28px 90px rgba(0, 0, 0, 0.58);
|
||||
}
|
||||
.compat-teleport > section > button {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
font-size: 20px;
|
||||
}
|
||||
.compat-teleport h2 {
|
||||
margin: 0 44px 15px 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
.compat-teleport .compat-probe {
|
||||
margin: 0;
|
||||
}
|
||||
.settings-group > .compat-event-heading {
|
||||
min-height: 66px;
|
||||
}
|
||||
.compat-event-heading button {
|
||||
min-height: 34px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
.compat-event-log {
|
||||
max-height: 360px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
border-top: 1px solid var(--line);
|
||||
list-style: none;
|
||||
}
|
||||
.compat-event-log li {
|
||||
display: grid;
|
||||
grid-template-columns: 58px minmax(0, 1fr) auto;
|
||||
gap: 10px;
|
||||
align-items: start;
|
||||
padding: 9px 13px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.045);
|
||||
}
|
||||
.compat-event-log time,
|
||||
.compat-event-log small {
|
||||
color: #686e7b;
|
||||
font-size: 9px;
|
||||
}
|
||||
.compat-event-log span {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.compat-event-log strong {
|
||||
overflow: hidden;
|
||||
font-size: 10px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.compat-event-log code {
|
||||
color: #afa1ff;
|
||||
font-size: 10px;
|
||||
}
|
||||
.empty-state {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
@@ -926,6 +1159,19 @@ html[data-pwa-edge-guard="active"] body {
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.compat-controls--three,
|
||||
.settings-group > .compat-probe-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.compat-event-log li {
|
||||
grid-template-columns: 52px minmax(0, 1fr);
|
||||
}
|
||||
.compat-event-log li > small {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
|
||||
@@ -1,37 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { NativeDismissGesture, useNativeRouter } from '@native-vue-router/core'
|
||||
import AppAvatar from '../components/AppAvatar.vue'
|
||||
import { useDemoStore } from '../data'
|
||||
import { computed, ref } from "vue";
|
||||
import { NativeSheet, useNativeRouter } from "@native-vue-router/core";
|
||||
import AppAvatar from "../components/AppAvatar.vue";
|
||||
import { useDemoStore } from "../data";
|
||||
|
||||
const native = useNativeRouter()
|
||||
const store = useDemoStore()
|
||||
const query = ref('')
|
||||
const native = useNativeRouter();
|
||||
const store = useDemoStore();
|
||||
const query = ref("");
|
||||
const sheetBreakpoint = ref(0.62);
|
||||
const snapEnabled = ref(true);
|
||||
const sheetBreakpoints = computed(() =>
|
||||
snapEnabled.value ? [0.38, 0.62, 1] : [],
|
||||
);
|
||||
|
||||
async function choose(id: string) {
|
||||
await native.cancelInteractive()
|
||||
await native.replace(`/chat/${id}`, { presentation: 'push' })
|
||||
await native.cancelInteractive();
|
||||
await native.replace(`/chat/${id}`, { presentation: "push" });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NativeDismissGesture as="main" class="screen sheet-screen">
|
||||
<div class="sheet-handle" aria-hidden="true" />
|
||||
<header class="sheet-header">
|
||||
<button type="button" @click="native.dismiss()">Cancel</button>
|
||||
<h1>New message</h1>
|
||||
<span />
|
||||
</header>
|
||||
<label class="search-field compose-search">
|
||||
<span>To:</span>
|
||||
<input v-model="query" autofocus placeholder="Search people" />
|
||||
</label>
|
||||
<div class="conversation-list">
|
||||
<button v-for="person in store.people.value.filter((item) => item.name.toLowerCase().includes(query.toLowerCase()))" :key="person.id" class="conversation-row" @click="choose(person.id)">
|
||||
<AppAvatar :person="person" />
|
||||
<div class="conversation-copy"><strong>{{ person.name }}</strong><p>{{ person.handle }}</p></div>
|
||||
<span class="chevron">›</span>
|
||||
</button>
|
||||
</div>
|
||||
</NativeDismissGesture>
|
||||
<NativeSheet
|
||||
v-model="sheetBreakpoint"
|
||||
class="compose-sheet"
|
||||
aria-label="New message"
|
||||
:breakpoints="sheetBreakpoints"
|
||||
:initial-breakpoint="0.62"
|
||||
>
|
||||
<main class="compose-sheet-content">
|
||||
<header class="sheet-header">
|
||||
<button type="button" @click="native.dismiss()">Cancel</button>
|
||||
<div>
|
||||
<h1>New message</h1>
|
||||
<small data-testid="sheet-size">
|
||||
{{ snapEnabled ? `${Math.round(sheetBreakpoint * 100)}%` : "Auto" }}
|
||||
</small>
|
||||
</div>
|
||||
<button type="button" @click="snapEnabled = !snapEnabled">
|
||||
{{ snapEnabled ? "Fit content" : "Use snap points" }}
|
||||
</button>
|
||||
</header>
|
||||
<label class="search-field compose-search">
|
||||
<span>To:</span>
|
||||
<input v-model="query" autofocus placeholder="Search people" />
|
||||
</label>
|
||||
<div class="conversation-list">
|
||||
<button
|
||||
v-for="person in store.people.value.filter((item) =>
|
||||
item.name.toLowerCase().includes(query.toLowerCase()),
|
||||
)"
|
||||
:key="person.id"
|
||||
class="conversation-row"
|
||||
@click="choose(person.id)"
|
||||
>
|
||||
<AppAvatar :person="person" />
|
||||
<div class="conversation-copy">
|
||||
<strong>{{ person.name }}</strong>
|
||||
<p>{{ person.handle }}</p>
|
||||
</div>
|
||||
<span class="chevron">›</span>
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
</NativeSheet>
|
||||
</template>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { NativeLink, useNativeRouter } from '@native-vue-router/core'
|
||||
import AppHeader from '../components/AppHeader.vue'
|
||||
import { setStoryEntryBlocked, storyEntryGuard } from '../guard-state'
|
||||
import { NativeLink, useNativeRouter } from "@native-vue-router/core";
|
||||
import AppHeader from "../components/AppHeader.vue";
|
||||
import { setStoryEntryBlocked, storyEntryGuard } from "../guard-state";
|
||||
|
||||
const native = useNativeRouter()
|
||||
const native = useNativeRouter();
|
||||
|
||||
function toggleStoryGuard() {
|
||||
setStoryEntryBlocked(!storyEntryGuard.blockEntry)
|
||||
setStoryEntryBlocked(!storyEntryGuard.blockEntry);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -24,13 +24,35 @@ function toggleStoryGuard() {
|
||||
</div>
|
||||
</section>
|
||||
<section class="settings-list">
|
||||
<a href="/profile/runtime-lab" @click.prevent="native.sibling('/profile/runtime-lab')"><span>⌁</span><strong>Runtime stress lab</strong><i>›</i></a>
|
||||
<button type="button" aria-label="Block Stories re-entry" :aria-pressed="storyEntryGuard.blockEntry" @click="toggleStoryGuard">
|
||||
<span>⌽</span><strong>Block cached Stories re-entry</strong><i data-testid="story-guard-status">{{ storyEntryGuard.blockEntry ? storyEntryGuard.status : 'Off' }}</i>
|
||||
<a
|
||||
href="/profile/runtime-lab"
|
||||
@click.prevent="native.sibling('/profile/runtime-lab')"
|
||||
><span>⌁</span><strong>Runtime stress lab</strong><i>›</i></a
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Block Stories re-entry"
|
||||
:aria-pressed="storyEntryGuard.blockEntry"
|
||||
@click="toggleStoryGuard"
|
||||
>
|
||||
<span>⌽</span><strong>Block cached Stories re-entry</strong
|
||||
><i data-testid="story-guard-status">{{
|
||||
storyEntryGuard.blockEntry ? storyEntryGuard.status : "Off"
|
||||
}}</i>
|
||||
</button>
|
||||
<NativeLink to="/settings"
|
||||
><span>⚙︎</span><strong>Navigation lab</strong><i>›</i></NativeLink
|
||||
>
|
||||
<NativeLink to="/profile/vue-lab/alpha?mode=manual#route-state"
|
||||
><span>Vue</span><strong>Vue compatibility lab</strong
|
||||
><i>›</i></NativeLink
|
||||
>
|
||||
<a href="https://github.com" target="_blank" rel="noreferrer"
|
||||
><span>⌘</span><strong>Project source</strong><i>↗</i></a
|
||||
>
|
||||
<button type="button">
|
||||
<span>◐</span><strong>Appearance</strong><i>System</i>
|
||||
</button>
|
||||
<NativeLink to="/settings"><span>⚙︎</span><strong>Navigation lab</strong><i>›</i></NativeLink>
|
||||
<a href="https://github.com" target="_blank" rel="noreferrer"><span>⌘</span><strong>Project source</strong><i>↗</i></a>
|
||||
<button type="button"><span>◐</span><strong>Appearance</strong><i>System</i></button>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
103
apps/demo/src/views/VueCompatibilityAwayView.vue
Normal file
103
apps/demo/src/views/VueCompatibilityAwayView.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { useNativeRouter } from "@native-vue-router/core";
|
||||
import { useRoute } from "vue-router";
|
||||
import AppHeader from "../components/AppHeader.vue";
|
||||
import {
|
||||
compatibilityLab,
|
||||
recordCompatibilityEvent,
|
||||
} from "../compatibility-lab";
|
||||
|
||||
const route = useRoute();
|
||||
const native = useNativeRouter();
|
||||
const unloadStatus = ref("Lab remains cached");
|
||||
const labLocation = computed(() => ({
|
||||
name: "vue-compatibility",
|
||||
params: { sample: route.params.sample },
|
||||
query: route.query,
|
||||
hash: route.hash,
|
||||
}));
|
||||
|
||||
function unloadLab() {
|
||||
const count = native.unload(labLocation.value);
|
||||
unloadStatus.value = count
|
||||
? "Lab view evicted; Back will create a new instance"
|
||||
: "No matching inactive lab view was mounted";
|
||||
recordCompatibilityEvent("Away screen", "native.unload", `${count} view(s)`);
|
||||
}
|
||||
|
||||
async function unloadAndReturn() {
|
||||
unloadLab();
|
||||
await native.pop();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main
|
||||
class="screen compatibility-screen"
|
||||
data-testid="vue-compatibility-away"
|
||||
>
|
||||
<AppHeader
|
||||
title="Cached-route checkpoint"
|
||||
subtitle="The compatibility lab is behind this view"
|
||||
back
|
||||
/>
|
||||
|
||||
<section class="lab-intro compat-intro">
|
||||
<span>↩</span>
|
||||
<div>
|
||||
<strong>Inspect native cache behavior</strong>
|
||||
<p>
|
||||
The previous lab instance is inactive but still mounted until you
|
||||
explicitly unload it.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-group">
|
||||
<h2>Inactive route controls</h2>
|
||||
<div>
|
||||
<span
|
||||
><strong>Compatibility lab</strong
|
||||
><small>{{ unloadStatus }}</small></span
|
||||
><b>behind</b>
|
||||
</div>
|
||||
<p>
|
||||
Use normal Back to observe native activate/show without Vue remount
|
||||
hooks. Use “Unload and return” to observe beforeUnmount/unmounted
|
||||
followed by a fresh component instance.
|
||||
</p>
|
||||
</section>
|
||||
<div class="compat-controls">
|
||||
<button type="button" data-testid="compat-unload-lab" @click="unloadLab">
|
||||
Unload cached lab
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="compat-unload-return"
|
||||
@click="unloadAndReturn"
|
||||
>
|
||||
Unload and return
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section class="settings-group compat-event-section">
|
||||
<div class="compat-event-heading">
|
||||
<span
|
||||
><strong>Shared lifecycle journal</strong
|
||||
><small>Events survive route eviction</small></span
|
||||
>
|
||||
</div>
|
||||
<ol class="compat-event-log" data-testid="compat-away-event-log">
|
||||
<li v-for="event in compatibilityLab.events" :key="event.id">
|
||||
<time>{{ event.timestamp }}</time>
|
||||
<span
|
||||
><strong>{{ event.source }}</strong
|
||||
><code>{{ event.hook }}</code></span
|
||||
>
|
||||
<small>{{ event.detail }}</small>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
427
apps/demo/src/views/VueCompatibilityView.vue
Normal file
427
apps/demo/src/views/VueCompatibilityView.vue
Normal file
@@ -0,0 +1,427 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
computed,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
provide,
|
||||
ref,
|
||||
} from "vue";
|
||||
import {
|
||||
onNativeViewActivate,
|
||||
onNativeViewDeactivate,
|
||||
onNativeViewEvict,
|
||||
onNativeViewHide,
|
||||
onNativeViewShow,
|
||||
useNativeRouter,
|
||||
useNativeViewLifecycle,
|
||||
} from "@native-vue-router/core";
|
||||
import { useRoute } from "vue-router";
|
||||
import AppHeader from "../components/AppHeader.vue";
|
||||
import CompatibilityAsyncProbe from "../components/CompatibilityAsyncProbe.vue";
|
||||
import CompatibilityCompositionProbe from "../components/CompatibilityCompositionProbe.vue";
|
||||
import CompatibilityInjectProbe from "../components/CompatibilityInjectProbe.vue";
|
||||
import CompatibilityKeepAliveProbe from "../components/CompatibilityKeepAliveProbe.vue";
|
||||
import CompatibilityOptionsProbe from "../components/CompatibilityOptionsProbe.vue";
|
||||
import {
|
||||
clearCompatibilityEvents,
|
||||
compatibilityLab,
|
||||
compatibilityLabContextKey,
|
||||
recordCompatibilityEvent,
|
||||
} from "../compatibility-lab";
|
||||
|
||||
const route = useRoute();
|
||||
const native = useNativeRouter();
|
||||
const nativeLifecycle = useNativeViewLifecycle();
|
||||
const instanceId = Math.random().toString(36).slice(2, 7);
|
||||
const revision = ref(0);
|
||||
const probesMounted = ref(true);
|
||||
const keepAliveVariant = ref<"A" | "B">("A");
|
||||
const transitionVisible = ref(true);
|
||||
const teleportOpen = ref(false);
|
||||
const suspenseRequest = ref(1);
|
||||
const routeLabel = computed(() => route.fullPath);
|
||||
const nativeVisible = nativeLifecycle.isVisible;
|
||||
const nativeActive = nativeLifecycle.isActive;
|
||||
const eventCount = computed(() => compatibilityLab.events.length);
|
||||
|
||||
provide(compatibilityLabContextKey, {
|
||||
source: `Vue compatibility lab ${instanceId}`,
|
||||
routeLabel,
|
||||
});
|
||||
|
||||
const recordPageEvent = (hook: string, detail?: string) =>
|
||||
recordCompatibilityEvent(`Lab page ${instanceId}`, hook, detail);
|
||||
|
||||
onMounted(() => recordPageEvent("onMounted", route.fullPath));
|
||||
onBeforeUnmount(() => recordPageEvent("onBeforeUnmount"));
|
||||
onUnmounted(() => recordPageEvent("onUnmounted"));
|
||||
onNativeViewActivate(() => recordPageEvent("native activate"));
|
||||
onNativeViewDeactivate(() => recordPageEvent("native deactivate"));
|
||||
onNativeViewShow(() => recordPageEvent("native show"));
|
||||
onNativeViewHide(() => recordPageEvent("native hide"));
|
||||
onNativeViewEvict((reason) => recordPageEvent("native evict", String(reason)));
|
||||
|
||||
function updateProbes() {
|
||||
revision.value += 1;
|
||||
recordPageEvent("revision changed", String(revision.value));
|
||||
}
|
||||
|
||||
function toggleProbeMount() {
|
||||
probesMounted.value = !probesMounted.value;
|
||||
recordPageEvent(probesMounted.value ? "probes inserted" : "probes removed");
|
||||
}
|
||||
|
||||
function switchKeptInstance() {
|
||||
keepAliveVariant.value = keepAliveVariant.value === "A" ? "B" : "A";
|
||||
}
|
||||
|
||||
function recordTransition(hook: string) {
|
||||
recordCompatibilityEvent("Transition", hook);
|
||||
}
|
||||
|
||||
function reloadSuspense() {
|
||||
suspenseRequest.value += 1;
|
||||
}
|
||||
|
||||
function pushAlternateRoute() {
|
||||
const sample = route.params.sample === "alpha" ? "beta" : "alpha";
|
||||
void native.push({
|
||||
name: "vue-compatibility",
|
||||
params: { sample },
|
||||
query: { mode: "parameter", revision: revision.value },
|
||||
hash: "#route-state",
|
||||
});
|
||||
}
|
||||
|
||||
function replaceQueryAndHash() {
|
||||
void native.replace(
|
||||
{
|
||||
name: "vue-compatibility",
|
||||
params: { sample: route.params.sample },
|
||||
query: { mode: "replaced", tick: Date.now().toString().slice(-5) },
|
||||
hash: "#event-log",
|
||||
},
|
||||
{ presentation: "fade" },
|
||||
);
|
||||
}
|
||||
|
||||
function openAwayRoute() {
|
||||
void native.push({
|
||||
name: "vue-compatibility-away",
|
||||
params: { sample: route.params.sample },
|
||||
query: route.query,
|
||||
hash: route.hash,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main
|
||||
class="screen compatibility-screen"
|
||||
data-testid="vue-compatibility-view"
|
||||
:data-instance-id="instanceId"
|
||||
>
|
||||
<AppHeader
|
||||
title="Vue compatibility"
|
||||
subtitle="Native component laboratory"
|
||||
back
|
||||
>
|
||||
<span class="compat-header-state" :class="{ active: nativeActive }">{{
|
||||
nativeActive ? "active" : nativeVisible ? "transitioning" : "cached"
|
||||
}}</span>
|
||||
</AppHeader>
|
||||
|
||||
<section class="lab-intro compat-intro">
|
||||
<span>Vue</span>
|
||||
<div>
|
||||
<strong>Exercise real framework behavior</strong>
|
||||
<p>
|
||||
Every control below runs inside a routed, cached NativeRouterView
|
||||
entry.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
id="route-state"
|
||||
class="settings-group compat-route-state"
|
||||
data-testid="compat-route-state"
|
||||
>
|
||||
<h2>Scoped route state</h2>
|
||||
<div>
|
||||
<span
|
||||
><strong>Full path</strong
|
||||
><small>useRoute() and Options API $route</small></span
|
||||
><code data-testid="compat-full-path">{{ route.fullPath }}</code>
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
><strong>Named route</strong
|
||||
><small>Matched record identity</small></span
|
||||
><code>{{ String(route.name) }}</code>
|
||||
</div>
|
||||
<div>
|
||||
<span><strong>Param</strong><small>route.params.sample</small></span
|
||||
><code data-testid="compat-param">{{ route.params.sample }}</code>
|
||||
</div>
|
||||
<div>
|
||||
<span><strong>Query</strong><small>route.query</small></span
|
||||
><code data-testid="compat-query">{{
|
||||
JSON.stringify(route.query)
|
||||
}}</code>
|
||||
</div>
|
||||
<div>
|
||||
<span><strong>Hash</strong><small>route.hash</small></span
|
||||
><code data-testid="compat-hash">{{ route.hash || "(empty)" }}</code>
|
||||
</div>
|
||||
<div>
|
||||
<span><strong>Matched</strong><small>route.matched</small></span
|
||||
><code>{{
|
||||
route.matched.map((record) => String(record.name)).join(" → ")
|
||||
}}</code>
|
||||
</div>
|
||||
</section>
|
||||
<div class="compat-controls compat-controls--three">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="compat-change-param"
|
||||
@click="pushAlternateRoute"
|
||||
>
|
||||
Push alternate param
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="compat-change-query"
|
||||
@click="replaceQueryAndHash"
|
||||
>
|
||||
Replace query + hash
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="compat-open-away"
|
||||
@click="openAwayRoute"
|
||||
>
|
||||
Cache this view
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section class="settings-group">
|
||||
<h2>provide() / inject()</h2>
|
||||
<CompatibilityInjectProbe location="route tree" />
|
||||
<p>
|
||||
The same probe is rendered inside the Teleport below to verify
|
||||
logical-tree injection and scoped routing.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="settings-group">
|
||||
<h2>Options and Composition lifecycle</h2>
|
||||
<div>
|
||||
<span
|
||||
><strong>Shared revision</strong
|
||||
><small>Changing it triggers beforeUpdate / updated</small></span
|
||||
><b data-testid="compat-revision">{{ revision }}</b>
|
||||
</div>
|
||||
<div class="compat-inline-actions">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="compat-update-probes"
|
||||
@click="updateProbes"
|
||||
>
|
||||
Update props
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="compat-toggle-probes"
|
||||
@click="toggleProbeMount"
|
||||
>
|
||||
{{ probesMounted ? "Unmount probes" : "Mount probes" }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="probesMounted" class="compat-probe-grid">
|
||||
<CompatibilityOptionsProbe
|
||||
:instance-name="`Options ${instanceId}`"
|
||||
:revision="revision"
|
||||
/>
|
||||
<CompatibilityCompositionProbe
|
||||
:instance-name="`Composition ${instanceId}`"
|
||||
:revision="revision"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-group">
|
||||
<h2>KeepAlive</h2>
|
||||
<div>
|
||||
<span
|
||||
><strong>Current cached child</strong
|
||||
><small>Switch away and back after incrementing</small></span
|
||||
><b>{{ keepAliveVariant }}</b>
|
||||
</div>
|
||||
<div class="compat-inline-actions">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="compat-switch-keepalive"
|
||||
@click="switchKeptInstance"
|
||||
>
|
||||
Switch to {{ keepAliveVariant === "A" ? "B" : "A" }}
|
||||
</button>
|
||||
</div>
|
||||
<KeepAlive>
|
||||
<CompatibilityKeepAliveProbe
|
||||
:key="keepAliveVariant"
|
||||
:name="keepAliveVariant"
|
||||
/>
|
||||
</KeepAlive>
|
||||
</section>
|
||||
|
||||
<section class="settings-group">
|
||||
<h2>Transition</h2>
|
||||
<div>
|
||||
<span
|
||||
><strong>CSS transition target</strong
|
||||
><small>Hooks are recorded in the event journal</small></span
|
||||
><b>{{ transitionVisible ? "shown" : "removed" }}</b>
|
||||
</div>
|
||||
<div class="compat-inline-actions">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="compat-toggle-transition"
|
||||
@click="transitionVisible = !transitionVisible"
|
||||
>
|
||||
Toggle transition
|
||||
</button>
|
||||
</div>
|
||||
<Transition
|
||||
name="compat-fade"
|
||||
@before-enter="recordTransition('before-enter')"
|
||||
@after-enter="recordTransition('after-enter')"
|
||||
@before-leave="recordTransition('before-leave')"
|
||||
@after-leave="recordTransition('after-leave')"
|
||||
>
|
||||
<article
|
||||
v-if="transitionVisible"
|
||||
class="compat-transition-card"
|
||||
data-testid="compat-transition-card"
|
||||
>
|
||||
Transition child is mounted
|
||||
</article>
|
||||
</Transition>
|
||||
</section>
|
||||
|
||||
<section class="settings-group">
|
||||
<h2>Teleport</h2>
|
||||
<div>
|
||||
<span
|
||||
><strong>Body-level overlay</strong
|
||||
><small
|
||||
>Automatically hidden when this native view is inactive</small
|
||||
></span
|
||||
><b>{{ teleportOpen ? "armed" : "closed" }}</b>
|
||||
</div>
|
||||
<div class="compat-inline-actions">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="compat-open-teleport"
|
||||
@click="teleportOpen = true"
|
||||
>
|
||||
Open teleported overlay
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Teleport to="body">
|
||||
<Transition name="compat-fade">
|
||||
<div
|
||||
v-if="teleportOpen && nativeVisible"
|
||||
class="compat-teleport"
|
||||
data-testid="compat-teleport-overlay"
|
||||
@click.self="teleportOpen = false"
|
||||
>
|
||||
<section
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="compat-teleport-title"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Close teleported overlay"
|
||||
@click="teleportOpen = false"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<h2 id="compat-teleport-title">Teleported route content</h2>
|
||||
<CompatibilityInjectProbe location="teleport" />
|
||||
</section>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
|
||||
<section class="settings-group">
|
||||
<h2>Suspense</h2>
|
||||
<div>
|
||||
<span
|
||||
><strong>Async setup request</strong
|
||||
><small>Fallback remains for 750 ms</small></span
|
||||
><b>#{{ suspenseRequest }}</b>
|
||||
</div>
|
||||
<div class="compat-inline-actions">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="compat-reload-suspense"
|
||||
@click="reloadSuspense"
|
||||
>
|
||||
Reload async child
|
||||
</button>
|
||||
</div>
|
||||
<Suspense
|
||||
:key="suspenseRequest"
|
||||
:timeout="0"
|
||||
@pending="recordCompatibilityEvent('Suspense', 'pending')"
|
||||
@fallback="recordCompatibilityEvent('Suspense', 'fallback')"
|
||||
@resolve="recordCompatibilityEvent('Suspense', 'resolve')"
|
||||
>
|
||||
<CompatibilityAsyncProbe :request-id="suspenseRequest" />
|
||||
<template #fallback>
|
||||
<article
|
||||
class="compat-probe compat-probe--loading"
|
||||
data-testid="compat-suspense-fallback"
|
||||
>
|
||||
<span class="lab-spinner" />
|
||||
<div>
|
||||
<strong>Suspense fallback</strong>
|
||||
<p>Waiting for async setup…</p>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
</Suspense>
|
||||
</section>
|
||||
|
||||
<section id="event-log" class="settings-group compat-event-section">
|
||||
<div class="compat-event-heading">
|
||||
<span
|
||||
><strong>Lifecycle event journal</strong
|
||||
><small>{{ eventCount }} retained events · newest first</small></span
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="compat-clear-events"
|
||||
@click="clearCompatibilityEvents"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
<ol class="compat-event-log" data-testid="compat-event-log">
|
||||
<li v-for="event in compatibilityLab.events" :key="event.id">
|
||||
<time>{{ event.timestamp }}</time>
|
||||
<span
|
||||
><strong>{{ event.source }}</strong
|
||||
><code>{{ event.hook }}</code></span
|
||||
>
|
||||
<small>{{ event.detail }}</small>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
Reference in New Issue
Block a user