feat(unlisted-sharing): QR code + per-link expiry picker (M8.5)

SharedLinkControls now renders a lazy QR code (qrcode npm) and a
datetime-local "Läuft ab" picker. Both stay in sync with the active
URL — regenerating the link rebuilds the QR; clearing the expiry
re-publishes with no `expiresAt`.

Wired across all three unlisted collections:
- Calendar: LocalEvent.unlistedExpiresAt + setUnlistedExpiry +
  preserve-on-refresh + clear-on-flip; both Workbench DetailView and
  EventDetailModal pass expiresAt+onExpiryChange to SharedLinkControls.
- Library: same pattern in libraryEntriesStore + DetailView.
- Places: same pattern in placesStore + DetailView.

setVisibility clears any prior expiry so a flip-away-flip-back gets
a fresh "never expires" link. refreshUnlistedSnapshot and
regenerateUnlistedToken preserve the existing expiry so a content
edit or token rotation never silently extends a link's lifetime.

The qrcode dep ships as a regular `dependencies` entry on
@mana/shared-privacy so any consuming app picks it up via the
workspace.

Note: an unrelated svelte-check error in writing/components/DraftCard
("draft" not assignable to DragType) exists from a parallel session
and is not introduced by this commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-25 12:29:53 +02:00
parent 85fca7ccdc
commit b7a54ccd10
16 changed files with 379 additions and 215 deletions

View file

