refactor: update branding assets and convert settings to accordion UI
- Replace SVG favicons with PNG icons from new smeshicon assets - Add theme-aware Icon component using smeshiconlight/dark PNGs - Refactor Settings page to use collapsible accordion sections - Add Radix UI accordion component with animations - Update QrCode component to use new PNG icon - Remove old favicon.svg and nostr.json files - Add new logo assets in resources/ and src/assets/ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@@ -13,7 +13,7 @@
|
||||
|
||||
<meta name="apple-mobile-web-app-title" content="Smesh" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="48x48" />
|
||||
<link rel="icon" href="/favicon.svg" sizes="any" type="image/svg+xml" />
|
||||
<link rel="icon" href="/favicon.png" sizes="256x256" type="image/png" />
|
||||
<meta name="theme-color" content="#171717" media="(prefers-color-scheme: dark)" />
|
||||
<meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)" />
|
||||
|
||||
|
||||
349
package-lock.json
generated
@@ -15,6 +15,7 @@
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@getalby/bitcoin-connect-react": "^3.10.0",
|
||||
"@noble/hashes": "^1.6.1",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.4",
|
||||
"@radix-ui/react-avatar": "^1.1.2",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
@@ -2683,6 +2684,189 @@
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
|
||||
"integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="
|
||||
},
|
||||
"node_modules/@radix-ui/react-accordion": {
|
||||
"version": "1.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz",
|
||||
"integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-collapsible": "1.1.12",
|
||||
"@radix-ui/react-collection": "1.1.7",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-direction": "1.1.1",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
||||
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-collection": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
|
||||
"integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-direction": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
|
||||
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-id": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
|
||||
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-use-controllable-state": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
|
||||
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-effect-event": "0.0.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-alert-dialog": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.4.tgz",
|
||||
@@ -2955,6 +3139,171 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collapsible": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz",
|
||||
"integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-presence": "1.1.5",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
||||
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-id": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
|
||||
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
|
||||
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-use-controllable-state": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
|
||||
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-effect-event": "0.0.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collection": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz",
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@getalby/bitcoin-connect-react": "^3.10.0",
|
||||
"@noble/hashes": "^1.6.1",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.4",
|
||||
"@radix-ui/react-avatar": "^1.1.2",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"names": {
|
||||
"_": "f4eb8e62add1340b9cadcd9861e669b2e907cea534e0f7f3ac974c11c758a51a",
|
||||
"cody": "8125b911ed0e94dbe3008a0be48cfe5cd0c0b05923cfff917ae7e87da8400883",
|
||||
"cody2": "24462930821b45f530ec0063eca0a6522e5a577856f982fa944df0ef3caf03ab"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 9.4 KiB |
BIN
public/favicon.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 15 KiB |
BIN
resources/smeshdark.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
resources/smeshicondark.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
resources/smeshiconlight.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
resources/smeshlight.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
@@ -1,24 +1,10 @@
|
||||
import { useTheme } from '@/providers/ThemeProvider'
|
||||
import iconLight from './smeshiconlight.png'
|
||||
import iconDark from './smeshicondark.png'
|
||||
|
||||
export default function Icon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 1080 1228"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
xmlSpace="preserve"
|
||||
style={{
|
||||
fill: 'currentcolor',
|
||||
fillRule: 'evenodd',
|
||||
clipRule: 'evenodd',
|
||||
strokeLinejoin: 'round',
|
||||
strokeMiterlimit: 2
|
||||
}}
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
id="Icon-Curve-Cut"
|
||||
d="M360.047,1225.75c-31.046,-3.901 -75.11,-14.46 -106.756,-25.58c-101.676,-35.727 -175.164,-93.066 -215.387,-168.055c-12.079,-22.521 -30.071,-71.422 -27.297,-74.195c0.736,-0.736 11.648,5.578 24.249,14.031c135.436,90.86 301.047,169.043 465.056,219.547l32.77,10.091l-20.27,7.416c-43.455,15.896 -105.159,22.678 -152.365,16.745Zm166.293,-59.234c-168.523,-50.004 -331.475,-126.514 -481.755,-226.196c-37.737,-25.031 -41.489,-28.372 -43.419,-38.663c-3.585,-19.109 1.498,-83.894 9.798,-124.886c7.343,-36.266 27.664,-106.034 32.278,-110.818c2.023,-2.099 217.924,48.207 221.274,51.557c0.975,0.975 -1.132,11.339 -4.682,23.032c-24.542,80.842 -27.217,127.586 -9.935,173.593c22.507,59.917 114.521,99.888 177.281,77.012c29.23,-10.654 56.593,-41.085 82.629,-91.894c29.288,-57.155 32.348,-64.988 196.483,-503.076c81.138,-216.562 148.499,-394.821 149.692,-396.131c2.1,-2.304 217.949,76.926 223.076,81.884c2.056,1.988 -262.476,712.505 -307.806,826.747c-18.422,46.426 -56.939,123.045 -77.918,154.993c-10.157,15.469 -30.753,40.901 -45.769,56.515c-27.821,28.93 -66.46,58.952 -75.447,58.621c-2.738,-0.106 -23.339,-5.631 -45.78,-12.29Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
const { theme } = useTheme()
|
||||
const iconSrc = theme === 'light' ? iconLight : iconDark
|
||||
|
||||
return <img src={iconSrc} alt="Smesh" className={className} />
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 16 KiB |
BIN
src/assets/smeshicondark.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
src/assets/smeshiconlight.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 12 KiB |
@@ -1,6 +1,6 @@
|
||||
import QRCodeStyling from 'qr-code-styling'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import iconSvg from '../../assets/favicon.svg'
|
||||
import iconImg from '../../assets/smeshicondark.png'
|
||||
|
||||
export default function QrCode({ value, size = 180 }: { value: string; size?: number }) {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
@@ -13,7 +13,7 @@ export default function QrCode({ value, size = 180 }: { value: string; size?: nu
|
||||
qrOptions: {
|
||||
errorCorrectionLevel: 'M'
|
||||
},
|
||||
image: iconSvg,
|
||||
image: iconImg,
|
||||
width: size * pixelRatio,
|
||||
height: size * pixelRatio,
|
||||
data: value,
|
||||
|
||||
@@ -1,91 +1,586 @@
|
||||
import AboutInfoDialog from '@/components/AboutInfoDialog'
|
||||
import Donation from '@/components/Donation'
|
||||
import Emoji from '@/components/Emoji'
|
||||
import EmojiPackList from '@/components/EmojiPackList'
|
||||
import EmojiPickerDialog from '@/components/EmojiPickerDialog'
|
||||
import FavoriteRelaysSetting from '@/components/FavoriteRelaysSetting'
|
||||
import MailboxSetting from '@/components/MailboxSetting'
|
||||
import NoteList from '@/components/NoteList'
|
||||
import Tabs from '@/components/Tabs'
|
||||
import {
|
||||
toAppearanceSettings,
|
||||
toEmojiPackSettings,
|
||||
toGeneralSettings,
|
||||
toPostSettings,
|
||||
toRelaySettings,
|
||||
toSystemSettings,
|
||||
toWallet
|
||||
} from '@/lib/link'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useSecondaryPage } from '@/PageManager'
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger
|
||||
} from '@/components/ui/accordion'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Tabs as RadixTabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import {
|
||||
BIG_RELAY_URLS,
|
||||
DEFAULT_FAVICON_URL_TEMPLATE,
|
||||
MEDIA_AUTO_LOAD_POLICY,
|
||||
NSFW_DISPLAY_POLICY,
|
||||
PRIMARY_COLORS,
|
||||
TPrimaryColor
|
||||
} from '@/constants'
|
||||
import { LocalizedLanguageNames, TLanguage } from '@/i18n'
|
||||
import { cn, isSupportCheckConnectionType } from '@/lib/utils'
|
||||
import MediaUploadServiceSetting from '@/pages/secondary/PostSettingsPage/MediaUploadServiceSetting'
|
||||
import DefaultZapAmountInput from '@/pages/secondary/WalletPage/DefaultZapAmountInput'
|
||||
import DefaultZapCommentInput from '@/pages/secondary/WalletPage/DefaultZapCommentInput'
|
||||
import LightningAddressInput from '@/pages/secondary/WalletPage/LightningAddressInput'
|
||||
import QuickZapSwitch from '@/pages/secondary/WalletPage/QuickZapSwitch'
|
||||
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||
import { useTheme } from '@/providers/ThemeProvider'
|
||||
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
|
||||
import { useUserTrust } from '@/providers/UserTrustProvider'
|
||||
import { useZap } from '@/providers/ZapProvider'
|
||||
import storage from '@/services/local-storage.service'
|
||||
import { TMediaAutoLoadPolicy, TNsfwDisplayPolicy } from '@/types'
|
||||
import { disconnect, launchModal } from '@getalby/bitcoin-connect-react'
|
||||
import {
|
||||
Check,
|
||||
ChevronRight,
|
||||
Cog,
|
||||
Columns2,
|
||||
Copy,
|
||||
Info,
|
||||
KeyRound,
|
||||
LayoutList,
|
||||
List,
|
||||
Monitor,
|
||||
Moon,
|
||||
Palette,
|
||||
PanelLeft,
|
||||
PencilLine,
|
||||
RotateCcw,
|
||||
Server,
|
||||
Settings2,
|
||||
Smile,
|
||||
Sun,
|
||||
Wallet
|
||||
} from 'lucide-react'
|
||||
import { forwardRef, HTMLProps, useState } from 'react'
|
||||
import { kinds } from 'nostr-tools'
|
||||
import { forwardRef, HTMLProps, useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type TEmojiTab = 'my-packs' | 'explore'
|
||||
|
||||
const THEMES = [
|
||||
{ key: 'system', label: 'System', icon: <Monitor className="size-5" /> },
|
||||
{ key: 'light', label: 'Light', icon: <Sun className="size-5" /> },
|
||||
{ key: 'dark', label: 'Dark', icon: <Moon className="size-5" /> },
|
||||
{ key: 'pure-black', label: 'Pure Black', icon: <Moon className="size-5 fill-current" /> }
|
||||
] as const
|
||||
|
||||
const LAYOUTS = [
|
||||
{ key: false, label: 'Two-column', icon: <Columns2 className="size-5" /> },
|
||||
{ key: true, label: 'Single-column', icon: <PanelLeft className="size-5" /> }
|
||||
] as const
|
||||
|
||||
const NOTIFICATION_STYLES = [
|
||||
{ key: 'detailed', label: 'Detailed', icon: <LayoutList className="size-5" /> },
|
||||
{ key: 'compact', label: 'Compact', icon: <List className="size-5" /> }
|
||||
] as const
|
||||
|
||||
export default function Settings() {
|
||||
const { t } = useTranslation()
|
||||
const { t, i18n } = useTranslation()
|
||||
const { pubkey, nsec, ncryptsec } = useNostr()
|
||||
const { push } = useSecondaryPage()
|
||||
const { isSmallScreen } = useScreenSize()
|
||||
const [copiedNsec, setCopiedNsec] = useState(false)
|
||||
const [copiedNcryptsec, setCopiedNcryptsec] = useState(false)
|
||||
const [openSection, setOpenSection] = useState<string>('')
|
||||
const accordionRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// General settings
|
||||
const [language, setLanguage] = useState<TLanguage>(i18n.language as TLanguage)
|
||||
const {
|
||||
autoplay,
|
||||
setAutoplay,
|
||||
nsfwDisplayPolicy,
|
||||
setNsfwDisplayPolicy,
|
||||
hideContentMentioningMutedUsers,
|
||||
setHideContentMentioningMutedUsers,
|
||||
mediaAutoLoadPolicy,
|
||||
setMediaAutoLoadPolicy,
|
||||
faviconUrlTemplate,
|
||||
setFaviconUrlTemplate
|
||||
} = useContentPolicy()
|
||||
const {
|
||||
hideUntrustedNotes,
|
||||
updateHideUntrustedNotes,
|
||||
hideUntrustedInteractions,
|
||||
updateHideUntrustedInteractions,
|
||||
hideUntrustedNotifications,
|
||||
updateHideUntrustedNotifications
|
||||
} = useUserTrust()
|
||||
const {
|
||||
quickReaction,
|
||||
updateQuickReaction,
|
||||
quickReactionEmoji,
|
||||
updateQuickReactionEmoji,
|
||||
enableSingleColumnLayout,
|
||||
updateEnableSingleColumnLayout,
|
||||
notificationListStyle,
|
||||
updateNotificationListStyle
|
||||
} = useUserPreferences()
|
||||
|
||||
// Appearance settings
|
||||
const { themeSetting, setThemeSetting, primaryColor, setPrimaryColor } = useTheme()
|
||||
|
||||
// Wallet settings
|
||||
const { isWalletConnected, walletInfo } = useZap()
|
||||
|
||||
// Relay settings
|
||||
const [relayTabValue, setRelayTabValue] = useState('favorite-relays')
|
||||
|
||||
// Emoji settings
|
||||
const [emojiTab, setEmojiTab] = useState<TEmojiTab>('my-packs')
|
||||
|
||||
// System settings
|
||||
const [filterOutOnionRelays, setFilterOutOnionRelays] = useState(storage.getFilterOutOnionRelays())
|
||||
|
||||
const handleLanguageChange = (value: TLanguage) => {
|
||||
i18n.changeLanguage(value)
|
||||
setLanguage(value)
|
||||
}
|
||||
|
||||
const handleAccordionChange = useCallback((value: string) => {
|
||||
setOpenSection(value)
|
||||
if (value) {
|
||||
// Scroll the opened section into view
|
||||
setTimeout(() => {
|
||||
const item = accordionRef.current?.querySelector(`[data-state="open"]`)
|
||||
if (item) {
|
||||
const rect = item.getBoundingClientRect()
|
||||
const scrollContainer = accordionRef.current?.closest('[data-radix-scroll-area-viewport]') || window
|
||||
if (scrollContainer === window) {
|
||||
const scrollTop = window.scrollY + rect.top - 16
|
||||
window.scrollTo({ top: scrollTop, behavior: 'smooth' })
|
||||
} else {
|
||||
const containerRect = (scrollContainer as HTMLElement).getBoundingClientRect()
|
||||
const scrollTop = (scrollContainer as HTMLElement).scrollTop + rect.top - containerRect.top - 16
|
||||
;(scrollContainer as HTMLElement).scrollTo({ top: scrollTop, behavior: 'smooth' })
|
||||
}
|
||||
}
|
||||
}, 50)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SettingItem className="clickable" onClick={() => push(toGeneralSettings())}>
|
||||
<div className="flex items-center gap-4">
|
||||
<Settings2 />
|
||||
<div>{t('General')}</div>
|
||||
</div>
|
||||
<ChevronRight />
|
||||
</SettingItem>
|
||||
<SettingItem className="clickable" onClick={() => push(toAppearanceSettings())}>
|
||||
<div className="flex items-center gap-4">
|
||||
<Palette />
|
||||
<div>{t('Appearance')}</div>
|
||||
</div>
|
||||
<ChevronRight />
|
||||
</SettingItem>
|
||||
<SettingItem className="clickable" onClick={() => push(toRelaySettings())}>
|
||||
<div className="flex items-center gap-4">
|
||||
<Server />
|
||||
<div>{t('Relays')}</div>
|
||||
</div>
|
||||
<ChevronRight />
|
||||
</SettingItem>
|
||||
{!!pubkey && (
|
||||
<SettingItem className="clickable" onClick={() => push(toWallet())}>
|
||||
<div className="flex items-center gap-4">
|
||||
<Wallet />
|
||||
<div>{t('Wallet')}</div>
|
||||
</div>
|
||||
<ChevronRight />
|
||||
</SettingItem>
|
||||
)}
|
||||
{!!pubkey && (
|
||||
<SettingItem className="clickable" onClick={() => push(toPostSettings())}>
|
||||
<div className="flex items-center gap-4">
|
||||
<PencilLine />
|
||||
<div>{t('Post settings')}</div>
|
||||
</div>
|
||||
<ChevronRight />
|
||||
</SettingItem>
|
||||
)}
|
||||
{!!pubkey && (
|
||||
<SettingItem className="clickable" onClick={() => push(toEmojiPackSettings())}>
|
||||
<div className="flex items-center gap-4">
|
||||
<Smile />
|
||||
<div>{t('Emoji Packs')}</div>
|
||||
</div>
|
||||
<ChevronRight />
|
||||
</SettingItem>
|
||||
)}
|
||||
<div ref={accordionRef}>
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
value={openSection}
|
||||
onValueChange={handleAccordionChange}
|
||||
className="w-full"
|
||||
>
|
||||
{/* General */}
|
||||
<AccordionItem value="general">
|
||||
<AccordionTrigger className="px-4 hover:no-underline">
|
||||
<div className="flex items-center gap-4">
|
||||
<Settings2 className="size-4" />
|
||||
<span>{t('General')}</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4 space-y-4">
|
||||
<SettingItem>
|
||||
<Label htmlFor="languages" className="text-base font-normal">
|
||||
{t('Languages')}
|
||||
</Label>
|
||||
<Select defaultValue="en" value={language} onValueChange={handleLanguageChange}>
|
||||
<SelectTrigger id="languages" className="w-48">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(LocalizedLanguageNames).map(([key, value]) => (
|
||||
<SelectItem key={key} value={key}>
|
||||
{value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SettingItem>
|
||||
<SettingItem>
|
||||
<Label htmlFor="media-auto-load-policy" className="text-base font-normal">
|
||||
{t('Auto-load media')}
|
||||
</Label>
|
||||
<Select
|
||||
defaultValue="wifi-only"
|
||||
value={mediaAutoLoadPolicy}
|
||||
onValueChange={(value: TMediaAutoLoadPolicy) => setMediaAutoLoadPolicy(value)}
|
||||
>
|
||||
<SelectTrigger id="media-auto-load-policy" className="w-48">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={MEDIA_AUTO_LOAD_POLICY.ALWAYS}>{t('Always')}</SelectItem>
|
||||
{isSupportCheckConnectionType() && (
|
||||
<SelectItem value={MEDIA_AUTO_LOAD_POLICY.WIFI_ONLY}>{t('Wi-Fi only')}</SelectItem>
|
||||
)}
|
||||
<SelectItem value={MEDIA_AUTO_LOAD_POLICY.NEVER}>{t('Never')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SettingItem>
|
||||
<SettingItem>
|
||||
<Label htmlFor="autoplay" className="text-base font-normal">
|
||||
<div>{t('Autoplay')}</div>
|
||||
<div className="text-muted-foreground">{t('Enable video autoplay on this device')}</div>
|
||||
</Label>
|
||||
<Switch id="autoplay" checked={autoplay} onCheckedChange={setAutoplay} />
|
||||
</SettingItem>
|
||||
<SettingItem>
|
||||
<Label htmlFor="hide-untrusted-notes" className="text-base font-normal">
|
||||
{t('Hide untrusted notes')}
|
||||
</Label>
|
||||
<Switch
|
||||
id="hide-untrusted-notes"
|
||||
checked={hideUntrustedNotes}
|
||||
onCheckedChange={updateHideUntrustedNotes}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem>
|
||||
<Label htmlFor="hide-untrusted-interactions" className="text-base font-normal">
|
||||
{t('Hide untrusted interactions')}
|
||||
</Label>
|
||||
<Switch
|
||||
id="hide-untrusted-interactions"
|
||||
checked={hideUntrustedInteractions}
|
||||
onCheckedChange={updateHideUntrustedInteractions}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem>
|
||||
<Label htmlFor="hide-untrusted-notifications" className="text-base font-normal">
|
||||
{t('Hide untrusted notifications')}
|
||||
</Label>
|
||||
<Switch
|
||||
id="hide-untrusted-notifications"
|
||||
checked={hideUntrustedNotifications}
|
||||
onCheckedChange={updateHideUntrustedNotifications}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem>
|
||||
<Label htmlFor="hide-content-mentioning-muted-users" className="text-base font-normal">
|
||||
{t('Hide content mentioning muted users')}
|
||||
</Label>
|
||||
<Switch
|
||||
id="hide-content-mentioning-muted-users"
|
||||
checked={hideContentMentioningMutedUsers}
|
||||
onCheckedChange={setHideContentMentioningMutedUsers}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem>
|
||||
<Label htmlFor="nsfw-display-policy" className="text-base font-normal">
|
||||
{t('NSFW content display')}
|
||||
</Label>
|
||||
<Select
|
||||
value={nsfwDisplayPolicy}
|
||||
onValueChange={(value: TNsfwDisplayPolicy) => setNsfwDisplayPolicy(value)}
|
||||
>
|
||||
<SelectTrigger id="nsfw-display-policy" className="w-48">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={NSFW_DISPLAY_POLICY.HIDE}>{t('Hide completely')}</SelectItem>
|
||||
<SelectItem value={NSFW_DISPLAY_POLICY.HIDE_CONTENT}>{t('Show but hide content')}</SelectItem>
|
||||
<SelectItem value={NSFW_DISPLAY_POLICY.SHOW}>{t('Show directly')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SettingItem>
|
||||
<SettingItem>
|
||||
<Label htmlFor="quick-reaction" className="text-base font-normal">
|
||||
<div>{t('Quick reaction')}</div>
|
||||
<div className="text-muted-foreground">
|
||||
{t('If enabled, you can react with a single click. Click and hold for more options')}
|
||||
</div>
|
||||
</Label>
|
||||
<Switch id="quick-reaction" checked={quickReaction} onCheckedChange={updateQuickReaction} />
|
||||
</SettingItem>
|
||||
{quickReaction && (
|
||||
<SettingItem>
|
||||
<Label htmlFor="quick-reaction-emoji" className="text-base font-normal">
|
||||
{t('Quick reaction emoji')}
|
||||
</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => updateQuickReactionEmoji('+')}
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<RotateCcw />
|
||||
</Button>
|
||||
<EmojiPickerDialog
|
||||
onEmojiClick={(emoji) => {
|
||||
if (!emoji) return
|
||||
updateQuickReactionEmoji(emoji)
|
||||
}}
|
||||
>
|
||||
<Button variant="ghost" size="icon" className="border">
|
||||
<Emoji emoji={quickReactionEmoji} />
|
||||
</Button>
|
||||
</EmojiPickerDialog>
|
||||
</div>
|
||||
</SettingItem>
|
||||
)}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
{/* Appearance */}
|
||||
<AccordionItem value="appearance">
|
||||
<AccordionTrigger className="px-4 hover:no-underline">
|
||||
<div className="flex items-center gap-4">
|
||||
<Palette className="size-4" />
|
||||
<span>{t('Appearance')}</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4 space-y-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-base">{t('Theme')}</Label>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 w-full">
|
||||
{THEMES.map(({ key, label, icon }) => (
|
||||
<OptionButton
|
||||
key={key}
|
||||
isSelected={themeSetting === key}
|
||||
icon={icon}
|
||||
label={t(label)}
|
||||
onClick={() => setThemeSetting(key)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{!isSmallScreen && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-base">{t('Layout')}</Label>
|
||||
<div className="grid grid-cols-2 gap-4 w-full">
|
||||
{LAYOUTS.map(({ key, label, icon }) => (
|
||||
<OptionButton
|
||||
key={key.toString()}
|
||||
isSelected={enableSingleColumnLayout === key}
|
||||
icon={icon}
|
||||
label={t(label)}
|
||||
onClick={() => updateEnableSingleColumnLayout(key)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-base">{t('Notification list style')}</Label>
|
||||
<div className="grid grid-cols-2 gap-4 w-full">
|
||||
{NOTIFICATION_STYLES.map(({ key, label, icon }) => (
|
||||
<OptionButton
|
||||
key={key}
|
||||
isSelected={notificationListStyle === key}
|
||||
icon={icon}
|
||||
label={t(label)}
|
||||
onClick={() => updateNotificationListStyle(key)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-base">{t('Primary color')}</Label>
|
||||
<div className="grid grid-cols-4 gap-4 w-full">
|
||||
{Object.entries(PRIMARY_COLORS).map(([key, config]) => (
|
||||
<OptionButton
|
||||
key={key}
|
||||
isSelected={primaryColor === key}
|
||||
icon={
|
||||
<div
|
||||
className="size-8 rounded-full shadow-md"
|
||||
style={{ backgroundColor: `hsl(${config.light.primary})` }}
|
||||
/>
|
||||
}
|
||||
label={t(config.name)}
|
||||
onClick={() => setPrimaryColor(key as TPrimaryColor)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
{/* Relays */}
|
||||
<AccordionItem value="relays">
|
||||
<AccordionTrigger className="px-4 hover:no-underline">
|
||||
<div className="flex items-center gap-4">
|
||||
<Server className="size-4" />
|
||||
<span>{t('Relays')}</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4">
|
||||
<RadixTabs value={relayTabValue} onValueChange={setRelayTabValue} className="space-y-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="favorite-relays">{t('Favorite Relays')}</TabsTrigger>
|
||||
<TabsTrigger value="mailbox">{t('Read & Write Relays')}</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="favorite-relays">
|
||||
<FavoriteRelaysSetting />
|
||||
</TabsContent>
|
||||
<TabsContent value="mailbox">
|
||||
<MailboxSetting />
|
||||
</TabsContent>
|
||||
</RadixTabs>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
{/* Wallet */}
|
||||
{!!pubkey && (
|
||||
<AccordionItem value="wallet">
|
||||
<AccordionTrigger className="px-4 hover:no-underline">
|
||||
<div className="flex items-center gap-4">
|
||||
<Wallet className="size-4" />
|
||||
<span>{t('Wallet')}</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4 space-y-4">
|
||||
{isWalletConnected ? (
|
||||
<>
|
||||
<div>
|
||||
{walletInfo?.node.alias && (
|
||||
<div className="mb-2">
|
||||
{t('Connected to')} <strong>{walletInfo.node.alias}</strong>
|
||||
</div>
|
||||
)}
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive">{t('Disconnect Wallet')}</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{t('Are you absolutely sure?')}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t('You will not be able to send zaps to others.')}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t('Cancel')}</AlertDialogCancel>
|
||||
<AlertDialogAction variant="destructive" onClick={() => disconnect()}>
|
||||
{t('Disconnect')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
<DefaultZapAmountInput />
|
||||
<DefaultZapCommentInput />
|
||||
<QuickZapSwitch />
|
||||
<LightningAddressInput />
|
||||
</>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button className="bg-foreground hover:bg-foreground/90" onClick={() => launchModal()}>
|
||||
{t('Connect Wallet')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
)}
|
||||
|
||||
{/* Post Settings */}
|
||||
{!!pubkey && (
|
||||
<AccordionItem value="posts">
|
||||
<AccordionTrigger className="px-4 hover:no-underline">
|
||||
<div className="flex items-center gap-4">
|
||||
<PencilLine className="size-4" />
|
||||
<span>{t('Post settings')}</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4">
|
||||
<MediaUploadServiceSetting />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
)}
|
||||
|
||||
{/* Emoji Packs */}
|
||||
{!!pubkey && (
|
||||
<AccordionItem value="emoji-packs">
|
||||
<AccordionTrigger className="px-4 hover:no-underline">
|
||||
<div className="flex items-center gap-4">
|
||||
<Smile className="size-4" />
|
||||
<span>{t('Emoji Packs')}</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4">
|
||||
<Tabs
|
||||
value={emojiTab}
|
||||
tabs={[
|
||||
{ value: 'my-packs', label: 'My Packs' },
|
||||
{ value: 'explore', label: 'Explore' }
|
||||
]}
|
||||
onTabChange={(tab) => setEmojiTab(tab as TEmojiTab)}
|
||||
/>
|
||||
{emojiTab === 'my-packs' ? (
|
||||
<EmojiPackList />
|
||||
) : (
|
||||
<NoteList
|
||||
showKinds={[kinds.Emojisets]}
|
||||
subRequests={[{ urls: BIG_RELAY_URLS, filter: {} }]}
|
||||
hideUntrustedNotes={hideUntrustedNotes}
|
||||
/>
|
||||
)}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
)}
|
||||
|
||||
{/* System */}
|
||||
<AccordionItem value="system">
|
||||
<AccordionTrigger className="px-4 hover:no-underline">
|
||||
<div className="flex items-center gap-4">
|
||||
<Cog className="size-4" />
|
||||
<span>{t('System')}</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4 space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="favicon-url" className="text-base font-normal">
|
||||
{t('Favicon URL')}
|
||||
</Label>
|
||||
<Input
|
||||
id="favicon-url"
|
||||
type="text"
|
||||
value={faviconUrlTemplate}
|
||||
onChange={(e) => setFaviconUrlTemplate(e.target.value)}
|
||||
placeholder={DEFAULT_FAVICON_URL_TEMPLATE}
|
||||
/>
|
||||
</div>
|
||||
<SettingItem>
|
||||
<Label htmlFor="filter-out-onion-relays" className="text-base font-normal">
|
||||
{t('Filter out onion relays')}
|
||||
</Label>
|
||||
<Switch
|
||||
id="filter-out-onion-relays"
|
||||
checked={filterOutOnionRelays}
|
||||
onCheckedChange={(checked) => {
|
||||
storage.setFilterOutOnionRelays(checked)
|
||||
setFilterOutOnionRelays(checked)
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
{/* Non-accordion items */}
|
||||
{!!nsec && (
|
||||
<SettingItem
|
||||
className="clickable"
|
||||
@@ -118,13 +613,6 @@ export default function Settings() {
|
||||
{copiedNcryptsec ? <Check /> : <Copy />}
|
||||
</SettingItem>
|
||||
)}
|
||||
<SettingItem className="clickable" onClick={() => push(toSystemSettings())}>
|
||||
<div className="flex items-center gap-4">
|
||||
<Cog />
|
||||
<div>{t('System')}</div>
|
||||
</div>
|
||||
<ChevronRight />
|
||||
</SettingItem>
|
||||
<AboutInfoDialog>
|
||||
<SettingItem className="clickable">
|
||||
<div className="flex items-center gap-4">
|
||||
@@ -135,7 +623,6 @@ export default function Settings() {
|
||||
<div className="text-muted-foreground">
|
||||
v{import.meta.env.APP_VERSION} ({import.meta.env.GIT_COMMIT})
|
||||
</div>
|
||||
<ChevronRight />
|
||||
</div>
|
||||
</SettingItem>
|
||||
</AboutInfoDialog>
|
||||
@@ -151,7 +638,7 @@ const SettingItem = forwardRef<HTMLDivElement, HTMLProps<HTMLDivElement>>(
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex justify-between select-none items-center px-4 py-2 h-[52px] rounded-lg [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
'flex justify-between select-none items-center px-4 min-h-9 [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -163,3 +650,28 @@ const SettingItem = forwardRef<HTMLDivElement, HTMLProps<HTMLDivElement>>(
|
||||
}
|
||||
)
|
||||
SettingItem.displayName = 'SettingItem'
|
||||
|
||||
const OptionButton = ({
|
||||
isSelected,
|
||||
onClick,
|
||||
icon,
|
||||
label
|
||||
}: {
|
||||
isSelected: boolean
|
||||
onClick: () => void
|
||||
icon: React.ReactNode
|
||||
label: string
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
'flex flex-col items-center gap-2 py-4 rounded-lg border-2 transition-all',
|
||||
isSelected ? 'border-primary' : 'border-border hover:border-muted-foreground/40'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-center w-8 h-8">{icon}</div>
|
||||
<span className="text-xs font-medium">{label}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
51
src/components/ui/accordion.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import * as AccordionPrimitive from '@radix-ui/react-accordion'
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const Accordion = AccordionPrimitive.Root
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ComponentRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item ref={ref} className={cn('border-b', className)} {...props} />
|
||||
))
|
||||
AccordionItem.displayName = 'AccordionItem'
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ComponentRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex flex-1 items-center justify-between py-4 font-medium transition-all [&[data-state=open]>svg]:rotate-180',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
))
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ComponentRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn('pb-4 pt-0', className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
))
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
@@ -428,6 +428,7 @@ export default {
|
||||
'Connect to your Rizful Vault': 'Connect to your Rizful Vault',
|
||||
'Paste your one-time code here': 'Paste your one-time code here',
|
||||
Connect: 'Connect',
|
||||
'Connect Wallet': 'Connect Wallet',
|
||||
'Set up your wallet to send and receive sats!': 'Set up your wallet to send and receive sats!',
|
||||
'Set up': 'Set up',
|
||||
Pinned: 'Pinned',
|
||||
|
||||
@@ -59,7 +59,19 @@ export default {
|
||||
highlight: 'hsl(var(--highlight))'
|
||||
},
|
||||
animation: {
|
||||
shimmer: 'shimmer 3s ease-in-out infinite'
|
||||
shimmer: 'shimmer 3s ease-in-out infinite',
|
||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||
'accordion-up': 'accordion-up 0.2s ease-out'
|
||||
},
|
||||
keyframes: {
|
||||
'accordion-down': {
|
||||
from: { height: '0' },
|
||||
to: { height: 'var(--radix-accordion-content-height)' }
|
||||
},
|
||||
'accordion-up': {
|
||||
from: { height: 'var(--radix-accordion-content-height)' },
|
||||
to: { height: '0' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||