Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .evolve/tangle-cloud-design-audit-2026-06-08.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Tangle Cloud Design Audit - 2026-06-08

Status: implemented and browser verified on `feat/tangle-cloud-tangle-dapp-system`.

## Verdict

The Blueprints page moved from glass-on-glass catalog cards to a list-first infrastructure console that matches the original Tangle dapp's operational hierarchy while keeping Cloud-specific density. The remaining surface is not a marketing hero; it is a service catalog for deployers, operators, and publishers.

## Problems Addressed

- Duplicate Blueprints title in top nav and page header.
- Separate nav/title layer doing no unique work.
- Blueprints catalog readability at roughly 6/10: same-background cards, weak hierarchy, small labels, hidden wallet address, italic controls, and decorative button dots.
- Missing default blueprint visual identity for chain-only blueprints.
- Local preview defaulting to Anvil and firing localhost RPC/indexer calls while the UI showed Base Sepolia.
- Blueprint data refetching felt uncached after leaving and returning to the page.
- Add-capacity/register flow could look blank or broken.

## Product Direction

- Use the original Tangle dapp design system selectively: wallet/network affordances, compact chrome, crisp borders, action hierarchy, and restrained Tangle color accents.
- Keep Cloud as an infrastructure console: list/table first, cards only for repeated catalog/mobile/modals, no hero marketing treatment.
- Make Blueprints read like inventory: capacity, trust, source, usage, and actions are first-order columns.

## Changes Shipped

- Removed the `/blueprints` top-nav breadcrumb title and let the page own the large `Blueprints` heading.
- Kept wallet connection visible in the top-right shell and made the connected-address affordance visible.
- Forced network/action controls to normal font style and removed button pseudo-dot artifacts.
- Added default blueprint visuals for chain-only catalog rows/cards.
- Added list-first catalog layout with metrics, availability filters, category/filter controls, grid/list toggle, pagination, and mobile cards.
- Added React Query stale/cache behavior for blueprint queries.
- Normalized Cloud's local-preview network to Base Sepolia unless `VITE_FORCE_LOCAL_CHAIN=true`.
- Moved network normalization before indexer health checks and aligned audited-status reads with the selected Cloud network.
- Made the local sandbox `Button` wrapper forward refs for Radix `asChild` compatibility.
- Tightened the add-capacity drawer into a true right-side panel.
- Stubbed `window.scrollTo` in shared Vitest setup to remove noisy route-test output.

## Verification

- `NX_DAEMON=false NX_SKIP_NX_CACHE=true yarn nx typecheck tangle-cloud`
- `NX_DAEMON=false NX_SKIP_NX_CACHE=true yarn nx test tangle-cloud`
- `NX_DAEMON=false NX_SKIP_NX_CACHE=true yarn nx build tangle-cloud`
- Playwright screenshots and console checks:
- `/tmp/tangle-cloud-dapp-system-audit/blueprints-desktop-dark.png`
- `/tmp/tangle-cloud-dapp-system-audit/blueprints-desktop-light.png`
- `/tmp/tangle-cloud-dapp-system-audit/blueprints-mobile-dark.png`
- `/tmp/tangle-cloud-dapp-system-audit/instances-desktop-dark.png`
- `/tmp/tangle-cloud-dapp-system-audit/blueprints-add-capacity-flow.png`

Final browser counters:

- `local8545`: 0
- `local8080`: 0
- Radix ref warnings: 0
- italic controls: 0
- measured overflow: 0
3 changes: 2 additions & 1 deletion apps/tangle-cloud/PRODUCT_BRIEF.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Primary actions: Connect wallet, switch network, create service, register operat

Trust, risk, and compliance: Money, permissions, and protocol identity must be exact, copyable, and auditable. Destructive or irreversible actions need clear state, provenance, and confirmation. Embedded apps must expose origin, permissions, denied actions, and unavailable bridge methods. Empty/error/loading states must be truthful and actionable.

Design posture: Dense infrastructure console with restrained blueprint identity accents. Use compact tables, split workspaces, tabs, ledgers, event timelines, thin borders, and low-chroma status colors. Cards belong mainly to catalog discovery, repeated items, and modals.
Design posture: Dense infrastructure console with restrained blueprint identity accents. Use compact tables, split workspaces, tabs, ledgers, event timelines, thin borders, and low-chroma status colors. Cards belong mainly to catalog discovery, repeated items, and modals. Cloud should inherit the original Tangle dapp's wallet, network, chrome, and action hierarchy where it improves operator trust, but keep Cloud pages data-first rather than marketing-first.

Non-goals: Do not reskin this as the AI trading app. Do not make a marketing landing page, decorative hero wall, app-store gallery, or purple/glow-heavy SaaS dashboard. Do not duplicate sidebar, topbar, breadcrumbs, page headers, tabs, and local nav unless each layer has a distinct job.

