(wip): add onboarding
All checks were successful
/ build-web (push) Successful in 7m19s
/ build-desktop (linux) (push) Successful in 11m52s
/ build-mobile (push) Successful in 18m26s
/ release (push) Successful in 2m59s

(fix): login page reloading
This commit is contained in:
Alois 2026-07-24 12:18:05 +02:00
commit bf8f449f03
Signed by: alois
SSH key fingerprint: SHA256:GBzT2DXvAuGV9XIV5W3WrzVpjU54FThmxHXdbz95J24
19 changed files with 461 additions and 439 deletions

View file

@ -63,6 +63,7 @@
"@tensamin/ui": "*", "@tensamin/ui": "*",
"@tensamin/user": "workspace:*", "@tensamin/user": "workspace:*",
"@tensamin/notifications": "workspace:*", "@tensamin/notifications": "workspace:*",
"@tensamin/onboarding": "workspace:*",
"aria-hidden": "^1.2.4", "aria-hidden": "^1.2.4",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",

View file

@ -8,6 +8,7 @@ import * as React from "react";
import { z } from "zod"; import { z } from "zod";
import { isTauri } from "@tauri-apps/api/core"; import { isTauri } from "@tauri-apps/api/core";
import QrCodeScanner from "@tensamin/tauri/qrCodeScanner"; import QrCodeScanner from "@tensamin/tauri/qrCodeScanner";
import { useNavigate } from "@tanstack/react-router";
const fetchedUser = z.object({ const fetchedUser = z.object({
id: z.uuidv4(), id: z.uuidv4(),
@ -80,6 +81,7 @@ export default function Form() {
const uploadRef = React.useRef<HTMLInputElement | null>(null); const uploadRef = React.useRef<HTMLInputElement | null>(null);
const [isDragging, setIsDragging] = React.useState(false); const [isDragging, setIsDragging] = React.useState(false);
const { save } = useStorage(); const { save } = useStorage();
const navigate = useNavigate();
const loginPendingRef = React.useRef(false); const loginPendingRef = React.useRef(false);
const persistLogin = React.useCallback( const persistLogin = React.useCallback(
@ -91,18 +93,13 @@ export default function Form() {
await save("mtp_keyring", privateKey, { secure: true }); await save("mtp_keyring", privateKey, { secure: true });
await save("session_id", Date.now()); await save("session_id", Date.now());
await save("user_id", userId); await save("user_id", userId);
window.history.replaceState( await navigate({ to: "/", replace: true });
null,
"",
window.location.protocol === "file:" ? "#/" : "/",
);
window.location.reload();
return true; return true;
} finally { } finally {
loginPendingRef.current = false; loginPendingRef.current = false;
} }
}, },
[save], [navigate, save],
); );
// Process dropped files // Process dropped files

View file

@ -1,271 +0,0 @@
import { useState, useCallback, useEffect } from "react";
import { useStorage } from "@tensamin/storage/context";
import {
Button,
Checkbox,
CreateScreen,
ErrorScreen,
Label,
Link,
Spinner,
} from "@tensamin/ui";
import { z } from "zod";
import { legalDocsSchema } from "@tensamin/shared/features/legal/schema";
import { log } from "@tensamin/shared/log";
// Prevents the user from using Tensamin without accepting the privacy policy and terms of service.
export default function Screen(props: { children: React.ReactNode }) {
const { load, save } = useStorage();
const [error, setError] = useState("");
const [errorDescription, setErrorDescription] = useState("");
const [remoteDocs, setRemoteDocs] = useState<
z.infer<typeof legalDocsSchema> | undefined
>(undefined);
const [localDocs, setLocalDocs] = useState<
z.infer<typeof legalDocsSchema> | undefined
>(undefined);
const [loading, setLoading] = useState(true);
const [showLoading, setShowLoading] = useState(false);
const [acceptedPP, acceptPP] = useState(false);
const [acceptedTOS, acceptTOS] = useState(false);
const [hasContinued, setHasContinued] = useState(false);
const [userId, setUserId] = useState<number | undefined>(undefined);
// Saves
const handleContinueLegal = useCallback((): void => {
const currentDocs = remoteDocs;
if (!currentDocs) {
return;
}
save("accepted_privacy_policy", true);
save("accepted_terms_of_service", true);
save("legal_docs", currentDocs);
setHasContinued(true);
}, [remoteDocs, save]);
useEffect(() => {
let active = true;
let loadingTimer: ReturnType<typeof setTimeout> | undefined;
void (async () => {
try {
const id = await load("user_id");
if (!active) {
return;
}
setUserId(id);
if (id === 0) {
return;
}
loadingTimer = setTimeout(() => {
if (active) setShowLoading(true);
}, 200);
const response = await fetch("https://legal.tensamin.net/api/current");
if (!response.ok) {
throw new Error(`Legal documents request failed: ${response.status}`);
}
const current: unknown = await response.json();
if (!active) {
return;
}
const safeCurrent = legalDocsSchema.safeParse(current);
if (!safeCurrent.success) {
setError("Failed to load legal documents");
setErrorDescription(
"The legal documents data received from the server is invalid. Please try again later.",
);
log(
0,
"Legal",
"red",
"Invalid legal documents data",
safeCurrent.error,
);
return;
}
setRemoteDocs(safeCurrent.data);
const currentLocalDocs = await load("legal_docs");
setLocalDocs(currentLocalDocs);
const [loadedAcceptedPP, loadedAcceptedTOS] = await Promise.all([
load("accepted_privacy_policy"),
load("accepted_terms_of_service"),
]);
if (!active) {
return;
}
acceptPP(
loadedAcceptedPP &&
!!currentLocalDocs &&
currentLocalDocs.pp.hash === safeCurrent.data.pp.hash,
);
acceptTOS(
loadedAcceptedTOS &&
!!currentLocalDocs &&
currentLocalDocs.tos.hash === safeCurrent.data.tos.hash,
);
} catch (err) {
if (!active) return;
setError("Failed to load legal documents");
setErrorDescription(
"An error occurred while fetching the legal documents from the server. Please try again later.",
);
log(0, "Legal", "red", "Failed to fetch legal documents", err);
} finally {
clearTimeout(loadingTimer);
if (active) {
setLoading(false);
}
}
})();
return () => {
active = false;
clearTimeout(loadingTimer);
};
}, [load]);
if (error !== "" && errorDescription !== "") {
return <ErrorScreen error={error} description={errorDescription} />;
}
if (loading || userId === undefined) {
if (!showLoading) return null;
return (
<CreateScreen>
<div className="flex flex-col items-center gap-3">
<Spinner className="size-8" />
<p className="text-sm text-muted-foreground">
Fetching legal documents...
</p>
<Link
label="status.methanium.net"
link="https://status.methanium.net"
/>
</div>
</CreateScreen>
);
}
const docsMatch =
localDocs !== undefined &&
remoteDocs !== undefined &&
localDocs.pp.hash === remoteDocs.pp.hash &&
localDocs.tos.hash === remoteDocs.tos.hash;
if (
(acceptedPP && acceptedTOS && (docsMatch || hasContinued)) ||
userId === 0
) {
return <>{props.children}</>;
}
return (
<CreateScreen>
<div className="h-full flex flex-col gap-15 p-10 py-20 md:p-40 w-full lg:w-2/3">
<h1 className="text-3xl md:text-4xl font-bold">
Privacy Policy & ToS
<p className="text-muted-foreground text-[20px] font-normal pt-3">
{remoteDocs?.pp.version} / {remoteDocs?.tos.version}
</p>
</h1>
<div className="w-full h-full flex flex-col items-center justify-center gap-5">
<div className="justify-start items-start flex flex-col gap-2">
<BigCheckbox
id="acceptPP"
checked={acceptedPP}
onChange={acceptPP}
label="I agree to the Privacy Policy"
/>
<BigCheckbox
id="acceptTOS"
checked={acceptedTOS}
onChange={acceptTOS}
label="I agree to the Terms of Service"
/>
<div className="w-full border-t-2" />
<Link
label="Privacy Policy"
link={`https://legal.tensamin.net/pp/${remoteDocs?.pp.version}`}
/>
<Link
label="Terms of Service"
link={`https://legal.tensamin.net/tos/${remoteDocs?.tos.version}`}
/>
</div>
</div>
<ContinueButton
disabled={!acceptedPP || !acceptedTOS}
onClick={handleContinueLegal}
/>
</div>
</CreateScreen>
);
}
// Components
function ContinueButton({
onClick,
disabled,
}: {
onClick: () => void;
disabled?: boolean;
}) {
return (
<div className="w-full flex justify-end">
<Button
size="lg"
className="text-md w-full md:w-auto"
onClick={onClick}
disabled={disabled}
>
Continue
</Button>
</div>
);
}
function BigCheckbox({
id,
label,
checked,
onChange,
}: {
id: string;
label: string;
checked: boolean;
onChange: (checked: boolean) => void;
}) {
return (
<div className="flex items-center space-x-2">
<Checkbox
id={id}
checked={checked}
onCheckedChange={onChange}
className="size-5.5 rounded-md flex items-center justify-center"
/>
<Label htmlFor={id} className="text-lg">
{label}
</Label>
</div>
);
}

View file

@ -15,6 +15,7 @@ import NotFound from "@/routes/404";
import AppLayout from "@/routes/app/layout"; import AppLayout from "@/routes/app/layout";
import { createSettingsRoute } from "@tensamin/settings"; import { createSettingsRoute } from "@tensamin/settings";
import OnboardingGate from "@tensamin/onboarding";
import Home from "@/routes/app/home"; import Home from "@/routes/app/home";
import ChatScreen from "@tensamin/chat/screen"; import ChatScreen from "@tensamin/chat/screen";
@ -43,7 +44,6 @@ import Crypto from "@tensamin/crypto/context";
import DesktopMediaProvider from "@tensamin/shared/desktopMedia"; import DesktopMediaProvider from "@tensamin/shared/desktopMedia";
import { log } from "@tensamin/shared/log"; import { log } from "@tensamin/shared/log";
import LegalWrapper from "@/features/legal/screen";
import CacheSync from "@tensamin/cache/sync"; import CacheSync from "@tensamin/cache/sync";
import { useStorage } from "@tensamin/storage/context"; import { useStorage } from "@tensamin/storage/context";
@ -244,13 +244,7 @@ function RootShell() {
<Storage> <Storage>
<ThemeStorageBridge /> <ThemeStorageBridge />
<LoginWrapper> <LoginWrapper>
<LegalWrapper>
<Crypto>
<DesktopMediaProvider>
<Outlet /> <Outlet />
</DesktopMediaProvider>
</Crypto>
</LegalWrapper>
</LoginWrapper> </LoginWrapper>
</Storage> </Storage>
</TooltipProvider> </TooltipProvider>
@ -262,6 +256,9 @@ function RootShell() {
function AppShell() { function AppShell() {
return ( return (
<OnboardingGate>
<Crypto>
<DesktopMediaProvider>
<MTPProvider> <MTPProvider>
<CacheSync /> <CacheSync />
<Session> <Session>
@ -280,6 +277,9 @@ function AppShell() {
</UserProvider> </UserProvider>
</Session> </Session>
</MTPProvider> </MTPProvider>
</DesktopMediaProvider>
</Crypto>
</OnboardingGate>
); );
} }

View file

@ -136,6 +136,7 @@ export default defineConfig({
"@tensamin/markdown", "@tensamin/markdown",
"@tensamin/mtp", "@tensamin/mtp",
"@tensamin/notifications", "@tensamin/notifications",
"@tensamin/onboarding",
"@tensamin/shared", "@tensamin/shared",
"@tensamin/shared/data", "@tensamin/shared/data",
"@tensamin/shared/log", "@tensamin/shared/log",

View file

@ -1,7 +1,7 @@
import { Button } from "@tensamin/ui"; import { Button, withTooltip } from "@tensamin/ui";
import { toggleDeaf, useCall } from "../../store"; import { toggleDeaf, useCall } from "../../store";
import { HeadphoneOff, Headphones } from "lucide-react"; import { HeadphoneOff, Headphones } from "lucide-react";
import { type CallButtonProps, iconScale, withTooltip } from "./tooltipButton"; import { type CallButtonProps, iconScale } from "./tooltipButton";
export default function DeafButton({ export default function DeafButton({
className, className,

View file

@ -1,7 +1,7 @@
import { LeaveIcon } from "@livekit/components-react"; import { LeaveIcon } from "@livekit/components-react";
import { Button } from "@tensamin/ui"; import { Button, withTooltip } from "@tensamin/ui";
import { disconnect, useCall } from "../../store"; import { disconnect, useCall } from "../../store";
import { type CallButtonProps, iconScale, withTooltip } from "./tooltipButton"; import { type CallButtonProps, iconScale } from "./tooltipButton";
export default function LeaveButton({ export default function LeaveButton({
className, className,

View file

@ -1,7 +1,7 @@
import { Button } from "@tensamin/ui"; import { Button, withTooltip } from "@tensamin/ui";
import { toggleMute, useCall } from "../../store"; import { toggleMute, useCall } from "../../store";
import { Mic, MicOff } from "lucide-react"; import { Mic, MicOff } from "lucide-react";
import { type CallButtonProps, iconScale, withTooltip } from "./tooltipButton"; import { type CallButtonProps, iconScale } from "./tooltipButton";
export default function MuteButton({ export default function MuteButton({
className, className,

View file

@ -1,6 +1,3 @@
import { type ReactElement } from "react";
import { Tooltip, TooltipContent, TooltipTrigger } from "@tensamin/ui";
export type CallButtonProps = { export type CallButtonProps = {
className?: string; className?: string;
iconSize?: number; iconSize?: number;
@ -11,22 +8,3 @@ export type CallButtonProps = {
export function iconScale(iconSize?: number) { export function iconScale(iconSize?: number) {
return { scale: (iconSize ? iconSize + 100 : 100) + "%" }; return { scale: (iconSize ? iconSize + 100 : 100) + "%" };
} }
export function withTooltip(
button: ReactElement,
tooltip?: string,
portalContainer?: HTMLElement,
) {
if (!tooltip) {
return button;
}
return (
<Tooltip>
<TooltipTrigger render={button} />
<TooltipContent portalProps={{ container: portalContainer }}>
{tooltip}
</TooltipContent>
</Tooltip>
);
}

View file

@ -0,0 +1,23 @@
{
"name": "@tensamin/onboarding",
"private": true,
"version": "0.0.0",
"type": "module",
"exports": {
".": "./src/index.tsx"
},
"scripts": {
"format": "pnpm exec prettier --write .",
"lint": "eslint src",
"build": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@tensamin/shared": "workspace:*",
"@tensamin/storage": "workspace:*",
"@tensamin/ui": "*",
"lucide-react": "^1.14.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"zod": "^4.3.6"
}
}

View file

@ -0,0 +1,178 @@
import { useCallback, useEffect, useState, type ReactNode } from "react";
import {
CreateScreen,
ErrorScreen,
Link,
OnboardingFlow,
Spinner,
type OnboardingStep,
} from "@tensamin/ui";
import { useStorage } from "@tensamin/storage/context";
import { legalDocsSchema } from "@tensamin/shared/features/legal/schema";
import { log } from "@tensamin/shared/log";
import type { z } from "zod";
import LegalPage from "./pages/legal";
import { onboardingSteps } from "./steps";
export {
useOnboardingStep,
type OnboardingStep,
type OnboardingStepControls,
} from "@tensamin/ui";
type LegalDocs = z.infer<typeof legalDocsSchema>;
interface GateState {
docs: LegalDocs;
acceptedPP: boolean;
acceptedTOS: boolean;
includeLegal: boolean;
includeOnboarding: boolean;
}
export default function OnboardingGate({ children }: { children: ReactNode }) {
const { load, save } = useStorage();
const [state, setState] = useState<GateState>();
const [complete, setComplete] = useState(false);
const [showLoading, setShowLoading] = useState(false);
const [error, setError] = useState("");
const [errorDescription, setErrorDescription] = useState("");
useEffect(() => {
let active = true;
const loadingTimer = setTimeout(() => {
if (active) setShowLoading(true);
}, 200);
void (async () => {
try {
const response = await fetch("https://legal.tensamin.net/api/current");
if (!response.ok) {
throw new Error(`Legal documents request failed: ${response.status}`);
}
const current: unknown = await response.json();
if (!active) return;
const parsed = legalDocsSchema.safeParse(current);
if (!parsed.success) {
setError("Failed to load legal documents");
setErrorDescription(
"The legal documents data received from the server is invalid. Please try again later.",
);
log(0, "Legal", "red", "Invalid legal documents data", parsed.error);
return;
}
const [localDocs, acceptedPP, acceptedTOS, onboardingDone] =
await Promise.all([
load("legal_docs"),
load("accepted_privacy_policy"),
load("accepted_terms_of_service"),
load("onboarding_done"),
]);
if (!active) return;
const currentAcceptedPP =
acceptedPP && localDocs.pp.hash === parsed.data.pp.hash;
const currentAcceptedTOS =
acceptedTOS && localDocs.tos.hash === parsed.data.tos.hash;
const existingUser = acceptedPP && acceptedTOS;
const includeOnboarding = !onboardingDone && !existingUser;
if (!onboardingDone && existingUser) {
await save("onboarding_done", true);
if (!active) return;
}
setState({
docs: parsed.data,
acceptedPP: currentAcceptedPP,
acceptedTOS: currentAcceptedTOS,
includeLegal: !currentAcceptedPP || !currentAcceptedTOS,
includeOnboarding,
});
} catch (caught) {
if (!active) return;
setError("Failed to load legal documents");
setErrorDescription(
"An error occurred while fetching the legal documents from the server. Please try again later.",
);
log(0, "Legal", "red", "Failed to fetch legal documents", caught);
} finally {
clearTimeout(loadingTimer);
}
})();
return () => {
active = false;
clearTimeout(loadingTimer);
};
}, [load, save]);
const acceptLegal = useCallback(async () => {
if (!state) return;
await Promise.all([
save("accepted_privacy_policy", true),
save("accepted_terms_of_service", true),
save("legal_docs", state.docs),
]);
setState((current) =>
current ? { ...current, acceptedPP: true, acceptedTOS: true } : current,
);
}, [save, state]);
const finish = useCallback(async () => {
if (state?.includeOnboarding) {
await save("onboarding_done", true);
}
setComplete(true);
}, [save, state?.includeOnboarding]);
if (error && errorDescription) {
return <ErrorScreen error={error} description={errorDescription} />;
}
if (!state) {
if (!showLoading) return null;
return (
<CreateScreen>
<div className="flex flex-col items-center gap-3">
<Spinner className="size-8" />
<p className="text-sm text-muted-foreground">
Fetching legal documents...
</p>
<Link
label="status.methanium.net"
link="https://status.methanium.net"
/>
</div>
</CreateScreen>
);
}
const steps: OnboardingStep[] = [];
if (state.includeLegal) {
steps.push({
id: "legal",
defaultCanContinue: false,
content: (
<LegalPage
docs={state.docs}
initiallyAcceptedPP={state.acceptedPP}
initiallyAcceptedTOS={state.acceptedTOS}
onAccept={acceptLegal}
/>
),
});
}
if (state.includeOnboarding) {
steps.push(...onboardingSteps);
}
if (complete || steps.length === 0) return <>{children}</>;
return <OnboardingFlow steps={steps} onFinish={finish} />;
}

View file

@ -0,0 +1,98 @@
import { useCallback, useState } from "react";
import { Checkbox, Label, Link } from "@tensamin/ui";
import { legalDocsSchema } from "@tensamin/shared/features/legal/schema";
import type { z } from "zod";
import { useOnboardingStep } from "@tensamin/ui";
type LegalDocs = z.infer<typeof legalDocsSchema>;
export default function LegalPage({
docs,
initiallyAcceptedPP,
initiallyAcceptedTOS,
onAccept,
}: {
docs: LegalDocs;
initiallyAcceptedPP: boolean;
initiallyAcceptedTOS: boolean;
onAccept: () => Promise<void>;
}) {
const [acceptedPP, setAcceptedPP] = useState(initiallyAcceptedPP);
const [acceptedTOS, setAcceptedTOS] = useState(initiallyAcceptedTOS);
const handleContinue = useCallback(async () => {
if (!acceptedPP || !acceptedTOS) return false;
await onAccept();
}, [acceptedPP, acceptedTOS, onAccept]);
useOnboardingStep({
canContinue: acceptedPP && acceptedTOS,
onContinue: handleContinue,
});
return (
<div className="mx-auto flex h-full w-full max-w-5xl flex-col gap-10 p-10 py-20 md:p-24">
<div>
<h1 className="text-3xl font-bold md:text-4xl" tabIndex={-1}>
Privacy Policy &amp; ToS
</h1>
<p className="pt-3 text-xl text-muted-foreground">
{docs.pp.version} / {docs.tos.version}
</p>
</div>
<div className="flex flex-1 items-center justify-center">
<div className="flex flex-col items-start gap-2">
<BigCheckbox
id="acceptPP"
checked={acceptedPP}
onChange={setAcceptedPP}
label="I agree to the Privacy Policy"
/>
<BigCheckbox
id="acceptTOS"
checked={acceptedTOS}
onChange={setAcceptedTOS}
label="I agree to the Terms of Service"
/>
<div className="w-full border-t-2" />
<Link
label="Privacy Policy"
link={`https://legal.tensamin.net/pp/${docs.pp.version}`}
/>
<Link
label="Terms of Service"
link={`https://legal.tensamin.net/tos/${docs.tos.version}`}
/>
</div>
</div>
</div>
);
}
function BigCheckbox({
id,
label,
checked,
onChange,
}: {
id: string;
label: string;
checked: boolean;
onChange: (checked: boolean) => void;
}) {
return (
<div className="flex items-center space-x-2">
<Checkbox
id={id}
checked={checked}
onCheckedChange={onChange}
className="flex size-5.5 items-center justify-center rounded-md"
/>
<Label htmlFor={id} className="text-lg">
{label}
</Label>
</div>
);
}

View file

@ -0,0 +1,9 @@
export default function OnboardingPage() {
return (
<div className="flex h-full items-center justify-center p-8">
<h1 className="text-3xl font-bold" tabIndex={-1}>
Onboarding
</h1>
</div>
);
}

View file

@ -0,0 +1,10 @@
import type { OnboardingStep } from "@tensamin/ui";
import OnboardingPage from "./pages/onboarding";
// Add future onboarding pages here. Completed users skip this entire manifest.
export const onboardingSteps: OnboardingStep[] = [
{
id: "onboarding",
content: <OnboardingPage />,
},
];

View file

@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"skipLibCheck": true,
"noEmit": true
},
"include": ["src"]
}

View file

@ -1,14 +1,4 @@
import { import { EditableList, LabeledSwitch } from "@tensamin/ui";
Button,
Checkbox,
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
Input,
Label,
Switch as UISwitch,
} from "@tensamin/ui";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { storageDefaults, type Storage } from "@tensamin/shared/data"; import { storageDefaults, type Storage } from "@tensamin/shared/data";
@ -24,9 +14,12 @@ type ListStorageKey = {
}[keyof Storage]; }[keyof Storage];
type ListStorageItem<K extends ListStorageKey> = type ListStorageItem<K extends ListStorageKey> =
Storage[K] extends Array<infer Item> ? Item : never; Storage[K] extends Array<infer Item> ? Item & (string | number) : never;
export function Switch({ label, id }: { export function Switch({
label,
id,
}: {
label: React.ReactNode; label: React.ReactNode;
id: keyof typeof settingsStorageDefaults & BooleanStorageKey; id: keyof typeof settingsStorageDefaults & BooleanStorageKey;
}) { }) {
@ -38,91 +31,55 @@ export function Switch({ label, id }: {
}, [id, load]); }, [id, load]);
return ( return (
<div className="flex gap-1"> <LabeledSwitch
<UISwitch id={id} checked={value} onCheckedChange={(nextValue) => { id={id}
label={label}
checked={value}
onCheckedChange={(nextValue) => {
setValue(nextValue); setValue(nextValue);
save(id, nextValue); save(id, nextValue);
}} /> }}
<Label htmlFor={id}>{label}</Label> />
</div>
); );
} }
export function List<K extends ListStorageKey>({ label, id }: { export function List<K extends ListStorageKey>({
label,
id,
}: {
label: React.ReactNode; label: React.ReactNode;
id: K; id: K;
}) { }) {
const { save, load } = useStorage(); const { save, load } = useStorage();
const [items, setItems] = useState<Storage[K]>(storageDefaults[id]); const [items, setItems] = useState<Storage[K]>(storageDefaults[id]);
const [inputValue, setInputValue] = useState("");
const [selectedItems, setSelectedItems] = useState<Set<number>>(new Set());
useEffect(() => { useEffect(() => {
load(id).then((value) => setItems(value)); load(id).then((value) => setItems(value));
}, [id, load]); }, [id, load]);
const persistItems = (nextItems: Storage[K]) => { const persistItems = (nextItems: ListStorageItem<K>[]) => {
setItems(nextItems); setItems(nextItems as Storage[K]);
save(id, nextItems); save(id, nextItems as Storage[K]);
}; };
const toStorageItem = (value: string): ListStorageItem<K> => { const toStorageItem = (value: string): ListStorageItem<K> => {
const referenceItem = items[0] ?? storageDefaults[id][0]; const referenceItem = items[0] ?? storageDefaults[id][0];
return (typeof referenceItem === "number" ? Number(value) : value) as ListStorageItem<K>; return (
}; typeof referenceItem === "number" ? Number(value) : value
) as ListStorageItem<K>;
const addItem = () => {
const trimmedValue = inputValue.trim();
if (!trimmedValue) return;
const nextItem = toStorageItem(trimmedValue);
if (typeof nextItem === "number" && Number.isNaN(nextItem)) return;
persistItems([...items, nextItem] as Storage[K]);
setInputValue("");
};
const deleteItems = (indexes: Set<number>) => {
const nextItems = items.filter((_, index) => !indexes.has(index)) as Storage[K];
setItems(nextItems);
setSelectedItems(new Set());
save(id, nextItems);
}; };
return ( return (
<div className="flex flex-col gap-2 pt-4"> <EditableList<ListStorageItem<K>>
<Label>{label}</Label> label={label}
<div className="flex flex-col gap-0 overflow-hidden p-1 border-2 rounded-xl"> items={items as ListStorageItem<K>[]}
<div className="flex gap-1"> parseItem={(value) => {
<Input value={inputValue} onChange={(event) => setInputValue(event.target.value)} onKeyDown={(event) => { const item = toStorageItem(value);
if (event.key === "Enter") addItem(); return typeof item === "number" && Number.isNaN(item)
}} /> ? undefined
<Button onClick={addItem}>Add item</Button> : item;
</div> }}
<div className="flex flex-col gap-0"> onItemsChange={persistItems}
{items.map((item, index) => { />
const labelId = `${String(id)}-${index}`;
const selected = selectedItems.has(index);
const deletingSelectedItems = selectedItems.size > 1;
return (
<ContextMenu key={`${String(item)}-${index}`}>
<ContextMenuTrigger render={<div className="grid grid-cols-[auto_auto_1fr] items-center gap-2 border-b px-1 py-2 last:border-b-0">
<Checkbox id={labelId} checked={selected} onCheckedChange={(checked) => setSelectedItems((previous) => {
const nextSelected = new Set(previous);
if (checked) nextSelected.add(index);
else nextSelected.delete(index);
return nextSelected;
})} />
<Label htmlFor={labelId}>{String(item)}</Label><div />
</div>} />
<ContextMenuContent>
<ContextMenuItem variant="destructive" onClick={() => deleteItems(deletingSelectedItems ? selectedItems : new Set([index]))}>
{deletingSelectedItems ? "Delete Selected" : "Delete"}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
);
})}
</div>
</div>
</div>
); );
} }

View file

@ -421,6 +421,7 @@ export interface Storage extends SettingsStorageDefaults {
session_id: number; session_id: number;
user_id: number; user_id: number;
mtp_keyring: string; mtp_keyring: string;
onboarding_done: boolean;
ppandtos_done: boolean; ppandtos_done: boolean;
accepted_terms_of_service: boolean; accepted_terms_of_service: boolean;
accepted_privacy_policy: boolean; accepted_privacy_policy: boolean;
@ -458,6 +459,7 @@ export const storageDefaults: Storage = {
session_id: 0, session_id: 0,
user_id: 0, user_id: 0,
mtp_keyring: "", mtp_keyring: "",
onboarding_done: false,
ppandtos_done: false, ppandtos_done: false,
accepted_terms_of_service: false, accepted_terms_of_service: false,
accepted_privacy_policy: false, accepted_privacy_policy: false,

85
pnpm-lock.yaml generated
View file

@ -5,7 +5,7 @@ settings:
excludeLinksFromLockfile: false excludeLinksFromLockfile: false
overrides: overrides:
'@tensamin/ui': https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz '@tensamin/ui': https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz
mtp: https://git.methanium.net/methanium/mtp/releases/download/0.2.0-3919eb46fd/mtp-0.2.0.tgz mtp: https://git.methanium.net/methanium/mtp/releases/download/0.2.0-3919eb46fd/mtp-0.2.0.tgz
importers: importers:
@ -13,8 +13,8 @@ importers:
.: .:
dependencies: dependencies:
'@tensamin/ui': '@tensamin/ui':
specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz
version: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) version: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
mtp: mtp:
specifier: https://git.methanium.net/methanium/mtp/releases/download/0.2.0-3919eb46fd/mtp-0.2.0.tgz specifier: https://git.methanium.net/methanium/mtp/releases/download/0.2.0-3919eb46fd/mtp-0.2.0.tgz
version: https://git.methanium.net/methanium/mtp/releases/download/0.2.0-3919eb46fd/mtp-0.2.0.tgz version: https://git.methanium.net/methanium/mtp/releases/download/0.2.0-3919eb46fd/mtp-0.2.0.tgz
@ -107,8 +107,8 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../../packages/shared version: link:../../packages/shared
'@tensamin/ui': '@tensamin/ui':
specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz
version: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) version: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
react: react:
specifier: ^19.2.0 specifier: ^19.2.0
version: 19.2.7 version: 19.2.7
@ -257,6 +257,9 @@ importers:
'@tensamin/notifications': '@tensamin/notifications':
specifier: workspace:* specifier: workspace:*
version: link:../../packages/notifications version: link:../../packages/notifications
'@tensamin/onboarding':
specifier: workspace:*
version: link:../../packages/onboarding
'@tensamin/settings': '@tensamin/settings':
specifier: workspace:* specifier: workspace:*
version: link:../../packages/settings version: link:../../packages/settings
@ -273,8 +276,8 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../../packages/tauth version: link:../../packages/tauth
'@tensamin/ui': '@tensamin/ui':
specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz
version: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) version: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
'@tensamin/user': '@tensamin/user':
specifier: workspace:* specifier: workspace:*
version: link:../../packages/user version: link:../../packages/user
@ -547,8 +550,8 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../storage version: link:../storage
'@tensamin/ui': '@tensamin/ui':
specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz
version: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) version: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
'@tensamin/user': '@tensamin/user':
specifier: workspace:* specifier: workspace:*
version: link:../user version: link:../user
@ -610,8 +613,8 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../storage version: link:../storage
'@tensamin/ui': '@tensamin/ui':
specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz
version: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) version: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
'@tensamin/user': '@tensamin/user':
specifier: workspace:* specifier: workspace:*
version: link:../user version: link:../user
@ -661,8 +664,8 @@ importers:
specifier: ^6.41.1 specifier: ^6.41.1
version: 6.43.4 version: 6.43.4
'@tensamin/ui': '@tensamin/ui':
specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz
version: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) version: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
'@twemoji/api': '@twemoji/api':
specifier: ^17.0.3 specifier: ^17.0.3
version: 17.0.3 version: 17.0.3
@ -691,8 +694,8 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../storage version: link:../storage
'@tensamin/ui': '@tensamin/ui':
specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz
version: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) version: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
lucide-react: lucide-react:
specifier: ^1.14.0 specifier: ^1.14.0
version: 1.23.0(react@19.2.7) version: 1.23.0(react@19.2.7)
@ -740,8 +743,8 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../storage version: link:../storage
'@tensamin/ui': '@tensamin/ui':
specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz
version: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) version: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
'@tensamin/user': '@tensamin/user':
specifier: workspace:* specifier: workspace:*
version: link:../user version: link:../user
@ -758,6 +761,30 @@ importers:
specifier: ^4.3.6 specifier: ^4.3.6
version: 4.4.3 version: 4.4.3
packages/onboarding:
dependencies:
'@tensamin/shared':
specifier: workspace:*
version: link:../shared
'@tensamin/storage':
specifier: workspace:*
version: link:../storage
'@tensamin/ui':
specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz
version: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
lucide-react:
specifier: ^1.14.0
version: 1.23.0(react@19.2.7)
react:
specifier: ^19.2.0
version: 19.2.7
react-dom:
specifier: ^19.2.0
version: 19.2.7(react@19.2.7)
zod:
specifier: ^4.3.6
version: 4.4.3
packages/settings: packages/settings:
dependencies: dependencies:
'@tanstack/react-router': '@tanstack/react-router':
@ -779,8 +806,8 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../storage version: link:../storage
'@tensamin/ui': '@tensamin/ui':
specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz
version: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) version: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
'@tensamin/user': '@tensamin/user':
specifier: workspace:* specifier: workspace:*
version: link:../user version: link:../user
@ -801,8 +828,8 @@ importers:
packages/shared: packages/shared:
dependencies: dependencies:
'@tensamin/ui': '@tensamin/ui':
specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz
version: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) version: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
lucide-react: lucide-react:
specifier: ^1.14.0 specifier: ^1.14.0
version: 1.23.0(react@19.2.7) version: 1.23.0(react@19.2.7)
@ -831,8 +858,8 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../shared version: link:../shared
'@tensamin/ui': '@tensamin/ui':
specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz
version: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) version: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
react: react:
specifier: ^19.2.0 specifier: ^19.2.0
version: 19.2.7 version: 19.2.7
@ -864,8 +891,8 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../../apps/tauri version: link:../../apps/tauri
'@tensamin/ui': '@tensamin/ui':
specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz
version: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3) version: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
'@tensamin/user': '@tensamin/user':
specifier: workspace:* specifier: workspace:*
version: link:../user version: link:../user
@ -2125,9 +2152,9 @@ packages:
'@tauri-apps/plugin-notification@2.3.3': '@tauri-apps/plugin-notification@2.3.3':
resolution: {integrity: sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg==} resolution: {integrity: sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg==}
'@tensamin/ui@https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz': '@tensamin/ui@https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz':
resolution: {integrity: sha512-xyzw3/XAYKczV1cYQ2QuzhFzFYbU9U2U/gi/TMU/U7MC0vp1xHCVssTcB1kot8yk9YEIEcDBK2X49Ftzj6bJeQ==, tarball: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz} resolution: {integrity: sha512-H68VGR6Dy/3cO+VhhUdUdMkPWnUqP0lDmu3v3NPdfuB++SUmSm/zVVXB0I5Y5bnNXTD3FOVLIdP+AplaXuEGDg==, tarball: https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz}
version: 0.0.40 version: 0.0.41
peerDependencies: peerDependencies:
react: ^19.2.7 react: ^19.2.7
react-dom: ^19.2.7 react-dom: ^19.2.7
@ -6206,7 +6233,7 @@ snapshots:
dependencies: dependencies:
'@tauri-apps/api': 2.11.1 '@tauri-apps/api': 2.11.1
'@tensamin/ui@https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)': '@tensamin/ui@https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)':
dependencies: dependencies:
'@base-ui/react': 1.6.0(@date-fns/tz@1.5.0)(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@base-ui/react': 1.6.0(@date-fns/tz@1.5.0)(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@fontsource-variable/inter': 5.2.8 '@fontsource-variable/inter': 5.2.8

View file

@ -6,5 +6,5 @@ allowBuilds:
electron-winstaller: true electron-winstaller: true
esbuild: true esbuild: true
overrides: overrides:
"@tensamin/ui": "https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz" "@tensamin/ui": "https://git.methanium.net/tensamin/ui/releases/download/0.0.41/tensamin-ui.tgz"
mtp: "https://git.methanium.net/methanium/mtp/releases/download/0.2.0-3919eb46fd/mtp-0.2.0.tgz" mtp: "https://git.methanium.net/methanium/mtp/releases/download/0.2.0-3919eb46fd/mtp-0.2.0.tgz"