rl-core-front 0.19.1 → 0.19.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/_utils/calc.ts +116 -12
- package/src/components/ui/money-input.tsx +136 -27
package/package.json
CHANGED
package/src/_utils/calc.ts
CHANGED
|
@@ -8,9 +8,12 @@
|
|
|
8
8
|
* enquanto este parser recusa o que não entende em vez de tentar adivinhar.
|
|
9
9
|
*
|
|
10
10
|
* O que ele entende: `+ - * / %`, parênteses, sinal negativo, e número no
|
|
11
|
-
* formato brasileiro (ponto de milhar, vírgula decimal)
|
|
11
|
+
* formato brasileiro (ponto de milhar, vírgula decimal) — com os dígitos
|
|
12
|
+
* soltos lidos em centavos, como o campo de dinheiro faz (`4250` é 42,50).
|
|
12
13
|
*/
|
|
13
14
|
|
|
15
|
+
import { maskCurrencyBR } from "#core/_utils/format";
|
|
16
|
+
|
|
14
17
|
/**
|
|
15
18
|
* Por que a conta não fechou. É código e não frase pronta porque a mensagem é
|
|
16
19
|
* texto de tela: quem traduz é o componente, com o dicionário de quem está
|
|
@@ -83,15 +86,99 @@ const SYMBOL_TO_GLYPH = new Map<string, string>(OPERATOR_GLYPHS);
|
|
|
83
86
|
|
|
84
87
|
const SYMBOLS = "+-*/%()";
|
|
85
88
|
|
|
89
|
+
/** O desenho de uma tecla: `*` vira `×`; o resto fica como é. */
|
|
90
|
+
export const glyphOf = (symbol: string): string => SYMBOL_TO_GLYPH.get(symbol) ?? symbol;
|
|
91
|
+
|
|
92
|
+
/** Um pedaço da conta: um número (e se ele é dinheiro ou não) ou um símbolo. */
|
|
93
|
+
interface Run {
|
|
94
|
+
kind: "number" | "symbol";
|
|
95
|
+
text: string;
|
|
96
|
+
/** Só em número: quantidade ou porcentagem, e não dinheiro — ver `toNumber`. */
|
|
97
|
+
plain: boolean;
|
|
98
|
+
}
|
|
99
|
+
|
|
86
100
|
/**
|
|
87
|
-
* A conta
|
|
101
|
+
* A conta em pedaços, com cada número já sabendo se é dinheiro.
|
|
88
102
|
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
103
|
+
* É a mesma pergunta que o tokenizador faz — o que vem depois de `×`/`÷` e
|
|
104
|
+
* o que vem antes de `%` não é dinheiro —, respondida sobre o texto cru para
|
|
105
|
+
* o visor e a normalização usarem sem avaliar a conta (que pode estar pela
|
|
106
|
+
* metade). O que não é número nem símbolo é descartado.
|
|
107
|
+
*/
|
|
108
|
+
const splitRuns = (source: string): Run[] => {
|
|
109
|
+
const runs: Run[] = [];
|
|
110
|
+
let index = 0;
|
|
111
|
+
|
|
112
|
+
while (index < source.length) {
|
|
113
|
+
const character: string = source[index];
|
|
114
|
+
|
|
115
|
+
if (NUMBER_PART.test(character)) {
|
|
116
|
+
let end: number = index;
|
|
117
|
+
while (end < source.length && NUMBER_PART.test(source[end])) {
|
|
118
|
+
end += 1;
|
|
119
|
+
}
|
|
120
|
+
runs.push({ kind: "number", text: source.slice(index, end), plain: false });
|
|
121
|
+
index = end;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const symbol: string = GLYPH_TO_SYMBOL.get(character) ?? character;
|
|
126
|
+
if (SYMBOLS.includes(symbol)) {
|
|
127
|
+
runs.push({ kind: "symbol", text: symbol, plain: false });
|
|
128
|
+
}
|
|
129
|
+
index += 1;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
runs.forEach((run, position) => {
|
|
133
|
+
if (run.kind !== "number") {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const previous: Run | undefined = runs[position - 1];
|
|
137
|
+
const next: Run | undefined = runs[position + 1];
|
|
138
|
+
|
|
139
|
+
run.plain =
|
|
140
|
+
(previous?.kind === "symbol" && (previous.text === "*" || previous.text === "/")) ||
|
|
141
|
+
(next?.kind === "symbol" && next.text === "%");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return runs;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* O texto do visor, como a pessoa o vê e edita, na forma que a conta lê.
|
|
149
|
+
*
|
|
150
|
+
* O visor é um campo de texto que mostra dinheiro formatado (`44,90`), e a
|
|
151
|
+
* pessoa digita, apaga e cola nele. O que sai daqui é a forma canônica: os
|
|
152
|
+
* símbolos em vez dos desenhos, o dinheiro só em dígitos (`4490` — a vírgula
|
|
153
|
+
* e o ponto são da máscara, e o que a pessoa digitou de ponto ou vírgula num
|
|
154
|
+
* valor em reais é descartado, como o campo faz), e a quantidade e a
|
|
155
|
+
* porcentagem como vieram, que ali `1,5` é literal.
|
|
156
|
+
*/
|
|
157
|
+
export const normalizeExpression = (display: string): string =>
|
|
158
|
+
splitRuns(display)
|
|
159
|
+
.map((run) =>
|
|
160
|
+
run.kind === "number" && !run.plain ? run.text.replace(/\D/g, "") : run.text,
|
|
161
|
+
)
|
|
162
|
+
.join("");
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* A conta como se lê na tela: `4490*2+10%` vira `44,90×2+10%`.
|
|
166
|
+
*
|
|
167
|
+
* O dinheiro sai formatado como o campo o mostra — `4490` é 44,90 —, para
|
|
168
|
+
* a pessoa ver o mesmo que a conta vai ler; a quantidade e a porcentagem
|
|
169
|
+
* ficam como estão. Mora aqui, junto do que faz o caminho de volta
|
|
170
|
+
* (`normalizeExpression`), e não no componente: são a mesma correspondência,
|
|
171
|
+
* e separadas elas saem de sincronia calada.
|
|
91
172
|
*/
|
|
92
173
|
export const toDisplayExpression = (source: string): string =>
|
|
93
|
-
|
|
94
|
-
.map((
|
|
174
|
+
splitRuns(source)
|
|
175
|
+
.map((run) => {
|
|
176
|
+
if (run.kind === "symbol") {
|
|
177
|
+
return SYMBOL_TO_GLYPH.get(run.text) ?? run.text;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return run.plain ? run.text : maskCurrencyBR(run.text);
|
|
181
|
+
})
|
|
95
182
|
.join("");
|
|
96
183
|
|
|
97
184
|
/**
|
|
@@ -109,14 +196,25 @@ export const isCalcCharacter = (character: string): boolean =>
|
|
|
109
196
|
/**
|
|
110
197
|
* Número brasileiro para `number`.
|
|
111
198
|
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* vírgula,
|
|
115
|
-
*
|
|
199
|
+
* **Dígitos soltos são centavos**, como no campo: quem digita `4250` no campo
|
|
200
|
+
* vê 42,50, e a calculadora lendo 4.250,00 era a mesma tecla dando dois
|
|
201
|
+
* valores. A vírgula, quando vem, manda — `42,50` é literal. Duas exceções,
|
|
202
|
+
* que são o que não é dinheiro (`plain`): a porcentagem (`10%` são dez por
|
|
203
|
+
* cento, não 0,10%) e o que vem depois de `×` ou `÷` — quantidade, não
|
|
204
|
+
* valor: `4250×3` são três de 42,50, e `100,00÷4` é dividir em quatro.
|
|
205
|
+
*
|
|
206
|
+
* O ponto sozinho é milhar: `1.200` é mil e duzentos para quem digita em
|
|
207
|
+
* reais, e 1,2 para o `Number`. A regra é a do país — havendo vírgula, todo
|
|
208
|
+
* ponto é milhar; sem vírgula, o ponto ainda é milhar quando separa exatamente
|
|
209
|
+
* três dígitos no fim, ou quando aparece mais de uma vez.
|
|
116
210
|
*/
|
|
117
|
-
const toNumber = (raw: string): number => {
|
|
211
|
+
const toNumber = (raw: string, plain: boolean): number => {
|
|
118
212
|
let text: string = raw;
|
|
119
213
|
|
|
214
|
+
if (/^\d+$/.test(text) && !plain) {
|
|
215
|
+
return Number(text) / 100;
|
|
216
|
+
}
|
|
217
|
+
|
|
120
218
|
if (text.includes(",")) {
|
|
121
219
|
text = text.replace(/\./g, "").replace(/,/g, ".");
|
|
122
220
|
} else if ((text.match(/\./g) ?? []).length > 1 || /\d\.\d{3}$/.test(text)) {
|
|
@@ -148,7 +246,13 @@ const tokenize = (source: string): Token[] => {
|
|
|
148
246
|
while (end < source.length && NUMBER_PART.test(source[end])) {
|
|
149
247
|
end += 1;
|
|
150
248
|
}
|
|
151
|
-
|
|
249
|
+
// O `%` logo depois, ou o `×`/`÷` logo antes, mudam a leitura do
|
|
250
|
+
// número — ver `toNumber`.
|
|
251
|
+
const previous: Token | undefined = tokens[tokens.length - 1];
|
|
252
|
+
const plain: boolean =
|
|
253
|
+
/^\s*%/.test(source.slice(end)) ||
|
|
254
|
+
(previous?.type === "*" || previous?.type === "/");
|
|
255
|
+
tokens.push({ type: "number", value: toNumber(source.slice(index, end), plain) });
|
|
152
256
|
index = end;
|
|
153
257
|
continue;
|
|
154
258
|
}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { Calculator, Delete } from "lucide-react";
|
|
4
|
-
import type { JSX, KeyboardEvent } from "react";
|
|
4
|
+
import type { ChangeEvent, JSX, KeyboardEvent } from "react";
|
|
5
5
|
import { useRef, useState } from "react";
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
CalcError,
|
|
9
9
|
CalcErrorCode,
|
|
10
10
|
evaluateExpression,
|
|
11
|
+
glyphOf,
|
|
11
12
|
isCalcCharacter,
|
|
13
|
+
normalizeExpression,
|
|
12
14
|
toDisplayExpression,
|
|
13
15
|
} from "#core/_utils/calc";
|
|
14
16
|
import {
|
|
@@ -61,7 +63,7 @@ type KeyRole = "digit" | "operator" | "aux";
|
|
|
61
63
|
interface CalcKey {
|
|
62
64
|
/**
|
|
63
65
|
* O que a tecla acrescenta à conta — e também o seu desenho, passado pelo
|
|
64
|
-
* `
|
|
66
|
+
* `toGlyphs`: a tecla do `*` mostra `×` sem que o glifo precise
|
|
65
67
|
* ser escrito aqui de novo.
|
|
66
68
|
*/
|
|
67
69
|
input?: string;
|
|
@@ -126,8 +128,15 @@ export const MoneyInput = ({
|
|
|
126
128
|
}: MoneyInputProps): JSX.Element => {
|
|
127
129
|
const { t } = useI18n();
|
|
128
130
|
const [open, setOpen] = useState(false);
|
|
131
|
+
/*
|
|
132
|
+
A conta na forma canônica — símbolos, dinheiro só em dígitos (`4490`). O
|
|
133
|
+
visor mostra e edita a forma desenhada (`44,90`), e cada mudança volta
|
|
134
|
+
para cá por `normalizeExpression`: é o que deixa a pessoa digitar, apagar
|
|
135
|
+
e colar num campo formatado sem a máscara e o cursor brigarem.
|
|
136
|
+
*/
|
|
129
137
|
const [expression, setExpression] = useState("");
|
|
130
|
-
const
|
|
138
|
+
const visorRef = useRef<HTMLInputElement>(null);
|
|
139
|
+
const display: string = toDisplayExpression(expression);
|
|
131
140
|
|
|
132
141
|
/**
|
|
133
142
|
* Traduz o erro do parser. O código vem do `calc.util`, que não conhece
|
|
@@ -165,24 +174,115 @@ export const MoneyInput = ({
|
|
|
165
174
|
}
|
|
166
175
|
})();
|
|
167
176
|
|
|
168
|
-
/**
|
|
177
|
+
/**
|
|
178
|
+
* Abrir semeia a conta com o que já está no campo, para somar em cima.
|
|
179
|
+
*
|
|
180
|
+
* Em dígitos (`359`), e não no texto do campo (`3,59`): com a vírgula o
|
|
181
|
+
* número vira literal, e cada dígito digitado depois entrava como casa
|
|
182
|
+
* decimal a mais — `3,5922` arredondava de volta para 3,59, e o valor
|
|
183
|
+
* parecia travado. Em dígitos o visor mostra o mesmo 3,59, e digitar em
|
|
184
|
+
* cima empurra o valor como o campo faz.
|
|
185
|
+
*/
|
|
169
186
|
const handleOpenChange = (next: boolean): void => {
|
|
170
187
|
if (next) {
|
|
171
|
-
|
|
188
|
+
const cents: number = Math.round(parseCurrencyBR(value) * 100);
|
|
189
|
+
setExpression(cents > 0 ? String(cents) : "");
|
|
172
190
|
}
|
|
173
191
|
setOpen(next);
|
|
174
192
|
};
|
|
175
193
|
|
|
194
|
+
/**
|
|
195
|
+
* O visor como ficou depois de uma edição, e onde o cursor estava nele.
|
|
196
|
+
*
|
|
197
|
+
* O texto novo é normalizado e desenhado de novo — a máscara pode ter
|
|
198
|
+
* mudado de lugar (`449` é 4,49; `4490` é 44,90) —, e o cursor é posto
|
|
199
|
+
* com o mesmo número de caracteres "de verdade" (tudo que não é ponto,
|
|
200
|
+
* vírgula ou espaço) **à direita** dele. É o que faz digitar no meio de
|
|
201
|
+
* `44,90` cair onde a pessoa clicou, e não no fim.
|
|
202
|
+
*
|
|
203
|
+
* Contado do fim, e não do início, por causa do zero que a máscara põe na
|
|
204
|
+
* frente: `9` desenha `0,09`, e contando do início o cursor caía depois
|
|
205
|
+
* desse zero — o `8` seguinte entrava antes da vírgula, e `987` virava
|
|
206
|
+
* 80,79 em vez de 9,87.
|
|
207
|
+
*/
|
|
208
|
+
const commit = (nextDisplay: string, caret: number): void => {
|
|
209
|
+
const significantAfter: number = Array.from(nextDisplay.slice(caret)).filter(
|
|
210
|
+
(character) => !/[.,\s]/.test(character),
|
|
211
|
+
).length;
|
|
212
|
+
const raw: string = normalizeExpression(nextDisplay);
|
|
213
|
+
const redrawn: string = toDisplayExpression(raw);
|
|
214
|
+
|
|
215
|
+
let position: number = redrawn.length;
|
|
216
|
+
let seen = 0;
|
|
217
|
+
while (position > 0 && seen < significantAfter) {
|
|
218
|
+
if (!/[.,\s]/.test(redrawn[position - 1])) {
|
|
219
|
+
seen += 1;
|
|
220
|
+
}
|
|
221
|
+
position -= 1;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
setExpression(raw);
|
|
225
|
+
// Depois do render, quando o campo já mostra o texto redesenhado.
|
|
226
|
+
requestAnimationFrame(() => {
|
|
227
|
+
visorRef.current?.focus();
|
|
228
|
+
visorRef.current?.setSelectionRange(position, position);
|
|
229
|
+
});
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/** Onde a seleção está no visor — no fim, se o campo ainda não tem foco. */
|
|
233
|
+
const selection = (): [number, number] => [
|
|
234
|
+
visorRef.current?.selectionStart ?? display.length,
|
|
235
|
+
visorRef.current?.selectionEnd ?? display.length,
|
|
236
|
+
];
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* A tecla escreve onde o cursor está — e por cima do que estiver
|
|
240
|
+
* selecionado, como qualquer campo de texto.
|
|
241
|
+
*
|
|
242
|
+
* O visor foi um texto fixo em que as teclas só acrescentavam no fim: para
|
|
243
|
+
* trocar o `500` do meio da conta era apagar até lá e digitar tudo de novo,
|
|
244
|
+
* e selecionar tudo e digitar acrescentava em vez de substituir.
|
|
245
|
+
*/
|
|
246
|
+
const write = (text: string): void => {
|
|
247
|
+
const [start, end] = selection();
|
|
248
|
+
|
|
249
|
+
commit(display.slice(0, start) + text + display.slice(end), start + text.length);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* O backspace da tecla: apaga a seleção, ou o caractere antes do cursor —
|
|
254
|
+
* pulando o ponto e a vírgula, que são da máscara e voltam sozinhos.
|
|
255
|
+
*/
|
|
256
|
+
const erase = (): void => {
|
|
257
|
+
const [start, end] = selection();
|
|
258
|
+
let from: number = start;
|
|
259
|
+
|
|
260
|
+
if (start === end) {
|
|
261
|
+
while (from > 0 && /[.,]/.test(display[from - 1])) {
|
|
262
|
+
from -= 1;
|
|
263
|
+
}
|
|
264
|
+
from = Math.max(from - 1, 0);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
commit(display.slice(0, from) + display.slice(end), from);
|
|
268
|
+
};
|
|
269
|
+
|
|
176
270
|
const press = (key: CalcKey): void => {
|
|
177
271
|
if (key.action === "clear") {
|
|
178
272
|
setExpression("");
|
|
273
|
+
visorRef.current?.focus();
|
|
179
274
|
return;
|
|
180
275
|
}
|
|
181
276
|
if (key.action === "backspace") {
|
|
182
|
-
|
|
277
|
+
erase();
|
|
183
278
|
return;
|
|
184
279
|
}
|
|
185
|
-
|
|
280
|
+
write(key.input ?? "");
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
/** O que foi digitado, apagado ou colado direto no visor. */
|
|
284
|
+
const handleVisorChange = (event: ChangeEvent<HTMLInputElement>): void => {
|
|
285
|
+
commit(event.target.value, event.target.selectionStart ?? event.target.value.length);
|
|
186
286
|
};
|
|
187
287
|
|
|
188
288
|
const apply = (): void => {
|
|
@@ -198,21 +298,16 @@ export const MoneyInput = ({
|
|
|
198
298
|
event.target instanceof HTMLElement && event.target.tagName === "BUTTON";
|
|
199
299
|
|
|
200
300
|
// Com o foco numa tecla, o Enter é o clique dela — mexer nisso quebraria
|
|
201
|
-
// quem navega por Tab.
|
|
301
|
+
// quem navega por Tab. No visor e no resto do painel, o Enter usa o valor.
|
|
202
302
|
if (event.key === "Enter" && !onButton) {
|
|
203
303
|
event.preventDefault();
|
|
204
304
|
apply();
|
|
205
305
|
return;
|
|
206
306
|
}
|
|
207
|
-
|
|
307
|
+
// Fora do visor (numa tecla, por Tab), digitar ainda escreve na conta.
|
|
308
|
+
if (event.target !== visorRef.current && event.key.length === 1 && isCalcCharacter(event.key)) {
|
|
208
309
|
event.preventDefault();
|
|
209
|
-
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
if (event.key.length === 1 && isCalcCharacter(event.key)) {
|
|
213
|
-
event.preventDefault();
|
|
214
|
-
// No teclado numérico o decimal é ponto; em reais, é vírgula.
|
|
215
|
-
setExpression((current) => current + (event.key === "." ? "," : event.key));
|
|
310
|
+
write(event.key);
|
|
216
311
|
}
|
|
217
312
|
};
|
|
218
313
|
|
|
@@ -252,28 +347,39 @@ export const MoneyInput = ({
|
|
|
252
347
|
</PopoverAnchor>
|
|
253
348
|
|
|
254
349
|
<PopoverContent
|
|
255
|
-
ref={panelRef}
|
|
256
350
|
align="end"
|
|
257
351
|
aria-label={t("calculator.title")}
|
|
258
352
|
onKeyDown={handleKeyDown}
|
|
259
|
-
// O foco
|
|
260
|
-
// valor em vez de apertar o "C".
|
|
353
|
+
// O foco vai para o visor, com o cursor no fim: é onde se digita, e é
|
|
354
|
+
// onde o Enter usa o valor em vez de apertar o "C".
|
|
261
355
|
onOpenAutoFocus={(event) => {
|
|
262
356
|
event.preventDefault();
|
|
263
|
-
|
|
357
|
+
const visor = visorRef.current;
|
|
358
|
+
visor?.focus();
|
|
359
|
+
visor?.setSelectionRange(visor.value.length, visor.value.length);
|
|
264
360
|
}}
|
|
265
|
-
tabIndex={-1}
|
|
266
361
|
className="w-72"
|
|
267
362
|
>
|
|
363
|
+
{/*
|
|
364
|
+
O visor é a linha grande, e é editável: é nela que a pessoa clica
|
|
365
|
+
para trocar um número. O resultado fica embaixo, pequeno — é
|
|
366
|
+
consequência, não o que se mexe.
|
|
367
|
+
*/}
|
|
268
368
|
<div className="flex min-h-14 flex-col justify-between gap-1 rounded-md border border-border bg-muted/50 px-3 py-1.5 text-right">
|
|
269
|
-
<
|
|
270
|
-
{
|
|
271
|
-
|
|
369
|
+
<input
|
|
370
|
+
ref={visorRef}
|
|
371
|
+
value={display}
|
|
372
|
+
onChange={handleVisorChange}
|
|
373
|
+
aria-label={t("calculator.title")}
|
|
374
|
+
autoComplete="off"
|
|
375
|
+
spellCheck={false}
|
|
376
|
+
className="w-full bg-transparent text-right font-mono text-base font-medium tabular-nums outline-none"
|
|
377
|
+
/>
|
|
272
378
|
{outcome.message ? (
|
|
273
379
|
<span className="break-words text-xs text-destructive">{outcome.message}</span>
|
|
274
380
|
) : (
|
|
275
|
-
<span className="
|
|
276
|
-
{formatMoneyBR(outcome.value ?? 0)}
|
|
381
|
+
<span className="min-h-4 font-mono text-xs text-muted-foreground">
|
|
382
|
+
{expression ? `= ${formatMoneyBR(outcome.value ?? 0)}` : ""}
|
|
277
383
|
</span>
|
|
278
384
|
)}
|
|
279
385
|
</div>
|
|
@@ -285,6 +391,9 @@ export const MoneyInput = ({
|
|
|
285
391
|
type="button"
|
|
286
392
|
variant={key.role === "aux" ? "outline" : "secondary"}
|
|
287
393
|
aria-label={key.labelKey ? t(key.labelKey) : undefined}
|
|
394
|
+
// O clique não tira o foco do visor: é a seleção de lá que a
|
|
395
|
+
// tecla substitui, e o cursor de lá que ela avança.
|
|
396
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
288
397
|
onClick={() => press(key)}
|
|
289
398
|
className={cn(
|
|
290
399
|
"h-8 px-0 font-mono text-sm",
|
|
@@ -292,7 +401,7 @@ export const MoneyInput = ({
|
|
|
292
401
|
key.role === "aux" && "text-muted-foreground",
|
|
293
402
|
)}
|
|
294
403
|
>
|
|
295
|
-
{key.label ?? (key.input ?
|
|
404
|
+
{key.label ?? (key.input ? glyphOf(key.input) : <Delete />)}
|
|
296
405
|
</Button>
|
|
297
406
|
))}
|
|
298
407
|
</div>
|