TrdHuy / TrdHuy.github.io

0 stars 0 forks source link

Custom control implementation #8

Open TrdHuy opened 4 weeks ago

TrdHuy commented 4 weeks ago
class HorImageContainer extends HTMLElement {
    constructor() {
        super();
        const shadowRoot = this.attachShadow({ mode: 'closed' });

        const scrollPanel = document.createElement('div');
        scrollPanel.className = 'scroll-panel';
        scrollPanel.style.position = 'relative';
        scrollPanel.style.width = '100%';
        scrollPanel.style.overflow = 'hidden';
        scrollPanel.style.cursor = 'grab';
        scrollPanel.style.userSelect = 'none';

        this.imageContainer = document.createElement('div');
        const imageContainer = this.imageContainer;
        imageContainer.className = 'image-container';
        imageContainer.style.display = 'flex';
        imageContainer.style.overflowX = 'auto';
        imageContainer.style.whiteSpace = 'nowrap';
        imageContainer.style.padding = '10px';
        imageContainer.style.scrollBehavior = 'smooth';
        // Nhập nội dung từ slot
        const slot = document.createElement('slot');
        imageContainer.appendChild(slot);

        // Nút mũi tên trái
        const leftButton = document.createElement('button');
        leftButton.className = 'arrow-button arrow-left';
        leftButton.innerHTML = '‹';
        leftButton.style.position = 'absolute';
        leftButton.style.top = '50%';
        leftButton.style.transform = 'translateY(-50%)';
        leftButton.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
        leftButton.style.color = 'white';
        leftButton.style.border = 'none';
        leftButton.style.padding = '10px';
        leftButton.style.cursor = 'pointer';
        leftButton.style.display = 'none';
        leftButton.style.zIndex = '1';
        leftButton.style.left = '0';
        leftButton.onclick = () => this.scrollLeft();

        // Nút mũi tên phải
        const rightButton = document.createElement('button');
        rightButton.className = 'arrow-button arrow-right';
        rightButton.innerHTML = '›';
        rightButton.style.position = 'absolute';
        rightButton.style.top = '50%';
        rightButton.style.transform = 'translateY(-50%)';
        rightButton.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
        rightButton.style.color = 'white';
        rightButton.style.border = 'none';
        rightButton.style.padding = '10px';
        rightButton.style.cursor = 'pointer';
        rightButton.style.display = 'none';
        rightButton.style.zIndex = '1';
        rightButton.style.right = '0';
        rightButton.onclick = () => this.scrollRight();

        // Thêm các phần tử vào panel cuộn
        scrollPanel.appendChild(imageContainer);
        scrollPanel.appendChild(leftButton);
        scrollPanel.appendChild(rightButton);

        const style = document.createElement('style');
        style.textContent = `.image-container::-webkit-scrollbar{height: 8px;}
.image-container::-webkit-scrollbar-track{background: #f1f1f1;}
.image-container::-webkit-scrollbar-thumb{background:#888;border-radius:10px;}
.image-container::-webkit-scrollbar-thumb:hover{background: #555;}`
        // Thêm panel cuộn vào shadow DOM
        shadowRoot.append(style, scrollPanel);

        // Sự kiện kéo để cuộn
        let isDown = false;
        let startX;
        let scrollLeft;

        scrollPanel.addEventListener('mousedown', (e) => {
            isDown = true;
            scrollPanel.classList.add('active');
            startX = e.pageX - imageContainer.offsetLeft;
            scrollLeft = imageContainer.scrollLeft;
            imageContainer.style.pointerEvents = 'none';
        });

        scrollPanel.addEventListener('mouseleave', () => {
            isDown = false;
            scrollPanel.classList.remove('active');
            imageContainer.style.pointerEvents = 'all';
        });

        scrollPanel.addEventListener('mouseup', () => {
            isDown = false;
            scrollPanel.classList.remove('active');
            imageContainer.style.pointerEvents = 'all';
        });

        scrollPanel.addEventListener('mousemove', (e) => {
            if (!isDown) return;
            e.preventDefault();
            const x = e.pageX - imageContainer.offsetLeft;
            const walk = (x - startX) * 2;
            imageContainer.scrollLeft = scrollLeft - walk;
        });

        // Cuộn bằng chuột
        scrollPanel.addEventListener('wheel', (e) => {
            e.preventDefault();
            imageContainer.scrollLeft += e.deltaY;
        });

        // Hiển thị nút khi hover vào scrollPanel
        scrollPanel.addEventListener('mouseover', () => {
            leftButton.style.display = 'block';
            rightButton.style.display = 'block';
        });

        scrollPanel.addEventListener('mouseout', () => {
            leftButton.style.display = 'none';
            rightButton.style.display = 'none';
        });
    }

