[{"data":1,"prerenderedAt":751},["ShallowReactive",2],{"content-/mf-miller-columns-ui":3,"all-pages-for-dir":749,"og-image-/mf-miller-columns-ui":750},{"id":4,"title":5,"body":6,"category":729,"description":730,"extension":731,"meta":732,"navigation":733,"ogImage":734,"path":735,"project_name":736,"published":737,"publishedAt":738,"seo":739,"stem":740,"tags":741,"todo":747,"unpublished":737,"updatedAt":734,"__hash__":748},"pages/2026-03/2026-03-23/mf-miller-columns-ui.md","Chrome拡張の会計サービス連携UIをタブからミラーカラムズに書き直した記録",{"type":7,"value":8,"toc":714},"minimark",[9,13,22,25,30,33,36,39,41,44,50,53,84,280,287,289,293,296,301,304,307,310,459,462,465,467,471,474,481,485,492,658,660,664,677,684,686,689,699,701,704,707,710],[10,11,5],"h1",{"id":12},"chrome拡張の会計サービス連携uiをタブからミラーカラムズに書き直した記録",[14,15,16,17,21],"p",{},"最初に組んだタブUIがレビューで「ナビと中身を並べて見たい」と却下され、ミラーカラムズレイアウトに全面書き直しした。左にナビ、右にコンテンツの2カラム構成。MDX-Playgroundのデザインシステム（",[18,19,20],"code",{},"/design-principles/a/1","）を開いて構造を揃え、機能統合やレスポンシブ対応まで含めて半日かかった。",[23,24],"hr",{},[26,27,29],"h2",{"id":28},"タブuiが却下されるまで","タブUIが却下されるまで",[14,31,32],{},"会計サービス連携の拡張には、スプレッドシートへのインポート、明細の一括取得、事業者設定の3つの機能がある。最初はブラウザ拡張でよくあるタブ切替UIで組んだ。「インポート」「明細取得」「設定」のタブが横に並ぶ構成。",[14,34,35],{},"ユーザーから「タブだと今どの機能にいるか見失う。左にナビを常時出して、右で操作したい」と指摘が入った。確かに、タブを切り替えると前の画面が消える。設定を参照しながらインポートを操作するには毎回行ったり来たりになる。",[14,37,38],{},"ミラーカラムズレイアウト――macOSのFinderでいうカラム表示のような、左ナビ＋右コンテンツの2カラム構造に切り替えることにした。",[23,40],{},[26,42,43],{"id":43},"デザインシステムを参照して構造を揃える",[14,45,46,47,49],{},"MDX-Playgroundにはデザインガイドライン（",[18,48,20],{},"）がある。ここに定義されたカラー、スペーシング、コンポーネント粒度を踏襲して、拡張のUIにも同じトーンを持ち込んだ。",[14,51,52],{},"ポイントは3つ:",[54,55,56,64,74],"ul",{},[57,58,59,63],"li",{},[60,61,62],"strong",{},"ナビ項目のハイライト",": 選択中のパネルをアクティブ表示し、現在地を視覚的に示す",[57,65,66,69,70,73],{},[60,67,68],{},"コンテンツ高さの固定",": パネルを切り替えてもコンテナのリサイズが起きない。",[18,71,72],{},"min-height"," を固定して、レイアウトシフトを殺した",[57,75,76,79,80,83],{},[60,77,78],{},"900px以上で2列表示",": ポップアップ幅が狭いときは縦積み、広ければ左右分割。",[18,81,82],{},"@media (min-width: 900px)"," で切り替え",[85,86,91],"pre",{"className":87,"code":88,"language":89,"meta":90,"style":90},"language-css shiki shiki-themes vitesse-light vitesse-light",".miller-columns {\n  display: flex;\n  flex-direction: column;\n}\n@media (min-width: 900px) {\n  .miller-columns {\n    flex-direction: row;\n  }\n  .miller-nav { width: 200px; flex-shrink: 0; }\n  .miller-content { flex: 1; min-height: 480px; }\n}\n","css","",[18,92,93,109,126,139,145,176,186,199,205,242,275],{"__ignoreMap":90},[94,95,98,102,106],"span",{"class":96,"line":97},"line",1,[94,99,101],{"class":100},"shFtX",".",[94,103,105],{"class":104},"s4oTP","miller-columns",[94,107,108],{"class":100}," {\n",[94,110,112,116,119,123],{"class":96,"line":111},2,[94,113,115],{"class":114},"sz8Xr","  display",[94,117,118],{"class":100},":",[94,120,122],{"class":121},"snbK4"," flex",[94,124,125],{"class":100},";\n",[94,127,129,132,134,137],{"class":96,"line":128},3,[94,130,131],{"class":114},"  flex-direction",[94,133,118],{"class":100},[94,135,136],{"class":121}," column",[94,138,125],{"class":100},[94,140,142],{"class":96,"line":141},4,[94,143,144],{"class":100},"}\n",[94,146,148,151,155,158,161,163,167,171,174],{"class":96,"line":147},5,[94,149,150],{"class":100},"@",[94,152,154],{"class":153},"sHkkW","media",[94,156,157],{"class":100}," (",[94,159,160],{"class":114},"min-width",[94,162,118],{"class":100},[94,164,166],{"class":165},"sM54T"," 900",[94,168,170],{"class":169},"stQ0i","px",[94,172,173],{"class":100},")",[94,175,108],{"class":100},[94,177,179,182,184],{"class":96,"line":178},6,[94,180,181],{"class":100},"  .",[94,183,105],{"class":104},[94,185,108],{"class":100},[94,187,189,192,194,197],{"class":96,"line":188},7,[94,190,191],{"class":114},"    flex-direction",[94,193,118],{"class":100},[94,195,196],{"class":121}," row",[94,198,125],{"class":100},[94,200,202],{"class":96,"line":201},8,[94,203,204],{"class":100},"  }\n",[94,206,208,210,213,216,219,221,224,226,229,232,234,237,239],{"class":96,"line":207},9,[94,209,181],{"class":100},[94,211,212],{"class":104},"miller-nav",[94,214,215],{"class":100}," {",[94,217,218],{"class":114}," width",[94,220,118],{"class":100},[94,222,223],{"class":165}," 200",[94,225,170],{"class":169},[94,227,228],{"class":100},";",[94,230,231],{"class":114}," flex-shrink",[94,233,118],{"class":100},[94,235,236],{"class":165}," 0",[94,238,228],{"class":100},[94,240,241],{"class":100}," }\n",[94,243,245,247,250,252,254,256,259,261,264,266,269,271,273],{"class":96,"line":244},10,[94,246,181],{"class":100},[94,248,249],{"class":104},"miller-content",[94,251,215],{"class":100},[94,253,122],{"class":114},[94,255,118],{"class":100},[94,257,258],{"class":165}," 1",[94,260,228],{"class":100},[94,262,263],{"class":114}," min-height",[94,265,118],{"class":100},[94,267,268],{"class":165}," 480",[94,270,170],{"class":169},[94,272,228],{"class":100},[94,274,241],{"class":100},[94,276,278],{"class":96,"line":277},11,[94,279,144],{"class":100},[14,281,282,283,286],{},"コンテンツ高さを固定したのは、パネル切替のたびにポップアップがビクビク伸縮するのを止めるため。480pxで固定して、中身がはみ出す場合は ",[18,284,285],{},"overflow-y: auto"," でスクロールさせた。",[23,288],{},[26,290,292],{"id":291},"機能統合-3画面を2パネルに減らす","機能統合: 3画面を2パネルに減らす",[14,294,295],{},"タブUI時代は「インポート」「明細取得」「設定」の3画面だった。書き直しにあたって機能を整理した。",[297,298,300],"h3",{"id":299},"エクスポートインポートを1パネルに統合","エクスポート+インポートを1パネルに統合",[14,302,303],{},"スプレッドシートインポートと明細全取得は、操作の起点が同じスプレッドシートボタン（SSボタン）だった。別画面に分ける必然性がなかったので、1つの「エクスポート/インポート」パネルに統合した。SSボタンを押すとスプレッドシートへの書き出しが走り、同じパネル内でインポート状態も確認できる。",[297,305,306],{"id":306},"サービス選択チェックボックスのチップ表示",[14,308,309],{},"会計サービスの事業者（サービス）を選ぶチェックボックスは、対象が5件以下の場合がほとんどだった。デザインシステムの「5件以下は展開表示」ルールに従い、ドロップダウンではなくチップとして全件並べた。選択状態が一目で見える。",[85,311,315],{"className":312,"code":313,"language":314,"meta":90,"style":90},"language-html shiki shiki-themes vitesse-light vitesse-light","\u003Cdiv class=\"service-chips\">\n  \u003Clabel v-for=\"svc in services\" :key=\"svc.id\" class=\"chip\">\n    \u003Cinput type=\"checkbox\" v-model=\"selectedServices\" :value=\"svc.id\" />\n    {{ svc.name }}\n  \u003C/label>\n\u003C/div>\n","html",[18,316,317,344,389,435,441,450],{"__ignoreMap":90},[94,318,319,322,325,328,331,335,339,341],{"class":96,"line":97},[94,320,321],{"class":100},"\u003C",[94,323,324],{"class":153},"div",[94,326,327],{"class":104}," class",[94,329,330],{"class":100},"=",[94,332,334],{"class":333},"sMJiu","\"",[94,336,338],{"class":337},"sdGka","service-chips",[94,340,334],{"class":333},[94,342,343],{"class":100},">\n",[94,345,346,349,352,355,357,359,362,364,367,369,371,374,376,378,380,382,385,387],{"class":96,"line":111},[94,347,348],{"class":100},"  \u003C",[94,350,351],{"class":153},"label",[94,353,354],{"class":104}," v-for",[94,356,330],{"class":100},[94,358,334],{"class":333},[94,360,361],{"class":337},"svc in services",[94,363,334],{"class":333},[94,365,366],{"class":104}," :key",[94,368,330],{"class":100},[94,370,334],{"class":333},[94,372,373],{"class":337},"svc.id",[94,375,334],{"class":333},[94,377,327],{"class":104},[94,379,330],{"class":100},[94,381,334],{"class":333},[94,383,384],{"class":337},"chip",[94,386,334],{"class":333},[94,388,343],{"class":100},[94,390,391,394,397,400,402,404,407,409,412,414,416,419,421,424,426,428,430,432],{"class":96,"line":128},[94,392,393],{"class":100},"    \u003C",[94,395,396],{"class":153},"input",[94,398,399],{"class":104}," type",[94,401,330],{"class":100},[94,403,334],{"class":333},[94,405,406],{"class":337},"checkbox",[94,408,334],{"class":333},[94,410,411],{"class":104}," v-model",[94,413,330],{"class":100},[94,415,334],{"class":333},[94,417,418],{"class":337},"selectedServices",[94,420,334],{"class":333},[94,422,423],{"class":104}," :value",[94,425,330],{"class":100},[94,427,334],{"class":333},[94,429,373],{"class":337},[94,431,334],{"class":333},[94,433,434],{"class":100}," />\n",[94,436,437],{"class":96,"line":141},[94,438,440],{"class":439},"sG7-3","    {{ svc.name }}\n",[94,442,443,446,448],{"class":96,"line":147},[94,444,445],{"class":100},"  \u003C/",[94,447,351],{"class":153},[94,449,343],{"class":100},[94,451,452,455,457],{"class":96,"line":178},[94,453,454],{"class":100},"\u003C/",[94,456,324],{"class":153},[94,458,343],{"class":100},[297,460,461],{"id":461},"設定は別パネル",[14,463,464],{},"事業者一覧テーブルは情報量が多い。全事業者 x 全年度のCTI（Content Type Identifier）とスプレッドシートURLを管理する画面なので、独立した「設定」パネルに残した。ここだけで結構な面積を使う。",[23,466],{},[26,468,470],{"id":469},"スプレッドシートurl入力の自動保存","スプレッドシートURL入力の自動保存",[14,472,473],{},"設定パネルの事業者テーブルでは、各事業者にスプレッドシートURLを紐づける。当初は全行を編集して「一括保存」ボタンを押す設計だった。",[14,475,476,477,480],{},"実際に触ると、1行変えるたびに下までスクロールして保存ボタンを押す操作がだるい。入力欄の ",[18,478,479],{},"blur"," イベントで即保存に切り替えた。一括保存ボタンは削除。",[297,482,484],{"id":483},"urlタイトルの自動取得","URLタイトルの自動取得",[14,486,487,488,491],{},"スプレッドシートURLを入力すると、",[18,489,490],{},"validateSpreadsheet"," APIを叩いてスプレッドシートのタイトルを自動取得する。URLの正当性チェックとタイトル取得を兼ねていて、無効なURLならエラーが返る。取得したタイトルはURL入力欄の下に表示して、正しいシートを指定できているか確認できるようにした。",[85,493,497],{"className":494,"code":495,"language":496,"meta":90,"style":90},"language-javascript shiki shiki-themes vitesse-light vitesse-light","const onUrlBlur = async (bizId, url) => {\n  if (!url) return\n  const { title, error } = await validateSpreadsheet(url)\n  if (error) {\n    showError(bizId, error)\n    return\n  }\n  await saveUrl(bizId, url)\n  showTitle(bizId, title)\n}\n","javascript",[18,498,499,532,550,584,597,612,617,621,639,654],{"__ignoreMap":90},[94,500,501,504,508,511,514,516,519,522,525,527,530],{"class":96,"line":97},[94,502,503],{"class":169},"const",[94,505,507],{"class":506},"senZ8"," onUrlBlur",[94,509,510],{"class":100}," =",[94,512,513],{"class":169}," async",[94,515,157],{"class":100},[94,517,518],{"class":104},"bizId",[94,520,521],{"class":100},",",[94,523,524],{"class":104}," url",[94,526,173],{"class":100},[94,528,529],{"class":100}," =>",[94,531,108],{"class":100},[94,533,534,537,539,542,545,547],{"class":96,"line":111},[94,535,536],{"class":153},"  if",[94,538,157],{"class":100},[94,540,541],{"class":169},"!",[94,543,544],{"class":104},"url",[94,546,173],{"class":100},[94,548,549],{"class":153}," return\n",[94,551,552,555,557,560,562,565,568,570,573,576,579,581],{"class":96,"line":128},[94,553,554],{"class":169},"  const",[94,556,215],{"class":100},[94,558,559],{"class":104}," title",[94,561,521],{"class":100},[94,563,564],{"class":104}," error",[94,566,567],{"class":100}," }",[94,569,510],{"class":100},[94,571,572],{"class":153}," await",[94,574,575],{"class":506}," validateSpreadsheet",[94,577,578],{"class":100},"(",[94,580,544],{"class":104},[94,582,583],{"class":100},")\n",[94,585,586,588,590,593,595],{"class":96,"line":141},[94,587,536],{"class":153},[94,589,157],{"class":100},[94,591,592],{"class":104},"error",[94,594,173],{"class":100},[94,596,108],{"class":100},[94,598,599,602,604,606,608,610],{"class":96,"line":147},[94,600,601],{"class":506},"    showError",[94,603,578],{"class":100},[94,605,518],{"class":104},[94,607,521],{"class":100},[94,609,564],{"class":104},[94,611,583],{"class":100},[94,613,614],{"class":96,"line":178},[94,615,616],{"class":153},"    return\n",[94,618,619],{"class":96,"line":188},[94,620,204],{"class":100},[94,622,623,626,629,631,633,635,637],{"class":96,"line":201},[94,624,625],{"class":153},"  await",[94,627,628],{"class":506}," saveUrl",[94,630,578],{"class":100},[94,632,518],{"class":104},[94,634,521],{"class":100},[94,636,524],{"class":104},[94,638,583],{"class":100},[94,640,641,644,646,648,650,652],{"class":96,"line":207},[94,642,643],{"class":506},"  showTitle",[94,645,578],{"class":100},[94,647,518],{"class":104},[94,649,521],{"class":100},[94,651,559],{"class":104},[94,653,583],{"class":100},[94,655,656],{"class":96,"line":244},[94,657,144],{"class":100},[23,659],{},[26,661,663],{"id":662},"oauthクライアントidの差し替え","OAuthクライアントIDの差し替え",[14,665,666,667,672,673,676],{},"前日の作業（",[668,669,671],"a",{"href":670},"/chrome-extension-sheets-api","Chrome拡張にSheets API連携を追加した記録","）で設定したOAuthクライアントIDが、翌日になって ",[18,674,675],{},"bad client id"," エラーを返し始めた。Google Cloud Consoleで確認すると、古いクライアントIDが残ったまま新しいIDも並存している状態だった。",[14,678,679,680,683],{},"manifest.jsonのoauth2セクションで古いIDを新しいIDに差し替え、",[18,681,682],{},"chrome.identity.clearAllCachedAuthTokens()"," でトークンキャッシュを掃除して解決。IDが2つ存在していたこと自体は問題ないが、manifest.jsonが古い方を参照していたのが原因だった。",[23,685],{},[26,687,688],{"id":688},"マージとプッシュ",[14,690,691,694,695,698],{},[18,692,693],{},"drive_-review"," ブランチで作業していた全変更を ",[18,696,697],{},"master"," にマージしてプッシュした。コンフリクトなし。ブランチ名にハイフンが2つ入っているのは命名ミスだが、作業ブランチなのでそのまま通した。",[23,700],{},[26,702,703],{"id":703},"振り返り",[14,705,706],{},"タブUIからミラーカラムズへの書き直しは、コードの半分以上を捨てることになった。ただ、既存のデザインシステムを参照しながら組んだおかげで、UIコンポーネントの設計判断で迷う場面は少なかった。「5件以下は展開表示」「コンテンツ高さ固定」のようなルールが先にあると、実装中に手が止まらない。",[14,708,709],{},"一括保存を即時保存に変えたのは、自分で5回触って「毎回スクロールするのが面倒」と気づいたから。UIの善し悪しは、実際に繰り返し操作してみないと見えない。",[711,712,713],"style",{},"html pre.shiki code .shFtX, html code.shiki .shFtX{--shiki-default:#999999;--shiki-dark:#999999}html pre.shiki code .s4oTP, html code.shiki .s4oTP{--shiki-default:#B07D48;--shiki-dark:#B07D48}html pre.shiki code .sz8Xr, html code.shiki .sz8Xr{--shiki-default:#998418;--shiki-dark:#998418}html pre.shiki code .snbK4, html code.shiki .snbK4{--shiki-default:#A65E2B;--shiki-dark:#A65E2B}html pre.shiki code .sHkkW, html code.shiki .sHkkW{--shiki-default:#1E754F;--shiki-dark:#1E754F}html pre.shiki code .sM54T, html code.shiki .sM54T{--shiki-default:#2F798A;--shiki-dark:#2F798A}html pre.shiki code .stQ0i, html code.shiki .stQ0i{--shiki-default:#AB5959;--shiki-dark:#AB5959}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sMJiu, html code.shiki .sMJiu{--shiki-default:#B5695977;--shiki-dark:#B5695977}html pre.shiki code .sdGka, html code.shiki .sdGka{--shiki-default:#B56959;--shiki-dark:#B56959}html pre.shiki code .sG7-3, html code.shiki .sG7-3{--shiki-default:#393A34;--shiki-dark:#393A34}html pre.shiki code .senZ8, html code.shiki .senZ8{--shiki-default:#59873A;--shiki-dark:#59873A}",{"title":90,"searchDepth":111,"depth":111,"links":715},[716,717,718,723,726,727,728],{"id":28,"depth":111,"text":29},{"id":43,"depth":111,"text":43},{"id":291,"depth":111,"text":292,"children":719},[720,721,722],{"id":299,"depth":128,"text":300},{"id":306,"depth":128,"text":306},{"id":461,"depth":128,"text":461},{"id":469,"depth":111,"text":470,"children":724},[725],{"id":483,"depth":128,"text":484},{"id":662,"depth":111,"text":663},{"id":688,"depth":111,"text":688},{"id":703,"depth":111,"text":703},"dev","会計サービス連携Chrome拡張のUIを、タブ切替からミラーカラムズ（左ナビ+右コンテンツの2カラム構成）にリファクタリング。デザインシステム準拠、機能統合、レスポンシブ対応、URL自動保存まで","md",{},true,null,"/mf-miller-columns-ui","misc-dev",false,"2026-03-23T00:00:00.000Z",{"title":5,"description":730},"2026-03/2026-03-23/mf-miller-columns-ui",[742,743,744,745,746],"Chrome拡張機能","UI設計","ミラーカラムズ","クラウド会計","デザインシステム","memo","oCRl5H54fFOM0UOSEGbk7oFdLh13be6YLoQO-kVrdOM",[],"https://log.eurekapu.com/og/blog/mf-miller-columns-ui.png?v=2026-03-23T00%3A00%3A00.000Z&title=Chrome%E6%8B%A1%E5%BC%B5%E3%81%AE%E4%BC%9A%E8%A8%88%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E9%80%A3%E6%90%BAUI%E3%82%92%E3%82%BF%E3%83%96%E3%81%8B%E3%82%89%E3%83%9F%E3%83%A9%E3%83%BC%E3%82%AB%E3%83%A9%E3%83%A0%E3%82%BA%E3%81%AB%E6%9B%B8%E3%81%8D%E7%9B%B4%E3%81%97%E3%81%9F%E8%A8%98%E9%8C%B2&author=Kei%20Komatsu&sig=2a6250b41475ee37",1782528820292]