Merge pull request #90 from MutinyWallet/activity-design-update

Activity design update
This commit is contained in:
Paul Miller
2023-05-11 17:57:15 -05:00
committed by GitHub
42 changed files with 706 additions and 552 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "mws", "name": "mws",
"version": "0.2.0", "version": "0.3.0",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"dev": "solid-start dev", "dev": "solid-start dev",
@@ -10,7 +10,7 @@
}, },
"type": "module", "type": "module",
"devDependencies": { "devDependencies": {
"@types/node": "^18.16.6", "@types/node": "^18.16.8",
"@typescript-eslint/eslint-plugin": "^5.59.5", "@typescript-eslint/eslint-plugin": "^5.59.5",
"@typescript-eslint/parser": "^5.59.5", "@typescript-eslint/parser": "^5.59.5",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
@@ -30,13 +30,13 @@
"workbox-window": "^6.5.4" "workbox-window": "^6.5.4"
}, },
"dependencies": { "dependencies": {
"@kobalte/core": "^0.8.2", "@kobalte/core": "^0.9.6",
"@kobalte/tailwindcss": "^0.5.0", "@kobalte/tailwindcss": "^0.5.0",
"@modular-forms/solid": "^0.13.2", "@modular-forms/solid": "^0.13.2",
"@mutinywallet/mutiny-wasm": "^0.3.0", "@mutinywallet/mutiny-wasm": "^0.3.2",
"@mutinywallet/waila-wasm": "^0.1.5", "@mutinywallet/waila-wasm": "^0.1.5",
"@solid-primitives/upload": "^0.0.111", "@solid-primitives/upload": "^0.0.111",
"@solidjs/meta": "^0.28.4", "@solidjs/meta": "^0.28.5",
"@solidjs/router": "^0.8.2", "@solidjs/router": "^0.8.2",
"@thisbeyond/solid-select": "^0.14.0", "@thisbeyond/solid-select": "^0.14.0",
"class-variance-authority": "^0.4.0", "class-variance-authority": "^0.4.0",
@@ -45,7 +45,7 @@
"solid-js": "^1.7.5", "solid-js": "^1.7.5",
"solid-qr-code": "^0.0.8", "solid-qr-code": "^0.0.8",
"solid-start": "^0.2.26", "solid-start": "^0.2.26",
"undici": "^5.22.0" "undici": "^5.22.1"
}, },
"engines": { "engines": {
"node": ">=16.8" "node": ">=16.8"

238
pnpm-lock.yaml generated
View File

@@ -2,8 +2,8 @@ lockfileVersion: '6.0'
dependencies: dependencies:
'@kobalte/core': '@kobalte/core':
specifier: ^0.8.2 specifier: ^0.9.6
version: 0.8.2(solid-js@1.7.5) version: 0.9.6(solid-js@1.7.5)
'@kobalte/tailwindcss': '@kobalte/tailwindcss':
specifier: ^0.5.0 specifier: ^0.5.0
version: 0.5.0(tailwindcss@3.3.2) version: 0.5.0(tailwindcss@3.3.2)
@@ -11,8 +11,8 @@ dependencies:
specifier: ^0.13.2 specifier: ^0.13.2
version: 0.13.2(solid-js@1.7.5) version: 0.13.2(solid-js@1.7.5)
'@mutinywallet/mutiny-wasm': '@mutinywallet/mutiny-wasm':
specifier: ^0.3.0 specifier: ^0.3.2
version: 0.3.0 version: 0.3.2
'@mutinywallet/waila-wasm': '@mutinywallet/waila-wasm':
specifier: ^0.1.5 specifier: ^0.1.5
version: 0.1.5 version: 0.1.5
@@ -20,8 +20,8 @@ dependencies:
specifier: ^0.0.111 specifier: ^0.0.111
version: 0.0.111(solid-js@1.7.5) version: 0.0.111(solid-js@1.7.5)
'@solidjs/meta': '@solidjs/meta':
specifier: ^0.28.4 specifier: ^0.28.5
version: 0.28.4(solid-js@1.7.5) version: 0.28.5(solid-js@1.7.5)
'@solidjs/router': '@solidjs/router':
specifier: ^0.8.2 specifier: ^0.8.2
version: 0.8.2(solid-js@1.7.5) version: 0.8.2(solid-js@1.7.5)
@@ -45,15 +45,15 @@ dependencies:
version: 0.0.8(qr.js@0.0.0)(solid-js@1.7.5) version: 0.0.8(qr.js@0.0.0)(solid-js@1.7.5)
solid-start: solid-start:
specifier: ^0.2.26 specifier: ^0.2.26
version: 0.2.26(@solidjs/meta@0.28.4)(@solidjs/router@0.8.2)(solid-js@1.7.5)(solid-start-node@0.2.26)(vite@4.3.5) version: 0.2.26(@solidjs/meta@0.28.5)(@solidjs/router@0.8.2)(solid-js@1.7.5)(solid-start-node@0.2.26)(vite@4.3.5)
undici: undici:
specifier: ^5.22.0 specifier: ^5.22.1
version: 5.22.0 version: 5.22.1
devDependencies: devDependencies:
'@types/node': '@types/node':
specifier: ^18.16.6 specifier: ^18.16.8
version: 18.16.6 version: 18.16.8
'@typescript-eslint/eslint-plugin': '@typescript-eslint/eslint-plugin':
specifier: ^5.59.5 specifier: ^5.59.5
version: 5.59.5(@typescript-eslint/parser@5.59.5)(eslint@8.40.0)(typescript@4.9.5) version: 5.59.5(@typescript-eslint/parser@5.59.5)(eslint@8.40.0)(typescript@4.9.5)
@@ -86,7 +86,7 @@ devDependencies:
version: 8.4.23 version: 8.4.23
solid-start-node: solid-start-node:
specifier: ^0.2.26 specifier: ^0.2.26
version: 0.2.26(solid-start@0.2.26)(undici@5.22.0)(vite@4.3.5) version: 0.2.26(solid-start@0.2.26)(undici@5.22.1)(vite@4.3.5)
tailwindcss: tailwindcss:
specifier: ^3.3.2 specifier: ^3.3.2
version: 3.3.2 version: 3.3.2
@@ -95,7 +95,7 @@ devDependencies:
version: 4.9.5 version: 4.9.5
vite: vite:
specifier: ^4.3.5 specifier: ^4.3.5
version: 4.3.5(@types/node@18.16.6) version: 4.3.5(@types/node@18.16.8)
vite-plugin-pwa: vite-plugin-pwa:
specifier: ^0.14.7 specifier: ^0.14.7
version: 0.14.7(vite@4.3.5)(workbox-build@6.5.4)(workbox-window@6.5.4) version: 0.14.7(vite@4.3.5)(workbox-build@6.5.4)(workbox-window@6.5.4)
@@ -1467,6 +1467,40 @@ packages:
'@floating-ui/core': 1.2.6 '@floating-ui/core': 1.2.6
dev: false dev: false
/@formatjs/ecma402-abstract@1.15.0:
resolution: {integrity: sha512-7bAYAv0w4AIao9DNg0avfOLTCPE9woAgs6SpXuMq11IN3A+l+cq8ghczwqSZBM11myvPSJA7vLn72q0rJ0QK6Q==}
dependencies:
'@formatjs/intl-localematcher': 0.2.32
tslib: 2.5.0
dev: false
/@formatjs/fast-memoize@2.0.1:
resolution: {integrity: sha512-M2GgV+qJn5WJQAYewz7q2Cdl6fobQa69S1AzSM2y0P68ZDbK5cWrJIcPCO395Of1ksftGZoOt4LYCO/j9BKBSA==}
dependencies:
tslib: 2.5.0
dev: false
/@formatjs/icu-messageformat-parser@2.4.0:
resolution: {integrity: sha512-6Dh5Z/gp4F/HovXXu/vmd0If5NbYLB5dZrmhWVNb+BOGOEU3wt7Z/83KY1dtd7IDhAnYHasbmKE1RbTE0J+3hw==}
dependencies:
'@formatjs/ecma402-abstract': 1.15.0
'@formatjs/icu-skeleton-parser': 1.4.0
tslib: 2.5.0
dev: false
/@formatjs/icu-skeleton-parser@1.4.0:
resolution: {integrity: sha512-Qq347VM616rVLkvN6QsKJELazRyNlbCiN47LdH0Mc5U7E2xV0vatiVhGqd3KFgbc055BvtnUXR7XX60dCGFuWg==}
dependencies:
'@formatjs/ecma402-abstract': 1.15.0
tslib: 2.5.0
dev: false
/@formatjs/intl-localematcher@0.2.32:
resolution: {integrity: sha512-k/MEBstff4sttohyEpXxCmC3MqbUn9VvHGlZ8fauLzkbwXmVrEeyzS+4uhrvAk9DWU9/7otYWxyDox4nT/KVLQ==}
dependencies:
tslib: 2.5.0
dev: false
/@hapi/hoek@9.3.0: /@hapi/hoek@9.3.0:
resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==}
@@ -1501,14 +1535,15 @@ packages:
'@swc/helpers': 0.4.14 '@swc/helpers': 0.4.14
dev: false dev: false
/@internationalized/number@3.2.0: /@internationalized/message@3.1.0:
resolution: {integrity: sha512-GUXkhXSX1Ee2RURnzl+47uvbOxnlMnvP9Er+QePTjDjOPWuunmLKlEkYkEcLiiJp7y4l9QxGDLOlVr8m69LS5w==} resolution: {integrity: sha512-Oo5m70FcBdADf7G8NkUffVSfuCdeAYVfsvNjZDi9ELpjvkc4YNJVTHt/NyTI9K7FgAVoELxiP9YmN0sJ+HNHYQ==}
dependencies: dependencies:
'@swc/helpers': 0.4.14 '@swc/helpers': 0.4.14
intl-messageformat: 10.3.5
dev: false dev: false
/@internationalized/string@3.1.0: /@internationalized/number@3.2.0:
resolution: {integrity: sha512-TJQKiyUb+wyAfKF59UNeZ/kELMnkxyecnyPCnBI1ma4NaXReJW+7Cc2mObXAqraIBJUVv7rgI46RLKrLgi35ng==} resolution: {integrity: sha512-GUXkhXSX1Ee2RURnzl+47uvbOxnlMnvP9Er+QePTjDjOPWuunmLKlEkYkEcLiiJp7y4l9QxGDLOlVr8m69LS5w==}
dependencies: dependencies:
'@swc/helpers': 0.4.14 '@swc/helpers': 0.4.14
dev: false dev: false
@@ -1547,16 +1582,16 @@ packages:
'@jridgewell/resolve-uri': 3.1.0 '@jridgewell/resolve-uri': 3.1.0
'@jridgewell/sourcemap-codec': 1.4.14 '@jridgewell/sourcemap-codec': 1.4.14
/@kobalte/core@0.8.2(solid-js@1.7.5): /@kobalte/core@0.9.6(solid-js@1.7.5):
resolution: {integrity: sha512-EoBYKpYa3+Csr5Zh7l3aY3yAg7fk1O3ZM9lGyD1mdQ1FutTuwTkyj8z1CvSSj1Klb+rBL+X1N662Occ8Bmsi2w==} resolution: {integrity: sha512-nuo3+ncZHC2Fl531DdliLE/kRcmdMf2FflSTVqM0FqqgilbzIbdJCFXJddkZj4KtML9F4rHRiPq5reSXMMrFLg==}
peerDependencies: peerDependencies:
solid-js: ^1.6.15 solid-js: ^1.7.3
dependencies: dependencies:
'@floating-ui/dom': 1.2.7 '@floating-ui/dom': 1.2.7
'@internationalized/date': 3.2.0 '@internationalized/date': 3.2.0
'@internationalized/message': 3.1.0
'@internationalized/number': 3.2.0 '@internationalized/number': 3.2.0
'@internationalized/string': 3.1.0 '@kobalte/utils': 0.7.2(solid-js@1.7.5)
'@kobalte/utils': 0.6.1(solid-js@1.7.5)
solid-js: 1.7.5 solid-js: 1.7.5
dev: false dev: false
@@ -1568,17 +1603,17 @@ packages:
tailwindcss: 3.3.2 tailwindcss: 3.3.2
dev: false dev: false
/@kobalte/utils@0.6.1(solid-js@1.7.5): /@kobalte/utils@0.7.2(solid-js@1.7.5):
resolution: {integrity: sha512-YvBqe9t9j0iYFUHfKXSMLQKM3s5+nL72RvT9b75W+IOxUpSpN4rdaI8C2j97k3LsEt7qY4ktJdt8lPM1rr8JXw==} resolution: {integrity: sha512-ZdINbHemz+jnixJ63VFi9wUEHEMAsP7iDGEADciKdSKrK4bDuccDw5th1O+5/PykfHqFwSI++JhhUpOd+iZ5jg==}
peerDependencies: peerDependencies:
solid-js: ^1.6.12 solid-js: ^1.7.3
dependencies: dependencies:
'@solid-primitives/event-listener': 2.2.11(solid-js@1.7.5) '@solid-primitives/event-listener': 2.2.11(solid-js@1.7.5)
'@solid-primitives/keyed': 1.2.0(solid-js@1.7.5) '@solid-primitives/keyed': 1.2.0(solid-js@1.7.5)
'@solid-primitives/media': 2.2.1(solid-js@1.7.5) '@solid-primitives/media': 2.2.1(solid-js@1.7.5)
'@solid-primitives/props': 3.1.5(solid-js@1.7.5) '@solid-primitives/props': 3.1.5(solid-js@1.7.5)
'@solid-primitives/refs': 1.0.3(solid-js@1.7.5) '@solid-primitives/refs': 1.0.3(solid-js@1.7.5)
'@solid-primitives/utils': 5.5.2(solid-js@1.7.5) '@solid-primitives/utils': 6.1.1(solid-js@1.7.5)
solid-js: 1.7.5 solid-js: 1.7.5
dev: false dev: false
@@ -1590,8 +1625,8 @@ packages:
solid-js: 1.7.5 solid-js: 1.7.5
dev: false dev: false
/@mutinywallet/mutiny-wasm@0.3.0: /@mutinywallet/mutiny-wasm@0.3.2:
resolution: {integrity: sha512-K+u2u/XMX1269U8af3T/ZvS+SzzrQcVYrdMi420dWCa14gke0vPWbGp+01zN7SCqBL4jp929emHTUZ4YBEpkzQ==} resolution: {integrity: sha512-m0VyEmVJ6Gl3YiTYYZLegeHFFVW21S2khtFljRyKKtcm0T8FZwJi0w2gNBaLQTakl5mpXwBgjTQwLqFnKSuhuQ==}
dev: false dev: false
/@mutinywallet/waila-wasm@0.1.5: /@mutinywallet/waila-wasm@0.1.5:
@@ -1644,7 +1679,7 @@ packages:
rollup: 2.79.1 rollup: 2.79.1
dev: true dev: true
/@rollup/plugin-commonjs@24.1.0(rollup@3.21.5): /@rollup/plugin-commonjs@24.1.0(rollup@3.21.6):
resolution: {integrity: sha512-eSL45hjhCWI0jCCXcNtLVqM5N1JlBGvlFfY0m6oOYnLCJ6N0qEXoZql4sY2MOUArzhH4SA/qBpTxvvZp2Sc+DQ==} resolution: {integrity: sha512-eSL45hjhCWI0jCCXcNtLVqM5N1JlBGvlFfY0m6oOYnLCJ6N0qEXoZql4sY2MOUArzhH4SA/qBpTxvvZp2Sc+DQ==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
peerDependencies: peerDependencies:
@@ -1653,15 +1688,15 @@ packages:
rollup: rollup:
optional: true optional: true
dependencies: dependencies:
'@rollup/pluginutils': 5.0.2(rollup@3.21.5) '@rollup/pluginutils': 5.0.2(rollup@3.21.6)
commondir: 1.0.1 commondir: 1.0.1
estree-walker: 2.0.2 estree-walker: 2.0.2
glob: 8.1.0 glob: 8.1.0
is-reference: 1.2.1 is-reference: 1.2.1
magic-string: 0.27.0 magic-string: 0.27.0
rollup: 3.21.5 rollup: 3.21.6
/@rollup/plugin-json@6.0.0(rollup@3.21.5): /@rollup/plugin-json@6.0.0(rollup@3.21.6):
resolution: {integrity: sha512-i/4C5Jrdr1XUarRhVu27EEwjt4GObltD7c+MkCIpO2QIbojw8MUs+CCTqOphQi3Qtg1FLmYt+l+6YeoIf51J7w==} resolution: {integrity: sha512-i/4C5Jrdr1XUarRhVu27EEwjt4GObltD7c+MkCIpO2QIbojw8MUs+CCTqOphQi3Qtg1FLmYt+l+6YeoIf51J7w==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
peerDependencies: peerDependencies:
@@ -1670,8 +1705,8 @@ packages:
rollup: rollup:
optional: true optional: true
dependencies: dependencies:
'@rollup/pluginutils': 5.0.2(rollup@3.21.5) '@rollup/pluginutils': 5.0.2(rollup@3.21.6)
rollup: 3.21.5 rollup: 3.21.6
/@rollup/plugin-node-resolve@11.2.1(rollup@2.79.1): /@rollup/plugin-node-resolve@11.2.1(rollup@2.79.1):
resolution: {integrity: sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==} resolution: {integrity: sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==}
@@ -1688,7 +1723,7 @@ packages:
rollup: 2.79.1 rollup: 2.79.1
dev: true dev: true
/@rollup/plugin-node-resolve@15.0.2(rollup@3.21.5): /@rollup/plugin-node-resolve@15.0.2(rollup@3.21.6):
resolution: {integrity: sha512-Y35fRGUjC3FaurG722uhUuG8YHOJRJQbI6/CkbRkdPotSpDj9NtIN85z1zrcyDcCQIW4qp5mgG72U+gJ0TAFEg==} resolution: {integrity: sha512-Y35fRGUjC3FaurG722uhUuG8YHOJRJQbI6/CkbRkdPotSpDj9NtIN85z1zrcyDcCQIW4qp5mgG72U+gJ0TAFEg==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
peerDependencies: peerDependencies:
@@ -1697,13 +1732,13 @@ packages:
rollup: rollup:
optional: true optional: true
dependencies: dependencies:
'@rollup/pluginutils': 5.0.2(rollup@3.21.5) '@rollup/pluginutils': 5.0.2(rollup@3.21.6)
'@types/resolve': 1.20.2 '@types/resolve': 1.20.2
deepmerge: 4.3.1 deepmerge: 4.3.1
is-builtin-module: 3.2.1 is-builtin-module: 3.2.1
is-module: 1.0.0 is-module: 1.0.0
resolve: 1.22.2 resolve: 1.22.2
rollup: 3.21.5 rollup: 3.21.6
/@rollup/plugin-replace@2.4.2(rollup@2.79.1): /@rollup/plugin-replace@2.4.2(rollup@2.79.1):
resolution: {integrity: sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==} resolution: {integrity: sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==}
@@ -1715,7 +1750,7 @@ packages:
rollup: 2.79.1 rollup: 2.79.1
dev: true dev: true
/@rollup/plugin-replace@5.0.2(rollup@3.21.5): /@rollup/plugin-replace@5.0.2(rollup@3.21.6):
resolution: {integrity: sha512-M9YXNekv/C/iHHK+cvORzfRYfPbq0RDD8r0G+bMiTXjNGKulPnCT9O3Ss46WfhI6ZOCgApOP7xAdmCQJ+U2LAA==} resolution: {integrity: sha512-M9YXNekv/C/iHHK+cvORzfRYfPbq0RDD8r0G+bMiTXjNGKulPnCT9O3Ss46WfhI6ZOCgApOP7xAdmCQJ+U2LAA==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
peerDependencies: peerDependencies:
@@ -1724,9 +1759,9 @@ packages:
rollup: rollup:
optional: true optional: true
dependencies: dependencies:
'@rollup/pluginutils': 5.0.2(rollup@3.21.5) '@rollup/pluginutils': 5.0.2(rollup@3.21.6)
magic-string: 0.27.0 magic-string: 0.27.0
rollup: 3.21.5 rollup: 3.21.6
dev: true dev: true
/@rollup/pluginutils@3.1.0(rollup@2.79.1): /@rollup/pluginutils@3.1.0(rollup@2.79.1):
@@ -1741,7 +1776,7 @@ packages:
rollup: 2.79.1 rollup: 2.79.1
dev: true dev: true
/@rollup/pluginutils@5.0.2(rollup@3.21.5): /@rollup/pluginutils@5.0.2(rollup@3.21.6):
resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==} resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
peerDependencies: peerDependencies:
@@ -1753,7 +1788,7 @@ packages:
'@types/estree': 1.0.1 '@types/estree': 1.0.1
estree-walker: 2.0.2 estree-walker: 2.0.2
picomatch: 2.3.1 picomatch: 2.3.1
rollup: 3.21.5 rollup: 3.21.6
/@scure/base@1.1.1: /@scure/base@1.1.1:
resolution: {integrity: sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==} resolution: {integrity: sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==}
@@ -1859,14 +1894,6 @@ packages:
solid-js: 1.7.5 solid-js: 1.7.5
dev: false dev: false
/@solid-primitives/utils@5.5.2(solid-js@1.7.5):
resolution: {integrity: sha512-L52ig3eHKU6CqbPCKJIb4lweBuINHBOERcE1duApyKozEN8+zCqEKwD1Qo9ljKeEzJTBGWClxNpwEiNTUWTGvg==}
peerDependencies:
solid-js: ^1.6.12
dependencies:
solid-js: 1.7.5
dev: false
/@solid-primitives/utils@6.1.1(solid-js@1.7.5): /@solid-primitives/utils@6.1.1(solid-js@1.7.5):
resolution: {integrity: sha512-wxxUdxja126jTROs9Ro8Z5ExbHs9rv2Tl744S3Qmzki/gTcTXW8D1TvTArQcjqkCvSw8OIQ2EO2NI8sR28Trxg==} resolution: {integrity: sha512-wxxUdxja126jTROs9Ro8Z5ExbHs9rv2Tl744S3Qmzki/gTcTXW8D1TvTArQcjqkCvSw8OIQ2EO2NI8sR28Trxg==}
peerDependencies: peerDependencies:
@@ -1875,8 +1902,8 @@ packages:
solid-js: 1.7.5 solid-js: 1.7.5
dev: false dev: false
/@solidjs/meta@0.28.4(solid-js@1.7.5): /@solidjs/meta@0.28.5(solid-js@1.7.5):
resolution: {integrity: sha512-1USElsQuGVcJnmZ6CxPfUVmKvCsVdBQoGrUyMxLtFw36Ytt90dPs/qLyXLvPR/ZPD16/qauWqg6APEkbrDOLcA==} resolution: {integrity: sha512-52luJR6hVNMA1K8Od5OD0d8WVz/svqZG4is8lrDimiUGxdia3DzuLF+pK56dnEzbNt9cA42qVFL134U9LkC9Gg==}
peerDependencies: peerDependencies:
solid-js: '>=1.4.0' solid-js: '>=1.4.0'
dependencies: dependencies:
@@ -1955,8 +1982,8 @@ packages:
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
dev: true dev: true
/@types/node@18.16.6: /@types/node@18.16.8:
resolution: {integrity: sha512-N7KINmeB8IN3vRR8dhgHEp+YpWvGFcpDoh5XZ8jB5a00AdFKCKEyyGTOPTddUf4JqU1ZKTVxkOxakDvchNVI2Q==} resolution: {integrity: sha512-p0iAXcfWCOTCBbsExHIDFCfwsqFwBTgETJveKMT+Ci3LY9YqQCI91F5S+TB20+aRCXpcWfvx5Qr5EccnwCm2NA==}
/@types/offscreencanvas@2019.7.0: /@types/offscreencanvas@2019.7.0:
resolution: {integrity: sha512-PGcyveRIpL1XIqK8eBsmRBt76eFgtzuPiSTyKHZxnGemp2yzGzWpjYKAfK3wIMiU7eH+851yEpiuP8JZerTmWg==} resolution: {integrity: sha512-PGcyveRIpL1XIqK8eBsmRBt76eFgtzuPiSTyKHZxnGemp2yzGzWpjYKAfK3wIMiU7eH+851yEpiuP8JZerTmWg==}
@@ -1965,7 +1992,7 @@ packages:
/@types/resolve@1.17.1: /@types/resolve@1.17.1:
resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==}
dependencies: dependencies:
'@types/node': 18.16.6 '@types/node': 18.16.8
dev: true dev: true
/@types/resolve@1.20.2: /@types/resolve@1.20.2:
@@ -2343,7 +2370,7 @@ packages:
hasBin: true hasBin: true
dependencies: dependencies:
caniuse-lite: 1.0.30001486 caniuse-lite: 1.0.30001486
electron-to-chromium: 1.4.387 electron-to-chromium: 1.4.392
node-releases: 2.0.10 node-releases: 2.0.10
update-browserslist-db: 1.0.11(browserslist@4.21.5) update-browserslist-db: 1.0.11(browserslist@4.21.5)
@@ -2626,8 +2653,8 @@ packages:
jake: 10.8.5 jake: 10.8.5
dev: true dev: true
/electron-to-chromium@1.4.387: /electron-to-chromium@1.4.392:
resolution: {integrity: sha512-tutLf+alr1/0YqJwKPdstVvDLmxmLb5xNyDLNS0RZmenHcEYk9qKfpKDCVZEKJ00JVbnayJm1MZAbYhYDFpcOw==} resolution: {integrity: sha512-TXQOMW9tnhIms3jGy/lJctLjICOgyueZFJ1KUtm6DTQ+QpxX3p7ZBwB6syuZ9KBuT5S4XX7bgY1ECPgfxKUdOg==}
/emoji-regex@8.0.0: /emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -3572,6 +3599,15 @@ packages:
side-channel: 1.0.4 side-channel: 1.0.4
dev: true dev: true
/intl-messageformat@10.3.5:
resolution: {integrity: sha512-6kPkftF8Jg3XJCkGKa5OD+nYQ+qcSxF4ZkuDdXZ6KGG0VXn+iblJqRFyDdm9VvKcMyC0Km2+JlVQffFM52D0YA==}
dependencies:
'@formatjs/ecma402-abstract': 1.15.0
'@formatjs/fast-memoize': 2.0.1
'@formatjs/icu-messageformat-parser': 2.4.0
tslib: 2.5.0
dev: false
/is-array-buffer@3.0.2: /is-array-buffer@3.0.2:
resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==}
dependencies: dependencies:
@@ -3767,7 +3803,7 @@ packages:
resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==}
engines: {node: '>= 10.13.0'} engines: {node: '>= 10.13.0'}
dependencies: dependencies:
'@types/node': 18.16.6 '@types/node': 18.16.8
merge-stream: 2.0.0 merge-stream: 2.0.0
supports-color: 7.2.0 supports-color: 7.2.0
dev: true dev: true
@@ -4397,10 +4433,10 @@ packages:
jest-worker: 26.6.2 jest-worker: 26.6.2
rollup: 2.79.1 rollup: 2.79.1
serialize-javascript: 4.0.0 serialize-javascript: 4.0.0
terser: 5.17.2 terser: 5.17.3
dev: true dev: true
/rollup-plugin-visualizer@5.9.0(rollup@3.21.5): /rollup-plugin-visualizer@5.9.0(rollup@3.21.6):
resolution: {integrity: sha512-bbDOv47+Bw4C/cgs0czZqfm8L82xOZssk4ayZjG40y9zbXclNk7YikrZTDao6p7+HDiGxrN0b65SgZiVm9k1Cg==} resolution: {integrity: sha512-bbDOv47+Bw4C/cgs0czZqfm8L82xOZssk4ayZjG40y9zbXclNk7YikrZTDao6p7+HDiGxrN0b65SgZiVm9k1Cg==}
engines: {node: '>=14'} engines: {node: '>=14'}
hasBin: true hasBin: true
@@ -4412,17 +4448,17 @@ packages:
dependencies: dependencies:
open: 8.4.2 open: 8.4.2
picomatch: 2.3.1 picomatch: 2.3.1
rollup: 3.21.5 rollup: 3.21.6
source-map: 0.7.4 source-map: 0.7.4
yargs: 17.7.2 yargs: 17.7.2
/rollup-route-manifest@1.0.0(rollup@3.21.5): /rollup-route-manifest@1.0.0(rollup@3.21.6):
resolution: {integrity: sha512-3CmcMmCLAzJDUXiO3z6386/Pt8/k9xTZv8gIHyXI8hYGoAInnYdOsFXiGGzQRMy6TXR1jUZme2qbdwjH2nFMjg==} resolution: {integrity: sha512-3CmcMmCLAzJDUXiO3z6386/Pt8/k9xTZv8gIHyXI8hYGoAInnYdOsFXiGGzQRMy6TXR1jUZme2qbdwjH2nFMjg==}
engines: {node: '>=8'} engines: {node: '>=8'}
peerDependencies: peerDependencies:
rollup: '>=2.0.0' rollup: '>=2.0.0'
dependencies: dependencies:
rollup: 3.21.5 rollup: 3.21.6
route-sort: 1.0.0 route-sort: 1.0.0
/rollup@2.79.1: /rollup@2.79.1:
@@ -4433,8 +4469,8 @@ packages:
fsevents: 2.3.2 fsevents: 2.3.2
dev: true dev: true
/rollup@3.21.5: /rollup@3.21.6:
resolution: {integrity: sha512-a4NTKS4u9PusbUJcfF4IMxuqjFzjm6ifj76P54a7cKnvVzJaG12BLVR+hgU2YDGHzyMMQNxLAZWuALsn8q2oQg==} resolution: {integrity: sha512-SXIICxvxQxR3D4dp/3LDHZIJPC8a4anKMHd4E3Jiz2/JnY+2bEjqrOokAauc5ShGVNFHlEFjBXAXlaxkJqIqSg==}
engines: {node: '>=14.18.0', npm: '>=8.0.0'} engines: {node: '>=14.18.0', npm: '>=8.0.0'}
hasBin: true hasBin: true
optionalDependencies: optionalDependencies:
@@ -4560,28 +4596,28 @@ packages:
'@babel/types': 7.21.5 '@babel/types': 7.21.5
solid-js: 1.7.5 solid-js: 1.7.5
/solid-start-node@0.2.26(solid-start@0.2.26)(undici@5.22.0)(vite@4.3.5): /solid-start-node@0.2.26(solid-start@0.2.26)(undici@5.22.1)(vite@4.3.5):
resolution: {integrity: sha512-8vciTGoQV+lIlCUSVHJPazlaoKDRfBowDkPeBr/EZdmtbcMOKoJYf/APPQWFspylF+nhzunMf0+zJP90VtMEYg==} resolution: {integrity: sha512-8vciTGoQV+lIlCUSVHJPazlaoKDRfBowDkPeBr/EZdmtbcMOKoJYf/APPQWFspylF+nhzunMf0+zJP90VtMEYg==}
peerDependencies: peerDependencies:
solid-start: '*' solid-start: '*'
undici: ^5.8.0 undici: ^5.8.0
vite: '*' vite: '*'
dependencies: dependencies:
'@rollup/plugin-commonjs': 24.1.0(rollup@3.21.5) '@rollup/plugin-commonjs': 24.1.0(rollup@3.21.6)
'@rollup/plugin-json': 6.0.0(rollup@3.21.5) '@rollup/plugin-json': 6.0.0(rollup@3.21.6)
'@rollup/plugin-node-resolve': 15.0.2(rollup@3.21.5) '@rollup/plugin-node-resolve': 15.0.2(rollup@3.21.6)
compression: 1.7.4 compression: 1.7.4
polka: 1.0.0-next.22 polka: 1.0.0-next.22
rollup: 3.21.5 rollup: 3.21.6
sirv: 2.0.3 sirv: 2.0.3
solid-start: 0.2.26(@solidjs/meta@0.28.4)(@solidjs/router@0.8.2)(solid-js@1.7.5)(solid-start-node@0.2.26)(vite@4.3.5) solid-start: 0.2.26(@solidjs/meta@0.28.5)(@solidjs/router@0.8.2)(solid-js@1.7.5)(solid-start-node@0.2.26)(vite@4.3.5)
terser: 5.17.2 terser: 5.17.3
undici: 5.22.0 undici: 5.22.1
vite: 4.3.5(@types/node@18.16.6) vite: 4.3.5(@types/node@18.16.8)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
/solid-start@0.2.26(@solidjs/meta@0.28.4)(@solidjs/router@0.8.2)(solid-js@1.7.5)(solid-start-node@0.2.26)(vite@4.3.5): /solid-start@0.2.26(@solidjs/meta@0.28.5)(@solidjs/router@0.8.2)(solid-js@1.7.5)(solid-start-node@0.2.26)(vite@4.3.5):
resolution: {integrity: sha512-kne2HZlnSMzsirdnvNs1CsDqBl0L0uvKKt1t4de1CH7JIngyqoMcER97jTE0Ejr84KknANaKAdvJAzZcL7Ueng==} resolution: {integrity: sha512-kne2HZlnSMzsirdnvNs1CsDqBl0L0uvKKt1t4de1CH7JIngyqoMcER97jTE0Ejr84KknANaKAdvJAzZcL7Ueng==}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@@ -4621,7 +4657,7 @@ packages:
'@babel/preset-env': 7.21.5(@babel/core@7.21.8) '@babel/preset-env': 7.21.5(@babel/core@7.21.8)
'@babel/preset-typescript': 7.21.5(@babel/core@7.21.8) '@babel/preset-typescript': 7.21.5(@babel/core@7.21.8)
'@babel/template': 7.20.7 '@babel/template': 7.20.7
'@solidjs/meta': 0.28.4(solid-js@1.7.5) '@solidjs/meta': 0.28.5(solid-js@1.7.5)
'@solidjs/router': 0.8.2(solid-js@1.7.5) '@solidjs/router': 0.8.2(solid-js@1.7.5)
'@types/cookie': 0.5.1 '@types/cookie': 0.5.1
chokidar: 3.5.3 chokidar: 3.5.3
@@ -4637,18 +4673,18 @@ packages:
get-port: 6.1.2 get-port: 6.1.2
parse-multipart-data: 1.5.0 parse-multipart-data: 1.5.0
picocolors: 1.0.0 picocolors: 1.0.0
rollup: 3.21.5 rollup: 3.21.6
rollup-plugin-visualizer: 5.9.0(rollup@3.21.5) rollup-plugin-visualizer: 5.9.0(rollup@3.21.6)
rollup-route-manifest: 1.0.0(rollup@3.21.5) rollup-route-manifest: 1.0.0(rollup@3.21.6)
sade: 1.8.1 sade: 1.8.1
set-cookie-parser: 2.6.0 set-cookie-parser: 2.6.0
sirv: 2.0.3 sirv: 2.0.3
solid-js: 1.7.5 solid-js: 1.7.5
solid-start-node: 0.2.26(solid-start@0.2.26)(undici@5.22.0)(vite@4.3.5) solid-start-node: 0.2.26(solid-start@0.2.26)(undici@5.22.1)(vite@4.3.5)
terser: 5.17.2 terser: 5.17.3
undici: 5.22.0 undici: 5.22.1
vite: 4.3.5(@types/node@18.16.6) vite: 4.3.5(@types/node@18.16.8)
vite-plugin-inspect: 0.7.26(rollup@3.21.5)(vite@4.3.5) vite-plugin-inspect: 0.7.26(rollup@3.21.6)(vite@4.3.5)
vite-plugin-solid: 2.7.0(solid-js@1.7.5)(vite@4.3.5) vite-plugin-solid: 2.7.0(solid-js@1.7.5)(vite@4.3.5)
wait-on: 6.0.1(debug@4.3.4) wait-on: 6.0.1(debug@4.3.4)
transitivePeerDependencies: transitivePeerDependencies:
@@ -4850,8 +4886,8 @@ packages:
unique-string: 2.0.0 unique-string: 2.0.0
dev: true dev: true
/terser@5.17.2: /terser@5.17.3:
resolution: {integrity: sha512-1D1aGbOF1Mnayq5PvfMc0amAR1y5Z1nrZaGCvI5xsdEfZEVte8okonk02OiaK5fw5hG1GWuuVsakOnpZW8y25A==} resolution: {integrity: sha512-AudpAZKmZHkG9jueayypz4duuCFJMMNGRMwaPvQKWfxKedh8Z2x3OCoDqIIi1xx5+iwx1u6Au8XQcc9Lke65Yg==}
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
dependencies: dependencies:
@@ -4969,8 +5005,8 @@ packages:
which-boxed-primitive: 1.0.2 which-boxed-primitive: 1.0.2
dev: true dev: true
/undici@5.22.0: /undici@5.22.1:
resolution: {integrity: sha512-fR9RXCc+6Dxav4P9VV/sp5w3eFiSdOjJYsbtWfd4s5L5C4ogyuVpdKIVHeW0vV1MloM65/f7W45nR9ZxwVdyiA==} resolution: {integrity: sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw==}
engines: {node: '>=14.0'} engines: {node: '>=14.0'}
dependencies: dependencies:
busboy: 1.6.0 busboy: 1.6.0
@@ -5044,19 +5080,19 @@ packages:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
/vite-plugin-inspect@0.7.26(rollup@3.21.5)(vite@4.3.5): /vite-plugin-inspect@0.7.26(rollup@3.21.6)(vite@4.3.5):
resolution: {integrity: sha512-gRjBay+OxLr/Dr+HXlfJVXZH0cqhE5hkkBvo2du2cA1LGUBnV8Aym89AdPrURkSpTk3Rvw9dNWM2VLIuw6RKJg==} resolution: {integrity: sha512-gRjBay+OxLr/Dr+HXlfJVXZH0cqhE5hkkBvo2du2cA1LGUBnV8Aym89AdPrURkSpTk3Rvw9dNWM2VLIuw6RKJg==}
engines: {node: '>=14'} engines: {node: '>=14'}
peerDependencies: peerDependencies:
vite: ^3.1.0 || ^4.0.0 vite: ^3.1.0 || ^4.0.0
dependencies: dependencies:
'@antfu/utils': 0.7.2 '@antfu/utils': 0.7.2
'@rollup/pluginutils': 5.0.2(rollup@3.21.5) '@rollup/pluginutils': 5.0.2(rollup@3.21.6)
debug: 4.3.4 debug: 4.3.4
fs-extra: 11.1.1 fs-extra: 11.1.1
picocolors: 1.0.0 picocolors: 1.0.0
sirv: 2.0.3 sirv: 2.0.3
vite: 4.3.5(@types/node@18.16.6) vite: 4.3.5(@types/node@18.16.8)
transitivePeerDependencies: transitivePeerDependencies:
- rollup - rollup
- supports-color - supports-color
@@ -5068,12 +5104,12 @@ packages:
workbox-build: ^6.5.4 workbox-build: ^6.5.4
workbox-window: ^6.5.4 workbox-window: ^6.5.4
dependencies: dependencies:
'@rollup/plugin-replace': 5.0.2(rollup@3.21.5) '@rollup/plugin-replace': 5.0.2(rollup@3.21.6)
debug: 4.3.4 debug: 4.3.4
fast-glob: 3.2.12 fast-glob: 3.2.12
pretty-bytes: 6.1.0 pretty-bytes: 6.1.0
rollup: 3.21.5 rollup: 3.21.6
vite: 4.3.5(@types/node@18.16.6) vite: 4.3.5(@types/node@18.16.8)
workbox-build: 6.5.4 workbox-build: 6.5.4
workbox-window: 6.5.4 workbox-window: 6.5.4
transitivePeerDependencies: transitivePeerDependencies:
@@ -5093,7 +5129,7 @@ packages:
merge-anything: 5.1.6 merge-anything: 5.1.6
solid-js: 1.7.5 solid-js: 1.7.5
solid-refresh: 0.5.2(solid-js@1.7.5) solid-refresh: 0.5.2(solid-js@1.7.5)
vite: 4.3.5(@types/node@18.16.6) vite: 4.3.5(@types/node@18.16.8)
vitefu: 0.2.4(vite@4.3.5) vitefu: 0.2.4(vite@4.3.5)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -5103,10 +5139,10 @@ packages:
peerDependencies: peerDependencies:
vite: ^2 || ^3 || ^4 vite: ^2 || ^3 || ^4
dependencies: dependencies:
vite: 4.3.5(@types/node@18.16.6) vite: 4.3.5(@types/node@18.16.8)
dev: true dev: true
/vite@4.3.5(@types/node@18.16.6): /vite@4.3.5(@types/node@18.16.8):
resolution: {integrity: sha512-0gEnL9wiRFxgz40o/i/eTBwm+NEbpUeTWhzKrZDSdKm6nplj+z4lKz8ANDgildxHm47Vg8EUia0aicKbawUVVA==} resolution: {integrity: sha512-0gEnL9wiRFxgz40o/i/eTBwm+NEbpUeTWhzKrZDSdKm6nplj+z4lKz8ANDgildxHm47Vg8EUia0aicKbawUVVA==}
engines: {node: ^14.18.0 || >=16.0.0} engines: {node: ^14.18.0 || >=16.0.0}
hasBin: true hasBin: true
@@ -5131,10 +5167,10 @@ packages:
terser: terser:
optional: true optional: true
dependencies: dependencies:
'@types/node': 18.16.6 '@types/node': 18.16.8
esbuild: 0.17.18 esbuild: 0.17.18
postcss: 8.4.23 postcss: 8.4.23
rollup: 3.21.5 rollup: 3.21.6
optionalDependencies: optionalDependencies:
fsevents: 2.3.2 fsevents: 2.3.2
@@ -5146,7 +5182,7 @@ packages:
vite: vite:
optional: true optional: true
dependencies: dependencies:
vite: 4.3.5(@types/node@18.16.6) vite: 4.3.5(@types/node@18.16.8)
/wait-on@6.0.1(debug@4.3.4): /wait-on@6.0.1(debug@4.3.4):
resolution: {integrity: sha512-zht+KASY3usTY5u2LgaNqn/Cd8MukxLGjdcZxT2ns5QzDmTFc4XoWBgC+C/na+sMRZTuVygQoMYwdcVjHnYIVw==} resolution: {integrity: sha512-zht+KASY3usTY5u2LgaNqn/Cd8MukxLGjdcZxT2ns5QzDmTFc4XoWBgC+C/na+sMRZTuVygQoMYwdcVjHnYIVw==}

View File

@@ -0,0 +1,3 @@
<svg width="14" height="16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.0778 6.68331 4.44176 15.5633c-.24.246-.638-.039-.482-.345l3.074-6.06599c.02328-.04578.03442-.09677.03235-.14809-.00207-.05132-.01728-.10125-.04418-.14501-.02689-.04375-.06457-.07987-.10943-.10489-.04485-.02502-.09538-.03811-.14674-.03801H.299757c-.059058-.00005-.116788-.01752-.165955-.05024-.0491673-.03272-.087584-.07922-.1104352-.13368-.02285107-.05446-.02912021-.11445-.01802154-.17246.01109864-.058.03907164-.11144.08041244-.15362L8.09576.0913129c.232-.2349999.618.0230001.489.3280001l-2.297 5.414997c-.01945.04591-.02715.09594-.02241.14557.00475.04963.02179.0973.04958.13869.02779.04139.06546.07521.10961.09838.04414.02318.09336.03499.14322.03436l6.29104-.078c.0593-.00095.1176.01573.1675.04794.0499.03221.0891.0785.1127.133.0235.0545.0304.11477.0197.17317-.0108.0584-.0386.11231-.0799.15489l-.001.001Z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 921 B

View File

@@ -0,0 +1,4 @@
<svg width="17" height="17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="m3.2916 7.53561-2.12 2.121C.421438 10.4068 0 11.4242 0 12.4851s.421438 2.0783 1.1716 2.8285c.75017.7502 1.76761 1.1716 2.8285 1.1716 1.0609 0 2.07834-.4214 2.8285-1.1716l2.828-2.828c.3715-.3714.6661-.8124.8671-1.2977.2011-.4853.3045-1.0055.3045-1.53079 0-.5253-.1034-1.04546-.3045-1.53078-.201-.48531-.4956-.92628-.8671-1.29772l-1.06 1.06c.23222.23216.41643.50778.54211.81114.12567.30336.19036.6285.19036.95686 0 .32836-.06469.65349-.19036.95689-.12568.3033-.30989.579-.54211.8111l-2.831 2.828c-.4715.4554-1.10301.7074-1.7585.7017-.65549-.0057-1.28252-.2686-1.74604-.7321-.46352-.4636-.72645-1.0906-.73214-1.7461-.0057-.6555.24629-1.287.70168-1.7585l2.12-2.12099-1.06-1.061h.001Z" fill="#fff"/>
<path d="m12.1304 7.8886 2.121-2.12c.4655-.46947.7261-1.10423.7248-1.76538-.0014-.66114-.2646-1.29483-.732-1.7624-.4674-.46756-1.1011-.73093-1.7622-.73247-.6612-.00154-1.296.25887-1.7656.72425l-2.82899 2.828c-.23222.23216-.41643.50779-.5421.81114-.12568.30336-.19037.6285-.19037.95686 0 .32836.06469.65351.19037.95686.12567.30336.30988.57899.5421.81114l-1.06 1.06c-.37146-.37143-.66612-.8124-.86715-1.29772-.20103-.48531-.3045-1.00547-.3045-1.53078 0-.5253.10347-1.04546.3045-1.53078.20103-.48531.49569-.92628.86715-1.29772l2.828-2.828C10.4056.421438 11.423-1e-8 12.4839 0c1.0609 1e-8 2.0783.421438 2.8285 1.1716.7502.75017 1.1716 1.76761 1.1716 2.8285 0 1.0609-.4214 2.07834-1.1716 2.8285l-2.121 2.121-1.061-1.06v-.001Z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="m9.9998 13.5998 5.9-5.9c.1833-.18333.4167-.275.7-.275.2833 0 .5167.09167.7.275.1833.18334.275.41667.275.7 0 .28334-.0917.51667-.275.7l-6.6 6.6c-.2.2-.4333.3-.7.3-.26666 0-.5-.1-.7-.3l-2.6-2.6c-.18333-.1833-.275-.4167-.275-.7 0-.2833.09167-.5167.275-.7.18334-.1833.41667-.275.7-.275.28334 0 .51667.0917.7.275l1.9 1.9Z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 425 B

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 3c-.53043 0-1.03914.21071-1.41421.58579C3.21071 3.96086 3 4.46957 3 5v14c0 .5304.21071 1.0391.58579 1.4142C3.96086 20.7893 4.46957 21 5 21h14c.5304 0 1.0391-.2107 1.4142-.5858S21 19.5304 21 19V5.5L18.5 3H17v6c0 .26522-.1054.51957-.2929.70711C16.5196 9.89464 16.2652 10 16 10H8c-.26522 0-.51957-.10536-.70711-.29289C7.10536 9.51957 7 9.26522 7 9V3H5Zm7 1v5h3V4h-3Zm-5 8h10c.2652 0 .5196.1054.7071.2929S18 12.7348 18 13v6H6v-6c0-.2652.10536-.5196.29289-.7071C6.48043 12.1054 6.73478 12 7 12Z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 601 B

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 20c-.55 0-1.021-.196-1.413-.588C4.195 19.02 3.99934 18.5493 4 18v-3h2v3h12v-3h2v3c0 .55-.196 1.021-.588 1.413-.392.392-.8627.5877-1.412.587H6Zm5-4V7.85l-2.6 2.6L7 9l5-5 5 5-1.4 1.45-2.6-2.6V16h-2Z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 308 B

View File

@@ -1,15 +1,13 @@
import send from '~/assets/icons/send.svg'; import { LoadingSpinner, NiceP, SmallAmount, SmallHeader } from './layout';
import receive from '~/assets/icons/receive.svg'; import { For, Match, ParentComponent, Show, Switch, createMemo, createResource, createSignal } from 'solid-js';
import { ButtonLink, Card, LoadingSpinner, NiceP, SmallAmount, SmallHeader, VStack } from './layout';
import { For, Match, ParentComponent, Show, Suspense, Switch, createMemo, createResource, createSignal } from 'solid-js';
import { useMegaStore } from '~/state/megaStore'; import { useMegaStore } from '~/state/megaStore';
import { MutinyInvoice } from '@mutinywallet/mutiny-wasm'; import { MutinyInvoice } from '@mutinywallet/mutiny-wasm';
import { prettyPrintTime } from '~/utils/prettyPrintTime';
import { JsonModal } from '~/components/JsonModal'; import { JsonModal } from '~/components/JsonModal';
import mempoolTxUrl from '~/utils/mempoolTxUrl'; import mempoolTxUrl from '~/utils/mempoolTxUrl';
import wave from "~/assets/wave.gif"
import utxoIcon from '~/assets/icons/coin.svg'; import utxoIcon from '~/assets/icons/coin.svg';
import { getRedshifted } from '~/utils/fakeLabels'; import { getRedshifted } from '~/utils/fakeLabels';
import { ActivityItem } from './ActivityItem';
import { MutinyTagItem } from '~/utils/tags';
export const THREE_COLUMNS = 'grid grid-cols-[auto,1fr,auto] gap-4 py-2 px-2 border-b border-neutral-800 last:border-b-0' export const THREE_COLUMNS = 'grid grid-cols-[auto,1fr,auto] gap-4 py-2 px-2 border-b border-neutral-800 last:border-b-0'
export const CENTER_COLUMN = 'min-w-0 overflow-hidden max-w-full' export const CENTER_COLUMN = 'min-w-0 overflow-hidden max-w-full'
@@ -27,7 +25,8 @@ export type OnChainTx = {
height: number height: number
time: number time: number
} }
} },
labels: string[]
} }
export type UtxoItem = { export type UtxoItem = {
@@ -38,15 +37,16 @@ export type UtxoItem = {
} }
keychain: string keychain: string
is_spent: boolean, is_spent: boolean,
redshifted?: boolean redshifted?: boolean,
} }
const SubtleText: ParentComponent = (props) => { const SubtleText: ParentComponent = (props) => {
return <h3 class='text-xs text-gray-500 uppercase'>{props.children}</h3> return <h3 class='text-xs text-gray-500 uppercase'>{props.children}</h3>
} }
function OnChainItem(props: { item: OnChainTx }) { function OnChainItem(props: { item: OnChainTx, labels: MutinyTagItem[] }) {
const isReceive = createMemo(() => props.item.received > 0); const [store, actions] = useMegaStore();
const isReceive = () => props.item.received > props.item.sent
const [open, setOpen] = createSignal(false) const [open, setOpen] = createSignal(false)
@@ -57,26 +57,21 @@ function OnChainItem(props: { item: OnChainTx }) {
Mempool Link Mempool Link
</a> </a>
</JsonModal> </JsonModal>
<div class={THREE_COLUMNS} onClick={() => setOpen(!open())}> {/* {JSON.stringify(props.labels)} */}
<div class="flex items-center"> <ActivityItem
{isReceive() ? <img src={receive} alt="receive arrow" /> : <img src={send} alt="send arrow" />} kind={"onchain"}
</div> labels={props.labels}
<div class={CENTER_COLUMN}> amount={isReceive() ? props.item.received : props.item.sent}
<h2 class={MISSING_LABEL}>Unknown</h2> date={props.item.confirmation_time?.Confirmed?.time}
{isReceive() ? <SmallAmount amount={props.item.received} /> : <SmallAmount amount={props.item.sent} />} positive={isReceive()}
</div> onClick={() => setOpen(!open())}
<div class={RIGHT_COLUMN}> />
<SmallHeader>
<span class="text-neutral-500">On-chain</span>&nbsp;{isReceive() ? <span class="text-m-green">Receive</span> : <span class="text-m-red">Send</span>}
</SmallHeader>
<SubtleText>{props.item.confirmation_time?.Confirmed ? prettyPrintTime(props.item.confirmation_time?.Confirmed?.time) : "Unconfirmed"}</SubtleText>
</div>
</div>
</> </>
) )
} }
function InvoiceItem(props: { item: MutinyInvoice }) { function InvoiceItem(props: { item: MutinyInvoice, labels: MutinyTagItem[] }) {
const [store, actions] = useMegaStore();
const isSend = createMemo(() => props.item.is_send); const isSend = createMemo(() => props.item.is_send);
const [open, setOpen] = createSignal(false) const [open, setOpen] = createSignal(false)
@@ -84,21 +79,7 @@ function InvoiceItem(props: { item: MutinyInvoice }) {
return ( return (
<> <>
<JsonModal open={open()} data={props.item} title="Lightning Transaction" setOpen={setOpen} /> <JsonModal open={open()} data={props.item} title="Lightning Transaction" setOpen={setOpen} />
<div class={THREE_COLUMNS} onClick={() => setOpen(!open())}> <ActivityItem kind={"lightning"} labels={props.labels} amount={props.item.amount_sats || 0n} date={props.item.last_updated} positive={!isSend()} onClick={() => setOpen(!open())} />
<div class="flex items-center">
{isSend() ? <img src={send} alt="send arrow" /> : <img src={receive} alt="receive arrow" />}
</div>
<div class={CENTER_COLUMN}>
<h2 class={MISSING_LABEL}>Unknown</h2>
<SmallAmount amount={props.item.amount_sats || 0} />
</div>
<div class={RIGHT_COLUMN}>
<SmallHeader>
<span class="text-neutral-500">Lightning</span>&nbsp;{!isSend() ? <span class="text-m-green">Receive</span> : <span class="text-m-red">Send</span>}
</SmallHeader>
<SubtleText>{prettyPrintTime(Number(props.item.expire))}</SubtleText>
</div>
</div >
</> </>
) )
} }
@@ -135,122 +116,45 @@ function Utxo(props: { item: UtxoItem }) {
) )
} }
export function Activity() { type ActivityItem = { type: "onchain" | "lightning", item: OnChainTx | MutinyInvoice, time: number, labels: MutinyTagItem[] }
const [state, _] = useMegaStore();
const getTransactions = async () => {
console.log("Getting onchain txs");
const txs = await state.mutiny_wallet?.list_onchain() as OnChainTx[];
return txs.reverse();
}
const getInvoices = async () => {
console.log("Getting invoices");
const invoices = await state.mutiny_wallet?.list_invoices() as MutinyInvoice[];
return invoices.filter((inv) => inv.paid).reverse();
}
const getUtXos = async () => {
console.log("Getting utxos");
const utxos = await state.mutiny_wallet?.list_utxos() as UtxoItem[];
return utxos;
}
const [transactions, { refetch: _refetchTransactions }] = createResource(getTransactions);
const [invoices, { refetch: _refetchInvoices }] = createResource(getInvoices);
const [utxos, { refetch: _refetchUtxos }] = createResource(getUtXos);
return (
<VStack>
<Suspense>
<Card title="On-chain">
<Switch>
<Match when={transactions.loading}>
<LoadingSpinner wide />
</Match>
<Match when={transactions.state === "ready" && transactions().length === 0}>
<code>No transactions (empty state)</code>
</Match>
<Match when={transactions.state === "ready" && transactions().length >= 0}>
<For each={transactions()}>
{(tx) =>
<OnChainItem item={tx} />
}
</For>
</Match>
</Switch>
</Card>
<Card title="Lightning">
<Switch>
<Match when={invoices.loading}>
<LoadingSpinner wide />
</Match>
<Match when={invoices.state === "ready" && invoices().length === 0}>
<code>No invoices (empty state)</code>
</Match>
<Match when={invoices.state === "ready" && invoices().length >= 0}>
<For each={invoices()}>
{(invoice) =>
<InvoiceItem item={invoice} />
}
</For>
</Match>
</Switch>
</Card>
<Card title="UTXOs">
<Switch>
<Match when={utxos.loading}>
<LoadingSpinner wide />
</Match>
<Match when={utxos.state === "ready" && utxos().length === 0}>
<code>No utxos (empty state)</code>
</Match>
<Match when={utxos.state === "ready" && utxos().length >= 0}>
<For each={utxos()}>
{(utxo) =>
<Utxo item={utxo} />
}
</For>
</Match>
</Switch>
<ButtonLink href="/redshift" layout="small" class="flex items-center gap-2 self-center hover:text-m-red">Redshift <img src={wave} class="h-4" alt="redshift"></img></ButtonLink>
</Card>
</Suspense>
</VStack>
)
}
type ActivityItem = { type: "onchain" | "lightning", item: OnChainTx | MutinyInvoice, time: number }
function sortByTime(a: ActivityItem, b: ActivityItem) { function sortByTime(a: ActivityItem, b: ActivityItem) {
return b.time - a.time; return b.time - a.time;
} }
export function CombinedActivity(props: { limit?: number }) { export function CombinedActivity(props: { limit?: number }) {
const [state, actions] = useMegaStore();
const [state, _] = useMegaStore();
const getAllActivity = async () => { const getAllActivity = async () => {
console.log("Getting all activity"); console.log("Getting all activity");
const txs = await state.mutiny_wallet?.list_onchain() as OnChainTx[]; const txs = await state.mutiny_wallet?.list_onchain() as OnChainTx[];
const invoices = await state.mutiny_wallet?.list_invoices() as MutinyInvoice[]; const invoices = await state.mutiny_wallet?.list_invoices() as MutinyInvoice[];
const tags = await actions.listTags();
const activity: ActivityItem[] = []; let activity: ActivityItem[] = [];
txs.forEach((tx) => { for (let i = 0; i < txs.length; i++) {
activity.push({ type: "onchain", item: tx, time: tx.confirmation_time?.Confirmed?.time || Date.now() }) activity.push({ type: "onchain", item: txs[i], time: txs[i].confirmation_time?.Confirmed?.time || Date.now(), labels: [] })
}) }
invoices.forEach((invoice) => { for (let i = 0; i < invoices.length; i++) {
activity.push({ type: "lightning", item: invoice, time: Number(invoice.expire) }) if (invoices[i].paid) {
}) activity.push({ type: "lightning", item: invoices[i], time: Number(invoices[i].expire), labels: [] })
}
}
if (props.limit) { if (props.limit) {
return activity.sort(sortByTime).slice(0, props.limit); activity = activity.sort(sortByTime).slice(0, props.limit);
} else { } else {
return activity.sort(sortByTime); activity.sort(sortByTime);
} }
for (let i = 0; i < activity.length; i++) {
// filter the tags to only include the ones that have an id matching one of the labels
activity[i].labels = tags.filter((tag) => activity[i].item.labels.includes(tag.id));
}
return activity;
} }
const [activity] = createResource(getAllActivity); const [activity] = createResource(getAllActivity);
@@ -268,10 +172,12 @@ export function CombinedActivity(props: { limit?: number }) {
{(activityItem) => {(activityItem) =>
<Switch> <Switch>
<Match when={activityItem.type === "onchain"}> <Match when={activityItem.type === "onchain"}>
<OnChainItem item={activityItem.item as OnChainTx} /> {/* FIXME */}
<OnChainItem item={activityItem.item as OnChainTx} labels={activityItem.labels} />
</Match> </Match>
<Match when={activityItem.type === "lightning"}> <Match when={activityItem.type === "lightning"}>
<InvoiceItem item={activityItem.item as MutinyInvoice} /> {/* FIXME */}
<InvoiceItem item={activityItem.item as MutinyInvoice} labels={activityItem.labels} />
</Match> </Match>
</Switch> </Switch>
} }

View File

@@ -0,0 +1,100 @@
import { ParentComponent, createMemo, createResource } from "solid-js";
import { InlineAmount } from "./AmountCard";
import { satsToUsd } from "~/utils/conversions";
import bolt from "~/assets/icons/bolt.svg"
import chain from "~/assets/icons/chain.svg"
import { timeAgo } from "~/utils/prettyPrintTime";
import { MutinyTagItem } from "~/utils/tags";
import { generateGradient } from "~/utils/gradientHash";
export const ActivityAmount: ParentComponent<{ amount: string, price: number, positive?: boolean }> = (props) => {
const amountInUsd = createMemo(() => {
const parsed = Number(props.amount);
if (isNaN(parsed)) {
return props.amount;
} else {
return satsToUsd(props.price, parsed, true);
}
})
const prettyPrint = createMemo(() => {
const parsed = Number(props.amount);
if (isNaN(parsed)) {
return props.amount;
} else {
return parsed.toLocaleString();
}
})
return (
<div class="flex flex-col items-end">
<div class="text-base"
classList={{ "text-m-green": props.positive }}
>{props.positive && "+ "}{prettyPrint()}&nbsp;<span class="text-sm">SATS</span>
</div>
<div class="text-sm text-neutral-500">&#8776;&nbsp;{amountInUsd()}&nbsp;<span class="text-sm">USD</span></div>
</div>
)
}
function LabelCircle(props: { name?: string, contact: boolean }) {
// TODO: don't need to run this if it's not a contact
const [gradient] = createResource(props.name, async (name: string) => {
return generateGradient(name || "?")
})
const text = () => (props.contact && props.name && props.name.length) ? props.name[0] : (props.name && props.name.length) ? "≡" : "?"
const bg = () => (props.name && props.contact) ? gradient() : "gray"
return (
<div class="flex-none h-[3rem] w-[3rem] rounded-full flex items-center justify-center text-3xl uppercase border-t border-b border-t-white/50 border-b-white/10"
style={{ background: bg() }}
>
{text()}
</div>
)
}
// function that takes a list of MutinyTagItems and returns bool if one of those items is of kind Contact
function includesContact(labels: MutinyTagItem[]) {
return labels.some((label) => label.kind === "Contact")
}
// sort the labels so that the contact is always first
function sortLabels(labels: MutinyTagItem[]) {
const contact = labels.find(label => label.kind === "Contact");
return contact ? [contact, ...labels.filter(label => label !== contact)] : labels;
}
// return a string of each label name separated by a comma and a space. if the array is empty return "Unknown"
function labelString(labels: MutinyTagItem[]) {
return labels.length ? labels.map(label => label.name).join(", ") : "Unknown"
}
export function ActivityItem(props: { kind: "lightning" | "onchain", labels: MutinyTagItem[], amount: number | bigint, date?: number | bigint, positive?: boolean, onClick?: () => void }) {
const labels = () => sortLabels(props.labels)
return (
<div
onClick={() => props.onClick && props.onClick()}
class="grid grid-cols-[auto_minmax(0,_1fr)_minmax(0,_max-content)] pb-4 gap-4 border-b border-neutral-800 last:border-b-0"
classList={{ "cursor-pointer": !!props.onClick }}
>
<div class="flex gap-2 md:gap-4 items-center">
<div class="">
{props.kind === "lightning" ? <img class="w-[1rem]" src={bolt} alt="lightning" /> : <img class="w-[1rem]" src={chain} alt="onchain" />}
</div>
<div class="">
<LabelCircle name={labels().length ? labels()[0].name : ""} contact={includesContact(labels())} />
</div>
</div>
<div class="flex flex-col">
<span class="text-base font-semibold truncate" classList={{ "text-neutral-500": labels().length === 0 }}>{labelString(labels())}</span>
<time class="text-sm text-neutral-500">{timeAgo(props.date)}</time>
</div>
<div class="">
<ActivityAmount amount={props.amount.toString()} price={30000} positive={props.positive} />
</div>
</div>
)
}

View File

@@ -135,7 +135,7 @@ export const AmountEditable: ParentComponent<{ initialAmountSats: string, initia
const DIALOG_CONTENT = "h-full safe-bottom flex flex-col justify-between p-4 backdrop-blur-xl bg-neutral-800/70" const DIALOG_CONTENT = "h-full safe-bottom flex flex-col justify-between p-4 backdrop-blur-xl bg-neutral-800/70"
return ( return (
<Dialog.Root isOpen={isOpen()}> <Dialog.Root open={isOpen()}>
<button onClick={() => setIsOpen(true)} class="px-4 py-2 rounded-xl border-2 border-m-blue flex gap-2 items-center"> <button onClick={() => setIsOpen(true)} class="px-4 py-2 rounded-xl border-2 border-m-blue flex gap-2 items-center">
{/* <Amount amountSats={Number(displayAmount())} showFiat /><span>&#x270F;&#xFE0F;</span> */} {/* <Amount amountSats={Number(displayAmount())} showFiat /><span>&#x270F;&#xFE0F;</span> */}
<Show when={displayAmount() !== "0"} fallback={<div class="inline-block font-semibold">Set amount</div>}> <Show when={displayAmount() !== "0"} fallback={<div class="inline-block font-semibold">Set amount</div>}>

View File

@@ -1,35 +1,42 @@
import logo from '~/assets/icons/mutiny-logo.svg'; import logo from '~/assets/icons/mutiny-logo.svg';
import { DefaultMain, MutinyWalletGuard, SafeArea, VStack, Card } from "~/components/layout"; import { DefaultMain, SafeArea, VStack, Card, LoadingSpinner } from "~/components/layout";
import BalanceBox from "~/components/BalanceBox"; import BalanceBox, { LoadingShimmer } from "~/components/BalanceBox";
import NavBar from "~/components/NavBar"; import NavBar from "~/components/NavBar";
import ReloadPrompt from "~/components/Reload"; import ReloadPrompt from "~/components/Reload";
import { A } from 'solid-start'; import { A } from 'solid-start';
import { OnboardWarning } from '~/components/OnboardWarning'; import { OnboardWarning } from '~/components/OnboardWarning';
import { CombinedActivity } from './Activity'; import { CombinedActivity } from './Activity';
import userClock from '~/assets/icons/user-clock.svg'; import userClock from '~/assets/icons/user-clock.svg';
import { useMegaStore } from '~/state/megaStore';
import { Show } from 'solid-js';
export default function App() { export default function App() {
const [state, _actions] = useMegaStore();
return ( return (
<MutinyWalletGuard>
<SafeArea> <SafeArea>
<DefaultMain> <DefaultMain>
<header class="w-full flex justify-between items-center mt-4 mb-2"> <header class="w-full flex justify-between items-center mt-4 mb-2">
<img src={logo} class="h-10" alt="logo" /> <img src={logo} class="h-10" alt="logo" />
<A class="md:hidden p-2 hover:bg-white/5 rounded-lg active:bg-m-blue" href="/activity"><img src={userClock} alt="Activity" /></A> <A class="md:hidden p-2 hover:bg-white/5 rounded-lg active:bg-m-blue" href="/activity"><img src={userClock} alt="Activity" /></A>
</header> </header>
<Show when={!state.wallet_loading}>
<OnboardWarning /> <OnboardWarning />
<ReloadPrompt /> <ReloadPrompt />
<BalanceBox /> </Show>
<BalanceBox loading={state.wallet_loading} />
<Card title="Activity"> <Card title="Activity">
<div class="p-1" />
<VStack> <VStack>
<Show when={!state.wallet_loading} fallback={<LoadingShimmer />}>
<CombinedActivity limit={3} /> <CombinedActivity limit={3} />
</Show>
{/* <ButtonLink href="/activity">View All</ButtonLink> */} {/* <ButtonLink href="/activity">View All</ButtonLink> */}
<A href="/activity" class="text-m-red active:text-m-red/80 text-xl font-semibold no-underline self-center">View All</A>
</VStack> </VStack>
<A href="/activity" class="text-m-red active:text-m-red/80 text-xl font-semibold no-underline self-center">View All</A>
</Card> </Card>
</DefaultMain> </DefaultMain>
<NavBar activeTab="home" /> <NavBar activeTab="home" />
</SafeArea> </SafeArea>
</MutinyWalletGuard>
); );
} }

View File

@@ -1,7 +1,8 @@
import { Show, Suspense } from "solid-js"; import { Show, Suspense } from "solid-js";
import { ButtonLink, FancyCard, Indicator } from "~/components/layout"; import { Button, ButtonLink, FancyCard, Indicator } from "~/components/layout";
import { useMegaStore } from "~/state/megaStore"; import { useMegaStore } from "~/state/megaStore";
import { Amount } from "./Amount"; import { Amount } from "./Amount";
import { useNavigate } from "solid-start";
function prettyPrintAmount(n?: number | bigint): string { function prettyPrintAmount(n?: number | bigint): string {
if (!n || n.valueOf() === 0) { if (!n || n.valueOf() === 0) {
@@ -10,19 +11,38 @@ function prettyPrintAmount(n?: number | bigint): string {
return n.toLocaleString() return n.toLocaleString()
} }
export default function BalanceBox() { export function LoadingShimmer() {
return (<div class="flex flex-col gap-2 animate-pulse">
<h1 class="text-4xl font-light">
<div class="w-[12rem] rounded bg-neutral-700 h-[2.5rem]"></div>
</h1>
<h2 class="text-xl font-light text-white/70" >
<div class="w-[8rem] rounded bg-neutral-700 h-[1.75rem]"></div>
</h2>
</div>)
}
export default function BalanceBox(props: { loading?: boolean }) {
const [state, actions] = useMegaStore(); const [state, actions] = useMegaStore();
const emptyBalance = () => (state.balance?.confirmed || 0n) === 0n && (state.balance?.lightning || 0n) === 0n
const navigate = useNavigate()
return ( return (
<> <>
<FancyCard title="Lightning"> <FancyCard title="Lightning">
<Show when={!props.loading} fallback={<LoadingShimmer />}>
<Amount amountSats={state.balance?.lightning || 0} showFiat /> <Amount amountSats={state.balance?.lightning || 0} showFiat />
</Show>
</FancyCard> </FancyCard>
<FancyCard title="On-Chain" tag={state.is_syncing && <Indicator>Syncing</Indicator>}> <FancyCard title="On-Chain" tag={state.is_syncing && <Indicator>Syncing</Indicator>}>
<Show when={!props.loading} fallback={<LoadingShimmer />}>
<div onClick={actions.sync}> <div onClick={actions.sync}>
<Amount amountSats={state.balance?.confirmed} showFiat /> <Amount amountSats={state.balance?.confirmed} showFiat />
</div> </div>
</Show>
<Suspense> <Suspense>
<Show when={state.balance?.unconfirmed}> <Show when={state.balance?.unconfirmed}>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
@@ -37,8 +57,8 @@ export default function BalanceBox() {
</Suspense> </Suspense>
</FancyCard> </FancyCard>
<div class="flex gap-2 py-4"> <div class="flex gap-2 py-4">
<ButtonLink href="/send" intent="green">Send</ButtonLink> <Button onClick={() => navigate("/send")} disabled={emptyBalance() || props.loading} intent="green">Send</Button>
<ButtonLink href="/receive" intent="blue">Receive</ButtonLink> <Button onClick={() => navigate("/receive")} disabled={props.loading} intent="blue">Receive</Button>
</div> </div>
</> </>
) )

View File

@@ -1,59 +0,0 @@
import { RadioGroup as Kobalte } from '@kobalte/core';
import { type JSX, Show, splitProps, For } from 'solid-js';
type RadioGroupProps = {
name: string;
label?: string | undefined;
options: { label: string; value: string }[];
value: string | undefined;
error: string;
required?: boolean | undefined;
disabled?: boolean | undefined;
ref: (element: HTMLInputElement | HTMLTextAreaElement) => void;
onInput: JSX.EventHandler<HTMLInputElement | HTMLTextAreaElement, InputEvent>;
onChange: JSX.EventHandler<HTMLInputElement | HTMLTextAreaElement, Event>;
onBlur: JSX.EventHandler<HTMLInputElement | HTMLTextAreaElement, FocusEvent>;
};
type Color = "blue" | "green" | "red" | "gray"
export const colorVariants = {
blue: "bg-m-blue",
green: "bg-m-green",
red: "bg-m-red",
gray: "bg-[#898989]",
}
export function ColorRadioGroup(props: RadioGroupProps) {
const [rootProps, inputProps] = splitProps(
props,
['name', 'value', 'required', 'disabled'],
['ref', 'onInput', 'onChange', 'onBlur']
);
return (
<Kobalte.Root
{...rootProps}
validationState={props.error ? 'invalid' : 'valid'}
class="flex flex-col gap-2"
>
<Show when={props.label}>
<Kobalte.Label class="text-sm uppercase font-semibold">
{props.label}
</Kobalte.Label>
</Show>
<div class="flex gap-2">
<For each={props.options}>
{(option) => (
<Kobalte.Item value={option.value} class="ui-checked:bg-neutral-950 rounded outline outline-black/50 ui-checked:outline-white ui-checked:outline-2">
<Kobalte.ItemInput {...inputProps} />
<Kobalte.ItemControl class={`${colorVariants[option.value as Color]} w-8 h-8 rounded`}>
<Kobalte.ItemIndicator />
</Kobalte.ItemControl>
{/* <Kobalte.ItemLabel>{option.label}</Kobalte.ItemLabel> */}
</Kobalte.Item>
)}
</For>
</div>
<Kobalte.ErrorMessage>{props.error}</Kobalte.ErrorMessage>
</Kobalte.Root>
);
}

View File

@@ -1,24 +1,17 @@
import { Match, Switch, createSignal, createUniqueId } from 'solid-js'; import { Match, Switch, createSignal } from 'solid-js';
import { SmallHeader, TinyButton } from '~/components/layout'; import { SmallHeader, TinyButton } from '~/components/layout';
import { Dialog } from '@kobalte/core'; import { Dialog } from '@kobalte/core';
import close from "~/assets/icons/close.svg"; import close from "~/assets/icons/close.svg";
import { SubmitHandler } from '@modular-forms/solid'; import { SubmitHandler } from '@modular-forms/solid';
import { ContactItem } from '~/state/contacts';
import { ContactForm } from './ContactForm'; import { ContactForm } from './ContactForm';
import { ContactFormValues } from './ContactViewer';
const INITIAL: ContactItem = { id: createUniqueId(), kind: "contact", name: "", color: "gray" } export function ContactEditor(props: { createContact: (contact: ContactFormValues) => void, list?: boolean }) {
export function ContactEditor(props: { createContact: (contact: ContactItem) => void, list?: boolean }) {
const [isOpen, setIsOpen] = createSignal(false); const [isOpen, setIsOpen] = createSignal(false);
// What we're all here for in the first place: returning a value // What we're all here for in the first place: returning a value
const handleSubmit: SubmitHandler<ContactItem> = (c: ContactItem) => { const handleSubmit: SubmitHandler<ContactFormValues> = (c: ContactFormValues) => {
// TODO: why do the id and color disappear? props.createContact(c)
const odd = { id: createUniqueId(), kind: "contact" }
props.createContact({ ...odd, ...c })
setIsOpen(false); setIsOpen(false);
} }
@@ -26,7 +19,7 @@ export function ContactEditor(props: { createContact: (contact: ContactItem) =>
const DIALOG_CONTENT = "h-full safe-bottom flex flex-col justify-between p-4 backdrop-blur-xl bg-neutral-800/70" const DIALOG_CONTENT = "h-full safe-bottom flex flex-col justify-between p-4 backdrop-blur-xl bg-neutral-800/70"
return ( return (
<Dialog.Root isOpen={isOpen()}> <Dialog.Root open={isOpen()}>
<Switch> <Switch>
<Match when={props.list}> <Match when={props.list}>
<button onClick={() => setIsOpen(true)} class="flex flex-col items-center gap-2"> <button onClick={() => setIsOpen(true)} class="flex flex-col items-center gap-2">
@@ -50,7 +43,7 @@ export function ContactEditor(props: { createContact: (contact: ContactItem) =>
<img src={close} alt="Close" /> <img src={close} alt="Close" />
</button> </button>
</div> </div>
<ContactForm title="New contact" cta="Create contact" handleSubmit={handleSubmit} initialValues={INITIAL} /> <ContactForm title="New contact" cta="Create contact" handleSubmit={handleSubmit} />
</Dialog.Content> </Dialog.Content>
</div> </div>
</Dialog.Portal> </Dialog.Portal>

View File

@@ -1,13 +1,10 @@
import { SubmitHandler, createForm, required } from "@modular-forms/solid"; import { SubmitHandler, createForm, required } from "@modular-forms/solid";
import { ContactItem } from "~/state/contacts";
import { Button, LargeHeader, VStack } from "~/components/layout"; import { Button, LargeHeader, VStack } from "~/components/layout";
import { TextField } from "~/components/layout/TextField"; import { TextField } from "~/components/layout/TextField";
import { ColorRadioGroup } from "~/components/ColorRadioGroup"; import { ContactFormValues } from "./ContactViewer";
const colorOptions = [{ label: "blue", value: "blue" }, { label: "green", value: "green" }, { label: "red", value: "red" }, { label: "gray", value: "gray" }] export function ContactForm(props: { handleSubmit: SubmitHandler<ContactFormValues>, initialValues?: ContactFormValues, title: string, cta: string }) {
const [_contactForm, { Form, Field }] = createForm<ContactFormValues>({ initialValues: props.initialValues });
export function ContactForm(props: { handleSubmit: SubmitHandler<ContactItem>, initialValues?: ContactItem, title: string, cta: string }) {
const [_contactForm, { Form, Field }] = createForm<ContactItem>({ initialValues: props.initialValues });
return ( return (
<Form onSubmit={props.handleSubmit} class="flex flex-col flex-1 justify-around gap-4 max-w-[400px] mx-auto w-full"> <Form onSubmit={props.handleSubmit} class="flex flex-col flex-1 justify-around gap-4 max-w-[400px] mx-auto w-full">
@@ -19,16 +16,11 @@ export function ContactForm(props: { handleSubmit: SubmitHandler<ContactItem>, i
<TextField {...props} placeholder='Satoshi' value={field.value} error={field.error} label="Name" /> <TextField {...props} placeholder='Satoshi' value={field.value} error={field.error} label="Name" />
)} )}
</Field> </Field>
<Field name="npub" validate={[]}> {/* <Field name="npub" validate={[]}>
{(field, props) => ( {(field, props) => (
<TextField {...props} placeholder='npub...' value={field.value} error={field.error} label="Nostr npub or NIP-05 (optional)" /> <TextField {...props} placeholder='npub...' value={field.value} error={field.error} label="Nostr npub or NIP-05 (optional)" />
)} )}
</Field> </Field> */}
<Field name="color">
{(field, props) => (
<ColorRadioGroup options={colorOptions} {...props} value={field.value} error={field.error} label="Color" />
)}
</Field>
</VStack> </VStack>
</div> </div>
<Button type="submit" intent="blue" class="w-full flex-none"> <Button type="submit" intent="blue" class="w-full flex-none">

View File

@@ -3,16 +3,24 @@ import { Button, Card, NiceP, SmallHeader } from '~/components/layout';
import { Dialog } from '@kobalte/core'; import { Dialog } from '@kobalte/core';
import close from "~/assets/icons/close.svg"; import close from "~/assets/icons/close.svg";
import { SubmitHandler } from '@modular-forms/solid'; import { SubmitHandler } from '@modular-forms/solid';
import { ContactItem } from '~/state/contacts';
import { ContactForm } from './ContactForm'; import { ContactForm } from './ContactForm';
import { showToast } from './Toaster'; import { showToast } from './Toaster';
import { Contact } from '@mutinywallet/mutiny-wasm';
export function ContactViewer(props: { contact: ContactItem, gradient: string, saveContact: (contact: ContactItem) => void }) { export type ContactFormValues = {
name: string,
npub?: string,
}
export function ContactViewer(props: { contact: Contact, gradient: string, saveContact: (contact: Contact) => void }) {
const [isOpen, setIsOpen] = createSignal(false); const [isOpen, setIsOpen] = createSignal(false);
const [isEditing, setIsEditing] = createSignal(false); const [isEditing, setIsEditing] = createSignal(false);
const handleSubmit: SubmitHandler<ContactItem> = (c: ContactItem) => { const handleSubmit: SubmitHandler<ContactFormValues> = (c: ContactFormValues) => {
props.saveContact({ ...props.contact, ...c }) // FIXME: merge with existing contact if saving (need edit contact method)
// FIXME: npub not valid? other undefineds
const contact = new Contact(c.name, undefined, undefined, undefined)
props.saveContact(contact)
setIsEditing(false) setIsEditing(false)
} }
@@ -20,7 +28,7 @@ export function ContactViewer(props: { contact: ContactItem, gradient: string, s
const DIALOG_CONTENT = "h-full safe-bottom flex flex-col justify-between p-4 backdrop-blur-xl bg-neutral-800/70" const DIALOG_CONTENT = "h-full safe-bottom flex flex-col justify-between p-4 backdrop-blur-xl bg-neutral-800/70"
return ( return (
<Dialog.Root isOpen={isOpen()}> <Dialog.Root open={isOpen()}>
<button onClick={() => setIsOpen(true)} class="flex flex-col items-center gap-2 w-16 flex-shrink-0 overflow-x-hidden"> <button onClick={() => setIsOpen(true)} class="flex flex-col items-center gap-2 w-16 flex-shrink-0 overflow-x-hidden">
<div class="flex-none h-16 w-16 rounded-full flex items-center justify-center text-4xl uppercase border-t border-b border-t-white/50 border-b-white/10" <div class="flex-none h-16 w-16 rounded-full flex items-center justify-center text-4xl uppercase border-t border-b border-t-white/50 border-b-white/10"
style={{ background: props.gradient }} style={{ background: props.gradient }}

View File

@@ -53,7 +53,7 @@ export function DeleteEverything() {
return ( return (
<> <>
<Button onClick={confirmReset}>Delete Everything</Button> <Button onClick={confirmReset}>Delete Everything</Button>
<ConfirmDialog loading={confirmLoading()} isOpen={confirmOpen()} onConfirm={resetNode} onCancel={() => setConfirmOpen(false)}> <ConfirmDialog loading={confirmLoading()} open={confirmOpen()} onConfirm={resetNode} onCancel={() => setConfirmOpen(false)}>
This will delete your node's state. This can't be undone! This will delete your node's state. This can't be undone!
</ConfirmDialog> </ConfirmDialog>
</> </>

View File

@@ -7,9 +7,9 @@ const DIALOG_POSITIONER = "fixed inset-0 z-50 flex items-center justify-center"
const DIALOG_CONTENT = "w-[80vw] max-w-[400px] p-4 bg-gray/50 backdrop-blur-md shadow-xl rounded-xl border border-white/10" const DIALOG_CONTENT = "w-[80vw] max-w-[400px] p-4 bg-gray/50 backdrop-blur-md shadow-xl rounded-xl border border-white/10"
// TODO: implement this like toast so it's just one global confirm and I can call it with `confirm({ title: "Are you sure?", description: "This will delete your node" })` // TODO: implement this like toast so it's just one global confirm and I can call it with `confirm({ title: "Are you sure?", description: "This will delete your node" })`
export const ConfirmDialog: ParentComponent<{ isOpen: boolean; loading: boolean; onCancel: () => void, onConfirm: () => void }> = (props) => { export const ConfirmDialog: ParentComponent<{ open: boolean; loading: boolean; onCancel: () => void, onConfirm: () => void }> = (props) => {
return ( return (
<Dialog.Root isOpen={props.isOpen} onOpenChange={props.onCancel}> <Dialog.Root open={props.open} onOpenChange={props.onCancel}>
<Dialog.Portal> <Dialog.Portal>
<Dialog.Overlay class={OVERLAY} /> <Dialog.Overlay class={OVERLAY} />
<div class={DIALOG_POSITIONER}> <div class={DIALOG_POSITIONER}>

View File

@@ -67,7 +67,7 @@ export function ImportExport() {
<Button onClick={uploadFile}>Upload Saved State</Button> <Button onClick={uploadFile}>Upload Saved State</Button>
</VStack> </VStack>
</InnerCard> </InnerCard>
<ConfirmDialog loading={confirmLoading()} isOpen={confirmOpen()} onConfirm={importJson} onCancel={() => setConfirmOpen(false)}> <ConfirmDialog loading={confirmLoading()} open={confirmOpen()} onConfirm={importJson} onCancel={() => setConfirmOpen(false)}>
Do you want to replace your state with {files()[0].name}? Do you want to replace your state with {files()[0].name}?
</ConfirmDialog> </ConfirmDialog>
</> </>

View File

@@ -13,7 +13,7 @@ export function JsonModal(props: { title: string, open: boolean, data?: unknown,
const [copy, copied] = useCopy({ copiedTimeout: 1000 }); const [copy, copied] = useCopy({ copiedTimeout: 1000 });
return ( return (
<Dialog.Root isOpen={props.open} onOpenChange={(isOpen) => props.setOpen(isOpen)}> <Dialog.Root open={props.open} onOpenChange={(isOpen) => props.setOpen(isOpen)}>
<Dialog.Portal> <Dialog.Portal>
<Dialog.Overlay class={OVERLAY} /> <Dialog.Overlay class={OVERLAY} />
<div class={DIALOG_POSITIONER}> <div class={DIALOG_POSITIONER}>

View File

@@ -110,7 +110,7 @@ function ConnectPeer(props: { refetchPeers: RefetchPeersType }) {
<form class="flex flex-col gap-4" onSubmit={onSubmit} > <form class="flex flex-col gap-4" onSubmit={onSubmit} >
<TextField.Root <TextField.Root
value={value()} value={value()}
onValueChange={setValue} onChange={setValue}
validationState={(value() == "" || value().startsWith("mutiny:")) ? "valid" : "invalid"} validationState={(value() == "" || value().startsWith("mutiny:")) ? "valid" : "invalid"}
class="flex flex-col gap-4" class="flex flex-col gap-4"
> >
@@ -167,7 +167,7 @@ function ChannelItem(props: { channel: MutinyChannel, network?: string }) {
<Button intent="glowy" layout="xs" onClick={handleCloseChannel}>Close Channel</Button> <Button intent="glowy" layout="xs" onClick={handleCloseChannel}>Close Channel</Button>
</VStack> </VStack>
<ConfirmDialog isOpen={confirmOpen()} onConfirm={confirmCloseChannel} onCancel={() => setConfirmOpen(false)} loading={confirmLoading()}> <ConfirmDialog open={confirmOpen()} onConfirm={confirmCloseChannel} onCancel={() => setConfirmOpen(false)} loading={confirmLoading()}>
<p>Are you sure you want to close this channel?</p> <p>Are you sure you want to close this channel?</p>
</ConfirmDialog> </ConfirmDialog>
</Collapsible.Content> </Collapsible.Content>
@@ -259,7 +259,7 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
<form class="flex flex-col gap-4" onSubmit={onSubmit} > <form class="flex flex-col gap-4" onSubmit={onSubmit} >
<TextField.Root <TextField.Root
value={peerPubkey()} value={peerPubkey()}
onValueChange={setPeerPubkey} onChange={setPeerPubkey}
class="flex flex-col gap-2" class="flex flex-col gap-2"
> >
<TextField.Label class="text-sm font-semibold uppercase" >Pubkey</TextField.Label> <TextField.Label class="text-sm font-semibold uppercase" >Pubkey</TextField.Label>
@@ -267,7 +267,7 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
</TextField.Root> </TextField.Root>
<TextField.Root <TextField.Root
value={amount()} value={amount()}
onValueChange={setAmount} onChange={setAmount}
class="flex flex-col gap-2" class="flex flex-col gap-2"
> >
<TextField.Label class="text-sm font-semibold uppercase" >Amount</TextField.Label> <TextField.Label class="text-sm font-semibold uppercase" >Amount</TextField.Label>
@@ -313,7 +313,7 @@ function LnUrlAuth() {
<form class="flex flex-col gap-4" onSubmit={onSubmit} > <form class="flex flex-col gap-4" onSubmit={onSubmit} >
<TextField.Root <TextField.Root
value={value()} value={value()}
onValueChange={setValue} onChange={setValue}
validationState={(value() == "" || value().toLowerCase().startsWith("lnurl")) ? "valid" : "invalid"} validationState={(value() == "" || value().toLowerCase().startsWith("lnurl")) ? "valid" : "invalid"}
class="flex flex-col gap-4" class="flex flex-col gap-4"
> >
@@ -327,6 +327,32 @@ function LnUrlAuth() {
) )
} }
function ListTags() {
const [_state, actions] = useMegaStore()
const [tags] = createResource(actions.listTags)
return (
<Collapsible.Root>
<Collapsible.Trigger class="w-full">
<h2 class="truncate text-start text-lg font-mono bg-neutral-200 text-black rounded px-4 py-2">
{">"} Tags
</h2>
</Collapsible.Trigger>
<Collapsible.Content>
<VStack>
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
{JSON.stringify(tags(), null, 2)}
</pre>
</VStack>
</Collapsible.Content>
</Collapsible.Root>
)
}
export default function KitchenSink() { export default function KitchenSink() {
@@ -339,6 +365,9 @@ export default function KitchenSink() {
<ChannelsList /> <ChannelsList />
<Hr /> <Hr />
<LnUrlAuth /> <LnUrlAuth />
<Hr />
<ListTags />
<Hr /> <Hr />
<ImportExport /> <ImportExport />
</Card> </Card>

View File

@@ -7,7 +7,7 @@ export function Logs() {
async function handleSave() { async function handleSave() {
const logs = await state.mutiny_wallet?.get_logs() const logs = await state.mutiny_wallet?.get_logs()
downloadTextFile(logs.join() || "", "mutiny-logs.txt", "text/plain") downloadTextFile(logs.join("") || "", "mutiny-logs.txt", "text/plain")
} }
return ( return (

View File

@@ -1,7 +1,10 @@
import { Show, createSignal, onMount } from "solid-js"; import { Show, createSignal, onMount } from "solid-js";
import { Button, ButtonLink, SmallHeader, VStack } from "./layout"; import { Button, ButtonLink, SmallHeader } from "./layout";
import { useMegaStore } from "~/state/megaStore"; import { useMegaStore } from "~/state/megaStore";
import { showToast } from "./Toaster"; import { showToast } from "./Toaster";
import save from "~/assets/icons/save.svg"
import close from "~/assets/icons/close.svg";
import restore from "~/assets/icons/upload.svg";
export function OnboardWarning() { export function OnboardWarning() {
const [state, actions] = useMegaStore(); const [state, actions] = useMegaStore();
@@ -18,30 +21,42 @@ export function OnboardWarning() {
return ( return (
<> <>
{/* TODO: show this once we have a restore flow */} {/* TODO: show this once we have a restore flow */}
<Show when={!state.dismissed_restore_prompt && false}> <Show when={false}>
<div class='rounded-xl p-4 flex flex-col gap-2 bg-neutral-950 overflow-x-hidden'> <div class="grid grid-cols-[auto_minmax(0,_1fr)_auto] rounded-xl p-4 gap-4 bg-neutral-950/50">
<SmallHeader>Welcome!</SmallHeader> <div class="self-center">
<VStack> <img src={restore} alt="backup" class="w-8 h-8" />
<p class="text-2xl font-light">
Do you want to restore an existing Mutiny Wallet?
</p>
<div class="w-full flex gap-2">
<Button intent="green" onClick={() => { showToast({ title: "Unimplemented", description: "We don't do that yet" }) }}>Restore</Button>
<Button onClick={actions.dismissRestorePrompt}>Nope</Button>
</div> </div>
</VStack> <div class='flex md:flex-row flex-col items-center gap-4'>
<div class="flex flex-col">
<SmallHeader>Welcome!</SmallHeader>
<p class="text-base font-light">
If you've used Mutiny before you can restore from a backup. Otherwise you can skip this and enjoy your new wallet!
</p>
</div>
<Button intent="green" layout="xs" class="self-start md:self-auto" onClick={() => { showToast({ title: "Unimplemented", description: "We don't do that yet" }) }}>Restore</Button>
</div>
<button tabindex="-1" onClick={() => { actions.dismissRestorePrompt() }} class="self-center hover:bg-white/10 rounded-lg active:bg-m-blue w-8">
<img src={close} alt="Close" />
</button>
</div> </div>
</Show> </Show>
<Show when={!state.has_backed_up && hasMoney() && !dismissedBackup()}> <Show when={!state.has_backed_up && hasMoney() && !dismissedBackup()}>
<div class='rounded-xl p-4 flex flex-col gap-2 bg-neutral-950 overflow-x-hidden'> <div class="grid grid-cols-[auto_minmax(0,_1fr)_auto] rounded-xl p-4 gap-4 bg-neutral-950/50">
<div class="self-center">
<img src={save} alt="backup" class="w-8 h-8" />
</div>
<div class='flex md:flex-row flex-col items-center gap-4'>
<div class="flex flex-col">
<SmallHeader>Secure your funds</SmallHeader> <SmallHeader>Secure your funds</SmallHeader>
<p class="text-2xl font-light"> <p class="text-base font-light">
You have money stored in this browser. Let's make sure you have a backup. You have money stored in this browser. Let's make sure you have a backup.
</p> </p>
<div class="w-full flex gap-2">
<ButtonLink intent="blue" href="/backup">Backup</ButtonLink>
<Button onClick={() => { setDismissedBackup(true) }}>Nope</Button>
</div> </div>
<ButtonLink intent="blue" layout="xs" class="self-start md:self-auto" href="/backup">Backup</ButtonLink>
</div>
<button tabindex="-1" onClick={() => { setDismissedBackup(true) }} class="self-center hover:bg-white/10 rounded-lg active:bg-m-blue w-8">
<img src={close} alt="Close" />
</button>
</div> </div>
</Show> </Show>
</> </>

View File

@@ -25,7 +25,7 @@ const ReloadPrompt: Component = () => {
return ( return (
<Show when={offlineReady() || needRefresh()}> <Show when={offlineReady() || needRefresh()}>
<Card title="PWA settings"> {/* <Card title="PWA settings">
<div> <div>
<Show <Show
fallback={<span>New content available, click on reload button to update.</span>} fallback={<span>New content available, click on reload button to update.</span>}
@@ -38,7 +38,7 @@ const ReloadPrompt: Component = () => {
<Button onClick={() => updateServiceWorker(true)}>Reload</Button> <Button onClick={() => updateServiceWorker(true)}>Reload</Button>
</Show> </Show>
<Button onClick={() => close()}>Close</Button> <Button onClick={() => close()}>Close</Button>
</Card> </Card> */}
</Show> </Show>
) )
} }

View File

@@ -35,13 +35,12 @@ export function StringShower(props: { text: string }) {
return ( return (
<> <>
<JsonModal open={open()} data={props.text} title="Details" setOpen={setOpen} /> <JsonModal open={open()} data={props.text} title="Details" setOpen={setOpen} />
<div class="flex gap-2"> <div class="w-full grid grid-cols-[minmax(0,_1fr)_auto]">
<pre class="truncate text-neutral-400">{props.text}</pre> <pre class="truncate text-neutral-400">{props.text}</pre>
<button class="w-[16rem]" onClick={() => setOpen(true)}> <button class="w-[2rem]" onClick={() => setOpen(true)}>
<img src={eyeIcon} alt="eye" /> <img src={eyeIcon} alt="eye" />
</button> </button>
</div> </div>
</> </>
) )
} }

View File

@@ -1,9 +1,12 @@
import { Select, createOptions } from "@thisbeyond/solid-select"; import { Select, createOptions } from "@thisbeyond/solid-select";
import "~/styles/solid-select.css" import "~/styles/solid-select.css"
import { For, createUniqueId } from "solid-js"; import { For } from "solid-js";
import { ContactEditor } from "./ContactEditor"; import { ContactEditor } from "./ContactEditor";
import { ContactItem, TagItem, TextItem, addContact } from "~/state/contacts";
import { TinyButton } from "./layout"; import { TinyButton } from "./layout";
import { ContactFormValues } from "./ContactViewer";
import { MutinyTagItem } from "~/utils/tags";
import { Contact } from "@mutinywallet/mutiny-wasm";
import { useMegaStore } from "~/state/megaStore";
// take two arrays, subtract the second from the first, then return the first // take two arrays, subtract the second from the first, then return the first
function subtract<T>(a: T[], b: T[]) { function subtract<T>(a: T[], b: T[]) {
@@ -11,12 +14,20 @@ function subtract<T>(a: T[], b: T[]) {
return a.filter(x => !set.has(x)); return a.filter(x => !set.has(x));
} }
const createValue = (name: string): TextItem => { const createLabelValue = (label: string): Partial<MutinyTagItem> => {
return { id: createUniqueId(), name, kind: "text" }; return { id: label, name: label, kind: "Label" };
}; };
export function TagEditor(props: { values: TagItem[], setValues: (values: TagItem[]) => void, selectedValues: TagItem[], setSelectedValues: (values: TagItem[]) => void, placeholder: string }) { export function TagEditor(props: {
const onChange = (selected: TagItem[]) => { values: MutinyTagItem[],
setValues: (values: MutinyTagItem[]) => void,
selectedValues: MutinyTagItem[],
setSelectedValues: (values: MutinyTagItem[]) => void,
placeholder: string
}) {
const [state, actions] = useMegaStore();
const onChange = (selected: MutinyTagItem[]) => {
props.setSelectedValues(selected); props.setSelectedValues(selected);
console.log(selected) console.log(selected)
@@ -31,12 +42,23 @@ export function TagEditor(props: { values: TagItem[], setValues: (values: TagIte
key: "name", key: "name",
disable: (value) => props.selectedValues.includes(value), disable: (value) => props.selectedValues.includes(value),
filterable: true, // Default filterable: true, // Default
createable: createValue, createable: createLabelValue,
}); });
const newContact = async (contact: ContactItem) => { async function createContact(contact: ContactFormValues) {
await addContact(contact) // FIXME: undefineds
onChange([...props.selectedValues, contact]) // FIXME: npub not valid? other undefineds
const c = new Contact(contact.name, undefined, undefined, undefined);
const newContactId = await state.mutiny_wallet?.create_new_contact(c);
const contactItem = await state.mutiny_wallet?.get_contact(newContactId ?? "");
const mutinyContactItem: MutinyTagItem = { id: contactItem?.id || "", name: contactItem?.name || "", kind: "Contact", last_used_time: 0n };
if (contactItem) {
// @ts-ignore
// FIXME: make typescript less mad about this
onChange([...props.selectedValues, mutinyContactItem])
} else {
console.error("Failed to create contact")
}
} }
return ( return (
@@ -52,13 +74,13 @@ export function TagEditor(props: { values: TagItem[], setValues: (values: TagIte
<div class="flex gap-2 flex-wrap"> <div class="flex gap-2 flex-wrap">
<For each={subtract(props.values, props.selectedValues).slice(0, 3)}> <For each={subtract(props.values, props.selectedValues).slice(0, 3)}>
{(tag) => ( {(tag) => (
<TinyButton onClick={() => onChange([...props.selectedValues, tag])} <TinyButton tag={tag} onClick={() => onChange([...props.selectedValues, tag])}
> >
{tag.name} {tag.name}
</TinyButton> </TinyButton>
)} )}
</For> </For>
<ContactEditor createContact={newContact} /> <ContactEditor createContact={createContact} />
</div> </div>
</div > </div >
) )

View File

@@ -43,13 +43,10 @@ export function ToastItem(props: { toastId: number, title: string, description:
</p> </p>
</Toast.Description> </Toast.Description>
</div> </div>
<Toast.CloseButton class="hover:bg-white/10 rounded-lg active:bg-m-blue w-[5rem] flex-0"> <Toast.CloseButton class="hover:bg-white/10 rounded-lg active:bg-m-blue flex-0">
<img src={close} alt="Close" /> <img src={close} alt="Close" />
</Toast.CloseButton> </Toast.CloseButton>
</div> </div>
{/* <Toast.ProgressTrack class="toast__progress-track">
<Toast.ProgressFill class="toast__progress-fill" />
</Toast.ProgressTrack> */}
</Toast.Root> </Toast.Root>
) )
} }

View File

@@ -4,7 +4,7 @@ import { Dynamic } from "solid-js/web";
import { A } from "solid-start"; import { A } from "solid-start";
import { LoadingSpinner } from "."; import { LoadingSpinner } from ".";
const button = cva("p-3 rounded-xl text-xl font-semibold disabled:opacity-50 disabled:grayscale transition", { const button = cva("p-3 rounded-xl font-semibold disabled:opacity-50 disabled:grayscale transition", {
variants: { variants: {
// TODO: button hover has to work different than buttonlinks (like disabled state) // TODO: button hover has to work different than buttonlinks (like disabled state)
intent: { intent: {
@@ -16,10 +16,10 @@ const button = cva("p-3 rounded-xl text-xl font-semibold disabled:opacity-50 dis
green: "bg-m-green text-white shadow-inner-button hover:bg-m-green-dark text-shadow-button", green: "bg-m-green text-white shadow-inner-button hover:bg-m-green-dark text-shadow-button",
}, },
layout: { layout: {
flex: "flex-1", flex: "flex-1 text-xl",
pad: "px-8", pad: "px-8 text-xl",
small: "px-4 py-2 w-auto text-lg", small: "px-4 py-2 w-auto text-lg",
xs: "px-2 py-1 w-auto rounded-lg font-normal text-base" xs: "px-4 py-2 w-auto rounded-lg text-base"
}, },
}, },
defaultVariants: { defaultVariants: {
@@ -32,7 +32,8 @@ const button = cva("p-3 rounded-xl text-xl font-semibold disabled:opacity-50 dis
type StyleProps = VariantProps<typeof button> type StyleProps = VariantProps<typeof button>
interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement>, StyleProps { interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement>, StyleProps {
loading?: boolean loading?: boolean,
disabled?: boolean,
} }
export const Button: ParentComponent<ButtonProps> = props => { export const Button: ParentComponent<ButtonProps> = props => {
@@ -42,6 +43,7 @@ export const Button: ParentComponent<ButtonProps> = props => {
return ( return (
<button <button
{...attrs} {...attrs}
disabled={props.disabled || props.loading}
class={button({ class={button({
class: local.class || "", class: local.class || "",
intent: local.intent, intent: local.intent,

View File

@@ -18,7 +18,7 @@ type FullscreenModalProps = {
export function FullscreenModal(props: FullscreenModalProps) { export function FullscreenModal(props: FullscreenModalProps) {
return ( return (
<Dialog.Root isOpen={props.open} onOpenChange={(isOpen) => props.setOpen(isOpen)}> <Dialog.Root open={props.open} onOpenChange={(isOpen) => props.setOpen(isOpen)}>
<Dialog.Portal> <Dialog.Portal>
<div class={DIALOG_POSITIONER}> <div class={DIALOG_POSITIONER}>
<Dialog.Content class={DIALOG_CONTENT}> <Dialog.Content class={DIALOG_CONTENT}>

View File

@@ -7,7 +7,7 @@ type Choices = { value: string, label: string, caption: string }[]
export function StyledRadioGroup(props: { value: string, choices: Choices, onValueChange: (value: string) => void, small?: boolean, accent?: "red" | "white" }) { export function StyledRadioGroup(props: { value: string, choices: Choices, onValueChange: (value: string) => void, small?: boolean, accent?: "red" | "white" }) {
return ( return (
// TODO: rewrite this with CVA, props are bad for tailwind // TODO: rewrite this with CVA, props are bad for tailwind
<RadioGroup.Root value={props.value} onValueChange={(e) => props.onValueChange(e)} <RadioGroup.Root value={props.value} onChange={(e) => props.onValueChange(e)}
class={"grid w-full gap-4"} class={"grid w-full gap-4"}
classList={{ "grid-cols-2": props.choices.length === 2, "grid-cols-3": props.choices.length === 3, "gap-2": props.small }} classList={{ "grid-cols-2": props.choices.length === 2, "grid-cols-3": props.choices.length === 3, "gap-2": props.small }}
> >

View File

@@ -1,8 +1,11 @@
import { JSX, ParentComponent, Show, Suspense } from "solid-js" import { JSX, ParentComponent, Show, Suspense, createResource, createSignal } from "solid-js"
import Linkify from "./Linkify" import Linkify from "./Linkify"
import { Button, ButtonLink } from "./Button" import { Button, ButtonLink } from "./Button"
import { Separator } from "@kobalte/core" import { Checkbox as KCheckbox, Separator } from "@kobalte/core"
import { useMegaStore } from "~/state/megaStore" import { useMegaStore } from "~/state/megaStore"
import check from "~/assets/icons/check.svg"
import { MutinyTagItem } from "~/utils/tags"
import { generateGradient } from "~/utils/gradientHash"
export { export {
Button, Button,
@@ -118,12 +121,23 @@ export const SmallAmount: ParentComponent<{ amount: number | bigint, sign?: stri
} }
export const NiceP: ParentComponent = (props) => { export const NiceP: ParentComponent = (props) => {
return (<p class="text-2xl font-light">{props.children}</p>) return (<p class="text-xl font-light">{props.children}</p>)
} }
export const TinyButton: ParentComponent<{ onClick: () => void }> = (props) => { export const TinyButton: ParentComponent<{ onClick: () => void, tag?: MutinyTagItem }> = (props) => {
// TODO: don't need to run this if it's not a contact
const [gradient] = createResource(props.tag?.name, async (name: string) => {
return generateGradient(name || "?")
})
const bg = () => (props.tag?.name && props.tag?.kind === "Contact") ? gradient() : "rgb(255 255 255 / 0.1)"
console.log("tiny tag", props.tag?.name, gradient())
return ( return (
<button class="py-1 px-2 rounded-lg bg-white/10" onClick={() => props.onClick()}> <button class="py-1 px-2 rounded-lg bg-white/10" onClick={() => props.onClick()}
style={{ background: bg() }}
>
{props.children} {props.children}
</button> </button>
) )
@@ -134,3 +148,17 @@ export const Indicator: ParentComponent = (props) => {
<div class="box-border animate-pulse px-2 py-1 -my-1 bg-white/70 rounded text-xs uppercase text-black">{props.children}</div> <div class="box-border animate-pulse px-2 py-1 -my-1 bg-white/70 rounded text-xs uppercase text-black">{props.children}</div>
) )
} }
export function Checkbox(props: { label: string, checked: boolean, onChange: (checked: boolean) => void }) {
return (
<KCheckbox.Root class="inline-flex items-center gap-2" checked={props.checked} onChange={props.onChange}>
<KCheckbox.Input class="" />
<KCheckbox.Control class="flex-0 w-8 h-8 rounded-lg border-2 border-white bg-neutral-800 ui-checked:bg-m-red">
<KCheckbox.Indicator>
<img src={check} class="w-8 h-8" alt="check" />
</KCheckbox.Indicator>
</KCheckbox.Control>
<KCheckbox.Label class="flex-1 text-xl font-light">{props.label}</KCheckbox.Label>
</KCheckbox.Root>
)
}

View File

@@ -5,23 +5,41 @@ import { BackLink } from "~/components/layout/BackLink";
import { CombinedActivity } from "~/components/Activity"; import { CombinedActivity } from "~/components/Activity";
import { A } from "solid-start"; import { A } from "solid-start";
import settings from '~/assets/icons/settings.svg'; import settings from '~/assets/icons/settings.svg';
import { ContactItem, addContact, editContact, listContacts } from "~/state/contacts";
import { Tabs } from "@kobalte/core"; import { Tabs } from "@kobalte/core";
import { gradientsPerContact } from "~/utils/gradientHash"; import { gradientsPerContact } from "~/utils/gradientHash";
import { ContactEditor } from "~/components/ContactEditor"; import { ContactEditor } from "~/components/ContactEditor";
import { ContactViewer } from "~/components/ContactViewer"; import { ContactFormValues, ContactViewer } from "~/components/ContactViewer";
import { useMegaStore } from "~/state/megaStore";
import { Contact } from "@mutinywallet/mutiny-wasm";
import { showToast } from "~/components/Toaster";
function ContactRow() { function ContactRow() {
const [contacts, { refetch }] = createResource(listContacts) const [state, actions] = useMegaStore();
const [contacts, { refetch }] = createResource(async () => {
const contacts = state.mutiny_wallet?.get_contacts();
console.log(contacts)
let c: Contact[] = []
if (contacts) {
for (let contact in contacts) {
c.push(contacts[contact])
}
}
return c || []
})
const [gradients] = createResource(contacts, gradientsPerContact); const [gradients] = createResource(contacts, gradientsPerContact);
async function createContact(contact: ContactItem) { async function createContact(contact: ContactFormValues) {
await addContact(contact) // FIXME: npub not valid? other undefineds
const c = new Contact(contact.name, undefined, undefined, undefined);
await state.mutiny_wallet?.create_new_contact(c)
refetch(); refetch();
} }
async function saveContact(contact: ContactItem) { //
await editContact(contact) async function saveContact(contact: ContactFormValues) {
showToast(new Error("Unimplemented"))
// await editContact(contact)
refetch(); refetch();
} }
@@ -31,7 +49,7 @@ function ContactRow() {
<Show when={contacts() && gradients()}> <Show when={contacts() && gradients()}>
<For each={contacts()}> <For each={contacts()}>
{(contact) => ( {(contact) => (
<ContactViewer contact={contact} gradient={gradients()?.get(contact.id)} saveContact={saveContact} /> <ContactViewer contact={contact} gradient={gradients()?.get(contact.name)} saveContact={saveContact} />
)} )}
</For> </For>
</Show> </Show>
@@ -49,6 +67,7 @@ export default function Activity() {
<BackLink /> <BackLink />
<LargeHeader action={<A class="md:hidden p-2 hover:bg-white/5 rounded-lg active:bg-m-blue" href="/settings"><img src={settings} alt="Settings" /></A>}>Activity</LargeHeader> <LargeHeader action={<A class="md:hidden p-2 hover:bg-white/5 rounded-lg active:bg-m-blue" href="/settings"><img src={settings} alt="Settings" /></A>}>Activity</LargeHeader>
<ContactRow /> <ContactRow />
<Tabs.Root defaultValue="mutiny"> <Tabs.Root defaultValue="mutiny">
<Tabs.List class="relative flex justify-around mt-4 mb-8 gap-1 bg-neutral-950 p-1 rounded-xl"> <Tabs.List class="relative flex justify-around mt-4 mb-8 gap-1 bg-neutral-950 p-1 rounded-xl">
<Tabs.Trigger value="mutiny" class={TAB}>Mutiny</Tabs.Trigger> <Tabs.Trigger value="mutiny" class={TAB}>Mutiny</Tabs.Trigger>
@@ -58,7 +77,10 @@ export default function Activity() {
<Tabs.Content value="mutiny"> <Tabs.Content value="mutiny">
{/* <MutinyActivity /> */} {/* <MutinyActivity /> */}
<Card title="Activity"> <Card title="Activity">
<div class="p-1" />
<VStack>
<CombinedActivity /> <CombinedActivity />
</VStack>
</Card> </Card>
</Tabs.Content> </Tabs.Content>
<Tabs.Content value="nostr"> <Tabs.Content value="nostr">

View File

@@ -1,16 +1,39 @@
import { Button, DefaultMain, LargeHeader, NiceP, MutinyWalletGuard, SafeArea, VStack } from "~/components/layout"; import { Button, DefaultMain, LargeHeader, NiceP, MutinyWalletGuard, SafeArea, VStack, Checkbox } from "~/components/layout";
import NavBar from "~/components/NavBar"; import NavBar from "~/components/NavBar";
import { useNavigate } from 'solid-start'; import { useNavigate } from 'solid-start';
import { SeedWords } from '~/components/SeedWords'; import { SeedWords } from '~/components/SeedWords';
import { useMegaStore } from '~/state/megaStore'; import { useMegaStore } from '~/state/megaStore';
import { Show, createSignal } from 'solid-js'; import { Show, createEffect, createSignal } from 'solid-js';
import { BackLink } from "~/components/layout/BackLink"; import { BackLink } from "~/components/layout/BackLink";
function Quiz(props: { setHasCheckedAll: (hasChecked: boolean) => void }) {
const [one, setOne] = createSignal(false);
const [two, setTwo] = createSignal(false);
const [three, setThree] = createSignal(false);
createEffect(() => {
if (one() && two() && three()) {
props.setHasCheckedAll(true)
} else {
props.setHasCheckedAll(false)
}
})
return (
<VStack>
<Checkbox checked={one()} onChange={setOne} label="I wrote down the words" />
<Checkbox checked={two()} onChange={setTwo} label="I understand that my funds are my responsibility" />
<Checkbox checked={three()} onChange={setThree} label="I'm not lying just to get this over with" />
</VStack>
)
}
export default function App() { export default function App() {
const [store, actions] = useMegaStore(); const [store, actions] = useMegaStore();
const navigate = useNavigate(); const navigate = useNavigate();
const [hasSeenBackup, setHasSeenBackup] = createSignal(false); const [hasSeenBackup, setHasSeenBackup] = createSignal(false);
const [hasCheckedAll, setHasCheckedAll] = createSignal(false);
function wroteDownTheWords() { function wroteDownTheWords() {
actions.setHasBackedUp() actions.setHasBackedUp()
@@ -23,6 +46,7 @@ export default function App() {
<DefaultMain> <DefaultMain>
<BackLink /> <BackLink />
<LargeHeader>Backup</LargeHeader> <LargeHeader>Backup</LargeHeader>
<VStack> <VStack>
<NiceP>Let's get these funds secured.</NiceP> <NiceP>Let's get these funds secured.</NiceP>
<NiceP>We'll show you 12 words. You write down the 12 words.</NiceP> <NiceP>We'll show you 12 words. You write down the 12 words.</NiceP>
@@ -32,9 +56,9 @@ export default function App() {
<NiceP>Mutiny is self-custodial. It's all up to you...</NiceP> <NiceP>Mutiny is self-custodial. It's all up to you...</NiceP>
<SeedWords words={store.mutiny_wallet?.show_seed() || ""} setHasSeen={setHasSeenBackup} /> <SeedWords words={store.mutiny_wallet?.show_seed() || ""} setHasSeen={setHasSeenBackup} />
<Show when={hasSeenBackup()}> <Show when={hasSeenBackup()}>
<NiceP>You are responsible for your funds!</NiceP> <Quiz setHasCheckedAll={setHasCheckedAll} />
</Show> </Show>
<Button disabled={!hasSeenBackup()} intent="blue" onClick={wroteDownTheWords}>I wrote down the words</Button> <Button disabled={!hasSeenBackup() || !hasCheckedAll()} intent="blue" onClick={wroteDownTheWords}>I wrote down the words</Button>
</VStack> </VStack>
</DefaultMain> </DefaultMain>
<NavBar activeTab="none" /> <NavBar activeTab="none" />

View File

@@ -14,10 +14,10 @@ import { StyledRadioGroup } from "~/components/layout/Radio";
import { showToast } from "~/components/Toaster"; import { showToast } from "~/components/Toaster";
import { useNavigate } from "solid-start"; import { useNavigate } from "solid-start";
import megacheck from "~/assets/icons/megacheck.png"; import megacheck from "~/assets/icons/megacheck.png";
import { TagItem, listTags } from "~/state/contacts";
import { AmountCard } from "~/components/AmountCard"; import { AmountCard } from "~/components/AmountCard";
import { ShareCard } from "~/components/ShareCard"; import { ShareCard } from "~/components/ShareCard";
import { BackButton } from "~/components/layout/BackButton"; import { BackButton } from "~/components/layout/BackButton";
import { MutinyTagItem, UNKNOWN_TAG, sortByLastUsed, tagsToIds } from "~/utils/tags";
type OnChainTx = { type OnChainTx = {
transaction: { transaction: {
@@ -43,8 +43,6 @@ type OnChainTx = {
} }
} }
const createUniqueId = () => Math.random().toString(36).substr(2, 9);
const RECEIVE_FLAVORS = [{ value: "unified", label: "Unified", caption: "Sender decides" }, { value: "lightning", label: "Lightning", caption: "Fast and cool" }, { value: "onchain", label: "On-chain", caption: "Just like Satoshi did it" }] const RECEIVE_FLAVORS = [{ value: "unified", label: "Unified", caption: "Sender decides" }, { value: "lightning", label: "Lightning", caption: "Fast and cool" }, { value: "onchain", label: "On-chain", caption: "Just like Satoshi did it" }]
type ReceiveFlavor = "unified" | "lightning" | "onchain" type ReceiveFlavor = "unified" | "lightning" | "onchain"
@@ -52,7 +50,7 @@ type ReceiveState = "edit" | "show" | "paid"
type PaidState = "lightning_paid" | "onchain_paid"; type PaidState = "lightning_paid" | "onchain_paid";
export default function Receive() { export default function Receive() {
const [state, _] = useMegaStore() const [state, actions] = useMegaStore()
const navigate = useNavigate(); const navigate = useNavigate();
const [amount, setAmount] = createSignal("") const [amount, setAmount] = createSignal("")
@@ -62,8 +60,8 @@ export default function Receive() {
const [shouldShowAmountEditor, setShouldShowAmountEditor] = createSignal(true) const [shouldShowAmountEditor, setShouldShowAmountEditor] = createSignal(true)
// Tagging stuff // Tagging stuff
const [selectedValues, setSelectedValues] = createSignal<TagItem[]>([]); const [selectedValues, setSelectedValues] = createSignal<MutinyTagItem[]>([]);
const [values, setValues] = createSignal<TagItem[]>([{ id: createUniqueId(), name: "Unknown", kind: "text" }]); const [values, setValues] = createSignal<MutinyTagItem[]>([UNKNOWN_TAG]);
// The data we get after a payment // The data we get after a payment
const [paymentTx, setPaymentTx] = createSignal<OnChainTx>(); const [paymentTx, setPaymentTx] = createSignal<OnChainTx>();
@@ -86,8 +84,8 @@ export default function Receive() {
}) })
onMount(() => { onMount(() => {
listTags().then((tags) => { actions.listTags().then((tags) => {
setValues(prev => [...prev, ...tags || []]) setValues(prev => [...prev, ...tags.sort(sortByLastUsed) || []])
}); });
}) })
@@ -104,8 +102,7 @@ export default function Receive() {
async function getUnifiedQr(amount: string) { async function getUnifiedQr(amount: string) {
const bigAmount = BigInt(amount); const bigAmount = BigInt(amount);
try { try {
// FIXME: actual labels const raw = await state.mutiny_wallet?.create_bip21(bigAmount, tagsToIds(selectedValues()));
const raw = await state.mutiny_wallet?.create_bip21(bigAmount, []);
// Save the raw info so we can watch the address and invoice // Save the raw info so we can watch the address and invoice
setBip21Raw(raw); setBip21Raw(raw);

View File

@@ -341,7 +341,7 @@ export default function Redshift() {
<Match when={shiftStage() === "choose"}> <Match when={shiftStage() === "choose"}>
<VStack> <VStack>
<NiceP>Where is this going?</NiceP> <NiceP>Where is this going?</NiceP>
<StyledRadioGroup red value={shiftType()} onValueChange={(newValue) => setShiftType(newValue as ShiftOption)} choices={SHIFT_OPTIONS} /> <StyledRadioGroup accent="red" value={shiftType()} onValueChange={(newValue) => setShiftType(newValue as ShiftOption)} choices={SHIFT_OPTIONS} />
</VStack> </VStack>
<VStack> <VStack>
<NiceP>Choose your <span class="inline-block"><img class="h-4" src={wave} alt="sine wave" /></span> UTXO to begin</NiceP> <NiceP>Choose your <span class="inline-block"><img class="h-4" src={wave} alt="sine wave" /></span> UTXO to begin</NiceP>

View File

@@ -17,9 +17,9 @@ import mempoolTxUrl from "~/utils/mempoolTxUrl";
import { BackLink } from "~/components/layout/BackLink"; import { BackLink } from "~/components/layout/BackLink";
import { useNavigate } from "solid-start"; import { useNavigate } from "solid-start";
import { TagEditor } from "~/components/TagEditor"; import { TagEditor } from "~/components/TagEditor";
import { TagItem, createUniqueId, listTags } from "~/state/contacts";
import { StringShower } from "~/components/ShareCard"; import { StringShower } from "~/components/ShareCard";
import { AmountCard } from "~/components/AmountCard"; import { AmountCard } from "~/components/AmountCard";
import { MutinyTagItem, UNKNOWN_TAG, sortByLastUsed, tagsToIds } from "~/utils/tags";
type SendSource = "lightning" | "onchain"; type SendSource = "lightning" | "onchain";
@@ -97,23 +97,6 @@ function DestinationShower(props: {
) )
} }
function SendTags() {
// Tagging stuff
const [selectedValues, setSelectedValues] = createSignal<TagItem[]>([]);
const [values, setValues] = createSignal<TagItem[]>([{ id: createUniqueId(), name: "Unknown", kind: "text" }]);
onMount(() => {
listTags().then((tags) => {
setValues(prev => [...prev, ...tags || []])
});
})
return (
<TagEditor values={values()} setValues={setValues} selectedValues={selectedValues()} setSelectedValues={setSelectedValues} placeholder="Where's it going to?" />
)
}
export default function Send() { export default function Send() {
const [state, actions] = useMegaStore(); const [state, actions] = useMegaStore();
const navigate = useNavigate() const navigate = useNavigate()
@@ -136,6 +119,10 @@ export default function Send() {
const [sending, setSending] = createSignal(false); const [sending, setSending] = createSignal(false);
const [sentDetails, setSentDetails] = createSignal<SentDetails>(); const [sentDetails, setSentDetails] = createSignal<SentDetails>();
// Tagging stuff
const [selectedValues, setSelectedValues] = createSignal<MutinyTagItem[]>([]);
const [values, setValues] = createSignal<MutinyTagItem[]>([UNKNOWN_TAG]);
function clearAll() { function clearAll() {
setDestination(undefined); setDestination(undefined);
setAmountSats(0n); setAmountSats(0n);
@@ -158,6 +145,10 @@ export default function Send() {
setDestination(state.scan_result); setDestination(state.scan_result);
actions.setScanResult(undefined); actions.setScanResult(undefined);
} }
actions.listTags().then((tags) => {
setValues(prev => [...prev, ...tags.sort(sortByLastUsed) || []])
});
}) })
// Rerun every time the destination changes // Rerun every time the destination changes
@@ -235,17 +226,16 @@ export default function Send() {
sentDetails.destination = bolt11; sentDetails.destination = bolt11;
// If the invoice has sats use that, otherwise we pass the user-defined amount // If the invoice has sats use that, otherwise we pass the user-defined amount
if (invoice()?.amount_sats) { if (invoice()?.amount_sats) {
await state.mutiny_wallet?.pay_invoice(firstNode, bolt11); await state.mutiny_wallet?.pay_invoice(firstNode, bolt11, undefined, tagsToIds(selectedValues()));
sentDetails.amount = invoice()?.amount_sats; sentDetails.amount = invoice()?.amount_sats;
} else { } else {
await state.mutiny_wallet?.pay_invoice(firstNode, bolt11, amountSats()); await state.mutiny_wallet?.pay_invoice(firstNode, bolt11, amountSats(), tagsToIds(selectedValues()));
sentDetails.amount = amountSats(); sentDetails.amount = amountSats();
} }
} else if (source() === "lightning" && nodePubkey()) { } else if (source() === "lightning" && nodePubkey()) {
const nodes = await state.mutiny_wallet?.list_nodes(); const nodes = await state.mutiny_wallet?.list_nodes();
const firstNode = nodes[0] as string || "" const firstNode = nodes[0] as string || ""
const payment = await state.mutiny_wallet?.keysend(firstNode, nodePubkey()!, amountSats()); const payment = await state.mutiny_wallet?.keysend(firstNode, nodePubkey()!, amountSats(), tagsToIds(selectedValues()));
console.log(payment?.value)
// TODO: handle timeouts // TODO: handle timeouts
if (!payment?.paid) { if (!payment?.paid) {
@@ -254,9 +244,8 @@ export default function Send() {
sentDetails.amount = amountSats(); sentDetails.amount = amountSats();
} }
} else if (source() === "onchain" && address()) { } else if (source() === "onchain" && address()) {
// FIXME: actual labels
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const txid = await state.mutiny_wallet?.send_to_address(address()!, amountSats(), []); const txid = await state.mutiny_wallet?.send_to_address(address()!, amountSats(), tagsToIds(selectedValues()));
sentDetails.amount = amountSats(); sentDetails.amount = amountSats();
sentDetails.destination = address(); sentDetails.destination = address();
// TODO: figure out if this is necessary, it takes forever // TODO: figure out if this is necessary, it takes forever
@@ -320,7 +309,7 @@ export default function Send() {
<Card> <Card>
<VStack> <VStack>
<DestinationShower source={source()} description={description()} invoice={invoice()} address={address()} nodePubkey={nodePubkey()} clearAll={clearAll} /> <DestinationShower source={source()} description={description()} invoice={invoice()} address={address()} nodePubkey={nodePubkey()} clearAll={clearAll} />
<SendTags /> <TagEditor values={values()} setValues={setValues} selectedValues={selectedValues()} setSelectedValues={setSelectedValues} placeholder="Where's it going to?" />
</VStack> </VStack>
</Card> </Card>
<AmountCard amountSats={amountSats().toString()} setAmountSats={setAmountSats} fee={fakeFee().toString()} isAmountEditable={!(invoice()?.amount_sats)} /> <AmountCard amountSats={amountSats().toString()} setAmountSats={setAmountSats} fee={fakeFee().toString()} isAmountEditable={!(invoice()?.amount_sats)} />

View File

@@ -1,7 +1,9 @@
import { ActivityItem } from "~/components/ActivityItem";
import { AmountCard } from "~/components/AmountCard"; import { AmountCard } from "~/components/AmountCard";
import NavBar from "~/components/NavBar"; import NavBar from "~/components/NavBar";
import { OnboardWarning } from "~/components/OnboardWarning";
import { ShareCard } from "~/components/ShareCard"; import { ShareCard } from "~/components/ShareCard";
import { DefaultMain, LargeHeader, SafeArea, VStack } from "~/components/layout"; import { Card, DefaultMain, LargeHeader, SafeArea, VStack } from "~/components/layout";
const SAMPLE = "bitcoin:tb1prqm8xtlgme0vmw5s30lgf0a4f5g4mkgsqundwmpu6thrg8zr6uvq2qrhzq?amount=0.001&lightning=lntbs1m1pj9n9xjsp5xgdrmvprtm67p7nq4neparalexlhlmtxx87zx6xeqthsplu842zspp546d6zd2seyaxpapaxx62m88yz3xueqtjmn9v6wj8y56np8weqsxqdqqnp4qdn2hj8tfknpuvdg6tz9yrf3e27ltrx9y58c24jh89lnm43yjwfc5xqrpwjcqpj9qrsgq5sdgh0m3ur5mu5hrmmag4mx9yvy86f83pd0x9ww80kgck6tac3thuzkj0mrtltaxwnlfea95h2re7tj4qsnwzxlvrdmyq2h9mgapnycpppz6k6" const SAMPLE = "bitcoin:tb1prqm8xtlgme0vmw5s30lgf0a4f5g4mkgsqundwmpu6thrg8zr6uvq2qrhzq?amount=0.001&lightning=lntbs1m1pj9n9xjsp5xgdrmvprtm67p7nq4neparalexlhlmtxx87zx6xeqthsplu842zspp546d6zd2seyaxpapaxx62m88yz3xueqtjmn9v6wj8y56np8weqsxqdqqnp4qdn2hj8tfknpuvdg6tz9yrf3e27ltrx9y58c24jh89lnm43yjwfc5xqrpwjcqpj9qrsgq5sdgh0m3ur5mu5hrmmag4mx9yvy86f83pd0x9ww80kgck6tac3thuzkj0mrtltaxwnlfea95h2re7tj4qsnwzxlvrdmyq2h9mgapnycpppz6k6"
export default function Admin() { export default function Admin() {
@@ -9,12 +11,16 @@ export default function Admin() {
<SafeArea> <SafeArea>
<DefaultMain> <DefaultMain>
<LargeHeader>Storybook</LargeHeader> <LargeHeader>Storybook</LargeHeader>
<OnboardWarning />
<VStack> <VStack>
<AmountCard amountSats={"100000"} fee={"69"} /> <AmountCard amountSats={"100000"} fee={"69"} />
<AmountCard amountSats={"100000"} />
<AmountCard amountSats={"100000"} isAmountEditable />
<AmountCard amountSats={"0"} isAmountEditable />
<ShareCard text={SAMPLE} /> <ShareCard text={SAMPLE} />
<Card title="Activity">
<ActivityItem kind="lightning" labels={["benthecarman"]} amount={100000} date={1683664966} />
<ActivityItem kind="onchain" labels={["tony"]} amount={42000000} positive date={1683664966} />
<ActivityItem kind="onchain" labels={["a fake name thati is too long"]} amount={42000000} date={1683664966} />
<ActivityItem kind="onchain" labels={["a fake name thati is too long"]} amount={42000000} date={1683664966} />
</Card>
</VStack> </VStack>
</DefaultMain> </DefaultMain>
<NavBar activeTab="none" /> <NavBar activeTab="none" />

View File

@@ -1,58 +0,0 @@
export type TagItem = TextItem | ContactItem;
export type TextItem = {
id: string;
kind: "text";
name: string;
}
export type ContactItem = {
id: string;
kind: "contact";
name: string;
npub?: string;
color: Color;
}
export type Color = "blue" | "green" | "red" | "gray"
export const createUniqueId = () => Math.random().toString(36).substr(2, 9);
export async function listContacts(): Promise<ContactItem[]> {
// get contacts from localstorage
const contacts: ContactItem[] = JSON.parse(localStorage.getItem("contacts") || "[]");
return contacts;
}
export async function listTexts(): Promise<TextItem[]> {
// get texts from localstorage
const texts: TextItem[] = JSON.parse(localStorage.getItem("texts") || "[]");
return texts;
}
export async function listTags(): Promise<TagItem[]> {
const contacts = await listContacts();
const texts = await listTexts();
return [...contacts, ...texts];
}
export async function addContact(contact: ContactItem): Promise<void> {
const contacts = await listContacts();
contacts.push(contact);
localStorage.setItem("contacts", JSON.stringify(contacts));
}
export async function editContact(contact: ContactItem): Promise<void> {
const contacts = await listContacts();
const index = contacts.findIndex(c => c.id === contact.id);
contacts[index] = contact;
localStorage.setItem("contacts", JSON.stringify(contacts));
}
export async function addTextTag(text: TextItem): Promise<void> {
const texts = await listTexts();
texts.push(text);
localStorage.setItem("texts", JSON.stringify(texts));
}

View File

@@ -6,6 +6,7 @@ import { createStore } from "solid-js/store";
import { MutinyWalletSettingStrings, setupMutinyWallet } from "~/logic/mutinyWalletSetup"; import { MutinyWalletSettingStrings, setupMutinyWallet } from "~/logic/mutinyWalletSetup";
import { MutinyBalance, MutinyWallet } from "@mutinywallet/mutiny-wasm"; import { MutinyBalance, MutinyWallet } from "@mutinywallet/mutiny-wasm";
import { ParsedParams } from "~/routes/Scanner"; import { ParsedParams } from "~/routes/Scanner";
import { MutinyTagItem } from "~/utils/tags";
const MegaStoreContext = createContext<MegaStore>(); const MegaStoreContext = createContext<MegaStore>();
@@ -23,7 +24,8 @@ export type MegaStore = [{
last_sync?: number; last_sync?: number;
price: number price: number
has_backed_up: boolean, has_backed_up: boolean,
dismissed_restore_prompt: boolean dismissed_restore_prompt: boolean,
wallet_loading: boolean
}, { }, {
fetchUserStatus(): Promise<UserStatus>; fetchUserStatus(): Promise<UserStatus>;
setupMutinyWallet(settings?: MutinyWalletSettingStrings): Promise<void>; setupMutinyWallet(settings?: MutinyWalletSettingStrings): Promise<void>;
@@ -33,6 +35,7 @@ export type MegaStore = [{
sync(): Promise<void>; sync(): Promise<void>;
dismissRestorePrompt(): void; dismissRestorePrompt(): void;
setHasBackedUp(): void; setHasBackedUp(): void;
listTags(): Promise<MutinyTagItem[]>;
}]; }];
export const Provider: ParentComponent = (props) => { export const Provider: ParentComponent = (props) => {
@@ -49,7 +52,8 @@ export const Provider: ParentComponent = (props) => {
balance: undefined as MutinyBalance | undefined, balance: undefined as MutinyBalance | undefined,
last_sync: undefined as number | undefined, last_sync: undefined as number | undefined,
is_syncing: false, is_syncing: false,
dismissed_restore_prompt: localStorage.getItem("dismissed_restore_prompt") === "true" dismissed_restore_prompt: localStorage.getItem("dismissed_restore_prompt") === "true",
wallet_loading: true
}); });
const actions = { const actions = {
@@ -81,8 +85,9 @@ export const Provider: ParentComponent = (props) => {
}, },
async setupMutinyWallet(settings?: MutinyWalletSettingStrings): Promise<void> { async setupMutinyWallet(settings?: MutinyWalletSettingStrings): Promise<void> {
try { try {
setState({ wallet_loading: true })
const mutinyWallet = await setupMutinyWallet(settings) const mutinyWallet = await setupMutinyWallet(settings)
setState({ mutiny_wallet: mutinyWallet }) setState({ mutiny_wallet: mutinyWallet, wallet_loading: false })
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }
@@ -126,6 +131,9 @@ export const Provider: ParentComponent = (props) => {
dismissRestorePrompt() { dismissRestorePrompt() {
localStorage.setItem("dismissed_restore_prompt", "true") localStorage.setItem("dismissed_restore_prompt", "true")
setState({ dismissed_restore_prompt: true }) setState({ dismissed_restore_prompt: true })
},
async listTags(): Promise<MutinyTagItem[]> {
return state.mutiny_wallet?.get_tag_items() as MutinyTagItem[]
} }
}; };

View File

@@ -1,6 +1,6 @@
import { ContactItem } from "~/state/contacts"; import { Contact } from "@mutinywallet/mutiny-wasm";
async function generateGradientFromHashedString(str: string) { export async function generateGradient(str: string) {
const encoder = new TextEncoder(); const encoder = new TextEncoder();
const data = encoder.encode(str); const data = encoder.encode(str);
const digestBuffer = await crypto.subtle.digest('SHA-256', data); const digestBuffer = await crypto.subtle.digest('SHA-256', data);
@@ -13,11 +13,12 @@ async function generateGradientFromHashedString(str: string) {
return gradient; return gradient;
} }
export async function gradientsPerContact(contacts: ContactItem[]) { export async function gradientsPerContact(contacts: Contact[]) {
console.log(contacts);
const gradients = new Map(); const gradients = new Map();
for (const contact of contacts) { for (const contact of contacts) {
const gradient = await generateGradientFromHashedString(contact.name); const gradient = await generateGradient(contact.name);
gradients.set(contact.id, gradient); gradients.set(contact.name, gradient);
} }
return gradients; return gradients;

View File

@@ -10,3 +10,30 @@ export function prettyPrintTime(ts: number) {
return new Date(ts * 1000).toLocaleString('en-US', options); return new Date(ts * 1000).toLocaleString('en-US', options);
} }
export function timeAgo(ts?: number | bigint): string {
if (!ts || ts === 0) return "Pending";
const timestamp = Number(ts) * 1000;
const now = Date.now();
const elapsedMilliseconds = now - timestamp;
const elapsedSeconds = Math.floor(elapsedMilliseconds / 1000);
const elapsedMinutes = Math.floor(elapsedSeconds / 60);
const elapsedHours = Math.floor(elapsedMinutes / 60);
const elapsedDays = Math.floor(elapsedHours / 24);
if (elapsedSeconds < 60) {
return "Just now";
} else if (elapsedMinutes < 60) {
return `${elapsedMinutes} minute${elapsedMinutes > 1 ? 's' : ''} ago`;
} else if (elapsedHours < 24) {
return `${elapsedHours} hour${elapsedHours > 1 ? 's' : ''} ago`;
} else if (elapsedDays < 7) {
return `${elapsedDays} day${elapsedDays > 1 ? 's' : ''} ago`;
} else {
const date = new Date(timestamp);
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
return `${month}/${day}/${year}`;
}
}

27
src/utils/tags.ts Normal file
View File

@@ -0,0 +1,27 @@
import { TagItem } from "@mutinywallet/mutiny-wasm"
export type MutinyTagItem = {
id: string,
kind: "Label" | "Contact"
name: string,
last_used_time: bigint,
npub?: string,
ln_address?: string,
lnurl?: string,
}
export const UNKNOWN_TAG: MutinyTagItem = { id: "Unknown", kind: "Label", name: "Unknown", last_used_time: 0n }
export function tagsToIds(tags: MutinyTagItem[]): string[] {
return tags.filter((tag) => tag.id !== "Unknown").map((tag) => tag.id)
}
export function tagToMutinyTag(tag: TagItem): MutinyTagItem {
// @ts-ignore
// FIXME: make typescript less mad about this
return tag as MutinyTagItem
}
export function sortByLastUsed(a: MutinyTagItem, b: MutinyTagItem) {
return Number(b.last_used_time - a.last_used_time);
}