    scrollLeft() {
        const imageContainer = this.imageContainer;
        imageContainer.scrollBy({
            left: -300,
            behavior: 'smooth'
        });
    }

    scrollRight() {
        const imageContainer = this.imageContainer;
        imageContainer.scrollBy({
            left: 300,
            behavior: 'smooth'
        });
    }

}
customElements.define('hor-image-container', HorImageContainer);

class LoadingImage extends HTMLElement {
    constructor() {
        super();
        this.host = this.attachShadow({ mode: 'closed' });
        const shadowRoot = this.host;
        shadowRoot.host.style.overflow = 'clip';
        shadowRoot.host.style.display = 'flex';
        shadowRoot.host.style.justifyContent = 'center';
        shadowRoot.host.style.alignItems = 'center';

        // Tạo container
        this.container = document.createElement('div');
        const container = this.container;
        container.className = 'image-container';

        // Tạo spinner
        this.spinerContainer = document.createElement('div');
        this.spinerContainer.style.width = '300px';
        this.spinerContainer.style.top = '50%';
        this.spinerContainer.style.display = 'flex';
        this.spinerContainer.style.justifyContent = 'center';
        this.spinner = document.createElement('div');
        const spinner = this.spinner;
        spinner.style.border = '4px solid rgba(0, 0, 0, 0.1)';
        spinner.style.borderLeftColor = '#000';
        spinner.style.borderRadius = '50%';
        spinner.style.width = '50px';
        spinner.style.height = '50px';
        spinner.style.zIndex = '10';
        spinner.style.animation = 'spin 1s linear infinite';

        // Tạo thẻ img
        this.img = document.createElement('img');
        const img = this.img;
        img.style.maxWidth = '100%';
        img.style.maxHeight = '100%';
        img.style.display = 'none';

        // Đặt thuộc tính src từ thuộc tính của custom element
        this.imgSrc = this.getAttribute('src');
        const altText = this.getAttribute('alt');
        img.alt = altText;

        img.onload = () => {
            spinner.style.display = 'none';
            img.style.display = 'block';

            // Cập nhật tỷ lệ của container dựa trên tỷ lệ của ảnh
            const aspectRatio = img.naturalWidth / img.naturalHeight;
            const parentWidth = 0;
            const parentHeight = shadowRoot.host.clientHeight;
            if (parentWidth != 0 && parentHeight != 0) {
                if (parentWidth / parentHeight > aspectRatio) {
                    container.style.height = `${parentHeight}px`;
                    container.style.width = `${parentHeight * aspectRatio}px`;
                } else {
                    container.style.width = `${parentWidth}px`;
                    container.style.height = `${parentWidth / aspectRatio}px`;
                }
            } else if (parentHeight != 0) {
                container.style.height = `${parentHeight}px`;
                container.style.width = `${parentHeight * aspectRatio}px`;
            }

        };

        img.onerror = () => {
            spinner.style.display = 'none';
            console.error('Image failed to load');
        };

        img.style.display = 'none';
        this.spinerContainer.appendChild(spinner);
        container.appendChild(this.spinerContainer);
        container.appendChild(img);

        const style = document.createElement('style');
        style.textContent = `@keyframes spin{0%{transform:rotate(0deg);}100% {transform: rotate(360deg);}}`;

        shadowRoot.append(style, container);
    }

    connectedCallback() {
        const customContainer = this.closest('hor-image-container');
        // Nếu ở bên trong custom-image-container thì apply cơ chế lazy load theo IntersectionObserver 
        if (customContainer) {
            const observer = new IntersectionObserver(entries => {
                entries.forEach(entry => {
                    if (entry.isIntersecting) {
                        setTimeout(() => {
                            this.img.src = this.imgSrc;
                        }, 2000); // Delay for demonstration, can be removed
                        observer.unobserve(this);
                    }
                });
            }, {
                root: null, // set null thì element sẽ dựa theo kích thước cửa sổ 
                threshold: 0.1 // 10% of the image is visible
            });

            observer.observe(this);
        } else {
            this.img.src = this.imgSrc;
        }

    }

}
customElements.define('loading-image', LoadingImage);
TrdHuy commented 4 weeks ago
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const glob = require('glob');
const fs = require('fs');
const crypto = require('crypto');

