diff --git a/apps/tangle-cloud/netlify.toml b/apps/tangle-cloud/netlify.toml index ed9c687ef4..c6dfa9f6c5 100644 --- a/apps/tangle-cloud/netlify.toml +++ b/apps/tangle-cloud/netlify.toml @@ -2,6 +2,32 @@ publish = "dist/apps/tangle-cloud" command = "yarn nx build tangle-cloud" +# ─── Per-context environment ─────────────────────────────────────────── +# +# Vite reads VITE_* vars at build time and inlines them into the bundle, +# so flipping one of these requires a rebuild — not a runtime config +# change. Setting the iframe kill-switch here keeps the source of truth +# in-repo and reviewable. Override per-branch by adding [context.] +# blocks if we ever need to differ between staging and production. + +[context.production.environment] + # Production is iframe-on. Falls back to link-out automatically for any + # blueprint that doesn't satisfy publisher + host + metadata gates, so + # this flag is safe to leave on once the in-app trust gates are doing + # their job. To disarm, flip to "false" and redeploy — single env flip + # mass-disables every iframe app without an app-side change. + VITE_BLUEPRINT_IFRAME_ENABLED = "true" + +[context.deploy-preview.environment] + # Preview deploys (PR builds) inherit production's iframe-on default + # so contributors testing iframe-mode manifests on a PR-preview URL + # see the same surface production users will. + VITE_BLUEPRINT_IFRAME_ENABLED = "true" + +[context.branch-deploy.environment] + # Same default for any non-master branch deploy (e.g. staging). + VITE_BLUEPRINT_IFRAME_ENABLED = "true" + # If the branch is not master, continue the build process # If the branch is master, check if the project's CHANGELOG.md file has changed # If the CHANGELOG.md file has changed, continue the build process, @@ -32,3 +58,29 @@ Permissions-Policy = "camera=(), microphone=(), geolocation=(), payment=(), usb=(), bluetooth=(), accelerometer=(), gyroscope=(), magnetometer=(), midi=(), serial=()" X-Frame-Options = "SAMEORIGIN" Referrer-Policy = "strict-origin-when-cross-origin" + +# ─── Cache headers ────────────────────────────────────────────────────── +# +# Vite emits hashed filenames into /assets, so the file at any given URL +# is content-addressed and safe to mark immutable. Repeat-visit loads +# become near-instant because the browser doesn't even revalidate. +[[headers]] + for = "/assets/*" + [headers.values] + Cache-Control = "public, max-age=31536000, immutable" + +# Fonts are also content-addressed when present; mark them immutable and +# permit cross-origin use so they can be loaded from sibling subdomains. +[[headers]] + for = "/fonts/*" + [headers.values] + Cache-Control = "public, max-age=31536000, immutable" + Access-Control-Allow-Origin = "*" + +# index.html must NOT be cached aggressively — it's the entry point that +# references the hashed asset bundles, so it has to be replaced atomically +# on every deploy. +[[headers]] + for = "/index.html" + [headers.values] + Cache-Control = "public, max-age=0, must-revalidate" diff --git a/apps/tangle-cloud/src/app/app.tsx b/apps/tangle-cloud/src/app/app.tsx index 596f2d4435..dd7ee99878 100644 --- a/apps/tangle-cloud/src/app/app.tsx +++ b/apps/tangle-cloud/src/app/app.tsx @@ -50,7 +50,7 @@ const PaymentsPoolPage = lazy(() => import('../pages/payments/pool')); const PaymentsCreditsPage = lazy(() => import('../pages/payments/credits')); const NotFoundPage = lazy(() => import('../pages/notFound')); -const PageFallback = () => ( +const RouteFallback = () => (
@@ -61,7 +61,7 @@ const PageFallback = () => ( // Wrap lazy page in layout + Suspense so layout (with Header) stays visible during load const withLayout = (LayoutCmp: FC<{ children: ReactNode }>, Page: FC) => ( - }> + }> @@ -209,7 +209,7 @@ const App: FC = () => { }> + }> } diff --git a/apps/tangle-cloud/src/blueprintApps/components/IframeAppApprovalModal.tsx b/apps/tangle-cloud/src/blueprintApps/components/IframeAppApprovalModal.tsx index e5b8c2b707..e0d16e34fe 100644 --- a/apps/tangle-cloud/src/blueprintApps/components/IframeAppApprovalModal.tsx +++ b/apps/tangle-cloud/src/blueprintApps/components/IframeAppApprovalModal.tsx @@ -163,7 +163,7 @@ const IframeAppApprovalModal: FC = ({ return ( { + onOpenChange={(open: boolean) => { if (!open && !submitting) handleReject(); }} > diff --git a/apps/tangle-cloud/src/blueprintApps/modules/sandbox/SandboxBlueprintServicePage.tsx b/apps/tangle-cloud/src/blueprintApps/modules/sandbox/SandboxBlueprintServicePage.tsx index af2429f699..2868b10561 100644 --- a/apps/tangle-cloud/src/blueprintApps/modules/sandbox/SandboxBlueprintServicePage.tsx +++ b/apps/tangle-cloud/src/blueprintApps/modules/sandbox/SandboxBlueprintServicePage.tsx @@ -16,6 +16,7 @@ import { Button, Card, CardContent, + EmptyState, } from '@tangle-network/sandbox-ui/primitives'; import { useAccount } from 'wagmi'; import type { JobCall } from '@tangle-network/tangle-shared-ui/data/graphql'; @@ -192,11 +193,10 @@ const SandboxBlueprintServicePage: FC = ({ entry, serviceId }) => {
)) ) : ( -
-

- No job activity indexed for this service yet. -

-
+ )} diff --git a/apps/tangle-cloud/src/components/Text.tsx b/apps/tangle-cloud/src/components/Text.tsx new file mode 100644 index 0000000000..ead8927f68 --- /dev/null +++ b/apps/tangle-cloud/src/components/Text.tsx @@ -0,0 +1,146 @@ +/** + * Tangle Cloud canonical Text component. + * + * Single source of truth for the type ramp across tangle-cloud. Consumes the + * `@tangle-network/brand` 0.3 token system: `--font-sans`, `--font-display`, + * `--font-size-*`, `--line-height-*`, plus the Tailwind-mapped `text-foreground` + * / `text-muted-foreground` colors driven by the brand HSL bridge. + * + * Replaces ~13 per-page `Text` shims that all reimplemented the same ramp with + * subtly different variant unions ('h4' | 'h5' | 'body1' | 'body2' | 'body3' | + * 'body4'). The new variant union mirrors brand vocabulary; legacy variant names + * are accepted as aliases for backwards compatibility, so existing call sites + * keep type-checking. + */ +import type { ComponentProps, ElementType, FC } from 'react'; + +/** + * Brand-aligned variants. Mapping rationale: + * + * - `h1` → 4xl display (page hero) + * - `h2` → 3xl display (section heading; replaces legacy `h4`) + * - `h3` → xl display (sub-section; replaces legacy `h5`) + * - `h4` → lg display + * - `body-lg` → text-base (replaces legacy `body1`) + * - `body` → text-sm (replaces legacy `body2`, the default) + * - `body-sm` → text-xs (replaces legacy `body3` / `body4`) + * - `caption` → text-[11px] muted (timestamps, meta) + * + * Legacy aliases ('h4', 'h5', 'body1', 'body2', 'body3', 'body4') resolve to + * the same classes the per-page shims used, preserving visual parity. + */ +export type TextVariant = + // Brand-aligned canonical names + | 'h1' + | 'h2' + | 'h3' + | 'h4' + | 'body-lg' + | 'body' + | 'body-sm' + | 'caption' + // Legacy aliases — kept so existing call sites continue to type-check. + // Map: 'h4' → 'h2' (display 3xl), 'h5' → 'h3' (display xl), + // 'body1' → 'body-lg', 'body2' → 'body', 'body3' / 'body4' → 'body-sm'. + | 'h5' + | 'body1' + | 'body2' + | 'body3' + | 'body4'; + +export type TextProps = ComponentProps<'p'> & { + variant?: TextVariant; + fw?: 'bold' | 'semibold'; + /** Override the rendered semantic element. Defaults are sensible per variant. */ + as?: ElementType; +}; + +/** + * Maps a (possibly legacy) variant to the canonical token set. We resolve once + * here so both `defaultElementFor` and `classFor` work off canonical names. + */ +const canonicalize = ( + variant: TextVariant, +): Exclude => { + switch (variant) { + case 'h4': + // Legacy 'h4' was used as a top-level page heading (3xl display). + // Brand 'h2' is the canonical 3xl display. + return 'h2'; + case 'h5': + // Legacy 'h5' was used for section headings (xl display). + return 'h3'; + case 'body1': + return 'body-lg'; + case 'body2': + return 'body'; + case 'body3': + case 'body4': + return 'body-sm'; + default: + return variant; + } +}; + +const defaultElementFor = ( + variant: ReturnType, +): ElementType => { + switch (variant) { + case 'h1': + return 'h1'; + case 'h2': + return 'h2'; + case 'h3': + return 'h3'; + case 'h4': + return 'h4'; + case 'caption': + return 'span'; + default: + return 'p'; + } +}; + +const classFor = (variant: ReturnType): string => { + switch (variant) { + case 'h1': + return 'font-display text-4xl tracking-tight text-foreground'; + case 'h2': + return 'font-display text-3xl tracking-tight text-foreground'; + case 'h3': + return 'font-display text-xl text-foreground'; + case 'h4': + return 'font-display text-lg text-foreground'; + case 'body-lg': + return 'text-base text-foreground'; + case 'body': + return 'text-sm text-foreground'; + case 'body-sm': + return 'text-xs text-muted-foreground'; + case 'caption': + return 'text-[11px] text-muted-foreground'; + } +}; + +export const Text: FC = ({ + variant = 'body', + fw, + as, + className = '', + ...props +}) => { + const canonical = canonicalize(variant); + const Component = (as ?? defaultElementFor(canonical)) as ElementType; + const variantClass = classFor(canonical); + const weightClass = + fw === 'bold' ? 'font-bold' : fw === 'semibold' ? 'font-semibold' : ''; + + return ( + + ); +}; diff --git a/apps/tangle-cloud/src/components/TxHistoryDrawer.tsx b/apps/tangle-cloud/src/components/TxHistoryDrawer.tsx index 794764ca94..ccbf103d3d 100644 --- a/apps/tangle-cloud/src/components/TxHistoryDrawer.tsx +++ b/apps/tangle-cloud/src/components/TxHistoryDrawer.tsx @@ -16,6 +16,7 @@ import useTxHistoryStore, { import useEvmAddress from '@tangle-network/tangle-shared-ui/hooks/useEvmAddress'; import { useEvmAssetMetadatas } from '@tangle-network/tangle-shared-ui/hooks/useEvmAssetMetadatas'; import { Alert, Button, Chip, Text } from './sandbox/SandboxUi'; +import { EmptyState } from '@tangle-network/sandbox-ui/primitives'; import { formatDistanceToNow } from 'date-fns'; import { capitalize } from 'lodash'; import { type FC, useMemo } from 'react'; @@ -149,9 +150,10 @@ const TxHistoryDrawer: FC = () => { {relevantTransactions === null || relevantTransactions.length === 0 ? ( - - No transactions yet. - + ) : ( relevantTransactions.map((tx) => ( diff --git a/apps/tangle-cloud/src/components/sandbox/SandboxUi.tsx b/apps/tangle-cloud/src/components/sandbox/SandboxUi.tsx index e3e9da30e1..62c0e8e150 100644 --- a/apps/tangle-cloud/src/components/sandbox/SandboxUi.tsx +++ b/apps/tangle-cloud/src/components/sandbox/SandboxUi.tsx @@ -33,7 +33,14 @@ import { TabsTrigger, Textarea, } from '@tangle-network/sandbox-ui/primitives'; -import type { ComponentProps, ElementType, FC, ReactNode } from 'react'; +import type { ChangeEvent, ComponentProps, FC, ReactNode } from 'react'; + +// Re-export the canonical tangle-cloud Text from the dedicated module so we +// preserve the existing import surface (`import { Text } from '...sandbox/SandboxUi'`) +// while routing through a single Text implementation that consumes +// @tangle-network/brand 0.3 tokens. +export { Text } from '../Text'; +export type { TextProps, TextVariant } from '../Text'; export { Badge, @@ -123,43 +130,6 @@ export const CircularProgress: FC<{ ); -export type TextProps = ComponentProps<'p'> & { - variant?: 'h4' | 'h5' | 'body1' | 'body2' | 'body3' | 'body4'; - fw?: 'bold' | 'semibold'; -}; - -export const Text: FC = ({ - variant = 'body2', - fw, - className = '', - ...props -}) => { - const Component = ( - variant === 'h4' ? 'h1' : variant === 'h5' ? 'h2' : 'p' - ) as ElementType; - const variantClass = - variant === 'h4' - ? 'font-display text-3xl tracking-tight text-foreground' - : variant === 'h5' - ? 'font-display text-xl text-foreground' - : variant === 'body1' - ? 'text-base text-foreground' - : variant === 'body3' || variant === 'body4' - ? 'text-xs text-muted-foreground' - : 'text-sm text-foreground'; - const weightClass = - fw === 'bold' ? 'font-bold' : fw === 'semibold' ? 'font-semibold' : ''; - - return ( - - ); -}; - export type ButtonProps = Omit< ComponentProps, 'variant' | 'size' @@ -252,7 +222,9 @@ export const Input: FC = ({ ] .filter(Boolean) .join(' ')} - onChange={(event) => onChange?.(event.currentTarget.value)} + onChange={(event: ChangeEvent) => + onChange?.(event.currentTarget.value) + } /> {leftIcon && (
diff --git a/apps/tangle-cloud/src/containers/payments/CreditBalanceContainer.tsx b/apps/tangle-cloud/src/containers/payments/CreditBalanceContainer.tsx index aea3216dc7..b8f250e932 100644 --- a/apps/tangle-cloud/src/containers/payments/CreditBalanceContainer.tsx +++ b/apps/tangle-cloud/src/containers/payments/CreditBalanceContainer.tsx @@ -1,5 +1,6 @@ import { FC } from 'react'; -import { Card, Skeleton, Text } from '../../components/sandbox/SandboxUi'; +import { Skeleton, Text } from '../../components/sandbox/SandboxUi'; +import { EmptyState } from '@tangle-network/sandbox-ui/primitives'; import { useCreditsContext } from '../../app/CreditsProvider'; import CreditAccountCard from '../../components/payments/CreditAccountCard'; import useCreditAccountState from '../../data/payments/useCreditAccountState'; @@ -69,12 +70,10 @@ const CreditBalanceContainer: FC = () => {
{creditAccounts.length === 0 ? ( - - - No credit accounts yet. Fund one from the shielded pool to get - started. - - + ) : (
{creditAccounts.map((acct) => ( diff --git a/apps/tangle-cloud/src/pages/blueprints/BlueprintListing.tsx b/apps/tangle-cloud/src/pages/blueprints/BlueprintListing.tsx index daeefc17ab..52d5d8461a 100644 --- a/apps/tangle-cloud/src/pages/blueprints/BlueprintListing.tsx +++ b/apps/tangle-cloud/src/pages/blueprints/BlueprintListing.tsx @@ -6,13 +6,16 @@ import { Input, Skeleton, } from '../../components/sandbox/SandboxUi'; +import { + SegmentedControl, + type SegmentedControlOption, +} from '@tangle-network/sandbox-ui/primitives'; import { Search } from '@tangle-network/icons'; import type { UseAllBlueprintsReturn } from '@tangle-network/tangle-shared-ui/data/graphql'; import type { Blueprint } from '@tangle-network/tangle-shared-ui/types/blueprint'; import { Dispatch, FC, - ReactNode, SetStateAction, useDeferredValue, useEffect, @@ -307,31 +310,38 @@ const BlueprintListing: FC = ({
-
+
Category - setSelectedCategory(ALL_CATEGORIES)} - > - All - - {blueprintItems.length} - - - {categories.map(({ category, count }) => ( - setSelectedCategory(category)} - > - {category.replace(/^AI /, '')} - - {count} - - - ))} + + aria-label="Filter blueprints by category" + value={selectedCategory} + onValueChange={setSelectedCategory} + options={[ + { + value: ALL_CATEGORIES, + label: 'All', + adornment: ( + + {blueprintItems.length} + + ), + } satisfies SegmentedControlOption, + ...categories.map( + ({ category, count }) => + ({ + value: category, + label: category.replace(/^AI /, ''), + adornment: ( + + {count} + + ), + }) satisfies SegmentedControlOption, + ), + ]} + />
@@ -672,26 +682,3 @@ const BlueprintMetric = ({

); - -const CategoryPill = ({ - isActive, - onClick, - children, -}: { - isActive: boolean; - onClick: () => void; - children: ReactNode; -}) => ( - -); diff --git a/apps/tangle-cloud/src/pages/blueprints/[id]/deploy/DeploySteps/AssetConfigurationStep.tsx b/apps/tangle-cloud/src/pages/blueprints/[id]/deploy/DeploySteps/AssetConfigurationStep.tsx index 97bab42aa3..92f567b31d 100644 --- a/apps/tangle-cloud/src/pages/blueprints/[id]/deploy/DeploySteps/AssetConfigurationStep.tsx +++ b/apps/tangle-cloud/src/pages/blueprints/[id]/deploy/DeploySteps/AssetConfigurationStep.tsx @@ -156,7 +156,7 @@ export const AssetConfigurationStep: FC = ({
+ onValueChange={(commitment: string) => setValue('creditCommitment', commitment, { shouldDirty: true, shouldValidate: true, @@ -369,7 +369,7 @@ export const PaymentStep: FC = ({ { + onValueChange={(assetId: string) => { const asset = assets?.get(assetId as `0x${string}`) as | StakingAsset | undefined; diff --git a/apps/tangle-cloud/src/pages/blueprints/[id]/deploy/DeploySteps/RequestArgsConfigurationStep.tsx b/apps/tangle-cloud/src/pages/blueprints/[id]/deploy/DeploySteps/RequestArgsConfigurationStep.tsx index cf9ba9a9c4..7f0ddfa63f 100644 --- a/apps/tangle-cloud/src/pages/blueprints/[id]/deploy/DeploySteps/RequestArgsConfigurationStep.tsx +++ b/apps/tangle-cloud/src/pages/blueprints/[id]/deploy/DeploySteps/RequestArgsConfigurationStep.tsx @@ -112,7 +112,9 @@ export const RequestArgsConfigurationStep: FC<