diff --git a/docs/development/ui-ux/design-language.md b/docs/development/ui-ux/design-language.md index d8fe562c..0a039ce2 100644 --- a/docs/development/ui-ux/design-language.md +++ b/docs/development/ui-ux/design-language.md @@ -2,80 +2,48 @@ この文書は、Workspace Webをどう知覚させ、情報、状態、操作をどの視覚文法で表現するかを定義する。Product固有のresource構成は[`product-ux.md`](product-ux.md)、application shellとSidebar slotの実装構造は[`application-architecture.md`](application-architecture.md)、CSSとsource配置は[`../../../web/workspace/README.md`](../../../web/workspace/README.md)をauthorityとする。 -一つの規則は、次の4階層のうち最も具体的な一箇所だけをauthorityとする。別の章で同じ禁止や手段を再掲せず、必要な場合はauthorityとなる章を参照する。 +一つの規則は、以下の各章のうち最も具体的な一箇所だけをauthorityとする。別の章で同じ禁止や手段を再掲せず、必要な場合はauthorityとなる章を参照する。 + +この文書はrendered resultが満たす設計規則を定義する。実装者自身がbefore/afterをcaptureして完了判定する手順は、[`visual-review.md`](visual-review.md)をauthorityとする。 ## 1. Principles -なぜこの設計にするかを定義する。個別の表示規則に迷った場合は、この章へ戻って判断する。 +この章は具体的な見た目や配置を定めない。複数の規則や要求が競合したときに、どちらを優先するか判断するための原理だけを定める。 -### 基本思想 +### Task before explanation -- 作業ツールとして、シンプルで直感的なUX -- UIのために曖昧な情報を推測しない。データを正直に表示し、操作を見通せるようにする -- 状態・scope・操作を、装飾ではなく構造で読ませ、情報量を増やしても騒がしくならない -- 説明的に見せるのではなく、配置と関係で理解させる +現在のtaskと判断を、網羅的な説明、metadataの露出、装飾より優先する。UIはユーザーが次の有効な操作へ到達するまでの距離を短くする。 -### 知覚原則 +### Meaning before decoration -size、weight、color、spacing、line、background、motionを使うときは、次のどの知覚を成立させるためか説明できなければならない。説明できない視覚表現は追加しない。 +視覚上の差は、情報の重要度、関係、操作可能性、または状態の違いを伝えるためにだけ使う。意味を説明できない視覚表現は追加しない。 -#### Hierarchy — 何が重要か +### Authority before convenience -- 情報は、primary、secondary、technicalの順に読む構造にする。 -- 重要度は、最初に配置順、次にsize、weight、contrastで示す。colorや装飾だけで重要度を作らない。 -- 同じscopeに最も強いheading、filled action、attention表現を複数置かない。 -- primary contentと、その判断に必要なstateまたはactionを初期viewportへ置く。 -- metadataと補助操作は、主要なidentityやcurrent stateより強く見せない。 +UIの都合でdata、state、permission、利用可能なactionを推測しない。表示とaffordanceは、現在のauthorityが確定している内容に一致させる。 -#### Grouping — 何と何が同じグループか +### Complexity follows need -- 同じ判断や操作に使う情報は、一つのまとまりとして知覚できるようにする。 -- groupの単位は、共通のownership、behavior、またはユーザーが行う一つの判断から決める。 -- label、value、help、error、actionの対応関係を、別の説明を読まなくても追えるようにする。 -- data sourceやDTOが同じという理由だけでgroup化しない。 -- 一つの関係へ複数の視覚cueを重ねず、何がgroupを成立させているか一つに定める。 - -#### Separation — 何が別物か - -- 異なるgroupの境界を知覚でき、その強さが意味上の距離と一致するようにする。 -- すべての境界について、何と何を分けているか説明できなければならない。 -- 一つの意味上の境界へ複数のseparation cueを重ねない。 -- 境界がどのgroupの開始または終端を示すか、読み順から判別できるようにする。 -- 異なるhierarchyの境界をすべて同じ強さで表現しない。 - -#### Affordance — 何が操作できるか - -- actionにはbutton、navigationにはlink、選択にはcontrolを使い、役割を見た目とsemantic elementで一致させる。 -- interactive elementは、通常、hover、focus、active、disabledを区別できるようにする。 -- staticなstatus、key、metadataをbuttonやpillの形にしない。 -- clickable rowは、click可能な範囲と遷移先を一つにする。 -- permissionがない操作をdisabled controlとして見せることでaffordanceを偽らない。 - -#### State — 現在どういう状態か - -- 権威を持つdata sourceのstateをそのまま表示し、UIの都合でunknown、stale、unavailableを推測で埋めない。 -- stateは対象resourceまたはoperationの近くに置き、何のstateかを位置関係で判別できるようにする。 -- stateはmarkerとtextで示し、colorを補助に使う。 -- loading、empty、error、permission denied、unavailable、staleを別の状態として扱う。 -- 非同期operationでは、開始、処理中、完了、失敗を見通せるようにする。 - -#### Focus — 今どこを見るべきか - -- 一つのscopeでは、primary resource、current attention、next actionのいずれか一つを最初のfocusにする。 -- accent、filled action、強いcontrast、motionを複数箇所で競合させない。 -- normal stateは静かに保ち、attention表現は現在判断が必要な例外へ限定する。 -- 視覚的なfocusとkeyboard focusを一致させる。 -- focusを作るために周囲の情報を過度に薄くしたり、小さくしたりしない。 - -### 検証原則 - -この文書はrendered resultが満たす設計規則を定義する。実装者自身がbefore/afterをcaptureし、grouping、separator、spacing、wrapping、responsive、stateを目視して完了判定する手順は、[`visual-review.md`](visual-review.md)をauthorityとする。 +複雑さを消すのではなく、判断に必要になる位置と時点で開示する。初期表示では、現在のtaskと競合する詳細を前面へ出さない。 ## 2. Semantics / Grammar -何を、どの関係と意味で表現するかを定義する。 +この章は、Workspace Webで情報、操作、状態を何として扱い、どう表現するかを定める。 -### 情報階層 +### Information structure + +#### Priority and focus + +- 情報はprimary、secondary、technicalの順に読む構造にする。 +- 一つのscopeでは、primary resource、current attention、next actionのいずれか一つを最初のfocusにする。 +- 重要度は最初に配置順、次にsize、weight、contrastで示す。colorや装飾だけで重要度を作らない。 +- 同じscopeに最も強いheading、filled action、attention表現を複数置かない。 +- metadataと補助操作は、主要なidentityやcurrent stateより強く見せない。 +- accent、filled action、強いcontrast、motionを複数箇所で競合させない。 +- normal stateは静かに保ち、attention表現は現在判断が必要な例外へ限定する。 +- focusを作るために周囲の情報を過度に薄くしたり、小さくしたりしない。 + +#### Page and section structure route titleはshell headerとmain contentを通じて一度だけ表示する。 @@ -85,9 +53,18 @@ route titleはshell headerとmain contentを通じて一度だけ表示する。 - route分類、breadcrumb、header titleをeyebrow、heading、keyで繰り返さない。 - subtitleとledeは原則として置かない。 -一つのsectionは、一つの目的、ownership、または読み順を持つ。目的が異なる内容を同じsectionへ入れず、同じ目的の内容を見た目だけで複数sectionへ分けない。Section間とsection内の関係は`Principles`のGroupingとSeparationに従い、具体的なcueは`Primitives / Tokens`だけから選ぶ。 +一つのsectionは、一つの目的、ownership、または読み順を持つ。目的が異なる内容を同じsectionへ入れず、同じ目的の内容を見た目だけで複数sectionへ分けない。Section間とsection内の関係は次のGrouping and separationに従い、具体的なcueは`Primitives / Tokens`だけから選ぶ。 -### Metadata +#### Grouping and separation + +- 同じ判断や操作に使う情報は、一つのまとまりとして知覚できるようにする。 +- groupの単位は、共通のownership、behavior、またはユーザーが行う一つの判断から決める。data sourceやDTOが同じという理由だけでgroup化しない。 +- label、value、help、error、actionの対応関係を、別の説明を読まなくても追えるようにする。 +- 異なるgroupの境界を知覚でき、その強さが意味上の距離と一致するようにする。 +- 境界は何と何を分け、どのgroupの開始または終端を示すか説明できるようにする。 +- 一つのgroupまたは境界へ複数の視覚cueを重ねず、異なるhierarchyの境界をすべて同じ強さで表現しない。 + +#### Metadata 主要表示に置くのは、identity、healthまたはattention、比較、次の操作に必要な値だけとする。 @@ -102,7 +79,7 @@ route titleはshell headerとmain contentを通じて一度だけ表示する。 機械的な値にはmonospace familyを使う。DTOに値が存在することだけを理由に表示しない。 -### 段階的な開示 +#### 段階的な開示 - primary viewにはidentity、current state、attention、次の有効なactionを表示する。 - 副次的な運用情報はplain section、side panel、またはlabel付き`details`へ置く。 @@ -111,7 +88,25 @@ route titleはshell headerとmain contentを通じて一度だけ表示する。 - create formとedit formは明示的なactionの後に表示する。 - blocking error、permission requirement、validation constraint、unsaved changeを閉じたdisclosureへ隠さない。 -### Authority、権限、操作 +### Interaction and state + +#### Affordance + +- actionにはbutton、navigationにはlink、選択にはcontrolを使い、役割を見た目とsemantic elementで一致させる。 +- interactive elementは、通常、hover、focus、active、disabledを区別できるようにする。 +- staticなstatus、key、metadataをbuttonやpillの形にしない。 +- clickable rowは、click可能な範囲と遷移先を一つにする。 +- permissionがない操作をdisabled controlとして見せることでaffordanceを偽らない。 + +#### State representation + +- 権威を持つdata sourceのstateをそのまま表示し、UIの都合でunknown、stale、unavailableを推測で埋めない。 +- stateは対象resourceまたはoperationの近くに置き、何のstateかを位置関係で判別できるようにする。 +- stateはmarkerとtextで示し、colorを補助に使う。 +- loading、empty、error、permission denied、unavailable、staleを別の状態として扱う。 +- 非同期operationでは、開始、処理中、完了、失敗を見通せるようにする。 + +#### Authority、権限、操作 - permissionはcontrolのenabled状態だけでなく、表示構成そのものへ反映する。 - readは可能だがmutationできない場合、有用なread viewだけを構成する。 @@ -121,7 +116,7 @@ route titleはshell headerとmain contentを通じて一度だけ表示する。 - destructive actionには対象と結果を明記し、復元できない場合は意図的なconfirmationを要求する。 - 同じscopeのfilled primary actionは一つだけとする。 -### Loading、empty、error、unavailable +loading、empty、error、unavailableは同じfallbackへまとめず、それぞれ次の表現を使う。 #### Loading @@ -148,7 +143,9 @@ route titleはshell headerとmain contentを通じて一度だけ表示する。 resource unavailable、capability missing、permission deniedを別の状態として扱う。`403`をempty listとして表示したり、別scopeへredirectしたりしない。 -### 文言 +## 3. Language / Copy + +UI copyの語彙、長さ、説明責任を定める。 - 具体的な名詞と動詞を使う。 - identifier、API名、type名は正確性が必要な場合に原文を維持する。 @@ -157,6 +154,10 @@ resource unavailable、capability missing、permission deniedを別の状態と - empty copyとstatus copyは短く、事実を直接書く。 - buttonは操作を表す。`Submit`、`OK`、装飾的なcategory labelを避ける。 +## 4. Cross-cutting requirements + +viewport、input modality、支援技術にかかわらず、すべての表現とcomponentが満たす要件を定める。 + ### Responsive - page全体にhorizontal scrollを発生させない。 @@ -171,14 +172,14 @@ resource unavailable、capability missing、permission deniedを別の状態と - semantic elementを使う。 - すべてのcontrolに明示的なaccessible nameを与える。 -- keyboard focusを常に見えるようにする。 +- 視覚的なfocusとkeyboard focusを一致させ、常に見えるようにする。 - tab orderを画面上の順序と作業順に合わせる。 - icon-only controlには具体的な`aria-label`を付ける。 - stateはcolor以外のtextまたはsemantic distinctionを持つ。 - 必要なhorizontal scroll regionはfocus可能にし、accessible nameを付ける。 - lightとdarkで同じsemantic tokenを使う。 -## 3. Primitives / Tokens +## 5. Primitives / Tokens 色、文字、間隔、形、motionの最小単位を定義する。 @@ -204,9 +205,6 @@ colorはCSS custom propertyをauthorityとし、通常のWorkspace colorはOKLCH --danger --interactive-hover --interactive-selected ---bevel-highlight ---bevel-shadow ---bevel-face-width --shadow-overlay ``` @@ -214,11 +212,6 @@ colorはCSS custom propertyをauthorityとし、通常のWorkspace colorはOKLCH - muted textは新しいhueを増やす前にlightnessとchromaを下げる。 - accentとstatus colorはstate、focus、navigationへ限定する。 - componentへraw colorを追加せず、semantic tokenを使う。 -- `--bevel-highlight`と`--bevel-shadow`はBevel共通の照明endpointとし、componentのbackgroundから派生させない。Bevel wrapper、`profile`、`depth`、描画辺へsurface色を与えてはならず、Bevelはedge lightingだけを所有する。背景色を変更できるのは内側のsemantic childが所有するtext/content areaだけとする。 -- Bevelの一面の幅はproject-wideな`--bevel-face-width: 2px`へ固定し、component APIから変更させない。`edge`は一面2px、`ridge`と`BevelLine`は二面を重ねた合計4pxとする。 -- Bevelのgeometryは`profile = edge | ridge`、凹凸方向は`depth = raised | inset`として直交させる。`ridge + inset`はgrooveを表す。Lineも`depth = raised | inset`を使う。 -- Box Bevelは`top`、`right`、`bottom`、`left`で描画辺を個別に選べる。defaultは全辺有効とし、角丸はその角に隣接する2辺がともに有効な場合だけ描画する。ridgeの内側面も有効辺からだけinsetする。 -- 独立した構造separatorは`BevelLine`を使い、片側のsolid borderで代用しない。外周border、focus ring、status marker、表の意味的なgridは別のprimitiveとして扱う。 - 意味が重なるtoken aliasを作らない。 ConsoleとterminalのANSI paletteは専用tokenを使い、通常のstatusやformへ流用しない。 @@ -262,15 +255,29 @@ ConsoleとterminalのANSI paletteは専用tokenを使い、通常のstatusやfor - motionは任意かつ短くし、意味の必須条件にしない。 - 繰り返しまたは連続animationではreduced-motion preferenceを尊重する。 -## 4. Components / Patterns +## 6. Components / Patterns -実際のUIを、上のprinciple、grammar、tokenから組み立てる。完成pageを固定templateにしない。 +実際のUIを、上のprinciple、grammar、communication、cross-cutting requirement、tokenから組み立てる。完成pageを固定templateにしない。 + +### Bevel / Structural edge + +- raised edgeは、周囲より前にあるcontrolまたはsurfaceを表す場合に使う。 +- inset edgeは、入力を受け取る領域または周囲より奥にあるsurfaceを表す場合に使う。 +- ridgeとgrooveは構造上の境界を表すために使い、status、attention、単なる装飾には使わない。 +- spacingやalignmentだけで関係を十分に示せる場合、Bevelを追加しない。 +- Bevel自体へbackgroundやsurface toneを与えず、必要なbackgroundは内側のsemantic childが持つtext/content areaへ適用する。 +- 隣接するsurfaceでは、実際に境界となる辺だけを見せ、存在しない境界やcornerを装飾目的で追加しない。 +- standaloneな閉じた領域は、領域自体をBevelで囲う。ページ全体を分割するHeaderとSidebarはclosed boxとして隣接させず、DesktopではHeaderのbottom edgeとSidebarのright edgeだけを有効にして内部境界を示す。viewport外周に重なるtop/left edgeは描画しない。 +- `BevelLine`は開いた領域内の構造separatorだけに使う。同じlayerではheading、本文、Lineの端を揃え、Lineだけに端方向のpaddingやmarginを追加しない。 +- 内側のlayerを分離するときは、親layoutがそのlayer全体へinline方向の余白を与え、content、row、Lineを一緒に内側へ移す。Lineだけを短くして階層を表現しない。 +- 外周border、focus ring、status marker、tableの意味的なgridはBevelや`BevelLine`へ置き換えない。 ### Sidebar - Sidebarは現在位置と利用可能なscopeを一つのnavigation hierarchyとして示す。 - desktop幅は`clamp(220px, 20vw, 280px)`を基本とする。 -- navigation contentだけをscrollさせ、fold controlはframe下部に残す。 +- navigation contentだけをscrollさせる。Desktopのfold controlはSidebar下部、Mobileのfold controlはHeaderに置く。 +- MobileでSidebarを表示するときはHeader下の残りviewport全体をSidebarに使い、main contentと同時表示しない。 - navigationとscope controlだけを置く。 - navigation linkはsidebar幅全体を使うflat rowとする。 - active stateは一つのcueと`aria-current="page"`で示す。 diff --git a/web/workspace/src/app.css b/web/workspace/src/app.css index 411e6b9b..b64a73df 100644 --- a/web/workspace/src/app.css +++ b/web/workspace/src/app.css @@ -51,7 +51,7 @@ --interactive-selected: oklch(93% 0.02 230); --bevel-highlight: #fff; --bevel-shadow: #000; - --bevel-face-width: 2px; + --bevel-face-width: 1px; --shadow-overlay: 0 12px 32px oklch(0% 0 0 / 0.14); --font-sans: "Gen Interface JP", sans-serif; diff --git a/web/workspace/src/lib/workspace/sidebar/SidebarFrame.svelte b/web/workspace/src/lib/workspace/sidebar/SidebarFrame.svelte index 4a4902d4..668af15b 100644 --- a/web/workspace/src/lib/workspace/sidebar/SidebarFrame.svelte +++ b/web/workspace/src/lib/workspace/sidebar/SidebarFrame.svelte @@ -1,60 +1,51 @@ - + + + + diff --git a/web/workspace/src/lib/workspace/sidebar/sidebar.css b/web/workspace/src/lib/workspace/sidebar/sidebar.css index b20aaf7f..075f83f9 100644 --- a/web/workspace/src/lib/workspace/sidebar/sidebar.css +++ b/web/workspace/src/lib/workspace/sidebar/sidebar.css @@ -5,31 +5,28 @@ --sidebar-item-hover: oklch(96% 0 0); --sidebar-item-active: oklch(92.5% 0 0); + display: grid; grid-column: 1; grid-row: 1 / 3; - position: relative; - display: flex; - flex-direction: column; width: clamp(220px, 20vw, 280px); min-width: 0; min-height: 0; overflow: hidden; + } + .sidebar-frame .bevel.sidebar-frame__bevel { + width: 100%; + min-width: 0; + min-height: 0; + } + .sidebar-frame__surface { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + min-width: 0; + min-height: 0; padding-block: var(--space-4); } - .sidebar-frame__divider { - position: absolute; - z-index: 2; - pointer-events: none; - } - .sidebar-frame__divider--vertical { - inset-block: 0; - inset-inline-end: 0; - } - .sidebar-frame__divider--horizontal { - display: none; - inset-inline: 0; - inset-block-end: 0; - } @media (prefers-color-scheme: dark) { .sidebar-frame { --sidebar-item-hover: oklch(24% 0 0); @@ -39,7 +36,6 @@ .sidebar-frame.folded { width: max-content; overflow: hidden; - padding-inline: 0; } .sidebar-frame-content { flex: 1 1 auto; @@ -628,17 +624,18 @@ } @media (max-width: 760px) { - .sidebar-frame, - .sidebar-frame.folded { + .sidebar-frame { + z-index: 20; grid-column: 1; - grid-row: 1; - width: auto; + grid-row: 2; + width: 100%; + height: 100%; } - .sidebar-frame__divider--vertical { + .sidebar-frame.folded { display: none; } - .sidebar-frame__divider--horizontal { - display: block; + .sidebar-control-row { + display: none; } } } diff --git a/web/workspace/src/lib/workspace/ui/Bevel.svelte b/web/workspace/src/lib/workspace/ui/Bevel.svelte index 57eed535..d71ca612 100644 --- a/web/workspace/src/lib/workspace/ui/Bevel.svelte +++ b/web/workspace/src/lib/workspace/ui/Bevel.svelte @@ -128,7 +128,7 @@ inset: 0; box-sizing: border-box; border-style: solid; - border-color: var(--line-strong); + border-color: var(--_fallback-border, var(--line-strong)); border-width: var(--_top-w) var(--_right-w) var(--_bottom-w) var(--_left-w); border-radius: inherit; pointer-events: none; @@ -155,11 +155,11 @@ } .bevel[data-profile='edge'][data-depth='inset']::before { - border-color: var(--line); + --_fallback-border: var(--line); } .bevel[data-profile='ridge'][data-depth='inset']::before { - border-color: var(--line); + --_fallback-border: var(--line); } .bevel[data-profile='ridge']::after { @@ -168,7 +168,7 @@ var(--bevel-right-inset) var(--bevel-bottom-inset) var(--bevel-left-inset); - border-color: var(--line); + --_fallback-border: var(--line); border-radius: max(0px, calc(var(--bevel-top-left-radius) - var(--bevel-face-width))) max(0px, calc(var(--bevel-top-right-radius) - var(--bevel-face-width))) @@ -177,7 +177,7 @@ } .bevel[data-profile='ridge'][data-depth='inset']::after { - border-color: var(--line-strong); + --_fallback-border: var(--line-strong); } .bevel[data-invalid='true'], diff --git a/web/workspace/src/routes/+layout.svelte b/web/workspace/src/routes/+layout.svelte index 4fc555df..5df3f149 100644 --- a/web/workspace/src/routes/+layout.svelte +++ b/web/workspace/src/routes/+layout.svelte @@ -2,7 +2,7 @@ import { page } from '$app/state'; import { setContext } from 'svelte'; import WorkspaceAlerts from '$lib/workspace/alerts/WorkspaceAlerts.svelte'; - import BevelLine from '$lib/workspace/ui/BevelLine.svelte'; + import Bevel from '$lib/workspace/ui/Bevel.svelte'; import { provideHeaderController, type HeaderController } from '$lib/workspace/header/context'; import GlobalSidebar from '$lib/workspace/sidebar/GlobalSidebar.svelte'; import SidebarFrame from '$lib/workspace/sidebar/SidebarFrame.svelte'; @@ -17,6 +17,11 @@ sidebar = activeSidebar; }); const headerController = $state({ content: null }); + let sidebarFolded = $state(false); + + function toggleSidebar() { + sidebarFolded = !sidebarFolded; + } provideHeaderController(headerController); setContext(SIDEBAR_CONTEXT, { @@ -26,8 +31,8 @@ -
- +
+ -
-
- {#if headerController.content}{@render headerController.content()}{/if} -
- - -
+ +
+
+ {#if headerController.content}{@render headerController.content()}{/if} +
+ +
+
{@render children()}
@@ -72,11 +91,14 @@ min-width: 0; } - .app-shell__topbar { - position: relative; + .app-shell :global(.bevel.app-shell__topbar-bevel) { z-index: 30; grid-column: 2; grid-row: 1; + min-width: 0; + } + + .app-shell__topbar { display: flex; align-items: center; justify-content: space-between; @@ -88,12 +110,6 @@ backdrop-filter: blur(14px); } - :global(.app-shell__topbar-divider) { - position: absolute; - inset-inline: 0; - inset-block-end: 0; - } - .app-shell__topbar-location { flex: 1 1 auto; min-width: 0; @@ -123,6 +139,14 @@ color: var(--text-muted); } + .app-shell__mobile-sidebar-toggle { + display: none; + padding: 0; + border: 0; + background: transparent; + cursor: pointer; + } + .app-shell__icon { width: 1.1rem; height: 1.1rem; @@ -150,25 +174,36 @@ @media (max-width: 760px) { .app-shell { - grid-template-columns: 1fr; - grid-template-rows: auto auto 1fr; + grid-template-columns: minmax(0, 1fr); + grid-template-rows: auto minmax(0, 1fr); width: 100vw; - height: auto; - min-height: 100dvh; - overflow: visible; + height: 100dvh; + min-height: 0; + overflow: hidden; + } + + .app-shell :global(.bevel.app-shell__topbar-bevel) { + grid-column: 1; + grid-row: 1; } .app-shell__topbar { - grid-column: 1; - grid-row: 2; padding: 0 var(--space-4); } + .app-shell__mobile-sidebar-toggle { + display: inline-flex; + } + .app-shell__main { grid-column: 1; - grid-row: 3; - overflow: visible; + grid-row: 2; + overflow-y: auto; padding: var(--space-5) var(--space-4); } + + .app-shell.sidebar-open .app-shell__main { + display: none; + } } diff --git a/web/workspace/src/routes/design-lab/workspace-web-ux/showroom.css b/web/workspace/src/routes/design-lab/workspace-web-ux/showroom.css index 2f3c44d9..8698fe02 100644 --- a/web/workspace/src/routes/design-lab/workspace-web-ux/showroom.css +++ b/web/workspace/src/routes/design-lab/workspace-web-ux/showroom.css @@ -167,7 +167,6 @@ gap: var(--space-4); padding: var(--space-3) var(--space-4); border-radius: inherit; - background: var(--bg-raised); color: var(--text); } @@ -297,6 +296,7 @@ .resource-list { min-width: 0; + margin-inline: var(--space-2); } .resource-row { @@ -409,7 +409,7 @@ .key-value, .technical-values { - margin: 0; + margin: 0 var(--space-2); } .key-value > div:not(.bevel-line), @@ -553,6 +553,7 @@ .sample-form { display: grid; gap: var(--space-5); + margin-inline: var(--space-2); } .form-grid { diff --git a/web/workspace/tests/bevel-contract.test.ts b/web/workspace/tests/bevel-contract.test.ts index 29648d45..8a2f884b 100644 --- a/web/workspace/tests/bevel-contract.test.ts +++ b/web/workspace/tests/bevel-contract.test.ts @@ -39,10 +39,15 @@ Deno.test("Bevel keeps depth visual while semantic controls remain children", as assert( source.includes("@supports") && source.includes("border-style: solid") && + source.includes( + "border-color: var(--_fallback-border, var(--line-strong))", + ) && + source.includes("--_fallback-border: var(--line)") && + source.includes("--_fallback-border: var(--line-strong)") && source.includes( "border-width: var(--_top-w) var(--_right-w) var(--_bottom-w) var(--_left-w)", ), - "Bevel must retain a side-selective plain border fallback around the enhanced edge", + "Bevel fallback colors must not outrank the enhanced transparent border", ); assert( source.includes("calc(max(cos(180deg - var(--_L)), 0) * 100%)") && @@ -52,12 +57,12 @@ Deno.test("Bevel keeps depth visual while semantic controls remain children", as "Bevel must clamp unlit faces to the configured shadow without ambient light", ); assert( - appCss.includes("--bevel-face-width: 2px") && + appCss.includes("--bevel-face-width: 1px") && source.includes("--_W: var(--bevel-face-width)") && source.includes("inset: var(--bevel-face-width)") && !source.includes("BevelSize") && !source.includes("data-size"), - "Bevel must use the project-wide 2px edge and compose ridge from two 2px faces", + "Bevel must use the project-wide 1px edge and compose ridge from two 1px faces", ); assert( source.includes("data-top={top ? 'true' : 'false'}") && @@ -94,9 +99,8 @@ Deno.test("Bevel keeps depth visual while semantic controls remain children", as "Bevel must use the shared highlight and shadow palette without owning a surface tone", ); assert( - designLanguage.includes("Bevelはedge") && - designLanguage.includes("lightingだけを所有する") && - designLanguage.includes("text/content areaだけとする"), + designLanguage.includes("Bevel自体へbackgroundやsurface toneを与えず") && + designLanguage.includes("text/content areaへ適用する"), "The design language must prohibit Bevel surface colors outside the child content area", ); assert( @@ -118,6 +122,15 @@ Deno.test("Workspace showroom exercises raised inset and ridge edges", async () import.meta.url, ), ); + const showroomCss = await Deno.readTextFile( + new URL( + "../src/routes/design-lab/workspace-web-ux/showroom.css", + import.meta.url, + ), + ); + const tactileGroupRule = showroomCss.match( + /\.tactile-group \{([\s\S]*?)\n \}/, + )?.[1]; assert( workspaceSource.includes('profile="edge" depth="raised"') && @@ -128,6 +141,10 @@ Deno.test("Workspace showroom exercises raised inset and ridge edges", async () workspaceSource.includes("pressed"), "Workspace showroom must demonstrate edge and ridge depths with full and adjacent side selections", ); + assert( + tactileGroupRule && !tactileGroupRule.includes("background"), + "Showroom ridge items must leave their content area background unset", + ); assert( settingsSource.includes('profile="edge" depth="inset"') && settingsSource.includes('aria-invalid="true"'), diff --git a/web/workspace/tests/bevel-line-contract.test.ts b/web/workspace/tests/bevel-line-contract.test.ts index 3a775f32..59e500e2 100644 --- a/web/workspace/tests/bevel-line-contract.test.ts +++ b/web/workspace/tests/bevel-line-contract.test.ts @@ -28,14 +28,20 @@ Deno.test("BevelLine exposes a semantic directional separator", async () => { } assert( - appCss.includes("--bevel-face-width: 2px") && + appCss.includes("--bevel-face-width: 1px") && source.includes("--bevel-line-face-width: var(--bevel-face-width)") && source.includes( "--bevel-line-width: calc(var(--bevel-line-face-width) * 2)", ) && !source.includes("BevelLineWeight") && !source.includes("data-weight"), - "BevelLine must compose a fixed 4px ridge from two project-wide 2px faces", + "BevelLine must compose a fixed 2px ridge from two project-wide 1px faces", + ); + assert( + !source.includes("--bevel-line-inset") && + !source.includes("padding-inline") && + !source.includes("padding-block"), + "BevelLine must fill its parent layer without independent endpoint spacing", ); assert( source.includes("background: var(--line-strong)") && @@ -76,7 +82,7 @@ Deno.test("Workspace showroom demonstrates both BevelLine directions", async () ); }); -Deno.test("Workspace shell and showroom use BevelLine for structural separators", async () => { +Deno.test("Shell regions use single-edge Bevel while open separators use BevelLine", async () => { const rootLayout = await Deno.readTextFile( new URL("../src/routes/+layout.svelte", import.meta.url), ); @@ -95,18 +101,50 @@ Deno.test("Workspace shell and showroom use BevelLine for structural separators" import.meta.url, ), ); + const designLanguage = await Deno.readTextFile( + new URL( + "../../../docs/development/ui-ux/design-language.md", + import.meta.url, + ), + ); assert( - rootLayout.includes("app-shell__topbar-divider") && - sidebarFrame.includes("sidebar-frame__divider--vertical") && - sidebarFrame.includes("sidebar-frame__divider--horizontal"), - "Workspace shell must render structural dividers through BevelLine", + rootLayout.includes('class="app-shell__topbar-bevel"') && + rootLayout.includes('depth="inset"') && + rootLayout.includes("top={false} right={false} left={false}") && + rootLayout.includes("bind:folded={sidebarFolded}") && + rootLayout.includes("app-shell__mobile-sidebar-toggle") && + rootLayout.includes("class:sidebar-open={!sidebarFolded}") && + !rootLayout.includes("BevelLine") && + sidebarFrame.includes('class="sidebar-frame__bevel"') && + sidebarFrame.includes('depth="inset"') && + sidebarFrame.includes("top={false} bottom={false} left={false}") && + sidebarFrame.includes("$bindable(false)") && + !sidebarFrame.includes("BevelLine") && + sidebarCss.includes(".sidebar-frame.folded") && + sidebarCss.includes("display: none") && + sidebarCss.includes("display: grid"), + "Desktop shell boundaries must use one Bevel edge and Mobile must share Header fold state", + ); + assert( + designLanguage.includes( + "Headerのbottom edgeとSidebarのright edgeだけを有効", + ) && + designLanguage.includes("Mobileのfold controlはHeaderに置く") && + designLanguage.includes("main contentと同時表示しない") && + designLanguage.includes("同じlayerではheading、本文、Lineの端を揃え") && + designLanguage.includes( + "親layoutがそのlayer全体へinline方向の余白を与え", + ) && + showroomCss.includes(".resource-list {") && + showroomCss.includes("margin-inline: var(--space-2)"), + "The design language and showroom must align same-layer lines and inset nested layers as a whole", ); assert( !sidebarCss.includes("border-right:") && !sidebarCss.includes("border-bottom:") && !showroomCss.includes("border-top: 1px solid") && !showroomCss.includes("border-bottom: 1px solid"), - "Shell and showroom structural separators must not fall back to one-sided solid borders", + "Shell and showroom structural boundaries must not fall back to one-sided solid borders", ); });