class ReplaceJsonPathsPlugin {
  constructor() {
    this.assetMap = new Map();
  }

  apply(compiler) {
    compiler.hooks.compilation.tap('ReplaceJsonPathsPlugin', (compilation) => {
      // Hook into the processing of assets to capture original and hashed names
      compilation.hooks.processAssets.tap({
        name: 'ReplaceJsonPathsPlugin',
        stage: compilation.PROCESS_ASSETS_STAGE_ADDITIONAL,
      }, (assets) => {
        for (const assetName in assets) {
          // Chỉ xử lý các file hình ảnh
          if (assetName.match(/\.(png|jpe?g|gif|svg)$/)) {
            const assetInfo = compilation.getAsset(assetName);
            let sourcePath = assetInfo.info.sourceFilename; // Lấy đường dẫn gốc
            const newAssetPath = assetName; // Đường dẫn sau khi bị hash

            // Loại bỏ 'src/' ở đầu nếu tồn tại
            if (sourcePath.startsWith('src/')) {
              sourcePath = sourcePath.substring(4);
            }

            // Chuyển đổi đường dẫn để bắt đầu bằng './'
            const formattedSourcePath = './' + path.posix.normalize(sourcePath).replace(/\\/g, '/');
            const formattedNewAssetPath = './' + path.posix.normalize(newAssetPath).replace(/\\/g, '/');

            // Lưu vào map với key là đường dẫn gốc và value là đường dẫn đã hash
            this.assetMap.set(formattedSourcePath, formattedNewAssetPath);
          }
        }
      });
    });

    compiler.hooks.emit.tapAsync('ReplaceJsonPathsPlugin', (compilation, callback) => {
      const assets = Object.keys(compilation.assets);

      // Lấy ra danh sách các file JSON từ assets
      assets.forEach((asset) => {
        if (asset.endsWith('.json')) {
          const assetPath = compilation.assets[asset].source();

          let jsonContent = JSON.parse(assetPath);

          // Thay thế đường dẫn hình ảnh trong JSON
          jsonContent = jsonContent.map(item => {
            // Duyệt qua tất cả các thuộc tính của đối tượng
            for (let key in item) {
              if (item.hasOwnProperty(key)) {
                const originalValue = item[key];
                const newValue = this.assetMap.get(originalValue);

                if (newValue) {
                  item[key] = newValue; // Thay thế giá trị bằng giá trị mới nếu tìm thấy
                }
              }
            }
            return item;
          });

          // Ghi đè nội dung JSON đã chỉnh sửa
          const updatedJsonContent = JSON.stringify(jsonContent, null, 2);
          compilation.assets[asset] = {
            source: () => updatedJsonContent,
            size: () => updatedJsonContent.length
          };
        }
      });

      callback();
    });
  }
}
class RemoveLocalLinksAndScriptsPlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap('RemoveUnwantedTagsPlugin', (compilation) => {
      const HtmlWebpackPlugin = require('html-webpack-plugin');
      HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tapAsync(
        'RemoveUnwantedTagsPlugin',
        (data, callback) => {
          let htmlContent = data.html;

          // Loại bỏ tất cả các thẻ <script> có src bắt đầu bằng './assets/js/'
          htmlContent = htmlContent.replace(/<script\b[^>]*src="\.\/assets\/js\/[^"]*"[^>]*><\/script>/gi, '');
          htmlContent = htmlContent.replace(/<script\b[^>]*src="\.\/assets\/jscc\/[^"]*"[^>]*><\/script>/gi, '');

          // Loại bỏ tất cả các thẻ <link> có href bắt đầu bằng './assets/css/'
          htmlContent = htmlContent.replace(/<link\b[^>]*href="\.\/assets\/css\/[^"]*"[^>]*>/gi, '');

          // Cập nhật lại nội dung HTML sau khi xóa
          data.html = htmlContent;
          callback(null, data);
        }
      );
    });
  }
}

class ReplaceClassPlugin {
  constructor(classMappingsCache) {
    this.classMappingsCache = classMappingsCache;
  }