@ -46,6 +46,10 @@
await eventsStore.setVisibility(event.id, 'space');
}
async function handleExpiryChange(expiresAt: Date | null) {
await eventsStore.setUnlistedExpiry(event.id, expiresAt);
}
const shareUrl = $derived.by(() => {
if (!event.unlistedToken) return '';
const origin = typeof window === 'undefined' ? 'https://mana.how' : window.location.origin;
@ -276,8 +280,10 @@
<SharedLinkControls
token={event.unlistedToken}
url={shareUrl}
expiresAt={event.unlistedExpiresAt}
onRegenerate={handleRegenerate}
onRevoke={handleRevoke}
onExpiryChange={handleExpiryChange}
/>
</div>
</div>

View file

@ -436,6 +436,7 @@ export const eventsStore = {
tagIds: data.tagIds || [],
visibility: data.visibility ?? 'private',
unlistedToken: data.unlistedToken ?? '',
unlistedExpiresAt: data.unlistedExpiresAt ?? null,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
blockType: 'event',
@ -523,6 +524,7 @@ export const eventsStore = {
blob,
});
patch.unlistedToken = token;
patch.unlistedExpiresAt = undefined;
} else if (before === 'unlisted') {
const jwt = await authStore.getValidToken();
if (jwt) {
@ -534,6 +536,7 @@ export const eventsStore = {
});
}
patch.unlistedToken = undefined;
patch.unlistedExpiresAt = undefined;
}
await db.table('events').update(id, patch);
@ -596,6 +599,39 @@ export const eventsStore = {
}
},
/**
* Set or clear the unlisted-share expiry. Re-publishes the snapshot
* with the new `expiresAt`; mirrors the value locally so the
* SharedLinkControls picker shows the right state without a server
* round-trip. No-op if the event isn't currently 'unlisted'.
*/
async setUnlistedExpiry(id: string, expiresAt: Date | null) {
const existing = await db.table<LocalEvent>('events').get(id);
if (!existing || existing.visibility !== 'unlisted') return;
const jwt = await authStore.getValidToken();
if (!jwt) return;
try {
const blob = await buildUnlistedBlob('events', id);
const spaceId =
(existing as unknown as { spaceId?: string }).spaceId ?? getActiveSpace()?.id ?? '';
await publishUnlistedSnapshot({
apiUrl: getManaApiUrl(),
jwt,
collection: 'events',
recordId: id,
spaceId,
blob,
expiresAt,
});
await db.table('events').update(id, {
unlistedExpiresAt: expiresAt ? expiresAt.toISOString() : undefined,
updatedAt: new Date().toISOString(),
});
} catch (e) {
console.error('[calendar/events] setUnlistedExpiry failed', e);
}
},
/**
* Re-publish the unlisted snapshot for an event. Called by
* updateEvent/updateSingleInstance/etc. when the owning record is
@ -622,6 +658,9 @@ export const eventsStore = {
recordId: id,
spaceId,
blob,
// Preserve any existing expiry so a content edit doesn't
// silently extend the link's lifetime.
expiresAt: existing.unlistedExpiresAt ? new Date(existing.unlistedExpiresAt) : undefined,
});
} catch (e) {
console.error('[calendar/events] refreshUnlistedSnapshot failed', e);

View file

@ -30,6 +30,8 @@ export interface LocalEvent extends BaseRecord {
visibilityChangedAt?: string;
visibilityChangedBy?: string;
unlistedToken?: string;
/** Local mirror of the server-side unlisted-snapshot expiry. */
unlistedExpiresAt?: string;
}
export type CalendarViewType = 'week' | 'month' | 'agenda';
@ -60,6 +62,8 @@ export interface CalendarEvent {
* to know whether to render the share-link controls).
*/
unlistedToken: string;
/** ISO expiry for the active unlisted-share, null when never expires. */
unlistedExpiresAt: string | null;
createdAt: string;
updatedAt: string;
// TimeBlock metadata (for universal calendar view)
@ -114,6 +118,7 @@ export function timeBlockToCalendarEvent(
// they stay invisible on the website (public requires explicit opt-in).
visibility: eventData?.visibility ?? 'space',
unlistedToken: eventData?.unlistedToken ?? '',
unlistedExpiresAt: eventData?.unlistedExpiresAt ?? null,
createdAt: block.createdAt,
updatedAt: block.updatedAt,
blockType: block.type,

View file

@ -111,6 +111,10 @@
await eventsStore.setVisibility(eventId, 'space');
}
async function handleExpiryChange(expiresAt: Date | null) {
await eventsStore.setUnlistedExpiry(eventId, expiresAt);
}
const shareUrl = $derived.by(() => {
const token = detail.entity?.unlistedToken;
if (!token) return '';
@ -158,8 +162,10 @@
<SharedLinkControls
token={event.unlistedToken}
url={shareUrl}
expiresAt={event.unlistedExpiresAt}
onRegenerate={handleRegenerate}
onRevoke={handleRevoke}
onExpiryChange={handleExpiryChange}
/>
</div>
{/if}

View file

@ -111,6 +111,7 @@
: { kind: 'comic' },
visibility: 'private',
unlistedToken: '',
unlistedExpiresAt: null,
createdAt: '',
updatedAt: '',
id: '',

View file

@ -37,6 +37,7 @@ export function toLibraryEntry(local: LocalLibraryEntry): LibraryEntry {
// space-type-aware default at create time in entries.svelte.ts.
visibility: local.visibility ?? 'space',
unlistedToken: local.unlistedToken ?? '',
unlistedExpiresAt: local.unlistedExpiresAt ?? null,
createdAt: local.createdAt ?? now,
updatedAt: local.updatedAt ?? now,
};

View file

@ -259,6 +259,7 @@ export const libraryEntriesStore = {
blob,
});
patch.unlistedToken = token;
patch.unlistedExpiresAt = undefined;
} else if (before === 'unlisted') {
const jwt = await authStore.getValidToken();
if (jwt) {
@ -270,6 +271,7 @@ export const libraryEntriesStore = {
});
}
patch.unlistedToken = undefined;
patch.unlistedExpiresAt = undefined;
}
await libraryEntryTable.update(id, patch);
@ -309,6 +311,9 @@ export const libraryEntriesStore = {
recordId: id,
spaceId,
blob,
// Preserve any existing expiry — regenerate is about leaking
// the URL, not extending the lifetime.
expiresAt: existing.unlistedExpiresAt ? new Date(existing.unlistedExpiresAt) : undefined,
});
await libraryEntryTable.update(id, {
unlistedToken: token,
@ -321,6 +326,38 @@ export const libraryEntriesStore = {
}
},
/**
* Set or clear the unlisted-share expiry. Mirrors
* eventsStore.setUnlistedExpiry re-publishes with the new expiry
* and stores it locally so the picker stays in sync.
*/
async setUnlistedExpiry(id: string, expiresAt: Date | null) {
const existing = await libraryEntryTable.get(id);
if (!existing || existing.visibility !== 'unlisted') return;
const jwt = await authStore.getValidToken();
if (!jwt) return;
try {
const blob = await buildUnlistedBlob('libraryEntries', id);
const spaceId =
(existing as unknown as { spaceId?: string }).spaceId ?? getActiveSpace()?.id ?? '';
await publishUnlistedSnapshot({
apiUrl: getManaApiUrl(),
jwt,
collection: 'libraryEntries',
recordId: id,
spaceId,
blob,
expiresAt,
});
await libraryEntryTable.update(id, {
unlistedExpiresAt: expiresAt ? expiresAt.toISOString() : undefined,
updatedAt: new Date().toISOString(),
});
} catch (e) {
console.error('[library] setUnlistedExpiry failed', e);
}
},
/**
* Re-publish unlisted snapshot when whitelist fields change. Called
* fire-and-forget after updateEntry/setStatus/rate. No-op if the
@ -342,6 +379,9 @@ export const libraryEntriesStore = {
recordId: id,
spaceId,
blob,
// Preserve any existing expiry so a content edit doesn't
// silently extend the link's lifetime.
expiresAt: existing.unlistedExpiresAt ? new Date(existing.unlistedExpiresAt) : undefined,
});
} catch (e) {
console.error('[library] refreshUnlistedSnapshot failed', e);

View file

@ -102,6 +102,8 @@ export interface LocalLibraryEntry extends BaseRecord {
* when visibility moves back to anything else.
*/
unlistedToken?: string;
/** ISO timestamp when the unlisted snapshot expires; absent = never. */
unlistedExpiresAt?: string;
}
// ─── Domain Type (plaintext, for UI) ─────────────────────
@ -129,6 +131,8 @@ export interface LibraryEntry {
visibility: VisibilityLevel;
/** Server-issued share token. Empty when not 'unlisted'. */
unlistedToken: string;
/** ISO timestamp when the unlisted snapshot expires, or null = never. */
unlistedExpiresAt: string | null;
createdAt: string;
updatedAt: string;
}

