Move keyboardOptions to useKeyboardLayouts

Manage state to eliminate rerenders by judicious use of useMemo.
Also removed the extraneous resetKeyboardState.
This commit is contained in:
Marc Brooks 2025-08-21 18:08:21 +00:00
parent 580b3397bf
commit cbc3f2016f
9 changed files with 66 additions and 66 deletions

View File

@ -12,7 +12,7 @@ import {
MAX_KEYS_PER_STEP, MAX_KEYS_PER_STEP,
} from "@/constants/macros"; } from "@/constants/macros";
import { KeySequence } from "@/hooks/stores"; import { KeySequence } from "@/hooks/stores";
import { useKeyboardLayout } from "@/hooks/useKeyboardLayout"; import useKeyboardLayout from "@/hooks/useKeyboardLayout";
interface ValidationErrors { interface ValidationErrors {
name?: string; name?: string;
@ -45,7 +45,7 @@ export function MacroForm({
const [keyQueries, setKeyQueries] = useState<Record<number, string>>({}); const [keyQueries, setKeyQueries] = useState<Record<number, string>>({});
const [errors, setErrors] = useState<ValidationErrors>({}); const [errors, setErrors] = useState<ValidationErrors>({});
const [errorMessage, setErrorMessage] = useState<string | null>(null); const [errorMessage, setErrorMessage] = useState<string | null>(null);
const { keyboard } = useKeyboardLayout(); const { selectedKeyboard } = useKeyboardLayout();
const showTemporaryError = (message: string) => { const showTemporaryError = (message: string) => {
setErrorMessage(message); setErrorMessage(message);
@ -236,7 +236,7 @@ export function MacroForm({
} }
onDelayChange={delay => handleDelayChange(stepIndex, delay)} onDelayChange={delay => handleDelayChange(stepIndex, delay)}
isLastStep={stepIndex === (macro.steps?.length || 0) - 1} isLastStep={stepIndex === (macro.steps?.length || 0) - 1}
keyboard={keyboard} keyboard={selectedKeyboard}
/> />
))} ))}
</div> </div>

View File

