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
3 changes: 2 additions & 1 deletion CLAUDE.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/carpincho-icon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Carpincho, a local Canton wallet for development." />
<meta name="theme-color" content="#f7f7f7" media="(prefers-color-scheme: light)" />
<meta name="theme-color" content="#14152b" media="(prefers-color-scheme: dark)" />
<title>Carpincho Wallet</title>
</head>
<body>
Expand Down
3 changes: 3 additions & 0 deletions public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
"description": "Local Canton wallet for development.",
"version": "0.1.0",
"permissions": ["storage", "activeTab", "favicon", "system.display"],
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'"
},
"icons": {
"16": "icons/carpincho-16.png",
"32": "icons/carpincho-32.png",
Expand Down
7 changes: 5 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,13 @@ const Shell = (): JSX.Element => {
}, [showHeader])
if (view === 'loading') {
return (
<div className="w-popup mx-auto px-3 pt-3 pb-8 min-h-screen flex flex-col items-center justify-center gap-3 text-muted-foreground">
<main
aria-busy
className="w-popup mx-auto px-3 pt-3 pb-8 min-h-screen flex flex-col items-center justify-center gap-3 text-muted-foreground"
>
{SPINNER_ICON}
<p className="m-0 font-sans text-[0.95rem] font-medium">Loading...</p>
</div>
</main>
)
}
return (
Expand Down
2 changes: 1 addition & 1 deletion src/components/AmountField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export const AmountField = ({
type="button"
data-testid={testId === undefined ? undefined : `${testId}-max`}
onClick={onMax}
className="shrink-0 rounded-sm px-2 py-1 text-[0.72rem] font-bold uppercase tracking-wide text-primary transition-colors hover:bg-primary-soft focus-visible:outline-none focus-visible:shadow-focus"
className="shrink-0 rounded-sm px-2 py-1 text-[0.72rem] font-bold uppercase tracking-wide text-primary-text transition-colors hover:bg-primary-soft focus-visible:outline-none focus-visible:shadow-focus"
>
Max
</button>
Expand Down
8 changes: 5 additions & 3 deletions src/components/ConfirmPasswordForm.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type FormEvent, type ReactNode, useState } from 'react'
import { type FormEvent, type ReactNode, useId, useState } from 'react'
import { PrimaryButton } from '@/components/ui/Button'
import { PasswordInput } from '@/components/ui/PasswordInput'
import { useVault } from '@/vault/useVault'
Expand Down Expand Up @@ -28,6 +28,7 @@ export const ConfirmPasswordForm = ({
submitTestId,
}: ConfirmPasswordFormProps): JSX.Element => {
const v = useVault()
const errorId = useId()
const [current, setCurrent] = useState('')
const [error, setError] = useState<string | null>(initialError)
const hasError = error !== null
Expand All @@ -52,7 +53,7 @@ export const ConfirmPasswordForm = ({
<PasswordInput
aria-label={label}
data-testid={passwordTestId}
aria-errormessage={hasError ? 'confirm-password-error' : undefined}
aria-errormessage={hasError ? errorId : undefined}
placeholder="Current password"
autoComplete="current-password"
error={hasError}
Expand All @@ -64,7 +65,8 @@ export const ConfirmPasswordForm = ({
/>
{hasError && (
<p
id="confirm-password-error"
id={errorId}
role="alert"
className="text-[0.85rem] text-danger"
>
{error}
Expand Down
4 changes: 3 additions & 1 deletion src/components/ConnectionFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ export const ConnectionFooter = ({
title="Disconnect"
className={cn(
PLAIN_ICON_BUTTON_CLASS,
'ml-0.5 size-[18px] text-soft hover:bg-danger/10 hover:text-danger [&>svg]:size-[13px]',
// size-6 is the 24px WCAG 2.5.8 floor; the glyph stays small so the
// footer row does not grow around it.
'-my-1 ml-0.5 size-6 text-soft hover:bg-danger/10 hover:text-danger [&>svg]:size-[13px]',
)}
>
{DISCONNECT_ICON}
Expand Down
2 changes: 1 addition & 1 deletion src/components/EndpointListRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export const EndpointListRow = ({
actions={
<>
{active && (
<span className="pointer-events-none relative z-10 shrink-0 text-primary">
<span className="pointer-events-none relative z-10 shrink-0 text-primary-text">
{CHECK_ICON}
</span>
)}
Expand Down
2 changes: 1 addition & 1 deletion src/components/HomeTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export const HomeTabs = ({
>
<span>Activity</span>
{pendingTransferCount > 0 ? (
<span className="ml-1 inline-grid min-w-5 place-items-center rounded-full bg-accent px-1.5 text-[0.72rem] leading-5 text-primary-foreground">
<span className="ml-1 inline-grid min-w-5 place-items-center rounded-full bg-primary px-1.5 text-[0.72rem] leading-5 text-primary-foreground">
{pendingTransferCount}
</span>
) : null}
Expand Down
2 changes: 1 addition & 1 deletion src/components/PasswordStrengthIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const FAIR: Tier = { segmentColor: 'bg-warning', labelColor: 'text-warning', lab
const STRONG: Tier = { segmentColor: 'bg-success', labelColor: 'text-success', label: 'Strong' }
const EXCELLENT: Tier = {
segmentColor: 'bg-primary',
labelColor: 'text-primary',
labelColor: 'text-primary-text',
label: 'Excellent',
}

Expand Down
2 changes: 1 addition & 1 deletion src/components/SendTokenForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export const SendTokenForm = ({
data-testid="send-contacts"
aria-label="Contacts"
onClick={onOpenContacts}
className="inline-flex items-center justify-center rounded-sm text-primary transition-colors hover:text-primary-hover focus-visible:outline-none focus-visible:shadow-focus"
className="inline-flex items-center justify-center rounded-sm text-primary-text transition-colors hover:text-primary-hover focus-visible:outline-none focus-visible:shadow-focus"
>
{CONTACTS_ICON}
</button>
Expand Down
2 changes: 1 addition & 1 deletion src/components/WelcomeHero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ interface WelcomeHeroProps {
}

const WORDMARK_BASE =
'font-display font-semibold text-primary dark:bg-[image:var(--bg-gradient-brand)] dark:bg-clip-text dark:text-transparent leading-[1.02] tracking-[-0.03em] lowercase'
'font-display font-semibold text-primary-text dark:bg-[image:var(--bg-gradient-brand)] dark:bg-clip-text dark:text-transparent leading-[1.02] tracking-[-0.03em] lowercase'

const DEFAULT_LOGO_SIZE: Record<WelcomeHeroLayout, number> = {
stacked: 132,
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/AddRowButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const AddRowButton = ({ label, onClick, testId }: AddRowButtonProps): JSX
type="button"
data-testid={testId}
onClick={onClick}
className="mt-1 flex w-full items-center justify-center gap-2 rounded-sm border border-dashed border-border p-2 font-semibold text-primary transition-colors hover:border-primary/40 hover:bg-primary-soft"
className="mt-1 flex w-full items-center justify-center gap-2 rounded-sm border border-dashed border-border p-2 font-semibold text-primary-text transition-colors hover:border-primary/40 hover:bg-primary-soft"
>
<span aria-hidden="true">+</span> {label}
</button>
Expand Down
8 changes: 4 additions & 4 deletions src/components/ui/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ const BASE_INTERACTIVE =
'duration-200 ease-out active:scale-[0.98] disabled:active:scale-100 ' +
'focus-visible:outline-none focus-visible:shadow-focus'

export const GHOST_BUTTON_CLASS = `${BASE_INTERACTIVE} border-0 bg-transparent text-primary enabled:hover:text-primary-hover text-[0.95rem] font-semibold p-0`
export const GHOST_BUTTON_CLASS = `${BASE_INTERACTIVE} border-0 bg-transparent text-primary-text enabled:hover:text-primary-hover text-[0.95rem] font-semibold p-0`

export const ICON_BUTTON_CLASS =
'inline-grid place-items-center text-muted-foreground transition-colors ' +
'enabled:hover:text-primary enabled:hover:bg-primary-soft ' +
'enabled:hover:text-primary-text enabled:hover:bg-primary-soft ' +
'focus-visible:outline-none focus-visible:shadow-focus'

// Like ICON_BUTTON_CLASS but no hover background fill — for icons inline beside text.
export const PLAIN_ICON_BUTTON_CLASS =
'inline-grid place-items-center rounded-sm text-muted-foreground transition-colors ' +
'hover:text-primary focus-visible:outline-none focus-visible:shadow-focus'
'hover:text-primary-text focus-visible:outline-none focus-visible:shadow-focus'

// Primary chrome recoloured for destructive actions; layout (e.g. w-full) is left to the caller.
const DANGER_OVERRIDE =
Expand All @@ -32,7 +32,7 @@ const VARIANT_CLASS = {
'before:opacity-0 before:transition-opacity before:duration-200 enabled:hover:before:opacity-100',
secondary:
`${BASE_INTERACTIVE} py-2.5 px-4 leading-none rounded-md font-semibold text-[0.94rem] text-foreground ` +
'bg-surface border border-border-strong enabled:hover:bg-muted enabled:hover:text-primary',
'bg-surface border border-border-strong enabled:hover:bg-muted enabled:hover:text-primary-text',
ghost: GHOST_BUTTON_CLASS,
} as const

Expand Down
6 changes: 4 additions & 2 deletions src/components/ui/FileDropInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,14 @@ export const FileDropInput = ({
type="file"
accept={accept}
aria-label={ariaLabel}
className="sr-only"
className="peer sr-only"
onChange={(event) => onSelect(event.currentTarget.files?.[0] ?? null)}
/>
{/* The input is visually hidden but still tabbable, so the dropzone has to show
its focus ring, or a keyboard user lands on nothing they can see. */}
<label
htmlFor={id}
className="cursor-pointer rounded-md border border-dashed border-border bg-surface px-4 py-6 text-center hover:border-primary/60"
className="cursor-pointer rounded-md border border-dashed border-border bg-surface px-4 py-6 text-center hover:border-primary/60 peer-focus-visible:border-primary peer-focus-visible:shadow-focus"
>
{fileName === null ? (
<span className="text-[0.82rem] font-medium text-muted-foreground">{prompt}</span>
Expand Down
27 changes: 17 additions & 10 deletions src/components/ui/JsonView.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { JsonView as RjvJsonView } from '@uiw/react-json-view'
import type { CSSProperties } from 'react'
import { type CSSProperties, lazy, Suspense } from 'react'
import { cn } from '@/utils/cn'

// The json tree is the heaviest thing the popup imports and only ever renders inside a
// sheet the user opened, so it loads on demand instead of on every popup open.
const RjvJsonView = lazy(async () => ({
default: (await import('@uiw/react-json-view')).JsonView,
}))

// Map the json tree's CSS variables onto the wallet's existing token palette.
const THEME_STYLE: CSSProperties = {
fontSize: '0.74rem',
Expand Down Expand Up @@ -40,14 +45,16 @@ export const JsonView = ({ value, className }: JsonViewProps): JSX.Element => {
)}
>
{isObject ? (
<RjvJsonView
value={value as object}
style={THEME_STYLE}
displayDataTypes={false}
displayObjectSize={false}
enableClipboard
collapsed={2}
/>
<Suspense fallback={<pre className="m-0 font-mono text-muted-foreground">Loading...</pre>}>
<RjvJsonView
value={value as object}
style={THEME_STYLE}
displayDataTypes={false}
displayObjectSize={false}
enableClipboard
collapsed={2}
/>
</Suspense>
) : (
<pre className="m-0 whitespace-pre-wrap break-words font-mono text-foreground">
{value == null ? '' : String(value)}
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/OptionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const OptionList = <T extends string>({
className={cn(
MENU_ROW_BASE_CLASS,
'flex items-center justify-between text-left text-foreground',
active && 'border-primary text-primary',
active && 'border-primary text-primary-text',
)}
>
<span className="font-medium">{opt.label}</span>
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const Select = ({
className={ITEM_CLASS}
>
<RadixSelect.ItemText>{option.label}</RadixSelect.ItemText>
<RadixSelect.ItemIndicator className="text-primary">
<RadixSelect.ItemIndicator className="text-primary-text">
{CHECK_ICON}
</RadixSelect.ItemIndicator>
</RadixSelect.Item>
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/Stepper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const stateOf = (index: number, current: number): StepState =>

const NODE_CLASS: Record<StepState, string> = {
complete: 'bg-primary border-primary text-primary-foreground',
active: 'border-primary text-primary bg-primary-soft',
active: 'border-primary text-primary-text bg-primary-soft',
upcoming: 'border-border text-muted-foreground bg-surface',
}

Expand Down
6 changes: 5 additions & 1 deletion src/components/ui/ToastProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ const BASE_TOAST_CLASS = cn(
// Per-variant accent: only the rail and icon badge carry colour; message text stays neutral.
const VARIANT_ACCENT: Record<FeedbackVariant, { rail: string; badge: string; icon: JSX.Element }> =
{
info: { rail: 'before:bg-primary', badge: 'bg-primary-soft text-primary', icon: INFO_ICON },
info: {
rail: 'before:bg-primary',
badge: 'bg-primary-soft text-primary-text',
icon: INFO_ICON,
},
success: { rail: 'before:bg-success', badge: 'bg-success-soft text-success', icon: CHECK_ICON },
warning: {
rail: 'before:bg-warning',
Expand Down
3 changes: 2 additions & 1 deletion src/components/utils/UtilsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ const ROW_CLASS =
'flex w-full items-center gap-3 rounded-md border border-border bg-surface px-3 py-2.5 text-left ' +
'outline-none transition-colors enabled:hover:border-primary/60 enabled:hover:bg-primary-soft/40 ' +
'focus-visible:shadow-focus disabled:opacity-70'
const ICON_WRAP_CLASS = 'grid size-9 shrink-0 place-items-center rounded-full bg-muted text-primary'
const ICON_WRAP_CLASS =
'grid size-9 shrink-0 place-items-center rounded-full bg-muted text-primary-text'

interface UtilsListProps {
account: AccountPublic
Expand Down
41 changes: 32 additions & 9 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,21 @@
--color-muted: #ececf0;

--color-border: #e2e2ea;
--color-border-strong: #ccccd6;
--color-border-strong: #8a8b9c;

--color-foreground: #2c2d40;
--color-muted-foreground: #4b4d60;
--color-soft: #6b6d80;
--color-soft: #5f6175;

--color-primary: #692581;
--color-primary-hover: #8b46a4;
--color-primary-soft: #f3e8f8;
--color-primary-ink: #4a1a5c;
--color-primary-foreground: #ffffff;
/* Primary as text or an icon. Split from --color-primary because that one also
fills the primary button, and in dark no single purple can both carry a white
label (needs to stay dark) and read at 4.5:1 on the surface (needs to be light). */
--color-primary-text: #692581;

--logo-shadow-color: rgba(44, 45, 64, 0.5);

Expand All @@ -40,7 +44,7 @@
--color-warning: #8a6400;
--color-warning-soft: #f6ecd2;

--color-form-placeholder: #6f7185;
--color-form-placeholder: #666879;

--color-scrim: rgba(20, 21, 43, 0.42);

Expand Down Expand Up @@ -69,17 +73,18 @@
--color-muted: #232539;

--color-border: #2c2e46;
--color-border-strong: #5f6178;
--color-border-strong: #6b6d84;

--color-foreground: #fafbfc;
--color-muted-foreground: #c5c2cb;
--color-soft: #e2e0e7;

--color-primary: #9d54b8;
--color-primary-hover: #b06cc9;
--color-primary-hover: #c58ada;
--color-primary-soft: #2a1f3a;
--color-primary-ink: #ecd9f5;
--color-primary-foreground: #ffffff;
--color-primary-text: #b87ad2;

--logo-shadow-color: rgba(0, 0, 0, 0.6);

Expand All @@ -94,7 +99,7 @@
--color-warning: #d4a85a;
--color-warning-soft: #332a16;

--color-form-placeholder: #7a7c92;
--color-form-placeholder: #8d8fa4;

--color-scrim: rgba(8, 8, 18, 0.65);

Expand Down Expand Up @@ -129,6 +134,7 @@
--color-primary-soft: var(--color-primary-soft);
--color-primary-ink: var(--color-primary-ink);
--color-primary-foreground: var(--color-primary-foreground);
--color-primary-text: var(--color-primary-text);
--color-accent: var(--color-accent);
--color-success: var(--color-success);
--color-success-soft: var(--color-success-soft);
Expand Down Expand Up @@ -351,9 +357,26 @@
display: none;
}

.animate-drift,
.animate-logo-shadow {
animation: none;
/*
* Everything that moves is cut to a single instant pass, so entry animations still
* land on their final frame (Radix reads animationend to unmount a closing sheet)
* and nothing loops. These rules sit outside any @layer, which is what lets them
* beat the Tailwind animate-* utilities without !important. The spinners are
* exempt: they report progress, not decoration.
*/
*,
*::before,
*::after {
animation-delay: 0ms;
animation-duration: 1ms;
animation-iteration-count: 1;
transition-duration: 1ms;
}

.animate-spin,
.animate-spin-fast {
animation-duration: 900ms;
animation-iteration-count: infinite;
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/views/AddNetworkAccount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const AddNetworkAccount = (): JSX.Element => {
})

return (
<div className="flex min-h-0 flex-1 flex-col">
<main className="flex min-h-0 flex-1 flex-col">
<div className="min-h-0 flex-1 overflow-y-auto">
<WelcomeHero
description="Canton development wallet."
Expand Down Expand Up @@ -58,6 +58,6 @@ export const AddNetworkAccount = (): JSX.Element => {
onOpenChange={setConnectionOpen}
/>
)}
</div>
</main>
)
}
Loading