View file

@ -28,6 +28,10 @@
await libraryEntriesStore.setVisibility(entry.id, 'space');
}
async function onExpiryChange(expiresAt: Date | null) {
await libraryEntriesStore.setUnlistedExpiry(entry.id, expiresAt);
}
const shareUrl = $derived.by(() => {
if (!entry.unlistedToken) return '';
const origin = typeof window === 'undefined' ? 'https://mana.how' : window.location.origin;
@ -167,8 +171,10 @@
<SharedLinkControls
token={entry.unlistedToken}
url={shareUrl}
expiresAt={entry.unlistedExpiresAt}
{onRegenerate}
{onRevoke}
{onExpiryChange}
/>
</dd>
{/if}

View file

@ -26,6 +26,7 @@ export function toPlace(local: LocalPlace): Place {
tagIds: local.tagIds ?? [],
visibility: local.visibility ?? 'space',
unlistedToken: local.unlistedToken ?? '',
unlistedExpiresAt: local.unlistedExpiresAt ?? null,
createdAt: local.createdAt ?? new Date().toISOString(),
updatedAt: local.updatedAt ?? new Date().toISOString(),
};

View file

@ -201,6 +201,7 @@ export const placesStore = {
blob,
});
patch.unlistedToken = token;
patch.unlistedExpiresAt = undefined;
} else if (before === 'unlisted') {
const jwt = await authStore.getValidToken();
if (jwt) {
@ -212,6 +213,7 @@ export const placesStore = {
});
}
patch.unlistedToken = undefined;
patch.unlistedExpiresAt = undefined;
}
await placeTable.update(id, patch);
@ -246,6 +248,7 @@ export const placesStore = {
recordId: id,
spaceId,
blob,
expiresAt: existing.unlistedExpiresAt ? new Date(existing.unlistedExpiresAt) : undefined,
});
await placeTable.update(id, {
unlistedToken: token,
@ -258,6 +261,33 @@ export const placesStore = {
}
},
async setUnlistedExpiry(id: string, expiresAt: Date | null) {
const existing = await placeTable.get(id);
if (!existing || existing.visibility !== 'unlisted') return;
const jwt = await authStore.getValidToken();
if (!jwt) return;
try {
const blob = await buildUnlistedBlob('places', id);
const spaceId =
(existing as unknown as { spaceId?: string }).spaceId ?? getActiveSpace()?.id ?? '';
await publishUnlistedSnapshot({
apiUrl: getManaApiUrl(),
jwt,
collection: 'places',
recordId: id,
spaceId,
blob,
expiresAt,
});
await placeTable.update(id, {
unlistedExpiresAt: expiresAt ? expiresAt.toISOString() : undefined,
updatedAt: new Date().toISOString(),
});
} catch (e) {
console.error('[places] setUnlistedExpiry failed', e);
}
},
async refreshUnlistedSnapshot(id: string) {
const existing = await placeTable.get(id);
if (!existing || existing.visibility !== 'unlisted') return;
@ -274,6 +304,7 @@ export const placesStore = {
recordId: id,
spaceId,
blob,
expiresAt: existing.unlistedExpiresAt ? new Date(existing.unlistedExpiresAt) : undefined,
});
} catch (e) {
console.error('[places] refreshUnlistedSnapshot failed', e);

View file

@ -23,6 +23,8 @@ export interface LocalPlace extends BaseRecord {
visibilityChangedAt?: string;
visibilityChangedBy?: string;
unlistedToken?: string;
/** ISO timestamp when the unlisted snapshot expires; absent = never. */
unlistedExpiresAt?: string;
}
export interface LocalLocationLog extends BaseRecord {
@ -54,6 +56,8 @@ export interface Place {
visibility: VisibilityLevel;
/** Server-issued share token. Empty when not 'unlisted'. */
unlistedToken: string;
/** ISO timestamp when the unlisted snapshot expires, or null = never. */
unlistedExpiresAt: string | null;
createdAt: string;
updatedAt: string;
}

View file

@ -171,6 +171,10 @@
await placesStore.setVisibility(placeId, 'space');
}
async function handleExpiryChange(expiresAt: Date | null) {
await placesStore.setUnlistedExpiry(placeId, expiresAt);
}
const shareUrl = $derived.by(() => {
const token = detail.entity?.unlistedToken;
if (!token) return '';
@ -261,8 +265,10 @@
<SharedLinkControls
token={place.unlistedToken}
url={shareUrl}
expiresAt={place.unlistedExpiresAt}
onRegenerate={handleRegenerate}
onRevoke={handleRevoke}
onExpiryChange={handleExpiryChange}
/>
</div>
{/if}

View file

@ -26,9 +26,11 @@
},
"dependencies": {
"@mana/shared-icons": "workspace:*",
"qrcode": "^1.5.4",
"zod": "^3.25.76"
},
"devDependencies": {
"@types/qrcode": "^1.5.5",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"typescript": "^5.7.3",

View file

@ -4,16 +4,14 @@
Shown below the VisibilityPicker in a module's DetailView when
`visibility === 'unlisted'` and a token exists. Dumb — owner (the
module DetailView) passes the token + handlers; this component only
renders the URL, manages copy-to-clipboard, and dispatches
regenerate/revoke/expiry actions.
QR-code rendering is a later polish (M8.5) — currently stubbed as
a button that disables itself. Expiry-picker is present but
minimal — datetime-local input, no fancy picker.
renders the URL, manages copy-to-clipboard, QR-code display, and
dispatches regenerate/revoke/expiry actions.
See docs/plans/unlisted-sharing.md §7.
-->
<script lang="ts">
import QRCode from 'qrcode';
let {
/** The 32-char token — used only for display fallback. */
token,
@ -47,6 +45,33 @@
let copied = $state(false);
let showConfirmRegenerate = $state(false);
let editingExpiry = $state(false);
let showQr = $state(false);
let qrSvg = $state<string | null>(null);
let qrError = $state<string | null>(null);
// Lazy-render QR. Re-runs whenever the URL changes (e.g. after
// regenerate) so the QR always tracks the live token.
$effect(() => {
if (!showQr) {
qrSvg = null;
qrError = null;
return;
}
QRCode.toString(url, {
type: 'svg',
errorCorrectionLevel: 'M',
margin: 1,
width: 220,
})
.then((svg) => {
qrSvg = svg;
qrError = null;
})
.catch((e: unknown) => {
qrError = e instanceof Error ? e.message : 'QR-Code-Generierung fehlgeschlagen';
qrSvg = null;
});
});
// datetime-local wants YYYY-MM-DDTHH:MM (no seconds, no zone).
const expiryInputValue = $derived.by(() => {
@ -117,6 +142,16 @@
</div>
<div class="slc__actions">
<button
type="button"
class="slc__btn"
onclick={() => (showQr = !showQr)}
{disabled}
aria-pressed={showQr}
title="QR-Code anzeigen"
>
🔗 QR
</button>
<button
type="button"
class="slc__btn"
@ -137,6 +172,20 @@
</button>
</div>
{#if showQr}
<div class="slc__qr">
{#if qrSvg}
<!-- QR is plain SVG returned by `qrcode/toString`, safe to inline. -->
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html qrSvg}
{:else if qrError}
<p class="slc__qr-error">{qrError}</p>
{:else}
<p class="slc__qr-loading">QR-Code wird erstellt …</p>
{/if}
</div>
{/if}
{#if onExpiryChange}
<div class="slc__expiry">
{#if editingExpiry || expiresAt}
@ -296,6 +345,32 @@
color: inherit;
font-size: 0.75rem;
}
.slc__qr {
display: flex;
justify-content: center;
padding: 0.5rem;
background: #ffffff;
border-radius: 0.5rem;
border: 1px solid rgba(0, 0, 0, 0.08);
}
.slc__qr :global(svg) {
display: block;
max-width: 220px;
width: 100%;
height: auto;
}
.slc__qr-loading,
.slc__qr-error {
margin: 0;
padding: 1rem;
font-size: 0.8125rem;
color: #6b7280;
text-align: center;
}
.slc__qr-error {
color: rgb(248, 113, 113);
}
.slc__token {
margin: 0;
font-family: ui-monospace, monospace;

355
pnpm-lock.yaml generated
View file

@ -141,14 +141,14 @@ importers:
version: link:../../../../packages/shared-landing-ui
astro:
specifier: ^5.16.0
version: 5.18.1(@azure/storage-blob@12.31.0)(@types/node@20.19.39)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)
version: 5.18.1(@azure/storage-blob@12.31.0)(@types/node@20.19.39)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)
typescript:
specifier: ^5.9.2
version: 5.9.3
devDependencies:
'@astrojs/tailwind':
specifier: ^6.0.2
version: 6.0.2(astro@5.18.1(@azure/storage-blob@12.31.0)(@types/node@20.19.39)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3))
version: 6.0.2(astro@5.18.1(@azure/storage-blob@12.31.0)(@types/node@20.19.39)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3))
'@tailwindcss/typography':
specifier: ^0.5.18
version: 0.5.19(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3))
@ -157,13 +157,13 @@ importers:
version: 20.19.39
eslint:
specifier: ^9.0.0
version: 9.39.4(jiti@1.21.7)
version: 9.39.4(jiti@2.6.1)
eslint-config-prettier:
specifier: ^9.1.0
version: 9.1.2(eslint@9.39.4(jiti@1.21.7))
version: 9.1.2(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-astro:
specifier: ^1.0.0
version: 1.6.0(eslint@9.39.4(jiti@1.21.7))
version: 1.6.0(eslint@9.39.4(jiti@2.6.1))
prettier:
specifier: ^3.6.2
version: 3.8.1
@ -256,10 +256,10 @@ importers:
version: 3.7.2
'@astrojs/tailwind':
specifier: ^6.0.0
version: 6.0.2(astro@5.18.1(@azure/storage-blob@12.31.0)(@types/node@24.12.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3))
version: 6.0.2(astro@5.18.1(@azure/storage-blob@12.31.0)(@types/node@24.12.2)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3))
astro:
specifier: ^5.16.11
version: 5.18.1(@azure/storage-blob@12.31.0)(@types/node@24.12.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)
version: 5.18.1(@azure/storage-blob@12.31.0)(@types/node@24.12.2)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)
tailwindcss:
specifier: ^3.4.17
version: 3.4.19(tsx@4.21.0)(yaml@2.8.3)
@ -2238,10 +2238,16 @@ importers:
'@mana/shared-icons':
specifier: workspace:*
version: link:../shared-icons
qrcode:
specifier: ^1.5.4
version: 1.5.4
zod:
specifier: ^3.25.76
version: 3.25.76
devDependencies:
'@types/qrcode':
specifier: ^1.5.5
version: 1.5.6
svelte:
specifier: ^5.0.0
version: 5.55.1
@ -17353,16 +17359,6 @@ snapshots:
transitivePeerDependencies:
- ts-node
'@astrojs/tailwind@6.0.2(astro@5.18.1(@azure/storage-blob@12.31.0)(@types/node@20.19.39)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3))':
dependencies:
astro: 5.18.1(@azure/storage-blob@12.31.0)(@types/node@20.19.39)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)
autoprefixer: 10.4.27(postcss@8.5.8)
postcss: 8.5.8
postcss-load-config: 4.0.2(postcss@8.5.8)
tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.8.3)
transitivePeerDependencies:
- ts-node
'@astrojs/tailwind@6.0.2(astro@5.18.1(@azure/storage-blob@12.31.0)(@types/node@20.19.39)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3))':
dependencies:
astro: 5.18.1(@azure/storage-blob@12.31.0)(@types/node@20.19.39)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)
@ -17383,6 +17379,16 @@ snapshots:
transitivePeerDependencies:
- ts-node
'@astrojs/tailwind@6.0.2(astro@5.18.1(@azure/storage-blob@12.31.0)(@types/node@24.12.2)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3))':
dependencies:
astro: 5.18.1(@azure/storage-blob@12.31.0)(@types/node@24.12.2)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)
autoprefixer: 10.4.27(postcss@8.5.8)
postcss: 8.5.8
postcss-load-config: 4.0.2(postcss@8.5.8)
tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.8.3)
transitivePeerDependencies:
- ts-node
'@astrojs/tailwind@6.0.2(astro@5.18.1(@azure/storage-blob@12.31.0)(@types/node@24.12.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3))':
dependencies:
astro: 5.18.1(@azure/storage-blob@12.31.0)(@types/node@24.12.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)
@ -19554,11 +19560,6 @@ snapshots:
'@esbuild/win32-x64@0.27.7':
optional: true
'@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@1.21.7))':
dependencies:
eslint: 9.39.4(jiti@1.21.7)
eslint-visitor-keys: 3.4.3
'@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1))':
dependencies:
eslint: 9.39.4(jiti@2.6.1)
@ -24638,108 +24639,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
astro@5.18.1(@azure/storage-blob@12.31.0)(@types/node@20.19.39)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3):
dependencies:
'@astrojs/compiler': 2.13.1
'@astrojs/internal-helpers': 0.7.6
'@astrojs/markdown-remark': 6.3.11
'@astrojs/telemetry': 3.3.0
'@capsizecss/unpack': 4.0.0
'@oslojs/encoding': 1.1.0
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
acorn: 8.16.0
aria-query: 5.3.2
axobject-query: 4.1.0
boxen: 8.0.1
ci-info: 4.4.0
clsx: 2.1.1
common-ancestor-path: 1.0.1
cookie: 1.1.1
cssesc: 3.0.0
debug: 4.4.3
deterministic-object-hash: 2.0.2
devalue: 5.7.0
diff: 8.0.4
dlv: 1.1.3
dset: 3.1.4
es-module-lexer: 1.7.0
esbuild: 0.27.7
estree-walker: 3.0.3
flattie: 1.1.1
fontace: 0.4.1
github-slugger: 2.0.0
html-escaper: 3.0.3
http-cache-semantics: 4.2.0
import-meta-resolve: 4.2.0
js-yaml: 4.1.1
magic-string: 0.30.21
magicast: 0.5.2
mrmime: 2.0.1
neotraverse: 0.6.18
p-limit: 6.2.0
p-queue: 8.1.1
package-manager-detector: 1.6.0
piccolore: 0.1.3
picomatch: 4.0.4
prompts: 2.4.2
rehype: 13.0.2
semver: 7.7.4
shiki: 3.23.0
smol-toml: 1.6.1
svgo: 4.0.1
tinyexec: 1.0.4
tinyglobby: 0.2.15
tsconfck: 3.1.6(typescript@5.9.3)
ultrahtml: 1.6.0
unifont: 0.7.4
unist-util-visit: 5.1.0
unstorage: 1.17.5(@azure/storage-blob@12.31.0)(ioredis@5.10.1)
vfile: 6.0.3
vite: 6.4.2(@types/node@20.19.39)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vitefu: 1.1.3(vite@6.4.2(@types/node@20.19.39)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
xxhash-wasm: 1.1.0
yargs-parser: 21.1.1
yocto-spinner: 0.2.3
zod: 3.25.76
zod-to-json-schema: 3.25.2(zod@3.25.76)
zod-to-ts: 1.2.0(typescript@5.9.3)(zod@3.25.76)
optionalDependencies:
sharp: 0.34.5
transitivePeerDependencies:
- '@azure/app-configuration'
- '@azure/cosmos'
- '@azure/data-tables'
- '@azure/identity'
- '@azure/keyvault-secrets'
- '@azure/storage-blob'
- '@capacitor/preferences'
- '@deno/kv'
- '@netlify/blobs'
- '@planetscale/database'
- '@types/node'
- '@upstash/redis'
- '@vercel/blob'
- '@vercel/functions'
- '@vercel/kv'
- aws4fetch
- db0
- idb-keyval
- ioredis
- jiti
- less
- lightningcss
- rollup
- sass
- sass-embedded
- stylus
- sugarss
- supports-color
- terser
- tsx
- typescript
- uploadthing
- yaml
astro@5.18.1(@azure/storage-blob@12.31.0)(@types/node@20.19.39)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3):
dependencies:
'@astrojs/compiler': 2.13.1
@ -24944,6 +24843,108 @@ snapshots:
- uploadthing
- yaml
astro@5.18.1(@azure/storage-blob@12.31.0)(@types/node@24.12.2)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3):
dependencies:
'@astrojs/compiler': 2.13.1
'@astrojs/internal-helpers': 0.7.6
'@astrojs/markdown-remark': 6.3.11
'@astrojs/telemetry': 3.3.0
'@capsizecss/unpack': 4.0.0
'@oslojs/encoding': 1.1.0
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
acorn: 8.16.0
aria-query: 5.3.2
axobject-query: 4.1.0
boxen: 8.0.1
ci-info: 4.4.0
clsx: 2.1.1
common-ancestor-path: 1.0.1
cookie: 1.1.1
cssesc: 3.0.0
debug: 4.4.3
deterministic-object-hash: 2.0.2
devalue: 5.7.0
diff: 8.0.4
dlv: 1.1.3
dset: 3.1.4
es-module-lexer: 1.7.0
esbuild: 0.27.7
estree-walker: 3.0.3
flattie: 1.1.1
fontace: 0.4.1
github-slugger: 2.0.0
html-escaper: 3.0.3
http-cache-semantics: 4.2.0
import-meta-resolve: 4.2.0
js-yaml: 4.1.1
magic-string: 0.30.21
magicast: 0.5.2
mrmime: 2.0.1
neotraverse: 0.6.18
p-limit: 6.2.0
p-queue: 8.1.1
package-manager-detector: 1.6.0
piccolore: 0.1.3
picomatch: 4.0.4
prompts: 2.4.2
rehype: 13.0.2
semver: 7.7.4
shiki: 3.23.0
smol-toml: 1.6.1
svgo: 4.0.1
tinyexec: 1.0.4
tinyglobby: 0.2.15
tsconfck: 3.1.6(typescript@5.9.3)
ultrahtml: 1.6.0
unifont: 0.7.4
unist-util-visit: 5.1.0
unstorage: 1.17.5(@azure/storage-blob@12.31.0)(ioredis@5.10.1)
vfile: 6.0.3
vite: 6.4.2(@types/node@24.12.2)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vitefu: 1.1.3(vite@6.4.2(@types/node@24.12.2)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
xxhash-wasm: 1.1.0
yargs-parser: 21.1.1
yocto-spinner: 0.2.3
zod: 3.25.76
zod-to-json-schema: 3.25.2(zod@3.25.76)
zod-to-ts: 1.2.0(typescript@5.9.3)(zod@3.25.76)
optionalDependencies:
sharp: 0.34.5
transitivePeerDependencies:
- '@azure/app-configuration'
- '@azure/cosmos'
- '@azure/data-tables'
- '@azure/identity'
- '@azure/keyvault-secrets'
- '@azure/storage-blob'
- '@capacitor/preferences'
- '@deno/kv'
- '@netlify/blobs'
- '@planetscale/database'
- '@types/node'
- '@upstash/redis'
- '@vercel/blob'
- '@vercel/functions'
- '@vercel/kv'
- aws4fetch
- db0
- idb-keyval
- ioredis
- jiti
- less
- lightningcss
- rollup
- sass
- sass-embedded
- stylus
- sugarss
- supports-color
- terser
- tsx
- typescript
- uploadthing
- yaml
astro@5.18.1(@azure/storage-blob@12.31.0)(@types/node@24.12.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3):
dependencies:
'@astrojs/compiler': 2.13.1
@ -26775,11 +26776,6 @@ snapshots:
eslint: 9.39.4(jiti@2.6.1)
semver: 7.7.4
eslint-compat-utils@0.6.5(eslint@9.39.4(jiti@1.21.7)):
dependencies:
eslint: 9.39.4(jiti@1.21.7)
semver: 7.7.4
eslint-compat-utils@0.6.5(eslint@9.39.4(jiti@2.6.1)):
dependencies:
eslint: 9.39.4(jiti@2.6.1)
@ -26789,10 +26785,6 @@ snapshots:
dependencies:
eslint: 9.39.4(jiti@2.6.1)
eslint-config-prettier@9.1.2(eslint@9.39.4(jiti@1.21.7)):
dependencies:
eslint: 9.39.4(jiti@1.21.7)
eslint-config-prettier@9.1.2(eslint@9.39.4(jiti@2.6.1)):
dependencies:
eslint: 9.39.4(jiti@2.6.1)
@ -26837,20 +26829,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-plugin-astro@1.6.0(eslint@9.39.4(jiti@1.21.7)):
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@1.21.7))
'@jridgewell/sourcemap-codec': 1.5.5
'@typescript-eslint/types': 8.58.0
astro-eslint-parser: 1.4.0
eslint: 9.39.4(jiti@1.21.7)
eslint-compat-utils: 0.6.5(eslint@9.39.4(jiti@1.21.7))
globals: 16.5.0
postcss: 8.5.8
postcss-selector-parser: 7.1.1
transitivePeerDependencies:
- supports-color
eslint-plugin-astro@1.6.0(eslint@9.39.4(jiti@2.6.1)):
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1))
@ -27024,47 +27002,6 @@ snapshots:
eslint-visitor-keys@5.0.1: {}
eslint@9.39.4(jiti@1.21.7):
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@1.21.7))
'@eslint-community/regexpp': 4.12.2
'@eslint/config-array': 0.21.2
'@eslint/config-helpers': 0.4.2
'@eslint/core': 0.17.0
'@eslint/eslintrc': 3.3.5
'@eslint/js': 9.39.4
'@eslint/plugin-kit': 0.4.1
'@humanfs/node': 0.16.7
'@humanwhocodes/module-importer': 1.0.1
'@humanwhocodes/retry': 0.4.3
'@types/estree': 1.0.8
ajv: 6.14.0
chalk: 4.1.2
cross-spawn: 7.0.6
debug: 4.4.3
escape-string-regexp: 4.0.0
eslint-scope: 8.4.0
eslint-visitor-keys: 4.2.1
espree: 10.4.0
esquery: 1.7.0
esutils: 2.0.3
fast-deep-equal: 3.1.3
file-entry-cache: 8.0.0
find-up: 5.0.0
glob-parent: 6.0.2
ignore: 5.3.2
imurmurhash: 0.1.4
is-glob: 4.0.3
json-stable-stringify-without-jsonify: 1.0.1
lodash.merge: 4.6.2
minimatch: 3.1.5
natural-compare: 1.4.0
optionator: 0.9.4
optionalDependencies:
jiti: 1.21.7
transitivePeerDependencies:
- supports-color
eslint@9.39.4(jiti@2.6.1):
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1))
@ -34182,23 +34119,6 @@ snapshots:
lightningcss: 1.32.0
terser: 5.46.1
vite@6.4.2(@types/node@20.19.39)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3):
dependencies:
esbuild: 0.25.12
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
postcss: 8.5.8
rollup: 4.60.1
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 20.19.39
fsevents: 2.3.3
jiti: 1.21.7
lightningcss: 1.32.0
terser: 5.46.1
tsx: 4.21.0
yaml: 2.8.3
vite@6.4.2(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3):
dependencies:
esbuild: 0.25.12
@ -34233,6 +34153,23 @@ snapshots:
tsx: 4.21.0
yaml: 2.8.3
vite@6.4.2(@types/node@24.12.2)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3):
dependencies:
esbuild: 0.25.12
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
postcss: 8.5.8
rollup: 4.60.1
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 24.12.2
fsevents: 2.3.3
jiti: 1.21.7
lightningcss: 1.32.0
terser: 5.46.1
tsx: 4.21.0
yaml: 2.8.3
vite@6.4.2(@types/node@24.12.2)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3):
dependencies:
esbuild: 0.25.12
@ -34250,10 +34187,6 @@ snapshots:
tsx: 4.21.0
yaml: 2.8.3
vitefu@1.1.3(vite@6.4.2(@types/node@20.19.39)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
optionalDependencies:
vite: 6.4.2(@types/node@20.19.39)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vitefu@1.1.3(vite@6.4.2(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
optionalDependencies:
vite: 6.4.2(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
@ -34262,6 +34195,10 @@ snapshots:
optionalDependencies:
vite: 6.4.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vitefu@1.1.3(vite@6.4.2(@types/node@24.12.2)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
optionalDependencies:
vite: 6.4.2(@types/node@24.12.2)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vitefu@1.1.3(vite@6.4.2(@types/node@24.12.2)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
optionalDependencies:
vite: 6.4.2(@types/node@24.12.2)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)