@ -1,3 +1,4 @@
import { useMemo } from "react";
import { LuArrowUp, LuArrowDown, LuX, LuTrash2 } from "react-icons/lu"; import { LuArrowUp, LuArrowDown, LuX, LuTrash2 } from "react-icons/lu";
import { Button } from "@/components/Button"; import { Button } from "@/components/Button";
@ -12,15 +13,6 @@ import { keys, modifiers } from "@/keyboardMappings";
// Filter out modifier keys since they're handled in the modifiers section // Filter out modifier keys since they're handled in the modifiers section
const modifierKeyPrefixes = ['Alt', 'Control', 'Shift', 'Meta']; const modifierKeyPrefixes = ['Alt', 'Control', 'Shift', 'Meta'];
const keyOptions = (keyDisplayMap: Record<string, string>) => {
return Object.keys(keys)
.filter(key => !modifierKeyPrefixes.some(prefix => key.startsWith(prefix)))
.map(key => ({
value: key,
label: keyDisplayMap[key] || key,
}));
}
const modifierOptions = Object.keys(modifiers).map(modifier => ({ const modifierOptions = Object.keys(modifiers).map(modifier => ({
value: modifier, value: modifier,
label: modifier.replace(/^(Control|Alt|Shift|Meta)(Left|Right)$/, "$1 $2"), label: modifier.replace(/^(Control|Alt|Shift|Meta)(Left|Right)$/, "$1 $2"),
@ -93,16 +85,26 @@ export function MacroStepCard({
}: MacroStepCardProps) { }: MacroStepCardProps) {
const { keyDisplayMap } = keyboard; const { keyDisplayMap } = keyboard;
const getFilteredKeys = () => { const keyOptions = useMemo(() =>
Object.keys(keys)
.filter(key => !modifierKeyPrefixes.some(prefix => key.startsWith(prefix)))
.map(key => ({
value: key,
label: keyDisplayMap[key] || key,
})),
[keyDisplayMap]
);
const filteredKeys = useMemo(() => {
const selectedKeys = ensureArray(step.keys); const selectedKeys = ensureArray(step.keys);
const availableKeys = keyOptions(keyDisplayMap).filter(option => !selectedKeys.includes(option.value)); const availableKeys = keyOptions.filter(option => !selectedKeys.includes(option.value));
if (keyQuery === '') { if (keyQuery === '') {
return availableKeys; return availableKeys;
} else { } else {
return availableKeys.filter(option => option.label.toLowerCase().includes(keyQuery.toLowerCase())); return availableKeys.filter(option => option.label.toLowerCase().includes(keyQuery.toLowerCase()));
} }
}; }, [keyOptions, keyQuery, step.keys]);
return ( return (
<Card className="p-4"> <Card className="p-4">
@ -211,7 +213,7 @@ export function MacroStepCard({
}} }}
displayValue={() => keyQuery} displayValue={() => keyQuery}
onInputChange={onKeyQueryChange} onInputChange={onKeyQueryChange}
options={getFilteredKeys} options={() => filteredKeys}
disabledMessage="Max keys reached" disabledMessage="Max keys reached"
size="SM" size="SM"
immediate immediate

View File

@ -14,7 +14,7 @@ import DetachIconRaw from "@/assets/detach-icon.svg";
import { cx } from "@/cva.config"; import { cx } from "@/cva.config";
import { useHidStore, useUiStore } from "@/hooks/stores"; import { useHidStore, useUiStore } from "@/hooks/stores";
import useKeyboard from "@/hooks/useKeyboard"; import useKeyboard from "@/hooks/useKeyboard";
import { useKeyboardLayout } from "@/hooks/useKeyboardLayout"; import useKeyboardLayout from "@/hooks/useKeyboardLayout";
import { keys, modifiers, latchingKeys, decodeModifiers } from "@/keyboardMappings"; import { keys, modifiers, latchingKeys, decodeModifiers } from "@/keyboardMappings";
export const DetachIcon = ({ className }: { className?: string }) => { export const DetachIcon = ({ className }: { className?: string }) => {
@ -30,12 +30,19 @@ function KeyboardWrapper() {
const { isAttachedVirtualKeyboardVisible, setAttachedVirtualKeyboardVisibility } = useUiStore(); const { isAttachedVirtualKeyboardVisible, setAttachedVirtualKeyboardVisibility } = useUiStore();
const { keysDownState, /* keyboardLedState,*/ isVirtualKeyboardEnabled, setVirtualKeyboardEnabled } = useHidStore(); const { keysDownState, /* keyboardLedState,*/ isVirtualKeyboardEnabled, setVirtualKeyboardEnabled } = useHidStore();
const { handleKeyPress, executeMacro } = useKeyboard(); const { handleKeyPress, executeMacro } = useKeyboard();
const { selectedKeyboard } = useKeyboardLayout();
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const [position, setPosition] = useState({ x: 0, y: 0 }); const [position, setPosition] = useState({ x: 0, y: 0 });
const [newPosition, setNewPosition] = useState({ x: 0, y: 0 }); const [newPosition, setNewPosition] = useState({ x: 0, y: 0 });
const { keyboard } = useKeyboardLayout(); const keyDisplayMap = useMemo(() => {
return selectedKeyboard.keyDisplayMap;
}, [selectedKeyboard]);
const virtualKeyboard = useMemo(() => {
return selectedKeyboard.virtualKeyboard;
}, [selectedKeyboard]);
//const isCapsLockActive = useMemo(() => { //const isCapsLockActive = useMemo(() => {
// return (keyboardLedState.caps_lock); // return (keyboardLedState.caps_lock);
@ -269,8 +276,8 @@ function KeyboardWrapper() {
buttons: keyNamesForDownKeys.join(" "), buttons: keyNamesForDownKeys.join(" "),
}, },
]} ]}
display={keyboard.keyDisplayMap} display={keyDisplayMap}
layout={keyboard.virtualKeyboard.main} layout={virtualKeyboard.main}
disableButtonHold={true} disableButtonHold={true}
enableLayoutCandidates={false} enableLayoutCandidates={false}
preventMouseDownDefault={true} preventMouseDownDefault={true}
@ -290,8 +297,8 @@ function KeyboardWrapper() {
layoutName="default" layoutName="default"
onKeyPress={onKeyDown} onKeyPress={onKeyDown}
onKeyReleased={onKeyUp} onKeyReleased={onKeyUp}
display={keyboard.keyDisplayMap} display={keyDisplayMap}
layout={keyboard.virtualKeyboard.control} layout={virtualKeyboard.control}
disableButtonHold={true} disableButtonHold={true}
enableLayoutCandidates={false} enableLayoutCandidates={false}
preventMouseDownDefault={true} preventMouseDownDefault={true}
@ -308,8 +315,8 @@ function KeyboardWrapper() {
theme="simple-keyboard hg-theme-default hg-layout-default" theme="simple-keyboard hg-theme-default hg-layout-default"
onKeyPress={onKeyDown} onKeyPress={onKeyDown}
onKeyReleased={onKeyUp} onKeyReleased={onKeyUp}
display={keyboard.keyDisplayMap} display={keyDisplayMap}
layout={keyboard.virtualKeyboard.arrows} layout={virtualKeyboard.arrows}
disableButtonHold={true} disableButtonHold={true}
enableLayoutCandidates={false} enableLayoutCandidates={false}
preventMouseDownDefault={true} preventMouseDownDefault={true}

View File

@ -11,7 +11,7 @@ import { useJsonRpc } from "@/hooks/useJsonRpc";
import { useHidStore, useRTCStore, useUiStore, useSettingsStore } from "@/hooks/stores"; import { useHidStore, useRTCStore, useUiStore, useSettingsStore } from "@/hooks/stores";
import { keys, modifiers } from "@/keyboardMappings"; import { keys, modifiers } from "@/keyboardMappings";
import { KeyStroke } from "@/keyboardLayouts"; import { KeyStroke } from "@/keyboardLayouts";
import { useKeyboardLayout } from "@/hooks/useKeyboardLayout"; import useKeyboardLayout from "@/hooks/useKeyboardLayout";
import notifications from "@/notifications"; import notifications from "@/notifications";
const hidKeyboardPayload = (modifier: number, keys: number[]) => { const hidKeyboardPayload = (modifier: number, keys: number[]) => {
@ -36,7 +36,7 @@ export default function PasteModal() {
const close = useClose(); const close = useClose();
const { setKeyboardLayout } = useSettingsStore(); const { setKeyboardLayout } = useSettingsStore();
const { keyboard } = useKeyboardLayout(); const { selectedKeyboard } = useKeyboardLayout();
useEffect(() => { useEffect(() => {
send("getKeyboardLayout", {}, resp => { send("getKeyboardLayout", {}, resp => {
@ -56,13 +56,13 @@ export default function PasteModal() {
setDisableVideoFocusTrap(false); setDisableVideoFocusTrap(false);
if (rpcDataChannel?.readyState !== "open" || !TextAreaRef.current) return; if (rpcDataChannel?.readyState !== "open" || !TextAreaRef.current) return;
if (!keyboard) return; if (!selectedKeyboard) return;
const text = TextAreaRef.current.value; const text = TextAreaRef.current.value;
try { try {
for (const char of text) { for (const char of text) {
const keyprops = keyboard.chars[char]; const keyprops = selectedKeyboard.chars[char];
if (!keyprops) continue; if (!keyprops) continue;
const { key, shift, altRight, deadKey, accentKey } = keyprops; const { key, shift, altRight, deadKey, accentKey } = keyprops;
@ -102,7 +102,7 @@ export default function PasteModal() {
); );
}); });
} }
}, [keyboard, rpcDataChannel?.readyState, send, setDisableVideoFocusTrap, setPasteModeEnabled]); }, [selectedKeyboard, rpcDataChannel?.readyState, send, setDisableVideoFocusTrap, setPasteModeEnabled]);
useEffect(() => { useEffect(() => {
if (TextAreaRef.current) { if (TextAreaRef.current) {
@ -152,7 +152,7 @@ export default function PasteModal() {
// @ts-expect-error TS doesn't recognize Intl.Segmenter in some environments // @ts-expect-error TS doesn't recognize Intl.Segmenter in some environments
[...new Intl.Segmenter().segment(value)] [...new Intl.Segmenter().segment(value)]
.map(x => x.segment) .map(x => x.segment)
.filter(char => !keyboard.chars[char]), .filter(char => !selectedKeyboard.chars[char]),
), ),
]; ];
@ -173,7 +173,7 @@ export default function PasteModal() {
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<p className="text-xs text-slate-600 dark:text-slate-400"> <p className="text-xs text-slate-600 dark:text-slate-400">
Sending text using keyboard layout: {keyboard.isoCode}-{keyboard.name} Sending text using keyboard layout: {selectedKeyboard.isoCode}-{selectedKeyboard.name}
</p> </p>
</div> </div>
</div> </div>

View File

@ -1,11 +1,17 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { useSettingsStore } from "@/hooks/stores"; import { useSettingsStore } from "@/hooks/stores";
import { KeyboardLayout, selectedKeyboard } from "@/keyboardLayouts"; import { keyboards } from "@/keyboardLayouts";
export function useKeyboardLayout(): { keyboard: KeyboardLayout } { export default function useKeyboardLayout() {
const { keyboardLayout } = useSettingsStore(); const { keyboardLayout } = useSettingsStore();
const keyboardOptions = useMemo(() => {
return keyboards.map((keyboard) => {
return { label: keyboard.name, value: keyboard.isoCode }
});
}, []);
const isoCode = useMemo(() => { const isoCode = useMemo(() => {
// If we don't have a specific layout, default to "en-US" because that was the original layout // If we don't have a specific layout, default to "en-US" because that was the original layout
// developed so it is a good fallback. Additionally, we replace "en_US" with "en-US" because // developed so it is a good fallback. Additionally, we replace "en_US" with "en-US" because
@ -13,15 +19,17 @@ export function useKeyboardLayout(): { keyboard: KeyboardLayout } {
// ISO code for English/United State. To ensure we remain backward compatible with devices that // ISO code for English/United State. To ensure we remain backward compatible with devices that
// have not had their Keyboard Layout selected by the user, we want to treat "en_US" as if it was // have not had their Keyboard Layout selected by the user, we want to treat "en_US" as if it was
// "en-US" to match the ISO standard codes now used in the keyboardLayouts. // "en-US" to match the ISO standard codes now used in the keyboardLayouts.
console.log("Current keyboard layout from store:", keyboardLayout); console.debug("Current keyboard layout from store:", keyboardLayout);
if (keyboardLayout && keyboardLayout.length > 0) if (keyboardLayout && keyboardLayout.length > 0)
return keyboardLayout.replace("en_US", "en-US"); return keyboardLayout.replace("en_US", "en-US");
return "en-US"; return "en-US";
}, [keyboardLayout]); }, [keyboardLayout]);
const keyboard = useMemo(() => { const selectedKeyboard = useMemo(() => {
return selectedKeyboard(isoCode); // fallback to original behaviour of en-US if no isoCode given or matching layout not found
return keyboards.find(keyboard => keyboard.isoCode === isoCode)
?? keyboards.find(keyboard => keyboard.isoCode === "en-US")!;
}, [isoCode]); }, [isoCode]);
return { keyboard }; return { keyboardOptions, isoCode, selectedKeyboard };
} }

View File

@ -14,7 +14,7 @@ export interface KeyboardLayout {
}; };
} }
// to add a new layout, create a file like the above and add it to the list // To add a new layout, create a file like the above and add it to the list
import { cs_CZ } from "@/keyboardLayouts/cs_CZ" import { cs_CZ } from "@/keyboardLayouts/cs_CZ"
import { de_CH } from "@/keyboardLayouts/de_CH" import { de_CH } from "@/keyboardLayouts/de_CH"
import { de_DE } from "@/keyboardLayouts/de_DE" import { de_DE } from "@/keyboardLayouts/de_DE"
@ -29,15 +29,3 @@ import { nb_NO } from "@/keyboardLayouts/nb_NO"
import { sv_SE } from "@/keyboardLayouts/sv_SE" import { sv_SE } from "@/keyboardLayouts/sv_SE"
export const keyboards: KeyboardLayout[] = [ cs_CZ, de_CH, de_DE, en_UK, en_US, es_ES, fr_BE, fr_CH, fr_FR, it_IT, nb_NO, sv_SE ]; export const keyboards: KeyboardLayout[] = [ cs_CZ, de_CH, de_DE, en_UK, en_US, es_ES, fr_BE, fr_CH, fr_FR, it_IT, nb_NO, sv_SE ];
export const selectedKeyboard = (isoCode: string): KeyboardLayout => {
// fallback to original behaviour of en-US if no isoCode given or matching layout not found
return keyboards.find(keyboard => keyboard.isoCode == isoCode)
?? keyboards.find(keyboard => keyboard.isoCode == "en-US")!;
};
export const keyboardOptions = () => {
return keyboards.map((keyboard) => {
return { label: keyboard.name, value: keyboard.isoCode }
});
}

View File

@ -2,11 +2,10 @@ import { useCallback, useEffect } from "react";
import { useSettingsStore } from "@/hooks/stores"; import { useSettingsStore } from "@/hooks/stores";
import { useJsonRpc } from "@/hooks/useJsonRpc"; import { useJsonRpc } from "@/hooks/useJsonRpc";
import { useKeyboardLayout } from "@/hooks/useKeyboardLayout"; import useKeyboardLayout from "@/hooks/useKeyboardLayout";
import { SettingsPageHeader } from "@components/SettingsPageheader"; import { SettingsPageHeader } from "@components/SettingsPageheader";
import { Checkbox } from "@/components/Checkbox"; import { Checkbox } from "@/components/Checkbox";
import { SelectMenuBasic } from "@/components/SelectMenuBasic"; import { SelectMenuBasic } from "@/components/SelectMenuBasic";
import { keyboardOptions } from "@/keyboardLayouts";
import notifications from "@/notifications"; import notifications from "@/notifications";
import { SettingsItem } from "./devices.$id.settings"; import { SettingsItem } from "./devices.$id.settings";
@ -14,8 +13,7 @@ import { SettingsItem } from "./devices.$id.settings";
export default function SettingsKeyboardRoute() { export default function SettingsKeyboardRoute() {
const { setKeyboardLayout } = useSettingsStore(); const { setKeyboardLayout } = useSettingsStore();
const { showPressedKeys, setShowPressedKeys } = useSettingsStore(); const { showPressedKeys, setShowPressedKeys } = useSettingsStore();
const { keyboard } = useKeyboardLayout(); const { selectedKeyboard, keyboardOptions } = useKeyboardLayout();
const layoutOptions = keyboardOptions();
const { send } = useJsonRpc(); const { send } = useJsonRpc();
@ -62,9 +60,9 @@ export default function SettingsKeyboardRoute() {
size="SM" size="SM"
label="" label=""
fullWidth fullWidth
value={keyboard.isoCode} value={selectedKeyboard.isoCode}
onChange={onKeyboardLayoutChange} onChange={onKeyboardLayoutChange}
options={layoutOptions} options={keyboardOptions}
/> />
</SettingsItem> </SettingsItem>
<p className="text-xs text-slate-600 dark:text-slate-400"> <p className="text-xs text-slate-600 dark:text-slate-400">

View File

@ -20,7 +20,7 @@ import { MAX_TOTAL_MACROS, COPY_SUFFIX, DEFAULT_DELAY } from "@/constants/macros
import notifications from "@/notifications"; import notifications from "@/notifications";
import { ConfirmDialog } from "@/components/ConfirmDialog"; import { ConfirmDialog } from "@/components/ConfirmDialog";
import LoadingSpinner from "@/components/LoadingSpinner"; import LoadingSpinner from "@/components/LoadingSpinner";
import { useKeyboardLayout } from "@/hooks/useKeyboardLayout"; import useKeyboardLayout from "@/hooks/useKeyboardLayout";
const normalizeSortOrders = (macros: KeySequence[]): KeySequence[] => { const normalizeSortOrders = (macros: KeySequence[]): KeySequence[] => {
return macros.map((macro, index) => ({ return macros.map((macro, index) => ({
@ -35,7 +35,7 @@ export default function SettingsMacrosRoute() {
const [actionLoadingId, setActionLoadingId] = useState<string | null>(null); const [actionLoadingId, setActionLoadingId] = useState<string | null>(null);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [macroToDelete, setMacroToDelete] = useState<KeySequence | null>(null); const [macroToDelete, setMacroToDelete] = useState<KeySequence | null>(null);
const { keyboard } = useKeyboardLayout(); const { selectedKeyboard } = useKeyboardLayout();
const isMaxMacrosReached = useMemo( const isMaxMacrosReached = useMemo(
() => macros.length >= MAX_TOTAL_MACROS, () => macros.length >= MAX_TOTAL_MACROS,
@ -186,7 +186,7 @@ export default function SettingsMacrosRoute() {
step.modifiers.map((modifier, idx) => ( step.modifiers.map((modifier, idx) => (
<Fragment key={`mod-${idx}`}> <Fragment key={`mod-${idx}`}>
<span className="font-medium text-slate-600 dark:text-slate-200"> <span className="font-medium text-slate-600 dark:text-slate-200">
{keyboard.modifierDisplayMap[modifier] || modifier} {selectedKeyboard.modifierDisplayMap[modifier] || modifier}
</span> </span>
{idx < step.modifiers.length - 1 && ( {idx < step.modifiers.length - 1 && (
<span className="text-slate-400 dark:text-slate-600"> <span className="text-slate-400 dark:text-slate-600">
@ -211,7 +211,7 @@ export default function SettingsMacrosRoute() {
step.keys.map((key, idx) => ( step.keys.map((key, idx) => (
<Fragment key={`key-${idx}`}> <Fragment key={`key-${idx}`}>
<span className="font-medium text-blue-600 dark:text-blue-400"> <span className="font-medium text-blue-600 dark:text-blue-400">
{keyboard.keyDisplayMap[key] || key} {selectedKeyboard.keyDisplayMap[key] || key}
</span> </span>
{idx < step.keys.length - 1 && ( {idx < step.keys.length - 1 && (
<span className="text-slate-400 dark:text-slate-600"> <span className="text-slate-400 dark:text-slate-600">
@ -298,8 +298,8 @@ export default function SettingsMacrosRoute() {
actionLoadingId, actionLoadingId,
handleDeleteMacro, handleDeleteMacro,
handleMoveMacro, handleMoveMacro,
keyboard.modifierDisplayMap, selectedKeyboard.modifierDisplayMap,
keyboard.keyDisplayMap, selectedKeyboard.keyDisplayMap,
handleDuplicateMacro, handleDuplicateMacro,
navigate navigate
], ],

View File

@ -20,7 +20,6 @@ import { LinkButton } from "@/components/Button";
import { FeatureFlag } from "@/components/FeatureFlag"; import { FeatureFlag } from "@/components/FeatureFlag";
import LoadingSpinner from "@/components/LoadingSpinner"; import LoadingSpinner from "@/components/LoadingSpinner";
import { useUiStore } from "@/hooks/stores"; import { useUiStore } from "@/hooks/stores";
import useKeyboard from "@/hooks/useKeyboard";
import { cx } from "../cva.config"; import { cx } from "../cva.config";
@ -28,7 +27,6 @@ import { cx } from "../cva.config";
export default function SettingsRoute() { export default function SettingsRoute() {
const location = useLocation(); const location = useLocation();
const { setDisableVideoFocusTrap } = useUiStore(); const { setDisableVideoFocusTrap } = useUiStore();
const { resetKeyboardState } = useKeyboard();
const scrollContainerRef = useRef<HTMLDivElement>(null); const scrollContainerRef = useRef<HTMLDivElement>(null);
const [showLeftGradient, setShowLeftGradient] = useState(false); const [showLeftGradient, setShowLeftGradient] = useState(false);
const [showRightGradient, setShowRightGradient] = useState(false); const [showRightGradient, setShowRightGradient] = useState(false);
@ -66,13 +64,12 @@ export default function SettingsRoute() {
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {
setDisableVideoFocusTrap(true); setDisableVideoFocusTrap(true);
resetKeyboardState();
}, 500); }, 500);
return () => { return () => {
setDisableVideoFocusTrap(false); setDisableVideoFocusTrap(false);
}; };
}, [resetKeyboardState, setDisableVideoFocusTrap]); }, [setDisableVideoFocusTrap]);
return ( return (
<div className="pointer-events-auto relative mx-auto max-w-4xl translate-x-0 transform text-left dark:text-white"> <div className="pointer-events-auto relative mx-auto max-w-4xl translate-x-0 transform text-left dark:text-white">