Merge branch 'master' into metamask2

partest
bunsenstraat 3 weeks ago committed by GitHub
commit ef52d4ed2f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 116
      apps/remix-ide-e2e/src/tests/script-runner.test.ts
  2. 7
      apps/remix-ide/src/app.js
  3. 6
      apps/remix-ide/src/app/components/hidden-panel.tsx
  4. 8
      apps/remix-ide/src/app/panels/terminal.tsx
  5. 2
      apps/remix-ide/src/app/plugins/matomo.ts
  6. 2
      apps/remix-ide/src/app/tabs/compile-and-run.ts
  7. 1
      apps/remix-ide/src/app/tabs/locales/en/remixUiTabs.json
  8. 399
      apps/remix-ide/src/app/tabs/script-runner-ui.tsx
  9. 282
      apps/remix-ide/src/assets/fontawesome/css/all.css
  10. BIN
      apps/remix-ide/src/assets/fontawesome/webfonts/custom-icons.ttf
  11. BIN
      apps/remix-ide/src/assets/fontawesome/webfonts/custom-icons.woff2
  12. 1
      apps/remix-ide/src/assets/img/solid-gear-circle-play.svg
  13. 10
      apps/remix-ide/src/remixAppManager.js
  14. 1
      apps/remix-ide/src/remixEngine.js
  15. 1
      libs/remix-api/src/lib/plugins/fileSystem-api.ts
  16. 14
      libs/remix-api/src/lib/plugins/menuicons-api.ts
  17. 2
      libs/remix-api/src/lib/remix-api.ts
  18. 5
      libs/remix-simulator/src/methods/accounts.ts
  19. 10
      libs/remix-simulator/src/methods/transactions.ts
  20. 2
      libs/remix-ui/scriptrunner/src/index.ts
  21. 168
      libs/remix-ui/scriptrunner/src/lib/custom-script-runner.tsx
  22. 103
      libs/remix-ui/scriptrunner/src/lib/script-runner-ui.tsx
  23. 37
      libs/remix-ui/scriptrunner/src/types/index.ts
  24. 20
      libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx
  25. 8
      libs/remix-ui/terminal/src/lib/actions/terminalAction.ts
  26. 2
      libs/remix-ui/terminal/src/lib/remix-ui-terminal.tsx
  27. 2
      libs/remix-ui/workspace/src/lib/actions/index.tsx
  28. 3
      tsconfig.paths.json

@ -0,0 +1,116 @@
'use strict'
import { NightwatchBrowser } from 'nightwatch'
import init from '../helpers/init'
module.exports = {
before: function (browser: NightwatchBrowser, done: VoidFunction) {
init(browser, done, 'http://127.0.0.1:8080', false)
},
'Should load default script runner': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('scriptRunnerBridge')
.waitForElementVisible('[data-id="sr-loaded-default"]')
.waitForElementVisible('[data-id="dependency-ethers-^5"]')
.waitForElementVisible('[data-id="sr-toggle-ethers6"]')
},
'Should load script runner ethers6': function (browser: NightwatchBrowser) {
browser
.click('[data-id="sr-toggle-ethers6"]')
.waitForElementVisible('[data-id="sr-loaded-ethers6"]')
.waitForElementPresent('[data-id="dependency-ethers-^6"]')
},
'Should have config file in .remix/script.config.json': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('filePanel')
.waitForElementVisible('[data-path=".remix"]')
.waitForElementVisible('[data-id="treeViewDivDraggableItem.remix/script.config.json"]')
.openFile('.remix/script.config.json')
},
'check config file content': function (browser: NightwatchBrowser) {
browser
.getEditorValue((content) => {
console.log(JSON.parse(content))
const parsed = JSON.parse(content)
browser.assert.ok(parsed.defaultConfig === 'ethers6', 'config file content is correct')
})
},
'execute ethers6 script': function (browser: NightwatchBrowser) {
browser
.click('*[data-id="treeViewUltreeViewMenu"]') // make sure we create the file at the root folder
.addFile('deployWithEthersJs.js', { content: deployWithEthersJs })
.pause(1000)
.click('[data-id="treeViewDivtreeViewItemcontracts"]')
.openFile('contracts/2_Owner.sol')
.clickLaunchIcon('solidity')
.click('*[data-id="compilerContainerCompileBtn"]')
.executeScriptInTerminal('remix.execute(\'deployWithEthersJs.js\')')
.waitForElementContainsText('*[data-id="terminalJournal"]', '0xd9145CCE52D386f254917e481eB44e9943F39138', 60000)
},
'switch workspace it should be default again': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('filePanel')
.pause(2000)
.waitForElementVisible('*[data-id="workspacesMenuDropdown"]')
.click('*[data-id="workspacesMenuDropdown"]')
.click('*[data-id="workspacecreate"]')
.waitForElementPresent('*[data-id="create-semaphore"]')
.scrollAndClick('*[data-id="create-semaphore"]')
.modalFooterOKClick('TemplatesSelection')
.clickLaunchIcon('scriptRunnerBridge')
.waitForElementVisible('[data-id="sr-loaded-default"]')
.waitForElementVisible('[data-id="dependency-ethers-^5"]')
.waitForElementVisible('[data-id="sr-toggle-ethers6"]')
},
'switch to default workspace that should be on ethers6': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('filePanel')
.switchWorkspace('default_workspace')
.clickLaunchIcon('scriptRunnerBridge')
.waitForElementVisible('[data-id="sr-loaded-ethers6"]')
.waitForElementPresent('[data-id="dependency-ethers-^6"]')
}
}
const deployWithEthersJs = `
import { ethers } from 'ethers'
/**
* Deploy the given contract
* @param {string} contractName name of the contract to deploy
* @param {Array<any>} args list of constructor' parameters
* @param {Number} accountIndex account index from the exposed account
* @return {Contract} deployed contract
*
*/
const deploy = async (contractName: string, args: Array<any>, accountIndex?: number): Promise<ethers.Contract> => {
console.log(\`deploying \${contractName}\`)
// Note that the script needs the ABI which is generated from the compilation artifact.
// Make sure contract is compiled and artifacts are generated
const artifactsPath = \`contracts/artifacts/\${contractName}.json\` // Change this for different path
const metadata = JSON.parse(await remix.call('fileManager', 'getFile', artifactsPath))
// 'web3Provider' is a remix global variable object
const signer = await (new ethers.BrowserProvider(web3Provider)).getSigner(accountIndex)
const factory = new ethers.ContractFactory(metadata.abi, metadata.data.bytecode.object, signer)
const contract = await factory.deploy(...args)
// The contract is NOT deployed yet; we must wait until it is mined
await contract.waitForDeployment()
return contract
}
(async () => {
try {
const contract = await deploy('Owner', [])
console.log(\`address: \${await contract.getAddress()}\`)
} catch (e) {
console.log(e.message)
}
})()`

@ -77,6 +77,7 @@ const remixLib = require('@remix-project/remix-lib')
import { QueryParams } from '@remix-project/remix-lib'
import { SearchPlugin } from './app/tabs/search'
import { ScriptRunnerUIPlugin } from './app/tabs/script-runner-ui'
import { ElectronProvider } from './app/files/electronProvider'
const Storage = remixLib.Storage
@ -246,6 +247,9 @@ class AppComponent {
//----- search
const search = new SearchPlugin()
//---------------- Script Runner UI Plugin -------------------------
const scriptRunnerUI = new ScriptRunnerUIPlugin(this.engine)
//---- templates
const templates = new TemplatesPlugin()
@ -396,6 +400,7 @@ class AppComponent {
pluginStateLogger,
matomo,
templateSelection,
scriptRunnerUI,
remixAI
])
@ -649,7 +654,7 @@ class AppComponent {
})
// activate solidity plugin
this.appManager.activatePlugin(['solidity', 'udapp', 'deploy-libraries', 'link-libraries', 'openzeppelin-proxy'])
this.appManager.activatePlugin(['solidity', 'udapp', 'deploy-libraries', 'link-libraries', 'openzeppelin-proxy', 'scriptRunnerBridge'])
}
}