Expand All @@ -39,6 +39,7 @@ Evidence:
- `src/pages/services/[id]/page.tsx` mixes service console, blueprint presentation, ACL, jobs, billing, and upgrade surfaces in one vertical stack.
- `src/pages/blueprints/[id]/deploy/page.tsx` submits a request but sends users back to blueprint detail instead of request/service status.
- Parallel audit reports on 2026-06-05 converged on app/workspace-first IA and infrastructure-console visual direction.
- 2026-06-08 design-system pass restored Tangle dapp-style wallet/network affordances, removed the duplicate Blueprints nav title, converted the catalog to a list-first operator-capacity surface, added blueprint default visuals, and browser-verified desktop/mobile/light/dark render states.

Open questions:

Expand Down
30 changes: 17 additions & 13 deletions apps/tangle-cloud/src/app/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,25 @@ const NetworkSync: FC<PropsWithChildren> = ({ children }) => {
const selectedNetwork = useNetworkStore((store) => store.network2);
const setNetwork = useNetworkStore((store) => store.setNetwork);

useEffect(() => {
if (forceLocalChain) {
return;
}
const needsCloudNetworkReset =
!forceLocalChain &&
selectedNetwork?.evmChainId === ANVIL_LOCAL_NETWORK.evmChainId;

if (selectedNetwork?.evmChainId === ANVIL_LOCAL_NETWORK.evmChainId) {
useEffect(() => {
if (needsCloudNetworkReset) {
setNetwork(BASE_SEPOLIA_NETWORK);
}
}, [forceLocalChain, selectedNetwork?.evmChainId, setNetwork]);
}, [needsCloudNetworkReset, setNetwork]);

useNetworkSync(TANGLE_CLOUD_NETWORKS);
useLocalChainGuard({
enabled: forceLocalChain && isLocalPreviewHost(),
targetChainId: ANVIL_LOCAL_NETWORK.evmChainId ?? 31337,
});
if (needsCloudNetworkReset) {
return null;
}

return children;
};

Expand Down Expand Up @@ -75,14 +79,14 @@ const Providers: FC<PropsWithChildren> = ({ children }) => {
reconnectOnMount={reconnectOnMount}
>
<QueryClientProvider client={queryClient}>
<IndexerStatusProvider>
<ToastProvider>
<ToastAccessibilityPatch />
<NetworkSync>
<NetworkSync>
<IndexerStatusProvider>
<ToastProvider>
<ToastAccessibilityPatch />
<PaymentProviders>{children}</PaymentProviders>
</NetworkSync>
</ToastProvider>
</IndexerStatusProvider>
</ToastProvider>
</IndexerStatusProvider>
</NetworkSync>
</QueryClientProvider>
</WagmiProvider>
);
Expand Down
57 changes: 28 additions & 29 deletions apps/tangle-cloud/src/components/sandbox/SandboxUi.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import {
forwardRef,
type ChangeEvent,
type ComponentProps,
type ComponentRef,
type ElementRef,
type FC,
type ReactNode,
} from 'react';
Expand Down Expand Up @@ -156,11 +156,8 @@ export type ButtonProps = Omit<
disabledTooltip?: string;
};

