@nockchain/rose 0.1.4-nightly.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/artifacts.yml +33 -0
- package/.github/workflows/ci.yml +68 -0
- package/.github/workflows/publish-sdk.yml +35 -0
- package/.nvmrc +1 -0
- package/.prettierignore +5 -0
- package/.prettierrc +8 -0
- package/LICENSE +22 -0
- package/README.md +117 -0
- package/extension/background/index.ts +1500 -0
- package/extension/content/index.ts +59 -0
- package/extension/icons/rose.svg +27 -0
- package/extension/icons/rose128.png +0 -0
- package/extension/icons/rose16.png +0 -0
- package/extension/icons/rose256.png +0 -0
- package/extension/icons/rose32.png +0 -0
- package/extension/icons/rose48.png +0 -0
- package/extension/icons/rose512.png +0 -0
- package/extension/inpage/index.ts +86 -0
- package/extension/manifest.json +48 -0
- package/extension/popup/Popup.tsx +94 -0
- package/extension/popup/Router.tsx +121 -0
- package/extension/popup/assets/arrow-down-icon.svg +3 -0
- package/extension/popup/assets/arrow-left-icon.svg +3 -0
- package/extension/popup/assets/arrow-right-icon.svg +3 -0
- package/extension/popup/assets/arrow-up-icon.svg +3 -0
- package/extension/popup/assets/arrow-up-right-icon.svg +3 -0
- package/extension/popup/assets/checkmark-icon.svg +3 -0
- package/extension/popup/assets/checkmark-pencil-icon.svg +3 -0
- package/extension/popup/assets/checkmark-success-icon.svg +3 -0
- package/extension/popup/assets/clock-icon.svg +3 -0
- package/extension/popup/assets/close-x-icon.svg +3 -0
- package/extension/popup/assets/copy-icon.svg +6 -0
- package/extension/popup/assets/explorer-icon.svg +3 -0
- package/extension/popup/assets/eye-off-icon.svg +3 -0
- package/extension/popup/assets/eye-open-icon.svg +4 -0
- package/extension/popup/assets/feedback-icon.svg +3 -0
- package/extension/popup/assets/green-status-dot.svg +3 -0
- package/extension/popup/assets/info-icon.svg +3 -0
- package/extension/popup/assets/iris-logo-40.svg +27 -0
- package/extension/popup/assets/iris-logo-96.svg +27 -0
- package/extension/popup/assets/iris-logo-blue.svg +27 -0
- package/extension/popup/assets/iris-logo-no-eye.svg +27 -0
- package/extension/popup/assets/iris-logo-orange.svg +27 -0
- package/extension/popup/assets/iris-logo.svg +27 -0
- package/extension/popup/assets/key-icon.svg +3 -0
- package/extension/popup/assets/lock-icon-yellow.svg +3 -0
- package/extension/popup/assets/lock-icon.svg +3 -0
- package/extension/popup/assets/pencil-edit-icon.svg +3 -0
- package/extension/popup/assets/permissions-icon.svg +3 -0
- package/extension/popup/assets/receipt-icon.svg +5 -0
- package/extension/popup/assets/refresh-icon.svg +3 -0
- package/extension/popup/assets/settings-gear-icon.svg +8 -0
- package/extension/popup/assets/settings-icon.svg +3 -0
- package/extension/popup/assets/theme-icon.svg +3 -0
- package/extension/popup/assets/trash-bin-icon.svg +3 -0
- package/extension/popup/assets/trend-down-arrow.svg +5 -0
- package/extension/popup/assets/trend-up-arrow.svg +5 -0
- package/extension/popup/assets/user-account-icon.svg +3 -0
- package/extension/popup/assets/vector-bottom-left.svg +9 -0
- package/extension/popup/assets/vector-left.svg +9 -0
- package/extension/popup/assets/vector-right.svg +9 -0
- package/extension/popup/assets/vector-top-right-rotated.svg +8 -0
- package/extension/popup/assets/vector-top-right.svg +9 -0
- package/extension/popup/assets/wallet-dropdown-arrow.svg +5 -0
- package/extension/popup/assets/wallet-icon-style-1.svg +6 -0
- package/extension/popup/assets/wallet-icon-style-10.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-11.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-12.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-13.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-14.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-15.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-2.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-3.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-4.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-5.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-6.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-7.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-8.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-9.svg +8 -0
- package/extension/popup/components/AccountIcon.tsx +78 -0
- package/extension/popup/components/AccountSelector.tsx +246 -0
- package/extension/popup/components/Alert.tsx +48 -0
- package/extension/popup/components/ConfirmModal.tsx +81 -0
- package/extension/popup/components/PasswordInput.tsx +49 -0
- package/extension/popup/components/ScreenContainer.tsx +17 -0
- package/extension/popup/components/SiteIcon.tsx +60 -0
- package/extension/popup/components/ThemeToggle.tsx +44 -0
- package/extension/popup/components/icons/ArrowDownLeftIcon.tsx +20 -0
- package/extension/popup/components/icons/ArrowUpRightIcon.tsx +20 -0
- package/extension/popup/components/icons/CheckIcon.tsx +20 -0
- package/extension/popup/components/icons/ChevronDownIcon.tsx +15 -0
- package/extension/popup/components/icons/ChevronLeftIcon.tsx +15 -0
- package/extension/popup/components/icons/ChevronRightIcon.tsx +15 -0
- package/extension/popup/components/icons/ChevronUpIcon.tsx +15 -0
- package/extension/popup/components/icons/CloseIcon.tsx +26 -0
- package/extension/popup/components/icons/CopyIcon.tsx +20 -0
- package/extension/popup/components/icons/EditIcon.tsx +20 -0
- package/extension/popup/components/icons/EyeIcon.tsx +13 -0
- package/extension/popup/components/icons/EyeOffIcon.tsx +13 -0
- package/extension/popup/components/icons/InfoIcon.tsx +20 -0
- package/extension/popup/components/icons/LockIcon.tsx +20 -0
- package/extension/popup/components/icons/PlusIcon.tsx +15 -0
- package/extension/popup/components/icons/ReceiveArrowIcon.tsx +14 -0
- package/extension/popup/components/icons/ReceiveCircleIcon.tsx +20 -0
- package/extension/popup/components/icons/SendPaperPlaneIcon.tsx +18 -0
- package/extension/popup/components/icons/SentArrowIcon.tsx +21 -0
- package/extension/popup/components/icons/SettingsIcon.tsx +26 -0
- package/extension/popup/components/icons/ShieldIcon.tsx +20 -0
- package/extension/popup/components/icons/UploadIcon.tsx +20 -0
- package/extension/popup/components/icons/WalletIcon.tsx +20 -0
- package/extension/popup/contexts/ThemeContext.tsx +105 -0
- package/extension/popup/hooks/useApprovalDetection.ts +128 -0
- package/extension/popup/hooks/useAutoFocus.ts +36 -0
- package/extension/popup/hooks/useAutoRejectOnClose.ts +25 -0
- package/extension/popup/hooks/useClickOutside.ts +33 -0
- package/extension/popup/hooks/useCopyToClipboard.ts +33 -0
- package/extension/popup/hooks/useFavicon.ts +64 -0
- package/extension/popup/hooks/useNumericInput.ts +93 -0
- package/extension/popup/index.html +13 -0
- package/extension/popup/index.tsx +24 -0
- package/extension/popup/screens/AboutScreen.tsx +118 -0
- package/extension/popup/screens/HomeScreen.tailwind.css +85 -0
- package/extension/popup/screens/HomeScreen.tsx +902 -0
- package/extension/popup/screens/KeySettingsPasswordScreen.tsx +164 -0
- package/extension/popup/screens/LockTimeScreen.tsx +155 -0
- package/extension/popup/screens/ReceiveScreen.tsx +149 -0
- package/extension/popup/screens/RecoveryPhraseScreen.tsx +183 -0
- package/extension/popup/screens/SendReviewScreen.tsx +308 -0
- package/extension/popup/screens/SendScreen.tsx +825 -0
- package/extension/popup/screens/SendSubmittedScreen.tsx +193 -0
- package/extension/popup/screens/SettingsScreen.tsx +116 -0
- package/extension/popup/screens/ThemeSettingsScreen.tsx +107 -0
- package/extension/popup/screens/TransactionDetailsScreen.tsx +346 -0
- package/extension/popup/screens/ViewSecretPhraseScreen.tsx +212 -0
- package/extension/popup/screens/WalletPermissionsScreen.tsx +123 -0
- package/extension/popup/screens/WalletSettingsScreen.tsx +381 -0
- package/extension/popup/screens/WalletStylingScreen.tsx +306 -0
- package/extension/popup/screens/approvals/ConnectApprovalScreen.tsx +136 -0
- package/extension/popup/screens/approvals/SignMessageScreen.tsx +140 -0
- package/extension/popup/screens/approvals/SignRawTxScreen.tsx +320 -0
- package/extension/popup/screens/approvals/TransactionApprovalScreen.tsx +167 -0
- package/extension/popup/screens/onboarding/BackupScreen.tsx +254 -0
- package/extension/popup/screens/onboarding/CreateScreen.tsx +273 -0
- package/extension/popup/screens/onboarding/ImportScreen.tsx +676 -0
- package/extension/popup/screens/onboarding/ImportScreenV0.tsx +678 -0
- package/extension/popup/screens/onboarding/ImportSuccessScreen.tsx +236 -0
- package/extension/popup/screens/onboarding/ResumeBackupScreen.tsx +166 -0
- package/extension/popup/screens/onboarding/StartScreen.tsx +142 -0
- package/extension/popup/screens/onboarding/SuccessScreen.tsx +193 -0
- package/extension/popup/screens/onboarding/VerifyScreen.tsx +220 -0
- package/extension/popup/screens/system/LockedScreen.tsx +288 -0
- package/extension/popup/screens/transactions/ReceiveScreen.tsx +84 -0
- package/extension/popup/screens/transactions/SentScreen.tsx +138 -0
- package/extension/popup/store.ts +482 -0
- package/extension/popup/styles.css +246 -0
- package/extension/popup/utils/format.ts +58 -0
- package/extension/popup/utils/formatWalletError.ts +36 -0
- package/extension/popup/utils/memo.ts +299 -0
- package/extension/popup/utils/messaging.ts +16 -0
- package/extension/shared/address-encoding.ts +69 -0
- package/extension/shared/balance-query.ts +123 -0
- package/extension/shared/constants.ts +386 -0
- package/extension/shared/currency.ts +128 -0
- package/extension/shared/first-name-derivation.ts +128 -0
- package/extension/shared/keyfile.ts +58 -0
- package/extension/shared/onboarding.ts +78 -0
- package/extension/shared/price-api.ts +79 -0
- package/extension/shared/rpc-client-browser.ts +315 -0
- package/extension/shared/transaction-builder.ts +443 -0
- package/extension/shared/types.ts +450 -0
- package/extension/shared/utxo-diff.ts +212 -0
- package/extension/shared/utxo-store.ts +548 -0
- package/extension/shared/utxo-sync.ts +343 -0
- package/extension/shared/validators.ts +26 -0
- package/extension/shared/vault.ts +1580 -0
- package/extension/shared/wallet-crypto.ts +77 -0
- package/extension/shared/wasm-utils.ts +76 -0
- package/extension/shared/webcrypto.ts +67 -0
- package/extension/types/wasm.d.ts +13 -0
- package/package.json +39 -0
- package/postcss.config.js +6 -0
- package/rose-extension-dist.zip +0 -0
- package/sdk/README.md +88 -0
- package/sdk/examples/app.ts +166 -0
- package/sdk/examples/index.html +51 -0
- package/sdk/examples/tsconfig.json +15 -0
- package/sdk/examples/tx-builder.html +532 -0
- package/sdk/examples/tx-builder.ts +1766 -0
- package/sdk/package-lock.json +424 -0
- package/sdk/package.json +68 -0
- package/sdk/src/constants.ts +28 -0
- package/sdk/src/errors.ts +74 -0
- package/sdk/src/hooks/index.ts +1 -0
- package/sdk/src/hooks/use-rose.ts +94 -0
- package/sdk/src/index.ts +12 -0
- package/sdk/src/provider.ts +396 -0
- package/sdk/src/transaction.ts +163 -0
- package/sdk/src/types/rose-wasm.d.ts +14 -0
- package/sdk/src/types.ts +97 -0
- package/sdk/src/wasm.ts +13 -0
- package/sdk/tsconfig.json +20 -0
- package/sdk/vite.config.examples.ts +32 -0
- package/tailwind.config.ts +38 -0
- package/tsconfig.json +20 -0
- package/vite.config.ts +60 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useStore } from '../store';
|
|
3
|
+
import { send } from '../utils/messaging';
|
|
4
|
+
import { INTERNAL_METHODS } from '../../shared/constants';
|
|
5
|
+
import { formatWalletError } from '../utils/formatWalletError';
|
|
6
|
+
import { AccountIcon } from '../components/AccountIcon';
|
|
7
|
+
import { ConfirmModal } from '../components/ConfirmModal';
|
|
8
|
+
import { CloseIcon } from '../components/icons/CloseIcon';
|
|
9
|
+
import { ChevronRightIcon } from '../components/icons/ChevronRightIcon';
|
|
10
|
+
import UserAccountIcon from '../assets/user-account-icon.svg';
|
|
11
|
+
import ThemeIcon from '../assets/theme-icon.svg';
|
|
12
|
+
import CopyIcon from '../assets/copy-icon.svg';
|
|
13
|
+
import CheckmarkIcon from '../assets/checkmark-pencil-icon.svg';
|
|
14
|
+
import PencilEditIcon from '../assets/pencil-edit-icon.svg';
|
|
15
|
+
import SettingsGearIcon from '../assets/settings-gear-icon.svg';
|
|
16
|
+
|
|
17
|
+
export function WalletSettingsScreen() {
|
|
18
|
+
const { navigate, wallet, syncWallet } = useStore();
|
|
19
|
+
|
|
20
|
+
// Get current account from vault
|
|
21
|
+
const currentAccount = wallet.currentAccount || wallet.accounts[0];
|
|
22
|
+
const walletName = currentAccount?.name || 'Wallet';
|
|
23
|
+
const walletAddress = currentAccount?.address || '';
|
|
24
|
+
|
|
25
|
+
const [isEditingName, setIsEditingName] = useState(false);
|
|
26
|
+
const [editedName, setEditedName] = useState(walletName);
|
|
27
|
+
const [copySuccess, setCopySuccess] = useState(false);
|
|
28
|
+
const [showRemoveConfirm, setShowRemoveConfirm] = useState(false);
|
|
29
|
+
const [showLastAccountError, setShowLastAccountError] = useState(false);
|
|
30
|
+
|
|
31
|
+
// Format creation date
|
|
32
|
+
const walletCreatedDate = currentAccount?.createdAt
|
|
33
|
+
? new Date(currentAccount.createdAt).toLocaleDateString('en-US', {
|
|
34
|
+
month: 'long',
|
|
35
|
+
day: 'numeric',
|
|
36
|
+
year: 'numeric',
|
|
37
|
+
hour: '2-digit',
|
|
38
|
+
minute: '2-digit',
|
|
39
|
+
})
|
|
40
|
+
: 'Unknown';
|
|
41
|
+
|
|
42
|
+
// Keep editedName in sync with current account name
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
setEditedName(walletName);
|
|
45
|
+
}, [walletName]);
|
|
46
|
+
|
|
47
|
+
function handleClose() {
|
|
48
|
+
navigate('home');
|
|
49
|
+
}
|
|
50
|
+
function handleStyling() {
|
|
51
|
+
navigate('wallet-styling');
|
|
52
|
+
}
|
|
53
|
+
function handleEditName() {
|
|
54
|
+
setIsEditingName(true);
|
|
55
|
+
setEditedName(walletName);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function handleSaveName() {
|
|
59
|
+
if (!editedName.trim() || !currentAccount) {
|
|
60
|
+
setIsEditingName(false);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Call the vault to rename the account
|
|
65
|
+
const result = await send<{ ok?: boolean; error?: string }>(INTERNAL_METHODS.RENAME_ACCOUNT, [
|
|
66
|
+
currentAccount.index,
|
|
67
|
+
editedName.trim(),
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
if (result?.ok) {
|
|
71
|
+
// Update wallet state with new name
|
|
72
|
+
const updatedAccounts = wallet.accounts.map(acc =>
|
|
73
|
+
acc.index === currentAccount.index ? { ...acc, name: editedName.trim() } : acc
|
|
74
|
+
);
|
|
75
|
+
const updatedCurrentAccount = { ...currentAccount, name: editedName.trim() };
|
|
76
|
+
|
|
77
|
+
syncWallet({
|
|
78
|
+
...wallet,
|
|
79
|
+
accounts: updatedAccounts,
|
|
80
|
+
currentAccount: updatedCurrentAccount,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
setIsEditingName(false);
|
|
84
|
+
} else if (result?.error) {
|
|
85
|
+
alert(`Failed to rename account: ${formatWalletError(result.error)}`);
|
|
86
|
+
setIsEditingName(false);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function handleNameInputChange(e: React.ChangeEvent<HTMLInputElement>) {
|
|
90
|
+
setEditedName(e.target.value);
|
|
91
|
+
}
|
|
92
|
+
function handleNameInputKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
|
|
93
|
+
if (e.key === 'Enter') handleSaveName();
|
|
94
|
+
if (e.key === 'Escape') {
|
|
95
|
+
setIsEditingName(false);
|
|
96
|
+
setEditedName(walletName);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function handleCopyAddress() {
|
|
100
|
+
if (!walletAddress) return;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
await navigator.clipboard.writeText(walletAddress);
|
|
104
|
+
setCopySuccess(true);
|
|
105
|
+
setTimeout(() => setCopySuccess(false), 2000);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.error('Failed to copy address:', err);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function handleRemoveWallet() {
|
|
111
|
+
setShowRemoveConfirm(true);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function confirmRemoveWallet() {
|
|
115
|
+
if (!currentAccount) return;
|
|
116
|
+
|
|
117
|
+
const result = await send<{ ok?: boolean; switchedTo?: number; error?: string }>(
|
|
118
|
+
INTERNAL_METHODS.HIDE_ACCOUNT,
|
|
119
|
+
[currentAccount.index]
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
if (result?.ok) {
|
|
123
|
+
// Update wallet state - filter out hidden account
|
|
124
|
+
const updatedAccounts = wallet.accounts.map(acc =>
|
|
125
|
+
acc.index === currentAccount.index ? { ...acc, hidden: true } : acc
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// If we switched accounts, update the current account
|
|
129
|
+
let updatedCurrentAccount = currentAccount;
|
|
130
|
+
if (result.switchedTo !== undefined) {
|
|
131
|
+
updatedCurrentAccount =
|
|
132
|
+
updatedAccounts.find(acc => acc.index === result.switchedTo) || currentAccount;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
syncWallet({
|
|
136
|
+
...wallet,
|
|
137
|
+
accounts: updatedAccounts,
|
|
138
|
+
currentAccount: updatedCurrentAccount,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Close settings and go back to home
|
|
142
|
+
setShowRemoveConfirm(false);
|
|
143
|
+
navigate('home');
|
|
144
|
+
} else if (result?.error) {
|
|
145
|
+
setShowRemoveConfirm(false);
|
|
146
|
+
if (result.error === 'CANNOT_HIDE_LAST_ACCOUNT') {
|
|
147
|
+
setShowLastAccountError(true);
|
|
148
|
+
} else {
|
|
149
|
+
alert(`Failed to hide account: ${formatWalletError(result.error)}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function cancelRemoveWallet() {
|
|
155
|
+
setShowRemoveConfirm(false);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const addressStart = walletAddress.slice(0, 6);
|
|
159
|
+
const addressMiddle = walletAddress.slice(6, -5);
|
|
160
|
+
const addressEnd = walletAddress.slice(-5);
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<div
|
|
164
|
+
className="w-[357px] h-[600px] flex flex-col"
|
|
165
|
+
style={{ backgroundColor: 'var(--color-bg)', color: 'var(--color-text-primary)' }}
|
|
166
|
+
>
|
|
167
|
+
{/* Header */}
|
|
168
|
+
<header
|
|
169
|
+
className="flex items-center justify-between px-4 py-3 min-h-[64px]"
|
|
170
|
+
style={{ backgroundColor: 'var(--color-bg)' }}
|
|
171
|
+
>
|
|
172
|
+
<div className="w-8 h-8 flex items-center justify-center">
|
|
173
|
+
<AccountIcon
|
|
174
|
+
styleId={currentAccount?.iconStyleId}
|
|
175
|
+
color={currentAccount?.iconColor}
|
|
176
|
+
className="w-6 h-6"
|
|
177
|
+
/>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<h1 className="m-0 text-base font-medium leading-[22px] tracking-[0.16px]">{walletName}</h1>
|
|
181
|
+
|
|
182
|
+
<button
|
|
183
|
+
type="button"
|
|
184
|
+
onClick={handleClose}
|
|
185
|
+
className="w-8 h-8 flex items-center justify-center rounded-lg transition-colors focus:outline-none focus-visible:ring-2"
|
|
186
|
+
style={{ color: 'var(--color-text-primary)' }}
|
|
187
|
+
onMouseEnter={e => (e.currentTarget.style.backgroundColor = 'var(--color-surface-800)')}
|
|
188
|
+
onMouseLeave={e => (e.currentTarget.style.backgroundColor = 'transparent')}
|
|
189
|
+
aria-label="Close"
|
|
190
|
+
>
|
|
191
|
+
<CloseIcon />
|
|
192
|
+
</button>
|
|
193
|
+
</header>
|
|
194
|
+
|
|
195
|
+
{/* Content */}
|
|
196
|
+
<div
|
|
197
|
+
className="flex flex-col justify-between h-[536px]"
|
|
198
|
+
style={{ backgroundColor: 'var(--color-bg)' }}
|
|
199
|
+
>
|
|
200
|
+
<div className="flex flex-col gap-4 px-4 py-2">
|
|
201
|
+
{/* Settings Options */}
|
|
202
|
+
<div className="flex flex-col gap-2">
|
|
203
|
+
{/* Account Name */}
|
|
204
|
+
<div
|
|
205
|
+
className="flex items-center justify-between p-2 rounded-lg transition-colors"
|
|
206
|
+
onMouseEnter={e =>
|
|
207
|
+
(e.currentTarget.style.backgroundColor = 'var(--color-surface-800)')
|
|
208
|
+
}
|
|
209
|
+
onMouseLeave={e => (e.currentTarget.style.backgroundColor = 'transparent')}
|
|
210
|
+
>
|
|
211
|
+
<div className="flex items-center gap-2.5 flex-1">
|
|
212
|
+
<div
|
|
213
|
+
className="w-8 h-8 flex items-center justify-center rounded-lg"
|
|
214
|
+
style={{ backgroundColor: 'var(--color-surface-800)' }}
|
|
215
|
+
>
|
|
216
|
+
<img src={UserAccountIcon} alt="Account" className="w-5 h-5" />
|
|
217
|
+
</div>
|
|
218
|
+
<div className="text-sm font-medium leading-[18px] tracking-[0.14px]">
|
|
219
|
+
Wallet name
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
{isEditingName ? (
|
|
224
|
+
<div
|
|
225
|
+
className="flex items-center gap-2 rounded-lg px-2 py-1.5"
|
|
226
|
+
style={{ border: '1px solid var(--color-divider)' }}
|
|
227
|
+
>
|
|
228
|
+
<input
|
|
229
|
+
type="text"
|
|
230
|
+
value={editedName}
|
|
231
|
+
onChange={handleNameInputChange}
|
|
232
|
+
onKeyDown={handleNameInputKeyDown}
|
|
233
|
+
autoFocus
|
|
234
|
+
maxLength={30}
|
|
235
|
+
className="w-[100px] bg-transparent outline-none text-sm font-medium leading-[18px] tracking-[0.14px]"
|
|
236
|
+
style={{ color: 'var(--color-text-primary)' }}
|
|
237
|
+
placeholder="Wallet name"
|
|
238
|
+
/>
|
|
239
|
+
<button
|
|
240
|
+
type="button"
|
|
241
|
+
onClick={handleSaveName}
|
|
242
|
+
className="p-1 rounded transition-opacity hover:opacity-80 focus:outline-none focus-visible:ring-2"
|
|
243
|
+
aria-label="Save name"
|
|
244
|
+
>
|
|
245
|
+
<img src={CheckmarkIcon} alt="" className="w-5 h-5" />
|
|
246
|
+
</button>
|
|
247
|
+
</div>
|
|
248
|
+
) : (
|
|
249
|
+
<button
|
|
250
|
+
type="button"
|
|
251
|
+
onClick={handleEditName}
|
|
252
|
+
className="flex items-center gap-2 rounded-lg px-2 py-1.5 transition-colors focus:outline-none focus-visible:ring-2"
|
|
253
|
+
style={{ backgroundColor: 'var(--color-surface-800)' }}
|
|
254
|
+
onMouseEnter={e =>
|
|
255
|
+
(e.currentTarget.style.backgroundColor = 'var(--color-surface-700)')
|
|
256
|
+
}
|
|
257
|
+
onMouseLeave={e =>
|
|
258
|
+
(e.currentTarget.style.backgroundColor = 'var(--color-surface-800)')
|
|
259
|
+
}
|
|
260
|
+
>
|
|
261
|
+
<span
|
|
262
|
+
className="text-sm font-medium leading-[18px] tracking-[0.14px] whitespace-nowrap"
|
|
263
|
+
style={{ color: 'var(--color-text-muted)' }}
|
|
264
|
+
>
|
|
265
|
+
{walletName}
|
|
266
|
+
</span>
|
|
267
|
+
<img src={PencilEditIcon} alt="" className="w-5 h-5" />
|
|
268
|
+
</button>
|
|
269
|
+
)}
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
{/* Styling */}
|
|
273
|
+
<button
|
|
274
|
+
type="button"
|
|
275
|
+
onClick={handleStyling}
|
|
276
|
+
className="flex items-center justify-between p-2 rounded-lg transition-colors focus:outline-none focus-visible:ring-2"
|
|
277
|
+
onMouseEnter={e =>
|
|
278
|
+
(e.currentTarget.style.backgroundColor = 'var(--color-surface-800)')
|
|
279
|
+
}
|
|
280
|
+
onMouseLeave={e => (e.currentTarget.style.backgroundColor = 'transparent')}
|
|
281
|
+
>
|
|
282
|
+
<div className="flex items-center gap-2.5 flex-1">
|
|
283
|
+
<div
|
|
284
|
+
className="w-8 h-8 flex items-center justify-center rounded-lg"
|
|
285
|
+
style={{ backgroundColor: 'var(--color-surface-800)' }}
|
|
286
|
+
>
|
|
287
|
+
<AccountIcon styleId={1} color="var(--color-text-muted)" className="w-5 h-5" />
|
|
288
|
+
</div>
|
|
289
|
+
<div className="text-sm font-medium leading-[18px] tracking-[0.14px]">Styling</div>
|
|
290
|
+
</div>
|
|
291
|
+
<div className="p-1">
|
|
292
|
+
<ChevronRightIcon className="w-4 h-4" />
|
|
293
|
+
</div>
|
|
294
|
+
</button>
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
{/* Address Box */}
|
|
298
|
+
<div
|
|
299
|
+
className="flex flex-col items-center gap-5 px-3 pt-5 pb-3 rounded-lg"
|
|
300
|
+
style={{ backgroundColor: 'var(--color-surface-800)' }}
|
|
301
|
+
>
|
|
302
|
+
<div
|
|
303
|
+
className="text-sm font-medium leading-[18px] tracking-[0.14px] text-center break-words w-full"
|
|
304
|
+
style={{ wordBreak: 'break-all' }}
|
|
305
|
+
>
|
|
306
|
+
<span style={{ color: 'var(--color-text-primary)' }}>{addressStart}</span>
|
|
307
|
+
<span style={{ color: 'var(--color-text-muted)' }}>{addressMiddle}</span>
|
|
308
|
+
<span style={{ color: 'var(--color-text-primary)' }}>{addressEnd}</span>
|
|
309
|
+
</div>
|
|
310
|
+
<button
|
|
311
|
+
type="button"
|
|
312
|
+
onClick={handleCopyAddress}
|
|
313
|
+
className="inline-flex items-center justify-center gap-[6px] py-[7px] pl-3 pr-4 bg-transparent rounded-full text-sm font-medium leading-[18px] tracking-[0.14px] transition-opacity hover:opacity-80 focus:outline-none focus-visible:ring-2"
|
|
314
|
+
style={{
|
|
315
|
+
border: '1px solid var(--color-text-primary)',
|
|
316
|
+
color: 'var(--color-text-primary)',
|
|
317
|
+
}}
|
|
318
|
+
>
|
|
319
|
+
<img src={CopyIcon} alt="" className="w-4 h-4 shrink-0" />
|
|
320
|
+
{copySuccess ? 'Copied!' : 'Copy address'}
|
|
321
|
+
</button>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
|
|
325
|
+
{/* Footer */}
|
|
326
|
+
<div className="flex flex-col gap-2 px-4 py-3">
|
|
327
|
+
<div
|
|
328
|
+
className="flex items-center justify-between gap-2 px-2 rounded-lg"
|
|
329
|
+
style={{ backgroundColor: 'var(--color-bg)' }}
|
|
330
|
+
>
|
|
331
|
+
<div
|
|
332
|
+
className="text-[10px] leading-4 tracking-[0.24px] flex-1"
|
|
333
|
+
style={{ color: 'var(--color-text-muted)' }}
|
|
334
|
+
>
|
|
335
|
+
Wallet created on {walletCreatedDate}
|
|
336
|
+
</div>
|
|
337
|
+
<div className="flex items-center justify-center rounded-lg py-2 px-3">
|
|
338
|
+
<AccountIcon styleId={1} color="var(--color-text-muted)" className="w-4 h-4" />
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
|
|
342
|
+
<button
|
|
343
|
+
type="button"
|
|
344
|
+
onClick={handleRemoveWallet}
|
|
345
|
+
className="flex items-center justify-center px-5 py-[15px] rounded-lg transition-opacity hover:opacity-80 focus:outline-none focus-visible:ring-2"
|
|
346
|
+
style={{ backgroundColor: 'var(--color-red-light)' }}
|
|
347
|
+
>
|
|
348
|
+
<div
|
|
349
|
+
className="text-sm font-medium leading-[18px] tracking-[0.14px] text-center"
|
|
350
|
+
style={{ color: 'var(--color-red)' }}
|
|
351
|
+
>
|
|
352
|
+
Remove wallet
|
|
353
|
+
</div>
|
|
354
|
+
</button>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
{/* Remove Confirmation Modal */}
|
|
359
|
+
<ConfirmModal
|
|
360
|
+
isOpen={showRemoveConfirm}
|
|
361
|
+
title="Are you sure?"
|
|
362
|
+
message={`This will remove "${walletName}" from your wallet. The account and its funds will remain on the blockchain, but no longer be visible in this wallet.`}
|
|
363
|
+
confirmText="Confirm"
|
|
364
|
+
cancelText="Cancel"
|
|
365
|
+
onConfirm={confirmRemoveWallet}
|
|
366
|
+
onCancel={cancelRemoveWallet}
|
|
367
|
+
variant="danger"
|
|
368
|
+
/>
|
|
369
|
+
|
|
370
|
+
{/* Last Account Error Modal */}
|
|
371
|
+
<ConfirmModal
|
|
372
|
+
isOpen={showLastAccountError}
|
|
373
|
+
title="Cannot Remove Wallet"
|
|
374
|
+
message="You cannot remove your last visible account. You must have at least one account in your wallet."
|
|
375
|
+
confirmText="OK"
|
|
376
|
+
onConfirm={() => setShowLastAccountError(false)}
|
|
377
|
+
onCancel={() => setShowLastAccountError(false)}
|
|
378
|
+
/>
|
|
379
|
+
</div>
|
|
380
|
+
);
|
|
381
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { useStore } from '../store';
|
|
3
|
+
import { send } from '../utils/messaging';
|
|
4
|
+
import { INTERNAL_METHODS, ACCOUNT_COLORS } from '../../shared/constants';
|
|
5
|
+
import { ChevronLeftIcon } from '../components/icons/ChevronLeftIcon';
|
|
6
|
+
import { ChevronRightIcon } from '../components/icons/ChevronRightIcon';
|
|
7
|
+
|
|
8
|
+
// Icons
|
|
9
|
+
import WalletStyle1 from '../assets/wallet-icon-style-1.svg';
|
|
10
|
+
import WalletStyle2 from '../assets/wallet-icon-style-2.svg';
|
|
11
|
+
import WalletStyle3 from '../assets/wallet-icon-style-3.svg';
|
|
12
|
+
import WalletStyle4 from '../assets/wallet-icon-style-4.svg';
|
|
13
|
+
import WalletStyle5 from '../assets/wallet-icon-style-5.svg';
|
|
14
|
+
import WalletStyle6 from '../assets/wallet-icon-style-6.svg';
|
|
15
|
+
import WalletStyle7 from '../assets/wallet-icon-style-7.svg';
|
|
16
|
+
import WalletStyle8 from '../assets/wallet-icon-style-8.svg';
|
|
17
|
+
import WalletStyle9 from '../assets/wallet-icon-style-9.svg';
|
|
18
|
+
import WalletStyle10 from '../assets/wallet-icon-style-10.svg';
|
|
19
|
+
import WalletStyle11 from '../assets/wallet-icon-style-11.svg';
|
|
20
|
+
import WalletStyle12 from '../assets/wallet-icon-style-12.svg';
|
|
21
|
+
import WalletStyle13 from '../assets/wallet-icon-style-13.svg';
|
|
22
|
+
import WalletStyle14 from '../assets/wallet-icon-style-14.svg';
|
|
23
|
+
import WalletStyle15 from '../assets/wallet-icon-style-15.svg';
|
|
24
|
+
|
|
25
|
+
export function WalletStylingScreen() {
|
|
26
|
+
const { navigate, wallet, syncWallet } = useStore();
|
|
27
|
+
|
|
28
|
+
// Get current account
|
|
29
|
+
const currentAccount = wallet.currentAccount || wallet.accounts[0];
|
|
30
|
+
|
|
31
|
+
// Load initial values from current account or use defaults
|
|
32
|
+
const [selectedStyle, setSelectedStyle] = useState(currentAccount?.iconStyleId || 1);
|
|
33
|
+
const [selectedColor, setSelectedColor] = useState(currentAccount?.iconColor || '#5968fb');
|
|
34
|
+
const [svgContent, setSvgContent] = useState<string>('');
|
|
35
|
+
|
|
36
|
+
// Track if we're scrolled to the end (false = at start, true = at end)
|
|
37
|
+
const [isScrolledRight, setIsScrolledRight] = useState(false);
|
|
38
|
+
const colorScrollRef = useRef<HTMLDivElement>(null);
|
|
39
|
+
|
|
40
|
+
const iconStyles = [
|
|
41
|
+
{ id: 1, icon: WalletStyle1 },
|
|
42
|
+
{ id: 2, icon: WalletStyle2 },
|
|
43
|
+
{ id: 3, icon: WalletStyle3 },
|
|
44
|
+
{ id: 4, icon: WalletStyle4 },
|
|
45
|
+
{ id: 5, icon: WalletStyle5 },
|
|
46
|
+
{ id: 6, icon: WalletStyle6 },
|
|
47
|
+
{ id: 7, icon: WalletStyle7 },
|
|
48
|
+
{ id: 8, icon: WalletStyle8 },
|
|
49
|
+
{ id: 9, icon: WalletStyle9 },
|
|
50
|
+
{ id: 10, icon: WalletStyle10 },
|
|
51
|
+
{ id: 11, icon: WalletStyle11 },
|
|
52
|
+
{ id: 12, icon: WalletStyle12 },
|
|
53
|
+
{ id: 13, icon: WalletStyle13 },
|
|
54
|
+
{ id: 14, icon: WalletStyle14 },
|
|
55
|
+
{ id: 15, icon: WalletStyle15 },
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
// Use shared color constants
|
|
59
|
+
const colors = ACCOUNT_COLORS;
|
|
60
|
+
|
|
61
|
+
// Sync state when current account changes
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (currentAccount) {
|
|
64
|
+
setSelectedStyle(currentAccount.iconStyleId || 1);
|
|
65
|
+
const color = currentAccount.iconColor || '#5968fb';
|
|
66
|
+
setSelectedColor(color);
|
|
67
|
+
}
|
|
68
|
+
}, [currentAccount?.index]);
|
|
69
|
+
|
|
70
|
+
// Load and modify SVG based on selected style and color
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
const selectedIcon = iconStyles.find(s => s.id === selectedStyle);
|
|
73
|
+
if (!selectedIcon) return;
|
|
74
|
+
|
|
75
|
+
fetch(selectedIcon.icon)
|
|
76
|
+
.then(res => res.text())
|
|
77
|
+
.then(text => {
|
|
78
|
+
// Replace CSS var `--fill-0` with the chosen color
|
|
79
|
+
const modifiedSvg = text.replace(/var\(--fill-0,\s*#[A-Fa-f0-9]{6}\)/g, selectedColor);
|
|
80
|
+
setSvgContent(modifiedSvg);
|
|
81
|
+
})
|
|
82
|
+
.catch(err => console.error('Failed to load SVG:', err));
|
|
83
|
+
}, [selectedStyle, selectedColor]);
|
|
84
|
+
|
|
85
|
+
// Persist styling changes
|
|
86
|
+
async function handleStyleChange(styleId: number) {
|
|
87
|
+
if (!currentAccount) return;
|
|
88
|
+
|
|
89
|
+
setSelectedStyle(styleId);
|
|
90
|
+
|
|
91
|
+
const result = await send<{ ok?: boolean; error?: string }>(
|
|
92
|
+
INTERNAL_METHODS.UPDATE_ACCOUNT_STYLING,
|
|
93
|
+
[currentAccount.index, styleId, selectedColor]
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
if (result?.ok) {
|
|
97
|
+
// Update wallet state
|
|
98
|
+
const updatedAccounts = wallet.accounts.map(acc =>
|
|
99
|
+
acc.index === currentAccount.index
|
|
100
|
+
? { ...acc, iconStyleId: styleId, iconColor: selectedColor }
|
|
101
|
+
: acc
|
|
102
|
+
);
|
|
103
|
+
const updatedCurrentAccount = {
|
|
104
|
+
...currentAccount,
|
|
105
|
+
iconStyleId: styleId,
|
|
106
|
+
iconColor: selectedColor,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
syncWallet({
|
|
110
|
+
...wallet,
|
|
111
|
+
accounts: updatedAccounts,
|
|
112
|
+
currentAccount: updatedCurrentAccount,
|
|
113
|
+
});
|
|
114
|
+
} else if (result?.error) {
|
|
115
|
+
console.error('Failed to update styling:', result.error);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function handleColorChange(color: string) {
|
|
120
|
+
if (!currentAccount) return;
|
|
121
|
+
|
|
122
|
+
setSelectedColor(color);
|
|
123
|
+
|
|
124
|
+
const result = await send<{ ok?: boolean; error?: string }>(
|
|
125
|
+
INTERNAL_METHODS.UPDATE_ACCOUNT_STYLING,
|
|
126
|
+
[currentAccount.index, selectedStyle, color]
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
if (result?.ok) {
|
|
130
|
+
// Update wallet state
|
|
131
|
+
const updatedAccounts = wallet.accounts.map(acc =>
|
|
132
|
+
acc.index === currentAccount.index
|
|
133
|
+
? { ...acc, iconStyleId: selectedStyle, iconColor: color }
|
|
134
|
+
: acc
|
|
135
|
+
);
|
|
136
|
+
const updatedCurrentAccount = {
|
|
137
|
+
...currentAccount,
|
|
138
|
+
iconStyleId: selectedStyle,
|
|
139
|
+
iconColor: color,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
syncWallet({
|
|
143
|
+
...wallet,
|
|
144
|
+
accounts: updatedAccounts,
|
|
145
|
+
currentAccount: updatedCurrentAccount,
|
|
146
|
+
});
|
|
147
|
+
} else if (result?.error) {
|
|
148
|
+
console.error('Failed to update styling:', result.error);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function handleBack() {
|
|
153
|
+
navigate('wallet-settings');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function handleColorScrollLeft() {
|
|
157
|
+
if (!colorScrollRef.current) return;
|
|
158
|
+
// Scroll to the start
|
|
159
|
+
colorScrollRef.current.scrollTo({
|
|
160
|
+
left: 0,
|
|
161
|
+
behavior: 'smooth',
|
|
162
|
+
});
|
|
163
|
+
setIsScrolledRight(false);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function handleColorScrollRight() {
|
|
167
|
+
if (!colorScrollRef.current) return;
|
|
168
|
+
// Scroll to the end
|
|
169
|
+
colorScrollRef.current.scrollTo({
|
|
170
|
+
left: colorScrollRef.current.scrollWidth,
|
|
171
|
+
behavior: 'smooth',
|
|
172
|
+
});
|
|
173
|
+
setIsScrolledRight(true);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<div
|
|
178
|
+
className="w-[357px] h-[600px] flex flex-col"
|
|
179
|
+
style={{ backgroundColor: 'var(--color-bg)', color: 'var(--color-text-primary)' }}
|
|
180
|
+
>
|
|
181
|
+
{/* Header */}
|
|
182
|
+
<header
|
|
183
|
+
className="flex items-center justify-between px-4 py-3 min-h-[64px]"
|
|
184
|
+
style={{ backgroundColor: 'var(--color-bg)' }}
|
|
185
|
+
>
|
|
186
|
+
<button
|
|
187
|
+
type="button"
|
|
188
|
+
onClick={handleBack}
|
|
189
|
+
aria-label="Back"
|
|
190
|
+
className="w-8 h-8 p-2 flex items-center justify-center rounded-lg transition-colors focus:outline-none focus-visible:ring-2"
|
|
191
|
+
style={{ color: 'var(--color-text-primary)' }}
|
|
192
|
+
onMouseEnter={e => (e.currentTarget.style.backgroundColor = 'var(--color-surface-800)')}
|
|
193
|
+
onMouseLeave={e => (e.currentTarget.style.backgroundColor = 'transparent')}
|
|
194
|
+
>
|
|
195
|
+
<ChevronLeftIcon className="w-5 h-5" />
|
|
196
|
+
</button>
|
|
197
|
+
<h1 className="m-0 text-base font-medium leading-[22px] tracking-[0.16px]">Styling</h1>
|
|
198
|
+
<div className="w-8 h-8" />
|
|
199
|
+
</header>
|
|
200
|
+
|
|
201
|
+
{/* Content */}
|
|
202
|
+
<div className="flex flex-col gap-[20px] h-[536px] pt-[16px] px-0 pb-0">
|
|
203
|
+
{/* Preview */}
|
|
204
|
+
<div className="flex items-center justify-center shrink-0">
|
|
205
|
+
<div className="w-24 h-24 block" dangerouslySetInnerHTML={{ __html: svgContent }} />
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
{/* Inner wrapper for padding */}
|
|
209
|
+
<div className="flex flex-col gap-[32px] px-[16px] py-[12px] flex-1 min-h-0">
|
|
210
|
+
{/* Icon Styles Section */}
|
|
211
|
+
<div className="flex flex-col gap-[10px] flex-1 min-h-0">
|
|
212
|
+
<h2 className="text-sm font-medium leading-[18px] tracking-[0.14px] text-center m-0">
|
|
213
|
+
Icon style
|
|
214
|
+
</h2>
|
|
215
|
+
<div className="flex-1 overflow-y-auto min-h-0">
|
|
216
|
+
<div className="flex flex-wrap gap-[8px] justify-center">
|
|
217
|
+
{iconStyles.map(style => {
|
|
218
|
+
const selected = selectedStyle === style.id;
|
|
219
|
+
return (
|
|
220
|
+
<button
|
|
221
|
+
key={style.id}
|
|
222
|
+
type="button"
|
|
223
|
+
onClick={() => handleStyleChange(style.id)}
|
|
224
|
+
className={`flex items-center justify-center p-[10px] rounded-[12px] transition-colors focus:outline-none focus-visible:ring-2 shrink-0 ${!selected ? 'hover:bg-[var(--color-surface-900)]' : ''}`}
|
|
225
|
+
style={{
|
|
226
|
+
width: '58px',
|
|
227
|
+
height: '58px',
|
|
228
|
+
backgroundColor: 'var(--color-bg)',
|
|
229
|
+
border: `1px solid ${selected ? 'var(--color-text-primary)' : 'var(--color-surface-800)'}`,
|
|
230
|
+
}}
|
|
231
|
+
aria-pressed={selected}
|
|
232
|
+
>
|
|
233
|
+
<img src={style.icon} alt={`Style ${style.id}`} className="w-6 h-6" />
|
|
234
|
+
</button>
|
|
235
|
+
);
|
|
236
|
+
})}
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
|
|
241
|
+
{/* Icon Color Section */}
|
|
242
|
+
<div className="shrink-0 flex flex-col gap-[10px] pb-[24px]">
|
|
243
|
+
<div className="flex items-center gap-[9px]">
|
|
244
|
+
<button
|
|
245
|
+
type="button"
|
|
246
|
+
onClick={handleColorScrollLeft}
|
|
247
|
+
disabled={!isScrolledRight}
|
|
248
|
+
className="p-[8px] transition-opacity focus:outline-none focus-visible:ring-2 disabled:opacity-30"
|
|
249
|
+
aria-label="Previous color"
|
|
250
|
+
>
|
|
251
|
+
<ChevronLeftIcon className="w-5 h-5" />
|
|
252
|
+
</button>
|
|
253
|
+
<h2 className="flex-1 text-sm font-medium leading-[18px] tracking-[0.14px] text-center m-0">
|
|
254
|
+
Icon color
|
|
255
|
+
</h2>
|
|
256
|
+
<button
|
|
257
|
+
type="button"
|
|
258
|
+
onClick={handleColorScrollRight}
|
|
259
|
+
disabled={isScrolledRight}
|
|
260
|
+
className="p-[8px] transition-opacity focus:outline-none focus-visible:ring-2 disabled:opacity-30"
|
|
261
|
+
aria-label="Next color"
|
|
262
|
+
>
|
|
263
|
+
<ChevronRightIcon className="w-5 h-5" />
|
|
264
|
+
</button>
|
|
265
|
+
</div>
|
|
266
|
+
{/* The rail */}
|
|
267
|
+
<div className="overflow-hidden">
|
|
268
|
+
<div
|
|
269
|
+
ref={colorScrollRef}
|
|
270
|
+
className="flex gap-[8px] justify-start overflow-x-auto snap-x snap-mandatory"
|
|
271
|
+
style={{
|
|
272
|
+
scrollbarWidth: 'none',
|
|
273
|
+
msOverflowStyle: 'none',
|
|
274
|
+
scrollPaddingLeft: 0,
|
|
275
|
+
scrollPaddingRight: 0,
|
|
276
|
+
}}
|
|
277
|
+
>
|
|
278
|
+
{colors.map(color => {
|
|
279
|
+
const selected = selectedColor === color;
|
|
280
|
+
return (
|
|
281
|
+
<button
|
|
282
|
+
key={color}
|
|
283
|
+
type="button"
|
|
284
|
+
onClick={() => handleColorChange(color)}
|
|
285
|
+
className={`flex items-center justify-center p-0 rounded-[12px] transition-colors focus:outline-none focus-visible:ring-2 shrink-0 snap-start ${!selected ? 'hover:bg-[var(--color-surface-900)]' : ''}`}
|
|
286
|
+
style={{
|
|
287
|
+
width: '46px',
|
|
288
|
+
height: '46px',
|
|
289
|
+
backgroundColor: 'var(--color-bg)',
|
|
290
|
+
border: `1px solid ${selected ? 'var(--color-text-primary)' : 'var(--color-surface-800)'}`,
|
|
291
|
+
}}
|
|
292
|
+
aria-label={`Color ${color}`}
|
|
293
|
+
aria-pressed={selected}
|
|
294
|
+
>
|
|
295
|
+
<div className="w-5 h-5 rounded-full" style={{ backgroundColor: color }} />
|
|
296
|
+
</button>
|
|
297
|
+
);
|
|
298
|
+
})}
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
);
|
|
306
|
+
}
|