feat(486 B07 CAD 화면): 제목줄 · 램프 · 하단 도면 칸 지움 · ↶ ↷ = 리본 탭 줄 오른쪽 · 손가락 단추 = 자판 없는 기기만 · 탭 줄 빈 자리 변수

- 제목줄(Aislo CAD · 부제 · 「현재 도면」 램프) 지움 · --cad-title-height 0 · 💾 · JSON 내보내기 = DOM 두고 숨김(QUICK_ACCESS_HIDDEN)
- 빠른 실행 = 리본 탭 줄 빈 자리 뒤 · 접기 단추 앞(줄 높이 26 그대로 · M02 hostSave 면 안 그림)
- 빈 자리([출력] 오른쪽 ~ ↶ 왼쪽) → .cad-host 에 --cad-tab-slot-left/right(iframe 끝 기준 px) — 부모 탭 줄 겹침 자리
- ☰ 비키기 패딩 = 리본 탭 줄로 · 손가락 단추 = (hover: none) and (pointer: coarse) 만
- 하단 도면 이름 · ‹ › · 확정 칸(QuantityPanel) · 이동 요청 지움
- 시험 test_cad_touch(미디어 조건) · test_cad_host_tabs(머리줄 · 빈 자리 변수) 고쳐 씀

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 22:07:35 +09:00
co-authored by Claude Opus 5.5
parent afe6e4a799
commit dce63ccc21
13 changed files with 87 additions and 311 deletions
+13 -210
View File
@@ -3,7 +3,8 @@
@import "../../../ui_template/ui_template_theme.css";
:root {
--cad-title-height: 42px;
/* 제목줄 없음(요구 280-6) — 칸들이 이 변수로 자리 잡아 0 으로 둠 */
--cad-title-height: 0px;
/* 116px 에서는 리본 줄의 내용(85px)이 보이는 높이(81px)를 넘어 세로 스크롤바가
생겼다(2026-09-04 실측) — 모자란 4px + 여유 2px 를 더한다. */
--cad-ribbon-height: 122px;
@@ -55,11 +56,7 @@ button {
z-index: 2;
pointer-events: none;
}
.cad-host-save {
--cad-title-height: 0px;
}
.cad-bare {
--cad-title-height: 0px;
--cad-status-height: 0px;
}
.controls {
@@ -85,64 +82,6 @@ body > canvas[data-id="canvas"] {
cursor: none;
}
.cad-titlebar {
position: fixed;
inset: 0 0 auto 0;
height: var(--cad-title-height);
display: flex;
align-items: center;
gap: 24px;
padding: 0 12px;
background: var(--cad-chrome);
border-bottom: 1px solid var(--cad-line);
box-shadow: var(--shadow-sm);
}
.cad-brand {
display: flex;
align-items: baseline;
gap: 9px;
min-width: 220px;
}
.cad-brand strong {
color: var(--color-text);
font-size: 14px;
}
.cad-brand span,
.cad-file-state {
color: var(--cad-text-dim);
font-size: 11px;
}
.cad-file-state {
display: flex;
align-items: center;
gap: 7px;
}
.cad-file-state__dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--color-success);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-success) 25%, transparent);
}
.cad-title-actions {
display: flex;
gap: 4px;
margin-left: auto;
}
.cad-title-actions button {
height: 28px;
min-width: 32px;
padding: 0 9px;
border: 1px solid transparent;
border-radius: 3px;
background: transparent;
font-size: 12px;
}
.cad-title-actions button:hover {
border-color: var(--cad-line);
background: var(--cad-hover);
}
.cad-ribbon {
position: fixed;
top: var(--cad-title-height);
@@ -168,8 +107,11 @@ body > canvas[data-id="canvas"] {
padding: 0 8px;
border-bottom: 1px solid var(--cad-line);
}
.cad-ribbon-tabs__slot {
flex: 1;
min-width: 0;
}
.cad-ribbon-tabs__collapse {
margin-left: auto;
width: 24px;
height: 20px;
align-self: center;
@@ -275,14 +217,15 @@ body > canvas[data-id="canvas"] {
.cad-quick-access {
display: flex;
gap: 2px;
margin-left: 14px;
align-self: center;
}
.cad-quick-access button {
width: 26px;
height: 24px;
.cad-ribbon-tabs .cad-quick-access button {
width: 24px;
height: 20px;
padding: 0;
border: 1px solid transparent;
border-radius: 3px;
background: transparent;
border-radius: 4px;
line-height: 1;
font-size: 13px;
}
.cad-quick-access button:hover {
@@ -718,153 +661,13 @@ body.cad-file-dragging::after {
font-size: 10px;
}
/* -----------------------------------------------------------------------------
* 수량 산출표 패널 (CAD 화면 하단 중심, 접이식) — 첨부 양식
* -------------------------------------------------------------------------- */
.cad-qtable {
position: fixed;
z-index: 3;
/* 명령행 위로 띄운다 — 겹치면 명령 입력칸을 클릭으로 누를 수 없다(2026-08-31).
명령행을 접으면 --cad-command-height가 0이라 예전처럼 상태막대 위에 붙는다. */
bottom: calc(var(--cad-status-height) + var(--cad-command-height) + 8px);
left: calc(var(--cad-panel-width) + (100vw - var(--cad-panel-width)) / 2);
transform: translateX(-50%);
/* 표(가장 넓은 자식) 기준 폭 고정 → 접었다 펴도 폭이 변하지 않는다 */
width: max-content;
max-width: min(1040px, calc(100vw - var(--cad-panel-width) - 24px));
border: 1px solid var(--cad-line);
border-radius: 6px;
background: var(--cad-chrome-raised);
box-shadow: var(--shadow-lg);
color: var(--cad-text);
}
.cad-qtable__header {
display: flex;
align-items: center;
gap: 8px;
padding: 5px 8px;
}
.cad-qtable__title {
display: flex;
flex: 1;
align-items: baseline;
justify-content: center;
gap: 8px;
min-width: 0;
}
.cad-qtable__title strong {
color: var(--color-text);
font-size: 14px;
}
.cad-qtable__title span {
overflow: hidden;
color: var(--cad-text-dim);
font-size: 11px;
text-overflow: ellipsis;
white-space: nowrap;
}
.cad-qtable__status {
padding: 1px 7px;
border-radius: 999px;
font-size: 10px;
font-style: normal;
}
.cad-qtable__status[data-confirmed="true"] {
background: color-mix(in srgb, var(--color-success) 22%, transparent);
color: var(--color-success);
}
.cad-qtable__status[data-confirmed="false"] {
background: color-mix(in srgb, var(--color-warning) 22%, transparent);
color: var(--color-warning);
}
.cad-qtable__nav,
.cad-qtable__collapse {
width: 26px;
height: 24px;
border: 1px solid var(--cad-line);
border-radius: 4px;
background: var(--cad-chrome-sunken);
color: var(--cad-text);
font-size: 13px;
}
.cad-qtable__nav:hover:not(:disabled),
.cad-qtable__collapse:hover {
background: var(--cad-hover);
}
.cad-qtable__nav:disabled {
opacity: 0.35;
cursor: not-allowed;
}
.cad-qtable__body {
padding: 0 8px 8px;
overflow-x: auto;
}
/* 접힘: 표는 DOM에 남겨 폭은 유지하되 세로로만 감춘다 */
.cad-qtable[data-collapsed="true"] .cad-qtable__body {
max-height: 0;
padding-top: 0;
padding-bottom: 0;
overflow: hidden;
visibility: hidden;
}
.cad-qtable table {
margin: 0 auto;
border-collapse: collapse;
}
.cad-qtable th,
.cad-qtable td {
padding: 0;
border: 1px solid var(--cad-line);
font-size: 11px;
text-align: center;
vertical-align: middle;
}
.cad-qtable__label {
padding: 2px 9px;
color: var(--cad-text-dim);
font-weight: 600;
white-space: nowrap;
background: var(--cad-chrome);
}
.cad-qtable__label--vertical {
width: 18px;
writing-mode: vertical-rl;
text-orientation: upright;
letter-spacing: 1px;
}
.cad-qtable__value input {
width: 72px;
padding: 3px 4px;
border: 0;
background: transparent;
color: var(--cad-text);
font: inherit;
text-align: center;
}
.cad-qtable__value input:focus {
outline: 2px solid var(--color-focus-ring);
outline-offset: -2px;
background: var(--cad-hover);
}
.cad-qtable__value--derived input {
color: var(--cad-text-muted);
background: var(--cad-chrome-sunken);
cursor: not-allowed;
}
@media (max-width: 800px) {
:root {
--cad-panel-width: 200px;
}
.cad-file-state,
.cad-statusbar__hint {
display: none;
}
.cad-brand {
min-width: auto;
}
}
/* 특성 팔레트 — 값을 바로 고치는 입력들 */
-2
View File
@@ -1,14 +1,12 @@
import './App.css';
import { ToastContainer } from 'react-toastify';
import { FramePlaceholderPanel } from './components/FramePlaceholderPanel.tsx';
import { QuantityPanel } from './components/QuantityPanel.tsx';
import { Toolbar } from './components/Toolbar.tsx';
function App() {
return (
<div className="cad-app">
<Toolbar />
<QuantityPanel />
<FramePlaceholderPanel />
<ToastContainer position="bottom-right" theme="light" />
</div>
@@ -1,57 +0,0 @@
import { type FC, useCallback, useEffect, useState } from 'react';
import { HtmlEvent } from '../App.types';
import { requestDrawingNavigation } from '../integration/aislo-drawing-bridge';
import { getDesignMeta } from '../state';
/**
* 도면 내비게이션 바 (CAD 화면 하단 중심).
* 수량 산출표는 HTML 테이블 대신 도면 자체의 CAD 테이블 레이어(b08-cross-table)로
* 렌더되므로, 여기서는 제목·측점정보·확정상태·이전/다음 이동만 담당한다.
*/
export const QuantityPanel: FC = () => {
const [meta, setMeta] = useState(getDesignMeta());
const refresh = useCallback(() => setMeta(getDesignMeta()), []);
useEffect(() => {
window.addEventListener(HtmlEvent.UPDATE_STATE, refresh);
return () => window.removeEventListener(HtmlEvent.UPDATE_STATE, refresh);
}, [refresh]);
if (!meta) return null;
return (
<section className="cad-qtable controls" data-collapsed="true">
<header className="cad-qtable__header">
<button
type="button"
className="cad-qtable__nav"
disabled={!meta.hasPrev}
title="이전 도면"
onClick={() => requestDrawingNavigation('prev')}
>
‹
</button>
<div className="cad-qtable__title">
<strong>{meta.title}</strong>
{meta.info && <span>{meta.info}</span>}
<em
className="cad-qtable__status"
data-confirmed={meta.confirmed}
title={meta.confirmed ? '설계 확정됨' : '미확정'}
>
{meta.confirmed ? '확정' : '미확정'}
</em>
</div>
<button
type="button"
className="cad-qtable__nav"
disabled={!meta.hasNext}
title="다음 도면"
onClick={() => requestDrawingNavigation('next')}
>
›
</button>
</header>
</section>
);
};
@@ -1,26 +1,14 @@
/** 제목표시줄 + 빠른 실행 도구막대 (AutoCAD 상단 막대) */
import { type FC, useEffect, useState } from 'react';
import { HtmlEvent } from '../App.types';
/** 빠른 실행 단추(↶ ↷) — 리본 탭 줄 오른쪽(접기 단추 앞)에 그린다 · 숨김 목록은 DOM 만 둔다 */
import type { FC } from 'react';
import { getCommandById } from '../commands/registry';
import { runCommand } from '../commands/run-command';
import { QUICK_ACCESS_COMMANDS } from '../ribbon/ribbon.config';
import { getHostTitle, isHostSave } from '../state';
import { QUICK_ACCESS_COMMANDS, QUICK_ACCESS_HIDDEN } from '../ribbon/ribbon.config';
import { isHostSave } from '../state';
export const QuickAccessBar: FC = () => {
const [hostTitle, setTitle] = useState(getHostTitle());
useEffect(() => {
const refresh = () => setTitle(getHostTitle());
window.addEventListener(HtmlEvent.UPDATE_STATE, refresh);
return () => window.removeEventListener(HtmlEvent.UPDATE_STATE, refresh);
}, []);
// 부모 페이지가 저장·이름을 맡는 M02 에선 제목 줄을 숨겨 그림 칸을 넓힌다.
// 부모 [저장]이 맡는 M02 는 우하단 ViewControls 에 ↶ ↷ 가 있어 안 그린다
if (isHostSave()) return null;
return (
<header className="cad-titlebar controls">
<div className="cad-brand">
<strong>Aislo CAD</strong>
<span>{hostTitle}</span>
</div>
<div className="cad-quick-access">
{QUICK_ACCESS_COMMANDS.map((id) => {
const command = getCommandById(id);
@@ -29,6 +17,7 @@ export const QuickAccessBar: FC = () => {
<button
type="button"
key={command.id}
hidden={QUICK_ACCESS_HIDDEN.includes(id)}
title={`${command.label} (${command.id})`}
onClick={() => runCommand(command)}
>
@@ -37,10 +26,5 @@ export const QuickAccessBar: FC = () => {
);
})}
</div>
<div className="cad-file-state">
<span className="cad-file-state__dot" />
현재 도면
</div>
</header>
);
);
};
@@ -1,5 +1,5 @@
/**
* CAD 화면 골격 조립 — 제목표시줄 · 리본 · 좌측 팔레트 · 탐색막대 · 명령행 · 상태막대.
* CAD 화면 골격 조립 — 리본(빠른 실행 포함) · 좌측 팔레트 · 탐색막대 · 명령행 · 상태막대.
* 각 조각은 자기 파일에 있고, 여기서는 배치와 표시 상태만 다룬다.
*/
import { type FC, useEffect, useState } from 'react';
@@ -8,7 +8,6 @@ import { InspectorPanel } from './InspectorPanel';
import { QuickProperties } from './QuickProperties';
import { BlockLibraryPanel } from './BlockLibraryPanel';
import { LayersWidget, PropertiesWidget, TextStyleWidget } from './RibbonWidgets';
import { QuickAccessBar } from './QuickAccessBar';
import { Ribbon } from '../ribbon/Ribbon';
import { StatusBar } from './StatusBar';
import { useCadRefresh } from './use-cad-refresh';
@@ -50,7 +49,6 @@ export const Toolbar: FC = () => {
return (
<>
<QuickAccessBar />
<Ribbon
activeTool={activeTool}
widgets={{
@@ -1,10 +1,10 @@
/* 손가락 기기 명령 단추(기기대응 4-1 · `input-touch.ts`) — 자판 없이 Esc · Enter · 지움 · 되돌리기 · 다시 · 스냅.
마우스만 있는 화면엔 안 보임. 그림 면 오른쪽 가운데에 세로로. */
자판 없는 기기(손가락이 주 입력 · 마우스 스침 없음)만 보임 — 터치 노트북엔 안 보임(요구 280-6). 그림 면 오른쪽 가운데에 세로로. */
.cad-touch-bar {
display: none;
}
@media (any-pointer: coarse) {
@media (hover: none) and (pointer: coarse) {
.cad-touch-bar {
position: fixed;
top: 50%;
@@ -41,7 +41,6 @@ export const AISLO_DRAWING_ERROR_MESSAGE = 'aislo:b08:drawing-error';
export const AISLO_DRAWING_CHANGED_MESSAGE = 'aislo:b08:drawing-changed';
export const AISLO_DRAWING_SAVE_REQUEST_MESSAGE = 'aislo:b08:save-request';
export const AISLO_DRAWING_SAVE_RESPONSE_MESSAGE = 'aislo:b08:save-response';
export const AISLO_DRAWING_NAVIGATE_MESSAGE = 'aislo:b08:navigate';
export const AISLO_DRAWING_EXPORT_MESSAGE = 'aislo:b08:export-file';
export const AISLO_DRAWING_HOST_SAVE_MESSAGE = 'aislo:b08:host-save';
/** 작도 영역 가이드(169-1 · 169-3) — 부모 숫자 칸 → CAD 갈기 · CAD 끌기 → 부모 숫자 칸 */
@@ -95,11 +94,6 @@ function notifyParent(type: string, payload: Record<string, unknown> = {}) {
window.parent.postMessage({ type, ...payload }, window.location.origin);
}
/** 수량 패널에서 이전/다음 도면으로 이동 요청 (부모가 처리). */
export function requestDrawingNavigation(direction: 'prev' | 'next') {
notifyParent(AISLO_DRAWING_NAVIGATE_MESSAGE, { direction });
}
/** 저장 요청을 부모 페이지 [저장]에 넘긴다 — 캐드는 양식 파일을 모른다. */
export function requestHostSave() {
notifyParent(AISLO_DRAWING_HOST_SAVE_MESSAGE);
@@ -1,4 +1,4 @@
/* 앱 ☰ 자리 비키기(기기대응 7-2) — 값은 host-burger.ts 가 적음 · 틀 없이 띄우면 0 */
html .cad-titlebar {
padding-left: max(12px, var(--host-burger-pad, 0px));
html .cad-ribbon-tabs {
padding-left: max(8px, var(--host-burger-pad, 0px));
}
@@ -1,6 +1,6 @@
/**
* 앱 ☰ 자리 비키기(기기대응 7-2) — 앱(B07)이 왼쪽 위에 띄운 ☰ 가 이 틀(iframe) 맨 위 줄에 걸치면
* 걸친 폭 + 틈 8 을 `--host-burger-pad` 로 적음(App.css `.cad-titlebar` 가 그만큼 왼쪽을 비움).
* 걸친 폭 + 틈 8 을 `--host-burger-pad` 로 적음(리본 탭 줄 `.cad-ribbon-tabs` 가 그만큼 왼쪽을 비움).
* 좌측 패널을 여닫으면 틀 폭이 바뀌어 resize 로 다시 잼 · 홀로 띄운 CAD(틀 없음)는 0.
*/
import './host-burger.css';
@@ -1,6 +1,7 @@
/** 리본 렌더러 — ribbon.config.ts의 데이터와 명령 레지스트리만 보고 그린다. */
import { type FC, type ReactNode, useEffect, useRef, useState } from 'react';
import type { CadCommand } from '../commands/command.types';
import { QuickAccessBar } from '../components/QuickAccessBar';
import { getCommandById } from '../commands/registry';
import { runCommand } from '../commands/run-command';
import {
@@ -157,8 +158,33 @@ export const Ribbon: FC<RibbonProps> = ({ activeTool, widgets, onCommandRun }) =
// (오피스 「리본 자동 숨기기」). 접기 단추로 펼치면 고정 펼침.
const [peek, setPeek] = useState(false);
const navRef = useRef<HTMLElement>(null);
const slotRef = useRef<HTMLSpanElement>(null);
const peeking = collapsed && peek;
// 탭 줄 빈 자리([출력] 오른쪽 ~ ↶ 왼쪽)를 부모 `.cad-host` 에 적음 — 부모 페이지가 자기 탭 줄을 거기 겹침.
// 값 = iframe 왼쪽 끝 → 빈 자리 왼쪽 · 빈 자리 오른쪽 → iframe 오른쪽 끝(px · CSS right 꼴) · 짝 선례 `integration/host-burger.ts`.
useEffect(() => {
const host = window.frameElement?.parentElement;
const slot = slotRef.current;
if (!host?.classList.contains('cad-host') || !slot) return;
const update = (): void => {
const s = slot.getBoundingClientRect();
host.style.setProperty('--cad-tab-slot-left', `${Math.round(s.left)}px`);
host.style.setProperty(
'--cad-tab-slot-right',
`${Math.round(window.innerWidth - s.right)}px`
);
};
update();
const observer = new ResizeObserver(update);
observer.observe(slot);
window.addEventListener('resize', update);
return () => {
observer.disconnect();
window.removeEventListener('resize', update);
};
}, []);
useEffect(() => {
if (!peeking) return;
const close = (event: PointerEvent) => {
@@ -189,6 +215,8 @@ export const Ribbon: FC<RibbonProps> = ({ activeTool, widgets, onCommandRun }) =
{tab.label}
</button>
))}
<span ref={slotRef} className="cad-ribbon-tabs__slot" />
<QuickAccessBar />
<button
type="button"
className="cad-ribbon-tabs__collapse"
@@ -204,5 +204,7 @@ export const RIBBON_TABS: RibbonTab[] = [
},
];
/** 제목표시줄의 빠른 실행 도구막대 */
/** 리본 탭 줄 오른쪽 빠른 실행 단추 */
export const QUICK_ACCESS_COMMANDS: string[] = ['QSAVE', 'EXPORT', 'UNDO', 'REDO'];
/** 그려 두고 숨기는 빠른 실행 — 출력 탭에 같은 명령이 있음 · 되살림 = 여기서 뺌 */
export const QUICK_ACCESS_HIDDEN: string[] = ['QSAVE', 'EXPORT'];
+20 -1
View File
@@ -218,7 +218,26 @@ def test_B07_목록에서_연_도면이_탭_미저장_편집_지킴():
assert "tab.dirty ? tab.drawing : undefined" in page # 돌아오면 받아 둔 편집본
assert "cadDirty = !!stash;" in page and "cad.load(stash ?? response.drawing" in page
assert "if (!cadDirty && !tabs.anyDirty()) return true;" in page
assert "void openTab(drawings[target]);" in page # 이전 · 다음 = 그 도면 탭
def test_CAD_머리줄_없음_탭줄_빈자리_변수():
"""요구 280-6 — 제목줄 · 하단 도면 칸(‹ ›) 지움 · ↶ ↷ = 리본 탭 줄 · 빈 자리를 부모 탭 줄 자리로 적음."""
src = ROOT / "B07_DesignDetail/openwebcad/src"
ribbon = (src / "ribbon/Ribbon.tsx").read_text("utf-8")
quick = (src / "components/QuickAccessBar.tsx").read_text("utf-8")
css = (src / "App.css").read_text("utf-8")
assert '<span ref={slotRef} className="cad-ribbon-tabs__slot" />' in ribbon
assert ribbon.index("cad-ribbon-tabs__slot") < ribbon.index("<QuickAccessBar />")
assert "'--cad-tab-slot-left', `${Math.round(s.left)}px`" in ribbon
assert "'--cad-tab-slot-right', `${Math.round(window.innerWidth - s.right)}px`" in ribbon
assert "hidden={QUICK_ACCESS_HIDDEN.includes(id)}" in quick and "cad-titlebar" not in quick
assert "QUICK_ACCESS_HIDDEN: string[] = ['QSAVE', 'EXPORT']" in (
src / "ribbon/ribbon.config.ts"
).read_text("utf-8")
assert "--cad-title-height: 0px;" in css and ".cad-titlebar" not in css
assert ".cad-qtable" not in css and not (src / "components/QuantityPanel.tsx").exists()
assert "QuantityPanel" not in (src / "App.tsx").read_text("utf-8")
assert "html .cad-ribbon-tabs {" in (src / "integration/host-burger.css").read_text("utf-8")
def test_여러_장_문서_장_도번_백업_칸(got):
+10 -3
View File
@@ -1,14 +1,16 @@
"""B07 CAD 손가락(PLAN_기기대응 4-1 · `openwebcad/src/inputController/input-touch.ts`) — 빌드된 CAD 화면.
① 손가락 기기 = 명령 단추 6개(44px) · 마우스 기기 = 안 보임 ② 확대 막기(user-scalable=no) 없음
① 자판 없는 기기(hover:none · pointer:coarse) = 명령 단추 6개(44px) · 마우스 · 터치 노트북 = 안 보임(요구 280-6)
② 확대 막기(user-scalable=no) 없음
③ 캔버스 touch-action none · 두 손가락 벌림 · 한 손가락 끌기가 페이지를 확대 · 밀지 않음(페이지 오류 0)
"""
import os
import urllib.request
import pytest
URL = "http://localhost:5174/b07-cad/"
URL = os.environ.get("CAD_TOUCH_URL", "http://localhost:5174/b07-cad/")
def _up() -> bool:
@@ -42,7 +44,12 @@ def test_cad_touch_bar_and_gestures():
mouse.evaluate("getComputedStyle(document.querySelector('.cad-touch-bar')).display")
== "none"
)
# 터치 노트북(any-pointer: coarse · 주 입력 마우스) — 단추는 hover:none 일 때만(에뮬은 터치 노트북을 못 꾸밈)
media = mouse.evaluate(
"[...document.styleSheets].flatMap(s => [...s.cssRules]).filter(r => r.media"
" && [...r.cssRules].some(c => c.selectorText === '.cad-touch-bar')).map(r => r.media.mediaText)"
)
assert media == ["(hover: none) and (pointer: coarse)"], media
ctx = browser.new_context(
viewport={"width": 1180, "height": 820}, has_touch=True, is_mobile=True
)