export const Button = forwardRef<
ComponentRef<typeof SandboxButton>,
ButtonProps
>(
(
export const Button = forwardRef<ElementRef<typeof SandboxButton>, ButtonProps>(
function Button(
{
variant,
size,
Expand All @@ -178,29 +175,31 @@ export const Button = forwardRef<
...props
},
ref,
) => (
<SandboxButton
ref={ref}
variant={
variant === 'utility'
? 'outline'
: variant === 'primary'
? 'default'
: variant
}
size={isJustIcon ? 'icon' : size}
disabled={disabled || isDisabled}
loading={isLoading}
className={[isFullWidth ? 'w-full' : '', className]
.filter(Boolean)
.join(' ')}
{...props}
>
{leftIcon}
{children}
{rightIcon}
</SandboxButton>
),
) {
return (
<SandboxButton
ref={ref}
variant={
variant === 'utility'
? 'outline'
: variant === 'primary'
? 'default'
: variant
}
size={isJustIcon ? 'icon' : size}
disabled={disabled || isDisabled}
loading={isLoading}
className={[isFullWidth ? 'w-full' : '', className]
.filter(Boolean)
.join(' ')}
{...props}
>
{leftIcon}
{children}
{rightIcon}
</SandboxButton>
);
},
);

Button.displayName = 'Button';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
fetchAttestations,
fetchAuditorOnChain,
} from '@tangle-network/tangle-shared-ui/data/blueprints/useBinaryVersions';
import useNetworkStore from '@tangle-network/tangle-shared-ui/context/useNetworkStore';
import {
AttestationKind,
type Auditor,
Expand Down Expand Up @@ -970,7 +971,9 @@ export default BlueprintListing;
* the same react-query cache entry — no duplicate chain reads.
*/
const useAuditedStatusMap = (blueprintIds: bigint[]): Map<string, boolean> => {
const chainId = useChainId();
const wagmiChainId = useChainId();
const networkChainId = useNetworkStore((store) => store.network2?.evmChainId);
const chainId = networkChainId ?? wagmiChainId;
const publicClient = usePublicClient({ chainId });
const fallback = useMemo(() => auditorFallbackRegistry(), []);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,7 @@ const RegistrationDrawer: FC<RegistrationDrawerProps> = ({

return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="fixed right-0 left-auto top-0 z-[80] h-screen max-h-screen w-full max-w-xl translate-x-0 translate-y-0 rounded-none border-y-0 border-r-0 p-0 sm:max-w-xl">
<DialogContent className="fixed !left-auto !right-0 !top-0 z-[80] flex h-screen max-h-screen w-full max-w-xl !translate-x-0 !translate-y-0 flex-col overflow-hidden rounded-none border-y-0 border-r-0 p-0 sm:max-w-xl">
<div className="shrink-0 flex items-center justify-between p-4 border-b border-border">
<DialogTitle className="text-lg font-bold">
Register as Operator
Expand Down
28 changes: 23 additions & 5 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ import reactRefresh from 'eslint-plugin-react-refresh';
import storybook from 'eslint-plugin-storybook';
import tseslint from 'typescript-eslint';

const reactCompilerReadinessRules = {
'react-hooks/config': 'warn',
'react-hooks/error-boundaries': 'warn',
'react-hooks/gating': 'warn',
'react-hooks/globals': 'warn',
'react-hooks/immutability': 'warn',
'react-hooks/preserve-manual-memoization': 'warn',
'react-hooks/purity': 'warn',
'react-hooks/refs': 'warn',
'react-hooks/set-state-in-effect': 'warn',
'react-hooks/set-state-in-render': 'warn',
'react-hooks/static-components': 'warn',
'react-hooks/unsupported-syntax': 'warn',
'react-hooks/use-memo': 'warn',
};

export default tseslint.config(
tseslint.configs.recommended,
js.configs.recommended,
Expand Down Expand Up @@ -39,6 +55,12 @@ export default tseslint.config(
ignoreRestSiblings: true,
},
],
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
// The v7 hooks plugin adds React Compiler readiness diagnostics to the
// Nx React preset. Keep them visible without making compiler adoption a
// repo-wide CI gate before the codebase is migrated.
...reactCompilerReadinessRules,
'@nx/enforce-module-boundaries': [
'error',
{
Expand All @@ -55,11 +77,7 @@ export default tseslint.config(
},
},
{
ignores: [
'**/.netlify/',
'**/contracts/**',
'**/scripts/migration/**',
],
ignores: ['**/.netlify/', '**/contracts/**', '**/scripts/migration/**'],
},
{
files: ['**/tailwind.config.ts', '**/eslint.config.{mjs,js}'],
Expand Down
9 changes: 7 additions & 2 deletions libs/tangle-shared-ui/src/context/IndexerStatusContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
getEnvioNetworkFromChainId,
type EnvioNetwork,
} from '../utils/executeEnvioGraphQL';
import useNetworkStore from './useNetworkStore';

// Data source type
export type DataSource = 'graphql' | 'onchain' | 'unavailable';
Expand Down Expand Up @@ -48,7 +49,9 @@ const IndexerStatusContext = createContext<IndexerStatus | null>(null);
* Should be placed high in the component tree, after WagmiProvider.
*/
export const IndexerStatusProvider: FC<PropsWithChildren> = ({ children }) => {
const chainId = useChainId();
const wagmiChainId = useChainId();
const networkChainId = useNetworkStore((store) => store.network2?.evmChainId);
const chainId = networkChainId ?? wagmiChainId;
const network = getEnvioNetworkFromChainId(chainId);

// Use the shared health check hook
Expand Down Expand Up @@ -147,7 +150,9 @@ export const useIsGraphQLEnabled = (): {
* Useful for hooks that may be used outside the provider.
*/
export const useIndexerStatusStandalone = () => {
const chainId = useChainId();
const wagmiChainId = useChainId();
const networkChainId = useNetworkStore((store) => store.network2?.evmChainId);
const chainId = networkChainId ?? wagmiChainId;
const { data: isHealthy, isLoading: isCheckingHealth } =
useEnvioHealthCheckByChainId(chainId);

Expand Down
6 changes: 6 additions & 0 deletions libs/tangle-shared-ui/src/utils/setupTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ Object.defineProperty(window, 'matchMedia', {
})),
});

Object.defineProperty(window, 'scrollTo', {
configurable: true,
writable: true,
value: vi.fn(),
});

process.on('unhandledRejection', (reason) => {
console.log('FAILED TO HANDLE PROMISE REJECTION');
console.log('REASON', reason);
Expand Down
Loading