-
π
+
+
+
Approver Authentication
diff --git a/apps/web/src/components/VoteButtons.tsx b/apps/web/src/components/VoteButtons.tsx
index 5eaabe9..bf9d0c7 100644
--- a/apps/web/src/components/VoteButtons.tsx
+++ b/apps/web/src/components/VoteButtons.tsx
@@ -1,6 +1,7 @@
import { useState } from 'react';
import { ApiError, api } from '../api';
import { ErrorNote } from './parts';
+import { IconCheck, IconX, IconClock } from './icons';
interface Props {
holdId: string;
@@ -54,13 +55,13 @@ export function VoteButtons({ holdId, disabled = false, alreadyVoted = false, on
className="btn ok grow"
disabled={off}
onClick={() => vote('approve')}
- style={{ letterSpacing: '0.01em' }}
+ style={{ letterSpacing: '0.01em', gap: '8px' }}
>
{busy === 'approve' ? (
'Savingβ¦'
) : (
<>
- β
+
Approve
>
)}
@@ -70,13 +71,13 @@ export function VoteButtons({ holdId, disabled = false, alreadyVoted = false, on
className="btn danger grow"
disabled={off}
onClick={() => vote('deny')}
- style={{ letterSpacing: '0.01em' }}
+ style={{ letterSpacing: '0.01em', gap: '8px' }}
>
{busy === 'deny' ? (
'Savingβ¦'
) : (
<>
- β
+
Deny
>
)}
@@ -91,9 +92,10 @@ export function VoteButtons({ holdId, disabled = false, alreadyVoted = false, on
borderRadius: 'var(--radius-xs)',
border: '1px solid var(--border)',
gap: 'var(--space-2)',
+ alignItems: 'center',
}}
>
- β³
+
You voted. Waiting for the other approver.
)}
diff --git a/apps/web/src/components/icons.tsx b/apps/web/src/components/icons.tsx
new file mode 100644
index 0000000..8f0b5dc
--- /dev/null
+++ b/apps/web/src/components/icons.tsx
@@ -0,0 +1,463 @@
+/**
+ * Centralized icon library β clean 24 Γ 24 stroke SVGs.
+ * Stroke-only, 1.75px weight, rounded caps. No emojis, no font icons.
+ * All icons are purely decorative (aria-hidden is set by callers via props).
+ */
+
+interface IconProps {
+ size?: number;
+ className?: string;
+ style?: React.CSSProperties;
+ 'aria-hidden'?: boolean | 'true' | 'false';
+ title?: string;
+}
+
+const base = (size: number) => ({
+ width: size,
+ height: size,
+ display: 'inline-block',
+ flexShrink: 0,
+ verticalAlign: 'middle',
+});
+
+function Svg({ size = 16, children, style, ...rest }: IconProps & { children: React.ReactNode }) {
+ return (
+
+ );
+}
+
+export function IconShield(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconShieldCheck(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconMic(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconVolume(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconVolumeOff(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconSend(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconRefresh(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconGrid(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconList(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconUsers(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconSettings(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconTrendingUp(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconBell(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconLogOut(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconLock(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconAlertTriangle(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconCheckCircle(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconSearch(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconZap(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconClock(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconChevronRight(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconPackage(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconActivity(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconServer(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconArrowRight(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconDollarSign(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconHandshake(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconClipboard(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconXCircle(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconWifi(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconBattery(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconCheck(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconX(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconCreditCard(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconMoon(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconUser(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconLightbulb(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconScale(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconBuilding(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconPill(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconMail(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconSmartphone(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconMonitor(p: IconProps) {
+ return (
+
+ );
+}
+
+export function IconSparkles(p: IconProps) {
+ return (
+
+ );
+}
diff --git a/apps/web/src/components/parts.tsx b/apps/web/src/components/parts.tsx
index aadbb1c..fdd4719 100644
--- a/apps/web/src/components/parts.tsx
+++ b/apps/web/src/components/parts.tsx
@@ -1,6 +1,7 @@
import type { ReactNode } from 'react';
import { STATUS_LABEL } from '../format';
import type { HoldStatus, TraceEntry } from '../types';
+import { IconShieldCheck, IconXCircle } from './icons';
export function Badge({ tone, children }: { tone: 'ok' | 'warn' | 'danger' | 'info' | 'neutral'; children: ReactNode }) {
return (
@@ -49,20 +50,19 @@ export function EmptyState({ title, children }: { title: string; children?: Reac
- π‘οΈ
+
{title}
@@ -87,6 +87,7 @@ export function TraceView({ trace, traceId }: { trace: TraceEntry[]; traceId?: s
borderRadius: '50%',
background: t.ok ? 'var(--ok)' : 'var(--danger)',
display: 'inline-block',
+ flexShrink: 0,
}}
/>
{t.tool}
@@ -135,18 +136,23 @@ export function Spinner({ label = 'Loading' }: { label?: string }) {
export function ErrorNote({ message }: { message: string }) {
return (
-
+
{message}
-
+
);
}
diff --git a/apps/web/src/routes/Demo.tsx b/apps/web/src/routes/Demo.tsx
index f0e1f6e..a7de3c1 100644
--- a/apps/web/src/routes/Demo.tsx
+++ b/apps/web/src/routes/Demo.tsx
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import { ApiError, api } from '../api';
import { ElderDevice, newMessage, type Message } from '../components/ElderDevice';
import { FamilyPhone } from '../components/FamilyPhone';
+import { IconRefresh, IconShieldCheck, IconVolume, IconVolumeOff } from '../components/icons';
import { ErrorNote } from '../components/parts';
import { useAppState } from '../hooks/useAppState';
import { useSpeech } from '../hooks/useSpeech';
@@ -62,39 +63,63 @@ export function Demo() {
return (