refactor(client): ♻️ split source files for readability
This commit is contained in:
parent
479234ef1d
commit
95c471e5d8
178
client/app.js
Normal file
178
client/app.js
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
// Input handlers
|
||||||
|
document
|
||||||
|
.querySelector('#move-input')
|
||||||
|
.addEventListener('change', (event) =>
|
||||||
|
assignCommands('backward', 'forward', event)
|
||||||
|
)
|
||||||
|
document
|
||||||
|
.querySelector('#rotate-input')
|
||||||
|
.addEventListener('change', (event) =>
|
||||||
|
assignCommands('left', 'right', event)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Buttons handlers
|
||||||
|
for (const button of document
|
||||||
|
.querySelector('#touch-controls')
|
||||||
|
.querySelectorAll('button')) {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
const { command, value } = button.dataset
|
||||||
|
|
||||||
|
// Check datas are set for button
|
||||||
|
if (command === undefined || value === undefined) {
|
||||||
|
alert(
|
||||||
|
`Pas de command ou de valeur assigné au bouton "${button.title}"`
|
||||||
|
)
|
||||||
|
throw new Error(`no command or value assigned to ${button.title}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check command is valid
|
||||||
|
if (
|
||||||
|
!['forward', 'backward', 'left', 'right', 'stop'].includes(command)
|
||||||
|
) {
|
||||||
|
alert(
|
||||||
|
`La command "${command}" n'est pas valide en tant que ('forward', 'backward', 'left', 'right', 'stop')`
|
||||||
|
)
|
||||||
|
throw new Error(
|
||||||
|
`specified command "${command}" is not in ['forward', 'backward', 'left', 'right', 'stop']`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check value is valid finite number
|
||||||
|
if (!Number.isFinite(Number(value))) {
|
||||||
|
alert(`La valeur "${value}" n'est convertible en entier fini`)
|
||||||
|
throw new Error(
|
||||||
|
`value "${value}" cannot be casted to finite integer`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
sendCommand(command, Number(value))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard handler
|
||||||
|
document.addEventListener('keydown', ({ code }) => {
|
||||||
|
if (code === 'ArrowUp') {
|
||||||
|
return sendCommand('forward', 2)
|
||||||
|
}
|
||||||
|
if (code === 'ArrowDown') {
|
||||||
|
return sendCommand('backward', 2)
|
||||||
|
}
|
||||||
|
if (code === 'ArrowLeft') {
|
||||||
|
return sendCommand('left', 5)
|
||||||
|
}
|
||||||
|
if (code === 'ArrowRight') {
|
||||||
|
return sendCommand('right', 5)
|
||||||
|
}
|
||||||
|
if (code === 'Space') {
|
||||||
|
return sendCommand('stop', 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Settings handler
|
||||||
|
document
|
||||||
|
.querySelector('#settings')
|
||||||
|
.addEventListener('submit', async (event) => {
|
||||||
|
// Don't interrupt event if not form submitting
|
||||||
|
if (!(event instanceof SubmitEvent)) return true
|
||||||
|
if (event.target === null) return true
|
||||||
|
// Disable form sending
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
const form = new FormData(event.target)
|
||||||
|
const ipAddress = form.get('ip-address')
|
||||||
|
|
||||||
|
if (ipAddress === null) return
|
||||||
|
|
||||||
|
const endpoint = `http://${ipAddress}`
|
||||||
|
await testEndpoint(endpoint)
|
||||||
|
setEndpoint(endpoint)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A command string.
|
||||||
|
* @typedef {('forward' | 'backward' | 'left' | 'right' | 'stop')} Command
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send command to the robot.
|
||||||
|
*
|
||||||
|
* @param {Command} command Command to send.
|
||||||
|
* @param {number} value Value of the command if needed.
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function sendCommand(command, value) {
|
||||||
|
const endpoint = getEndpoint()
|
||||||
|
const url = new URL(`/get?command=${command}&value=${value}`, endpoint)
|
||||||
|
const response = await fetch(url)
|
||||||
|
const text = await response.text()
|
||||||
|
console.log(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get robot endpoint address.
|
||||||
|
*
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function getEndpoint() {
|
||||||
|
const endpoint = sessionStorage.getItem('robot-endpoint')
|
||||||
|
if (endpoint === null) {
|
||||||
|
alert("Aucune adresse IP n'a été renseignée !")
|
||||||
|
throw new Error('no given ip address')
|
||||||
|
}
|
||||||
|
return endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set robot endpoint address.
|
||||||
|
*
|
||||||
|
* @param {string} ip Robot ip.
|
||||||
|
*
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
function setEndpoint(ip) {
|
||||||
|
sessionStorage.setItem('robot-endpoint', ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test connection to endpoint.
|
||||||
|
*
|
||||||
|
* @param {string} endpoint Endpoint to test for.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
* @throws {Error} Endpoint unreachable.
|
||||||
|
*/
|
||||||
|
async function testEndpoint(endpoint) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(endpoint)
|
||||||
|
if (response.ok) return
|
||||||
|
} catch (cause) {
|
||||||
|
alert(`Impossible de joindre l'adresse "${endpoint}"`)
|
||||||
|
throw new Error(`unable to connect to robot at ${endpoint}`, {
|
||||||
|
cause,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assign a command to an input.
|
||||||
|
* Do negative command if value < 0 else do positive command.
|
||||||
|
*
|
||||||
|
* @param {Command} negativeCommand Command if value < 0.
|
||||||
|
* @param {Command} positiveCommand Command if value >= 0.
|
||||||
|
* @param {event} event Event of the input.
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
function assignCommands(negativeCommand, positiveCommand, event) {
|
||||||
|
if (event.target === null) return
|
||||||
|
|
||||||
|
/** @type {HTMLInputElement} */
|
||||||
|
const target = event.target
|
||||||
|
const value = target.valueAsNumber
|
||||||
|
|
||||||
|
if (value < 0) {
|
||||||
|
return sendCommand(negativeCommand, Math.abs(value))
|
||||||
|
}
|
||||||
|
return sendCommand(positiveCommand, value)
|
||||||
|
}
|
|
@ -3,6 +3,8 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="stylesheet" href="style.css" />
|
||||||
|
<script type="module" src="app.js"></script>
|
||||||
<title>Contrôle du robot</title>
|
<title>Contrôle du robot</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -104,309 +106,4 @@
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
<script type="module">
|
|
||||||
// Input handlers
|
|
||||||
document
|
|
||||||
.querySelector('#move-input')
|
|
||||||
.addEventListener('change', (event) =>
|
|
||||||
assignCommands('backward', 'forward', event)
|
|
||||||
)
|
|
||||||
document
|
|
||||||
.querySelector('#rotate-input')
|
|
||||||
.addEventListener('change', (event) =>
|
|
||||||
assignCommands('left', 'right', event)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Buttons handlers
|
|
||||||
for (const button of document
|
|
||||||
.querySelector('#touch-controls')
|
|
||||||
.querySelectorAll('button')) {
|
|
||||||
button.addEventListener('click', () => {
|
|
||||||
const { command, value } = button.dataset
|
|
||||||
|
|
||||||
// Check datas are set for button
|
|
||||||
if (command === undefined || value === undefined) {
|
|
||||||
alert(
|
|
||||||
`Pas de command ou de valeur assigné au bouton "${button.title}"`
|
|
||||||
)
|
|
||||||
throw new Error(
|
|
||||||
`no command or value assigned to ${button.title}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check command is valid
|
|
||||||
if (
|
|
||||||
!['forward', 'backward', 'left', 'right', 'stop'].includes(
|
|
||||||
command
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
alert(
|
|
||||||
`La command "${command}" n'est pas valide en tant que ('forward', 'backward', 'left', 'right', 'stop')`
|
|
||||||
)
|
|
||||||
throw new Error(
|
|
||||||
`specified command "${command}" is not in ['forward', 'backward', 'left', 'right', 'stop']`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check value is valid finite number
|
|
||||||
if (!Number.isFinite(Number(value))) {
|
|
||||||
alert(
|
|
||||||
`La valeur "${value}" n'est convertible en entier fini`
|
|
||||||
)
|
|
||||||
throw new Error(
|
|
||||||
`value "${value}" cannot be casted to finite integer`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
sendCommand(command, Number(value))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keyboard handler
|
|
||||||
document.addEventListener('keydown', ({ code }) => {
|
|
||||||
if (code === 'ArrowUp') {
|
|
||||||
return sendCommand('forward', 2)
|
|
||||||
}
|
|
||||||
if (code === 'ArrowDown') {
|
|
||||||
return sendCommand('backward', 2)
|
|
||||||
}
|
|
||||||
if (code === 'ArrowLeft') {
|
|
||||||
return sendCommand('left', 5)
|
|
||||||
}
|
|
||||||
if (code === 'ArrowRight') {
|
|
||||||
return sendCommand('right', 5)
|
|
||||||
}
|
|
||||||
if (code === 'Space') {
|
|
||||||
return sendCommand('stop', 0)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Settings handler
|
|
||||||
document
|
|
||||||
.querySelector('#settings')
|
|
||||||
.addEventListener('submit', async (event) => {
|
|
||||||
// Don't interrupt event if not form submitting
|
|
||||||
if (!(event instanceof SubmitEvent)) return true
|
|
||||||
if (event.target === null) return true
|
|
||||||
// Disable form sending
|
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
const form = new FormData(event.target)
|
|
||||||
const ipAddress = form.get('ip-address')
|
|
||||||
|
|
||||||
if (ipAddress === null) return
|
|
||||||
|
|
||||||
const endpoint = `http://${ipAddress}`
|
|
||||||
await testEndpoint(endpoint)
|
|
||||||
setEndpoint(endpoint)
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A command string.
|
|
||||||
* @typedef {('forward' | 'backward' | 'left' | 'right' | 'stop')} Command
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send command to the robot.
|
|
||||||
*
|
|
||||||
* @param {Command} command Command to send.
|
|
||||||
* @param {number} value Value of the command if needed.
|
|
||||||
*
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async function sendCommand(command, value) {
|
|
||||||
const endpoint = getEndpoint()
|
|
||||||
const url = new URL(
|
|
||||||
`/get?command=${command}&value=${value}`,
|
|
||||||
endpoint
|
|
||||||
)
|
|
||||||
const response = await fetch(url)
|
|
||||||
const text = await response.text()
|
|
||||||
console.log(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get robot endpoint address.
|
|
||||||
*
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function getEndpoint() {
|
|
||||||
const endpoint = sessionStorage.getItem('robot-endpoint')
|
|
||||||
if (endpoint === null) {
|
|
||||||
alert("Aucune adresse IP n'a été renseignée !")
|
|
||||||
throw new Error('no given ip address')
|
|
||||||
}
|
|
||||||
return endpoint
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set robot endpoint address.
|
|
||||||
*
|
|
||||||
* @param {string} ip Robot ip.
|
|
||||||
*
|
|
||||||
* @returns void
|
|
||||||
*/
|
|
||||||
function setEndpoint(ip) {
|
|
||||||
sessionStorage.setItem('robot-endpoint', ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test connection to endpoint.
|
|
||||||
*
|
|
||||||
* @param {string} endpoint Endpoint to test for.
|
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
* @throws {Error} Endpoint unreachable.
|
|
||||||
*/
|
|
||||||
async function testEndpoint(endpoint) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(endpoint)
|
|
||||||
if (response.ok) return
|
|
||||||
} catch (cause) {
|
|
||||||
alert(`Impossible de joindre l'adresse "${endpoint}"`)
|
|
||||||
throw new Error(`unable to connect to robot at ${endpoint}`, {
|
|
||||||
cause,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assign a command to an input.
|
|
||||||
* Do negative command if value < 0 else do positive command.
|
|
||||||
*
|
|
||||||
* @param {Command} negativeCommand Command if value < 0.
|
|
||||||
* @param {Command} positiveCommand Command if value >= 0.
|
|
||||||
* @param {event} event Event of the input.
|
|
||||||
*
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
function assignCommands(negativeCommand, positiveCommand, event) {
|
|
||||||
if (event.target === null) return
|
|
||||||
|
|
||||||
/** @type {HTMLInputElement} */
|
|
||||||
const target = event.target
|
|
||||||
const value = target.valueAsNumber
|
|
||||||
|
|
||||||
if (value < 0) {
|
|
||||||
return sendCommand(negativeCommand, Math.abs(value))
|
|
||||||
}
|
|
||||||
return sendCommand(positiveCommand, value)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--translucent-high: rgba(255, 255, 255, 0.8);
|
|
||||||
--translucent-low: rgba(255, 255, 255, 0.4);
|
|
||||||
--padding: 0.5rem;
|
|
||||||
--padding-half: calc(var(--padding) / 2);
|
|
||||||
--border-size: 0.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: rgb(247, 234, 219);
|
|
||||||
height: 100dvh;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
font-family: system-ui, -apple-system, BlinkMacSystemFont,
|
|
||||||
'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
|
|
||||||
'Helvetica Neue', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1,
|
|
||||||
h2 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
padding: var(--padding);
|
|
||||||
height: 100%;
|
|
||||||
text-wrap: pretty;
|
|
||||||
}
|
|
||||||
|
|
||||||
.touch-controls {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: var(--padding);
|
|
||||||
padding: var(--padding);
|
|
||||||
|
|
||||||
button {
|
|
||||||
height: 5.5rem;
|
|
||||||
font-size: 3rem;
|
|
||||||
border-radius: var(--padding);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
border: solid var(--border-size) var(--translucent-high);
|
|
||||||
background: var(--translucent-high);
|
|
||||||
border-radius: var(--padding-half);
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
height: 100%;
|
|
||||||
font-size: 110%;
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
border-color: var(--translucent-low);
|
|
||||||
background-color: var(--translucent-low);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-controls,
|
|
||||||
.settings {
|
|
||||||
padding: var(--padding);
|
|
||||||
display: flex;
|
|
||||||
gap: var(--padding);
|
|
||||||
border: solid var(--border-size) var(--translucent-high);
|
|
||||||
border-radius: var(--padding-half);
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-around;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inputs {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(700px, 1fr));
|
|
||||||
gap: var(--padding);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.infos {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(800px, 1fr));
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: var(--padding);
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
display: grid;
|
|
||||||
gap: var(--padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
border: solid var(--border-size) var(--translucent-high);
|
|
||||||
background: var(--translucent-low);
|
|
||||||
border-radius: var(--padding-half);
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
height: 100%;
|
|
||||||
padding: var(--padding-half);
|
|
||||||
font-family: 'Courier New', Courier, monospace;
|
|
||||||
outline: none;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
background-color: var(--translucent-high);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</html>
|
</html>
|
||||||
|
|
114
client/style.css
Normal file
114
client/style.css
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
:root {
|
||||||
|
--translucent-high: rgba(255, 255, 255, 0.8);
|
||||||
|
--translucent-low: rgba(255, 255, 255, 0.4);
|
||||||
|
--padding: 0.5rem;
|
||||||
|
--padding-half: calc(var(--padding) / 2);
|
||||||
|
--border-size: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: rgb(247, 234, 219);
|
||||||
|
height: 100dvh;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
||||||
|
Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
padding: var(--padding);
|
||||||
|
height: 100%;
|
||||||
|
text-wrap: pretty;
|
||||||
|
}
|
||||||
|
|
||||||
|
.touch-controls {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: var(--padding);
|
||||||
|
padding: var(--padding);
|
||||||
|
|
||||||
|
button {
|
||||||
|
height: 5.5rem;
|
||||||
|
font-size: 3rem;
|
||||||
|
border-radius: var(--padding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border: solid var(--border-size) var(--translucent-high);
|
||||||
|
background: var(--translucent-high);
|
||||||
|
border-radius: var(--padding-half);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
height: 100%;
|
||||||
|
font-size: 110%;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
border-color: var(--translucent-low);
|
||||||
|
background-color: var(--translucent-low);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-controls,
|
||||||
|
.settings {
|
||||||
|
padding: var(--padding);
|
||||||
|
display: flex;
|
||||||
|
gap: var(--padding);
|
||||||
|
border: solid var(--border-size) var(--translucent-high);
|
||||||
|
border-radius: var(--padding-half);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputs {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(700px, 1fr));
|
||||||
|
gap: var(--padding);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infos {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(800px, 1fr));
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--padding);
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
border: solid var(--border-size) var(--translucent-high);
|
||||||
|
background: var(--translucent-low);
|
||||||
|
border-radius: var(--padding-half);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
height: 100%;
|
||||||
|
padding: var(--padding-half);
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background-color: var(--translucent-high);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue