feat: refine workspace bevel hierarchy
This commit is contained in:
@@ -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とする。
|
この文書は、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
|
## 1. Principles
|
||||||
|
|
||||||
なぜこの設計にするかを定義する。個別の表示規則に迷った場合は、この章へ戻って判断する。
|
この章は具体的な見た目や配置を定めない。複数の規則や要求が競合したときに、どちらを優先するか判断するための原理だけを定める。
|
||||||
|
|
||||||
### 基本思想
|
### Task before explanation
|
||||||
|
|
||||||
- 作業ツールとして、シンプルで直感的なUX
|
現在のtaskと判断を、網羅的な説明、metadataの露出、装飾より優先する。UIはユーザーが次の有効な操作へ到達するまでの距離を短くする。
|
||||||
- UIのために曖昧な情報を推測しない。データを正直に表示し、操作を見通せるようにする
|
|
||||||
- 状態・scope・操作を、装飾ではなく構造で読ませ、情報量を増やしても騒がしくならない
|
|
||||||
- 説明的に見せるのではなく、配置と関係で理解させる
|
|
||||||
|
|
||||||
### 知覚原則
|
### Meaning before decoration
|
||||||
|
|
||||||
size、weight、color、spacing、line、background、motionを使うときは、次のどの知覚を成立させるためか説明できなければならない。説明できない視覚表現は追加しない。
|
視覚上の差は、情報の重要度、関係、操作可能性、または状態の違いを伝えるためにだけ使う。意味を説明できない視覚表現は追加しない。
|
||||||
|
|
||||||
#### Hierarchy — 何が重要か
|
### Authority before convenience
|
||||||
|
|
||||||
- 情報は、primary、secondary、technicalの順に読む構造にする。
|
UIの都合でdata、state、permission、利用可能なactionを推測しない。表示とaffordanceは、現在のauthorityが確定している内容に一致させる。
|
||||||
- 重要度は、最初に配置順、次にsize、weight、contrastで示す。colorや装飾だけで重要度を作らない。
|
|
||||||
- 同じscopeに最も強いheading、filled action、attention表現を複数置かない。
|
|
||||||
- primary contentと、その判断に必要なstateまたはactionを初期viewportへ置く。
|
|
||||||
- metadataと補助操作は、主要なidentityやcurrent stateより強く見せない。
|
|
||||||
|
|
||||||
#### Grouping — 何と何が同じグループか
|
### Complexity follows need
|
||||||
|
|
||||||
- 同じ判断や操作に使う情報は、一つのまとまりとして知覚できるようにする。
|
複雑さを消すのではなく、判断に必要になる位置と時点で開示する。初期表示では、現在のtaskと競合する詳細を前面へ出さない。
|
||||||
- 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とする。
|
|
||||||
|
|
||||||
## 2. Semantics / Grammar
|
## 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を通じて一度だけ表示する。
|
route titleはshell headerとmain contentを通じて一度だけ表示する。
|
||||||
|
|
||||||
@@ -85,9 +53,18 @@ route titleはshell headerとmain contentを通じて一度だけ表示する。
|
|||||||
- route分類、breadcrumb、header titleをeyebrow、heading、keyで繰り返さない。
|
- route分類、breadcrumb、header titleをeyebrow、heading、keyで繰り返さない。
|
||||||
- subtitleとledeは原則として置かない。
|
- 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、比較、次の操作に必要な値だけとする。
|
主要表示に置くのは、identity、healthまたはattention、比較、次の操作に必要な値だけとする。
|
||||||
|
|
||||||
@@ -102,7 +79,7 @@ route titleはshell headerとmain contentを通じて一度だけ表示する。
|
|||||||
|
|
||||||
機械的な値にはmonospace familyを使う。DTOに値が存在することだけを理由に表示しない。
|
機械的な値にはmonospace familyを使う。DTOに値が存在することだけを理由に表示しない。
|
||||||
|
|
||||||
### 段階的な開示
|
#### 段階的な開示
|
||||||
|
|
||||||
- primary viewにはidentity、current state、attention、次の有効なactionを表示する。
|
- primary viewにはidentity、current state、attention、次の有効なactionを表示する。
|
||||||
- 副次的な運用情報はplain section、side panel、またはlabel付き`details`へ置く。
|
- 副次的な運用情報はplain section、side panel、またはlabel付き`details`へ置く。
|
||||||
@@ -111,7 +88,25 @@ route titleはshell headerとmain contentを通じて一度だけ表示する。
|
|||||||
- create formとedit formは明示的なactionの後に表示する。
|
- create formとedit formは明示的なactionの後に表示する。
|
||||||
- blocking error、permission requirement、validation constraint、unsaved changeを閉じたdisclosureへ隠さない。
|
- 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状態だけでなく、表示構成そのものへ反映する。
|
- permissionはcontrolのenabled状態だけでなく、表示構成そのものへ反映する。
|
||||||
- readは可能だがmutationできない場合、有用なread viewだけを構成する。
|
- readは可能だがmutationできない場合、有用なread viewだけを構成する。
|
||||||
@@ -121,7 +116,7 @@ route titleはshell headerとmain contentを通じて一度だけ表示する。
|
|||||||
- destructive actionには対象と結果を明記し、復元できない場合は意図的なconfirmationを要求する。
|
- destructive actionには対象と結果を明記し、復元できない場合は意図的なconfirmationを要求する。
|
||||||
- 同じscopeのfilled primary actionは一つだけとする。
|
- 同じscopeのfilled primary actionは一つだけとする。
|
||||||
|
|
||||||
### Loading、empty、error、unavailable
|
loading、empty、error、unavailableは同じfallbackへまとめず、それぞれ次の表現を使う。
|
||||||
|
|
||||||
#### Loading
|
#### Loading
|
||||||
|
|
||||||
@@ -148,7 +143,9 @@ route titleはshell headerとmain contentを通じて一度だけ表示する。
|
|||||||
|
|
||||||
resource unavailable、capability missing、permission deniedを別の状態として扱う。`403`をempty listとして表示したり、別scopeへredirectしたりしない。
|
resource unavailable、capability missing、permission deniedを別の状態として扱う。`403`をempty listとして表示したり、別scopeへredirectしたりしない。
|
||||||
|
|
||||||
### 文言
|
## 3. Language / Copy
|
||||||
|
|
||||||
|
UI copyの語彙、長さ、説明責任を定める。
|
||||||
|
|
||||||
- 具体的な名詞と動詞を使う。
|
- 具体的な名詞と動詞を使う。
|
||||||
- identifier、API名、type名は正確性が必要な場合に原文を維持する。
|
- identifier、API名、type名は正確性が必要な場合に原文を維持する。
|
||||||
@@ -157,6 +154,10 @@ resource unavailable、capability missing、permission deniedを別の状態と
|
|||||||
- empty copyとstatus copyは短く、事実を直接書く。
|
- empty copyとstatus copyは短く、事実を直接書く。
|
||||||
- buttonは操作を表す。`Submit`、`OK`、装飾的なcategory labelを避ける。
|
- buttonは操作を表す。`Submit`、`OK`、装飾的なcategory labelを避ける。
|
||||||
|
|
||||||
|
## 4. Cross-cutting requirements
|
||||||
|
|
||||||
|
viewport、input modality、支援技術にかかわらず、すべての表現とcomponentが満たす要件を定める。
|
||||||
|
|
||||||
### Responsive
|
### Responsive
|
||||||
|
|
||||||
- page全体にhorizontal scrollを発生させない。
|
- page全体にhorizontal scrollを発生させない。
|
||||||
@@ -171,14 +172,14 @@ resource unavailable、capability missing、permission deniedを別の状態と
|
|||||||
|
|
||||||
- semantic elementを使う。
|
- semantic elementを使う。
|
||||||
- すべてのcontrolに明示的なaccessible nameを与える。
|
- すべてのcontrolに明示的なaccessible nameを与える。
|
||||||
- keyboard focusを常に見えるようにする。
|
- 視覚的なfocusとkeyboard focusを一致させ、常に見えるようにする。
|
||||||
- tab orderを画面上の順序と作業順に合わせる。
|
- tab orderを画面上の順序と作業順に合わせる。
|
||||||
- icon-only controlには具体的な`aria-label`を付ける。
|
- icon-only controlには具体的な`aria-label`を付ける。
|
||||||
- stateはcolor以外のtextまたはsemantic distinctionを持つ。
|
- stateはcolor以外のtextまたはsemantic distinctionを持つ。
|
||||||
- 必要なhorizontal scroll regionはfocus可能にし、accessible nameを付ける。
|
- 必要なhorizontal scroll regionはfocus可能にし、accessible nameを付ける。
|
||||||
- lightとdarkで同じsemantic tokenを使う。
|
- lightとdarkで同じsemantic tokenを使う。
|
||||||
|
|
||||||
## 3. Primitives / Tokens
|
## 5. Primitives / Tokens
|
||||||
|
|
||||||
色、文字、間隔、形、motionの最小単位を定義する。
|
色、文字、間隔、形、motionの最小単位を定義する。
|
||||||
|
|
||||||
@@ -204,9 +205,6 @@ colorはCSS custom propertyをauthorityとし、通常のWorkspace colorはOKLCH
|
|||||||
--danger
|
--danger
|
||||||
--interactive-hover
|
--interactive-hover
|
||||||
--interactive-selected
|
--interactive-selected
|
||||||
--bevel-highlight
|
|
||||||
--bevel-shadow
|
|
||||||
--bevel-face-width
|
|
||||||
--shadow-overlay
|
--shadow-overlay
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -214,11 +212,6 @@ colorはCSS custom propertyをauthorityとし、通常のWorkspace colorはOKLCH
|
|||||||
- muted textは新しいhueを増やす前にlightnessとchromaを下げる。
|
- muted textは新しいhueを増やす前にlightnessとchromaを下げる。
|
||||||
- accentとstatus colorはstate、focus、navigationへ限定する。
|
- accentとstatus colorはstate、focus、navigationへ限定する。
|
||||||
- componentへraw colorを追加せず、semantic tokenを使う。
|
- 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を作らない。
|
- 意味が重なるtoken aliasを作らない。
|
||||||
|
|
||||||
ConsoleとterminalのANSI paletteは専用tokenを使い、通常のstatusやformへ流用しない。
|
ConsoleとterminalのANSI paletteは専用tokenを使い、通常のstatusやformへ流用しない。
|
||||||
@@ -262,15 +255,29 @@ ConsoleとterminalのANSI paletteは専用tokenを使い、通常のstatusやfor
|
|||||||
- motionは任意かつ短くし、意味の必須条件にしない。
|
- motionは任意かつ短くし、意味の必須条件にしない。
|
||||||
- 繰り返しまたは連続animationではreduced-motion preferenceを尊重する。
|
- 繰り返しまたは連続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
|
||||||
|
|
||||||
- Sidebarは現在位置と利用可能なscopeを一つのnavigation hierarchyとして示す。
|
- Sidebarは現在位置と利用可能なscopeを一つのnavigation hierarchyとして示す。
|
||||||
- desktop幅は`clamp(220px, 20vw, 280px)`を基本とする。
|
- 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とscope controlだけを置く。
|
||||||
- navigation linkはsidebar幅全体を使うflat rowとする。
|
- navigation linkはsidebar幅全体を使うflat rowとする。
|
||||||
- active stateは一つのcueと`aria-current="page"`で示す。
|
- active stateは一つのcueと`aria-current="page"`で示す。
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
--interactive-selected: oklch(93% 0.02 230);
|
--interactive-selected: oklch(93% 0.02 230);
|
||||||
--bevel-highlight: #fff;
|
--bevel-highlight: #fff;
|
||||||
--bevel-shadow: #000;
|
--bevel-shadow: #000;
|
||||||
--bevel-face-width: 2px;
|
--bevel-face-width: 1px;
|
||||||
--shadow-overlay: 0 12px 32px oklch(0% 0 0 / 0.14);
|
--shadow-overlay: 0 12px 32px oklch(0% 0 0 / 0.14);
|
||||||
|
|
||||||
--font-sans: "Gen Interface JP", sans-serif;
|
--font-sans: "Gen Interface JP", sans-serif;
|
||||||
|
|||||||
@@ -1,60 +1,51 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import BevelLine from '$lib/workspace/ui/BevelLine.svelte';
|
import Bevel from '$lib/workspace/ui/Bevel.svelte';
|
||||||
import './sidebar.css';
|
import './sidebar.css';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: Snippet<[]>;
|
children: Snippet<[]>;
|
||||||
|
folded?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
let { children }: Props = $props();
|
let { children, folded = $bindable(false) }: Props = $props();
|
||||||
let folded = $state(false);
|
|
||||||
|
|
||||||
function toggleFold() {
|
function toggleFold() {
|
||||||
folded = !folded;
|
folded = !folded;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<aside class="sidebar-frame" class:folded aria-label="Sidebar">
|
<div class="sidebar-frame" class:folded>
|
||||||
{#if !folded}
|
<Bevel as="div" class="sidebar-frame__bevel" depth="inset" top={false} bottom={false} left={false} fill>
|
||||||
<div class="sidebar-frame-content">
|
<aside class="sidebar-frame__surface" aria-label="Sidebar">
|
||||||
{@render children()}
|
{#if !folded}
|
||||||
</div>
|
<div class="sidebar-frame-content">
|
||||||
{/if}
|
{@render children()}
|
||||||
|
</div>
|
||||||
<div class="sidebar-control-row">
|
|
||||||
<button
|
|
||||||
class="sidebar-fold-button"
|
|
||||||
type="button"
|
|
||||||
aria-label={folded ? 'Unfold sidebar' : 'Fold sidebar'}
|
|
||||||
aria-expanded={!folded}
|
|
||||||
title={folded ? 'Unfold sidebar' : 'Fold sidebar'}
|
|
||||||
onclick={toggleFold}
|
|
||||||
>
|
|
||||||
{#if folded}
|
|
||||||
<svg class="sidebar-icon" aria-hidden="true" viewBox="0 0 24 24">
|
|
||||||
<path d="m6 17 5-5-5-5" />
|
|
||||||
<path d="m13 17 5-5-5-5" />
|
|
||||||
</svg>
|
|
||||||
{:else}
|
|
||||||
<svg class="sidebar-icon" aria-hidden="true" viewBox="0 0 24 24">
|
|
||||||
<path d="m11 17-5-5 5-5" />
|
|
||||||
<path d="m18 17-5-5 5-5" />
|
|
||||||
</svg>
|
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<BevelLine
|
<div class="sidebar-control-row">
|
||||||
class="sidebar-frame__divider sidebar-frame__divider--vertical"
|
<button
|
||||||
direction="y"
|
class="sidebar-fold-button"
|
||||||
length="100%"
|
type="button"
|
||||||
decorative
|
aria-label={folded ? 'Unfold sidebar' : 'Fold sidebar'}
|
||||||
/>
|
aria-expanded={!folded}
|
||||||
<BevelLine
|
title={folded ? 'Unfold sidebar' : 'Fold sidebar'}
|
||||||
class="sidebar-frame__divider sidebar-frame__divider--horizontal"
|
onclick={toggleFold}
|
||||||
direction="x"
|
>
|
||||||
length="100%"
|
{#if folded}
|
||||||
decorative
|
<svg class="sidebar-icon" aria-hidden="true" viewBox="0 0 24 24">
|
||||||
/>
|
<path d="m6 17 5-5-5-5" />
|
||||||
</aside>
|
<path d="m13 17 5-5-5-5" />
|
||||||
|
</svg>
|
||||||
|
{:else}
|
||||||
|
<svg class="sidebar-icon" aria-hidden="true" viewBox="0 0 24 24">
|
||||||
|
<path d="m11 17-5-5 5-5" />
|
||||||
|
<path d="m18 17-5-5 5-5" />
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</Bevel>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -5,31 +5,28 @@
|
|||||||
--sidebar-item-hover: oklch(96% 0 0);
|
--sidebar-item-hover: oklch(96% 0 0);
|
||||||
--sidebar-item-active: oklch(92.5% 0 0);
|
--sidebar-item-active: oklch(92.5% 0 0);
|
||||||
|
|
||||||
|
display: grid;
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
grid-row: 1 / 3;
|
grid-row: 1 / 3;
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: clamp(220px, 20vw, 280px);
|
width: clamp(220px, 20vw, 280px);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
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);
|
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) {
|
@media (prefers-color-scheme: dark) {
|
||||||
.sidebar-frame {
|
.sidebar-frame {
|
||||||
--sidebar-item-hover: oklch(24% 0 0);
|
--sidebar-item-hover: oklch(24% 0 0);
|
||||||
@@ -39,7 +36,6 @@
|
|||||||
.sidebar-frame.folded {
|
.sidebar-frame.folded {
|
||||||
width: max-content;
|
width: max-content;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding-inline: 0;
|
|
||||||
}
|
}
|
||||||
.sidebar-frame-content {
|
.sidebar-frame-content {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
@@ -628,17 +624,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
.sidebar-frame,
|
.sidebar-frame {
|
||||||
.sidebar-frame.folded {
|
z-index: 20;
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
grid-row: 1;
|
grid-row: 2;
|
||||||
width: auto;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
.sidebar-frame__divider--vertical {
|
.sidebar-frame.folded {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.sidebar-frame__divider--horizontal {
|
.sidebar-control-row {
|
||||||
display: block;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,7 +128,7 @@
|
|||||||
inset: 0;
|
inset: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border-style: solid;
|
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-width: var(--_top-w) var(--_right-w) var(--_bottom-w) var(--_left-w);
|
||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
@@ -155,11 +155,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bevel[data-profile='edge'][data-depth='inset']::before {
|
.bevel[data-profile='edge'][data-depth='inset']::before {
|
||||||
border-color: var(--line);
|
--_fallback-border: var(--line);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bevel[data-profile='ridge'][data-depth='inset']::before {
|
.bevel[data-profile='ridge'][data-depth='inset']::before {
|
||||||
border-color: var(--line);
|
--_fallback-border: var(--line);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bevel[data-profile='ridge']::after {
|
.bevel[data-profile='ridge']::after {
|
||||||
@@ -168,7 +168,7 @@
|
|||||||
var(--bevel-right-inset)
|
var(--bevel-right-inset)
|
||||||
var(--bevel-bottom-inset)
|
var(--bevel-bottom-inset)
|
||||||
var(--bevel-left-inset);
|
var(--bevel-left-inset);
|
||||||
border-color: var(--line);
|
--_fallback-border: var(--line);
|
||||||
border-radius:
|
border-radius:
|
||||||
max(0px, calc(var(--bevel-top-left-radius) - var(--bevel-face-width)))
|
max(0px, calc(var(--bevel-top-left-radius) - var(--bevel-face-width)))
|
||||||
max(0px, calc(var(--bevel-top-right-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 {
|
.bevel[data-profile='ridge'][data-depth='inset']::after {
|
||||||
border-color: var(--line-strong);
|
--_fallback-border: var(--line-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bevel[data-invalid='true'],
|
.bevel[data-invalid='true'],
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { setContext } from 'svelte';
|
import { setContext } from 'svelte';
|
||||||
import WorkspaceAlerts from '$lib/workspace/alerts/WorkspaceAlerts.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 { provideHeaderController, type HeaderController } from '$lib/workspace/header/context';
|
||||||
import GlobalSidebar from '$lib/workspace/sidebar/GlobalSidebar.svelte';
|
import GlobalSidebar from '$lib/workspace/sidebar/GlobalSidebar.svelte';
|
||||||
import SidebarFrame from '$lib/workspace/sidebar/SidebarFrame.svelte';
|
import SidebarFrame from '$lib/workspace/sidebar/SidebarFrame.svelte';
|
||||||
@@ -17,6 +17,11 @@
|
|||||||
sidebar = activeSidebar;
|
sidebar = activeSidebar;
|
||||||
});
|
});
|
||||||
const headerController = $state<HeaderController>({ content: null });
|
const headerController = $state<HeaderController>({ content: null });
|
||||||
|
let sidebarFolded = $state(false);
|
||||||
|
|
||||||
|
function toggleSidebar() {
|
||||||
|
sidebarFolded = !sidebarFolded;
|
||||||
|
}
|
||||||
|
|
||||||
provideHeaderController(headerController);
|
provideHeaderController(headerController);
|
||||||
setContext<SidebarController>(SIDEBAR_CONTEXT, {
|
setContext<SidebarController>(SIDEBAR_CONTEXT, {
|
||||||
@@ -26,8 +31,8 @@
|
|||||||
|
|
||||||
<WorkspaceAlerts />
|
<WorkspaceAlerts />
|
||||||
|
|
||||||
<div class="app-shell">
|
<div class="app-shell" class:sidebar-open={!sidebarFolded}>
|
||||||
<SidebarFrame>
|
<SidebarFrame bind:folded={sidebarFolded}>
|
||||||
<GlobalSidebar
|
<GlobalSidebar
|
||||||
currentPath={page.url.pathname}
|
currentPath={page.url.pathname}
|
||||||
content={sidebar}
|
content={sidebar}
|
||||||
@@ -35,25 +40,39 @@
|
|||||||
workspaceError={data.workspaceCatalogError}
|
workspaceError={data.workspaceCatalogError}
|
||||||
/>
|
/>
|
||||||
</SidebarFrame>
|
</SidebarFrame>
|
||||||
<header class="app-shell__topbar">
|
<Bevel as="div" class="app-shell__topbar-bevel" depth="inset" top={false} right={false} left={false} fill>
|
||||||
<div class="app-shell__topbar-location">
|
<header class="app-shell__topbar">
|
||||||
{#if headerController.content}{@render headerController.content()}{/if}
|
<div class="app-shell__topbar-location">
|
||||||
</div>
|
{#if headerController.content}{@render headerController.content()}{/if}
|
||||||
<nav class="app-shell__topbar-actions" aria-label="Global navigation">
|
</div>
|
||||||
<a class="app-shell__icon-button" href="/account" aria-label="Open Account" title="Account">
|
<nav class="app-shell__topbar-actions" aria-label="Global navigation">
|
||||||
<svg class="app-shell__icon" aria-hidden="true" viewBox="0 0 24 24">
|
<button
|
||||||
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" />
|
class="app-shell__icon-button app-shell__mobile-sidebar-toggle"
|
||||||
<circle cx="12" cy="7" r="4" />
|
type="button"
|
||||||
</svg>
|
aria-label={sidebarFolded ? 'Show sidebar' : 'Hide sidebar'}
|
||||||
</a>
|
aria-expanded={!sidebarFolded}
|
||||||
</nav>
|
title={sidebarFolded ? 'Show sidebar' : 'Hide sidebar'}
|
||||||
<BevelLine
|
onclick={toggleSidebar}
|
||||||
class="app-shell__topbar-divider"
|
>
|
||||||
direction="x"
|
<svg class="app-shell__icon" aria-hidden="true" viewBox="0 0 24 24">
|
||||||
length="100%"
|
{#if sidebarFolded}
|
||||||
decorative
|
<path d="m6 17 5-5-5-5" />
|
||||||
/>
|
<path d="m13 17 5-5-5-5" />
|
||||||
</header>
|
{:else}
|
||||||
|
<path d="m11 17-5-5 5-5" />
|
||||||
|
<path d="m18 17-5-5 5-5" />
|
||||||
|
{/if}
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<a class="app-shell__icon-button" href="/account" aria-label="Open Account" title="Account">
|
||||||
|
<svg class="app-shell__icon" aria-hidden="true" viewBox="0 0 24 24">
|
||||||
|
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" />
|
||||||
|
<circle cx="12" cy="7" r="4" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
</Bevel>
|
||||||
<main class="app-shell__main">
|
<main class="app-shell__main">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</main>
|
</main>
|
||||||
@@ -72,11 +91,14 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-shell__topbar {
|
.app-shell :global(.bevel.app-shell__topbar-bevel) {
|
||||||
position: relative;
|
|
||||||
z-index: 30;
|
z-index: 30;
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell__topbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -88,12 +110,6 @@
|
|||||||
backdrop-filter: blur(14px);
|
backdrop-filter: blur(14px);
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.app-shell__topbar-divider) {
|
|
||||||
position: absolute;
|
|
||||||
inset-inline: 0;
|
|
||||||
inset-block-end: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-shell__topbar-location {
|
.app-shell__topbar-location {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -123,6 +139,14 @@
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-shell__mobile-sidebar-toggle {
|
||||||
|
display: none;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.app-shell__icon {
|
.app-shell__icon {
|
||||||
width: 1.1rem;
|
width: 1.1rem;
|
||||||
height: 1.1rem;
|
height: 1.1rem;
|
||||||
@@ -150,25 +174,36 @@
|
|||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
.app-shell {
|
.app-shell {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: minmax(0, 1fr);
|
||||||
grid-template-rows: auto auto 1fr;
|
grid-template-rows: auto minmax(0, 1fr);
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: auto;
|
height: 100dvh;
|
||||||
min-height: 100dvh;
|
min-height: 0;
|
||||||
overflow: visible;
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell :global(.bevel.app-shell__topbar-bevel) {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-shell__topbar {
|
.app-shell__topbar {
|
||||||
grid-column: 1;
|
|
||||||
grid-row: 2;
|
|
||||||
padding: 0 var(--space-4);
|
padding: 0 var(--space-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-shell__mobile-sidebar-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
.app-shell__main {
|
.app-shell__main {
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
grid-row: 3;
|
grid-row: 2;
|
||||||
overflow: visible;
|
overflow-y: auto;
|
||||||
padding: var(--space-5) var(--space-4);
|
padding: var(--space-5) var(--space-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-shell.sidebar-open .app-shell__main {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -167,7 +167,6 @@
|
|||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
padding: var(--space-3) var(--space-4);
|
padding: var(--space-3) var(--space-4);
|
||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
background: var(--bg-raised);
|
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,6 +296,7 @@
|
|||||||
|
|
||||||
.resource-list {
|
.resource-list {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
margin-inline: var(--space-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.resource-row {
|
.resource-row {
|
||||||
@@ -409,7 +409,7 @@
|
|||||||
|
|
||||||
.key-value,
|
.key-value,
|
||||||
.technical-values {
|
.technical-values {
|
||||||
margin: 0;
|
margin: 0 var(--space-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.key-value > div:not(.bevel-line),
|
.key-value > div:not(.bevel-line),
|
||||||
@@ -553,6 +553,7 @@
|
|||||||
.sample-form {
|
.sample-form {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--space-5);
|
gap: var(--space-5);
|
||||||
|
margin-inline: var(--space-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-grid {
|
.form-grid {
|
||||||
|
|||||||
@@ -39,10 +39,15 @@ Deno.test("Bevel keeps depth visual while semantic controls remain children", as
|
|||||||
assert(
|
assert(
|
||||||
source.includes("@supports") &&
|
source.includes("@supports") &&
|
||||||
source.includes("border-style: solid") &&
|
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(
|
source.includes(
|
||||||
"border-width: var(--_top-w) var(--_right-w) var(--_bottom-w) var(--_left-w)",
|
"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(
|
assert(
|
||||||
source.includes("calc(max(cos(180deg - var(--_L)), 0) * 100%)") &&
|
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",
|
"Bevel must clamp unlit faces to the configured shadow without ambient light",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
appCss.includes("--bevel-face-width: 2px") &&
|
appCss.includes("--bevel-face-width: 1px") &&
|
||||||
source.includes("--_W: var(--bevel-face-width)") &&
|
source.includes("--_W: var(--bevel-face-width)") &&
|
||||||
source.includes("inset: var(--bevel-face-width)") &&
|
source.includes("inset: var(--bevel-face-width)") &&
|
||||||
!source.includes("BevelSize") &&
|
!source.includes("BevelSize") &&
|
||||||
!source.includes("data-size"),
|
!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(
|
assert(
|
||||||
source.includes("data-top={top ? 'true' : 'false'}") &&
|
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",
|
"Bevel must use the shared highlight and shadow palette without owning a surface tone",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
designLanguage.includes("Bevelはedge") &&
|
designLanguage.includes("Bevel自体へbackgroundやsurface toneを与えず") &&
|
||||||
designLanguage.includes("lightingだけを所有する") &&
|
designLanguage.includes("text/content areaへ適用する"),
|
||||||
designLanguage.includes("text/content areaだけとする"),
|
|
||||||
"The design language must prohibit Bevel surface colors outside the child content area",
|
"The design language must prohibit Bevel surface colors outside the child content area",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
@@ -118,6 +122,15 @@ Deno.test("Workspace showroom exercises raised inset and ridge edges", async ()
|
|||||||
import.meta.url,
|
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(
|
assert(
|
||||||
workspaceSource.includes('profile="edge" depth="raised"') &&
|
workspaceSource.includes('profile="edge" depth="raised"') &&
|
||||||
@@ -128,6 +141,10 @@ Deno.test("Workspace showroom exercises raised inset and ridge edges", async ()
|
|||||||
workspaceSource.includes("pressed"),
|
workspaceSource.includes("pressed"),
|
||||||
"Workspace showroom must demonstrate edge and ridge depths with full and adjacent side selections",
|
"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(
|
assert(
|
||||||
settingsSource.includes('profile="edge" depth="inset"') &&
|
settingsSource.includes('profile="edge" depth="inset"') &&
|
||||||
settingsSource.includes('aria-invalid="true"'),
|
settingsSource.includes('aria-invalid="true"'),
|
||||||
|
|||||||
@@ -28,14 +28,20 @@ Deno.test("BevelLine exposes a semantic directional separator", async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert(
|
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-face-width: var(--bevel-face-width)") &&
|
||||||
source.includes(
|
source.includes(
|
||||||
"--bevel-line-width: calc(var(--bevel-line-face-width) * 2)",
|
"--bevel-line-width: calc(var(--bevel-line-face-width) * 2)",
|
||||||
) &&
|
) &&
|
||||||
!source.includes("BevelLineWeight") &&
|
!source.includes("BevelLineWeight") &&
|
||||||
!source.includes("data-weight"),
|
!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(
|
assert(
|
||||||
source.includes("background: var(--line-strong)") &&
|
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(
|
const rootLayout = await Deno.readTextFile(
|
||||||
new URL("../src/routes/+layout.svelte", import.meta.url),
|
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,
|
import.meta.url,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
const designLanguage = await Deno.readTextFile(
|
||||||
|
new URL(
|
||||||
|
"../../../docs/development/ui-ux/design-language.md",
|
||||||
|
import.meta.url,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
assert(
|
assert(
|
||||||
rootLayout.includes("app-shell__topbar-divider") &&
|
rootLayout.includes('class="app-shell__topbar-bevel"') &&
|
||||||
sidebarFrame.includes("sidebar-frame__divider--vertical") &&
|
rootLayout.includes('depth="inset"') &&
|
||||||
sidebarFrame.includes("sidebar-frame__divider--horizontal"),
|
rootLayout.includes("top={false} right={false} left={false}") &&
|
||||||
"Workspace shell must render structural dividers through BevelLine",
|
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(
|
assert(
|
||||||
!sidebarCss.includes("border-right:") &&
|
!sidebarCss.includes("border-right:") &&
|
||||||
!sidebarCss.includes("border-bottom:") &&
|
!sidebarCss.includes("border-bottom:") &&
|
||||||
!showroomCss.includes("border-top: 1px solid") &&
|
!showroomCss.includes("border-top: 1px solid") &&
|
||||||
!showroomCss.includes("border-bottom: 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",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user