Compare commits

..

3 commits
master ... dev

Author SHA1 Message Date
arabcoders
60b8e7c4a6 fix: video player did not support multi subs
Some checks failed
build-docker-containers / Validate Build Configuration (push) Has been cancelled
build-docker-containers / DockerHub README sync (push) Has been cancelled
build-docker-containers / Run Tests & Prepare Frontend (push) Has been cancelled
build-docker-containers / Build Container (amd64) (push) Has been cancelled
build-docker-containers / Build Container (arm64) (push) Has been cancelled
build-docker-containers / Publish multi-arch manifest (push) Has been cancelled
2026-06-23 10:42:01 +03:00
arabcoders
e14387e399 refactor: fix card width in mobile 2026-06-22 03:49:50 +03:00
arabcoders
52d6672fb3 refactor: update design layout 2026-06-21 21:25:47 +03:00
43 changed files with 3168 additions and 3015 deletions

View file

@ -30,5 +30,8 @@
"editor.defaultFormatter": "oxc.oxc-vscode", "editor.defaultFormatter": "oxc.oxc-vscode",
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.formatOnSaveMode": "file" "editor.formatOnSaveMode": "file"
},
"files.associations": {
"*.css": "tailwindcss"
} }
} }

5
FAQ.md
View file

@ -3,8 +3,9 @@
Certain configuration values can be set via environment variables, using the `-e` parameter on the docker command line, Certain configuration values can be set via environment variables, using the `-e` parameter on the docker command line,
or the `environment:` section in `compose.yaml` file. or the `environment:` section in `compose.yaml` file.
<details> <details>
<summary>Click to expand</summary> <summary>Click to expand all environment Variables</summary>
| Environment Variable | Description | Default | | Environment Variable | Description | Default |
| ------------------------------- | ------------------------------------------------------------------- | --------------------- | | ------------------------------- | ------------------------------------------------------------------- | --------------------- |
@ -64,7 +65,6 @@ or the `environment:` section in `compose.yaml` file.
| YTP_THUMB_GENERATE | Enable ffmpeg thumbnail generation when no local thumbnail exists. | `true` | | YTP_THUMB_GENERATE | Enable ffmpeg thumbnail generation when no local thumbnail exists. | `true` |
| YTP_THUMB_SIDECAR | Save generated thumbnails next to media instead of temp cache. | `false` | | YTP_THUMB_SIDECAR | Save generated thumbnails next to media instead of temp cache. | `false` |
| YTP_DISABLE_EXEC | Strip some dangerous yt-dlp options. | `false` | | YTP_DISABLE_EXEC | Strip some dangerous yt-dlp options. | `false` |
</details>
> [!NOTE] > [!NOTE]
> To raise the worker limit for a specific extractor, set an env variable using this format: `YTP_MAX_WORKERS_FOR_<EXTRACTOR_NAME>` > To raise the worker limit for a specific extractor, set an env variable using this format: `YTP_MAX_WORKERS_FOR_<EXTRACTOR_NAME>`
@ -77,6 +77,7 @@ or the `environment:` section in `compose.yaml` file.
> `YTP_AUTO_CLEAR_HISTORY_DAYS` `0` days means no automatic clearing of the download history. lowest value that will > `YTP_AUTO_CLEAR_HISTORY_DAYS` `0` days means no automatic clearing of the download history. lowest value that will
> trigger the clearing is `1` day. This setting will **NOT** delete the downloaded files, it will only clear the > trigger the clearing is `1` day. This setting will **NOT** delete the downloaded files, it will only clear the
> history from the database. > history from the database.
</details>
# Browser extensions & bookmarklets # Browser extensions & bookmarklets

View file

@ -7,5 +7,69 @@ export default defineAppConfig({
success: 'emerald', success: 'emerald',
neutral: 'stone', neutral: 'stone',
}, },
formField: {
slots: {
label: 'block font-bold text-default',
},
},
tooltip: {
slots: {
content: 'pointer-events-auto',
},
},
dropdownMenu: {
slots: {
content: 'ytp-floating-surface',
},
},
popover: {
slots: {
content: 'ytp-floating-surface',
},
},
select: {
slots: {
content: 'ytp-floating-surface',
},
variants: {
variant: {
outline: 'bg-elevated/40 hover:bg-elevated/55 disabled:bg-elevated/20',
},
},
},
selectMenu: {
slots: {
content: 'ytp-floating-surface',
},
variants: {
variant: {
outline: 'bg-elevated/40 hover:bg-elevated/55 disabled:bg-elevated/20',
},
},
},
button: {
compoundVariants: [
{
color: 'neutral',
variant: 'outline',
class:
'bg-transparent hover:bg-elevated/30 active:bg-elevated/40 disabled:bg-transparent aria-disabled:bg-transparent',
},
{
color: 'neutral',
variant: 'soft',
class:
'bg-elevated/30 hover:bg-elevated/45 active:bg-elevated/55 disabled:bg-elevated/20 aria-disabled:bg-elevated/20',
},
{
color: 'neutral',
variant: 'ghost',
class: 'hover:bg-elevated/30 active:bg-elevated/40 focus-visible:bg-elevated/30',
},
],
defaultVariants: {
size: 'sm',
},
},
}, },
}); });

View file