  apply(compiler) {
    compiler.hooks.compilation.tap('ReplaceClassPlugin', (compilation) => {
      HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tapAsync(
        'ReplaceClassPlugin',
        (data, callback) => {
          // Sử dụng cache trực tiếp để thay thế các lớp CSS trong thuộc tính class của HTML
          let htmlContent = data.html;

          // Regex để tìm các thuộc tính class="..."
          const classAttributeRegex = /class=["']([^"']+)["']/g;

          // Thay thế các tên lớp trong thuộc tính class
          htmlContent = htmlContent.replace(classAttributeRegex, (match, classNames) => {
            // Tách các class ra thành một mảng
            const classes = classNames.split(/\s+/);

            // Thay thế các tên lớp dựa trên mappings
            const replacedClasses = classes.map(originalClass => {
              for (const mappings of Object.values(this.classMappingsCache)) {
                if (mappings[originalClass]) {
                  return mappings[originalClass];
                }
              }
              return originalClass; // Trả về lớp gốc nếu không tìm thấy trong mappings
            });

            // Ghép lại thành chuỗi và trả về thay thế cho thuộc tính class
            return `class="${replacedClasses.join(' ')}"`;
          });

          // Cập nhật lại nội dung HTML
          data.html = htmlContent;
          callback(null, data);
        }
      );
    });
  }
}

const classMappingsCache = {};

const jsEntry = {
  ...glob
    .sync('./src/assets/js/*.js')
    .reduce((acc, entry) => {
      const entryName = path.basename(entry, path.extname(entry));
      acc[entryName] = path.resolve(__dirname, entry);
      return acc;
    }, {}),
  ...glob
    .sync('./src/assets/si/*.si.js')
    .reduce((acc, entry) => {
      const entryName = path.basename(entry, path.extname(entry));
      acc[entryName] = path.resolve(__dirname, entry);
      return acc;
    }, {}),
  ...glob
    .sync('./src/assets/jscc/*.jscc.js')
    .reduce((acc, entry) => {
      const entryName = path.basename(entry, path.extname(entry));
      acc[entryName] = path.resolve(__dirname, entry);
      return acc;
    }, {}),
};

function generateHtmlPlugins() {
  const templateFiles = glob.sync('./src/**/*.html');
  return templateFiles.map((item) => {
    item = item.replace(/\\/g, '/');
    const parts = item.split('/');
    const name = parts[parts.length - 1].split('.')[0];

    let relatedChunks;
    // if (name == 'index') {
    //   relatedChunks = Object.keys(jsEntry).filter((chunkName) =>
    //     chunkName.includes(name)
    //   );
    // } else {
    //   relatedChunks = Object.keys(jsEntry).filter((chunkName) =>
    //     chunkName.includes(name) || chunkName.includes('common')
    //   );
    // }
    relatedChunks = Object.keys(jsEntry).filter((chunkName) =>
      chunkName.includes(name) || chunkName.includes('common')
    );
    relatedChunks.push('contract');

    return new HtmlWebpackPlugin({
      template: path.resolve(__dirname, item),
      filename: `${name}.html`,
      chunks: relatedChunks,
      chunksSortMode: (chunk1, chunk2) => {
        // Ưu tiên 'contract' đứng đầu tiên
        if (chunk1 === 'contract') return -1;
        if (chunk2 === 'contract') return 1;

        // Ưu tiên các chunk chứa 'common' sau 'contract'
        if (chunk1.includes('common') && !chunk2.includes('common')) return -1;
        if (!chunk1.includes('common') && chunk2.includes('common')) return 1;

        return 0; // Giữ nguyên thứ tự cho các chunk khác
      },
    });
  });
}
const htmlPlugins = generateHtmlPlugins();
const nonHashedClassesPath = path.resolve(__dirname, 'non-hashed-classes.json');
const nonHashedClasses = JSON.parse(fs.readFileSync(nonHashedClassesPath, 'utf-8')).classes;

function generateHash(content) {
  return crypto.createHash('md5').update(content).digest('hex');
}

