Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 92baa626d6 | |||
| cd7ef8dccc | |||
| 58efc66ca7 | |||
| 1e139fb86e | |||
| eeb713f03d | |||
| 837b5a326e | |||
| b1f37752d6 | |||
| 854c4b91a8 |
@@ -33,6 +33,10 @@ web-build/
|
|||||||
apps/desktop/out/
|
apps/desktop/out/
|
||||||
apps/desktop/release/
|
apps/desktop/release/
|
||||||
|
|
||||||
|
# Bundle visualizer reports
|
||||||
|
apps/desktop/stats.html
|
||||||
|
apps/desktop/stats.json
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
|
|||||||
@@ -10,6 +10,9 @@
|
|||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
|
import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
import { visualizer } from 'rollup-plugin-visualizer';
|
||||||
|
|
||||||
|
const ANALYZE = process.env.ANALYZE === 'true';
|
||||||
|
|
||||||
const rendererAliases = {
|
const rendererAliases = {
|
||||||
'@': path.resolve(__dirname, './src'),
|
'@': path.resolve(__dirname, './src'),
|
||||||
@@ -44,7 +47,30 @@ export default defineConfig({
|
|||||||
// bundle. Relative base produces `./assets/...` which works in both
|
// bundle. Relative base produces `./assets/...` which works in both
|
||||||
// dev (served from /) and packaged builds.
|
// dev (served from /) and packaged builds.
|
||||||
base: './',
|
base: './',
|
||||||
plugins: [react()],
|
// Visualizer plugins are gated behind ANALYZE=true so the production
|
||||||
|
// build never pays the analysis cost. Re-enable with:
|
||||||
|
// ANALYZE=true pnpm --filter @chat-app/desktop build
|
||||||
|
// which writes apps/desktop/stats.html (treemap) + stats.json (raw).
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
...(ANALYZE
|
||||||
|
? [
|
||||||
|
visualizer({
|
||||||
|
filename: 'stats.html',
|
||||||
|
template: 'treemap',
|
||||||
|
gzipSize: true,
|
||||||
|
brotliSize: true,
|
||||||
|
open: false,
|
||||||
|
}),
|
||||||
|
visualizer({
|
||||||
|
filename: 'stats.json',
|
||||||
|
template: 'raw-data',
|
||||||
|
gzipSize: true,
|
||||||
|
brotliSize: true,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: rendererAliases,
|
alias: rendererAliases,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -147,6 +147,17 @@ async function createWindow(): Promise<BrowserWindow> {
|
|||||||
attachWindowState(win, WINDOW_STATE_FILE);
|
attachWindowState(win, WINDOW_STATE_FILE);
|
||||||
|
|
||||||
if (!app.isPackaged) {
|
if (!app.isPackaged) {
|
||||||
|
// Auto-open DevTools in dev — the menu bar is stripped (Discord-style)
|
||||||
|
// so F12 / Ctrl+Shift+I have no chord; opening detached gives a
|
||||||
|
// separate inspector window for easy debugging.
|
||||||
|
win.webContents.openDevTools({ mode: 'detach' });
|
||||||
|
// Forward renderer console messages to the main-process stdout so
|
||||||
|
// errors during local dev are visible in the terminal too (helps when
|
||||||
|
// the inspector isn't focused).
|
||||||
|
win.webContents.on('console-message', (_event, level, message, line, sourceId) => {
|
||||||
|
const tag = level === 3 ? 'error' : level === 2 ? 'warn' : level === 1 ? 'log' : 'info';
|
||||||
|
console.log('[renderer ' + tag + ']', message, '(' + sourceId + ':' + line + ')');
|
||||||
|
});
|
||||||
await win.loadURL(DEV_URL);
|
await win.loadURL(DEV_URL);
|
||||||
} else {
|
} else {
|
||||||
await win.loadFile(resolveRendererIndex());
|
await win.loadFile(resolveRendererIndex());
|
||||||
|
|||||||
@@ -6,8 +6,10 @@
|
|||||||
// (<1ms per op for the current workload).
|
// (<1ms per op for the current workload).
|
||||||
//
|
//
|
||||||
// Binding param style: Tauri's plugin-sql used $1, $2... positionals with
|
// Binding param style: Tauri's plugin-sql used $1, $2... positionals with
|
||||||
// bindings as an array. SQLite natively accepts $N so existing queries
|
// bindings as an array. SQLite parses `$NAME` as a NAMED parameter
|
||||||
// keep working unmodified.
|
// (NAME = `1`, `2`, …), not as positional, so better-sqlite3 wants the
|
||||||
|
// bindings as `{ '1': v1, '2': v2 }` not `[v1, v2]`. We accept the old
|
||||||
|
// array-shape from callers and convert to the named-object on the way in.
|
||||||
|
|
||||||
import { app, ipcMain } from 'electron';
|
import { app, ipcMain } from 'electron';
|
||||||
import Database from 'better-sqlite3';
|
import Database from 'better-sqlite3';
|
||||||
@@ -40,6 +42,20 @@ function requireHandle(h: string): Handle {
|
|||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert a positional bindings array `[v1, v2]` to the named-params object
|
||||||
|
// `{ '1': v1, '2': v2 }` that better-sqlite3 needs when the SQL uses
|
||||||
|
// `$1`/`$2` named placeholders. Returns the original array (spread later)
|
||||||
|
// when it's empty.
|
||||||
|
function bindParams(bindings: unknown[] | undefined): Record<string, unknown> | [] {
|
||||||
|
const arr = bindings ?? [];
|
||||||
|
if (arr.length === 0) return [];
|
||||||
|
const obj: Record<string, unknown> = {};
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
obj[String(i + 1)] = arr[i];
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
export function register(): void {
|
export function register(): void {
|
||||||
ipcMain.handle(CHANNELS.SQL_LOAD, async (_evt, args: SqlLoadArgs): Promise<string> => {
|
ipcMain.handle(CHANNELS.SQL_LOAD, async (_evt, args: SqlLoadArgs): Promise<string> => {
|
||||||
const rawName = stripPrefix(args.name);
|
const rawName = stripPrefix(args.name);
|
||||||
@@ -59,7 +75,8 @@ export function register(): void {
|
|||||||
async (_evt, args: SqlExecuteArgs): Promise<SqlExecuteResult> => {
|
async (_evt, args: SqlExecuteArgs): Promise<SqlExecuteResult> => {
|
||||||
const entry = requireHandle(args.handle);
|
const entry = requireHandle(args.handle);
|
||||||
const stmt = entry.db.prepare(args.query);
|
const stmt = entry.db.prepare(args.query);
|
||||||
const info = stmt.run(...((args.bindings ?? []) as unknown[]));
|
const params = bindParams(args.bindings);
|
||||||
|
const info = Array.isArray(params) ? stmt.run() : stmt.run(params);
|
||||||
return {
|
return {
|
||||||
rowsAffected: info.changes,
|
rowsAffected: info.changes,
|
||||||
lastInsertId:
|
lastInsertId:
|
||||||
@@ -75,7 +92,11 @@ export function register(): void {
|
|||||||
async (_evt, args: SqlSelectArgs): Promise<SqlSelectResult> => {
|
async (_evt, args: SqlSelectArgs): Promise<SqlSelectResult> => {
|
||||||
const entry = requireHandle(args.handle);
|
const entry = requireHandle(args.handle);
|
||||||
const stmt = entry.db.prepare(args.query);
|
const stmt = entry.db.prepare(args.query);
|
||||||
const rows = stmt.all(...((args.bindings ?? []) as unknown[])) as Record<string, unknown>[];
|
const params = bindParams(args.bindings);
|
||||||
|
const rows = (Array.isArray(params) ? stmt.all() : stmt.all(params)) as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>[];
|
||||||
return rows;
|
return rows;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
"electron-vite": "^2.3.0",
|
"electron-vite": "^2.3.0",
|
||||||
"postcss": "^8.4.49",
|
"postcss": "^8.4.49",
|
||||||
"rimraf": "^6.0.0",
|
"rimraf": "^6.0.0",
|
||||||
|
"rollup-plugin-visualizer": "^7.0.1",
|
||||||
"tailwindcss": "^3.4.15",
|
"tailwindcss": "^3.4.15",
|
||||||
"vite": "^5.4.11"
|
"vite": "^5.4.11"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
downloadAndDecryptAttachment,
|
downloadAndDecryptAttachment,
|
||||||
downloadAndDecryptAttachmentThumb,
|
downloadAndDecryptAttachmentThumb,
|
||||||
} from '@chat-app/shared/chat';
|
} from '@chat-app/shared/chat';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { getCachedAttachment, putCachedAttachment } from '../lib/attachmentCache';
|
import { getCachedAttachment, putCachedAttachment } from '../lib/attachmentCache';
|
||||||
import { supabase } from '../lib/supabase';
|
import { supabase } from '../lib/supabase';
|
||||||
@@ -166,11 +166,21 @@ export function AttachmentImage({ handle, mine = false }: Props) {
|
|||||||
// skipped the eager full-blob download above). Resolves into the same
|
// skipped the eager full-blob download above). Resolves into the same
|
||||||
// `fullUrl` state that the Lightbox consumes; the thumb URL keeps
|
// `fullUrl` state that the Lightbox consumes; the thumb URL keeps
|
||||||
// backing the bubble until the lightbox actually mounts.
|
// backing the bubble until the lightbox actually mounts.
|
||||||
|
//
|
||||||
|
// CRITICAL: do NOT revoke the just-created blob URL in this effect's
|
||||||
|
// cleanup. Setting `fullUrl` re-triggers the effect (state change → re-
|
||||||
|
// run → previous cleanup fires → URL revoked → Lightbox renders
|
||||||
|
// referenced-but-revoked URL → "ERR_FILE_NOT_FOUND"). The dedicated
|
||||||
|
// unmount-only effect below tracks the current URL via ref and revokes
|
||||||
|
// it once when the component truly leaves the tree.
|
||||||
|
//
|
||||||
|
// Deps locked to `handle.id` (not `handle`) — handles are immutable per
|
||||||
|
// attachment id, so object-identity churn from parent re-renders must
|
||||||
|
// not re-trigger the fetch.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!lightboxOpen) return;
|
if (!lightboxOpen) return;
|
||||||
if (fullUrl) return;
|
if (fullUrl) return;
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
const created: string[] = [];
|
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const cached = await getCachedAttachment(handle.id);
|
const cached = await getCachedAttachment(handle.id);
|
||||||
let blob: Blob;
|
let blob: Blob;
|
||||||
@@ -187,14 +197,27 @@ export function AttachmentImage({ handle, mine = false }: Props) {
|
|||||||
}
|
}
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
const u = URL.createObjectURL(blob);
|
const u = URL.createObjectURL(blob);
|
||||||
created.push(u);
|
|
||||||
setFullUrl(u);
|
setFullUrl(u);
|
||||||
})();
|
})();
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
for (const u of created) URL.revokeObjectURL(u);
|
|
||||||
};
|
};
|
||||||
}, [lightboxOpen, fullUrl, handle]);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [lightboxOpen, handle.id]);
|
||||||
|
|
||||||
|
// Track the currently-published fullUrl in a ref so the unmount-only
|
||||||
|
// cleanup below can revoke whatever URL is live at teardown time
|
||||||
|
// without subscribing to fullUrl changes (which would re-trigger and
|
||||||
|
// revoke prematurely — see the comment above the fetch effect).
|
||||||
|
const fullUrlRef = useRef<string | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
fullUrlRef.current = fullUrl;
|
||||||
|
}, [fullUrl]);
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (fullUrlRef.current) URL.revokeObjectURL(fullUrlRef.current);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const blobUrl = thumbUrl ?? fullUrl;
|
const blobUrl = thumbUrl ?? fullUrl;
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
|
|||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { useConversationsContext } from '../context/ConversationsContext';
|
||||||
import { supabase } from '../lib/supabase';
|
import { supabase } from '../lib/supabase';
|
||||||
import { ArchiveIcon, AtIcon, BellIcon, BellOffIcon, MoreVerticalIcon } from './icons';
|
import { ArchiveIcon, AtIcon, BellIcon, BellOffIcon, MoreVerticalIcon } from './icons';
|
||||||
|
|
||||||
@@ -54,6 +55,7 @@ interface MenuPos {
|
|||||||
// under the trigger so it doesn't push off-screen on narrow windows.
|
// under the trigger so it doesn't push off-screen on narrow windows.
|
||||||
export function ConversationRowMenu({ conversationId, archived, mutedUntil, mentionsOnly }: Props) {
|
export function ConversationRowMenu({ conversationId, archived, mutedUntil, mentionsOnly }: Props) {
|
||||||
const { t } = useTranslation(['app']);
|
const { t } = useTranslation(['app']);
|
||||||
|
const { patchConversation } = useConversationsContext();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [submenuOpen, setSubmenuOpen] = useState<'mute' | null>(null);
|
const [submenuOpen, setSubmenuOpen] = useState<'mute' | null>(null);
|
||||||
const [menuPos, setMenuPos] = useState<MenuPos | null>(null);
|
const [menuPos, setMenuPos] = useState<MenuPos | null>(null);
|
||||||
@@ -122,44 +124,59 @@ export function ConversationRowMenu({ conversationId, archived, mutedUntil, ment
|
|||||||
const isMuted =
|
const isMuted =
|
||||||
mutedUntil !== null && new Date(mutedUntil).getTime() > Date.now();
|
mutedUntil !== null && new Date(mutedUntil).getTime() > Date.now();
|
||||||
|
|
||||||
|
// Optimistic updates: flip the local state before the server RPC so the
|
||||||
|
// bell / archive icon / checkmark update on the same frame as the click.
|
||||||
|
// Realtime echo via ConversationsContext will reconcile (no-op since the
|
||||||
|
// optimistic patch already matches the server row). On error we restore
|
||||||
|
// the previous value so the menu doesn't lie about persisted state.
|
||||||
const handleArchive = useCallback(
|
const handleArchive = useCallback(
|
||||||
async (next: boolean) => {
|
async (next: boolean) => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
const previous = archived;
|
||||||
|
patchConversation(conversationId, { archived: next });
|
||||||
try {
|
try {
|
||||||
await setConversationArchived(supabase, conversationId, next);
|
await setConversationArchived(supabase, conversationId, next);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
|
patchConversation(conversationId, { archived: previous });
|
||||||
console.error('archive toggle failed', err);
|
console.error('archive toggle failed', err);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[conversationId],
|
[conversationId, archived, patchConversation],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleMute = useCallback(
|
const handleMute = useCallback(
|
||||||
async (minutes: number | null) => {
|
async (minutes: number | null) => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
setSubmenuOpen(null);
|
setSubmenuOpen(null);
|
||||||
|
const previous = mutedUntil;
|
||||||
|
const nextIso = muteDurationToIso(minutes);
|
||||||
|
patchConversation(conversationId, { mutedUntil: nextIso });
|
||||||
try {
|
try {
|
||||||
await setConversationMutedUntil(supabase, conversationId, muteDurationToIso(minutes));
|
await setConversationMutedUntil(supabase, conversationId, nextIso);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
|
patchConversation(conversationId, { mutedUntil: previous });
|
||||||
console.error('mute toggle failed', err);
|
console.error('mute toggle failed', err);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[conversationId],
|
[conversationId, mutedUntil, patchConversation],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleMentionsOnly = useCallback(
|
const handleMentionsOnly = useCallback(
|
||||||
async (next: boolean) => {
|
async (next: boolean) => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
const previous = mentionsOnly;
|
||||||
|
patchConversation(conversationId, { mentionsOnly: next });
|
||||||
try {
|
try {
|
||||||
await setConversationMentionsOnly(supabase, {
|
await setConversationMentionsOnly(supabase, {
|
||||||
conversationId,
|
conversationId,
|
||||||
mentionsOnly: next,
|
mentionsOnly: next,
|
||||||
});
|
});
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
|
patchConversation(conversationId, { mentionsOnly: previous });
|
||||||
console.warn('mentions-only toggle failed', err);
|
console.warn('mentions-only toggle failed', err);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[conversationId],
|
[conversationId, mentionsOnly, patchConversation],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -41,20 +41,42 @@ export function ImageAnnotator({ file, onCancel, onSave }: Props) {
|
|||||||
const draftRef = useRef<AnnotatorOp | null>(null);
|
const draftRef = useRef<AnnotatorOp | null>(null);
|
||||||
const [draftTick, setDraftTick] = useState(0);
|
const [draftTick, setDraftTick] = useState(0);
|
||||||
|
|
||||||
|
// Hold a stable ref to onCancel so the image-load effect doesn't depend
|
||||||
|
// on its identity. Without this, parents that pass an inline `() => …`
|
||||||
|
// re-render the modal on every keystroke / state change, the effect re-
|
||||||
|
// runs, the previous URL.createObjectURL gets revoked WHILE the new img
|
||||||
|
// is still decoding → img.onerror fires ("file not found") → onCancel →
|
||||||
|
// modal flashes open + closes instantly.
|
||||||
|
const onCancelRef = useRef(onCancel);
|
||||||
|
useEffect(() => { onCancelRef.current = onCancel; }, [onCancel]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// React 18 strict mode in dev double-mounts effects to test idempotency.
|
||||||
|
// The first run creates a blob URL, sets img.src, returns a cleanup
|
||||||
|
// that revokes — and the cleanup fires BEFORE the (still-in-flight)
|
||||||
|
// image fetch completes. The browser then emits ERR_FILE_NOT_FOUND for
|
||||||
|
// the revoked URL → img.onerror → modal closes instantly. The
|
||||||
|
// `cancelled` flag guards every callback so a torn-down run can't
|
||||||
|
// close the modal that the second mount just opened.
|
||||||
|
let cancelled = false;
|
||||||
const url = URL.createObjectURL(file);
|
const url = URL.createObjectURL(file);
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
|
if (cancelled) return;
|
||||||
imageRef.current = img;
|
imageRef.current = img;
|
||||||
setImageLoaded(true);
|
setImageLoaded(true);
|
||||||
};
|
};
|
||||||
img.onerror = () => {
|
img.onerror = () => {
|
||||||
|
if (cancelled) return;
|
||||||
console.error('ImageAnnotator: failed to decode source image');
|
console.error('ImageAnnotator: failed to decode source image');
|
||||||
onCancel();
|
onCancelRef.current();
|
||||||
};
|
};
|
||||||
img.src = url;
|
img.src = url;
|
||||||
return () => URL.revokeObjectURL(url);
|
return () => {
|
||||||
}, [file, onCancel]);
|
cancelled = true;
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
}, [file]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!imageLoaded) return;
|
if (!imageLoaded) return;
|
||||||
|
|||||||
@@ -54,6 +54,15 @@ interface ConversationsContextValue {
|
|||||||
refresh: () => Promise<void>;
|
refresh: () => Promise<void>;
|
||||||
markRead: (conversationId: string) => void;
|
markRead: (conversationId: string) => void;
|
||||||
setActiveConversation: (conversationId: string | null) => void;
|
setActiveConversation: (conversationId: string | null) => void;
|
||||||
|
// Optimistic patch for the caller's per-membership preferences (mute /
|
||||||
|
// mentions-only / archive). Mutations to `conversation_members` echo back
|
||||||
|
// via the realtime channel and `refresh()` reconciles canonically, but the
|
||||||
|
// ~100-200ms roundtrip leaves the UI looking unresponsive. Callers patch
|
||||||
|
// immediately, snapshot the previous state, and roll back on failure.
|
||||||
|
patchConversation: (
|
||||||
|
conversationId: string,
|
||||||
|
patch: Partial<Pick<ConversationSummary, 'archived' | 'mutedUntil' | 'mentionsOnly'>>,
|
||||||
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConversationsContext = createContext<ConversationsContextValue | null>(null);
|
const ConversationsContext = createContext<ConversationsContextValue | null>(null);
|
||||||
@@ -164,6 +173,19 @@ export function ConversationsProvider({ children }: { children: ReactNode }) {
|
|||||||
[markRead],
|
[markRead],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const patchConversation = useCallback<ConversationsContextValue['patchConversation']>(
|
||||||
|
(convId, patch) => {
|
||||||
|
setConversations((prev) => {
|
||||||
|
const idx = prev.findIndex((c) => c.id === convId);
|
||||||
|
if (idx === -1) return prev;
|
||||||
|
const next = [...prev];
|
||||||
|
next[idx] = { ...next[idx]!, ...patch };
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!myId) {
|
if (!myId) {
|
||||||
setConversations([]);
|
setConversations([]);
|
||||||
@@ -308,6 +330,7 @@ export function ConversationsProvider({ children }: { children: ReactNode }) {
|
|||||||
refresh,
|
refresh,
|
||||||
markRead,
|
markRead,
|
||||||
setActiveConversation,
|
setActiveConversation,
|
||||||
|
patchConversation,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
conversations,
|
conversations,
|
||||||
@@ -318,6 +341,7 @@ export function ConversationsProvider({ children }: { children: ReactNode }) {
|
|||||||
refresh,
|
refresh,
|
||||||
markRead,
|
markRead,
|
||||||
setActiveConversation,
|
setActiveConversation,
|
||||||
|
patchConversation,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -64,10 +64,37 @@ export function useOwnDevices(): {
|
|||||||
|
|
||||||
const revoke = useCallback(
|
const revoke = useCallback(
|
||||||
async (deviceId: string) => {
|
async (deviceId: string) => {
|
||||||
|
// Optimistic: flip `revokedAt` on the row so the "Abgemeldet" badge
|
||||||
|
// appears on the same frame as the click. Capture a snapshot so we
|
||||||
|
// can restore exactly on RPC failure (the realtime subscription's
|
||||||
|
// own UPDATE echo would otherwise reconcile back to "not revoked"
|
||||||
|
// anyway). Skip if the row isn't in our list — nothing to undo.
|
||||||
|
let snapshot: DeviceRecord[] | null = null;
|
||||||
|
const stampedAt = new Date().toISOString();
|
||||||
|
setState((prev) => {
|
||||||
|
if (!prev.devices.some((d) => d.id === deviceId)) return prev;
|
||||||
|
snapshot = prev.devices;
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
devices: prev.devices.map((d) =>
|
||||||
|
d.id === deviceId ? { ...d, revokedAt: d.revokedAt ?? stampedAt } : d,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
try {
|
||||||
await revokeDevice(supabase, deviceId);
|
await revokeDevice(supabase, deviceId);
|
||||||
await refresh();
|
// Skip the eager refresh: the realtime UPDATE on `devices` triggers
|
||||||
|
// refresh() via the subscription and the optimistic row already
|
||||||
|
// shows the badge. Avoids a list flicker between optimistic and
|
||||||
|
// canonical state.
|
||||||
|
} catch (err) {
|
||||||
|
if (snapshot) {
|
||||||
|
setState((prev) => ({ ...prev, devices: snapshot! }));
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[refresh],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
return { devices: state.devices, loading: state.loading, error: state.error, refresh, revoke };
|
return { devices: state.devices, loading: state.loading, error: state.error, refresh, revoke };
|
||||||
|
|||||||
@@ -1,12 +1,26 @@
|
|||||||
import { listPinnedMessages, type PinnedMessage } from '@chat-app/shared/chat';
|
import { listPinnedMessages, type PinnedMessage } from '@chat-app/shared/chat';
|
||||||
import { useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { supabase } from './supabase';
|
import { supabase } from './supabase';
|
||||||
|
|
||||||
|
export interface UsePinnedMessagesResult {
|
||||||
|
pins: PinnedMessage[];
|
||||||
|
// Optimistic insert. Caller flips the UI immediately; server insert +
|
||||||
|
// realtime echo will reconcile (dedup'd by messageId). Returns the
|
||||||
|
// previous snapshot so the caller can roll back on error.
|
||||||
|
applyOptimisticPin: (messageId: string, pinnedBy: string) => PinnedMessage[];
|
||||||
|
applyOptimisticUnpin: (messageId: string) => PinnedMessage[];
|
||||||
|
// Hard restore for rollback after a failed server call.
|
||||||
|
restorePins: (snapshot: PinnedMessage[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
// Live list of pinned messages for one conversation. Subscribes to the
|
// Live list of pinned messages for one conversation. Subscribes to the
|
||||||
// `pinned_messages` realtime channel for the conv so the header pill +
|
// `pinned_messages` realtime channel for the conv so the header pill +
|
||||||
// side-panel update without a refetch.
|
// side-panel update without a refetch. The `applyOptimistic*` helpers let
|
||||||
export function usePinnedMessages(conversationId: string | undefined): PinnedMessage[] {
|
// callers flip local state synchronously on user action so the pin button
|
||||||
|
// doesn't appear unresponsive while the ~100-200ms server roundtrip + the
|
||||||
|
// realtime refetch round complete.
|
||||||
|
export function usePinnedMessages(conversationId: string | undefined): UsePinnedMessagesResult {
|
||||||
const [pins, setPins] = useState<PinnedMessage[]>([]);
|
const [pins, setPins] = useState<PinnedMessage[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -44,5 +58,42 @@ export function usePinnedMessages(conversationId: string | undefined): PinnedMes
|
|||||||
};
|
};
|
||||||
}, [conversationId]);
|
}, [conversationId]);
|
||||||
|
|
||||||
return pins;
|
const applyOptimisticPin = useCallback<UsePinnedMessagesResult['applyOptimisticPin']>(
|
||||||
|
(messageId, pinnedBy) => {
|
||||||
|
if (!conversationId) return pins;
|
||||||
|
let snapshot: PinnedMessage[] = pins;
|
||||||
|
setPins((prev) => {
|
||||||
|
snapshot = prev;
|
||||||
|
if (prev.some((p) => p.messageId === messageId)) return prev;
|
||||||
|
const optimistic: PinnedMessage = {
|
||||||
|
conversationId,
|
||||||
|
messageId,
|
||||||
|
pinnedBy,
|
||||||
|
pinnedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
// Newest first matches the listPinnedMessages order.
|
||||||
|
return [optimistic, ...prev];
|
||||||
|
});
|
||||||
|
return snapshot;
|
||||||
|
},
|
||||||
|
[conversationId, pins],
|
||||||
|
);
|
||||||
|
|
||||||
|
const applyOptimisticUnpin = useCallback<UsePinnedMessagesResult['applyOptimisticUnpin']>(
|
||||||
|
(messageId) => {
|
||||||
|
let snapshot: PinnedMessage[] = pins;
|
||||||
|
setPins((prev) => {
|
||||||
|
snapshot = prev;
|
||||||
|
return prev.filter((p) => p.messageId !== messageId);
|
||||||
|
});
|
||||||
|
return snapshot;
|
||||||
|
},
|
||||||
|
[pins],
|
||||||
|
);
|
||||||
|
|
||||||
|
const restorePins = useCallback<UsePinnedMessagesResult['restorePins']>((snapshot) => {
|
||||||
|
setPins(snapshot);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { pins, applyOptimisticPin, applyOptimisticUnpin, restorePins };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -241,24 +241,34 @@ export function ConversationPage() {
|
|||||||
// Auto-clears on a successful send so the composer doesn't accidentally
|
// Auto-clears on a successful send so the composer doesn't accidentally
|
||||||
// burn the message-after-next.
|
// burn the message-after-next.
|
||||||
const [viewOnceNext, setViewOnceNext] = useState(false);
|
const [viewOnceNext, setViewOnceNext] = useState(false);
|
||||||
const pins = usePinnedMessages(id);
|
const { pins, applyOptimisticPin, applyOptimisticUnpin, restorePins } = usePinnedMessages(id);
|
||||||
const [pinnedPanelOpen, setPinnedPanelOpen] = useState(false);
|
const [pinnedPanelOpen, setPinnedPanelOpen] = useState(false);
|
||||||
const pinnedIds = useMemo(() => new Set(pins.map((p) => p.messageId)), [pins]);
|
const pinnedIds = useMemo(() => new Set(pins.map((p) => p.messageId)), [pins]);
|
||||||
|
|
||||||
|
// Optimistic pin/unpin: flip the local list synchronously so the pin badge
|
||||||
|
// / panel updates on the same frame as the click. Realtime echo via
|
||||||
|
// usePinnedMessages will refetch and reconcile (no-op since the optimistic
|
||||||
|
// row matches the server). On error we restore the snapshot so the badge
|
||||||
|
// doesn't lie about persisted state.
|
||||||
const handleTogglePin = useCallback(
|
const handleTogglePin = useCallback(
|
||||||
async (messageId: string) => {
|
async (messageId: string) => {
|
||||||
if (!id || !myId) return;
|
if (!id || !myId) return;
|
||||||
|
const wasPinned = pinnedIds.has(messageId);
|
||||||
|
const snapshot = wasPinned
|
||||||
|
? applyOptimisticUnpin(messageId)
|
||||||
|
: applyOptimisticPin(messageId, myId);
|
||||||
try {
|
try {
|
||||||
if (pinnedIds.has(messageId)) {
|
if (wasPinned) {
|
||||||
await unpinMessage(supabase, id, messageId);
|
await unpinMessage(supabase, id, messageId);
|
||||||
} else {
|
} else {
|
||||||
await pinMessage(supabase, id, messageId, myId);
|
await pinMessage(supabase, id, messageId, myId);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
restorePins(snapshot);
|
||||||
console.warn('pin toggle failed', err);
|
console.warn('pin toggle failed', err);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[id, myId, pinnedIds],
|
[id, myId, pinnedIds, applyOptimisticPin, applyOptimisticUnpin, restorePins],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleGifPick = useCallback(
|
const handleGifPick = useCallback(
|
||||||
@@ -424,6 +434,22 @@ export function ConversationPage() {
|
|||||||
return out;
|
return out;
|
||||||
}, [messages, pending, displayCount]);
|
}, [messages, pending, displayCount]);
|
||||||
|
|
||||||
|
// Snapshot of the saved position for this conversation, captured once on
|
||||||
|
// mount. Used to derive the `initialTopMostItemIndex` we hand to the
|
||||||
|
// Virtuoso instance below — Virtuoso applies that index synchronously
|
||||||
|
// before its first paint, so re-entering a chat shows the saved row in
|
||||||
|
// one frame rather than a "starts at top, jumps" flicker.
|
||||||
|
//
|
||||||
|
// Declared HERE (above `initialTopMostIndex`) rather than further down
|
||||||
|
// because the useMemo that consumes it would otherwise hit a TDZ on
|
||||||
|
// first render — `const` refs aren't hoisted.
|
||||||
|
const savedPositionRef = useRef<{ topmostIndex: number; stickToBottom: boolean } | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
if (savedPositionRef.current === null && id) {
|
||||||
|
savedPositionRef.current = scrollPositions.get(id) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
// Initial scroll position for the freshly-mounted Virtuoso instance.
|
// Initial scroll position for the freshly-mounted Virtuoso instance.
|
||||||
// Default = bottom (newest message). If we have a saved position from a
|
// Default = bottom (newest message). If we have a saved position from a
|
||||||
// previous visit to this chat AND the user wasn't sticking to the
|
// previous visit to this chat AND the user wasn't sticking to the
|
||||||
@@ -638,17 +664,8 @@ export function ConversationPage() {
|
|||||||
// The old useLayoutEffect that wrote `scrollTop = scrollHeight` is no
|
// The old useLayoutEffect that wrote `scrollTop = scrollHeight` is no
|
||||||
// longer needed: Virtuoso owns scroll positioning now.
|
// longer needed: Virtuoso owns scroll positioning now.
|
||||||
|
|
||||||
// Snapshot of the saved position for this conversation, captured once on
|
// (savedPositionRef declared earlier — see TDZ note above the
|
||||||
// mount. Used to derive the `initialTopMostItemIndex` we hand to the
|
// initialTopMostIndex useMemo.)
|
||||||
// Virtuoso instance below — Virtuoso applies that index synchronously
|
|
||||||
// before its first paint, so re-entering a chat shows the saved row in
|
|
||||||
// one frame rather than a "starts at top, jumps" flicker.
|
|
||||||
const savedPositionRef = useRef<{ topmostIndex: number; stickToBottom: boolean } | null>(
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
if (savedPositionRef.current === null && id) {
|
|
||||||
savedPositionRef.current = scrollPositions.get(id) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track whether the user is currently scrolled to the bottom. Virtuoso
|
// Track whether the user is currently scrolled to the bottom. Virtuoso
|
||||||
// calls this whenever the bottom-state changes; we feed it into
|
// calls this whenever the bottom-state changes; we feed it into
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
# Phase 7 — Composer Redesign (Hybrid)
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development. Steps use checkbox (`- [ ]`) syntax.
|
||||||
|
|
||||||
|
**Goal:** Reduce composer toolbar from 9 cluttered icons to 5 hierarchically-organized buttons. Move "creative activities" (Whiteboard, Watch-Together, Mini-Games) into a `+` popover. Move View-Once from global composer toggle to per-attachment flag in the upload preview.
|
||||||
|
|
||||||
|
**Rollback anchor:** tag `pre-phase7-composer` (set in T1).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: Rollback anchor
|
||||||
|
|
||||||
|
- [ ] Run:
|
||||||
|
```bash
|
||||||
|
cd "D:\Programmieren\ChatApp-Electron\chat-app"
|
||||||
|
git tag -a pre-phase7-composer -m "Rollback anchor before Phase 7 composer redesign"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: `<ComposerActionsMenu>` popover component
|
||||||
|
|
||||||
|
**Files:** Create `apps/desktop/src/components/ComposerActionsMenu.tsx`
|
||||||
|
|
||||||
|
**Shape:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
interface Props {
|
||||||
|
anchorRef: React.RefObject<HTMLButtonElement | null>;
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onAttachFile: () => void;
|
||||||
|
onCreatePoll: () => void;
|
||||||
|
onCreateWhiteboard: () => void;
|
||||||
|
onStartWatchTogether: () => void;
|
||||||
|
onStartGame: () => void;
|
||||||
|
canStartGame?: boolean;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Layout (floating panel anchored above `anchorRef`):
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────┐
|
||||||
|
│ 📎 Bild / Datei │
|
||||||
|
│ 📊 Umfrage │
|
||||||
|
├─────────────────────────────┤
|
||||||
|
│ AKTIVITÄTEN │
|
||||||
|
│ ✏ Whiteboard │
|
||||||
|
│ 📺 Watch Together │
|
||||||
|
│ 🎮 Spiel starten │
|
||||||
|
└─────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- Use existing icons from `apps/desktop/src/components/icons.tsx` (grep for `PaperclipIcon`/`PlusIcon`, `PollIcon`, `MonitorShareIcon`, `PlayBoxIcon`, `GameIcon`).
|
||||||
|
- Click outside or `Esc` → `onClose`.
|
||||||
|
- Disabled items: `opacity-50 cursor-not-allowed` + `title` hint (e.g. "Spiele nur in 1:1-Chats").
|
||||||
|
- Each row ≥ 44px tall, `role="menu"`/`role="menuitem"`, arrow-up/down keyboard nav.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: Refactor ConversationPage composer
|
||||||
|
|
||||||
|
**Files:** Modify `apps/desktop/src/pages/ConversationPage.tsx`
|
||||||
|
|
||||||
|
**Target layout:**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────┐
|
||||||
|
│ [+] [😊] [GIF] [🎤] Nachricht schreiben… [→] │
|
||||||
|
└────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
1. **Remove** inline buttons for: file-attach, poll, whiteboard, watch-together, game-picker.
|
||||||
|
2. **Add** a `+` button at position 1 with a `useRef` anchor.
|
||||||
|
3. **State:** `const [menuOpen, setMenuOpen] = useState(false);` + render `<ComposerActionsMenu>` with the existing handlers wired (`handleCreateWhiteboard`, `handleStartWatchTogether`, `handleStartGame`, `() => setPollDialogOpen(true)`, `() => fileInputRef.current?.click()`).
|
||||||
|
4. **Remove** the standalone View-Once toggle button (moves to T4 per-attachment).
|
||||||
|
5. **Keep inline:** Emoji picker, GIF picker, voice mic, send arrow.
|
||||||
|
6. **Auto-close menu** after any item action.
|
||||||
|
7. Pass `canStartGame={conversation?.members?.length === 2}` so the dropdown reflects the DM-only constraint.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: View-Once per-attachment in `AttachmentPreview`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify `apps/desktop/src/pages/ConversationPage.tsx` (`AttachmentPreview` component + the `attachments[]` state shape).
|
||||||
|
- Modify `apps/desktop/src/hooks/useConversationMessages.ts` (`send()` signature + per-attachment handling).
|
||||||
|
- Possibly extend the per-attachment encrypt/upload helper if it still treats `viewOnce` as a per-message flag.
|
||||||
|
|
||||||
|
**Behavior:**
|
||||||
|
|
||||||
|
Add a third hover-button on each image preview next to `✏` and `✕`: a `👁` icon that toggles `viewOnce` per attachment.
|
||||||
|
- Active: icon switches (e.g. crossed-eye) + small `1×` badge in lower-right corner of the thumb.
|
||||||
|
- Image-only (`file.type.startsWith('image/')`). Hidden on non-image previews.
|
||||||
|
|
||||||
|
**State refactor:**
|
||||||
|
|
||||||
|
Change `attachments: File[]` → `attachments: Array<{ file: File; viewOnce: boolean }>`. Every consumer site updated:
|
||||||
|
- `setAttachments((prev) => [...prev, ...newOnes.map((f) => ({ file: f, viewOnce: false }))])`
|
||||||
|
- `attachments.map((a, idx) => <AttachmentPreview file={a.file} ... onToggleViewOnce={() => setAttachments(prev => prev.map((x, i) => i === idx ? { ...x, viewOnce: !x.viewOnce } : x))} />)`
|
||||||
|
- `setAttachments((prev) => prev.filter((_, i) => i !== idx))` — unchanged shape
|
||||||
|
|
||||||
|
**Send path:**
|
||||||
|
|
||||||
|
The `send()` currently accepts a `viewOnce` option that applies globally. Refactor so the per-attachment flag flows through:
|
||||||
|
- Either change `send(payload, attachments, replyTo, { viewOnce })` → `send(payload, attachmentsWithFlags, replyTo)` where each entry carries its own `viewOnce`
|
||||||
|
- OR pass a parallel `viewOnceFlags: boolean[]` array aligned with attachments
|
||||||
|
|
||||||
|
The encrypt/upload helper already supports per-attachment `view_once` (P2.T14 column `message_attachments.view_once`). The renderer just needs to pass the right flag per row.
|
||||||
|
|
||||||
|
**Grep first** to find the existing wiring: `Grep -rn "view_once\|viewOnce" apps/desktop/src/ packages/shared/src/chat/` — adapt to what's actually there.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: Cleanup + Final gate
|
||||||
|
|
||||||
|
- [ ] `pnpm --filter @chat-app/desktop typecheck` — green
|
||||||
|
- [ ] `pnpm --filter @chat-app/shared test -- --run` — green (71 tests)
|
||||||
|
- [ ] `pnpm --filter @chat-app/desktop test -- --run` — green
|
||||||
|
- [ ] `git status` — clean
|
||||||
|
- [ ] Tag `phase7-done`
|
||||||
|
- [ ] Report smoke-test points:
|
||||||
|
1. Composer shows 5 inline buttons (was 9)
|
||||||
|
2. Click `+` → popover opens; click anywhere outside or `Esc` closes it
|
||||||
|
3. Attach image → preview shows ✏/👁/✕ on hover
|
||||||
|
4. Toggle 👁 on attachment-1 only → recipient sees attachment-1 as view-once, attachment-2 normally
|
||||||
|
5. Everything else unchanged (emoji, GIF, voice, send, edit-message, etc.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- No emoji-as-icon (uses existing SVG icons).
|
||||||
|
- No slash-commands (deferred to potential Phase 7B).
|
||||||
|
- No reordering of inline buttons beyond the spec.
|
||||||
|
- No per-attachment poll-attach (polls remain message-level).
|
||||||
Generated
+184
@@ -144,6 +144,9 @@ importers:
|
|||||||
rimraf:
|
rimraf:
|
||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.1.3
|
version: 6.1.3
|
||||||
|
rollup-plugin-visualizer:
|
||||||
|
specifier: ^7.0.1
|
||||||
|
version: 7.0.1(rollup@4.60.1)
|
||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^3.4.15
|
specifier: ^3.4.15
|
||||||
version: 3.4.19
|
version: 3.4.19
|
||||||
@@ -2615,6 +2618,10 @@ packages:
|
|||||||
builder-util@25.1.7:
|
builder-util@25.1.7:
|
||||||
resolution: {integrity: sha512-7jPjzBwEGRbwNcep0gGNpLXG9P94VA3CPAZQCzxkFXiV2GMQKlziMbY//rXPI7WKfhsvGgFXjTcXdBEwgXw9ww==}
|
resolution: {integrity: sha512-7jPjzBwEGRbwNcep0gGNpLXG9P94VA3CPAZQCzxkFXiV2GMQKlziMbY//rXPI7WKfhsvGgFXjTcXdBEwgXw9ww==}
|
||||||
|
|
||||||
|
bundle-name@4.1.0:
|
||||||
|
resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
bytes@3.1.2:
|
bytes@3.1.2:
|
||||||
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -2760,6 +2767,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
cliui@9.0.1:
|
||||||
|
resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
clone-deep@4.0.1:
|
clone-deep@4.0.1:
|
||||||
resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==}
|
resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -2991,6 +3002,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
default-browser-id@5.0.1:
|
||||||
|
resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
default-browser@5.5.0:
|
||||||
|
resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
default-gateway@4.2.0:
|
default-gateway@4.2.0:
|
||||||
resolution: {integrity: sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==}
|
resolution: {integrity: sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -3010,6 +3029,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==}
|
resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
define-lazy-prop@3.0.0:
|
||||||
|
resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
define-properties@1.2.1:
|
define-properties@1.2.1:
|
||||||
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
|
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -3131,6 +3154,9 @@ packages:
|
|||||||
engines: {node: '>= 12.20.55'}
|
engines: {node: '>= 12.20.55'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
emoji-regex@10.6.0:
|
||||||
|
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
|
||||||
|
|
||||||
emoji-regex@8.0.0:
|
emoji-regex@8.0.0:
|
||||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||||
|
|
||||||
@@ -3722,6 +3748,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
||||||
engines: {node: 6.* || 8.* || >= 10.*}
|
engines: {node: 6.* || 8.* || >= 10.*}
|
||||||
|
|
||||||
|
get-east-asian-width@1.6.0:
|
||||||
|
resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
get-intrinsic@1.3.0:
|
get-intrinsic@1.3.0:
|
||||||
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -4054,6 +4084,11 @@ packages:
|
|||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
is-docker@3.0.0:
|
||||||
|
resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==}
|
||||||
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
is-extglob@2.1.1:
|
is-extglob@2.1.1:
|
||||||
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -4074,6 +4109,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
is-in-ssh@1.0.0:
|
||||||
|
resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
|
is-inside-container@1.0.0:
|
||||||
|
resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==}
|
||||||
|
engines: {node: '>=14.16'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
is-interactive@1.0.0:
|
is-interactive@1.0.0:
|
||||||
resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==}
|
resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -4172,6 +4216,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
|
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
is-wsl@3.1.1:
|
||||||
|
resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
isarray@1.0.0:
|
isarray@1.0.0:
|
||||||
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||||
|
|
||||||
@@ -4965,6 +5013,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
|
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
open@11.0.0:
|
||||||
|
resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
open@7.4.2:
|
open@7.4.2:
|
||||||
resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==}
|
resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -5190,6 +5242,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==}
|
resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
|
powershell-utils@0.1.0:
|
||||||
|
resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
prebuild-install@7.1.3:
|
prebuild-install@7.1.3:
|
||||||
resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
|
resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -5586,11 +5642,28 @@ packages:
|
|||||||
resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==}
|
resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==}
|
||||||
engines: {node: '>=8.0'}
|
engines: {node: '>=8.0'}
|
||||||
|
|
||||||
|
rollup-plugin-visualizer@7.0.1:
|
||||||
|
resolution: {integrity: sha512-UJUT4+1Ho4OcWmPYU3sYXgUqI8B8Ayfe06MX7y0qCJ1K8aGoKtR/NDd/2nZqM7ADkrzny+I99Ul7GgyoiVNAgg==}
|
||||||
|
engines: {node: '>=22'}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
rolldown: 1.x || ^1.0.0-beta || ^1.0.0-rc
|
||||||
|
rollup: 2.x || 3.x || 4.x
|
||||||
|
peerDependenciesMeta:
|
||||||
|
rolldown:
|
||||||
|
optional: true
|
||||||
|
rollup:
|
||||||
|
optional: true
|
||||||
|
|
||||||
rollup@4.60.1:
|
rollup@4.60.1:
|
||||||
resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==}
|
resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==}
|
||||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
run-applescript@7.1.0:
|
||||||
|
resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
run-parallel@1.2.0:
|
run-parallel@1.2.0:
|
||||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||||
|
|
||||||
@@ -5826,6 +5899,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
source-map@0.7.6:
|
||||||
|
resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
|
||||||
|
engines: {node: '>= 12'}
|
||||||
|
|
||||||
split-on-first@1.1.0:
|
split-on-first@1.1.0:
|
||||||
resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==}
|
resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -5893,6 +5970,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
|
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
string-width@7.2.0:
|
||||||
|
resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
string.prototype.trim@1.2.10:
|
string.prototype.trim@1.2.10:
|
||||||
resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==}
|
resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -6479,6 +6560,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
|
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
wrap-ansi@9.0.2:
|
||||||
|
resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
wrappy@1.0.2:
|
wrappy@1.0.2:
|
||||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||||
|
|
||||||
@@ -6524,6 +6609,10 @@ packages:
|
|||||||
utf-8-validate:
|
utf-8-validate:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
wsl-utils@0.3.1:
|
||||||
|
resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
xcode@3.0.1:
|
xcode@3.0.1:
|
||||||
resolution: {integrity: sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==}
|
resolution: {integrity: sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
@@ -6565,10 +6654,18 @@ packages:
|
|||||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
yargs-parser@22.0.0:
|
||||||
|
resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.12.0 || >=23}
|
||||||
|
|
||||||
yargs@17.7.2:
|
yargs@17.7.2:
|
||||||
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
|
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
yargs@18.0.0:
|
||||||
|
resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.12.0 || >=23}
|
||||||
|
|
||||||
yauzl@2.10.0:
|
yauzl@2.10.0:
|
||||||
resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==}
|
resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==}
|
||||||
|
|
||||||
@@ -9566,6 +9663,10 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
bundle-name@4.1.0:
|
||||||
|
dependencies:
|
||||||
|
run-applescript: 7.1.0
|
||||||
|
|
||||||
bytes@3.1.2: {}
|
bytes@3.1.2: {}
|
||||||
|
|
||||||
cac@6.7.14: {}
|
cac@6.7.14: {}
|
||||||
@@ -9750,6 +9851,12 @@ snapshots:
|
|||||||
strip-ansi: 6.0.1
|
strip-ansi: 6.0.1
|
||||||
wrap-ansi: 7.0.0
|
wrap-ansi: 7.0.0
|
||||||
|
|
||||||
|
cliui@9.0.1:
|
||||||
|
dependencies:
|
||||||
|
string-width: 7.2.0
|
||||||
|
strip-ansi: 7.2.0
|
||||||
|
wrap-ansi: 9.0.2
|
||||||
|
|
||||||
clone-deep@4.0.1:
|
clone-deep@4.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-plain-object: 2.0.4
|
is-plain-object: 2.0.4
|
||||||
@@ -9969,6 +10076,13 @@ snapshots:
|
|||||||
|
|
||||||
deepmerge@4.3.1: {}
|
deepmerge@4.3.1: {}
|
||||||
|
|
||||||
|
default-browser-id@5.0.1: {}
|
||||||
|
|
||||||
|
default-browser@5.5.0:
|
||||||
|
dependencies:
|
||||||
|
bundle-name: 4.1.0
|
||||||
|
default-browser-id: 5.0.1
|
||||||
|
|
||||||
default-gateway@4.2.0:
|
default-gateway@4.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
execa: 1.0.0
|
execa: 1.0.0
|
||||||
@@ -9988,6 +10102,8 @@ snapshots:
|
|||||||
|
|
||||||
define-lazy-prop@2.0.0: {}
|
define-lazy-prop@2.0.0: {}
|
||||||
|
|
||||||
|
define-lazy-prop@3.0.0: {}
|
||||||
|
|
||||||
define-properties@1.2.1:
|
define-properties@1.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
define-data-property: 1.1.4
|
define-data-property: 1.1.4
|
||||||
@@ -10161,6 +10277,8 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
emoji-regex@10.6.0: {}
|
||||||
|
|
||||||
emoji-regex@8.0.0: {}
|
emoji-regex@8.0.0: {}
|
||||||
|
|
||||||
emoji-regex@9.2.2: {}
|
emoji-regex@9.2.2: {}
|
||||||
@@ -10945,6 +11063,8 @@ snapshots:
|
|||||||
|
|
||||||
get-caller-file@2.0.5: {}
|
get-caller-file@2.0.5: {}
|
||||||
|
|
||||||
|
get-east-asian-width@1.6.0: {}
|
||||||
|
|
||||||
get-intrinsic@1.3.0:
|
get-intrinsic@1.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind-apply-helpers: 1.0.2
|
call-bind-apply-helpers: 1.0.2
|
||||||
@@ -11314,6 +11434,8 @@ snapshots:
|
|||||||
|
|
||||||
is-docker@2.2.1: {}
|
is-docker@2.2.1: {}
|
||||||
|
|
||||||
|
is-docker@3.0.0: {}
|
||||||
|
|
||||||
is-extglob@2.1.1: {}
|
is-extglob@2.1.1: {}
|
||||||
|
|
||||||
is-finalizationregistry@1.1.1:
|
is-finalizationregistry@1.1.1:
|
||||||
@@ -11334,6 +11456,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-extglob: 2.1.1
|
is-extglob: 2.1.1
|
||||||
|
|
||||||
|
is-in-ssh@1.0.0: {}
|
||||||
|
|
||||||
|
is-inside-container@1.0.0:
|
||||||
|
dependencies:
|
||||||
|
is-docker: 3.0.0
|
||||||
|
|
||||||
is-interactive@1.0.0: {}
|
is-interactive@1.0.0: {}
|
||||||
|
|
||||||
is-lambda@1.0.1: {}
|
is-lambda@1.0.1: {}
|
||||||
@@ -11415,6 +11543,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-docker: 2.2.1
|
is-docker: 2.2.1
|
||||||
|
|
||||||
|
is-wsl@3.1.1:
|
||||||
|
dependencies:
|
||||||
|
is-inside-container: 1.0.0
|
||||||
|
|
||||||
isarray@1.0.0: {}
|
isarray@1.0.0: {}
|
||||||
|
|
||||||
isarray@2.0.5: {}
|
isarray@2.0.5: {}
|
||||||
@@ -12347,6 +12479,15 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
mimic-fn: 2.1.0
|
mimic-fn: 2.1.0
|
||||||
|
|
||||||
|
open@11.0.0:
|
||||||
|
dependencies:
|
||||||
|
default-browser: 5.5.0
|
||||||
|
define-lazy-prop: 3.0.0
|
||||||
|
is-in-ssh: 1.0.0
|
||||||
|
is-inside-container: 1.0.0
|
||||||
|
powershell-utils: 0.1.0
|
||||||
|
wsl-utils: 0.3.1
|
||||||
|
|
||||||
open@7.4.2:
|
open@7.4.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-docker: 2.2.1
|
is-docker: 2.2.1
|
||||||
@@ -12548,6 +12689,8 @@ snapshots:
|
|||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
powershell-utils@0.1.0: {}
|
||||||
|
|
||||||
prebuild-install@7.1.3:
|
prebuild-install@7.1.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
detect-libc: 2.1.2
|
detect-libc: 2.1.2
|
||||||
@@ -13018,6 +13161,15 @@ snapshots:
|
|||||||
sprintf-js: 1.1.3
|
sprintf-js: 1.1.3
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
rollup-plugin-visualizer@7.0.1(rollup@4.60.1):
|
||||||
|
dependencies:
|
||||||
|
open: 11.0.0
|
||||||
|
picomatch: 4.0.4
|
||||||
|
source-map: 0.7.6
|
||||||
|
yargs: 18.0.0
|
||||||
|
optionalDependencies:
|
||||||
|
rollup: 4.60.1
|
||||||
|
|
||||||
rollup@4.60.1:
|
rollup@4.60.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.8
|
'@types/estree': 1.0.8
|
||||||
@@ -13049,6 +13201,8 @@ snapshots:
|
|||||||
'@rollup/rollup-win32-x64-msvc': 4.60.1
|
'@rollup/rollup-win32-x64-msvc': 4.60.1
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
run-applescript@7.1.0: {}
|
||||||
|
|
||||||
run-parallel@1.2.0:
|
run-parallel@1.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
queue-microtask: 1.2.3
|
queue-microtask: 1.2.3
|
||||||
@@ -13308,6 +13462,8 @@ snapshots:
|
|||||||
|
|
||||||
source-map@0.6.1: {}
|
source-map@0.6.1: {}
|
||||||
|
|
||||||
|
source-map@0.7.6: {}
|
||||||
|
|
||||||
split-on-first@1.1.0: {}
|
split-on-first@1.1.0: {}
|
||||||
|
|
||||||
sprintf-js@1.0.3: {}
|
sprintf-js@1.0.3: {}
|
||||||
@@ -13364,6 +13520,12 @@ snapshots:
|
|||||||
emoji-regex: 9.2.2
|
emoji-regex: 9.2.2
|
||||||
strip-ansi: 7.2.0
|
strip-ansi: 7.2.0
|
||||||
|
|
||||||
|
string-width@7.2.0:
|
||||||
|
dependencies:
|
||||||
|
emoji-regex: 10.6.0
|
||||||
|
get-east-asian-width: 1.6.0
|
||||||
|
strip-ansi: 7.2.0
|
||||||
|
|
||||||
string.prototype.trim@1.2.10:
|
string.prototype.trim@1.2.10:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind: 1.0.9
|
call-bind: 1.0.9
|
||||||
@@ -14007,6 +14169,12 @@ snapshots:
|
|||||||
string-width: 5.1.2
|
string-width: 5.1.2
|
||||||
strip-ansi: 7.2.0
|
strip-ansi: 7.2.0
|
||||||
|
|
||||||
|
wrap-ansi@9.0.2:
|
||||||
|
dependencies:
|
||||||
|
ansi-styles: 6.2.3
|
||||||
|
string-width: 7.2.0
|
||||||
|
strip-ansi: 7.2.0
|
||||||
|
|
||||||
wrappy@1.0.2: {}
|
wrappy@1.0.2: {}
|
||||||
|
|
||||||
write-file-atomic@2.4.3:
|
write-file-atomic@2.4.3:
|
||||||
@@ -14028,6 +14196,11 @@ snapshots:
|
|||||||
|
|
||||||
ws@8.20.0: {}
|
ws@8.20.0: {}
|
||||||
|
|
||||||
|
wsl-utils@0.3.1:
|
||||||
|
dependencies:
|
||||||
|
is-wsl: 3.1.1
|
||||||
|
powershell-utils: 0.1.0
|
||||||
|
|
||||||
xcode@3.0.1:
|
xcode@3.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
simple-plist: 1.3.1
|
simple-plist: 1.3.1
|
||||||
@@ -14056,6 +14229,8 @@ snapshots:
|
|||||||
|
|
||||||
yargs-parser@21.1.1: {}
|
yargs-parser@21.1.1: {}
|
||||||
|
|
||||||
|
yargs-parser@22.0.0: {}
|
||||||
|
|
||||||
yargs@17.7.2:
|
yargs@17.7.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
cliui: 8.0.1
|
cliui: 8.0.1
|
||||||
@@ -14066,6 +14241,15 @@ snapshots:
|
|||||||
y18n: 5.0.8
|
y18n: 5.0.8
|
||||||
yargs-parser: 21.1.1
|
yargs-parser: 21.1.1
|
||||||
|
|
||||||
|
yargs@18.0.0:
|
||||||
|
dependencies:
|
||||||
|
cliui: 9.0.1
|
||||||
|
escalade: 3.2.0
|
||||||
|
get-caller-file: 2.0.5
|
||||||
|
string-width: 7.2.0
|
||||||
|
y18n: 5.0.8
|
||||||
|
yargs-parser: 22.0.0
|
||||||
|
|
||||||
yauzl@2.10.0:
|
yauzl@2.10.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
buffer-crc32: 0.2.13
|
buffer-crc32: 0.2.13
|
||||||
|
|||||||
Reference in New Issue
Block a user