From acdc4b7017f1e6258aad9e45de3504b4918eaea3 Mon Sep 17 00:00:00 2001
From: Bekiboo
Date: Wed, 28 Jan 2026 09:17:13 +0300
Subject: [PATCH 1/8] fix: add allowClickOutside prop to Modal for improved
interaction
---
.../pictique/src/lib/ui/Modal/Modal.svelte | 21 +++++++++++++++++--
1 file changed, 19 insertions(+), 2 deletions(-)
diff --git a/platforms/pictique/src/lib/ui/Modal/Modal.svelte b/platforms/pictique/src/lib/ui/Modal/Modal.svelte
index 9fea3b835..528964d4e 100644
--- a/platforms/pictique/src/lib/ui/Modal/Modal.svelte
+++ b/platforms/pictique/src/lib/ui/Modal/Modal.svelte
@@ -4,10 +4,11 @@
interface IModalProps {
open: boolean;
onclose?: () => void;
+ allowClickOutside?: boolean;
children?: Snippet;
}
- const { open, onclose, children }: IModalProps = $props();
+ const { open, onclose, allowClickOutside, children }: IModalProps = $props();
let modal: HTMLDialogElement | null = $state(null);
@@ -19,9 +20,25 @@
event.clientY <= rect.top + rect.height &&
rect.left <= event.clientX &&
event.clientX <= rect.left + rect.width;
- if (!isInDialog) {
+ if (!isInDialog && allowClickOutside) {
modal.close();
onclose?.();
+ } else {
+ event.stopPropagation();
+ // make the modal pulse to indicate it is still open
+ modal.animate(
+ [
+ { transform: 'scale(1)' },
+ { transform: 'scale(1.025)' },
+ { transform: 'scale(1)' },
+ { transform: 'scale(1.0125)' },
+ { transform: 'scale(1)' }
+ ],
+ {
+ duration: 250,
+ easing: 'ease-in-out'
+ }
+ );
}
};
From 6a958873e983e7c4ef95ce341ebda21007983651 Mon Sep 17 00:00:00 2001
From: Bekiboo
Date: Wed, 28 Jan 2026 11:06:43 +0300
Subject: [PATCH 2/8] fix: enhance DisclaimerModal with tooltip and animation
effects
---
.../src/components/disclaimer-modal.tsx | 190 +++++++++++-------
.../src/components/ui/dialog.tsx | 16 +-
2 files changed, 129 insertions(+), 77 deletions(-)
diff --git a/platforms/group-charter-manager/src/components/disclaimer-modal.tsx b/platforms/group-charter-manager/src/components/disclaimer-modal.tsx
index 18fa3aaa5..f806f1b04 100644
--- a/platforms/group-charter-manager/src/components/disclaimer-modal.tsx
+++ b/platforms/group-charter-manager/src/components/disclaimer-modal.tsx
@@ -9,80 +9,128 @@ import {
DialogDescription,
DialogFooter,
} from "@/components/ui/dialog";
-import { useState } from "react";
-import { useAuth } from "@/components/auth/auth-provider";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import { useState, useEffect } from "react";
+import { cn } from "@/lib/utils";
+
+const DISCLAIMER_KEY = "group-charter-disclaimer-accepted";
export default function DisclaimerModal() {
- const { logout } = useAuth();
- const [disclaimerAccepted, setDisclaimerAccepted] = useState(false);
+ const [disclaimerAccepted, setDisclaimerAccepted] = useState(true); // Start as true to prevent flash
+ const [showHint, setShowHint] = useState(false);
+ const [isPulsing, setIsPulsing] = useState(false);
+
+ useEffect(() => {
+ // Check if disclaimer was previously accepted
+ const accepted = localStorage.getItem(DISCLAIMER_KEY) === "true";
+ setDisclaimerAccepted(accepted);
+ }, []);
+
+ const handleInteractOutside = (e: Event) => {
+ e.preventDefault();
+ setIsPulsing(true);
+ setShowHint(true);
+ setTimeout(() => setIsPulsing(false), 400);
+ };
+
+ if (disclaimerAccepted) return null;
+
return (
- <>
- {!disclaimerAccepted ? (
-
>
);
diff --git a/platforms/eVoting/src/components/ui/dialog.tsx b/platforms/eVoting/src/components/ui/dialog.tsx
index 248380ea1..6382ca292 100644
--- a/platforms/eVoting/src/components/ui/dialog.tsx
+++ b/platforms/eVoting/src/components/ui/dialog.tsx
@@ -31,8 +31,10 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, children, ...props }, ref) => (
+ React.ComponentPropsWithoutRef & {
+ hideCloseButton?: boolean
+ }
+>(({ className, children, hideCloseButton, ...props }, ref) => (
{children}
+ {!hideCloseButton && (
+
+
+ Close
+
+ )}
));
From ea5e8b78a8cb9a89810204bcca4a7b6314250765 Mon Sep 17 00:00:00 2001
From: Bekiboo
Date: Wed, 28 Jan 2026 11:31:21 +0300
Subject: [PATCH 5/8] fix: improve disclaimer handling with localStorage check
and user hint
---
.../pictique/src/lib/ui/Modal/Modal.svelte | 3 ++-
.../src/routes/(protected)/+layout.svelte | 27 +++++++++++++++----
2 files changed, 24 insertions(+), 6 deletions(-)
diff --git a/platforms/pictique/src/lib/ui/Modal/Modal.svelte b/platforms/pictique/src/lib/ui/Modal/Modal.svelte
index 528964d4e..b22ac495c 100644
--- a/platforms/pictique/src/lib/ui/Modal/Modal.svelte
+++ b/platforms/pictique/src/lib/ui/Modal/Modal.svelte
@@ -23,7 +23,7 @@
if (!isInDialog && allowClickOutside) {
modal.close();
onclose?.();
- } else {
+ } else if (!isInDialog) {
event.stopPropagation();
// make the modal pulse to indicate it is still open
modal.animate(
@@ -39,6 +39,7 @@
easing: 'ease-in-out'
}
);
+ onclose?.();
}
};
diff --git a/platforms/pictique/src/routes/(protected)/+layout.svelte b/platforms/pictique/src/routes/(protected)/+layout.svelte
index 73a5043ff..161260259 100644
--- a/platforms/pictique/src/routes/(protected)/+layout.svelte
+++ b/platforms/pictique/src/routes/(protected)/+layout.svelte
@@ -18,6 +18,9 @@
let profile = $state(null);
let confirmedDisclaimer = $state(false);
+ let showHint = $state(false);
+
+ const DISCLAIMER_KEY = 'pictique-disclaimer-accepted';
async function fetchProfile() {
ownerId = getAuthId();
@@ -37,7 +40,14 @@
}
}
- onMount(fetchProfile);
+ onMount(() => {
+ fetchProfile();
+ const accepted = localStorage.getItem(DISCLAIMER_KEY) === 'true';
+ if (accepted) {
+ confirmedDisclaimer = true;
+ closeDisclaimerModal();
+ }
+ });
@@ -59,9 +69,7 @@
open={$isDisclaimerModalOpen}
onclose={() => {
if (!confirmedDisclaimer) {
- removeAuthToken();
- removeAuthId();
- goto('/auth');
+ showHint = true;
}
}}
>
@@ -91,11 +99,20 @@
{
+ localStorage.setItem(DISCLAIMER_KEY, 'true');
closeDisclaimerModal();
confirmedDisclaimer = true;
}}>I Understand
+ {#if showHint}
+
+ 💡 You must accept the disclaimer to continue. This will only appear once.
+
+ {/if}
From 43162b44f2ad8c172d59e1fe0984394d4f2822df Mon Sep 17 00:00:00 2001
From: Bekiboo
Date: Wed, 28 Jan 2026 12:04:02 +0300
Subject: [PATCH 6/8] fix: update Modal to use onClickOutside prop for improved
interaction and animation
---
.../pictique/src/lib/ui/Modal/Modal.svelte | 31 ++++++-------------
.../src/routes/(protected)/+layout.svelte | 16 ++++++++--
2 files changed, 24 insertions(+), 23 deletions(-)
diff --git a/platforms/pictique/src/lib/ui/Modal/Modal.svelte b/platforms/pictique/src/lib/ui/Modal/Modal.svelte
index b22ac495c..00e66339f 100644
--- a/platforms/pictique/src/lib/ui/Modal/Modal.svelte
+++ b/platforms/pictique/src/lib/ui/Modal/Modal.svelte
@@ -4,11 +4,11 @@
interface IModalProps {
open: boolean;
onclose?: () => void;
- allowClickOutside?: boolean;
+ onClickOutside?: (modal: HTMLDialogElement) => void;
children?: Snippet;
}
- const { open, onclose, allowClickOutside, children }: IModalProps = $props();
+ const { open, onclose, onClickOutside, children }: IModalProps = $props();
let modal: HTMLDialogElement | null = $state(null);
@@ -20,26 +20,15 @@
event.clientY <= rect.top + rect.height &&
rect.left <= event.clientX &&
event.clientX <= rect.left + rect.width;
- if (!isInDialog && allowClickOutside) {
- modal.close();
- onclose?.();
- } else if (!isInDialog) {
+ if (!isInDialog) {
event.stopPropagation();
- // make the modal pulse to indicate it is still open
- modal.animate(
- [
- { transform: 'scale(1)' },
- { transform: 'scale(1.025)' },
- { transform: 'scale(1)' },
- { transform: 'scale(1.0125)' },
- { transform: 'scale(1)' }
- ],
- {
- duration: 250,
- easing: 'ease-in-out'
- }
- );
- onclose?.();
+ if (onClickOutside) {
+ onClickOutside(modal);
+ } else {
+ // Default behavior: close the modal
+ modal.close();
+ onclose?.();
+ }
}
};
diff --git a/platforms/pictique/src/routes/(protected)/+layout.svelte b/platforms/pictique/src/routes/(protected)/+layout.svelte
index 161260259..5c21bb191 100644
--- a/platforms/pictique/src/routes/(protected)/+layout.svelte
+++ b/platforms/pictique/src/routes/(protected)/+layout.svelte
@@ -8,7 +8,6 @@
import type { userProfile } from '$lib/types';
import { Button, Modal } from '$lib/ui';
import { apiClient, getAuthId, getAuthToken } from '$lib/utils';
- import { removeAuthId, removeAuthToken } from '$lib/utils';
import type { AxiosError } from 'axios';
import { onMount } from 'svelte';
@@ -67,9 +66,22 @@
{
+ onClickOutside={(modal) => {
if (!confirmedDisclaimer) {
showHint = true;
+ modal.animate(
+ [
+ { transform: 'scale(1)' },
+ { transform: 'scale(1.025)' },
+ { transform: 'scale(1)' },
+ { transform: 'scale(1.0125)' },
+ { transform: 'scale(1)' }
+ ],
+ {
+ duration: 250,
+ easing: 'ease-in-out'
+ }
+ );
}
}}
>
From 728d13870f71023fd6b0628ba9f6708f16328fb1 Mon Sep 17 00:00:00 2001
From: Bekiboo
Date: Wed, 28 Jan 2026 12:13:48 +0300
Subject: [PATCH 7/8] fix: implement safe localStorage access methods for
disclaimer handling across components
---
.../src/components/layout/common-layout.tsx | 21 +++++++++++++--
platforms/eVoting/src/app/(app)/layout.tsx | 26 ++++++++++++++-----
.../src/components/disclaimer-modal.tsx | 21 +++++++++++++--
.../src/routes/(protected)/+layout.svelte | 21 +++++++++++++--
4 files changed, 77 insertions(+), 12 deletions(-)
diff --git a/platforms/blabsy/src/components/layout/common-layout.tsx b/platforms/blabsy/src/components/layout/common-layout.tsx
index 09d0c27bc..b3ff406b9 100644
--- a/platforms/blabsy/src/components/layout/common-layout.tsx
+++ b/platforms/blabsy/src/components/layout/common-layout.tsx
@@ -13,6 +13,23 @@ export type LayoutProps = {
const DISCLAIMER_KEY = 'blabsy-disclaimer-accepted';
+// Safe localStorage access for restricted environments
+const safeGetItem = (key: string): string | null => {
+ try {
+ return localStorage.getItem(key);
+ } catch {
+ return null;
+ }
+};
+
+const safeSetItem = (key: string, value: string): void => {
+ try {
+ localStorage.setItem(key, value);
+ } catch {
+ // Silently fail in restricted environments
+ }
+};
+
export function ProtectedLayout({ children }: LayoutProps): JSX.Element {
const user = useRequireAuth();
const { signOut } = useAuth();
@@ -22,7 +39,7 @@ export function ProtectedLayout({ children }: LayoutProps): JSX.Element {
const [isPulsing, setIsPulsing] = useState(false);
useEffect(() => {
- const accepted = localStorage.getItem(DISCLAIMER_KEY) === 'true';
+ const accepted = safeGetItem(DISCLAIMER_KEY) === 'true';
setDisclaimerAccepted(accepted);
}, []);
@@ -98,7 +115,7 @@ export function ProtectedLayout({ children }: LayoutProps): JSX.Element {
type='button'
className='w-full bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600'
onClick={() => {
- localStorage.setItem(DISCLAIMER_KEY, 'true');
+ safeSetItem(DISCLAIMER_KEY, 'true');
setDisclaimerAccepted(true);
}}
>
diff --git a/platforms/eVoting/src/app/(app)/layout.tsx b/platforms/eVoting/src/app/(app)/layout.tsx
index a3ffb9f87..6ac642d6d 100644
--- a/platforms/eVoting/src/app/(app)/layout.tsx
+++ b/platforms/eVoting/src/app/(app)/layout.tsx
@@ -1,6 +1,5 @@
"use client";
import Navigation from "@/components/navigation";
-import { useAuth } from "@/lib/auth-context";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
@@ -23,6 +22,23 @@ import { cn } from "@/lib/utils";
const DISCLAIMER_KEY = "evoting-disclaimer-accepted";
+// Safe localStorage access for restricted environments
+const safeGetItem = (key: string): string | null => {
+ try {
+ return localStorage.getItem(key);
+ } catch {
+ return null;
+ }
+};
+
+const safeSetItem = (key: string, value: string): void => {
+ try {
+ localStorage.setItem(key, value);
+ } catch {
+ // Silently fail in restricted environments
+ }
+};
+
// Deeplink handling for reveal functionality
declare global {
interface WindowEventMap {
@@ -35,14 +51,13 @@ export default function AppLayout({
}: Readonly<{
children: React.ReactNode;
}>) {
- const { logout } = useAuth();
const [disclaimerAccepted, setDisclaimerAccepted] = useState(false);
const [isPulsing, setIsPulsing] = useState(false);
const [showHint, setShowHint] = useState(false);
const router = useRouter();
useEffect(() => {
- const accepted = localStorage.getItem(DISCLAIMER_KEY) === "true";
+ const accepted = safeGetItem(DISCLAIMER_KEY) === "true";
if (accepted) {
setDisclaimerAccepted(true);
}
@@ -96,8 +111,7 @@ export default function AppLayout({
info@metastate.foundation
@@ -114,8 +121,13 @@ export function ProtectedLayout({ children }: LayoutProps): JSX.Element {
{
- safeSetItem(DISCLAIMER_KEY, 'true');
+ try {
+ localStorage.setItem(DISCLAIMER_KEY, 'true');
+ } catch {
+ // Ignore storage failures; allow access for this session.
+ }
setDisclaimerAccepted(true);
}}
>