module.exports = (env, argv) => {
  const isProduction = argv.mode === 'production';

  return {
    mode: isProduction ? 'production' : 'development',
    entry: jsEntry,
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: isProduction ? '[contenthash].bundle.js' : '[name].js',
      clean: true,
    },
    module: {
      rules: [
        {
          test: /\.json$/,
          type: 'asset/resource',
          include: path.resolve(__dirname, 'src/data'),
          generator: {
            filename: 'data/[name][ext]' // Giữ nguyên tên file và cấu trúc đường dẫn
          }
        },
        {
          test: /\.html$/,
          use: [{
            loader: 'html-loader',
            options: {
              // Tùy chọn này có thể bỏ qua xử lý tự động các thẻ script và link
              sources: {
                list: [
                  // Giữ lại xử lý tự động của hình ảnh và các nguồn tài nguyên khác
                  {
                    tag: 'img',
                    attribute: 'src',
                    type: 'src',
                  },
                  {
                    tag: 'loading-image',
                    attribute: 'src',
                    type: 'src',
                  },
                  // Chặn việc xử lý các thẻ script và link với các đường dẫn cụ thể
                  {
                    tag: 'script',
                    attribute: 'src',
                    type: 'src',
                    filter: (tag, attribute, attributes) => {
                      const srcAttr = attributes.find(attr => attr.name === 'src');
                      return srcAttr ? !/\.\/assets\/js\//.test(srcAttr.value) : true;
                    },
                  },
                  {
                    tag: 'script',
                    attribute: 'src',
                    type: 'src',
                    filter: (tag, attribute, attributes) => {
                      const srcAttr = attributes.find(attr => attr.name === 'src');
                      return srcAttr ? !/\.\/assets\/jscc\//.test(srcAttr.value) : true;
                    },
                  },
                  {
                    tag: 'link',
                    attribute: 'href',
                    type: 'src',
                    filter: (tag, attribute, attributes) => {
                      const hrefAttr = attributes.find(attr => attr.name === 'href');
                      return hrefAttr ? !/\.\/assets\/css\//.test(hrefAttr.value) : true;
                    },
                  },
                ],
              },
            },
          }],
        },
        {
          test: /\.css$/,
          use: [
            {
              loader: MiniCssExtractPlugin.loader,
            },
            {
              loader: 'css-loader',
              options: {
                modules: {
                  getLocalIdent: (context, localIdentName, localName, options) => {

                    const hash = generateHash(
                      context.resourcePath + localName
                    );
                    const className = `_${hash.substring(9, 14)}_${hash.substring(0, 8)}`;

                    // Lưu mappings vào cache thay vì ghi tệp liên tục
                    const cssFileName = path.basename(context.resourcePath);

                    if (!classMappingsCache[cssFileName]) {
                      classMappingsCache[cssFileName] = {};
                    }

                    classMappingsCache[cssFileName][localName] = className;
                    if (nonHashedClasses.includes(localName)) {
                      classMappingsCache[cssFileName][localName] = localName;
                      return localName;
                    }
                    return className;
                  },
                },
                sourceMap: true,
                importLoaders: 1,
              },
            },
            'postcss-loader',
          ],
        },
        {
          test: /\.js$/,
          exclude: /node_modules/,
          use: {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/preset-env'],
            },
          },
        },
        {
          test: /\.(png|jpe?g|gif|svg)$/,
          type: 'asset/resource',
          generator: {
            filename: 'assets/images/[hash][ext][query]',
          },
        },
      ],
    },
    plugins: [
      ...htmlPlugins,
      new MiniCssExtractPlugin({
        filename: isProduction ? '[contenthash].css' : '[name].css',
      }),
      new ReplaceJsonPathsPlugin(),
      new RemoveLocalLinksAndScriptsPlugin(),
      new ReplaceClassPlugin(classMappingsCache), // Truyền mappings vào plugin
      {
        apply: (compiler) => {
          compiler.hooks.emit.tapAsync('SaveClassMappingsPlugin', (compilation, callback) => {
            if (!isProduction) {
              const jsonFilePath = path.resolve(__dirname, 'bin/classMappings.json');
              fs.writeFileSync(
                jsonFilePath,
                JSON.stringify(classMappingsCache, null, 2)
              );
            }

            callback();
          });
        }
      }
    ],
    optimization: {
      minimize: isProduction,
      minimizer: [
        new TerserPlugin({
          parallel: true,
        }),
      ],
      removeEmptyChunks: true,
    },
    resolve: {
      extensions: ['.js', '.jsx', '.json'],
    },
    stats: {
      children: true,
      errorDetails: true,
    },
    devtool: isProduction ? false : 'source-map',
  };
};
TrdHuy commented 4 weeks ago
<head>
    <link rel="stylesheet" href="./assets/css/index.css">
    <link rel="stylesheet" href="./assets/css/portfolio_autoblocker.css">
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600&display=swap" rel="stylesheet">
    <script defer="defer" src="./assets/jscc/customcontrol.jscc.js"></script>

    <style>
        hor-image-container img {
            height: 350px;
            margin-right: 10px;
            border-radius: 8px;
            pointer-events: none;
        }

        hor-image-container img:last-child {
            margin-right: 0;
        }
    </style>
