スマホの入力を簡単にする?「Two Tap Input」

【更新履歴】

・2026/1/30 バージョン1.0公開。
・2026/1/30 バージョン1.1公開。
・2026/1/31 バージョン1.3公開。
・2026/1/31 バージョン1.4公開。(検索、AI、変換強化)
・2026/1/31 バージョン1.4(英語版)公開。


画像
ひらがな入力


画像
英語入力(小文字)
画像
数字・記号入力
画像
履歴から入力
画像
絵文字入力


画像
英語版



《このツールの概要》

・スマホ画面のテキスト入力方式といえば、
 フリック入力が一般的ですが、
 このツールでは、独自の方式で入力します。

・まあ、フリック以外の入力方式を試すために作った
 という感じですが、意外とはやるかもしれません。^^;

・入力したテキストは、
 コピ(コピー)ボタンを押すことで
 他のwebページなどで使用できる他、
 検索ボタンやAIボタンを押すことで
 Googleの検索結果を表示したり、
 ChatGptからの解答を表示することができます。

《 PCでスマホの画面を試す方法 》

・htmlファイルをクリックして、Chromeで開く。

・Chromeのメニューバーから、
 「設定」→「その他のツール」→「デベロッパーツール」
 をクリックして、スマホのマークをクリックすると、
 スマホの画面で操作することができます。

画像

・ダウンロードされる方はこちら。↓

・ソースコードはこちら。↓

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Two Tap Input 1.4.3</title>
<style>
    :root {
        --bg-color: #121212;
        --text-color: #fff;
        
        /* 行カラー */
        --c-a: #e60012; --c-k: #004098; --c-s: #40e0d0; --c-t: #ffd900;
        --c-n: #009944; --c-h: #ff69b4; --c-m: #800080; --c-y: #f39800;
        --c-r: #b08d55; --c-w: #6cbb5a;
        --c-sym: #555555;
        
        /* タブカラー */
        --tab-hira: #6f4b3e; --tab-kata: #1d2088; --tab-eng: #ff69b4;
        --tab-num: #40e0d0;
        --tab-history: #8b4513;
        --tab-emoji: #ffd900;

        /* ボタン色設定 */
        --col-ent: #ffd900;
        --col-ctl: #40e0d0;
        
        --col-ai: #ff1493;
        --col-sch: #00e676;
        --col-cpy: #00bfff;
        --col-pst: #ff8c00;
        --col-silver: #c0c0c0;

        --cursor-color: #00ffff;
        --bg-inactive-btn: #555555;
        --bg-candidate: #444444;
        --bg-chip: #666666;
    }

    @keyframes border-blink {
        0%   { border-color: var(--cursor-color); box-shadow: 0 0 5px var(--cursor-color); }
        50%  { border-color: transparent; box-shadow: none; }
        100% { border-color: var(--cursor-color); box-shadow: 0 0 5px var(--cursor-color); }
    }

    body {
        margin: 0; padding: 0;
        background-color: var(--bg-color);
        color: var(--text-color);
        font-family: "Hiragino Kaku Gothic ProN", sans-serif;
        height: 100vh; height: 100dvh; width: 100vw;
        display: flex; flex-direction: column;
        overflow: hidden; user-select: none; touch-action: none; 
    }

    /* --- 上部エリア --- */
    #top-area {
        padding-top: max(env(safe-area-inset-top), 10px);
        padding-bottom: 2px; padding-left: 5px; padding-right: 5px;
        flex-shrink: 0; z-index: 100; box-sizing: border-box;
        background: #000; border-bottom: 2px solid #333;
        display: flex; flex-direction: column; gap: 3px;
        transition: all 0.3s ease;
    }

    /* エディタ全画面モード */
    #top-area.fullscreen-editor {
        position: fixed; top: 0; left: 0; right: 0; bottom: 0;
        height: 100dvh; width: 100vw;
        padding-bottom: max(env(safe-area-inset-bottom), 20px);
        background: #000; z-index: 5000;
    }
    #top-area.fullscreen-editor #display-wrapper { height: 100%; flex-direction: column; }
    #top-area.fullscreen-editor #line-indicator { width: 100%; flex-direction: row; height: 30px; gap: 10px; padding: 0 10px; }
    #top-area.fullscreen-editor #input-line { flex: 1; height: auto; font-size: 20px; }
    #top-area.fullscreen-editor .side-controls, 
    #top-area.fullscreen-editor #sub-bar { display: none; }

    /* 候補全画面モード */
    #top-area.fullscreen-cand {
        position: fixed; top: 0; left: 0; right: 0; bottom: 0;
        height: 100dvh; width: 100vw;
        padding-bottom: max(env(safe-area-inset-bottom), 20px);
        background: #000; z-index: 5000;
        display: flex; flex-direction: column;
    }
    #top-area.fullscreen-cand #display-wrapper { display: none; }
    #top-area.fullscreen-cand #sub-bar { height: 100%; flex-direction: column; }
    #top-area.fullscreen-cand #candidate-bar { flex: 1; align-content: flex-start; }
    #top-area.fullscreen-cand .extended-tools { 
        height: 60px; flex-shrink: 0; flex-direction: row; width: 100%; 
        justify-content: flex-end; padding: 5px; box-sizing: border-box;
    }
    #top-area.fullscreen-cand .tool-col { width: 60px; height: 100%; }

    #display-wrapper {
        display: flex; align-items: stretch;
        height: 90px; gap: 4px;
        transition: height 0.3s;
    }
    
    /* 全画面解除用のフローティングボタン (エディタ用) */
    #exit-full-editor-btn {
        display: none;
        position: absolute; bottom: 20px; right: 20px;
        width: 50px; height: 50px; border-radius: 25px;
        background: rgba(255, 217, 0, 0.8); color: #000;
        font-size: 24px; border: 2px solid #fff;
        align-items: center; justify-content: center;
        z-index: 5001;
    }
    #top-area.fullscreen-editor #exit-full-editor-btn { display: flex; }

    /* 行番号 */
    #line-indicator {
        width: 40px; 
        display: flex; flex-direction: column; 
        align-items: center; justify-content: flex-start;
        padding-top: 6px;
        background: #111; border-radius: 4px; flex-shrink: 0;
        line-height: 1.0;
    }
    .ln-num { font-size: 20px; font-weight: bold; color: #fff; }
    .ln-label { font-size: 10px; color: #fff; margin-top: 2px; }

    /* 入力欄 */
    #input-line {
        font-size: 18px; line-height: 1.5;
        white-space: pre-wrap; overflow-y: auto;
        flex: 1;
        border: 2px solid #555; background: #ffffff; color: #000000;
        padding: 6px; border-radius: 6px;
        word-break: break-all;
        outline: none; caret-color: #000;
    }
    [contenteditable] { -webkit-user-select: text; user-select: text; }
    .composing-span { background: #d0ebff; border-bottom: 2px solid #00b4d8; }

    /* サイドコントロール */
    .side-controls {
        display: grid;
        grid-template-columns: 1fr 1fr;
        grid-template-rows: 1fr 1fr;
        gap: 2px; width: 70px; flex-shrink: 0;
    }
    .ctrl-btn {
        border: 2px solid transparent; border-radius: 4px;
        color: #000; font-size: 16px; font-weight: bold;
        cursor: pointer; display: flex; align-items: center; justify-content: center;
        background: var(--col-ctl); box-sizing: border-box;
    }
    .btn-ent { background: var(--col-ent); font-size: 20px; color: #000; } 
    .btn-full { background: var(--col-ctl); color: #000; font-size: 16px; border: 1px solid #333; }

    /* ホバーエフェクト */
    .ctrl-btn:hover, .ctrl-btn:active,
    .sub-btn:hover, .sub-btn:active,
    .key:hover, .key:active,
    .cand-chip:hover, .cand-chip:active {
        border: 2px solid var(--cursor-color);
        animation: border-blink 0.8s infinite;
        z-index: 10; position: relative;
    }

    #sub-bar {
        height: 100px; 
        display: flex; gap: 4px; flex-shrink: 0; align-items: stretch;
    }
    
    #candidate-bar {
        flex: 1; display: flex; flex-wrap: wrap; align-content: flex-start; overflow-y: auto;
        background: var(--bg-candidate); padding: 4px; border-radius: 4px; gap: 4px;
    }
    .cand-chip {
        background: #ffffff; color: #000000;
        padding: 6px 14px;
        border-radius: 6px; 
        font-size: 18px; font-weight: bold;
        border: 2px solid #ccc;
        height: auto; min-height: 36px;
        box-sizing: border-box; display: flex; align-items: center;
        cursor: pointer;
    }
    
    .extended-tools {
        display: flex; gap: 3px; flex-shrink: 0;
    }
    
    .tool-col {
        display: flex; flex-direction: column; gap: 2px;
        width: 40px; flex-shrink: 0;
    }
    .tool-col.col-full { width: 40px; }

    .sub-btn {
        border: 2px solid transparent; border-radius: 4px;
        font-size: 11px; font-weight: bold;
        display: flex; align-items: center; justify-content: center;
        cursor: pointer; box-sizing: border-box; width: 100%;
    }
    .tool-col .sub-btn { flex: 1; }

    .btn-cpy { background: var(--col-cpy); color: #fff; }
    .btn-pst { background: var(--col-pst); color: #fff; }
    .btn-cand-full { background: var(--col-ctl); color: #000; font-size: 16px; border: 1px solid #333; }
    .btn-sch { background: var(--col-sch); color: #fff; font-size: 10px; }
    .btn-ai  { background: var(--col-ai);  color: #fff; font-size: 12px; }


    /* --- メインエリア --- */
    #main-stage {
        flex: 1; position: relative; overflow: hidden;
        background: #111; padding: 2px;
    }

    #twothumb-board {
        display: grid;
        grid-template-columns: 1fr 2.8fr; 
        gap: 6px; 
        height: 100%; width: 100%; box-sizing: border-box;
    }
    
    #left-panel { display: grid; grid-template-columns: 1fr; grid-auto-rows: 1fr; gap: 3px; }
    #left-panel.jp-layout { grid-template-columns: 1fr 1fr; grid-template-rows: repeat(6, 1fr); }

    #right-panel { display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(4, 1fr); gap: 3px; }
    #right-panel.jp-layout { grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(5, 1fr); }
    #right-panel.sym-layout { grid-template-columns: repeat(2, 1fr); grid-template-rows: repeat(4, 1fr); }
    #right-panel.list-layout { grid-template-columns: 1fr; grid-template-rows: repeat(4, 1fr); }
    
    /* 英語高密度レイアウト: 5列 x 6行 = 30キー */
    #right-panel.eng-layout { 
        grid-template-columns: repeat(5, 1fr); 
        grid-template-rows: repeat(6, 1fr); 
    }

    .key {
        display: flex; align-items: center; justify-content: center;
        font-weight: bold; border-radius: 5px; 
        cursor: pointer; color: #fff;
        box-shadow: 0 2px 0 rgba(0,0,0,0.3);
        border: 2px solid transparent; box-sizing: border-box;
        text-align: center; line-height: 1.1; overflow: hidden;
    }
    .key-left.active {
        border-color: var(--cursor-color);
        animation: border-blink 1s infinite;
        filter: brightness(1.2);
        z-index: 5;
    }
    .font-emoji { font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", sans-serif; }

    .row-a { background: var(--c-a); } .row-k { background: var(--c-k); }
    .row-s { background: var(--c-s); color: #000; text-shadow: none; }
    .row-t { background: var(--c-t); color: #000; text-shadow: none; }
    .row-n { background: var(--c-n); } .row-h { background: var(--c-h); }
    .row-m { background: var(--c-m); } .row-y { background: var(--c-y); color: #000; text-shadow: none; }
    .row-r { background: var(--c-r); } .row-w { background: var(--c-w); }
    .row-sym { background: var(--c-sym); }
    .key-del { background: #d62828; font-size: 14px; }

    .key-cat { background: var(--bg-inactive-btn); border: 1px solid #777; font-size: 14px; color: #fff; }
    .key-cat.active { background: #eee; color: #000; border: 2px solid #fff; }
    
    .key-pager { background: var(--col-silver); font-size: 20px; color: #000; border: 1px solid #999; }

    .key-char { background: #333; font-size: 28px; }
    .key-jp-char { font-size: 24px; }
    
    /* 英字高密度モード用のスタイル調整 */
    .eng-layout .key-char { font-size: 20px; }
    
    .key-hist { background: #333; font-size: 14px; color: #fff; text-align: left; padding: 4px; justify-content: flex-start; white-space: nowrap; text-overflow: ellipsis; display: block; overflow: hidden; }

    /* --- 下部タブ --- */
    #tab-bar {
        height: auto;
        min-height: 50px;
        background: #111; border-top: 1px solid #333;
        display: flex; flex-shrink: 0; z-index: 10;
        /* 余白をさらに拡大 (+15px) */
        padding-bottom: max(env(safe-area-inset-bottom) + 70px, 70px);
    }
    .tab {
        flex: 1; display: flex; align-items: center; justify-content: flex-start;
        font-size: 12px; font-weight: bold; color: #888;
        border-right: 1px solid #222; flex-direction: column;
        cursor: pointer; background: #222; transition: all 0.2s;
        line-height: 1.1;
        padding-top: 8px;
    }
    .tab-lbl-main { font-size: 14px; margin-bottom: 2px;}
    .tab-lbl-sub { font-size: 9px; opacity: 0.8; }
    .t-hira.active { background: var(--tab-hira); color: #fff; }
    .t-kata.active { background: var(--tab-kata); color: #fff; }
    .t-eng.active { background: var(--tab-eng); color: #fff; }
    .t-num.active { background: var(--tab-num); color: #000; }
    .t-hist.active { background: var(--tab-history); color: #fff; }
    .t-emoji.active { background: var(--tab-emoji); color: #000; }
</style>
</head>
<body>

<div id="top-area">
    <div id="display-wrapper">
        <div id="line-indicator">
            <div class="ln-num" id="ln-num-text">1</div>
            <div class="ln-label">行</div>
        </div>
        <div id="input-line" contenteditable="true" inputmode="none" spellcheck="false" 
             oninput="handleInput()" onclick="updateLineNum()" onkeyup="updateLineNum()"></div>
        
        <div class="side-controls">
            <button class="ctrl-btn btn-ent no-focus-steal" onmousedown="event.preventDefault()" onclick="insertDirect('\n')">↵</button>
            <button class="ctrl-btn no-focus-steal" onmousedown="event.preventDefault()" onclick="moveLine(-1)">▲</button>
            <button class="ctrl-btn btn-full no-focus-steal" onmousedown="event.preventDefault()" onclick="toggleEditorFull()">□</button>
            <button class="ctrl-btn no-focus-steal" onmousedown="event.preventDefault()" onclick="moveLine(1)">▼</button>
        </div>
    </div>
    
    <div id="sub-bar">
        <div id="candidate-bar"></div>
        <div class="extended-tools">
            <div class="tool-col col-full">
                <button class="sub-btn btn-cand-full no-focus-steal" onmousedown="event.preventDefault()" onclick="toggleCandFull()">□</button>
            </div>
            <div class="tool-col">
                <button class="sub-btn btn-cpy no-focus-steal" onmousedown="event.preventDefault()" onclick="copyAll()">コピ</button>
                <button class="sub-btn btn-pst no-focus-steal" onmousedown="event.preventDefault()" onclick="pasteFromClipboard()">貼付</button>
            </div>
            <div class="tool-col">
                <button class="sub-btn btn-sch no-focus-steal" onmousedown="event.preventDefault()" onclick="searchWeb()">検索</button>
                <button class="sub-btn btn-ai no-focus-steal" onmousedown="event.preventDefault()" onclick="askAI()">AI</button>
            </div>
        </div>
    </div>
    
    <div id="exit-full-editor-btn" onclick="toggleEditorFull()">×</div>
</div>

<div id="main-stage">
    <div id="twothumb-board">
        <div id="left-panel"></div>
        <div id="right-panel"></div>
    </div>
</div>

<div id="tab-bar">
    <div class="tab t-hira active" onclick="switchMode('hira', this)"><span class="tab-lbl-main">あ</span><span class="tab-lbl-sub">平仮名</span></div>
    <div class="tab t-kata" onclick="switchMode('kata', this)"><span class="tab-lbl-main">ア</span><span class="tab-lbl-sub">カタカナ</span></div>
    <div class="tab t-eng" onclick="switchMode('english', this)"><span class="tab-lbl-main">Abc</span><span class="tab-lbl-sub">英字</span></div>
    <div class="tab t-num" onclick="switchMode('num', this)"><span class="tab-lbl-main">123</span><span class="tab-lbl-sub">数記</span></div>
    <div class="tab t-hist" onclick="switchMode('history', this)"><span class="tab-lbl-main">🕒</span><span class="tab-lbl-sub">履歴</span></div>
    <div class="tab t-emoji" onclick="switchMode('emoji', this)"><span class="tab-lbl-main">😀</span><span class="tab-lbl-sub">絵文字</span></div>
</div>

<script>
    const rainbowColors = ['#e60012','#f39800','#ffd900','#99cc00','#009944','#00a0e9','#004098','#4b0082','#800080','#e60033'];
    const jpRows = [{id:'a',l:'あ',c:'row-a'},{id:'k',l:'か',c:'row-k'},{id:'s',l:'さ',c:'row-s'},{id:'t',l:'た',c:'row-t'},{id:'n',l:'な',c:'row-n'},{id:'h',l:'は',c:'row-h'},{id:'m',l:'ま',c:'row-m'},{id:'y',l:'や',c:'row-y'},{id:'r',l:'ら',c:'row-r'},{id:'w',l:'わ',c:'row-w'}];
    const jpMap = {
        'hira': {'a':['あ','ぁ','', 'い','ぃ','', 'う','ぅ','', 'え','ぇ','', 'お','ぉ',''], 'k':['か','が','', 'き','ぎ','', 'く','ぐ','', 'け','げ','', 'こ','ご',''], 's':['さ','ざ','', 'し','じ','', 'す','ず','', 'せ','ぜ','', 'そ','ぞ',''], 't':['た','だ','', 'ち','ぢ','', 'つ','づ','っ', 'て','で','', 'と','ど',''], 'n':['な','','', 'に','','', 'ぬ','','', 'ね','','', 'の','',''], 'h':['は','ば','ぱ', 'ひ','び','ぴ', 'ふ','ぶ','ぷ', 'へ','べ','ぺ', 'ほ','ぼ','ぽ'], 'm':['ま','','', 'み','','', 'む','','', 'め','','', 'も','',''], 'y':['や','ゃ','', '','','', 'ゆ','ゅ','', '','','', 'よ','ょ',''], 'r':['ら','','', 'り','','', 'る','','', 'れ','','', 'ろ','',''], 'w':['わ','ゎ','', 'を','','', 'ん','','', '','','', '','',''], 'sym':[' ', '\n', '、', '。', '「', '」', '(', ')']},
        'kata': {'a':['ア','ァ','', 'イ','ィ','', 'ウ','ゥ','', 'エ','ェ','', 'オ','ォ',''], 'k':['カ','ガ','', 'キ','ギ','', 'ク','グ','', 'ケ','ゲ','', 'コ','ゴ',''], 's':['サ','ザ','', 'シ','ジ','', 'ス','ズ','', 'セ','ゼ','', 'ソ','ゾ',''], 't':['タ','ダ','', 'チ','ヂ','', 'ツ','ヅ','ッ', 'テ','デ','', 'ト','ド',''], 'n':['ナ','','', 'ニ','','', 'ヌ','','', 'ネ','','', 'ノ','',''], 'h':['ハ','バ','パ', 'ヒ','ビ','ピ', 'フ','ブ','プ', 'ヘ','ベ','ペ', 'ホ','ボ','ポ'], 'm':['マ','','', 'ミ','','', 'ム','','', 'メ','','', 'モ','',''], 'y':['ヤ','ャ','', '','','', 'ユ','ュ','', '','','', 'ヨ','ョ',''], 'r':['ラ','','', 'リ','','', 'ル','','', 'レ','','', 'ロ','',''], 'w':['ワ','ヮ','', 'ヲ','','', 'ン','','', '','','', '','',''], 'sym':[' ', '\n', '、', '。', '「', '」', '(', ')']}
    };
    const numCategories = [{id:'digits',l:'数字',src:"1234567890".split("")},{id:'math',l:'式',src:"+-*/=<>".split("")},{id:'point',l:'点',src:".,:;".split("")},{id:'bra',l:'カッコ',src:"()[]{}''\"\"".split("")},{id:'other',l:'その他',src:"%$#&@^|~`".split("")}];
    
    // 英字レイアウト設定
    const englishPages = [
        // Page 0: Lowercase (高密度5x6レイアウト用: 26文字+記号+SP+DEL)
        "abcdefghijklmnopqrstuvwxyz,.".split(""),
        // Page 1: Uppercase
        "ABCDEFGHIJKLMNOPQRSTUVWXYZ,.".split(""),
        // Page 2: Symbols (標準3x4レイアウト)
        ".,!?@_#+-*/=".split("")
    ];
    // 英単語辞書 (簡易版)
    const englishWords = [
        "the", "be", "to", "of", "and", "a", "in", "that", "have", "I", "it", "for", "not", "on", "with", "he", "as", "you", "do", "at", "this", "but", "his", "by", "from", "they", "we", "say", "her", "she", "or", "an", "will", "my", "one", "all", "would", "there", "their", "what", "so", "up", "out", "if", "about", "who", "get", "which", "go", "me", "when", "make", "can", "like", "time", "no", "just", "him", "know", "take", "people", "into", "year", "your", "good", "some", "could", "them", "see", "other", "than", "then", "now", "look", "only", "come", "its", "over", "think", "also", "back", "after", "use", "two", "how", "our", "work", "first", "well", "way", "even", "new", "want", "because", "any", "these", "give", "day", "most", "us", "is", "are", "was", "were", "hello", "thank", "please", "yes", "sorry"
    ];

    const emojiData = [..."😀😁😂🤣😃😄😅😆😉😊😋😎😍😘🥰😗😙😚🙂🤗🤩🤔🤨😐😑😶🙄😏😣😥😮🤐😯😪😫😴😌😛😜😝🤤😒😓😔😕🙃🤑😲☹️🙁😖😞😟😤😢😭😦😧😨😩🤯😬😰😱🥵🥶😳🤪😵😡😠🤬😷🤒"];
    const basicDictionary = [{k:'あ',v:['有る']},{k:'あい',v:['愛','AI']},{k:'か',v:['可','蚊']},{k:'かい',v:['貝','回','会']},{k:'きょう',v:['今日']},{k:'わたし',v:['私']},{k:'あした',v:['明日']}];
    let userDictionary = {};
    let historyData = [];

    let currentMode = 'hira';
    let currentSelection = 'a';
    let isNumFullWidth = false; 
    let composingText = "";
    const inputEl = document.getElementById('input-line');
    const candBar = document.getElementById('candidate-bar');

    switchMode('hira', document.querySelector('.t-hira'));
    inputEl.focus();

    document.addEventListener('selectionchange', () => {
        if(document.activeElement === inputEl) updateLineNum();
    });

    function switchMode(mode, tabEl) {
        currentMode = mode;
        document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
        if(tabEl) tabEl.classList.add('active');
        const lp = document.getElementById('left-panel');
        const rp = document.getElementById('right-panel');
        lp.className = ""; rp.className = "";
        
        // 英語以外は確定
        if (mode !== 'hira' && mode !== 'kata' && mode !== 'english') confirmComposition();

        if (mode === 'hira' || mode === 'kata') {
            lp.classList.add("jp-layout"); rp.classList.add("jp-layout"); currentSelection = 'a'; renderLeftJP(); renderRightJP();
        } else if (mode === 'num') {
            currentSelection = 0; isNumFullWidth = false; renderLeftCategories(); renderRightCategory();
        } else if (mode === 'emoji') {
            currentSelection = 0; renderLeftEmojiNav(); renderRightEmoji();
        } else if (mode === 'history') {
            currentSelection = 0; renderLeftHistoryNav(); renderRightHistory();
        } else if (mode === 'english') {
            currentSelection = 0; renderLeftPagesEnglish(); renderRightPageEnglish();
        }
    }

    function createKey(cls, text, onClick) {
        const div = document.createElement('div');
        div.className = cls + " no-focus-steal"; div.innerText = text;
        div.onmousedown = (e) => e.preventDefault(); div.onclick = onClick; return div;
    }

    function renderLeftJP() {
        const el = document.getElementById('left-panel'); el.innerHTML = "";
        jpRows.forEach(row => {
            const btn = createKey(`key key-left ${row.c}`, row.l, () => { currentSelection = row.id; renderLeftJP(); renderRightJP(); });
            if(row.id === currentSelection) btn.classList.add('active'); el.appendChild(btn);
        });
        const symBtn = createKey("key key-left row-sym", "記", () => { currentSelection = 'sym'; renderLeftJP(); renderRightJP(); });
        if(currentSelection === 'sym') symBtn.classList.add('active'); el.appendChild(symBtn);
        el.appendChild(createKey("key key-left key-del", "削除", deleteChar));
    }

    function renderRightJP() {
        const el = document.getElementById('right-panel'); el.innerHTML = "";
        if(currentSelection === 'sym') el.className = 'sym-layout'; else el.className = 'jp-layout';
        const map = (currentMode === 'kata') ? jpMap['kata'] : jpMap['hira'];
        const chars = map[currentSelection] || [];
        const activeL = document.querySelector('.key-left.active');
        const bg = activeL ? window.getComputedStyle(activeL).backgroundColor : '#333';
        const fg = activeL ? window.getComputedStyle(activeL).color : '#fff';
        chars.forEach(char => {
            let label = char; if(char === ' ') label = '空白'; if(char === '\n') label = '改行';
            const btn = createKey("key key-right key-jp-char", label, () => {
                if (currentSelection === 'sym') insertDirect(char); else insertComposing(char);
            });
            if(char || char === ' ' || char === '\n') {
                btn.style.backgroundColor = bg; btn.style.color = fg; btn.style.filter = "brightness(0.9)";
            } else {
                btn.style.background = "transparent"; btn.style.boxShadow = "none"; btn.style.border = "none"; btn.onclick = null; btn.classList.remove('key');
            }
            el.appendChild(btn);
        });
    }

    // --- 英字モード用 ---
    function renderLeftPagesEnglish() {
        const el = document.getElementById('left-panel'); el.innerHTML = "";
        const labels = ["abc", "ABC", "記号"];
        labels.forEach((lbl, idx) => {
            const btn = createKey("key key-cat", lbl, () => { currentSelection = idx; renderLeftPagesEnglish(); renderRightPageEnglish(); });
            if(idx === currentSelection) btn.classList.add('active'); 
            el.appendChild(btn);
        });
    }

    function renderRightPageEnglish() {
        const el = document.getElementById('right-panel'); el.innerHTML = "";
        const items = englishPages[currentSelection] || [];
        
        // 0(小文字)と1(大文字)は高密度レイアウト、2(記号)は通常
        const isDense = (currentSelection === 0 || currentSelection === 1);
        
        if (isDense) {
            el.className = "eng-layout"; // 5x6 grid
        } else {
            el.className = ""; // 3x4 grid
        }

        items.forEach(char => {
            const btn = createKey("key key-char", char, () => {
                // 記号以外は単語予測のためcomposingに入れる
                if (isDense) insertComposing(char); 
                else insertDirect(char);
            });
            
            // 色付けロジック
            if (!isDense) {
                // 記号ページの簡易的な色付け
                btn.style.backgroundColor = "var(--col-silver)"; btn.style.color = "#000";
            } else {
                // アルファベットの色分け
                const code = char.toLowerCase().charCodeAt(0) - 97;
                if (code >= 0 && code < 26) {
                    const cIdx = Math.floor(code / 3) % rainbowColors.length;
                    btn.style.backgroundColor = rainbowColors[cIdx];
                    if(['#ffd900', '#99cc00', '#f39800', '#40e0d0'].includes(rainbowColors[cIdx])) btn.style.color = "#000";
                } else {
                    btn.style.backgroundColor = "#555";
                }
            }
            el.appendChild(btn);
        });

        // 残りのスペースに機能キーを追加
        if (isDense) {
            // 28文字入ったので残り2枠 (30 - 28)
            const spc = createKey("key key-char", "Space", () => insertDirect(" "));
            spc.style.backgroundColor = "#777"; spc.style.fontSize="12px";
            el.appendChild(spc);
            
            const del = createKey("key key-char", "Del", deleteChar);
            del.style.backgroundColor = "#d62828"; del.style.fontSize="12px";
            el.appendChild(del);
        } else {
            // 記号ページ用
            const spc = createKey("key key-char", "空白", () => insertDirect(" "));
            spc.style.backgroundColor = "#777"; spc.style.fontSize="16px";
            el.appendChild(spc);
            const del = createKey("key key-char", "削除", deleteChar);
            del.style.backgroundColor = "#d62828"; del.style.fontSize="16px";
            el.appendChild(del);
        }
    }

    function renderLeftCategories() {
        const el = document.getElementById('left-panel'); el.innerHTML = "";
        numCategories.forEach((cat, idx) => {
            const btn = createKey("key key-cat", cat.l, () => { currentSelection = idx; renderLeftCategories(); renderRightCategory(); });
            if(idx === currentSelection) btn.classList.add('active'); el.appendChild(btn);
        });
    }
    function renderRightCategory() {
        const el = document.getElementById('right-panel'); el.innerHTML = "";
        const cat = numCategories[currentSelection];
        cat.src.forEach(char => {
            let val = char; let display = char;
            if (isNumFullWidth) { val = toFullWidth(char); display = val; }
            const btn = createKey("key key-char", display, () => insertDirect(val));
            btn.style.backgroundColor = "#d1f5fd"; btn.style.color = "#000"; btn.style.textShadow = "none";
            el.appendChild(btn);
        });
        
        if (cat.id === 'other') {
             const fullHalf = createKey("key key-char", isNumFullWidth?"全":"半", () => {
                 isNumFullWidth = !isNumFullWidth; renderRightCategory();
             });
             fullHalf.style.backgroundColor = "#40e0d0"; fullHalf.style.color="#000"; fullHalf.style.fontSize="16px";
             el.appendChild(fullHalf);

             const spc = createKey("key key-char", "空白", () => insertDirect(isNumFullWidth?'\u3000':' '));
             spc.style.backgroundColor = "#777"; spc.style.color="#fff"; spc.style.fontSize="16px";
             el.appendChild(spc);
             const del = createKey("key key-char", "削除", deleteChar);
             del.style.backgroundColor = "#d62828"; del.style.color="#fff"; del.style.fontSize="16px";
             el.appendChild(del);
        }
    }

    function renderLeftEmojiNav() {
        const el = document.getElementById('left-panel'); el.innerHTML = "";
        el.appendChild(createKey("key key-pager", "▲", () => { if(currentSelection > 0) { currentSelection--; renderRightEmoji(); } }));
        el.appendChild(createKey("key key-pager", "▼", () => { const maxPage = Math.ceil(emojiData.length / 12) - 1; if(currentSelection < maxPage) { currentSelection++; renderRightEmoji(); } }));
    }
    function renderRightEmoji() {
        const el = document.getElementById('right-panel'); el.innerHTML = "";
        const items = emojiData.slice(currentSelection * 12, currentSelection * 12 + 12);
        items.forEach(char => {
            const btn = createKey("key key-char font-emoji", char, () => insertDirect(char));
            btn.style.fontSize = "24px"; btn.style.backgroundColor = "#ffffff"; btn.style.color = "#000"; btn.style.textShadow = "none";
            el.appendChild(btn);
        });
    }

    // --- 履歴モード関連 ---
    function addToHistory(text) {
        if(!text) return;
        text = text.trim();
        if(!text) return;
        historyData = historyData.filter(t => t !== text);
        historyData.unshift(text);
        if(historyData.length > 100) historyData.pop(); 
        if(currentMode === 'history') { renderLeftHistoryNav(); renderRightHistory(); }
    }
    
    function renderLeftHistoryNav() {
        const el = document.getElementById('left-panel'); el.innerHTML = "";
        el.appendChild(createKey("key key-pager", "▲", () => { if(currentSelection > 0) { currentSelection--; renderRightHistory(); } }));
        el.appendChild(createKey("key key-pager", "▼", () => { const maxPage = Math.ceil(historyData.length / 4) - 1; if(currentSelection < maxPage) { currentSelection++; renderRightHistory(); } }));
    }
    
    function renderRightHistory() {
        const el = document.getElementById('right-panel'); el.innerHTML = "";
        el.className = "list-layout"; 
        
        const start = currentSelection * 4;
        const items = historyData.slice(start, start + 4);
        
        items.forEach(text => {
            const btn = createKey("key key-hist", text, () => insertDirect(text));
            btn.style.fontSize = "16px"; 
            btn.style.whiteSpace = "normal"; 
            btn.style.wordBreak = "break-all";
            btn.style.lineHeight = "1.2";
            btn.style.justifyContent = "flex-start";
            btn.style.padding = "8px";
            btn.style.textAlign = "left";
            el.appendChild(btn);
        });
    }

    // Logic
    function insertComposing(char) { composingText += char; updateComposingDisplay(); updateCandidates(); }
    function updateComposingDisplay() {
        let span = inputEl.querySelector('.composing-span');
        if (!span) { span = document.createElement('span'); span.className = 'composing-span'; insertNodeAtCursor(span); }
        span.innerText = composingText; placeCaretAfter(span); updateLineNum();
    }
    function confirmComposition(textToCommit) {
        let commitVal = textToCommit;
        if (commitVal === undefined) commitVal = composingText;
        
        if (commitVal) {
            // 履歴への追加
            if(userDictionary[commitVal]) userDictionary[commitVal]++; else userDictionary[commitVal] = 1;
            addToHistory(commitVal);
            
            // 英語モードで単語選択時は自動でスペースを入れると便利だが、今回はシンプルな挿入にする
            if(currentMode === 'english' && textToCommit) commitVal += " ";
        }
        
        // 全画面候補表示なら閉じる
        document.getElementById('top-area').classList.remove('fullscreen-cand');
        
        if (!composingText && !textToCommit) return;
        
        let span = inputEl.querySelector('.composing-span');
        if (span) { 
            const textNode = document.createTextNode(commitVal); 
            span.parentNode.replaceChild(textNode, span); 
            placeCaretAfter(textNode); 
        } else { 
            insertDirect(commitVal); 
        }
        
        composingText = ""; candBar.innerHTML = ''; updateLineNum();
    }
    function insertDirect(text) { 
        if (composingText) confirmComposition(); 
        document.execCommand('insertText', false, text); 
        if(text.trim().length > 0) addToHistory(text);
        updateLineNum(); 
    }
    function deleteChar() {
        if (composingText) { composingText = composingText.slice(0, -1); if (!composingText) { let span = inputEl.querySelector('.composing-span'); if(span) span.remove(); candBar.innerHTML = ''; } else { updateComposingDisplay(); updateCandidates(); } } else { document.execCommand('delete'); } updateLineNum();
    }
    function insertNodeAtCursor(node) {
        inputEl.focus(); const sel = window.getSelection();
        if (sel.rangeCount > 0) { const range = sel.getRangeAt(0); range.deleteContents(); range.insertNode(node); } else { inputEl.appendChild(node); }
    }
    function placeCaretAfter(node) { const range = document.createRange(); range.setStartAfter(node); range.collapse(true); const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); }
    function toFullWidth(str) { return str.replace(/[!-~]/g, s => String.fromCharCode(s.charCodeAt(0) + 0xFEE0)).replace(/ /g, "\u3000"); }
    
    function updateLineNum() { 
        const sel = window.getSelection();
        if (sel.rangeCount === 0) return;
        let node = sel.anchorNode;
        let isInside = false;
        while(node) { if(node === inputEl) { isInside = true; break; } node = node.parentNode; }
        if(!isInside) return;
        const range = sel.getRangeAt(0);
        const preCaretRange = range.cloneRange();
        preCaretRange.selectNodeContents(inputEl);
        preCaretRange.setEnd(range.endContainer, range.endOffset);
        const text = preCaretRange.toString();
        const currentLine = text.split('\n').length;
        document.getElementById('ln-num-text').innerText = currentLine; 
    }
    
    function handleInput() { updateLineNum(); }
    function copyAll() { navigator.clipboard.writeText(inputEl.innerText).then(()=>alert("コピーしました")); }
    async function pasteFromClipboard() { try { const text = await navigator.clipboard.readText(); insertDirect(text); } catch(e) {} }
    
    // エディタ全画面
    function toggleEditorFull() { 
        document.getElementById('top-area').classList.toggle('fullscreen-editor'); 
        document.getElementById('top-area').classList.remove('fullscreen-cand');
    }
    
    // 候補全画面
    function toggleCandFull() {
        document.getElementById('top-area').classList.toggle('fullscreen-cand');
        document.getElementById('top-area').classList.remove('fullscreen-editor');
    }

    function moveLine(d) { inputEl.scrollTop += d * 30; }

    function updateCandidates() {
        candBar.innerHTML = ""; if (!composingText) return;
        let candidates = [];
        
        if (currentMode === 'english') {
            // 英単語検索
            const lowerComp = composingText.toLowerCase();
            const matches = englishWords.filter(w => w.toLowerCase().startsWith(lowerComp));
            candidates = candidates.concat(matches);
            // 入力そのものも候補に
            if(!candidates.includes(composingText)) candidates.unshift(composingText);
        } else {
            // 日本語検索
            basicDictionary.filter(e => e.k.startsWith(composingText)).forEach(h => candidates = candidates.concat(h.v));
            candidates.push(composingText);
            if (currentMode === 'hira') candidates.push(composingText.replace(/[\u3041-\u3096]/g, c => String.fromCharCode(c.charCodeAt(0) + 0x60)));
        }

        candidates = [...new Set(candidates)];
        candidates.sort((a, b) => (userDictionary[b]||0) - (userDictionary[a]||0));
        
        candidates.forEach(c => {
            const chip = document.createElement('div'); chip.className = 'cand-chip no-focus-steal'; chip.innerText = c;
            chip.onmousedown = (e) => e.preventDefault(); chip.onclick = () => confirmComposition(c); candBar.appendChild(chip);
        });
    }

    function searchWeb() { const t = inputEl.innerText; if(t) window.open(`https://www.google.com/search?q=${encodeURIComponent(t)}`, '_blank'); }
    function askAI() { const t = inputEl.innerText; if(t) window.open(`https://chatgpt.com/?q=${encodeURIComponent(t)}`, '_blank'); }
</script>
</body>
</html>

・英語版はこちら。↓

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Two Tap Input 1.4.2 (EN)</title>
<style>
    :root {
        --bg-color: #121212;
        --text-color: #fff;
        
        /* Row Colors (Rainbow based) */
        --c-red: #e60012; --c-org: #f39800; --c-ylw: #ffd900; --c-grn: #009944;
        --c-cyn: #00a0e9; --c-blu: #004098; --c-prp: #800080; --c-gry: #555555;
        
        /* Tab Colors */
        --tab-eng: #ff69b4;
        --tab-num: #40e0d0;
        --tab-history: #8b4513;
        --tab-emoji: #ffd900;

        /* Button Colors */
        --col-ent: #ffd900;
        --col-ctl: #40e0d0;
        
        --col-ai: #ff1493;
        --col-sch: #00e676;
        --col-cpy: #00bfff;
        --col-pst: #ff8c00;
        --col-silver: #c0c0c0;

        --cursor-color: #00ffff;
        --bg-inactive-btn: #333333;
        --bg-candidate: #444444;
        --bg-chip: #ffffff;
    }

    @keyframes border-blink {
        0%   { border-color: var(--cursor-color); box-shadow: 0 0 5px var(--cursor-color); }
        50%  { border-color: transparent; box-shadow: none; }
        100% { border-color: var(--cursor-color); box-shadow: 0 0 5px var(--cursor-color); }
    }

    body {
        margin: 0; padding: 0;
        background-color: var(--bg-color);
        color: var(--text-color);
        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
        height: 100vh; height: 100dvh; width: 100vw;
        display: flex; flex-direction: column;
        overflow: hidden; user-select: none; touch-action: none; 
    }

    /* --- Top Area --- */
    #top-area {
        padding-top: max(env(safe-area-inset-top), 10px);
        padding-bottom: 2px; padding-left: 5px; padding-right: 5px;
        flex-shrink: 0; z-index: 100; box-sizing: border-box;
        background: #000; border-bottom: 2px solid #333;
        display: flex; flex-direction: column; gap: 3px;
        transition: all 0.3s ease;
    }

    /* Fullscreen Editor Mode */
    #top-area.fullscreen-editor {
        position: fixed; top: 0; left: 0; right: 0; bottom: 0;
        height: 100dvh; width: 100vw;
        padding-bottom: max(env(safe-area-inset-bottom), 20px);
        background: #000; z-index: 5000;
    }
    #top-area.fullscreen-editor #display-wrapper { height: 100%; flex-direction: column; }
    #top-area.fullscreen-editor #line-indicator { width: 100%; flex-direction: row; height: 30px; gap: 10px; padding: 0 10px; }
    #top-area.fullscreen-editor #input-line { flex: 1; height: auto; font-size: 20px; }
    #top-area.fullscreen-editor .side-controls, 
    #top-area.fullscreen-editor #sub-bar { display: none; }

    /* Fullscreen Candidate Mode */
    #top-area.fullscreen-cand {
        position: fixed; top: 0; left: 0; right: 0; bottom: 0;
        height: 100dvh; width: 100vw;
        padding-bottom: max(env(safe-area-inset-bottom), 20px);
        background: #000; z-index: 5000;
        display: flex; flex-direction: column;
    }
    #top-area.fullscreen-cand #display-wrapper { display: none; }
    #top-area.fullscreen-cand #sub-bar { height: 100%; flex-direction: column; }
    #top-area.fullscreen-cand #candidate-bar { flex: 1; align-content: flex-start; }
    #top-area.fullscreen-cand .extended-tools { 
        height: 60px; flex-shrink: 0; flex-direction: row; width: 100%; 
        justify-content: flex-end; padding: 5px; box-sizing: border-box;
    }
    #top-area.fullscreen-cand .tool-col { width: 60px; height: 100%; }

    #display-wrapper {
        display: flex; align-items: stretch;
        height: 90px; gap: 4px;
        transition: height 0.3s;
    }
    
    #exit-full-editor-btn {
        display: none;
        position: absolute; bottom: 20px; right: 20px;
        width: 50px; height: 50px; border-radius: 25px;
        background: rgba(255, 217, 0, 0.8); color: #000;
        font-size: 24px; border: 2px solid #fff;
        align-items: center; justify-content: center;
        z-index: 5001;
    }
    #top-area.fullscreen-editor #exit-full-editor-btn { display: flex; }

    /* Line Indicator */
    #line-indicator {
        width: 40px; 
        display: flex; flex-direction: column; 
        align-items: center; justify-content: flex-start;
        padding-top: 6px;
        background: #111; border-radius: 4px; flex-shrink: 0;
        line-height: 1.0;
    }
    .ln-num { font-size: 20px; font-weight: bold; color: #fff; }
    .ln-label { font-size: 10px; color: #fff; margin-top: 2px; }

    /* Input Line */
    #input-line {
        font-size: 18px; line-height: 1.5;
        white-space: pre-wrap; overflow-y: auto;
        flex: 1;
        border: 2px solid #555; background: #ffffff; color: #000000;
        padding: 6px; border-radius: 6px;
        word-break: break-word;
        outline: none; caret-color: #000;
    }
    [contenteditable] { -webkit-user-select: text; user-select: text; }
    /* Composing Style: Blue underline */
    .composing-span { background: #d0ebff; border-bottom: 3px solid #00b4d8; }

    /* Side Controls */
    .side-controls {
        display: grid;
        grid-template-columns: 1fr 1fr;
        grid-template-rows: 1fr 1fr;
        gap: 2px; width: 70px; flex-shrink: 0;
    }
    .ctrl-btn {
        border: 2px solid transparent; border-radius: 4px;
        color: #000; font-size: 16px; font-weight: bold;
        cursor: pointer; display: flex; align-items: center; justify-content: center;
        background: var(--col-ctl); box-sizing: border-box;
    }
    .btn-ent { background: var(--col-ent); font-size: 20px; color: #000; } 
    .btn-full { background: var(--col-ctl); color: #000; font-size: 16px; border: 1px solid #333; }

    /* Hover & Active Effects */
    .ctrl-btn:hover, .ctrl-btn:active,
    .sub-btn:hover, .sub-btn:active,
    .key:hover, .key:active,
    .cand-chip:hover, .cand-chip:active {
        border: 2px solid var(--cursor-color);
        animation: border-blink 0.8s infinite;
        z-index: 10; position: relative;
    }

    /* Sub Bar (Candidates & Tools) */
    #sub-bar {
        height: 100px; 
        display: flex; gap: 4px; flex-shrink: 0; align-items: stretch;
    }
    
    #candidate-bar {
        flex: 1; display: flex; flex-wrap: wrap; align-content: flex-start; overflow-y: auto;
        background: var(--bg-candidate); padding: 4px; border-radius: 4px; gap: 4px;
    }
    .cand-chip {
        background: #ffffff; color: #000000;
        padding: 6px 14px;
        border-radius: 6px; 
        font-size: 18px; font-weight: bold;
        border: 2px solid #ccc;
        height: auto; min-height: 36px;
        box-sizing: border-box; display: flex; align-items: center;
        cursor: pointer;
    }
    
    .extended-tools {
        display: flex; gap: 3px; flex-shrink: 0;
    }
    
    .tool-col {
        display: flex; flex-direction: column; gap: 2px;
        width: 40px; flex-shrink: 0;
    }
    .tool-col.col-full { width: 40px; }

    .sub-btn {
        border: 2px solid transparent; border-radius: 4px;
        font-size: 11px; font-weight: bold;
        display: flex; align-items: center; justify-content: center;
        cursor: pointer; box-sizing: border-box; width: 100%;
    }
    .tool-col .sub-btn { flex: 1; }

    .btn-cpy { background: var(--col-cpy); color: #fff; }
    .btn-pst { background: var(--col-pst); color: #fff; }
    .btn-cand-full { background: var(--col-ctl); color: #000; font-size: 16px; border: 1px solid #333; }
    .btn-sch { background: var(--col-sch); color: #fff; font-size: 10px; }
    .btn-ai  { background: var(--col-ai);  color: #fff; font-size: 12px; }

    /* --- Main Stage --- */
    #main-stage {
        flex: 1; position: relative; overflow: hidden;
        background: #111; padding: 2px;
    }

    #twothumb-board {
        display: grid;
        grid-template-columns: 1fr 2.8fr; 
        gap: 6px; 
        height: 100%; width: 100%; box-sizing: border-box;
    }
    
    #left-panel { display: grid; grid-template-columns: 1fr; grid-auto-rows: 1fr; gap: 3px; }
    #right-panel { display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(4, 1fr); gap: 3px; }
    
    #right-panel.list-layout { grid-template-columns: 1fr; grid-template-rows: repeat(4, 1fr); }
    
    /* 5x6 High Density Layout for English */
    #right-panel.eng-layout { 
        grid-template-columns: repeat(5, 1fr); 
        grid-template-rows: repeat(6, 1fr); 
    }

    .key {
        display: flex; align-items: center; justify-content: center;
        font-weight: bold; border-radius: 5px; 
        cursor: pointer; color: #fff;
        box-shadow: 0 2px 0 rgba(0,0,0,0.3);
        border: 2px solid transparent; box-sizing: border-box;
        text-align: center; line-height: 1.1; overflow: hidden;
    }
    .key-left.active {
        border-color: var(--cursor-color);
        animation: border-blink 1s infinite;
        filter: brightness(1.2);
        z-index: 5;
    }
    .font-emoji { font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", sans-serif; }

    .key-cat { background: var(--bg-inactive-btn); border: 1px solid #777; font-size: 26px; color: #fff; }
    .key-cat.active { background: #eee; color: #000; border: 2px solid #fff; }
    
    .key-pager { background: var(--col-silver); font-size: 32px; color: #000; border: 1px solid #999; }

    .key-char { background: #333; font-size: 28px; }
    /* Smaller font for 5x6 grid */
    .eng-layout .key-char { font-size: 20px; }
    
    .key-hist { background: #333; font-size: 16px; color: #fff; text-align: left; padding: 8px; justify-content: flex-start; white-space: normal; word-break: break-all; display: block; overflow: hidden; }

    /* --- Tab Bar --- */
    #tab-bar {
        height: auto;
        min-height: 50px;
        background: #111; border-top: 1px solid #333;
        display: flex; flex-shrink: 0; z-index: 10;
        padding-bottom: max(env(safe-area-inset-bottom) + 70px, 70px);
    }
    .tab {
        flex: 1; display: flex; align-items: center; justify-content: flex-start;
        font-size: 12px; font-weight: bold; color: #888;
        border-right: 1px solid #222; flex-direction: column;
        cursor: pointer; background: #222; transition: all 0.2s;
        line-height: 1.1;
        padding-top: 8px;
    }
    .tab-lbl-main { font-size: 14px; margin-bottom: 2px;}
    .tab-lbl-sub { font-size: 9px; opacity: 0.8; }
    
    .t-eng.active { background: var(--tab-eng); color: #fff; }
    .t-num.active { background: var(--tab-num); color: #000; }
    .t-hist.active { background: var(--tab-history); color: #fff; }
    .t-emoji.active { background: var(--tab-emoji); color: #000; }
</style>
</head>
<body>

<div id="top-area">
    <div id="display-wrapper">
        <div id="line-indicator">
            <div class="ln-num" id="ln-num-text">1</div>
            <div class="ln-label">Line</div>
        </div>
        <div id="input-line" contenteditable="true" inputmode="none" spellcheck="false" 
             oninput="handleInput()" onclick="updateLineNum()" onkeyup="updateLineNum()"></div>
        
        <div class="side-controls">
            <button class="ctrl-btn btn-ent no-focus-steal" onmousedown="event.preventDefault()" onclick="insertDirect('\n')">↵</button>
            <button class="ctrl-btn no-focus-steal" onmousedown="event.preventDefault()" onclick="moveLine(-1)">▲</button>
            <button class="ctrl-btn btn-full no-focus-steal" onmousedown="event.preventDefault()" onclick="toggleEditorFull()">□</button>
            <button class="ctrl-btn no-focus-steal" onmousedown="event.preventDefault()" onclick="moveLine(1)">▼</button>
        </div>
    </div>
    
    <div id="sub-bar">
        <div id="candidate-bar"></div>
        <div class="extended-tools">
            <div class="tool-col col-full">
                <button class="sub-btn btn-cand-full no-focus-steal" onmousedown="event.preventDefault()" onclick="toggleCandFull()">□</button>
            </div>
            <div class="tool-col">
                <button class="sub-btn btn-cpy no-focus-steal" onmousedown="event.preventDefault()" onclick="copyAll()">Copy</button>
                <button class="sub-btn btn-pst no-focus-steal" onmousedown="event.preventDefault()" onclick="pasteFromClipboard()">Paste</button>
            </div>
            <div class="tool-col">
                <button class="sub-btn btn-sch no-focus-steal" onmousedown="event.preventDefault()" onclick="searchWeb()">Web</button>
                <button class="sub-btn btn-ai no-focus-steal" onmousedown="event.preventDefault()" onclick="askAI()">AI</button>
            </div>
        </div>
    </div>
    
    <div id="exit-full-editor-btn" onclick="toggleEditorFull()">×</div>
</div>

<div id="main-stage">
    <div id="twothumb-board">
        <div id="left-panel"></div>
        <div id="right-panel"></div>
    </div>
</div>

<div id="tab-bar">
    <div class="tab t-eng active" onclick="switchMode('english', this)"><span class="tab-lbl-main">abc</span><span class="tab-lbl-sub">English</span></div>
    <div class="tab t-num" onclick="switchMode('num', this)"><span class="tab-lbl-main">123</span><span class="tab-lbl-sub">Num/Op</span></div>
    <div class="tab t-hist" onclick="switchMode('history', this)"><span class="tab-lbl-main">🕒</span><span class="tab-lbl-sub">History</span></div>
    <div class="tab t-emoji" onclick="switchMode('emoji', this)"><span class="tab-lbl-main">😀</span><span class="tab-lbl-sub">Emoji</span></div>
</div>

<script>
    const rainbowColors = ['#e60012','#f39800','#ffd900','#99cc00','#009944','#00a0e9','#004098','#4b0082','#800080','#e60033'];
    
    // Simple English Dictionary for demo
    const commonEnglishWords = [
        "the","be","to","of","and","a","in","that","have","I","it","for","not","on","with","he","as","you","do","at",
        "this","but","his","by","from","they","we","say","her","she","or","an","will","my","one","all","would","there","their","what",
        "so","up","out","if","about","who","get","which","go","me","when","make","can","like","time","no","just","him","know","take",
        "people","into","year","your","good","some","could","them","see","other","than","then","now","look","only","come","its","over","think","also",
        "back","after","use","two","how","our","work","first","well","way","even","new","want","because","any","these","give","day","most","us",
        "hello","hi","thanks","please","sorry","yes","why","where","right","left","help","love","great","ok","sure"
    ].sort();

    const numCategories = [{id:'digits',l:'Digit',src:"1234567890".split("")},{id:'math',l:'Math',src:"+-*/=<>".split("")},{id:'point',l:'Point',src:".,:;".split("")},{id:'bra',l:'Brckt',src:"()[]{}''\"\"".split("")},{id:'other',l:'Other',src:"%$#&@^|~`".split("")}];
    
    // New 3-Page English Structure
    const englishPages = [
        // 0: abc (26 letters)
        "abcdefghijklmnopqrstuvwxyz".split(""),
        // 1: ABC (26 letters)
        "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split(""),
        // 2: Sym (26 symbols to fit 5x6 layout with 4 footer keys)
        "!?'\"@#&()[]:;-+=_/<>{}$%*~|\\".split("") 
    ];

    const emojiData = [..."😀😁😂🤣😃😄😅😆😉😊😋😎😍😘🥰😗😙😚🙂🤗🤩🤔🤨😐😑😶🙄😏😣😥😮🤐😯😪😫😴😌😛😜😝🤤😒😓😔😕🙃🤑😲☹️🙁😖😞😟😤😢😭😦😧😨😩🤯😬😰😱🥵🥶😳🤪😵😡😠🤬😷🤒"];
    
    let historyData = [];
    let currentMode = 'lower';
    let currentSelection = 0;
    let composingText = "";
    const inputEl = document.getElementById('input-line');
    const candBar = document.getElementById('candidate-bar');

    // Init
    switchMode('english', document.querySelector('.t-eng'));
    inputEl.focus();

    document.addEventListener('selectionchange', () => {
        if(document.activeElement === inputEl) updateLineNum();
    });

    function switchMode(mode, tabEl) {
        currentMode = mode;
        document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
        if(tabEl) tabEl.classList.add('active');
        
        if (composingText) confirmComposition();

        const lp = document.getElementById('left-panel');
        const rp = document.getElementById('right-panel');
        lp.className = ""; rp.className = "";
        
        if (mode === 'num') {
            currentSelection = 0; renderLeftCategories(); renderRightCategory();
        } else if (mode === 'emoji') {
            currentSelection = 0; renderLeftEmojiNav(); renderRightEmoji();
        } else if (mode === 'history') {
            currentSelection = 0; renderLeftHistoryNav(); renderRightHistory();
        } else if (mode === 'english') {
            currentSelection = 0; renderLeftPagesEnglish(); renderRightPageEnglish();
        }
    }

    function createKey(cls, text, onClick) {
        const div = document.createElement('div');
        div.className = cls + " no-focus-steal"; div.innerText = text;
        div.onmousedown = (e) => e.preventDefault(); div.onclick = onClick; return div;
    }

    // --- English Mode Renderers ---
    function renderLeftPagesEnglish() {
        const el = document.getElementById('left-panel'); el.innerHTML = "";
        const labels = ["abc", "ABC", "Sym"];
        
        labels.forEach((lbl, idx) => {
            const btn = createKey("key key-cat", lbl, () => { currentSelection = idx; renderLeftPagesEnglish(); renderRightPageEnglish(); });
            if(idx === currentSelection) btn.classList.add('active'); 
            el.appendChild(btn);
        });
    }

    function renderRightPageEnglish() {
        const el = document.getElementById('right-panel'); el.innerHTML = "";
        const items = englishPages[currentSelection] || [];
        
        // Use High Density Layout (5x6 = 30 keys)
        el.className = "eng-layout"; 

        items.forEach(char => {
            // Letters trigger composing for prediction, Symbols trigger direct insert
            const isSymPage = (currentSelection === 2);
            
            const btn = createKey("key key-char", char, () => {
                if(isSymPage) insertDirect(char);
                else insertComposing(char);
            });
            
            // Color logic
            if (isSymPage) {
                btn.style.backgroundColor = "var(--col-silver)"; btn.style.color = "#000";
            } else {
                const code = char.toLowerCase().charCodeAt(0) - 97;
                if (code >= 0 && code < 26) {
                    const cIdx = Math.floor(code / 3) % rainbowColors.length;
                    btn.style.backgroundColor = rainbowColors[cIdx];
                    if(['#ffd900', '#99cc00', '#f39800', '#40e0d0'].includes(rainbowColors[cIdx])) btn.style.color = "#000";
                } else {
                    btn.style.backgroundColor = "#555";
                }
            }
            el.appendChild(btn);
        });
        
        // Add the 4 common footer keys
        // Comma
        const comma = createKey("key key-char", ",", () => insertDirect(","));
        comma.style.backgroundColor = "#777"; comma.style.fontSize="20px";
        el.appendChild(comma);

        // Period
        const period = createKey("key key-char", ".", () => insertDirect("."));
        period.style.backgroundColor = "#777"; period.style.fontSize="20px";
        el.appendChild(period);

        // Space
        const spc = createKey("key key-char", "Space", () => insertDirect(" "));
        spc.style.backgroundColor = "#777"; spc.style.fontSize="14px";
        el.appendChild(spc);

        // Delete
        const del = createKey("key key-char", "Del", deleteChar);
        del.style.backgroundColor = "#d62828"; del.style.fontSize="14px";
        el.appendChild(del);
    }

    // --- Number/Symbol Renderers ---
    function renderLeftCategories() {
        const el = document.getElementById('left-panel'); el.innerHTML = "";
        numCategories.forEach((cat, idx) => {
            const btn = createKey("key key-cat", cat.l, () => { currentSelection = idx; renderLeftCategories(); renderRightCategory(); });
            if(idx === currentSelection) btn.classList.add('active'); el.appendChild(btn);
        });
    }
    function renderRightCategory() {
        const el = document.getElementById('right-panel'); el.innerHTML = "";
        const cat = numCategories[currentSelection];
        cat.src.forEach(char => {
            const btn = createKey("key key-char", char, () => insertDirect(char));
            btn.style.backgroundColor = "#d1f5fd"; btn.style.color = "#000"; btn.style.textShadow = "none";
            el.appendChild(btn);
        });
        
        if (cat.id === 'other') {
             const spc = createKey("key key-char", "Space", () => insertDirect(" "));
             spc.style.backgroundColor = "#777"; spc.style.color="#fff"; spc.style.fontSize="16px";
             el.appendChild(spc);
             const del = createKey("key key-char", "Del", deleteChar);
             del.style.backgroundColor = "#d62828"; del.style.color="#fff"; del.style.fontSize="16px";
             el.appendChild(del);
        }
    }

    // --- Emoji Renderers ---
    function renderLeftEmojiNav() {
        const el = document.getElementById('left-panel'); el.innerHTML = "";
        el.appendChild(createKey("key key-pager", "▲", () => { if(currentSelection > 0) { currentSelection--; renderRightEmoji(); } }));
        el.appendChild(createKey("key key-pager", "▼", () => { const maxPage = Math.ceil(emojiData.length / 12) - 1; if(currentSelection < maxPage) { currentSelection++; renderRightEmoji(); } }));
    }
    function renderRightEmoji() {
        const el = document.getElementById('right-panel'); el.innerHTML = "";
        const items = emojiData.slice(currentSelection * 12, currentSelection * 12 + 12);
        items.forEach(char => {
            const btn = createKey("key key-char font-emoji", char, () => insertDirect(char));
            btn.style.fontSize = "24px"; btn.style.backgroundColor = "#ffffff"; btn.style.color = "#000"; btn.style.textShadow = "none";
            el.appendChild(btn);
        });
    }

    // --- History Renderers ---
    function addToHistory(text) {
        if(!text) return;
        text = text.trim();
        if(!text) return;
        historyData = historyData.filter(t => t !== text);
        historyData.unshift(text);
        if(historyData.length > 100) historyData.pop(); 
        if(currentMode === 'history') { renderLeftHistoryNav(); renderRightHistory(); }
    }
    
    function renderLeftHistoryNav() {
        const el = document.getElementById('left-panel'); el.innerHTML = "";
        el.appendChild(createKey("key key-pager", "▲", () => { if(currentSelection > 0) { currentSelection--; renderRightHistory(); } }));
        el.appendChild(createKey("key key-pager", "▼", () => { const maxPage = Math.ceil(historyData.length / 4) - 1; if(currentSelection < maxPage) { currentSelection++; renderRightHistory(); } }));
    }
    
    function renderRightHistory() {
        const el = document.getElementById('right-panel'); el.innerHTML = "";
        el.className = "list-layout"; 
        const start = currentSelection * 4;
        const items = historyData.slice(start, start + 4);
        items.forEach(text => {
            const btn = createKey("key key-hist", text, () => {
                insertDirect(text + " ");
            });
            el.appendChild(btn);
        });
    }

    // --- Logic & Prediction ---

    function insertComposing(char) { 
        composingText += char; 
        updateComposingDisplay(); 
        updateCandidates(); 
    }

    function updateComposingDisplay() {
        let span = inputEl.querySelector('.composing-span');
        if (!span) { 
            span = document.createElement('span'); 
            span.className = 'composing-span'; 
            insertNodeAtCursor(span); 
        }
        span.innerText = composingText; 
        placeCaretAfter(span); 
        updateLineNum();
    }

    function confirmComposition(textToCommit) {
        let commitVal = textToCommit;
        if (commitVal === undefined) commitVal = composingText;
        
        if (commitVal) {
            let finalStr = commitVal;
            // Add space if selected from candidates
            if (textToCommit !== undefined) {
                finalStr += " "; 
                addToHistory(commitVal); 
            }
            // Logic to append
            let span = inputEl.querySelector('.composing-span');
            if (span) { 
                const textNode = document.createTextNode(finalStr); 
                span.parentNode.replaceChild(textNode, span); 
                placeCaretAfter(textNode); 
            } else { 
                insertDirect(finalStr); 
            }
        }
        
        // Close fullscreen candidate if open
        document.getElementById('top-area').classList.remove('fullscreen-cand');
        
        composingText = ""; 
        candBar.innerHTML = ''; 
        updateLineNum();
    }

    function updateCandidates() {
        candBar.innerHTML = ""; 
        if (!composingText) return;
        const lowerInput = composingText.toLowerCase();
        let candidates = commonEnglishWords.filter(w => w.toLowerCase().startsWith(lowerInput));
        if (candidates[0] !== composingText) candidates.unshift(composingText);

        candidates.forEach(c => {
            const chip = document.createElement('div'); 
            chip.className = 'cand-chip no-focus-steal'; 
            chip.innerText = c;
            chip.onmousedown = (e) => e.preventDefault(); 
            chip.onclick = () => confirmComposition(c); 
            candBar.appendChild(chip);
        });
    }

    function insertDirect(text) { 
        if (composingText) confirmComposition(); 
        document.execCommand('insertText', false, text); 
        updateLineNum(); 
    }

    function deleteChar() {
        if (composingText) { 
            composingText = composingText.slice(0, -1); 
            if (!composingText) { 
                let span = inputEl.querySelector('.composing-span'); 
                if(span) span.remove(); 
                candBar.innerHTML = ''; 
            } else { 
                updateComposingDisplay(); 
                updateCandidates(); 
            } 
        } else { 
            document.execCommand('delete'); 
        } 
        updateLineNum();
    }

    function insertNodeAtCursor(node) {
        inputEl.focus(); const sel = window.getSelection();
        if (sel.rangeCount > 0) { const range = sel.getRangeAt(0); range.deleteContents(); range.insertNode(node); } else { inputEl.appendChild(node); }
    }
    function placeCaretAfter(node) { const range = document.createRange(); range.setStartAfter(node); range.collapse(true); const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); }
    
    function updateLineNum() { 
        const sel = window.getSelection();
        if (sel.rangeCount === 0) return;
        let node = sel.anchorNode;
        let isInside = false;
        while(node) { if(node === inputEl) { isInside = true; break; } node = node.parentNode; }
        if(!isInside) return;
        const range = sel.getRangeAt(0);
        const preCaretRange = range.cloneRange();
        preCaretRange.selectNodeContents(inputEl);
        preCaretRange.setEnd(range.endContainer, range.endOffset);
        const text = preCaretRange.toString();
        const currentLine = text.split('\n').length;
        document.getElementById('ln-num-text').innerText = currentLine; 
    }
    
    function handleInput() { updateLineNum(); }
    function copyAll() { navigator.clipboard.writeText(inputEl.innerText).then(()=>alert("Copied!")); }
    async function pasteFromClipboard() { try { const text = await navigator.clipboard.readText(); insertDirect(text); } catch(e) {} }
    
    function toggleEditorFull() { 
        document.getElementById('top-area').classList.toggle('fullscreen-editor'); 
        document.getElementById('top-area').classList.remove('fullscreen-cand');
    }
    
    function toggleCandFull() {
        document.getElementById('top-area').classList.toggle('fullscreen-cand');
        document.getElementById('top-area').classList.remove('fullscreen-editor');
    }

    function moveLine(d) { inputEl.scrollTop += d * 30; }

    function searchWeb() { const t = inputEl.innerText; if(t) window.open(`https://www.google.com/search?q=${encodeURIComponent(t)}`, '_blank'); }
    function askAI() { const t = inputEl.innerText; if(t) window.open(`https://chatgpt.com/?q=${encodeURIComponent(t)}`, '_blank'); }
</script>
</body>
</html>

いいなと思ったら応援しよう!

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
小ぶりなプログラムを試しに作っているんですが、 ここではその説明書きをしていこうと思います。 こういう機能をつけてみてほしいだとかいった要望があれば コメント欄に書いてみて下さい。 ひまをみて対応します。
スマホの入力を簡単にする?「Two Tap Input」|古井和雄
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word

mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1