@ -24,10 +24,16 @@ export class HiddenPanel extends AbstractPanel {
addView(profile: any, view: any): void {
super.removeView(profile)
this.renderComponent()
super.addView(profile, view)
this.renderComponent()
}
removeView(profile: any): void {
super.removeView(profile)
this.renderComponent()
}
updateComponent(state: any) {
return <RemixPluginPanel header={<></>} plugins={state.plugins} />
}

@ -117,10 +117,10 @@ class Terminal extends Plugin {
}
onDeactivation() {
this.off('scriptRunner', 'log')
this.off('scriptRunner', 'info')
this.off('scriptRunner', 'warn')
this.off('scriptRunner', 'error')
this.off('scriptRunnerBridge', 'log')
this.off('scriptRunnerBridge', 'info')
this.off('scriptRunnerBridge', 'warn')
this.off('scriptRunnerBridge', 'error')
}
logHtml(html) {

@ -11,7 +11,7 @@ const profile = {
version: '1.0.0'
}
const allowedPlugins = ['LearnEth', 'etherscan', 'vyper', 'circuit-compiler', 'doc-gen', 'doc-viewer', 'solhint', 'walletconnect', 'scriptRunner', 'dgit', 'contract-verification']
const allowedPlugins = ['LearnEth', 'etherscan', 'vyper', 'circuit-compiler', 'doc-gen', 'doc-viewer', 'solhint', 'walletconnect', 'scriptRunner', 'scriptRunnerBridge', 'dgit', 'contract-verification']
export class Matomo extends Plugin {

@ -62,7 +62,7 @@ export class CompileAndRun extends Plugin {
if (clearAllInstances) {
await this.call('udapp', 'clearAllInstances')
}
await this.call('scriptRunner', 'execute', content, fileName)
await this.call('scriptRunnerBridge', 'execute', content, fileName)
} catch (e) {
this.call('notification', 'toast', e.message || e)
}

@ -7,6 +7,7 @@
"remixUiTabs.tooltipText6": "Enable RemixAI Copilot [BETA]",
"remixUiTabs.tooltipText7": "Disable RemixAI Copilot [BETA]",
"remixUiTabs.tooltipText8": "Remix AI Tools Documentation",
"remixUiTabs.tooltipText9": "Configure scripting dependencies",
"remixUiTabs.tooltipTextDisabledCopilot": "To use RemixAI Copilot, choose a .sol file",
"remixUiTabs.zoomOut": "Zoom out",
"remixUiTabs.zoomIn": "Zoom in"

@ -0,0 +1,399 @@
import { IframePlugin, IframeProfile, ViewPlugin } from '@remixproject/engine-web'
import * as packageJson from '../../../../../package.json'
import React from 'react' // eslint-disable-line
import { customScriptRunnerConfig, ProjectConfiguration, ScriptRunnerConfig, ScriptRunnerUI } from '@remix-scriptrunner' // eslint-disable-line
import { Profile } from '@remixproject/plugin-utils'
import { Engine, Plugin } from '@remixproject/engine'
import axios from 'axios'
import { AppModal } from '@remix-ui/app'
import { isArray } from 'lodash'
import { PluginViewWrapper } from '@remix-ui/helper'
import { CustomRemixApi } from '@remix-api'
const profile = {
name: 'scriptRunnerBridge',
displayName: 'Script configuration',
methods: ['execute'],
events: ['log', 'info', 'warn', 'error'],
icon: 'assets/img/solid-gear-circle-play.svg',
description: 'Configure the dependencies for running scripts.',
kind: '',
location: 'sidePanel',
version: packageJson.version,
maintainedBy: 'Remix'
}
const configFileName = '.remix/script.config.json'
let baseUrl = 'https://remix-project-org.github.io/script-runner-generator'
const customBuildUrl = 'http://localhost:4000/build' // this will be used when the server is ready
interface IScriptRunnerState {
customConfig: customScriptRunnerConfig
configurations: ProjectConfiguration[]
activeConfig: ProjectConfiguration
enableCustomScriptRunner: boolean
}
export class ScriptRunnerUIPlugin extends ViewPlugin {
engine: Engine
dispatch: React.Dispatch<any> = () => { }
workspaceScriptRunnerDefaults: Record<string, string>
customConfig: ScriptRunnerConfig
configurations: ProjectConfiguration[]
activeConfig: ProjectConfiguration
enableCustomScriptRunner: boolean
plugin: Plugin<any, CustomRemixApi>
scriptRunnerProfileName: string
constructor(engine: Engine) {
super(profile)
this.engine = engine
this.workspaceScriptRunnerDefaults = {}
this.plugin = this
this.enableCustomScriptRunner = false // implement this later
}
async onActivation() {
this.on('filePanel', 'setWorkspace', async (workspace: string) => {
this.activeConfig = null
this.customConfig =
{
defaultConfig: 'default',
customConfig: {
baseConfiguration: 'default',
dependencies: []
}
}
await this.loadCustomConfig()
await this.loadConfigurations()
this.renderComponent()
})
this.plugin.on('fileManager', 'fileSaved', async (file: string) => {
if (file === configFileName && this.enableCustomScriptRunner) {
await this.loadCustomConfig()
this.renderComponent()
}
})
await this.loadCustomConfig()
await this.loadConfigurations()
this.renderComponent()
}
render() {
return (
<div id="scriptrunnerTab">
<PluginViewWrapper plugin={this} />
</div>
)
}
setDispatch(dispatch: React.Dispatch<any>) {
this.dispatch = dispatch
this.renderComponent()
}
renderComponent() {
this.dispatch({
customConfig: this.customConfig,
configurations: this.configurations,
activeConfig: this.activeConfig,
enableCustomScriptRunner: this.enableCustomScriptRunner
})
}
updateComponent(state: IScriptRunnerState) {
return (
<ScriptRunnerUI
customConfig={state.customConfig}
configurations={state.configurations}
activeConfig={state.activeConfig}
enableCustomScriptRunner={state.enableCustomScriptRunner}
activateCustomScriptRunner={this.activateCustomScriptRunner.bind(this)}
saveCustomConfig={this.saveCustomConfig.bind(this)}
openCustomConfig={this.openCustomConfig.bind(this)}
loadScriptRunner={this.selectScriptRunner.bind(this)} />
)
}
async selectScriptRunner(config: ProjectConfiguration) {
if (await this.loadScriptRunner(config))
await this.saveCustomConfig(this.customConfig)
}
async loadScriptRunner(config: ProjectConfiguration): Promise<boolean> {
const profile: Profile = await this.plugin.call('manager', 'getProfile', 'scriptRunner')
this.scriptRunnerProfileName = profile.name
const testPluginName = localStorage.getItem('test-plugin-name')
const testPluginUrl = localStorage.getItem('test-plugin-url')
let url = `${baseUrl}?template=${config.name}&timestamp=${Date.now()}`
if (testPluginName === 'scriptRunner') {
// if testpluginurl has template specified only use that
if (testPluginUrl.indexOf('template') > -1) {
url = testPluginUrl
} else {
baseUrl = `//${new URL(testPluginUrl).host}`
url = `${baseUrl}?template=${config.name}&timestamp=${Date.now()}`
}
}
//console.log('loadScriptRunner', profile)
const newProfile: IframeProfile = {
...profile,
name: profile.name + config.name,
location: 'hiddenPanel',
url: url
}
let result = null
try {
this.setIsLoading(config.name, true)
const plugin: IframePlugin = new IframePlugin(newProfile)
if (!this.engine.isRegistered(newProfile.name)) {
await this.engine.register(plugin)
}
await this.plugin.call('manager', 'activatePlugin', newProfile.name)
this.activeConfig = config
this.on(newProfile.name, 'log', this.log.bind(this))
this.on(newProfile.name, 'info', this.info.bind(this))
this.on(newProfile.name, 'warn', this.warn.bind(this))
this.on(newProfile.name, 'error', this.error.bind(this))
this.on(newProfile.name, 'dependencyError', this.dependencyError.bind(this))
this.customConfig.defaultConfig = config.name
this.setErrorStatus(config.name, false, '')
result = true
} catch (e) {
console.log('Error loading script runner: ', newProfile.name, e)
const iframe = document.getElementById(`plugin-${newProfile.name}`);
if (iframe) {
await this.call('hiddenPanel', 'removeView', newProfile)
}
delete (this.engine as any).manager.profiles[newProfile.name]
delete (this.engine as any).plugins[newProfile.name]
console.log('Error loading script runner: ', newProfile.name, e)
this.setErrorStatus(config.name, true, e)
result = false
}
this.setIsLoading(config.name, false)
this.renderComponent()
return result
}
async execute(script: string, filePath: string) {
this.call('terminal', 'log', { value: `running ${filePath} ...`, type: 'info' })
if (!this.scriptRunnerProfileName || !this.engine.isRegistered(`${this.scriptRunnerProfileName}${this.activeConfig.name}`)) {
if (!await this.loadScriptRunner(this.activeConfig)) {
console.error('Error loading script runner')
return
}
}
try {
this.setIsLoading(this.activeConfig.name, true)
await this.call(`${this.scriptRunnerProfileName}${this.activeConfig.name}`, 'execute', script, filePath)
} catch (e) {
console.error('Error executing script', e)
}
this.setIsLoading(this.activeConfig.name, false)
}
async setErrorStatus(name: string, status: boolean, error: string) {
this.configurations.forEach((config) => {
if (config.name === name) {
config.errorStatus = status
config.error = error
}
})
this.renderComponent()
}
async setIsLoading(name: string, status: boolean) {
if (status) {
this.emit('statusChanged', {
key: 'loading',
type: 'info',
title: 'loading...'
})
} else {
this.emit('statusChanged', {
key: 'none'
})
}
this.configurations.forEach((config) => {
if (config.name === name) {
config.isLoading = status
}
})
this.renderComponent()
}
async dependencyError(data: any) {
console.log('Script runner dependency error: ', data)
let message = `Error loading dependencies: `
if (isArray(data.data)) {
data.data.forEach((data: any) => {
message += `${data}`
})
}
const modal: AppModal = {
id: 'TemplatesSelection',
title: 'Missing dependencies',
message: `${message} \n\n You may need to setup a script engine for this workspace to load the correct dependencies. Do you want go to setup now?`,
okLabel: window._intl.formatMessage({ id: 'filePanel.ok' }),
cancelLabel: 'ignore'
}
const modalResult = await this.plugin.call('notification' as any, 'modal', modal)
if (modalResult) {
await this.plugin.call('menuicons', 'select', 'scriptRunnerBridge')
} else {
}
}
async log(data: any) {
this.emit('log', data)
}
async warn(data: any) {
this.emit('warn', data)
}
async error(data: any) {
this.emit('error', data)
}
async info(data: any) {
this.emit('info', data)
}
async loadCustomConfig(): Promise<void> {
try {
const content = await this.plugin.call('fileManager', 'readFile', configFileName)
const parsed = JSON.parse(content)
this.customConfig = parsed
} catch (e) {
this.customConfig = {
defaultConfig: 'default',
customConfig: {
baseConfiguration: 'default',
dependencies: []
}
}
}
}
async openCustomConfig() {
try {
await this.plugin.call('fileManager', 'open', '.remix/script.config.json')
} catch (e) {
}
}
async loadConfigurations() {
try {
const response = await axios.get(`${baseUrl}/projects.json?timestamp=${Date.now()}`);
this.configurations = response.data;
// find the default otherwise pick the first one as the active
this.configurations.forEach((config) => {
if (config.name === (this.customConfig.defaultConfig)) {
this.activeConfig = config;
}
});
if (!this.activeConfig) {
this.activeConfig = this.configurations[0];
}
} catch (error) {
console.error("Error fetching the projects data:", error);
}
}
async saveCustomConfig(content: ScriptRunnerConfig) {
if (content.customConfig.dependencies.length === 0 && content.defaultConfig === 'default') {
try {
const exists = await this.plugin.call('fileManager', 'exists', '.remix/script.config.json')
if (exists) {
await this.plugin.call('fileManager', 'remove', '.remix/script.config.json')
}
} catch (e) {
}
return
}
await this.plugin.call('fileManager', 'writeFile', '.remix/script.config.json', JSON.stringify(content, null, 2))
}
async activateCustomScriptRunner(config: customScriptRunnerConfig) {
try {
const result = await axios.post(customBuildUrl, config)
if (result.data.hash) {
const newConfig: ProjectConfiguration = {
name: result.data.hash,
title: 'Custom configuration',
publish: true,
description: `Extension of ${config.baseConfiguration}`,
dependencies: config.dependencies,
replacements: {},
errorStatus: false,
error: '',
isLoading: false
};
this.configurations.push(newConfig)
this.renderComponent()
await this.loadScriptRunner(result.data.hash)
}
return result.data.hash
} catch (error) {
let message
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
console.log('Error status:', error.response.status);
console.log('Error data:', error.response.data); // This should give you the output being sent
console.log('Error headers:', error.response.headers);
if (error.response.data.error) {
if (isArray(error.response.data.error)) {
const message = `${error.response.data.error[0]}`
this.plugin.call('notification', 'alert', {
id: 'scriptalert',
message,
title: 'Error'
})
throw new Error(message)
}
message = `${error.response.data.error}`
}
message = `Uknown error: ${error.response.data}`
this.plugin.call('notification', 'alert', {
id: 'scriptalert',
message,
title: 'Error'
})
throw new Error(message)
} else if (error.request) {
// The request was made but no response was received
console.log('No response received:', error.request);
throw new Error('No response received')
} else {
// Something happened in setting up the request that triggered an Error
console.log('Error message:', error.message);
throw new Error(error.message)
}
}
}
}

@ -2,26 +2,28 @@
font-family: var(--fa-style-family, "Font Awesome 6 Pro");
font-weight: var(--fa-style, 900); }
.fa,
.fa-classic,
.fa-sharp,
.fas,
.fa-solid,
.far,
.fa-regular,
.fasr,
.fa-brands,
.fas,
.far,
.fab,
.fal,
.fa-light,
.fasl,
.fat,
.fa-thin,
.fast,
.fad,
.fa-duotone,
.fass,
.fasr,
.fasl,
.fast,
.fasds,
.fa-light,
.fa-thin,
.fa-duotone,
.fa-sharp,
.fa-sharp-duotone,
.fa-sharp-solid,
.fab,
.fa-brands {
.fa-classic,
.fa {
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
display: var(--fa-display, inline-block);
@ -31,14 +33,14 @@
text-rendering: auto; }
.fas,
.fa-classic,
.fa-solid,
.far,
.fa-regular,
.fal,
.fa-light,
.fat,
.fa-thin {
.fa-solid,
.fa-regular,
.fa-light,
.fa-thin,
.fa-classic {
font-family: 'Font Awesome 6 Pro'; }
.fab,
@ -50,6 +52,14 @@
.fa-duotone {
font-family: 'Font Awesome 6 Duotone'; }
.fasds,
.fa-sharp-duotone {
font-family: 'Font Awesome 6 Sharp Duotone'; }
.fasds,
.fa-sharp-duotone {
font-weight: 900; }
.fass,
.fasr,
.fasl,
@ -133,7 +143,7 @@
position: relative; }
.fa-li {
left: calc(var(--fa-li-width, 2em) * -1);
left: calc(-1 * var(--fa-li-width, 2em));
position: absolute;
text-align: center;
width: var(--fa-li-width, 2em);
@ -155,101 +165,59 @@
margin-left: var(--fa-pull-margin, 0.3em); }
.fa-beat {
-webkit-animation-name: fa-beat;
animation-name: fa-beat;
-webkit-animation-delay: var(--fa-animation-delay, 0s);
animation-delay: var(--fa-animation-delay, 0s);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out);
animation-timing-function: var(--fa-animation-timing, ease-in-out); }
.fa-bounce {
-webkit-animation-name: fa-bounce;
animation-name: fa-bounce;
-webkit-animation-delay: var(--fa-animation-delay, 0s);
animation-delay: var(--fa-animation-delay, 0s);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1));
animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1)); }
.fa-fade {
-webkit-animation-name: fa-fade;
animation-name: fa-fade;
-webkit-animation-delay: var(--fa-animation-delay, 0s);
animation-delay: var(--fa-animation-delay, 0s);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1));
animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); }
.fa-beat-fade {
-webkit-animation-name: fa-beat-fade;
animation-name: fa-beat-fade;
-webkit-animation-delay: var(--fa-animation-delay, 0s);
animation-delay: var(--fa-animation-delay, 0s);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1));
animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); }
.fa-flip {
-webkit-animation-name: fa-flip;
animation-name: fa-flip;
-webkit-animation-delay: var(--fa-animation-delay, 0s);
animation-delay: var(--fa-animation-delay, 0s);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out);
animation-timing-function: var(--fa-animation-timing, ease-in-out); }
.fa-shake {
-webkit-animation-name: fa-shake;
animation-name: fa-shake;
-webkit-animation-delay: var(--fa-animation-delay, 0s);
animation-delay: var(--fa-animation-delay, 0s);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, linear);
animation-timing-function: var(--fa-animation-timing, linear); }
.fa-spin {
-webkit-animation-name: fa-spin;
animation-name: fa-spin;
-webkit-animation-delay: var(--fa-animation-delay, 0s);
animation-delay: var(--fa-animation-delay, 0s);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 2s);
animation-duration: var(--fa-animation-duration, 2s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, linear);
animation-timing-function: var(--fa-animation-timing, linear); }
.fa-spin-reverse {
@ -257,15 +225,10 @@
.fa-pulse,
.fa-spin-pulse {
-webkit-animation-name: fa-spin;
animation-name: fa-spin;
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, steps(8));
animation-timing-function: var(--fa-animation-timing, steps(8)); }
@media (prefers-reduced-motion: reduce) {
@ -278,218 +241,96 @@
.fa-shake,
.fa-spin,
.fa-spin-pulse {
-webkit-animation-delay: -1ms;
animation-delay: -1ms;
-webkit-animation-duration: 1ms;
animation-duration: 1ms;
-webkit-animation-iteration-count: 1;
animation-iteration-count: 1;
-webkit-transition-delay: 0s;
transition-delay: 0s;
-webkit-transition-duration: 0s;
transition-duration: 0s; } }
@-webkit-keyframes fa-beat {
0%, 90% {
-webkit-transform: scale(1);
transform: scale(1); }
45% {
-webkit-transform: scale(var(--fa-beat-scale, 1.25));
transform: scale(var(--fa-beat-scale, 1.25)); } }
@keyframes fa-beat {
0%, 90% {
-webkit-transform: scale(1);
transform: scale(1); }
45% {
-webkit-transform: scale(var(--fa-beat-scale, 1.25));
transform: scale(var(--fa-beat-scale, 1.25)); } }
@-webkit-keyframes fa-bounce {
0% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); }
10% {
-webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0);
transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); }
30% {
-webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em));
transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); }
50% {
-webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0);
transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); }
57% {
-webkit-transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em));
transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); }
64% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); }
100% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); } }
@keyframes fa-bounce {
0% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); }
10% {
-webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0);
transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); }
30% {
-webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em));
transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); }
50% {
-webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0);
transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); }
57% {
-webkit-transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em));
transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); }
64% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); }
100% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); } }
@-webkit-keyframes fa-fade {
50% {
opacity: var(--fa-fade-opacity, 0.4); } }
@keyframes fa-fade {
50% {
opacity: var(--fa-fade-opacity, 0.4); } }
@-webkit-keyframes fa-beat-fade {
0%, 100% {
opacity: var(--fa-beat-fade-opacity, 0.4);
-webkit-transform: scale(1);
transform: scale(1); }
50% {
opacity: 1;
-webkit-transform: scale(var(--fa-beat-fade-scale, 1.125));
transform: scale(var(--fa-beat-fade-scale, 1.125)); } }
@keyframes fa-beat-fade {
0%, 100% {
opacity: var(--fa-beat-fade-opacity, 0.4);
-webkit-transform: scale(1);
transform: scale(1); }
50% {
opacity: 1;
-webkit-transform: scale(var(--fa-beat-fade-scale, 1.125));
transform: scale(var(--fa-beat-fade-scale, 1.125)); } }
@-webkit-keyframes fa-flip {
50% {
-webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg));
transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); } }
@keyframes fa-flip {
50% {
-webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg));
transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); } }
@-webkit-keyframes fa-shake {
0% {
-webkit-transform: rotate(-15deg);
transform: rotate(-15deg); }
4% {
-webkit-transform: rotate(15deg);
transform: rotate(15deg); }
8%, 24% {
-webkit-transform: rotate(-18deg);
transform: rotate(-18deg); }
12%, 28% {
-webkit-transform: rotate(18deg);
transform: rotate(18deg); }
16% {
-webkit-transform: rotate(-22deg);
transform: rotate(-22deg); }
20% {
-webkit-transform: rotate(22deg);
transform: rotate(22deg); }
32% {
-webkit-transform: rotate(-12deg);
transform: rotate(-12deg); }
36% {
-webkit-transform: rotate(12deg);
transform: rotate(12deg); }
40%, 100% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg); } }
@keyframes fa-shake {
0% {
-webkit-transform: rotate(-15deg);
transform: rotate(-15deg); }
4% {
-webkit-transform: rotate(15deg);
transform: rotate(15deg); }
8%, 24% {
-webkit-transform: rotate(-18deg);
transform: rotate(-18deg); }
12%, 28% {
-webkit-transform: rotate(18deg);
transform: rotate(18deg); }
16% {
-webkit-transform: rotate(-22deg);
transform: rotate(-22deg); }
20% {
-webkit-transform: rotate(22deg);
transform: rotate(22deg); }
32% {
-webkit-transform: rotate(-12deg);
transform: rotate(-12deg); }
36% {
-webkit-transform: rotate(12deg);
transform: rotate(12deg); }
40%, 100% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg); } }
@-webkit-keyframes fa-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg); }
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg); } }
@keyframes fa-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg); }
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg); } }
.fa-rotate-90 {
-webkit-transform: rotate(90deg);
transform: rotate(90deg); }
.fa-rotate-180 {
-webkit-transform: rotate(180deg);
transform: rotate(180deg); }
.fa-rotate-270 {
-webkit-transform: rotate(270deg);
transform: rotate(270deg); }
.fa-flip-horizontal {
-webkit-transform: scale(-1, 1);
transform: scale(-1, 1); }
.fa-flip-vertical {
-webkit-transform: scale(1, -1);
transform: scale(1, -1); }
.fa-flip-both,
.fa-flip-horizontal.fa-flip-vertical {
-webkit-transform: scale(-1, -1);
transform: scale(-1, -1); }
.fa-rotate-by {
-webkit-transform: rotate(var(--fa-rotate-angle, 0));
transform: rotate(var(--fa-rotate-angle, 0)); }
.fa-stack {
@ -1799,6 +1640,7 @@ readers do not read off random characters that represent icons */
.fa-diamond-half::before { content: "\e5b7"; }
.fa-diamond-half-stroke::before { content: "\e5b8"; }
.fa-diamond-turn-right::before { content: "\f5eb"; }
.fa-diamonds-4::before { content: "\e68b"; }
.fa-dice::before { content: "\f522"; }
.fa-dice-d10::before { content: "\f6cd"; }
.fa-dice-d12::before { content: "\f6ce"; }
@ -2383,6 +2225,7 @@ readers do not read off random characters that represent icons */
.fa-globe-pointer::before { content: "\e60e"; }
.fa-globe-snow::before { content: "\f7a3"; }
.fa-globe-stand::before { content: "\f5f6"; }
.fa-globe-wifi::before { content: "\e685"; }
.fa-glove-boxing::before { content: "\f438"; }
.fa-goal-net::before { content: "\e3ab"; }
.fa-golf-ball::before { content: "\f450"; }
@ -2687,6 +2530,7 @@ readers do not read off random characters that represent icons */
.fa-humidity::before { content: "\f750"; }
.fa-hundred-points::before { content: "\e41c"; }
.fa-hurricane::before { content: "\f751"; }
.fa-hydra::before { content: "\e686"; }
.fa-hyphen::before { content: "\2d"; }
.fa-i::before { content: "\49"; }
.fa-i-cursor::before { content: "\f246"; }
@ -2854,6 +2698,7 @@ readers do not read off random characters that represent icons */
.fa-lightbulb-exclamation::before { content: "\f671"; }
.fa-lightbulb-exclamation-on::before { content: "\e1ca"; }
.fa-lightbulb-gear::before { content: "\e5fd"; }
.fa-lightbulb-message::before { content: "\e687"; }
.fa-lightbulb-on::before { content: "\f672"; }
.fa-lightbulb-slash::before { content: "\f673"; }
.fa-lighthouse::before { content: "\e612"; }
@ -3208,6 +3053,7 @@ readers do not read off random characters that represent icons */
.fa-octagon-minus::before { content: "\f308"; }
.fa-octagon-plus::before { content: "\f301"; }
.fa-octagon-xmark::before { content: "\f2f0"; }
.fa-octopus::before { content: "\e688"; }
.fa-oil-can::before { content: "\f613"; }
.fa-oil-can-drip::before { content: "\e205"; }
.fa-oil-temp::before { content: "\f614"; }
@ -4196,9 +4042,12 @@ readers do not read off random characters that represent icons */
.fa-table::before { content: "\f0ce"; }
.fa-table-cells::before { content: "\f00a"; }
.fa-table-cells-column-lock::before { content: "\e678"; }
.fa-table-cells-column-unlock::before { content: "\e690"; }
.fa-table-cells-large::before { content: "\f009"; }
.fa-table-cells-lock::before { content: "\e679"; }
.fa-table-cells-row-lock::before { content: "\e67a"; }
.fa-table-cells-row-unlock::before { content: "\e691"; }
.fa-table-cells-unlock::before { content: "\e692"; }
.fa-table-columns::before { content: "\f0db"; }
.fa-table-layout::before { content: "\e290"; }
.fa-table-list::before { content: "\f00b"; }
@ -4311,9 +4160,11 @@ readers do not read off random characters that represent icons */
.fa-theta::before { content: "\f69e"; }
.fa-thought-bubble::before { content: "\e32e"; }
.fa-thumb-tack::before { content: "\f08d"; }
.fa-thumb-tack-slash::before { content: "\e68f"; }
.fa-thumbs-down::before { content: "\f165"; }
.fa-thumbs-up::before { content: "\f164"; }
.fa-thumbtack::before { content: "\f08d"; }
.fa-thumbtack-slash::before { content: "\e68f"; }
.fa-thunderstorm::before { content: "\f76c"; }
.fa-thunderstorm-moon::before { content: "\f76d"; }
.fa-thunderstorm-sun::before { content: "\f76e"; }
@ -4556,6 +4407,7 @@ readers do not read off random characters that represent icons */
.fa-user-alt::before { content: "\f406"; }
.fa-user-alt-slash::before { content: "\f4fa"; }
.fa-user-astronaut::before { content: "\f4fb"; }
.fa-user-beard-bolt::before { content: "\e689"; }
.fa-user-bounty-hunter::before { content: "\e2bf"; }
.fa-user-chart::before { content: "\f6a3"; }
.fa-user-check::before { content: "\f4fc"; }
@ -4584,6 +4436,7 @@ readers do not read off random characters that represent icons */
.fa-user-hard-hat::before { content: "\f82c"; }
.fa-user-headset::before { content: "\f82d"; }
.fa-user-helmet-safety::before { content: "\f82c"; }
.fa-user-hoodie::before { content: "\e68a"; }
.fa-user-injured::before { content: "\f728"; }
.fa-user-large::before { content: "\f406"; }
.fa-user-large-slash::before { content: "\f4fa"; }
@ -4833,7 +4686,7 @@ readers do not read off random characters that represent icons */
border-width: 0; }
/*!
* Font Awesome Pro 6.5.2 by @fontawesome - https://fontawesome.com
* Font Awesome Pro 6.6.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license (Commercial License)
* Copyright 2024 Fonticons, Inc.
*/
@ -4954,6 +4807,9 @@ readers do not read off random characters that represent icons */
.fa-jxl:before {
content: "\e67b"; }
.fa-dart-lang:before {
content: "\e693"; }
.fa-hire-a-helper:before {
content: "\f3b0"; }
@ -5893,6 +5749,9 @@ readers do not read off random characters that represent icons */
.fa-twitch:before {
content: "\f1e8"; }
.fa-flutter:before {
content: "\e694"; }
.fa-ravelry:before {
content: "\f2d9"; }
@ -6428,7 +6287,7 @@ readers do not read off random characters that represent icons */
content: "\f3f6"; }
/*!
* Font Awesome Pro 6.5.2 by @fontawesome - https://fontawesome.com
* Font Awesome Pro 6.6.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license (Commercial License)
* Copyright 2024 Fonticons, Inc.
*/
@ -6448,7 +6307,7 @@ readers do not read off random characters that represent icons */
font-weight: 300; }
/*!
* Font Awesome Pro 6.5.2 by @fontawesome - https://fontawesome.com
* Font Awesome Pro 6.6.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license (Commercial License)
* Copyright 2024 Fonticons, Inc.
*/
@ -6468,7 +6327,7 @@ readers do not read off random characters that represent icons */
font-weight: 400; }
/*!
* Font Awesome Pro 6.5.2 by @fontawesome - https://fontawesome.com
* Font Awesome Pro 6.6.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license (Commercial License)
* Copyright 2024 Fonticons, Inc.
*/
@ -6488,7 +6347,7 @@ readers do not read off random characters that represent icons */
font-weight: 900; }
/*!
* Font Awesome Pro 6.5.2 by @fontawesome - https://fontawesome.com
* Font Awesome Pro 6.6.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license (Commercial License)
* Copyright 2024 Fonticons, Inc.
*/
@ -7505,6 +7364,9 @@ readers do not read off random characters that represent icons */
.fad.fa-trillium::after, .fa-duotone.fa-trillium::after {
content: "\e588\e588"; }
.fad.fa-table-cells-unlock::after, .fa-duotone.fa-table-cells-unlock::after {
content: "\e692\e692"; }
.fad.fa-music-slash::after, .fa-duotone.fa-music-slash::after {
content: "\f8d1\f8d1"; }
@ -8084,6 +7946,9 @@ readers do not read off random characters that represent icons */
.fad.fa-phone-square-alt::after, .fa-duotone.fa-phone-square-alt::after {
content: "\f87b\f87b"; }
.fad.fa-user-beard-bolt::after, .fa-duotone.fa-user-beard-bolt::after {
content: "\e689\e689"; }
.fad.fa-cart-plus::after, .fa-duotone.fa-cart-plus::after {
content: "\f217\f217"; }
@ -9581,6 +9446,9 @@ readers do not read off random characters that represent icons */
.fad.fa-coffin-cross::after, .fa-duotone.fa-coffin-cross::after {
content: "\e051\e051"; }
.fad.fa-octopus::after, .fa-duotone.fa-octopus::after {
content: "\e688\e688"; }
.fad.fa-spell-check::after, .fa-duotone.fa-spell-check::after {
content: "\f891\f891"; }
@ -9770,6 +9638,12 @@ readers do not read off random characters that represent icons */
.fad.fa-passport::after, .fa-duotone.fa-passport::after {
content: "\f5ab\f5ab"; }
.fad.fa-thumbtack-slash::after, .fa-duotone.fa-thumbtack-slash::after {
content: "\e68f\e68f"; }
.fad.fa-thumb-tack-slash::after, .fa-duotone.fa-thumb-tack-slash::after {
content: "\e68f\e68f"; }
.fad.fa-inbox-in::after, .fa-duotone.fa-inbox-in::after {
content: "\f310\f310"; }
@ -11090,6 +10964,9 @@ readers do not read off random characters that represent icons */
.fad.fa-ufo-beam::after, .fa-duotone.fa-ufo-beam::after {
content: "\e048\e048"; }
.fad.fa-hydra::after, .fa-duotone.fa-hydra::after {
content: "\e686\e686"; }
.fad.fa-circle-caret-up::after, .fa-duotone.fa-circle-caret-up::after {
content: "\f331\f331"; }
@ -11783,6 +11660,9 @@ readers do not read off random characters that represent icons */
.fad.fa-podium::after, .fa-duotone.fa-podium::after {
content: "\f680\f680"; }
.fad.fa-diamonds-4::after, .fa-duotone.fa-diamonds-4::after {
content: "\e68b\e68b"; }
.fad.fa-memo-circle-check::after, .fa-duotone.fa-memo-circle-check::after {
content: "\e1d9\e1d9"; }
@ -15038,6 +14918,9 @@ readers do not read off random characters that represent icons */
.fad.fa-pipe-valve::after, .fa-duotone.fa-pipe-valve::after {
content: "\e439\e439"; }
.fad.fa-lightbulb-message::after, .fa-duotone.fa-lightbulb-message::after {
content: "\e687\e687"; }
.fad.fa-arrow-up-from-arc::after, .fa-duotone.fa-arrow-up-from-arc::after {
content: "\e4b4\e4b4"; }
@ -15554,6 +15437,9 @@ readers do not read off random characters that represent icons */
.fad.fa-oil-well::after, .fa-duotone.fa-oil-well::after {
content: "\e532\e532"; }
.fad.fa-table-cells-column-unlock::after, .fa-duotone.fa-table-cells-column-unlock::after {
content: "\e690\e690"; }
.fad.fa-person-simple::after, .fa-duotone.fa-person-simple::after {
content: "\e220\e220"; }
@ -15851,6 +15737,9 @@ readers do not read off random characters that represent icons */
.fad.fa-toolbox::after, .fa-duotone.fa-toolbox::after {
content: "\f552\f552"; }
.fad.fa-globe-wifi::after, .fa-duotone.fa-globe-wifi::after {
content: "\e685\e685"; }
.fad.fa-envelope-dot::after, .fa-duotone.fa-envelope-dot::after {
content: "\e16f\e16f"; }
@ -16586,6 +16475,9 @@ readers do not read off random characters that represent icons */
.fad.fa-transporter-2::after, .fa-duotone.fa-transporter-2::after {
content: "\e044\e044"; }
.fad.fa-user-hoodie::after, .fa-duotone.fa-user-hoodie::after {
content: "\e68a\e68a"; }
.fad.fa-hands-holding-diamond::after, .fa-duotone.fa-hands-holding-diamond::after {
content: "\f47c\f47c"; }
@ -19121,6 +19013,9 @@ readers do not read off random characters that represent icons */
.fad.fa-brain-circuit::after, .fa-duotone.fa-brain-circuit::after {
content: "\e0c6\e0c6"; }
.fad.fa-table-cells-row-unlock::after, .fa-duotone.fa-table-cells-row-unlock::after {
content: "\e691\e691"; }
.fad.fa-user-injured::after, .fa-duotone.fa-user-injured::after {
content: "\f728\f728"; }
@ -19394,6 +19289,7 @@ readers do not read off random characters that represent icons */
.fak.fa-fa-dock-l::before, .fa-kit.fa-fa-dock-l::before { content: "\e007"; }
.fak.fa-fa-dock-r::before, .fa-kit.fa-fa-dock-r::before { content: "\e006"; }
.fak.fa-lexon::before, .fa-kit.fa-lexon::before { content: "\e004"; }
.fak.fa-solid-gear-circle-play::before, .fa-kit.fa-solid-gear-circle-play::before { content: "\e009"; }
.fak.fa-solidity-mono::before, .fa-kit.fa-solidity-mono::before { content: "\e005"; }
.fak.fa-ts-logo::before, .fa-kit.fa-ts-logo::before { content: "\e003"; }
.fak.fa-vyper2::before, .fa-kit.fa-vyper2::before { content: "\e002"; }

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--!Font Awesome Pro 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2024 Fonticons, Inc.--><path d="M16 166.6c4.4-11.9 9.7-23.3 15.8-34.3l4.7-8.1c6.6-11 14-21.4 22.1-31.2c5.9-7.2 15.7-9.6 24.5-6.8l55.7 17.7c13.4-10.3 28.2-18.9 44-25.4l12.5-57.1c2-9.1 9-16.3 18.2-17.8C227.3 1.2 241.5 0 256 0s28.7 1.2 42.5 3.5c9.2 1.5 16.2 8.7 18.2 17.8l12.5 57.1c15.8 6.5 30.6 15.1 44 25.4l55.7-17.7c8.8-2.8 18.6-.3 24.5 6.8c8.1 9.8 15.5 20.2 22.1 31.2l4.7 8.1c6.1 11 11.4 22.4 15.8 34.3l-.1 .1c3.2 8.7 .5 18.4-6.4 24.6l-1.1 1C394.7 196.1 320 273.3 320 368c0 22.7 4.3 44.3 12.1 64.2c-1 .4-2 .9-3 1.3l-12.5 57.1c-2 9.1-9 16.3-18.2 17.8c-13.8 2.3-28 3.5-42.5 3.5s-28.7-1.2-42.5-3.5c-9.2-1.5-16.2-8.7-18.2-17.8l-12.5-57.1c-15.8-6.5-30.6-15.1-44-25.4L83.1 425.9c-8.8 2.8-18.6 .3-24.5-6.8c-8.1-9.8-15.5-20.2-22.1-31.2l-4.7-8.1c-6.1-11-11.4-22.4-15.8-34.3c-3.2-8.7-.5-18.4 6.4-24.6l43.3-39.4C64.6 273.1 64 264.6 64 256s.6-17.1 1.7-25.4L22.4 191.2c-6.9-6.2-9.6-15.9-6.4-24.6zM186.7 216c-14.3 24.8-14.3 55.2 0 80s40.7 40 69.3 40s55-15.2 69.3-40s14.3-55.2 0-80s-40.7-40-69.3-40s-55 15.2-69.3 40zM352 368c0-51.4 27.4-99 72-124.7s99.4-25.7 144 0s72 73.3 72 124.7s-27.4 99-72 124.7s-99.4 25.7-144 0S352 419.4 352 368zm96-48l0 96c0 5.8 3.1 11.1 8.1 13.9s11.2 2.8 16.1-.2l80-48c4.8-2.9 7.8-8.1 7.8-13.7s-2.9-10.8-7.8-13.7l-80-48c-4.9-3-11.1-3-16.1-.2s-8.1 8.2-8.1 13.9z"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@ -25,6 +25,7 @@ let requiredModules = [
'blockchain',
'web3Provider',
'scriptRunner',
'scriptRunnerBridge',
'fetchAndCompile',
'mainPanel',
'hiddenPanel',
@ -109,6 +110,10 @@ const isVM = (name) => {
return name.startsWith('vm')
}
const isScriptRunner = (name) => {
return name.startsWith('scriptRunner')
}
export function isNative(name) {
// nativePlugin allows to bypass the permission request
@ -142,7 +147,7 @@ export function isNative(name) {
'walletconnect',
'contract-verification'
]
return nativePlugins.includes(name) || requiredModules.includes(name) || isInjectedProvider(name) || isVM(name)
return nativePlugins.includes(name) || requiredModules.includes(name) || isInjectedProvider(name) || isVM(name) || isScriptRunner(name)
}
/**
@ -195,6 +200,8 @@ export class RemixAppManager extends PluginManager {
}
}
await this.toggleActive(name)
}else{
console.log('cannot deactivate', name)
}
}
@ -302,6 +309,7 @@ export class RemixAppManager extends PluginManager {
return plugins.map(plugin => {
if (plugin.name === 'dgit' && Registry.getInstance().get('platform').api.isDesktop()) { plugin.url = 'https://dgit4-76cc9.web.app/' }
if (plugin.name === testPluginName) plugin.url = testPluginUrl
//console.log('plugin', plugin)
return new IframePlugin(plugin)
})
}

@ -30,7 +30,6 @@ export class RemixEngine extends Engine {
if (name === 'remixAI') return { queueTimeout: 60000 * 20 }
if (name === 'cookbookdev') return { queueTimeout: 60000 * 3 }
if (name === 'contentImport') return { queueTimeout: 60000 * 3 }
return { queueTimeout: 10000 }
}

@ -9,5 +9,6 @@ export interface IExtendedFileSystem extends IFileSystem {
refresh(): Promise<void>
hasGitSubmodules(): Promise<boolean>
isGitRepo(): Promise<boolean>
exists(file: string): Promise<boolean>
};
}

@ -0,0 +1,14 @@
import { IFilePanel } from '@remixproject/plugin-api'
import { Profile, StatusEvents } from '@remixproject/plugin-utils'
export interface IMenuIconsApi {
events: {
toggleContent: (name: string) => void,
showContent: (name: string) => void
} & StatusEvents
methods: {
select: (name: string) => void
linkContent: (profile: Profile) => void
unlinkContent: (profile: Profile) => void
}
}

@ -15,6 +15,7 @@ import { ILayoutApi } from "./plugins/layout-api"
import { IMatomoApi } from "./plugins/matomo-api"
import { IRemixAI } from "./plugins/remixai-api"
import { IRemixAID } from "./plugins/remixAIDesktop-api"
import { IMenuIconsApi } from "./plugins/menuicons-api"
import { IDgitPlugin } from "./plugins/dgitplugin-api"
export interface ICustomRemixApi extends IRemixApi {
@ -33,6 +34,7 @@ export interface ICustomRemixApi extends IRemixApi {
pinnedPanel: IPinnedPanelApi
layout: ILayoutApi
matomo: IMatomoApi
menuicons: IMenuIconsApi
remixAI: IRemixAI,
remixAID: IRemixAID
}

@ -76,6 +76,7 @@ export class Web3Accounts {
methods (): Record<string, unknown> {
return {
eth_requestAccounts: this.eth_requestAccounts.bind(this),
eth_accounts: this.eth_accounts.bind(this),
eth_getBalance: this.eth_getBalance.bind(this),
eth_sign: this.eth_sign.bind(this),
@ -85,6 +86,10 @@ export class Web3Accounts {
}
}
eth_requestAccounts (_payload, cb) {
return cb(null, Object.keys(this.accounts))
}
eth_accounts (_payload, cb) {
return cb(null, Object.keys(this.accounts))
}

@ -308,7 +308,6 @@ export class Transactions {
const txBlock = this.vmContext.blockByTxHash[receipt.transactionHash]
const tx = this.vmContext.txByHash[receipt.transactionHash]
// TODO: params to add later
const r: Record<string, unknown> = {
blockHash: bytesToHex(txBlock.hash()),
@ -322,11 +321,10 @@ export class Transactions {
input: receipt.input,
nonce: bigIntToHex(tx.nonce),
transactionIndex: this.TX_INDEX,
value: bigIntToHex(tx.value)
// "value":"0xf3dbb76162000" // 4290000000000000
// "v": "0x25", // 37
// "r": "0x1b5e176d927f8e9ab405058b2d2457392da3e20f328b16ddabcebc33eaac5fea",
// "s": "0x4ba69724e8f69de52f0125ad8b3c5c2cef33019bac3249e2c0a2192766d1721c"
value: bigIntToHex(tx.value),
v: bigIntToHex(tx.v),
r: bigIntToHex(tx.r),
s: bigIntToHex(tx.s)
}
if (receipt.to) {

@ -0,0 +1,2 @@
export { ScriptRunnerUI } from './lib/script-runner-ui';
export * from './types';

@ -0,0 +1,168 @@
import React, { useEffect, useState } from "react";
import { customScriptRunnerConfig, Dependency, ProjectConfiguration } from "../types";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faToggleOff, faToggleOn, faTrash } from "@fortawesome/free-solid-svg-icons";
import { CustomTooltip } from "@remix-ui/helper";
export interface ScriptRunnerUIProps {
publishedConfigurations: ProjectConfiguration[];
openCustomConfig: () => any;
saveCustomConfig(content: customScriptRunnerConfig): void;
activateCustomScriptRunner(config: customScriptRunnerConfig): Promise<string>;
customConfig: customScriptRunnerConfig;
}
export const CustomScriptRunner = (props: ScriptRunnerUIProps) => {
const [dependencies, setDependencies] = useState<Dependency[]>([]);
const [name, setName] = useState<string>('');
const [alias, setAlias] = useState<string>('');
const [version, setVersion] = useState<string>('');
const [baseConfig, setBaseConfig] = useState<string>('default');
const [loading, setLoading] = useState<boolean>(false);
const [useRequire, setUseRequire] = useState<boolean>(false)
const { customConfig } = props;
useEffect(() => {
if (!customConfig) return;
setDependencies(customConfig.dependencies);
setBaseConfig(customConfig.baseConfiguration);
},[customConfig])
const handleAddDependency = () => {
if (name.trim() && version.trim()) {
const newDependency: Dependency = { name, version, require: useRequire, alias };
setDependencies([...dependencies, newDependency]);
setName('');
setVersion('');
} else {
alert('Please fill out both name and version.');
}
};
const handleRemoveDependency = (index: number) => {
const updatedDependencies = dependencies.filter((_, i) => i !== index);
setDependencies(updatedDependencies);
};
const handleSaveToFile = () => {
const fileData = JSON.stringify(dependencies, null, 2);
console.log(fileData, baseConfig);
const customConfig: customScriptRunnerConfig = { baseConfiguration: baseConfig, dependencies };
console.log(customConfig);
props.saveCustomConfig(customConfig);
};
const openConfig = async () => {
const fileData: customScriptRunnerConfig = await props.openCustomConfig();
}
const activateCustomConfig = async () => {
const customConfig: customScriptRunnerConfig = { baseConfiguration: baseConfig, dependencies };
setLoading(true);
try {
await props.activateCustomScriptRunner(customConfig);
} catch (e) {
console.log(e)
} finally {
setLoading(false);
}
}
const onSelectBaseConfig = (e: React.ChangeEvent<HTMLSelectElement>) => {
setBaseConfig(e.target.value);
}
const toggleRequire = () => {
setUseRequire((prev) => !prev)
}
if (loading) {
return <div style={{ padding: '20px', maxWidth: '400px', margin: 'auto' }}>
<div className="text-center py-5">
<i className="fas fa-spinner fa-pulse fa-2x"></i>
</div>
</div>
}
return (
<div style={{ padding: '20px', maxWidth: '400px', margin: 'auto' }}>
<h5>Custom configuration</h5>
<label>Select a base configuration</label>
<select value={baseConfig} className="form-control" onChange={onSelectBaseConfig} style={{ marginBottom: '10px' }}>
<option value="none">Select a base configuration</option>
{props.publishedConfigurations.map((config: ProjectConfiguration, index) => (
<option key={index} value={config.name}>
{config.name}
</option>
))}
</select>
<label>Add dependencies</label>
<div style={{ marginBottom: '10px' }}>
<input
type="text"
placeholder="Dependency Name"
value={name}
className="form-control"
onChange={(e) => setName(e.target.value)}
style={{ marginRight: '10px' }}
/>
<input
type="text"
placeholder="Alias"
className="form-control mt-1"
value={alias}
onChange={(e) => setAlias(e.target.value)} />
<input
type="text"
placeholder="Version"
className="form-control mt-1"
value={version}
onChange={(e) => setVersion(e.target.value)}
/>
<CustomTooltip
placement="bottom"
tooltipText="use require when the module doesn't support import statements"
>
<div>
<label className="pr-2 pt-2">Use 'require':</label>
<FontAwesomeIcon className={useRequire ? 'text-success' : ''} onClick={toggleRequire} icon={useRequire ? faToggleOn : faToggleOff}></FontAwesomeIcon>
</div>
</CustomTooltip>
<button
className="btn btn-primary w-100 mt-1"
onClick={handleAddDependency}>
Add
</button>
</div>
<ul>
{dependencies.map((dependency, index) => (
<li key={index} style={{ marginBottom: '5px' }}>
<div className="d-flex align-items-baseline justify-content-between">
{dependency.name} - {dependency.version}
<button
onClick={() => handleRemoveDependency(index)}
className="btn btn-danger"
style={{ marginLeft: '10px' }}
>
<FontAwesomeIcon icon={faTrash} />
</button>
</div>
</li>
))}
</ul>
{dependencies.length > 0 && (
<button className="btn btn-primary w-100" onClick={handleSaveToFile} style={{ marginTop: '20px' }}>
Save config
</button>
)}
<button className="btn btn-primary w-100" onClick={openConfig} style={{ marginTop: '20px' }}>
Open config
</button>
{dependencies.length > 0 && (
<button className="btn btn-success w-100" onClick={activateCustomConfig} style={{ marginTop: '20px' }}>
Activate
</button>)}
</div>
);
}

@ -0,0 +1,103 @@
import React, { useEffect, useState } from "react";
import { Accordion, Button } from "react-bootstrap";
import { customScriptRunnerConfig, ProjectConfiguration } from "../types";
import { faCaretDown, faCaretRight, faCheck, faExclamationCircle, faRedoAlt, faToggleOn } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { CustomScriptRunner } from "./custom-script-runner";
import { CustomTooltip } from "@remix-ui/helper";
export interface ScriptRunnerUIProps {
loadScriptRunner: (config: ProjectConfiguration) => void;
openCustomConfig: () => any;
saveCustomConfig(content: customScriptRunnerConfig): void;
activateCustomScriptRunner(config: customScriptRunnerConfig): Promise<string>;
customConfig: customScriptRunnerConfig;
configurations: ProjectConfiguration[];
activeConfig: ProjectConfiguration;
enableCustomScriptRunner: boolean;
}
export const ScriptRunnerUI = (props: ScriptRunnerUIProps) => {
const { loadScriptRunner, configurations, activeConfig, enableCustomScriptRunner } = props;
const [activeKey, setActiveKey] = useState('default');
useEffect(() => {
if (activeConfig) {
setActiveKey(activeConfig.name)
}
},[activeConfig])
if (!configurations) {
return <div>Loading...</div>;
}
return (
<div className="px-1">
<Accordion activeKey={activeKey} defaultActiveKey="default">
{configurations.filter((config) => config.publish).map((config: ProjectConfiguration, index) => (
<div key={index}>
<div className="d-flex align-items-baseline justify-content-between">
<Accordion.Toggle as={Button} variant="link" eventKey={config.name}
style={{
overflowX: 'hidden',
textOverflow: 'ellipsis'
}}
onClick={() => setActiveKey(activeKey === config.name ? '' : config.name)}
>
<div className="d-flex">
{activeKey === config.name ?
<FontAwesomeIcon icon={faCaretDown}></FontAwesomeIcon> :
<FontAwesomeIcon icon={faCaretRight}></FontAwesomeIcon>}
<div data-id={`sr-list-${config.name}`} className="pl-2">{config.title || config.name}</div>
</div>
</Accordion.Toggle>
<div className="d-flex align-items-baseline">
{config.isLoading && <div className="">
<i className="fas fa-spinner fa-spin mr-1"></i>
</div>}
{config.errorStatus && config.error && <div className="text-danger">
<CustomTooltip tooltipText={config.error}>
<FontAwesomeIcon data-id={`sr-error-${config.name}`} icon={faExclamationCircle}></FontAwesomeIcon>
</CustomTooltip>
</div>}
{!config.isLoading && config.errorStatus && config.error &&
<div onClick={() => loadScriptRunner(config)} className="pointer px-2">
<FontAwesomeIcon data-id={`sr-reload-${config.name}`} icon={faRedoAlt}></FontAwesomeIcon>
</div>}
{!config.isLoading && !config.errorStatus && !config.error &&
<div onClick={() => loadScriptRunner(config)} className="pointer px-2">
{activeConfig && activeConfig.name !== config.name ?
<FontAwesomeIcon data-id={`sr-toggle-${config.name}`} icon={faToggleOn}></FontAwesomeIcon> :
<FontAwesomeIcon data-id={`sr-loaded-${config.name}`} className="text-success" icon={faCheck}></FontAwesomeIcon>
}
</div>
}
</div>
</div>
<Accordion.Collapse className="px-4" eventKey={config.name}>
<>
<p><strong>Description: </strong>{config.description}</p>
<p><strong>Dependencies:</strong></p>
<ul>
{config.dependencies.map((dep, depIndex) => (
<li data-id={`dependency-${dep.name}-${dep.version}`} key={depIndex}>
<strong>{dep.name}</strong> (v{dep.version})
</li>
))}
</ul></>
</Accordion.Collapse></div>))}
</Accordion>
{enableCustomScriptRunner &&
<CustomScriptRunner
customConfig={props.customConfig}
activateCustomScriptRunner={props.activateCustomScriptRunner}
saveCustomConfig={props.saveCustomConfig}
openCustomConfig={props.openCustomConfig}
publishedConfigurations={configurations.filter((config) => config.publish)}
/>}
</div>
);
};

@ -0,0 +1,37 @@
import { defaultConfig } from "@web3modal/ethers5/react";
export interface Dependency {
version: string;
name: string;
alias?: string;
import?: boolean;
require: boolean;
windowImport?: boolean;
}
export interface Replacements {
[key: string]: string;
}
export interface ProjectConfiguration {
name: string;
publish: boolean;
description: string;
dependencies: Dependency[];
replacements: Replacements;
title: string;
errorStatus: boolean;
error: string;
isLoading: boolean;
}
export interface customScriptRunnerConfig {
baseConfiguration: string;
dependencies: Dependency[];
}
export interface ScriptRunnerConfig {
defaultConfig: string,
customConfig: customScriptRunnerConfig
}

@ -208,7 +208,7 @@ export const TabsUI = (props: TabsUIProps) => {
const path = active().substr(active().indexOf('/') + 1, active().length)
const content = await props.plugin.call('fileManager', 'readFile', path)
if (tabsState.currentExt === 'js' || tabsState.currentExt === 'ts') {
await props.plugin.call('scriptRunner', 'execute', content, path)
await props.plugin.call('scriptRunnerBridge', 'execute', content, path)
_paq.push(['trackEvent', 'editor', 'clickRunFromEditor', tabsState.currentExt])
} else if (tabsState.currentExt === 'sol' || tabsState.currentExt === 'yul') {
await props.plugin.call('solidity', 'compile', path)
@ -225,7 +225,25 @@ export const TabsUI = (props: TabsUIProps) => {
<i className="fas fa-play"></i>
</button>
</CustomTooltip>
{(tabsState.currentExt === 'ts' || tabsState.currentExt === 'js')
&& <CustomTooltip
placement="bottom"
tooltipId="overlay-tooltip-run-script-config"
tooltipText={
<span>
<FormattedMessage id="remixUiTabs.tooltipText9" />
</span>
}><button
data-id="script-config"
className="btn text-dark border-left ml-2 pr-0 py-0 d-flex"
onClick={async () => {
props.plugin.call('menuicons', 'select', 'scriptRunnerBridge')
}}
>
<i className="fa-kit fa-solid-gear-circle-play"></i>
</button></CustomTooltip>
}
<div className="d-flex border-left ml-2 align-items-center" style={{ height: "3em" }}>
<CustomTooltip
placement="bottom"

@ -79,28 +79,28 @@ export const filterFnAction = (name: string, filterFn, dispatch: React.Dispatch<
}
export const registerLogScriptRunnerAction = (on, commandName, commandFn, dispatch: React.Dispatch<any>) => {
on('scriptRunner', commandName, (msg) => {
on('scriptRunnerBridge', commandName, (msg) => {
commandFn.log.apply(commandFn, msg.data) // eslint-disable-line
dispatch({ type: commandName, payload: { commandFn, message: msg.data } })
})
}
export const registerInfoScriptRunnerAction = (on, commandName, commandFn, dispatch: React.Dispatch<any>) => {
on('scriptRunner', commandName, (msg) => {
on('scriptRunnerBridge', commandName, (msg) => {
commandFn.info.apply(commandFn, msg.data) // eslint-disable-line
dispatch({ type: commandName, payload: { commandFn, message: msg.data } })
})
}
export const registerWarnScriptRunnerAction = (on, commandName, commandFn, dispatch: React.Dispatch<any>) => {
on('scriptRunner', commandName, (msg) => {
on('scriptRunnerBridge', commandName, (msg) => {
commandFn.warn.apply(commandFn, msg.data) // eslint-disable-line
dispatch({ type: commandName, payload: { commandFn, message: msg.data } })
})
}
export const registerErrorScriptRunnerAction = (on, commandName, commandFn, dispatch: React.Dispatch<any>) => {
on('scriptRunner', commandName, (msg) => {
on('scriptRunnerBridge', commandName, (msg) => {
commandFn.error.apply(commandFn, msg.data) // eslint-disable-line
dispatch({ type: commandName, payload: { commandFn, message: msg.data } })
})

@ -245,7 +245,7 @@ export const RemixUiTerminal = (props: RemixUiTerminalProps) => {
await call('remixAI', 'solidity_answer', script) // No streaming supported in terminal
_paq.push(['trackEvent', 'ai', 'remixAI', 'askFromTerminal'])
} else {
await call('scriptRunner', 'execute', script)
await call('scriptRunnerBridge', 'execute', script)
}
done()
} catch (error) {

@ -505,7 +505,7 @@ export const runScript = async (path: string) => {
if (error) {
return dispatch(displayPopUp(error))
}
plugin.call('scriptRunner', 'execute', content, path)
plugin.call('scriptRunnerBridge', 'execute', content, path)
})
}

@ -188,6 +188,9 @@
"@remix-api": [
"libs/remix-api/src/index.ts"
],
"@remix-scriptrunner": [
"libs/remix-ui/scriptrunner/src/index.ts"
],
"@remix-git": [
"libs/remix-git/"
]

Loading…
Cancel
Save