From 4e0b0f61b90c78b13038ea69f29673b3f1d5b612 Mon Sep 17 00:00:00 2001 From: Veenoway <77930262+Veenoway@users.noreply.github.com> Date: Tue, 29 Jul 2025 17:22:03 +0200 Subject: [PATCH 1/4] refactor(Dialog): extract logic into custom hooks --- packages/account-sdk/src/ui/Dialog/Dialog.tsx | 228 +++++++----------- packages/account-sdk/src/ui/hooks/index.ts | 5 + .../src/ui/hooks/useDragToDismiss.ts | 69 ++++++ .../account-sdk/src/ui/hooks/useMediaQuery.ts | 23 ++ .../src/ui/hooks/usePhonePortrait.ts | 7 + .../account-sdk/src/ui/hooks/useUsername.ts | 46 ++++ 6 files changed, 239 insertions(+), 139 deletions(-) create mode 100644 packages/account-sdk/src/ui/hooks/index.ts create mode 100644 packages/account-sdk/src/ui/hooks/useDragToDismiss.ts create mode 100644 packages/account-sdk/src/ui/hooks/useMediaQuery.ts create mode 100644 packages/account-sdk/src/ui/hooks/usePhonePortrait.ts create mode 100644 packages/account-sdk/src/ui/hooks/useUsername.ts diff --git a/packages/account-sdk/src/ui/Dialog/Dialog.tsx b/packages/account-sdk/src/ui/Dialog/Dialog.tsx index 20df9942..70e5c3ed 100644 --- a/packages/account-sdk/src/ui/Dialog/Dialog.tsx +++ b/packages/account-sdk/src/ui/Dialog/Dialog.tsx @@ -2,48 +2,19 @@ import { clsx } from 'clsx'; import { FunctionComponent, render } from 'preact'; +import { useCallback, useEffect, useMemo, useState } from 'preact/hooks'; -import { getDisplayableUsername } from ':core/username/getDisplayableUsername.js'; -import { store } from ':store/store.js'; import { BaseLogo } from ':ui/assets/BaseLogo.js'; -import { useEffect, useMemo, useState } from 'preact/hooks'; +import { useDragToDismiss, usePhonePortrait, useUsername } from '../hooks/index.js'; import css from './Dialog-css.js'; const closeIcon = `data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTEzIDFMMSAxM20wLTEyTDEzIDEzIiBzdHJva2U9IiM5Q0EzQUYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PC9zdmc+`; -// Helper function to detect phone portrait mode -function isPhonePortrait(): boolean { - return window.innerWidth <= 600 && window.innerHeight > window.innerWidth; -} - // Handle bar component for mobile bottom sheet const DialogHandleBar: FunctionComponent = () => { - const [showHandleBar, setShowHandleBar] = useState(false); - - useEffect(() => { - // Only show handle bar on phone portrait mode - const checkOrientation = () => { - setShowHandleBar(isPhonePortrait()); - }; - - // Initial check - checkOrientation(); - - // Listen for orientation/resize changes - window.addEventListener('resize', checkOrientation); - window.addEventListener('orientationchange', checkOrientation); - - return () => { - window.removeEventListener('resize', checkOrientation); - window.removeEventListener('orientationchange', checkOrientation); - }; - }, []); - - if (!showHandleBar) { - return null; - } - - return
; + const isPhonePortrait = usePhonePortrait(); + + return isPhonePortrait ?
: null; }; export type DialogProps = { @@ -86,7 +57,18 @@ export class Dialog { this.render(); } + public dismissItem(key: number): void { + const item = this.items.get(key); + this.items.delete(key); + this.render(); + item?.onClose?.(); + } + public clear(): void { + // Call onClose for all items before clearing + for (const [, item] of this.items) { + item.onClose?.(); + } this.items.clear(); if (this.root) { render(null, this.root); @@ -94,84 +76,48 @@ export class Dialog { } private render(): void { - if (this.root) { - render( -
- - {Array.from(this.items.entries()).map(([key, itemProps]) => ( - { - this.clear(); - itemProps.onClose?.(); - }} - /> - ))} - -
, - this.root - ); - } + if (!this.root) return; + + render( +
+ + {Array.from(this.items.entries()).map(([key, itemProps]) => ( + { + this.dismissItem(key); + }} + /> + ))} + +
, + this.root + ); } } -export const DialogContainer: FunctionComponent = (props) => { - const [dragY, setDragY] = useState(0); - const [isDragging, setIsDragging] = useState(false); - const [startY, setStartY] = useState(0); - - // Touch event handlers for drag-to-dismiss (entire dialog area) - const handleTouchStart = (e: any) => { - // Only enable drag on mobile portrait mode - if (!isPhonePortrait()) return; - - const touch = e.touches[0]; - setStartY(touch.clientY); - setIsDragging(true); - }; - - const handleTouchMove = (e: any) => { - if (!isDragging) return; - - const touch = e.touches[0]; - const deltaY = touch.clientY - startY; - - // Only allow dragging down (positive deltaY) - if (deltaY > 0) { - setDragY(deltaY); - e.preventDefault(); // Prevent scrolling +export const DialogContainer: FunctionComponent = ({ children }) => { + const handleDismiss = useCallback(() => { + // Find the dialog instance and trigger its close handler + const closeButton = document.querySelector( + '.-base-acc-sdk-dialog-instance-header-close' + ) as HTMLElement; + if (closeButton) { + closeButton.click(); } - }; - - const handleTouchEnd = () => { - if (!isDragging) return; - - setIsDragging(false); + }, []); - // Dismiss if dragged down more than 100px - if (dragY > 100) { - // Find the dialog instance and trigger its close handler - const closeButton = document.querySelector( - '.-base-acc-sdk-dialog-instance-header-close' - ) as HTMLElement; - if (closeButton) { - closeButton.click(); - } - } else { - // Animate back to original position - setDragY(0); - } - }; + const { dragY, isDragging, handlers } = useDragToDismiss(handleDismiss); return (
{ }} > - {props.children} + {children}
@@ -195,8 +141,7 @@ export const DialogInstance: FunctionComponent = ({ handleClose, }) => { const [hidden, setHidden] = useState(true); - const [isLoadingUsername, setIsLoadingUsername] = useState(true); - const [username, setUsername] = useState(null); + const { isLoading: isLoadingUsername, username } = useUsername(); useEffect(() => { const timer = window.setTimeout(() => { @@ -208,26 +153,36 @@ export const DialogInstance: FunctionComponent = ({ }; }, []); - useEffect(() => { - const fetchEnsName = async () => { - const address = store.account.get().accounts?.[0]; - - if (address) { - const username = await getDisplayableUsername(address); - setUsername(username); - } - - setIsLoadingUsername(false); - }; - fetchEnsName(); - }, []); - const headerTitle = useMemo(() => { return username ? `Signed in as ${username}` : 'Base Account'; }, [username]); const shouldShowHeaderTitle = !isLoadingUsername; + // Memoize action buttons + const actionButtons = useMemo(() => { + if (!actionItems?.length) return null; + + return ( +
+ {actionItems.map((action, i) => ( + + ))} +
+ ); + }, [actionItems]); + return (
= ({
)}
-
- -
+
+
{title}
{message}
- {actionItems && actionItems.length > 0 && ( -
- {actionItems.map((action, i) => ( - - ))} -
- )} + + {actionButtons} ); -}; +}; \ No newline at end of file diff --git a/packages/account-sdk/src/ui/hooks/index.ts b/packages/account-sdk/src/ui/hooks/index.ts new file mode 100644 index 00000000..e0f64e4f --- /dev/null +++ b/packages/account-sdk/src/ui/hooks/index.ts @@ -0,0 +1,5 @@ +export { useDragToDismiss } from "./useDragToDismiss.js"; +export { useMediaQuery } from "./useMediaQuery.js"; +export { usePhonePortrait } from "./usePhonePortrait.js"; +export { useUsername } from "./useUsername.js"; + diff --git a/packages/account-sdk/src/ui/hooks/useDragToDismiss.ts b/packages/account-sdk/src/ui/hooks/useDragToDismiss.ts new file mode 100644 index 00000000..557fca99 --- /dev/null +++ b/packages/account-sdk/src/ui/hooks/useDragToDismiss.ts @@ -0,0 +1,69 @@ +import { useCallback, useState } from "preact/hooks"; +import { usePhonePortrait } from "./usePhonePortrait.js"; + +const DRAG_DISMISS_THRESHOLD = 100; + +interface DragState { + dragY: number; + isDragging: boolean; + startY: number; +} + +export function useDragToDismiss(onDismiss: () => void) { + const [dragState, setDragState] = useState({ + dragY: 0, + isDragging: false, + startY: 0 + }); + + const isPhonePortrait = usePhonePortrait(); + + const handleTouchStart = useCallback((e: TouchEvent) => { + if (!isPhonePortrait) return; + + const touch = e.touches[0]; + setDragState(prev => ({ + ...prev, + startY: touch.clientY, + isDragging: true + })); + }, [isPhonePortrait]); + + const handleTouchMove = useCallback((e: TouchEvent) => { + if (!dragState.isDragging) return; + + const touch = e.touches[0]; + const deltaY = touch.clientY - dragState.startY; + + // Only allow dragging down (positive deltaY) + if (deltaY > 0) { + setDragState(prev => ({ ...prev, dragY: deltaY })); + e.preventDefault(); // Prevent scrolling + } + }, [dragState.isDragging, dragState.startY]); + + const handleTouchEnd = useCallback(() => { + if (!dragState.isDragging) return; + + const shouldDismiss = dragState.dragY > DRAG_DISMISS_THRESHOLD; + + if (shouldDismiss) { + onDismiss(); + } else { + // Reset to original position + setDragState(prev => ({ ...prev, dragY: 0 })); + } + + setDragState(prev => ({ ...prev, isDragging: false })); + }, [dragState.isDragging, dragState.dragY, onDismiss]); + + return { + dragY: dragState.dragY, + isDragging: dragState.isDragging, + handlers: { + onTouchStart: handleTouchStart, + onTouchMove: handleTouchMove, + onTouchEnd: handleTouchEnd + } + }; +} diff --git a/packages/account-sdk/src/ui/hooks/useMediaQuery.ts b/packages/account-sdk/src/ui/hooks/useMediaQuery.ts new file mode 100644 index 00000000..33328397 --- /dev/null +++ b/packages/account-sdk/src/ui/hooks/useMediaQuery.ts @@ -0,0 +1,23 @@ +import { useEffect, useState } from "preact/hooks"; + +export function useMediaQuery(query: string): boolean { + const [matches, setMatches] = useState(() => { + if (typeof window !== 'undefined') { + return window.matchMedia(query).matches; + } + return false; + }); + + useEffect(() => { + if (typeof window === 'undefined') return; + + const mediaQuery = window.matchMedia(query); + const handler = (event: MediaQueryListEvent) => setMatches(event.matches); + + mediaQuery.addEventListener('change', handler); + return () => mediaQuery.removeEventListener('change', handler); + }, [query]); + + return matches; +} + diff --git a/packages/account-sdk/src/ui/hooks/usePhonePortrait.ts b/packages/account-sdk/src/ui/hooks/usePhonePortrait.ts new file mode 100644 index 00000000..e5a5abca --- /dev/null +++ b/packages/account-sdk/src/ui/hooks/usePhonePortrait.ts @@ -0,0 +1,7 @@ +import { useMediaQuery } from "./useMediaQuery.js"; + +const PHONE_PORTRAIT_BREAKPOINT = 600; + +export function usePhonePortrait(): boolean { + return useMediaQuery(`(max-width: ${PHONE_PORTRAIT_BREAKPOINT}px) and (orientation: portrait)`); + } \ No newline at end of file diff --git a/packages/account-sdk/src/ui/hooks/useUsername.ts b/packages/account-sdk/src/ui/hooks/useUsername.ts new file mode 100644 index 00000000..bf597433 --- /dev/null +++ b/packages/account-sdk/src/ui/hooks/useUsername.ts @@ -0,0 +1,46 @@ +import { getDisplayableUsername } from ':core/username/getDisplayableUsername.js'; +import { store } from ':store/store.js'; +import { useEffect, useRef, useState } from "preact/hooks"; + +interface UsernameState { + isLoading: boolean; + username: string | null; +} + +export function useUsername() { + const [state, setState] = useState({ + isLoading: true, + username: null + }); + + const addressRef = useRef(null); + + useEffect(() => { + const fetchUsername = async () => { + const currentAddress = store.account.get().accounts?.[0]; + + // Skip if address hasn't changed + if (currentAddress === addressRef.current) { + return; + } + + addressRef.current = currentAddress ?? null; + + if (currentAddress) { + try { + const username = await getDisplayableUsername(currentAddress); + setState({ isLoading: false, username }); + } catch (error) { + console.warn('Failed to fetch username:', error); + setState({ isLoading: false, username: null }); + } + } else { + setState({ isLoading: false, username: null }); + } + }; + + fetchUsername(); + }, []); + + return state; +} From e067ecc190297a3419169ed4dfffcdf622c24bde Mon Sep 17 00:00:00 2001 From: Veenoway <77930262+Veenoway@users.noreply.github.com> Date: Tue, 29 Jul 2025 17:29:26 +0200 Subject: [PATCH 2/4] feature: update dialog test --- .../account-sdk/src/ui/Dialog/Dialog.test.tsx | 129 +++++++++++++++++- 1 file changed, 127 insertions(+), 2 deletions(-) diff --git a/packages/account-sdk/src/ui/Dialog/Dialog.test.tsx b/packages/account-sdk/src/ui/Dialog/Dialog.test.tsx index a33f6de9..a3de1407 100644 --- a/packages/account-sdk/src/ui/Dialog/Dialog.test.tsx +++ b/packages/account-sdk/src/ui/Dialog/Dialog.test.tsx @@ -5,6 +5,37 @@ import { vi } from 'vitest'; import { DialogContainer, DialogInstance, DialogInstanceProps } from './Dialog.js'; +// Mock des hooks +vi.mock('../hooks/index.js', () => ({ + usePhonePortrait: vi.fn(() => false), + useDragToDismiss: vi.fn(() => ({ + dragY: 0, + isDragging: false, + handlers: { + onTouchStart: vi.fn(), + onTouchMove: vi.fn(), + onTouchEnd: vi.fn(), + } + })), + useUsername: vi.fn(() => ({ + isLoading: false, + username: 'testuser.eth' + })) +})); + +// Mock du store et getDisplayableUsername +vi.mock(':store/store.js', () => ({ + store: { + account: { + get: vi.fn(() => ({ accounts: ['0x123'] })) + } + } +})); + +vi.mock(':core/username/getDisplayableUsername.js', () => ({ + getDisplayableUsername: vi.fn(() => Promise.resolve('testuser.eth')) +})); + const renderDialogContainer = (props?: Partial) => render( @@ -16,6 +47,8 @@ describe('DialogContainer', () => { beforeEach(() => { vi.useFakeTimers(); vi.spyOn(window, 'setTimeout'); + // Reset mocks + vi.clearAllMocks(); }); afterEach(() => { @@ -53,6 +86,7 @@ describe('DialogContainer', () => { const button = screen.getByText('Try again'); expect(button).toBeInTheDocument(); + expect(button.tagName).toBe('BUTTON'); // Vérifie que c'est un bouton sémantique fireEvent.click(button); expect(onClick).toHaveBeenCalledTimes(1); @@ -72,6 +106,7 @@ describe('DialogContainer', () => { const button = screen.getByText('Cancel'); expect(button).toBeInTheDocument(); + expect(button.tagName).toBe('BUTTON'); fireEvent.click(button); expect(onClick).toHaveBeenCalledTimes(1); @@ -84,8 +119,11 @@ describe('DialogContainer', () => { const closeButton = document.getElementsByClassName( '-base-acc-sdk-dialog-instance-header-close' )[0]; + + expect(closeButton.tagName).toBe('BUTTON'); // Vérifie que c'est un bouton + expect(closeButton).toHaveAttribute('aria-label', 'Close dialog'); // Accessibilité + fireEvent.click(closeButton); - expect(handleClose).toHaveBeenCalledTimes(1); }); @@ -111,4 +149,91 @@ describe('DialogContainer', () => { expect(screen.getByText('Primary')).toBeInTheDocument(); expect(screen.getByText('Secondary')).toBeInTheDocument(); }); -}); + + test('displays username when loaded', () => { + renderDialogContainer(); + + // Le mock retourne 'testuser.eth' + expect(screen.getByText('Signed in as testuser.eth')).toBeInTheDocument(); + }); + + test('displays default title when no username', async () => { + // Mock pour retourner pas d'username + const { useUsername } = vi.mocked(await import('../hooks/index.js')); + useUsername.mockReturnValue({ + isLoading: false, + username: null + }); + + renderDialogContainer(); + + expect(screen.getByText('Base Account')).toBeInTheDocument(); + }); + + test('uses drag handlers from hook', async () => { + const mockHandlers = { + onTouchStart: vi.fn(), + onTouchMove: vi.fn(), + onTouchEnd: vi.fn(), + }; + + const { useDragToDismiss } = vi.mocked(await import('../hooks/index.js')); + useDragToDismiss.mockReturnValue({ + dragY: 50, + isDragging: true, + handlers: mockHandlers + }); + + renderDialogContainer(); + + const backdrop = document.getElementsByClassName('-base-acc-sdk-dialog-backdrop')[0]; + + // Vérifie que les handlers sont attachés + fireEvent.touchStart(backdrop); + fireEvent.touchMove(backdrop); + fireEvent.touchEnd(backdrop); + + expect(mockHandlers.onTouchStart).toHaveBeenCalled(); + expect(mockHandlers.onTouchMove).toHaveBeenCalled(); + expect(mockHandlers.onTouchEnd).toHaveBeenCalled(); + }); + + test('applies drag transform from hook', async () => { + const { useDragToDismiss } = vi.mocked(await import('../hooks/index.js')); + useDragToDismiss.mockReturnValue({ + dragY: 100, + isDragging: true, + handlers: { + onTouchStart: vi.fn(), + onTouchMove: vi.fn(), + onTouchEnd: vi.fn(), + } + }); + + renderDialogContainer(); + + const dialog = document.getElementsByClassName('-base-acc-sdk-dialog')[0]; + expect(dialog).toHaveStyle('transform: translateY(100px)'); + expect(dialog).toHaveStyle('transition: none'); + }); + + test('shows handle bar on phone portrait', async () => { + const { usePhonePortrait } = vi.mocked(await import('../hooks/index.js')); + usePhonePortrait.mockReturnValue(true); + + renderDialogContainer(); + + const handleBar = document.getElementsByClassName('-base-acc-sdk-dialog-handle-bar')[0]; + expect(handleBar).toBeInTheDocument(); + }); + + test('hides handle bar on desktop', async () => { + const { usePhonePortrait } = vi.mocked(await import('../hooks/index.js')); + usePhonePortrait.mockReturnValue(false); + + renderDialogContainer(); + + const handleBar = document.getElementsByClassName('-base-acc-sdk-dialog-handle-bar')[0]; + expect(handleBar).toBeUndefined(); + }); +}); \ No newline at end of file From 701fd6bf01f9b200238706c603c1947a5b97f705 Mon Sep 17 00:00:00 2001 From: Sebastien Cloiseau Date: Tue, 29 Jul 2025 22:09:53 +0200 Subject: [PATCH 3/4] feat: gitignore --- examples/testapp/.gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 examples/testapp/.gitignore diff --git a/examples/testapp/.gitignore b/examples/testapp/.gitignore new file mode 100644 index 00000000..6036b3a0 --- /dev/null +++ b/examples/testapp/.gitignore @@ -0,0 +1 @@ +src/components/test-dialog.tsx From 71d5d23d00192a8a48e59f6383bf7960a72b52e8 Mon Sep 17 00:00:00 2001 From: Sebastien Cloiseau Date: Mon, 11 Aug 2025 23:39:38 +0200 Subject: [PATCH 4/4] fix: format --- biome.json | 389 +++++++++--------- .../account-sdk/src/ui/Dialog/Dialog.test.tsx | 32 +- packages/account-sdk/src/ui/Dialog/Dialog.tsx | 12 +- packages/account-sdk/src/ui/hooks/index.ts | 9 +- .../src/ui/hooks/useDragToDismiss.ts | 66 +-- .../account-sdk/src/ui/hooks/useMediaQuery.ts | 9 +- .../src/ui/hooks/usePhonePortrait.ts | 6 +- .../account-sdk/src/ui/hooks/useUsername.ts | 12 +- 8 files changed, 260 insertions(+), 275 deletions(-) diff --git a/biome.json b/biome.json index 1cd1123d..1f822ebe 100644 --- a/biome.json +++ b/biome.json @@ -1,205 +1,186 @@ { - "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", - "vcs": { - "enabled": true, - "clientKind": "git", - "useIgnoreFile": true - }, - "files": { - "ignoreUnknown": false, - "ignore": [] - }, - "formatter": { - "enabled": true, - "useEditorconfig": true, - "formatWithErrors": false, - "indentStyle": "space", - "indentWidth": 2, - "lineEnding": "lf", - "lineWidth": 100, - "attributePosition": "auto", - "bracketSpacing": true, - "ignore": [ - "**/package.json", - "**/yarn.lock", - "coverage/**", - "**/coverage/**", - "**/build", - "**/dist", - "**/node_modules", - "**/vendor-js/**", - "**/*-css.ts", - "**/*-svg.ts" - ] - }, - "organizeImports": { - "enabled": true - }, - "linter": { - "enabled": true, - "rules": { - "recommended": false, - "a11y": { - "noBlankTarget": "error" - }, - "complexity": { - "noBannedTypes": "error", - "noExtraBooleanCast": "error", - "noMultipleSpacesInRegularExpressionLiterals": "error", - "noUselessCatch": "error", - "noUselessConstructor": "off", - "noUselessRename": "warn", - "noUselessStringConcat": "warn", - "noUselessTernary": "error", - "noUselessThisAlias": "error", - "noUselessTypeConstraint": "error", - "noUselessUndefinedInitialization": "error", - "noWith": "error", - "useArrowFunction": "warn" - }, - "correctness": { - "noConstAssign": "error", - "noConstantCondition": "error", - "noEmptyCharacterClassInRegex": "error", - "noEmptyPattern": "off", - "noGlobalObjectCalls": "error", - "noInnerDeclarations": "error", - "noInvalidConstructorSuper": "error", - "noNewSymbol": "error", - "noNonoctalDecimalEscape": "error", - "noPrecisionLoss": "error", - "noSelfAssign": "error", - "noSetterReturn": "error", - "noSwitchDeclarations": "error", - "noUndeclaredVariables": "error", - "noUnreachable": "error", - "noUnreachableSuper": "error", - "noUnsafeFinally": "error", - "noUnsafeOptionalChaining": "error", - "noUnusedImports": "error", - "noUnusedLabels": "error", - "noUnusedVariables": "error", - "useArrayLiterals": "off", - "useExhaustiveDependencies": "warn", - "useHookAtTopLevel": "error", - "useIsNan": "error", - "useJsxKeyInIterable": "error", - "useValidForDirection": "error", - "useYield": "error" - }, - "security": { - "noDangerouslySetInnerHtml": "warn" - }, - "style": { - "noArguments": "warn", - "noDoneCallback": "error", - "noNamespace": "error", - "noRestrictedGlobals": { - "level": "error", - "options": { - "deniedGlobals": [ - "parseInt" - ] - } - }, - "noUselessElse": "warn", - "noVar": "warn", - "useAsConstAssertion": "error", - "useBlockStatements": "off", - "useCollapsedElseIf": "error", - "useConsistentBuiltinInstantiation": "error", - "useTemplate": "warn" - }, - "suspicious": { - "noAssignInExpressions": "error", - "noAsyncPromiseExecutor": "error", - "noCatchAssign": "error", - "noClassAssign": "error", - "noCommentText": "error", - "noCompareNegZero": "error", - "noConsole": { - "level": "error", - "options": { - "allow": [ - "warn", - "error", - "info" - ] - } - }, - "noControlCharactersInRegex": "error", - "noDebugger": "error", - "noDuplicateCase": "error", - "noDuplicateClassMembers": "error", - "noDuplicateJsxProps": "error", - "noDuplicateObjectKeys": "error", - "noDuplicateParameters": "error", - "noEmptyBlockStatements": "off", - "noExplicitAny": "warn", - "noExportsInTest": "error", - "noExtraNonNullAssertion": "error", - "noFallthroughSwitchClause": "error", - "noFocusedTests": "error", - "noFunctionAssign": "error", - "noGlobalAssign": "error", - "noImportAssign": "error", - "noMisleadingCharacterClass": "error", - "noMisleadingInstantiator": "error", - "noMisplacedAssertion": "error", - "noPrototypeBuiltins": "error", - "noRedeclare": "error", - "noShadowRestrictedNames": "error", - "noSkippedTests": "warn", - "noSparseArray": "error", - "noUnsafeDeclarationMerging": "error", - "noUnsafeNegation": "error", - "useGetterReturn": "error", - "useValidTypeof": "error" - } - }, - "ignore": [ - "**/*.md", - "**/build", - "**/dist", - "**/node_modules", - "**/vendor-js/**", - "**/*.json" - ] - }, - "javascript": { - "formatter": { - "jsxQuoteStyle": "double", - "quoteProperties": "asNeeded", - "trailingCommas": "es5", - "semicolons": "always", - "arrowParentheses": "always", - "bracketSameLine": false, - "quoteStyle": "single", - "attributePosition": "auto", - "bracketSpacing": true - }, - "jsxRuntime": "transparent", - "globals": [ - "global", - "browser", - "expect" - ] - }, - "overrides": [ - { - "include": [ - "**/*.test.*" - ], - "linter": { - "rules": { - "suspicious": { - "noExplicitAny": "off" - }, - "correctness": { - "noUndeclaredVariables": "off" - } - } - } - } - ] -} \ No newline at end of file + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": false, + "ignore": [] + }, + "formatter": { + "enabled": true, + "useEditorconfig": true, + "formatWithErrors": false, + "indentStyle": "space", + "indentWidth": 2, + "lineEnding": "lf", + "lineWidth": 100, + "attributePosition": "auto", + "bracketSpacing": true, + "ignore": [ + "**/package.json", + "**/yarn.lock", + "coverage/**", + "**/coverage/**", + "**/build", + "**/dist", + "**/node_modules", + "**/vendor-js/**", + "**/*-css.ts", + "**/*-svg.ts" + ] + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": false, + "a11y": { + "noBlankTarget": "error" + }, + "complexity": { + "noBannedTypes": "error", + "noExtraBooleanCast": "error", + "noMultipleSpacesInRegularExpressionLiterals": "error", + "noUselessCatch": "error", + "noUselessConstructor": "off", + "noUselessRename": "warn", + "noUselessStringConcat": "warn", + "noUselessTernary": "error", + "noUselessThisAlias": "error", + "noUselessTypeConstraint": "error", + "noUselessUndefinedInitialization": "error", + "noWith": "error", + "useArrowFunction": "warn" + }, + "correctness": { + "noConstAssign": "error", + "noConstantCondition": "error", + "noEmptyCharacterClassInRegex": "error", + "noEmptyPattern": "off", + "noGlobalObjectCalls": "error", + "noInnerDeclarations": "error", + "noInvalidConstructorSuper": "error", + "noNewSymbol": "error", + "noNonoctalDecimalEscape": "error", + "noPrecisionLoss": "error", + "noSelfAssign": "error", + "noSetterReturn": "error", + "noSwitchDeclarations": "error", + "noUndeclaredVariables": "error", + "noUnreachable": "error", + "noUnreachableSuper": "error", + "noUnsafeFinally": "error", + "noUnsafeOptionalChaining": "error", + "noUnusedImports": "error", + "noUnusedLabels": "error", + "noUnusedVariables": "error", + "useArrayLiterals": "off", + "useExhaustiveDependencies": "warn", + "useHookAtTopLevel": "error", + "useIsNan": "error", + "useJsxKeyInIterable": "error", + "useValidForDirection": "error", + "useYield": "error" + }, + "security": { + "noDangerouslySetInnerHtml": "warn" + }, + "style": { + "noArguments": "warn", + "noDoneCallback": "error", + "noNamespace": "error", + "noRestrictedGlobals": { + "level": "error", + "options": { + "deniedGlobals": ["parseInt"] + } + }, + "noUselessElse": "warn", + "noVar": "warn", + "useAsConstAssertion": "error", + "useBlockStatements": "off", + "useCollapsedElseIf": "error", + "useConsistentBuiltinInstantiation": "error", + "useTemplate": "warn" + }, + "suspicious": { + "noAssignInExpressions": "error", + "noAsyncPromiseExecutor": "error", + "noCatchAssign": "error", + "noClassAssign": "error", + "noCommentText": "error", + "noCompareNegZero": "error", + "noConsole": { + "level": "error", + "options": { + "allow": ["warn", "error", "info"] + } + }, + "noControlCharactersInRegex": "error", + "noDebugger": "error", + "noDuplicateCase": "error", + "noDuplicateClassMembers": "error", + "noDuplicateJsxProps": "error", + "noDuplicateObjectKeys": "error", + "noDuplicateParameters": "error", + "noEmptyBlockStatements": "off", + "noExplicitAny": "warn", + "noExportsInTest": "error", + "noExtraNonNullAssertion": "error", + "noFallthroughSwitchClause": "error", + "noFocusedTests": "error", + "noFunctionAssign": "error", + "noGlobalAssign": "error", + "noImportAssign": "error", + "noMisleadingCharacterClass": "error", + "noMisleadingInstantiator": "error", + "noMisplacedAssertion": "error", + "noPrototypeBuiltins": "error", + "noRedeclare": "error", + "noShadowRestrictedNames": "error", + "noSkippedTests": "warn", + "noSparseArray": "error", + "noUnsafeDeclarationMerging": "error", + "noUnsafeNegation": "error", + "useGetterReturn": "error", + "useValidTypeof": "error" + } + }, + "ignore": ["**/*.md", "**/build", "**/dist", "**/node_modules", "**/vendor-js/**", "**/*.json"] + }, + "javascript": { + "formatter": { + "jsxQuoteStyle": "double", + "quoteProperties": "asNeeded", + "trailingCommas": "es5", + "semicolons": "always", + "arrowParentheses": "always", + "bracketSameLine": false, + "quoteStyle": "single", + "attributePosition": "auto", + "bracketSpacing": true + }, + "jsxRuntime": "transparent", + "globals": ["global", "browser", "expect"] + }, + "overrides": [ + { + "include": ["**/*.test.*"], + "linter": { + "rules": { + "suspicious": { + "noExplicitAny": "off" + }, + "correctness": { + "noUndeclaredVariables": "off" + } + } + } + } + ] +} diff --git a/packages/account-sdk/src/ui/Dialog/Dialog.test.tsx b/packages/account-sdk/src/ui/Dialog/Dialog.test.tsx index a3de1407..1fbfa9d4 100644 --- a/packages/account-sdk/src/ui/Dialog/Dialog.test.tsx +++ b/packages/account-sdk/src/ui/Dialog/Dialog.test.tsx @@ -15,25 +15,25 @@ vi.mock('../hooks/index.js', () => ({ onTouchStart: vi.fn(), onTouchMove: vi.fn(), onTouchEnd: vi.fn(), - } + }, })), useUsername: vi.fn(() => ({ isLoading: false, - username: 'testuser.eth' - })) + username: 'testuser.eth', + })), })); // Mock du store et getDisplayableUsername vi.mock(':store/store.js', () => ({ store: { account: { - get: vi.fn(() => ({ accounts: ['0x123'] })) - } - } + get: vi.fn(() => ({ accounts: ['0x123'] })), + }, + }, })); vi.mock(':core/username/getDisplayableUsername.js', () => ({ - getDisplayableUsername: vi.fn(() => Promise.resolve('testuser.eth')) + getDisplayableUsername: vi.fn(() => Promise.resolve('testuser.eth')), })); const renderDialogContainer = (props?: Partial) => @@ -119,10 +119,10 @@ describe('DialogContainer', () => { const closeButton = document.getElementsByClassName( '-base-acc-sdk-dialog-instance-header-close' )[0]; - + expect(closeButton.tagName).toBe('BUTTON'); // Vérifie que c'est un bouton expect(closeButton).toHaveAttribute('aria-label', 'Close dialog'); // Accessibilité - + fireEvent.click(closeButton); expect(handleClose).toHaveBeenCalledTimes(1); }); @@ -152,7 +152,7 @@ describe('DialogContainer', () => { test('displays username when loaded', () => { renderDialogContainer(); - + // Le mock retourne 'testuser.eth' expect(screen.getByText('Signed in as testuser.eth')).toBeInTheDocument(); }); @@ -162,11 +162,11 @@ describe('DialogContainer', () => { const { useUsername } = vi.mocked(await import('../hooks/index.js')); useUsername.mockReturnValue({ isLoading: false, - username: null + username: null, }); renderDialogContainer(); - + expect(screen.getByText('Base Account')).toBeInTheDocument(); }); @@ -181,13 +181,13 @@ describe('DialogContainer', () => { useDragToDismiss.mockReturnValue({ dragY: 50, isDragging: true, - handlers: mockHandlers + handlers: mockHandlers, }); renderDialogContainer(); const backdrop = document.getElementsByClassName('-base-acc-sdk-dialog-backdrop')[0]; - + // Vérifie que les handlers sont attachés fireEvent.touchStart(backdrop); fireEvent.touchMove(backdrop); @@ -207,7 +207,7 @@ describe('DialogContainer', () => { onTouchStart: vi.fn(), onTouchMove: vi.fn(), onTouchEnd: vi.fn(), - } + }, }); renderDialogContainer(); @@ -236,4 +236,4 @@ describe('DialogContainer', () => { const handleBar = document.getElementsByClassName('-base-acc-sdk-dialog-handle-bar')[0]; expect(handleBar).toBeUndefined(); }); -}); \ No newline at end of file +}); diff --git a/packages/account-sdk/src/ui/Dialog/Dialog.tsx b/packages/account-sdk/src/ui/Dialog/Dialog.tsx index 70e5c3ed..56602683 100644 --- a/packages/account-sdk/src/ui/Dialog/Dialog.tsx +++ b/packages/account-sdk/src/ui/Dialog/Dialog.tsx @@ -13,7 +13,7 @@ const closeIcon = `data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQ // Handle bar component for mobile bottom sheet const DialogHandleBar: FunctionComponent = () => { const isPhonePortrait = usePhonePortrait(); - + return isPhonePortrait ?
: null; }; @@ -205,20 +205,20 @@ export const DialogInstance: FunctionComponent = ({ type="button" aria-label="Close dialog" > - Close
- +
{title}
{message}
- + {actionButtons} ); -}; \ No newline at end of file +}; diff --git a/packages/account-sdk/src/ui/hooks/index.ts b/packages/account-sdk/src/ui/hooks/index.ts index e0f64e4f..064451bd 100644 --- a/packages/account-sdk/src/ui/hooks/index.ts +++ b/packages/account-sdk/src/ui/hooks/index.ts @@ -1,5 +1,4 @@ -export { useDragToDismiss } from "./useDragToDismiss.js"; -export { useMediaQuery } from "./useMediaQuery.js"; -export { usePhonePortrait } from "./usePhonePortrait.js"; -export { useUsername } from "./useUsername.js"; - +export { useDragToDismiss } from './useDragToDismiss.js'; +export { useMediaQuery } from './useMediaQuery.js'; +export { usePhonePortrait } from './usePhonePortrait.js'; +export { useUsername } from './useUsername.js'; diff --git a/packages/account-sdk/src/ui/hooks/useDragToDismiss.ts b/packages/account-sdk/src/ui/hooks/useDragToDismiss.ts index 557fca99..9b485372 100644 --- a/packages/account-sdk/src/ui/hooks/useDragToDismiss.ts +++ b/packages/account-sdk/src/ui/hooks/useDragToDismiss.ts @@ -1,5 +1,5 @@ -import { useCallback, useState } from "preact/hooks"; -import { usePhonePortrait } from "./usePhonePortrait.js"; +import { useCallback, useState } from 'preact/hooks'; +import { usePhonePortrait } from './usePhonePortrait.js'; const DRAG_DISMISS_THRESHOLD = 100; @@ -13,48 +13,54 @@ export function useDragToDismiss(onDismiss: () => void) { const [dragState, setDragState] = useState({ dragY: 0, isDragging: false, - startY: 0 + startY: 0, }); - + const isPhonePortrait = usePhonePortrait(); - const handleTouchStart = useCallback((e: TouchEvent) => { - if (!isPhonePortrait) return; - - const touch = e.touches[0]; - setDragState(prev => ({ - ...prev, - startY: touch.clientY, - isDragging: true - })); - }, [isPhonePortrait]); + const handleTouchStart = useCallback( + (e: TouchEvent) => { + if (!isPhonePortrait) return; - const handleTouchMove = useCallback((e: TouchEvent) => { - if (!dragState.isDragging) return; + const touch = e.touches[0]; + setDragState((prev) => ({ + ...prev, + startY: touch.clientY, + isDragging: true, + })); + }, + [isPhonePortrait] + ); - const touch = e.touches[0]; - const deltaY = touch.clientY - dragState.startY; + const handleTouchMove = useCallback( + (e: TouchEvent) => { + if (!dragState.isDragging) return; - // Only allow dragging down (positive deltaY) - if (deltaY > 0) { - setDragState(prev => ({ ...prev, dragY: deltaY })); - e.preventDefault(); // Prevent scrolling - } - }, [dragState.isDragging, dragState.startY]); + const touch = e.touches[0]; + const deltaY = touch.clientY - dragState.startY; + + // Only allow dragging down (positive deltaY) + if (deltaY > 0) { + setDragState((prev) => ({ ...prev, dragY: deltaY })); + e.preventDefault(); // Prevent scrolling + } + }, + [dragState.isDragging, dragState.startY] + ); const handleTouchEnd = useCallback(() => { if (!dragState.isDragging) return; const shouldDismiss = dragState.dragY > DRAG_DISMISS_THRESHOLD; - + if (shouldDismiss) { onDismiss(); } else { // Reset to original position - setDragState(prev => ({ ...prev, dragY: 0 })); + setDragState((prev) => ({ ...prev, dragY: 0 })); } - - setDragState(prev => ({ ...prev, isDragging: false })); + + setDragState((prev) => ({ ...prev, isDragging: false })); }, [dragState.isDragging, dragState.dragY, onDismiss]); return { @@ -63,7 +69,7 @@ export function useDragToDismiss(onDismiss: () => void) { handlers: { onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, - onTouchEnd: handleTouchEnd - } + onTouchEnd: handleTouchEnd, + }, }; } diff --git a/packages/account-sdk/src/ui/hooks/useMediaQuery.ts b/packages/account-sdk/src/ui/hooks/useMediaQuery.ts index 33328397..c8b8aa1a 100644 --- a/packages/account-sdk/src/ui/hooks/useMediaQuery.ts +++ b/packages/account-sdk/src/ui/hooks/useMediaQuery.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from "preact/hooks"; +import { useEffect, useState } from 'preact/hooks'; export function useMediaQuery(query: string): boolean { const [matches, setMatches] = useState(() => { @@ -9,15 +9,14 @@ export function useMediaQuery(query: string): boolean { }); useEffect(() => { - if (typeof window === 'undefined') return; - + if (typeof window === 'undefined') return; + const mediaQuery = window.matchMedia(query); const handler = (event: MediaQueryListEvent) => setMatches(event.matches); - + mediaQuery.addEventListener('change', handler); return () => mediaQuery.removeEventListener('change', handler); }, [query]); return matches; } - diff --git a/packages/account-sdk/src/ui/hooks/usePhonePortrait.ts b/packages/account-sdk/src/ui/hooks/usePhonePortrait.ts index e5a5abca..38a971c9 100644 --- a/packages/account-sdk/src/ui/hooks/usePhonePortrait.ts +++ b/packages/account-sdk/src/ui/hooks/usePhonePortrait.ts @@ -1,7 +1,7 @@ -import { useMediaQuery } from "./useMediaQuery.js"; +import { useMediaQuery } from './useMediaQuery.js'; const PHONE_PORTRAIT_BREAKPOINT = 600; export function usePhonePortrait(): boolean { - return useMediaQuery(`(max-width: ${PHONE_PORTRAIT_BREAKPOINT}px) and (orientation: portrait)`); - } \ No newline at end of file + return useMediaQuery(`(max-width: ${PHONE_PORTRAIT_BREAKPOINT}px) and (orientation: portrait)`); +} diff --git a/packages/account-sdk/src/ui/hooks/useUsername.ts b/packages/account-sdk/src/ui/hooks/useUsername.ts index bf597433..a1c3b4b6 100644 --- a/packages/account-sdk/src/ui/hooks/useUsername.ts +++ b/packages/account-sdk/src/ui/hooks/useUsername.ts @@ -1,6 +1,6 @@ import { getDisplayableUsername } from ':core/username/getDisplayableUsername.js'; import { store } from ':store/store.js'; -import { useEffect, useRef, useState } from "preact/hooks"; +import { useEffect, useRef, useState } from 'preact/hooks'; interface UsernameState { isLoading: boolean; @@ -10,22 +10,22 @@ interface UsernameState { export function useUsername() { const [state, setState] = useState({ isLoading: true, - username: null + username: null, }); - + const addressRef = useRef(null); useEffect(() => { const fetchUsername = async () => { const currentAddress = store.account.get().accounts?.[0]; - + // Skip if address hasn't changed if (currentAddress === addressRef.current) { return; } - + addressRef.current = currentAddress ?? null; - + if (currentAddress) { try { const username = await getDisplayableUsername(currentAddress);