fix: simplify workspace borders and design rules
This commit is contained in:
@@ -1,191 +1,85 @@
|
||||
# Workspace Web Design Language
|
||||
|
||||
この文書は、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のrendered resultが満たす設計規則を定義する。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)、visual review手順は[`visual-review.md`](visual-review.md)をauthorityとする。
|
||||
|
||||
一つの規則は、以下の各章のうち最も具体的な一箇所だけをauthorityとする。別の章で同じ禁止や手段を再掲せず、必要な場合はauthorityとなる章を参照する。
|
||||
|
||||
この文書はrendered resultが満たす設計規則を定義する。実装者自身がbefore/afterをcaptureして完了判定する手順は、[`visual-review.md`](visual-review.md)をauthorityとする。
|
||||
一つの規則は最も具体的な一箇所だけをauthorityとする。具体的な二案の一方を棄却できない文は規則として残さない。
|
||||
|
||||
## 1. Principles
|
||||
|
||||
この章は具体的な見た目や配置を定めない。複数の規則や要求が競合したときに、どちらを優先するか判断するための原理だけを定める。
|
||||
複数の規則や要求が競合した場合だけ、この章の優先原理を使う。
|
||||
|
||||
### Task before explanation
|
||||
|
||||
現在のtaskと判断を、網羅的な説明、metadataの露出、装飾より優先する。UIはユーザーが次の有効な操作へ到達するまでの距離を短くする。
|
||||
次の有効なactionへ到達する前の操作、scroll、常設説明が少ない案を選ぶ。ただし、説明を省くと対象、結果、permission、不可逆性を誤認する場合は説明をactionより前に置く。
|
||||
|
||||
### Meaning before decoration
|
||||
|
||||
視覚上の差は、情報の重要度、関係、操作可能性、または状態の違いを伝えるためにだけ使う。意味を説明できない視覚表現は追加しない。
|
||||
情報の関係、重要度、操作可能性、stateのいずれも変えない視覚差を追加しない。
|
||||
|
||||
### Authority before convenience
|
||||
|
||||
UIの都合でdata、state、permission、利用可能なactionを推測しない。表示とaffordanceは、現在のauthorityが確定している内容に一致させる。
|
||||
|
||||
### Complexity follows need
|
||||
|
||||
複雑さを消すのではなく、判断に必要になる位置と時点で開示する。初期表示では、現在のtaskと競合する詳細を前面へ出さない。
|
||||
Backend authorityが確定していないdata、state、permission、actionを、UIの都合で補完または推測する案を棄却する。
|
||||
|
||||
## 2. Semantics / Grammar
|
||||
|
||||
この章は、Workspace Webで情報、操作、状態を何として扱い、どう表現するかを定める。
|
||||
|
||||
### Information structure
|
||||
|
||||
#### Priority and focus
|
||||
- pageと、page内で一つのtaskまたは判断を完結させる領域をscopeとする。
|
||||
- 各scopeで最も強い視覚要素はheading、現在判断が必要なattention、filled actionのいずれか一つだけにする。同じ強さのheading、attention、filled actionを複数置かない。
|
||||
- hierarchyをcolorだけで表現しない。DOMと画面上の配置順を一致させた上で、size、weight、contrastを使う。
|
||||
- scopeを識別するprimary titleは、一つのscopeにつき一度だけ表示する。
|
||||
- actionは、そのactionが影響するscope内に置く。
|
||||
- DTOにfieldが存在するという理由だけで表示せず、current taskの判断またはactionを変える値だけを常設する。
|
||||
|
||||
- 情報は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を作るために周囲の情報を過度に薄くしたり、小さくしたりしない。
|
||||
**Disclosure**
|
||||
|
||||
#### Page and section structure
|
||||
|
||||
route titleはshell headerとmain contentを通じて一度だけ表示する。
|
||||
|
||||
- 標準headerまたはbreadcrumbが現在routeを十分に識別している場合、main contentは最初の操作対象またはsectionから始める。
|
||||
- shell headerがscopeだけを示し、main contentの対象を識別できない場合に限り、main側へ一つのtitleを置く。
|
||||
- primary actionは、そのactionがpage全体の主要操作である場合だけtitleと同じrowへ置く。
|
||||
- route分類、breadcrumb、header titleをeyebrow、heading、keyで繰り返さない。
|
||||
- subtitleとledeは原則として置かない。
|
||||
|
||||
一つのsectionは、一つの目的、ownership、または読み順を持つ。目的が異なる内容を同じsectionへ入れず、同じ目的の内容を見た目だけで複数sectionへ分けない。Section間とsection内の関係は次のGrouping and separationに従い、具体的なcueは`Primitives / Tokens`だけから選ぶ。
|
||||
|
||||
#### Grouping and separation
|
||||
|
||||
- 同じ判断や操作に使う情報は、一つのまとまりとして知覚できるようにする。
|
||||
- groupの単位は、共通のownership、behavior、またはユーザーが行う一つの判断から決める。data sourceやDTOが同じという理由だけでgroup化しない。
|
||||
- label、value、help、error、actionの対応関係を、別の説明を読まなくても追えるようにする。
|
||||
- 異なるgroupの境界を知覚でき、その強さが意味上の距離と一致するようにする。
|
||||
- 境界は何と何を分け、どのgroupの開始または終端を示すか説明できるようにする。
|
||||
- 一つのgroupまたは境界へ複数の視覚cueを重ねず、異なるhierarchyの境界をすべて同じ強さで表現しない。
|
||||
|
||||
#### Metadata
|
||||
|
||||
主要表示に置くのは、identity、healthまたはattention、比較、次の操作に必要な値だけとする。
|
||||
|
||||
次の値は通常、label付きのdetail、debug、copy affordanceの後ろへ置く。
|
||||
|
||||
- 内部ID
|
||||
- schema、config、recordのrevision
|
||||
- digestとfingerprint
|
||||
- provider diagnostics
|
||||
- authority source label
|
||||
- raw requestまたはresponse payload
|
||||
|
||||
機械的な値にはmonospace familyを使う。DTOに値が存在することだけを理由に表示しない。
|
||||
|
||||
#### 段階的な開示
|
||||
|
||||
- primary viewにはidentity、current state、attention、次の有効なactionを表示する。
|
||||
- 副次的な運用情報はplain section、side panel、またはlabel付き`details`へ置く。
|
||||
- debug evidenceとraw metadataは、ユーザーが明示的に開いた場合だけ表示する。
|
||||
- destructive controlは、ユーザーが該当flowへ入るまでprimary actionより弱く表示する。
|
||||
- create formとedit formは明示的なactionの後に表示する。
|
||||
- blocking error、permission requirement、validation constraint、unsaved changeを閉じたdisclosureへ隠さない。
|
||||
- Raw dataやデバッグ情報など、通常は必要としない詳細な情報は折りたたみ領域に隠す。
|
||||
- 作成または編集のformを表示用screenへ常時同居させず、明示的なactionで開く折りたたみ領域または別pageに置く。
|
||||
|
||||
### Interaction and state
|
||||
|
||||
#### Affordance
|
||||
|
||||
- actionにはbutton、navigationにはlink、選択にはcontrolを使い、役割を見た目とsemantic elementで一致させる。
|
||||
- interactive elementは、通常、hover、focus、active、disabledを区別できるようにする。
|
||||
- actionにはbutton、navigationにはlink、選択には対応するform controlを使う。
|
||||
- interactive elementはhover、focus、active、disabledを視覚的に区別する。
|
||||
- staticなstatus、key、metadataをbuttonやpillの形にしない。
|
||||
- clickable rowは、click可能な範囲と遷移先を一つにする。
|
||||
- permissionがない操作をdisabled controlとして見せることでaffordanceを偽らない。
|
||||
- clickable rowは遷移先を一つだけ持ち、row全体で同じ遷移を実行する。
|
||||
- permissionがないactionをdisabled controlとして表示しない。
|
||||
|
||||
#### State representation
|
||||
**State and operation**
|
||||
|
||||
- 権威を持つdata sourceのstateをそのまま表示し、UIの都合でunknown、stale、unavailableを推測で埋めない。
|
||||
- stateは対象resourceまたはoperationの近くに置き、何のstateかを位置関係で判別できるようにする。
|
||||
- stateはmarkerとtextで示し、colorを補助に使う。
|
||||
- loading、empty、error、permission denied、unavailable、staleを別の状態として扱う。
|
||||
- 非同期operationでは、開始、処理中、完了、失敗を見通せるようにする。
|
||||
- stateの違いによってユーザーの判断または利用できるactionが変わる場合、その違いを一つの表示へ統合しない。
|
||||
- UI stateはそのstateを所有するauthorityから取得し、別domainやtransportのsignalから推測しない。
|
||||
- 非同期mutationでは楽観的更新か悲観的更新を選ぶ。楽観的更新は暫定stateを明示し、failure時のrollbackまたは再取得を定義できる場合だけ使う。それ以外は確定済みstateを維持し、operationのpending stateを別に表示する。
|
||||
- loading、failure、unavailableの表示は利用不能になった最小のscopeだけを置き換え、利用可能な親scopeとsibling scopeを残す。
|
||||
- destructive actionは対象と結果を明記し、復元できない場合だけconfirmationを要求する。
|
||||
|
||||
#### Authority、権限、操作
|
||||
### Language / Copy
|
||||
|
||||
- permissionはcontrolのenabled状態だけでなく、表示構成そのものへ反映する。
|
||||
- readは可能だがmutationできない場合、有用なread viewだけを構成する。
|
||||
- endpointからdataを取得できたかどうかでpermissionを推測しない。
|
||||
- domain stateとtransport stateを混同しない。
|
||||
- current stateで有効なactionだけを利用可能として提示する。
|
||||
- destructive actionには対象と結果を明記し、復元できない場合は意図的なconfirmationを要求する。
|
||||
- 同じscopeのfilled primary actionは一つだけとする。
|
||||
- title、navigation label、control labelで明らかな内容を説明文で繰り返さない。
|
||||
- 常設proseは、対象またはactionの選択を変えるwarning、permission、validation constraint、error recoveryに必要な内容だけにする。
|
||||
- button labelには具体的なactionを表す動詞を使い、`Submit`と`OK`を使わない。
|
||||
- identifier、API名、type名を別の語へ言い換えない。
|
||||
|
||||
loading、empty、error、unavailableは同じfallbackへまとめず、それぞれ次の表現を使う。
|
||||
|
||||
#### Loading
|
||||
|
||||
- page frameとsection位置を維持する。
|
||||
- scopeが明らかでない場合は、何を読み込んでいるか示す。
|
||||
- page全体を中央spinnerへ置き換えない。
|
||||
- spinnerだけに意味を持たせない。
|
||||
|
||||
#### Empty
|
||||
|
||||
- absenceを直接表現する。
|
||||
- permissionがあり、actionが実際に利用できる場合だけ、次のactionを一つ提示する。
|
||||
- recordが存在しない状態と、読み込めなかった状態を区別する。
|
||||
|
||||
#### Error
|
||||
|
||||
- field validationは対象fieldの近くに置く。
|
||||
- operation errorは失敗したactionの近くに置く。
|
||||
- section load errorは対象section内に置き、利用できるsibling sectionは残す。
|
||||
- routeの主要目的を果たせない場合だけroute-level errorにする。
|
||||
- diagnosticsはboundedかつactionableにする。
|
||||
|
||||
#### Unavailableとpermission
|
||||
|
||||
resource unavailable、capability missing、permission deniedを別の状態として扱う。`403`をempty listとして表示したり、別scopeへredirectしたりしない。
|
||||
|
||||
## 3. Language / Copy
|
||||
|
||||
UI copyの語彙、長さ、説明責任を定める。
|
||||
|
||||
- 具体的な名詞と動詞を使う。
|
||||
- identifier、API名、type名は正確性が必要な場合に原文を維持する。
|
||||
- title、navigation label、明らかなcontrol behavior、componentの存在理由を説明文で繰り返さない。
|
||||
- proseを追加するのは、判断を変える具体的な情報、warning、permission、validation constraint、error recoveryに必要な場合だけとする。
|
||||
- empty copyとstatus copyは短く、事実を直接書く。
|
||||
- buttonは操作を表す。`Submit`、`OK`、装飾的なcategory labelを避ける。
|
||||
|
||||
## 4. Cross-cutting requirements
|
||||
|
||||
viewport、input modality、支援技術にかかわらず、すべての表現とcomponentが満たす要件を定める。
|
||||
|
||||
### Responsive
|
||||
### Responsive behavior
|
||||
|
||||
- page全体にhorizontal scrollを発生させない。
|
||||
- board、table、code view、terminalは、自分の範囲内でbounded horizontal scrollを所有してよい。
|
||||
- side-by-side表示が同じ判断を支えなくなった場合、一列にする。
|
||||
- stack化によって比較関係が失われる場合、比較面を横方向に維持する。
|
||||
- wrappingまたはstack化によって情報の対応関係が失われる場合だけ、その領域内にbounded horizontal scrollを持たせる。それ以外のside-by-side layoutは一列にする。
|
||||
- primary actionを初期viewportから追い出さない。
|
||||
- touch targetの操作性を維持し、overflow解消のためにtextを縮小しない。
|
||||
- 長いkey、ref、diagnosticはwrapまたはtruncateし、完全な値へaccessできる手段を用意する。
|
||||
- contentをtruncateする場合、完全な値をcopyまたはdetailで取得できるようにする。
|
||||
|
||||
### Accessibility
|
||||
|
||||
- semantic elementを使う。
|
||||
- すべてのcontrolに明示的なaccessible nameを与える。
|
||||
- 視覚的な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を使う。
|
||||
- action、navigation、form controlを非semantic elementだけで実装しない。
|
||||
- すべてのcontrolにaccessible nameを与える。
|
||||
- keyboard focusを不可視にしない。
|
||||
- tab orderを画面上の順序と一致させる。
|
||||
- stateをcolorだけで区別しない。
|
||||
- horizontal scroll regionをkeyboard focus可能にし、accessible nameを与える。
|
||||
|
||||
## 5. Primitives / Tokens
|
||||
|
||||
色、文字、間隔、形、motionの最小単位を定義する。
|
||||
## 3. Primitives / Tokens
|
||||
|
||||
### Color / Theme
|
||||
|
||||
colorはCSS custom propertyをauthorityとし、通常のWorkspace colorはOKLCHで定義する。lightとdarkは同じsemantic tokenを使う。
|
||||
colorはCSS custom propertyをauthorityとし、通常のWorkspace colorはOKLCHで定義する。lightとdarkは同じsemantic token名を使う。
|
||||
|
||||
```css
|
||||
--bg
|
||||
@@ -208,29 +102,27 @@ colorはCSS custom propertyをauthorityとし、通常のWorkspace colorはOKLCH
|
||||
--shadow-overlay
|
||||
```
|
||||
|
||||
- backgroundとlayout surfaceはneutralにする。
|
||||
- muted textは新しいhueを増やす前にlightnessとchromaを下げる。
|
||||
- accentとstatus colorはstate、focus、navigationへ限定する。
|
||||
- componentへraw colorを追加せず、semantic tokenを使う。
|
||||
- 意味が重なるtoken aliasを作らない。
|
||||
|
||||
ConsoleとterminalのANSI paletteは専用tokenを使い、通常のstatusやformへ流用しない。
|
||||
- `--bg`、`--bg-raised`、`--bg-subtle`へhueを持たせない。
|
||||
- accentはfocus、current navigation、current selectionだけに使い、通常本文や通常actionの背景へ使わない。
|
||||
- success、warning、dangerを、それぞれ対応するstate以外へ使わない。
|
||||
- componentへraw colorを追加せず、上記semantic tokenを使う。
|
||||
- ConsoleとterminalのANSI paletteを通常のstatusやformへ流用しない。
|
||||
|
||||
### Typography
|
||||
|
||||
| 用途 | size / line height | 使用箇所 |
|
||||
| -------------------------------------- | ------------------ | ----------------------------- |
|
||||
| Route title | `24px / 32px` | shellまたはmainのどちらか一方 |
|
||||
| Body、control、row、section title | `14px / 20px` | contentとinteraction |
|
||||
| Metadata、table heading、machine value | `12px / 16px` | 副次情報と機械的な値 |
|
||||
font sizeは、その領域でユーザーが行う読み方によって選ぶ。
|
||||
|
||||
- defaultは`14px / 20px`とし、個々のtextを読むこと自体がtaskの中心になる領域に使う。
|
||||
- 同じ構造の反復を一覧として走査する領域と、位置、順序、形、選択状態を中心に識別するUI chromeには`12px / 16px`を使う。textを読まなければactionの意味や結果を判断できない場合は`14px / 20px`を使う。
|
||||
- `24px / 32px`はページまたはdocument内で唯一かつ最大のheadingだけに使う。
|
||||
- 収めるため、または情報を弱く見せるためにfont sizeを下げない。`12px`未満と`13px`を使わない。
|
||||
- 機械的な値にはmonospace familyを使う。
|
||||
|
||||
```css
|
||||
--font-sans
|
||||
--font-mono
|
||||
```
|
||||
|
||||
別のfont sizeを増やす前に、配置、spacing、weight、muted colorでhierarchyを作る。textを`12px`未満へ縮小しない。
|
||||
|
||||
### Spacing / Shape
|
||||
|
||||
```css
|
||||
@@ -243,69 +135,56 @@ ConsoleとterminalのANSI paletteは専用tokenを使い、通常のstatusやfor
|
||||
--radius-soft
|
||||
```
|
||||
|
||||
- groupingはspacingとalignmentを最初のcueとする。
|
||||
- separationにはspacing、line、surface backgroundのいずれか一つをprimary cueとして選ぶ。
|
||||
- 汎用的な`Card` primitiveは設けない。
|
||||
- nested surfaceはparentと同じ境界表現を繰り返さない。
|
||||
- shadowは通常flowから浮くmenu、popover、Tooltipなどの一時的overlayだけが使用できる。
|
||||
- radiusはinteractive controlだけが使用できる。
|
||||
- 一つの境界にspacing、line、surface backgroundを重ねず、primary cueを一つだけ使う。
|
||||
- 汎用的な`Card` primitiveを設けない。
|
||||
- nested surfaceでparentと同じ境界表現を繰り返さない。
|
||||
- shadowは通常flowから浮くmenu、popover、Tooltipだけに使う。
|
||||
- radiusはinteractive controlだけに使う。
|
||||
|
||||
### Motion
|
||||
|
||||
- motionは任意かつ短くし、意味の必須条件にしない。
|
||||
- 繰り返しまたは連続animationではreduced-motion preferenceを尊重する。
|
||||
- motion完了をactionの受付、state change、content理解の条件にしない。
|
||||
- 繰り返しまたは連続animationを、`prefers-reduced-motion: reduce`で停止する。
|
||||
|
||||
## 6. Components / Patterns
|
||||
## 4. Components / Patterns
|
||||
|
||||
実際のUIを、上のprinciple、grammar、communication、cross-cutting requirement、tokenから組み立てる。完成pageを固定templateにしない。
|
||||
### Border / Structural edge
|
||||
|
||||
### 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`へ置き換えない。
|
||||
- `Bevel`は1pxの単色borderを描くvisual wrapperであり、照明、raised/inset、ridge/grooveを表現しない。
|
||||
- `Bevel`自体へbackgroundやsurface toneを与えず、必要なbackgroundは内側のsemantic childが持つtext/content areaへ適用する。
|
||||
- 隣接するsurfaceでは、実際に境界となる辺だけを有効にする。
|
||||
- standaloneな閉じた領域は全辺を`Bevel`で囲う。Desktop shellではHeaderのbottom edgeとSidebarのright edgeだけを有効にし、viewport外周のtop/left edgeを描かない。
|
||||
- `BevelLine`は開いた領域内の1px separatorだけに使う。同じlayerではheading、本文、Lineの端を揃え、Lineだけに端方向のpaddingやmarginを追加しない。
|
||||
- nested layerでは親layoutがそのlayer全体へinline方向の余白を与え、content、row、Lineを一緒に移動する。Lineだけを短くしない。
|
||||
- control自身のborder、focus ring、status marker、table gridを`Bevel`または`BevelLine`で置き換えない。
|
||||
|
||||
### Sidebar
|
||||
|
||||
- Sidebarは現在位置と利用可能なscopeを一つのnavigation hierarchyとして示す。
|
||||
- desktop幅は`clamp(220px, 20vw, 280px)`を基本とする。
|
||||
- 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"`で示す。
|
||||
- iconだけでlabelを置き換えない。
|
||||
- fold controlは一つだけ置き、現在stateに対応するaccessible nameを使う。
|
||||
- narrow viewportでも同じSidebarの意味とnavigation hierarchyを維持する。
|
||||
- Desktop幅は`clamp(220px, 20vw, 280px)`とする。
|
||||
- navigation contentだけをscrollさせる。
|
||||
- Desktopのfold controlはSidebar下部、Mobileのfold controlはHeaderに置く。
|
||||
- MobileでSidebarを表示するときはHeader下の残りviewport全体を使い、main contentと同時表示しない。
|
||||
- navigationとscope control以外を置かない。
|
||||
- navigation linkはSidebar幅全体を使うflat rowとする。
|
||||
- active routeは一つの視覚cueと`aria-current="page"`で示す。
|
||||
- iconだけでnavigation labelを置き換えない。
|
||||
- fold controlは一つだけ置き、current stateに対応するaccessible nameを使う。
|
||||
|
||||
### Tooltip / Contextual help
|
||||
|
||||
- operationやtable全体を説明する常設proseを置く前に、button labelとcolumn headingを明確にする。
|
||||
- labelだけでは表現しきれない補助説明は、対象へbindしたTooltipで表示する。
|
||||
- operationやtable全体を説明する常設proseを置く前に、button labelとcolumn headingだけで識別できるようにする。
|
||||
- labelだけでは表現できない補助説明だけを、対象へbindしたTooltipに入れる。
|
||||
- pointer hoverとkeyboard focusの両方で開く。
|
||||
- triggerとTooltipは`aria-describedby`で関連付ける。
|
||||
- hoverは短いdelayを持たせ、focusでは直ちに表示する。
|
||||
- `Escape`で閉じ、triggerからfocusを奪わない。
|
||||
- triggerとTooltipを`aria-describedby`で関連付ける。
|
||||
- hoverはdelay後、focusは直ちに表示する。
|
||||
- `Escape`で閉じ、triggerからfocusを移動しない。
|
||||
- 複数paragraph、form、link、buttonを入れない。
|
||||
- error、permission、validation constraint、不可逆操作の結果、current stateをTooltipだけへ隠さない。
|
||||
- touch環境でも同じ説明へ到達できるようにする。
|
||||
- error、permission、validation constraint、不可逆操作の結果、current stateをTooltipへ入れない。
|
||||
- touch環境ではTooltipだけを説明への到達手段にしない。
|
||||
|
||||
### 汎用component pattern
|
||||
### Generic component patterns
|
||||
|
||||
- **Action**: `primary`、`secondary`、`text`、`destructive`、`disabled`を同じ高さ、padding、focus contractで表現する。
|
||||
- **Form field**: label、control、constraintまたはhelp、field errorの順に配置する。
|
||||
- **Status**: markerと短いtextを一組にする。
|
||||
- **Feedback**: loading、empty、operation error、permission boundaryを別の状態として表現する。
|
||||
- **Resource row**: keyまたはname、判断に必要なmetadata、attentionまたはstate、一つのdestinationを置く。
|
||||
- **Comparable data**: 同じfieldを比較する場合だけtableを使う。
|
||||
- **Key-value**: 一つのresourceの属性には`dl`を使う。
|
||||
- **Disclosure**: secondary metadataやdiagnosticは具体的なlabelを持つ`details`などへ置く。
|
||||
|
||||
Design-labではcomponent名と実際のsampleだけを表示する。規則とrationaleをUIへ書かない。
|
||||
|
||||
@@ -49,9 +49,6 @@
|
||||
--danger: oklch(54% 0.14 25);
|
||||
--interactive-hover: oklch(94.5% 0 0);
|
||||
--interactive-selected: oklch(93% 0.02 230);
|
||||
--bevel-highlight: #fff;
|
||||
--bevel-shadow: #000;
|
||||
--bevel-face-width: 1px;
|
||||
--shadow-overlay: 0 12px 32px oklch(0% 0 0 / 0.14);
|
||||
|
||||
--font-sans: "Gen Interface JP", sans-serif;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</script>
|
||||
|
||||
<div class="sidebar-frame" class:folded>
|
||||
<Bevel as="div" class="sidebar-frame__bevel" depth="inset" top={false} bottom={false} left={false} fill>
|
||||
<Bevel as="div" class="sidebar-frame__bevel" top={false} bottom={false} left={false} fill>
|
||||
<aside class="sidebar-frame__surface" aria-label="Sidebar">
|
||||
{#if !folded}
|
||||
<div class="sidebar-frame-content">
|
||||
|
||||
@@ -1,35 +1,26 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
export type BevelProfile = 'edge' | 'ridge';
|
||||
export type BevelDepth = 'raised' | 'inset';
|
||||
|
||||
type Props = {
|
||||
children: Snippet<[]>;
|
||||
profile?: BevelProfile;
|
||||
depth?: BevelDepth;
|
||||
top?: boolean;
|
||||
right?: boolean;
|
||||
bottom?: boolean;
|
||||
left?: boolean;
|
||||
fill?: boolean;
|
||||
invalid?: boolean;
|
||||
pressed?: boolean;
|
||||
as?: 'span' | 'div';
|
||||
class?: string;
|
||||
};
|
||||
|
||||
let {
|
||||
children,
|
||||
profile = 'edge',
|
||||
depth = 'raised',
|
||||
top = true,
|
||||
right = true,
|
||||
bottom = true,
|
||||
left = true,
|
||||
fill = false,
|
||||
invalid = false,
|
||||
pressed = false,
|
||||
as = 'span',
|
||||
class: className = '',
|
||||
}: Props = $props();
|
||||
@@ -38,35 +29,28 @@
|
||||
<svelte:element
|
||||
this={as}
|
||||
class={`bevel ${fill ? 'bevel--fill' : ''} ${className}`.trim()}
|
||||
data-profile={profile}
|
||||
data-depth={depth}
|
||||
data-top={top ? 'true' : 'false'}
|
||||
data-right={right ? 'true' : 'false'}
|
||||
data-bottom={bottom ? 'true' : 'false'}
|
||||
data-left={left ? 'true' : 'false'}
|
||||
data-invalid={invalid || undefined}
|
||||
data-pressed={pressed || undefined}
|
||||
>
|
||||
{@render children()}
|
||||
</svelte:element>
|
||||
|
||||
<style>
|
||||
.bevel {
|
||||
--bevel-light: 315deg;
|
||||
--bevel-radius: var(--radius-soft);
|
||||
--bevel-top-left-radius: var(--bevel-radius);
|
||||
--bevel-top-right-radius: var(--bevel-radius);
|
||||
--bevel-bottom-right-radius: var(--bevel-radius);
|
||||
--bevel-bottom-left-radius: var(--bevel-radius);
|
||||
--bevel-top-inset: var(--bevel-face-width);
|
||||
--bevel-right-inset: var(--bevel-face-width);
|
||||
--bevel-bottom-inset: var(--bevel-face-width);
|
||||
--bevel-left-inset: var(--bevel-face-width);
|
||||
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
display: inline-grid;
|
||||
min-width: 0;
|
||||
border-style: solid;
|
||||
border-color: var(--line-strong);
|
||||
border-width: 1px;
|
||||
border-radius:
|
||||
var(--bevel-top-left-radius)
|
||||
var(--bevel-top-right-radius)
|
||||
@@ -78,25 +62,29 @@
|
||||
.bevel[data-top='false'] {
|
||||
--bevel-top-left-radius: 0px;
|
||||
--bevel-top-right-radius: 0px;
|
||||
--bevel-top-inset: 0px;
|
||||
|
||||
border-top-width: 0;
|
||||
}
|
||||
|
||||
.bevel[data-right='false'] {
|
||||
--bevel-top-right-radius: 0px;
|
||||
--bevel-bottom-right-radius: 0px;
|
||||
--bevel-right-inset: 0px;
|
||||
|
||||
border-right-width: 0;
|
||||
}
|
||||
|
||||
.bevel[data-bottom='false'] {
|
||||
--bevel-bottom-right-radius: 0px;
|
||||
--bevel-bottom-left-radius: 0px;
|
||||
--bevel-bottom-inset: 0px;
|
||||
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
|
||||
.bevel[data-left='false'] {
|
||||
--bevel-bottom-left-radius: 0px;
|
||||
--bevel-top-left-radius: 0px;
|
||||
--bevel-left-inset: 0px;
|
||||
|
||||
border-left-width: 0;
|
||||
}
|
||||
|
||||
.bevel--fill {
|
||||
@@ -104,8 +92,6 @@
|
||||
}
|
||||
|
||||
.bevel > :global(*) {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
min-width: 0;
|
||||
border-radius: inherit;
|
||||
}
|
||||
@@ -114,235 +100,30 @@
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.bevel::before,
|
||||
.bevel[data-profile='ridge']::after {
|
||||
--_W: var(--bevel-face-width);
|
||||
--_top-w: var(--_W);
|
||||
--_right-w: var(--_W);
|
||||
--_bottom-w: var(--_W);
|
||||
--_left-w: var(--_W);
|
||||
|
||||
content: '';
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
inset: 0;
|
||||
box-sizing: border-box;
|
||||
border-style: solid;
|
||||
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;
|
||||
}
|
||||
|
||||
.bevel[data-top='false']::before,
|
||||
.bevel[data-top='false'][data-profile='ridge']::after {
|
||||
--_top-w: 0px;
|
||||
}
|
||||
|
||||
.bevel[data-right='false']::before,
|
||||
.bevel[data-right='false'][data-profile='ridge']::after {
|
||||
--_right-w: 0px;
|
||||
}
|
||||
|
||||
.bevel[data-bottom='false']::before,
|
||||
.bevel[data-bottom='false'][data-profile='ridge']::after {
|
||||
--_bottom-w: 0px;
|
||||
}
|
||||
|
||||
.bevel[data-left='false']::before,
|
||||
.bevel[data-left='false'][data-profile='ridge']::after {
|
||||
--_left-w: 0px;
|
||||
}
|
||||
|
||||
.bevel[data-profile='edge'][data-depth='inset']::before {
|
||||
--_fallback-border: var(--line);
|
||||
}
|
||||
|
||||
.bevel[data-profile='ridge'][data-depth='inset']::before {
|
||||
--_fallback-border: var(--line);
|
||||
}
|
||||
|
||||
.bevel[data-profile='ridge']::after {
|
||||
inset:
|
||||
var(--bevel-top-inset)
|
||||
var(--bevel-right-inset)
|
||||
var(--bevel-bottom-inset)
|
||||
var(--bevel-left-inset);
|
||||
--_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)))
|
||||
max(0px, calc(var(--bevel-bottom-right-radius) - var(--bevel-face-width)))
|
||||
max(0px, calc(var(--bevel-bottom-left-radius) - var(--bevel-face-width)));
|
||||
}
|
||||
|
||||
.bevel[data-profile='ridge'][data-depth='inset']::after {
|
||||
--_fallback-border: var(--line-strong);
|
||||
}
|
||||
|
||||
.bevel[data-invalid='true'],
|
||||
.bevel:has(> :global([aria-invalid='true'])) {
|
||||
box-shadow: 0 0 0 1px var(--danger);
|
||||
border-color: var(--danger);
|
||||
}
|
||||
|
||||
.bevel:has(> :global(:is(:disabled, [aria-disabled='true']))) {
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
@supports
|
||||
(color: color-mix(in oklab, white 50%, black)) and
|
||||
(width: calc(cos(0deg) * 1px)) and
|
||||
((mask-composite: exclude) or (-webkit-mask-composite: xor)) {
|
||||
.bevel {
|
||||
--_s0: var(--bevel-highlight);
|
||||
--_s1: color-mix(in oklab, var(--bevel-highlight) 96.5926%, var(--bevel-shadow));
|
||||
--_s2: color-mix(in oklab, var(--bevel-highlight) 86.6025%, var(--bevel-shadow));
|
||||
--_s3: color-mix(in oklab, var(--bevel-highlight) 70.7107%, var(--bevel-shadow));
|
||||
--_s4: color-mix(in oklab, var(--bevel-highlight) 50%, var(--bevel-shadow));
|
||||
--_s5: color-mix(in oklab, var(--bevel-highlight) 25.8819%, var(--bevel-shadow));
|
||||
--_s6: var(--bevel-shadow);
|
||||
--_shade:
|
||||
var(--_s0) 0deg,
|
||||
var(--_s1) 15deg,
|
||||
var(--_s2) 30deg,
|
||||
var(--_s3) 45deg,
|
||||
var(--_s4) 60deg,
|
||||
var(--_s5) 75deg,
|
||||
var(--_s6) 90deg,
|
||||
var(--_s6) 270deg,
|
||||
var(--_s5) 285deg,
|
||||
var(--_s4) 300deg,
|
||||
var(--_s3) 315deg,
|
||||
var(--_s2) 330deg,
|
||||
var(--_s1) 345deg,
|
||||
var(--_s0) 360deg;
|
||||
}
|
||||
|
||||
.bevel::before,
|
||||
.bevel[data-profile='ridge']::after {
|
||||
--_W: var(--bevel-face-width);
|
||||
--_RTL: var(--bevel-top-left-radius);
|
||||
--_RTR: var(--bevel-top-right-radius);
|
||||
--_RBR: var(--bevel-bottom-right-radius);
|
||||
--_RBL: var(--bevel-bottom-left-radius);
|
||||
--_L: var(--bevel-light);
|
||||
--_top: color-mix(
|
||||
in oklab,
|
||||
var(--bevel-highlight) calc(max(cos(0deg - var(--_L)), 0) * 100%),
|
||||
var(--bevel-shadow)
|
||||
);
|
||||
--_right: color-mix(
|
||||
in oklab,
|
||||
var(--bevel-highlight) calc(max(cos(90deg - var(--_L)), 0) * 100%),
|
||||
var(--bevel-shadow)
|
||||
);
|
||||
--_bottom: color-mix(
|
||||
in oklab,
|
||||
var(--bevel-highlight) calc(max(cos(180deg - var(--_L)), 0) * 100%),
|
||||
var(--bevel-shadow)
|
||||
);
|
||||
--_left: color-mix(
|
||||
in oklab,
|
||||
var(--bevel-highlight) calc(max(cos(270deg - var(--_L)), 0) * 100%),
|
||||
var(--bevel-shadow)
|
||||
);
|
||||
|
||||
border-style: solid;
|
||||
border-color: transparent;
|
||||
border-width: var(--_top-w) var(--_right-w) var(--_bottom-w) var(--_left-w);
|
||||
border-radius: var(--_RTL) var(--_RTR) var(--_RBR) var(--_RBL);
|
||||
background:
|
||||
conic-gradient(from var(--_L) at 100% 100%, var(--_shade)) 0 0 / var(--_RTL) var(--_RTL) no-repeat,
|
||||
conic-gradient(from var(--_L) at 0 100%, var(--_shade)) 100% 0 / var(--_RTR) var(--_RTR) no-repeat,
|
||||
conic-gradient(from var(--_L) at 0 0, var(--_shade)) 100% 100% / var(--_RBR) var(--_RBR) no-repeat,
|
||||
conic-gradient(from var(--_L) at 100% 0, var(--_shade)) 0 100% / var(--_RBL) var(--_RBL) no-repeat,
|
||||
linear-gradient(var(--_top) 0 0) var(--_RTL) 0 /
|
||||
calc(100% - var(--_RTL) - var(--_RTR)) var(--_top-w) no-repeat,
|
||||
linear-gradient(var(--_bottom) 0 0) var(--_RBL) 100% /
|
||||
calc(100% - var(--_RBL) - var(--_RBR)) var(--_bottom-w) no-repeat,
|
||||
linear-gradient(var(--_left) 0 0) 0 var(--_RTL) / var(--_left-w)
|
||||
calc(100% - var(--_RTL) - var(--_RBL)) no-repeat,
|
||||
linear-gradient(var(--_right) 0 0) 100% var(--_RTR) / var(--_right-w)
|
||||
calc(100% - var(--_RTR) - var(--_RBR)) no-repeat;
|
||||
background-origin: border-box;
|
||||
background-clip: border-box;
|
||||
box-shadow: none;
|
||||
-webkit-mask:
|
||||
linear-gradient(#000 0 0) padding-box,
|
||||
linear-gradient(#000 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask:
|
||||
linear-gradient(#000 0 0) padding-box,
|
||||
linear-gradient(#000 0 0);
|
||||
mask-composite: exclude;
|
||||
}
|
||||
|
||||
.bevel[data-depth='inset']::before,
|
||||
.bevel[data-profile='edge'][data-pressed='true']::before,
|
||||
.bevel[data-profile='edge'][data-depth='raised']:has(
|
||||
> :global(:is(button, [role='button']):active)
|
||||
)::before,
|
||||
.bevel[data-profile='edge'][data-depth='raised']:has(
|
||||
> :global([aria-pressed='true'])
|
||||
)::before {
|
||||
--_L: calc(var(--bevel-light) + 180deg);
|
||||
}
|
||||
|
||||
.bevel[data-profile='ridge']::before {
|
||||
--_W: var(--bevel-face-width);
|
||||
}
|
||||
|
||||
.bevel[data-profile='ridge']::after {
|
||||
--_W: var(--bevel-face-width);
|
||||
--_RTL: max(0px, calc(var(--bevel-top-left-radius) - var(--bevel-face-width)));
|
||||
--_RTR: max(0px, calc(var(--bevel-top-right-radius) - var(--bevel-face-width)));
|
||||
--_RBR: max(0px, calc(var(--bevel-bottom-right-radius) - var(--bevel-face-width)));
|
||||
--_RBL: max(0px, calc(var(--bevel-bottom-left-radius) - var(--bevel-face-width)));
|
||||
inset:
|
||||
var(--bevel-top-inset)
|
||||
var(--bevel-right-inset)
|
||||
var(--bevel-bottom-inset)
|
||||
var(--bevel-left-inset);
|
||||
}
|
||||
|
||||
.bevel[data-profile='ridge'][data-depth='raised']::after {
|
||||
--_L: calc(var(--bevel-light) + 180deg);
|
||||
}
|
||||
|
||||
.bevel[data-profile='ridge'][data-depth='inset']::after {
|
||||
--_L: var(--bevel-light);
|
||||
}
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
.bevel {
|
||||
forced-color-adjust: auto;
|
||||
}
|
||||
|
||||
.bevel::before,
|
||||
.bevel[data-profile='ridge']::after {
|
||||
border-color: ButtonText;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
-webkit-mask: none;
|
||||
mask: none;
|
||||
forced-color-adjust: auto;
|
||||
}
|
||||
|
||||
.bevel[data-invalid='true'],
|
||||
.bevel:has(> :global([aria-invalid='true'])) {
|
||||
outline: 2px solid Mark;
|
||||
outline-offset: 1px;
|
||||
box-shadow: none;
|
||||
border-color: Mark;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.bevel::before,
|
||||
.bevel[data-profile='ridge']::after {
|
||||
.bevel {
|
||||
border-color: currentColor;
|
||||
background: none;
|
||||
-webkit-mask: none;
|
||||
mask: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<script lang="ts">
|
||||
export type BevelLineDirection = 'x' | 'y';
|
||||
export type BevelLineDepth = 'raised' | 'inset';
|
||||
|
||||
type Props = {
|
||||
direction?: BevelLineDirection;
|
||||
length?: string;
|
||||
depth?: BevelLineDepth;
|
||||
decorative?: boolean;
|
||||
as?: 'span' | 'div';
|
||||
class?: string;
|
||||
@@ -14,7 +12,6 @@
|
||||
let {
|
||||
direction = 'x',
|
||||
length = '100%',
|
||||
depth = 'raised',
|
||||
decorative = false,
|
||||
as = 'span',
|
||||
class: className = '',
|
||||
@@ -25,7 +22,6 @@
|
||||
this={as}
|
||||
class={`bevel-line ${className}`.trim()}
|
||||
data-direction={direction}
|
||||
data-depth={depth}
|
||||
role={decorative ? undefined : 'separator'}
|
||||
aria-orientation={decorative ? undefined : direction === 'x' ? 'horizontal' : 'vertical'}
|
||||
aria-hidden={decorative ? 'true' : undefined}
|
||||
@@ -35,112 +31,34 @@
|
||||
|
||||
<style>
|
||||
.bevel-line {
|
||||
--bevel-light: 315deg;
|
||||
--bevel-line-face-width: var(--bevel-face-width);
|
||||
--bevel-line-width: calc(var(--bevel-line-face-width) * 2);
|
||||
|
||||
display: block;
|
||||
flex: 0 0 auto;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background: var(--line-strong);
|
||||
border-color: var(--line-strong);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.bevel-line[data-direction='x'] {
|
||||
height: var(--bevel-line-width);
|
||||
height: 0;
|
||||
border-top-style: solid;
|
||||
border-top-width: 1px;
|
||||
}
|
||||
|
||||
.bevel-line[data-direction='y'] {
|
||||
width: var(--bevel-line-width);
|
||||
}
|
||||
|
||||
@supports
|
||||
(color: color-mix(in oklab, white 50%, black)) and
|
||||
(width: calc(cos(0deg) * 1px)) {
|
||||
.bevel-line {
|
||||
--_L: var(--bevel-light);
|
||||
--_s0: var(--bevel-highlight);
|
||||
--_s1: color-mix(in oklab, var(--bevel-highlight) 96.5926%, var(--bevel-shadow));
|
||||
--_s2: color-mix(in oklab, var(--bevel-highlight) 86.6025%, var(--bevel-shadow));
|
||||
--_s3: color-mix(in oklab, var(--bevel-highlight) 70.7107%, var(--bevel-shadow));
|
||||
--_s4: color-mix(in oklab, var(--bevel-highlight) 50%, var(--bevel-shadow));
|
||||
--_s5: color-mix(in oklab, var(--bevel-highlight) 25.8819%, var(--bevel-shadow));
|
||||
--_s6: var(--bevel-shadow);
|
||||
--_shade:
|
||||
var(--_s0) 0deg,
|
||||
var(--_s1) 15deg,
|
||||
var(--_s2) 30deg,
|
||||
var(--_s3) 45deg,
|
||||
var(--_s4) 60deg,
|
||||
var(--_s5) 75deg,
|
||||
var(--_s6) 90deg,
|
||||
var(--_s6) 270deg,
|
||||
var(--_s5) 285deg,
|
||||
var(--_s4) 300deg,
|
||||
var(--_s3) 315deg,
|
||||
var(--_s2) 330deg,
|
||||
var(--_s1) 345deg,
|
||||
var(--_s0) 360deg;
|
||||
--_top: color-mix(
|
||||
in oklab,
|
||||
var(--bevel-highlight) calc(max(cos(0deg - var(--_L)), 0) * 100%),
|
||||
var(--bevel-shadow)
|
||||
);
|
||||
--_right: color-mix(
|
||||
in oklab,
|
||||
var(--bevel-highlight) calc(max(cos(90deg - var(--_L)), 0) * 100%),
|
||||
var(--bevel-shadow)
|
||||
);
|
||||
--_bottom: color-mix(
|
||||
in oklab,
|
||||
var(--bevel-highlight) calc(max(cos(180deg - var(--_L)), 0) * 100%),
|
||||
var(--bevel-shadow)
|
||||
);
|
||||
--_left: color-mix(
|
||||
in oklab,
|
||||
var(--bevel-highlight) calc(max(cos(270deg - var(--_L)), 0) * 100%),
|
||||
var(--bevel-shadow)
|
||||
);
|
||||
}
|
||||
|
||||
.bevel-line[data-depth='inset'] {
|
||||
--_L: calc(var(--bevel-light) + 180deg);
|
||||
}
|
||||
|
||||
.bevel-line[data-direction='x'] {
|
||||
background:
|
||||
conic-gradient(from var(--_L) at 100% 50%, var(--_shade)) 0 0 /
|
||||
var(--bevel-line-face-width) var(--bevel-line-width) no-repeat,
|
||||
conic-gradient(from var(--_L) at 0 50%, var(--_shade)) 100% 0 /
|
||||
var(--bevel-line-face-width) var(--bevel-line-width) no-repeat,
|
||||
linear-gradient(to bottom, var(--_top) 0 50%, var(--_bottom) 50% 100%)
|
||||
var(--bevel-line-face-width) 0 /
|
||||
calc(100% - var(--bevel-line-width)) 100% no-repeat;
|
||||
}
|
||||
|
||||
.bevel-line[data-direction='y'] {
|
||||
background:
|
||||
conic-gradient(from var(--_L) at 50% 100%, var(--_shade)) 0 0 /
|
||||
var(--bevel-line-width) var(--bevel-line-face-width) no-repeat,
|
||||
conic-gradient(from var(--_L) at 50% 0, var(--_shade)) 0 100% /
|
||||
var(--bevel-line-width) var(--bevel-line-face-width) no-repeat,
|
||||
linear-gradient(to right, var(--_left) 0 50%, var(--_right) 50% 100%)
|
||||
0 var(--bevel-line-face-width) / 100%
|
||||
calc(100% - var(--bevel-line-width)) no-repeat;
|
||||
}
|
||||
width: 0;
|
||||
border-left-style: solid;
|
||||
border-left-width: 1px;
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
.bevel-line {
|
||||
background: CanvasText;
|
||||
border-color: CanvasText;
|
||||
forced-color-adjust: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.bevel-line {
|
||||
background: currentColor;
|
||||
border-color: currentColor;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
workspaceError={data.workspaceCatalogError}
|
||||
/>
|
||||
</SidebarFrame>
|
||||
<Bevel as="div" class="app-shell__topbar-bevel" depth="inset" top={false} right={false} left={false} fill>
|
||||
<Bevel as="div" class="app-shell__topbar-bevel" top={false} right={false} left={false} fill>
|
||||
<header class="app-shell__topbar">
|
||||
<div class="app-shell__topbar-location">
|
||||
{#if headerController.content}{@render headerController.content()}{/if}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<h1 id="action-heading" class="showroom-heading">Action</h1>
|
||||
<BevelLine direction="x" length="100%" decorative />
|
||||
<div class="action-row">
|
||||
<Bevel profile="edge" depth="raised">
|
||||
<Bevel>
|
||||
<button class="action action--primary" type="button">Create Worker</button>
|
||||
</Bevel>
|
||||
<Tooltip id="rerun-help" text="Runs validation against the current Workspace configuration." placement="bottom" align="start">
|
||||
@@ -28,22 +28,22 @@
|
||||
</Bevel>
|
||||
</div>
|
||||
|
||||
<div class="ridge-matrix">
|
||||
<Bevel as="div" profile="ridge" depth="raised" fill>
|
||||
<div class="tactile-group" role="group" aria-label="Raised validation controls">
|
||||
<div class="border-matrix">
|
||||
<Bevel as="div" fill>
|
||||
<div class="tactile-group" role="group" aria-label="Bordered validation controls">
|
||||
<span>
|
||||
<strong>Raised ridge</strong>
|
||||
<strong>Closed border</strong>
|
||||
<small>3 checks passed at 14:32</small>
|
||||
</span>
|
||||
<Bevel pressed>
|
||||
<Bevel>
|
||||
<button class="compact-control" type="button" aria-pressed="true">Live</button>
|
||||
</Bevel>
|
||||
</div>
|
||||
</Bevel>
|
||||
<Bevel as="div" profile="ridge" depth="inset" fill>
|
||||
<div class="tactile-group" role="group" aria-label="Inset ridge controls">
|
||||
<Bevel as="div" fill>
|
||||
<div class="tactile-group" role="group" aria-label="Read-only bordered controls">
|
||||
<span>
|
||||
<strong>Inset ridge</strong>
|
||||
<strong>Read-only border</strong>
|
||||
<small>Read-only control group</small>
|
||||
</span>
|
||||
<span class="status"><span class="status__marker" data-tone="muted"></span>Locked</span>
|
||||
@@ -57,7 +57,7 @@
|
||||
</span>
|
||||
</div>
|
||||
</Bevel>
|
||||
<Bevel as="div" profile="ridge" depth="inset" top={false} left={false} fill>
|
||||
<Bevel as="div" top={false} left={false} fill>
|
||||
<div class="tactile-group" role="group" aria-label="Right and bottom groove">
|
||||
<span>
|
||||
<strong>Right + bottom</strong>
|
||||
@@ -73,10 +73,10 @@
|
||||
<BevelLine direction="x" length="100%" decorative />
|
||||
<div class="line-showcase">
|
||||
<div class="line-sample line-sample--horizontal">
|
||||
<span class="line-sample__label">Raised · x · 100%</span>
|
||||
<span class="line-sample__label">Horizontal · 100%</span>
|
||||
<BevelLine direction="x" length="100%" />
|
||||
<span class="line-sample__label">Inset · x · 160px</span>
|
||||
<BevelLine direction="x" length="160px" depth="inset" />
|
||||
<span class="line-sample__label">Horizontal · 160px</span>
|
||||
<BevelLine direction="x" length="160px" />
|
||||
</div>
|
||||
<div class="line-sample line-sample--vertical">
|
||||
<span>Before</span>
|
||||
|
||||
@@ -16,14 +16,14 @@
|
||||
<div class="form-grid">
|
||||
<label class="form-field">
|
||||
<span>Runtime label</span>
|
||||
<Bevel profile="edge" depth="inset" fill>
|
||||
<Bevel fill>
|
||||
<input name="runtime-label" value="Remote Linux runner" />
|
||||
</Bevel>
|
||||
</label>
|
||||
|
||||
<label class="form-field">
|
||||
<span>Endpoint</span>
|
||||
<Bevel profile="edge" depth="inset" fill invalid>
|
||||
<Bevel fill invalid>
|
||||
<input
|
||||
name="runtime-endpoint"
|
||||
value="http://runner.internal.example:8787/api/runtime"
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
<label class="form-field">
|
||||
<span>Architecture</span>
|
||||
<Bevel profile="edge" depth="inset" fill>
|
||||
<Bevel fill>
|
||||
<select name="architecture">
|
||||
<option>arm64</option>
|
||||
<option selected>x86_64</option>
|
||||
|
||||
@@ -153,7 +153,7 @@
|
||||
font: 600 12px/1 var(--font-mono);
|
||||
}
|
||||
|
||||
.ridge-matrix {
|
||||
.border-matrix {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--space-4);
|
||||
@@ -637,7 +637,7 @@
|
||||
|
||||
.showroom-split,
|
||||
.line-showcase,
|
||||
.ridge-matrix {
|
||||
.border-matrix {
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--space-6);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ function assert(condition: unknown, message: string): asserts condition {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
|
||||
Deno.test("Bevel keeps depth visual while semantic controls remain children", async () => {
|
||||
Deno.test("Bevel draws a plain selectable border around semantic children", async () => {
|
||||
const source = await Deno.readTextFile(
|
||||
new URL("../src/lib/workspace/ui/Bevel.svelte", import.meta.url),
|
||||
);
|
||||
@@ -19,97 +19,71 @@ Deno.test("Bevel keeps depth visual while semantic controls remain children", as
|
||||
|
||||
for (
|
||||
const contract of [
|
||||
"export type BevelProfile = 'edge' | 'ridge'",
|
||||
"export type BevelDepth = 'raised' | 'inset'",
|
||||
"children: Snippet<[]>",
|
||||
"top?: boolean",
|
||||
"right?: boolean",
|
||||
"bottom?: boolean",
|
||||
"left?: boolean",
|
||||
"pointer-events: none",
|
||||
"data-pressed={pressed || undefined}",
|
||||
"data-invalid={invalid || undefined}",
|
||||
"border-style: solid",
|
||||
"border-color: var(--line-strong)",
|
||||
"border-width: 1px",
|
||||
"aria-invalid='true'",
|
||||
"forced-colors: active",
|
||||
"mask-composite: exclude",
|
||||
]
|
||||
) {
|
||||
assert(source.includes(contract), `Bevel must preserve ${contract}`);
|
||||
}
|
||||
|
||||
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 fallback colors must not outrank the enhanced transparent border",
|
||||
);
|
||||
assert(
|
||||
source.includes("calc(max(cos(180deg - var(--_L)), 0) * 100%)") &&
|
||||
source.includes("var(--_s6) 90deg") &&
|
||||
source.includes("var(--_s6) 270deg") &&
|
||||
!source.includes("+ 1) * 50%"),
|
||||
"Bevel must clamp unlit faces to the configured shadow without ambient light",
|
||||
);
|
||||
assert(
|
||||
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 1px edge and compose ridge from two 1px faces",
|
||||
);
|
||||
assert(
|
||||
source.includes("data-top={top ? 'true' : 'false'}") &&
|
||||
source.includes("data-right={right ? 'true' : 'false'}") &&
|
||||
source.includes("data-bottom={bottom ? 'true' : 'false'}") &&
|
||||
source.includes("data-left={left ? 'true' : 'false'}") &&
|
||||
source.includes(".bevel[data-top='false'] {") &&
|
||||
source.includes("border-top-width: 0") &&
|
||||
source.includes("border-right-width: 0") &&
|
||||
source.includes("border-bottom-width: 0") &&
|
||||
source.includes("border-left-width: 0") &&
|
||||
source.includes("--bevel-top-left-radius: 0px") &&
|
||||
source.includes("--bevel-top-right-radius: 0px") &&
|
||||
source.includes(".bevel[data-right='false'] {") &&
|
||||
source.includes("--bevel-bottom-right-radius: 0px") &&
|
||||
source.includes(".bevel[data-bottom='false'] {") &&
|
||||
source.includes("--bevel-bottom-left-radius: 0px") &&
|
||||
source.includes(".bevel[data-left='false'] {") &&
|
||||
source.includes("--bevel-top-inset: 0px") &&
|
||||
source.includes("--bevel-right-inset: 0px") &&
|
||||
source.includes("--bevel-bottom-inset: 0px") &&
|
||||
source.includes("--bevel-left-inset: 0px"),
|
||||
"Bevel must draw selected sides and round only corners shared by two enabled sides",
|
||||
source.includes("--bevel-bottom-right-radius: 0px"),
|
||||
"Bevel must draw only selected sides and round only corners shared by enabled sides",
|
||||
);
|
||||
assert(
|
||||
source.includes("data-profile={profile}") &&
|
||||
source.includes("[data-profile='ridge'][data-depth='raised']::after") &&
|
||||
source.includes("[data-profile='ridge'][data-depth='inset']::after") &&
|
||||
!source.includes("data-depth='ridge'"),
|
||||
"Bevel must keep edge/ridge profile orthogonal to raised/inset depth",
|
||||
!source.includes("BevelProfile") &&
|
||||
!source.includes("BevelDepth") &&
|
||||
!source.includes("profile") &&
|
||||
!source.includes("depth") &&
|
||||
!source.includes("pressed") &&
|
||||
!source.includes("::before") &&
|
||||
!source.includes("::after") &&
|
||||
!source.includes("gradient") &&
|
||||
!source.includes("mask") &&
|
||||
!source.includes("color-mix") &&
|
||||
!source.includes("cos(") &&
|
||||
!source.includes("pow(") &&
|
||||
!appCss.includes("--bevel-highlight") &&
|
||||
!appCss.includes("--bevel-shadow") &&
|
||||
!appCss.includes("--bevel-highlight-curve") &&
|
||||
!appCss.includes("--bevel-face-width"),
|
||||
"Bevel must remain a plain border without lighting or depth simulation",
|
||||
);
|
||||
assert(
|
||||
appCss.includes("--bevel-highlight: #fff") &&
|
||||
appCss.includes("--bevel-shadow: #000") &&
|
||||
!source.includes("surface") &&
|
||||
!source.includes("tone") &&
|
||||
!source.includes("background-color:"),
|
||||
"Bevel must use the shared highlight and shadow palette without owning a surface tone",
|
||||
!source.includes("background:") &&
|
||||
!source.includes("background-color:") &&
|
||||
source.includes("button, input, select, textarea"),
|
||||
"Bevel must not own surface color and must keep semantic controls as children",
|
||||
);
|
||||
assert(
|
||||
designLanguage.includes("Bevel自体へbackgroundやsurface toneを与えず") &&
|
||||
designLanguage.includes("1pxの単色borderを描くvisual wrapper") &&
|
||||
designLanguage.includes(
|
||||
"照明、raised/inset、ridge/grooveを表現しない",
|
||||
) &&
|
||||
designLanguage.includes("text/content areaへ適用する"),
|
||||
"The design language must prohibit Bevel surface colors outside the child content area",
|
||||
);
|
||||
assert(
|
||||
source.includes("button, input, select, textarea"),
|
||||
"Bevel must leave native semantic controls inside the visual wrapper",
|
||||
"The design language must define Bevel as a plain border without surface ownership",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("Workspace showroom exercises raised inset and ridge edges", async () => {
|
||||
Deno.test("Workspace showroom exercises closed and adjacent borders", async () => {
|
||||
const workspaceSource = await Deno.readTextFile(
|
||||
new URL(
|
||||
"../src/routes/design-lab/workspace-web-ux/+page.svelte",
|
||||
@@ -133,21 +107,26 @@ Deno.test("Workspace showroom exercises raised inset and ridge edges", async ()
|
||||
)?.[1];
|
||||
|
||||
assert(
|
||||
workspaceSource.includes('profile="edge" depth="raised"') &&
|
||||
workspaceSource.includes('profile="ridge" depth="raised"') &&
|
||||
workspaceSource.includes('profile="ridge" depth="inset"') &&
|
||||
workspaceSource.includes('<div class="border-matrix">') &&
|
||||
workspaceSource.includes("right={false} bottom={false}") &&
|
||||
workspaceSource.includes("top={false} left={false}") &&
|
||||
workspaceSource.includes("pressed"),
|
||||
"Workspace showroom must demonstrate edge and ridge depths with full and adjacent side selections",
|
||||
workspaceSource.includes("Closed border") &&
|
||||
workspaceSource.includes("Read-only border") &&
|
||||
!workspaceSource.includes("profile=") &&
|
||||
!workspaceSource.includes("depth=") &&
|
||||
!workspaceSource.includes("<Bevel pressed"),
|
||||
"Workspace showroom must demonstrate closed and adjacent plain borders",
|
||||
);
|
||||
assert(
|
||||
tactileGroupRule && !tactileGroupRule.includes("background"),
|
||||
"Showroom ridge items must leave their content area background unset",
|
||||
"Showroom bordered items must leave their content area background unset",
|
||||
);
|
||||
assert(
|
||||
settingsSource.includes('profile="edge" depth="inset"') &&
|
||||
settingsSource.includes('aria-invalid="true"'),
|
||||
"Settings showroom must demonstrate inset and invalid fields",
|
||||
settingsSource.includes("<Bevel fill>") &&
|
||||
settingsSource.includes("<Bevel fill invalid>") &&
|
||||
settingsSource.includes('aria-invalid="true"') &&
|
||||
!settingsSource.includes("profile=") &&
|
||||
!settingsSource.includes("depth="),
|
||||
"Settings showroom must demonstrate plain and invalid borders",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -7,9 +7,6 @@ Deno.test("BevelLine exposes a semantic directional separator", async () => {
|
||||
const source = await Deno.readTextFile(
|
||||
new URL("../src/lib/workspace/ui/BevelLine.svelte", import.meta.url),
|
||||
);
|
||||
const appCss = await Deno.readTextFile(
|
||||
new URL("../src/app.css", import.meta.url),
|
||||
);
|
||||
|
||||
for (
|
||||
const contract of [
|
||||
@@ -28,41 +25,25 @@ Deno.test("BevelLine exposes a semantic directional separator", async () => {
|
||||
}
|
||||
|
||||
assert(
|
||||
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 2px ridge from two project-wide 1px faces",
|
||||
source.includes("border-color: var(--line-strong)") &&
|
||||
source.includes("border-top-style: solid") &&
|
||||
source.includes("border-top-width: 1px") &&
|
||||
source.includes("border-left-style: solid") &&
|
||||
source.includes("border-left-width: 1px") &&
|
||||
!source.includes("background:") &&
|
||||
!source.includes("gradient") &&
|
||||
!source.includes("color-mix") &&
|
||||
!source.includes("BevelLineDepth") &&
|
||||
!source.includes("depth"),
|
||||
"BevelLine must remain a plain one-pixel directional border",
|
||||
);
|
||||
assert(
|
||||
!source.includes("--bevel-line-inset") &&
|
||||
!source.includes("padding-inline") &&
|
||||
!source.includes("padding-block"),
|
||||
!source.includes("padding-inline") &&
|
||||
!source.includes("padding-block") &&
|
||||
!source.includes("margin-inline") &&
|
||||
!source.includes("margin-block"),
|
||||
"BevelLine must fill its parent layer without independent endpoint spacing",
|
||||
);
|
||||
assert(
|
||||
source.includes("background: var(--line-strong)") &&
|
||||
source.includes("@supports") &&
|
||||
source.includes("linear-gradient(to bottom") &&
|
||||
source.includes("linear-gradient(to right"),
|
||||
"BevelLine must retain a plain fallback and directional enhanced edges",
|
||||
);
|
||||
assert(
|
||||
source.includes("var(--bevel-highlight)") &&
|
||||
source.includes("var(--bevel-shadow)") &&
|
||||
!source.includes("surface"),
|
||||
"BevelLine must use the shared highlight and shadow palette without a surface color",
|
||||
);
|
||||
assert(
|
||||
source.includes("calc(max(cos(180deg - var(--_L)), 0) * 100%)") &&
|
||||
source.includes("var(--_s6) 90deg") &&
|
||||
source.includes("var(--_s6) 270deg") &&
|
||||
!source.includes("+ 1) * 50%"),
|
||||
"BevelLine must clamp unlit faces to the configured shadow without ambient light",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("Workspace showroom demonstrates both BevelLine directions", async () => {
|
||||
@@ -76,13 +57,14 @@ Deno.test("Workspace showroom demonstrates both BevelLine directions", async ()
|
||||
assert(
|
||||
source.includes('direction="x" length="100%"') &&
|
||||
source.includes('direction="y" length="72px"') &&
|
||||
source.includes('direction="x" length="160px" depth="inset"') &&
|
||||
source.includes('direction="x" length="160px"') &&
|
||||
!source.includes("depth=") &&
|
||||
!source.includes("weight="),
|
||||
"Workspace showroom must demonstrate horizontal, vertical, and inset BevelLine states",
|
||||
"Workspace showroom must demonstrate horizontal and vertical plain separators",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("Shell regions use single-edge Bevel while open separators use BevelLine", async () => {
|
||||
Deno.test("Shell regions use selected Bevel edges while open separators use BevelLine", async () => {
|
||||
const rootLayout = await Deno.readTextFile(
|
||||
new URL("../src/routes/+layout.svelte", import.meta.url),
|
||||
);
|
||||
@@ -110,21 +92,21 @@ Deno.test("Shell regions use single-edge Bevel while open separators use BevelLi
|
||||
|
||||
assert(
|
||||
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("depth=") &&
|
||||
!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("depth=") &&
|
||||
!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",
|
||||
"Desktop shell boundaries must use one selected border edge and Mobile must share Header fold state",
|
||||
);
|
||||
assert(
|
||||
designLanguage.includes(
|
||||
@@ -138,13 +120,13 @@ Deno.test("Shell regions use single-edge Bevel while open separators use BevelLi
|
||||
) &&
|
||||
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",
|
||||
"The design language and showroom must preserve adjacent area and line hierarchy",
|
||||
);
|
||||
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 boundaries must not fall back to one-sided solid borders",
|
||||
"Shell and showroom structural boundaries must use shared Bevel components",
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user