mirror of
https://git.sr.ht/~ashkeel/strimertul
synced 2024-09-20 02:00:49 +00:00
Commands are finally done!
This commit is contained in:
parent
ea87bfbaf3
commit
1ae499a664
15 changed files with 1060 additions and 20 deletions
240
frontend/package-lock.json
generated
240
frontend/package-lock.json
generated
|
@ -394,6 +394,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"@fontsource/space-mono": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/space-mono/-/space-mono-4.5.0.tgz",
|
||||
"integrity": "sha512-1tzGlLH2N/k1sovGSA4s7MpIRMciKeJbXKfdrKc1H84lg1FPcmiYGYdWiQA5MoQyjF6Bqx+IWpyIcOktMKvViA=="
|
||||
},
|
||||
"@nodelib/fs.scandir": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz",
|
||||
|
@ -433,6 +438,31 @@
|
|||
"@babel/runtime": "^7.13.10"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-alert-dialog": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-0.1.5.tgz",
|
||||
"integrity": "sha512-Lq9h3GSvw752e7dFll3UWvm4uWiTlYAXLFX6wr/VQPRoa7XaQO8/1NBu4ikLHAecGEd/uDGZLY3aP7ovGPQYtg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "0.1.0",
|
||||
"@radix-ui/react-compose-refs": "0.1.0",
|
||||
"@radix-ui/react-context": "0.1.1",
|
||||
"@radix-ui/react-dialog": "0.1.5",
|
||||
"@radix-ui/react-primitive": "0.1.3",
|
||||
"@radix-ui/react-slot": "0.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-0.1.3.tgz",
|
||||
"integrity": "sha512-fcyADaaAx2jdqEDLsTs6aX50S3L1c9K9CC6XMpJpuXFJCU4n9PGTFDZRtY2gAoXXoRCPIBsklCopSmGb6SsDjQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-slot": "0.1.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-checkbox": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-0.1.4.tgz",
|
||||
|
@ -521,6 +551,103 @@
|
|||
"@babel/runtime": "^7.13.10"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-dialog": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-0.1.5.tgz",
|
||||
"integrity": "sha512-WftvXcQSszUphCTLQkkpBIkrYYU0IYqgIvACLQady4BN4YHDgdNlrwdg2ti9QrXgq1PZ+0S/6BIaA1dmSuRQ2g==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "0.1.0",
|
||||
"@radix-ui/react-compose-refs": "0.1.0",
|
||||
"@radix-ui/react-context": "0.1.1",
|
||||
"@radix-ui/react-dismissable-layer": "0.1.3",
|
||||
"@radix-ui/react-focus-guards": "0.1.0",
|
||||
"@radix-ui/react-focus-scope": "0.1.3",
|
||||
"@radix-ui/react-id": "0.1.4",
|
||||
"@radix-ui/react-portal": "0.1.3",
|
||||
"@radix-ui/react-presence": "0.1.1",
|
||||
"@radix-ui/react-primitive": "0.1.3",
|
||||
"@radix-ui/react-slot": "0.1.2",
|
||||
"@radix-ui/react-use-controllable-state": "0.1.0",
|
||||
"aria-hidden": "^1.1.1",
|
||||
"react-remove-scroll": "^2.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-id": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-0.1.4.tgz",
|
||||
"integrity": "sha512-/hq5m/D0ZfJWOS7TLF+G0l08KDRs87LBE46JkAvgKkg1fW4jkucx9At9D9vauIPSbdNmww5kXEp566hMlA8eXA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-use-layout-effect": "0.1.0"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-primitive": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-0.1.3.tgz",
|
||||
"integrity": "sha512-fcyADaaAx2jdqEDLsTs6aX50S3L1c9K9CC6XMpJpuXFJCU4n9PGTFDZRtY2gAoXXoRCPIBsklCopSmGb6SsDjQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-slot": "0.1.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-dismissable-layer": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-0.1.3.tgz",
|
||||
"integrity": "sha512-3veE7M8K13Qb+6+tC3DHWmWV9VMuuRoZvRLdrvz7biSraK/qkGBN4LbKZDaTdw2D2HS7RNpSd/sF8pFd3TaAgA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "0.1.0",
|
||||
"@radix-ui/react-context": "0.1.1",
|
||||
"@radix-ui/react-primitive": "0.1.3",
|
||||
"@radix-ui/react-use-body-pointer-events": "0.1.0",
|
||||
"@radix-ui/react-use-callback-ref": "0.1.0",
|
||||
"@radix-ui/react-use-escape-keydown": "0.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-0.1.3.tgz",
|
||||
"integrity": "sha512-fcyADaaAx2jdqEDLsTs6aX50S3L1c9K9CC6XMpJpuXFJCU4n9PGTFDZRtY2gAoXXoRCPIBsklCopSmGb6SsDjQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-slot": "0.1.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-focus-guards": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-0.1.0.tgz",
|
||||
"integrity": "sha512-kRx/swAjEfBpQ3ns7J3H4uxpXuWCqN7MpALiSDOXiyo2vkWv0L9sxvbpZeTulINuE3CGMzicVMuNc/VWXjFKOg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-focus-scope": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-0.1.3.tgz",
|
||||
"integrity": "sha512-bKi+lw14SriQqYWMBe13b/wvxSqYMC+3FylMUEwOKA6JrBoldpkhX5XffGDdpDRTTpjbncdH3H7d1PL5Bs7Ikg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-compose-refs": "0.1.0",
|
||||
"@radix-ui/react-primitive": "0.1.3",
|
||||
"@radix-ui/react-use-callback-ref": "0.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-0.1.3.tgz",
|
||||
"integrity": "sha512-fcyADaaAx2jdqEDLsTs6aX50S3L1c9K9CC6XMpJpuXFJCU4n9PGTFDZRtY2gAoXXoRCPIBsklCopSmGb6SsDjQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-slot": "0.1.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-icons": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.0.3.tgz",
|
||||
|
@ -547,6 +674,27 @@
|
|||
"@radix-ui/react-primitive": "0.1.2"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-portal": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-0.1.3.tgz",
|
||||
"integrity": "sha512-DrV+sPYLs0HhmX5/b7yRT6nLM9Nl6FtQe2KUG+46kiCOKQ+0XzNMO5hmeQtyq0mRf/qlC02rFu6OMsWpIqVsJg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-primitive": "0.1.3",
|
||||
"@radix-ui/react-use-layout-effect": "0.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-0.1.3.tgz",
|
||||
"integrity": "sha512-fcyADaaAx2jdqEDLsTs6aX50S3L1c9K9CC6XMpJpuXFJCU4n9PGTFDZRtY2gAoXXoRCPIBsklCopSmGb6SsDjQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-slot": "0.1.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-presence": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-0.1.1.tgz",
|
||||
|
@ -646,6 +794,15 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-use-body-pointer-events": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-body-pointer-events/-/react-use-body-pointer-events-0.1.0.tgz",
|
||||
"integrity": "sha512-svPyoHCcwOq/vpWNEvdH/yD91vN9p8BtiozNQbjVmJRxQ/vS12zqk70AxTGWe+2ZKHq2sggpEQNTv1JHyVFlnQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-use-layout-effect": "0.1.0"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-use-callback-ref": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-0.1.0.tgz",
|
||||
|
@ -663,6 +820,15 @@
|
|||
"@radix-ui/react-use-callback-ref": "0.1.0"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-use-escape-keydown": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-0.1.0.tgz",
|
||||
"integrity": "sha512-tDLZbTGFmvXaazUXXv8kYbiCcbAE8yKgng9s95d8fCO+Eundv0Jngbn/hKPhDDs4jj9ChwRX5cDDnlaN+ugYYQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-use-callback-ref": "0.1.0"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-use-layout-effect": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-0.1.0.tgz",
|
||||
|
@ -952,6 +1118,14 @@
|
|||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"aria-hidden": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.1.3.tgz",
|
||||
"integrity": "sha512-RhVWFtKH5BiGMycI72q2RAFMLQi8JP9bLuQXgR5a8Znp7P5KOIADSJeyfI8PCVxLEp067B2HbP5JIiI/PXIZeA==",
|
||||
"requires": {
|
||||
"tslib": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"array-includes": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.3.tgz",
|
||||
|
@ -1206,6 +1380,11 @@
|
|||
"object-keys": "^1.0.12"
|
||||
}
|
||||
},
|
||||
"detect-node-es": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
|
||||
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="
|
||||
},
|
||||
"dir-glob": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||
|
@ -1859,6 +2038,11 @@
|
|||
"has-symbols": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"get-nonce": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
|
||||
"integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="
|
||||
},
|
||||
"get-symbol-description": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
|
||||
|
@ -2059,6 +2243,14 @@
|
|||
"side-channel": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"invariant": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
||||
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"is-arrayish": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
|
||||
|
@ -2712,6 +2904,27 @@
|
|||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"react-remove-scroll": {
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.4.3.tgz",
|
||||
"integrity": "sha512-lGWYXfV6jykJwbFpsuPdexKKzp96f3RbvGapDSIdcyGvHb7/eqyn46C7/6h+rUzYar1j5mdU+XECITHXCKBk9Q==",
|
||||
"requires": {
|
||||
"react-remove-scroll-bar": "^2.1.0",
|
||||
"react-style-singleton": "^2.1.0",
|
||||
"tslib": "^1.0.0",
|
||||
"use-callback-ref": "^1.2.3",
|
||||
"use-sidecar": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"react-remove-scroll-bar": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.2.0.tgz",
|
||||
"integrity": "sha512-UU9ZBP1wdMR8qoUs7owiVcpaPwsQxUDC2lypP6mmixaGlARZa7ZIBx1jcuObLdhMOvCsnZcvetOho0wzPa9PYg==",
|
||||
"requires": {
|
||||
"react-style-singleton": "^2.1.0",
|
||||
"tslib": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"react-router": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.1.1.tgz",
|
||||
|
@ -2729,6 +2942,16 @@
|
|||
"react-router": "6.1.1"
|
||||
}
|
||||
},
|
||||
"react-style-singleton": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.1.1.tgz",
|
||||
"integrity": "sha512-jNRp07Jza6CBqdRKNgGhT3u9umWvils1xsuMOjZlghBDH2MU0PL2WZor4PGYjXpnRCa9DQSlHMs/xnABWOwYbA==",
|
||||
"requires": {
|
||||
"get-nonce": "^1.0.0",
|
||||
"invariant": "^2.2.4",
|
||||
"tslib": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"react-toastify": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-8.1.0.tgz",
|
||||
|
@ -3143,8 +3366,7 @@
|
|||
"tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
},
|
||||
"tsutils": {
|
||||
"version": "3.21.0",
|
||||
|
@ -3196,6 +3418,20 @@
|
|||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"use-callback-ref": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.2.5.tgz",
|
||||
"integrity": "sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg=="
|
||||
},
|
||||
"use-sidecar": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.0.5.tgz",
|
||||
"integrity": "sha512-k9jnrjYNwN6xYLj1iaGhonDghfvmeTmYjAiGvOr7clwKfPjMXJf4/HOr7oT5tJwYafgp2tG2l3eZEOfoELiMcA==",
|
||||
"requires": {
|
||||
"detect-node-es": "^1.1.0",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"v8-compile-cache": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
|
||||
|
|
|
@ -3,8 +3,11 @@
|
|||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@billjs/event-emitter": "^1.0.3",
|
||||
"@fontsource/space-mono": "^4.5.0",
|
||||
"@radix-ui/colors": "^0.1.8",
|
||||
"@radix-ui/react-alert-dialog": "^0.1.5",
|
||||
"@radix-ui/react-checkbox": "^0.1.4",
|
||||
"@radix-ui/react-dialog": "^0.1.5",
|
||||
"@radix-ui/react-icons": "^1.0.3",
|
||||
"@radix-ui/react-label": "^0.1.3",
|
||||
"@radix-ui/react-tabs": "^0.1.4",
|
||||
|
|
|
@ -4,6 +4,7 @@ import { Provider } from 'react-redux';
|
|||
import { BrowserRouter } from 'react-router-dom';
|
||||
|
||||
import 'inter-ui/inter.css';
|
||||
import '@fontsource/space-mono/index.css';
|
||||
import 'normalize.css/normalize.css';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import 'overlayscrollbars/css/OverlayScrollbars.css';
|
||||
|
|
|
@ -104,14 +104,40 @@
|
|||
"title": "Bot commands",
|
||||
"desc": "Define custom chat commands to set up autoresponders, counters, etc.",
|
||||
"add-button": "New command",
|
||||
"search-placeholder": "Search command by name"
|
||||
"search-placeholder": "Search command by name",
|
||||
"command-header-new": "New command",
|
||||
"command-header-edit": "Edit command",
|
||||
"command-name": "Command name",
|
||||
"command-name-placeholder": "!command",
|
||||
"command-desc": "Description (optional)",
|
||||
"command-desc-placeholder": "This command does something",
|
||||
"command-response": "Response",
|
||||
"command-response-placeholder": "Hello {0}!",
|
||||
"command-acl": "Access level",
|
||||
"command-acl-help": "This specifies the minimum level, eg. if you choose VIPs, moderators and streamer can still use the command",
|
||||
"command-action-new": "Create",
|
||||
"command-action-edit": "Edit",
|
||||
"acl": {
|
||||
"everyone": "Everyone",
|
||||
"subscribers": "Subscribers",
|
||||
"vip": "VIPs",
|
||||
"moderators": "Moderators",
|
||||
"streamer": "Streamer only"
|
||||
},
|
||||
"remove-command-title": "Remove command {{name}}?"
|
||||
}
|
||||
},
|
||||
"form-actions": {
|
||||
"save": "Save",
|
||||
"saving": "Saving...",
|
||||
"saved": "Saved",
|
||||
"error": "Error"
|
||||
"error": "Error",
|
||||
"edit": "Edit",
|
||||
"enable": "Enable",
|
||||
"disable": "Disable",
|
||||
"delete": "Delete",
|
||||
"cancel": "Cancel",
|
||||
"ok": "OK"
|
||||
},
|
||||
"debug": {
|
||||
"dev-build": "Development build"
|
||||
|
|
|
@ -25,12 +25,15 @@ interface TwitchBotConfig {
|
|||
chat_history: number;
|
||||
}
|
||||
|
||||
export type AccessLevelType =
|
||||
| 'everyone'
|
||||
| 'subscribers'
|
||||
| 'vip'
|
||||
| 'moderators'
|
||||
| 'streamer';
|
||||
export const accessLevels = [
|
||||
'everyone',
|
||||
'subscribers',
|
||||
'vip',
|
||||
'moderators',
|
||||
'streamer',
|
||||
] as const;
|
||||
|
||||
export type AccessLevelType = typeof accessLevels[number];
|
||||
|
||||
export interface TwitchBotCustomCommand {
|
||||
description: string;
|
||||
|
|
74
frontend/src/ui/components/AlertContent.tsx
Normal file
74
frontend/src/ui/components/AlertContent.tsx
Normal file
|
@ -0,0 +1,74 @@
|
|||
import React from 'react';
|
||||
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
|
||||
import { VariantProps } from '@stitches/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
AlertOverlay,
|
||||
AlertContainer,
|
||||
AlertTitle,
|
||||
AlertDescription,
|
||||
AlertActions,
|
||||
AlertAction,
|
||||
AlertCancel,
|
||||
} from '../theme/alert';
|
||||
import { Button } from '../theme';
|
||||
|
||||
export interface DialogProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
actionText?: string;
|
||||
showCancel?: boolean;
|
||||
cancelText?: string;
|
||||
actionButtonProps?: VariantProps<typeof Button>;
|
||||
variation?: 'default' | 'danger';
|
||||
onAction?: () => void;
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
actionText,
|
||||
actionButtonProps,
|
||||
showCancel,
|
||||
cancelText,
|
||||
variation,
|
||||
onAction,
|
||||
}: React.PropsWithChildren<DialogProps>) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<AlertDialogPrimitive.Portal>
|
||||
<AlertOverlay />
|
||||
<AlertContainer variation={variation ?? 'default'}>
|
||||
{title && (
|
||||
<AlertTitle variation={variation ?? 'default'}>{title}</AlertTitle>
|
||||
)}
|
||||
{description && (
|
||||
<AlertDescription variation={variation ?? 'default'}>
|
||||
{description}
|
||||
</AlertDescription>
|
||||
)}
|
||||
{children}
|
||||
<AlertActions>
|
||||
<AlertAction asChild>
|
||||
<Button
|
||||
variation="primary"
|
||||
{...actionButtonProps}
|
||||
onClick={() => (onAction ? onAction() : null)}
|
||||
>
|
||||
{actionText || t('form-actions.ok')}
|
||||
</Button>
|
||||
</AlertAction>
|
||||
{showCancel && (
|
||||
<AlertCancel asChild>
|
||||
<Button>{cancelText || t('form-actions.cancel')}</Button>
|
||||
</AlertCancel>
|
||||
)}
|
||||
</AlertActions>
|
||||
</AlertContainer>
|
||||
</AlertDialogPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(DialogContent);
|
41
frontend/src/ui/components/DialogContent.tsx
Normal file
41
frontend/src/ui/components/DialogContent.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import React from 'react';
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { Cross2Icon } from '@radix-ui/react-icons';
|
||||
import {
|
||||
DialogOverlay,
|
||||
DialogContainer,
|
||||
IconButton,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from '../theme';
|
||||
|
||||
export interface DialogProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
closeButton?: boolean;
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
}: React.PropsWithChildren<DialogProps>) {
|
||||
return (
|
||||
<DialogPrimitive.Portal>
|
||||
<DialogOverlay />
|
||||
<DialogContainer>
|
||||
{title && <DialogTitle>{title}</DialogTitle>}
|
||||
{description && <DialogDescription>{description}</DialogDescription>}
|
||||
{children}
|
||||
|
||||
<DialogPrimitive.DialogClose asChild>
|
||||
<IconButton>
|
||||
<Cross2Icon />
|
||||
</IconButton>
|
||||
</DialogPrimitive.DialogClose>
|
||||
</DialogContainer>
|
||||
</DialogPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(DialogContent);
|
|
@ -27,7 +27,7 @@ function SaveButton(
|
|||
</Button>
|
||||
);
|
||||
default:
|
||||
return <Button>{t('form-actions.save')}</Button>;
|
||||
return <Button variation="primary">{t('form-actions.save')}</Button>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -183,7 +183,7 @@ function WebhookIntegration() {
|
|||
return (
|
||||
<>
|
||||
<p>{t('pages.stulbe.auth-message')}</p>
|
||||
<Button onClick={startAuthFlow} disabled={!client}>
|
||||
<Button variation="primary" onClick={startAuthFlow} disabled={!client}>
|
||||
<ExternalLinkIcon /> {t('pages.stulbe.auth-button')}
|
||||
</Button>
|
||||
<SectionHeader>{t('pages.stulbe.current-status')}</SectionHeader>
|
||||
|
@ -191,7 +191,7 @@ function WebhookIntegration() {
|
|||
<SectionHeader>{t('pages.stulbe.sim-events')}</SectionHeader>
|
||||
<ButtonGroup>
|
||||
{Object.keys(eventSubTestFn).map((ev: keyof typeof eventsubTests) => (
|
||||
<Button onClick={() => sendFakeEvent(ev)}>
|
||||
<Button key={ev} onClick={() => sendFakeEvent(ev)}>
|
||||
{t(`pages.stulbe.sim.${ev}`, { defaultValue: ev })}
|
||||
</Button>
|
||||
))}
|
||||
|
|
|
@ -1,18 +1,330 @@
|
|||
import { PlusIcon } from '@radix-ui/react-icons';
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useModule } from '../../lib/react-utils';
|
||||
import { modules } from '../../store/api/reducer';
|
||||
import {
|
||||
accessLevels,
|
||||
AccessLevelType,
|
||||
TwitchBotCustomCommand,
|
||||
} from '../../store/api/types';
|
||||
import AlertContent from '../components/AlertContent';
|
||||
import DialogContent from '../components/DialogContent';
|
||||
import {
|
||||
Button,
|
||||
ComboBox,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogClose,
|
||||
Field,
|
||||
FieldNote,
|
||||
FlexRow,
|
||||
InputBox,
|
||||
Label,
|
||||
PageContainer,
|
||||
PageHeader,
|
||||
PageTitle,
|
||||
styled,
|
||||
Textarea,
|
||||
TextBlock,
|
||||
} from '../theme';
|
||||
import { Alert, AlertTrigger } from '../theme/alert';
|
||||
|
||||
const CommandList = styled('div', { marginTop: '1rem' });
|
||||
const CommandItemContainer = styled('article', {
|
||||
backgroundColor: '$gray2',
|
||||
margin: '0.5rem 0',
|
||||
padding: '0.5rem',
|
||||
borderLeft: '5px solid $teal8',
|
||||
borderRadius: '0.25rem',
|
||||
borderBottom: '1px solid $gray4',
|
||||
transition: 'all 50ms',
|
||||
'&:hover': {
|
||||
backgroundColor: '$gray3',
|
||||
},
|
||||
variants: {
|
||||
status: {
|
||||
enabled: {},
|
||||
disabled: {
|
||||
borderLeftColor: '$red7',
|
||||
backgroundColor: '$gray3',
|
||||
color: '$gray10',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const CommandHeader = styled('header', {
|
||||
display: 'flex',
|
||||
gap: '0.5rem',
|
||||
alignItems: 'center',
|
||||
marginBottom: '0.4rem',
|
||||
});
|
||||
const CommandName = styled('span', {
|
||||
color: '$teal10',
|
||||
fontWeight: 'bold',
|
||||
variants: {
|
||||
status: {
|
||||
enabled: {},
|
||||
disabled: {
|
||||
color: '$gray10',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const CommandDescription = styled('span', {
|
||||
flex: 1,
|
||||
});
|
||||
const CommandActions = styled('div', {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.25rem',
|
||||
});
|
||||
const CommandText = styled('div', {
|
||||
fontFamily: 'Space Mono',
|
||||
fontSize: '10pt',
|
||||
margin: '-0.5rem',
|
||||
marginTop: '0',
|
||||
padding: '0.5rem',
|
||||
backgroundColor: '$gray4',
|
||||
lineHeight: '1.2rem',
|
||||
});
|
||||
const ACLIndicator = styled('span', {
|
||||
fontFamily: 'Space Mono',
|
||||
fontSize: '10pt',
|
||||
marginRight: '0.5rem',
|
||||
});
|
||||
|
||||
interface CommandItemProps {
|
||||
name: string;
|
||||
item: TwitchBotCustomCommand;
|
||||
onToggle?: () => void;
|
||||
onEdit?: () => void;
|
||||
onDelete?: () => void;
|
||||
}
|
||||
|
||||
function CommandItemEl({
|
||||
name,
|
||||
item,
|
||||
onToggle,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}: CommandItemProps): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<CommandItemContainer status={item.enabled ? 'enabled' : 'disabled'}>
|
||||
<CommandHeader>
|
||||
<CommandName status={item.enabled ? 'enabled' : 'disabled'}>
|
||||
{name}
|
||||
</CommandName>
|
||||
<CommandDescription>{item.description}</CommandDescription>
|
||||
<CommandActions>
|
||||
{item.access_level !== 'everyone' && (
|
||||
<ACLIndicator>
|
||||
{t(`pages.botcommands.acl.${item.access_level}`)}
|
||||
{item.access_level !== 'streamer' && '+'}
|
||||
</ACLIndicator>
|
||||
)}
|
||||
<Button
|
||||
styling="link"
|
||||
size="small"
|
||||
onClick={() => (onToggle ? onToggle() : null)}
|
||||
>
|
||||
{t(item.enabled ? 'form-actions.disable' : 'form-actions.enable')}
|
||||
</Button>
|
||||
<Button
|
||||
styling="link"
|
||||
size="small"
|
||||
onClick={() => (onEdit ? onEdit() : null)}
|
||||
>
|
||||
{t('form-actions.edit')}
|
||||
</Button>
|
||||
<Alert>
|
||||
<AlertTrigger asChild>
|
||||
<Button styling="link" size="small">
|
||||
{t('form-actions.delete')}
|
||||
</Button>
|
||||
</AlertTrigger>
|
||||
<AlertContent
|
||||
variation="danger"
|
||||
title={t('pages.botcommands.remove-command-title', { name })}
|
||||
description="This cannot be undone"
|
||||
actionText="Delete"
|
||||
actionButtonProps={{ variation: 'danger' }}
|
||||
showCancel={true}
|
||||
onAction={() => (onDelete ? onDelete() : null)}
|
||||
/>
|
||||
</Alert>
|
||||
</CommandActions>
|
||||
</CommandHeader>
|
||||
<CommandText>{item.response}</CommandText>
|
||||
</CommandItemContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const CommandItem = React.memo(CommandItemEl);
|
||||
|
||||
type DialogPrompt =
|
||||
| { kind: 'new' }
|
||||
| { kind: 'edit'; name: string; item: TwitchBotCustomCommand };
|
||||
|
||||
function CommandDialog({
|
||||
kind,
|
||||
name,
|
||||
item,
|
||||
onSubmit,
|
||||
}: {
|
||||
kind: 'new' | 'edit';
|
||||
name?: string;
|
||||
item?: TwitchBotCustomCommand;
|
||||
onSubmit?: (name: string, item: TwitchBotCustomCommand) => void;
|
||||
}) {
|
||||
const [commandName, setCommandName] = useState(name ?? '');
|
||||
const [description, setDescription] = useState(item?.description ?? '');
|
||||
const [response, setResponse] = useState(item?.response ?? '');
|
||||
const [accessLevel, setAccessLevel] = useState(
|
||||
item?.access_level ?? 'everyone',
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<DialogContent title={t(`pages.botcommands.command-header-${kind}`)}>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (onSubmit) {
|
||||
onSubmit(commandName, {
|
||||
...item,
|
||||
description,
|
||||
response,
|
||||
access_level: accessLevel as AccessLevelType,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Field spacing="narrow" size="fullWidth">
|
||||
<Label htmlFor="command-name">
|
||||
{t('pages.botcommands.command-name')}
|
||||
</Label>
|
||||
<InputBox
|
||||
id="command-name"
|
||||
value={commandName}
|
||||
onChange={(e) => setCommandName(e.target.value)}
|
||||
placeholder={t('pages.botcommands.command-name-placeholder')}
|
||||
/>
|
||||
</Field>
|
||||
<Field spacing="narrow" size="fullWidth">
|
||||
<Label htmlFor="command-description">
|
||||
{t('pages.botcommands.command-desc')}
|
||||
</Label>
|
||||
<InputBox
|
||||
id="command-description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder={t('pages.botcommands.command-desc-placeholder')}
|
||||
/>
|
||||
</Field>
|
||||
<Field spacing="narrow" size="fullWidth">
|
||||
<Label htmlFor="command-response">
|
||||
{t('pages.botcommands.command-response')}
|
||||
</Label>
|
||||
<Textarea
|
||||
value={response}
|
||||
onChange={(e) => setResponse(e.target.value)}
|
||||
id="command-response"
|
||||
placeholder={t('pages.botcommands.command-response-placeholder')}
|
||||
>
|
||||
{item?.response}
|
||||
</Textarea>
|
||||
</Field>
|
||||
<Field spacing="narrow" size="fullWidth">
|
||||
<Label htmlFor="command-acl">
|
||||
{t('pages.botcommands.command-acl')}
|
||||
</Label>
|
||||
<ComboBox
|
||||
id="command-acl"
|
||||
value={accessLevel}
|
||||
onChange={(e) => setAccessLevel(e.target.value as AccessLevelType)}
|
||||
>
|
||||
{accessLevels.map((level) => (
|
||||
<option key={level} value={level}>
|
||||
{t(`pages.botcommands.acl.${level}`)}
|
||||
</option>
|
||||
))}
|
||||
</ComboBox>
|
||||
<FieldNote>{t('pages.botcommands.command-acl-help')}</FieldNote>
|
||||
</Field>
|
||||
<DialogActions>
|
||||
<Button variation="primary">
|
||||
{t(`pages.botcommands.command-action-${kind}`)}
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button type="button">{t('form-actions.cancel')}</Button>
|
||||
</DialogClose>
|
||||
</DialogActions>
|
||||
</form>
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TwitchBotCommandsPage(): React.ReactElement {
|
||||
const [botCommands, setBotCommands] = useModule(modules.twitchBotCommands);
|
||||
const [commandFilter, setCommandFilter] = useState('');
|
||||
const [activeDialog, setActiveDialog] = useState<DialogPrompt>(null);
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const commandFilterLC = commandFilter.toLowerCase();
|
||||
|
||||
const setCommand = (newName: string, data: TwitchBotCustomCommand): void => {
|
||||
switch (activeDialog.kind) {
|
||||
case 'new':
|
||||
dispatch(
|
||||
setBotCommands({
|
||||
...botCommands,
|
||||
[newName]: {
|
||||
...data,
|
||||
enabled: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case 'edit': {
|
||||
const oldName = activeDialog.name;
|
||||
dispatch(
|
||||
setBotCommands({
|
||||
...botCommands,
|
||||
[oldName]: undefined,
|
||||
[newName]: data,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
setActiveDialog(null);
|
||||
};
|
||||
|
||||
const deleteCommand = (cmd: string): void => {
|
||||
dispatch(
|
||||
setBotCommands({
|
||||
...botCommands,
|
||||
[cmd]: undefined,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const toggleCommand = (cmd: string): void => {
|
||||
dispatch(
|
||||
setBotCommands({
|
||||
...botCommands,
|
||||
[cmd]: {
|
||||
...botCommands[cmd],
|
||||
enabled: !botCommands[cmd].enabled,
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
|
@ -22,14 +334,58 @@ export default function TwitchBotCommandsPage(): React.ReactElement {
|
|||
</PageHeader>
|
||||
|
||||
<FlexRow spacing="1" align="left">
|
||||
<Button>
|
||||
<Button
|
||||
variation="primary"
|
||||
onClick={() => setActiveDialog({ kind: 'new' })}
|
||||
>
|
||||
<PlusIcon /> {t('pages.botcommands.add-button')}
|
||||
</Button>
|
||||
|
||||
<InputBox
|
||||
css={{ flex: 1 }}
|
||||
placeholder={t('pages.botcommands.search-placeholder')}
|
||||
value={commandFilter}
|
||||
onChange={(e) => setCommandFilter(e.target.value)}
|
||||
/>
|
||||
</FlexRow>
|
||||
<CommandList>
|
||||
{Object.keys(botCommands ?? {})
|
||||
?.filter((cmd) => cmd.toLowerCase().includes(commandFilterLC))
|
||||
.sort()
|
||||
.map((cmd) => (
|
||||
<CommandItem
|
||||
key={cmd}
|
||||
name={cmd}
|
||||
item={botCommands[cmd]}
|
||||
onToggle={() => toggleCommand(cmd)}
|
||||
onEdit={() =>
|
||||
setActiveDialog({
|
||||
kind: 'edit',
|
||||
name: cmd,
|
||||
item: botCommands[cmd],
|
||||
})
|
||||
}
|
||||
onDelete={() => deleteCommand(cmd)}
|
||||
/>
|
||||
))}
|
||||
</CommandList>
|
||||
|
||||
<Dialog
|
||||
open={!!activeDialog}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
// Reset dialog status on dialog close
|
||||
setActiveDialog(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{activeDialog && (
|
||||
<CommandDialog
|
||||
{...activeDialog}
|
||||
onSubmit={(name, data) => setCommand(name, data)}
|
||||
/>
|
||||
)}
|
||||
</Dialog>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
|
118
frontend/src/ui/theme/alert.ts
Normal file
118
frontend/src/ui/theme/alert.ts
Normal file
|
@ -0,0 +1,118 @@
|
|||
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
|
||||
import { keyframes } from '@stitches/react';
|
||||
import { styled } from './theme';
|
||||
|
||||
export const Alert = AlertDialogPrimitive.Root;
|
||||
export const AlertTrigger = AlertDialogPrimitive.Trigger;
|
||||
export const AlertAction = AlertDialogPrimitive.AlertDialogAction;
|
||||
export const AlertCancel = AlertDialogPrimitive.AlertDialogCancel;
|
||||
|
||||
const overlayShow = keyframes({
|
||||
'0%': { opacity: 0 },
|
||||
'100%': { opacity: 1 },
|
||||
});
|
||||
|
||||
const contentShow = keyframes({
|
||||
'0%': { opacity: 0, transform: 'translate(-50%, -48%) scale(.96)' },
|
||||
'100%': { opacity: 1, transform: 'translate(-50%, -50%) scale(1)' },
|
||||
});
|
||||
|
||||
export const AlertOverlay = styled(AlertDialogPrimitive.Overlay, {
|
||||
backgroundColor: '$blackA10',
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
'@media (prefers-reduced-motion: no-preference)': {
|
||||
animation: `${overlayShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
|
||||
},
|
||||
});
|
||||
|
||||
export const AlertContainer = styled(AlertDialogPrimitive.Content, {
|
||||
backgroundColor: '$gray2',
|
||||
borderRadius: '0.25rem',
|
||||
boxShadow:
|
||||
'hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px',
|
||||
position: 'fixed',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: '90vw',
|
||||
maxWidth: '600px',
|
||||
maxHeight: '85vh',
|
||||
padding: '1rem',
|
||||
'@media (prefers-reduced-motion: no-preference)': {
|
||||
animation: `${contentShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
|
||||
},
|
||||
border: '2px solid $teal8',
|
||||
'&:focus': { outline: 'none' },
|
||||
variants: {
|
||||
variation: {
|
||||
default: {},
|
||||
danger: {
|
||||
borderColor: '$red8',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const AlertTitle = styled(AlertDialogPrimitive.Title, {
|
||||
fontWeight: 'bold',
|
||||
color: '$teal12',
|
||||
fontSize: '15pt',
|
||||
borderBottom: '1px solid $teal6',
|
||||
margin: '-1rem',
|
||||
marginBottom: '1.5rem',
|
||||
padding: '1rem',
|
||||
lineHeight: '1.25',
|
||||
variants: {
|
||||
variation: {
|
||||
default: {},
|
||||
danger: {
|
||||
borderBottomColor: '$red6',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const AlertDescription = styled(AlertDialogPrimitive.Description, {
|
||||
margin: '10px 0 20px',
|
||||
color: '$teal12',
|
||||
fontSize: 15,
|
||||
lineHeight: 1.5,
|
||||
variants: {
|
||||
variation: {
|
||||
default: {},
|
||||
danger: {
|
||||
borderBottomColor: '$red12',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const AlertActions = styled('div', {
|
||||
display: 'flex',
|
||||
gap: '0.5rem',
|
||||
justifyContent: 'flex-end',
|
||||
borderTop: '1px solid $gray6',
|
||||
margin: '-1rem',
|
||||
marginTop: '1.5rem',
|
||||
padding: '1rem 1.5rem',
|
||||
});
|
||||
|
||||
export const IconButton = styled('button', {
|
||||
all: 'unset',
|
||||
fontFamily: 'inherit',
|
||||
borderRadius: '100%',
|
||||
height: 25,
|
||||
width: 25,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '$teal11',
|
||||
position: 'absolute',
|
||||
cursor: 'pointer',
|
||||
top: 15,
|
||||
right: 15,
|
||||
|
||||
'&:hover': { backgroundColor: '$teal4' },
|
||||
'&:focus': { boxShadow: `0 0 0 2px $teal7` },
|
||||
});
|
92
frontend/src/ui/theme/dialog.ts
Normal file
92
frontend/src/ui/theme/dialog.ts
Normal file
|
@ -0,0 +1,92 @@
|
|||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { keyframes } from '@stitches/react';
|
||||
import { styled } from './theme';
|
||||
|
||||
export const Dialog = DialogPrimitive.Root;
|
||||
export const DialogTrigger = DialogPrimitive.Trigger;
|
||||
export const DialogClose = DialogPrimitive.Close;
|
||||
|
||||
const overlayShow = keyframes({
|
||||
'0%': { opacity: 0 },
|
||||
'100%': { opacity: 1 },
|
||||
});
|
||||
|
||||
const contentShow = keyframes({
|
||||
'0%': { opacity: 0, transform: 'translate(-50%, -48%) scale(.96)' },
|
||||
'100%': { opacity: 1, transform: 'translate(-50%, -50%) scale(1)' },
|
||||
});
|
||||
|
||||
export const DialogOverlay = styled(DialogPrimitive.Overlay, {
|
||||
backgroundColor: '$blackA10',
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
'@media (prefers-reduced-motion: no-preference)': {
|
||||
animation: `${overlayShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
|
||||
},
|
||||
});
|
||||
|
||||
export const DialogContainer = styled(DialogPrimitive.Content, {
|
||||
backgroundColor: '$gray2',
|
||||
borderRadius: '0.25rem',
|
||||
boxShadow:
|
||||
'hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px',
|
||||
position: 'fixed',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: '90vw',
|
||||
maxWidth: '600px',
|
||||
maxHeight: '85vh',
|
||||
padding: '1rem',
|
||||
'@media (prefers-reduced-motion: no-preference)': {
|
||||
animation: `${contentShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
|
||||
},
|
||||
'&:focus': { outline: 'none' },
|
||||
});
|
||||
|
||||
export const DialogTitle = styled(DialogPrimitive.Title, {
|
||||
fontWeight: 'bold',
|
||||
color: '$teal12',
|
||||
fontSize: '15pt',
|
||||
borderBottom: '1px solid $teal6',
|
||||
margin: '-1rem',
|
||||
marginBottom: '1.5rem',
|
||||
padding: '1rem',
|
||||
lineHeight: '1.25',
|
||||
});
|
||||
|
||||
export const DialogDescription = styled(DialogPrimitive.Description, {
|
||||
margin: '10px 0 20px',
|
||||
color: '$teal11',
|
||||
fontSize: 15,
|
||||
lineHeight: 1.5,
|
||||
});
|
||||
|
||||
export const DialogActions = styled('div', {
|
||||
display: 'flex',
|
||||
gap: '0.5rem',
|
||||
justifyContent: 'flex-end',
|
||||
borderTop: '1px solid $gray6',
|
||||
margin: '-1rem',
|
||||
marginTop: '1.5rem',
|
||||
padding: '1rem 1.5rem',
|
||||
});
|
||||
|
||||
export const IconButton = styled('button', {
|
||||
all: 'unset',
|
||||
fontFamily: 'inherit',
|
||||
borderRadius: '100%',
|
||||
height: 25,
|
||||
width: 25,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '$teal11',
|
||||
position: 'absolute',
|
||||
cursor: 'pointer',
|
||||
top: 15,
|
||||
right: 15,
|
||||
|
||||
'&:hover': { backgroundColor: '$teal4' },
|
||||
'&:focus': { boxShadow: `0 0 0 2px $teal7` },
|
||||
});
|
|
@ -10,6 +10,11 @@ export const Field = styled('fieldset', {
|
|||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
variants: {
|
||||
spacing: {
|
||||
narrow: {
|
||||
marginBottom: '1rem',
|
||||
},
|
||||
},
|
||||
size: {
|
||||
fullWidth: {
|
||||
flexDirection: 'column',
|
||||
|
@ -56,6 +61,27 @@ export const InputBox = styled('input', {
|
|||
},
|
||||
});
|
||||
|
||||
export const Textarea = styled('textarea', {
|
||||
all: 'unset',
|
||||
fontWeight: '300',
|
||||
border: '1px solid $gray6',
|
||||
padding: '0.5rem',
|
||||
borderRadius: '0.3rem',
|
||||
backgroundColor: '$gray2',
|
||||
'&:hover': {
|
||||
borderColor: '$teal7',
|
||||
},
|
||||
'&:focus': {
|
||||
borderColor: '$teal7',
|
||||
backgroundColor: '$gray3',
|
||||
},
|
||||
'&:disabled': {
|
||||
backgroundColor: '$gray4',
|
||||
borderColor: '$gray5',
|
||||
color: '$gray8',
|
||||
},
|
||||
});
|
||||
|
||||
export const ButtonGroup = styled('div', {
|
||||
display: 'flex',
|
||||
gap: '0.5rem',
|
||||
|
@ -68,25 +94,52 @@ export const Button = styled('button', {
|
|||
padding: '0.5rem 1rem',
|
||||
borderRadius: '0.3rem',
|
||||
fontSize: '1.1rem',
|
||||
border: '1px solid $teal6',
|
||||
backgroundColor: '$teal4',
|
||||
border: '1px solid $gray6',
|
||||
backgroundColor: '$gray4',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
'&:hover': {
|
||||
backgroundColor: '$gray5',
|
||||
borderColor: '$gray8',
|
||||
},
|
||||
'&:active': {
|
||||
background: '$gray6',
|
||||
},
|
||||
transition: 'all 0.2s',
|
||||
variants: {
|
||||
styling: {
|
||||
link: {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: '$teal11',
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
},
|
||||
size: {
|
||||
small: {
|
||||
padding: '0.3rem 0.5rem',
|
||||
fontSize: '0.9rem',
|
||||
},
|
||||
},
|
||||
variation: {
|
||||
primary: {
|
||||
border: '1px solid $teal6',
|
||||
backgroundColor: '$teal4',
|
||||
'&:hover': {
|
||||
backgroundColor: '$teal5',
|
||||
borderColor: '$teal8',
|
||||
},
|
||||
'&:active': {
|
||||
background: '$teal6',
|
||||
},
|
||||
transition: 'all 0.2s',
|
||||
variants: {
|
||||
variation: {
|
||||
},
|
||||
success: {
|
||||
border: '1px solid $grass6',
|
||||
backgroundColor: '$grass4',
|
||||
'&:hover': {
|
||||
backgroundColor: '$grass5',
|
||||
borderColor: '$grass8',
|
||||
},
|
||||
'&:active': {
|
||||
background: '$grass6',
|
||||
|
@ -97,6 +150,18 @@ export const Button = styled('button', {
|
|||
backgroundColor: '$red4',
|
||||
'&:hover': {
|
||||
backgroundColor: '$red5',
|
||||
borderColor: '$red8',
|
||||
},
|
||||
'&:active': {
|
||||
background: '$red6',
|
||||
},
|
||||
},
|
||||
danger: {
|
||||
border: '1px solid $red6',
|
||||
backgroundColor: '$red4',
|
||||
'&:hover': {
|
||||
backgroundColor: '$red5',
|
||||
borderColor: '$red8',
|
||||
},
|
||||
'&:active': {
|
||||
background: '$red6',
|
||||
|
@ -106,6 +171,28 @@ export const Button = styled('button', {
|
|||
},
|
||||
});
|
||||
|
||||
export const ComboBox = styled('select', {
|
||||
margin: 0,
|
||||
color: '$teal13',
|
||||
fontWeight: '300',
|
||||
border: '1px solid $gray6',
|
||||
padding: '0.5rem',
|
||||
borderRadius: '0.3rem',
|
||||
backgroundColor: '$gray2',
|
||||
'&:hover': {
|
||||
borderColor: '$teal7',
|
||||
},
|
||||
'&:focus': {
|
||||
borderColor: '$teal7',
|
||||
backgroundColor: '$gray3',
|
||||
},
|
||||
'&:disabled': {
|
||||
backgroundColor: '$gray4',
|
||||
borderColor: '$gray5',
|
||||
color: '$gray8',
|
||||
},
|
||||
});
|
||||
|
||||
export const Checkbox = styled(CheckboxPrimitive.Root, {
|
||||
all: 'unset',
|
||||
width: 25,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
export * from './theme';
|
||||
export * from './brand';
|
||||
export * from './dialog';
|
||||
export * from './forms';
|
||||
export * from './pages';
|
||||
export * from './tabs';
|
||||
export * from './theme';
|
||||
export * from './utils';
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
yellowDark,
|
||||
grassDark,
|
||||
redDark,
|
||||
blackA,
|
||||
} from '@radix-ui/colors';
|
||||
import { globalCss, createStitches } from '@stitches/react';
|
||||
|
||||
|
@ -34,6 +35,7 @@ export const { styled, theme } = createStitches({
|
|||
...yellowDark,
|
||||
...grassDark,
|
||||
...redDark,
|
||||
...blackA,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue