* Add comprehensive Chinese internationalization support
- Implemented i18n framework with react-i18next for multi-language support
- Added Chinese (zh) and English (en) translation files with comprehensive coverage
- Localized Admin interface, authentication flows, and error messages
- Translated FileManager operations and UI elements
- Updated HomepageAuth component with localized authentication messages
- Localized LeftSidebar navigation and host management
- Added language switcher component (shown after login only)
- Configured default language as English with Chinese as secondary option
- Localized TOTPSetup two-factor authentication interface
- Updated Docker build to include translation files
- Achieved 95%+ UI localization coverage across core components
Co-Authored-By: Claude <noreply@anthropic.com>
* Extend Chinese localization coverage to Host Manager components
- Added comprehensive translations for HostManagerHostViewer component
- Localized all host management UI text including import/export features
- Translated error messages and confirmation dialogs for host operations
- Added translations for HostManagerHostEditor validation messages
- Localized connection details, organization settings, and form labels
- Fixed syntax error in FileManagerOperations component
- Achieved near-complete localization of SSH host management interface
- Updated placeholders and tooltips for better user guidance
Co-Authored-By: Claude <noreply@anthropic.com>
* Complete comprehensive Chinese localization for Termix
- Added full localization support for Tunnel components (connected/disconnected states, retry messages)
- Localized all tunnel status messages and connection errors
- Added translations for port forwarding UI elements
- Verified Server, TopNavbar, and Tab components already have complete i18n support
- Achieved 99%+ localization coverage across entire application
- All core UI components now fully support Chinese and English languages
This completes the comprehensive internationalization effort for the Termix SSH management platform.
Co-Authored-By: Claude <noreply@anthropic.com>
* Localize additional Host Manager components and authentication settings
- Added translations for all authentication options (Password, Key, SSH Private Key)
- Localized form labels in HostManagerHostEditor (Pin Connection, Enable Terminal/Tunnel/FileManager)
- Translated Upload/Update Key button states
- Localized Host Viewer and Add/Edit Host tab labels
- Added Chinese translations for all host management settings
- Fixed duplicate translation keys in JSON files
Co-Authored-By: Claude <noreply@anthropic.com>
* Extend localization coverage to UI components and common strings
- Added comprehensive common translations (online/offline, success/error, etc.)
- Localized status indicator component with all status states
- Updated FileManagerLeftSidebar toast messages for rename/delete operations
- Added translations for UI elements (close, toggle sidebar, etc.)
- Expanded placeholder translations for form inputs
- Added Chinese translations for all new common strings
- Improved consistency across component status messages
Co-Authored-By: Claude <noreply@anthropic.com>
* Complete Chinese localization for remaining UI components
- Add comprehensive Chinese translations for Host Manager component
- Translate all form labels, buttons, and descriptions
- Add translations for SSH configuration warnings and instructions
- Localize tunnel connection settings and port forwarding options
- Localize SSH Tools panel
- Translate key recording functionality
- Add translations for settings and configuration options
- Translate homepage welcome messages and navigation elements
- Add Chinese translations for login success messages
- Localize "Updates & Releases" section title
- Translate sidebar "Host Manager" button
- Fix translation key display issues
- Remove duplicate translation keys in both language files
- Ensure all components properly reference translation keys
- Fix hosts.tunnelConnections key mapping
This completes the full Chinese localization of the Termix application,
achieving near 100% UI translation coverage while maintaining English
as the default language.
* Complete final Chinese localization for Host Manager tunnel configuration
- Add Chinese translations for authentication UI elements
- Translate "Authentication", "Password", and "Key" tab labels
- Localize SSH private key and key password fields
- Add translations for key type selector
- Localize tunnel connection configuration descriptions
- Translate retry attempts and retry interval descriptions
- Add dynamic tunnel forwarding description with port parameters
- Localize endpoint SSH configuration labels
- Fix missing translation keys
- Add "upload" translation for file upload button
- Ensure all FormLabel and FormDescription elements use translation keys
This completes the comprehensive Chinese localization of the entire
Termix application, achieving 100% UI translation coverage.
* Fix PR feedback: Improve Profile section translations and UX
- Fixed password reset translations in Profile section
- Moved language selector from TopNavbar to Profile page
- Added profile.selectPreferredLanguage translation key
- Improved user experience for language preferences
* Apply critical OIDC and notification system fixes while preserving i18n
- Merge OIDC authentication fixes from 3877e90:
* Enhanced JWKS discovery mechanism with multiple backup URLs
* Better support for non-standard OIDC providers (Authentik, etc.)
* Improved error handling for "Failed to get user information"
- Migrate to unified Sonner toast notification system:
* Replace custom success/error state management
* Remove redundant alert state variables
* Consistent user feedback across all components
- Improve code quality and function naming conventions
- PRESERVE all existing i18n functionality and Chinese translations
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix OIDC errors for "Failed to get user information"
* Fix OIDC errors for "Failed to get user information"
* Fix spelling error
* Migrate everything to alert system, update user.ts for OIDC updates.
* Fix OIDC errors for "Failed to get user information"
* Fix OIDC errors for "Failed to get user information"
* Fix spelling error
* Migrate everything to alert system, update user.ts for OIDC updates.
* Update env
* Fix users.ts and schema for override
* Convert web app to Electron desktop application
- Add Electron main process with developer tools support
- Create preload script for secure context bridge
- Configure electron-builder for packaging
- Update Vite config for Electron compatibility (base: './')
- Add environment variable support for API host configuration
- Fix i18n to use relative paths for Electron file protocol
- Restore multi-port backend architecture (8081-8085)
- Add enhanced backend startup script with port checking
- Update package.json with Electron dependencies and build scripts
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Complete Electron desktop application implementation
- Add backend auto-start functionality in main process
- Fix authentication token storage for Electron environment
- Implement localStorage-based token management in Electron
- Add proper Electron environment detection via preload script
- Fix WebSocket connections for terminal functionality
- Resolve font file loading issues in packaged application
- Update API endpoints to work with backend auto-start
- Streamline build scripts with unified electron:package command
- Fix better-sqlite3 native module compatibility issues
- Ensure all services start automatically in production mode
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Remove releases folder from git and force Desktop UI.
* Improve mobile support with half-baked custom keyboard
* Fix API routing
* Upgrade mobile keyboard with more keys.
* Add cross-platform support and clean up obsolete files
- Add electron-packager scripts for Windows, macOS, and Linux
- Include universal architecture support for macOS
- Add electron:package:all for building all platforms
- Remove obsolete start-backend.sh script (replaced by Electron auto-start)
- Improve ignore patterns to exclude repo-images folder
- Add platform-specific icon configurations
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix build system by removing electron-builder dependency
- Remove electron-builder and @electron/rebuild packages to resolve build errors
- Clean up package.json scripts that depend on electron-builder
- Fix merge conflict markers in AdminSettings.tsx and PasswordReset.tsx
- All build commands now work correctly:
- npm run build (frontend + backend)
- npm run build:frontend
- npm run build:backend
- npm run electron:package (using electron-packager)
The build system is now stable and functional without signing requirements.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
423 lines
14 KiB
TypeScript
423 lines
14 KiB
TypeScript
import {useEffect, useRef, useState, useImperativeHandle, forwardRef} from 'react';
|
|
import {useXTerm} from 'react-xtermjs';
|
|
import {FitAddon} from '@xterm/addon-fit';
|
|
import {ClipboardAddon} from '@xterm/addon-clipboard';
|
|
import {Unicode11Addon} from '@xterm/addon-unicode11';
|
|
import {WebLinksAddon} from '@xterm/addon-web-links';
|
|
import {useTranslation} from 'react-i18next';
|
|
|
|
interface SSHTerminalProps {
|
|
hostConfig: any;
|
|
isVisible: boolean;
|
|
title?: string;
|
|
showTitle?: boolean;
|
|
splitScreen?: boolean;
|
|
}
|
|
|
|
export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|
{hostConfig, isVisible, splitScreen = false},
|
|
ref
|
|
) {
|
|
const {t} = useTranslation();
|
|
const {instance: terminal, ref: xtermRef} = useXTerm();
|
|
const fitAddonRef = useRef<FitAddon | null>(null);
|
|
const webSocketRef = useRef<WebSocket | null>(null);
|
|
const resizeTimeout = useRef<NodeJS.Timeout | null>(null);
|
|
const wasDisconnectedBySSH = useRef(false);
|
|
const pingIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
|
const [visible, setVisible] = useState(false);
|
|
const isVisibleRef = useRef<boolean>(false);
|
|
|
|
|
|
const lastSentSizeRef = useRef<{ cols: number; rows: number } | null>(null);
|
|
const pendingSizeRef = useRef<{ cols: number; rows: number } | null>(null);
|
|
const notifyTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
const DEBOUNCE_MS = 140;
|
|
|
|
useEffect(() => {
|
|
isVisibleRef.current = isVisible;
|
|
}, [isVisible]);
|
|
|
|
function hardRefresh() {
|
|
try {
|
|
if (terminal && typeof (terminal as any).refresh === 'function') {
|
|
(terminal as any).refresh(0, terminal.rows - 1);
|
|
}
|
|
} catch (_) {
|
|
}
|
|
}
|
|
|
|
function scheduleNotify(cols: number, rows: number) {
|
|
if (!(cols > 0 && rows > 0)) return;
|
|
pendingSizeRef.current = {cols, rows};
|
|
if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current);
|
|
notifyTimerRef.current = setTimeout(() => {
|
|
const next = pendingSizeRef.current;
|
|
const last = lastSentSizeRef.current;
|
|
if (!next) return;
|
|
if (last && last.cols === next.cols && last.rows === next.rows) return;
|
|
if (webSocketRef.current?.readyState === WebSocket.OPEN) {
|
|
webSocketRef.current.send(JSON.stringify({type: 'resize', data: next}));
|
|
lastSentSizeRef.current = next;
|
|
}
|
|
}, DEBOUNCE_MS);
|
|
}
|
|
|
|
useImperativeHandle(ref, () => ({
|
|
disconnect: () => {
|
|
if (pingIntervalRef.current) {
|
|
clearInterval(pingIntervalRef.current);
|
|
pingIntervalRef.current = null;
|
|
}
|
|
webSocketRef.current?.close();
|
|
},
|
|
fit: () => {
|
|
fitAddonRef.current?.fit();
|
|
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
|
|
hardRefresh();
|
|
},
|
|
sendInput: (data: string) => {
|
|
if (webSocketRef.current?.readyState === 1) {
|
|
webSocketRef.current.send(JSON.stringify({type: 'input', data}));
|
|
}
|
|
},
|
|
notifyResize: () => {
|
|
try {
|
|
const cols = terminal?.cols ?? undefined;
|
|
const rows = terminal?.rows ?? undefined;
|
|
if (typeof cols === 'number' && typeof rows === 'number') {
|
|
scheduleNotify(cols, rows);
|
|
hardRefresh();
|
|
}
|
|
} catch (_) {
|
|
}
|
|
},
|
|
refresh: () => hardRefresh(),
|
|
}), [terminal]);
|
|
|
|
useEffect(() => {
|
|
window.addEventListener('resize', handleWindowResize);
|
|
return () => window.removeEventListener('resize', handleWindowResize);
|
|
}, []);
|
|
|
|
function handleWindowResize() {
|
|
if (!isVisibleRef.current) return;
|
|
fitAddonRef.current?.fit();
|
|
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
|
|
hardRefresh();
|
|
}
|
|
|
|
function getCookie(name: string) {
|
|
return document.cookie.split('; ').reduce((r, v) => {
|
|
const parts = v.split('=');
|
|
return parts[0] === name ? decodeURIComponent(parts[1]) : r;
|
|
}, "");
|
|
}
|
|
|
|
function getUseRightClickCopyPaste() {
|
|
return getCookie("rightClickCopyPaste") === "true"
|
|
}
|
|
|
|
|
|
|
|
function setupWebSocketListeners(ws: WebSocket, cols: number, rows: number) {
|
|
ws.addEventListener('open', () => {
|
|
|
|
ws.send(JSON.stringify({type: 'connectToHost', data: {cols, rows, hostConfig}}));
|
|
terminal.onData((data) => {
|
|
ws.send(JSON.stringify({type: 'input', data}));
|
|
});
|
|
|
|
pingIntervalRef.current = setInterval(() => {
|
|
if (ws.readyState === WebSocket.OPEN) {
|
|
ws.send(JSON.stringify({type: 'ping'}));
|
|
}
|
|
}, 30000);
|
|
|
|
|
|
});
|
|
|
|
ws.addEventListener('message', (event) => {
|
|
try {
|
|
const msg = JSON.parse(event.data);
|
|
if (msg.type === 'data') terminal.write(msg.data);
|
|
else if (msg.type === 'error') terminal.writeln(`\r\n[${t('terminal.error')}] ${msg.message}`);
|
|
else if (msg.type === 'connected') {
|
|
} else if (msg.type === 'disconnected') {
|
|
wasDisconnectedBySSH.current = true;
|
|
terminal.writeln(`\r\n[${msg.message || t('terminal.disconnected')}]`);
|
|
}
|
|
} catch (error) {
|
|
}
|
|
});
|
|
|
|
ws.addEventListener('close', () => {
|
|
if (!wasDisconnectedBySSH.current) {
|
|
terminal.writeln(`\r\n[${t('terminal.connectionClosed')}]`);
|
|
}
|
|
});
|
|
|
|
ws.addEventListener('error', () => {
|
|
terminal.writeln(`\r\n[${t('terminal.connectionError')}]`);
|
|
});
|
|
}
|
|
|
|
async function writeTextToClipboard(text: string): Promise<void> {
|
|
try {
|
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
await navigator.clipboard.writeText(text);
|
|
return;
|
|
}
|
|
} catch (_) {
|
|
}
|
|
const textarea = document.createElement('textarea');
|
|
textarea.value = text;
|
|
textarea.style.position = 'fixed';
|
|
textarea.style.left = '-9999px';
|
|
document.body.appendChild(textarea);
|
|
textarea.focus();
|
|
textarea.select();
|
|
try {
|
|
document.execCommand('copy');
|
|
} finally {
|
|
document.body.removeChild(textarea);
|
|
}
|
|
}
|
|
|
|
async function readTextFromClipboard(): Promise<string> {
|
|
try {
|
|
if (navigator.clipboard && navigator.clipboard.readText) {
|
|
return await navigator.clipboard.readText();
|
|
}
|
|
} catch (_) {
|
|
}
|
|
return '';
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (!terminal || !xtermRef.current || !hostConfig) return;
|
|
|
|
terminal.options = {
|
|
cursorBlink: true,
|
|
cursorStyle: 'bar',
|
|
scrollback: 10000,
|
|
fontSize: 14,
|
|
fontFamily: '"JetBrains Mono Nerd Font", "MesloLGS NF", "FiraCode Nerd Font", "Cascadia Code", "JetBrains Mono", Consolas, "Courier New", monospace',
|
|
theme: {background: '#18181b', foreground: '#f7f7f7'},
|
|
allowTransparency: true,
|
|
convertEol: true,
|
|
windowsMode: false,
|
|
macOptionIsMeta: false,
|
|
macOptionClickForcesSelection: false,
|
|
rightClickSelectsWord: false,
|
|
fastScrollModifier: 'alt',
|
|
fastScrollSensitivity: 5,
|
|
allowProposedApi: true,
|
|
};
|
|
|
|
const fitAddon = new FitAddon();
|
|
const clipboardAddon = new ClipboardAddon();
|
|
const unicode11Addon = new Unicode11Addon();
|
|
const webLinksAddon = new WebLinksAddon();
|
|
|
|
fitAddonRef.current = fitAddon;
|
|
terminal.loadAddon(fitAddon);
|
|
terminal.loadAddon(clipboardAddon);
|
|
terminal.loadAddon(unicode11Addon);
|
|
terminal.loadAddon(webLinksAddon);
|
|
terminal.open(xtermRef.current);
|
|
|
|
const element = xtermRef.current;
|
|
const handleContextMenu = async (e: MouseEvent) => {
|
|
if (!getUseRightClickCopyPaste()) return;
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
try {
|
|
if (terminal.hasSelection()) {
|
|
const selection = terminal.getSelection();
|
|
if (selection) {
|
|
await writeTextToClipboard(selection);
|
|
terminal.clearSelection();
|
|
}
|
|
} else {
|
|
const pasteText = await readTextFromClipboard();
|
|
if (pasteText) terminal.paste(pasteText);
|
|
}
|
|
} catch (_) {
|
|
}
|
|
};
|
|
element?.addEventListener('contextmenu', handleContextMenu);
|
|
|
|
const resizeObserver = new ResizeObserver(() => {
|
|
if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
|
|
resizeTimeout.current = setTimeout(() => {
|
|
if (!isVisibleRef.current) return;
|
|
fitAddonRef.current?.fit();
|
|
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
|
|
hardRefresh();
|
|
}, 100);
|
|
});
|
|
|
|
resizeObserver.observe(xtermRef.current);
|
|
|
|
const readyFonts = (document as any).fonts?.ready instanceof Promise ? (document as any).fonts.ready : Promise.resolve();
|
|
readyFonts.then(() => {
|
|
setTimeout(() => {
|
|
fitAddon.fit();
|
|
setTimeout(() => {
|
|
fitAddon.fit();
|
|
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
|
|
hardRefresh();
|
|
setVisible(true);
|
|
if (terminal && !splitScreen) {
|
|
terminal.focus();
|
|
}
|
|
}, 0);
|
|
|
|
const cols = terminal.cols;
|
|
const rows = terminal.rows;
|
|
|
|
const isDev = process.env.NODE_ENV === 'development' &&
|
|
(window.location.port === '3000' || window.location.port === '5173' || window.location.port === '');
|
|
|
|
const isElectron = (window as any).IS_ELECTRON === true || (window as any).electronAPI?.isElectron === true;
|
|
|
|
const wsUrl = isDev
|
|
? 'ws://localhost:8082'
|
|
: isElectron
|
|
? 'ws://127.0.0.1:8082'
|
|
: `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/ssh/websocket/`;
|
|
|
|
const ws = new WebSocket(wsUrl);
|
|
webSocketRef.current = ws;
|
|
wasDisconnectedBySSH.current = false;
|
|
|
|
setupWebSocketListeners(ws, cols, rows);
|
|
}, 300);
|
|
});
|
|
|
|
return () => {
|
|
resizeObserver.disconnect();
|
|
element?.removeEventListener('contextmenu', handleContextMenu);
|
|
if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current);
|
|
if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
|
|
if (pingIntervalRef.current) {
|
|
clearInterval(pingIntervalRef.current);
|
|
pingIntervalRef.current = null;
|
|
}
|
|
webSocketRef.current?.close();
|
|
};
|
|
}, [xtermRef, terminal, hostConfig]);
|
|
|
|
useEffect(() => {
|
|
if (isVisible && fitAddonRef.current) {
|
|
setTimeout(() => {
|
|
fitAddonRef.current?.fit();
|
|
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
|
|
hardRefresh();
|
|
if (terminal && !splitScreen) {
|
|
terminal.focus();
|
|
}
|
|
}, 0);
|
|
|
|
if (terminal && !splitScreen) {
|
|
setTimeout(() => {
|
|
terminal.focus();
|
|
}, 100);
|
|
}
|
|
}
|
|
}, [isVisible, splitScreen, terminal]);
|
|
|
|
useEffect(() => {
|
|
if (!fitAddonRef.current) return;
|
|
setTimeout(() => {
|
|
fitAddonRef.current?.fit();
|
|
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
|
|
hardRefresh();
|
|
if (terminal && !splitScreen && isVisible) {
|
|
terminal.focus();
|
|
}
|
|
}, 0);
|
|
}, [splitScreen, isVisible, terminal]);
|
|
|
|
return (
|
|
<div
|
|
ref={xtermRef}
|
|
className="h-full w-full m-1"
|
|
style={{opacity: visible && isVisible ? 1 : 0, overflow: 'hidden'}}
|
|
onClick={() => {
|
|
if (terminal && !splitScreen) {
|
|
terminal.focus();
|
|
}
|
|
}}
|
|
/>
|
|
);
|
|
});
|
|
|
|
const style = document.createElement('style');
|
|
style.innerHTML = `
|
|
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap');
|
|
|
|
/* Load NerdFonts locally */
|
|
@font-face {
|
|
font-family: 'JetBrains Mono Nerd Font';
|
|
src: url('./fonts/JetBrainsMonoNerdFont-Regular.ttf') format('truetype');
|
|
font-weight: normal;
|
|
font-style: normal;
|
|
font-display: swap;
|
|
}
|
|
|
|
@font-face {
|
|
font-family: 'JetBrains Mono Nerd Font';
|
|
src: url('./fonts/JetBrainsMonoNerdFont-Bold.ttf') format('truetype');
|
|
font-weight: bold;
|
|
font-style: normal;
|
|
font-display: swap;
|
|
}
|
|
|
|
@font-face {
|
|
font-family: 'JetBrains Mono Nerd Font';
|
|
src: url('./fonts/JetBrainsMonoNerdFont-Italic.ttf') format('truetype');
|
|
font-weight: normal;
|
|
font-style: italic;
|
|
font-display: swap;
|
|
}
|
|
|
|
.xterm .xterm-viewport::-webkit-scrollbar {
|
|
width: 8px;
|
|
background: transparent;
|
|
}
|
|
.xterm .xterm-viewport::-webkit-scrollbar-thumb {
|
|
background: rgba(180,180,180,0.7);
|
|
border-radius: 4px;
|
|
}
|
|
.xterm .xterm-viewport::-webkit-scrollbar-thumb:hover {
|
|
background: rgba(120,120,120,0.9);
|
|
}
|
|
.xterm .xterm-viewport {
|
|
scrollbar-width: thin;
|
|
scrollbar-color: rgba(180,180,180,0.7) transparent;
|
|
}
|
|
|
|
.xterm {
|
|
font-feature-settings: "liga" 1, "calt" 1;
|
|
text-rendering: optimizeLegibility;
|
|
-webkit-font-smoothing: antialiased;
|
|
-moz-osx-font-smoothing: grayscale;
|
|
}
|
|
|
|
.xterm .xterm-screen {
|
|
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font', 'Cascadia Code', 'JetBrains Mono', Consolas, "Courier New", monospace !important;
|
|
font-variant-ligatures: contextual;
|
|
}
|
|
|
|
.xterm .xterm-screen .xterm-char {
|
|
font-feature-settings: "liga" 1, "calt" 1;
|
|
}
|
|
|
|
.xterm .xterm-screen .xterm-char[data-char-code^="\\uE"] {
|
|
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|