@ -1,3 +1,5 @@
@layer theme, base, legacy, components, utilities;
@import 'tailwindcss'; @import 'tailwindcss';
@import '@nuxt/ui'; @import '@nuxt/ui';
@ -30,6 +32,11 @@
html.simple-mode.bg-fanart #__nuxt { html.simple-mode.bg-fanart #__nuxt {
background-image: none !important; background-image: none !important;
} }
html.dark.simple-mode.bg-fanart body,
html.dark.simple-mode.bg-fanart #__nuxt {
background-color: var(--ui-bg) !important;
}
} }
@layer components { @layer components {
@ -51,9 +58,9 @@
.shell-surface { .shell-surface {
background: background:
radial-gradient(circle at top left, rgb(99 102 241 / 0.1), transparent 28%), radial-gradient(circle at top left, rgb(99 102 241 / 0.14), transparent 28%),
radial-gradient(circle at top right, rgb(245 158 11 / 0.08), transparent 22%), radial-gradient(circle at top right, rgb(245 158 11 / 0.12), transparent 22%),
linear-gradient(180deg, rgb(255 255 255 / 0.84), rgb(248 250 252 / 0.9)); linear-gradient(180deg, rgb(255 255 255 / 0.6), rgb(248 250 252 / 0.72));
backdrop-filter: blur(16px); backdrop-filter: blur(16px);
} }
@ -65,6 +72,115 @@
backdrop-filter: blur(16px); backdrop-filter: blur(16px);
} }
.ytp-page-header {
@apply flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between;
}
.ytp-page-heading {
@apply flex min-w-0 items-center gap-3;
}
.ytp-page-kicker {
@apply flex flex-wrap items-center gap-2 text-xs font-medium uppercase tracking-[0.2em] text-toned;
}
.ytp-page-icon {
@apply inline-flex size-11 shrink-0 items-center justify-center rounded-md border bg-elevated/70 text-primary;
border-color: var(--ytp-surface-border-solid);
}
.ytp-section-icon {
@apply inline-flex size-10 shrink-0 items-center justify-center rounded-md border bg-elevated/70 text-primary;
border-color: var(--ytp-surface-border-solid);
}
.ytp-detail-icon {
@apply inline-flex size-9 shrink-0 items-center justify-center rounded-md border bg-elevated/70 text-primary;
border-color: var(--ytp-surface-border-solid);
}
.ytp-card {
@apply min-w-0 max-w-full rounded-lg border bg-elevated/20;
border-color: var(--ytp-surface-border-soft);
}
.ytp-card-padded {
@apply min-w-0 max-w-full rounded-lg border bg-elevated/20 p-4;
border-color: var(--ytp-surface-border-soft);
}
.ytp-card-padded-lg {
@apply min-w-0 max-w-full rounded-lg border bg-elevated/20 p-4 sm:p-5;
border-color: var(--ytp-surface-border-soft);
}
.ytp-panel {
@apply min-w-0 max-w-full rounded-lg border;
border-color: var(--ytp-surface-border-soft);
}
.ytp-panel-padded {
@apply min-w-0 max-w-full rounded-lg border p-4;
border-color: var(--ytp-surface-border-soft);
}
.ytp-panel-padded-lg {
@apply min-w-0 max-w-full rounded-lg border p-4 sm:p-5;
border-color: var(--ytp-surface-border-soft);
}
.ytp-frame {
@apply min-w-0 max-w-full rounded-lg border;
border-color: var(--ytp-surface-border-soft);
}
.ytp-table-surface {
@apply min-w-0 max-w-full rounded-sm border bg-elevated/20;
border-color: var(--ytp-surface-border-solid);
}
.ytp-floating-surface {
@apply border bg-default/95 shadow-xl backdrop-blur-md;
border-color: var(--ytp-surface-border-solid);
box-shadow: 0 1rem 2rem rgb(15 23 42 / 0.1);
}
.ytp-floating-surface [data-slot='item'][data-highlighted]::before,
.ytp-floating-surface [data-slot='item'][data-state='open']::before,
.ytp-floating-surface [data-slot='item']:hover::before {
background-color: color-mix(in oklab, var(--ui-primary) 11%, transparent) !important;
}
.ytp-floating-surface [data-slot='item'][data-disabled]::before {
background-color: transparent !important;
}
.dark .ytp-floating-surface {
background: color-mix(in oklab, var(--ui-bg) 92%, var(--ui-bg-elevated) 8%);
box-shadow: 0 1rem 2rem rgb(0 0 0 / 0.3);
}
.dark .ytp-floating-surface [data-slot='item'][data-highlighted]::before,
.dark .ytp-floating-surface [data-slot='item'][data-state='open']::before,
.dark .ytp-floating-surface [data-slot='item']:hover::before {
background-color: color-mix(in oklab, var(--ui-primary) 16%, transparent) !important;
}
.ytp-border-top-soft {
@apply border-t;
border-color: var(--ytp-surface-border-soft);
}
.ytp-border-bottom-soft {
@apply border-b;
border-color: var(--ytp-surface-border-soft);
}
.ytp-border-left-soft {
@apply border-l;
border-color: var(--ytp-surface-border-soft);
}
.ytp-terminal { .ytp-terminal {
display: block; display: block;
width: 100%; width: 100%;
@ -167,6 +283,15 @@
--ui-radius: 0.5rem; --ui-radius: 0.5rem;
--ui-container: 96rem; --ui-container: 96rem;
--ui-header-height: 4.25rem; --ui-header-height: 4.25rem;
--ytp-border-strong: var(--ui-border-accented);
--ytp-surface-border-soft: var(--ui-border-accented);
--ytp-surface-border-solid: var(--ui-border-accented);
}
.dark {
--ytp-border-strong: var(--ui-border);
--ytp-surface-border-soft: color-mix(in oklab, var(--ui-border) 70%, transparent);
--ytp-surface-border-solid: var(--ui-border);
} }
html:not(.no-page-anim) .page-enter-active { html:not(.no-page-anim) .page-enter-active {

View file

@ -261,9 +261,7 @@
</div> </div>
</div> </div>
<div <div class="grid gap-3 ytp-card-padded md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto]">
class="grid gap-3 rounded-lg border border-default bg-default p-4 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto]"
>
<UFormField :ui="fieldUi"> <UFormField :ui="fieldUi">
<template #label> <template #label>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">

View file

@ -1,7 +1,7 @@
<template> <template>
<div v-if="'bool' === type && compact" class="w-full space-y-1.5"> <div v-if="'bool' === type && compact" class="w-full space-y-1.5">
<div <div
class="flex min-h-11 items-start justify-between gap-3 rounded-md border border-default bg-default/80 px-3 py-2" class="flex min-h-11 items-start justify-between gap-3 rounded-md border border-default bg-elevated/20 px-3 py-2"
> >
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<div class="inline-flex min-w-0 items-center gap-2 text-sm font-semibold text-default"> <div class="inline-flex min-w-0 items-center gap-2 text-sm font-semibold text-default">
@ -27,7 +27,7 @@
v-model="boolModel" v-model="boolModel"
:disabled="disabled" :disabled="disabled"
color="success" color="success"
:label="boolModel ? 'Yes' : 'No'" label=""
size="lg" size="lg"
class="shrink-0" class="shrink-0"
:ui="{ root: 'items-center gap-2', wrapper: 'ms-0 text-sm' }" :ui="{ root: 'items-center gap-2', wrapper: 'ms-0 text-sm' }"
@ -70,7 +70,7 @@
:disabled="disabled" :disabled="disabled"
size="lg" size="lg"
class="w-full" class="w-full"
:ui="{ root: 'w-full', base: 'w-full bg-default/90' }" :ui="{ root: 'w-full', base: 'w-full bg-elevated/60' }"
/> />
<UTextarea <UTextarea

View file

@ -20,7 +20,7 @@
<div <div
v-if="open && suggestions.length" v-if="open && suggestions.length"
ref="dropdownRef" ref="dropdownRef"
class="absolute inset-x-0 top-full z-20 mt-1 max-h-40 overflow-y-auto rounded-md border border-default bg-default shadow-lg" class="absolute inset-x-0 top-full z-20 mt-1 max-h-40 overflow-y-auto rounded-md ytp-floating-surface"
role="listbox" role="listbox"
> >
<button <button

View file

@ -12,7 +12,7 @@
variant="outline" variant="outline"
color="neutral" color="neutral"
class="w-full" class="w-full"
:ui="{ root: 'w-full', base: 'w-full bg-default/90', leadingIcon: iconClass }" :ui="{ root: 'w-full', base: 'w-full bg-elevated/60', leadingIcon: iconClass }"
@focus="onFocus" @focus="onFocus"
@blur="hideList" @blur="hideList"
@input="onInput" @input="onInput"
@ -25,7 +25,7 @@
<div <div
v-if="showList && filteredOptions.length" v-if="showList && filteredOptions.length"
ref="dropdownRef" ref="dropdownRef"
class="absolute inset-x-0 top-full z-20 mt-1 max-h-40 overflow-y-auto rounded-md border border-default bg-default shadow-lg" class="absolute inset-x-0 top-full z-20 mt-1 max-h-40 overflow-y-auto rounded-md ytp-floating-surface"
role="menu" role="menu"
> >
<button <button

View file

@ -25,10 +25,7 @@
</UButton> </UButton>
</div> </div>
<div <div v-if="!limits && limitsLoading" class="ytp-card flex min-h-72 items-center justify-center">
v-if="!limits && limitsLoading"
class="flex min-h-72 items-center justify-center rounded-md border border-default bg-default/90"
>
<div class="flex flex-col items-center gap-3 text-center text-toned"> <div class="flex flex-col items-center gap-3 text-center text-toned">
<UIcon name="i-lucide-loader-circle" class="size-10 animate-spin text-info" /> <UIcon name="i-lucide-loader-circle" class="size-10 animate-spin text-info" />
@ -66,7 +63,7 @@
title="Download queue is paused" title="Download queue is paused"
/> />
<div class="rounded-md border border-default bg-default shadow-sm"> <div class="ytp-card shadow-sm">
<div class="grid gap-0 lg:grid-cols-[minmax(0,1.05fr)_minmax(0,0.95fr)]"> <div class="grid gap-0 lg:grid-cols-[minmax(0,1.05fr)_minmax(0,0.95fr)]">
<section <section
class="space-y-4 border-b border-default px-4 py-4 sm:px-5 lg:border-r lg:border-b-0" class="space-y-4 border-b border-default px-4 py-4 sm:px-5 lg:border-r lg:border-b-0"
@ -197,13 +194,10 @@
description="Overrides and extractor usage appear here once activity is detected." description="Overrides and extractor usage appear here once activity is detected."
/> />
<div <div v-else class="w-full min-w-0 max-w-full overflow-hidden ytp-table-surface">
v-else
class="w-full min-w-0 max-w-full overflow-hidden rounded-md border border-default bg-default"
>
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain"> <div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
<table class="min-w-180 w-full text-sm"> <table class="min-w-180 w-full text-sm">
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned"> <thead class="bg-elevated/60 text-xs uppercase tracking-wide text-toned">
<tr <tr
class="text-left [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0" class="text-left [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
> >

View file

@ -119,9 +119,7 @@
@click="sourceOpen = !sourceOpen" @click="sourceOpen = !sourceOpen"
> >
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<span <span class="ytp-detail-icon">
class="inline-flex size-9 shrink-0 items-center justify-center rounded-md border border-default bg-elevated/70 text-primary"
>
<UIcon name="i-lucide-file-code" class="size-4" /> <UIcon name="i-lucide-file-code" class="size-4" />
</span> </span>
<p class="text-base font-semibold text-highlighted">Source</p> <p class="text-base font-semibold text-highlighted">Source</p>
@ -167,9 +165,7 @@
@click="fieldsOpen = !fieldsOpen" @click="fieldsOpen = !fieldsOpen"
> >
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<span <span class="ytp-detail-icon">
class="inline-flex size-9 shrink-0 items-center justify-center rounded-md border border-default bg-elevated/70 text-primary"
>
<UIcon name="i-lucide-tags" class="size-4" /> <UIcon name="i-lucide-tags" class="size-4" />
</span> </span>
<p class="text-base font-semibold text-highlighted">Fields</p> <p class="text-base font-semibold text-highlighted">Fields</p>
@ -225,14 +221,16 @@
> >
<button <button
type="button" type="button"
class="flex w-full items-center justify-between gap-3 px-3 py-2 text-left" class="grid w-full grid-cols-[minmax(7rem,0.45fr)_minmax(0,1fr)] items-center gap-3 px-3 py-2 text-left sm:grid-cols-[minmax(0,12rem)_minmax(0,1fr)]"
@click="toggleField(field.key)" @click="toggleField(field.key)"
> >
<span class="text-[11px] font-semibold uppercase tracking-wide text-toned"> <span
class="min-w-0 truncate text-[11px] font-semibold uppercase tracking-wide text-toned"
>
{{ field.label }} {{ field.label }}
</span> </span>
<div class="flex items-center gap-2"> <div class="flex min-w-0 items-center justify-end gap-2">
<span v-if="field.preview" class="max-w-md truncate text-xs text-toned"> <span v-if="field.preview" class="min-w-0 truncate text-xs text-toned">
{{ field.preview }} {{ field.preview }}
</span> </span>
<UIcon <UIcon
@ -306,9 +304,7 @@
@click="rawJsonOpen = !rawJsonOpen" @click="rawJsonOpen = !rawJsonOpen"
> >
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<span <span class="ytp-detail-icon">
class="inline-flex size-9 shrink-0 items-center justify-center rounded-md border border-default bg-elevated/70 text-primary"
>
<UIcon name="i-lucide-braces" class="size-4" /> <UIcon name="i-lucide-braces" class="size-4" />
</span> </span>
<p class="text-base font-semibold text-highlighted">Raw Data</p> <p class="text-base font-semibold text-highlighted">Raw Data</p>

View file

@ -1,8 +1,7 @@
<template> <template>
<main class="space-y-4"> <main class="space-y-4">
<form autocomplete="off" class="space-y-4" @submit.prevent="addDownload"> <form autocomplete="off" class="space-y-4" @submit.prevent="addDownload">
<UPageCard variant="outline" :ui="downloadCardUi"> <div class="ytp-card p-4 sm:p-6 space-y-4">
<template #body>
<div class="space-y-4"> <div class="space-y-4">
<UFormField class="w-full" :ui="downloadFieldUi"> <UFormField class="w-full" :ui="downloadFieldUi">
<template #label> <template #label>
@ -26,7 +25,7 @@
v-model="form.url" v-model="form.url"
:disabled="addInProgress" :disabled="addInProgress"
size="lg" size="lg"
variant="outline" :variant="show_description ? 'soft' : 'outline'"
color="neutral" color="neutral"
class="w-full" class="w-full"
:rows="3" :rows="3"
@ -47,7 +46,7 @@
placeholder="URLs to download" placeholder="URLs to download"
:disabled="addInProgress" :disabled="addInProgress"
size="lg" size="lg"
variant="outline" :variant="show_description ? 'soft' : 'outline'"
color="neutral" color="neutral"
class="w-full" class="w-full"
:ui="{ :ui="{
@ -97,7 +96,7 @@
<div class="flex w-full gap-2"> <div class="flex w-full gap-2">
<UButton <UButton
color="neutral" color="neutral"
variant="outline" :variant="show_description ? 'soft' : 'outline'"
icon="i-lucide-info" icon="i-lucide-info"
square square
class="shrink-0" class="shrink-0"
@ -158,7 +157,7 @@
<UButton <UButton
type="button" type="button"
color="neutral" color="neutral"
variant="outline" :variant="showAdvanced ? 'soft' : 'outline'"
icon="i-lucide-settings-2" icon="i-lucide-settings-2"
size="lg" size="lg"
class="w-full justify-center" class="w-full justify-center"
@ -185,11 +184,9 @@
</span> </span>
</button> </button>
</div> </div>
</template> </div>
</UPageCard>
<UPageCard v-if="showAdvanced" variant="outline" :ui="downloadCardUi"> <div v-if="showAdvanced" class="ytp-card p-4 sm:p-6 space-y-4">
<template #body>
<div class="space-y-4"> <div class="space-y-4">
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-12"> <div class="grid gap-3 md:grid-cols-2 xl:grid-cols-12">
<div class="xl:col-span-2"> <div class="xl:col-span-2">
@ -402,10 +399,7 @@
</UFormField> </UFormField>
</div> </div>
<div <div v-if="config.dl_fields.length > 0" class="grid gap-4 md:grid-cols-2 xl:grid-cols-2">
v-if="config.dl_fields.length > 0"
class="grid gap-4 md:grid-cols-2 xl:grid-cols-2"
>
<DLInput <DLInput
v-for="(fi, index) in sortedDLFields" v-for="(fi, index) in sortedDLFields"
:id="fi?.id || `dlf-${index}`" :id="fi?.id || `dlf-${index}`"
@ -421,9 +415,7 @@
</div> </div>
</div> </div>
<div <div class="flex flex-wrap items-center justify-between gap-2 border-t border-default pt-4">
class="flex flex-wrap items-center justify-between gap-2 border-t border-default pt-4"
>
<UDropdownMenu class="sm:hidden" :items="mobileActionGroups" :modal="false"> <UDropdownMenu class="sm:hidden" :items="mobileActionGroups" :modal="false">
<UButton <UButton
color="neutral" color="neutral"
@ -438,13 +430,11 @@
<div class="hidden flex-wrap items-center gap-2 sm:flex"> <div class="hidden flex-wrap items-center gap-2 sm:flex">
<UButton <UButton
type="button" type="button"
color="info" color="neutral"
variant="outline" variant="outline"
icon="i-lucide-info" icon="i-lucide-info"
:disabled="addInProgress || !hasValidUrl" :disabled="addInProgress || !hasValidUrl"
@click=" @click="emitter('getInfo', splitUrls(form.url || '')[0] || '', form.preset, form.cli)"
emitter('getInfo', splitUrls(form.url || '')[0] || '', form.preset, form.cli)
"
> >
yt-dlp Information yt-dlp Information
</UButton> </UButton>
@ -452,7 +442,7 @@
<UButton <UButton
v-if="config.app.console_enabled" v-if="config.app.console_enabled"
type="button" type="button"
color="warning" color="neutral"
variant="outline" variant="outline"
icon="i-lucide-terminal" icon="i-lucide-terminal"
:disabled="!hasValidUrl" :disabled="!hasValidUrl"
@ -464,7 +454,7 @@
<UButton <UButton
v-if="config.app.console_enabled" v-if="config.app.console_enabled"
type="button" type="button"
color="success" color="neutral"
variant="outline" variant="outline"
icon="i-lucide-flask-conical" icon="i-lucide-flask-conical"
:disabled="!hasValidUrl" :disabled="!hasValidUrl"
@ -476,7 +466,7 @@
<UButton <UButton
type="button" type="button"
color="error" color="neutral"
variant="outline" variant="outline"
icon="i-lucide-rotate-ccw" icon="i-lucide-rotate-ccw"
:disabled="!!form?.id" :disabled="!!form?.id"
@ -485,8 +475,7 @@
Reset local settings Reset local settings
</UButton> </UButton>
</div> </div>
</template> </div>
</UPageCard>
</form> </form>
<UModal <UModal
@ -707,13 +696,6 @@ const testResultsPreClasses = computed(() => [
testResultsClasses.value, testResultsClasses.value,
]); ]);
const downloadCardUi = {
root: 'w-full',
container: 'w-full p-4 sm:p-6',
wrapper: 'w-full items-stretch',
body: 'w-full space-y-4',
};
const downloadFieldUi = { const downloadFieldUi = {
label: 'font-semibold text-default', label: 'font-semibold text-default',
container: 'space-y-2', container: 'space-y-2',
@ -793,7 +775,6 @@ const mobileActionGroups = computed(() => {
{ {
label: 'Reset local settings', label: 'Reset local settings',
icon: 'i-lucide-rotate-ccw', icon: 'i-lucide-rotate-ccw',
color: 'error',
disabled: Boolean(form.value?.id), disabled: Boolean(form.value?.id),
onSelect: () => void resetConfig(), onSelect: () => void resetConfig(),
}, },

View file

@ -86,7 +86,7 @@
<UIcon name="i-lucide-check" class="size-3.5" /> <UIcon name="i-lucide-check" class="size-3.5" />
</UButton> </UButton>
<UButton <UButton
color="error" color="neutral"
variant="ghost" variant="ghost"
size="xs" size="xs"
square square
@ -121,7 +121,7 @@
</UButton> </UButton>
<UButton <UButton
color="error" color="neutral"
variant="ghost" variant="ghost"
size="sm" size="sm"
icon="i-lucide-trash" icon="i-lucide-trash"

View file

@ -0,0 +1,36 @@
<template>
<div class="ytp-page-header">
<div class="ytp-page-heading">
<span class="ytp-page-icon">
<slot name="icon">
<UIcon :name="icon" class="size-5" />
</slot>
</span>
<div class="min-w-0" :class="contentClass">
<div class="ytp-page-kicker">
<slot name="kicker">
<span>{{ sectionLabel }}</span>
<span>/</span>
<span>{{ pageLabel }}</span>
</slot>
</div>
</div>
</div>
<div
v-if="$slots.actions"
class="flex w-full flex-wrap items-center gap-2 xl:w-auto xl:justify-end"
>
<slot name="actions" />
</div>
</div>
</template>
<script setup lang="ts">
import type { YtpPageHeaderProps } from '~/types';
withDefaults(defineProps<YtpPageHeaderProps>(), {
contentClass: 'space-y-1',
});
</script>

View file

@ -30,15 +30,15 @@
<template #body> <template #body>
<div class="w-full space-y-6"> <div class="w-full space-y-6">
<UPageCard variant="subtle" class="w-full" :ui="settingsCardUi"> <div class="ytp-card w-full">
<template #header> <div class="p-4 sm:p-5 ytp-border-bottom-soft">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<UIcon name="i-lucide-layout-dashboard" class="size-4 text-toned" /> <UIcon name="i-lucide-layout-dashboard" class="size-4 text-toned" />
<span class="text-sm font-semibold text-highlighted">Page View</span> <span class="text-sm font-semibold text-highlighted">Page View</span>
</div> </div>
</template> </div>
<template #body> <div class="p-4 sm:p-5 space-y-4">
<UFormField label="" class="w-full" :ui="settingsFieldUi"> <UFormField label="" class="w-full" :ui="settingsFieldUi">
<USelect <USelect
v-model="draftMode" v-model="draftMode"
@ -73,18 +73,18 @@
:label="page_anims ? 'Animations On' : 'Animations Off'" :label="page_anims ? 'Animations On' : 'Animations Off'"
description="Enable page transition animations." description="Enable page transition animations."
/> />
</template> </div>
</UPageCard> </div>
<UPageCard variant="subtle" class="w-full" :ui="settingsCardUi"> <div class="ytp-card w-full">
<template #header> <div class="p-4 sm:p-5 ytp-border-bottom-soft">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<UIcon name="i-lucide-image" class="size-4 text-toned" /> <UIcon name="i-lucide-image" class="size-4 text-toned" />
<span class="text-sm font-semibold text-highlighted">Background</span> <span class="text-sm font-semibold text-highlighted">Background</span>
</div> </div>
</template> </div>
<template #body> <div class="p-4 sm:p-5 space-y-4">
<USwitch <USwitch
v-model="bg_enable" v-model="bg_enable"
class="w-full" class="w-full"
@ -95,7 +95,7 @@
<UButton <UButton
v-if="bg_enable" v-if="bg_enable"
color="info" color="neutral"
variant="outline" variant="outline"
icon="i-lucide-image-up" icon="i-lucide-image-up"
class="w-full justify-center" class="w-full justify-center"
@ -121,18 +121,18 @@
class="w-full" class="w-full"
/> />
</UFormField> </UFormField>
</template> </div>
</UPageCard> </div>
<UPageCard variant="subtle" class="w-full" :ui="settingsCardUi"> <div class="ytp-card w-full">
<template #header> <div class="p-4 sm:p-5 ytp-border-bottom-soft">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<UIcon name="i-lucide-monitor" class="size-4 text-toned" /> <UIcon name="i-lucide-monitor" class="size-4 text-toned" />
<span class="text-sm font-semibold text-highlighted">Downloads</span> <span class="text-sm font-semibold text-highlighted">Downloads</span>
</div> </div>
</template> </div>
<template #body> <div class="p-4 sm:p-5 space-y-4">
<UFormField v-if="!modeOn" label="URL Separator" class="w-full" :ui="settingsFieldUi"> <UFormField v-if="!modeOn" label="URL Separator" class="w-full" :ui="settingsFieldUi">
<USelect <USelect
v-model="separator" v-model="separator"
@ -179,18 +179,18 @@
:label="show_popover ? 'Popover On' : 'Popover Off'" :label="show_popover ? 'Popover On' : 'Popover Off'"
description="Show additional information over certain elements." description="Show additional information over certain elements."
/> />
</template> </div>
</UPageCard> </div>
<UPageCard variant="subtle" class="w-full" :ui="settingsCardUi"> <div class="ytp-card w-full">
<template #header> <div class="p-4 sm:p-5 ytp-border-bottom-soft">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<UIcon name="i-lucide-download" class="size-4 text-toned" /> <UIcon name="i-lucide-download" class="size-4 text-toned" />
<span class="text-sm font-semibold text-highlighted">Queue</span> <span class="text-sm font-semibold text-highlighted">Queue</span>
</div> </div>
</template> </div>
<template #body> <div class="p-4 sm:p-5 space-y-4">
<USwitch <USwitch
v-model="queue_auto_refresh" v-model="queue_auto_refresh"
class="w-full" class="w-full"
@ -219,18 +219,18 @@
How often to refresh the queue (5-60 seconds). Lower values increase server load. How often to refresh the queue (5-60 seconds). Lower values increase server load.
</p> </p>
</UFormField> </UFormField>
</template> </div>
</UPageCard> </div>
<UPageCard variant="subtle" class="w-full" :ui="settingsCardUi"> <div class="ytp-card w-full">
<template #header> <div class="p-4 sm:p-5 ytp-border-bottom-soft">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<UIcon name="i-lucide-bell" class="size-4 text-toned" /> <UIcon name="i-lucide-bell" class="size-4 text-toned" />
<span class="text-sm font-semibold text-highlighted">Notifications</span> <span class="text-sm font-semibold text-highlighted">Notifications</span>
</div> </div>
</template> </div>
<template #body> <div class="p-4 sm:p-5 space-y-4">
<USwitch <USwitch
v-model="allow_toasts" v-model="allow_toasts"
class="w-full" class="w-full"
@ -288,8 +288,8 @@
:ui="settingsSwitchUi" :ui="settingsSwitchUi"
:label="toast_dismiss_on_click ? 'Dismiss on click' : 'Keep on click'" :label="toast_dismiss_on_click ? 'Dismiss on click' : 'Keep on click'"
/> />
</template> </div>
</UPageCard> </div>
</div> </div>
</template> </template>
</USlideover> </USlideover>
@ -337,13 +337,6 @@ 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);
const settingsCardUi = {
root: 'w-full',
container: 'w-full p-4 sm:p-5',
wrapper: 'w-full items-stretch',
body: 'w-full space-y-4',
};
const settingsFieldUi = { const settingsFieldUi = {
root: 'w-full', root: 'w-full',
container: 'mt-2 w-full', container: 'mt-2 w-full',
@ -415,6 +408,10 @@ const toastPositionItems: Array<{ label: string; value: toastPosition }> = [
{ label: 'bottom-right', value: 'bottom-right' }, { label: 'bottom-right', value: 'bottom-right' },
]; ];
const closeScrollLock = (): void => {
document.body.classList.remove('settings-panel-open');
};
const handleKeydown = (e: KeyboardEvent) => { const handleKeydown = (e: KeyboardEvent) => {
if ('Escape' === e.key && props.isOpen) { if ('Escape' === e.key && props.isOpen) {
e.preventDefault(); e.preventDefault();
@ -434,7 +431,10 @@ onMounted(async () => {
document.addEventListener('keydown', handleKeydown); document.addEventListener('keydown', handleKeydown);
}); });
onBeforeUnmount(() => document.removeEventListener('keydown', handleKeydown)); onBeforeUnmount(() => {
document.removeEventListener('keydown', handleKeydown);
closeScrollLock();
});
const onNotificationTargetChange = async (): Promise<void> => { const onNotificationTargetChange = async (): Promise<void> => {
if ('browser' === toast_target.value) { if ('browser' === toast_target.value) {
@ -455,7 +455,7 @@ watch(
draftMode.value = mode.value; draftMode.value = mode.value;
document.body.classList.add('settings-panel-open'); document.body.classList.add('settings-panel-open');
} else { } else {
document.body.classList.remove('settings-panel-open'); closeScrollLock();
} }
}, },
); );

View file

@ -1,16 +1,22 @@
<template> <template>
<div class="rounded-lg border border-default/70 bg-elevated/40 p-3"> <div class="ytp-card bg-elevated/40 p-3">
<div class="flex items-center justify-between gap-3"> <div class="flex items-center justify-between gap-3">
<div class="min-w-0 space-y-1"> <div class="min-w-0 space-y-1">
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-toned">{{ label }}</p> <p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-toned">{{ label }}</p>
<UTooltip v-if="tooltip" :text="tooltip" :content="{ side: 'left' }"> <UTooltip v-if="tooltip" :text="tooltip" :content="{ side: 'left' }">
<div class="flex items-baseline gap-1.5"> <div :class="valueWrap ? 'space-y-1' : 'flex items-baseline gap-1.5'">
<span class="text-sm font-semibold text-highlighted">{{ value }}</span> <span class="text-sm font-semibold text-highlighted">{{ value }}</span>
<span v-if="hint" class="truncate text-xs text-toned">{{ hint }}</span> <span v-if="hint" class="truncate text-xs text-toned">{{ hint }}</span>
</div> </div>
</UTooltip> </UTooltip>
<div v-else class="flex items-baseline gap-1.5"> <div v-else :class="valueWrap ? 'space-y-1' : 'flex items-baseline gap-1.5'">
<span class="text-sm font-semibold text-highlighted">{{ value }}</span> <span
:class="[
'text-sm font-semibold text-highlighted',
valueWrap ? 'block wrap-break-word' : '',
]"
>{{ value }}</span
>
<span v-if="hint" class="truncate text-xs text-toned">{{ hint }}</span> <span v-if="hint" class="truncate text-xs text-toned">{{ hint }}</span>
</div> </div>
</div> </div>
@ -37,8 +43,9 @@ const props = withDefaults(
hint?: string; hint?: string;
tooltip?: string; tooltip?: string;
color?: Color; color?: Color;
valueWrap?: boolean;
}>(), }>(),
{ color: 'primary', hint: '', tooltip: '' }, { color: 'primary', hint: '', tooltip: '', valueWrap: false },
); );
const TILE_BG: Record<Color, string> = { const TILE_BG: Record<Color, string> = {

View file

@ -363,12 +363,10 @@
</UButton> </UButton>
</div> </div>
<div <div class="w-full min-w-0 max-w-full overflow-hidden ytp-table-surface">
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default"
>
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain"> <div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
<table class="min-w-215 table-fixed w-full text-sm"> <table class="min-w-215 table-fixed w-full text-sm">
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned"> <thead class="bg-elevated/60 text-xs uppercase tracking-wide text-toned">
<tr <tr
class="text-left [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0" class="text-left [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
> >

View file

@ -116,7 +116,7 @@
<span>Result:</span> <span>Result:</span>
</div> </div>
<div class="overflow-hidden rounded-lg border border-default bg-default"> <div class="overflow-hidden ytp-frame">
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain"> <div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
<pre <pre
class="min-w-0 max-w-full overflow-x-auto p-4 text-xs leading-6 text-default" class="min-w-0 max-w-full overflow-x-auto p-4 text-xs leading-6 text-default"

View file

@ -34,7 +34,7 @@
<div <div
v-if="showList && filteredOptions.length" v-if="showList && filteredOptions.length"
class="absolute inset-x-0 top-full z-20 mt-1 overflow-hidden rounded-md border border-default bg-default shadow-lg" class="absolute inset-x-0 top-full z-20 mt-1 overflow-hidden rounded-md ytp-floating-surface"
role="menu" role="menu"
> >
<button <button

View file

@ -94,6 +94,7 @@
:label="nativeSubtitleTrack.name || 'Subtitles'" :label="nativeSubtitleTrack.name || 'Subtitles'"
default default
:src="uri(nativeSubtitleTrack.url)" :src="uri(nativeSubtitleTrack.url)"
@load="handleNativeTrackLoad"
/> />
Your browser does not support the video tag. Your browser does not support the video tag.
</video> </video>
@ -170,16 +171,30 @@
@click="forceSwitchToHls" @click="forceSwitchToHls"
/> />
</UTooltip> </UTooltip>
<UButton <USelect
v-if="hasSubtitles" v-if="hasSubtitles"
v-model="subtitleSelectValue"
:items="subtitleSelectItems"
value-key="value"
label-key="label"
color="neutral" color="neutral"
variant="soft" variant="soft"
size="sm" size="sm"
trailing-icon=""
:content="subtitleSelectContent"
:portal="helpPortal"
:ui="subtitleSelectUi"
class="opacity-65 transition-opacity hover:opacity-100 focus-visible:opacity-100" class="opacity-65 transition-opacity hover:opacity-100 focus-visible:opacity-100"
:icon="subtitleEnabled ? 'i-lucide-captions' : 'i-lucide-captions-off'" :aria-label="subtitleButtonLabel"
:aria-label="subtitleEnabled ? 'Disable subtitles' : 'Enable subtitles'" >
@click="subtitleEnabled = !subtitleEnabled" <template #default>
<span class="sr-only">{{ subtitleButtonLabel }}</span>
<UIcon
:name="subtitleEnabled ? 'i-lucide-captions' : 'i-lucide-captions-off'"
class="size-4 shrink-0"
/> />
</template>
</USelect>
<UButton <UButton
color="neutral" color="neutral"
variant="soft" variant="soft"
@ -429,9 +444,12 @@ const timeLabel = computed(() => {
return `${currentLabel} / ${durationLabel}`; return `${currentLabel} / ${durationLabel}`;
}); });
const { const {
subtitleTracks,
subtitleLoading, subtitleLoading,
subtitleLoadError, subtitleLoadError,
subtitleEnabled, subtitleEnabled,
selectedSubtitleTrack,
selectedSubtitleTrackId,
nativeSubtitleTrack, nativeSubtitleTrack,
usesAssSubtitleTrack, usesAssSubtitleTrack,
hasSubtitles, hasSubtitles,
@ -445,6 +463,40 @@ const {
overlay: assOverlayElement, overlay: assOverlayElement,
}); });
const SUBTITLE_OFF_VALUE = '__off__';
const subtitleSelectContent = { align: 'end' as const } as const;
const subtitleSelectUi = {
base: 'size-8 min-h-8 min-w-8 justify-center rounded-md px-0',
value: 'flex items-center justify-center px-0',
trailing: 'hidden',
leading: 'hidden',
content: 'z-40 min-w-56 w-auto max-w-[min(24rem,calc(100vw-2rem))]',
} as const;
const subtitleSelectItems = computed(() => [
{ label: 'Off', value: SUBTITLE_OFF_VALUE },
...subtitleTracks.value.map((track) => ({ label: track.name, value: track.url })),
]);
const subtitleButtonLabel = computed(() => {
if (!subtitleEnabled.value || !selectedSubtitleTrack.value) {
return 'Subtitles off';
}
return selectedSubtitleTrack.value.name || 'Subtitles';
});
const subtitleSelectValue = computed<string>({
get: () =>
subtitleEnabled.value
? (selectedSubtitleTrackId.value ?? SUBTITLE_OFF_VALUE)
: SUBTITLE_OFF_VALUE,
set: (value: string) => {
if (SUBTITLE_OFF_VALUE === value) {
subtitleEnabled.value = false;
return;
}
selectedSubtitleTrackId.value = value;
subtitleEnabled.value = true;
},
});
useHead(() => (title.value ? { title: formatPageTitle(`Playing: ${title.value}`) } : {})); useHead(() => (title.value ? { title: formatPageTitle(`Playing: ${title.value}`) } : {}));
watch( watch(
@ -960,6 +1012,10 @@ function updateMediaSessionPosition(target: EventTarget | null) {
} }
async function restoreDefaultTextTrack() { async function restoreDefaultTextTrack() {
if (true === destroyed.value) {
return;
}
const el = videoElement.value; const el = videoElement.value;
if (!el) { if (!el) {
return; return;
@ -971,36 +1027,6 @@ async function restoreDefaultTextTrack() {
return; return;
} }
const firstTrack = tracksList[0] as TextTrack | undefined;
const needsReload = firstTrack && (!firstTrack.cues || firstTrack.cues.length === 0);
if (needsReload) {
const trackElements = el.querySelectorAll('track');
trackElements.forEach((trackEl, idx) => {
const parent = trackEl.parentNode;
const clone = trackEl.cloneNode(true) as HTMLTrackElement;
trackEl.remove();
setTimeout(() => {
if (parent) {
parent.appendChild(clone);
clone.addEventListener(
'load',
() => {
const trackObj = clone.track;
if (trackObj) {
trackObj.mode = idx === 0 && subtitleEnabled.value ? 'showing' : 'disabled';
}
},
{ once: true },
);
}
}, idx * 10);
});
return;
}
for (let i = 0; i < tracksList.length; i += 1) { for (let i = 0; i < tracksList.length; i += 1) {
const track = tracksList[i] as TextTrack | undefined; const track = tracksList[i] as TextTrack | undefined;
if (track) { if (track) {
@ -1010,18 +1036,34 @@ async function restoreDefaultTextTrack() {
await new Promise((resolve) => setTimeout(resolve, 50)); await new Promise((resolve) => setTimeout(resolve, 50));
for (let i = 0; i < tracksList.length; i += 1) { if (el !== videoElement.value) {
const track = tracksList[i] as TextTrack | undefined; return;
}
if (false === subtitleEnabled.value || null === nativeSubtitleTrack.value) {
return;
}
const activeTracksList = el.textTracks;
for (let i = 0; i < activeTracksList.length; i += 1) {
const track = activeTracksList[i] as TextTrack | undefined;
if (!track) { if (!track) {
continue; continue;
} }
track.mode = i === 0 && subtitleEnabled.value ? 'showing' : 'disabled'; track.mode = i === 0 ? 'showing' : 'disabled';
} }
} catch (error) { } catch (error) {
console.warn('Failed to restore subtitle track state', error); console.warn('Failed to restore subtitle track state', error);
} }
} }
function handleNativeTrackLoad() {
if (true === destroyed.value) {
return;
}
void restoreDefaultTextTrack();
}
function applyMediaSessionMetadata() { function applyMediaSessionMetadata() {
if (false === 'mediaSession' in navigator) { if (false === 'mediaSession' in navigator) {
return; return;
@ -1242,6 +1284,10 @@ watch(subtitleEnabled, () => {
void nextTick(() => restoreDefaultTextTrack()); void nextTick(() => restoreDefaultTextTrack());
}); });
watch(selectedSubtitleTrack, () => {
void nextTick(() => restoreDefaultTextTrack());
});
onMounted(async () => { onMounted(async () => {
disableOpacity(); disableOpacity();
isTouchDevice.value = window.matchMedia('(pointer: coarse)').matches; isTouchDevice.value = window.matchMedia('(pointer: coarse)').matches;

View file

@ -123,12 +123,10 @@
<UBadge color="neutral" variant="soft" size="sm">{{ group.items.length }}</UBadge> <UBadge color="neutral" variant="soft" size="sm">{{ group.items.length }}</UBadge>
</div> </div>
<div <div class="w-full min-w-0 max-w-full overflow-hidden ytp-table-surface">
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default"
>
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain"> <div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
<table class="min-w-180 w-full table-auto text-sm"> <table class="min-w-180 w-full table-auto text-sm">
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned"> <thead class="bg-elevated/60 text-xs uppercase tracking-wide text-toned">
<tr <tr
class="text-left [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0" class="text-left [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
> >
@ -188,10 +186,7 @@
</section> </section>
</template> </template>
<div <div v-else class="w-full min-w-0 max-w-full overflow-hidden ytp-table-surface">
v-else
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default"
>
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain"> <div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
<table class="min-w-215 w-full table-auto text-sm"> <table class="min-w-215 w-full table-auto text-sm">
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned"> <thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned">

View file

@ -11,12 +11,7 @@
/> />
</div> </div>
<UPageCard <div class="ytp-panel p-0 relative w-full max-w-xl overflow-hidden bg-default/95">
variant="outline"
:ui="pageCardUi"
class="relative w-full max-w-xl overflow-hidden bg-default/95"
>
<template #body>
<div class="space-y-6 px-5 py-6 sm:px-7 sm:py-8" role="status" aria-live="polite"> <div class="space-y-6 px-5 py-6 sm:px-7 sm:py-8" role="status" aria-live="polite">
<div <div
class="inline-flex items-center gap-2 rounded-full border border-default bg-elevated/60 px-3 py-1.5 text-xs font-semibold tracking-[0.22em] text-toned uppercase" class="inline-flex items-center gap-2 rounded-full border border-default bg-elevated/60 px-3 py-1.5 text-xs font-semibold tracking-[0.22em] text-toned uppercase"
@ -49,16 +44,8 @@
description="The native app is closing background services and should exit shortly." description="The native app is closing background services and should exit shortly."
/> />
</div> </div>
</template> </div>
</UPageCard>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts"></script>
const pageCardUi = {
root: 'w-full bg-transparent shadow-2xl shadow-primary/5',
container: 'w-full p-0',
wrapper: 'w-full items-stretch',
body: 'w-full p-0',
};
</script>

View file

@ -54,7 +54,10 @@ export function usePlayerSubtitles(options: UsePlayerSubtitlesOptions) {
const subtitleLoading = ref(false); const subtitleLoading = ref(false);
const subtitleLoadError = ref(''); const subtitleLoadError = ref('');
const subtitleEnabled = ref(true); const subtitleEnabled = ref(true);
const selectedTrack = computed(() => tracks.value[0] || null); const selectedTrackId = ref<string | null>(null);
const selectedTrack = computed(
() => tracks.value.find((track) => track.url === selectedTrackId.value) || null,
);
const nativeSubtitleTrack = computed(() => { const nativeSubtitleTrack = computed(() => {
const track = selectedTrack.value; const track = selectedTrack.value;
return subtitleEnabled.value && track?.renderer === 'native' ? track : null; return subtitleEnabled.value && track?.renderer === 'native' ? track : null;
@ -82,6 +85,7 @@ export function usePlayerSubtitles(options: UsePlayerSubtitlesOptions) {
assRequestId += 1; assRequestId += 1;
destroyAssRenderer(); destroyAssRenderer();
tracks.value = []; tracks.value = [];
selectedTrackId.value = null;
subtitleLoadError.value = ''; subtitleLoadError.value = '';
if (!manifestUrl || !isVideo || !canPlay) { if (!manifestUrl || !isVideo || !canPlay) {
@ -104,6 +108,7 @@ export function usePlayerSubtitles(options: UsePlayerSubtitlesOptions) {
tracks.value = (payload as SubtitleManifestResponse).subtitles || []; tracks.value = (payload as SubtitleManifestResponse).subtitles || [];
if (tracks.value.length > 0) { if (tracks.value.length > 0) {
selectedTrackId.value = tracks.value[0]?.url || null;
subtitleEnabled.value = true; subtitleEnabled.value = true;
} }
} catch { } catch {
@ -212,6 +217,7 @@ export function usePlayerSubtitles(options: UsePlayerSubtitlesOptions) {
subtitleLoadError, subtitleLoadError,
subtitleEnabled, subtitleEnabled,
selectedSubtitleTrack: selectedTrack, selectedSubtitleTrack: selectedTrack,
selectedSubtitleTrackId: selectedTrackId,
nativeSubtitleTrack, nativeSubtitleTrack,
usesAssSubtitleTrack: usesAssTrack, usesAssSubtitleTrack: usesAssTrack,
hasSubtitles, hasSubtitles,

View file

@ -1,8 +1,7 @@
<template> <template>
<NuxtLayout name="error-layout"> <NuxtLayout name="error-layout">
<div class="flex min-h-full flex-1 items-start justify-center py-4 sm:py-8"> <div class="flex min-h-full flex-1 items-start justify-center py-4 sm:py-8">
<UPageCard variant="outline" :ui="pageCardUi" class="w-full"> <div class="ytp-card p-0 w-full">
<template #body>
<div class="space-y-6 px-4 py-5 sm:px-6 sm:py-6 lg:px-7"> <div class="space-y-6 px-4 py-5 sm:px-6 sm:py-6 lg:px-7">
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between"> <div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div class="min-w-0 space-y-2"> <div class="min-w-0 space-y-2">
@ -17,8 +16,8 @@
</h1> </h1>
<p class="max-w-3xl text-sm leading-6 text-toned"> <p class="max-w-3xl text-sm leading-6 text-toned">
An unexpected error interrupted this view. You can go back home or retry the An unexpected error interrupted this view. You can go back home or retry the current
current route. route.
</p> </p>
</div> </div>
@ -65,8 +64,7 @@
</div> </div>
</section> </section>
</div> </div>
</template> </div>
</UPageCard>
</div> </div>
</NuxtLayout> </NuxtLayout>
</template> </template>
@ -80,13 +78,6 @@ const props = defineProps<{
const showStacks = ref(false); const showStacks = ref(false);
const pageCardUi = {
root: 'w-full bg-default',
container: 'w-full p-0',
wrapper: 'w-full items-stretch',
body: 'w-full p-0',
};
const handleRetry = async (): Promise<void> => { const handleRetry = async (): Promise<void> => {
await clearError({ redirect: useRoute().fullPath }); await clearError({ redirect: useRoute().fullPath });
}; };

View file

@ -183,7 +183,7 @@
<UButton <UButton
v-if="true === config.app.is_native" v-if="true === config.app.is_native"
color="error" color="neutral"
variant="ghost" variant="ghost"
size="sm" size="sm"
icon="i-lucide-power" icon="i-lucide-power"

View file

@ -6,19 +6,14 @@
aria-hidden="true" aria-hidden="true"
/> />
<div class="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between"> <div class="ytp-page-header">
<div class="flex min-w-0 items-start gap-3"> <div class="ytp-page-heading">
<span <span class="ytp-page-icon">
class="inline-flex size-11 shrink-0 items-center justify-center rounded-md border border-default bg-elevated/70 text-primary"
>
<UIcon :name="pageShell.icon" class="size-5" /> <UIcon :name="pageShell.icon" class="size-5" />
</span> </span>
<div class="min-w-0 flex-1 space-y-3"> <div class="min-w-0 flex-1 space-y-3">
<nav <nav aria-label="Breadcrumb" class="min-w-0 ytp-page-kicker">
aria-label="Breadcrumb"
class="flex min-w-0 flex-wrap items-center gap-2 text-xs font-medium uppercase tracking-[0.2em] text-toned"
>
<span>{{ pageShell.sectionLabel }}</span> <span>{{ pageShell.sectionLabel }}</span>
<span>/</span> <span>/</span>
<span>{{ pageShell.pageLabel }}</span> <span>{{ pageShell.pageLabel }}</span>
@ -50,7 +45,7 @@
</div> </div>
</div> </div>
<div class="flex min-w-0 flex-wrap items-center justify-end gap-2 xl:justify-end"> <div class="flex w-full flex-wrap items-center gap-2 xl:w-auto xl:justify-end">
<UButton <UButton
color="neutral" color="neutral"
:variant="show_filter ? 'soft' : 'outline'" :variant="show_filter ? 'soft' : 'outline'"
@ -123,7 +118,7 @@
<div <div
v-if="controlEnabled && hasItems" v-if="controlEnabled && hasItems"
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-default bg-default px-3 py-3" class="flex flex-wrap items-center justify-between gap-3 ytp-card px-3 py-3"
> >
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<UButton <UButton
@ -169,11 +164,11 @@
<div <div
v-if="contentStyle === 'list' && hasItems" v-if="contentStyle === 'list' && hasItems"
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default" class="w-full min-w-0 max-w-full overflow-hidden ytp-table-surface"
> >
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain"> <div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
<table class="min-w-360 w-full text-sm"> <table class="min-w-360 w-full text-sm">
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned"> <thead class="bg-elevated/60 text-xs uppercase tracking-wide text-toned">
<tr <tr
class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0" class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
> >
@ -320,18 +315,12 @@
</div> </div>
<div v-else-if="hasItems" class="grid gap-4 md:grid-cols-2 xl:grid-cols-3"> <div v-else-if="hasItems" class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
<UCard <div
v-for="item in filteredItems" v-for="item in filteredItems"
:key="item.path" :key="item.path"
class="flex h-full flex-col" class="ytp-card flex h-full flex-col overflow-hidden"
:ui="{
root: 'bg-default border border-default',
header: 'p-4 pb-3',
body: 'flex flex-1 flex-col gap-4 p-4 pt-0',
footer: 'border-t border-default px-4 py-4',
}"
> >
<template #header> <div class="p-4 pb-3 ytp-border-bottom-soft">
<div class="flex items-start justify-between gap-3"> <div class="flex items-start justify-between gap-3">
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
@ -364,8 +353,9 @@
</label> </label>
</div> </div>
</div> </div>
</template> </div>
<div class="flex flex-1 flex-col gap-4 p-4 pt-0">
<div class="flex flex-wrap gap-2 text-sm *:min-w-32 *:flex-1"> <div class="flex flex-wrap gap-2 text-sm *:min-w-32 *:flex-1">
<div <div
class="min-w-0 rounded-md border border-default bg-muted/20 px-3 py-2 text-center text-default" class="min-w-0 rounded-md border border-default bg-muted/20 px-3 py-2 text-center text-default"
@ -387,8 +377,9 @@
</UTooltip> </UTooltip>
</div> </div>
</div> </div>
</div>
<template v-if="controlEnabled" #footer> <div v-if="controlEnabled" class="ytp-border-top-soft px-4 py-4">
<div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1"> <div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1">
<UButton <UButton
v-if="item.type === 'file'" v-if="item.type === 'file'"
@ -437,8 +428,8 @@
Delete Delete
</UButton> </UButton>
</div> </div>
</template> </div>
</UCard> </div>
</div> </div>
<div v-if="localSearch && !hasItems && !isLoading" class="space-y-3"> <div v-if="localSearch && !hasItems && !isLoading" class="space-y-3">
@ -610,7 +601,6 @@ const bulkActionGroups = computed<DropdownMenuItem[][]>(() => [
{ {
label: 'Delete Selected', label: 'Delete Selected',
icon: 'i-lucide-trash', icon: 'i-lucide-trash',
color: 'error',
disabled: !hasSelected.value || isLoading.value, disabled: !hasSelected.value || isLoading.value,
onSelect: () => void handleDeleteSelected(), onSelect: () => void handleDeleteSelected(),
}, },

View file

@ -1,17 +1,13 @@
<template> <template>
<main class="w-full min-w-0 max-w-full space-y-6"> <main class="w-full min-w-0 max-w-full space-y-6">
<div class="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between"> <div class="ytp-page-header">
<div class="flex min-w-0 items-center gap-3"> <div class="ytp-page-heading">
<span <span class="ytp-page-icon">
class="inline-flex size-11 shrink-0 items-center justify-center rounded-md border border-default bg-elevated/70 text-primary"
>
<UIcon :name="pageShell.icon" class="size-5" /> <UIcon :name="pageShell.icon" class="size-5" />
</span> </span>
<div class="min-w-0 space-y-2"> <div class="min-w-0 space-y-2">
<div <div class="ytp-page-kicker">
class="flex flex-wrap items-center gap-2 text-xs font-medium uppercase tracking-[0.2em] text-toned"
>
<span>{{ pageShell.sectionLabel }}</span> <span>{{ pageShell.sectionLabel }}</span>
<span>/</span> <span>/</span>
<span>{{ pageShell.pageLabel }}</span> <span>{{ pageShell.pageLabel }}</span>
@ -21,7 +17,7 @@
</div> </div>
</div> </div>
<div class="flex min-w-0 flex-wrap items-center justify-end gap-2 xl:justify-end"> <div class="flex w-full flex-wrap items-center gap-2 xl:w-auto xl:justify-end">
<UButton <UButton
v-if="logs.length > 0" v-if="logs.length > 0"
color="neutral" color="neutral"
@ -64,89 +60,108 @@
/> />
<template v-else> <template v-else>
<div v-if="filteredLogs.length > 0" class="space-y-4"> <div v-if="filteredLogs.length > 0" class="space-y-5">
<UPageCard v-for="log in filteredLogs" :key="log.tag" variant="outline" :ui="pageCardUi"> <section v-for="log in filteredLogs" :key="log.tag" class="space-y-3">
<template #body> <section class="space-y-3">
<div class="space-y-4"> <button
<div class="flex flex-wrap items-start justify-between gap-3"> type="button"
<div class="min-w-0 space-y-2"> class="flex w-full flex-wrap items-center justify-between gap-3 text-left"
<div class="flex flex-wrap items-center gap-2"> @click="toggleRelease(log.tag)"
<div
class="inline-flex items-center gap-2 text-base font-semibold text-highlighted"
> >
<UIcon name="i-lucide-git-branch" class="size-4 text-toned" /> <div class="flex min-w-0 items-center gap-3">
<span>{{ log.tag }}</span> <span class="ytp-section-icon">
</div> <UIcon
:name="isInstalled(log) ? 'i-lucide-badge-check' : 'i-lucide-git-branch'"
<UBadge v-if="isInstalled(log)" color="success" variant="soft" size="sm"> class="size-5"
Installed />
</UBadge>
</div>
</div>
<div class="flex flex-wrap items-center justify-end gap-2 text-xs text-toned">
<span
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
>
<UIcon name="i-lucide-list" class="size-3.5" />
<span>{{ log.commits?.length || 0 }} commits</span>
</span> </span>
<div class="min-w-0 space-y-1">
<h2 class="truncate text-base font-semibold text-highlighted">
{{ log.tag }}
</h2>
<div class="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-toned">
<UTooltip v-if="log.date" :text="`Release Date: ${log.date}`"> <UTooltip v-if="log.date" :text="`Release Date: ${log.date}`">
<span <span class="inline-flex cursor-help items-center gap-1.5">
class="inline-flex cursor-help items-center gap-1 rounded-md border border-default px-2 py-1" <UIcon name="i-lucide-calendar-days" class="size-3.5 text-muted" />
>
<UIcon name="i-lucide-calendar-days" class="size-3.5" />
<span>{{ moment(log.date).fromNow() }}</span> <span>{{ moment(log.date).fromNow() }}</span>
</span> </span>
</UTooltip> </UTooltip>
</div> </div>
</div> </div>
</div>
<div class="space-y-3 border-t border-default pt-4"> <div class="flex shrink-0 items-center gap-2">
<UBadge
v-if="isInstalled(log)"
color="success"
variant="soft"
size="sm"
icon="i-lucide-check"
>
Installed
</UBadge>
<UBadge color="neutral" variant="outline" size="sm" icon="i-lucide-list">
{{ log.commits?.length || 0 }} commits
</UBadge>
<UIcon
name="i-lucide-chevron-right"
:class="[
'size-4 text-toned transition-transform',
isReleaseOpen(log.tag) ? 'rotate-90' : '',
]"
/>
</div>
</button>
<div v-if="isReleaseOpen(log.tag)" class="space-y-2">
<article <article
v-for="commit in log.commits" v-for="commit in log.commits"
:key="commit.sha" :key="commit.sha"
class="flex flex-col gap-2 rounded-md border border-default bg-muted/20 px-3 py-3" class="rounded-md border border-default bg-elevated/20 px-3 py-3 transition-colors hover:bg-elevated/35"
> >
<div class="min-w-0">
<NuxtLink <NuxtLink
:to="`${REPO}/commit/${commit.full_sha}`" :to="`${REPO}/commit/${commit.full_sha}`"
target="_blank" target="_blank"
class="block min-w-0 text-sm text-default hover:underline" class="block min-w-0 text-sm font-medium leading-5 text-highlighted transition hover:text-primary"
> >
<span class="font-semibold text-highlighted"> {{ formatCommitMessage(commit.message) }}
{{ ucFirst(commit.message).replace(/\.$/, '') }}.
</span>
</NuxtLink> </NuxtLink>
</div>
<div class="flex flex-wrap items-center gap-2 text-xs text-toned"> <div class="mt-2 flex flex-wrap items-center gap-2 text-xs text-toned">
<span <span
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1" class="inline-flex items-center gap-1 rounded-sm border border-default px-2 py-1"
> >
<UIcon name="i-lucide-user" class="size-3.5" /> <UIcon name="i-lucide-user" class="size-3.5 text-muted" />
{{ commit.author }} <span>{{ commit.author }}</span>
</span> </span>
<UTooltip :text="`Date: ${commit.date}`"> <UTooltip :text="`Date: ${commit.date}`">
<span <time
class="inline-flex cursor-help items-center gap-1 rounded-md border border-default px-2 py-1" class="inline-flex cursor-help items-center gap-1 rounded-sm border border-default px-2 py-1"
> >
<UIcon name="i-lucide-clock-3" class="size-3.5" /> <UIcon name="i-lucide-clock-3" class="size-3.5 text-muted" />
{{ moment(commit.date).fromNow() }} <span>{{ moment(commit.date).fromNow() }}</span>
</span> </time>
</UTooltip> </UTooltip>
<UTooltip :text="`SHA: ${commit.full_sha}`"> <UTooltip :text="`SHA: ${commit.full_sha}`">
<span <NuxtLink
class="inline-flex cursor-help items-center gap-1 rounded-md border border-default px-2 py-1 font-medium" :to="`${REPO}/commit/${commit.full_sha}`"
target="_blank"
class="inline-flex items-center gap-1 rounded-sm border border-default px-2 py-1 font-mono transition hover:border-primary hover:text-primary"
> >
<UIcon name="i-lucide-git-commit-horizontal" class="size-3.5" /> <UIcon name="i-lucide-git-commit-horizontal" class="size-3.5 text-muted" />
{{ commit.sha }} <span>{{ commit.sha }}</span>
</span> </NuxtLink>
</UTooltip> </UTooltip>
<span <span
v-if="commit.full_sha === app_sha" v-if="commit.full_sha === app_sha"
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1 font-medium" class="inline-flex items-center gap-1 rounded-sm border border-default px-2 py-1 font-medium"
> >
<UIcon name="i-lucide-check" class="size-3.5 text-success" /> <UIcon name="i-lucide-check" class="size-3.5 text-success" />
<span>Installed</span> <span>Installed</span>
@ -154,9 +169,8 @@
</div> </div>
</article> </article>
</div> </div>
</div> </section>
</template> </section>
</UPageCard>
</div> </div>
<div v-else class="space-y-3"> <div v-else class="space-y-3">
@ -206,13 +220,7 @@ const isLoading = ref(true);
const query = ref(''); const query = ref('');
const toggleFilter = ref(false); const toggleFilter = ref(false);
const latestOnly = useStorage<boolean>('changelog_latest_only', true); const latestOnly = useStorage<boolean>('changelog_latest_only', true);
const openReleases = useStorage<string[]>('changelog_open_releases', []);
const pageCardUi = {
root: 'w-full bg-default',
container: 'w-full p-4 sm:p-5',
wrapper: 'w-full items-stretch',
body: 'w-full',
};
watch(toggleFilter, () => { watch(toggleFilter, () => {
if (!toggleFilter.value) { if (!toggleFilter.value) {
@ -250,6 +258,27 @@ const filteredLogs = computed<changelogs>(() => {
.filter((log): log is changeset => log !== null); .filter((log): log is changeset => log !== null);
}); });
watch(filteredLogs, (items) => {
if (openReleases.value.length > 0 || items.length < 1) {
return;
}
openReleases.value = items.slice(0, 3).map((log) => log.tag);
});
const isReleaseOpen = (tag: string): boolean => openReleases.value.includes(tag);
const toggleRelease = (tag: string): void => {
if (isReleaseOpen(tag)) {
openReleases.value = openReleases.value.filter((entry) => entry !== tag);
return;
}
openReleases.value = [...openReleases.value, tag];
};
const formatCommitMessage = (message: string): string => `${ucFirst(message).replace(/\.$/, '')}.`;
const loadContent = async (): Promise<void> => { const loadContent = async (): Promise<void> => {
if (app_version.value === '' || logs.value.length > 0) { if (app_version.value === '' || logs.value.length > 0) {
return; return;

View file

@ -1,17 +1,13 @@
<template> <template>
<main class="w-full min-w-0 max-w-full space-y-6"> <main class="w-full min-w-0 max-w-full space-y-6">
<div class="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between"> <div class="ytp-page-header">
<div class="flex min-w-0 items-center gap-3"> <div class="ytp-page-heading">
<span <span class="ytp-page-icon">
class="inline-flex size-11 shrink-0 items-center justify-center rounded-md border border-default bg-elevated/70 text-primary"
>
<UIcon :name="pageShell.icon" class="size-5" /> <UIcon :name="pageShell.icon" class="size-5" />
</span> </span>
<div class="min-w-0 space-y-2"> <div class="min-w-0 space-y-2">
<div <div class="ytp-page-kicker">
class="flex flex-wrap items-center gap-2 text-xs font-medium uppercase tracking-[0.2em] text-toned"
>
<span>{{ pageShell.sectionLabel }}</span> <span>{{ pageShell.sectionLabel }}</span>
<span>/</span> <span>/</span>
<span>{{ pageShell.pageLabel }}</span> <span>{{ pageShell.pageLabel }}</span>
@ -21,7 +17,7 @@
</div> </div>
</div> </div>
<div class="flex min-w-0 flex-wrap items-center justify-end gap-2 xl:justify-end"> <div class="flex w-full flex-wrap items-center gap-2 xl:w-auto xl:justify-end">
<UButton <UButton
v-if="items.length > 0" v-if="items.length > 0"
color="neutral" color="neutral"
@ -84,7 +80,7 @@
<div <div
v-if="!isLoading && filteredItems.length > 0" v-if="!isLoading && filteredItems.length > 0"
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-default bg-default px-3 py-3" class="flex flex-wrap items-center justify-between gap-3 ytp-card px-3 py-3"
> >
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<UButton <UButton
@ -129,11 +125,11 @@
<div <div
v-if="contentStyle === 'list' && filteredItems.length > 0" v-if="contentStyle === 'list' && filteredItems.length > 0"
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default" class="w-full min-w-0 max-w-full overflow-hidden ytp-table-surface"
> >
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain"> <div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
<table class="min-w-235 w-full text-sm"> <table class="min-w-235 w-full text-sm">
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned"> <thead class="bg-elevated/60 text-xs uppercase tracking-wide text-toned">
<tr <tr
class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0" class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
> >
@ -238,16 +234,8 @@
<div v-else-if="filteredItems.length > 0" class="grid gap-4 md:grid-cols-2 xl:grid-cols-3"> <div v-else-if="filteredItems.length > 0" class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
<div v-for="cond in filteredItems" :key="cond.id" class="min-w-0 w-full max-w-full"> <div v-for="cond in filteredItems" :key="cond.id" class="min-w-0 w-full max-w-full">
<UCard <div class="ytp-card flex h-full min-w-0 w-full max-w-full flex-col overflow-hidden">
class="flex h-full min-w-0 w-full max-w-full flex-col" <div class="p-4 pb-3 ytp-border-bottom-soft">
:ui="{
root: 'bg-default border border-default',
header: 'p-4 pb-3',
body: 'flex flex-1 flex-col gap-4 p-4 pt-0',
footer: 'border-t border-default px-4 py-4',
}"
>
<template #header>
<div class="flex min-w-0 items-start justify-between gap-3"> <div class="flex min-w-0 items-start justify-between gap-3">
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
@ -283,8 +271,9 @@
</label> </label>
</div> </div>
</div> </div>
</template> </div>
<div class="flex flex-1 flex-col gap-4 p-4 pt-0">
<div class="space-y-2 text-sm text-default"> <div class="space-y-2 text-sm text-default">
<div class="flex flex-wrap gap-2 text-xs text-toned *:min-w-32 *:flex-1"> <div class="flex flex-wrap gap-2 text-xs text-toned *:min-w-32 *:flex-1">
<button <button
@ -319,7 +308,9 @@
<UIcon name="i-lucide-filter" class="mt-0.5 size-4 shrink-0 text-toned" /> <UIcon name="i-lucide-filter" class="mt-0.5 size-4 shrink-0 text-toned" />
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<div class="text-xs font-medium text-toned">Filter</div> <div class="text-xs font-medium text-toned">Filter</div>
<span :class="['block', expandClass(cond.id, 'filter')]">{{ cond.filter }}</span> <span :class="['block', expandClass(cond.id, 'filter')]">{{
cond.filter
}}</span>
</div> </div>
</button> </button>
@ -375,8 +366,9 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<template #footer> <div class="ytp-border-top-soft px-4 py-4">
<div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1"> <div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1">
<UButton <UButton
color="neutral" color="neutral"
@ -398,8 +390,8 @@
Delete Delete
</UButton> </UButton>
</div> </div>
</template> </div>
</UCard> </div>
</div> </div>
</div> </div>
@ -566,7 +558,6 @@ const bulkActionGroups = computed<DropdownMenuItem[][]>(() => [
{ {
label: 'Remove Selected', label: 'Remove Selected',
icon: 'i-lucide-trash', icon: 'i-lucide-trash',
color: 'error',
disabled: !hasSelected.value || massDelete.value, disabled: !hasSelected.value || massDelete.value,
onSelect: () => void deleteSelected(), onSelect: () => void deleteSelected(),
}, },

View file

@ -1,17 +1,13 @@
<template> <template>
<main class="flex min-h-0 w-full min-w-0 max-w-full flex-1 flex-col gap-6"> <main class="flex min-h-0 w-full min-w-0 max-w-full flex-1 flex-col gap-6">
<div class="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between"> <div class="ytp-page-header">
<div class="flex min-w-0 items-start gap-3"> <div class="ytp-page-heading items-start">
<span <span class="ytp-page-icon">
class="inline-flex size-11 shrink-0 items-center justify-center rounded-md border border-default bg-elevated/70 text-primary"
>
<UIcon :name="pageShell.icon" class="size-5" /> <UIcon :name="pageShell.icon" class="size-5" />
</span> </span>
<div class="min-w-0 space-y-2"> <div class="min-w-0 space-y-2">
<div <div class="ytp-page-kicker">
class="flex flex-wrap items-center gap-2 text-xs font-medium uppercase tracking-[0.2em] text-toned"
>
<span>{{ pageShell.sectionLabel }}</span> <span>{{ pageShell.sectionLabel }}</span>
<span>/</span> <span>/</span>
<span>{{ pageShell.pageLabel }}</span> <span>{{ pageShell.pageLabel }}</span>
@ -45,19 +41,15 @@
</div> </div>
</div> </div>
<UPageCard variant="naked" :ui="pageCardUi" class="flex min-h-0 flex-1"> <div class="ytp-card flex min-h-0 flex-1 p-0 sm:p-0 flex-col">
<template #body>
<div class="flex min-h-0 flex-1 flex-col gap-4"> <div class="flex min-h-0 flex-1 flex-col gap-4">
<div <div
class="flex min-h-72 min-w-0 flex-1 overflow-hidden rounded-sm border border-default bg-neutral-950/95 shadow-sm" class="flex min-h-72 min-w-0 flex-1 overflow-hidden rounded-sm border border-default bg-neutral-950/95 shadow-sm"
> >
<div <div ref="terminal_window" class="terminal-host h-full min-h-0 w-full overflow-hidden" />
ref="terminal_window"
class="terminal-host h-full min-h-0 w-full overflow-hidden"
/>
</div> </div>
<div class="rounded-xl border border-default bg-default shadow-sm"> <div class="ytp-card shadow-sm">
<div <div
class="flex flex-col gap-3 border-b border-default bg-muted/10 px-4 py-3 lg:flex-row lg:items-start lg:justify-between" class="flex flex-col gap-3 border-b border-default bg-muted/10 px-4 py-3 lg:flex-row lg:items-start lg:justify-between"
> >
@ -172,9 +164,7 @@
:ui="historyCardUi" :ui="historyCardUi"
> >
<div class="flex flex-wrap items-center justify-between gap-3"> <div class="flex flex-wrap items-center justify-between gap-3">
<div <div class="flex items-center gap-2 text-sm font-semibold text-highlighted">
class="flex items-center gap-2 text-sm font-semibold text-highlighted"
>
<UIcon name="i-lucide-history" class="size-4 text-toned" /> <UIcon name="i-lucide-history" class="size-4 text-toned" />
<span>Recent runs</span> <span>Recent runs</span>
</div> </div>
@ -201,7 +191,7 @@
<div <div
v-else v-else
class="max-h-96 w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default" class="max-h-96 w-full min-w-0 max-w-full overflow-hidden ytp-table-surface"
> >
<div class="w-full max-w-full overflow-auto overscroll-contain"> <div class="w-full max-w-full overflow-auto overscroll-contain">
<table class="w-full text-sm"> <table class="w-full text-sm">
@ -261,9 +251,7 @@
.format('YYYY-M-DD H:mm Z') .format('YYYY-M-DD H:mm Z')
" "
> >
<span <span class="inline-flex items-center gap-1.5 cursor-help">
class="inline-flex items-center gap-1.5 cursor-help"
>
<UIcon name="i-lucide-clock-3" class="size-3.5" /> <UIcon name="i-lucide-clock-3" class="size-3.5" />
<time <time
:datetime=" :datetime="
@ -370,8 +358,7 @@
</div> </div>
</div> </div>
</div> </div>
</template> </div>
</UPageCard>
</main> </main>
</template> </template>
@ -436,13 +423,6 @@ const commandInput = ref<InstanceType<typeof InputAutocomplete> | null>(null);
const commandTextarea = ref<InstanceType<typeof TextareaAutocomplete> | null>(null); const commandTextarea = ref<InstanceType<typeof TextareaAutocomplete> | null>(null);
const storedCommand = useStorage<string>('console_command', ''); const storedCommand = useStorage<string>('console_command', '');
const pageCardUi = {
root: 'flex min-h-0 flex-1 w-full bg-transparent',
container: 'flex min-h-0 flex-1 w-full p-0 sm:p-0',
wrapper: 'flex min-h-0 flex-1 w-full items-stretch',
body: 'flex min-h-0 flex-1 w-full flex-col',
};
const historyCardUi = { const historyCardUi = {
body: 'space-y-3 p-4', body: 'space-y-3 p-4',
}; };

View file

@ -1,17 +1,13 @@
<template> <template>
<main class="w-full min-w-0 max-w-full space-y-6"> <main class="w-full min-w-0 max-w-full space-y-6">
<div class="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between"> <div class="ytp-page-header">
<div class="flex min-w-0 items-start gap-3"> <div class="ytp-page-heading items-start">
<span <span class="ytp-page-icon">
class="inline-flex size-11 shrink-0 items-center justify-center rounded-md border border-default bg-elevated/70 text-primary"
>
<UIcon :name="pageShell.icon" class="size-5" /> <UIcon :name="pageShell.icon" class="size-5" />
</span> </span>
<div class="min-w-0 space-y-2"> <div class="min-w-0 space-y-2">
<div <div class="ytp-page-kicker">
class="flex flex-wrap items-center gap-2 text-xs font-medium uppercase tracking-[0.2em] text-toned"
>
<span>{{ pageShell.sectionLabel }}</span> <span>{{ pageShell.sectionLabel }}</span>
<span>/</span> <span>/</span>
<span>{{ pageShell.pageLabel }}</span> <span>{{ pageShell.pageLabel }}</span>
@ -21,7 +17,7 @@
</div> </div>
</div> </div>
<div class="flex min-w-0 flex-wrap items-center justify-end gap-2 xl:justify-end"> <div class="flex w-full flex-wrap items-center gap-2 xl:w-auto xl:justify-end">
<UButton <UButton
color="neutral" color="neutral"
variant="outline" variant="outline"
@ -47,10 +43,7 @@
</div> </div>
</div> </div>
<div <div v-if="!report && isLoading" class="ytp-card flex min-h-72 items-center justify-center">
v-if="!report && isLoading"
class="flex min-h-72 items-center justify-center rounded-md border border-default bg-default/90"
>
<div class="flex flex-col items-center gap-3 text-center text-toned"> <div class="flex flex-col items-center gap-3 text-center text-toned">
<UIcon name="i-lucide-loader-circle" class="size-10 animate-spin text-info" /> <UIcon name="i-lucide-loader-circle" class="size-10 animate-spin text-info" />
<div class="space-y-1"> <div class="space-y-1">
@ -86,28 +79,15 @@
</div> </div>
<div class="grid gap-3 sm:grid-cols-2 xl:grid-cols-4"> <div class="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
<article <StatCard
v-for="item in summaryCards" v-for="item in summaryCards"
:key="item.label" :key="item.label"
class="rounded-md border border-default bg-default px-3 py-3 shadow-sm" :label="item.label"
> :value="item.value"
<div class="flex items-start justify-between gap-3"> :hint="item.description"
<div class="min-w-0"> :icon="item.icon"
<p class="text-xs font-medium uppercase tracking-wide text-toned"> :color="item.color"
{{ item.label }} />
</p>
<p class="mt-1 text-xs text-toned">{{ item.description }}</p>
</div>
<span
class="inline-flex size-8 shrink-0 items-center justify-center rounded-md border border-default bg-default"
>
<UIcon :name="item.icon" class="size-4 text-toned" />
</span>
</div>
<p :class="['mt-4 text-2xl font-semibold', item.valueClass]">{{ item.value }}</p>
</article>
</div> </div>
</section> </section>
@ -118,28 +98,16 @@
</div> </div>
<div class="grid gap-3 sm:grid-cols-2 xl:grid-cols-3"> <div class="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
<article <StatCard
v-for="row in runtimeRows" v-for="row in runtimeRows"
:key="row.label" :key="row.label"
class="rounded-md border border-default bg-default px-3 py-3 shadow-sm" :label="row.label"
> :value="row.value"
<div class="flex items-start justify-between gap-3"> :hint="row.description"
<div class="min-w-0"> :icon="row.icon"
<p class="text-xs font-medium uppercase tracking-wide text-toned"> color="neutral"
{{ row.label }} value-wrap
</p> />
<p class="mt-1 text-xs text-toned">{{ row.description }}</p>
</div>
<span
class="inline-flex size-8 shrink-0 items-center justify-center rounded-md border border-default bg-default"
>
<UIcon :name="row.icon" class="size-4 text-toned" />
</span>
</div>
<p class="mt-4 wrap-break-word text-sm font-semibold text-default">{{ row.value }}</p>
</article>
</div> </div>
</section> </section>
@ -155,11 +123,7 @@
</div> </div>
<div class="grid gap-4 lg:grid-cols-2 2xl:grid-cols-3"> <div class="grid gap-4 lg:grid-cols-2 2xl:grid-cols-3">
<article <article v-for="item in section.items" :key="item.id" class="ytp-card-padded shadow-sm">
v-for="item in section.items"
:key="item.id"
class="rounded-md border border-default bg-default px-4 py-4 shadow-sm"
>
<div class="min-w-0 space-y-3"> <div class="min-w-0 space-y-3">
<div class="space-y-2"> <div class="space-y-2">
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
@ -198,6 +162,7 @@
<script setup lang="ts"> <script setup lang="ts">
import moment from 'moment'; import moment from 'moment';
import StatCard from '~/components/StatCard.vue';
import type { DiagnosticCheck, DiagnosticStatus } from '~/types/diagnostics'; import type { DiagnosticCheck, DiagnosticStatus } from '~/types/diagnostics';
import { requirePageShell } from '~/utils/topLevelNavigation'; import { requirePageShell } from '~/utils/topLevelNavigation';
import { copyText } from '~/utils'; import { copyText } from '~/utils';
@ -207,7 +172,7 @@ type SummaryCard = {
description: string; description: string;
value: number; value: number;
icon: string; icon: string;
valueClass: string; color: 'success' | 'error' | 'warning' | 'neutral';
}; };
type DetailRow = { type DetailRow = {
@ -295,28 +260,28 @@ const summaryCards = computed<Array<SummaryCard>>(() => {
description: 'Checks that passed.', description: 'Checks that passed.',
value: current.summary.pass, value: current.summary.pass,
icon: 'i-lucide-badge-check', icon: 'i-lucide-badge-check',
valueClass: current.summary.pass > 0 ? 'text-success' : 'text-default', color: current.summary.pass > 0 ? 'success' : 'neutral',
}, },
{ {
label: 'Required fails', label: 'Required fails',
description: 'Items that block core use.', description: 'Items that block core use.',
value: current.summary.required_failed, value: current.summary.required_failed,
icon: 'i-lucide-octagon-alert', icon: 'i-lucide-octagon-alert',
valueClass: current.summary.required_failed > 0 ? 'text-error' : 'text-default', color: current.summary.required_failed > 0 ? 'error' : 'neutral',
}, },
{ {
label: 'Warnings', label: 'Warnings',
description: 'Optional or incomplete items.', description: 'Optional or incomplete items.',
value: current.summary.warn, value: current.summary.warn,
icon: 'i-lucide-triangle-alert', icon: 'i-lucide-triangle-alert',
valueClass: current.summary.warn > 0 ? 'text-warning' : 'text-default', color: current.summary.warn > 0 ? 'warning' : 'neutral',
}, },
{ {
label: 'Skipped', label: 'Skipped',
description: 'Not configured or not needed.', description: 'Not configured or not needed.',
value: current.summary.skip, value: current.summary.skip,
icon: 'i-lucide-minus', icon: 'i-lucide-minus',
valueClass: current.summary.skip > 0 ? 'text-toned' : 'text-default', color: 'neutral',
}, },
]; ];
}); });

View file

@ -1,17 +1,13 @@
<template> <template>
<main class="w-full min-w-0 max-w-full space-y-6"> <main class="w-full min-w-0 max-w-full space-y-6">
<div class="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between"> <div class="ytp-page-header">
<div class="flex min-w-0 items-center gap-3"> <div class="ytp-page-heading">
<span <span class="ytp-page-icon">
class="inline-flex size-11 shrink-0 items-center justify-center rounded-md border border-default bg-elevated/70 text-primary"
>
<UIcon :name="pageShell.icon" class="size-5" /> <UIcon :name="pageShell.icon" class="size-5" />
</span> </span>
<div class="min-w-0 space-y-2"> <div class="min-w-0 space-y-2">
<div <div class="ytp-page-kicker">
class="flex flex-wrap items-center gap-2 text-xs font-medium uppercase tracking-[0.2em] text-toned"
>
<span>{{ pageShell.sectionLabel }}</span> <span>{{ pageShell.sectionLabel }}</span>
<span>/</span> <span>/</span>
<span>{{ pageShell.pageLabel }}</span> <span>{{ pageShell.pageLabel }}</span>
@ -21,7 +17,7 @@
</div> </div>
</div> </div>
<div class="flex min-w-0 flex-wrap items-center justify-end gap-2 xl:justify-end"> <div class="flex w-full flex-wrap items-center gap-2 xl:w-auto xl:justify-end">
<UButton <UButton
v-if="items.length > 0" v-if="items.length > 0"
color="neutral" color="neutral"
@ -84,7 +80,7 @@
<div <div
v-if="!isLoading && filteredItems.length > 0" v-if="!isLoading && filteredItems.length > 0"
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-default bg-default px-3 py-3" class="flex flex-wrap items-center justify-between gap-3 ytp-card px-3 py-3"
> >
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<UButton <UButton
@ -129,11 +125,11 @@
<div <div
v-if="contentStyle === 'list' && filteredItems.length > 0" v-if="contentStyle === 'list' && filteredItems.length > 0"
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default" class="w-full min-w-0 max-w-full overflow-hidden ytp-table-surface"
> >
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain"> <div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
<table class="min-w-235 w-full text-sm"> <table class="min-w-235 w-full text-sm">
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned"> <thead class="bg-elevated/60 text-xs uppercase tracking-wide text-toned">
<tr <tr
class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0" class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
> >
@ -237,16 +233,8 @@
<div v-else-if="filteredItems.length > 0" class="grid gap-4 md:grid-cols-2 xl:grid-cols-3"> <div v-else-if="filteredItems.length > 0" class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
<div v-for="field in filteredItems" :key="field.id" class="min-w-0 w-full max-w-full"> <div v-for="field in filteredItems" :key="field.id" class="min-w-0 w-full max-w-full">
<UCard <div class="ytp-card flex h-full min-w-0 w-full max-w-full flex-col overflow-hidden">
class="flex h-full min-w-0 w-full max-w-full flex-col" <div class="p-4 pb-3 ytp-border-bottom-soft">
:ui="{
root: 'bg-default border border-default',
header: 'p-4 pb-3',
body: 'flex flex-1 flex-col gap-4 p-4 pt-0',
footer: 'border-t border-default px-4 py-4',
}"
>
<template #header>
<div class="flex min-w-0 items-start justify-between gap-3"> <div class="flex min-w-0 items-start justify-between gap-3">
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
@ -282,8 +270,9 @@
</label> </label>
</div> </div>
</div> </div>
</template> </div>
<div class="flex flex-1 flex-col gap-4 p-4 pt-0">
<div class="space-y-2 text-sm text-default"> <div class="space-y-2 text-sm text-default">
<div class="flex flex-wrap gap-2 text-xs text-toned *:min-w-32 *:flex-1"> <div class="flex flex-wrap gap-2 text-xs text-toned *:min-w-32 *:flex-1">
<span <span
@ -310,7 +299,9 @@
<UIcon name="i-lucide-terminal" class="mt-0.5 size-4 shrink-0 text-toned" /> <UIcon name="i-lucide-terminal" class="mt-0.5 size-4 shrink-0 text-toned" />
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<div class="text-xs font-medium text-toned">Associated option</div> <div class="text-xs font-medium text-toned">Associated option</div>
<span :class="['block', expandClass(field.id, 'field')]">{{ field.field }}</span> <span :class="['block', expandClass(field.id, 'field')]">{{
field.field
}}</span>
</div> </div>
</button> </button>
@ -333,8 +324,9 @@
</button> </button>
</div> </div>
</div> </div>
</div>
<template #footer> <div class="ytp-border-top-soft px-4 py-4">
<div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1"> <div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1">
<UButton <UButton
color="neutral" color="neutral"
@ -356,8 +348,8 @@
Delete Delete
</UButton> </UButton>
</div> </div>
</template> </div>
</UCard> </div>
</div> </div>
</div> </div>
@ -491,7 +483,6 @@ const bulkActionGroups = computed<DropdownMenuItem[][]>(() => [
{ {
label: 'Remove Selected', label: 'Remove Selected',
icon: 'i-lucide-trash', icon: 'i-lucide-trash',
color: 'error',
disabled: !hasSelected.value || massDelete.value, disabled: !hasSelected.value || massDelete.value,
onSelect: () => void deleteSelected(), onSelect: () => void deleteSelected(),
}, },

View file

@ -1,17 +1,13 @@
<template> <template>
<main class="space-y-6"> <main class="w-full min-w-0 max-w-full space-y-6">
<div class="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between"> <div class="ytp-page-header">
<div class="flex min-w-0 items-center gap-3"> <div class="ytp-page-heading">
<span <span class="ytp-page-icon">
class="inline-flex size-11 shrink-0 items-center justify-center rounded-md border border-default bg-elevated/70 text-primary"
>
<UIcon :name="pageShell.icon" class="size-5" /> <UIcon :name="pageShell.icon" class="size-5" />
</span> </span>
<div class="min-w-0 space-y-2"> <div class="min-w-0 space-y-2">
<div <div class="ytp-page-kicker">
class="flex flex-wrap items-center gap-2 text-xs font-medium uppercase tracking-[0.2em] text-toned"
>
<span>{{ pageShell.sectionLabel }}</span> <span>{{ pageShell.sectionLabel }}</span>
<span>/</span> <span>/</span>
<span>{{ pageShell.pageLabel }}</span> <span>{{ pageShell.pageLabel }}</span>
@ -20,42 +16,24 @@
<p class="max-w-3xl text-sm text-toned">{{ pageShell.description }}</p> <p class="max-w-3xl text-sm text-toned">{{ pageShell.description }}</p>
</div> </div>
</div> </div>
<div class="flex flex-wrap justify-end gap-2 xl:justify-end">
<UButton
v-for="entry in docsEntries"
:key="entry.id"
:to="entry.route"
:color="entry.file === docEntry.file ? 'primary' : 'neutral'"
:variant="entry.file === docEntry.file ? 'solid' : 'outline'"
size="sm"
:icon="entry.icon"
>
{{ entry.navLabel }}
</UButton>
</div>
</div> </div>
<UPageCard variant="outline" :ui="pageCardUi"> <div class="ytp-card p-0 overflow-hidden">
<template #body> <div class="min-w-0 max-w-full px-4 py-5 sm:px-6 sm:py-6 lg:px-7">
<div class="px-4 py-5 sm:px-6 sm:py-6 lg:px-7">
<Markdown :file="`/api/docs/${docEntry.file}`" /> <Markdown :file="`/api/docs/${docEntry.file}`" />
</div> </div>
</template> </div>
</UPageCard>
</main> </main>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Markdown from '~/components/Markdown.vue'; import Markdown from '~/components/Markdown.vue';
import { DOCS_ENTRIES, getDocsEntryBySlug } from '~/composables/useDocs'; import { getDocsEntryBySlug } from '~/composables/useDocs';
import { formatPageTitle } from '~/utils'; import { formatPageTitle } from '~/utils';
import { requirePageShell } from '~/utils/topLevelNavigation'; import { requirePageShell } from '~/utils/topLevelNavigation';
const route = useRoute(); const route = useRoute();
const docsEntries = DOCS_ENTRIES;
const docEntry = computed(() => { const docEntry = computed(() => {
const entry = getDocsEntryBySlug(route.params.slug as string | string[] | undefined); const entry = getDocsEntryBySlug(route.params.slug as string | string[] | undefined);
@ -74,11 +52,4 @@ const pageShell = computed(() => requirePageShell(docEntry.value.id));
useHead(() => ({ useHead(() => ({
title: formatPageTitle(docEntry.value.title), title: formatPageTitle(docEntry.value.title),
})); }));
const pageCardUi = {
root: 'w-full bg-default',
container: 'w-full p-0',
wrapper: 'w-full items-stretch',
body: 'w-full p-0',
};
</script> </script>

View file

@ -6,18 +6,14 @@
aria-hidden="true" aria-hidden="true"
/> />
<div class="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between"> <div class="ytp-page-header">
<div class="flex min-w-0 items-center gap-3"> <div class="ytp-page-heading">
<span <span class="ytp-page-icon">
class="inline-flex size-11 shrink-0 items-center justify-center rounded-md border border-default bg-elevated/70 text-primary"
>
<UIcon :name="pageShell.icon" class="size-5" /> <UIcon :name="pageShell.icon" class="size-5" />
</span> </span>
<div class="min-w-0 space-y-2"> <div class="min-w-0 space-y-2">
<div <div class="ytp-page-kicker">
class="flex flex-wrap items-center gap-2 text-xs font-medium uppercase tracking-[0.2em] text-toned"
>
<span>{{ pageShell.sectionLabel }}</span> <span>{{ pageShell.sectionLabel }}</span>
<span>/</span> <span>/</span>
<span>{{ pageShell.pageLabel }}</span> <span>{{ pageShell.pageLabel }}</span>
@ -27,7 +23,7 @@
</div> </div>
</div> </div>
<div class="flex min-w-0 flex-wrap items-center justify-end gap-2 xl:justify-end"> <div class="flex w-full flex-wrap items-center gap-2 xl:w-auto xl:justify-end">
<UButton <UButton
color="neutral" color="neutral"
variant="outline" variant="outline"
@ -102,7 +98,7 @@
<div class="w-full min-w-0 max-w-full space-y-4"> <div class="w-full min-w-0 max-w-full space-y-4">
<div <div
v-if="hasItems" v-if="hasItems"
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-default bg-default px-3 py-3" class="flex flex-wrap items-center justify-between gap-3 ytp-card px-3 py-3"
> >
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<UButton <UButton
@ -143,11 +139,11 @@
<div <div
v-if="'list' === contentStyle && hasItems" v-if="'list' === contentStyle && hasItems"
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default" class="w-full min-w-0 max-w-full overflow-hidden ytp-table-surface"
> >
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain"> <div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
<table class="min-w-210 table-fixed w-full text-sm"> <table class="min-w-210 table-fixed w-full text-sm">
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned"> <thead class="bg-elevated/60 text-xs uppercase tracking-wide text-toned">
<tr <tr
class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0" class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
> >
@ -395,16 +391,8 @@
:min-height="showThumbnails ? 410 : 210" :min-height="showThumbnails ? 410 : 210"
class="min-h-0 min-w-0 w-full max-w-full" class="min-h-0 min-w-0 w-full max-w-full"
> >
<UCard <div class="ytp-card flex h-full min-w-0 w-full max-w-full flex-col overflow-hidden">
class="flex h-full min-w-0 w-full max-w-full flex-col overflow-hidden" <div class="ytp-border-bottom-soft p-4 pb-3">
:ui="{
body: 'flex flex-1 flex-col gap-4 p-4',
footer: 'border-t border-default px-4 py-4',
header: 'p-4 pb-3',
root: 'bg-default border border-default',
}"
>
<template #header>
<div class="flex min-w-0 flex-wrap items-start justify-between gap-3"> <div class="flex min-w-0 flex-wrap items-start justify-between gap-3">
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<UTooltip :text="item.title"> <UTooltip :text="item.title">
@ -474,8 +462,9 @@
</label> </label>
</div> </div>
</div> </div>
</template> </div>
<div class="flex flex-1 flex-col gap-4 p-4">
<div <div
v-if="showThumbnails" v-if="showThumbnails"
class="-mx-4 -mt-4 overflow-hidden border-b border-default bg-muted/20" class="-mx-4 -mt-4 overflow-hidden border-b border-default bg-muted/20"
@ -626,7 +615,7 @@
</p> </p>
</div> </div>
<template #footer> <div class="ytp-border-top-soft px-4 py-4">
<div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1"> <div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1">
<UButton <UButton
v-if="showRetryAction(item)" v-if="showRetryAction(item)"
@ -685,8 +674,9 @@
</UButton> </UButton>
</UDropdownMenu> </UDropdownMenu>
</div> </div>
</template> </div>
</UCard> </div>
</div>
</LateLoader> </LateLoader>
</div> </div>

View file

@ -1,17 +1,13 @@
<template> <template>
<div class="space-y-6"> <div class="space-y-6">
<div class="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between"> <div class="ytp-page-header">
<div class="flex min-w-0 items-center gap-3"> <div class="ytp-page-heading">
<span <span class="ytp-page-icon">
class="inline-flex size-11 shrink-0 items-center justify-center rounded-md border border-default bg-elevated/70 text-primary"
>
<UIcon :name="pageShell.icon" class="size-5" /> <UIcon :name="pageShell.icon" class="size-5" />
</span> </span>
<div class="min-w-0 space-y-2"> <div class="min-w-0 space-y-2">
<div <div class="ytp-page-kicker">
class="flex flex-wrap items-center gap-2 text-xs font-medium uppercase tracking-[0.2em] text-toned"
>
<span>{{ pageShell.sectionLabel }}</span> <span>{{ pageShell.sectionLabel }}</span>
<span>/</span> <span>/</span>
<span>{{ pageShell.pageLabel }}</span> <span>{{ pageShell.pageLabel }}</span>
@ -21,7 +17,7 @@
</div> </div>
</div> </div>
<div class="flex min-w-0 flex-wrap items-center justify-end gap-2 xl:justify-end"> <div class="flex w-full flex-wrap items-center gap-2 xl:w-auto xl:justify-end">
<UButton <UButton
color="neutral" color="neutral"
:variant="toggleFilter ? 'soft' : 'outline'" :variant="toggleFilter ? 'soft' : 'outline'"
@ -56,7 +52,7 @@
<UButton <UButton
color="neutral" color="neutral"
variant="outline" :variant="config.showForm ? 'soft' : 'outline'"
size="sm" size="sm"
icon="i-lucide-plus" icon="i-lucide-plus"
@click="config.showForm = !config.showForm" @click="config.showForm = !config.showForm"
@ -109,7 +105,7 @@
<div class="w-full min-w-0 max-w-full space-y-4"> <div class="w-full min-w-0 max-w-full space-y-4">
<div v-if="!socket.isConnected" class="flex justify-end"> <div v-if="!socket.isConnected" class="flex justify-end">
<UButton <UButton
color="info" color="neutral"
variant="outline" variant="outline"
size="sm" size="sm"
icon="i-lucide-refresh-cw" icon="i-lucide-refresh-cw"
@ -122,7 +118,7 @@
<div <div
v-if="hasItems" v-if="hasItems"
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-default bg-default px-3 py-3" class="flex flex-wrap items-center justify-between gap-3 ytp-card px-3 py-3"
> >
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<UButton <UButton
@ -175,11 +171,11 @@
<div <div
v-if="'list' === contentStyle && hasItems" v-if="'list' === contentStyle && hasItems"
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default" class="w-full min-w-0 max-w-full overflow-hidden ytp-table-surface"
> >
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain"> <div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
<table class="min-w-210 table-fixed w-full text-sm"> <table class="min-w-210 table-fixed w-full text-sm">
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned"> <thead class="bg-elevated/60 text-xs uppercase tracking-wide text-toned">
<tr <tr
class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0" class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
> >
@ -409,16 +405,8 @@
:min-height="showThumbnails ? 475 : 265" :min-height="showThumbnails ? 475 : 265"
class="min-h-0 min-w-0 w-full max-w-full" class="min-h-0 min-w-0 w-full max-w-full"
> >
<UCard <div class="ytp-card flex h-full min-w-0 w-full max-w-full flex-col overflow-hidden">
class="flex h-full min-w-0 w-full max-w-full flex-col overflow-hidden" <div class="p-4 pb-3 ytp-border-bottom-soft">
:ui="{
body: 'flex flex-1 flex-col gap-4 p-4',
footer: 'border-t border-default px-4 py-4',
header: 'p-4 pb-3',
root: 'bg-default border border-default',
}"
>
<template #header>
<div class="flex min-w-0 flex-wrap items-start justify-between gap-3"> <div class="flex min-w-0 flex-wrap items-start justify-between gap-3">
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<UTooltip :text="item.title"> <UTooltip :text="item.title">
@ -494,8 +482,9 @@
</label> </label>
</div> </div>
</div> </div>
</template> </div>
<div class="flex flex-1 flex-col gap-4 p-4">
<div <div
v-if="showThumbnails" v-if="showThumbnails"
class="-mx-4 -mt-4 overflow-hidden border-b border-default bg-muted/20" class="-mx-4 -mt-4 overflow-hidden border-b border-default bg-muted/20"
@ -572,7 +561,10 @@
@click="toggleExpand(item._id, 'preset')" @click="toggleExpand(item._id, 'preset')"
> >
<span class="inline-flex w-full items-center justify-center gap-2"> <span class="inline-flex w-full items-center justify-center gap-2">
<UIcon name="i-lucide-sliders-horizontal" class="size-4 shrink-0 text-toned" /> <UIcon
name="i-lucide-sliders-horizontal"
class="size-4 shrink-0 text-toned"
/>
<span :class="['min-w-0 text-center', expandClass(item._id, 'preset')]"> <span :class="['min-w-0 text-center', expandClass(item._id, 'preset')]">
{{ item.preset }} {{ item.preset }}
</span> </span>
@ -610,8 +602,9 @@
</span> </span>
</button> </button>
</div> </div>
</div>
<template #footer> <div class="ytp-border-top-soft px-4 py-4">
<div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1"> <div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1">
<UButton <UButton
color="neutral" color="neutral"
@ -657,8 +650,8 @@
</UButton> </UButton>
</UDropdownMenu> </UDropdownMenu>
</div> </div>
</template> </div>
</UCard> </div>
</LateLoader> </LateLoader>
</div> </div>

View file

@ -17,11 +17,9 @@
<template> <template>
<main class="w-full min-w-0 max-w-full space-y-6"> <main class="w-full min-w-0 max-w-full space-y-6">
<div class="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between"> <div class="ytp-page-header">
<div class="flex min-w-0 items-start gap-3"> <div class="ytp-page-heading items-start">
<span <span class="ytp-page-icon">
class="inline-flex size-11 shrink-0 items-center justify-center rounded-md border border-default bg-elevated/70 text-primary"
>
<UIcon <UIcon
name="i-lucide-file-text" name="i-lucide-file-text"
:class="[ :class="[
@ -34,9 +32,7 @@
</span> </span>
<div class="min-w-0 space-y-2"> <div class="min-w-0 space-y-2">
<div <div class="ytp-page-kicker">
class="flex flex-wrap items-center gap-2 text-xs font-medium uppercase tracking-[0.2em] text-toned"
>
<span>{{ pageShell.sectionLabel }}</span> <span>{{ pageShell.sectionLabel }}</span>
<span>/</span> <span>/</span>
<span>{{ pageShell.pageLabel }}</span> <span>{{ pageShell.pageLabel }}</span>
@ -46,7 +42,7 @@
</div> </div>
</div> </div>
<div class="flex min-w-0 flex-wrap items-center justify-end gap-2 xl:justify-end"> <div class="flex w-full flex-wrap items-center gap-2 xl:w-auto xl:justify-end">
<UButton <UButton
v-if="!autoScroll" v-if="!autoScroll"
color="neutral" color="neutral"
@ -123,9 +119,8 @@
</div> </div>
</div> </div>
<UPageCard variant="naked" :ui="pageCardUi"> <div class="ytp-card min-w-0 max-w-full p-4 sm:p-5 overflow-hidden">
<template #body> <div class="ytp-frame overflow-hidden shadow-sm">
<div class="overflow-hidden rounded-sm border border-default bg-default shadow-sm">
<div <div
ref="logContainer" ref="logContainer"
class="w-full min-w-0 max-w-full min-h-[55vh] max-h-[60vh] overflow-y-auto overflow-x-hidden bg-transparent font-mono text-sm text-default overscroll-x-contain" class="w-full min-w-0 max-w-full min-h-[55vh] max-h-[60vh] overflow-y-auto overflow-x-hidden bg-transparent font-mono text-sm text-default overscroll-x-contain"
@ -249,8 +244,7 @@
</div> </div>
</div> </div>
</div> </div>
</template> </div>
</UPageCard>
<LogDetailModal v-model="detailsOpen" :log="selectedLog" /> <LogDetailModal v-model="detailsOpen" :log="selectedLog" />
</main> </main>
@ -316,13 +310,6 @@ const reachedEnd = ref(false);
const detailsOpen = ref(false); const detailsOpen = ref(false);
const expandedRows = ref<Set<string>>(new Set()); const expandedRows = ref<Set<string>>(new Set());
const pageCardUi = {
root: 'w-full min-w-0 max-w-full bg-transparent',
container: 'w-full min-w-0 max-w-full p-4 sm:p-5',
wrapper: 'w-full min-w-0 items-stretch',
body: 'w-full min-w-0 max-w-full overflow-hidden',
};
const query = ref<string>( const query = ref<string>(
(() => { (() => {
const filter = route.query.filter ?? ''; const filter = route.query.filter ?? '';

View file

@ -1,17 +1,13 @@
<template> <template>
<main class="w-full min-w-0 max-w-full space-y-6"> <main class="w-full min-w-0 max-w-full space-y-6">
<div class="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between"> <div class="ytp-page-header">
<div class="flex min-w-0 items-center gap-3"> <div class="ytp-page-heading">
<span <span class="ytp-page-icon">
class="inline-flex size-11 shrink-0 items-center justify-center rounded-md border border-default bg-elevated/70 text-primary"
>
<UIcon :name="pageShell.icon" class="size-5" /> <UIcon :name="pageShell.icon" class="size-5" />
</span> </span>
<div class="min-w-0 space-y-2"> <div class="min-w-0 space-y-2">
<div <div class="ytp-page-kicker">
class="flex flex-wrap items-center gap-2 text-xs font-medium uppercase tracking-[0.2em] text-toned"
>
<span>{{ pageShell.sectionLabel }}</span> <span>{{ pageShell.sectionLabel }}</span>
<span>/</span> <span>/</span>
<span>{{ pageShell.pageLabel }}</span> <span>{{ pageShell.pageLabel }}</span>
@ -21,7 +17,7 @@
</div> </div>
</div> </div>
<div class="flex min-w-0 flex-wrap items-center justify-end gap-2 xl:justify-end"> <div class="flex w-full flex-wrap items-center gap-2 xl:w-auto xl:justify-end">
<UButton <UButton
v-if="notifications.length > 0" v-if="notifications.length > 0"
color="neutral" color="neutral"
@ -97,7 +93,7 @@
<div <div
v-if="!isLoading && filteredTargets.length > 0" v-if="!isLoading && filteredTargets.length > 0"
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-default bg-default px-3 py-3" class="flex flex-wrap items-center justify-between gap-3 ytp-card px-3 py-3"
> >
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<UButton <UButton
@ -142,11 +138,11 @@
<div <div
v-if="contentStyle === 'list' && filteredTargets.length > 0" v-if="contentStyle === 'list' && filteredTargets.length > 0"
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default" class="w-full min-w-0 max-w-full overflow-hidden ytp-table-surface"
> >
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain"> <div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
<table class="min-w-235 w-full text-sm"> <table class="min-w-235 w-full text-sm">
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned"> <thead class="bg-elevated/60 text-xs uppercase tracking-wide text-toned">
<tr <tr
class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0" class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
> >
@ -275,16 +271,8 @@
<div v-else-if="filteredTargets.length > 0" class="grid gap-4 md:grid-cols-2 xl:grid-cols-3"> <div v-else-if="filteredTargets.length > 0" class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
<div v-for="item in filteredTargets" :key="item.id" class="min-w-0 w-full max-w-full"> <div v-for="item in filteredTargets" :key="item.id" class="min-w-0 w-full max-w-full">
<UCard <div class="ytp-card flex h-full min-w-0 w-full max-w-full flex-col overflow-hidden">
class="flex h-full min-w-0 w-full max-w-full flex-col" <div class="p-4 pb-3 ytp-border-bottom-soft">
:ui="{
root: 'bg-default border border-default',
header: 'p-4 pb-3',
body: 'flex flex-1 flex-col gap-4 p-4 pt-0',
footer: 'border-t border-default px-4 py-4',
}"
>
<template #header>
<div class="flex min-w-0 items-start justify-between gap-3"> <div class="flex min-w-0 items-start justify-between gap-3">
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
@ -323,8 +311,9 @@
</label> </label>
</div> </div>
</div> </div>
</template> </div>
<div class="flex flex-1 flex-col gap-4 p-4 pt-0">
<div class="space-y-2 text-sm text-default"> <div class="space-y-2 text-sm text-default">
<div class="flex flex-wrap gap-2 text-xs text-toned *:min-w-32 *:flex-1"> <div class="flex flex-wrap gap-2 text-xs text-toned *:min-w-32 *:flex-1">
<button <button
@ -434,8 +423,9 @@
</button> </button>
</div> </div>
</div> </div>
</div>
<template #footer> <div class="ytp-border-top-soft px-4 py-4">
<div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1"> <div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1">
<UButton <UButton
color="neutral" color="neutral"
@ -457,8 +447,8 @@
Delete Delete
</UButton> </UButton>
</div> </div>
</template> </div>
</UCard> </div>
</div> </div>
</div> </div>
@ -657,7 +647,6 @@ const bulkActionGroups = computed<DropdownMenuItem[][]>(() => [
{ {
label: 'Remove Selected', label: 'Remove Selected',
icon: 'i-lucide-trash', icon: 'i-lucide-trash',
color: 'error',
disabled: !hasSelected.value || massDelete.value, disabled: !hasSelected.value || massDelete.value,
onSelect: () => void deleteSelected(), onSelect: () => void deleteSelected(),
}, },

View file

@ -1,17 +1,13 @@
<template> <template>
<main class="w-full min-w-0 max-w-full space-y-6"> <main class="w-full min-w-0 max-w-full space-y-6">
<div class="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between"> <div class="ytp-page-header">
<div class="flex min-w-0 items-center gap-3"> <div class="ytp-page-heading">
<span <span class="ytp-page-icon">
class="inline-flex size-11 shrink-0 items-center justify-center rounded-md border border-default bg-elevated/70 text-primary"
>
<UIcon :name="pageShell.icon" class="size-5" /> <UIcon :name="pageShell.icon" class="size-5" />
</span> </span>
<div class="min-w-0 space-y-2"> <div class="min-w-0 space-y-2">
<div <div class="ytp-page-kicker">
class="flex flex-wrap items-center gap-2 text-xs font-medium uppercase tracking-[0.2em] text-toned"
>
<span>{{ pageShell.sectionLabel }}</span> <span>{{ pageShell.sectionLabel }}</span>
<span>/</span> <span>/</span>
<span>{{ pageShell.pageLabel }}</span> <span>{{ pageShell.pageLabel }}</span>
@ -21,7 +17,7 @@
</div> </div>
</div> </div>
<div class="flex min-w-0 flex-wrap items-center justify-end gap-2 xl:justify-end"> <div class="flex w-full flex-wrap items-center gap-2 xl:w-auto xl:justify-end">
<UButton <UButton
v-if="presets.length > 0" v-if="presets.length > 0"
color="neutral" color="neutral"
@ -83,7 +79,7 @@
<div <div
v-if="!isLoading && filteredPresets.length > 0" v-if="!isLoading && filteredPresets.length > 0"
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-default bg-default px-3 py-3" class="flex flex-wrap items-center justify-between gap-3 ytp-card px-3 py-3"
> >
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<UButton <UButton
@ -128,11 +124,11 @@
<div <div
v-if="contentStyle === 'list' && filteredPresets.length > 0" v-if="contentStyle === 'list' && filteredPresets.length > 0"
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default" class="w-full min-w-0 max-w-full overflow-hidden ytp-table-surface"
> >
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain"> <div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
<table class="min-w-200 w-full text-sm"> <table class="min-w-200 w-full text-sm">
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned"> <thead class="bg-elevated/60 text-xs uppercase tracking-wide text-toned">
<tr <tr
class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0" class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
> >
@ -236,16 +232,8 @@
<div v-else-if="filteredPresets.length > 0" class="grid gap-4 md:grid-cols-2 xl:grid-cols-3"> <div v-else-if="filteredPresets.length > 0" class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
<div v-for="item in filteredPresets" :key="item.id" class="min-w-0 w-full max-w-full"> <div v-for="item in filteredPresets" :key="item.id" class="min-w-0 w-full max-w-full">
<UCard <div class="ytp-card flex h-full min-w-0 w-full max-w-full flex-col overflow-hidden">
class="flex h-full min-w-0 w-full max-w-full flex-col" <div class="p-4 pb-3 ytp-border-bottom-soft">
:ui="{
root: 'bg-default border border-default',
header: 'p-4 pb-3',
body: 'flex flex-1 flex-col gap-4 p-4 pt-0',
footer: 'border-t border-default px-4 py-4',
}"
>
<template #header>
<div class="flex min-w-0 items-start justify-between gap-3"> <div class="flex min-w-0 items-start justify-between gap-3">
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
@ -283,8 +271,9 @@
</label> </label>
</div> </div>
</div> </div>
</template> </div>
<div class="flex flex-1 flex-col gap-4 p-4 pt-0">
<div class="space-y-2 text-sm text-default"> <div class="space-y-2 text-sm text-default">
<div class="flex flex-wrap gap-2 text-xs text-toned *:min-w-32 *:flex-1"> <div class="flex flex-wrap gap-2 text-xs text-toned *:min-w-32 *:flex-1">
<span <span
@ -367,8 +356,9 @@
</div> </div>
</button> </button>
</div> </div>
</div>
<template #footer> <div class="ytp-border-top-soft px-4 py-4">
<div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1"> <div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1">
<UButton <UButton
color="neutral" color="neutral"
@ -390,8 +380,8 @@
Delete Delete
</UButton> </UButton>
</div> </div>
</template> </div>
</UCard> </div>
</div> </div>
</div> </div>
@ -540,7 +530,6 @@ const bulkActionGroups = computed<DropdownMenuItem[][]>(() => [
{ {
label: 'Remove Selected', label: 'Remove Selected',
icon: 'i-lucide-trash', icon: 'i-lucide-trash',
color: 'error',
disabled: !hasSelected.value || massDelete.value, disabled: !hasSelected.value || massDelete.value,
onSelect: () => void deleteSelected(), onSelect: () => void deleteSelected(),
}, },

View file

@ -1,6 +1,7 @@
<template> <template>
<div>
<AppRoot mode="simple" @ready="init" v-slot="{ openSettings }"> <AppRoot mode="simple" @ready="init" v-slot="{ openSettings }">
<div class="shell-stage flex min-h-screen flex-col bg-default/95 backdrop-blur-sm"> <div class="shell-stage shell-surface flex min-h-screen flex-col">
<ConnectionBanner /> <ConnectionBanner />
<div <div
@ -14,8 +15,7 @@
<div class="transition-transform duration-300"> <div class="transition-transform duration-300">
<div class="mx-auto w-full transition-all duration-300" :class="formContainerClass"> <div class="mx-auto w-full transition-all duration-300" :class="formContainerClass">
<UPageCard variant="outline" :ui="formCardUi"> <div class="ytp-card p-4 sm:p-5">
<template #body>
<form autocomplete="off" class="space-y-4" @submit.prevent="addDownload"> <form autocomplete="off" class="space-y-4" @submit.prevent="addDownload">
<div class="flex items-start justify-between gap-3"> <div class="flex items-start justify-between gap-3">
<div class="min-w-0 space-y-1"> <div class="min-w-0 space-y-1">
@ -30,7 +30,7 @@
<div class="flex shrink-0 items-center gap-1 sm:gap-2"> <div class="flex shrink-0 items-center gap-1 sm:gap-2">
<UTooltip v-if="!socketStore.isConnected" text="Reload queue"> <UTooltip v-if="!socketStore.isConnected" text="Reload queue">
<UButton <UButton
color="info" color="neutral"
variant="ghost" variant="ghost"
size="sm" size="sm"
icon="i-lucide-refresh-cw" icon="i-lucide-refresh-cw"
@ -64,8 +64,8 @@
<UTooltip :text="showExtras ? 'Hide extra options' : 'Show extra options'"> <UTooltip :text="showExtras ? 'Hide extra options' : 'Show extra options'">
<UButton <UButton
type="button" type="button"
color="info" color="neutral"
variant="outline" :variant="showExtras ? 'soft' : 'outline'"
size="lg" size="lg"
:icon="showExtras ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'" :icon="showExtras ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
class="shrink-0 justify-center w-12" class="shrink-0 justify-center w-12"
@ -99,7 +99,7 @@
</UButton> </UButton>
</div> </div>
<div v-if="showExtras" class="space-y-3 border-t border-default pt-4"> <div v-if="showExtras" class="space-y-3 ytp-border-top-soft pt-4">
<UFormField label="Preset" :ui="fieldUi" class="w-full"> <UFormField label="Preset" :ui="fieldUi" class="w-full">
<template #label> <template #label>
<span class="inline-flex items-center gap-2 font-semibold"> <span class="inline-flex items-center gap-2 font-semibold">
@ -155,8 +155,7 @@
</div> </div>
</div> </div>
</form> </form>
</template> </div>
</UPageCard>
</div> </div>
</div> </div>
@ -203,13 +202,12 @@
tag="div" tag="div"
class="grid grid-cols-1 gap-3 lg:grid-cols-2" class="grid grid-cols-1 gap-3 lg:grid-cols-2"
> >
<UCard <div
v-for="item in queueItems" v-for="item in queueItems"
:key="`queue-${item._id}`" :key="`queue-${item._id}`"
class="w-full min-w-0 max-w-full overflow-hidden" class="ytp-card w-full min-w-0 max-w-full overflow-hidden"
:ui="queueCardUi"
> >
<template #header> <div class="p-4 pb-0 ytp-border-bottom-soft">
<div <div
v-if="downloadingStatuses.has(item.status) || item.status === null" v-if="downloadingStatuses.has(item.status) || item.status === null"
class="queue-progress rounded-md border border-default bg-muted/20" class="queue-progress rounded-md border border-default bg-muted/20"
@ -235,8 +233,9 @@
<span>{{ updateProgress(item) }}</span> <span>{{ updateProgress(item) }}</span>
</div> </div>
</div> </div>
</template> </div>
<div class="p-4">
<div class="flex min-w-0 flex-col gap-4 sm:flex-row"> <div class="flex min-w-0 flex-col gap-4 sm:flex-row">
<figure <figure
class="relative w-full shrink-0 overflow-hidden rounded-lg border border-default bg-muted/20 sm:w-52" class="relative w-full shrink-0 overflow-hidden rounded-lg border border-default bg-muted/20 sm:w-52"
@ -299,15 +298,33 @@
<div class="flex flex-wrap items-center gap-2 text-xs"> <div class="flex flex-wrap items-center gap-2 text-xs">
<UBadge <UBadge
:color="downloadingStatuses.has(item.status) ? 'info' : 'warning'" :color="
downloadingStatuses.has(item.status) ? 'info' : 'warning'
"
variant="soft" variant="soft"
size="sm" size="sm"
class="gap-1"
> >
{{ downloadingStatuses.has(item.status) ? 'Active' : 'Queued' }} <UIcon
:name="getQueueIcon(item)"
:class="['size-3.5', getStatusIconAnimation(item)]"
/>
<span>{{
downloadingStatuses.has(item.status) ? 'Active' : 'Queued'
}}</span>
</UBadge> </UBadge>
<UBadge :color="getStatusColor(item)" variant="soft" size="sm"> <UBadge
{{ getStatusLabel(item) }} :color="getStatusColor(item)"
variant="soft"
size="sm"
class="gap-1"
>
<UIcon
:name="getStatusIcon(item)"
:class="['size-3.5', getStatusIconAnimation(item)]"
/>
<span>{{ getStatusLabel(item) }}</span>
</UBadge> </UBadge>
<span <span
@ -332,10 +349,12 @@
</template> </template>
</p> </p>
<div class="mt-auto flex flex-wrap items-center justify-end gap-2 pt-1"> <div
class="mt-auto flex flex-wrap items-center justify-end gap-2 pt-1"
>
<UButton <UButton
v-if="!item.status && item.auto_start === false" v-if="!item.status && item.auto_start === false"
color="success" color="neutral"
variant="soft" variant="soft"
size="xs" size="xs"
icon="i-lucide-play-circle" icon="i-lucide-play-circle"
@ -346,7 +365,7 @@
<UButton <UButton
v-if="!item.status && item.auto_start === true" v-if="!item.status && item.auto_start === true"
color="warning" color="neutral"
variant="soft" variant="soft"
size="xs" size="xs"
icon="i-lucide-pause" icon="i-lucide-pause"
@ -356,7 +375,7 @@
</UButton> </UButton>
<UButton <UButton
color="error" color="neutral"
variant="outline" variant="outline"
size="xs" size="xs"
icon="i-lucide-x" icon="i-lucide-x"
@ -367,7 +386,8 @@
</div> </div>
</div> </div>
</div> </div>
</UCard> </div>
</div>
</TransitionGroup> </TransitionGroup>
<UAlert <UAlert
@ -443,12 +463,12 @@
v-if="historyEntries.length > 0" v-if="historyEntries.length > 0"
class="grid grid-cols-1 gap-3 lg:grid-cols-2" class="grid grid-cols-1 gap-3 lg:grid-cols-2"
> >
<UCard <div
v-for="item in historyEntries" v-for="item in historyEntries"
:key="`history-${historyPagination.page}-${item._id}`" :key="`history-${historyPagination.page}-${item._id}`"
class="w-full min-w-0 max-w-full overflow-hidden" class="ytp-card w-full min-w-0 max-w-full overflow-hidden"
:ui="queueCardUi"
> >
<div class="p-4">
<div class="flex min-w-0 flex-col gap-4 sm:flex-row"> <div class="flex min-w-0 flex-col gap-4 sm:flex-row">
<figure <figure
class="relative w-full shrink-0 overflow-hidden rounded-lg border border-default bg-muted/20 sm:w-52" class="relative w-full shrink-0 overflow-hidden rounded-lg border border-default bg-muted/20 sm:w-52"
@ -512,8 +532,17 @@
<div class="flex flex-wrap items-center gap-2 text-xs"> <div class="flex flex-wrap items-center gap-2 text-xs">
<UBadge color="neutral" variant="soft" size="sm">History</UBadge> <UBadge color="neutral" variant="soft" size="sm">History</UBadge>
<UBadge :color="getStatusColor(item)" variant="soft" size="sm"> <UBadge
{{ getStatusLabel(item) }} :color="getStatusColor(item)"
variant="soft"
size="sm"
class="gap-1"
>
<UIcon
:name="getStatusIcon(item)"
:class="['size-3.5', getStatusIconAnimation(item)]"
/>
<span>{{ getStatusLabel(item) }}</span>
</UBadge> </UBadge>
<span <span
@ -538,7 +567,9 @@
</template> </template>
</p> </p>
<div class="mt-auto flex flex-wrap items-center justify-end gap-2 pt-1"> <div
class="mt-auto flex flex-wrap items-center justify-end gap-2 pt-1"
>
<UButton <UButton
v-if="getDownloadLink(item)" v-if="getDownloadLink(item)"
color="primary" color="primary"
@ -554,7 +585,7 @@
<UButton <UButton
v-if="!item.filename" v-if="!item.filename"
color="info" color="neutral"
variant="soft" variant="soft"
size="xs" size="xs"
icon="i-lucide-rotate-cw" icon="i-lucide-rotate-cw"
@ -564,7 +595,7 @@
</UButton> </UButton>
<UButton <UButton
color="error" color="neutral"
variant="outline" variant="outline"
size="xs" size="xs"
icon="i-lucide-trash" icon="i-lucide-trash"
@ -575,7 +606,8 @@
</div> </div>
</div> </div>
</div> </div>
</UCard> </div>
</div>
</div> </div>
<UAlert <UAlert
@ -663,6 +695,7 @@
</div> </div>
</div> </div>
</AppRoot> </AppRoot>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -756,19 +789,6 @@ const fieldUi = {
container: 'space-y-2', container: 'space-y-2',
}; };
const formCardUi = {
root: 'w-full border border-default bg-default',
container: 'w-full p-4 sm:p-5',
wrapper: 'w-full items-stretch',
body: 'w-full',
};
const queueCardUi = {
root: 'w-full border border-default bg-default',
header: 'p-4 pb-0',
body: 'p-4',
};
const urlInputUi = { const urlInputUi = {
root: 'w-full', root: 'w-full',
base: 'bg-elevated/60 ring-default focus-visible:ring-primary', base: 'bg-elevated/60 ring-default focus-visible:ring-primary',
@ -1090,6 +1110,57 @@ const getStatusLabel = (item: StoreItem): string => {
return statusOverrides[item.status] ?? ucFirst(item.status.replace(/_/g, ' ')); return statusOverrides[item.status] ?? ucFirst(item.status.replace(/_/g, ' '));
}; };
const getQueueIcon = (item: StoreItem): string => {
if (downloadingStatuses.has(item.status)) {
return item.is_live ? 'i-lucide-globe' : 'i-lucide-download';
}
return 'i-lucide-clock-3';
};
const getStatusIcon = (item: StoreItem): string => {
if (!item.auto_start) {
return 'i-lucide-clock-3';
}
if (item.status === null && paused.value === true) {
return 'i-lucide-circle-pause';
}
if (item.status === null) {
return 'i-lucide-circle-question-mark';
}
if (isDownloadSkipped(item)) {
return 'i-lucide-circle-slash';
}
if (item.status === 'downloading' && item.is_live) {
return 'i-lucide-globe';
}
const map: Record<string, string> = {
downloading: 'i-lucide-download',
postprocessing: 'i-lucide-settings-2',
preparing: 'i-lucide-loader-circle',
finished: 'i-lucide-circle-check',
error: 'i-lucide-triangle-alert',
cancelled: 'i-lucide-circle-x',
not_live: 'i-lucide-clock-3',
skip: 'i-lucide-circle-slash',
};
return map[item.status] ?? 'i-lucide-circle-question-mark';
};
const getStatusIconAnimation = (item: StoreItem): string => {
const icon = getStatusIcon(item);
return ['i-lucide-globe', 'i-lucide-settings-2', 'i-lucide-loader-circle'].includes(icon)
? 'animate-spin'
: '';
};
const getStatusColor = (item: StoreItem): 'neutral' | 'info' | 'success' | 'error' | 'warning' => { const getStatusColor = (item: StoreItem): 'neutral' | 'info' | 'success' | 'error' | 'warning' => {
if (item.status === null) { if (item.status === null) {
return 'neutral'; return 'neutral';

View file

@ -1,17 +1,13 @@
<template> <template>
<main class="w-full min-w-0 max-w-full space-y-6"> <main class="w-full min-w-0 max-w-full space-y-6">
<div class="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between"> <div class="ytp-page-header">
<div class="flex min-w-0 items-center gap-3"> <div class="ytp-page-heading">
<span <span class="ytp-page-icon">
class="inline-flex size-11 shrink-0 items-center justify-center rounded-md border border-default bg-elevated/70 text-primary"
>
<UIcon :name="pageShell.icon" class="size-5" /> <UIcon :name="pageShell.icon" class="size-5" />
</span> </span>
<div class="min-w-0 space-y-2"> <div class="min-w-0 space-y-2">
<div <div class="ytp-page-kicker">
class="flex flex-wrap items-center gap-2 text-xs font-medium uppercase tracking-[0.2em] text-toned"
>
<span>{{ pageShell.sectionLabel }}</span> <span>{{ pageShell.sectionLabel }}</span>
<span>/</span> <span>/</span>
<span>{{ pageShell.pageLabel }}</span> <span>{{ pageShell.pageLabel }}</span>
@ -21,7 +17,7 @@
</div> </div>
</div> </div>
<div class="flex min-w-0 flex-wrap items-center justify-end gap-2 xl:justify-end"> <div class="flex w-full flex-wrap items-center gap-2 xl:w-auto xl:justify-end">
<UButton <UButton
v-if="definitions.length > 0" v-if="definitions.length > 0"
color="neutral" color="neutral"
@ -101,7 +97,7 @@
<div <div
v-if="!isLoading && filteredDefinitions.length > 0" v-if="!isLoading && filteredDefinitions.length > 0"
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-default bg-default px-3 py-3" class="flex flex-wrap items-center justify-between gap-3 ytp-card px-3 py-3"
> >
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<UButton <UButton
@ -134,11 +130,11 @@
<div <div
v-if="contentStyle === 'list' && filteredDefinitions.length > 0" v-if="contentStyle === 'list' && filteredDefinitions.length > 0"
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default" class="w-full min-w-0 max-w-full overflow-hidden ytp-table-surface"
> >
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain"> <div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
<table class="min-w-255 w-full text-sm"> <table class="min-w-255 w-full text-sm">
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned"> <thead class="bg-elevated/60 text-xs uppercase tracking-wide text-toned">
<tr <tr
class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0" class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
> >
@ -266,16 +262,8 @@
:key="definition.id" :key="definition.id"
class="min-w-0 w-full max-w-full" class="min-w-0 w-full max-w-full"
> >
<UCard <div class="ytp-card flex h-full min-w-0 w-full max-w-full flex-col overflow-hidden">
class="flex h-full min-w-0 w-full max-w-full flex-col" <div class="p-4 pb-3 ytp-border-bottom-soft">
:ui="{
root: 'bg-default border border-default',
header: 'p-4 pb-3',
body: 'flex flex-1 flex-col gap-4 p-4 pt-0',
footer: 'border-t border-default px-4 py-4',
}"
>
<template #header>
<div class="flex min-w-0 items-start justify-between gap-3"> <div class="flex min-w-0 items-start justify-between gap-3">
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
@ -313,8 +301,9 @@
</label> </label>
</div> </div>
</div> </div>
</template> </div>
<div class="flex flex-1 flex-col gap-4 p-4 pt-0">
<div class="space-y-2 text-sm text-default"> <div class="space-y-2 text-sm text-default">
<div class="flex flex-wrap gap-2 text-xs text-toned *:min-w-32 *:flex-1"> <div class="flex flex-wrap gap-2 text-xs text-toned *:min-w-32 *:flex-1">
<button <button
@ -352,8 +341,9 @@
</div> </div>
</button> </button>
</div> </div>
</div>
<template #footer> <div class="ytp-border-top-soft px-4 py-4">
<div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1"> <div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1">
<UButton <UButton
color="neutral" color="neutral"
@ -375,8 +365,8 @@
Delete Delete
</UButton> </UButton>
</div> </div>
</template> </div>
</UCard> </div>
</div> </div>
</div> </div>
@ -572,7 +562,6 @@ const bulkActionGroups = computed<DropdownMenuItem[][]>(() => [
{ {
label: 'Remove Selected', label: 'Remove Selected',
icon: 'i-lucide-trash', icon: 'i-lucide-trash',
color: 'error',
disabled: !hasSelected.value || massDelete.value, disabled: !hasSelected.value || massDelete.value,
onSelect: () => void deleteSelected(), onSelect: () => void deleteSelected(),
}, },

View file

@ -1,17 +1,13 @@
<template> <template>
<main class="w-full min-w-0 max-w-full space-y-6"> <main class="w-full min-w-0 max-w-full space-y-6">
<div class="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between"> <div class="ytp-page-header">
<div class="flex min-w-0 items-center gap-3"> <div class="ytp-page-heading">
<span <span class="ytp-page-icon">
class="inline-flex size-11 shrink-0 items-center justify-center rounded-md border border-default bg-elevated/70 text-primary"
>
<UIcon :name="pageShell.icon" class="size-5" /> <UIcon :name="pageShell.icon" class="size-5" />
</span> </span>
<div class="min-w-0 space-y-2"> <div class="min-w-0 space-y-2">
<div <div class="ytp-page-kicker">
class="flex flex-wrap items-center gap-2 text-xs font-medium uppercase tracking-[0.2em] text-toned"
>
<span>{{ pageShell.sectionLabel }}</span> <span>{{ pageShell.sectionLabel }}</span>
<span>/</span> <span>/</span>
<span>{{ pageShell.pageLabel }}</span> <span>{{ pageShell.pageLabel }}</span>
@ -21,7 +17,7 @@
</div> </div>
</div> </div>
<div class="flex min-w-0 flex-wrap items-center justify-end gap-2 xl:justify-end"> <div class="flex w-full flex-wrap items-center gap-2 xl:w-auto xl:justify-end">
<UButton <UButton
v-if="tasks.length > 0" v-if="tasks.length > 0"
color="neutral" color="neutral"
@ -83,7 +79,7 @@
<div <div
v-if="!isLoading && filteredTasks.length > 0" v-if="!isLoading && filteredTasks.length > 0"
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-default bg-default px-3 py-3" class="flex flex-wrap items-center justify-between gap-3 ytp-card px-3 py-3"
> >
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<UButton <UButton
@ -128,11 +124,11 @@
<div <div
v-if="contentStyle === 'list' && filteredTasks.length > 0" v-if="contentStyle === 'list' && filteredTasks.length > 0"
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default" class="w-full min-w-0 max-w-full overflow-hidden ytp-table-surface"
> >
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain"> <div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
<table class="min-w-210 table-fixed w-full text-sm"> <table class="min-w-210 table-fixed w-full text-sm">
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned"> <thead class="bg-elevated/60 text-xs uppercase tracking-wide text-toned">
<tr <tr
class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0" class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
> >
@ -339,16 +335,8 @@
class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3" class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3"
> >
<div v-for="item in filteredTasks" :key="item.id" class="min-w-0 w-full max-w-full"> <div v-for="item in filteredTasks" :key="item.id" class="min-w-0 w-full max-w-full">
<UCard <div class="ytp-card flex h-full min-w-0 w-full max-w-full flex-col overflow-hidden">
class="flex h-full min-w-0 w-full max-w-full flex-col" <div class="p-4 pb-3 ytp-border-bottom-soft">
:ui="{
root: 'bg-default border border-default',
header: 'p-4 pb-3',
body: 'flex flex-1 flex-col gap-4 p-4 pt-0',
footer: 'border-t border-default px-4 py-4',
}"
>
<template #header>
<div class="flex min-w-0 items-start justify-between gap-3"> <div class="flex min-w-0 items-start justify-between gap-3">
<div class="min-w-0 flex-1 space-y-2"> <div class="min-w-0 flex-1 space-y-2">
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
@ -414,8 +402,9 @@
</label> </label>
</div> </div>
</div> </div>
</template> </div>
<div class="flex flex-1 flex-col gap-4 p-4 pt-0">
<div class="space-y-2 text-sm text-default"> <div class="space-y-2 text-sm text-default">
<div class="grid grid-cols-2 gap-2 text-xs text-toned sm:flex sm:flex-wrap"> <div class="grid grid-cols-2 gap-2 text-xs text-toned sm:flex sm:flex-wrap">
<button <button
@ -473,7 +462,11 @@
<div <div
v-if=" v-if="
item.timer || item.folder || item.template || item.cli || willTaskBeProcessed(item) item.timer ||
item.folder ||
item.template ||
item.cli ||
willTaskBeProcessed(item)
" "
class="feature-meta-grid" class="feature-meta-grid"
> >
@ -565,8 +558,9 @@
</button> </button>
</div> </div>
</div> </div>
</div>
<template #footer> <div class="ytp-border-top-soft px-4 py-4">
<div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1"> <div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1">
<UButton <UButton
color="neutral" color="neutral"
@ -600,8 +594,8 @@
</UButton> </UButton>
</UDropdownMenu> </UDropdownMenu>
</div> </div>
</template> </div>
</UCard> </div>
</div> </div>
</div> </div>
@ -853,7 +847,6 @@ const bulkActionGroups = computed<DropdownMenuItem[][]>(() => [
{ {
label: 'Remove Selected', label: 'Remove Selected',
icon: 'i-lucide-trash', icon: 'i-lucide-trash',
color: 'error',
disabled: !hasSelected.value || massDelete.value, disabled: !hasSelected.value || massDelete.value,
onSelect: () => void deleteSelected(), onSelect: () => void deleteSelected(),
}, },

View file

@ -35,10 +35,18 @@ type TerminalSessionsResponse = {
items: Array<TerminalSessionItem>; items: Array<TerminalSessionItem>;
}; };
type YtpPageHeaderProps = {
icon?: string;
sectionLabel?: string;
pageLabel?: string;
contentClass?: string;
};
export { export {
ImportedItem, ImportedItem,
TerminalSessionItem, TerminalSessionItem,
TerminalSessionStatus, TerminalSessionStatus,
TerminalSessionsResponse, TerminalSessionsResponse,
YtpPageHeaderProps,
version_check, version_check,
}; };

View file

@ -214,8 +214,7 @@ const NavItems: Array<NavDefinition> = [
group: 'docs', group: 'docs',
label: 'Changelog', label: 'Changelog',
pageLabel: 'Changelog', pageLabel: 'Changelog',
description: description: 'Latest project changes',
'Latest project changes, loaded remotely when available and falling back to the bundled changelog file.',
icon: 'i-lucide-git-commit-horizontal', icon: 'i-lucide-git-commit-horizontal',
to: '/changelog', to: '/changelog',
matchPath: '/changelog', matchPath: '/changelog',

View file

@ -11,9 +11,9 @@
"typecheck": "nuxt typecheck", "typecheck": "nuxt typecheck",
"format": "oxfmt --write app/", "format": "oxfmt --write app/",
"format:check": "oxfmt --check app/", "format:check": "oxfmt --check app/",
"lint": "eslint .", "lint": "eslint . --no-warn-ignored",
"lint:ci": "eslint . --quiet", "lint:ci": "eslint . --quiet --no-warn-ignored",
"lint:fix": "eslint . --fix", "lint:fix": "eslint . --fix --no-warn-ignored",
"test": "bun test", "test": "bun test",
"test:watch": "bun test --watch", "test:watch": "bun test --watch",
"test:ci": "bun test --ci --dots", "test:ci": "bun test --ci --dots",

224
uv.lock
View file

@ -147,14 +147,14 @@ wheels = [
[[package]] [[package]]
name = "anyio" name = "anyio"
version = "4.13.0" version = "4.14.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "idna" }, { name = "idna" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } sdist = { url = "https://files.pythonhosted.org/packages/1c/b5/001890774a9552aff22502b8da382593109ce0c95314abaebbb116567545/anyio-4.14.0.tar.gz", hash = "sha256:b47c1f9ccf73e67021df785332508f99379c68fa7d0684e8e3492cb1d4b23f89", size = 253586, upload-time = "2026-06-15T22:00:49.021Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, { url = "https://files.pythonhosted.org/packages/ba/16/9826f089383c593cdfc4a6e5aca94d9e91ae1692c57af82c3b2aa5e810f7/anyio-4.14.0-py3-none-any.whl", hash = "sha256:dd9b7a2a9799ed6552fde617b2c5df02b7fdd7d88392fc48101e51bae46164d9", size = 123506, upload-time = "2026-06-15T22:00:47.595Z" },
] ]
[[package]] [[package]]
@ -253,11 +253,11 @@ wheels = [
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2026.5.20" version = "2026.6.17"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" },
] ]
[[package]] [[package]]
@ -437,7 +437,7 @@ wheels = [
[[package]] [[package]]
name = "dateparser" name = "dateparser"
version = "1.4.0" version = "1.4.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "python-dateutil" }, { name = "python-dateutil" },
@ -445,9 +445,9 @@ dependencies = [
{ name = "regex" }, { name = "regex" },
{ name = "tzlocal" }, { name = "tzlocal" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/46/2d/a0ccdb78788064fa0dc901b8524e50615c42be1d78b78d646d0b28d09180/dateparser-1.4.0.tar.gz", hash = "sha256:97a21840d5ecdf7630c584f673338a5afac5dfe84f647baf4d7e8df98f9354a4", size = 321512, upload-time = "2026-03-26T09:56:10.292Z" } sdist = { url = "https://files.pythonhosted.org/packages/d3/f4/561c49bca97af561d34eed27e3e831135eb5cb88e754c1150be41820f5c6/dateparser-1.4.1.tar.gz", hash = "sha256:f265df13c0380e2e07543ba74b67c0681aaa1096981ffcd35227e1aa0cb81c7c", size = 314734, upload-time = "2026-06-15T08:45:47.659Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/b4/0b/3c3bb7cbe757279e693a0be6049048012f794d01f81099609ecd53b899f0/dateparser-1.4.0-py3-none-any.whl", hash = "sha256:7902b8e85d603494bf70a5a0b1decdddb2270b9c6e6b2bc8a57b93476c0df378", size = 300379, upload-time = "2026-03-26T09:56:08.409Z" }, { url = "https://files.pythonhosted.org/packages/8b/7c/2e5dcf53909deddd0bf38cbe277ad9806be038276b1c6c436561b4d9b2e2/dateparser-1.4.1-py3-none-any.whl", hash = "sha256:f25d4e051a84be27a35bd297e3e1dc59ff78373701b89be352ba80372d22d0d0", size = 300503, upload-time = "2026-06-15T08:45:45.951Z" },
] ]
[[package]] [[package]]
@ -551,59 +551,59 @@ wheels = [
[[package]] [[package]]
name = "greenlet" name = "greenlet"
version = "3.5.1" version = "3.5.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6d/6e/802acd792aebb2256fbbee8cacf2727faaeb6f240ac11008f09eae4414bc/greenlet-3.5.1.tar.gz", hash = "sha256:5a56aeb7d5d9cc4b3a735efb5095bd4b4f6f0e4f93e5ca876d0e2315137b7829", size = 197356, upload-time = "2026-05-20T15:05:03.917Z" } sdist = { url = "https://files.pythonhosted.org/packages/dd/8b/befc3cb36965f397d87e86fb3b00e3ec0dc67c1ecb0986d7f54ee528f018/greenlet-3.5.2.tar.gz", hash = "sha256:c1b906220d83c140361cdd12eef970fb5881a168b98ee58a43786426173da14c", size = 199243, upload-time = "2026-06-17T20:19:01.317Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/27/69/7f7e5372d998b81001899b1c0823c957aa413ba0f2662e65821611cc31e4/greenlet-3.5.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:51518ff74664078fc51bffcc6fc529b0df5ae58da192691cee765d45ce944a2b", size = 285060, upload-time = "2026-05-20T13:08:51.899Z" }, { url = "https://files.pythonhosted.org/packages/d0/3c/bb37b9d40d65b0741a8b040ca5c307034d0a9822994dff5f825c88dd7a6b/greenlet-3.5.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:0629377725977252159de1ebd3c6e49c170a63856e585446797bb3d66d4d9c34", size = 287178, upload-time = "2026-06-17T17:35:25.132Z" },
{ url = "https://files.pythonhosted.org/packages/b1/bf/387f9b6b865fd2ae0d0be09e0004827295a01b71be76ed350dd1e28a91a4/greenlet-3.5.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ffdb3c0bb002c99cd8f298957e046c3dbf6006b5b7cdf11a4e19194624a0a0a", size = 604370, upload-time = "2026-05-20T14:00:07.492Z" }, { url = "https://files.pythonhosted.org/packages/f0/a6/0c5902393f492f8ceb19d0b5cf139284e3a11b333a049739643b1036b6f8/greenlet-3.5.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2ddf9eddc617681108dd071b3feabf3f4a4cd64846254aec4d4ceda098b639a", size = 606900, upload-time = "2026-06-17T18:07:21.692Z" },
{ url = "https://files.pythonhosted.org/packages/32/f5/169ce3d4e4c67291bd18f8cbe0299c9f3e45102c7f1fb3c14780c93e4532/greenlet-3.5.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7715a5a2c3378ba602c3a440558261e13a820bb53a82693aacd7b7f6d964e283", size = 616987, upload-time = "2026-05-20T14:05:44.237Z" }, { url = "https://files.pythonhosted.org/packages/d8/7c/42899c31d4b87148ae4e3f87f63e13398824be6241f4dde42ded95768a34/greenlet-3.5.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f41feb9f2b59e2e61ac9bea4e344ddd9396bf3cacb2583f73a3595ed7df6f8e7", size = 619265, upload-time = "2026-06-17T18:29:44.837Z" },
{ url = "https://files.pythonhosted.org/packages/19/ba/c24110c55dffa55aa6e1d98b45310da33801aeba7686ff0190fe5d46fd32/greenlet-3.5.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d40a890035c0058cadbdc4af7569800fd28a0e527a0fdbb7b5f9418f176846ce", size = 622911, upload-time = "2026-05-20T14:09:10.598Z" }, { url = "https://files.pythonhosted.org/packages/6a/7e/28f991affb413b232b1e7d768db24c37b3f4d5daecc3f19b455d40bd2dea/greenlet-3.5.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9dc23f0e5ad76415457212a4b947d22ebe4dc80baf02adf7dd5647a90f38bb4e", size = 625044, upload-time = "2026-06-17T18:39:29.046Z" },
{ url = "https://files.pythonhosted.org/packages/ee/e5/7f2e41d5273be07e77560d61ea4e56485b4d6c316d2a84518c62d1364061/greenlet-3.5.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc71ff466927a201b08305acac451ebe1aedfcea002f62f1f2f2ac2ac1e6a135", size = 613911, upload-time = "2026-05-20T13:14:27.539Z" }, { url = "https://files.pythonhosted.org/packages/d3/52/4ff8c98d3cfe62b4515f8584ae14510a58f35c549cc5292b78d9b7a40b70/greenlet-3.5.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09201fa698768db245920b00fdc86ee3e73540f01ca6db162be9632642e1a473", size = 616187, upload-time = "2026-06-17T17:39:29.473Z" },
{ url = "https://files.pythonhosted.org/packages/ec/7b/d20db2e8a5ad6c038702f3179b136f93f0a3d1a21a0c0777f3e470cdf4b2/greenlet-3.5.1-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:67821bb03e4e98664490edb787ff6af501194c29bbee0f5c1dfdcf1dc3d9d436", size = 425228, upload-time = "2026-05-20T14:01:40.837Z" }, { url = "https://files.pythonhosted.org/packages/29/05/0cc9ec660e7acff85f93b0a048b6654371c822c884add44c02a465cf70e0/greenlet-3.5.2-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:423167363c510a75b649f5cd58d873c29498ea03598b9e4b1c3b73e0f899f3d5", size = 427322, upload-time = "2026-06-17T18:41:20.892Z" },
{ url = "https://files.pythonhosted.org/packages/c5/a4/fbdc67579b73615a1f91615e814303cc71e06128f7baaba87be79b8fb90c/greenlet-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cd443683db272ebaaca03af98c0b063ab30db70ea8a31a1559f35e3f7b744ccd", size = 1570689, upload-time = "2026-05-20T14:02:27.225Z" }, { url = "https://files.pythonhosted.org/packages/c9/a6/269c8bf9aefc13361ce1088f0e392b154cb21005de7862e42b5d782b81fd/greenlet-3.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a1759fa4f14c398508cf20dc8037de55cc23ae8bd14c185c2718257837195ca5", size = 1573778, upload-time = "2026-06-17T18:22:13.497Z" },
{ url = "https://files.pythonhosted.org/packages/e6/b4/77abbe35078be39718a46cd49caf16bceb35662f97a34101dca28aa98e47/greenlet-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:089fff7a6ce8d9316d1f65ebc00273a56be258c1725b32b94de90a3a979557e1", size = 1635602, upload-time = "2026-05-20T13:14:36.344Z" }, { url = "https://files.pythonhosted.org/packages/1f/9b/391d015cbc6323e81b14c02cf825fdca7e0049c9bb489bf4ac72883118ba/greenlet-3.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9318cdeb9abdbfdd8bc8464ee4a06dffde2c7846e1def138365a6240ab2c9a5", size = 1638092, upload-time = "2026-06-17T17:40:08.163Z" },
{ url = "https://files.pythonhosted.org/packages/37/f7/129f27ca700845b8ee8ca88ce7f43435a1239c2eddb7677fc938822762cf/greenlet-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:110a1ca7b49b014b097f6078272c3f4ed31af45b254de5228b79adba879f6af9", size = 238683, upload-time = "2026-05-20T13:11:50.57Z" }, { url = "https://files.pythonhosted.org/packages/49/53/5b4df711f4356c62e85d9f819d87966d526d1cfb32bae49a8f7d6fc36ea4/greenlet-3.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:2c3b3311af72b3d3b03cc0f1ffd11f072e834be5d0444105cf715fc44434e39c", size = 239352, upload-time = "2026-06-17T17:38:51.593Z" },
{ url = "https://files.pythonhosted.org/packages/6d/5c/a485a36e87df8d8fd0632ee01511244f5156a20ed3746cc6599340326395/greenlet-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f16ba1efc0715b680a18b8123d90dad887c6112ae3555b4b5c32c149540c6b4e", size = 235499, upload-time = "2026-05-20T13:12:42.028Z" }, { url = "https://files.pythonhosted.org/packages/bb/b6/18efc3a329ec035c3f344b8f2b60356451950ddf9b7b64ff00023778a1dd/greenlet-3.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:f9bbd6216c45a563c2a61e478e038b439d9f248bde44f775ea37d339da643af4", size = 237635, upload-time = "2026-06-17T17:35:36.632Z" },
{ url = "https://files.pythonhosted.org/packages/8a/cb/c62454606daf5640369c94d8a9dd540599b1bfc090e2d2180cb77f4038d2/greenlet-3.5.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8ab31c9de8651a2facdd5c5bb0011f2380dd1a7af78ce2adf4b56095294fc07", size = 285579, upload-time = "2026-05-20T13:08:56.396Z" }, { url = "https://files.pythonhosted.org/packages/c7/89/aaafc8e14de4ac882e02ccb963225329b0e8578aba4365e71eb678e45722/greenlet-3.5.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:1c31219badba285858ba8ed117f403dea7fafee6bade9a1991875aae530c3ceb", size = 287676, upload-time = "2026-06-17T17:33:31.514Z" },
{ url = "https://files.pythonhosted.org/packages/ec/71/c4270398c2eba968a6071af1dfbdcaeee6ec1c24bc8b435b8cc452700da6/greenlet-3.5.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e300185139abc337ade480c327183adf42a875ac7181bfe66d7d4efea31fbea", size = 651106, upload-time = "2026-05-20T14:00:09.448Z" }, { url = "https://files.pythonhosted.org/packages/b8/fc/2308249206c12ac70de7b9a00970f84f07d10b3cd60e05d2fbcaa84124e8/greenlet-3.5.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f96ed6f4adc1066954ae95f45717657cb67468ef3b89e9a3632e14a625a8f39", size = 653552, upload-time = "2026-06-17T18:07:23.493Z" },
{ url = "https://files.pythonhosted.org/packages/1a/ab/71e34b78a44ec271fb5f550c17bc46d301ddc5953890d935f270b0dcdb5a/greenlet-3.5.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7ffdb990dcaa0234cf9845aead5df2e3c3a8b6507d409274dd87e0d5ab05ffc2", size = 663478, upload-time = "2026-05-20T14:05:45.88Z" }, { url = "https://files.pythonhosted.org/packages/7c/24/47730d1f8f1336b9b089237521ed7a26eee997065dcb4cab81cdca333abc/greenlet-3.5.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5795e883e915333c0d5648faaa691857fbc7180136883edc377f50f0d509c2a8", size = 665756, upload-time = "2026-06-17T18:29:46.616Z" },
{ url = "https://files.pythonhosted.org/packages/c6/2d/2d80842910da44f78c286532d084b8a5c3717c844ae80ceb3858738ae89a/greenlet-3.5.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c09df69dc1712d131332054a858a3e5cca400967fa3a672e2324fbb0971448c", size = 667767, upload-time = "2026-05-20T14:09:12.15Z" }, { url = "https://files.pythonhosted.org/packages/23/5c/2664d290cbd1fef9eb3f69b5d3bc5aa91b6fa907519298ca6af93a90c6cb/greenlet-3.5.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6e9e49d732ee92a189bb7035e293029244aeba648297a9b856dc733d17ca7f0d", size = 669989, upload-time = "2026-06-17T18:39:30.79Z" },
{ url = "https://files.pythonhosted.org/packages/77/96/4efd6fa5c62c85426a0c19077a586258ebc3a2a146ff2493e4312a697a22/greenlet-3.5.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f82b3597e9d83b63408affed0b48fd0f54935edac4302237b9a837be0dae33c", size = 660800, upload-time = "2026-05-20T13:14:29.129Z" }, { url = "https://files.pythonhosted.org/packages/99/69/d6c99db15dc0b5e892ac3cc7b942c8b21f4a9cc3bd9ea0bc3b0f339ffbd4/greenlet-3.5.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26aed8d9503ca78889141a9739d71b383efea5f472a7c522b5410f7eb2a1b163", size = 663228, upload-time = "2026-06-17T17:39:31.073Z" },
{ url = "https://files.pythonhosted.org/packages/e9/d3/dad2eecedfbb1ed7050a20dcfae40c1442b74bc7423608be2c7e03ee7133/greenlet-3.5.1-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:a4764e0bfc6a4d114c865b32520805c16a990ef5f286a514413b05d5ecd6a23d", size = 470786, upload-time = "2026-05-20T14:01:42.064Z" }, { url = "https://files.pythonhosted.org/packages/42/d4/fcb53fa9847d7fbd4723fbed9469c3869b9e3544c4e001d9d5aa2f66162d/greenlet-3.5.2-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:537c5c4f30395020bb9f48f53146070e3b997c3c75da14011ab732aaa19ce3ef", size = 472888, upload-time = "2026-06-17T18:41:22.511Z" },
{ url = "https://files.pythonhosted.org/packages/7a/e0/6c71401a25cac7000261304e866a2f2cc04dc74810d40e2f118aa4799495/greenlet-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c0141e37414c10164e702b8fb1473304221ad98f71600850c6ef7ff4880feba0", size = 1617518, upload-time = "2026-05-20T14:02:28.662Z" }, { url = "https://files.pythonhosted.org/packages/4f/88/9e603f448e2bc107c883e95817b980fb9b45ba6aea0299b2e9978124bea2/greenlet-3.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dbebc038fcdda8f8f21cce985fd04e34e0f42007e7fc7ab7ad285caf77974b95", size = 1620723, upload-time = "2026-06-17T18:22:14.817Z" },
{ url = "https://files.pythonhosted.org/packages/41/26/c5c06643e8c0af9e7bf18e16cb51d0ab7625155f0392e1c9015d66d556cd/greenlet-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:50ae25a67bea74ea41fb14b960bc532df73eb713417b2d61892dced82fe8d3bc", size = 1681593, upload-time = "2026-05-20T13:14:39.417Z" }, { url = "https://files.pythonhosted.org/packages/11/91/26da17e3777858c16fdb8d020a4c68f3a03cb92f238de8f5351d5d5186e9/greenlet-3.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a207023f1cf8695fd82580b8099c09c5809be18bc2282362cdfb965dd884a317", size = 1684227, upload-time = "2026-06-17T17:40:09.536Z" },
{ url = "https://files.pythonhosted.org/packages/8a/bd/e11a108317485075e68af9d23039619b86b28130c3b50d227d42edece64b/greenlet-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:8a17c42330e261299766b75ac1ea32caa437a9453c8f65d16a13140db378ecd3", size = 239800, upload-time = "2026-05-20T13:09:30.128Z" }, { url = "https://files.pythonhosted.org/packages/2d/44/b3a11f7aa34cb38f1b7f3df8bcd9fcd09bac9d342c2a2c9b8686c804bcd2/greenlet-3.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:c674a1dd4fe41f6a93febe7ab366ceabf15080ea31a9307811c56dac5f435f73", size = 240257, upload-time = "2026-06-17T17:35:23.359Z" },
{ url = "https://files.pythonhosted.org/packages/47/f8/8e8e8417b7bf28639a5a56356ef934d0375e1d0c70a57e04d7701e870ffe/greenlet-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:7b5f5fae05b8ac6d176a61b60c394a8cbdc2b5b91b81793066e68745cf165e54", size = 236862, upload-time = "2026-05-20T13:09:10.498Z" }, { url = "https://files.pythonhosted.org/packages/de/e3/3b62145fe917311732041a258adb218248add00542e3131c48bd047fbed5/greenlet-3.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:3c417cd6c593bbbef6f7aa31a79f37d3db7d18832fc56b694a2150130bde784e", size = 239038, upload-time = "2026-06-17T17:37:56.792Z" },
{ url = "https://files.pythonhosted.org/packages/90/12/41bf27fde4d3605d3773ae57751eda182b8be2f5398011c041173b1d9534/greenlet-3.5.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:ea8da1e900d758d078810d4255d8c6aa572181896a31ec79d779eb79c3adc9ad", size = 293637, upload-time = "2026-05-20T13:12:35.529Z" }, { url = "https://files.pythonhosted.org/packages/47/ac/d3bad483e9f6cd1848604fdffa32cac25846dd6dfcec0e6f81c790185518/greenlet-3.5.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:a96457a30384de52d9c5d2fd33abf6c1daae3db392cd556738f408b1a79a1cf0", size = 295668, upload-time = "2026-06-17T17:36:02.293Z" },
{ url = "https://files.pythonhosted.org/packages/44/44/ba14b23e9757707050c2f397d305bbcae62e5d7cad122f8b6baec5ae4a1f/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a19570c52a21420dcbc94e661994bc325c0b5b11304540fed514586da5dc8f2e", size = 650840, upload-time = "2026-05-20T14:00:11.079Z" }, { url = "https://files.pythonhosted.org/packages/00/e9/3a7e557b895fd0469b00cd0b2bd498ba950e8bfdf6d7adeecf2c5e4130a6/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4af5d4961818ab651d09c1448a03b1ba2a1726a076266ebb62330bab9f3238c", size = 652820, upload-time = "2026-06-17T18:07:24.95Z" },
{ url = "https://files.pythonhosted.org/packages/a8/37/5ddc2b686a6844f91abecef43411842426da2e1573f60b49ecf2547f4ae1/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3d955c89b75eeca4723d7cc14135f393cd47c32e2a6cb4a8e4c6e760a26b0986", size = 656416, upload-time = "2026-05-20T14:05:47.118Z" }, { url = "https://files.pythonhosted.org/packages/78/67/6225d5c5e4afc04be0fd161eec82e4b72017e8a100d222f25d7b42b0140d/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a1789a6244ea1ba61fd4386c9a6a31873e9b0234762103364be98ef87dcb19f3", size = 658697, upload-time = "2026-06-17T18:29:48.365Z" },
{ url = "https://files.pythonhosted.org/packages/8c/46/5987dcd1a2570ba84f3b187536b2ca3ae97613387e57f5cfa99df068fe5e/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea37d5a157eb9493820d3792ac4ece28619a394391d2b9f2f78057d396ff0f0f", size = 656607, upload-time = "2026-05-20T14:09:13.949Z" }, { url = "https://files.pythonhosted.org/packages/35/ad/9b3058f999b81750a9c6d9ec424f509462d232b58002086fe2ba63b66407/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ee6288f1933d698b4f098127ed17bda2910a75d2807915bd16294a972055d6c", size = 658945, upload-time = "2026-06-17T18:39:32.509Z" },
{ url = "https://files.pythonhosted.org/packages/e1/f0/d17510297c35a2992712f0bf84de3779749999f7d3d63aa1f09db7c62dbe/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2daaaebd1a5aa88c49045b6baf9310b3263796bd88db713edf37cf53e7bb4e", size = 654397, upload-time = "2026-05-20T13:14:30.696Z" }, { url = "https://files.pythonhosted.org/packages/fa/99/6324b8ef916dcaddccb340b304c992ca3f947614ce0f2685d438187300b8/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3be00501fb4a8c37f6b4b3c4773808ceb26ea65c7ea64fd5735d0f330b3786de", size = 656436, upload-time = "2026-06-17T17:39:32.509Z" },
{ url = "https://files.pythonhosted.org/packages/2c/c1/6da0a9ddcc29d7e51ef14883fa3dc1e53b3f4ffba00582106c7bf55da1d8/greenlet-3.5.1-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:8d8a23250ea3ec7b36de8fa4b541e9e2db3ee82915cc060ab0631609ad8b28de", size = 488287, upload-time = "2026-05-20T14:01:43.143Z" }, { url = "https://files.pythonhosted.org/packages/92/75/1b6ecd8c027b69ab1b6798a84094df79aab5e69ac7e249c78b9d361dd1fa/greenlet-3.5.2-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:b4cad42662c796334c2d24607c411e3ed82481c1fb4e1e8ec3a5a8416060092e", size = 490529, upload-time = "2026-06-17T18:41:23.954Z" },
{ url = "https://files.pythonhosted.org/packages/37/eb/147387705bb89092645b012586e7273cb5ed3c90ef7eaf3a69173eaf0209/greenlet-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bfbd69cc349e43bf3a8ae1c85548ff0718efc887615c2db16c3833d7b0b072d", size = 1614469, upload-time = "2026-05-20T14:02:30.192Z" }, { url = "https://files.pythonhosted.org/packages/a9/ee/f5bf9daac27c5e1b011965f64b5630a32b415daf7381b312943629e12c2a/greenlet-3.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1d554cd96841a68d464d75a3736f8e87408a7b02b1930a75fa32feb408ad62f8", size = 1617193, upload-time = "2026-06-17T18:22:16.252Z" },
{ url = "https://files.pythonhosted.org/packages/a6/4e/37ee0da7732b7aa9896f17e15579a9df34b9fcb9dd494f0adfa749af6623/greenlet-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4378720dd888136c27215a0214d32a4d37c3852765d45bc37aad0623423cfd78", size = 1675115, upload-time = "2026-05-20T13:14:40.972Z" }, { url = "https://files.pythonhosted.org/packages/8a/21/b05d5b12715bda92ce27c118d64971d21e9b8f3563ed959a7d271e2d4223/greenlet-3.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3dff6cd3aac35f6cd3fc23460105acf576f5faf6c378de0bc088bf37c913864a", size = 1677512, upload-time = "2026-06-17T17:40:10.771Z" },
{ url = "https://files.pythonhosted.org/packages/57/f3/97dfcf4a6eb5077f8a672234216fb5923eb89f2cab7081cb10b2cf75b605/greenlet-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:45718441607f9325d948db98cbc691276059316d0358c188c246da4e1d4d23d2", size = 245246, upload-time = "2026-05-20T13:12:22.646Z" }, { url = "https://files.pythonhosted.org/packages/b8/97/1b8f1314b868041b327dc1051603e8142b826480cb0ecb8a7b7632aee9c4/greenlet-3.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:36cfea2aa075d544617176b2e84450480f0797070ad8799a8c41ada2fe449d32", size = 243145, upload-time = "2026-06-17T17:34:37.502Z" },
{ url = "https://files.pythonhosted.org/packages/5d/73/d7f72e34b582f694f4a9b248162db7b09cc458a259ba8f0c0bfa1a34ea7d/greenlet-3.5.1-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:2baee5ca02031757ffe8cc3d69f0cc0aec7065ce362622da74f32d3bcab1c541", size = 285575, upload-time = "2026-05-20T13:12:07.043Z" }, { url = "https://files.pythonhosted.org/packages/36/07/1b5311775e04c718a118c504d7a3a312430e2a1bd1347226aff4774e4549/greenlet-3.5.2-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:a0314aa832c94633355dc6f3ee54f195159533355a323f26926fc63b98b2ccbb", size = 288315, upload-time = "2026-06-17T17:34:34.04Z" },
{ url = "https://files.pythonhosted.org/packages/df/59/fa9c6e87dc8ad27a95dabe2f29f372b733d05a8a67470f6c901ed9975655/greenlet-3.5.1-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b1ec3274918a81d3ea778b9e75b56b72b33f300edb6cf7f3a7fe1dae56683de", size = 656428, upload-time = "2026-05-20T14:00:12.556Z" }, { url = "https://files.pythonhosted.org/packages/ed/cc/6abcd2a486b58b9f77b7a93b690d59cb2c11a5906ed2ad4c63c7b9c1113d/greenlet-3.5.2-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24c59cb7db9d5c694cb8fd0c76eef8e456b2123afdfa7e4b8f2a67a0860d7682", size = 659130, upload-time = "2026-06-17T18:07:26.354Z" },
{ url = "https://files.pythonhosted.org/packages/f6/f9/e753408871eaa61dfe35e619cfc67512b036fde99893685d50eea9e07146/greenlet-3.5.1-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:111e2390ffffc47d5840b01711dd7fac07d4c09283d0283e7f3264b14e284c64", size = 667064, upload-time = "2026-05-20T14:05:48.662Z" }, { url = "https://files.pythonhosted.org/packages/f2/12/f4aaad6d3d383233f700ab322568a4f29f2c701a4861d85f4811d99689b2/greenlet-3.5.2-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7bb811753703739ad318112f16eccfaabdac050037b6d092debaa8b23566b4ce", size = 669724, upload-time = "2026-06-17T18:29:50.13Z" },
{ url = "https://files.pythonhosted.org/packages/dc/74/807a047255bf1e09303627c46dc043dca596b6958a354d904f32ab382005/greenlet-3.5.1-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:10a9a1c0bfbc93d41156ffcb90c75fbc05544054faf15dcc1fdf9765f8b607f0", size = 672962, upload-time = "2026-05-20T14:09:15.532Z" }, { url = "https://files.pythonhosted.org/packages/53/e0/4ce3a046b51e53934eae93d7f9c13975a97285741e9e1fcadf8751314c37/greenlet-3.5.2-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2debcd0ef9455b7d4879589903efc8e497d4b8fb8c0ae772309e44d1ca5e957f", size = 673494, upload-time = "2026-06-17T18:39:34.196Z" },
{ url = "https://files.pythonhosted.org/packages/96/27/5565b5b40389f1c7753003a07e21892fda8660926787036d5bc0308b8113/greenlet-3.5.1-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e630136e905fe5ff43e86945ae41220b6d1470956a39220e708110ac48d01ea5", size = 665697, upload-time = "2026-05-20T13:14:32.943Z" }, { url = "https://files.pythonhosted.org/packages/91/2a/a089811fc31c6bf8742f40a4e73470d6d401cef18e4314eb20dc399b377c/greenlet-3.5.2-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6d78b5c1c178dad90447f1b8452262709d3eef4c98f825569e74c9d0b2260ac9", size = 668089, upload-time = "2026-06-17T17:39:33.808Z" },
{ url = "https://files.pythonhosted.org/packages/76/32/19d4e13225193c29b13e308015223f7d75fd3d8623d49dd19040d2ce8ec1/greenlet-3.5.1-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:ef08c1567c78074b22d1a200183d52d04a14df447bf70bcbb6a3507a48e776fc", size = 476047, upload-time = "2026-05-20T14:01:44.39Z" }, { url = "https://files.pythonhosted.org/packages/52/e0/9c18721e63445dce02ee67e4c81c0f281626604ff55ae6f7b7f4354d7129/greenlet-3.5.2-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:9558cae989faeab6fbb425cd98a0cfa4190a47fba6443973fbee0a1eb0b0b6c3", size = 479721, upload-time = "2026-06-17T18:41:25.726Z" },
{ url = "https://files.pythonhosted.org/packages/cf/82/e7de4178c0c2d1c9a5a3be3cc0b33e46a85b3ee4a77c071bf7ad8600e079/greenlet-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:975eac34b44a7077ca4d421348455b94f0f518246a7f14bc6d2fdcfe5b584368", size = 1621256, upload-time = "2026-05-20T14:02:31.91Z" }, { url = "https://files.pythonhosted.org/packages/0f/1c/2f47c7d5fcfa98a62b705bf9a0505d86f4563c0d81cab1f7159ff1e743b7/greenlet-3.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:0977af2df83136f81c1f76e76d4e2fe7d0dc56ea9c101a86af26a95190b9ca32", size = 1625684, upload-time = "2026-06-17T18:22:17.664Z" },
{ url = "https://files.pythonhosted.org/packages/00/10/f2dddcf7dacac17dfc68691809589adad06135eb28930429cf58a6467a2f/greenlet-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:9ab3c3a0b2ae6198e67c898dad5215a49f9ae0d0081b3c3ec59f333e39eeca26", size = 1685956, upload-time = "2026-05-20T13:14:42.55Z" }, { url = "https://files.pythonhosted.org/packages/b9/bf/661dd24624f70b7b32972d7693d0344ecde10278f647d7b828baf739899c/greenlet-3.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f9ed777c6891d8253e54468576f55e27f8fc1a662a664f946a191003574c0a74", size = 1688043, upload-time = "2026-06-17T17:40:12.403Z" },
{ url = "https://files.pythonhosted.org/packages/22/17/4a232b32133230ada52f70e9d7f5b65b0caef8772f01849bd8d149e7e4ca/greenlet-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:cbfc69be86e10dcfef5b1e6269d1d6926552aa89ee39e1de3353360c1b6989ab", size = 239802, upload-time = "2026-05-20T13:13:15.481Z" }, { url = "https://files.pythonhosted.org/packages/60/49/d9bde1d15a21296b3b521fe083eb8aabd54ac05d15de9832918f3d639543/greenlet-3.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:c0ea4eb3de23f0bac1d75205e10ccfa9b418b17b01a2d7bf19e3b69dda08900a", size = 240531, upload-time = "2026-06-17T17:35:47.448Z" },
{ url = "https://files.pythonhosted.org/packages/c2/ae/4e623a7e6d4d2a5f4cb8e4c82de4169fc637942caae68d6e676b8a128ac5/greenlet-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:92fd6d44ac5e5a887c8a5dc4a8ba0ba908527c31c12f78c6bc7dcfe8aab279f6", size = 236853, upload-time = "2026-05-20T13:15:37.301Z" }, { url = "https://files.pythonhosted.org/packages/7f/4d/86d7768bd53e9907de0333df215c2018cd01a593b3715cbd79aa82dd94b7/greenlet-3.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:7a7bfc200be40d04961d7e80e8337d726c0c1a50777e588123c3ed8ba731dcb9", size = 239579, upload-time = "2026-06-17T17:39:39.954Z" },
{ url = "https://files.pythonhosted.org/packages/7a/57/816d9cff29119da3505b3d6a5e14a8af89006ac36f47f891ff293ee05af1/greenlet-3.5.1-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:a6fdf2433a5441ef9a95464f7c3e674775da1c8c1177fff311cee1acad4626ed", size = 293877, upload-time = "2026-05-20T13:10:19.078Z" }, { url = "https://files.pythonhosted.org/packages/92/15/907be5e8900901039bae752fa9a31c03a3c1e064833f35a4e49449184581/greenlet-3.5.2-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:98a52d6a50d4deaba304331d83ee3e10ebbdc1517fcca40b2715d1de4534065c", size = 296697, upload-time = "2026-06-17T17:37:15.887Z" },
{ url = "https://files.pythonhosted.org/packages/23/a1/59b0a7c7d140ff1a75626680b9a9899b79a9176cab298b394968fb023295/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7546556f0d649f99f6a361098a55f761181bb2ea12ff150bb16d26092ad88244", size = 655333, upload-time = "2026-05-20T14:00:14.758Z" }, { url = "https://files.pythonhosted.org/packages/95/5c/08c57be575c3d6a3c023bbf22144a1c7dc6ed4d134527bb36ded4dbf04a8/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1587ff8b58fdf806993ed1490a06ac19c22d47b219c68b30954380029045d8d4", size = 656710, upload-time = "2026-06-17T18:07:28.046Z" },
{ url = "https://files.pythonhosted.org/packages/72/1b/5efe127597625042218939d01855109f352779050768b670b52edcc16a6c/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5ee3ea898009fa898f85f9982255d35278c477bebe185beca249cab42d4526c", size = 659443, upload-time = "2026-05-20T14:05:50.159Z" }, { url = "https://files.pythonhosted.org/packages/8c/d0/749f917bdc9fc90fceea4aa65fbf6556e617a50714d1496bdc8ad190bb36/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:feb721811d2754bfd16b48de151dd6b1f222c048e625151f2ca44cfdfd69f59c", size = 662629, upload-time = "2026-06-17T18:29:51.728Z" },
{ url = "https://files.pythonhosted.org/packages/c9/9d/1dcdf7b95ab3cf8c7b6d7277c18a5e167312f2b362ddfcc5d5e6d8d84b43/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a57b0d05a0448eed231d59c0ceb287dde984551e54cbc51ac2d4865712838e9c", size = 659998, upload-time = "2026-05-20T14:09:16.912Z" }, { url = "https://files.pythonhosted.org/packages/55/87/10776cd88df54d0f563e9e21e98363f2d6af94bedc553b1da0972fa87f80/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a9476cbead736dc48ce89e3cd97acff95ecc48cbf21273603a438f9870c4a014", size = 663191, upload-time = "2026-06-17T18:39:35.639Z" },
{ url = "https://files.pythonhosted.org/packages/6c/6d/c404246ea4d22d097a7426d0efb5b781bd7eb67715f09e79001bd552ab18/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5c81f74d204d3edd136ebfd50dce53acbb776995d721a0fe801626cfc93b8cd", size = 658356, upload-time = "2026-05-20T13:14:35.091Z" }, { url = "https://files.pythonhosted.org/packages/5a/a5/68cefae3a07f6d0093a490cf28ab604f14578f3e60205a2a2b2d5cd70af2/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fe6062b1f35534e1e8fb28dfed406cf4eeff3e0bca3a0d9f8ff69f20a4abb00", size = 660147, upload-time = "2026-06-17T17:39:35.068Z" },
{ url = "https://files.pythonhosted.org/packages/05/7e/c4959664fc231d587d66d8e81f2095e98056ba1954beafdcbe635e251052/greenlet-3.5.1-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:b0703c2cef53e01baec47f7a3868009913ad71ec678bbecb42a6f40895e4ce62", size = 494470, upload-time = "2026-05-20T14:01:45.611Z" }, { url = "https://files.pythonhosted.org/packages/02/aa/26ddf92826a99d87bfb8fdb8f3a262a6f16495a5d8e579737baa92fb4543/greenlet-3.5.2-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:5930d3946ecae99fa7fc0e3f3ae515426ad85058ebd9bfc6c00cca8016e6206b", size = 498199, upload-time = "2026-06-17T18:41:27.464Z" },
{ url = "https://files.pythonhosted.org/packages/51/02/f8ee37fb6d2219329f350af241c27fcf12df57e723d11f6fc6d3bacdadaa/greenlet-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:2c18ef16bf6d4dd410e4dd52996888ea1497be26892fe5bbc73580aba4287b8e", size = 1619216, upload-time = "2026-05-20T14:02:33.403Z" }, { url = "https://files.pythonhosted.org/packages/d2/6b/b9156d8397e4750220f54c7c5c34650f1e740a8d2f66eab9cfd1b7b53b69/greenlet-3.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b4ac902af825cbac8e9b2fccab8122236fd2ba6c8b71a080116d2c2ec72671b1", size = 1621675, upload-time = "2026-06-17T18:22:18.873Z" },
{ url = "https://files.pythonhosted.org/packages/93/c5/3dc9475ace2c7a3680da12372cddd7f1ac874eb410a1ac48d3e9dab83782/greenlet-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:17d86354f0ae6b61bf9be5148d0dd34e06c3cb7c602c671f79f29ac3b150e659", size = 1678427, upload-time = "2026-05-20T13:14:43.71Z" }, { url = "https://files.pythonhosted.org/packages/b0/e3/d3250f4fa01c211a93d04e34fded63187e648dbec17b9b1a14d388040593/greenlet-3.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:6f1e473c06ae8be00c9034c2bb10fa277b08a93287e3111c395b839f01d27e1f", size = 1680577, upload-time = "2026-06-17T17:40:14.055Z" },
{ url = "https://files.pythonhosted.org/packages/df/4e/750c15c317a41ffb36f0bf40b933e3d744a7dede61889f74443ea69690cf/greenlet-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:e7516cf6ae6b8a582c2770a0caed47b8a48373ed732c33d69a72913ae6ac923e", size = 245225, upload-time = "2026-05-20T13:13:59.366Z" }, { url = "https://files.pythonhosted.org/packages/55/ba/eaee8bda4419770d7096b5a009ebff0ab20a2a28cdd83c4b591bfdf36fa9/greenlet-3.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:3c2315045f9983e2e50d7e89d95405c21bddb8745f2da4487bc080ab3525f904", size = 243482, upload-time = "2026-06-17T17:37:34.741Z" },
{ url = "https://files.pythonhosted.org/packages/4f/fd/d3baea2eeb7b617efd47e87ca06e2ec2c6118d303aa9e918e0ce16eadc10/greenlet-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:5028648bf2253ec4745add746129d3904121fa7fe871a76bed23c5720573ce0a", size = 239590, upload-time = "2026-05-20T13:13:37.382Z" }, { url = "https://files.pythonhosted.org/packages/37/45/f794a81c91e9942c61f9110bd1f9a38a0ea565eab57f8b08cd53d3131e48/greenlet-3.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:db548d5ab6c2a8ead82c013f875090d79b5d7d2b67fc513934ce6cf66492ad7f", size = 242062, upload-time = "2026-06-17T17:35:39.814Z" },
] ]
[[package]] [[package]]
@ -1398,7 +1398,7 @@ wheels = [
[[package]] [[package]]
name = "pytest" name = "pytest"
version = "9.1.0" version = "9.1.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" }, { name = "colorama", marker = "sys_platform == 'win32'" },
@ -1407,9 +1407,9 @@ dependencies = [
{ name = "pluggy" }, { name = "pluggy" },
{ name = "pygments" }, { name = "pygments" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/84/0e/b5858858d74958632c49b72cb25a3976ff9f632397626715be71c89d3971/pytest-9.1.0.tar.gz", hash = "sha256:41dd9148c08072446394cefd3d79701701335a9f4cae69ba92e39f6c7f5c061c", size = 1634181, upload-time = "2026-06-13T18:52:45.983Z" } sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/5a/ba30a81239b909821b3153e303e7def45178bf353da4f72380e6c5e8793b/pytest-9.1.0-py3-none-any.whl", hash = "sha256:8ebb0e7888bdf2bdfc602ec51f8f62d50200af37356c74e503c79a94f5c81f32", size = 386453, upload-time = "2026-06-13T18:52:44.045Z" }, { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
] ]
[[package]] [[package]]
@ -1727,32 +1727,32 @@ wheels = [
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.15.17" version = "0.15.18"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8c/a9/3abdf488f1bf3d24c699415e454ed554a6350d5d89ce183be1ee0a3361ac/ruff-0.15.17.tar.gz", hash = "sha256:2ec446937fd16c8c4de2674a209cc5af64d9c6f17d21fbf1151054fa0bcf5219", size = 4743346, upload-time = "2026-06-11T17:54:47.663Z" } sdist = { url = "https://files.pythonhosted.org/packages/74/98/1295ad5a5aa9bc85bdcdfa5d82fe7b49c61af5657df4f227637ff9de0da6/ruff-0.15.18.tar.gz", hash = "sha256:2698a964c70e8bf402dcb99c8810472d270d141e7aa8c4e13599fd52033a2f33", size = 4761437, upload-time = "2026-06-18T18:25:39.224Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/db/4d/e11259f5da07cb6afb2d074c31bf09da9671993f7329d4f15d2fdc458301/ruff-0.15.17-py3-none-linux_armv6l.whl", hash = "sha256:d9feddb927fc68bd295f5eebc587a7e42cfaf9b65f60ca4a2386febff575da8f", size = 10856677, upload-time = "2026-06-11T17:54:49.533Z" }, { url = "https://files.pythonhosted.org/packages/b9/d0/686e984941269621e2be72612d5c1e461f8f7b38415a2a7d7a81c8ae6715/ruff-0.15.18-py3-none-linux_armv6l.whl", hash = "sha256:8b6850172348c8381b8b3084c5915a4393c2373b9b54cd5b5e1ea15812bc10df", size = 10887308, upload-time = "2026-06-18T18:25:03.062Z" },
{ url = "https://files.pythonhosted.org/packages/29/3e/772d679e1a0dc058e58875bd2c0cb713a0530877b4a76fee3c7966df0d49/ruff-0.15.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:25805a226d741c47d274a35ad5c10a7dde175fcddfa511d7cf3da0a21eb3eab7", size = 11223443, upload-time = "2026-06-11T17:55:00.573Z" }, { url = "https://files.pythonhosted.org/packages/ed/21/bc4123e3f5515ee99f8ce1eb93a14a0628fe4d1678663cd08f933ac16931/ruff-0.15.18-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3fccc153a85417dcd976883160cacce486997b0a0058dd18f54b8aaaac7d1ce2", size = 11281305, upload-time = "2026-06-18T18:25:30.026Z" },
{ url = "https://files.pythonhosted.org/packages/68/58/bd41f7688b2fd5623012605130ed70e60aa7f2244baa3d5066bdd61530c8/ruff-0.15.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f6ad73b14c2d18a3bf8ad7cb6974294d7f613a7898604826058e6ac64918ef4d", size = 10566458, upload-time = "2026-06-11T17:55:07.52Z" }, { url = "https://files.pythonhosted.org/packages/51/93/4769464c25cf7ab2acb3c7dda9cad3d867eb41c59565b3e2a9d17249c90c/ruff-0.15.18-py3-none-macosx_11_0_arm64.whl", hash = "sha256:08d4c86a68f2c3ec2c9d56380a71fb4a4f65373055cbb8caabd645e9102f38d4", size = 10641215, upload-time = "2026-06-18T18:25:15.802Z" },
{ url = "https://files.pythonhosted.org/packages/d8/5b/733371013fcf1ec339e477ece6ab42bfe10bdd9bba8ee88a9516aa56bfc0/ruff-0.15.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ba0c1e4f95bcb3869d0d30cbd5917071ef2e28665abfec970cdab0492c713ed", size = 10914483, upload-time = "2026-06-11T17:55:05.501Z" }, { url = "https://files.pythonhosted.org/packages/6c/42/56926d17120db2c208d76bf60a1a019644dd9e91dc27f0f95c9caddb1366/ruff-0.15.18-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37e5108745c2c0705da916d7d4de533ddf547051ef45f62888c31bae73f66318", size = 10957224, upload-time = "2026-06-18T18:25:36.955Z" },
{ url = "https://files.pythonhosted.org/packages/bd/cc/6f24251cc0252f7239391ccb85833f320efad14ebe5b443943f37ced6332/ruff-0.15.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:81647960f10bff57d2e51cadd0c3950fe598400c852863a038720ef5b8cca91e", size = 10647497, upload-time = "2026-06-11T17:54:57.733Z" }, { url = "https://files.pythonhosted.org/packages/22/4f/d43fab8d8189afde803103022d000a8ef9f230616d436d52a8b2b8d63b50/ruff-0.15.18-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56949a6ce8b3abde54c0bcb22cebfe57e8771cadc84b407ae8b8eaf67ebdcd43", size = 10699024, upload-time = "2026-06-18T18:25:05.707Z" },
{ url = "https://files.pythonhosted.org/packages/68/dd/0d10c17ce1a1624d6fc3156309c3f834fdb5dfaad026ec90c85684f3990e/ruff-0.15.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e01a84ddbc8c16c23055ba3924476850f1bbc1917cebbb9376665a63e74260d", size = 11416967, upload-time = "2026-06-11T17:54:51.461Z" }, { url = "https://files.pythonhosted.org/packages/63/42/1e3e4c68bd408b9768cf3e439acbe2c78245225faef253f7028a0cdb63e0/ruff-0.15.18-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01a754cd6a1b630d3f97e33eb452cf7a98040482318e870f8bc52a5a30e62657", size = 11491458, upload-time = "2026-06-18T18:25:20.275Z" },
{ url = "https://files.pythonhosted.org/packages/2f/91/556bfb156f6144f355e831c23db00b2fc4120f86b3ce81cc5f7fd2df51f3/ruff-0.15.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fe9f653152f8f294f9f7e03bf3a453d8b4a27f7a59c78c8666167f2b17b96c", size = 12335770, upload-time = "2026-06-11T17:54:45.793Z" }, { url = "https://files.pythonhosted.org/packages/20/77/47a3484bea8521e14a203d98c389c5c97846675e4f02734672da4a69b52a/ruff-0.15.18-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ba7a07e03a44dbf10bb086ee06705b173625014ec99f73a7e6836a5e5590a0c", size = 12383752, upload-time = "2026-06-18T18:25:22.535Z" },
{ url = "https://files.pythonhosted.org/packages/88/82/8b5999aa13355e926f06d9f42a32dcca862f623bf0363785ff89d607dffd/ruff-0.15.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c0fe88a7676e7a05b73174d4d4a59cb2ac21ff8263583f87a81a6018475a978", size = 11575441, upload-time = "2026-06-11T17:54:32.661Z" }, { url = "https://files.pythonhosted.org/packages/0a/ca/054159590787023d83b658a1a1819c4c8910114e7015069340b71c0961cb/ruff-0.15.18-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a2c40a41a4cadbcf5897b548ab29dfe248b20c540961c0247d98a3973c70403", size = 11577923, upload-time = "2026-06-18T18:25:10.702Z" },
{ url = "https://files.pythonhosted.org/packages/11/93/f10377bb04109ca0e8cbc483ff1982c54b6d418210041776f93e8cdc7fa9/ruff-0.15.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecfc3c7878fff94633ab0348524e093f9ce3243080416dd7d14f8ba400174719", size = 11557614, upload-time = "2026-06-11T17:54:34.698Z" }, { url = "https://files.pythonhosted.org/packages/6d/ff/d353d6b7bbd73cc0ec37f4463d7540e45e894338abdd9964eee0de332708/ruff-0.15.18-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f0480ce690cbb6c4db6e5d08f19fce98e10ba131a8b60c1bcdac42771e3ae2d", size = 11583925, upload-time = "2026-06-18T18:25:32.391Z" },
{ url = "https://files.pythonhosted.org/packages/c7/a6/eeeae7f7d5493df41649ab3db92f086b2d0a30199e4efdf8e3dd7a033f24/ruff-0.15.17-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:b8461180b22420b1bdc289909410930761629fddf2a5aaf60fae1ab26cedc4c4", size = 11544450, upload-time = "2026-06-11T17:54:39.042Z" }, { url = "https://files.pythonhosted.org/packages/c1/4a/891f89b9c296ed3e5f3ece1a5629badc989d9a8fdaa30431aaf4774bc1c2/ruff-0.15.18-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2330215f1f393fa8733f55edce04fcf94c36a2c460fcde31f78cc84e4951e9b1", size = 11582834, upload-time = "2026-06-18T18:25:27.309Z" },
{ url = "https://files.pythonhosted.org/packages/32/88/5991ce565129a24dd4a00db1254b3b5db2e53018cbe4018ea5a89738e727/ruff-0.15.17-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6eccbe50a038b503e7140b441aa9c7fc8c1f36edf23ebef9f4165c2f28f568b7", size = 10892524, upload-time = "2026-06-11T17:55:09.432Z" }, { url = "https://files.pythonhosted.org/packages/32/a3/ed9e370154bf85de360b93c03026157f02d4943b2d01ff4945f4429f8e8a/ruff-0.15.18-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6aa6a3d979e48ae617578183674bf264fbe7d0114a796a26bd678d67963c7ff", size = 10927328, upload-time = "2026-06-18T18:25:34.676Z" },
{ url = "https://files.pythonhosted.org/packages/f5/1d/0fdd248313425f55223968af04b0a42125466a8d88d21c1d99c6af0a51e8/ruff-0.15.17-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:382fc0521025f5a8ad447d8bdd523545d0d7646adb718eb1c2dac5065ec27c0f", size = 10659573, upload-time = "2026-06-11T17:54:36.824Z" }, { url = "https://files.pythonhosted.org/packages/f5/d1/5cf5909329fedb5d39d555ee818ba5cf4638e1a301b89785d34f2905bfcb/ruff-0.15.18-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a81beadbbff2c9c245561ae3f77b16709d87f35eec650d0501679239d3449b22", size = 10693187, upload-time = "2026-06-18T18:25:08.245Z" },
{ url = "https://files.pythonhosted.org/packages/9e/0e/072e8260deb9461062ce9311ced27a8e541229a6ffd483013dd37661e43e/ruff-0.15.17-py3-none-musllinux_1_2_i686.whl", hash = "sha256:456d41fcd1b2777ad63f09a6e7121d43f7b688bbc76a800c10f7f8fb1f912c3f", size = 11127818, upload-time = "2026-06-11T17:55:03.124Z" }, { url = "https://files.pythonhosted.org/packages/fd/44/ff6c635cf2c4f4e7b618b6640da057376baa36014695487d88aed4794268/ruff-0.15.18-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2186d9e940ae332ab293623a75b5f4fe49565f449954d50a72a046683aa6b809", size = 11208721, upload-time = "2026-06-18T18:25:41.327Z" },
{ url = "https://files.pythonhosted.org/packages/ab/b4/55060a34163121498014696b5f656db5b8c6963768f227dbf0d76b311073/ruff-0.15.17-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b1a04bcc94ae6194e9db05d16ad31f298a7194bfbcb08258bbe589cee1d587b8", size = 11655901, upload-time = "2026-06-11T17:54:53.562Z" }, { url = "https://files.pythonhosted.org/packages/88/d9/5baa2a30861adfb7022cf33c1e35b2fc18085b08c16f83eff4c7b99a5f48/ruff-0.15.18-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5c2abf140438032bc77b2284a6c9944ecd8a19e5f1c7b52b1b8e4a0a80d19a7a", size = 11678599, upload-time = "2026-06-18T18:25:13.607Z" },
{ url = "https://files.pythonhosted.org/packages/49/71/9b29d6b87cef468d697f43c6a91e3fae4a80185779d7d5a4ef27d173439f/ruff-0.15.17-py3-none-win32.whl", hash = "sha256:596065960ab1ff593f744220c9fe6580eda00a95003cffa9f4048bb5b1bf0392", size = 10925574, upload-time = "2026-06-11T17:54:55.723Z" }, { url = "https://files.pythonhosted.org/packages/c3/1a/0725a7cfdc32ff769efb96ee782bec882e16448c5d9e3be947ec4c04ce27/ruff-0.15.18-py3-none-win32.whl", hash = "sha256:02299e6e9fa5b297a3f6d5d10d7bcd655c925b028bb8b9d4588214549c6b9ec4", size = 10901903, upload-time = "2026-06-18T18:25:24.755Z" },
{ url = "https://files.pythonhosted.org/packages/3d/b2/8fc77f3723228836fa5d12497eb71c808f83782e10d058d2b15cfa14640b/ruff-0.15.17-py3-none-win_amd64.whl", hash = "sha256:6769e5fa1710b179b92e0bfa5a51735b35baea9013dadb06d5f44cbcf9547084", size = 12058788, upload-time = "2026-06-11T17:54:41.042Z" }, { url = "https://files.pythonhosted.org/packages/f3/51/805d9f6fb7970505c3504794a5ec350f605361b807fef4dcf214ebd35e72/ruff-0.15.18-py3-none-win_amd64.whl", hash = "sha256:dac80dc8d26b2257dbefabed62f5d255c3937b4ccb122da1fc634794fa3578b3", size = 12041189, upload-time = "2026-06-18T18:25:17.915Z" },
{ url = "https://files.pythonhosted.org/packages/2d/c7/c53e8dbff9c9dc4b7928773421ae294a5d28fcb8dcda1a089579d3a7e510/ruff-0.15.17-py3-none-win_arm64.whl", hash = "sha256:f3be1fbb34bcdfd146240d8fb92a709d4c2c8191348580a3c044ec60fa0b4456", size = 11355275, upload-time = "2026-06-11T17:54:43.635Z" }, { url = "https://files.pythonhosted.org/packages/29/4c/67bb45e41609eb4726f1bfeb59e083cf91d14c696d4bd14c234a980be93d/ruff-0.15.18-py3-none-win_arm64.whl", hash = "sha256:b2c9257fcbd4a3e5b977a1904e6facca016bafe2edc17df24db67cfaee03b4e4", size = 11329958, upload-time = "2026-06-18T18:25:43.686Z" },
] ]
[[package]] [[package]]
name = "selenium" name = "selenium"
version = "4.44.0" version = "4.45.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "certifi" }, { name = "certifi" },
@ -1762,9 +1762,9 @@ dependencies = [
{ name = "urllib3", extra = ["socks"] }, { name = "urllib3", extra = ["socks"] },
{ name = "websocket-client" }, { name = "websocket-client" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/2d/4a/6d0a4f4a07e2a91511a51398203ee82bf6ce644a448aaa35c59b44aa9531/selenium-4.44.0.tar.gz", hash = "sha256:b03a831fcfcab9d912b4682f60718c48a04560d6c62f7496c16b7498c9a4427e", size = 993133, upload-time = "2026-05-12T22:48:19.246Z" } sdist = { url = "https://files.pythonhosted.org/packages/86/48/486aa67320f27452e9f551b8608f1a59ce7091c8fe7ebc9f4eba274775d4/selenium-4.45.0.tar.gz", hash = "sha256:563f0c4102f112df1cda30d46ce6d177b2e4a7a3d4b0756902d5dc84d3a8a365", size = 1005503, upload-time = "2026-06-16T04:43:57.915Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/1f/bc/885047e975e996cb317db31c4551caa915aafc6befea990f082c7233adc2/selenium-4.44.0-py3-none-any.whl", hash = "sha256:d01ea3e5ecad8149460a765f7cf5177194c21dcc0173093fc05427c289b1bf24", size = 9654291, upload-time = "2026-05-12T22:48:16.836Z" }, { url = "https://files.pythonhosted.org/packages/e4/8a/6ff6beb9c7c6cc642f628df9328a8b6637f86602eff8d28e70b5d4e8bca7/selenium-4.45.0-py3-none-any.whl", hash = "sha256:1fd9d0dc08192b2f8100e264ed720f83b05d2dd3a7feff673df04e0c7580df4b", size = 9536616, upload-time = "2026-06-16T04:43:55.968Z" },
] ]
[[package]] [[package]]
@ -1814,36 +1814,36 @@ wheels = [
[[package]] [[package]]
name = "sqlalchemy" name = "sqlalchemy"
version = "2.0.50" version = "2.0.51"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
{ name = "typing-extensions" }, { name = "typing-extensions" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/57/da/6fbf010c8ebb347679d0d100b22fe9ba5e13fd04046c5df7280d2f0bf706/sqlalchemy-2.0.50.tar.gz", hash = "sha256:af5607d11ef90fd6a5c0549fe0045dce1663d427426bcfb506dcb5346a85a3b9", size = 9907424, upload-time = "2026-05-24T19:20:04.018Z" } sdist = { url = "https://files.pythonhosted.org/packages/02/f1/a7a892f18d4d224e6b26f706531eafccc41e37594d37d304786969ee13cb/sqlalchemy-2.0.51.tar.gz", hash = "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9", size = 9912201, upload-time = "2026-06-15T15:41:20.012Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/c4/c42356b527296e9862f67990efce31ef78b4cf69cd3f80873a528a060320/sqlalchemy-2.0.50-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:06a9210bdc5f4298cff0781087e2ff45683922252dacc452846373a58761f093", size = 2156697, upload-time = "2026-05-24T19:27:54.764Z" }, { url = "https://files.pythonhosted.org/packages/54/fe/a210d52fd1a90ecfae8a78e9d8b27e18d733d60818a8bf250ff690b75120/sqlalchemy-2.0.51-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07", size = 2157184, upload-time = "2026-06-15T16:08:50.374Z" },
{ url = "https://files.pythonhosted.org/packages/60/a1/b1a70e3c4365ac7fe9e347f3710f19b562c866fb96d45e3c891588789a7b/sqlalchemy-2.0.50-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b53784972ade4f8174b9aa661f31a06f8a936d2cfdd602913ff3c6dd40ae873", size = 3284260, upload-time = "2026-05-24T20:09:34.195Z" }, { url = "https://files.pythonhosted.org/packages/17/6b/2dce8369b199cb855110e056032f94a9f66dacc2237d3d39c115a86eac56/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195", size = 3284735, upload-time = "2026-06-15T16:19:46.934Z" },
{ url = "https://files.pythonhosted.org/packages/3f/4a/f3ac3caa19f263d57b0a47f8c91bbf56583dc2d3fc63acfbf644abb24fe0/sqlalchemy-2.0.50-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31648fa14460537e768a7303b078e4344d208e0d23e06867c1f376a227ed82db", size = 3302280, upload-time = "2026-05-24T20:17:17.825Z" }, { url = "https://files.pythonhosted.org/packages/53/ff/dbc495b8a14da840faffb353857a72d4190113cac33727906fb997047f0f/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f", size = 3302756, upload-time = "2026-06-15T16:26:41.336Z" },
{ url = "https://files.pythonhosted.org/packages/66/55/ccada3e3d62254587819749a0bc69f41173eb48a6e385d10e66d32a9c88e/sqlalchemy-2.0.50-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:03f4323c980ad0e918cc9e5369b015f759f4e534db5bbaf4dc36832c10d05064", size = 3231580, upload-time = "2026-05-24T20:09:36.406Z" }, { url = "https://files.pythonhosted.org/packages/cf/d5/fde8f4dddcf518ee15ab35a7c6a28acc32c8ba548d1d2aa451f96e6dbb0b/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400", size = 3232055, upload-time = "2026-06-15T16:19:49.286Z" },
{ url = "https://files.pythonhosted.org/packages/05/f6/6809349130a2de0e109e7f00fd7d431da9565b9b2868b32ee684754f672b/sqlalchemy-2.0.50-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2b9dcc43afef8ac157cd92fce96985d6b8b0cfbd3df4d666f66b4d55a75d202f", size = 3269375, upload-time = "2026-05-24T20:17:20.34Z" }, { url = "https://files.pythonhosted.org/packages/67/d1/43d3a0ac955a58601c24fa23038b1c55ee3a1ec02c0f96ebb1eae2bcf614/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d", size = 3269850, upload-time = "2026-06-15T16:26:43.017Z" },
{ url = "https://files.pythonhosted.org/packages/48/84/278a811ef4e07be9c89dc5cdd7be833268509a66a68c4897cf585e67428f/sqlalchemy-2.0.50-cp313-cp313-win32.whl", hash = "sha256:60922d6599065ddca2c6f376b9aa2f41a6b85a271725e0909490bbc50b1998a5", size = 2117229, upload-time = "2026-05-24T19:50:08.215Z" }, { url = "https://files.pythonhosted.org/packages/94/df/de669c7054cd47c4439ac34b1b2ee8b804a794791fbb10720e997a2c87c7/sqlalchemy-2.0.51-cp313-cp313-win32.whl", hash = "sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b", size = 2117721, upload-time = "2026-06-15T16:23:12.36Z" },
{ url = "https://files.pythonhosted.org/packages/f6/1c/067cc6187ed32d2ec222fe6d2643acc1659a6d0659f8a7cbc5ad3ae83280/sqlalchemy-2.0.50-cp313-cp313-win_amd64.whl", hash = "sha256:287086e67275a212c4582d166a6fb03a65ccc5551d80866270ce0dd9f34eccd3", size = 2143126, upload-time = "2026-05-24T19:50:09.691Z" }, { url = "https://files.pythonhosted.org/packages/d0/8a/403c51d064196bae20a0bc2476577f83a3f8dd299719a97417086b7f2ec5/sqlalchemy-2.0.51-cp313-cp313-win_amd64.whl", hash = "sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5", size = 2143615, upload-time = "2026-06-15T16:23:13.906Z" },
{ url = "https://files.pythonhosted.org/packages/df/32/10ac51b4be7cdecd7e93d069251c86dfbf70b7adbd7c67b48ccea6c49e1c/sqlalchemy-2.0.50-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c966932507a4d7d0a37314927dbfcd89720e3f37d2a1e3352e7ae7939fa8e8a0", size = 2158519, upload-time = "2026-05-24T19:27:56.472Z" }, { url = "https://files.pythonhosted.org/packages/b1/49/a739be2e1d02a96a658eb71ab45d921c874249252358ad24a5bffdd02525/sqlalchemy-2.0.51-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491", size = 2158999, upload-time = "2026-06-15T16:08:51.759Z" },
{ url = "https://files.pythonhosted.org/packages/5a/76/e703d2f7681d7d66c4c891af3f07c7ccf4c76ad7f18351de035b5eda007a/sqlalchemy-2.0.50-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:faffef4bcc20a1892e65e155293d99d60855bbbc79250ab712819cfd56a8e6bb", size = 3282063, upload-time = "2026-05-24T20:09:38.57Z" }, { url = "https://files.pythonhosted.org/packages/23/6b/2e0e38cf75c8780eca78d9b2e78164f8bcfd70125e5caa588ff5cbb9c9f4/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d", size = 3282539, upload-time = "2026-06-15T16:19:51.065Z" },
{ url = "https://files.pythonhosted.org/packages/31/26/ef168b184a25701f9995e8fb7e503fafd7a99c1c77cda1bc1a26ea2ed486/sqlalchemy-2.0.50-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c206aec519a2e7bd08abbfb33436e325fd22c632d9c21a9047e376ce241646e", size = 3287069, upload-time = "2026-05-24T20:17:21.942Z" }, { url = "https://files.pythonhosted.org/packages/dd/a1/e77854cb5336fd37dc3c6ae3b71de242c98caac5725120be0b526b31cbd0/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54", size = 3287545, upload-time = "2026-06-15T16:26:44.735Z" },
{ url = "https://files.pythonhosted.org/packages/c2/15/765acc2bc693bccc43ca4a95d5b69750da8aaf6db1b5c616536e087f8920/sqlalchemy-2.0.50-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bef4ac756363227ef6402a75fee025a4bc690f92328e825868939b3b3a446a6d", size = 3230453, upload-time = "2026-05-24T20:09:40.398Z" }, { url = "https://files.pythonhosted.org/packages/f6/ab/9e17272fd4dac8df3b83c4fbe52b998a1c9d89a843c8c35ff29b74ff7364/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e", size = 3230929, upload-time = "2026-06-15T16:19:52.625Z" },
{ url = "https://files.pythonhosted.org/packages/63/61/08e03c3adbf5db0087a0b6816746fec8f3032fb2f7fc899a9bb9b2a48ce4/sqlalchemy-2.0.50-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96fbee6b19c19cd1556c8bf9419447cf2ec149ffcab7ab64348c23e54ef8547f", size = 3252413, upload-time = "2026-05-24T20:17:24.067Z" }, { url = "https://files.pythonhosted.org/packages/02/3c/52f408ea701781caee975606beccc48845f2aee8711ac29843d612c0306c/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d", size = 3252888, upload-time = "2026-06-15T16:26:46.454Z" },
{ url = "https://files.pythonhosted.org/packages/03/0c/370a1f2db38436c615e10134c8a37de3688e74084792380695f3f5083860/sqlalchemy-2.0.50-cp314-cp314-win32.whl", hash = "sha256:8f00e3eb43ba30eb1b238ee03a8a62309486d1321eda3328bb611e0340033ad8", size = 2120063, upload-time = "2026-05-24T19:50:11.08Z" }, { url = "https://files.pythonhosted.org/packages/24/16/3efd2ee6bc4ca4693a30a1dd17a91b606cae15d517d2a4746611d9b73ce8/sqlalchemy-2.0.51-cp314-cp314-win32.whl", hash = "sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8", size = 2120551, upload-time = "2026-06-15T16:23:15.629Z" },
{ url = "https://files.pythonhosted.org/packages/7f/a0/fe92bb9817863bc13ba093bda931979a26cc2ca69f8e8f26d07add3d7c6f/sqlalchemy-2.0.50-cp314-cp314-win_amd64.whl", hash = "sha256:15708c613cd5005b7dffe1f66ee6a63ee8f5e46799f71c70ebad74178c676a39", size = 2145830, upload-time = "2026-05-24T19:50:12.452Z" }, { url = "https://files.pythonhosted.org/packages/7b/78/55b12e70f45bccc40d9e483925c065027b3b98ea4cbbdf6f8c2546feaf6c/sqlalchemy-2.0.51-cp314-cp314-win_amd64.whl", hash = "sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499", size = 2146318, upload-time = "2026-06-15T16:23:17.108Z" },
{ url = "https://files.pythonhosted.org/packages/cc/ff/e5640a98a0b2f491eb8fde10fb6c773621a2e44340de231fafcc9370f4a9/sqlalchemy-2.0.50-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3699dac4be410e97049a1658e9480da9cde956594aa0f3aebc60b88f21c5ba70", size = 2178435, upload-time = "2026-05-24T19:42:58.889Z" }, { url = "https://files.pythonhosted.org/packages/21/db/a9574ed40fed418924b1b1a3e54f47ee3963053b3d3d325a0d36b41f2c08/sqlalchemy-2.0.51-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de", size = 2178920, upload-time = "2026-06-15T15:59:56.285Z" },
{ url = "https://files.pythonhosted.org/packages/b7/85/337116e186f1236375b5fb70c21cfac98e8e8ab0d3a47be838dc47a59e08/sqlalchemy-2.0.50-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f96233858e3df43932ac11589e22520da6e8aeb624b03fedfeebb0e8ea213086", size = 3566059, upload-time = "2026-05-24T20:01:20.848Z" }, { url = "https://files.pythonhosted.org/packages/bf/90/a1bb5c7cbba76b7bc1fbd586d0a5479a7bc9c27b4a8298f22ec9423b2bb3/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7", size = 3566534, upload-time = "2026-06-15T15:58:35.024Z" },
{ url = "https://files.pythonhosted.org/packages/96/34/bb0e190e161c3c2c24314a65add57218be14a4a9486886b7f5047c1ff7c8/sqlalchemy-2.0.50-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c4e70c46fad30c3bcc6a4708bc0130a3173e11a5b25f0ea4a9d8911b450f1f52", size = 3535366, upload-time = "2026-05-24T20:03:56.768Z" }, { url = "https://files.pythonhosted.org/packages/15/4b/481f1fed30e0e9e8dd24aecbb49f29eb57fe7657ece5cf06ee9b84bb97d8/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72", size = 3535844, upload-time = "2026-06-15T16:02:43.973Z" },
{ url = "https://files.pythonhosted.org/packages/df/5a/a7f759f97e4fd499c5d4e4488c760d5a7fbecf3028b465a04274fcd52384/sqlalchemy-2.0.50-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1918a3cf564d16d95bca7301005f41ab2ad50b07cd3b9da50d3ed986db148d6a", size = 3474879, upload-time = "2026-05-24T20:01:23.058Z" }, { url = "https://files.pythonhosted.org/packages/02/71/0aa64aeda645510af0a43f7d9ee70932f0d1dc4263aed34c50ee891d9df3/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23", size = 3475355, upload-time = "2026-06-15T15:58:36.592Z" },
{ url = "https://files.pythonhosted.org/packages/9d/d9/2907ea38eb60687d297bf9c39e5ee58053c87b57fe8a9cae97090cecbf10/sqlalchemy-2.0.50-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b00098cdbdbd38c7be3d568b0c9c3122b8c0ec62b911b57cd5e6e0254d60a76d", size = 3486117, upload-time = "2026-05-24T20:03:59.052Z" }, { url = "https://files.pythonhosted.org/packages/05/db/6061db32316446135a3abae5f308d144ab988a34234726042da3e58b1c63/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522", size = 3486591, upload-time = "2026-06-15T16:02:45.346Z" },
{ url = "https://files.pythonhosted.org/packages/f2/e3/5aa06f167559f8c0bdae487e297d23ba548150ab016a3418265d617a4985/sqlalchemy-2.0.50-cp314-cp314t-win32.whl", hash = "sha256:1fbd55a969d7ac44a98e3dec75016074f809fa08f871585ace58dde110d1bf3e", size = 2150823, upload-time = "2026-05-24T20:08:58.644Z" }, { url = "https://files.pythonhosted.org/packages/0d/c9/f14fdf71bb8957e0c7e39db69bbdf12b5c80f4ef775fdfa127bf4e0d6760/sqlalchemy-2.0.51-cp314-cp314t-win32.whl", hash = "sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7", size = 2151313, upload-time = "2026-06-15T16:03:39.127Z" },
{ url = "https://files.pythonhosted.org/packages/65/9b/112fb8f977582d7489d036e409e3723948bcf5320b3ac465f3c481bbe8f9/sqlalchemy-2.0.50-cp314-cp314t-win_amd64.whl", hash = "sha256:c5c3cdb753a9004183e1ccb634b41611654c989e61bc68617ce878e46d6f1e51", size = 2185794, upload-time = "2026-05-24T20:09:00.319Z" }, { url = "https://files.pythonhosted.org/packages/6a/c6/673e618e6f4f297e126d9b56ea2f6478708f6c1af4e3223835c22e2c3697/sqlalchemy-2.0.51-cp314-cp314t-win_amd64.whl", hash = "sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2", size = 2186280, upload-time = "2026-06-15T16:03:40.569Z" },
{ url = "https://files.pythonhosted.org/packages/d0/10/f7220e9b784d295d241c86ed99aeb537f92afcd469a64861f2717e9bb077/sqlalchemy-2.0.50-py3-none-any.whl", hash = "sha256:92064363517a3ff8212b5a93b8c62876579d8dfd1ca5b561335f30152d884fa9", size = 1943861, upload-time = "2026-05-24T19:59:01.119Z" }, { url = "https://files.pythonhosted.org/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5", size = 1944334, upload-time = "2026-06-15T16:09:22.418Z" },
] ]
[[package]] [[package]]
@ -1909,14 +1909,14 @@ wheels = [
[[package]] [[package]]
name = "tzlocal" name = "tzlocal"
version = "5.3.1" version = "5.4.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "tzdata", marker = "sys_platform == 'win32'" }, { name = "tzdata", marker = "sys_platform == 'win32'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" } sdist = { url = "https://files.pythonhosted.org/packages/48/55/15e2340963d2bfedcc6042da3911438fd336f8ae96b65bdbe3a29766da0c/tzlocal-5.4.3.tar.gz", hash = "sha256:3a8c9bc18cf47e1dcde252ea0e6a72a6cde320a400b6ac6db1f1f8cccd553c00", size = 30873, upload-time = "2026-06-17T04:17:41.764Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" }, { url = "https://files.pythonhosted.org/packages/42/28/fc144409c71569e928585f8f3c629d80d1ca3ef40175e9222f01588f98c9/tzlocal-5.4.3-py3-none-any.whl", hash = "sha256:24ce97bb58e2a973f7640ec2553ab4e6f6d5a0d0d1aa9dc43bca21d89e1feb82", size = 18039, upload-time = "2026-06-17T04:17:40.027Z" },
] ]
[[package]] [[package]]