rerfactor: add page transition toggle
This commit is contained in:
parent
e61df14ec0
commit
acca33a7dc
5 changed files with 227 additions and 42 deletions
|
|
@ -151,3 +151,19 @@
|
||||||
--ui-container: 96rem;
|
--ui-container: 96rem;
|
||||||
--ui-header-height: 4.25rem;
|
--ui-header-height: 4.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html:not(.no-page-anim) .page-enter-active {
|
||||||
|
transition:
|
||||||
|
opacity 0.25s ease,
|
||||||
|
transform 0.25s ease;
|
||||||
|
}
|
||||||
|
html:not(.no-page-anim) .page-leave-active {
|
||||||
|
transition: opacity 0.1s ease;
|
||||||
|
}
|
||||||
|
html:not(.no-page-anim) .page-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
html:not(.no-page-anim) .page-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,15 @@
|
||||||
:label="simpleMode ? 'Simple View' : 'Regular View'"
|
:label="simpleMode ? 'Simple View' : 'Regular View'"
|
||||||
description="The simple view is ideal for non-technical users and mobile devices."
|
description="The simple view is ideal for non-technical users and mobile devices."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<USwitch
|
||||||
|
v-model="page_anims"
|
||||||
|
class="w-full"
|
||||||
|
size="lg"
|
||||||
|
:ui="settingsSwitchUi"
|
||||||
|
:label="page_anims ? 'Animations On' : 'Animations Off'"
|
||||||
|
description="Enable page transition animations."
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</UPageCard>
|
</UPageCard>
|
||||||
|
|
||||||
|
|
@ -307,6 +316,7 @@ const show_popover = useStorage<boolean>('show_popover', true);
|
||||||
const thumbnail_ratio = useStorage<'is-16by9' | 'is-3by1'>('thumbnail_ratio', 'is-3by1');
|
const thumbnail_ratio = useStorage<'is-16by9' | 'is-3by1'>('thumbnail_ratio', 'is-3by1');
|
||||||
const separator = useStorage<string>('url_separator', separators[0]?.value ?? ',');
|
const separator = useStorage<string>('url_separator', separators[0]?.value ?? ',');
|
||||||
const simpleMode = useStorage<boolean>('simple_mode', config.app.simple_mode || false);
|
const simpleMode = useStorage<boolean>('simple_mode', config.app.simple_mode || false);
|
||||||
|
const page_anims = useStorage<boolean>('page_anims', true);
|
||||||
const queue_auto_refresh = useStorage<boolean>('queue_auto_refresh', true);
|
const queue_auto_refresh = useStorage<boolean>('queue_auto_refresh', true);
|
||||||
const queue_auto_refresh_delay = useStorage<number>('queue_auto_refresh_delay', 10000);
|
const queue_auto_refresh_delay = useStorage<number>('queue_auto_refresh_delay', 10000);
|
||||||
const isSecureContext = ref<boolean>(false);
|
const isSecureContext = ref<boolean>(false);
|
||||||
|
|
|
||||||
|
|
@ -568,6 +568,7 @@ const loadedImage = ref();
|
||||||
const loadingImage = ref(false);
|
const loadingImage = ref(false);
|
||||||
const bg_enable = useStorage('random_bg', true);
|
const bg_enable = useStorage('random_bg', true);
|
||||||
const bg_opacity = useStorage('random_bg_opacity', 0.95);
|
const bg_opacity = useStorage('random_bg_opacity', 0.95);
|
||||||
|
const page_anims = useStorage<boolean>('page_anims', true);
|
||||||
const app_shutdown = ref<boolean>(false);
|
const app_shutdown = ref<boolean>(false);
|
||||||
const simpleMode = useStorage<boolean>('simple_mode', config.app.simple_mode || false);
|
const simpleMode = useStorage<boolean>('simple_mode', config.app.simple_mode || false);
|
||||||
const show_settings = ref(false);
|
const show_settings = ref(false);
|
||||||
|
|
@ -1029,6 +1030,18 @@ watch(bg_opacity, () => {
|
||||||
syncOpacity();
|
syncOpacity();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
page_anims,
|
||||||
|
(val) => {
|
||||||
|
if (val) {
|
||||||
|
document.documentElement.classList.remove('no-page-anim');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.add('no-page-anim');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
watch(loadedImage, () => {
|
watch(loadedImage, () => {
|
||||||
if (false === bg_enable.value) {
|
if (false === bg_enable.value) {
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -255,24 +255,17 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex shrink-0 flex-wrap justify-end gap-2">
|
<div class="flex shrink-0 flex-wrap justify-end gap-2">
|
||||||
<UButton
|
<UDropdownMenu :items="copyMenuItems" :content="{ align: 'end' }" :modal="false">
|
||||||
color="neutral"
|
<UButton
|
||||||
variant="outline"
|
color="neutral"
|
||||||
size="xs"
|
variant="outline"
|
||||||
icon="i-lucide-copy"
|
size="xs"
|
||||||
@click="copyLogMessage(selectedLog)"
|
icon="i-lucide-copy"
|
||||||
>
|
trailing-icon="i-lucide-chevron-down"
|
||||||
Message
|
>
|
||||||
</UButton>
|
Copy
|
||||||
<UButton
|
</UButton>
|
||||||
color="neutral"
|
</UDropdownMenu>
|
||||||
variant="outline"
|
|
||||||
size="xs"
|
|
||||||
icon="i-lucide-braces"
|
|
||||||
@click="copyLogRaw(selectedLog)"
|
|
||||||
>
|
|
||||||
JSON
|
|
||||||
</UButton>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="min-w-0 space-y-2 sm:col-span-2">
|
<div class="min-w-0 space-y-2 sm:col-span-2">
|
||||||
|
|
@ -354,21 +347,74 @@
|
||||||
<UIcon name="i-lucide-tags" class="size-4 text-primary" />
|
<UIcon name="i-lucide-tags" class="size-4 text-primary" />
|
||||||
Fields
|
Fields
|
||||||
</button>
|
</button>
|
||||||
<dl v-if="fieldsOpen" class="grid gap-2 sm:grid-cols-2">
|
<div v-if="fieldsOpen" class="space-y-2">
|
||||||
<div
|
<div
|
||||||
v-for="row in fieldRows(selectedLog)"
|
v-for="field in fieldRows(selectedLog)"
|
||||||
:key="row.label"
|
:key="field.key"
|
||||||
class="rounded-sm border border-default bg-elevated/40 p-3"
|
class="rounded-sm border border-default bg-elevated/40"
|
||||||
>
|
>
|
||||||
<dt
|
<button
|
||||||
class="inline-flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide text-toned"
|
type="button"
|
||||||
|
class="flex w-full items-center justify-between gap-3 px-3 py-2 text-left"
|
||||||
|
@click="toggleField(field.key)"
|
||||||
>
|
>
|
||||||
<UIcon :name="row.icon" class="size-3.5" />
|
<span class="text-[11px] font-semibold uppercase tracking-wide text-toned">
|
||||||
<span>{{ row.label }}</span>
|
{{ field.label }}
|
||||||
</dt>
|
</span>
|
||||||
<dd class="mt-1 wrap-break-word font-mono text-xs text-default">{{ row.value }}</dd>
|
<div class="flex items-center gap-2">
|
||||||
|
<span v-if="field.preview" class="max-w-md truncate text-xs text-toned">
|
||||||
|
{{ field.preview }}
|
||||||
|
</span>
|
||||||
|
<UIcon
|
||||||
|
name="i-lucide-chevron-right"
|
||||||
|
:class="[
|
||||||
|
'size-4 shrink-0 text-toned transition-transform',
|
||||||
|
fieldOpen(field.key) ? 'rotate-90' : '',
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="fieldOpen(field.key)"
|
||||||
|
class="border-t border-default/70 px-3 py-3"
|
||||||
|
>
|
||||||
|
<div class="mb-3 flex items-center justify-between gap-3">
|
||||||
|
<UInput
|
||||||
|
v-if="field.kind !== 'scalar'"
|
||||||
|
:model-value="fieldFilter(field.key)"
|
||||||
|
type="search"
|
||||||
|
icon="i-lucide-filter"
|
||||||
|
placeholder="Filter field lines"
|
||||||
|
size="sm"
|
||||||
|
class="w-full"
|
||||||
|
@update:model-value="setFieldFilter(field.key, $event)"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
size="xs"
|
||||||
|
color="neutral"
|
||||||
|
variant="outline"
|
||||||
|
icon="i-lucide-copy"
|
||||||
|
@click="copyFieldValue(field)"
|
||||||
|
>
|
||||||
|
Copy
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<pre
|
||||||
|
v-if="field.kind === 'json'"
|
||||||
|
class="max-h-96 overflow-auto rounded-sm border border-default bg-elevated/50 p-3 text-xs whitespace-pre-wrap text-default"
|
||||||
|
>{{ displayedFieldValue(field) }}</pre>
|
||||||
|
<pre
|
||||||
|
v-else-if="field.kind === 'text'"
|
||||||
|
class="max-h-96 overflow-auto rounded-sm border border-default bg-elevated/50 p-3 text-xs whitespace-pre-wrap text-default"
|
||||||
|
>{{ displayedFieldValue(field) }}</pre>
|
||||||
|
<p v-else class="wrap-break-word font-mono text-xs text-default">
|
||||||
|
{{ field.value }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="space-y-2">
|
<section class="space-y-2">
|
||||||
|
|
@ -419,6 +465,13 @@ type DetailRow = {
|
||||||
value: string;
|
value: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
};
|
};
|
||||||
|
type LogFieldRow = {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
preview: string;
|
||||||
|
kind: 'scalar' | 'text' | 'json';
|
||||||
|
};
|
||||||
type LevelFilterItem = {
|
type LevelFilterItem = {
|
||||||
label: string;
|
label: string;
|
||||||
value: LogLevel;
|
value: LogLevel;
|
||||||
|
|
@ -458,6 +511,8 @@ const exceptionOpen = useStorage<boolean>('logs_exception_open', false);
|
||||||
const fieldsOpen = useStorage<boolean>('logs_fields_open', true);
|
const fieldsOpen = useStorage<boolean>('logs_fields_open', true);
|
||||||
const rawJsonOpen = useStorage<boolean>('logs_raw_json_open', false);
|
const rawJsonOpen = useStorage<boolean>('logs_raw_json_open', false);
|
||||||
const sourceOpen = useStorage<boolean>('logs_source_open', true);
|
const sourceOpen = useStorage<boolean>('logs_source_open', true);
|
||||||
|
const fieldOpenState = ref<Record<string, boolean>>({});
|
||||||
|
const fieldFilters = ref<Record<string, string>>({});
|
||||||
const selectedLevels = useStorage<LogLevel[]>('logs_level_filter', [...LOG_LEVELS]);
|
const selectedLevels = useStorage<LogLevel[]>('logs_level_filter', [...LOG_LEVELS]);
|
||||||
const sseController = ref<AbortController | null>(null);
|
const sseController = ref<AbortController | null>(null);
|
||||||
const runtimeLogLevel = ref<LogLevel | null>(null);
|
const runtimeLogLevel = ref<LogLevel | null>(null);
|
||||||
|
|
@ -482,6 +537,29 @@ const detailsModalUi = {
|
||||||
body: 'max-h-[75vh] overflow-y-auto',
|
body: 'max-h-[75vh] overflow-y-auto',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const copyMenuItems = computed(() => [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
label: 'Copy Message',
|
||||||
|
icon: 'i-lucide-message-square-text',
|
||||||
|
onSelect: () => {
|
||||||
|
if (selectedLog.value) {
|
||||||
|
copyText(selectedLog.value.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Copy JSON',
|
||||||
|
icon: 'i-lucide-braces',
|
||||||
|
onSelect: () => {
|
||||||
|
if (selectedLog.value) {
|
||||||
|
copyText(logRaw(selectedLog.value));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
const query = ref<string>(
|
const query = ref<string>(
|
||||||
(() => {
|
(() => {
|
||||||
const filter = route.query.filter ?? '';
|
const filter = route.query.filter ?? '';
|
||||||
|
|
@ -1033,21 +1111,89 @@ const detailRows = (log: log_line): DetailRow[] =>
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const fieldRows = (log: log_line): DetailRow[] =>
|
const fieldRows = (log: log_line): LogFieldRow[] => {
|
||||||
compactRows(
|
if (!log.fields) return [];
|
||||||
Object.entries(log.fields ?? {}).map(([label, value]) => ({
|
const rows: LogFieldRow[] = [];
|
||||||
label,
|
for (const [key, rawValue] of Object.entries(log.fields)) {
|
||||||
|
if (rawValue === undefined || rawValue === null || rawValue === '') continue;
|
||||||
|
const jsonValue =
|
||||||
|
typeof rawValue === 'string'
|
||||||
|
? parseJsonContainerString(rawValue)
|
||||||
|
: isJsonContainer(rawValue)
|
||||||
|
? rawValue
|
||||||
|
: null;
|
||||||
|
const value = jsonValue ? JSON.stringify(jsonValue, null, 2) : formatFieldValue(rawValue);
|
||||||
|
const kind = jsonValue
|
||||||
|
? 'json'
|
||||||
|
: typeof rawValue === 'string'
|
||||||
|
? rawValue.includes('\n')
|
||||||
|
? 'text'
|
||||||
|
: 'scalar'
|
||||||
|
: 'scalar';
|
||||||
|
rows.push({
|
||||||
|
key,
|
||||||
|
label: normalizeFieldLabel(key),
|
||||||
value,
|
value,
|
||||||
icon: 'i-lucide-tag',
|
preview: formatFieldValue(rawValue).replaceAll('\n', ' '),
|
||||||
})),
|
kind,
|
||||||
);
|
});
|
||||||
|
}
|
||||||
const copyLogMessage = (log: log_line): void => {
|
return rows;
|
||||||
copyText(log.message);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyLogRaw = (log: log_line): void => {
|
const fieldOpen = (key: string) => fieldOpenState.value[key] ?? false;
|
||||||
copyText(logRaw(log));
|
|
||||||
|
const toggleField = (key: string) => {
|
||||||
|
fieldOpenState.value = { ...fieldOpenState.value, [key]: !fieldOpen(key) };
|
||||||
|
};
|
||||||
|
|
||||||
|
const fieldFilter = (key: string) => fieldFilters.value[key] ?? '';
|
||||||
|
|
||||||
|
const setFieldFilter = (key: string, value: string | number) => {
|
||||||
|
fieldFilters.value = { ...fieldFilters.value, [key]: String(value ?? '') };
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayedFieldValue = (field: LogFieldRow): string => {
|
||||||
|
const query = fieldFilter(field.key);
|
||||||
|
if (!query) return field.value;
|
||||||
|
const needle = query.toLowerCase();
|
||||||
|
return field.value.split('\n').filter((line) => line.toLowerCase().includes(needle)).join('\n');
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyFieldValue = (field: LogFieldRow): void => {
|
||||||
|
copyText(field.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isJsonLike = (value: string): boolean => {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
return (
|
||||||
|
((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
|
||||||
|
(trimmed.startsWith('[') && trimmed.endsWith(']'))) &&
|
||||||
|
trimmed.length > 1
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isJsonContainer = (value: unknown): value is Record<string, unknown> | unknown[] =>
|
||||||
|
Array.isArray(value) || (!!value && typeof value === 'object');
|
||||||
|
|
||||||
|
const parseJsonContainerString = (value: string): Record<string, unknown> | unknown[] | null => {
|
||||||
|
if (!isJsonLike(value)) return null;
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value) as unknown;
|
||||||
|
if (Array.isArray(parsed)) return parsed;
|
||||||
|
if (parsed && typeof parsed === 'object') return parsed as Record<string, unknown>;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeFieldLabel = (key: string) => key.replaceAll('_', ' ');
|
||||||
|
|
||||||
|
const formatFieldValue = (value: unknown): string => {
|
||||||
|
if (typeof value === 'string') return value;
|
||||||
|
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
||||||
|
return JSON.stringify(value, null, 2);
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ export default defineNuxtConfig({
|
||||||
{ rel: 'apple-touch-startup-image', href: 'images/logo.png' },
|
{ rel: 'apple-touch-startup-image', href: 'images/logo.png' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
pageTransition: { name: 'page' },
|
pageTransition: { name: 'page', mode: 'out-in' },
|
||||||
},
|
},
|
||||||
modules: ['@nuxt/ui', '@vueuse/nuxt', '@nuxt/eslint'],
|
modules: ['@nuxt/ui', '@vueuse/nuxt', '@nuxt/eslint'],
|
||||||
icon: {
|
icon: {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue