mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-02 13:39:41 +02:00
feat(readmymind): alternates row, phone accessory key, phone-sized modal (phase 3 part 1)
The Read My Mind modal grows up and reaches phones: - Alternate suggestions (the predictor's verify/redirect kinds) now render as tappable rows below the main field. Tapping one swaps it into the editable field; the edit you were making folds back into the row you leave, so toggling between alternates never loses typing. Rethink now records the WHOLE shown set (main + alternates) as rejected. - Phones get a 🧠 key on the keyboard accessory bar (both simple and extended layouts), gated on the same synced readMyMindEnabled setting via an rmm-enabled marker class on the BAR element: setMode() rebuilds the buttons' innerHTML, so per-key state would be wiped. Synced at init and re-synced by applyHeaderVisibilitySettings() on every settings apply, so a live toggle needs no reload. The header button stays off phones. - On phones the modal renders as a small dialog (mirrors modal-sm) instead of the full-screen default, with wrap-friendly finger-sized footer buttons. Not modal-sm itself: that caps desktop width at 340px and this modal wants 560px there. - On touch devices the ready/swap paths no longer focus the field, so the OS keyboard does not pop over the alternates that just rendered. - New static guard test/readmymind-phone-key.test.ts pins the dual-template key, the marker-class gating, the phone-hidden header button, the small-dialog phone modal, and the no-innerHTML discipline. Part 2 of phase 3 (rethink steering, the free-text steer note) is next; the API already accepts steer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
// Port: none (pure static analysis — runs in CI, no browser/server).
|
||||
//
|
||||
// Read My Mind phase 3 part 1 guards: the modal's alternates row and the phone
|
||||
// keyboard-accessory 🧠 key. The mobile Playwright suite is excluded from CI,
|
||||
// so like test/mobile-header-buttons-policy.test.ts this parses the frontend
|
||||
// assets directly to pin the wiring that only a phone would exercise:
|
||||
//
|
||||
// 1. the 🧠 key ships in BOTH accessory-bar templates (setMode() swaps the
|
||||
// bar's innerHTML between them, so a key present in only one layout would
|
||||
// silently vanish when the user toggles `extendedKeyboardBar`) and routes
|
||||
// to the shared modal;
|
||||
// 2. the key is hidden unless the bar carries the `rmm-enabled` marker class
|
||||
// — gating must live on the BAR element because setMode() rebuilds the
|
||||
// buttons — synced at init and re-synced by settings-ui.js on every
|
||||
// settings apply (a live toggle needs no reload);
|
||||
// 3. the header 🧠 button STAYS off phones (the key is the phone surface);
|
||||
// 4. the modal renders on phones as a small dialog, not the full-screen
|
||||
// default that phone `.modal-content` rules would impose;
|
||||
// 5. readmymind-ui.js keeps the no-innerHTML discipline (predictor output is
|
||||
// injectable content) and renders alternates into the i18n-skipped
|
||||
// container declared in index.html.
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const HERE = fileURLToPath(new URL('.', import.meta.url));
|
||||
const PUBLIC = join(HERE, '../src/web/public');
|
||||
const read = (name: string) => readFileSync(join(PUBLIC, name), 'utf-8');
|
||||
|
||||
describe('read my mind phone key + alternates (static guards)', () => {
|
||||
const accessory = read('keyboard-accessory.js');
|
||||
const styles = read('styles.css');
|
||||
const mobile = read('mobile.css');
|
||||
const html = read('index.html');
|
||||
const ui = read('readmymind-ui.js');
|
||||
const settingsUi = read('settings-ui.js');
|
||||
// Everything phone-specific lives in the max-width 430px block of mobile.css.
|
||||
const phoneBlock = mobile.slice(mobile.indexOf('@media (max-width: 430px)'));
|
||||
|
||||
it('ships the 🧠 key in BOTH accessory bar templates and routes it to the modal', () => {
|
||||
const simple = accessory.match(/_simpleButtons\s*:\s*`([\s\S]*?)`/)?.[1] ?? '';
|
||||
const extended = accessory.match(/_extendedButtons\s*:\s*`([\s\S]*?)`/)?.[1] ?? '';
|
||||
expect(simple).toMatch(/accessory-btn-rmm[^>]*data-action="readmymind"/);
|
||||
expect(extended).toMatch(/accessory-btn-rmm[^>]*data-action="readmymind"/);
|
||||
expect(accessory).toMatch(/case 'readmymind':/);
|
||||
expect(accessory).toMatch(/openReadMyMind/);
|
||||
});
|
||||
|
||||
it('hides the key until the bar carries rmm-enabled, synced at init and on settings apply', () => {
|
||||
expect(styles).toMatch(/\.keyboard-accessory-bar \.accessory-btn-rmm \{\s*display: none;/);
|
||||
expect(styles).toMatch(/\.keyboard-accessory-bar\.rmm-enabled \.accessory-btn-rmm \{\s*display: inline-flex;/);
|
||||
// init() applies the gate as soon as the bar exists…
|
||||
expect(accessory).toMatch(/this\.syncReadMyMind\(\);/);
|
||||
// …and settings-ui re-syncs it on every settings apply (live toggle).
|
||||
expect(settingsUi).toMatch(/KeyboardAccessoryBar\.syncReadMyMind/);
|
||||
});
|
||||
|
||||
it('keeps the header 🧠 button off phones (the accessory key is the phone surface)', () => {
|
||||
expect(phoneBlock).toMatch(/\.btn-icon-header\.btn-readmymind\s*\{\s*display: none !important;/);
|
||||
});
|
||||
|
||||
it('renders the modal as a small dialog on phones, not the full-screen default', () => {
|
||||
expect(phoneBlock).toMatch(/\.modal-content\.readmymind-modal\s*\{[^}]*height: auto;/);
|
||||
});
|
||||
|
||||
it('declares the i18n-skipped alternates container and keeps the no-innerHTML discipline', () => {
|
||||
expect(html).toMatch(/id="readMyMindAlternates"[^>]*data-i18n-skip/);
|
||||
// Predictor output is injectable content: value/textContent only, ever.
|
||||
// (`.innerHTML`: property ACCESS — the fileoverview's "never innerHTML"
|
||||
// prose is allowed to say the word.)
|
||||
expect(ui).not.toMatch(/\.innerHTML/);
|
||||
expect(ui).toContain('readMyMindAlternates');
|
||||
expect(ui).toMatch(/\.textContent = suggestion\.prompt/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user