From 519b068e1465b30584a3d85f37409bdba7162e61 Mon Sep 17 00:00:00 2001 From: Masaki Aida <51500566+aida0710@users.noreply.github.com> Date: Wed, 11 Nov 2026 20:00:18 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix(web):=20=E3=83=95=E3=82=A9=E3=83=BC?= =?UTF-8?q?=E3=82=AB=E3=82=B9=E3=81=AE=E6=9E=A0=E3=81=8C=E3=80=81=E3=81=AF?= =?UTF-8?q?=E3=81=BF=E5=87=BA=E3=81=97=E3=82=92=E5=88=87=E3=82=8B=E8=A6=AA?= =?UTF-8?q?=E3=81=A7=E6=AC=A0=E3=81=91=E3=82=8B=E9=83=A8=E5=93=81=E3=82=92?= =?UTF-8?q?=E7=9B=B4=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit フォーカスの枠は要素の外へ4px出る。端まで届く行や、横に流れるタブの列の中のボタンでは、 親が枠を切って片側が見えなかった。 - 全ページを巡回する配置の検査(e2e/support/layoutDefects.ts)に cut-focus-outline を足す。 PCの大きさの画面で、枠の片側が親に切られる部品を報告する。直す前は延べ167件 - Connectionsのグループの行、サイドバーの「新しいセッション」、接続エディタのタブ、スニペットの行、 SFTPのタブとパンくずは、枠を内側に描く(sshc-focus-inset) - 接続先を追加するフォームは、縦に流れる枠の左右に余白を取る Claude-Session: https://claude.ai/code/session_014WPnWu8NLT7prLQhhzYpc1 --- docs/design.md | 2 +- web/e2e/support/layoutDefects.ts | 38 ++++++++++++++++++- web/src/connections/ConnectionGroupFacets.tsx | 2 +- web/src/connections/CreateConnectionPane.tsx | 3 +- web/src/connections/HostDetail.tsx | 2 +- web/src/sftp/SFTPTabStrip.tsx | 6 +-- web/src/sftp/SFTPToolbar.tsx | 2 +- web/src/snippets/SnippetList.tsx | 2 +- web/src/terminal/WorkspaceTree.tsx | 2 +- 9 files changed, 48 insertions(+), 11 deletions(-) diff --git a/docs/design.md b/docs/design.md index efcc17508..1787d2588 100644 --- a/docs/design.md +++ b/docs/design.md @@ -116,7 +116,7 @@ - カード外周は子要素に隠れるinset shadowではなく、背景との差を抑えた`--ui-line`の実borderで描きます。外周と区切りは面の境界が分かる最小限の強さにし、操作対象の`--ui-control-line`より明確に弱くします。角丸は役割で決め、`web/src/index.css`の`@theme`にある5段階と、スマホの入力欄の22px(`rounded-composer`)だけを使います。板・ダイアログ・メニューは12px(`rounded-lg`)、板の中に置く箱・注意の箱・キーバーのキー・プレビューの枠・アイコンの台は10px(`rounded-inset`)、入力欄・ボタン・アイコンボタン・選ぶ行・メニューの項目は8px(`rounded-md`)、小さな札・チェックボックス・サムネイルは6px(`rounded-sm`)です。スマホのカードとタイル、ログイン前の画面のカードは14px(`rounded-card`)にします。素の`rounded`、`rounded-[10px]`のような直書き、`rounded-xl`や`rounded-2xl`は使いません。図の細い棒や升目に付ける3px以下の角丸だけは直書きできます。`web/src/ui/radius.test.ts`が違反をファイル名と行番号付きで報告します。`.sshc-card`(`Card`とモーダルの枠)は12pxの角丸を自分で持つので、使う側で角丸を重ねて指定しません。表の内部や連続したrowには角を付けず、接続状態badgeなど意味のあるpillだけ`rounded-full`を使用します。分割paneの左右headerは同じ最小高さとpaddingに揃えます。 - Connectionsの基本、踏み台・転送・その他、表示とターミナル、最終的な値の各タブは、tablistと選択中tabpanelを同じ外枠へ入れます。tabpanelを内側へpaddingし、タブから離れた別ブロックに見せません。各tabの`aria-controls`とtabpanelの`aria-labelledby`を対にします。踏み台・転送・その他のタブは、踏み台・ポート転送・その他の設定を節に分けて1画面に並べ、タブの中にさらにタブを置きません。Hostブロックの文をそのまま編集する欄は、見出しの「…」の[テキストで編集]で開き、URLでは`advanced=raw`で表します。 - 主要操作は対象と動作を明確にし、説明や未設定状態よりも目立たせます。Homeは検索・必要なグループ・接続先を主役にし、未所属や未接続の繰り返しを省きます。PCではカードのクリックで選択、ダブルクリックで接続します。タッチは1タップ、キーボードはEnter/Spaceで接続します。Homeには常設Connectボタンと操作方法の案内文を置かず、開始中の表示とカードの操作メニューを残します。Connectionsの設定画面にはConnectボタンを置きます。 -- フォーカスリングとチェック済みの印にも同じaccentを使います。ターミナルとコードエディタは各自のフォーカス/選択描画を維持します。フォーカスの枠は、`web/src/index.css`の決まりが付ける2pxのアウトライン(要素から2px離す)の1つの形だけです。ボタン・リンク・入力欄には決まりが自動で付けるので、部品の側でリングや枠線の色を重ねません。親がはみ出しを切っていて外側の枠が欠ける要素には`sshc-focus-inset`を付け、枠を内側に描きます。押せないボタンの不透明度も同じファイルの決まりが50%にそろえるので、部品の側に`disabled:opacity-*`を書きません(書いても効きません)。明暗それぞれで小さな文字・選択中の文字・主要ボタンの文字のコントラストを確認します。 +- フォーカスリングとチェック済みの印にも同じaccentを使います。ターミナルとコードエディタは各自のフォーカス/選択描画を維持します。フォーカスの枠は、`web/src/index.css`の決まりが付ける2pxのアウトライン(要素から2px離す)の1つの形だけです。ボタン・リンク・入力欄には決まりが自動で付けるので、部品の側でリングや枠線の色を重ねません。親がはみ出しを切っていて外側の枠が欠ける要素には`sshc-focus-inset`を付け、枠を内側に描きます。PCの大きさの画面で枠が欠ける部品は、全ページを巡回する`web/e2e/layout-audit.spec.ts`が`cut-focus-outline`として報告します。押せないボタンの不透明度も同じファイルの決まりが50%にそろえるので、部品の側に`disabled:opacity-*`を書きません(書いても効きません)。明暗それぞれで小さな文字・選択中の文字・主要ボタンの文字のコントラストを確認します。 - 文字の大きさは、Tailwindの`text-xs`(12px)・`text-sm`(14px)・`text-base`(16px)・`text-lg`・`text-xl`・`text-2xl`と、`web/src/index.css`の`@theme`が足す`text-3xs`(10px、札やキーの印)・`text-2xs`(11px、小さなラベルとセッションの行の2行目)・`text-ui`(13px、入力欄・ボタン・エディタ)だけを使います。名前は14px、補助の文は12pxです。`text-[12.5px]`のような直書きと0.5px刻みは使いません(`web/src/ui/fontSize.test.ts`が検査します。丸い印の中の記号の8px以下は対象外です)。足した3つは行の高さを持たないので、行の高さは置く場所の`leading-*`で決めます。共有の入力欄(`control`)に`text-xs`などを重ねても、CSSの並び順で`text-ui`が勝つので効きません。 - 重なりの順番は、ページの中(上に貼り付く見出し、スマホの下のナビゲーション、引き出し、メニュー、ダイアログ)を`z-50`までの数で書きます。それより手前に出す層は`web/src/index.css`が名前を付けます。コマンドパレットは`z-palette`(70)、ダイアログを開いていても見せる知らせ・リンクの行き先・コマンドのカードは`z-above-dialog`(80)、エンジンに届かないときの画面は`z-fallback`(100)です。`z-[70]`のような直書きは`web/src/ui/layers.test.ts`が検査します。 - パレットの規則は `web/src/ui/palette.test.ts` で検査します。Tailwind のパレット名(`text-red-400` など)、任意値(`text-[#ff0000]`)、inline style の hex 値を走査し、違反箇所をファイル名と行番号付きで報告します。例外は `palette-exempt` を記載した行だけで、現在は native の色入力が独自の既定値を必要とする 2 行に使用しています。 diff --git a/web/e2e/support/layoutDefects.ts b/web/e2e/support/layoutDefects.ts index 9a6987382..ad32c6d7a 100644 --- a/web/e2e/support/layoutDefects.ts +++ b/web/e2e/support/layoutDefects.ts @@ -20,7 +20,9 @@ export type LayoutDefectKind = // An icon or a dot beside a line of text, higher or lower than the middle of the line. | "off-centre-mark" // Rows of one list built alike whose leading marks differ in size, or whose text starts at different places. - | "uneven-rows"; + | "uneven-rows" + // The outline that marks the control in focus is cut away on one side by a box that hides its overflow. + | "cut-focus-outline"; export type LayoutDefect = { kind: LayoutDefectKind; @@ -372,6 +374,38 @@ function listLayoutDefects({ ignored }: ListingOptions): LayoutDefect[] { return Math.round(bottom - top) + 1; }; + // The outline of a control in focus is 2px wide and 2px away from it (index.css). A control marked + // sshc-focus-inset draws it inside itself, where nothing cuts it. + const outlineReachPx = 4; + // sideWhereFocusOutlineIsCut names the side on which a box that hides its overflow leaves no room for the whole + // outline, with the box. A control used by touch alone shows no outline, so a phone's layout is not judged. + const sideWhereFocusOutlineIsCut = (control: Element, box: DOMRect): { toString(): string; by: string } | null => { + if (laidOutAsPhone || control.classList.contains("sshc-focus-inset")) return null; + // A field that is hidden and drawn by the element after it (a switch, a swatch) shows the outline on that one. + if (getComputedStyle(control).opacity === "0") return null; + for (let holder = control.parentElement; holder !== null; holder = holder.parentElement) { + const style = getComputedStyle(holder); + const cutsSideways = style.overflowX !== "visible"; + const cutsDown = style.overflowY !== "visible"; + if (!cutsSideways && !cutsDown) continue; + const outer = holder.getBoundingClientRect(); + // What the holder shows is inside its border. + const shown = { left: outer.left + holder.clientLeft, top: outer.top + holder.clientTop, right: outer.left + holder.clientLeft + holder.clientWidth, bottom: outer.top + holder.clientTop + holder.clientHeight }; + const sides: Array<[string, boolean]> = [ + ["left", cutsSideways && holder.scrollLeft === 0 && box.left - outlineReachPx < shown.left - sidewaysTolerancePx], + ["right", cutsSideways && holder.scrollLeft + holder.clientWidth >= holder.scrollWidth - 1 && box.right + outlineReachPx > shown.right + sidewaysTolerancePx], + ["top", cutsDown && holder.scrollTop === 0 && box.top - outlineReachPx < shown.top - sidewaysTolerancePx], + ["bottom", cutsDown && holder.scrollTop + holder.clientHeight >= holder.scrollHeight - 1 && box.bottom + outlineReachPx > shown.bottom + sidewaysTolerancePx], + ]; + const cut = sides.find(([, isCut]) => isCut); + if (cut !== undefined) { + const by = `${holder.tagName.toLowerCase()}${holder.getAttribute("aria-label") === null ? "" : ` "${holder.getAttribute("aria-label")}"`}`; + return { toString: () => cut[0], by }; + } + } + return null; + }; + for (const control of controls) { control.scrollIntoView({ block: "center", inline: "nearest", behavior: "instant" }); const box = control.getBoundingClientRect(); @@ -386,6 +420,8 @@ function listLayoutDefects({ ignored }: ListingOptions): LayoutDefect[] { note("covered-control", control, `a press at its middle lands on ${hit === null ? "nothing" : `<${hit.tagName.toLowerCase()}> "${nameOf(hit)}"`}`); continue; } + const cutSide = sideWhereFocusOutlineIsCut(control, box); + if (cutSide !== null) note("cut-focus-outline", control, `the outline around it is cut away on the ${cutSide} by <${cutSide.by}>`); if (!laidOutAsPhone || control.hasAttribute("data-touch-compact")) continue; // A link in a line of text is the height of the line. It is pressed as a part of what is being read. if (control.tagName === "A" && getComputedStyle(control).display === "inline") continue; diff --git a/web/src/connections/ConnectionGroupFacets.tsx b/web/src/connections/ConnectionGroupFacets.tsx index 23b9d3801..6a39ef002 100644 --- a/web/src/connections/ConnectionGroupFacets.tsx +++ b/web/src/connections/ConnectionGroupFacets.tsx @@ -30,7 +30,7 @@ function floatingListSide(pointerX: number, listWidth: number): "left" | "right" } function facetLook(active: boolean): string { - return `flex min-h-[30px] w-full min-w-0 items-center gap-2.5 rounded-md px-2 text-left text-ui transition-colors ${ + return `sshc-focus-inset flex min-h-[30px] w-full min-w-0 items-center gap-2.5 rounded-md px-2 text-left text-ui transition-colors ${ active ? "bg-select-fill font-medium text-ink" : "text-ink-muted hover:bg-hover hover:text-ink" }`; } diff --git a/web/src/connections/CreateConnectionPane.tsx b/web/src/connections/CreateConnectionPane.tsx index fb11b41fc..52e945eea 100644 --- a/web/src/connections/CreateConnectionPane.tsx +++ b/web/src/connections/CreateConnectionPane.tsx @@ -78,7 +78,8 @@ export function CreateConnectionPane({ onSubmit={(event) => void form.submit(event)} className={`${editorContainer} flex min-h-0 flex-1 flex-col`} > -
+ {/* 縦に流れる枠は横のはみ出しも切る。欄のフォーカスの枠(外へ4px)が欠けないよう、左右に同じだけ余白を取る。 */} +
)} From 0ba6faff4bd411bd0f851390ef5462264918dae7 Mon Sep 17 00:00:00 2001 From: Masaki Aida <51500566+aida0710@users.noreply.github.com> Date: Wed, 11 Nov 2026 20:07:25 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix(web):=20=E3=83=AF=E3=83=B3=E3=82=BF?= =?UTF-8?q?=E3=82=A4=E3=83=A0=E3=83=91=E3=82=B9=E3=83=AF=E3=83=BC=E3=83=89?= =?UTF-8?q?=E3=81=AE=E3=82=B3=E3=83=BC=E3=83=89=E3=81=AE=E3=83=9B=E3=83=90?= =?UTF-8?q?=E3=83=BC=E3=82=92=E3=80=81=E3=83=A9=E3=82=A4=E3=83=88=E3=81=A7?= =?UTF-8?q?=E3=82=82=E8=A6=8B=E3=81=88=E3=82=8B=E8=89=B2=E3=81=AB=E3=81=99?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #823 で hover に替えたが、前後のコードは bg-well の箱の中にあり、ライトでは hover(#f0f0f0)と well(#f2f2f2)の見分けが付かない。一段濃い surface にする。 Claude-Session: https://claude.ai/code/session_014WPnWu8NLT7prLQhhzYpc1 --- web/src/secrets/TOTPCode.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/secrets/TOTPCode.tsx b/web/src/secrets/TOTPCode.tsx index 0fefdec32..c9eb117ef 100644 --- a/web/src/secrets/TOTPCode.tsx +++ b/web/src/secrets/TOTPCode.tsx @@ -27,7 +27,8 @@ function CopyableCode({ code, label, className }: { code: string; label: string; aria-label={label} title={refused ? t("copy.refused") : label} onClick={copy} - className={`flex items-center gap-1.5 rounded-md px-1.5 py-0.5 hover:bg-hover ${className}`} + // 前後のコードは bg-well の箱の中にある。ライトでは hover と well がほぼ同じ色なので、一段濃い surface を使う。 + className={`flex items-center gap-1.5 rounded-md px-1.5 py-0.5 hover:bg-surface ${className}`} > {readableCode(code)} {state === "idle" ? null : }