</head>

<body class="portfolio-container" style="color: azure;">
    <div class="overview overview-container">
        <h1 class="item-title">Samsung Honey Board Overview:</h1>
        <p>HoneyBoard is a versatile keyboard application designed specifically for Samsung devices,
            offering a smooth,
            convenient,
            and intuitive typing experience. With its sleek design and extensive customization options,
            HoneyBoard allows users to personalize their keyboard to fit their unique style and preferences. Whether
            you're typing messages, emails, or notes, HoneyBoard provides a seamless interface that enhances
            productivity and comfort,
            making it the perfect companion for all your Samsung devices.</p>
    </div>
    <div>
        <hor-image-container class="image-container">
            <img src="assets/images/project-1.jpg" alt="Image 1" />
            <img src="assets/images/project-2.png" alt="Image 2" />
            <img src="assets/images/project-3.jpg" alt="Image3" />
            <img src="assets/images/project-4.png" alt="Image 4" />
        </hor-image-container>

        <h1>HoneyBoard Features</h1>
        <h2>1. Multi-Device Compatibility</h2>
        <p>HoneyBoard is optimized for use across various Samsung devices,
            including smartphones,
            tablets,
            foldable devices (Galaxy Z Fold, Z Flip),
            and wearables like the Galaxy Watch. It ensures a consistent and high-quality typing experience across all
            device types.</p>
        <h2>2. Integration with Samsung Pass</h2>
        <p>Seamlessly integrates with Samsung Pass,
            allowing users to securely store and autofill passwords directly from the keyboard. Enhances security and
            convenience for logging into apps and websites.</p>
        <h2>3. Spotify Plugin</h2>
        <p>Includes a Spotify plugin that allows users to search for and share music directly from the keyboard without
            leaving the conversation.</p>
        <h2>4. Rich Media Support</h2>
        <p>Offers extensive support for rich media including emojis,
            GIFs,
            and stickers,
            enabling creative expression in messaging.</p>
        <h2>5. AI-Powered Features</h2>
        <p>Features AI-powered functionalities such as predictive text and voice recognition,
            which enhance typing accuracy and efficiency by suggesting words based on context and converting speech to
            text.</p>
        <h2>6. Multilingual Support</h2>
        <p>Provides robust multilingual support,
            allowing users to easily switch between languages and ensuring accurate autocorrection in each language.</p>
        <h2>7. Customization Options</h2>
        <p>Allows users to personalize their keyboard experience through customization options like adjusting keyboard
            size,
            changing themes,
            and rearranging key layouts.</p>
        <h2>8. Accessibility Features</h2>
        <p>Includes accessibility features such as high-contrast themes,
            larger key sizes,
            and voice input options,
            making the keyboard more accessible for users with visual or motor impairments.</p>
        <h2>9. Gesture Typing and Quick Access</h2>
        <p>Supports gesture typing,
            where users can swipe across the keyboard to type,
            and offers quick access shortcuts for functions like copy-paste and undo-redo.</p>
        <h2>10. Secure Folder Integration</h2>
        <p>Integrated with Samsung's Secure Folder, allowing users to manage and input sensitive information securely.
        </p>
        <h2>11. Enhanced Clipboard Management</h2>
        <p>Features advanced clipboard management,
            enabling users to store and quickly access multiple items from the clipboard,
            which is particularly useful for multitasking.</p>
    </div>
</body>
<footer>
    <script type="module" src="https://unpkg.com/ionicons@5.5.2/dist/ionicons/ionicons.esm.js"></script>
    <script nomodule src="https://unpkg.com/ionicons@5.5.2/dist/ionicons/ionicons.js"></script>
    <script src="./assets/js/contract.js"></script>
</footer>